上一篇博客我們介紹了InstantRun
的初始化邏輯著蛙,接下來我們來看下在運(yùn)行時(shí)階段,InstantRun
是如何加載修改的代碼的耳贬。
上一篇博客的末尾我們介紹了InstantRun
在初始化完成后踏堡,會(huì)啟動(dòng)一個(gè)server。不難猜測(cè)咒劲,這個(gè)server就是在監(jiān)聽是否有代碼更新顷蟆。當(dāng)用戶更改代碼后,AndroidStudio會(huì)將相關(guān)更新發(fā)送給server缎患,server獲取到更新后執(zhí)行修復(fù)邏輯慕的。
1 SocketServerReplyThread
server的主要實(shí)現(xiàn)由其內(nèi)部類SocketServerReplyThread
,首先來看下其實(shí)現(xiàn):
private class SocketServerReplyThread extends Thread {
private final LocalSocket mSocket;
SocketServerReplyThread(LocalSocket socket) {
this.mSocket = socket;
}
public void run() {
try {
DataInputStream input = new DataInputStream(this.mSocket.getInputStream());
DataOutputStream output = new DataOutputStream(this.mSocket.getOutputStream());
try {
handle(input, output);
} finally {
try {
input.close();
} catch (IOException ignore) {
}
try {
output.close();
} catch (IOException ignore) {
}
}
return;
} catch (IOException e) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Fatal error receiving messages", e);
}
}
}
private void handle(DataInputStream input, DataOutputStream output) throws IOException {
long magic = input.readLong();
if (magic != 890269988L) {
Log.w("InstantRun", "Unrecognized header format " + Long.toHexString(magic));
return;
}
int version = input.readInt();
output.writeInt(4);
if (version != 4) {
Log.w("InstantRun", "Mismatched protocol versions; app is using version 4 and tool is using version " + version);
} else {
int message;
for (; ; ) {
message = input.readInt();
switch (message) {
case 7:
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received EOF from the IDE");
}
return;
case 2:
boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null;
output.writeBoolean(active);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received Ping message from the IDE; returned active = " + active);
}
break;
case 3:
String path = input.readUTF();
long size = FileManager.getFileSize(path);
output.writeLong(size);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size);
}
break;
case 4:
long begin = System.currentTimeMillis();
path = input.readUTF();
byte[] checksum = FileManager.getCheckSum(path);
if (checksum != null) {
output.writeInt(checksum.length);
output.write(checksum);
if (Log.isLoggable("InstantRun", 2)) {
long end = System.currentTimeMillis();
String hash = new BigInteger(1, checksum)
.toString(16);
Log.v("InstantRun", "Received checksum(" + path
+ ") from the " + "IDE: took "
+ (end - begin) + "ms to compute "
+ hash);
}
} else {
output.writeInt(0);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received checksum(" + path
+ ") from the "
+ "IDE: returning ");
}
}
break;
case 5:
if (!authenticate(input)) {
return;
}
Activity activity = Restarter
.getForegroundActivity(Server.this.mApplication);
if (activity != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Restarting activity per user request");
}
Restarter.restartActivityOnUiThread(activity);
}
break;
case 1:
if (!authenticate(input)) {
return;
}
List changes = ApplicationPatch
.read(input);
if (changes != null) {
boolean hasResources = Server.hasResources(changes);
int updateMode = input.readInt();
updateMode = Server.this.handlePatches(changes,
hasResources, updateMode);
boolean showToast = input.readBoolean();
output.writeBoolean(true);
Server.this.restart(updateMode, hasResources,
showToast);
}
break;
case 6:
String text = input.readUTF();
Activity foreground = Restarter
.getForegroundActivity(Server.this.mApplication);
if (foreground != null) {
Restarter.showToast(foreground, text);
} else if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Couldn't show toast (no activity) : "
+ text);
}
break;
}
}
}
}
}
socket開啟后,開始讀取數(shù)據(jù)挤渔,先進(jìn)行一些簡(jiǎn)單的校驗(yàn)肮街,判斷讀取的數(shù)據(jù)是否正確。然后依次讀取文件數(shù)據(jù)判导。
- 如果讀到7嫉父,則表示已經(jīng)讀到文件的末尾,退出讀取操作
- 如果讀到2眼刃,則表示獲取當(dāng)前Activity活躍狀態(tài)绕辖,并且進(jìn)行記錄
- 如果讀到3,讀取UTF-8字符串路徑擂红,讀取該路徑下文件長(zhǎng)度仪际,并且進(jìn)行記錄
- 如果讀到4,讀取UTF-8字符串路徑昵骤,獲取該路徑下文件MD5值树碱,如果沒有,則記錄0变秦,否則記錄MD5值和長(zhǎng)度成榜。
- 如果讀到5,先校驗(yàn)輸入的值是否正確(根據(jù)token來判斷)蹦玫,如果正確赎婚,則在UI線程重啟Activity
- 如果讀到1刘绣,先校驗(yàn)輸入的值是否正確(根據(jù)token來判斷),如果正確挣输,獲取代碼變化的List纬凤,處理代碼的改變(handlePatches,這個(gè)之后具體分析)歧焦,然后重啟
- 如果讀到6移斩,讀取UTF-8字符串,showToast
當(dāng)讀到1時(shí)绢馍,獲取代碼變化的ApplicationPatch列表向瓷,然后調(diào)用handlePatches來處理代碼的變化。
handlePatches:
private int handlePatches(List changes, boolean hasResources, int updateMode) {
if (hasResources) {
FileManager.startUpdate();
}
for (ApplicationPatch change : changes) {
String path = change.getPath();
if (path.endsWith(".dex")) {
handleColdSwapPatch(change);
boolean canHotSwap = false;
for (ApplicationPatch c : changes) {
if (c.getPath().equals("classes.dex.3")) {
canHotSwap = true;
break;
}
}
if (!canHotSwap) {
updateMode = 3;
}
} else if (path.equals("classes.dex.3")) {
updateMode = handleHotSwapPatch(updateMode, change);
} else if (isResourcePath(path)) {
updateMode = handleResourcePatch(updateMode, change, path);
}
}
if (hasResources) {
FileManager.finishUpdate(true);
}
return updateMode;
}
本方法主要通過判斷Change的內(nèi)容舰涌,來判斷采用什么模式(熱部署猖任、溫部署或冷部署)
- 如果后綴為“.dex”,冷部署處理handleColdSwapPatch
- 如果后綴為“classes.dex.3”,熱部署處理handleHotSwapPatch
- 其他情況,溫部署,處理資源handleResourcePatch
2 熱部署
我們知道如果僅僅修改某個(gè)方法的內(nèi)部實(shí)現(xiàn)瓷耙,InstantRun
可以通過熱部署的方式更新朱躺。還是以上一篇博客的例子,我們對(duì)代碼進(jìn)行一點(diǎn)修改,將Toast彈出的文字從'click'變?yōu)?click!!!':
@Override
public void onClick(View view) {
Toast.makeText(this, "click!!!", Toast.LENGTH_SHORT).show();
}
此時(shí)如果點(diǎn)擊運(yùn)行搁痛,可以看到應(yīng)用在沒有重啟的情況更新了邏輯长搀。當(dāng)點(diǎn)擊run按鈕后,在build/intermediates/transforms/instantRun/debug/folders/4000/5目錄下會(huì)出現(xiàn)我們輸出即將發(fā)送給終端的patch:
可以看到patch總共分為兩部分:
修改后的代碼,對(duì)應(yīng)圖中的
com.alibaba.sdk.instantdemo.MainActivity$override
-
com.android.tools.fd.runtime.AppPatchesLoaderImpl.class
用于記錄哪些類被修改了,如本例中的MainActivitypublic class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl { public static final long BUILD_ID = 76160209775610L; public AppPatchesLoaderImpl() { } public String[] getPatchedClasses() { return new String[]{"com.alibaba.sdk.instandemo.MainActivity"}; } }
?
2.1 修改后的代碼
修改后的代碼會(huì)重新生成一個(gè)新的類名:舊類名+$override
鸡典。如本例中的MainActivity$override
源请,接下來看下MainActivity$override
的源碼:
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) {
}
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);
AndroidInstantRuntime.setPrivateField($this, (Button)$this.findViewById(2131427416), MainActivity.class, "btn");
((Button)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "btn")).setOnClickListener($this);
}
public static void onClick(MainActivity $this, View view) {
Toast.makeText($this, "click!!!", 0).show();
}
public Object access$dispatch(String var1, Object... var2) {
switch(var1.hashCode()) {
case -1912803358:
onClick((MainActivity)var2[0], (View)var2[1]);
return null;
case -641568046:
onCreate((MainActivity)var2[0], (Bundle)var2[1]);
return null;
case 1345615064:
init$body((MainActivity)var2[0], (Object[])var2[1]);
return null;
case 1495908858:
return init$args((MainActivity[])var2[0], (Object[])var2[1]);
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/alibaba/sdk/instandemo/MainActivity"}));
}
}
}
我們看到,MainActivity$override
實(shí)現(xiàn)了IncrementalChange
并覆寫了access$dispatch
方法彻况。
該patch會(huì)通過server被寫到應(yīng)用的私有目錄下谁尸,然后通過handleHotSwapPatch進(jìn)行加載。
2.2 hot swap:handleHotSwapPatch
private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received incremental code patch");
}
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null) {
Log.e("InstantRun", "No file to write the code to");
return updateMode;
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder()
.getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
this.mApplication.getCacheDir().getPath(),
nativeLibraryPath, getClass().getClassLoader());
Class aClass = Class.forName(
"com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
dexClassLoader);
try {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher class " + aClass);
}
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher instance " + loader);
}
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod("getPatchedClasses", new Class[0])
.invoke(loader, new Object[0]);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the list of classes ");
for (String getPatchedClass : getPatchedClasses) {
Log.v("InstantRun", "class " + getPatchedClass);
}
}
if (!loader.load()) {
updateMode = 3;
}
} catch (Exception e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
e.printStackTrace();
updateMode = 3;
}
} catch (Throwable e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
updateMode = 3;
}
return updateMode;
}
該方法將patch的dex文件寫入到臨時(shí)目錄纽甘,然后使用DexClassLoader去加載dex良蛮。然后反射調(diào)用AppPatchesLoaderImpl類的load方法。
AppPatchesLoaderImpl繼承自抽象類AbstractPatchesLoaderImpl悍赢,并實(shí)現(xiàn)了抽象方法:getPatchedClasses决瞳。而AbstractPatchesLoaderImpl抽象類代碼如下:
public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
public abstract String[] getPatchedClasses();
public boolean load() {
try {
for (String className : getPatchedClasses()) {
ClassLoader cl = getClass().getClassLoader();
Class aClass = cl.loadClass(className + "$override");
Object o = aClass.newInstance();
Class originalClass = cl.loadClass(className);
Field changeField = originalClass.getDeclaredField("$change");
changeField.setAccessible(true);
Object previous = changeField.get(null);
if (previous != null) {
Field isObsolete = previous.getClass().getDeclaredField("$obsolete");
if (isObsolete != null) {
isObsolete.set(null, Boolean.valueOf(true));
}
}
changeField.set(null, o);
if ((Log.logging != null) && (Log.logging.isLoggable(Level.FINE))) {
Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { className }));
}
}
} catch (Exception e) {
if (Log.logging != null) {
Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), e);
}
return false;
}
return true;
}
}
現(xiàn)在我們?cè)倩剡^頭去看下MainActivity
的代碼:
package com.alibaba.sdk.instandemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
import com.android.tools.fd.runtime.IncrementalChange;
import com.android.tools.fd.runtime.InstantReloadException;
public class MainActivity extends AppCompatActivity
implements View.OnClickListener
{
public static final long serialVersionUID = 0L;
private Button btn;
public MainActivity()
{
}
MainActivity(Object[] paramArrayOfObject, InstantReloadException paramInstantReloadException)
{
this();
}
public void onClick(View paramView)
{
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null)
{
localIncrementalChange.access$dispatch("onClick.(Landroid/view/View;)V", new Object[] { this, paramView });
return;
}
Toast.makeText(this, "click", 0).show();
}
public void onCreate(Bundle paramBundle)
{
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null)
{
localIncrementalChange.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[] { this, paramBundle });
return;
}
super.onCreate(paramBundle);
setContentView(2130968603);
this.btn = ((Button)findViewById(2131427416));
this.btn.setOnClickListener(this);
}
}
結(jié)合兩段代碼,不難看出左权,loadClass方法的原理其實(shí)就是通過反射的方法將原有class中的$change
設(shè)置為修復(fù)類皮胡,然后通過access$dispatch
執(zhí)行更新后的邏輯。
這里有一個(gè)問題涮总。如果我多次修改MainActivity
,handleHotSwapPatch
就會(huì)加載多次MainActivity$override
祷舀,難道不會(huì)沖突嗎瀑梗?一個(gè)類不是只能加載一次嗎烹笔?其實(shí)這個(gè)不用擔(dān)心,因?yàn)?code>handleHotSwapPatch每次都重新創(chuàng)建了一個(gè)DexClassLoader
,不同的ClassLoader即使加載同一個(gè)class也會(huì)被認(rèn)為是不同class抛丽,所以不用擔(dān)心谤职。
2.3 warm swap:handleResourcePatch
private static int handleResourcePatch(int updateMode, ApplicationPatch patch, String path){
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received resource changes (" + path + ")");
}
FileManager.writeAaptResources(path, patch.getBytes());
updateMode = Math.max(updateMode, 2);
return updateMode;
}
調(diào)用了FileManager.writeAaptResources方法寫入Aapt resource。
public static void writeAaptResources(String relativePath, byte[] bytes){
File resourceFile = getResourceFile(getWriteFolder(false));
File file = resourceFile;
File folder = file.getParentFile();
if (!folder.isDirectory()) {
boolean created = folder.mkdirs();
if (!created) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Cannot create local resource file directory " + folder);
}
return;
}
}
if (relativePath.equals("resources.ap_"))
{
writeRawBytes(file, bytes);
}
else
writeRawBytes(file, bytes);
}
可以看到它去獲取了對(duì)應(yīng)的資源文件亿鲜,就是我們?cè)谏厦嫣岬降?data/data/[applicationId]/files/instant-run/resources.ap_允蜈,InstantRun直接對(duì)它進(jìn)行了字節(jié)碼操作,把通過Socket傳過來的修改過的資源傳遞了進(jìn)去蒿柳。對(duì)Android上的資源打包不了解的同學(xué)可以去看老羅的Android應(yīng)用程序資源的編譯和打包過程分析這篇文章饶套。
2.4 cold swap:handleColdSwapPatch
private static void handleColdSwapPatch(ApplicationPatch patch) {
if (patch.path.startsWith("slice-")) {
File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
if (Log.isLoggable("InstantRun", 2))
Log.v("InstantRun", "Received dex shard " + file);
}
}
public static File writeDexShard(byte[] bytes, String name){
File dexFolder = getDexFileFolder(getDataFolder(), true);
if (dexFolder == null) {
return null;
}
File file = new File(dexFolder, name);
writeRawBytes(file, bytes);
return file;
}
對(duì)于cold swap,其實(shí)就是把數(shù)據(jù)寫進(jìn)對(duì)應(yīng)的dex中垒探,所以在art的情況下需要重啟app妓蛮,而對(duì)于API20以下的只能重新構(gòu)建和部署了。
3 總結(jié)
兩篇博客大致介紹了InstantRun
的原理圾叼,從宏觀上講蛤克,InstantRun
通過創(chuàng)建宿主Application的方式來代理所有來的加載,為熱更新提供了Runtime夷蚊。從微觀上來講构挤,三種情況的原理各有不同:
- hot swap玩的方法替換,通過重新生成一個(gè)新類惕鼓,并將原有類的方法映射到新類中的方法筋现。思想上比較類似AndFix,不過AndFix的更新在native層完成呜笑,hot swap則是在java層通過插樁完成夫否。不熟悉AndFix的朋友可以看下這篇博客:AndFix Bug熱修復(fù)框架及源碼解析
- warm swap的原理是加載
resources.ap_
并寫入到AssetManager的加載路徑中 - cold swap的原理其實(shí)就是把數(shù)據(jù)寫進(jìn)對(duì)應(yīng)的dex中。