Android基础(11)—你需要知道的内存知识

内存

基本概念

垃圾回收(GC)

和Java一样,Android也是基于垃圾回收(Gabage Collection ,简称GC)机制实现内存的自动回收的。目前最基本的垃圾回收算法有四种:标记—清除算法(Mark—Sweep)、标记—压缩算法(Mark—Compact)、复制算法(Copying)以及引用计数算法(Reference Counting)。现代流行的垃圾收集算法一般是由这四种中的其中几种算法组合而成。在Android虚拟机中,无论是Dalvik还是Art,都是使用标记—清除(Mark—Sweep)算法进行垃圾回收的。

内存泄漏

在 Android 中,内存泄漏是指不再使用的对象依然占用内存,或者它们占用的内存没有及时得到释放,从而造成内存空间的不断减少的现象,由于Android应用可使用的内存较少,发生内存泄漏会使得内存使用更加紧张,甚至最终由于内存耗尽而发生00M(OutOfMemoryError)错误,导致应用崩溃。

软引用

使用SoftReference关联的对象,用来表示一些有用但不是必需的对象,被SoftReference关联的对象,只有在内存不足的时候才会被垃圾回收。

弱引用

使用WeakReference关联的对象,用来表示非必需的对象, 在虚拟机进行垃圾回收时,,无论内存是否充足,,这类对象都会被回收。

引用队列

引用队列ReferenceQueue一般是作为WeakReference(SoftReference) 的构造函数参数传入, 在WeakReference(SoftReference)指向的对象被垃圾回收后,ReferenceQueue就是用来保存这些已经被回收的Reference。

内存泄漏

造成内存泄漏的原因及处理

1.大量的图片、音频、视频处理,当在内存比较低的系统上也容易造成内存溢出

处理:建议使用第三方,或者JNI来进行处理

2.Bitmap对象的不正确处理(内存溢出)

处理:不要在主线程中处理图片;使用Bitmap对象要用recycle释放

3.非静态匿名内部类Handler由于持有外部类上下文的引用所造成的内存泄漏

处理:根据WeakReference对象,对handler使用弱引用,并且调用removeCallbacksAndMessages移除

4.线程由于匿名内部类runnable持有Activity的引用,从而关闭Activity,线程未完成造成内存泄漏

处理:把线程改为静态内部类,调用WeakReference来持有外部资源

5.BroadcastReceiver、File、Cursor等资源的使用未及时关闭

处理:在销毁Activity时,应该及时销毁或者回收

6.static关键字修饰的变量由于生命周期过长,容易造成内存泄漏

处理:尽量少使用静态变量,一定要使用时要及时进行置null处理

7.单例模式造成的内存泄漏,如context的使用,单例中传入的是Activity的context,在关闭Activity时,Activity的内存无法被回收,因为单例持有Activity的引用

处理:在context的使用上,应该传入application的context到单例模式中,这样就保证了单例的生命周期跟application的生命周期一样;单例模式应该尽量少持有生命周期不同的外部对象,一旦持有该对象的时候,必须在该对象的生命周期结束前置null。

使用Profiler

Profiler是一个能够实时获取应用资源使用情况的工具。需要注意的是,这个工具只能作用于正在运行的应用,因此,确保要测试的应用已经安装到你的设备中,并且你的设备已经连接到计算机上。

Profiler已经内置在Android Studio中,因此,可以点击Android Studio底部的“Profiler”这个tab来切换到资源使用情况,然后点击上面的“Memory”就可以看到内存使用情况了。

Profiler内存使用情况.png

花一些时间与设备交互,并且关注内存使用情况。最终已分配的内存会增长,直到没有内存可用。此时,系统就会释放触发GC内存,当你看到已分配的内存明显下降时就代表GC操作被触发了。GC通常情况下会将无用的内存释放,但当你看到App在短时间内快速增长或者GC变得非常频繁,这就是内存泄漏的信号。

要追踪一个可疑的内存泄漏问题,可以直接点击内存使用节点,打开Heap工具,查看情况。这个工具提供了许多关于应用堆内存使用情况的数据,包含App内分配的各种对象、分配的对象数量以及这些对象占用了多少空间。Heap输出信息会在GC事件之后,因为可以手动点击GC按键来触发GC,使得Heap内存数据尽快地显示出来。一旦GC事件被触发了,Heap标签下就会更新App的堆内存使用信息。

Heap.png
内存泄漏检测函数库——LeakCanary

LeakCanary是Square公司开源的一个检测内存泄漏的函数库,可以方便地和你的项目进行集成,在Debug版本中监控Activity、Fragment等的内存泄漏。使用这个函数库后,Square修复了很多内存泄漏问题,甚至发现了Android SDK中的泄漏,号称解决了自家产品中94%的OOM问题。集成LeakCanary到工程之后,在检测到内存泄漏时,会在发送消息到系统通知栏,点击后打开名为DisplayLeakActivity的页面,并显示泄漏的跟踪信息,Logcat上面也会有对应的日志输出。同时,如果觉得跟踪信息不总以定位时,DisplayLeakActivity默认保存了最近的7个dump文件到APP的目录下,可以使用MAT等工具对dump文件进行分析。

用法:

创建一个新的Android Studio项目,在build.gradle的dependencies中添加LeakCanary的引用:

dependencies{
    //省略其他代码
     debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
}

然后创建一个Application子类:

public class ExampleApplication extends Application {
    
    private static RefWatcher mRefWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
        mRefWatcher = LeakCanary.install(this);
    }
    
    public static RefWatcher getmRefWatcher(){
        return mRefWatcher;
    }
}

注意,在ExampleApplication的onCreate函数中调用了LeakCanary.install函数,该函数返回到了一个RefWatcher,它用于监控、追踪应用中的对象引用。然后在AndroidManifest.xml中将该ExampleApplication设置为应用的Application:

 

此时LeakCanary就集成完毕了。下面模拟一次内存泄漏。应用中包含MainActivity和LeakActivity两个Activity。用户从MainActivity中进入到LeakActivity中,而在LeakActivity中的ActivityMgr会将LeakActivity添加到一个Activity容器中,由于ActivityMgr是一个静态单例,因此,LeakActivity就无法释放:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_main);
        //点击进入LeakActivity
        findViewById(R.id.btn_to_leak_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, LeakActivity.class);
                startActivity(intent);
            }
        });
    }
}

在LeakActivity的onCreate函数中,调用ActivityMgr单例对象的addActivity函数将LeakActivity对象添加到Activity列表中:

public class LeakActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        //持有了LeakActivity的引用
        ActivityMgr.getsInstance().addActivity(this);
    }
}

ActivityMgr是一个单例类,里面有一个List

public class ActivityMgr {

