ANR

  • 主线程 (“事件处理线程” / “UI线程”) 在5秒内没有响应输入事件;
  • BroadcastReceiver 没有在10秒内完成返回;
  • Service没有在20秒内完成

获取日志 adb pull /data/anr ./

手动生成traces.txt文件
进入Android的shell命令行:
adb shell
得到app pid
$ps
发送信号
$kill -3 target_app_pid

$adb pull /data/anr ./

转-http://rayleeya.iteye.com/blog/1955657

1.2 如何分析ANR问题

引起ANR问题的根本原因,总的来说可以归纳为两类:

  • 应用进程自身引起的,例如:

主线程阻塞、挂起、死循环

应用进程的其他线程的CPU占用率高,使得主线程无法抢占到CPU时间片

  • 其他进程间接引起的,例如:

当前应用进程进行进程间通信请求其他进程,其他进程的操作长时间没有反馈

其他进程的CPU占用率高,使得当前应用进程无法抢占到CPU时间片

分析ANR问题时,以上述可能的几种原因为线索,通过分析各种日志信息,大多数情况下你就可以很容易找到问题所在了。

注意:我很诚恳的向读者说明,确实有一些ANR问题很难调查清楚,因为整个系统不稳定的因素很多,例如Linux Kernel本身的bug引起的内存碎片过多、硬件损坏等。这类比较底层的原因引起的ANR问题往往无从查起,并且这根本不是应用程序的问题,浪费了应用开发人员很多时间,如果你从事过整个系统的开发和维护工作的话会深有体会。所以我不能保证了解了本章的所有内容后能够解决一切ANR问题,如果出现了很疑难的ANR问题,我建议最好去和做驱动和内核的朋友聊聊,或者,如果问题只是个十万分之一的偶然现象,不影响程序的正常运行,我倒是建议不去理它。

1.2.1 分析ANR的利器

Android会在ANR发生时输出很多有用的信息帮助分析问题原因,我们先来看一下ANR的异常信息,使用logcat命令查看会得到类似如下的log:

//WindowManager所在的进程是system_server,进程号是127

I/WindowManager( 127): Input event dispatching timed out sending to com.example.anrdemo/com.example.anrdemo.ANRActivity

//system_server进程中的ActivityManagerService请求kernel向5033进程发送SIGNAL_QUIT请求

//你可以在shell中使用命令达到相同的目的:adb shell kill -3 5033

//和其他的Java虚拟机一样,SIGNAL_QUIT也是Dalvik内部支持的功能之一

I/Process ( 127): Sending signal. PID: 5033 SIG: 3

//5033进程的虚拟机实例接收到SIGNAL_QUIT信号后会将进程中各个线程的函数堆栈信息输出到traces.txt文件中

//发生ANR的进程正常情况下会第一个输出

I/dalvikvm( 5033): threadid=4: reacting to signal 3

I/dalvikvm( 5033): Wrote stack traces to ‘/data/anr/traces.txt’

… …//另外还有其他一些进程

//随后会输出CPU使用情况

E/ActivityManager( 127): ANR in com.example.anrdemo (com.example.anrdemo/.ANRActivity)

//Reason表示导致ANR问题的直接原因

E/ActivityManager( 127): Reason: keyDispatchingTimedOut

E/ActivityManager( 127): Load: 3.85 / 3.41 / 3.16

//请注意ago,表示ANR发生之前的一段时间内的CPU使用率,并不是某一时刻的值

E/ActivityManager( 127): CPU usage from 26835ms to 3662ms ago with 99% awake:

E/ActivityManager( 127): 9.4% 98/mediaserver: 9.4% user + 0% kernel

E/ActivityManager( 127): 8.9% 127/system_server: 6.9% user + 2% kernel / faults: 1823 minor

… …

E/ActivityManager( 127): +0% 5033/com.example.anrdemo: 0% user + 0% kernel

E/ActivityManager( 127): 39% TOTAL: 32% user + 6.1% kernel

//这里是later,表示ANR发生之后

E/ActivityManager( 127): CPU usage from 601ms to 1132ms later with 99% awake:

E/ActivityManager( 127): 10% 127/system_server: 1.7% user + 8.9% kernel / faults: 5 minor

E/ActivityManager( 127): 10% 163/InputDispatcher: 1.7% user + 8.9% kernel

E/ActivityManager( 127): 1.7% 127/system_server: 1.7% user + 0% kernel

E/ActivityManager( 127): 1.7% 135/SurfaceFlinger: 0% user + 1.7% kernel

E/ActivityManager( 127): 1.7% 2814/Binder Thread #: 1.7% user + 0% kernel

… …

E/ActivityManager( 127): 37% TOTAL: 27% user + 9.2% kernel

从log中能够知道,发生ANR时Android为我们提供了两种“利器”:traces文件和CPU使用率。以上做了简单注释,不过稍后再详细分析它们。

1.2.2 ANR信息是如何输出的

我们再来看看这些log是怎样被输出的,这很重要,知其然,也要知其所以然。代码在ActivityManagerService类中,找到它的appNotResponding函数。

final void appNotResponding(ProcessRecord app, ActivityRecord activity,

ActivityRecord parent, final String annotation) {

//firstPids和lastPids两个集合存放那些将会在traces中输出信息的进程的进程号

ArrayList<Integer> firstPids = new ArrayList<Integer>(5);

SparseArray<Boolean> lastPids = new SparseArray<Boolean>(20);

//mController是IActivityController接口的实例,是为Monkey测试程序预留的,默认为null

if (mController != null) {

… …

}

long anrTime = SystemClock.uptimeMillis();

if (MONITOR_CPU_USAGE) {

updateCpuStatsNow(); //更新CPU使用率

}

synchronized (this) { //一些特定条件下会忽略ANR

if (mShuttingDown) {

Slog.i(TAG, “During shutdown skipping ANR: ” + app + ” ” + annotation);

return;

} else if (app.notResponding) {

Slog.i(TAG, “Skipping duplicate ANR: ” + app + ” ” + annotation);

return;

} else if (app.crashing) {

Slog.i(TAG, “Crashing app skipping ANR: ” + app + ” ” + annotation);

return;

}

//使用一个标志变量避免同一个应用在没有处理完时重复输出log

app.notResponding = true;

… …

//①当前发生ANR的应用进程被第一个添加进firstPids集合中

firstPids.add(app.pid);

… …

}

… …

//②dumpStackTraces是输出traces文件的函数

File tracesFile = dumpStackTraces(true, firstPids, processStats, lastPids, null);

String cpuInfo = null;

if (MONITOR_CPU_USAGE) { //MONITOR_CPU_USAGE默认为true

updateCpuStatsNow(); //再次更新CPU信息

synchronized (mProcessStatsThread) {

//输出ANR发生前一段时间内的CPU使用率

cpuInfo = mProcessStats.printCurrentState(anrTime);

}

info.append(processStats.printCurrentLoad());

info.append(cpuInfo);

}

//输出ANR发生后一段时间内的CPU使用率

info.append(processStats.printCurrentState(anrTime));

… …

//③将ANR信息同时输出到DropBox中

addErrorToDropBox(“anr”, app, app.processName, activity, parent, annotation,

cpuInfo, tracesFile, null);

… …

//在Android4.0中可以设置是否不显示ANR提示对话框,如果设置的话就不会显示对话框,并且会杀掉ANR进程

boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),

Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;

synchronized (this) {

if (!showBackground && !app.isInterestingToUserLocked() && app.pid != MY_PID) {

… …

Process.killProcessQuiet(app.pid);

return;

}

… …

// 显示ANR提示对话框

Message msg = Message.obtain();

HashMap map = new HashMap();

msg.what = SHOW_NOT_RESPONDING_MSG;

msg.obj = map;

map.put(“app”, app);

if (activity != null) {

map.put(“activity”, activity);

}

mHandler.sendMessage(msg);

}

}

有三个关键点需要注意:

① 当前发生ANR的应用进程被第一个添加进firstPids集合中,所以会第一个向traces文件中写入信息。反过来说,traces文件中出现的第一个进程正常情况下就是发生ANR的那个进程。不过有时候会很不凑巧,发生ANR的进程还没有来得及输出trace信息,就由于某种原因退出了,所以偶尔会遇到traces文件中找不到发生ANR的进程信息的情况。

② dumpStackTraces是输出traces文件的函数,接下来分析这个函数

③ addErrorToDropBox函数将ANR信息同时输出到DropBox中,它也是个非常有用的日志存放工具,后面也会分析它的作用。

public static File dumpStackTraces(boolean clearTraces, ArrayList<Integer> firstPids,

ProcessStats processStats, SparseArray<Boolean> lastPids, String[] nativeProcs) {

//系统属性“dalvik.vm.stack-trace-file”用来配置trace信息输出文件

String tracesPath = SystemProperties.get(“dalvik.vm.stack-trace-file”, null);

if (tracesPath == null || tracesPath.length() == 0) {

return null;

}

File tracesFile = new File(tracesPath);

try {

File tracesDir = tracesFile.getParentFile();

if (!tracesDir.exists()) tracesFile.mkdirs();

//FileUtils.setPermissions是个很有用的函数,设置文件属性时经常会用到

FileUtils.setPermissions(tracesDir.getPath(), 0775, -1, -1); // drwxrwxr-x

//clearTraces为true,会删除旧文件,创建新文件

if (clearTraces && tracesFile.exists()) tracesFile.delete();

tracesFile.createNewFile();

FileUtils.setPermissions(tracesFile.getPath(), 0666, -1, -1); // -rw-rw-rw-

} catch (IOException e) {

Slog.w(TAG, “Unable to prepare ANR traces file: ” + tracesPath, e);

return null;

}

//一个重载函数

dumpStackTraces(tracesPath, firstPids, processStats, lastPids, nativeProcs);

return tracesFile;

}

有两个关键点需要注意:

① 聪明的你肯定已经知道,之所以trace信息会输出到“/data/anr/traces.txt”文件中,就是系统属性“dalvik.vm.stack-trace-file”设置的。你可以通过在设备的shell中使用setprop和getprop对系统属性进行设置和读取:

getpropdalvik.vm.stack-trace-file

setprop dalvik.vm.stack-trace-file /tmp/stack-traces.txt

② 每次发生ANR时都会删除旧的traces文件,重新创建新文件。也就是说Android只保留最后一次发生ANR时的traces信息,那么以前的traces信息就丢失了么?稍后回答。

接着来看重载的dumpStackTraces函数。

private static void dumpStackTraces(String tracesPath, ArrayList<Integer> firstPids,

ProcessStats processStats, SparseArray<Boolean> lastPids, String[] nativeProcs) {

//使用FileObserver监听应用进程是否已经完成写入traces文件的操作

//Android在判断桌面壁纸文件是否设置完成时也是用的FileObserver,很有用的类

FileObserver observer = new FileObserver(tracesPath, FileObserver.CLOSE_WRITE) {

public synchronized void onEvent(int event, String path) { notify(); }

};

… …

//首先输出firstPids集合中指定的进程,这些也是对ANR问题来说最重要的进程

if (firstPids != null) {

try {

int num = firstPids.size();

for (int i = 0; i < num; i++) {

synchronized (observer) {

//前面提到的SIGNAL_QUIT

Process.sendSignal(firstPids.get(i), Process.SIGNAL_QUIT);

observer.wait(200);

… …

}

提示:如果你在解决其他问题时也需要查看Java进程中各个线程的函数堆栈信息,就可以使用向目标进程发送SIGNAL_QUIT(3)这个技巧。其实这个名称有点误导,它并不会让进程真正退出。

1.2.3 DropBox

刚才留了一个问题:Android只保留最后一次发生ANR时的traces信息,那么以前的traces信息就丢失了么?为了增强Android的异常信息收集管理能力,从2.2开始增加了DropBox功能。

DropBox(简称DB)是系统进程中的一个服务,在system_server进程启动时创建,并且它没有运行在单独的线程中,而是运行在system_server的ServerThread线程中。我们可以将ServerThread称作system_server的主线程,ServerThread线程除了启动并维护各个服务外,还负责检测一些重要的服务是否死锁,这一点到后面的Watchdog部分再分析<!– Watchdog写完后注意补充章节号 –>。DB被创建的代码如下。

SystemServer.java → ServerThread.run()

Slog.i(TAG, “DropBox Service”);

ServiceManager.addService(Context.DROPBOX_SERVICE, //服务名称为“dropbox”

new DropBoxManagerService(context, new File(“/data/system/dropbox”)));

“/data/system/dropbox”是DB指定的文件存放位置。下面来看一下DB服务的主要功能。

1. DropBoxManagerService

DropBoxManagerService(简称DBMS)就是DB服务的本尊,它的主要功能接口包括以下几个函数:

public voidadd(DropBoxManager.Entryentry)

DBMS将所有要添加的日志都用DropBoxManager.Entry类型的对象表示,通过add函数添加,并且直到目前为止一个Entry对象对应着一个日志文件。

publicboolean isTagEnabled(String tag)

通过给每一个Entry设置一个tag可以标识不同类型的日志,并且可以灵活的启用/禁用某种类型的日志,isTagEnabled用来判断指定类型的日志是否被启用/禁用了,一旦禁用就不会再记录这种类型的日志。默认是不禁用任何类型的日志的。稍后说明如何启用/禁用日志。

publicsynchronized DropBoxManager.Entry getNextEntry(String tag, long millis)

我们可以通过getNextEntry函数获取指定类型和指定时间点之后的第一条日志,要使用这个功能应用程序需要有“android.permission.READ_LOGS”的权限,并且在使用完毕返回的Entry对象后要调用其close函数确保关闭日志文件的文件描述符(如果不关闭的话可能造成进程打开的文件描述符超过1024而崩溃,Android中限制每个进程的文件描述符上限为1024)。

DBMS提供了很多的配置项用来限制对磁盘的使用,通过SettingsProvider应用程序维护,数据存放在其settings.db数据库中。这些配置项也都有默认值,罗列如下:

  • Settings.Secure.DROPBOX_AGE_SECONDS = “dropbox_age_seconds”

日志文件保存的最长时间,默认3天

  • Settings.Secure.DROPBOX_MAX_FILES = “dropbox_max_files”

日志文件的最大数量,默认值是1000

  • Settings.Secure.DROPBOX_QUOTA_KB = “dropbox_quota_kb”

磁盘空间最大使用量

  • Settings.Secure.DROPBOX_QUOTA_PERCENT = “dropbox_quota_percent”

  • Settings.Secure.DROPBOX_RESERVE_PERCENT = “dropbox_reserve_percent”

  • Settings.Secure.DROPBOX_TAG_PREFIX = “dropbox:”

//应用程序可以利用DropBox来做事情,收集日志等

1.2.4 traces.txt

终于到大明星出场的时候了,一起来看一下traces.txt的庐山真面目。以下是笔者写的一个演示程序制造出的一次ANR的trace信息:

//文件中输出的第一个进程的trace信息,正是发生ANR的演示程序

//开头显示进程号、ANR发生的时间点和进程名称

—– pid 9183 at 2012-09-28 22:20:42 —–

Cmd line: com.example.anrdemo

DALVIK THREADS: //以下是各个线程的函数堆栈信息

//mutexes表示虚拟机实例中各种线程相关对象锁的value值

(mutexes: tll=0 tsl=0 tscl=0 ghl=0 hwl=0 hwll=0)

//依次是:线程名、线程优先级、线程创建时的序号、①线程当前状态

“main” prio=5 tid=1 TIMED_WAIT

//依次是:线程组名称、suspendCount、debugSuspendCount、线程的Java对象地址、线程的Native对象地址

| group=”main” sCount=1 dsCount=0 obj=0x4025b1b8 self=0xce68

//sysTid是线程号,主线程的线程号和进程号相同

| sysTid=9183 nice=0 sched=0/0 cgrp=default handle=-1345002368

| schedstat=( 140838632 210998525 213 )

at java.lang.VMThread.sleep(Native Method)

at java.lang.Thread.sleep(Thread.java:1213)

at java.lang.Thread.sleep(Thread.java:1195)

at com.example.anrdemo.ANRActivity.makeANR(ANRActivity.java:44)

at com.example.anrdemo.ANRActivity.onClick(ANRActivity.java:38)

at android.view.View.performClick(View.java:2486)

at android.view.View$PerformClick.run(View.java:9130)

at android.os.Handler.handleCallback(Handler.java:587)

at android.os.Handler.dispatchMessage(Handler.java:92)

at android.os.Looper.loop(Looper.java:130)

at android.app.ActivityThread.main(ActivityThread.java:3703)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:507)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599)

at dalvik.system.NativeStart.main(Native Method)

//②Binder线程是进程的线程池中用来处理binder请求的线程

“Binder Thread #2” prio=5 tid=8 NATIVE

| group=”main” sCount=1 dsCount=0 obj=0x40750b90 self=0x1440b8

| sysTid=9190 nice=0 sched=0/0 cgrp=default handle=1476256

| schedstat=( 915528 18463135 4 )

at dalvik.system.NativeStart.run(Native Method)

“Binder Thread #1” prio=5 tid=7 NATIVE

| group=”main” sCount=1 dsCount=0 obj=0x4074f848 self=0x78d40

| sysTid=9189 nice=0 sched=0/0 cgrp=default handle=1308088

| schedstat=( 3509523 25543212 10 )

at dalvik.system.NativeStart.run(Native Method)

//线程名称后面标识有daemon,说明这是个守护线程

“Compiler” daemon prio=5 tid=6 VMWAIT

| group=”system” sCount=1 dsCount=0 obj=0x4074b928 self=0x141e78

| sysTid=9188 nice=0 sched=0/0 cgrp=default handle=1506000

| schedstat=( 21606438 21636964 101 )

at dalvik.system.NativeStart.run(Native Method)

//JDWP线程是支持虚拟机调试的线程,不需要关心

“JDWP” daemon prio=5 tid=5 VMWAIT

| group=”system” sCount=1 dsCount=0 obj=0x4074b878 self=0x16c958

| sysTid=9187 nice=0 sched=0/0 cgrp=default handle=1510224

| schedstat=( 366211 2807617 7 )

at dalvik.system.NativeStart.run(Native Method)

//“Signal Catcher”负责接收和处理kernel发送的各种信号,例如SIGNAL_QUIT、SIGNAL_USR1等就是被该线程

//接收到,这个文件的内容就是由该线程负责输出的,可以看到它的状态是RUNNABLE,不过此线程也不需要关心

“Signal Catcher” daemon prio=5 tid=4 RUNNABLE

| group=”system” sCount=0 dsCount=0 obj=0x4074b7b8 self=0x150008

| sysTid=9186 nice=0 sched=0/0 cgrp=default handle=1501664

| schedstat=( 1708985 6286621 9 )

at dalvik.system.NativeStart.run(Native Method)

“GC” daemon prio=5 tid=3 VMWAIT

| group=”system” sCount=1 dsCount=0 obj=0x4074b710 self=0x168010

| sysTid=9185 nice=0 sched=0/0 cgrp=default handle=1503184

| schedstat=( 305176 4821778 2 )

at dalvik.system.NativeStart.run(Native Method)

“HeapWorker” daemon prio=5 tid=2 VMWAIT

| group=”system” sCount=1 dsCount=0 obj=0x4074b658 self=0x16a080

| sysTid=9184 nice=0 sched=0/0 cgrp=default handle=550856

| schedstat=( 33691407 26336669 15 )

at dalvik.system.NativeStart.run(Native Method)

—– end 9183 —–

—– pid 127 at 2012-09-28 22:20:42 —–

Cmd line: system_server

… …

//省略其他进程的信息

有一个关键点需要注意:

➀ 线程有很多状态,了解这些状态的意义对分析ANR的原因是有帮助的,总结如下:

Thread.java中定义的状态

Thread.cpp中定义的状态

说明

TERMINATED

ZOMBIE

线程死亡,终止运行

RUNNABLE

RUNNING/RUNNABLE

线程可运行或正在运行

TIMED_WAITING

TIMED_WAIT

执行了带有超时参数的wait、sleep或join函数

BLOCKED

MONITOR

线程阻塞,等待获取对象锁

WAITING

WAIT

执行了无超时参数的wait函数

NEW

INITIALIZING

新建,正在初始化,为其分配资源

NEW

STARTING

新建,正在启动

RUNNABLE

NATIVE

正在执行JNI本地函数

WAITING

VMWAIT

正在等待VM资源

RUNNABLE

SUSPENDED

线程暂停,通常是由于GC或debug被暂停

UNKNOWN

未知状态

Thread.java中的状态和Thread.cpp中的状态是有对应关系的。可以看到前者更加概括,也比较容易理解,面向Java的使用者;而后者更详细,面向虚拟机内部的环境。traces.txt中显示的线程状态都是Thread.cpp中定义的。另外,所有的线程都是遵循POSIX标准的本地线程。关于线程更多的说明可以查阅源码/dalvik/vm/Thread.cpp中的说明。<!– 线程的ThreadGroup最好也写进去 –>

traces.txt文件中的这些信息是由每个Dalvik进程的SignalCatcher线程输出的,相关代码可以查看/dalvik/vm/目录下的SignalCatcher.cpp::logThreadStacks函数和Thread.cpp:: dvmDumpAllThreadsEx函数。另外请注意,输出堆栈信息时SignalCatcher会暂停所有线程。

通过该文件很容易就能知道问题进程的主线程发生ANR时正在执行怎样的操作。例如上述示例, ANRActivity在makeANR函数中执行线程sleep时发生ANR,可以推测sleep时间过长,超过了超时上限导致。这是一种比较简单的情况,实际开发中会遇到很多诡异的、更加复杂的情况,在后面的实例讲解一节会详细说明。

1.2.5 CPU使用率

这部分的内容主要涉及到Linux的相关知识,数据是从“/proc/stat”文件中读取的,Android中仅仅是汇总和记录这些数据而已,熟悉Linux的读者可以跳过本节内容。

前面简单说明了CPU使用率信息,我们回顾一下,这次会有更多的知识点要说明。

E/ActivityManager( 127): ANR in com.example.anrdemo (com.example.anrdemo/.ANRActivity)

E/ActivityManager( 127): Reason: keyDispatchingTimedOut

E/ActivityManager( 127): Load: 3.85 / 3.41 / 3.16 //➀ CPU平均负载

//②ANR发生之前的一段时间内的CPU使用率

E/ActivityManager( 127): CPU usage from 26835ms to 3662ms ago with 99% awake://③

E/ActivityManager( 127): 9.4% 98/mediaserver: 9.4% user + 0% kernel

E/ActivityManager( 127): 8.9% 127/system_server: 6.9% user + 2% kernel / faults: 1823 minor //⑤ minor或者major的页错误次数

… …

E/ActivityManager( 127)://⑥+0% 5033/com.example.anrdemo: 0% user + 0% kernel

E/ActivityManager( 127): 39% TOTAL: 32% user + 6.1% kernel

//⑦ANR发生之后的一段时间内的CPU使用率

E/ActivityManager( 127): CPU usage from 601ms to 1132ms later with 99% awake:

E/ActivityManager( 127): 10% 127/system_server: 1.7% user + 8.9% kernel / faults: 5 minor

E/ActivityManager( 127): 10% 163/InputDispatcher: 1.7% user + 8.9% kernel

E/ActivityManager( 127): 1.7% 127/system_server: 1.7% user + 0% kernel

E/ActivityManager( 127): 1.7% 135/SurfaceFlinger: 0% user + 1.7% kernel

E/ActivityManager( 127): 1.7% 2814/Binder Thread #: 1.7% user + 0% kernel

… …

E/ActivityManager( 127): 37% TOTAL: 27% user + 9.2% kernel

以上信息其实包含了两个概念:CPU负载和CPU使用率,它们是不同的。不过负载的概念主要是做大型服务器端应用时关注的性能指标,在Android开发中我们更加关注的是使用率。下面详细说明,有八个关键点需要注意:

① CPU负载/平均负载

CPU负载是指某一时刻系统中运行队列长度之和加上当前正在CPU上运行的进程数,而CPU平均负载可以理解为一段时间内正在使用和等待使用CPU的活动进程的平均数量。在Linux中“活动进程”是指当前状态为运行或不可中断阻塞的进程。通常所说的负载其实就是指平均负载。

用一个从网上看到的很生动的例子来说明(不考虑CPU时间片的限制),把设备中的一个单核CPU比作一个电话亭,把进程比作正在使用和等待使用电话的人,假如有一个人正在打电话,有三个人在排队等待,此刻电话亭的负载就是4。使用中会不断的有人打完电话离开,也会不断的有其他人排队等待,为了得到一个有参考价值的负载值,可以规定每隔5秒记录一下电话亭的负载,并将某一时刻之前的一分钟、五分钟、十五分钟的的负载情况分别求平均值,最终就得到了三个时段的平均负载。

实际上我们通常关心的就是在某一时刻的前一分钟、五分钟、十五分钟的CPU平均负载,例如以上日志中这三个值分别是3.85、3.41、3.16,说明前一分钟内正在使用和等待使用CPU的活动进程平均有3.85个,依此类推。在大型服务器端应用中主要关注的是第五分钟和第十五分钟的两个值,但是Android主要应用在便携手持设备中,有特殊的软硬件环境和应用场景,短时间内的系统的较高负载就有可能造成ANR,所以笔者认为一分钟内的平均负载相对来说更具有参考价值。

CPU的负载和使用率没有必然关系,有可能只有一个进程在使用CPU,但执行的是复杂的操作;也有可能等待和正在使用CPU的进程很多,但每个进程执行的都是简单操作。

实际处理问题时偶尔会遇到由于平均负载高引起的ANR,典型的特征就是系统中应用进程数量多,CPU总使用率较高,但是每个进程的CPU使用率不高,当前应用进程主线程没有异常阻塞,一分钟内的CPU平均负载较高。

提示:Linux内核不断进行着CPU负载的记录,我们可以在任意时刻通过在shell中执行“cat /proc/loadavg”查看。

② ANR发生之前和之后一段时间的CPU使用率

CPU使用率可以理解为一段时间(记作T)内除CPU空闲时间(记作I)之外的时间与这段时间T的比值,用公式表示可以写为:

CPU使用率= (T – I) / T

而时间段T是两个采样时间点的时间差值。

之所以可以这样计算,是因为Linux内核会把从系统启动开始到当前时刻CPU活动的所有时间信息都记录下来,我们可以通过查看“/proc/stat”文件获取这些信息。主要包括以下几种时间,这些时间都是从系统启动开始计算的,单位都是0.01秒:

user: CPU在用户态的运行时间,不包括nice值为负数的进程运行的时间

nice: CPU在用户态并且nice值为负数的进程运行的时间

system:CPU在内核态运行的时间

idle: CPU空闲时间,不包括iowait时间

iowait: CPU等待I/O操作的时间

irq: CPU硬中断的时间

softirq:CPU软中断的时间

注意:随着Linux内核版本的不同,包含的时间类型有可能不同,例如2.6.11中增加的stealstolen等。但根据Android源码,我们只需要关心以上七种类型即可。

CPU使用率的计算是在ProcessStats类中实现的。如果在某两个时刻T1和T2(T1 < T2)进行采样记录,CPU使用率的整个算法可以归纳为以下几个公式:

userTime = (user2 + nice2) – (user1 + nice1)

systemTime = system2 – system1

idleTime = idle2 – idle1

iowaitTime = iowait2 – iowait1

irqTime = irq2 – irq1

softirqTime = softirq2 – softirq1

TotalTime = userTime + systemTime + idleTime + iowaitTime + irqTime + softirqTime

有了以上数据就可以计算具体的使用率了,例如用户态CPU使用率为:

userCpuUsage = userTime / TotalTime

依此类推可以计算其他类型的使用率。而整个时间段内CPU使用率为:

CpuUsage = (TotalTime – idleTime) / TotalTime

以上计算的是整个系统的CPU使用率,对于指定进程的使用率是通过读取该进程的“/proc/进程号/stat”文件计算的,而对于指定进程的指定线程的使用率是通过读取该线程的“/proc/进程号/task/线程号/stat”文件计算的。进程和线程的CPU使用率只包含该进程或线程的总使用率、用户态使用率和内核态使用率。

AMS在执行appNotResponding函数过程中,共输出了两个时间段的CPU使用率,通常情况下在ANR发生时间点之前和之后各有一段。两段使用率都是两次调用ProcessStats对象的update函数,每次调用update函数时会保留上一时间点的采样数据,并记录当前时间点的采样数据。然后再调用ProcessStats对象的printCurrentState函数,按照上述公式归纳的算法计算CPU使用率,并输出最终的结果。再详细看一下代码:

ActivityManagerService.java → appNotResponding

//第一次使用成员变量mProcessStats采样

if (MONITOR_CPU_USAGE) {

updateCpuStatsNow();

}

… …

//声明了一个局部变量,参数true表示包括线程信息

final ProcessStats processStats = new ProcessStats(true);

//将processStats作为实参传入,在dumpStackTraces中相隔500毫秒两次调用其update函数进行采样

File tracesFile = dumpStackTraces(true, firstPids, processStats, lastPids);

String cpuInfo = null;

if (MONITOR_CPU_USAGE) {

//因为在第一次调用后,可能由于输出trace信息等操作,中间执行了较长的时间,所以有第二次使用成员变量

//mProcessStats采样,尽量使得采样时间点靠后。

//此函数中要求连续两次采样时间间隔不少于5秒,所以一般不会执行第二次采样。一旦执行,就会出现两个采样

//时间点一个在ANR发生之前,另一个在其之后,或者两个时间点都在ANR发生之后的情况。

updateCpuStatsNow();

synchronized (mProcessStatsThread) {

//mProcessStats是成员变量,创建对象时传入的参数是false,所以不包括线程信息

//此处先输出ANR发生之前一段时间内的CPU使用率

cpuInfo = mProcessStats.printCurrentState(anrTime);

}

info.append(processStats.printCurrentLoad());

info.append(cpuInfo);

}

//processStats对象是在ANR发生后创建并采样的,所以输出的是ANR发生之后一段时间内的CPU使用率

info.append(processStats.printCurrentState(anrTime));

非睡眠时间百分比

在记录CPU使用率的每个采样时间点时使用了两种记录方法:SystemClock.uptimeMillis()和SystemClock.elapsedRealtime(),两者的区别就是uptimeMillis不包含睡眠时间,所以两个采样时间点之间的uptimeMillis和elapsedRealtime之比就是非睡眠时间百分比。

页错误次数

进程的CPU使用率最后输出的“faults: xxx minor/major”部分表示的是页错误次数,当次数为0时不显示。major是指Major Page Fault(主要页错误,简称MPF),内核在读取数据时会先后查找CPU的高速缓存和物理内存,如果找不到会发出一个MPF信息,请求将数据加载到内存。Minor是指Minor Page Fault(次要页错误,简称MnPF),磁盘数据被加载到内存后,内核再次读取时,会发出一个MnPF信息。一个文件第一次被读写时会有很多的MPF,被缓存到内存后再次访问MPF就会很少,MnPF反而变多,这是内核为减少效率低下的磁盘I/O操作采用的缓存技术的结果。

如果ANR发生时发现CPU使用率中iowait占比很高,可以通过查看进程的major次数来推断是哪个进程在进行磁盘I/O操作。<!– 求证一下 –>

新增和移除的进程或线程

如果一个进程或线程的CPU使用率前有“+”,说明该进程或线程是在最后两次CPU使用率采样时间段内新建的;反之如果是“-”,说明该进程或线程在采样时间段内终止了;如果是空,说明该进程或线程是在倒数第二次采样时间点之前已经存在。

至此,所有与ANR相关的日志内容都已介绍完毕,相信读者以后处理ANR问题时能够有的放矢了。

转-内存泄露问题:

http://rayleeya.iteye.com/blog/1956059

http://rayleeya.iteye.com/blog/1956638

文章都为原创,转载请注明出处,未经允许而盗用者追究法律责任。 

很久之前写的了,留着有点浪费,共享之。 

编写者:李文栋

2.1.1 什么是内存溢出

2.1.2 为什么会有内存溢出

Android 主要应用在嵌入式设备当中,而嵌入式设备由于一些众所周知的条件限制,通常都不会有很高的配置,特别是内存比较有限。如果我们编写的代码当中有太多的对内存使用不当的地方,难免会使得我们的设备运行缓慢,甚至是死机。为了能够使系统安全且快速的运行,Android 的每个应用程序都运行在单独的进程中,这个进程是由 Zygote 进程孵化出来的,每个应用进程中都有且仅有一个虚拟机实例。如果程序在运行过程中出现了内存泄漏的问题,只会影响自己的进程,不会直接影响其他进程。

Java虽然有自己的垃圾回收机制,但并不是说用Java编写的程序就不会内存溢出了。Java程序运行在虚拟机中,虚拟机初始化时会设定它的堆内存的上限值,在Android中这个上限值默认是“16m”,而你可以根据实际的硬件配置来调整这个上限值,调整的方法是在系统启动时加载的某个配置文件中设置一个系统属性:

dalvik.vm.heapsize=24m

当然也可以设置成更大的值(例如“32m”)。这样Android中每个应用进程的DalvikVM实例的堆内存上限值就变成了24MB,也就是说一个应用进程中可以同时存在更多的Java数据对象了。有一些大型的应用程序(例如游戏)运行时需要比较多的内存,heapsize太小的话根本无法运行,此时就需要考虑调整heapsize的大小了。heapsize的大小是同时对整个系统生效的,原生代码中无法单独的调整某一个Java进程的heapsize(除非我们自己修改源码,不过我们从来没这么做过)。

当代码中的缺陷造成内存泄漏时,泄漏的内存无法在虚拟机GC的时候被释放,因为这些内存被一些数据对象占用着,而这些数据对象之所以没有被释放,可以归结为两类情况:

a) 被强引用着

例如被一个正在运行的线程、一个类中的static变量强引用着,或者当前对象被注册进了framework中的一些接口中。

b) 被JNI中的指针引用着

