AllenHsu的技術手扎

學海無涯,努力做個永遠年輕的人

0%

Android View 渲染效能優化

好久沒寫文章了,應該至少兩年多了,這兩年工作轉換了很多,現在又重回 Android 開發的行列,最近工作上剛好遇到需要優化一個頁面的渲染性能(解決卡頓問題),所以剛好透過這篇文章來覆盤跟總結一下 Android 性能優化

造成 View 渲染緩慢的原因

在這個 Case 中,有幾個非常常見的 View 卡頓原因,這邊就列出這幾個常見的 Case

Layout 佈局過於複雜

因為 inflate 需要花費的時間過久,如果你的 Layout 層次過於複雜的話,所以需要盡量減少 Layout 佈局的複雜度,善用ConstraintLayout去減少佈局的複雜度

RecycleView 頻繁全局刷新

這個蠻容易在開發過程中出現的一個錯誤,RecyclerView 主要的更新流程是透過

GetData –> setAdapter –> 通知 RecyclerView 更新
在通知 RecyclerView 更新這個環節有許多的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
final void	notifyDataSetChanged()
Notify any registered observers that the data set has changed.

final void notifyItemChanged(int position, Object payload)
Notify any registered observers that the item at position has changed with an optional payload object.

final void notifyItemChanged(int position)
Notify any registered observers that the item at position has changed.

final void notifyItemInserted(int position)
Notify any registered observers that the item reflected at position has been newly inserted.

final void notifyItemMoved(int fromPosition, int toPosition)
Notify any registered observers that the item reflected at fromPosition has been moved to toPosition.

final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
Notify any registered observers that the itemCount items starting at position positionStart have changed.

final void notifyItemRangeChanged(int positionStart, int itemCount)
Notify any registered observers that the itemCount items starting at position positionStart have changed.

final void notifyItemRangeInserted(int positionStart, int itemCount)
Notify any registered observers that the currently reflected itemCount items starting at positionStart have been newly inserted.

final void notifyItemRangeRemoved(int positionStart, int itemCount)
Notify any registered observers that the itemCount items previously located at positionStart have been removed from the data set.

final void notifyItemRemoved(int position)
Notify any registered observers that the item previously located at position has been removed from the data set.

但是這邊常犯的錯誤是直接呼叫 notifyDataSetChanged,這個會直接使整個 RecyclerView 全局刷新,但我們有時候獲得的資料只是一小部分的改動,所以我們應該根據當下情況去使用其他的 function,或者使用 Android 官方提供的DiffUtil去幫助做資料更新的比對

ref : https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil

局部更新造成全局更新

這個在這個 case 上,很容易發生的是在動態改變 text 內容的 TextView 之上,假設今天有一個場景,是有一個倒數計時的 UI 呈現,那麼就會定時地去更新該 TextView,但是如果 TextView 的 width 跟 height 是wrap_content,那就會造成這個 TextView 有機會的去更新全局的 UI。

onDraw, onMeasure, onLayout 時做了耗時操作

這幾個關鍵函數,都應該只做他們的自我權責,不應該在這些 Function 裡面做不必要的耗時操作,尤其是 onDraw,這也會造成 UI 渲染的效能瓶頸

頻繁 Add/Remove

頻繁 Add/Remove 也是會觸發整個佈局的重新繪製,所以應該避免

如何找出緩慢的貧頸

上述就是常見的幾種錯誤使用方式,但是我們在開發的時候,可能會遇到難以查找是什麼原因造成整體重繪(有可能上述狀況都有,不好定義),可以透過 Android 自帶的工具或者一些方式

在 RootView 的 requestLayout print stacktree

可以複寫 RootView 的 requestLayout function,並且在裡面打印 stacktree,這樣可以查找出是哪一個 UI 元件呼叫 stacktree,

透過開發者工具測量速度

在開發者選項裡有一個”硬件加速渲染“,裡面有一個“調試 GPU 過度繪製”,這個會在屏幕上以顏色來區分 overdraw(過度繪製,也就是進行了不必要的繪製)的嚴重重度:

  • 藍色 1 倍 overdraw
  • 綠色 2 倍 overdraw
  • 紅色 3 倍 overdraw
  • 紫色 4 倍 overdraw

在開發者選項面有一個是監控
選擇GPU使用情況,選擇第二個
在螢幕上顯示條型,就可以在手機上即時觀測到目前的使用情況

參考文件 :

React中的虛擬DOM詳解

在 React 還沒出現前的前端開發,我們需要直接操作DOM結構,操作既複雜,兼容性又差,當我們有了jQuery之後,透過jQuery封裝的API,我們能夠更方便的操作DOM,但是DOM是很慢的,其元素非常巨大,頁面的性能問題常常會因為操作DOM而出現瓶頸,於是乎有什麼方法可以讓操作DOM的效率提升呢? ReactJs 提出了虛擬DOM的概念

Read more »

React組件拆分與組間之間的傳值

React 是一個基於組件化開發的前端框架,那什麼是組件化的概念呢,以下圖來做比喻的話

img

圖中的每一個區塊都可以拆分成一個一個的組件,像是上面的Header,下面內容的每一個區塊,都會是一個組件。為什麼我們需要拆分組件呢,在開發前端頁面的時候,這個頁面的邏輯可能相對來得相當複雜,如果還是採用原有的開發方式,那就會造成我們在寫代碼之中的困擾。而當我們將頁面拆分成一塊一塊的組件之後,相對來說,每個組件的邏輯就會比整個頁面上來的簡單。

Read more »

Golang 初心者筆記-變數類型

Golang 在近代算是一個成長快速的語言,這邊簡單記錄一下 Golang 學習的過程,所以從最簡單的變量類型開始,希望透過邊學習邊紀錄的過程,讓自己可以快速入門這個語言,並且實做一個小型的side project

Read more »

設計模式-代理模式(一)

從出社會開始,因緣際會的讀了一本深入淺出設計模式(用Java實踐的),當時就覺得設計模式很有趣,索幸用C++把裡面的範例都實踐了一遍,最近因為開始SSM的練習,為了了解Mybatis,想從裡面的設計模式開始著手導讀,知道裡面用了動態代理的模式,但這邊先不談動態代理,而是想先從簡單的靜態代理開始,在下一篇才會繼續深度探索Mybatis的動態代理怎麼實踐的,這邊就先拿靜態代理的設計模式來練練手吧。

Read more »

CI/CD,全名叫做Continuous Integration / Continuous Delivery,中文名叫持續集成與持續交付,從2014年開始,得以有幸接觸了從零開始的CI/CD的搭建以及流程梳理,也有在一個已經既有的開發體系中,如何去逐步導入CI/CD的相關經驗,透過這篇文章分享一下自己多年以來對於CI/CD的經驗與心路歷程。

關於持續集成與持續交付

在目前軟體業界普遍以敏捷開發作為精神,必須透過連續的小型的開發循環(Sprint)來迭代我們的產品以及驗證需求,以這樣為前提之下,我們必須不斷地從需求、研發、測試、交付,中多個階段不斷地循環迭代。所以我們為了上述的概念,我們必須有了持續集成與持續交付。

持續集成與持續交付,對於我個人的理解來說有以下來幾個重點。

  • 讓開發團隊可以在一個不斷提升開發品質的體系當中。
  • 從**需求研發測試交付**四個階段中能夠有一套完整且嚴格的體系及框架。
  • 自動化節省時間 - 讓開發團隊從一些瑣事的泥沼中解放出來。
  • 減少風險 - 因為有了上述幾點,所以可以有效提升軟體品質,而不會因為迭代過快造成軟體工程的不穩定。
Read more »

Rxjava的線程切換與異步操作

傳統Android開發中,我們可以透過Handler/Thread等方式來做異步操作,那在RxJava中,假設我們想要做Thread的切換我們應該怎麼做呢?基本上是透過 observeOnsubscribeOn 這兩個操作符,去實作所謂的線程切換與異步操作。

observeOn 與 subscribeOn 。

什麼是 observeOn :

specify the Scheduler on which an observer will observe this Observable

指定一個觀察者在特定的 Scheduler 上觀察這個 Observable 。

什麼是 subscribeOn :

specify the Scheduler on which an Observable will operate

指定這個 Observable 自身在哪一個特定的 Scheduler 上操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Observable.just(1)
.map(integer -> {
Log.i("Rxjava", "map-1:"+Thread.currentThread().getName());
return integer;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.newThread())
.doOnSubscribe(s -> {
Log.i("Rxjava", "doOnSubscribe:"+Thread.currentThread().getName());
})
.doOnNext(next -> {
Log.i("Rxjava", "doOnNext:"+Thread.currentThread().getName());
})
.subscribe(
x -> {
Log.i("Rxjava", "next-1:"+Thread.currentThread().getName());
}
);

以上面的代碼為例子,map的操作符會在newThread做操作,doOnSubscribe/doOnNext 都會在 mainThread做操作,透過這樣兩個簡單的操作符可以很簡單的去做Rxjava的線程調度。