写点什么

百度工程师移动开发避坑指南——内存泄漏篇

作者:百度Geek说
  • 2023-05-17
    上海
  • 本文字数:6354 字

    阅读完需:约 21 分钟

百度工程师移动开发避坑指南——内存泄漏篇

作者 | 启明星小组


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

一、Android 端

内存泄漏(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, 0, 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, 0, 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 端

目前我们已经有了 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];});
复制代码


----------  END  ----------


推荐阅读【技术加油站】系列:


百度程序员开发避坑指南(Go语言篇)


百度程序员开发避坑指南(3)


百度程序员开发避坑指南(移动端篇)


百度程序员开发避坑指南(前端篇)



发布于: 刚刚阅读数: 4
用户头像

百度Geek说

关注

百度官方技术账号 2021-01-22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
百度工程师移动开发避坑指南——内存泄漏篇_ios_百度Geek说_InfoQ写作社区