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

相关内容

热门资讯

中山东方医院标准化就诊流程:从... 在医疗服务质量不断提升的今天,标准化就诊流程建设已成为医院提升服务效率、改善患者体验的重要抓手。医院...
彩票卖不动了?去年全国彩票收入... 中国彩票收入增速持续放缓。 1月30日,财政部公布2025年12月份全国彩票销售情况。2025年全年...
原创 超... 当消费者为家中购置新物品时,功能之外,产品在“家”中的融入感、协调性如何,正成为越来越重要的考量——...
寒武纪预计2025年至高盈利2... 《科创板日报》1月30日讯(记者 郭辉)寒武纪发布2025年年度业绩预告。 公告显示,寒武纪预计20...
2025年我国基本医保统筹基金... 2025年我国基本医保统筹基金收入约2.95万亿元 新华社北京1月30日电(记者彭韵佳)记者1月3...
美股收盘:三大指数集体收低 多... 财联社1月31日讯(编辑 赵昊)周五(1月30日),美股低开低收,三大指数集体下跌。 截至收盘,道琼...
原创 金... 金价暴跌就该抛? 1月30日今日金价,黄金暴涨5%,踏空的人又哭了。 但你可能不知道,1月29号上海...
谈谈“体育强国”融入“健康中国... 聚焦健康中国 体育(第一健康报道融媒体中心 老柴体悟) 一谈到“健康中国”,我们往往会联想到“长寿...
服用这3类药不宜“多喝水” 提到吃药,“多喝水”似乎是默认的常识,可以帮助药物吞咽、减少对食道刺激、促进药物吸收,但并非所有药物...
为何看中医最好选“早上”? 68岁的陈阿姨常年失眠、乏力,夜里总在凌晨三点惊醒,醒来口干却不想喝水,白天精神萎靡,吃了一年多的安...
沃什提名引爆金银“血洗”!盘中... 美国总统特朗普提名沃什(Kevin Warsh)出任美联储主席引爆了贵金属数十年来最惨烈的抛售。 周...
深夜突发!金价大跳水,日内跌超... 北京时间1月31日凌晨,恐慌性抛售席卷全球贵金属市场。 截至发稿,现货白银日内跌幅扩大至34.67%...
拆解字节公益:新时代的新公益范... 最近,李亚鹏的嫣然天使儿童医院欠租新闻,又一次把“传统公益”推上了风口浪尖。 大家讨论得很热烈,捐...
平衡稳进破解投资难题 农银瑞恒... 当前利率处于低位,A股围绕4100点附近震荡,在此背景下,由农银汇理基金打造的农银瑞恒债券型证券投资...
记者手记|欧元汇率重返1.20... 新华社法兰克福1月30日电 记者手记|欧元汇率重返1.20背后的美元体系失序 新华社记者马悦然 近年...
为便捷付费,还是放弃拥有?订阅... 在“订阅制”日益普及的今天,人们对内容、软件和服务的“拥有权”似乎正逐渐让位于“使用权”。从流媒体平...
联合北大国发院 小红书将发起“... 真正的生活经济,就是让商业回归“为人服务”的本质。小红书方面表示,未来将与北京大学国家发展研究院等智...
“冻品一哥”转行做面包? 出品 | 创业最前线 作者 | 付艳翠 编辑 | 冯羽 美编 | 邢静 审核 | 颂文 最近,西贝与...
机器人“烧钱”也要上春晚打拳 记者 任晓宁 1月底,央视马年春晚彩排现场,宇树科技、银河通用、魔法原子、松延动力等具身智能公司的人...
芳烃与农化率先“突围” A股化... 本报记者 王僖 2026年以来,多个化工品价格强势反弹,并带动A股化工板块“春潮”涌动。 Wind资...