iOS -- 关于内存管理的理解

2022-06-24  本文已影响0人  Harry__Li

因为大多数情况都是自己独立开发,所以平常都不会刻意的去检测内存。只有在app发生bug的时候,才去查是不是内存泄露引起的bug问题。这种做法肯定是不可取的,能把问题解决在萌芽,还是要尽早解决。

如何检测内存是否泄露

检查定位内存泄露的方法这里介绍两种.

1.使用Xcode检测

在我们运行项目的时候,xcode会显示当前运行项目的内存大小.


截屏2022-06-24 14.54.08.png

点击Memory 就能看到一个内存使用情况,如果我们要定位内存泄露的代码,需要点击profile in instruments 按钮,这时候就进入到了leaks界面了然后再如下设置


截屏2022-06-20 14.38.01.png
双击代码行就可以定位到xcode内存泄露的代码行了.
2 使用MLeaksFinder

使用xcode leaks更适合大范围的查找,全部的泄漏点.而MLeaksFinder在我们开发过程中,有泄露就会提示出泄露的地方.
在开发过程中,离开当前界面的时候是需要释放掉的.我们可以使用dealloc方法来查看,当离开界面的时候dealloc没有调用,那说明当前的界面没有释放
但是我们定位不到具体是哪出现了内存泄露.使用MLeaksFinder可以帮助定位.
原理:为基类NSObject添加一个方法 willdealoc方法.然后用weak弱指针指向self,在2s后,通过这个弱指针区调用assertNotDealloc.

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}

如果当前界面没有释放,那么self就存在,就回去调用assertNotDealloc方法.这个时候就需要报告泄露的地址了

- (void)assertNotDealloc {
    // 是否已经添加进泄漏对象名单
    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }
    // 添加进内存泄露名单
    [MLeakedObjectProxy addLeakedObject:self];

    NSString *className = NSStringFromClass([self class]);
    NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
}

- (NSSet *)parentPtrs {
    NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
    if (!parentPtrs) {
        parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
    }
    return parentPtrs;
}

通过构造了一个MLeakedObjectProxy对象,并将其加入到leakedObjectPtrs集合中,弹出alert提示框.然后找到引用环.这样就可以具体定位到泄露的地址了.

引起内存泄露的总结

-1. NStime内存泄露

 self.testtimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runbtnclick) userInfo:nil repeats:YES];

-(void)runbtnclick{
    
    NSLog(@"定时器");
    
}
-(void)dealloc{
    NSLog(@"销毁当前界面");
}

运行上面的代码,能发现并没执行dealloc里面的打印.说明当前界面并没有释放. 造成这个的原因是:定时器还一直在走,打印"定时器".当前界面self持有timer,而在timer里面target 又持有了self,所以造成了循环引用,释放不掉.
那应该如何解决呢?

-(void)viewWillDisappear:(BOOL)animated{
    [self.testtimer invalidate];
    self.testtimer = nil;
}
self.testtimer=[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
//        NSLog(@"定时器");
//    }];

这样就不会相互持有.

[configuration.userContentController addScriptMessageHandler:self name:@"openUserAgreement"];
#pragma mark - WKScriptMessageHandler

/*
 1、js调用原生的方法就会走这个方法
 2、message参数里面有2个参数我们比较有用,name和body,
   2.1 :其中name就是之前已经通过addScriptMessageHandler:name:方法注入的js名称
   2.2 :其中body就是我们传递的参数了,我在js端传入的是一个字典,所以取出来也是字典,字典里面包含原生方法名以及被点击图片的url
 */
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    //NSLog(@"%@,%@",message.name,message.body);

    NSDictionary *imageDict = message.body;
    NSString *src = [NSString string];
    if (imageDict[@"imageSrc"]) {
        src = imageDict[@"imageSrc"];
    }else{
        src = imageDict[@"videoSrc"];
    }
    NSString *name = imageDict[@"methodName"];

    //如果方法名是我们需要的,那么说明是时候调用原生对应的方法了
    if ([picMethodName isEqualToString:name]) {
        SEL sel = NSSelectorFromString(picMethodName);

#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
        //写在这个中间的代码,都不会被编译器提示PerformSelector may cause a leak because its selector is unknown类型的警告
        [self performSelector:sel withObject:src];
#pragma clang diagnostic pop
    }else if ([videoMethodName isEqualToString:name]){

        SEL sel = NSSelectorFromString(name);
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"

        [self performSelector:sel withObject:src];
#pragma clang diagnostic pop

    }
}

