热修复 阿里的AndFix

介绍

大致原理
apkpatch将两个apk做一次对比,然后找出不同的部分。
将生成的apatch文件后缀改成zip再解压开, 可以看到里面有一个dex文件。
通过jadx查看一下源码,里面就是被修复的代码所在的类文件,这些更改过的类都加上了一个_CF的后缀,并且变动的方法都被加上了一个叫 @MethodReplace 的 annotation,通过 clazz 和 method 指定了需要替换的方法。
然后客户端得到补丁文件后就会根据 annotation 来寻找需要替换的方法。最后由JNI层完成方法的替换。

多次打补丁
如果本地保存了多个补丁,那么 AndFix 会按照补丁生成的时间顺序加载补丁。具体是根据 .apatch 文件中的 PATCH.MF 的字段 Created-Time。
PS:刚开始做的 demo 中,每次产生的 apatch 文件用的名字都是相同的,结果导致只有第一次的补丁能生效。 看了源码后发现只有每次名字不同才能加载。

安全性
readme 提示开发者需要验证下载过来的 apatch 文件的签名是否就是在使用 apkpatch 工具时使用的签名,如果不验证那么任何人都可以制作自己的 apatch 文件来对你的 APP 进行修改。
但是我看到 AndFix 已经做了验证,如果补丁文件的证书和当前 apk 的证书不是同一个的话,就不能加载补丁。
官网还有一条,提示需要验证 optimize file 的指纹,应该是为了防止有人替换掉本地保存的补丁文件,所以要验证 MD5 码,然而 SecurityChecker 类里面也已经做了这个工作。但是这个 MD5 码是保存在 sharedpreference 里面,如果手机已经 root 那么还是可以被访问的。

局限性
  • 无法添加新类和新的字段
  • 需要使用加固前的 apk 制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露
  • 使用加固平台可能会使热补丁功能失效(看到有人在360加固提了这个问题,自己还未验证)

文档

GitHub    API文档 

AndFix is a library that offer hot-fix for Android App.
AndFix is a solution to fix the bugs online instead of redistributing重新发布 Android App. It is distributed分布式、分布、发布 as Android Library.
Andfix is an acronym缩写 for "Android hot-fix".
AndFix supports Android version from 2.3 to 7.0, both ARM and X86 architecture, both Dalvik and ART runtime, both 32bit and 64bit.
The compressed被压缩的 file format of AndFix's patch is .apatch. It is dispatched from your own server to client to fix your App's bugs.

Principle 原理

The implementation principle实现原理 of AndFix is method body's replacing,
AndFix judges判断 the methods should be replaced by java custom annotation and replaces it by hooking it. AndFix has a native method art_replaceMethod in ART or dalvik_replaceMethod in Dalvik.

For more details, here.

Fix Process 修复过程


Developer Tool 开发工具

AndFix provides a patch-making tool called apkpatch.

How to use?
Prepare two android packages, one is the online package, the other one is the package after you fix bugs by coding.
Generate .apatch file by providing the two package:
usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
 -a,--alias <alias>     alias.
 -e,--epassword <***>   entry password.
 -f,--from <loc>        new Apk file path.
 -k,--keystore <loc>    keystore path.
 -n,--name <name>       patch name.
 -o,--out <dir>         output dir.
 -p,--kpassword <***>   keystore password.
 -t,--to <loc>          old Apk file path.
9
9
 
1
usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
2
 -a,--alias <alias>     alias.
3
 -e,--epassword <***>   entry password.
4
 -f,--from <loc>        new Apk file path.
5
 -k,--keystore <loc>    keystore path.
6
 -n,--name <name>       patch name.
7
 -o,--out <dir>         output dir.
8
 -p,--kpassword <***>   keystore password.
9
 -t,--to <loc>          old Apk file path.

Sometimes, your team members may fix each other's bugs, and generate not only one .apatch. For this situation, you can merge .apatch files using this tool:
usage: apkpatch -m <apatch_path...> -k <keystore> -p <***> -a <alias> -e <***>
 -a,--alias <alias>     alias.
 -e,--epassword <***>   entry password.
 -k,--keystore <loc>    keystore path.
 -m,--merge <loc...>    path of .apatch files.
 -n,--name <name>       patch name.
 -o,--out <dir>         output dir.
 -p,--kpassword <***>   keystore password.
8
8
 
1
usage: apkpatch -m <apatch_path...> -k <keystore> -p <***> -a <alias> -e <***>
2
 -a,--alias <alias>     alias.
3
 -e,--epassword <***>   entry password.
4
 -k,--keystore <loc>    keystore path.
