一、问题背景
测试使用monkey工具,压力测试主题商店应用时,最后出现电话本内存泄露信息:
appCrashed_android.process.contacts_2016-12-30 00_34_36.txt
// CRASH: android.process.contacts (pid 5172)
// Short Msg: java.lang.OutOfMemoryError
刚开始看到这个问题,感觉一头雾水,为何测试主题商店应用,会导致电话本内存泄露呢? 这两个看起来八竿子打不着的应用, 会存在着怎样的纠葛,当时不知。
项目要继续,问题要解决。之前没有处理过OOM问题,只好查阅资料,询问同事协助处理。
二、问题分析
首先想确认问题的复现概率, 测试说基本上跑个两三个小时的monkey测试,就能复现,于是我自己也本地在尝试,果然能否复现。向导主题商店的测试内容主要为切换主题,于是就编写脚本,
压力测试切换主题,看能否使问题复现的时间缩短。
切换主题的脚本:
@echo off
adb remount
adb push 240699_Men.theme /sdcard/Themes/
pause
echo --------start to change themes-----
##循环切换主题, 1000次
FOR /l %%i IN (1,1,1000) DO (
adb shell am startservice -n com.nearme.themespace/com.nearme.themespace.services.ThemeApplyService --es THEME_PATH /sdcard/Themes/240699_Men.theme
##每次间隔8秒钟
ping -n 8 127.0.0.1>nul
echo %%i
)
pause
通过脚本运行,果然复现时间缩短了很多,很快就能复现。
接下来就是查找哪里出现的问题了,在网上找到一篇老外分析短信内存泄露的方法,分析中有如何使用脚本工具获取内存信息,很好用:
http://stevevallay.github.io/blog/2014/11/17/memory-leak/
文中说明为了获取内存信息,我们需要用到linux提供的
procrank
procmem
这两个工具,一般我们的手机中不会自带这两个工具,需要去编译的版本中查找拷贝过来使用。
具体项目工具适配方法:
1、由于procmem和 Procrank 工具在我们的手机中一般没有放置,因此在调试各个项目时需要到各个项目编译的out下面的去找到对应的工具以及so库拷贝到目录中进行替换使用,
目录根据机器是32位还是 64位 有所不同:
out\target\product\msm8952_64\system\lib
out\target\product\msm8952_64\system\lib64
2、bat脚本也需要根据32位和64位push so库到对应目录, shell脚本需要根据需要调试的应用进程进行修改:
放置工具到手机中的bat脚本如下:
adb remount
adb shell rm -rf /sdcard/mem_info_log/
adb push libpagemap.so /system/lib64
adb push procrank /system/xbin
adb push procmem /system/xbin
adb shell chmod 777 /system/xbin/procrank
adb shell chmod 777 /system/xbin/procmem
pause
###放置到手机后台运行导出内存信息的shell脚本
adb push log.sh /system/bin
adb shell chmod 777 /system/bin/log.sh
adb shell sh /system/bin/log.sh &
pause
在手机中运行的shell脚本文件参照老外的写法如下:
#!/system/bin/sh
set `ps| grep android.process.contacts`
pidOfMms=$2
echo "pid of contacts is: $pidOfMms"
set `ps|grep com.nearme.themespace`
pidOfPhone=$2
echo "pid of themespace is: $pidOfPhone"
if [ ! -d "/sdcard/mem_info_log" ]; then
mkdir "/sdcard/mem_info_log"
echo "create new dir /sdcard/mem_info_log"
fi
while [ -d "/sdcard/mem_info_log" ]
do
echo "start capture mem_info_log"
t=`date +"%Y-%m-%d-%H-%M-%S"`
echo $$ > /sdcard/mem_info_log/pid.log
procrank > "/sdcard/mem_info_log/procrank"$t".log"
procmem -p $pidOfMms > "/sdcard/mem_info_log/procmem.contacts"$t".log"
procmem -p $pidOfPhone > "/sdcard/mem_info_log/procmem.themespace"$t".log"
dumpsys meminfo android.process.contacts > "/sdcard/mem_info_log/meminfo.contacts"$t".log"
dumpsys meminfo com.nearme.themespace > "/sdcard/mem_info_log/meminfo.themespace"$t".log"
cat /proc/$pidOfPhone/maps > "/sdcard/mem_info_log/maps.themespace"$t".log"
cat /proc/$pidOfMms/maps > "/sdcard/mem_info_log/maps.contacts"$t".log"
sleep 300
done
脚本中每隔五分钟会使用
procrank
procmem
工具分别导出手机在压力测试过程中当前的内存信息,形成一个个文本放置在手机的一个目录下面,便于后续分析。
其中最终看到的有用信息在memInfo中:
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 4160 3992 0 0 7936 6563 1372
Dalvik Heap 13589 13464 0 0 23033 22945 88
Dalvik Other 635 632 0 0
Stack 212 212 0 0
Ashmem 6 4 0 0
Other dev 4 0 4 0
.so mmap 797 80 28 0
.apk mmap 479 0 116 0
.ttf mmap 11 0 0 0
.dex mmap 3644 8 3632 0
.oat mmap 2844 0 796 0
.art mmap 1716 940 0 0
Other mmap 96 8 16 0
Unknown 110 104 0 0
TOTAL 28303 19444 4592 0 30969 29508 1460
App Summary
Pss(KB)
------
Java Heap: 14404
Native Heap: 3992
Code: 4660
Stack: 212
Graphics: 0
Private Other: 768
System: 4267
TOTAL: 28303 TOTAL SWAP (KB): 0
Objects
Views: 389 ViewRootImpl: 1
AppContexts: 7 Activities: 4
Assets: 3 AssetManagers: 2
Local Binders: 62 Proxy Binders: 27
Parcel memory: 9 Parcel count: 36
Death Recipients: 0 OpenSSL Sockets: 0
SQL
MEMORY_USED: 0
PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
看到了Activities的应用在持续增长,这样就能够确认应用确实存在OOM了,于是开始查对应Activity里面的代码。
最后发现在一个activity中存在了注册观察者处理,但是没有注销,其注册方式为:
getActivity().getContentResolver().registerContentObserver
问题的巧合之处就是原本正常的activity的生命周期其实每次就一次onCreate, 注册的调用是在这个方法中,如果是反复直接进入这个activity, 最多也就有一次引用,
因为再次进入是直接运行onstart的生命周期,不会在走入onCreate中了,所以如果反复测试电话本应用本身,此问题复现不了,反而是由于切换主题后,导致电话本又重新运行了
onCreate方法,这样每次都会运行注册,增加引用次数,导致后面出现引用过多,activity迟迟不能回收后导致OOM, 使用脚本观察,发现一般在15次左右就会出现OOM, 此时
电话本应用的内存已经接近120M, 这应该也是系统分配给一个应用的最大内存使用数值。
三、问题解决
当然发现问题原因后,解决起来就比较容易了,在onDestroy中增加注销即可:
getActivity().getContentResolver().unregisterContentObserver
随后再网上查阅了一下常用的OOM问题:
https://juejin.im/entry/5762b1d7816dfa00544680a0
我这个问题就是属于
监听器没有注销造成的内存泄漏
所以注册和注销一定要记得成对使用。
问题过后又细想了一下为何这种注册没有注销会导致OOM, 于是猜测这种监听器的底层实现应该是使用了静态static 列表,将每个注册传递过来的activity或者其他参数
放入静态列表中,所以会导致无法回收。 并不是所有的用getActivity()作为参数传递过去之后都会导致OOM,比如初始化中获取资源文件,有时候也会使用这个方法传递参数
作为context, 这种就是没有问题的:
FeatureOption.init(getActivity());
不要风声鹤唳,以后所有的地方都不敢获取activity 传递参数了。