llvm优化alloc
为什么调用alloc
最终调用了objc_alloc
?
objc源码中探索分析
在源码中我们点击alloc
会进入到+ (id)alloc
方法,但是在实际调试中却是先调用的objc_alloc
,系统是怎么做到的呢?
imp
的重新绑定将alloc
绑定到了objc_alloc
上面。当然retain
、release
等都进行了同样的操作。既然在
_read_images
中出现问题的时候尝试进行fixup
,那么意味着正常情况下在_read_images
之前llvm
的编译阶段就完成了绑定。llvm源码探索分析
那么直接在llvm
中搜索objc_alloc
,在ObjCRuntime.h
中发现了如下注释:
/// When this method returns true, Clang will turn non-super message sends of
/// certain selectors into calls to the corresponding entrypoint:
/// alloc => objc_alloc
/// allocWithZone:nil => objc_allocWithZone
这说明方向没有错,最中在CGObjC.cpp
中找到了如下代码:
case OMF_alloc:
if (isClassMessage &&
Runtime.shouldUseRuntimeFunctionsForAlloc() &&
ResultType->isObjCObjectPointerType()) {
// [Foo alloc] -> objc_alloc(Foo) or
// [self alloc] -> objc_alloc(self)
if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
// [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or
// [self allocWithZone:nil] -> objc_allocWithZone(self)
if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 &&
Args.size() == 1 && Args.front().getType()->isPointerType() &&
Sel.getNameForSlot(0) == "allocWithZone") {
const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal();
if (isa<llvm::ConstantPointerNull>(arg))
return CGF.EmitObjCAllocWithZone(Receiver,
CGF.ConvertType(ResultType));
return None;
}
}
break;
可以看出来alloc
最后执行到了objc_alloc
。那么具体的实现就要看CGF.EmitObjCAlloc
方法:
llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
llvm::Type *resultType) {
return emitObjCValueOperation(*this, value, resultType,
CGM.getObjCEntrypoints().objc_alloc,
"objc_alloc");
}
llvm::Value *CodeGenFunction::EmitObjCAllocWithZone(llvm::Value *value,
llvm::Type *resultType) {
return emitObjCValueOperation(*this, value, resultType,
CGM.getObjCEntrypoints().objc_allocWithZone,
"objc_allocWithZone");
}
llvm::Value *CodeGenFunction::EmitObjCAllocInit(llvm::Value *value,
llvm::Type *resultType) {
return emitObjCValueOperation(*this, value, resultType,
CGM.getObjCEntrypoints().objc_alloc_init,
"objc_alloc_init");
}
alloc
以及objc_alloc_init
相关的逻辑。这样就实现了绑定。那么系统是怎么走到OMF_alloc
的逻辑的呢?通过发送消息走到这块流程:
CodeGen::RValue CGObjCRuntime::GeneratePossiblySpecializedMessageSend(
CodeGenFunction &CGF, ReturnValueSlot Return, QualType ResultType,
Selector Sel, llvm::Value *Receiver, const CallArgList &Args,
const ObjCInterfaceDecl *OID, const ObjCMethodDecl *Method,
bool isClassMessage) {
//尝试发送消息
if (Optional<llvm::Value *> SpecializedResult =
tryGenerateSpecializedMessageSend(CGF, ResultType, Receiver, Args,
Sel, Method, isClassMessage)) {
return RValue::get(SpecializedResult.getValue());
}
return GenerateMessageSend(CGF, Return, ResultType, Sel, Receiver, Args, OID,
Method);
}
alloc
等特殊函数做了hook
,会先走底层的标记emitObjCValueOperation
。最终再走到alloc
等函数。第一次会走tryGenerateSpecializedMessageSend
分支,第二次就走GenerateMessageSend
分支了。- 也就是第一次
alloc
调用了objc_alloc
,第二次alloc
后就没有调用objc_alloc
走了正常的objc_msgSend
:alloc-> objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc
。这也就是callAlloc
走两次的原因。 - 再创建个对象调用流程就变成了:
alloc -> objc_alloc -> callAlloc
。
内存分配优化
HPObject *hpObject = [HPObject alloc];
NSLog(@"%@:",hpObject);
对于hpObject
我们查看它的内存数据如下:
(lldb) x hpObject
0x6000030cc2e0: c8 74 e6 0e 01 00 00 00 00 00 00 00 00 00 00 00 .t..............
0x6000030cc2f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(lldb) p 0x000000010ee674c8
(long) $4 = 4544951496
可以打印的isa
是4544951496
并不是HPObject
。因为这里要&mask
,在源码中有一个&mask结构。
arm64`定义如下:
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_MASK 0x007ffffffffffff8ULL
# else
# define ISA_MASK 0x0000000ffffffff8ULL
这样计算后就得到isa
了:
(lldb) po 0x000000010ee674c8 & 0x007ffffffffffff8
HPObject
给HPObjetc
添加属性并赋值,修改逻辑如下:
@interface HPObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) BOOL marry;
@end
HPObject *hpObject = [HPObject alloc];
hpObject.name = @"HotpotCat";
hpObject.age = 18;
hpObject.height = 180.0;
hpObject.marry = YES;
age
和marry
存在了isa
后面存在了一起。那么多增加几个
BOOL
属性呢?@interface HPObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) BOOL marry;
@property (nonatomic, assign) BOOL flag1;
@property (nonatomic, assign) BOOL flag2;
@property (nonatomic, assign) BOOL flag3;
@end
int
类型的age
单独存放了,5
个bool
值放在了一起。这也就是内存分配做的优化。init源码探索
既然alloc
已经完成了内存分配和isa
与类的关联那么init
中做了什么呢?
initinit
源码定义如下:
- (id)init {
return _objc_rootInit(self);
}
_objc_rootInit
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
中调用了_objc_rootInit
,而_objc_rootInit
直接返回obj
没有做任何事情。就是给子类用来重写的,提供接口便于扩展。所以如果没有重写init
方法,那么在创建对象的时候可以不调用init
方法。
有了alloc
底层骚操作的经验后,打个断点调试下:
NSObject *obj = [NSObject alloc];
[obj init];
这里alloc
和init
分开写是为了避免被优化。这时候调用流程和源码看到的相同。
那么修改下调用逻辑:
NSObject *obj = [[NSObject alloc] init];
alloc init
一起调用后会先进入objc_alloc_init
方法。
objc_alloc_init
id
objc_alloc_init(Class cls)
{
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}
objc_alloc_init
调用了callAlloc
和init
。
new源码探索
既然alloc init
和new
都能创建对象,那么它们之间有什么区别呢?
new
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
alloc
init
一起调用的不同点是checkNil
传递的是fasle
。源码调试发现
new
调用的是objc_opt_new
// Calls [cls new]
id
objc_opt_new(Class cls)
{
#if __OBJC2__
if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
return [callAlloc(cls, false/*checkNil*/) init];
}
#endif
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}
在objc2
下也是callAlloc
和init
。
init
方法内部默认没有进行任何操作,只是返回了对象本身。allot init
与new
底层实现一致,都是调用callAlloc
和init
。所以如果自定义了init
方法调用两者效果相同。
objc_alloc_init
和objc_opt_new
的绑定与objc_alloc
的实现相同。同样的实现绑定的还有:
const char *AppleObjCTrampolineHandler::g_opt_dispatch_names[] = {
"objc_alloc",//alloc
"objc_autorelease",//autorelease
"objc_release",//release
"objc_retain",//retain
"objc_alloc_init",// alloc init
"objc_allocWithZone",//allocWithZone
"objc_opt_class",//class
"objc_opt_isKindOfClass",//isKindOfClass
"objc_opt_new",//new
"objc_opt_respondsToSelector",//respondsToSelector
"objc_opt_self",//self
};
总结
alloc
调用过程:
objc_alloc
alloc
底层首先调用的是objc_alloc
。objc_alloc
与alloc
是在llvm
编译阶段进行关联的。苹果会对系统特殊函数做hook
进行标记。
callAlloc
判断应该初始化的分支。_class_createInstanceFromZone
进行真正的开辟和关联操作:instacneSize
计算应该开辟的内存空间。alignedInstanceSize
内部进行字节对齐。fastInstanceSize
内部会进行内存对齐。
calloc
开辟内存空间。initInstanceIsa
关联isa
与创建的对象。
- init & new
init
方法内部默认没有进行任何操作,只是为了方便扩展。allot init
与new
底层实现一致,都是调用callAlloc
和init
。
作者:HotPotCat
链接:https://www.jianshu.com/p/884275c811d5