阅读 2897

iOS开发小记-基础篇

前几年学习过程中陆陆续续整理的知识点,今天开始迁移到掘金。由于当年在翻阅国产技术书籍时,发现知识点有不少错误,踩了不少坑,当然可能仍然有错误和遗漏,欢迎指正~

KVC


KVC允许以字符串形式间接操作对象的属性,全称为Key Value Coding,即键值编码。

  • 底层实现机制
- (void)setValue:(nullable id)value forKey:(NSString *)key;
复制代码
  1. 首先查找-set<Key>:代码通过setter方法赋值。(勘误1)
  2. 否则,检查+(BOOL)accessInstanceVariablesDirectly方法,如果你重写了该方法并使其返回NO,则KVC下一步会执行setValue:forUndefinedKey:,默认抛出异常。
  3. 否则,KVC会依次搜索该类中名为_<key>_<isKey><key><isKey>的成员变量。
  4. 如果都没有,则执行setValue:forUndefinedKey:方法,默认抛出异常。

勘误1:经验证,在查找`-set<Key>:`后,如果没有找到,还会去查找`-_set<Key>:`方法,然后才会进入步骤2,感谢@QXCloud 的指正~

- (nullable id)valueForKey:(NSString *)key;
复制代码
  1. 首次依次查找-get<Key>-<key>-is<Key>代码通过getter方法获取值。
  2. 否则,查找-countOf<Key>-objectIn<Key>AtIndex:-<key>AtIndexes:方法,如果count方法和另外两个中的一个被找到,返回一个能响应所有NSArray方法的代理集合,简单来说就是可以当NSArray用。
  3. 否则,查找-countOf<Key>-enumeratorOf<Key>-memberOf<Key>:方法,如果三个都能找到,返回一个能响应所有NSSet方法的代理集合,简单来说就是可以当NSSet使用。
  4. 否则,依次搜索该类中名为_<key>_<isKey><key><isKey>的成员变量,返回该成员变量的值。
  5. 如果都没有,则执行valueForUndefinedKey:方法,默认抛出异常。
  • Key Path
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
复制代码

KVC不仅可以操作对象属性,还可以操作对象的“属性链”。如Person中有一个类型为Date的birthday属性,而Date中又有year,month,day等属性,那么Person可以直接通过birthday.year这种Key Path来操作birthday的year属性。

  • 如何避免KVC修改readonly属性?

从上述机制可以看出,在没有setter方法时,会检查+(BOOL)accessInstanceVariablesDirectly来决定是否搜索相似成员变量,因此只需要重写该方法并返回NO即可。

  • 如何校验KVC的正确性?

开发中可能有些需要设定对象属性不可以设置某些值,此时就需要检验Value的可用性,通过如下方法

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
复制代码

这个方法的默认实现是去探索类里面是否有一个这样的方法-(BOOL)validate<Key>:error:,如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES。 注意:在KVC设值的时候,并不会主动调用该方法去校验,需要开发者手动调用校验,意味着即使实现此方法,也可以赋值成功。

  • 常见的异常情况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
复制代码

没有找到相关key,会抛出NSUndefinedKeyException异常,使用KVC时一般需要重写这两个方法。

- (void)setNilValueForKey:(NSString *)key;
复制代码

当给基础类型的对象属性设置nil时,会抛出NSInvalidArgumentException异常,一般也需要重写。

  • 常见应用场景
  1. 可以灵活的使用字符串动态取值和设值,但通过KVC操作对象的性能比getter和setter更差。
  2. 访问和修改私有属性,最常见的就是修改UITextField中的placeHolderText。
  3. 通过- (void)setValuesForKeysWithDictionary:字典转model,如股票字段。
  4. 当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作,由此我们可以有效的提取容器中每个对象的指定属性值集合。
  5. 使用函数操作容器中的对象,快速对各对象中的基础类型属性做运算,如@avg@count@max@min @sum

KVO


