2020-Android-大厂面试(五)插件化、模块化,不同层级的Android开发者的不同行为
1. startActivity 的時候最終會走到 AMS 的 startActivity 方法
 2. 系統(tǒng)會檢查一堆的信息驗證這個 Activity 是否合法。
 3. 然后會回調(diào) ActivityThread 的 Handler 里的 handleLaunchActivity
 4. 在這里走到了 performLaunchActivity 方法去創(chuàng)建 Activity 并回調(diào)一系列生命周期的方法
 5. 創(chuàng)建 Activity 的時候會創(chuàng)建一個 LoaderApk對象,然后使用這個對象的 getClassLoader 來創(chuàng)建 Activity
 6. 我們查看 getClassLoader() 方法發(fā)現(xiàn)返回的是 PathClassLoader,然后他繼承自 BaseDexClassLoader
 7. 然后我們查看 BaseDexClassLoader 發(fā)現(xiàn)他創(chuàng)建時創(chuàng)建了一個 DexPathList 類型的 pathList對象,然后在 findClass 時調(diào)用了 pathList.findClass 的方法
 8. 然后我們查看 DexPathList類 中的 findClass 發(fā)現(xiàn)他內(nèi)部維護了一個 Element[] dexElements的dex 數(shù)組,findClass 時是從數(shù)組中遍歷查找的
2.插件化原理分析
DexClassLoader和PathClassLoader,它們都繼承于BaseDexClassLoader。
 DexClassloader多傳了一個optimizedDirectory
DexPathList
多DexClassLoader
每個插件單獨一個DexClassLoader,相對隔離,RePlugin采用該方案
單DexClassLoader
將插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。方便插件與宿主(插件)之間的調(diào)用,Small采用該方案
插件調(diào)用主工程
 主工程的ClassLoader作為插件ClassLoader的父加載器
主工程調(diào)用插件
 若使用多ClassLoader機制,通過插件的ClassLoader先加載類,再通過反射調(diào)用
 若使用單ClassLoader機制,直接通過類名去訪問插件中的類,弊端是庫的版本可能不一致,需要規(guī)范
資源加載
//創(chuàng)建AssetManager對象
 AssetManager assets = new AssetManager();
 //將apk路徑添加到AssetManager中
 if (assets.addAssetPath(resDir) == 0){
 return null;
 }
 //創(chuàng)建Resource對象
r = new Resources(assets, metrics, getConfiguration(), compInfo);
插件apk的路徑加入到AssetManager中
 通過反射去創(chuàng)建,并且部分Rom對創(chuàng)建的Resource類進行了修改,所以需要考慮不同Rom的兼容性。
資源路徑的處理
Context的處理
// 第一步:創(chuàng)建Resource
if (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> map = (Map<Object, WeakReference>) 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中,并且根據(jù)Android版本做了不同處理
//第三步:關(guān)聯(lián)resource和Activity
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
 activity.setIntent(intent);
 //設(shè)置Activity的mResources屬性,Activity中訪問資源時都通過mResources
ReflectUtil.setField(ContextThemeWrapper.class, acti
《Android學(xué)習(xí)筆記總結(jié)+最新移動架構(gòu)視頻+大廠安卓面試真題+項目實戰(zhàn)源碼講義》
開源分享完整內(nèi)容戳這里
vity, “mResources”, plugin.getResources());
資源沖突
資源id是由8位16進制數(shù)表示,表示為0xPPTTNNNN, 由三部分組成:PackageId+TypeId+EntryId
修改aapt源碼,編譯期修改PP段。
 修改resources.arsc文件,該文件列出了資源id到具體資源路徑的映射。
// Main.cpp
 result = handleCommand(&bundle);
 case kCommandPackage: return doPackage(bundle);
// Command.cpp
 int doPackage(Bundle* bundle) {
 if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
 err = buildResources(bundle, assets, builder);
 if (err != 0) {
 goto bail;
 }
 }
 }
Resource.cpp
 buildResources
ResourceTable.cpp
switch(mPackageType) {
 case App:
 case AppFeature:
 packageId = 0x7f;
 break;
 case System:
 packageId = 0x01;
 break;
 case SharedLibrary:
 packageId = 0x00;
 break;
 }
