日本电影一区二区_日本va欧美va精品发布_日本黄h兄妹h动漫一区二区三区_日本欧美黄色

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

作者 | 阿里巴巴文娛高級(jí)無線開發(fā)工程師 孫瓏達(dá)

責(zé)編 | 屠敏

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

背景

為了適應(yīng)產(chǎn)品的快速迭代,通常大量的研發(fā)資源會(huì)投入在新功能的開發(fā)上,而針對(duì)無用功能的治理卻很少被關(guān)注。隨著時(shí)間的推移,線上應(yīng)用會(huì)積累大量的無用代碼,再加上人員更迭以及功能交接,治理無用代碼的成本越來越高。最終應(yīng)用安裝包過大,導(dǎo)致應(yīng)用下載轉(zhuǎn)化率降低、應(yīng)用平臺(tái)上架受限(例如超過100M的應(yīng)用不能上架谷歌商店)、研發(fā)效率降低等等。

如何治理無用代碼?首先是代碼靜態(tài)掃描。對(duì)于Android應(yīng)用,ProGuard工具可以在構(gòu)建階段靜態(tài)分析代碼引用關(guān)系,自動(dòng)裁減掉沒有被引用到的代碼,減少安裝包大小。

當(dāng)然,只有代碼靜態(tài)掃描是不夠的,因?yàn)樗荒艽砭€上用戶實(shí)際的使用情況,所以還需要一套線上用戶代碼覆蓋率的統(tǒng)計(jì)方案。

下面我將從Android應(yīng)用的線上代碼覆蓋率統(tǒng)計(jì)切入,分享優(yōu)酷的無用代碼治理的技術(shù)思考和落地方案。

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

傳統(tǒng)采集方案

首先,在需要統(tǒng)計(jì)的代碼處加上統(tǒng)計(jì)代碼。當(dāng)代碼被執(zhí)行時(shí),進(jìn)行統(tǒng)計(jì)和上報(bào)。應(yīng)用的代碼行數(shù)通常都數(shù)以萬計(jì),手動(dòng)添加顯然是不現(xiàn)實(shí)的,所以一般會(huì)在構(gòu)建階段通過面向切面編程(AOP)來插入統(tǒng)計(jì)代碼(以下簡(jiǎn)稱為插樁),可以借助一些成熟的AOP中間件完成,例如,Jacoco、ASM。

其次,需要思考是,我們期望采集的粒度是什么?一般來說,粒度從細(xì)到粗分為:指令、分支、方法、類級(jí)別,粒度越細(xì),代碼覆蓋率結(jié)果越準(zhǔn)確,但性能損耗也越大。例如,如果想采集的粒度為指令級(jí)別,就需要對(duì)每個(gè)指令進(jìn)行插樁,但這種插裝會(huì)導(dǎo)致指令數(shù)也翻倍,安裝包增大并且運(yùn)行時(shí)性能下降。

優(yōu)酷曾嘗試過用Jacoco進(jìn)行分支粒度的插樁,當(dāng)時(shí)希望覆蓋盡量多的用戶,因?yàn)楦采w的用戶越多結(jié)果越準(zhǔn)確。但經(jīng)測(cè)試,此方案使安裝包增大10M,運(yùn)行時(shí)性能嚴(yán)重惡化,果斷放棄了此方案。

為了權(quán)衡性能和采集粒度,目前我們一般都采取類級(jí)別粒度的插樁,一方面是因?yàn)檫@樣對(duì)性能影響較小,另一方面過細(xì)的采集粒度反而會(huì)加重業(yè)務(wù)方治理的難度。但此方案還不夠完美:

1)運(yùn)行時(shí)性能:當(dāng)類首次加載時(shí)會(huì)執(zhí)行統(tǒng)計(jì)代碼,App啟動(dòng)過程會(huì)加載成千上萬個(gè)類 ,會(huì)對(duì)啟動(dòng)性能造成一定影響;

2)包大?。河卸嗌賯€(gè)類,就會(huì)插入多少行統(tǒng)計(jì)代碼,對(duì)于像優(yōu)酷這種大型App,也會(huì)增加不少的安裝包大??;

3)構(gòu)建耗時(shí):因?yàn)闃?gòu)建過程中需要對(duì)每個(gè)類進(jìn)行插樁,增加了構(gòu)建耗時(shí);

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

新采集方案—SlimLady

? 目標(biāo)

優(yōu)酷希望有一套方案可以無損地采集線上代碼覆蓋率,核心目標(biāo)如下:

  1. 運(yùn)行時(shí)性能:無任何影響;

  2. 包大?。簾o任何影響;

  3. 構(gòu)建耗時(shí):無任何影響;

? 實(shí)現(xiàn)

通過研究源碼,發(fā)現(xiàn)可以通過動(dòng)態(tài)查詢DVM虛擬機(jī)已加載類的信息來獲取類級(jí)別的代碼覆蓋率,下圖中“覆蓋率采集”部分即為SlimLady采集的原理圖,這里我們只關(guān)注這部分,其他部分將在后面的整體方案中進(jìn)行講解。

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

ClassTable

Java虛擬機(jī)規(guī)范規(guī)定,類在使用前要先被虛擬機(jī)加載。Android中,是通過ClassLoader來完成類加載的,最后保存在Native層的ClassTable中,所以如果我們獲取了所有ClassLoader的ClassTable對(duì)象,就有可能判斷出虛擬機(jī)加載了哪些類。

首先,獲取所有的ClassLoader對(duì)象。對(duì)于APK中的類,如果無特殊聲明,一般都會(huì)被默認(rèn)的PathClassLoader加載;對(duì)于動(dòng)態(tài)加載的類,需要在自定義的ClassLoader中加載,例如Atlas會(huì)為每個(gè)Bundle創(chuàng)建一個(gè)相應(yīng)的ClassLoader,通過這個(gè)ClassLoader來加載Bundle中的類。一旦明確了App中用到了哪些ClassLoder,獲取是易如反掌的

其次,通過ClassLoader來獲取ClassTable的對(duì)象的地址。通過Java層ClassLoader類的源碼可知,ClassLoader有一個(gè)成員變量classTable(7.0及以上版本),這個(gè)變量保存了Native層ClassTable對(duì)象的地址,我們可以通過反射獲取這個(gè)地址:

ClassLoader classLoader = XXX;
Field classTableField = ClassLoader.class.getDeclaredField("classTable");
classTableField.setAccessible(true);
long classTableAddr = classTableField.getLong(classLoader);

但在9.0的系統(tǒng)中成員變量classTable被加入了深灰名單,限制了直接反射,需要通過系統(tǒng)類進(jìn)行反射繞過此限制:

ClassLoader classLoader = XXX;
Method metaGetDeclaredField = Class.class.getDeclaredMethod("getDeclaredField", String.class);
Field classTableField = (Field) metaGetDeclaredField.invoke(ClassLoader.class, "classTable");
classTableField.setAccessible(true);
long classTableAddr = classTableField.getLong(classLoader);

至此,我們就獲得了所有的ClassTable對(duì)象的地址,里面保存了全部的類加載信息。

類名列表

通過閱讀源碼發(fā)現(xiàn)ClassTable有個(gè)方法可以通過類名查詢類是否被加載過(下節(jié)將詳細(xì)介紹),這樣我們只需要獲得所有類名的列表,再調(diào)用那個(gè)方法,即可以判斷類是否被加載過。

APK中的類名列表可以通過DexFile進(jìn)行獲取,如下:

List<String> classes = new ArrayList<>;
DexFile df = new DexFile(context.getPackageCodePath);
for (Enumeration<String> iter = df.entries; iter.hasMoreElements; ) {
classes.add(iter.nextElement);
}

同理,動(dòng)態(tài)加載的類也可以通過DexFile獲取;

類是否被加載

通過閱讀源碼發(fā)現(xiàn)class_table.cc中,ClassTable有個(gè)Lookup方法,傳入類名和類名的hash值,返回類對(duì)象的地址,如下:

mirror::Class* ClassTable::Lookup(const char* descriptor, size_t hash)

如果返回值為ptr,說明沒有加載過此類,否則,說明加載過。

mirror::Class* ClassTable::Lookup(const char* descriptor, size_t hash)

獲取此方法地址的方法:

  1. 加載so:class_table.cc在libart.so中,所有我們需要用dlopen加載libart.so獲得此so的handler。其實(shí)在加載前,libart.so在當(dāng)前進(jìn)程一定已經(jīng)被加載過了,此次加載只是為了獲得handler,并不耗時(shí);

  2. 符號(hào)表:通過readelf查詢Lookup的符號(hào):_ZN3art10ClassTable6LookupEPKcj;

  3. 方法指針:調(diào)用dlsym,傳入handler和符號(hào)表,即可以找到Lookup方法的地址;

