查看: 79|回复: 0

Android 优化——内存优化

[复制链接]

Android 优化——内存优化[复制链接]

zhaishao 发表于 2019-1-18 20:45:18 [显示全部楼层] 回帖奖励 |倒序浏览 |阅读模式 回复:  0 浏览:  79
优化的意义


  • 减少 OOM,提高应用稳定性。
  • 减少卡顿,提高应用流畅度。
  • 减少内存占用,提高应用后台运行时的存活率。
  • 减少异常发生,减少代码逻辑隐患。
垃圾回收

在 GC 的过程中,其它在工作的线程会暂停,包括负责绘制的 UI 线程,并且在不同区域的内存释放速度也有一定的差异,但不管在哪个区域,都要到这次 GC 内存回收完成后,才会继续执行原来的线程。
虽然一次消耗性能不大,但如果大量这样的重复,就会影响到应用的渲染 工作,造成垃圾回收动作太频繁。这种情况很容易发生在短时间内申请大量 的对象时,并且它们在极少的情况下能得到有效的释放,这样会出现内存泄漏的情况。
一旦达到了剩余内存的阈值,垃圾回收活动就会启动。即使有时内存申请 很小,**它们仍然会给应用程序的堆内存造成压力,还是会启动垃圾回收,**在 GC 频繁的工作过程中消耗了非常多的时间,并且可能导致卡顿。为了避免这样的情况,设置一个 16ms 界线,只要 GC 消耗的时间超过了 16ms 的阈值,就会有丢帧的情况出现。
分析工具

