普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-Joe天青色

iOS底层之类扩展和关联对象

作者 Joe天青色
2023年3月23日 16:57

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

iOS底层之类的加载
iOS底层之分类的加载

序言

在前面文章中,从底层源码探索了分类的加载流程,今天从源码层面探索一下类扩展及关联对象的本质。

类扩展

通过cpp文件查看类扩展

定义类LGStudent的属性方法,以及类扩展实现

@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

- (void)instanceMethod;
+ (void)classMethod;

@end

// 类扩展
@interface LGStudent ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, assign) int ext_age;
- (void)ext_instanceMethod;
+ (void)ext_classMethod;
@end

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

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

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

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

@end

通过clang然后生成cpp文件,查看类扩展属性

image.png

在类扩展中定义的属性ext_nameext_age,在cpp文件和主类的属性nameage一样处理生成带下划线成员变量

image.png

image.png

这里对比可以看到,

  • 在成员列表_ivar_list_t中是有_age_ext_age_name_ext_name4个成员变量;
  • 在属性列表_prop_list_t中只有nameage两个属性,说明在类扩展中的属性为私有;

看一下类扩展中的方法

image.png

类扩展中定义的实例方法ext_instanceMethod以及属性ext_nameext_agesettergetter方法,都编译在主类实例方法中。

image.png

类扩展中定义的类方法ext_classMethod和主类的类方法classMethod在编译在主类的类方法中。

通过类的加载查看类扩展

定义类LGPerson和类扩展LGPerson+LG

image.png

《iOS底层之类的加载》中我们知道了非懒加载类加载到内存是在启动时完成的。

为方便调试在主类实现+load方法,类扩展方法以ext开头,在LGperson.m中不实现类扩展方法ext_saySomthing

image.png

methodizeClass方法中对LGPerson准确下断点,运行调试

image.png

先来到的为LGPerson的元类,在从ro读取的method_list_t中,包含主类中的类方法+load和类扩展中的类方法ext_classMethod

过掉断点会再次进来,这次是对类LGPerson的加载 image.png

LGPerson类加载时,从ro读取的method_list_t中,包含主类中的实例方法sayHellosayByeBye,同时还有分类中的实例方法ext_instanceMethod,属性ext_namesettergetter方法,并没有未实现的方法ext_saySomthing

类扩展总结

  • 类扩展可以给类添加成员属性,但是是私有变量;
  • 类扩展可以给类添加方法,也是私有方法;

关联对象

分类的作用是给类添加新的方法,但是不能添加属性,即使添加了属性,也只会生成属性的settergetter方法声明,不能生成方法实现和带下划线的成员变量

我们可以通过Runtime给分类添加属性,就是通过关联对象的形式实现,今天从源码层面实例探究一下关联对象是如何实现的给分类添加属性的。

源码分析

关联对象存储

关联对象存储值时使用的apiobjc_setAssociatedObject,参数有4个

  • object: 关联对象;
  • key:标识符;
  • value:关联值;
  • policy:关联策略;
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}

objc_setAssociatedObject直接调用_object_set_associative_reference,这样做是维持最上层的api稳定,不管底层怎么更新,我们做关联对象使用的就是objc_setAssociatedObject

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // 关联对象为nil切关联值也为nil
    if (!object && !value) return;
    // object不允许关联对象,直接崩
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    // 包装关联对象成统一对象类型,
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // 将value和policy包装成ObjcAssociation类型
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    bool isFirstAssociation = false; // 首次关联对象标识
    {
        AssociationsManager manager; // 实例化关联对象处理manager
        AssociationsHashMap &associations(manager.get()); // 获取关联对象表
        if (value) { // 关联值判断
            // 从关联对象表中,根据disguised获取关联对象存储地址,没有就创建插入
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) { // 是否为第一次关联值
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            // 获取disguised的关联对象存储地址
            auto &refs = refs_result.first->second;
            // 以key-value的形式,存储association
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) { 
            // 如果之前存储过关联值,用之前的值first替换调value,后面将association释放
                association.swap(result.first->second);
            }
        } else { // 关联值位nil,如果之前有key对应关联值,则清除
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) { // 获取到关联对象的存储地址
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) { // 判断key对应的关联值,有的话就清除释放掉
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) { // 关联对象中没有关联值,则释放掉关联对象
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }

//     Call setHasAssociatedObjects outside the lock, since this
//     will call the object's _noteAssociatedObjects method if it
//     has one, and this may trigger +initialize which might do
//     arbitrary stuff, including setting more associated objects.
    // 给关联对象object的isa中的has_assoc设置值true,表示有关联对象
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // release the old value (outside of the lock).
    association.releaseHeldValue(); // 如果有新值替换则释放旧值
}

_object_set_associative_reference中的工作处理流程:
1.value有值

  • 实例化全局关联表associations
  • associations调用try_emplace将关联对象disguised作为key,获取disguised对应的关联对象表ObjectAssociationMap的地址,如果没有就创建一个ObjectAssociationMap,返回为refs_result
  • 如果refs_result.second有值,则disguised为第一次做关联值;
  • 通过refs_result.first->second获取关联对象表的存储地址refs
  • refs调用try_emplace以标识符key存储关联值association,返回为result
  • 如果result.second为false,说明之前已经做过key对应的关联值,调用association.swap方法存储旧值地址存入association准备释放旧值;
  • 如果为第一次关联值,调用setHasAssociatedObjectsisahas_assoc设置为true,表示object有关联对象,在dealloc时将关联表释放。
  • 如果不是第一次关联,此时association存储的是旧值,调用releaseHeldValue释放掉旧值

2.value为空值

  • 如果关联值为空,获取disguised的关联表,清除掉key对应的关联值,如果disguised关联表中没有关联值,则将disguised关联表释放。

关联对象取值

关联对象取值时使用的apiobjc_getAssociatedObject,参数为关联对象object和标识符key

id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

同样是一层封装过渡,直接调用_object_get_associative_reference

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};
    { 
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        // 通过关联对象object获取关联表
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
           // 获取关联表的地址
            ObjectAssociationMap &refs = i->second;
            // 通过key值获取关联值
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue(); // retain处理
            }
        }
    }
    return association.autoreleaseReturnedValue(); // 返回value
}
  • 通过关联对象object获取关联表;
  • 获取关联表的地址,通过key值获取关联值

关联对象销毁

对象销毁时会调用dealloc方法

- (void)dealloc {
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);
    obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
    // TaggedPointer小对象直接返回
    if (isTaggedPointer()) return// fixme necessary?
    /**
     * nonpointer & !weakl & !has_*assoc & !has_cxx_dtor & !has_sidetable_rc
     * nonpointer类型
     * 无弱引用
     * 无关联对象
     * 无C++析构函数
     * 无类的C++析构函数
     * 无引用计数
     */ 
    if (fastpath(isa().nonpointer                     &&
                 !isa().weakly_referenced             &&
                 !isa().has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa().has_cxx_dtor                  &&
#else
                 !isa().getClass(**false**)->hasCxxDtor() &&
#endif
                 !isa().has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);// 直接释放
    } 
    else {
        object_dispose((id)this);
    }
}

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        // 获取是否有关联对象
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        // 如果有关联对象则移除关联对象
        if (assoc) _object_remove_associations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }

    return obj;
}

inline bool
objc_object::hasAssociatedObjects()
{
    if (isTaggedPointer()) return true;
    if (isa().nonpointer) return isa().has_assoc;
    return true;
}

  • dealloc中调用_objc_rootDealloc
  • _objc_rootDealloc中调用对象的rootDealloc方法;
  • rootDealloc中判断对象为TaggedPointer不用处理;
  • 不是TaggedPointer,如果为nonpointer指针且无弱引用无关联对象无C++析构函数无类的C++析构函数无引用计数则直接free,否则就调用object_dispose
  • object_dispose中先调用objc_destructInstance,然后再free
  • objc_destructInstance中根据hasAssociatedObjects判断有无关联对象,如果有就调用_object_remove_associations移除关联对象;
  • hasAssociatedObjectsnonpointer类型的isa,根据值has_assoc判断有无关联对象;

看一下_object_remove_associations的源码是怎么移除关联对象的

void
_object_remove_associations(id object, bool deallocating)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            // 获取关联对象表的地址
            refs.swap(i->second);

            // If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
            bool didReInsert = false;
            if (!deallocating) { // deallocating值为true
                for (auto &ref: refs) {
                    if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
                        i->second.insert(ref);
                        didReInsert = true;
                    }
                }
            }
            if (!didReInsert)
                associations.erase(i); // 全局关联表中释放对象关联表
        }

    }

    // Associations to be released after the normal ones.
    SmallVector<ObjcAssociation *, 4> laterRefs;
    // release everything (outside of the lock).
    for (auto &i: refs) {
        if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
            // If we are not deallocating, then RELEASE_LATER associations don't get released.
            if (deallocating)
                laterRefs.append(&i.second);
        } else {
            i.second.releaseHeldValue();
        }
    }
    for (auto *later: laterRefs) {
        later->releaseHeldValue();
    }
}
  • 全局关联表associations通过对象object获取关联对象表对应的表i
  • 获取表地址空间refs
  • for循环取出所有的关联值,先清除释放策略不是OBJC_ASSOCIATION_SYSTEM_OBJECT,策略为OBJC_ASSOCIATION_SYSTEM_OBJECT放入laterRefs
  • 再对laterRefs循环清除。

实例验证

定义主类LGPerson和分类LGPerson+CatA,在分类中设置属性cate_name,并在分类中实现cate_namesettergetter方法

- (void)setCate_name:(NSString *)cate_name{
    /**对象,标识符,value,策略* */
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_name{
    return  objc_getAssociatedObject(self, "cate_name");
}

mian函数中给cate_name赋值

image.png_object_set_associative_reference中加断点调试

第一次关联值

image.png

  • 第一次关联值为Hello cat!,这里isFirstAssociation值为true
  • 此时refs还没有调用try_emplaceBuckets的值为nil

继续Step Over一下

image.png

这里refs的存储了值,result.secondtrue说明第一次关联不需要association.swap

setHasAssociatedObjects打断点,进入调试 image.png

这里对newisahas_assoc赋值为true

releaseHeldValue打断点,进入调试

image.png

这里_valuenil,且_policy0

第二次关联值

放掉断点,进入第二次关联值流程,断点来到refs.try_emplace

image.png

  • 第二次关联值为Hello dog!,这里isFirstAssociation值为false
  • refs的存储了值,Buckets的值为关联值的地址;

继续Step Over一下

image.png

image.png

refs.try_emplaceresult.second的值为false,进入swap方法,association的存储值为上一次关联的旧值。

releaseHeldValue打断点,进入调试 image.png

这里要释放掉的_value为上一次关联的Hello cat!

关联值值nil

过掉断点,进入关联值为nil的流程,断点到erase

image.png

  • 这里valuenil
  • association.swap的值为second_valueHello dog!,最后存储的关联值,准备释放;
  • refs.size的值为1,有一个关联值;

继续Step Over一下,过掉erase

image.png

refs.size的值为0,没有关联值,调用associationserase方法释放关联对象表refs_it

关联对象取值

注释掉关联对象赋值为nil,打印cate_name

image.png

_object_get_associative_reference加断点调试

image.png 这里取到的值为ObjectAssociationMapsecond最新值为Hello dog!

关联对象释放

走完main的作用域,person就会进入dealloc流程

image.pngdealloc中根据字符串匹配LGPerson精准下断点,然后进入rootDealloc

image.png 这里通过isa()输出,has_assoc值为1,进入object_dispose流程,在_object_remove_associations打断点

image.png 看到这里objectLGPerson对象,关联值有1

image.png 输出值为Hello dog!

关联对象总结

关联对象的存储结构为

  • 总哈希表AssociationsHashMap,存储的是关联对象和关联对象对应的关联表ObjectAssociationMap
  • 关联表ObjectAssociationMap中的Buckets为表的地址值;
  • 关联表中存储的是标识符key,和将valuepolicy包装在一起的ObjcAssociation

image.png

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

iOS底层之类的加载
iOS底层之分类的加载

以上是对类扩展和关联对象的探索总结,难免有不足和错误之处,如有疑问请在评论区留言。

iOS底层之分类的加载

作者 Joe天青色
2023年3月21日 15:22

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底层之Runtime探索(三)

作者 Joe天青色
2023年3月10日 15:17

序言

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

iOS底层之Runtime探索(一)
iOS底层之Runtime探索(二)
前面的文章中讲到了objc_msgSend的方法查找过程,在通过lookUpImpOrForward方法的慢速查找没有找到imp后,就到了动态方法决议流程resolveMethod_locked

动态方法决议

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    // 元类判断
    if (! cls->isMetaClass()) { // 对象方法流程
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else { // 类方法流程
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
  • 判断当前的class是否为元类
  • 不是元类,走实力方法流程resolveInstanceMethod
  • 元类,走类方法流程resolveClassMethod,判断resolveClassMethod是否已解决问题,未解决再走resolveInstanceMethod流程;
  • 走完动态决议流程,重新找一次imp

resolveInstanceMethod方法解析

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    // 1.查询元类是否实现了resolveInstanceMethod:方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    // 2.发送消息resolveInstanceMethod:
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls

    // 3.再次查询sel对应的imp,resolveInstanceMethod可动态修改
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
  1. 定义resolveInstanceMethod:方法;
  2. lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(true)):查询元类是否实现了resolveInstanceMethod:方法,cls->ISA(true)为类isa指向的元类,如果未实现直接返回;
  3. 通过objc_msgSend发送resolveInstanceMethod:消息,将返回结果给resolved
  4. lookUpImpOrNilTryCache(inst, sel, cls):再次查询当前类cls是否实现了sel对应的imp

lookUpImpOrNilTryCache方法解析

image.png

  • lookUpImpOrNilTryCache:是直接调用了_lookUpImpTryCachebehavior值为LOOKUP_NIL
  • _lookUpImpTryCache:的核心功能是再次走一次快速慢速方法查询。behavior值为LOOKUP_NIL,所以即使再次查不到也不会再走resolveMethod_locked流程。

resolveClassMethod方法解析

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    // 1.检查元类是否实现方法resolveClassMethod:
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    // 2.通过cls,获取处理的nonmeta
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }

    // 3.发送resolveClassMethod: 消息
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls

    // 4.再次查询imp是否实现
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
  1. 定义resolveClassMethod:方法;
  2. 通过方法lookUpImpOrNilTryCache,检查元类是否实现方法resolveClassMethod:,如果未实现直接返回;
  3. 通过cls,获取处理的nonmeta
  4. 通过objc_msgSend发送resolveClassMethod:消息,将返回结果给resolved
  5. lookUpImpOrNilTryCache(inst, sel, cls):再次查询当前元类cls是否实现了sel对应的imp

动态方法决议流程图

动态方法决议-导出(1).png

动态决议实例

定义一个类LGTeacher,声明两个方法,不在m文件实现。

  • 实例方法sayHelllo
  • 类方法sayByeBye

实现方法resolveInstanceMethod:但不做处理

image.png

实例方法

调用实例方法sayHello方法运行

image.pngLGTeacher中的resolveInstanceMethod:方法成功进来,并且进来了两次,然后打开resolveInstanceMethod:中的注释代码运行。

image.png 运行成功,我们成功通过动态向类添加方法lg_instanceMethod处理了sayHello方法找不到,引起的崩溃问题。

类方法

调用类方法sayByeBye方法运行

image.pngLGTeacher中的resolveClassMethod:方法成功进来,也是进来了两次,然后打开resolveClassMethod:中的注释代码运行。

image.png 运行成功,通过动态向元类添加方法lg_instanceMethod处理了sayByeBye方法找不到,引起的崩溃问题。

总结

我们可以在给公共父类NSObject添加分类,重写方法实现resolveInstanceMethod:,这样就可以成功解决所有imp找不到引起的崩溃问题了,同时我们也可以通过方法命名规范知道是哪个模块的哪个功能引起的问题,做BUG收集。
缺点:

  • 如果要对各个功能进行不同处理,需要大量业务功能逻辑判断,变得很复杂;
  • NSObject的分类中,很多系统的方法处理也会进来这里,会降低运行效率。

消息转发

如果我们未实现动态决议方法resolveInstanceMethod:,调用未实现的方法系统就会崩溃,这是我们通过bt指令查看堆栈信息 image.png 是通过doesNotRecognizeSelector:抛出错误的,在这之前还会经过_CF_forwarding_prep_0___forwarding___流程,这都是在CoreFoundation库里,苹果并没有真正开源CoreFoundation库。

image.png

在查阅苹果文档动态方法解析中,提到了消息转发 Message Forwarding,消息转发正是苹果给你提供的imp找不到的第二次机会。

Sending a message to an object that does not handle that message is an error. However, before announcing the error, the runtime system gives the receiving object a second chance to handle the message.

原理

  • 当对象发送的sel找不到imp时,系统会发送forwardInvocation:来通知该对象;
  • forwardInvocation:是从NSObject继承而来的,而在NSObject中只是调用doesNotRecognizeSelector:抛出错误;
  • 我们可以重写forwardInvocation:来实现消息的转发。
  • forwardInvocation:的唯一参数是NSInvocation
  • NSInvocation中,可以指定消息接收者target,调用invoke实现消息转发;
  • 消息转发完成,返回值将会返回给原始消息发送者;

文档里还介绍了有意思的点,在我们自己的类中要实现一个特定功能的方法,该功能已经在别的类中实现,我们又不能继承这个类,因为可能设计在不同的模块中,那我们就可以通过forwardInvocation:把消息转发特有的类去实现,这就实现类似多继承了。

image.png

实例

在关于forwardInvocation:api文档中,有个重要提示: image.png

除了forwardInvocation:之外,还必要重写methodSignatureForSelector:转发消息的机制使用从methodSignatureForSelector获得的信息来创建要转发的NSInvocation对象。重写方法必须为给定的选择器提供适当的方法签名方法签名可以是预先制定的,也可以是向另一个对象请求方法签名

LGTeacher中加上methodSignatureForSelectorforwardInvocation:方法后,再调用sayHello运行

image.png 成功运行,进入methodSignatureForSelectorforwardInvocation:方法,没有引起崩溃。
定义LGStudent实现sayHello方法,在LGTeacher中的forwardInvocation:,将消息转发给LGStudent

image.png

同样,在类方法的消息流程转发中,跟实例方法的实现是一致的,不过方法上要注意是+

image.png

forwardingTargetForSelector

在文档 forwardInvocation中还提供了forwardingTargetForSelector:方法

image.png

该方法的主要作用

无法识别的消息首先应指向的对象

如果对象实现(或继承)此方法,并返回非nil(和非self)结果,则返回的对象将用作新的接收方对象消息分派将恢复到该新对象。(显然,如果从该方法返回self,代码将陷入无限循环。)

如果您在非根类中实现此方法,如果您的类对于给定的选择器没有要返回的内容,那么您应该返回调用super实现的结果。

该方法为对象提供了一个机会,在更昂贵的forwardInvocation:机械接管之前重定向发送给它的未知消息。当您只想将消息重定向到另一个对象,并且可以比常规转发快一个数量级时,这非常有用。如果转发的目标是捕获NSInvocation,或在转发过程中操纵参数返回值,则此选项不适用

  • 大概意思是,如果只需要对未识别的消息,做简单的重定向,那么用此方法会比forwardInvocation:流程快很多很多,如果有额外的复杂操作,如捕获NSInvocation操纵参数返回值则不适用该方法。
  • forwardingTargetForSelector:的调用要在forwardInvocation:的前面。

image.png

forwardingTargetForSelector:实现重定向到LGStudent

image.png

流程图

KC消息转发机制.png

消息总结

到这里整个Runtime中对消息的处理流程objc_msgSend的解读都已完成,可能不够全面,中间有些细节难免有遗漏,不过重在整个流程的思维探索。

  1. 方法调用 --> objc_msgSend(id receiver, Sel)
  2. 通过isa获取类的地址cls
  3. 快速查找流程CacheLookup
  4. 慢速查找流程lookUpImpOrForward
  5. 动态方法解析resolveMethod_locked;
  6. 消息重定向forwardingTargetForSelector:;
  7. 消息转发forwardInvocation:

有兴趣的伙伴也可以搞一份源码跑一跑
iOS 全网最新objc4 可调式/编译源码

还有编译好的源码的下载地址

iOS底层之Runtime探索(二)

作者 Joe天青色
2023年3月7日 18:03

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

序言

在前一篇iOS底层之Runtime探索(一)中,已经知道了在selimp的整个缓存查找过程,这个过程是用汇编实现,是一个快速方法查找流程,今天就来探究一下缓存没有查找到后面的流程。

__objc_msgSend_uncached 方法探究

前面流程是对缓存进行查找imp,如果找不到就走方法__objc_msgSend_uncached方法 image.png__objc_msgSend_uncached中,共两条语句MethodTableLookupTailCallFunctionPointer x17

MethodTableLookup方法解析

image.pngMethodTableLookup中的代码逻辑

  • x0receiverx1sel
  • mov x2, x16: 将x16也就是clsx2
  • mov x3, #3: 将常量值3x3
  • 调用_lookUpImpOrForward: x0~x3是传的参数;
  • mov x17, x0: 将_lookUpImpOrForward的返回值x0x17

通过注释也可以大致看出,调用方法lookUpImpOrForward获取imp,并返回。

TailCallFunctionPointer定义

image.png 这里仅仅是对传入的$0直接调用。

我们可以总结,__objc_msgSend_uncached方法流程是通过MethodTableLookup获取imp,然后传值到TailCallFunctionPointer定义调用imp

这里的核心就是MethodTableLookup中的lookUpImpOrForward是怎么获取的imp

lookUpImpOrForward方法探究

lookUpImpOrForward通过方法命名,可以看出一些端倪,就是查找imp找不到就forward操作。

实例分析

  • 对上面定义的LGPerson添加分类,并在分类中同样实现sayHello方法;
  • NSObject添加分类,并自定义方法sayNB
  • LGTeacher定义方法sayByeBye,但不添加实现;

image.png 结果

  • sayHello调用的是LGPerson的分类方法,why?
  • 用类对象LGTeacher调用NSObjectsayNB方法成功,why?
  • 未实现方法sayByeBye报错unrecognized selector sent to instance 0x600000008000,why?

lookUpImpOrForward源码分析

源代码添加了中文注释

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    //定义消息转发forward_imp
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // 判断当前类是否初始化,
    if (slowpath(!cls->isInitialized())) {
        /*
         enum {
             LOOKUP_INITIALIZE = 1,
             LOOKUP_RESOLVER = 2,
             LOOKUP_NIL = 4,
             LOOKUP_NOCACHE = 8,
         };
         */

        // 如果没有初始化 behavior = LOOKUP_INITIALIZE | LOOKUP_RESOLVER | LOOKUP_NOCACHE
        behavior |= LOOKUP_NOCACHE;
    }

    // 加锁防止多线程访问出现错乱
    runtimeLock.lock();

    // 检查当前类是否被dyld加载的类
    checkIsKnownClass(cls);

    // 如果尚未实现给定的类,则实现该类;如果尚未初始化,则初始化该类。
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);

    runtimeLock.assertLocked();
    curClass = cls;


     //在我们获取锁后,用于再次查找类缓存的代码,但对于大多数情况,证据表明这在大多数情况下都是失败的,因此会造成时间损失。
     //在没有执行某种缓存查找的情况下,唯一调用此函数的代码路径是class_getInstanceMethod()。
    for (unsigned attempts = unreasonableClassCount();;) {// 开启循环查找imp
        // 继续检查共享缓存是否有方法
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel); // cache_getImp流程获取imp
            if (imp) goto done_unlock; // 找到imp走”done_unlock“流程
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // 从curClass的method list中查找method.
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done; // 查找到imp,跳转done流程
            }

            // 未从curClass的method list中查找到对应的method,继续往父类查找,直到父类为nil
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp; // 查找不到跳出for循环
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel); // 这里curClass已指向父类,查找父类的缓存中的imp
        if (slowpath(imp == forward_imp)) { // 表示未查找到跳出循环
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) { // 从父类缓存中查找到imp,跳转到done
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }
    // No implementation found. Try method resolver once.
    // 为找到imp,开始resolveMethod_locked流程,动态方法决议
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) { // 已完成类的初始化
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
        log_and_fill_cache(cls, imp, sel, inst, curClass); // 将获取的imp插入缓存
    }

 done_unlock:
    runtimeLock.unlock(); //runtimeLock 解锁
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