KVO提供了一种机制(基于NSKeyValueObserving协议,所有Object都实现了此协议)可以供观察者监听对象属性的变化并接收通知,全称为Key Value Observing,即键值监听。

常用API
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
复制代码
观察者重写
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
复制代码
  • 底层实现机制

当观察者对对象A注册一个监听时,系统此时会动态创建一个名为NSKVONotifying_A的新类,该类继承自对象A原本的类,并重写被观察者的setter方法,在原setter方法的调用前后通知观察者值的改变。然后将对象A的isa指针(isa指针告诉Runtime这个对象的类是什么)指向NSKVONotifying_A这个新类,那么对象A就变成了新创建新类的实例。 不仅如此,Apple还重写了-class方法来隐藏该新类,让人们以为注册前后对象A的类并没有改变,但实际上如果手动新建一个NSKVONotifying_A类,在观察者运行到注册时,便会引起重复类崩溃。

  • 注意事项

从上述实现原理来看,很明显可以知道,如果没有通过setter赋值,直接赋值该成员变量是不会触发KVO机制的,但是KVC除外,这也侧面证明了KVC与KVO是有内在联系的。

alloc/init与new


  • alloc/init

alloc负责为对象的所有成员变量分配内存空间,并且为各成员变量重置为默认值,如int型为0,BOOL型为NO,指针型变量为nil。 仅仅分配空间还不够,还需要init来对对象执行初始化操作,才可以使用它。如果只调用alloc不调用init,也能运行,但可能会出现未知结果。

  • new

所做的事情与alloc差不多,也是分配内存和初始化。

  • 两者区别

new只能使用默认的init初始化,而alloc可以使用其他初始化方法,因为显示调用总比隐式调用好,所以往往使用alloc/init来初始化。

@"hello"和[NSString stringWithFormat:@"hello"]有何区别?


NSString *A = @"hello";
NSString *B = @"hello";
NSString *C = [NSString stringWithFormat:@"hello"];
NSString *D = [NSString stringWithFormat:@"hello"];
NSString *E = [[NSString alloc] initWithFormat:@"hello"];
NSString *F = [[NSString alloc] initWithFormat:@"hello"];

NSLog(@"A=%p\n B=%p\n C=%p\n D=%p\n E=%p\n F=%p\n", A, B, C, D, E, F);

// 结果
A=0x104ba0070
B=0x104ba0070
C=0xdbd16c40a2e07e99
D=0xdbd16c40a2e07e99
E=0xdbd16c40a2e07e99
F=0xdbd16c40a2e07e99
复制代码

@"hello"位于常量池中,可重复使用,其中A和B指向的都是同一份内存地址。 而stringWithFormatinitWithFormat是在运行时创建出来的,保存在运行时内存(即堆内存),它们在堆里面请求对应的值,如果存在,系统便不再分配地址。

Propoty修饰符


具体可分为四类:线程安全、读写权限、内存管理和指定读写方法。

  • 线程安全(atomic,nonatomic)

如果不写该类修饰符,默认就是atomic。两者最大的区别就是决定编译器生成的getter/setter方法是否属于原子操作,如果自己写了getter/setter方法,此时用什么都一样。 对于atomic来说,getter/setter方法增加了锁来确保操作的完整性,不受其他线程影响。例如线程A的getter方法运行到一半,线程B调用setter方法,那么线程A还是能得到一个完整的Value。 而对于nonatomic来说,多个线程能同时访问操作,就无法保证是否是完整的Value,还会引发脏数据。但是nonatomic更快,开发中往往在可控情况下安全换效率。

注意:atomic并不能完全保证线程安全,只能保证数据操作的线程安全,例如线程A使用getter方法,同时线程B、C使用setter方法,那最后线程A获取到的值有三种可能:原始值、B set的值或者C set的值;又例如线程A使用getter方法,线程B同时调用release方法,由于release方法并没有加锁,所以有可能会导致cash。

  • 读写权限(readonly,readwrite)

readonly只读属性,只会生成getter方法,不会生成setter方法。 readwrite读写属性,会生成getter/setter方法,默认是该修饰符。

  • 内存管理(strong,weak,assign,copy)