5
 -m,--merge <loc...>    path of .apatch files.
6
 -n,--name <name>       patch name.
7
 -o,--out <dir>         output dir.
8
 -p,--kpassword <***>   keystore password.

Now you get the application savior救星, the patch file. Then you need to dispatch it to your client in some way, push or pull.

ProGuard 混淆

If you enable ProGuard, you must save the mapping.txt, so your new version's build can use it with " -applymapping".
And it is necessary to keep classes as follow,
  • Native method    com.alipay.euler.andfix.AndFix
  • Annotation    com.alipay.euler.andfix.annotation.MethodReplace

To ensure that these classes can be found after running an obfuscation混淆的、困惑的 and static analysis tool like ProGuard, add the configuration below to your ProGuard configuration file.
-keep class * extends java.lang.annotation.Annotation
-keepclasseswithmembernames class * {
    native <methods>;
}
-keep class com.alipay.euler.andfix.** { *; }
5
5
 
1
-keep class * extends java.lang.annotation.Annotation
2
-keepclasseswithmembernames class * {
3
    native <methods>;
4
}
5
-keep class com.alipay.euler.andfix.** { *; }

Security 安全

The following is important but out of AndFix's range.
  • verify the signature of patch file
  • verify the fingerprint指纹 of optimize优化 file

使用步骤

1、引入依赖:
compile 'com.alipay.euler:andfix:0.5.0@aar'
1
1
 
1
compile 'com.alipay.euler:andfix:0.5.0@aar'
不混淆:
-keep class * extends java.lang.annotation.Annotation
-keepclasseswithmembernames class * {
    native <methods>;
}
-keep class com.alipay.euler.andfix.** { *; } 
 
1
-keep class * extends java.lang.annotation.Annotation
2
-keepclasseswithmembernames class * {
3
    native <methods>;
4
}
5
-keep class com.alipay.euler.andfix.** { *; } 

2、初始化
在Application.onCreate() 中初始化
PatchManager patchManager = new PatchManager(this);
patchManager.init("版本号");
patchManager.loadPatch();
7
1
PatchManager patchManager = new PatchManager(this);
2
patchManager.init("版本号");
3
patchManager.loadPatch();
注意每次 appversion 变更都会导致所有补丁被删除,如果 appversion 没有改变,则会加载已经保存的所有补丁。

3、加载补丁
在需要的地方调用 PatchManager 的 addPatch 方法加载新补丁,比如可以在进入应用时请求一下服务器,如果服务器告诉客户端有补丁(服务器应有针对性的告诉不同版本的客户端是否需要下载补丁,以及需要下载那个补丁),则可以在补丁下载完成之后调用。
patchManager.addPatch(patchPath);//path of the patch file that was downloaded
 
1
patchManager.addPatch(patchPath);//path of the patch file that was downloaded

4、生成补丁包
如果发现目前线上的安装包有bug,则可以通过如下步骤打补丁包:
  • 4.1、首先生成一个具有bug的 old.apk 的安装包,并将其安装到手机上
  • 4.2、然后更改代码把这个bug改掉,改掉后再生成一个新的 new.apk 的安装包
  • 4.3、通过官方提供的工具 apkpatch 在 MD 命令行中生成补丁文件:
C:\Users\baiqi>cd/d D:\bqt
D:\bqt>apkpatch -f new.apk -t old.apk -o . -n out1 -k test.keystore -p *** -a zxwk -e ***
2
2
 
1
C:\Users\baiqi>cd/d D:\bqt
2
D:\bqt>apkpatch -f new.apk -t old.apk -o . -n out1 -k test.keystore -p *** -a zxwk -e ***
执行以后会生成一个 .apatch 格式的补丁文件。
接下来通过网络传输等方式将 apatch 文件传到手机上,在运行到 addPatch 的时候就会加载补丁。
加载过的补丁会被保存到 data/packagename/files/apatch_opt 目录下,所以下载过来的补丁用过一次就可以删除了。

案例

Application

public class App extends Application {
	
	@Override
	public void onCreate() {
		super.onCreate();
		initAndFix();
	}
	
	private void initAndFix() {
		try {
			PatchManager patchManager = new PatchManager(this);
			String appversion = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
			patchManager.init(appversion);
			patchManager.loadPatch();
		} catch (PackageManager.NameNotFoundException e) {
			e.printStackTrace();
		}
	}
}
x
 
