阅读视图

发现新文章,点击刷新页面。

iOS底层之分类的加载

iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址

序言

在前面文章《iOS底层之类的加载》中探索了类的加载流程,本篇将对分类展开探索,从分类的结构到分类的加载流程,来探索分类的本质。

Runtime优化

《WWDC 2020 关于Runtime的优化》中介绍了关于Runtime的优化内容,核心内容是对rw扩展出rwe,来优化整个运行时的性能。

我们的应用程序装载到设备时,系统会为安装程序分配一段内存,这段内存是不可变的称为clean memory也就是ro,当程序运行启动时,系统会开辟新的内存来运行程序,这段内存是可变化的称之为dirty memory也就是rw,因为系统内存有限,所以rw这段内存是比较宝贵的。

但是在rw中的数据很多是不会改变的,直接从ro读取即可,需要改变的数据通过Runtime运行时操作的数据,比如类的方法、属性、协议等,将这些数据存放在rwe上,这样就可以达到对dirty memory的优化。

分类的意义就是要动态的给类添加方法等,那么分类的探索就从rwe入手。

分类的结构

自定义类LGPerson和分类LGPerson (Cat),然后通过clang生成cpp文件查看

@interface LGPerson : NSObject
{
    NSString * name;
}

@property (nonatomic, copy) NSString * nickName;
- (void)instanceMethod;
+ (void)classMethod;

@end

@implementation LGPerson
- (void)instanceMethod {
    NSLog(@"%s", __func__ );
}

+ (void)classMethod {
    NSLog(@"%s", __func__ );
}

@interface LGPerson (Cat)

@property (nonatomic, copy) NSString * lg_nickName;

- (void)lg_categoryInstanceMethod;
+ (void)lg_categoryClassMethod;

@end

@implementation LGPerson (Cat)

- (void)lg_categoryInstanceMethod {
    NSLog(@"%s", **__func__** );
}

+ (void)lg_categoryClassMethod {
    NSLog(@"%s", **__func__** );
}

@end
 

主类的cpp代码 image.png

LGPerson的主类cpp中有实例方法类方法、以及属性的setget方法;

分类的cpp代码

image.png

到在分类中有实例方法类方法,并没有属性lg_nickNamesetget方法

LGPerson (Cat)分类遵守协议NSObject,重新生成cpp

image.png 分类的类型是_category_t,通过category_t在源码中可以找到分类的结构定义

image.png

struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
    WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);

    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};
  • name: 分类名称;
  • cls:主类;
  • instanceMethods:实例方法;
  • classMethods: 类方法;
  • protocols: 所遵守的协议;
  • instanceProperties:实例属性,并没有setget方法;
  • _classProperties:类属性

通过分类的结构可以看出,分类是没有元类的,主类的类方法是在元类中,分类的类方法是在classMethods中。

分类的加载

rwe是通过类中的extAllocIfNeeded方法创建,如果已有值直接返回rwe,如果没有则通过extAlloc创建。

image.png

在源码中全局搜索extAllocIfNeeded,调用的方法有
attachCategories分类、class_setVersion设置版本、addMethods_finish动态添加方法、class_addProtocol添加协议、_class_addProperty添加属性、objc_duplicateClass类的复制、demangledName修改类名

这些方法中有关分类的只有attachCategories方法

attachCategories方法探究

// 将方法列表、属性和协议从类别附加到类。假设cats中的类别都是按加载顺序加载和排序的,最旧的类别优先。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     * 只有少数类在启动期间具有超过64个类别。这使用了一个小堆栈,并避免了malloc。
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     * 类别必须以正确的顺序添加,即从后到前。
     * 为了实现分块,我们从前到后迭代cats_list,向后构建本地缓冲区,
     * 并对块调用attachList。attachLists预先准备好列表,
     * 因此最终结果按预期顺序排列。
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);   // 是否为元类
    auto rwe = cls->data()->extAllocIfNeeded(); // 初始化rwe
    // cats_count分类数量,循环处理分类
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];
        // 方法处理
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) { // 第一次mcount = 0
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__ );
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            //++mcount,将mlist放在mlists的最后
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist; 
            fromBundle |= entry.hi->isBundle();
        }
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            //++propcount,将proplist放在proplists的最后
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        // 向类中添加方法并排序
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__ );
        // rwe中添加方法
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__ , [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    // rwe中添加属性
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    // rwe中添加协议
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

分析一下方法

  • 初始化rwe
  • 通过分类数量cats_count,循环处理分类中的方法属性协议
  • 循环中按倒序插入法,将所有的方法存在mlists,协议存放在protolists,属性存放在proplists
  • 如果mcount大于0,说明有分类方法,通过prepareMethodLists向类中添加方法并排序,然后rwe中的methods调用attachLists,添加分类方法到rwe
  • rwe中的properties调用attachLists,添加分类属性到rwe
  • rwe中的protocols调用attachLists,添加分类协议到rwe

rwe中的方法属性协议都是调用的attachLists插入数据,是因为他们的定义都继承list_array_tt,我们看一下list_array_tt中的attachLists方法

attachLists方法探究

void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) { // 已有数据且为多条
            // many lists -> many lists
            uint32_t oldCount = array()->count; // 取出旧值
            uint32_t newCount = oldCount + addedCount; // 总数据数量
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount)); // 开辟新空间
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--) // 将旧值从后往前取出,插入到新数组的后面
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++) // 将新值从前往后添加到新数组
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) { // 没有数据且插入数据只有一条
            // 0 lists -> 1 list
            list = addedLists[0];// 直接插入
            validate();
        } 
        else { // 没有数据且插入多条数据
            // 1 list -> many lists
            Ptr<List> oldList = list; // 取出旧值
            uint32_t oldCount = oldList ? 1 : 0; // 有旧值oldCount=1否则oldCount=0
            uint32_t newCount = oldCount + addedCount;// 总数据数量
            setArray((array_t *)malloc(array_t::byteSize(newCount)));// 开辟新空间
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList; //将旧值存放在最后的位置
            for (unsigned i = 0; i < addedCount; i++) // 从前往后循环插入新值
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

算法分析:根据已有数据做不同情况处理

  1. 0 -> 1:无数据且新插入数据为1条
  • 直接插入到list
  1. 1 list -> many lists: 只有1条数据或插入多条数据
  • 取出旧值;
  • oldCount赋值:有旧值oldCount=1否则oldCount=0
  • 计算插入后数据总量newCount = oldCount + addedCount
  • 开辟新空间;
  • 将旧值存放在新空间最后;
  • 根据addedCount从前往后插入新值;
  1. many lists -> many lists: 已有多条数据
  • 取出旧值;
  • 计算插入后数据总量newCount = oldCount + addedCount
  • 开辟新空间;
  • 根据oldCount循环从后往前取出旧值,存放在新空间的后面位置;
  • 根据addedCount从前往后插入新值;

根据hasArraysetArray判断,只要调用setArrayarray()->count中的count值大于0,hasArray即为YESimage.png

所有分类加载后,rwemethods结构应该为

动态方法决议-导出.png

分类加载实例探究

类的加载探索中,我们知道了类的加载时机区分为懒加载类非懒加载类,即是否实现+load方法,分类的加载我们同样按照懒加载非懒加载的形式探索。

  1. 主类和分类都为懒加载;
  2. 主类非懒加载,分类懒加载;
  3. 主类懒加载,分类非懒加载;
  4. 主类和分类都非懒加载;

实例类为LGPerson,就在attachCategories方法中通过类名mangledName比较LGPerson来精确加断点、打印输出调试

image.png

1.主类和分类都为懒加载

主类LGPerson声明实例方法sayHellosayByeBye image.png 分类CatA中声明实例方法sayHello_A,以及重写主类方法sayHello image.pngmain函数中调用sayHello

image.png 并没有进入attachCategories函数中,同时可以看到sayHello是调用的分类的方法,由此可以知道如果主类分类都未实现+load方法,分类的方法等信息是在编译时就和主类编译在一起了,在类的加载流程中验证

image.pngmain函数中调用LGPersonalloc方法开始加载类,这里的method_list_t是从ro中读取的数据,输出查看

image.png

image.png 此时,list中的方法数主类和分类的方法集合,主类方法放在集合的最后位置,但这里方法还没有经过排序处理,通过prepareMethodLists会对list进行排序修复

image.png 通过断点跟进,再输出一下经过fixupMethodList处理后的list

image.png 处理后,主类的sayHello方法排在了分类sayHello后面。在调用sayHello时,消息查找流程在对排序好的list进行二分法查找,并且会通过while循环找到最前面的同名方法,这样分类方法就覆盖了主类方法

image.png

2.主类非懒加载、分类懒加载

LGPerson的主类实现+load方法,分类不实现

image.pngmain函数中调用sayHello

image.png 也没有进入attachCategories函数中,sayHello是调用的分类的方法,由此可以知道如果主类非懒加载和分类懒加载,分类的方法等信息也是在编译时就和主类编译在一起了。
下面在类的加载流程中验证

image.png 这里可以看出

  • LGPerson是在程序启动时_read_images实现加载的,
  • 分类的方法也是编译时就和主类方法编译在一起了,这里是通过ro获取的method_list_t,分类方法也在ro中。

3.主类懒加载、分类非懒加载

主类LGPerson不实现+load方法,分类实现

image.png 运行查看

image.png 也没有进入attachCategories函数中,sayHello是调用的分类的方法,由此可以知道如果主类懒加载和分类非懒加载,分类的方法等信息也是在编译时就和主类编译在一起了。
在类的加载流程中验证

image.png

  • LGPerson是在程序启动时_read_images实现加载的,分类非懒加载会导致主类被迫成为非懒加载类
  • 分类的方法也是编译时就和主类方法编译在一起了,这里是通过ro获取的method_list_t,分类方法也在ro中。

4.主类和分类都为非懒加载

主类LGPerson和分类CatA都实现+load方法,运行查看

image.png

通过输出打印可以看出,主类的加载分类的加载是在不同的流程中

主类加载

image.png

可以看出主类的加载是在_read_images流程,这里从ro读取的method_list_t中只有主类的两个方法。

分类的加载

image.png

分类的加载流程是load_images->loadAllCategories->load_categories_nolock->attachCategories,在attachCategories中会创建rwe,并调用prepareMethodLists对分类方法进行排序处理,然后调用rwemethodsattachLists插入分类的mlist

这里的LGPerson的分类有3个,只在分类CatA中实现了+load方法,我们调用分类CatC的方法

image.png

发现加载分类的时候,并没有输出分类CatC的名字,也就是没有分类CatC的加载,为什么可以调用sayHello_C成功了呢?

猜测:分类CatC没有实现+load,会不会是编译时和主类编译在一起了

再跟踪一下主类的加载流程

image.png

这里看到,主类ro中只有主类自己实现的两个方法,所以猜想不成立。

再跟踪一下分类的加载流程

image.png

这里看到,在加载分类CatB时,确有分类CatC的方法,他们的共同点是都没有实现+load,系统在编译时会把未实现+load方法的分类合并成一个分类来处理,从而简化分类的加载流程。

我们再增加一个分类CatD做验证

image.png

image.png 可以看到,分类加载时把未实现+load方法的CatBCatCCatD一起加载的,验证了系统在编译时会把未实现+load方法的分类合并成一个分类来处理。

总结

分类的加载原理和类同样区分为是否为懒加载,两者组合可分为4种情况

动态方法决议-导出.png

以上是对分类的加载流程探索过程的总结,难免有不足和错误之处,如有疑问请在评论区留言吧

iOS底层之对象的本质

序言

在iOS日常开发中,我们每天都会创建对象,那么对象的本质是什么呢?对象和类又是如何绑定关联的呢?

clang

OC的底层实现是C++代码,我们可以通过clang中的命令,用C++来还原OC代码。

  • Clang 是一个C语言C++Objective-C语言的轻量级编译器,是由Apple主导编写的
  • Clang 主要用于把源文件编译成底层文件,比如把main.m 文件编译成main.cppmain.o或者可执行文件

clang 常用命令

clang -rewrite-objc main.m -o main.cpp 
//UIKit报错 
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
// xcrun命令基于clang基础上进行了封装更好用
//3、模拟器编译 
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
//4、真机编译 
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp

对象本质探索

在工程中的mian.m文件中定义如下代码,然后编译成main.cpp文件

OC代码

@interface LGPerson : NSObject
{
    NSString * LGName;
}
@property (nonatomic,assign) NSInteger  age;
@end

@implementation LGPerson
@end

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

cpp代码

image.png

  • cpp文件中,可以看出LGPerson的定义其实是struct,也就是对象的本质是结构体
  • LGPerson_IMP的定义中,有一个struct NSObject_IMPL NSObject_IVARS;,这里的NSObject_IVARS是继承自NSObjectivars,在NSObject_IMPL的定义中,可以看到其ivars只有isa一个成员。

image.png

isa的定义是Class,那我们再找到Class的定义

image.pngClass 类型实际是一个 objc_class类型的结构体指针,这里也能看到我们常用的id其实是objc_object类型的结构体指针,以及SEL函数的一个结构体指针。


struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *LGName;
    NSInteger _age;
};

static NSInteger _I_LGPerson_age(LGPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_LGPerson$_age)); }

static void _I_LGPerson_setAge_(LGPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_LGPerson$_age)) = age; }

可以看到age属性变量有生成getset方法,同时生成了_age变量,而声明的LGName则没有这些方法变化。

总结

  1. 对象的本质其实就是一个结构体,其内存大小成员变量有关,也就是isa+成员+属性,这也验证了在alloc探索中开辟内存大小和ivars有关;
  2. 所有自定义的类都继承自NSObject,而NSObjectobjc_object类型的结构体,其内部有且只有一个成员属性isa

isa指针探究

在前面alloc的底层探究中,我们知道了对象通过obj->initInstanceIsa初始化isa关联类。接下来就看一下isa的结构是怎么样的,以及如何通过isa知道关联类的?

initInstanceIsa方法

inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());
    // 初始化isa
    initIsa(cls, true, hasCxxDtor);
}

initIsa 方法

inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer());
    isa_t newisa(0);   // isa初始化
    if (!nonpointer) { // 非nonpointer指针直接绑定cls
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

initIsa方法中,如果是非nonpointer类型指针,则直接调setClass方法绑定;否则现有一些位域赋值操作,再调setClass方法绑定。

联合体isa_t

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;
    
public:

#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD// defined in isa.h
    };
    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }

    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

在联合体isa_t中,有clsbits两个变量,还有一个ISA_BITFIELD是结构体里面的宏定义变量,是按位域定义的,ISA_BITFIELD的结构如下:

image.png

isa64情况.jpeg 各变量的解释:

  • nonpointer:表示是否对 isa 指针开启指针优化, 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等;
  • has_assoc:关联对象标志位,0-没有,1-存在;
  • has_cxx_dtor:该对象时候有C++Objc析构器,如果有析构函数,则需要做析构逻辑,没有则可以更快的释放对象;
  • shiftcls:存储类指针的值,开启指针优化的情况下,在ARM64架构中有33位来存储类指针值;
  • magic:用于调试器判断当前对象是真的对象还是未初始化的空间;
  • weakly_referenced:标志对象是否被指向或曾经指向一个ARC的弱变量,没有弱引用对象则可以更快的释放;
  • unused:对象是否释放;
  • has_sidetable_rc:当对象引用计数大于10时,则需要借用该变量存储进位;
  • extra_rc:对象的引用计数值(实际上是引用计数减1),例如:引用计数是10,那么extra_rc为9,如果引用计数大于10,则需要使用到has_sidetable_rc

isa分析总结

  • isa指针分为nonpointer和非nonpointer类型,非nonpointer类型就是一个纯指针,nonpointer类型指针开启了指针优化
  • 指针优化的设计,可以更好的利用isa的内存,程序中有大量的对象,针对isa的优化节省了大量内存。

通过对对象的本质和isa指针的分析,我们已经知道对象是如何与类进行关联的,后面就来探究一下OC中类的结构。

以上是关于OC中对象的本质分析,如有错误或疑问之处,请在评论区留言吧!!!

❌