????????大多數(shù)用戶感知到的卡頓等性能問(wèn)題主要原因都是因?yàn)殇秩拘阅堋?/p>
????????Android 系統(tǒng)每隔大概16.6毫秒(1000ms / 60)發(fā)出VSYNC信號(hào)耸采,觸發(fā)對(duì)UI進(jìn)行渲染,如果每次渲染成功麦备,這樣就能夠達(dá)到畫面所需要的的60fps ,要做到實(shí)現(xiàn)60fps,就意味著程序渲染工作需要在16ms之內(nèi)完成。
認(rèn)識(shí)卡頓現(xiàn)象
12fps: 類似于手動(dòng)快速翻書的幀率;
24fps:人肉眼感知的連續(xù)線的運(yùn)動(dòng)效果凛篙;通常是電影膠卷使用的幀率
60fps:是人眼與大腦之間的協(xié)作無(wú)感知的畫面更新
????????CPU: 負(fù)責(zé)measure黍匾、layout、record呛梆、execute的計(jì)算操作
????????GPU:負(fù)責(zé)將資源組件拆分到不同的像素上顯示锐涯;
????????display: 負(fù)責(zé)將Frame buffer中的數(shù)據(jù)顯示出來(lái)
????????當(dāng)系統(tǒng)每隔16ms發(fā)出VSYNC時(shí),如果CPU ,GPU都已經(jīng)完成了相關(guān)的操作填物,那么display繪制就會(huì)很順暢纹腌,但是如果CPU/GPU還在生產(chǎn)幀數(shù)據(jù),從幀緩存中讀取出來(lái)的數(shù)據(jù)就是之前的滞磺,這樣在兩個(gè)刷新周期之間顯示了同一幀的數(shù)據(jù)升薯,這就是我們通常說(shuō)的發(fā)生了<u>丟幀現(xiàn)象</u>
監(jiān)控卡頓的方法:
????????如果需要準(zhǔn)確分析卡頓發(fā)生在哪一個(gè)函數(shù),資源占用情況如何等击困,我們介紹兩種比較主流的監(jiān)控方案:
1. 利用主線程的Looper打印日志 (BlockCanary的原理)
先來(lái)看看Looper.java的源碼
//此接口的實(shí)現(xiàn)類涎劈,系統(tǒng)給我們提供了一個(gè)LogPrinter
public interface Printer {
/**
* Write a line of text to the output. There is no need to terminate
* the given string with a newline.
*/
void println(String x);
}
。阅茶。蛛枚。。脸哀。蹦浦。。企蹭。白筹。。谅摄。。系馆。送漠。。由蘑。闽寡。。尼酿。爷狈。。裳擎。涎永。。。羡微。谷饿。。妈倔。博投。。盯蝴。毅哗。。捧挺。虑绵。。松忍。蒸殿。。鸣峭。宏所。。摊溶。爬骤。。莫换。霞玄。。拉岁。坷剧。。喊暖。馒稍。兽愤。县匠。肥缔。。巩掺。偏序。。胖替。研儒。豫缨。。殉摔。州胳。。逸月。栓撞。。
/**
* Control logging of messages as they are processed by this Looper. If
* enabled, a log message will be written to <var>printer</var>
* at the beginning and ending of each message dispatch, identifying the
* target Handler and message contents.
*
* @param printer A Printer object that will receive log messages, or
* null to disable message logging.
*/
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
碗硬。瓤湘。。恩尾。弛说。。翰意。木人。。冀偶。醒第。。进鸠。稠曼。。客年。霞幅。。量瓜。司恳。。绍傲。抵赢。。唧取。。划提。枫弟。。鹏往。淡诗。骇塘。。韩容。款违。。群凶。插爹。。请梢。赠尾。。毅弧。气嫁。。够坐。寸宵。。元咙。梯影。。蛾坯。光酣。。脉课。救军。。倘零。
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
//此處如果logging不為空唱遭,那么他會(huì)調(diào)用Printer的println()打印一些信息,而Printer是一個(gè)接口呈驶,就需要我們自己去實(shí)現(xiàn)這個(gè)接口拷泽,并完成我們自己的Println()函數(shù)的方法體,打印我們自己需要的信息
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
//此處打印結(jié)束
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
????????如果我們想用主線程的Looper來(lái)打印相關(guān)的日志信息袖瞻,那么我們就需要給Looper實(shí)例提供一個(gè)Printer的實(shí)例司致,而Printer是一個(gè)接口,那么我們就來(lái)實(shí)現(xiàn)一個(gè)Printer的實(shí)現(xiàn)類:LogMonitor.java
public class LogMonitor implements Printer {
private StackSampler mStackSampler;
private boolean mPrintingStarted = false;
private long mStartTimestamp;
// 卡頓閾值
private long mBlockThresholdMillis = 3000;
//采樣頻率
private long mSampleInterval = 1000;
private Handler mLogHandler;
public LogMonitor() {
mStackSampler = new StackSampler(mSampleInterval);
HandlerThread handlerThread = new HandlerThread("block-canary-io");
handlerThread.start();
mLogHandler = new Handler(handlerThread.getLooper());
}
@Override
public void println(String x) {
//從if到else會(huì)執(zhí)行 dispatchMessage聋迎,如果執(zhí)行耗時(shí)超過(guò)閾值脂矫,輸出卡頓信息
if (!mPrintingStarted) {
//記錄開始時(shí)間
mStartTimestamp = System.currentTimeMillis();
mPrintingStarted = true;
mStackSampler.startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
//出現(xiàn)卡頓
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
mStackSampler.stopDump();
}
}
private void notifyBlockEvent(final long endTime) {
mLogHandler.post(new Runnable() {
@Override
public void run() {
//獲得卡頓時(shí) 主線程堆棧
List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
for (String stack : stacks) {
Log.e("block-canary", stack);
}
}
});
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
}
????????再來(lái)一個(gè)堆棧信息采集器:
public class StackSampler {
public static final String SEPARATOR = "\r\n";
public static final SimpleDateFormat TIME_FORMATTER =
new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
private Handler mHandler;
private Map<Long, String> mStackMap = new LinkedHashMap<>();
private int mMaxCount = 100;
private long mSampleInterval;
//是否需要采樣
protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
public StackSampler(long sampleInterval) {
mSampleInterval = sampleInterval;
HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
/**
* 開始采樣 執(zhí)行堆棧
*/
public void startDump() {
//避免重復(fù)開始
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, mSampleInterval);
}
public void stopDump() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
mHandler.removeCallbacks(mRunnable);
}
public List<String> getStacks(long startTime, long endTime) {
ArrayList<String> result = new ArrayList<>();
synchronized (mStackMap) {
for (Long entryTime : mStackMap.keySet()) {
if (startTime < entryTime && entryTime < endTime) {
result.add(TIME_FORMATTER.format(entryTime)
+ SEPARATOR
+ SEPARATOR
+ mStackMap.get(entryTime));
}
}
}
return result;
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString()).append("\n");
}
synchronized (mStackMap) {
//最多保存100條堆棧信息
if (mStackMap.size() == mMaxCount) {
mStackMap.remove(mStackMap.keySet().iterator().next());
}
mStackMap.put(System.currentTimeMillis(), sb.toString());
}
if (mShouldSample.get()) {
mHandler.postDelayed(mRunnable, mSampleInterval);
}
}
};
}
????????再來(lái)一個(gè)靜態(tài)方法調(diào)用使用:
public class BlockCanary {
public static void install() {
LogMonitor logMonitor = new LogMonitor();
Looper.getMainLooper().setMessageLogging(logMonitor);
}
}
????????來(lái)看一下BlockCanary的工作流程圖
????????采集的信息截圖,當(dāng)然你也可以修改成保存到文件中去:
2. 使用Choreographer.FrameCallback監(jiān)控
????????當(dāng)每一幀被渲染時(shí)會(huì)觸發(fā)此接口的回調(diào)霉晕,并帶著底層VSYNC信息到達(dá)的時(shí)間戳庭再。
/**
* Implement this interface to receive a callback when a new display frame is
* being rendered. The callback is invoked on the {@link Looper} thread to
* which the {@link Choreographer} is attached.
*/
public interface FrameCallback {
/**
* Called when a new display frame is being rendered.
* <p>
* This method provides the time in nanoseconds when the frame started being rendered.
* The frame time provides a stable time base for synchronizing animations
* and drawing. It should be used instead of {@link SystemClock#uptimeMillis()}
* or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame
* time helps to reduce inter-frame jitter because the frame time is fixed at the time
* the frame was scheduled to start, regardless of when the animations or drawing
* callback actually runs. All callbacks that run as part of rendering a frame will
* observe the same frame time so using the frame time also helps to synchronize effects
* that are performed by different callbacks.
* </p><p>
* Please note that the framework already takes care to process animations and
* drawing using the frame time as a stable time base. Most applications should
* not need to use the frame time information directly.
* </p>
*
* @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
* in the {@link System#nanoTime()} timebase. Divide this value by {@code 1000000}
* to convert it to the {@link SystemClock#uptimeMillis()} time base.
*/
public void doFrame(long frameTimeNanos);
}
????????所以我們就自定義一個(gè)該接口的實(shí)現(xiàn):
public class ChoreographerHelper {
public static void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
//上次回調(diào)時(shí)間
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (diff > 16.6f) {
//掉幀數(shù)
int droppedCount = (int) (diff / 16.6);
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
}
????????上面的實(shí)現(xiàn)可以幫助我們獲取實(shí)時(shí)的幀數(shù)和掉幀數(shù)捞奕,如果掉的比較多時(shí)(有可能卡頓),我們可以用第一種監(jiān)控方式里面的堆棧信息采集器來(lái)進(jìn)行采集拄轻,同時(shí)也可以輸出采集的信息颅围。
???????? 以上兩種監(jiān)控卡頓的方式僅供參考,感謝騰訊-享學(xué)課堂的老師的資料恨搓,
常見(jiàn)卡頓的處理:
-
嵌套太深院促,過(guò)于復(fù)雜的布局
????????系統(tǒng)對(duì)于視圖的繪制過(guò)程包括measure ,layout ,draw三個(gè)過(guò)程,如果嵌套的太深奶卓,自然對(duì)每個(gè)視圖都進(jìn)行三步測(cè)繪過(guò)程就會(huì)需要更多的時(shí)間來(lái)完成一疯,這樣就會(huì)造成卡頓等現(xiàn)象。
解決方案:
????????參考《Android性能優(yōu)化之布局優(yōu)化》
-
過(guò)度的繪制(OverDraw)
????????如果在屏幕上的某一個(gè)像素點(diǎn)有多次繪制夺姑,就是過(guò)度繪制了墩邀,較為常見(jiàn)的就是重復(fù)的繪制背景繪制一些不可見(jiàn)的UI元素。
????????我們可以在我們?cè)O(shè)備的“系統(tǒng)設(shè)置”->"開發(fā)者選項(xiàng)"->"調(diào)試GPU過(guò)度繪制"中開啟調(diào)試盏浙,此時(shí)你的設(shè)備界面可能出現(xiàn)五種顏色標(biāo)識(shí):
????????????????原色: 沒(méi)有過(guò)度繪制
????????????????藍(lán)色: 1次過(guò)度繪制
????????????????綠色: 2次過(guò)度繪制
????????????????粉色: 3次過(guò)度繪制
????????????????紅色: 大于等于4次繪制
???????? 解決方案:
????????1)移除一些不需要的背景
????????如果有些子視圖有背景眉睹,而且會(huì)覆蓋父視圖時(shí),那么主視圖的背景就不必要設(shè)置废膘,系統(tǒng)對(duì)于沒(méi)有背景的是不會(huì)直接渲染內(nèi)容竹海,這樣就可以提高渲染的性能。
????????2)使視圖層級(jí)結(jié)構(gòu)扁平化
????????優(yōu)化布局的層次層次來(lái)減少重疊的視圖
????????3) 降低透明度
????????系統(tǒng)對(duì)于不透明的view ,之需要渲染一次就可以顯示出來(lái)丐黄,而如果設(shè)置了透明度斋配,則至少需要渲染兩次(因?yàn)橄到y(tǒng)要先知道他的下層元素是什么,然后再結(jié)合上層的view進(jìn)行混色處理)灌闺。
????????透明的動(dòng)畫艰争,淡入淡出和陰影效果等都是和透明度相關(guān)的,這樣就會(huì)造成過(guò)度繪制桂对。
-
異步加載布局
????????LayoutInflater 在加載XML布局的過(guò)程中會(huì)在主線程使用IO讀取XML文件進(jìn)行解析甩卓,再跟進(jìn)解析的結(jié)果利用反射創(chuàng)建布局中的View/ViewGroup對(duì)象。這個(gè)過(guò)程會(huì)雖然布局的復(fù)雜度上升蕉斜,耗時(shí)自然也會(huì)隨之增大逾柿。
????????AsyncLayoutInflater 內(nèi)部是一個(gè)線程來(lái)進(jìn)行遞歸遍歷xml文件的節(jié)點(diǎn),然后全部解析完成后將結(jié)果通過(guò)callback回調(diào)到主線程宅此。
implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
}
});
}
????????AsyncLayoutInflater的使用局限性:
????????????????1) 不支持包含F(xiàn)ragment的layout
????????????????2) 不支持設(shè)置LayoutInflater.Factory 或LayoutInflator.Factory2
????????????????3) 需要一個(gè)線程安全的generateLayoutParams 的parent
????????????????4) 如果無(wú)法異步構(gòu)造的布局机错,則會(huì)自動(dòng)退回到UI主線程上
????????????????5) 需要構(gòu)建的view中不能直接使用Handler或者調(diào)用Looper.myLooper(),因?yàn)楫惒骄€程默認(rèn)情況下是沒(méi)有調(diào)用Looper.prepare();