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目录内的网页资源文件流。

相关内容

热门资讯

瞄准未获Mythos使用权限的... 法国AI初创公司Mistral AI正与欧洲多家 银行洽谈,计划部署其对标Anthropic PBC...
一件代发怎么找云仓?按这四步走... 想做无货源电商,或者想把自己从打包发货中解放出来,“一件代发”是电商卖家无法绕不开的。问题来了,市面...
优化房地产政策促市场热度提升 4月28日召开的中共中央政治局会议指出,要努力稳定房地产市场。近期,多城市调整优化房地产调控政策,从...
“视听北京·金融作品征集”活动... 2026年5月13日,第六届中国(北京)广电媒体融合发展大会“金融新视界・视听新动能”金融与视听产业...
黄金走势图蓄势反弹 低成本布局... 来源:环球市场播报 如果你一直在关注SPDR 黄金份额 ETF(GLD),会发现金价近期处于盘整震荡...
马斯克点赞宇树载人机甲:很酷 站长之家(ChinaZ.com)5月13日 消息:宇树科技在5月12日扔出了一颗重磅炸弹。这家公司正...
「数据看盘」IM期指空头大幅加... 龙虎榜方面,红板科技获多家量化资金和游资的关注,获一家量化(摩根大通中国银城中路)买入0.62亿,遭...
原创 今... 2026年5月13日金价:大家不必继续盲目等待了!接下来,金价有可能会重演历史! 国内黄金价格继续处...
“中国最大AI包工头”冲击IP... 记者|鄢银婵 编辑|何小桃 廖丹 杜恒峰 校对|金冥羽 2026年4月29日,上海基流科技股份有限公...
白敬亭沈腾成立开门见衫公司 大象新闻记者 林林 天眼查App显示,5月12日,上海开门见衫品牌管理有限公司成立,法定代表人为上官...
原创 从... 今天来给大家聊一下中国磷化铟。2026年第一季度,全球前六大光模块厂商,中国占据四席;800G和1....
财报会释放重要信号,吴泳铭解读... 新京报贝壳财经讯(记者程子姣)5月13日,阿里巴巴集团发布2026财年第四季度与全年财报。在当晚的财...
抖音让大流量转化为大消费 “3、2、1,上链接!”不再是一句直播间的卖货口号,而是吃喝玩乐一站式服务的标语。文旅风光、特色餐饮...
千亿资本开支换来自由现金流转负... AI行业的竞争已从“模型竞赛”转入“算力消耗战”,为了应对这一趋势,阿里也正在大举进行新一轮AI基础...
原创 美... 美国诺克斯堡金库,那座号称囤积了4500吨黄金的神秘仓库,再次成为舆论焦点。而美国总统特朗普,这位以...
东方嘉富人寿:童超当选公司董事... 北京商报讯(记者 李秀梅)5月13日,东方嘉富人寿保险有限公司(以下简称“东方嘉富人寿”)公告,根据...
布朗32分孙铭徽复出 浙江广厦... 【搜狐体育战报】北京时间5月13日CBA季后赛,主场作战的浙江浙商证券以91-67击败山西汾酒,伤缺...
原创 外... 外汇储备被网友戏称为“金融核武”,因为其不仅在稳定汇率、保障经济安全上发挥关键作用,还有助于推动人民...
美股首只纯存储ETF,刷新华尔... 财联社5月13日讯(编辑 史正丞)近期存储芯片板块的猛烈上涨,使得一只成立不到6周的ETF成为华尔街...