前言:
什么是内存泄漏? Android开发从GC root分析内存泄漏 - 简书 (jianshu.com)
如何发现内存泄漏:
一般从两个方面检查内存泄漏:
-- back键: 进入应用->一些操作 >back键退出应用到桌面
-- home键: 进入应用->一些操作 >home键退出应用到桌面
不断的重复上述操作, 结合LeakCanary和Profile 和adb shell dumpsys meminfo发现内存泄漏
内存泄漏的分类:
-- 显式内存泄漏: 能被检测工具发现的内存泄漏:
不断地操作back键, 一般可以被LeakCanary和Profile 检测到,
-- 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才被释放
可以通过 home键操作结合adb shell dumpsys meminfo发现内存不断增加
一, 检测工具
1.LeakCanary
如何使用: Getting Started - LeakCanary (square.github.io)
分析:
一系列操作后: 检查logcat
搜索关键字: Leaking: YES
如: Activity.shouldShowRequestPermissionRationale 导致的内存泄漏:
详情见: Android S原生系统内存泄露问题案例_reportfragment-CSDN博客
Leaking 4: shouldShowRequestPermissionRationale 导致:
GC Root: Global variable in native code
01-28 10:46:33.163 D/LeakCanary( 8100): ├─ android.app.AppOpsManager$4 instance
01-28 10:46:33.163 D/LeakCanary( 8100): │ Leaking: UNKNOWN
01-28 10:46:33.163 D/LeakCanary( 8100): │ Retaining 9.3 kB in 110 objects
01-28 10:46:33.163 D/LeakCanary( 8100): │ Anonymous subclass of com.android.internal.app.IAppOpsStartedCallback$Stub
01-28 10:46:33.163 D/LeakCanary( 8100): │ ↓ AppOpsManager$4.val$callback
01-28 10:46:33.163 D/LeakCanary( 8100): │ ~~~~~~~~~~~~
01-28 10:46:33.163 D/LeakCanary( 8100): ├─ android.permission.PermissionUsageHelper instance
01-28 10:46:33.163 D/LeakCanary( 8100): │ Leaking: UNKNOWN
01-28 10:46:33.171 D/LeakCanary( 8100): │ Retaining 126 B in 5 objects
01-28 10:46:33.171 D/LeakCanary( 8100): │ mContext instance of com.xxx.MainActivity with mDestroyed = true
01-28 10:46:33.171 D/LeakCanary( 8100): │ ↓ PermissionUsageHelper.mContext
01-28 10:46:33.171 D/LeakCanary( 8100): │ ~~~~~~~~
01-28 10:46:33.172 D/LeakCanary( 8100): ╰→ com.xxx.MainActivity instance
01-28 10:46:33.172 D/LeakCanary( 8100): Leaking: YES (ObjectWatcher was watching this because com.xxx.MainActivity received
01-28 10:46:33.172 D/LeakCanary( 8100): Activity#onDestroy() callback and Activity#mDestroyed is true)
01-28 10:46:33.172 D/LeakCanary( 8100): Retaining 9.1 MB in 7484 objects
01-28 10:46:33.172 D/LeakCanary( 8100): key = 0217d51a-25f2-49d8-aa59-32364d92a3b2
01-28 10:46:33.172 D/LeakCanary( 8100): watchDurationMillis = 5835
01-28 10:46:33.172 D/LeakCanary( 8100): retainedDurationMillis = 832
01-28 10:46:33.172 D/LeakCanary( 8100): mApplication instance of com.xxx.MyApplication
01-28 10:46:33.172 D/LeakCanary( 8100): mBase instance of android.app.ContextImpl
2. Profiler
如何使用: Android Studio 底部工具栏找到Profiler
-- 点击+ 选择你的应用进程:(确保应用在运行)
-- 一系列操作后 点击Memory, 选择Capture heap dump, 点击Recode按钮
-- dump完成后得到结果如图:
leaks 是泄漏的对象个数,双击可以查看具体对象, 以及该对象的引用链
3.adb shell dumpsys meminfo your pkg name -d
有的时候我们通过LeakCanary 和Profile 检查不出内存泄漏,但是应用的内存越用越大
针对这种情况我们可以通过以下步骤检查内存问题:
-- 反复操作: 进入应用->一些操作 -> adb shell dumpsys meminfo your pkg name -d ->退出应用到桌面(home键/back键)
-- 比较meminfo中 TOTAL PSS:的变化情况, 如果TOTAL PSS有明显递增, 则说明有内存没有被释放
-- 进一步通过 对比 Activities: Views 等 数量变化可以进一步定位泄漏点, 然后再通过Profiler工具检查具体的怀疑对象, 观察其是否随着操作有递增趋势
adb shell dumpsys meminfo your pkg name -d 打印信息
Applications Memory Usage (in Kilobytes):
Uptime: 186869 Realtime: 186869
** MEMINFO in pid 6984 [com.xxx.xxx.xxx] **
Pss Private Private Swap Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 50552 50540 0 0 52800 59240 58260 979
Dalvik Heap 38909 38804 0 0 46896 62423 37847 24576
Dalvik Other 5597 4092 0 0 8080
Stack 1844 1844 0 0 1852
Ashmem 998 0 0 0 2824
Other dev 86 0 84 0 384
.so mmap 5014 272 0 0 55336
.jar mmap 1989 0 4 0 32976
.apk mmap 26264 44 25320 0 29872
.ttf mmap 166 0 116 0 280
.dex mmap 126 96 16 0 556
.oat mmap 103 0 0 0 3760
.art mmap 4947 4808 0 0 14812
Other mmap 2166 4 448 0 7120
GL mtrack 16120 16120 0 0 16120
Unknown 463 456 0 0 924
TOTAL 155344 117080 25988 0 274592 121663 96107 25555
App Summary
Pss(KB) Rss(KB)
------ ------
Java Heap: 43612 61708
Native Heap: 50540 52800
Code: 25892 125872
Stack: 1844 1852
Graphics: 16120 16120
Private Other: 5060
System: 12276
Unknown: 16240
TOTAL PSS: 155344 TOTAL RSS: 274592 TOTAL SWAP (KB): 0
Objects
Views: 276 ViewRootImpl: 1
AppContexts: 7 Activities: 1
Assets: 28 AssetManagers: 0
Local Binders: 129 Proxy Binders: 48
Parcel memory: 25 Parcel count: 103
Death Recipients: 5 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 806
PAGECACHE_OVERFLOW: 315 MALLOC_SIZE: 46
DATABASES
pgsz dbsz Lookaside(b) cache Dbname
4 136 124 7/37/9 /data/user/0/com.xxx/no_backup/androidx.work.workdb
4 8 0/0/0 (attached) temp
4 136 32 2/15/3 /data/user/0/com.xxx/no_backup/androidx.work.workdb (2)
4 44 104 141/48/12 /data/user/0/com.xxx/databases/playqueue.db
4 36 123 67/24/10 /data/user/0/com.xxx/databases/metadata.db
4 56 112 10/33/7 /data/user/0/xxx/databases/picnic
4 56 54 1/16/2 /data/user/0/com.xxx/databases/picnic (1)
4 28 65 7/22/6 /data/user/0/com.xxx/databases/browser.db
二. 自动化测试脚本
有的时候需要测试几十次上百次才能发现明显的内存泄漏, 为了节省事件我们可以写一个自动化测试脚本, 来执行测试
以shell脚本为例:
#! /bin/bash
## KEYCODE_BACK= 4;
## KEYCODE_DPAD_UP= 19;
## KEYCODE_DPAD_DOWN= 20;
## KEYCODE_DPAD_LEFT= 21;
## KEYCODE_DPAD_RIGHT= 22;
## KEYCODE_DPAD_CENTER= 23;
## KEYCODE_ENTER= 66;
## KEYCODE_HOME= 3;
function press_key() {
echo "press key code:"$1
adb shell input keyevent $1
sleep 1
}
function dump_meminfo() {
echo "start dump meminfo"
adb shell dumpsys meminfo com.xxx.xxx -d >> meminfo.txt
}
function startplayer() {
echo "start player"
adb shell am start -n com.xxx/.MainActivity
}
function wait_sleep(){
echo "wait_sleep code:"$1
sleep $1
}
for((i=0;i<30;i++))
do
echo "run loop "$i startplayer
wait_sleep 4
press_key 23 #enter
wait_sleep 10
dump_meminfo
press_key 3 #home
done
三 常见的内存泄漏
1.非静态内部类/匿名内部类
由于非静态内部类/匿名内部类持有外部类的引用, 因此将非静态内部类/匿名内部类创建的对象(Object C)作为参数传递给其他对象(Object B)的时候, Object B 可能间接持有Object A的引用
这种情况下我们应该做到:
在本对象(Object A)生命周期结束前, 将Object B中持有的Object C 置空.
常见的有listener, runnable, handler 等...
例1: listener
我们创建listener 时通常使用匿名内部类来创建 :
ActivityA.javavoid onCreate() {
remoteService = AIDLService.getService()
remoteService.setListtener(new AIDLService.ServiceListener(){
@Override
void onListenerCallback() {
}
} );}
void onDestory(){
remoteService.setListtener(null);
}
AIDLService.java
AIDLService() {
ServiceListener listener;
public interface ServiceListener {
void onListenerCallback();
}
public void setListtener(ServiceListener listener) {
this.listener = listener;
}
}
例2: handler
使用静态内部类来创建handler, handler 可以持有外部类的弱引用, 避免内存泄漏
Activity 生命周期结束的时候清空handler的缓存消息: handler.removeCallbacksAndMessages(null);
public class MyActivity {
private static class MyHandler extends Handler {
private final WeakReference<MyActivity> mActivity;
public MyHandler(MyActivity activity) {
mActivity = new WeakReference<>(activity);
}
public void handleMessage(Message msg) {
MyActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private MyHandler handler = new MyHandler(this);
void postDelay(){
handler.postDelayed(runnable, delayedTime);
}
void onDestory(){
handler.removeCallbacksAndMessages(null);
}
}
2.静态变量
如果静态变量是Activity, Service,Fragment 类型的化, 如果Activity, Service,Fragment生命周期结束的时候, 静态变量持有的引用还指向这些对象, 就会导致这些对象不能被回收, 从而出现内存泄漏
例1: ToastUtil
如果 这样创建toast: toast = Toast.makeText(context, text, duration);
那么Toast将持有context 的引用, 如果context是Activity 类型, 由于静态变量的生命周期长于activty , 会引起Activity内存泄漏
因此避免内存泄漏要做到以下两点:
1.Activity onDestory 的时候将toast 置空, 调用cancel()
2.使用ApplicationContext 创建Toast
toast = Toast.makeText(context.getApplicationContext(), text,
duration);
public final class ToastUtil {
private static Toast toast;
private ToastUtil() {
// not called
}
public static void showToast(Context context, String text, int duration) {
makeText(context, text, duration).show();
}
public static void cancel() {
if (toast != null) {
toast.cancel();
// When toast canceled, this toast instance can not show again.
toast = null;
}
}
private static Toast makeText(Context context, String text, int duration) {
if (toast != null) {
toast.setText(text);
toast.setDuration(duration);
} else {
toast = Toast.makeText(context.getApplicationContext(), text,
duration);
}
return toast;
}
}
3.IO流要正确关闭 InputStream,OutputStream,
public void readFromFile() {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("file.txt");
// 读取数据
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
**4.资源未关闭造成的内存泄漏
**
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,,Cursor,SqliteDatabase, Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。
1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。
2)资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
4)Bitmap, 在Android系统3.0之前,它的内存一部分在虚拟机中,一部分在虚拟机外。因此它的一部分内存不参与垃圾回收,需要我们主动调用recycler()才能回收。3.0 以后的bitmap应该是不需要手动recycle了,内存已经在java层了。bitmap = null 就可以释放内存
5.AIDL 接口中的listener 导致的内存泄漏
曾经遇到一个问题, home键退出再进, heap 内存中 MyTask 对象的数量就会增加一个
原因是传递给AIDL 的 listener 是用匿名内部类构造的, 它持有外部类MyTask 的引用, 由于AIDL接口所在进程一直没有释放listener, 所以home键回来的时候, 会重新构造一个新的listener, 之前的listener并没有释放, 进而之前的 MyTask 对象也没有释放, 导致MyTask对象越来越多, 由于是第三方的AIDL, 所以我们没有办法控制, 只能把Listener 写成单例模式, 弱引用外部类, 这样的话, 尽管AIDL进程没有释放listener, 也能保证堆内存中至多有一个listener对象,
public class MyTask {
private IRemoteServiceListener mWrapperListener;
public MyTask() {
mWrapperListener = WrapperRemoteServiceListener.getInstance(new WeakReference<MyTask>(this));
}
@Override
public void run() {
super.run();
RemoteServiceWrapper.getService.doSomething(mWrapperListener);
}
static class WrapperRemoteServiceListener extends IRemoteServiceListener.Stub { WeakReference<MyTask> mTaskWeakReference;
private static final WrapperRemoteServiceListener SINGLE_INSTANCE = new WrapperRemoteServiceListener ();
private WrapperRemoteServiceListener () { }
public static WrapperRemoteServiceListener getInstance(WeakReference<MyTask> weakReference) { SINGLE_INSTANCE.mTaskWeakReference= weakReference; return SINGLE_INSTANCE;
}
@Override
public void onDoSomethingResult(int result) throws RemoteException {
}
}
}
6. 系统Api bug造成的内存泄漏
在Android S 上只要调用 Activity.shouldShowRequestPermissionRationale(p)
就会引起Activity 不能被回收, 导致内存泄漏:Android S原生系统内存泄露问题案例_reportfragment-CSDN博客
解决办法: 尽量不调用, 少调用Activity.shouldShowRequestPermissionRationale(p)
if (!context.checkSelfPermission(p) == PackageManager.PERMISSION_GRANTED) { mShouldShowRequestReadPermissionRationale =
mActivity.shouldShowRequestPermissionRationale(p);
} else {
mShouldShowRequestReadPermissionRationale = false;
}