属性关键字和所有权修饰符
atomic 修饰的属性是怎么样保存线程安全的?
atomic
原子性(默认),编译器会自动生成互斥锁,对setter和getter方法进行加锁,可以保证属性的赋值和取值
的原子性是线程安全的,但不包括操作和访问
比如说atomic修饰的是一个数组的话,那么我们对数组进行赋值和取值是可以保证线程安全的,但是如果我们对数组进行操作,比如说给数组添加对象或者移除对象,是不在atomic的负责范围之内的,所以给被atomic修饰的数组添加对象或者移除对象是没办法保证线程安全的。
assign修饰对象类型
修饰对象类型时,不增加其引用计数,会产生悬垂指针
(assign修饰的对象在被释放之后,指针仍然指向原对象地址,该指针变为悬垂指针,这时候如果继续通过该指针访问原对象的话,就可能导致程序崩溃)
Category和Extension
Category 的使用场合
① 给一个类添加新的方法,可以为系统的类扩展功能
。
② 分解体积庞大的类文件
,可以将一个类按功能拆解成多个模块,方便代码管理。
③ 创建对私有方法的前向引用
:声明私有方法,把 Framework 的私有方法公开等。直接调用其他类的私有方法时编译器会报错的,这时候可以创建一个该类的分类,在分类中声明这些私有方法(不必提供方法实现),接着导入这个分类的头文件就可以正常调用这些私有方法。
④ 向对象添加非正式协议
:创建一个 NSObject 或其子类的分类称为 “创建一个非正式协议”。
(正式协议
是通过 protocol 指定的一系列方法的声明,然后由遵守该协议的类自己去实现这些方法。而非正式协议是通过给 NSObject 或其子类添加一个分类来实现
。非正式协议已经渐渐被正式协议取代,正式协议最大的优点就是可以使用泛型约束,而非正式协议不可以。)
Category 中都可以添加哪些内容?
实例方法、类方法、协议、属性(只生成 setter 和 getter 方法的声明,不会生成 setter 和 getter 方法的实现以及下划线成员变量
);
默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中,但可以通过关联对象来间接实现这种效果。
Category 的实现原理
① 分类的实现原理取决于运行时决议;
② 同名分类方法谁能生效取决于编译顺序,最后参与编译的分类中的同名方法会最终生效;
③ 分类方法会“覆盖”同名的宿主类(原类)方法,这里说的“覆盖”并不是指原来的方法没了。消息传递过程中优先查找宿主类中靠前的元素,找到同名方法就进行调用,但实际上宿主类中原有同名方法的实现仍然是存在的。我们可以通过一些手段来调用到宿主类原有同名方法的实现,如可以通过Runtime
的class_copyMethodList
方法打印类的方法列表,找到宿主类方法的imp
,进行调用(可以交换方法实现)。
加载处理过程
在编译时,Category
中的数据还没有合并到类中,而是在程序运行的时候通过Runtime
机制将所有分类数据合并到类(类对象、元类对象)中去。下面我们来看一下 Category 的加载处理过程。
① 通过Runtime加载某个类的所有 Category 数据;
② 把所有的分类数据(方法、属性、协议),合并到一个大数组
中;(后面参与编译的 Category 数据,会在数组的前面)
③ 将合并后的分类数据(方法、属性、协议),插入到宿主类原来数据的前面
。(所以会优先调用最后参与编译的分类中的同名方法)
load和 initialize
load方法的调用
① 调用时刻:
+load方法会在Runtime加载类、分类时调用(不管有没有用到这些类,在程序运行起来的时候都会加载进内存,并调用+load方法);
每个类、分类的+load ,在程序运行过程中只调用一次(除非开发者手动调用)。
② 调用方式: 系统自动调用+load 方式为直接通过函数地址
调用,开发者手动调用+load 方式为消息机制objc_msgSend函数调用。
③ 调用顺序:先调用类的+load
,按照编译先后顺序调用(先编译,先调用),调用子类的+load 之前会先调用父类的+load
;再调用分类的+load
,按照编译先后顺序调用(先编译,先调用)(注意:分类的其它方法是:后编译,优先调用)。
initialize 方法的调用
① 调用时刻:+initialize
方法会在类第一次接收到消息时调用。如果子类没有实现+initialize方法,会调用父类的+initialize
,所以父类的+initialize方法可能会被调用多次,但不代表父类初始化多次,每个类只会初始化一次。
② 调用方式: 消息机制objc_msgSend函数调用。
③ 调用顺序: 先调用父类的+initialize,再调用子类的+initialize
(先初识化父类,再初始化子类)
相关面试题
Category 中有 +load 方法吗?+load 方法是什么时候调用的?+load 方法能继承吗?
分类中有+load方法;
+load方法在Runtime加载类、分类的时候调用;
+load方法可以继承,但是一般情况下不会手动去调用+load方法,都是让系统自动调用。
手动调用 Student 类的 +load 方法,但是 Student 类没有实现该方法,为什么会去调用父类的 +load 方法,且是调用父类的分类的 +load 方法呢?
因为+load方法可以继承,[Student load]手动调用方式为是消息机制objc_msgSend函数的调用,会去类方法列表里找对应的方法,由于 Student 类没有实现,就会去父类的方法列表中查找,且优先调用分类的+load方法。而系统自动调用的+load方法是直接通过函数地址调用的。
Swift Objc 混编
Objc override alloc兼容问题
1 | + (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead"); |
在 Swift 中 alloc 方法被禁用,alloc 和 init 自动合并了,内部会处理内存分配。而且在 Swift 中实例化 Objective-C,也不会调用 alloc 方法,也是直接调用构造器方法。
问题描述
在基础库中定义了一个 DefaultLoadingView
1 | @interface DefaultLoadingView : UIView <LoadingViewProtocol> |
业务方可以通过实现DefaultLoadingView+sub
分类来自定义 loadingView
1 | @implementation DefaultLoadingView (sub) |
在 Objective-C 中我们可以直接通过 [[DefaultLoadingView alloc] init]
来创建 loadingView 实例,如果业务方有自定义的那创建的就是 CustomLoadingView
类型,否则就是 DefaultLoadingView
类型。
但在 Swift 中通过 DefaultLoadingView() 创建实例的话,都将是 DefaultLoadingView 类型,因为它不会调用 alloc 方法
。
解决方案一
如果要继续保留采用alloc方案的话,就在Objective-C中包个中间层。这里可以新增一个工厂方法来调用alloc和构造器方法,然后使用 NS_SWIFT_NAME
重命名该工厂方法使其作为构造器导入到Swift中以方便使用,同时使用 NS_SWIFT_UNAVAILABLE
将原先的构造器方法标记为在Swift中不可用且指示调用者使用新的构造器方法。
1 | @interface DefaultLoadingView : UIView <LoadingViewProtocol> |
这个方案的缺点是:如果原先构造器方法有多个,那么就要新增多个对应的工厂方法,并添加 NS_SWIFT_NAME
和 NS_SWIFT_UNAVAILABLE
。
解决方案二
在 Swift 中直接取出想要的那个类,摒弃 alloc 方案,当然你也可以为了向后兼容对其进行保留。通过协议工厂
(维护 protocol
和 implClass
的映射关系),在基础库中对协议 LoadingViewProtocol
注册 defaultImplClass DefaultLoadingView,业务方可以对协议注册 implClass CustomLoadingView。使用的时候通过协议去工厂中取出 implClass 或 implInstance,内部实现是如果未注册 implClass 则取默认的 defaultImplClass 或 defaultImplInstance。
1 | @implementation DefaultLoadingView |