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小时内与您取得联系。