Android App性能优化总结

优化方向

Android系统性能已经非常流畅了。但是,到了各大厂商手里,改源码自定系统,使得Android原生系统变得鱼龙混杂,然后到了不同层次的开发工程师手里,因为技术水平的参差不齐,即使很多手机在跑分软件性能非常高,打开应用依然存在卡顿现象。另外,随着产品内容迭代,功能越来越复杂,UI页面也越来越丰富,也成为流畅运行的一种阻碍。综上所述,对APP进行性能优化已成为开发者该有的一种综合素质,也是开发者能够完成高质量应用程序作品的保证。

本着人道主义,一切从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定不希望玩着玩着突然闪退,然后就是遇到画面内容很丰富的时候不希望卡顿与无响应,其次就是耗电和耗流量不希望太严重,最后就是版本更新的时候安装包希望能小一点。好了,四个方面总结如下:

  • 稳定(降低Crash率,不要在用户使用过程中崩溃)
  • 流畅(使用时避免出现卡顿和ANR,响应速度快,减少用户等待的时间)
  • 耗损(节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫)
  • 安装包(安装包小可以降低用户的安装成本)

一、稳定

Android 应用的稳定性定义很宽泛,影响稳定性的原因很多,比如内存使用不合理(内存泄漏、内存溢出)、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中代码逻辑导致的Crash情况非常多,无法在这里展开,最关键的还是内存泄漏与内存溢出导致的崩溃,我们着重优化。首先需要了解一下引用的强软弱虚的概念。

引用的强软弱虚

早在JDK1.2,Java就把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

强引用

Java默认就是强引用。当内存不足, jvm开始垃圾回收,对于强引用的对象,就算出现OOM异常也不会对该对象进行回收,Android内存泄露大部分都是强引用导致的。虽然把object1=null,然后object2还是存在。

Object object1 = new Object();
Object object2 = object1;
object1 = null;
System.gc();
System.out.println("object2="+object2);

使用场景
Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收强引用的对象来解决内存不足的场景。

软引用

软引用是相对强引用弱点的引用,需要使用SoftReference类来实现,当系统内存充足时,它不会被回收,当系统内存不足时,它会被回收,软引用通常使用在对内存比较敏感时使用。下面代码虽然我把object1=null了,但是从软引用中获取的对象还是不为null,这就是证明了在内存足够时候,不会回收,如果把内存配置成了10m或者更低就会回收。

Object object1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(object1);
object1=null;
System.gc();
System.out.println("object1="+object1);
System.out.println("软引用-->"+softReference.get());

使用场景
用来处理图片这种占用内存大的类。

View view = findViewById(R.id.some_view);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
Drawable drawable = new BitmapDrawable(bitmap);
SoftReference<Drawable> drawableSoftReference = new SoftReference<Drawable>(drawable);
if(drawableSoftReference != null) {
    view.setBackground(drawableSoftReference.get());
}

这样的好处是:通过软引用的get()方法,取得drawable对象实例的强引用,发现对象被未回收。在GC在内存充足的情况下,不会回收软引用对象。此时view的背景显示。内存吃紧,系统开始会GC。这次GC后,drawables.get()不再返回Drawable对象,而是返回null,这时屏幕上背景图不显示,说明在系统内存紧张的情况下,软引用被回收。使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

弱引用

弱引用需要用WeakReference来实现,它比软引用的生存期更短.不管内存是否够用,只要gc扫描到它都会被回。下面代码中,在gc之后object1内存会立刻被回收,weakReference.get()变为null。

Object object1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(object1);
System.out.println("object1="+object1);
System.out.println("弱引用中的对象="+weakReference.get());
object1 = null;
System.gc();
System.out.println("object1="+object1);
System.out.println("弱引用中的对象="+weakReference.get());

使用场景
静态内部类Handler声明非静态成员变量下的使用防止内存泄露。

public class MainActivity extends AppCompatActivity {
 
    private Handler handler  ;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler = new MyHandler( this ) ;
        ...
    }
 
    private static class MyHandler extends Handler {
        WeakReference<MainActivity> weakReference ;
 
        public MyHandler(MainActivity activity ){
            weakReference  = new WeakReference<MainActivity>( activity) ;
        }
 
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if ( weakReference.get() != null ){
                // update android ui
            }
        }
    }
}

虚引用

虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。它和没任何引用一样,在任何时候都有可能被垃圾回收, 它不能单独使用也不能通过它访问对象,虚引用必须配合ReferenceQueue一起使用,虚引用的作用主要是跟踪对象被垃圾回收的状态。使用例子:

Object object1 = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(object1, new ReferenceQueue<>());

使用场景
对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效。

内存泄漏

内存泄漏通俗的讲是一个本该被回收的对象却因为某些原因导致其不能回收 。我们都知道对象是有生命周期的,从生到死,当对象的任务完成之后,由Android系统进行垃圾回收。我们知道Android系统为某个App分配的内存是有限的(这个可能根据机型的不同而不同),当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,最终导致OOM(OutOfMemory)使程序崩溃。下面列举常见的内存泄漏:

错误使用单例造成的内存泄漏

单例模式或者静态方法长期持有Context对象,如果持有的Context对象生命周期与单例生命周期更短时,或导致Context无法被释放回收,则有可能造成内存泄漏。

public class SingleManager {

    private static SingleManager mInstance;
    private Context mContext;

    private SingleManager(Context context) {
    	//持有一般的context
        this.mContext = context;
    }

    public static SingleManager getInstance(Context context) {
        if (mInstance == null) {
            synchronized (SingleManager.class) {
                if (mInstance == null) {
                    mInstance = new SingleManager(context);
                }
            }
        }
        return mInstance;
    }
}

当我们在传入的Context是Activity时,在这个Activity销毁之后,这个单例还持有它的引用,造成内存泄漏。解决办法:引用的Context和AppLication的生命周期一样或者将Context声明成弱引用。

private SingleManager(Context context) {
	//用aplication的Context
	this.mContext = context.getApplicationContext();
	//private WeakReference<Context> mContext;
	//this.mContext = new WeakReference<>(context);
}

集合类造成内存泄漏

如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。比如:

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
}

在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。

除此之外,还有一些集合,虽然有添加和删除的逻辑,但是由于生命周期的冲突导致内存泄漏。


public class myApplication extends Application {
	...
    private List<Activity> list = new ArrayList<>();
    
    public void addActivity(Activity activity) {
    	list.add(activity);
	}

	public void exit() {
	    try {
	        for (Activity activity : list) {
	            if (activity != null)
	                activity.finish();
	        }
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}
}

在上面的逻辑中,在我们启动的Activity中,调用addActivity()方法添加启动的Activty,退出程序的地方,调用exit()方法,删除添加的Actiivty。这样做看似优雅,但是是使用的过程中,调用过myApplication.getInstance().addActivity(this)方法的Activity全都内存泄漏了!
为什么呢?这个List是在myApplication中的,这个myApplication的生命周期是整个App的生命周期,因此它自然要比单个Activity的生命周期要长。假如我们从一个Ativity A跳到了另一个Activity B,那么A就到了后台,假设这个时候系统内存不足了,想要回收他,却发现有一个和APP生命周期一样长的List还持有他的引用,完了,明明没有用的Activity实例却回收不了,自然就造成了内存泄漏。
所以这种看似优雅的方式,实际上使用不好就极为不优雅。其实解决上述问题的方法也很简单,回收不了是因为List持有的是Activity的强引用,我们只要想办法给list改为成弱引用即可。

private WeakReference<List<Activity>> list = new WeakReference<>(new ArrayList<>());
//list.get().add(activity);
//list.get().get(0).finish();

WebView造成内存泄露

关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。
最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 先从父控件中移除WebView
    mWebViewContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
}

属性动画造成内存泄露

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

资源未关闭或者注销造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Sqlite,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null。
比如下面的获取媒体库图片地址代码,在查询结束的时候一定要调用Cursor 的关闭方法防止造成内存泄漏。

String columns[] = new String[]{
	MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID, MediaStore.Images.Media.TITLE, MediaStore.Images.Media.DISPLAY_NAME
};
Cursor cursor = this.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, null);
if (cursor != null) {
    int photoIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
    //显示每张图片的地址,但是首先要判断一下,Cursor是否有值
    while (cursor.moveToNext()) {
        String photoPath = cursor.getString(photoIndex); //这里获取到的就是图片存储的位置信息
    }
    cursor.close();
}

Timer、TimerTask、AsyncTask等异步导致内存泄露

处理一个比较耗时的操作时,可能还没处理结束Activity就执行了退出操作,但是此时如果这些异步依然持有对Activity的引用就会导致AsynTaskActivity无法释放回收引发内存泄漏。

private void testAsyn(){
    asyncTask = new AsyncTask<Void, Void, Integer>() {
        @Override
        protected Integer doInBackground(Void... voids) {
            int i=0;
            //在任务没有结束的时候会一直持有Activity的引用
            while (!isCancelled()){
                i++;
                if(i>10000000000l){
                    break;
                }
            }
            return i;
        }

        @Override
        protected void onPostExecute(Integer integer) {
            super.onPostExecute(integer);
            mTextView.setText(String.valueOf(integer));
        }
    };
    asyncTask.execute();
    //或者无限循环任务,mTimerTask中会一直持有Activity的引用
    mTimer.schedule(mTimerTask, 3000, 3000);
}

由于它们一直持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel掉Timer、TimerTask、AsyncTask等异步操作,以避免发生内存泄漏。

Handler造成的内存泄漏

通过内部类的方式创建mHandler对象,此时mHandler会隐式地持有一个外部类对象引用这里就是HandlerActivity,当执行postDelayed方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

public class HandlerActivity extends AppCompatActivity {

    private Handler mHandler = new Handler();
    private TextView mTextView;
    
    private void textHandler() {
        //当退出Activity时Handler任务还没执行完毕就会造成内存泄漏.
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mTextView.setText("ceshi");
            }
        },60*1000);
    }
}

要想避免Handler引起内存泄漏问题,需要我们在Activity关闭退出的时候的移除消息队列中所有消息和所有的Runnable。

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
    mHandler=null;
}

非静态内部类创建静态实例造成的内存泄漏

内部类都会持有一个外部类引用,这里这个外部类就是MainActivity,然而内部类实例又是static静态变量其生命周期与Application生命周期一样,所以在MainActivity关闭的时候,内部类静态实例依然持有对MainActivity的引用,导致MainActivity无法被回收释放,引发内存泄漏。

public class MainActivity extends AppCompatActivity {

	private static TestResource mResource = null;
	
	@Override    
	protected void onCreate(Bundle savedInstanceState) {
	    super.onCreate(savedInstanceState);
	    setContentView(R.layout.activity_main);
	    if(mManager == null){
	    	mManager = new TestResource();
	    }
	}
	
	class TestResource {
	 	...
	}
} 

对于这种泄漏的解决办法就是将内部类改成静态内部类,不再持有MainActivity的引用即可,修改后的代码如下:

static class TestResource {
	 ...
}

匿名内部类造成的内存泄漏

非静态匿名内部类会默认持有外部类的引用,因此这个新创建的线程会持有 MainActivity 的引用。而如果要销毁这个 Activity 之前,线程还在运行的话就会造成该线程持有 MainActivity 的引用,造成 MainActivity 的资源无法回收导致内存泄漏。

public class MainActivity extends Activity {  

    private void exampleOne() {  
    	//匿名内部类,非静态的匿名类会持有外部类的一个隐式引用
        new Thread() {              		
			@Override  
            public void run() {  
                ...
            }  
        }.start();  
    }  
}

解决方案:将非静态匿名内部类修改为静态匿名内部类

 private static void exampleOne() {  
    	//匿名内部类,非静态的匿名类会持有外部类的一个隐式引用
        new Thread() {              		
			@Override  
            public void run() {  
                ...
            }  
        }.start();  
    }  

内存溢出

Dalvik 主要管理的内存有 Java heap 和 native heap 两大块,而对于一个安卓应用来说,由于手机设备的限制,一般应用使用的RAM不能超过某个设定值,如果你想要分配超过大于该分配值的内存的话,就会报Out Of Memory 错误。不同产商默认值不太一样,一般常见的有16M,24M,32M,48M。也就是说app的 Java heap + native heap < 默认值就不会内存溢出。查看内存限制的方法:

//查询当前APP的Heap Size阈值,单位是MB
int maxMemoryMB = ActivityManager.getMemoryClass();
//查看每个应用程序最高可用内存是多少,单位是KB
int maxMemoryKB = (int) (Runtime.getRuntime().maxMemory() / 1024);  

内存溢出跟内存泄漏有着密不可分的联系,当足够多的内存泄漏必然会导致内存溢出,这个上面已经讨论过,这里重点讨论怎么优化大内存使用场景防止内存溢出。

图片的内存消耗

图片时最常用也是最容易导致OOM的对象,在做优化之前需要科普一下Android中图片的基础知识。

常见的颜色模型有RGB、YUV、CMYK等,在大多数图像API中采用的都是RGB模型,Android也是如此;另外,在Android中还有包含透明度Alpha的颜色模型,即ARGB。在不考虑透明度的情况下,一个像素点的颜色值在计算机中的表示方法有以下3种:

  • 浮点数编码:比如float: (1.0, 0.5, 0.75),每个颜色分量各占1个float字段,其中1.0表示该分量的值为全红或全绿或全蓝;
  • 24位的整数编码:比如24-bit:(255, 128, 196),每个颜色分量各占8位,取值范围0-255,其中255表示该分量的值为全红或全绿或全蓝;
  • 16位的整数编码:比如16-bit:(31, 45, 31),第1和第3个颜色分量各占5位,取值范围0-31,第2个颜色分量占6位,取值范围0-63;

在Java中,float类型的变量占32位,int类型的变量占32位,short和char类型的变量都在16位,因此可以看出,用浮点数表示法编码一个像素的颜色,内存占用量是96位即12字节;而用24位整数表示法编码,只要一个int类型变量,占用4个字节(高8位空着,低24位用于表示颜色);用16位整数表示法编码,只要一个short类型变量,占2个字节;因此可以看出采用整数表示法编码颜色值,可以大大节省内存,当然,颜色质量也会相对低一些。在Android中获取Bitmap的时候一般也采用整型编码。

以上2种整型编码的表示法中,R、G、B各分量的顺序可以是RGB或BGR,Android里采用的是RGB的顺序,本文也都是遵循此顺序来讨论。在24位整型表示法中,由于R、G、B分量各占8位,有时候业内也以RGB888来指代这一信息;类似的,在16位整型表示法中,R、G、B分量分别占5、6、5位,就以RGB565来指代这一信息。

现在再考虑有透明度的颜色编码,其实方式与无透明度的编码方式一样:24位整型编码RGB模型采用int类型变量,其闲置的高8位正好用于放置透明度分量,其中0表示全透明,255表示完全不透明;按照A、R、G、B的顺序,就可以以ARGB8888来概括这一情形;而16位整型编码的RGB模型采用short类型变量,调整各分量所占为数分别至4位,那么正好可以空出4位来编码透明度值;按照A、R、G、B的顺序,就可以以ARGB4444来概括这一情形。回想一下Android的BitmapConfig类中,有ARGB_8888、ARGB_4444、RGB565等常量,现在可以知道它们分别代表了什么含义。同时也可以计算一张图片在内存中可能占用的大小,比如计算一张1920*1200的图片占用内存:

//ARGB_8888/ARGB_888
1920*1200*4/1024/1024=8.79MB
//ARGB_4444/RGB565
1920*1200*2/1024/1024=4.395MB

图片压缩优化

在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存,而且在性能上还可能会带来负面影响。图片的操作重点讲解BitmapFactory.Options这个类,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。通过设置BitmapFactory.Options中inSampleSize的值就可以实现倍数压缩:

//reqWidth和reqHeight为实际要显示控件的宽高值
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {  
    // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小  
    final BitmapFactory.Options options = new BitmapFactory.Options();  
    options.inJustDecodeBounds = true;  
    BitmapFactory.decodeResource(res, resId, options);  
    // 调用上面定义的方法计算inSampleSize值  
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);  
    // 使用获取到的inSampleSize值再次解析图片  
    options.inJustDecodeBounds = false;  
    return BitmapFactory.decodeResource(res, resId, options);  
} 

public static int calculateInSampleSize(BitmapFactory.Options options,  int reqWidth, int reqHeight) {  
    // 源图片的高度和宽度  
    final int height = options.outHeight;  
    final int width = options.outWidth;  
    //String imageType = options.outMimeType; 
    int inSampleSize = 1;  
    if (height > reqHeight || width > reqWidth) {  
        // 计算出实际宽高和目标宽高的比率  
        final int heightRatio = Math.round((float) height / (float) reqHeight);  
        final int widthRatio = Math.round((float) width / (float) reqWidth);  
        // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高  
        // 一定都会大于等于目标的宽和高。  
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;  
    }  
    return inSampleSize;  
} 

图片缓存优化

在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用RecyclerView、ListView、GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片,频繁申请内存释放内存,增加了内存的开销,我们应该想办法去避免这个情况的发生。

这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。市面缓存技术很多,Picasso为追求小而美,有功能取舍,比如,它无法支持下载动态图片。还有Google的Glide或Facebook的Fresco,它们各有特点,Glide比较小巧,Fresco性能好。如果自己实现的话,可以使用一种叫作LRU(least recently used,最近最少使用)的存储策略。基于该种策略,当存储空间用尽时,缓存会自动清除最近最少使用的对象。

初始化LruCache来缓存Bitmap

LruCache<String, Bitmap> mLruCache;
//获取手机最大内存,单位kb
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//一般都将1/8设为LruCache的最大缓存
int cacheSize = maxMemory / 8;
mLruCache = new LruCache<String, Bitmap>(maxMemory / 8) {
    /**
    * 这个方法从源码中看出来是设置已用缓存的计算方式的
    * 默认返回的值是 1,也就是每缓存一张图片就将已用缓存大小加 1
    * 缓存图片看的是占用的内存的大小,每张图片的占用内存也是不一样的
    * 因此要重写这个方法,手动将这里改为本次缓存的图片的大小
    */
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount() / 1024;
    }
};

使用

//加入缓存
mLruCache.put("key", bitmap);
//从缓存中读取
Bitmap bitmap = mLruCache.get("key");

临时Bitmap的及时回收

临时创建的Bitmap用完之后一定不要忘记主动释放内存

if(bitmap != null) {
	bitmap.recycle();
}

使用合适的数据结构

Android为移动操作系统特意编写了一些更加高效的容器SparseArray和 ArrayMap,在特定情况下用来替换HashMap数据结构。合理运用这些数据结构将为我们节省内存空间。下面我们进行对比:

HashMap

创建一个 hashMap时, 默认是一个容量为16的数组,数组中的每一个元素却又是一个链表的头节点。或者说HashMap内部存储结构 是使用哈希表的拉链结构(数组+链表,这种存储数据的方法 叫做拉链法。在这里插入图片描述
缺点

  • 就算没有数据,也需要分配默认16个元素的数组
  • 一旦数据量达到Hashmap限定容量的75%,就将按两倍扩容,当我们有几十万、几百万数据时,hashMap 将造成内存空间的消耗和浪费。

优点

  • 数据较大的时候(1000级别),hash查找效率比二分法高

HashMap获取数据是通过遍历Entry[]数组来得到对应的元素,在数据量很大时候会比较慢,所以在Android中,HashMap是比较费内存的,我们在一些情况下可以使用SparseArray和ArrayMap来代替HashMap。

SparseArray

SparseArray比HashMap更省内存,默认初始size为0,每次增加元素,size++。在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型)。它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间。而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快,但是在数据量大的情况下性能并不明显,将降低至少50%。删除元素的时候,咱们先不删除,通过value赋值的方式,给它贴一个标签,然后我们再gc的时候再根据这个标志进行压缩和空间释放,在添加元素的时候,我们发现如果对应的元素正好被标记了“删除”,那么我们直接覆盖它即可,在效率上是一个很可观的提高。

优点

  • 占用内存小
  • 放弃hash查找,使用二分查找,1000级别以下效率更高。
  • 频繁的插入删除操作效率高(延迟删除机制保证了效率)
  • 避免存取元素时的装箱和拆箱,性能更好

缺点

  • 二分查找的时间复杂度O(log n),1000级别以上数据量下,效率没有HashMap高
  • key只能是int 或者long

ArrayMap

ArrayMap是一个<key,value>映射的数据结构,key可以为任意的类型,它设计上更多的是考虑内存的优化,内部是使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值,它和SparseArray一样,也会对key使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作,所以,应用场景和SparseArray的一样,如果在数据量比较大的情况下,那么它的性能将退化至少50%。

优点

  • 1000以内数据量,内存利用率高,及时的空间压缩机制
  • 迭代效率高,可以使用索引来迭代(keyAt()方法以及valueAt() 方法),相比于HashMap迭代使用迭代器模式,效率要高很多
  • key可以为任意的类型

缺点

  • 二分查找的时间复杂度O(log n),1000级别以上数据量下,效率没有HashMap高
  • 没有实现Serializable,不利于在Android中借助Bundle传输。
  • 存取数据复杂度高,花费大

应用场景

  • 首先二者都是适用于数据量小(1000以内)的情况,但是SparseArray以及他的三兄弟们避免了自动装箱和拆箱问题,也就是说在特定场景下,比如你存储的value值全部是int类型,并且key也是int类型,那么就采用SparseArray,其它情况就采用ArrayMap。
  • 数据量多的时候当然还是使用HashMap啦

不要使用Enum枚举

Android官方的Training课程里面有下面这样一句话:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

Android官方强烈建议不要在Android程序里面使用到enum,因为会比静态增加两倍以上的内存。enum编译生成dex后的class文件,每个变量都是一个对象,并且还有一个value对象数组,不仅吃内存,还吃字节数,加大apk的大小。可用注解替代枚举。

private static final int ADD = 0;
private static final int SUB = 1;
private static final int MUL = 2;
private static final int DIV = 3;

@IntDef({ADD,SUB,MUL,DIV})
@Retention(RetentionPolicy.SOURCE)
public @interface Operation{}

public void text(@Operation int operation){
	//...
}
text(ADD);

Try catch某些大内存分配的操作

在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。

不要使用String进行字符串拼接

使用String的“+”拼接,每次都会生成一个StringBuilder对象,当大量操作的时候会有大量的内存垃圾产生,会导致OOM以及内存抖动(频繁创建对象以及频繁GC)。

频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。

二、流畅

Android 应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。总的来说造成卡顿的原因有如下几种:

  • UI的绘制上

绘制的层级深、页面复杂、刷新不合理与过度绘制,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。

  • 数据处理上

导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是耗时数据在主线程处理,这个是初级工程师会犯的错误。二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。

ANR

一般轻微的卡顿还好,如果一些比较严重的卡顿会造成ANR,全称:Application Not Responding,也就是应用程序无响应。

Android系统中,ActivityManagerService(简称AMS)和WindowManagerService(简称WMS)会检测App的响应时间,如果App在特定时间无法相应屏幕触摸或键盘输入时间,或者特定事件没有处理完毕,就会出现ANR。产生ANR后,会有Log日志以及生成“/data/anr/traces.txt”日志文件。

造成ANR发生的情况

  • InputDispatching Timeout:5秒内无法响应屏幕触摸事件或键盘输入事件
  • BroadcastQueue Timeout:在执行前台广播(BroadcastReceiver)的+ onReceive()函数时10秒没有处理完成,后台为60秒。
  • Service Timeout:前台服务20秒内,后台服务在200秒内没有执行完毕。
  • ContentProvider Timeout:ContentProvider的publish在10s内没进行完。

ANR重现

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_anr_test);
    // 这是Android提供线程休眠函数,与Thread.sleep()最大的区别是
    // 该使用该函数不会抛出InterruptedException异常。
    SystemClock.sleep(20 * 1000);
}

traces.txt日志分析

Cmd line: com.yhd.anrtest ----> ANR产生包名
...
at java.lang.Thread.sleep(Native method) ----> 产生ANR的基础类方法
...
at android.os.SystemClock.sleep(SystemClock.java:122) ----> 产生ANR的方法
at com.yhd.anrtest.ANRTestActivity.onCreate(ANRTestActivity.java:20) ----> 产生ANR的行数
...

