Android性能优化常用方案(持续更新)

一、内存优化

1、预防内存泄漏

a:非静态内部类或者匿名内部类持有外部类引用

在 Java 中非静态内部类和匿名内部类会持有他们所属外部类对象的引用,如果这个非静态内部类对象或者匿名内部类对象被一个耗时的线程(或者其他 GC Root)直接或者间接的引用,甚至这些内部类对象本身就在做一些耗时操作,这样就会导致这个内部类对象直接或者间接无法释放,内部类对象无法释放,外部类的对象也就无法释放造成内存泄漏,而且如果无法释放的对象积累起来就会造成 OOM。解决方案:使用静态内部类

b:静态变量造成的内存泄漏

由于静态变量的生命周期和应用一样长,所以如果静态变量持有 Activity 或者 Activity 中 View 对象的引用,就会导致该静态变量一直直接或者间接持有 Activity 的引用,导致该 Activity即使退出了也无法释放内存,从而引发内存泄漏。 解决方案:让静态变量直接或者间接持有 Activity 的强引用,可以将其修改为 soft reference 或者 weak reference ;或者将 Activity Context 更换为 Application Context

c:资源对象没关闭造成的内存泄漏

资源性对象比如(Cursor,File 文件等)往往都用了一些缓冲,我们在不使用的时候应该及时关闭它们,以便它们的缓冲对象被及时回收。对于资源性对象在不使用的时候,应该调用它的 close() 函数,将其关闭掉,然后再置为 null,在我们的程序退出时一定要确保我们的资源性对象已经关闭。

d:集合中对象没清理造成的内存泄漏

如果在一个对象使用结束之后未将该对象从该容器中移除掉,就会造成该对象不能被正确回收,从而造成内存泄漏,解决办法是

在使用完之后将该对象从容器中移除。示例如下:

ArrayList list = new ArrayList<>();
list.clear();
list = null;

e:检测内存泄漏方案

StrictMode可以检测是否存在内存泄漏,示例如下:

StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                .detectLeakedClosableObjects().detectActivityLeaks()
                .penaltyLog()
                .penaltyDeath()
                .build());

 

2、Bitmap优化

a:使用软引用包裹Bitmap,内存不足时,GC会将软引用对象回收掉

b:根据ImageView的宽高设置合适的采样率进行压缩

c:使用RGB_565代替ARGB_8888解码

对于缩略图这种用户对图片质量要求不高的图片,使用RGB_565进行解码,相比于ARGB_8888能节省一半的内存。同理对于常驻内存的图片,比如微信、QQ的聊天背景,用户对其图片质量要求不高,也可以用RGB_565进行解码,节省一半的内存。

3、避免创建不必要的对象

如果有需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。 避免创建短命的临时对象,减少对象的创建就能降低垃圾收集概率,进而减少对用户体验的影响。

4、使用SparseArray、SparseBooleanArray、SparseIntArray、SparseLongArray代替key为integer类型的HashMap

 

 

二、磁盘优化

1、原则

a:减少磁盘I/O的次数,特别是主线程的I/O操作

b:使用缓存,避免重复的磁盘I/O操作

每次打开、关闭或者读/写文件,操作系统都需要从用户态切换到内核态,这种状态切换本身是很消耗性能的,所以为了提高文
件的读/写效率,就需要尽量减少用户态和内核态的切换。使用缓存可以避免重复读/写,对于需要多次访问的数据,在第一次取出数据时,将数据放到缓存中,下次再访问这些数据时,就可以从缓存中取出来。

2、优化点

a: SharedPreferences的commit与apply区别

SharedPreferences操作的是xml文件,读写数据都是对磁盘的IO操作,commit提交是同步的,commit返回以后才能执行下面的操作,commit有返回值(Boolean)。Apply提交是异步的,apply调用以后会立即执行下面的操作,apply没有返回值。如果关注提交成功与否使用commit,如果不关注提交成功与否或者在主线程中记录数据的话,最好使用apply。

b:SharedPreferences不要频繁调用get方法取值

SharedPreferences是对磁盘的IO操作,不要频繁调用getBoolean(xxx)等get方法取值,最好只调用一次get方法,然后将数据保存在内存中,以后使用内存中的数据。

c:读取文件、写文件时合理设置Buffer数组长度

在读一个文件我们一般会设置一个buffer。即先把文件读到buffer中,然后再读取buffer的数据,所以真正对文件的读取次数 = 文件大小 / buffer大小 。 如果buffer比较小的话,那么读取文件的次数会非常多,当然在写文件时buffer是一样的道理。如果设置1KB的buffer,byte buffer[] = new byte[1024],要读取的文件有20KB, 那么根据这个buffer的大小,这个文件要被读取20次才能读完。buffer的大小可以取值文件所挂载的目录的区块大小block size,一般sd卡的区块大小为4KB。

