InstantRun原理(2)——更新邏輯
上一篇博客我們介紹了InstantRun
的初始化邏輯,接下來我們來看下在運行時階段,InstantRun
是如何加載修改的代碼的。
上一篇博客的末尾我們介紹了InstantRun
在初始化完成後,會啟動一個server。不難猜測,這個server就是在監聽是否有代碼更新。當用戶更改代碼後,AndroidStudio會將相關更新發送給server,server獲取到更新後執行修複邏輯。
1 SocketServerReplyThread
server的主要實現由其內部類SocketServerReplyThread
,首先來看下其實現:
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開啟後,開始讀取數據,先進行一些簡單的校驗,判斷讀取的數據是否正確。然後依次讀取文件數據。
- 如果讀到7,則表示已經讀到文件的末尾,退出讀取操作
- 如果讀到2,則表示獲取當前Activity活躍狀態,並且進行記錄
- 如果讀到3,讀取UTF-8字符串路徑,讀取該路徑下文件長度,並且進行記錄
- 如果讀到4,讀取UTF-8字符串路徑,獲取該路徑下文件MD5值,如果沒有,則記錄0,否則記錄MD5值和長度。
- 如果讀到5,先校驗輸入的值是否正確(根據token來判斷),如果正確,則在UI線程重啟Activity
- 如果讀到1,先校驗輸入的值是否正確(根據token來判斷),如果正確,獲取代碼變化的List,處理代碼的改變(handlePatches,這個之後具體分析),然後重啟
- 如果讀到6,讀取UTF-8字符串,showToast
當讀到1時,獲取代碼變化的ApplicationPatch列表,然後調用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的內容,來判斷采用什麼模式(熱部署、溫部署或冷部署)
- 如果後綴為“.dex”,冷部署處理handleColdSwapPatch
- 如果後綴為“classes.dex.3”,熱部署處理handleHotSwapPatch
- 其他情況,溫部署,處理資源handleResourcePatch
2 熱部署
我們知道如果僅僅修改某個方法的內部實現,InstantRun
可以通過熱部署的方式更新。還是以上一篇博客的例子,我們對代碼進行一點修改,將Toast彈出的文字從'click'變為'click!!!':
@Override
public void onClick(View view) {
Toast.makeText(this, "click!!!", Toast.LENGTH_SHORT).show();
}
此時如果點擊運行,可以看到應用在沒有重啟的情況更新了邏輯。當點擊run按鈕後,在build/intermediates/transforms/instantRun/debug/folders/4000/5目錄下會出現我們輸出即將發送給終端的patch:
可以看到patch總共分為兩部分:
修改後的代碼,對應圖中的
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 修改後的代碼
修改後的代碼會重新生成一個新的類名:舊類名+$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
實現了IncrementalChange
並覆寫了access$dispatch
方法。
該patch會通過server被寫到應用的私有目錄下,然後通過handleHotSwapPatch進行加載。
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文件寫入到臨時目錄,然後使用DexClassLoader去加載dex。然後反射調用AppPatchesLoaderImpl類的load方法。
AppPatchesLoaderImpl繼承自抽象類AbstractPatchesLoaderImpl,並實現了抽象方法: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;
}
}
現在我們再回過頭去看下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);
}
}
結合兩段代碼,不難看出,loadClass方法的原理其實就是通過反射的方法將原有class中的$change
設置為修複類,然後通過access$dispatch
執行更新後的邏輯。
這裏有一個問題。如果我多次修改MainActivity
,handleHotSwapPatch
就會加載多次MainActivity$override
,難道不會衝突嗎?一個類不是隻能加載一次嗎?其實這個不用擔心,因為handleHotSwapPatch
每次都重新創建了一個DexClassLoader
,不同的ClassLoader即使加載同一個class也會被認為是不同class,所以不用擔心。
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;
}
調用了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);
}
可以看到它去獲取了對應的資源文件,就是我們在上麵提到的/data/data/[applicationId]/files/instant-run/resources.ap_,InstantRun直接對它進行了字節碼操作,把通過Socket傳過來的修改過的資源傳遞了進去。對Android上的資源打包不了解的同學可以去看老羅的Android應用程序資源的編譯和打包過程分析這篇文章。
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;
}
對於cold swap,其實就是把數據寫進對應的dex中,所以在art的情況下需要重啟app,而對於API20以下的隻能重新構建和部署了。
3 總結
兩篇博客大致介紹了InstantRun
的原理,從宏觀上講,InstantRun
通過創建宿主Application的方式來代理所有來的加載,為熱更新提供了Runtime。從微觀上來講,三種情況的原理各有不同:
- hot swap玩的方法替換,通過重新生成一個新類,並將原有類的方法映射到新類中的方法。思想上比較類似AndFix,不過AndFix的更新在native層完成,hot swap則是在java層通過插樁完成。不熟悉AndFix的朋友可以看下這篇博客:AndFix Bug熱修複框架及源碼解析
- warm swap的原理是加載
resources.ap_
並寫入到AssetManager的加載路徑中 - cold swap的原理其實就是把數據寫進對應的dex中。
最後更新:2017-09-11 18:03:12