前言
最近DoKit V3.3.1版本已經(jīng)發(fā)布了,新版本增加了很多重磅的功能,同時也在庫的名字上對Androidx和Android support進行了區(qū)分价认。
具體的更新信息參考:DoKit Android版本信息
感興趣的小伙伴們趕快通過Android參考文檔去升級體驗吧儒喊。
技術(shù)背景
業(yè)務(wù)代碼零侵入一直是DoKit秉持的底線喊递。
DoKit作為一款終端一站式研發(fā)解決方案。我們在不斷的給社區(qū)用戶提供各種各樣優(yōu)秀工具來幫助用戶提升研發(fā)效率使鹅,于此同時我們也要盡可能保證用戶的線上代碼交付質(zhì)量篇亭。慶幸的是绝淡,從DoKit推出到現(xiàn)在我們累計收獲了10000+的用戶宙刘,至今還沒有收到過一起用戶反饋的由于集成DoKit而引發(fā)的線上bug。那我們是如何做到在業(yè)務(wù)代碼零侵入的情況下給用戶提供各種強大的工具的呢牢酵?其實這背后離不開AOP的功勞悬包。
DoKit AOP原理
(以下圖片來自于我在滴滴集團內(nèi)部的DoKit專題分享)
AOP方案選型
在社區(qū)中針對Android的主流的AOP實現(xiàn)方案主要有以下兩個:AspectJ和AS插件+ASM。其實DoKit在早期的版本中用的就是AspectJ的方案馍乙,但是隨著DoKit的社區(qū)越來越健壯布近、社區(qū)用戶也越來越多,漸漸的就開始有很多人反饋AspectJ會和他們項目中的AspectJ由于版本不一致造成沖突丝格,從而導致編譯失敗撑瞧。DoKit團隊一直很重視社區(qū)用戶的使用體驗,所以針對這一問題显蝌,我們經(jīng)過了大量的調(diào)研和社區(qū)驗證预伺,最終決定將整個AOP技術(shù)方案替換為AS Plugin+ASM。
在經(jīng)過幾個版本的驗證以后琅束,我們發(fā)現(xiàn)ASM在項目集成過程中的沖突相比AspectJ明顯減少,這也堅定了我們后續(xù)大力優(yōu)化該套方案的信心算谈。ASM是比較偏底層的方案涩禀,它是直接作用在JVM字節(jié)碼上的。所以我們在使用ASM方案的時候需要克服以下兩個難點:
1.你要對JVM的字節(jié)碼有一定的了解(感興趣的小伙伴可以通過https://asm.ow2.io了解更多信息)然眼。
2.為了尋找最優(yōu)的Hook點艾船,我們需要了解主流第三方的庫原理。
AOP原理
在確定好技術(shù)選型以后我們來看下ASM的相關(guān)原理高每。其實通過上圖我們已經(jīng)能夠大概了解其大致的原理屿岂。AS Gradle的編譯會將我們的java class文件、jar包以及resource資源文件打包最為最原始的數(shù)據(jù)輸出給第一個Transform鲸匿,第一個transform處理完的產(chǎn)物再輸出給第二個transform爷怀,以此類推形成完整的鏈路。而ASM就是作用于圖中的第一個紅色TransformA带欢。它會拿到一開始的原始數(shù)據(jù)以后會進行一定的分析运授。并且按照JVM字節(jié)碼的格式針對類、變量乔煞、方法等類型調(diào)用相關(guān)的回調(diào)方法吁朦。在相應(yīng)的回調(diào)方法中我們可以對相關(guān)的字節(jié)碼指令進行操作。比如新增渡贾、刪除等等逗宜。中間的圖片就是它具體的運行時序圖。最后兩者結(jié)合編譯就會產(chǎn)生新的JVM class 文件。
AOP落地場景
站在巨人的肩膀上能夠幫助我們更快更好的實現(xiàn)相關(guān)功能纺讲。秉持著不重復造輪子的理念擂仍,我們在進行廣泛的技術(shù)選型以后,決定使用滴滴的Booster作為DoKit插件的底層實現(xiàn)刻诊。Booster為我們屏蔽了各個Gradle版本之間的API差異防楷,功能非常強大,強烈建議感興趣的的小伙伴們了解一下则涯。
為了更加便于理解复局,我這里舉一個具體的例子。從圖中的例子我們能夠發(fā)現(xiàn)粟判,經(jīng)過DoKit AOP插件編譯以后就相當于我們替用戶主動寫了一部分代碼亿昏。通過這種代理的編程模式,我們就能發(fā)在運行時拿到用戶的對象档礁,并達到修改對象屬性的目的角钩。
如圖所示,到目前為止AOP在DoKit中的大部分功能中都得到了落地呻澜。
DoKit AOP場景落地
下面我們來具體看一下在這些落地場景中递礼,DoKit是如何用比較優(yōu)雅的方式來進行字節(jié)碼操作的。
(DoKit所有的字節(jié)碼操作只針對Debug包生效羹幸,所以不用擔心會污染線上代碼)
(由于篇幅的原因脊髓,我只選取了社區(qū)中比較關(guān)心的幾個功能進行一下分析,其實字節(jié)碼操作的原理都差不多栅受,我們需要的是創(chuàng)意以及大量的三方源碼閱讀将硝,這樣才能找到最優(yōu)雅的插樁點)
大圖檢測
大圖檢測其實社區(qū)中已經(jīng)有一篇分析得很詳細的文章了,我這里就不具體分析了屏镊,大家參考一下:通過ASM實現(xiàn)大圖監(jiān)控
函數(shù)耗時
函數(shù)耗時可以參考我以前寫過的一篇文章:滴滴DoKit Android核心原理揭秘之函數(shù)耗時
功能開關(guān)配置
DoKit中針對每一項插件功能在編譯期都設(shè)置了一個開關(guān)功能依疼,防止某些字節(jié)碼操作在特定場景下會造成編譯失敗以及運行時bug,同時也是為了更友好的提醒用戶該項功能的狀態(tài)而芥,我們會在運行時判斷用戶在編譯期的開關(guān)狀態(tài)律罢。那么問題來了,DoKit是如何拿到gradle.properties或者build.gradle里的配置信息的呢棍丐,其實這背后也是字節(jié)碼的功勞弟翘。下面我們來具體看一下它的實現(xiàn)邏輯。
DoraemonKitReal內(nèi)置了一個空的pluginConfig方法骄酗,用來做字節(jié)碼插裝稀余。然后定義了一個DokitPluginConfig類用來存儲和讀取相關(guān)配置信息。
public class DokitPluginConfig {
/**
* 注入插件配置 動態(tài)注入到DoraemonKitReal#pluginConfig方法中
*/
public static void inject(Map config) {
//LogHelper.i(TAG, "map====>" + config);
SWITCH_DOKIT_PLUGIN = (boolean) config.get("dokitPluginSwitch");
SWITCH_METHOD = (boolean) config.get("methodSwitch");
SWITCH_BIG_IMG = (boolean) config.get("bigImgSwitch");
SWITCH_NETWORK = (boolean) config.get("networkSwitch");
SWITCH_GPS = (boolean) config.get("gpsSwitch");
VALUE_METHOD_STRATEGY = (int) config.get("methodStrategy");
}
}
那么我們只要編譯期動態(tài)的往pluginConfig的方法中插入DokitPluginConfig.inject(map)就可以了趋翻,這個map里存儲的就是我們前面編譯期配置信息睛琳。
下面我們來看一下字節(jié)碼操作的相關(guān)代碼CommTransformer:
if (className == "com.didichuxing.doraemonkit.DoraemonKitReal") {
//插件配置
klass.methods?.find {
it.name == "pluginConfig"
}.let { methodNode ->
"${context.projectDir.lastPath()}->insert map to the DoraemonKitReal pluginConfig succeed".println()
methodNode?.instructions?.insert(createPluginConfigInsnList())
}
}
/**
* 創(chuàng)建pluginConfig代碼指令
*/
private fun createPluginConfigInsnList(): InsnList {
//val insnList = InsnList()
return with(InsnList()) {
//new HashMap
add(TypeInsnNode(NEW, "java/util/HashMap"))
add(InsnNode(DUP))
add(MethodInsnNode(INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false))
//保存變量
add(VarInsnNode(ASTORE, 0))
//獲取第一個變量
add(VarInsnNode(ALOAD, 0))
add(LdcInsnNode("dokitPluginSwitch"))
add(InsnNode(if (DoKitExtUtil.dokitPluginSwitchOpen()) ICONST_1 else ICONST_0))
add(
MethodInsnNode(
INVOKESTATIC,
"java/lang/Boolean",
"valueOf",
"(Z)Ljava/lang/Boolean;",
false
)
)
add(
MethodInsnNode(
INVOKEINTERFACE,
"java/util/Map",
"put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
true
)
)
add(InsnNode(POP))
.........
//將HashMap注入到DokitPluginConfig中
add(VarInsnNode(ALOAD, 0))
add(
MethodInsnNode(
INVOKESTATIC,
"com/didichuxing/doraemonkit/aop/DokitPluginConfig",
"inject",
"(Ljava/util/Map;)V",
false
)
)
this
}
//return insnList
}
由于字節(jié)碼指令有點長,我這邊只選取一部分的代碼。首先我們通過全限定名在編譯的過程中找到class中找到需要操作的方法师骗。然后在通過ASM API動態(tài)的去插入相關(guān)代碼历等。通過以上的操作最后生成的代碼如下:
private final void pluginConfig() {
HashMap hashMap = new HashMap();
hashMap.put("dokitPluginSwitch", true);
hashMap.put("gpsSwitch", true);
hashMap.put("networkSwitch", true);
hashMap.put("bigImgSwitch", true);
hashMap.put("methodSwitch", true);
hashMap.put("methodStrategy", 0);
DokitPluginConfig.inject(hashMap);
}
大家感興趣的話可以通過我們的github上的demo,看下編譯前后的pluginConfig方法里的差別辟癌。
位置模擬
滴滴作為一家出行行業(yè)的獨角獸企業(yè)寒屯,我們DoKit需要協(xié)助開發(fā)和測試模擬各種位置信息。所以這也是我們在集團內(nèi)部被廣泛使用的一款工具黍少。下面我們來看一下具體的實現(xiàn)寡夹。
目前市面上主要有高德、騰訊厂置、百度再加上Android自帶的幾款地圖SDK菩掏。目前DoKit已經(jīng)全部兼容。
系統(tǒng)自帶
其中系統(tǒng)自帶的經(jīng)緯度我們是通過hook LocationService的方式來實現(xiàn)的昵济,具體的代碼參考:LocationHooker智绸。由于這一塊不涉及到字節(jié)碼操作,我就不具體分析了
三方地圖
由于我們不知道用戶的項目中具體集成的是哪個地圖SDK访忿,所以我們通過compileOnly的方式引入(ext文件參考如下:config.gradle):
//高德地圖定位
compileOnly rootProject.ext.dependencies["amap_location"]
//騰訊地圖定位
compileOnly rootProject.ext.dependencies["tencent_location"]
//百度地圖定位
compileOnly files('libs/BaiduLBS_Android.jar')
這樣能夠避免引入用戶不需要的地圖SDK瞧栗,減少編譯沖突。
由于百度海铆、騰訊迹恐、高德地圖的SDK調(diào)用API都是差不多的,下面我就以高德為例進行分析游添。
首先我們通過demo來看一下高德是如何返回經(jīng)緯度的:
private var mapLocationListener = AMapLocationListener { aMapLocation ->
val errorCode = aMapLocation.errorCode
val errorInfo = aMapLocation.errorInfo
Log.i(
TAG,
"高德定位===lat==>" + aMapLocation.latitude + " lng==>" + aMapLocation.longitude + " errorCode===>" + errorCode + " errorInfo===>" + errorInfo
)
}
mLocationClient!!.setLocationListener(mapLocationListener)
如果我們能夠把代碼變成如下的方式其實就可以拿到用戶的AMapLocationListener對象
//這是AMapLocationClient編譯后的反編譯代碼
public void setLocationListener(AMapLocationListener aMapLocationListener) {
AMapLocationListenerProxy aMapLocationListenerProxy = new AMapLocationListenerProxy(aMapLocationListener);
try {
if (this.f110b != null) {
this.f110b.mo19841a((AMapLocationListener) aMapLocationListenerProxy);
}
} catch (Throwable th) {
CoreUtil.m1617a(th, "AMClt", "sLocL");
}
}
DoKit內(nèi)置AMapLocationListener代理對象
public class AMapLocationListenerProxy implements AMapLocationListener {
AMapLocationListener aMapLocationListener;
public AMapLocationListenerProxy(AMapLocationListener aMapLocationListener) {
this.aMapLocationListener = aMapLocationListener;
}
@Override
public void onLocationChanged(AMapLocation mapLocation) {
if (GpsMockManager.getInstance().isMocking()) {
try {
mapLocation.setLatitude(GpsMockManager.getInstance().getLatitude());
mapLocation.setLongitude(GpsMockManager.getInstance().getLongitude());
//通過反射強制改變p的值 原因:看mapLocation.setErrorCode
ReflectUtils.reflect(mapLocation).field("p", 0);
mapLocation.setErrorInfo("success");
} catch (Exception e) {
e.printStackTrace();
}
}
if (aMapLocationListener != null) {
aMapLocationListener.onLocationChanged(mapLocation);
}
}
}
那么具體落地到字節(jié)碼中是如何操作的呢系草?
//插入高德地圖相關(guān)字節(jié)碼
if (className == "com.amap.api.location.AMapLocationClient") {
klass.methods?.find {
it.name == "setLocationListener"
}.let {
methodNode ->
methodNode?.instructions?.insert(createAmapLocationInsnList())
}
}
//插入字節(jié)碼
private fun createAmapLocationInsnList(): InsnList {
return with(InsnList()) {
//在AMapLocationClient的setLocationListener方法之中插入自定義代理回調(diào)類
add(TypeInsnNode(NEW, "com/didichuxing/doraemonkit/aop/AMapLocationListenerProxy"))
add(InsnNode(DUP))
//訪問第一個參數(shù)
add(VarInsnNode(ALOAD, 1))
add(MethodInsnNode(
INVOKESPECIAL,
"com/didichuxing/doraemonkit/aop/AMapLocationListenerProxy",
"<init>",
"(Lcom/amap/api/location/AMapLocationListener;)V",
false
)
)
//對第一個參數(shù)進行重新賦值
add(VarInsnNode(ASTORE, 1))
this
}
我們會去遍歷所有的class資源文件通熄,然后通過全限定名找到指定的setLocationListener方法唆涝,然后我們通過ASM提供的inset方法在setLocationListener方法開始的的地方去操作和插入我們內(nèi)置的代碼,從而達到用戶無感知的目的唇辨。
數(shù)據(jù)Mock
數(shù)據(jù)Mock作為DoKit的重磅功能廊酣,我們現(xiàn)在基本上已經(jīng)實現(xiàn)了全平臺(Android、iOS赏枚、H5 js以及小程序)的覆蓋同時該項功能也是在社區(qū)中引起廣泛討論以及評價非常高的功能亡驰。所以我們可以重點分析一下。
傳統(tǒng)解決方案
首先我們來看一下在平時的開發(fā)過程中饿幅,假如不使用DoKit的數(shù)據(jù)Mock方案我們是如何來進行數(shù)據(jù)Mock的凡辱。我們開發(fā)和測試經(jīng)常會使用抓包工具來查看和修改網(wǎng)絡(luò)返回的數(shù)據(jù)。
首先我們來看一下現(xiàn)有的抓包方案都存在哪些問題:
1)無法支持多人協(xié)同操作同一個接口
2)無法針對同一接口返回不同的場景數(shù)據(jù)栗恩。
3)抓包操作起來非常繁瑣透乾,需要和手機保證在同一個局域網(wǎng),還要修改ip和端口號。
針對這些問題乳乌,DoKit提出了打造面向全平臺的數(shù)據(jù)Mock方案捧韵。
為了實現(xiàn)這個目標我經(jīng)過一定程度的調(diào)研,我總結(jié)了一下要實現(xiàn)這個目標我們要解決的難點汉操。
1)統(tǒng)一Android端繁多的網(wǎng)路框架再来。
2)保證業(yè)務(wù)代碼零侵入。
3)為了攔截到H5中Ajax的請求我們必須還要hook Webview磷瘤。
接下來我們來具體看一下DoKit在Andoid端上是如何來解決這些問題的芒篷。
(整個鏈路還是有點長的,請大家耐心往下看膀斋。)
數(shù)據(jù)Mock(終端)
這是DoKit數(shù)據(jù)Mock終端方案在編譯期和運行時的一個簡單流程圖梭伐。由于今天主要的側(cè)重點是AOP字節(jié)碼,所以我們就來看一下DoKit是如何來實現(xiàn)的仰担。
1糊识、統(tǒng)一網(wǎng)絡(luò)請求
我們都知道Android終端封裝的三方網(wǎng)絡(luò)框架有很多,但是仔細分析其實最底層基本上都是基于HttpClient(Google放棄維護不考慮兼容)摔蓝、HttpUrlConnection赂苗、Okhttp(使用最多)。所以我們只要統(tǒng)一HttpUrlConnection和OkHttp兩套框架就可以了贮尉。經(jīng)過調(diào)研拌滋,OkHttp官方提供了一個將HttpUrlConnection轉(zhuǎn)化為OkHttp請求的解決方案:ObsoleteUrlFactory。
所以我們可以通過以下代碼將HttpUrlConnection轉(zhuǎn)化為okhttp的請求猜谚。
if (protocol.equalsIgnoreCase("http")) {
return new ObsoleteUrlFactory.OkHttpURLConnection(url, OkhttpClientUtil.INSTANCE.getOkhttpClient());
}
if (protocol.equalsIgnoreCase("https")) {
return new ObsoleteUrlFactory.OkHttpsURLConnection(url, OkhttpClientUtil.INSTANCE.getOkhttpClient());
}
找到了HttpUrlConnection轉(zhuǎn)化為OkHttp的方案以后败砂,接下來就是想辦法拿到這個HttpUrlConnection對象。
val url = URL(path)
//打開連接
val urlConnection = url.openConnection() as HttpURLConnection
//得到輸入流
val `is` = urlConnection.inputStream
以上的代碼是HttpUrlConnection的標準api魏铅,urlConnection對象是通過url.openConnection()創(chuàng)建而來的昌犹。所以我們需要在編譯期間把以上的代碼改成下面的代碼就可以了。
val url = URL(path)
//打開連接
val urlConnection = HttpUrlConnectionProxyUtil.proxy(url.openConnection()) as HttpURLConnection
//得到輸入流
val `is` = urlConnection.inputStream
那么具體落到字節(jié)碼上是怎么來實現(xiàn)的呢览芳?代碼如下:
private val SHADOW_URL = "com/didichuxing/doraemonkit/aop/urlconnection/HttpUrlConnectionProxyUtil"
private val DESC = "(Ljava/net/URLConnection;)Ljava/net/URLConnection;"
klass.methods.forEach { method ->
method.instructions?.iterator()?.asIterable()?.filterIsInstance(MethodInsnNode::class.java)?.filter {
it.opcode == INVOKEVIRTUAL &&
it.owner == "java/net/URL" &&
it.name == "openConnection" &&
it.desc == "()Ljava/net/URLConnection;"
}?.forEach {
method.instructions.insert(it, MethodInsnNode(INVOKESTATIC, SHADOW_URL, "proxy", DESC, false))
}
}
通過以上的這些操作我們基本上就實現(xiàn)網(wǎng)絡(luò)框架的統(tǒng)一斜姥。
2、插入攔截器
我們都知道OkHttp的核心就是其攔截器沧竟,所以我們只需要在項目啟動的時候把我們自己的內(nèi)置攔截器查插入到攔截器列表的頭部這樣就能對項目中的所有網(wǎng)絡(luò)請求進行攔截了铸敏。通過仔細的源碼閱讀,我們發(fā)現(xiàn)Okhttp攔截器列表的初始化是在OkHttpClient#Build的中進行初始化的悟泵。
public static final class Builder {
Dispatcher dispatcher;
@Nullable Proxy proxy;
List<Protocol> protocols;
List<ConnectionSpec> connectionSpecs;
//通用攔截器列表
final List<Interceptor> interceptors = new ArrayList<>();
//網(wǎng)絡(luò)攔截器列表
final List<Interceptor> networkInterceptors = new ArrayList<>();
EventListener.Factory eventListenerFactory;
ProxySelector proxySelector;
}
那么我們就需要在OkHttpClient#Build構(gòu)造方法的最后在往攔截器列表的頭部加入我們自己的內(nèi)置攔截器杈笔。代碼如下CommTransformer:
if (className == "okhttp3.OkHttpClient\$Builder") {
//空參數(shù)的構(gòu)造方法
klass.methods?.find {
it.name == "<init>" && it.desc == "()V"
}.let { zeroConsMethodNode ->
zeroConsMethodNode?
.instructions?
.getMethodExitInsnNodes()?
.forEach {
zeroConsMethodNode
.instructions
.insertBefore(it,createOkHttpZeroConsInsnList())
}
}
//一個參數(shù)的構(gòu)造方法
klass.methods?.find {
it.name == "<init>" && it.desc == "(Lokhttp3/OkHttpClient;)V"
}.let { oneConsMethodNode ->
oneConsMethodNode?
.instructions?
.getMethodExitInsnNodes()?
.forEach {
oneConsMethodNode
.instructions
.insertBefore(it,createOkHttpOneConsInsnList())
}
}
}
我們看下經(jīng)過編譯以后的代碼是怎么樣的。
public Builder() {
this.interceptors = new ArrayList();
this.networkInterceptors = new ArrayList();
this.dispatcher = new Dispatcher();
......
this.pingInterval = 0;
//編譯期插入的代碼
this.interceptors.addAll(OkHttpHook.globalInterceptors);
this.networkInterceptors.addAll(OkHttpHook.globalNetworkInterceptors);
}
Builder(OkHttpClient okHttpClient) {
this.interceptors = new ArrayList();
this.networkInterceptors = new ArrayList();
this.dispatcher = okHttpClient.dispatcher;
......
//編譯期插入的代碼
OkHttpHook.performOkhttpOneParamBuilderInit(this, okHttpClient);
}
DoKit SDK中內(nèi)置了4個攔截器OkHttpHook
public static void installInterceptor() {
if (IS_INSTALL) {
return;
}
try {
//可能存在用戶沒有引入okhttp的情況
globalInterceptors.add(new MockInterceptor());
globalInterceptors.add(new LargePictureInterceptor());
globalInterceptors.add(new DoraemonInterceptor());
globalNetworkInterceptors.add(new DoraemonWeakNetworkInterceptor());
IS_INSTALL = true;
} catch (Exception e) {
e.printStackTrace();
}
}
至此終端的網(wǎng)絡(luò)攔截功能已經(jīng)完成糕非。此項功能同時也是抓包蒙具、數(shù)據(jù)Mock敦第、弱網(wǎng)模擬、大圖檢測等功能的基礎(chǔ)店量。感興趣的小伙伴可以通過源碼更加深入的了解下芜果。
數(shù)據(jù)Mock(js)
說完了數(shù)據(jù)mock在終端上的實現(xiàn),下面我們來看下H5中的js請求我們要如何才能攔截到融师。
如圖所示右钾,要想攔截到j(luò)s的請求有個技術(shù)前提那就是WebViewClient#shouldInterceptRequest(大家可以去了解一下該方法的作用)。按照慣例旱爆,我們還是得先hook WebView(通過Webview可以拿到WebViewClient)舀射。比如下面的代碼:
mWebView = findViewById<WebView>(R.id.normal_web_view)
initWebView(mWebView)
mWebView.loadUrl(url)
我們要加載h5,那么就必須要調(diào)用loadUrl怀伦。所以我們需要在loadUrl之前對webView進行一些操作脆烟。比如這樣:
mWebView = findViewById<WebView>(R.id.normal_web_view)
initWebView(mWebView)
WebViewHook.inject(mWebView);
mWebView.loadUrl(url)
看起來好像不是很復雜,但是這樣有一個難點房待,我們需要通過字節(jié)碼的方式去改變字節(jié)碼棧頂?shù)捻樞蛐细帷N覀兺ㄟ^代碼來直觀的感受下吧。
klass.methods.forEach { method ->
method.instructions?.iterator()?
.asIterable()?
.filterIsInstance(MethodInsnNode::class.java)?
.filter {
it.opcode == INVOKEVIRTUAL &&
it.name == "loadUrl" &&
it.desc == "(Ljava/lang/String;)V" &&
isWebViewOwnerNameMatched(it.owner)
}?.forEach {
method.instructions.insertBefore(
it,
createWebViewInsnList())
}
}
/**
* 創(chuàng)建webView函數(shù)指令集
* 參考:http://www.reibang.com/p/7d623f441bed
*/
private fun createWebViewInsnList(): InsnList {
return with(InsnList()) {
//復制棧頂?shù)?個指令 指令集變?yōu)?比如 aload 2 aload0 aload 2 aload0
add(InsnNode(DUP2))
//拋出最上面的指令 指令集變?yōu)?aload 2 aload0 aload 2 其中 aload 2即為我們所需要的對象
add(InsnNode(POP))
add(
MethodInsnNode(
INVOKESTATIC,
"com/didichuxing/doraemonkit/aop/WebViewHook",
"inject",
"(Ljava/lang/Object;)V",
false
)
)
this
}
}
注意DUP2和POP指令的配合使用桑孩,注釋里已經(jīng)寫了原因拜鹤。這是這一塊的難點×鹘罚可以看到字節(jié)碼指令非常強大敏簿,大家如果對字節(jié)碼有深入的了解的話,真的可以為所欲為宣虾。
所以其實通過我們插件編譯以后的代碼是這樣的:
mWebView = findViewById<WebView>(R.id.normal_web_view)
initWebView(mWebView)
String var3 = this.url;
WebViewHook.inject(mWebView);
mWebView.loadUrl(url)
多了一行url的賦值代碼惯裕,但是這基本上不影響我們的功能,我們也不需要在意绣硝。
最后我們拿到Webview對象以后我們就能注入自己的WebviewClient蜻势。WebViewHook
private static void injectNormal(WebView webView) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!(WebViewCompat.getWebViewClient(webView) instanceof DokitWebViewClient)) {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setAllowUniversalAccessFromFileURLs(true);
webView.addJavascriptInterface(new DokitJSI(), "dokitJsi");
webView.setWebViewClient(new DokitWebViewClient(WebViewCompat.getWebViewClient(webView), settings.getUserAgentString()));
}
}
}
一開始我們已經(jīng)說過了shouldInterceptRequest方法的入?yún)o法拿到post的body信息。所以這里又遇到問題域那,經(jīng)過一番調(diào)研咙边,我們其實在該方法中是可以拿到原始的html數(shù)據(jù)流的猜煮,那么我們只需要在Webview開始渲染之前次员,在原始的html數(shù)據(jù)中插入我們自己的一段js腳本,腳本中根據(jù)js的原型鏈原理王带,我們會去指定XmlHttpRequest和Fetch的幾個核心方法的原型淑蔚,具體參考:dokit_js_hook.html和dokit_js_vconsole_hook.html。
然后我們在通過jsBridge將js的請求信息告知終端愕撰,終端拿到請求以后再通過okhttp去代理轉(zhuǎn)發(fā)刹衫,于是整條鏈路又回到了終端數(shù)據(jù)mock的流程醋寝。
最終H5助手的效果圖如下:
<img src="https://pt-starimg.didistatic.com/static/starimg/img/EUJrTzZCLU1603943247344.jpg" width="250"/>
業(yè)務(wù)價值
到此數(shù)據(jù)Mock的整條鏈路在Android上的實現(xiàn)都已經(jīng)分析完了。這一塊由于篇幅的原因沒有深入到每一個技術(shù)點去講带迟,只是簡單的闡述了一下AOP方案音羞,歡迎感興趣的小伙伴和我進行深入的交流。
總結(jié)
DoKit一直追求給開發(fā)者提供最便捷和最直觀的開發(fā)體驗,同時我們也十分歡迎社區(qū)中能有更多的人參與到DoKit的建設(shè)中來并給我們提出寶貴的意見或PR仓犬。
DoKit的未來需要大家共同的努力嗅绰。
最后,厚臉皮的拉一波star搀继。來都來了窘面,點個star再走唄。DoKit