使用 Memory Profiler 查看 Java 堆和内存分配(https://developer.android.com/studio/profile/memory-profiler)可分析内存情况和内存泄露。
内存泄露

内存泄漏就是存在一些被分配的对象,可达但不可用,用不着了但还有链接引用着,导致 GC 无法回收。会导致内存空间不断减少,最终内存耗尽引起 OOM 问题。
分类


  • 资源对象未关闭
资源性对象比如 BraodcastReceiver、Cursor、File 等、往往都用了一些缓冲,在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。
它们的缓冲不仅存在于 Java 虚拟机内,还存在于 Java 虚拟机外。如果我们仅仅是把它的引用设置为 null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如 SQLiteCursor(在析构函数finalize(),如果没有关闭它,它自己会调 close() 关闭),但是这样的效率太低。
对于资源性对象不使用的时候,应该立即调用它的 close() 函数,将其关闭掉,然后再置为 null。

  • 注册对象未注销
比如广播、观察者监听未解除注册,会导致所在的 Activity 退出后无法释放,不断重新进入,可能造成多个对象一直释放不掉。

  • 类的静态变量持有大数据对象
静态变量长期维持对象的引用,阻止垃圾回收,如果静态变量持有大的 数据对象,如 Bitmap 等,就很容易引起内存不足等问题。
比如 Activity 里创建静态的 View,而 View 又持有 Activity 对象,导致资源无法释放。

  • 非静态内部类的静态实例
非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被系统回收。
比如 AsyncTask 或线程 new Runnable 都会有一个匿名内部类,因此它们对当前 Activity 都有一个隐式引用,如果 Activity 在销毁之前任务还未完成,那么将导致 Activity 的内存资源无法回收,造成内存泄漏。

  • 非静态 Handler
Handler 通过发送 Message 与主线程交互,Message 发出之后存储在 MessageQueue 中,有些 Message 不能马上被处理。
在 Message 中存在一个 target,是 Handler 的一个引用,如果 Message 在 Queue 中存在的时间过长,就会导致 Handler 无法被回收。
如果 Handler 是非静态的,则会导致 Activity 或者 Service 不会被回收。所以 Handler 应该定义为静态内部类,通过弱引用持有 Activity。
  1. java   static class MyHandler extends Handler {         
  2. WeakReference<Activity> mActivityReference;         
  3. MyHandler(Activity activity) {            
  4. mActivityReference = new WeakReference<Activity>(activity);        
  5.   }        
  6.    @Override        
  7.    public void handleMessage(Message msg) {            
  8.    final Activity activity = mActivityReference.get();            
  9.     if (activity != null) {                 
  10.     activity.mImageView.setImageBitmap(mBitmap);            
  11.     }         
  12.     }     
  13.     }     
复制代码
退出时 mHandler.removeCallbacksAndMessages(null),移除消息队列中所有消息和所有的 Runnable

  • 集合中对象没清理
把一些对象的引用加入到了集合中,当不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是 static 的话,情况就更严重。

  • WebView 泄露
为 WebView 开启独立的一个进程,使用 AIDL 与应用的主进程通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

  • HandlerThread 没有主动调用 quit
HandlerThread 的 run 方法是一个死循环,它不会自己结束。线程的生命周期超过了 Activity 生命周期,当横竖屏切换,HandlerThread 线程的数量会随着 Activity 重建次数的增加而增加。
应该在 onDestroy 时将线程停止掉:mThread.getLooper().quit(),比如 IntentService 里做完任务自动调用了 stopSelf,进而调用 quit。

  • Bitmap 使用不当
用完 Bitmap 时,要及时的 recycle 掉。recycle 并不能确定立即就会将 Bitmap 释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。

  • 获取系统服务
用 ApplicationContext 代替 Activity。
检测函数库 LeakCanary

LeakCanary 是 Square 公司的检测内存泄漏的函数库,在 Debug 版本中监控 Activity、Fragment 等的内存泄漏。检测到内存泄漏时会将消息发到系统通知栏,点击后打开 DisplayLeakActivity 的页面,显示泄漏的跟踪消息,还默认保存了最近的 7 个 dump 文件到 APP 的目录中,可以用 MAT 等工具进一步分析。
使用
配置 gradle 文件:
  1. dependencies {
  2.    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
  3.    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  4.    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  5. }
复制代码
只有 Debug 版本使用,Release 和 Test 版本用 no-op 版本,没有实际代码和操作,不会对 APP 体积和性能产生影响。
在 Application 中初始化:
  1. public class ExampleApplication extends Application {
  2.     @Override public void onCreate() {
  3.     super.onCreate();
  4.     if (LeakCanary.isInAnalyzerProcess(this)) {
  5.       // This process is dedicated to LeakCanary for heap analysis.
  6.       // You should not init your app in this process.
  7.       return;
  8.     }
  9.     LeakCanary.install(this);
  10.     // Normal app init code...
  11.   }
  12. }
复制代码
其中,LeakCanary.install 方法会自动启动一个 ActivityRefWatcher,自动监控应用中调用 Activity.onDestroy 之后发生泄漏的 Activity。
如果想监控其它的对象,比如 Fragment,可以通过 install 方法返回的 RefWatcher 去监控。
  1.     public class ExampleApplication extends Application {
  2.         @Override public void onCreate() {
  3.         super.onCreate();
  4.         if (LeakCanary.isInAnalyzerProcess(this)) {
  5.           // This process is dedicated to LeakCanary for heap analysis.
  6.           // You should not init your app in this process.
  7.           return;
  8.         }
  9.     refWatcher = LeakCanary.install(this);
  10.     // Normal app init code...
  11.   }
  12.     private RefWatcher refWatcher;
  13.     // get 方法返回 RefWatcher 对象
  14.     public static RefWatcher getRefWatcher(Context context) {
  15.         ExampleApplication application = (ExampleApplication) context.getApplicationContext();
  16.         return application.refWatcher;
  17.     }
  18. }
复制代码
然后在 Fragment 的 onDestroy 方法中调用 refWatcher 监控
  1. @Override
  2. public void onDestroy() {
  3.     super.onDestroy();
  4.     RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
  5.     refWatcher.watch(this);
  6. }
复制代码
可以使用 watch 来监控任何你认为已经销毁的对象。
原理


  • RefWatcher.watch() 为被监控对象创建一个 KeyedWeakReference 弱引用对象,它是
    WeakReference 的子类,添加键值对,后面会根据指定 Key 找到弱引用对象。
  • 在后台线程 AndroidWatchExecutor 中,检查 KeyedWeakReference
    弱引用是否被清除,如果存在则触发一次垃圾回收。垃圾回收后,如果弱引用对象依然存在,说明已经内存泄漏,会将 Heap 内存导出到
    .hprof 文件中,并将文件放在 APP 的文件目录中。
  • 在一个独立的进程中启动 HeapAnalyzerService 服务,解析 heap dump 信息。基于唯一的 reference
    key,在 heap dump 中找到对应的
    KeyedWeakReference,并定位发生内存泄漏的对象引用。HeapAnalyzer 会计算 GC Roots
    的最短强引用路径,并判断是否存在泄漏,并构建出导致泄漏的对象引用链。
定制

RefWatcher 的自定义
由于 Release 版本使用的 leakcanary-android-no-op 库,若自定义 LeakCanary,需确保只影响 Debug 版本,因为可能引用到 leakcanary-android-no-op 中没有的 API。因此需要将 Release 和 Debug 部分的代码分离。例如定义 ExampleApplication 用于 Release 版本,DebugExampleApplication 用于 Debug 版本,继承 ExampleApplication。
  1. public class ExampleApplication extends Application {
  2.     public static RefWatcher getRefWatcher(Context context) {
  3.         ExampleRefWatcher application = (ExampleRefWatcher) context.getApplicationContext();
  4.         return application.refWatcher();
  5.     }
  6.     private RefWatcher refWatcher;
  7.     @Override
  8.     public void onCreate() {
  9.         super.onCreate();
  10.         ...
  11.         // 不再是调用 install 方法
  12.         refWatcher = installLeakCanary();
  13.         ...
  14.     }
  15.     protected RefWatcher installLeakCanary() {
  16.         return RefWatcher.DISABLED;
  17.     }
  18. }
复制代码
新建 src/debug/java 文件夹,在其中创建 DebugExampleApplication:
  1. // Debug 版本的 Application 类
  2. public class DebugExampleApplication extends ExampleApplication {
  3.     protected RefWatcher installLeakCanary() {
  4.         RefWatcher refWatcher = LeakCanary.install(this);
  5.         return refWatcher;
  6.     }
  7. }
复制代码
在 src/debug 中新建 AndroidManifest.xml 文件:
  1. <?xml version="1.0 encoding="utf-8" ?>
  2. <manifest ...>
  3.     <application
  4.         tools:replace="android:name"
  5.         android:name=".DebugExampleApplication" />
  6. </manifest>
复制代码
Gradle 构建时,如果是 debug 版本,会将 src/debug/AndroidManifest.xml 的内容合并入 src/main/AndroidManifest.xml 文件中。同时由于使用了 tools:replace属性,所以 android:name 的值 DebugExampleApplication 会替换 ExampleApplication。
通知页面样式的自定义
内存泄漏通知页面 DisplayLeakActivity 默认的图标和标签两个值,可以进行覆盖。
图标定义在 res 下的 drawable-hdpi/drawable-mdpi/drawable-xhdpi/drawable-xxhdpi/drawable-xxxhdpi 里,名为 __leak_canary_icon.png。
标签定义在:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3.     <string name="__leak_canary_display_activity_label">MyLeaks</string>
  4. </resources>
复制代码
内存泄漏堆栈信息保存个数的自定义
默认情况下,DisplayLeakActivity 在 APP 目录中最多保存 7 个 HeapDump 文件和泄漏堆栈信息,可以在 APP 中定义 R.integer.__leak_canary_max_stored_leaks 来修改。
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3.     <string name="__leak_canary_max_stored_leaks">20</string>
  4. </resources>
复制代码
Watcher 的延时
通过定义 R.integer.leak_canary_watch_delay_millis 来修改弱引用对象被认为出现内存泄漏的延时时间,默认 5 秒,下面修改为 1.5 秒:
  1. dependencies {
  2.    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
  3.    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  4.    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  5. }
  6. 0
复制代码
自定义堆栈信息和 heap dump 的处理方式
可以通过继承 DisplayLeakService 并重写其中的 afterDefaultHandling 函数来实现定制化操作,例如将 heap dump 文件发送到服务端:
  1. dependencies {
  2.    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
  3.    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  4.    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  5. }
  6. 1
复制代码
为了使 LeakUploadService 生效,需要在 AndroidManifest.xml 中注册。
忽略特定的弱引用
实现自己的 ExcludedRefs 忽略某些特定的弱引用对象,不对其进行内存泄漏的监视。
  1. dependencies {
  2.    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
  3.    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  4.    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  5. }
  6. 2
复制代码
不监视特定 Activity
默认会监视所有 Activity 的内存泄漏,默认只支持 Android 4.0 以上的系统,如果 4.0 以下需要在 onDestroy 中主动 watch。
  1. dependencies {
  2.    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
  3.    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  4.    testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  5. }
  6. 3
复制代码
内存优化


  • 使用软/弱/虚引用
  • 使用 ArrayMap 代替 HashMap
  • 使用 SparseArray,SparseBooleanArray,SparseLongArray 和 SparseIntArray 替换
    HashMap,以减少装箱带来的内存占用,也避免了拆箱。
  • @IntDef,@StringDef 代替枚举
  • zipalign 优化 apk
  • 节制使用 Service  如果需要使用 Service 来执行后台任务,一定要任务正在执行的时候才启动
    Service。另外,当任务执行完之后去停止 Service 的时候,要小心停止失败导致内存泄漏的情况。  可以使用
    IntentService,后台任务结束后会自动停止,从而极大程度上避免了 Service 内存泄漏的可能性。
  • 当界面不可见时释放内存  Activity 中重写 onTrimMemory(),当处于 TRIM_MEMORY_UI_HIDDEN
    这个级别时,表明用户已经离   开了程序,所有界面都不可见,此时可以进行一些资源释放操作。    @Override   public
    void onTrimMemory(int level) {       super.onTrimMemory(level);
    switch (level) {       case TRIM_MEMORY_UI_HIDDEN:           // 释放资源
    break;       }   }
图片优化


  • 设置位图规格  ARGB_8888 占用内存最高,是系统默认。  RGB_565
    会损失较多的图片数据,但除了大图,一般看不出什么区别。但它不支持 PNG 图片的透明通道。  ARGB_4444
    减少一半的数据,但保留了透明通道,视觉差异变化较大,一般用于用户头像,特别是圆角头像。  Aplha_8 主要用于 Alpha
    通道模板,相当于做一个染色。图像要渲染两次,虽然减少内存,但增加了 绘制的开销。  在 Android 的基本文件结构中不支持
    PNG、JPEG 和 WEBP 格式,因此需要通过 inPreferredConfig 参数来实现不同的位图规格
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    BitmapFactory.decodeStream(is, null, options);
  • 设置采样率  BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(getResource(), R.drawable.ic, options);
    int height = options.outHeight; int width = options.outWidth; String
    imageType = options.outMimeType; options.inSampleSize = 2;
    options.inJustDecodeBounds = false;
    BitmapFactory.decodeResource(getResource(), R.drawable.ic, options)
  • inScaled,inDensity 和 inTargetDensity  BitmapFactory.Options options =
    new BitmapFactory.Options();  options.inScaled = true;
    options.inDensity = srcWidth;  options.inTargetDensity = dstWidth;
    BitmapFactory.decodeStream(is, null, options);   当 inScaled 设为 true
    时,系统会按照现有的密度来划分目标密度,通过 派生绽放数来应用到位图上,使用这个方法会重设图片大小,并对它应用一个新的过滤。
    虽然这些方法都非常好用,并且减少图片显示需要的内存,但因为过多的算法,导致图片显示的过程需要更多的时间开销,如果图片很多的话,就影响到图片的显示效果。
    最好的方案是结合这两个方法,首先使用 inSampleSize 处理图片,转换为接近目标的 2 次幂,然后用 inDensity 和
    inTargetdensy 生成最终想要的准确大小,因为 inSamplesize 会减少像素的数量,而
    基于输出密度的需要对像素重新过滤。  BitmapFactory.Options options = new
    BitmapFactory.Options(); options.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(is, null, options);  options.inScaled =
    true;  options.inDensity = options.outWidth;  options.inSampleSize =
    4;  options.inTargetDensity = dstWith * options.inSampleSize;
    options.inJustDecodeBounds = false;  BitmapFactory.decodeStream(is,
    null, options);
  • inBitmap  Android 3.0(API 11)引进了 BitmapFactory.Options.inBitmap
    字段,设置该属性后,当使用 了带有该 Options 参数的 decode 方法加载内容时,decode
    方法会尝试重用一个已经存在的位图。这意味着位图内存被重用,从而改善性能,并且没有内存的分配和释放过程。  常见的使用方案可以结合
    LruCache 来实现,在 LruCache 移除超出 cache size 的图片时,暂时缓存 Bitmap
    到一个软引用集合,需要创建新的 Bitmap 时,可以从这个软引用集合中找到最适合重用的 Bitmap 来重用它的内存区域。  新申请
    Bitmap 与旧的 Bitmap 必须有相同的解码格式,并且在 Android 4.4 之前,只能重用相同大小的 Bitmap
    的内存区域,Android 4.4 后可以重用任何 bitmap 的内存区域。
  • drawable 目录  不同的目录对应不同的显示密度
    目录名称 Density     res/drawable 0   res/drawable-hdpi 240   res/drawable-ldpi 120   res/drawable-mdpi 160   res/drawable-xhdpi
    320   res/drawable-xxhdpi 480
    加载资源图片时,会先算出屏幕密度,然后再到对应的资源目录下寻找图片,如果没有,则到最近的目录中寻找。  比如一张图片只放在了
    res/drawable-mdpi,但当前设备密度是 480,那么系统会将这张图片放大 3 倍加载到内存。  res/drawable
    在不同的设备下会被替换成不同的密度,即系统本身的默认密度。
    所以抓不准该放到哪个目录的图片,就尽量问设计人员要高品质图片然后往高密度目录下放,这样在低密屏上“放大倍数”是小于 1
    的,在保证画质的前提下,内存也是可控的。  拿不准的图片,使用 Drawable.createFromStream 替换
    getResources().getDrawable 来加载,这样就可以绕过 Android 的这套默认适配法则。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1
QQ