Android開發者:從零開始分析InstantRun源碼

2021-02-21 享學課堂online

背景

Android Studio 2.0 中引入的 Instant Run 是 Run 和 Debug 命令的行為,可以大幅縮短應用更新的時間。儘管首次構建可能需要花費較長的時間,Instant Run 在向應用推送後續更新時則無需構建新的 APK,因此,這樣可以更快地看到更改,曾經是Android開發人員的開發的利器,現在已被廢棄,用Apply changes替代。但我們仍然可以學習它的源碼,提升自己的開發技術。

使用

instant-run加載更新有三種方式hot swap,cold swap,warm swap,當然在不同gradle版本中不一定都有這個三個功能。

Hot Swap(熱插拔)

hot swap是所有swap方式中效率最高的,不需要重新安裝和重啟,但是hot swap不會對程序中的對象重新進行初始化,也就是說某些場景需要重啟Activity才能看出具體的變更內容,Android Studio對於hot swap這種情況默認是重啟Activity的,當然你也可以到設置中去改變這一默認行為,具體路徑是 Settings -> Build, Execution, Deployment -> Instant Run -> Restart activity on code changes。Hot Swap適用的條件比較少,只有一種情況會被視為hop swap類型,就是修改一個現有方法中的代碼。

Warm Swap(溫插拔)

只有一種情況會被Android Studio視為warm swap類型,就是修改或刪除一個現有的資源文件,要求必須重啟Activity

Cold Swap(冷插拔)

Android Studio會自動記錄我們項目的每次修改,然後將修改的這部分內容打成一個dex文件發送到手機上,儘管這種swap類型仍然不需要去安裝一個全新的APK,但是為了加載這個新的dex文件,整個應用程式必須進行重啟才行。另外,cold swap的工作原理是基於multidex機制來實現的,在不引入外部library的情況下,只有5.0及以上的設備才支持multidex,5.0以下只能重新安裝。該模式在3.0時候被廢棄。

cold swap的使用場景非常多,如下

添加、刪除或修改一個註解,欄位,方法

添加一個類

修改一個類的繼承結構

修改一個類的接口實現

修改一個類的static修飾符

涉及資源文件id的改動

使用時的注意點:

如果應用的minSdkVersion小於21,可能多數的Instant Run功能會掛掉,這裡提供一個解決方法,通過product flavor建立一個minSdkVersion大於21的新分支,用來debug。

Instant Run目前只能在主進程裡運行,如果應用是多進程的,類似微信,把webView抽出來單獨一個進程,那熱、溫拔插會被降級為冷拔插。後面的版本好像就只能在主進程中了,冷插拔都沒了

在Windows下,Windows Defender Real-Time Protection可能會導致Instant Run掛掉,可用通過添加白名單列表解決。

暫時不支持Jack compiler,Instrumentation Tests,或者同時部署到多臺設備。

Instant Run的設計需要Android構建工具和Android Studio的配合,相關的源碼在兩個庫中,這兩個庫都在AOSP的源碼中,Google使用基於git開發的版本管理工具repo進行管理,全部開源的代碼及其龐大,我們只需要下載相關的git倉庫就行。

配置代理

配置代理,我的代理使用的是藍燈,有HTTP和SOCKS埠,使用HTTP速度只有幾百kb,而使用SOCKS真是快啊,這玩意這麼快在下載一些源碼庫的時候非常有用

HTTP(S)代理伺服器:127.0.0.1:54504SOCKS代理伺服器:127.0.0.1:54505git config --global http.proxy http:git config --global http.proxy socks5:

配置代理的常用命令
git config --global http.proxy socks5:git config --global --unset http.proxygit config --global --unset https.proxyenv | grep -i proxyexport http_proxy="http://127.0.0.1:1087"unset http_proxy
查看 埠所在線程 lsof -i:8080(埠)查看mac終端埠命令 netstat -AaLlnW (相當於linux的 netstat -lntp)
查看埠是否被佔用: sudo lsof -i :8080結束佔用埠的所有進程: lsof -P | grep ':8080' | awk '{print $2}' | xargs kill -9

獲取源碼

Instant Run的設計需要Android構建工具和Android Studio的配合,這個庫中有Android gradle插件的代碼,instant-run框架的代碼全部在其中的 instant-run目錄中

在3.5版本的Android Studio之後,Google使用了新的apply change架構代替了instant run,所以最新的代碼中看不到,需要切換到studio-3.2.1這個tag,最新的apply change使用時有諸多限制

需要重啟應用(不是重啟Activity)才能實現的代碼更改

某些代碼和資源更改必須在重啟應用之後才能應用,其中包括以下更改:

添加或刪除方法或欄位

更改方法籤名

更改方法或類的修飾符

更改類繼承行為

更改枚舉中的值

添加或移除資源

更改應用清單

更改原生庫(SO 文件)

所以我感覺這玩意以後可以用來在修改代碼邏輯的時候使用,使用的範圍非常有限。

知乎上也有人提問Android Studio3.5提供的Apply Changes是什麼原理?

這裡引用weishu大佬的回答,「猜測是使用JVMTI實現的,JVMTI 的全稱是 JVM Tool Interface。它是 Java 虛擬機(ART)實現的一部分,包含了虛擬機中線程 / 內存 / 類 / 方法 / 變量 / 事件 / 定時器處理等等 20 多類功能。比如:內存控制和對象獲取、線程和鎖、調試功能。對這個「Apply Changes」來說,比較重要的應該是 ClassTransform 和 ClassRedefine;它允許虛擬機在運行時動態修改類(Redefine只在9.0上實現了)。

比如說 Activity 這個 class,你可以通過此接口在字節碼層面往裡面直接添加方法/修改方法,然後虛擬機會為你重新加載這個類,之後這個被改過的類就是原來那個貨真價值的 Activity 類。所以,這個技術跟 Instant Run/Robust 編譯期字節碼編織 / ClassLoader 替換 / AndFix 方法替換那種動態修改完全不是一個層面的東西,這是 運行時動態字節碼編織

另一個需要下載的庫中有Android Studio相關的原始碼,其中可以看到AS是如何配合instant-run工作的,需要切換到studio-3.2.1這個tag

我們可以在build.gradle中添加一行代碼,查看啟動gradle的命令和全部參數

println getGradle().getStartParameter()

我在3.4.2的Android Studio中看到 projectProperties={android.optional.compilation=INSTANT_DEV,這裡就表示開啟instant-run支持了

StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[:app:assembleDebug],projectPath='null'}], excludedTaskNames=[], currentDir=F:\GitAndroid\RxDemo, searchUpwards=true, projectProperties={android.optional.compilation=INSTANT_DEV, android.injected.build.density=xxhdpi, android.injected.coldswap.mode=MULTIAPK, android.injected.build.api=28, android.injected.invoked.from.ide=true, android.injected.build.abi=arm64-v8a,armeabi-v7a,armeabi, android.injected.restrict.variant.name=debug, android.injected.restrict.variant.project=:app}, systemPropertiesArgs={}, gradleUserHomeDir=C:\Users\Jackie\.gradle, gradleHome=C:\Users\Jackie\.gradle\wrapper\dists\gradle-4.4-all\9br9xq1tocpiv8o6njlyu5op1\gradle-4.4, logLevel=LIFECYCLE, showStacktrace=INTERNAL_EXCEPTIONS, buildFile=null, initScripts=[], dryRun=false, rerunTasks=false, recompileScripts=false, offline=false, refreshDependencies=false, parallelProjectExecution=false, configureOnDemand=false, maxWorkerCount=8, buildCacheEnabled=false, interactive=false}:app:buildInfoDebugLoader

在3.6.0的Android Studio中就看不到了

StartParameter{taskRequests=[DefaultTaskExecutionRequest{args=[:app:assembleDebug],projectPath='null'}], excludedTaskNames=[], currentDir=/Users/jackie/Desktop/WorkPlace/AndroidWorkPlace/MyApplication2, searchUpwards=true, projectProperties={android.injected.build.density=xhdpi, android.injected.build.api=29, android.injected.invoked.from.ide=true, android.injected.build.abi=x86}, systemPropertiesArgs={idea.active=true, idea.version=3.6}, gradleUserHomeDir=/Users/jackie/.gradle, gradleHome=/Users/jackie/.gradle/wrapper/dists/gradle-5.6.4-all/ankdp27end7byghfw1q2sw75f/gradle-5.6.4, logLevel=LIFECYCLE, showStacktrace=INTERNAL_EXCEPTIONS, buildFile=null, initScripts=[], dryRun=false, rerunTasks=false, recompileScripts=false, offline=false, refreshDependencies=false, parallelProjectExecution=false, configureOnDemand=false, maxWorkerCount=12, buildCacheEnabled=false, interactive=false, writeDependencyLocks=false}app: 'annotationProcessor' dependencies won't be recognized as kapt annotation processors. Please change the configuration name to 'kapt' for these artifacts: 'com.alibaba:arouter-compiler:1.2.2'.

到了android.gradle插件的執行邏輯裡,會被轉成如下枚舉定義,分別表示不同的編譯類型:


public enum OptionalCompilationStep {
INSTANT_DEV, RESTART_ONLY, FULL_APK,}

Gradle4.1的Instant Run

源碼分析是基於Gradle4.1版本研究的Instant Run,但是這個版本的Instant Run功能已經削減很多了,下面還會介紹其他版本的Gradle。

運行後反編譯app-debug.apk會找到多個dex(一般是兩個),一開始是通過dex2jar-2.0和jd-gui,但是有時候有些方法無法進行反編譯而是依舊顯示初始的字節碼,很不方便閱讀,後來使用了jadx-gui進行直接反編譯apk,使用很方便,但依舊還是會有些方法還是顯示字節碼,所以我是兩者交叉著看,但是有時候甚至兩者都是字節碼,只能上網上直接找別人的博客代碼了。

因為我研究的版本是基於Gradle4.1的,僅僅剩下可憐的熱插拔和處理資源補丁,而且我還找不到intant-run.zip了,所以我找不到項目中的代碼了,不在dex文件中,原本這玩意解壓apk之後就有了,所以暫時只能在build下的目錄裡面尋找了,後面再看看這些文件時如何弄到apk當中。

InstantRunContentProvider的onCreate方法中初始化Socket

  public boolean onCreate() {    if (isMainProcess()) {        Log.i("InstantRun", "starting instant run server: is main process");      Server.create(getContext());       return true;    }     Log.i("InstantRun", "not starting instant run server: not main process");    return true;  }

然後啟動一個socket監聽Android Studio推送的消息

  private class SocketServerThread extends Thread {    private SocketServerThread() {}        public void run() {      while (true) {        try {          LocalServerSocket localServerSocket = Server.this.serverSocket;          if (localServerSocket == null)            return;           LocalSocket localSocket = localServerSocket.accept();          if (Log.isLoggable("InstantRun", 2))            Log.v("InstantRun", "Received connection from IDE: spawning connection thread");           (new Server.SocketServerReplyThread(localSocket)).run();          if (wrongTokenCount > 50) {            if (Log.isLoggable("InstantRun", 2))              Log.v("InstantRun", "Stopping server: too many wrong token connections");             Server.this.serverSocket.close();            return;          }         } catch (Throwable throwable) {          if (Log.isLoggable("InstantRun", 2))            Log.v("InstantRun", "Fatal error accepting connection on local socket", throwable);         }       }     }  }

然後在SocketServerReplyThread的run方法值接受數據並處理

private int handlePatches(List<ApplicationPatch> paramList, boolean paramBoolean, int paramInt) {    if (paramBoolean)      FileManager.startUpdate();     for (ApplicationPatch applicationPatch : paramList) {      String str = applicationPatch.getPath();      if (str.equals("classes.dex.3")) {         paramInt = handleHotSwapPatch(paramInt, applicationPatch);        continue;      }       if (isResourcePath(str))                paramInt = handleResourcePatch(paramInt, applicationPatch, str);     }     if (paramBoolean)      FileManager.finishUpdate(true);     return paramInt;  }

這裡先來看看ApplicationPatch是什麼

public static List<ApplicationPatch> read(DataInputStream paramDataInputStream) throws IOException {    int j = paramDataInputStream.readInt();    if (Log.logging != null && Log.logging.isLoggable(Level.FINE))      Log.logging.log(Level.FINE, "Receiving " + j + " changes");     ArrayList<ApplicationPatch> arrayList = new ArrayList(j);    for (int i = 0; i < j; i++) {      String str = paramDataInputStream.readUTF();      byte[] arrayOfByte = new byte[paramDataInputStream.readInt()];      paramDataInputStream.readFully(arrayOfByte);      arrayList.add(new ApplicationPatch(str, arrayOfByte));    }     return arrayList;  }

可以看到ApplicationPatch是從Socket接收到的數據輸入流中調用readFully來讀取的,關於readFully的使用while循環判斷byte數組是否已經讀滿所有數據,如果沒有讀滿則繼續讀取補充直到讀滿為止,從而改善輸入流出現空檔,造成read方法直接跳出的問題。即通過緩衝來保證數量的完整,也算是常用的一種方法。所以以後若要讀取特定長度的數據,使用readFully讀取更加安全。

1.處理熱插拔

下面來看看是如何處理熱插拔的

private int handleHotSwapPatch(int paramInt, ApplicationPatch paramApplicationPatch) {    if (Log.isLoggable("InstantRun", 2))      Log.v("InstantRun", "Received incremental code patch");     try {            String str1 = FileManager.writeTempDexFile(paramApplicationPatch.getBytes());      if (str1 == null) {        Log.e("InstantRun", "No file to write the code to");        return paramInt;      }       if (Log.isLoggable("InstantRun", 2))        Log.v("InstantRun", "Reading live code from " + str1);            String str2 = FileManager.getNativeLibraryFolder().getPath();            Class<?> clazz = Class.forName("com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, (ClassLoader)new DexClassLoader(str1, this.context.getCacheDir().getPath(), str2, getClass().getClassLoader()));      try {        if (Log.isLoggable("InstantRun", 2))          Log.v("InstantRun", "Got the patcher class " + clazz);         PatchesLoader patchesLoader = (PatchesLoader)clazz.newInstance();        if (Log.isLoggable("InstantRun", 2))          Log.v("InstantRun", "Got the patcher instance " + patchesLoader);                 String[] arrayOfString = (String[])clazz.getDeclaredMethod("getPatchedClasses", new Class[0]).invoke(patchesLoader, new Object[0]);        if (Log.isLoggable("InstantRun", 2)) {          Log.v("InstantRun", "Got the list of classes ");          int j = arrayOfString.length;          for (int i = 0; i < j; i++) {            String str = arrayOfString[i];            Log.v("InstantRun", "class " + str);          }         }                 boolean bool = patchesLoader.load();        if (!bool)          paramInt = 3;       } catch (Exception exception) {}    } catch (Throwable throwable) {      Log.e("InstantRun", "Couldn't apply code changes", throwable);      paramInt = 3;    }     return paramInt;  }public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {  public abstract String[] getPatchedClasses();    public boolean load() {    try {            for (String str : getPatchedClasses()) {        ClassLoader classLoader = getClass().getClassLoader();        Object object1 = classLoader.loadClass(str + "$override").newInstance();        Field field = classLoader.loadClass(str).getDeclaredField("$change");        field.setAccessible(true);        Object object2 = field.get(null);        if (object2 != null) {          object2 = object2.getClass().getDeclaredField("$obsolete");          if (object2 != null)            object2.set(null, Boolean.valueOf(true));         }         field.set(null, object1);        if (Log.logging != null && Log.logging.isLoggable(Level.FINE))          Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { str }));       }     } catch (Exception exception) {      if (Log.logging != null)        Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), exception);       return false;    }     return true;  }}public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl {    public static final long BUILD_ID = 1597285889481L;
public AppPatchesLoaderImpl() { }
public String[] getPatchedClasses() { return new String[]{"com.example.jackie.instantrundemo.MainActivity"}; }}

其中DexClassLoader的構造方法定義

DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)

我們的MainActivity會被修改成這樣

public class MainActivity extends AppCompatActivity {    public static final long serialVersionUID = 2158910920756968252L;
public MainActivity() { IncrementalChange var1 = $change; if(var1 != null) { Object[] var10001 = (Object[])var1.access$dispatch("init$args.([Lcom/example/jackie/instantrundemo/MainActivity;[Ljava/lang/Object;)Ljava/lang/Object;", new Object[]{null, new Object[0]}); Object[] var2 = (Object[])var10001[0]; this(var10001, (InstantReloadException)null); var2[0] = this; var1.access$dispatch("init$body.(Lcom/example/jackie/instantrundemo/MainActivity;[Ljava/lang/Object;)V", var2); } else { super(); } }
public void onCreate(Bundle savedInstanceState) { IncrementalChange var2 = $change; if(var2 != null) { var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState}); } else { super.onCreate(savedInstanceState); this.setContentView(2130968603); if(this.test(30) > 20333005) { Log.d("jackie", "==4444099994==sf=dd=ddecf==999=abc=="); } else { Log.d("jackie", "==999999999999="); }
byte b = 0; Toast.makeText(this, "hellodd4fdddd", 1).show(); Log.d("jackie", "===d=666==dd=dddd==abc==" + b); } }
public int test(int a) { IncrementalChange var2 = $change; if(var2 != null) { return ((Number)var2.access$dispatch("test.(I)I", new Object[]{this, new Integer(a)})).intValue(); } else { byte age = 100; int b = 300189 + age;
for(int i = 0; i < b + 9; ++i) { a += b; }
return 20 + a; } } MainActivity(Object[] var1, InstantReloadException var2) { String var3 = (String)var1[1]; switch(var3.hashCode()) { case -2089128195: super(); return; case 173992496: this(); return; default: throw new InstantReloadException(String.format("String switch could not find \'%s\' with hashcode %s in %s", new Object[]{var3, Integer.valueOf(var3.hashCode()), "com/example/jackie/instantrundemo/MainActivity"})); } }}

