As we all know,Android手机系统本质上是一个基于Linux的应用程序,它以Linux系统为内核。因此系统的启动过程包括Linux内核启动和Android框架启动两个阶段。

Linux内核启动

1、装载引导程序bootloader
Linux内核启动时首先装载执行bootloader引导程序,装载完成后进入内核程序。
2、加载Linux内核
Linux内核加载主要包括初始化kernel核心(内存初始化,打开中断,初始化进程表等)、初始化驱动、启动内核后台(daemons)线程、安装根(root)文件系统等。
Linux加载的最后阶段启动执行第一个用户级进程init(内核引导参数上一般都会设置“init=/init”,由kernel自动执行,PID为1,是所有进程的父进程)。由此进入Android框架的启动阶段。

android框架的启动

Android框架的启动始于init进程,这个阶段也是本文要重点讲解的。概括起来启动过程可以分为以下几个主要的阶段:
1、init进程启动
2、init.rc脚本启动
3、zygote服务启动
4、System Server进程启动
5、Home应用启动



下面将从Android5.0源码中,和网络达人对此的总结中,对此过程加以学习了解和总结,

以下学习过程中代码片段中均有省略不完整,请参照源码。

Init进程的启动

init是一个进程,确切地说,它是Linux系统中用户空间的第一个进程。由于Android是基于Linux内核的,所以init也是Android系统中用户空间的第一个进程,它的进程号是1。


init进程的入口函数是main,system\core\init\init.c

init进程可以在/system/core/init找到。
init.rc文件可以在/system/core/rootdir/init.rc找到。
readme.txt可以在/system/core/init/readme.txt找到。

int main(int argc, char **argv){intdevice_fd = -1;intproperty_set_fd = -1;intsignal_recv_fd = -1;intkeychord_fd = -1;int fd_count;ints[2];intfd;structsigaction act;chartmp[PROP_VALUE_MAX];structpollfd ufds[4];char*tmpdev;char*debuggable;//设置子进程退出的信号处理函数,该函数为sigchld_handler。act.sa_handler = sigchld_handler;act.sa_flags= SA_NOCLDSTOP;act.sa_mask = 0;act.sa_restorer = NULL;sigaction(SIGCHLD, &act, 0);......//创建一些文件夹,并挂载设备,这些是和Linux相关的,不拟做过多讨论。mkdir("/dev/socket", 0755);mount("devpts", "/dev/pts", "devpts", 0,NULL);mount("proc", "/proc", "proc", 0, NULL);mount("sysfs", "/sys", "sysfs", 0, NULL);//重定向标准输入/输出/错误输出到/dev/_null_。open_devnull_stdio();/*设置init的日志输出设备为/dev/__kmsg__,不过该文件打开后,会立即被unlink了,这样,其他进程就无法打开这个文件读取日志信息了。*/log_init();//上面涉及很多和Linux系统相关的知识,不熟悉的读者可自行研究,它们不影响我们的分析//解析init.rc配置文件parse_config_file("/init.rc");......//下面这个函数通过读取/proc/cpuinfo得到机器的Hardware名,我的HTCG7手机为bravo。get_hardware_name();snprintf(tmp,sizeof(tmp), "/init.%s.rc", hardware);//解析这个和机器相关的配置文件,我的G7手机对应文件为init.bravo.rc。parse_config_file(tmp);/*解析完上述两个配置文件后,会得到一系列的Action(动作),下面两句代码将执行那些处于early-init阶段的Action。init将动作执行的时间划分为四个阶段:early-init、init、early-boot、boot。由于有些动作必须在其他动作完成后才能执行,所以就有了先后之分。哪些动作属于哪个阶段由配置文件决定。后面会介绍配置文件的相关知识。*/action_for_each_trigger("early-init", action_add_queue_tail);drain_action_queue();/*创建利用Uevent和Linux内核交互的socket。关于Uevent的知识,第9章中对Vold进行分析时会做介绍。*/device_fd = device_init();//初始化和属性相关的资源property_init();//初始化/dev/keychord设备,这和调试有关,本书不讨论它的用法。读者可以自行研究,//内容比较简单。keychord_fd = open_keychord();....../*INIT_IMAGE_FILE定义为”/initlogo.rle”,下面这个函数将加载这个文件作为系统的开机画面,注意,它不是开机动画控制程序bootanimation加载的开机动画文件。*/if(load_565rle_image(INIT_IMAGE_FILE) ) {/*如果加载initlogo.rle文件失败(可能是没有这个文件),则会打开/dev/ty0设备,并输出”ANDROID”的字样作为开机画面。在模拟器上看到的开机画面就是它。*/......}}if(qemu[0])import_kernel_cmdline(1);......//调用property_set函数设置属性项,一个属性项包括属性名和属性值。property_set("ro.bootloader", bootloader[0] ? bootloader :"unknown");......//执行位于init阶段的动作action_for_each_trigger("init", action_add_queue_tail);drain_action_queue();//启动属性服务property_set_fd = start_property_service();/*调用socketpair函数创建两个已经connect好的socket。socketpair是Linux的系统调用,不熟悉的读者可以利用man socketpair查询相关信息。后面就会知道它们的用处了。*/if(socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {signal_fd = s[0];signal_recv_fd = s[1];......}......//执行配置文件中early-boot和boot阶段的动作。action_for_each_trigger("early-boot", action_add_queue_tail);action_for_each_trigger("boot", action_add_queue_tail);drain_action_queue();......//init关注来自四个方面的事情。ufds[0].fd= device_fd;//device_fd用于监听来自内核的Uevent事件ufds[0].events = POLLIN;ufds[1].fd = property_set_fd;//property_set_fd用于监听来自属性服务器的事件ufds[1].events= POLLIN;//signal_recv_fd由socketpair创建,它的事件来自另外一个socket。ufds[2].fd = signal_recv_fd;ufds[2].events = POLLIN;fd_count = 3;if(keychord_fd > 0) {//如果keychord设备初始化成功,则init也会关注来自这个设备的事件。ufds[3].fd = keychord_fd;ufds[3].events = POLLIN;fd_count++;}......#if BOOTCHART......//与Boot char相关,不做讨论了。/*Boot chart是一个小工具,它能对系统的性能进行分析,并生成系统启动过程的图表,以提供一些有价值的信息,而这些信息最大的用处就是帮助提升系统的启动速度。*/#endiffor(;;) {//从此init将进入一个无限循环。int nr, i, timeout = -1;for (i = 0; i < fd_count; i++)ufds[i].revents = 0;//在循环中执行动作drain_action_queue();restart_processes(); //重启那些已经死去的进程......#if BOOTCHART...... // Boot Chart相关#endif//调用poll等待一些事情的发生nr= poll(ufds, fd_count, timeout);......//ufds[2]保存的是signal_recv_fd,用于接收来自socket的消息。if(ufds[2].revents == POLLIN) {//有一个子进程去世,init要处理这个事情read(signal_recv_fd, tmp, sizeof(tmp));while (!wait_for_one_process(0));continue;}if(ufds[0].revents == POLLIN)handle_device_fd(device_fd);//处理Uevent事件if(ufds[1].revents == POLLIN)handle_property_set_fd(property_set_fd);//处理属性服务的事件。if(ufds[3].revents == POLLIN)handle_keychord(keychord_fd);//处理keychord事件。}return0;}

这个函数摘抄过来已经精简了不少。总的来说,在函数中执行了:文件夹建立,挂载,rc文件解析,属性设置,启动服务,执行动作,socket监听……

总的来说init的工作流程精简为以下几点:

创建一些文件夹并挂载设备

解析两个配置文件init.rc和init.hardware.rc,其中,将分析对init.rc文件的解析。
执行各个阶段的动作,创建Zygote的工作就是在其中的某个阶段完成的。
调用property_init初始化属性相关的资源,并且通过property_start_service启动属性服务。

init进入一个无限循环,并且等待一些事情的发生。重点关注init如何处理来自socket和来自属性服务器相关的事情。


1、解析 Init.rc

对于init.rc文件,Android中有特定的格式以及规则。在Android中,我们叫做Android初始化语言。
Android初始化语言由四大类型的声明组成,即Actions(动作)、Commands(命令)、Services(服务)、以及Options(选项)。
Action(动作):动作是以命令流程命名的,有一个触发器决定动作是否发生。
语法


on <trigger>
<command>
<command>
<command>
Service(服务):服务是init进程启动的程序、当服务退出时init进程会视情况重启服务。
语法


service <name> <pathname> [<argument>]*
<option>
<option>
...
Options(选项)
选项是对服务的描述。它们影响init进程如何以及何时启动服务。
咱们来看看默认的init.rc文件。这里我只列出了主要的事件以及服务。

Action/Service 描述
on early-init 设置init进程以及它创建的子进程的优先级,设置init进程的安全环境
on init 设置全局环境,为cpu accounting创建cgroup(资源控制)挂载点
on fs 挂载mtd分区
on post-fs 改变系统目录的访问权限
on post-fs-data 改变/data目录以及它的子目录的访问权限
on boot 基本网络的初始化,内存管理等等
service servicemanager 启动系统管理器管理所有的本地服务,比如位置、音频、Shared preference等等…
service zygote 启动zygote作为应用进程
在这个阶段你可以在设备的屏幕上看到“Android”logo了。


需要重点说明的是init.rc脚本文件配置了一些重要的服务,init进程通过创建子进程启动这些服务,这里创建的service都属于native服务,运行在Linux空间,通过socket向上层提供特定的服务,并以守护进程的方式运行在后台。脚本中服务的定义示例如下:
service servicemanager /system/bin/servicemanager      class core      user system      group system      critical      onrestart restart zygote  08    onrestart restart media    service ril-daemon /system/bin/rild      class main      socket rild stream 660 root radio      socket rild-debug stream 660 radio system      user root      group radio cache inet misc audio sdcard_rw log    service surfaceflinger /system/bin/surfaceflinger      class main      user system      group graphics      onrestart restart zygote    service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server      class main      socket zygote stream 666      onrestart write /sys/android_power/request_state wake      onrestart write /sys/power/state on      onrestart restart media      onrestart restart netd    service media /system/bin/mediaserver      class main      user media      group audio camera inet net_bt net_bt_admin net_bw_acct drmrpc      ioprio rt 4


通过init.rc脚本系统启动了以下几个重要的服务:
1)servicemanager:启动binder IPC,管理所有的Android系统服务
2)mountd:设备安装Daemon,负责设备安装及状态通知
3)debuggerd:启动debug system,处理调试进程的请求
4)rild:启动radio interface layer daemon服务,处理电话相关的事件和请求
5)mediaserver:启动AudioFlinger,MediaPlayerService and CameraService,负责多媒体播放相关的功能,包括音视频解码、显示输出
6)zygote:进程孵化器,启动Android Java VMRuntime和启动systemserver,负责Android应用进程的孵化工作

