【Android驱动14】Android系统Crash工具使用方法和分析

1 系统简介

当 Linux 系统内核发生崩溃的时候,可以通 KEXEC+KDUMP 等方式收集内核崩溃之前的内存,生成一个转储文件 vmcore。内核开发者通过分析该 vmcore 文件就可以诊断出内核崩溃的原因,从而进行操作系统的代码改进。主要用于分析以下问题场景:

• Android 系统内存泄露导致的卡顿、 ANR 重启问题

• Linux 内核软件 Panic Oops 崩溃问题、死锁问题

• Linux 内核态 Memory overflow 内存溢出、 OOM 内存分配失败问题

2 Crash 工具使用方法和分析

免费crash工具资源下载路径。

2.1 CrashDump 分析所需资源

Crashdump 需要的资源如下:

  1. 需要小机固件对应的内核符号表 vmlinux。
    • linux-5.4 及以后放在 longan 目录下的 out/kernel/build;
    • linux-4.9 放在内核目录下;
    注: linux-5.4 可以到 longon/build 目录下,执行 getvmlinux.sh 固件路径,会在该目录生
    成一个 output 目录,里面包含了固件的 vmlinux。
  2. crash 解析程序 crash_arm64(32 位为 crash_arm),跟版本相关。

2.2 解析内存镜像

将 crash_arm(64), vmlinux 以及 dump 出来的内存镜像放到同一个目录下,然后执行以下

指令解析内存镜像:

linux-4.9 下使用这条指令:

c 复制代码
./crash_arm64 vmlinux dram_data_20191210113324@0x40000000

linux-5.4 下使用这条指令:

c 复制代码
./crash_arm64 vmlinux dram_data_2020514192634@0x40000000 --machdep vabits_actual=39 --
machdep kimage_voffset=0xffffffbfd0000000

linux-5.10 及 linux-5.15 下使用这条指令:(注意,需要使用 8.0.1++ 版本的工具)

c 复制代码
./crash_arm64 vmlinux dram_data_202292093322@0x40000000 --machdep vabits_actual=39 --
machdep kimage_voffset=0xffffffbfc8000000 --kaslr 0x80000

参数的解释:

• vmlinux:当前固件对应的 vmlinux

版权所有 © 珠海全志科技股份有限公司。保留一切权利 13文档密级:秘密

• dram_data_2020514192634 : dump 出来的数据文件名,或者通过 decrypt 工具进行解

密后的文件名;

• @0x40000000 :自定义的 dram 的起始地址(由 Memory Map Spec 指定);

• vabits_actual=39:设置访问的位宽,通过 CONFIG_ARM64_VA_BITS_39 进行配置;

• machdep kimage_voffset=0xffffffbfd0000000:指定内核镜像的偏移地址,不便于计算,

可在内核中添加打印获取其值;

• -kaslr 0x80000: kaslr 特性会对内核加载地址做 relocation,使能该特性后,内核

实际映射的运行时地址和链接地址是不一样的,中间差距 kaslr offset 值(crash 时

dump_kernel_offset 函数会打印具体的值)。

若编译的固件为 ARM32,则使用 crash_arm32 工具和以下指令进行解析:(建议使用高版本

的 crash_arm32 工具进行解析)

c 复制代码
./crash_arm32 vmlinux dram_data_20191210113324@0x40000000

如需调试,请优先考虑从固件中解压 vmlinux。

使用方法如下:

• 使用脚本进行提取 vmlinux,脚本存放路径为: build/getvmlinux.sh。

c 复制代码
./getvmlinux.sh <aw-format-firmware>

2.3 常见命令使用

解析完内存镜像后会进去控制台,常见的指令如下:

  1. bt:显示调用堆栈:
