Android笔记(二十三):以插件化APK方式启动带WebView的Activity
admin
2024-02-21 20:51:03
0次

背景

本文记录插件化学习过程中,如何以插件化apk方式启动带WebView的Activity,分三大步骤完成。

步骤

解决插件Activity启动问题

  • 目前有以下三种实现Activity插件化的方案:
    • 通过反射实现,会影响性能。

    • 通过接口实现,dynamic-load-apk的实现原理。

    • 通过Hook技术实现,本文采用此方式实现。

      • 一般Hook技术有两种Hook点选择:
        1. Hook IActivityManager
        2. Hook Instrumentation,本文采用此选择
  1. 在Application onCreate调用以下代码,反射设置ProxyInstrumentation hook系统的Instrumentation
public class HookUtils {public static void executeInstrumentationHook(Context context){try {Class activityThreadClass=Class.forName("android.app.ActivityThread");Field activityThreadField=activityThreadClass.getDeclaredField("sCurrentActivityThread");activityThreadField.setAccessible(true);//获取ActivityThread对象sCurrentActivityThreadObject activityThread=activityThreadField.get(null);Field instrumentationField=activityThreadClass.getDeclaredField("mInstrumentation");instrumentationField.setAccessible(true);//从sCurrentActivityThread中获取成员变量mInstrumentationInstrumentation instrumentation= (Instrumentation) instrumentationField.get(activityThread);//创建代理对象InstrumentationProxyProxyInstrumentation proxy = new ProxyInstrumentation(instrumentation, context.getPackageManager());//将sCurrentActivityThread中成员变量mInstrumentation替换成代理类InstrumentationProxyinstrumentationField.set(activityThread,proxy);} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}
}
  1. ProxyInstrumentation要做的两件事:先用占坑的Activity去骗AMS;启动运行时,在将目标Activity替换掉占坑的Activity的。
public class ProxyInstrumentation extends Instrumentation {private final Instrumentation instrumentation;private final PackageManager mPackageManager;public ProxyInstrumentation(Instrumentation instrumentation, PackageManager mPackageManager){this.instrumentation = instrumentation;this.mPackageManager = mPackageManager;}public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options){List infos = mPackageManager.queryIntentActivities(intent, 0);if (infos == null || infos.size() == 0) {//Hook成功,执行了startActivityintent.putExtra("TARGET_INTENT_NAME",intent.getComponent().getClassName());// 将要送去AMS验证的Activity换成占坑的intent.setClassName(who,"ProxyActivity完整路径");}try {Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);} catch (Exception e) {e.printStackTrace();}return null;}@Overridepublic Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {try {//这里需要setExtrasClassLoader ,否则Parecleable对象可能加载不了抛异常//try catch 是防止恶意攻击导致android.os.BadParcelableException: ClassNotFoundException when unmarshallingintent.setExtrasClassLoader(cl);}catch (Exception e){e.printStackTrace();}String intentName = intent.getStringExtra("TARGET_INTENT_NAME");if (!StringUtil.isBlank(intentName)){return instrumentation.newActivity(PluginLoader.getPluginClassLoader(), intentName, intent);}else {return super.newActivity(cl, className, intent);}}
}
  1. 在Application onCreate调用PluginLoader.initClassLoader,使用DexClassLoader加载插件apk,localPluginsPath传入apk存放路径
public class PluginLoader {private static String LOCAL_PLUGINS_PATH;private static DexClassLoader pluginClassLoader;public static DexClassLoader getPluginClassLoader() {return pluginClassLoader;}public static void initClassLoader(Context context, String localPluginsPath){innerInit(context, localPluginsPath);}private static void innerInit(Context context, String localPluginsPath) {String filesDir = context.getFilesDir().getAbsolutePath() + File.separator + "plugins";String dexOutPath = filesDir + File.separator +  "dexout";// 生成 DexClassLoader 用来加载插件类pluginClassLoader = new DexClassLoader(localPluginsPath, dexOutPath, null, context.getClassLoader());}
}
  • 原理总结:在启动Activity,AMS校验前,系统会调用Instrumentation的execStartActivity方法,通过拦截该方法,判断要启动的Activity是否是未注册的,是的话就将要启动的Activity完整类名暂存到intent,替换成占坑ProxyActivity的完整类名。通过AMS校验后,系统接着会调用newActivity,开始创建Activity实例,这时候再从Intent取出之前保存的真实Activity完整类名传入className,ClassLoader传入PluginLoader.getPluginClassLoader()。

解决插件apk资源加载问题

在打开的插件apk的activity onCreate()方法中,调用如下方法将插件apk资源设置到打开的activity mResource字段中:

public class ResHookUtil {public static void loadExtraRes(Activity activity, @Nullable String @NotNull extraPath) {try {//创建一个新的AssetManagerAssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();//获取addAssetPath方法Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);mAddAssetPath.setAccessible(true);//调用addAssetPath方法加载外部插件apk资源)if (((Integer) mAddAssetPath.invoke(newAssetManager, extraPath)) == 0) {throw new IllegalStateException("Could not create new AssetManager");}Resources pluginResource =  new Resources(newAssetManager, activity.getApplication().getResources().getDisplayMetrics(), activity.getApplication().getResources().getConfiguration());// 反射ContextThemeWrapper类 , Activity是ContextThemeWrapper的子类// Resources mResources成员定义在ContextThemeWrapper中Class contextThemeWrapperClass = null;try {contextThemeWrapperClass = Class.forName("android.view.ContextThemeWrapper");} catch (ClassNotFoundException e) {e.printStackTrace();}// 反射获取ContextThemeWrapper类的mResources字段Field mResourcesField = null;try {mResourcesField = contextThemeWrapperClass.getDeclaredField("mResources");} catch (NoSuchFieldException e) {e.printStackTrace();}// 设置字段可见性mResourcesField.setAccessible(true);// 将插件资源设置到插件 Activity 中try {mResourcesField.set(activity, pluginResource);} catch (IllegalAccessException e) {e.printStackTrace();}} catch (Exception e) {e.printStackTrace();}}
}

解决插件 WebView加载本地网页问题

插件apk activity页面的webview使用以下封装好的,可以加载asset目录下的网页

public class PluginWebView extends WebView {private Context mContext;private final String ANDROID_ASSET_PREFIX = "file:///android_asset/";private final String REPLACE_ASSET_PREFIX = "http://android.asset/";public PluginWebView(Context context) {super(context, null);}public PluginWebView(Context context, AttributeSet attrs) {super(context, attrs, 0);}public PluginWebView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context);}private void init(Context context) {mContext = context;setWebViewClient(new WebViewClient());}@Overridepublic void loadUrl(String url) {if (url.startsWith(ANDROID_ASSET_PREFIX)) {url = url.replace(ANDROID_ASSET_PREFIX, REPLACE_ASSET_PREFIX);}super.loadUrl(url);}@Overridepublic void setWebViewClient(WebViewClient client) {super.setWebViewClient(new PluginWebViewClient(client, mContext));}class PluginWebViewClient extends WebViewClient {private WebViewClient mWebViewClient;private Context mContext;public WarpWebViewClient(WebViewClient webViewClient, Context context) {mWebViewClient = webViewClient;mContext = context;}private WebResourceResponse getInterceptResponse(String url) {if (url.startsWith(REPLACE_ASSET_PREFIX)) {int end = url.indexOf("?");if (end == -1) {end = url.length();}String filePath = url.substring(REPLACE_ASSET_PREFIX.length(), end);String mime = "text/html";if (filePath.contains(".css")) {mime = "text/css";} else if (filePath.contains(".js")) {mime = "application/x-javascript";} else if (filePath.contains(".jpg") || filePath.contains(".gif") ||filePath.contains(".png") || filePath.contains(".jpeg")) {mime = "image/*";}try {return new WebResourceResponse(mime, "utf-8", mContext.getAssets().open(filePath));} catch (IOException ignored) {}}return null;}@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {return mWebViewClient.shouldOverrideUrlLoading(view, url);}@TargetApi(Build.VERSION_CODES.N)@Overridepublic boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {return mWebViewClient.shouldOverrideUrlLoading(view, request);}@Overridepublic void onPageStarted(WebView view, String url, Bitmap favicon) {mWebViewClient.onPageStarted(view, url, favicon);}@Overridepublic void onPageFinished(WebView view, String url) {mWebViewClient.onPageFinished(view, url);}@Overridepublic void onLoadResource(WebView view, String url) {mWebViewClient.onLoadResource(view, url);}@TargetApi(Build.VERSION_CODES.M)@Overridepublic void onPageCommitVisible(WebView view, String url) {mWebViewClient.onPageCommitVisible(view, url);}@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, String url) {WebResourceResponse resourceResponse = getInterceptResponse(url);if (resourceResponse != null) {return resourceResponse;}return mWebViewClient.shouldInterceptRequest(view, url);}@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {String url = request.getUrl().toString();WebResourceResponse resourceResponse = getInterceptResponse(url);if (resourceResponse != null) {return resourceResponse;}return mWebViewClient.shouldInterceptRequest(view, request);}@Overridepublic void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) {mWebViewClient.onTooManyRedirects(view, cancelMsg, continueMsg);}@Overridepublic void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {mWebViewClient.onReceivedError(view, errorCode, description, failingUrl);}@TargetApi(Build.VERSION_CODES.M)@Overridepublic void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {mWebViewClient.onReceivedError(view, request, error);}@TargetApi(Build.VERSION_CODES.M)@Overridepublic void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {mWebViewClient.onReceivedHttpError(view, request, errorResponse);}@Overridepublic void onFormResubmission(WebView view, Message dontResend, Message resend) {mWebViewClient.onFormResubmission(view, dontResend, resend);}@Overridepublic void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {mWebViewClient.doUpdateVisitedHistory(view, url, isReload);}@Overridepublic void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {mWebViewClient.onReceivedSslError(view, handler, error);}@TargetApi(Build.VERSION_CODES.LOLLIPOP)@Overridepublic void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {mWebViewClient.onReceivedClientCertRequest(view, request);}@Overridepublic void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {mWebViewClient.onReceivedHttpAuthRequest(view, handler, host, realm);}@Overridepublic boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {return mWebViewClient.shouldOverrideKeyEvent(view, event);}@Overridepublic void onUnhandledKeyEvent(WebView view, KeyEvent event) {mWebViewClient.onUnhandledKeyEvent(view, event);}@Overridepublic void onScaleChanged(WebView view, float oldScale, float newScale) {mWebViewClient.onScaleChanged(view, oldScale, newScale);}@Overridepublic void onReceivedLoginRequest(WebView view, String realm, String account, String args) {mWebViewClient.onReceivedLoginRequest(view, realm, account, args);}@TargetApi(Build.VERSION_CODES.O)@Overridepublic boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {return mWebViewClient.onRenderProcessGone(view, detail);}@TargetApi(Build.VERSION_CODES.O_MR1)@Overridepublic void onSafeBrowsingHit(WebView view, WebResourceRequest request, int threatType, SafeBrowsingResponse callback) {mWebViewClient.onSafeBrowsingHit(view, request, threatType, callback);}}
}
  • 原理总结:将WebView loadUrl(“file:///android_asset/xxx”),换成http协议,这样就能在shouldInterceptRequest中拦截到该请求,于是可以手动构造网页资源WebResourceResponse,传入assets目录内的网页资源文件流。

相关内容

热门资讯

蜻蜓订购APP贵金属订购非法期...   蜻蜓订购APP以"投资平台"名义对外宣传,主打贵金属期货(订购)交易。该平台一方面强调自身"正规...
悟空订购APP非法期货交易欺骗...   近期,有投资者反映,“悟空订购APP”等平台以白银、贵金属订购为名,宣传所谓“高收益”“低门槛”...
小赢淘金APP期货投资骗局,平...   “小赢淘金”之流,最擅长的就是用“注册送现金”、“充值返券”的糖衣炮弹来引流。别天真地以为那是福...
八方淘金APP非法期货交易欺骗...   警惕!八方生财、每日淘金、小赢淘金、悟空订购、蜻蜓订购、斗金订购等APP均涉嫌非法期货交易!  ...
智易购APP贵金属订购交易骗局...   很多人看到“3元起投”“充值送福利”“操作简单”“随时提现”这类宣传,很容易把智易购APP当成普...
喜玛银庄APP商品订购投资骗局...   喜玛银庄APP白银、铂金“现货订购”骗局警示:无资质平台涉嫌非法期货交易,投资者爆仓后如何维权?...
吉鑫乐享APP炒贵金属订购交易...   近期,多名投资者反映在“吉鑫乐享APP”进行贵金属投资时遭遇严重亏损,甚至血本无归。看似正规的“...
易掘金APP虚假期货投资骗局,...   “易掘金”的APP它打着“高收益、灵活投资”的旗号,宣称通过“商品订购”就能轻松赚钱,吸引了不少...
在大吉订购APP订购交易亏损被...   近期,不少投资者反映在“大吉订购App”上投资白银、铂金等商品遭遇严重亏损。该平台宣称提供现货订...
铂银壹号APP商品订购交易亏损...   许多中小投资者误以为网上的“商品现货订购”是合规合法的理财渠道,殊不知这背后隐藏着巨大的合规风险...
兴鑫淘金APP贵金属订购交易亏...   白银、贵金属或其他商品类投资,除国家批准的正规期货交易场所及持牌期货公司外,市面上大量第三方交易...
铂银掘金APP无资质的违规平台...   铂银掘金等平台的投资者,往往是被“注册送现金”“充值返现券”“首单免亏”等广告话术吸引,进而下载...
八方淘金APP订购交易不合规,...   “八方淘金APP”“铂银掘金APP”“铂银壹号APP”等软件被指以“现货订购”为名,实际采用高杠...
在指尖订购APP订购商品亏损被...   指尖订购APP这类平台,表面是“订购交易”,实则是披着电商外衣的非法期货陷阱。绝大多数投资者被蒙...
在犀贝易购APP做商品订购交易...   市面上打着“现货投资”“贵金属交易”旗号的APP并不少,但真正合规、受监管的平台并不多。犀贝易购...
易达淘金APP虚假商品订购交易...   易达淘金APP存在严重的虚假宣传与违规交易行为。该平台打着“商品订购”的旗号,实则通过不合规的交...
大浪淘金APP白银订购亏损被骗...   大浪淘金APP最隐蔽的套路,在于其完全封闭的内盘交易系统。表面上,它宣称与国际贵金属市场接轨;实...
一品好购APP上订购白银出现亏...   一品好购APP原名“一品订购”,运营方为上海宝智未来信息技术有限公司。近期不少投资者反映在该平台...
九州订购APP贵金属订购投资骗...   九州订购APP正通过抖.音、快手、今日头条等平台大量投放投资广告,用“5元起投”“充值返现”“0...
坚锅淘金APP交易软件并不正规...   当前,市面上大量所谓的“现货商城订购”交易软件,早已偏离了正常的实物商品买卖,演变为高杠杆、高手...