首先找到入口類:Main.cpp:main函數(shù),解析參數(shù),然后調(diào)用handleCommand函數(shù)處理參數(shù)對應(yīng)的邏輯,我們看到了有一個函數(shù)doPackage。
然后就搜索到了Command.cpp:在他內(nèi)部的doPackage函數(shù)中進行編譯工具的一個函數(shù):buildResources函數(shù),在全局搜索,發(fā)現(xiàn)了Resource.cpp:發(fā)現(xiàn)這里就是處理編譯工作,構(gòu)建ResourceTable的邏輯,在ResourceTable.cpp中,也是獲取PackageId的地方,下面我們就來看看如何修改呢?
其實最好的方法是,能夠修改aapt源碼,添加一個參數(shù),把我們想要編譯的PackageId作為輸入值,傳進來最好了,那就是Bundle類型,他是從Main.cpp中的main函數(shù)傳遞到了最后的buildResources函數(shù)中,那么我們就可以把這個參數(shù)用Bundle進行攜帶。
————————————————————————————————————————————————
在整個過程中,需要修改到R文件、resources.arsc和二進制的xml文件
四大組件支持
ProxyActivity代理
代理方式的關(guān)鍵總結(jié)起來有下面兩點:
ProxyActivity中需要重寫getResouces,getAssets,getClassLoader方法返回插件的相應(yīng)對象。生命周期函數(shù)以及和用戶交互相關(guān)函數(shù),如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要轉(zhuǎn)發(fā)給插件。
 PluginActivity中所有調(diào)用context的相關(guān)的方法,如setContentView,getLayoutInflater,getSystemService等都需要調(diào)用ProxyActivity的相應(yīng)方法。
該方式有幾個明顯缺點:
插件中的Activity必須繼承PluginActivity,開發(fā)侵入性強。
 如果想支持Activity的singleTask,singleInstance等launchMode時,需要自己管理Activity棧,實現(xiàn)起來很繁瑣。
 插件中需要小心處理Context,容易出錯。
 如果想把之前的模塊改造成插件需要很多額外的工作。
預(yù)埋StubActivity,hook系統(tǒng)啟動Activity的過程
VirtualAPK通過替換了系統(tǒng)的Instrumentation,hook了Activity的啟動和創(chuàng)建,省去了手動管理插件Activity生命周期的繁瑣,讓插件Activity像正常的Activity一樣被系統(tǒng)管理,并且插件Activity在開發(fā)時和常規(guī)一樣,即能獨立運行又能作為插件被主工程調(diào)用。
其他插件框架在處理Activity時思想大都差不多,無非是這兩種方式之一或者兩者的結(jié)合。在hook時,不同的框架可能會選擇不同的hook點。如360的RePlugin框架選擇hook了系統(tǒng)的ClassLoader,即構(gòu)造Activity2的ClassLoader,在判斷出待啟動的Activity是插件中的時,會調(diào)用插件的ClassLoader構(gòu)造相應(yīng)對象。另外RePlugin為了系統(tǒng)穩(wěn)定性,選擇了盡量少的hook,因此它并沒有選擇hook系統(tǒng)的startActivity方法來替換intent,而是通過重寫Activity的startActivity,因此其插件Activity是需要繼承一個類似PluginActivity的基類的。不過RePlugin提供了一個Gradle插件將插件中的Activity的基類換成了PluginActivity,用戶在開發(fā)插件Activity時也是沒有感知的。
 復(fù)制代碼
www.jianshu.com/p/ac96420fc…
sanjay-f.github.io/2016/04/17/…
www.jianshu.com/p/d43e1fb42…
Service插件化總結(jié)
初始化時通過ActivityManagerProxy Hook住了IActivityManager。
 服務(wù)啟動時通過ActivityManagerProxy攔截,判斷是否為遠程服務(wù),如果為遠程服務(wù),啟動RemoteService,如果為同進程服務(wù)則啟動LocalService。
 如果為LocalService,則通過DexClassLoader加載目標(biāo)Service,然后反射調(diào)用attach方法綁定Context,然后執(zhí)行Service的onCreate、onStartCommand方法
 如果為RemoteService,則先加載插件的遠程Service,后續(xù)跟LocalService一致。
 復(fù)制代碼
3.模塊化實現(xiàn)(好處,原因)
1、模塊間解耦,復(fù)用。
 (原因:對業(yè)務(wù)進行模塊化拆分后,為了使各業(yè)務(wù)模塊間解耦,因此各個都是獨立的模塊,它們之間是沒有依賴關(guān)系。
 每個模塊負責(zé)的功能不同,業(yè)務(wù)邏輯不同,模塊間業(yè)務(wù)解耦。模塊功能比較單一,可在多個項目中使用。)
2、可單獨編譯某個模塊,提升開發(fā)效率。
 (原因:每個模塊實際上也是一個完整的項目,可以進行單獨編譯,調(diào)試)
3、可以多團隊并行開發(fā),測試。
 原因:每個團隊負責(zé)不同的模塊,提升開發(fā),測試效率。
組件化與模塊化
組件化是指以重用化為目的,將一個系統(tǒng)拆分為一個個單獨的組件
避免重復(fù)造輪子,節(jié)省開發(fā)維護成本;
 降低項目復(fù)雜性,提升開發(fā)效率;
 多個團隊公用同一個組件,在一定層度上確保了技術(shù)方案的統(tǒng)一性。
模塊化業(yè)務(wù)分層:由下到上
基礎(chǔ)組件層:
 底層使用的庫和封裝的一些工具庫(libs),比如okhttp,rxjava,rxandroid,glide等
 業(yè)務(wù)組件層:
 與業(yè)務(wù)相關(guān),封裝第三方sdk,比如封裝后的支付,即時通行等
 業(yè)務(wù)模塊層:
 按照業(yè)務(wù)劃分模塊,比如說IM模塊,資訊模塊等
Library Module開發(fā)問題
在把代碼抽取到各個單獨的Library Module中,會遇到各種問題。
 最常見的就是R文件問題,Android開發(fā)中,各個資源文件都是放在res目錄中,在編譯過程中,會生成R.java文件。
 R文件中包含有各個資源文件對應(yīng)的id,這個id是靜態(tài)常量,但是在Library Module中,這個id不是靜態(tài)常量,那么在開發(fā)時候就要避開這樣的問題。
舉個常見的例子,同一個方法處理多個view的點擊事件,有時候會使用switch(view.getId())這樣的方式,
 然后用case R.id.btnLogin這樣進行判斷,這時候就會出現(xiàn)問題,因為id不是經(jīng)常常量,那么這種方式就用不了。
4.熱修復(fù)、插件化
宿主: 就是當(dāng)前運行的APP
 插件: 相對于插件化技術(shù)來說,就是要加載運行的apk類文件
 補丁: 相對于熱修復(fù)技術(shù)來說,就是要加載運行的.patch,.dex,*.apk等一系列包含dex修復(fù)內(nèi)容的文件。
QQ 空間超級補丁方案
Tinker
HotFix
當(dāng)然就熱修復(fù)的實現(xiàn),各個大廠還有各自的實現(xiàn),比如餓了嗎的Amigo,美團的Robust,實現(xiàn)及優(yōu)缺點各有差異,但總的來說就是兩大類
ClassLoader 加載方案
 Native層替換方案
 或者是參考Android Studio Instant Run 的思路實現(xiàn)代碼整體的增量更新。但這樣勢必會帶來性能的影響。
Sophix
底層替換方案
 原理:在已經(jīng)加載的類中直接替換掉原有方法,是在原有類的結(jié)構(gòu)基礎(chǔ)上進行修改的。在hook方法入口ArtMethod時,通過構(gòu)造一個新的ArtMethod實現(xiàn)替換方法入口的跳轉(zhuǎn)。
 應(yīng)用:能即時生效,Andfix采用此方案。
 缺點:底層替換穩(wěn)定性不好,適用范圍存在限制,通過改造代碼繞過限制既不優(yōu)雅也不方便,并且還沒提供資源及so的修復(fù)。
 類加載方案
 原理:讓app重新啟動后讓ClassLoader去加載新的類。如果不重啟,原來的類還在虛擬機中無法重復(fù)加載。
優(yōu)點:修復(fù)范圍廣,限制少。
應(yīng)用:騰訊系包括QQ空間,手QFix,Tinker采用此方案。
 QQ空間會侵入打包流程。
 QFix需要獲取底層虛擬機的函數(shù),不穩(wěn)定。
 Tinker是完整的全量dex加載。
Tinker與Sophix方案不同之處
 Tinker采用dex merge生成全量DEX方案。反編譯為smali,然后新apk跟基線apk進行差異對比,最后得到補丁包。
 Dalvik下Sophix和Tinker相同,在Art下,Sophix不需要做dex merge,因為Art下本質(zhì)上虛擬機已經(jīng)支持多dex的加載,要做的僅僅是把補丁dex作為主dex(classes.dex)加載而已:
 將補丁dex命名為classes.dex,原apk中的dex依次命名為classes(2, 3, 4…).dex就好了,然后一起打包為一個壓縮文件。然后DexFile.loadDex得到DexFile對象,最后把該DexFile對象整個替換舊的dexElements數(shù)組就好了。
資源修復(fù)方案
 基本參考InstantRun的實現(xiàn):構(gòu)造一個包含所有新資源的新的AssetManager。并在所有之前引用到原來的AssetManager通過反射替換掉。
 Sophix不修改AssetManager的引用,構(gòu)造的補丁包中只包含有新增或有修改變動的資源,在原AssetManager中addAssetPath這個包就可以了。資源包不需要在運行時合成完整包。
so庫修復(fù)方案
 本質(zhì)是對native方法的修復(fù)和替換。類似類修復(fù)反射注入方式,將補丁so庫的路徑插入到nativeLibraryDirectories數(shù)據(jù)最前面。
Method Hook
5.項目組件化的理解
總結(jié)
 組件化相較于單一工程,在組件模式下可以提高編譯速度,方便單元測試,提高開發(fā)效率。
 開發(fā)人員分工更加明確,基本上做到互不干擾。
 業(yè)務(wù)組件的架構(gòu)也可以自由選擇,不影響同伴之間的協(xié)作。
 降低維護成本,代碼結(jié)構(gòu)更加清晰。
6.描述清點擊 Android Studio 的 build 按鈕后發(fā)生了什么
apply plugin : ‘com.android.application’
 apply plugin : ‘com.android.library’
編譯五階段
1.準(zhǔn)備依賴包 Preparation of dependecies
 2.合并資源并處理清單 Merging resources and proccesssing Manifest
 3.編譯 Compiling
 4.后期處理 Postprocessing
 5.包裝和出版 Packaging and publishing
簡單構(gòu)建流程:
 1. Android編譯器(5.0之前是Dalvik,之后是ART)將項目的源代碼(包括一些第三方庫、jar包和aar包)轉(zhuǎn)換成DEX文件,將其他資源轉(zhuǎn)換成已編譯資源。
2. APK打包器將DEX文件和已編譯資源在使用秘鑰簽署后打包。
3. 在生成最終 APK 之前,打包器會使用zipalign 等工具對應(yīng)用進行優(yōu)化,減少其在設(shè)備上運行時的內(nèi)存占用。
構(gòu)建流程結(jié)束后獲得測試或發(fā)布用的apk。
圖中的矩形表示用到或者生成的文件,橢圓表示工具。
 1. 通過aapt打包res資源文件,生成R.java、resources.arsc和res文件
 2. 處理.aidl文件,生成對應(yīng)的Java接口文件
 3. 通過Java Compiler編譯R.java、Java接口文件、Java源文件,生成.class文件
 4. 通過dex命令,將.class文件和第三方庫中的.class文件處理生成classes.dex
 5. 通過apkbuilder工具,將aapt生成的resources.arsc和res文件、assets文件和classes.dex一起打包生成apk
 6. 通過Jarsigner工具,對上面的apk進行debug或release簽名
 7. 通過zipalign工具,將簽名后的apk進行對齊處理。
 這樣就得到了一個可以安裝運行的Android程序。
7.徹底搞懂Gradle、Gradle Wrapper與Android Plugin for Gradle的區(qū)別和聯(lián)系
Offline work時可能出現(xiàn)"No cached version of com.android.tools.build:gradle:xxx available for offline mode"問題
Gradle: gradle-wrapper.properties中的distributionUrl=https/😕/services.gradle.org/distributions/gradle-2.10-all.zip
 Gradle插件:build.gradle中依賴的classpath ‘com.android.tools.build:gradle:2.1.2’
Gradle:
 一個構(gòu)建系統(tǒng),構(gòu)建項目的工具,用來編譯Android app,能夠簡化你的編譯、打包、測試過程。
Gradle是一個基于Apache Ant和Apache Maven概念的項目自動化建構(gòu)工具。它使用一種基于Groovy的特定領(lǐng)域語言來聲明項目設(shè)置,而不是傳統(tǒng)的XML。當(dāng)前其支持的語言限于Java、Groovy和Scala
 現(xiàn)"No cached version of com.android.tools.build:gradle:xxx available for offline mode"問題
Gradle: gradle-wrapper.properties中的distributionUrl=https/😕/services.gradle.org/distributions/gradle-2.10-all.zip
 Gradle插件:build.gradle中依賴的classpath ‘com.android.tools.build:gradle:2.1.2’
Gradle:
 一個構(gòu)建系統(tǒng),構(gòu)建項目的工具,用來編譯Android app,能夠簡化你的編譯、打包、測試過程。
Gradle是一個基于Apache Ant和Apache Maven概念的項目自動化建構(gòu)工具。它使用一種基于Groovy的特定領(lǐng)域語言來聲明項目設(shè)置,而不是傳統(tǒng)的XML。當(dāng)前其支持的語言限于Java、Groovy和Scala
總結(jié)
以上是生活随笔為你收集整理的2020-Android-大厂面试(五)插件化、模块化,不同层级的Android开发者的不同行为的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 10办公软件软件分享
- 下一篇: C语言程序对夏令时的处理
