注册

llvm优化alloc

为什么调用alloc最终调用了objc_alloc


objc源码中探索分析

在源码中我们点击alloc会进入到+ (id)alloc方法,但是在实际调试中却是先调用的objc_alloc,系统是怎么做到的呢?

86f1e953aa0c00113f76749c2e77c184.png

5e728ed6a0ac252c8c9b152c094bb0f7.png


可以看到在这个方法中进行了imp的重新绑定将alloc绑定到了objc_alloc上面。当然retainrelease等都进行了同样的操作。
既然在_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_msgSendalloc-> 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

可以打印的isa4544951496并不是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;
这个时候发现agemarry存在了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单独存放了,5bool值放在了一起。这也就是内存分配做的优化。

init源码探索

既然alloc已经完成了内存分配和isa与类的关联那么init中做了什么呢?

init
init源码定义如下:

- (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];

这里allocinit分开写是为了避免被优化。这时候调用流程和源码看到的相同。

那么修改下调用逻辑:

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调用了callAllocinit

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下也是callAllocinit

  • init方法内部默认没有进行任何操作,只是返回了对象本身。
  • allot initnew底层实现一致,都是调用callAllocinit。所以如果自定义了init方法调用两者效果相同。

objc_alloc_initobjc_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_allocalloc是在llvm编译阶段进行关联的。苹果会对系统特殊函数做hook进行标记。
  • callAlloc判断应该初始化的分支。
  • _class_createInstanceFromZone进行真正的开辟和关联操作:
    • instacneSize计算应该开辟的内存空间。
      • alignedInstanceSize内部进行字节对齐。
      • fastInstanceSize内部会进行内存对齐。
    • calloc开辟内存空间。
    • initInstanceIsa关联isa与创建的对象。
  • init & new
    • init方法内部默认没有进行任何操作,只是为了方便扩展。
    • allot initnew底层实现一致,都是调用callAllocinit



作者:HotPotCat
链接:https://www.jianshu.com/p/884275c811d5


0 个评论

要回复文章请先登录注册