前言
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的性能优化是一个很大的模块,我在没写这篇文章前对某些地方也是并不注意,但是当我为了写好这篇文章查看了大量的资料之后也了解了很多我平常没注意到的东西,其实如果我们开发的项目不属于中大型项目,只是比较小型的项目时,也不需要对性能做到斤斤计较的地步,但是作为一个有文化有素养有追求的程序员,我们对性能还是要有一定追求的,保不准以后我们就参与大型的项目了呢,到时某些坏习惯养成了要改也是很麻烦的。所以虽然不至于到斤斤计较的地步但是在平常开发中我们在编写代码的时候也应该注意性能方面的优化。