写点什么

2020Android 大厂面试(五)插件化,字节 Android 面试必问

用户头像
Android架构
关注
发布于: 刚刚

// PathClassLoaderpublic class PathClassLoader extends BaseDexClassLoader {public PathClassLoader(String dexPath, ClassLoader parent) {super(dexPath, null, null, parent);}public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {super(dexPath, null, librarySearchPath, parent);}}// DexClassLoaderpublicclass DexClassLoader extends BaseDexClassLoader {public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);}}


![](https://upload-images.jianshu.io/upload_images/24216715-0aa934cfa26b505c.png?imageMo


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


gr2/auto-orient/strip%7CimageView2/2/w/1240)


BaseDexClassLoader


dexPath:要加載的程序文件(一般是 dex 文件,也可以是 jar/apk/zip 文件)所在目錄。optimizedDirectory:dex 文件的輸出目錄(因為在加載 jar/apk/zip 等壓縮格式的程序文件時會解壓出其中的 dex 文件,該目錄就是專門用於存放這些被解壓出來的 dex 文件的)。libraryPath:加載程序文件時需要用到的庫路徑。parent:父加載器



阿里系:DeXposed、andfix:從底層二進制入手(c 語言)。阿里 andFix hook 方法在 native 的具體字段。art 虛擬機上是一個叫 ArtMethod 的結構體。通過修改該結構體上有 bug 的字段來達到修復 bug 方法的目的,但這個 artMethod 是根據安卓原生的結構寫死的,國內很多第三方廠家會改寫 ArtMethod 結構,導致替換失效。騰訊系:tinker:從 java 加載機制入手。qq 的 dex 插裝就類似上面分析的那種。通過將修復的 dex 文件插入到 app 的 dexFileList 的前面,達到更新 bug 的效果,但是不能及時生效,需要重啟。但虛擬機在安裝期間會為類打上 CLASS_ISPREVERIFIED 標誌,是為了提高性能的,我們強制防止類被打上標誌是否會有些影響性能美團 robust:是在編譯器為每個方法插入了一段邏輯代碼,併為每個類創建了一個 ChangeQuickRedirect 靜態成員變量,當它不為空會轉入新的代碼邏輯達到修復 bug 的目的。優點是兼容性高,但是會增加應用體積


PathClassLoader 和 DexClassLoader 都繼承自 BaseDexClassLoader1、Android 使用 PathClassLoader 作為其類加載器,只能去加載已經安裝到 Android 系統中的 apk 文件;2、DexClassLoader 可以從.jar 和.apk 類型的文件內部加載 classes.dex 文件就好了。熱修復也用到這個類。(1)動態改變 BaseDexClassLoader 對象間接引用的 dexElements;(2)在 app 打包的時候,阻止相關類去打上 CLASS_ISPREVERIFIED 標誌。(3)我們使用 hook 思想代理 startActivity 這個方法,使用佔坑的方式。


1. startActivity 的時候最終會走到 AMS 的 startActivity 方法 2. 系統會檢查一堆的信息驗證這個 Activity 是否合法。3. 然後會回調 ActivityThread 的 Handler 裡的 handleLaunchActivity4. 在這裡走到了 performLaunchActivity 方法去創建 Activity 並回調一系列生命週期的方法 5. 創建 Activity 的時候會創建一個 LoaderApk 對象,然後使用這個對象的 getClassLoader 來創建 Activity6. 我們查看 getClassLoader() 方法發現返回的是 PathClassLoader,然後他繼承自 BaseDexClassLoader7. 然後我們查看 BaseDexClassLoader 發現他創建時創建了一個 DexPathList 類型的 pathList 對象,然後在 findClass 時調用了 pathList.findClass 的方法 8. 然後我們查看 DexPathList 類 中的 findClass 發現他內部維護了一個 Element[] dexElements 的 dex 數組,findClass 時是從數組中遍歷查找的

2.插件化原理分析


DexClassLoader 和 PathClassLoader,它們都繼承於 BaseDexClassLoader。DexClassloader 多傳了一個 optimizedDirectory


DexPathList



多 DexClassLoader



每個插件單獨一個 DexClassLoader,相對隔離,RePlugin 採用該方案


單 DexClassLoader


將插件的 DexClassLoader 中的 pathList 合併到主工程的 DexClassLoader 中。方便插件與宿主(插件)之間的調用,Small 採用該方案


插件調用主工程主工程的 ClassLoader 作為插件 ClassLoader 的父加載器主工程調用插件若使用多 ClassLoader 機制,通過插件的 ClassLoader 先加載類,再通過反射調用若使用單 ClassLoader 機制,直接通過類名去訪問插件中的類,弊端是庫的版本可能不一致,需要規範


資源加載


//創建 AssetManager 對象 AssetManager assets = new AssetManager();//將 apk 路徑添加到 AssetManager 中 if (assets.addAssetPath(resDir) == 0){


return null;


}//創建 Resource 對象 r = new Resources(assets, metrics, getConfiguration(), compInfo);插件 apk 的路徑加入到 AssetManager 中通過反射去創建,並且部分 Rom 對創建的 Resource 類進行了修改,所以需要考慮不同 Rom 的兼容性。


資源路徑的處理



Context 的處理


// 第一步:創建 Resourceif (Constants.COMBINE_RESOURCES) {//插件和主工程資源合併時需要 hook 住主工程的資源 Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());ResourcesManager.hookResources(context, resources);


return resources;} else {


//插件資源獨立,該 resource 只能訪問插件自己的資源 Resources hostResources = context.getResources();AssetManager assetManager = createAssetManager(context, apk);


return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());}


//第二步:hook 主工程的 Resource//對於合併式的資源訪問方式,需要替換主工程的 Resource,下面是具體替換的代碼。public static void hookResources(Context base, Resources resources) {try {ReflectUtil.setField(base.getClass(), base, "mResources", resources);Object loadedApk = ReflectUtil.getPackageInfo(base);ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);Object activityThread = ReflectUtil.getActivityThread(base);Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");


if (Build.VERSION.SDK_INT < 24) {Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");Object key = map.keySet().iterator().next();map.put(key, new WeakReference<>(resources));} else {


// still hook Android N Resources, even though it's unnecessary, then nobody will be strange.Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls");Object key = map.keySet().iterator().next();Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl");map.put(key, new WeakReference<>(resourcesImpl));}} catch (Exception e) {e.printStackTrace();}


替換了主工程 context 中 LoadedApk 的 mResource 對象


將新的 Resource 添加到主工程 ActivityThread 的 mResourceManager 中,並且根據 Android 版本做了不同處理


//第三步:關聯 resource 和 ActivityActivity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);activity.setIntent(intent);//設置 Activity 的 mResources 屬性,Activity 中訪問資源時都通過 mResourcesReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());


資源衝突


資源 id 是由 8 位 16 進制數表示,表示為 0xPPTTNNNN,由三部分組成:PackageId+TypeId+EntryId


修改 aapt 源碼,編譯期修改 PP 段。修改 resources.arsc 文件,該文件列出了資源 id 到具體資源路徑的映射。


blog.csdn.net/jiangwei091…


// Main.cppresult = handleCommand(&bundle);case kCommandPackage: return doPackage(bundle);// Command.cppint doPackage(Bundle* bundle) {if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {err = buildResources(bundle, assets, builder);if (err != 0) {goto bail;}}}Resource.cppbuildResourcesResourceTable.cppswitch(mPackageType) {case App:case AppFeature:packageId = 0x7f;break;case System:packageId = 0x01;break;case SharedLibrary:packageId = 0x00;break;


}


首先找到入口類:Main.cpp:main 函數,解析參數,然後調用 handleCommand 函數處理參數對應的邏輯,我們看到了有一個函數 doPackage。然後就搜索到了 Command.cpp:在他內部的 doPackage 函數中進行編譯工具的一個函數:buildResources 函數,在全局搜索,發現了 Resource.cpp:發現這裡就是處理編譯工作,構建 ResourceTable 的邏輯,在 ResourceTable.cpp 中,也是獲取 PackageId 的地方,下面我們就來看看如何修改呢?其實最好的方法是,能夠修改 aapt 源碼,添加一個參數,把我們想要編譯的 PackageId 作為輸入值,傳進來最好了,那就是 Bundle 類型,他是從 Main.cpp 中的 main 函數傳遞到了最後的 buildResources 函數中,那麼我們就可以把這個參數用 Bundle 進行攜帶。



juejin.im/entry/5c008…www.jianshu.com/p/8d691b6bf…


————————————————————————————————————————————————


cloud.tencent.com/developer/a…


在整個過程中,需要修改到 R 文件、resources.arsc 和二進制的 xml 文件


四大組件支持


ProxyActivity 代理



代理方式的關鍵總結起來有下面兩點:ProxyActivity 中需要重寫 getResouces,getAssets,getClassLoader 方法返回插件的相應對象。生命週期函數以及和用戶交互相關函數,如 onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged 等需要轉發給插件。PluginActivity 中所有調用 context 的相關的方法,如 setContentView,getLayoutInflater,getSystemService 等都需要調用 ProxyActivity 的相應方法。該方式有幾個明顯缺點:插件中的 Activity 必須繼承 PluginActivity,開發侵入性強。如果想支持 Activity 的 singleTask,singleInstance 等 launchMode 時,需要自己管理 Activity 棧,實現起來很繁瑣。插件中需要小心處理 Context,容易出錯。如果想把之前的模塊改造成插件需要很多額外的工作。


預埋 StubActivity,hook 系統啟動 Activity 的過程



VirtualAPK 通過替換了系統的 Instrumentation,hook 了 Activity 的啟動和創建,省去了手動管理插件 Activity 生命週期的繁瑣,讓插件 Activity 像正常的 Activity 一樣被系統管理,並且插件 Activity 在開發時和常規一樣,即能獨立運行又能作為插件被主工程調用。其他插件框架在處理 Activity 時思想大都差不多,無非是這兩種方式之一或者兩者的結合。在 hook 時,不同的框架可能會選擇不同的 hook 點。如 360 的 RePlugin 框架選擇 hook 了系統的 ClassLoader,即構造 Activity2 的 ClassLoader,在判斷出待啟動的 Activity 是插件中的時,會調用插件的 ClassLoader 構造相應對象。另外 RePlugin 為了系統穩定性,選擇了儘量少的 hook,因此它並沒有選擇 hook 系統的 startActivity 方法來替換 intent,而是通過重寫 Activity 的 startActivity,因此其插件 Activity 是需要繼承一個類似 PluginActivity 的基類的。不過 RePlugin 提供了一個 Gradle 插件將插件中的 Activity 的基類換成了 PluginActivity,用戶在開發插件 Activity 時也是沒有感知的。


www.jianshu.com/p/ac96420fc…


Service 插件化總結初始化時通過 ActivityManagerProxy Hook 住了 IActivityManager。服務啟動時通過 ActivityManagerProxy 攔截,判斷是否為遠程服務,如果為遠程服務,啟動 RemoteService,如果為同進程服務則啟動 LocalService。如果為 LocalService,則通過 DexClassLoader 加載目標 Service,然後反射調用 attach 方法綁定 Context,然後執行 Service 的 onCreate、onStartCommand 方法如果為 RemoteService,則先加載插件的遠程 Service,後續跟 LocalService 一致。

3.模塊化實現(好處,原因)

www.cnblogs.com/Jackie-zhan…


1、模塊間解耦,複用。(原因:對業務進行模塊化拆分後,為了使各業務模塊間解耦,因此各個都是獨立的模塊,它們之間是沒有依賴關係。每個模塊負責的功能不同,業務邏輯不同,模塊間業務解耦。模塊功能比較單一,可在多個項目中使用。)2、可單獨編譯某個模塊,提升開發效率。(原因:每個模塊實際上也是一個完整的項目,可以進行單獨編譯,調試)3、可以多團隊並行開發,測試。原因:每個團隊負責不同的模塊,提升開發,測試效率。


組件化與模塊化


組件化是指以重用化為目的,將一個系統拆分為一個個單獨的組件


避免重複造輪子,節省開發維護成本;降低項目複雜性,提升開發效率;多個團隊公用同一個組件,在一定層度上確保了技術方案的統一性。


模塊化業務分層:由下到上


基礎組件層:底層使用的庫和封裝的一些工具庫(libs),比如 okhttp,rxjava,rxandroid,glide 等業務組件層:與業務相關,封裝第三方 sdk,比如封裝後的支付,即時通行等業務模塊層:按照業務劃分模塊,比如說 IM 模塊,資訊模塊等


Library Module 開發問題


在把代碼抽取到各個單獨的 Library Module 中,會遇到各種問題。最常見的就是 R 文件問題,Android 開發中,各個資源文件都是放在 res 目錄中,在編譯過程中,會生成 R.java 文件。R 文件中包含有各個資源文件對應的 id,這個 id 是靜態常量,但是在 Library Module 中,這個 id 不是靜態常量,那麼在開發時候就要避開這樣的問題。舉個常見的例子,同一個方法處理多個 view 的點擊事件,有時候會使用 switch(view.getId())這樣的方式,然後用 case R.id.btnLogin 這樣進行判斷,這時候就會出現問題,因為 id 不是經常常量,那麼這種方式就用不了。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
2020Android大厂面试(五)插件化,字节Android面试必问