#pragma mark - WKUIDelegate(js弹框需要实现的代理方法)
//使用了WKWebView后,在JS端调用alert()是不会在HTML中显式弹出窗口,是我们需要在该方法中手动弹出iOS系统的alert的
//该方法中的message参数就是我们JS代码中alert函数里面的参数内容
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
//    NSLog(@"js弹框了");
    UIAlertController *alertView = [UIAlertController alertControllerWithTitle:@"JS-Coder" message:message preferredStyle:UIAlertControllerStyleAlert];

    [alertView addAction:[UIAlertAction actionWithTitle:@"Sure" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        //一定要调用下这个block
        //API说明:The completion handler to call after the alert panel has been dismissed
        completionHandler();
    }]];
    [self presentViewController:alertView animated:YES completion:nil];

}

在上面的代码中是存在相互引用的内存泄露问题的.self持有wkwebview,而在config添加的方法中 又强引用了self,造成了相互引用.这个时候我们需要两个步骤去解决它.
首先,我们需要打断这种引用关系.所以需要用weak来修饰.

#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>

@property (nonatomic,weak)id<WKScriptMessageHandler> scriptDelegate;
- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;
@end

#import "WeakScriptMessageDelegate.h"

@implementation WeakScriptMessageDelegate

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate{
    self = [super init];
    if (self) {
        _scriptDelegate = scriptDelegate;
    }
    return self;
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}


- (void)dealloc {
    
}
@end

通过WeakScriptMessageDelegate类,weak修饰来打破这种相互引用.
其次,在我们离开当前界面的时候,需要把添加的messagehander删除掉.

[_webView.configuration.userContentController removeScriptMessageHandlerForName:@"setappToken"];

这样就可以解决wkwebview的内存问题.

@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>

@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;

@end


@interface QiAnimationButton : UIButton

@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;

- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画
    __weak typeof(self) weakSelf = self;

    [self.operationQueue addOperationWithBlock:^{

        __strong typeof(weakSelf) strongSelf = weakSelf;

        if (completionHandler) {
            
            KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);
            
            completionHandler([strongSelf serialReaderWithRequest:request]);
        }
    }];
for (int i = 0; i<100000; i++) {
        
        AMHomeVC *homeVC = [[AMHomeVC alloc]init];
    }
    
    for (int i = 0; i<100000; i++) {
        @autoreleasepool {
            AMHomeVC *homeVC = [[AMHomeVC alloc]init];
        }
    }

使用autoreleasepool,及时的释放占用的内存.减少内存占用峰值.

上面就是一些内存泄漏的场景.下面我们来看一下内存管理的原理实现.

内存原理
内存分布
 要弄清楚ios内存管理,我们首先需要知道,我们启动一直app进程以后,它的内存是如何划分的.我们在平常的开发中需要注意的是哪块的内存管理.
  代码区:代码段是用来存放可执行文件的操作命令,允许读取操作,不允许写入操作
 全局静态区 ,在程序运行中,此内存一直存在,程序结束后由系统释放
      1.数据区:存放程序静态分配的变量和全局变量
      2 bss区 包含了程序中未初始化全局变量
 常量区 常量存储区,编译时分配,在程序结束后由系统释放
 堆:是由程序员分配和释放的,用于存放进程运行中被动态分配的内存段.现在使用ARC来管理对象,所以不用我们手动是realse对象.
    存储的是oc中alloc或者new开辟出来的空间创建对象
    优点:灵活方便 数据适用广泛   缺点:需要管理,速度慢而且容易产生内存碎片
 
 栈:是由编译器自动分配并释放,用户存放程序临时创建的局部变量.我们是不用管栈上存储,它都是有系统来释放的.
    存储的是局部变量 以及函数的参数
 优点: 快速高效 不会产生内存碎片  优点 数据不灵活 内存大小啊有限制
内存管理机制

在我们启动一个app的时候,有主线程对应的也就有了autoreleasePool.
首先遇到的一个疑问就是,哪些数据会加入到autoreleasePool中?
1.非自己创建的对象:一般是系统提供的类方法/工厂方法.
这又有一个问题,什么是自己创建的对象? 自己创建的对象就是,使用alloc.new、copy、mutablecopy及其驼峰变形allocobject newobject方法创建的对象.就是开发者自己创建需要申请内存的对象.

 NSArray *arr=[[NSArray alloc]init];
 NSArray *testarr=[NSArray array];
 上面的alloc是不会加入到autoreleasePool中,而array创建的会加入到自动释放池中.
 
 2.函数返回值:对象作为方法的返回值   在方法里有局部对象的,除了方法作用于会立刻释放.
 
 那接下来又有一个问题?自己创建的对象放在哪.我理解的是 他们在引用计数表中.在引用计数变为0的时候 被系统释放掉.那么,是不是可以这么总结:在点开一个app的时候,主线程 runloop对应的一个自动释放池.它管理的整个app,加入到autoreleasePool的释放问题.当然我们在项目里肯定会alloc初始化创建各个类,这些类是在引用计数表中的.但在这些类中又有各种方法 各种局部变量,他们是放在不同的autoreleasePool之中管理的.所以类中的autoreleasePool能不能释放 它也关系到这个类最后能不能释放掉.
 
 上面说了app内存管理的管理实现,接下来开始一个一个的说明 指针存储 sidetable列表 autoreleasePool.
Tagged Pointer

苹果引入了Tagged Pointer 用来优化NSNumber,NSDate NSString等小对象的存储,在没有引入之前都是动态创建,根据引用计数来释放的.
而现在NSNumber指针里面存储的数据是:tag+data.也就是把数据直接存储到指针里面了. 我们看下面的两段代码会有一个直观的体验

 dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
         for (int i = 0 ; i < 10000; i ++) {
             dispatch_async(queue, ^{
                 self.name = [NSString stringWithFormat:@"%@",@"123sdfasfdas"];
             });
         }

上面的代码可能崩溃,因为name有可能在调用之前被释放掉.变成僵尸对象,从而引起崩溃.

dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
         for (int i = 0 ; i < 10000; i ++) {
             dispatch_async(queue, ^{
                 self.name = [NSString stringWithFormat:@"%@",@"123"];
             });
         }

这段代码不会崩溃,这是因为他是一个小对象,只是修改指针上面的值就可以了,不存在realse释放这一说.

SideTables哈希列表

除了指针存储数据,还可以用sidetables来存储.散列表中存储的都是各个属性的引用计数.而记录引用计数的又分为了SideTable weak_table_t
SideTable 的两个成员:refcents是一个hash map,其key是objc的地址,而value则是objc对象的引用计数
weaak_table 则存储了弱引用objc的指针地址,其本质是一个以objc地址为key,弱引用objc的指针地址作为value的hash表
关于sidetable先说这么多,后续在慢慢补充吧.

自动释放池

autoreleasePool并不是一个结构体,它是由AutoreleasePoolPage组成的.
下面我们来看一下AutoreleasePoolPage是什么
结构体中包含了
magic_t const magic
id *next
pthread_t const thread
AutoreleasePoolPage *const parent
AutoreleasePoolPage *child
depth
hiwat
结构体中包含了parent child和next,page是以一个双向链表的形式连接在一起的.从网上盗用的图


截屏2022-06-24 14.49.21.png

next是最上面的 当当前的page要满了时,这时要创建新的page,next指向了新page的底部.

再看下面图所示,显示pool释放时候的过程. 截屏2022-06-24 14.49.29.png

要释放的时候,加入一个哨兵对象.哨兵对象就是一个nil,只不过是换了一种形式.
根据传入的哨兵对象地址找到哨兵对象所处的page,
2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次release消息,并向回移动next指针到正确的位置
关于AutoreleasePool和引用列表只是简单的介绍了一下,后续在慢慢的补充.

以上就是一些 个人对内存管理的理解.

上一篇下一篇

猜你喜欢

热点阅读