c 复制代码
crash_arm64> bt -f
PID: 1191 TASK: ffffff8046b1da00 CPU: 0 COMMAND: "kworker/0:2H"
bt: WARNING: cannot determine starting stack frame for task ffffff8046b1da00
crash_arm64> bt -a # 查看当前SOC内所有CPU的任务堆栈
PID: 1191 TASK: ffffff8046b1da00 CPU: 0 COMMAND: "kworker/0:2H"
bt: WARNING: cannot determine starting stack frame for task ffffff8046b1da00
PID: 0 TASK: ffffff80c01eec00 CPU: 1 COMMAND: "swapper/1"
bt: WARNING: cannot determine starting stack frame for task ffffff80c01eec00
PID: 0 TASK: ffffff80c01e9200 CPU: 2 COMMAND: "swapper/2"
bt: WARNING: cannot determine starting stack frame for task ffffff80c01e9200
PID: 0 TASK: ffffff80c01f9200 CPU: 3 COMMAND: "swapper/3"
bt: WARNING: cannot determine starting stack frame for task ffffff80c01f9200
PID: 0 TASK: ffffff80c01f8000 CPU: 4 COMMAND: "swapper/4"
bt: WARNING: cannot determine starting stack frame for task ffffff80c01f8000
PID: 2022 TASK: ffffff8084915a00 CPU: 5 COMMAND: "irq/297-gt9xx"
bt: WARNING: cannot determine starting stack frame for task ffffff8084915a00
PID: 0 TASK: ffffff80c01fb600 CPU: 6 COMMAND: "swapper/6"
bt: WARNING: cannot determine starting stack frame for task ffffff80c01fb600
PID: 0 TASK: ffffff80c01fa400 CPU: 7 COMMAND: "swapper/7"
bt: WARNING: cannot determine starting stack frame for task ffffff80c01fa400
  1. log/dmesg:显示内核死机前打印信息
c 复制代码
crash_arm64> log
[33108.133841] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 52000000Hz bm PP pm ON vdd 23 width
8 timing MMC-HS200 dt B
[33108.146503] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 150000000Hz bm PP pm ON vdd 23
width 8 timing MMC-HS200 dt B
[33108.161514] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 150000000Hz bm PP pm ON vdd 23
width 8 timing MMC-HS(SDR20) dt B
[33108.175606] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 52000000Hz bm PP pm ON vdd 23 width
8 timing MMC-HS(SDR20) dt B
[33108.189617] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 50000000Hz bm PP pm ON vdd 23 width
8 timing MMC-HS400 dt B
[33108.202060] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 150000000Hz bm PP pm ON vdd 23
width 8 timing MMC-HS400 dt B
crash_arm64> dmesg
[33108.133841] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 52000000Hz bm PP pm ON vdd 23 width
8 timing MMC-HS200 dt B
[33108.146503] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 150000000Hz bm PP pm ON vdd 23
width 8 timing MMC-HS200 dt B
[33108.161514] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 150000000Hz bm PP pm ON vdd 23
width 8 timing MMC-HS(SDR20) dt B
[33108.175606] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 52000000Hz bm PP pm ON vdd 23 width
8 timing MMC-HS(SDR20) dt B
[33108.189617] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 50000000Hz bm PP pm ON vdd 23 width
8 timing MMC-HS400 dt B
[33108.202060] sunxi-mmc 4022000.sdmmc: sdc set ios:clk 150000000Hz bm PP pm ON vdd 23
width 8 timing MMC-HS400 dt B
[33108.244306] prate:2400000000, drate:480000000
  1. ps:任务列表的展示:
c 复制代码
crash_arm64> ps # 展示所有的任务,就像在一个实时系统上一样
PID PPID CPU TASK ST %MEM VSZ RSS COMM
0 0 0 ffffffc00aec2100 RU 0.0 0 0 [swapper/0]
> 0 0 1 ffffff80c01eec00 RU 0.0 0 0 [swapper/1]
> 0 0 2 ffffff80c01e9200 RU 0.0 0 0 [swapper/2]
> 0 0 3 ffffff80c01f9200 RU 0.0 0 0 [swapper/3]
> 0 0 4 ffffff80c01f8000 RU 0.0 0 0 [swapper/4]
0 0 5 ffffff80c01fc800 RU 0.0 0 0 [swapper/5]
> 0 0 6 ffffff80c01fb600 RU 0.0 0 0 [swapper/6]
> 0 0 7 ffffff80c01fa400 RU 0.0 0 0 [swapper/7]
1 0 7 ffffff80c01c9200 IN 0.1 2239960 3848 init
2 0 1 ffffff80c01c8000 IN 0.0 0 0 [kthreadd]
3 2 0 ffffff80c01cc800 ID 0.0 0 0 [rcu_gp]
4 2 0 ffffff80c01cb600 ID 0.0 0 0 [netns]
8 2 0 ffffff80c01e6c00 ID 0.0 0 0 [mm_percpu_wq]
10 2 0 ffffff80c01e0000 IN 0.0 0 0 [rcu_tasks_kthre]
11 2 0 ffffff80c01e4800 IN 0.0 0 0 [rcu_tasks_trace]
12 2 0 ffffff80c01e3600 RU 0.0 0 0 [ksoftirqd/0]
13 2 7 ffffff80c01e2400 ID 0.0 0 0 [rcu_preempt]
14 2 6 ffffff80c01e5a00 IN 0.0 0 0 [rcub/0]
c 复制代码
crash_arm64> ps 1 # 根据PID 查看对应任务信息
PID PPID CPU TASK ST %MEM VSZ RSS COMM
1 0 7 ffffff80c01c9200 IN 0.1 2239960 3848 init
c 复制代码
crash_arm64> ps init # 根据任务名查看对应任务信息
PID PPID CPU TASK ST %MEM VSZ RSS COMM
1 0 7 ffffff80c01c9200 IN 0.1 2239960 3848 init
229 0 6 ffffff80c02b6c00 IN 0.1 2239960 3848 init
230 1 2 ffffff80c028c800 IN 0.1 2167104 2796 init
  1. files
  2. fuser:可以看到谁正在使用的文件路径或模式
  3. list/tree:可以从内核结构见列表或基数/ rbtree
  4. irq:中断信息查询
c 复制代码
crash_arm64> irq # 展示所有已经注册的中断信息,包括IRQ中断亲和力
IRQ IRQ_DESC/_DATA IRQACTION NAME
0 (unused) (unused)
1 ffffff80c000f600 ffffff80c00cd000 "IPI"
2 ffffff80c000c200 ffffff80c00cd280 "IPI"
3 ffffff80c000c600 ffffff80c00cd500 "IPI"
4 ffffff80c000dc00 ffffff80c00cde00 "IPI"
5 ffffff80c000ce00 ffffff80c00cd600 "IPI"
6 ffffff80c000fc00 ffffff80c00cd580 "IPI"
7 ffffff80c000d600 ffffff80c00cd700 "IPI"
8 ffffff80c000e400 (unused)
9 ffffff80c000d400 (unused)
10 ffffff80c000f200 (unused)
11 ffffff80c000e800 ffffff80c00ce000 "arch_timer"
12 ffffff80c000d200 (unused)
13 ffffff80c19cf800 ffffffc00ae1a700
14 ffffff80c19cf000 ffffff80c7f93e00 "timer@3008000"
15 ffffff80c19cec00 ffffff80c1755a00 "arm-pmu"
16 ffffff80c03b3400 ffffff80c414b680 "7121000.dma1-controller"
17 ffffff80c03b0a00 ffffff80c4126200 "7090000.rtc"
18 ffffff80c03b0200 ffffff80c19c0400 "3002000.dma-controller"
crash_arm64> irq | grep "twi0"
416 ffffff80c2ab4e00 ffffff80c3e6f580 "2502000.twi0"
420 ffffff80c2ab5400 ffffff80c3e5dd80 "7081400.s_twi0"
crash_arm64> irq 416 # 展示某一个具体IRQ的信息
IRQ IRQ_DESC/_DATA IRQACTION NAME
416 ffffff80c2ab4e00 ffffff80c3e6f580 "2502000.twi0"
  1. dis:反汇编代码 --> 这对检查比特翻转问题有用 高频使用
  2. rd/wr:读写存储器地址
  3. ps : 显示任务结构 高频使用
