iOS OC基础知识笔记

iOS OC基础知识笔记

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

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

#import 
#import 
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找父类
iOS OC基础知识笔记_第1张图片

  • 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 {}

class ObjectAssociationMap : public std::map {}

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
}

关联对象并不是存储在被关联对象本身内存中
关联对象存储在全局的统一的一个AssociationsManager中
设置关联对象为nil,就相当于是移除关联对象

iOS OC基础知识笔记_第2张图片

  • 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的变量捕获机制
iOS OC基础知识笔记_第3张图片

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>

iOS OC基础知识笔记_第4张图片

每一种类型的block调用copy后的结果如下所示
iOS OC基础知识笔记_第5张图片

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

  1. block作为函数返回值时
  2. 将block赋值给__strong指针时
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时
  4. 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被拷贝到堆上

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

如果block从堆上移除

  1. 会调用block内部的dispose函数
  2. dispose函数内部会调用_Block_object_dispose函数
  3. _Block_object_dispose函数会自动释放引用的auto变量(release)
  • __block的作用是什么?有什么使用注意点?
  1. __block用于修改block中无法修改的值。
  2. __block不能修饰全局变量、静态变量(static)
  3. 编译器会将__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的指针,并不是进行修改。

你可能感兴趣的:(iOS)