前言
上一篇我们探索了dyld的加载过程,从dyld源码探索到oc源码,是oc中的_objc_init方法通过调用_dyld_objc_notify_register函数向dyld中注册回调,注册回调就跟我们写block一样,目的就是让dyld执行这个block。_dyld_objc_notify_register中有三个参数,&map_images和, load_images, unmap_image,上一篇我们已经探索了load_images就是加载load()方法,下面我们看下&map_images进而探索一下类的加载。可调式objc4源码地址
objc_init
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
//读取环境变量
environ_init();
//线程绑定
tls_init();
//运行c++构造函数
static_init();
//runtime运行时环境初始化
runtime_init();
//异常处理
exception_init();
#if __OBJC2__
//cache_t初始化
cache_t::init();
#endif
_imp_implementationWithBlock_init();
//向dyld注册回调
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
分析:我们看一下objc_init都干了什么,毕竟这是OC的init方法啊,大兄弟!
environ_init():读取影响运行时的环境变量,可以在控制台打印所有环境export OBJC_HELP=1tls_init():线程key的绑定,比如每个线程数据的析构函数static_init():运行 C++ 静态构造函数。在dyld调用我们的静态构造函数之前,libc会调用 _objc_init(), 因此我们必须自己做。runtime_init():runtime运行时环境初始化,里面主要是unattachedCategories和allocatedClasses两张表exception_init:异常处理系统cache_t::init():缓存条件初始化_imp_implementationWithBlock_init():启动回调机制。通常不会做什么,因为所有的初始化都是惰性的都是惰性的,但是对于某些进程,会迫不及待的加载trampolines dylib_dyld_objc_notify_register:向dyld注册回调
下面挑选几个比较重要的分析一下
environ_init
void environ_init(void)
{
//....
for (char **p = *_NSGetEnviron(); *p != nil; p++){
//读取环境变量,如果设置了环境变量,PrintHelp和PrintOptions会被设置为true
}
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
}
}
}
分析:这块是一个很实用的功能,当设置了环境变量,PrintHelp和PrintOptions会被设置为true,就是说可以通过设置环境变量来控制控制台的打印信息。控制台export OBJC_HELP = 1打印一下可以设置哪些环境变量
如上图,环境变量有很多,可以在xcode中设置这些环境变量帮助我们分析程序。下面举两个例子看一下
OBJC_PRINT_LOAD_METHODS=YES:打印哪里调用了load方法。OBJC_DISABLE_NONPOINTER_ISA=YES:打印纯指针。优化过的isa中包含了指针的引用、关联类等信息,纯指针就是没有优化过的指针,只有isa关联类。xcode设置如下
控制台打印如下:
分析:第一个红框显示所有调用
load()方法的地方都打印了日志,如果我们要分析项目中有哪些库调用了load()方法,毕竟load()方法在main()之前,如果想优化APP启动时间load()就要优化,这个日志打印还是有帮助的。第二个红框是isa内存值最右边低位是0,代表是纯指针,没有多余的isa信息,如果是1的话就是优化过的isa指针。
exception_init()
static void _objc_terminate(void)
{
if (PrintExceptions) {
_objc_inform("EXCEPTIONS: terminating");
}
if (! __cxa_current_exception_type()) {
(*old_terminate)();
}
else {
@try {
__cxa_rethrow();
} @catch (id e) {
//把异常放进uncaught_handler回调
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
(*old_terminate)();
}
}
}
分析:首先纠正一下异常不是错误,只是这个异常不符合系统底层的规则,系统会发出有异常的信号。exception_init里调用了_objc_terminate方法,而_objc_terminate方法里主要有个回调方法uncaught_handler,把异常放进了这个回调里。搜索这个回调发现uncaught_handler是可以通过API设置的
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn;
return result;
}
分析:在oc底层可以通过调用objc_setUncaughtExceptionHandler设置回调处理这个异常,而在上层苹果给我们封装了oc方法NSSetUncaughtExceptionHandler()去设置这个异常回调。这就给我们bug收集与分析提供了思路,我们完全可以设置这个回调来捕获异常,然后上传到服务器分析,这应该是也是第三方bug收集平台的思路,后面有时间写一些实用性的文章,把这块完善一下。
_dyld_objc_notify_register
dyld_objc_notify_register(&map_images, load_images, unmap_image)方法向dyld中注册回调方法,让dyld执行。为什么&map_images前面是一个取地址符号呢?这是地址传递,因为map_images是一个耗时而且可能会变化的地方,地址传递可以保持方法的同步。点击map_images跟进去看下
void
map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[])
{
mutex_locker_t lock(runtimeLock);
return map_images_nolock(count, paths, mhdrs);
}
主要是调用了map_images_nolock,跟进去看下源码
void
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[])
{
//...
//获取所有镜像imge,hcount是镜像的数量
if (hCount > 0) {
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
}
map_images_nolock主要是获取镜像image,如果数量大于0,那么就调用_read_images,主要看下_read_images干了什么,这是map_images核心,因为dyld把image加载进来了,下面肯定是需要读取image的。map_images->map_images_nolock->_read_images
_read_images
代码很长,但比较幸运的是源码中的ts.log日志帮助了我们分析,我们就根据日志把源码按步骤归类如下
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
//1.0创建存放所有类的表,只创建一次
if (!doneOnce) {
//...省略
}
//2.0 修复预编译阶段的`@selector`的混乱的问题
static size_t UnfixedSelectors;
{
//...省略
}
ts.log("IMAGE TIMES: fix up selector references");
//3.0 发现类,修复未解决的未来类
bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
//...
ts.log("IMAGE TIMES: discover classes");
//4.0 修复重映射一些没有被镜像文件加载进来的类
if (!noClassesRemapped()) {
}
ts.log("IMAGE TIMES: remap classes");
#if SUPPORT_FIXUP
//5.0 修复一些消息
for (EACH_HEADER) {
ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
//6.0 修复协议
for (EACH_HEADER) {
//...
}
ts.log("IMAGE TIMES: discover protocols");
//7.0 修复没有被加载的协议
for (EACH_HEADER) {
//...
}
ts.log("IMAGE TIMES: fix up @protocol references");
//8.0 分类处理
if (didInitialAttachCategories) {
for (EACH_HEADER) {
load_categories_nolock(hi);
}
}
ts.log("IMAGE TIMES: discover categories");
//9.0 类的处理
for (EACH_HEADER) {
//..
addClassTableEntry(cls);
realizeClassWithoutSwift(cls, nil);
}
}
ts.log("IMAGE TIMES: realize non-lazy classes");
// 10.0 没有被处理的类,优化那些被侵犯的类
if (resolvedFutureClasses) {
//...
free(resolvedFutureClasses);
}
ts.log("IMAGE TIMES: realize future classes");
//...
}
分析:我们看到read_images做了很多工作,大部分都是为了保持系统的稳定做一些修复处理以及类与分类的处理,大致做了如下10件事情。
创建存放所有类的表gdb_objc_realized_classes,只创建一次。- 修复预编译阶段的
@selector的混乱的问题。因为不同类中可能相同的方法,但是虽然是相同的方法但是地址不同,对那些混乱的方法进行修复。因为方法是存放在类中,每个类中的位置是不一样的,所以方法的地址也就不一样。简单点说就是把从macho文件中获取的相对方法地址修复为加上偏移量的重绑定的真实地址。 发现类,修复未解决的未来类- 修复重映射一些没有被镜像文件加载进来的类
- 修复一些
消息 - 修复
协议 - 修复没有被加载的协议
分类加载的处理类加载的处理- 没有被处理的类,优化那些被侵犯的类
我们的目标是探索类,所以就分析跟类有关的流程
创建类表gdb_objc_realized_classes
int namedClassesSize =
(isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;
gdb_objc_realized_classes =NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
分析:NXCreateMapTable创建一张存放所有类的表gdb_objc_realized_classes,只创建一次,所有类不管是实现的还是没有实现的类都会存放进去。上面objc_init中runtime_init()方法也创建了两张表,unattachedCategories和allocatedClasses,这是第三张表gdb_objc_realized_classes
发现类,修复未解决的未来类
//获取所有类
classref_t const *classlist = _getObjc2ClassList(hi, &count);
bool headerIsBundle = hi->isBundle();
bool headerIsPreoptimized = hi->hasPreoptimizedClasses();
//遍历类
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
if (newCls != cls && newCls) {
//类发生移动,但是没有被删除,就是残留的类。理论上来说不应该存在,但是确实发生了
resolvedFutureClasses = (Class *)
realloc(resolvedFutureClasses,
(resolvedFutureClassCount+1) * sizeof(Class));
resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
}
}
分析:因为我们分析的是类,这里很明显有个readClass,字面意思就是读取类,所以必须跟进去。
readClass
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{ //maco中获取类名
const char *mangledName = cls->nonlazyMangledName();
if (missingWeakSuperclass(cls)) {
//...返回的是nil,不分析
}
//注意:方便断点调试,本人自己添加的判断,非源码
if(strcmp(mangledName, "LGPerson")==0){
printf("LGperon来了");
}
cls->fixupBackwardDeployingStableSwift();
Class replacing = nil;
if (mangledName != nullptr) {
if (Class newCls = popFutureNamedClass(mangledName)) {
//...
class_rw_t *rw = newCls->data();
const class_ro_t *old_ro = rw->ro();
memcpy(newCls, cls, sizeof(objc_class));
newCls->setSuperclass(cls->getSuperclass());
newCls->initIsa(cls->getIsa());
rw->set_ro((class_ro_t *)newCls->data());
newCls->setData(rw);
freeIfMutable((char *)old_ro->getName());
free((void *)old_ro);
addRemappedClass(cls, newCls);
replacing = cls;
cls = newCls;
}
}
if (headerIsPreoptimized && !replacing) {
//...
} else {
if (mangledName) {
//添加进表gdb_objc_realized_classes
addNamedClass(cls, mangledName, replacing);
} else {
Class meta = cls->ISA();
const class_ro_t *metaRO = meta->bits.safe_ro();
}
//添加进表allocatedClasses
addClassTableEntry(cls);
}
//...
return cls;
}
分析:从machO中获取类名mangledName,如果类名不为空并且是等于popFutureNamedClass,就会进行ro、rw创建并赋值。再往下会调用addNamedClass方法,接着再调用addClassTableEntry方法。实践是检验真理的唯一标准,写个LGPerson类打个断点测试一下,上面写了一个strcmp(mangledName, "LGPerson")==0进行判断,分析一下当读取到LGPerson类的时候是否会走下面的流程,这样可以精准定位,否则其他一些系统类进来的时候会干扰我们分析。
断点来了单步玩下走,发现 if(Class newCls = popFutureNamedClass(mangledName))判断并不成立,也就是说并没有进行ro、rw的赋值。接着断点往下运行,发现运行了addNamedClass方法和addClassTableEntry方法`。看一下这两个方法。
addNamedClass
static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
runtimeLock.assertLocked();
Class old;
if ((old = getClassExceptSomeSwift(name)) && old != replacing) {
inform_duplicate(name, old, cls);
addNonMetaClass(cls);
} else {
NXMapInsert(gdb_objc_realized_classes, name, cls);
}
}
分析:addNamedClass就是把类添加进hash表gdb_objc_realized_classes,key是类名,value是类。
addClassTableEntry
static void
addClassTableEntry(Class cls, bool addMeta = true)
{
runtimeLock.assertLocked();
auto &set = objc::allocatedClasses.get();
//如果allocatedClasses中没有cls就插入
if (!isKnownClass(cls)){
set.insert(cls);
}
//插入元类
if (addMeta)
addClassTableEntry(cls->ISA(), false);
}
分析:addClassTableEntry方法就是从allocatedClasses表中通过类名获取类,如果获取不到就插入,如果获取到了就把元类插入表allocatedClasses中。其实就是插入类和元类进表allocatedClasses。
总结:
在第一次读取类的时候也就是readClass时,并没有进行ro、rw的创建,只是把类插入gdb_objc_realized_classe和allocatedClasses表中,其实想想也通,应该是先插表,把类名和类关联,然后再进行ro、rw赋值。那么什么时候进行ro、rw赋值的呢?
类加载的处理
回到上面分析_read_images中第九点类加载的处理,毕竟我们还是要分析类的,看下这边的源码
for (EACH_HEADER) {
//获取所有非懒加载的类
classref_t const *classlist = hi->nlclslist(&count);
for (i = 0; i < count; i++) {
//遍历获取类
Class cls = remapClass(classlist[i]);
if (!cls) continue;
//方便断点调试,本人自己添加的判断,不是源码
const char *mangledName = cls->nonlazyMangledName();
if(strcmp(mangledName, "LGPerson")==0){
printf("LGperon来了");
}
//添加进表allocatedClasses
addClassTableEntry(cls);
if (cls->isSwiftStable()) {
//...
}
realizeClassWithoutSwift(cls, nil);
}
}
ts.log("IMAGE TIMES: realize non-lazy classes");
分析:这边注意这里获取的是所有非懒加载的类,如果有非懒加载的类就循环遍历获取类,如果allocatedClasses表中没有这个类就插入表,然后调用realizeClassWithoutSwift初始化类。先看下懒加载和非懒加载的区别。
非懒加载:实现load()方法的类。懒加载:没有实现load()方法,第一次调用类的方法时加载的类,按需加载。 为什么只处理非懒加载?因为大部分类都是懒加载的,毕竟处理类是需要耗时的,为了性能只处理我们实现了load()方法的懒加载的类。上面我同样添加了一个strcmp(mangledName, "LGPerson")==0的判断,精准定位读取LGPerson类的时候是如何realizeClassWithoutSwift初始化类的,同时别忘了在LGPerson中添加load()方法,否则断点进不去。
realizeClassWithoutSwift
我们在之前分析消息慢速查找的时候看过这个方法,我们说这里面初始化了类以及根据类的isa以及继承关系初始化了关联类,我们再看下源码以及断点跟一下。
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
runtimeLock.assertLocked();
class_rw_t *rw;
Class supercls;
Class metacls;
if (!cls) return nil;
if (cls->isRealized()) {
//...
}
//获取ro
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
//拷贝ro进入rw
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}
cls->cache.initializeToEmptyOrPreoptimizedInDisguise();
#if FAST_CACHE_META
if (isMeta) cls->cache.setBit(FAST_CACHE_META);
#endif
//....
//递归初始化父类 ro rw
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
//递归初始化元类 ro rw
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
#if SUPPORT_NONPOINTER_ISA
if (isMeta) {
//如果是元类,isa是纯isa,isa只有类的信息没有关联信息
cls->setInstancesRequireRawIsa();
} else {
//不是元类 还有类的其他信息 比如引用计数等等
bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
bool rawIsaIsInherited = false;
static bool hackedDispatch = false;
if (DisableNonpointerIsa) {
instancesRequireRawIsa = true;
}
else if (!hackedDispatch && 0 == strcmp(ro->getName(), "OS_object"))
{
hackedDispatch = true;
instancesRequireRawIsa = true;
}
else if (supercls && supercls->getSuperclass() &&
supercls->instancesRequireRawIsa())
{
instancesRequireRawIsa = true;
rawIsaIsInherited = true;
}
if (instancesRequireRawIsa) {
cls->setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
}
}
// SUPPORT_NONPOINTER_ISA
#endif
//设置父类与元类
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
cls->setInstanceSize(ro->instanceSize);
//...
// 父类子类关联
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
//自己添加的判断 用于断点调试 非源码
const char *mangledName = cls->nonlazyMangledName();
if (strcmp(mangledName, "LGPerson") == 0)
{
if (!isMeta) {
printf("%s LGPerson....\n",__func__);
}
}
//方法处理以及分类处理
methodizeClass(cls, previously);
return cls;
}
我们看到ro的获取是这样的auto ro = (const class_ro_t *)cls->data();看下cls->data()源码
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
cls->data()只是一个地址指针,我们知道ro是clean Memory是从沙盒中获取的,这里获取到类的ro的地址指针,然后苹果通过解析这个地址指针获取到方法、属性、协议等。
再看下rw的赋值情况
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
rw是通过objc::zalloc动态创建的,然后set_ro(ro)把ro拷贝进rw,接着再调用setData(w)把rw设置进去,所以难怪说rw是Dirty Memory脏内存,它是动态创建的是耗性能的。
分析:根据注释整体流程还是比较清晰的
获取ro,然后创建rw,把ro拷贝进rw,这点很有意思。- 递归调用
realizeClassWithoutSwift初始化类的父类与元类的ro、rw。 - 判断是否是元类,
元类的isa是纯isa,只有关联类的信息没有其他如指针引用等信息。 - 建立
类、元类、父类的关联关系,其实就是根据类的isa以及继承关系初始化类。
在methodizeClass之前我们下个断点看一下ro,在LGPerson添加几个方法saySomething、say1、say2
分析:断点调试发现ro确实是获取到了,并且是有方法列表的,下面看下
methodizeClass是干嘛的。
methodizeClass
static void methodizeClass(Class cls, Class previously)
{
runtimeLock.assertLocked();
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro();
auto rwe = rw->ext();
//...
//准备方法
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
//断点看rwe为Null 不会走
if (rwe) rwe->methods.attachLists(&list, 1);
}
//属性
property_list_t *proplist = ro->baseProperties;
if (rwe && proplist) {
rwe->properties.attachLists(&proplist, 1);
}
//协议
protocol_list_t *protolist = ro->baseProtocols;
if (rwe && protolist) {
rwe->protocols.attachLists(&protolist, 1);
}
//...
//分类处理
objc::unattachedCategories.attachToClass(cls, cls,
isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
//...
}
分析:断点调试发现rwe为Null,rwe是什么?苹果爸爸对runtime进行了优化,专门为类分配了一块内存空间存放如分类一样动态添加的内存即rwe,这里rwe为空应该是没有分类,rwe还没有被动态创建,下一篇文章再分析。断点只走了prepareMethodLists()以及unattachedCategories.attachToClass方法,这边看一下prepareMethodLists(),unattachedCategories明显是跟分类有关的下一篇文章再分析。
prepareMethodLists()
跟进prepareMethodLists(),发现调用了fixupMethodList()对方法进行了排序
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
runtimeLock.assertLocked();
if (!mlist->isUniqued()) {
mutex_locker_t lock(selLock);
for (auto& meth : *mlist) {
const char *name = sel_cname(meth.name());
printf("%s--%p\n",name,meth.name());
meth.setName(sel_registerNameNoLock(name, bundleCopy));
}
}
if (sort && !mlist->isSmallList() && mlist->entsize() == method_t::bigSize) {
method_t::SortBySELAddress sorter;
std::stable_sort(&mlist->begin()->big(), &mlist->end()->big(), sorter);
}
if (!mlist->isSmallList()) {
mlist->setFixedUp();
}
}
分析:通过stable_sort对方法进行排序,只有排序后我们再慢速查找方法时才能进行二分法查找,我们断点调试打印一下看看是否真的排序了。
**排序前 say2 - 0x100003e1a**
**排序前 say1 - 0x100003e1f**
**排序前 saySomething - 0x100003e24**
**排序前 name - 0x100003e31**
**排序前 setName: - 0x100003e36**
**排序前 age - 0x100003e3f**
**排序前 setAge: - 0x100003e43**
**排序前 height - 0x100003e4b**
**排序前 setHeight: - 0x100003e52**
**排序前 nickName - 0x100003e5d**
**排序前 setNickName: - 0x100003e66**
***************************
**排序后 say2 - 0x100003e1a**
**排序后 say1 - 0x100003e1f**
**排序后 saySomething - 0x100003e24**
**排序后 name - 0x7fff7bb5ef23**
**排序后 setName: - 0x7fff7bb65099**
**排序后 setHeight: - 0x7fff7bb8347d**
**排序后 height - 0x7fff7bb87665**
**排序后 age - 0x7fff7be09112**
**排序后 setAge: - 0x7fff7be09116**
**排序后 nickName - 0x7fff7c059d49**
**排序后 setNickName: - 0x7fff7c059d52**
发现方法果然排序了,排序后Height的set方法在age前面了。
总结
realizeClassWithoutSwift主要是初始化类、父类、元类的ro、rw,然后调用prepareMethodLists方法进行方法排序,只有方法排序后查找时才能通过二分法查找
总结
类的初始化是通过调用realizeClassWithoutSwift方法,并且prepareMethodLists进行了方法排序ro的获取是从沙盒磁盘中获取的,是不会变的,rw是动态创建的,拷贝ro进rw。非懒加载类流程:readClass->_getObjc2NonlazyClassList->realizeClassWithoutSwift->methodizeClass懒加载类流程:我们在消息的慢速查找中分析过主要是lookUpImpOrForward>realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass