底层探索--内存管理的本质

定时器

1. CADisplayLink、NSTimer使用注意

  • CADisplayLinkNSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用,从而导致对象无法释放。

  • 解决方案如下:

      //解决方式1:使用闭包(>= ios1 0才能使用) 
      [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
             // 所指针引用self 
      }];
      
      //解决方式2:使用NSProxy对象(推荐)
      
      /// 定时器-代理对象:专门解决定时器内存问题的
      @interface TimerProxy : NSProxy
      
      /** NSProxy:特殊的专门用来做代理对象的,与NSObject不同的是,转发时它的效率更高(消息转发时,只会从自身类的类对象寻找方法,不会再到父类去寻找方法,且转发阶段,立马会走慢速转发方法:methodSignatureForSelector,不会经历方法解析和快速转发阶段)
       */
      
      /// 初始化
      /// @param target 代理对象(一般传self)
      + (instancetype)proxyWithTarget:(id)target;
      
      @end
      
      @interface TimerProxy()
      
      @property (nonatomic, weak) id tatget; //目标对象
      
      @end
      
      @implementation TimerProxy
      
      //初始化
      + (instancetype)proxyWithTarget:(id)target {
          //NSProxy:专门用于解决代理对象问题,效率比NSObject高,然没有init方法
          TimerProxy *proxy = [TimerProxy alloc];
          proxy.tatget = target;
          return proxy;
      }
      
      //消息签名
      - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
          if (self.tatget) {
              return [self.tatget methodSignatureForSelector:sel];
          }
          return [super methodSignatureForSelector:sel];
      }
      
      //消息转发
      - (void)forwardInvocation:(NSInvocation *)invocation {
          if (self.tatget) {
              [invocation invokeWithTarget:self.tatget];
           }
      }
    
      @end
    

2. GCD自定义定时器

//创建线程
dispatch_queue_t queue =  dispatch_get_global_queue(0, 0);
//初始化定时器
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置时间(参数-1.资源本身,2.开始时间,3.间隔时间,4.偏差,默认为0)
//开始时间:dispatch_time(DISPATCH_TIME_NOW, (2*NSEC_PER_SEC))从现在开始,2秒后执行
dispatch_source_set_timer(source, 0, (ti*NSEC_PER_SEC), 0);
//设置回调
dispatch_source_set_event_handler(source, ^{
    //处理逻辑。。。
});
//启动定时器
dispatch_resume(source);

iOS程序的内存布局

多线程的iOS程序的内存布局.png

Tagged Pointer详解

参考链接

// objc-internal.h 
static inline bool 
// 判断是否是TaggedPointer的指针
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0  // MacOS
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1  // iOS
#endif

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)  // _OBJC_TAG_MASK -- iOS
#else
#   define _OBJC_TAG_MASK 1UL       // _OBJC_TAG_MASK -- MacOS
#endif
  • 字符串继承链:
    __NSCFConstantString -> __NSCFString -> NSMutableString -> NSString -> NSObject

  • 特点:在64位机器上

    • Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。这是一个特别的指针,不指向任何一个地址,当指针不够存储数据时,才会使用动态分配内存的方式来存储数据,这个标志位,也是在最高4位来表示的。
    • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
    • 在内存读取上有着3倍的效率,创建时比以前快106倍。
    • 如何判断一个指针是否为Tagged Pointer
      • iOS平台,最高有效位是1(第64bit)
      • Mac平台,最低有效位是1。
    • 在字符串长度在9个以内时,iOS其实使用了Tagged pointer做了优化的, 直到字符串长度大于9,字符串才真正成为了__NSCFString类型(即对象类型)
    • 位图:
      • 1. NSNumber(1标识位 2-4类型识位 后四位数据类型,其他存储数据)
      • 2. NSString(1标识位 2-4类型识位 后四位字符串长度,其他存储数据)
        内存管理之taggedPinter-NSNumber.png

        内存管理之taggedPinter-NSString.png
  • 什么时候使用的Tagged Pointer?
    • NSString:[动态字符串调用 copy]且length<=9,[NSString stringWithFormat]且length<=9
    • NSNumber:存储较小的值,如果 8 字节承载不了时,则又用以前的方式来生成普通的指针。
  • 经典题目:
    //经典题目:加锁、原子、同步、串行解决
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abcdefghij"]; //崩溃,因为同时访问,name的setter方法先release后retain,可能遇到两次都release造成过度释放而访问野指针
            //self.name = [NSString stringWithFormat:@"abcdefghi"]; //为Tagged Pointer,相当于直接赋值,不是一个真正的OC对象,不会调用setter方法进行
        });
    }
    
    - (void)setName:(NSString *)name {
        if(_name != name) { 
            [_name release];
            _name = [name retain]; // or [name copy]
        }
    }

MRC的对象的setter方法