Framework中的一些类经常会在Java层创建一个对象,同时也在C++层创建一个对象,然后通过JNI让这两个对象相互引用(保存对方的地址),BinderProxy对象就是一个很典型的例子,在这种情况下,Java层的对象同样不会被释放。

当泄漏的内存随着程序的运行越来越多时,最终就会达到heapsize设定的上限值,此时虚拟机就会抛出OutOfMemoryError错误,内存溢出了。

2.2 容易引起内存泄漏的常见问题

2.2.1 Cursor对象未正确关闭

关于此类问题其实已经是老生常谈了,但是由于Android应用源码中的缺陷和使用的场合比较复杂,所以还是会时常出现这类问题。

1. 问题举例

Cursor cursor = getContentResolver().query(…);

        if (cursor.moveToNext()) {

        … …

}

2. 问题修正

Cursor cursor = null;

try {

        cursor = getContentResolver().query(…);

        if (cursor != null && cursor.moveToNext()) {

        … …

        }

} catch (Exception e) {

        … …

} finally {

        if (cursor != null) {

                cursor.close();

        }

}

3. 引申内容

(1) 实际在使用的时候代码的逻辑通常会比上述示例要复杂的多,但总的原则是一定要在使用完毕Cursor以后正确的关闭。

(2) 如果你的Cursor需要在Activity的不同的生命周期方法中打开和关闭,那么一般可以这样做:

在onCreate()中打开,在onDestroy()中关闭;

在onStart() 中打开,在onStop() 中关闭;

在onResume()中打开,在onPause() 中关闭;

即要在成对的生命周期方法中打开/关闭。

(3) 如果程序中使用了CursorAdapter(例如Music),那么可以使用它的changeCursor(Cursor cursor)方法同时完成关闭旧Cursor使用新Cursor的操作。

(4) 至于在cursor.close时需不需要try…catch(cursor非空时),其实在close时做的工作就是释放资源,包括通过Binder跨进程注销ContentObserver时已经捕获了RemoteException异常,所以其实可以不用try…catch。

(5) 关于deactive和close,deactive不等同于close,看他们的API comments就能知道,如果deactive了一个Cursor,说明以后还是会用到它(利用requery方法),这个Cursor会释放一部分资源,但是并没有完全释放;如果确认不再使用这个Cursor了,一定要close。

(6)除了Cursor有时我们也会对Database对象做操作,例如要修正MediaProvider中的一个attachVolume方法,在每次检测到attach的是一个external的volume时就重新建立一个数据库,而不是采用以前的,那么在remove旧的数据库对象的时候不要忘记关闭它。<!– 第6点关于Database是否考虑去掉 –>

4. 影响范围

如果没有关闭Cursor,在测试次数足够多的情况下,就会出现:

(1) 内存泄漏

我们先简单的看一下Cursor的结构,这样会更好理解。数据库操作涉及到服务端的ContentProvider和客户端程序,客户端通常会通过ContentResolver.query函数查询并获取一个结果集的Cursor对象。而这个Cursor对象实际上也只是一个代理,因为要考虑到客户端和服务端在不同进程的情况,所以Cursor的使用本身也是利用了Binder机制的,而客户端和服务端的数据共享是利用共享内存来实现的,如下图所示。

客户端和服务端使用的Cursor经过了层层封装,显得十分臃肿,但它们的工作其实可以简单的从控制流和数据流两个方面来看。在控制流方面,客户端为了能和远端的服务端通信,使用实现了IBulkCursor接口的BulkCursorProxy和CusorToBulkCursorAdapter对象,例如要获取结果集数据时,客户端通过BulkCursoryProxy.onMove函数调用到CursorToBulkCursorAdapter.onMove函数,然后再调用到SQLiteCursor.onMove函数来填充数据的。在数据流方面,服务端的SQLiteCursor将从数据库中查询到的结果集写入到共享内存中,然后Binder调用返回到客户端,客户端就可以从共享内存中获取到想要的数据了。客户端的控制流和数据流的访问由BulkCursorToCursorAdapter负责,服务端则是分别由CursorToBulkCursorAdapter和SQLiteCursor负责。

如果Cursor没有正常关闭,那么客户端和服务端的CursorWindow对象和申请的那块共享内存都不会被回收,尽管其他相关的Java对象可能由于没有强引用而被回收,但是真正占用内存的通常是存放结果集数据的共享内存。大量的Cursor没有关闭的话,你可能会看到以下类型的异常信息:

  • 创建新的Java对象时发现没有足够的内存,抛出内存溢出错误:OutOfMemoryError

  • 创建新的CursorWindow时无法申请到足够的内存,可能的异常信息有:
    RuntimeException: No memory for native window object
    IllegalStateException: Couldn’t init cursor window
    CursorWindow heap allocation failed
    failed to create the CursorWindow heap

(2) 文件描述符泄漏

当然有可能很幸运,每次查询的结果集都很小,做几千次查询都不会内存溢出,但是Android的Linux内核还有另外一个限制,就是文件描述符的上限,这个上限默认是1024。

文件描述符本身是一个整数,用来表示每一个被进程所打开的文件和Socket,第一个打开的文件是0,第二个是1,依此类推。而Linux给每个进程能打开的文件数量设置了一个上限,可以使用命令“ulimit -n”查看。另外,操作系统还有一个系统级的限制。

每次创建一个Cursor对象,都会向内核申请创建一块共享内存,这块内存以文件形式提供给应用进程,应用进程会获得这个文件的描述符,并将其映射到自己的进程空间中。如果有大量的Cursor对象没有正常关闭,可想而知就会有大量的共享内存的文件描述符无法关闭,同时再加上应用进程中的其他文件描述符,就很容易达到1024这个上限,一旦达到,进程就挂掉了。

提示:可以到系统的“/proc/进程号/fd”目录中查看进程所有的文件描述符。

(3) GREF has increased to 2001

先说明一下“死亡代理”的概念。利用Binder做进程间通信时,允许对Binder的客户端代理设置一个DeathRecipient对象,它只有一个名为binderDied的函数。当Binder的服务端进程死掉了,binder驱动会通知客户端进程,最终回调DeathRecipient对象的binderDied函数,客户端进程可以借此做一些清理工作。

需要注意的是,“死亡代理”的概念只对进程间通信有效,对进程内通信没有意义;另外,Binder的客户端和服务端的概念是相对的,例如BulkCursorProxy是CursorToBulkCursorAdapter的客户端,而后者又有一个IContentObserver的客户端,其对应的服务端在BulkCursorToCursorAdapter的getObserver函数中创建。这里需要关注的就是在CursorToBulkCursorAdapter对象被创建时,会同时将该对象注册为IContentObserver的客户端对象的“死亡代理”,代码如下:

CursorToBulkCursorAdaptor的内部类ContentObserverProxy的构造函数中

public ContentObserverProxy(IContentObserver remoteObserver, DeathRecipient recipient) {

        super(null);

        mRemote = remoteObserver;

        try {

                //此处的recipient就是CursorToBulkCursorAdapter对象

                remoteObserver.asBinder().linkToDeath(recipient, 0);

        } catch (RemoteException e) {

        }

}

“死亡代理”对象的引用会被Native层的Binder代理对象的mObituaries集合引用,所以“死亡代理”对象及其关联对象由于被强引用而不会被垃圾回收掉,同时JNI在实现linkToDeath函数的过程中也创建了一些具有全局性的引用,被称作“Global Reference(简写为GREF)”,每一个GREF都会被记录到虚拟机中维护的一个“全局引用表”中。

eng模式下,JNI全局引用计数(GREF)有一个上限值为2000,如果大量Cursor对象没有被正常关闭,服务端进程就会因为“死亡代理”对象的创建使得虚拟机中的全局引用计数增多,当超过2000时,虚拟机就会抛出异常,导致进程挂掉,典型的异常信息就是“GREF has increased to 2001”。

提示:全局引用计数的上限2000已经是一个比较大的值,正常情况下很难达到。Android在eng模式下开启这项检查,就是为了能够在开发阶段发现Native层的内存泄漏问题。在usr模式下这项检查会被禁用,此时如果有内存泄漏就只有等到抛出内存溢出错误或者文件描述符超出上限等其他异常时才能发现了。

Cursor未正常关闭是导致GREF越界的原因之一,后续会在其他章节中详细讨论。

2.2.2 释放对象的引用

内存的问题是Bugzilla中的常客,经常会在不经意间遗留一些对象没有释放或销毁。

1. 静态成员变量

有时因为一些原因(比如希望节省Activity初始化时间等),将一些对象设置为static的,比如:

