1. 类Field修复的说明
不允许新增类Field。
不允许修改构造函数第一个点很好理解, 第二点由于不允许修改构造函数,所以导致类Field是不允许直接修改的。
//修改前
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...";
}
}
2. 代码明明没有新增全局field, 为什么补丁工具提示新增了field?
代码修复前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直接报错异常退出,打补丁失败,需要避免这种情况。
3. 代码明明没有新增method, 为什么补丁工具提示新增了method?
修复前, 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. 混淆配置导致的其它坑
其实除了4.9.2以及4.9.3节中说明可能导致的新增filed/method情况之外, 还有另一种情况. 我们知道打包编译的阶段可能会进行一系列的优化,包括方法的内敛/裁剪等等,这些东西平时我们是不关注的,但是在生成patch包的时候就会有影响. 如果你的应用混淆配置文件中没有加上-dontoptimize
这一项, 那么很有可能导致方法被内敛/裁剪。举例如下, 以下例子都是在gradle2.14.1版本的表现, 其它gradle版本表现是否一致,有待考证。
实例1 方法的内敛
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方法异常退出, 打补丁失败。需要避免这种情况。
实例2 方法的裁剪
这里先假设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语句也很可能会被优化掉的。
实例3 android默认混淆配置文件
一般情况下项目的混淆配置都会使用到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'
}
}