且听风吟

Don't panic! I'm a programmer.

分析Android App内存占用

| Comments

使用dumpsys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
shell@android:/ # dumpsys meminfo com.android.systemui
Applications Memory Usage (kB):
Uptime: 296724 Realtime: 296723
** MEMINFO in pid 2786 [com.android.systemui] **
                         Shared  Private     Heap     Heap     Heap
                   Pss    Dirty    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------
       Native        0        0        0    12540    12384       83
       Dalvik     8256     5128     8088    13192     8327     4865
       Cursor        0        0        0
       Ashmem        0        0        0
    Other dev        4       24        0
     .so mmap     1704     1996     1256
    .jar mmap        0        0        0
    .apk mmap      750        0        0
    .ttf mmap        4        0        0
    .dex mmap     1284        0        0
   Other mmap       83       20       32
      Unknown     5692      652     5676
        TOTAL    17777     7820    15052    25732    20711     4948

 Objects
               Views:      155         ViewRootImpl:        7
         AppContexts:       12           Activities:        0
              Assets:        3        AssetManagers:        3
       Local Binders:       38        Proxy Binders:       38
    Death Recipients:        3
     OpenSSL Sockets:        0

 SQL
         MEMORY_USED:        0
  PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0

有价值的两个值:

  • PSS(Proportional Set Size )
    应用本身所占的物理内存 + 和其他应用share的内存。这个值会被系统作为这个应用的phisical memory footprint
  • Private Dirty
    应用本身所占的物理内存,如果把该应用杀掉,那么就会释放这些内存 通过AppContexts和Activities可以判断应用是否有activity内存泄漏(如果这个值一直在增长)。

Tips for memory usage

http://developer.android.com/training/articles/memory.html

  1. Service会阻止进程被系统杀掉,不要让service一直运行,最好使用IntentService,运行完一个job就退出。
  2. 在onTrimMemory回调中释放UI资源(注意:这里不同于onStop)
  3. 使用getMemoryClass()获得应用的heap大小。app也可以使用android:largeHeap选项请求大的heap(慎用!)。
  4. 使用优化过的数据结构: SparseArray, SparseBooleanArray, and LongSparseArray.
  5. 除非有必要,否则不要抽象代码
  6. 使用nano protocol buffer来取代xml序列化数据
  7. 谨慎使用第三方库,因为这些库并不适合移动设备的运行环境。也不要为了使用so库中一两个功能,而引入整个库。
  8. 使用ProGuard来压缩代码:去除无用的代码,同时混淆代码
  9. 涉及到UI资源的进程很占内存。所以如果需要后台运行的service,如音乐播放,可以将这个service配置到另外的进程里,这样系统可以不用为了保持这个service服务的运行,而必须保留占内存的UI进程。

其它影响应用内存占用的因素:

  1. 内存泄露。如使用static的Context引用,保存过多大对象,如Bitmap。
  2. 线程对象引起的内存泄露。当一个线程作为一个activity的内部类时,它的对象会隐式持有一个外部类,也就是Activity实例的引用,而线程的生命周期不容易控制,很容易引起Activity资源的泄露。解决方案:

    • 使用static的内部类
    • 使用弱引用保存Context引用
  3. 数据库游标泄露。

其他工具

adb shell procrank
adb shell cat /proc/meminfo

Reference

http://developer.android.com/tools/debugging/debugging-memory.html
http://elinux.org/Android_Memory_Usage
http://stackoverflow.com/questions/2298208/how-to-discover-memory-usage-of-my-application-in-android/

Bitmap内存管理

| Comments

每个App所分配的最大内存在Android Compatibility Definition Document (CDD)中3.7节Virtual Machine Capability中有说明,最小至16MB。对Bitmap的处理不当,将很容易超出这个限制而导致OutOfMemoryException。

以下是官方文档推荐的一些处理Bitmap的优化措施。

Scale Bitmap before loading into memory

加载bitmap前的必要工作,使用BitmapFactory.OptionsinJustDecodeBounds 选项加载按比例压缩后的bitmap到内存。

使用AsyncTask加载bitmap

这里需要注意在ListView,GridView这种组件中,当用户快速滑动时,处理不当将产生大量AsyncTask,并且AsyncTask由于完成的时机是不确定的,如何将UI上的Bitmap对应到AsyncTask上将是一个问题。

使用缓存机制

Memory cache

LruCache非常适合用来做Bitmap缓存。

Android 2.3之后,SoftReference不再适合在缓存中使用,因为2.3之后VM将更加激进的回收SoftReference对象,导致起不到缓存的效果。

Disk cache

使用磁盘缓存DiskLruCache来持久化memory cache。

重用bitmap内存

Android 3.0之前,bitmap的内存分配在native层,需要依靠app来显示调用recycle来释放它所占用的内存。在3.0之后,bitmap的内存分配在VM的堆上,因此bitmap的内存有GC来管理,一旦侦测到bitmap没有对象引用到它,GC会自动释放bitmap的内存。

随之而来的问题是,bitmap占用的空间比较大,GC在释放bitmap的内存时是一个昂贵的操作,如果你不断的创建新的bitmap对象,GC将不断的被触发起来工作,从而影响程序的性能。

Android3.0之后通过BitmapFactory.Options.inBitmap这个选项提供了重用bitmap内存这个功能。如果新的bitmap的大小小于一个已经存在的bitmap,那么我们可以重用这个bitmap对象,从而避免创建多个bitmap对象带来的昂贵的GC开销。

处理系统Configuration changed事件

系统configuration changed的时候由于Activity将默认被重启,如果加载Bitmap的AsyncTask或者线程没有被正确处理,可能导致产生许多无用的线程。

Reference

Bitmap Allocation
ListView加载Bitmap分析
BitmapFun 示例

Pmap介绍

| Comments

pmap用来打印进程地址空间占用。通常也可以通过如下命令来查看:

cat /proc/self/maps 

但用pmap包含更多的信息,显示结果也更直观:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ pmap -d 4827
    4827:   bash
    Address           Kbytes Mode  Offset           Device    Mapping
    0000000000400000     896 r-x-- 0000000000000000 008:00005 bash
    00000000006e0000       4 r---- 00000000000e0000 008:00005 bash
    00000000006e1000      36 rw--- 00000000000e1000 008:00005 bash
    00000000006ea000      24 rw--- 0000000000000000 000:00000   [ anon ]
    0000000001a8d000    5388 rw--- 0000000000000000 000:00000   [ anon ]
    00007f1c6c5cd000      48 r-x-- 0000000000000000 008:00005 libnss_files-2.15.so
    00007f1c6c5d9000    2044 ----- 000000000000c000 008:00005 libnss_files-2.15.so
    00007f1c6c7d8000       4 r---- 000000000000b000 008:00005 libnss_files-2.15.so
    00007f1c6c7d9000       4 rw--- 000000000000c000 008:00005 libnss_files-2.15.so
    00007f1c6c7da000      40 r-x-- 0000000000000000 008:00005 libnss_nis-2.15.so
    00007f1c6c7e4000    2048 ----- 000000000000a000 008:00005 libnss_nis-2.15.so
    00007f1c6c9e4000       4 r---- 000000000000a000 008:00005 libnss_nis-2.15.so
    00007f1c6c9e5000       4 rw--- 000000000000b000 008:00005 libnss_nis-2.15.so
    00007f1c6c9e6000      92 r-x-- 0000000000000000 008:00005 libnsl-2.15.so
    00007f1c6c9fd000    2044 ----- 0000000000017000 008:00005 libnsl-2.15.so
    00007f1c6cbfc000       4 r---- 0000000000016000 008:00005 libnsl-2.15.so
    00007f1c6cbfd000       4 rw--- 0000000000017000 008:00005 libnsl-2.15.so
    00007f1c6cbfe000       8 rw--- 0000000000000000 000:00000   [ anon ]
    00007f1c6cc00000      32 r-x-- 0000000000000000 008:00005 libnss_compat-2.15.so
    00007f1c6cc08000    2044 ----- 0000000000008000 008:00005 libnss_compat-2.15.so
    00007f1c6ce07000       4 r---- 0000000000007000 008:00005 libnss_compat-2.15.so
    00007f1c6ce08000       4 rw--- 0000000000008000 008:00005 libnss_compat-2.15.so
    00007f1c6ce09000    7052 r---- 0000000000000000 008:00005 locale-archive
    00007f1c6d4ec000    1748 r-x-- 0000000000000000 008:00005 libc-2.15.so
    00007f1c6d6a1000    2048 ----- 00000000001b5000 008:00005 libc-2.15.so
    00007f1c6d8a1000      16 r---- 00000000001b5000 008:00005 libc-2.15.so
    00007f1c6d8a5000       8 rw--- 00000000001b9000 008:00005 libc-2.15.so
    00007f1c6d8a7000      20 rw--- 0000000000000000 000:00000   [ anon ]
    00007f1c6d8ac000       8 r-x-- 0000000000000000 008:00005 libdl-2.15.so
    00007f1c6d8ae000    2048 ----- 0000000000002000 008:00005 libdl-2.15.so
    00007f1c6daae000       4 r---- 0000000000002000 008:00005 libdl-2.15.so
    00007f1c6daaf000       4 rw--- 0000000000003000 008:00005 libdl-2.15.so
    00007f1c6dab0000     136 r-x-- 0000000000000000 008:00005 libtinfo.so.5.9
    00007f1c6dad2000    2048 ----- 0000000000022000 008:00005 libtinfo.so.5.9
    00007f1c6dcd2000      16 r---- 0000000000022000 008:00005 libtinfo.so.5.9
    00007f1c6dcd6000       4 rw--- 0000000000026000 008:00005 libtinfo.so.5.9
    00007f1c6dcd7000     136 r-x-- 0000000000000000 008:00005 ld-2.15.so
    00007f1c6ded7000      12 rw--- 0000000000000000 000:00000   [ anon ]
    00007f1c6def0000      28 r--s- 0000000000000000 008:00005 gconv-modules.cache
    00007f1c6def7000       8 rw--- 0000000000000000 000:00000   [ anon ]
    00007f1c6def9000       4 r---- 0000000000022000 008:00005 ld-2.15.so
    00007f1c6defa000       8 rw--- 0000000000023000 008:00005 ld-2.15.so
    00007fffe147d000     132 rw--- 0000000000000000 000:00000   [ stack ]
    00007fffe15ff000       4 r-x-- 0000000000000000 000:00000   [ anon ]
    ffffffffff600000       4 r-x-- 0000000000000000 000:00000   [ anon ]
    mapped: 30276K    writeable/private: 5668K    shared: 28K

可以看出,这个进程的虚拟地址空间大小微30276k,实际占用的物理内存微5668k。

Bind Mount的使用

| Comments

如果你需要暂时修改一个配置文件用来测试,但是这个配置文件是read only的,你不想大费周折,怎么办?这时bind mount就可以派上用场。

mount命令的常规用法是将一个块设备上的文件系统挂载一个指定的路径。而bind选项可以将一个目录挂载到一个指定的路径。

假设我们需要临时修改一下config文件,但是当前用户没有权限修改这个文件:

1
2
3
4
5
$ ls /tmp/etc/
total 0
-rw-r----- 1 root root 0 Nov 12 10:38 config
$ sudo cat /tmp/etc/config 
sky=0

现在我们将/tmp/bind_dir挂载到/tmp/etc:

1
2
3
4
5
$ sudo mount --bind /tmp/bind_dir /tmp/etc
    $ ls /tmp/bind_dir/
    total 0
    $ ls /tmp/etc/
    total 0

现在/tmp/bind_dir被挂载到了/tmp/etc,也就是说访问/tmp/etc实际上是访问的是/etc/bind_dir目录。现在我们可以往/tmp/etc目录写入我们想要的修改:

1
2
3
4
$ touch /tmp/etc/config
    $ echo "tmp=1" >> /tmp/etc/config
    $ cat /tmp/etc/config
    tmp=1

现在就达到了修改/tmp/etc/config的目的,可以执行测试。测试完毕后,执行umount:

1
$ sudo umount /tmp/etc/

/tmp/etc目录下的内容没有变化:

1
2
3
4
5
6
$ sudo umount /tmp/etc/
$ ls /tmp/etc/
total 4.0K
-rw-r----- 1 root root 6 Nov 12 10:41 config
$ sudo cat /tmp/etc/config
sky=0

mount的过程实际上是inode被替换的过程,这里我们将/tmp/bind_dir挂载到/tmp/etc上,实际上的实现过程是将/tmp/etc的dentry目录项所指向的inode重定向到/tmp/bind_dir的inode索引节点,也就是说让/tmp/bind_dir和/tmp/etc指向同一个inode节点:

