本文记录插件化学习过程中,如何以插件化apk方式启动带WebView的Activity,分三大步骤完成。
通过反射实现,会影响性能。
通过接口实现,dynamic-load-apk的实现原理。
通过Hook技术实现,本文采用此方式实现。
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();}
}
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);}}
}
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());}
}
在打开的插件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();}}
}
插件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);}}
}