通过上方日志很容易看出原因,但是特别注意的是:产生新的ANR,原来的 traces.txt 文件会被覆盖,也就是traces.txt只保留最后一次发生ANR时的信息。如果想查看历史日志,可以查看DropBox。DropBox保留历史上发生的所有ANR的log。“/data/system/dropbox”是DB指定的文件存放位置。日志保存的最长时间, 默认是3天。

shell@yinhaide:/data/system/dropbox # ls
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]

要讨论解决卡顿的方案,就要先了解 Android 系统的渲染机制。

Android渲染机制

我们首先需要知道一个大概是生物领域的一个知识点

人眼与大脑之间的协作无法感知超过60fps的画面更新。12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的 效果。24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。但是低于30fps是 无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的(据说Dart能够带来120fps的体验)。

大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。从设计师的角度,他们希望App能够有更多的动画,图片等时尚元素来实现流畅的用 户体验。但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染, 如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。
在这里插入图片描述
如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。
在这里插入图片描述
用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原 因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行 的次数过多。这些都会导致CPU或者GPU负载过重。VSync机制就像是一台转速固定的发动机(60转/s)。每一转会带动着去做一些UI相关的事情,但不是每一转都会有工作去做(就像有时在空挡,有时在D档)。有时候因为各种阻力某一圈工作量比较重超过了16.6ms,那么这台发动机这秒内就不是60转了,当然也有可能被其他因素影响,比如给油不足(主线程里干的活太多)等等,就会出现转速降低的状况。我们把这个转速叫做流畅度。当流畅度越小的时候说明当前程序越卡顿。

我们可以通过一些工具来定位问题,比如可以使用HierarchyViewer来查找Activity中的布局是否过于复杂,也可以使用手机设置里 面的开发者选项,打开Show GPU Overdraw等选项进行观察。你还可以使用TraceView来观察CPU的执行情况,更加快捷的找到性能瓶颈。

UI的绘制优化减少卡顿

Overdraw的理解与优化建议

Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
在这里插入图片描述
当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。幸运的是,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,可以观察UI上的Overdraw情况。

设置 -> 开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制

在这里插入图片描述
蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面 的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加 蓝色区域的占比。这一措施能够显著提升程序性能。

Overdraw 的处理方案一:移除不必要的background

在这里插入图片描述

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white"
    android:orientation="horizontal">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:orientation="horizontal">
        <ImageView
            android:id="@+id/chat_author_avatar1"
            android:layout_width="@dimen/left_width_height"
            android:layout_height="@dimen/left_width_height"
            android:layout_margin="@dimen/around_margin"
            android:src="@mipmap/ic_launcher"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="vertical">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:text="@string/up_text" />
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:text="@string/down_text"/>
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

我们发现,不居中确实出现了好多次的backgound的配置,所以移除一些不必要的background:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <ImageView
        android:id="@+id/chat_author_avatar1"
        android:layout_width="@dimen/left_width_height"
        android:layout_height="@dimen/left_width_height"
        android:layout_margin="@dimen/around_margin"
        android:src="@mipmap/ic_launcher"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/up_text" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/down_text"/>
    </LinearLayout>
</LinearLayout>

在这里插入图片描述
明显有改善了,所以在我们实际生产中,需要非常有耐心的去不断诊断,不断去调试,有些时候我们使用ImageView的时候,可能会给它设置一个background,在代码中,还有可能给它设了一个imageDrawable,从而发生过度绘制的情况,切记切记。解决方案是把背景图和真正加载的图片都通过imageDrawable方法进行设置。

还有一个注意点,我们的这个Activity对应的layout布局最终会添加在DecorView中,因为可视部分是Activity,如果这个DecorView会的的背景没有必要的话,我们可以调用mDecor.setWindowBackground(drawable)去掉无关背景,那么可以在Activity调用getWindow().setBackgroundDrawable(null)。

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	//清除DecorView的背景色
	getWindow().setBackgroundDrawable(null);
}

Overdraw 的处理方案二:clipRect的妙用

对于那些过于复杂的自定义的View(重写了onDraw方法),Android系统无法检测具体在onDraw里面会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来 帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠 组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执 行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

在这里插入图片描述
上图书开启Show Override GPU之后的效果,可以看到,卡片叠加处明显的过度渲。

public class CardView extends View{

    private Bitmap[] mCards = new Bitmap[3];
    private int[] mImgId = new int[]{R.drawable.alex, R.drawable.chris, R.drawable.claire};

    public CardView(Context context) {
        super(context);
        Bitmap bm = null;
        for (int i = 0; i < mCards.length; i++){
            bm = BitmapFactory.decodeResource(getResources(), mImgId[i]);
            mCards[i] = Bitmap.createScaledBitmap(bm, 400, 600, false);
        }
        setBackgroundColor(0xff00ff00);
    }

    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(20, 120);
        for (Bitmap bitmap : mCards){
            canvas.translate(120, 0);
            canvas.drawBitmap(bitmap, 0, 0, null);
        }
        canvas.restore();
    }
}

考虑下如何去优化,其实很明显哈,我们上面已经说了使用cliprect方法,那么我们目标直指自定义View的onDraw方法。 修改后的代码:

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);
    canvas.save();
    canvas.translate(20, 120);
    for (int i = 0; i < mCards.length; i++){
        canvas.translate(120, 0);
        canvas.save();
        if (i < mCards.length - 1){
            canvas.clipRect(0, 0, 120, mCards[i].getHeight());
        }
        canvas.drawBitmap(mCards[i], 0, 0, null);
        canvas.restore();
    }
    canvas.restore();
}

分析得出,除了最后一张需要完整的绘制,其他的都只需要绘制部分;所以我们在循环的时候,给i到n-1都添加了clipRect的代码。最后的效果图:

在这里插入图片描述
可以看到,所有卡片变为了淡紫色,对比参照图,都是1X过度绘制,那么是因为我的View添加了一个ff00ff00的背景,可以说明已经是最优了。
如果你按照上面的修改,会发现最终效果图不是淡紫色,而是青色(2X),那是为什么呢?因为你还忽略了 一个优化的地方,本View已经有了不透明的背景,完全可以移除Window的背景了,即在Activity中,添加getWindow().setBackgroundDrawable(null)。

除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作,这里就不多讲了。

Overdraw 的处理方案三:巧用Hierarchy Viewe

我们平时在做UI布局优化的时候,时常提起的一个工具Hierarchy Viewer,它提供了一个很直观的可视化界面来观测布局界面的层级,可以检查布局层次结构中每个视图的属性和布局速度。它可以帮助我们查找由视图层次结构导致的性能瓶颈,从而帮助我们简化层次结构并减少过度绘制(Overdraw)的问题。
在这里插入图片描述
具体怎么使用这就不多讲,读者自行查找资料~

使用include 和merge标签减少布局嵌套

相信大家使用的最多的布局标签就是 include了。include的用途就是将布局中的公共部分提取出来以供其他Layout使用,从而实现布局的优化。布局复用的步骤大致为:

  • 1、创建一个正常的可用布局layout文件A_layout.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="张三" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="李四" />
</merge>
  • 2、在需要添加复用布局(A_layout.xml)的当前布局内B_layout.xml,使用include标签
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <include layout="@layout/A_layout"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />
</LinearLayout>

include使用起来很简单,只需要指定一个layout属性为需要包含的布局文件即可。如果include设置id后,原有的根布局Id已经被替换为在 include中指定的id了,所以在 findViewById查找原有id的时候就会报空指针异常。

注意点:

  • 使用include的时候可以不用填写“android:layout_width”与“android:layout_height”两个参数。
  • 非layout属性则无法在include标签当中进行覆写。
  • 如果我们想要在include标签当中覆写layout属性,必须要将layout_width和layout_height这两个属性也进行覆写,否则覆写效果将不会生效。
  • merge标签是作为include标签的一种辅助扩展来使用的,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套。
  • merge必须放在布局文件的根节点上;
  • merge并不是一个ViewGroup,也不是一个View,它相当于声明了一些视图,等待被添加。
  • merge标签被添加到A容器下,那么merge下的所有视图将被添加到A容器下。
  • 因为merge标签并不是View,所以在通过LayoutInflate.inflate方法渲染的时候, 第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。
  • 如果Activity的布局文件根节点是FrameLayout,可以替换为merge标签,这样,执行setContentView之后,会减少一层FrameLayout节点。
  • 自定义View如果继承LinearLayout,建议让自定义View的布局文件根节点设置成merge,这样能少一层结点。
  • 因为merge不是View,所以对merge标签设置的所有属性都是无效的。

使用ViewStub懒加载减少渲染元素

ViewStub标签实质上是一个宽高都为 0 的不可见 View. 通过延迟加载布局的方式优化布局提升渲染性能.

这里的延迟加载是指初始化时, 程序无需显示该标签所指向的布局文件, 只有在特定的条件下, 所指向的布局文件才需要被渲染, 且此布局文件直接将当前的 ViewStub替换掉. 但这里的替换并不是完全意义上的替换, 布局文件的 layout params 是以 ViewStub 为优先.

当初次渲染布局文件时, ViewStub 控件虽然也占据内存, 但是相比于其他控件, 它所占内存很小. 它主要是作为一个“占位符”, 放置于 View Tree中, 且它本身是不可见的.

使用场景

通常用于不常使用的控件. 比如

  • 网络请求失败的提示
  • 列表为空的提示
  • 新内容、新功能的引导, 因为引导基本上只显示一次
  • 通用的自定义 View,但其中部分子 View 只在部分情况下才显示.

下面以在一个布局main.xml中加入网络错误时的提示页面network_error.xml为例。main.mxl代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <ViewStub
        android:id="@+id/network_error_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/network_error" />
</RelativeLayout>

其中network_error.xml为只有在网络错误时才需要显示的布局,默认不会被解析,示例代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/network_setting"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="@string/network_setting" />
    <Button
        android:id="@+id/network_refresh"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_below="@+id/network_setting"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/dp_10"
        android:text="@string/network_refresh" />
</RelativeLayout>

在代码中通过(ViewStub)findViewById(id)找到ViewStub,通过stub.inflate()展开ViewStub,然后得到子View,如下:

private View networkErrorView;

private void showNetError() {
  if (networkErrorView != null) {
    networkErrorView.setVisibility(View.VISIBLE);
  }else{
    ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
    if(stub !=null){
      networkErrorView = stub.inflate();
      //  下面两行代码效果和上面一行是一样的
      //  ViewStub被展开后的布局所替换,setVisibility首次执行会触发stub.inflate
      //  stub.setVisibility(View.VISIBLE);   
      //  networkErrorView =  findViewById(R.id.network_error_layout); // 获取展开后的布局
    }
 }
}

private void showNormal() {
  if (networkErrorView != null) {
    networkErrorView.setVisibility(View.GONE);
  }
}

在上面showNetError()中展开了ViewStub,同时我们对networkErrorView进行了保存,这样下次不用继续inflate。这里我对ViewStub的实例进行了一个非空判断,这是因为ViewStub在XML中定义的id只在一开始有效,一旦ViewStub中指定的布局加载之后,这个id也就失败了,那么此时findViewById()得到的值也会是空。

要被加载的布局通过 android:layout 属性来设置. 然后在程序中调用 inflate() 方法来加载. 还可以设定 Visibility 为 VISIBLE 或 INVISIBLE, 也会触发 inflate(). 但只有直接使用 inflate() 方法能返回布局文件的根 View. 但是这里只会在首次使用 setVisibility() 会加载要渲染的布局文件. 再次使用只是单纯的设置可见性。对 inflate() 操作也只能进行一次, 因为 inflate() 的时候是其指向的布局文件替换掉当前 ViewStub标签. 之后, 原来的布局文件中就没有ViewStub标签了. 因此, 如果多次 inflate() 操作, 会报错:

ViewStub must have a non-null ViewGroup viewParent

注意

  • ViewStub所加载的布局不可以使用merge标签。

merge是include的辅助类,ViewStub不能使用。因此这有可能导致加载出来的布局存在着多余的嵌套结构,具体如何去取舍就要根据各自的实际情况来决定了,对于那些隐藏的布局文件结构相当复杂的情况,使用ViewStub还是一种相当不错的选择的,即使增加了一层无用的布局结构,仍然还是利大于弊。

ViewStub vs View.GONE

我们经常会遇到这样的情况,运行时动态根据条件来决定显示哪个View或布局。常用的做法是把View都写在上面,先把它们的可见性都设为View.GONE,然后在代码中动态的更改它的可见性。这样的做法的优点是逻辑简单而且控制起来比较灵活,不占位的隐藏,内存不足时,资源会被优先回收掉,再次显示时会重新绘制。但是它的缺点就是,耗费资源。虽然把View的初始可见View.GONE,但是在Inflate布局的时候View仍然会被Inflate,也就是说仍然会创建对象,会被实例化,会被设置属性。也就是说,会耗费内存等资源。结论:

  • View.GONE仍然会被Inflate,会正常占用内存等资源

ViewStub是一个轻量级的View,它一个看不见的,不占布局位置,占用资源非常小的控件。可以为ViewStub指定一个布局,在Inflate布局的时候,只有ViewStub会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。这样,就可以使用ViewStub来方便的在运行时,要还是不要显示某个布局。结论:

  • ViewStub非常轻量级,占用资源非常小

不能使用ScrollView包裹列表控件

使用ScrollView包裹ListView/GridView/ExpandableListVIew/RecyclerView,会把 ListView 的所有 Item 都加载到内存中,要消耗巨大的内存和 cpu 去绘制图面。说明:ScrollView 中嵌套 List 或 RecyclerView 的做法官方明确禁止。除了开发过程中遇到的各种视觉和交互问题,这种做法对性能也有较大损耗。ListView 等 UI 组件自身有垂直滚动功能,也没有必要在嵌套一层 ScrollView。目前为了较好的 UI 体验,更贴近 Material Design 的设计,推荐使用 NestedScrollView。

RelativeLayout和LinearLayout性能比较

我们要探讨的性能问题,说的简单明了一点就是:当RelativeLayout和LinearLayout分别作为ViewGroup,表达相同布局时绘制在屏幕上时谁更快一点。我们分别来追踪下RelativeLayout和LinearLayout这三大流程的执行耗时。

LinearLayout

  • Measure:0.738ms
  • Layout:0.176ms
  • draw:7.655ms

RelativeLayout

  • Measure:2.280ms
  • Layout:0.153ms
  • draw:7.696ms

从这个数据来看无论使用RelativeLayout还是LinearLayout,layout和draw的过程两者相差无几,考虑到误差的问题,几乎可以认为两者不分伯仲,关键是Measure的过程RelativeLayout却比LinearLayout慢了一大截。我们从源码分析:

RelativeLayout->onMeasure

@Override    
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
	...
	//横向测量全部的子View
	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();  
		    applyHorizontalSizeRules(params, myWidth);  
		    measureChildHorizontal(child, params, myWidth, myHeight);  
		    if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {  
		          offsetHorizontalAxis = true;  
		    }  
		}  
	}  
	//纵向测量全部的子View
	views = mSortedVerticalChildren;  
	count = views.length;  
	for (int i = 0; i < count; i++) {  
		View child = views[i];  
		if (child.getVisibility() != GONE) {  
		    LayoutParams params = (LayoutParams) child.getLayoutParams();  
		    applyVerticalSizeRules(params, myHeight);  
		    measureChild(child, params, myWidth, myHeight);  
		    ...
		}  
	}  
  ...  
}

根据上述关键代码,RelativeLayout分别对所有子View进行两次measure,横向纵向分别进行一次,这是为什么呢?首先RelativeLayout中子View的排列方式是基于彼此的依赖关系,而这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置的时候,需要先给所有的子View排序一下。又因为RelativeLayout允许A,B 2个子View,横向上B依赖A,纵向上A依赖B。所以需要横向纵向分别进行一次排序测量。 mSortedHorizontalChildren和mSortedVerticalChildren是分别对水平方向的子控件和垂直方向的子控件进行排序后的View数组。除此之外,RelativeLayout还有另一个性能问题 。先看看View的onMeasure方法都做了啥。

View->onMeasure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
		//是否强制刷新
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        //View位置是否有变化
        final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly 
        || !isSpecExactly || !matchesSpecSize);
        if (forceLayout || needsLayout) {
			...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //重新测量
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
        }
        ...
    }

View的measure方法里对绘制过程做了一个优化,如果我们或者我们的子View没有要求强制刷新,而父View给子View的传入值也没有变化(也就是说子View的位置没变化),就不会做无谓的measure。但是上面已经说了RelativeLayout要做两次measure,而在做横向的测量时,纵向的测量结果尚未完成,只好暂时使用myHeight传入子View系统,假如子View的Height不等于(设置了margin)myHeight的高度,那么measure中上面代码所做得优化将不起作用,这一过程将进一步影响RelativeLayout的绘制性能。而LinearLayout则无这方面的担忧。解决这个问题也很好办,如果可以,尽量使用padding代替margin,所以一个重要的结论是:

使用RelativeLayout的情况下,请用padding代替margin提高性能。

LinearLayout->onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

//LinearLayout会先做一个简单横纵方向判断,我们选择纵向这种情况继续分析  
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	...
	for (int i = 0; i < count; ++i) {    
	     final View child = getVirtualChildAt(i);    
	     //... child为空、Gone以及分界线的情况略去  
	     //累计权重  
	     LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();    
	     totalWeight += lp.weight;    
	     //计算
	     if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {    
	     	//精确模式的情况下,子控件layout_height=0dp且weight大于0无法计算子控件的高度  
	     	//但是可以先把margin值合入到总值中,后面根据剩余空间及权值再重新计算对应的高度  
	      	final int totalLength = mTotalLength;    
	     	mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);    
	     } else {    
	        if (lp.height == 0 && lp.weight > 0) {    
	        	//如果这个条件成立,就代表 heightMode不是精确测量以及wrap_conent模式  
	         	//也就是说布局是越小越好,你还想利用权值多分剩余空间是不可能的,只设为wrap_content模式  
	          	lp.height = LayoutParams.WRAP_CONTENT;    
	         }    
	         // 子控件测量  
	         measureChildBeforeLayout(child, i, widthMeasureSpec,0, heightMeasureSpec,totalWeight== 0 ? mTotalLength :0);           
	         //获取该子视图最终的高度,并将这个高度添加到mTotalLength中  
	         final int childHeight = child.getMeasuredHeight();    
	         final int totalLength = mTotalLength;    
	         mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));   
		}  
	} 
    ...  
}

源码中已经标注了一些注释,需要注意的是在每次对child测量完毕后,都会调用child.getMeasuredHeight()获取该子视图最终的高度,并将这个高度添加到mTotalLength中。但是getMeasuredHeight暂时避开了lp.weight>0的子View,因为后面会将把剩余高度按weight分配给相应的子View。因此可以得出以下结论:

  • 如果我们在LinearLayout中不使用weight属性,将只进行一次measure的过程。
  • 如果使用了weight属性,LinearLayout在第一次测量时避开设置过weight属性的子View,之后再对它们做第二次measure。由此可见,weight属性对性能是有影响的,而且本身有大坑,请注意避让。

似乎看起来Linearlayout比relativelayout性能更好。

但是谷歌的官方说明

A RelativeLayout is a very powerful utility for designing a user interface because it can eliminate nested view groups and keep your layout hierarchy flat, which improves performance. If you find yourself using several nested LinearLayout groups, you may be able to replace them with a single RelativeLayout.

Google的意思是“性能至上”, RelativeLayout 在性能上更好,因为在诸如 ListView/RecyclerView等控件中,使用 LinearLayout 容易产生多层嵌套的布局结构,这在性能上是不好的。而 RelativeLayout 因其原理上的灵活性,通常层级结构都比较扁平,很多使用LinearLayout 的情况都可以用一个 RelativeLayout 来替代,以降低布局的嵌套层级,优化性能。所以从这一点来看,Google比较推荐开发者使用RelativeLayout,因此就将其作为Blank Activity的默认布局了。

不能单纯说Linearlayout和Relativelayout谁性能好就用谁,还要结合布局层级要,层级优先。

LinearLayout vs FrameLayout

LinearLayout
Measure:2.058ms
Layout:0.296ms
draw:3.857ms

FrameLayout
Measure:1.334ms
Layout:0.213ms
draw:3.680ms

结论

  • 三种常见的ViewGroup的同层级下绘制速度:FrameLayout> LinerLayout> RelativeLayout
  • ConstraintLayout是一个更高性能的消灭布局层级的神器
  • RecycleView中item 一般用ConstraintLayout或直接使用控件来布局,以业务需求为准。
  • 使用布局优先级:FrameLayout>ConstraintLayout>LinearLayout>RelativeLayout,但要结合效率和需求实现
  • RelativeLayout会让子View调用2次onMeasure,LinearLayout 在有weight时,也会调用子View 2次onMeasure
  • RelativeLayout的子View如果高度和自己高度不同,则会引发多次测量导致的效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin提高性能,LinearLayout没有这个问题。
  • 在不影响层级深度的情况下,使用LinearLayout和FrameLayout而不是RelativeLayout。
  • 优先考虑布局层级,如果LinearLayout无法减少布局层级,请用RelativeLayout。
  • RelativeLayout将对所有的子View进行两次measure,而LinearLayout在使用weight属性进行布局时也会对子View进行两次measure,如果他们位于整个View树的顶端时并可能进行多层的嵌套时,位于底层的View将会进行大量的measure操作,大大降低程序性能。因此,应尽量将RelativeLayout和LinearLayout置于View树的底层,并减少嵌套。
  • ListView/RecyclerView等控件中,列表项布局使用 LinearLayout 容易产生多层嵌套的布局结构,这在性能上是不好的。而 RelativeLayout 因其原理上的灵活性,通常层级结构都比较扁平,很多使用LinearLayout 的情况都可以用一个 RelativeLayout 来替代,以降低布局的嵌套层级,优化性能。
  • 尽量减少使用wrap_content,推荐使用mathch_parent或固定尺寸配合gravity=“center”,因为 在测量过程中,match_parent和固定宽高度对应EXACTLY,而wrap_content对应AT_MOST,这两者对比AT_MOST耗时较多。

数据处理优化减少卡顿

  • 人为在UI线程中做轻微耗时操作,导致UI线程卡顿

主线程也叫UI线程主要的任务是处理用户交互、绘制界面、显示数据、消息处理等工作,如果耗时操作如请求网络数据、操作数据库、读取文件等就不能放在主线程来,可以开启子线程来操作。使用线程池来代替单独创建子线程,因为频繁的创建和销毁线程很耗时,创建太多的子线程也会抢占主线程的CPU使用,从而导致卡顿。

  • 同一时间动画执行的次数过多,导致CPU或GPU负载过重;

动画的执行频率一定不能过快,需要在一个合理的范围之内,繁殖卡顿

  • 内存抖动导致暂时阻塞渲染操作,造成卡顿

内存泄漏的积累和大量对象的创建,都容易触发GC过多造成内存抖动。内存抖动会导致暂时阻塞渲染操作,造成卡顿。我们需要在代码中尽量避免内存泄漏的发生以及大量对象的创建。

  • 工作线程优先级未设置为Process.THREAD_PRIORITY_BACKGROUND导致后台线程抢占UI线程cpu时间片,阻塞渲染操作

异步不总是灵丹妙药,不正确的异步方式不仅不能较好的完成异步任务,反而会加剧卡顿。UI线程优先级为THREAD_PRIORITY_DEFAULT = 0。线程优先级有继承性,如果从UI线程启动,则该线程优先级默认为Default,会平等的和UI线程争夺CPU资源。线程数一旦数量增加,抢占明显,造成卡顿。这一点尤其需要注意,在对UI性能要求高的场景下建议将线程优先级设置为THREAD_PRIORITY_BACKGROUND = 10(值越大优先级越低),以此降低与主线程竞争的能力。

new Thread () {
    @Override
    public void run() {
      super.run();
        android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    }
}.start();

三、耗损

电量(重点),电量是我们需要认真考虑的一方面,手机的续航能力是现在用户关注的一个点,如果手机电量消耗过快,用户可能会卸载那些消耗电量过大的应用。

根据物理学中的知识,功 = 电压 * 电流 * 时间,但是一部手机中,电压值U正常来说是不会变的,所以可以忽略,只通过电流和时间就可以表示电量。模块电量(mAh) = 模块电流(mA) * 模块耗时(h)。模块耗时比较容易理解,但是模块电流怎样获取呢?以Nexus 6P为例,使用apktool,对/system/framework/framework-res.apk进行反解析,获取到手机里面的power_profile.xml文件,文件中定义了该手机各耗电模块在不同状态下的电流值,内容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<device name="Android">
    <item name="none">0</item>
    <item name="screen.on">169.4278765</item>
    <item name="screen.full">79.09344216</item>
    <item name="bluetooth.active">25.2</item>
    <item name="bluetooth.on">1.7</item>
    <item name="wifi.on">21.21733311</item>
    <item name="wifi.active">98.04989804</item>
    <item name="wifi.scan">129.8951166</item>
    <item name="dsp.audio">26.5</item>
    <item name="dsp.video">242.0</item>
    <item name="gps.on">5.661105191</item>
    <item name="radio.active">64.8918361</item>
    <item name="radio.scanning">19.13559783</item>
    <array name="radio.on">
        <value>17.52231575</value>
        <value>5.902211798</value>
        <value>6.454893079</value>
        <value>6.771166916</value>
        <value>6.725541238</value>
    </array>
    <array name="cpu.speeds.cluster0">
        <value>384000</value>
        <value>460800</value>
        <value>600000</value>
        <value>672000</value>
        <value>768000</value>
        <value>864000</value>
        <value>960000</value>
        <value>1248000</value>
        <value>1344000</value>
        <value>1478400</value>
        <value>1555200</value>
    </array>
    <array name="cpu.speeds.cluster1">
        <value>384000</value>
        <value>480000</value>
        <value>633600</value>
        <value>768000</value>
        <value>864000</value>
        <value>960000</value>
        <value>1248000</value>
        <value>1344000</value>
        <value>1440000</value>
        <value>1536000</value>
        <value>1632000</value>
        <value>1728000</value>
        <value>1824000</value>
        <value>1958400</value>
    </array>
    <item name="cpu.idle">0.144925583</item>
    <item name="cpu.awake">9.488210416</item>
    <array name="cpu.active.cluster0">
        <value>202.17</value>
        <value>211.34</value>
        <value>224.22</value>
        <value>238.72</value>
        <value>251.89</value>
        <value>263.07</value>
        <value>276.33</value>
        <value>314.40</value>
        <value>328.12</value>
        <value>369.63</value>
        <value>391.05</value>
    </array>
    <array name="cpu.active.cluster1">
        <value>354.95</value>
        <value>387.15</value>
        <value>442.86</value>
        <value>510.20</value>
        <value>582.65</value>
        <value>631.99</value>
        <value>812.02</value>
        <value>858.84</value>
        <value>943.23</value>
        <value>992.45</value>
        <value>1086.32</value>
        <value>1151.96</value>
        <value>1253.80</value>
        <value>1397.67</value>
    </array>
    <array name="cpu.clusters.cores">
        <value>4</value>
        <value>4</value>
    </array>
    <item name="battery.capacity">3450</item>
    <array name="wifi.batchedscan">
        <value>.0003</value>
        <value>.003</value>
        <value>.03</value>
        <value>.3</value>
        <value>3</value>
    </array>
</device>

从文件内容中可以看到,power_profile.xml文件中,定义了消耗电量的各模块。如下图所示:
在这里插入图片描述
看一看出,App电量 = ∑App模块电量。可以清楚的知道这个手机上,哪些模块会耗电,以及哪些模块在什么状态下耗电量最高。那么测试的时候,应该重点关注调用了这些模块的地方。比如App在哪些地方使用WiFi、蓝牙、GPS等等。例如最近对比测试其他App发现,在一些特定的场景下,该App置于前台20min内,扫描了WiFi 50次,这种异常会导致App耗电量大大增加。并且反过来,当有case报App耗电量异常时,也可以从这些点去考虑,帮助定位问题。

网络优化(Radio模块)

对电池来说,网络连接是最耗电的工作。手机里面有一个芯片,Ham radio,他的功能就是连接当地的电话信号塔并和它们进行大量的数据传输。但是这个芯片不是一直活跃的,一旦你发送了数据,无线芯片会在一定时间内保持开启状态再接收返回的数据。但是如果没有活动,这个硬件就会休眠以节省电量。电量优化包含着网络优化。

避免DNS解析

DNS域名的系统,主要的功能根据应用请求所用的域名URL去网络上面映射表中查相对应的IP地址,这个过程有可能会消耗上百毫秒,而且可能存在着DNS劫持的危险,可以替换为Ip直接连接的方式来代替域名访问的方法,从而达到更快的网络请求,但是使用Ip地址不够灵活,当后台变换了Ip地址的话,会出现访问不了,前段的App需要发包,解决方法是增加Ip地址动态更新的能力,或者是在IP地址访问失败了,切换到域名的访问。

合并网络请求

一次完整的Http请求,首先进行的是DNS查找,通过TCP三次握手,从而建立连接,过多的请求次数耗电耗时耗流量。如果是https请求的话,还要经过TLS握手成功后才可以进行连接,对于网络请求,减少接口,能够合并的网络请求就尽量合并。

压缩数据的大小

  • 使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗。
  • 使用webP格式代替图片格式(webP是google新出的一种图片格式,旨在缩小图片体积的情况下,尽量好的显示图片。加快图片加载速度,提升用户体验。据说现在某宝和某东都再用webP格式的图片了)。
  • 如果我们的接口每次传输的数据量很大的话,从网络流量优化的角度可以考虑下protobuf, 会比JSON数据量小很多。当然相比来说,JSON也有其优势, 可读性更高。
  • 在请求图片的url中添加诸如质量, 格式, width, height等path来获取合适的图片资源。

使用缓存策略

对于图片或者文件,内存缓存+磁盘缓存+网络缓存三级缓存策略,一般我们本地需要做的是二级缓存,当缓存中存在图片或者是文件,直接从缓存中读取,不会走网络,下载图片,在Android中使用LruCache实现内存缓存,DiskLruCache实现本地缓存。http协议自带的缓存策略,当资源没有修改时,http status 为304。适当的缓存, 既可以让我们的应用看起来更快, 也能避免一些不必要的流量消耗。

使用JobScheduler

Android 5.0 发布的JobScheduler和Android 6.0出现的Doze都一样,总结来说就是限制应用频繁唤醒硬件,将任务集中处理,从而达到省电的效果。 当你需要在Android设备满足某种场合才需要去执行处理数据,例如:

  • 应用具有您可以推迟的非面向用户的工作(定期数据库数据更新)
  • 应用具有当插入设备时您希望优先执行的工作(充电时才希望执行的工作备份数据)
  • 需要访问网络或 Wi-Fi 连接的任务(如向服务器拉取内置数据)
  • 希望作为一个批次定期运行的许多任务

结合JobScheduler来根据实际情况做网络请求. 比方说Splash闪屏广告图片, 我们可以在连接到Wifi时下载缓存到本地; 新闻类的App可以在充电、Wifi状态下做离线缓存。这样做有两个好处:

  • 避免频繁的唤醒硬件模块,造成不必要的电量消耗。
  • 避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下的)执行过多的任务消耗电量;

不同的网络状况,做不同的事

在WiFi,4G,3G等不同的网络下设计不同大小的预取数据量策略,我们还需要把当前的网络环境情况添加到设计预取数据量的策略当中去。

  • 我们可以把网络请求延迟划分为三档:例如把网络延迟小于60ms的划分为GOOD,大于220ms的划分为BAD,介于两者之间的划分为OK(这里的60ms,220ms会需要根据不同的场景提前进行预算推测)。如果网络延迟属于GOOD的范畴,我们就可以做更多比较激进的预取数据的操作,如果网络延迟属于BAD的范畴,我们就应该考虑把当下的网络请求操作Hold住等待网络状况恢复到GOOD的状态再进行处理。
  • 我们可以根据WiFi,4G,3G等不同网络状况动态调整网络超时的时间。

屏幕显示优化(Screen模块)

屏幕显示是耗电大户,在一段时间后,android设备的屏幕会变暗,直至关闭,然后会停止cpu运行,减少设备的功耗。但某些场景我们需要来保持屏幕常量,比如电子书阅读器,视频软件,视频聊天软件等等,我们会用到一些策略,让屏幕保持常量。

