本文共 22847 字,大约阅读时间需要 76 分钟。
http://blog.csdn.net/vastyh/article/details/8154932
Android之我是ADB
最近有个项目需要修改ADB,对ADB的代码进行了阅读和分析。以Android4.0ICS的源码为例,ADB的代码在/system/core/adb中。
先看Android.mk文件,在这个工程中,一共可以编译出三个模块:1,PC机端的ADB,针对不同的操作系统平台,所包含的文件不一样,具体可看Android.mk文件;2终端(这里所谓的终端就是手机)上的ADBD,作为一个daemon在终端上运行的;3终端上的ADB,作为host端的ADB连接,一般用不到。在编译的时候当ADB_HOST=1时,表明的是编译的HOSTADB,当ADB_HOST=0时,表示编译的SlaveADB,即ADBD。在代码中可以看到有很多宏使用这个变量来区分主从的代码。
在分析代码之前先说下ADB中采用的技术和实现方法,在ADB中大量运用了socket编程,进程间,线程间的数据交互都是通过网络传输来实现的。PC端和终端的传输也是最后抽象为TCP的传输,用的端口是5037。USB的传输与5037端口的传输进行绑定,即向端口5037的读写就是对USB的读写。
ADB的数据连接实现可以分为三层:1,USB层,通过读USB接口的信息,确定连接,对USB接口进行读写,向上虚拟为5037端口;2传输层(见transport.c,fdevent.c),负责管理上层的通道连接和下层对5037端口的连接,其实就是将多个连接统一入口传给5037端口,使得USB层进行传输,这里是进行双向的传输;3socket层,建立通道连接,对传输协议做一个包装,见socket.c中。
鉴于ADB的多样化,他可以于虚拟设备进行连接,所以在这里我们只介绍通过USB接口进行连接的ADB协议实现。并且关于ADBHOST的功能如LOGCAT,SHELL的实现可以自己看,我也没看啊,这里直介绍底层的通信实现。
下面我们就从万物的核心main中开始分析ADB这个东东是怎么运转起来的。首先我们看HOST端,在前期的初始化中两端有些不同,但是到后期的等待数据,发送数据的时候,其结构就类似了。
ADBHOST
首先我们可以打开adb.c中的最后一个函数就是main函数了,很简单的几句话,现在我们只看ADB_HOST包含的内容。可以看到:
1276 adb_sysdeps_init();
1277 adb_trace_init();
1278 D("Handling commandline()\n");
1279 return adb_commandline(argc - 1, argv + 1)
很简单的四句话,前两句是在对LOG的一些初始化,这些我们可以忽略,这里值得说的是ADB的LOG体系,设计地不错,通过对宏的修改,或者是环境变量的设置,可以设置LOG的模块等级,有兴趣的同学可以看看log相关的内容。这里面主要做事的是adb_commandline这句,我们顺藤摸瓜,找着这个函数的定义在commmandline.c中的822行。这函数的前面是对输入参数的判断并相应的赋值给内部使用信号,如什么is_server,is_daemon,product等等。这些内部信号,使用的时候就知道他是什么意思了,或者直接从变量名也能看出来是看啥的。在这里插一句ADBHOST的实现方式,大致是这样的,当ADB启动时,在第一次连接的时候,ADB会去启动一个server,注意这个就类似于daemon,如果不手动杀死它,它会一直运行的,它是负责对USB接口的监听,并建立5037端口的server,共各client进行connect。话题又扯原来,咱们再看看adb_commandline里面的内容吧。在913行,发现有个直接调用adb_main的和launch_server的,当然必须是is_server==1才行啊,这里我们先不用管这俩分支,再往下看,发现貌似是ADB命令的参数啊,当然第一个是最容易的devices,这不是获取设备信息吗,那我们就看这个简单的,反正向下看只是参数不一样,但是流程应该都是一样的。我们看932-943行,发现值得注意的就剩下了adb_query,看来这个还是挺重要的,那我们就link到它的实现吧。
话说我们现在移驾到了adb_query,他在adb_client.c中,在这个函数没几行后,又一个操作让我们眼前一亮,adb_connect,我靠,终于找着个连接了,它会返回一个文件描述符,接下来就是对文件描述符的读写了,那我们断定这个adb_connect就是完成了主从的连接啊,别看这么不起眼,但是他可做了很多事,真是人不可貌相啊。那我们就在深入参观这个adb_connect了,索性他就在adb_client里面,第一行就把我给镇住了,它又来了个_adb_connect,看着函数的第一个字母为_,我很欣慰,原来google工程也会遵循这个“潜规则”,内部函数一般都是以”_”开头,看来我们已经打入内部了啊。那干脆就继续深入呗,这样我们就到了_adb_connect中,通过我们对上面代码的分析,我们已经有火眼金睛了,直奔到socket_loopback_client,为了给大家点惊喜我来个剧透吧,可以看到这个函数的第一个参数是个port,一看就是tcp的port嘛,那他是干啥的呢,自己去找或者我来说说我的理解吧,就是建立一个client与这个port的连接,那个这个port是啥呢----5037,哦那就明白了。可以我们还没有建立这个种服务呢,那怎么办啊,这简单,这个就返回错误了呗,那返回错误_adb_connect就返回-2了呗,哎呀,终于我们的堆栈少了许多,那我们就继续看adb_connect了,一看返回-2,这号啊,进入adb_client.c中的216行了,然后有个标签很是响亮--start_server,我们这时终于找着了组织啊,然后就直接launch_server了,这个我们似曾相识啊,原来开个server如此容易直接launch_server就可以了,哈哈哈。
经过山路十八弯我们又到了adb.c中的launch_server,看来这个adb.c深藏不漏啊,刚说简单,就到了这个不简单了。这里面就要说下我们的google工程师了,针对不同的平台可以做一层适配层吗,干嘛在这还有个宏来区分代码呢,可能工程师也想偷懒吧。不管咋样,我们继续往下看,对于Linux编程人员当然是看linux系统下的实现了,直奔773行。这里就有些端倪了,fork了一个进程,哈哈,终于找着这个server的开始的地方了,毫不犹豫去了它的子进程,pid==0的地方,这里用了个管道,将子进程的错误输出传给了父进程?至今我没想明白,它为啥用错误输出呢,哎。之后看着了我们亲爱的execl,话说这个path就是我们的adb程序,看来还是走我们的main来实现的啊,值得注意的是,加了三个参数,adb,fork-server,server.然后我们再看看父进程,父进程是要等子进程的信息啊,它要等到子进程给他发OK,那我们就等着子进程吧。
话说子进程重新进入main后,直奔commandline,其中adb这个参数被斩断,只剩fork-server,server俩个参数。这时我们又来到了亲切的adb_commandline函数,发现两个参数派上用场了,直接将is_server,is_daemon赋值1了,一看,这个走到adb_main这个分支了,哈哈,看来好戏开场了。它带了is_daemon,server_port。显然is_daemon==1,server_port =ADB_DEFAULT_PORT。
无独有偶,ADBSlave也是走到了这个函数里面,不过,他的第一个参数是0。不行我们得加快速度啊,从adb_main开始我们就要取其精华,去其糟粕啊。
854 init_transport_registration();看实现,很简单
657 int s[2];
658
659 if(adb_socketpair(s)){
660 fatal_errno("cannotopen transport registration socketpair");
661 }
662
663 transport_registration_send= s[0];
664 transport_registration_recv= s[1];
665
666 fdevent_install(&transport_registration_fde,
667 transport_registration_recv,
668 transport_registration_func,
669 0);
670
671 fdevent_set(&transport_registration_fde, FDE_READ)
初始化传输注册?这个ADBSlave端也有。这里我们学习下几个知识点:
Adb_socketpair,这是向操作系统申请一对socket,给他们俩个端口建立一个管道,供通信。
Fdevent_install:这个要想了解详细的内容的话,可以看看fdevent.c文件,他其实就是文件监听事件管理,只要利用fdevent_install注册到他的管理后,当注册的文件有里面有活动的时候,表述有读或者是写,这里是transport_registration_recv,就会触发func,这里的func就是transport_registration_func。
这俩连起来来看就是只要transport_registration_send发送数据,就会触发函数transport_registration_func。那我们就有必要看看transport_registration_func这个函数了。至于啥时候发送数据,那待稍后分解。
Transport_registration_func这个函数待遇真牛,我们得另来一段了。
578 if(transport_read_action(_fd, &m)) {
这个就是取数据了,我们就不做介绍了。
617 if(adb_socketpair(s)) {
625
626 fdevent_install(&(t->transport_fde),
627 t->transport_socket,
628 transport_socket_events,
629 t);
630
631 fdevent_set(&(t->transport_fde), FDE_READ);
632
633 if(adb_thread_create(&input_thread_ptr, input_thread,t)){
634 fatal_errno("cannot create input thread");
635 }
636
637 if(adb_thread_create(&output_thread_ptr, output_thread,t)){
638 fatal_errno("cannot create output thread");
639 }
这可是这个函数的重点啊,这里插一句我们看代码的时候一定要心无旁骛,直接切中要害,找单一正确的分支走下去,这就像是走迷宫一样,真相只有一个。所以我们来到了这个分支。
经过上面的介绍这个个也不足为奇了,但是下面create两个线程,这个就和USB有关系啦,终于衔接上了,这两个线程分别对USB的读和写,数据就是从这个t->transport_socket来的。那我们看看transport_socket_events吧,这就开始处理从USB来的数据喽,独到数据后给了handle_packet。发现了ADB的底层协议通信,如果想进一步了解可以看看TRANSPORT.TXT文档。
858 HOST = 1;
859 usb_vendors_init();//关于VENDORS的过滤,
860 usb_init();
861 local_init(DEFAULT_ADB_LOCAL_TRANSPORT_PORT);//内部端口,不考虑
好了,我们再看看USB的实现了,那就剩下了usb_init了。那我们继续找linux下的usb_init实现喽。这个是在usb_linux.c中,这里其线程对USB设备进行查看,找到适合的USB设备进行register_devices.填充usb_handle数据结构,加入链表中。注意,里面的注册会调用register_usb_transport。这里就会产生一个atransport,-》register_transport。然后向transport_registration_send发送命令,这就和上面衔接上了,transport_socket_events正式启动。
865 if(install_listener(local_name, "*smartsocket*", NULL))
这个调用可是ADBHOST端server最后的重要函数了。在这里面申请一个alistener对象,建立监听5037端口的server,但有数据到来时,就建立一个socket,具体可以查看ss_listener_event_func函数的实现。
下面我们看看adb.c:966行,我们进入这个if后,向异常输出为OK,可以看到我们前面创建子进程后,父进程就是在等这个信息,终于等到了,内流满面啊。
接下来是
979 fdevent_loop();
这个是在一看就是在里面实现了循环,它就是在fdevent_install后,然后对这些文件描述符进行监听,然后处理的过程。我们先放放,且看ADBSlave的实现。
ADBSlave的实现
话说天下大事,合久必分,分久必合。ADBSlave在进入main之后就和ADBHost分道扬镳,收敛于adb_main,然而我们的ADBHOST Server也会执行到这边来,所以我们可是轻车熟路啊。且看ADBSlave adb_main的执行。
854 init_transport_registration();
这不和HOST端一样,那功能自然也是一样一样的。然后后面的什么secure,prop之类的我们可以直接略过了。哈哈来到了938行
938 if(install_listener(local_name, "*smartsocket*",NULL)) {
不说了,这个和主端也是一样一样的,下面直接到USB的初始化
956 usb_init();这个usb_init的实现是在usb_linux_client.c中,打开/dev/android_adb设备,这个设备文件时内核暴露出来和应用通信的ADB驱动接口。我们对ADB的数据,最终是通过写,读这个文件实现的,可以看到,在usb_open_thread中,调用
register_usb_transport来实现对上层的注册,从而上层得到一个atransport。
最后我们来看看fdevent_loop,这是ADB,ADBD大多数时间工作的地方。前面的一大推都是为了这个做准备的。
684 fdevent_subproc_setup();
685
686 for(;;) {
687 D("--- ---- waiting for events\n");
688
689 fdevent_process();
690
691 while((fde = fdevent_plist_dequeue())) {
692 fdevent_call_fdfunc(fde);
693 }
694 }
很简单的几句话,fdevent_subproc_setup又是注册了一个fdevent,这个是接受推出信息的,我们就不看他了。我们只看看fdevent_process(),这函数就是查看fdevent中的信息到来,如果有信息,将信息入队列,然后再691行出队列,进而执行它的fdevent_call_fdfunc函数。
还有关于sockets.do_cmd的一些功能没有详细阐述,相信以上对你有用。由于电脑上没有VIso,所以懒得画图了,ADB就到此为止了。
--------------------------------------------------------------------------------------------------------------------------------------
目录 1.
1.
构建不同文件,通过传入Android.mk的$(BUILD_SIMULATOR)变量是否为真。源码中由ADB_HOST宏用来区分本地主机(adb)和目标机(adbd)。 区分不同OS,通过传入Android.mk的$(HOST_OS)。它的有效取值包括linux、darwin、freebsd和windows。不同平台的主要差异是USB的控制方法和文件路径。
2.1.
adb整体架构和数据传输图如下: P1
DDMS和Jdwp不做多的了解。只看adb模块,如下图: P2
system/core/adb/OVERVIEW.txt文件中对它们的关系进行了描述。 system/core/adb/protocol.txt和OVERVIEW.txt描述了各模块之间通信协作的协议格式。
2.2.
2.2.1.
2, adb_main() Adbd创建两个socket,一个用来控制连接,一个用来连接adb server。在该函数中初始化jdwp用来使adbd和jvm之间的交互,就是说adbd能够发送消息给app和接收app来自的消息(包括event、data等)。 P3
在init_transport_registration函数中调用adb_socketpair。
1.1.
1.1.1.
2.4.
三者通信整个流程是这样的:
adb client连接adb server: P6
Adb server 和 client 连接流程 P7
2.5.
DDMS 的工作原理: DDMS将搭建起IDE与测试终端(Emulator 或者connected device)的链接,它们应用各自独立的端口监听调试器的信息,DDMS可以实时监测到测试终端的连接情况。当有新的测试终端连接后,DDMS将捕捉到终端的ID,并通过adb建立调试器,从而实现发送指令到测试终端的目的。 888 DDMS监听第一个终端App进程的端口为8600,APP进程将分配8601,如果有更多终端或者更多APP进程将按照这个顺序依次类推。DDMS通过8700端口(”base port”)接收所有终端的指令。
DDMS选项卡: (1)Device选项卡 Device中罗列了Emulator中所有的进程,选项卡右上角那一排按钮分别为:调试进程、更新进程、更新进程堆栈信息、停止某个进程,最后一个图片按钮是抓取Emulator目前的屏幕。当你选中某个进程,并按下调试进程按钮时,如果eclipse中有这个进程的代码,那就可以进行源代码级别的调试。有点像gdb attach。图片抓取按钮可以把当前android的显示桌面抓到你的机器上,也是非常有用。 (2)Threads选项卡 显示线程统计信息 (3)Heap选项卡 显示栈信息 (4)File Explorer选项卡 显示GPhone Emulator的文件系统信息。 (5)Emulator Control选项卡 通过它可以向手机发送短信、打电话、更新手机位置信息。 总结: Eclipse ADT目前提供的的ddms功能只是真正ddms的一小部分,你可以通过ddms.bat命令来使用所有功能。其中有一个查看进程内存分配的功能比较有用。
2.6.
3.
adb shell command 描述见文件:system/core/adb/SERVICES.TXT
3.2.
物理设备与adb server通信: 传输类型: transport-usb used for switching transport to the only USB transport transport-local used for switching transport to the only local transport transport-any used for switching transport to the only transport
Host和physical device采用TCP/IP来进行通信,adb daemon需要使用5555到5585之间的奇数端口。首先看一下下面这段源代码,出自system/core/adb/adb.c adb_main()函数 property_get("service.adb.tcp.port", value, ""); if (!value[0])
3.2.2.
不过adb server 和 adbd传输和接收的都是apacket结构数据,定义如下: struct apacket {
定义了6种消息类型: (标识符"local-id" 和 "remote-id"是相对而言,是对于消息的收发角色而定) CONNECT(version, maxdata, "system-identity-string") OPEN(local-id, 0, "destination") READY(local-id, remote-id, "") WRITE(0, remote-id, "data") CLOSE(local-id, remote-id, "") SYNC(online, sequence, "")
--- amessage command 类型:(代码见:adb.h)-- #define A_SYNC 0x434e5953 #define A_CNXN 0x4e584e43 #define A_OPEN 0x4e45504f #define A_OKAY 0x59414b4f #define A_CLSE 0x45534c43 #define A_WRTE 0x45545257 描述: A_SYNC A_CNXN:send CNXN SIG to device/emulator after receive SYNC A_OPEN: open local service socket A_OKAY: ready A_CLSE: close socket A_WRTE: write to remote socket
4.
5.
查询设备状态 了解adb服务端连接的模拟器或物理设备可以帮助更好的使用adb命令,这可以通过devices命令列举出来:
操作指定的模拟器或物理设备 如果有多个模拟器或手机正在运行,当使用adb命令的时候就需要指定目标设备,这可以通过使用-s选项参数实现,用法是: adb -s <serialNumber> <command> 即可以在adb命令中使用序列号指定特定的目标,前文已经提到的devices命令可以实现查询设备的序列号信息。 例如: adb -s emulator-5556 install helloWorld.apk 需要注意的是,如果使用了-s而没有指定设备的话,adb会报错。
安装应用程序 可以使用adb从开发用电脑中复制应用程序并且安装到模拟器或手机上,使用install命令即可,在这个命令中,必须指定待安装的.apk文件的路径: adb install <path_to_apk> 如果使用了安装有ADT插件的Eclipse开发环境,就不需要直接使用adb或aapt命令来安装应用程序了,ADT插件可以自动完成这些操作。 关于创建可安装的应用的更多信息,请参见Android Asset Packaging Tool (aapt).
转发端口
在TCP网络编程中,Client的Socket(C)如果调用Connect()成功,就说明已经和Server端的Socket(S)连接上,可以通讯了。但是如果使用adb forward做端口映射,就不一样了。端口映射的实质是,让ADB-server作为一个switcher转发ADB-client的数据包,送给 adbd,adbd再发给设备端的对应端口。因此一旦建立了映射,就相当于ADB-server开始监听这个目标端口。而此时如果有C去尝试 Connect这个端口,是一定会成功的,因为与C连接的是ADB-server,而非真正的设备上的目标程序。这就出现了,即使Connect()成功,却完全无法知道究竟是否成功连接到S。 因此,判断真正连接成功的方法,只有轮询收发握手数据包。程序中约定好事先做个交互:C发送一个数据包,等待S回复;C如果收到了S的回复包,说明连通;如果接收超时,则认为没有连通。在没有连通的情况下,需要重新建立Socket,并Connect(),然后再尝试握手。
与模拟器或手机传输文件 可以使用adb的 pull 和 push 命令从模拟器或物理设备中复制文件,或者将文件复制到模拟器或物理设备中。与 install 命令不同,它仅能复制.apk文件到特定的位置, pull 和 push 命令可以复制任意文件夹和文件到模拟器或物理设备的任何位置。 从模拟器或手机中复制一个文件或文件夹(递归的)使用: adb pull <remote> <local> 复制一个文件或文件夹(递归的)到模拟器或物理设备中使用: adb push <local> <remote> 在这个命令中<local>和<remote>引用的是文件或文件夹的路径,在开发用电脑上的是local,在模拟器或物理设备上的是remote。 例如: adb push foo.txt /sdcard/foo.txt
adb命令列表
类别 | 命令 | 说明 | 备注 |
可选项 | -d | 命令仅对USB设备有效 | 如果有多个USB设备就会返回错误 |
-e | 命令仅对运行中的模拟器有效 | 如果有多个运行中的模拟器就会返回错误 | |
-s <serialNumber> | 命令仅对adb关联的特定序列号的模拟器或手机有效(例如 "emulator-5556"). | 如果不指定设备就会返回错误 | |
一般项 | devices | 输出所有关联的模拟器或手机设备列表 | 参见 以获得更多信息。 |
help | 输出adb支持的命令 | | |
version | 输出adb的版本号 | | |
调试项 | logcat [<option>] [<filter-specs>] | 在屏幕上输出日志信息 | |
bugreport | 为报告bug,在屏幕上输出dumpsys,dumpstate和 logcat数据 | | |
jdwp | 输出有效的JDWP进程信息 | 可以使用 forward jdwp:<pid> 转换端口以连接到指定的JDWP 进程,例如: adb forward tcp:8000 jdwp:472 jdb -attach localhost:8000 | |
数据项 | install <path-to-apk> | 安装应用程序(用完整路径指定.apk文件) | |
pull <remote> <local> | 从开发机COPY指定的文件到模拟器或手机 | | |
push <local> <remote> | 从模拟器或手机COPY文件到开发机 | | |
端口和网络项 | forward <local> <remote> | 从本地端口转换连接到模拟器或手机的指定端口 | 端口可以使用以下格式表示: |
ppp <tty> [parm]... | 通过USB运行UPP 注意不用自动启动PPP连接 | | |
脚本项 | get-serialno | 输出adb对象的序列号 | 参见 以获得更多信息。 |
get-state | 输出adb设备的状态 | ||
wait-for-device | 阻塞执行直到设备已经连接,即设备状态是 device. | 可以在其他命令前加上此项,那样的话adb就会等到模拟器或手机设备已经连接才会执行命令,例如: 注意该命令并不等待系统完全启动,因此不能追加需要在系统完全启动才能执行的命令,例如install 命令需要Android包管理器支持,但它必须在系统完全启动后才有效。下面的命令 会在模拟器或手机与adb发生连接后就执行install,但系统还没有完全启动,所以会引起错误。 | |
服务端项 | start-server | 检测adb服务进程是否启动,如果没启动则启动它。 | |
kill-server | 终止服务端进程 | | |
Shell | shell | 在目标模拟器或手机上启动远程SHELL | 参见 以获得更多信息。 |
shell [<shellCommand>] | 在目标模拟器或手机上执行shellCommand然后退出远程SHELL |
5.2.
从远程shell检查sqlite3 数据库
使用Monkey进行UI或应用程序测试
关于monkey更多的选项及详细信息,请参见UI/Application Exerciser Monkey。 还有一个monkeyrunner。monkeyrunner工具提供了一个API,使用此API写出的程序可以在Android代码之外控制Android设备和模拟器。具体可见:
其他Shell命令
Shell 命令 | 描述 | 备注 |
dumpsys | 在屏幕上显示系统数据 | The (DDMS) 工具提供了更易于使用的智能的调试环境。 |
dumpstate | 将状态输出到文件 | |
logcat [<option>]... [<filter-spec>]... | 输出日志信息 | |
dmesg | 在屏幕上输出核心调试信息 | |
start | 启动或重新启动模拟器或手机 | |
stop | 停止模拟器或手机 | |
使用 logcat 命令
过滤日志输出
这里是一个日志输出的消息,优先级是“I”,标记是“ActivityManager”: I/ActivityManager(
过滤器表达式的格式是tag:priority ... ,其中tag是标记, priority是最小的优先级,该标记标识的所有大于等于指定优先级的消息被写入日志。也可以在一个过滤器表达式中提供多个这样的过滤,它们之间用空格隔开。
下面给出的例子是仅输出标记为“ActivityManager”并且优先级大于等于“Info”和标记为“MyApp”并且优先级大于等于“Debug”的日志: adb logcat ActivityManager:I MyApp:D *:S 上述表达式最后的 *:S 用于设置所有标记的日志优先级为S,这样可以确保仅有标记为“View”(译者注:应该为ActivityManager,原文可能是笔误)和“MyApp”的日志被输出,使用 *:S 是可以确保输出符合指定的过滤器设置的一种推荐的方式,这样过滤器就成为了日志输出的“白名单”。 下面的表达是显示所有优先级大于等于“warning”的日志: adb logcat *:W 如果在开发用电脑上运行 logcat
控制日志格式
Viewing Alternative Log Buffers
查看stdout和stderr 默认的,Android系统发送 stdout 和 stderr (System.out 和 System.err) 输出到 /dev/null。 在 Dalvik VM进程,可以将输出复制到日志文件,在这种情况下,系统使用 stdout 和 stderr标记写入日志,优先级是I。
Logcat命令选项列表
选项 | 描述 |
-b | 加载不同的缓冲区日志,例如 |
-c | 清空(刷新)所有的日志并且退出 |
-d | 在屏幕上输出日志并退出 |
-f <filename> | 将日志输出到文件<filename>,默认输出是stdout. |
-g | 输出日志的大小 |
-n <count> | 设置最大的循环数据<count>,默认是4,需要-r选项 |
-r <kbytes> | 每<kbytes>循环日志文件,默认是16,需要 |
-s | 设置默认的过滤器为无输出 |
-v <format> | 设置输出格式,默认的是brief,支持的格式列表参见. |
原文: