Android高手筆記-屏幕適配 & UI優化
屏幕與適配
- 由于Android碎片化嚴重,屏幕分辨率千奇百怪,而想要在各種分辨率的設備上顯示基本一致的效果,適配成本越來越高;
- 屏幕適配究其根本只有兩個問題:
- 在不同尺寸及分辨率上UI的一致(影響著用戶體驗);
- 從效果圖到UI界面代碼的轉化效率(影響著開發效率);
適配方式:
px
- 在標識尺寸時,Android官方并不推薦使用px(像素),因為不同分辨率的屏幕上,同樣像素大小的控件,在分辨率越高的手機上UI顯示效果越小;(因為分辨率越高單位尺寸內容納的像素數越多)
dp
- 所以官方推薦使用dp作為尺寸單位來適配UI,dp在不同分辨率和尺寸的手機上代表了不同的真實像素;(px和dp的轉化關系:px=dp*(dpi/160),其中dpi是像素密度,是系統軟件上指定的單位尺寸的像素數量,往往是寫在系統出廠配置文件的一個固定值,這和屏幕硬件的ppi(物理像素密度)是不同的,是參考了物理像素密度后,人為指定的一個值,保證了某個區間內的物理像素密度在軟件上都使用同一個值,有利于UI適配的簡化)這就是最原始的Android適配方案:dp+自適應布局和weight比例布局,基本可以解決不同手機上的適配問題;
- 但是這種方案有兩個缺陷:
- 只能適配大部分手機(也做不到和效果圖的完全一致),某些特殊機型仍需單獨適配(比如同樣1920*1080的手機的dpi卻可能不同);
- 設計稿到布局代碼的實現效率低(設計稿的寬高和手機屏幕的寬高不同,px和dp間的轉換,往往需要百分比或估算等,會極大地拉低開發效率);
寬高限定符適配
- 窮舉市面上所有的Android手機的寬高像素值,設定一個基準的分辨率(最好和設計稿的寬高一致),其他分辨率根據這個基準分辨率來計算,生成對應的dimens文件(可通過java、python腳本實現自動生成),放在不同的尺寸文件夾(values)內部;(插圖)如480320為基準,對于800480的dimens文件:x1=(480/320)1=1.5px; x2=(480/320)2=3px;但是這種方案也有個致命缺陷,需要精準命中才能適配,如1440*750的手機如果找不到對應的尺寸文件夾就只能用統一默認的dimens文件,UI就可能變形,而Android手機廠商眾多,機型更是不可枚舉,所以容錯機制很差;
鴻洋的AndroidAutoLayout適配方案等動態計算UI適配框架
- 鴻洋的適配方案也來自于寬高限定符方案的啟發(目前已經停止維護);因為框架要在運行時會在onMeasure里面做變換,我們自定義的控件可能會被影響或限制,可能有些特定的控件,需要單獨適配,這里面可能存在的暗坑是不可預見的;整個適配工作是有框架完成的,而不是系統完成的,一旦使用這個框架,未來一旦遇到很難解決的問題,替換起來是非常麻煩的,而且項目一旦停止維護,后續的升級就只能靠你自己了;
smallestWidth適配 或者叫sw限定符適配
- 指的是Android會識別屏幕可用高度和寬度的較小者的dp值(其實就是手機的寬度值),然后根據識別到的結果去資源文件中尋找對應限定符的文件夾下的資源文件。這種機制和上文提到的寬高限定符適配原理上是一樣的,都是系統通過特定的規則來選擇對應的文件舉個例子,小米5的dpi是480,橫向像素是1080px,根據px=dp(dpi/160),橫向的dp值是1080/(480/160),也就是360dp,系統就會去尋找是否存在value-sw360dp的文件夾以及對應的資源文件;(插圖)smallestWidth限定符適配和寬高限定符適配最大的區別在于,有很好的容錯機制,如果沒有value-sw360dp文件夾,系統會向下尋找,比如離360dp最近的只有value-sw350dp,
那么Android就會選擇value-sw350dp文件夾下面的資源文件,這個特性就完美的解決了
上文提到的寬高限定符的容錯問題。(通過java、python腳本實現自動生成dimens文件)(插圖)
(這種方案的優勢是穩定性,不會有暗坑) - smallestWidth適配方案有一個小問題,那就是它是在Android 3.2 以后引入的,Google的本意是用它來適配平板的布局文件(但是實際上顯然用于diemns適配的效果更好)所以這種方案支持的最小版本就是Android3.2了;
- 還有一個缺陷就是多個dimens文件可能導致apk變大,根據生成的dimens文件的覆蓋范圍和尺寸范圍,apk可能會增大300kb-800kb左右;
- 糗百的拉丁吳大佬生成好的文件https://github.com/ladingwu/dimens_sw
- 所有的適配方案都不是用來取代match_parent,wrap_content的,而是用來完善他們的;
今日頭條適配方案
通過修改density(density = dpi / 160)值,強行把所有不同尺寸分辨率的手機的寬度dp值改成一個統一的值,這樣就解決了所有的適配問題這個方案侵入性很低,而且也沒有涉及私有API,只是對老項目是不太友好;
如果我們想在所有設備上顯示完全一致,其實是不現實的,因為屏幕高寬比不是固定的,16:9、4:3甚至其他寬高比層出不窮,寬高比不同,顯示完全一致就不可能了。但是通常下,我們只需要以寬(支持上下滑動的頁面)或高(不支持上下滑動的頁面)一個維度去適配
-
通過閱讀源碼,我們可以得知,density 是 DisplayMetrics 中的成員變量,而 DisplayMetrics 實例通過 Resources#getDisplayMetrics 可以獲得,而Resouces通過Activity或者Application的Context獲得;DisplayMetrics 中和適配相關的幾個變量:
- DisplayMetrics.density 就是上述的density;
- DisplayMetrics.densityDpi 就是上述的dpi;
- DisplayMetrics#scaledDensity 字體的縮放因子,正常情況下和density相等,但是調節系統字體大小后會改變這個值;
布局文件中dp的轉換,最終都是調用TypedValue#applyDimension(int unit, float value,DisplayMetrics metrics) (插圖)來進行轉換,方法中用到的DisplayMetrics正是從Resources中獲得的;再看看圖片的decode,BitmapFactory#decodeResourceStream方法(插圖),也是通過 DisplayMetrics 中的值來計算的;因此,想要滿足上述需求,我們只需要修改 DisplayMetrics 中和 dp 轉換相關的變量即可;所以得到了下面適配方案:假設設計圖寬度是360dp,以寬維度來適配,那么適配后的 density = 設備真實寬(單位px) / 360,接下來只需要把我們計算好的 density 在系統中修改下即可,同時在 Activity#onCreate 方法中調用下但是會有字體過小的現象,原因是在上面的適配中,我們忽略了DisplayMetrics#scaledDensity的特殊性,將DisplayMetrics#scaledDensity和DisplayMetrics#density設置為同樣的值,從而某些用戶在系統中修改了字體大小失效了,但是我們還不能直接用原始的scaledDensity,直接用的話可能導致某些文字超過顯示區域,因此我們可以通過計算之前scaledDensity和density的比獲得現在的scaledDensity;但是測試后發現另外一個問題,就是如果在系統設置中切換字體,再返回應用,字體并沒有變化。于是還得監聽下字體切換,調用 Application#registerComponentCallbacks 注冊下onConfigurationChanged 監聽即可;
可以參考https://github.com/Blankj/AndroidUtilCode,https://github.com/JessYanCoding/AndroidAutoSize這兩個開源庫;
UI優化
CPU 與 GPU
- Android的繪制實現主要是借助CPU與GPU結合刷新機制共同完成的。
- 除了屏幕,UI 渲染還依賴兩個核心的硬件:CPU 與 GPU。
- UI 組件在繪制到屏幕之前,都需要經過 Rasterization(柵格化)操作,而柵格化操作又是一個非常耗時的操作。GPU(Graphic Processing Unit )也就是圖形處理器,它主要用于處理圖形運算,可以幫助我們加快柵格化操作。
- CPU軟件繪制使用的是 Skia 庫,它是一款能在低端設備如手機上呈現高質量的 2D 跨平臺圖形框架,類似 Chrome、Flutter 內部使用的都是 Skia 庫;
OpenGL 與 Vulkan
- 對于硬件繪制,我們通過調用 OpenGL ES 接口利用 GPU 完成繪制。OpenGL是一個跨平臺的圖形 API,它為 2D/3D 圖形處理硬件指定了標準軟件接口。而 OpenGL ES 是 OpenGL 的子集,專為嵌入式設備設計。
- Android 7.0 把 OpenGL ES 升級到最新的 3.2 版本同時,還添加了對Vulkan的支持。Vulkan 是用于高性能 3D 圖形的低開銷、跨平臺 API。相比 OpenGL ES,Vulkan 在改善功耗、多核優化提升繪圖調用上有著非常明顯的優勢。
- 把應用程序圖形渲染過程當作一次繪畫過程,那么繪畫過程中 Android 的各個圖形組件的作用是:
1. 畫筆:Skia 或者 OpenGL。我們可以用 Skia 畫筆繪制 2D 圖形,也可以用 OpenGL 來繪制 2D/3D 圖形。正如前面所說,前者使用 CPU 繪制,后者使用 GPU 繪制。
2. 畫紙:Surface。所有的元素都在 Surface 這張畫紙上進行繪制和渲染。在 Android 中,Window 是 View 的容器,每個窗口都會關聯一個 Surface。
而 WindowManager 則負責管理這些窗口,并且把它們的數據傳遞給 SurfaceFlinger。
3. 畫板:Graphic Buffer。Graphic Buffer 緩沖用于應用程序圖形的繪制,在 Android 4.1 之前使用的是雙緩沖機制;在 Android 4.1 之后,使用的是三緩沖機制。
4. 顯示:SurfaceFlinger。它將 WindowManager 提供的所有 Surface,通過硬件合成器 Hardware Composer 合成并輸出到顯示屏。
Android 渲染的演進
- 在 Android 3.0 之前,或者沒有啟用硬件加速時,系統都會使用軟件方式來渲染 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 數據,由于大部分顯示設備刷新頻率都是 60Hz(一秒刷新 60 次),也就是說一幀數據的準備工作都要在 16ms 內完成。三緩沖機制 Triple Buffering:Android 4.1 之前,Android 使用雙緩沖機制,CPU、GPU 和顯示設備都能使用各自的緩沖區工作,互不影響
- Android 4.1還新增了 Systrace 性能數據采樣和分析工具。
- Tracer for OpenGL ES 也是 Android 4.1 新增加的工具,它可逐幀、逐函數的記錄 App 用 OpenGL ES 的繪制過程。它提供了每個 OpenGL 函數調用的消耗時間,所以很多時候用來做性能分析。但因為其強大的記錄功能,在分析渲染問題時,當 Traceview、Systrace 都顯得棘手時,還找不到渲染問題所在時,此時這個工具就會派上用場了。
- Android 4.2,系統增加了檢測繪制過度工具;
- Android 5.0:RenderThread:
- 經過 Project Butter 黃油計劃之后,Android 的渲染性能有了很大的改善。但是不知道你有沒有注意到一個問題,雖然我們利用了 GPU 的圖形高性能運算,但是從計算 DisplayList,到通過 GPU 繪制到 Frame Buffer,整個計算和繪制都在 UI 主線程中完成。
- Android 5.0 引入了兩個比較大的改變。一個是引入了 RenderNode 的概念,它對 DisplayList 及一些 View 顯示屬性做了進一步封裝。另一個是引入了 RenderThread,所有的 GL 命令執行都放到這個線程上,渲染線程在 RenderNode 中存有渲染幀的所有信息,可以做一些屬性動畫,這樣即便主線程有耗時操作的時候也可以保證動畫流暢。
- 還可以開啟 Profile GPU Rendering 檢查。
- Android 6.0 ,在 gxinfo 添加了更詳細的信息;
- 在 Android 7.0 又對 HWUI 進行了一些重構,而且支持了 Vulkan;
- 在 Android P 支持了 Vulkun 1.1。
UI 渲染測量
- 測試工具:Profile GPU Rendering 和 Show GPU Overdraw。
- 問題定位工具:Systrace 和 Tracer for OpenGL ES
- Layout Inspector: AndroidStudio自帶的工具,它的主要作用就是用來查看視圖層級結構的,開啟路徑如下: 點擊Tools工具欄 ->第三欄的Layout Inspector -> 選中當前的進程;
- Choreographer:用來獲取FPS的,并且可以用于線上使用,具備實時性,但是僅能在Api 16之后使用,具體的調用代碼如下:
- 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; /** * 設置計算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); } }); }
- 我們需要排除掉頁面沒有操作的情況,即只在界面存在繪制的時候才做統計。我們可以通過 addOnDrawListener 去監聽界面是否存在繪制行為: getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
- 在 Android Studio 3.1 之后,Android 推薦使用Graphics API Debugger(GAPID)來替代 Tracer for OpenGL ES 工具。GAPID 可以說是升級版,它不僅可以跨平臺,而且功能更加強大,支持 Vulkan 與回放。
- 通過上面的幾個工具,我們可以初步判斷應用 UI 渲染的性能是否達標,例如是否經常出現掉幀、掉幀主要發生在渲染的哪一個階段、是否存在 Overdraw 等。
- 雖然這些圖形化界面工具非常好用,但是它們難以用在自動化測試場景中,那有哪些測量方法可以用于自動化測量 UI 渲染性能呢?
1. gfxinfo
- gfxinfo可以輸出包含各階段發生的動畫以及幀相關的性能信息,具體命令如下:
- adb shell dumpsys gfxinfo 包名
- 除了渲染的性能之外,gfxinfo 還可以拿到渲染相關的內存和 View hierarchy 信息。在 Android 6.0 之后,gxfinfo 命令新增了 framestats 參數,可以拿到最近 120 幀每個繪制階段的耗時信息: adb shell dumpsys gfxinfo 包名 framestats
2. SurfaceFlinger
- 除了耗時,我們還比較關心渲染使用的內存。可以通過下面的命令拿到系統 SurfaceFlinger 相關的信息: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的方式監控了Activity的布局加載耗時,那么,如果我們需要監控每一個控件的加載耗時,該怎么實現呢?
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 使用LayoutInflaterCompat.Factory2全局監控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方法前調用,否則無效
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
unBinder = ButterKnife.bind(this);
mActivity = this;
ActivityCollector.getInstance().addActivity(this);
onViewCreated();
initToolbar();
initEventAndData();
}
UI優化的常用手段
1. 盡量使用硬件加速
- 之所以不能使用硬件加速,是因為硬件加速不能支持所有的 Canvas API;
- 如果使用了不支持的 API,系統就需要通過 CPU 軟件模擬繪制,這也是漸變、磨砂、圓角等效果渲染性能比較低的原因。
- SVG 也是一個非常典型的例子,SVG 有很多指令硬件加速都不支持。
- 但我們可以用一個取巧的方法,提前將這些 SVG 轉換成 Bitmap 緩存起來,這樣系統就可以更好地使用硬件加速繪制。
- 同理,對于其他圓角、漸變等場景,我們也可以改為 Bitmap 實現。
2. Create View 優化
- View 的創建也是在 UI 線程里,對于一些非常復雜的界面,這部分的耗時不容忽視。包括各種 XML 的隨機讀的 I/O 時間、解析 XML 的時間、生成對象的時間(Framework 會大量使用到反射)。
- 優化方式:
- 使用代碼創建;
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);
- 異步創建:那我們能不能在線程提前創建 View,實現 UI 的預加載嗎?可以通過又一個非常取巧的方式來實現。在使用線程創建 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; } } } // 要注意的是,在創建完 View 后我們需要把線程的 Looper 恢復成原來的。 private static boolean isMainThread() { return Looper.myLooper() == Looper.getMainLooper(); }
- View 重用:ListView、RecycleView 通過 View 的緩存與重用大大地提升渲染性能。因此我們可以參考它們的思想,實現一套可以在不同 Activity 或者 Fragment 使用的 View 緩存機制。
- AsynclayoutInflater異步創建View
implementation 'com.android.support:asynclayoutinflater:28.0.0' // 內部分別使用了IO和反射的方式去加載布局解析器和創建對應的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是通過側面緩解的方式去緩解布局加載過程中的卡頓,但是它依然存在一些問題:
- 不能設置LayoutInflater.Factory,需要通過自定義AsyncLayoutInflater的方式解決,由于它是一個final,所以需要將代碼直接拷處進行修改。
- 因為是異步加載,所以需要注意在布局加載過程中不能有依賴于主線程的操作。
- Android AsyncLayoutInflater 限制及改進:
/** * 實現異步加載布局的功能,修改點: * * 1. super.onCreate之前調用沒有了默認的Factory; * 2. 排隊過多的優化; */ public class AsyncLayoutInflaterPlus { private static final String TAG = "AsyncLayoutInflaterPlus"; private Handler mHandler; private LayoutInflater mInflater; private InflateRunnable mInflateRunnable; // 真正執行加載任務的線程池 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); } /** * 判斷這個任務是否已經開始執行 * * @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的優點,并解決了其IO操作和反射的性能問題。開發人員只需要正常寫XML代碼即可,在編譯期,X2C會利用APT工具將XML代碼翻譯為Java代碼。
3. measure/layout 優化
- 渲染流程中 measure 和 layout 也是需要 CPU 在主線程執行的;
- 優化方法:減少 UI 布局層次,優化 layout 的開銷,盡量不要重復去設置背景
- 布局優化:
- 單層布局:盡量選擇LinearLayout或FrameLayout,而少用 RelativeLayout,應為RelativeLayout功能較復雜,更耗性能;
但從程序擴展性的角度看,更傾向于RelativeLayout - 多層布局:布局較復雜時,RelativeLayout能夠有效的減少布局層級
- <include/>標簽:實現布局文件的復用,如app自定義的TitleBar
只支持 layout_xx 和id屬性,當include和被包含布局的根標簽都指定了id時,以include為準;指定layout_xx屬性時,
必須也要指定layout_width和layout_height,否則無法生效 - <merge/>標簽:在UI的結構優化中起著非常重要的作用,它可以刪減多余的層級,優化UI。
<merge/>多用于替換FrameLayout或者當一個布局包含另一個時,<merge/>標簽消除視圖層次結構中多余的視圖組。
例如你的主布局文件是垂直布局,引入了一個垂直布局的include,這是如果include布局使用的LinearLayout就沒意義了,
使用的話反而減慢你的UI表現。這時可以使用<merge/>標簽優化。 - <ViewStub/>標簽:懶加載,不會影響UI初始化時的性能;
各種不常用的布局,如進度條、顯示錯誤消息等可以使用ViewStub標簽,以減少內存使用量,加快渲染速度 - 使用 style 來定義通用的屬性,從而重復利用代碼,減少代碼量
- 封裝組合view實現view復用
- 使用 LinearLayoutCompat 組件來實現線性布局元素之間的分割線,從而減少了使用View來實現分割線效果
- 單層布局:盡量選擇LinearLayout或FrameLayout,而少用 RelativeLayout,應為RelativeLayout功能較復雜,更耗性能;
- 布局優化:
- Litho:異步布局
- Litho是 Facebook 開源的聲明式 Android UI 渲染框架,它是基于另外一個 Facebook 開源的布局引擎Yoga開發的。
1. 配置Litho的相關依賴 // 項目下 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,用來聲明去構建一個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內部使用建造者模式以實現組件屬性的鏈式調用,下面設置的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、設置一個LithoView去展示Text組件:LithoView.create內部新建了一個 // LithoView實例,并用給定的Component(lithoText)進行初始化 setContentView(LithoView.create(componentContext, lithoText)); 4. 使用自定義Component Litho中的視圖單元叫做Component,即組件,它的設計理念來源于React組件化的思想。 每個組件持有描述一個視圖單元所必須的屬性與狀態,用于視圖布局的計算工作。 視圖最終的繪制工作是由組件指定的繪制單元(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、構建ListItem組件 ListItem listItem = ListItem.create(componentContext).build();
- Flutter:自己的布局 + 渲染引擎
- RenderThread 與 RenderScript
- Android 5.0,系統增加了 RenderThread,對于 ViewPropertyAnimator 和 CircularReveal 動畫,我們可以使用RenderThead 實現動畫的異步渲染。
參考文章
- Android開發高手課-UI 優化(上):UI 渲染的幾個關鍵概念
- Android開發高手課-UI 優化(下):如何優化 UI 渲染?
- Android 目前穩定高效的UI適配方案
- 一種極低成本的Android屏幕適配方式
- Android 屏幕適配終結者
- 騷年你的屏幕適配方式該升級了!-今日頭條適配方案
- 深入探索Android布局優化
- Vulkan - 高性能渲染
- Android Project Butter分析
- Android 屏幕繪制機制及硬件加速
- Litho
- Yoga
- RenderThread:異步渲染動畫
- RenderScript渲染利器
- RenderScript :簡單而快速的圖像處理
- Android AsyncLayoutInflater 限制及改進