车载通信中的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 )
}
这种方法简单粗暴,但是必须要管控好窗口的状态和置空的时机,否则可能会导致内存泄漏。
而且这种方法也会导致窗口的闪烁,最好在系统切换时有一个过度的效果动画。
Settings.System数据库本来是用来存储用户偏好设置的机制,在车载系统开发时,由于车辆的设置信号都发给了底层的控制器ECU等去记忆了,这一块更多的是被用来做键值对存储和 IPC跨进程通信使用。
存储位置和权限 在Android中,用户的默认设置和偏好设置是存在数据库中,在Android 6.0 以前,settings的数据是存在settings.db中,系统可以通过sqlite来进行读写。 这样的话,所有的第三方应用都可以对settings.db进行操作,修改用户设置的数据。存储位置为,{system, secure, global} 对应的是目录 /data/data/com.android.providers.settings/databases/settings.db 的三个表。
所以在 在Android 6.0版本以后,SettingsProvider被重构,从安全和性能等方面考虑,把SettingsProvider中原本保存在settings.db中的数据,目前全部保存在XML文件中。一般位于 /data/system/users/0 目录下,该目录的settings_global.xml,settings_secure.xml和settings_system.xml三个xml文件就是SettingsProvider中的数据文件。
Settingsprovider中对数据也进行了分类,分别是Global、System、Secure、Ssaid四种类型,说明如下:
Global:所有的偏好设置对系统的所有用户公开,第三方APP有读没有写的权限; System:包含各种各样的用户偏好系统设置,第三方APP有读没有写的权限; Secure:安全性的用户偏好系统设置,第三方APP有读没有写的权限。 另外,还有一个不被熟知的Ssaid 表:此表包括所有应用的id;这样的话,只是可以从文件权限类型来做权限的管控,可以让第三方APP有读没有写的权限,或者直接不给读写权限等 车载使用很多,可以当作键值对存储使用,也可以多进程共享发通知使用。
第三方APP使用 读取数据 使用adb写入一个测试字段
montecarlo:/ # settings put global audio_test_result 234
使用Settings.Global.getInt来读取这个字段,可以看到显示。
CoroutineScope ( Dispatchers . IO ). launch {
val tesetData = Settings . Global . getInt ( this @MainActivity . contentResolver , "audio_test_result" )
Log . i ( TAG , "getGlobalData : $tesetData" )
}
监听 除了单次读取数据库的值,还可以通过 contentResolver.registerContentObserver 来添加持续的监听。
private val observer = object : ContentObserver ( null ) {
override fun onChange ( selfChange : Boolean ) {
val stringData =
Settings . System . getString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY
)
debugLog ( "onChange: data:$stringData" )
}
}
fun registerSystemSettingOberver () {
appContext . contentResolver . registerContentObserver (
Settings . System . getUriFor ( ACTION_MUTUAL_NOTIFY ),
true ,
observer
)
}
fun unRegisterSystemSettingOberver () {
appContext . contentResolver . unregisterContentObserver (
observer
)
}
尝试写入 CoroutineScope ( Dispatchers . IO ). launch {
Settings . Global . putInt ( this @MainActivity . contentResolver , "audio_test_result" , 2415 )
}
报错信息可以看到系统拒绝了第三方APP的写入操作:
Process: com.example.composedemo, PID: 28630
java.lang.SecurityException: Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS
at android.os.Parcel.createExceptionOrNull(Parcel.java:3011)
at android.os.Parcel.createException(Parcel.java:2995)
at android.os.Parcel.readException(Parcel.java:2978)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:190)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:142)
at android.content.ContentProviderProxy.call(ContentProviderNative.java:732)
at android.provider.Settings$NameValueCache.putStringForUser(Settings.java:3017)
at android.provider.Settings$Global.putStringForUser(Settings.java:16970)
at android.provider.Settings$Global.putString(Settings.java:16811)
at android.provider.Settings$Global.putInt(Settings.java:17041)
at com.example.composedemo.MainActivity$onCreate$1.invokeSuspend(MainActivity.kt:42)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@d6b0268, Dispatchers.IO]
Caused by: android.os.RemoteException: Remote stack trace:
at com.android.providers.settings.SettingsProvider.enforceWritePermission(SettingsProvider.java:2299)
at com.android.providers.settings.SettingsProvider.mutateGlobalSetting(SettingsProvider.java:1452)
at com.android.providers.settings.SettingsProvider.insertGlobalSetting(SettingsProvider.java:1406)
at com.android.providers.settings.SettingsProvider.call(SettingsProvider.java:450)
at android.content.ContentProvider.call(ContentProvider.java:2511)
系统APP的通信 系统app可以写入系统数据库的内容,很多场景下也被用来作为 IPC 多进程通信的方式。
有两种使用场景
数据传输 一方来修改需要传输的值,另一方监听变化读取获取。
修改数据库的值,以字符串数据为例,调用 Settings.System.putString()
修改方:
fun changeSystemSettingData () {
val tsStringData = "test string"
Settings . System . putString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY ,
tsStringData
)
}
接收方,这里和第三方APP没有区别:
private val observer = object : ContentObserver ( null ) {
override fun onChange ( selfChange : Boolean ) {
val stringData =
Settings . System . getString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY
)
debugLog ( "onChange: data:$stringData" )
}
}
fun registerSystemSettingOberver () {
appContext . contentResolver . registerContentObserver (
Settings . System . getUriFor ( ACTION_MUTUAL_NOTIFY ),
true ,
observer
)
}
fun unRegisterSystemSettingOberver () {
appContext . contentResolver . unregisterContentObserver (
observer
)
}
单次通知式 这种类似于广播,做触发式的逻辑,但是希望点对点建立通信协议,互相发通知。
一般是在一个单独的Service里面加入监听和发送通知的逻辑,同时还要屏蔽自己发出去的通知。
修改数据库的值,以字符串数据为例,调用 Settings.System.putString()
需要注意的是,这里的onChange回调是只有在变化时才会调用的。如果两次写入的是一样的值,接收方是收不到通知的。
所以这种连续发通知式的调用,再调用写入之后,还要调用notifyChange()方法。
fun changeSystemSettingData () {
val tsStringData = "same string"
Settings . System . putString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY ,
tsStringData
)
appContext . contentResolver . notifyChange (
Settings . System . getUriFor ( ACTION_MUTUAL_NOTIFY ), null
)
}
如果是两方需要互相发通知怎么办呢,自己发出去的修改,自己的observer也收到了。
这时候我们打开notifyChange的源码看一看:
/**
* Notify registered observers that a row was updated and attempt to sync
* changes to the network.
* <p>
* To observe events sent through this call, use
* {@link #registerContentObserver(Uri, boolean, ContentObserver)}.
* <p>
* Starting in {@link android.os.Build.VERSION_CODES#O}, all content
* notifications must be backed by a valid {@link ContentProvider}.
*
* @param uri The uri of the content that was changed.
* @param observer The observer that originated the change, may be
* <code>null</null>. The observer that originated the change
* will only receive the notification if it has requested to
* receive self-change notifications by implementing
* {@link ContentObserver#deliverSelfNotifications()} to return
* true.
*/
public void notifyChange ( @NonNull Uri uri , @Nullable ContentObserver observer ) {
notifyChange ( uri , observer , true /* sync to network */ );
}
在调用notifyChange的时候,将第二个参数observer传入自己这方的监听器,同时observer在继承的时候,需要复写deliverSelfNotifications()方法返回true,这样在自己发通知的时候,onChangee方法的回调selfChange标志位会被正确的置为true,可以用以筛选。
private val observer = object : ContentObserver ( null ) {
override fun onChange ( selfChange : Boolean ) {
val stringData =
Settings . System . getString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY
)
debugLog ( "onChange: data:$stringData selfchange:$selfChange" )
}
override fun deliverSelfNotifications () = true
}
ContentProvider 见另一篇文章: 【2022-9-12-四大组件之ContentProvider】
内部存储(Internal Storage) 内部存储用于存储应用私有的数据,其他应用无法访问。数据存储在应用的内部目录中,注意,此处的文件会随应用的卸载而删除。适合存储应用的缓存文件,配置文件等。
路径: /storage/emulated/0/Android/data/${packageName}/files
存储代码示例,以下是一个app管理功能中存储应用图标Drawable文件到内部缓存目录:
/**
* 存储图标png到app内部目录
*/
private fun saveDrawableToFile ( context : Context , drawable : Drawable , fileName : String ) {
val bitmap = drawable . toBitmap ()
val file = File ( context . getExternalFilesDir ( null ), "$fileName.png" )
val fos = FileOutputStream ( file )
infoLog ( "path:${file.absolutePath}" )
bitmap . compress ( Bitmap . CompressFormat . PNG , 100 , fos )
fos . flush ()
fos . close ()
}
外部存储(External Storage) 用于存储公共的、可共享的数据,其他应用可以访问。数据存储在设备的外部存储设备(如SD卡)上,即使应用被卸载,数据仍然保留。适合存储用户生成的文件,如照片、视频等。
路径一般为sdcard内。此处的存储操作想要顺利完成,一般需要手动申请运行时权限。
存储代码示例,往sdcard的Pictures目录下写一张图片:
/**
* 保存一个bitmap到本地sdcard的Pictures目录
*/
fun saveImageToGallery (
context : Context ,
bitmap : Bitmap ,
filename : String = "FilmSimulation.jpg" ,
) {
val values = ContentValues (). apply {
put ( MediaStore . MediaColumns . DISPLAY_NAME , filename )
put ( MediaStore . Images . Media . MIME_TYPE , "image/jpeg" ) // 文件类型
put ( MediaStore . MediaColumns . RELATIVE_PATH , Environment . DIRECTORY_PICTURES )
}
runCatching {
context . contentResolver . insert ( MediaStore . Images . Media . EXTERNAL_CONTENT_URI , values )
?. let {
context . contentResolver . openOutputStream ( it ) ?. apply {
bitmap . compress ( Bitmap . CompressFormat . JPEG , 100 , this )
flush ()
close ()
}
context . contentResolver . notifyChange ( it , null )
}
}. onFailure {
Log . e ( TAG , "save image error:${it.message}" )
}
}
数据库存储 数据库存储用于存储结构化的数据,如用户信息、聊天记录等。数据库存储可以支持复杂的查询和关联操作。
数据库存储的路径一般在
/storage/emulated/0/Android/data/${packageName}/databases
像下面的用以举例的 Demo,数据库路径如下:
emu64xa:/data/data/com.example.roomdemo/databases # ls
camera_database camera_database-shm camera_database-wal
Room数据库介绍 SQLite数据库
Android平台选取了SqLlite数据库作为结构化数据库的存储方案。
用于存储结构化的数据,如用户信息、聊天记录等。轻量级的关系型数据库,支持SQL查询,适合存储大量的结构化数据。应用可极大地受益于在本地保留这些数据。最常见的使用场景是缓存相关的数据,这样一来,当设备无法访问网络时,用户仍然可以在离线状态下浏览该内容。
长期以来,SQLite 数据库繁杂的使用体验,也让开发者们感到困惑。
Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:
提供针对 SQL 查询的编译时验证。 提供方便注解,可最大限度减少重复和容易出错的样板代码。 简化了数据库迁移路径。 使用Room数据库 首先添加依赖库,app级的 build.gradle.kts 依赖添加:
val room_version = "2.6.1"
implementation ( "androidx.room:room-runtime:$room_version" )
/**
* KSP (Kotlin Symbol Processing)是以 Kotlin 优先的 kapt 替代方案。
* KSP 可直接分析 Kotlin 代码,使得速度提高多达 2 倍。
* 此外,它还可以更好地了解 Kotlin 的语言结构。
*/
ksp ( "androidx.room:room-compiler:$room_version" )
Kapt背景知识: Kapt可以将 Java 注解处理器与 Kotlin 代码搭配使用,即使这些处理器没有特定的 Kotlin 支持也是如此。方法是从 Kotlin 文件生成 Java 桩,然后处理器就可以读取这些桩。生成桩是一项成本高昂的操作,并且对构建速度有很大影响。
而KSP 可以说是Kapt的升级替代方案,它是一个 Kotlin 编译器插件,它可以在编译时读取和分析 Kotlin 代码,然后生成 Java 代码。KSP 可以直接分析 Kotlin 代码,而不需要通过 Java 桩。这使得 KSP 可以更好地了解 Kotlin 的语言结构。
如果没有提前添加ksp插件,上面的依赖引入应该是报红的。
添加KSP插件
项目顶级build.gradle.kts文件: plugins {
id ( "com.google.devtools.ksp" ) version "2.0.21-1.0.27" apply false
}
app级的build.gradle.kts文件: plugins {
id ( "com.google.devtools.ksp" )
}
Room数据库组件 Room 包含三个主要组件:
数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点。 数据实体,用于表示应用的数据库中的表。 数据访问对象 (DAO),为您的应用提供在数据库中查询、更新、插入和删除数据的方法。 也就是说,至少需要定义三个类,才可以使用Room数据库来存储数据。
数据实体 数据实体是数据库中表的映射。数据实体是一个类,需要添加 @Entity 注解。
@Entity
data class Camera (
@PrimaryKey
val cameraId : Int ,
@ColumnInfo ( name = "brand_name" ) val brandName : String ,
@ColumnInfo ( name = "camera_model" ) val cameraModel : String ,
)
数据访问对象 (DAO) 数据访问对象 (DAO) 是用于在数据库中执行查询和更新的接口。数据访问对象 (DAO) 是一个接口,需要添加 @Dao 注解。
@Dao
interface CameraDao {
@Query ( "SELECT * FROM camera" )
fun getAll (): List < Camera >
@Query ( "SELECT * FROM camera WHERE cameraId IN (:cameraIds)" )
fun loadAllByIds ( cameraIds : IntArray ): List < Camera >
@Query ( "SELECT * FROM camera WHERE brand_name LIKE :first AND " + "camera_model LIKE :last LIMIT 1" )
fun findByName ( first : String , last : String ): Camera
@Insert
fun insertAll ( vararg cameras : Camera )
@Delete
fun delete ( camera : Camera )
}
数据库类 数据库类是应用的入口点,用于访问应用的数据库。
数据库类为应用提供与该数据库关联的 DAO 的实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。
数据库类是一个抽象类,需要继承它,并且需要添加 @Database 注解。
@Database 注解有两个参数:entities 和 version。entities 是一个数组,用于指定数据库中包含的实体类。version 是一个整数,用于指定数据库的版本号。
@Database ( entities = [ Camera :: class ], version = 1 )
abstract class CameraDatabase : RoomDatabase () {
abstract fun cameraDao (): CameraDao
}
数据库使用 数据库类是应用的入口点,用于访问应用的数据库。
使用 Room.databaseBuilder 方法来创建数据库实例。然后,可以使用 CameraDatabase 中的抽象方法获取 DAO 的实例,转而可以使用 DAO 实例中的方法与数据库进行交互。
下面是Demo测试代码,注意要在 IO 线程中进行数据库的创建,读写等操作:
CoroutineScope ( Dispatchers . IO ). launch {
val db = Room . databaseBuilder ( appContext , CameraDatabase :: class . java , "camera_database" ). build ()
val camereDao = db . cameraDao ()
camereDao . insertAll (
Camera ( 1000 , "Canon" , "EOS R6 II" ),
Camera ( 1001 , "Sony" , "A9 II" ),
Camera ( 1002 , "LUMIX" , "S5M2K" )
)
delay ( 3000L )
Log . i ( TAG , "room database test: ${camereDao.getAll()}" )
}
四种流行的键值对存储 前三种方案对比结论,来自扔物线朱凯大佬的测试数据。
SharedPreferences 如果您有想要保存的相对较小键值对集合,则可以使用 SharedPreferences API。SharedPreferences 对象指向包含键值对的文件,并提供读写这些键值对的简单方法。每个 SharedPreferences 文件均由框架进行管理,可以是私有文件,也可以是共享文件。
键值对的存储在移动开发里非常常见。比如深色模式的开关、软件语言、字体大小,这些用户偏好设置,很适合用键值对来存。
SharedPreferences 使用起来很简单,但是有性能问题,容易卡顿,甚至有时候会出现 ANR。
MMKV 腾讯开源了一个叫做 MMKV 的项目。它和 SP 一样,都是做键值对存储的,可是它的性能比 SP 强很多。
MMKV的开发背景:
微信在遇到一些无法处理的字符的时候,会出现崩溃的问题,而微信为了及时地找出导致崩溃的字符或者字符串,所有的对话内容在显示之前,先保存到磁盘再显示。而且防止崩溃之后数据还没存好,必须要在主线程去完成这个写操作,耗时就绝对无法避免。一帧的时间也就 16 毫秒,在16 毫秒里来个写磁盘的操作,用户很可能就会感受到一次卡顿。如果用户点开了一个活跃的群,这个群里有几百条没看过的消息。而如果把这几条消息都记录下来,是不是每条消息的记录都会涉及一次写磁盘的操作?这几次写磁盘行为,是发生在同一帧里的,所以在这一帧里因为记录文字而导致的主线程耗时,也会相比起刚才的例子翻上好几倍,卡顿时间就同样也会翻上好几倍。
最终微信找到了解决方案。使用了一种叫做内存映射(mmap())的底层方法。
它可以让系统为你指定的文件开辟一块专用的内存,这块内存和文件之间是自动映射、自动同步的关系,你对文件的改动会自动写到这块内存里,对这块内存的改动也会自动写到文件里。
有了这一层内存作为中间人,我们就可以用「写入内存」的方式来实现「写入磁盘」的目标了。它在程序崩溃的时候,并不会随着进程一起被销毁掉,而是会继续有条不紊地把它里面还没同步完的内容同步到它所映射的文件里面去。
MMKV缺点:
MMKV 优势:
SP和DataStore对比 DataStore 被创造出来的目标就是替代 SharedPreferences,而它解决的 SharedPreferences 最大的问题有两点:一是性能问题,二是回调问题。
先说性能问题:SharedPreferences 虽然可以用异步的方式来保存更改,以此来避免 I/O 操作所导致的主线程的耗时。
但在 Activity 启动和关闭的时候,Activity 会等待这些异步提交完成保存之后再继续,这就相当于把异步操作转换成同步操作了,从而会导致卡顿甚至 ANR(程序未响应)。
这是为了保证数据的一致性而不得不做的决定,但它也确实成为了 SharedPreferences 的一个弱点。
但是,SharedPreferences 所导致的卡顿和 ANR,是非常低概率的事件。
读取文件卡顿 其实除了写数据时的卡顿,SharedPreferences 在读取数据的时候也会卡顿。
虽然它的文件加载过程是在后台进行的,但如果代码在它加载完成之前就去尝试读取键值对,线程就会被卡住,直到文件加载完成,而如果这个读取的过程发生在主线程,就会造成界面卡顿,并且数据文件越大就会越卡。
这种卡顿,不是 SharedPreferences 独有的,MMKV 也是存在的,因为它初始化的过程同样也是从磁盘里读取文件,而且是一股脑把整个文件读完,所以耗时并不会比 SharedPreferences 少。
而 DataStore,就没有这种问题。DataStore 不管是读文件还是写文件,都是用的协程在后台进行读写,所有的 I/O 操作都是在后台线程发生的,所以不论读还是写,都不会卡主线程。
DataStore回调更方便 DataStore 解决的 SharedPreferences 的另一个问题就是回调。
SharedPreferences 如果使用同步方式来保存更改commit(),会导致主线程的耗时;
但如果使用异步的方式,给它加回调又很不方便,也就是如果你想做一些「等这个异步提交完成之后再怎么怎么样」的工作,会很麻烦。
而 DataStore 由于是用协程来做的,线程的切换是非常简单的,你就把「保存完成之后做什么」直接写在保存代码的下方就可以了,很直观、很简单。
对比来说,MMKV 虽然没有使用协程,但是它太快了,所以大多数时候并不需要切线程也不会卡顿。
三种方式的总结 区别大概就是这么些区别了,大致总结一下就是:
如果你有多进程支持的需求,可以选择MMKV,也可以选择DataStore(1.1.0版本新增);如果你有高频写入的需求,你也应该优先考虑 MMKV。但如果你使用 MMKV,一定要知道它是可能丢失数据的,不过概率很低就是了,所以你要在权衡之后做好决定:是自行实现数据的备份和恢复方案,还是直接接受丢数据的事实,在每次丢失数据之后帮用户把相应的数据进行初始化。
DataStore 在任何时候都不会卡顿,而 MMKV 在写大字符串和初次加载文件的时候是可能会卡顿的,而且初次加载文件的卡顿不是概率性的,只要文件大到了引起卡顿的程度,就是 100% 的卡顿。
三种方案提取的工具类 在我之前研究AOSP redfin平台的项目的时候,在CommonHelper库里面对这三种存储方式都做了一个很简单的工具类。
SharedPreferences /**
* SharedPreference存储工具类
* 不会丢数据
* 但是加回调不方便
*/
object SPHelper {
private lateinit var share : SharedPreferences
private lateinit var editor : SharedPreferences . Editor
private const val SHARED_NAME = "SPHelper"
fun init ( context : Context ) {
share = context . getSharedPreferences ( SHARED_NAME , Context . MODE_PRIVATE );
editor = share . edit ();
}
// 采用同步保存,获取保存成功与失败的result
fun putString ( key : String , value : String ?): Boolean {
infoLog ( "putString key: $key, value: $value" )
editor . putString ( key , value )
return editor . commit ()
}
fun getString ( key : String ): String ? {
val value = share . getString ( key , null )
infoLog ( "getString key: $key, value: $value" )
return value
}
fun getString ( key : String , defaultValue : String ): String {
val value = share . getString ( key , null )
infoLog ( "getString key: $key, value: $value, defaultValue: $defaultValue" )
return value ?: defaultValue
}
fun putLong ( key : String ?, value : Long ): Boolean {
infoLog ( "putLong key: $key, value: $value" )
editor . putLong ( key , value )
return editor . commit ()
}
fun putFloat ( key : String ?, value : Float ): Boolean {
infoLog ( "putFloat key: $key, value: $value" )
editor . putFloat ( key , value )
return editor . commit ()
}
fun putInt ( key : String ?, value : Int ): Boolean {
infoLog ( "putInt key: $key, value: $value" )
editor . putInt ( key , value )
return editor . commit ()
}
fun putBoolean ( key : String ?, value : Boolean ): Boolean {
infoLog ( "putBoolean key: $key, value: $value" )
editor . putBoolean ( key , value )
return editor . commit ()
}
fun getLong ( key : String ?): Long {
val value = share . getLong ( key , - 1 )
infoLog ( "getLong key: $key, value: $value" )
return value
}
fun getInt ( key : String ?, defaultValue : Int ): Int {
val value = share . getInt ( key , defaultValue )
infoLog ( "getInt key: $key, value: $value" )
return value
}
fun getFloat ( key : String ?, defaultValue : Float ): Float {
val value = share . getFloat ( key , defaultValue )
infoLog ( "getFloat key: $key, value: $value" )
return value
}
fun getBoolean ( key : String ?, defaultValue : Boolean ): Boolean {
val value = share . getBoolean ( key , defaultValue )
infoLog ( "getBoolean key: $key, value: $value" )
return value
}
fun removeSharedPreferenceByKey ( key : String ?): Boolean {
infoLog ( "removeSharedPreferenceByKey key: $key" )
editor . remove ( key )
return editor . commit ()
}
}
MMKV /**
* 最适合同步写入小数据
* 支持多进程,高频写入性能好
* 但有可能丢数据
*/
object MMKVHelper {
private lateinit var mmkv : MMKV
fun init ( context : Context , databaseId : String , isMultiProcess : Boolean ) {
val rootDir = MMKV . initialize ( context )
infoLog ( "MMKV rootDir: $rootDir" )
mmkv =
if ( isMultiProcess ) MMKV . mmkvWithID ( databaseId , MMKV . MULTI_PROCESS_MODE )
else MMKV . mmkvWithID ( databaseId )
}
fun putString ( key : String , value : String ?) {
infoLog ( "putString key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun getString ( key : String ): String ? {
val value = mmkv . decodeString ( key )
infoLog ( "getString key: $key, value: $value" )
return value
}
fun getString ( key : String , defaultValue : String ): String {
val value = mmkv . decodeString ( key )
infoLog ( "getString key: $key, value: $value, defaultValue: $defaultValue" )
return value ?: defaultValue
}
fun putLong ( key : String ?, value : Long ) {
infoLog ( "putLong key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun putFloat ( key : String ?, value : Float ) {
infoLog ( "putFloat key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun putInt ( key : String ?, value : Int ) {
infoLog ( "putInt key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun putBoolean ( key : String ?, value : Boolean ) {
infoLog ( "putBoolean key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun getLong ( key : String ?): Long {
val value = mmkv . decodeLong ( key , - 1 )
infoLog ( "getLong key: $key, value: $value" )
return value
}
fun getInt ( key : String ?, defaultValue : Int ): Int {
val value = mmkv . decodeInt ( key , defaultValue )
infoLog ( "getInt key: $key, value: $value" )
return value
}
fun getFloat ( key : String ?, defaultValue : Float ): Float {
val value = mmkv . decodeFloat ( key , defaultValue )
infoLog ( "getFloat key: $key, value: $value" )
return value
}
fun getBoolean ( key : String ?, defaultValue : Boolean ): Boolean {
val value = mmkv . decodeBool ( key , defaultValue )
infoLog ( "getBoolean key: $key, value: $value" )
return value
}
}
DataStore /**
* 谷歌推荐的最新存储方式
* 协程实现,可以方便地获取存储的结果回调
*/
object DataStoreHelper {
// 定义一个 DataStore 实例
val Context . dataStore : DataStore < Preferences > by preferencesDataStore ( name = "data_store_settings" )
private lateinit var outDataStore : DataStore < Preferences >
/**
* 初始化,使get和set不受Context域限制
*/
fun init ( context : Context ) {
outDataStore = context . dataStore
}
// 定义一个 suspend 函数,用于从 DataStore 中读取数据
suspend fun < T > getData ( key : Preferences . Key < T >, defaultValue : T ): T {
return ( outDataStore . data . first ()[ key ] ?: defaultValue )
}
// 定义一个 suspend 函数,用于将数据保存到 DataStore 中
suspend fun < T > saveData ( key : Preferences . Key < T >, value : T ) {
outDataStore . edit { preferences ->
preferences [ key ] = value
}
}
}
使用:
CoroutineScope ( Dispatchers . IO ). launch {
val INT_PREF_KEY = intPreferencesKey ( "IntKey" )
val FLOAT_PERF_KEY = floatPreferencesKey ( "FloatKey" )
val STRING_PREF_KEY = stringPreferencesKey ( "SteringKey" )
DataStoreHelper . saveData ( INT_PREF_KEY , 45 )
DataStoreHelper . saveData ( FLOAT_PERF_KEY , 45.0f )
DataStoreHelper . saveData ( STRING_PREF_KEY , "45" )
delay ( 1000L )
// 拿取刚刚存的值
Log . i ( TAG , "intData: ${DataStoreHelper.getData(INT_PREF_KEY, -1)}" )
Log . i ( TAG , "floatData: ${DataStoreHelper.getData(FLOAT_PERF_KEY, -1f)}" )
Log . i ( TAG , "stringData: ${DataStoreHelper.getData(STRING_PREF_KEY, " default ")}" )
}
Setting.System系统数据库 这里在车载上使用的更多,单独写了一篇总结:
【Android系统数据库通信使用方式】
Fragment 是 Android UI 开发中一个非常重要的组件,用于构建模块化、可复用且灵活的用户界面。
Fragment 可以被视为一个Activity 的一部分或行为 。它拥有自己的生命周期、布局和输入事件,但它必须托管在一个 Activity 中。一个 Activity 可以包含一个或多个 Fragment,也可以在不同的 Activity 中复用同一个 Fragment。
Fragment 的主要作用 模块化 UI: 可以将一个复杂的用户界面分解成独立的、可管理的模块。UI 可复用性: 可以在不同的 Activity 或同一 Activity 的不同配置(如横竖屏)中复用 Fragment。适应不同屏幕尺寸: 尤其在平板电脑等大屏幕设备上,可以同时显示多个 Fragment,例如列表-详情布局(List-Detail Flow)。简化 Activity 代码: 将 UI 逻辑和行为从 Activity 中分离出来,使 Activity 变得更轻量和专注于协调。支持回退栈: 可以像 Activity 一样管理 Fragment 的回退栈,实现前进和后退导航。生命周期 Fragment 的生命周期与它所依附的 Activity 的生命周期紧密相关 。理解这些回调方法对于正确管理 Fragment 的状态至关重要。
以下是 Fragment 生命周期中几个关键的方法及其大致顺序:
onAttach() : 当 Fragment 与 Activity 关联时调用。此时可以获取到 Context 对象。onCreate() : Fragment 被创建时调用。在这里进行非 UI 的初始化,如变量设置、数据加载等。onCreateView() : 创建 Fragment 的用户界面(View)。在这里膨胀(inflate)布局并返回根视图。onViewCreated() : onCreateView() 返回后调用。在这里可以初始化 View 组件,设置监听器等。onActivityCreated() : 当宿主 Activity 的 onCreate() 方法完成时调用。可以在这里执行依赖于 Activity 已创建的代码。onStart() : Fragment 可见时调用。onResume() : Fragment 获得焦点并可与用户交互时调用。onPause() : Fragment 失去焦点,但仍然部分可见时调用(例如,另一个 Fragment 覆盖了它)。onStop() : Fragment 不再可见时调用。onDestroyView() : Fragment 的视图被移除时调用。在这里释放与 View 相关的资源。onDestroy() : Fragment 实例被销毁时调用。在这里释放所有非 View 相关的资源。onDetach() : Fragment 与 Activity 解除关联时调用。使用流程 1. 创建 Fragment 创建一个继承自 androidx.fragment.app.Fragment 的 Java/Kotlin 类,并通常重写 onCreateView() 方法来提供其布局:
class MyFragment : Fragment () {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
// Fragment 初始化逻辑
}
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ?,
savedInstanceState : Bundle ?
): View ? {
// 膨胀 Fragment 的布局
return inflater . inflate ( R . layout . fragment_my , container , false )
}
override fun onViewCreated ( view : View , savedInstanceState : Bundle ?) {
super . onViewCreated ( view , savedInstanceState )
// 初始化 View 组件
// val myTextView = view.findViewById<TextView>(R.id.myTextView)
// myTextView.text = "Hello from Fragment!"
}
}
对应的 fragment_my.xml 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "match_parent" >
<TextView
android:id= "@+id/myTextView"
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
android:text= "My Fragment Content"
android:textSize= "24sp"
android:layout_gravity= "center" />
</FrameLayout>
2. 将 Fragment 添加到 Activity 有两种主要方式将 Fragment 添加到 Activity 中:
a. 在布局 XML 中声明 你可以在 Activity 的布局 XML 文件中直接声明一个 Fragment。这是 静态添加 Fragment 的方式。
<LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:orientation= "vertical" >
<fragment
android:id= "@+id/my_static_fragment"
android:name= "com.example.yourapp.MyFragment" // 完整的 Fragment 类名
android:layout_width= "match_parent"
android:layout_height= "match_parent" />
</LinearLayout>
这种方式下,Fragment 的生命周期会与 Activity 的生命周期紧密耦合,并且在 Activity 创建时就被实例化。
b. 运行时动态添加(推荐) 通过 FragmentManager 和 FragmentTransaction 在 Activity 运行时动态添加、移除、替换或显示/隐藏 Fragment。这是最常用的方式,因为它提供了更大的灵活性。
class MainActivity : AppCompatActivity () {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
// 检查 Fragment 是否已经添加,避免重复添加(例如在配置变化后)
if ( savedInstanceState == null ) {
val fragmentManager : FragmentManager = supportFragmentManager
val fragmentTransaction : FragmentTransaction = fragmentManager . beginTransaction ()
val myFragment = MyFragment ()
// 将 Fragment 添加到一个容器视图中 (例如一个 FrameLayout)
fragmentTransaction . add ( R . id . fragment_container , myFragment )
// fragmentTransaction.addToBackStack(null) // 可选:添加到回退栈
fragmentTransaction . commit ()
}
}
}
初始化添加时最好是先行检查一下 savedInstanceState 是否为 null,避免重复添加 Fragment。如果是系统的配置变更,如语言和主题,我们知道Activity会自动重建,而FagmentManager 会在 Activity 重建时,自动恢复那些在 Activity 被销毁前已经存在的 Fragment 实例。如果此时又调用了 fragmentTransaction.add() 方法添加 Fragment,就会导致重复添加,引发异常,界面内容可能会重叠显示多个fragment。
对应的 activity_main.xml 布局需要一个用于容纳 Fragment 的容器:
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:id= "@+id/fragment_container"
android:layout_width= "match_parent"
android:layout_height= "match_parent" />
Fragment 的通信 由于 Fragment 之间是独立的,它们之间以及与宿主 Activity 之间需要明确的通信机制。
Fragment 到 Activity:
推荐方式: 定义一个接口,让 Activity 实现该接口。Fragment 通过 onAttach() 获取 Activity 实例并将其转换为接口类型,然后调用接口方法。// Fragment
class MyFragment : Fragment () {
interface OnMessageListener {
fun onMessageFromFragment ( message : String )
}
private var listener : OnMessageListener ? = null
override fun onAttach ( context : Context ) {
super . onAttach ( context )
if ( context is OnMessageListener ) {
listener = context
} else {
throw RuntimeException ( "$context must implement OnMessageListener" )
}
}
// ... 某个事件触发时
fun sendMessage () {
listener ?. onMessageFromFragment ( "Hello from Fragment!" )
}
override fun onDetach () {
super . onDetach ()
listener = null
}
}
// Activity
class MainActivity : AppCompatActivity (), MyFragment . OnMessageListener {
override fun onMessageFromFragment ( message : String ) {
Log . d ( "MainActivity" , "Received message: $message" )
}
// ...
}
ViewModel (推荐,尤其是 Fragment 间通信): 使用共享的 ViewModel 可以非常方便地在 Fragment 和 Activity 之间共享数据和通信,尤其是在导航组件的场景下。Activity 到 Fragment:
调用 Fragment 公开方法: Activity 可以获取 Fragment 实例并直接调用其公共方法。// Activity
fun sendMessageToFragment ( message : String ) {
val myFragment = supportFragmentManager . findFragmentById ( R . id . fragment_container ) as ? MyFragment
myFragment ?. updateText ( message )
}
// Fragment
class MyFragment : Fragment () {
fun updateText ( message : String ) {
// 更新 TextView
}
}
通过 Bundle 传递参数: 在创建 Fragment 实例时,通过 setArguments(Bundle) 方法传递参数。// Activity
val args = Bundle (). apply {
putString ( "key_message" , "Data from Activity" )
}
val myFragment = MyFragment (). apply {
arguments = args
}
// Fragment
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
val message = arguments ?. getString ( "key_message" )
// 使用 message
}
Fragment 到 Fragment:
通过宿主 Activity 中转(旧版,不推荐): 一个 Fragment 通知 Activity,Activity 再通知另一个 Fragment。共享 ViewModel (推荐): 多个 Fragment 可以观察同一个 ViewModel 中的 LiveData,实现数据共享和通信。Parent-to-Child FragmentManager (如果存在嵌套 Fragment): 可以通过 getParentFragmentManager() 或 getChildFragmentManager() 获取对应的 FragmentManager。Navigation Component (推荐): 使用 Android Navigation 组件是处理 Fragment 之间导航和参数传递的现代化且强大的方式。FragmentTransaction 和回退栈 当使用 FragmentManager 动态管理 Fragment 时,FragmentTransaction 是执行操作(如添加、移除、替换)的批处理API。
add(containerId, fragment): 将一个 Fragment 添加到容器中。remove(fragment): 移除一个 Fragment。replace(containerId, fragment): 移除容器中现有 Fragment,然后添加新的 Fragment。hide(fragment) / show(fragment): 隐藏或显示一个 Fragment,但不会销毁其 View。addToBackStack(name): 将当前 FragmentTransaction 添加到 Activity 的回退栈中。当用户按返回键时,会依次弹出栈中的 Fragment 事务,回退到之前的 Fragment 状态。commit(): 提交事务。这是异步操作。commitNow(): 提交事务。这是同步操作,但可能会阻塞 UI 线程,除非确定操作很快。通常不推荐。commitAllowingStateLoss(): 提交事务,即使 Activity 状态已保存,允许状态丢失。一般不推荐,除非你清楚这样做的后果。最佳实践与注意事项 避免 Fragment 嵌套过多: 复杂的 Fragment 嵌套会导致生命周期管理变得困难,并可能引入性能问题。使用 setArguments() 传递参数: 避免在 Fragment 构造函数中传递参数,因为系统可能会在屏幕旋转等情况下重新创建 Fragment 而不调用自定义构造函数。Fragment 应该尽可能独立和可复用: 它们不应该直接依赖于特定的 Activity 类型,而是通过接口或 ViewModel 进行通信。处理配置变更: Fragment 在 Activity 重建时也会重建。确保在 onSaveInstanceState() 和 onCreate()/onCreateView() 中正确保存和恢复 Fragment 的状态。内存泄漏: 注意在 onDestroyView() 或 onDestroy() 中释放不再需要的引用(尤其是对 View 的引用),以避免内存泄漏。例如,清理在 onCreateView() 中创建的监听器。getChildFragmentManager() vs getFragmentManager() / getParentFragmentManager():getParentFragmentManager() (原 getFragmentManager()):用于获取管理当前 Fragment 的 FragmentManager。getChildFragmentManager():用于获取管理当前 Fragment 内部嵌套 Fragment 的 FragmentManager。在使用 FragmentContainerView 或 supportFragmentManager.beginTransaction() 动态添加 Fragment 时,请确保使用正确的 FragmentManager。 Navigation Component: 对于复杂的导航和 Fragment 间的通信,强烈推荐使用 Android Jetpack 的 Navigation Component。它简化了 Fragment 的管理、深层链接和安全参数传递。View Binding 或 Data Binding: 在 Fragment 中使用 View Binding 或 Data Binding 可以更安全、高效地访问 View 组件,避免 findViewById 带来的空指针异常。常见用例 标签页(Tabbed Layouts): 每个标签页内容可以是一个 Fragment。滑动视图(Swipe Views / ViewPager2): ViewPager2 经常与 FragmentStateAdapter 结合使用,每个页面都是一个 Fragment。大屏幕设备布局: 例如,在平板上,一个 Fragment 显示列表,另一个 Fragment 显示详情。底部导航栏(Bottom Navigation): 每个导航项对应一个 Fragment。向导流(Wizard Flows): 多个 Fragment 按顺序引导用户完成任务。作为一名 Android 开发者,Activity 绝对是你最常用、也是最重要的组件。它是用户界面的单一入口点,承载着应用与用户交互的各种操作。你可以把它想象成应用中的一个“屏幕”或“页面”。
Activity 提供一个绑定好的窗口,你可以在其中使用各种View和ViewGroup来绘制 UI 界面(如按钮、文本框、图片等),供用户进行交互。 Activity 拥有一套定义好的生命周期回调方法,应用开发者根据这些回调来配置特定的任务,比如在创建的时候配置View的交互行为,数据初始化,销毁时释放资源。 每个 Activity 实例都与一个任务(task)相关联。当用户启动应用时,系统会为它创建一个任务,并在这个任务中管理 Activity 的堆栈。 Activity 也可以启动其他 Activity(包括自己应用内或第三方应用的 Activity),并通过 Intent 和 Bundle 传递数据。 生命周期 理解 Activity 的生命周期是 Android 开发的基石。当你用户在应用中导航、接电话、切换应用等操作时,Activity 的状态会发生变化,系统会调用相应的回调方法。
以下是 Activity 生命周期中的几个核心方法:
onCreate() :何时调用: Activity 首次创建时调用。作用: 进行 Activity 的初始化工作,如设置布局(setContentView())、初始化视图组件、绑定数据、恢复 savedInstanceState 中的数据等。这是你放置大部分一次性设置代码的地方。onStart() :何时调用: Activity 可见时调用,无论是因为首次创建还是从后台回到前台。作用: Activity 即将对用户可见,但尚未获得用户焦点。onResume() :何时调用: Activity 获得用户焦点并可与用户交互时调用。作用: 在这里启动动画、访问设备相机或传感器等独占性资源。任何需要 Activity 处于前台才能进行的轻量级操作都应放在这里。onPause() :何时调用: Activity 失去焦点时调用,例如用户点击返回键,或启动了另一个 Activity 但当前 Activity 仍然部分可见(如弹出一个对话框),或屏幕关闭。作用: 在这里暂停动画、释放独占性资源(如相机预览),并保存任何需要持久化的小量数据(但不要在这里执行耗时操作,因为下一个 Activity 必须等待当前 Activity 的 onPause() 执行完毕才能 onResume())。onStop() :何时调用: Activity 不再可见时调用,例如用户切换到另一个应用、按下 Home 键、或启动了一个完全覆盖当前 Activity 的新 Activity。作用: 释放那些在 Activity 不可见时不再需要的资源。重量级 CPU 操作,例如向数据库写入数据,应该在这里完成。onDestroy() :何时调用: Activity 被系统销毁时调用。这可能是以下原因之一:用户通过按下返回键完全退出 Activity。 系统为了回收资源而销毁 Activity(例如,当内存不足时)。 配置变更(如屏幕旋转、主题切换)导致 Activity 重新创建。 作用: 释放所有在 onCreate() 中创建的资源,如解绑广播接收器、关闭数据库游标、停止后台线程等。可以用下面这个图来概括:
Activity 状态和数据保存 当 Activity 被销毁后又重建时(例如屏幕旋转,主题切换),你可能需要恢复之前的用户界面状态或数据。Activity提供了以下两个方法,分别用于在Activity被销毁前和重建后保存和恢复数据:
onSaveInstanceState(Bundle outState) :Activity 即将被销毁,但未来可能会被重新创建时调用(例如屏幕旋转、内存不足导致系统回收)。 你可以将少量瞬态数据(如 UI 状态、滚动位置等)保存到 Bundle 中。这个 Bundle 会在 Activity 重新创建时通过 onCreate() 方法传递回来。 onRestoreInstanceState(Bundle savedInstanceState) :在 onStart() 之后,并且仅当 Activity 之前因系统原因被销毁并重新创建时调用。 在这里恢复 onSaveInstanceState() 中保存的数据。通常,也可以在 onCreate() 中通过 savedInstanceState 参数来恢复这些数据。 注意: 对于大量数据或需要长期保存的数据,不应依赖 onSaveInstanceState()。而应该使用 Room 数据库、SharedPreferences(建议迁移到DataStore)或 ViewModel 来持久化保存数据。
启动 Activity 和数据传递 你可以使用 Intent 来启动其他 Activity。
// 启动一个显式 Activity (明确指定要启动的 Activity 类)
val intent = Intent ( this , SecondActivity :: class . java )
startActivity ( intent )
// 启动 Activity 并传递数据
val dataIntent = Intent ( this , DetailActivity :: class . java ). apply {
putExtra ( "item_id" , 123 )
putExtra ( "item_name" , "Awesome Product" )
}
startActivity ( dataIntent )
// 在 DetailActivity 中获取 Intent 的数据
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// val itemId = intent.getIntExtra("item_id", -1)
// val itemName = intent.getStringExtra("item_name")
// }
// 4. 启动 Activity 并获取返回结果 (旧方法,现在推荐 Activity Result API)
// override fun onCreate(...) {
// val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// if (result.resultCode == Activity.RESULT_OK) {
// val data: Intent? = result.data
// val message = data?.getStringExtra("return_message")
// // 处理返回的数据
// }
// }
//
// // 在某个点击事件中启动
// myButton.setOnClickListener {
// val intent = Intent(this, ResultActivity::class.java)
// startForResult.launch(intent)
// }
// }
如果一个Activity已经启动到前台了,但是其他组件仍然调用了startActivity,这个时候一般会回调 onNewIntent() 方法,可以在这个回调里获取数据。
Activity 任务栈 (Task Stack) Android 系统通过 任务(Task) 来管理 Activity 的组织结构。一个任务是用户执行某项工作时与之交互的 Activity 的集合。这些 Activity 被组织在一个“后退栈”(Back Stack)中,以 堆栈(LIFO,后进先出) 的形式排列。
当用户启动一个新 Activity 时,它会被推送到当前任务栈的顶部,并成为焦点。 当用户按下返回键时,栈顶的 Activity 会被弹出并销毁,前一个 Activity 恢复到顶部。 当栈中最后一个 Activity 被弹出时,这个task任务就不再存在。 你可以在 AndroidManifest.xml 中使用 android:launchMode 来调整 Activity 的启动模式 ,从而影响其在任务栈中的行为,有以下四种启动模式:
standard (默认): 每次启动都会创建新的实例。singleTop: 也叫栈顶复用,如果目标 Activity 已经在栈顶,则不会创建新实例,而是调用其 onNewIntent() 方法。singleTask: 也叫栈内复用,确保一个任务栈中只有一个该 Activity 的实例。如果实例已存在于任何位置,则将其移动到栈顶并清理其上方的所有 Activity。singleInstance: 类似于 singleTask,单例模式,但它会创建一个全新的任务来包含这个 Activity,并且这个任务中只能有这一个 Activity。Activity 和 Fragment 的关系 一般稍微大型一点的项目,都会使用 Activity 和 Fragment 一起工作。理解它们的区别和联系至关重要:
Activity 是骨架: Activity 提供应用窗口和基本框架,管理整个屏幕的生命周期。Fragment 是模块: Fragment 是 Activity 的一部分,有自己的生命周期和布局,但必须依附于 Activity。它用于构建模块化、可复用的 UI 片段。分工合作: Activity 负责协调不同 Fragment 之间的交互、处理系统事件,而 Fragment 负责管理其内部的 UI 逻辑和数据。跳转和创建流程 可以参考应用冷启动流程的文章,详细介绍了进程初始化和Activity内部窗口,DecorView和contentView的绑定流程。
APP冷启动流程解析
车载Android开发和手机端的一个重大区别之一,就是车载Android设备通常拥有多个屏幕,比如一个主屏幕和一个副屏幕。在手机端通常只需要一个屏幕,但是在车载Android开发中,我们很多时候需要同时处理多个屏幕。
例如现在相当一部分的新能源车拥有主副驾两块大屏幕,主驾显示的界面为导航和车辆状态等,副驾屏幕用来显示一些娱乐app的流媒体等。甚至很多车企,还会有吸顶屏幕给后排乘客使用。
本文将介绍Android多屏开发主流的两种实现方式:Presentation和Activity。以下两种方式默认层级场景下,无系统权限的app也可以使用。
多屏设备的获取 首先,我们需要获取到多屏设备的信息,包括屏幕的数量、屏幕的尺寸、屏幕的方向等。
在Android中,我们可以通过DisplayManager来获取到多屏设备的信息。
fun getConnectedScreenIds ( context : Context ): List < Int > {
val displayManager = context . getSystemService ( Context . DISPLAY_SERVICE ) as DisplayManager
val displays = displayManager . displays
val screenIds = mutableListOf < Int >()
for ( display in displays ) {
screenIds . add ( display . displayId )
display . name
infoLog ( "displayId = ${display.displayId}, name = ${display.name}" )
}
return screenIds
}
其中,displayId是屏幕的唯一标识符,displayName是屏幕的名称。
其中display.getSize方法已经废弃,改为采用windowmanager中获取密度的方法来获取尺寸。
每次开关机之后,,displayId有可能不是固定的,主要看系统厂商是否对多屏的id进行了重置。
Presentation Presentation是Android提供的一种用于显示UI的类,它可以在一个单独的窗口中显示UI。
Presentation继承自Dialog类,因此它也可以设置窗口的属性,比如窗口的大小、窗口的位置等。
设置Presentation时,需要传入一个Context,一个Display对象,以确定Presentation的显示位置。然后在onCreate方法中设置UI即可。
class MyPresentation ( context : Context , display : Display ) : Presentation ( context , display ) {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . presentation )
}
}
创建实例,调用show方法。同时,我们也可以精细地定义其显示层级,显示大小,以及是否可以点击外部取消。
val displayManager =
this @MainActivity . getSystemService ( DISPLAY_SERVICE ) as DisplayManager
val display = displayManager . getDisplay ( 5 )
val presentation = MyPresentation ( this @MainActivity , display )
presentation . setCancelable ( false )
presentation . show ()
Activity 第二种方案可以直接使用Activity来实现多屏开发。
使用到了ActivityOptions类,官方定义为,其是一个用于构建一个选项Bundle的辅助类,该Bundle可与Context.startActivity(Intent, Bundle)及相关方法一起使用。
我们可以利用它来定义转场动画等关于显示的很多参数。这里用到了其指定displayid来显示的功能。
fun startPassengerActivity() {
val intent = Intent().apply {
setComponent(ComponentName("com.stephen.redfindemo", "com.stephen.redfindemo.PassengerActivity"))
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
val displayId = 5;
val options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(displayId);
try {
this.startActivity(intent, options.toBundle());
} catch (e: Exception) {
e.printStackTrace()
}
}
Pagination © 2024. All rights reserved. LICENSE | NOTICE | CHANGELOG
Powered by Hydejack v9.2.1