基于Instant Run思想的HotFix方案实现

Posted 安卓巴士Android开发者门户

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Instant Run思想的HotFix方案实现相关的知识,希望对你有一定的参考价值。

点击上方“安卓巴士android开发者门户”即可关注!


Instant Run

AndroidStudio从2.0开始,加入了一个功能叫做:Instant Run,顾名思义,这个功能的作用就是让开发者能够立即运行自己的程序。具体点说,就是我们不用再像以前那样每次修改完代码都要重新构建整个app,而是可以直接点击运行,修改的代码就可以作用于我们的app。




前言



实现效果


先让大家看看具体的实现效果是怎样的,很简单,一个Acitivty中点击按钮,会弹出Toast“我有bug!”,然后加载补丁,再点击按钮,会弹出“我是补丁,没有bug啦!”


基于Instant Run思想的HotFix方案实现


接下来让我们看看怎么实现这个库。


Instant Run原理



Instant run的原理是采用了狸猫换太子的戏法,在编译阶段给每个类都注入了一个$change(代理,即补丁)变量,并且在每个方法前都注入了一段代码,判断$change是否为空,如果不为空,就执行代理里的方法。

实现步骤


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,不知是不是使用java bytecode editor手动操作,如果有同学知道,希望留言中告知。


接着写一个补丁记录类,用来记录有哪些补丁

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.遍历PatchBox中记录的补丁类并实例化,反射对应原有类的$savior字段,赋值为补丁实例。


至此,补丁类就已经加载完毕,此时调用原有类的bug方法,实际上调用的是补丁类的修复方法。


结论


以上就是一个简单的HotFix库,当然这只供大家学习使用,离商用还差的很远,真正的HotFix库要考虑的地方还有很多,毕竟是用来修复bug的,总不能库自身一堆bug吧。不过Instant Run确实是实现HotFix的一个很好的方案,不需要考虑android版本兼容性的问题,还可以实现修复的即时生效,期待美团Hotfix库的开源!



https://github.com/HalfStackDeveloper/Savior


若您对此项目有一些自己的想法 , 欢迎来提Pull Request。如果你觉得还不错,觉得不错的可以给作者的Github个Star。支持小编可以在下方+1,也欢迎各位同学来稿哈!

大家都在看









微信不支持外链,所以只能点击原文下载源码~

以上是关于基于Instant Run思想的HotFix方案实现的主要内容,如果未能解决你的问题,请参考以下文章

AndroidStudio2.2的Instant Run严重Bug

引入 Tinker 之后如何在 Debug 模式下开启 Instant Run

深入理解Android Instant Run运行机制

Hotfix安卓热部署方案https://github.com/dodola/HotFix

AS 2.0新功能 Instant Run

WebView JavaScript 接口似乎只能通过 Android Studio 的 Instant-Run 工作,但不能通过常规 gradle 编译