Android高手筆記-屏幕適配 & UI優(yōu)化
屏幕與適配
- 由于Android碎片化嚴重摇邦,屏幕分辨率千奇百怪巩检,而想要在各種分辨率的設(shè)備上顯示基本一致的效果术幔,適配成本越來越高元咙;
- 屏幕適配究其根本只有兩個問題:
- 在不同尺寸及分辨率上UI的一致(影響著用戶體驗)梯影;
- 從效果圖到UI界面代碼的轉(zhuǎn)化效率(影響著開發(fā)效率);
適配方式:
px
- 在標識尺寸時庶香,Android官方并不推薦使用px(像素)甲棍,因為不同分辨率的屏幕上,同樣像素大小的控件赶掖,在分辨率越高的手機上UI顯示效果越懈忻汀;(因為分辨率越高單位尺寸內(nèi)容納的像素數(shù)越多)
dp
- 所以官方推薦使用dp作為尺寸單位來適配UI奢赂,dp在不同分辨率和尺寸的手機上代表了不同的真實像素陪白;(px和dp的轉(zhuǎn)化關(guān)系:px=dp*(dpi/160),其中dpi是像素密度,是系統(tǒng)軟件上指定的單位尺寸的像素數(shù)量膳灶,往往是寫在系統(tǒng)出廠配置文件的一個固定值咱士,這和屏幕硬件的ppi(物理像素密度)是不同的,是參考了物理像素密度后轧钓,人為指定的一個值序厉,保證了某個區(qū)間內(nèi)的物理像素密度在軟件上都使用同一個值,有利于UI適配的簡化)這就是最原始的Android適配方案:dp+自適應(yīng)布局和weight比例布局毕箍,基本可以解決不同手機上的適配問題弛房;
- 但是這種方案有兩個缺陷:
- 只能適配大部分手機(也做不到和效果圖的完全一致),某些特殊機型仍需單獨適配(比如同樣1920*1080的手機的dpi卻可能不同)而柑;
- 設(shè)計稿到布局代碼的實現(xiàn)效率低(設(shè)計稿的寬高和手機屏幕的寬高不同文捶,px和dp間的轉(zhuǎn)換,往往需要百分比或估算等媒咳,會極大地拉低開發(fā)效率)拄轻;
寬高限定符適配
- 窮舉市面上所有的Android手機的寬高像素值,設(shè)定一個基準的分辨率(最好和設(shè)計稿的寬高一致)伟葫,其他分辨率根據(jù)這個基準分辨率來計算,生成對應(yīng)的dimens文件(可通過java院促、python腳本實現(xiàn)自動生成)筏养,放在不同的尺寸文件夾(values)內(nèi)部斧抱;(插圖)如480320為基準,對于800480的dimens文件:x1=(480/320)1=1.5px; x2=(480/320)2=3px;但是這種方案也有個致命缺陷渐溶,需要精準命中才能適配辉浦,如1440*750的手機如果找不到對應(yīng)的尺寸文件夾就只能用統(tǒng)一默認的dimens文件,UI就可能變形茎辐,而Android手機廠商眾多宪郊,機型更是不可枚舉,所以容錯機制很差拖陆;
鴻洋的AndroidAutoLayout適配方案等動態(tài)計算UI適配框架
- 鴻洋的適配方案也來自于寬高限定符方案的啟發(fā)(目前已經(jīng)停止維護)弛槐;因為框架要在運行時會在onMeasure里面做變換,我們自定義的控件可能會被影響或限制依啰,可能有些特定的控件乎串,需要單獨適配,這里面可能存在的暗坑是不可預見的速警;整個適配工作是有框架完成的叹誉,而不是系統(tǒng)完成的,一旦使用這個框架闷旧,未來一旦遇到很難解決的問題长豁,替換起來是非常麻煩的,而且項目一旦停止維護忙灼,后續(xù)的升級就只能靠你自己了匠襟;
smallestWidth適配 或者叫sw限定符適配
- 指的是Android會識別屏幕可用高度和寬度的較小者的dp值(其實就是手機的寬度值),然后根據(jù)識別到的結(jié)果去資源文件中尋找對應(yīng)限定符的文件夾下的資源文件缀棍。這種機制和上文提到的寬高限定符適配原理上是一樣的宅此,都是系統(tǒng)通過特定的規(guī)則來選擇對應(yīng)的文件舉個例子,小米5的dpi是480,橫向像素是1080px爬范,根據(jù)px=dp(dpi/160)父腕,橫向的dp值是1080/(480/160),也就是360dp,系統(tǒng)就會去尋找是否存在value-sw360dp的文件夾以及對應(yīng)的資源文件;(插圖)smallestWidth限定符適配和寬高限定符適配最大的區(qū)別在于青瀑,有很好的容錯機制璧亮,如果沒有value-sw360dp文件夾,系統(tǒng)會向下尋找斥难,比如離360dp最近的只有value-sw350dp枝嘶,
那么Android就會選擇value-sw350dp文件夾下面的資源文件,這個特性就完美的解決了
上文提到的寬高限定符的容錯問題哑诊。(通過java群扶、python腳本實現(xiàn)自動生成dimens文件)(插圖)
(這種方案的優(yōu)勢是穩(wěn)定性,不會有暗坑) - smallestWidth適配方案有一個小問題,那就是它是在Android 3.2 以后引入的竞阐,Google的本意是用它來適配平板的布局文件(但是實際上顯然用于diemns適配的效果更好)所以這種方案支持的最小版本就是Android3.2了缴饭;
- 還有一個缺陷就是多個dimens文件可能導致apk變大,根據(jù)生成的dimens文件的覆蓋范圍和尺寸范圍骆莹,apk可能會增大300kb-800kb左右颗搂;
- 糗百的拉丁吳大佬生成好的文件https://github.com/ladingwu/dimens_sw
- 所有的適配方案都不是用來取代match_parent,wrap_content的,而是用來完善他們的幕垦;
今日頭條適配方案
通過修改density(density = dpi / 160)值丢氢,強行把所有不同尺寸分辨率的手機的寬度dp值改成一個統(tǒng)一的值,這樣就解決了所有的適配問題這個方案侵入性很低先改,而且也沒有涉及私有API疚察,只是對老項目是不太友好;
如果我們想在所有設(shè)備上顯示完全一致盏道,其實是不現(xiàn)實的稍浆,因為屏幕高寬比不是固定的撰茎,16:9称勋、4:3甚至其他寬高比層出不窮流礁,寬高比不同轻绞,顯示完全一致就不可能了潭枣。但是通常下悼嫉,我們只需要以寬(支持上下滑動的頁面)或高(不支持上下滑動的頁面)一個維度去適配
-
通過閱讀源碼文虏,我們可以得知节视,density 是 DisplayMetrics 中的成員變量论皆,而 DisplayMetrics 實例通過 Resources#getDisplayMetrics 可以獲得益楼,而Resouces通過Activity或者Application的Context獲得;DisplayMetrics 中和適配相關(guān)的幾個變量:
- DisplayMetrics.density 就是上述的density;
- DisplayMetrics.densityDpi 就是上述的dpi;
- DisplayMetrics#scaledDensity 字體的縮放因子点晴,正常情況下和density相等感凤,但是調(diào)節(jié)系統(tǒng)字體大小后會改變這個值;
布局文件中dp的轉(zhuǎn)換,最終都是調(diào)用TypedValue#applyDimension(int unit, float value,DisplayMetrics metrics) (插圖)來進行轉(zhuǎn)換,方法中用到的DisplayMetrics正是從Resources中獲得的粒督;再看看圖片的decode陪竿,BitmapFactory#decodeResourceStream方法(插圖),也是通過 DisplayMetrics 中的值來計算的屠橄;因此族跛,想要滿足上述需求,我們只需要修改 DisplayMetrics 中和 dp 轉(zhuǎn)換相關(guān)的變量即可锐墙;所以得到了下面適配方案:假設(shè)設(shè)計圖寬度是360dp礁哄,以寬維度來適配,那么適配后的 density = 設(shè)備真實寬(單位px) / 360溪北,接下來只需要把我們計算好的 density 在系統(tǒng)中修改下即可桐绒,同時在 Activity#onCreate 方法中調(diào)用下但是會有字體過小的現(xiàn)象夺脾,原因是在上面的適配中,我們忽略了DisplayMetrics#scaledDensity的特殊性掏膏,將DisplayMetrics#scaledDensity和DisplayMetrics#density設(shè)置為同樣的值劳翰,從而某些用戶在系統(tǒng)中修改了字體大小失效了,但是我們還不能直接用原始的scaledDensity馒疹,直接用的話可能導致某些文字超過顯示區(qū)域,因此我們可以通過計算之前scaledDensity和density的比獲得現(xiàn)在的scaledDensity乙墙;但是測試后發(fā)現(xiàn)另外一個問題颖变,就是如果在系統(tǒng)設(shè)置中切換字體,再返回應(yīng)用听想,字體并沒有變化腥刹。于是還得監(jiān)聽下字體切換,調(diào)用 Application#registerComponentCallbacks 注冊下onConfigurationChanged 監(jiān)聽即可汉买;
可以參考https://github.com/Blankj/AndroidUtilCode衔峰,https://github.com/JessYanCoding/AndroidAutoSize這兩個開源庫;
UI優(yōu)化
CPU 與 GPU
- Android的繪制實現(xiàn)主要是借助CPU與GPU結(jié)合刷新機制共同完成的蛙粘。
- 除了屏幕垫卤,UI 渲染還依賴兩個核心的硬件:CPU 與 GPU。
- UI 組件在繪制到屏幕之前出牧,都需要經(jīng)過 Rasterization(柵格化)操作穴肘,而柵格化操作又是一個非常耗時的操作。GPU(Graphic Processing Unit )也就是圖形處理器舔痕,它主要用于處理圖形運算评抚,可以幫助我們加快柵格化操作。
- CPU軟件繪制使用的是 Skia 庫伯复,它是一款能在低端設(shè)備如手機上呈現(xiàn)高質(zhì)量的 2D 跨平臺圖形框架慨代,類似 Chrome、Flutter 內(nèi)部使用的都是 Skia 庫;
OpenGL 與 Vulkan
- 對于硬件繪制啸如,我們通過調(diào)用 OpenGL ES 接口利用 GPU 完成繪制侍匙。OpenGL是一個跨平臺的圖形 API,它為 2D/3D 圖形處理硬件指定了標準軟件接口组底。而 OpenGL ES 是 OpenGL 的子集丈积,專為嵌入式設(shè)備設(shè)計。
- Android 7.0 把 OpenGL ES 升級到最新的 3.2 版本同時债鸡,還添加了對Vulkan的支持江滨。Vulkan 是用于高性能 3D 圖形的低開銷、跨平臺 API厌均。相比 OpenGL ES唬滑,Vulkan 在改善功耗、多核優(yōu)化提升繪圖調(diào)用上有著非常明顯的優(yōu)勢。
- 把應(yīng)用程序圖形渲染過程當作一次繪畫過程晶密,那么繪畫過程中 Android 的各個圖形組件的作用是:
1. 畫筆:Skia 或者 OpenGL擒悬。我們可以用 Skia 畫筆繪制 2D 圖形,也可以用 OpenGL 來繪制 2D/3D 圖形稻艰。正如前面所說懂牧,前者使用 CPU 繪制,后者使用 GPU 繪制尊勿。
2. 畫紙:Surface僧凤。所有的元素都在 Surface 這張畫紙上進行繪制和渲染。在 Android 中元扔,Window 是 View 的容器躯保,每個窗口都會關(guān)聯(lián)一個 Surface。
而 WindowManager 則負責管理這些窗口澎语,并且把它們的數(shù)據(jù)傳遞給 SurfaceFlinger途事。
3. 畫板:Graphic Buffer。Graphic Buffer 緩沖用于應(yīng)用程序圖形的繪制擅羞,在 Android 4.1 之前使用的是雙緩沖機制尸变;在 Android 4.1 之后,使用的是三緩沖機制祟滴。
4. 顯示:SurfaceFlinger振惰。它將 WindowManager 提供的所有 Surface,通過硬件合成器 Hardware Composer 合成并輸出到顯示屏垄懂。
Android 渲染的演進
- 在 Android 3.0 之前骑晶,或者沒有啟用硬件加速時,系統(tǒng)都會使用軟件方式來渲染 UI;
- Androd 3.0 開始草慧,Android 開始支持硬件加速;
- Android 4.0 時桶蛔,默認開啟硬件加速;
- Android 4.1:
- 開啟了Project Butter: 主要包含兩個組成部分,一個是 VSYNC漫谷,一個是 Triple Buffering仔雷。VSYNC信號:對于 Android 4.0,CPU 可能會因為在忙別的事情舔示,導致沒來得及處理 UI 繪制碟婆。 為解決這個問題,Project Buffer 引入了VSYNC惕稻,它類似于時鐘中斷。每收到 VSYNC 中斷俺祠,CPU 會立即準備 Buffer 數(shù)據(jù)公给,由于大部分顯示設(shè)備刷新頻率都是 60Hz(一秒刷新 60 次)借帘,也就是說一幀數(shù)據(jù)的準備工作都要在 16ms 內(nèi)完成。三緩沖機制 Triple Buffering:Android 4.1 之前淌铐,Android 使用雙緩沖機制肺然,CPU、GPU 和顯示設(shè)備都能使用各自的緩沖區(qū)工作腿准,互不影響
- Android 4.1還新增了 Systrace 性能數(shù)據(jù)采樣和分析工具际起。
- Tracer for OpenGL ES 也是 Android 4.1 新增加的工具,它可逐幀释涛、逐函數(shù)的記錄 App 用 OpenGL ES 的繪制過程加叁。它提供了每個 OpenGL 函數(shù)調(diào)用的消耗時間,所以很多時候用來做性能分析唇撬。但因為其強大的記錄功能,在分析渲染問題時展融,當 Traceview窖认、Systrace 都顯得棘手時,還找不到渲染問題所在時告希,此時這個工具就會派上用場了扑浸。
- Android 4.2,系統(tǒng)增加了檢測繪制過度工具燕偶;
- Android 5.0:RenderThread:
- 經(jīng)過 Project Butter 黃油計劃之后喝噪,Android 的渲染性能有了很大的改善。但是不知道你有沒有注意到一個問題指么,雖然我們利用了 GPU 的圖形高性能運算酝惧,但是從計算 DisplayList,到通過 GPU 繪制到 Frame Buffer伯诬,整個計算和繪制都在 UI 主線程中完成晚唇。
- Android 5.0 引入了兩個比較大的改變。一個是引入了 RenderNode 的概念盗似,它對 DisplayList 及一些 View 顯示屬性做了進一步封裝哩陕。另一個是引入了 RenderThread,所有的 GL 命令執(zhí)行都放到這個線程上赫舒,渲染線程在 RenderNode 中存有渲染幀的所有信息悍及,可以做一些屬性動畫,這樣即便主線程有耗時操作的時候也可以保證動畫流暢接癌。
- 還可以開啟 Profile GPU Rendering 檢查心赶。
- Android 6.0 ,在 gxinfo 添加了更詳細的信息扔涧;
- 在 Android 7.0 又對 HWUI 進行了一些重構(gòu)园担,而且支持了 Vulkan届谈;
- 在 Android P 支持了 Vulkun 1.1。
UI 渲染測量
- 測試工具:Profile GPU Rendering 和 Show GPU Overdraw弯汰。
- 問題定位工具:Systrace 和 Tracer for OpenGL ES
- Layout Inspector: AndroidStudio自帶的工具艰山,它的主要作用就是用來查看視圖層級結(jié)構(gòu)的,開啟路徑如下: 點擊Tools工具欄 ->第三欄的Layout Inspector -> 選中當前的進程咏闪;
- Choreographer:用來獲取FPS的曙搬,并且可以用于線上使用,具備實時性鸽嫂,但是僅能在Api 16之后使用纵装,具體的調(diào)用代碼如下:
- Choreographer.getInstance().postFrameCallback();
- 使用Choreographer獲取FPS的完整代碼如下
private long mStartFrameTime = 0; private int mFrameCount = 0; /** * 單次計算FPS使用160毫秒 */ private static final long MONITOR_INTERVAL = 160L; private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L; /** * 設(shè)置計算fps的單位時間間隔1000ms,即fps/s */ private static final long MAX_INTERVAL = 1000L; @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void getFPS() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { return; } Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { if (mStartFrameTime == 0) { mStartFrameTime = frameTimeNanos; } long interval = frameTimeNanos - mStartFrameTime; if (interval > MONITOR_INTERVAL_NANOS) { double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL; // log輸出fps LogUtils.i("當前實時fps值為: " + fps); mFrameCount = 0; mStartFrameTime = 0; } else { ++mFrameCount; } Choreographer.getInstance().postFrameCallback(this); } }); }
- 我們需要排除掉頁面沒有操作的情況,即只在界面存在繪制的時候才做統(tǒng)計据某。我們可以通過 addOnDrawListener 去監(jiān)聽界面是否存在繪制行為: getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
- 在 Android Studio 3.1 之后橡娄,Android 推薦使用Graphics API Debugger(GAPID)來替代 Tracer for OpenGL ES 工具。GAPID 可以說是升級版癣籽,它不僅可以跨平臺挽唉,而且功能更加強大称勋,支持 Vulkan 與回放零院。
- 通過上面的幾個工具,我們可以初步判斷應(yīng)用 UI 渲染的性能是否達標崖媚,例如是否經(jīng)常出現(xiàn)掉幀埂材、掉幀主要發(fā)生在渲染的哪一個階段塑顺、是否存在 Overdraw 等。
- 雖然這些圖形化界面工具非常好用俏险,但是它們難以用在自動化測試場景中严拒,那有哪些測量方法可以用于自動化測量 UI 渲染性能呢?
1. gfxinfo
- gfxinfo可以輸出包含各階段發(fā)生的動畫以及幀相關(guān)的性能信息寡喝,具體命令如下:
- adb shell dumpsys gfxinfo 包名
- 除了渲染的性能之外糙俗,gfxinfo 還可以拿到渲染相關(guān)的內(nèi)存和 View hierarchy 信息。在 Android 6.0 之后预鬓,gxfinfo 命令新增了 framestats 參數(shù)巧骚,可以拿到最近 120 幀每個繪制階段的耗時信息: adb shell dumpsys gfxinfo 包名 framestats
2. SurfaceFlinger
- 除了耗時,我們還比較關(guān)心渲染使用的內(nèi)存格二∨耄可以通過下面的命令拿到系統(tǒng) SurfaceFlinger 相關(guān)的信息:adb shell dumpsys SurfaceFlinger
獲取界面布局耗時
- 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();
}
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
}
- LayoutInflaterCompat.setFactory2
- 上面我們使用了AOP的方式監(jiān)控了Activity的布局加載耗時,那么顶猜,如果我們需要監(jiān)控每一個控件的加載耗時沧奴,該怎么實現(xiàn)呢?
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 使用LayoutInflaterCompat.Factory2全局監(jiān)控Activity界面每一個控件的加載耗時长窄,
// 也可以做全局的自定義控件替換處理滔吠,比如:將TextView全局替換為自定義的TextView纲菌。
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (TextUtils.equals(name, "TextView")) {
// 生成自定義TextView
}
long time = System.currentTimeMillis();
// 1
View view = getDelegate().createView(parent, name, context, attrs);
LogHelper.i(name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
// 2、setFactory2方法需在super.onCreate方法前調(diào)用疮绷,否則無效
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
unBinder = ButterKnife.bind(this);
mActivity = this;
ActivityCollector.getInstance().addActivity(this);
onViewCreated();
initToolbar();
initEventAndData();
}
UI優(yōu)化的常用手段
1. 盡量使用硬件加速
- 之所以不能使用硬件加速翰舌,是因為硬件加速不能支持所有的 Canvas API;
- 如果使用了不支持的 API冬骚,系統(tǒng)就需要通過 CPU 軟件模擬繪制椅贱,這也是漸變、磨砂只冻、圓角等效果渲染性能比較低的原因庇麦。
- SVG 也是一個非常典型的例子,SVG 有很多指令硬件加速都不支持喜德。
- 但我們可以用一個取巧的方法山橄,提前將這些 SVG 轉(zhuǎn)換成 Bitmap 緩存起來,這樣系統(tǒng)就可以更好地使用硬件加速繪制舍悯。
- 同理驾胆,對于其他圓角、漸變等場景贱呐,我們也可以改為 Bitmap 實現(xiàn)。
2. Create View 優(yōu)化
- View 的創(chuàng)建也是在 UI 線程里入桂,對于一些非常復雜的界面奄薇,這部分的耗時不容忽視。包括各種 XML 的隨機讀的 I/O 時間抗愁、解析 XML 的時間馁蒂、生成對象的時間(Framework 會大量使用到反射)。
- 優(yōu)化方式:
- 使用代碼創(chuàng)建;
Button button=new Button(this); button.setBackgroundColor(Color.RED); button.setText("Hello World"); ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null); viewGroup.addView(button);
- 異步創(chuàng)建:那我們能不能在線程提前創(chuàng)建 View蜘腌,實現(xiàn) UI 的預加載嗎沫屡?可以通過又一個非常取巧的方式來實現(xiàn)。在使用線程創(chuàng)建 UI 的時候撮珠,先把線程的 Looper 的 MessageQueue 替換成 UI 線程 Looper 的 Queue沮脖。
public static boolean prepareLooperWithMainThreadQueue(boolean reset) { if (isMainThread()) { return true; } else { ThreadLocal<Looper> threadLocal = (ThreadLocal) ReflectionHelper.getStaticFieldValue(Looper.class, "sThreadLocal"); if (threadLocal == null) { return false; } else { Looper looper = null; if (!reset) { Looper.prepare(); looper = Looper.myLooper(); Object queue = ReflectionHelper.invokeMethod(Looper.getMainLooper(), "getQUeue", new Class[0], new Object[0]); if (!(queue instanceof MessageQueue)) { return false; } } ReflectionHelper.invokeMethod(threadLocal, "set", new Class[]{Object.class}, new Object[]{looper}); return true; } } } // 要注意的是,在創(chuàng)建完 View 后我們需要把線程的 Looper 恢復成原來的芯急。 private static boolean isMainThread() { return Looper.myLooper() == Looper.getMainLooper(); }
- View 重用:ListView勺届、RecycleView 通過 View 的緩存與重用大大地提升渲染性能。因此我們可以參考它們的思想娶耍,實現(xiàn)一套可以在不同 Activity 或者 Fragment 使用的 View 緩存機制免姿。
- AsynclayoutInflater異步創(chuàng)建View
implementation 'com.android.support:asynclayoutinflater:28.0.0' // 內(nèi)部分別使用了IO和反射的方式去加載布局解析器和創(chuàng)建對應(yīng)的View // setContentView(R.layout.activity_main); // 使用AsyncLayoutInflater進行布局的加載 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); // findViewById、視圖操作等 } }); super.onCreate(savedInstanceState);
- AsyncLayoutInflater是通過側(cè)面緩解的方式去緩解布局加載過程中的卡頓榕酒,但是它依然存在一些問題:
- 不能設(shè)置LayoutInflater.Factory胚膊,需要通過自定義AsyncLayoutInflater的方式解決故俐,由于它是一個final,所以需要將代碼直接拷處進行修改紊婉。
- 因為是異步加載药版,所以需要注意在布局加載過程中不能有依賴于主線程的操作。
- Android AsyncLayoutInflater 限制及改進:
/** * 實現(xiàn)異步加載布局的功能肩榕,修改點: * * 1. super.onCreate之前調(diào)用沒有了默認的Factory刚陡; * 2. 排隊過多的優(yōu)化; */ public class AsyncLayoutInflaterPlus { private static final String TAG = "AsyncLayoutInflaterPlus"; private Handler mHandler; private LayoutInflater mInflater; private InflateRunnable mInflateRunnable; // 真正執(zhí)行加載任務(wù)的線程池 private static ExecutorService sExecutor = Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors() - 2)); // InflateRequest pool private static Pools.SynchronizedPool<AsyncLayoutInflaterPlus.InflateRequest> sRequestPool = new Pools.SynchronizedPool<>(10); private Future<?> future; public AsyncLayoutInflaterPlus(@NonNull Context context) { mInflater = new AsyncLayoutInflaterPlus.BasicInflater(context); mHandler = new Handler(mHandlerCallback); } @UiThread public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull CountDownLatch countDownLatch, @NonNull AsyncLayoutInflaterPlus.OnInflateFinishedListener callback) { if (callback == null) { throw new NullPointerException("callback argument may not be null!"); } AsyncLayoutInflaterPlus.InflateRequest request = obtainRequest(); request.inflater = this; request.resid = resid; request.parent = parent; request.callback = callback; request.countDownLatch = countDownLatch; mInflateRunnable = new InflateRunnable(request); future = sExecutor.submit(mInflateRunnable); } public void cancel() { future.cancel(true); } /** * 判斷這個任務(wù)是否已經(jīng)開始執(zhí)行 * * @return */ public boolean isRunning() { return mInflateRunnable.isRunning(); } private Handler.Callback mHandlerCallback = new Handler.Callback() { @Override public boolean handleMessage(Message msg) { AsyncLayoutInflaterPlus.InflateRequest request = (AsyncLayoutInflaterPlus.InflateRequest) msg.obj; if (request.view == null) { request.view = mInflater.inflate( request.resid, request.parent, false); } request.callback.onInflateFinished( request.view, request.resid, request.parent); request.countDownLatch.countDown(); releaseRequest(request); return true; } }; public interface OnInflateFinishedListener { void onInflateFinished(View view, int resid, ViewGroup parent); } private class InflateRunnable implements Runnable { private InflateRequest request; private boolean isRunning; public InflateRunnable(InflateRequest request) { this.request = request; } @Override public void run() { isRunning = true; try { request.view = request.inflater.mInflater.inflate( request.resid, request.parent, false); } catch (RuntimeException ex) { // Probably a Looper failure, retry on the UI thread Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI" + " thread", ex); } Message.obtain(request.inflater.mHandler, 0, request) .sendToTarget(); } public boolean isRunning() { return isRunning; } } private static class InflateRequest { AsyncLayoutInflaterPlus inflater; ViewGroup parent; int resid; View view; AsyncLayoutInflaterPlus.OnInflateFinishedListener callback; CountDownLatch countDownLatch; InflateRequest() { } } private static class BasicInflater extends LayoutInflater { private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app." }; BasicInflater(Context context) { super(context); if (context instanceof AppCompatActivity) { // 加上這些可以保證AppCompatActivity的情況下株汉,super.onCreate之前 // 使用AsyncLayoutInflater加載的布局也擁有默認的效果 AppCompatDelegate appCompatDelegate = ((AppCompatActivity) context).getDelegate(); if (appCompatDelegate instanceof LayoutInflater.Factory2) { LayoutInflaterCompat.setFactory2(this, (LayoutInflater.Factory2) appCompatDelegate); } } } @Override public LayoutInflater cloneInContext(Context newContext) { return new AsyncLayoutInflaterPlus.BasicInflater(newContext); } @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack // at it. } } return super.onCreateView(name, attrs); } } public AsyncLayoutInflaterPlus.InflateRequest obtainRequest() { AsyncLayoutInflaterPlus.InflateRequest obj = sRequestPool.acquire(); if (obj == null) { obj = new AsyncLayoutInflaterPlus.InflateRequest(); } return obj; } public void releaseRequest(AsyncLayoutInflaterPlus.InflateRequest obj) { obj.callback = null; obj.inflater = null; obj.parent = null; obj.resid = 0; obj.view = null; sRequestPool.release(obj); } }
- X2C: 框架保留了XML的優(yōu)點筐乳,并解決了其IO操作和反射的性能問題。開發(fā)人員只需要正常寫XML代碼即可乔妈,在編譯期蝙云,X2C會利用APT工具將XML代碼翻譯為Java代碼。
3. measure/layout 優(yōu)化
- 渲染流程中 measure 和 layout 也是需要 CPU 在主線程執(zhí)行的;
- 優(yōu)化方法:減少 UI 布局層次路召,優(yōu)化 layout 的開銷勃刨,盡量不要重復去設(shè)置背景
- 布局優(yōu)化:
- 單層布局:盡量選擇LinearLayout或FrameLayout,而少用 RelativeLayout股淡,應(yīng)為RelativeLayout功能較復雜身隐,更耗性能;
但從程序擴展性的角度看唯灵,更傾向于RelativeLayout - 多層布局:布局較復雜時贾铝,RelativeLayout能夠有效的減少布局層級
- <include/>標簽:實現(xiàn)布局文件的復用,如app自定義的TitleBar
只支持 layout_xx 和id屬性埠帕,當include和被包含布局的根標簽都指定了id時垢揩,以include為準;指定layout_xx屬性時敛瓷,
必須也要指定layout_width和layout_height叁巨,否則無法生效 - <merge/>標簽:在UI的結(jié)構(gòu)優(yōu)化中起著非常重要的作用,它可以刪減多余的層級呐籽,優(yōu)化UI锋勺。
<merge/>多用于替換FrameLayout或者當一個布局包含另一個時,<merge/>標簽消除視圖層次結(jié)構(gòu)中多余的視圖組绝淡。
例如你的主布局文件是垂直布局宙刘,引入了一個垂直布局的include,這是如果include布局使用的LinearLayout就沒意義了牢酵,
使用的話反而減慢你的UI表現(xiàn)悬包。這時可以使用<merge/>標簽優(yōu)化。 - <ViewStub/>標簽:懶加載馍乙,不會影響UI初始化時的性能布近;
各種不常用的布局垫释,如進度條、顯示錯誤消息等可以使用ViewStub標簽撑瞧,以減少內(nèi)存使用量棵譬,加快渲染速度 - 使用 style 來定義通用的屬性,從而重復利用代碼预伺,減少代碼量
- 封裝組合view實現(xiàn)view復用
- 使用 LinearLayoutCompat 組件來實現(xiàn)線性布局元素之間的分割線订咸,從而減少了使用View來實現(xiàn)分割線效果
- 單層布局:盡量選擇LinearLayout或FrameLayout,而少用 RelativeLayout股淡,應(yīng)為RelativeLayout功能較復雜身隐,更耗性能;
- 布局優(yōu)化:
- Litho:異步布局
- Litho是 Facebook 開源的聲明式 Android UI 渲染框架,它是基于另外一個 Facebook 開源的布局引擎Yoga開發(fā)的酬诀。
1. 配置Litho的相關(guān)依賴 // 項目下 repositories { jcenter() } // module下 dependencies { // ... // Litho implementation 'com.facebook.litho:litho-core:0.33.0' implementation 'com.facebook.litho:litho-widget:0.33.0' annotationProcessor 'com.facebook.litho:litho-processor:0.33.0' // SoLoader implementation 'com.facebook.soloader:soloader:0.5.1' // For integration with Fresco implementation 'com.facebook.litho:litho-fresco:0.33.0' // For testing testImplementation 'com.facebook.litho:litho-testing:0.33.0' // Sections (options脏嚷,用來聲明去構(gòu)建一個list) implementation 'com.facebook.litho:litho-sections-core:0.33.0' implementation 'com.facebook.litho:litho-sections-widget:0.33.0' compileOnly 'com.facebook.litho:litho-sections-annotations:0.33.0' annotationProcessor 'com.facebook.litho:litho-sections-processor:0.33.0' } 2. Application下的onCreate方法中初始化SoLoader: @Override public void onCreate() { super.onCreate(); SoLoader.init(this, false); //Litho使用了Yoga進行布局,而Yoga包含有native依賴瞒御,在Soloader.init方法中對這些native依賴進行了加載父叙。 } 3. 在Activity的onCreate方法中添加如下代碼即可顯示單個的文本視圖: // 1、將Activity的Context對象保存到ComponentContext中肴裙,并同時初始化 // 一個資源解析者實例ResourceResolver供其余組件使用趾唱。 ComponentContext componentContext = new ComponentContext(this); // 2、Text內(nèi)部使用建造者模式以實現(xiàn)組件屬性的鏈式調(diào)用蜻懦,下面設(shè)置的text甜癞、 // TextColor等屬性在Litho中被稱為Prop,此概念引申字React宛乃。 Text lithoText = Text.create(componentContext) .text("Litho text") .textSizeDip(64) .textColor(ContextCompat.getColor(this, R.color.light_deep_red)) .build(); // 3带欢、設(shè)置一個LithoView去展示Text組件:LithoView.create內(nèi)部新建了一個 // LithoView實例,并用給定的Component(lithoText)進行初始化 setContentView(LithoView.create(componentContext, lithoText)); 4. 使用自定義Component Litho中的視圖單元叫做Component烤惊,即組件,它的設(shè)計理念來源于React組件化的思想吁朦。 每個組件持有描述一個視圖單元所必須的屬性與狀態(tài)柒室,用于視圖布局的計算工作。 視圖最終的繪制工作是由組件指定的繪制單元(View或Drawable)來完成的逗宜。 @LayoutSpec public class ListItemSpec { @OnCreateLayout static Component onCreateLayout(ComponentContext context) { // Column的作用類似于HTML中的<div>標簽 return Column.create(context) .paddingDip(YogaEdge.ALL, 16) .backgroundColor(Color.WHITE) .child(Text.create(context) .text("Litho Study") .textSizeSp(36) .textColor(Color.BLUE) .build()) .child(Text.create(context) .text("JsonChao") .textSizeSp(24) .textColor(Color.MAGENTA) .build()) .build(); } } // 2雄右、構(gòu)建ListItem組件 ListItem listItem = ListItem.create(componentContext).build();
- Flutter:自己的布局 + 渲染引擎
- RenderThread 與 RenderScript
- Android 5.0,系統(tǒng)增加了 RenderThread纺讲,對于 ViewPropertyAnimator 和 CircularReveal 動畫擂仍,我們可以使用RenderThead 實現(xiàn)動畫的異步渲染。
參考文章
- Android開發(fā)高手課-UI 優(yōu)化(上):UI 渲染的幾個關(guān)鍵概念
- Android開發(fā)高手課-UI 優(yōu)化(下):如何優(yōu)化 UI 渲染熬甚?
- Android 目前穩(wěn)定高效的UI適配方案
- 一種極低成本的Android屏幕適配方式
- Android 屏幕適配終結(jié)者
- 騷年你的屏幕適配方式該升級了!-今日頭條適配方案
- 深入探索Android布局優(yōu)化
- Vulkan - 高性能渲染
- Android Project Butter分析
- Android 屏幕繪制機制及硬件加速
- Litho
- Yoga
- RenderThread:異步渲染動畫
- RenderScript渲染利器
- RenderScript :簡單而快速的圖像處理
- Android AsyncLayoutInflater 限制及改進