逻辑流程分析

  1. 判断当前类是否初始化,若未初始化执行behavior |= LOOKUP_NOCACHE
  2. checkIsKnownClass(cls)检查当前类是否被加载到dyld,类的加载属于懒加载,未加载这里会做加载处理;
  3. realizeAndInitializeIfNeeded_locked如果尚未实现给定的类,则实现该类;如果尚未初始化,则初始化该类;
  4. 进入for循环开始一层层遍历查找imp;
  5. curClass->cache.isConstantOptimizedCache检查共享缓存,这个时候,可能别的地方进行缓存了,如果有则直接跳转done_unlock返回imp
  6. 上面没有缓存,getMethodNoSuper_nolock从当前类的methodlist中查找method
  7. 如果找到跳转done,将找到的imp进行缓存插入,再返回imp
  8. 如果未找到,if (slowpath((curClass = curClass->getSuperclass()) == nil))这里将curClass指向父类,并判断如果父类为nil,将imp指向forward_imp,break跳出for循环;
  9. 如果父类存在,通过cache_getImp检查父类缓存是否有imp,如果impforward_imp则跳出循环,然后再检查imp,如果imp有效则跳转done流程;
  10. 查找for循环结束,没有找到imp,按LOOKUP_RESOLVER判断走动态方法决议流程resolveMethod_locked

归总分析就是,消息查找会沿着继承链一层一层往上查找,直到找到nil,如果有找到则插入缓存并返回imp,如果找不到则imp指向forward_imp也就是_objc_msgForward_impcache

上面LGTeacher调用NSObject的分类方法sayNB的过程,就是LGTeacher沿着元类的继承链找到了NSObject,并调用sayNB方法。

lookUpImpOrForward流程图

消息慢速查找流程-导出(1).png

getMethodNoSuper_nolock方法查找解析

getMethodNoSuper_nolock的方法中,查找method的算法就是findMethodInSortedMethodList方法中的二分查找算法实现的。

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);
    // method_list_t 数组是通过将sel转为uintptr_t类型的value值进行排序的
    auto first = list->begin(); // 起始位置 0
    auto base = first;
    decltype(first) probe;
    
    uintptr_t keyValue = (uintptr_t)key; // 目标sel
    uint32_t count; // 数组个数

    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1); // probe = base + floor(count / 2)

        uintptr_t probeValue = (uintptr_t)getName(probe)

        if (keyValue == probeValue) { // 目标命中
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            // 一直循环找最前面的同名方法
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }

        if (keyValue > probeValue) { // 目标value大于,二分法的中间数
            base = probe + 1;
            count--;
        }
    }
    return nil;
}

这里的二分算法设计还是挺巧妙的,可以慢慢品玩;其中在keyValue == probeValue的时候,会进入while循环,让probe--,直到找到同名方法中最前面的方法,同名方法就是我们分类中重写的方法,分类中方法会放在前面,所以调用是会优先调用分类中的方法实现。

cache_getImp方法解析

image.png 根据之前对CacheLookup的分析,cache_getImpMissLabelDynamic传的是LGetImpMissDynamic,因此如果CacheLookup中找不到imp就会进入LGetImpMissDynamic,这里仅仅是返回了0值,所以cache_getImp流程在这里也就断了返回的是0

forward_imp解析

在取父类(curClass = curClass->getSuperclass()) == nil)的判断中,如果往上没有父类了,即条件成立,那imp会指向_objc_msgForward_impcache类型的forward_imp,并返回执行imp

image.png

  • _objc_msgForward_impcache中只是对__objc_msgForward进行调用;
  • __objc_msgForward是对__objc_forward_handler相关的方法操作返回值到x17,并通过TailCallFunctionPointer调用x17

image.png__objc_forward_handler就是函数objc_defaultForwardHandler,这里面就是直接抛出错误unrecognized selector sent to instance

这里的错误信息字符串中,对+-的处理是通过元类判断的,底层中没有+号方法或-号方法,都是OC的语法设计。

总结

  • 在缓存cache中没有找到imp,就会通过lookUpImpOrForward开启从当前类NSObject方法列表进行层层遍历,这个过程为“慢速查找”过程;
  • 如果找到了就会返回imp,并执行;
  • 如果找不到就会让imp指向forward_imp并返回,执行forward_imp系统会直接抛出方法未找到的错误;
  • 在返回forward_imp之前,还有一步resolveMethod_locked操作,这就是动态方法决议的流程,在下一篇幅展开细讲。

以上是Runtime中对objc_msgSned方法的慢速查找流程解析,如有疑问或错误之处,请在评论区留言或私信我,感谢支持~❤

iOS底层之Runtime探索(一)

作者 Joe天青色
2023年3月6日 17:38

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

Runtime简介

Runtime 简称运行时Objective-C语言将尽可能多的决策从编译时链接时推迟到运行时。只要可能,它都会动态地进行操作。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译的代码。运行时系统充当Objective-C语言的一种操作系统(官方翻译)。
了解Objective-C运行时系统的工作原理以及如何利用它。但是,通常情况下,编写Cocoa应用程序时,您不需要了解和理解这些材料(官方翻译🐶)。

  • 编译时:顾名思义就是正在编译的时候。就是编译器帮你把源代码翻译成机器能识别的代码。编译器进行代码的语法分析,发现其中的编译错误和警告等,叫做静态类型检查
  • 运行时:代码跑起来被装载到内存中,运行时类型检查和编译时类型检查不一样,不是简单的代码扫描分析,而是在内存中做些操作。

Runtime官方介绍:Objective-C Runtime Programming Guide

Runtim探究

按照官方文档:

Objective-C programs interact with the runtime system at three distinct levels: through Objective-C source code; through methods defined in the NSObject class of the Foundation framework; and through direct calls to runtime functions.

三种和Runtime的交互方式:

  • 自定义方法调用: [person sayHello]
  • 系统动态库api:isKindOfClass
  • Runtime的api:class_getInstanceSize

我们探究Runtime就从最熟悉的自定义方法调用开始入手。

cpp方式查看

自定义类LGPersonLGPerson中自定义实例方法sayHello,然后在main函数中调用,并生成cpp文件查看。

image.png 共调用了4个方法

  • LGPersonalloc类方法;
  • 实例方法sayPerson
  • NSObject的方法isKindOfClass:
  • NSObjectclass类方法;

我们来看cpp中的代码实现

image.png 可以看到,不论实例方法还是类方法都是调用的函数objc_msgSend,我们对objc_msgSend进行梳理发现它的结构是objc_msgSend(id receiver, sel),那我们是不是也可以直接调用objc_msgSend呢?

objc_msgSend调用实现

image.png 调用成功,这里也就验证了方法的调用其实就是消息发送。在查看objc_msgSend时我还发现了一个方法objc_msgSendSuper

objc_msgSendSuper调用实现

image.png 查看objc_msgSendSuper定义 image.png 有2个参数,一个objc_super类型的指针,一个SEL,看一下objc_super

image.png 这里的成员super_class是第一要查找的类。

我们自定义LGTeacher类继承自LGPerson,调用父类的方法sayHelloobjc_msgSendobjc_msgSendSuper

image.png 可以看到,三种方式都能实现,那么objc_msgSend是怎么实现消息发送的呢?

objc_msgSend探究

