Android_内存优化篇
我们知道,Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。虽然Android 4.4发布了一个ART运行时,准备用来替换掉之前一直使用的Dalvik虚拟机,希望籍此解决饱受诟病的性能问题。但是这里我们先通过罗升阳的Dalvik虚拟机简要介绍和学习计划了解Dalvik虚拟机的内存管理与垃圾回收,对我们分析Android的内存优化会有很多的启示的。
内存泄漏
- 内存泄漏的原因:垃圾回收器无法回收原本应该被回收的对象,这个对象就引发了内存泄露。
- 内存泄露的危害:
- 过多的内存泄露最终会导致内存溢出(OOM)
- 内存泄露导致可用内存不足,会频繁触发GC,不管是Android2.2以前的单线程GC还是现在的CMS和G1,都有一部分的操作会导致用户线程停止(就是所谓的Stop the world),从而导致UI卡顿。
内存溢出
Android为每个进程设置Dalvik Heap Size阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果APP想要分配的内存超过这个阈值,就会发生OOM。
避免内存泄漏
1.Bitmap的处理
- Bitmap压缩
- Lru机制处理Bitmap,也可以使用那些有名的图片缓存框架
2.持有Context的引用导致的内存泄漏
在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着对整个activity保持引用。
示例一(LeakCanary的示例)
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_home);
findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startAsyncTask();
}
});
}
/*关于隐式引用----内部类可以直接去调用外部类的成员(属性和方法),
如果没有持有外部类的引用,内部类是没办法去调用外部类的成员,
但是内部类又没有显示的去指定声明引用,所以称之为隐式引用。*/
/*AsyncTask是一个匿名的内部类,隐式的持有外部类(MainActivity)的引用,
当activity被销毁的时候,如果AsyncTask(代码sleep 20秒,模拟了一个耗时操作)
没有执行完成,则MainActivity将会泄漏*/
void startAsyncTask() {
// This async task is an anonymous class and therefore has a hidden reference to the outer
// class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
// the activity instance will leak.
new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
// Do some slow work in background
SystemClock.sleep(20000);
return null;
}
}.execute();
}
}
示例二
package com.example.idea.memorytest;
import android.content.Context;
/**
* Created by idea on 2016/12/30.
* 单例模式持有context 对象引发内存泄漏示例
*/
public class MyInstanceTest {
private Context context;
private static MyInstanceTest mInstance;
public static MyInstanceTest getInstance(Context context) {
if (mInstance == null) {
synchronized (MyInstanceTest.class) {
if (mInstance == null)
mInstance = new MyInstanceTest(context);
//解决方法 把context的引用替换成ApplicationContext的引用
mInstance = new MyInstanceTest(context.getApplicationContext());
}
}
return mInstance;
}
private MyInstanceTest(Context context) {
this.context = context;
}
}
示例三
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_home);
test();
}
//加上static,变成静态匿名内部类
public static void test() {
//匿名内部类会引用其外围实例HomeActivity.this,所以会导致内存泄漏
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
//解决方法
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_home);
test();
}
//加上static,变成静态匿名内部类
public static void test() {
//匿名内部类会引用其外围实例HomeActivity.this,所以会导致内存泄漏
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
示例四
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_home);
findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
loadData();
}
});
}
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 0:
// 刷新数据
break;
default:
break;
}
};
};
private void loadData() {
//获取数据
mHandler.sendEmptyMessage(0);
}
}
//解决方法
//第一步,将Handler改成静态内部类。
private static class MyHandler extends Handler {
//第二步,将需要引用Activity的地方,改成弱引用。
private WeakReference homaActivityInstance;
public MyHandler(HomeActivity hai) {
this.homaActivityInstance = new WeakReference(hai);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
HomeActivity aty = homaActivityInstance == null ? null : homaActivityInstance.get();
//如果Activity被释放回收了,则不处理这些消息
if (aty == null||aty.isFinishing()) {
return;
}
}
}
private void loadData() {
// 获取数据
myHandler.sendEmptyMessage(0);
}
@Override
protected void onDestroy() {
//第三步,在Activity退出的时候移除回调
super.onDestroy();
myHandler.removeCallbacksAndMessages(null);
}
总结:避免context泄露:
-
在Android中其生命周期是在进程启动时开始,进程死亡时结束。所以在程序的运行期间,如果进程没有被杀死,静态变量就会一直存在,不会被回收掉。如果静态变量强引用了某个Activity中变量,那么这个Activity就同样也不会被释放,即便是该Activity执行了onDestroy(不要将执行onDestroy和被回收划等号)。这类问题的解决方案为:
- 1.寻找与该静态变量生命周期差不多的替代对象。
- 2.若找不到,将强引用方式改成弱引用。比较典型的例子如下:
-
在java中,创建一个非静态的内部类实例,就会引用它的外围实例。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。这类问题的解决方案为:
- 1.将内部类变成静态内部类
- 2.如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用。
- 3.在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务。
3.Cursor游标结果集,I/O流,数据库,网络的连接用完及时关闭。
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如 SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。
4.在Android程序里面存在很多需要register 和 unregister的监听器,我们需要确保及时unregister监听器。
代码中要注意的地方
5.使用ArrayMap/SparseArray来代替HashMap,ArrayMap/SparseArray是专门为移动设备设计的高效的数据结构。
6.不要轻易使用Enum
这点在Google的Android官方培训课程提到过,具体可以参考胡凯前辈的《 Android性能优化典范(三)》
7.避免创建不必要的对象
最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象
- String s = new String("hello world"); //don't do this!
- 使用单例模式
- 当心无意识的自动装箱
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
8.资源文件需要选择合适的文件夹进行存放
hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。
9.谨慎使用static对象
static对象的生命周期过长,应该谨慎使用
10.不要使用String进行字符串拼接
严格的讲,String拼接只能归结到内存抖动中,因为产生的String副本能够被GC,不会造成内存泄露。
频繁的字符串拼接,使用StringBuffer(不建议使用)或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。
11.非静态内部类内存泄露
在Activity中创建非静态内部类,非静态内部类会持有Activity的隐式引用,若内部类生命周期长于Activity,会导致Activity实例无法被回收。(屏幕旋转后会重新创建Activity实例,如果内部类持有引用,将会导致旋转前的实例无法被回收)。
解决方案:如果一定要使用内部类,就改用static内部类,在内部类中通过WeakReference的方式引用外界资源。
正确的代码示例:
static class ImageDownloadTask extends AsyncTask {
private String url;
private WeakReference photoAdapter;
public ImageDownloadTask(PhotoAdapter photoAdapter) {
this.photoAdapter = new WeakReference(photoAdapter);
}
@Override
protected Bitmap doInBackground(String... params) {
//在后台开始下载图片
url = params[0];
Bitmap bitmap = photoAdapter.get().loadBitmap(url);
if (bitmap != null) {
//把下载好的图片放入LruCache中
String key = MD5Tools.decodeString(url);
photoAdapter.get().put(key, bitmap);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
//把下载好的图片显示出来
ImageView mImageView = (ImageView) photoAdapter.get().mGridView.get().findViewWithTag(MD5Tools.decodeString(url));
if (mImageView != null && bitmap != null) {
mImageView.setImageBitmap(bitmap);
photoAdapter.get().mDownloadTaskList.remove(this);//把下载好的任务移除
}
}
}
12.匿名内部类内存泄漏
跟非静态内部类一样,匿名内部类也会持有外部类的隐式引用,比较常见的情况有,耗时Handler,耗时Thread,都会造成内存泄漏,解决方式也是static+WeakReference。
13.webview对象没有及时的destroy
一般情况下我们在activity的destory()方法里面会调用webView.destory(),但注意在android5.1之后,这样做会有引起内存泄漏的风险。具体可参考Android 5.1 WebView内存泄漏分析
14.慎用Services
service用于在后台执行一些耗时操作,只用当它执行任务的时候才开启,否则其他时刻都应该不工作,service完成任务之后要主动停止,否则如果用户发现有常驻后台行为的应用并且可能卸载它甚至引起内存泄漏。
当你开启一个service,系统会倾向为了保留这个service而一直保留service所在的进程。这使得进程的运行代价很高,因为系统没有办法把service所占用的RAM空间腾出来让给其他组件。
推荐使用IntentService, 它会在工作线程处理完交代给它的intent任务之后自动停止。
IntentService是Service类的子类,用来处理异步请求。客户端可以通过startService(Intent)
方法传递请求给IntentService。IntentService在onCreate()函数中通过HandlerThread单独
开启一个线程来处理所有Intent请求对象(通过startService的方式发送过来的)所对应的任务,这
样以免事务处理阻塞主线程。执行完所一个Intent请求对象所对应的工作之后,如果没有新的Intent
请求达到,则自动停止Service;否则执行下一个Intent请求所对应的任务。
IntentService在处理事务时,还是采用的Handler方式,创建一个名叫ServiceHandler的内部
Handler,并把它直接绑定到HandlerThread所对应的子线程。 ServiceHandler把处理一个intent
所对应的事务都封装到叫做onHandleIntent的虚函数;因此我们直接实现虚函数onHandleIntent,再
在里面根据Intent的不同进行不同的事务处理就可以了。
另外,IntentService默认实现了Onbind()方法,返回值为null。
使用IntentService需要两个步骤:
1、写构造函数
2、实现虚函数onHandleIntent,并在里面根据Intent的不同进行不同的事务处理就可以了。
好处:处理异步请求的时候可以减少写代码的工作量,比较轻松地实现项目的需求
注意:IntentService的构造函数一定是参数为空的构造函数,然后再在其中调用super("name")这种形式的构造函数。
因为Service的实例化是系统来完成的,而且系统是用参数为空的构造函数来实例化Service的
内存泄漏的检测
- 在AndroidStudio的Terminal中输入 adb shell dumpsys meminfo (应用包名) 查看内存信息
2.LeakCanary,一款非常好用的内存泄露检测工具,安装在手机上,能够通过Log的方式告诉你是哪块代码发生了内存泄露。
LeakCanary的Github上的地址是:https://github.com/square/leakcanary
有待补充~~
参考:
Android 性能优化&内存篇
Android性能优化那些事
EffectiveJave 中文版 第二版(JoshuaBloch 著 杨村花 俞黎敏 译)
检测工具
Android内存泄漏检测工具LeakCanary上手指南
Android内存泄漏终极解决篇