偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

R8疑難雜癥分析實(shí)戰(zhàn):外聯(lián)優(yōu)化設(shè)計(jì)缺陷引起的崩潰

開(kāi)發(fā) 前端 Android
在Android開(kāi)發(fā)中,即使是AGP、R8這樣的官方工具鏈升級(jí),也要保持足夠的警惕。畢竟Android生態(tài)太過(guò)復(fù)雜,再加上開(kāi)發(fā)者們千奇百怪的代碼寫(xiě)法,不論多么完善的測(cè)試流程都無(wú)法規(guī)避這類特定場(chǎng)景的bug。

一、背景

二、復(fù)現(xiàn)問(wèn)題

三、問(wèn)題分析

    1. ApiModel外聯(lián)是什么?

    2. 為什么會(huì)多生成一個(gè)new-instance指令?

    3. R8是如何計(jì)算出API的版本?

    4. 為什么try-catch也會(huì)導(dǎo)致該問(wèn)題?

    5. 為什么只升級(jí)AGP會(huì)導(dǎo)致R8功能出問(wèn)題?

四、解決方案

    1. 禁用ApiModel

    2. 官方修復(fù)

    3. 自行修復(fù)

    4. 業(yè)務(wù)改造(推薦)

五、總結(jié)

一、背景

R8作為谷歌官方的編譯優(yōu)化工具,在編譯階段會(huì)對(duì)字節(jié)碼進(jìn)行大規(guī)模修改,以追求包體優(yōu)化和性能提升。但是Android應(yīng)用開(kāi)發(fā)者數(shù)量太過(guò)龐大,無(wú)論測(cè)試流程多么完善,終究難以避免在一些特定場(chǎng)景下出現(xiàn)問(wèn)題。

近期我們?cè)谏?jí)項(xiàng)目的AGP,遇到了一個(gè)指向系統(tǒng)SurfaceTexture類的native崩潰問(wèn)題。經(jīng)反編譯分析發(fā)現(xiàn)問(wèn)題最終指向了smali字節(jié)碼中多余的一行new-instance指令。

圖片圖片

圖片圖片

該指令創(chuàng)建了一個(gè)SurfaceTexture對(duì)象,但是并未調(diào)用其<init>方法,這意味著構(gòu)造方法沒(méi)有執(zhí)行,但是這個(gè)類重寫(xiě)了finalize方法,后續(xù)被gc回收時(shí)會(huì)調(diào)用其中的nativeFinalize這個(gè)JNI方法,最終在native層執(zhí)行析構(gòu)函數(shù)時(shí)觸發(fā)了SIGNALL 11的內(nèi)存訪問(wèn)錯(cuò)誤.

圖片圖片

圖片圖片

二、復(fù)現(xiàn)問(wèn)題

我們注意到多出來(lái)的new-instance指令下面緊接著的是對(duì)a0.e 類中的靜態(tài)方法 i() 的調(diào)用,其內(nèi)部實(shí)現(xiàn)就是SurfaceTexture的構(gòu)造方法。這是典型的代碼外聯(lián)操作,即一段相同的代碼在工程中多次出現(xiàn),則會(huì)被抽出來(lái)單獨(dú)作為一個(gè)靜態(tài)函數(shù),原先的調(diào)用點(diǎn)則替換成該函數(shù)的調(diào)用,這樣可以減小代碼體積,是常見(jiàn)的編碼思路。

例如:

class Activity{
    void onCreate(){
        // ...
        String a = xx.xxx();
        String b = xx.xxx();
        Log.e("log",a+b);
        //...
    }


    void onReusme(){
        // ...
        String a = xx.xxx();
        String b = xx.xxx();
        Log.e("log",a+b);
        //...
    }


}
class Activity{
    void onCreate(){
        // ...
        Activity$Outline.log();
        //...
    }


    void onReusme(){
        // ...
        Activity$Outline.log();
        //...
    }
}
//外聯(lián)生成的類
class Activity$Outline{
    public static void log(){
        String a = xx.xxx();
        String b = xx.xxx();
        Log.e("log",a+b);
    }
}

我們根據(jù)這個(gè)生成類的類名可以知道是R8中ApiModelOutline功能生成了這個(gè)類。

圖片圖片

