内存优化
内存抖动
- 在Java中每创建一个对象,就会申请一块内存,用来存储对象信息
- 每分配一块内存,程序的可用内存就少一块
- 当程序被占用的内存达到一个临界,垃圾回收器(GC)开始工作,释放一部分不再被使用的内存,这本身没问题
- 但当频繁的创建对象就会造成被占用的内存不断攀升,回收后又迅速涨起来,接着又回收
- 这种短时间内反复内存增长和回收现象就是内存抖动
可通过Android Studio的Profiler查看
引发问题
内存抖动可能导致程序卡顿甚至OOM内存溢出
卡顿
- 内存回收在Java中采用的是GC机制,无论哪种方式实现的GC在执行时都不可避免的需要STW(Stop The World)
- STW意味着所有的工作线程都将被暂停,虽然时间很短,但终究会存在时间成本,一两次内存回收不易被察觉,但多次内存回收集中在短时间内爆发,这就会造成较大程度的界面卡顿风险
例:当用户在界面滑动或点击某个按钮时,此时虚拟机在运行GC线程,进行内存回收,响应事件的线程就会被GC暂停,等恢复后才能响应事件,对用户的直观感受就是程序卡顿。
OOM
- 内存抖动除了会导致卡顿外,也会造成内存溢出(OOM)
- 若垃圾回收采用的是标记-清除算法,那么此算法会导致大量的内存碎片
当程序频繁创建与回收对象时,会导致程序中连续内存不足
例:需要创建一个占用10个格子大小内存的字节数组对象时,若可用内存中连续内存不足,就会出现OOM。虽然回收后,可用内存不止10个格子大小,但没有连续的内存。这就是内存碎片,可用的连续空间比要申请的空间小,导致这些小内存块不能被利用
Android中尽量避免在View的onDraw()中创建对象,因为onDraw()会被频繁调用
尽量避免在频繁执行或循环中创建对象
内存泄漏
程序中已动态分配的堆内存由于某种原因未被释放或无法释放,造成系统内存浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
原因
GC机制中判断一个对象是否可被回收,垃圾对象检测主要有两种算法:引用计数器法 和 可达性分析法
引用计数器法(java未使用该方法)
为每个对象设置一个引用计数器,每当有一个地方引用时,计数器+1,引用失效时,计数器-1,任何时刻计数器值为0的对象,就表示该对象不可再被使用,但当两个对象相互引用就会导致无法回收
可达性分析法(java使用该方法)
通过一系列称为"GC Roots"的对象为起始点,从这些节点向下搜索,搜索所有的引用链,当一个对象到GC Roots没有任何引用链时(即GC Roots到对象不可达),则该对象不可用
- Java中可作为GC Roots的对象包括
- 方法区:类静态属性对象、常量对象
- 虚拟机栈(本地变量表)中的对象
- 本地方法栈JNI(Native方法)中的对象
示例
- 单例引起的内存泄漏
单例静态特性导致它的生命周期与程序生命周期一样,若有对象不再使用,却被单例持有,就会导致其无法回收,进而导致内存泄漏
kotlin
/**
* 若context对象传入了Activity,就会导致Activity销毁时无法被回收
* 解决方案:传入Application上下文
*/
object CommonTool {
lateinit var mContext: Application
fun getContext(): Application? {
if (::mContext.isInitialized) {
return mContext
}
return null
}
}
- 非静态内部类创建静态实例引起的内存泄漏
非静态内部类会默认持有外部类引用,若在外部类中去创建这个内部类对象,当频繁打开关闭Activity时,会导致重复创建对象,造成资源浪费。为避免该问题,可把该实例设为静态,解决重复创建实例问题,但会引起其他问题(静态成员变量生命周期和程序生命周期一样),静态成员变量持有Activity引用,所以Activity销毁时,无法回收
java
/**
* 解决方案:将Test改为静态内部类(static class Text)
*/
class XxxActivity extends AppCompatActivity {
private static Test test = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_xxx);
if(test == null){
test = new Test();
}
// ...
}
class Test {
// ...
}
}
- Handler引起的内存泄漏
java
/**
* 程序启动时,主线程会创建Looper对象,内部维护一个MessageQueue消息队列,按时间顺序存放Message
* 若Handler通过内部类创建,内部类持有外部类引用(Activity),而队列中的消息target指向Handler,即消息持有Handler引用
* 若Activity销毁时队列中的消息还未处理完,这些未处理完的消息会持有Activity引用,导致Activity无法回收
*
* 解决方案:Handler改为静态内部类,Activity#onDestroy()中移除队列中消息,Handler内弱引用Activity对象,让内部类不再持有外部类的引用时,程序就不允许Handler操作Activity对象
*/
class XxxActivity extends AppCompatActivity {
XxxHandler mHandler = new XxxHandler(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
mHandler.sendMessage(Message.obtain());
}
}).start();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 移除对应的Runnable或者是Message
// mHandler.removeCallbacks(runnable);
// mHandler.removeMessages(what);
mHandler.removeCallbacksAndMessages(null);
}
private static class XxxHandler extends Handler {
private WeakReference<Activity> mActivity;
public XxxHandler(Activity activity) {
mActivity = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
if (mActivity.get() == null || mActivity.isFinishing()) {
return;
}
// to do something..
}
}
}
- Asynctask引起的内存泄漏
和Handler类似,Asynctask内部类持有外部类引用,改为静态内部类并在onDestroy中取消任务即可
- WebView引起的内存泄漏
WebView在不同的版本上和不同的机型上会存在不同的问题
- 方式一
直接为WebView开辟一个进程,结束操作时,结束进程(System.exit(0)),进程间通讯可采用AIDL、Messager、ContentProvider、Broadcast、Intent等方式
- 方式二
不在xml中定义,定义一个view容器(例FrameLayout(layout)),在代码中动态添加(context可使用弱引用),onDestroy()销毁时直接layout.removeAllViews(),移除WebView
- 资源对象未关闭引起的内存泄漏
广播、服务、数据库cursor、多媒体、文件、套接字等不使用时需关闭
- 三方注册资源未取消注册(例EventBus);为便于管理Activity,将其添加到Activity栈中,销毁时未从栈中移除等