閱讀393 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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用於記錄哪些類被修改了,如本例中的MainActivity

    public 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執行更新後的邏輯。

這裏有一個問題。如果我多次修改MainActivityhandleHotSwapPatch就會加載多次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

  上一篇:go  InstantRun原理(1)——初始化邏輯
  下一篇:go  如何授權一個子賬號管理某個redis實例