• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

刨根问底Objective-CRuntime

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

http://chun.tips/blog/2014/11/05/bao-gen-wen-di-objective%5Bnil%5Dc-runtime-(2)%5Bnil%5D-object-and-class-and-meta-class/

刨根问底Objective-C Runtime(1)- Self & Super

刨根问底Objective-C Runtime(2)- Object & Class & Meta Class

刨根问底Objective-C Runtime(3)- 消息和Category

刨根问底Objective-C Runtime(4)- 成员变量与属性

刨根问底Objective-C Runtime(1)- Self & Super

下面的代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
@implementation Son : Father
- (id)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

答案:都输出 Son

1
2
2014-11-05 11:06:18.060 Test[8566:568584] NSStringFromClass([self class]) = Son
2014-11-05 11:06:18.061 Test[8566:568584] NSStringFromClass([super class]) = Son

解惑:这个题目主要是考察关于objc中对 self 和 super 的理解。

self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。而不同的是,super是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

真的是这样吗?继续看:

使用clang重写命令:

1
$ clang -rewrite-objc test.m

发现上述代码被转化为:

1
2
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));

从上面的代码中,我们可以发现在调用 [self class] 时,会转化成 objc_msgSend函数。看下函数定义:

1
id objc_msgSend(id self, SEL op, ...)

我们把 self 做为第一个参数传递进去。

而在调用 [super class]时,会转化成 objc_msgSendSuper函数。看下函数定义:

1
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一个参数是 objc_super 这样一个结构体,其定义如下:

1
2
3
4
struct objc_super {
   __unsafe_unretained id receiver;
   __unsafe_unretained Class super_class;
};

结构体有两个成员,第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self 。第二个成员是记录当前类的父类是什么。

所以,当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。

objc Runtime开源代码对- (Class)class方法的实现:

1
2
3
- (Class)class {
    return object_getClass(self);
}

而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。第二步是去 Father这个类里去找- (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用,此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son。

刨根问底Objective-C Runtime(2)- Object & Class & Meta Clas

本篇笔记主要是讲述objc runtime中关于Object & Class & Meta Class的细节。

习题内容

下面代码的运行结果是?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
        BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
        BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
        NSLog(@"%d %d %d %d", res1, res2, res3, res4);
    }
    return 0;
}

运行结果为:

1
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0

这里先看几个概念

什么是 id

id 在 objc.h 中定义如下:

1
2
/// A pointer to an instance of a class.
typedef struct objc_object *id;

就像注释中所说的这样 id 是指向一个 objc_object 结构体的指针。

id 这个struct的定义本身就带了一个 *, 所以我们在使用其他NSObject类型的实例时需要在前面加上 *, 而使用 id 时却不用。

那么objc_object又是什么呢

objc_object 在 objc.h 中定义如下:

1
2
3
4
/// Represents an instance of a class.
struct objc_object {
    Class isa;
};

这个时候我们知道Objective-C中的object在最后会被转换成C的结构体,而在这个struct中有一个 isa 指针,指向它的类别 Class。

那么什么是Class呢

在 objc.h 中定义如下:

1
2
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

我们可以看到 Class本身指向的也是一个C的struct objc_class。

继续看在runtime.h中objc_class定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
} OBJC2_UNAVAILABLE;

该结构体中,isa 指向所属Class, super_class指向父类别。

继续看

下载objc源代码,在 objc-runtime-new.h 中,我们发现 objc_class有如下定义:

1
2
3
4
5
6
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;   
    ...
    ...
}

豁然开朗,我们看到在Objective-C的设计哲学中,一切都是对象。Class在设计中本身也是一个对象。而这个Class对象的对应的类,我们叫它 Meta Class。即Class结构体中的 isa 指向的就是它的 Meta Class。

Meta Class

根据上面的描述,我们可以把Meta Class理解为 一个Class对象的Class。简单的说:

1
2
当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类的方法列表里查找
当我们发送一个消息给一个类时,这条消息会在类的Meta Class的方法列表里查找

而 Meta Class本身也是一个Class,它跟其他Class一样也有自己的 isa 和 super_class 指针。看下图:

  • 每个Class都有一个isa指针指向一个唯一的Meta Class

  • 每一个Meta Class的isa指针都指向最上层的Meta Class(图中的NSObject的Meta Class)

  • 最上层的Meta Class的isa指针指向自己,形成一个回路

  • 每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class。但是最上层的Meta Class的 Super Class指向NSObject Class本身

  • 最上层的NSObject Class的super class指向 nil

解惑

为了更加清楚的知道整个函数调用过程,我们使用clang -rewrite-objc main.m重写,可获得如下代码:

1
2
3
4
BOOL res1 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isKindOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));
BOOL res2 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));
BOOL res3 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Sark"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));
BOOL res4 = ((BOOL (*)(id, SEL, Class))(void *)objc_msgSend)((id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Sark"), sel_registerName("class")), sel_registerName("isMemberOfClass:"), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class")));

先看前两个调用:

  • 最外层是 objc_msgSend函数,转发消息。

  • 函数第一个参数是 (id)((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class"))

  • 函数第二个参数是转发的selector

  • 函数第三个参数是 ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class"))

我们注意到第一个参数和第三个参数对应重写的是[NSObject class],即使用objc_msgSend向 NSObject Class 发送 @selector(class) 这个消息

打开objc源代码,在 Object.mm 中发现+ (Class)class实现如下:

1
2
3
+ (Class)class {
    return self;
}

所以即返回Class类的对象本身。看如下输出:

1
2
3
4
NSLog(@"%p", [NSObject class]);
NSLog(@"%p", [NSObject class]);
2014-11-05 18:48:30.939 Test[11682:865988] 0x7fff768d40f0
2014-11-05 18:48:30.940 Test[11682:865988] 0x7fff768d40f0

继续打开objc源代码,在 Object.mm 中,我们发现 isKindOfClass的实现如下:

1
2
3
4
5
6
7
8
- (BOOL)isKindOf:aClass
{
    Class cls;
    for (cls = isa; cls; cls = cls->superclass) 
        if (cls == (Class)aClass)
            return YES;
    return NO;
}

对着上面Meta Class的图和实现,我们可以看出

  • 当 NSObject Class对象第一次进行比较时,得到它的isa为 NSObject的Meta Class, 这个时候 NSObject Meta Class 和 NSObject Class不相等。

  • 然后取NSObject 的Meta Class 的Super class,这个时候又变成了 NSObject Class, 所以返回相等。

所以上述第一个输出结果是 YES 。

我们在看下 ‘isMemberOfClass’的实现:

1
2
3
4
- (BOOL)isMemberOf:aClass
{
    return isa == (Class)aClass;
}

综上所述,当前的 isa 指向 NSObject 的 Meta Class, 所以和 NSObject Class不相等。

所以上述第二个输出结果为 NO 。

继续看后面两个调用:

  • Sark Class 的isa指向的是 Sark的Meta Class,和Sark Class不相等

  • Sark Meta Class的super class 指向的是 NSObject Meta Class, 和 Sark Class不相等

  • NSObject Meta Class的 super class 指向 NSObject Class,和 Sark Class 不相等

  • NSObject Class 的super class 指向 nil, 和 Sark Class不相等

所以后面两个调用的结果都输出为 NO 。

刨根问底Objective-C Runtime(3)- 消息 和 Category

本篇笔记主要是讲述objc runtime的 消息和Category。

习题内容

下面的代码会?Compile Error / Runtime Crash / NSLog…?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo
{
    NSLog(@"IMP: -[NSObject(Sark) foo]");
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [NSObject foo];
        [[NSObject new] foo];
    }
    return 0;
}

答案:代码正常输出,输出结果如下:

1
2
2014-11-06 13:11:46.694 Test[14872:1110786] IMP: -[NSObject(Sark) foo]
2014-11-06 13:11:46.695 Test[14872:1110786] IMP: -[NSObject(Sark) foo]

使用clang -rewrite-objc main.m重写,我们可以发现 main 函数中两个方法调用被转换成如下代码:

1
2
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("foo"));
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")), sel_registerName("foo"));

我们发现上述两个方法最终转换成使用 objc_msgSend 函数传递消息。

这里先看几个概念

objc_msgSend函数定义如下:

1
id objc_msgSend(id self, SEL op, ...)

关于 id 的解释请看objc runtime系列第二篇博文: objc runtime中Object & Class & Meta Class的细节

什么是 SEL

打开objc.h文件,看下SEL的定义如下:

1
typedef struct objc_selector *SEL;

SEL是一个指向objc_selector结构体的指针。而 objc_selector 的定义并没有在runtime.h中给出定义。我们可以尝试运行如下代码:

1
2
3
4
5
6
7
SEL sel = @selector(foo);
NSLog(@"%s", (char *)sel);
NSLog(@"%p", sel);
const char *selName = [@"foo" UTF8String];
SEL sel2 = sel_registerName(selName);
NSLog(@"%s", (char *)sel2);
NSLog(@"%p", sel2);

输出如下:

1
2
3
4
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114
2014-11-06 13:46:08.058 Test[15053:1132268] foo
2014-11-06 13:46:08.058 Test[15053:1132268] 0x7fff8fde5114

Objective-C在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID。只要方法名称相同,那么它们的ID就是相同的。

两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么它的SEL就是一样的。每一个方法都对应着一个SEL。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。而SEL本质是一个字符串,所以直接比较它们的地址即可。

当然,不同的类可以拥有相同的selector。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。

那么什么是IMP呢

继续看定义:

1
typedef id (*IMP)(id, SEL, ...);

IMP本质就是一个函数指针,这个被指向的函数包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。因此我们可以通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。

那么 objc_msgSend 到底是怎么工作的呢?

在Objective-C中,消息直到运行时才会绑定到方法的实现上。编译器会把代码中[target doSth]转换成 objc_msgSend消息函数,这个函数完成了动态绑定的所有事情。它的运行流程如下:

  1. 检查selector是否需要忽略。(ps: Mac开发中开启GC就会忽略retain,release方法。)

  2. 检查target是否为nil。如果为nil,直接cleanup,然后return。(这就是我们可以向nil发送消息的原因。)

  3. 然后在target的Class中根据Selector去找IMP

寻找IMP的过程:

  1. 先从当前class的cache方法列表(cache methodLists)里去找

  2. 找到了,跳到对应函数实现

  3. 没找到,就从class的方法列表(methodLists)里找

  4. 还找不到,就到super class的方法列表里找,直到找到基类(NSObject)为止

  5. 最后再找不到,就会进入动态方法解析和消息转发的机制。(这部分知识,下次再细谈)

那么什么是方法列表呢?

上一篇博文中提到了objc_class结构体定义,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
} OBJC2_UNAVAILABLE;
struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}

1) objc_method_list 就是用来存储当前类的方法链表,objc_method存储了类的某个方法的信息。

Method

1
typedef struct objc_method *Method;

Method 是用来代表类中某个方法的类型,它实际就指向objc_method结构体,如下:

1
2
3
4
5
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

method_types是个char指针,存储着方法的参数类型和返回值类型。

SEL 和 IMP 就是我们上文提到的,所以我们可以理解为objc_class中 method list保存了一组SEL<->IMP的映射。

2)objc_cache 用来缓存用过的方法,提高性能。

Cache

1
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

实际指向objc_cache结构体,如下:

1
2
3
4
5
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};
  • mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置

  • occupied: 实际占用cache buckets的总数

  • buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

objc_msgSend每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。

说完了 objc_msgSend, 那么题目中的Category又是怎么工作的呢?

继续看概念

我们知道Catagory可以动态地为已经存在的类添加新的方法。这样可以保证类的原始设计规模较小,功能增加时再逐步扩展。在runtime.h中查看定义:

1
typedef struct objc_category *Category;

同样也是指向一个 objc_category 的C 结构体,定义如下:

1
2
3
4
5
6
7
struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;
    char *class_name                                         OBJC2_UNAVAILABLE;
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

通过上面的结构体,大家可以很清楚的看出存储的内容。我们继续往下看,打开objc源代码,在 objc-runtime-new.h中我们可以发现如下定义:

1
2
3
4
5
6
7
8
struct category_t {
    const char *name;
    clas-s-ref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

上面的定义需要提到的地方有三点:

  1. name 是指 class_name 而不是 category_name

  2. cls是要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象

  3. instanceProperties表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的

为了验证上述内容,我们使用clang -rewrite-objc main.m重写,题目中的Category被编译器转换成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @interface NSObject (Sark)
// + (void)foo;
/* @end */
// @implementation NSObject (Sark)
static void _I_NSObject_Sark_foo(NSObject * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_dd1ee3_mi_0);
}
// @end
static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "NSObject",
    0, // &OBJC_CLASS_$_NSObject,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Sark,
    0,
    0,
    0,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
    &_OBJC_$_CATEGORY_NSObject_$_Sark,
};
  • _OBJC_$_CATEGORY_NSObject_$_Sark是按规则生成的字符串,我们可以清楚的看到是NSObject类,且Sark是NSObject类的Category

  • _category_t结构体第二项 clas-s-ref_t 没有数据,验证了我们上面的说法

  • 由于题目中只有 - (void)foo方法,所以结构体中存储的list只有第三项instanceMethods被填充。

  • _I_NSObject_Sark_foo代表了Category的foo方法,I表示实例方法

  • 最后这个类的Category生成了一个数组,存在了__objc_catlist里,目前数组的内容只有一个&_OBJC_$_CATEGORY_NSObject_$_Sark

最终这些Category里面的方法是如何被加载的呢?

1.打开objc源代码,找到 objc-os.mm, 函数_objc_init为runtime的加载入口,由libSystem调用,进行初始化操作。

2.之后调用objc-runtime-new.mm -> map_images加载map到内存

3.之后调用objc-runtime-new.mm->_read_images初始化内存中的map, 这个时候将会load所有的类,协议还有Category。NSOBject的+load方法就是这个时候调用的

这里贴上Category被加载的代码:

1
2
3
4
5
6

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
Objective-C中Nil和nil发布时间:2022-07-12
下一篇:
iOS--EffectiveObjective-C阅读笔记(7)发布时间:2022-07-12
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap