Android應用測速組件實現(xiàn)原理

Rabbit是目前我正在開發(fā)的一個框架,它主要用來提高App開發(fā)的效率和質(zhì)量,總體定位上偏向于一個APM框架梳杏。

統(tǒng)計應用冷啟動時長钱反、頁面渲染時長APM系統(tǒng)不可缺少一個功能盛垦。Rabbit中這個功能的實現(xiàn)主要參考自Android自動化頁面測速在美團的實踐,目前已經(jīng)完成下面功能點:

  1. Application.onCreate耗時統(tǒng)計
  2. 應用冷啟動耗時統(tǒng)計
  3. Activity.onCreate耗時統(tǒng)計
  4. Activity首次inflate耗時統(tǒng)計
  5. Activity首次渲染耗時
  6. 頁面網(wǎng)絡請求耗時監(jiān)控

具體統(tǒng)計時機如下圖:

測速組件名詞解釋.png

最終輸出的效果如下:

應用啟動測速


app_speed.png

頁面啟動測速


page_render_speed.png

網(wǎng)絡耗時測速


page_request_speed.png

使用方法

整個測速組件實現(xiàn)的思路主要是:利用Gradle插件在應用編譯時動態(tài)注入監(jiān)控代碼潮剪。因此使用時需要在應用的build.gradle中應用插件:

apply plugin: 'rabbit-tracer-transform'

為了支持網(wǎng)絡監(jiān)控功能,需要在OkHttpClient初始化時插入攔截器(目前只支持OkHttp的網(wǎng)絡監(jiān)控):

OkHttpClient.Builder().addInterceptor(Rabbit.getApiTracerInterceptor())

后面會考慮把Interceptor的初始化做成AOP的方式。

除此之外Rabbit的測速功能不需要其他的初始化代碼,接下來就大概過一下上面功能的實現(xiàn)原理:

應用onCreate耗時統(tǒng)計

實現(xiàn)思路:

  1. 編譯應用時在Application.attachBaseContext()開始Application.onCreate()結(jié)束方法中插入耗時統(tǒng)計代碼环肘。
  2. SDK收集測速數(shù)據(jù),然后展示集灌。

對于編譯時的字節(jié)碼插入本文就不做詳細實現(xiàn)分析悔雹,具體實現(xiàn)可以參考Rabbit源碼中的實現(xiàn),最終插入效果如下:

public class CustomApplication extends Application {

    protected void attachBaseContext(Context base) {
        AppStartTracer.recordApplicationCreateStart();
        super.attachBaseContext(base);
    }

    public void onCreate() {
        super.onCreate();
        Rabbit.init(this);
        AppStartTracer.recordApplicationCreateEnd();
    }
}

頁面渲染耗時統(tǒng)計

什么時候才算頁面渲染完成呢?

Rabbit定義ActivityContentView繪制完成就是頁面渲染完成,我們可以通過監(jiān)聽ViewGroup.dispatchDraw()來監(jiān)聽Activity.ContentView繪制完成欣喧。

具體實現(xiàn)思路是: 手動為Activity.setContentView()設(shè)置的View添加一層自定義父View腌零,用于計算繪制完成的時間

public class ActivitySpeedMonitor extends FrameLayout {

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        RabbitTracerEventNotifier.eventNotifier.activityDrawFinish(getContext(), System.currentTimeMillis());
    }

    public static void wrapperViewOnActivityCreateEnd(Activity activity) {
        FrameLayout contentView = activity.findViewById(android.R.id.content);
        ViewGroup contentViewParent = (ViewGroup) contentView.getParent();

        if (contentView != null && contentViewParent != null) {
            ActivitySpeedMonitor newParent = new ActivitySpeedMonitor(contentView.getContext());
            if (contentView.getLayoutParams() != null) {
                newParent.setLayoutParams(contentView.getLayoutParams());
            }
            contentViewParent.removeView(contentView);
            newParent.addView(contentView);
            contentViewParent.addView(newParent);
        }
    }
}

上面ActivitySpeedMonitor.wrapperViewOnActivityCreateEnd()代碼會在編譯時插入在Activity.onCreate方法中:

public class TransformTestActivity extends AppCompatActivity {

    protected void onCreate(Bundle savedInstanceState) {
        ActivitySpeedMonitor.activityCreateStart(this);
        super.onCreate(savedInstanceState);
        this.setContentView(2131296286);
        ActivitySpeedMonitor.wrapperViewOnActivityCreateEnd(this);
    }

}

Activity首次inflate耗時統(tǒng)計

我們知道ViewGroup.dispatchDraw()方法在ViewTree發(fā)生改變時就會調(diào)用,而一般第一次會導致dispatchDraw()被調(diào)用代碼是:

setContentView(R.layout.activity_transform_test);

因此RabbitActivity的第一dispatchDraw()方法完成時間當做Activity首次Inflate結(jié)束時間點唆阿。

其實這個時間的長短可以代表Activity的布局復雜度益涧。

Activity首次渲染耗時

這個耗時統(tǒng)計的時間結(jié)束點為: 頁面發(fā)起網(wǎng)絡請求拿到數(shù)據(jù),并完成頁面渲染

舉個例子驯鳖,比如你的應用首頁有3個接口饰躲,這3個接口的數(shù)據(jù)組成了整個首頁的UI, 首頁的渲染耗時就是3個接口完成請求,并且數(shù)據(jù)渲染完成臼隔。

Rabbit中對頁面的渲染耗時統(tǒng)計需要配置嘹裂,即配置一個頁面哪些接口完成才算頁面渲染完成, 具體配置約定為assest文件夾下提供rabbit_speed_monitor.json文件:

{
  "home_activity": "MainActivity",  
  "page_list": [
    {
      "page": "MainActivity",
      "api": [
        "xxx/api/getHomePageRecPosts",
        "xxx/api/getAvailablePreRegistrations",
        "xxxx/api/appHome"
      ]
    }
    ...
  ]
}

home_activity配置統(tǒng)計應用冷啟動耗時。

page_list配置需要統(tǒng)計渲染耗時的頁面摔握。

Rabbit會在指定的所有接口都完成并且ViewGroup.dispatchDraw()方法完成時記錄下這個時間點來作為渲染耗時:

RabbitAppSpeedMonitor.java

fun activityDrawFinish(activity: Any, drawFinishTime: Long) {
    val apiStatus = pageApiStatusInfo[currentPageName]
    if (apiStatus != null) {
        if (apiStatus.allApiRequestFinish()) { //所有請求已經(jīng)完成
            pageSpeedCanRecord = false //只統(tǒng)計一次
            pageSpeedInfo.fullDrawFinishTime = drawFinishTime
            RabbitDbStorageManager.save(pageSpeedInfo)
        }
    }   
}

如何統(tǒng)計接口完成呢寄狼?

網(wǎng)絡請求耗時監(jiān)控

也是利用RabbitAppSpeedInterceptor,不過這里監(jiān)控的網(wǎng)絡耗時時間并不是我們真正理解的網(wǎng)絡請求耗時,時間大概介于 : 網(wǎng)絡請求耗時 ~ 應用網(wǎng)絡處理耗時,具體實現(xiàn)核心代碼如下:

class RabbitAppSpeedInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val startTime = System.currentTimeMillis()
        val request = chain.request()
        val requestUrl = request.url().url().toString()
        val response = chain.proceed(request)

        if (!RabbitTracer.monitorRequest(requestUrl)) return response // 不需要監(jiān)控這個請求

        val costTime = System.currentTimeMillis() - startTime

        RabbitTracer.markRequestFinish(requestUrl, costTime)

        return response
    }
}

App冷啟動耗時統(tǒng)計

結(jié)合上面的敘述氨淌,Rabbit定義App冷啟動耗時HomeActivity渲染完成時 - Application.attachBaseContext()開始時泊愧。

對于HomeActivity可以通過rabbit_speed_monitor.json進行配置:

{
  "home_activity": "MainActivity",  
  "page_list": [
    ...
  ]
}

總結(jié)

應用測速組件的實現(xiàn)原理并不是很復雜,不過還是涉及到了很多點盛正。具體實現(xiàn)邏輯可以參考 : Rabbit

Rabbit中目前使用的統(tǒng)計時機可能并不是最合適的删咱,如果你知道更合適的統(tǒng)計時機,歡迎交流豪筝。

Rabbit功能的實現(xiàn)原理見:Rabbit實現(xiàn)原理剖析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痰滋,一起剝皮案震驚了整個濱河市摘能,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敲街,老刑警劉巖团搞,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異多艇,居然都是意外死亡逻恐,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門峻黍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來复隆,“玉大人,你說我怎么就攤上這事姆涩』杳” “怎么了?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵阵面,是天一觀的道長轻局。 經(jīng)常有香客問我,道長样刷,這世上最難降的妖魔是什么仑扑? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮置鼻,結(jié)果婚禮上镇饮,老公的妹妹穿的比我還像新娘。我一直安慰自己箕母,他們只是感情好储藐,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嘶是,像睡著了一般钙勃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上聂喇,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天辖源,我揣著相機與錄音,去河邊找鬼希太。 笑死克饶,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的誊辉。 我是一名探鬼主播矾湃,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼堕澄!你這毒婦竟也來了邀跃?” 一聲冷哼從身側(cè)響起霉咨,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坞嘀,沒想到半個月后躯护,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惊来,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡丽涩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了裁蚁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片矢渊。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖枉证,靈堂內(nèi)的尸體忽然破棺而出矮男,到底是詐尸還是另有隱情,我是刑警寧澤室谚,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布毡鉴,位于F島的核電站,受9級特大地震影響秒赤,放射性物質(zhì)發(fā)生泄漏猪瞬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一入篮、第九天 我趴在偏房一處隱蔽的房頂上張望陈瘦。 院中可真熱鬧,春花似錦潮售、人聲如沸痊项。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鞍泉。三九已至,卻和暖如春肮帐,著一層夾襖步出監(jiān)牢的瞬間塞弊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工泪姨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留游沿,地道東北人。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓肮砾,卻偏偏與公主長得像诀黍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子仗处,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355

推薦閱讀更多精彩內(nèi)容