簡介
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來說,我們僅僅改變目標方法的屬性來替代它。