前言
?卡頓是在用戶使用過(guò)程中很直觀的不良感受,主要分為由代碼、內(nèi)存不足等問(wèn)題引起的常規(guī)卡頓和ANR異常,我們可以利用線上和線下相結(jié)合的方式全覆蓋監(jiān)測(cè)卡頓點(diǎn),還要特別針對(duì)一些不易監(jiān)測(cè)到的難點(diǎn)進(jìn)行優(yōu)化。
1 手動(dòng)監(jiān)測(cè)
?手動(dòng)監(jiān)測(cè)方案可以查看Android性能優(yōu)化之啟動(dòng)優(yōu)化工具(TraceView、Systrace、Profiler),核心思想就是通過(guò)計(jì)算方法耗時(shí)、查看CPU的使用情況等去找到卡頓的地方并解決問(wèn)題。
2 自動(dòng)監(jiān)測(cè)
2.1 StrictMode
?如果開發(fā)者在UI線程中進(jìn)行了網(wǎng)絡(luò)操作或者IO操作,而這些操作可能會(huì)影響到App的性能,甚至出現(xiàn)ANR對(duì)話框。Android提供了一種運(yùn)行時(shí)檢測(cè)機(jī)制,主要用于檢測(cè)兩大問(wèn)題:
一、線程策略
自定義的耗時(shí)調(diào)用,detectCustomSlowCalls()
磁盤讀取操作,detectDiskReads
網(wǎng)絡(luò)請(qǐng)求,detectNetwork
二、虛擬機(jī)策略
Activity泄露,detectActivityLeaks()
Sqlite對(duì)象泄露。detectLeakedSqliteObjects
檢測(cè)實(shí)例數(shù)量,setClassIntancceLimit()
2.2 StrictMode實(shí)戰(zhàn)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
initStrictMode(true)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
writeToExternalStorageInMainThread()//模擬Closable對(duì)象未關(guān)閉
}
fun writeToExternalStorageInMainThread() {
val externalStorage: File = Environment.getExternalStorageDirectory()
val destFile = File(externalStorage, "hello.txt")
try {
val output: OutputStream = FileOutputStream(destFile, true)
output.write("I am testing io".toByteArray())
output.flush()
output.close()
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun initStrictMode(isDebug: Boolean) {
if (isDebug) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls()//API 11
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() //or .detectAll()
.penaltyLog()//在Logcat 中發(fā)音違規(guī)異常信息 or .penalDialog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.setClassInstanceLimit(ExplainAction::class.java, 1)
.detectActivityLeaks()
.detectLeakedClosableObjects()//監(jiān)測(cè)Closable對(duì)象
.penaltyLog()
.build()
)
}
}
}
#
?我們可以通過(guò)使用StrictMode進(jìn)行實(shí)例監(jiān)測(cè)、或者內(nèi)存泄露監(jiān)測(cè)等等,這些問(wèn)題都是可能引起卡頓的問(wèn)題點(diǎn)。上面運(yùn)行代碼之后可以在Logcat里看到錯(cuò)誤的堆棧信息:
3 Looper
?利用UI線程中,事件發(fā)生時(shí)Looper.loop會(huì)執(zhí)行dispatchMessage的原理,如果UI線程發(fā)生卡頓,即dispatchMessage發(fā)生了卡頓。因此我們可在分發(fā)消息前和分發(fā)消息后時(shí)間間隔與我們?cè)O(shè)置的閾值對(duì)比,實(shí)現(xiàn)卡頓自動(dòng)化監(jiān)測(cè)。
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next();
//可替換成自己的Printer
Printer logging = me.mLogging;
//1.消息分發(fā)前打印日志
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
//2.消息分發(fā)
msg.target.dispatchMessage(msg);
//3.消息分發(fā)后打印日志
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
msg.recycleUnchecked();
}
}
?記錄事件發(fā)生前后的事件,并與閾值進(jìn)行對(duì)比。
package com.example.myapplication
import android.os.Looper
import android.util.Log
class LogMonitor{
companion object{
var thresholdTime = 100;
private var isStart = true
private var mStartTime = 0L;
private var mEndTime = 0L;
fun recordTime(){
if (isStart){
mStartTime = System.currentTimeMillis();
isStart = false
}else{
mEndTime = System.currentTimeMillis()
isStart = true
if (isBlock(mEndTime)){
Log.d("LogMonotor","發(fā)生卡頓了。。。")
}
}
}
private fun isBlock(endTime:Long):Boolean{
return endTime- mStartTime> thresholdTime
}
}
}
?在Application的Oncreate中實(shí)現(xiàn)對(duì)Printer的替換。
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
mainLooper.setMessageLogging {
LogMonitor.recordTime()
}
}
}
?我們還可以使用第三方庫(kù)AndroidPerformanceMonitor進(jìn)行卡頓監(jiān)測(cè)。AndroidPerformanceMonitor就是利用了以上所述相同的原理。
?我們可以利用此方法頻繁采集堆棧信息,并將超過(guò)閾值的堆棧存入文件中上傳給服務(wù)器,在上傳前對(duì)文件進(jìn)行Hash排重處理。文件過(guò)大是首先可將文件拆分,然后使用Hash除余將相同的的堆棧放入一個(gè)文件進(jìn)行排重,最后合并文件上傳。
總結(jié)
?造成卡頓的主要原因有以下幾點(diǎn):
1.過(guò)于復(fù)雜的布局
原因:UI布局層次太深, 或是自定義控件的onDraw中有復(fù)雜運(yùn)算, CPU的相關(guān)運(yùn)算就可能大于16ms, 導(dǎo)致卡頓。
解決方案:可通過(guò)Android Studio的Layout Inspector去查看層級(jí),并改善層級(jí)深度,在開發(fā)中建議使用ConstrainLayout改善減少層級(jí)。
2.過(guò)度繪制
原因:像素被多次繪制。
解決方案:可在開發(fā)者模式中,打開顯示邊界布局,查看繪制顏色。
1.原色 – 沒(méi)有被過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上沒(méi)有繪制。
2.藍(lán)色 – 1次過(guò)度繪制– 這部分的像素點(diǎn)只在屏幕上繪制了一次。
3.綠色 – 2次過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了二次。
4.粉色 – 3次過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了三次。
5.紅色 – 4次過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了四次及以上。
2.1clipRect、clipRect
?使用canvas.clipRect后,繪制區(qū)域之外的繪制指令都不會(huì)被執(zhí)行,那些部分內(nèi)容在矩形區(qū)域內(nèi)的組件,仍然會(huì)得到繪制。
?canvas.quickreject()來(lái)判斷是否沒(méi)和某個(gè)矩形相交,從而跳過(guò)那些非矩形區(qū)域內(nèi)的繪制操作
3. 耗時(shí)事件
原因:在UI線程中執(zhí)行耗時(shí)事件,會(huì)導(dǎo)致UI線程loop卡頓。
解決方案:如果UI線程發(fā)生卡頓,即dispatchMessage發(fā)生了卡頓。我們可在dispatchMessage的原理分發(fā)消息前和分發(fā)消息后時(shí)間間隔與我們?cè)O(shè)置的閾值對(duì)比。
4. 頻繁GC
原因:短時(shí)間內(nèi)創(chuàng)建大量對(duì)象進(jìn)入新生區(qū),導(dǎo)致頻繁的GC。gc會(huì)大量占用ui線程和cpu資源,會(huì)導(dǎo)致app整體卡頓
解決方案:使用Profiler查看CPU抖動(dòng)位置,跟蹤內(nèi)存分配情況找到對(duì)象重復(fù)創(chuàng)建的對(duì)象。
5. 內(nèi)存不足
原因:低內(nèi)存會(huì)導(dǎo)致磁盤 IO 變多, 如果頻繁進(jìn)行磁盤 IO , 由于磁盤IO 很慢, 那么主線程會(huì)有很多進(jìn)程處于等 IO 的狀態(tài)。
解決方案:使用 SharedPerforence 時(shí)用 Apply而不是commit等等。
6 幀率與刷新率不匹配
原因:屏幕幀率和系統(tǒng)的 fps 不相符 , 那么有可能會(huì)導(dǎo)致畫面不是那么順暢. 比如使用 90 Hz 的屏幕搭配 60 fps 的動(dòng)畫
解決方案:使用以下代碼獲取屏幕刷新率,根據(jù)屏幕刷新率進(jìn)行動(dòng)畫計(jì)算。
Display display = getWindowManager().getDefaultDisplay();
float refreshRate = display.getRefreshRate();
?除以上問(wèn)題外,造成卡頓的原因還有GPU頻繁渲染、頻繁調(diào)用 buildDrawingCache、使用CPU渲染而不是使用GPU、WebView 性能不足等等問(wèn)題都會(huì)造成卡頓。