59. Instant Run 筆記

1.現象


1. 打開 Instant Run,首次運行,會用到 Transform API 修改字節碼。


2. 會輸出 Instant Run 產出的相關類,在 Application/build/incremental/transforms/instantRun/debug/folders/1/5


instant_run_notes_1


3. 所有源碼中的方法:都會加上 $change 代碼段,$changeIncrementalChange 接口類型。會判斷是否爲 null,不爲 null,然後 調用 $changeaccess$dispatch。參數爲方法簽名字符串和方法參數數組,否則調用原邏輯。


instant_run_notes_2


4. 後面的 運行:會生成補丁類 dex。輸出目錄爲:Application/build/incremental/transforms/instantRun/debug/folders/4000


instant_run_notes_3


5. 該目錄中會有 你修改類的 $override 類。比如修改了 MainActivity 的源代碼,就會生成 A$override implements IncrementalChange 類。


instant_run_notes_4


6. 然後,IncrementalChange 是接口,得實現 IncrementalChange 接口的 access$dispatch 方法,然後根據第 3. 傳來的 方法簽名參數 調用改方法。


7. 最後會在 3. 那的 $change 修改爲 MainActivity$change(例子),這樣不爲 null 的話,就會走到 A$change.access$dispatch 達到 Hook Fix 效果 。


8. 4000/5/xxx/com/android/tools/fd/runtime/ 中,會找到 AppPatchesLoaderImpl 類,該類記錄了所有 改動類。也繼承了 AbstractPatchesLoaderImpl


instant_run_notes_5



總結現象


1. 爲生成的 class 添加 $change 佔位字段。


2. $change 未來可能賦的值是,通過 AppPatchesLoaderImpl 內的記錄非第一次運行後的所有改動類 ,然後供 load 方法支持設置被修改原類 $change 字段,當收到補丁通知時,只需新建一個 DexClassLoader,去反射加載補丁 dex 中的 AppPatchesLoaderImpl 類,調用 load 方法即可,load 方法中會去加載全部補丁類,並賦值給對應原類的 $change



替換 BootstrapApplication 爲 RealApplication

Instant Run 的項目,構建的 Application 不是項目的 RealApplication,而是 BootstrapApplication。只是被後來 Hook 替換爲 RealApplication 了。


Application/build/incremental/bundles/debug/instant-run/AndroidManifest.xml 可以查看到 替換後的 xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.camnter.instantrunresearch"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="14"
        android:targetSdkVersion="25" />

    <application
        android:name="com.android.tools.fd.runtime.BootstrapApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity android:name="com.camnter.instantrunresearch.MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


1. 會執行 BootstrapApplicationattachBaseContext


2. attachBaseContextcreateResources(...),主要是判斷資源 resource.ap_ 是否改變,然後保存 resource.ap_ 的路徑到 externalResourcePath 中。


3. attachBaseContextsetupClassLoaders(...)。進行對 ApplicationClassLoader.parentHook 操作。Hook 爲插件的 DexClassLoader(IncrementalClassLoader),這裏封裝了一個 IncrementalClassLoader 就可以通過 nativeLibraryPath 路徑加載 dex。然後替換 ApplicationClassLoader.parent 爲 IncrementalClassLoader。將 PathClassLoader -> BootClassLoader 轉換爲 PathClassLoader -> IncrementalClassLoader -> BootClassLoader


4. attachBaseContextcreateRealApplication(...)。會通過 動態生成的 AppInfo 拿到 App 自定義的 RealApplication name。假如有自定義的 RealApplication,反射拿到自定義的 RealApplication


5. 因爲 XML 內的替換爲 BootstrapApplicationApp 實際上是實例化的是 BootstrapApplication。然而,我們期望在的行爲自定義在 RealApplication


6. 所以,會在 attachBaseContext(...) 內拿到 3. 反射得到的自定義 RealApplication。然後再反射調用自定義 RealApplication.attachBaseContext(...) 完成自定義行爲。其實,也是 反射代理


