移动开发避坑指南——内存泄漏

news2024/11/16 1:51:37

在日常编写代码时难免会遇到各种各样的问题和坑,这些问题可能会影响我们的开发效率和代码质量,因此我们需要不断总结和学习,以避免这些问题的出现。接下来我们将围绕移动开发中常见问题做出总结,以提高大家的开发质量。本系列文章讲围绕内存泄漏、语言开发注意事项等展开。本篇我们将介绍Android/iOS常见的内存泄漏问题。

一、Android端

GEEK TALK

内存泄漏(Memory Leak),简单说就是不再使用的对象无法被GC回收,占用内存无法释放,导致应用占用内存越来越多,内存空间不足而出现OOM崩溃;另外因为内存可用空间变少,GC更加频繁,更容易触发FULL GC,停止线程工作,导致应用卡顿。

Android应用程序中的内存泄漏是一种常见的问题,以下是一些常见的Android内存泄漏:

1.1 匿名内部类

匿名内部类持有外部类的引用,匿名内部类对象泄露,从而导致外部类对象内存泄漏,常见Handler、Runnable匿名内部类,持有外部Activity的引用,如果Activity已经被销毁,但是Handler未处理完消息,导致Handler内存泄露,从而导致Activity内存泄露。

示例1:

public class TestActivity extends AppCompatActivity {
    private static final int FINISH_CODE = 1;
    private Handler handler = new Handler() {        @Override        public void handleMessage(@NonNull Message msg) {            if (msg.what == FINISH_CODE) {                TestActivity.this.finish();            }        }    };
    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        handler.sendEmptyMessageDelayed(FINISH_CODE, 60000);    }}

示例2:

public class TestActivity extends AppCompatActivity {
    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        new Handler().postDelayed(new Runnable() {            @Override            public void run() {                TestActivity.this.finish();            }        }, 60000);    }}

示例1和示例2均为简单计时一分钟关闭页面,如果页面在之前被主动关闭销毁,Handler中仍有消息等待执行,就存在到Activity的引用链,导致Activity销毁后无法被GC回收,造成内存泄露;示例1为Handler匿名内部类,持有外部Activity引用:主线程 —> ThreadLocal —> Looper —> MessageQueue —> Message —> Handler —> Activity;示例2为Runnable匿名内部类,持有外部Activity引用:Message —> Runnable —> Activity.

修复方法1:主要针对Handler,在Activity生命周期移除所有消息。

    @Override    protected void onDestroy() {        super.onDestroy();        handler.removeCallbacksAndMessages(null);    }

修复方法2:静态内部类+弱引用,去掉强引用关系,可以修复类似匿名内部类造成内存泄露。

    static class FinishRunnable implements Runnable {
        private WeakReference<Activity> activityWeakReference;
        FinishRunnable(Activity activity) {            activityWeakReference = new WeakReference<>(activity);        }
        @Override        public void run() {            Activity activity = activityWeakReference.get();            if (activity != null) {                activity.finish();            }        }    }        new Handler().postDelayed(new FinishRunnable(TestActivity.this), 60000);

1.2 单例/静态变量

单例/静态变量持有Activity的引用,即使Activity已经被销毁,它的引用仍然存在,从而导致内存泄漏。

示例:

    static class Singleton {
        private static Singleton instance;
        private Context context;
        private Singleton(Context context) {            this.context = context;        }
        public static Singleton getInstance(Context context) {            if (instance == null) {                instance = new Singleton(context);            }            return instance;        }    }        Singleton.getInstance(TestActivity.this); 

调用示例中的单例,传递Context参数,使用Activity对象,即使Activity销毁,也一直被静态变量Singleton引用,导致无法回收造成内存泄露。

修复方法:

Singleton.getInstance(Application.this);

尽量使用Application的Context作为单例参数,除非一些需要需要Activity的功能,比如显示Dialog,如果非要使用Activity作为单例参数,可以参考匿名内部类修复方法,在合适时机比如Activity的onDestroy生命周期释放单例,或者使用弱引用持有Activity。

1.3 监听器

示例: EventBus注册监听未解绑,导致注册到EventBus一直被引用,无法回收。

public class TestActivity extends AppCompatActivity {        @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        EventBus.getDefault().register(this);    }}

修复方法: 在对应注册监听的生命周期解绑,onCreate对应onDestroy。

    @Override    protected void onDestroy() {        super.onDestroy();        EventBus.getDefault().unregister(this);    }

1.4 文件/数据库资源

示例: 打开文件数据库或者文件,发生异常,未关闭,导致资源一直存在,导致内存泄漏。

    public static void copyStream(File inFile, File outFile) {        try {            FileInputStream inputStream = new FileInputStream(inFile);            FileOutputStream outputStream = new FileOutputStream(outFile);            byte[] buffer = new byte[1024];            int len;            while ((len = inputStream.read(buffer)) != -1) {                outputStream.write(buffer, , len);            }        } catch (IOException e) {            e.printStackTrace();        }    }

修复:在finally代码块中关闭文件流,保证发生异常后一定能执行到

    public static void copyStream(File inFile, File outFile) {        FileInputStream inputStream = null;        FileOutputStream outputStream = null;        try {            inputStream = new FileInputStream(inFile);            outputStream = new FileOutputStream(outFile);            byte[] buffer = new byte[1024];            int len;            while ((len = inputStream.read(buffer)) != -1) {                outputStream.write(buffer, , len);            }        } catch (IOException e) {            e.printStackTrace();        } finally {            close(inputStream);            close(outputStream);        }    }
    public static void close(Closeable closeable) {        if (closeable != null) {            try {                closeable.close();            } catch (Exception e) {                e.printStackTrace();            }        }    }

1.5 动画

示例: Android动画未及时取消释放动画资源,导致内存泄露。

public class TestActivity extends AppCompatActivity {
    private ImageView imageView;    private Animation animation;
    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_test);
        imageView = (ImageView) findViewById(R.id.image_view);        animation = AnimationUtils.loadAnimation(this, R.anim.test_animation);        imageView.startAnimation(animation);    }}

修复: 在页面退出销毁时取消动画,及时释放动画资源。

@Overrideprotected void onDestroy() {    super.onDestroy();    if (animation != null) {        animation.cancel();        animation = null;    }}

二、IOS端

GEEK TALK

目前我们已经有了ARC(自动引用计数)来替代MRC(手动引用计数),申请的对象在没有被强引用时会自动释放。但在编码不规范的情况下,引用计数无法及时归零,还是会存在引入内存泄露的风险,这可能会造成一些非常严重的后果。以直播场景举例,如果直播业务的ViewController无法释放,会导致依赖于ViewController的点位统计数据异常,且用户关闭直播页面后仍然可以听到直播声音。熟悉内存泄漏场景、养成避免内存泄露的习惯是十分重要的。下面介绍一些iOS常见内存泄漏及解决方案。

2.1 block引起的循环引用

block引入的循环引用是常见的一类内存泄露问题。常见的引用环是对象->block->对象,此时对象和block的引用计数均为1,无法被释放。

[self.contentView setActionBlock:^{    [self doSomething];}];

例子代码中,self强引用成员变量contentView,contentView强引用actionBlock,actionBlock又强引用了self,引入内存泄露问题。

解除循环引用,就是解除强引用环,需要将某一强引用替换为弱引用。如:

__weak typeof(self) weakSelf = self;[self.contentView setActionBlock:^{    __strong typeof(weakSelf) strongSelf = weakSelf;    [strongSelf doSomething];}];

此时actionBlock弱引用self,循环引用被打破,可以正常释放。

或者使用RAC提供的更简便的写法:

@weakify(self);[self setTaskActionBlock:^{    @strongify(self);    [self doSomething];}];

需要注意的是,可能和block存在循环引用的不仅仅是self,所有实例对象都有可能存在这样的问题,而这也是开发过程中很容易忽略的。比如:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {    Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];    @weakify(self);    cell.clickItemBlock = ^(CellModel * _Nonnull model) {        @strongify(self);        [self didSelectRowMehod:model tableView:tableView];    };    return cell;}

这个例子中,self和block之间的循环引用被打破,self可以正常释放了,但是需要注意的是还存在一条循环引用链:tableView强引用cell,cell强引用block,block强引用tableView。这同样会导致tableView和cell无法释放。

正确的写法为:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {    Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];    @weakify(self);    @weakify(tableView);    cell.clickItemBlock = ^(CellModel * _Nonnull model) {        @strongify(self);        @strongify(tableView);        [self didSelectRowMehod:model tableView:tableView];    };    return cell;}

2.2 delegate引起的循环引用

@protocol TestSubClassDelegate <NSObject>
- (void)doSomething;
@end
@interface TestSubClass : NSObject
@property (nonatomic, strong) id<TestSubClassDelegate> delegate;
@end
@interface TestClass : NSObject <TestSubClassDelegate>
@property (nonatomic, strong) TestSubClass *subObj;
@end

上述例子中,TestSubClass对delegate使用了strong修饰符,导致设置代理后,TestClass实例和TestSubClass实例相互强引用,造成循环引用。大部分情况下,delegate都需要使用weak修饰符来避免循环引用。

2.3 NSTimer强引用

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];[NSRunLoop.currentRunLoop addTimer:self.timer forMode:NSRunLoopCommonModes];

NSTimer实例会强引用传入的target,就会出现self和timer的相互强引用。此时必须手动维护timer的状态,在timer停止或view被移除时,主动销毁timer,打破循环引用。

解决方案1:换用iOS10后提供的block方式,避免NSTimer强引用target。

@weakify(self);self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {    @strongify(self);    [self doSomething];}];

解决方案2:使用NSProxy解决强引用问题。

// WeakProxy@interface TestWeakProxy : NSProxy
@property (nullable, nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation TestWeakProxy
- (instancetype)initWithTarget:(id)target {    _target = target;    return self;}
+ (instancetype)proxyWithTarget:(id)target {    return [[TestWeakProxy alloc] initWithTarget:target];}
- (void)forwardInvocation:(NSInvocation *)invocation {    if ([self.target respondsToSelector:[invocation selector]]) {        [invocation invokeWithTarget:self.target];    }}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {    return [self.target methodSignatureForSelector:aSelector];}
- (BOOL)respondsToSelector:(SEL)aSelector {    return [self.target respondsToSelector:aSelector];}
@end
// 调用self.timer = [NSTimer timerWithTimeInterval:1 target:[TestWeakProxy proxyWithTarget:self] selector:@selector(doSomething) userInfo:nil repeats:YES];

2.4 非引用类型内存泄漏

ARC的自动释放是基于引用计数来实现的,只会维护oc对象。直接使用C语言malloc申请的内存,是不被ARC管理的,需要手动释放。常见的如使用CoreFoundation、CoreGraphics框架自定义绘图、读取文件等操作。

如通过CVPixelBufferRef生成UIImage:

CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);CIImage* bufferImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];CIContext *context = [CIContext contextWithOptions:nil];CGImageRef frameCGImage = [context createCGImage:bufferImage fromRect:bufferImage.extent];UIImage *uiImage = [UIImage imageWithCGImage:frameCGImage];CGImageRelease(frameCGImage);CFRelease(sampleBuffer);

2.5 延迟释放问题

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{    [self doSomething];});

上述例子中,使用dispatch_after延迟20秒后执行doSomething方法。这并不会造成self对象的内存泄漏问题。但假设self是一个UIViewController,即使self已经从导航栈中移除,不需要再使用了,self也会在block执行后才会被释放,造成业务上出现类似内存泄露的现象。

在这种长时间的延时执行中,好也加入weakify-strongify对,避免强持有。

@weakify(self);dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{    @strongify(self);    [self doSomething];});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1595516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

爬虫的目的是做什么

通过网站域名获取HTML数据解析数据&#xff0c;获取想要的信息存储爬取的信息如果有必要&#xff0c;移动到另一个网页重复过程 这本书上的代码的网址是 &#xff1a; GitHub - REMitchell/python-scraping: Code samples from the book Web Scraping with Python http://shop.…

低代码集成Java系列:高效构建自定义插件

前言 随着软件开发的快速发展和需求的不断增长&#xff0c;开发人员面临着更多的压力和挑战。传统的开发方法需要花费大量的时间和精力&#xff0c;而低代码开发平台的出现为开发人员提供了一种更加高效、快速的开发方式。今天小编就以构建命令插件为例&#xff0c;展示如何使…

不要顺从胃的指示进食

没有人喜欢一直胖着&#xff0c;但想瘦&#xff0c;运动力、运动量、毅力、耐力、坚持、饮食管控方面等都不可缺&#xff0c;道理懂得都懂&#xff0c;但大多数超重胖子却都是有心而无力的。原因&#xff0c;除了生理体积影响了行动外&#xff0c;更重要的一点是&#xff1a;由…

汽车抗疲劳驾驶测试铸铁试验底座技术要求有哪些

铸铁平台试验台底座的主要技术参数要求 1、 试验台底座设计制造符合JB/T794-1999《铸铁平板》标准。 2、 试验铁底板及所有附件的计量单位全部采用 单位&#xff08;SI&#xff09;标准。 3、铸铁平台平板材质&#xff1a;用细密的灰口铸铁HT250或HT200&#xff0c;强度符…

Mysql的事务隔离级别以及事务的四大特性。

MySQL 的事务隔离级别是数据库管理系统中的一个重要概念&#xff0c;它决定了事务如何隔离和影响其他并发事务。MySQL 支持四种事务隔离级别&#xff0c;分别是&#xff1a;读未提交&#xff08;READ UNCOMMITTED&#xff09;、读已提交&#xff08;READ COMMITTED&#xff09;…

Collection与数据结构 二叉树(二):二叉树精选OJ例题(上)

1. 判断是否为相同的二叉树 OJ链接 public boolean isSameTree(Node p, Node q) {if (p null && q ! null || p ! null && q null){//结构不同return false;}if (p null && q null){//结构相同,都是空树return true;}if (p.value ! q.value){//…

STC89C52学习笔记(十二)

STC89C52学习笔记&#xff08;十二&#xff09; 一、AD/DA 1.定义 AD能够将模拟信号转化为数字信号&#xff0c;DA能够将数字信号转化为模拟信号。 2.两种类型的DA转换器 &#xff08;1&#xff09;PWM型DA滤波器 由于PWM是通过脉冲调制的方法来调整的&#xff0c;低通滤…

【数字IC/FPGA】什么是无符号数?什么是有符号数?

进制 虽然在日常生活中&#xff0c;我们已经习惯了使用10进制数字&#xff0c;但在由数字电路构成的数字世界中&#xff0c;2进制才是效率更高的选择。 10进制与2进制 10进制&#xff08;decimal&#xff09;计数法&#xff08;一般也叫阿拉伯计数法&#xff09;是在日常生活…

C++ | Leetcode C++题解之第19题删除链表的倒数第N个结点

题目&#xff1a; 题解&#xff1a; class Solution { public:ListNode* removeNthFromEnd(ListNode* head, int n) {ListNode* dummy new ListNode(0, head);ListNode* first head;ListNode* second dummy;for (int i 0; i < n; i) {first first->next;}while (fi…

UE4_导入内容_骨架网格体

FBX 导入支持 骨架网格体&#xff08;Skeletal Mesh&#xff09; 。这提供了一种简化的处理流程来将有动画的网格体从 3D应用程序中导入到虚幻引擎内&#xff0c;以便在游戏中使用。除了导入网格体外&#xff0c;如果需要&#xff0c;动画和变形目标都可以使用FBX格式 在同一文…

IDA导入jni.h头文件步骤

源地址&#xff1a;https://www.ctvol.com/asreverse/2273.html 导入步骤1&#xff1a; 点击IDA Pro 菜单项“File->Load file->Parse C header file ” 选择jni.h头文件。 导入步骤2&#xff1a; 1、点击IDA Pro 主界面上的“Structures”选项卡。 2、按下Insert键…

为什么会有c++内存模型

1. 引言 c的内存模型主要解决的问题是多线程的问题。怎么理解多线程呢&#xff1f;单核时候&#xff0c;只有1个CPU内核处理多线程&#xff0c;各线程之间随着时间的推进&#xff0c;会不断的切换&#xff0c;如下图形便于理解。 实际上线程间的切换是非常快的&#xff0c;所以…

OpenHarmony实战开发-异步并发概述 (Promise和async/await)。

Promise和async/await提供异步并发能力&#xff0c;是标准的JS异步语法。异步代码会被挂起并在之后继续执行&#xff0c;同一时间只有一段代码执行&#xff0c;适用于单次I/O任务的场景开发&#xff0c;例如一次网络请求、一次文件读写等操作。 异步语法是一种编程语言的特性&…

信息系统项目管理师——管理类计算

风险管理——风险曝光度 风险曝光度概率*影响&#xff0c;概率指风险发生的概率&#xff0c;影响指风险一旦发生&#xff0c;受到影响的项。 题号【GX20061101](61) 知识点[风险曝光度] 风险的成本估算完成后&#xff0c;可以针对风险表中每个风险计算其风险曝光度。某软件小…

h5 笔记4 表格与表单

<table></table>设置表格&#xff1b; <tr></tr>设置行数&#xff1b; <td></td>设置列数&#xff1b; <caption></caption>设置表格标题&#xff1b; <th></th>设置列标题。 直列&#xff1a;column&#xf…

LeetCode 678——有效的括号字符串

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 需要两个栈&#xff0c;一个用来保存左括号所在的位置索引&#xff0c;一个用来保存星号所在的位置索引。 从左往右遍历字符串&#xff0c;如果是左括号或者星号&#xff0c;则将位置索引分别入栈&#xff0c;如…

【网站项目】面向企事业单位的项目申报小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

精通技术写作:如何写出高质量技术文章?

CSDN 的朋友你们好&#xff0c;我是未来&#xff0c;今天给大家带来专栏【程序员博主教程&#xff08;完全指南&#xff09;】的第 7 篇文章“如何撰写高质量技术文章”。本文深入探讨了如何写好一篇技术文章。文章给出了好的技术文章的定义和分析&#xff0c;并提供了从选题、…

02 MySQL 之 DQL专题

3. 数据库中仅有月薪字段&#xff08;month_salary&#xff09;&#xff0c;要求查询所有员工的年薪&#xff0c;并以年薪(year_salary)输出&#xff1a; 分析&#xff1a; 查询操作中&#xff0c;字段可以参与数学运算as 起别名&#xff0c;但实际上可以省略 #以下两句效果…

202209青少年软件编程(scratch图形化) 等级考试试卷(四级)

第1题&#xff1a;【 单选题】 运行下列程序&#xff0c;说法正确的是&#xff1f;&#xff08;&#xff09; A:列表中的数字全部小于11 B:列表的长度为 10 C:变量i最终值为 20 D:列表中有大于 10 的数字 【正确答案】: D 【试题解析】 : 程序运行后&#xff0c;变量i最…