IOS 基础/内存管理

285 阅读10分钟

基本概念

一、OC的三大特性为:封装、继承、多态

1、封装:

  • 成员变量的封装:setter、getter 方法,self调用方便高效
  • 普通方法的封装:简洁、高效,不用关心内部过程
  • SDK的封装:使用方便、私有隐藏,安全性高(不会泄露数据和代码)

2、继承

  • 抽取了重复代码:子类可以拥有父类中的所有成员变量和方法
  • 建立了类与类之间的联系
  • 每个类中都有一个superclass指针指向父类
  • 所有类的根类都是NSObject,便于isa指针 查询
  • 调用某个对象的方法时,优先去当前类中找,如果找不到,再去父类中找
  • 子类重新实现父类的某个方法,会覆盖父类以前的方法。

继承的缺点:耦合性太高(类与类之间的关系过于紧密),没有多继承,OC里面都是单继承,多继承可以用protocol委托代理来模拟实现;可以通过实现多个接口完成OC的多重继承。

3、多态

例如: -(void) animal:(id); id 就是多态,传入时,才识别 具体类的真实形象。runtime 也是运行时,才识别具体类。

  • 要想使用多态必须使用继承(继承是多态的前提)
  • 多态:父类指针指向子类对象 Animal *aa = [Dog new]; 调用方法时会检测对象的真实形象
  • 好处:如果函数或方法参数中使用的是父类类型,可以传入父类,子类对象。
  • 局限性:父类类型的变量不能直接调用子类特有的方法,必须强制转换为子类类型变量后,才能使用。

4.property的关键字分三类

1.原子性(也就线程安全)

有atomic和nonatomic, acomic就是线程安全,但是一般使用nonacomic,因为acomic的线程安全开销太大,影响性能,即使需要保证线程安全,我们也可以通过自已的代码控制,而不用acomic。

2.引用计数

  • assign:assign用于非指针变量,基本修饰数据类型,统一由系统的栈区进行内存管理。
  • weak:对对象的弱引用,不增加引用计数,也不持有对象,当对象消失后指针自动变为nil。
  • copy:分为深拷贝和浅拷贝
  • 浅拷贝:对内存地址的复制,让目标对象指针和原对象指向同一片内存空间会增加引用计数
  • 深拷贝:对对象内容的复制,开辟新的内存空间
  • strong:是每对这个属性引用一次,retainCount 就会+1,只能修饰 NSObject 对象,不能修饰基本数据类型。是 id 和 对象 的默认修饰符。
  • unsafe_unretained:和week 非常相似, 也是可以同时修饰基本数据类型和 NSObject 对象 ,其实它本身是 week 的前身 , 在 iOS5 之后,基本都用 week 代替了 unsafe_unretained 。 但它们之间还是稍微有点区别的,并不是完全一样,对上层代码来说,能用 unsafe_unretained 的地方,都可以用 week 代替。同时要注意一点,这个修饰符修饰的变量不属于编译器的内存管理对象

5.类别Category

重写一个类的方式用继承还是分类,取决于具体情况:

  • 假如目标类有许多的子类,我们需要拓展这个类又不希望影响到原有的代码,继承后比较好。
  • 如果仅仅是拓展方法,分类更好.(不需要涉及到原先的代码)

优点:

  • 分类中方法的优先级比原来类中的方法高,也就是说,在分类中重写了原来类中的方法,那么分类中的方法会覆盖原来类中的方法。+(void)load方法是一个特例,它会在当前类执行完之后,category中的再执行。)
  • 可以用runtime进行method swizzling(方法的偷梁换柱)来处理异常调用的方法。

缺点:

通过观察头文件我们可以发现,Cocoa框架中的许多类都是通过category来实现功能的,可能不经意间你就覆盖了这些方法中的其一,有时候就会产生一些无法排查的异常原因。

其他问题

浅拷贝和深拷贝的区别?

  • 浅拷贝:只复制指向对象的指针,指针指向同一个地址,而不复制引用对象本身。
  • 深拷贝:复制引用对象本身。内存中存在了两份独立对象本身,当修改A 时,B 不变。(即:B = [A copy]; )

内存管理

案例:办公室开关灯,有人开灯,无人关灯;进人加一,出人减一;第一个开灯的人持有此灯,第一个人能废弃此灯,其他人只是弱引用。

内存管理原则

  • 1.自己生成的对象,自己持有
  • 2.非自己生成的对象,自己也能持有
  • 3.不再需要自己持有的对象时,释放掉
  • 4.非自己持有的对象,无法释放(释放会崩溃)

对象操作与Objective-C方法的对应

对象操作Objective-C方法
生成并持有对象alloc、new、copy、mutableCopy
持有对象retain (引用计数加1)
释放对象release(引用计数减1)
废弃对象dealloc(引用计数为0)

相关释放函数:autorelease、NSAutoreleasePool、@autoreleasepool

使用autorelease方法,可以取得对象的存在,但是自己不持有对象(obj不立刻释放掉,注册到autoreleasePool中)

ARC 环境下的使用功能规则

  • 不能使用retain、release、retainCount、autorelease
  • 不能使用NSAllocateObject、NSDeallocateObject
  • 必须遵循内存管理命名规则(驼峰命名法)
  • 不要显式调用dealloc
  • 使用 @autorelease 代替 NSAutoreleasePool
  • 不能使用区域 (NSZone)
  • 对象型变量不能作为C语言结构体(struct/union)的成员
  • 显式转换 “id” 和 “void *”

会让对象引用计数增加的操作

    1. new、alloc、retain、copy、mutableCopy
    1. 用 addview 把一个控件添加到视图上,这个控件的引用计数+1;
    1. 把一个对象添加到数组中,数组内部会把这个对象的引用计数+1;
    1. 属性赋值,会让对象的引用计数+1;
    1. xib中得控件,跟代码关联后,会让对象的引用计数+1;
    1. push这个操作会让对象的引用计数增加。

ARC、MRC混编

  • ARC环境下 使用MRC代码,使用 -fno-objc-arc转换
  • MRC环境下 使用ARC代码,使用 -fobjc-arc转换

所有权修饰符

  • __strong 修饰符
  • __weak 修饰符
  • __unsafe_unretained 修饰符(MRC环境下使用)
  • __autoreleasing 修饰符 (ARC环境下使用)
  1. __strong修饰符是id类型和对象类型的默认修饰符,一般是隐式的; id obj = [[NSObject alloc] init]; //同上 id __strong obj = [[NSObject alloc] init];

  2. __weak 解决 循环引用的 修饰符,防止内存泄漏。在持有某对象的弱引用时,若该对象被废弃,则弱引用自动失效,且被赋值nil,不在持有该对象。

  3. __unsafe_unretained 和 _ _weak修饰的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会被立即释放。

  4. __autoreleasing 隐式修饰符:非自己生成的对象,自己也能持有,会注册到自动释放池中。

释放对象时,废弃对象不被持有的流程

  • 1、objc_release (释放)
  • 2、因为引用计数为0,所以执行dealloc(释放内存)
  • 3、_objc_rootDealloc(根解除)
  • 4、object_dispose(废弃处理)
  • 5、objc_destructInstance(销毁)
  • 6、objc_clear_deallocating(清除)

对象被废弃时,最后调用objc_clear_deallocating的动作(相关表 SideTables)

  • 1、从weak表中获取废弃对象的记录(地址为键值)。address= h(key)
  • 2、将包含在记录中的所有附有_ _weak修饰符变量的地址,赋值nil。
  • 3、从weak表中删除该记录。
  • 4、从引用计数表中删除废弃对象的记录(地址为键值)。

其他

  • 内存泄漏:应当废弃的对象在超出其生存周期后继续存在。
  • 野指针:指针变量未初始化,其值是随机的;指针释放后未置空,指向不存在的内存。
  • 悬垂指针: 指向曾经存在的对象,但是该对象已经不存在了。
  • 标记指针: Tagged Pointer 是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。(后面做专门讲解)
  • isa 指针: NONPOINTER_ISA 对象的isa指针,用来表明对象所属的类类型和一些附加信息。

标记指针: Tagged Pointer

image.png

看到苹果对于Tagged Pointer特点的介绍:

  • Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
  • Tagged Pointer指针的值不再是地址了,而是真正的值。实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
  • 在内存读取上有着3倍的效率,创建时比以前快10倍

