Android性能優化之佈局優化實戰

​本文首發於微信公衆號「Android開發之旅」,歡迎關注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

優化工具選擇

Systrace

這個咱們在啓動優化中講過具體的使用,這裏呢,咱們主要關注他的Frames一行,顯示綠色圓點表示正常,顯示黃色或者紅色表示出現了丟幀,出現丟幀的狀況的時候咱們須要去查看Alerts欄。框架

Layout Inspector

這個是Android Studio自帶的檢測工具,在Tools欄目下。它能夠幫助咱們查看視圖的層次結構。async

從圖中咱們能夠看到左側一覽顯示佈局的層級。ide

ChoreoGrapher

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中避免建立較大的對象和作耗時的操做等等。

總結

以上就是相關佈局優化相關的操做,也是從耗時到優化各個階段的說明和操做。讀者朋友們在看完本章節後,本身動手實踐下,只有實際實踐了才能發現問題,加深本身印象。

推薦閱讀:

App性能概覽與平臺化實踐理論

Android性能優化之啓動優化實戰

如何監測Android應用卡頓?這篇就夠了

掃描下方二維碼關注公衆號,及時獲取文章推送。

二維碼
相關文章
相關標籤/搜索