Android内存优化篇
Posted 花姓-老花
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android内存优化篇相关的知识,希望对你有一定的参考价值。
在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能,而且每台设备配置不一,分配内存大小也不一样
首先看android中的ActivityManager,这个类可以得到“设备配置的属性”,"进程信息","任务信息",“服务”,“正在运行的程序”
ActivityManager的内部类:
ActivityManager.MemoryInfo : 系统内存使用情况的信息 ,可以通过getMemoryInfo(ActivityManager.MemoryInfo). 来获得该类对象
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
ActivityManager.ProcessErrorStateInfo: 错误状态的进程
ActivityManager.RecentTaskInfo:近期用户打开的任务信息
ActivityManager.RunningAppProcessInfo: 正在运行的应用程序进程
ActivityManager.RunningServiceInfo: 正在运行的服务信息
ActivityManager.RunningTaskInfo: 正在运行的任务信息
ActivityManager类中的方法:
public ConfigurationInfo getDeviceConfigurationInfo ()
获得当前设备的配置信息
public int getLargeMemoryClass ()
如果在AndroidManifest.xml中的<application >标签下将属性android:largeHeap=“true”,这个属性决定你的应用进程是否应当在更大的Dalvik堆中创建,为了在运行时查看可用内存(我这边设置了堆的大小还是一样大【一般情况下建议不要设置】)
getLargeMemoryClass()获取系统分配的最大堆内存,单位为M
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
int largeMenClass = activityManager.getLargeMemoryClass();//单位为M
public int getLauncherLargeIconDensity ()
获取Launcher下图标的密度,其返回值: 密度DPI
public int getLauncherLargeIconSize ()
获取Launcher下图标的大小
public int getMemoryClass ()
获取内存信息(正常的堆内存的大小)
public void getMemoryInfo (ActivityManager.MemoryInfo outInfo)
获取系统可用内存信息,并将信息存入参数 outInfo中
ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(info);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("系统剩余内存:"+(info.availMem >> 10)+"k")
.append("\\n")
.append("系统是否处于低内存运行:"+info.lowMemory)
.append("\\n")
.append("当系统剩余内存低于"+info.threshold+"时就看成低内存运行");
availMem:表示系统剩余内存
lowMemory:它是boolean值,表示系统是否处于低内存运行
hreshold:它表示当系统剩余内存低于多少时就看成低内存运行
public static void getMyMemoryState (ActivityManager.RunningAppProcessInfo outState)
获取系统内存状态信息,并将信息存入参数 outState中,但不是全部信息
public MemoryInfo[] getProcessMemoryInfo (int[] pids)
获取每个进程(ID)使用的内存信息MemoryInfo,一一对应,返回一个内存信息集合MemoryInfo[]
参数: 进程的pid信息集合
int[] pid = {18752};
Debug.MemoryInfo[] infos = activityManager.getProcessMemoryInfo(pid);
StringBuilder stringBuilder = new StringBuilder();
for (Debug.MemoryInfo info : infos){
stringBuilder.append(info.toString()+"\\n");
}
dalvikPrivateDirty:The private dirty pages used by dalvik。
dalvikPss: The proportional set size for dalvik.
dalvikSharedDirty: The shared dirty pages used by dalvik.
nativePrivateDirty:The private dirty pages used by the native heap.
nativePss: The proportional set size for the native heap.
nativeSharedDirty: The shared dirty pages used by the native heap.
otherPrivateDirty: The private dirty pages used by everything else.
otherPss: The proportional set size for everything else.
otherSharedDirty: The shared dirty pages used by everything else.
dalvik: 是指dalvik所使用的内存
什么是Dalvik:
Dalvik是Google公司自己设计用于Android平台的Java虚拟机。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
什么是ART:
Android操作系统已经成熟,Google的Android团队开始将注意力转向一些底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快执行效率更高更省电的替代ART运行时。 ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。
ART优点:
1、系统性能的显著提升。
2、应用启动更快、运行更快、体验更流畅、触感反馈更及时。
3、更长的电池续航能力。
4、支持更低的硬件。
ART缺点:
1、更大的存储空间占用,可能会增加10%-20%。
2、更长的应用安装时间
native:是被native堆使用的内存,应该指使用C\\C++在堆上分配的内存。
other: 是指除dalvik和native使用的内存。但是具体是指什么呢?至少包括在C\\C++分配的非堆内存,比如分配在栈上的内存。
public List<ActivityManager.ProcessErrorStateInfo> getProcessesInErrorState ()
返回一个处于错误状态的进程列表,如果没有错误状态的进程,返回Null
public List<ActivityManager.RecentTaskInfo> getRecentTasks (int maxNum, int flags)
获得最近开启的任务
参数: maxNum: 返回的最大数量
flags: 返回的类型May be any combination of RECENT_WITH_EXCLUDED and RECENT_IGNORE_UNAVAILABLE.
public List<ActivityManager.RunningAppProcessInfo> getRunningAppProcesses ()
返回正在运行的应用程序进程列表
public PendingIntent getRunningServiceControlPanel (ComponentName service)
返回一个PendingIntent ,给一个组件名,如果有与这个名字匹配的Service则返回,否则返回Null
public List<ActivityManager.RunningServiceInfo> getRunningServices (int maxNum)
获取系统里正在运行的服务, maxNum 为要获取服务的最大数量,一般为20或者50
public List<ActivityManager.RunningTaskInfo> getRunningTasks (int maxNum)
获取系统里正在运行的任务, maxNum 为要获取服务的最大数量
public void killBackgroundProcesses (String packageName)
立即杀掉给定包名的进程,释放进程占用的资源(内存等) 【只有用户进程可以kill】
通过命令行
adb shell:进入android底层Linux系统命令
ps:查看系统里面进程的命令
adb shell dumpsys meminfo +包名:将得到该包下进程使用的内存的信息
内存的分配策略概述
程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、堆区和栈区。
静态存储区(方法区):内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在。它主要存放静态数据、全局static数据和常量。
栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存(Java则依赖垃圾回收器)。
动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉。
堆和栈的区别:
在函数中(说明是局部变量)定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
堆内存用于存放所有由new创建的对象(内容包括该对象其中的所有成员变量)和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。
在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。
堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G),所以堆的空间比较灵活,比较大。
栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在编译时确定,VC中可设置)。
对于堆,频繁的new/delete会造成大量内存碎片,使程序效率降低。对于栈,它是先进后出的队列,进出一一对应,不产生碎片,运行效率稳定高。
内存泄露产生的原因
在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但它只能回收无用并且不再被其它对象引用的那些对象所占用的空间。
Java的内存垃圾回收机制是从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收。
GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
在Java中,这些无用的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。
虽然,我们有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC。
通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。
但是我们仍然可以去监听系统的GC过程,以此来分析我们应用程序当前的内存状态。那么怎样才能去监听系统的GC过程呢?
其实非常简单,系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下所示:
首先第一部分GC_Reason,这个是触发这次GC操作的
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>
原因,一般情况下一共有以下几种触发GC操作的原因:
GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。
GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。
GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。
GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。
接下来第二部分Amount_freed,表示系统通过这次GC操作释放了多少内存。
然后Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)。
最后Pause_time表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,Android在2.3的版本当中进行过一次优化,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。虽说这个阻塞的过程并不会很长,也就是几百毫秒,但是用户在使用我们的程序时还是有可能会感觉到略微的卡顿。
而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了。
我们来看看Java中需要被回收的垃圾:
{
Person p1 = new Person();
……
}
引用句柄p1的作用域是从定义到“}”处,执行完这对大括号中的所有代码后,产生的Person对象就会变成垃圾,因为引用这个对象的句柄p1已超过其作用
域,p1失效,在栈中被销毁,因此堆上的Person对象不再被任何句柄引用了。 因此person变为垃圾,会被回收。
这里我们需要讲述一个关键词:引用,通过A能调用并访问到B,那就说明A持有B的引用,或A就是B的引用,B的引用计数+1.
(1)比如 Person p1 = new Person();通过P1能操作Person对象,因此P1是Person的引用;
(2)比如类O中有一个成员变量是I类对象,因此我们可以使用o.i的方式来访问I类对象的成员,因此o持有一个i对象的引用。
GC过程与对象的引用类型是严重相关的,我们来看看Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference
在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。
假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。
如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。
相关内存泄漏情况
activity泄漏(非静态内部类)
比如经常进出某个activity,在该activity需要处理一个耗时的操作,一般耗时工作都交给一个线程来处理,代码如下:
public class TwoActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_two);
ThreadClass threadClass = new ThreadClass();
threadClass.start();
}
private class ThreadClass extends Thread {
@Override
public void run() {
super.run();
while (true){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
上面代码存在activity内存泄漏,因为是ThreadClass是一个匿名内部类,正常来说该activity退出时就应该被回收了,但由于ThreadClass持有该activity引用,内存回收器(CG)不能回收掉,所以反复进出该activity很容量造成内存泄漏,下面一个内存分析工具(Android Device Monitor)来分析这种现象:
没进入该activity之前,内存情况
反复进入上面activity内存情况
那怎么处理呢? 可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例
public class ThreeActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_three);
ThreadClass threadClass = new ThreadClass();
threadClass.start();
}
public static class ThreadClass extends Thread {
@Override
public void run() {
super.run();
while (true){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
这样虽然避免了Activity内存泄露,但是这个线程却发生了内存泄露。在Java中线程是垃圾回收机制的根源,也就是说,在运行系统中DVM虚拟机总会使硬件持有所有运行状态的进程的引用,结果导致处于运行状态的线程将永远不会被回收。因此,你必须为你的后台线程实现销毁逻辑!下面是一种解决办法:
public class ThreeActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_three);
ThreadClass threadClass = new ThreadClass();
threadClass.start();
}
public static class ThreadClass extends Thread {
private boolean mRunning = false;
@Override
public void run() {
super.run();
mRunning = true;
while (mRunning){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void close() {
mRunning = false;
}
}
}
我们在Activity退出时,可以在 onDestroy()方法中显示调用mThread.close();以此来结束该线程,这就避免了线程的内存泄漏问题
单例造成的内存泄漏
Android的单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏。
因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
public class Single {
private static Single instance;
private Context mContext;
public Single(Context context){
this.mContext = context;
}
public static Single getInstance(Context context){
if (instance == null){
instance = new Single(context);
}
return instance;
}
}
这里需要传入的是上下文Context,所以这个Context的生命周期的长短非常重要
1)、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长;
2)、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}
上面是一段简单的Handler的使用。当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)。而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。另外,如果你执行了Handler的postDelayed()方法:
handler.postDelayed(this, 2000); //每两秒实现一次的定时器操作
该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在Looper中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
使用Handler导致内存泄露的解决方法
方法一:通过程序逻辑来进行保护。
1).在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
2).如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
方法二:将Handler声明为静态类。
静态类不持有外部类的对象,所以你的Activity可以随意被回收。代码如下:
static class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}
但其实没这么简单。使用了以上代码之后,你会发现,由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference):
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) {
mImageView.setImageBitmap(mBitmap);
}
}
}
将代码改为以上形式之后,就算完成了。
资源对象没关闭造成的内存泄漏
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
Cursor cursor = getContentResolver().query(uri...);
if (cursor.moveToNext()) {
... ...
}
修正示例代码:
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri...);
if (cursor != null &&cursor.moveToNext()) {
... ...
}
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
//ignore this
}
}
}
Bitmap没有回收导致的内存溢出
Bitmap的不当处理极可能造成OOM,绝大多数情况都是因这个原因出现的。Bitamp位图是Android中当之无愧的胖小子,所以在操作的时候当然是十分的小心了。由于Dalivk并不会主动的去回收,需要开发者在Bitmap不被使用的时候recycle掉。使用的过程中,及时释放是非常重要的。同时如果需求允许,也可以去BItmap进行一定的缩放,通过BitmapFactory.Options的inSampleSize属性进行控制。如果仅仅只想获得Bitmap的属性,其实并不需要根据BItmap的像素去分配内存,只需在解析读取Bmp的时候使用BitmapFactory.Options的inJustDecodeBounds属性。最后建议大家在加载网络图片的时候,使用软引用或者弱引用并进行本地缓存
构造Adapter时,没有使用缓存的convertView
在ListView的BaseAdapter中提供了getView()方法中使用缓存机制,这个平时开发中用都比较多,相信大家都比较熟悉了,就不在总结了
内存抖动
内存抖动是因为大量的对象被创建又在短时间内马上被释放
瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题
解决上面的问题有简洁直观方法,如果你在Memory Monitor里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。
同时我们还可以通过Allocation Tracker来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。
当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,
避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。
预防OOM的几点建议
Android开发过程中,在 Activity的生命周期里协调耗时任务可能会很困难,你一不小心就会导致内存泄漏问题。下面是一些小提示,能帮助你预防内存泄漏问题的发生:
1、合理使用static:
每一个非静态内部类实例都会持有一个外部类的引用,若该引用是Activity 的引用,那么该Activity在被销毁时将无法被回收。如果你的静态内部类需要一个相关Activity的引用以确保功能能够正常运行,那么你得确保你在对象中使用的是一个Activity的弱引用,否则你的Activity将会发生意外的内存泄漏。但是要注意,当此类在全局多处用到时在这样干,因为static声明变量的生命周期其实是和APP的生命周期一样的,有点类似与Application。如果大量的使用的话,就会占据内存空间不释放,积少成多也会造成内存的不断开销,直至挂掉。static的合理使用一般用来修饰基本数据类型或者轻量级对象,尽量避免修复集合或者大对象,常用作修饰全局配置项、工具类方法、内部类。
2、善用SoftReference/WeakReference/LruCache
Java、Android中有没有这样一种机制呢,当内存吃紧或者GC扫过的情况下,就能及时把一些内存占用给释放掉,从而分配给需要分配的地方。答案是肯定的,java为我们提供了两个解决方案。如果对内存的开销比较关注的APP,可以考虑使用WeakReference,当GC回收扫过这块内存区域时就会回收;如果不是那么关注的话,可以使用SoftReference,它会在内存申请不足的情况下自动释放,同样也能解决OOM问题。同时Android自3.0以后也推出了LruCache类,使用LRU算法就释放内存,一样的能解决OOM,如果兼容3.0一下的版本,请导入v4包。关于第二条的无关引用的问题,我们传参可以考虑使用WeakReference包装一下。
3、谨慎handler
在处理异步操作的时候,handler + thread是个不错的选择。但是相信在使用handler的时候,大家都会遇到警告的情形,这个就是lint为开发者的提醒。handler运行于UI线程,不断处理来自MessageQueue的消息,如果handler还有消息需要处理但是Activity页面已经结束的情况下,Activity的引用其实并不会被回收,这就造成了内存泄漏。解决方案,一是在Activity的onDestroy方法中调handler.removeCallbacksAndMessages(null);取消所有的消息的处理,包括待处理的消息;二是声明handler的内部类为static。
4、不要总想着Java 的垃圾回收机制会帮你解决所有内存回收问题
就像上面的示例,我们以为垃圾回收机制会帮我们将不需要使用的内存回收,例如:我们需要结束一个Activity,那么它的实例和相关的线程都该被回收。但现实并不会像我们剧本那样走。Java线程会一直存活,直到他们都被显式关闭,抑或是其进程被Android系统杀死。所以,为你的后台线程实现销毁逻辑是你在使用线程时必须时刻铭记的细节,此外,你在设计销毁逻辑时要根据Activity的生命周期去设计,避免出现Bug。
5、使用更优的数据结构
参考博文:http://m.blog.csdn.net/article/details?id=51579080
以上是关于Android内存优化篇的主要内容,如果未能解决你的问题,请参考以下文章
[Android 性能优化系列]内存之基础篇--Android怎样管理内存