我的學(xué)習(xí)手冊 - 熱更新了解了一下下

目錄

我的學(xué)習(xí)手冊 - 熱更新了解了一下下
我的學(xué)習(xí)手冊 - Glide了解了一下下
我的學(xué)習(xí)手冊 - 進(jìn)程弊馄活了解了一下下
我的學(xué)習(xí)手冊 - EventBus了解了一下下
我的學(xué)習(xí)手冊 - ARouter了解了一下下

熱修復(fù)(Tinker)

先來個源碼 先來個源碼 先來個源碼

一义辕、這個是什么東西

正常開發(fā)流程

版本1.0上線
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布版本1.1 -> 用戶下載安裝
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布版本1.2 -> 用戶下載安裝
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布版本1.3 -> 用戶下載安裝
-> 應(yīng)用更新 -> 發(fā)布版本1.4

熱修復(fù)開發(fā)流程

版本1.0上線
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布補丁 -> 應(yīng)用自動修復(fù)
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布補丁 -> 應(yīng)用自動修復(fù)
-> 發(fā)現(xiàn)Bug -> 修復(fù)Bug -> 發(fā)布補丁 -> 應(yīng)用自動修復(fù)
-> 應(yīng)用更新 -> 發(fā)布版本1.1

二、原理

App中所有的類都是從dex中獲取的宰译,通過BaseDexClassLoader的findClass(String name)方法獲取,dex可以通過AndroidStudio分析Apk看到泡一,也可以解壓apk看到兰吟。

我們可以從下面兩段代碼中看到和一些命名中可以很容易的看出App找一個類的時候是從dex列表中一個一個的遍歷如果找到了就返回這個類,沒有找到會拋出ClassNotFoundExcepton仙逻。而Tinker熱修復(fù)的原理就是通過添加一個補丁Dex來讓找類的時候先通過查找這個補丁Dex中的類驰吓,如果找到類那么就直接返回找到的類而不會繼續(xù)向下尋找后面的出現(xiàn)Bug的Dex,這就是插樁系奉,再來一張我畫的圖吧就更容易理解了

然后還有一點檬贰,我個人認(rèn)為熱修復(fù)和Windows的補丁(開啟自動修復(fù))類似缺亮,首先你需要開機翁涤,然后請求微軟的某個接口,知道現(xiàn)在需要打補丁了萌踱,然后開始下載葵礼,下載完成后進(jìn)行更新。熱更新也是同樣的并鸵,進(jìn)入App之后請求后臺章咧,發(fā)現(xiàn)當(dāng)前版本有一個補丁,之后下載補丁到本地能真,熱更新工具發(fā)現(xiàn)本地補丁文件夾下面有一個補丁,那么就會在初始化的時候把這個補丁dex拿過來插入pathList中,來進(jìn)行修復(fù)bug的功能實現(xiàn)

public class BaseDexClassLoader extends ClassLoader {
    //這個就是存放所有的dex
    private final DexPathList pathList;

    ...

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    
    ...
    
}
final class DexPathList {

    ...

    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                    }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
           suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
       return null;
    }
    
    ...
    
}
    
插樁.jpg

三粉铐、代碼演示

這里我們先創(chuàng)建一個App疼约,將一些主要文件放入classes.dex,將bug文件放入classes2.dex蝙泼,然后在其中創(chuàng)建一個bug程剥,打包安裝,此時裝入手機的是Bug包汤踏,出現(xiàn)bug的文件類在classes2.dex中织鲸。
然后我們修復(fù)bug,buildApk溪胶,將apk解壓拿到classess2.dex搂擦,這個就類似補丁包,實際上的補丁包會更小哗脖,只是本地測試使用瀑踢,將這個補丁包復(fù)制到App的私有目錄中模擬從服務(wù)器中下載。
點擊修復(fù)按鈕才避,將文件復(fù)制橱夭,然后退出應(yīng)用(殺死進(jìn)程),重新打開App就可以看到修復(fù)后的效果

一桑逝、我們需要App有多個dex包棘劣,至少有一個主包,一個bug包楞遏,先進(jìn)行分包處理
App

class App:Application(){
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        MultiDex.install(this)
        //加載補丁包
        HotfixUtils.loadFixedDex(this)
    }
}
build.gradle中對主包進(jìn)行一些配置

multiDexEnabled true
multiDexKeepFile file('MultiDexKeep.txt')


    dexOptions{
        javaMaxHeapSize "4g"
        preDexLibraries = false
        additionalParameters = [
                '--multi-dex',
                '--set-max-idx-number=50000',
                '--main-dex-list='+'/MultiDexKeep.txt',
                '--minimal-main-dex'
        ]
    }
