Android 開發者需要知道的 Linux 知識

去年年底學習了張紹文的《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 靜態庫:

靜態庫是編譯時拷貝了調用代碼,也就是說編譯鏈接後如果靜態庫不存在或者刪除了,運行也是不會報錯的。動態庫是運行時動態加載的,如果動態庫不存在了,運行時就會出錯,而且在生成 .o 文件時需要生成與位置無關的代碼。

3. 虛擬內存

虛擬內存是計算機系統內存管理的一種技術。它使得應用程序認爲它擁有連續的可用的內存一個連續完整的地址空間,而實際上,它通常是被分隔成多個物理內存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數據交換。目前,大多數操作系統都使用了虛擬內存,如 Windows 家族的“虛擬內存”;Linux 的“交換空間”等。

最後給大家分享一下《Android開發高手課》,爲啥在課程完結了才分享給大家呢?首先筆者已經學習過了,真的非常不錯纔敢推薦給大家。最後還是再囉囉嗦嗦一下,很多東西大家不要眼高手低,一定要動手實踐。比如在啓動優化那一章,大家可以跟着一起去優化自己項目的啓動流程,不要只是聽一遍,而是要用到自己的項目中。還有很多內容我們也不一定能聽懂,只要是不影響學習進度大家可以先過一遍,好的學習資料都是第一遍可以學到很多東西第二遍有進步第三遍還可以有提升。分享採取的是音頻加文章的方式,雖然紹文已經給大家總結了文章,但我們仍然需要自己去做筆記總結和歸納,讓其能真正變成我們自己的知識。

視頻鏈接:https://pan.baidu.com/s/14ctKrevigccds6wY_QY5Gw
視頻密碼:h5mj

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章