前言
最近發(fā)現(xiàn)熱修復比較火伴奥,很多文章也做了介紹写烤。所以自己也簡單的學習下。因為自己在實際項目中并沒有用到拾徙。所以為了防止忘記洲炊,寫成博客做成筆記,同時也幫助一些沒有接觸過的小伙伴能快速使用與入門暂衡。廢話少說。進入主題玄叠。
熱修復的概念
上面是熱修復古徒。簡單解釋就是在線更新。比如我們已發(fā)布的應用突然產(chǎn)生了嚴重的BUG读恃,按照舊方法隧膘,只能能下一次版本修復后重新發(fā)布。然后用戶重新去下載寺惫。這樣其實給用戶的體驗就很不好疹吃。既浪費流量同時在重新下載的時候也會產(chǎn)生用戶流失等等一系列影響∥魅福可能只是一個小小的BUG就到時用于的流失萨驶,顯然不是我們想看到的。那么有沒有上面方法可以在不發(fā)布新版本艇肴,重新下載的情況下修復BUG呢腔呜?因此熱修復技術應運而生。下面我們來現(xiàn)在現(xiàn)在比較火的而修復都有那些:
這4中熱修復技術各有優(yōu)缺點他們分別是微信-Tinker再悼,QQ空間超級補丁-QZone,阿里-AndFix核畴,美團-Robust。從圖中我們也可以了解到冲九。功能最少的是AndFix谤草,Tinker最復雜。當然這不是決定他們好壞的標準。具體的選擇還是根據(jù)自身的時機情況而定丑孩。而選擇學習的話冀宴,選一個最容易和一個最復雜的。在熟練后理解其他的也會水到渠成温学。
AndFix
這篇文章就先來學習下AndFix略贮。關于Tinker請參考我的另一篇文章。
-
特點:
與Tinker相比他的特點就是即生效枫浙,不過只能修復方法級別的BUG刨肃,不支持gradle。他的修復流程這里就不過多介紹了箩帚,詳細見AndFix官網(wǎng)真友。用一句話總結,就是找到BUG的方法紧帕,修改后生成apatch文件并通過注解標記修復的方法盔然。在修復時就加載修復補丁文件,完成修復是嗜。下面我們就來具體使用下愈案。具體步驟如下:
- 引入依賴
//引入AndFix熱修復模塊
compile 'com.alipay.euler:andfix:0.5.0@aar'
可以看到和我們平時引用其他庫基本是一樣的,非常簡單鹅搪。
-
核心方法
其實不光他的引入非常簡單站绪,使用起來也是非常簡單。主要的Api就4個方法丽柿。如下:
PatchManager patchManager = new PatchManager(this); //創(chuàng)建PatchManager 他是Andfix的核心類
patchManager.init(appVersion);//初始化 傳入應用版本號
patchManager.loadPatch(); //加載 官網(wǎng)建議我們初始化完成后就調用
patchManager.addPatch(path); //傳入指定文件 修復BUG
好了應用層的核心代碼就這些了恢准。下面就來實現(xiàn)一個有BUG的應用,然后利用AndFix去修復甫题。代碼如下:
XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.ggxiaozhi.hotfix.MainActivity">
<Button
android:onClick="onClick"
android:text="Bug"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:onClick="onFix"
android:text="Fix"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
MainActivity代碼:
public class MainActivity extends AppCompatActivity {
private static final String FILE_END = ".apatch";//規(guī)定修復補丁的文件格式是.apatch文件
private String mPatchDir;//修復補丁的存放路徑
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//最后的文件所在路徑為storage/emulated/0/Android/data/com.example.ggxiaozhi.hotfix/cache/apatch/
mPatchDir = getExternalCacheDir().getAbsolutePath() + "/apatch/";
//創(chuàng)建文件夾
File file = new File(mPatchDir);
if (!file.exists()) {
file.mkdir();
}
}
private void showToast() {
boolean isShow = false;
String str = "存在一個BUG";
if (isShow) {
str = "方法BUG修復完成";
}
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}
public void onClick(View view) {
showToast();
}
public void onFix(View view) {
AndFixPatchManager.getInstance().addPatch(getPatchName());
}
//加載修復文件的文件名
public String getPatchName() {
return mPatchDir.concat("andfix").concat(FILE_END);
}
}
代碼也非常簡單馁筐。這里講解下。首先定義2個常量坠非,一個是固定了我們加載補丁的文件格式敏沉。另一個是補丁文件所在的文件夾。這里使用的是應用的內部文件夾下的apatch文件夾炎码。這個就是放補丁文件的文件夾位置盟迟。前面我們說了AndFix只能修復方法級的BUG,所以正常情況下點擊產(chǎn)生BUG會Toast:存在一個BUG潦闲,當我們修復完成夠就會Toast:方法BUG修復完成队萤。道理很簡單。現(xiàn)在我們要做的就是生成一個有問題的帶簽名apk(關于如何生成帶簽名的apk這里我就不過多介紹了)矫钓。生成后改名為old.apk,也就是存在BUG的apk。
提示:這里我們用到的AndFixPatchManager只不過是對PatchManager的簡單封裝:
AndFixPatchManager:
public class AndFixPatchManager {
private static AndFixPatchManager mInstance;
private static PatchManager mPatchManager;
public static AndFixPatchManager getInstance() {
if (mInstance == null) {
synchronized (AndFixPatchManager.class) {
if (mInstance == null) {
mInstance = new AndFixPatchManager();
}
}
}
return mInstance;
}
/**
* 初始化AndFix
*
* @param context 上下文
*/
public void initPatch(Context context) {
//初始化
mPatchManager = new PatchManager(context);
mPatchManager.init(Utils.getVersionName(context));
//加載patch
mPatchManager.loadPatch();
}
/**
* 加載我們的Patch文件
* @param path .patch文件 路徑
*/
public void addPatch(String path) {
try {
if (mInstance!=null){
mPatchManager.addPatch(path);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
這里沒有什么難點新娜,不過別忘了在Application中初始化調用initPatch()方法赵辕,同時要加上讀寫內存卡的權限
- 修改BUG
現(xiàn)在我們想修復這個BUG,那么我們修改代碼如下:
...省略
private void showToast() {
boolean isShow = true;
String str = "存在一個BUG";
if (isShow) {
str = "方法BUG修復完成";
}
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}
...省略
這里也非常簡單概龄,我們只是把isShow改成true还惠,這樣修復后就與old.apk打印的結果不同。完成修復私杜。省略部分完全不變蚕键。
-
命令行生成.apatch補丁文件
準備工作已經(jīng)完成了下面我們利用Andfix工具來生成.patch補丁文件。下載完成后目錄如下:
圖片.png
這里對應.bat是Window用來生成補丁文件衰粹。另一個是mac生成補丁文件锣光。我這里用的是Window所以我用的是.bat(如果想直接在命令行使用別忘記配置環(huán)境變量)。同時把有BUG的old.apk文件與修復BUG后的new.apk文件同時將簽名文件一起放在這個目錄下铝耻。完成這些后此路徑如下:圖片.png完成這些后使用命令行輸入如下:
QQ圖片20180114151621.png
這里為了考慮一些基礎比較差的小伙伴誊爹。來簡單說明下參數(shù):
首先有可以看到有2個命令
- -f 這個是用來生成.apatch補丁文件的;
- -m 是用來合并多個.apatch文件的瓢捉。
這里我們是生成所以用到第一個命令频丘。這里需要填入的參數(shù)分別為:
-a 簽名文件的別名
-e 簽名文件別名的密碼
-f 修改BUG的.apk文件
-k 簽名文件的路徑
-n 簽名文件的名字
-o 補丁文件的輸出文件夾路徑(沒有會自動創(chuàng)建)
-p 簽名文件密碼
-
-t 存在BUG的.apk文件
在明白這些后我們就會在outputs文件下生成.apatch文件。在實際開發(fā)中我們可以將這個文件方法服務器上泡态。然后用戶去拉取文件下載到指定目錄搂漠,或是服務器主動推到用戶應用上。這里為了簡便我是直接將補丁文件裝到了上面我們定義的應用內部路徑上的(生成的.apatch文件的名字記得要修改某弦,因為前面我們定義了文件名為annfix.apatch文件桐汤。避免找不到)。
- 修復BUG
最后直接運行后點擊修復Button就完成修復了刀崖。這里我用的是真機惊科。截圖有些麻煩就不給大家截圖了。親測有效亮钦。
-
總結
上面就是Andfix的使用馆截。下面我們來總結下- 首先熱修復都存在一些兼容性的問題如果選擇Andfix(其他也一樣)要做好兼容性的處理。我在使用時先用的小米2A(api=19)修復失敗蜂莉。魅藍Note(api=21)修復成功蜡娶。如果流程沒有問題看看是不是機型不支持熱修復
- 只能修復方法級別的BUG。通過官網(wǎng)我們也已經(jīng)知道了映穗。所以它無法添加新類和新字段窖张。.無法動態(tài)加入新功能模塊,有別于dex的替換蚁滋。他的思路如下:
圖片.png
但是也不是所有的方法級別的BUG都能修復宿接。如:
image注 此圖是通過其他文章所得赘淮,具體實際情況還需要自己實踐
- 因為他本身是一個依賴,所以在混淆等操作時處理一致睦霎。
一些基本的注意點和總結目前就這些梢卸,待以后熟練后在加入一些新的要注意的點。
AndFix原理分析
說道原理分期副女,那我們就不得不去看下他的源碼蛤高。下面我們就從上面提到的4個API入手看下。
mPatchManager = new PatchManager(context);
我們先看下這個方法:
public class PatchManager {
private static final String TAG = "PatchManager";
// patch extension
private static final String SUFFIX = ".apatch";
private static final String DIR = "apatch";
private static final String SP_NAME = "_andfix_";
private static final String SP_VERSION = "version";
/**
* context
*/
private final Context mContext;
/**
* AndFix manager 真正修復的類
*/
private final AndFixManager mAndFixManager;
/**
* patch directory 修復補丁路徑
*/
private final File mPatchDir;
/**
* patchs .apatch文件包裝類
*/
private final SortedSet<Patch> mPatchs;
/**
* classloaders
*/
private final Map<String, ClassLoader> mLoaders;
/**
* @param context
* context
*/
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);
mPatchDir = new File(mContext.getFilesDir(), DIR);
mPatchs = new ConcurrentSkipListSet<Patch>();
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
....
}
首先在PatchManager類中定義了幾個常量碑幅,同時在構造方法中進行初始化戴陡。下面我們接著看第二個api:
mPatchManager.init(Utils.getVersionName(context));
PatchManager#init()方法如下:
...
/**
* initialize
*
* @param appVersion
* App version
*/
public void init(String appVersion) {
//如果文件路徑不存在 直接返回
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
//如果不是路徑 直接返回
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
//讀取數(shù)據(jù)存儲XML中版本號
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {//如果.apatch文件中的版本號與上次不同說明進行了版本迭代 那么就刪除所有的.apatch補丁文件
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
//初始化補丁文件
initPatchs();
}
}
private void initPatchs() {
//得到補丁文件夾下的所有補丁文件集合
File[] files = mPatchDir.listFiles();
for (File file : files) {
//遍歷集合 將文件封裝成Patch類
addPatch(file);
}
}
/**
* add patch file
*
* @param file
* @return patch
*/
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {//判斷傳入的文件是否是補丁文件格式
try {
// 將文件封裝成Patch類
patch = new Patch(file);
//加入集合
mPatchs.add(patch);
} catch (IOException e) {
Log.e(TAG, "addPatch", e);
}
}
//返回
return patch;
}
//版本升級則刪除補丁文件
private void cleanPatch() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
mAndFixManager.removeOptFile(file);
if (!FileUtil.deleteFile(file)) {
Log.e(TAG, file.getName() + " delete error.");
}
}
}
...
這里省略其他代碼,主要的代碼都有注釋沟涨。大致流程就是恤批,在調用init()初始化的時候,先判斷有沒有版本更新拷窜。補丁文件與應用版本一致那么就會遍歷補丁文件夾下的所有文件并封裝成Patch類同時加入mPatchs集合中开皿。那么我來看下Patch類主要封裝了些神馬:
public class Patch implements Comparable<Patch> {
private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private static final String CLASSES = "-Classes";
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String CREATED_TIME = "Created-Time";
private static final String PATCH_NAME = "Patch-Name";
/**
* patch file
*/
private final File mFile;
/**
* name
*/
private String mName;
/**
* create time
*/
private Date mTime;
/**
* classes of patch key: 補丁文件名 value:修改了那些類,這些類的信息
*/
private Map<String, List<String>> mClassesMap;
public Patch(File file) throws IOException {
mFile = file;
init();
}
@SuppressWarnings("deprecation")
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
//將補丁文件封裝成JarFile
jarFile = new JarFile(mFile);
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
//獲取補丁文件的name
mName = main.getValue(PATCH_NAME);
mTime = new Date(main.getValue(CREATED_TIME));
mClassesMap = new HashMap<String, List<String>>();
Attributes.Name attrName;
String name;
List<String> strings;
//遍歷補丁文件中修改了那些類
for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
//判斷傳入的文件格式是否是我們Andfix能夠處理的格式
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// "-Classes"
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
...
}
Patch類中的核心代碼就init()方法篮昧。將我們的補丁文件封裝成jarFile格式赋荆,然后去解析。得到補丁文件中修改了BUG的類的信息懊昨。并將其放到mClassesMap集合中并對外提供方法方便其他類調用窄潭。
我們對第二個api:PatchManager#init()方法分析完成后,下面我們在對第三個API分析下:PatchManager#loadPatch():
/**
* load patch,call when application start
*
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
可以看到這個方法的注解 加載補丁文件酵颁,在我們的Application啟動的時候嫉你,這個是為什么我們在初始化的時候就調用loadPatch()方法的原因□锿铮可以看到這個就是講我們前面在AndFix指定的目錄下得到的patch文件集合進行遍歷并調用mAndFixManager.fix()方法幽污。看來這個方法才是真正修復BUG的簿姨。
public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
//進行一些安全性的判斷
if (!mSupport) {
return;
}
if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}
try {
File optfile = new File(mOptDir, file.getName());
boolean saveFingerprint = true;
if (optfile.exists()) {
// need to verify fingerprint when the optimize file exist,
// prevent someone attack on jailbreak device with
// Vulnerability-Parasyte.
// btw:exaggerated android Vulnerability-Parasyte
// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
if (mSecurityChecker.verifyOpt(optfile)) {
saveFingerprint = false;
} else if (!optfile.delete()) {
return;
}
}
//根據(jù)補丁文件創(chuàng)建DexFile
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
//找到那些需要修復并且有注解的類
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
//利用對DexFile的遍歷并找到我們要修復的class文件
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
//調用這個方法區(qū)修復
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace; //這個注解就是說明那個方法需要修復的注解
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
//進行方法替換
replaceMethod(classLoader, clz, meth, method);
}
}
}
/**
* replace method
*
* @param classLoader classloader
* @param clz class
* @param meth name of target method
* @param method source method
*/
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
...
//一直跟蹤會調用到下面AndFix類中的這方法
private static native void replaceMethod(Method dest, Method src);
這個流程就是就是先找到補丁文件中要修復的類距误,找到類后再找到這么類中要修復的方法。如何判斷哪些方法是需要修復的呢扁位?就是通過注解准潭。最后將這個帶注解的類利用類加載去加載,最后利用native層去實現(xiàn)替換域仇。由于本人能力有限刑然,native就不去分析了。那么這個注解和這個結果到底是怎么樣的呢暇务?能不能直觀的去看見呢泼掠?那么我們就從這個補丁文件入手怔软,其實這個補丁文件核心如下:
其實補丁文件的核心就是這個dex文件
META-INF下為:
可以看到這與在Patch類中定義的格式是一樣的。所以Patch是對補丁文件的包裝成類的武鲁。
上面我們可以看到AndFix是通過注解來獲取要被替換的方法爽雄,大家看AndFix的集成文檔可以看到這段: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。沒錯沐鼠,就是在生成補丁文件的時候把發(fā)生改變的類增加了一個CF后綴,然后把對應的方法動態(tài)加上了注解叹谁,最后丟到了補丁中的dex文件中饲梭,我們反編譯一下這個dex文件,看看AndFix幫我們生成的類:
可以看到,AndFix自動幫我們加上了一個methodReplace的注解焰檩,注解里的內容就是要被替換的原類中的類名和方法憔涉。最后我們看一下native層真正做的事情。到這里也就分析結束了析苫。
結語
這篇文章雖然對使用中的太多坑沒有過多的講解兜叨。不過對于完全沒有接觸過的小伙伴應該還是很有幫助的吧。從使用到原理我們都有了一定的認識衩侥。由于本人接觸也沒多久国旷,這篇文章主要是記錄自己的學習與幫助沒有接觸過的小伙伴應。如果更深入的理解還需要對.dex .class文件以及虛擬機和DVM都要有一定的理解(我也不懂茫死!哈哈)跪但。不過孰能生巧,在熟練使用后在去探究更深層次峦萎,會更容易理解屡久。如果想簡單了解.dex .class文件以及虛擬機和DVM請參考我的另外兩個筆記。下篇我們講講最難的Tinker的使用與分析爱榔。下篇見
如果對熱修復已經(jīng)很了解了:
推薦文章 黑科技熱修復的Java層實現(xiàn) 也可以用java層實現(xiàn)熱修復
本人是個接觸Android不就的菜鳥被环。很多熱修復也沒有研究的很透徹。如果有錯誤希望大家指出详幽,不勝感激筛欢。如果對您有幫助別忘了點個贊,評個論妒潭,留下你的足跡悴能。