1
public class App extends Application {
2
    
3
    @Override
4
    public void onCreate() {
5
        super.onCreate();
6
        initAndFix();
7
    }
8
    
9
    private void initAndFix() {
10
        try {
11
            PatchManager patchManager = new PatchManager(this);
12
            String appversion = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
13
            patchManager.init(appversion);
14
            patchManager.loadPatch();
15
        } catch (PackageManager.NameNotFoundException e) {
16
            e.printStackTrace();
17
        }
18
    }
19
}

BugClass

public class BugClass {//有问题的类,将会被 下面的类 替换掉
	public static String staticMethod(String str) {
		return "有问题的类:" + str;
	}
	
	public int normalMethod() {
		return 0;
	}
}

/*public class BugClass {//用于修复Bug后 的新类,注意,打包时是用原先的类名和包名,也就是会用这个新类完全替换旧的类
	
	public static String staticMethod(String str) {
		return "修复后的类:" + str;
	}
	
	public int normalMethod() {
		return 10086;
	}
}*/
x
1
public class BugClass {//有问题的类,将会被 下面的类 替换掉
2
    public static String staticMethod(String str) {
3
        return "有问题的类:" + str;
4
    }
5
    
6
    public int normalMethod() {
7
        return 0;
8
    }
9
}
10
11
/*public class BugClass {//用于修复Bug后 的新类,注意,打包时是用原先的类名和包名,也就是会用这个新类完全替换旧的类
12
    
13
    public static String staticMethod(String str) {
14
        return "修复后的类:" + str;
15
    }
16
    
17
    public int normalMethod() {
18
        return 10086;
19
    }
20
}*/

MainActivity

public class MainActivity extends ListActivity {
	private static final String APATCH_PATH = "/bqt/out.apatch";//文件名应该是在下发时告诉我们
	
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		String[] array = {"修复补丁",
				"测试静态方法",
				"测试普通方法",};
		setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new ArrayList<>(Arrays.asList(array))));
	}
	
	@Override
	protected void onListItemClick(ListView l, View v, int position, long id) {
		switch (position) {
			case 0:
				addPatch();
				break;
			case 1:
				Toast.makeText(this, BugClass.staticMethod("hello"), Toast.LENGTH_SHORT).show();
				break;
			case 2:
				Toast.makeText(this, "值为:" + new BugClass().normalMethod(), Toast.LENGTH_SHORT).show();
				break;
		}
	}
	
	private void addPatch() {
		try {
			PatchManager patchManager = new PatchManager(this);
			String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
			patchManager.addPatch(patchPath);//path of the patch file that was downloaded
			Toast.makeText(this, "热修复完成", Toast.LENGTH_SHORT).show();
			File f = new File(patchPath);
			if (f.exists()) {
				boolean result = f.delete();
				Log.i("bqt", "加载补丁成功后,删除下载的补丁:" + result);
			}
		} catch (IOException e) {
			e.printStackTrace();
			Toast.makeText(this, "修复失败", Toast.LENGTH_SHORT).show();
		}
	}
}
43
 
1
public class MainActivity extends ListActivity {
2
    private static final String APATCH_PATH = "/bqt/out.apatch";//文件名应该是在下发时告诉我们
3
    
4
    protected void onCreate(Bundle savedInstanceState) {
5
        super.onCreate(savedInstanceState);
6
        String[] array = {"修复补丁",
7
                "测试静态方法",
8
                "测试普通方法",};
9
        setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new ArrayList<>(Arrays.asList(array))));
10
    }
11
    
12
    @Override
13
    protected void onListItemClick(ListView l, View v, int position, long id) {
14
        switch (position) {
15
            case 0:
16
                addPatch();
17
                break;
18
            case 1:
19
                Toast.makeText(this, BugClass.staticMethod("hello"), Toast.LENGTH_SHORT).show();
20
                break;
21
            case 2:
22
                Toast.makeText(this, "值为:" + new BugClass().normalMethod(), Toast.LENGTH_SHORT).show();
23
                break;
24
        }
25
    }
26
    
27
    private void addPatch() {
28
        try {
29
            PatchManager patchManager = new PatchManager(this);
30
            String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
31
            patchManager.addPatch(patchPath);//path of the patch file that was downloaded
32
            Toast.makeText(this, "热修复完成", Toast.LENGTH_SHORT).show();
33
            File f = new File(patchPath);
34
            if (f.exists()) {
35
                boolean result = f.delete();
36
                Log.i("bqt", "加载补丁成功后,删除下载的补丁:" + result);
37
            }
38
        } catch (IOException e) {
39
            e.printStackTrace();
40
            Toast.makeText(this, "修复失败", Toast.LENGTH_SHORT).show();
41
        }
42
    }
43
}
2018-6-11




猜你喜欢

转载自www.cnblogs.com/baiqiantao/p/9168094.html