在解析rc脚本文件时,将相应的类型放入各自的List中:

  \system\core\init\Init_parser.c :init_parse_config_file( )存入到action_queue、 action_list、 service_list中,解析过程可以看一下parse_config函数,类似状态机形式

  这其中包含了服务:adbd、servicemanager、vold、ril-daemon、debuggerd、surfaceflinger、zygote、media……

2、Nativie服务启动(包括ServiceManager

文件解析完成之后将service放入到service_list中。

  \system\core\init\builtins.c

Service的启动是在do_class_start函数中完成:
int do_class_start(int nargs, char **args){    service_for_each_class(args[1], service_start_if_not_disabled);    return 0;}

遍历所有名称为classname,状态不为SVC_DISABLED的Service启动
void service_for_each_class(const char *classname,                            void (*func)(struct service *svc)){       ……}static void service_start_if_not_disabled(struct service *svc){    if (!(svc->flags & SVC_DISABLED)) {        service_start(svc, NULL);    }}

do_class_start对应的命令:

  KEYWORD(class_start, COMMAND, 1, do_class_start)


init.rc文件中搜索class_start:class_start main 、class_start core、……

  main、core即为do_class_start参数classname

init.rc文件中Service class名称都是main:

service drm /system/bin/drmserver

    class main

  service surfaceflinger /system/bin/surfaceflinger

   class main

于是就能够通过main名称遍历到所有的Service,将其启动。

do_class_start调用:

init.rc中

    on boot  //action
      class_start core    //执行command 对应 do_class_start
     class_start main

Init进程main函数中:
system/core/init/init.c中:
int main(){    //挂载文件       //解析配置文件:init.rc……       //初始化化action queue     ……       for(;;){              execute_one_command();              restart_processes();              for (i = 0; i < fd_count; i++) {            if (ufds[i].revents == POLLIN) {                if (ufds[i].fd == get_property_set_fd())                    handle_property_set_fd();                else if (ufds[i].fd == get_keychord_fd())                    handle_keychord();                else if (ufds[i].fd == get_signal_fd())                    handle_signal();            }        }       }}

  循环调用service_start,将状态SVC_RESTARTING启动, 将启动后的service状态设置为SVC_RUNNING。
  pid=fork();
  execve();

  在消息循环中:Init进程执行了Android的Command,启动了Android的NativeService,监听Service的变化需求,Signal处理。

Init进程是作为属性服务(Property service),维护这些NativeService。
需要重点说明的是init.rc脚本文件配置了一些重要的服务,init进程通过创建子进程启动这些服务,这里创建的service都属于native服务,运行在Linux空间,通过socket向上层提供特定的服务,并以守护进程的方式运行在后台


重点看下servicemanager ,在.rc脚本文件中servicemanager的描述:

service servicemanager /system/bin/servicemanager
  class core
  user system
  group system
  critical
  onrestart restart zygote
  onrestart restart media
  onrestart restart surfaceflinger
  onrestart restart drm


ServiceManager用来管理系统中所有的binder service,不管是本地的c++实现的还是java语言实现的都需要
这个进程来统一管理,最主要的管理就是,注册添加服务,获取服务。所有的Service使用前都必须先在servicemanager中进行注册。

  do_find_service( )
  do_add_service( )
  svcmgr_handler( )
  代码位置:frameworks\base\cmds\servicemanager\Service_manager.c

3、zygote服务启动

在Java中,我们知道不同的虚拟机实例会为不同的应用分配不同的内存。假如Android应用应该尽可能快地启动,但如果Android系统为每一个应用启动不同的Dalvik虚拟机实例,就会消耗大量的内存以及时间。因此,为了克服这个问题,Android系统创造了”Zygote”。Zygote让Dalvik虚拟机共享代码、低内存占用以及最小的启动时间成为可能。Zygote是一个虚拟器进程,正如我们在前一个步骤所说的在系统引导的时候启动。Zygote预加载以及初始化核心库类。通常,这些核心类一般是只读的,也是Android SDK或者核心框架的一部分。在Java虚拟机中,每一个实例都有它自己的核心库类文件和堆对象的拷贝。


zygote进程孵化了所有的Android应用进程,是Android Framework的基础,该进程的启动也标志着Framework框架初始化启动的开始。zygote服务进程的主要功能:


1)注册底层功能的JNI函数到虚拟机
2)预加载java类和资源
3)fork并启动System Server核心进程
4)作为守护进程监听处理“孵化新进程”的请求
加载ZygoteInit类,源代码:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
registerZygoteSocket()为zygote命令连接注册一个服务器套接字。
preloadClassed “preloaded-classes”是一个简单的包含一系列需要预加载类的文本文件,你可以在<Android Source>/frameworks/base找到“preloaded-classes”文件。
preloadResources() preloadResources也意味着本地主题、布局以及android.R文件中包含的所有东西都会用这个方法加载。
在这个阶段,你可以看到启动动画。


