Android性能优化主要内容包括布局优化、绘制优化、内存泄漏优化、响应速度优化、ListView/RecyclerView优化、Bitmap优化、线程优化以及代码性能优化,此处会一一讲解,并给出一些优化建议。
一、布局优化
优化思路很明确,就是尽可能的减少布局文件的中间层级,原因很简单,中间层级减少了,Android的View在measure、layout和draw上面耗费的时间就减少了,自然就快了。优化原则:
1、尽可能的删除布局中无用的控件和层级,比如我们在做listView列表的时候,会用一个View放在item的底部做分割线,其实这个View是没必要的。可以通过其他手段实现。比如一个TextView的DrawableBottom属性设置一个图片等。
2、有选择的删除性能比较弱的ViewGroup,比如RelativeLayout。在既可以使用LinearLayout又可以用RelativeLayout,那就优先使用LinearLayout,因为后者的measure方法简单很多。
3、尽可能的减少布局中的嵌套层级,在用RelativeLayout可以减少布局的层及数量,使用LinearLayout会增加层级数量的情况下,还是考虑优先使用RelativeLayout。因为RelativeLayout虽然实现比较耗费资源,但是减少了层级,这时候二者有一个平衡,但是层级越少,代码越简洁,效率越高。
4、布局层级优化,太复杂的布局可以考虑使用标签,简化布局代码。
二、绘制优化
绘制优化主要是指在View及ViewGroup的onDraw方法里面应避免执行太过复杂且大量的操作。体现在以下两方面:
1、onDraw()里面不要创建新的布局变量尤其是对象,因为onDraw可能被高频率调用,这样就会产生大量的临时对象,增加内存消耗以及gc回收难度,耗费资源。
2、onDraw()里面不要做耗时操作以及量大的循环(如for循环一万次,while(true)循环),这样的循环被CPU反复调用将会很耗时间。视觉上UI就会有卡顿失帧。
三、内存泄漏优化
这个是项目开发中需要引起足够重视的问题,有效避免内存泄漏对开发者有较高的经验和开发意识要求。内存泄漏主要分两方面:一是开发者避免写出容易导致内存泄漏的代码,二是通过一些内存优化的三方库进行监督监视。此处主要分析一些内存泄漏优化的例子以增加理解。
场景1:
静态变量导致的内存泄漏,这种泄露的主要原因是静态变量持有另一个对象,当该对象被回收的时候,由于静态变量依然持有,导致回收失败:
public class AsyncTaskActivity extends AppCompatActivity {
static View sView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
sView = new View(this);
}
}
静态变量因为一直持有AsyncTaskActivity这个对象,所以导致回收该Activity对象失败。
场景2:
单例模式下的内存泄漏,这种泄露很容易被我们忽略:单例内部持有Context或Activity对象,当Activity被释放时,依然被单例持有。
public class Test {
static Context mContext;
public static Test instance;
public static Test getInstance(Activity activity){
getObject(activity);
if (instance == null){
synchronized (Test.class){
if (instance == null){
instance = new Test();
}
}
}
return instance;
}
static void getObject(Activity activity){
mContext = activity;
//TODO:
}
}
场景3:
属性动画引起的内存泄漏,属性动画中的无限循环动画在Activity中播放,如果在onDestroy方法中没有停止这个动画,那么由于Activity的view一直被持有,而该view又持有Activity,最终导致Activity无法被释放回收,引起内存泄漏。
场景4:
注意 Activity 的泄漏:通常来说,Activity 的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,我们需要特别注意以下两种情况导致的 Activity 泄漏:
内部类引用导致 Activity 的泄漏
最典型的场景是 Handler 导致的 Activity 泄漏,如果 Handler 中有延迟的任务或者是等待执行的任务队列过长,都有可能因为 Handler 继续执行而导致 Activity 发生泄漏。此时的引用关系链是 Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在 UI 退出之前,执行 remove Handler 消息队列中的消息与 runnable 对象。或者是使用 Static + WeakReference 的方式来达到断开 Handler 与 Activity 之间存在引用关系的目的。
Activity、Context
被传递到其他实例中,这可能导致自身被引用而发生泄漏。内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意!我们可以考虑尽量使用 static 类型的内部类,同时使用 WeakReference 的机制来避免因为互相引用而出现的泄露。
尽量地采用 Application Context:
对于大部分非必须使用 Activity Context 的情况(Dialog 的 Context 就必须是Activity Context),我们都可以考虑使用 Application Context 而不是 Activity 的 Context,这样可以避免不经意的 Activity 泄露。
场景5:
注意 Cursor 对象是否及时关闭:在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用 Cursor 之后没有及时关闭的情况。这些 Cursor 的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对 Cursor 对象的及时关闭。
场景6:
注意 WebView 的泄漏Android中的WebView存在很大的兼容性问题,不仅仅是 Android 系统版本的不同对WebView 产生很大的差异,不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的 WebView本就存在内存泄露的问题。
四、响应速度优化和ANR日志分析
响应速度优化的核心是避免在主线程中做耗时操作,若耗时操作无法避免,那就让其在子线程中执行。响应速度过慢更多的体现在Activity的启动速度和对用户交互时的响应速度。Android规定,如果Activity在5秒内无法响应屏幕触摸或者键盘输入时间,就会出现ANR; BroadcastReceiver如果10秒内还没执行完操作也会出现ANR。实际开发中ANR很难从代码上发现。解决方式是分析ANR日志,这个日志是ANR发生时被创建保存在/data/anr目录下的,另外,复杂的同步也可能导致ANR。
五、ListView/RecyclerView列表优化主要涉及以下几个方面:
1、ViewItem的复用。
2、不在Adapter的getView方法里面做耗时操作。比如直接加载图片,这时需要通过耗时操作来实现,比如Glide三方包。
3、控制异步任务的执行频率。比如在列表高速滑动的时候,getView依然加载图片或则数据,这时候瞬间就会产生数十个甚至更多的异步任务,大大加重CPU的负担,导致响应缓慢。解决方案是在列表滑动的时候停止加载数据,列表停下来后再加载数据。如下:
Public voidonScrollStateChanged(AbsLisrView view, int scrollState){
If(scrollState == OnScrollListener。SCROLL_STATE_IDLE){
isListSlid = true;
mAdapter.notifyDataSetChanged();
}else{
isListSlid = false
}
}
然后再getView方法中:
If(isListSlid && mCangetBitmapFromNetWork){
imageView.setTag(uri);
imageLoader.bindBitmap(uri, imageView, imageWidth, imageHeight);
}
经过上面两个步骤,能优化解决95%以上的列表加载卡顿。
六、Bitmap优化
Bitmap的优化主要是通过设置和里的BitmapFactory.Options来对图片进行采样缩放图片,采样过程中有一个重要的参数:inSampleSize,
当他为1时,新的图片大小为原图大小;当他为2时,新的图片宽高均为原图的1/2,而大小为原图的1/4。也就是说inSampleSize是一个直接影响图片大小的倍率参数。inSampleSize小于1时,无效果。另外倍率参数inSampleSize的取值应为2的指数倍数关系,即inSampleSize = 2^x,其中x >= 0。
public class TestList {
public static Bitmap sampleBitmap(Resources res, int id, int rewWidth,int reqHei){
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, id, options);
options.inSampleSize = initSampleSize(options, rewWidth, reqHei);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, id, options);
}
public static int initSampleSize(BitmapFactory.Options options, int rewWidth, int reqHei){
final int wid = options.outWidth;
final int hei = options.outHeight;
int sampleSize = 1;
if(wid > rewWidth || hei > reqHei){
final int newWid = wid / 2;
final int newHei = hei / 2;
while ((newWid / sampleSize ) >= rewWidth && (newHei / sampleSize ) > reqHei){
sampleSize *= 2;
}
}
return iSampleSize;
}
}
上面decodesampleBitmap方法返回的对象就是经过优化的,在实际使用中就很简单了,比如我想让ImageView加载100*100的图片,就可以直接decodesampleBitmap(res, R.drawable.peacture, 100, 100);
七、线程优化:核心思路就是避免产生大量的子线程,并用线程池技术来实现多线程需求。
八、Java代码性能优化
1、少用枚举类:枚举类将会占用远远超过整型数据的内存空间,可以用switch语句块替代。
2、开发细节的注意:针对字符串的拼接,使用StringBuilder/StringBuffe替代String;尽量使用原字符串的 subString;慎用异常,使用异常会导致性能降低;数据额结构和数据算法的优化;及时IO等。这方便涉及到的细节很多,需要开发中事件中多注意,养成良好的开发习惯。
**3、善用 ArrayMap替代hashMap、SparseArray替代List:**HashMap,它非常好用,毋庸置疑,但它却非常耗内存。它内部使用两个数组进行工作,其中一个数组记录 key值,也就是hash过后的顺序列表,另外一个数组按key的顺序记录- Value值。
注意两点:一是ArrayMap 的插入与删除的效率是不够高的,但如果数据的长度只是在一百这个数量级上,则完全不用担心这些插入与删除的效率问题。另外,与HashMap相比,ArrayMap在循环遍历的时候也更加高效,因为其采用的是fori循环,而HashMap使用的是Iterator迭代输出。
4、尽量地采用 int 类型替代Itenget类型
5、避免在onDraw()里面创建对象以及调用频繁的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,引起频繁的 gc,甚至是内存抖动。
代码的微优化是非常耗费时间的,没有必要从头到尾将所有代码都优化一遍。开发者应该根据具体的业务逻辑去专门针对某部分代码做优化。比如应用中可能有一些方法会被反复调用,那么这部分代码就值得专门做优化。其它的代码,需要开发者在写代码过程中去注意。
九、App启动速度优化
应用的启动分为冷启动、热启动、温启动,而启动最慢、挑战最大的就是冷启动:系统和App本身都有更多的工作要从头开始!应用在冷启动之前,要执行三个任务:
1、加载启动App;
2、App启动之后立即展示出一个空白的Window;
3、创建App的进程;
这三个任务执行完后会马上执行以下任务:
1、创建App对象;
2、启动Main Thread;
3、创建启动的Activity对象;
4、加载View;
5、布置屏幕;
6、进行第一次绘制;
完成了第一次绘制,系统进程就会用Main Activity替换已经展示的Background Window,此时用户就可以使用App了。
App进程的创建等环节我们无法主动控制,可以优化的也就是Application、Activity创建以及回调等过程。*Google给出了启动加速的方向*:
1、利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
2、避免在启动时做密集沉重的初始化(Heavy app initialization);
3、定位问题:避免I/O操作、反序列化、网络操作、布局深层嵌套等。
对于App启动速度优化,我们会利用主题去防止出现白屏;针对启动速度慢,主要从以下几个方面进行优化:
1、尽可能减少Application的onCreate()方法中要做的事情,比如一些不重要的SDK延迟或者异步加载;
2、多进程情况下一定要在onCreate中去区分进程做一些初始化工作;
3、部分将要使用到的类异步加载;
十、apk瘦身
Apk瘦身,主要有以下几个方面入手:
1、利用ProGuard压缩代码去除无用资源;
2、andresguard进一步压缩与混淆资源;
3、合理配置去除不必要的配置,仅保留中文配置等;
4、第三方开源库的瘦身,仅保留自己需要的部分;
5、动态下发一些资源:字库、so、换肤包等;
6、so的优化与配置,只保留一类so;
7、极致的图片压缩与webp的使用,无透明要求的图片用jpg或者webp格式,大小相比png将会缩小一半以上;
第7比较麻烦,需要后台的配合,其他的都是常规方案。
十一、电池优化
我们先简单总结汇总一下耗电的相关因素:
1、屏幕亮暗相关;
2、传感器;
3、CPU运行相关;
4、网络;
5、设备awake,sleep的切换,尤其是唤醒;
6、App后台数据操作。
优化建议:
(1)、点滴积累:我们都知道屏幕的渲染及CPU的运行是耗电的主要因素之一。所以其实当我们在做内存优化、渲染优化、计算优化、后台操作优化等的时候,就已然在做电量优化。
(2)、监听手机充电状态:我们可以通过下面的代码来获取手机的当前电量状态:
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERRY_CHANGED);
Intent batteryStatus = this.register.ReceiVer(null, filter);
Int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
Boolean acCharge = (chargePlug == BatteryManager.EXTRA_PLUGGED_AC);
If(acCharge ) {
log.v(TAG, “The phone is charging!”);
}
得到电量状态(尤其是充电状态)信息之后,我们可以有针对性的对部分代码做优化。比如我们可以判断只有当前手机为AC充电状态时 才去执行一些非常耗电的操作。
(3)、屏幕唤醒:当Android设备空闲时,屏幕会变暗,然后关闭屏幕,最后停止CPU的运行,防止电池电量掉的快。但是我们的一些操作比如游戏是要求屏幕一直亮着的,不能自动息屏。这时候我们可以这样做来实现:
在对应的Activity的onCreate()方法中,setContentView()方法之后添加Flag标记:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_animation);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
这个操作不需要特定的权限。并且能正确管理不同app之间的切换,不用担心无用资源的释放问题。
这里是通过Java代码动态的实现这个功能,我们也可以在xml文件里面静态的实现:那就是在xml根布局的ViewGroup里面添加:android:keepScreenOn = “true”,他的作用跟通过添加Flag是一样的:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">……
当我们不需要屏幕一直保存唤醒的状态时,可以通过下面的代码关闭:
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
注意!注意!!注意!!!一般不需要人为的去掉FLAG_KEEP_SCREEN_ON的flag,windowManager会管理好程序进入后台回到前台的的操作。
(4)、WakeLock:
wake_lock锁主要是相对系统的休眠而言的,意思就是我的程序给CPU加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题。需要使用PowerManager这个系统服务的唤醒锁(wake locks)特征来保持CPU处于唤醒状态。唤醒锁允许程序控制宿主设备的电量状态。创建和持有唤醒锁对电池的续航有较大的影响,所以,除非是真的需要唤醒锁完成尽可能短的时间在后台完成的任务时才使用它。如果需要关闭屏幕,使用上述的FLAG_KEEP_SCREEN_ON。
(5)、定位
选择合适的Location Provider,Android系统支持多个Location Provider:
**GPS_PROVIDER:**GPS定位,利用GPS芯片通过卫星获得自己的位置信息。定位精准度高,一般在10米左右,耗电量非常大;并且在室内,基本没用。
NETWORK_PROVIDER:网络定位,利用手机基站和WIFI节点的地址来大致定位位置,这种定位方式取决于将基站或WIF节点信息翻译成位置信息的服务器的能力。
PASSIVE_PROVIDER:被动定位,就是用现成的,当其他应用使用定位更新了定位信息,系统会保存下来,该应用接收到消息后直接读取就可以了。
及时注销定位监听:在获取到定位之后或者程序处于后台时,监听GPS的传感器相当于执行no-op(无操作指令),用户不会有感知但是却耗电,所以我们要适时德注销定位监听。
(6)、传感器
使用传感器,选择合适的采样率,越高的采样率类型则越费电。应用在后台时要及时注销传感器监听。
SENSOR_DELAY_NOMAL (200000微秒)、SENSOR_DELAY_UI (60000微秒)、
SENSOR_DELAY_GAME (20000微秒)、 SENSOR_DELAY_FASTEST (0微秒)。