基于Instant Run思想的HotFix方案實現

CorinaZLE 8年前發布 | 7K 次閱讀 Java 安卓開發 Android開發 移動開發

近一年來,各種HotFix庫層出不窮,各家大廠百花齊放,QQ空間最早提出了自己的熱修復實現,接著阿里也開源了自家的AndFix(貌似阿里百川已經給開發者提供了新的Hotfix功能),現在微信又有了Tinker,各家都如此關心HotFix,無非是線上版本的bug對產品影響太大,尤其是DAU比較高的app,更是不能容忍。前幾天看到美團基于Instant run原理推出了自己的Hotfix庫,不過貌似沒有開源,于是自己就按照Instant run的原理也鼓搗出了一個簡單的HotFix實現, 可以在不重啟App和Activity的條件下實現修復, 代碼地址會在文章最后貼出,供大家研究學習。

實現效果

先讓大家看看具體的實現效果是怎樣的,很簡單,一個Acitivty中點擊按鈕,會彈出Toast“我有bug!”,然后加載補丁,再點擊按鈕,會彈出“我是補丁,沒有bug啦!”

接下來讓我們看看怎么實現這個庫。

Instant Run原理

Instant run的原理是采用了貍貓換太子的戲法,在編譯階段給每個類都注入了一個$change(代理,即補丁)變量,并且在每個方法前都注入了一段代碼,判斷$change是否為空,如果不為空,就執行代理里的方法。

關于Instant Run具體的原理,我在文章 《淺談Instan-Run中的熱替換》 中已經講了很多,這里不再贅述,建議不了解的同學在閱讀本文前先看看這篇文章。

實現

Step1:代碼注入

上面說到Instan Run在編譯期間給每個類都注入了變量和代碼,那么這是怎么實現的呢?其實很簡單,android studio給我們提供了transform Api,transform其實也就是打包過程中的一個task,我們可以根據這個特性,利用javassist來注入代碼。關于代碼注入這塊,我參考了文章 《通過自定義Gradle插件修改編譯后的class文件》 ,謝謝這位同學慷慨的分享,具體過程大家可以看看這篇文章,我就不講詳細的步驟了。

代碼注入的實現代碼如下:

File dir = new File(path)
   if (dir.isDirectory()) {
       dir.eachFileRecurse { File file ->

           String filePath = file.absolutePath
           //確保當前文件是class文件,并且不是系統自動生成的class文件
           if (filePath.endsWith(".class")
                   && !filePath.contains('R$')
                   && !filePath.contains('R.class')
                   && !filePath.contains("BuildConfig.class")
                   && !filePath.contains("\$Patch.class")
                   && !filePath.contains("PatchBox.class")) {
               // 判斷當前目錄是否是在我們的應用包里面
               int index = filePath.indexOf(packageName);
               boolean isMyPackage = index != -1;
               if (isMyPackage) {
                   int end = filePath.length() - 6 // .class = 6
                   String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.')
                   //開始修改class文件
                   CtClass c = pool.getCtClass(className)

                   if (c.isFrozen()) {
                       c.defrost()
                   }
                   pool.importPackage("com.wangxiandeng.savior")

                   //給類添加$savior變量,即補丁變量
                   CtField savior = new CtField(pool.get("com.wangxiandeng.savior.Savior"), "\$savior", c);
                   savior.setModifiers(Modifier.STATIC);
                   c.addField(savior);

                   //遍歷類的所有方法
                   CtMethod[] methods = c.getDeclaredMethods();
                   for (CtMethod method : methods) {
                       //在每個方法之前插入判斷語句,判斷類的補丁實例是否存在
                       StringBuilder injectStr = new StringBuilder();
                       injectStr.append("if(\$savior!=null){\n")
                       String javaThis = "null,"
                       if (!Modifier.isStatic(method.getModifiers())) {
                           javaThis = "this,"
                       }
                       String runStr = "\$savior.dispatchMethod(" + javaThis + "\"" + method.getName() + "." + method.getSignature() + "\" ,\$args)"
                       injectStr.append(addReturnStr(method, runStr))
                       injectStr.append("}")
                       print("插入了:" + injectStr.toString() + "語句")
                       method.insertBefore(injectStr.toString())
                   }
                   c.writeFile(path)
                   c.detach()
               }
           }
       }
   }

上面這段代碼中,我們給每個類都注入了一個類型為Savior的靜態變量$savior,并且在每個方法前加入了一段代碼,判斷$savior是否為null,如果不為null,則執行$savior.dispatchMethod(),傳入方法的方法簽名和參數,讓補丁類代以執行。

Step2:制作補丁類

補丁類的命名方式必須為XXX$Patch,比如MainActivity有bug,那么就制作一個名為MainActivity$Patch的補丁類,注意補丁類必須和原有類要放在同一包下。

先讓我們寫一個MainActivity,該類有bug(當然不是真的有bug啊)

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //將補丁文件從資源目錄拷貝到sd卡
        FileUtil.copyJarToFile(this);

        findViewById(R.id.btn_show).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                show();
            }
        });
        //點擊加載補丁
        findViewById(R.id.btn_load).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                       PatchLoader.getInstance().loadPatch(PatchUtil.PATCH_PATH);
                    Toast.makeText(MainActivity.this, "load success", Toast.LENGTH_SHORT).show();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void show() {
        Toast.makeText(this, "我有bug!", Toast.LENGTH_SHORT).show();
    }
}

現在我們要制作一個補丁類,用來修復bug。如果原有類繼承自某個類,則補丁類同樣要繼承自該類,并且要實現Savior接口。

public class MainActivity$Patch extends Activity implements Savior{

    @Override
    public Object dispatchMethod(Object host, String methodSign, Object[] params) {
        MainActivity mainActivity = (MainActivity) host;
        switch (methodSign.hashCode()) {
            case -641568046:
                onCreate(mainActivity, (Bundle) (params[0]));
                break;
            case -340027132:
                show(mainActivity);
                break;
        }

        return null;
    }

    protected void onCreate(final MainActivity host, Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn_show).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                show(host);
            }
        });

        findViewById(R.id.btn_load).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    PatchLoader.getInstance().loadPatch(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getPath() + "/patch.dex");
                    Toast.makeText(host, "load success", Toast.LENGTH_SHORT).show();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void show(MainActivity host) {
        Toast.makeText(host, "我是補丁,沒有bug啦!", Toast.LENGTH_SHORT).show();
    }
}

補丁類需要實現原有類的所有方法,并且要重寫原有類有bug的方法,在該例子中,原有類的show()方法含有bug,所以重寫了show()方法,當然其他方法可能也會視情況有一些變動。補丁類需要重寫接口中dispatchMethod()方法,根據方法簽名的hashcode來進行具體的方法調用。

這里還有一個我未解決的問題,即在補丁類中調用原有類的super()方法,Instant run采用的是在每個原有類里又添加了一個函數access$super(),用來調用super方法,這樣補丁類遇到super方法時,直接調用原有類的access$super()方法即可,不過就像美團在文章中所說的,該方法會增加app的方法數,所以不采用。美團采用的方法是修改super方法的調用指令。

我們來用javap -c 命令看一下MainActivity的字節碼

其中調用super.onCreate()的指令為:

30: invokespecial #2                  // Method android/support/v7/app/AppCompatActivity.onCreate:(Landroid/os/Bundle;)V

可以看見調用的是invokespecial指令(不知美團為何說是invokesuper),該指令用于調用實例構造器方法,私有方法和父類方法。美團的意思應該是去修改指令為invokespecial,具體我還不知怎么操作,如果有同學知道,希望留言中告知。

接著寫一個補丁記錄類,用來記錄有哪些補丁

public class PatchBox implements IPatchBox {
    @Override
    public List<String> getPatchClasses() {
        List<String> list = new ArrayList<>();
        list.add("com.wangxiandeng.saviortest.MainActivity$Patch");
        return list;
    }
}

Step3:補丁打包

補丁寫完后,需要打包成dex,首先在編譯過程,拷貝出補丁類和PatchBox.class文件,依據補丁類所在包名,放在文件夾下,比如新建一個總文件夾patch,再新建一個

com/wangxiandeng/saviortest/文件夾,放入MainActivity$Patch.class和PatchBox.class, 然后按照以下步驟操作:

1.cd 到patch目錄;

2.利用jar cvf patch.jar * 指令打包成jar文件。

3.利用build-tools目錄下的dx指令:dx –dex –output=patch_dex.jar patch.jar指令,打包成dex的jar包,patch_dex.jar即為我們打包好的補丁。

Step4:補丁加載

將補丁放在sd卡中,執行補丁加載過程。補丁加載的核心代碼如下:

//加載補丁Dex文件
DexClassLoader dexClassLoader = new DexClassLoader(patchPath, getOdexPath(), null, getClass().getClassLoader());

//加載補丁裝載類PatchBox
Class<?> patchBoxClass = Class.forName(mPatchBoxName, true, dexClassLoader);
IPatchBox patchBox = (IPatchBox) patchBoxClass.newInstance();

//遍歷加載補丁類
for (String className : patchBox.getPatchClasses()) {
    Class<?> patchClass = dexClassLoader.loadClass(className);
    Object patchInstance = patchClass.newInstance();

    //反射修改bug類的mSavior字段
    int index = className.indexOf("$Patch");
    if (index == -1) {
        Log.e("Savior:", "incorrect name for patch, please rename your patch according to the README.md");
        return;
    }
    String bugClassName = className.substring(0, index);
    Class<?> bugClass = getClass().getClassLoader().loadClass(bugClassName);
    Field saviorField = bugClass.getDeclaredField("$savior");
    saviorField.setAccessible(true);
    saviorField.set(null, patchInstance);
}

補丁加載過程主要分為3步:

1.利用DexClassLoader加載補丁;

2.加載補丁記錄類PatchBox;

3.根據遍歷記錄的補丁類并實例化,反射原有類的$savior字段,賦值為補丁實例。

至此,補丁類就已經加載完畢,此時調用原有類的bug方法,實際上調用的是補丁類的修復方法。

 

來自:https://halfstackdeveloper.github.io/2016/09/23/基于Instant-Run思想的HotFix方案實現/

 

 本文由用戶 CorinaZLE 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!