7. 執行 BootstrapApplicationattachBaseContext 之後,會執行到 BootstrapApplicationonCreate


8. BootstrapApplicationonCreate 主要分爲三個模塊:
- - 8.1 替換 ActivityThread 內所有的 BootstrapApplicationRealApplication
- - 8.2 如果 resource.ap_ 文件有改變,那麼新建一個 AssetManager 對象 newAssetManager,然後用 newAssetManager 對象 替換 所有當前 Resource、Resource.Theme 的 mAssets 成員變量。
- - 8.3 如果當前的已經有 Activity 啓動了,還需要替換所有 Activity 中 mAssets 成員變量。
- - 8.4 啓動 Server,開啓 Socket,開始讀取數據,當讀到 MESSAGE_PATCHES 時,獲取代碼變化的 ApplicationPatch 列表,然後調用 handlePatches 來處理代碼的變化,執行 熱溫冷 部署的分發。



BootstrapApplication


1. createResources: 創建資源 - 本質就是 copy resources.ap_:
- - 1.1 /data/data/…/files/instant-run/inbox/resources.ap_ 是否存在。
- - 1.2 存在的話,複製到 /data/data/…/files/instant-run/left(or right)/resources.ap_ 下。
- - 1.3 判斷是否 複製成功,即有文件。
- - 1.4 判斷 2. 路徑下的 resources.ap_ 是否沒被修改( 0L = 不存在 ),並且如果資源文件的修改時間小於 APP 的 APK 修改時間的話,那麼說明這是一個 舊的資源文件( 失效的舊的 resources.ap_ ),應該 忽略( externalResourcePath = null )。


2. setupClassLoaders: HOOK BootstrapApplication 的 ClassLoader 的 類加載機制:
- - 2.1 獲取 /data/data/…/files/instant-run/dex 下的所有 .dex 路徑( List )。
- - 2.2 如果有 dex 路徑 List 沒有內容,直接 return。
- - 2.3 獲取加載 BootstrapApplication 的 ClassLoader。
- - 2.4 反射該 ClassLoader 的 getLdLibraryPath 方法拿到 nativeLibraryPath。
- - - - 2.4.1 如果成功,則直接複製給 nativeLibraryPath。
- - - - 2.4.2 如果失敗,捕獲異常,打印 Log 後,設置 nativeLibraryPath = /data/data/…/lib。
- - 2.5 調用了 靜態方法 IncrementalClassLoader.inject(….) 後,直接 HOOK 了 該 ClassLoader 的加載模式爲:BootClassLoader -> incrementalClassLoader -> classLoader。
- - 2.6 這樣的話 BootstrapApplication 的 ClassLoader 的加載 Class 機制就會先走插件 incrementalClassLoader。


3. createRealApplication: 創建 真正的 Application( App 內自定義的 Application ):
- - 3.1 AppInfo 中取出,真正 Application 的 packageName,forName(…) 實例化一個 Class

MonkeyPatcher


1. monkeyPatchApplication( Hook BootstrapApplication ):
- - 1.1 Hook 掉 ActivityThread 內的所有 BootstrapApplication 爲 RealApplication。
- - 1.2 Hook 掉 ActivityThread 內的所有 LoadedApk 內部的:
- - - - 1.2.1 BootstrapApplication 爲 RealApplication。
- - - - 1.2.2 mResDir 爲 externalResourceFile。


2. monkeyPatchExistingResources( 加載補丁資源,並 Hook 進 App 內 ):
- - 2.1 反射調用 AssetManager.addAssetPath 方法加載 補丁資源。
- - 2.2 Hook Resource or ResourcesImpl 中的 mAssets,Hook 爲 補丁資源。
- - 2.3 Hook Resource or ResourcesImpl 內 Theme or ThemeImpl 中的 mAssets,Hook 爲 補丁資源。
- - 2.4 Hook Activity( ContextThemeWrapper )的 initializeTheme 方法去初始化 Theme。
- - 2.5 如果 < 7.0, 先 Hook AssetManager 的 createTheme 方法去創建一個 補丁 Theme。然後 Hook Activity 的 Theme 的 mTheme Field 爲 補丁 Theme。
- - 2.6 調用 pruneResourceCaches(@NonNull Object resources) 方法去刪除 資源緩存。


