作者 | 启明星小组
在日常编写代码时难免会遇到各种各样的问题和坑,这些问题可能会影响咱们的开发效率和代码品质,因而咱们须要一直总结和学习,以防止这些问题的呈现。接下来咱们将围绕挪动开发中常见问题做出总结,以进步大家的开发品质。本系列文章讲围绕内存透露、语言开发注意事项等开展。本篇咱们将介绍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)
百度程序员开发避坑指南(挪动端篇)
百度程序员开发避坑指南(前端篇)