strong强引用,适用于对象,引用计数+1,对象默认是该修饰符。 weak弱引用,为这种属性设置新值时,设置方法既不释放旧值,也不保留新值,不会使引用计数加1。当所指对象被销毁时,指针会自动被置为nil,防止野指针。

assgin适用于基础数据类型,如NSIntger,CGFloat,int等,只进行简单赋值,基础数据类型默认是该修饰符。如果用此修饰符修饰对象,对象被销毁时,并不会置空,会造成野指针。 copy是为了解决上下文的异常依赖,实际赋值类型不可变对象时,浅拷贝;可变对象时,深拷贝。

  • 指定读写方法(setter=,getter=)

给getter/setter方法起别名,可以不一致,并且可以与其他属性的getter/setter重名,例如Person类中定义如下

@property (nonatomic, copy, setter=setNewName:, getter=oldName) NSString *name;
@property (nonatomic, copy) NSString *oldName;
复制代码

那么此时p1.oldName始终是_name的值,而如果声明的顺序交换,此时p1.oldName就是_oldName的值了,如果想得到_name的值,使用p1.name即可,但是此时不能使用-setName:。所以别名都是有意义且不重复的,避免一些想不到的问题。

  • strong和copy的区别

strong是浅拷贝,仅拷贝指针并增加引用计数;而copy在对于实际赋值对象是可变对象时,是深拷贝。不可变对象使用copy修饰,如NSString、NSArray、NSSet等;可变对象使用strong修饰,如NSMutableString、NSMutableArray、NSMutableSet等,这是为什么呢? 由于父类属性可以指向子类对象,试想这样一个例子:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

NSMutableString *mutableName = [NSMutableString stringWithFormat:@"hello"];
p.name = mutableName;
[mutableName appendString:@" world"];
复制代码

由于Person.name使用的strong修饰,它对于赋值对象进行的浅拷贝,那么Person.name此时实际指向与mutableName指向的同一块的内存区,如果将mutableName的内容修改,此时Person.name也会修改,这并不是我们想要的,所以我们使用copy来修饰,这样即使赋值对象是一个可变对象,也会在setter方法中copy一份不可变对象再赋值。 而对于可变对象的属性来说,如果使用copy修饰,从上面可知会得到一个不可变对象再赋值,那么如果你想要修改对象内容的时候,就会抛出异常,所以我们用strong。

  • assgin和weak的区别

assgin用于基础类型,可以修饰对象,但是这个对象在销毁后,这个指针并不会置空,会造成野指针错误。 weak用于对象,无法修饰基础类型,并且在对象销毁后,指针会自动置为nil,不会引起野指针崩溃。

  • var、getter、setter 是如何生成并添加到这个类中的?

完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字。

  • @protocol 和 category 中如何使用 @property

在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性。 category 使用 @property 也是只会生成 setter 和 getter 方法的声明,如果我们真的需要给 category 增加属性的实现,需要借助于Runtime的关联对象。

深拷贝与浅拷贝


  • 深拷贝

深拷贝是对内容的拷贝,即复制一份原来的内容放在其他内存下,新对象指针指向该内存区域,与原来的对象没有关系。

  • 浅拷贝

浅拷贝是对指针的拷贝,即创建一个新指针也指向原对象的内存空间,相当于给原来对象索引计数+1。

  • Copy与MutableCopy

对于可变对象来说,Copy和MutableCopy都是深拷贝; 对于不可变对象来说,Copy是浅拷贝,MutableCopy是深拷贝; Copy返回的都是不可变对象,MutableCopy返回的都是可变对象。

  • 测试案例
    NSArray *arr1 = @[];
    NSArray *arr2 = arr1;
    NSArray *arr3 = [arr1 copy];
    NSArray *arr4 = [arr1 mutableCopy];
    
    NSMutableArray *arr5 = [NSMutableArray array];
    NSMutableArray *arr6 = arr5;
    NSMutableArray *arr7 = [arr5 copy];
    NSMutableArray *arr8 = [arr5 mutableCopy];
    
    NSLog(@"%p %x", arr1, &arr1);
    NSLog(@"%p %x", arr2, &arr2);
    NSLog(@"%p %x", arr3, &arr3);
    NSLog(@"%p %x", arr4, &arr4);
    NSLog(@"%p %x", arr5, &arr5);
    NSLog(@"%p %x", arr6, &arr6);
    NSLog(@"%p %x", arr7, &arr7);
    NSLog(@"%p %x", arr8, &arr8);
复制代码

打印结果

0x604000001970 e7ba22d8
0x604000001970 e7ba22c8
0x604000001970 e7ba22c0
0x604000458360 e7ba22b8
0x6040004581e0 e7ba22b0
0x6040004581e0 e7ba22a8
0x604000001970 e7ba22a0
0x604000458270 e7ba2298
复制代码

Weak的实现原理


  • 前提

在Runtime中,为了管理所有对象的引用计数和weak指针,创建了一个全局的SideTables,实际是一个hash表,里面都是SideTable的结构体,并且以对象的内存地址作为key,SideTable部分定义如下

struct SideTable {
    //保证原子操作的自旋锁
    spinlock_t slock;
    //保存引用计数的hash表
    RefcountMap refcnts;
    //用于维护weak指针的结构体
    weak_table_t weak_table;
    ....
};
复制代码

其中用来维护weak指针的结构体weak_table_t是一个全局表,其定义如下

struct weak_table_t {
    //保存所有弱引用表的入口,包含所有对象的弱引用表
    weak_entry_t *weak_entries;
    //存储空间
    size_t num_entries;
    //参与判断引用计数辅助量
    uintptr_t mask;
    //hash key 最大偏移值
    uintptr_t max_hash_displacement;
};
复制代码

其中所有的weak指针正是存在weak_entry_t中,其部分定义如下

struct weak_entry_t {
    //被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            //可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
    ...
};
复制代码
  • 大致流程

weak由于不增加引用计数,所以不能在SideTable中与引用计数表放在一起,Runtime单独使用了一个全局hash表weak_table_t来管理weak,其中底层结构体weak_entry_t以weak指向的对象内存地址为key,value是一个存储该对象所有weak指针的数组。当这个对象dealloc时,假设该对象的内存地址为a,查出对应的SideTable,搜索key为a对应的指针数组,并且遍历数组将所有weak对象置为nil,并清除记录。

  • 代码分析(NSObject.mm)
//创建weak对象
id __weak obj1 = obj;

//Runtime会调用如下方法初始化
id objc_initWeak(id *location, id newObj)
{
    //如果对象实例为nil,当前weak对象直接置空
    if (!newObj) {
        *location = nil;
        return nil;
    }
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

//更新指针指向
static id  storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

 //查询当前weak指针原指向的oldSideTable与当前newObj的newSideTable
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    .....
    //解除weak指针在旧对象中注册
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    //添加weak到新对象的注册
    if (haveNew) {
        newObj = (objc_object *)
            //这个地方仍然需要newObj来核对内存地址来找到weak_entry_t,从而删除
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }
        *location = (id)newObj;
    } else {}
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}
复制代码

Category(分类)


分类用于对已有的类添加新方法,并不需要创建新的子类,不需要访问原有类的源代码,可以将类定义模块化地分布到多个分类中。

  • 特点
  1. 分类的方法可以与原来类同名,如果在分类中实现了该方法,分类的方法优先级大于原有类方法。(不建议同名,分类的作用是新增方法,应使用子类重写或者统一加前缀)
  2. 分类只能添加方法,不能添加成员变量。
  3. 分类不仅影响原有类,还影响其子类。
  4. 一个类支持定义多个分类。
  5. 如果多个分类中有相同方法,运行时到底调用哪个方法由编译器决定,最后一个参与编译的方法会被调用。
  6. 分类是在运行时加载的,不是在编译时。
  7. 可以添加属性,但是@property不会生成setter和getter方法,也不会生成对应成员变量。(实际没有意义)
  • 使用场景
  1. 模块化设计:对于一个超大功能类来说,通过分类将其功能拆分,是个十分有效的方式,有利于管理和协同开发。
  2. 声明私有方法:我们可以利用分类声明一个私有方法,这样可以外部直接使用该方法,不会报错。
  3. 实现非正式协议:由于分类中的方法可以只声明不实现,原来协议中不支持可选方法,就可以通过分类声明可选方法,实现非正式协议。
  • 为什么不能添加成员变量?

分类的实现是基于Runtime动态的将分类中方法添加到类中,Runtime中通过class_addIvar()方法添加成员变量,但苹果对该方法只能在构造一个类的过程中调用,不允许对一个已存在的类动态添加成员变量。 为什么苹果不允许?这是因为对象在运行期已经给成员变量都分配了内存,如果动态的添加属性,不仅需要破坏内部布局,而且已经创建的类的实例也不符合当前类的定义,这简直是灾难性的。但是方法保存在类的可变区域中,修改是不会影响类的内存布局的,所以没问题。

  • 如何添加有效属性?

在分类中声明属性可以编译通过,但是使用该属性,会报找不到getter/setter方法,这是由于即使声明属性,也不回生成_成员变量,自然也没有必要实现getter/setter方法,那么我们就需要通过Runtime的关联对象来为属性实现getter/setter方法。 例如对Person的一个分类增加SpecialName属性,实现代码如下

#import "Person+Test.h"
#import <objc/runtime.h>

// 定义关联的key
static const char* specialNameKey = "specialName";

@implementation Person (Test)

- (void)setSpecialName:(NSString *)specialName {
    // 第一个参数:给哪个对象添加关联
    // 第二个参数:关联的key,通过这个key获取
    // 第三个参数:关联的value
    // 第四个参数:关联的策略
    objc_setAssociatedObject(self, specialNameKey, specialName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)specialName {
    // 根据关联的key,获取关联的值。
    return objc_getAssociatedObject(self, specialNameKey);
}

@end
复制代码

其中关联对象也是存在一个hash表中,通过内存寻址,当对象销毁时,会找到对应存储的关联对象做清理工作。

详见《深入理解Objective-C:Category》

Extension(扩展)


扩展与分类相似,相当于匿名分类,但分类通常有.h和.m文件,而扩展常用于临时对某个类的接口进行扩展,一般声明私有属性、私有方法、私有成员变量。

  • 特点
  1. 可以单独以文件定义,命名方式与分类相同。
  2. 通常放在主类的.m中。
  3. 扩展是在编译时加载的。
  4. 扩展新添加的方法,类一定要实现。

Block


block是对C语言的扩展,用来实现匿名函数的特性。

  • 特性
  1. 对于局部变量默认是只读属性。
  2. 如果要修改外部变量,声明__block。
  3. block在OC中是对象,持有block的对象可能也被block持有,从而引发循环引用,可以使用weakSelf。
  4. block只是保存了一份代码,只有调用时才会执行。
  • 底层实现

block对应的结构体如下

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};
struct Block_layout {
    //所有对象都有该指针,用于实现对象相关的功能
    void *isa; 
    //用于按 bit 位表示一些 block 的附加信息
    int flags;
    //保留变量
    int reserved;
    //函数指针,指向具体的 block 实现的函数调用地址
    void (*invoke)(void *, ...);
    //表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针
    struct Block_descriptor *descriptor;
    /* Imported variables. */
    //捕获的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中
};
复制代码

在 Objective-C 语言中,一共有 3 种类型的 block: _NSConcreteGlobalBlock全局的静态 block,不会访问任何外部变量。 _NSConcreteStackBlock保存在栈中的 block,当函数返回时会被销毁。 _NSConcreteMallocBlock保存在堆中的 block,当引用计数为 0 时会被销毁。

  • 适用场景

事件响应,数据传递,链式语法。

  • 答疑
  1. 为什么不能直接修改局部变量?