c 复制代码
crash_arm64> ps | grep gt9xx
> 2022 2 5 ffffff8084915a00 RU 0.0 0 0 [irq/297-gt9xx]
crash_arm64> task 2022
PID: 2022 TASK: ffffff8084915a00 CPU: 5 COMMAND: "irq/297-gt9xx"
struct task_struct {
thread_info = {
flags = 8,
ttbr0 = 1118797824,
{
  1. struct:对结构体进行解析, 高频使用
c 复制代码
crash_arm64> struct i2c_msg # 显示内核结构及其大小
struct i2c_msg {
__u16 addr;
__u16 flags;
__u16 len;
__u8 *buf;
}
crash_arm64> struct i2c_msg -o # 显示成员的偏移量
struct i2c_msg {
[0] __u16 addr;
[2] __u16 flags;
[4] __u16 len;
[8] __u8 *buf;
}
SIZE: 16
crash_arm64> struct i2c_msg ffffffc010feb9e8 # 加上结构体的地址,可以打印出对应的元素值
struct i2c_msg {
addr = 93,
flags = 0,
len = 3,
buf = 0xffffffc010feb9f8 "\201N"
}
  1. waitq:可以看到任务在 waitq 待定
  2. search:在内存范围内搜索值/字符串
  3. vm:可以看到任务的虚拟内存映射,虚拟机可以看到物理内存映射。
  4. kmem:显示内核内存页信息。
  5. p:打印一个表达式或变量, struct 的成员
  6. mod:载一个模块,才能继续打印分析 ko 内的数据信息

2.4 应用场景案例

2.4.1 访问非法地址

使用 dmesg 命令查看死机现场的 log。看到最后的死机现场如下所示:

c 复制代码
PC is at sunxi_uart_dev_info_show+0x28/0x8c

再用反汇编命令 dis 解析出代码对应的源码及汇编:

c 复制代码
dis -l sunxi_uart_dev_info_show+0x28

可以看出跑飞的代码为: /home/lidaxin/AndroidQ/longan/kernel/linux-4.9/drivers/tty/serial/sunxiuart.c: 1501。对应汇编为: str wzr,x1,这里是要往 x1 地址内存写 0, log 信息可以看到

x1=0, 往 0 地址写 0,那就是访问非法地址了。

可以看出跑飞的代码为:

c 复制代码
linux-4.9/drivers/tty/serial/sunxi-uart.c:1501。

对应汇编为: str wzr,x1,这里是要往 x1 地址内存写 0, log 信息可以看到 x1=0, 往 0 地址

写 0,那就是访问非法地址了。

2.4.2 OOM

通过 dmesg 查看 OOM 的报错详细信息。解析后的 panic 原因是: Out of memory.

再通过 dmesg 查看 OOM 的报错详细信息。内存使用情况: free 只有 11252kB,low 水位是

18024kB,此时报出 oom 为正常。 anon 页面和 file 页面都很少,说明进程占用的内存已经

很少。 mlock 页面为 1609904kB, unevictable 页面 1609904kB 。 mlock 的页面即为 unevictable 的。所以这里要看是哪个进程把内存 mlock 了。

再查看进程的内存占用信息:可以看出内存占用最高的进程是 memtester 进程。

所以这里造成 oom 的原因是 memtester 进程内存占用太高。

2.4.3 内核链表信息被破坏

使用 dmesg 命令查看死机现场的 log。

c 复制代码
crash_arm64> dmesg

可以看到最后的死机现场如下:

查看死机地址 PC 指针所处源码位置:

c 复制代码
crash_arm64> dis -l 0xffffff800848a9b8

得到:

查看源码:

可以看到 strcmp 取第 0 个参数时取到了非法指针 0x1f00000000000000.

进一步查看是哪里调用了 strcmp,查看 LR 指针所处源码位置:

c 复制代码
crash_arm64> dis -l 0xffffff8008115f8c

得到:

查看源码,如下:

得知是内核在操作以 all_lock_classes 为头的 lock_class 结构体链表时,取到了非法指针。这

里的非法指针是 0x1f00000000000000,所以逃过了内核的指针为 NULL 的合法性检查,将错

误传入到了下一层的 strcmp。

Dump 出 all_lock_classes 为头的链 表上 所有 lock_class 结构详细 信息, 并转储 到

all_lock_classes.txt 文件。

c 复制代码
crash_arm64> list -H all_lock_classes lock_class.lock_entry -s lock_class >
all_lock_classes.txt

查看 all_lock_classes.txt 文件,搜索 name 关键字:

找到 line22891 行,数据结构的 name 字符串指针非法。

再往上找到此结构的地址:

发现数据结构地址非法(指针末尾没有 4 对齐)

再查看链表的上一个数据成员的 next 指针:

链表上一个 struct 的 next 指针已经被破坏,导致下一个数据结构地址非法,取到的 name 指

针自然非法, strcmp 取到非法指针 0x1f00000000000000,内核崩溃。参见 dump 信息中

X0 寄存器(strcmp 函数的第 0 个参数)值为 0x1f00000000000000,非法 name 指针也为

0x1f00000000000000,严格匹配 strcmp dst1 地址,证实 strcmp() 函数进行字符串匹配时

取到非法指针,内核崩溃。

为进一步证实推论(单个链表指针被破坏),参照正常 struct 结构规律:

Next 指针值为 struct 指针值加 0x200(struct 指针值 0xffffff800998caf8+0x200=next 指

针值 0xffffff800998ccf8),所以被破坏结构的 next 指针合法值应为:

0xffffff800998cce8+0x200=0xffffff800998cee8。

Struct 结构地址为: 0xffffff800998cee8-0x10=0xffffff800998ced8。

获取 0xffffff800998ced8 结构体详细信息,看是否合法:

c 复制代码
crash_arm64> struct lock_class 0xffffff800998ced8

显式结果合法,取到了合法的 prev/next 指针及合法的 name 字符串:

如上推测得到证实。如果 pc 指针能退回,并将此错误指针修改回去,内核就又可以欢快的 run

了

2.4.4 内核指针被破坏

使用 dmesg 命令查看死机现场的 log。

c 复制代码
crash_arm64> dmesg

可以看到最后的死机现场如下:

查看死机地址 PC 指针所处源码位置:

c 复制代码
crash_arm64> dis -l 0xffffff8008232bcc

得到:

查看源码:

得知内核在执行寻址 oldpage 结构体成员时取到了非法指针: 0xffffffaf00d59828。对应汇编如

下

看最后一句指令功能。 x19 寄存器中存放 page 结构体指针,偏移 40 寻找其 pobjects 结构成

员。从 log 信息可以获知, x19 寄存器值: 0xffffffaf00d59800 就已经不是一个合法指针了。

根据 linux 启动后的 memory mapping 图:

推测 x19 是从 0xffffffbf00d59800 翻转到 0xffffffaf00d59800 的。 0xffffffbf00d59800 是一

个合法的 page 指针。为了证实这个结论,进一步查看 0xffffffbf00d59800 指向的内容是否是

一个合法的 page 结构,如下:

c 复制代码
crash_arm64> struct page 0xffffffbf00d59800

得到了一个合法的 page 结构,如下,证实推测(x19 是从 0xffffffbf00d59800 翻转到

0xffffffaf00d59800 的):

2.4.5 指针访问权限非法

使用 dmesg 命令查看死机现场的 log。

c 复制代码
crash_arm64> dmesg

可以看到最后的死机现场如下:

查看死机地址 PC 指针所处源码位置:

c 复制代码
crash_arm64> dis -l 0xffffff800814a1fc

得到:

死机时 CPU 正在执行指令: ldr x23, sp, #48,一个普通的存取堆栈操作。

对照死机 log sp 指针值: 0xffffffc03866bd40 是个内核合法地址(如下图内核地址范围):

用 crash 工具尝试读取此地址:

c 复制代码
crash_arm64> rd 0xffffffc03866bd40 0x100

得到:

说明内存中内核页表是 ok 的。

查看 log 所处的内核源码,内核在 line: 350 行报错:

说明 fault 地址一定小于 4G。但此处 sp 远大于 4G,而且内核页表完好, CPU 不应该进入缺页

异常分支。推测 CPU MMU 硬件出错。

2.4.6 访问内核地址报错

使用 dmesg 命令查看死机现场的 log。

c 复制代码
crash_arm64> dmesg

可以看到最后的死机现场如下:

fault 地址 0xffffffc03699f96c,是一个合法的内核地址,在下图内核 mapping 空间内:

查看死机地址 PC 指针所处源码位置:

c 复制代码
crash_arm64> dis -l ffffff800873362c

得到:

触发异常的指令是一条正常 load/store 指令,其中 x28 寄存器是合法内核指针,在如下地址空

间内:

尝试读取引发 fault 的地址: 0xffffffc03699f96c, crash 工具可以正常读取和完成地址转换:

c 复制代码
crash_arm64> rd ffffffc03699f96c 10

如下图:

手动读取内存中的 3 级内核页表。为了找到根页表,读取 init_mm 结构体,如下:

c 复制代码
crash_arm64> p init_mm

结果如下:

得到内核页表基地址: pgd = 0xffffff800a4d8000

读取内核基页表并存到 kernel_memory_pgd_table.txt 中:

c 复制代码
crash_arm64> rd 0xffffff800a4d8000 512 > kenel_memory_table.txt

根据 fault 地址: 0xffffffc03699f96c,找到一级页目录项,如下:

末尾 2bit 全 1,是一个合法页目录项。

读取二级页目录到 kenel_memory_pmd_c_table.txt 文件中:

c 复制代码
crash_arm64> rd -p 000000007f7ea000 512 > kenel_memory_pmd_c_table.txt

根据 fault 地址: 0xffffffc03699f96c,找到二级页目录项,如下:

末尾 2bit 全 1,是一个合法的页目录项。

读取三级页目录到 kenel_memory_pte_139_table.txt 文件中

c 复制代码
crash_arm64> rd -p 000000007f6b0000 512 > kernel_memory_pte_139_table.txt

截图如下,都是合法页表:

以上过程再次说明内核页表没问题,问题处在 CPU MMU 硬件上。

3 注意事项

3.1 WARNING: cannot access vmalloc'd module

memory

crash 加载过程中出现如下的 log:

c 复制代码
machine type mismatch / "WARNING: cannot access vmalloc'd module memory "

表示 DRAM 数据与 vmlinux 不匹配,需要重新提供匹配的数据。

3.2 crash: cannot determine page size

crash 加载过程中出现如下的 log:

c 复制代码
crash: cannot determine page size

• 表示 crash 工具不匹配,区分是 arm32 还是 arm64 的平台;

• 要注意 dump 内存的大小,要完全 dump 出来。详见 3.1.1 章节检查 crash 数据的获取时配

置是否正确;

• 可以在解析命令后添加参数-p 4k指定 page size 的大小;

• 也可能是 crash 工具的版本不匹配,推荐使用最新的 crash 进行调试验证。

5.3 crash_arm64: read error: kernel virtual address: ffffffc0087cf580 type: "kernel_config_data"

crash 加载过程中出现如下的 log:

c 复制代码
crash_arm64: read error: kernel virtual address: ffffffc0087cf580 type: "
kernel_config_data"
WARNING: cannot read kernel_config_data
crash_arm64: read error: kernel virtual address: ffffffc008e68860 type: "possible"
WARNING: cannot read cpu_possible_map
crash_arm64: read error: kernel virtual address: ffffffc008e68858 type: "present"
WARNING: cannot read cpu_present_map
crash_arm64: read error: kernel virtual address: ffffffc008e68850 type: "online"
WARNING: cannot read cpu_online_map
crash_arm64: read error: kernel virtual address: ffffffc008e68878 type: "active"
WARNING: cannot read cpu_active_map
crash_arm64: read error: kernel virtual address: ffffffc008f875a8 type: "shadow_timekeeper
xtime_sec"
crash_arm64: read error: kernel virtual address: ffffffc008e6dc44 type: "init_uts_ns"
crash_arm64: tmp/vmlinux and /var/tmp/ramdump_elf_sDmBG5 do not match!

• 表示 crash 工具指定的 kimage_voffset 有误,需要提供正确的内核镜像偏移量参数。

• 对于 ARM 64 位系统, mmu 模块会将 kimage_voffset 导出为全局符号,见kernel/linux

-5.15/arch/arm64/mm/mmu.c。

• 在任意能够运行的驱动添加如下代码将信息偏移量信息打印出来替换掉该参数即可。

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui