Android性能优化解决方案

1、内存优化

内存优化主要从治理内存泄漏、内存抖动、内存占用过多/内存溢出三个方面着手。

1.1、内存泄漏

1.1.1、Java内存泄漏

一、监控方案

1、静态代码扫描 - Lint、FindBugs

使用Lint静态扫描工程代码,Analyze->Inspect Code->WholeProject。

我扫描了下我的工程,在Android/Lint/Performance目录下有一项内存泄漏警告,如下图:

 出现原因,单例类init方法中传入了context。


object PlayerManager : IPlayerManager {
  const val TAG = "PlayManager"
  const val CONNECT_TIME_OUT: Long = 1000
  private var isServiceConnected = false
  private lateinit var playService: PlayService
  private lateinit var context: Context
  private lateinit var eventListener: PlayerEventListener

  override fun init(context: Context, eventListener: PlayerEventListener) {
    this.context = context
    this.eventListener = eventListener
    bindService()
  }

init时传入Context这种做法是不恰当的,正确应该在构造方法就传入Application Context,但是单例类的构造方法是是私有的,无法对外传入Context,怎么办呢?在构造方法中引入一个全局的静态Application Context即可消除Lint的警告。

2、系统API - StrictMode

        StrictMode 是 Android 系统提供的 API,在开发环境下引入可以更早的暴露发现问题给开发者,于开发阶段解决它,StrictMode 最常被使用来检测在主线程中进行读写磁盘或者网络操作等耗时任务,把这些耗时任务放置于主线程会造成主线程阻塞卡顿甚至可能出现 ANR。以及检测SqlLite相关内存泄漏、Closable未关闭内存泄漏。

public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
 }

        StrictMode 只是在测试环境下启用,到了线上环境就不要开启这个功能。启用 StrictMode 之后,JVM在检测到内存泄漏或者主线程耗时任务时,会在 logcat 输出一堆红色告警 log。

3、LeakCanary

LeakCanary主要用于检测Activity和Fragment内存泄漏,使用如下:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    //LeakCanary dump堆栈快照,分析内存泄漏引入路径的进程,此处直接返回
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

        如果程序出现了内存泄漏会弹出 notification,点击这个 notification 就会进入到下面这个界面,或者集成 LeakCanary 之后在桌面会有一个 LeakCanary 的图标,点击进去是所有的内存泄漏列表。

​​​​​

LeakCanary运行原理

1、 LeakCanary.install(application)时会通过registerActivityLifecycleCallbacks监听activity的生命周期。

2、在Activity的onDestroy回调时,LeakCanary内部创建相应的 WeakReference 和 RefrenceQueue。

3、一段时间之后,从 RefrenceQueue 读取,若读取不到相应 activity 的 Refrence,有可能发生泄露了,这个时候再触发 gc,一段时间之后,再去读取,若在从 RefrenceQueue 还是读取不到相应 activity 的 refrence,可以断定是发生内存泄露了。

4、内存泄漏发生时,LeakCanary会启动一个独立进程,在这个进程中使用HAHA开源库dump堆栈快照,HeapAnalyzer 计算到 GC roots的最短强引用路径,建立导致泄露的引用链,已通知的形式反馈给开发者。

4、AndroidStuido Memory Profiler

        Profiler 是 Android Studio 自带的一个监控工具,可以监测内存分配、内存泄漏、线程分配、线程存活、CPU使用率等。我的习惯是在发版前对demo/app进行压测,观察内存占用、CPU占用情况,没有内存泄漏时应该是一条平滑的曲线,如果随着时间的推移内存占用逐渐上升,应该是发生了内存泄漏,选择监测的启动和终点,可dump这段时间的堆内存分配情况,分析即可。

二、常见泄漏场景以及解决方案

1、非静态内部类或者匿名内部类持有外部类引用

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

2、静态变量造成的内存泄漏

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

3、资源对象没关闭造成的内存泄漏

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

4、集合中对象没清理造成的内存泄漏

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

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

5、未取消注册导致的内存泄漏

        registerReceiver 后未调用unregisterReceiver,registerReceiver和unregisterReceiver一定要成对出现,一般在activity的onDestroy中unregisterReceiver。

1.1.2、Native内存泄漏

一、监控方案

1、腾讯性能狗-Perfdog

通常用Perfdog来检测应用程序内存问题,这个工具的实现原理是不断的读取系统的内存值。通过Perfdog,我们只能观察内存总量,去判断是否存在内存异常问题。可是,Perfdog无法提供有效的堆栈信息,帮助开发定位问题所在。

