不會(huì) Android 性能優(yōu)化?你還差一個(gè)開源庫痰滋!

簡介

由于本人工作需要摘能,需要解決一些性能問題续崖,雖然有 ProfilerSystrace等工具团搞,但是無法實(shí)時(shí)監(jiān)控袜刷,多少有些不方便,于是計(jì)劃寫一個(gè)能實(shí)時(shí)監(jiān)控性能的小工具莺丑。經(jīng)過學(xué)習(xí)大佬們的文章著蟹,最終完成了這個(gè)開源的性能實(shí)時(shí)檢測庫。初步能達(dá)到預(yù)期效果梢莽,這里做個(gè)記錄萧豆,算是小結(jié)了。

開源庫的地址是:

github.com/XanderWang/…

幸苦各位能給個(gè)小小的 star 鼓勵(lì)下昏名。

Android性能優(yōu)化實(shí)戰(zhàn)案列解析:
Android開發(fā)—性能優(yōu)化大廠實(shí)戰(zhàn)教程(微信涮雷、今日頭條、抖音....)_嗶哩嗶哩_bilibili

這個(gè)性能檢測庫轻局,可以檢測以下問題:

  • UI 線程 block 檢測洪鸭。
  • App 的 FPS 檢測。
  • 線程的創(chuàng)建和啟動(dòng)監(jiān)控以及線程池的創(chuàng)建監(jiān)控仑扑。
  • IPC (進(jìn)程間通訊)監(jiān)控览爵。

同時(shí)還實(shí)現(xiàn)了以下功能:

  • 實(shí)時(shí)通過 logcat 打印檢測到的問題。
  • 保存檢測到的信息到文件镇饮。
  • 提供上報(bào)信息文件接口蜓竹。

接入指南

1 在 APP 工程目錄下面的 build.gradle 添加如下內(nèi)容。

dependencies {
  // 基礎(chǔ)依賴储藐,必須添加
  debugImplementation 'io.github.xanderwang:performance:0.3.1'
  releaseImplementation 'io.github.xanderwang:performance-noop:0.3.1'

  // hook 方案封裝俱济,必須添加
  debugImplementation 'io.github.xanderwang:hook:0.3.1'

  // 以下是 hook 方案選擇一個(gè)就好了。如果運(yùn)行報(bào)錯(cuò)钙勃,就換另外一個(gè)蛛碌,如果還是報(bào)錯(cuò),就提個(gè) issue
  // SandHook 方案辖源,推薦添加蔚携。如果運(yùn)行報(bào)錯(cuò),可以替換為 epic 庫同木。
  debugImplementation 'io.github.xanderwang:hook-sandhook:0.3.1'

  // epic 方法浮梢。如果運(yùn)行報(bào)錯(cuò)跛十,可以替換為 SandHook彤路。
  // debugImplementation 'io.github.xanderwang:hook-epic:0.3.1'
}

2 APP 工程的 Application 類新增類似如下初始化代碼。

Java 初始化示例

  private void initPERF(final Context context) {
    final PERF.LogFileUploader logFileUploader = new PERF.LogFileUploader() {
      @Override
      public boolean upload(File logFile) {
        return false;
      }
    };
    PERF.init(new PERF.Builder()
        .checkUI(true, 100) // 檢查 ui lock
        .checkIPC(true) // 檢查 ipc 調(diào)用
        .checkFps(true, 1000) // 檢查 fps
        .checkThread(true) // 檢查線程和線程池
        .globalTag("test_perf") // 全局 logcat tag ,方便過濾
        .cacheDirSupplier(new PERF.IssueSupplier<File>() {
          @Override
          public File get() {
            // issue 文件保存目錄
            return context.getCacheDir();
          }
        })
        .maxCacheSizeSupplier(new PERF.IssueSupplier<Integer>() {
          @Override
          public Integer get() {
            // issue 文件最大占用存儲(chǔ)空間
            return 10 * 1024 * 1024;
          }
        })
        .uploaderSupplier(new PERF.IssueSupplier<PERF.LogFileUploader>() {
          @Override
          public PERF.LogFileUploader get() {
            // issue 文件上傳接口
            return logFileUploader;
          }
        })
        .build());
  }

kotlin 示例

  private fun doUpload(log: File): Boolean {
    return false
  }

  private fun initPERF(context: Context) {
    PERF.init(PERF.Builder()
        .checkUI(true, 100)// 檢查 ui lock
        .checkIPC(true) // 檢查 ipc 調(diào)用
        .checkFps(true, 1000) // 檢查 fps
        .checkThread(true)// 檢查線程和線程池
        .globalTag("test_perf")// 全局 logcat tag ,方便過濾
        .cacheDirSupplier { context.cacheDir } // issue 文件保存目錄
        .maxCacheSizeSupplier { 10 * 1024 * 1024 } // issue 文件最大占用存儲(chǔ)空間
        .uploaderSupplier { // issue 文件的上傳接口實(shí)現(xiàn)
          PERF.LogFileUploader { logFile -> doUpload(logFile) }
        }
        .build()
    )
  }

主要更新記錄

  • 0.3.1 新增給 ImageView 設(shè)置比實(shí)際控件尺寸大的圖片檢測
  • 0.3.0 修改依賴庫發(fā)布方式為 MavenCentral
  • 0.2.0 線程耗時(shí)的監(jiān)控芥映,同時(shí)可以監(jiān)控線程優(yōu)先級(setPriority)的改變洲尊。
  • 0.1.12 線程創(chuàng)建的監(jiān)控远豺,加入 thread name 信息收集。同時(shí)接入 startup 庫做必要的初始化坞嘀,以及調(diào)整 multi dex 的時(shí)候躯护,配置文件找不到的問題。
  • 0.1.11 優(yōu)化 hook 方案的封裝丽涩,通過 SandHook 開源庫棺滞,可以按照 IPC 的耗時(shí)時(shí)間長短來檢測。
  • 0.1.10 FPS 的檢測時(shí)間間隔從默認(rèn) 2s 調(diào)整為 1s矢渊,同時(shí)支持自定義時(shí)間間隔继准。
  • 0.1.9 優(yōu)化線程池創(chuàng)建的監(jiān)控。
  • 0.1.8 初版發(fā)布矮男,完成基本的功能移必。

不建議直接在線上使用這個(gè)庫,在編寫這個(gè)庫毡鉴,測試 hook 的時(shí)候崔泵,在不同的機(jī)器和 rom 上,會(huì)有不同的問題猪瞬,這里建議先只在線下自測使用這個(gè)檢測庫憎瘸。

原理介紹

UI 線程 block 檢測原理

主要參考了 AndroidPerformanceMonitor 庫的思路,對 UI 線程的 Looper 里面處理 Message 的過程進(jìn)行監(jiān)控陈瘦。

具體做法是含思,在 Looper 開始處理 Message 前,在異步線程開啟一個(gè)延時(shí)任務(wù)甘晤,用于后續(xù)收集信息含潘。如果這個(gè) Message 在指定的時(shí)間段內(nèi)完成了處理,那么在這個(gè) Message 被處理完后线婚,就取消之前的延時(shí)任務(wù)遏弱,說明 UI 線程沒有 block 。如果在指定的時(shí)間段內(nèi)沒有完成任務(wù)塞弊,說明 UI 線程有 block 漱逸。此時(shí),異步線程可以執(zhí)行剛才的延時(shí)任務(wù)游沿。如果我們在這個(gè)延時(shí)任務(wù)里面打印 UI 線程的方法調(diào)用棧饰抒,就可以知道 UI 線程在做什么了。這個(gè)就是 UI 線程 block 檢測的基本原理诀黍。

但是這個(gè)方案有一個(gè)缺點(diǎn)袋坑,就是無法處理 InputManager 的輸入事件,比如 TV 端的遙控按鍵事件眯勾。通過對按鍵事件的調(diào)用方法鏈進(jìn)行分析枣宫,發(fā)現(xiàn)最終每個(gè)按鍵事件都調(diào)用了 DecorView 類的 dispatchKeyEvent 方法婆誓,而非 Looper 的處理 Message 流程。所以 AndroidPerformanceMonitor 庫是無法準(zhǔn)確監(jiān)控 TV 端應(yīng)用 UI block 的情況也颤。針對 TV 端應(yīng)用按鍵處理洋幻,需要找到一個(gè)新的切入點(diǎn),這個(gè)切入點(diǎn)就是剛剛的 DecorView 類的 dispatchKeyEvent 方法翅娶。

那如何介入 DecorView 類的 dispatchKeyEvent 方法呢文留?我們可以通過 epic 庫來 hook 這個(gè)方法的調(diào)用。hook 成功后竭沫,我們可以在 DecorView 類的 dispatchKeyEvent 方法調(diào)用前后都接收到一個(gè)回調(diào)方法厂庇,在 dispatchKeyEvent 方法調(diào)用前我們可以在異步線程執(zhí)行一個(gè)延時(shí)任務(wù),在 dispatchKeyEvent 方法調(diào)用后输吏,取消這個(gè)延時(shí)任務(wù)权旷。如果 dispatchKeyEvent 方法耗時(shí)時(shí)間小于指定的時(shí)間閾值,延時(shí)任務(wù)在執(zhí)行前被取消贯溅,可以認(rèn)為沒有 block 拄氯,此時(shí)移除了延時(shí)任務(wù)。如果 dispatchKeyEvent 方法耗時(shí)時(shí)間大于指定的時(shí)間閾值說明此時(shí) UI 線程是有 block 的它浅。此時(shí)译柏,異步線程可以執(zhí)行這個(gè)延時(shí)任務(wù)來收集必要的信息。

以上就是修改后的 UI 線程 block 的檢測原理了姐霍,目前做的還比較粗糙鄙麦,后續(xù)計(jì)劃考慮參考 AndroidPerformanceMonitor 打印 CPU 、內(nèi)存等更多的信息镊折。

最終終端 log 打印效果如下:

com.xander.performace.demo W/demo_Issue: =================================================
    type: UI BLOCK
    msg: UI BLOCK
    create time: 2021-01-13 11:24:41
    trace:
        java.lang.Thread.sleep(Thread.java:-2)
        java.lang.Thread.sleep(Thread.java:442)
        java.lang.Thread.sleep(Thread.java:358)
        com.xander.performance.demo.MainActivity.testANR(MainActivity.kt:49)
        java.lang.reflect.Method.invoke(Method.java:-2)
        androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
        android.view.View.performClick(View.java:7496)
        android.view.View.performClickInternal(View.java:7473)
        android.view.View.access$3600(View.java:831)
        android.view.View$PerformClick.run(View.java:28641)
        android.os.Handler.handleCallback(Handler.java:938)
        android.os.Handler.dispatchMessage(Handler.java:99)
        android.os.Looper.loop(Looper.java:236)
        android.app.ActivityThread.main(ActivityThread.java:7876)
        java.lang.reflect.Method.invoke(Method.java:-2)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

FPS 檢測的原理

FPS 檢測的原理胯府,利用了 Android 的屏幕繪制原理。這里簡單說下 Android 的屏幕繪制原理恨胚。

系統(tǒng)每隔 16 ms 就會(huì)發(fā)送一個(gè) VSync 信號骂因。 如果應(yīng)用注冊了這個(gè) VSync 信號,就會(huì)在 VSync信號到來的時(shí)候赃泡,收到回調(diào)寒波,從而開始準(zhǔn)備繪制。如果準(zhǔn)備順利升熊,也就是 CPU 準(zhǔn)備數(shù)據(jù)俄烁、GPU 柵格化等,如果這些任務(wù)在 16 ms 之內(nèi)完成级野,那么下一個(gè) VSync 信號到來前就可以繪制這一幀界面了页屠。就沒有掉幀,界面很流暢。如果在 16 ms 內(nèi)沒準(zhǔn)備好卷中,可能就需要更多的時(shí)間這個(gè)畫面才能顯示出來矛双,在這種情況下就發(fā)生了丟幀渊抽,如果丟幀很多就卡頓了蟆豫。

檢測 FPS 的原理其實(shí)挺簡單的,就是通過一段時(shí)間內(nèi)懒闷,比如 1s十减,統(tǒng)計(jì)繪制了多少個(gè)畫面,就可以計(jì)算出 FPS 了愤估。那如何知道應(yīng)用 1s 內(nèi)繪制了多少個(gè)界面呢帮辟?這個(gè)就要靠 VSync 信號監(jiān)聽了。

在開始準(zhǔn)備繪制前玩焰,往 UI 線程的 MessageQueue 里面放一個(gè)同步屏障由驹,這樣 UI 線程就只會(huì)處理異步消息,直到同步屏障被移除昔园。刷新前蔓榄,應(yīng)用會(huì)注冊一個(gè) VSync 信號監(jiān)聽,當(dāng) VSync 信號到達(dá)的時(shí)候默刚,系統(tǒng)會(huì)通知應(yīng)用甥郑,讓應(yīng)用會(huì)給 UI 線程的 MessageQueue 里面放一個(gè)異步 Message 。由于之前 MessageQueue 里有了一個(gè)同步屏障荤西,所以后續(xù) UI 線程會(huì)優(yōu)先處理這個(gè)異步 Message 澜搅。這個(gè)異步 Message 做的事情就是從 ViewRootImpl 開始我們熟悉的 measurelayoutdraw 邪锌。

我們可以通過 Choreographer 注冊 VSync 信號監(jiān)聽勉躺。16ms 后,我們收到了 VSync 的信號觅丰,給 MessageQueue 里面放一個(gè)同步消息赂蕴,我們不做特別處理,只是做一個(gè)計(jì)數(shù)舶胀,然后監(jiān)聽下一次的 VSync 信號概说,這樣,我們就可以知道 1s 內(nèi)我們監(jiān)聽到了多少個(gè) VSync 信號嚣伐,就可以得出幀率糖赔。

為什么監(jiān)聽到的 VSync 信號數(shù)量就是幀率呢?

由于 Looper 處理 Message 是串行的轩端,就是一次只處理一個(gè) Message 放典,處理完了這個(gè) Message 才會(huì)處理下一個(gè) Message 。而繪制的時(shí)候,繪制任務(wù) Message 是異步消息奋构,會(huì)優(yōu)先執(zhí)行壳影,繪制任務(wù) Message 執(zhí)行完成后,就會(huì)執(zhí)行上面說的 VSync 信號計(jì)數(shù)的任務(wù)弥臼。如果忽略計(jì)數(shù)任務(wù)的耗時(shí)宴咧,那么最后統(tǒng)計(jì)到的 VSync 信號數(shù)量可以粗略認(rèn)為是某段時(shí)間內(nèi)繪制的幀數(shù)。然后就可以通過這段時(shí)間的長度和 VSync 信號數(shù)量來計(jì)算幀率了径缅。

最終終端 log 打印效果如下:

com.xander.performace.demo W/demo_FPSTool: APP FPS is: 54 Hz
com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz
com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz

線程的創(chuàng)建和啟動(dòng)監(jiān)控以及線程池的創(chuàng)建監(jiān)控

線程和線程池的監(jiān)控掺栅,主要是監(jiān)控線程和線程池在哪里創(chuàng)建和執(zhí)行的,如果我們可以知道這些信息纳猪,我們就可以比較清楚線程和線程池的創(chuàng)建和啟動(dòng)時(shí)機(jī)是否合理氧卧。從而得出優(yōu)化方案。