获取block size如下:

public static long getSdCardBlockSize() {
        File sdCardRoot = Environment.getExternalStorageDirectory();
        StatFs sdCardStatFs = new StatFs(sdCardRoot.getPath());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return sdCardStatFs.getBlockSizeLong();
        } else {
            return sdCardStatFs.getBlockSize();
        }
}

    public static long getDataBlockSize() {
        StatFs sdCardStatFs = new StatFs("/data/data");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            return sdCardStatFs.getBlockSizeLong();
        } else {
            return sdCardStatFs.getBlockSize();
        }
    }

d:序列化时直接使用ObjectOutputStream效率很低

使用ObjectOutputStream在序列化对象时,每个数据成员都会带来一次I/O操作,效率非常低下。解决方案:在ObjectOutputStream上面再封装一个输出流ByteArrayOutputStream或者BufferedOutputStream,先将对象序列化后的信息写到缓存区中,然后再一次性地写到磁盘上,这样只需要一次I/O操作。同理,反序列化时ObjectInputStream上面再封装一个输入流ByteArrayInputStream或者BufferedInputStream,只需要从磁盘读取一次即可。


e:重复打开、关闭数据库

SQLiteDatabase的源码getWriteableDatabase()方法的注释说明:一旦打开数据库,该连接就会被缓存,以供下次使用,只有当真正不需要时,调用close关闭即可。  每次打开数据库都会伴随了I/O操作,getWriteableDatabase()会比较耗时,该操作不能在主线程中进行。  解决方案:数据库在打开后,先不要关闭,在应用程序退出时再关闭。

f:主键最好不要自增

主键最好不要自增长(INTEGER PRIMARY KEY后面最好不要跟上AUTOINCREMENT),主键最好手工插入唯一的值。 SQLite创建一个叫sqlite_sequence的内部表来记录数据库表使用的最大的行号。如果指定使用AUTOINCREMENT来创建表,则sqlite_sequence也随之创建。UPDATE、INSERT和DELETE语句可能会修改sqlite_sequence的内容。因为维护sqlite_sequence表带来的额外开销将会导致INSERT的效率降低。 AUTO INCREMENT可以保证主键的严格递增,但AUTO INCREMENT关键词会增加CPU,内存,磁盘I/O的负担,所以尽量不要用,除非必需。

g:解码Bitmap使用decodeStream(同时传给decodeStream的文件流是BufferedInputStream)代替decodeFile

Android4.4以及以后的系统版本尽量使用BitmapFactory.decodeStream 而不是bitmapfactory.decodeResource或者bitmapfactory.decodeFile。decodeFile源码中使用的是FileInputStream,这需要多次读取磁盘,效率很低。decodeResource同样存在这个问题,建议使用decodeResourceStream。

3、检查磁盘I/O操作

StrictMode可以发现并定位在主线程中读写SD卡的操作,示例如下:

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork().detectCustomSlowCalls()  
                .penaltyLog()
                .build());

三、CPU优化

1、  能用int就不用long,能用float就不用double,减轻CPU运算压力

2、  设计算法要考虑时间复杂度,减少CPU运算时间

3、  解码图片时尽量使用cache,最好只解码一次,避免重复解码

 

四、网络优化

网络优化主要关注三个方向,网络请求成功率、网络延迟、宽带成本(流量消耗)。

1、网络请求成功率

典型的网络差的两种场景,弱信号网络(进电梯、隧道)+拥塞网络(演唱会时分享图片)。弱信号场景,App能做的就是在应用层做重试,因为很可能这个弱信号是一时的。拥塞网络场景,如果App不断重试只会使得拥塞更为严重,App要做的就是少发送网络请求,核心业务发送少量的网络请求,非核心业务就不要发送网络请求了。

2、网络延迟                 

网络时延原因  解决方案
DNS解析耗时(200-2000ms不等) IP直连
TCP三次握手耗时,短连接每次请求都会进行TCP连接(Keep-Alive除外) 使用长连接,只进行一次TCP连接
TCP拥塞控制逻辑 长连接可以绕过拥塞策略中的慢启动

                             

3、宽带成本(流量消耗)

图片、视频、文件压缩后上传。

五、线程优化

1、使用线程池代替创建大量的线程

大量创建Thread或Runnable的弊端:

内存方面考虑:大量的线程的创建和销毁很容易导致GC执行,从而发生内存抖动现象,造成页面卡顿;

时间方面考虑:线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,耗时会比较明显。

用线程池的益处:

一、  重用线程池中的线程,降低系统资源消耗,避免频繁创建和销毁线程带来内存和CPU上的损耗

二、  提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行

三、  方便线程并发量的控制,线程若是无限制的创建,不仅会消耗大量的系统资源,而且可能造成线程溢出(华为手机限制在400多个线程,很容易出现线程溢出)

 

你可能感兴趣的:(Android)