Android的logger机制分析
Logger机制是在Android系统中提供的一个轻量级的日志系统,这个日志系统是以驱动程序的形式在内核空间实现的,在用户空间分别提供了Java接口和C/C++接口来使用这个日志系统,使用的接口取决于编写的是Android应用程序还是系统组件。
下面我准备从应用开发和源码分析两部分来分析安卓的Logger机制。
这一部分将简要地介绍一下在Android应用程序开发中Log的使用方法
在程序开发过程中,LOG是广泛使用的用来记录程序执行过程的机制,它既可以用于程序调试,也可以用于产品运营中的事件记录。
在Android系统中,提供了简单、便利的LOG机制,开发人员可以方便地使用。下面我将介绍在Android内核空间和用户空间中LOG的使用和查看方法。
Android内核是基于Linux Kernel 2.36的,因此,Linux Kernel的LOG机制同样适合于Android内核,这就是与C语言的printf齐名的printk。与printf类似,printk提供格式化输入功能,同时,它也具有所有LOG机制的特点——提供日志级别过虑功能。
printk提供了8种日志级别( linux/kernel.h ):
在Android系统中,printk输出的日志信息保存在/proc/kmsg中,要查看/proc/kmsg的内容,需要在后台中运行模拟器: USER-NAME@MACHINE-NAME:~/Android$ emulator 启动adb shell工具: USER-NAME@MACHINE-NAME:~/Android$ adb shell 查看/proc/kmsg文件: root@android:/# cat /proc/kmsg
2.3用户空间程序开发时LOG的使用
Android系统在用户空间中提供了轻量级的logger日志系统,它是在内核中实现的一种设备驱动,与用户空间的logcat工具配合使用能够方便地跟踪调试程序。在Android系统中,分别为C/C++和Java语言提供两种不同的logger访问接口。
C/C++日志接口一般是在编写硬件抽象层模块或者编写JNI方法时使用,而Java接口一般是在应用层编写APP时使用。
Android系统中的C/C++日志接口是通过宏来使用的。
在system/core/include/android/log.h定义了日志的级别:
* This is the local tag used for the following simplified
* logging macros. You can change this preprocessor definition
* before using the other macros to change the tag.
*/
#ifndef LOG_TAG
#define LOG_TAG NULL
#endif
/*
* Simplified macro to send a verbose log message using the current LOG_TAG.
*/
#ifndef LOGV
#if LOG_NDEBUG
#define LOGV(...) ((void)0)
#else
#define LOGV(...) ((void)LOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__))
#endif
#endif
/*
* Basic log message macro.
*
* Example:
* LOG(LOG_WARN, NULL, "Failed with error %d", errno);
*
* The second argument may be NULL or "" to indicate the "global" tag.
*/
#ifndef LOG
#define LOG(priority, tag, ...) \
LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__)
#endif
/*
* Log macro that allows you to specify a number for priority.
*/
#ifndef LOG_PRI
#define LOG_PRI(priority, tag, ...) \
android_printLog(priority, tag, __VA_ARGS__)
#endif
/*
* ================================================================
*
* The stuff in the rest of this file should not be used directly.
*/
#define android_printLog(prio, tag, fmt...) \
__android_log_print(prio, tag, fmt)
因此,如果要使用C/C++日志接口,只要定义自己的LOG_TAG宏和包含头文件system/core/include/cutils/log.h就可以了:
例如使用LOGV:
LOGV("This is the log printed by LOGV in androiduser space.");
2.32 JAVA日志接口 Android系统在Frameworks层中定义了Log接口(frameworks/base/core/java/android/util/Log.java):
因此,如果要使用Java日志接口,只要在类中定义的LOG_TAG常量和引用android.util.Log就可以了:
private static final String LOG_TAG ="MY_LOG_TAG"; Log.i(LOG_TAG, "This is the log printed by Log.i inandroid user space."); Log.e(LOG_TAG, "This is the logprinted by Log.e in android user space.");
要查看这些LOG的输出,可以配合logcat工具。如果是在Eclipse环境(ADT)下运行,直接在Eclipse就可以查看了:
如果是在自己编译的Android源代码工程中使用,则在后台中运行模拟器:
启动adb shell工具: USER-NAME@MACHINE-NAME:~/Android$ adb shell
使用logcat命令查看日志: root@android:/ # logcat
这样就可以看到输出的日志了。
以上是Logger在应用开发中的使用的简单分析,除了Logger在应用开发中的使用,我将更进一步地分析Logger驱动程序的源代码,更加深刻的认识Android日志系统。
因为Android日志系统是以驱动程序的形式实现在内核空间的,所以需要获取Android内核源代码来分析。在下载好Android源代码工程中,Logger驱动程序主要由两个文件构成,分别是:
kernel/common/drivers/staging/android/logger.h
kernel/common/drivers/staging/android/logger.c
接下来,我将首先分析Logger驱动程序的相关数据结构,然后对Logger驱动程序源代码的使用情景进行分析,比如日志系统初始化情景、日志读取情景和日志写入情景。
#define LOGGER_ENTRY_MAX_LEN (4*1024)
#define LOGGER_ENTRY_MAX_PAYLOAD \
(LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))
#define __LOGGERIO 0xAE
#define LOGGER_GET_LOG_BUF_SIZE _IO(__LOGGERIO, 1) /* size of log */
#define LOGGER_GET_LOG_LEN _IO(__LOGGERIO, 2) /* used log len */
#define LOGGER_GET_NEXT_ENTRY_LEN _IO(__LOGGERIO, 3) /* next entry len */
#define LOGGER_FLUSH_LOG _IO(__LOGGERIO, 4) /* flush log */
#endif/* _LINUX_LOGGER_H */
len成员变量记录了这条记录的有效负载的长度,有效负载指定的日志记录本身的长度,但是不包括用于描述这个记录的struct logger_entry结构体。我们调用android.util.Log接口来使用日志系统时,会指定日志的优先级别Priority、Tag字符串以及Msg字符串,Priority + Tag + Msg三者内容的长度加起来就是记录的有效负载长度。
__pad成员变量是用来对齐结构体的。
pid和tid成员变量分别用来记录是哪条进程写入了这条记录。
sec和nsec成员变量记录日志写的时间。
msg成员变量记录有效负载的内容,它的大小由len成员变量来确定。
#define LOGGER_ENTRY_MAX_LEN (4*1024)
#define LOGGER_ENTRY_MAX_PAYLOAD \
(LOGGER_ENTRY_MAX_LEN - sizeof(struct logger_entry))从这两个宏可以看出,每条日志记录的有效负载长度加上结构体logger_entry的长度不能超过4K个字节。
logger.h文件中还定义了其它宏,这里就不一一分析了。
再来看logger.c文件中,其它相关数据结构的定义:
3.22 logger.c中的相关数据结构
* struct logger_log - represents a specific log, such as main or radio
*
* This structure lives from module insertion until module removal, so it does
* not need additional reference counting. The structure is protected by the
* mutex mutex.
*/
struct logger_log {
unsignedchar* buffer;/* the ring buffer itself */
struct miscdevice misc; /* misc device representing the log */
wait_queue_head_t wq;/* wait queue for readers */
struct list_head readers;/* this logs readers */
struct mutex mutex; /* mutex protecting buffer */
size_t w_off; /* current write head offset */
size_t head; /* new readers start here */
size_t size; /* size of the log */};
/*
* struct logger_reader - a logging device open for reading
*
* This object lives from open to release, so we dont need additional
* reference counting. The structure is protected by log- mutex.
*/
struct logger_reader {
struct logger_log* log; /* associated log */
struct list_head list; /* entry in logger_logs list */
size_t r_off; /* current read head offset */};
/* logger_offset - returns index n into the log via (optimized) modulus */
#define logger_offset(n) ((n) (log- size - 1))
buffer成员变量是用于保存日志信息的内存缓冲区,它的大小由size成员变量确定。
从misc成员变量可以看出,logger驱动程序使用的设备属于misc类型的设备,通过在Android模拟器上执行cat /proc/devices命令,可以看出,misc类型设备的主设备号是10。
wq成员变量是一个等待队列,用于保存正在等待读取日志的进程。
readers成员变量用来保存当前正在读取日志的进程,正在读取日志的进程由结构体logger_reader来描述。
mutex成员变量是一个互斥量,用来保护log的并发访问。因为可以看出,这里的日志系统的读写问题,其实是一个生产者-消费者的问题,因此,需要互斥量来保护log的并发访问。
w_off成员变量用来记录下一条日志应该从哪里开始写。
head成员变量用来表示打开日志文件中,应该从哪一个位置开始读取日志。
struct logger_reader是用来表示一个读取日志的进程的结构体,log成员变量指向要读取的日志缓冲区。list成员变量用来连接其它读者进程。r_off成员变量表示当前要读取的日志在缓冲区中的位置。
struct logger_log结构体中用于保存日志信息的内存缓冲区buffer是一个循环使用的环形缓冲区,缓冲区中保存的内容是以struct logger_entry为单位的,每个单位的组成为:
structlogger_entry | priority | tag | msg
由于是内存缓冲区buffer是一个循环使用的环形缓冲区,给定一个偏移值,它在buffer中的位置由下logger_offset来确定:
#definelogger_offset(n) ((n) (log- size -1))
logger.c文件中定义了三个日志设备:
/*
* Defines a log structure with name NAME and a size of SIZE bytes, which
* must be a power of two, greater than LOGGER_ENTRY_MAX_LEN, and less than
* LONG_MAX minus LOGGER_ENTRY_MAX_LEN.
*/
#define DEFINE_LOGGER_DEVICE(VAR, NAME, SIZE) \
static unsigned char _buf_ ## VAR[SIZE]; \
static struct logger_log VAR = { \
.buffer = _buf_ ## VAR, \
.misc = { \
.minor = MISC_DYNAMIC_MINOR, \
.name = NAME, \
.fops = logger_fops, \
.parent = NULL, \
}, \
.wq = __WAIT_QUEUE_HEAD_INITIALIZER(VAR .wq), \
.readers = LIST_HEAD_INIT(VAR .readers), \
.mutex = __MUTEX_INITIALIZER(VAR .mutex), \
.w_off = 0, \
.head = 0, \
.size = SIZE, \
};
DEFINE_LOGGER_DEVICE(log_main, LOGGER_LOG_MAIN,64*1024)
DEFINE_LOGGER_DEVICE(log_events, LOGGER_LOG_EVENTS,256*1024)
DEFINE_LOGGER_DEVICE(log_radio, LOGGER_LOG_RADIO,64*1024)
分别是log_main、log_events和log_radio,名称分别LOGGER_LOG_MAIN、LOGGER_LOG_EVENTS和LOGGER_LOG_RADIO,它们的次设备号为MISC_DYNAMIC_MINOR,即为在注册时动态分配。
在logger.h文件中,有这三个宏的定义:
#defineLOGGER_LOG_RADIO
"log_radio" /* radio-related messages */
#define LOGGER_LOG_EVENTS "log_events" /* system/hardware events */
#define LOGGER_LOG_MAIN "log_main" /* everything else */
注释说明了这三个日志设备的用途。
注册的日志设备文件操作方法为logger_fops:
logger_init函数通过调用init_log函数来初始化了上述提到的三个日志设备:
init_log函数主要调用了misc_register函数来注册misc设备,misc_register函数定义在kernel/common/drivers/char/misc.c文件中:
mutex_unlock( misc_mtx);
return-EBUSY;
}
}
if(misc- minor== MISC_DYNAMIC_MINOR){
int i= DYNAMIC_MINORS;
while(--i =0)
if((misc_minors[i 3] (1 (i 7)))==0)
break;
if(i 0){
mutex_unlock( misc_mtx);
return-EBUSY;
}
misc- minor= i;
}
if(misc- minor DYNAMIC_MINORS)
misc_minors[misc- minor 3]|=1 (misc- minor 7);
dev = MKDEV(MISC_MAJOR, misc- minor);
misc- this_device= device_create(misc_class, misc- parent, dev,NULL,
"%s", misc- name);
if(IS_ERR(misc- this_device)){
err = PTR_ERR(misc- this_device);
goto out;
}
/*
* Add it to the front, so that later devices can "override"
* earlier defaults
*/
list_add( misc- list, misc_list);
out:
mutex_unlock( misc_mtx);
return err;
}
注册完成后,通过device_create创建设备文件节点。这里,将创建/dev/log/main、/dev/log/events和/dev/log/radio三个设备文件,这样,用户空间就可以通过读写这三个文件和驱动程序进行交互。
*
* Optimal read size is LOGGER_ENTRY_MAX_LEN. Will set errno to EINVAL if read
* buffer is insufficient to hold next entry.
*/
static ssize_t logger_read(struct file *file,char __user*buf,
size_t count, loff_t*pos)
{
struct logger_reader*reader= file- private_data;
struct logger_log*log= reader- log;
ssize_t ret;
DEFINE_WAIT(wait);
start:
while(1){
prepare_to_wait( log- wq, wait, TASK_INTERRUPTIBLE);
mutex_lock( log- mutex);
ret =(log- w_off== reader- r_off);
mutex_unlock( log- mutex);
if(!ret)
break;
if(file- f_flags O_NONBLOCK){
ret =-EAGAIN;
break;
}
if(signal_pending(current)){
ret =-EINTR;
break;
}
schedule();
}
finish_wait( log- wq, wait);
if(ret)
return ret;
mutex_lock( log- mutex);
/* is there still something to read or did we race? */
if(unlikely(log- w_off== reader- r_off)){
mutex_unlock( log- mutex);
goto start;
}
/* get the size of the next entry */
ret = get_entry_len(log, reader- r_off);
if(count ret){
ret =-EINVAL;
goto out;
}
/* get exactly one entry from the log */
ret = do_read_log_to_user(log, reader, buf, ret);
out:
mutex_unlock( log- mutex);
return ret;
}
需要注意的是,在函数开始的地方,表示读取日志上下文的structlogger_reader是保存在文件指针的private_data成员变量里面的,这是在打开设备文件时设置的,设备文件打开方法为logger_open:
*
* Note how near a no-op this is in the write-only case. Keep it that way!
*/
staticint logger_open(struct inode *inode,struct file*file)
{
struct logger_log*log;
int ret;
ret = nonseekable_open(inode, file);
if(ret)
return ret;
log = get_log_from_minor(MINOR(inode- i_rdev));
if(!log)
return-ENODEV;
if(file- f_mode FMODE_READ){
struct logger_reader*reader;
reader = kmalloc(sizeof(struct logger_reader), GFP_KERNEL);
if(!reader)
return-ENOMEM;
reader- log= log;
INIT_LIST_HEAD( reader- list);
mutex_lock( log- mutex);
reader- r_off= log- head;
list_add_tail( reader- list, log- readers);
mutex_unlock( log- mutex);
file- private_data= reader;
}else
file- private_data= log;
return0;
}
新打开日志设备文件时,是从log- head位置开始读取日志的,保存在struct logger_reader的成员变量r_off中。
如果没有新的日志可读,并且设备文件不是以非阻塞O_NONBLOCK的方式打开或者这时有信号要处理(signal_pending(current)),那么就直接返回,不再等待新的日志写入。判断当前是否有新的日志可读的方法是: ret = (log- w_off == reader- r_off);
即判断当前缓冲区的写入位置和当前读进程的读取位置是否相等,如果不相等,则说明有新的日志可读。
之后的代码表示,如果有新的日志可读,那么就首先通过get_entry_len来获取下一条可读的日志记录的长度,从这里可以看出,日志读取进程是以日志记录为单位进行读取的,一次只读取一条记录。get_entry_len的函数实现如下:
/*
* get_entry_len - Grabs the length of the payload of the next entry starting
* from off.
*
* Caller needs to hold log- mutex.
*/
static __u32 get_entry_len(struct logger_log *log, size_t off)
{
__u16 val;
switch(log- size- off){
case1:
memcpy( val, log- buffer+ off,1);
memcpy(((char*) val)+1, log- buffer,1);
break;
default:
memcpy( val, log- buffer+ off,2);
}
returnsizeof(struct logger_entry)+ val;
}
上面提到,每一条日志记录是由两大部分组成的,一个用于描述这条日志记录的结构体struct logger_entry,另一个是记录体本身,即有效负载。结构体structlogger_entry的长度是固定的,只要知道有效负载的长度,就可以知道整条日志记录的长度了。而有效负载的长度是记录在结构体struct logger_entry的成员变量len中,而len成员变量的地址与struct logger_entry的地址相同,因此,只需要读取记录的开始位置的两个字节就可以了。
又由于日志记录缓冲区是循环使用的,这两个节字有可能是第一个字节存放在缓冲区最后一个字节,而第二个字节存放在缓冲区的第一个节,除此之外,这两个字节都是连在一起的。因此,分两种情况来考虑,对于前者,分别通过读取缓冲区最后一个字节和第一个字节来得到日志记录的有效负载长度到本地变量val中,对于后者,直接读取连续两个字节的值到本地变量val中。
这两种情况是通过判断日志缓冲区的大小和要读取的日志记录在缓冲区中的位置的差值来区别的,如果相差1,就说明是前一种情况了。
最后,把有效负载的长度val加上struct logger_entry的长度就得到了要读取的日志记录的总长度了。
/*
* We read from the log in two disjoint operations. First, we read from
* the current read head offset up to count bytes or to the end of
* the log, whichever comes first.
*/
len = min(count, log- size- reader- r_off);
if(copy_to_user(buf, log- buffer+ reader- r_off, len))
return-EFAULT;
/*
* Second, we read any remaining bytes, starting back at the head of
* the log.
*/
if(count!= len)
if(copy_to_user(buf+ len, log- buffer, count - len))
return-EFAULT;
reader- r_off= logger_offset(reader- r_off+ count);
return count;
}
这个函数简单地调用copy_to_user函数来把位于内核空间的日志缓冲区指定的内容拷贝到用户空间的内存缓冲区就可以了,同时,把当前读取日志进程的上下文信息中的读偏移r_off前进到下一条日志记录的开始的位置上。
logger.c文件,注册的写入日志设备文件的方法为logger_aio_write:
/*
* logger_aio_write - our write method, implementing support for write(),
* writev(), and aio_write(). Writes are our fast path, and we try to optimize
* them above all else.
*/
ssize_t logger_aio_write(struct kiocb *iocb,conststruct iovec*iov,
unsignedlong nr_segs, loff_t ppos)
{
struct logger_log*log= file_get_log(iocb- ki_filp);
size_t orig = log- w_off;
struct logger_entry header;
struct timespec now;
ssize_t ret =0;
now = current_kernel_time();
header.pid= current- tgid;
header.tid= current- pid;
header.sec= now.tv_sec;
header.nsec= now.tv_nsec;
header.len= min_t(size_t, iocb- ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
/* null writes succeed, return zero */
if(unlikely(!header.len))
return0;
mutex_lock( log- mutex);
/*
* Fix up any readers, pulling them forward to the first readable
* entry after (what will be) the new write offset. We do this now
* because if we partially fail, we can end up with clobbered log
* entries that encroach on readable buffer.
*/
fix_up_readers(log,sizeof(struct logger_entry)+ header.len);
do_write_log(log, header,sizeof(struct logger_entry));
while(nr_segs-- 0){
size_t len;
ssize_t nr;
/* figure out how much of this vector we can keep */
len = min_t(size_t, iov- iov_len, header.len- ret);
/* write out this segments payload */
nr = do_write_log_from_user(log, iov- iov_base, len);
if(unlikely(nr 0)){
log- w_off= orig;
mutex_unlock( log- mutex);
return nr;
}
iov++;
ret += nr;
}
mutex_unlock( log- mutex);
/* wake up any blocked readers */
wake_up_interruptible( log- wq);
return ret;
}
输入的参数iocb表示io上下文,iov表示要写入的内容,长度为nr_segs,表示有nr_segs个段的内容要写入。我们知道,每个要写入的日志的结构形式为:
struct logger_entry | priority | tag | msg其中, priority、tag和msg这三个段的内容是由iov参数从用户空间传递下来的,分别对应iov里面的三个元素。而logger_entry是由内核空间来构造的: struct logger_entry header;
structtimespec now;
now =current_kernel_time();
header.pid= current- tgid;
header.tid= current-
header.sec= now.tv_sec;
header.nsec= now.tv_nsec;
header.len= min_t(size_t, iocb- ki_left, LOGGER_ENTRY_MAX_PAYLOAD);
然后调用do_write_log首先把logger_entry结构体写入到日志缓冲区中:
接着,通过一个while循环把iov的内容写入到日志缓冲区中,也就是日志的优先级别priority、日志Tag和日志主体Msg:
* Fix up any readers, pulling them forward to the first readable
* entry after (what will be) the new write offset. We do this now
* because if we partially fail, we can end up with clobbered log
* entries that encroach on readable buffer.
*/
fix_up_readers(log,sizeof(struct logger_entry)+ header.len);
为什么要调用fix_up_reader这个函数呢?这个函数又是作什么用的呢?这是由于日志缓冲区是循环使用的,即旧的日志记录如果没有及时读取,而缓冲区的内容又已经用完时,就需要覆盖旧的记录来容纳新的记录。而这部分将要被覆盖的内容,有可能是某些reader的下一次要读取的日志所在的位置,以及为新的reader准备的日志开始读取位置head所在的位置。因此,需要调整这些位置,使它们能够指向一个新的有效的位置。我们来看一下fix_up_reader函数的实现:
/*
* fix_up_readers - walk the list of all readers and "fix up" any who were
* lapped by the writer; also do the same for the default "start head".
* We do this by "pulling forward" the readers and start head to the first
* entry after the new write head.
*
* The caller needs to hold log- mutex.
*/
staticvoid fix_up_readers(struct logger_log *log, size_t len)
{
size_t old = log- w_off;
size_t new = logger_offset(old+ len);
struct logger_reader*reader;
if(clock_interval(old, new, log- head))
log- head= get_next_entry(log, log- head, len);
list_for_each_entry(reader, log- readers, list)
if(clock_interval(old, new, reader- r_off))
reader- r_off= get_next_entry(log, reader- r_off, len);
}
判断log- head和所有读者reader的当前读偏移reader- r_off是否在被覆盖的区域内,如果是,就需要调用get_next_entry来取得下一个有效的记录的起始位置来调整当前位置:
/*
* get_next_entry - return the offset of the first valid entry at least len
* bytes after off.
*
* Caller must hold log- mutex.
*/
static size_t get_next_entry(struct logger_log *log, size_t off, size_t len)
{
size_t count =0;
do{
size_t nr = get_entry_len(log, off);
off = logger_offset(off+ nr);
count += nr;
}while(count len);
return off;
}
而判断log- head和所有读者reader的当前读偏移reader- r_off是否在被覆盖的区域内,是通过clock_interval函数来实现的:
/*
* clock_interval - is a c b in mod-space? Put another way, does the line
* from a to b cross c?
*/
static inline int clock_interval(size_t a, size_t b, size_t c)
{
if(b a){
if(a c|| b = c)
return1;
}else{
if(a c b = c)
return1;
}
return0;
}
至此,Logger驱动程序的主要逻辑就分析完成了,还有其它的一些接口,如logger_poll、 logger_ioctl和logger_release函数,比较简单,就不再分析了。
还有一点需要注意的是,由于Logger驱动程序模块在退出系统时,是不会卸载的,所以这个模块没有module_exit函数,而对于模块里面定义的对象,也没有用对引用计数技术。
Android组件化开发(七)--从零开始教你分析项目需求并实现 前面几篇文章我们封装了几个组件化功能组件:包括:**网络请求组件,图片加载请求组件,应用保活组件,音乐播放组件封装。** 每个组件都可以直接拿到自己项目中使用,当然还需根据自己项目要求进行优化。
相关文章
- android onresume方法,非静态方法’onResume’Android Studio
- android开机动画多长时间_Android开机动画及黑屏[通俗易懂]
- android中适配器的作用,适配器模式 在Android中的简单理解「建议收藏」
- Android应用setContentView与LayoutInflater加载解析机制源码分析
- android 定时器实例,Android定时器和Handler用法实例分析
- android跳转到相册需要权限,Android打开相册获取图片路径[通俗易懂]
- 【Android 性能优化】应用启动优化 ( 方法追踪代码模板 | 示例项目 | SD 卡访问权限 | 示例代码 | 获取 Trace 文件 | Android Studio 查看文件)
- 【Android 内存优化】使用 Memory Analyzer ( MAT ) 工具分析内存 ( MAT 工具使用 | 最大对象 | 类实例个数 | 引用与被引用 | GC Roots 最短链 )
- 【Android 安全】DEX 加密 ( Application 替换 | Android 应用启动原理 | LoadedApk 源码分析 )
- 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 三 )
- 【Android 事件分发】事件分发源码分析 ( ViewGroup 事件传递机制 五 )
- 【Android 插件化】Hook 插件化框架 ( Hook Activity 启动流程 | 反射获取 IActivityManager 对象 )
- 【Android 插件化】VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )
- 【Android 插件化】VirtualApp 源码分析 ( 添加应用源码分析 | LaunchpadAdapter 适配器 | 适配器添加元素 | PackageAppData 元素 )
- 【Android 逆向】函数拦截 ( GOT 表数据结构分析 | 函数根据 GOT 表进行跳转的流程 )
- 【Android 逆向】类加载器 ClassLoader ( 加载 Android 组件的类加载器 | 双亲委派机制实例分析 )
- android守护进程详解手机开发
- Android 平台创建 XY 图表的完整例子详解编程语言
- Android之网络通信案例分析
- 深入Android线程的相关问题解惑