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都...
“末日将至”!《大空头》原型化... 智通财经获悉,上周二,电影《大空头》的原型人物迈克尔·伯里(Michael Burry)刚刚亮出一系...
高股息吸引力拉满!银行板块逆势... 7月6日,银行板块盘中逆势走强, 青岛银行(002948.SZ)涨幅超4%,引领 西安银行(6009...
比音勒芬的新“联姻”:“衣中茅... 轻中有重。 《投资者网》蔡俊 如果你穿梭在国内各大机场、高铁站,一定看到过比音勒芬(002832....
团体保险业务风险提示:防范职业... 国家金融监管总局上海监管局办公室近日发布《关于团体保险业务有关风险提示的通知》,通报了一起涉农团体意...
现货黄金重回4200美元关口 6日早间,现货黄金持续拉涨,盘中再次突破4200美元/盎司,涨超0.6%。 在经历了连续四周下挫后...
烟花厂防爆挡墙竟是空心的!谁在... 防护屏障,俗称防爆挡墙,是烟花爆竹厂的相关区域出现危险状况的时候,用来减轻冲击波等因素对其他区域造成...
港股异动丨航空股普涨 南航涨超... 港股航空股普遍上涨,其中,中国南方航空涨超4%,中国东方航空涨3.7%,中国国航涨超3%,美兰空港涨...
为啥老百姓感觉酒店越来越贵,锦... 最近一两年,越来越多的人开始抱怨酒店变贵了。“月薪2万住不起如家汉庭”的话题频频登上热搜。打开订房软...
3日两融余额减少195.59亿... 上证报中国证券网讯(卢伊 记者 江小轩)7月3日,A股两融余额为30033.37亿元,较上一交易日减...
鲁汶仪器尚未盈利冲科创板,IP... 瑞财经 王敏 6月30日,据上交所,江苏鲁汶仪器股份有限公司(以下简称“鲁汶仪器”)科创板IPO获受...
沃镭智能IPO:中信建投保荐,... 作者:方 升 编辑:张佳茗 杭州沃镭智能科技股份有限公司(下称:沃镭智能)专注于智能检测装备、智能制...
智利两大矿企为锂合资项目增产7... 【智利两大矿企为锂合资项目增产70%规划路径】财联社7月4日电,智利化工矿业(SQM)和智利国家铜业...
日TURING目标最早2028... IT之家 7 月 6 日消息,日本智驾技术初创企业 TURING 当地时间今日宣布完成 7900 万...
IPO研究丨本周4家上会,76... 瑞财经 李兰 7月6日,据上交所、深交所、北交所,本周4家企业上会,2只新股将进行申购。 上周新股首...
原创 暴... 7月3日晚,存储龙头一纸公告,炸得整个A股半导体圈彻夜难眠。 江波龙,这家深圳的存储模组企业,交出了...
国投白银LOF提示二级市场溢价... 7月2日,国投瑞银基金管理有限公司公告,公司旗下国投瑞银白银期货证券投资基金(LOF)A类基金份额(...
皇姑核芯的“低密答案”:TA,... 2026年的沈阳楼市,正走过一个微妙的分水岭,楼市进入分化阶段已经成为行业共识。 数据释放的信号很明...
德国金属材料产业链调整:中国园... (来源:欧洲并购与投资) 德国金属材料产业链调整:中国园区该承接什么、不该承接什么 本系列前两篇分...