3. pruneResourceCache( 由於 hook 進來了 newAssetManager,所以需要把原來運行 Activity 的資源緩存清空 ):
- - 3.1 刪除 Resource 內部的 TypedArrayPool 的資源緩存。
- - 3.2 刪除 Resource 圖片、動畫、顏色等資源緩存。
- - 3.3 刪除 ResourceImpl 圖片、動畫、顏色等資源緩存。



IncrementalClassLoader


可以理解 IncrementalClassLoader 就是 插件 class 的 classLoader


1. 提供了靜態方法 inject(ClassLoader classLoader,String nativeLibraryPath,String codeCacheDir,List dexes) 方便將目標 classLoader 的 parent 替換爲 IncrementalClassLoader。


2. 這樣的話 parent 的父加載的 class 和 res 就走的是 IncrementalClassLoader 的加載。BootClassLoader -> classLoader 就會變爲 BootClassLoader -> IncrementalClassLoader -> classLoader。


3. 然而 IncrementalClassLoader 的加載的邏輯又靠 DelegateClassLoader。DelegateClassLoader 是 BaseDexClassLoader 的子類,覆寫了 findClass 方法,但是,只是爲了打點 Log。



Server


handle(DataInputStream input, DataOutputStream output):


1. 資源校驗( res/resources.ap_ )。


2. 處理補丁
- - 2.1 dex 結尾的格式,就執行 handleColdSwapPatch(…) 冷部署。
- - 2.2 dex 結尾的格式 並且 名字爲 “classes.dex.3” 則記錄爲 熱部署。
- - 2.3 名字爲 “classes.dex.3” 直接執行熱部署 handleHotSwapPatch(…)。
- - 2.4 “res/resources.ap_” 那麼直接處理資源補丁 handleResourcePatch(…)。
- - 2.5
- - - - 2.5.1 溫部署 加載補丁 ( 處理資源補丁 ):調用 FileManager.writeAaptResources(…) 處理資源補丁。
- - - - 2.5.2 熱部署 加載補丁:
- - - - - - 2.5.2.1 將補丁文件 保存爲 /data/data/…/files/instant-run/dex-temp/reload0x?04x.dex。
- - - - - - 2.5.2.2 然後 通過 此 dex 去創建一個 DexClassLoader。
- - - - - - 2.5.2.3 通過創建的 DexClassLoader 去尋找內部的 AppPatchesLoaderImpl 類。
- - - - - - 2.5.2.4 進而獲取 getPatchedClasses 方法,得到 String[] classes。
- - - - - - 2.5.2.5 然後打 String[] classes 的 Log。
- - - - - - 2.5.2.6 AppPatchesLoaderImpl 向上轉爲 PatchesLoader 類型。
- - - - - - 2.5.2.7 調用 ( AppPatchesLoaderImpl )PatchesLoader.load() 方法打上 $override 和 $change 標記位。
- - - - 2.5.3 冷部署 加載補丁:
- - - - - - 2.5.3.1 判斷補丁是否是 slice- 開頭。
- - - - - - 2.5.3.2 將補丁保存在 /data/data/…/files/instant-run/dex/ 目錄下。


3. 重啓流程處理
- - 3.1 熱部署:如果更新模式 是 None 或者 熱部署。如果要顯示 toast。獲取前臺 Activity,然後用 前臺 Activity 顯示 toast,然後返回。
- - 3.2 冷部署:
- - - - 3.2.1 獲取所有沒有 paused 的 Activity。
- - - - 3.2.2 獲取外部資源文件路徑 /data/data/…/files/instant-run/left(right)/resources.ap_。
- - - - 3.2.3 如果不存在資源文件:MonkeyPatcher.monkeyPatchApplication + MonkeyPatcher.monkeyPatchExistingResources;如果存在存在資源文件:設置更新模式 - 冷部署。
- - 3.3 溫部署:
- - - - 3.3.1 先拿到前臺顯示的 Activity。
- - - - 3.3.2 如果是 溫部署 :
- - - - - - 3.3.2.1 然後反射獲取 onHandleCodeChange 方法,進而傳入 0L 爲參數,進行反射調用。
- - - - - - 3.3.2.2 如果,剛纔的 handledRestart 標記爲 true,那麼繼續顯示 toast,然後重啓 Activity 後返回。
- - - - - - 3.3.2.3 最後將更新模式設置爲 冷部署。
- - - - 3.3.3 判斷更新模式如果是冷部署則返回( 證明沒成功調用 onHandleCodeChange )。



Restarter


1. restartActivityOnUiThread:只在 UiThread 線程執行 updateActivity(…)


2. restartActivity:重啓 Activity。
- - 2.1 拿到該 Activity 的最頂層 Parent Activity。
- - 2.2 然後用 最頂層 Parent Activity 執行 recreate 方法。


3. restartApp:重啓 App。
- - 3.1 判斷 activities 是否沒有內容。
- - - - 3.1.1 沒有的話,這個方法就不做任何事情。
- - - - 3.1.2 有的話,繼續。
- - 3.2 獲取前臺 Activity。
- - - - 3.2.1 前臺 Activity 爲 null,那麼就拿到 activities 的第一個 Activity 打 Toast,然後直接關閉 App( 殺死進程 )。
- - - - 3.2.2 前臺 Activity 爲 存在,那麼就拿 前臺 Activity 打 Toast,然後繼續。
- - 3.3 定製了一個 PendingIntent 是爲了在未來打開這個 前臺 Activity。
- - 3.4 獲取 AlarmManager,設置定時任務,再未來的 100ms 後,通過 PendingIntent 打開這個 前臺 Activity。
- - 3.5 殺死進程,等待 3.4 的定時任務執行,並打開 前臺 Activity,實現重啓 App 的效果。


4. showToast:顯示 toast。
- - 4.1 嘗試獲取 activity 的 base context。
4.1.1 拿不到的話,return。
- - 4.2 如果如果 Toast 的內容大於 60 或者有換行( \n ),那麼持續時間長。否則,短。
- - 4.3 調用 Toast.makeText(…).show() 顯示 Toast。


5. getForegroundActivity:獲取前臺顯示的 Activity,也就是獲取全部沒有 paused 的 Activity,然後從這個取第一個。


6. getActivities:獲取沒有 paused 的 Activity。
- - 6.1 反射獲取 ActivityThread 的 mActivities Field。
- - 6.2 獲取 mActivities 的值,根據版本兼容:
- - - - 6.2.1 拿不到的話,return。
- - - - 6.2.2 如果 > 4.4 && 是 ArrayMap 的話,轉。
- - - - 6.2.3 都不是的話,會返回初始化好,沒內容的 list。
- - 6.3 遍歷 mActivities 值,拿到每一個 ActivityRecord。
- - - - 6.3.1 判斷是否是 foregroundOnly:
- - - - - - 6.3.1.1 true 的話,過濾出 ActivityRecord 的 paused == true 的 ActivityRecord。
- - - - - - 6.3.1.2 false 的話,不走過濾邏輯。
- - 6.4 然後反射 3. 下來的 ActivityRecord 的 activity Field。
- - 6.5 拿到 ActivityRecord 的 activity Field 的值,添加到 list 裏。


7. updateActivity:調用 restartActivity 重啓 Activity。


8. showToastWhenPossible:如果可能的話,顯示 Toast。
- - 8.1 獲取前臺 Activity。
- - 8.2 如果拿到了,就調用 Restarter.showToast(…);如果沒拿到,進入重試方法 showToastWhenPossible(…),根據重試次數,不斷嘗試顯示 Toast。


