滴滴DoKit-Android核心原理揭秘之AOP字節(jié)碼實現(xiàn)

前言

最近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方案選型

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原理

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落地場景

AOP實現(xiàn)

站在巨人的肩膀上能夠幫助我們更快更好的實現(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(終端)

數(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 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.htmldokit_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ù)價值

業(yè)務(wù)價值

到此數(shù)據(jù)Mock的整條鏈路在Android上的實現(xiàn)都已經(jīng)分析完了。這一塊由于篇幅的原因沒有深入到每一個技術(shù)點去講带迟,只是簡單的闡述了一下AOP方案音羞,歡迎感興趣的小伙伴和我進行深入的交流。

總結(jié)

DoKit一直追求給開發(fā)者提供最便捷和最直觀的開發(fā)體驗,同時我們也十分歡迎社區(qū)中能有更多的人參與到DoKit的建設(shè)中來并給我們提出寶貴的意見或PR仓犬。

DoKit的未來需要大家共同的努力嗅绰。

最后,厚臉皮的拉一波star搀继。來都來了窘面,點個star再走唄。DoKit

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末叽躯,一起剝皮案震驚了整個濱河市财边,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌点骑,老刑警劉巖酣难,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異黑滴,居然都是意外死亡鲸鹦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門跷跪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馋嗜,“玉大人,你說我怎么就攤上這事吵瞻「鸸剑” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵橡羞,是天一觀的道長眯停。 經(jīng)常有香客問我,道長卿泽,這世上最難降的妖魔是什么莺债? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮签夭,結(jié)果婚禮上齐邦,老公的妹妹穿的比我還像新娘。我一直安慰自己第租,他們只是感情好措拇,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著慎宾,像睡著了一般丐吓。 火紅的嫁衣襯著肌膚如雪浅悉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天券犁,我揣著相機與錄音术健,去河邊找鬼。 笑死粘衬,一個胖子當著我的面吹牛苛坚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播色难,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼泼舱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枷莉?” 一聲冷哼從身側(cè)響起娇昙,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎笤妙,沒想到半個月后冒掌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡蹲盘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年股毫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片召衔。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡铃诬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出苍凛,到底是詐尸還是另有隱情趣席,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布醇蝴,位于F島的核電站宣肚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏悠栓。R本人自食惡果不足惜霉涨,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惭适。 院中可真熱鬧笙瑟,春花似錦、人聲如沸腥沽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽今阳。三九已至师溅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盾舌,已是汗流浹背墓臭。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留妖谴,地道東北人窿锉。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像膝舅,于是被迫代替她去往敵國和親嗡载。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355