最近一直有事没有更新,今天抽时间赶紧整理一下。
1、LGPerson第一次alloc为什么会先执行objc_alloc
,再执行alloc
?
为了验证这个问题,,我们先利用machOView工具查看一下MachO文件中的符号表,我们在符号表中搜索一下“alloc”字符串,是否有这个符号。
我们发现在符号表中并没有alloc
符号,但是有objc_alloc
符号
小结:说明在编译阶段 编译器就把
alloc
符号替换成了objc_alloc
符号,那么我需要看一下编译器LLVM源码中做了哪些操作
我们使用文本查看工具全局检索一下llvm源码中的alloc
关键字
上图我们看到在函数
tryGenerateSpecializedMessageSend
中有alloc->objc_alloc
注释。
我们先解读一下注释,大概意思是说:objc
在运行时提供了更快的入口点,而不是通过普通的消息发送(objc_msgSend
)。这个入口点也比普通的消息发送速度更快。如果运行时确实支持所需的入口点时,这个方法就会调用并返回结果,否则返回None
,就会调用自己生成一个消息发送。
分析理解:
- 在LLVM 编译阶段,如果方法支持更快的入口点,则会标识这个方法(alloc改成objc_alloc),则不会再走普通的消息发送,否则就会走普通的消息发送进行方法调用。
- 之所以所谓的“入口点”比objc_msgSend更快,是因为没有了objc_msgSend中缓存查找的过程,如:
alloc
会先查找缓存,没有时再调用alloc执行callAlloc方法进行内存开辟和类绑定等操作,而objc_alloc
则可以直接调用callAlloc进行操作。
我们查看一下上图中标注的代码中做了什么?是否能验证我们的分析理解
首先看条件if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
是否成立。这个isUnarySelector()
方法意思是否在SelectorTable
中被标记(具体逻辑细节不再贴图,有兴趣可以下载源码研究),如果有标记且是"alloc"的话,就会走EmitObjCAlloc
方法。 我们看到在
EmitObjCAlloc
方法中生成了一个objc_alloc
的入口点
,我们看注释:“分配给定的objc对象”
,由此我们可以验证以上的分析。
我们根据LLVM源码搜索的结果,查看一下tryGenerateSpecializedMessageSend
函数调用过程。
检索tryGenerateSpecializedMessageSend
方法调用 我们看到在
GeneratePossiblySpecializedMessageSend
中有个条件判断,从代码可以分析出:
如果tryGenerateSpecializedMessageSend
方法返回None
,这里判断为NO
,就会走GenerateMessageSend
方法(也就是调用者自己生成一个普通的objc_msgSend
),相反就会走特殊的入口点。
总结:到此我们可以得出结论,系统针对alloc,allocWithZone等系统函数,在LLVM编译阶段在底层就进行了拦截(相当于HOOK操作)和优化(特殊的入口点)。
扫描二维码关注公众号,回复: 14287860 查看本文章![]()
2、LGPerson第二次alloc为什么执行objc_alloc
后,不再执行alloc(objc_msgSend调用)
?
这个问题我也看了很多优秀的博客,大多的解释是第一次进来没有缓存,第二次进来有缓存了就不再通过消息发送执行alloc
了(解释不够清晰和准确)。接下来我们通过代码调试来看下底层到底做了些什么?
alloc函数如果缓存的话,是应该缓存在metaClass
类中,那我们来调试验证一下。
- 第一次执行[LGPerson alloc]
,并且在callAlloc
方法打上断点(objc_alloc --> callAlloc
),查看 “LGPerson的元类” 中的cache_t
中的信息。
点击
Continue program execution
继续执行 上图中输出 “LGPerson的元类” 中的
cache_t
中的信息,我们可以看到此时cache中_maybeMask
代码缓存的长度为0, _flags
为0;
LLBD命令:
(lldb)
p cls
(Class) $0 = LGPerson
(lldb)
x/4gx cls
// 输出LGPerson类的4个成员变量0x1000083c0: 0x0000000100008398 0x000000010036a140 0x1000083d0: 0x00000001003623a0 0x0000000000000000
(lldb)
x/4gx 0x0000000100008398
// 输出LGPerson元类的4个成员变量0x100008398: 0x000000010036a0f0 0x000000010036a0f0
0x1000083a8: 0x00000001003623a0 0x0000000000000000
(lldb)
p/x 0x100008398 + 16
// 输出LGPerson元类cache_t的地址(long) $2 = 0x00000001000083a8
(lldb)
p (cache_t *)0x00000001000083a8
// 强转(cache_t *) $3 = 0x00000001000083a8
(lldb)
p *$3
// 输出cache_t内容(cache_t) $4 = {
_bucketsAndMaybeMask = {
std::__1::atomic《unsigned long》 = { // 此处《 因为尖括号冲突才这样写
Value = 4298515360
}
}
= {
= {
_maybeMask = {
std::__1::atomic《unsigned int》= {
Value = 0
}
}
_flags = 0
_occupied = 0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = nil
}
}
}
}
我们来看fastpath(!cls->ISA()->hasCustomAWZ())
函数中如何判断的(cls->ISA()
获取元类)
FAST_CACHE_HAS_DEFAULT_AWZ宏定义表示 是否实现
alloc/allocWithZone
位域标识位
我们继续下一步: 继续下一步:
小结:第一次执行
[LGPerson alloc]
,第一次执行objc_alloc
->callAlloc
,fastpath(!cls->ISA()->hasCustomAWZ())
返回结果为NO,所以在callAlloc
方法中会执行objc_msgSend
调用alloc
我们继续执行Continue program execution
- 第一次执行[LGPerson alloc]
,第二次来到callAlloc
方法([LGPerson alloc]
-> objc_alloc
-> callAlloc
-> (objc_msgSend)alloc
->_objc_rootAlloc
->callAlloc
),我看到LGPerson元类cache_t
缓存中的信息
_maybeMask
代码缓存的长度依然为0,alloc
方法并没有并缓存起来;- 而
_flags
却变成了57393。
那么我们继续看下hasCustomAWZ()
方法中的判断 此时我们可以根据上图中的结果可以看到,返回结果为
YES
,进入_objc_rootAllocWithZone
方法
小结:
- alloc函数不会在缓存中存储,会在
cache_t
中的_flags
中第15位来表示是否已经完成了实现。(凡是alloc第一次执行后在缓存中的说法,显然说法不够准确)- 由此也产生了一个问题,这个
_flags
被赋值的时机是是什么时候?(后面我们来调试解答)
- 第二次执行[LGPerson alloc]
,(在 OC对象底层内存开辟和实现(上)中已经说明过执行流程,我们直接跳过中间步骤),我们直接看hasCustomAWZ()
方法中的判断
直接进入
hasCustomAWZ()
方法里面的判断
返回结果为
YES
,表示已经实现了alloc/allocWithZone
方法,所以第二次执行[LGPerson alloc]
执行objc_alloc
后,不再执行alloc(objc_msgSend调用)
,从而直接调用_objc_rootAllocWithZone
方法。
总结:
- LGPerson第一次alloc会先执行
objc_alloc
,再通过objc_msgSend
执行alloc
,然后会在其元类中的cache_t
的_flags
中标识已经实现alloc/allocWithZone
方法。- LGPerson第二次alloc执行
objc_alloc
后,不再执行alloc
(objc_msgSend调用),因为缓存中已经标识过了。
3、NSOject第一次alloc为什么执行objc_alloc
后,不再执行alloc
?
因为NSOject
是所有类的基类,和它的类初始化时机有关,还记得我们抛出一个问题吗, 元类cache_t
中_flags
赋值时机是什么时候?在做这个问题中我们来做解答。
4、init和new底层做了哪些操作?
init函数底层实现,比较简单直接贴代码不再截图展示了,如下所示:
-(id) init {
return _objc_rootInit(self);
}
进入 _objc_rootInit
// C++函数
id _objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
总结:
init
方法返回的是对象本身init
可以提供自定义实现 ,通过id
类型实现强转,返回需要的类型
new
函数的实现如下:
+(id)new {
return [callAlloc(self, false) init];
}
- 和
alloc
差不多,alloc是_objc_rootAlloc->callAlloc
- new是直接走了
callAlloc
的方法流程,然后走了init
方法 ,new
相当于是alloc
+init
5、元类cache_t
中_flags
赋值时机是什么时候?
我们还是用断点调试的方式来看一下_flags
赋值的时机和几种情况,
- 第一种情况我们在类懒加载
(没有实现+load)情况下看下赋值的时机
首先我们来看一下给_flags
赋值的方法
vid setBit(uint16_t set) {
__c11_atomic_fetch_or((_Atomic(uint16_t) *)&_flags, set, __ATOMIC_RELAXED);
}
__c11_atomic_fetch_or
函数解释:是一个C++11函数,表示把set和_flags按位或结果,保存到_flags并返回
我们在[LGPerson alloc]
和setBit(uint16_t set)
方法分别打上断点,我们先通过堆栈信息来看一下赋值的时机。 接下来我们直接看下什么时候会调用
setBit
方法
- 第一次调用如下:
我们看到堆栈的调用信息为:
第一次objc_alloc
-> callAlloc
-> objc_msgSend(alloc
) -> _objc_msgSend_uncached
->lookUpImpOrForward
->realizeAndInitializeIfNeeded_locked
-> realizeClassMaybeSwiftAndLeaveLocked
-> realizeClassMaybeSwiftMaybeRelock
-> realizeClassWithoutSwift
方法中调用,代码如下:
#if FAST_CACHE_META
if (isMeta) cls->cache.setBit(FAST_CACHE_META);
#endif
表示_flags
第1位是否为元类标识,此时_flags
为等于1。
- 第二次调用如下
同样是在
realizeClassWithoutSwift
方法中调用,代码如下:
if (isMeta) {
// Metaclasses do not need any features from non pointer ISA
// This allows for a faspath for classes in objc_retain/objc_release.
cls->setInstancesRequireRawIsa();
}
void setInstancesRequireRawIsa() {
cache.setBit(FAST_CACHE_REQUIRES_RAW_ISA);
}
// class's instances requires raw isa 类的实例需要原始isa
#define FAST_CACHE_REQUIRES_RAW_ISA (1<<13)
表示_flags
第14位标识类实例(类对象)的isa是纯的isa,指向元类,不再是isa_t联合体。 此时_flags
为等于8193。
- 第三次调用如下:
还是在
realizeClassWithoutSwift
方法中调用,代码如下:
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);
void setInstanceSize(uint32_t newSize) {
ASSERT(isRealized());
ASSERT(data()->flags & RW_REALIZING);
auto ro = data()->ro();
if (newSize != ro->instanceSize) {
ASSERT(data()->flags & RW_COPIED_RO);
const_cast<uint32_t *>(&ro->instanceSize) = newSize;
}
cache.setFastInstanceSize(newSize);
}
void setFastInstanceSize(size_t newSize)
{
// Set during realization or construction only. No locking needed.
uint16_t newBits = _flags & ~FAST_CACHE_ALLOC_MASK;
uint16_t sizeBits;
// Adding FAST_CACHE_ALLOC_DELTA16 allows for FAST_CACHE_ALLOC_MASK16
// to yield the proper 16byte aligned allocation size with a single mask
sizeBits = word_align(newSize) + FAST_CACHE_ALLOC_DELTA16;
sizeBits &= FAST_CACHE_ALLOC_MASK;
if (newSize <= sizeBits) {
newBits |= sizeBits;
}
_flags = newBits;
}
表示_flags
第4-13位来存储对象大小,最小8字节(1000),最大为4M(1 0000 0000 0000),上图中我们看到在元类的class_ro_t中instanceStart
为40,表示类对象的开始大小就是40字节。
此时
_flags
为等于8241, 二进制格式为:0b0010 0000 0011 0001。
小结:执行到这里其实是 类加载的过程(类懒加载到内存),包括初始化类结构,计算缓存对象空间大小,绑定父类关系等等。
在方法lookUpImpOrForward
有两个判断,我可以来看下
static Class realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
runtimeLock.assertLocked();
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (slowpath(initialize && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
return cls;
}
- 第一个判断是 类是否已经完成加载;
- 第二个判断是 类是否完成类初始化;
所以上面的整个过程都是在第一个判断条件中执行的。接下来我们继续看类初始化过程:
- 第四-五次调用如下:
此时的堆栈调用变为:第一次objc_alloc
-> callAlloc
-> objc_msgSend(alloc
) -> _objc_msgSend_uncached
->lookUpImpOrForward
->initializeAndLeaveLocked
-> initializeAndMaybeRelock
-> initializeNonMetaClass
-> lockAndFinishInitializing
-> _finishInitializing
->objc_class::setInitialized
方法中调用,代码如下
void objc_class::setInitialized() {
Class metacls;
Class cls;
ASSERT(!isMetaClass());
cls = (Class)this;
metacls = cls->ISA();
mutex_locker_t lock(runtimeLock);
// Special cases:
// - NSObject AWZ class methods are default.
// - NSObject RR class and instance methods are default.
// - NSObject Core class and instance methods are default.
objc::AWZScanner::scanInitializedClass(cls, metacls);
objc::RRScanner::scanInitializedClass(cls, metacls);
objc::CoreScanner::scanInitializedClass(cls, metacls);
#if CONFIG_USE_PREOPT_CACHES
cls->cache.maybeConvertToPreoptimized();
metacls->cache.maybeConvertToPreoptimized();
#endif
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: setInitialized(%s)",
objc_thread_self(), cls->nameForLogging());
}
// Update the +initialize flags. 更新+initialize标识
metacls->changeInfo(RW_INITIALIZED, RW_INITIALIZING);
}
注意:上述标颜色的代码是C++的模板调函数调用,会一次调用2次cache.setBit
方法,调用代码依次是
void setHasDefaultAWZ() {
cache.setBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
void setHasDefaultRR() {
bits.setBits(FAST_HAS_DEFAULT_RR);
}
void setHasDefaultCore() {
return cache.setBit(FAST_CACHE_HAS_DEFAULT_CORE);
}
FAST_CACHE_HAS_DEFAULT_AWZ
标识在cache_t
的_flags
赋值时机有什么不一样.的15位是否是有实现默认的alloc/allocWithZone方法;FAST_CACHE_HAS_DEFAULT_CORE
标识在cache_t
的_flags
的16位是否有默认的类或超类new/self/class/respondsToSelector/iskindof方法实现;FAST_HAS_DEFAULT_RR
是在class_data_bit_t的bit
中来标识,表是否有默认的类或超类的retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference等方法实现;
总结:这部分是类初始化的过程,在类第一次使用的时候(也就是第一次objc_alloc->callAlloc 方法中用objc_msgSend执行alloc时)会依次执行类加载和类初始化的操作,会调用
+initialize
方法;在
initializeNonMetaClass
方法中会递归判断父类是否实现初始化,所以会先执行父类的+initialize`方法;这也就解释了
NSOject第一次alloc为什么执行objc_alloc后,不再执行alloc
。是因为在初始化其他的类的时候,NSObject已经被初始化。metacls->changeInfo(RW_INITIALIZED, RW_INITIALIZING); 当类初始化完后会更行类的标识状态,在
class_rw_t
的flags
的第30位来标识,宏定义状态RW_INITIALIZED (1<<29)。
第二种情况我们在类非懒加载(实现+load)情况下看下赋值的时机
- 那么我们在LGPerson中先实现+load方法,然后运行看下
cache_t
的_flags
赋值时机有什么不一样。
第一次赋值时机: 第二次赋值时机:
第三次赋值时机:
以上我们可以看出类的加载出现在了dyld动态链接阶段,会执行
+load
方法;但此时并不会初始化类(不会执行+initialize
方法)。此时的cache_t
的_flags
的值为8241,标识已经完成加载。
在我们继续执行到第一次objc_alloc
时: 我们看到在
lookUpImpOrForward
方法中直接走了类的初始化,会调用执行+initialize
方法;
总结:
- 在非懒加载类中,类的加载会提前到动态链接阶段(dyld);
- 在懒加载类中,类的加载会在第一次类的使用时;
- 类的初始化都是在第一次类的使用时才会被初始化。