1
2
3
$ ls -lid /tmp/bind\_dir/ /tmp/etc/
1094756 drwxrwxr-x 2 calvin calvin 4.0K Nov 12 10:47 /tmp/bind\_dir/
1094756 drwxrwxr-x 2 calvin calvin 4.0K Nov 12 10:47 /tmp/etc/

可见两个路径都指向了1094756的inode索引节点。

另外几个应用bind mount的例子:
* http://docs.1h.com/Bind_mounts
* http://backdrift.org/how-to-use-bind-mounts-in-linux

Reference

ext3 mount过程

禁用内核进程地址随机

| Comments

今天接触了Linux下的VDSO(Virtual Dynamically-linked Shared Object),用来使系统调用更加快速和高效,但在查看进程vdso.so在进程地址空间的分布时,发现每次打印出来的进程地址都不一样:

$ ldd /bin/ls
    linux-vdso.so.1 =>  (0x00007ffff7ffe000)
    libselinux.so.1 => /lib/x86\_64-linux-gnu/libselinux.so.1 (0x00007ffff7dc0000)
    librt.so.1 => /lib/x86\_64-linux-gnu/librt.so.1 (0x00007ffff7bb8000)
    libacl.so.1 => /lib/x86\_64-linux-gnu/libacl.so.1 (0x00007ffff79af000)
    libc.so.6 => /lib/x86\_64-linux-gnu/libc.so.6 (0x00007ffff75ef000)
    libdl.so.2 => /lib/x86\_64-linux-gnu/libdl.so.2 (0x00007ffff73eb000)
    /lib64/ld-linux-x86-64.so.2 (0x0000555555554000)
    libpthread.so.0 => /lib/x86\_64-linux-gnu/libpthread.so.0 (0x00007ffff71cd000)
    libattr.so.1 => /lib/x86\_64-linux-gnu/libattr.so.1 (0x00007ffff6fc8000)
$ cat /proc/self/maps 
00400000-0040b000 r-xp 00000000 08:05 1438995                            /bin/cat
0060a000-0060b000 r--p 0000a000 08:05 1438995                            /bin/cat
0060b000-0060c000 rw-p 0000b000 08:05 1438995                            /bin/cat
0060c000-0062d000 rw-p 00000000 00:00 0                                  [heap]
7ffff7337000-7ffff7a1a000 r--p 00000000 08:05 137790                     /usr/lib/locale/locale-archive
7ffff7a1a000-7ffff7bcf000 r-xp 00000000 08:05 407680                     /lib/x86\_64-linux-gnu/libc-2.15.so
7ffff7bcf000-7ffff7dcf000 ---p 001b5000 08:05 407680                     /lib/x86\_64-linux-gnu/libc-2.15.so
7ffff7dcf000-7ffff7dd3000 r--p 001b5000 08:05 407680                     /lib/x86\_64-linux-gnu/libc-2.15.so
7ffff7dd3000-7ffff7dd5000 rw-p 001b9000 08:05 407680                     /lib/x86\_64-linux-gnu/libc-2.15.so
7ffff7dd5000-7ffff7dda000 rw-p 00000000 00:00 0 
7ffff7dda000-7ffff7dfc000 r-xp 00000000 08:05 407692                     /lib/x86\_64-linux-gnu/ld-2.15.so
7ffff7fd9000-7ffff7fdc000 rw-p 00000000 00:00 0 
7ffff7ff9000-7ffff7ffb000 rw-p 00000000 00:00 0 
7ffff7ffb000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00022000 08:05 407692                     /lib/x86\_64-linux-gnu/ld-2.15.so
7ffff7ffd000-7ffff7fff000 rw-p 00023000 08:05 407692                     /lib/x86\_64-linux-gnu/ld-2.15.so
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

原来在现代Linux内核中,为了安全起见,会对每个进程的进程地址进行随机化。用以下命令即可禁用这个功能:

echo "0" > /proc/sys/kernel/randomize_va_space

Chromium Base库介绍

| Comments

Chromium的base库包含了一些常用的基础代码,包括:

* 文件操作
* json读写
* 字符串操作
* 时间操作
* 智能指针

在线代码浏览: https://code.google.com/p/chromium/codesearch#chromium/src/base/