一個(gè)比較容易想到的方法就是氏堤,應(yīng)用代碼里面的所有線程和線程池繼承同一個(gè)線程基類和線程池基類沙绝。然后在構(gòu)造函數(shù)和啟動(dòng)函數(shù)里面打印方法調(diào)用棧,這樣我們就知道哪里創(chuàng)建和執(zhí)行了線程或者線程池鼠锈。

讓應(yīng)用所有的線程和線程池繼承同一個(gè)基類闪檬,可以通過編譯插件來實(shí)現(xiàn),定制一個(gè)特殊的 Transform 脚祟,通過 ASM 編輯生成的字節(jié)碼來改變繼承關(guān)系谬以。但是,這個(gè)方法有一定的上手難度由桌,不太適合新手为黎。

除了這個(gè)方法,我們還有另外一種方法行您,就是 hook 铭乾。通過 hook 線程或者線程池的構(gòu)造方法和啟動(dòng)方法,我們就可以在線程或者線程池的構(gòu)造方法和啟動(dòng)方法的前后做一些切片處理娃循,比如打印當(dāng)前方法調(diào)用棧等炕檩。這個(gè)也就是線程和線程池監(jiān)控的基本原理。

線程池的監(jiān)控沒有太大難度捌斧,一般都是 ThreadPoolExecutor 的子類笛质,所以我們 hook 一下 ThreadPoolExecutor 的構(gòu)造方法就可以監(jiān)控線程池的創(chuàng)建了。線程池的執(zhí)行主要就是 hookThreadPoolExecutor 類的 execute 方法捞蚂。

線程的創(chuàng)建和執(zhí)行的監(jiān)控方法就稍微要費(fèi)些腦筋了妇押,因?yàn)榫€程池里面會(huì)創(chuàng)建線程,所以這個(gè)線程的創(chuàng)建和執(zhí)行應(yīng)該和線程池綁定的姓迅。需要找到線程和線程池的聯(lián)系敲霍,之前看到一個(gè)庫俊马,好像是通過線程和線程池的 ThreadGroup 來建立關(guān)聯(lián)的,本來我也計(jì)劃按照這個(gè)關(guān)系來寫代碼的肩杈,但是我發(fā)現(xiàn)柴我,我們有的小伙伴寫的線程池的 ThreadFactory 里面創(chuàng)建線程并沒有傳入ThreadGroup ,這個(gè)就尷尬了扩然,就建立不了聯(lián)系了艘儒。經(jīng)過查閱相關(guān)源碼發(fā)現(xiàn)了一個(gè)關(guān)鍵的類与学,ThreadPoolExecutor 的內(nèi)部類Worker 彤悔,由于這個(gè)類是內(nèi)部類嘉抓,所以這個(gè)類實(shí)際的構(gòu)造方法里面會(huì)傳入一個(gè)外部類的實(shí)例,也就是 ThreadPoolExecutor 實(shí)例卵佛。同時(shí)植捎, Worker 這個(gè)類還是一個(gè) Runnable 實(shí)現(xiàn),在 Worker 類通過 ThreadFactory 創(chuàng)建線程的時(shí)候暑椰,會(huì)把自己作為一個(gè) Runnable 傳給 Thread 所以低滩,我們通過這個(gè)關(guān)系,就可以知道 WorkerThread 的關(guān)聯(lián)了妇穴。這樣腾它,我們通過 ThreadPoolExecutorWorker 的關(guān)聯(lián),以及 WorkerThread 的關(guān)聯(lián),就可以得到 ThreadPoolExecutor 和它創(chuàng)建的 Thread 的關(guān)聯(lián)了世剖。這個(gè)也就是線程和線程池的監(jiān)控原理了旁瘫。

最終終端 log 打印效果如下:

