iOS OC基础知识笔记

    xiaoxiao2023-11-24  169

    iOS OC基础知识笔记

    一个NSObject对象占用多少内存?

    系统分配了16个字节给NSObject对象(通过malloc_size函数获得) 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

    #import <objc/runtime.h> #import <malloc/malloc.h> NSObject * p = [[NSObject alloc] init]; NSLog(@" %zd", class_getInstanceSize([p class])); NSLog(@" %zd", malloc_size((__bridge const void *)(p))); 源码下载地址: https://opensource.apple.com/source/ class_getInstanceSize是获取类实例对象的大小 在objc源码中查找class_getInstanceSize的实现 size_t class_getInstanceSize(Class cls) { if (!cls) return 0; return cls->alignedInstanceSize(); } // Class's ivar size rounded up to a pointer-size boundary. uint32_t alignedInstanceSize() { return word_align(unalignedInstanceSize()); } 将Objective-C代码转换为C\C++代码,查看底层实现 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件 如果需要链接其他框架,使用-framework参数。比如-framework UIKit 如果需要支持arc和runtime,使用-fobjc-arc和-fobjc-runtime=ios-8.0.0参数 NSObject对象的底层实现 struct NSObject_IMPL { Class isa; }; 一个对象继承与NSObject @interface TestObject : NSObject { int _age; double _price; } @end 查看转换成底层C++代码的实现 struct TestObject_IMPL { struct NSObject_IMPL NSObject_IVARS; // 等价于 Class isa; int _age; double _price; }; 对象的isa指针指向哪里?

    instance对象的isa指向class对象 class对象的isa指向meta-class对象 meta-class对象的isa指向基类的meta-class对象

    TestObject * p = [[TestObject alloc] init]; 获取一个实例对象的类对象 [p class] 获取一个实例对象的元类对象 id metaClass = object_getClass([p class]); 判断是否是元类对象 NSLog(@" %d" , class_isMetaClass(metaClass)); NSLog(@" %d" , class_isMetaClass([p class]));

    每个类在内存中有且只有一个class对象 objectMetaClass是NSObject的meta-class对象(元类对象) 每个类在内存中有且只有一个meta-class对象 meta-class对象和class对象的内存结构是一样的,但是用途不一样

    instance的isa指向class 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用 class的isa指向meta-class 当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用

    总结: instance的isa指向class class的isa指向meta-class meta-class的isa指向基类的meta-class

    class的superclass指向父类的class,如果没有父类,superclass指针为nil meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class

    instance调用对象方法的轨迹,isa找到class,方法不存在,就通过superclass找父类 class调用类方法的轨迹,isa找meta-class,方法不存在,就通过superclass找父类

    OC的类信息存放在哪里?

    对象方法、属性、成员变量、协议信息,存放在class对象中 类方法,存放在meta-class对象中 成员变量的具体值,存放在instance对象中

    查看objc源码查看类底层实现代码

    在objc-runtime-new.h中 struct objc_class : objc_object { Class superclass; cache_t cache; class_data_bits_t bits; class_rw_t *data() {} } 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 firstSubclass; Class nextSiblingClass; char *demangledName; } struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; #ifdef __LP64__ uint32_t reserved; #endif const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; method_list_t *baseMethods() const { return baseMethodList; } }; iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

    利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数

    _NSSetXXXValueAndNotify函数中的实现原理: willChangeValueForKey: 父类原来的setter didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:) 如何查看重写的set方法的内部实现呢? 利用Runtime方法获取添加KVO之后的setKey方法的实现 NSLog(@"添加KVO监听之后 - %p", [object methodForSelector:@selector(setAge:)]); 利用lldb打印该地址的实现 (lldb) po (IMP)0x10e33dcf2 (Foundation`_NSSetIntValueAndNotify) 如何窥探Foundation框架中的_NSSetIntValueAndNotify方法的内部实现呢? 1、利用逆向知识在越狱手机找到动态库共享缓存/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 2、拷贝到电脑利用dsc_extractor将Foundation框架拆分出来 ./dsc_extractor dyld_shared_cache_arm64 ./frameworks 3、利用hopper或者IDA查看_NSSetIntValueAndNotify汇编实现 如何手动触发KVO?

    手动调用willChangeValueForKey:和didChangeValueForKey:

    直接修改成员变量会触发KVO么?

    不会触发KVO

    通过KVC修改属性会触发KVO么?

    会触发KVO

    KVC的赋值和取值过程是怎样的?原理是什么? - (void)setValue: forKey:的赋值过程 首先按照setkey、_setkey的顺序寻找该方法为key赋值,如果没有找到两个方法,则会查看+ (BOOL)accessInstanceVariablesDirectly方法返回值,如果返回NO,则会抛出NSUnknownKeyException异常,如果返回YES,则按照_key、_isKey、key、isKey的顺序寻找成员变量的顺序赋值,如果都没有,则会抛出NSUnknownKeyException异常 验证setkey、_setkey方法 object中是没有name属性的 [object setValue:@"zhangsan" forKey:@"name"]; 实现两个方法验证,然后屏蔽改方法 - (void)setName:(NSString *)name { NSLog(@"setName"); } - (void)_setName:(NSString *)name { NSLog(@"_setName"); } 验证_key、_isKey、key、isKey的顺序 依次屏蔽属性打断点验证 + (BOOL)accessInstanceVariablesDirectly { return YES; } @interface TestObject : NSObject { NSString * _name; // 1 NSString * name; // 3 NSString * _isName; // 2 NSString * isName; // 4 } @end - (id)valueForKey:取值过程 首先按照getKey、key、isKey、_key的顺序查找方法获取返回值,如果都没有,则会获取accessInstanceVariablesDirectly方法的返回值,返回NO,抛出NSUnknownKeyException异常,如果返回YES、会按照_key、_isKey、key、isKey的顺序获取属性的值,如果都没有找到则会抛出NSUnknownKeyException异常 Category的实现原理

    Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中),后参与编译的Category数据会在前面。

    将类目文件转换成C++查看类目编译结构 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc TestObject+CategoryOne.m -o testobject_category.cpp struct _category_t { const char *name; 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; }; objc中的源码结构 struct category_t { const char *name; classref_t cls; struct method_list_t *instanceMethods; struct method_list_t *classMethods; struct protocol_list_t *protocols; struct property_list_t *instanceProperties; // Fields below this point are not always present on disk. struct property_list_t *_classProperties; method_list_t *methodsForMeta(bool isMeta) { if (isMeta) return classMethods; else return instanceMethods; } property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); }; objc关于objc的源码解读顺序 objc-os.mm 1、_objc_init 2、map_images 3、map_images_nolock objc-runtime-new.mm 1、_read_images 2、remethodizeClass 3、attachCategories 4、attachLists 5、realloc、memmove、 memcpy Category和Class Extension的区别是什么?

    Class Extension在编译的时候,它的数据就已经包含在类信息中 Category是在运行时,才会将数据合并到类信息中

    Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

    有load方法 load方法在runtime加载类、分类的时候调用 load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

    每个类、分类的+load,在程序运行过程中只调用一次 调用顺序: 1、先调用类的+load 按照编译先后顺序调用(先编译,先调用) 调用子类的+load之前会先调用父类的+load 2、再调用分类的+load 按照编译先后顺序调用(先编译,先调用)

    objc4源码解读过程:objc-os.mm _objc_init load_images 1. prepare_load_methods 2. schedule_class_load 3. add_class_to_loadable_list 4. add_category_to_loadable_list call_load_methods 1. call_class_loads 2. call_category_loads 3. (*load_method)(cls, SEL_load) +load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用 load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程? load、initialize方法的区别什么? 1.调用方式 1> load是根据函数地址直接调用 2> initialize是通过objc_msgSend调用 2.调用时刻 1> load是runtime加载类、分类的时候调用(只会调用1次) 2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次) load、initialize的调用顺序? 1.load 1> 先调用类的load a) 先编译的类,优先调用load b) 调用子类的load之前,会先调用父类的load 2> 再调用分类的load a) 先编译的分类,优先调用load 2.initialize 1> 先初始化父类 2> 再初始化子类(可能最终调用的是父类的initialize方法) objc4源码解读过程: objc-msg-arm64.s 1. objc_msgSend objc-runtime-new.mm 1. class_getInstanceMethod 2. lookUpImpOrNil 3. lookUpImpOrForward 4. _class_initialize 5. callInitialize 6. objc_msgSend(cls, SEL_initialize) Category能否添加成员变量?如果可以,如何给Category添加成员变量?

    不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果。

    关联对象

    默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现。

    添加关联对象 void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy) 获得关联对象 id objc_getAssociatedObject(id object, const void * key) 移除所有的关联对象 void objc_removeAssociatedObjects(id object) key的常见用法 1. static void *MyKey = &MyKey; 2. static char MyKey; 3. 使用属性名作为key 4. 使用get方法的@selecor作为key 原理: 核心对象 AssociationsManager AssociationsHashMap ObjectAssociationMap ObjcAssociation objc4源码解读:objc-references.mm void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy) class AssociationsManager { // associative references: object pointer -> PtrPtrHashMap. static AssociationsHashMap *_map; }; class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *> {} class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {} class ObjcAssociation { uintptr_t _policy; id _value; } 关联对象并不是存储在被关联对象本身内存中 关联对象存储在全局的统一的一个AssociationsManager中 设置关联对象为nil,就相当于是移除关联对象

    block的原理是怎样的?本质是什么?

    封装了函数调用以及调用环境的OC对象

    利用clang查看Block编译成C++后的代码结构 int main(int argc, const char * argv[]) { @autoreleasepool { void(^block)(void) = ^(void){ NSLog(@"block"); }; block(); } return 0; } C++结构代码: int main(int argc, const char * argv[]) { { void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); } return 0; } struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* 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; } }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_t7_fvvtxpcs1ysg4pb96h77scv40000gn_T_main_70e44b_mi_0); } 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)};

    block的变量捕获机制

    auto变量就是普通的局部变量

    block的3种类型

    __NSGlobalBlock__ ( _NSConcreteGlobalBlock ) __NSStackBlock__ ( _NSConcreteStackBlock ) __NSMallocBlock__ ( _NSConcreteMallocBlock ) MRC模式下 int a = 5; void(^block)(void) = ^(void){ NSLog(@"block a = %d", a); }; static int b = 5; void(^block1)(void) = ^(void){ NSLog(@"block b = %d", b); }; NSLog(@"%@", block); NSLog(@"%@", block1); NSLog(@"%@", [block copy]); block = <__NSStackBlock__: 0x7ffeefbff558> block1 = <__NSGlobalBlock__: 0x100001078> [block copy] = <__NSMallocBlock__: 0x10078d190>

    每一种类型的block调用copy后的结果如下所示

    在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

    block作为函数返回值时将block赋值给__strong指针时block作为Cocoa API中方法名含有usingBlock的方法参数时block作为GCD API的方法参数时

    block访问了对象类型的局部变量,编译成C++的代码中增加了内存管理的代码

    block = ^(void){ NSLog(@"name = %@", object.name); }; C++代码: static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->object, (void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->object, 3/*BLOCK_FIELD_IS_OBJECT*/);} static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

    当block内部访问了对象类型的auto变量时 如果block是在栈上,将不会对auto变量产生强引用

    如果block被拷贝到堆上

    会调用block内部的copy函数copy函数内部会调用_Block_object_assign函数_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

    如果block从堆上移除

    会调用block内部的dispose函数dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的auto变量(release) __block的作用是什么?有什么使用注意点? __block用于修改block中无法修改的值。__block不能修饰全局变量、静态变量(static)编译器会将__block变量包装成一个对象 编译成C++代码分析: __block int a = 5; block = ^(void){ a = 10; NSLog(@"a = %d", a); }; 在使用__blcok修饰变量之后,实际上a变成了一个对象。 C++代码分析: struct __Block_byref_a_0 { void *__isa; __Block_byref_a_0 *__forwarding; int __flags; int __size; int a; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_a_0 *a; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 在访问a的时候(a->__forwarding->a) = 10;

    对象类型的局部变量(auto) 当block在栈上时,对它们都不会产生强引用

    对象类型的编译源码查看: 普通的对象类型 block = ^(void){ NSLog(@"name = %@", object); }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; TestObject *__strong object; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, TestObject *__strong _object, int flags=0) : object(_object) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 使用__weak修饰的对象类型 __weak typeof(object) weakObject = object; TestBlock block; block = ^(void){ NSLog(@"name = %@", weakObject); }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; TestObject *__weak weakObject; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, TestObject *__weak _weakObject, int flags=0) : weakObject(_weakObject) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 使用__block修饰的对象类型 __block typeof(object) strongObject = object; TestBlock block; block = ^(void){ NSLog(@"name = %@", strongObject); }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_strongObject_0 *strongObject; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_strongObject_0 *_strongObject, int flags=0) : strongObject(_strongObject->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 当block拷贝到堆上时,都会通过copy函数来处理它们 __block变量(假设变量名叫做a) _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/); 对象类型的auto变量(假设变量名叫做p) _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/); 当block从堆上移除时,都会通过dispose函数来释放它们 __block变量(假设变量名叫做a) _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/); 对象类型的auto变量(假设变量名叫做p) _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

    block的属性修饰词为什么是copy?使用block有哪些使用注意? block一旦没有进行copy操作,就不会在堆上 使用注意:循环引用问题

    block在修改NSMutableArray,需不需要添加__block? 不需要,block内部操作的是NSMutableArray的指针,并不是进行修改。

    最新回复(0)