在第一篇《Android啟動速度優(yōu)化一啟動速度測量》日志里面有提到怎樣在APP啟動過程中測量整個啟動過程的時間叹卷,這一節(jié)記錄了在啟動過程中如何去測量和優(yōu)化View的創(chuàng)建時間字旭。
我們知道在Android中叉讥,Activity都是通過調(diào)用setContentView這個方法來給Activity設(shè)置視圖测蹲。一般的開發(fā)過程是:
- 依據(jù)UI像屋、UX在layout目錄下定義好布局的XML
- 創(chuàng)建Activity
- 在Activity的onCreate方法中調(diào)用setContentView加載布局
- 加載刷新視圖的數(shù)據(jù)(這部分可能是預(yù)加載资铡,也可能是延遲加載)
- 通過findViewById獲取目標(biāo)View电禀,并設(shè)置用戶響應(yīng)事件(例如點擊,滑動等)
- 使用加載好的數(shù)據(jù)對目標(biāo)View進(jìn)行視覺刷新
在這個過程中笤休,本文重點關(guān)注布局的定義和setContentView這兩個事件尖飞,其他的在后面的章節(jié)進(jìn)行討論。
一店雅、View加載時間的測量
在做優(yōu)化之前我們需要先知道View加載的實際耗時是多少政基,加載過程中是哪些維度占用時間多,了解了時間占用情況后才能結(jié)合實際場景和業(yè)務(wù)邏輯進(jìn)行加載優(yōu)化闹啦。
1.1沮明、測量setContentView的總耗時
我們知道setContentView里面會去解析布局文件,而Android的View體系是一個樹狀的數(shù)據(jù)結(jié)構(gòu)窍奋,這個就使得系統(tǒng)需要通過遞歸的形式來解析荐健,那如果你的布局層次越多,遞歸的時間就會越長琳袄。
在解析完了之后Android實際上是通過反射來實例化View的江场。基于java發(fā)射機(jī)制的話本身就會有一定的性能影響窖逗,如果這個時候你在自己的View的構(gòu)造函數(shù)中執(zhí)行過多的且耗時的操作址否,也是會直接影響View加載的時間。
基于以上現(xiàn)象滑负,我們需要在啟動過程中知道整個View加載的總時間和每一個View的創(chuàng)建時間
- 侵入式打點獲得總耗時
這個比較簡單在张,在setContentView前后打點記錄時間就好
long start = System.currentTimeMillis();
setContentView(R.layout.activity_main);
Log.d("SpeedTest","setContentView cost:" + (System.currentTimeMillis() - start)+ "ms");
- AOP實現(xiàn)非侵入式獲得總耗時
為了能夠快速且方便的集成AOP用含,這里提供一個集成AOP的插件矮慕,可以直接使用,簡單兩步就可以集成啄骇。
https://github.com/alexluotututu/aop_plugin
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TestViewInflate {
@Around("call(* android.app.Activity.setContentView(..))")
public void setContentViewTest(ProceedingJoinPoint point) {
long start = System.currentTimeMillis();
Signature signature = point.getSignature();
try {
point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.d("SpeedTest", point.getTarget().getClass().getName()
+ " setContentView total time:" + (System.currentTimeMillis() - start));
}
}
我們這里不討論AOP是怎樣集成到項目中的痴鳄,這個不是本章的重點,需要集成的話可以參考其他博客的文章缸夹。
我們可以看到以上兩種方式都可以統(tǒng)計到setContentView的總耗時痪寻,輸出結(jié)果如下:
05-19 10:39:11.061 26091 26091 D SpeedTest: com.snail.speeddemo.MainActivity setContentView total time:83
05-19 10:39:11.061 26091 26091 D SpeedTest: setContentView cost:83ms
兩者的耗時統(tǒng)計是一致的。
- Systrace統(tǒng)計總耗時
TraceCompat.beginSection("setContentView");
setContentView(R.layout.activity_main);
TraceCompat.endSection();
我們抓到的trace中找到setContentView這個節(jié)點虽惭,便可以知道總的耗時是多少了
1.2橡类、測量每個View的加載時間
在第1.1節(jié)中我們通過打點或者Systrace來獲取到setContentView的總耗時,但是在優(yōu)化過程中我們還是需要知道每個View的加載耗時芽唇,這個時候怎么辦顾画。結(jié)合Android的View加載機(jī)制和網(wǎng)上一些大神們的做法取劫,可以通過設(shè)置LayoutInflater的factory來實現(xiàn),下面是針對兩種不同的父Activity的設(shè)置方法
- 繼承自AppCompatActivity的實現(xiàn)方法
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View view, @NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
long start = System.currentTimeMillis();
View createdView = getDelegate().createView(view,s,context,attributeSet);
Log.d("SpeedTest","create "+s+" cost:"+(System.currentTimeMillis() - start));
return createdView;
}
@Nullable
@Override
public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
return null;
}
});
我們需要注意的是需要在super.onCreate之前調(diào)用這段代碼研侣,如果不是的話會出現(xiàn)crash的問題,因為super里面會設(shè)置factory谱邪,再重新設(shè)置的話系統(tǒng)會拋出以下異常
Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
我們最終的輸出結(jié)果:
05-19 11:20:26.722 6894 6894 D SpeedTest: create LinearLayout cost:3
05-19 11:20:26.722 6894 6894 D SpeedTest: create ViewStub cost:0
05-19 11:20:26.723 6894 6894 D SpeedTest: create FrameLayout cost:0
05-19 11:20:26.725 6894 6894 D SpeedTest: create androidx.appcompat.widget.ActionBarOverlayLayout cost:0
05-19 11:20:26.727 6894 6894 D SpeedTest: create androidx.appcompat.widget.ContentFrameLayout cost:0
05-19 11:20:26.728 6894 6894 D SpeedTest: create androidx.appcompat.widget.ActionBarContainer cost:0
05-19 11:20:26.731 6894 6894 D SpeedTest: create androidx.appcompat.widget.Toolbar cost:0
05-19 11:20:26.736 6894 6894 D SpeedTest: create androidx.appcompat.widget.ActionBarContextView cost:0
05-19 11:20:26.745 6894 6894 D SpeedTest: create androidx.constraintlayout.widget.ConstraintLayout cost:0
05-19 11:20:26.755 6894 6894 D SpeedTest: create TextView cost:1
這樣一來我們就可以看到每個View的創(chuàng)建時間了
- 繼承自framework中的Activity或者FragmentActivity的設(shè)置方法
LayoutInflaterCompat.setFactory(getLayoutInflater(), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
long start = System.currentTimeMillis();
View view = null;
try{
view = getLayoutInflater().createView(name,null,attrs);
}catch (ClassNotFoundException ex){
}
Log.d("SpeedTest","create "+name+" cost:"+(System.currentTimeMillis() - start));
return view;
}
});
同樣也是可以得到每個View的加載時間
二、優(yōu)化View的加載時間
通過第一節(jié)中的測量View加載方法庶诡,我們可以知道加載的總耗時和每個View的加載耗時惦银,為我們做進(jìn)一步優(yōu)化加載時間提供更多的輸入。
2.1末誓、優(yōu)化布局層次
布局層次會影響系統(tǒng)遞歸遍歷布局的時間扯俱,因此我們在布局的時候應(yīng)該要勁量采用輕量化的布局層次來實現(xiàn)功能。
2.1.1喇澡、 查看布局層次
我們可以使用Android Studio提供的Layout Inspector來查看布局層級
2.1.2蘸吓、 ConstraintLayout VS RelativeLayout
我們在布局的時候為了實現(xiàn)一些特殊的布局,可能會用到LinearLayout撩幽,RelativeLayout或者ConstraintLayout库继。后兩者都是可以減小布局層級的,但是這兩位中RelativeLayout如果子View太多也是有性能問題窜醉。因此我們在布局的時候還是要做一個綜合的評估宪萄。總體上ConstraintLayout的性能優(yōu)于RelativeLayout榨惰,因此都會建議大家用ConstraintLayout來實現(xiàn)復(fù)雜布局
2.1.3拜英、 merge標(biāo)簽減少布局層次
merge標(biāo)簽可以減少一個布局層次,使用merge需要注意以下幾點
- merge標(biāo)簽只能作為布局xml中的root節(jié)點
- merge不是View琅催,對它設(shè)置布局參數(shù)是沒用的
- 使用merge的時候如果要動態(tài)inflate布局居凶,需要設(shè)置parent,且attachToParent參數(shù)要為true
getLayoutInflater().inflate(R.layout.activity_main,mParent,true);
- merge標(biāo)簽如果需要在Android Studio中預(yù)覽到視圖的話需要添加parentTag
tools:parentTag="android.widget.LinearLayout"
2.1.4藤抡、 ViewStub懶加載
ViewStub的使用方法網(wǎng)上已經(jīng)有很多了侠碧,因此我們這里不討論怎樣用。主要看一下ViewStub為什么會可以后話加載時間缠黍。
- ViewStub是一個輕量化的View弄兜,它默認(rèn)的顯示狀態(tài)是View.GONE,并且不會繪制任何東西瓷式。我們從它的源碼里面可以看出來
構(gòu)造函數(shù)中把自己設(shè)置為不可見的
public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context);
//其他代碼
setVisibility(GONE);
setWillNotDraw(true);
}
- ViewStub本身不繪制任何東西替饿,所有加載時間很快
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);
}
@Override
public void draw(Canvas canvas) {
}
@Override
protected void dispatchDraw(Canvas canvas) {
}
可以從源碼上看到,它的draw方法和dispatchDraw直接是空的贸典,onMeasure方法也是直接設(shè)置寬度和高度為0
- ViewStub在調(diào)用inflate加載真正的View之后自己會被替換掉
我們可以從源碼中看到有這段邏輯,因此在使用它的時候要注意這種情況
inflate方法
public View inflate() {
//ViewStub和真正加載的View的parent為同一個
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
//這里會先通過LayoutInfater把ViewStub中指定的layout加載進(jìn)來
final View view = inflateViewNoAdd(parent);
//這里就會把真正的View添加到parent里面视卢,并且把ViewStub移除掉
replaceSelfWithView(view, parent);
mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
return view;
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
replaceSelfWithView把自己干掉
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
//先把自己從parent中移除
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
//下面的這段代碼就是把真正的View添加到原來ViewStub的位置上
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
三、其他的一些關(guān)鍵優(yōu)化項
3.1廊驼、APP主界面需要平衡包大小和啟動速度的關(guān)系据过,勁量少使用webp
webp和png之間的差異主要在于壓縮這個維度颊埃,但是在內(nèi)存占用上兩者是差不多的。因為壓縮率的問題蝶俱,導(dǎo)致webp在解碼的時候會比png慢4~5倍的樣子
3.2班利、不是很必要的話不要在布局中使用大量的TextView
TextView因為要處理文本排班和測量繪制的原因,導(dǎo)致TextView的性能比較低榨呆,主界面不是非必要的話文本內(nèi)容可以做懶加載
3.3罗标、 Lottie動畫文件的加載
現(xiàn)在很多APP可能都會使用Lottie來實現(xiàn)一些動畫,為了方便動畫的加載积蜻,會直接在布局文件中制定動畫json文件闯割。這個也是會消耗掉一部分的加載時間,因此如果可以做懶加載的話會比較好
四竿拆、總結(jié)
啟動速度和性能有話是一個看上去簡單宙拉,但是做起來難,特別是持續(xù)做比較難的一件事情丙笋。需要細(xì)心持續(xù)的去做谢澈,本節(jié)羅列和記錄了一些普遍常用的工具和方法。后續(xù)會有一些進(jìn)階的部分逐步呈現(xiàn)出來御板。