OC 学习记录随笔

全是随笔 笔记。没有规律。

动态特性 VS 静态特性

  • OC: 动态类型(id)、多态绑定([obj msgSend])、 多态加载(图片2x3x替换,动态加方法和变量)

OC Alloc Init New

  • alloc 开辟内存,绑定指针isa
  • init return(id)self,工厂构造方法,工厂设计,给开发者初始化数据提供重写入口

内存检测

  • 僵尸对象zombie object :已经被系统回收的内存,但是没有置空, 随时可能被其他申请和覆盖.
  • 内存,进程分别有虚拟页面,在去访问物理内存。只有活跃内存加载到物理内存。ios每页为16k,mac和linux为4k
  • 虚拟内存,解决了安全问题和内存不够用,因为不能直接访问,而且不会全部加载
  • 缺页异常,页面置换, ios软件被杀死,点开重启

OC 二进制重排

  • xcode 里面设置linkmap选项,会在编译地址内多出来当前项目的order文件
  • 修改order文件后,xcode里面设置导入当前order文件的地址进行导入
  • 为了将常用的代码或者启动代码全部方法比较靠前的二进制的内存页面加载,减少内存页的频繁申请和覆盖,(因为大部分都是经常活跃的方法的内存页)

.m 编译成 cpp文件

使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm63.cpp 而不是 clang -rewrite-objc main.m -o main.cpp
因为前者指定了sdkiphone 并且 指定了架构是arm64, 所以生产的代码更具针对性。代码也要少很多。

获取对象类的内存大小

 NSObject *objclass = [[NSObject alloc] init];
  // NSObject 类实例对象大小(成员变量所占用的内存大小)  //8
  NSLog(@"%zd",       class_getInstanceSize([objclass class]));
  
  //所指obj指针内存大小 //16
  NSLog(@"%zd", malloc_size((__bridge const void*) objclass));
  
  //TODO  //8
  NSLog(@"%zd",       sizeof(objclass));

16进制和位

0x100749340 -> 89 6C DF 4A F8 FF 1D 01 代表的使用16进制来表示。1个16进制数代表 4位(16进制的F 表示为 1111)。
1个字节是8位,二进制8位:xxxxxxxx 范围从00000000-11111111,表示0到255。一位16进制数(用二进制表示是xxxx)最多只表示到15(即对应16进制的F),要表示到255,就还需要第二位。所以1个字节=2个16进制字符,一个16进制位=0.5个字节。

综上:0x100749340 -> 89 6C DF 4A F8 FF 1D 01 占了8字节
取自 NSObject的内存布局。

大端小端

如果把一个数看成一个字符串,比如11223344看成"11223344",末尾是个’\0’,'11’到’44’个占用一个存储单元,那么它的尾端很显然是44,前面的高还是低就表示尾端放在高地址还是低地址,它在内存中的放法非常直观,如下图:
请添加图片描述

  • 大端: 高尾端: 代表高地址存放 的 最后面的字符 44。 正序。
  • 小端:低尾端:代表高地址存放的是 最前面的字符 11。 iOS就是小端模式。 反序。

NSObject 分配内存大小

@interface Student : NSObject {
    
    
    int _no;
    int _age;
    int _height;
}. 
@end. 

Student 的结构体isa 8字节对齐后需要 24字节内存。但是使用calloc方式开辟内存,calloc 会调用libmalloc库中 nano_calloc 方法里面具体下面方法的开辟内存方法。 进行16字节的倍数分配(代码见下方segregated_size_to_fit)。最大数量为#define NANO_MAX_SIZE 256.
所以最终内存大小为32
calloc 开辟的内容会自动填充为0, malloc 则不会

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    
    
	size_t k, slot_bytes;

	if (0 == size) {
    
    
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
	slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
	*pKey = k - 1;													// Zero-based!

	return slot_bytes;
}

OC 对象的分类

  • instance对象(实例对象):通过alloc出来的对象,每次调用alloc都会产生新的instance对象。 Student *stu = [[Student alloc] init]
  • class 对象(类对象), 每个类只有一个对象
  • meta-class对象(元类对象), 每个类只有一个元类对象

实例对象

  • 存放 isa,成员变量的具体指

类对象

