天天看點

59. Instant Run 筆記

1.現象

1. 打開 Instant Run,首次運作,會用到 Transform API 修改位元組碼。

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

59. Instant Run 筆記

3. 所有源碼中的方法:都會加上 $change 代碼段,$change 是 IncrementalChange 接口類型。會判斷是否為 null,不為 null,然後 調用 $change 的

access$dispatch

。參數為方法簽名字元串和方法參數數組,否則調用原邏輯。

59. Instant Run 筆記

4. 後面的 運作:會生成更新檔類 dex。輸出目錄為:Application/build/incremental/transforms/instantRun/debug/folders/4000。

59. Instant Run 筆記

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

59. Instant Run 筆記

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。

59. Instant Run 筆記

總結現象

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. 會執行 BootstrapApplication 的 attachBaseContext。

2. attachBaseContext 的

createResources(...)

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

3. attachBaseContext 的

setupClassLoaders(...)

。進行對 Application 的 ClassLoader.parent 的 Hook 操作。Hook 為插件的 DexClassLoader(IncrementalClassLoader),這裡封裝了一個 IncrementalClassLoader 就可以通過 nativeLibraryPath 路徑加載 dex。然後替換 Application 的 ClassLoader.parent 為 IncrementalClassLoader。将 PathClassLoader -> BootClassLoader 轉換為 PathClassLoader -> IncrementalClassLoader -> BootClassLoader。

4. attachBaseContext 的

createRealApplication(...)

。會通過 動态生成的 AppInfo 拿到 App 自定義的 RealApplication name。假如有自定義的 RealApplication,反射拿到自定義的 RealApplication。

5. 因為 XML 内的替換為 BootstrapApplication,App 實際上是執行個體化的是 BootstrapApplication。然而,我們期望在的行為自定義在 RealApplication。

6. 是以,會在

attachBaseContext(...)

内拿到 3. 反射得到的自定義 RealApplication。然後再反射調用自定義

RealApplication.attachBaseContext(...)

完成自定義行為。其實,也是 反射代理。

7. 執行 BootstrapApplication 的

attachBaseContext

之後,會執行到 BootstrapApplication 的

onCreate

8. BootstrapApplication 的

onCreate

主要分為三個子產品:

- - 8.1 替換 ActivityThread 内所有的 BootstrapApplication 為 RealApplication。

- - 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