前言
Android性能優化對Android程序的維護和拓展是有很大幫助的,我們知道Android手機不管是內存還是CPU都無法同PC相比,這也就意味著我們必須要謹慎的去使用內存和CPU資源。因為稍稍不注意可能就會引發諸如OOM、ANR、內存泄漏等問題,所以熟悉Android性能優化的幾個方法可以有效地提高應用程序的性能,我們可能都能說出一些性能優化的方法,比如布局優化、繪制優化、線程優化等等,但是可能我們會忽視某些小細節,比如布局優化我們可能都知道可以使用< include >來減少布局的層級和布局重用,但是我們很少會考慮使用< merge >標簽,甚至有些人都不知道有這個標簽(說這句話前我特地詢問了十來個Android程序員..),所以這篇文章我嘗試把Android性能優化的方法列舉出來,然后對某些細節也進行解析。
布局優化
布局優化的核心思想就是盡量減少布局文件的層級,層級越少Android在進行布局繪制時工作量也就越少,所以程序的性能也就得到提高了。下面是一些常見的布局優化方法
盡量使用性能較高的ViewGroup
所以我們在設計界面布局時要避免使用多余的嵌套以及在使用ViewGroup時盡量選擇性能較高的布局,比如如果一個布局既可以用LinearLayout實現也可以用RelativeLayout實現,這時我們就應該采用LinearLayout,我們知道一個view的繪制流程是經過onMeasure(),onLayout,onDraw()三個流程才得以呈現的,比較它們的性能無非也就比較它們在這三個方法中的操作耗時,由于篇幅
有限這里就不進行方法耗時測試了,我們直接看源碼說結論RelativeLayout的onLayout和onDraw兩個方法耗時差別不大,所以就不列舉了,真正導致它們性能差異的是onMeasure()這個方法
RelativeLayout的onMeasure方法源碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
View[] views = mSortedHorizontalChildren;
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
LayoutParams params = (LayoutParams) child.getLayoutParams();
int[] rules = params.getRules(layoutDirection);
applyHorizontalSizeRules(params, myWidth, rules);
measureChildHorizontal(child, params, myWidth, myHeight);
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
offsetHorizontalAxis = true;
}
}
}
views = mSortedVerticalChildren;
count = views.length;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
final LayoutParams params = (LayoutParams) child.getLayoutParams();
applyVerticalSizeRules(params, myHeight, child.getBaseline());
measureChild(child, params, myWidth, myHeight);
if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
offsetVerticalAxis = true;
}
if (isWrapContentWidth) {
if (isLayoutRtl()) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, myWidth - params.mLeft);
} else {
width = Math.max(width, myWidth - params.mLeft - params.leftMargin);
}
} else {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
width = Math.max(width, params.mRight);
} else {
width = Math.max(width, params.mRight + params.rightMargin);
}
}
}
if (isWrapContentHeight) {
if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
height = Math.max(height, params.mBottom);
} else {
height = Math.max(height, params.mBottom + params.bottomMargin);
}
}
if (child != ignore || verticalGravity) {
left = Math.min(left, params.mLeft - params.leftMargin);
top = Math.min(top, params.mTop - params.topMargin);
}
if (child != ignore || horizontalGravity) {
right = Math.max(right, params.mRight + params.rightMargin);
bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
}
}
}
從源碼可以看到,RelativeLayout在onMeasure對子view進行了兩次measure,之所以會measure兩次是因為RelativeLayout子view本身是相對的關系,由于子view在布局中的順序不同,在確定子view的具體位置時要先給子view進行排序,RelativeLayout允許A,B 2個子View,在橫向上A相對B,縱向上B相對A。所以需要橫向縱向分別進行一次排序測量。
LinearLayout的onMeasure()方法源碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
LinearLayout的onMeasure是先判斷線性規則是豎直還是水平,然后再在對應方向上進行測量,下面是豎直方向的測量源碼
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;//保存已經measure過的child所占用的高度,初始為0
float totalWeight = 0;//累計所有子視圖的weight值
...
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
//對每一個child進行測量
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
}
這里面還有個細節,就是如果不使用LinearLayout的weight屬性,LinearLayout會在當前方向上進行一次measure的過程,如果使用weight屬性,LinearLayout會避開設置過weight屬性的view做第一次measure,然后再對設置過weight屬性的view進行第二次measure。所以,weight屬性對性能是有影響的,使用時要謹慎。
所以如果RelativeLayout和LinearLayout或者FrameLayout都能完成布局的情況下優先考慮使用LinearLayout或FrameLayout這些比較高效的ViewGroup,而如果單純一個LinearLayout或FrameLayout無法完成要求而要進行嵌套時,而RelativeLayout一層就可以繪制時就建議使用RelativeLayout了。
布局可以重用時就封裝好進行重用
這里說的重用時使用< include >標簽,這個標簽可以將一個指定的布局文件加載進當前的布局文件中。
<include layout="@layout/xxx"/>
比如xxx是一個特定的布局(例如標題欄),這樣就不用在當前布局中再寫一次xxx的布局文件的內容了,值得說的是這個標簽支持除了android:id這個屬性之外只支持android:layout_開頭的屬性,比如
android:layout_width="match_parent"
還有如果指定了其他除android:layout_width,和android:layout_height的android:layout_屬性時,前兩個屬性必須存在,否則android:layout_是沒有效果的。這個標簽我們應該很常用,所以就不舉例了。值得一說的是< merge >標簽,這個標簽一般是和< include >標簽進行配合使用的,比如當前布局是豎直的LinearLayout,而 < include >要引用的布局也是豎直LinearLayout時,就可以把要引入布局的LinearLayout換成< merge>即可,這樣又可以減少了一個布局的層級。
ViewStub
ViewStub是一個輕量級且寬高都是0,它不參與任何布局和繪制過程。ViewStub的作用在于按需加載所需布局,在我們開發過程中,有些布局是在某些情況下不需要顯示的,比如,一個界面需要網絡數據時才顯示,網絡異常時會顯示另一個界面,這時候我們就不需要一開始就把異常界面加載金布局,只有當網絡異常時才加載這個布局,這時候通過ViewStub就可以做到在使用的時候再加載,提高了程序初始化時的性能。
<ViewStub
android:layout_width="wrap_content"
android:id="@+id/vs"
android:layout_height="wrap_content"
android:inflatedId="@+id/ll_import"
android:layout="@layout/ll_network_error"
/>
比如上面的例子,ll_network_error就是網絡異常時要加載的布局,而ll_import是這個布局根元素的id。通過這個標簽就可以做到按需加載了,這樣只要在java代碼中
findviewById(R.id.vs).setVisibility(View.VISIBLE);
或
findviewById(R.id.vs).inflate()
ViewStub就會被ll_network_error替換,這時View就不在是整個布局結構的一部分了,當然目前ViewStub不支持< merge >標簽。
繪制優化
繪制優化是說在View調用onDraw時應該避免大量操作,比如在onDraw時不要創建新局部對象,因為onDraw方法可能會被頻繁調用。如果被頻繁調用就會瞬間產生大量的臨時變量,占用過多內存和導致系統頻繁觸發gc降低了程序的執行效率。還有一個是不要再onDraw中進行耗時操作,比如上千次循環,因為大量循環會搶占CPU造成View繪制不流暢Google給出的性能優化典范標準中,view的繪制幀率保證60fps是最佳的,也就是說,每幀的繪制事件不能超過16ms(1000/60),所以在View繪制時,應該盡量避免在onDraw上進行復雜耗時的操作。
線程優化
線程優化是避免在程序中使用大量的Thread,而是采用內部的線程池,這樣就可以避免線程的創建和銷毀時帶來性能上不必要的開銷,線程池也能控制線程的最大并發數,避免了大量線程因為互相搶占系統資源導致程序阻塞的現象。在實際開發中,我們是不應該創建Thread對象而是使用線程池策略的,關于線程池策略可以參考我的另一篇文章:
Android 關于線程池的理解
ListView優化
ListView優化主要是以下幾個方面:
- 復用ConvertView
- 自定義靜態類ViewHolder
- 使用分頁加載
- 使用弱引用(WeakRefrence)引用ImageView對象
- 避免在getView中執行耗時操作
Bitmap優化
Bitmap優化主要是以下幾個方面:
- 及時回收Bitmap的內存
- 緩存通用的Bitmap對象
- 壓縮圖片
4. 及時關閉資源
內存泄漏優化
內存泄漏優化主要有兩個方面,一個是在開發過程中避免寫出有內存泄漏的代碼,一個是通過分析工具找出可能存在的內存泄漏問題然后解決。
避免寫出內存泄漏的代碼比較考驗的是程序員的經驗和開發意識了,下面列出一些常見的內存泄漏的例子,讓我們在以后開發時積累和避免寫出導致內存泄漏的代碼。
靜態變量導致內存泄漏
public class MainActivity extends Activity {
private static Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContext = this;
}
}
如上,這是一種最簡單的內存泄漏,Activity是無法正常銷毀的,因為靜態變量mContext引用了這個Activity。
屬性動畫沒停止導致內存泄漏
這個理解起來也比較簡單,如果一個Activity中播放了無限循環的屬性動畫而不在onDestroy方法中去停止動畫時,動畫就會一直播放下去(即使界面上已經沒有動畫效果),解決方法也很簡單,只要在onDestroy調用xx.cancel()停止動畫即可。
單例模式導致內存泄漏
這種情況可以有下面的情形:比如在一個單例中持有一個Activity的引用,當Activity退出時這個Activity應該被回收,但是單例的生命周期是和Application一致的,這就導致了Activity不能被回收,造成內存泄漏
內存泄漏分析工具
MAT這個工具是一款強大的內存泄漏分析工具,這個工具的使用方法就不在這篇博客里面詳述了(因為感覺寫得太多了你們看著也煩..),具體可以參考這篇博文:
內存分析工具 MAT 的使用
響應速度優化
其實這個優化也就是在開發時避免在主線程中進行耗時操作,因為Android系統規定,Activity5秒無法響應屏幕觸摸事件或鍵盤輸入事件就會出現ANR,BroadcastReceiver在10秒內沒有執行完畢也會出現ANR,當我們程序出現ANR時一般我們是很難直接從代碼上定位到問題的,這個Android系統也幫我們考慮到了,所以當程序出現ANR時,系統會在/data/anr目錄下創建一個traces.txt文件,童工分析文件就可以找到ANR的原因了,下面用一個例子去模擬ANR
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SystemClock.sleep(30000);
}
}
例子很簡單就在主線程睡了30秒,然后肯定會引起ANR了,所以我們查看traces.txt文件夾,信息很多我只截取跟我們有關的
----- pid 1472 at 2016-07-16 03:07:28 -----
Cmd line: com.example.sjr.zhenanim
JNI: CheckJNI is off; workarounds are off; pins=0; globals=155
DALVIK THREADS:
(mutexes: tll=0 tsl=0 tscl=0 ghl=0)
"main" prio=5 tid=1 TIMED_WAIT
| group="main" sCount=1 dsCount=0 obj=0xa4c13480 self=0xb94b0bd0
| sysTid=1472 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1216614368
| state=S schedstat=( 350771760 398313811 1915 ) utm=12 stm=22 core=0
at java.lang.VMThread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:1013)
at java.lang.Thread.sleep(Thread.java:995)
at android.os.SystemClock.sleep(SystemClock.java:115)
at com.example.sjr.zhenanim.MainActivity.onCreate(MainActivity.java:20)
at android.app.Activity.performCreate(Activity.java:5133)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2175)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2261)
at android.app.ActivityThread.access$600(ActivityThread.java:141)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1256)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:5103)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:737)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
"Thread-103" prio=5 tid=11 NATIVE
| group="main" sCount=1 dsCount=0 obj=0xa509c578 self=0xb94fd1a8
| sysTid=1486 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1185982336
| state=S schedstat=( 12313 10224280 3 ) utm=0 stm=0 core=0
#00 pc 0002e2a1 /system/lib/libc.so (accept+17)
at android.net.LocalSocketImpl.accept(Native Method)
at android.net.LocalSocketImpl.accept(LocalSocketImpl.java:299)
at android.net.LocalServerSocket.accept(LocalServerSocket.java:94)
at com.android.tools.fd.runtime.Server$SocketServerThread.run(Server.java:150)
at java.lang.Thread.run(Thread.java:841)
"Binder_2" prio=5 tid=10 NATIVE
| group="main" sCount=1 dsCount=0 obj=0xa507d268 self=0xb94f66c0
| sysTid=1485 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1185979272
| state=S schedstat=( 3756308 15291656 13 ) utm=0 stm=0 core=0
#00 pc 0002cff4 /system/lib/libc.so (__ioctl+20)
at dalvik.system.NativeStart.run(Native Method)
"Binder_1" prio=5 tid=9 NATIVE
| group="main" sCount=1 dsCount=0 obj=0xa507d180 self=0xb94f6010
| sysTid=1484 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1185990552
| state=S schedstat=( 8102476 41618382 11 ) utm=0 stm=0 core=0
#00 pc 0002cff4 /system/lib/libc.so (__ioctl+20)
at dalvik.system.NativeStart.run(Native Method)
"FinalizerWatchdogDaemon" daemon prio=5 tid=8 WAIT
| group="system" sCount=1 dsCount=0 obj=0xa5078640 self=0xb94f2fb0
| sysTid=1483 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1185991656
| state=S schedstat=( 3031794 41298066 7 ) utm=0 stm=0 core=0
at java.lang.Object.wait(Native Method)
- waiting on <0xa4c1b3b8> (a java.lang.Daemons$FinalizerWatchdogDaemon)
at java.lang.Object.wait(Object.java:364)
at java.lang.Daemons$FinalizerWatchdogDaemon.waitForObject(Daemons.java:230)
at java.lang.Daemons$FinalizerWatchdogDaemon.run(Daemons.java:207)
at java.lang.Thread.run(Thread.java:841)
"FinalizerDaemon" daemon prio=5 tid=7 WAIT
| group="system" sCount=1 dsCount=0 obj=0xa50784a0 self=0xb94f26f8
| sysTid=1482 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1185993888
| state=S schedstat=( 1334759 16682975 5 ) utm=0 stm=0 core=0
at java.lang.Object.wait(Native Method)
- waiting on <0xa4c04568> (a java.lang.ref.ReferenceQueue)
at java.lang.Object.wait(Object.java:401)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:102)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:73)
at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:170)
at java.lang.Thread.run(Thread.java:841)
"ReferenceQueueDaemon" daemon prio=5 tid=6 WAIT
| group="system" sCount=1 dsCount=0 obj=0xa5078348 self=0xb94ca578
| sysTid=1481 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1185996120
| state=S schedstat=( 1647741 1925287 6 ) utm=0 stm=0 core=0
at java.lang.Object.wait(Native Method)
- waiting on <0xa4c04490>
at java.lang.Object.wait(Object.java:364)
at java.lang.Daemons$ReferenceQueueDaemon.run(Daemons.java:130)
at java.lang.Thread.run(Thread.java:841)
"Compiler" daemon prio=5 tid=5 VMWAIT
| group="system" sCount=1 dsCount=0 obj=0xa5078260 self=0xb94c9f08
| sysTid=1480 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1186046664
| state=S schedstat=( 9217427 7831506 9 ) utm=0 stm=0 core=0
#00 pc 0002ed67 /system/lib/libc.so (__futex_syscall4+23)
at dalvik.system.NativeStart.run(Native Method)
"JDWP" daemon prio=5 tid=4 VMWAIT
| group="system" sCount=1 dsCount=0 obj=0xa5078180 self=0xb94c9aa0
| sysTid=1479 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1186006992
| state=S schedstat=( 87784861 47188330 130 ) utm=0 stm=7 core=0
#00 pc 0002d1e0 /system/lib/libc.so (select+32)
at dalvik.system.NativeStart.run(Native Method)
"Signal Catcher" daemon prio=5 tid=3 RUNNABLE
| group="system" sCount=0 dsCount=0 obj=0xa5078088 self=0xb94c9638
| sysTid=1478 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1186244424
| state=R schedstat=( 13450535 1316670 12 ) utm=0 stm=1 core=0
at dalvik.system.NativeStart.run(Native Method)
"GC" daemon prio=5 tid=2 VMWAIT
| group="system" sCount=1 dsCount=0 obj=0xa5077fa8 self=0xb94c91d0
| sysTid=1475 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=-1186040176
| state=S schedstat=( 12677193 73095526 1316 ) utm=1 stm=0 core=0
#00 pc 0002ed67 /system/lib/libc.so (__futex_syscall4+23)
at dalvik.system.NativeStart.run(Native Method)
----- end 1472 -----
可以看到,日志里有這么一行:
com.example.sjr.zhenanim.MainActivity.onCreate(MainActivity.java:20)
這就是 SystemClock.sleep(30000);所在的行數了,在日常開發中ANR很容易出現,避免出現ANR只能靠程序員的開發意識了,但是當出現ANR時可以通過分析traces.txt文件去快速定位并解決問題了。
其他優化建議
這是一些對性能優化的小建議,通過這些小技巧可以在一定程度上提高性能:
- 避免創建過多對象;
- 不要過多使用枚舉,因為枚舉占用的內存空間比整型大兩倍以上;
- 常量使用static final來修飾;
- 使用一些Android特有的數據結構,比如SparseArry和Pair等,它們都具有更好的性能;
- 適當使用軟引用和弱引用;
- 采用內存緩存和磁盤緩存;
- 盡量采用靜態內部類,這樣可以避免潛在的由于內部類而導致的內存泄漏。
總結
Android的性能優化是一個很大的模塊,我在沒寫這篇文章前對某些地方也是并不注意,但是當我為了寫好這篇文章查看了大量的資料之后也了解了很多我平常沒注意到的東西,其實如果我們開發的項目不屬于中大型項目,只是比較小型的項目時,也不需要對性能做到斤斤計較的地步,但是作為一個有文化有素養有追求的程序員,我們對性能還是要有一定追求的,保不準以后我們就參與大型的項目了呢,到時某些壞習慣養成了要改也是很麻煩的。所以雖然不至于到斤斤計較的地步但是在平常開發中我們在編寫代碼的時候也應該注意性能方面的優化。