NSObject *ob = [[NSObject alloc] init];
Class objClass = [ob class];  
Class objClass1 = object_getClass([NSObjcet class]); 

存储:

  • isa 指针
  • superClass 指针
  • 类的属性信息(@property
  • 类的对象方法信息(instance method
  • 类的协议信息(protocol
  • 类的成员变量信息(ivar)(放类型名字之类)

元类对象

Class objClass = [NSObject class];  
Class metaClass = object_getClass(objClass); //元类对象

//class_isMetaClass 可判读是否是元类对象
  • object_getClass传入类对象得到元类对象。
  • [[NSObject class] class] 无论多少次都是实例对象
    存储:
  • 和class对象是一样的,只是其他部分存储的是空值
  • isa 指针
  • superClass 指针
  • 类的类方法(工厂方法)信息(class method

获取类对象 的 方式

  • Class _Nullable objc_getClass(const char * _Nonnull name) 返回类对象(只传入类名,所以只能返回固定的一种对象类型)
  • Class _Nullable object_getClass(id _Nullable obj) 返回类对象或者元类对象
  • + (Class)class 返回类对象

isa 、superClass

在这里插入图片描述

  • instance 的 isa 指向 class
  • class 的 isa 指向 metaClass
  • metaClass的isa 指向基类的 metaClass (特殊
  • class的superClass 指向父类的class
  • metaClass的superClass指向父类的metaClass
  • 如果没有父类,superClass为nil
  • 基类metaClass的superClass指向 基类的 class (特殊

ISA_MASK TODO:

#     define ISA_MASK   0x0000000ffffffff8ULL
  • isa 指针需要mask才能获取到真正的 指针
  • superClass 不需要mask值就直接获取到父类的指针

缩写全称

class_rw_t;
rw -> readwrite 
t -> table
class_ro_t;
ro -> readonly

oc 对象存储位置:

objc-runtimme-new.h -> struct objc_class : objc_object -> class_rw_t *-> class_rw_ext_t(class_ro_t)

objc_class 结构体主要成员如下:

	Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

获取路径如下
在这里插入图片描述

KVO

会在runtime 生成 中间类 NSKVONotifying_Person , person 的类的isa 指针指向 中间类,然后中间类的superClass指向person类。

过程:

  • 利用runtime生成中间kvo类,并且让自己的实例对象的isa指向这个中间类对象
  • 中间类重写了监控对象的set方法
  • 当修改监控的对象的属性的时候,并在set方法内部调用foundation库的函数_NSSetXXXValueAndNotify
  • 调用willChangeValueForKey: (可重写验证)
  • 父类原来的set方法
  • 调用didChangeValueForKey:(可重写验证)
  • 内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObject:change:context)的方法

也就是说只要在未生成set方法 self->age = 5 的调用前后 手动调用willChangeValueForKeydidChangeValueForKey 就可以触发系统的监听函数。

通过方法名字查找runtime方法地址名字

    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"11111"];
    NSLog(@"before: %p", [self.person methodForSelector:@selector(setAge:)]);

通过上述打印笔记: 的地址,在 lldb 调试出runtime的运行时方法:

// p (IMP)0x7fff207a3963

nm 工具

nm Foundation | grep xxx 可以反汇编 Foundation 及其他框架的方法名字列表, 但是前提需要iOS的二进制框架

KVC

setValue:forKey: 的原理(设置)

在这里插入图片描述

valueForKey: 的原理 (获取)

  • 调用valueForKey
  • 按照getKey key isKey _key 的顺序查找方法 -> 找到后直接调用
  • 没有找到后,查看 accessInstanceVariablesDirectly 的返回值是不是 YES, 如果为 NO -> 抛出最后的错误
  • YES -> 查找成员变量,顺序为 _key _isKey key isKey -> 找到后直接取值
  • 没有找到 -> 抛出错误 -> 调用valueForUnderfinedKey: 并抛出异常 NSUnknownKeyException
    在这里插入图片描述

KVC 修改会触发 KVO监听么

会触发, 如果没有生成 set方法, 那么他的内部应该会手动调用 手动调用willChangeValueForKeydidChangeValueForKey 就可以触发系统的监听函数。

Category

会在运行时将编译生成的stuct提取到原有类的属性的最前面,具体的排序是编译的反顺序

编译生成的cpp实现struct

在编译期间生成cpp文件可以查看,创建了 _category_t 结构体,并赋值

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; //只存在属性,没有成员变量(ivar)
};

static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    
    
	"MJPerson", // name
	0, // &OBJC_CLASS_$_MJPerson,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,  //实例方法名字
	0,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_MJPerson_$_Eat,  //properties
};

运行时绑定步骤

  • _objc_init() 所有运行时的主入口
  • _dyld_objc_notify_register(&map_images, load_images, unmap_image);
  • map_images
  • map_images_nolock
  • _read_images
  • load_categories_nolock(hi);
  • attachCategories() mlists、proplists、protolists 分别获取所有的Category的数据存入额弱鸡
  • void attachLists(List* const * addedLists, uint32_t addedCount) 将上步骤的方法属性等,分别调用此方法插入到原方法属性的前面(malloc所有对象)

Category 和 class extension 的区别

  • class extension 在编译的时候,数据就已经包含在类的信息中了。
  • Category是在运行时,才会合并在类的信息中
  • class extension 必须在源码的m文件中编写,作为类的私有成员

添加属性(间接添加成员变量)

  • Category 不能添加成员变量,只能添加属性。struct _category_t 因为里面并没有存成员变量的属性(ivar
  • 添加属性后,必须手动实现 set get方法,并且 调用 objc_setAsscoiatedObject() 来关联对象。
  • TOTO 需看 objc_setAsscoiatedObject() 源码
@property (nonatomic, copy) NSString *mname;

//m 里面实现
#define KeyName = "mname"
- (void)setMname:(NSString *)mname {
    
    
 //第二个参数需要传入 void *的地址 @selector(mname) 方法地址,或者 &KeyName 的地址都可以。随意使用,注意和get方法使用key相同即可。
    objc_setAssociatedObject(self, @selector(mname), mname, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)mname {
    
    
    // _cmd == 当前方法的地址(每个方法传入的隐藏参数)
    return objc_getAssociatedObject(self, @selector(mname));
}

objc_setAsscoiatedObject 源码探析

在这里插入图片描述

AssociationsManager 
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
class ObjcAssociation {
    
    
    uintptr_t _policy;
    id _value;
    };

涉及到的上层api:

  • id _object_get_associative_reference(id object, const void *key) 获取key 的value
  • void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) 设为nil就会移除当前的key
  • void _object_remove_assocations(id object, bool deallocating) 移除某个对象的所有关联

+load 方法

源码步骤

  • _objc_init() 所有运行时的主入口
  • _dyld_objc_notify_register(&map_images, load_images, unmap_image);
  • load_images
  • call_load_methods();
  • call_class_loads(); call_category_loads(); 分别单独调用类的loadCategoryload方法
  • (*load_method)(cls, @selector(load)); 函数的指针进行调用
    在这里插入图片描述

初步结论

所以load方法是在main之前调用
所以Categoryload方法不会覆盖原来类的load方法
+load() 通过函数的指针进行调用,不是通objc_msgSend()调用,所以不会去查找isa指针指向的类对象,所以不会调用到被覆盖的load函数。
app的生命周期中,每个类的load方法只会调用一次

load个各类的调用顺序

classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
    
    
        schedule_class_load(remapClass(classlist[i]));
}
.......

// Recursively schedule +load for cls and any un-+load-ed superclasses.
// cls must already be connected.
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 优先调用 父类
    schedule_class_load(cls->getSuperclass());

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

实例对象

  • _getObjc2NonlazyClassList 获取编译顺序的实例对象
  • cls->getSuperclass() 每个实例类,优先递归调用父类的load 方法加载到 数组中
  • 结论:先调用编译顺序的依次调用,每个类调用的时候,迭代调用父类,加入loadable_classes, 然后依次调用。

Category类

  • 直接按照 编译顺序 依次加入loadable_categories数组,然后依次调用

+initialize 方法

第一次接受消息的时候调用,例如([Person alloc]), 如果没有地方调用则不会被自动调用。

+initilaze 方法是通过 objc_msgSend() 方法调用的,所以只会有其中的一个分类或者类的方法被调用。

调用流程

通过汇编来直接调用的。 void callInitialize(Class cls) asm("_CALLING_SOME_+initialize_METHOD"); ,代码中有这个方法,所以在代码中直接断点 initialize 方法,会看到堆栈中的 方法名字叫做 _CALLING_SOME_+initialize_METHOD

  • 。。。暂时找不到
  • Method class_getClassMethod(Class cls, SEL sel) //class_getClassMethod. Return the class method for the specified class and selector.
  • Method class_getInstanceMethod(Class cls, SEL sel) 或者可能是 static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)(cache_getImp 的实现在汇编中,直接查找缓存的IMP)
  • IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
  • static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
  • static Class initializeAndMaybeRelock(Class cls, id inst, mutex_t& lock, bool leaveLocked)
  • void initializeNonMetaClass(Class cls) 先递归调用最父类的方法
  • void callInitialize(Class cls)
  • ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));

