Android 优化——内存优化

Android 优化目录


优化的意义

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

垃圾回收

在 GC 的过程中,其它在工作的线程会暂停,包括负责绘制的 UI 线程,并且在不同区域的内存释放速度也有一定的差异,但不管在哪个区域,都要到这次 GC 内存回收完成后,才会继续执行原来的线程。

虽然一次消耗性能不大,但如果大量这样的重复,就会影响到应用的渲染 工作,造成垃圾回收动作太频繁。这种情况很容易发生在短时间内申请大量 的对象时,并且它们在极少的情况下能得到有效的释放,这样会出现内存泄漏的情况。

一旦达到了剩余内存的阈值,垃圾回收活动就会启动。即使有时内存申请 很小,它们仍然会给应用程序的堆内存造成压力,还是会启动垃圾回收,在 GC 频繁的工作过程中消耗了非常多的时间,并且可能导致卡顿。为了避免这样的情况,设置一个 16ms 界线,只要 GC 消耗的时间超过了 16ms 的阈值,就会有丢帧的情况出现。

分析工具

使用 Memory Profiler 查看 Java 堆和内存分配可分析内存情况和内存泄露。

内存泄露

内存泄漏就是存在一些被分配的对象,可达但不可用,用不着了但还有链接引用着,导致 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。

        ```java
        static class MyHandler extends Handler {  
            WeakReference<Activity> mActivityReference;  
            MyHandler(Activity activity) {  
                mActivityReference = new WeakReference<Activity>(activity);  
            }  
            @Override  
            public void handleMessage(Message msg) {  
                final Activity activity = mActivityReference.get();  
                if (activity != null) {  
                    activity.mImageView.setImageBitmap(mBitmap);  
                }  
            }  
        }  
        ```
      
    • 退出时 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 文件:

dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
}

只有 Debug 版本使用,Release 和 Test 版本用 no-op 版本,没有实际代码和操作,不会对 APP 体积和性能产生影响。

在 Application 中初始化:

public class ExampleApplication extends Application {
    @Override public void onCreate() {
    super.onCreate();
    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...
  }
}

其中,LeakCanary.install 方法会自动启动一个 ActivityRefWatcher,自动监控应用中调用 Activity.onDestroy 之后发生泄漏的 Activity。

如果想监控其它的对象,比如 Fragment,可以通过 install 方法返回的 RefWatcher 去监控。

public class ExampleApplication extends Application {
    @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }

    refWatcher = LeakCanary.install(this);
    // Normal app init code...
  }

    private RefWatcher refWatcher;

    // get 方法返回 RefWatcher 对象
    public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context.getApplicationContext();
        return application.refWatcher;
    }
}

然后在 Fragment 的 onDestroy 方法中调用 refWatcher 监控

@Override
public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
}

可以使用 watch 来监控任何你认为已经销毁的对象。

原理

  1. RefWatcher.watch() 为被监控对象创建一个 KeyedWeakReference 弱引用对象,它是 WeakReference 的子类,添加键值对,后面会根据指定 Key 找到弱引用对象。
  2. 在后台线程 AndroidWatchExecutor 中,检查 KeyedWeakReference 弱引用是否被清除,如果存在则触发一次垃圾回收。垃圾回收后,如果弱引用对象依然存在,说明已经内存泄漏,会将 Heap 内存导出到 .hprof 文件中,并将文件放在 APP 的文件目录中。
  3. 在一个独立的进程中启动 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。

public class ExampleApplication extends Application {
    public static RefWatcher getRefWatcher(Context context) {
        ExampleRefWatcher application = (ExampleRefWatcher) context.getApplicationContext();
        return application.refWatcher();
    }

    private RefWatcher refWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
        ...
        // 不再是调用 install 方法
        refWatcher = installLeakCanary();
        ...
    }

    protected RefWatcher installLeakCanary() {
        return RefWatcher.DISABLED;
    }
}

新建 src/debug/java 文件夹,在其中创建 DebugExampleApplication:

// Debug 版本的 Application 类
public class DebugExampleApplication extends ExampleApplication {
    protected RefWatcher installLeakCanary() {
        RefWatcher refWatcher = LeakCanary.install(this);
        return refWatcher;
    }
}

在 src/debug 中新建 AndroidManifest.xml 文件:

<?xml version="1.0 encoding="utf-8" ?>
<manifest ...>
    <application
        tools:replace="android:name"
        android:name=".DebugExampleApplication" />
</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

标签定义在:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="__leak_canary_display_activity_label">MyLeaks</string>
</resources>

内存泄漏堆栈信息保存个数的自定义

默认情况下,DisplayLeakActivity 在 APP 目录中最多保存 7 个 HeapDump 文件和泄漏堆栈信息,可以在 APP 中定义 R.integer.__leak_canary_max_stored_leaks 来修改。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="__leak_canary_max_stored_leaks">20</string>
</resources>

Watcher 的延时

通过定义 R.integer.leak_canary_watch_delay_millis 来修改弱引用对象被认为出现内存泄漏的延时时间,默认 5 秒,下面修改为 1.5 秒:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="leak_canary_watch_delay_millis">1500</string>

自定义堆栈信息和 heap dump 的处理方式

可以通过继承 DisplayLeakService 并重写其中的 afterDefaultHandling 函数来实现定制化操作,例如将 heap dump 文件发送到服务端:

public class LeakUploadService extends DisplayLeakService {
    @Override
    protected void afterDefaultHandling(HeapDump headDump, AnalysisResult result, String leakInfo) {
        if (!result.leakFound || result.excludedLeak) {
            return;
        }
        myServer.uploadLeakBlocking(heapDump.headDumpFile, leakInfo);
    }
}

public class DebugExampleApplication extends ExampleApplication {
    protected RefWatcher installLeakCanary() {
        return LeakCanary.install(app, LeakUploadService.class, AndroidExcludedRefs.createAppDefaults().build());
    }
}

为了使 LeakUploadService 生效,需要在 AndroidManifest.xml 中注册。

忽略特定的弱引用

实现自己的 ExcludedRefs 忽略某些特定的弱引用对象,不对其进行内存泄漏的监视。

public class DebugExampleApplication extends ExampleApplication {
    protected RefWatcher installLeakCanary() {
        ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults()
            .instanceField("com.example.Example.class", "exampleField")
            .build();
        return LeakCanary.install(this, DisplayLeakService.class, excludedRefs);
    }
}

不监视特定 Activity

默认会监视所有 Activity 的内存泄漏,默认只支持 Android 4.0 以上的系统,如果 4.0 以下需要在 onDestroy 中主动 watch。

public class DebugExampleApplication extends ExampleApplication {
    @Override
    protected RefWatcher installLeakCanary() {
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return RefWatcher.DISABLED;
        } else {
            ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults().build();
            LeakCanary.enableDisplayLeakActivity(this);
            ServiceHeapDumpListener heapDumpListener = new ServiceHeapDumpListener(this, DisplayLeakService.class);
            final RefWatcher refWatcher = LeakCanary.androidWathcer(this, heapDumpListener, exlcudedRefs);
            registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
                public void onActivityDestroyed(Activity activity) {
                    if (activity instanceof MainActivyt) { // 排除某些 Activity
                        return;
                    }
                    refWatcher.watch(activity);
                }
            });
            return refWatcher;
        }
    }
}

内存优化

  • 使用软/弱/虚引用

  • 使用 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 的这套默认适配法则。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容