簡介
由于本人工作需要摘能,需要解決一些性能問題续崖,雖然有 Profiler
、Systrace
等工具团搞,但是無法實(shí)時(shí)監(jiān)控袜刷,多少有些不方便,于是計(jì)劃寫一個(gè)能實(shí)時(shí)監(jiān)控性能的小工具莺丑。經(jīng)過學(xué)習(xí)大佬們的文章著蟹,最終完成了這個(gè)開源的性能實(shí)時(shí)檢測庫。初步能達(dá)到預(yù)期效果梢莽,這里做個(gè)記錄萧豆,算是小結(jié)了。
開源庫的地址是:
幸苦各位能給個(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
開始我們熟悉的 measure
、layout
和 draw
邪锌。
我們可以通過 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í)行主要就是 hook
住 ThreadPoolExecutor
類的 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)系,就可以知道 Worker
和 Thread
的關(guān)聯(lián)了妇穴。這樣腾它,我們通過 ThreadPoolExecutor
和 Worker
的關(guān)聯(lián),以及 Worker
和 Thread
的關(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)用 BinderProxy
的 transact
方法飞袋,所以 BinderProxy
的 transact
方法是一個(gè)很好的切入點(diǎn)戳气。
本來我也是計(jì)劃通過 hook
住 BinderProxy
類的 transact
方法來做 IPC 的檢測的。但是 epic
庫在 hook
含有 Parcel
類型參數(shù)的方法的時(shí)候巧鸭,不穩(wěn)定瓶您,會(huì)有異常。由于暫時(shí)還沒能力解決這個(gè)異常纲仍,只能重新找切入點(diǎn)呀袱。最后發(fā)現(xiàn) AIDL
demo 生成的代碼里面,除了調(diào)用了 調(diào)用 BinderProxy
的 transact
方法外郑叠,還調(diào)用了 Parcel
的 readException
方法夜赵,于是決定 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)系刪除。