在MainActivity中做個小修改,點擊小閃電執行Instant Run,可以看到build下面文件夾4000中會找一個MainActivity$override.class和AppPatchesLoaderImpl.class。

要替換的MainActivity$override實現了IncrementalChange,從這裡面進行方法的替換,所有的方法都會被替換,因為 change值不為空

public class MainActivity$override implements IncrementalChange {    public MainActivity$override() {    }
public static Object init$args(MainActivity[] var0, Object[] var1) { Object[] var2 = new Object[]{new Object[]{var0, new Object[0]}, "android/support/v7/app/AppCompatActivity.()V"}; return var2; }
public static void init$body(MainActivity $this, Object[] var1) { AndroidInstantRuntime.setPrivateField($this, new Integer(100), MainActivity.class, "cmd"); }
public static void onCreate(MainActivity $this, Bundle savedInstanceState) { Object[] var2 = new Object[]{savedInstanceState}; MainActivity.access$super($this, "onCreate.(Landroid/os/Bundle;)V", var2); $this.setContentView(2130968603); if($this.test(30) > 20333005) { Log.d("jackie", "==44440999940==sf=dd=ddecf==999=abc=="); } else { Log.d("jackie", "==999999999999="); }
byte b = 0; AndroidInstantRuntime.setPrivateField($this, new Integer(((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "cmd")).intValue() + 100), MainActivity.class, "cmd"); Toast.makeText($this, "hellodd4fdddd", 1).show(); Log.d("jackie", "===d=666==dd=dddd==abc==" + b); }
public static int test(MainActivity $this, int a) { int ageabc = 100 + ((Number)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "cmd")).intValue(); int b = 300189 + ageabc;
for(int i = 0; i < b + 9; ++i) { a += b; }
return 20 + a; }
public Object access$dispatch(String var1, Object... var2) { switch(var1.hashCode()) { case -1227667971: return new Integer(test((MainActivity)var2[0], ((Number)var2[1]).intValue())); case -641568046: onCreate((MainActivity)var2[0], (Bundle)var2[1]); return null; case 435530788: return init$args((MainActivity[])var2[0], (Object[])var2[1]); case 1043612718: init$body((MainActivity)var2[0], (Object[])var2[1]); return null; default: throw new InstantReloadException(String.format("String switch could not find \'%s\' with hashcode %s in %s", new Object[]{var1, Integer.valueOf(var1.hashCode()), "com/example/jackie/instantrundemo/MainActivity"})); } }}

2.處理資源補丁(溫插拔)

下面來看看是如何處理資源補丁的,但是設計到資源的處理需要重啟當前界面,我們先來看看重啟App這種狀況下的邏輯

case 5:    if (authenticate(param1DataInputStream)) {      Activity activity1 = Restarter.getForegroundActivity(Server.this.context);      if (activity1 != null) {        if (Log.isLoggable("InstantRun", 2))          Log.v("InstantRun", "Restarting activity per user request");         Restarter.restartActivityOnUiThread(activity1);      }       continue;    }     return;case 1:    if (authenticate(param1DataInputStream)) {      List<ApplicationPatch> list = ApplicationPatch.read(param1DataInputStream);      if (list != null) {        bool = Server.hasResources(list);        i = param1DataInputStream.readInt();                i = Server.this.handlePatches(list, bool, i);        boolean bool1 = param1DataInputStream.readBoolean();        param1DataOutputStream.writeBoolean(true);                Server.this.restart(i, bool, bool1);      }       continue;}

我們先來看看handlePatches裡面的handleResourcePatch是如何處理資源的

private static int handleResourcePatch(int paramInt, ApplicationPatch paramApplicationPatch, String paramString) {    if (Log.isLoggable("InstantRun", 2))      Log.v("InstantRun", "Received resource changes (" + paramString + ")");     FileManager.writeAaptResources(paramString, paramApplicationPatch.getBytes());    return Math.max(paramInt, 2);  }  public static void writeAaptResources(String paramString, byte[] paramArrayOfbyte) {    File file1 = getResourceFile(getWriteFolder(false));     File file2 = file1.getParentFile();    if (!file2.isDirectory() && !file2.mkdirs()) {      if (Log.isLoggable("InstantRun", 2))        Log.v("InstantRun", "Cannot create local resource file directory " + file2);       return;    }         if (paramString.equals("resources.ap_")) {      writeRawBytes(file1, paramArrayOfbyte);      return;    }        writeRawBytes(file1, paramArrayOfbyte);  }public static File getWriteFolder(boolean paramBoolean) {    String str;    if (leftIsActive()) {      str = "right";    } else {      str = "left";    }     File file = new File(getDataFolder(), str);    if (paramBoolean && file.exists()) {      delete(file);      if (!file.mkdirs())        Log.e("InstantRun", "Failed to create folder " + file);     }     return file;  }private static File getResourceFile(File paramFile) { return new File(paramFile, "resources.ap_"); }
public static boolean writeRawBytes(File paramFile, byte[] paramArrayOfbyte) { try { BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(paramFile)); try { bufferedOutputStream.write(paramArrayOfbyte); bufferedOutputStream.flush(); return true; } finally { bufferedOutputStream.close(); } } catch (IOException iOException) { Log.wtf("InstantRun", "Failed to write file, clean project and rebuild " + paramFile, iOException); throw new RuntimeException(String.format("InstantRun could not write file %1$s, clean project and rebuild ", new Object[] { paramFile })); } }public static String writeTempDexFile(byte[] paramArrayOfbyte) { File file = getTempDexFile(); if (file != null) { writeRawBytes(file, paramArrayOfbyte); return file.getPath(); } Log.e("InstantRun", "No file to write temp dex content to"); return null; }

下面來看看 Server.this.restart(i,bool,bool1)是如何處理的,不必拘泥於細節是如何啟動的,在裡面找到一行關鍵代碼

MonkeyPatcher.monkeyPatchExistingResources(this.context, str, list);

具體實現如下

