浅析Android插件化
前言
Android P preview版本中,已限制對@hide api的反射調用,具體的原理可以閱讀Android P調用隱藏API限制原理這篇文章。由于最近團隊分享也在分享插件化、熱修復相關的東西。因此,寫一篇文章,好好記錄一下。
準備知識
- 反射、動態代理
- Android中的幾個相關的ClassLoader,注意PathClassLoader在ART虛擬機上是可以加載未安裝的APK的,Dalvik虛擬機則不可以。
- Android中四大組件的相關原理
- PackageManagerServer
- 資源加載、資源打包
- 其他
文章中所涉及到的代碼均通過Nexus 5(dalvik虛擬機) Android 6.0版本的測試
文章中所涉及到的一切資源都在這個倉庫下
特別說明,本博客不會特別解釋過多原理性的東西。如果讀者不具備相關的知識儲備,建議先閱讀weishu和gityuan兩位大神的博客,資源打包的知識可以閱讀 老羅的博客。
- Weishu's Notes
- gityuan
Activity的插件化
首先需要說明一點的是,啟動一個完全沒有在AndroidManifest注冊的Activity是不可能的。因為在啟動的過程中,存在一個校驗的過程,而這個校驗則是由PMS來完成的,這個我們無法干預。因此,Activity的插件化方案大多使用占坑的思想。不同的是如何在檢驗之前替換,在生成對象的時候還原。就目前來看,有兩種比較好方案:
- Hook Instrumentation方案
- 干預startActivity等方法,干預ClassLoader findClass的方案
這里說一下Hook Instrumentation方法。根據上面提到的想法,我們需要在先繞過檢查,那么,我們如何繞過檢查呢?通過分析Activity的啟動流程會發現,在Instrumentation#execStartActivity中,會有個checkStartActivityResult的方法去檢查錯誤,因此,我們可以復寫這個方法,讓啟動參數能通過系統的檢查。那么,我們如何做呢?首先,我們需要檢查要啟動的Intent能不能匹配到,匹配不到的話,將ClassName修改為我們預先在AndroidManifest中配置的占坑Activity,并且吧當前的這個ClassName放到當前intent的extra中,以便后續做恢復,看下代碼。
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);if (infos == null || infos.size() == 0) {//沒查到,要啟動的這個沒注冊intent.putExtra(TARGET_ACTIVITY, intent.getComponent().getClassName());intent.setClassName(who, "com.guolei.plugindemo.StubActivity");}Class instrumentationClz = Instrumentation.class;try {Method execMethod = instrumentationClz.getDeclaredMethod("execStartActivity",Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);return (ActivityResult) execMethod.invoke(mOriginInstrumentation, who, contextThread, token,target, intent, requestCode, options);} catch (Exception e) {e.printStackTrace();}return null;}我們繞過檢測了,現在需要解決的問題是還原,我們知道,系統啟動Activity的最后會調用到ActivityThread里面,在這里,會通過Instrumentation#newActivity方法去反射構造一個Activity的對象,因此,我們只需要在這里還原即可。代碼如下:
@Overridepublic Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,IllegalAccessException, ClassNotFoundException {if (!TextUtils.isEmpty(intent.getStringExtra(TARGET_ACTIVITY))) {return super.newActivity(cl, intent.getStringExtra(TARGET_ACTIVITY), intent);}return super.newActivity(cl, className, intent);}一切準備就緒,我們最后的問題是,如何替換掉系統的Instrumentation。要替換掉也簡單,替換掉ActivityThread中的mInstrumentation字段即可。
private void hookInstrumentation() {Context context = getBaseContext();try {Class contextImplClz = Class.forName("android.app.ContextImpl");Field mMainThread = contextImplClz.getDeclaredField("mMainThread");mMainThread.setAccessible(true);Object activityThread = mMainThread.get(context);Class activityThreadClz = Class.forName("android.app.ActivityThread");Field mInstrumentationField = activityThreadClz.getDeclaredField("mInstrumentation");mInstrumentationField.setAccessible(true);mInstrumentationField.set(activityThread,new HookInstrumentation((Instrumentation) mInstrumentationField.get(activityThread),context.getPackageManager()));} catch (Exception e) {e.printStackTrace();Log.e("plugin", "hookInstrumentation: error");}}這樣,我們就能啟動一個沒有注冊在AndroidManifest文件中的Activity了,但是這里要注意一下,由于我們這里使用的ClassLoader是宿主的ClassLoader,這樣的話,我們需要將插件的dex文件添加到我們宿主中。這一點很重要。有一些多ClassLoader架構的實現,這里的代碼需要變下。
Service的插件化
啟動一個未注冊的Service,并不會崩潰退出,只不過有點警告。并且,service啟動直接由ContextImpl交給AMS處理了,我們看下代碼。
private ComponentName startServiceCommon(Intent service, UserHandle user) {try {validateServiceIntent(service);service.prepareToLeaveProcess(this);ComponentName cn = ActivityManagerNative.getDefault().startService(mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(getContentResolver()), getOpPackageName(), user.getIdentifier());if (cn != null) {if (cn.getPackageName().equals("!")) {throw new SecurityException("Not allowed to start service " + service+ " without permission " + cn.getClassName());} else if (cn.getPackageName().equals("!!")) {throw new SecurityException("Unable to start service " + service+ ": " + cn.getClassName());}}return cn;} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}并且創建對象的過程不由Instrumentation來創建了,而直接在ActivityThread#handleCreateService反射生成。那么,Activity的思路我們就不能用了,怎么辦呢?既然我們無法做替換還原,那么,我們可以考慮代理,我們啟動一個真實注冊了的Service,我們啟動這個Service,并讓這個Service,就按照系統服務Service的處理,原模原樣的處理我們插件的Service。
說做就做,我們以startService為例。我們首先要做的是,hook掉AMS,因為AMS啟動service的時候,假如要啟動插件的Service,我們需要怎么做呢?把插件service替換成真是的代理Service,這樣,代理Service就啟動起來了,我們在代理Service中,構建插件的Service,并調用attach、onCreate等方法。
Hook AMS代碼如下:
private void hookAMS() {try {Class activityManagerNative = Class.forName("android.app.ActivityManagerNative");Field gDefaultField = activityManagerNative.getDeclaredField("gDefault");gDefaultField.setAccessible(true);Object origin = gDefaultField.get(null);Class singleton = Class.forName("android.util.Singleton");Field mInstanceField = singleton.getDeclaredField("mInstance");mInstanceField.setAccessible(true);Object originAMN = mInstanceField.get(origin);Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),new Class[]{Class.forName("android.app.IActivityManager")},new ActivityManagerProxy(getPackageManager(),originAMN));mInstanceField.set(origin, proxy);Log.e(TAG, "hookAMS: success" );} catch (Exception e) {Log.e(TAG, "hookAMS: " + e.getMessage());}}我們在看一下ActivityManagerProxy這個代理。
@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (method.getName().equals("startService")) {Intent intent = (Intent) args[1];List<ResolveInfo> infos = mPackageManager.queryIntentServices(intent, PackageManager.MATCH_ALL);if (infos == null || infos.size() == 0) {intent.putExtra(TARGET_SERVICE, intent.getComponent().getClassName());intent.setClassName("com.guolei.plugindemo", "com.guolei.plugindemo.StubService");}}return method.invoke(mOrigin, args);}代碼很清晰、也很簡單,不需要在做多余的了,那么,我們看下代理Service是如何啟動并且調用我們的插件Service的。
@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {Log.e(TAG, "onStartCommand: stub service ");if (intent != null && !TextUtils.isEmpty(intent.getStringExtra(TARGET_SERVICE))) {//啟動真正的serviceString serviceName = intent.getStringExtra(TARGET_SERVICE);try {Class activityThreadClz = Class.forName("android.app.ActivityThread");Method getActivityThreadMethod = activityThreadClz.getDeclaredMethod("getApplicationThread");getActivityThreadMethod.setAccessible(true);//獲取ActivityThreadClass contextImplClz = Class.forName("android.app.ContextImpl");Field mMainThread = contextImplClz.getDeclaredField("mMainThread");mMainThread.setAccessible(true);Object activityThread = mMainThread.get(getBaseContext());Object applicationThread = getActivityThreadMethod.invoke(activityThread);//獲取token值Class iInterfaceClz = Class.forName("android.os.IInterface");Method asBinderMethod = iInterfaceClz.getDeclaredMethod("asBinder");asBinderMethod.setAccessible(true);Object token = asBinderMethod.invoke(applicationThread);//Service的attach方法Class serviceClz = Class.forName("android.app.Service");Method attachMethod = serviceClz.getDeclaredMethod("attach",Context.class, activityThreadClz, String.class, IBinder.class, Application.class, Object.class);attachMethod.setAccessible(true);Class activityManagerNative = Class.forName("android.app.ActivityManagerNative");Field gDefaultField = activityManagerNative.getDeclaredField("gDefault");gDefaultField.setAccessible(true);Object origin = gDefaultField.get(null);Class singleton = Class.forName("android.util.Singleton");Field mInstanceField = singleton.getDeclaredField("mInstance");mInstanceField.setAccessible(true);Object originAMN = mInstanceField.get(origin);Service targetService = (Service) Class.forName(serviceName).newInstance();attachMethod.invoke(targetService, this, activityThread, intent.getComponent().getClassName(), token,getApplication(), originAMN);//service的oncreate方法Method onCreateMethod = serviceClz.getDeclaredMethod("onCreate");onCreateMethod.setAccessible(true);onCreateMethod.invoke(targetService);targetService.onStartCommand(intent, flags, startId);} catch (Exception e) {e.printStackTrace();Log.e(TAG, "onStartCommand: " + e.getMessage());}}return super.onStartCommand(intent, flags, startId);}代碼較長,邏輯如下:
- 檢測到需要啟動插件Service
- 構建插件Service attach方法需要的參數
- 構造一個插件Service
- 調用插件Service的attach方法
- 調用插件Service的onCreate方法
這樣,一個插件Service就啟動起來了。
BroadcastReceiver的插件化
BroadcastReceiver分為兩種,靜態注冊,和動態注冊。靜態注冊的是PMS在安裝或者系統啟動的時候掃描APK,解析配置文件,并存儲在PMS端的,這個我們無法干預,并且,我們的插件由于未安裝,靜態注冊的是無法通過系統正常行為裝載的。而動態注冊的,由于沒有檢測這一步,因此,也不需要我們干預。我們現在需要解決的問題就是,怎么能裝載插件中靜態注冊的。
我們可以通過解析配置文件,自己調用動態注冊的方法去注冊這個。
代碼這里就不貼了,和下面ContentProvider的一起貼。
ContentProvider的插件化
和其他三個組件不一樣的是,ContentProvider是在進程啟動入口,也就是ActivityThread中進行安裝的。那么我們可以按照這個思路,自己去進行安裝的操作。
代碼如下。
Field providersField = packageClz.getDeclaredField("providers");providersField.setAccessible(true);ArrayList providers = (ArrayList) providersField.get(packageObject);Class providerClz = Class.forName("android.content.pm.PackageParser$Provider");Field providerInfoField = providerClz.getDeclaredField("info");providersField.setAccessible(true);List<ProviderInfo> providerInfos = new ArrayList<>();for (int i = 0; i < providers.size(); i++) {ProviderInfo providerInfo = (ProviderInfo) providerInfoField.get(providers.get(i));providerInfo.applicationInfo = getApplicationInfo();providerInfos.add(providerInfo);}Class contextImplClz = Class.forName("android.app.ContextImpl");Field mMainThread = contextImplClz.getDeclaredField("mMainThread");mMainThread.setAccessible(true);Object activityThread = mMainThread.get(this.getBaseContext());Class activityThreadClz = Class.forName("android.app.ActivityThread");Method installContentProvidersMethod = activityThreadClz.getDeclaredMethod("installContentProviders", Context.class, List.class);installContentProvidersMethod.setAccessible(true);installContentProvidersMethod.invoke(activityThread, this, providerInfos);貼一下整體的代碼,這里的代碼,包括Multidex方法加dex,BroadcastReceiver的插件化以及ContentProvider的插件化。
private void loadClassByHostClassLoader() {File apkFile = new File("/sdcard/plugin_1.apk");ClassLoader baseClassLoader = this.getClassLoader();try {Field pathListField = baseClassLoader.getClass().getSuperclass().getDeclaredField("pathList");pathListField.setAccessible(true);Object pathList = pathListField.get(baseClassLoader);Class clz = Class.forName("dalvik.system.DexPathList");Field dexElementsField = clz.getDeclaredField("dexElements");dexElementsField.setAccessible(true);Object[] dexElements = (Object[]) dexElementsField.get(pathList);Class elementClz = dexElements.getClass().getComponentType();Object[] newDexElements = (Object[]) Array.newInstance(elementClz, dexElements.length + 1);Constructor<?> constructor = elementClz.getConstructor(File.class, boolean.class, File.class, DexFile.class);File file = new File(getFilesDir(), "test.dex");if (file.exists()) {file.delete();}file.createNewFile();Object pluginElement = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(),file.getAbsolutePath(), 0));Object[] toAddElementArray = new Object[]{pluginElement};System.arraycopy(dexElements, 0, newDexElements, 0, dexElements.length);// 插件的那個element復制進去System.arraycopy(toAddElementArray, 0, newDexElements, dexElements.length, toAddElementArray.length);dexElementsField.set(pathList, newDexElements);AssetManager assetManager = getResources().getAssets();Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);method.invoke(assetManager, apkFile.getPath());// PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_RECEIVERS); // if (packageInfo != null) { // for (ActivityInfo info : packageInfo.receivers) { // Log.e(TAG, "loadClassByHostClassLoader: " + info.name ); // // } // }Class packageParseClz = Class.forName("android.content.pm.PackageParser");Object packageParser = packageParseClz.newInstance();Method parseMethod = packageParseClz.getDeclaredMethod("parsePackage", File.class, int.class);parseMethod.setAccessible(true);Object packageObject = parseMethod.invoke(packageParser, apkFile, 1 << 2);Class packageClz = Class.forName("android.content.pm.PackageParser$Package");Field receiversField = packageClz.getDeclaredField("receivers");receiversField.setAccessible(true);ArrayList receives = (ArrayList) receiversField.get(packageObject);Class componentClz = Class.forName("android.content.pm.PackageParser$Component");Field intents = componentClz.getDeclaredField("intents");intents.setAccessible(true);Field classNameField = componentClz.getDeclaredField("className");classNameField.setAccessible(true);for (int i = 0; i < receives.size(); i++) {ArrayList<IntentFilter> intentFilters = (ArrayList<IntentFilter>) intents.get(receives.get(i));String className = (String) classNameField.get(receives.get(i));registerReceiver((BroadcastReceiver) getClassLoader().loadClass(className).newInstance(), intentFilters.get(0));}// 安裝ContentProviderField providersField = packageClz.getDeclaredField("providers");providersField.setAccessible(true);ArrayList providers = (ArrayList) providersField.get(packageObject);Class providerClz = Class.forName("android.content.pm.PackageParser$Provider");Field providerInfoField = providerClz.getDeclaredField("info");providersField.setAccessible(true);List<ProviderInfo> providerInfos = new ArrayList<>();for (int i = 0; i < providers.size(); i++) {ProviderInfo providerInfo = (ProviderInfo) providerInfoField.get(providers.get(i));providerInfo.applicationInfo = getApplicationInfo();providerInfos.add(providerInfo);}Class contextImplClz = Class.forName("android.app.ContextImpl");Field mMainThread = contextImplClz.getDeclaredField("mMainThread");mMainThread.setAccessible(true);Object activityThread = mMainThread.get(this.getBaseContext());Class activityThreadClz = Class.forName("android.app.ActivityThread");Method installContentProvidersMethod = activityThreadClz.getDeclaredMethod("installContentProviders", Context.class, List.class);installContentProvidersMethod.setAccessible(true);installContentProvidersMethod.invoke(activityThread, this, providerInfos);} catch (Exception e) {e.printStackTrace();Log.e(TAG, "loadClassByHostClassLoader: " + e.getMessage());}}到這里,四大組件的插件化方案介紹了一點點,雖然每種組件只介紹了一種方法。上面的內容忽略了大部分源碼細節。這部分內容需要大家自己去補。
資源的插件化方案
資源的插件化方案,目前有兩種
- 合并資源方案
- 各個插件構造自己的資源方案
今天,我們介紹第一種方案,合并資源方案,合并資源方案,我們只需要往現有的AssetManager中調用addAsset添加一個資源即可,當然,存在比較多適配問題,我們暫時忽略。合并資源方案最大的問題就是資源沖突。要解決資源沖突,有兩種辦法。
- 修改AAPT,能自由修改PP段
- 干預編譯過程,修改ASRC和R文件
為了簡單演示,我直接只用VirtualApk的編譯插件去做。實際上VirtualApk的編譯插件來自以Small的編譯插件。只要對文件格式熟悉,這個還是很好寫的。
AssetManager assetManager = getResources().getAssets();Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);method.invoke(assetManager, apkFile.getPath());我們只需要上面簡單的代碼,就能完成資源的插件化。當然,這里忽略了版本差異。
SO的插件化方案
so的插件化方案,我這里介紹修改dexpathlist的方案。我們要做的是什么呢?只需要往nativeLibraryPathElements中添加SO的Element,并且往nativeLibraryDirectories添加so路徑就可以了。代碼如下。
Method findLibMethod = elementClz.getDeclaredMethod("findNativeLibrary",String.class);findLibMethod.setAccessible(true); // Object soElement = constructor.newInstance(new File("/sdcard/"), true, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), // file.getAbsolutePath(), 0)); // findLibMethod.invoke(pluginElement,System.mapLibraryName("native-lib"));ZipFile zipFile = new ZipFile(apkFile);ZipEntry zipEntry = zipFile.getEntry("lib/armeabi/libnative-lib.so");InputStream inputStream = zipFile.getInputStream(zipEntry);File outSoFile = new File(getFilesDir(), "libnative-lib.so");if (outSoFile.exists()) {outSoFile.delete();}FileOutputStream outputStream = new FileOutputStream(outSoFile);byte[] cache = new byte[2048];int count = 0;while ((count = inputStream.read(cache)) != -1) {outputStream.write(cache, 0, count);}outputStream.flush();outputStream.close();inputStream.close();// 構造ElementObject soElement = constructor.newInstance(getFilesDir(), true, null, null); // findLibMethod.invoke(soElement,System.mapLibraryName("native-lib"));// 將soElement填充到nativeLibraryPathElements中,Field soElementField = clz.getDeclaredField("nativeLibraryPathElements");soElementField.setAccessible(true);Object[] soElements = (Object[]) soElementField.get(pathList);Object[] newSoElements = (Object[]) Array.newInstance(elementClz, soElements.length + 1);Object[] toAddSoElementArray = new Object[]{soElement};System.arraycopy(soElements, 0, newSoElements, 0, soElements.length);// 插件的那個element復制進去System.arraycopy(toAddSoElementArray, 0, newSoElements, soElements.length, toAddSoElementArray.length);soElementField.set(pathList, newSoElements);//將so的文件夾填充到nativeLibraryDirectories中Field libDir = clz.getDeclaredField("nativeLibraryDirectories");libDir.setAccessible(true);List libDirs = (List) libDir.get(pathList);libDirs.add(getFilesDir());libDir.set(pathList,libDirs);總結
在前人的精心研究下,插件化方案已經很成熟了。插件化方案的難點主要在適配方面。其他倒還好。
PS:熱修復的相關知識,PPT已經寫好了,下篇應該會淺析一下熱修復。
作者:_StriveG
鏈接:https://juejin.im/post/5ac5d015f265da239a600a72
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
總結
以上是生活随笔為你收集整理的浅析Android插件化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android P 调用隐藏API限制原
- 下一篇: Xposed简介以及小米去桌面广告的简单