OC语法相关
1、一般什么情况下用分类
- 用分类可以声明私有属性,但是一般情况下不会用分类(category),而是用类扩张(extention)
- 可以用分类拆解体积庞大的类,比如常把AppDelegate 利用分类拆分成推送,分享,登录等不同功能模块的分类
- 可以用于Hook第三方库的方法,如果你想不修改别人的代码,但是需要修改他们提供的方法,完全可以为需要修改的方法所在类编写一个分类之后hook。
2、分类和类扩张的区别
- extension相当于一个头文件而已,extension(类扩张)只能编写方法声明,所编写的方法和属性都需要在.m文件实现。如果你在extension中书写了属性(property),那么必需要把extension文件import到.m文件中,让系统帮你生成setter和getter方法,或者导入之后自己手动生成setter方法和getter方法。
- 分类(category)有自己的数据结构objc_category,可以编写方法和关联属性。分类编写的方法会在runtime的时候,调用remethodizeClass 把方法整合到类对象中。
- 如果类A有两个分类并且有同名的分类方法,那么后编译的分类的方法会插入到类对象方法列表的前面。之后如果调用该方法,会出发msg_Send方法查找,后编译的分类同名方法会被优先查找到。所以会产生类似后编译分类的同名方法覆盖其他分类的同名方法现象。
- 分类的+load方法会在类的+load方法后调用。分类的+load方法调用顺序和编译顺序有关
- 父类的+load方法调用之后再调用子类的+load方法,父类和子类的+load方法调用完之后再调用分类的+load方法(调用顺序只和编译顺序有关)
- initialize方法会在类第一次接受到消息的时候调用,调用顺序是父类->子类。如果分类有重写initialize方法,则会覆盖父类的initialize。如果多个分类重写了initialize方法,那么就会调用最后编译分类的initialize方法。
- 利用关联对象实现的setter和getter方法,可以被KVO。
继承关系 BeautyCat -> Cat - > Animal()
类和分类 Animal,Animal+bb,Animal+aa,Cat,Cat+bb,Cat+aa,BeautyCat,BeautyCat+aa
测试结果:
/*------------------------------------------------------*
2021-04-15 11:17:46.141459+0800 cccExtention[40555:7540175] +[Animal load]
2021-04-15 11:17:46.159957+0800 cccExtention[40555:7540175] +[Cat load]
2021-04-15 11:17:46.161076+0800 cccExtention[40555:7540175] +[BeautyCat load]
2021-04-15 11:17:46.161541+0800 cccExtention[40555:7540175] +[BeautyCat(aa) load]
2021-04-15 11:17:46.163238+0800 cccExtention[40555:7540175] +[Animal(aa) load]
2021-04-15 11:17:46.163901+0800 cccExtention[40555:7540175] +[Animal(bb) load]
2021-04-15 11:17:46.164747+0800 cccExtention[40555:7540175] +[Cat(aa) load]
2021-04-15 11:17:46.166138+0800 cccExtention[40555:7540175] +[Cat(bb) load]
2021-04-15 11:17:46.252135+0800 cccExtention[40555:7540175] +[Animal(bb) initialize]
2021-04-15 11:17:46.252318+0800 cccExtention[40555:7540175] +[Cat(bb) initialize]
2021-04-15 11:17:46.253647+0800 cccExtention[40555:7540175] +[BeautyCat(aa) initialize]
疑问
- category分类的属性是存储在管理对象上的,那么为什么在attachCategories方法中还需要把property_list_t整合到类中呢?
- 在obj4-781 版本中,attachCategories方法对一个类的分类数量做了限制(64个),如果多出64个会怎么处理?
- xcode 14 如何编译运行obj4-818.2?
3、关联对象
- 系统维护了一个全局 AssociationsManager专门用于管理所有关联对象
- 通过类指针(利用哈希查找)可以快速在AssociationsManager中的AssociationHashMap找到该类的关联对象存储结构ObjectAssociationMap。
- ObjectAssociationMap 的key就是我们调用objc_setAssociatedObject 设置的key,value 是一个ObjcAssociation 对象
- ObjcAssociation对象包含关联属性的值和内存管理方案
- 可以通过把objc_setAssociatedObject方法的参数value设置为nil,删除某个关联对象值.
ObjcAssociation对象 在runtime源码的定义
class ObjcAssociation {
uintptr_t _policy; // 内存管理策略()
id _value; // 存储的值
};
policy 可以取的值
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
示例代码
-(void)setVersion:(NSString * _Nonnull)version
{
objc_setAssociatedObject(self, @selector(version), version, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)version
{
NSString *ver = objc_getAssociatedObject(self, _cmd);
return ver;
}
4、KVO
- KVO 是key-value observing的缩写。
- KVO是 objective-c 对观察者模式的实现。
- 如果一个实例对象ABC添加了KVO,那么系统在运行时会为这个对象生成一个中间类——NSKVONotifying_ABC,之后把实例对象ABC的isa指针指向NSKVONotifying_ABC,NSKVONotifying_ABC的superClass指针指向原本类对象。所以当调用set方法的时候,会先调用NSKVONotifying_ABC的set方法。在NSKVONotifying_ABC 的set方法中会调用willChangeValueForKey 和didChangeValueForKey两个方法。具体实现如下
-(void)setName:(NSString *)name
{
[self willChangeValueForKey:@"name"];
[super setName:name]; //原来的setter方法
[self didChangeValueForKey:@"name"]; // 调用observeValueForKeyPath 方法
}
示例代码
//添加监测网页加载进度的观察者
[self.webView addObserver:self
forKeyPath:NSStringFromSelector(@selector(estimatedProgress))
options:0
context:nil];
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context{
if ([keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]
&& object == _webView) {
NSLog(@"网页加载进度 = %f",_webView.estimatedProgress);
}
}
5、KVC
流程图
- +(BOOL)accessInstanceVariablesDirectly 返回YES,告诉系统寻找相似的key,返回NO告诉系统不找相应的key,默认返回YES,valueForKey方法和setValueForKey方法当没找到key都会导致崩溃
6、isa走位图
isa走位图.png7、 属性关键字有哪些?可以分为几类?
- 属性关键大体上可以分为三类——读写相关,引用计数器相关,原子性操作相关。
读写相关
- readonly,表示这个属性在外部紧紧可读
- readwrite,表示这个属性在外部可读可写
- 如果你要申明一个属性对外部可读,对内部可读可写,那么可以在.h文件用readonly,之后再.m文件再用readwrite
引用计数器内存管理相关
- copy属性关键字常用于修饰不可变对象,比如NSString对象和NSArray对象。不可以对象的copy操作属于浅拷贝,所以不可变对象用strong修饰也是没问题的。So,用retain也一样。
- retain 强引用持有对象,在ARC环境下,用strong取代retain。
- strong 强引用持有对象,会让对象的引用计数器+1
- weak 弱引用,不会导致对象的引用计数器增加,并且对象释放的时候,会把指针置为nil。
- assign 一般来说,用于修饰基本数据结构,在MRC环境下也可以用于修饰对象,但是不会导致对象的引用计数器增加,所以类似 unsafe_unretain,是不安全的。
- unsafe_unretain MRC环境下的属性关键字,引用对象,但是不会导致对象引用的计算器+1,也就是普通的指针(对比C++的智能指针)。
线程安全相关
- nonatomic,非原子操作,默认也是这个属性。所以默认情况下,属性读写是没有线程安全的。
- atomic,原子操作,被这个关键字修饰的属性,读写是线程安全。但是,如何是容器类对象,那么修改容器类对象的内容依然是线程不安全的,所以最好还是自己加锁实现线程安全代码,而不是用atomic。
8、objc源码常见的数据结构
isa_t
isa_t 是一个联合体,部分源码如下
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if SUPPORT_PACKED_ISA
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
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;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
};
可以看出isa_t 只有两个数据成员,一个是指针类型Class cls,一个是unsigned long类型uintptr_t bits,所以整个联合体的内存只有64位。
- nonpointer 表示是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等。
- has_assoc 表示是否存在关联对象。
- has_cxx_dtor:该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象。
- shiftcls 存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针
- magic:判断当前对象是真的对象还是一段没有初始化的空间(有待研究)
- weakly_referenced:是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快
- deallocating:是否正在释放
- has_sidetable_rc:当对象引用计数大于10时,则需要进位
- extra_rc:表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc
objc_object 结构体
struct objc_object {
private:
isa_t isa;
public:
.....
}
可以看到objc_object在源码中只有一个私有成员变量isa,类型是isa_t,这个结构类型是一个联合体,里面只包含64位数据,但是会根据不同的情况,64位数据表示方式有所区别。
接下来就是这个结构体提供给外部使用的方法
//部分objc_object 结构体中的方法如下
//初始化方法
void initIsa(Class cls /*nonpointer=false*/);
void initClassIsa(Class cls /*nonpointer=maybe*/);
void initProtocolIsa(Class cls /*nonpointer=maybe*/);
void initInstanceIsa(Class cls, bool hasCxxDtor);
//isa 类型判断方法
bool hasNonpointerIsa();
bool isTaggedPointer();
bool isBasicTaggedPointer();
bool isExtTaggedPointer();
//内存管理相关方法
id retain();
void release();
id autorelease();
Class 类型
Class 类型在runtime源码定义为
typedef struct objc_class *Class;
objc_class 结构体
objc_class 继承自 objc_object,在runtime的部分代码如下
// objc_class继承于objc_object,因此
// objc_class中也有isa结构体
struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
......
bool isARC() {
return data()->ro->flags & RO_IS_ARC;
}
......
}
objc_class 结构体其实就是我们平时所说的类对象,有指向父类的isa指针(superclass),有方法缓存cache_t,有类的方法,属性,协议(保存在class_rw_t中)等信息。
cache_t 结构体
先看看cache_t 结构体的源码
struct cache_t {
struct bucket_t *_buckets; //散列表
mask_t _mask; //散列表的长度 -1
mask_t _occupied; //已经缓存的数量
......
}
bucket_t 在arm64的定义为
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
可以看到cache_t 是利用散列表来缓存bucket_t对象,而bucket_t保存了缓存key和方法调用的 IMP指针
class_data_bits_t 结构体
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
......
}
- 相当于 unsigned long bits; 占64位 bits实际上是一个地址(是一个对象的指针,可以指向class_ro_t,也可以指向class_rw_t),其实是对class_rw_t 结构体的一个简单封装,提供一些操作class_rw_t的方法
- 为什么要这个设计,有待验证??
class_rw_t 结构体
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
// 方法信息
method_array_t methods;
// 属性信息
property_array_t properties;
// 协议信息
protocol_array_t protocols;
......
}
可以看到class_rw_t 结构体的rw(readwrite)表示可读写的意思,这里保存了类的方法,属性,协议等信息,class_ro_t 结构体也保存了以上信息,但是他是不可读写的(ro,readonly),所以在运行时会把分类的方法,协议等信息动态添加到这里。
method_t 结构体
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
method_t 有三个成员变量
- SEL name 也就是我们调用方法的时候,传递的选择子selector
- const char *types; 方法的返回值和参数,比如@v等字符串(类型编码)
- MethodListIMP imp; 函数体(IMP)
property_t结构体
struct property_t {
const char *name;
const char *attributes;
};
- name 属性名字
- attributes 属性修饰符(nonatomic,readonly,strong等)
9、@dynamic和@synthesize
这两个关键字都是和@property 相关的,如果这两个关键字都没有写,只写了@property来声明属性,那么默认就是@synthesize var = _var;
如果添加了@dynamic 关键字,那么就是告诉编译器自己手动生成setter和getter方法(如果是readonly就只需生成getter),如何你没有生动生成,那么运行时就不会报错。
额外:
.m文件反编译cpp文件的指令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
UI相关
1、UIView和CALayer的关系
持续更新ing
网友评论