我們進(jìn)到R8工程中檢索下相關(guān)的關(guān)鍵字,再加上demo多次嘗試,可以確認(rèn)滿足以下條件能夠必現(xiàn)該問(wèn)題:

  1. 使用了高于當(dāng)前minSdkVersion的系統(tǒng)函數(shù)/變量(僅限系統(tǒng)類,自己寫(xiě)的無(wú)效)
  2. 用synchronized或者try語(yǔ)句塊包裹了該調(diào)用,或者給該函數(shù)傳參時(shí)有任何計(jì)算行為(除了傳局部變量)。例如:
  1. new SurfaceTexture( getParmas() )
  2. new SurfaceTexture( if(enable) 1 : 2)
  3. new SurfaceTexture ( (boolean) enable )

三、問(wèn)題分析

在確認(rèn)復(fù)現(xiàn)條件之后,我們帶著幾個(gè)問(wèn)題來(lái)逐個(gè)分析。

ApiModel外聯(lián)是什么?

R8中的優(yōu)化大多數(shù)跟包體優(yōu)化有關(guān),代碼外聯(lián)也是其中一種,但是外聯(lián)的前提是代碼重復(fù)的次數(shù)滿足一定閾值,但是ApiModel會(huì)對(duì)所有調(diào)用了高版本系統(tǒng)API的代碼做外聯(lián),包括只調(diào)用一次的場(chǎng)景。

ApiModel并非為了包體優(yōu)化,我們通過(guò)R8工程的issueTracker(https://issuetracker.google.com/issues/333477035)檢索到了相關(guān)的信息:

圖片圖片

譯:AGP新增的ApiModel功能是為了防止在低版本設(shè)備上不可能執(zhí)行的代碼引起類驗(yàn)證錯(cuò)誤,從而降低App啟動(dòng)耗時(shí)。

從這篇介紹ART虛擬機(jī)類驗(yàn)證的文檔(https://chromium.googlesource.com/chromium/src/+/HEAD/build/android/docs/class_verification_failures.md#chromium_s-solution)就能夠理解上面這句話的含義:

ART虛擬機(jī)會(huì)在APK安裝之后立刻執(zhí)行 AOT class verification,即對(duì)dex文件中所有的類進(jìn)行驗(yàn)證,如果驗(yàn)證成功則后續(xù)運(yùn)行時(shí)將不需要再進(jìn)行驗(yàn)證,反之若失敗,則該class會(huì)被ART打上RetryVerificationAtRuntime的標(biāo)記,后續(xù)運(yùn)行時(shí)還得重新執(zhí)行類驗(yàn)證。

同時(shí)這些失敗的類也將無(wú)法被dex2oat優(yōu)化成oat格式的優(yōu)化字節(jié)碼(oat字節(jié)碼的加載和執(zhí)行速度更快)。

圖片圖片

如果是在MainActivity,啟動(dòng)任務(wù)中使用了這些高版本API,那么在低版本設(shè)備App啟動(dòng)時(shí)就必須額外執(zhí)行一次類驗(yàn)證(比較耗時(shí),有的類能到8ms https://issues.chromium.org/issues/40574431),而ApiModel外聯(lián)則是相當(dāng)于將這些肯定驗(yàn)證失敗的函數(shù)的調(diào)用單獨(dú)抽到一個(gè)生成類中,這樣運(yùn)行時(shí)就能將類驗(yàn)證失敗問(wèn)題徹底隔離在生成類中,從而規(guī)避運(yùn)行時(shí)的類驗(yàn)證耗時(shí)。

//安裝apk后驗(yàn)證失敗,運(yùn)行時(shí)驗(yàn)證失敗,但是能正常執(zhí)行
class MainActivity{
    void onCreate(){
        if(android.sdk > 26){
            new SurfaceTexture(false);
        }
    }
}

ApiModel后

class MainActivity{
    void onCreate(){
        if(android.sdk > 26){
            a0.b(); //這樣類驗(yàn)證就能成功
        }
    }
}
//生成的外聯(lián)類,類驗(yàn)證會(huì)失敗,但是運(yùn)行時(shí)不可能走到,不影響
class a0{
    public static void b(){
        new SurfaceTexture(false);
    }
}

更多關(guān)于ApiModel的詳細(xì)介紹,見(jiàn)這篇文章:https://medium.com/androiddevelopers/mitigating-soft-verification-issues-in-r8-and-d8-7e9e06827dfd

為什么會(huì)多生成一個(gè)new-instance指令?

介紹完ApiModel之后,我們已經(jīng)知道了為什么<init>方法的調(diào)用被替換成了一個(gè)生成函數(shù)的調(diào)用,接下來(lái)我們?cè)俜治鱿聦?dǎo)致崩潰的罪魁禍?zhǔn)?new-instance 指令是如何出現(xiàn)的。

我們先來(lái)了解下java文件在編譯過(guò)程中的格式轉(zhuǎn)換過(guò)程,因?yàn)锳piModel是基于IRCode格式(R8自定義的格式)來(lái)做外聯(lián)。

文件轉(zhuǎn)換

javac

javac將java文件編譯成class文件

值得一提的是sychronized語(yǔ)句塊在javac編譯之后會(huì)為其內(nèi)部代碼生成try-catch,這是為了確保在語(yǔ)句塊拋異常時(shí)能夠正常釋放鎖,因此和問(wèn)題有關(guān)的是try-catch語(yǔ)句塊,和synchronized無(wú)關(guān)。

圖片圖片

D8

目前R8已經(jīng)整合D8,因此輸入class文件之后就會(huì)先通過(guò)D8轉(zhuǎn)為dex格式,并持有在內(nèi)存中。

轉(zhuǎn)換之后的指令基本和class字節(jié)碼基本類似。

圖片

IRcode

為了做進(jìn)一步的優(yōu)化,會(huì)將dex格式的代碼轉(zhuǎn)化成R8自定義的IRcode格式,其特點(diǎn)是代碼分塊。

案例:

圖片圖片

問(wèn)題根因

在R8工程里檢索ApiModel關(guān)鍵字,最終定位到針對(duì)構(gòu)造函數(shù)生成外聯(lián)函數(shù)和指令替換的代碼:

InstanceInitializerOutliner->rewriteCode

執(zhí)行此方法之前的指令如下:

java:
new SurfaceTexture(false);
dex:
: -1: NewInstance          v1 <-  android.graphics.SurfaceTexture
: -1: ConstNumber          v2(0) <-  0 (INT)
: -1: Invoke-Direct        v1, v2(0); method: void android.graphics.SurfaceTexture.<init>(boolean)
  • 對(duì)整個(gè)方法中所有的指令從上往下進(jìn)行遍歷,第一次遍歷主要是:

檢索 <init>方法調(diào)用的指令

判斷該方法的androidApiLevel是否高于minSDK

生成包含完整構(gòu)造函數(shù)指令的外聯(lián)函數(shù),并替換<init>函數(shù)調(diào)用為外聯(lián)函數(shù)調(diào)用。

執(zhí)行完替換邏輯,就記錄信息到map中,key是<init>對(duì)應(yīng)的new-instance指令,value是前一步中替換的新指令。

經(jīng)過(guò)這一步,字節(jié)碼會(huì)變成這樣:

圖片圖片

具體替換邏輯如下(可以參考注釋理解):

圖片圖片

  • 第二次遍歷則是對(duì)new-instance指令的處理:

找到new-instance指令

查詢map,確認(rèn)<init>方法已完成替換

根據(jù)canSkipClInit方法返回的結(jié)果分為兩種場(chǎng)景:

無(wú)類初始化邏輯:直接移除new-instance指令,不影響原代碼的語(yǔ)義。

圖片圖片

  • 有類初始化邏輯:生成外聯(lián)函數(shù),只包含該new-instance指令,和前一次遍歷一樣進(jìn)行指令替換。

圖片圖片

具體替換邏輯:

圖片圖片

  • 問(wèn)題重點(diǎn)就在于canSkipClInit這個(gè)函數(shù)的實(shí)現(xiàn)。

它會(huì)檢查 new-intance指令和invoke <init>指令之間是否存在任何局部變量聲明以外的指令,如果存在,他會(huì)認(rèn)為這些指令是這個(gè)類初始化的邏輯,因此為了保留源代碼的執(zhí)行順序,這種情況下就是需要額外執(zhí)行一次new-instance指令來(lái)觸發(fā)類初始化。

圖片圖片

但是實(shí)際上,如果在調(diào)用這個(gè)構(gòu)造函數(shù)傳參時(shí)執(zhí)行了任何運(yùn)算(和類加載無(wú)關(guān)),都會(huì)生成相關(guān)的指令插在中間,例如:


java寫(xiě)法

new-intance和invoke <init>指令之間的指令

new SurfaceTexture( getParmas() )

invoke-virtual   v2 <-; method: void xx.xx.xx

new SurfaceTexture( if(enable) 1 : 2)

StaticGet            v3 <- ; field: boolean  xxx.xxx.xx

new SurfaceTexture ( (boolean) enable )

: -1: CheckCast            v5 <- v3; java.lang.Boolean

: -1: Invoke-Virtual       v6 <- v5; method: boolean java.lang.Boolean.booleanValue()

從作者留下的todo也能看出,后續(xù)準(zhǔn)備擴(kuò)展這個(gè)方法,實(shí)現(xiàn)對(duì)這些夾在中間的指令的判斷,如果是對(duì)類初始化無(wú)影響的入?yún)⒂?jì)算邏輯,則也將正常移除new-intance指令。

圖片圖片

值得一提的是,我們最終APK里 new-intance指令并沒(méi)有被外聯(lián),這是因?yàn)镾urfaceTexture這個(gè)類本身在安卓21之前的版本就已經(jīng)存在,只是入?yún)閎ool類型的構(gòu)造方法是在安卓26新增的,所以他其實(shí)是被外聯(lián)之后又被內(nèi)聯(lián)回到了調(diào)用處,因此看起來(lái)像是沒(méi)有被外聯(lián)。

圖片圖片

小結(jié)

至此,我們就明白了多出來(lái)一個(gè)看似無(wú)用的new-intance指令,實(shí)際上是為了保全源代碼的語(yǔ)義,觸發(fā)類加載用的,但是作者沒(méi)有考慮到這些被優(yōu)化的類可能重寫(xiě)了finalize方法來(lái)釋放一些本就不存在的資源。

而且不局限于調(diào)用native函數(shù),只要是重寫(xiě)了finalize,并在里面訪問(wèn)一些在構(gòu)造函數(shù)中初始化的成員變量,一樣可能造成NPE等崩潰。

R8是如何計(jì)算出API的版本?

圖片圖片

R83.3版本開(kāi)始,它編譯時(shí)會(huì)下載一個(gè).ser格式的數(shù)據(jù)庫(kù)文件,里面記錄了所有系統(tǒng)API、變量與安卓版本號(hào)的映射信息,在運(yùn)行時(shí)通過(guò)行號(hào)和偏移量來(lái)尋找各自的版本號(hào)。

圖片圖片

為什么try-catch也會(huì)導(dǎo)致該問(wèn)題?

前面解釋了在構(gòu)造函數(shù)入?yún)⒅刑砑雍瘮?shù)調(diào)用等寫(xiě)法導(dǎo)致的字節(jié)碼異常原因,但是實(shí)際上這次我們遇到的崩潰場(chǎng)景是在sychronized里new了一個(gè)SurfaceTexture。

圖片圖片

前文中已經(jīng)解釋過(guò),sychronized在編譯成class后會(huì)生成try-catch語(yǔ)句塊,這段代碼改成用try-catch語(yǔ)句塊包裹,一樣會(huì)復(fù)現(xiàn)崩潰,因此我們跟蹤try-catch在文件轉(zhuǎn)換過(guò)程中對(duì)字節(jié)碼的影響即可。

回到class文件轉(zhuǎn)dex文件的階段,我們發(fā)現(xiàn)try語(yǔ)句塊中的每一行指令,都會(huì)在其后生成一條FALLTHROUGH指令。

dex格式:

圖片圖片

FALLTHROUGH是什么指令,他是做什么的?

FALLTHROUGH指令表示指令自然流轉(zhuǎn),沒(méi)有實(shí)際含義,它主要是為了幫助優(yōu)化器識(shí)別哪些指令是可達(dá)的。

例如下面這種寫(xiě)法,case1沒(méi)有寫(xiě)break,這樣會(huì)接著執(zhí)行case2的代碼:

switch (value) {
            case 1:
                System.out.println("One");
                // 故意不寫(xiě)break
            case 2:
                System.out.println("Two");
                break;
            case 3:
                System.out.println("Three");
                break;
        }

其字節(jié)碼如下:

正常有break的話,會(huì)對(duì)應(yīng)一條GOTO 指令跳轉(zhuǎn)到switch語(yǔ)句塊最后一行,但是沒(méi)寫(xiě)break的話,就會(huì)出現(xiàn):

在12行執(zhí)行 goto 13 跳轉(zhuǎn)到13行的指令,這種指令毫無(wú)意義,且運(yùn)行時(shí)會(huì)消耗性能,因此可以替換成FALLTHROUGH指令,這樣最終在生成dex文件時(shí)會(huì)被移除掉,從而避免浪費(fèi)性能。

public static void switchWithFallthrough(int);
  Code:
    stack=2, locals=1, args_size=1


    // 加載參數(shù)
    0: iload_0


    // 檢查case 1
    1: iconst_1
    2: if_icmpne 13    // 如果不等于1,跳轉(zhuǎn)到case 2
    5: getstatic #2    // Field java/lang/System.out:Ljava/io/PrintStream;
    8: ldc #3          // String One
    10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: goto 13


    // case 2 (fallthrough目標(biāo))
    13: iconst_2
    14: if_icmpne 28   // 如果不等于2,跳轉(zhuǎn)到case 3
    17: getstatic #2   // Field java/lang/System.out:Ljava/io/PrintStream;
    20: ldc #5         // String Two
    22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    25: goto 40        // 跳轉(zhuǎn)到switch結(jié)束


    // case 3
    28: iconst_3
    29: if_icmpne 40   // 如果不等于3,跳轉(zhuǎn)到結(jié)束
    32: getstatic #2   // Field java/lang/System.out:Ljava/io/PrintStream;
    35: ldc #6         // String Three
    37: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V


    // switch結(jié)束
    40: return

既然沒(méi)用為什么還要加這個(gè)指令?

class文件是通過(guò)Exception table來(lái)指定異常處理的指令范圍,而dex文件則是通過(guò)為每一行可能產(chǎn)生throwable的指令后面添加FALLTHROUGH指令來(lái)實(shí)現(xiàn)try-catch。

這里會(huì)把每一行可能崩潰的指令都鏈接到catch指令所在的block中,確保任意位置的崩潰都能正常走到catch中。

圖片

問(wèn)題根因

在R8 4.0.26版本,IRCode翻譯器新增了對(duì)FALLTHROUGH指令的處理,即新建一個(gè)block并生成一條GOTO指令指向新的block。

圖片圖片

根據(jù)前文的結(jié)論,GOTO指令一樣會(huì)被認(rèn)為是類初始化相關(guān)的邏輯,因此try-catch語(yǔ)句塊一樣會(huì)導(dǎo)致最終多出來(lái)一個(gè)new-instance字節(jié)碼。

為什么只升級(jí)AGP會(huì)導(dǎo)致R8功能出問(wèn)題?

我們?cè)跀?shù)個(gè)版本之前就已經(jīng)單獨(dú)升級(jí)了R8,正好涵蓋了ApiModel這個(gè)變更,但是直到近期才升級(jí)了AGP。

可以看到從AGP7.3-beta版本開(kāi)始,才默認(rèn)打開(kāi)ApiModel功能,這就解釋了為什么升級(jí)AGP之后才出現(xiàn)此崩潰。

圖片

四、解決方案

禁用ApiModel

ApiModel通過(guò)犧牲些微包體,換來(lái)啟動(dòng)階段類驗(yàn)證耗時(shí),但是從他覆蓋的類范圍來(lái)看,對(duì)啟動(dòng)速度的收益微乎其微,因此可以直接通過(guò)配置開(kāi)關(guān)關(guān)閉整個(gè)功能。

System.setProperty("com.android.tools.r8.disableApiModeling", "1")

雖說(shuō)這是個(gè)實(shí)驗(yàn)中的功能,且邏輯相對(duì)獨(dú)立,但是考慮到后續(xù)還有內(nèi)聯(lián)優(yōu)化等操作,貿(mào)然關(guān)閉整個(gè)功能無(wú)法評(píng)估影響面,潛在的穩(wěn)定性風(fēng)險(xiǎn)較高。

官方修復(fù)

該問(wèn)題反饋給R8團(tuán)隊(duì)后,官方提供了臨時(shí)規(guī)避的方案,即確保高版本API在單獨(dú)的函數(shù)中調(diào)用。

https://issuetracker.google.com/issues/441137561

圖片圖片

隨后不久就提了MR針對(duì)SurfaceTexture這個(gè)類禁用了ApiModel,并未徹底解決此問(wèn)題。https://r8-review.googlesource.com/c/r8/+/109044

圖片圖片

官方的修復(fù)方案比較權(quán)威,且影響面較小,但是并未徹底解決問(wèn)題。

自行修復(fù)

如果要修復(fù)此問(wèn)題,關(guān)鍵是要將多余的new-instance指令替換成一個(gè)合適的觸發(fā)類加載的指令,根據(jù)java官方文檔里的介紹,只有new對(duì)象,訪問(wèn)靜態(tài)的成員變量或者函數(shù)的指令才能安全的觸發(fā)類加載,比較理想的方案是改成訪問(wèn)靜態(tài)變量,但是很多類并沒(méi)有靜態(tài)變量,比如SurfaceTexture就沒(méi)有。

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5

圖片

因此我們可以考慮結(jié)合getStatic指令和掃描finalize的方式來(lái)解決該問(wèn)題:

圖片圖片

雖說(shuō)可以通過(guò)打印日志來(lái)約束此改動(dòng)的影響面,但畢竟要自行修改并編譯R8的jar包,且需要自行長(zhǎng)期維護(hù),整體影響面還是偏大,對(duì)穩(wěn)定性要求高的App不建議采用該方案。

業(yè)務(wù)改造(推薦)

在前文中提到的外聯(lián)函數(shù)生成處打印日志,即可感知到工程中有哪些類受ApiModel影響,如果數(shù)量不多,分別讓業(yè)務(wù)改造其相關(guān)的寫(xiě)法,確保傳參時(shí)是局部變量且無(wú)try-catch/synchronized語(yǔ)句塊即可。

圖片

考慮到App整體的穩(wěn)定性,最終我們采用了業(yè)務(wù)改造的方式繞過(guò)了此問(wèn)題,并在R8異常代碼處添加了日志告警來(lái)預(yù)防后續(xù)增量問(wèn)題,并仿照官方MR中的寫(xiě)法補(bǔ)充了類的黑名單,用于應(yīng)對(duì)無(wú)法編輯的三方庫(kù)引入此問(wèn)題的場(chǎng)景。

五、總結(jié)

在Android開(kāi)發(fā)中,即使是AGP、R8這樣的官方工具鏈升級(jí),也要保持足夠的警惕。畢竟Android生態(tài)太過(guò)復(fù)雜,再加上開(kāi)發(fā)者們千奇百怪的代碼寫(xiě)法,不論多么完善的測(cè)試流程都無(wú)法規(guī)避這類特定場(chǎng)景的bug。

這次的ApiModel外聯(lián)優(yōu)化問(wèn)題就是一個(gè)很好的例子——它只在特定條件下才會(huì)暴露,但一旦出現(xiàn)就是必現(xiàn)的native崩潰。所以對(duì)于這種影響面無(wú)法評(píng)估的重大升級(jí),還是需要經(jīng)過(guò)足夠長(zhǎng)時(shí)間的獨(dú)立灰度驗(yàn)證,才能合入主干分支。

責(zé)任編輯:武曉燕 來(lái)源: 得物技術(shù)
相關(guān)推薦

2012-11-26 10:23:08

醫(yī)療大數(shù)據(jù)R統(tǒng)計(jì)語(yǔ)言

2015-09-15 10:09:09

TCP網(wǎng)絡(luò)協(xié)議

2018-10-31 14:40:19

TCP協(xié)議ISO

2009-02-05 10:12:00

2022-07-17 12:58:43

Docke技巧

2022-04-06 13:55:22

DockerLinux

2009-04-29 14:46:15

ADSL寬帶掉線

2015-09-06 11:41:15

快碼眾包加速器

2009-01-11 09:29:00

局域網(wǎng)共享?yè)芴?hào)

2022-01-20 22:05:19

ChromiumtransformCSS

2010-08-19 09:48:46

IE6

2010-08-26 09:03:05

IE6

2019-02-21 09:32:13

MQ中間件SQL

2014-06-23 13:59:18

互聯(lián)網(wǎng)電商運(yùn)營(yíng)

2022-07-28 14:29:38

機(jī)器學(xué)習(xí)技術(shù)

2016-08-19 12:59:06

醫(yī)療信息疑難雜癥

2018-11-20 09:25:00

2018-04-11 07:48:16

2021-11-04 15:17:59

網(wǎng)絡(luò)數(shù)據(jù)技術(shù)

2017-11-28 17:09:52

華為云
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)