9. showToastWhenPossible:重試顯示 Toast 方法,根據重試次數,不斷嘗試顯示 Toast。
- - 9.1 先實例化一個主線程 Handler,用於與主線程通信( 現在 Toast )。
- - 9.2 然後希望在主線程執行的任務 Runnable 內,拿到獲取前臺顯示 Activity。
- - - - 9.2.1 如果此次拿到了,直接調用 showToast(…) 方法顯示 Toast。
- - - - 9.2.2 如果此次拿不到,那麼遞歸到下次,繼續嘗試拿,一直遞歸到重試次數大於 0 爲止。



FileManager


1. checkInbox:複製資源文件 resources.ap_( 主要用於 創建資源 )。
- - 1.1 判斷 /data/data/( applicationId )/files/instant-run/inbox/resources.ap_ 是否存在。
- - 1.2 存在的話,複製到 /data/data/…/files/instant-run/left(or right)/resources.ap_ 下。


2. getDexList:獲取 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路徑集合( 主要用於 HOOK BootstrapApplication 的 ClassLoader 的 類加載機制 )。
- - 2.1 獲取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夾下,最近修改的.dex 文件的更新時間,記錄爲 newestHotswapPatch。
- - 2.2 獲取 File: /data/data/( applicationId )/files/instant-run/dex ,但不一定創建。
- - 2.3 校驗 /data/data/( applicationId )/files/instant-run/dex 文件夾:
- - - - 2.3.1 如果不存在,那麼會創建該文件夾後,將 instant-run.zip 內的所有 .dex ,加上前綴 “slice-” 複製到 /data/data/( applicationId )/files/instant-run/dex 文件夾 中。最後,獲取該文件夾內的所有文件,保存在 File[] dexFiles。
- - - - 2.3.2 如果直接存在,直接獲取 /data/data/( applicationId )/files/instant-run/dex 文件夾中的所有文件,保存在 File[] dexFiles。
- - 2.4 如果 2.3 內提取 instant-run.zip:
- - - - 2.4.1 失敗了。再次校驗 /data/data/( applicationId )/files/instant-run/dex 文件夾。遍歷所有文件,如果有一個文件的修改時間小於 APK 的修改時間,證明存在舊的 dex。將 instant-run.zip 內的所有 .dex ,加上前綴”slice-” 複製到 /data/data/( applicationId )/files/instant-run/dex 文件夾 中。然後,清空不是提取複製過來的 dex( 舊 dex )。
- - - - 2.4.2 成功了。判斷 1. 中的 dex-temp 文件夾是否存在 dex。存在的話,清空 dex-temp 文件夾。
- - 2.5 最後判斷 hotSwap 的時間是不是比 coldSwap 的時間新。實質上就是 dex-temp 文件夾內的 files 和 dex 文件夾內的 files,誰最新!如果 hotSwap 的時間比 coldSwap 的時間新,調用 Restarter.showToastWhenPossible 提示 the app is older。
- - 2.6 返回 /data/data/( applicationId )/files/instant-run/dex 的 .dex 路徑集合。


3. extractSlices: 提取 instant-run.zip 的資源( 主要用於獲取所有 dex 集合,然後實例化一個補丁 ClassLoader 進而 HOOK BootstrapApplication 的 ClassLoader 的 類加載機制 )。
- - 3.1 Class.getResourceAsStream(“/instant-run.zip”) 去加載 instant-run.zip 的資源。
- - 3.2 提取出 instant-run.zip 內的資源( 內部都是 .dex 文件 ):
- - - - 3.2.1 過濾掉 META-INF。
- - - - 3.2.2 過濾掉有 “/” 的 文件或文件夾。
- - - - 3.2.3 找出所有 .dex 文件,將其文件名加上前綴 “slice-” 保存在 Set sliceNames。
- - - - 3.2.4 再將這些 .dex 文件,加載前綴 “slice-“,複製到 /data/data/( applicationId )/files/instant-run/dex 文件夾中。
- - - - 3.2.5 校驗 /data/data/( applicationId )/files/instant-run/dex 文件夾中,是非存在不是 3.2.4 複製過來的文件。如果不是 2.4 複製過來的文件,證明是舊 “slice-” 文件,則刪除。