PowerManager 用来控制设备的电源状态. 而PowerManager.WakeLock也称作唤醒锁, 是一种保持 CPU 运转防止设备休眠的方式。例如播放音乐,即使在屏幕关闭时也需要程序在后台运行。但是我们要谨慎使用WakeLock,WakeLock获取释放成对出现以及
使用超时WakeLock, 以防出异常导致没有释放。这里要尽量使用 acquire(long timeout) 设置超时, (也被称作超时锁). 例如网络请求的数据返回时间不确定, 导致本来只需要10s的事情一直等待了1个小时, 这样会使得电量白白浪费了。 设置超时之后, 会自动释放节省电量。

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
// 创建唤醒锁
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyWakelockTag");
// 获得唤醒锁,需要加上超时机制
wakeLock.acquire(timeout);
// 释放唤醒锁, 如果没有其它唤醒锁存在, 设备会很快进入休眠状态
wakelock.release();

当然还有更好的唤醒策略,只有在特定的界面才会保持屏幕,不像唤醒锁(wake locks),需要申请android.permission.WAKE_LOCK权限。并且不用担心界面切换以及资源释放问题。

//Activity中使用FLAG_KEEP_SCREEN_ON 的Flag。
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

还有一种布局属性:keepScreenOn

<LinearLayout
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:keepScreenOn="true">
</LinearLayout>

定位优化(GPS模块)

不同的定位策略耗电不一样,我们应该根据应用的实际情况使用合适的定位策略,达到省电的目的。Android系统支持多个Location Provider:

  • GPS_PROVIDER
    GPS定位,利用GPS芯片通过卫星获得自己的位置信息。定位精准度高,一般在10米左右,耗电量大;但是在室内,GPS定位基本没用。
  • NETWORK_PROVIDER
    网络定位,利用手机基站和WIFI节点的地址来大致定位位置,这种定位方式取决于服务器,即取决于将基站或WIF节点信息翻译成位置信息的服务器的能力。
  • PASSIVE_PROVIDER
    被动定位,就是用现成的,当其他应用使用定位更新了定位信息,系统会保存下来,该应用接收到消息后直接读取就可以了。

注意:位置更新监听频率的设定,minTime用来指定间更新通知的最小的时间间隔,单位是毫秒,minDistance用来指定位置更新通知的最小的距离,单位是米,根据业务需求设置一个合理的更新频率值。定位中使用GPS, 请记得及时关闭。

 // Remove the listener you previously added
locationManager.removeUpdates(locationListener)

其他模块

其他模块的省电优化这里就不多讲,我们可以再实际开发中借助一些工具,比如Batterystats & bugreport或者Battery Historian逐步分析,找出耗电大户,对症下药优化。

四、安装包

用户常常避免下载太大的APP,尤其是使用移动流量的情况下,而且太大的APP也会占用更多的内存并消耗更多的资源,导致安装速度和加载速度变慢,特别是在低配手机上,这些情况尤为严重。

APK的组成结构

在这里插入图片描述
Raw File Size表示原文件大小,Download Size表示经过Google play处理压缩后的apk大小。可以看到占空间最多的主要是四个部分:lib、res、classes.dex、resources.arsc。也是重要的优化对象。

  • lib:so引用库的文件夹
  • res:包含了资源文件,比如图片、布局文件等等
  • classes.dex:包含有 Java 代码的字节码文件
  • resources.arsc:包含所有的值资源文件,如 strings, dimensions, styles, integers 等等。

1、整体优化

插件化

从应用功能扩张的角度看,APK包体积的增大是必然的,然而插件化技术的出现很好的解决了这个问题。通过分离应用中比较独立的模块,然后以插件的形式进行加载。比如爱奇艺Android客户端有很多相对独立的功能,游戏、漫画、文学、电影票、应用商店等,都是通过插件的方式,从服务器下载,然后以插件的额方式加载到我们的主工程。

移除用不到的语言资源文件

官方的 support library,默认是支持国际化的,也就是包含了很多不同语言的资源文件,我们就可以通过这样设置来移除用不到的语言资源文件。如果你的应用不需要支持国际化,那么可以设置 resConfigs 为 “zh”,“en”,即只支持中英文:

defaultConfig {
    resConfigs "zh","en"
}

2、lib库优化

在使用一些三方库的时候,会集成大量的so文件到项目中,这些so文件都对应着不同的CPU架构。Android系统目前支持以下七种不同的CPU架构:ARMv5、ARMv7、x86、MIPS、ARMv8、MIPS64、x86_64,每一个CPU架构对应一个ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。目前市面上绝大部分的CPU架构都是 ARMv7/ARMv8,所以可以在gradle中加入配置,只保留v7和v8。同时,Google市场要求必须提供相应的支持64位so,所以可以配置如下:

defaultConfig {
    ...
    ndk {
        abiFilters 'armeabi-v7a', 'arm64-v8a' 
    }
}

3、res资源优化

采用一套图片资源

Android在适配图片资源的时候,如果只有一套资源,低密度的手机会缩放图片,高密度的手机会拉伸图片。我们利用这个特性,存放一套资源图就可以供所有密度的手机使用。综合考虑图片清晰度,静态大小和内存占用情况,建议取1080p的资源,放到xxhdpi目录。

合并重复资源

很多时候,随着工程的增大,以及开发人员的变动,有些资源文件名字不同,但是内容却完全不同。我们可以同过扫描文件的MD5值,找出名字不同,内容相同的图片并删除,做到图片不重复。

移除无用的资源

  • 用Android Lint工具进行无用资源、无效索引删除,在Analyze中Run Inspection By Name检索unused resources,进行清理。
    在这里插入图片描述
  • 可以通过设置shrinkResources=true让Gradle移走无用的资源,否则默认混淆情况下,Gradle编译只会移除无用代码,而不会关心无用资源。需要特别注意的是shrinkResources依赖于minifyEnabled,必须和minifyEnabled一起用,即打开shrinkResources也必须打开minifyEnabled。
buildTypes {
	release {
		minifyEnabled true
		shrinkResources true
	}
}

整体移除res下某个文件夹

如果想整体移除res下某个文件夹可以添加如下aaptOptions配置,而不用打包时手工删除,多个文件夹用:隔开

android {
    aaptOptions {
        ignoreAssetsPattern 'drawable-hdpi;drawable-mhdpi'
    }
}

png图片压缩

可以通过使用图片压缩工具对png图片进行压缩,压缩效果比较好的工具有:pngcrush,pngquant,zopflipng等,可以在保持图片质量的前提下,缩减图片的大小。还可以通过网站对图片进行压缩,如比较有名的www.tinypng.com,tinypng 是一个支持压缩png和jpg图片格式的网站,通过其独特的算法(通过一种叫“量化”的技术,把原本png文件的24位真彩色压缩为8位的索引演示,是一 种矢量压缩方法,把颜色值用数值123等代替。)可以实现在无损压缩的情况下图片文件大小缩小到原来的30%-50%,但是只支持500张免费图片,更多图片处理是要收费的。

采用WebP格式

目前WEBP与JPG相比较,编码速度慢10倍,解码速度慢1.5倍,虽然会增加额外的解码时间,但是由于减少了文件体积,缩短了加载的时间,实际上文件的渲染速度反而变快了。如果你的 Android Studio 为 2.3,并且项目的 minimum version 为 18 或以上,应该使用 webp 而不是 png 图片。webp 图片有更小的体积,图片质量还没有什么损失。我们可以选中 drawable 和 mipmap 文件夹,右键后选择 convert to webp,将图片转为 webp 格式。
在这里插入图片描述

svg矢量代替图片

svg矢量图。其实是图片的描述文件,牺牲CPU的计算能力的,节省空间。适用于简单的图标。

用shape代替图片

能用shape就绝不用图片。对于纯色或渐变的图片,能用shape渲染的就优先使用shape。不仅文件体积小,还渲染速度快,也不用考虑适配问题。

4、arsc文件优化

只保留需要的语言

android {
    defaultConfig {
            resConfigs "zh", "zh_CN", "zh_HK", "zh_MO", "zh_TW", "en"
    }
}

缩小访问路径

resource.arsc文件中保存着资源文件夹中各个资源的路径。微信开源的AndResGuard资源混淆工具只针对资源,他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a。,再生成新的resource.arsc文件,替换源文件打包签名即可。

发布了17 篇原创文章 · 获赞 100 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yinhaide/article/details/104167310