2、AndroidStudio Profiler

        应用进行压测,观察总内存和native内存是否随着时间的移动而增长,这种方法同样只能确定native是否在增长,提供不了堆栈信息。可以确定哪个功能或者需求引入的内存泄漏,从而再排查代码。

3、腾讯开源的loli_profiler

        腾讯内部研发的开源工具,兼容性好,无系统版本要求;
使用时,需要通过adb连接pc端工具,需要注意同时打开AndroidStudio会占用adb导致工具无法检测;完成检测后,数据只能展示这一段的时间过程中总的未释放的内存。由于内存的申请和释放是一个持续的过程,有可能是在结束检测之后才释放。这样,我们就不能够准确的说未释放的内存是发生内存泄漏导致。

参考Android端native内存泄漏监控

二、常见native内存泄漏场景以及解决方案

1、动态库使用完后没有释放native内存

        动态库的init和destroy要成对调用,so使用完一定要主动调用动态库中的destroy方法释放native中申请的内存。一般来说,init时C++会通过malloc申请内存,destroy中会通过free释放内存:

void free(void *ptr);

2、jni中的内存泄漏

        在jni中new出来的对象,如果作为返回值返回的话,可以不必delete,Java会自己回收;如果不返回的必须delete掉,否则会引起内存泄漏,如下所示:

jstring jstr = env->NewStringUTF((*p).sess_id);
//使用jstr
env->DeleteLocalRef( jstr);//没有返回,使用完成后必须delete掉
 
jobject jobj = env->NewObject(clazz,midInit);//jobj作为返回值返回了,Java会自动回收
return jobj;

FindClass:

jclass ref= (env)->FindClass("java/lang/String");
 
env->DeleteLocalRef(ref); 

GetObjectField/GetObjectClass/GetObjectArrayElement:

jclass ref = env->GetObjectClass(robj);
 
env->DeleteLocalRef(ref);

GetByteArrayElements和GetStringUTFChars:

jbyte* array= (*env)->GetByteArrayElements(env,jarray,&isCopy);
(*env)->ReleaseByteArrayElements(env,jarray,array,0);
 
const char* input =(*env)->GetStringUTFChars(env,jinput, &isCopy);
(*env)->ReleaseStringUTFChars(env,jinput,input);

NewString / NewStringUTF / NewObject / NewByteArray:

env->DeleteLocalRef(ref)

1.2、内存抖动

        大量的对象被频繁创建时,会导致GC频繁的回收堆内存,进而出现卡顿。频繁的内存分配和释放,也会导致内存区域里面存在很多碎片,当这些碎片足够多,new 一个大对象的时候,所有的碎片中没有一个碎片足够大以分配给这个对象,但是所有的碎片空间加起来又是足够的时候,于是出现 了OOM。

        最常见产生内存抖动的例子就是在 ListView 的 getView 方法中未复用 convertView 导致 View 的频繁创建和释放,针对这个问题的处理方式那当然就是复用 convertView;或者是 String 拼接创建大量小的对象;或者在 for 循环中创建对象。解决方案就是避免频繁创建大量、临时的小对象。

1.3、内存占用过多/内存溢出

        内存泄漏追其原因是代码不规范导致,除了解决这些代码不规范导致的内存泄漏,我们还要关注在不影响用户体验的前提下尽量降低内存占用,合理使用内存。

1.3.1、降低图片占用内存

1、使用软引用包裹Bitmap,内存不足时,GC会将软引用对象回收掉。并使用图片缓存池。

2、根据ImageView的宽高设置合适的采样率进行压缩。

3、使用RGB_565代替ARGB_8888解码

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

4、动画不用帧动画,采用补间动画或者属性动画。

1.3.2、尽量使用系统内置资源

        Android 系统本身内置了大量的资源,比如一些通用的字符串、颜色定义、常用 icon 图片,还有些动画和页面样式以及简单布局,如果没有特别的要求,这些资源都可以在应用程序中直接引用。直接使用系统资源不仅可以在一定程度上减少内存的开销,还可以减少应用程序 APK 的体积。Flutter中就内置了大量的系统图片(矢量图),比如回退、删除、头像、抽屉按钮、悬浮按钮,开发者可以直接使用。

1.3.3、合理使用数据结构

        ArrayMap 以及 SparseArray 是 Android为了替代HashMap设计的数据结构,对于 key 为 int 的 HashMap 尽量使用 SparseArray 替代,大概可以省30%的内存,而对于其他类型使用ArrayMap代替 HashMap。

1.3.4、多进程方案

直播、VR场景,需要加载地图、高清图片、WebView加载H5、视频播放,需要消耗大量的内存,测试发现:

1、直播demo内存占用为350 - 430M;主工程开启直播时内存占用为630 - 720。

