avaScript 在 ES6 上有很多數組方法,每種方法都有獨特的用途和好處。
在開發應用程序時,大多使用數組方法來獲取特定的值列表并獲取單個或多個匹配項。
在列出這兩種方法的區別之前,我們先來一一了解這些方法。
JavaScript find()方法
ES6 find() 方法返回通過測試函數的第一個元素的值。如果沒有值滿足測試函數,則返回 undefined。
語法
以下語法中使用的箭頭函數。
find((element)=> { /* ... */ } )
find((element, index)=> { /* ... */ } )
find((element, index, array)=> { /* ... */ } )
我們有一個包含名稱 age 和 id 屬性的用戶對象列表,如下所示:
let users=[{
id:1,
name: 'John',
age: 22
}, {
id:2,
name: 'Tom',
age: 22
}, {
id:3,
name: 'Balaji',
age: 24
}];
以下代碼使用 find() 方法查找年齡大于 23 的第一個用戶。
console.log(users.find(user=> user.age > 23));
//console
//{ id: 3, name: 'Balaji', age:24}
現在我們要找到第一個年齡為 22 的用戶
console.log(users.find(user=> user.age===22));
//console
//{ id: 1, name: 'John', age:22}
假設沒有找到匹配意味著它返回 undefined
console.log(users.find(user=> user.age===25));
//console
//undefined
JavaScript filter() 方法
filter() 方法創建一個包含所有通過測試函數的元素的新數組。如果沒有元素滿足測試函數,則返回一個空數組。
語法
filter((element)=> { /* ... */ } )
filter((element, index)=> { /* ... */ } )
filter((element, index, array)=> { /* ... */ } )
我們將使用相同的用戶數組和測試函數作為過濾器示例。
以下代碼使用 filter() 方法查找年齡大于 23 的第一個用戶。
console.log(users.filter(user=> user.age > 23));
//console
//[{ id: 3, name: 'Balaji', age:24}]
現在我們要過濾年齡為 22 歲的用戶
console.log(users.filter(user=> user.age===22));
//console
//[{ id: 1, name: 'John', age:22},{ id: 2, name: 'Tom', age:22}]
假設沒有找到匹配意味著它返回一個空數組
console.log(users.filter(user=> user.age===25));
//console
//[]
find() 和 filter() 的區別與共點
共點
高階函數:這兩個函數都是高階函數。
區別
1、通過一個測試功能
find() 返回第一個元素
filter() 返回一個包含所有通過測試函數的元素的新數組
2、如果沒有值滿足測試函數
find() 返回未定義
filter() 返回一個空數組
Linux 命令中,find 是比較復雜難用的命令。使用該命令搜索文件時,常常發現自己找了一些例子能用,但稍微改一下條件,就搜不到想要的結果。
下面會以一些實例來說明使用 find 命令的關鍵要點和注意事項,解釋清楚各個條件能夠工作、或者不能工作的原因。
具體包含下面的內容:
要使用一個命令,首先要了解命令的格式,知道要提供什么參數、參數作用是什么。
查看 man find 對該命令的說明如下:
即,find 命令的作用是在目錄層次結構下搜索文件,默認會遞歸搜索所給目錄的子目錄,對查找到的每一個文件名(目錄名也屬于文件名)依次進行后面表達式的判斷,來決定是否打印搜索到的文件名、或者進行其他的操作。
注意:對每一個搜索到的文件名都依次進行表達式評估是非常關鍵的點,find 命令會把搜索到的每一個文件名都依次作為參數傳遞給后面的表達式進行評估,來決定如何處理這個文件,某個文件的表達式評估為 false,還是會繼續評估下一個文件,除非主動執行了結束的操作。
理解這一點,就會清楚為什么有些文件名會打印出來,而有些文件名不會打印出來,因為它們本身就相互不關聯。
下面具體說明 find 命令格式各個部分的含義:
關于 find 命令的說明,也可以查看 GNU find 的在線幫助手冊 https://www.gnu.org/software/findutils/manual/html_mono/find.html,這里面的說明比 man find 詳細,并提供了不少例子,可供參考。
在 Linux 中,目錄也屬于文件,find 在查找時,把目錄也當成文件處理,會查找并處理目錄名,并不是只處理文件名。
后面在說明時,如無特別備注,所說的文件名包含了目錄名。
find 命令最簡單的用法就是直接執行這個命令,不提供任何參數,默認會查找當前目錄、及其子目錄下的所有文件,并打印出所有文件名。
具體舉例如下:
$ ls
Makefile.am src tests
$ find
.
./src
./src/main.c
./tests
./tests/bre.tests
./Makefile.am
可以看到,在 shell 的當前工作目錄下執行 find 命令,不提供任何參數,會打印出當前目錄、及其子目錄下的所有文件名,包括目錄名。
可以在 find 命令后面提供目錄名,指定要查找哪個目錄:
$ find .
.
./src
./src/main.c
./tests
./tests/bre.tests
./Makefile.am
$ find src
src
src/main.c
$ find src tests
src
src/main.c
tests
tests/bre.tests
在 Linux 下,點號 ‘.’ 對應當前目錄,所以 find . 就是查找當前目錄下的所有文件,當沒有提供目錄參數時,默認就是使用 ‘.’ 這個參數。
find src 命令指定只查找 src 這個目錄下的所有文件。
find src tests 命令指定查找 src、tests 這兩個目錄下的所有文件,可以同時提供多個目錄名來指定查找這些目錄。
find src tests 命令也可以寫為 find ./src ./tests。
如果在 find 命令后面提供文件名,則只在當前目錄下查找該文件,不會在子目錄下查找:
$ find Makefile.am
Makefile.am
$ find main.c
find: `main.c': No such file or directory
結合上面打印的文件信息,可以看到當前目錄下有一個 Makefile.am 文件,find Makefile.am 命令可以找到這個文件,不會報錯。
而 main.c 文件是在 src 子目錄下,find main.c 命令執行報錯,提示找不到這個文件,它不會進入 src 子目錄進行查找。
注意:前面提到,查找條件要求以 ‘-’、‘(’、或者 ‘!’ 開頭,在遇到以這幾個字符開頭的任意參數之前,前面的參數都會被當作目錄參數,指定查找多個目錄時,直接在 find 命令后面寫上這些目錄的路徑,用空格隔開即可,不用加上 -o、-path 等選項,加上反而有異常。
剛接觸 find 命令,常見的誤區之一就是認為要用 -o 選項來指定查找多個目錄。
例如認為 find src -o tests 是同時查找 src、tests 這兩個目錄,這是錯誤的寫法,執行會報錯:
$ find src -o tests
find: paths must precede expression: tests
可以看到,執行報錯,提示目錄路徑參數必須在表達式參數之前提供。-o 參數以 - 開頭,會被認為是表達式參數,它自身、以及在它之后的所有參數都會認為是表達式參數,之后提供的目錄名不會被當作要查找的目錄。
某些表達式參數的后面可以提供目錄名,但是這些目錄名并不是用于指定查找該目錄下的文件,而是另有含義。
另一個誤區是,執行 find src -o tests 命令報錯后還不知道錯在哪里,望文生義,又加上 -path 選項,誤寫為 find src -o -path tests、或者 find src -path -o tests。這兩個命令都會報錯,自行測試即知。
雖然寫為 find src -path tests 不會報錯,但是它并不會打印出 src、tests 這兩個目錄下的文件名。
后面會具體說明 -path 參數的用法。
基于上面例子的目錄結構,如果想查找當前目錄下的文件,且忽略 tests 目錄,可以執行下面的命令:
$ find . -path ./tests -prune -o -print
.
./src
./src/main.c
./Makefile.am
可以看到,打印的文件名里面沒有 tests 目錄名、以及它底下的文件名。
但是如果把上面 -path 后面的 ./tests 改成 tests,還是會查找 tests 目錄下的文件:
$ find . -path tests -prune -o -print
.
./src
./src/main.c
./tests
./tests/bre.tests
./Makefile.am
這個結果比較奇怪,查找時想要忽略 tests 目錄,寫為 -path ./tests 可以忽略,寫為 -path tests 就不能忽略。
這是使用 find 命令的 -path 參數時常見的錯誤,別人的例子可以生效,自己寫的時候就不生效。這需要理解 -path 參數的含義才能正確使用它。
前面提到,不同的表達式之間要用操作符分隔開,如果沒有提供操作符,默認使用 -and 操作符。
所以 find . -path ./tests -prune -o -print 命令的完整格式其實是 find . -path ./tests -and -prune -o -print。
下面對這個完整命令格式的各個參數進行詳細說明,以便理解它的工作原理,就能知道為什么寫為 -path ./tests 可以忽略,寫為 -path tests 不能忽略。
這是一個 test 類型表達式,GNU find 在線幫助手冊對該表達式的說明如下:
Test: -path pattern
True if the entire file name, starting with the command line argument under which the file was found, matches shell pattern pattern.
To ignore a whole directory tree, use ‘-prune’ rather than checking every file in the tree.
The “entire file name” as used by find starts with the starting-point specified on the command line, and is not converted to an absolute pathname.
即,當 find 命令查找到的文件名完全匹配所給的 pattern 模式時,該表達式返回 true。
這里面最關鍵的點是,要完全匹配 find 命令查找到的名稱,而不是部分匹配,也不是匹配文件的絕對路徑名。
具體舉例說明如下:
$ find . -path ./tests
./tests
$ find . -path tests
$ find . -path ./tests/
$ find tests
tests
tests/bre.tests
$ find tests -path tests
tests
$ find tests -path ./tests
可以看到,find . -path ./tests 命令打印了 ./tests 目錄名,但是 find . -path tests 命令什么都沒有打印。
查看上面 find . 命令打印的信息,可以看到該命令打印的 tests 目錄名是 ./tests,-path 參數要求是完全匹配才會返回 true,所以基于打印結果,就是要寫為 -path ./tests 才會返回 true。
前面貼出的 man find 說明提到,沒有提供除了 -prune 表達式之外的其他 action 類型表達式時,默認會對所有返回 true 的文件名執行 -print 表達式,打印該文件名。
所以打印結果里面只有匹配到的 ./tests 目錄名,那些沒有完全匹配 ./tests 的文件名會返回 false,沒有被打印。
由于 find . 命令打印的目錄名后面沒有加上 / 字符,所以 find . -path ./tests/ 也匹配不到任何文件名,沒有打印任何信息。
類似的,執行 find tests 命令,打印的 tests 目錄名是 tests,那么 find tests -path tests 命令可以完全匹配 tests 模式,打印出這個目錄名。
而 find tests -path ./tests 就匹配不到,沒有打印。
即,根據傳入的目錄參數不同,find 打印的目錄名不同,-path 后面要提供的目錄名也不同。
總的來說,在 -path 后面跟著的目錄名,需要完全匹配 find 命令打印的目錄名,而不是部分匹配。如果不確定 find 命令打印的目錄名是什么,可以先不加 -path 參數執行一次 find 命令,看打印的文件名是什么,再把對應的文件名寫到 -path 參數后面。
在 -path 后面的 pattern 模式可以用通配符匹配特定模式的文件名,常見的通配符是用 * 來匹配零個或多個字符。
在 find 中使用時有一些需要注意的地方,舉例說明如下:
$ find . -path *test*
$ find . -path ./test*
./tests
$ find . -path \*test\*
./tests
./tests/bre.tests
$ find . -path "*test*"
./tests
./tests/bre.tests
可以看到,find . -path *test* 什么都沒有打印,*test* 沒有匹配到 ./tests 這個名稱。
原因是這里的 * 通配符是由 bash 來處理,通過文件名擴展來得到當前目錄下的子目錄名或者文件名,但是不會在目錄名前面加上 ./。
即,這里的 find . -path *test* 相當于 find . -path tests,前面已經說明這是不匹配的。
find . -path ./test* 可以打印出匹配到的目錄名,經過 bash 擴展后,這個命令相當于 find . -path ./tests。
find . -path \*test\* 命令不但匹配到了 ./tests 目錄,還匹配到了該目錄下的 ./tests/bre.tests 文件。
這里用 \* 對 * 進行轉義,對 bash 來說它不再是通配符,不做擴展處理,而是把 * 這個字符傳遞給 find 命令,由 find 命令自身進行通配符處理,可以匹配到更多的文件。
這里面涉及到 bash 和 find 對 * 通配符擴展的區別,bash 在文件名擴展 * 時,遇到斜線字符 / 則停止,不會擴展到目錄底下的文件名。
而 find 沒有把 / 當成特殊字符,會繼續擴展到目錄底下的文件名。
查看 GNU find 在線幫助手冊 https://www.gnu.org/software/findutils/manual/html_mono/find.html#Shell-Pattern-Matching 的說明如下:
Slash characters have no special significance in the shell pattern matching that find and locate do, unlike in the shell, in which wildcards do not match them.
find . -path "*test*" 命令的打印結果跟 find . -path \*test\* 相同。
原因是,bash 沒有把雙引號內的 * 當成通配符,會傳遞這個字符給 find,由 find 來處理通配符擴展。
如果不想用 \* 來轉義,可以用雙引號把模式字符串括起來。
注意:雖然 -path 表達式的名字看起來是對應目錄路徑,但是也能用于匹配文件名,并不只限于目錄。
在 man find 里面提到,有一個 -wholename 表達式和 -path 表達式是等效的,但是只有 GNU find 命令支持 -wholename 表達式,其他版本的 find 命令不支持該表達式。從名字上來說,-wholename 表達式比較準確地表達出要完全匹配文件名稱。
這是一個 operator 操作符,GNU find 在線幫助手冊對該操作符的說明如下:
expr1 expr2
expr1 -a expr2
expr1 -and expr2
And; expr2 is not evaluated if expr1 is false.
可以看到,-and 操作符有三個不同的寫法,都是等效的。
find 命令的操作符把多個表達式組合到一起,成為一個新的組合表達式,組合表達式也會有自身的返回值。
使用 -and 操作符組合的表達式要求兩個表達式都是 true,該組合表達式才是 true。
左邊的 expr1 表達式為 false 時,不再評估右邊的 expr2 表達式,該組合表達式會返回 false。
上面例子的 find . -path tests 命令什么都沒有打印,就跟 -and 操作符的特性有關。
由于該命令沒有提供 action 類型表達式,默認會加上 -print 表達式,也就是 find . -path tests -print。
由于在 -path tests 和 -print 之間沒有提供操作符,默認會加上 -and 操作符,也就是 find . -path tests -and -print。
而 find . 命令搜索到的所有文件名都不匹配 -path tests 模式,都返回 false,基于 -and 操作符的特性,不往下執行 -print 表達式,也就不會打印任何文件名。
這是一個 action 類型表達式,GNU find 在線幫助手冊對該表達式的說明如下:
Action: -prune
If the file is a directory, do not descend into it. The result is true.
For example, to skip the directory src/emacs and all files and directories under it, and print the names of the other files found:
find . -wholename './src/emacs' -prune -o -print
The above command will not print ./src/emacs among its list of results. This however is not due to the effect of the ‘-prune’ action (which only prevents further descent, it doesn’t make sure we ignore that item).
Instead, this effect is due to the use of ‘-o’. Since the left hand side of the “or” condition has succeeded for ./src/emacs, it is not necessary to evaluate the right-hand-side (‘-print’) at all for this particular file.
這里舉的例子就類似于我們現在討論的例子,里面也解釋了查找時能夠忽略目錄的原因,可供參考。
前面提到,find 命令會把搜索到的每一個文件名都依次作為參數傳遞給后面的表達式進行評估。
如果傳遞到 -prune 表達式的文件名是一個目錄,那么不會進入該目錄進行查找。
這個表達式的返回值總是 true。
具體舉例說明如下:
$ find . -path \*test\* -prune
./tests
$ find . -path \*test\* -o -prune
.
前面例子提到,find . -path \*test\* 會匹配到 ./tests 目錄和該目錄下的 ./tests/bre.tests 文件。
而這里的 find . -path \*test\* -prune 只匹配到 ./tests 目錄,沒有進入該目錄下查找文件,就是受到了 -prune 表達式的影響。
基于前面的說明,find . -path \*test\* -prune 相當于 find . -path \*test\* -and -prune -and print。
對于不匹配 \*test\* 模式的文件名,-path \*test\* 表達式返回 false,不往下處理,不打印不匹配的文件名。
對于匹配 \*test\* 模式的文件名,-path \*test\* 表達式返回 true,會往下處理,遇到 -prune 表達式。該表達式總是返回 true,繼續往下處理 -print 表達式,打印出該目錄名。
由于 -prune 表達式指定不進入對應的目錄,所以沒有查找該目錄下的文件,沒有查找到 ./tests/bre.tests 文件。
這是一個 operator 操作符,GNU find 在線幫助手冊對該操作符的說明如下:
expr1 -o expr2
expr1 -or expr2
Or; expr2 is not evaluated if expr1 is true.
使用 -o 操作符組合的表達式要求兩個表達式都是 false,該組合表達式才是 false。
左邊的 expr1 表達式為 true 時,不再評估右邊的 expr2 表達式,該組合表達式會返回 true。
前面提到, find . -path tests 命令什么都沒有打印,跟使用了 -and 操作符有關,如果改成 -o 操作符,結果就會不一樣。
具體舉例如下:
$ find . -path tests -o -print
.
./src
./src/main.c
./tests
./tests/bre.tests
./Makefile.am
$ find . -path ./tests -o -print
.
./src
./src/main.c
./tests/bre.tests
./Makefile.am
可以看到,find . -path tests -o -print 命令打印了當前目錄下的所有文件名。
由于 -path tests 什么都匹配不到,都返回 false,基于 -o 操作符的特性,全都執行后面的 -print 表達式,打印所有文件名。
這個結果跟 find . -path tests 命令完全相反。
類似的,find . -path ./tests -o -print 命令的打印結果跟 find . -path ./tests 命令也相反。
前者的打印結果不包含 ./tests 目錄名,后者的打印結果只包含 ./tests 目錄名。
對于匹配 -path ./tests 模式的目錄名,該表達式返回 true,基于 -o 操作符的特性,不往下執行 -print 表達式,所以不打印該目錄名。
這是一個 action 類型表達式,GNU find 在線幫助手冊對該表達式的說明如下:
Action: -print
True; print the entire file name on the standard output, followed by a newline.
前面例子已經說明過 -print 表達式的作用,它會打印傳遞下來的完整文件路徑名,會自動添加換行符。
如果沒有提供除了 -prune 之外的其他 action 類型表達式,find 默認會加上 -print 表達式,并用 -and 來連接前面的表達式。
這個行為可能會帶來一些誤解,認為 find 命令總是會打印搜索到、或者匹配到的文件名,但有時候搜索到、或者匹配到的文件名反而不打印。
例如上面 find . -path ./tests -o -print 的例子。
要消除這個誤解,就一定要清楚地認識到,find 命令想要打印文件名,就必須執行到 -print 表達式、或者其他可以打印文件名的表達式。
即,要執行可以打印文件名的表達式才會打印文件名,否則不會打印。
至于是匹配特定模式的文件名會打印,還是不匹配特定模式的文件名才會打印,取決于各個表達式、包括操作符組合表達式的判斷結果,看是否會執行到可以打印文件名的表達式。
結合上面的說明,對 find . -path ./tests -and -prune -o -print 命令在查找時能夠忽略 ./tests 目錄底下文件的原因總結如下:
總的來說,使用 find 命令查找時,如果要忽略一個目錄,可以用類似 find . -path ./tests -prune -o -print 這樣的寫法。
理解了上面對該命令的說明后,想要忽略其他模式的目錄,應該就比較容易了。
如果想要忽略多個目錄,要使用 -o 操作符把多個 -path pattern 表達式組合起來。
基于上面例子的目錄結構,舉例如下:
$ find . \( -path ./tests -o -path ./src \) -prune -o -print
.
./Makefile.am
可以看到,find . \( -path ./tests -o -path ./src \) -prune -o -print 命令打印的查找結果里面,沒有 ./src、./tests 這兩個目錄、及其底下文件,也就是忽略了這兩個目錄。
基于 -o 操作符的特性,-path ./tests -o -path ./src 組合表達式在不匹配 ./tests 模式時,會再嘗試匹配 ./src 模式,兩個模式都不匹配,才會返回 false。
由于 -and 操作符優先級高于 -o 操作符,所以要用小括號 () 把 -path ./tests -o -path ./src 組合表達式括起來,形成一個獨立的表達式,再跟后面的 -prune 組合成新的表達式。
小括號在 bash 中有特殊含義,所以要加 \ 轉義字符,寫成 \(,避免 bash 對小括號進行特殊處理。
注意:在 \( 和 \) 前后要用空格隔開,這兩個是單獨的操作符,如果不加空格,會組合成其他名稱。
其他表達式的含義和作用可以參考前面例子的說明。
如果能夠基于這個命令的各個表達式、各個操作符的作用,推導出打印結果,就基本理解 find 命令的工作原理了。
上面說明的 -path pattern 表達式要求完全匹配整個目錄路徑,如果想要只匹配文件名,不包含目錄路徑部分,可以使用 -name pattern 表達式。
這是一個 test 類型表達式,GNU find 在線幫助手冊對該表達式的說明如下:
Test: -name pattern
True if the base of the file name (the path with the leading directories removed) matches shell pattern pattern. As an example, to find Texinfo source files in /usr/local/doc:
find /usr/local/doc -name '*.texi'
Notice that the wildcard must be enclosed in quotes in order to protect it from expansion by the shell.
如這個幫助說明所舉的例子,一般常用這個表達式來匹配特定后綴名的文件。具體舉例如下。
下面是匹配單個后綴名的例子:
$ find . -name '*.c'
./src/main.c
可以看到,find . -name '*.c' 命令打印出所有后綴名為 .c 的文件名。
注意 *.c 要用引號括起來,避免 bash 當 * 號當成通配符處理。
該命令相當于 find . -name '*.c' -and -print,只有 -name '*.c' 表達式返回為 true 的文件名才會執行到 -print 表達式,打印出該文件名。
注意:使用 -name pattern 表達式并不表示只查找符合 pattern 模式的文件,find 命令還是會查找出所給目錄的所有文件,并把每個文件名依次傳給后面的表達式進行評估,只有符合 -name pattern 表達式的文件名才會返回 true,才會被打印出來。
不符合這個表達式的文件也會被查找到,只是沒有打印出來而已。
CSDN 編者按】對于眾多 Android 程序員而言,在需求與應用性能之間,主要精力都會放到新需求的開發。隨著項目復雜度的增加,應用性能越來越低,出現各種問題。程序員們奔波于各種“救火現場”,疲于奔命。本文作者 Aritra Roy 分享了自己在 Android 應用程序開發過程中所遇以及思考,針對內存泄漏提煉出一套可以應用于開發中的方法論。也許會讓你的開發效率事半功倍。
作者 | Aritra Roy,Android 開發者
譯者 | 羅昭成,責編 | 唐小引
封圖 | CSDN 付費下載自東方 IC
出品 | CSDN(ID:CSDNnews)
以下為譯文:
我們都知道,寫一個 Android 的應用很容易,但是要寫一個高性能的應用可就不容易了。以我的個人經驗來說,在 App 的開發過程中,主要的精力都會放在新功能、新模塊、新組件的開發上。
開發過程中,看得見的 UI 比看不見的性能更能吸引我們的目光。所以我強制自己將“優化應用程序(如內存泄漏)”的優先級提高,并養成習慣。
長期以來,不關注性能,帶來了很多的技術債。經過一年多的努力調整, 比起一年前,我有很多的心得體會。
對于很多開發者來說,內存泄漏都是一個老大難的問題。關于處理內存泄漏,你有可能會覺得太難,又或是太費時,又或者是覺得完全沒有意義。但我要告訴你的是,事實并非如此。當你開始處理這些問題的時候,你會發現,這感覺超級棒。
在本篇文章中,我會以盡可能簡單的方式講解這些問題,即使你是一個初學者,也可以學習到如何構建一個高質量、高性能的應用。
垃圾回收
Java 是一個非常強大的語言。在 Android 中,我們幾乎不會像 C / C++ 那樣,手動分配和釋放內存。因為 Java 會自動清理內存。
讓我們來思考一個問題,如果 Java 內建的垃圾回收系統可以在我們不需要的時候自動回收內存,那我們為什么還需要關心內存呢?是因為垃圾回收機制不夠完善嗎?
當然不是,Java 垃圾回收機制當然是完善的。垃圾回收機制是可以正常工作,但是,如果我們的應用程序出現 Bug, 導致垃圾回收器不能正常檢查出不需要的內存塊,就會導致問題。
總體來說,是我們自己的代碼錯誤導致垃圾回收不可用。不可否認,垃圾回收機制是 Java 最偉大的設計之一。
關于垃圾回收器
在處理內存問題之前,你需要了解垃圾回收器的工作原理。它的概念非常簡單,但在它背后,有著極其復雜的邏輯。但是你也別擔心,我們也只關心一些簡單的概念。
如圖所示,Android 或者 Java 應用程序都有一個起點,從對象的初始化,并且調用方法。我們可以認為,這個點就是圖中的 "GC Roots"。有一些對象引用被 GC Roots 直接持有,剩下的則由它們自己創建并持有。
如此,整個引用鏈構成了內存樹。垃圾回收器從 GC roots 開始,直接或間接的鏈接著其它的對象。當整個遍歷結束,還有一些不能被訪問到的對象,就是變成了垃圾,這些對象就會被垃圾回收器回收。
內存泄漏
到現在,你已經知道了垃圾回收的概念,也知道了垃圾回收在 Android 中是如何管理內存的。下面,我們將深入研究一下內存泄漏。
簡單來說,內存泄漏是指你的對象已經使用結束,但是它卻不能被釋放掉。每個對象在完成它自己的生命周期過后,都需要從內存中清理出來。但是如果一個對象被另一個對象直接或間接的持有,垃圾回收器不能檢查出它已經使用結束。朋友們,這樣子就導致了內存泄漏。
值得慶幸的是,我們并不需要太擔心所有的內存泄漏,因為并不是所有的內存泄漏都是有害的。有一些內存泄漏是無關痛癢(只泄漏幾 KB 的內存),并且,在 Android Framwork 層也會有一些內存泄漏,但是你并不需要去修復,因為它們對 App 的性能影響微乎其微,你可以忽略。
但是有一些會引起 App 崩潰, 或者嚴重卡頓。這些都是需要你時刻注意的。
為什么要解決內存泄漏?
沒有人會想使用一個又慢又占內存的應用。如果使用一段時間就會崩潰,你的用戶也會“崩潰”掉,如果長時間出現這樣子的問題,你的用戶會毫不猶豫的卸載掉你的應用,并且再也不會使用它。
如果你的應用中存在內存泄漏,垃圾回收器不能回收不使用的內存,隨著用戶使用時間的增長,內存的占用會越來越多。如此下去,當系統不能在給它分配更多內存的時候,就會導致 OutOfMemoryError, 然后應用程序會崩潰掉。
垃圾回收有利有弊,垃圾回收是一龐大的系統,在應用中,盡可能少的讓垃圾回收器運行,這樣對應用體驗會更好。
隨著你的應用使用的堆內存逐漸增加,Short GC 就會觸發,來保證立即清理無用對象。現在這些快速清理內存的 GC 運行在不同的線程中,這些 GC 不會導致你的應用變慢。
但是如果你的應用中存在嚴重的內存泄漏,Short GC 沒有辦法回收內存,并且占用內存持續增加,這將會導致 Larger GC 被觸發。它會將整個應用程序掛起,阻塞大概 50~100ms,這會導致應用程序變慢并且有可能不能使用。
修復內存泄漏,減少對 App 的影響,給用戶提供更好的體驗。
如何發現內存泄漏?
現在,你已經認識到,你需要修復隱藏在你 App 中的內存泄漏。但是,我們如何才能找到它們呢?
Android Studio 為我們提供了一個非常強大的工具:Monitors。
通過它,你能看到網絡、CPU、GPU、內存的使用情況。
在調試運行 App 的時候,要密切關注內存監視器。內存泄漏的第一個現象就是,在使用的過程中,內存一直增加,不能減少,即使你把 APP 退到后臺也不能釋放。內存分配監視器能夠清楚的看到不同對象所占用的內存,可以清楚的知道哪個對象占用內存較多,需要處理。
但是,它本身還不夠,它需要你指定時間,然后轉存出對應的內存堆。這是一個很無趣的工作。
幸運的是,我們現在已經有更好的方式來實現。LeakCanary, 一個和 App 一起運行的庫,它會在內存泄漏的時候,轉存出內存信息,然后給我們發送一個通知并給我們一個有用的棧信息。
常見的內存泄漏
從我的經驗來看,有很多相似且經常出現內存泄漏的問題,你在你每天的開發中,都有可能會遇到它們。一但你清楚了它們發生的時間、地點、原因 ,你就可以很輕松的修復它們。
未取消的 Listener
很多時候,你在 Activity/Fragment 中注冊了一個 Listener, 但是忘記取消注冊了。如果你的運氣不好,它很可能會引起一個嚴重的內存泄漏問題。一般來說,這些 Listener 的 注冊與取消注冊是同步出現的,在你使用的時候需要注冊,在不使用的時候需要取消注冊。
舉個例子,當我們的應用程序需要獲取定位的時候,需要使用 LocationManager,你會從系統服務中拿到它,并且給其設置一個地理位置更新的回調:
private void registerLocationUpdats {
mManager=(LocationManager) getSystemService(
Context.LOCATION_SERVICE);
mManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
TimeUnit.MINUTES.toMillis(1),
100,
this);
}
在代碼中,可以看出來,使用了 Activity 自己來實現了地理位置更新的回調。LocationManager 會持有這個回調的引用。當你退出了這個頁面,Android 系統會調用 onDestory ,但是垃圾回收器并不能清理掉它,因為 LocationManager 持有它的強引用。
當然,解決方案也很簡單,就是在 onDestory 方法中,取消注冊就可以了。
@Override
public voidonDestroy {
super.onDestroy;
if (mManager !=) {
mManager.removeUpdates(this);
}
}
內部類
內部類在 Java 和 Android 開發中經常用到,非常簡單,但是如果使用不當,也會造成嚴重的內存泄漏。讓我們先來看一個簡單的例子:
public class BadActivity extends Activity {
private TextView mMessageView;
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_bad_activity);
mMessageView=(TextView) findViewById(R.id.messageView);
new LongRunningTask.execute;
}
private class LongRunningTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
// 做一些耗時操作
return "Am finally done!";
}
@Override
protected voidonPostExecute(String result) {
mMessageView.setText(result);
}
}
}
這是一個很簡單的 Activity 頁面,在頁面啟動的時候,在后臺啟動了一個耗時的任務(比如說,復雜的數據庫查詢或者是很慢的網絡)。等到任務執行結束,把拿到的結果顯示到頁面上。看起來,這樣做并沒有問題。事實上,非靜態的內部類會隱式的持有外部類的引用(在這里,就是 Activity)。如果在耗時任務執行完之前,你旋轉屏幕或者退出這個頁面,垃圾回收器就不能從內存中清理掉 Activity 的實例。這個簡單的問題會導致很嚴重的內存泄漏問題。
當然,解決方案也非常地簡單,如下:
public class GoodActivity extends Activity {
private AsyncTask mLongRunningTask;
private TextView mMessageView;
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_good_activity);
mMessageView=(TextView) findViewById(R.id.messageView);
mLongRunningTask=new LongRunningTask(mMessageView).execute;
}
@Override
protected voidonDestroy {
super.onDestroy;
mLongRunningTask.cancel(true);
}
private static class LongRunningTask extends AsyncTask<Void, Void, String> {
private final WeakReference<TextView> messageViewReference;
publicLongRunningTask(TextView messageView) {
this.messageViewReference=new WeakReference<>(messageView);
}
@Override
protected String doInBackground(Void... params) {
String message=;
if (!isCancelled) {
message="I am finally done!";
}
return message;
}
@Override
protected voidonPostExecute(String result) {
TextView view=messageViewReference.get;
if (view !=) {
view.setText(result);
}
}
}
}
正如你看到的代碼,首先我將非靜態內部類改成了靜態內部類,這樣它就不會持有外部類的引用了。當然,使用靜態的內部類,非靜態的變量就不能訪問了。所以我們需要將 TextView 通過構造方法把它傳過去。
在這里,我強烈推薦使用 WeakReference ,它能更好的避免引起內存泄漏。你應該去學習 Java 中關于不同引用類型的知識:
http://javarevisited.blogspot.in/2014/03/difference-between-weakreference-vs-softreference-phantom-strong-reference-java.html
匿名內部類
匿名內部類也是在開發過程中經常使用到的一個東西,它的定義和使用都非常的簡潔。但以我的經驗來看,匿名內部類造成了大量的內存泄漏的問題。
匿名內部類與非靜態內部類相似,造成內部類的原因也和上面說的一樣。你有可能在好多地方都使用了匿名內部類,如果使用不當,會嚴重影響 App 的性能。
public class MoviesActivity extends Activity {
private TextView mNoOfMoviesThisWeek;
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_movies_activity);
mNoOfMoviesThisWeek=(TextView) findViewById(R.id.no_of_movies_text_view);
MoviesRepository repository=((MoviesApp) getApplication).getRepository;
repository.getMoviesThisWeek
.enqueue(new Callback<List<Movie>> {
@Override
public voidonResponse(Call<List<Movie>> call,
Response<List<Movie>> response) {
int numberOfMovies=response.body.size;
mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
}
@Override
public voidonFailure(Call<List<Movie>> call, Throwable t) {
// Oops.
}
});
}
}
上面的例子中,我使用一個常用的網絡庫 Retrofit 發送了一個網絡請求,然后在 TextView 中顯示返回的結果。很明顯,那個 Callback 對象持有 Activity 的引用。如果現在網絡很慢,在網絡響應回來之前,頁面旋轉或者關閉,就會導致 Activity 泄漏。
我強烈建議,在需要的時候,盡量使用靜態的內部類,而非匿名內部類。當然,我的意思不是不在使用匿名內部類,如果你需要使用匿名內部類,你需要注意引起內存泄漏的問題,保證不會出現問題。
Bitmaps
在應用中,你看到的所有圖片都是 Bitmap 對象,包含了所有的像素數據。現在這些 Bitmap 數據非常的大,一個處理不好,就會引起 OOM, 造成 APP 崩潰。在 APP 中使用的圖片資源生成的 Bitmap 會由系統進行管理,但是如果你需要自己處理 Bitmap ,要記住,使用完過后要調用 bitmap.recycle 來釋放資源。
在處理 Bitmap 時,需要將一張大的圖縮放變小過后,在使用,多重用同一個圖片數據。Google 官方有一個關于處理 Bitmap 內存的文檔:
https://developer.android.com/training/displaying-bitmaps/manage-memory.html
Contexts
另一個是關于 Context 的濫用引起的內存泄漏。Activity / Application / Service 都是繼承自 Context 并實現它們自己的功能,但是你也需要搞清楚它們之間的區別,什么是 activity 級別的 Context,什么是 application 級別的 Context,根據項目需求的場景去選擇使用哪一個 Context 。錯誤地使用 Activity Context,導致引用不能被釋放,就會引起內存泄漏。
結語
現在,你知道了什么是垃圾回收器,什么是內存泄漏,內存泄漏給你帶來的影響。你也知道如何檢測和修復內存泄漏。
從現在開始,構建高質量/高性能的應用。處理內存泄漏不僅能讓你的應用有更好的用戶體驗,也能讓你成為更好的開發者。
原文:Everything You Need To Know About Memory Leaks In Android Apps
鏈接:https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859
【END】
*請認真填寫需求信息,我們會在24小時內與您取得聯系。