这是因为block会重新生成一份变量,所以局部变量修改不会影响block中的变量,而且编译器加了限制,block中的变量也不允许修改。

  1. 为什么能修改全局变量和静态变量?

全局变量所占用的内存只有一份,供所有函数共同调用,block可以直接使用,而不需要深拷贝或者使用变量指针。 静态变量实际与__block修饰类似,block是直接使用的指向该静态变量的指针,并未重新深拷贝。

  1. 如何修改局部变量?

将局部变量使用__block修饰,告诉编译器这个局部变量是可以修改的,那么block不会再生成一份,而是复制使用该局部变量的指针。

  1. 为什么要在block中使用strongSelf?

我们为了防止循环引用使用了weakSelf,但是某些情况在block的执行过程中,会出现self突然释放的情况,导致运行不正确,所以我们使用strongSelf来增加强引用,保证后续代码都可以正常运行。 那么岂不是会导致循环引用?确实会,但是只是在block代码块的作用域里,一旦执行结束,strongSelf就会释放,这个临时的循环引用就会自动打破。

  1. block用copy还是strong修饰?

MRC下使用copy,ARC下都可以。 MRC下block创建时,如果block中使用了成员变量,其类型是_NSConcreteStackBlock,它的内存是放在栈区,作用域仅仅是在初始化的区域内,一旦外部使用,就可能造成崩溃,所以一般使用copy来将block拷贝到堆内存,此时类型为_NSConcreteMallocBlock,使得block可以在声明域外使用。 ARC下只有_NSConcreteGlobalBlock_NSConcreteMallocBlock类型,如果block中使用了成员变量,其类型是_NSConcreteMallocBlock,所以无论是strong还是copy都可以。

  1. 如何不使用__block修改局部变量?

虽然编译器做了限制,但是我们仍然可以在block中通过指针修改,如

int a = 1;
void (^test)() = ^ {
    //通过使用指针绕过了编译器限制,但是由于block中是外面局部变量的拷贝,所以即使修改了,外面局部变量也不会变,实际作用不大。
    int *p = &a;
    *p = 2;
    NSLog(@"%d", a);
};
test();
NSLog(@"%d", a);
复制代码

详见《谈Objective-C block的实现》

Objective-C对象模型


所有对象在runtime层都是以struct展示的,NSObject就是一个包含了isa指针的结构体,如下

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
复制代码

而Class也是个包含了isa的结构体,如下

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
复制代码

objc_object中的isa指针告诉了runtime自己指向了什么类,objc_class中的isa指向父类,最终根元类的isa会指向自己,形成闭环。 Objective-C 2.0中并未具体暴露实现,但我们可以看到 Objective-C 2.0中的大概实现,包含了父类,成员变量表,方法表等。

  • 常见使用
  1. 动态的修改isa的值,即isa swizzling,例如KVO。
  2. 动态的修改methodLists,即Method Swizzling,例如Category。

ARC与GC


ARC(Automatic Reference Counting)自动引用计数,是苹果在WWDC 2011大会上提出的内存管理技术,常应用于iOS和MacOS。 GC(Garbage Collection)垃圾回收机制,由于Java的流行而广为人知,简单说就是系统定期查找不用的对象,并释放其内存。

  • ARC一定不会内存泄漏么?

不是,虽然大部分使用ARC的内存管理都做得很好,但是如果使用不当,仍然会造成内存泄漏,例如循环引用;OC与Core Foundation类进行桥接的时候,管理不当也会内存泄漏;指针未清空,造成野指针等。

  • 两者的区别
  1. 在性能上,GC需要一套额外的系统来跟踪处理内存,分析哪些内存是需要释放的,相对来说就需要更多的计算;ARC是开发者自己来管理资源的释放,不需要额外系统,性能比GC高。
  2. GC回收内存时,由于定时跟踪回收,无用内存无法及时释放,并且需要暂停当前程序,如果资源很多,这个延迟将会很大;ARC只需要引用计数为0便立即释放,没有延迟。

内存分区


分为五个区:栈区,堆区,全局区,常量区,代码区。程序启动后,全局区,常量区和代码区是已经固定的,不会再更改。

  • 栈区(stack)

存一些局部变量,函数跳转地址,现场保护等,该区由系统处理,无需我们干预。大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃 。

  • 堆区(heap)

即运行时内存,我们创建对象就是在这里,需要开发者来管理。

  • 全局区/静态区

用于存放全局变量和静态变量,初始化的放在一块区域,未初始化的放在相邻的一块区域。

  • 常量区

存放常量,如字符串常量,const常量。

  • 代码区

存放代码。

static、const与extern


static修饰的变量存储在静态区,在编译时就分配好了内存,会一直存在app内存中直到停止运行。该静态变量只会初始化一次,在内存中只有一份,并且限制了它只能在声明的作用域中使用,例如单例。 注:static也可以在.h文件中声明,但是由于头文件可以被其他文件任意引入使用,此时限制作用域没有任何意义,违背了它的初衷,而且重复声明也会报错。

const用于声明常量,只读不可写,该常量存储在常量区,编译时就分配了相关内存,也会一直存在app内存直到停止运行,示例代码如下:

int const *p   //  *p只读 ;p变量
int * const p  // *p变量 ; p只读
const int * const p //p和*p都只读
int const * const p   //p和*p都只读
复制代码

extern用于声明外部全局变量/常量,告诉编译器需要找对应的全局变量,需要在.m中实现,如下写法是错误的

//Person.h
extern NSString *const Test = @"test";
复制代码

正确的使用方法是

//Person.h
extern NSString *const Test;

//Person.m
NSString *const Test = @"test";
复制代码

它常用于让当前类可以使用其他类的全局变量/常量,也经常用于统一管理全局变量/常量,更规范整洁,并且在打包时配合const使用,可以避免其他人修改。 extern可以在多处声明,但是实现只能是一份,否则会报重复定义。

预处理


预处理是C语言的一部分,在编译之前,编译器会对这些预处理命令进行处理,这些预处理的结果与源程序一起编译。

  • 特征
  1. 预处理命令都必须以#开头。
  2. 通常位于程序开头部分。
  • 常用预处理命令
  1. 宏定义:#define,#undef。
  2. 条件编译:#ifdef,#ifndef,#else,#endif。
  3. #include(C),#import(Objective-C)。
  1. 宏并不是C语句,既不是变量也不是常量,所以无需使用=号赋值,也无需用;结束。
  2. 编译器对宏只进行查找和替换,将所有出现宏的地方换成该宏的字符串,因此需要开发者自己保证宏定义是正确的。
  3. 宏可以带参数,最好是将参数用()包住,否则如果参数是个算术式,直接替换会导致结果错误。
  4. 占用代码段,大量使用会导致二进制文件增大。

@class与#import


@class仅仅是告诉编译器有这个类,至于类里有什么信息,这里不需要知道,无法使用该类的实例变量,属性和方法。其编译效率较#import更高,因为#import需要把引用类的所有头文件都走一遍,而@class不用。 #import还会造成递归引用,如果A、B两类只相互引用,不会报错,但是如果任意一方声明了对方的实例,就会报错。

如何禁止调用已有方法?


由于OC中并不能隐藏系统方法,例如我们在实现单例时,为了避免其他人对单例类new、alloc、copy及mutableCopy,保证整个系统中只有一个单例实例,我们可以在头文件中声明不可用的方法,如下:

//更简洁
+(instancetype) alloc NS_UNAVAILABLE;
+(instancetype) new NS_UNAVAILABLE;
-(instancetype) copy NS_UNAVAILABLE;
-(instancetype) mutableCopy NS_UNAVAILABLE;
//能自定义提示语
+(instancetype) alloc __attribute__((unavailable("alloc not available, call sharedInstance instead")));
+(instancetype) new __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) copy __attribute__((unavailable("call sharedInstance instead")));
-(instancetype) mutableCopy __attribute__((unavailable("call sharedInstance instead")));
复制代码

CocoaPods


  • 如何使用? 安装CocoaPods环境后,cd到需要的工程根目录下,通过pod init创建Podfile文件,打开该文件,添加需要的三方库pod 'xxx',保存关闭,输入pod install即可安装成功。 如果成功后import不到三方库的头文件,可以在User header search paths中添加$(SRCROOT)并且选择recursive。

  • 原理 将所有的依赖库都放到另一个名为Pods的项目中,然后让主项目依赖Pods项目,这样源码管理工作就从主项目移到了Pods项目。这个Pods项目最终会变成一个libPods.a的文件,主项目只需要依赖这个.a文件即可。

nil Nil NULL及NSNull 之间的区别


NULL是C语言的用法,此时调用函数或者访问成员变量,会报错。可以用来赋值基本数据类型来表示空。 nil和Nil是OC语法,调用函数或者访问成员变量不会报错,nil是对object对象置为空,Nil是对Class类型的指针置空。 NSNull是一个类,由于nil比较特殊,在Array和Dictionary中被用于标记结束,所以不能存放nil,我们可以通过NSNull来表示该数据为空。但是向NSNull发送消息会报错。

NSDictionary实现原理


NSDictionary(字典)是使用哈希表 Hash table(也叫散列表)来实现的。哈希表是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键(key)值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做哈希表。也就是说哈希表的本质是一个数组,数组中每一个元素其实就是NSDictionary键值对。

.a与.framework


  • 什么是库?

库是共享程序代码的方式,一般分为静态库和动态库。

  • 静态库与动态库的区别?

静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。文件后缀一般为.a,开发者自己建立的.framework是静态库。 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。文件后缀一般为.dylib,系统的. framework就是动态库。

  • .a与.framework的区别

.a是纯二进制文件,还需要.h文件以及资源文件,而.framework可以直接使用。

详见《iOS中.a与.framework库的区别》

响应链


详见《iOS响应链(Responder Chain)》

main()函数之前发生了什么?


详见《iOS 程序 main函数之前发生什么》

@synthesize和@dynamic?


@synthesize语义是如果你没有手动实现setter/getter方法,那么编译器会自动加上这两个方法。可以用来改变实例变量的名称,如@synthesize firstName = _myFirstName;

@dynamic是告诉编译器不需要它自动生成,由用户自己生成(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

  • 有了自动合成属性实例变量之后,@synthesize还有哪些使用场景

我们要搞清楚一个问题,什么情况下不会autosynthesis(自动合成)?

  1. 同时重写了 setter 和 getter 时
  2. 重写了只读属性的 getter 时
  3. 使用了 @dynamic 时
  4. 在 @protocol 中定义的所有属性
  5. 在 category 中定义的所有属性
  6. 重载的属性 当你在子类中重载了父类中的属性,你必须 使用 @synthesize 来手动合成ivar。 除了后三条,对其他几个我们可以总结出一个规律:当你想手动管理 @property 的所有内容时,你就会尝试通过实现 @property 的所有“存取方法”(the accessor methods)或者使用 @dynamic 来达到这个目的,这时编译器就会认为你打算手动管理 @property,于是编译器就禁用了 autosynthesis(自动合成)。 因为有了 autosynthesis(自动合成),大部分开发者已经习惯不去手动定义ivar,而是依赖于 autosynthesis(自动合成),但是一旦你需要使用ivar,而 autosynthesis(自动合成)又失效了,如果不去手动定义ivar,那么你就得借助 @synthesize 来手动合成 ivar。

BAD_ACCESS


访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。

  • 如何调试?
  1. 重写object的respondsToSelector方法,显示出现EXEC_BAD_ACCESS前访问的最后一个object。
  2. 通过Edit Scheme-Diagnostics-Zombie Objects。
  3. 通过全局断点。
  4. 通过Edit Scheme-Diagnostics -Address Sanitizer。
关注下面的标签,发现更多相似文章
评论