做程序開(kāi)發(fā),基礎(chǔ)很重要。同樣是擰螺絲人家擰出來(lái)的可以經(jīng)久不壞,你擰出來(lái)的遇到點(diǎn)風(fēng)浪就開(kāi)始顫抖,可見(jiàn)基本功的重要性。再?gòu)?fù)雜的技術(shù),也是由一個(gè)一個(gè)簡(jiǎn)單的邏輯構(gòu)成。先了解核心基礎(chǔ),才能更好理解前沿高新技術(shù)。
網(wǎng)站設(shè)計(jì)制作過(guò)程拒絕使用模板建站;使用PHP+MYSQL原生開(kāi)發(fā)可交付網(wǎng)站源代碼;符合網(wǎng)站優(yōu)化排名的后臺(tái)管理系統(tǒng);網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計(jì)收費(fèi)合理;免費(fèi)進(jìn)行網(wǎng)站備案等企業(yè)網(wǎng)站建設(shè)一條龍服務(wù).我們是一家持續(xù)穩(wěn)定運(yùn)營(yíng)了10年的創(chuàng)新互聯(lián)建站網(wǎng)站建設(shè)公司。
- 先看效果{github Demo地址}:(https://github.com/18598925736/HotUpdateDemo)
- Demo使用方法
- Demo源碼概覽
- 熱修復(fù)核心技術(shù)
- 基礎(chǔ)知識(shí)預(yù)備
- hook思路
- TIPS
熱更新技術(shù),不是新話題。目前最熱門的熱更新由兩種,一種是騰訊tinker為代表的 需重啟app的熱更新,一種是美團(tuán)app為代表的instant Run,無(wú)需重啟app. 今天先探究 前者的核心原理。
先看效果[github Demo地址] :(https://github.com/18598925736/HotUpdateDemo)
假如說(shuō)這是我們的app界面,這個(gè)界面有個(gè)bug,我們直接用一個(gè) TextView
來(lái)表示
然而,我們的開(kāi)發(fā)人員發(fā)現(xiàn)了這個(gè)bug,但是產(chǎn)品已經(jīng)上線。這時(shí)候,由于引起bug的 代碼,只有一行,
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceStata) {
super.onCreate(savedINstanceState);
srtContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.tv);
Bug bug = new Bug():
String s = bug.getstr():
textView.setText(s):
}
}
這個(gè)時(shí)候,機(jī)智的程序員用最快的方式修復(fù)了這個(gè)bug,也只是改了一行代碼:
那么,產(chǎn)品已經(jīng)在線上,怎么辦?我們通過(guò)后臺(tái),向app推送了一個(gè) fix.dex
文件, 等這個(gè)文件下載完成,app提示用戶,發(fā)現(xiàn)新的更新,需要重啟app. 待用戶重啟,代碼修復(fù) 即會(huì)生效。無(wú)需發(fā)布新版本!
Demo使用方法
下載Demo代碼之后,會(huì)在assets下看到一個(gè)fix.dex
文件
按照正常的邏輯,我們做bug修復(fù)一定是把fix.dex
放到服務(wù)器上, app去服務(wù)器下載它,然后存放在app私有目錄,重啟app之后,fix.dex
生效, 當(dāng)加載到這個(gè)類的時(shí)候,就會(huì)去讀fix.dex
中當(dāng)時(shí)打包的已修復(fù)bug的類. 但是,我這里為了演示方便,直接放在assets,然后使用 項(xiàng)目中的 AssetsFileUtil
類 用io流將它讀寫到 app私有目錄下.
演示方法:
起作用的是誰(shuí)?就是這個(gè)fix.dex
文件.
如上圖所示: 核心類其實(shí)就只有一個(gè): ClassLoaderHookHelper
,它 就是 讓 fix.dex
這個(gè)補(bǔ)丁發(fā)揮作用的 " 幕后大佬". 這個(gè)核心類:有3個(gè)方法,分別是在不同的系統(tǒng)版本上,來(lái)對(duì)源碼程序邏輯進(jìn)行 hook,提高h(yuǎn)ook的兼容性.
下面是完整 ClassLoaderHookHelper
代碼 以及 使用它的 MyApp
完整代碼 :
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class ClassLoaderHookHelper {
//23和19的差別,就是 makeXXXElements 方法名和參數(shù)要求不同
//后者是 makeDexElements(ArrayList files, File optimizedDirectory,ArrayList suppressedExceptions)
//前者是 makePathElements(List files, File optimizedDirectory,List suppressedExceptions)
public static void hookV23(ClassLoader classLoader,File outDexFilePath,File optimizedDirectory)throws IllegalAccessException, InvocationTargetException {
Field pathList =ReflectionUtil.getField(classLoader,"pathList");//1、獲DexPathList pathList 屬性
object dexpathListobj =pathList.get(classLoader);//2、獲DexPathList pathList對(duì)象
Field dexElementsField =ReflectionUtil.getField(dexPathListObj, "dexElements");//3、獲得DexPathList的dexElements屬性
Object[] oldElements =(Object[]) dexElementsField.get(dexPathListObj);//4、獲得pathList對(duì)象中 dexElements 的屬性值
...
}
}
其實(shí) 熱修復(fù)的核心技術(shù),就一句話,
HookClassLoader
,但是要深入了解它,需要相當(dāng)多的基礎(chǔ)知識(shí),下面列舉出必須要知道的一些東西。基礎(chǔ)知識(shí)預(yù)備
1.Dex文件是什么?
我們寫安卓,目前還是用 java比較多,就算是用 kotlin,它最終也是要轉(zhuǎn)換成 java來(lái)運(yùn)行。 java文件,被編譯成 class之后,多個(gè) class文件,會(huì)被打包成
classes.dex
,被放到apk
中,安卓設(shè)備拿到apk
,去安裝解析( 預(yù)編譯balabala...),當(dāng)我們運(yùn)行 app時(shí), app的程序邏輯全都是在classes.dex
中。所以,dex
文件是什么?一句話,dex
文件是 android app的源代碼的最終打包
androidStudio
打包 apk
的時(shí)候會(huì)生成 Dex
,其實(shí)它使用的是 SDK
的 dx命令,我們可以用 dx命令自己去打包想要打包的 class. 命令格式為:dx --dex --output=output.dex xxxx.class
將上面的output 和 xxxx換成你想要的文件名即可。
注:dx.bat在 安卓 SDK的目錄下:比如我d的`C:\XXXXX\AndroidStudioAbout\sdk1\build-tools\28.0.3\dx.bat
ClassLoader
是什么?ClassLoader
來(lái)自 jdk
,翻譯為 :類加載器,用于將 class文件中的類,加載到內(nèi)存中,生成 class對(duì)象。只有存在了 Class對(duì)象,我們才可以創(chuàng)建我們想要的對(duì)象。 android SDK
繼承了JDK
的 classLoader
,創(chuàng)造出了新的 ClassLoader
子類。下圖表示了 android9.0-28 所有的ClassLoader
直接或者間接子類.
比較多的是 BaseDexClassLoader
, DexClassLoader
, PathClassLoader
, 其他這些,應(yīng)該是谷歌大佬 創(chuàng)造出來(lái)新的 類加載器子類吧,還沒(méi)研究過(guò)。
注: 關(guān)于 DexClassLoader
和 PathClassLoader
,網(wǎng)上資料有個(gè)誤區(qū),應(yīng)該不少人都認(rèn)為, PathClassLoader
用于加載 app內(nèi)部的 dex
文件, DexClassLoader
用于加載外部的 dex
文件,但是其實(shí)只要看一眼 這兩個(gè)類的關(guān)系,就會(huì)發(fā)現(xiàn),它們都是繼承自 BaseDexClassLoader
,他們的構(gòu)造函數(shù)內(nèi)部都會(huì)去執(zhí)行父類的構(gòu)造函數(shù)。他們只有一個(gè)差別,那就是 PathClssLoader
不用傳 optimizedDirectory
這個(gè)參數(shù),但是 DexClassLoader
必須傳。這個(gè)參數(shù)的作用是,傳入一個(gè) dex
優(yōu)化之后的存放目錄。而事實(shí)上,雖然 PathClassLoader
不要求傳這個(gè) optimizedDirectory
,但是它實(shí)際上是給了一個(gè)默認(rèn)值。emmmm............所以不要再認(rèn)為 PathClassLoader
不能加載外部的 dex
了,它只是沒(méi)有讓你傳 optimizedDirectory
而已。
另外:BootClassLoader
用于加載 AndroidFramework
層class文件( SDK中沒(méi)有這個(gè)BootClassLoader
,也是很奇怪) PathClassLoader
是用于Android應(yīng)用程序類的加載器,可以加載指定的 dex,以及 jar、 zip、 apk中的 classes.dex
。 DexClassLoader
可以加載指定的 dex
,以及 jar、 zip、 apk中的 classes.dex
。
ClassLoader
的雙親委托機(jī)制是什么?android里面 ClassLoader
的作用,是將 dex
文件中的類,加載到內(nèi)存中,生成 Class對(duì)象,供我們使用 (舉個(gè)例子:我寫了一個(gè) A類,app運(yùn)行起來(lái)之后,當(dāng)我需要new 一個(gè) A, ClassLoader
首先會(huì)幫我查找 A的 Class對(duì)象是否存在,如果存在,就直接給我 Class對(duì)象,讓我拿去 new A,如果不存在,就會(huì)出創(chuàng)建這個(gè) A的 Class對(duì)象。) 這個(gè)查找的過(guò)程,就遵循 雙親委托機(jī)制。一句話解釋 雙親委托機(jī)制:某個(gè) 類加載器在加載某個(gè) 類的時(shí)候,首先會(huì)將 這件事委托給 parent類加載器,依次遞歸,如果 parent類加載器可以完成加載,就會(huì)直接返回 Class對(duì)象。如果 parent找不到或者沒(méi)有父了,就會(huì) 自己加載。
下圖是 安卓源碼 ClassLoader.java
:
紅字注解,很容易讀懂 ClassLoader
去 load一個(gè) class的過(guò)程.
OK,現(xiàn)在可以來(lái)解讀我是如何去hook ClassLoader
的了. 解讀之前,先弄清楚,我為何 要 hookClassLoader
,為什么 hook了它之后,我的 fix.dex
就能發(fā)揮作用?先解決這個(gè)疑問(wèn),既然是 hook,那么自然要讀懂源碼,因?yàn)?hook就是在理解源碼思維的前提下,更改源碼邏輯。 一張圖解決你的疑問(wèn):
按照上面圖,去追蹤源碼,會(huì)發(fā)現(xiàn), ClassLoader
最終會(huì)從 DexFile
對(duì)象中去獲得一個(gè) Class對(duì)象。并且在 DexPathList
類中 findClass
的時(shí)候,存在一個(gè) Element數(shù)組的遍歷。這就意味著,如果存在多個(gè) dex
文件,多個(gè) dex
文件中都存在同樣一個(gè) class,那么它會(huì)從第一個(gè)開(kāi)始找,如果找到了,就會(huì)立即返回。如果沒(méi)找到,就往下一個(gè)dex
去找。
也就是說(shuō),如果我們可以在 這個(gè)數(shù)組中插入我們自己的修復(fù)bug的 fix.dex
,那我們就可以讓我們 已經(jīng)修復(fù)bug的補(bǔ)丁類發(fā)揮作用,讓類加載器優(yōu)先讀取我們的 補(bǔ)丁類.
OK,理解了源碼的邏輯,那我們可以動(dòng)手了。來(lái)解析SDK 23的 hookClassLoader
過(guò)程吧!
確定思路,我們要改變app啟動(dòng)之后,自帶的ClassLoader對(duì)象(具體實(shí)現(xiàn)類是PathClassLoader )中 DexPathList 中 Element[] element 的實(shí)際值。
那么,步驟:
1.取得
PathClassLoader
的pathList
的屬性
2.取得PathClassLoader
的pathList
的屬性真實(shí)值(得到一個(gè)DexPathList
對(duì)象)
3.獲得DexPathList
中的dexElements
屬性
4.獲得DexPathList
對(duì)象中dexElements
屬性的真實(shí)值(它是一個(gè)Element數(shù)組) 做完這4個(gè)步驟,我們得到下面的代碼
5.用外部傳入的Dex
文件路徑,構(gòu)建一個(gè)我們自己的Element數(shù)組
6.將從外部傳入的ClassLoader
中得到的Element數(shù)組和 我們自己的Element數(shù)組合并起來(lái), 注意,我們自己的數(shù)組元素要放前面!
7.將剛才合并的新Element數(shù)組,設(shè)置到 外部傳入的ClassLoader里面去。
OK,收官!
上面的內(nèi)容,讀起來(lái)可能會(huì)有一些疑問(wèn),我預(yù)估到了一些,將答案寫在下面
1. 當(dāng)我們需要反射獲得一個(gè)類的某個(gè)方法或者成員變量時(shí),我們只想拿
getDeclareXX
,因?yàn)槲覀冎幌肽帽绢愔械某蓡T,但是僅僅getDeclareXX
不能跨越繼承關(guān)系 拿到 父類中的非私有成員,所以我寫了ReflectionUtil.java
,支持跨越繼承關(guān)系 拿到父類的非私有成員。
2. 這種熱修復(fù),是不是下載的包會(huì)很大,和原先的apk
差不多大?答案是,NO,我們只需要將我們修復(fù)bug之后的補(bǔ)丁dex
下載到設(shè)備,讓app重啟,去讀取這個(gè)dex
即可。補(bǔ)丁包很小,甚至只有1K.
3. 這種修復(fù)方式必須重啟么? 是的,必須重啟,當(dāng)然,存在不需要重啟就可以修復(fù)bug的方法,那種方法叫做instant run方案,本文不涉及。而,當(dāng)前這種方案叫做:MultipleDex
即,多dex
方案。
*4.** 為什么要對(duì)SDK
23 ,19,14 寫不同的hook代碼?因?yàn)?code>SDK版本的變遷,導(dǎo)致 一些類的關(guān)系,變量名,方法名,方法參數(shù)(個(gè)數(shù)和類型)都會(huì)發(fā)生變化,所以,要針對(duì)各個(gè)變遷的版本進(jìn)行兼容。
另外有需要云服務(wù)器可以了解下創(chuàng)新互聯(lián)cdcxhl.cn,海內(nèi)外云服務(wù)器15元起步,三天無(wú)理由+7*72小時(shí)售后在線,公司持有idc許可證,提供“云服務(wù)器、裸金屬服務(wù)器、高防服務(wù)器、香港服務(wù)器、美國(guó)服務(wù)器、虛擬主機(jī)、免備案服務(wù)器”等云主機(jī)租用服務(wù)以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡(jiǎn)單易用、服務(wù)可用性高、性價(jià)比高”等特點(diǎn)與優(yōu)勢(shì),專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應(yīng)用場(chǎng)景需求。