感谢您的反馈!
当前通过我们测试,已确定包含(并不限于)的机型接近800款,包含了市面上绝大部分主流机器,可点击下载查看。(注意:不排除因刷机or系统升级导致机型不支持的情况,具体case请加入钉钉群和我们沟通)
BaseBug.md
文件中关于注解的说明)违反上述规则, 则可能导致打补丁包或者加载补丁失败的情况, 代码实例请参考Demo工程下的BaseBug.md
文件下的详细说明, 另一方面这些限制随着SDK版本的升级会逐步的减少, 敬请期待.
应用在上线打包APK时,往往会进行混淆操作,但是由于修复前后两个APK混淆结果不同会导致patch无效,无法修复bug。
所以,需要注意的是:应用打包APK的时候修复前后两个APK必须使用同一份mapping.txt
,以保证两个APK混淆结果一致。
如果app应用了混淆配置, 那么需要做如下处理. 如果没有应用混淆配置, 则不需要如下处理
-printmapping mapping.txt
命令发现在gradle升级到最新版本后不生效了, 所以mapping.txt文件移步build目录下查找修复后的项目, 修改proguard-rules.pro文件, 保证混淆结果一致
-applymapping mapping.txt此时会在当前模块目录下查找mapping.txt文件, 所以务必确保mapping.txt文件移动到了当前模块的目录下
可以,比如谷歌MultiDex方案,最新的补丁工具打补丁包过程中会把多个dex整合为一个完整的dex, 然后分析前后dex的差异打出补丁包, 所以最后不管apk中有几个dex, 都是能正确打出补丁包.
PS: 正确打出补丁包的前提是apk未加固, 加固后将直接改变apk的dex结构, 会导致补丁工具生成补丁失败
支持的, 但是必须使用加固前的apk生成补丁包, 加固后的apk能正常加载补丁,遇到具体case欢迎来钉钉群交流。
动态部署框架一般属于多classloader实现, 暂时不支持.
去掉compile ‘com.alibaba.sdk.android.plugins.jar:alisdk-utdid:0.0.1’依赖即可
详细说明参考Demo工程BaseBug.md
文件中关于注解的说明
//修改前 public class BaseBug { private String temp = "old apk..."; } //修改后 public class BaseBug { private String temp = "new apk..." //情况1 { temp = "new apk..." //情况2 } public BaseBug(){ temp = "new apk..."; //情况3 } }
这样是不允许的, 因为类Field的直接修改会反应在构造函数中, 所以不允许
当然如果你是在除了构造函数之外任何方法中修改, 都是没问题的
public class BaseBug { private String temp = "old apk..."; public void test(Context context) { temp = "new apk..."; } }
¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨
代码修复前BaseBug没有配置任何混淆配置, 那么temp域因为没有被任何代码块引用所以被移除
public class BaseBug { private String temp; public void test() { temp = "new apk..."; } }
修复后, 因为temp域在test方法中被引用, 所以不会被混淆移除, 所以新apk中的BaseBug类就存在了新域temp
public class BaseBug { private String temp; public void test(Context context) { temp = "new apk"; Toast.makeText(context.getApplicationContext(), temp, Toast.LENGTH_SHORT).show(); } }
hotfix不支持新增域, 打补丁工具检测到新增类field直接报错异常退出, 打补丁失败. 需要避免这种情况
¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨
修复前, InnerClass作为内部类, 假设此时的私有变量s, 没有被任何类引用
public class BaseBug { public void test(Context context) { Toast.makeText(context.getApplicationContext(), "old apk...", Toast.LENGTH_SHORT).show(); } class InnerClass { private String s; private InnerClass(String s) { this.s = s; } } }
修复后test方法引用了内部类inner.s
, 内部类会在编译期编译为跟外部类一样的顶级类. 所以外部类为了能访问private/protected修饰的内部类方法, 那么编译期间自动为内部类生成access$**
相关方法, 此处修复后apk自动为InnerClass内部类生成access$100方法. 同样的如果此时匿名内部类需要访问外部类的private属性/方法, 内部类也会自动生成access$**
相关方法.
public class BaseBug { public void test(Context context) { InnerClass inner = new InnerClass("old apk"); Toast.makeText(context.getApplicationContext(), inner.s, Toast.LENGTH_SHORT).show(); } class InnerClass { private String s; private InnerClass(String s) { this.s = s; } } }
因为old apk没有access$100方法, 打补丁工具检测到新增了类方法直接报错异常退出, 打补丁失败. 需要避免这种情况
¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨
其实除了4.9.2以及4.9.3节中说明可能导致的新增filed/method情况之外, 还有另一种情况. 我们知道打包编译的阶段可能会进行一系列的优化,包括方法的内敛/裁剪等等,这些东西平时我们是不关注的,但是在生成patch包的时候就会有影响. 如果你的应用混淆配置文件中没有加上-dontoptimize
这一项, 那么很有可能导致方法被内敛/裁剪. 举例如下, 以下例子都是在gradle2.14.1版本的表现, 其它gradle版本表现是否一致, 有待考证.
public class BaseBug { public static void test(Context context) { Toast.makeText(context.getApplicationContext(), "old apk...", Toast.LENGTH_SHORT).show(); print("haha"); } public static void print(String s) { Log.d("BaseBug", s); } }
测试发现, 一个方法如果只被调用了一次那么该方法将会被内敛, 此时假如print方法只在test方法中被调用, 但是test方法被不止一个地方调用, 那么test方法没被内敛, print方法被内敛. 查看下生成的mapping.txt文件, 印证了这个结论, 没有print方法的映射
com.taobao.hotfix.demo.BaseBug -> com.taobao.hotfix.demo.a: void test(android.content.Context) -> a
如果恰好将要patch的一个方法调用了print方法, 那么print被调用了两次, 在新的apk中不会被内敛, 所以此时打补丁发现新增了print方法异常退出, 打补丁失败. 需要避免这种情况
这里先假设test方法没有被内敛掉, 修复前代码如下
public class BaseBug { public static void test(Context context) { Log.d("BaseBug", "test"); } }
查看下生成的mapping.txt文件
com.taobao.hotfix.demo.BaseBug -> com.taobao.hotfix.demo.a: void test$faab20d() -> a
此时test方法context参数没被使用, 所以test方法的context参数被裁剪, 混淆任务首先生成test$faab20d()
裁剪过后的无参方法, 然后再混淆. 所以如果将要patch该test方法恰好用到了context参数, 那么新apk中新增了test(context)方法, 所以此时打补丁发现新增了方法异常退出, 打补丁失败.
那如何解决实例二的这种问题呢?当然是有办法的,参数引用住,不让编译器在优化的时候认为这是一个无用的参数就好了,可以采取的方法很多,这里介绍一种最有效的方法:
public static void test(Context context) { if (Boolean.FALSE.booleanValue()) { context.getApplicationContext(); } Log.d("BaseBug", "test"); }
注意这里不能用基本类型false,必须用包装类Boolean,因为如果写基本类型这个if语句也很可能会被优化掉的
一般情况下项目的混淆配置都会使用到android sdk
默认的混淆配置文件proguard-android-optimize.txt
或者proguard-android.txt
, 但是如果不了解这些原理的情况下, 强烈推荐不使用proguard-android-optimize.txt
, 这个配置文件虽然会让最终的apk包变小, 但是也因为默认会在执行混淆任务的时候optimize代码从而导致实例1,2
中举例的不可预料情况. 如下推荐使用proguard-android.txt
这个配置文件有-dontoptimize
这一项, 最后不会导致方法被裁剪/内敛
buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.pro' } }
方法如果只是被运行时注解是可以被patch的, 注意是被运行时注解的方法可以被patch, 而不是运行注解本身可以被patch.
但是如果是编译期注解就值得商榷, 因为无法确保编译期注解前后两次打包生成apk中生成的注解帮助类是否一致, 就算前后两次编译期间生成的注解类一致, 但如果生成的注解帮助类反射调用了方法, 那该方法也是不能被patch的.
另外一方面鉴于一般的开发者对注解框架原理不大熟悉, 注解框架可能既使用了编译期也使用运行期注解, 此时是不支持被patch的.
当然如果没有使用注解框架, 那么自定义运行时注解那么是可以被patch的. 或者使用了注解框架, 开发者能确保前后两次编译生成的注解编译类一致, 同时能自行辨认该方法不受注解的影响(该方法不被反射调用), 这样的情况下也是可以被patch的.
鉴于此, 所以hotfix统一标识:注解的类不支持被patch.
因为部分手机禁用了跨进程aidl导致应用连接失败, 主要问题发生在魅族/小米手机上居多,只要手动设置开启即可
UnsatisfiedLinkError... libandfix.so
找不到so文件, 参考part1节说明
IllegalArgumentException: Expected receiver of type com.taobao.hotfix.demo.BaseBug_CF_1476949855532, but got com.taobao.hotfix.demo.BaseBug
patch方法被反射调用了
java.io.InvalidClassException: com.demo.Temp; local class incompatible: stream classdesc serialVersionUID = -3445057096334719727, local class serialVersionUID = 4436690298739845368
反序列话失败, 我们知道实现了Serializable接口的一个类如果发生了任何变更(不包括方法内部的具体实现), 那么运行期间computeSerialVersionUID计算得到的SUID和反序列化文件得到的SUID不一致, 所以反序列号失败. 实际上类你可能没有手动修改它, 但是由于hotfix内部运行的需要可能会把这个类的一些method和field的访问符强制转为public, 那么这种情况下就会导致反序列失败. 这种情况下, 我们建议强制写死static final long serialVersionUID=value