2、直播demo开启讲房工具时内存占用为450 - 660;主工程直播时开启讲房工具内存用800 - 1g。

4、主工程进入VR带看时峰值到达1.2G。

使用场景如下图:

        统计发现,单进程时App异常退出率未7%,像直播、VR这种内存消耗大户,常用内存优化方案已经不可用,只能再另外启一个子进程。采用子进程方案后,由于oom导致的异常退出率降到1%。

2、CPU优化

2.1、线程优化

1、合理设置线程池参数

        根据实际的业务场景采用合适的线程池,并设置合适的线程池参数,才能最大限度的发挥CPU的性能,线程池介绍请看这篇

2、合理使用数据结构

        同样是map,单线程场景使用HashMap;多线程场景使用CurrentHashMap,弃用Hashtable,CurrentHashMap采用分段多,多线程并发时效率更高。还有ArrayList、linkedList、vector,查询操作多使用ArrayList,增删操作多使用LinkedList,单线程没必要使用线程安全的vector。

3、合理加载so动态库

        64位的CPU可以加载32位和64位的动态库,32位的CPU只能加载32位的动态库。64位CPU加载运行32位的动态库没有发挥出CPU的实际性能,运算速度比使用64位的动态库慢许多。目前流行的so加载方案是不分机型统一加载armeabi-v7a,这种方案虽说减少了一部分包体积但对于64位CPU的设备来说运算速度会稍慢,而且目前绝大部分的机型都已经是64位的CPU架构了,因此可以考虑针对不同机型动态加载so,so不内嵌到apk中而是从云端下载加载。离线语音识别和语音合成中,经常会遇到低配CPU加载超大的算法模型库,语音识别/合成慢的问题。算法模型库中涉及到矩阵运算很消耗CPU性能,同样的算法模型库低配CPU需要较长时间进行矩阵运算,因此给到结果也慢。针对这种情况,需要针对不同的设备机型选择模型库,高配CPU加载大模型,低配CPU加载优化后的小模型,这样低端机型也能进行流畅的识别,但带来的结果是识别准确率会降低。

2.2、绘制UI优化

        UI的渲染同样消耗CPU性能,再不影响用户体验的条件下要使用合理的view和布局。

1、合理使用布局

能用线性布局,不用相对布局。能用一个布局,就不嵌套。flutter中去掉了Android中的xml布局的方式,通过代码创建widget树,体验大大提升。

2、避免过渡绘制

理想情况下, 每次屏幕刷新时, 每个像素点应该只被绘制一次, 如果有多次绘制, 就是Overdraw。在"系统设置"-->"开发者选项"-->"调试GPU过度绘制"可以开启调试。

3、view延迟加载-ViewStub

在程序运行时动态根据条件来决定显示哪个View或某个布局。

4、解码图片、音视频数据时使用cache,避免重复多次解码消耗大量CPU

5、合理使用基本数据类型

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

3、包体积优化

1、so只加载一个平台的,armeabi-v7a可以做到兼容所有设备。

2、so和算法模型还可以考虑动态加载,第一次使用时从服务端拉取,按需加载。

3、speex、opus、FFmpeg等开源库裁剪,只集成自己需要的功能,缩小so体积。

4、自己的C++库,编译so时去符号表。

5、产物是AAR的话,AAR中要去掉资源文件,AAR打包完成后去掉资源文件后再向maven上传。

6、压缩图片 - TinyPngPlugin

4、磁盘优化

4.1、磁盘读写原则

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

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

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

4.2、磁盘优化点

1、SharedPreferences的commit与apply

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

2、SharedPreferences不要频繁调用get方法取值

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

3、读写文件时合理设置Buffer数组长度

        在读文件时我们一般会设置一个buffer。即先把文件读到buffer中,然后再读取buffer的数据,所以真正对文件的读取次数 = 文件大小 / buffer大小 。 如果buffer比较小的话,那么读取文件的次数会非常多,当然在写文件时buffer是一样的道理。如果设置1KB的buffer,byte buffer[] = new byte[1024],要读取的文件有20KB, 那么根据这个buffer的大小,这个文件要被读取20次才能读完。那buffer的大小如何设置?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();
        }
    }

4、序列化时直接使用ObjectOutputStream效率很低

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


5、重复打开、关闭数据库

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

6、数据库表主键最好不要自增

        主键最好不要自增长,也就是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的负担,所以尽量不要用,除非必需。

7、解码Bitmap使用decodeStream

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

8、检查磁盘I/O操作

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

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

5、网络优化

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

1、网络请求成功率

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

2、网络延迟                 

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值