常用宏

  1. DISALLOW_COPY/DISALLOW_ASSIGN/DISALLOW_COPY_AND_ASSIGN 位于base/basictyps.h,它通过将拷贝构造函数和=运算符重载设置为private来禁用类对象的拷贝和赋值操作。 示例用法:sql/transaction.h

智能指针

scoped_ptr: base/memory/scoped_ptr.h 经过scoped_ptr封装的指针在出作用域后自动被释放, 该指针的ownership只能传递,不能拷贝。使用方法见头文件的注释。

文件操作

base/platform_file.h base/file_util.h base/files/file_path.h base/path_service.h

Strcpy和memcpy的区别

| Comments

今天被问到strcpy和memcpy的区别,一时语塞,于是研究一下记录下来。

strcpy

   #include <string.h>
   char *strcpy(char *dest, const char *src);

它的作用是将src指向的字符串,包括最后的’\0’,拷贝到dest指向的字符串。它的一个简单实现如下:

       char*
       strncpy(char *dest, const char *src, size_t n){
           size_t i;

           for (i = 0 ; i < n && src[i] != '\0' ; i++)
               dest[i] = src[i];
           for ( ; i < n ; i++)
               dest[i] = '\0';

           return dest;
       }

也就是说,strcpy会从头开始依次拷贝每个字符,但是一旦遇到了空字符NULL(’\0’),strcpy就会停止拷贝。

memcpy

   #include <string.h>
   void *memcpy(void *dest, const void *src, size_t n);

memcpy用来进行内存拷贝,它将src指向的地址后的n个字节拷贝到dest开始的内存里,可以用它拷贝任何内型的数据对象。


以下是stackoverflow上的一个demo,它清楚的显示了这两个函数的区别:

void dump5(char *str);

int main()
{
    char s[5]={'s','a','\0','c','h'};

    char membuff[5]; 
    char strbuff[5];
    memset(membuff, 0, 5); // init both buffers to nulls
    memset(strbuff, 0, 5);

    strcpy(strbuff,s);
    memcpy(membuff,s,5);

    dump5(membuff); // show what happened
    dump5(strbuff);

    return 0;
}

void dump5(char *str)
{
    char *p = str;
    for (int n = 0; n < 5; ++n)
    {
        printf("%2.2x ", *p);
        ++p;
    }

    printf("\t");

    p = str;
    for (int n = 0; n < 5; ++n)
    {
        printf("%c", *p ? *p : ' ');
        ++p;
    }

    printf("\n", str);
}

结果输出:

73 61 00 63 68  sa ch
73 61 00 00 00  sa

可见用strcpy拷贝时,ch被忽略了,但是memcpy也将它拷贝过来了。

详细讨论在这里

Eclipse 调试中的五种断点

| Comments

Eclipse调试支持五种不同的断点,这些断点都可以通过Run主菜单下的选项来添加或者删除。

Line breakpoints

Line breakpoints就是我们平时使用最多的一种断点类型,它以代码行为单位,代码执行到这一行时就会被suspend。有两个比较有用的属性:

  • Condition breakpoint 为该断点添加一个条件,只有当这个条件成立时才会suspend
  • Hit count 指定一个整数值N,只有当这个断点被执行过的次数达到N次时才会suspend。

Watchpoint

Watchpoint针对的是变量。如果我们对程序运行的过程不关心,而对某个关键变量的值的变化比较关心,那么我们可以对这个变量设置一个Watchpoint, 同时指定它的Access和Modification属性用来执行这个变量被访问时suspend还是被修改时suspend。

注意:
Android的Dalvik虚拟机目前还不支持这种breakpoint。

Method breakpoints

Method breakpoints用来指定针对一个方法的断点。他有两个属性:

  • Entry:当进入该方法时suspend
  • Exit:当执行玩该方法时suspend

Exception breakpoints

指定当某个exception发生时suspend当前线程。

Class Load breakpoints

指定当某个类被虚拟机加载时suspend当前线程。

以上五种断点都可以在Breakpoints 视图中查看,可以通过右上角的菜单,Group by,选择Breakpoint types,这样可以按照这五种类型来对当前所有的Breakpoint进行分类查看,比较方便。

/images/eclipse-debug.png

Reference

Eclipse Content Assist失效解决

| Comments

今天发现eclipse的conent assist的默认键Ctrl + Space与系统输入法冲突,于是打开Window –> Preference –> Keys,将content assist改为Alt + /,然而发现还是不起作用,最后找到解决方法:
Window –> Preference –> Keys –> Word Completion –> Unbind Command
然后apply。