注:從7.0系統(tǒng)開始,Google禁止了調(diào)用系統(tǒng)Native的API,這里我們通過/proc/self/maps找到libart.so的地址,將里面的符號(hào)表進(jìn)行拷貝,進(jìn)而繞過此限制;

至此,我們就可以通過調(diào)用ClassTable的Lookup方法,傳入類名和hash值,判斷類是否被加載過了。

總結(jié)

這樣,我們就能知道某一時(shí)刻有哪些類被加載過,對(duì)其上傳,進(jìn)行聚合和處理,再通過對(duì)比所有類名列表,就能得到代碼覆蓋率數(shù)據(jù)了。此方案不需要插樁,所以可以無損地采集覆蓋率。

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

新方案整體設(shè)計(jì)

上面提到的采集方案是整個(gè)方案的核心,除此之外還有上下游的配套流程,整體方案的設(shè)計(jì)如下圖:

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

1)APK分發(fā):通過構(gòu)建中心構(gòu)建出最新的APK,分發(fā)給用戶;

2)觸發(fā)采集:用戶安裝應(yīng)用,在使用過程中,將APP退后臺(tái)10s后,通過采樣率計(jì)算是否命中,若命中,則觸發(fā)代碼覆蓋率采集

3)配置下發(fā):在需要時(shí),可以通過配置中心下發(fā)配置來動(dòng)態(tài)調(diào)整功能開關(guān)、采樣率等配置;

4)數(shù)據(jù)采集:代碼覆蓋率采集中間件(SlimLady)統(tǒng)計(jì)出被加載的類,將已加載的類名保存在文件中,進(jìn)行壓縮,將壓縮后的數(shù)據(jù)傳給上傳中間件;

5)數(shù)據(jù)上傳:上傳中間件將數(shù)據(jù)上傳到云端;

6)數(shù)據(jù)下載:服務(wù)器定期對(duì)云端數(shù)據(jù)進(jìn)行下載;

7)類信息提供:服務(wù)器從構(gòu)建中心獲取類信息,包括所有類名列表和混淆文件;

8)數(shù)據(jù)解析:服務(wù)器按版本對(duì)代碼覆蓋率數(shù)據(jù)進(jìn)行解壓、反混淆、聚合統(tǒng)計(jì),聚合統(tǒng)計(jì)后的結(jié)果包含了被加載過的類及次數(shù),與所有類名列表進(jìn)行對(duì)比,即可以知道哪些類沒有被加載過,將結(jié)果保存至數(shù)據(jù)庫;

9)結(jié)果聚合:網(wǎng)頁端從數(shù)據(jù)庫讀取聚合結(jié)果,按模塊展示代碼覆蓋率、模塊熱度、模塊大小等信息。

和無用代碼說再見!阿里文娛無損代碼覆蓋率統(tǒng)計(jì)方案(阿里文娛app)

總結(jié)

本方案突破了傳統(tǒng)的插樁埋點(diǎn)統(tǒng)計(jì),動(dòng)態(tài)獲取虛擬機(jī)信息,無損地采集代碼覆蓋率。有了代碼覆蓋率數(shù)據(jù),能做的治理有很多,例如:下線無用代碼、模塊;瘦身或下線調(diào)用低頻、體積大的模塊;在集成階段添加代碼覆蓋率卡口等等。

相關(guān)新聞

聯(lián)系我們
聯(lián)系我們
公眾號(hào)
公眾號(hào)
在線咨詢
分享本頁
返回頂部
涟水县| 伊金霍洛旗| 怀化市| 治多县| 贡觉县| 莱州市| 开化县| 达拉特旗| 屏南县| 贡觉县| 九龙县| 洛宁县| 抚远县| 洛南县| 日土县| 清水河县| 裕民县| 宜州市| 海宁市| 鄂温| 定州市| 韶关市| 镇雄县| 河曲县| 高碑店市| 南澳县| 苍梧县| 商丘市| 独山县| 北碚区| 湄潭县| 安龙县| 乐业县| 遵义县| 禹州市| 姜堰市| 荥阳市| 府谷县| 铜山县| 新蔡县| 上饶县|