public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection<Activity> activities) {    Collection<WeakReference<Resources>> references;    if (externalResourceFile != null) {                AssetManager newAssetManager = AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);        Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});        mAddAssetPath.setAccessible(true);               if (((Integer) mAddAssetPath.invoke(newAssetManager, new Object[]{externalResourceFile})).intValue() == 0) {            throw new IllegalStateException("Could not create new AssetManager");        }        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);        mEnsureStringBlocks.setAccessible(true);                mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);        if (activities != null) {            for (Activity activity : activities) {                Resources resources = activity.getResources();                try {                    Field mAssets = Resources.class.getDeclaredField("mAssets");                    mAssets.setAccessible(true);                    mAssets.set(resources, newAssetManager);                } catch (Throwable e) {                    throw new IllegalStateException(e);                }                Resources.Theme theme = activity.getTheme();                try {                    Field ma = Resources.Theme.class.getDeclaredField("mAssets");                    ma.setAccessible(true);                    ma.set(theme, newAssetManager);                } catch (NoSuchFieldException e2) {                    Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");                    themeField.setAccessible(true);                    Object impl = themeField.get(theme);                    Field ma2 = impl.getClass().getDeclaredField("mAssets");                    ma2.setAccessible(true);                    ma2.set(impl, newAssetManager);                } catch (Throwable e3) {                    Log.e(Logging.LOG_TAG, "Failed to update existing theme for activity " + activity, e3);                }                Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");                mt.setAccessible(true);                mt.set(activity, (Object) null);                Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);                mtm.setAccessible(true);                mtm.invoke(activity, new Object[0]);                if (Build.VERSION.SDK_INT < 24) {                    Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);                    mCreateTheme.setAccessible(true);                    Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);                    Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");                    mTheme.setAccessible(true);                    mTheme.set(theme, internalTheme);                }                pruneResourceCaches(resources);            }        }                      if (Build.VERSION.SDK_INT >= 19) {            Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");            Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);            mGetInstance.setAccessible(true);            Object resourcesManager = mGetInstance.invoke((Object) null, new Object[0]);            try {                Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");                fMActiveResources.setAccessible(true);                references = ((ArrayMap) fMActiveResources.get(resourcesManager)).values();            } catch (NoSuchFieldException e4) {                Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");                mResourceReferences.setAccessible(true);                references = (Collection) mResourceReferences.get(resourcesManager);            }        } else {            Class<?> activityThread = Class.forName("android.app.ActivityThread");            Field fMActiveResources2 = activityThread.getDeclaredField("mActiveResources");            fMActiveResources2.setAccessible(true);            references = ((HashMap) fMActiveResources2.get(getActivityThread(context, activityThread))).values();        }                for (WeakReference<Resources> wr : references) {            Resources resources2 = (Resources) wr.get();            if (resources2 != null) {                try {                    Field mAssets2 = Resources.class.getDeclaredField("mAssets");                    mAssets2.setAccessible(true);                    mAssets2.set(resources2, newAssetManager);                } catch (Throwable th) {                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");                    mResourcesImpl.setAccessible(true);                    Object resourceImpl = mResourcesImpl.get(resources2);                    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");                    implAssets.setAccessible(true);                    implAssets.set(resourceImpl, newAssetManager);                }                                resources2.updateConfiguration(resources2.getConfiguration(), resources2.getDisplayMetrics());            }        }    }}

在研究過程中,本來想基於退出gradle4.1然後在gradle2.2.3重新再搞一下,後面想想不把4.1研究透再去搞2.2.3總是心有不甘,網上也基本找不到gradle 4.1的研究的,其實在2.3.0之後就沒有instant-run.zip包了,但是好像所有人都沒有提到這點,難道他們都是基於2.2.3或者更早的?

找不到的項目代碼去哪裡了

下面來看看我們的MainActivity,MyApplication等文件在去哪裡了,找到之前的Server(SocketServerThread),明白我們是從AS端接受這些文件的,接受這些dex文件後,存儲在app的cache文件中(getWriteFolder),並進行處理

private class SocketServerThread extends Thread {    private SocketServerThread() {}        public void run() {      while (true) {        try {          LocalServerSocket localServerSocket = Server.this.serverSocket;          if (localServerSocket == null)            return;                     LocalSocket localSocket = localServerSocket.accept();          if (Log.isLoggable("InstantRun", 2))            Log.v("InstantRun", "Received connection from IDE: spawning connection thread");           (new Server.SocketServerReplyThread(localSocket)).run();          if (wrongTokenCount > 50) {            if (Log.isLoggable("InstantRun", 2))              Log.v("InstantRun", "Stopping server: too many wrong token connections");             Server.this.serverSocket.close();            return;          }         } catch (Throwable throwable) {          if (Log.isLoggable("InstantRun", 2))            Log.v("InstantRun", "Fatal error accepting connection on local socket", throwable);         }       }     }  }

下面可以使用我們一開始下載的源碼,在instant-run下面的intant-run-client,InstantClient類中,將文件發送到設備中

private void transferBuildIdToDevice(@NonNull IDevice device, @NonNull String buildId) {    try {        String remoteIdFile = getDeviceIdFolder(mPackageName);                File local = File.createTempFile("build-id", "txt");        local.deleteOnExit();        Files.write(buildId, local, Charsets.UTF_8);        device.pushFile(local.getPath(), remoteIdFile);    } catch (IOException ioe) {        mLogger.warning("Couldn't write build id file: %s", ioe);    } catch (AdbCommandRejectedException | TimeoutException | SyncException e) {        mLogger.warning("%s", Throwables.getStackTraceAsString(e));    }}

我們回看之前的handleHotSwapPatch方法中讀取文件的方式,可以看到在cache文件中