通过汇编调试方法,发现objc_msgSend的定义是在libobjc库中 image.png 那我们就去源码找objc_msgSend,通过全局搜索锁定汇编文件objc-msg-arm64,接下来我们就来看objc_msgSend的汇编流程,加了一些注释

objc_msgSend汇编源码

// _objc_msgSend调用时有两个参数, id receiver(isa), SEL
ENTRY _objc_msgSend         //  _objc_msgSend 入口
UNWIND _objc_msgSend, NoFrame

cmp p0, #0       // 第一个参数receiver和0比较
#if SUPPORT_TAGGED_POINTERS  // 是否支持Taggedpointer类型
b.le LNilOrTagged //  (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif                      //cmp比较 receiver有值就走 endif
ldr p13, [x0] // p13 = isa (取出x0=isa赋值给p13)
GetClassFromIsa_p16 p13, 1, x0 // p16 = class (调用GetClassFromIsa_p16方法,p13, 1, x0作为参数传入)
LGetIsaDone: // 一个标记符号,拿到isa后操作完后,继续后面流程
// calls imp or objc_msgSend_uncached(调用CacheLookup,NORMAL, _objc_msgSend, __objc_msgSend_uncached作为参数传递)
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

  
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret

END_ENTRY _objc_msgSend

ENTRY _objc_msgLookup
UNWIND _objc_msgLookup, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LLookup_NilOrTagged //  (MSB tagged pointer looks negative)
#else
b.eq LLookup_Nil
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LLookup_GetIsaDone:
// returns imp
CacheLookup LOOKUP, _objc_msgLookup, __objc_msgLookup_uncached

伪代码复现一下代码逻辑

  1. 判断参数receider也就是isa是否为nil
  2. 是nil再判断是否支持Taggedpointer类型,如果支持则走LNilOrTagged流程,否则就走LReturnZero流程;
  3. receider不为nil,取出isa赋值给p13
  4. 调用GetClassFromIsa_p16,并传参数p13, 1, x0也就是isa,1,x0,回去class地址赋值给p16;
  5. 调用方法CacheLookup,并传参数NORMAL, _objc_msgSend, __objc_msgSend_uncached

GetClassFromIsa_p16方法解析

同样看一下GetClassFromIsa_p16源码,其核心功能是获取isa指向的class地址,这里也加了注释

// src = p13, needs_auth = 1, auth_address = x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA  // armv7k || (arm64 && !LP64)
// Indexed isa
mov p16, \src // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else   // 这里穿的needs_auth = 1,所以走else流程
// 64-bit packed isa
/**
 解析:
 src = p13(isa), needs_auth = 1, auth_address = x0
 .macro ExtractISA and    $0, $1, #ISA_MASK
 等于:
 (isa & #ISA_MASK) 赋值给 p16 --> 这里就是去出isa指向的class地址
 */
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
  • SUPPORT_INDEXED_ISAarmv7karm64切非LP64;
  • needs_auth参数为1;

根据上面两个条件GetClassFromIsa_p16的核心代码就是ExtractISA p16, \src, \auth_addressExtractISA也是宏定义源码为

.macro ExtractISA
    and    $0, $1, #ISA_MASK
.endmacro

结合GetClassFromIsa_p16ExtractISA解析

  • p16ExtractISA里面的 $0
  • srcp13也就是isaExtractISA里面的 $1
  • and $0, $1, #ISA_MASKisa & ISA_MASK = cls类的地址,即为从对象的isa获取class的过程。

这里得到$0也就是p16cls,继续走流程看CacheLookup

缓存查找

CacheLookup汇编源码解析

根据CacheLookup名称,我们也能猜出大概即从缓存中查找,从前面《类的缓存cache_分析》我们知道方法调用后是缓存在cache_t关联的bucket_t中,前面得到了p16也就是class,下面就是找类的bucket_t

CacheLookup源码

// NORMAL, _objc_msgSend, __objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

// 
    mov x15, x16 //x16 (p16 = isa) 取值 --> x15 (stash the original isa)
LLookupStart\Function:
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// (看真机环境)
ldr p11, [x16, #CACHE] // p11 = mask|buckets = cache
#if CONFIG_USE_PREOPT_CACHES // arm64 下为1
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function // 比较
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--
cmp p9, p1 //     if (sel != _cmd) {
b.ne 3f //         scan more

2: CacheHit \Mode // hit:    call or return imp

3: cbz p9, \MissLabelDynamic //     if (sel == 0) goto Miss;
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b

补充几个定义

  • 真机的CACHE_MASK_STORAGECACHE_MASK_STORAGE_HIGH_16,我们看真机环境;
  • #CACHE(2 * __SIZEOF_POINTER__)2倍指针大小2 * 8 = 16
  • arm64环境下CONFIG_USE_PREOPT_CACHES值为1;
  • __has_feature(ptrauth_calls): 是否为A12及更高处理器,我们看通用版本,默认这里为0;
  • PTRSHIFT值为3

按照上面定义复现一下代码逻辑

  1. mov x15, x16: 取x16也是p16(cls)x15
  2. ldr p11, [x16, #CACHE]: p16(cls)平移16字节得到cache,存在p11就是cache的地址;
  3. and p10, p11, #0x0000fffffffffffe: 0x0000fffffffffffebucketsMask掩码值,所以这里用cachebucketMask掩码值取得buckets地址存在p10
  4. eor p12, p1, p1, LSR #7p1SEL,这里对应源码cache_hash方法中的sel ^= sel >> 7p1右移7位得到的值再异或p1,存到p12
  5. and p12, p12, p11, LSR #48p11右移48位得到mask值,再和p12,即sel & mask得到sel的哈希下标值存在p12
  6. add p13, p10, p12, LSL #(1+PTRSHIFT)bucket_t的成员是selimp,内存大小为16字节,p12, LSL #(1+PTRSHIFT)相当于哈希下标值index左移4位,得到index对应与buckets首地址的偏移量, 通过p10也就是buckets首地址向下移动p12, LSL #(1+PTRSHIFT),取到bucket_t地址存在p13;

image.png

  1. 1:中的ldp p17, p9, [x13], #-BUCKET_SIZE相当于取出p13 bucket_t中的selp9,取impp17#-BUCKET_SIZE*buckets--先取值后--
  2. cmp p9, p1:比较缓存里的selp1是否一致,如果一致则走2:中的CacheHit缓存命中,\Mode为第一个参数值NORMAL,否则可能为哈希冲突也可能没有缓存该sel,进入3:语句;
  3. 3:中先判断p9是否有值,没有则说明没有缓存selMissLabelDynamic,也就是传入的第三个参数__objc_msgSend_uncached
  4. 如果p9有值,则比较p10p13是否同一个地址,不是则继续1:流程循环,如果是同一个地址,因为1:中是*buckets--遍历查找,也就意味着找到了buckets的首地址位置,那就跳转到buckets的最后位置继续循环。

image.png

  1. add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)): 取buckets中的最后一个bucket_t地址存在p13
  2. add p12, p10, p12, LSL #(1+PTRSHIFT):用p12记录第一次查找的位置;
  3. 4:中的逻辑是遍历最后一个位置到第一次查找的位置中的所有bucket_t,找到了就CacheHit,否则就MissLabelDynamic

CacheLookup中如果能够找到缓存方法,则走CacheHit中的NORMAL逻辑,找不到就走__objc_msgSend_uncached

CacheHit源码解析

image.pngCacheHit$0为传进来的NORMAL,所以这里的代码逻辑就是用TailCallCachedImp对缓存里找到的imp先解码再调用。

image.png

缓存查找流程图

类结构之cache_t-导出.png

总结

  • 汇编源码真恶心,慢慢跟流程还算能啃下来。
  • 通过上面的流程分析到objc_msgSend的调用,其实就是通过SEL查找IMP的过程,这个过程越快越好;
  • 汇编是比较接近机器码的,所以OC的设计是用汇编实现方法的缓存查找会提高方法调用的效率;
  • objc_msgSend流程就是先去类的缓存中找有没有对应的sel,找到了则直接调用缓存中的imp
  • 找不到imp就是下一个流程了,objc_msgSend慢速查找流程

以上是对Runtime的一些分析,以及方法调用过程中objc_msgSend的缓存查找实现流程分析,如有疑问或错误之处,请评论区留言或私信我。

iOS底层之类的cache分析

作者 Joe天青色
2023年3月3日 13:30

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

序言

在前面文章类的结构中,我们分析了bits的结构,isa以及superclass是为指针类型,还剩下一个cache没有分析,cache顾名思义就是缓存相关的,今天就来看一下cache是怎么个原理。

cache的数据结构

先自定义一个类LGPerson,代码实现 image.png

LLDB输出数据结构

LLDB调试输出,查看cache的数据

image.png 有几个关键数据:_bucketsAndMaybeMask_maybeMask_flags_occupied_originalPreoptCache

cache源码数据结构

然后我们再看cache_t的源码结构

image.png 总结

  • _bucketsAndMaybeMask:一个uintptr_t类型的指针;
  • 联合体:一个结构体和preopt_cache_t结构体类型的指针变量_originalPreoptCache
  • _maybeMask: mask_t泛型的变量;
  • _flagsuint16_t类型变量;
  • _occupieduint16_t类型变量
  • preopt_cache_tpreopt_cache_t结构体类型的指针变量;

这里我们还无从知道cache是怎样缓存的数据,以及缓存的是什么数据,是属性还是方法呢?

cache缓存数据类型

既然通过cache_t的数据结构看不出来,那我们就找方法

缓存应该有增删改查等方法,那就从这些方法下手吧。通过阅读源码,我们看到有一个insert方法和copyCacheNolock方法

image.pnginsert方法中,插入的是SELIMP,由此可以看出cache缓存的数据是方法method,然后再看一下insert的实现,找一下SELIMP是缓存在哪里。

cache缓存的存储位置

image.png 这里很明显是一个bucket_t类型的b,调用set方法插入SELIMP以及关联的Class

看一下bucket_t的结构。

image.png 这里我们可以简单总结一下类中cache_t的结构

类结构之cache_t-导出.png

cache缓存数据输出查看

现在我们已经找到了cache缓存的方法是存在bucket_t中,并且bucket_t有成员变量_sel_imp,在insert中是通过方法buckets()获取到的bucket_t,那我们就找到输出一下。

LLDB找到cache缓存数据

cache_t的结构体定义中,正好有buckets()方法,那我们在LLDB中获取到cache的地址变量就可以输出bucket_t

image.png

声明一个LGPerson类型的变量p,并调用对象方法sayHello

image.png 然后我们用LLDB调试输出信息

image.png 我们成功获取到了bucket_t类型的$3,但当我查看$3的内容是缺还是空值。why!!!why!!!why!!!

还是回归到insert源码,看一下到底是怎么插入的缓存吧。

image.png 天呢!漏了一个细节,这里缓存插入的时候是用了hash算法取下标的方式,那我们上面取到的第一个bucket_t的就可能为空值
既然这样,buckets()的存储结构是一个哈希数组,那我们就继续往下面找bucket_t

image.png 这里的_sel中的Value_imp中的Value明显和上面的不一样了,不再是nil0,那我们可以猜测这是一个有效的bucket_t。 找到bucket_t结构体中的方法sel()imp(),输出一下

image.png

image.png Done!!
这里我们成功找到了缓存的方法sayHello,但是我发现在LLDB这样调试很是麻烦,而且还依赖于源码的运行环境,如果有系统升级或者源码有更新,编译不了源码,难道只能GG吗,所以能不能脱离源码编译环境也能搞定上面的步骤呢

脱离源码分析cache

我们的目的是获取cache_t里面的bucket_tcache_t是在objc_class里面,那我们就按照源码objc_class的结构去自定义一个相似的结构体,这样就可以通过NSLog输出获取的内容信息。

typedef uint32_t mask_t// x86_64 & arm64 asm are less efficient with 16-bits
struct lg_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct lg_cache_t {

    struct lg_bucket_t * _buckets;
    mask_t               _maybeMask;
    uint16_t             _flags;
    uint16_t             _occupied;
};
struct lg_class_data_bits_t {
    uintptr_t bits;
};
struct lg_objc_class {
    Class isa;
    Class superclass;
    struct lg_cache_t cache;             // formerly cache pointer and vtable
    struct lg_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson * p = [LGPerson alloc];
        [p sayHello];
        
        Class pClass = [LGPerson class];
        struct lg_objc_class *lg_class = ( __bridge struct lg_objc_class *)(pClass);
        NSLog(@" - %hu  - %u",lg_class->cache._occupied,lg_class->cache._maybeMask);

        for (int i = 0; i < lg_class->cache._maybeMask; i++) {
            struct lg_bucket_t bucket = lg_class->cache._buckets[i];
            NSLog(@"SEL = %@ --- IMP = %p", NSStringFromSelector(bucket._sel), bucket._imp);
        }
        NSLog(@"Hello, World!");
    }
    return 0;
}

运行上面的代码,查看输出

image.png

成功输出,这里的_occupied为1,_maybeMask为3,我们再调两个方法sayHello_1sayHello_2验证一下。

image.png

image.png 这里发生了蹊跷,_occupied为1,_maybeMask变成了7,而缓存中只有方法sayHello_2,我们调用的sayHellosayHello_1却不在缓存中。既然这样,那就从头捋一遍源码,看看是不是又漏下什么细节了。

cache底层原理分析

对于底层原理分析,就从cache_t的插入方法insert入手

insert源码

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();
    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }
    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif
    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) { // 1.判断当前缓存是否为空的
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE; // 1 << 2 = 4
        reallocate(oldCapacity, capacity, /* freeOld */false); //开辟内存
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }
    
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;
    
    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }

    } while (fastpath((i = cache_next(i, m)) != begin));

    bad_cache(receiver, (SEL)sel);

#endif // !DEBUG_TASK_THREADS

}

整个代码流程我们按调用次数分布解析

第一次调用方法insert

image.png 第一次插入缓存时

  1. _occupied的值为0,所以newOccupied为1;
  2. capacity()取的是_maybeMask的值,所以oldCapacitycapacity值都为0;
  3. isConstantEmptyCache判断当前缓存是否为空,条件成立,进入if语句;
  4. capacity值为0,赋值为INIT_CACHE_SIZEINIT_CACHE_SIZE = 1 << 2值为4;
  5. 调用reallocate开辟内存;

image.pngreallocate方法中,开辟新内存,然后调用setBucketsAndMask方法,使cache_t中的成员变量_bucketsAndMaybeMask_maybeMask_occupied做关联

image.png 之后是再开辟的缓存空间中存入SELIMP

image.png 在存入SELIMP的方法中,有对IMP进行编码,实际上存入的是编码后的newImp

image.pngimp编码的源代码

image.png

image.png 我们用到的CACHE_IMP_ENCODING情况为CACHE_IMP_ENCODING_ISA_XOR,所以上面的编码算法是imp & cls

我们知道了第一次调用方法,会开辟空间为4缓存空间,当我们调用更多方法的时候,应该在什么时候扩容呢?

四分之三扩容

当我们不是第一次调用方法时,就会进入一个剩余空间容量判断

image.png

  • newOccupied:进入缓存的第几个方法;
  • CACHE_END_MARKER:宏定义值为1;
  • cache_fill_ratio(capacity): capacity * 3 / 4容量的3/4值;

这里我们知道当新的调用方法进入缓存时

  1. 如果不满足扩容条件,就会继续往开辟的缓存空间插入一条缓存数据。比如:调用sayHello_1时,newOccupied为2,capacity为4, 2 + 1 <= 4 *3 / 4的条件满足。
  2. 如果到达扩容条件,就会先开辟2倍的新内存,然后再插入新的缓存数据。比如:调用sayHello_2时,newOccupied为3,capacity为4, 3 + 1 <= 4 *3 / 4的条件不满足,就会进入else语句,开辟2倍容量的新内存。


在开辟新内存中调用方法reallocate时,传入的最后一个参数freeOldtrue,会把旧的缓存空间清理释放掉,不会copy缓存数据到新的缓存空间,这也是为什么调用sayHello_2时,输出的只有sayHello_2

image.png

总结

关于objc_class中的cache的原理分析,我们先是查看cache_t数据结构,根据数据结构我们无法知道其工作原理,然后我们通过结构体中的方法去找线索,最后锁定insert方法,根据insert方法来大致了解整个缓存插入的流程。
cache_t的工作原理流程图:

类结构之cache_t-导出(1).png

iOS底层之类的结构分析

作者 Joe天青色
2023年3月1日 15:08

isa流程分析

首先来看一张比较熟悉的isa的流程走位图

isa流程图.png

isa指向链

  • 实例对象instanceisa指向类class;
  • 类对象class也有isa指向的是元类meta
  • 元类mata中也有isa指向的是根元类root meta

isa走位图(1).png

类的继承链

  • 子类继承与父类父类继承与根类根类指向的是nil
  • 元类中也存在继承,子类的元类继承与父类的元类父类的元类继承与根元类根元类又继承与根类;

类的继承链.png

isa流程实例验证

补充:在获取到对象的isa值后,可以通过&(与)一个掩码ISA_MASK 0x007ffffffffffff8ULL来获取到对象关联的类地址。

image.png

isa指向链验证

定义一个LGPerson继承NSObject,同时定义LGTeacher继承LGPerson

LGPerson *p = [LGPerson alloc];
NSLog(@"%@",p);
LGTeacher *t = [LGTeacher alloc];
NSLog(@"%@",t);

LLDB调试对象t,通过tisa找到关联类LGTeacher的地址为0x0000000100008310

image.png 接着我们按同样的方式输出,找到LGTeacher类对象的isa关联类地址为0x00000001000082e8image.png 往下找,找到LGTeacher元类对象的isa关联类地址为0x00007ff85e5f2220

image.png 按同样的方式,找到NSObject类对象的isa关联地址为0x00007ff85e5f2220,和LGTeacher元类对象的isa关联类地址一致,可以验证元类isa指向根元类

image.png根元类isa指向自己。 image.png

类的继承链验证

同样按照LGPerson继承NSObjectLGTeacher继承LGPerson的定义。

Class tClass = LGTeacher.class;
Class pClass = class_getSuperclass(tClass);
Class nClass = class_getSuperclass(pClass);
Class rClass = class_getSuperclass(nClass);
NSLog(@"\n tClass-%@ \n pClass-%@ \n nClass-%@ \n rClass-%@ \n", tClass, pClass, nClass, rClass);

image.png

通过上面代码输出打印,很明显看出subClass->superClass->NSObject->nil的继承链关系。

LGTeacher * teacher = [LGTeacher alloc];
Class tClass = object_getClass(teacher);
Class mtClass = object_getClass(tClass);
Class mtSuperClass = class_getSuperclass(mtClass);
NSLog(@"\n teacher %p 实例对象 -- %p 类 -- %p 元类 -- %p 元类父类", teacher, tClass, mtClass, mtSuperClass);

LGPerson * person = [LGPerson alloc];
Class pClass = object_getClass(person);
Class mpClass = object_getClass(pClass);
Class mpSuperClass = class_getSuperclass(mpClass);
NSLog(@"\n person %p 实例对象 -- %p 类 -- %p 元类 -- %p 元类父类", person, pClass, mpClass, mpSuperClass);

NSObject * obj = [NSObject alloc];
Class objClass = object_getClass(obj);
Class mobjClass = object_getClass(objClass);
Class mobjSuperClass = class_getSuperclass(mobjClass);
NSLog(@"\n NSObject %p 实例对象 -- %p 类 -- %p 元类 -- %p 元类父类", obj, objClass, mobjClass, mobjSuperClass);

teacher 0x600000201ba0 实例对象 -- 0x100008310 类 -- 0x1000082e8 元类 -- 0x100008338 元类父类

person 0x600000201bc0 实例对象 -- 0x100008360 类 -- 0x100008338 元类 -- 0x7ff85e5f2220 元类父类

NSObject 0x600000004030 实例对象 -- 0x7ff85e5f2270 类 -- 0x7ff85e5f2220 元类 -- 0x7ff85e5f2270 元类父类

通过输出信息我们可以看出

  • teacher的元类父类地址0x100008338 == person的元类地址0x100008338;
  • person的元类父类地址0x7ff85e5f2220 == NSObject的元类地址0x7ff85e5f2220;
  • NSObject的元类父类地址0x7ff85e5f2270 == NSObject的类地址0x7ff85e5f2270;

综合,我们可以得出元类的继承链关系为Sub Meta->Super Meta->Root Meta-> NSObject

类的结构

前面对象的本质中,我们知道Class的定义是objc_class结构体类型,那么就看一下objc_class的结构体的结构是怎样定义的。

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;             // formerly cache pointer and vtable
class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
... 其他代码
}

这里objc_class的继承与objc_objectobjc_object的结构中只有一个isa指针, 因此,objc_class的成员组成为:

  1. objc_object继承的指针isa;
  2. superclass: Class类型指向父类的指针
  3. cache:缓存相关的东西
  4. bits:数据

类的结构.png

cache结构

cache_t也是一个结构体定义

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // uint32_t 4字节
#if __LP64__
            uint16_t                   _flags;     // 2字节
#endif
            uint16_t                   _occupied;  // 2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;  // 8字节
    };
 }

分析整个cache_t的结构,发现cache_t的内存总共为16字节。 关于cache_t的探究专门写了一篇文章。

bits探究

bits是今天的探究重点,bits到底存储了哪些数据,在objc_class里有一段源码是data操作。

class_rw_t *data() const {
    return bits.data();
}
void setData(class_rw_t *newData) {
    bits.setData(newData);
}

这里data的类型是class_rw_t,在class_rw_t的源码中,我们可以看到几个方法:

image.png

ro:成员变量、methods:方法、properties:属性、protocols协议 ,我们在类中定义的方法,属性等就行通过调取class_rw_t结构体中的方法获取的。

实例探究

下面我们通过实例,来验证一下类的结构是否如上面分析的一致。

我们自定义一个类LGPerson继承自NSObject,定义一些属性和实力方法以及类方法。

image.pngLLDB调试输出

image.png

这里我们可以看出第一个地址0x0000000100008228是类的第一个成员isa,第二个地址0x0000000100816140是类的第二个成员superclass

前面我们知道isa指针8字节,superclass指针8字节,cache结构体16字节,通过LGPerson的首地址便宜8 + 8 + 16 = 32字节我们就可以得到bits的内存地址。

image.png 这里用LGPerson的首地址0x100008250加上32字节,得到地址0x0000000100008270,然后转换一下指针类型,得到$9class_data_bits_t类型,调用data方法就得到$10地址为class_rw_t类型,下面就来探究class_rw_t中的属性、方法、协议。

注意

在转换class_data_bits_t这一步,需要在源码环境调试,关于源码的下载和编译,介绍一个靓仔,# iOS 全网最新objc4 可调式/编译源码还有编译好的源码的下载地址

属性properties

调用class_rw_tproperties()方法,得到property_array_t类型的数组,继承与list_array_tt,找到list下的ptrimage.png

image.pngptrproperty_list_t类型,是继承与entsize_list_tt

image.pngentsize_list_tt部分源码

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;  // 数量

    uint32_t entsize() const {
        return entsizeAndFlags & ~FlagMask;
    }
    uint32_t flags() const {
        return entsizeAndFlags & FlagMask;
    }
    Element& getOrEnd(uint32_t i) const {
        ASSERT(i <= count);
        return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
    }
    Element& get(uint32_t i) const { // 获取元素方法
        ASSERT(i < count);
        return getOrEnd(i);
    }
    // ... 省略
};

通过get方法,获取元素,可以看到下面的结果,LGPerson中的属性nameageproperties()里,而成员变量hobby不在这里。

image.png

方法methods

调用class_rw_tmethods()方法,得到method_array_t类型的数组,继承与list_array_tt,同样找到list下的ptr

