本文首發於微信公衆號「Android開發之旅」,歡迎關注android
手機渲染主要依賴於兩個硬件:CPU和GPU,其中CPU主要負責計算顯示內容,其中包括視圖建立、佈局計算、圖片解碼和文本繪製等。GPU主要負責柵格化(UI元素繪製到屏幕上),好比將Button、Bitmap拆分紅不一樣的像素進行顯示,最後完成繪製。性能優化
手機上顯示的文字就是先經過CPU換算成紋理後在交給GPU進行渲染。而圖片的顯示首先經過CPU進行計算,而後再加載到內存中,傳給GPU進行渲染。bash
咱們都知道Android系統每隔16ms就會發出Vsync信號(具體是由RootViewImpl類發起)觸發UI渲染,即要求每一幀都要在16ms內渲染完成,因此無論你的佈局邏輯多麼的複雜,你都要在16ms內繪製完成,不然就會出現界面卡頓的現象。微信
咱們市面上絕大部分Android手機的屏幕刷新頻率基本都是60Hz,由於60Hz每秒是人眼和大腦之間合做的極限,就像動畫每秒24幀同樣。app
這個咱們在啓動優化中講過具體的使用,這裏呢,咱們主要關注他的Frames一行,顯示綠色圓點表示正常,顯示黃色或者紅色表示出現了丟幀,出現丟幀的狀況的時候咱們須要去查看Alerts欄。框架
這個是Android Studio自帶的檢測工具,在Tools欄目下。它能夠幫助咱們查看視圖的層次結構。async
從圖中咱們能夠看到左側一覽顯示佈局的層級。ide
choreoGrapher能夠幫助咱們獲取應用的FPS,即上文中的60Hz,而且能夠線上使用,具有實時性。可是有一點須要注意的是必須API 16後使用。以下代碼:工具
private var mStartFrameTime: Long = 0
private var mFrameCount = 0
private val MONITOR_INTERVAL = 160L //單次計算FPS使用160毫秒
private val MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L
private val MAX_INTERVAL = 1000L //設置計算fps的單位時間間隔1000ms,即fps/s;
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
getFPS()
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private fun getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return
}
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (mStartFrameTime == 0L) {
mStartFrameTime = frameTimeNanos
}
val interval = frameTimeNanos - mStartFrameTime
if (interval > MONITOR_INTERVAL_NANOS) {
val fps = (mFrameCount.toLong() * 1000L * 1000L).toDouble() / interval * MAX_INTERVAL
Log.i("fps", fps.toString())
mFrameCount = 0
mStartFrameTime = 0
} else {
++mFrameCount
}
Choreographer.getInstance().postFrameCallback(this)
}
})
}
複製代碼
執行代碼後輸出:佈局
fps: 60.0158955700371
fps: 60.00346688030940
fps: 60.01226146521353
fps: 59.98537016806971
fps: 60.00205735054243
複製代碼
每次打印的數據都在60左右,說明頁面刷新沒有出現卡頓。
咱們常常寫的XML佈局文件是如何被加載的呢?又是如何顯示出來的?下面就帶着你們順着源碼往下看,這裏就不截圖了,讀者朋友們看完本章後本身能夠去熟悉下這塊代碼。
首先要從setContentView方法開始提及了,其中調用了getDeleate().setContentView(resid)方法,接着調用了 LayoutInflater.from(this.mContext).inflate(resId, contentParent)來填充佈局,這個API咱們你們應該都很熟悉了吧。緊接着調用getLayout方法,在getlayout方法中經過loadXmlResourceParser加載並解析XML佈局文件,後面調用createViewFromTag方法,根據標籤建立相對應爲view,具體view的建立則是由Factory或者Factory2來完成的,首先先判斷了Factory2爲否爲null,不爲null,則用其建立view,不然就判斷Factory是否爲null,不爲null,則由其建立。若是兩個都爲null,則不建立view,緊接着判斷了mPrivateFactory是否爲null,這裏須要說明的是mPrivateFactory是一個隱藏的API只有framework才能調用,若是都沒建立,那麼view則由後續邏輯經過onCreateView或者createView經過反射來建立。具體流程圖以下:
從上面的分析中咱們能夠看出加載佈局是有瓶頸的。其中有兩個瓶頸分別是在佈局文件解析的時候是一個IO過程,這確定是比較耗時的。再一個就是最後建立View的時候是經過反射的方式進行的。既然是反射性能確定也是有影響的,後面咱們也是圍繞這兩點進行佈局加載的優化。
咱們作優化的前提就是得知道哪裏是比較耗時的,因此檢測耗時的UI仍是蠻重要的。只有知道問題在哪了才能針對性的解決它。這裏講到檢測耗時,讀過我啓動優化一文的讀者確定能想到至少兩種方式,一種是手動埋點,另一種就是AOP的方式。手動埋點呢就是在setContentView方法的先後執行的地方手動打點。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LaunchRecord.startRecord()
setContentView(R.layout.activity_main)
LaunchRecord.endRecord("setContentView")
}
複製代碼
打印:
===setContentView===170
複製代碼
這種方式呢不夠優雅並且對代碼有侵入性。
下面咱們看下AOP的方式,操做和啓動優化一文中的同樣。
@Around("call(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.d("ContentViewTime", name + " cost " + (System.currentTimeMillis() - time));
}
複製代碼
控制檯打印:
ContentViewTime: MainActivity.setContentView(..) cost 74
複製代碼
以上兩種方法都是獲取所有佈局被加載完成後的時間,那麼若是想獲取單個控件的加載耗時如何作呢?這裏給你們介紹LayoutInflaterCompat.setFactory2方式(你們之後看到帶有Compat字段的都是兼容的API),其使用必須在super.onCreate以前調用。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
long start = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
long cost = System.currentTimeMillis() - start;
Log.d("onCreateView", "==" + name + "==cost==" + cost);
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
複製代碼
控制檯打印:
onCreateView: ==LinearLayout=cost==16
onCreateView: ==ViewStub=cost==0
onCreateView: ==FrameLayout=cost==0
onCreateView: ==android.support.v7.widget.ActionBarOverlayLayout=cost==0
onCreateView: ==android.support.v7.widget.ContentFrameLayout=cost==0
onCreateView: ==android.support.v7.widget.ActionBarContainer=cost==0
onCreateView: ==android.support.v7.widget.Toolbar=cost==0
onCreateView: ==android.support.v7.widget.ActionBarContextView=cost==0
onCreateView: ==android.support.constraint.ConstraintLayout=cost==0
onCreateView: ==TextView=cost==3
onCreateView: ==ImageView=cost==24
複製代碼
LayoutInflaterCompat.setFactory2的API不只僅是能夠統計View建立的時間,其實咱們還能夠用來替換系統控件的操做,好比某一天產品經理提了一個需求要咱們將應用的TextView統一改爲某種樣式,咱們就可使用這種方式來作。如:
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if(TextUtils.equals("TextView",name)){
//替換爲咱們本身的TextView
}
return null;//返回自定義View
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
複製代碼
只要咱們在基類Activity的onCreate中定義這個方法,就能夠實現相關效果。
基於佈局加載的兩個性能問題,谷歌給咱們提供了一個類AsyncLayoutInflater,它能夠從側面解決佈局加載耗時的問題,AsyncLayoutInflater是在工做線程中加載佈局,加載好後會回調到主線程,這樣能夠節省主線程的時間。這個類沒有包含在SDK中,須要咱們在gradle中配置,如:
implementation 'com.android.support:asynclayoutinflater:28.0.0-alpha1'
複製代碼
使用:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null,
new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
setContentView(view); //view以及加載完成
//能夠在這裏findViewById相關操做
}
});
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main); //這裏就不用設置佈局文件了
}
}
複製代碼
咱們在inflate的時候就將佈局文件設置給AsyncLayoutInflater,因此下面咱們就不須要在setContentView了。
上面說的AsyncLayoutInflater是從側面解決佈局加載耗時問題,那麼咱們如何從根本上解決這個問題呢?主要問題就是咱們書寫的XML文件須要加載解析和繪製,那若是咱們不使用XML文件寫佈局文件,問題是否是就解決?在Android中,還有另一種方式來寫佈局文件,那就是Java代碼,經過Java代碼來寫佈局,本質上是解決了性能問題,可是不便於開發,沒有實時預覽,並且可維護性太差。那麼若是能有一種解決方式就是,咱們開發人員仍是正常寫 XML文件,可是在加載的時候加載的是Java代碼,那這樣是否是很完美了。
因此下面給你們介紹一個新的框架:X2C,這是掌閱開源的一個框架,它保留了XML的優勢,同時解決了性能問題,開發人員寫XML文件,加載的時候只加載Java代碼。
X2C的原理就是經過APT編譯期時將XML翻譯爲Java代碼。使用也很簡單,首先gradle配置:
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
複製代碼
Java代碼使用:
@Xml(layouts = "activity_main")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main); //這裏就不用設置佈局文件了
}
}
複製代碼
編譯以後會在build/generated/source/apt/debug/ 下面生成相關的文件。如咱們的activity_main的佈局文件會被翻譯爲:
Resources res = ctx.getResources();
ConstraintLayout constraintLayout0 = new ConstraintLayout(ctx);
TextView textView1 = new TextView(ctx);
ConstraintLayout.LayoutParams layoutParam1 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
textView1.setId(R.id.mTextView);
layoutParam1.topMargin= (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,100,res.getDisplayMetrics())) ;
textView1.setText("Hello World!");
layoutParam1.leftToLeft = 0 ;
layoutParam1.rightToRight = 0 ;
layoutParam1.topToTop = 0 ;
layoutParam1.validate();
textView1.setLayoutParams(layoutParam1);
constraintLayout0.addView(textView1);
ImageView imageView2 = new ImageView(ctx);
ConstraintLayout.LayoutParams layoutParam2 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParam2.topMargin= (int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,20,res.getDisplayMetrics())) ;
imageView2.setImageResource(R.mipmap.ic_launcher);
layoutParam2.leftToLeft = 0 ;
layoutParam2.rightToRight = 0 ;
layoutParam2.topToBottom = R.id.mTextView ;
layoutParam2.validate();
imageView2.setLayoutParams(layoutParam2);
constraintLayout0.addView(imageView2);
複製代碼
運行APP,效果也是同樣的。
X2C雖好,但也有一些問題,就是部分屬性Java不支持,並且失去了系統的兼容性(AppCompat)。因此若是要帶到線上使用,那麼就要兼容不一樣的版本,因此須要定製化修改源碼。
咱們知道視圖的繪製一般經歷三個階段,測量,肯定view的大小。佈局,肯定view的具體位置包括view和viewGroup等。繪製,將view繪製完成。不論是測量、佈局仍是繪製,每個階段都是比較耗時的,都是自上而下的遍歷每個view,在某些場景下還會觸發屢次,好比嵌套使用RelativeLayout佈局。
因此爲了減小三個階段的耗時,咱們須要減小view樹的層級,不要嵌套使用RelativeLayout佈局,不在嵌套使用的LinearLayout中使用weight屬性。適當的使用merge標籤,它能夠減小一個view層級,可是必須使用在根view上。
這裏推薦你們使用ConstraintLayout佈局,ConstraintLayout幾乎實現了徹底扁平化的佈局,並且在構建複雜佈局上面性能更高,同時他還具有了RelativeLayout和LinearLayout的特性,使用很方便。
同時咱們在書寫佈局的時候還要注意避免過分繪製。Android手機在開發者選項中有個功能叫:調試GPU過分繪製。打開後手機界面會有一層蒙版,其中藍色表示能夠接受,紅色表色出現過分繪製了。那咱們如何避免過分繪製呢?首先是去掉多餘的背景色,減小複雜shape的使用,避免層級疊加,在用自定義view的時候使用ClipRect屏蔽被遮蓋view的繪製。
還有其餘的一些優化視圖繪製,好比使用Viewstub,它是一個高效的佔位符,能夠用來延遲加載view佈局。還有就是咱們在onDraw中避免建立較大的對象和作耗時的操做等等。
以上就是相關佈局優化相關的操做,也是從耗時到優化各個階段的說明和操做。讀者朋友們在看完本章節後,本身動手實踐下,只有實際實踐了才能發現問題,加深本身印象。