com.xander.performace.demo W/demo_Issue: =================================================
    type: THREAD
    msg: THREAD POOL CREATE
    create time: 2021-01-13 11:23:47
    create trace:
        com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39)
        com.xander.performance.ThreadTool$ThreadPoolExecutorConstructorHook.afterHookedMethod(ThreadTool.java:158)
        de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:265)
        me.weishu.epic.art.entry.Entry64.onHookObject(Entry64.java:64)
        me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:239)
        java.util.concurrent.Executors.newSingleThreadExecutor(Executors.java:179)
        com.xander.performance.demo.MainActivity.testThreadPool(MainActivity.kt:38)
        java.lang.reflect.Method.invoke(Method.java:-2)
        androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
        android.view.View.performClick(View.java:7496)
        android.view.View.performClickInternal(View.java:7473)
        android.view.View.access$3600(View.java:831)
        android.view.View$PerformClick.run(View.java:28641)
        android.os.Handler.handleCallback(Handler.java:938)
        android.os.Handler.dispatchMessage(Handler.java:99)
        android.os.Looper.loop(Looper.java:236)
        android.app.ActivityThread.main(ActivityThread.java:7876)
        java.lang.reflect.Method.invoke(Method.java:-2)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

IPC(進(jìn)程間通訊)監(jiān)控的原理

進(jìn)程間通訊的具體原理宁仔,也就是 Binder 機(jī)制翎苫,這里不做詳細(xì)的說明阻逮,也不是這個(gè)框架庫的原理事哭。

檢測進(jìn)程間通訊的方法和前面檢測線程的方法類似鳍咱,就是找到所有的進(jìn)程間通訊的方法的共同點(diǎn)涡戳,然后對共同點(diǎn)做一些修改或者說切片恍涂,讓應(yīng)用在進(jìn)行進(jìn)程間通訊的時(shí)候,打印一下調(diào)用棧内贮,然后繼續(xù)做原來的事情竞端。就達(dá)到了 IPC 監(jiān)控的目的。

那如何找到共同點(diǎn),或者說切片贱勃,就是本節(jié)的重點(diǎn)。

進(jìn)程間通訊離不開 Binder ,需要從 Binder 入手。

寫一個(gè) AIDL demo 后發(fā)現(xiàn)欧聘,自動(dòng)生成的代碼里面,接口 A 繼承自 IInterface 接口研叫,然后接口里面有個(gè)內(nèi)部抽象類 Stub 類,繼承自 Binder 嚷那,同時(shí)實(shí)現(xiàn)了接口 A 索绪。這個(gè) Stub 類里面還有一個(gè)內(nèi)部類 Proxy 凳寺,實(shí)現(xiàn)了接口 A 逆趋,并持有一個(gè) IBinder 實(shí)例。

我們在使用 AIDL 的時(shí)候脑慧,會(huì)用到 Stub 類的 asInterFace 的方法激况,這個(gè)方法會(huì)新建一個(gè) Proxy 實(shí)例骚露,并給這個(gè) Proxy 實(shí)例傳入 IBinder , 或者如果傳入的 IBinder 實(shí)例如果是接口 A 的話,就強(qiáng)制轉(zhuǎn)化為接口 A 實(shí)例。一般而言弛针,這個(gè) IBinder 實(shí)例是 ServiceConnection 的回調(diào)方法里面的實(shí)例,是 BinderProxy 的實(shí)例恤左。所以 Stub 類的 asInterFace 一般會(huì)創(chuàng)建一個(gè) Proxy 實(shí)例贴唇,查看這個(gè) Proxy 接口的實(shí)現(xiàn)方法,發(fā)現(xiàn)最終都會(huì)調(diào)用 BinderProxytransact 方法飞袋,所以 BinderProxytransact 方法是一個(gè)很好的切入點(diǎn)戳气。

本來我也是計(jì)劃通過 hookBinderProxy 類的 transact 方法來做 IPC 的檢測的。但是 epic 庫在 hook 含有 Parcel 類型參數(shù)的方法的時(shí)候巧鸭,不穩(wěn)定瓶您,會(huì)有異常。由于暫時(shí)還沒能力解決這個(gè)異常纲仍,只能重新找切入點(diǎn)呀袱。最后發(fā)現(xiàn) AIDL demo 生成的代碼里面,除了調(diào)用了 調(diào)用 BinderProxytransact 方法外郑叠,還調(diào)用了 ParcelreadException 方法夜赵,于是決定 hook 這個(gè)方法來切入 IPC 調(diào)用流程,從而達(dá)到 IPC 監(jiān)控的目的乡革。

最終終端 log 打印效果如下:

com.xander.performace.demo W/demo_Issue: =================================================
    type: IPC
    msg: IPC
    create time: 2021-01-13 11:25:04
    trace:
        com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39)
        com.xander.performance.IPCTool$ParcelReadExceptionHook.beforeHookedMethod(IPCTool.java:96)
        de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:229)
        me.weishu.epic.art.entry.Entry64.onHookVoid(Entry64.java:68)
        me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:220)
        me.weishu.epic.art.entry.Entry64.voidBridge(Entry64.java:82)
        android.app.IActivityManager$Stub$Proxy.getRunningAppProcesses(IActivityManager.java:7285)
        android.app.ActivityManager.getRunningAppProcesses(ActivityManager.java:3684)
        com.xander.performance.demo.MainActivity.testIPC(MainActivity.kt:55)
        java.lang.reflect.Method.invoke(Method.java:-2)
        androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
        android.view.View.performClick(View.java:7496)
        android.view.View.performClickInternal(View.java:7473)
        android.view.View.access$3600(View.java:831)
        android.view.View$PerformClick.run(View.java:28641)
        android.os.Handler.handleCallback(Handler.java:938)
        android.os.Handler.dispatchMessage(Handler.java:99)
        android.os.Looper.loop(Looper.java:236)
        android.app.ActivityThread.main(ActivityThread.java:7876)
        java.lang.reflect.Method.invoke(Method.java:-2)
        com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
        com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

大廠性能優(yōu)化實(shí)戰(zhàn)案列解析

Android開發(fā)—性能優(yōu)化大廠實(shí)戰(zhàn)教程(微信寇僧、今日頭條、抖音....)_嗶哩嗶哩_bilibili

本文轉(zhuǎn)自 https://juejin.cn/post/6916531888576266254沸版,如有侵權(quán)嘁傀,請聯(lián)系刪除。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末推穷,一起剝皮案震驚了整個(gè)濱河市心包,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌馒铃,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痕惋,死亡現(xiàn)場離奇詭異区宇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)值戳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門议谷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人堕虹,你說我怎么就攤上這事卧晓》沂祝” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵逼裆,是天一觀的道長郁稍。 經(jīng)常有香客問我,道長胜宇,這世上最難降的妖魔是什么耀怜? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮桐愉,結(jié)果婚禮上财破,老公的妹妹穿的比我還像新娘。我一直安慰自己从诲,他們只是感情好左痢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著系洛,像睡著了一般俊性。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碎罚,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天磅废,我揣著相機(jī)與錄音矢炼,去河邊找鬼缠诅。 笑死玩荠,一個(gè)胖子當(dāng)著我的面吹牛慕趴,可吹牛的內(nèi)容都是我干的委可。 我是一名探鬼主播嚼蚀,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼血公,長吁一口氣:“原來是場噩夢啊……” “哼荷荤!你這毒婦竟也來了玫鸟?” 一聲冷哼從身側(cè)響起导绷,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎屎飘,沒想到半個(gè)月后妥曲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钦购,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年檐盟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片押桃。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡葵萎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情羡忘,我是刑警寧澤谎痢,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站卷雕,受9級特大地震影響节猿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜爽蝴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一沐批、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蝎亚,春花似錦九孩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梅惯,卻和暖如春宪拥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背铣减。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工她君, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人葫哗。 一個(gè)月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓缔刹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親劣针。 傳聞我的和親對象是個(gè)殘疾皇子校镐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

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