重点:使用MRC,记住原则:“谁创建谁释放”

// getter方法直接返回
- (NSString *)name {
    return _name;
}

// setter 方法
- (void)setName:(NSString *)name {
    if(_name != name) {  //保证同一个对象不用重复操作
        [_name release]; //保证替换的上一个对象计数器-1
        _name = [name retain]; // or [name copy] //计数器+1,保证此对象被当前对象拥有,即使外面对象计数器减一,此对象也不会被释放
    }
}

拷贝Copy和mutableCopy

  • 重要规则:

    • 目的:类似文件夹,拷贝备份之后,源文件修改不影响copy文件,同时,copy文件修改也不影响源文件。总结:拷贝之后,互不影响
      • 不可变对象:copy直接引用+1,互相修改也不会影响;mutableCopy的副本能修改,则需生成新的对象
      • 可变对象:copy和mutableCopy的源本能修改,则需生成新的对象
    • 拷贝:Copy拷贝后都是不可变对象,mutableCopy拷贝后都是可变对象。(不包括自己实现的)
  • 定义解释

    • 浅拷贝:指针拷贝,相当于retain,引用计数+1
    • 深拷贝:内容拷贝,生成新的相同内容的对象,同时指针指向此对象。
  • 注意:

    • 申明属性时,如果用关键字copy,则最好不要用可变对象,因为经copy之后就变成不可变对象,所以在使用的时候就会出现无法增删改的错误。
    • 为什么NSString,系统常用copy?为了避免外部赋值的改变从而影响控件的UI显示的值。
  • 总结

    对象类型 copy mutableCopy
    不可变对象 浅copy,指针复制,返回值不可变 深copy,内容复制,返回值可变
    可变对象 深copy,内容复制,返回值不可变 深copy,内容复制,返回值可变

引用计数器

  • 在64bit中,引用计数可以直接存储在优化过的isa指针(详情见对象的本质)中,也可能存储在SideTable类中

      // SideTable的定义
      struct SideTable {
          spinlock_t slock;
          RefcountMap refcnts; //存放着对象引用计数的散列表
          weak_table_t weak_table; //存放着所有弱引用的对象指针的散列表
      }
    
  • dealloc释放过程:dealloc->_objc_rootDealloc->rootDealloc->object_dispose->objc_destructInstance/free

    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            // Read all of the flags at once for performance.
            bool cxx = obj->hasCxxDtor(); //是否有C++的析构函数
            bool assoc = obj->hasAssociatedObjects(); //是否有设置关联对象
    
            // This order is important.
            if (cxx) object_cxxDestruct(obj); //清楚成员变量
            if (assoc) _object_remove_assocations(obj); //移除关联对象
            obj->clearDeallocating(); //将指向当前对象的弱指针置为nil
        }
    
        return obj;
    }

自动释放池

主线程的自动释放池

//重新编译为C/C++ 文件后的源码
struct __AtAutoreleasePool {
    __AtAutoreleasePool() { //构造函数
        atautoreleasepoolobj = objc_autoreleasePoolPush(); //运行时的放入函数
    }
    ~__AtAutoreleasePool() { //析构函数
        objc_autoreleasePoolPop(atautoreleasepoolobj); //运行时的释放函数
    }
    void * atautoreleasepoolobj;
};

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    // @autoreleasepool
    { __AtAutoreleasePool __autoreleasepool;

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
  • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放所有的Autoreleautorelease对象的地址asePoolPage对象通过双向链表的形式连接在一起;
  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址;
  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
  • id *next指向了下一个能存放autorelease对象地址的区域。
    class AutoreleasePoolPage;
    struct AutoreleasePoolPageData
    {
        magic_t const magic;
        __unsafe_unretained id *next;
        pthread_t const thread;
        AutoreleasePoolPage * const parent;
        AutoreleasePoolPage *child;
        uint32_t const depth;
        uint32_t hiwat;
    
        AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
            : magic(), next(_next), thread(_thread),
              parent(_parent), child(nil),
              depth(_depth), hiwat(_hiwat)
        {
        }
    };
    
    //底层push函数-向RunloopPage里加入对象
    static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    
    //底层pop函数-释放RunloopPage里的对象
    static inline void pop(void *token)
    {
        AutoreleasePoolPage *page;
        id *stop;
        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            page = hotPage();
            if (!page) {
                // Pool was never used. Clear the placeholder.
                return setHotPage(nil);
            }
            // Pool was used. Pop its contents normally.
            // Pool pages remain allocated for re-use as usual.
            page = coldPage();
            token = page->begin();
        } else {
            page = pageForPointer(token);
        }
    
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }
    
        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }
    
        return popPage<false>(token, page, stop);
    }

ARC下的自动内存管理机制(结合上面的进行理解)

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

推荐阅读更多精彩内容