全是随笔 笔记。没有规律。
-
block:https://blog.csdn.net/goldWave01/article/details/121445902
-
Runtime:https://blog.csdn.net/goldWave01/article/details/121504224
-
RunLoop:https://blog.csdn.net/goldWave01/article/details/121877941
-
多线程:https://blog.csdn.net/goldWave01/article/details/122094507
-
内存管理
https://blog.csdn.net/goldWave01/article/details/122262611
动态特性 VS 静态特性
- OC: 动态类型(
id
)、多态绑定([obj msgSend]
)、 多态加载(图片2x3x替换,动态加方法和变量)
OC Alloc Init New
alloc
开辟内存,绑定指针isainit
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
因为前者指定了sdk
是iphone
并且 指定了架构是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
的调用前后 手动调用willChangeValueForKey
和 didChangeValueForKey
就可以触发系统的监听函数。
通过方法名字查找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
方法, 那么他的内部应该会手动调用 手动调用willChangeValueForKey
和 didChangeValueForKey
就可以触发系统的监听函数。
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 的valuevoid _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
设为nil就会移除当前的keyvoid _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();
分别单独调用类的load
和Category
的load
方法(*load_method)(cls, @selector(load));
函数的指针进行调用
初步结论
所以load
方法是在main
之前调用
所以Category
的load
方法不会覆盖原来类的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又是小端模式即存储地址是反方向即小尾端。
类簇
NSString
、NSArray
、NSDictionary
等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)