使用补丁工具打补丁过程中出现异常如何处理?

1. 类Field修复的说明

  1. 不允许新增类Field。

  2. 不允许修改构造函数第一个点很好理解, 第二点由于不允许修改构造函数,所以导致类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'
        }
    }