private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) {    ···    try {        String dexFile = FileManager.writeTempDexFile(patch.getBytes());        ···        String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();        DexClassLoader dexClassLoader = new DexClassLoader(dexFile,                context.getCacheDir().getPath(), nativeLibraryPath,                getClass().getClassLoader());

使用InstantRun更新的文件去哪裡了

下面來看看我們的 MainActivity$override, MyApplication$override,AppPatchesLoaderImpl等文件在去哪裡了,我們進入應用內部的 /data/data/com.example.jackie.instantrundemo/files/instant-run/dex-temp中會發現一個reload0x0000.dex文件,裡面就有提供更新的內容,instant-run裡面中的right是用於存儲resource.ap_。

其他一些Gradle版本Gradle2.2.3

在當前版本中,我們可以看到instant-run.zip包,裡面包含的項目的代碼和要替換的代碼。解壓後可以看到AndroidManifest.xml文件,從 AndroidManifest.xml 中我們看到了 MyApplication 被 BootstrapApplication 替代,那麼我們可以想像當 Application 為 Instant-run 自己的時,那麼它至少可以像加載插件一樣在應用啟動的時候(程序入口)加載替換自己的dex和資源文件,從而達到修改運行程序的目的。

@Overrideprotected void attachBaseContext(Context context) {                    if (!AppInfo.usingApkSplits) {        String apkFile = context.getApplicationInfo().sourceDir;        long apkModified = apkFile != null ? new File(apkFile).lastModified() : 0L;                createResources(apkModified);                        setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);    }        createRealApplication();
super.attachBaseContext(context);
if (realApplication != null) { try { Method attachBaseContext = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); attachBaseContext.setAccessible(true); attachBaseContext.invoke(realApplication, context); } catch (Exception e) { throw new IllegalStateException(e); } }}

該方法的主要目的在於,創建自定義的ClassLoader和真正的Application實例。而 BootstrapApplication 只起到一個殼子的作用。

替換Application的時候我們可以看看MonkeyPatcher中是如何替換的