如上面所述,zygote是通过init脚本启动的:


service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
socket zygote stream 666
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
上面脚本的含义为作为孵化进程(-Xzygote参数),通过system/bin/app_process启动zygote服务,同时启动SystemServer(--start-system-server参数)进程。该服务对应的可执行文件app_process的源码位于frameworks/base/cmds/app_process。


zygote服务在函数app_main.cpp:main()中启动。该函数的关键代码有两处:
1)创建AndroidRuntime对象:


AppRuntime runtime; // android运行空间
2)启动zygote和systemserver:


runtime.start("com.android.internal.os.ZygoteInit",
startSystemServer ? "start-system-server" : "");
方法start()代码位于frameworks/base/core/jni/AndroidRuntime.cpp中,主要逻辑如下:
   ---调用startVM创建虚拟机:内部使用以下代码创建vm对象:


if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
LOGE("JNI_CreateJavaVM failed\n");
goto bail;
}
---注册底层功能的JNI函数到JNIEnv: startReg()
   ---调用env->GetStaticMethodID和env->CallStaticVoidMethod()函数运行Java类ZygoteInit的main()
由此可见虚拟机启动后执行的第一个Java类是ZygoteInit.java,并进入ZygoteInit.java:main()函数中。在main函数中实现了以下逻辑:
1)启动服务端Socket端口:
调用registerZygoteSocket()实现,主要用于接受处理创建新进程的请求。
  2)预加载指定的java类和资源:
调用preloadClasses()预加载指定的java类,调用preloadResources()预加载指定的Resources。特别说明的是孵化器进程会把这些预先加载的类和资源共享给所有APK应用进程,这样有效的解决了Framework类和资源共享的问题。
  3)启动System Server进程:
调用startSystemServer()创建(fork)SystemServer进程。该函数的关键代码有三处:
---设定启动进程的相关信息:比如进程名称、启动后装载的第一个java类
---调用forkSystemServer()从当前的zygote进程孵化出新的进程
---调用函数hanldeSystemServerProcess()关闭从Zygote进程继承过来的Socket,调用RuntimeInit.zygoteInit()启动SystemServer.java:main()函数
  4)循环监听孵化新Dalvik进程的请求:
调用runSelectLoopMode()进入无限循环:监听客户端socket连接,根据请求孵化新的应用进程。Process类中保存了客户端socket,并由ActivityManagerService管理该客户端。每当需要启动新的Dalvik应用进程时,ActivityManagerService都会通过该socket客户端与Zygote进程的socket服务端进行通信,请求Zygote孵化出新的进程。
  至此,启动zygote服务工作完成,需要说明的是zygote进程即为app_process可执行程序所在进程。

4、System Server进程启动

SystemServer进程在Android的运行环境中扮演了“神经中枢”的作用,Android应用能够直接交互的大部分系统服务都在该进程中运行,如WindowManagerServer、ActivityManagerSystemService、PackageManagerServer等,这些系统服务都是以独立线程的方式存在于SystemServer进程中。System Server进程的主要功能:

1)加载android servers底层函数库
2)启动android系统中的native服务
3)创建、注册并启动Android的系统服务,在独立线程中运行
4)创建Looper消息循环,处理System Server进程中的事件消息


在zygote进程中调用函数startSystemServer()创建和启动Server进程,进程首先执行的函数是SystemServer.java:main()。该函数函数实现的主要逻辑为:
1)加载android_servers函数库
2)启动native服务:
调用本地函数init1()实现,该函数的源码位于文件frameworks/base/services/jni/com_android_server_systemService.cpp中,涉及的函数system_init()实现在文件frameworks/base/cmds/system_server/library/system_init.cpp中。
3)启动Android系统的各种系统服务:
调用函数init2()实现,该函数首先创建了一个ServerThread对象,该对象是一个线程,然后直接运行该线程,如以下代码所示:


public static final void init2() {
Slog.i(TAG, "Entered the Android system server!");
Thread thr = new ServerThread();
thr.setName("android.server.ServerThread");
thr.start();
}
从ServerThread的run()方法内部开始真正启动各种服务线程。
---创建Android系统服务对象,并注册到ServiceManager
---在SystemServer进程中建立Looper消息循环:通过Looper.prepare和Looper.loop来实现
   ---系统就绪通知:调用systemReady()通知各个服务


System Server进程启动过程中最核心的一步是“启动Android系统的各种系统服务”,这些系统服务构成了整个Android框架的基础(如图所示),通过Binder IPC为上层应用提供各种功能。Zygote创建新的进程去启动系统服务。你可以在ZygoteInit类的”startSystemServer”方法中找到源代码。介绍下几个重要系统服务的功能。


1)ActivityManagerService
Activity管理服务,主要功能包括:
---统一管理和调度各应用程序的Activity,维护系统中运行的所有应用Task和Activity
---内存管理:应用程序关闭时对应进程还在运行,当系统内存不足时根据策略kill掉优先级较低进程
---进程管理:维护和管理系统中运行的所有进程,并提供了查询进程信息的API
---Provider、Service和Broadcast管理和调度

2)WindowManagerService
窗口管理服务,主要功能包括为应用程序分配窗口,并管理这些窗口。包括分配窗口的大小、调节各窗口的叠放次序、隐藏或者显示窗口,程序退出时删除窗口。

3)PackageManagerService
程序包管理服务,主要功能为:
---根据intent查找匹配的Activity、Provider以及Service
---进行权限检查,即当应用程序调用某个需要一定权限的函数时,系统能够判断调用者是否具备该权限
---提供安装、删除应用程序的API

4)NotificationManagerService
通知管理服务,负责管理和通知后台事件的发生等,这个和statusbar服务结合在一起,一般会在statusbar上添加响应图标。用户可以通过这知道系统后台发生了什么事情。

5)AudioService
音频管理服务,AudioFlinger的上层管理封装,主要是音量、音效、声道及铃声等的管理。

6)TelephonyRegistry
电话服务管理,用于监听和上报电话状态,包括来电、通话、信号变化等。

到这里,Android Framework的启动已经完成,框架中提供的各种服务也已经就绪,可以正常运行并响应处理应用的各种操作请求。 一旦系统服务在内存中跑起来了,Android就完成了引导过程。在这个时候“ACTION_BOOT_COMPLETED”开机启动广播就会发出去。


核心服务:

启动电源管理器;
创建Activity管理器;
启动电话注册;
启动包管理器;
设置Activity管理服务为系统进程;
启动上下文管理器;
启动系统Context Providers;
启动电池服务;
启动定时管理器;
启动传感服务;
启动窗口管理器;
启动蓝牙服务;
启动挂载服务。
其他服务:
启动状态栏服务;
启动硬件服务;
启动网络状态服务;
启动网络连接服务;
启动通知管理器;
启动设备存储监视服务;
启动定位管理器;
启动搜索服务;
启动剪切板服务;
启动登记服务;
启动壁纸服务;
启动音频服务;
启动耳机监听;
启动AdbSettingsObserver(处理adb命令)。
启动完成后需要启动home应用进入手机主界面,这是下面第5节讲解的。


5、Home应用启动

在ServerThread:run()函数的最后调用了ActivityManagerService.self().systemReady方法,该方法实现了如下代码用于启动第一个Activity:

mMainStack.resumeTopActivityLocked(null);
public void systemReady(final Runnable goingCallback) {    ……    //ready callback       if (goingCallback != null)              goingCallback.run();       synchronized (this) {              // Start up initial activity.              // ActivityStack mMainStack;              mMainStack.resumeTopActivityLocked(null);       }……}final boolean resumeTopActivityLocked(ActivityRecord prev) {  // Find the first activity that is not finishing.  ActivityRecord next = topRunningActivityLocked(null);  if (next == null) {    // There are no more activities!  Let's just start up the    // Launcher...    if (mMainStack) {      //ActivityManagerService mService;      return mService.startHomeActivityLocked();    }  }  ……}


由于系统刚启动时没有任何Activity对象,代码会调用ActivityManagerService:startHomeActivityLocked函数启动Home应用:
        Intent intent = new Intent(            mTopAction,            mTopData != null ? Uri.parse(mTopData) : null);        intent.setComponent(mTopComponent);        if (mFactoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) {            intent.addCategory(Intent.CATEGORY_HOME);        }

最后用一张图片总结下 图片出处:http://blog.csdn.net/zirconsdu/article/details/8574049



参考资料: http://blog.jobbole.com/67931/ http://www.xuebuyuan.com/1029708.html http://www.cnblogs.com/bastard/archive/2012/08/28/2660389.html

更多相关文章

  1. 简单音乐播放实例的实现,Android(安卓)Service AIDL 远程调用服
  2. 消息推送服务厂家对比 个推 - 极光 - 信鸽
  3. Android(安卓)AM命令及使用
  4. Flutter踩坑记录
  5. Android(安卓)优雅地退出App
  6. 简述Android(安卓)framework之AMS、PMS、WMS
  7. android启动过程配置文件的解析与语法 .
  8. widget小插件--时间显示
  9. 【Android(安卓)开发教程】在服务中执行耗时操作

随机推荐

  1. Android(安卓)ScrollView的使用
  2. 【Android-tips】 Unable to execute dex
  3. Android(安卓)高德地图API学习笔记
  4. android中GridView关于间距的属性值介绍
  5. FIDO框架分析3(FIDO UAF Android客户端)
  6. Android如何获取视频预览图(或首帧)和获取
  7. Android(安卓)AppCompatActivity的Action
  8. Android计算地图上两点距离
  9. android10.0连接wifi后提示“已连接,但无
  10. android编译时出现'Unable to resolve ta