MultiDexKeep.txt 把一些不會出錯的文件keep住 放到主包里

com/memo/hotfix/App.class
com/memo/hotfix/BaseActivity.class
com/memo/hotfix/utils/ArrayUtils.class
com/memo/hotfix/utils/Constant.class
com/memo/hotfix/utils/FileUtils.class
com/memo/hotfix/utils/HotfixUtils.class
com/memo/hotfix/utils/LogUtils.class
com/memo/hotfix/utils/ReflectUtils.class
2茬暇、創(chuàng)建一個出現(xiàn)Bug的Activity,后續(xù)方便修改
 BugActivity

 override fun initialize() {
        ActivityCompat.requestPermissions(mActivity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)

        //模擬bug 點擊不做任何操作
        mBtnShow.setOnClickListener {
            //這里模擬有bug
            mTvResult.text = "這里有Bug"
            //mTvResult.text = "Bug修復(fù)了"
        }

        //點擊修復(fù)bug
        mBtnFix.setOnClickListener { fixBug() }
    }

    private fun fixBug() {
        //將補丁包patch.dex放到手機的根目錄下面 然后將文件從本地復(fù)制到私有目錄下面
        //實際上是從服務(wù)器下載到本地目錄下面
        val from: File = File(Environment.getExternalStorageDirectory(), Constant.PATCH_DEX)
        val to: File =
            File(getDir(Constant.DEX_DIR, Context.MODE_PRIVATE).absolutePath + File.separator + Constant.PATCH_DEX)
        if (to.exists()) {
            //如果之前的補丁包存在 就刪除
            val isDelete = to.delete()
            LogUtils.i("補丁包刪除$isDelete")
        }

        if (from.exists()) {
            //復(fù)制 模擬服務(wù)器下載
            FileUtils.copy(from, to)
            showToast("補丁加載成功")
            LogUtils.i("copy成功${to.exists()}")
            from.delete()
        } else {
            showToast("補丁包不存在")
        }
    }
3橱健、劃重點而钞,我們在App中進(jìn)行加載補丁包重點就是為了插樁,先獲取補丁patchDexElement拘荡,在獲取原有的oriDexElement臼节,然后合并生成心得dexElement(順序為補丁在前,原有在后)珊皿,然后賦值給App的pathList里的dexElement网缝,當(dāng)然需要使用反射
object HotfixUtils {

    /*** 補丁包集合 ***/
    private val patchDexSet: HashSet<File> by lazy { HashSet<File>() }

    fun loadFixedDex(mContext: Context) {
        //先清空
        patchDexSet.clear()
        //獲取補丁包目錄
        val patchDexDir: File = mContext.getDir(Constant.DEX_DIR, Context.MODE_PRIVATE)
        //遍歷這個補丁包下面的所有的文件
        val listFiles: Array<File> = patchDexDir.listFiles()
        for (file in listFiles) {
            if (file.name.endsWith(Constant.DEX_SUFFIX) && Constant.MAIN_DEX != file.name) {
                //找到文件夾下面的補丁包 放入自己的補丁包集合中
                patchDexSet.add(file)
            }
        }
        if (patchDexSet.size > 0) {
            //類加載器加載
            createDexClassLoader(mContext, patchDexDir)
        }
    }

    private fun createDexClassLoader(mContext: Context, patchDexDir: File) {
        //臨時dex解壓目錄 因為類加載器加載的是類而不是dex 所以需要將dex進(jìn)行解壓
        val optDirPath: String = patchDexDir.absolutePath + File.separator + Constant.DEX_OPT
        //創(chuàng)建
        val optDir = File(optDirPath)
        if (!optDir.exists()) {
            optDir.mkdirs()
        }
        for (dex in patchDexSet) {
            //自己創(chuàng)建一個補丁DexClassLoader
            val patchClassLoader = DexClassLoader(dex.absolutePath, optDirPath, null, mContext.classLoader)
            //每次獲取一個補丁文件,需要插樁一次

            //??????sāD酆!庇楞!最重要的環(huán)節(jié)!h九!??????
            hotFix(patchClassLoader, mContext)
        }

    }

    /**
     * ??????A蛱怠!!最重要的環(huán)節(jié)3酆蟆!矗愧!??????
     * ??????T钪ァ!唉韭!最重要的環(huán)節(jié)R固椤!属愤!??????
     * ??????E鳌!春塌!最重要的環(huán)節(jié)O堋!只壳!??????
     */
    private fun hotFix(patchClassLoader: DexClassLoader, mContext: Context) {
        //這里分為6步
        //1.獲取原有的PathClassLoader
        val pathClassLoader: PathClassLoader = mContext.classLoader as PathClassLoader

        try {

            //2.獲取補丁包列表 dexElement
            val patchDexElement = ReflectUtils.getDexElement(ReflectUtils.getPathList(patchClassLoader))

            //3.獲取原有的pathList
            val oriPathList = ReflectUtils.getPathList(pathClassLoader)

            //4.獲取原有包列表 dexElement
            val oriDexElement = ReflectUtils.getDexElement(oriPathList)

            //5.合并成為一個新的 補丁包在前 原有包在后的dexElement
            val finalDexElement = ArrayUtils.combineArray(patchDexElement, oriDexElement)

            //6.用合成后的dexElement重新賦值原有的pathList里面的dexElement屬性
            ReflectUtils.setDexElement(oriPathList, oriPathList.javaClass, finalDexElement)

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}
4俏拱、實際效果
show.gif
5、結(jié)合實際使用一下Tinker吼句,這里使用的Bugly

基本按照官網(wǎng)的示例文檔來就好來

1.創(chuàng)建TinkerApplicationLike锅必,進(jìn)行一些詳細(xì)的配置
class TinkerApplicationLike(
    application: Application, tinkerFlags: Int,
    tinkerLoadVerifyFlag: Boolean, applicationStartElapsedTime: Long,
    applicationStartMillisTime: Long, tinkerResultIntent: Intent
) : DefaultApplicationLike(
    application,
    tinkerFlags,
    tinkerLoadVerifyFlag,
    applicationStartElapsedTime,
    applicationStartMillisTime,
    tinkerResultIntent
) {


    override fun onCreate() {
        super.onCreate()
        // 這里實現(xiàn)SDK初始化,appId替換成你的在Bugly平臺申請的appId
        // 調(diào)試時惕艳,將第三個參數(shù)改為true
        Bugly.init(application, "(Bugly 申請的 appId)", true)
    }


    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    override fun onBaseContextAttached(base: Context) {
        super.onBaseContextAttached(base)
        // you must install multiDex whatever tinker is installed!
        MultiDex.install(base)

        // 安裝tinker
        Beta.installTinker(this)
        Beta.checkUpgrade()
        //自動下載
        Beta.canAutoDownloadPatch = true
        Beta.canAutoPatch = true
        //提示用戶需要重啟 內(nèi)部進(jìn)行來殺死進(jìn)程操作
        Beta.canNotifyUserRestart = true
        //補丁的監(jiān)聽
        Beta.betaPatchListener = object : BetaPatchListener {
            override fun onPatchReceived(patchFile: String) {
                Toast.makeText(application, "補丁下載地址$patchFile", Toast.LENGTH_SHORT).show()
            }

            override fun onDownloadReceived(savedLength: Long, totalLength: Long) {
                Toast.makeText(
                    application,
                    String.format(
                        Locale.getDefault(), "%s %d%%",
                        Beta.strNotificationDownloading,
                        (if (totalLength == 0L) 0 else savedLength * 100 / totalLength).toInt()
                    ),
                    Toast.LENGTH_SHORT
                ).show()
            }

            override fun onDownloadSuccess(msg: String) {
                Toast.makeText(application, "補丁下載成功", Toast.LENGTH_SHORT).show()
            }

            override fun onDownloadFailure(msg: String) {
                Toast.makeText(application, "補丁下載失敗", Toast.LENGTH_SHORT).show()

            }

            override fun onApplySuccess(msg: String) {
                Toast.makeText(application, "補丁應(yīng)用成功", Toast.LENGTH_SHORT).show()
            }

            override fun onApplyFailure(msg: String) {
                Toast.makeText(application, "補丁應(yīng)用失敗", Toast.LENGTH_SHORT).show()
            }

            override fun onPatchRollback() {
                Toast.makeText(application, "補丁回滾", Toast.LENGTH_SHORT).show()
            }
        }
    }

}

2.對于Tinker Gradle的配置

每次發(fā)線上包的時候需要把包作為基準(zhǔn)包進(jìn)行留存搞隐,這個很重要,如果沒有基準(zhǔn)包就沒有辦法打補丁包了远搪,切記切記
每次打補丁包的時候要把基準(zhǔn)包加進(jìn)來注意文件夾和文件名稱要和gradle中的配置一樣 仔細(xì)檢查 例如我的

 def baseApkDir = "base"  
 baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"

那么就是這樣子的


base.png
 // 主要修改的地方注意下面的名稱
 // 構(gòu)建基準(zhǔn)包和補丁包都要指定不同的tinkerId劣纲,并且必須保證唯一性
    //base-(版本號)
    tinkerId = "base-1.0.4"
    //patch-(版本號)-(補丁版本號)
    //tinkerId = "patch-1.0.4-1"

    // 編譯補丁包時,必需指定基線版本的apk谁鳍,默認(rèn)值為空
    // 如果為空癞季,則表示不是進(jìn)行補丁包的編譯
    // @{link tinkerPatch.oldApk }
    baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"

    // 對應(yīng)tinker插件applyResourceMapping
    baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-debug-R.txt"
apply plugin: 'com.tencent.bugly.tinker-support'

def bakPath = file("${buildDir}/bakApk/")

/**
 * 此處填寫每次構(gòu)建生成的基準(zhǔn)包目錄
 */
def baseApkDir = "base"

/**
 * 對于插件各參數(shù)的詳細(xì)解析請參考
 */
tinkerSupport {

    // 構(gòu)建基準(zhǔn)包和補丁包都要指定不同的tinkerId,并且必須保證唯一性
    //base-(版本號)
    tinkerId = "base-1.0.4"
    //patch-(版本號)-(補丁版本號)
    //tinkerId = "patch-1.0.4-1"

    // 編譯補丁包時倘潜,必需指定基線版本的apk绷柒,默認(rèn)值為空
    // 如果為空,則表示不是進(jìn)行補丁包的編譯
    // @{link tinkerPatch.oldApk }
    baseApk = "${bakPath}/${baseApkDir}/app-debug.apk"

    // 對應(yīng)tinker插件applyResourceMapping
    baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-debug-R.txt"

    // 對應(yīng)tinker插件applyMapping
    baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-debug-mapping.txt"


    // 開啟tinker-support插件涮因,默認(rèn)值true
    enable = true

    // 指定歸檔目錄废睦,默認(rèn)值當(dāng)前module的子目錄tinker
    autoBackupApkDir = "${bakPath}"

    // 是否啟用覆蓋tinkerPatch配置功能,默認(rèn)值false
    // 開啟后tinkerPatch配置不生效养泡,即無需添加tinkerPatch
    overrideTinkerPatchConfiguration = true

    // 構(gòu)建多渠道補丁時使用
    // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"

    // 是否啟用加固模式嗜湃,默認(rèn)為false.(tinker-spport 1.0.7起支持)
    isProtectedApp = true

    // 是否開啟反射Application模式
    enableProxyApplication = false

    // 是否支持新增非export的Activity(注意:設(shè)置為true才能修改AndroidManifest文件)
    supportHotplugComponent = true

}

/**
 * 一般來說,我們無需對下面的參數(shù)做任何的修改
 * 對于各參數(shù)的詳細(xì)介紹請參考:
 * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
 */
tinkerPatch {
    ignoreWarning = false
    useSign = true
    dex {
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
        pattern = ["lib/*/*.so"]
    }

    res {
        pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
        ignoreChange = []
        largeModSize = 100
    }

    packageConfig {
    }
    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
    }
    buildConfig {
        keepDexApply = false
    }
}

3.使用gradle創(chuàng)建補丁包上傳服務(wù)器即可奈应,如果成功的話,會彈出配置中的toast购披,然后殺死進(jìn)程后重新啟動就修復(fù)bug了
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钥组,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子今瀑,更是在濱河造成了極大的恐慌,老刑警劉巖点把,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件橘荠,死亡現(xiàn)場離奇詭異,居然都是意外死亡郎逃,警方通過查閱死者的電腦和手機哥童,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褒翰,“玉大人贮懈,你說我怎么就攤上這事∮叛担” “怎么了朵你?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長揣非。 經(jīng)常有香客問我抡医,道長,這世上最難降的妖魔是什么早敬? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任忌傻,我火速辦了婚禮,結(jié)果婚禮上搞监,老公的妹妹穿的比我還像新娘水孩。我一直安慰自己,他們只是感情好琐驴,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布俘种。 她就那樣靜靜地躺著,像睡著了一般棍矛。 火紅的嫁衣襯著肌膚如雪安疗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天够委,我揣著相機與錄音荐类,去河邊找鬼。 笑死茁帽,一個胖子當(dāng)著我的面吹牛玉罐,可吹牛的內(nèi)容都是我干的屈嗤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼吊输,長吁一口氣:“原來是場噩夢啊……” “哼饶号!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起季蚂,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤茫船,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扭屁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體算谈,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年料滥,在試婚紗的時候發(fā)現(xiàn)自己被綠了然眼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡葵腹,死狀恐怖高每,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情践宴,我是刑警寧澤鲸匿,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站浴井,受9級特大地震影響晒骇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜磺浙,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一洪囤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撕氧,春花似錦瘤缩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至不脯,卻和暖如春府怯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背防楷。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工牺丙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓冲簿,卻偏偏與公主長得像粟判,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子峦剔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355