熱修復設計之CLASS_ISPREVERIFIED(二)

阿里P7移動互聯網架構師進階視頻(每日更新中)免費學習請點擊:https://space.bilibili.com/474380680
本篇文章將繼續從CLASS_ISPREVERIFIED實戰來介紹熱修復設計:java

1、前言

本文將解決兩個問題。android

  1. 怎麼將修復後的Bug類打包成dex
  2. 怎麼將外部的dex插入到ClassLoader中

2、創建測試Demo

2.1 目錄結構

 
19956127-ee34540995528b64.png
 

2.2 源碼

activity_main.xml數組

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="click"
        android:text="小喵叫一聲"/>
</RelativeLayout>

MainActivity.class架構

package com.aitsuki.bugfix;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.aitsuki.bugfix.animal.Cat;

public class MainActivity extends AppCompatActivity {

    private Cat mCat;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCat = new Cat();
    }

    public void click(View view) {
        Toast.makeText(this, mCat.say(),Toast.LENGTH_SHORT).show();
    }
}

Cat.classapp

package com.aitsuki.bugfix.animal;

/**
 * Created by AItsuki on 2016/3/14.
 */
public class Cat {
    public String say() {
        return "汪汪汪!";
    }
}

2.3 運行結果

 
19956127-ffa85b60de3a1a37.png
 

假設這是咱們公司的開發項目,剛剛上線就發現了嚴重bug,貓會狗叫。
想修復bug,讓用戶再馬上更新一次顯然很不友好,此時熱補丁修復技術就有用了。框架

3、製做補丁

在加載dex的代碼以前,咱們先來製做補丁。
1. 首先咱們將Cat類修復,汪汪汪改爲喵喵喵,而後從新編譯項目。(Rebuild一下就好了)
2. 去保存項目的地方,將Cat.class文件拷貝出來,在這裏ide

 
19956127-0989390f7bb0258f.png
 

 

3. 新建文件夾,要和該Cat.class文件的包名一致,而後將Cat.class複製到這裏,如圖函數

 
19956127-da91cd0696e84f4b.png
 

 

4. 命令行進入到圖中的test目錄,運行一下命令,打包補丁。如圖:工具

 
19956127-ef3c14557d440be2.png
 

 

而後test目錄是這樣的學習

 

 
19956127-3c35a15d3e59a306.png
 

 

patch_dex.jar就是咱們打包好的補丁了,咱們將它放到sdCard中,待會從這裏加載補丁。

關於什麼用這麼複雜的方法打包補丁的說明:
你也能夠直接將java文件拷出來,經過javac -d帶包編譯再轉成jar。
但我這麼麻煩是有緣由的,由於用這種方法你可能會遇到ParseException,緣由是jar包版本和dx工具版本不一致。
而從項目中直接將編譯好的class直接轉成jar就沒問題,由於java會向下兼容,打出來的jar包和class版本是一致的。
總而言之,dx版本要和class編譯版本對應。

4、加載補丁

4.1 思路

經過上一篇博文,咱們知道dex保存在這個位置
BaseDexClassLoader–>pathList–>dexElements

apk的classes.dex能夠從應用自己的DexClassLoader中獲取。
path_dex的dex須要new一個DexClassLoader加載後再獲取。
分別經過反射取出dex文件,從新合併成一個數組,而後賦值給盈通自己的ClassLoader的dexElements

4.2 代碼實現

加載外部dex,咱們能夠在Application中操做。
首先新建一個HotPatchApplication,而後在清單文件中配置,順便加上讀取sdcard的權限,由於補丁就保存在那裏。

HotPatchApplication代碼以下:

package com.aitsuki.hotpatchdemo;

import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;

/**
 * Created by hp on 2016/4/6.
 */
public class HotPatchApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // 獲取補丁,若是存在就執行注入操做
        String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
        File file = new File(dexPath);
        if (file.exists()) {
            inject(dexPath);
        } else {
            Log.e("BugFixApplication", dexPath + "不存在");
        }
    }

    /**
     * 要注入的dex的路徑
     *
     * @param path
     */
    private void inject(String path) {
        try {
            // 獲取classes的dexElements
            Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
            Object pathList = getField(cl, "pathList", getClassLoader());
            Object baseElements = getField(pathList.getClass(), "dexElements", pathList);

            // 獲取patch_dex的dexElements(須要先加載dex)
            String dexopt = getDir("dexopt", 0).getAbsolutePath();
            DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
            Object obj = getField(cl, "pathList", dexClassLoader);
            Object dexElements = getField(obj.getClass(), "dexElements", obj);

            // 合併兩個Elements
            Object combineElements = combineArray(dexElements, baseElements);

            // 將合併後的Element數組從新賦值給app的classLoader
            setField(pathList.getClass(), "dexElements", pathList, combineElements);

            //======== 如下是測試是否成功注入 =================
            Object object = getField(pathList.getClass(), "dexElements", pathList);
            int length = Array.getLength(object);
            Log.e("BugFixApplication", "length = " + length);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 經過反射獲取對象的屬性值
     */
    private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }

    /**
     * 經過反射設置對象的屬性值
     */
    private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }

    /**
     * 經過反射合併兩個數組
     */
    private Object combineArray(Object firstArr, Object secondArr) {
        int firstLength = Array.getLength(firstArr);
        int secondLength = Array.getLength(secondArr);
        int length = firstLength + secondLength;

        Class<?> componentType = firstArr.getClass().getComponentType();
        Object newArr = Array.newInstance(componentType, length);
        for (int i = 0; i < length; i++) {
            if (i < firstLength) {
                Array.set(newArr, i, Array.get(firstArr, i));
            } else {
                Array.set(newArr, i, Array.get(secondArr, i - firstLength));
            }
        }
        return newArr;
    }

}

5、CLASS_ISPREVERIFIED

運行一下Demo,報如下錯誤。(AndroidStudio 2.0可能不會報錯,須要打包的時候纔會出現錯誤,這是Instant run致使的)

 

 
19956127-06454a85485d449f.png
 

 

dexElements的length = 2,看來咱們的patch_dex已經成功添加進去了。
可是從黃色框框和黃色框上面那一段log提示中能夠看出,MainActivity引用了Cat,可是發現他們在不一樣的Dex中。

看到這裏可能就會問:
爲何以前那麼多項目都採用分包方案,可是卻不會出現這個錯誤呢?
我在這裏總結了一個過程,想知道詳細分析過程的請看QQ空間開發團隊的原文。

在apk安裝的時候,虛擬機會將dex優化成odex後纔拿去執行。在這個過程當中會對全部class一個校驗。
校驗方式:假設A該類在它的static方法,private方法,構造函數,override方法中直接引用到B類。若是A類和B類在同一個dex中,那麼A類就會被打上CLASS_ISPREVERIFIED標記
被打上這個標記的類不能引用其餘dex中的類,不然就會報圖中的錯誤
在咱們的Demo中,MainActivity和Cat自己是在同一個dex中的,因此MainActivity被打上了CLASS_ISPREVERIFIED。而咱們修復bug的時候卻引用了另一個dex的Cat.class,因此這裏就報錯了
而普通分包方案則不會出現這個錯誤,由於引用和被引用的兩個類一開始就不在同一個dex中,因此校驗的時候並不會被打上CLASS_ISPREVERIFIED
補充一下第二條:A類若是還引用了一個C類,而C類在其餘dex中,那麼A類並不會被打上標記。換句話說,只要在static方法,構造方法,private方法,override方法中直接引用了其餘dex中的類,那麼這個類就不會被打上CLASS_ISPREVERIFIED標記。

5.1 解決方案

根據上面的第六條,咱們只要讓全部類都引用其餘dex中的某個類就能夠了。

下面是QQ控件給出的解決方案

 

 
19956127-33d70cfd35de67d2.png
 

 

在全部類的構造函數中插入這行代碼 System.out.println(AntilazyLoad.class);
這樣當安裝apk的時候,classes.dex內的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的標誌了,只要沒被打上這個標誌的類均可以進行打補丁操做。
hack.dex在應用啓動的時候就要先加載出來,否則AntilazyLoad類會被標記爲不存在,即便後面再加載hack.dex,AntilazyLoad類仍是會提示不存在。該類只要一次找不到,那麼就會永遠被標上找不到的標記了。
咱們通常在Application中執行dex的注入操做,因此在Application的構造中不能加上System.out.println(AntilazyLoad.class);這行代碼,由於此時hack.dex尚未加載進來,AntilazyLoad並不存在。
之因此選擇構造函數是由於他不增長方法數,一個類即便沒有顯式的構造函數,也會有一個隱式的默認構造函數。

5.2 插入代碼的難點

1.首先在源碼中手動插入不太可行,hack.dex此時並無加載進來,AntilazyLoad.class並不存在,編譯不經過。
2.因此咱們須要在源碼編譯成字節碼以後,在字節碼中進行插入操做。對字節碼進行操做的框架有不少,可是比較經常使用的則是ASM和javaassist
3.但AndroidStudio是使用Gradle構建項目,編譯-打包都是自動化的,咱們怎麼操做呢。

阿里P7移動互聯網架構師進階視頻(每日更新中)免費學習請點擊:https://space.bilibili.com/474380680
原文連接:https://blog.csdn.net/u010386612/article/details/51077291

相關文章
相關標籤/搜索