例如:NSString 其输出的class类型为 NSTaggedPointerString。在字符串长度在9个以内时,iOS其实使用了tagged pointer做了优化的。 直到字符串长度大于9,字符串才真正成为了__NSCFString类型。 注意:对于以前的@""符号创建的字符串还是常量字符串,这个没有改变,但是采用stringWithFormat 等NSString方法的创建字符串对象则有了区别。

    NSString *strS = @"123456789";
    NSLog(@"%@ : %p",strS.class,strS);

    strS = [NSString stringWithFormat:@"1"];
    NSLog(@"%@ : %p",strS.class,strS);
    strS = [NSString stringWithUTF8String:"1234567"];
    strS = [NSString stringWithUTF8String:"abcdabc"];//7
    NSLog(@"%@ : %p",strS.class,strS);

    strS = [NSString stringWithUTF8String:"abcdabcd"];//8
    strS = [NSString stringWithUTF8String:"teeeeeeet"];//9
    NSLog(@"%@ : %p",strS.class,strS);

    strS = [NSString stringWithUTF8String:"eeeeeeeeee"];//10
    strS = [NSString stringWithUTF8String:"eeeeeeeeeee"];//11
    NSLog(@"%@ : %p",strS.class,strS);

    strS = [NSString stringWithUTF8String:"eeeeeeeeeeee"];//12
    NSLog(@"%@ : %p",strS.class,strS);
    
    运行结果:
    __NSCFConstantString : 0x10c843820
    NSTaggedPointerString : 0xa000000000000311
    NSTaggedPointerString : 0xa636261646362617
    NSTaggedPointerString : 0xa040000000000049
    NSTaggedPointerString : 0xa00000000000000b
    __NSCFString : 0x6000000315c0

isa 指针(NONPOINTER_ISA)

  • isa:是一个指向对象所属Class类型的指针。
  • nonPointer_isa:对象的isa指针,用来表明对象所属的类类型和一些附加信息。

如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。于是,就像tagged pointer一样,对于isa指针,苹果同样进行了优化。isa指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了 「引用计数」extra_rc,是否有被weak引用标志位weakly_referenced,是否有附加对象标志位has_assoc等信息。(rc:reference counter 引用计数)

//------- 联合体(定义)------- 
union isa_t 
{
    isa_t() { }                             //构造函数1
    isa_t(uintptr_t value) : bits(value) { }    //构造函数2
    Class cls;          //成员1(占据64位内存空间)
    uintptr_t bits;     //成员2(占据64位内存空间)

#if SUPPORT_PACKED_ISA
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {                //成员3(占据64位内存空间:从低位到高位依次是nonpointer到extra_rc。成员后面的:表明了该成员占用几个bit。)
        uintptr_t nonpointer        : 1;  //(低位)注意:标志位,表明isa_t *是否是一个真正的指针!!!
        uintptr_t has_assoc         : 1;  // 关联对象
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;  //弱引用
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;  //引用计数 相关成员1
        uintptr_t extra_rc          : 19; //引用计数 相关成员2 (用19位来 记录对象的引用次数)
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
}

引用计数表的关系

其实在绝大多数情况下,仅用优化的isa_t 来记录对象的引用计数就足够了。只有在19位的extra_rc盛放不了那么大的引用计数时,才会借助SideTable出马。

SideTable:是一个 全局的引用计数表,它记录了所有对象的引用计数。

先说一下这四个数据结构的关系。 在runtime内存空间中,SideTables 是一个长度为8个元素 的hash数组,里面存储了SideTable。SideTables的hash键值就是一个对象obj的address。即:table1 = h(address1) 因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable。

  • spinlock_t slock: 自旋锁,用于上锁/解锁 SideTable。
  • RefcountMap refcnts :以DisguisedPtr<objc_object>为key的hash表,用来存储OC对象的引用计数(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)。
  • weak_table_t weak_table : 存储对象弱引用指针的hash表。是OC weak功能实现的核心数据结构。
struct weak_table_t {
    weak_entry_t *weak_entries; // 保存了所有指向指定对象的 weak 指针
    size_t num_entries; // 存储空间
    uintptr_t mask; // 参与判断引用计数辅助量
    uintptr_t max_hash_displacement; // hash key 最大偏移值
};
 

其中,refcents是一个hash map,其key是obj的地址,而value,则是obj对象的引用计数。 即:referenceCount = h ( address )

而weak_table则存储了弱引用obj的指针的地址,其本质是一个以obj地址为key,弱引用obj的指针的地址作为value的hash表。hash表的节点类型是weak_entry_t。 即:objPointer = h ( address )

image.png