iOS开发小记-基础篇

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

KVC


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

  • 底层实现机制
- (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便可。bash

  • 如何校验KVC的正确性?

开发中可能有些须要设定对象属性不能够设置某些值,此时就须要检验Value的可用性,经过以下方法数据结构

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

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

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

没有找到相关key,会抛出NSUndefinedKeyException异常,使用KVC时通常须要重写这两个方法。编辑器

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

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

  • 常见应用场景
  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"];
复制代码

@"hello"位于常量池中,可重复使用,其中A和B指向的都是同一分内存地址。 而[NSString stringWithFormat:@"hello"]是在运行时建立出来的,保存在运行时内存(即堆内存),其中C和D指向的内存地址不一样,与A、B也不相同。

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。
相关文章
相关标签/搜索