android使用美团的robuts对app进行热修复
网上借鉴的对比图,也是最终尝试robust的原因,robust可以增加类和方法,demo未测试增加类.据说资源方便也在测试中了
robuts修复原理:github地址
1.在项目跟目录的build.gradle文件中增加以下代码
2.在宿主程序(app目录)的build.gradle文件中添加一下代码
//apply plugin: 'auto-patch-plugin'
生成补丁文件时取消代码注释,需要放在除com.android.application其他的plugin之前.3.在app目录下(src同级目录)创建必要的robuts目录和robuts.xml文件
robuts.xml 重点修改两个地方:1.packname 需要修复文件的目录;2.patchpackname:要与patchmaninpulate实现类中设置setpatchesInfoImplClassFullName方法参数中的包名一样,类名默认都是PathesInfoImpl
<?xml version="1.0" encoding="utf-8"?> <resources> <switch> <!--true代表打开Robust,请注意即使这个值为true,Robust也默认只在Release模式下开启--> <!--false代表关闭Robust,无论是Debug还是Release模式都不会运行robust--> <turnOnRobust>true</turnOnRobust> <!--<turnOnRobust>false</turnOnRobust>--> <!--是否开启手动模式,手动模式会去寻找配置项patchPackname包名下的所有类,自动的处理混淆,然后把patchPackname包名下的所有类制作成补丁--> <!--这个开关只是把配置项patchPackname包名下的所有类制作成补丁,适用于特殊情况,一般不会遇到--> <!--<manual>true</manual>--> <manual>false</manual> <!--是否强制插入插入代码,Robust默认在debug模式下是关闭的,开启这个选项为true会在debug下插入代码--> <!--但是当配置项turnOnRobust是false时,这个配置项不会生效--> <!--<forceInsert>true</forceInsert>--> <forceInsert>false</forceInsert> <!--是否捕获补丁中所有异常,建议上线的时候这个开关的值为true,测试的时候为false--> <catchReflectException>true</catchReflectException> <!--<catchReflectException>false</catchReflectException>--> <!--是否在补丁加上log,建议上线的时候这个开关的值为false,测试的时候为true--> <!--<patchLog>true</patchLog>--> <patchLog>false</patchLog> <!--项目是否支持progaurd--> <proguard>true</proguard> <!--<proguard>false</proguard>--> <!--项目是否支持ASM进行插桩,默认使用ASM,推荐使用ASM,Javaassist在容易和其他字节码工具相互干扰--> <useAsm>true</useAsm> <!--<useAsm>false</useAsm>--> </switch> <!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码--> <!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名, 这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的--> <packname name="hotfixPackage"> <!--需要打补丁的包--> <!--<name>com.meituan</name>--> <!--<name>com.sankuai</name>--> <!--<name>com.dianping</name>--> <name>com.example.huangxiaoyu.hotfixtest</name> </packname> <!--不需要Robust插入代码的包名,Robust库不需要插入代码,如下的配置项请保留,还可以根据各个APP的情况执行添加--> <exceptPackname name="exceptPackage"> <name>com.meituan.robust</name> <name>com.meituan.sample.extension</name> </exceptPackname> <!--补丁的包名,请保持和类PatchManipulateImp中fetchPatchList方法中设置的补丁类名保持一致( setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl")), 各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是如下的配置项,类名必须是:PatchesInfoImpl--> <patchPackname name="patchPackname"> <name>com.example.huangxiaoyu.hotfixtest.patch</name> </patchPackname> <!--自动化补丁中,不需要反射处理的类,这个配置项慎重选择--> <noNeedReflectClass name="classes no need to reflect"> </noNeedReflectClass> </resources>
以上准备工作完成后开始打包修复前的apk,案例apk代码值演示robuts热修复功能,代码简单.2个页面.A页面跳转B页面,A页面两个按钮.一个加载修复后的打包文件.一个跳转到B页面.B页面根据修复补丁前后显示内容不同
mainactivity主要代码
findViewById(R.id.btn_upload).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {//加载补丁 new PatchExecutor(getApplicationContext(), new PatchManipulateImp(), new CallBack()).start(); } }); findViewById(R.id.btn_action).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(MainActivity.this, Activity_Second.class)); } });
secondactivity修复前
public class Activity_Second extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); TextView tv_text = findViewById(R.id.tv_text); tv_text.setText(getString()); } String getString() { return "修复前"; } }
patchmanipulateimp
public class PatchManipulateImp extends com.meituan.robust.PatchManipulate { @Override protected List<Patch> fetchPatchList(Context context) { //将app自己的robustApkHash上报给服务端,服务端根据robustApkHash来区分每一次apk build来给app下发补丁 //apkhash is the unique identifier for apk,so you cannnot patch wrong apk. //String robustApkHash = RobustApkHashUtils.readRobustApkHash(context); Patch patch = new Patch(); patch.setName("123"); //we recommend LocalPath store the origin patch.jar which may be encrypted,while TempPath is the true runnable jar patch.setLocalPath(context.getApplicationContext().getFilesDir().getAbsolutePath() + File.separator + "patch"); /*上面的路径看似设置的是目录,其实不是,在get方法中默认追加了.jar;temp默认则追加_temp.jar.可以理解为设置补丁的文件名.建议放在程序内部目录,提高安全性*/ /*com.example.huangxiaoyu.hotfixtest.patch 要和robuts中patchPackname节点里面的值保持一样并且存在的目录*/ patch.setPatchesInfoImplClassFullName("com.example.huangxiaoyu.hotfixtest.patch.PatchesInfoImpl"); List patches = new ArrayList<Patch>(); patches.add(patch); return patches; } @Override protected boolean verifyPatch(Context context, Patch patch) { patch.setTempPath(context.getCacheDir() + File.separator + "robust" + File.separator + "patch"); //in the sample we just copy the file try { copy(patch.getLocalPath(), patch.getTempPath()); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("copy source patch to local patch error, no patch execute in path " + patch.getTempPath()); } return true; } @Override protected boolean ensurePatchExist(Patch patch) { return true; } public void copy(String srcPath, String dstPath) throws IOException { File src = new File(srcPath); if (!src.exists()) { throw new RuntimeException("source patch does not exist "); } File dst = new File(dstPath); if (!dst.getParentFile().exists()) { dst.getParentFile().mkdirs(); } InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutputStream(dst); try { // Transfer bytes from in to out byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } } finally { out.close(); } } finally { in.close(); } } }
callback代码就是空实现
public class CallBack implements RobustCallBack { @Override public void onPatchListFetched(boolean result, boolean isNet, List<Patch> patches) { Log.d("RobustCallBack", "onPatchListFetched result: " + result); Log.d("RobustCallBack", "onPatchListFetched isNet: " + isNet); for (Patch patch : patches) { Log.d("RobustCallBack", "onPatchListFetched patch: " + patch.getName()); } } @Override public void onPatchFetched(boolean result, boolean isNet, Patch patch) { Log.d("RobustCallBack", "onPatchFetched result: " + result); Log.d("RobustCallBack", "onPatchFetched isNet: " + isNet); Log.d("RobustCallBack", "onPatchFetched patch: " + patch.getName()); } @Override public void onPatchApplied(boolean result, Patch patch) { Log.d("RobustCallBack", "onPatchApplied result: " + result); Log.d("RobustCallBack", "onPatchApplied patch: " + patch.getName()); } @Override public void logNotify(String log, String where) { Log.d("RobustCallBack", "logNotify log: " + log); Log.d("RobustCallBack", "logNotify where: " + where); } @Override public void exceptionNotify(Throwable throwable, String where) { Log.e("RobustCallBack", "exceptionNotify where: " + where, throwable); } }
mineapplication中对补丁进行启动加载.测试中发现如果不在application中加载,程序重启后修复内容失效.
以上完成后执行打包命令:
gradlew clean assembleRelease --stacktrace --no-daemon
注:如果打包命令不能执行,需配置环境变量path,增加adb正确路径,打完包后会安装生成的apk文件到手机.并运行软件
补丁前完成
把下面对应的mapping.txt和methoodsmap.robuts文件复制到app目录下(src同级目录)下的robuts目录
复制后
修改seondactivity,修改的方法或者类需要添加@modify注解,添加的类和方法需要添加@add注解,否则不能正常打补丁.
在app的build.gradle文件里面取消注释auto-patch-plugin
4.再次执行执行打包命令
gradlew clean assembleRelease --stacktrace --no-daemon*/
第二次次打包会抛出auto patch end successfully表示补丁已经生成,生异常日志如下:
在如下目录找到patch.jar,吧这个文件push到patchmanipulateimp类中设置的路径中去,注意路径的地址,其实代码是设置一个文件的路径.并不是设置补丁文件的目录,需要把文件push到最后一个字符串的前一个目录级别中去
本案例push文件的地址
push之后在打开的app中点击上面的hellword按钮加载补丁.然后在点击第二helloworld按钮,补丁就能看到B页面补丁后显示的内容:"修复后后后".
重启程序后会在application中启动加载补丁. 如果删除补丁文件,则程序重启后补丁失效.
完成修复
实际应用中可以根据当前的程序版本下载不同补丁包.每次补丁替换patch.jar文件.下载后加载一下补丁.下次程序再次启动会按照application中的设置自动加载补丁