首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一个优秀程序员不可避免的问题:内存泄漏

一个优秀程序员不可避免的问题:内存泄漏

作者头像
Android技术干货分享
发布于 2019-05-19 08:43:01
发布于 2019-05-19 08:43:01
70600
代码可运行
举报
文章被收录于专栏:Android技术分享Android技术分享
运行总次数:0
代码可运行

前言

内存泄漏,一个说大不大说下不小的瑕疵。作为开发者,我们都很清楚内存泄漏是我们代码问题导致的。但是话说回来,泄漏后果会很严重嘛?这不好说,如果我们不泄漏Bitmap这种大内存的对象,那么修补内存泄漏就像鸡肋一样,“食之无味,弃之可惜”。 就比如说我们项目组,近2000w的DAU,只要不明显影响用户体验,一切以上需求为主…

一、理论

先上一张图:

解释一下这张图,每个Android(或Java)应用程序都有一个起点(GC Root),从这个点中实例化对象、调用方法。。一些对象直接引用GC Root,另一些对象又引用了这些对象。因此,形成了引用链,就像上图一样。因此垃圾收集器从GC Root开始并遍历直接或间接链接到GC Root的对象。在此过程结束时,脱离GC Root的对象/对象链将被回收。

接下来咱们再想另一个问题:

什么是内存泄漏?

有了上图,理解内存泄漏的概念就很简单,说白了就是:长生命周期对象A持有了短生命周期的对象B,那么只要A不脱离GC Root的链,那么B对象永远没有可能被回收,因此B就泄漏了。

有什么危害?

危害的话,如开篇所说。如果泄漏的内存很小,几字节,几kb….对于现在的机器性能,就像星爵打灭霸…“伤害”基本无视。但是如果泄漏的足够多,普通的GC无法回收这些泄漏的内存,那么堆将持续增加,当堆足够大的时候,就会触发“stop-the-world” GC,直接在主线程进行耗时的GC。

主线程进行耗时操作,每一个android开发者都明白这意味着什么….

所以内存泄漏足够严重,其危害还是很严重的。

二、实践

对于我们日常开发来说,有比较多的场景稍不注意就会存在内存泄漏的风险。让我们一起留意一下:

2.1、内部类Inner classes

内部类存在内存泄漏的风险,是一个老生常谈的话题。说白了就是因为我们在new一个内部类时,编译器会在编译时让这个内部类的实例持有外部对象。

这也就是,为啥我们的内部类可以引用到外部类变量、方法的原因。

上段代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class BadActivity extends Activity {

    private TextView mMessageView;

    @Override
    protected void onCreate(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 void onPostExecute(String result) {
            mMessageView.setText(result);
        }
    }
}

大家应该都能看出这里的问题吧。作为非静态内部类的LongRunningTask,会持有BadActivity。并且LongRunningTask是一个长时间任务,也就是说,在这个任务没有完成时,BadActivity是不会被回收的,因此我们的BadActivity就被泄漏了。那么怎么改呢?

解决原理

首先我不能让LongRunningTask持有BadActivity。那么我们需要使用静态内部类(static class)。这样的确不会持有BadActivity,但是问题来了,我们LongRunningTask不持有BadActivity,也就意味着没办法引用到BadActivity中的变量,那么我们的更新UI的操作就做不了,也就是说还是要显示的传一个BadActivity中我们需要的变量进来…但是这样有造成了同样的泄漏问题。

因此,我们需要对传入的变量使用WeakReference进行包一层。但发生GC的时候,告诉GC收集器“我”可以被回收。

上改造后的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class GoodActivity extends Activity {

    private AsyncTask mLongRunningTask;
    private TextView mMessageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_good_activity);
        mMessageView = (TextView) findViewById(R.id.messageView);

        mLongRunningTask = new LongRunningTask(mMessageView).execute();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mLongRunningTask.cancel(true);
    }

    private static class LongRunningTask extends AsyncTask<Void, Void, String> {

        private final WeakReference<TextView> messageViewReference;

        public LongRunningTask(TextView messageView) {
            this.messageViewReference = new WeakReference<>(messageView);
        }

        @Override
        protected String doInBackground(Void... params) {
            String message = null;
            if (!isCancelled()) {
                message = "I am finally done!";
            }
            return message;
        }

        @Override
        protected void onPostExecute(String result) {
            TextView view = messageViewReference.get();
            if (view != null) {
                view.setText(result);
            }
        }
    }
}

2.2、匿名类 Anonymous classes

这一类和2.1很类似。本质都是持有外部对象的引用。

上一段很常见的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MoviesActivity extends Activity {

    private TextView mNoOfMoviesThisWeek;

    @Override
    protected void onCreate(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 void onResponse(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 void onFailure(Call<List<Movie>> call, Throwable t) {
                        // Oops.
                    }
                });
    }
}

2.3、注册Listener

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
SingleInstance.setMemoryLeakListener(new OnMemoryLeakListener(){
    //…..
})

这里写了段很常见的伪码,一个单例的对象,register了一个Listener,并且这个Listener被单例的一个成员变量引用。

OK,那么问题很明显了。单例作为静态变量,肯定是一直存在的。而其内部持有了Listener,而Listener作为一个匿名类,有持有了外部对象的引用。因此这条GC链上的所有对象都不会被释放。

解决也很简单,适当的时机,在单例中将Listener的引用置为null。这样,Listener和单例之间的引用关系断了,Listener链上的所有内容就可以被正常释放掉了。也就是咱们常做的在onDestory()进行unRegisterListener的操作。

类似不注意的内容,还包括Lambda。不过有一点值得注意的,在Kotlin的Lambda中,如果我们没有使用外部对象的变量或者方法,那么Kotlin在编译时,这个Lambda是不会持有外部对象的引用的。也算是Kotlin的一些优化吧

2.4、Contexts

上下文的滥用,也是泄漏的大客户。不过大家针对这类问题应该比较熟悉。

比如:长时间存活的对象,不建议持有Activity的context,而是使用ApplicationContext。如果ApplicationContext没办法完成业务,那么就需要好好考虑一下:这个长时间存活的对象,为什么必须要持有Activity的context。它设计的是否合理,是否它应该是一个长时间存活的对象(比如单例)。

尾声

关于内存泄漏,还是需要咱们平时多注意,对自己写的每一行代码都多思考。毕竟这东西“不是病,但疼起来真要命”。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.05.17 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
[oeasy]python0072_自定义小动物变色_cowsay_color_boxes_asciiart
修改颜色回忆上次内容上次搞的是 颜色 前景颜色 总共有 7 种基本色还有什么 好玩的 么?🤔可以 给小动物 上色 吗?🤔配合先将cowsay结果 输出重定向sudo apt install cowsaycowsay -f turtle "oeasy" > t.py 我想要 更换 所说话的颜色可能么? 先输出重定向 然后封进三引号再加上开头和结尾修改文件解决 行尾转义字符(escaping character)\ :%s/\\$/\\\\/g : 执行命令行模式% 对所有行执行命令s 执行的是替换命令\\
oeasy
2023/02/04
3250
[oeasy]python0072_自定义小动物变色_cowsay_color_boxes_asciiart
Python 基础(四):字符串
在之前的文章【Python 基础(一):入门必备知识】中我们已经提到了:字符串是 Python 的一种数据类型,它可以通过单引号 '、双引号 "、三引号 ''' 或 """ 来定义,本节我们来详细介绍一下。
Python小二
2020/08/17
4590
[oeasy]python0052_ raw格式字符串_单引号_双引号_反引号_ 退格键
转义字符回忆上次内容最近玩的是\n、\r 之外的转义序列 \a是 ␇ (bell)\t是 水平制表符\v是 换行不回车通过 16 进制数值转义 \xhh把(hh)16 进制对应的 ascii 字符输出通过 8 进制数值转义 \ooo把(nnn)8 进制对应的 ascii 字符输出这次加了 转义输出 反斜杠本身 \\ 输出 \总是转义 还是挺麻烦的能否直接输出原样输出呢?搜索帮助找到raw stringrawr的含义是 raw原始原样如果是有r就原样输出为什么raw就是原始原样呢?raw生的食物 没有
oeasy
2023/01/17
1.9K0
[oeasy]python0052_ raw格式字符串_单引号_双引号_反引号_ 退格键
[oeasy]python0079_控制序列_光标位置设置_ESC_逃逸字符_CSI
| 转义序列 | 中文含义 | 英文含义 | ascii序号 | | --- | --- | --- | --- | | \b | 退格 | backspace | 8 | | \t | 制表键 | tab | 9 | | \n | 换行 | line feed | 10 | | \r | 回车 | carriage | 13 |
oeasy
2023/08/05
3580
[oeasy]python0079_控制序列_光标位置设置_ESC_逃逸字符_CSI
[oeasy]python0074_修改字体背景颜色_background_color_背景色
修改背景色回忆上次内容上次将asciiart和颜色一起来玩 7 种基本色变化多端不过到目前为止 改的 都是前景色背景色可以修改吗?重温参数具体动手试试print("\033[40moeasy\33[0mgo")print("\033[41moeasy\33[0mgo")print("\033[42moeasy\33[0mgo")print("\033[43moeasy\33[0mgo")print("\033[44moeasy\33[0mgo")print("\033[45moeasy\33[0mgo"
oeasy
2023/02/07
1.2K0
[oeasy]python0074_修改字体背景颜色_background_color_背景色
Python 字符串
字符串 转义字符串 转义字符 描述 (在行尾时) 续行符 \ 反斜杠符号 ' 单引号 " 双引号 \a 响铃 \b 退格(Backspace) \e 转义 \000 空 \n 换行 \v 纵向制表符 \t 横向制表符 \r 回车 \f 换页 \oyy 八进制数,yy代表的字符,例如:\o12代表换行 \xyy 十六进制数,yy代表的字符,例如:\x0a代表换行 \other 其它的字符以普通格式输出
hankleo
2020/09/16
3820
[oeasy]python0072_修改字体前景颜色_foreground_color_font
修改颜色回忆上次内容m 可以改变字体样式 0-9 之间设置的都是字体效果0 重置为默认1 变亮2 变暗3 斜体4 下划线5 慢闪6 快闪7 前景背景互换8 隐藏9 中划线叠加效果 \33[1;3moeasy;分割取消效果 21 取消 122 取消 223 取消 3一直到 290 是全部取消,回到默认最后发现 真的可以 设置颜色???👁颜色是重要的不同颜色 可以提示出 信息重要性的级别颜色本身也是信息 OFF_INT = 2147483647ERROR_INT = 40000WARN_INT = 3000
oeasy
2023/02/03
8930
[oeasy]python0072_修改字体前景颜色_foreground_color_font
[oeasy]python0037_字符画艺术_asciiview_自制小动物_imagick_asciiart
​牛说(cowsay)回忆上次内容 我们狂飙了一路 从用shell 直接执行 python程序到用shell 循环执行 python程序 循环体中 把 python的 输出结果 用管道 交给了 figlet 把 figlet的 输出结果 用管道 交给了 cowsay 把 cowsay的 输出结果 用管道 交给了 lolcat 最后 提权 直接运行 shell程序 这一路真的好远啊! python3 是脚本解释器shell 也是脚本解释器 其实我们是 在shell中 利用 python3的 输出结果用she
oeasy
2023/01/03
9490
[oeasy]python0037_字符画艺术_asciiview_自制小动物_imagick_asciiart
[oeasy]python0066_控制序列_光标位置设置_ESC_逃逸字符_CSI
光标位置回忆上次内容上次讲了 三引号的输出三引号中 回车和引号 都会 被原样输出\ 还是需要从 \\转义黑暗森林 快被摸排清了 还有哪个 转义序列 没 研究过吗?🤔\e是 干什么的?🤔回忆转义转义转义 转化含义\反斜杠(backslash)加了之后字符就不是原来的意思了 转义么转义转义 转化含义所以\反斜杠这个字符 也叫做转义字符Escape character\b 这两个字符的序列算是一个转义序列 Escape sequence\ 这个转义字符会让 \b转义序列 转义为 Backspace 这个含义B
oeasy
2023/01/29
1.6K0
[oeasy]python0066_控制序列_光标位置设置_ESC_逃逸字符_CSI
[oeasy]python0053_ 续行符_line_continuation_python行尾续行
续行符与三引号回忆上次内容上次还是转义序列类型英文符号\abell响铃\bbackspace退格\ttab水平制表符\vvertical tab垂直制表符换行不回车\\backslash反斜杠\"double quote双引号\’single quote单引号\xhh具体字符输出(hh)16 进制对应的ascii 字符\ooo具体字符输出(nnn)8 进制对应的ascii 字符黑暗森林已经渐渐清晰上图中提到的续行符 line continuation character 是哪个字符呢?神奇的-反斜杠\\是
oeasy
2023/01/18
1.2K0
[oeasy]python0053_ 续行符_line_continuation_python行尾续行
[oeasy]python0071_字符串类型_str_string_下标运算符_中括号
oeasy
2023/07/07
1850
[oeasy]python0071_字符串类型_str_string_下标运算符_中括号
[oeasy]python0040_换行与回车的不同_通用换行符_universal_newlines
换行回车回忆上次内容区分概念 terminal终端 主机网络中 最终的 端点TeleTYpewriter 电传打印机终端硬件shell 终端硬件基础上的 软件壳子Console 控制台 主机旁边 的 控制面板存储文件 的 时候 我 在文件里 打了回车\n系统 将0x0a存入字节 进文件换行 自动就有 回车功能 了吗?🤔回忆一下 被忽略的 \r\r \r 也是一个 ascii字符 \是 转义字符\r是 转义序列 什么是 转义 呢? 转义转义 转化含义把原来 两个字符 : \和r转化为 \r 这样一个字符 没错
oeasy
2023/01/07
3.9K0
[oeasy]python0040_换行与回车的不同_通用换行符_universal_newlines
Python转义字符
有时我们并不想让转义字符生效,我们只想显示字符串原来的意思,这就要用r和R来定义原始字符串。如:
狼啸风云
2019/03/20
4.2K0
[oeasy]python0043_八进制_oct_octal_october_octave
八进制(oct)回忆上次内容什么是 转义? 转义转义 转化含义\ 是 转义字符\n、\r是 转义序列还有什么 转义序列 吗? \a是 响铃\b 退格键\t 水平制表符 tab键\v、\f 实现喂纸不回车通过 16进制数值 转义 \xhh输出 (hh)16进制对应的ascii字符如果我们不输入x 会发生什么呢?为什么会输出 S 呢?🤔查询文档查询主题 STRINGS查询结果表示方法 \xhh 是 16进制 表示方法\ooo 是 8进制 表示方法去试试从 16进制 到 8进制16进制表示法 没有问题那
oeasy
2023/01/10
4030
[oeasy]python0043_八进制_oct_octal_october_octave
[oeasy]python0022_框架标题的制作_banner_结尾字符串_end
​结尾字符串(end)回忆上次内容​python3​​ 的程序是一个 5.3M 的可执行文件​​python3​​ 里面存的是 cpu 指令可以执行的那种我们可以把指令对应的汇编找到​​objdump -d ~/python3 > python3.asm​​汇编语句是和当前机器架构的指令集相关的​​uname -a​​可以查询指令集我们执行的过程其实是系统先执行​​python3​​这个可执行文件在内存中构建解释器将参数​​hello.py​​ 放入解释器​python3​​解释器 对于​​hello.py
oeasy
2022/12/01
6260
[oeasy]python0022_框架标题的制作_banner_结尾字符串_end
【愚公系列】2021年12月 Python教学课程 04-字符串
字符串是 Python 中最常用的数据类型之一,使用单引号或双引号来创建字符串,使用 三引号创建多行字符串。 Python 不支持单字符类型,单字符在 Python 中也是作为一个字符串使用。 字符串是不可变的序列数据类型,不能直接修改字符串本身,和数字类型一样! 虽然字符串本身不可变,但可以通过方括号加下标的方式,访问或者获取它的子串,当 然也包括切片操作。这一切都不会修改字符串本身,当然也符合字符串不可变的原则。
愚公搬代码
2021/12/13
4680
【愚公系列】2021年12月 Python教学课程 04-字符串
笨办法学Python - 习题8-10:
注意:上述代码说明两个点,一个是%r 的作用,是占位符,可以将后面给的值按原数据类型输出(不会变),支持数字、字符串、列表、元组、字典等所有数据类型。
py3study
2020/02/10
5990
Python3基础数据-字符串
字符串是 Python 中最常用的数据类型。我们可以使用引号('或")来创建字符串。 创建字符串很简单,只要为变量分配一个值即可。例如:
用户5522200
2019/06/02
6370
[oeasy]python0074_设置高亮色_color_highlight_ansi_控制终端颜色
更多颜色回忆上次内容上次我们搞的还是颜色 FG foreground 前景色 30-37BG background 背景色 40-47这些 都可以和字体样式 结合起来难道 就这几种颜色 吗??🤔有点少啊!有些颜色 也和想象不一致 金黄色 也不够黄啊?!明确概念\是 转义字符escape character\和其他字符 可以构成转义序列\t\n\r\e 也是转义序列 \e 这个转义序列转义到 escape 这样的状态从正常的输出退出进入 control sequences 控制序列控制序列 不直接输出到屏
oeasy
2023/02/08
6020
[oeasy]python0074_设置高亮色_color_highlight_ansi_控制终端颜色
第五讲:Python数据类型之String
在Python 中,字符串是最常用的数据类型,我们可以使用引号(‘或”)来创建字符串。
Wu_Candy
2022/07/04
5240
第五讲:Python数据类型之String
推荐阅读
相关推荐
[oeasy]python0072_自定义小动物变色_cowsay_color_boxes_asciiart
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验