美团热修复的使用
如何集成?
-
在App的build.gradle,加入如下依赖
apply plugin: 'com.android.application' //制作补丁时将这个打开,auto-patch-plugin紧跟着com.android.application //apply plugin: 'auto-patch-plugin' apply plugin: 'robust' 在dependencies里加入 compile 'com.meituan.robust:robust:0.4.75'
-
在工程的build.gradle中,加入classpath
buildscript { repositories { jcenter() } dependencies { classpath 'com.meituan.robust:gradle-plugin:0.4.75' classpath 'com.meituan.robust:auto-patch-plugin:0.4.75' } }
-
在app目录下,配置robust.xml文件,具体配置见官方demo https://github.com/Meituan-Dianping/Robust/blob/master/app/robust.xml,以下是我的配置
<?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>false</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.client.api</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.kira.drivetest</name> </patchPackname> <!--自动化补丁中,不需要反射处理的类,这个配置项慎重选择--> <noNeedReflectClass name="classes no need to reflect"> </noNeedReflectClass> </resources>
这里面最重要的是packname和patchPackname这两个的配置。其他的基本可以不动。
packname里面的内容写上你要修复代码处的包名。patchPackname里的内容就写上继承了PatchManipulate类所在的位置,以下是我的配置
public class PatchManipulateImp extends PatchManipulate { /*** * connect to the network ,get the latest patches * l联网获取最新的补丁 * @param context * * @return */ @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); Log.w("robust","robustApkHash :" + robustApkHash); //connect to network to get patch list on servers //在这里去联网获取补丁列表 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 //LocalPath是存储原始的补丁文件,这个文件应该是加密过的,TempPath是加密之后的,TempPath下的补丁加载完毕就删除,保证安全性 //这里面需要设置一些补丁的信息,主要是联网的获取的补丁信息。重要的如MD5,进行原始补丁文件的简单校验,以及补丁存储的位置,这边推荐把补丁的储存位置放置到应用的私有目录下,保证安全性 patch.setLocalPath(Environment.getExternalStorageDirectory().getPath()+ File.separator+"robust"+File.separator + "patch"); //setPatchesInfoImplClassFullName 设置项各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是和xml配置项patchPackname保持一致,而且类名必须是:PatchesInfoImpl //请注意这里的设置 patch.setPatchesInfoImplClassFullName("com.kira.drivetest.PatchesInfoImpl"); List patches = new ArrayList<Patch>(); patches.add(patch); return patches; } /** * * @param context * @param patch * @return * * you can verify your patches here */ @Override protected boolean verifyPatch(Context context, Patch patch) { //do your verification, put the real patch to patch //放到app的私有目录 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; } 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(); } } /** * * @param patch * @return * * you may download your patches here, you can check whether patch is in the phone */ @Override protected boolean ensurePatchExist(Patch patch) { return true; } }
请注意,patch.setPatchesInfoImplClassFullName();这个调用,里面的参数写上当前这个类所在的包名及类名。
实现RobustCallBack接口,以下是具体实现
public class RobustCallBackSample 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);
}
}
这个类是关于补丁应用状况的一些回调,通知。
- 在需要加载补丁的地方,加上以下代码:
new PatchExecutor(this,new PatchManipulateImp(),new RobustCallBackSample()).start(); 参数一:传入Context上下文 参数二:传入PatchManipulate的继承类 参数二:传入RobustCallBack的实现类
如何使用?
- 平时打包的时候就把app下的build.gradle里的apply plugin:'robust' 打开,屏蔽掉apply plugin: 'auto-patch-plugin'
apply plugin: 'com.android.application' //制作补丁时将这个打开,auto-patch-plugin紧跟着com.android.application //apply plugin: 'auto-patch-plugin' apply plugin: 'robust'
然后将/app/build/outputs/robust里面的methodsMap.robust保存到app目录下的robust文件夹,若没有这个文件夹请创建。
- 修复bug的时候,将apply plugin:'auto-patch-plugin'打开,apply plugin:'robust'屏蔽。
- 修改代码,在改动的方法上面添加
@Modify
注解,对于Lambda表达式请在修改的方法里面调用RobustModify.modify()方法
@Modify
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
//或者是被修改的方法里面调用RobustModify.modify()方法
protected void onCreate(Bundle savedInstanceState) {
RobustModify.modify()
super.onCreate(savedInstanceState);
}
- 在robust.xml文件中的packname字段里插入该代码所在的位置,具体到包名即可
- 然后执行打包命令即可,当提示
Error:Execution failed for task ':app:transformClassesWithAutoPatchTransformForRelease'.
auto patch end successfully
就代表补丁包打出来了。位置处于app/build/robust/patch.jar。补丁包可以放到公司的服务器上,然后触发加载补丁地方的代码来联网获取即可。至此,整个热修复过程完成。
常见问题
在编译补丁的时候弹出java.lang.reflect.UndeclaredThrowableException错误。
弹出这个错误的原因是,有地方的代码修改了并且标注了Modify,但是没在robust.xml文件中的packname中加上这个修改代码的所在的包名。具体的位置信息,请在编译补丁的时候查看Gradle Console中的结果,Gradle Console在AndroidStudio工具的右下角 ,EventLog的右边。
补丁无法加载成功,并且抛出copy source patch to local patch error, no patch execute in path
请注意PatchManipulate继承类里的
patch.setLocalPath(Environment.getExternalStorageDirectory().getPath()+ File.separator+"robust"+ File.separator + "patch");
这段代码是设置patch的本地路径。
在verifyPatch方法里
protected boolean verifyPatch(Context context, Patch patch) { //do your verification, put the real patch to patch //放到app的私有目录 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; }
这个方法里会调用copy方法,patch.getLocalPath()会帮你之前设置的路径加上.jar
public String getLocalPath() { return this.localPath + ".jar"; }
所以您在服务器上的补丁名字要叫patch.jar,然后patch.setLocalPath里这样设置,patch不能加jar
patch.setLocalPath(Environment.getExternalStorageDirectory().getPath()+ File.separator+"robust"+ File.separator + "patch");
,否则在copy方法里会判断这个路径不存在,因为patch.getLocalPath()里会帮你加上.jar,如果您在之前的路径上加上了jar,就变成了patch.jar.jar这样就和你下载的文件名就不一样了,所以就会抛异常了。