Warning : 父类(Person)的+ initialize 方法可能被调用多次。(当子类没有实现+ initialize 方法)
伪代码如下:

         * SubPerson 继承于 Person
         * SubPerson 没有实现 +initialize()
         * Person 实现了 +initialize()
        */
        SubPerson *p = [SubPerson alloc]; //第一次 发送消息
        
        //SubPerson 第一次进来自己没有初始化
        if (cls->isInitialized()) {
    
    
            if (supercls  &&  !supercls->isInitialized()) {
    
    
                //同时父类Person也没有初始化,直接调用父类Person的initialize
                ((void(*)(Class, SEL))objc_msgSend)(supercls, @selector(initialize));
            }
            
            //然后给自己发initialize的消息,但是自己并没有实现这个方法,所以又调用了次次父类Person的initialize
            ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
        }

load 和 initialize的使用区别

load 调用的时候,环境不是很稳定,所以基本只是用来交换方法。不要在里面做比较耗时的操作。
initialize可以用来在编译期间不能初始化的全局变量。(不在这里交换方法,是因为如果initialize 是msgSend 机制的,如果有多个Category,可能导致交换方法所在的Category的initialize 不会被调用到)

Method 默认参数

  • OC 里面的所有方法,默认会有两个默认参数 Person * self SEL _cmd
  • self:代表方法调用者, _cmd 代表方法名字
