美文网首页
底层探索--内存管理的本质

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

作者: 永断阎罗 | 来源:发表于2021-10-15 10:01 被阅读0次

    定时器

    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()

    相关文章

      网友评论

          本文标题:底层探索--内存管理的本质

          本文链接:https://www.haomeiwen.com/subject/dmfjoltx.html