简介
AndFix是阿里开源的一个Android热补丁框架,App可以在不重新发布版本的情况下,通过补丁替换出现bug的方法,达到修复bug的目的。现在支持android2.3到7.0,支持ARM 和X86 (AndFix supports Android version from 2.3 to 7.0, both ARM and X86 architecture, both Dalvik and ART runtime, both 32bit and 64bit)
优点:集成方便,使用简单
缺点:只能替换方法,不支持新增方法,新增类,新增field等
AndFix文档
集成步骤
发现bug生成apatch—>将apatch下发到用户手机存储系统—>利用AndFix完成apatch安装,解决Bug
1.AndroidStudio 创建一个工程AndFixDemo,在app下的build.gradle添加引用
implementation 'com.alipay.euler:andfix:0.5.0@aar'//AndFix
根据官方文档,对AndFix进行使用
创建一个AndFixPatchManager,管理AndFix所有的api
public class AndFixPatchManager
{
private static AndFixPatchManager mInstance = null;
private static PatchManager mPatchManager = null;
public static AndFixPatchManager getInstance()
{
if (mInstance == null)
{
synchronized (AndFixPatchManager.class)
{
if (mInstance == null)
{
mInstance = new AndFixPatchManager();
}
}
}
return mInstance;
}
/**
* Initialize PatchManager
* @param context
*/
public void initPatch(Context context){
mPatchManager = new PatchManager(context);
mPatchManager.init(Utils.getAppversion(context));
//Load patch
mPatchManager.loadPatch();
}
/**
* Add patch
* @param path
*/
public void addPatch(String path){
if(mPatchManager!=null){
try
{
mPatchManager.addPatch(path);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
创建一个工具类Utils
public class Utils
{
/**
* 获取应用程序appversion
*/
public static String getAppversion(Context context)
{
String appversion = "";
try
{
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
appversion = pi.versionName;
}
catch (Exception e)
{
e.printStackTrace();
}
return appversion;
}
public static void printLog(){
//error设置成会闪退,用于生成bug
String error=null;
Log.e("error_log",error);
}
}
创建自定义的MyAppliction
public class MyAppliction extends Application
{
@Override
public void onCreate()
{
super.onCreate();
//初始化AndFix模块
initAndFix();
}
private void initAndFix(){
AndFixPatchManager.getInstance().initPatch(this);
}
}
在AndroidManifest.xml进行引用
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name=".MyAppliction">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
因为AndFix补丁的后缀是 .apatch,所以思路就是在程序里将.apatch文件保存到手机指定目录里,然后从本地目录加载.apatch进行修复
public class MainActivity extends AppCompatActivity
{
private static final String FILE_END = ".apatch";
private String mPatchDir;///storage/emulated/0/Android/data/drag.mandala.com.andfixdemo/cache/apatch/
private Button btnCreateBug,btnFixBug;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnCreateBug = findViewById(R.id.btn_create_bug);
btnFixBug = findViewById(R.id.btn_fix_bug);
mPatchDir = getExternalCacheDir().getAbsolutePath() + "/apatch/";
//创建文件夹
File file = new File(mPatchDir);
if (file == null || !file.exists())
{
file.mkdir();
}
}
public void createBug(View view){
Utils.printLog();
}
public void fixBug(View view){
AndFixPatchManager.getInstance().addPatch(getPatchName());
}
//patch文件名
private String getPatchName(){
return mPatchDir.concat("andFixRelease").concat(FILE_END);
}
}
我华为手机,本地的apatch目录在/storage/emulated/0/Android/data/drag.mandala.com.andfixdemo/cache/apatch/
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:gravity="center">
<Button
android:id="@+id/btn_create_bug"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="产生bug"
android:onClick="createBug"
android:padding="20dp"
android:textSize="20sp"
/>
<Button
android:id="@+id/btn_fix_bug"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="修复bug"
android:onClick="fixBug"
android:textSize="20sp"
/>
</LinearLayout>
就两个button,一个用于生成bug,一个用于修复bug
现在生成一个jks,打包一个apk,命名为old.apk,安装到手机上,点击“产生bug”按钮因为
public static void printLog(){
//error设置成会闪退,用于生成bug
String error=null;
Log.e("error_log",error);
}
所以会闪退
现在修复这个Bug
public static void printLog(){
//error设置成会闪退,用于生成bug
String error="修复bug";
Log.e("error_log",error);
}
重新用jks打包,命名为new.apk,官方文档下载apkpatch
下载好了,把两个apk放进去,新建一个outputs文件夹,根据官方文档,生成.apatch 文件
命令行定位到apkpatch所在目录
我放在了H盘
C:\Users\intel>H:
H:\>cd H:\apkpatch-1.0.3
H:\apkpatch-1.0.3>apkpatch.bat
ApkPatch v1.0.3 - a tool for build/merge Android Patch file (.apatch).
Copyright 2015 supern lee <sanping.li@alipay.com>
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.
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.
H:\apkpatch-1.0.3>apkpatch.bat -f new.apk -t old.apk -o outputs/ -k demo.jks
-p aaaaaa -a demo t -e aaaaaa
add modified Method:V printLog() in Class:Ldrag/mandala/com/andfixdemo/Utils;
H:\apkpatch-1.0.3>
“add modified Method:V printLog() in Class:Ldrag/mandala/com/andfixdemo/Utils;”这一句表示已经修改的方法
这样在outputs文件夹里就生成了apatch文件,修改文件名为andFixRelease.apatch,
和MainActivity里的保持一致,防止加载不到
//patch文件名
private String getPatchName(){
return mPatchDir.concat("andFixRelease").concat(FILE_END);
}
将andFixRelease.apatch放入手机指定文件夹里,就是MainActivity定义的目录地址
mPatchDir = getExternalCacheDir().getAbsolutePath() + "/apatch/";
重新运行app,点击产生bug,会闪退,点击修复bug,就会Add patch,进行修复
在实际开发中,不可能让用户自己将apatch文件传入手机,这个需要放到服务器,通过接口,获取是否有新的apatch文件,然后下载,下载到指定目录,成功以后,Add patch
注意:每次产生的apatch文件的名字如果是相同的,结果会导致只有第一次的补丁能生效。只有每次名字不同才能加载,可以在保存之前,将已经存在的补丁删除掉。
原理
先分析一下生成的apatch文件,先下载反编译工具jadx
jdax
将andFixRelease.apatch后缀修改成.zip,解压,解压以后的文件目录是
打开命令行,定位到jadx-1.0.0\lib的目录,然后运行
java -jar jadx-gui-1.0.0.jar
会打开一个界面,选择classes.dex,
发现有一个MethodReplace注解,再在Android项目中,打开源码,在AndFixManager类中有一个fixClass方法
里边也有MethodReplace,
引用一篇文章Andfix热修复技术使用对原理的解析:
apkpatch将两个apk做一次对比,然后找出不同的部分。可以看到生成的apatch了文件,后缀改成zip再解压开,里面有一个dex文件。通过jadx查看一下源码,里面就是被修复的代码所在的类文件,这些更改过的类都加上了一个_CF的后缀,并且变动的方法都被加上了一个叫@MethodReplace的annotation,通过clazz和method指定了需要替换的方法。然后客户端sdk得到补丁文件后就会根据annotation来寻找需要替换的方法。最后由JNI层完成方法的替换。如果本地保存了多个补丁,那么AndFix会按照补丁生成的时间顺序加载补丁。具体是根据.apatch文件中的PATCH.MF的字段Created-Time。
AndFix通过Java的自定义注解来判断一个方法是否应该被替换,如果可以就会hook该方法并进行替换。AndFix在ART架构上的Native方法是art_replaceMethod 、在X86架构上的Native方法是dalvik_replaceMethod。他们的实现方式是不同的。对于Dalvik,它将改变目标方法的类型为Native同时hook方法的实现至AndFix自己的Native方法,这个方法称为dalvik_dispatcher,这个方法将会唤醒已经注册的回调,这就是我们通常说的hooked(挂钩)。对于ART来说,我们仅仅改变目标方法的属性来替代它。