背景
Android Studio 2.0 中引入的 Instant Run 是 Run 和 Debug 命令的行為漩蟆,可以大幅縮短應用更新的時間。盡管首次構建可能需要花費較長的時間,Instant Run 在向應用推送后續(xù)更新時則無需構建新的 APK系宫,因此维费,這樣可以更快地看到更改种蝶,曾經是Android開發(fā)人員的開發(fā)的利器,現在已被廢棄靡羡,用Apply changes替代系洛。但我們仍然可以學習它的源碼,提升自己的開發(fā)技術略步。
使用
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文件發(fā)送到手機上檐盟,盡管這種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開發(fā)的版本管理工具repo進行管理襟己,全部開源的代碼及其龐大引谜,我們只需要下載相關的git倉庫就行。
配置代理
配置代理擎浴,我的代理使用的是藍燈员咽,有HTTP和SOCKS端口,使用HTTP速度只有幾百kb退客,而使用SOCKS真是快啊,這玩意這么快在下載一些源碼庫的時候非常有用
HTTP(S)代理服務器:127.0.0.1:54504
SOCKS代理服務器:127.0.0.1:54505
git config --global http.proxy http://127.0.0.1:1080(本機的端口)链嘀,可能會遇到Time out萌狂,使用下面那個
git config --global http.proxy socks5://127.0.0.1:54505
配置代理的常用命令
//設置git代理
git config --global http.proxy socks5://127.0.0.1:54505
//取消git代理:
git config --global --unset http.proxy
git config --global --unset https.proxy
//查看全局的所有代理
env | grep -i proxy
//只在當前shell窗口配置,也可以在配置文件中配置怀泊,那就是永久配置了
export http_proxy="http://127.0.0.1:1087"
//取消本機http代理
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
目錄中
git clone https://android.googlesource.com/platform/tools/base
在3.5版本的Android Studio之后霹琼,Google使用了新的apply change架構代替了instant run务傲,所以最新的代碼中看不到,需要切換到studio-3.2.1這個tag枣申,最新的apply change使用時有諸多限制
- 您使用調試構建變體來構建應用的 APK售葡。
- 您將應用部署到搭載 Android 8.0(API 級別 26)或更高版本的目標設備或模擬器上。
需要重啟應用(不是重啟Activity)才能實現的代碼更改
某些代碼和資源更改必須在重啟應用之后才能應用忠藤,其中包括以下更改:
- 添加或刪除方法或字段
- 更改方法簽名
- 更改方法或類的修飾符
- 更改類繼承行為
- 更改枚舉中的值
- 添加或移除資源
- 更改應用清單
- 更改原生庫(SO 文件)
所以我感覺這玩意以后可以用來在修改代碼邏輯的時候使用挟伙,使用的范圍非常有限。
知乎上也有人提問Android Studio3.5提供的Apply Changes是什么原理模孩?
這里引用weishu大佬的回答尖阔,“猜測是使用JVMTI實現的,JVMTI 的全稱是 JVM Tool Interface榨咐。它是 Java 虛擬機(ART)實現的一部分介却,包含了虛擬機中線程 / 內存 / 類 / 方法 / 變量 / 事件 / 定時器處理等等 20 多類功能。比如:內存控制和對象獲取块茁、線程和鎖齿坷、調試功能。
對這個「Apply Changes」來說,比較重要的應該是 ClassTransform 和 ClassRedefine胃夏;它允許虛擬機在運行時動態(tài)修改類(Redefine只在9.0上實現了)轴或。比如說 Activity 這個 class,你可以通過此接口在字節(jié)碼層面往里面直接添加方法/修改方法仰禀,然后虛擬機會為你重新加載這個類照雁,之后這個被改過的類就是原來那個貨真價值的 Activity 類。所以答恶,這個技術跟 Instant Run/Robust 編譯期字節(jié)碼編織 / ClassLoader 替換 / AndFix 方法替換那種動態(tài)修改完全不是一個層面的東西饺蚊,這是 運行時動態(tài)字節(jié)碼編織!
另一個需要下載的庫中有Android Studio相關的源代碼悬嗓,其中可以看到AS是如何配合instant-run工作的污呼,需要切換到studio-3.2.1這個tag
git clone https://android.googlesource.com/platform/tools/adt/idea
我們可以在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插件的執(zhí)行邏輯里燕酷,會被轉成如下枚舉定義,分別表示不同的編譯類型:
//源碼路徑 gradle3.0.0版本
//Users/jackie/Desktop/WorkPlace/InstantRun/base/build-system/builder-周瞎、model/src/main/java/com/android/builder/model/OptionalCompilationStep.java
/**
* enum describing possible optional compilation steps. This can be used to turn on java byte code
* manipulation in order to support instant reloading, or profiling, or anything related to
* transforming java compiler .class files before they are processed into .dex files.
*/
public enum OptionalCompilationStep {
/**
* presence will turn on the InstantRun feature.
*/
INSTANT_DEV,
/**
* Force rebuild of cold swap artifacts.
*
* <p>Dex files and/or resources.ap_ for ColdswapMode.MULTIDEX and some split APKs for
* ColdswapMode.MULTIAPK.
*/
RESTART_ONLY,
/**
* Force rebuild of fresh install artifacts.
*
* <p>A full apk for ColdswapMode.MULTIDEX and all the split apks for ColdswapMode.MULTIAPK.
*/
FULL_APK,
}
Gradle4.1的Instant Run
源碼分析是基于Gradle4.1版本研究的Instant Run苗缩,但是這個版本的Instant Run功能已經削減很多了,下面還會介紹其他版本的Gradle
運行后反編譯app-debug.apk會找到多個dex(一般是兩個)声诸,一開始是通過dex2jar-2.0和jd-gui酱讶,但是有時候有些方法無法進行反編譯而是依舊顯示初始的字節(jié)碼,很不方便閱讀彼乌,后來使用了jadx-gui進行直接反編譯apk泻肯,使用很方便,但依舊還是會有些方法還是顯示字節(jié)碼慰照,所以我是兩者交叉著看灶挟,但是有時候甚至兩者都是字節(jié)碼,只能上網上直接找別人的博客代碼了毒租。
因為我研究的版本是基于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監(jiān)聽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")) { //如果有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循環(huán)判斷byte數組是否已經讀滿所有數據剃允,如果沒有讀滿則繼續(xù)讀取補充直到讀滿為止沛简,從而改善輸入流出現空檔齐鲤,造成read方法直接跳出的問題。即通過緩沖來保證數量的完整椒楣,也算是常用的一種方法给郊。所以以后若要讀取特定長度的數據,使用readFully讀取更加安全捧灰。
1.處理熱插拔
下面來看看是如何處理熱插拔的
//處理熱插拔
private int handleHotSwapPatch(int paramInt, ApplicationPatch paramApplicationPatch) {
if (Log.isLoggable("InstantRun", 2))
Log.v("InstantRun", "Received incremental code patch");
try {
//創(chuàng)建或獲取“data/data/applicationid/files/instant-run/dex”文件路徑
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();
//反射構造AppPatchesLoaderImpl實例
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);
//獲取熱修復所要替換的類的classname
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);
}
}
//執(zhí)行AppPatchesLoaderImpl的load方法進行類修復
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;
}
//AppPatchesLoaderImpl實現抽象類AbstractPatchesLoaderImpl淆九,重寫getPatchedClasses方法,重寫方法中有提供需要熱更新的類
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;
}
}
//AppPatchesLoaderImpl繼承AbstractPatchesLoaderImpl,真正的實現類炭庙,這里我們只要求change的MainActivity這個類里面的方法
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的構造方法定義
//dexPath:被解壓的apk路徑,不能為空煌寇。
//optimizedDirectory:解壓后的.dex文件的存儲路徑焕蹄,不能為空。這個路徑強烈建議使用應用程序的私有路徑阀溶,不要放到sdcard上腻脏,否則代碼容易被注入攻擊。
//libraryPath:os庫的存放路徑银锻,可以為空永品,若有os庫,必須填寫徒仓。
//parent:父類加載器腐碱,一般為context.getClassLoader(),使用當前上下文的類加載器誊垢。
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中做個小修改喂走,點擊小閃電執(zhí)行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;
}
//替換相對應的方法,最后兩個方法我估計是前面MainActivity類里面的copy
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這種狀況下的邏輯
//Server類的handle()
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();
//處理acitivity或者把資源寫到文件中
i = Server.this.handlePatches(list, bool, i);
boolean bool1 = param1DataInputStream.readBoolean();
param1DataOutputStream.writeBoolean(true);
//重啟Activity
Server.this.restart(i, bool, bool1);
}
continue;
}
我們先來看看handlePatches里面的handleResourcePatch是如何處理資源的
//Server.class
//處理資源補丁
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);
}
//FileManager.class
//writeAaptResources
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;
}
//把內容寫到resources.ap_資源中
if (paramString.equals("resources.ap_")) {
writeRawBytes(file1, paramArrayOfbyte);
return;
}
//把raw資源寫入
writeRawBytes(file1, paramArrayOfbyte);
}
//getWriteFolder
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;
}
//getResourceFile
private static File getResourceFile(File paramFile) { return new File(paramFile, "resources.ap_"); }
//writeRawBytes
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)
是如何處理的,不必拘泥于細節(jié)是如何啟動的睡汹,在里面找到一行關鍵代碼
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
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);
//將當前的資源文件路徑添加到AssetManager中
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);
//進行資源初始化StringBlock對象
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);
}
}
//獲取當前JVM中的ResourcesManager的final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources
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();
}
//循環(huán)便利當前Resources肴甸,將其成員變量mAssets指向自定義的newAssetManager
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;
//接受AS發(fā)過來的文件
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類中碰辅,將文件發(fā)送到設備中
private void transferBuildIdToDevice(@NonNull IDevice device, @NonNull String buildId) {
try {
String remoteIdFile = getDeviceIdFolder(mPackageName);
//noinspection SSBasedInspection This should work
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
中會發(fā)現一個reload0x0000.dex文件介时,里面就有提供更新的內容没宾,instant-run里面中的right是用于存儲resource.ap_。
其他一些Gradle版本
Gradle2.2.3
在當前版本中沸柔,我們可以看到instant-run.zip包循衰,里面包含的項目的代碼和要替換的代碼。解壓后可以看到AndroidManifest.xml文件褐澎,從 AndroidManifest.xml
中我們看到了MyApplication
被 BootstrapApplication
替代会钝,那么我們可以想象當 Application
為 Instant-run
自己的時,那么它至少可以像加載插件一樣在應用啟動的時候(程序入口)加載替換自己的dex和資源文件工三,從而達到修改運行程序的目的迁酸。
@Override
protected void attachBaseContext(Context context) {
// As of Marshmallow, we use APK splits and don't need to rely on
// reflection to inject classes and resources for coldswap
//noinspection PointlessBooleanExpression
//是否使用了apk分裂安裝
if (!AppInfo.usingApkSplits) {
String apkFile = context.getApplicationInfo().sourceDir;
long apkModified = apkFile != null ? new File(apkFile).lastModified() : 0L;
//判斷資源resource.ap_是否進行了修改,將其路徑保存在externalResourcePath
createResources(apkModified);
//創(chuàng)建classloade
//delegateClassLoader->PathClassLoader->IncrementalClassLoader->BootClassLoader
setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
}
//創(chuàng)建正真的Application
createRealApplication();
// This is called from ActivityThread#handleBindApplication() -> LoadedApk#makeApplication().
// Application#mApplication is changed right after this call, so we cannot do the monkey
// patching here. So just forward this method to the real Application instance.
super.attachBaseContext(context);
if (realApplication != null) {
try {
Method attachBaseContext =
ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
attachBaseContext.setAccessible(true);
//執(zhí)行自己的Application的attachBaseContext方法
attachBaseContext.invoke(realApplication, context);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
該方法的主要目的在于俭正,創(chuàng)建自定義的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實例
activityThread = Class.forName("android.app.ActivityThread");
Object currentActivityThread = getActivityThread(context, activityThread);
Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
mInitialApplication.setAccessible(true);
//替換ActivityThread的mInitialApplication成員變量
Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
if (realApplication != null && initialApplication == bootstrap) {
mInitialApplication.set(currentActivityThread, realApplication);
}
//替換ActivityThread的mAllApplications隊列中的BootstrapApplication為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) {
}
//final ArrayMap<String, WeakReference<LoadedApk>> mPackages
//final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages
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) {
//將ActivityThread的mPackages|mResourcePackages的LoadedApk
//LoadedApk的成員變量mApplication的BootstrapApplication替換為realApplication
if (realApplication != null) {
mApplication.set(loadedApk, realApplication);
}
//LoadedApk的成員變量mResDir替換為externalResourceFile
if (externalResourceFile != null) {
mResDir.set(loadedApk, externalResourceFile);
}
//將realApplication的mLoadedApk替換為BootstrapApplication的mLoadedApk
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;
}
}
//獲取ActivityThread的靜態(tài)變量sCurrentActivityThread
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;
}
//獲取BootstrapApplication的mLoadedApk
Field mLoadedApk = context.getClass().getField("mLoadedApk");
mLoadedApk.setAccessible(true);
Object apk = mLoadedApk.get(context);
Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
//返回mLoadedApk的成員變量mActivityThread
return mActivityThreadField.get(apk);
}
/***部分代碼省略***/
}
-
獲取ActivityThread實例
- 先獲取ActivityThread的靜態(tài)變量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值
//ManifestMerger2.class
static final String BOOTSTRAP_APPLICATION
= "com.android.tools.fd.runtime.BootstrapApplication";
被調用的地方
//如方法定義一樣嫩挤,替換為instantrun的APPLICATION
@NonNull
private static XmlDocument instantRunReplacement(XmlDocument document) {
Optional<XmlElement> applicationOptional = document
.getByTypeAndKey(ManifestModel.NodeTypes.APPLICATION, null /* keyValue */);
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
函喉; - 創(chuàng)建自己的類加載器,修改正常的類加載器的加載順序荣月;
- 開啟
Socket
監(jiān)聽AndroidStudio
推送的消息管呵; - 處理消息(熱、溫哺窄、冷)
- 熱:給類的
$change
字段賦值捐下,改變運行邏輯; - 溫:替換加載新的資源萌业,重啟當前
Activity
生效坷襟; - 冷:寫入新的
dex
文件,重新加載新的dex
;
- 熱:給類的
Gradle2.3.0
去掉了 BootstrapApplication
替換生年,直接啟動一個 InstantRunService
用來啟動 Socket
與 Android Studio
進行信息傳遞婴程;
去掉了所謂的冷啟動(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() {
//“data/data/applicationid/files/instant-run”
File dataFolder = getDataFolder();
//“data/data/applicationid/files/instant-run/dex-temp”
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();
//文件以“reload"開頭衙四,以".dex"結尾
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) {
}
}
}
}
//創(chuàng)建版本號+1的reloadxxx.dex文件患亿,比如reload0x0000.dex
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的時候進行替換,地址在這步藕,我們可以參考他們的實現來進行我們自己的熱修復框架開發(fā)惦界。
build下面的文件
com.android.build.gradle.tasks.ir.FastDeployRuntimeExtractorTask
這個類負責從gradle插件的jar包中把instant-run-server.jar提取出來放到build目錄下面
// we could just extract the instant-runtime jar and place it as a stream once we
// don't have to deal with AppInfo replacement.
@TaskAction
public 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士骤,感覺熱修復也沒那么難啊范删,但不同框架使用不用的原理需要繼續(xù)研究。
參考文章
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