image.png 这里看到ptrmethod_list_t类型,同样继承与entsize_list_tt,其中有count为6,输出查看

image.png

这里的元素为method_t类型,method_t为结构体类型,其中的一个成员变量为big的结构体,里面是方法名称等信息。

image.png 我们在调用method_tbig方法,查看输出

image.png 这里看到6个方法分别是

  • 实例方法:sayHello
  • 属性name的get方法;
  • C++析构函数:.cxx_destruct
  • 属性nameset方法;
  • 属性ageget方法;
  • 属性ageget方法;

这里的6个方法都是实力对象方法,并没有我们定义的类方法sayWorld

协议protocols

我们自定义一个协议LGPersonDelegate,让LGPerson遵守并实现协议方法

image.png 调用class_rw_tprotocols()方法,得到protocol_array_t类型的数组,继承与list_array_tt,同样找到list下的ptr

image.png 这里看到ptrprotocol_list_t类型,不是继承自entsize_list_tt,那我们看一下protocol_list_t的定义

image.png 看到protocol_list_t的定义,我们知道count值为1,说明是有值,但是其成员是protocol_ref_tuintptr_t类型,那怎么输出查看这个count中的1到底是什么呢 image.png 查看protocol_ref_t的定义,通过注释信息,我们可以看到protocol_ref_t未映射到protocol_t类型,那我们就找protocol_t的定义

image.png 这里看到protocol_t中有mangledName以及instanceMethods等,只要得到protocol_t就可以输出我们想要的名称方法等信息,怎么才能从protocol_ref_t映射到protocol_t呢,全局找一下吧

image.png 一不小心找到了😁,这里我们看到,protocol_ref_t是可以强转protocol_t的,那我们就试试

image.png 强转成功,调用demangledName方法,我们就得到了LGPersonDelegate,那我们再找一下协议方法

image.png 按照method输出的经验,成功找到协议方法personDelegateMethod

ro

调用class_rw_tro方法,得到class_ro_t的结构体

image.png 查看class_ro_t的内容,看到这里有成员ivars,这引起了我们的注意

image.png 接着查看ivars,是ivar_list_t类型的结构体,也是继承entsize_list_tt,那么我们就可以调用get方法查看成员。

image.png

image.png

调用get方法查看输出,这里就可以看到3个成员变量,自定义hobby和系统帮我们定义的属性生成带_的成员变量。

image.png

类方法

在上面的methods方法中,我们并没有得到定义的类方法sayWorld,不由得我陷入了沉思,我们定义的类结构中,没有我们定义的类方法,那类方法会存在哪里呢?

这里我们换个思考角度想一下,在methods中全都是实例方法,也就是对象方法对象方法是存在类中;类方法类对象在调用,那类方法是不是存在元类中呢?不得而知,那么我们就去验证一下。

image.png

首先获取到LGPerson的元类地址,然后按照methods方面的方法步骤,我们成功输出sayWorld方法,也验证了上面的猜想。

这里我们不由的想,OC的底层是CC++实现的,在C语言C++中,不存在对象方法类方法的区分,有的都是函数实现,在OC的设计中,一个可以new出无数个对象,因此把方法存在中,而不是动态创建的对象中,是合理的。

因为OC对象方法类方法的定义是-+的区分,那么方法名称就会有重名的存在,因此才会引入元类的概念,元类的存在就是解决类方法的问题。

类的结构总结

类的结构总结用一张流程图

objc_class.png 其中protocol_ref_t是一个无符号长整型,可以强转为protocol_t,这是后面才找到的。

以上就是对于OC中类的结构分析,如有疑问或错误之处,请评论区留言或私信我,非常感谢!!!

iOS底层之对象的本质

作者 Joe天青色
2023年2月27日 13:55

序言

在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中对象的本质分析,如有错误或疑问之处,请在评论区留言吧!!!

iOS 底层之内存对齐

作者 Joe天青色
2023年2月23日 14:28

序言

数据类型都有固定的内存大小,在结构体当中属性的内存排序遵循内存对齐原则,那么内存对齐的原理是怎么回事,内存对齐有什么好处?在OC中是怎么实现内存对齐的呢?

这里是不同数据类型的字节数

image.png

内存对⻬的原则

  1. 数据成员对⻬规则
    结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。
  2. 结构体作为成员
    如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  3. 收尾工作
    结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。

实例

代码

先来看这三个结构体的内存大小

struct LGStruct1 {
    double a;       //8
    char b;         //1
    int c;          //4
    short d;        //2
}struct1;

struct LGStruct2 {
    double a;       //8
    int b;          //4
    char c;         //1
    short d;        //2
}struct2;

struct LGStruct3 {
    double a;      //8
    int b;         //4
    char c;        //1
    short d;       //2
    int e;         //4
    struct LGStruct1 str; //
}struct3;

通过sizeof获取到struct1的内存是24,struct2的内存是16,struct3的内存是48,struct1struct2这两个结构体所包含的数据成员是一样的,位置不一样,从而造成内存大小不一样,这就是内存对齐原则造成的。

具体分析

1.结构体struct1
  • a: 8字节,offset为0,即[0 ~ 7]存放a;
  • b: 1字节,offset为8,即[8]存放b;
  • c: 4字节,offset为9,但要找4的倍数,即[12 ~ 15]存放c;
  • d: 2字节,offset为16,16为2的倍数,即[16 ~ 17]存放d;

根据内部最大成员的整数倍原则,struct1的内存大小为24。

2.结构体struct2
  • a: 8字节,offset为0,即[0 ~ 7]存放a;
  • b: 4字节,offset为8,即[8~11]存放b;
  • c: 1字节,offset为12,即[12]存放c;
  • d: 2字节,offset为13,但要找2的倍数,即[14 ~ 15]存放d;

根据内部最大成员的整数倍原则,struct2的内存大小为16。

3.结构体struct3
  • a: 8字节,offset为0,即[0 ~ 7]存放a;
  • b: 4字节,offset为8,即[8~11]存放b;
  • c: 1字节,offset为12,即[12]存放c;
  • d: 2字节,offset为13,但要找2的倍数,即[14 ~ 15]存放d;
  • e: 4字节, offset为16,16为4的整数倍,即[16 ~ 19]存放e;
  • str: 24字节,offset为20,找24的整数倍,即[24 ~ 47]存放str;

根据内部最大成员的整数倍原则,struct3的内存大小为48,其实按结构体成员要从其内部最大元素大小的整数倍地址开始存储原则,str中最大元素是8,找到str的起始位置也是24。

内存对齐原因

  • 内存是以字节为基本单位,cpu在存取数据时,是以为单位存取,并不是以字节为单位存取。频繁存取未对齐的数据,会极大降低cpu的性能。字节对齐后,会减低cpu的存取次数,这种以空间换时间的做法目的降低cpu的开销。

  • cpu存取是以块为单位,存取未对齐的数据可能开始在上一个内存块,结束在另一个内存块。这样中间可能要经过复杂运算在合并在一起,降低了效率。字节对齐后,提高了cpu的访问速率。

思考

在OC中,我们所定义的类其实也是结构体,那么我们是不是在开发中也要注意声明变量的位置,系统会不会帮我们处理这个问题呢。
这里我们新建一个类LGPerson,他的成员和结构体struct1一样, image.png

LGPerson *person = [LGPerson alloc];
person.a = 8.0;
person.b = 'b';
person.c = 5;
person.d = 2;
NSLog(@"%lu", class_getInstanceSize(LGPerson.class));

通过输出我们发现LGPerson的实际内存大小也是24字节,但已知类中有isa要占8个字节,所以LGPerson的成员所占内存大小是16字节。

image.png

通过上图lldb调试信息可以看出,在类的成员属性存储中,系统是进行过优化的。

总结

内存对齐可以提高cpu的存取效率同时提升安全性,会有部分内存的浪费,但是系统又会根据数据存储情况进行内存优化,尽可能降低内存浪费,这样即保证了性能又减少了浪费。

image.png 图中是malloc_size对内存大小的计算,其中就有一个先左移4位,又右移4位的算法,其实就是16字节对齐算法。

❌
❌