App下載

對(duì)于使用android布局可以進(jìn)行哪些優(yōu)化?android優(yōu)化的一些實(shí)用建議!

漫步云海澗 2021-08-20 14:39:25 瀏覽數(shù) (1909)
反饋

作為程序員我們?cè)谕瓿身?xiàng)目的同時(shí)還需要考慮到各種代碼的優(yōu)化,那么今天我們來(lái)說(shuō)下“對(duì)于使用android布局可以進(jìn)行哪些優(yōu)化?”這個(gè)問(wèn)題吧!

前言

Android的繪制優(yōu)化其實(shí)可以分為兩個(gè)部分,即布局(UI)優(yōu)化和卡頓優(yōu)化,而布局優(yōu)化的核心問(wèn)題就是要解決因布局渲染性能不佳而導(dǎo)致應(yīng)用卡頓的問(wèn)題,所以它可以認(rèn)為是卡頓優(yōu)化的一個(gè)子集。

本文主要包括以下內(nèi)容

  1. 為什么要進(jìn)行布局優(yōu)化及android繪制,布局加載原理
  2. 獲取布局文件加載耗時(shí)的方法
  3. 介紹一些布局優(yōu)化的手段與方法
  4. 一些常規(guī)優(yōu)化手段

為什么要進(jìn)行布局優(yōu)化?

為什么要進(jìn)行布局優(yōu)化?答案是顯而易見(jiàn)的,如果布局嵌套過(guò)深,或者其他原因?qū)е虏季咒秩拘阅懿患眩赡軙?huì)導(dǎo)致應(yīng)用卡頓 那么布局到底是如何導(dǎo)致渲染性能不佳的呢?首先我們應(yīng)該了解下android繪制原理與布局加載原理

android繪制原理

Android的屏幕刷新中涉及到最重要的三個(gè)概念(為便于理解,這里先做簡(jiǎn)單介紹)

  • CPU:執(zhí)行應(yīng)用層的measure、layout、draw等操作,繪制完成后將數(shù)據(jù)提交給GPU
  • GPU:進(jìn)一步處理數(shù)據(jù),并將數(shù)據(jù)緩存起來(lái)
  • 屏幕:由一個(gè)個(gè)像素點(diǎn)組成,以固定的頻率(16.6ms,即1秒60幀)從緩沖區(qū)中取出數(shù)據(jù)來(lái)填充像素點(diǎn)

總結(jié)一句話就是:CPU 繪制后提交數(shù)據(jù)、GPU 進(jìn)一步處理和緩存數(shù)據(jù)、最后屏幕從緩沖區(qū)中讀取數(shù)據(jù)并顯示

雙緩沖機(jī)制

看完上面的流程圖,我們很容易想到一個(gè)問(wèn)題,屏幕是以16.6ms的固定頻率進(jìn)行刷新的,但是我們應(yīng)用層觸發(fā)繪制的時(shí)機(jī)是完全隨機(jī)的(比如我們隨時(shí)都可以觸摸屏幕觸發(fā)繪制). 如果在GPU向緩沖區(qū)寫(xiě)入數(shù)據(jù)的同時(shí),屏幕也在向緩沖區(qū)讀取數(shù)據(jù),會(huì)發(fā)生什么情況呢?有可能屏幕上就會(huì)出現(xiàn)一部分是前一幀的畫(huà)面,一部分是另一幀的畫(huà)面,這顯然是無(wú)法接受的,那怎么解決這個(gè)問(wèn)題呢?

所以,在屏幕刷新中,Android系統(tǒng)引入了雙緩沖機(jī)制

GPU只向Back Buffer中寫(xiě)入繪制數(shù)據(jù),且GPU會(huì)定期交換Back Buffer和Frame Buffer,交換的頻率也是60次/秒,這就與屏幕的刷新頻率保持了同步。

雖然我們引入了雙緩沖機(jī)制,但是我們知道,當(dāng)布局比較復(fù)雜,或設(shè)備性能較差的時(shí)候,CPU并不能保證在16.6ms內(nèi)就完成繪制數(shù)據(jù)的計(jì)算,所以這里系統(tǒng)又做了一個(gè)處理。當(dāng)你的應(yīng)用正在往Back Buffer中填充數(shù)據(jù)時(shí),系統(tǒng)會(huì)將Back Buffer鎖定。如果到了GPU交換兩個(gè)Buffer的時(shí)間點(diǎn),你的應(yīng)用還在往Back Buffer中填充數(shù)據(jù),GPU會(huì)發(fā)現(xiàn)Back Buffer被鎖定了,它會(huì)放棄這次交換。

這樣做的后果就是手機(jī)屏幕仍然顯示原先的圖像,這就是我們常常說(shuō)的掉幀

布局加載原理

由上面可知,導(dǎo)致掉幀的原因是CPU無(wú)法在16.6ms內(nèi)完成繪制數(shù)據(jù)的計(jì)算。而之所以布局加載可能會(huì)導(dǎo)致掉幀,正是因?yàn)樗谥骶€程上進(jìn)行了耗時(shí)操作,可能導(dǎo)致CPU無(wú)法按時(shí)完成數(shù)據(jù)計(jì)算

布局加載主要通過(guò)setContentView來(lái)實(shí)現(xiàn),我們就不在這里貼源碼了,一起來(lái)看看它的時(shí)序圖

我們可以看到,在setContentView中主要有兩個(gè)耗時(shí)操作

  • 解析xml,獲取XmlResourceParser,這是IO過(guò)程
  • 通過(guò)createViewFromTag,創(chuàng)建View對(duì)象,用到了反射

以上兩點(diǎn)就是布局加載可能導(dǎo)致卡頓的原因,也是布局的性能瓶頸

獲取布局文件加載耗時(shí)的方法

我們?nèi)绻枰獌?yōu)化布局卡頓問(wèn)題,首先最重要的就是:確定定量標(biāo)準(zhǔn) 所以我們首先介紹幾種獲取布局文件加載耗時(shí)的方法

常規(guī)獲取

首先介紹一下常規(guī)方法

val start = System.currentTimeMillis()
setContentView(R.layout.activity_layout_optimize)
val inflateTime = System.currentTimeMillis() - start

這種方法很簡(jiǎn)單,因?yàn)閟etContentView是同步方法,如果想要計(jì)算耗時(shí),直接將前后時(shí)間計(jì)算相減即可得到結(jié)果了

AOP(Aspectj,ASM)

上面的方式雖然簡(jiǎn)單,但是卻不夠優(yōu)雅,同時(shí)代碼有侵入性,如果要對(duì)所有Activity測(cè)量時(shí),就需要在基類中復(fù)寫(xiě)相關(guān)方法了,比較麻煩了 下面介紹一種AOP的方式計(jì)算耗時(shí)

    @Around("execution(* 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.i("aop inflate",name + " cost " + (System.currentTimeMillis() - time));
    }

上面用的Aspectj,比較簡(jiǎn)單,上面的注解的意思是在setContentView方法執(zhí)行內(nèi)部去調(diào)用我們寫(xiě)好的getSetContentViewTime方法 這樣就可以獲取相應(yīng)的耗時(shí) 我們可以看下打印的日志

I/aop inflate: AppCompatActivity.setContentView(..) cost 69
I/aop inflate: AppCompatActivity.setContentView(..) cost 25

這樣就可以實(shí)現(xiàn)無(wú)侵入的監(jiān)控每個(gè)頁(yè)面布局加載的耗時(shí) 具體源碼可見(jiàn)文末

獲取任一控件耗時(shí)

有時(shí)為了更精確的知道到底是哪個(gè)控件加載耗時(shí),比如我們新添加了自定義View,需要監(jiān)控它的性能 我們可以利用setFactory2來(lái)監(jiān)聽(tīng)每個(gè)控件的加載耗時(shí) 首先我們來(lái)回顧下setContentView方法

    public final View tryCreateView(@Nullable View parent, @NonNull String name,
        ...
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        ...
        return view;
    }

在真正進(jìn)行反射實(shí)例化xml結(jié)點(diǎn)前,會(huì)調(diào)用mFactory2的onCreateView方法 這樣如果我們重寫(xiě)onCreateView方法,在其前后加上耗時(shí)統(tǒng)計(jì),即可獲取每個(gè)控件的加載耗時(shí)

    private fun initItemInflateListener(){
        LayoutInflaterCompat.setFactory2(layoutInflater, object : Factory2 {
            override fun onCreateView(
                parent: View?,
                name: String,
                context: Context,
                attrs: AttributeSet
            ): View? {
                val time = System.currentTimeMillis()
                val view = delegate.createView(parent, name, context, attrs)
                Log.i("inflate Item",name + " cost " + (System.currentTimeMillis() - time))
                return view
            }

            override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
                return null
            }
        })
    }

如上所示:真正的創(chuàng)建View的方法,仍然是調(diào)用delegate.createView,我們只是其之前與之后做了埋點(diǎn) 注意,initItemInflateListener需要在onCreate之前調(diào)用 這樣就可以比較方便地實(shí)現(xiàn)監(jiān)聽(tīng)每個(gè)控件的加載耗時(shí)

布局加載優(yōu)化的一些方法介紹

布局加載慢的主要原因有兩個(gè),一個(gè)是IO,一個(gè)是反射 所以我們的優(yōu)化思路一般有兩個(gè)

  1. 側(cè)面緩解(異步加載)
  2. 根本解決(不需要IO,反射過(guò)程,如X2C,Anko,Compose等)

AsyncLayoutInflater方案

AsyncLayoutInflater 是來(lái)幫助做異步加載 layout 的,inflate(int, ViewGroup, OnInflateFinishedListener) 方法運(yùn)行結(jié)束之后 OnInflateFinishedListener 會(huì)在主線程回調(diào)返回 View;這樣做旨在 UI 的懶加載或者對(duì)用戶操作的高響應(yīng)。

簡(jiǎn)單的說(shuō)我們知道默認(rèn)情況下 setContentView 函數(shù)是在 UI 線程執(zhí)行的,其中有一系列的耗時(shí)動(dòng)作:Xml的解析、View的反射創(chuàng)建等過(guò)程同樣是在UI線程執(zhí)行的,AsyncLayoutInflater 就是來(lái)幫我們把這些過(guò)程以異步的方式執(zhí)行,保持UI線程的高響應(yīng)。

使用如下:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new AsyncLayoutInflater(AsyncLayoutActivity.this)
                .inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
                    @Override
                    public void onInflateFinished(View view, int resid, ViewGroup parent) {
                        setContentView(view);
                    }
                });
        // 別的操作
    }

這樣做的優(yōu)點(diǎn)在于將UI加載過(guò)程遷移到了子線程,保證了UI線程的高響應(yīng) 缺點(diǎn)在于犧牲了易用性,同時(shí)如果在初始化過(guò)程中調(diào)用了UI可能會(huì)導(dǎo)致崩潰

X2C方案

X2C是掌閱開(kāi)源的一套布局加載框架 它的主要是思路是在編譯期,將需要翻譯的layout翻譯生成對(duì)應(yīng)的java文件,這樣對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)寫(xiě)布局還是寫(xiě)原來(lái)的xml,但對(duì)于程序來(lái)說(shuō),運(yùn)行時(shí)加載的是對(duì)應(yīng)的java文件。這就將運(yùn)行時(shí)的開(kāi)銷轉(zhuǎn)移到了編譯時(shí) 如下所示,原始xml文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="10dp">

  <include
      android:id="@+id/head"
      layout="@layout/head"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerHorizontal="true" />

  <ImageView
      android:id="@+id/ccc"
      style="@style/bb"
      android:layout_below="@id/head" />
</RelativeLayout>

X2C 生成的 Java 文件

public class X2C_2131296281_Activity_Main implements IViewCreator {
  @Override
  public View createView(Context ctx, int layoutId) {
        Resources res = ctx.getResources();

        RelativeLayout relativeLayout0 = new RelativeLayout(ctx);
        relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0,0,0);

        View view1 =(View) new X2C_2131296283_Head().createView(ctx,0);
        RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
        view1.setLayoutParams(layoutParam1);
        relativeLayout0.addView(view1);
        view1.setId(R.id.head);
        layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);

        ImageView imageView2 = new ImageView(ctx);
        RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics())));
        imageView2.setLayoutParams(layoutParam2);
        relativeLayout0.addView(imageView2);
        imageView2.setId(R.id.ccc);
        layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);

        return relativeLayout0;
  }
}

使用時(shí)如下所示,使用X2C.setContentView替代原始的setContentView即可

// this.setContentView(R.layout.activity_main);
X2C.setContentView(this, R.layout.activity_main);

X2C優(yōu)點(diǎn)

  1. 在保留xml的同時(shí),又解決了它帶來(lái)的性能問(wèn)題
  2. 據(jù)X2C統(tǒng)計(jì),加載耗時(shí)可以縮小到原來(lái)的1/3

X2C問(wèn)題

  1. 部分屬性不能通過(guò)代碼設(shè)置,Java不兼容
  2. 將加載時(shí)間轉(zhuǎn)移到了編譯期,增加了編譯期耗時(shí)
  3. 不支持kotlin-android-extensions插件,犧牲了部分易用性

Anko方案

Anko是JetBrains開(kāi)發(fā)的一個(gè)強(qiáng)大的庫(kù),支持使用kotlin DSL的方式來(lái)寫(xiě)UI,如下所示

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        MyActivityUI().setContentView(this)
    }
}

class MyActivityUI : AnkoComponent<MyActivity> {
    override fun createView(ui: AnkoContext<MyActivity>) = with(ui) {
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { ctx.toast("Hello, ${name.text}!") }
            }
        }
    }
}

如上所示,Anko使用kotlin DSL實(shí)現(xiàn)布局,它比我們使用Java動(dòng)態(tài)創(chuàng)建布局方便很多,主要是更簡(jiǎn)潔,它和擁有xml創(chuàng)建布局的層級(jí)關(guān)系,能讓我們更容易閱讀 同時(shí),它去除了IO與反射過(guò)程,性能更好,以下是Anko與XML的性能對(duì)比

不過(guò)由于AnKo已經(jīng)停止維護(hù)了,這里不建議大家使用,了解原理即可 AnKo建議大家使用Jetpack Compose來(lái)替代使用

Compose方案

Compose 是 Jetpack 中的一個(gè)新成員,是 Android 團(tuán)隊(duì)在2019年I/O大會(huì)上公布的新的UI庫(kù),目前處于Beta階段 Compose使用純kotlin開(kāi)發(fā),使用簡(jiǎn)潔方便,但它并不是像Anko一樣對(duì)ViewGroup的封裝 Compose 并不是對(duì) View 和 ViewGroup 這套系統(tǒng)做了個(gè)上層包裝來(lái)讓寫(xiě)法更簡(jiǎn)單,而是完全拋棄了這套系統(tǒng),自己把整個(gè)的渲染機(jī)制從里到外做了個(gè)全新的。

可以確定的是,Compose是取代XML的官方方案

Compose的主要優(yōu)點(diǎn)就在于它的簡(jiǎn)單好用,具體來(lái)說(shuō)就是兩點(diǎn)

  1. 它的聲明式 UI
  2. 去掉了 xml,只使用 Kotlin 一種語(yǔ)言

由于本文并不是介紹Compose的,所以就不繼續(xù)介紹Compose了,總得來(lái)說(shuō),Compose是未來(lái)android UI開(kāi)發(fā)的方向,讀者可以自行查閱相關(guān)資料

一些常規(guī)優(yōu)化手段

上面介紹了一些改動(dòng)比較大的方案,其實(shí)我們?cè)趯?shí)際開(kāi)發(fā)中也有些常規(guī)的方法可以優(yōu)化布局加載 比如優(yōu)化布局層級(jí),避免過(guò)度繪制等,這些簡(jiǎn)單的手段可能正是可以應(yīng)用到項(xiàng)目中的

優(yōu)化布局層級(jí)及復(fù)雜度

  1. 使用ConstraintLayout,可以實(shí)現(xiàn)完全扁平化的布局,減少層級(jí)
  2. RelativeLayout本身盡量不要嵌套使用
  3. 嵌套的LinearLayout中,盡量不要使用weight,因?yàn)閣eight會(huì)重新測(cè)量?jī)纱?/li>
  4. 推薦使用merge標(biāo)簽,可以減少一個(gè)層級(jí)
  5. 使用ViewStub延遲加載

避免過(guò)度繪制

  1. 去掉多余背景色,減少?gòu)?fù)雜shape的使用
  2. 避免層級(jí)疊加
  3. 自定義View使用clipRect屏蔽被遮蓋View繪制

總結(jié)

那么對(duì)于“對(duì)于使用android布局可以進(jìn)行哪些優(yōu)化?”這個(gè)問(wèn)題的優(yōu)化和相關(guān)實(shí)現(xiàn)方法我們就分享到這里了,更多有關(guān)于android的相關(guān)內(nèi)容和知識(shí)我們都能在W3Cschool中進(jìn)行學(xué)習(xí)和了解! 

0 人點(diǎn)贊