public class MonkeyPatcher {    public static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication, String externalResourceFile) {        Class<?> activityThread;        Class<?> loadedApkClass;        try {                        activityThread = Class.forName("android.app.ActivityThread");            Object currentActivityThread = getActivityThread(context, activityThread);            Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");            mInitialApplication.setAccessible(true);                        Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);            if (realApplication != null && initialApplication == bootstrap) {                mInitialApplication.set(currentActivityThread, realApplication);            }                        if (realApplication != null) {                Field mAllApplications = activityThread.getDeclaredField("mAllApplications");                mAllApplications.setAccessible(true);                List<Application> allApplications = (List) mAllApplications.get(currentActivityThread);                for (int i = 0; i < allApplications.size(); i++) {                    if (allApplications.get(i) == bootstrap) {                        allApplications.set(i, realApplication);                    }                }            }            loadedApkClass = Class.forName("android.app.LoadedApk");        } catch (ClassNotFoundException e) {            loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");        } catch (Throwable e2) {            IllegalStateException illegalStateException = new IllegalStateException(e2);        }        Field mApplication = loadedApkClass.getDeclaredField("mApplication");        mApplication.setAccessible(true);        Field mResDir = loadedApkClass.getDeclaredField("mResDir");        mResDir.setAccessible(true);        Field mLoadedApk = null;        try {            mLoadedApk = Application.class.getDeclaredField("mLoadedApk");        } catch (NoSuchFieldException e3) {        }                        for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {            Field field = activityThread.getDeclaredField(fieldName);            field.setAccessible(true);            for (Entry<String, WeakReference<?>> entry : ((Map) field.get(currentActivityThread)).entrySet()) {                Object loadedApk = ((WeakReference) entry.getValue()).get();                if (loadedApk != null && mApplication.get(loadedApk) == bootstrap) {                                                            if (realApplication != null) {                        mApplication.set(loadedApk, realApplication);                    }                                        if (externalResourceFile != null) {                        mResDir.set(loadedApk, externalResourceFile);                    }                                        if (!(realApplication == null || mLoadedApk == null)) {                        mLoadedApk.set(realApplication, loadedApk);                    }                }            }        }    }
public static Object getActivityThread(Context context, Class<?> activityThread) { if (activityThread == null) { try { activityThread = Class.forName("android.app.ActivityThread"); } catch (Throwable th) { return null; } } Method m = activityThread.getMethod("currentActivityThread", new Class[0]); m.setAccessible(true); Object currentActivityThread = m.invoke(null, new Object[0]); if (currentActivityThread != null || context == null) { return currentActivityThread; } Field mLoadedApk = context.getClass().getField("mLoadedApk"); mLoadedApk.setAccessible(true); Object apk = mLoadedApk.get(context); Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread"); mActivityThreadField.setAccessible(true); return mActivityThreadField.get(apk); } }

獲取ActivityThread實例

先獲取ActivityThread的靜態變量sCurrentActivityThread;

否則獲取Application對象的成員變mLoadedApk的成員對象mActivityThread;

替換ActivityThread的mInitialApplication為realApplication

替換ActivityThread的mAllApplications中的所有的BootstrapApplication為realApplication

替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為realApplication。

替換realApplication中的mLoadedApk為BootstrapApplication的MLoadedApk

AndroidMainifest文件中的Application如何被替換

首先我們切換到之前下載的git庫中的base/build-system目錄下面,然後切換到tag gradle_2.2.2分支下,然後全局搜索該Application值

static final String BOOTSTRAP_APPLICATION        = "com.android.tools.fd.runtime.BootstrapApplication";

被調用的地方

@NonNullprivate static XmlDocument instantRunReplacement(XmlDocument document) {    Optional<XmlElement> applicationOptional = document            .getByTypeAndKey(ManifestModel.NodeTypes.APPLICATION, null );    if (applicationOptional.isPresent()) {        XmlElement application = applicationOptional.get();        Attr nameAttribute = application.getXml().getAttributeNodeNS(                SdkConstants.ANDROID_URI, "name");
if (nameAttribute != null) { String originalAppName = nameAttribute.getValue(); if (BOOTSTRAP_APPLICATION.equals(originalAppName)) { return document; } application.getXml().setAttribute(SdkConstants.ATTR_NAME, originalAppName); application.getXml().setAttributeNS( SdkConstants.ANDROID_URI, nameAttribute.getName(), BOOTSTRAP_APPLICATION); } else { application.getXml().setAttributeNS( SdkConstants.ANDROID_URI, SdkConstants.ANDROID_NS_NAME_PREFIX + SdkConstants.ATTR_NAME, BOOTSTRAP_APPLICATION); } } return document.reparse();}

調用鏈ManifestMerger2 ->MergeManifests ->TaskManager.createMergeLibManifestsTask ->LibraryTaskManager.createTasksForVariantData ->LibraryPlugin.createTaskManager,LibraryPlugin是一個gradle Plugin插件,會被我們自動註冊處理這些東西。

小結

修改原始碼,每個類增加 $change 欄位;

替換 Application ;

創建自己的類加載器,修改正常的類加載器的加載順序;

開啟 Socket 監聽 AndroidStudio 推送的消息;

處理消息(熱、溫、冷)

熱:給類的 $change 欄位賦值,改變運行邏輯;

溫:替換加載新的資源,重啟當前 Activity 生效;

冷:寫入新的 dex 文件,重新加載新的 dex;

Gradle2.3.0

去掉了 BootstrapApplication 替換,直接啟動一個 InstantRunService 用來啟動 Socket 與 AndroidStudio 進行信息傳遞;

去掉了所謂的冷啟動( handleColdSwapPatch),需要冷啟動的時候直接進行碎片安裝重啟不就好了;

啟動InstantRunService是通過adb命令啟動

Instant Run的一些問題

調用Instant Run後,殺死進程並重啟APP後,有時候這些新的的修改並沒有在當前的APP中。

FileManager.class的一些方法
public class FileManager {    public static final String CLASSES_DEX_SUFFIX = ".dex";    private static final String FILE_NAME_ACTIVE = "active";    private static final String FOLDER_NAME_LEFT = "left";    private static final String FOLDER_NAME_RIGHT = "right";    private static final String RELOAD_DEX_PREFIX = "reload";    private static final String RESOURCE_FILE_NAME = "resources.ap_";    private static final String RESOURCE_FOLDER_NAME = "resources";        public static String writeTempDexFile(byte[] bytes) {        File file = getTempDexFile();        if (file != null) {            writeRawBytes(file, bytes);            return file.getPath();        }        Log.e(BootstrapApplication.LOG_TAG, "No file to write temp dex content to");        return null;    }        public static File getTempDexFile() {                File dataFolder = getDataFolder();                File dexFolder = getTempDexFileFolder(dataFolder);        if (dexFolder.exists()) {            if (!sHavePurgedTempDexFolder) {                                purgeTempDexFiles(dataFolder);            }        } else if (dexFolder.mkdirs()) {            sHavePurgedTempDexFolder = true;        } else {            Log.e(BootstrapApplication.LOG_TAG, "Failed to create directory " + dexFolder);            return null;        }        File[] files = dexFolder.listFiles();        int max = -1;        if (files != null) {            for (File file : files) {                String name = file.getName();                                if (name.startsWith(RELOAD_DEX_PREFIX) && name.endsWith(CLASSES_DEX_SUFFIX)) {                    try {                                                int version = Integer.decode(name.substring(RELOAD_DEX_PREFIX.length(), name.length() - CLASSES_DEX_SUFFIX.length())).intValue();                        if (version > max) {                            max = version;                        }                    } catch (NumberFormatException e) {                    }                }            }        }                File file2 = new File(dexFolder, String.format("%s0x%04x%s", new Object[]{RELOAD_DEX_PREFIX, Integer.valueOf(max + 1), CLASSES_DEX_SUFFIX}));        if (!Log.isLoggable(BootstrapApplication.LOG_TAG, 2)) {            return file2;        }        Log.v(BootstrapApplication.LOG_TAG, "Writing new dex file: " + file2);        return file2;    }}

InstantTransfrom

InstantRun是通過ASM插件來給每個方法前插入change,然後在運行instant-run的時候進行替換,地址在這,我們可以參考他們的實現來進行我們自己的熱修復框架開發。

build下面的文件

com.android.build.gradle.tasks.ir.FastDeployRuntimeExtractorTask這個類負責從gradle插件的jar包中把instant-run-server.jar提取出來放到build目錄下面

@TaskActionpublic void extract() throws IOException {    URL fdrJar =            FastDeployRuntimeExtractorTask.class.getResource(                    "/instant-run/instant-run-server.jar");    if (fdrJar == null) {        throw new RuntimeException("Couldn't find Instant-Run runtime library");    }  URLConnection urlConnection = fdrJar.openConnection();  ···}

總結

從零開始分析InstantRun確實遇到不少問題:

git代理和全局代理,以後再以後下載一些第三方源碼時會大大提高效率;

反編譯使用不同的工具交叉查看

下載了不同版本的AS,ndk配置問題,graldle下載緩慢問題(3.3解決)

不同版本Gradle,InstantRun機制不一樣,原先使用instant-run.zip,然後被移除,直接弄到cache和dex-temp中,最終AS3.5廢棄而使用Apply Changes

理解的Instant Run,感覺熱修復也沒那麼難啊,但不同框架使用不用的原理需要繼續研究。

參考文章

https://blog.csdn.net/qq_33487412/article/details/78458000

https://juejin.im/post/6844903952287268877

https://developer.android.google.cn/studio/run/index.html?authuser=19#apply-changes

https://www.zhihu.com/question/309772986

https://blog.csdn.net/guolin_blog/article/details/51271369

https://github.com/stven0king/InstantRun-ApkParse(非常完整)

相關焦點

  • Android Studio 2.2.1關於InstantRun的致命Bug
    E/InstantRun: Failed to create directory /data/data/com.example.csy.myapplication/files/instant-run/dex10-18 16:29:31.726 26030-26030/?
  • Android系統啟動源碼分析
    羅享 的博客地址:http://blog.csdn.net/ynztlxdeai解讀Android的源碼可以讓我們更好的學習系統的優秀源碼,以及google工程師對於一個程序的是如何實現的,從源碼的分析也可以強化我們的編程思想。
  • WorkManager 流程分析和源碼解析 | 開發者說·DTalk
    在此之前,從 6.0 開始 Google 引入了 Doze 機制,並且在之後的幾個版本對 Android 的後臺行為及廣播的限制越來越嚴格。在 Android 8.0 時 Google 官方推薦開發者使用 JobScheduler 來代替 Service+Broadcast 的後臺任務管理方式。
  • Android汙點分析工具flowdroid源碼簡析
    1、準備a)下載相關源碼(包括soot、heros、jasmin、soot-infoflow、soot-infoflow、soot-infoflow-android)到同一文件夾中,使用eclipse將源碼依次導入就完成了整體項目的導入,儘量使用最新版eclipse,如果版本太老,導入後可能會出現各種問題;完成導入後整體項目結構如下所示:
  • Instant App 常見問題官方指南 | Android 開發者 FAQ Vol.6
    您必須使用符合 com.android.application 構建規則的模塊來構建您的可安裝應用,而當您構建免安裝應用時則需要使用符合 com.android.instantapp 構建規則的模塊。想要了解更多信息,請參閱 「項目結構」 :(https://developer.android.google.cn/topic/instant-apps/getting-started/structure.html#structure_of_a_basic_instant_app)Q:我能獨立編譯可安裝與免安裝應用嗎?
  • Android源碼閱讀技巧--查找開發者選項中顯示觸摸操作源碼
    作者丨啊源股來源:https://www.cnblogs.com/songsongman/p/11504744.html在開發者模式下
  • Android編譯及Dex過程源碼分析
    0x01 從BasePlugin入口說起1.1 BasePlugin入口Android Studio項目是基於Gradle構建的,module的build.gradle文件首部都會聲明apply plugin: 『com.android.application』或apply plugin: 『com.android.library』,對應到源碼是/build-system/gradle
  • Android APK安裝過程源碼解析
    本篇來自 lujianxin_ad 的投稿,給大家帶來了APK安裝的源碼分析,希望對大家有所幫助! lujianxin_ad 的博客地址:http://blog.csdn.net/qq_27540131本文關於 android 系統是如何安裝我們apk的流程分析。
  • Android Studio 2.0,先睹為快吧
    下載Android Studio 副本,請訪問這裡:tools.android.com/download/studio/canaryAndroid Studio 的「即時運行功能」允許您快速查看在您的設備或模擬器上運行的變化。開始使用很容易。如果您利用 Android Studio 2.0 創建一個新項目,那麼您的項目已經設置好了。
  • Android 上百實例源碼分析以及開源分析
    進入正題了,以下是大量的Android 的實例源碼分析(含 開源項目 ----都在下載包中),我一一詳解,簡單的或重複的技術就直接略過了,謝謝大家的指點。 1、360新版特性界面原始碼實現了360新版特性界面的效果,主要涉及到Qt的一些事件處理與自定義控制項。但源碼好像是c++。
  • 基於Instant Run思想的HotFix方案實現
    點擊上方「安卓巴士Android開發者門戶」即可關注!
  • 深入理解 SpringBoot 啟動機制:run()啟動源碼全過程分析
    核心原理:初始化流程(run方法)。那麼,這篇我們繼續往下面分析其核心 run 方法。二、SpringApplication 實例 run 方法運行過程下面繼續來分析SpringApplication對象的run方法實現過程以及運行原理。
  • 破解第一個Android程序
    在實際分析中,還可以使用IDA Pro直接分析APK文件,使用dex2jar與jd-gui配合進行Java源碼級的分析等。這些分析方法會在本書後面的章節中詳細介紹。反編譯APK文件ApkTool是一款常用的跨平臺APK文件反編譯工具,可以在Windows、macOS和Ubuntu平臺上使用。
  • Flutter源碼剖析(一):源碼獲取與構建
    概述 本文介紹了Flutter源碼的獲取與構建,後面會另有文章介紹Flutter源碼的版本管理、開發環境搭建等主題。準備工作 Flutter源碼分為兩個部分:flutter/flutter[1]是框架層,為開發者提供各種接口,主要是dart代碼。
  • Google Play Instant 正式上線,Cocos Creator 同步支持
    為了使即時應用和遊戲更容易構建,作為 Google Play Instant 技術接入合作方,在今年 5 月,Cocos 引擎就開始和 Google、Cocos 社區的 Beta 用戶一起做集成測試。目前,各項工作都已經結束,該功能正式發布。
  • 【再出發】Android11:Mac環境如何下載Android源碼?
    前言前面我們講過如何在linux虛擬機上下載源碼,這篇文章將告訴大家如何在Mac系統上下載源碼下載查看源碼的好處在於:1.可以藉助工具方便的跳轉2.可以在源碼中添加自己的批註方便日後查看,>3.可以編譯一份對應的系統鏡像來調試使用4.可以對照源碼梳理相關的調用流程5.可以方便的切換各個分支的Android源碼下面正文開始!
  • 一步步搞定Android換膚框架 從Debug 7.1.1源碼開始
    本篇博客的的demo中的build.gradle配置是:compileSdkVersion 25buildToolsVersion "25.0.0"然後進行debug源碼時,也是基於Android7.1.1的源碼進行的。後面一大波debug來襲,請留神。這篇文章我們會從最簡單的XML文件開始聊起。
  • android View繪製源碼分析(上)
    View繪製的源碼分析 ,它的三大流程都是在ViewRootImpl中完成的,從ViewRootImpl中的performTraversals開始,有三個方法performMeasure, performLayout, prformDraw分別對measure,layout,draw三個方法。
  • WorkManager流程分析和源碼解析
    本篇文章轉載自狐友技術團隊的博客,帶大家從源碼角度分析WorkManager的流程,相信會對大家有所幫助!同時也感謝作者貢獻的精彩文章!WorkManager統一了對於Android後臺任務的管理。在此之前,從6.0開始Google引入了Doze機制,並且在之後的幾個版本對Android的後臺行為及廣播的限制越來越嚴格。
  • Android系統層Watchdog機制源碼分析
    大體原理是, 在系統運行以後啟動了看門狗的計數器,看門狗就開始自動計數,如果到了一定的時間還不去清看門狗,那麼看門狗計數器就會溢出從而引起看門狗中斷,造成系統復位。註:本文以Android6.0代碼講解Android系統的Watchdog源碼路徑在此:frameworks/base/services/core/java/com/android/server/Watchdog.javaWatchdog的初始化位於SystemServer.