Android消息处理机制理解

| Comments

Android的消息处理机制是很多功能实现的基础,如UI绘制,键盘事件传递等等。在实现上,涉及到的类有Handler, Message, Looper等等,本篇研究Android消息处理机制的内部实现细节。

UI线程消息循环的启动

Android所谓UI线程的启动位于ActivityThread.java,在其main方法中会启动这个消息循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
    ...

    Looper.prepareMainLooper();
    if (sMainThreadHandler == null) {
        sMainThreadHandler = new Handler();
    }

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    Looper.loop();
}

首先调用Looper.prepareMainLooper()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    /**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
    public static void prepareMainLooper() {
        prepare();
        setMainLooper(myLooper());
        // main looper是不允许退出的,否则应用程序就没得玩了
        myLooper().mQueue.mQuitAllowed = false;
    }

     /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      * 
      * prepare方法将实例化一个Looper对象,并保存到一个ThreadLocal变量里, 这个
      * looper稍后可以通过myLooper()方法取出
      */
    public static void prepare() {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper());
    }

    private Looper() {
        // 初始化一个消息队列,其实现稍后解释
        mQueue = new MessageQueue();
        mRun = true;
        mThread = Thread.currentThread();
    }

接下来调用Looper.loop()启动消息循环:

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    // 取得当前线程关联的looper对象
    Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    MessageQueue queue = me.mQueue;
    ...
    while (true) {
        // 从消息队列中取出下一个需要处理的消息,这里可能会阻塞
        Message msg = queue.next(); // might block
        if (msg != null) {
            if (msg.target == null) {
                // No target is a magic identifier for the quit message.
                return;
            }
            ...
            msg.target.dispatchMessage(msg);
            ...
            msg.recycle();
        }
    }
}

消息队列的建立

每一个looper对象对应一个MessageQueue对象,Looper对象在这个消息队列上loop。每个消息是一个Message对象,而对这个消息队列的引用也是一个Message对象:

Message mMessages;

每个Message对象内部也有一个Message对象的引用,指向队列中的下一个message对象,这些message形成了一个单向队列。这个队列是按照message.when的大小顺序排列的。队首的消息是最先发生的。

MessageQueue另外还有一个重要的变量:

private int mPtr; // used by native code

每个MessageQueue在native层对应有一个C++的实现NativeMessageQueue,位于android_os_MessageQueue.cpp,Java层的MessageQueue的mPtr保存的就是这个对象的指针。

MessageQueue的构造方法为:

MessageQueue() {
    nativeInit();
}

nativeInit()方法在android_os_MessageQueue.cpp中实现:

static void android_os_MessageQueue_nativeInit(JNIEnv* env, jobject obj) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (! nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return;
    }

    // 将指针值设置给Java层MessageQueue的mPtr变量
    android_os_MessageQueue_setNativeMessageQueue(env, obj, nativeMessageQueue);
}

NativeMessageQueue没有太多逻辑实现,它作为C++层的Looper对象的包装类。

C++层的Looper对象实现为framework/base/libs/utils/Looper.cpp:

Looper::Looper(bool allowNonCallbacks) :
       mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
       mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
   int wakeFds[2];
   int result = pipe(wakeFds);
   LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe.  errno=%d", errno);

   mWakeReadPipeFd = wakeFds[0];
   mWakeWritePipeFd = wakeFds[1];

   result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
   LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking.  errno=%d",
           errno);

   result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
   LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking.  errno=%d",
           errno);

   // Allocate the epoll instance and register the wake pipe.
   mEpollFd = epoll_create(EPOLL_SIZE_HINT);
   LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance.  errno=%d", errno);

   struct epoll_event eventItem;
   memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
   eventItem.events = EPOLLIN;
   eventItem.data.fd = mWakeReadPipeFd;
   result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
   LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance.  errno=%d",
           errno);
}

这里先通过pipe系统调用创建了一个管道,这个管道非常重要:
当当前线程没有新的消息需要处理时,它会睡眠在管道的读端文件描述符上,直到有新消息到来为止;另一方面,当其他线程向这个线程的消息队列发送一个消息后,其他线程会在这个管道的写端文件描述符上写入数据,这样导致等待在读端文件描述符的looper唤醒,然后对消息队列中的消息进行处理。但是,它对其他线程写入写端文件描述符的数据是什么并不关心,因为这些数据仅仅是为了唤醒它而已。

开启消息循环

调用Looper.loop()开启消息循环,前面看到,loop()方法从next()#MessageQueue获取下一个待处理的消息:

final Message next() {
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;

    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        // 这个方法可能会阻塞,一旦返回,说明有新的message可以处理了,第一次进来nextPollTimeoutMillis为0,表示不作等待,立即返回。
        nativePollOnce(mPtr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            final Message msg = mMessages;
            if (msg != null) {
                final long when = msg.when;
                // 如果队首的消息的when小于当前时间,说明这个消息已经过期了,需要马上处理。
                if (now >= when) {
                    mBlocked = false;
                    mMessages = msg.next;
                    msg.next = null;
                    if (false) Log.v("MessageQueue", "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                } else {
                    // 计算出还要睡眠多长时间以后再取出下一个消息
                    nextPollTimeoutMillis = (int) Math.min(when - now, Integer.MAX_VALUE);
                }
            } else {
                nextPollTimeoutMillis = -1;
            }

            ...

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

nativePollOnce(mPtr, nextPollTimeoutMillis); 方法用来检查当前线程的消息队列是否有新的消息需要处理, nextPollTimeoutMillis表示如果没有发现新的消息,当前线程需要睡眠的时间,如果等于-1,表示它需要进入无限睡眠,直到被其他线程唤醒为止。

nativePollOnce函数在C++层的Looper对象的实现为pollOnce(),进而调用pollInner():

int Looper::pollInner(int timeoutMillis) {
    // Poll.
    int result = ALOOPER_POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;

    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    ...
    // 检查是哪个文件描述符上发生了IO事件
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeReadPipeFd) {
            if (epollEvents & EPOLLIN) {
                awoken();
            } else {
                LOGW("Ignoring unexpected epoll events 0x%x on wake read pipe.", epollEvents);
            }
        } else {
        ...
        }
    }
    return result;
}

如果是mWakeReadPipeFd上发生了IO事件,说明有其它线程在mWakeWritePipeFd上写入了数据,接下来在awoken()函数中读取这些数据,这样在后续有线程写入数据时可以被再次唤醒:

void Looper::awoken() {
    ... 
    char buffer[16];
    ssize_t nRead;
    do {
        nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
    } while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));
}

这里只是将数据读出,它并不关心这些数据是什么。

消息发送过程

发送消息最常见的方法就是使用sendMessage()#Handler, 这个方法最终会调用enqueueMessage()#MessageQueue.java:

final boolean enqueueMessage(Message msg, long when) {

        msg.when = when;
        //Log.d("MessageQueue", "Enqueing: " + msg);
        Message p = mMessages;
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked; // new head, might need to wake up
        } else {
            Message prev = null;
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
            msg.next = prev.next;
            prev.next = msg;
            needWake = false; // still waiting on head, no need to wake up
        }
    }
    if (needWake) {
        nativeWake(mPtr);
    }
    return true;
}

这个方法将新的message插入到消息队列中,并根据需要唤醒native的looper线程。消息的插入分为四种情况:

  • 消息队列是一个空队列;
  • 新消息的when等于0,表示需要立即处理;
  • 新消息的when小于队首消息的when;
  • 新消息的when大于或者等于队首消息的when;

前三种情况只需要将新消息插入到队首即可,并需要立即唤醒looper对这个新消息进行处理。第四种情况需要插入到队列中的合适位置,并且不需要唤醒looper。
唤醒looper通过nativeWake()方法实现: looper.cpp:

void Looper::wake() {
    ssize_t nWrite;
    do {
        nWrite = write(mWakeWritePipeFd, "W", 1);
    } while (nWrite == -1 && errno == EINTR);

    if (nWrite != 1) {
        if (errno != EAGAIN) {
            LOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

唤醒的过程就是望管道的写文件描述符mWakeWritePipeFd写入一些数据即可,这里写入了一个”W”字符,这样等待在管道另一端的正在睡眠的线程就会被唤醒,从而导致队首的消息被取出进行处理。

HanderThread

HandlerThread最重要的特点是它的looper是在一个子线程中loop的,从而不会阻塞UI线程。