    private static ActivityMgr sInstance = new ActivityMgr();
    List

运行应用,并且在MainActivity跳转到LeakActivity,然后再返回MainAcitvity,等待几秒后会看到通知栏上弹出一个通知,点击该通知会进入到一个显示了内存泄漏信息的页面。页面明确指出哪个对象被泄漏以及哪个对象持有了被泄漏的对象导致内存无法被释放。在该例子中,出现内存泄漏的对象就是LeakActivity对象,持有它的对象是ActivityMgr中的activities列表:

demo.png

内存优化

Random Access Memory(RAM)在任何软件开发环境中都是一个很宝贵的资源,这一点在物理内存通常很有限的移动操作系统上,显得尤为突出。尽管 Android时Dalvik 虚拟机扮演了常规的垃圾回收的角色,但这并不意味着你可以忽视App的内存分配与释放的时机与地点。为了 GC 能够从 App 中及时回收内存,需要注意避免内存泄路井且在适当的时机来释放引用对象。对于大多数App来说,Dalvik的GC会自动把离开活动线程的对象进行回收。

1.珍惜Services资源

如果你的 App需要在后台使用Service,除非它被触发执行一个任务,否则其他时候都应该是非运行状态。 同样需要注意,当这个 Service 已经完成任务后因为停止Service 失败而引起的泄漏。当你启动一个 Service,系统会倾向为了保留这个 Service 而一直保留 Service 所在的进程。这使得进程的运行代价很高,因为系统没有办法把Service 所占用的RAM 让给其他组件或者被pagedout.。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响App之间的切换效率。 它甚至会导致系统内存使用不稳定, 从而无法继续保持住所有目前正在运行的Service。

限制你的Service 的最好办法是使用IntentService,它会在处理完扔给它的 intent 任务之后尽快
结束自己。当一个 Service 已经不需要时还继续保留它, 这对 Android 应用的内存管理来说是最糟糕的错误之一。 因此, 千万不要贪婪地使得一个 Service 持续保留。 不仅仅是因为它会使得你的 App 因
RAM的限制而性能糟糕,而且用户会发现这些有着常驻后台行为的应用并且卸载它。

2.当UI隐藏时释放内存

当用户切换到其他应用并且你的应用 UI不再可见时, 应该释放UI上所占用的所有资源。 在这个时候释放UI资源可以显著地增加系统缓存进程的能力,它会对用户体验有着很直接的影响。为了能够接收到用户离开你的UI时的通知,需要实现 Activtiy 类里面的 onTrimMemory()回调方法。 应该使用这个方法来监听到 TRIM_MEMORY_UI_HIDDEN 级别的回调,此时意味着你的UI 已经隐藏,你应该释放那些仅被你的 UI 使用的资源。

请注意: 你的应用仅仅会在所有 UI 组件被隐藏时接收到 onTrimMemory()的回调并带有参数TRIM_MEMORY_UI_HIDDEN。这与onStop()的回调是不同的,onStop会在Activity 的实例隐藏时执行, 例如,当用户从你的 App 的某个 Activity 跳转到另外一个 Activity时 onStop会被执行。 因此,应该实现 onStop 回调,并且在此回调里面释放 Activity 的资源,例如,网络连接,unregister广播接收者。 除非接收到 onTrimMemory (TRIM_MEMORY_UI_HIDDEN)) 的回调,否则不应该释放你的UI资源。这确保了用户从其他 Activity 切换回来时,你的UI资源仍然可用, 并且可以迅速恢复Activity。

3.当内存紧张时释放部分内存

在你的 App生命周期的任何阶段,onTrimMemory 回调方法同样可以告诉你整个设备的内存资源己经开始系张。 你应该根据onTrimMemory 方法中的内存级别来进一步决定释放哪些资源。

TRIM_MEMORY_RUNNING_MODERATE:你的 App 正在运行并且不会被列为可杀死的,但是,设备此时正运行于低内存状态下,系统开始触发杀死LRU Cache 中的Process 的机制。

TRIM_MEMORY_RUNNING_LOW:你的 App 正在运行且没有被列为可杀死的,但是,设
备 正运行于更低内存的状态下, 你应该释放不用的资源用来提升系统性能 (但是这也会直接影响到你的 App 的性能)。

TRIM_MEMORY_RUNNING_CRITICAL:你的App 仍在运行,但是,系统已经把LRU Cache中的大多数进程都已经杀死, 因此, 应该立即释放所有非必须的资源。 如果系统不能回收到足够的RAM数量,系统将会清除所有的LRU缓存中的进程,并且开始杀死那些之前被认为不应该杀死的进程,例如,那个包含了一个运行态Service的进程。
同样,当你的App 进程正在被 cached时,可能会接收到从 onTrimMemory()中返回的下面的值之一。

TRIM_MEMORY_BACKGROUND:系统正运行于低内存状态并且你的进程正处于LRU缓存名单中最不容易杀掉的位置。 尽管你的App进程并不是处于被杀掉的高危险状态,系统可能已经开始杀掉LRU缓存中的其他进程了。你应该释放那些容易恢复的资源,以便于你的进程可以保留下来,这样当用户回退到你的App时才能够迅速恢复。

TRIM_MEMORY_MODERATE: 系统正运行于低内存状态并且你的进程已经接近LRU名单的中部位置。 如果系统开始变得更加内存紧张,你的进程是有可能被杀死的。

TRIM MEMORY_COMPLETE: 系统正运行与低内存的状态并且你的进程正处于LRU名单中最容易被杀掉的位置。你应该释放任何不影响你的 App恢复状态的资源。因为onTrimMemory()的回调是在API 14才被加进来的,对于老的版本,你可以使用onLowMemory回调来进行兼容。onLowMemory 相当与TRIM_MEMORY_COMPLETE。

Note: 当系统开始清除LRU缓存中的进程时, 尽管它首先按照LRU的顺序来操作,但是,它同样会考虑进程的内存使用量。因此,消耗越少的进程则越容易被留下来。

4.检查你应该使用多少内存

每一个Android 设备都会有不同的RAM总大小与可用空间,因此,不同设备为App提供了不同大小的heap 限制。可以通过调用getMemoryClass()来获取你的App的可用 heap大小。 如果你的App尝试申请更多的内存, 会出现 OutOfMemory 的错误。在一些特殊的情景下,可以通过在 manifest 的 application 标签下添加 largeHeap=true 的属性来声明一个更大的heap空间。如果这样做,可以通过 getLargeMemoryClass()来获取到一个更大的heap size。
然而,能够获取更大 heap 的设计本意是为了一小部分会消耗大量 RAM 的应用 (如一个大图片的编辑应用)。不要轻易地因为你需要使用大量的内存而去请求一个大的heap size。 只有当你清楚地知道哪里会使用大量的内存并且为什么这些内存必须被保留,才去使用large heap。 因此请尽量少使用 large heap。使用额外的内存会影响系统整体的用户体验,并且会使得GC的每次运行时间更长。 在任务切换时,系统的性能会变得大打折扣。

另外,large heap 并不一定能够获取到更大的 heap。在某些有严格限制的机器上,large heap 的大小和通常的heap size 是一样的。因此,即使你申请了large heap,还是应该通过执行 getMemoryClass()来检査实际获取到的 heap 大小。

5.避免bitmaps的浪费

当你加载一个 bitmap 时,仅仅需要保留适配当前屏幕设备分辨率的数据即可,如果原图高于你的设备分辨率,需要做缩小的动作。请记住,增加bitmap的尺寸会对内存呈现出2次方的增加,因为X与Y都在增加。

在 Android 2.3.x(API level 10) 及其以下版本,bitmap对象的 pixel data 是存放在native内存中的, 它不便于调试。 然而, 从Android 3.0 (API level 11) 开始,bitmap pixel data 是分配在你的App的Dalvik heap中,这提升了GC的工作效率并且更加容易Debug。 因此,如果你的App使用bitmap并在旧的机器上引发了一些内存问题,切换到Android3.0以上的机器上进行Debug。如果你的应用中需要使用图片加载的功能,建议使用成熟的 ImageLoader 框架,如 Glide、Picasso、Fresco等,这样能够避免加载图片时的很多问题。

6.使用优化的数据容器

利用 Android Framework 里面优化过的容器类,例如 SparseArray、 SparseBooleanArray与LongSparseArray。通常的 HashMap 的实现方式更加消耗内存, 因为它需要一个额外的实例对象来记录 Mapping 操作。 另外,SparseArray 更加高效在于它们避免了对key 与 value的 autobox 自动装箱,并且避免了装箱后的解箱。

7.请注意内存开销

对你所使用的语言与库的成本与开销有所了解,从开始到结束,在设计你的 App 时谨记这些信息。 通常,表面上看起来无关痛痒的事情也许实际上会导致大量的内存开销。 例如,Enums的内存消耗通常是static constants的2倍。 应该尽量避免在Android上使用enums。 在Java中的每一个类 (包括匿名内部类) 都会使用大概 500 bytes。 每一个类的实例产生的花销是 12-16 bytes。往HashMap 添加一个entry 需要一个额外占用的32 bytes 的 entry 对象。

8.请注意代码“抽象”

通常,开发者使用抽象作为 “好的编程实践”,因为抽象能够提升代码的灵活性与可维护性。然而,抽象会导致一个显著的开销: 通常它们需要同等量的代码用于可执行。 那些代码会被 map到内存中。 因此,如果你的抽象没有显著地提升效率,应该尽量避免它们。

9.为序列化的数据使用nano protobufs

Protocol buffers 是由 Google 为序列化结构数据而设计的,一种语言无关,平台无关,具有良好折展性的协议。类似xml,却比xml更加轻量、快速、简单。 如果需要为你的数据实现协议化,应该在客户端的代码中总是使用 nano protobufs。通常的协议化操作会生成大量繁琐的代码,这容易给你的 App带来许多问题: 增加RAM 的使用量,显著增加APK的大小,更慢的执行速度,更容易达到DEX的字符限制。

10.避免使用依赖注入框架

使用类似Guice 或者 RoboGuice等 framework injection 包是很有效的,因为它们能够简化你的代码。RoboGuice 2 通过依赖注入改变代码风格,让 Android 开发时的体验更好。RoboGuice把这些需要猜测性的工作移到 Android 开发以外去了。 注入你的 View、Resource、System Service或者其他对象,RoboGuice2 会负责这些细节。然而,那些框架会通过扫描你的代码执行许多初始化的操作,这会导致你的代码需要大量的RAM来map代码。但是,mapped pages会长时间的被保留在RAM中。

11.谨慎使用外部库

很多library的代码都不是为移动开发环境而编写的,因此,运用到移动开发时会导致影响 App效率。即使是针对Android 而设计的 library,也可能是很危险的。 因为每一个library 所做的事情都是不一样的。 例如,其中一个lib使用的是 nano protobufs,而另外一个使用的是micro protobufs。那么,在你的 App 里面就有2种protobuf的实现方式。这样的冲突同样可能发生在输出日志、加载图片、缓存等模块里面。同样不要陷入为了1个或者2个功能而导入整个 library 的陷阱。 如果没有一个合适的库与需求相吻合, 应该考虑自己去实现,而不是导入一个大而全的解决方案。

12.优化整体性能

谷歌官方中列出了许多优化整个 App 性能的文章: Best Practices for Performance。这篇文章就是其中之一。有些文章是讲解如何优化App的CPU使用效率,有些是如何优化App的内存使用效率。

13.使用ProGuard来剔除不需要的代码

ProGuard 能够通过移除不需要的代码,重命名类、域与方法等方式对代码进行压缩,优化与混淆。使用ProGuard 可以使你的代码更加紧凑,这样能够使用更少 mapped 代码所需要的RAM。

14.对最终的APK使用zipalign

在编写完所有代码,并通过编译系统生成APK之后,需要使用zipalign对APK进行重新校准。如果你不做这个步骤,会导致APK需要更多的RAM,因为一些类似图片资源的东西不能被mapped。注意:GooglePlay不接受没有经过zipalign的APK。

15.使用多进程

如何合适,有一个更高级的技术可以帮助你的APP管理内存使用:通过把App组件切分成多个组件,运行在不同的进程中。这个技术必须谨慎使用,大多数App都不应该运行在多个进程中。因为,如果使用不当,它会显著增加内存的使用,而不是减少。当你的App需要在后台运行与前台一样的大量的任务时,可以考虑使用这个技术。

一个典型的例子是创建一个可长时间后台播放的Music Player。如果整个App运行在一个进程中,当后台播放时,前台的那些UI资源也没有办法得到释放。类似这样的App可以切分成2个进程:一个用了操作UI,另外一个用来后台的Service。可以通过在AndroidManifest文件中声明“android:process”属性来实现某个组件运行在另外一个进程的操作:

通过这些方式能够使你的App内存占用得到一定的改善,然而,任何时候都不要轻视内存问题。只有再每个阶段都做好内存管理,才能使你的App更健壮、更高效。


上一篇:Android基础(10)—你需要知道的HTTP(S)、TCP、UDP
下一篇:Android基础(12)—常用第三方库 OKHttp

精彩内容不够看?更多精彩内容,请到微信搜索 “危君子频道” 订阅号,每周更新,欢迎大家关注订阅!

微信公众号

你可能感兴趣的:(Android基础(11)—你需要知道的内存知识)