作為程序員我們在完成項目的同時還需要考慮到各種代碼的優(yōu)化,那么今天我們來說下“對于使用android布局可以進行哪些優(yōu)化?”這個問題吧!
前言
Android的繪制優(yōu)化其實可以分為兩個部分,即布局(UI)優(yōu)化和卡頓優(yōu)化,而布局優(yōu)化的核心問題就是要解決因布局渲染性能不佳而導(dǎo)致應(yīng)用卡頓的問題,所以它可以認為是卡頓優(yōu)化的一個子集。
本文主要包括以下內(nèi)容
- 為什么要進行布局優(yōu)化及android繪制,布局加載原理
- 獲取布局文件加載耗時的方法
- 介紹一些布局優(yōu)化的手段與方法
- 一些常規(guī)優(yōu)化手段
為什么要進行布局優(yōu)化?
為什么要進行布局優(yōu)化?答案是顯而易見的,如果布局嵌套過深,或者其他原因?qū)е虏季咒秩拘阅懿患?,可能會?dǎo)致應(yīng)用卡頓 那么布局到底是如何導(dǎo)致渲染性能不佳的呢?首先我們應(yīng)該了解下android繪制原理與布局加載原理
android繪制原理
Android的屏幕刷新中涉及到最重要的三個概念(為便于理解,這里先做簡單介紹)
- CPU:執(zhí)行應(yīng)用層的measure、layout、draw等操作,繪制完成后將數(shù)據(jù)提交給GPU
- GPU:進一步處理數(shù)據(jù),并將數(shù)據(jù)緩存起來
- 屏幕:由一個個像素點組成,以固定的頻率(16.6ms,即1秒60幀)從緩沖區(qū)中取出數(shù)據(jù)來填充像素點
總結(jié)一句話就是:CPU 繪制后提交數(shù)據(jù)、GPU 進一步處理和緩存數(shù)據(jù)、最后屏幕從緩沖區(qū)中讀取數(shù)據(jù)并顯示
雙緩沖機制
看完上面的流程圖,我們很容易想到一個問題,屏幕是以16.6ms的固定頻率進行刷新的,但是我們應(yīng)用層觸發(fā)繪制的時機是完全隨機的(比如我們隨時都可以觸摸屏幕觸發(fā)繪制). 如果在GPU向緩沖區(qū)寫入數(shù)據(jù)的同時,屏幕也在向緩沖區(qū)讀取數(shù)據(jù),會發(fā)生什么情況呢?有可能屏幕上就會出現(xiàn)一部分是前一幀的畫面,一部分是另一幀的畫面,這顯然是無法接受的,那怎么解決這個問題呢?
所以,在屏幕刷新中,Android系統(tǒng)引入了雙緩沖機制
GPU只向Back Buffer中寫入繪制數(shù)據(jù),且GPU會定期交換Back Buffer和Frame Buffer,交換的頻率也是60次/秒,這就與屏幕的刷新頻率保持了同步。
雖然我們引入了雙緩沖機制,但是我們知道,當布局比較復(fù)雜,或設(shè)備性能較差的時候,CPU并不能保證在16.6ms內(nèi)就完成繪制數(shù)據(jù)的計算,所以這里系統(tǒng)又做了一個處理。當你的應(yīng)用正在往Back Buffer中填充數(shù)據(jù)時,系統(tǒng)會將Back Buffer鎖定。如果到了GPU交換兩個Buffer的時間點,你的應(yīng)用還在往Back Buffer中填充數(shù)據(jù),GPU會發(fā)現(xiàn)Back Buffer被鎖定了,它會放棄這次交換。
這樣做的后果就是手機屏幕仍然顯示原先的圖像,這就是我們常常說的掉幀
布局加載原理
由上面可知,導(dǎo)致掉幀的原因是CPU無法在16.6ms內(nèi)完成繪制數(shù)據(jù)的計算。而之所以布局加載可能會導(dǎo)致掉幀,正是因為它在主線程上進行了耗時操作,可能導(dǎo)致CPU無法按時完成數(shù)據(jù)計算
布局加載主要通過setContentView來實現(xiàn),我們就不在這里貼源碼了,一起來看看它的時序圖
我們可以看到,在setContentView中主要有兩個耗時操作
- 解析xml,獲取XmlResourceParser,這是IO過程
- 通過createViewFromTag,創(chuàng)建View對象,用到了反射
以上兩點就是布局加載可能導(dǎo)致卡頓的原因,也是布局的性能瓶頸
獲取布局文件加載耗時的方法
我們?nèi)绻枰獌?yōu)化布局卡頓問題,首先最重要的就是:確定定量標準 所以我們首先介紹幾種獲取布局文件加載耗時的方法
常規(guī)獲取
首先介紹一下常規(guī)方法
val start = System.currentTimeMillis() setContentView(R.layout.activity_layout_optimize) val inflateTime = System.currentTimeMillis() - start
這種方法很簡單,因為setContentView是同步方法,如果想要計算耗時,直接將前后時間計算相減即可得到結(jié)果了
AOP(Aspectj,ASM)
上面的方式雖然簡單,但是卻不夠優(yōu)雅,同時代碼有侵入性,如果要對所有Activity測量時,就需要在基類中復(fù)寫相關(guān)方法了,比較麻煩了 下面介紹一種AOP的方式計算耗時
@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,比較簡單,上面的注解的意思是在setContentView方法執(zhí)行內(nèi)部去調(diào)用我們寫好的getSetContentViewTime方法 這樣就可以獲取相應(yīng)的耗時 我們可以看下打印的日志
I/aop inflate: AppCompatActivity.setContentView(..) cost 69
I/aop inflate: AppCompatActivity.setContentView(..) cost 25
這樣就可以實現(xiàn)無侵入的監(jiān)控每個頁面布局加載的耗時 具體源碼可見文末
獲取任一控件耗時
有時為了更精確的知道到底是哪個控件加載耗時,比如我們新添加了自定義View,需要監(jiān)控它的性能 我們可以利用setFactory2來監(jiān)聽每個控件的加載耗時 首先我們來回顧下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; }
在真正進行反射實例化xml結(jié)點前,會調(diào)用mFactory2的onCreateView方法 這樣如果我們重寫onCreateView方法,在其前后加上耗時統(tǒng)計,即可獲取每個控件的加載耗時
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,我們只是其之前與之后做了埋點 注意,initItemInflateListener需要在onCreate之前調(diào)用 這樣就可以比較方便地實現(xiàn)監(jiān)聽每個控件的加載耗時
布局加載優(yōu)化的一些方法介紹
布局加載慢的主要原因有兩個,一個是IO,一個是反射 所以我們的優(yōu)化思路一般有兩個
- 側(cè)面緩解(異步加載)
- 根本解決(不需要IO,反射過程,如X2C,Anko,Compose等)
AsyncLayoutInflater方案
AsyncLayoutInflater 是來幫助做異步加載 layout 的,inflate(int, ViewGroup, OnInflateFinishedListener) 方法運行結(jié)束之后 OnInflateFinishedListener 會在主線程回調(diào)返回 View;這樣做旨在 UI 的懶加載或者對用戶操作的高響應(yīng)。
簡單的說我們知道默認情況下 setContentView 函數(shù)是在 UI 線程執(zhí)行的,其中有一系列的耗時動作:Xml的解析、View的反射創(chuàng)建等過程同樣是在UI線程執(zhí)行的,AsyncLayoutInflater 就是來幫我們把這些過程以異步的方式執(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)點在于將UI加載過程遷移到了子線程,保證了UI線程的高響應(yīng) 缺點在于犧牲了易用性,同時如果在初始化過程中調(diào)用了UI可能會導(dǎo)致崩潰
X2C方案
X2C是掌閱開源的一套布局加載框架 它的主要是思路是在編譯期,將需要翻譯的layout翻譯生成對應(yīng)的java文件,這樣對于開發(fā)人員來說寫布局還是寫原來的xml,但對于程序來說,運行時加載的是對應(yīng)的java文件。這就將運行時的開銷轉(zhuǎn)移到了編譯時 如下所示,原始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; } }
使用時如下所示,使用X2C.setContentView替代原始的setContentView即可
// this.setContentView(R.layout.activity_main); X2C.setContentView(this, R.layout.activity_main);
X2C優(yōu)點
- 在保留xml的同時,又解決了它帶來的性能問題
- 據(jù)X2C統(tǒng)計,加載耗時可以縮小到原來的1/3
X2C問題
- 部分屬性不能通過代碼設(shè)置,Java不兼容
- 將加載時間轉(zhuǎn)移到了編譯期,增加了編譯期耗時
- 不支持kotlin-android-extensions插件,犧牲了部分易用性
Anko方案
Anko是JetBrains開發(fā)的一個強大的庫,支持使用kotlin DSL的方式來寫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實現(xiàn)布局,它比我們使用Java動態(tài)創(chuàng)建布局方便很多,主要是更簡潔,它和擁有xml創(chuàng)建布局的層級關(guān)系,能讓我們更容易閱讀 同時,它去除了IO與反射過程,性能更好,以下是Anko與XML的性能對比
不過由于AnKo已經(jīng)停止維護了,這里不建議大家使用,了解原理即可 AnKo建議大家使用Jetpack Compose來替代使用
Compose方案
Compose 是 Jetpack 中的一個新成員,是 Android 團隊在2019年I/O大會上公布的新的UI庫,目前處于Beta階段 Compose使用純kotlin開發(fā),使用簡潔方便,但它并不是像Anko一樣對ViewGroup的封裝 Compose 并不是對 View 和 ViewGroup 這套系統(tǒng)做了個上層包裝來讓寫法更簡單,而是完全拋棄了這套系統(tǒng),自己把整個的渲染機制從里到外做了個全新的。
可以確定的是,Compose是取代XML的官方方案
Compose的主要優(yōu)點就在于它的簡單好用,具體來說就是兩點
- 它的聲明式 UI
- 去掉了 xml,只使用 Kotlin 一種語言
由于本文并不是介紹Compose的,所以就不繼續(xù)介紹Compose了,總得來說,Compose是未來android UI開發(fā)的方向,讀者可以自行查閱相關(guān)資料
一些常規(guī)優(yōu)化手段
上面介紹了一些改動比較大的方案,其實我們在實際開發(fā)中也有些常規(guī)的方法可以優(yōu)化布局加載 比如優(yōu)化布局層級,避免過度繪制等,這些簡單的手段可能正是可以應(yīng)用到項目中的
優(yōu)化布局層級及復(fù)雜度
- 使用ConstraintLayout,可以實現(xiàn)完全扁平化的布局,減少層級
- RelativeLayout本身盡量不要嵌套使用
- 嵌套的LinearLayout中,盡量不要使用weight,因為weight會重新測量兩次
- 推薦使用merge標簽,可以減少一個層級
- 使用ViewStub延遲加載
避免過度繪制
- 去掉多余背景色,減少復(fù)雜shape的使用
- 避免層級疊加
- 自定義View使用clipRect屏蔽被遮蓋View繪制
總結(jié)
那么對于“對于使用android布局可以進行哪些優(yōu)化?”這個問題的優(yōu)化和相關(guān)實現(xiàn)方法我們就分享到這里了,更多有關(guān)于android的相關(guān)內(nèi)容和知識我們都能在W3Cschool中進行學(xué)習(xí)和了解!