private static TextView mTv;

… …

mTv = (TextView) findViewById(…);

而且没有在Activity退出时释放mTv的引用,那么此时mTv本身,和与mTv相关的那个Activity的对象也不会在GC时被释放掉,Activity强引用的其他对象也无法被释放掉,这样就造成了内存泄漏。如果没有充分的理由,或者不能够清楚的控制这样做带来的影响,请不要这样写代码。

2. 正确注册/注销监听器对象

经常要用到一些XxxListener对象,或者是XxxObserver、XxxReceiver对象,然后用registerXxx方法注册,用unregisterXxx方法注销。本身用法也很简单,但是从一些实际开发中的代码来看,仍然会有一些问题:

(1) registerXxx和unregisterXxx方法的调用通常也和Cursor的打开/关闭类似,在Activity的生命周期中成对的出现即可:

在 onCreate() 中 register,在 onDestroy() 中 unregitster;

在 onStart() 中 register,在 onStop() 中 unregitster;

在 onResume() 中 register,在 onPause() 中 unregitster;

(2) 忘记unregister

以前看到过一段代码,在Activity中定义了一个PhoneStateListener的对象,将其注册到TelephonyManager中:

TelephonyManager.listen(l,PhoneStateListener.LISTEN_SERVICE_STATE);

但是在Activity退出的时候注销掉这个监听,即没有调用以下方法:

TelephonyManager.listen(l,PhoneStateListener.LISTEN_NONE);

因为PhoneStateListener的成员变量callback,被注册到了TelephonyRegistry中,TelephonyRegistry是后台的一个服务会一直运行着。所以如果不注销,则callback对象无法被释放,PhoneStateListener对象也就无法被释放,最终导致Activity对象无法被释放。

3. 适当的使用SoftReferenceWeakReference

如果要写一个缓存之类的类(例如图片缓存),建议使用SoftReference,而不要直接用强引用,例如:

private final ConcurrentHashMap<Long, SoftReference<Bitmap>> mBitmapCache = new ConcurrentHashMap<Long, SoftReference<Bitmap>>();

当加载的图片过多,应用可用堆内存不足的时候,就可以自动的释放这些缓存的Bitmap对象。

关于Java中的强引用、软引用、弱引用和虚引用是一些比较重要的概念,在Android开发中经常会用到。

2.2.3 构造 Adapter 时,没有使用缓存的 convertView

以构造 ListView 的 BaseAdapter 为例,在 BaseAdapter 中提供了以下方法:

public View getView(int position,View convertView,ViewGroup parent)

来向 ListView 提供每一个 item 所需要的 view 对象。初始时 ListView 会从 BaseAdapter 中根据当前的屏幕布局实例化一定数量的 view 对象,同时 ListView 会将这些 view 对象缓存起来 。当向上滚动ListView 时,原先位于最上面的 list item 的 view 对象会被回收,然后被用来构造新出现的最下面的 listitem。这个构造过程就是由 getView()方法完成的,getView()的第二个形参 View convertView 就是被缓存起来的 list item 的 view 对象(初始化时缓存中没有 view对象则 convertView 是 null)。由此可以看出,如果我们不去使用 convertView,而是每次都在 getView()中重新实例化一个 View 对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大ListView 回收listitem 的 view 对象的过程可以查看:android.widget.AbsListView类中的addScrapView(View scrap) 方法。

示例代码:

public View getView(int position,View convertView,ViewGroup parent) {

        View view = new Xxx(…);

        … …

        return view;

}

修正示例代码:

public View getView(int position,View convertView,ViewGroup parent) {

        View view = null;

        if (convertView != null) {

                view = convertView;

                populate(view,getItem(position));

                

        } else {

                view = new Xxx(…);

                

        }

        return view;

}

2.2.4 Bitmap 对象不再使用时调用 recycle()释放内存

有时我们会自己操作 Bitmap 对象,如果一个 Bitmap 对象比较占内存,当它不再被使用的时候,可以调用 Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的 ,视情况而定。可以看一下代码中的注释:

/**

* Free up the memory associated with this bitmap’s pixels,and mark the

* bitmap as “dead”,meaning it will throw an exception if getPixels() or

* setPixels() is called,and will draw nothing. This operation cannot be

* reversed,so it should only be called if you are sure there are no

* further uses for the bitmap. This is an advanced call,and normally need

* not be called,since the normal GC process will free up this memory when

* there are no more references to this bitmap.

*/

 

文章都为原创,转载请注明出处,未经允许而盗用者追究法律责任。 

很久之前写的了,留着有点浪费,共享之。 

编写者:李文栋   微博关注: 云且留猪

2.3 如何分析内存溢出问题

无论怎么小心,想完全避免 bad code 是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方。

既然要排查的是内存问题,自然需要与内存相关的工具,DDMS和MAT就是两个非常好的工具。下面详细介绍。

2.3.1 内存监测工具 DDMS –> Heap

Android tools 中的 DDMS 就带有一个很不错的内存监测工具 Heap(这里我使用 eclipse 的 ADT 插件,并以真机为例,在模拟器中的情况类似)。用 Heap 监测应用进程使用内存情况的步骤如下:

  1. 启动 eclipse 后,切换到 DDMS 透视图,并确认 Devices 视图、Heap 视图都是打开的;

  2. 将手机通过 USB 链接至电脑,链接时需要确认手机是处于“USB 调试”模式,而不是作为“Mass Storage”;

  3. 链接成功后,在 DDMS 的 Devices 视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息;

  4. 点击选中想要监测的进程,比如 system_process 进程;

  5. 点击选中 Devices 视图界面中最上方一排图标中的“Update Heap”图标;

  6. 点击 Heap 视图中的“Cause GC”按钮;

  7. 此时在 Heap 视图中就会看到当前选中的进程的内存使用量的详细情况[如图所示]。

说明:

  • 点击“Cause GC”按钮相当于向虚拟机请求了一次 gc 操作;

  • 当内存使用信息第一次显示以后,无须再不断的点击“Cause GC”,Heap 视图界面会定时刷新,在对应用的不断的操作过程中就可以看到内存使用的变化;

  • 内存使用信息的各项参数根据名称即可知道其意思,在此不再赘述。如何才能知道我们的程序是否有内存泄漏的可能性呢。这里需要注意一个值:Heap 视图中部有一个 Type 叫做 data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在 data object 一行中有一列是“Total Size”,其值就是当前进程中所有 Java 数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:

  • 不断的操作当前应用,同时注意观察 data object 的 Total Size 值;

  • 正常情况下 Total Size 值都会稳定在一个有限的范围内,也就是说由于程序中的代码良好,没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象 ,而在虚拟机不断的进行 GC 的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;

  • 反之如果代码中存在没有释放对象引用的情况,则 data object 的 Total Size 值在每次 GC后不会有明显的回落,随着操作次数的增多 Total Size 的值会越来越大,直到到达一个上限后导致进程被 kill 掉。

  • 此处已 system_process 进程为例,在我的测试环境中 system_process 进程所占用的内存的data object 的 Total Size 正常情况下会稳定在 2.2~2.8 之间,而当其值超过 3.55 后进程就会被kill。

总之,使用 DDMS 的 Heap 视图工具可以很方便的确认我们的程序是否存在内存泄漏的可能性。

2.3.2 内存分析工具 MAT(Memory Analyzer Tool)

如果使用 DDMS 确实发现了我们的程序中存在内存泄漏,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?如果从头到尾的分析代码逻辑,那肯定会把人逼疯,特别是在维护别人写的代码的时候。这里介绍一个极好的内存分析工具 — Memory Analyzer Tool(MAT)。

MAT 是一个 Eclipse 插件,同时也有单独的 RCP 客户端。官方下载地址、MAT 介绍和详细的使用教程请参见:www.eclipse.org/mat,在此不进行说明了。另外在 MAT 安装后的帮助文档里也有完备的使用教程。在此仅举例说明其使用方法。我自己使用的是 MAT 的eclipse 插件,使用插件要比 RCP 稍微方便一些。

MAT通过解析Hprof文件来分析内存使用情况。HPROF其实是在J2SE5.0中包含的用来分析CPU使用和堆内存占用的日志文件,实质上是虚拟机在某一时刻的内存快照,dalvik中也包含了这样的工具,但是其文件格式和JVM的格式不完全相同,可以用SDK中自带的hprof-conv工具进行转换,例如:

$./hprof-conv raw.hprof converted.hprof

可以使用hprof文件配合traceview来分析CPU使用情况(函数调用时间),此处仅仅讨论用它来分析内存使用情况,关于hprof的其他信息可以查看:http://java.sun.com/developer/technicalArticles/Programming/HPROF.html

以及Android源码中的/dalvik/docs/heap-profiling.html文件(这个比较重要,建议看看,例如kill -10在Android2.3中已经不支持了)。

使用 MAT 进行内存分析需要几个步骤,包括:生成.hprof 文件、打开 MAT 并导入hprof文件、使用 MAT 的视图工具分析内存。以下详细介绍。

1. 生成hprof 文件

生成hprof 文件的方法有很多,而且 Android 的不同版本中生成hprof 的方式也稍有差别,我使用的版本的是 2.1,各个版本中生成hprof 文件的方法请参考:

http://android.git.kernel.org/?p=platform/dalvik.git;a=blob_plain;f=docs/heap-profiling.html;hb=HEAD。

(1) 打开 eclipse 并切换到 DDMS 透视图,同时确认 Devices、Heap 和 logcat 视图已经打开了 ;

(2) 将手机设备链接到电脑,并确保使用“USB 调试”模式链接,而不是“Mass Storage“模式;

(3) 链接成功后在 Devices 视图中就会看到设备的序列号,和设备中正在运行的部分进程;

(4) 点击选中想要分析的应用的进程,在 Devices 视图上方的一行图标按钮中,同时选中“Update Heap”和“Dump HPROF file”两个按钮;

(5) 这是 DDMS 工具将会自动生成当前选中进程的.hprof 文件,并将其进行转换后存放在sdcard 当中,如果你已经安装了 MAT 插件,那么此时 MAT 将会自动被启用,并开始对.hprof文件进行分析;

注意: (4)步和第(5)步能够正常使用前提是我们需要有 sdcard,并且当前进程有向 sdcard中写入的权限(WRITE_EXTERNAL_STORAGE),否则.hprof 文件不会被生成,在 logcat 中会显示诸如ERROR/dalvikvm(8574): hprof: can’t open /sdcard/com.xxx.hprof-hptemp: Permission denied.的信息。

如果我们没有 sdcard,或者当前进程没有向 sdcard 写入的权限(如 system_process) 那我们可以这样做:

(6) 在当前程序中,例如 framework 中某些代码中,可以使用 android.os.Debug 中的:

public static void dumpHprofData(String fileName) throws IOException

方法,手动的指定.hprof 文件的生成位置。例如:

xxxButton.setOnClickListener(new View.OnClickListener() {

public void onClick(View view) {

android.os.Debug.dumpHprofData(“/data/temp/myapp.hprof”);

… …

}

}

上述代码意图是希望在 xxxButton 被点击的时候开始抓取内存使用信息,并保存在我们指定的位置:/data/temp/myapp.hprof,这样就没有权限的限制了,而且也无须用 sdcard。但要保证/data/temp 目录是存在的。这个路径可以自己定义,当然也可以写成 sdcard 当中的某个路径。

如果不确定进程什么时候会OOM,例如我们在跑Monkey的过程中出现了OOM,此时最好的办法就是让程序在出现OOM之后,而没有将OOM的错误信息抛给虚拟机之前就将进程的hprof抓取出来。方法也很简单,只需要在代码中你认为会抛出OutOfMemoryError的地方try…catch,并在catch块中使用android.os.Debug.dumpHprofData(String file)方法就可以请求虚拟机dump出hprof到你指定的文件中。例如我们之前为了排查应用进程主线程中发生的OOM,就在ActivityThread.main()方法中添加了以下代码:

try {

Looper.loop();

} catch (OutOfMemoryError e) {

String file = “path_to_file.hprof”

… …

try {

android.os.Debug.dumpHprofData(file);

} catch (IOException e1) {

e1.printStackTrace();

}

}

在设置hprof的文件路径时,需要考虑权限问题,包括SD卡访问权限、/data分区私有目录访问权限。

之所以在以上位置添加代码,是因为在应用进程主线程中如果发生异常和错误没有捕获,最终都会从Looper.loop()中抛出来。如果你需要排查在其他线程,或者framework中的OOM问题时,同样可以在适当的位置使用android.os.Debug.dumpHprofData(String file)方法dump hprof文件。

有了hprof文件,并且用hprof-conv转换格式之后,第二步就可以用MemoryAnalyzerTool(MAT)工具来分析内存使用情况了。

2. 使用 MAT 导入hprof 文件

(1) 如果是 eclipse 自动生成的hprof 文件,可以使用 MAT 插件直接打开(可能是比较新的 ADT才支持);

(2) 如 果 eclipse 自 动 生 成 的 .hprof 文 件 不 能 被 MAT 直 接 打 开 , 或 者 是 使 用android.os.Debug.dumpHprofData()方法手动生成的hprof 文件,则需要将hprof 文件进行转换,转换的方法:

例如我将hprof 文件拷贝到 PC 上的/ANDROID_SDK/tools 目录下,并输入命令 hprof-conv xxx.hprof yyy.hprof,其中 xxx.hprof 为原始文件,yyy.hprof 为转换过后的文件。转换过后的文件自动放在/ANDROID_SDK/tools 目录下。OK,到此为止,hprof 文件处理完毕,可以用来分析内存泄露情况了。

(3) 在 Eclipse 中点击 Windows->Open Perspective->Other->Memory Analyzer,或者打 Memory Analyzer Tool 的 RCP。在 MAT 中点击 File->Open File,浏览并导入刚刚转换而得到的hprof文件。

3. 使用 MAT 的视图工具分析内存

导入hprof 文件以后,MAT 会自动解析并生成报告,点击 Dominator Tree,并按 Package分组,选择自己所定义的 Package 类点右键,在弹出菜单中选择 List objects->With incoming references。这时会列出所有可疑类,右键点击某一项,并选择 Path to GC Roots -> exclude weak/soft references,会进一步筛选出跟程序相关的所有有内存泄露的类。据此,可以追踪到代码中的某一个产生泄露的类。

MAT 的界面如下图所示。

了解 MAT 中各个视图的作用很重要,例如 www.eclipse.org/mat/about/screenshots.php 中介绍的。

总之使用 MAT 分析内存查找内存泄漏的根本思路,就是找到哪个类的对象的引用没有被释放,找到没有被释放的原因,也就可以很容易定位代码中的哪些片段的逻辑有问题了。下一节将用一个示例来说明MAT详细的使用过程。

2.3.3 MAT使用方法

1. 构建演示程序

首先需要构建一个演示程序,并获取hprof文件。程序很简单,按下Button后就循环地new自定义对象SomeObj,并将对象add到ArrayList中,直到抛出OutOfMemoryError,此时会捕获该错误,同时使用android.os.Debug.dumpHprofData方法dump该进程的内存快照到/sdcard/oom.hprof文件中。

package com.demo.oom;

 

import java.io.IOException;

import java.util.ArrayList;

 

import android.app.Activity;

import android.os.Bundle;

import android.widget.Button;

import android.view.View;

 

publicclass OOMDemoActivity extends Activity implements View.OnClickListener {

privatestaticfinal String HPROF_FILE = “/sdcard/oom.hprof”;

private Button mBtn;

private ArrayList<SomeObj> list = new ArrayList<SomeObj>();

 

@Override

publicvoid onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

 

mBtn = (Button)findViewById(R.id.btn);

mBtn.setOnClickListener(this);

}

 

@Override

publicvoid onClick(View v) {

try {

while (true) {

list.add(new SomeObj());

}

} catch (OutOfMemoryError e) {

try {

android.os.Debug.dumpHprofData(HPROF_FILE);

throw e;

} catch (IOException e1) {

e1.printStackTrace();

}

}

}

 

private class SomeObj {

private static final intDATA_SIZE = 1 * 1024 * 1024;

private byte[] data;

SomeObj() {

data = newbyte[DATA_SIZE];

}

}

}

因为要写入SDCard,所以要在AndroidManifest.xml中声明WRITE_EXTERNAL_STORAGE的权限。

注意:演示程序中是使用平台API来获取dump hprof文件的,你也可以使用ADT的DDMS工具来dump。每个hprof都是针对某一个Java进程的,如果你dump的是com.demo.oom进程的hprof,是无法用来分析system_server进程的内存情况的。

编译并运行程序最终会在SDCard中生成oom.hprof文件,log中会打印相关的日志信息,请留意红色字体:

I/dalvikvm(1238): hprof: dumping heap strings to “/sdcard/oom.hprof”.

I/dalvikvm(1238): hprof: heap dump completed (21354KB)(虚拟机dump了hprof文件)

D/dalvikvm(1238): GC_HPROF_DUMP_HEAP freed <1K, 13% free 20992K/23879K, external 716K/1038K, paused 4034ms

D/AndroidRuntime(1238): Shutting down VM

W/dalvikvm(1238): threadid=1: thread exiting with uncaught exception (group=0x40015560)

E/AndroidRuntime(1238): FATAL EXCEPTION: main

E/AndroidRuntime(1238): java.lang.OutOfMemoryError(是OOM错误)

E/AndroidRuntime(1238): at com.demo.oom.OOMDemoActivity$SomeObj.<init>(OOMDemoActivity.java:45)

E/AndroidRuntime(1238): at com.demo.oom.OOMDemoActivity.onClick(OOMDemoActivity.java:29)

E/AndroidRuntime(1238): at android.view.View.performClick(View.java:2485)

E/AndroidRuntime(1238): at android.view.View$PerformClick.run(View.java:9080)

E/AndroidRuntime(1238): at android.os.Handler.handleCallback(Handler.java:587)

E/AndroidRuntime(1238): at android.os.Handler.dispatchMessage(Handler.java:92)

E/AndroidRuntime(1238): at android.os.Looper.loop(Looper.java:123)

E/AndroidRuntime(1238): at android.app.ActivityThread.main(ActivityThread.java:3683)

(从方法堆栈可以看到是应用进程的主线程中发生了OOM)

E/AndroidRuntime(1238): at java.lang.reflect.Method.invokeNative(Native Method)

E/AndroidRuntime(1238): at java.lang.reflect.Method.invoke(Method.java:507)

E/AndroidRuntime(1238): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)

E/AndroidRuntime(1238): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)

E/AndroidRuntime(1238): at dalvik.system.NativeStart.main(Native Method)

W/ActivityManager(61): Force finishing activity com.demo.oom/.OOMDemoActivity

D/dalvikvm(229): GC_EXPLICIT freed 8K, 55% free 2599K/5703K, external 716K/1038K, paused 1381ms

W/ActivityManager(61): Activity pause timeout for HistoryRecord{406671e8 com.demo.oom/.OOMDemoActivity}

W/ActivityManager(61): Activity destroy timeout for HistoryRecord{406671e8 com.demo.oom/.OOMDemoActivity}

I/Process(1238): Sending signal. PID: 1238 SIG: 9(错误没有捕获被抛给虚拟机,最终被kill掉)

I/ActivityManager(61): Process com.demo.oom (pid 1238) has died.(应用进程挂掉了)

获取hprof文件后再用hprof-conv工具转换一下格式:

D:\work\android\sdk\tools>hprof-conv.exe C:\Users\ray\Desktop\oom.hprof C:\Users

\ray\Desktop\oom\oom.hprof(将转换后的hprof放到一个单独的目录下,因为分析时会生成很多中间文件)

2. MAT提供的各种分析工具

使用MAT导入转换后的hprof文件,导入时会让你选择报告类型,选择“Leak Suspects Report”即可。然后就可以看到如下的初步分析报告:

MAT在Overview视图中用饼图展示了内存的使用情况,列出了占用内存最大的Java对象com.demo.oom.OOMDemoActivity,我们可以根据这个线索来继续调查,但如果没有这样的提示,也可以根据自己推断来分析。在进一步分析之前,需要先熟悉MAT为我们提供的各种工具。

(1) Histogram

列出每个类的实例对象的数量,是第一个非常有用的分析工具。

可以看到该视图一共有四列,点击列名可以按照不同的列以升序或降序排序。每一列的含义为:

Class Name:类名

Objects:每一种类型的对象数量

Shallow Heap:一个对象本身(不包括该对象引用的其他对象)所占用的内存

Retained Heap:一个对象本身,以及由该对象引用的其他对象的Shallow Heap的总和。官方文档中解释为:Generally speaking, shallow heap of an object is its size in the heap and retained size of the same object is the amount of heap memory that will be freed when the object is garbage collected.

默认情况下该视图是按照Class来分类的,也可以点击工具栏中的选择不同的分类类型,这样可以更方便的筛选需要的信息。

默认情况下该视图只是粗略的计算了每种类型所有对象的Retained Heap,如果要精确计算的话可以点击工具栏中的来选择。

有时为了分析进程的内存使用情况,会对一个在不同的时间点抓取多个hprof文件来观察,MAT提供了一个非常好的工具来对比这些hprof文件,点击工具栏中的可以选择已经打开的其他hprof文件,选择后MAT将会对当前的hprof和要对比的hprof做一个插值,这样就可以很方便的观察对象的变化了。不过这个工具只有在Histogram视图中才有。

列表的第一行是一个搜索框,可以输入正则式或者数量来过滤列表的内容。

            1. (2) Dominator Tree

列出进程中所有的对象,是第二个非常有用的分析工具。

和Histogram不同的是左侧列的是对象而不是类(每个对象还有内存地址,例如@0x40516b08),而且还多了Percentage一列。

右键点击任意一个类型,会弹出一个上下文菜单:

菜单中有很多其他非常有用的功能,例如:

List Objects(with outgoing references/with incoming references):列出由该对象引用的其他对象/引用该对象的其他对象;

Open Source File:打开该对象的源码文件;

Path To GC Roots:由当前对象到GC Roots引用链

GC Roots:A garbage collection root is an object that is accessible from outside the heap.也就是指那些不会被垃圾回收的对象。图中标识有黄色圆点的对象就是GC Roots,每个GC Root之后都会有灰黑色的标识表明这个对象之所以是GC Root的原因。使得一个对象成为GC Root的原因一般有以下几个:

System Class

Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .

JNI Local

Local variable in native code, such as user defined JNI code or JVM internal code.

JNI Global

Global variable in native code, such as user defined JNI code or JVM internal code.

Thread Block

Object referred to from a currently active thread block.

Thread

A started, but not stopped, thread.

Busy Monitor

Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.

Java Local

Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.

Native Stack

In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.

Finalizer

An object which is in a queue awaiting its finalizer to be run.

Unfinalized

An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.

Unreachable

An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.

Unknown

An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.

在上图的“Path To GC Roots”的菜单中可以选择排除不同的非强引用组合来筛选到GC Roots的引用链,这样就可以知道有哪些GC Roots直接或间接的强引用着当前对象,导致无法释放了。


(3) Top Consumers

以class和package分类表示占用内存比较多的对象。

(4) Leak Suspects

对内存泄露原因的简单分析,列出了可能的怀疑对象,这些对象可以做为分析的线索。

(5) OQL

MAT提供了一种叫做对象查询语言(Object Query Language,OQL)的工具,方便用于按照自己的规则过滤对象数据。例如想查询我的Activity的所有对象:

SELECT * FROM com.demo.oom.OOMDemoActivity

或者想查询指定package下的所有对象:

SELECT * FROM “com.demo.oom.*” (如果使用通配符,需要用引号)

或者想查询某一个类及其子类的所有对象:

SELECT * FROM INSTANCEOF android.app.Activity

还有很多高级的用法请参考帮助文档。

3. 使用MAT分析OOM原因

熟悉了以上的各种工具,就可以来分析问题原因了。分析的思路有很多。

思路一:

首先我们从MAT的提示中得知com.demo.oom.OOMDemoActivity @ 0x40516b08对象占用了非常多的内存(Shallow Size: 160 B Retained Size: 18 MB),我们可以在DominatorTree视图中查找该对象,或者通过OQL直接查询该类的对象。

按照Retained Heap降序排列,可以知道OOMDemoActivity对象之所以很大是因为有一个占用内存很大的ArrayList类型的成员变量,而根本原因是这个集合内包含了很多1MB以上的SomeObj对象。此时就可以查看代码中对SomeObj的操作逻辑,查找为什么会有大量SomeObj存在,为什么每个SomeObj都很大。找到问题后想办法解决,例如对SomeObj的存储使用SoftReference,或者减小SomeObj的体积,或者发现是由于SomeObj没有被正确的关闭/释放,或者有其他static的变量引用这SomeObj。

思路二

如果MAT没能给出任何有价值的提示信息,我们可以根据自己的判断来查找可以的对象。因为发生OOM的进程是com.demo.oom,可以使用OQL列出该进程package的所有对象,然后再查找可疑的对象。对应用程序来说,这是非常常用的方法,如下图。

通过查询发现SomeObj的对象数量特别多,假设正常情况下对象用完后应该立即释放才对,是什么导致这些对象没有被释放呢?通过“Path To GC Roots”的引用链可以知道是OOMDemoActivity中的list引用了SomeObj,所以可以考虑SomeObj是否错误的被添加进了list中,如下图。

总之,分析的根本目的就是找到那些数量很大或者体积很大的对象,以及他们被什么样的GC Roots引用而没有被释放,然后再通过检查代码逻辑找到问题原因。