4. getTempDexFile: 獲取 dex-temp 文件夾下,下版本要創建的 File ( 主要用於 熱部署 )。
- - 4.1 獲取 /data/data/( applicationId )/files/instant-run/dex-temp 文件夾。
- - 4.2 校驗 dex-temp 文件夾:
- - - - 4.2.1 不存在,則創建。
- - - - 4.2.2 存在,則判斷是否要清空。清空的話,則刪除該文件夾下的所有 .dex 文件。
- - 4.3 然後遍歷 dex-temp 文件夾下的文件:
- - - - 4.3.1 截斷 “reload” 和 “.dex” 之間的 十六進制版本號。
- - - - 4.3.2 找出版本號最大的 .dex 文件。
- - 4.4 根據 4.3.2 的找出的最大版本號的基礎上,最大版本號+1,然後創建一個 “reload最大版本號.dex” 的 File 返回。


5. writeRawBytes:二進制 生成 文件 ( 所有部署 )。主要將 二進制數據 輸出爲 resources.ap_ or .dex。
6. extractZip:提取出 instant-run.zip 流 內的 .dex 文件 ( 主要用於 溫部署 ) : 這提取出的 .dex ,不帶 “slice-” 前綴。與 extractSlices 方法不同。
- - 6.1 過濾掉 META-INF。
- - 6.2 如果父路徑文件夾不存在,則創建。


7. writeDexShard:生成 dex( 主要用於 冷部署 )。
- - 7.1 校驗目錄:/data/data/( applicationId )/files/instant-run/dex。沒有,則創建。
- - 7.2 通過調用 writeRawBytes 方法,在該目錄下保存 dex 文件。


8. writeAaptResources:生成資源文件 resources 或者 resources.ap_ ( 主要用於 溫部署 ) 路徑一般爲 /data/data/( applicationId )/files/instant-run/left( right )/resources.ap_( resources )。
- - 8.1 拿到以上路徑後,創建該路徑的父文件夾。
- - 8.2 生成資源文件:
- - - - 8.2.1 如果生成 resources.ap_:
- - - - - - 8.2.1.1 如果 USE_EXTRACTED_RESOURCES = true,那麼該流爲 instant-run.zip 的數據,直接複製出內部的 dex 到 /data/data/( applicationId )/files/instant-run/left( right )目錄下。
- - - - - - 8.2.1.2 如果 USE_EXTRACTED_RESOURCES = false,生成 /data/data/( applicationId )/files/instant-run/left( right )/resources.ap_。
- - - - 8.2.2 如果生成 resources,那麼直接寫出 /data/data/( applicationId )/files/instant-run/left( right )/resources。


9. writeTempDexFile:在 dex-temp 文件夾下 生成 dex ( 主要用於 熱部署 )。


10. purgeTempDexFiles:清空 dex-temp 下的 .dex 文件 ( 用於清空 熱部署 產生的 dex-temp 文件夾中的 dex )。


11. readRawBytes:讀取 inbox/resources.ap_ ( 主要用於創建資源時,讀取 inbox/resources.ap_ )。
- - 11.1 路徑爲: /data/data/( applicationId )/files/instant-run/inbox/resources.ap_。
- - 11.2 爲了複製到 /data/data/…/files/instant-run/left(or right)/resources.ap_。



AbstractPatchesLoaderImpl


1. 遍歷所有 被修改的 類名。


2. 拼接出 ???$override 類型。


3. 通過 ClassLoader 加載 ???$override 類。


4. 反射實例化一個 ???$override 類 的實例。


5. 加載 被修改的 類。


6. 反射 被修改的 類 的 $change Field。


7. 反射獲取 被修改的 類 的 $change Field 的值。


8. 判斷 被修改的 類 的 $change Field 的值。
- - 8.1 如果存在值,反射獲取其 $obsolete Field,如果不爲 null,則設置爲 true。


9. HOOK 被修改的 類 的 $change Field = 4. 實例化好的 ???$override 類。


10. 如果這寫過程中拋出異常,返回 false。否則,返回 true。



源碼

Instant Run

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章