这几行是对下面的调用函数的提取。
可以看见,objc_msgSend 默认第一个,和第二个参数就是 self 和 _cmd.

(self = objc_msgSendSuper({
    
    self, class_getSuperclass(objc_getClass("Person"))}, ("init")))
objc_msgSend(self, sel_registerName("setName:"),name);
objc_msgSend(self, sel_registerName("testMethod");


static instancetype _I_Person_initWithName_(Person * self, SEL _cmd, NSString *name) {
    
    
    if (self = ((Person sel_registerName*(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){
    
    (id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"))) {
    
    
        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("testMethod"));
    }
    return self;
}

Block

由于篇幅太大,单独移动到了 这篇

clang 使用__weak 和 __strong 的方式

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 会出现下列错误

/var/folders/9j/jxsl94c13kzbq69hbwnc6ytr0000gn/T/main-a89405.mi:29689:28: error: cannot create __weak reference because the current deployment target does not support weak references
            __attribute__((objc_ownership(weak))) Person *weakPerson = per;

所以需要添加 arc 环境和 runtime库的版本
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m

Runtime

由于篇幅太大,单独移动到了 这篇

栈空间 的地址方向

+由高地址往低地址方向 生长。 iOS又是小端模式即存储地址是反方向即小尾端。

类簇

NSStringNSArrayNSDictionary等class的名字和原本的名字不一样。
Class c = NSStringFromString(@"__NSDictonaryM")

堆栈被隐藏

小技巧:查看完成的堆栈信息。
方法堆栈显示有35条方法,可是5-31条被隐藏的情况下,可以输入 bt 指令查看完成堆栈。
在这里插入图片描述
如下图:
在这里插入图片描述

View 和 Layer

  • UIView侧重于对显示内容的管理
  • CALayer侧重于对内容的绘制。
  • view 显示layer内容的绘制
  • view 是layer的代理, 叫做layer的display (在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display)

其他收藏学习资料:

-iOS 保持界面流畅的技巧
-深入理解RunLoop

猜你喜欢

转载自blog.csdn.net/goldWave01/article/details/121163407