一.CPU/GPU
CPU的任務(wù)繁多雹拄,做邏輯計算外收奔,還要做內(nèi)存管理、顯示操作滓玖,因此 在實(shí)際運(yùn)算的時候性能會大打折扣坪哄,在沒有 GPU 的時代,不能顯示復(fù) 雜的圖形势篡,其運(yùn)算速度遠(yuǎn)跟不上今天復(fù)雜三維游戲的要求翩肌。即使 CPU 的工作頻率超過 2GHz 或更高,對它繪制圖形提高也不大禁悠。這時 GPU 的設(shè)計就出來了 念祭。
二.XML布局顯示到屏幕流程
三.卡頓原理
四.16ms主要處理兩件事
- 將UI對象轉(zhuǎn)換成多邊形和紋理。
- CPU傳遞數(shù)據(jù)到GPU碍侦,GPU進(jìn)行繪制粱坤。
五.如何減少時間
- CPU減少XML轉(zhuǎn)換成對象時間
- GPU減少重復(fù)繪制(GPU傻)
六.卡頓的原因
兩個單位的運(yùn)行時間超出16.66ms就會跳幀
- XML文件加載解析,到傳輸至底層到最終post到通知surfaceflinger的時間
-
GPU繪制產(chǎn)生數(shù)據(jù)的時間
所以想要處理就要控制這兩塊時間在16.66mm內(nèi)為最優(yōu)瓷产。
卡頓優(yōu)化的唯一核心就是讓CPU的數(shù)據(jù)處理和GPU的數(shù)據(jù)處理降低至16.66ms內(nèi)全部處理完站玄。
七.常用問題解決方案01-布局優(yōu)化
層級越深--->infalate--->遞歸走法---》內(nèi)存--->棧去里面也要消耗。層級越多CPU算力需要越多濒旦。
-
常用標(biāo)簽
include:頭尾等同質(zhì)化嚴(yán)重的可復(fù)用XML最好做成一個獨(dú)立布局文件引用蜒什。
merge:被include的具體布局采用merge,作用是會在加載時直接嵌入到父布局中,和include配合使用疤估。
viewstub:常規(guī)像密碼提示框那些東西只有在需要特定條件下觸發(fā)才展示的用ViewStub這個組件只用在狀態(tài)為visible時才會加載。 -
常用方案
1.調(diào)整布局結(jié)構(gòu)霎冯。
2.背景色匹配铃拇。
3.使用約束布局。
約束布局優(yōu)點(diǎn):
a.極大程度減少布局層級
b.可以實(shí)現(xiàn)一些其他布局管理器不能實(shí)現(xiàn)的樣式
約束布局缺點(diǎn):
每個被參考的控件都需要設(shè)置id
八.常用問題解決方案02-過度繪制
-
GPU過度繪制檢查
手機(jī)開發(fā)者功能中自帶檢測工具沈撞。 -
解決方案
1.移除布局中不必要的背景慷荔。
2.是視圖層次結(jié)構(gòu)扁平化。
3.裁剪不必要的繪制元素缠俺。
src = 一次opengl繪制
background = 一次opengl繪制
過度繪制的幾種情況:
1: 布局層級太深显晶, 用戶看不到的區(qū)域也會被繪制
2: 自定義控件中贷岸,onDraw方法做了過多的繪制
九.檢測工具01layout inspector
查看布局層次結(jié)構(gòu),主要用于布局優(yōu)化磷雇。
具體使用請查看:
https://blog.csdn.net/cadi2011/article/details/85212762
十.檢測工具02systrace
具體使用請查看:
https://www.cnblogs.com/wangjie1990/p/11327220.html
十一.檢測工具03Looper機(jī)制
因?yàn)樵贚ooper進(jìn)行消息轉(zhuǎn)發(fā)的時候偿警,會涉及到打印問題,且在執(zhí)行前后都會打印唯笙,利用這個機(jī)制在Looper中他提供能夠自定義Loging相關(guān)機(jī)制螟蒸。
public class LogMonitor implements Printer {
private StackSampler mStackSampler;
private boolean mPrintingStarted = false;
private long mStartTimestamp;
// 卡頓閾值
private long mBlockThresholdMillis = (long) (5 * 16.66);
//采樣頻率
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會執(zhí)行 dispatchMessage,如果執(zhí)行耗時超過閾值崩掘,輸出卡頓信息
if (!mPrintingStarted) {
//記錄開始時間
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() {
//獲得卡頓時主線程堆棧
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;
}
}
/**
* 適用于耗時代碼檢測
*/
public class BlockCanary {
public static void install() {
LogMonitor logMonitor = new LogMonitor();
Looper.getMainLooper().setMessageLogging(logMonitor);
}
}
/**
* 適用于耗時代碼檢測
*/
public class BlockCanary {
public static void install() {
LogMonitor logMonitor = new LogMonitor();
Looper.getMainLooper().setMessageLogging(logMonitor);
}
}
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);
}
}
};
}
十二.ChoreograhperHelper編舞者監(jiān)聽幀率
注意目的是看整個運(yùn)行情況定位到大塊七嫌,然后結(jié)合上面設(shè)定閾值進(jìn)行處理。
/**
* 細(xì)化幀數(shù)
* 適用于快速定位幀率監(jiān)控
*/
public class ChoreographerHelper {
private static final String TAG = "ChoreographerHelper";
static long lastFrameTimeNanos = 0;
public static void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//上次回調(diào)時間
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);
if (droppedCount > 2) {
Log.w(TAG, "UI線程超時(超過16ms)當(dāng)前:" + diff + "ms" + " , 丟幀:" + droppedCount);
}
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
}
十三.UI優(yōu)化思路與核心決策
-
如何研判是否需要做調(diào)整
優(yōu)化的方案一定是摳空間或者時間:
加入20個Fragment:優(yōu)化每一個Fragment到極致(1:數(shù)據(jù)能不能少加載點(diǎn)苞慢?2:流程能不能優(yōu)化诵原?視覺能不能優(yōu)化?)
20個Fragment能不能少加載點(diǎn)挽放?流程優(yōu)化绍赛?
空間重要還是時間 ?流程上能不能優(yōu)化骂维? -
方案上如何去做妥協(xié)
1.穩(wěn)定第一位
2.成本/用戶體驗(yàn)
卡頓分析與布局優(yōu)化
卡頓分析
Systrace
Systrace 是Android平臺提供的一款工具惹资,用于記錄短期內(nèi)的設(shè)備活動。該工具會生成一份報告航闺,其中匯總了
Android 內(nèi)核中的數(shù)據(jù)褪测,例如 CPU 調(diào)度程序、磁盤活動和應(yīng)用線程潦刃。Systrace主要用來分析繪制性能方面的問題侮措。在發(fā)生卡頓時,通過這份報告可以知道當(dāng)前整個系統(tǒng)所處的狀態(tài)乖杠,從而幫助開發(fā)者更直觀的分析系統(tǒng)瓶頸分扎,改進(jìn)性能。
App層面監(jiān)控卡頓
systrace可以讓我們了解應(yīng)用所處的狀態(tài)胧洒,了解應(yīng)用因?yàn)槭裁丛驅(qū)е碌奈废拧H粜枰獪?zhǔn)確分析卡頓發(fā)生在什么函數(shù),
資源占用情況如何卫漫,目前業(yè)界兩種主流有效的app監(jiān)控方式如下:
1菲饼、 利用UI線程的Looper打印的日志匹配;
2列赎、 使用Choreographer.FrameCallback
Looper日志檢測卡頓
Android主線程更新UI宏悦。如果界面1秒鐘刷新少于60次,即FPS小于60,用戶就會產(chǎn)生卡頓感覺饼煞。簡單來說源葫,
Android使用消息機(jī)制進(jìn)行UI更新,UI線程有個Looper砖瞧,在其loop方法中會不斷取出message息堂,調(diào)用其綁定的
Handler在UI線程執(zhí)行。如果在handler的dispatchMesaage方法里有耗時操作芭届,就會發(fā)生卡頓储矩。
其實(shí)這種方式也就是 BlockCanary 原理。
Choreographer.FrameCallback
Android系統(tǒng)每隔16ms發(fā)出VSYNC信號褂乍,來通知界面進(jìn)行重繪持隧、渲染,每一次同步的周期約為16.6ms逃片,代表一幀
的刷新頻率屡拨。通過Choreographer類設(shè)置它的FrameCallback函數(shù),當(dāng)每一幀被渲染時會觸發(fā)回調(diào)FrameCallback.doFrame (long frameTimeNanos) 函數(shù)褥实。frameTimeNanos是底層VSYNC信號到達(dá)的時間戳 呀狼。
通過 ChoreographerHelper 可以實(shí)時計算幀率和掉幀數(shù),實(shí)時監(jiān)測App頁面的幀率數(shù)據(jù)损离,發(fā)現(xiàn)幀率過低哥艇,還可以自動保存現(xiàn)場堆棧信息。
Looper比較適合在發(fā)布前進(jìn)行測試或者小范圍灰度測試然后定位問題僻澎,ChoreographerHelper適合監(jiān)控線上環(huán)境的 app 的掉幀情況來計算 app 在某些場景的流暢度然后有針對性的做性能優(yōu)化貌踏。
布局優(yōu)化
層級優(yōu)化
measure、layout窟勃、draw這三個過程都包含自頂向下的View Tree遍歷耗時祖乳,如果視圖層級太深自然需要更多的時間來完成整個繪測過程,從而造成啟動速度慢秉氧、卡頓等問題眷昆。而onDraw在頻繁刷新時可能多次出發(fā),因此onDraw更不能做耗時操作汁咏,同時需要注意內(nèi)存抖動亚斋。對于布局性能的檢測,依然可以使用systrace與traceview按照繪制流程檢查繪制耗時函數(shù)攘滩。
Layout Inspector
在較早的時代SDK中有一個hierarchy viewer 工具帅刊,但是早在 Android Studio 3.1 配套的SDK中(具體SDK版本不記得了)就已經(jīng)被棄用。現(xiàn)在應(yīng)在運(yùn)行時改用 Layout Inspector來檢查應(yīng)用的視圖層次結(jié)構(gòu)
使用merge標(biāo)簽
當(dāng)我們有一些布局元素需要被多處使用時轰驳,這時候我們會考慮將其抽取成一個單獨(dú)的布局文件。在需要使用的地方通過 include 加載。
使用ViewStub 標(biāo)簽
當(dāng)我們布局中存在一個View/ViewGroup级解,在某個特定時刻才需要他的展示時冒黑,可能會有同學(xué)把這個元素在xml中定義為invisible或者gone,在需要顯示時再設(shè)置為visible可見勤哗。比如在登陸時抡爹,如果密碼錯誤在密碼輸入框上顯示提示。
invisible
view設(shè)置為invisible時芒划,view在layout布局文件中會占用位置冬竟,但是view為不可見,該view還是會創(chuàng)建對
象民逼,會被初始化泵殴,會占用資源。
gone
view設(shè)置gone時拼苍,view在layout布局文件中不占用位置笑诅,但是該view還是會創(chuàng)建對象,會被初始化疮鲫,會占
用資源吆你。
如果view不一定會顯示,此時可以使用 ViewStub 來包裹此View 以避免不需要顯示view但是又需要加載view消耗資
源俊犯。
viewstub是一個輕量級的view妇多,它不可見,不用占用資源燕侠,只有設(shè)置viewstub為visible或者調(diào)用其inflater()方法
時者祖,其對應(yīng)的布局文件才會被初始化。
過度渲染
過度繪制是指系統(tǒng)在渲染單個幀的過程中多次在屏幕上繪制某一個像素贬循。例如咸包,如果我們有若干界面卡片堆疊在一
起,每張卡片都會遮蓋其下面一張卡片的部分內(nèi)容杖虾。但是烂瘫,系統(tǒng)仍然需要繪制堆疊中的卡片被遮蓋的部分随闽。
GPU 過度繪制檢查
手機(jī)開發(fā)者選項中能夠顯示過度渲染檢查功能氧卧,通過對界面進(jìn)行彩色編碼來幫我們識別過度繪制号胚。
解決過度繪制問題
可以采取以下幾種策略來減少甚至消除過度繪制:
移除布局中不需要的背景玫恳。
移除不必要的背景可以快速提高渲染性能损谦。不必要的背景可能永遠(yuǎn)不可見刁绒,因?yàn)樗鼤粦?yīng)用在該視圖上
繪制的任何其他內(nèi)容完全覆蓋婚惫。例如屡谐,當(dāng)系統(tǒng)在父視圖上繪制子視圖時皮仁,可能會完全覆蓋父視圖的背
景籍琳。使視圖層次結(jié)構(gòu)扁平化菲宴。
可以通過優(yōu)化視圖層次結(jié)構(gòu)來減少重疊界面對象的數(shù)量,從而提高性能趋急。降低透明度喝峦。
對于不透明的 view ,只需要渲染一次即可把它顯示出來呜达。但是如果這個 view 設(shè)置了 alpha 值谣蠢,則至少需要渲染兩次。這是因?yàn)槭褂昧?alpha 的 view 需要先知道混合 view 的下一層元素是什么查近,然后再結(jié)合上層的 view 進(jìn)行Blend混色處理眉踱。透明動畫、淡入淡出和陰影等效果都涉及到某種透明度霜威,這就會造成了過度繪制谈喳。可以通過減少要渲染的透明對象的數(shù)量侥祭,來改善這些情況下的過度繪制叁执。例如,如需獲得灰色文本矮冬,可以在 TextView 中繪制黑色文本谈宛,再為其設(shè)置半透明的透明度值。但是胎署,簡單地通過用灰色繪制文本也能獲得同樣的效果吆录,而且能夠大幅提升性能。
布局加載優(yōu)化
異步加載
LayoutInflater加載xml布局的過程會在主線程使用IO讀取XML布局文件進(jìn)行XML解析琼牧,再根據(jù)解析結(jié)果利用反射創(chuàng)建布局中的View/ViewGroup對象恢筝。這個過程隨著布局的復(fù)雜度上升,耗時自然也會隨之增大巨坊。Android為我們提供了 Asynclayoutinflater 把耗時的加載操作在異步線程中完成撬槽,最后把加載結(jié)果再回調(diào)給主線程。
dependencies {
implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"
}
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); //...... }
});
1趾撵、使用異步 inflate侄柔,那么需要這個 layout 的 parent 的 generateLayoutParams 函數(shù)是線程安全的;
2占调、所有構(gòu)建的 View 中必須不能創(chuàng)建 Handler 或者是調(diào)用 Looper.myLooper暂题;(因?yàn)槭窃诋惒骄€程中加載的,異步線程默認(rèn)沒有調(diào)用 Looper.prepare )究珊;
3薪者、AsyncLayoutInflater 不支持設(shè)置 LayoutInflater.Factory 或者 LayoutInflater.Factory2;
4剿涮、不支持加載包含 Fragment 的 layout
5言津、如果 AsyncLayoutInflater 失敗攻人,那么會自動回退到UI線程來加載布局;
掌閱X2C思路
https://github.com/iReaderAndroid/X2C/blob/master/README_CN.md