去年通读了深入理解Java虚拟机,对JVM的一系列特性有了系统的了解,然而作为Android开发,却还没有对Android平台特有的两代虚拟机做更为细致的学习,属实有点说不过去。在车机Android系统app的开发中,几乎涉及不到apk的动态安装卸载,一般是参与系统集成编译完就发布镜像,烧写后一直运行。所以对此专题,了解甚少。
JVM跨平台特性的原理是 字节码 ,字节码是一种中间代码,它是一种 面向虚拟机 的代码,而不是面向硬件的代码。每个JRE编译的时候针对每个平台编译,因此下载JRE(JVM、Java核心类库和支持文件)的时候是分平台的,JVM的作用是把平台无关的.class里面的字节码翻译成平台相关的机器码,来实现跨平台。
为何Android不直接使用JVM 从问题寻找答案,总是一个有效的学习方法,为什么Android平台当年不直接使用JVM作为硬件抽象之上的虚拟机供应用运行呢?
在早期(Android 4.4之前),Android使用的是Dalvik虚拟机,这是谷歌为安卓系统专门设计和开发的一款虚拟机,主要用于在资源受限的移动设备上高效运行应用程序。不直接使用JVM,主要有以下几个原因:
资源限制: 早期的移动设备内存和存储空间都很有限。 JVM需要较大的内存和存储空间 ,不适合资源受限的移动设备。Dalvik虚拟机针对移动设备进行了优化,更加轻量级。 性能考虑: JVM主要针对服务器和桌面环境优化,移动设备上,可能没有桌面和服务器上那么强大的性能,故JVM在移动设备上性能表现可能不佳。 Dalvik虚拟机采用了寄存器架构 ,相比 JVM的栈架构 在移动设备上有更好的性能表现。 许可证问题: JVM受Oracle公司的许可证限制,Android为了避免潜在的法律风险,选择开发自己的虚拟机。这也是一个为人乐道的历史原因。 定制化需求: Android需要一个 更加定制化的解决方案 来满足移动平台的特殊需求,如电源管理、内存管理等。Dalvik虚拟机可以更好地与Android系统集成。 字节码格式: Dalvik使用的.dex格式比Java的.class文件更紧凑,更适合移动设备有限的存储空间。 多进程架构: Android的应用运行在独立的进程中,每个进程有自己的Dalvik虚拟机实例,这种架构更适合移动操作系统的需求。 安全性考虑: Dalvik虚拟机在安全性方面也有一定的优势,如对内存访问的控制和对异常的处理。 事实证明,Android使用Dalvik虚拟机的选择是正确的,如果直接使用JVM,在以上这些因素的影响下,Android系统可能真的发展不起来。
Dalvik虚拟机 JVM虚拟机里面,解释运行的是class文件,而在Dalvik虚拟机里面,解释运行的是dex(即“Dalvik Executable”)文件。
dex文件 回顾一下class文件的组成:
魔数:用于标识文件类型,通常为0xCAFEBABE。 版本号:用于标识class文件的版本号,包括主版本号和次版本号。 常量池:用于存储常量,包括字符串、类名、字段名、方法名等。 访问标志:用于标识类的访问权限,如public、final等。 类信息:包括类名、父类名、接口名等。 字段信息:包括字段名、字段类型、访问标志等。 方法信息:包括方法名、方法类型、访问标志等。 属性信息:用于存储类的附加信息,如注解、泛型信息等。 为了减小执行文件的体积,安卓的Dalvik虚拟机选择dex文件来替代class文件。
class文件合并为dex文件这个过程,主要由 Android SDK 中的dx工具(在较新的Android Gradle插件中被d8工具替代)完成。通过这个转换过程,多个class文件被合并转换成一个更紧凑、更适合移动设备的dex文件,从而减小了应用的大小,提高了加载和执行效率。
class文件转换成dex文件的过程称为”dexing”,主要通过以下步骤完成:
收集所有的class文件: 首先,系统会收集所有需要转换的class文件。这些文件通常来自于你的Java源代码编译后的结果,以及你项目中使用的库。 解析class文件: 每个class文件都会被解析,提取其中的常量池、方法、字段等信息。 合并常量池: dex格式的一个主要优化是合并所有class文件的常量池。相同的字符串、类型和方法引用只会在dex文件中出现一次,大大减少了重复数据。 重新编码数据: class文件中的数据会被重新编码为dex格式。这包括将Java字节码转换为Dalvik字节码,以及重新组织类、方法和字段的结构。 优化: 在转换过程中,还会进行一些优化,例如删除未使用的字段和方法,内联一些简单的方法调用等。 生成dex文件: 最后,所有处理后的数据被写入到一个或多个dex文件中。一个dex文件可以包含多个class的内容。 验证: 生成的dex文件会经过验证,确保其格式正确,可以被Dalvik虚拟机(或ART运行时)正确加载和执行。 二者格式对比如下:
JVM和Dalvik运行流程 JVM基于栈操作 每个Java线程都有自己的 Java虚拟机栈 ,用于存储方法调用的信息,包括局部变量、部分结果和方法调用/返回等。
当一个方法被调用时,会在栈上创建一个新的 栈帧(Stack Frame) 。栈帧包含局部变量表,操作数栈,指向运行时常量池的引用,方法返回地址等信息。
JVM使用基于栈的指令集,这意味着大多数操作都是通过压栈和出栈来完成的。例如:
int foo ( int a , int b ) {
return ( a + b ) * ( a - b );
}
转换后的字节码指令为:
int foo(int, int);
Code:
0: iload_1 // 将局部变量a压入栈
1: iload_2 // 将局部变量b压入栈
2: iadd // 弹出栈顶的两个元素,相加,结果压入栈
3: iload_1 // 将局部变量a压入栈
4: iload_2 // 将局部变量b压入栈
5: isub // 弹出栈顶的两个元素,相减,结果压入栈
6: imul // 弹出栈顶的两个元素,相乘,结果压入栈
7: ireturn // 返回栈顶元素作为方法结果
Dalvik基于寄存器操作 在Dalvik虚拟机中,寄存器是虚拟的,不同于物理CPU中的硬件寄存器。这些虚拟寄存器实际上是存储在内存中的,用于存储局部变量、方法参数、中间计算结果等。
Dalvik虚拟机使用基于寄存器的指令集,这意味着大多数操作都是通过寄存器来完成的。还是上面同样的计算方法:
int foo ( int a , int b ) {
return ( a + b ) * ( a - b );
}
转换后的字节码指令
0000: add-int v0, v3, v4
0002: sub-int v1, v3, v4
0004: mul-int/2addr v0, v1
0005: return v0
add-int是一个需要两个操作数的指令,其指令格式是:add-int vAA, vBB, vCC。其指令的运算过程,是将后面两个寄存器中的值进行(加)运算,然后将结果放在(第一个)目标寄存器中。其余类似
计算流程如下图:
内存对比 一个JVM进程所处的Java虚拟机的内存可以用下面这张热门图片概括:
Dalvik虚拟机的内存模型略有不同。
Dalvik内存模型 Dalvik虚拟机用来分配对象的堆划分为两部分,一部分叫做Active Heap,另一部分叫做Zygote Heap。
Android系统启动后,会有一个Zygote进程创建第一个Dalvik虚拟机,它只维护了一个堆。以后启动的所有应用程序进程是被Zygote进程fork出来的,并都持有一个自己的Dalvik虚拟机。在创建应用程序的过程中,Dalvik虚拟机采用 COW策略 复制Zygote进程的地址空间。
COW策略,即Copy-On-Write,一开始的时候(未复制Zygote进程的地址空间的时候),应用程序进程和Zygote进程共享了同一个用来分配对象的堆。当Zygote进程或者应用程序进程对该堆 进行写操作时,内核就会执行真正的拷贝操作 ,使得Zygote进程和应用程序进程分别拥有自己的一份拷贝,这就是所谓的COW。因为copy是十分耗时的,所以必须尽量避免copy或者尽量少的copy。
为了实现这个目的,当创建第一个应用程序进程时,会将已经使用了的那部分堆内存划分为一部分作为 Zygote堆 ,还没有使用的堆内存划分为另外一部分称为 Active堆 。
Zygote Heap 堆内存中, 有一部分区域的内存是只读的, 如系统相关的库, 共享库, 预置库, 这些内存数据所有应用公用。这些预加载的类、资源和对象可以在Zygote进程和应用程序进程中做到 长期共享 。这样既能减少拷贝操作,还能减少对内存的需求。
这样只需把zygote堆中的内容复制给应用程序进程就可以了。以后无论是Zygote进程,还是应用程序进程,当 它们需要分配对象的时候,都在Active堆上进行 。这样就可以使得Zygote堆尽可能少地被执行写操作,因而就可以减少执行写时拷贝的操作。
DVM特点内存模型图
Card Table:用于 DVM Concurrtent GC,当第一次进行垃圾标记后,记录垃圾信息。 Heap Bitmap:分为两个,Live Bitmap 用来记录上次 GC 存活的对象,Mark Bitmap 用来记录这次 GC 存活的对象。 Mark Stack:在 GC 的标记阶段使用的,用来遍历存活的对象。
DVM垃圾回收 Dalvik垃圾收集主要是 mark-sweep 算法实现的。 mark-sweep 算法分为两个阶段,即mark阶段和sweep阶段。Dalvik的GC过程,可以大致归纳为如下过程:
整个标记开始之前,首先遍历一次堆地址空间。 Mark阶段: 从对象的根集对象开始标记引用对象。 Sweep阶段: 回收没有被标记的对象占用的内存。 Mark标记阶段 这里涉及到的一个核心概念就是我们怎么标记对象有没有被引用的,换句说就是通过什么数据结构来描述对象有没有被引用。
事实上,总共使用了两个bitmap, 一个是Live bitmap,一个是Mark bitmap。 这两个bitmap里的每一位对应一个对象,某个对象被引用了,就标1,没引用就标0。
Livebitmap主要用来标记上一次GC时被引用的对象,也就是那些没有被回收的对象,而markbitmap主要用来标记本轮 当前GC有被引用 的对象。因此那些 在Live bitmap中标为1,而在mark bitmap中标为0 的对象,就是需要回收的对象。mark bitmap实际上就是live bitmap的一个子集。
标记的STW现象
在mark阶段,要求除了GC线程外,其他的线程都需要停止,否则就可能不能正确的标记每个对象,因为可能刚标记完又被修改引用等等情况的发生。这种现象叫stop the world,会导致该应用程序中止执行,
在整个mark开始时,GC会先不得不中止一次程序的运行,从而对堆地址空间进行一次遍历,这次遍历可以获得每一个应用程序分配的对象,就能确认每个对象在内存堆中的大小、起始地址等等信息。 这个停顿在dalvik里是不得不做的事情 ,每次GC都会必须触发一次堆地址空间的遍历引起的停顿。
为了减少stop the world带来的负面影响,dalvik的GC采用了分阶段并行标记Concurrent的方案。分为了两个子阶段:
第一个阶段是标记根集对象,这个阶段是不允许GC线程之外的线程运行的。 第二个阶段是标记根集对象引用的对象,允许其他线程运行。 所谓根集对象,就是指在GC线程开始的时候,那些 被全局变量、栈变量和寄存器等引用的对象 。通过这些根集变量,可以顺着它们找到其余的被引用变量,其实这就是可达性分析。比如说,假如发现了一个栈变量引用一个对象,而这个对象又引用了另外一个对象,那这个被引用的对象也会被标记为正在使用。这个标记“被根集对象引用的对象”的过程就是第二个子阶段。
并行策略很容易想到的一个问题,就是在第二个阶段执行的过程中,如果某个运行的线程修改了一个对象了内容,由于很有可能引用了新的对象,所以这个对象也必须要记录起来。否则会发生被引用对象还在使用却被回收。
这种情况出现在只进行部分GC的情况,这时候 Card Table 的作用就是用来记录非GC堆对象对GC的堆对象的引用。
Dalvik的堆空间,分为zygote heap 和 active heap。前者主要存放一些在zygote时就分配的对象,后者主要用于之后新分配的空间,
Dalvik虚拟机进行部分垃圾收集时,实际上就是 只收集在Active heap上分配的对象 。Card Table就是用来记录在Zygote heap上分配的对象 在GC执行过程中 对在Active heap上分配的对象的引用。
Card table由一系列card组成,一个card实际上就是一个字节,它的值分为clean和dirty两种。如果一个Card的值是 CLEAN ,就表示与它对应的对象在Mark第二个子阶段 没有被程序修改过 。如果一个Card的值是 DIRTY ,就意味着 被程序修改过 。对于这些被修改过的对象。需要在Mark第二子阶段结束之后,再次禁止GC线程之外的其它线程执行,以便GC线程再次根据Card Table记录的信息对被修改过的对象引用的其它对象 进行重新标记 。这个二次标记的过程就是非并行的,确保本次Mark流程的标记都是准确的。
由于Mark 第二子阶段被修改的对象不会很多 ,这样就可以保证第二次子阶段结束后,再次执行标记对象的过程是很快的,因而此时对程序造成的停顿非常小。
在mark阶段,主要是标记的第二个子阶段,dalvik是采用递归的方式来遍历标记对象。但是这个递归并不是像一般的递归一样借助一个递归函数来实现,而是使用一个叫做mark stack的栈空间实现。大致过程是:一个被引用的对象在标记的过程中,先被标记,然后放在栈中,等该对象的父对象全部被标记完成后,依次弹出栈中的每一个对象,并标记其引用,然后把其引用再丢到栈中。
采用mark stack栈而不是函数递归的好处是:首先可以 采用并行的方式 来做,将栈进行分段,多个线程共同将栈中的数据递归标记。其次,可以 避免函数堆栈占用较深 。
至此,差不多介绍了dalvik的GC的mark阶段的过程。我们可以发现,在mark阶段,一共有3次停顿:
一次是在标记开始前遍历堆地址空间的停顿 一次是在标记的第一个阶段标记所有根集对象时的停顿 还有一次是在标记的第二个子阶段完成后处理card table时的停顿。 这3次停顿的时间直接影响了android上应用程序的表现,尤其是 卡顿现象 ,因此ART在这块有重点改进,等会介绍ART上的过程。
Sweep清除阶段 其实sweep阶段就很简单了,在mark阶段已经提到过,GC时回收的是在live bitmap里标为1而在mark bitmap里标为0的对象。
而这个mark bitmap实际上就是live bitmap的子集,因此在sweep阶段只需要处理二者的差集即可,在回收掉相应的对象后,只需要 再把live bitmap和mark bitmap的指针调换一下 ,即这次的mark bitmap作为下一次GC时的live bitmap,然后清空live bitmap,等到下一次GC流程开始,用来标记下一次的可回收对象。
Sweep的过程,在ART里没什么太大变化,而由于在android 5.0源码中已经去掉了dalvik,这环节的具体解释就在ART部分分析,但是实际上在sweep阶段dalvik和ART二者没有太大区别,因为主要只是处理相应的bitmap的对应的对象的内存,ART也没有什么优化的地方。
Art虚拟机 Art虚拟机是在Android 4.4就引入了,这个版本上留了一个开关供调试选择,其实就是一个标记位,手动开关更改这个值,重启系统之后就是使用上次选择的新的虚拟机方案。5.0版本之后,彻底移除了Dalvik虚拟机,完全使用ART虚拟机。
Dalvik之所以要被ART替代包含下面几个原因:
Dalvik是为32位设计的,不适用于64位CPU。 单纯的字节码解释加JIT编译的执行方式,性能要弱于本地机器码的执行。 无论是解释执行还是JIT编译都是单次运行过程中发生,每运行一次都可能需要重新做这些工作,这样做太浪费资源。 原先的垃圾回收机制不够好,会导致卡顿。 先接上文介绍下Art的垃圾回收策略,再介绍JIT,AOT等编译流程。
垃圾回收 ART同样采用了自动GC的策略,并且同样不可避免的使用到了经典的mark-sweep算法。
Art虚拟机的标记清除垃圾回收,根据轻重程度不同,分为三类, sticky,partial,full 。可以看到,ART里的GC的改进,首先就是收集器的多样化。 而根据GC时是否暂停所有的线程分类并行和非并行两类。所以在ART中一共定义了6个mark-sweep收集器。参看art/runtime/gc/heap.cc可见。
根据不同情况,ART会选择不同的GC collector进行GC工作。其实最复杂的就是 Concurrent Mark Sweep 收集器 。如果理解了最复杂的Concurrent Mark Sweep算法,其他5种GC收集器的工作原理就也理解了。同样的,垃圾回收工作从整体上可以划分两个大的阶段:Mark 和 Sweep。
Mark阶段 最重要的提升就是这个阶段 只暂停线程一次。将dalvik的三次缩短到一次 ,得到了较大的优化。和dalvik一样,标记阶段完成的工作也是完成从根集对象出发,进行递归遍历标记被引用的对象的整个过程。用到的主要的数据结构也是同样的 live bitmap和mark bitmap,以及card table和在递归遍历标记时用到的mark stack。
一个被引用的对象在标记的过程中先被标记,然后存入mark stack中,等待该对象的父对象全部被标记完成,再依次弹出栈中每一个对象然后,标记这个对象的引用,再把引用存入mark stack,重复这个过程直至整个栈为空。这个过程对mark stack的操作使用以及递归的方法和dalvik的递归过程是一样的。
但是在dalvik小节里提到了,在标记时mark阶段划分成了两个阶段,第一小阶段是禁止其他线程执行的,在mark两个阶段完成后处理card table时也是禁止其他线程执行的。
在ART里做出了改变,即 先Concurrent标记两遍 ,也就是说两个子阶段都可以允许其他线程运行了。然后 再Non-Concurrent标记一遍 。这样就大大缩短了dalvik里的第二次停顿的带来的卡顿时间。这个改进非常重要。
allocation stack
在标记开始阶段,有别于dalvik的要暂停所有线程 进行堆地址空间的遍历 来确定所有的对象的大小,地址信息。ART去掉了这个过程,替代的是 增加了一个叫作allocation stack结构 ,所有新分配的对象的信息会记录到allocation stack。
然后在Mark的时候,再在Live Bitmap中打上live的标记。Allocation stack和live stack其实是一个工作栈和备份栈。当在GC的时候,需要处理allocation stack,那么会把两个stack互换。新分配的对象会压到备份栈中,这个时候备份栈就当作新的工作栈。之前的allocation stack就当作本轮gc的 所有堆对象的记录栈 。这样一来,dalvik在每一次GC时产生的第一次停顿就完全消除了,从而产生了巨大的性能提升。
Live Stack和Live Bitmap名称很像,但其实是完全不同的东西。Live Bitmap用于记录当前VM进程中所有存在的对象,包括未标记和已标记的对象。与Mark Bitmap配合用于确定可回收的垃圾对象。Live Stack是allocation stack的备份栈,主要用于优化GC性能,快速访问新分配对象。
关于card table,和dalvik依旧类似,每个card用一个字节来描述。ART里多了一个结构ModUnionTable,是和card table配合使用的。
前面在ConCurrent的情况下,经过了两轮的递归遍历,基本上已经标记扫描的差不多了。但由于应用程序主线程是在一直运行的,不可避免地会修改之前已经mark过的bitmap。因此,需要 第三遍扫描,这次就需要在stop the world的情况下进行遍历 ,主要过程也就是上文提到的对card table的操作等等。
这次遍历扫的时候,除了重新标记根集以外,还需要扫描card table中Dirty Card的部分。关于live bitmap和mark bitmap的使用,ART和dalvik在这一块没有多少区别。Live Bitmap记录了当前存在于VM进程中所有的未标记的对象和标记过的对象。Mark Bitmap经过了Mark 的过程,记录了当前VM进程中所有被引用的object。Live Bitmap和Mark Bitmap中间的差集,便是所有为被系统引用的object,即是可以回收的垃圾了。
经过了前面3次扫描以及Mark,我们的mark bitmap已经很完整了。但是值得注意的是,由于Sweep的操作是对应于live bitmap,即那些在live bitmap中标记过,却在mark bitmap中没有标记的对象。也就是说,mark bitmap中标记的对象是live bitmap中标记对象的子集。
但目前为止live bitmap标记的对象还不是最全,因为前文有提到过,为了消除dalvik的第一次停顿,在扫描期间,Art计入了allocation stack中的对象,还没有标记。Allocation stack先“搁置”起来不让后面的主线程使用,启用备份的的live stack。
void Heap: : SwapStacks () {
allocation_stack_ . swap ( live_stack_ );
}
即在下一次垃圾回收开始时,将allocation stack和live stack进行交换。确保本轮处理的对象信息是最新的。
Sweep阶段 在完成了mark阶段后,对应已经标好的live bitmap和mark bitmap,需要进入sweep来回收相应的垃圾。Sweep阶段就是把那些二者的差集所占用的内存回收掉。
Finish阶段 不同于Dalvik,ART中可以归纳为有一个第三个阶段,就是类似的一个finish阶段。
void MarkSweep :: FinishPhase () {
base :: TimingLogger :: ScopedSplit split ( "FinishPhase" , & timings_ );
// Can't enqueue references if we hold the mutator lock.
Object * cleared_references = GetClearedReferences ();
Heap * heap = GetHeap ();
timings_ . NewSplit ( "EnqueueClearedReferences" );
heap -> EnqueueClearedReferences ( & cleared_references );
......
}
因为之前说过mark bitmap是live bitmap的一个子集,而mark bitmap中包含了所有的正在被引用的的非垃圾的对象,因此需要交换mark bitmap和live bitmap的指针,使mark bitmap作为下一次GC的live bitmap,并且重置新的mark bitmap。
//Clear all of the spaces' mark bitmaps.
for ( const auto & space : GetHeap () -> GetContinuousSpaces ()) {
if ( space -> GetGcRetentionPolicy () != space :: kGcRetentionPolicyNeverCollect ) {
space -> GetMarkBitmap () -> Clear ();
}
}
mark_stack_ -> Reset ();
另外,需要指出的是,由于mark stack的目的是为了方便标记的递归,所以在Finish阶段,也需要把mark stack给清空,至于实现可以看以上代码行。
sticky mark sweep收集器 其实sticky mark sweep的主要步骤也是和mark sweep的过程大致一样,主要完成三次并发的mark阶段,然后进行一个stop the world的非并发进行一次对堆对象的遍历。
void StickyMarkSweep :: BindBitmaps () {
PartialMarkSweep :: BindBitmaps ();
WriterMutexLock mu ( Thread :: Current (), * Locks :: heap_bitmap_lock_ );
// For sticky GC, we want to bind the bitmaps of all spaces as the allocation stack lets us
// know what was allocated since the last GC. A side-effect of binding the allocation space mark
// and live bitmap is that marking the objects will place them in the live bitmap.
for ( const auto & space : GetHeap () -> GetContinuousSpaces ()) {
if ( space -> GetGcRetentionPolicy () == space :: kGcRetentionPolicyAlwaysCollect ) {
BindLiveToMarkBitmap ( space );
}
}
GetHeap () -> GetLargeObjectsSpace () -> CopyLiveToMarked ();
}
但是可以通过实现方法发现,有一个 getGcRetenionPolicy ,获取的是一个枚举。
Sticky Mark Sweep 和 Full Mark Sweep 的主要区别为,Full Mark Sweep:回收整个堆内存空间。Sticky Mark Sweep:只回收 自上次 GC 以来新分配的对象和被修改过的对象所在的区域 。
只有符合总是收集的条件的,就把live bitmap和mark bitmap绑定起来。其余的过程和full是一样的。Sticky Mark sweep只扫描自从上次GC后被修改过的堆空间,并且也只回收自从上次GC后分配的空间。Sticky是只回收kGcRetentionPolicyAlwaysCollect的space。不回收其他两个,因此sticky的回收的力度是最小的。作为最全面的full mark sweep, 上面的三个策略都是会回收的。
partial mark sweep收集器 这是mark sweep收集器里使用的最少的GC收集策略,回收部分堆空间,通常包括年轻代和部分老年代。
按照官方文档,一般是使用sticky mark sweep较多。这里有一个概念就是吞吐率,即一次GC释放的字节数和GC持续的时间(秒)的比值。由于一般是sticky mark sweep进行GC,所以当上次GC的吞吐率小于同时的partial mark sweep的吞吐率时,就会把下次GC收集器从sticky变成partial。但是在partial执行一次GC后,就仍然会恢复到stick mark sweep收集器。
阅读源码发现,partial重写了父类的成员函数。
其实分析这些可以发现,从full mark sweep到partial mark sweep到stick mark sweep,GC的力度是越来越小的,因为 可以回收的越来越少 。之所以说回收力度大,就是指可以回收的space多,比如上图的partial, 是不回收 kGcRetentionPolicyFullCollect ,但是是会回收 kGcRetentionPolicyAlwaysCollect 的space的。
kGcRetentionPolicyFullCollect 表示该内存空间只在完全(Full)垃圾回收时才会被收集。kGcRetentionPolicyAlwaysCollect 表示该内存空间在每次垃圾回收时都会被收集,无论是哪种类型的 GC。
因此partial mark sweep每次执行一次GC后,就会自动切换到sticky策略,这样才能使系统更流畅得进行GC,并减少了GC带来的消耗。。
其他区别 其实观察space目录的文件可以发现,有一个新的概念就是large object space。事实上,ART还引入了一个新的的方法就是 大对象存储空间(large object space,LOS) ,这个空间与堆是相互独立的,但是仍然是驻留在应用程序的内存空间中。方便让ART可以更好的管理较大的对象,比如android里的bitmap。在dalvik中,在对堆空间进行分段时,占用空间较大的对象会带来一些问题。例如,在堆里分配一个 bitmap大对象时,由于占用空间较大,可能引起GC的启动次数也会增加,从而增加了开销 。有了LOS,GC收集器因堆空间分段而引发调用次数将会大大降低,这样垃圾收集器就能做更加合理的内存分配,从而降低运行时开销。
实验对比证明 老罗的博客里做的一些实验对比,证明了ART的GC的性能确实比dalvik的要好。
可以看到,在dalvik模式下刚启动支付宝的几秒内,触发了28次GC事件,总共停顿耗时4657ms。而在ART模式下,可以看到一共触发了2次GC事件,共耗时231.099ms。
在dalvik模式下刚启动百度地图的几秒内,触发了26次GC事件,总共停顿耗时5371ms。而在ART模式下,可以看到一共触发了4次GC事件,共耗时497.162ms。
对比可以看到,ART下的GC的性能明显提升了,几乎可以说是 提升了十倍左右 ,这是一个数量级的提升,GC环节带来的性能提升还是非常明显。
复制算法和标记清除算法哪个更好? 在垃圾回收中,复制算法 和标记-清除算法 的性能优劣取决于具体场景,两者的速度差异主要与堆内存状态、对象存活率以及硬件条件相关。
复制算法 ,对于 对象存活率低时 (如新生代垃圾回收),只需复制少量存活对象,效率极高。复制后堆空间紧凑,无碎片,分配新对象只需移动指针。适合对延迟敏感的场景(如Young GC)。主要缺点有两个,一是内存浪费,需保留一半空间作为空闲区(空间利用率50%)。二是存活率高时性能骤降,若大部分对象存活(如老年代),复制开销比较大。
标记-清除算法 ,在对象存活率高时,只需标记并清理少量垃圾,无需复制存活对象。内存方面,无需预留空间,适合堆内存紧张的场景。缺点就是内存碎片化,可能导致分配大对象时触发压缩。还有两次遍历开销**:标记和清除阶段需扫描整个堆,存活对象多时较慢。
实际应用 现代GC通常混合使用多种算法以平衡吞吐量、延迟和空间开销。
新生代 :使用复制算法(如Serial、Parallel Scavenge)。老年代 :使用标记-清除或标记-整理(如CMS、G1)。若堆内存充足且对象存活率低,复制算法更快。若存活率高或内存有限,标记-清除更高效。
编译流程 JIT JIT的全称是Just In Time,即 即时编译 ,它”即时”地进行编译,而不是在程序执行前完成所有编译工作。JIT是在运行时进行字节码到本地机器码的编译,这也是为什么Java普遍被认为效率比C++差的原因。无论是解释器的解释还是运行过程中即时编译,都比C++编译出的本地机器码执行多了一个耗费时间的过程。
AOT AOT,即Ahead of Time,即 提前编译 。当APK 在安装的时候 ,系统会通过一个名称为dex2oat的工具 将APK中的dex文件编译成包含本地机器码的oat文件存放下来 。这样做之后,在程序执行的时候,就可以直接使用已经编译好的机器码以加快效率。
下图描述了Dalvik虚拟机与(Android 5.0上的)ART虚拟机在安装APK时的区别:
从这幅图中我们看到:
在Dalvik虚拟机上,APK中的Dex文件在安装时会被优化成odex文件,在运行时,会被JIT编译器编译成native代码。 在ART虚拟机上安装时,Dex文件会直接由dex2oat工具翻译成oat格式的文件,oat文件中既包含了dex文件中原先的内容,也包含了已经编译好的native代码。 纯 AOT 存在的问题 应用程序编译生成的OAT文件会引用Framework中的代码。一旦系统发生升级,Framework中的实现发生变化,就需要重新修正所有应用程序的OAT文件,使得它们的引用是正确的,这就需要 重新编译所有的应用 。
在应用安装的时候也是一样的问题,AOT会在安装时将应用的dex文件编译为oat文件存下来,让安装时间对比Dalvik虚拟机也更长。
第三个问题是,编译生成的Oat文件中,既包含了原先的Dex文件,又包含了编译后的机器代码。而实际上,对于用户来说,并非会用到应用程序中的所有功能,因此很多时候编译生成的机器码是一直用不到的。一份数据存在两份结果(尽管它们的格式是不一样的)显然是一种存储空间的浪费。
混合编译 在 Android 7.0 中,Google又为Android添加了即时 (JIT) 编译器。JIT和AOT的配合,是取两者之长,避两者之短:在APK安装时,并不是一次性将所有代码全部编译成机器码。而是在实际运行过程中,对代码进行分析,将热点代码编译成机器码,让它可以在应用运行时持续提升 Android 应用的性能。
JIT编译器补充了ART当前的预先(AOT)编译器的功能,有助于提高运行时性能,节省存储空间,以及加快应用及系统更新速度。相较于 AOT编译器,JIT编译器的优势也更为明显,因为它不会在应用自动更新期间或重新编译应用(在无线下载 (OTA) 更新期间)时拖慢系统速度。
尽管JIT和AOT使用相同的编译器,它们所进行的一系列优化也较为相似,但它们生成的代码可能会有所不同。JIT会利用运行时类型信息,可以更高效地进行内联,并可让堆栈替换 (On Stack Replacement) 编译成为可能,而这一切都会使其生成的代码略有不同。
APK安装阶段 系统不再像纯AOT时代那样对整个APK进行完全编译。取而代之的是,系统会进行一些轻量级的处理:
验证DEX文件 优化DEX文件(例如字节码优化) 生成ODEX文件(优化后的DEX) 这个阶段不会生成大量的本地机器码,从而加快了安装速度。
首次运行 应用以解释模式开始运行。 JIT编译器开始工作:
监控代码执行 识别热点代码 将热点代码编译成本地机器码 编译后的代码存储在JIT代码缓存中,供后续快速访问。同时,系统开始收集应用使用的配置文件信息。
持续运行阶段 JIT编译器持续工作,不断优化热点代码。系统继续收集和更新应用的使用配置文件。随着时间推移,更多的代码可能被JIT编译。
设备空闲充电时: 系统触发”编译守护进程”(compilation daemon)。基于收集的配置文件信息,对应用进行部分AOT编译。这个过程被称为”配置文件引导编译”(profile-guided compilation)。编译结果保存为本地机器码,存储在设备上。
后续运行 应用启动时,系统会加载之前AOT编译的代码。对于未编译的部分,继续使用解释执行和JIT编译。JIT编译器继续工作,可能会编译之前未被AOT编译的代码。
长期优化 系统会定期(通常在设备空闲充电时)重新评估和更新AOT编译的代码。这个过程会考虑最新的使用模式,可能会编译新的热点代码,或者放弃编译不再频繁使用的代码。
系统更新时 当系统更新时(例如Android版本升级),之前的AOT编译结果可能会被丢弃。 应用会重新经历上述过程,以适应新的系统环境。
总的来说,这种JIT和AOT的混合策略充分利用了两种方法的优点,在 安装速度、启动时间、运行性能和存储空间使用之间取得了很好的平衡 ,显著提升了Android系统的整体用户体验。
Java的I/O模型 Java 的 I/O 模型 指的是 Java 如何处理输入和输出(Input/Output)操作的方式,尤其是在涉及文件、网络、控制台等数据流时的读写机制。不同的 I/O 模型在性能、阻塞行为、线程使用等方面有显著差异。
Java 的 I/O 模型经历了几个发展阶段:传统的阻塞式 I/O(java.io)、NIO(java.nio)和 NIO.2(java.nio.file)。
阻塞 I/O (Blocking I/O - BIO) Java 传统的 I/O 基于流的概念,将数据视为连续的字节序列或字符序列。
字节流 ,InputStream 和 OutputStream 是所有字节流的抽象基类,用于处理原始字节数据(例如图片、音频、视频文件)。常见的实现类有 FileInputStream, FileOutputStream, BufferedInputStream, BufferedOutputStream, DataInputStream, DataOutputStream, ObjectInputStream, ObjectOutputStream 等。
字符流 ,Reader 和 Writer 是所有字符流的抽象基类,用于处理字符数据(例如文本文件)。它们处理字符编码,能够将字节流转换为字符流。常见的实现类有 FileReader, FileWriter, BufferedReader, BufferedWriter, InputStreamReader, OutputStreamWriter 等。
传统的 I/O 是 阻塞式 (Blocking) 的。当一个线程执行 I/O 操作(如读取文件或网络数据)时,它会 一直等待直到操作完成 ,期间该线程无法执行其他任务。这在并发场景下会导致性能问题,因为每个连接可能需要一个独立的线程来处理。适用于客户端数量较少、连接时间较短的场景,例如简单的文件读写。
NIO (Non-blocking I/O - NIO) NIO 在 Java 1.4 中引入,旨在解决传统 I/O 的阻塞问题,提供更高效、可伸缩的 I/O 操作。
基于 通道 (Channel) ,表示到实体(如文件、网络套接字)的连接,通过通道读写数据。它与传统 I/O 的流不同,通道是双向的,可以同时进行读写操作。
常见实现类有 FileChannel, SocketChannel, ServerSocketChannel, DatagramChannel 等。所有数据都 通过缓冲区读写 。数据从通道读取到缓冲区,或者从缓冲区写入通道。缓冲区提供了对数据的结构化访问,支持在内存中对数据进行高效操作。最常用的是 ByteBuffer,此外还有 CharBuffer, IntBuffer 等。
NIO模型还允许 单个线程管理多个通道 。通过选择器,一个线程可以监控多个通道上的 I/O 事件(如连接就绪、读就绪、写就绪),从而实现非阻塞 I/O。这大大减少了线程创建和切换的开销,提高了服务器的并发处理能力。
NIO 是 非阻塞式 (Non-blocking) 的。当 I/O 操作无法立即完成时,线程不会被阻塞,而是可以去执行其他任务。当 I/O 准备就绪时,选择器会通知线程。相较于传统I/O,其特点有:
数据读写都通过缓冲区。 非阻塞,I/O 操作不阻塞线程。 多路复用,单个线程可以处理多个 I/O 通道,提高并发性能。 复杂性,相对于传统 I/O,NIO 的编程模型更复杂,需要管理缓冲区、通道和选择器。 适用于高并发、大量连接的场景,如网络服务器。
AIO (Asynchronous I/O) AIO,也叫 NIO.2 在 Java 7 中引入,主要增加了异步 I/O (Asynchronous I/O) 功能,以及对文件系统操作的增强 (java.nio.file 包)。
异步通道允许在 I/O 操作完成后,通过回调函数或 Future 对象来处理结果,而不需要显式地等待。例如 AsynchronousFileChannel, AsynchronousSocketChannel, AsynchronousServerSocketChannel。还定义了 CompletionHandler 作为I/O 操作完成时调用的回调接口,可以处理成功或失败的情况。
Path/Files,提供了更现代、功能更丰富的文件系统 API,解决了 java.io.File 类的一些局限性,例如更好的异常处理、符号链接支持、元数据访问等。
AIO 是异步式 (Asynchronous) 的。I/O 操作由操作系统执行,当操作完成时,操作系统会通知应用,应用可以注册回调函数来处理结果。这进一步减少了应用层面的线程管理负担。
适用于需要极致性能和扩展性的大型应用,例如高并发网络服务器、大数据处理。
常见分类标准 阻塞和非阻塞 阻塞IO是指在执行IO操作时,如果没有数据可用或者IO操作还没有完成,那么当前线程会被挂起,直到数据准备好或者IO操作完成。这种方式会导致线程阻塞,无法执行其他任务,适用于需要等待IO操作完成的场景。
非阻塞IO是指在执行IO操作时,如果没有数据可用或者IO操作还没有完成,当前线程不会被阻塞,而是会立即返回一个状态码或者错误码,告诉调用者当前IO操作还没有完成。这种方式不会导致线程阻塞,适用于需要同时处理多个IO操作的场景。
Java 中的 NIO 是非阻塞 IO,当用户发起读写的时候,线程不会阻塞,之后,用户可以通过轮询或者接受通知的方式,获取当前 IO 调度的结果。
缓冲IO和直接IO 非直接IO也称为缓冲IO,是大多数操作系统默认的文件访问方式。数据先被 复制到内核缓冲区 ,然后再 从内核缓冲区复制到用户空间 。可以减少实际磁盘操作次数,利用预读(read-ahead)和延迟写(write-behind)优化性能。但是数据需要在内核和用户空间之间 多次拷贝 ,在某些场景下可能增加延迟。适合小文件或随机访问
直接IO绕过内核缓冲区,直接在用户空间和存储设备之间传输数据。数据直接在用户空间和设备间传输,可以 减少数据拷贝次数 。这种方式,每次IO操作都是实际的设备操作。同时要求IO大小和内存对齐符合设备要求。适合大文件传输或已知访问模式的应用
缓冲是针对标准库的
Linux 标准库定义了很多操作系统的基础服务,比如输入/输出、字符串处理等等。Android 操作系统的标准库是 Bionic ,它可是应用层联系内核的桥梁,我们也可以通过 NDK 访问 Bionic。
使用标准库进行 IO 我们称为缓冲 IO,我们读文件的时候,经常遇到,读完一行才会让输出,在 Android 内部也做了类似的处理。
直接是针对内核的
使用 Binder 跨进程传递数据的时候,需要 将数据从用户空间传递到内核空间 ,非直接 IO 也这样,内核空间会多做一层页缓存,如果做直接 IO,应用程序会直接调用文件系统。
Android 的 Binder 通信 既不是 直接 I/O,也不是 非直接 I/O ,而是一种 进程间通信 机制。它主要依赖 内存映射(mmap) 和 内核缓冲区 来实现高效的数据传输,而不是传统的文件 I/O 方式。Binder 使用 mmap() 在内核和用户空间之间建立 共享内存区域,避免数据在用户态和内核态之间的多次拷贝。发送方(Client)和接收方(Server)通过这块共享内存进行数据交换。Binder 驱动维护一个 内核缓冲区,用于临时存储跨进程传递的数据。数据先写入内核缓冲区,再通过共享内存传递给目标进程。Binder 通过 mmap 实现 一次拷贝(用户空间 → 内核缓冲区 → 目标进程用户空间),提高效率。Binder 不属于传统IO,因为它 不涉及磁盘读写,而是纯内存操作 (进程间通信)。
缓冲和非直接 IO 就像 IO 调度的一级和二级缓存,为什么要做这么多缓存呢?因为操作磁盘本身就是消耗资源的,不加缓存频繁 IO 不仅会耗费资源也会耗时。
Android平台的 IO 在 Android 平台上,IO(输入/输出)操作是应用与外部世界(如文件系统、网络、硬件设备)进行交互的基础。
首先确定一个原则,Android 平台的任何耗时操作,都应该在后台线程中进行,主线程需要尽量保持不出现耗时大于16ms的非UI任务,避免出现卡顿现象。主线程的事件循环如果被卡顿5s以上,还会出现ANR的问题。
以下是 Android 平台上常见的 IO 模型及其特点:
阻塞 IO (Blocking I/O) 这是最简单、最直观的 IO 模型。当一个线程执行 IO 操作时(例如,从文件中读取数据,或者向网络发送数据),该线程会被阻塞,直到 IO 操作完成。编程模型简单,易于理解和实现。
需要注意的是,如果在 Android 的主线程(UI 线程)上执行阻塞 IO 操作,会导致应用无响应(ANR - Application Not Responding)错误,给用户带来糟糕的体验。在高并发场景下,每个连接都需要一个线程,线程切换的开销很大。
所以,仅适用于 非常小的、不频繁的 IO 操作,或者在专门的后台线程中进行。例如应用启动时,需要从 res/raw 或 assets 目录读取一个非常小的、固定的配置字符串,比如一个 API Key 或者一个版本号,并且这个读取操作是在一个 独立的后台线程中 进行的。
非阻塞 IO (Non-blocking I/O) 线程在进行 IO 操作时,如果数据尚未准备好,IO 调用会立即返回一个状态码,而不是阻塞线程。开发者需要通过轮询(polling)来检查 IO 操作是否完成。避免了线程阻塞,提高了线程的利用率。
这种非阻塞的模式,需要不断轮询,增加了编程的复杂性。而且频繁的轮询会消耗大量的 CPU 资源。
在 Android 开发中,直接使用纯非阻塞 IO 的场景相对较少。例如要自定义一个 网络服务器框架的底层 ,网络通信库或服务器框架(例如,一个模拟 Netty 行为的 Android 本地服务器),你可能会利用 Java NIO 的 SocketChannel 在非阻塞模式下读写数据。这种模式的编程难度很高,需要开发者手动管理缓冲区、检查返回状态码,并且处理各种边缘情况。
多路复用 IO (I/O Multiplexing) 这种模型允许单个线程同时监听多个 IO 流(或文件描述符)的事件。当任何一个 IO 流准备好读写时,系统会通知该线程,然后线程可以对相应的 IO 流进行操作。常见的实现包括 select、poll 和 epoll(在 Linux 内核中)。
一个线程可以处理多个连接,避免了大量线程创建和切换的开销。可以避免不必要的阻塞,线程只在有 IO 事件发生时才被唤醒。同样的,编程模型相对复杂,需要对底层系统调用有一定了解。
例如在 Android 应用中,如果需要实现一个轻量级的 本地服务器或者 P2P 连接 ,多路复用 IO 是一种高效的选择。
还有 异步网络请求库的底层 ,许多流行的网络请求库(如 OkHttp)底层都可能利用了多路复用 IO 的思想来管理并发网络连接。
异步 IO (Asynchronous I/O - AIO) 线程发起 IO 操作后,立即返回,无需等待 IO 完成。当 IO 操作真正完成时,系统会通知发起者(通常通过回调函数、事件或 Future/Promise 模式)。
线程无需等待 IO 完成,可以立即执行其他任务,进一步提高了并发性。相对于轮询, 回调函数 的方式可以简化编程逻辑。如果是 RxJava 这种框架,当嵌套的回调过多,可能导致代码难以阅读和维护(Callback Hell)。
使用上,例如 Retrofit + OkHttp + Coroutines 推荐组合。Retrofit 定义接口,OkHttp 执行实际的网络请求,Coroutines (suspend 函数和 Dispatchers.IO) 负责在后台线程执行请求并在请求完成后通知 UI 线程。
还有像大文件和频繁的读写。
suspend fun saveLargeFile ( data : ByteArray , fileName : String ) {
withContext ( Dispatchers . IO ) {
val file = File ( context . filesDir , fileName )
FileOutputStream ( file ). use { fos ->
fos . write ( data )
}
}
}
扩展:Android 阻塞IO 调用流程 一个系统调用的大致工作流程:
用户程序发起请求: 用户程序通过调用库函数(这些库函数内部会封装系统调用)或直接使用 syscall 指令,将系统调用号和参数传递给操作系统。 切换到内核态: CPU从用户态(用户程序运行的权限较低的状态)切换到内核态(操作系统核心运行的权限较高的状态)。这个切换通常通过软中断实现。 内核处理请求: 操作系统内核根据系统调用号找到对应的内核函数,并执行相应的操作。 返回结果: 内核完成操作后,将结果(成功或失败,以及相关数据)返回给用户程序。 切换回用户态: CPU切换回用户态,用户程序继续执行。 绝大部分的非直接IO都会通过系统调用这层桥梁来进行,应用进程不会直接联系内核。
VFS 在 Android 平台上,虚拟文件系统 (Virtual File System, VFS) 是一个至关重要的概念,它为应用程序提供了一个统一的文件访问接口,而无需关心底层存储的实际物理结构和文件系统类型。VFS 位于文件系统之上,抽象了不同文件系统之间的差异,使得应用程序可以以相同的方式处理各种文件和目录。
常常有下列的对象(C语言中的结构体)构成:
超级块 (Superblock): 每个文件系统都有一个超级块,它包含了文件系统的元数据,如文件系统类型、块大小、总块数等。索引节点 (Inode): 每个文件或目录都有一个索引节点,它包含了文件的元数据,如文件大小、权限、创建时间等。文件系统操作 (File System Operations): 每个文件系统都有一组文件系统操作函数,用于执行文件系统的各种操作,如打开文件、读取文件、写入文件等。目录项 (Directory Entry): 目录是一种特殊的文件,它包含了指向其他文件或目录的指针。文件 (File): 文件是存储在磁盘上的数据,可以是文本、图像、音频等。不过,光有这些对象可不行,VFS 还得知道如何操作它们,所以,每个对象中还存在对应的操作对象:
super_operation 对象:内核针对超级块所能调用的方法 inode_operation 对象:内核针对索引结点所能调用的方法 dentry_operation 对象:内核针对目录项所能操作的方法 file_operation 对象:内核针对进程中打开的文件所能操作的方法 大伙最熟悉的应该是文件,这是我们能够在进程中实实在在能够操作的,比如,在文件的 file_operation 中,就有我们熟悉的读、写、拷贝、打开、写入磁盘等方法。
超级块和索引节点存在于内存和磁盘,而目录项和文件只存在于内存。
我的理解是对于磁盘,索引节点已经足够记录文件信息,并不需要目录项再来记录层级关系;而对于内存来说,为了节省内存,只会把需要用到的文件和目录项所用到的索引节点加入内存,文件系统只有被挂载的时候超级块才会被加入到内存中。
VFS中的缓存
结合本文中的第一张图,我们会发现,VFS 有目录项缓存、索引节点缓存和页缓存,目录项和索引节点我们都知道什么意思,那页缓存呢?
页缓存是由 RAM 中的物理页组成的,对应着 ROM 上的物理地址。我们都知道,现在主流 Android 的 RAM 访问速度高达是 8.5 GB/S,而 ROM 的访问速度最高只有 6400 MB/S,所以访问 RAM 的速度要远远快于 ROM,页缓存的目的也在于此。
当发起一个读操作的时候,内核会首先检查需要的数据是否在页缓存,如果在,直接从内存中读取,我们称之为缓存命中;如果不在,那么内核在读取数据的时候,将读到的数据放入页缓存,需要注意的是,页缓存可以存入全部文件内容,也可以仅仅存几页。
文件系统
VFS 定义了文件系统的统一接口,具体的实现了交给了文件系统,超级块里面的数据如何组织、目录和索引结构如何设计、怎么分配和清理数据,这都是设计一个文件系统必须考虑的!
说白了,文件系统就是用来管理磁盘里的持久化的数据的,对于 Android 来说,最常见的就是 ext4 和 f2fs。
Ext2 (Second Extended File System) 是 Linux 内核最初使用的文件系统之一,由法国软件开发者 Rémy Card 设计。它取代了早期的 Ext 和 Minix 文件系统,解决了它们在文件大小、文件名长度等方面的限制。尽管现在更常用的是 Ext3 和 Ext4(它们在 Ext2 的基础上增加了日志功能),但理解 Ext2 的基本结构对于理解现代 Linux 文件系统仍然非常重要。
Ext2 文件系统结构
Ext2 文件系统将磁盘空间划分为逻辑块 (Blocks),然后将这些块进一步组织成 块组 (Block Groups) 。这种设计旨在减少磁盘碎片,并最小化磁头移动,从而提高性能。
每个块组通常包含以下几个部分:
引导块 (Boot Block):位于磁盘的起始位置,包含用于系统引导的信息。Ext2 文件系统本身并不使用这部分空间。
超级块 (Superblock):它是文件系统的核心,包含了文件系统的全局元数据。 重要信息:文件系统的大小、块大小、每个块组的块数量、空闲块和空闲 inode 的数量、文件系统状态(如是否干净卸载)、上次挂载时间、上次写入时间、魔数(标识文件系统类型)等。 冗余备份:为了容错,Ext2 会在每个块组的开始部分(或至少一些块组)存储超级块的副本。如果主超级块损坏,可以使用副本进行恢复。 块组描述符表 (Group Descriptor Table):紧跟在超级块之后,它包含了每个块组的描述信息。 重要信息:每个块组中空闲块位图的位置、inode 位图的位置、inode 表的起始位置以及该块组中空闲块和空闲 inode 的数量。 块位图 (Block Bitmap):一个位图,用于记录该块组中数据块的分配情况。每个位对应一个数据块,如果位是 1,表示该块已被占用;如果是 0,表示空闲。 操作系统利用块位图来查找空闲块,以分配给文件数据。 Inode 位图 (Inode Bitmap):类似于块位图,用于记录该块组中 inode 的分配情况。每个位对应一个 inode,1 表示已分配,0 表示空闲。 Inode 表 (Inode Table):存储该块组中所有 inode 结构体的数组。 Inode (Index Node) 是 Ext2 文件系统的核心概念之一,它代表了文件系统中的一个对象(文件、目录、符号链接等)。 Inode 包含的信息:文件类型(普通文件、目录、符号链接等) 访问权限(读、写、执行权限) 文件所有者和所属组 文件大小 创建时间、修改时间、上次访问时间 硬链接计数 指向数据块的指针:这是 Inode 最重要的部分。Inode 本身不存储文件内容,而是存储指向文件实际数据块的指针。Ext2 的 inode 结构通常包含:直接指针:通常有 12 个,直接指向文件的前 12 个数据块。 一级间接指针:指向一个块,该块中存储了更多的数据块指针。 二级间接指针:指向一个块,该块中存储了一级间接指针的地址。 三级间接指针:指向一个块,该块中存储了二级间接指针的地址。 这种多级索引方式使得 Ext2 能够支持非常大的文件。 数据块 (Data Blocks):存储文件实际内容(数据)的区域。这些块由 inode 指向。 目录在 Ext2 中也是一种特殊的文件,它的数据块中存储的是目录项(文件名到 inode 号的映射)。 Ext2 的 I/O 流程 (以读取文件为例) 当应用程序请求读取一个文件时,Ext2 文件系统通常会经历以下 I/O 流程:
VFS 层解析路径名: 应用程序通过系统调用(如 open())传入文件路径名。 操作系统内核的虚拟文件系统 (VFS) 层接收到请求。 VFS 层会从根目录开始,逐级解析路径名。对于每个目录,VFS 会:查找该目录的 inode(通过其父目录的 inode 指向的数据块中的目录项)。 读取该目录的数据块,查找与路径名中下一个组件匹配的目录项。 从目录项中获取对应文件或子目录的 inode 号。 获取目标文件的 Inode: 一旦 VFS 找到目标文件的 inode 号,它会根据 inode 号计算出该 inode 所在的块组以及在该块组的 Inode 表中的偏移量。 操作系统会从磁盘上读取包含该 inode 的块,并将其加载到内存中。这个 inode 包含了文件的大小、权限和最重要的数据块指针。 确定数据块位置: 假设应用程序请求读取文件内容的某个偏移量。 文件系统会根据文件偏移量和文件系统的块大小,计算出需要读取的逻辑块号。 然后,文件系统会使用 Inode 中存储的指针来查找该逻辑块对应的物理块地址:如果是前 12 个块,直接使用直接指针。 如果是后续的块,需要通过一级、二级或三级间接指针来查找实际的数据块地址。这可能涉及到多次磁盘读取,以获取间接块中的指针。 读取数据块: 文件系统通过计算出的物理块地址,向磁盘控制器发出读取请求。 磁盘控制器将数据从磁盘读取到内核缓冲区。 数据返回给用户: 内核将从磁盘读取的数据从内核缓冲区复制到应用程序指定的内存缓冲区中。 read() 系统调用返回,应用程序可以处理文件数据。虽然大部分的文件系统也都有超级块、索引节点和数据块,但是各个文件系统的实现却大不相同,这就导致了他们的侧重点也不一样。拿 ext4 和 f2fs 来讲,ext4连续读取大文件更强,占用的空间更小;而f2fs随机 IO 更快
说白了,也就是它们对于空闲空间分配和已有的数据管理方式不一致,不同的数据结构和算法导致了不同的结果。
块IO层 Linux 下面有两大基本设备类型:
块设备:能够随机访问固定大小数据片的硬件设备,硬盘和闪存(下面介绍)就是常见的块设备 字符设备:字符设备只能按照字符流的方式被有序访问,比如键盘和串口 这两个设备的区别就是是否能够随机访问。拿属于字符设备的键盘来说,当我们输入 Hello World 的时候,系统肯定不可以先得到得到 eholl wrodl,这样的话,输出就乱套了。而对于闪存来说,常常是看完这个这些数据库组成的图片,又要读间隔很远的数组块的小说内容,所以读取的块在磁盘上肯定不是连续的。
因为内核管理块设备实在太复杂了,所以就出现了管理块设备的子系统,就是上面说的文件系统。
块设备结构
块设备中常用的数据管理单位:
扇区:设备的最小寻址单元 块:文件系统的最小寻址单元,数倍大于扇区 片段:由数百至数千的块组成 因为 Linux 中常常用的硬盘,这里我有点疑问,这里的管理单位是否和下面闪存管理单位一致?
IO过程
如果当前有 IO 操作,内核会建立一个 bio 结构体的基本容器,它是由多个片段组成,每一个片段都是一小块连续的内存缓冲区。
之后,内核会将这些 IO 请求保存在一个 request_queue 的请求队列中。
如果按照 IO 请求产生的顺序发向块设备,性能肯定难以接受,所以内核会按照磁盘地址对进入队列之前提交的 IO 请求做合并与排序的预操作。
磁盘 移动设备中常用的持久化存储是 Nand 闪存,UFS 又是 Nand 闪存中的佼佼者,其特点是速度更快、体积小和更省电。当今 Android 旗舰机基本上标配 UFS 3.1,它们只是一块儿很小的芯片。
闪存是一种非易失性存储器,即使掉电了,数据也不会丢。闪存的存储单元从小到大有:
Cell(单元):是闪存存储的最小单位,根据存储的数量可以分为SLC(1bit/Cell)、MLC(2bit/Cell)、TLC(3bit/Cell)和QLC(4bit/Cell) Page(页):由大量的 Cell 构成,每个 Page 的大小通常是 16 kb,它是闪存能够读取的和写入的最小单位 Block(块):每个块由数百至数千的 Page 组成 Plane(面):Plane 由数百至数千的 Black 组成 Die(逻辑单元):每个 Die 由一个至多个 Plane,是闪存中可以执行命令或者回报状态的最小单元 到Cell这一层,再往下就是MOS管了,通过电压控制电子是否进入存储单元。
Android 平台上公认的系统层主要的动画类型有以下三种:属性动画,补间动画,帧动画
系统原生 补间动画 (View Animation / Tween Animation) Android 平台最早的动画系统之一,只能对 View对象 进行动画,也称为视图动画。
且只能对 View 的基本变换属性进行动画,包括:
透明度 (Alpha) 旋转 (Rotate) 缩放 (Scale) 平移 (Translate) 需要注意的是,补间动画 改变的是绘制位置,而非View的实际位置。 实际上并没有改变 View 的实际位置、大小等属性,它只是改变了 View 的绘制方式。这意味着即使 View 被动画移动了,它的点击事件响应区域仍在原来的位置。
对于简单的变换动画,补间动画的使用相对简单,可以通过 XML 或代码定义。适用于简单的视图变换,例如按钮点击后的放大缩小效果、图片渐入渐出效果等。对性能要求不高的简单 UI 动画。
使用 首先确认好动画的效果,是平移或是透明度等。在res文件等anim文件夹内,建立一个 translate_anim.xml
<!-- 平移动画 -->
<translate xmlns:android= "http://schemas.android.com/apk/res/android"
android:duration= "2000"
android:fromXDelta= "0%"
android:toXDelta= "300%"
android:interpolator= "@android:anim/linear_interpolator" />
在代码中读取这个anim对象,再调用view的 startAnimation 方法。
binding . apply {
tvViewAnimation . setOnClickListener {
val anim =
AnimationUtils . loadAnimation ( this @ViewAnimationActivity , R . anim . translate_anim )
tvViewAnimation . startAnimation ( anim )
}
}
我们就可以看到这个 textView 会平移到其三倍宽度的地方, XDelta 属性即为这个 View 自身宽度的尺度。动画完结view的显示会回到原来的位置,如果想要停在目标位置,可以设置 anim.fillAfter = true 。
在运动过程中,点击View原来的位置,可以看到动画重新开始了,可以说明View的实际位置没有改变。
原理 Android 系统会不断调用 View 的 onDraw() 方法,在onDraw()方法中,动画会根据当前时间计算出View应该处于的变换状态,然后应用这些变换(通过Canvas的translate()、scale()、rotate()、alpha()等方法),最后绘制View的内容。不断的重绘就形成了动画效果。
而View的实际位置是在 Layout 布局阶段确定的,补间动画并没有改变View的上下左右边界,所以触摸事件的生效区域还在最初 布局 阶段确定的原位置。
属性动画 (Property Animation) 属性动画系统是 Android 3.0 (API level 11) 引入的,它允许你对 任何对象的任何属性 进行动画。这意味着你不仅可以动画视图的属性(如位置、大小、透明度、旋转),还可以动画自定义对象的属性,甚至那些不直接绘制到屏幕上的属性。
属性动画的核心是改变属性的值。你可以指定动画的起始值和结束值,系统会根据时间插值器 (TimeInterpolator) 计算中间值,并通过一个估值器 (TypeEvaluator) 将计算出的值应用到目标对象的属性上。
属性动画不局限于 View,可以对任何 Java 对象进行动画,只要该对象有一个 setter 方法来设置要动画的属性。
有丰富的 API 来控制动画的各个方面,包括:
时长 (Duration): 动画的总时间。时间插值 (Time Interpolation): 控制动画的变化速度曲线(例如,加速、减速、先慢后快、回弹等)。重复计数和行为 (Repeat Count and Behavior): 指定动画是否重复播放,以及重复的次数和方式(例如,正向播放、反向播放)。动画集合 (Animator Sets): 将多个动画组合成一个集合,可以同时播放、按顺序播放或延迟播放。帧刷新延迟 (Frame Refresh Delay): 指定动画帧的刷新频率。场景 对视图的各种属性(如位置、大小、透明度、旋转、颜色、背景等)进行复杂、精细控制的动画。 ** 对非视图对象的属性进行动画。 实现自定义的动画效果。 Material Design 风格的动画,例如圆形揭露动画、共享元素过渡动画等。 使用 属性动画的使用相对复杂,需要创建一个 Animator 对象,并指定要动画的属性和目标值。然后,将这个 Animator 对象应用到目标对象上。
针对View的属性动画
tvPropAnimation . apply {
setOnClickListener {
val animator =
ObjectAnimator . ofFloat ( this , "translationX" , 0f , this . width * 3f )
animator . duration = 2000
animator . interpolator = LinearInterpolator ()
animator . start ()
}
}
这里我们可以看到,我们创建了一个属性动画,这个动画会让这个 textView 从自身的X轴方向平移到自身宽度三倍的位置。和上面补间动画一样的效果。
针对变量的属性动画
使用ValueAnimator来创建属性动画,ValueAnimator会根据时间插值器计算出中间值,并通过一个估值器将计算出的值应用到目标对象的属性上。
tvVarPropAnimation . apply {
setOnClickListener {
val valueAnimator = ValueAnimator . ofInt ( this . width , this . width * 3 )
valueAnimator . duration = 2000
valueAnimator . interpolator = LinearInterpolator ()
valueAnimator . addUpdateListener { animation ->
this . width = animation . animatedValue as Int
}
valueAnimator . start ()
}
}
这个动画会把TextView的宽度在2s内扩大3倍。
帧动画 (Frame Animation / Drawable Animation) 帧动画是指通过按顺序显示一系列 Drawable 资源来创建的动画,类似于电影胶片的原理。
由于需要加载多张图片,如果图片数量过多或分辨率过高,可能会占用较多的内存资源。通常在 XML 文件中定义,指定每个帧的 Drawable 资源和显示时长。
播放序列帧图片,例如加载动画、游戏中的角色跳跃动画、爆炸效果等。精确控制每一帧显示内容的场景。
使用 在 drawable 文件夹内,建立一个 frame_anim.xml 的文件。
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android= "http://schemas.android.com/apk/res/android"
android:oneshot= "true" >
<item
android:drawable= "@drawable/fragnance_open_00"
android:duration= "50" />
<item
android:drawable= "@drawable/fragnance_open_01"
android:duration= "50" />
<item
android:drawable= "@drawable/fragnance_open_02"
android:duration= "50" />
<item
android:drawable= "@drawable/fragnance_open_03"
android:duration= "50" />
</animation-list>
在代码中读取这个anim对象,一般会使用一个ImageView来承载,触发后,调用 ImageView 的 setBackgroundResource 方法。
binding . apply {
ivFrameAnimation . setOnClickListener {
ivFrameAnimation . setBackgroundResource ( R . drawable . frame_anim )
( ivFrameAnimation . background as AnimationDrawable ). start ()
}
}
原理 帧动画的原理是通过不断地切换 Drawable 资源来实现的。系统会根据每个 Drawable 的显示时长,依次显示每个 Drawable,形成动画效果。
小结 在现代 Android 开发中,属性动画 是推荐的首选动画方式,因为它提供了无与伦比的灵活性和强大的控制能力。视图动画和帧动画在某些特定场景下仍有其用武之地,但通常属性动画能够实现它们的功能,并且效果更好。
此外,对于 Jetpack Compose 这种声明式 UI 框架,也有其独特的动画 API,如 animateFloatAsState、AnimatedVisibility 等,它们在底层也是基于属性动画的原理实现的,提供了更简洁的动画开发体验。
三方扩展 Lottie动画 Lottie 是一个由 Airbnb 开发的开源动画库,它的核心理念是将 AE 中创建的动画导出为轻量级的 JSON 文件,然后 Lottie 库可以在各种平台上(包括 Android, iOS, Web, React Native, Windows, HarmonyOS 等)原生渲染这些动画。
Lottie 动画的优势
文件体积小巧,Lottie 动画文件通常比 GIF 或 APNG 等传统位图动画格式小得多。这意味着更快的加载速度和更少的带宽消耗,尤其是在移动端体验上优势明显。 高保真还原,Lottie 能够几乎完美地还原 AE 中设计的动画效果,包括路径动画、形状变换、颜色渐变、蒙版、预合成等,解决了传统方式下设计师和开发者之间协作的难题(即“所见即所得”)。 矢量性,大部分 Lottie 动画是基于矢量图形的,这意味着它们在不同分辨率的屏幕上都能保持清晰锐利,不会出现像素化的问题。 跨平台兼容性,同一个 JSON 文件可以在多个平台上使用,大大提高了开发效率和动画的一致性。 易于集成,Lottie 提供了简洁易用的 API,开发者可以轻松地将 Lottie 动画集成到他们的应用中。 原理 JSON 文件解析:
在应用程序运行时,Lottie 库会读取这个 JSON 文件。 它会解析 JSON 数据,将其映射为 Lottie 内部的数据结构,代表动画中的各种元素和它们的动画属性。 原生渲染
Lottie 库根据解析后的数据,利用各个平台原生的绘图 API 进行渲染。 在 Android 上,Lottie 主要通过 Canvas 进行 2D 渲染,并结合原生的 ValueAnimator 等动画组件来实现动画效果。 它会遍历动画的每一帧,计算当前帧每个图层和元素的变换(平移、缩放、旋转、透明度、路径变化等),然后使用 Canvas 的绘图方法(如 drawPath, drawCircle, drawRect 等)将其绘制出来。 使用 添加依赖:
在 build.gradle (Module: app) 中添加 Lottie 库的依赖。
dependencies {
implementation 'com.airbnb.android:lottie:6.0.0' // 替换为最新版本
}
放置 JSON 文件
将导出的 .json 动画文件放置在 src/main/assets 目录下。
在布局中添加 LottieAnimationView:
<com.airbnb.lottie.LottieAnimationView
android:id= "@+id/animation_view"
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
app:lottie_fileName= "your_animation.json"
app:lottie_autoPlay= "true"
app:lottie_loop= "true" />
app:lottie_fileName:指定位于 assets 目录下的 JSON 文件名。 app:lottie_autoPlay:是否自动播放。 app:lottie_loop:是否循环播放。
在代码中控制 你也可以通过代码来加载和控制动画。
LottieAnimationView animationView = findViewById ( R . id . animation_view );
// 从 assets 加载
animationView . setAnimation ( "your_animation.json" );
// 从 raw 资源加载
// animationView.setAnimation(R.raw.your_animation);
// 从 URL 加载
// animationView.setAnimationFromUrl("https://example.com/your_animation.json");
animationView . playAnimation (); // 播放
animationView . pauseAnimation (); // 暂停
animationView . cancelAnimation (); // 取消
animationView . loop ( true ); // 设置循环
animationView . setSpeed ( 1.5f ); // 设置播放速度
animationView . setProgress ( 0.5f ); // 设置播放进度
// ... 更多控制 API
Lottie 是现代移动和 Web 应用中实现高质量动画的强大工具,极大地简化了设计师和开发者之间的协作流程,并提供了出色的性能和灵活性。
PAG动画 PAG动画 是由腾讯开发的一种高效的动画渲染解决方案,全称为 Portable Animated Graphics 。它主要用于在移动端、Web 和桌面应用中实现高性能的矢量动画播放,广泛应用于社交、广告、游戏和短视频等领域。
PAG动画的核心特点:
高性能渲染,采用 硬件加速 (如OpenGL/Vulkan/Metal),确保动画流畅播放,即使在低端设备上也能保持高帧率。支持 多线程解码 ,减少主线程压力,提升响应速度。 全平台支持,覆盖 iOS、Android、Web、Windows、macOS 等平台,提供统一的动画体验。支持 Flutter、React Native 等跨平台框架。 矢量动画兼容性,支持从 Adobe After Effects (AE) 直接导出动画(通过PAG插件),保留图层、遮罩、特效等属性。兼容 Lottie 的部分特性,但性能更优,文件更小。 动态编辑能力,运行时可以修改文本、图片、颜色等元素(如替换广告文案或贴图),无需重新导出动画。支持 视频模板 ,方便替换视频片段。 文件体积小,采用二进制格式(.pag 文件),比JSON格式的Lottie(.json)更紧凑,加载更快。 使用 PAG动画的使用相对简单,主要分为以下几个步骤:
导包
tencent-libpag = { module = "com.tencent.tav:libpag" , version.ref = "tencentLibpag" }
放置源文件
将设计师所提供的 .pag动画 文件导入到项目中,一般放置于assets文件夹。
xml布局添加承载的pagView
<com.tencent.libpag.PAGView
android:id= "@+id/pagView"
android:layout_width= "match_parent"
android:layout_height= "match_parent" />
代码中加载动画
// 调用
playPAGView ( pagView , "anim_file.pag" )
//开始播放动效
fun playPAGView ( pagView : PAGView ?, fileName : String ) {
LogUtils . i ( TAG , "playPAGView: fileName:$fileName" )
if ( pagView != null ) {
if ( pagView . isPlaying ) {
pagView . stop ()
}
pagView . visibility = View . VISIBLE
val pf = PAGFile . Load ( appContext . assets , fileName )
pagView . composition = pf
pagView . setRepeatCount (- 1 )
pagView . play ()
}
}
PAG动画凭借其高性能和灵活性,逐渐成为替代Lottie的主流方案,特别适合需要复杂动效和实时编辑的场景。
原理 整体架构如下:
┌───────────────────────┐
│ Android App │
├───────────────────────┤
│ PAG SDK (Java/Kotlin)│
├───────────┬───────────┤
│ 文件解析层 │ 渲染引擎层 │
├───────────┴───────────┤
│ OpenGL ES/Skia │
└───────────────────────┘
(1) 文件解析与数据解码
.pag 文件格式:采用二进制编码(而非 Lottie 的 JSON),体积更小,解析更快。解析时仅解码当前帧所需的数据,减少内存占用。采用了多线程解码,在后台线程解析动画数据,避免阻塞 UI 线程。
(2) 渲染引擎(Skia + OpenGL ES)
PAG 的渲染核心依赖: Skia:处理矢量路径(Path)、颜色渐变、遮罩等 2D 图形操作。 OpenGL ES:实现硬件加速,通过 GPU 高效合成图层(类似游戏渲染)。
渲染流程:
图层树构建:解析 .pag 文件后生成图层树(类似 AE 的图层结构)。 帧数据计算:根据当前时间戳计算各图层的变换(位移、旋转、透明度等)。
GPU 绘制:
矢量路径 → 由 Skia 转换为 GPU 可识别的网格(Mesh)。 位图/图片 → 使用 OpenGL 纹理(Texture)渲染。 特效(模糊、渐变)→ 通过 GLSL 着色器实现。
(3) 动画控制与动态修改
时间轴驱动:基于 ValueAnimator 或 Choreographer 同步帧率(通常 60FPS)。
动态替换:
文本替换:运行时修改 PAGTextLayer 的内容。 图片替换:通过 PAGImageLayer 绑定 Bitmap 对象。 播放控制:支持播放、暂停、循环、进度跳转等。
PAG和Lottie对比 简单来说,Lottie动画的文件简单小巧,文件主要基于JSON打包,跨平台特性比较好。
而PAG动画可实现的效果更多,同时文件采用了二进制压缩,二进制格式通常具有更高的压缩率和解析速度,但可读性较差,不易直接修改。
选择 Lottie 还是 PAG 取决于你的具体需求:
如果你主要制作轻量级、矢量化、UI 相关的动画,且对 After Effects 复杂特效没有特别要求,那么 Lottie 通常是更好的选择,它更简洁、轻便。 如果你需要实现After Effects 中更复杂、更炫酷的动画效果,可能包含视频、图片、大量文本动态替换,或者需要制作短视频模板等,那么 PAG 能提供更高的还原度和更强大的运行时编辑能力。 kanzi && Unity 3D 这两个动画比较详细的介绍,此前已经有一篇记录。
【Android集成Unity的两种方式】
纲要转载自掘金老哥的一篇文章,实机测试及扩展而来。 【掘金原文】
检测是手机还是平板 Android中没有提供特定的方法来判断设备是手机还是平板,只能通过别的方式来间接判断,比如通过判断屏幕尺寸。
infoText . text = checkIsTablet ()
private fun checkIsTablet (): String {
val metrics = resources . displayMetrics
val widthInches = metrics . widthPixels / metrics . xdpi
val heightInches = metrics . heightPixels / metrics . ydpi
val diagonalInches = sqrt ( widthInches . pow ( 2.0f ) + heightInches . pow ( 2.0f ))
return if ( diagonalInches >= 7.0 ) {
"手机还是平板:平板"
} else {
"手机还是平板:手机"
}
}
判断是否为折叠屏 其实在折叠屏没出现的时候,判断手机或者是平板使用上述方法还是够用的,但是在折叠屏面前就显得信心不足了,折叠屏一展开,那就是一个长着平板脸的手机,为了识别折叠屏,Android10出来了一个新的感应器类型TYPE_HINGE_ANGLE,可以通过是否存在这种感应器来识别折叠屏。
private fun checkIsFoldScreen (): String {
val sensorManager = getSystemService ( Context . SENSOR_SERVICE ) as SensorManager
val hingeAngleSensor = sensorManager . getDefaultSensor ( Sensor . TYPE_HINGE_ANGLE )
return if ( hingeAngleSensor == null ) {
"是否折叠屏:否"
} else {
"是否折叠屏: 是"
}
}
如果说想要具体拿到折叠屏的状态,比如是全展开还是半展开,或者收起状态,就要使用Jetpack WindowManager这个库了,分别可以通过以下api拿到不同的状态。
屏幕密度与密度比例 这两个值相信基本每个项目都会用到,屏幕密度一般用来判断屏幕适配,加载不同的图片资源,密度比例一般用来单位换算,这俩值都可以通过DisplayMetrics来获得
infoText . text = checkScreenDpiAndDensity ()
private fun checkScreenDpiAndDensity (): String {
val displayMetric = resources . displayMetrics
val dpi = displayMetric . densityDpi
val density = displayMetric . density
return "屏幕密度:${dpi} 密度比例:${density}"
}
屏幕像素(宽高) private fun checkScreenPixel (): String {
val displayMetrics = DisplayMetrics ()
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . R ) {
display ?. getRealMetrics ( displayMetrics )
} else {
windowManager . defaultDisplay . getRealMetrics ( displayMetrics );
}
val wPixel = displayMetrics . widthPixels
val hPixel = displayMetrics . heightPixels
return "像素(宽):${wPixel} 像素(高):${hPixel}"
}
物理尺寸 物理尺寸在安卓上单位是英寸,它表示一个屏幕对角线的长度,至于如何计算对角线,就要用到上学时候用到的勾股定理,x,y分别是屏幕的宽高,注意的是由于单位是英寸,所以也要把上面计算出来的像素转换成英寸,具体代码如下
private fun checkPhysicalSize (): String {
val displayMetrics = DisplayMetrics ()
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . R ) {
display ?. getRealMetrics ( displayMetrics )
} else {
windowManager . defaultDisplay . getRealMetrics ( displayMetrics );
}
val widthInches = displayMetrics . widthPixels / displayMetrics . xdpi
val heightInches = displayMetrics . heightPixels / displayMetrics . ydpi
val diagonalInches = sqrt (
widthInches . pow ( 2.0f ) + heightInches . pow ( 2.0f )
)
return "物理尺寸 $diagonalInches"
}
刷新率 刷新率一般就是指Android屏幕上每秒更新画面的频率,单位是赫兹,正常来讲,普通设备的刷新率都为60赫兹,获取刷新率的代码如下
private fun checkRefreshRate (): String {
val mDisplay = if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . R ) {
display
} else {
windowManager . defaultDisplay
}
return "刷新率 ${mDisplay?.refreshRate}"
}
广色域 有的设备支持广色域,有的设备仅仅支持标准色域,广色域的意思是屏幕可以显示比标准色域(sRGB)更加丰富的颜色范围,判断一个设备是否支持广色域的方式如下
private fun checkColorGamut (): String {
val config : Configuration = resources . configuration
if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . O ) {
val isWideColorGamut : Boolean = config . isScreenWideColorGamut
val support = if ( isWideColorGamut ) "支持" else "不支持"
return "是否支持广色域模式:${support}"
}
return "不支持广色域"
}
获取内存(Runtime) 一般来讲应用想获取内存信息,用的最多的就是通过Runtime来获取,可以通过它获取应用最大可用内存,当前分配的内存,当前空闲内存,已使用内存
infoText . text = checkMemoryRuntime ()
private fun checkMemoryRuntime (): String {
val runtime = Runtime . getRuntime ()
val maxMemory = runtime . maxMemory () // 应用最大可用内存
val totalMemory = runtime . totalMemory () // 当前分配的内存
val freeMemory = runtime . freeMemory () // 当前空闲内存
val usedMemory = totalMemory - freeMemory // 已使用内存
return "最大可用内存:${maxMemory} 当前分配的内存:${totalMemory} 当前空闲内存:${freeMemory} 已使用内存${usedMemory}"
}
获取内存(MemoryInfo) 还有一种方式就是通过获取MemoryInfo来拿到内存信息,比如总内存,当前空闲内存以及判断内存是否过低.
infoText . text = checkMemoryMemoInfo ()
private fun checkMemoryMemoInfo (): String {
val activityManager = getSystemService ( Context . ACTIVITY_SERVICE ) as ActivityManager
val memoryInfo = ActivityManager . MemoryInfo ()
activityManager . getMemoryInfo ( memoryInfo )
val totalMem = memoryInfo . totalMem
val availMem = memoryInfo . availMem
val lowMemory = memoryInfo . lowMemory
return "总内存:${totalMem} 当前空闲内存:${availMem} 内存是否过低${lowMemory}"
}
磁盘空间(外部存储与内部存储) 可以通过StatFs来获取外部存储以及内存存储容量
//外部存储
infoText . text = checkExternalStorageInfo ()
private fun checkExternalStorageInfo (): String {
if ( Environment . getExternalStorageState () == Environment . MEDIA_MOUNTED ) {
val path = Environment . getExternalStorageDirectory () // 外部存储根目录
val stat = StatFs ( path . path )
val blockSize = stat . blockSizeLong
val totalBlocks = stat . blockCountLong
val availableBlocks = stat . availableBlocksLong
val totalSize = blockSize * totalBlocks
val availableSize = blockSize * availableBlocks
val usedSize = totalSize - availableSize
return "外部存储总容量:${totalSize} 外部存储可用容量:${availableSize} 外部存储已用容量:${usedSize}"
}
return ""
}
//内部存储
infoText . text = checkInternalStorageInfo ()
private fun checkInternalStorageInfo (): String {
val path = Environment . getDataDirectory () // 内部存储根目录
val stat = StatFs ( path . path )
val blockSize = stat . blockSizeLong // 每个block的大小
val totalBlocks = stat . blockCountLong // 总block数
val availableBlocks = stat . availableBlocksLong // 可用block数
val totalSize = blockSize * totalBlocks // 总容量
val availableSize = blockSize * availableBlocks // 可用容量
val usedSize = totalSize - availableSize // 已用容量
return "内部存储总容量:${totalSize} 内部存储可用容量:${availableSize} 内部存储已用容量:${usedSize}"
}
CPU内核数量 获取CPU的内核数量很简单,Runtime类中有现成的方法
infoText . text = checkCPUcoreNumber ()
private fun checkCPUcoreNumber () =
"cpu核心数 : ${Runtime.getRuntime().availableProcessors()}"
CPU架构 infoText . text = checkCPUArchitecture ()
private fun checkCPUArchitecture () =
"cpu架构:${Build.SUPPORTED_ABIS[0]}"
CPU最大频率 fun getCpuFreq (): String ? {
try {
val reader =
BufferedReader ( FileReader ( "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq" ))
val freq = reader . readLine ()
reader . close ()
// 将频率从 kHz 转换为 MHz
return "${(freq.toLong() / 1000)} MHz"
} catch ( e : IOException ) {
e . printStackTrace ()
}
return null
}
CPU硬件信息 infoText . text = checkCPUHardware ()
private fun checkCPUHardware () =
"硬件信息:${Build.HARDWARE}"
检测设备是否root 同样的没有任何api可以直接去判断设备是否有root权限,我们只能从以下几个方式去判断: 判断检查是否存在相关root文件
var fileRooted = false
val paths = arrayOf (
"/system/app/Superuser.apk" ,
"/sbin/su" ,
"/system/bin/su" ,
"/system/xbin/su" ,
"/data/local/xbin/su" ,
"/data/local/bin/su" ,
"/system/sd/xbin/su" ,
"/system/bin/failsafe/su" ,
"/data/local/su" ,
"/su/bin/su"
)
for ( path in paths ) {
if ( File ( path ). exists ()) {
fileRooted = true
}
}
检查是否存在su命令
var suCmdExest = false
var process : Process ? = null
try {
process = Runtime . getRuntime (). exec ( arrayOf ( "which" , "su" ))
val reader = BufferedReader ( InputStreamReader ( process . inputStream ))
suCmdExest = reader . readLine () != null
} catch ( e : Exception ) {
suCmdExest = false
} finally {
process ?. destroy ()
}
检查 Build.TAGS 里面是否存在 test-keys
```kotlinvar testKeys = false val buildTags = Build.TAGS testKeys = buildTags != null && buildTags.contains(“test-keys”)
**执行su命令**
```kotlin
var suCmdExecute = false
var suprocess: Process? = null
try {
suprocess = Runtime.getRuntime().exec("su")
val out = suprocess.outputStream
out.write("exit\n".toByteArray())
out.flush()
out.close()
suCmdExecute = suprocess.waitFor() == 0
} catch (e: java.lang.Exception) {
suCmdExecute = false
} finally {
suprocess?.destroy()
}
Magisk 文件是否存在
var giskFile = false
val magiskPaths = arrayOf (
"/sbin/.magisk" ,
"/sbin/magisk" ,
"/cache/.disable_magisk" ,
"/cache/magisk.log" ,
"/data/adb/magisk" ,
"/data/adb/modules" ,
"/data/magisk" ,
"/data/magisk.img"
)
for ( path in magiskPaths ) {
if ( File ( path ). exists ()) {
giskFile = true
}
}
所有条件综合判断:
val gotRoot = fileRooted || suCmdExest || testKeys || suCmdExecute || giskFile
网络情况 这个也是在应用当中经常会用到的一个属性,判断设备是连接的是wifi,还是连接的是2,3,4,5G网络,首先通过获取 NetworkCapabilities 来判断是否连接的是wifi还是移动网络。
private fun checkNetworkType (): String {
var net = ""
val cManager = getSystemService ( Context . CONNECTIVITY_SERVICE ) as ConnectivityManager
val capabilities : NetworkCapabilities ? =
cManager . getNetworkCapabilities ( cManager . activeNetwork )
capabilities ?. let { cb ->
if ( capabilities . hasTransport ( NetworkCapabilities . TRANSPORT_WIFI )) {
net = "WIFI"
} else if ( capabilities . hasTransport ( NetworkCapabilities . TRANSPORT_CELLULAR )) {
net = getMobileNetworkType ( cManager )
}
}
return "网络情况:${net}"
}
当判断出非wifi网络的时候,再通过getMobileNetworkType函数来得出具体的网络类型
private fun getMobileNetworkType ( cManager : ConnectivityManager ): String {
val networkInfo = cManager . getNetworkInfo ( ConnectivityManager . TYPE_MOBILE )
if ( networkInfo != null ) {
val networkType = networkInfo . subtype
return when ( networkType ) {
TelephonyManager . NETWORK_TYPE_GPRS , TelephonyManager . NETWORK_TYPE_EDGE -> "2G"
TelephonyManager . NETWORK_TYPE_UMTS , TelephonyManager . NETWORK_TYPE_HSPA -> "3G"
TelephonyManager . NETWORK_TYPE_LTE -> "4G"
TelephonyManager . NETWORK_TYPE_NR -> "5G"
else -> "UNKNOWN"
}
}
return "UNKNOWN"
}
服务导向架构(Service-Oriented Architecture,SOA)是一种软件设计方法论,它强调将应用程序构建为一系列松散耦合(loosely coupled) 、可独立部署(independently deployable) 且通过 定义良好的接口(well-defined APIs) 进行通信的 服务(services) 。
SOA 架构简介 在 SOA 中,每个服务都执行一个特定的业务功能,并且可以被其他服务或应用程序调用和重用。它不是一种技术,而是一种架构风格,可以通过各种技术(如 SOAP、REST、消息队列等)实现。
核心概念: 服务(Service) :SOA 的基本构建单元。服务是自包含的、模块化的、并且执行特定业务功能的软件组件。它们对外提供接口,隐藏内部实现细节。松散耦合(Loose Coupling) :服务之间相互独立,彼此之间的依赖性很小。这意味着一个服务的改变不会直接影响到其他服务,从而提高了系统的灵活性和可维护性。接口(Interface) :服务通过明确定义的接口进行通信。这些接口是公共的、标准化的,允许不同技术栈的服务之间进行交互。可重用性(Reusability) :由于服务是独立的且功能明确,它们可以在不同的应用程序和业务流程中被重复使用,减少了重复开发的工作。互操作性(Interoperability) :服务可以跨越不同的平台、编程语言和操作系统进行通信,因为它们依赖于标准化的通信协议和接口。SOA 的优势: 模块化和可重用性 :便于开发、测试、部署和维护新功能,并与其他系统集成。可扩展性 :可以独立扩展单个服务,以满足不断增长的需求,而无需扩展整个系统。灵活性和敏捷性 :更容易适应业务需求的变化,快速响应市场。易于维护 :问题定位和修复更简单,降低了维护成本。异构系统集成 :能够整合不同技术和平台构建的系统。SOA 在车载领域的应用 传统的汽车电子电气(E/E)架构是高度分散的,由数百个独立的电子控制单元(ECU)组成,每个 ECU 负责一个或几个特定功能(如发动机控制、车窗控制、信息娱乐等),它们之间通过 CAN、LIN 等低速总线进行通信。这种单片式(monolithic) 或信号导向(signal-oriented) 的架构随着车辆功能(如自动驾驶、智能座舱、车联网)的爆炸式增长,暴露出以下局限性:
复杂性高 :ECU 数量过多,布线复杂,系统集成困难。扩展性差 :添加新功能需要修改多个 ECU 的软件,甚至需要重新设计硬件。维护困难 :诊断和更新难度大,OTA(Over-The-Air)升级受限。数据孤岛 :不同 ECU 之间的数据共享不便。为了应对这些挑战,汽车行业正在向软件定义汽车(Software-Defined Vehicle, SDV) 的方向发展,而 SOA 正是实现 SDV 的关键使能技术。
1. 架构演进:从分布式到域控/区域控制,再到中央计算 域控制器(Domain Controller) :将传统上分散的 ECU 整合到几个大的域控制器中(如智能座舱域、自动驾驶域、车身控制域等)。区域控制器(Zonal Controller) :进一步将功能聚合,将车辆划分为几个物理区域,每个区域有一个区域控制器处理该区域内的传感器、执行器和通信。中央计算平台(Central Computing Platform) :最终目标是将大部分计算能力集中到一个或少数几个高性能计算单元中,形成车载“大脑”,负责处理跨域的复杂功能。在这些新的 E/E 架构中,高速以太网(Ethernet)逐渐取代传统的总线作为主要通信骨干网,为 SOA 提供必要的带宽和低延迟。相比于分布式架构,会有一个处理器作为 中央网关 ,来综合管理其他各个服务的订阅,数据分发。
2. SOA 如何应用于车载系统 在车载 SOA 中,车辆的各项功能被抽象为可独立调用和组合的“服务”。例如:
传感器数据服务 :提供来自摄像头、雷达、激光雷达等传感器的原始或处理后的数据。执行器控制服务 :例如“制动服务”、“转向服务”、“车窗控制服务”等。诊断服务 :提供车辆健康状态、故障码等信息。信息娱乐服务 :如导航、媒体播放、语音识别等。互联服务 :与外部云端、移动设备通信的服务。SOME/IP协议 SOME/IP,全称 Scalable Service-Oriented Middleware over IP (基于 IP 的可伸缩面向服务中间件),它是一种专为汽车域设计的高效通信协议。
起源与目的: 随着现代汽车电子架构的复杂性日益增加,传统的车载网络协议(如 CAN、LIN、FlexRay)在 带宽、灵活性和可伸缩性 方面逐渐遇到瓶颈。为了满足信息娱乐、高级驾驶辅助系统 (ADAS) 等对带宽和实时性要求更高的新功能,汽车行业开始引入以太网。SOME/IP 应运而生,旨在为基于 IP 的汽车网络提供一种面向服务的通信机制。
AUTOSAR 整合: SOME/IP 是 AUTOSAR (AUTomotive Open System ARchitecture) 标准的关键组成部分。AUTOSAR 致力于统一汽车软件架构,SOME/IP 作为其通信层的一部分,确保了不同供应商和制造商之间的软件组件兼容性和互操作性。
服务发现(Service Discovery) 服务发现的通信机制是通过SOME/IP-SD协议实现的,主要是为了实现在车载以太网中告知客户端当前服务实例的可用性及访问方式,可通过Find Service 和Offer Service来实现。
SOME/IP 服务发现流程可以分为以下三大基本步骤:
Client通过发送Find Service的报文去寻找车载网络中可用的服务实例; Server接收到Client的Find Server后通过UDP发送Offer Service响应; Client通过发送Subcribe Event Group去订阅相关Event; Server检查是否满足Client是否满足订阅条件,如果满足回复ACK,如果不满足,则回复NACK; Client成功订阅相关事件后,Server会按照事件本身属性来实现对已订阅该事件的Client的发布;
远程进程调用(RPC) 远程进程调用主要可分为四种通信模式:
1. Request/Response通信模式 Request-Response模型作为一种最为常见的通信方式,其主要任务就是客户端发送请求信息,服务端接收到请求,进行相关处理之后进行相应的响应。
2. Fire&Forget通信模式 该通信模型的主要任务就是客户端向服务端发送请求,服务端无需进行任何响应,有点类似诊断服务中的抑制正响应。
3. Notification Event通信模式 该通信模式主要描述了发布 /订阅消息内容,主要任务就是为了实现客户端向服务端订阅相关的事件组,当服务端的事件组发生或者值发生变化时,就需要向已订阅该事件组的客户端发布更新的内容。
4. 远程进程控制(Field) 访问进程通信机制主要是为了实现针对对应用程序的数据获取与更改,主要任务就是实现客户端通过Getter获取Server的值,通过Setter设置Server的值。
Field就可理解为一个Service的基本属性,可包含Getter,Setter,Notifier三种方式。其中Getter就是读取Field中某个值的方法,Setter就是一种改变Field值的方法,而Notifier则是一种当Field中的值发生变化的触发事件,发生变化时就通知Client。
错误处理机制 AUTOSAR为了更为高效的定位到通讯过程中的问题所在,制定了一套检查SOME/IP协议格式内容的错误处理机制。比如版本信息检查,服务ID等,其他故障信息可以在Payload中进行详细定义。目前SOME/IP支持以下两种错误处理机制,这两种uowu处理机制可以根据配置进行选择。
主要是校验协议首部结构,对Message Type和Return code进行赋值,通知对端。
车载通信中的CAN(Controller Area Network,控制器局域网)协议 是汽车电子系统中应用最广泛的串行通信协议之一,一种专门为恶劣环境设计的串行通信协议。它的老家是德国,由博世公司在1986年正式发布,后来被写进了ISO11898-1标准,定义了OSI模型的数据链路层和物理层。
其设计初衷是为了解决汽车内部日益复杂的电子控制单元(ECU)之间的通信需求,替代传统的点对点布线方式,以减少线束重量、降低成本并提高通信可靠性。
现在,它已广泛应用于汽车、工业自动化、医疗设备等领域,作为一种高效、可靠的通信方式。
一种典型架构
CAN 总线是一种消息导向(message-based) 的通信协议,而不是地址导向(address-based)。这意味着总线上的所有设备(称为节点 或 ECU - Electronic Control Unit )都能“听到”所有传输的消息,每个消息都包含一个标识符(ID) ,而不是一个目标地址。节点会根据这个 ID 来决定是否接收和处理该消息。
核心设计目标 CAN的诞生初衷是为了解决汽车内部电子控制单元(ECU)之间通信的麻烦。以前,ECU之间需要一大堆线缆连接,布线复杂得像蜘蛛网。CAN总线的出现让这一切变得简单:只需一对差分信号线,就能让所有ECU愉快地聊天。
CAN总线有以下几个特点:
多主站(Multi-master) :网络中没有中心控制器,所有节点(ECU)都可以在总线空闲时尝试发送消息,适合分布式电子架构 。广播(Broadcast) :所有发送的消息都会被网络中的所有节点接收。优先级仲裁(Arbitration) :当多个节点同时尝试发送消息时,CAN 总线通过一个基于消息 ID 的仲裁机制来解决冲突。ID 值越低,优先级越高,拥有更高优先级的消息会赢得总线,而低优先级的消息会暂停发送,等待总线空闲后重试。高可靠性和容错性(High Reliability and Fault Tolerance) :CAN 总线内置了强大的错误检测和错误处理机制,例如循环冗余校验(CRC)、位填充(Bit Stuffing)和应答(ACK),确保数据传输的完整性。即使出现错误,系统也能进行错误隔离和恢复。低成本 :简化线束设计(两根信号线即可连接所有节点),降低整车布线复杂度与成本。差分信号传输(Differential Signalling) :CAN 总线通过两根双绞线(CAN_H 和 CAN_L)传输差分信号,这有助于抵御电磁干扰(EMI),使其在嘈杂的环境中也能可靠工作。终端电阻(Termination Resistors) :CAN 总线两端需要各放置一个 120 欧姆的终端电阻,以消除信号反射,保证信号完整性。实时性 :通过优先级仲裁机制确保关键信息(如制动、安全气囊控制)优先传输。CAN协议的物理层与数据链路层 CAN协议分为物理层 和数据链路层 (逻辑链路控制子层LLC和介质访问控制子层MAC),其中物理层和MAC子层由CAN标准(ISO 11898)定义,LLC子层由用户自定义。
物理层 信号传输介质 :通常采用双绞线(CAN_H和CAN_L),通过差分信号传输(平衡传输),抗干扰能力强。信号电平 :显性电平(Dominant) :CAN_H电压高于CAN_L(典型值:CAN_H=3.5V,CAN_L=1.5V),逻辑“0”。隐性电平(Recessive) :CAN_H与CAN_L电压相等(约2.5V),逻辑“1”。优先级规则 :显性电平可覆盖隐性电平(类似“线与”逻辑),用于仲裁。通信速率 :支持多种速率(如125kbps、250kbps、500kbps、1Mbps等),速率越高,通信距离越短(例如1Mbps时最大距离约40米,500kbps时约100米)。数据链路层 一个标准CAN消息帧包含以下几个关键部分:
帧起始(SOF):一个显性位,标志着消息的开始,相当于敲门声,提醒大家有新消息来了。 标识符(ID):11位长度,决定了消息的优先级。ID越小,优先级越高。这就像在群聊里,谁的ID靠前,谁的消息就先被处理。 远程传输请求(RTR):通常是显性位,但当某个节点想请求数据时,会变成隐性位。 标识符扩展(IDE):显性位表示这是标准CAN帧,不是扩展帧。 数据长度码(DLC):4位,告诉接收方这次消息带了多少字节数据。 数据字段:实际要传的内容,最多8字节。比如发动机转速、油门开度啥的。 循环冗余校验(CRC):16位校验码,用来检测传输错误,堪称数据的保镖。 应答位(ACK):接收方如果正确收到消息,会把这个隐性位覆盖为显性位,相当于说:收到,靠谱! 帧结束(EOF):7位隐性位,用于标记消息结束,同时检测是否有位填充错误。 帧间间隔(IFS):一段空闲时间,让CAN控制器有空把收到的消息塞进缓冲区。 后来,CAN升级了,推出了扩展CAN,把标识符从11位扩展到29位,消息ID数量暴增到2的29次方,满足更复杂的应用场景。扩展帧在11位ID后加了个替代远程请求(SRR)位,IDE位变成隐性,表示后面还有18位ID。其他部分和标准帧差不多。
消息类型 CAN总线支持四种消息类型:
数据帧(Data Frame) :用于传输实际数据。这是最常见的帧类型。RTR和IDE都是显性位。远程帧(Remote Frame) :用于请求其他节点发送特定 ID 的数据帧。不带数据,RTR为隐性,用于请求某个节点发送数据,相当于喊一嗓子:兄弟,发个数据包过来!错误帧(Error Frame) :当节点检测到错误时发送,通知网络其他节点有错误发生,然后出错的节点会重发消息。过载帧(Overload Frame) :用于通知总线上当前负载过高,请求短暂延迟。当某个节点忙不过来,处理不过收到的帧时,会发个过载帧,争取点喘息时间。CAN协议的关键机制 多主仲裁机制 简单来说,就是解决多个节点同时想发消息时的优先级问题。
所有节点通过总线竞争发送数据时,ID数值越小的帧优先级越高(例如ID=0x100的帧优先于ID=0x200的帧)。 仲裁过程:节点同时发送数据时,逐位比较电平。若某节点发送隐性电平(1),而总线上出现显性电平(0),则该节点主动退出竞争,转为接收状态。 错误检测与处理 错误类型 :位错误(发送与接收电平不一致)、填充错误(连续5个相同电平未插入相反电平)、CRC错误、格式错误、ACK错误等。错误处理 :节点检测到错误后发送错误帧,通知所有节点丢弃当前帧;错误计数器记录错误次数,超过阈值(如127次)的节点进入“被动错误状态”(仅能发送被动错误帧),严重错误时进入“总线关闭状态”(暂时退出通信)。数据重传机制 若发送方未收到ACK确认(可能因接收方故障或总线冲突),会自动重传数据帧(最多重传多次,具体次数由实现决定)。 CAN协议的扩展版本 为满足更高数据传输需求,CAN协议衍生出多个扩展版本:
CAN FD(Flexible Data-Rate) :核心改进 :数据段速率可提升至5Mbps(仲裁段仍保持较低速率),数据场长度扩展至64字节(标准CAN仅8字节)。应用场景 :适用于需要传输大量数据的应用(如高级驾驶辅助系统ADAS、摄像头数据传输)。CAN XL :更高带宽 :支持更高速率(如10Mbps以上)和更大 payload(最高2048字节),面向未来智能汽车的高带宽需求。CAN协议在车载网络中的应用 CAN总线在汽车电子和工业控制领域简直无处不在。
比如新能源汽车的BMS(电池管理系统),通过CAN总线实时监控电池状态,SOC、SOH、温度、电压等数据飞速在ECU间传递。
工业领域,像是Modbus或DeviceNet这样的协议,底层也靠CAN总线撑腰。
相比传统的点对点连接,CAN总线的多主架构让系统扩展性强到爆,甚至随手加个节点都随随便便。
想象一下现代汽车,发动机控制、ABS、仪表盘、空调系统……每个模块都是一个ECU,它们通过CAN总线组成一个高效的通信网络。
就像一群人在群聊里实时交流,消息井然有序,互不干扰。比如你踩油门,发动机ECU立马收到指令,调整喷油量,整个过程非常顺畅。
典型应用场景 :动力系统:发动机控制、变速箱控制(需高实时性)。 底盘系统:ABS防抱死、ESP电子稳定程序。 车身电子:车窗控制、灯光控制、空调系统。 安全系统:安全气囊、碰撞检测。 与其他车载网络的协同 :CAN通常作为整车通信的骨干网络,与LIN(低速、低成本,用于车窗等简单设备)、FlexRay(高实时性、高带宽,用于底盘控制)、以太网(大数据传输,如自动驾驶传感器数据)协同工作,形成分层网络架构。 CAN协议凭借其高可靠性、实时性和低成本优势,成为汽车电子通信的基石。随着汽车智能化与电动化的发展,CAN FD和CAN XL等扩展版本进一步提升了带宽与数据处理能力,而CAN与其他车载网络(如以太网)的协同也将成为未来车载通信架构的核心趋势。
如何分析 CAN 总线通信协议 分析 CAN 总线通信通常涉及到捕获总线上的原始数据,然后对这些数据进行解码和解释,以理解各个消息的含义。对于 Android 开发者来说,这可能涉及到通过蓝牙 OBD-II 适配器与车辆 CAN 总线交互,或者在嵌入式系统中直接与 CAN 控制器通信。
1. 硬件工具准备: CAN 总线分析仪/接口卡 :这是最核心的工具。它们可以将 CAN 总线上的物理信号转换为计算机可以理解的数字信号。常见的有:USB 转 CAN 接口 :如 PCAN-USB, Kvaser USBcan, IXXAT USB-to-CAN 等。这些通常用于连接到 PC 进行分析。OBD-II 转 CAN 适配器 :对于汽车应用,许多 OBD-II 适配器(例如 ELM327 兼容的蓝牙或 Wi-Fi 适配器)可以让你通过手机或电脑访问车辆的 CAN 总线数据,但功能可能受限。连接线缆 :连接 CAN 分析仪和 CAN 总线(通常是 OBD-II 端口或直接连接到 CAN_H/CAN_L)。带有合适软件的计算机 :用于捕获、显示和解析 CAN 数据。2. 软件工具准备: CAN 总线监测软件 :大多数 CAN 接口卡都附带自己的软件,例如 PCAN-View, Kvaser CanKing 等。这些软件可以实时显示总线上的消息,包括 ID、数据、时间戳等。CAN 数据库文件(DBC 文件) :这是分析 CAN 数据的关键。DBC 文件是一种标准格式,它包含了 CAN 消息的定义,例如:每个 CAN ID 对应的消息名称。 每个消息中各个信号(Sensor Readings, Status, Commands)的起始位、长度、字节顺序(大小端)、缩放因子、偏移量和单位。 信号的有效值范围。 枚举值(例如,某个字节代表“开”或“关”)。 有了 DBC 文件,原始的十六进制数据就能被解析成有意义的物理值,例如发动机转速、车速、油门位置等。 数据分析工具 :如果需要更深入的分析,可以使用 MATLAB/Simulink, Python (使用 python-can 库), Wireshark (结合 CAN 插件) 等工具进行数据处理、可视化和模式识别。3. 分析步骤: 连接硬件 :将 CAN 分析仪连接到目标 CAN 总线。对于汽车,通常是连接到 OBD-II 端口。确保总线已上电。配置软件 :选择正确的 CAN 接口 :在软件中选择你连接的 CAN 接口设备。设置波特率(Baud Rate) :CAN 总线上的所有设备必须以相同的波特率通信(例如 500 kbps, 250 kbps)。你需要根据目标系统设置正确的波特率。许多分析仪支持自动检测。设置过滤(Filtering) :如果你只对特定 ID 的消息感兴趣,可以设置 ID 过滤器,以减少显示的数据量。捕获数据 :开始捕获 CAN 总线上的数据。你会看到一系列十六进制的 CAN 帧。观察总线活动 :注意哪些 ID 的消息频繁出现,哪些消息在特定操作下(例如踩油门、刹车)会发生变化。触发特定事件 :在分析车辆 CAN 总线时,执行特定的操作(如打开车窗、踩刹车、启动引擎等),然后观察哪些 CAN 消息随之变化。这有助于你找到与这些操作相关的消息 ID 和数据。解码数据(使用 DBC 文件) :导入 DBC 文件 :将对应的 DBC 文件导入你的 CAN 分析软件。实时解析 :软件会根据 DBC 文件的定义,将原始的十六进制数据自动解析成有意义的信号值(例如,从 0x1A0 ID 的消息中解析出“发动机转速”为 2500 RPM)。手动解码(如果无 DBC) :如果没有 DBC 文件,你需要进行逆向工程。这通常是一个耗时且需要经验的过程:隔离消息 :通过筛选和观察,找到你感兴趣的特定功能(如车速、门状态)对应的 CAN ID。分析数据变化 :对特定 ID 的数据字段进行多次捕获,每次都改变对应的物理量(例如,让车速从 0 加速到 100 km/h,观察数据字段的十六进制值如何变化)。推断数据格式 :根据数据变化的规律,推断出数据的字节顺序、长度、缩放因子、偏移量。这可能需要了解一些常见的编码方式,如 little-endian/big-endian,有符号/无符号整数,浮点数等。创建自己的 DBC 文件 :当你识别出一些信号后,可以创建自己的 DBC 文件来记录这些发现,方便后续分析和开发。数据分析与应用 :数据可视化 :将解析出的信号数据绘制成图表,更直观地观察其变化趋势。故障诊断 :通过分析 CAN 消息,可以识别通信故障、传感器故障或 ECU 内部问题。功能开发 :对于 Android 开发者,理解 CAN 消息后,你可以在应用程序中读取车辆数据(例如仪表盘信息、故障码),或发送特定指令来控制车辆功能(如果允许且安全)。CAN协议的优缺点 优点 :高可靠性:抗干扰能力强,错误检测与恢复机制完善。 实时性强:优先级仲裁确保关键信息优先传输。 成本低:简化线束设计,降低整车成本。 成熟生态:广泛支持的工具链和芯片(如NXP、Infineon的CAN控制器)。 缺点 :带宽有限:标准CAN最大速率1Mbps,数据场仅8字节,难以满足高清摄像头等大数据传输需求(需CAN FD或以太网补充)。 无内置加密:需额外机制(如SecOC)保障信息安全(针对新能源车的V2X通信需求)。 下一篇将介绍基于中央服务的SOA架构,和CAN总线的分布式架构做对比。
很早之前就了解到,Java方法传递参数是值传递,不是引用传递。
在 C++ 中,我们可以把方法的参数设置为外部变量的引用,就可以直接通过这个引用操作外部变量。例如C++的函数参数,如果在合适的时机,按引用传递,可以省去变量复制的步骤,优化性能。
#include <iostream>
using namespace std ;
void GetSquare ( int & number )
{
number *= number ;
}
int main ()
{
cout << "Enter a number you wish to square: " ;
int number = 0 ;
cin >> number ;
GetSquare ( number );
cout << "Square is: " << number << endl ;
return 0 ;
}
Java是没有这样的机制的。
一、Java 是值传递,不是引用传递 首先明确一点:
Java 中所有的参数传递都是值传递(pass by value),没有引用传递(pass by reference)。
这句话还有两个扩展结论:
当你传递一个基本类型(如 int, boolean 等)给方法时,传递的是它的值的副本。 当你传递一个对象(如 String, Activity 等)给方法时,传递的是该对象的引用的副本 ,而不是对象本身的副本。 举个例子:
void modifyObject ( MyObject obj ) {
obj . setValue ( 100 ); // 修改的是原对象的内容
obj = new MyObject (); // 修改的是局部变量 obj 的引用,不影响外部
}
MyObject myObj = new MyObject ();
modifyObject ( myObj );
// myObj 指向的对象被修改了,但 myObj 本身还是原来的引用
在这个例子中:
obj 是 myObj 引用的一个副本 ,它们指向同一个对象。所以通过 obj.setValue(100) 可以修改原对象的内容。 但是 obj = new MyObject() 只是让局部变量 obj 指向了一个新对象,不会影响外部的 myObj 。 二、 使用Activity 作为参数 如果是按照值的副本传递,那么Activity对象被当作参数,传递给外部方法并引用,在Activity销毁时是不会产生泄露现象的,正式因为传递的是引用的副本,所以这个引用关系仍然存在。
在 Android 中,当把一个 Activity 作为参数传递给某个方法时,似乎没有复制一个新的 Activity 对象,而是直接操作了原来的 Activity。
这 正是 Java 值传递的表现 。
具体解释: 假设你有如下代码:
startSomeProcess ( MainActivity . this );
这里的 MainActivity.this 是当前 Activity 的引用(即指向 Activity 对象的一个指针)。当你把这个引用作为参数传递给方法时:
void startSomeProcess ( Activity activity ) {
activity . setTitle ( "New Title" ); // 修改的是原 Activity 的标题
activity = new Activity (); // 这里只是修改了局部变量 activity 的指向
}
activity 是 MainActivity.this 引用的一个副本(即引用的值被复制了一份)。所以 activity.setTitle("New Title") 修改的是原来的 Activity 对象。 但是 activity = new Activity() 只是让方法内部的局部变量 activity 指向了一个新的 Activity 对象,不会影响外部的 MainActivity.this 。 这完全符合 Java 的值传递机制。但是下面几点需要注意:
1. Activity 是一个重量级对象,通常不应该作为方法参数频繁传递 Activity 本身包含大量状态信息、视图层次结构、生命周期管理等。将 Activity 作为参数传递,尤其是跨组件传递(如从 Fragment 传递到工具类、Service 等),是一种不好的实践,可能导致内存泄漏或逻辑混乱。 2. Activity 持有 Context,而 Context 是与生命周期强相关的 如果你在一个长生命周期对象 (如单例、静态变量、Service 等)中持有 Activity 的引用,可能会导致 Activity 无法被回收,从而引发内存泄漏。 这也是为什么 Android Lint 会对 static 字段持有 Activity 发出警告(StaticFieldLeak)。 正确做法建议 尽量避免直接传递 Activity 对象,而是通过接口、回调或者 Context(非 Activity 类型)来解耦。
使用 Application Context 替代 Activity Context,比如加载资源、启动 Service 等操作可以使用 getApplicationContext(),避免持有 Activity。
必要时,使用弱引用(WeakReference)来持有 Activity。如果确实需要在某个长生命周期对象中引用 Activity,可以使用 WeakReference<Activity>,这样即使 Activity 被销毁,也不会阻止垃圾回收。
示例:
private WeakReference < Activity > activityRef ;
public void setActivity ( Activity activity ) {
this . activityRef = new WeakReference <>( activity );
}
public void doSomething () {
Activity activity = activityRef . get ();
if ( activity != null && ! activity . isFinishing ()) {
activity . setTitle ( "Safe Title Change" );
}
}
AccessibilityService 是 Android 系统提供的一种特殊类型的服务,它允许应用程序监听系统和应用中的各种事件,并与用户界面(UI)进行交互。
它最初设计目的是为了帮助有视觉、听觉或运动障碍的用户更有效地使用 Android 设备。例如,它可以朗读屏幕上的内容、响应特定的手势或提供自定义的导航。
但由于其强大的能力,它也被广泛用于实现自动化任务 、屏幕内容监控 和手势模拟 等高级功能。
核心能力和工作原理 继承自 AccessibilityService ,实现的服务中使用最多的有以下核心功能:
1. 监听事件 它可以监听系统和应用发出的各种可访问性事件(AccessibilityEvent),这些事件包括:
窗口状态变化: 例如,新窗口打开、关闭或聚焦变化。视图内容变化: 例如,文本框中的文字被修改、列表中的项目被添加。焦点变化: 当用户或程序将焦点移动到不同的 UI 元素时。通知栏变化: 接收和处理通知栏的发布、更新和移除事件。2. 访问屏幕内容 服务可以获取到屏幕上当前活动窗口的 视图层次结构 (AccessibilityNodeInfo)。通过这个节点信息,服务可以获取任何可见 TextView、EditText 或其他可访问元素上的文本内容。还能通过资源 ID(例如 com.example.app:id/button_ok)定位特定的 UI 元素。
3. 模拟用户操作 这是 AccessibilityService 最强大的功能之一,它允许服务代替用户执行操作,实现自动化:
点击和长按: 模拟点击任何可点击的 UI 元素。输入文本: 填充 EditText 字段。滚动: 向上、向下、向左或向右滚动可滚动的视图。执行全局操作: 例如,返回(BACK)、主页(HOME)、打开最近任务列表或显示通知栏。模拟手势: 在屏幕的任意坐标上模拟复杂的触摸手势,如滑动(Swipe)。开发实践 以一个自动跳广告的Demo为例,展示如何使用 AccessibilityService 实现自动化任务。
创建服务类 首先定义服务类 AutoSkipAdsService ,继承自 AccessibilityService :
class AutoSkipAdsService : AccessibilityService ()
这时候会自动要求实现以下方法:
override fun onAccessibilityEvent ( event : AccessibilityEvent ) {
// 处理事件,例如查找广告并点击跳过按钮
}
override fun onInterrupt () {
// 服务被中断时回调,例如用户关闭了服务
}
一般来说,还需要在 onCreate() 中设置为前台服务,以提示用户服务正在运行。
override fun onCreate () {
super . onCreate ()
// 创建前台通知,提示用户服务正在运行
val notification = NotificationCompat . Builder ( this , CHANNEL_ID )
. setContentTitle ( getString ( R . string . accessibility_service_title ))
. setContentText ( getString ( R . string . accessibility_service_description ))
. setSmallIcon ( R . drawable . ic_accessibility )
. build ()
// 启动前台服务,显示通知
startForeground ( NOTIFICATION_ID , notification )
}
配置 Manifest 声明 在应用的 AndroidManifest.xml 文件中声明 AccessibilityService:
<service
android:description= "@string/description_in_manifest"
android:exported= "true"
android:foregroundServiceType= "mediaPlayback"
android:label= "自动跳过广告"
android:name= ".service.AutoSkipAdsService"
android:permission= "android.permission.BIND_ACCESSIBILITY_SERVICE"
tools:ignore= "ForegroundServicePermission" >
<intent-filter>
<action android:name= "android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name= "android.accessibilityservice"
android:resource= "@xml/accessibility_config" />
</service>
其中最重要的配置就是 accessibility_config.xml ,它定义了服务的行为和关注的事件类型。关键的配置项:
属性 描述 accessibilityEventTypes服务的关注事件类型(例如:typeAll、typeViewClicked)。 accessibilityFeedbackType服务提供的反馈类型(例如:feedbackGeneric 用于自动化)。 canRetrieveWindowContent设置为 true 才能访问窗口内容(读取屏幕信息)。packageNames服务仅关注的应用包名列表。如果不设置,则监听所有应用。 canRequestTouchExploration是否请求触摸探索模式。
我的配置如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android= "http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes= "typeAllMask"
android:accessibilityFeedbackType= "feedbackGeneric"
android:canPerformGestures= "true"
android:canRetrieveWindowContent= "true"
android:description= "@string/description_in_xml"
android:notificationTimeout= "100" />
实现逻辑 既然要跳广告,就需要在页面内容变化时,扫描页面上的广告元素。如果发现广告元素,就模拟点击 跳过 按钮。
在 AccessibilityService 整个作用域中,我们都可以获取到当前活动窗口的根节点 rootInActiveWindow ,它是一个 AccessibilityNodeInfo 对象,代表了当前活动窗口的视图层次结构。这个结构里包含了所有可见的 UI 元素,我们可以通过遍历这个结构,来查找广告元素和跳过按钮。
构建一个扩展方法 scanAndClickByText ,用于扫描页面上的文字元素,并点击指定的元素。在 onAccessibilityEvent() 回调方法中,我们可以调用这个扫描方法,来查找并点击广告跳过按钮。
/**
* 扫描文字,点击扫描到的第index个,默认第一个
*/
fun AccessibilityService . scanAndClickByText ( scanText : String , index : Int = 0 ) = try {
infoLog ( "scanText:$scanText" )
rootInActiveWindow ?. findAccessibilityNodeInfosByText ( scanText )
?. get ( index ) ?. apply {
infoLog ( this . text . toString ())
val rect = Rect ()
this . getBoundsInScreen ( rect )
val x = rect . centerX ()
val y = rect . centerY ()
infoLog ( "x:$x, y: $y" )
performClickByCoordinate ( x . toFloat (), y . toFloat ())
}
} catch ( e : Exception ) {
e . message ?. let { errorLog ( it ) }
}
有时候软件提供商会规避自己的文字被辅助服务扫描到,这时候也可以根据控件id来识别元素。先找到广告元素的id,然后根据id来点击跳过按钮。
/**
* 扫描控件id,点击扫描到的第index个,默认第一个
*/
fun AccessibilityService . scanAndClickById ( viewId : String , index : Int = 0 ) = try {
infoLog ( "scanViewId:$viewId" )
rootInActiveWindow ?. findAccessibilityNodeInfosByViewId ( viewId )
?. get ( index ) ?. apply {
infoLog ( this . text . toString ())
val rect = Rect ()
this . getBoundsInScreen ( rect )
val x = rect . centerX ()
val y = rect . centerY ()
infoLog ( "x:$x, y: $y" )
performClickByCoordinate ( x . toFloat (), y . toFloat ())
}
} catch ( e : Exception ) {
e . message ?. let { errorLog ( it ) }
}
performClickByCoordinate 这个方法又是怎么实现点击的呢?这里要用到 GestureDescription 类。
GestureDescription 是 Android 7.0 (API 24) 及以上版本中,AccessibilityService 用来创建和执行复杂触摸手势的核心类。它取代了之前通过 sendMotionEvent 模拟点击的旧方法,提供了更强大、更灵活的途径来自动化手势操作。
不管是滑动还是点击,都可以用 GestureDescription 来实现。
/**
* 创建滑动手势
* 使用:第二第三参数均可为空
* dispatchGesture(@NonNull GestureDescription gesture,
* @Nullable GestureResultCallback callback,
* @Nullable Handler handler)
*
*/
fun AccessibilityService . startSwipeGesture (
startX : Float ,
startY : Float ,
endX : Float ,
endY : Float ,
duration : Long = 500L ,
callback : GestureResultCallback ? = null ,
handler : Handler ? = null
) {
val path = Path ()
path . moveTo ( startX , startY )
path . lineTo ( endX , endY )
val builder = GestureDescription . Builder ()
// 立即开始
val startTime = 0L
// 滑动持续时间(单位:毫秒)
val duration = duration
val stroke = GestureDescription . StrokeDescription ( path , startTime , duration )
builder . addStroke ( stroke )
// 分发滑动手势
dispatchGesture ( builder . build (), callback , handler )
}
要模拟点击的话,将 duration 持续时间这个参数设置比较短即可。
用户授权 在项目代码中按照普通服务的启动方式,不管是 startService() 还是 bindService() 都是无法启动辅助服务的。
由于 AccessibilityService 拥有极高的权限,他可以做的事情和用户手动操作的权限是相同的。用户必须在 设置 - 辅助功能 中找到定义的服务祝福,并手动确认启用你的服务。在代码中,可以引导用户跳转到相应的设置页面进行授权。
其他典型应用场景 Android辅助服务主要用于以下场景:
辅助工具: 屏幕阅读器、盲人导航应用、放大镜等。自动化和效率工具: 自动跳过开屏广告。 模拟用户操作完成重复的签到、点赞等任务。 在特定条件下自动执行点击操作。 家长控制与安全监控: 监控孩子使用的应用、限制特定操作。跨应用功能: 实现全局快捷操作,例如截屏或启动特定功能。开发注意事项(重要!)
由于其高权限特性, Google 对 AccessibilityService 的使用有严格的政策要求:
目的透明: 你的应用必须有一个清晰且可访问的核心功能,直接需要 AccessibilityService 的权限才能工作。例如,如果你的应用是一个自动化工具,这是合理的。明确告知: 必须在应用内明显的位置(如 $\text{Google Play}$ 描述、首次使用提示)明确告知 用户你的应用使用此服务的原因,以及它将访问哪些数据。不得滥用: 严禁用于窃取用户隐私信息(如密码、银行卡信息)或在用户不知情的情况下进行欺诈性点击。总结来说,AccessibilityService 是 Android 开发者实现跨应用自动化和高级交互功能的强大工具。
使用时,务必遵守法律法规,确保用户的知情权和数据安全。
前言 这篇笔记用来收集在日常开发中所用到的安卓adb shell命令,参照了一些大佬的再加上我自己平时用到的整理在了一块儿,感谢无私共享的大佬们。 将会持续更新,欢迎收藏~
车载补充 采集tcp报文 基本语法: tcpdump [ -AbdDefhHIJKlLnNOpqStuUvxX# ] [ -B buffer_size ] [ -c count ]
[ -C file_size ] [ -E algo:secret ] [ -F file ] [ -G seconds ]
[ -i interface ] [ -j tstamptype ] [ -M secret ] [ -P in|out|inout ]
[ -r file ] [ -s snaplen ] [ -T type ] [ -w file ] [ -W filecount ]
[ -y datalinktype ] [ -z command ] [ -Z user ] [ expression ]
常用选项 -i:指定要监听的网络接口,如 eth0、wlan0 等。若不指定,tcpdump 会默认选择系统中的第一个可用网络接口。例如,tcpdump -i eth0表示在 eth0 接口上进行抓包。 -c:指定要捕获的数据包数量。当达到指定数量后,tcpdump 会自动停止抓包。例如,tcpdump -c 10表示只捕获 10 个数据包。 -s:设置每个数据包的抓取长度,默认一般为 68 字节或 96 字节,对于较大的数据包可能无法完整抓取。可以使用 - s 0 指定抓取完整的数据包。例如,tcpdump -s 1500表示抓取每个数据包的前 1500 字节。 -w:将捕获的数据包保存到指定的文件中,以便后续进行离线分析。例如,tcpdump -w packet.pcap会把捕获的数据包保存到 packet.pcap 文件中,该文件可以使用 Wireshark 等工具打开分析。 -r:从指定的文件中读取数据包并进行分析,而不是从网络接口实时捕获。例如,tcpdump -r packet.pcap可以查看之前保存的 packet.pcap 文件中的数据包。 过滤表达式 按协议过滤:可以指定要捕获的特定协议的数据包,如 tcp、udp、icmp 等。例如,tcpdump tcp只捕获 TCP 协议的数据包,tcpdump udp or icmp则捕获 UDP 或 ICMP 协议的数据包。 按主机过滤:可以指定源主机、目的主机或两者都指定。例如,tcpdump host 192.168.1.100捕获进出 192.168.1.100 主机的所有数据包,tcpdump src 192.168.1.100只捕获源地址为 192.168.1.100 的数据包,tcpdump dst 192.168.1.200只捕获目的地址为 192.168.1.200 的数据包。 按端口过滤:可以指定源端口、目的端口或两者都指定。例如,tcpdump port 80捕获进出端口 80 的所有数据包,tcpdump src port 22只捕获源端口为 22 的数据包,tcpdump dst port 53只捕获目的端口为 53 的数据包。 组合过滤:可以使用逻辑运算符 &&(与)、||(或)、!(非)将多个过滤条件组合起来。例如,tcpdump 'tcp port 80 && host 192.168.1.100'捕获来自或发往 192.168.1.100 主机且端口为 80 的 TCP 数据包。 示例 捕获并打印所有经过 eth0 接口的数据包: tcpdump -i eth0* 捕获 100 个经过 eth0 接口的 HTTP 数据包(端口 80): tcpdump -i eth0 -c 100 port 80* 捕获进出主机 192.168.1.100 的所有 ICMP 数据包,并保存到文件 icmp.pcap 中: tcpdump -i eth0 -w icmp.pcap host 192.168.1.100 and icmp tcpdump 命令功能强大,但需要一定的网络知识和经验才能更好地使用,在实际使用中可能需要根据具体需求结合不同的选项和表达式进行灵活运用。
替换系统apk 车机预制的应用一般都在/system/priv-app/ 目录下,用户安装的应用一般都在/data/app/ 目录下。 替换系统应用的步骤:
adb root
adb remount
adb push <apk_file> /system/priv-app/<app_name>/base.apk
adb shell chmod 777 /system/priv-app/<app_name>/base.apk
adb reboot
一、基本用法 命令语法 adb 命令的基本语法如下:
adb [-d|-e|-s <serialNumber>] <command>
如果只有一个设备/模拟器连接时,可以省略掉[-d|-e|-s <serialNumber>]这一部分,直接使用 adb <command>。 为命令指定目标设备。如果有多个设备/模拟器连接,则需要为命令指定目标设备。
-d
指定当前唯一通过 USB 连接的 Android 设备为命令目标
-e
指定当前唯一运行的模拟器为命令目标
-s <serialNumber>
指定相应 serialNumber 号的设备/模拟器为命令目标
在多个设备/模拟器连接的情况下较常用的是 -s <serialNumber> 参数,serialNumber 可以通过 adb devices 命令获取。如:
$ adb devices
List of devices attached
cf264b8f device
emulator-5554 device
10.129.164.6:5555 device
输出里的 cf264b8f、emulator-5554 和 10.129.164.6:5555 即为 serialNumber。
比如这时想指定 cf264b8f 这个设备来运行 adb 命令获取屏幕分辨率:
adb -s cf264b8f shell wm size
又如想给 10.129.164.6:5555 这个设备安装应用(这种形式的 serialNumber 格式为<IP>:<Port>,一般为无线连接的设备或 Genymotion 等第三方 Android 模拟器):
adb -s 10.129.164.6:5555 install test.apk
遇到多设备/模拟器的情况均使用这几个参数为命令指定目标设备,下文中为简化描述,不再重复。
启动/停止 启动 adb server 命令:
adb start-server
(一般无需手动执行此命令,在运行 adb 命令时若发现 adb server 没有启动会自动调起。)
停止 adb server 命令:
adb kill-server
查看 adb 版本 命令:
adb version
示例输出:
Android Debug Bridge version 1.0.36
Revision 8f855a3d9b35-android
以 root 权限运行 adbd
adb 的运行原理是 PC 端的 adb server 与手机端的守护进程 adbd 建立连接,然后 PC 端的 adb client 通过 adb server 转发命令,adbd 接收命令后解析运行。
所以如果 adbd 以普通权限执行,有些需要 root 权限才能执行的命令无法直接用 adb xxx 执行。这时可以 adb shell 然后 su 后执行命令,也可以让 adbd 以 root 权限执行,这个就能随意执行高权限命令了。
命令:
正常输出:
现在再运行 adb shell,看看命令行提示符是不是变成 # 了?
有些手机 root 后也无法通过 adb root 命令让 adbd 以 root 权限执行,比如三星的部分机型,会提示 adbd cannot run as root in production builds,此时可以先安装 adbd Insecure,然后 adb root 试试。
相应地,如果要恢复 adbd 为非 root 权限的话,可以使用
指定 adb server 的网络端口:
adb -P <port> start-server
默认端口为 5037。
二、设备连接管理 查询已连接设备/模拟器
adb devices
输出示例:
List of devices attached
cf264b8f device
emulator-5554 device
10.129.164.6:5555 device
输出格式为[serialNumber] [state],serialNumber 即我们常说的 SN,state 有如下几种:
offline —— 表示设备未连接成功或无响应。 device —— 设备已连接。注意这个状态并不能标识 Android 系统已经完全启动和可操作,在设备启动过程中设备实例就可连接到 adb,但启动完毕后系统才处于可操作状态。 no device —— 没有设备/模拟器连接。 以上输出显示当前已经连接了三台设备/模拟器,cf264b8f、 emulator-5554 和 10.129.164.6:5555 分别是它们的 SN。从 emulator-5554 这个名字可以看出它是一个 Android 模拟器,而 10.129.164.6:5555 这种形为<IP>:<Port> 的 serialNumber 一般是无线连接的设备或 Genymotion 等第三方 Android 模拟器。
常见异常输出:
没有设备/模拟器连接成功。 List of devices attached 设备/模拟器未连接到 adb 或无响应。 List of devices attached cf264b8f offline USB 连接 通过 USB 连接来正常使用 adb 需要保证几点:
硬件状态正常。 包括 Android 设备处于正常开机状态,USB 连接线和各种接口完好。 Android 设备的开发者选项和 USB 调试模式已开启。 可以到「设置」-「开发者选项」-「Android 调试」查看。 如果在设置里找不到开发者选项,那需要通过一个彩蛋来让它显示出来:在「设置」-「关于手机」连续点击「版本号」7 次。 设备驱动状态正常。 这一点貌似在 Linux 和 Mac OS X 下不用操心,在 Windows 下有可能遇到需要安装驱动的情况,确认这一点可以右键「计算机」-「属性」,到「设备管理器」里查看相关设备上是否有黄色感叹号或问号,如果没有就说明驱动状态已经好了。否则可以下载一个手机助手类程序来安装驱动先。 通过 USB 线连接好电脑和设备后确认状态。adb devices,如果能看到 xxxxxx device,说明连接成功。 无线连接(Android11 及以上) Android 11 及更高版本支持使用 Android 调试桥 (adb) 从工作站以无线方式部署和调试应用。例如,您可以将可调试应用部署到多台远程设备,而无需通过 USB 实际连接设备。这样就可以避免常见的 USB 连接问题,例如驱动程序安装方面的问题。
操作步骤:
更新到最新版本的 SDK 平台工具(至少30.0.0)。 将 Android 设备与要运行 adb 的电脑连接到同一个局域网,比如连到同一个 WiFi。 在开发者选项中启用无线调试。 在询问要允许在此网络上进行无线调试吗?的对话框中,点击允许。 选择使用配对码配对设备,使用弹窗中的 IP 地址和端口号。 adb pair ipaddr:port 提示Enter pairing code: 时输入弹窗中的配对码,成功后会显示Successfully paired to …。 使用无线调试下的 IP 地址和端口。 adb connect ipaddr:port 确认连接状态。adb devices,如果能看到,ipaddr:port device,说明连接成功。 无线连接(需要借助 USB 线) 除了可以通过 USB 连接设备与电脑来使用 adb,也可以通过无线连接——虽然连接过程中也有需要使用 USB 的步骤,但是连接成功之后你的设备就可以在一定范围内摆脱 USB 连接线的限制啦! 操作步骤:
将 Android 设备与要运行 adb 的电脑连接到同一个局域网,比如连到同一个 WiFi。 将设备与电脑通过 USB 线连接。 应确保连接成功(可运行 adb devices 看是否能列出该设备)。 让设备在 5555 端口监听 TCP/IP 连接: adb tcpip 5555 断开 USB 连接。 找到设备的 IP 地址。 一般能在「设置」-「关于手机」-「状态信息」-「IP地址」找到,也可以使用下文里[查看设备信息 - IP 地址][1] 一节里的方法用 adb 命令来查看。 通过 IP 地址连接设备。 adb connect <device-ip-address> 这里的<device-ip-address> 就是上一步中找到的设备 IP 地址。 确认连接状态。 adb devices 如果能看到 <device-ip-address>:5555 device 说明连接成功。 如果连接不了,请确认 Android 设备与电脑是连接到了同一个 WiFi,然后再次执行 adb connect <device-ip-address> 那一步; 如果还是不行的话,通过 adb kill-server 重新启动 adb 然后从头再来一次试试。
断开无线连接 adb disconnect <device-ip-address>
三、应用管理 查看应用列表 查看应用列表的基本命令格式是:
adb shell pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]
即在 adb shell pm list packages 的基础上可以加一些参数进行过滤查看不同的列表,支持的过滤参数如下: |参数|显示列表| |:—|:—-| |无|所有应用| |-f|所有应用及 apk 文件路径| |-d|所有禁用的应用| |-e|所有启用的应用| |-s|所有系统应用| |-3|所有第三方应用| |-i|所有已安装的应用| |-u|包含已卸载应用| |<FILTER>|包名包含 <FILTER> 字符串|
所有应用 命令:
adb shell pm list packages
输出示例:
package:com.android.smoketest
package:com.example.android.livecubes
package:com.android.providers.telephony
package:com.google.android.googlequicksearchbox
package:com.android.providers.calendar
package:com.android.providers.media
package:com.android.protips
package:com.android.documentsui
package:com.android.gallery
package:com.android.externalstorage
...
// other packages here
...
系统应用 命令:
adb shell pm list packages -s
第三方应用 命令:
adb shell pm list packages -3
包名包含某字符串的应用 比如要查看包名包含字符串 mazhuang 的应用列表,命令:
adb shell pm list packages mazhuang
当然也可以使用 grep 来过滤: adb shell pm list packages | grep mazhuang
安装 APK 命令格式:
adb install [-lrtsdg] <path_to_apk>
参数:
adb install 后面可以跟一些可选参数来控制安装 APK 的行为,可用参数及含义如下: |参数|含义| |:—|:—-| |-l|将应用安装到保护目录 /mnt/asec| |-r|允许覆盖安装| |-t|允许安装 AndroidManifest.xml 里 application 指定 android:testOnly=”true” 的应用| |-s|将应用安装到 sdcard| |-d|允许降级覆盖安装| |-g|授予所有运行时权限| |–abi abi-identifier|为特定 ABI 强制安装 apk,abi-identifier 可以是 armeabi-v7a、arm64-v8a、v86、x86_64 等|
运行命令后如果见到类似如下输出(状态为 Success)代表安装成功:
[100%] /data/local/tmp/1.apk
pkg: /data/local/tmp/1.apk
Success
上面是当前最新版 v1.0.36 的 adb 的输出,会显示 push apk 文件到手机的进度百分比。
使用旧版本 adb 的输出则是这样的:
12040 KB/s (22205609 bytes in 1.801s)
pkg: /data/local/tmp/SogouInput_android_v8.3_sweb.apk
Success
而如果状态为 Failure 则表示安装失败,比如:
[100%] /data/local/tmp/map-20160831.apk
pkg: /data/local/tmp/map-20160831.apk
Failure [INSTALL_FAILED_ALREADY_EXISTS]
常见安装失败输出代码、含义可以百度对应。
安装流程 参考:PackageManager.java adb install 内部原理简介 adb install 实际是分三步完成:
push apk 文件到 /data/local/tmp。 调用 pm install 安装。 删除 /data/local/tmp 下的对应 apk 文件。 所以,必要的时候也可以根据这个步骤,手动分步执行安装过程。
卸载应用 adb uninstall [-k] <packagename>
<packagename> 表示应用的包名,-k 参数可选,表示卸载应用但保留数据和缓存目录。 命令示例:
adb uninstall com.qihoo360.mobilesafe
表示卸载 360 手机卫士。
清除应用数据与缓存 adb shell pm clear <packagename>
<packagename> 表示应用名包,这条命令的效果相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」。
命令示例:
adb shell pm clear com.qihoo360.mobilesafe
表示清除 360 手机卫士的数据和缓存。
查看前台 Activity adb shell dumpsys activity activities | grep mResumedActivity
输出示例:
mResumedActivity: ActivityRecord{8079d7e u0 com.cyanogenmod.trebuchet/com.android.launcher3.Launcher t42}
其中的 com.cyanogenmod.trebuchet/com.android.launcher3.Launcher 就是当前处于前台的 Activity。
在 Windows 下以上命令可能不可用,可以尝试adb shell dumpsys activity activities | findstr mResumedActivity或adb shell "dumpsys activity activities | grep mResumedActivity"
查看正在运行的 Services adb shell dumpsys activity services [<packagename>]
<packagename> 参数不是必须的,指定<packagename> 表示查看与某个包名相关的 Services,不指定表示查看所有 Services。 <packagename> 不一定要给出完整的包名,比如运行 adb shell dumpsys activity services org.mazhuang,那么包名 org.mazhuang.demo1、org.mazhuang.demo2 和 org.mazhuang123 等相关的 Services 都会列出来。
查看应用详细信息 adb shell dumpsys package <packagename>
输出中包含很多信息,包括 Activity Resolver Table、Registered ContentProviders、包名、userId、安装后的文件资源代码等路径、版本信息、权限信息和授予状态、签名版本信息等。 <packagename> 表示应用包名。
输出示例:
Activity Resolver Table:
Non-Data Actions:
android.intent.action.MAIN:
5b4cba8 org.mazhuang.guanggoo/.SplashActivity filter 5ec9dcc
Action: "android.intent.action.MAIN" Category: "android.intent.category.LAUNCHER" AutoVerify=falseRegistered ContentProviders:
org.mazhuang.guanggoo/com.tencent.bugly.beta.utils.BuglyFileProvider:
Provider{7a3c394 org.mazhuang.guanggoo/com.tencent.bugly.beta.utils.BuglyFileProvider}
ContentProvider Authorities:
[org.mazhuang.guanggoo.fileProvider]:
Provider{7a3c394 org.mazhuang.guanggoo/com.tencent.bugly.beta.utils.BuglyFileProvider}
applicationInfo=ApplicationInfo{7754242 org.mazhuang.guanggoo}
Key Set Manager:
[org.mazhuang.guanggoo]
Signing KeySets: 501
Packages:
Package [org.mazhuang.guanggoo] (c1d7f):
userId=10394
pkg=Package{55f714c org.mazhuang.guanggoo}
codePath=/data/app/org.mazhuang.guanggoo-2
resourcePath=/data/app/org.mazhuang.guanggoo-2
legacyNativeLibraryDir=/data/app/org.mazhuang.guanggoo-2/lib
primaryCpuAbi=null
secondaryCpuAbi=null
versionCode=74 minSdk=15 targetSdk=25
versionName=1.1.74
splits=[base]
apkSigningVersion=2
applicationInfo=ApplicationInfo{7754242 org.mazhuang.guanggoo}
flags=[ HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]
privateFlags=[ RESIZEABLE_ACTIVITIES ]
dataDir=/data/user/0/org.mazhuang.guanggoo
supportsScreens=[small, medium, large, xlarge, resizeable, anyDensity]
timeStamp=2017-10-22 23:50:53
firstInstallTime=2017-10-22 23:50:25
lastUpdateTime=2017-10-22 23:50:55
installerPackageName=com.miui.packageinstaller
signatures=PackageSignatures{af09595 [53c7caa2]}
installPermissionsFixed=true installStatus=1
pkgFlags=[ HAS_CODE ALLOW_CLEAR_USER_DATA ALLOW_BACKUP ]
requested permissions:
android.permission.READ_PHONE_STATE
android.permission.INTERNET
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_WIFI_STATE
android.permission.READ_LOGS
android.permission.WRITE_EXTERNAL_STORAGE
android.permission.READ_EXTERNAL_STORAGE
install permissions:
android.permission.INTERNET: granted=true android.permission.ACCESS_NETWORK_STATE: granted=true android.permission.ACCESS_WIFI_STATE: granted=true User 0: ceDataInode=1155675 installed=true hidden=false suspended=false stopped=true notLaunched=false enabled=0
gids=[3003]
runtime permissions:
android.permission.READ_EXTERNAL_STORAGE: granted=true android.permission.READ_PHONE_STATE: granted=true android.permission.WRITE_EXTERNAL_STORAGE: granted=true User 999: ceDataInode=0 installed=false hidden=false suspended=false stopped=true notLaunched=true enabled=0
gids=[3003]
runtime permissions:
Dexopt state:
[org.mazhuang.guanggoo]
Instruction Set: arm64
path: /data/app/org.mazhuang.guanggoo-2/base.apk
status: /data/app/org.mazhuang.guanggoo-2/oat/arm64/base.odex [compilation_filter=speed-profile, status=kOatUpToDa
te]
查看应用安装路径 adb shell pm path <PACKAGE>
输出应用安装路径
adb shell pm path ecarx.weather
输出:
package:/data/app/ecarx.weather-1.apk
四、与应用交互 通常情况下adb与应用交互需要知道包名和Activity名,这里介绍了个在win系统时获取这些名的方法 命令:
aapt dump badging 包名(apk)| findstr package
这里实际是用了两个命令:aapt和findstr,aapt获取APK信息,findstr查找包含package的行(类似于linux的grep),结果如下:
package: name='com.naeiq1est.navi' versionCode='2023013001' versionName='2023.01.30.01'
以上为获取到了包名,如需获取到Activity名只需要修改findstr的字串为activity,结果如下:
launchable activity name='com.naviquest.navi.ScreenConfig'label='??????' icon=''
命令介绍 主要是使用am <command> 命令,常用的 <command> 如下:
start [options] <INTENT>
启动 <INTENT> 指定的 Activity
startservice [options] <INTENT>
启动 <INTENT> 指定的 Service
broadcast [options] <INTENT>
发送 <INTENT> 指定的广播
force-stop <packagename>
停止 <packagename> 相关的进程
<INTENT> 参数很灵活,和写 Android 程序时代码里的 Intent 相对应。
用于决定 intent 对象的选项如下:
参数
含义
-a <ACTION>
指定 action,比如 android.intent.action.VIEW
-c <CATEGORY>
指定 category,比如 android.intent.category.APP_CONTACTS
-n <COMPONENT>
指定完整 component 名,用于明确指定启动哪个 Activity,如 com.example.app/.ExampleActivity
<INTENT> 里还能带数据,就像写代码时的 Bundle 一样:
参数
含义
--esn <EXTRA_KEY>
null 值(只有 key 名)
`-e
--es <EXTRA_KEY> <EXTRA_STRING_VALUE>`
--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE>
boolean 值
--ei <EXTRA_KEY> <EXTRA_INT_VALUE>
integer 值
--el <EXTRA_KEY> <EXTRA_LONG_VALUE>
long 值
--ef <EXTRA_KEY> <EXTRA_FLOAT_VALUE>
float 值
--eu <EXTRA_KEY> <EXTRA_URI_VALUE>
URI
--ecn <EXTRA_KEY> <EXTRA_COMPONENT_NAME_VALUE>
component name
--eia <EXTRA_KEY> <EXTRA_INT_VALUE>[,<EXTRA_INT_VALUE...]
integer 数组
--ela <EXTRA_KEY> <EXTRA_LONG_VALUE>[,<EXTRA_LONG_VALUE...]
long 数组
启动应用/ 调起 Activity 指定Activity名称启动
命令格式:
adb shell am start [options] <INTENT>
例如:
adb shell am start -n com.tencent.mm/.ui.LauncherUI
表示调起微信主界面。
adb shell am start -n org.mazhuang.boottimemeasure/.MainActivity --es "toast""hello, world"
表示调起 org.mazhuang.boottimemeasure/.MainActivity 并传给它 string 数据键值对 toast - hello, world。
不指定Activity名称启动(启动主Activity) 命令格式:
adb shell monkey -p <packagename> -c android.intent.category.LAUNCHER 1
例如:
adb shell monkey -p com.tencent.mm -c android.intent.category.LAUNCHER 1
表示调起微信主界面。
调起 Service 命令格式:
adb shell am startservice [options] <INTENT>
例如:
adb shell am startservice -n com.tencent.mm/.plugin.accountsync.model.AccountAuthenticatorService
表示调起微信的某 Service。
另外一个典型的用例是如果设备上原本应该显示虚拟按键但是没有显示,可 以试试这个:
adb shell am startservice -n com.android.systemui/.SystemUIService
停止 Service 命令格式:
adb shell am stopservice [options] <INTENT>
发送广播 命令格式:
adb shell am broadcast [options] <INTENT>
可以向所有组件广播,也可以只向指定组件广播。
例如,向所有组件广播 BOOT_COMPLETED:
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
又例如,只向 org.mazhuang.boottimemeasure/.BootCompletedReceiver 广播 BOOT_COMPLETED:
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n org.mazhuang.boottimemeasure/.BootCompletedReceiver
这类用法在测试的时候很实用,比如某个广播的场景很难制造,可以考虑通过这种方式来发送广播。
既能发送系统预定义的广播,也能发送自定义广播。如下是部分系统预定义广播及正常触发时机:
android.net.conn.CONNECTIVITY_CHANGE
网络连接发生变化
android.intent.action.SCREEN_ON
屏幕点亮
android.intent.action.SCREEN_OFF
屏幕熄灭
android.intent.action.BATTERY_LOW
电量低,会弹出电量低提示框
android.intent.action.BATTERY_OKAY
电量恢复了
android.intent.action.BOOT_COMPLETED
设备启动完毕
android.intent.action.DEVICE_STORAGE_LOW
存储空间过低
android.intent.action.DEVICE_STORAGE_OK
存储空间恢复
android.intent.action.PACKAGE_ADDED
安装了新的应用
android.net.wifi.STATE_CHANGE
WiFi 连接状态发生变化
android.net.wifi.WIFI_STATE_CHANGED
WiFi 状态变为启用/关闭/正在启动/正在关闭/未知
android.intent.action.BATTERY_CHANGED
电池电量发生变化
android.intent.action.INPUT_METHOD_CHANGED
系统输入法发生变化
android.intent.action.ACTION_POWER_CONNECTED
外部电源连接
android.intent.action.ACTION_POWER_DISCONNECTED
外部电源断开连接
android.intent.action.DREAMING_STARTED
系统开始休眠
android.intent.action.DREAMING_STOPPED
系统停止休眠
android.intent.action.WALLPAPER_CHANGED
壁纸发生变化
android.intent.action.HEADSET_PLUG
插入耳机
android.intent.action.MEDIA_UNMOUNTED
卸载外部介质
android.intent.action.MEDIA_MOUNTED
挂载外部介质
android.os.action.POWER_SAVE_MODE_CHANGED
省电模式开启
(以上广播均可使用 adb 触发)
强制停止应用 命令:
adb shell am force-stop <packagename>
命令示例:
adb shell am force-stop com.qihoo360.mobilesafe
表示停止 360 安全卫士的一切进程与服务。
收紧内存 命令:
adb shell am send-trim-memory <pid> <level>
pid: 进程 ID level:HIDDEN、RUNNING_MODERATE、BACKGROUND、 RUNNING_LOW、MODERATE、RUNNING_CRITICAL、COMPLETE
命令示例:
adb shell am send-trim-memory 12345 RUNNING_LOW
表示向 pid=12345 的进程,发出 level=RUNNING_LOW 的收紧内存命令。
五、文件管理 复制设备里的文件到电脑 命令:
adb pull <设备里的文件路径> [电脑上的目录]
其中 电脑上的目录 参数可以省略,默认复制到当前目录。 例:
adb pull /sdcard/sr.mp4 ~/tmp/
小技巧:设备上的文件路径可能需要 root 权限才能访问,如果你的设备已经 root 过,可以先使用 adb shell 和 su 命令在 adb shell 里获取 root 权限后,先 cp /path/on/device /sdcard/filename 将文件复制到 sdcard,然后 adb pull /sdcard/filename /path/on/pc。
复制电脑里的文件到设备 命令:
adb push <电脑上的文件路径> <设备里的目录>
例:
adb push ~/sr.mp4 /sdcard/
小技巧:设备上的文件路径普通权限可能无法直接写入,如果你的设备已经 root 过,可以先 adb push /path/on/pc /sdcard/filename,然后 adb shell 和 su 在 adb shell 里获取 root 权限后,cp /sdcard/filename /path/on/device。
六、模拟按键/输入 在 adb shell 里有个很实用的命令叫 input,通过它可以做一些有趣的事情。 input 命令的完整 help 信息如下:
Usage: input [<source>] <command> [<arg>...]
The sources are:
mouse
keyboard
joystick
touchnavigation
touchpad
trackball
stylus
dpad
gesture
touchscreen
gamepad
The commands and default sources are:
text <string> (Default: touchscreen)
keyevent [--longpress] <key code number or name> ... (Default: keyboard)
tap <x> <y> (Default: touchscreen)
swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
press (Default: trackball)
roll <dx> <dy> (Default: trackball)
比如使用adb shell input keyevent <keycode> 命令,不同的 keycode 能实现不同的功能,完整的 keycode 列表详见 KeyEvent,摘引部分我觉得有意思的如下:
3
HOME 键
4
返回键
5
打开拨号应用
6
挂断电话
24
增加音量
25
降低音量
26
电源键
27
拍照(需要在相机应用里)
64
打开浏览器
82
菜单键
85
播放/暂停
86
停止播放
87
播放下一首
88
播放上一首
122
移动光标到行首或列表顶部
123
移动光标到行末或列表底部
126
恢复播放
127
暂停播放
164
静音
176
打开系统设置
187
切换应用
207
打开联系人
208
打开日历
209
打开音乐
210
打开计算器
220
降低屏幕亮度
221
提高屏幕亮度
223
系统休眠
224
点亮屏幕
231
打开语音助手
276
如果没有 wakelock 则让系统休眠
下面是 input 命令的一些用法举例。
电源键
命令:
adb shell input keyevent 26
执行效果相当于按电源键。
菜单键
命令:
adb shell input keyevent 82
HOME 键
命令:
adb shell input keyevent 3
返回键
命令:
adb shell input keyevent 4
音量控制
增加音量:
adb shell input keyevent 24
降低音量:
adb shell input keyevent 25
静音:
adb shell input keyevent 164
媒体控制
播放/暂停:
adb shell input keyevent 85
停止播放:
adb shell input keyevent 86
播放下一首:
adb shell input keyevent 87
播放上一首:
adb shell input keyevent 88
恢复播放:
adb shell input keyevent 126
暂停播放:
adb shell input keyevent 127
点亮/熄灭屏幕
可以通过上文讲述过的模拟电源键来切换点亮和熄灭屏幕,但如果明确地想要点亮或者熄灭屏幕,那可以使用如下方法。
点亮屏幕:
adb shell input keyevent 224
熄灭屏幕:
adb shell input keyevent 223
滑动解锁
如果锁屏没有密码,是通过滑动手势解锁,那么可以通过 input swipe 来解锁。
命令(参数以机型 Nexus 5,向上滑动手势解锁举例):
adb shell input swipe 300 1000 300 500
参数 300 1000 300 500 分别表示起始点x坐标 起始点y坐标 结束点x坐标 结束点y坐标。
输入文本
在焦点处于某文本框时,可以通过 input 命令来输入文本。
命令:
adb shell input text hello
现在 hello 出现在文本框了。
七、查看日志 Android 系统的日志分为两部分,底层的 Linux 内核日志输出到 /proc/kmsg,Android 的日志输出到 /dev/log。
Android 日志 命令格式:
[adb] logcat [<option>] ... [<filter-spec>] ...
常用用法列举如下:
按级别过滤日志 Android 的日志分为如下几个优先级(priority):
V —— Verbose(最低,输出得最多) D —— Debug I —— Info W —— Warning E —— Error F —— Fatal S —— Silent(最高,啥也不输出) 按某级别过滤日志则会将该级别及以上的日志输出。 比如,命令:
会将 Warning、Error、Fatal 和 Silent 日志输出。
(注: 在 macOS 下需要给 :W 这样以 * 作为 tag 的参数加双引号,如 adb logcat “ :W”,不然会报错 no matches found: *:W。)
按 tag 和级别过滤日志 <filter-spec> 可以由多个<tag>[:priority]组成。
比如,命令:
adb logcat ActivityManager:I MyApp:D *:S
表示输出 tag ActivityManager 的 Info 以上级别日志,输出 tag MyApp 的 Debug 以上级别日志,及其它 tag 的 Silent 级别日志(即屏蔽其它 tag 日志)。
日志格式 可以用 adb logcat -v <format> 选项指定日志输出格式。 日志支持按以下几种<format>:
* brief
默认格式。格式为:
<priority>/<tag>(<pid>): <message>
示例:
D/HeadsetStateMachine( 1785): Disconnected process message: 10, size: 0
* process
格式为:
<priority>(<pid>) <message>
示例:
D( 1785) Disconnected process message: 10, size: 0 (HeadsetStateMachine)
* tag
格式为:
<priority>/<tag>: <message>
示例:
D/HeadsetStateMachine: Disconnected process message: 10, size: 0
* raw
格式为:
<message>
示例:
Disconnected process message: 10, size: 0
* time
格式为:
<datetime> <priority>/<tag>(<pid>): <message>
示例:
08-28 22:39:39.974 D/HeadsetStateMachine( 1785): Disconnected process message: 10, size: 0
* threadtime
格式为:
<datetime> <pid> <tid> <priority> <tag>: <message>
示例:
08-28 22:39:39.974 1785 1832 D HeadsetStateMachine: Disconnected process message: 10, size: 0
* long
格式为:
[ <datetime> <pid>:<tid> <priority>/<tag> ]
<message>
示例:
[ 08-28 22:39:39.974 1785: 1832 D/HeadsetStateMachine ]
Disconnected process message: 10, size: 0
指定格式可与上面的过滤同时使用。比如:
adb logcat -v long ActivityManager:I *:S
清空日志 内核日志 命令:
输出示例:
<6>[14201.684016] PM: noirq resume of devices complete after 0.982 msecs
<6>[14201.685525] PM: early resume of devices complete after 0.838 msecs
<6>[14201.753642] PM: resume of devices complete after 68.106 msecs
<4>[14201.755954] Restarting tasks ... done.
<6>[14201.771229] PM: suspendexit 2016-08-28 13:31:32.679217193 UTC
<6>[14201.872373] PM: suspend entry 2016-08-28 13:31:32.780363596 UTC
<6>[14201.872498] PM: Syncing filesystems ... done.
中括号里的 [14201.684016]代表内核开始启动后的时间,单位为秒。
通过内核日志我们可以做一些事情,比如衡量内核启动时间,在系统启动完毕后的内核日志里找到 Freeing init memory 那一行前面的时间就是。
八、设备信息 型号 命令:
adb shell getprop ro.product.model
输出示例:
电池状况 命令:
adb shell dumpsys battery
输入示例:
Current Battery Service state:
AC powered: false USB powered: true Wireless powered: false status: 2
health: 2
present: true level: 44
scale: 100
voltage: 3872
temperature: 280
technology: Li-poly
其中 scale 代表最大电量,level 代表当前电量。上面的输出表示还剩下 44% 的电量。
屏幕分辨率 命令:
输出示例:
该设备屏幕分辨率为 1080px * 1920px。 如果使用命令修改过,那输出可能是:
Physical size: 1080x1920
Override size: 480x1024
表明设备的屏幕分辨率原本是 1080px * 1920px,当前被修改为 480px * 1024px。
屏幕密度 命令:
输出示例:
该设备屏幕密度为 420dpi。 如果使用命令修改过,那输出可能是:
Physical density: 480
Override density: 160
表明设备的屏幕密度原来是 480dpi,当前被修改为 160dpi。
显示屏参数 命令:
adb shell dumpsys window displays
输出示例:
WINDOW MANAGER DISPLAY CONTENTS (dumpsys window displays)
Display: mDisplayId=0
init=1080x1920 420dpi cur=1080x1920 app=1080x1794 rng=1080x1017-1810x1731
deferred=false layoutNeeded=false
其中 mDisplayId 为 显示屏编号,init 是初始分辨率和屏幕密度,app 的高度比 init 里的要小,表示屏幕底部有虚拟按键,高度为 1920 - 1794 = 126px 合 42dp。
android_id 查看命令:
adb shell settings get secure android_id
或:
adb shell content query --uri content://settings/secure --where "name=\'android_id\'"
输出示例:
设置命令 adb shell settings put secure android_id 123456789addvff
IMEI 在 Android 4.4 及以下版本可通过如下命令获取 IMEI:
adb shell dumpsys iphonesubinfo
输出示例:
Phone Subscriber Info:
Phone Type = GSM
Device ID = 860955027785041
其中的 Device ID 就是 IMEI。 而在 Android 5.0 及以上版本里这个命令输出为空,得通过其它方式获取了(需要 root 权限):
adb shell
su
service call iphonesubinfo 1
输出示例:
Result: Parcel(
0x00000000: 00000000 0000000f 00360038 00390030 '........8.6.0.9.' 0x00000010: 00350035 00320030 00370037 00350038 '5.5.0.2.7.7.8.5.' 0x00000020: 00340030 00000031 '0.4.1... ')
把里面的有效内容提取出来就是 IMEI 了,比如这里的是
参考:
adb shell dumpsys iphonesubinfo not working since Android 5.0 Lollipop
Android 系统版本 命令:
adb shell getprop ro.build.version.release
输出示例:
5.0.2
IP 地址 每次想知道设备的 IP 地址的时候都得「设置」-「关于手机」-「状态信息」-「IP地址」很烦对不对?通过 adb 可以方便地查看。 命令:
adb shell ifconfig | grep Mask
输出示例:
inet addr:10.130.245.230 Mask:255.255.255.252
inet addr:127.0.0.1 Mask:255.0.0.0
那么 10.130.245.230 就是设备 IP 地址。 在有的设备上这个命令没有输出,如果设备连着 WiFi,可以使用如下命令来查看局域网 IP:
输出示例:
wlan0: ip 10.129.160.99 mask 255.255.240.0 flags [up broadcast running multicast]
或
wlan0 Link encap:UNSPEC
inet addr:10.129.168.57 Bcast:10.129.175.255 Mask:255.255.240.0
inet6 addr: fe80::66cc:2eff:fe68:b6b6/64 Scope: Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:496520 errors:0 dropped:0 overruns:0 frame:0
TX packets:68215 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:3000
RX bytes:116266821 TX bytes:8311736
如果以上命令仍然不能得到期望的信息,那可以试试以下命令(部分系统版本里可用):
输出示例:
wlan0 UP 10.129.160.99/20 0x00001043 f8:a9:d0:17:42:4d
lo UP 127.0.0.1/8 0x00000049 00:00:00:00:00:00
p2p0 UP 0.0.0.0/0 0x00001003 fa:a9:d0:17:42:4d
sit0 DOWN 0.0.0.0/0 0x00000080 00:00:00:00:00:00
rmnet0 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rmnet1 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rmnet3 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rmnet2 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rmnet4 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rmnet6 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rmnet5 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rmnet7 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
rev_rmnet3 DOWN 0.0.0.0/0 0x00001002 4e:b7:e4:2e:17:58
rev_rmnet2 DOWN 0.0.0.0/0 0x00001002 4e:f0:c8:bf:7a:cf
rev_rmnet4 DOWN 0.0.0.0/0 0x00001002 a6:c0:3b:6b:c4:1f
rev_rmnet6 DOWN 0.0.0.0/0 0x00001002 66:bb:5d:64:2e:e9
rev_rmnet5 DOWN 0.0.0.0/0 0x00001002 0e:1b:eb:b9:23:a0
rev_rmnet7 DOWN 0.0.0.0/0 0x00001002 7a:d9:f6:81:40:5a
rev_rmnet8 DOWN 0.0.0.0/0 0x00001002 4e:e2:a9:bb:d0:1b
rev_rmnet0 DOWN 0.0.0.0/0 0x00001002 fe:65:d0:ca:82:a9
rev_rmnet1 DOWN 0.0.0.0/0 0x00001002 da:d8:e8:4f:2e:fe
可以看到网络连接名称、启用状态、IP 地址和 Mac 地址等信息。
Mac 地址 命令:
adb shell cat /sys/class/net/wlan0/address
输出示例:
这查看的是局域网 Mac 地址,移动网络或其它连接的信息可以通过前面的小节「IP 地址」里提到的 adb shell netcfg 命令来查看。
CPU 信息 命令:
adb shell cat /proc/cpuinfo
输出示例:
Processor : ARMv7 Processor rev 0 (v7l)
processor : 0
BogoMIPS : 38.40
processor : 1
BogoMIPS : 38.40
processor : 2
BogoMIPS : 38.40
processor : 3
BogoMIPS : 38.40
Features : swp half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt
CPU implementer : 0x51
CPU architecture: 7
CPU variant : 0x2
CPU part : 0x06f
CPU revision : 0
Hardware : Qualcomm MSM 8974 HAMMERHEAD (Flattened Device Tree)
Revision : 000b
Serial : 0000000000000000
这是 Nexus 5 的 CPU 信息,我们从输出里可以看到使用的硬件是 Qualcomm MSM 8974,processor 的编号是 0 到 3,所以它是四核的,采用的架构是 ARMv7 Processor rev 0 (v71)。
内存信息 命令:
adb shell cat /proc/meminfo
输出示例:
MemTotal: 1027424 kB
MemFree: 486564 kB
Buffers: 15224 kB
Cached: 72464 kB
SwapCached: 24152 kB
Active: 110572 kB
Inactive: 259060 kB
Active(anon): 79176 kB
Inactive(anon): 207736 kB
Active(file): 31396 kB
Inactive(file): 51324 kB
Unevictable: 3948 kB
Mlocked: 0 kB
HighTotal: 409600 kB
HighFree: 132612 kB
LowTotal: 617824 kB
LowFree: 353952 kB
SwapTotal: 262140 kB
SwapFree: 207572 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 265324 kB
Mapped: 47072 kB
Shmem: 1020 kB
Slab: 57372 kB
SReclaimable: 7692 kB
SUnreclaim: 49680 kB
KernelStack: 4512 kB
PageTables: 5912 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 775852 kB
Committed_AS: 13520632 kB
VmallocTotal: 385024 kB
VmallocUsed: 61004 kB
VmallocChunk: 209668 kB
其中,MemTotal 就是设备的总内存,MemFree 是当前空闲内存。
硬件与系统属性 设备的更多硬件与系统属性可以通过如下命令查看:
adb shell cat /system/build.prop
这会输出很多信息,包括前面几个小节提到的「型号」和「Android 系统版本」等。
输出里还包括一些其它有用的信息,它们也可通过 adb shell getprop <属性名> 命令单独查看,列举一部分属性如下:
ro.build.version.sdk
SDK 版本
ro.build.version.release
Android 系统版本
ro.build.version.security_patch
Android 安全补丁程序级别
ro.product.model
型号
ro.product.brand
品牌
ro.product.name
设备名
ro.product.board
处理器型号
ro.product.cpu.abilist
CPU 支持的 abi 列表[节注一]
persist.sys.isUsbOtgEnabled
是否支持 OTG
dalvik.vm.heapsize
每个应用程序的内存上限
ro.sf.lcd_density
屏幕密度
ro.build.id=GRI40
版本ID
ro.build.display.id=GRJ22
版本号
ro.build.version.incremental=eng.buildbot.20110619.060228
版本增量
ro.build.version.sdk=10
sdk版本
ro.build.version.codename=REL
版本代号
ro.build.version.release=2.3.4
Android 2.3.4系統无需修改,也可改为3.0装装B
ro.build.date=Sun Jun 19 06:02:58 UTC 2011
制作者制作的时间,可修改2011年X月X日 某某某制作
ro.build.date.utc=0
ro.build.type=user
ro.build.user=buildbot
ro.build.host=bb1
ro.build.tags=test-keys
ro.product.model=HTC Wildfire
HTC内部手机代号也就是手机名,改为你想改的名字
ro.product.brand=htc_wwe
手机品牌,改为中国山寨机
ro.product.name=htc_buzz
手机正式名称,改为你想改的名字
ro.product.device=buzz
采用的设备,改为China G8
ro.product.board=buzz
采用的处理器,改为China 800.8Ghz
ro.product.cpu.abi=armeabi-v6j
cpu的版本
ro.product.cpu.abi2=armeabi
cpu的品牌
ro.product.manufacturer=HTC
手机制造商,改为中国智造
ro.product.locale.language=en
手机默认语言,把en改为zh
ro.product.locale.region=US
地区语言,美国毛多呀美国,干掉US改为CN
ro.wifi.channels=
WIFI连接的渠道
ro.board.platform=msm7k
主板平台
ro.build.product=buzz
建立产品
ro.build.description=passion-user 2.3.3 GRI40 102588 release-keys
用户的KEY
ro.build.fingerprint=google/passion/passion:2.3.3/GRI40/102588:user/release-keys
机身码的啥玩意
节注一: 一些小厂定制的 ROM 可能修改过 CPU 支持的 abi 列表的属性名,如果用 ro.product.cpu.abilist 属性名查找不到,可以这样试试:
adb shell cat /system/build.prop | grep ro.product.cpu.abi
示例输出:
ro.product.cpu.abi=armeabi-v7a
ro.product.cpu.abi2=armeabi
查看/修改序列号 查看命令:
或:
adb shell getprop sys.serialno
修改序列号: 通常安卓系统正常流程需要修改 cmdline中的参数,可以通过命令查看cmdline参数:
adb shell cat /proc/cmdline
显示:
storagemedia=emmc androidboot.mode=emmc androidboot.dtbo_idx=0 androidboot.slot_suffix= androidboot.serialno=9501a97c59fe092 console=ttyFIQ0 androidboot.baseband=N/A androidboot.wificountrycode=US androidboot.veritymode=enforcing androidboot.hardware=rk30board androidboot.console=ttyFIQ0 firmware_class.path=/vendor/etc/firmware init=/init rootwait ro init=/init root=PARTUUID=af01642c-9b84-11e8-9b2a-234eb5e198a0 loop.max_part=7 androidboot.selinux=permissive buildvariant=userdebug earlyprintk=uart8250,mmio32,0xff690000 swiotlb=1 kpti=0 bt_type=4
九、修改设置 注: 修改设置之后,运行恢复命令有可能显示仍然不太正常,可以运行 adb reboot 重启设备,或手动重启。 修改设置的原理主要是通过 settings 命令修改 /data/data/com.android.providers.settings/databases/settings.db 里存放的设置值。
分辨率 命令: adb shell wm size 480x1024 表示将分辨率修改为 480px * 1024px。 恢复原分辨率命令: adb shell wm size reset
屏幕密度 命令: adb shell wm density 160 表示将屏幕密度修改为 160dpi。 恢复原屏幕密度命令: adb shell wm density reset
显示区域 命令: adb shell wm overscan 0,0,0,200 四个数字分别表示距离左、上、右、下边缘的留白像素,以上命令表示将屏幕底部 200px 留白。 恢复原显示区域命令: adb shell wm overscan reset
关闭 USB 调试模式 命令: adb shell settings put global adb_enabled 0 恢复: 用命令恢复不了了,毕竟关闭了 USB 调试 adb 就连接不上 Android 设备了。 去设备上手动恢复吧:「设置」-「开发者选项」-「Android 调试」。
允许/禁止访问非 SDK API 允许访问非 SDK API: adb shell settings put global hidden_api_policy_pre_p_apps 1 adb shell settings put global hidden_api_policy_p_apps 1 禁止访问非 SDK API: adb shell settings delete global hidden_api_policy_pre_p_apps adb shell settings delete global hidden_api_policy_p_apps 不需要设备获得 Root 权限。
命令最后的数字的含义:
0
禁止检测非 SDK 接口的调用。该情况下,日志记录功能被禁用,并且令 strict mode API,即 detectNonSdkApiUsage() 无效。不推荐。
1
仅警告——允许访问所有非 SDK 接口,但保留日志中的警告信息,可继续使用 strick mode API。
2
禁止调用深灰名单和黑名单中的接口。
3
禁止调用黑名单中的接口,但允许调用深灰名单中的接口。
状态栏和导航栏的显示隐藏 本节所说的相关设置对应 Cyanogenmod 里的「扩展桌面」。 命令:
adb shell settings put global policy_control <key-values>
<key-values> 可由如下几种键及其对应的值组成,格式为 <key1>=<value1>:<key2>=<value2>。
immersive.full
同时隐藏
immersive.status
隐藏状态栏
immersive.navigation
隐藏导航栏
immersive.preconfirms
?
这些键对应的值可则如下值用逗号组合:
apps
所有应用
*
所有界面
packagename
指定应用
-packagename
排除指定应用
例如:
adb shell settings put global policy_control immersive.full=*
表示设置在所有界面下都同时隐藏状态栏和导航栏。
adb shell settings put global policy_control immersive.status=com.package1,com.package2:immersive.navigation=apps,-com.package3
表示设置在包名为 com.package1 和 com.package2 的应用里隐藏状态栏,在除了包名为 com.package3 的所有应用里隐藏导航栏。
十、实用功能 屏幕截图 截图保存到电脑:
adb exec-out screencap -p > sc.png
如果 adb 版本较老,无法使用 exec-out 命令,这时候建议更新 adb 版本。无法更新的话可以使用以下麻烦点的办法:
先截图保存到设备里:
adb shell screencap -p /sdcard/sc.png
然后将 png 文件导出到电脑:
可以使用 adb shell screencap -h 查看 screencap 命令的帮助信息,下面是两个有意义的参数及含义:
-p
指定保存文件为 png 格式
-d display-id
指定截图的显示屏编号(有多显示屏的情况下)
实测如果指定文件名以 .png 结尾时可以省略 -p 参数;否则需要使用 -p 参数。如果不指定文件名,截图文件的内容将直接输出到 stdout。
另外一种一行命令截图并保存到电脑的方法:
Linux 和 Windows adb shell screencap -p | sed "s/\r$//" > sc.png
Mac OS X adb shell screencap -p | gsed "s/\r$//" > sc.png
这个方法需要用到 gnu sed 命令,在 Linux 下直接就有,在 Windows 下 Git 安装目录的 bin 文件夹下也有。如果确实找不到该命令,可以下载 sed for Windows 并将 sed.exe 所在文件夹添加到 PATH 环境变量里。
而在 Mac 下使用系统自带的 sed 命令会报错:
sed: RE error: illegal byte sequence
需要安装 gnu-sed,然后使用 gsed 命令:
录制屏幕 录制屏幕以 mp4 格式保存到 /sdcard:
adb shell screenrecord /sdcard/filename.mp4
需要停止时按 Ctrl-C,默认录制时间和最长录制时间都是 180 秒。 如果需要导出到电脑:
adb pull /sdcard/filename.mp4
可以使用 adb shell screenrecord –help 查看 screenrecord 命令的帮助信息,下面是常见参数及含义:
--size WIDTHxHEIGHT
视频的尺寸,比如 1280x720,默认是屏幕分辨率。
--bit-rate RATE
视频的比特率,默认是 4Mbps。
--time-limit TIME
录制时长,单位秒。
--verbose
输出更多信息。
重新挂载 system 分区为可写 注:需要 root 权限。
/system 分区默认挂载为只读,但有些操作比如给 Android 系统添加命令、删除自带应用等需要对 /system 进行写操作,所以需要重新挂载它为可读写。
步骤:
进入 shell 并切换到 root 用户权限。 命令: adb shell su 查看当前分区挂载情况。 命令: mount 输出示例:
rootfs / rootfs ro,relatime 0 0
tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,mode=755 0 0
devpts /dev/pts devpts rw,seclabel,relatime,mode=600 0 0
proc /proc proc rw,relatime 0 0
sysfs /sys sysfs rw,seclabel,relatime 0 0
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
none /var tmpfs rw,seclabel,relatime,mode=770,gid=1000 0 0
none /acct cgroup rw,relatime,cpuacct 0 0
none /sys/fs/cgroup tmpfs rw,seclabel,relatime,mode=750,gid=1000 0 0
none /sys/fs/cgroup/memory cgroup rw,relatime,memory 0 0
tmpfs /mnt/asec tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
tmpfs /mnt/obb tmpfs rw,seclabel,relatime,mode=755,gid=1000 0 0
none /dev/memcg cgroup rw,relatime,memory 0 0
none /dev/cpuctl cgroup rw,relatime,cpu 0 0
none /sys/fs/cgroup tmpfs rw,seclabel,relatime,mode=750,gid=1000 0 0
none /sys/fs/cgroup/memory cgroup rw,relatime,memory 0 0
none /sys/fs/cgroup/freezer cgroup rw,relatime,freezer 0 0
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/userdata /data ext4 rw,seclabel,nosuid,nodev,relatime,noauto_da_alloc,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/cache /cache ext4 rw,seclabel,nosuid,nodev,relatime,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/persist /persist ext4 rw,seclabel,nosuid,nodev,relatime,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/modem /firmware vfat ro,context=u:object_r:firmware_file:s0,relatime,uid=1000,gid=1000,fmask=0337,dmask=0227,codepage=cp437,iocharset=iso8859-1,shortname=lower,errors=remount-ro 0 0
/dev/fuse /mnt/shell/emulated fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0
/dev/fuse /mnt/shell/emulated/0 fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0
找到其中我们关注的带 /system 的那一行:
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
mount -o remount,rw -t yaffs2 /dev/block/platform/msm_sdcc.1/by-name/system /system
这里的 /dev/block/platform/msm_sdcc.1/by-name/system 就是我们从上一步的输出里得到的文件路径。
如果输出没有提示错误的话,操作就成功了,可以对 /system 下的文件为所欲为了。
查看连接过的 WiFi 密码 注:需要 root 权限。 命令:
adb shell
su
cat /data/misc/wifi/*.conf
输出示例:
network={
ssid="TP-LINK_9DFC" scan_ssid=1
psk="123456789" key_mgmt=WPA-PSK
group=CCMP TKIP
auth_alg=OPEN
sim_num=1
priority=13893
}
network={
ssid="TP-LINK_F11E" psk="987654321" key_mgmt=WPA-PSK
sim_num=1
priority=17293
}
ssid 即为我们在 WLAN 设置里看到的名称,psk 为密码,key_mgmt 为安全加密方式。
如果 Android O 或以后,WiFi 密码保存的地址有变化,是在 WifiConfigStore.xml 里面
adb shell
su
cat /data/misc/wifi/WifiConfigStore.xml
输出格式:
数据项较多,只需关注 ConfigKey(WiFi 名字)和 PreSharedKey(WiFi 密码)即可/
<stringname="ConfigKey">"Wi-Fi"WPA_PSK</string><stringname="PreSharedKey">"931907334"</string>
设置系统日期和时间 注:需要 root 权限。 命令:
adb shell
su
date -s 20160823.131500
表示将系统日期和时间更改为 2016 年 08 月 23 日 13 点 15 分 00 秒。
重启手机 命令: adb reboot
检测设备是否已 root 命令: adb shell su 此时命令行提示符是 $ 则表示没有 root 权限,是 # 则表示已 root。
使用 Monkey 进行压力测试 Monkey 可以生成伪随机用户事件来模拟单击、触摸、手势等操作,可以对正在开发中的程序进行随机压力测试。 简单用法:
adb shell monkey -p <packagename> -v 500
表示向 <packagename>指定的应用程序发送 500 个伪随机事件。 Monkey 的详细用法参考 官方文档。
开启/关闭 WiFi 注:需要 root 权限。 有时需要控制设备的 WiFi 状态,可以用以下指令完成。 开启 WiFi:
adb root
adb shell svc wifi enable
关闭 WiFi:
adb root
adb shell svc wifi disable
若执行成功,输出为空;若未取得 root 权限执行此命令,将执行失败,输出 Killed。
十一、刷机相关命令 重启到 Recovery 模式 命令:
从 Recovery 重启到 Android 命令:
重启到 Fastboot 模式 命令:
通过 sideload 更新系统 如果我们下载了 Android 设备对应的系统更新包到电脑上,那么也可以通过 adb 来完成更新。
以 Recovery 模式下更新为例:
重启到 Recovery 模式。 命令: 在设备的 Recovery 界面上操作进入 Apply update-Apply from ADB。注:不同的 Recovery 菜单可能与此有差异,有的是一级菜单就有 Apply update from ADB。 通过 adb 上传和更新系统。 命令: adb sideload <path-to-update.zip>
十二、安全相关命令 启用/禁用 SELinux
启用 SELinux
adb root
adb shell setenforce 1
禁用 SELinux
adb root
adb shell setenforce 0
启用/禁用 dm_verity
启用 dm_verity
adb root
adb enable-verity
禁用 dm_verity
adb root
adb disable-verity
十三、系统管理命令 Android 系统是基于 Linux 内核的,所以 Linux 里的很多命令在 Android 里也有相同或类似的实现,在 adb shell 里可以调用。本文档前面的部分内容已经用到了 adb shell 命令。
查看进程 命令:
输出示例:
USER PID PPID VSIZE RSS WCHAN PC NAME
root 1 0 8904 788 ffffffff 00000000 S /init
root 2 0 0 0 ffffffff 00000000 S kthreadd
...
u0_a71 7779 5926 1538748 48896 ffffffff 00000000 S com.sohu.inputmethod.sogou:classic
u0_a58 7963 5926 1561916 59568 ffffffff 00000000 S org.mazhuang.boottimemeasure
...
shell 8750 217 10640 740 00000000 b6f28340 R ps
各列含义: |列名|含义| |-|-| |USER|所属用户| |PID|进程 ID| |PPID|父进程 ID| |VSIZE|进程虚拟内存大小| |RSS|进程物理内存大小| |WCHAN|进程当前的等待事件| |PC|进程当前执行的指令地址| |NAME|进程名|
查看实时资源占用情况 命令:
输出示例:
User 0%, System 6%, IOW 0%, IRQ 0%
User 3 + Nice 0 + Sys 21 + Idle 280 + IOW 0 + IRQ 0 + SIRQ 3 = 307
PID PR CPU% S #THR VSS RSS PCY UID Name 8763 0 3% R 1 10640K 1064K fg shell top
131 0 3% S 1 0K 0K fg root dhd_dpc
6144 0 0% S 115 1682004K 115916K fg system system_server
132 0 0% S 1 0K 0K fg root dhd_rxf
1731 0 0% S 6 20288K 788K fg root /system/bin/mpdecision
217 0 0% S 6 18008K 356K fg shell /sbin/adbd
...
7779 2 0% S 19 1538748K 48896K bg u0_a71 com.sohu.inputmethod.sogou:classic
7963 0 0% S 18 1561916K 59568K fg u0_a58 org.mazhuang.boottimemeasure
...
各列含义: |列名|含义| |-|-| |PID|进程 ID| |PR|优先级| |CPU%|当前瞬间占用 CPU 百分比| |S|进程状态(R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)| |#THR|线程数| |VSS|Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)| |RSS|Resident Set Size 实际使用物理内存(包含共享库占用的内存)| |PCY|调度策略优先级,SP_BACKGROUND/SPFOREGROUND| |UID|进程所有者的用户 ID| |NAME|进程名|
top 命令还支持一些命令行参数,详细用法如下:
Usage: top [ -m max_procs ] [ -n iterations ] [ -d delay ] [ -s sort_column ] [ -t ] [ -h ]
-m num 最多显示多少个进程
-n num 刷新多少次后退出
-d num 刷新时间间隔(单位秒,默认值 5)
-s col 按某列排序(可用 col 值:cpu, vss, rss, thr)
-t 显示线程信息
-h 显示帮助文档
查看进程 UID 有两种方案:
adb shell dumpsys package <packagename> | grep userId=
如:
$ adb shell dumpsys package org.mazhuang.guanggoo | grep userId=
userId=10394
第二种是通过 ps 命令找到对应进程的 pid 之后 adb shell cat /proc/<pid>/status | grep Uid 如:
$ adb shell
gemini:/ $ ps | grep org.mazhuang.guanggoo
u0_a394 28635 770 1795812 78736 SyS_epoll_ 0000000000 S org.mazhuang.guanggoo
gemini:/ $ cat /proc/28635/status | grep Uid
Uid: 10394 10394 10394 10394
gemini:/ $
其它 如下是其它常用命令的简单描述,前文已经专门讲过的命令不再额外说明: |命令|功能| |-|-| |cat|显示文件内容| |cd|切换目录| |chmod|改变文件的存取模式/访问权限| |df|查看磁盘空间使用情况| |grep|过滤输出| |kill|杀死指定 PID 的进程| |ls|列举目录内容| |mount|挂载目录的查看和管理| |mv|移动或重命名文件| |ps|查看正在运行的进程| |rm|删除文件| |top|查看进程的资源占用情况|
常见问题 启动 adb server 失败 出错提示 error: protocol fault (couldn’t read status): No error 可能原因 adb server 进程想使用的 5037 端口被占用。 解决方案 找到占用 5037 端口的进程,然后终止它。以 Windows 下为例:
netstat -ano | findstr LISTENING
...
TCP 0.0.0.0:5037 0.0.0.0:0 LISTENING 1548
...
这里 1548 即为进程 ID,用命令结束该进程:
然后再启动 adb 就没问题了。
以下各个api的开发与测试均在车载设备上,可能在手机上不一定生效。
并且主动触发系统环境切换的方法,只有系统权限的app才可以调用。
触发入口调用 一般厂商,都会自己定义一个切换入口,像系统设置,或者桌面的通知页面。这里面去调用系统的api,来达到切换的目的。
主题切换 主题切换可以直接使用系统的UiModeManager即可。
private val uimodeManager =
appContext . getSystemService ( UI_MODE_SERVICE ) as UiModeManager
例如深浅主题切换调用:
// UiModeManager.java
/**
* Sets the system-wide night mode.
*
* @param mode the night mode to set
* @see #getNightMode()
* @see #setApplicationNightMode(int)
*/
public void setNightMode ( @NightMode int mode ) {
if ( mService != null ) {
try {
mService . setNightMode ( mode );
} catch ( RemoteException e ) {
throw e . rethrowFromSystemServer ();
}
}
}
我们自定义的触发按钮,按下时可以这样设置:
mMainView . findViewById < Button >( R . id . btn_night ). setOnClickListener {
uimodeManager . nightMode = UiModeManager . MODE_NIGHT_YES
}
mMainView . findViewById < Button >( R . id . btn_light ). setOnClickListener {
uimodeManager . nightMode = UiModeManager . MODE_NIGHT_NO
}
语言切换 通过反射调用ActivityManager的updatePersistentConfiguration方法,即可实现系统语言切换。
/**
* 切换语言
* @param language 语言
*/
fun changeLanguageSettings ( language : Locale ) {
try {
val activityManagerNative = Class . forName ( "android.app.ActivityManager" )
val am = activityManagerNative . getMethod ( "getService" ). invoke ( activityManagerNative )
val config = am ?. javaClass ?. getMethod ( "getConfiguration" )
?. invoke ( am ) as Configuration
config . setLocale ( language )
config . javaClass . getDeclaredField ( "userSetLocale" ). setBoolean ( config , true )
am . javaClass . getMethod ( "updatePersistentConfiguration" , config . javaClass )
. invoke ( am , config )
BackupManager . dataChanged ( "com.android.providers.settings" )
} catch ( e : Exception ) {
e . message ?. let { error ( it ) }
}
}
触发按钮调用:
mMainView . findViewById < Button >( R . id . btn_chinese ). setOnClickListener {
changeLanguageSettings ( Locale . SIMPLIFIED_CHINESE )
}
mMainView . findViewById < Button >( R . id . btn_english ). setOnClickListener {
changeLanguageSettings ( Locale . ENGLISH )
}
适配方app 在上面的app完成触发之后,系统会将环境切换到对应的深浅模式,或者对应的语言状态下,这时候其他的app就需要响应刷新自己的页面。
在开发过程中,可以按下面的几种方法来适配。
资源目录设置 首先不管是Activity应用还是浮窗应用,我们都需要在res资源目录下添加对应的语言和主题的资源目录。
语言目录
以英文为例,新建value-en目录,将翻译之后的strings.xml复制进去即可,字段的名称和中文目录是一致的。
主题目录
深色的主题资源放在带-night后缀的目录下。 例如图片等资源,放置于drawable-mdpi-night目录下,color字段放置于values-night目录下。和语言切换一样,图片,颜色等文件名称和浅色主题一致,切换的时候使用 R 类会自己索引。
逻辑代码 Activity型应用 切换主题和语言时,Activity都会销毁重建,按照触发顺序,大致为:
onConfigurationChanged:当系统配置发生变化时,例如屏幕方向改变或主题切换,Activity会首先调用onConfigurationChanged方法。你可以重写这个方法来处理配置变化,例如重新加载资源或更新UI。 onSaveInstanceState:在Activity可能被销毁之前,系统会调用onSaveInstanceState方法,允许你保存一些关键的状态信息,以便在Activity重新创建时恢复。 全部的流程如下 {thread:main(2) MainActivity:431 onPause}
{thread:main(2) MainActivity:495 onStop}
{thread:main(2) MainActivity:170 onSaveInstanceState}
{thread:main(2) MainActivity:500 onDestroy}
{thread:main(2) MainActivity:131 onCreate}
{thread:main(2) MainActivity:180 initData}
{thread:main(2) MainActivity:400 initView}
{thread:main(2) MainActivity:155 onStart}
{thread:main(2) MainActivity:175 onResume}
{thread:main(2) MainActivity:481 onWindowFocusChanged} onWindowFocusChanged hasFocus: true
重建之后,Activity会按照提前设置好的资源目录进行资源的获取,自动地刷新界面。切到浅色主题,就会拿取drawable-mdpi目录下的资源,深色主题则会拿取drawable-mdpi-night目录下的资源。
Service加浮窗型应用 在车机开发中,经常会设计一些临时性的悬浮窗app,例如天气,时间,快捷车控等功能。
这类app一般的架构为,开机之后,会启动一个Service,然后在Service中获取WindowManager,来进行悬浮窗的添加移除等管理操作。
// LanguageService.kt
private val mWmParams = WindowManager . LayoutParams (). apply {
//设置可以显示在状态栏上
flags = ( WindowManager . LayoutParams . FLAG_LAYOUT_IN_SCREEN
or WindowManager . LayoutParams . FLAG_LAYOUT_NO_LIMITS
or WindowManager . LayoutParams . FLAG_NOT_FOCUSABLE
or WindowManager . LayoutParams . FLAG_KEEP_SCREEN_ON
or WindowManager . LayoutParams . FLAG_WATCH_OUTSIDE_TOUCH
or WindowManager . LayoutParams . FLAG_NOT_TOUCH_MODAL )
type = WindowManager . LayoutParams . TYPE_APPLICATION_OVERLAY
//设置窗口长宽数据
width = WindowManager . LayoutParams . WRAP_CONTENT
height = WindowManager . LayoutParams . WRAP_CONTENT
gravity = Gravity . CENTER_HORIZONTAL or Gravity . TOP
x = 600
y = 0
format = PixelFormat . TRANSLUCENT
}
private val mWindowManager = appContext . getSystemService ( WINDOW_SERVICE ) as WindowManager
private lateinit var mMainView : View
fun showWindow (){
mMainView = LayoutInflater . from ( appContext ). inflate ( R . layout . layout_language_change , null , false )
mWindowManager . addView ( mMainView , mWmParams )
}
不像Activity都是自动化,高层级的悬浮窗app的生命周期比较复杂,需要我们自己去管理。
这时候系统不会自动去重走生命周期,刷新资源了,我们需要手动去切换主题和语言。
首先,需要在service中复写onConfigurationChanged方法。系统在语言和主题切换时,会调用这个方法。主题其他类型的配置切换,例如旋转屏幕等,也会走这个方法。
所以需要设置一个主题(语言)管理类,对比前后的状态,是否这个触发的类型是主题(语言)切换。
监听到了变化之后,有两种方案:
手动刷新置换资源 第一种,对于界面简单的浮窗界面,可以直接在onConfigurationChanged中,重新加载资源,然后重新设置布局。这种方法不用考虑窗口的状态,直接对每个View进行定点刷新,不容易出问题。
语言切换:
override fun onConfigurationChanged ( newConfig : Configuration ) {
super . onConfigurationChanged ( newConfig )
LogUtils . i ( TAG , "onConfigurationChanged" )
mMainView . findViewById < Button >( R . id . btn_chinese ). text = getString ( R . string . chinese )
mMainView . findViewById < Button >( R . id . btn_english ). text = getString ( R . string . english )
}
主题切换:
override fun onConfigurationChanged ( newConfig : Configuration ) {
super . onConfigurationChanged ( newConfig )
LogUtils . i ( TAG , "onConfigurationChanged" )
mMainView . setBackgroundColor (
ResourcesCompat . getColor ( this . resources , R . color . theme_test , null )
)
mMainView . findViewById < ImageView >( R . id . iv_close )
. setImageResource ( R . drawable . ic_close_dialog )
}
置空重建 第二种方案比较适合复杂的View,内部组件众多,挨个手动替换比较麻烦。
这时就可以模仿Activity的切换方式,直接移除掉之前的view,将其置空后,重新创建一个view,设置好子View的监听方法,然后重新添加到windowManager中。
override fun onConfigurationChanged ( newConfig : Configuration ) {
super . onConfigurationChanged ( newConfig )
LogUtils . i ( TAG , "onConfigurationChanged" )
mWindowManager . removeView ( mMainView )
mMainView = null
mMainView =
LayoutInflater . from ( appContext )
. inflate ( R . layout . layout_language_change , null , false )
initViews ()
mWindowManager . addView ( mMainView , mWmParams )
}
这种方法简单粗暴,但是必须要管控好窗口的状态和置空的时机,否则可能会导致内存泄漏。
而且这种方法也会导致窗口的闪烁,最好在系统切换时有一个过度的效果动画。
Pagination © 2024. All rights reserved. LICENSE | NOTICE | CHANGELOG
Powered by Hydejack v9.2.1