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

因为大多数情况都是自己独立开发,所以平常都不会刻意的去检测内存。只有在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,所以造成了循环引用,释放不掉.
那应该如何解决呢?

  • (1) 在退出当前面时,把它至为空
-(void)viewWillDisappear:(BOOL)animated{
    [self.testtimer invalidate];
    self.testtimer = nil;
}
  • (2)可以使用block来实现
self.testtimer=[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
//        NSLog(@"定时器");
//    }];

这样就不会相互持有.

  • 2 wkwebview和js交互
    项目中,wkwebview和H5需要交互.这个时候就会用到
[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的内存问题.

  • 3 代理delegate
    如果代理用strong修饰,self会强引用view,view强引用delegate.相互引用造成了内存泄露
@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; //!< 最后的修改动画
    1. block循环引用
      并不是所有的block都能引发循环引用的.
      如果blick被当前self持有,这是在block内部我们去调用self持有的方法或者属性,这时就造成了循环引用.
    __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]);
        }
    }];
  • 5 . 加载大图
    加载大图或者多个图片的时候,使用imagenamed方法使用了系统缓存,会长时间占用内存.而使用imagewithcontentsoffile加载本地文件 不会有缓存.
    1. 循环中对象占用内存
      在循环次数比较大的时候,循环生成的对象占用内存较大.产生大量的临时对象,直到循环结束才释放.可能导致内存泄露
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开辟出来的空间创建对象
    优点:灵活方便 数据适用广泛   缺点:需要管理,速度慢而且容易产生内存碎片
 
 栈:是由编译器自动分配并释放,用户存放程序临时创建的局部变量.我们是不用管栈上存储,它都是有系统来释放的.
    存储的是局部变量 以及函数的参数
 优点: 快速高效 不会产生内存碎片  优点 数据不灵活 内存大小啊有限制
僵尸对象

一个OC对象引用计数为0被释放后就变成僵尸对象了,僵尸对象的内存已经被系统回收,虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用,它的内存是随时可能被别的对象申请而占用的。

野指针

指向僵尸对象(不可用内存)的指针
给野指针发消息会报EXC_BAD_ACCESS错误

空指针

没有指向存储空间的指针(里面存的是nil, 也就是0)

给空指针发消息是没有任何反应的

为了避免野指针错误的常见办法

在对象被销毁之后, 将指向对象的指针变为空指针

内存管理机制

在我们启动一个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释放这一说.
总结:
Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString),小对象不在是简单的地址,而是地址+值。所以它不在是一个对象,可以当成是一个普通的变量。能够直接进行读取。
小对象不会进行retain和release,而是直接返回,不需要ARC进行管理,是由系统自主的进行释放和回收的。
Tagged Pointer的内存并不在堆上,而是在常量区。也不需要malloc。所以它比在堆上读取更加的高效。
Tagged Pointer的内存管理方式,要比常规的管理方式快的多。

SideTables哈希列表

除了指针存储数据,还可以用sidetables来存储.散列表中存储的都是各个属性的引用计数.而记录引用计数的又分为了SideTable weak_table_t
SideTable 的两个成员:refcents是一个hash map,其key是objc的地址,而value则是objc对象的引用计数
weaak_table 则存储了弱引用objc的指针地址,其本质是一个以objc地址为key,弱引用objc的指针地址作为value的hash表

1.散列表为什么是多张的?
如果散列表只有一张表,意味着全局所有的对象都会存储在一张表,需要不停地开锁解锁。在开锁是,由于所有数据都在表上,则数据就不安全了。
那如果每个对象都开一张表,会耗费性能。
2.为什么是散列表,而不是数组或者链表。
数组:查询方便,通过下标就可以查询。但增删比较麻烦。
链表:增删方便,查询不方便。
散列表:哈希表集合了数组和链表的长处,增删改查都比较方便
关于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指针到正确的位置
总结:
1.自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储页,每一个AutoreleasePoolPage都是以双向链表的形式连接的。
2.自动释放池的压栈和出栈主要是通过结构体的够站函数,实际上是调用的AutoreleasePoolPage的push和pop两个方法。
3.调用push操作其实就是创建一个新的AutoreleasePoolPage,具体操作就是 插图一个哨兵对象POOL_BOUNDARY,并返回POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况:

a.当page存在,且不满时,调用add方法将对象添加至page的next指针处,并将next指向下一位;
b.当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中;
c.当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中。
4.调用pop操作,会传入一个值,这个值就是push操作返回的哨兵对象地址。所以pop内部的实现就是根据token找到的哨兵对象所处的page中,然后然后使用 objc_release 释放 token之前的对象,并把next 指针到正确位置。

关于AutoreleasePool和引用列表只是简单的介绍了一下,后续在慢慢的补充.

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342