Comments

0x00 ndk-stack

Google从NDK r6开始提供这个方便的工具,帮助开发者定位Native 异常。和ndk-build一样,这个命令行工具被放在NDK安装目录下。作为Debug 工具,其操作方法也十分简单:只需在参数里指定包含符号表的so文件即可。

如果需要实时获取异常信息,我们可以直接运行:adb shell logcat | ndk-stack -sym $PROJECT_DIR/obj/local/armeabi;有的时候日志文件并不一定能直接看到,该工具也支持“异步”分析。这里我们首先运行:adb shell logcat > android.log来模拟“异步”获取日志。 在获得日志文件之后,我们拿它喂给命令行工具:ndk-stack -sym $PROJECT_DIR/obj/local/armeabi -dump android.log

就这样,天书一般的日志文件瞬间变得友好可读了。

0x01 addr2line以及objdump

对于有经验的程序员来说,可能更熟悉这2款工具。方法类似,结果相同,所以这里就不再赘述了。

Comments

一句log”!!! FAILED BINDER TRANSACTION !!!“,伴随着App的崩溃,留下的是程序员深深的思考。定位到android 的源码,此log 出自android_util_Binder.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void signalExceptionForError(JNIEnv* env, jobject obj, status_t err, bool canThrowRemoteException)
{
    switch (err) {
        ...
        case FAILED_TRANSACTION:
            ALOGE("!!! FAILED BINDER TRANSACTION !!!");
            // TransactionTooLargeException is a checked exception, only throw from certain methods.
            // FIXME: Transaction too large is the most common reason for FAILED_TRANSACTION
            //        but it is not the only one.  The Binder driver can return BR_FAILED_REPLY
            //        for other reasons also, such as if the transaction is malformed or
            //        refers to an FD that has been closed.  We should change the driver
            //        to enable us to distinguish these cases in the future.
            jniThrowException(env, canThrowRemoteException? "android/os/TransactionTooLargeException": "java/lang/RuntimeException", NULL);
            break;
        ...
    }
}

很明显,按照注释的描述,我们遇到的问题就转换为了TransactionTooLargeException引起的程序崩溃了。去官网一查,它给出的说明更清晰:远程调用的时候,参数和返回值都以Parcel 的形式保存在Binder 事务的缓存对象中。如果参数或返回值过大,则会抛出TransactionTooLargeException异常。

按照这个提示,我跟到驱动层的binder.c代码看了一下binder_transaction以及binder_alloc_buf方法,证实了前面注释代码所言非虚。另外,官方文档提到了一个1M 大小的buffer空间的限制,我翻了几个版本的代码来看,发现其实这个数值并没有写死。buffer 应该是不大于2M而不是它说的1M,但正如文档所说,由于binder 被所有跨进程通讯所共享,即使你的parcel 不大,但是使用binder 的人很多,也有可能造成上述异常。所以,这里解决问题的方法,应该是尽可能的传递小一点的parcel或者使用其他数据通讯方案。是的,说的就是你,不要用binder 传那么多那么大的高清图片了!

Comments

话说,任何开发环境,想要取得上乘的性能,内存优化是一个不可避免的话题。虽然android 开发过程中,有GC陪伴,但这并不意味着你可以完全忽略内存的分配与释放。这是因为,不恰当的代码仍然会导致内存泄露,以至于GC也无力回天。

内存限制

为了维持多任务的运行环境,android 给每个运行的应用规定了一个内存上限(因不同设备的物理内存大小而变化,常见的有16M~48M不等)。每当你的应用企图在触碰上限的情况下分配内存,就会引发OutOfMemoryError

内存泄露

虚拟机中的内存泄露基本上就一条:代码残留了对象,让GC无法回收。也许这个情况一时半会不会翻起大浪,但也可能在下一秒,就触碰内存限制,导致程序崩溃。所以,内存泄露的BUG通常都非常隐蔽、随机、让人头大。

常见的泄露有以下几种:

  • Cursor没有关闭
  • registerReceiver之后没有成对的unregisterReceiver
  • I/O Stream没有关闭
  • Context泄露

前面3种情况,没啥好说的,在代码层面稍加注意即可避免。最后一种情况可能有些变种需要注意,这里使用2个例子尝试着描述一下。首先借用Romain Guy的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);

  TextView label = new TextView(this);
  label.setText("Leaks are bad");

  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);

  setContentView(label);
}

万恶的静态变量出现了,引用关系Drawable–> TextView–> Activity导致整个Activity 无法被回收。此外,经常被讨论的Webview 泄露,情况也是类似的。解决方法可以参考这个帖子。这里要谈的另外一种Context泄露是由于非静态内部类引用导致的:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DemoActivity extends Activity {
    class LeakRUN implements Runnable {

        @Override
        public void run() {
            try {
                TimeUnit.DAYS.sleep(365);
            } catch (InterruptedException e) {
            }
        }
    }

}

正确的处理这种泄露应当仿造ViewRoot中的内部类W那样使用弱引用。这么做的好处是,一来不会出现空指针,二来不会导致内存泄露。

内存工具

尽管上面罗列了种种常见的泄露情况,但实际开发中遇到的现象总是千奇百怪的,解决泄露的方法也会因情况而异。所以,这里更重要的是需要了解如何使用工具来发现应用内存是如何泄露的。

Logcat

越简单的方法,往往越直接有效。Logcat 提供实时的虚拟机日志,每当垃圾回收发生的时候,我们会看到这样的日志D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>。其中,垃圾回收的原因、回收内存的大小这些并不是现在关注的重点,我们的兴趣点应该放在堆的状态上。这个状态通常会提供两个数值,“空闲内存的比例”以及“内存占用的比例”。如果后者在每次垃圾回收之后,持续上升,那么几乎就可以断定出现内存泄露了

Monitor

Monitor是一个工具大集合,开发者可以从Android studio/ Eclipse直接找到它的入口,调试人员也可以直接从目录<sdk>/tools/下找到它。比起Logcat 被动的查看log, Heap Update提供了可视化的内存数据界面以及垃圾回收的操作接口。连续的在观察数据、回收内存以及应用交互的过程中往返,可以帮助我们定位“哪些操作”引起了内存激增Allocation Tracker则提供了更为精确的代码级定位

adb

adb 命令行工具提供的内存查询接口则更为灵活,信息也更加丰富。一行命令adb shell dumpsys meminfo <package_name>即可完成操作。有精力的开发者完全可以依此定制一款自己的工具来帮助内存泄露的定位。一般来说,我们可以关注Pss Total以及Private Dirty这两项数据来掌握内存信息,而Objects栏目的数据可以帮助引导我们更直观的发现泄漏的情况。

Heap Dump

最有效的工具,肯定需要最后出场。在Monitor 中选择Dump HPROF file,可以导出整个设备的虚拟机内存信息。由于该文件并不是传统的java 虚拟机内存文件,因此需要使用android sdk 中的hprof-conv工具对其进行转换。对于转换后的文件,可以使用Eclipse Memory Analyzer Tool (MAT)进行分析。该工具提供两种不同的视图(HistogramDominator tree)分别从不同的视角帮助你分析内存。值得一提的是,如果需要更为精确的导出时机的话,还可以在项目的代码中进行控制。

以上工具的具体使用方法,可以参考以下链接的文献,这里就不再赘述了。

参考资料