面试驱动技术合集(初中级iOS开发),关注仓库,及时获取更新 Interview-series
runtime
动态将分类的方法合并到类对象、元类对象中使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MNPerson+Test.m
函数,生产一个cpp文件,窥探其底层结构(编译状态)
struct _category_t {
//宿主类名称 - 这里的MNPerson
const char *name;
//宿主类对象,里面有isa
struct _class_t *cls;
//实例方法列表
const struct _method_list_t *instance_methods;
//类方法列表
const struct _method_list_t *class_methods;
//协议列表
const struct _protocol_list_t *protocols;
//属性列表
const struct _prop_list_t *properties;
};
//_class_t 结构
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
category_t
@implementation MNPerson (Test)
- (void)test{
NSLog(@"test - rua~");
}
@end
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
/* 二维数组( **mlists => 两颗星星,一个)
[
[method_t,],
[method_t,method_t],
[method_t,method_t,method_t],
]
*/
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;//宿主类,分类的总数
bool fromBundle = NO;
while (i--) {//倒序遍历,最先访问最后编译的分类
// 获取某一个分类
auto& entry = cats->list[i];
// 分类的方法列表
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
//最后编译的分类,最先添加到分类数组中
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 核心:将所有分类的对象方法,附加到类对象的方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
//realloc - 重新分配内存 - 扩容了
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
//memmove,内存挪动
//array()->lists 原来的方法列表
memmove(array()->lists + addedCount,
array()->lists,
oldCount * sizeof(array()->lists[0]));
//memcpy - 将分类的方法列表 copy 到原来的方法列表中
memcpy(array()->lists,
addedLists,
addedCount * sizeof(array()->lists[0]));
}
...
}
画图分析就是
Framework
的私有方法公开runtime
, 讲Category的数据,+ load
方法,无需导入,无需使用+ load
在程序运行过程中只会执行一次+ load
走的不是消息发送的 objc_msgSend
调用,而是找到 + load
函数的地址,直接调用void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren’t any more
while (loadable_classes_used > 0) {
//先加载宿主类的load方法(按照编译顺序,调用load方法)
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
// 递归调用,先将父类添加到load方法列表中,再将自己加进去
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
+ load
函数 + load
函数 实验证明:宿主类先调用,分类再调用
2019-02-27 17:28:00.519862+0800 load-Initialize-Demo[91107:2281575] MNPerson + load
2019-02-27 17:28:00.520032+0800 load-Initialize-Demo[91107:2281575] MNPerson (Play) + load
2019-02-27 17:28:00.520047+0800 load-Initialize-Demo[91107:2281575] MNPerson (Eat) + load
image
2019-02-27 17:39:10.354050+0800 load-Initialize-Demo[91308:2303030] MNDog + load (宿主类1)
2019-02-27 17:39:10.354237+0800 load-Initialize-Demo[91308:2303030] MNPerson + load (宿主类2)
2019-02-27 17:39:10.354252+0800 load-Initialize-Demo[91308:2303030] MNDog (Rua) + load (分类1)
2019-02-27 17:39:10.354263+0800 load-Initialize-Demo[91308:2303030] MNPerson (Play) + load(分类2)
2019-02-27 17:39:10.354274+0800 load-Initialize-Demo[91308:2303030] MNPerson (Eat) + load(分类3)
2019-02-27 17:39:10.354285+0800 load-Initialize-Demo[91308:2303030] MNDog (Run) + load(分类4)
+ Initialize
走的是消息发送的 objc_msgSend
调用/*父类*/
@interface MNPerson : NSObject
@end
@implementation MNPerson
+ (void)initialize{
NSLog(@"MNPerson + initialize");
}
@end
/*子类1*/
@interface MNTeacher : MNPerson
@end
@implementation MNTeacher
@end
/*子类2*/
@interface MNStudent : MNPerson
@end
@implementation MNStudent
@end
---------------------------------------------
问题出现:以下会输出什么结果
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MNTeacher alloc];
[MNStudent alloc];
}
return 0;
}
结果如下:
2019-02-27 17:57:33.305655+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
2019-02-27 17:57:33.305950+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
2019-02-27 17:57:33.306476+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
exo me? 为啥打印三次呢
原理分析:
initialize
在类第一次接收消息的时候会调用,OC里面的 [ xxx ]
调用都可以看成 objc_msgSend
,所以这时候,[MNTeacher alloc]
其实内部会调用 [MNTeacher initialize]
initialize
调用的时候,要先实现自己父类的 initialize
方法,第一次调用的时候,MNPerson
没被使用过,所以未被初始化,要先调用一下父类的 [MNPerson initialize]
,输出第一个MNPerson + initialize
MNPerson
调用了 initialize
之后,轮到MNTeacher
类自己了,由于他内部没有实现 initialize
方法,所以调用父类的initialize
, 输出第二个MNPerson + initialize
[MNStudent alloc]
,内部也是调用 [MNStudent initialize]
, 然后判断得知 父类MNPerson
类调用过initialize
了,因此调用自身的就够了,由于他和MNTeacher
一样,也没实现initialize
方法,所以同理调用父类的[MNPerson initialize]
,输出第3个MNPerson + initialize
load
or initialize
方法,再调用自己本身的;objc_msgSend
/*父类*/
@interface MNPerson : NSObject
@end
@implementation MNPerson
+ (void)initialize{
NSLog(@"MNPerson + initialize");
}
+ (void)load{
NSLog(@"MNPerson + load");
}
/*子类1*/
@interface MNTeacher : MNPerson
@end
@implementation MNTeacher
+ (void)load{
NSLog(@"MNTeacher + load");
}
/*子类2*/
@interface MNStudent : MNPerson
@end
@implementation MNStudent
+ (void)load{
NSLog(@"MNStudent + load");
}
------------------------------------
问题出现:以下会输出什么结果?
int main(int argc, const char * argv[]) {
@autoreleasepool {
[MNTeacher load];
}
return 0;
}
2019-02-27 18:17:12.034392+0800 load-Initialize-Demo[92064:2370496] MNPerson + load
2019-02-27 18:17:12.034555+0800 load-Initialize-Demo[92064:2370496] MNStudent + load
2019-02-27 18:17:12.034569+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
2019-02-27 18:17:12.034627+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034645+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034658+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
exo me again!怎么这么多!连load 也有了?
解释:
MNPerson + initialize
,因为是MNTeacher
的调用,所以会先让父类MNPerson
调用一次initialize
,输出第一个 MNPerson + initialize
MNPerson + initialize
, MNTeacher
自身调用,由于他自己没有实现 initialize
, 调用父类的initialize
, 输出第二个 MNPerson + initialize
MNTeacher + load
可能其实有点奇怪,不是说 load
只会加载一次吗,而且他还不走 objc_msgSend
吗,怎么还能调用这个方法? load
方法是系统调的,这时候不走 objc_msgSend
[MNTeacher load]
啊,这个就是objc_msgSend(MNTeacher,@selector(MNTeacher)),这就跑到MNTeacher + load
里了!load
函数,但是,还是可以调用的!这道题实际上考的就是关联对象
如果是普通类声明生命属性的话
@interface MNPerson : NSObject
@property (nonatomic, copy)NSString *property;
@end
上述代码系统内部会自动三件事:
get
方法 - (NSString *)property
set
方法 - (void)setProperty:(NSString *)property
@implementation MNPerson{
NSString *_property;
}
- (void)setProperty:(NSString *)property{
_property = property;
}
- (NSString *)property{
return _property;
}
@end
分类也是可以添加属性的 - 类结构里面,有个properties
列表,里面就是
存放属性的;
分类里面,生成属性,只会生成方法的声明,不会生成成员变量 && 方法实现!
人工智障翻译:实例变量不能放在分类中
所以:
不能直接给category 添加成员变量,但是可以间接实现分类有成员变量的效果(效果上感觉像成员变量)
@interface MNPerson (Test)
@property (nonatomic, assign) NSInteger age;
@end
@implementation MNPerson (Test)
@end
person.age = 10
等价于 [person setAge:10]
,所以证明了,给分类声明属性之后,并没有添加其对应的实现!
objc_setAssociatedObject Api
objc_setAssociatedObject( <#id _Nonnull object#>, (对象)
<#const void * _Nonnull key#>,(key)
<#id _Nullable value#>,(关联的值)
<#objc_AssociationPolicy policy#>)(关联策略)
关联策略,等价于属性声明
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
比如这里的age属性,默认声明是@property (nonatomic, assign) NSInteger age;
,就是 assign,所以这里选择OBJC_ASSOCIATION_ASSIGN
取值
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>)
int main(int argc, const char * argv[]) {
@autoreleasepool {
MNPerson *person = [[MNPerson alloc]init];
{
MNPerson *test = [[MNPerson alloc]init];
objc_setAssociatedObject(person,
@"test",
test,
OBJC_ASSOCIATION_ASSIGN);
}
NSLog(@"%@",objc_getAssociatedObject(person, @"test"));
}
return 0;
}
原因,关联的对象是person,关联的value是 test,test变量 出了他们的
{}
作用域之后,就会销毁; 此时通过key 找到 对应的对象,访问对象内部的value,因为test变量已经销毁了,所以程序崩溃了,这也说明了 => 内部 test 对 value是强引用!
在分类中,因为类的实例变量的布局已经固定,使用 @property 已经无法向固定的布局中添加新的实例变量(这样做可能会覆盖子类的实例变量),所以我们需要使用关联对象以及两个方法来模拟构成属性的三个要素。
引用自 关联对象 AssociatedObject 完全解析
实现关联对象技术的核心对象有
class AssociationsManager {
static spinlock_t _lock;//自旋锁,保证线程安全
static AssociationsHashMap *_map;
}
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap>
class ObjectAssociationMap : public std::map<void *, ObjcAssociation>
class ObjcAssociation {
uintptr_t _policy;
id _value;
}
以关联对象代码为例:
objc_setAssociatedObject(obj, @selector(key), @"hello world", OBJC_ASSOCIATION_COPY_NONATOMIC);
AssociationsManager
中ObjcAssociation
对象,关联的 value
就放在 ObjcAssociation
内AssociationsManager
管理并在 AssociationsHashMap
存储ObjectAssociationMap
以键值对的形式存储在 AssociationsHashMap
中ObjectAssociationMap
则是用于存储关联对象的数据结构has_assoc
指示对象是否含有关联对象AssociationsManager
内部有一持有一个_lock
,他其实是一个spinlock_t(自旋锁),用来保证AssociationsHashMap
操作的时候,是线程安全的Category
相关的问题一般初中级问的比较多,一般最深的就问到关联对象
,上面的问题以及解答已经把比较常见的 Category
的问题都罗列解决了一下,如果还有其他常见的 Category
的试题欢迎补充~
传言的互联网寒冬貌似真的来临了,在这种环境下,无法得知公司是否不裁员,还是让自己?起来!19年的 铜三铁四 从明天就要开始拉开帷幕了,也希望近期找工作的iOS们能找到一份满意的工作,看下寒冬下,iOS开发是不是叕没人要了~
本文基于 MJ老师 的基础知识之上,结合了包括 draveness 在内的一系列大神的文章总结的,如果不当之处,欢迎讨论~
友情演出:小马哥MJ
参考资料: