iOS 基础知识总结 更新中...

属性

属性与成员变量之间的关系

  • 属性对成员变量扩充了存储方法
  • 属性默认会生成带下划线的成员变量
  • 声明了成员变量不会生成属性

成员变量地址可以根据实例的内存地址偏移寻址。而属性的读写都需要函数调用,相对更慢。self对应类的成员变量的首地址

类别

利用OC的动态运行时为现有类添加方法,您可以在先有类中添加属性,但是不能添加成员变量,但是我们的编译器会把属性自动生成存储方法和带下划线的成员变量,所以我们的声明的属性必须是@dynamic类型的,及需要重写存储方法。

#import "Person.h"

@interface Person (Ex)

@property(nonatomic, strong)    NSString *name;

- (void)setName:(NSString *)name; //***
- (NSString *)name;//***

- (void)method_one;
@end

分类方法实现中可以访问原来类中声明的成员变量。

类别的优缺点

  • 不破坏原有的类的基础之上扩充方法。
  • 实现代码之间的解偶

缺点

  • 无法在类别中添加新的实例变量,类别中没有空间容纳实例变量
  • 命名冲突
    类别无法添加实例变量的原因,从Runtime角度,我们发现的确没有空间容纳实力变量,为什么可以添加属性,原因是属性的本质是实例变量+ getter方法 + setter方法;所以我们重写了set/get方法

同名方法调用的优先级为 分类 > 本类 > 父类

/* 
 *  Category Template //类别模版
 */
typedef struct objc_category *Category;

struct objc_category {
    char *category_name;
    char *class_name;
    struct objc_method_list *instance_methods;
    struct objc_method_list *class_methods;
    struct objc_protocol_list *protocols;
};

扩展

没有名字的类别

类别中创建的方法和属性都是私有的,只有这个类对象可以使用

优点

  • 可以添加实例变量
  • 可以更改读写权限(但是更改的权限的存储方法只能是私有的)
#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,readonly)  NSString *name; //只有getter方法,是公有的

@end


#import "Person.h"

@interface Person ()

@property (nonatomic, readwrite) NSString *name; //更改name的权限,但是setter方法是私有的不能被外部访问

@end

@implementation Person

@end

类别和扩展的区别

就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)extension在编译期决议,它就是类的一部分,但是category则完全不一样,它是在运行期决议的。extension在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它、extension伴随类的产生而产生,亦随之一起消亡。

  • extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension,除非创建子类再添加extension。而category不需要有类的源码,我们可以给系统提供的类添加category。
  • extension可以添加实例变量,而category不可以。
  • extension和category都可以添加属性,但是category的属性不能生成成员变量和getter、setter方法的实现。
    Extension
    在编译器决议,是类的一部分,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
    伴随着类的产生而产生,也随着类的消失而消失。
    Extension一般用来隐藏类的私有消息,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展
    Category
    是运行期决议的
    类扩展可以添加实例变量,分类不能添加实例变量
    原因:因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的。

非正式协议

创建一个NSObject的类别称为"非正式协议"

正式协议

正式协议中可以方法,同时协议也是可以继承的

@protocal MySuperDuberProtocol <MyParentProtocol>

@optional //可选

@required //必须要实现

@end

委托

委托就是某个对象指定另一个对象处理某些特定事物的设计模式

代理主要由三部分组成:

  • 协议:用来指定代理双方可以做什么,必须做什么。
  • 代理:根据指定的协议,完成委托方需要实现的功能。
  • 委托:根据指定的协议,指定代理去完成什么功能。

这里用一张图来阐述一下三方之间的关系:

代理使用原理

在iOS中代理的本质就是代理对象内存的传递和操作,我们在委托类设置代理对象后,实际上只是用一个id类型的指针将代理对象进行了一个弱引用。委托方让代理方执行操作,实际上是在委托类中向这个id类型指针指向的对象发送消息,而这个id类型指针指向的对象,就是代理对象。

代理原理

代理内存管理

为什么我们设置代理属性都使用weak呢?

我们定义的指针默认都是__strong类型的,而属性本质上也是一个成员变量和set、get方法构成的,strong类型的指针会造成强引用,必定会影响一个对象的生命周期,这也就会形成循环引用。


Block

原理:

//最基础的结构体实现
 void (^Blk)(void) = ^(void) {
     };
    
    Blk();

clang -rewrite-objc main.m后得到的结果

/*
// __block_impl 是 block 实现的结构体
struct __block_impl
{
    void *isa; //说明block是一个对象来实现的
    int Flags; //按位承载 block 的附加信息;
    int Reserved; //保留变量
    void *FuncPtr; //函数指针,指向Block需要执行的函数
};
*/

// __main_block_impl_0 是 block 实现的结构体,也是 block 实现的入口
struct __main_block_impl_0 {
  struct __block_impl impl; //实现的结构体变量及__block_impl
  struct __main_block_desc_0* Desc; //描述结构体变量
  //结构体的构造函数,初始化结构体变量impl、Desc
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//__main_block_func_0最终需要执行的函数代码块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

     }

// __main_block_desc_0 是 block 的描述信息结构体
static struct __main_block_desc_0 {
  size_t reserved; //结构体信息保留字段
  size_t Block_size; //结构体大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; //定义一个结构体变量,初始化结构体,计算结构体大小

int main(int argc, const char * argv[]) {

    void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}

isa 指向实例对象,表明 block 本身也是一个 Objective-C 对象。block 的三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,即当代码执行时,isa 有三种值

  • impl.isa = &_NSConcreteStackBlock;
  • impl.isa = &_NSConcreteMallocBlock;
  • impl.isa = &_NSConcreteGlobalBlock;
  1. NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

block 实现的执行流程

main() >> 调用__main_block_impl_0构造函数初始化结构体__main__block_impl_0(__main_block_func_0, __main_block_desc_0_DATA) >> 得到的__main_block_impl_0类型变量赋值给Blk >> 执行Blk->FuncPtr()函数 >> END

带参数的Block

    int intValue = 1;
    
    void (^Blk)(void) = ^(void) {
        NSLog(@"%d",intValue);
     };
    
    Blk();
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int intValue;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int intValue = __cself->intValue; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_r__t4y1n4fj5xlgntt308jvn7c80000gn_T_main_be769b_mi_0,intValue);
     }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {

    int intValue = 1;

    void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue));

    ((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}

原来 block 通过参数值传递获取到 intValue 变量,通过函数

__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue)

保存到 __main_block_impl_0 结构体的同名变量 intValue,通过代码 int intValue = __cself->intValue; 取出 intValue,打印出来。

构造函数 __main_block_impl_0 冒号后的表达式 intValue(_intValue) 的意思是,用 _intValue 初始化结构体成员变量 intValue。

有四种情况下应该使用初始化表达式来初始化成员:

  1. 初始化const成员
  2. 初始化引用成员
  3. 当调用基类的构造函数,而它拥有一组参数时
  4. 当调用成员类的构造函数,而它拥有一组参数时

KVC、KVO

KVC/KVO是观察者模式的一种实现,在Cocoa中是以被万物之源NSObject类实现的NSKeyValueCoding/NSKeyValueObserving非正式协议的形式被定义为基础框架的一部分。从协议的角度来说,KVC/KVO本质上是定义了一套让我们去遵守和实现的方法.当然,KVC/KVO实现的根本是Objective-C的动态性和runtime

KVC定义了一种按名称(字符串)访问对象的机制,而不是访问器

KVC的实现细节

-(void)setValue:(id)value forKey:(NSString *)key;

  1. 首先搜索set方法,有就直接赋值
  2. 如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
    • 返回 NO,则执行setValue:forUNdefinedKey:
    • 返回 YES,则按<key>,<isKey>,<key>,<isKey>的顺序搜索成员
  3. 还没有找到的话,就调用setValue:forUndefinedKey:
 // 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。
 + (BOOL)accessInstanceVariablesDirectly
 
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;

-(id)valueForKey:(NSString *)key;

  1. 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。

  2. 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly

    • 返回 NO,则执行valueForUndefinedKey:
    • 返回 YES,则按<key>,<isKey>,<key>,<isKey>的顺序搜索成员
  3. 还没有找到的话,调用valueForUndefinedKey:

KVC 与点语法比较

用 KVC 访问属性和用点语法访问属性的区别:

  • 用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 KVC 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。

  • 相比点语法用 KVC 方式 KVC 的效率会稍低一点,但是灵活,可以在程序运行时决定访问哪些属性。

  • 用 KVC 可以访问对象的私有成员变量。

KVO 实现原理

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

派生类 NSKVONotifying_Person 剖析:

在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。

因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。

子类 setter 方法剖析:

KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。 当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。 之后,observeValueForKey:ofObject:change:context: 也会被调用。

重写观察属性的 setter 方法这种方式是在运行时而不是编译时实现的。 KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

- (void)setName:(NSString *)newName
{
    [self willChangeValueForKey:@"name"];    // KVO在调用存取方法之前总调用
    [super setValue:newName forKey:@"name"]; // 调用父类的存取方法
    [self didChangeValueForKey:@"name"];     // KVO在调用存取方法之后总调用
}

总结: KVO 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法

系统会动态创建一个继承于 Person 的 NSKVONotifying_Person

person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法

重写NSKVONotifying_Person的setter方法:[super setName:newName]

#import "ViewController.h"
#import "Student.h"


@interface ViewController ()
{
    Student             *_student;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    _student = [[Student alloc] init];
    _student.stuName = @"oldName_hu";

    // 1.给student对象的添加观察者,观察其stuName属性
    [_student addObserver:self forKeyPath:@"stuName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

    // 此时,stuName发生了变化
    _student.stuName = @"newName_wang";
}

// stuName发生变化后,观察者(self)立马得到通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    // 最好判断目标对象object和属性路径keyPath
    if(object == _student && [keyPath isEqualToString:@"stuName"])
    {
        NSLog(@"----old:%@----new:%@",change[@"old"],change[@"new"]);
    }else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}


- (void)dealloc
{
    // 移除观察者
    [_student removeObserver:self forKeyPath:@"stuName"];
}

@end
@interface Target : NSObject
{
    int age;
}

// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }
    
    return self;
}

// for manual KVO - age
- (int) age
{
    return age;
}

- (void) setAge:(int)theAge
{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

@end

///
/**
 *  添加观察者
 *
 *  @param observer 观察者
 *  @param keyPath  被观察的属性名称
 *  @param options  观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
 *  @param context  上下文,可以为nil。
 */
[person addObserver: self
             forKeyPath: @"age"
                options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                context: nil];


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    Person *per = object;
    NSLog(@"keyPath: %@ object: %ld",keyPath, (long)per.age);
}


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

推荐阅读更多精彩内容

  • 喜欢就关注我呗! 1.设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的...
    iOS白水阅读 1,100评论 0 2
  • 1、category和extension的区别 category是分类,可以为类增加自定义方法 extension...
    大猿媛阅读 366评论 1 1
  • category 和 extension 的区别 -分类有名字,类扩展没有分类名字,是一种特殊的分类 -分类只能扩...
    白羊的羊阅读 428评论 0 1
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,135评论 30 470
  • 今天老师布置了家庭作业,请孩子和家长共同完成,这已经不是第一次了,但是和以往不一样的是这次是由孩子自己编故事,家...
    九月桔阅读 311评论 2 0