Android 开发者需要知道的 Linux 知识

2,071 阅读7分钟

去年年底学习了张绍文的《Android开发高手课》,课程真的非常不错,也学到了不少高级知识,好的东西自然是强烈推荐给大家。在《崩溃优化(下):应用崩溃了,你应该如何去分析?》一文中有这么几段话:

  • 虚拟内存。虚拟内存可以通过 /proc/self/status 得到,通过 /proc/self/maps 文件可以得到具体的分布情况。有时候我们一般不太重视虚拟内存,但是很多类似 OOM、tgkill 等问题都是虚拟内存不足导致的。

  • 文件句柄 fd。文件句柄的限制可以通过 /proc/self/limits 获得,一般单个进程允许打开的最大文件句柄个数为 1024。但是如果文件句柄超过 800 个就比较危险,需要将所有的 fd 以及对应的文件名输出到日志中,进一步排查是否出现了有文件或者线程的泄漏。

什么是虚拟内存,什么又是物理内存?文件句柄 fd 是什么?我们只知道文件打开了一定要关闭,那么不关闭会怎样呢?文件未关闭对我们应用的性能又有多大的影响?等等类似于这样一系列的问题,不知我们是否真的知晓呢?

其实上面所提到的都是 linux 内核方面的知识,我们平常写代码都是用 java 一把梭哈,可能觉得这些知识无关紧要,有些不了解也是正常。我们应该都知道 android 是基于 linux 内核的,因此去了解和学习这一块的知识完全是有必要的。再比如腾讯开源的 MMKV 用来代替 Android 的 SharedPreferences 我们是否能够看得懂其具体的实现呢?MMKV.cpp 中用到了 mmap() 函数,我们又是否了解其具体原理和实现?

开头说了这么多,无非就是想表达一下,作为一个合格的 Android 开发者,是有必要了解一些 linux 内核知识的。但我们毕竟不是从事 linux 内核和 Android Framework 相关的工作,因此作为一个 Android 应用开发者,只需要知道那些该知道的就差不多了。那哪些是该知道的呢?就是那些有利于开发和有利于性能优化的知识。推荐大家看两本书《深入理解LINUX内核》与《Linux内核源代码情景分析》。如果我们大学学过计算机组成原理和计算机操作系统就更好了,要是没学过大家又感兴趣,可以去听听国内外的一些公开课,我自己就曾听过清华大学的计算机操作系统公开课。

接下来想要跟大家分享的是,编译流程,动静态库,物理内存,虚拟内存。以下内容都是笔者归纳总结而得,也是看了一些资料和书籍,同时为了便于大家理解,难免会有些通俗,也可能会有些纰漏,欢迎大家指出批评。

1. gcc 编译四步骤

去年面试抖音 Android 开发岗位,被问到了你知道宏定义和 static 之间的区别吗?内联函数和普通函数有什么区别?你知道 Java 的内联吗?其实很多面试碰到的问题,以及技术上的难点,或者说遇到的 Bug 。 本质上是没啥区别的,当我们了解了最基本的原理,一切便可以迎刃而解。下面是我写的一段简单的代码:

#include <stdio.h>
#define TAG "Darren"
static char* tag="Jack";

int add(int a, int b){
  return a+b;
}

int main(){
  printf("Hello World!\n");
  printf("%s\n",TAG);
  printf("%s\n",tag);
  int sum = add(1, 2);
  return 0;
}

1.1 预处理阶段

执行命令是 gcc -E -o hello.i hello.c ,该阶段主要是将宏定义展开,全部的 #define 在这个阶段都会被展开,包含 #if #ifdef 一类的命令展开 #include 的文件,像上面 hello world 中的 stdio.h , 把 stdio.h 中的全部代码合并到 hello.c 中。具体查看文件是这样的:

// 省略了一部分 ......
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
# 912 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));



extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;


extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 942 "/usr/include/stdio.h" 3 4

# 2 "hello.c" 2



# 4 "hello.c"
static char* tag="Jack";

int add(int a, int b){
 return a+b;
}

int main(){
 printf("Hello World!\n");
 printf("%s\n","Darren");
 printf("%s\n",tag);
 int sum = add(1, 2);
 return 0;
}

1.2 预编译阶段

执行命令是在 gcc -S -o hello.s hello.i 这个阶段中,gcc 首先检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。

// 省略了一部分 ......
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	$.LC1, %edi
	call	puts
	movl	$.LC2, %edi
	call	puts
	movq	tag(%rip), %rax
	movq	%rax, %rdi
	call	puts
	movl	$2, %esi
	movl	$1, %edi
	call	add
	movl	%eax, -4(%rbp)
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
// 省略了一部分 ......

1.3 汇编阶段

命令是 gcc -c -o hello.o hello.s ,汇编阶段把 hello.s 文件翻译成二进制机器指令文件 hello.o , 文本打开乱码,需要借助工具才能打开。

// 省略了一部分 ......
0000000000000014 <main>:
  14:	55                   	push   %rbp
  15:	48 89 e5             	mov    %rsp,%rbp
  18:	48 83 ec 10          	sub    $0x10,%rsp
  1c:	bf 00 00 00 00       	mov    $0x0,%edi
  21:	e8 00 00 00 00       	callq  26 <main+0x12>
  26:	bf 00 00 00 00       	mov    $0x0,%edi
  2b:	e8 00 00 00 00       	callq  30 <main+0x1c>
  30:	48 8b 05 00 00 00 00 	mov    0x0(%rip),%rax        # 37 <main+0x23>
  37:	48 89 c7             	mov    %rax,%rdi
  3a:	e8 00 00 00 00       	callq  3f <main+0x2b>
  3f:	be 02 00 00 00       	mov    $0x2,%esi
  44:	bf 01 00 00 00       	mov    $0x1,%edi
  49:	e8 00 00 00 00       	callq  4e <main+0x3a>
  4e:	89 45 fc             	mov    %eax,-0x4(%rbp)
  51:	b8 00 00 00 00       	mov    $0x0,%eax
  56:	c9                   	leaveq 
  57:	c3                   	retq 

1.4 链接阶段

命令是 gcc -c -o hello.o hello.s ,该阶段主要做两件事情,利用 objdump 我们会看到如下代码,该阶段还会做一件事情就是合并数据段。

callq  400420 <__libc_start_main@plt+0x10>

callq  400526 <add>

callq  400420 <__libc_stdio_printf@plt+0x32>

2. 动态库与静态库

链接时我们可以链接静态库和动态库,在 android 开发中我们一般用 .so 动态库比较多,在下载 opencv 人脸识别 sdk 时,我们也能看到如下的一些 .a 静态库:

静态库.a

静态库是编译时拷贝了调用代码,也就是说编译链接后如果静态库不存在或者删除了,运行也是不会报错的。动态库是运行时动态加载的,如果动态库不存在了,运行时就会出错,而且在生成 .o 文件时需要生成与位置无关的代码。

3. 虚拟内存

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存一个连续完整的地址空间,而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的“虚拟内存”;Linux 的“交换空间”等。

大致的虚拟内存布局图

最后给大家分享一下《Android开发高手课》,为啥在课程完结了才分享给大家呢?首先笔者已经学习过了,真的非常不错才敢推荐给大家。最后还是再啰啰嗦嗦一下,很多东西大家不要眼高手低,一定要动手实践。比如在启动优化那一章,大家可以跟着一起去优化自己项目的启动流程,不要只是听一遍,而是要用到自己的项目中。还有很多内容我们也不一定能听懂,只要是不影响学习进度大家可以先过一遍,好的学习资料都是第一遍可以学到很多东西第二遍有进步第三遍还可以有提升。分享采取的是音频加文章的方式,虽然绍文已经给大家总结了文章,但我们仍然需要自己去做笔记总结和归纳,让其能真正变成我们自己的知识。

Android开发高手课

视频链接:pan.baidu.com/s/14ctKrevi…  视频密码:h5mj