【iOS】Category、Extension和关联对象

Category分类

Category 是 比继承更为简洁 的方法来对Class进行扩展,无需创建子类就可以为现有的类动态添加方法。

  • 可以给项目内任何已经存在的类 添加 Category
  • 甚至可以是系统库/闭源库等只暴露了声明文件的类 添加 Category (看不到.m 文件的类)
  • 通过 Category 可以添加 实例方法、类方法、属性
    • 注意:通过 Category 可以添加属性,会声明settergetter方法 ,但需要 开发者 自己 实现 settergetter方法(使用关联对象实现属性)
    • 通过 Class 添加属性,会默认生成并实现 settergetter方法
  • 分类也可以把framework私有方法公开化
    • 比如我们假如我有一个类里有一个私有方法A 外界是调用不到的 但是我给这个类写了个category 在里里面申明了一个方法(也叫A,只申明,不实现) 现在我import这个category 调用 这个A 的情况是怎么样的呢?实际上这时候就会调用私有方法这个A,我们通过分类将私有方法公开化了
  • 通过 Category 可以 重新实现 在 Class中已存在的 方法
  • 通过 Category 可以 重新实现 在 其它 Category中已存在/已实现 的方法
    [ 在iOS中,实例对象/类对象方法调用顺序严格依赖 源码文件的编译顺序,编译顺序的查看可以通过Xcode>Build Phases>Compile Sources查看:
    • 类与 各个分类 各自声明且实现各自的方法:没有方法的实现被覆盖,分类 只是扩展了 类的 功能
    • 类与 各个分类 存在 声明 且实现 了同名的 方法: 存在 方法的实现被覆盖(实际上不是被覆盖,而是方法地址后挪,系统会找到同名方法在内存地址中位置较前的方法 实现 调用)
      • 分类 方法实现 的优先级 > 原来的类
      • 各个分类 中 被覆盖的情况严格 依赖 源码 文件的编译顺序:
        • 先编译的 方法 会 先加入 方法列表「先入栈」
        • 后编译的 方法 会 后加入 方法列表「后入栈」
        • 系统在调用 方法 的实现的时候,通过 对象(实例对象、类对象等) 和 方法API 在底层发送消息,拿到方法 实现 的 实现 IMP指针 找到 方法的具体实现(实际上最终拿到的方法实现,是后编译的源码文件中的方法实现)

官方介绍的优点有两点:

  • 可以把类的实现分开在几个不同的文件里面
    • 可以减少分开文件的体积
    • 可以把不同的功能组织到不同的category里
    • 可以有多个开发者共同完成一个类
    • 可以按需加载想要的类别等等
  • 声明专有方法

Extension扩展

延展(Extension)可以理解成是匿名的Category

可以用来给类 添加 属性和方法 的声明,不作用在Subclass

可以 通过 在.m文件 给其它类 或者 当前 类 添加 Extension

给当前类添加 Extension 时,编译器会默认给 添加的属性 声明且实现 其setter&&getter方法

也可以 通过 .h文件 给类 添加 Extension

要对 Extension添加的 属性和方法进行实现

若 对 Extension的实现中 ,重新实现 原来 类或其它分类中已有的方法,不会对原来的方法执行产生影响(因为没有自身的.m文件,不存在源码实现文件的编译的情况)
Extension的作用更多在于拆分结构复杂的类,比较清晰的暴露声明

Category的实质

Category结构体

c 复制代码
typedef struct category_t *Category;
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;  //实例方法
    struct method_list_t *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);
};

分类的结构体中可以为类添加对象方法、类方法、协议、属性,但是并没有成员变量

将分类转成C++看起

接着我们将分类的.m文件转成C++文件来了解一下:

我们首先先创建一个分类:

objectivec 复制代码
#import "Car.h"
#import "protocolForCar.h"

NS_ASSUME_NONNULL_BEGIN

@interface Car (match)<CarProtocol>
@property (nonatomic, copy) NSString *carType;

- (void)matchPrint;
+ (void)matchClass;


@end

NS_ASSUME_NONNULL_END

我们在其中声明了一个实例方法、一个类方法、一个属性

分类遵循一个协议,协议里面也是一个类方法和对象方法

objectivec 复制代码
#ifndef protocolForCar_h
#define protocolForCar_h

@protocol CarProtocol <NSObject>

- (void)protocolMethod;
+ (void)protocolClassMethod;

@end

然后使用:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Car+match.m -o test.cpp,将该分类的.m文件转为C++文件:

cpp 复制代码
//category结构体
struct _category_t {
	const char *name;
	struct _class_t *cls;
	const struct _method_list_t *instance_methods;
	const struct _method_list_t *class_methods;
	const struct _protocol_list_t *protocols;
	const struct _prop_list_t *properties;
};

//category结构体赋值
static struct _category_t _OBJC_$_CATEGORY_Car_$_match __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Car",
	0, // &OBJC_CLASS_$_Car,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Car_$_match,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Car_$_match,
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Car_$_match,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Car_$_match,
};

//结构体数组
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_Car_$_match,
};

我们可以看到重点的三个元素

  • category结构体
  • category结构体的赋值语句
  • category结构体数组

对象方法列表结构体

cpp 复制代码
//本类对象方法的实现
static void _I_Car_match_matchPrint(Car * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_Car_match_633f34_mi_0);
}

//协议中对象方法的实现
static void _I_Car_match_protocolMethod(Car * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_Car_match_633f34_mi_2);
}

//对象方法列表结构体
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Car_$_match __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"matchPrint", "v16@0:8", (void *)_I_Car_match_matchPrint},
	{(struct objc_selector *)"protocolMethod", "v16@0:8", (void *)_I_Car_match_protocolMethod}}
};
  • - (void)matchPrint- (void)protocolMethod方法的实现
  • 对象方法结构体列表结构体

只要是在Category中实现了的对象方法(包括代理中的对象方法)。都会添加到对象方法列表结构体_OBJC_$_CATEGORY_INSTANCE_METHODS_Car_$_match中来,如果仅仅是定义,没有实现,不会加进来

类方法列表结构体

cpp 复制代码
//本类类方法的实现
static void _C_Car_match_matchClass(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_Car_match_633f34_mi_1);
}

//协议中的类方法
static void _C_Car_match_protocolClassMethod(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_Car_match_633f34_mi_3);
}

//类方法列表结构体
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_CLASS_METHODS_Car_$_match __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"matchClass", "v16@0:8", (void *)_C_Car_match_matchClass},
	{(struct objc_selector *)"protocolClassMethod", "v16@0:8", (void *)_C_Car_match_protocolClassMethod}}
};
  • + (void)matchClass+ (void)protocolClassMethod类方法的实现
  • 类方法列表结构体

只要是Category中实现了的类方法(包括代理中的类方法)。都会添加到类方法列表结构体_OBJC_$_CATEGORY_CLASS_METHODS_Car_$_match中来

协议列表结构体

cpp 复制代码
//协议结构体
struct _protocol_t {
	void * isa;  // NULL
	const char *protocol_name;
	const struct _protocol_list_t * protocol_list; // super protocols
	const struct method_list_t *instance_methods;
	const struct method_list_t *class_methods;
	const struct method_list_t *optionalInstanceMethods;
	const struct method_list_t *optionalClassMethods;
	const struct _prop_list_t * properties;
	const unsigned int size;  // sizeof(struct _protocol_t)
	const unsigned int flags;  // = 0
	const char ** extendedMethodTypes;
};

//分类中添加的协议列表结构体
static struct /*_protocol_list_t*/ {
	long protocol_count;  // Note, this is 32/64 bit
	struct _protocol_t *super_protocols[1];
} _OBJC_PROTOCOL_REFS_CarProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	1,
	&_OBJC_PROTOCOL_NSObject
};

//协议列表 对象方法列表结构体
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_CarProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"protocolMethod", "v16@0:8", 0}}
};

//协议列表 类方法列表结构体
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_PROTOCOL_CLASS_METHODS_CarProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"protocolClassMethod", "v16@0:8", 0}}
};

//结构体赋值
struct _protocol_t _OBJC_PROTOCOL_CarProtocol __attribute__ ((used)) = {
	0,
	"CarProtocol",
	(const struct _protocol_list_t *)&_OBJC_PROTOCOL_REFS_CarProtocol,
	(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_CarProtocol,
	(const struct method_list_t *)&_OBJC_PROTOCOL_CLASS_METHODS_CarProtocol,
	0,
	0,
	0,
	sizeof(_protocol_t),
	0,
	(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_CarProtocol
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_CarProtocol = &_OBJC_PROTOCOL_CarProtocol;

属性列表结构体

cpp 复制代码
//属性结构体
struct _prop_t {
	const char *name;
	const char *attributes;
};

//属性列表结构体
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Car_$_match __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"carType","T@\"NSString\",C,N"}}
};

从属性列表结构体源码中我们可以看到:只有person分类中添加的属性列表结构体_OBJC_$_PROP_LIST_NSObject_$_testCategory,没有成员变量结构体_ivar_list_t结构体。更没有对应的set/get方法相关的内容。

这也说明了Category中不能添加成员变量这一事实

category总结

主要包含下面几种部分内容:

  • _method_list_t 类型的 对象方法列表结构体
  • _method_list_t 类型的 类方法列表结构体
  • _protocol_list_t 类型的 协议列表结构体
  • _prop_list_t 类型的 属性列表结构体
  • _category_t结构体中并不包含_ivar_list_t类型,也就是不包含成员变量结构体

分类在运行期做了什么

(这部分内容比较臃肿,可以先去参考文章后面的分类加载的总结,然后结合总结看这里的源码分析会更有逻辑性)

要搞懂这个问题的话,我们就需要知道什么时候调用了分类的方法

_objc_init这个函数是runtime的初始化函数,我们就从_objc_init开始入手:

_objc_init

c 复制代码
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    //环境变量
    environ_init();
    //绑定线程析构函数
    tls_init();
    //静态构造函数
    static_init();
    //runtime准备,创建2张表
    runtime_init();
    //异常初始化
    exception_init();
#if __OBJC2__
    //缓存
    cache_t::init();
#endif
    //macos专有
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}

其中我们发现了一些初始化创建过程,这里我们主要关注一下runtime_init:

cpp 复制代码
void runtime_init(void)
{
    //分类加载表
    objc::unattachedCategories.init(32);
    //类的加载表
    objc::allocatedClasses.init();
}

可以看到其中有一张分类加载表。

接着我们在回到_objc_init中, map_images读取资源(images代表资源模块),来到map_images_nolock函数中找到_read_images函数,在_read_images函数中找到与分类相关的代码:

_read_images

c 复制代码
    // Discover categories. Only do this after the initial category
    // attachment has been done. For categories present at startup,
    // discovery is deferred until the first load_images call after
    // the call to _dyld_objc_notify_register completes. rdar://problem/53119145
	//发现类别。只有在初始类别之后才这样做
	//附件已完成。对于启动时出现的类别,
	//发现延迟到之后的第一个load_images调用
	//调用_dyld_objc_notify_register完成。rdar: / /问题/ 53119145
	//意思是非懒加载的分类走的是load_images
	
	//那么作为对应,懒加载的分类就走的是这里 _read_images中的操作
    //全局变量didInitialAttachCategories,执行load_images 的时候设置为YES
    //所以只有当执行过load_images的时候,这里才会遍历load_catagories_nolock去加载分类,而这里遍历的也是一些懒加载的类的分类。
    //这里的判断条件didInitialAttachCategories意思是是否进行完初始的分类添加(如果进行过的话,也就是非懒加载的分类以经添加了的话,就进去执行if分支中的内容)
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }

    ts.log("IMAGE TIMES: discover categories");

load_images

c 复制代码
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        // 加载所有的分类
        loadAllCategories();
    }

    // Return without taking locks if there are no +load methods here.
    //有load方法的话直接返回,没有的话才执行后面找load方法和调用load方法的代码
    //如果这里没有+load方法,返回时不带锁。
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        // 找到load方法
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }
    
    // Call +load methods (without runtimeLock - re-entrant)
    //调用load方法
    call_load_methods();
}

我们放上一张load_images的流程图,便于理解其中的整个过程。

其中有一个重要的点,就是在获取分类的load方法的时候,我们是先获取了非懒加载分类的列表,然后调用realizeClassWithoutSwift对其分类的主类进行了实现,这点非常重要。后续整个流程总结时会提到,调用realizeClassWithoutSwift的源码如下:

c 复制代码
//prepare_load_methods是load_images中获取主类和分类load方法时调用的函数
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        //此处调用realizeClassWithoutSwift实现了分类对应的主类
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

用于实现主类的方法:realizeClassWithoutSwift的源码如下:

c 复制代码
static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    class_rw_t *rw; // 读写数据
    Class supercls; // 父类
    Class metacls;  // 元类

    if (!cls) return nil; // 如果为空,返回nil
    if (cls->isRealized()) return cls;  // 如果已经实现,直接返回
    ASSERT(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?

    auto ro = (const class_ro_t *)cls->data(); // 读取类的数据
    auto isMeta = ro->flags & RO_META; // 是否是元类
    if (ro->flags & RO_FUTURE) { // rw已经有值的话走这里
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro();
        ASSERT(!isMeta);
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else { // 正常的类走这里
        // Normal class. Allocate writeable class data.
        rw = objc::zalloc<class_rw_t>(); // 开辟rw
        rw->set_ro(ro); // 把cls的数据ro赋值给rw
        rw->flags = RW_REALIZED|RW_REALIZING|isMeta; // 更新flags
        cls->setData(rw); // 再把rw设置为cls的data数据
    }

#if FAST_CACHE_META
    if (isMeta) cls->cache.setBit(FAST_CACHE_META);
#endif

    // Choose an index for this class.
    // Sets cls->instancesRequireRawIsa if indexes no more indexes are available
    cls->chooseClassArrayIndex();

    if (PrintConnecting) {
        _objc_inform("CLASS: realizing class '%s'%s %p %p #%u %s%s",
                     cls->nameForLogging(), isMeta ? " (meta)" : "", 
                     (void*)cls, ro, cls->classArrayIndex(),
                     cls->isSwiftStable() ? "(swift)" : "",
                     cls->isSwiftLegacy() ? "(pre-stable swift)" : "");
    }

    // Realize superclass and metaclass, if they aren't already.
    //实现超类和元类(如果尚未实现)。
    // This needs to be done after RW_REALIZED is set above, for root classes.
    //对于根类,需要在上面设置了RW_REALIZED之后执行此操作。
    // This needs to be done after class index is chosen, for root metaclasses.
    //对于根元类,需要在选择类索引之后执行此操作。
    // This assumes that none of those classes have Swift contents,
    //   or that Swift's initializers have already been called.
    //   fixme that assumption will be wrong if we add support
    //   for ObjC subclasses of Swift classes.
    // 递归调用 realizeClassWithoutSwift ,实现父类和元类
    supercls = realizeClassWithoutSwift(remapClass(cls->superclass), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

#if SUPPORT_NONPOINTER_ISA
    if (isMeta) { // 如果是元类,对isa处理
        // Metaclasses do not need any features from non pointer ISA
        // This allows for a faspath for classes in objc_retain/objc_release.
        cls->setInstancesRequireRawIsa();
    } else { // 不是元类,也是对isa处理
        // Disable non-pointer isa for some classes and/or platforms.
        // Set instancesRequireRawIsa.
        bool instancesRequireRawIsa = cls->instancesRequireRawIsa();
        bool rawIsaIsInherited = false;
        static bool hackedDispatch = false;

        if (DisableNonpointerIsa) {
            // Non-pointer isa disabled by environment or app SDK version
            instancesRequireRawIsa = true;
        }
        else if (!hackedDispatch  &&  0 == strcmp(ro->name, "OS_object"))
        {
            // hack for libdispatch et al - isa also acts as vtable pointer
            hackedDispatch = true;
            instancesRequireRawIsa = true;
        }
        else if (supercls  &&  supercls->superclass  &&
                 supercls->instancesRequireRawIsa())
        {
            // This is also propagated by addSubclass()
            // but nonpointer isa setup needs it earlier.
            // Special case: instancesRequireRawIsa does not propagate
            // from root class to root metaclass
            instancesRequireRawIsa = true;
            rawIsaIsInherited = true;
        }

        if (instancesRequireRawIsa) {
            cls->setInstancesRequireRawIsaRecursively(rawIsaIsInherited);
        }
    }
// SUPPORT_NONPOINTER_ISA
#endif

    // Update superclass and metaclass in case of remapping
    // 确定继承链,赋值父类和元类
    cls->superclass = supercls;
    cls->initClassIsa(metacls);

    // Reconcile instance variable offsets / layout.
     // 协调实例变量的偏移量/布局。
    // This may reallocate class_ro_t, updating our ro variable.
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Set fastInstanceSize if it wasn't set already.
    // 经过上一步,再次协调属性对齐后,设置实例大小
    cls->setInstanceSize(ro->instanceSize);

    // Copy some flags from ro to rw
    // 赋值一些 ro 中的 flags标识位 到 rw
    if (ro->flags & RO_HAS_CXX_STRUCTORS) {
        cls->setHasCxxDtor();
        if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
            cls->setHasCxxCtor();
        }
    }
    
    // Propagate the associated objects forbidden flag from ro or from
    // the superclass.
    // 从ro或父类传播关联的对象禁止标志。
    if ((ro->flags & RO_FORBIDS_ASSOCIATED_OBJECTS) ||
        (supercls && supercls->forbidsAssociatedObjects()))
    {
        rw->flags |= RW_FORBIDS_ASSOCIATED_OBJECTS;
    }

    // Connect this class to its superclass's subclass lists
    // 添加当前类到父类的子类列表中,如果没有父类,设置自己就是根类
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    // Attach categories
    // 附加分类
    methodizeClass(cls, previously);

    return cls;
}

可以看到最后也是执行了一个methodizeClass函数来向主类附加分类,methodizeClass源码如下:

c 复制代码
//methodizeClass函数用于附加分类
static void methodizeClass(Class cls, Class previously)
{
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro(); // 读取ro数据
    auto rwe = rw->ext(); // 读取ext,赋值给rwe

    // Methodizing for the first time
    if (PrintConnecting) {
        _objc_inform("CLASS: methodizing class '%s' %s", 
                     cls->nameForLogging(), isMeta ? "(meta)" : "");
    }

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods(); // 获取ro中的方法列表
    if (list) {
        // 对方法列表list重新排序
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        if (rwe) rwe->methods.attachLists(&list, 1); // 如果有rwe,添加方法列表list到rwe的methodsList
    }

    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);
    }

    // Root classes get bonus method implementations if they don't have 
    // them already. These apply before category replacements.
    if (cls->isRootMetaclass()) {
        // root metaclass 根元类添加initialize方法
        addMethod(cls, @selector(initialize), (IMP)&objc_noop_imp, "", NO);
    }

    // Attach categories. 附加分类
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);

#if DEBUG
    // Debug: sanity-check all SELs; log method list contents
    for (const auto& meth : rw->methods()) {
        if (PrintConnecting) {
            _objc_inform("METHOD %c[%s %s]", isMeta ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(meth.name));
        }
        ASSERT(sel_registerName(sel_getName(meth.name)) == meth.name); 
    }
#endif
}

还有我们发现load_images里面的确调用了loadAllCategories函数,接着我们再来看一下loadAllCategories的实现:

loadAllCategories

c 复制代码
static void loadAllCategories() {
    mutex_locker_t lock(runtimeLock);

    for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        //调用load_categories_nolock来加载分类
        load_categories_nolock(hi);
    }
}

接着我们来看load_categories_nolock函数的实现(这个方法在load_images中和map_images两个流程中都会有调用到,其中执行的内容各稍有不同):

load_categories_nolock

c 复制代码
static void load_categories_nolock(header_info *hi) {
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    size_t count;
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            locstamped_category_t lc{cat, hi};

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Ignore the category.
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class",
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category.
            if (cls->isStubClass()) {
                // Stub classes are never realized. Stub classes
                // don't know their metaclass until they're
                // initialized, so we have to add categories with
                // class methods or properties to the stub itself.
                // methodizeClass() will find them and add them to
                // the metaclass as appropriate.
                if (cat->instanceMethods ||
                    cat->protocols ||
                    cat->instanceProperties ||
                    cat->classMethods ||
                    cat->protocols ||
                    (hasClassProperties && cat->_classProperties))
                {
                    objc::unattachedCategories.addForClass(lc, cls);
                }
            } else {
                // First, register the category with its target class.
                // Then, rebuild the class's method lists (etc) if
                // the class is realized.
                //首先,将类别注册到它的目标类。
				//如果那个类已经实现就重建它的方法列表
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) {
                    	//一般是map_images中调用load_categories_nolock函数时cls都会实现的,所以会走这个方法去将分类中的内容粘贴到主类中,但是现版本中map_images的注释中说到将分类添加主类的操作延迟到了第一次load_images执行时
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    //而如果是load_images中调用的load_categories_nolock函数的话,一般cls都没实现,就会走下面的else里的方法,将分类添加到unattachedCategories(未向主类粘贴内容的分类表)表中
                    } else {
                        //这个表就是分类表
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }

                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) {
                    	//一般是map_images中调用load_categories_nolock函数时元类也都会实现的,所以会走这个方法去将分类中的内容粘贴到主元类中
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    //而如果是load_images中调用的load_categories_nolock函数的话,一般其元类都没实现,就会走下面的else里的方法,将分类添加到unattachedCategories(未向主元类类粘贴内容的分类表)表中
                    } else {
                    // runtime_init的时候创建,第一部分有讲到
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };

    processCatlist(hi->catlist(&count));
    processCatlist(hi->catlist2(&count));
}
  • 获取category列表list
  • 遍历category list中的每一个category
  • 获取category的对应的主类cls,如果没有cls就跳过(continue)这个继续获取下一个
    (分类对应的主类是类对象时)如果其有对应的主类,并其有实例方法、协议、属性,则调用objc::unattachedCategories.addForClassload_images中执行到这里时调用objc::unattachedCategories.addForClass来将分类注册到它对应的主类里面去,方便后续重建类的方法列表,如果是map_images中执行到这里时会调用attachCategories。这两种情况调用分支不同的原因就是在load_images中调用到的时候分类对应的主类并没有被实现,那些主类在load_images后续获取分类的load方法时才被实现,导致map_images执行到那里的时候分类对应的主类已经被实现了,所以if就走了不同的分支)
  • (分类对应的主类是元类对象时)如果其有对应的主类,并其有类方法、协议,则调用objc::unattachedCategories.addForClassload_images中执行到这里时调用objc::unattachedCategories.addForClass来将分类注册到它对应的主类里面去,方便后续重建类的方法列表,如果是map_images中执行到这里时会调用attachCategories

这里肯定会疑惑为什么load_images中先将分类添加到unattachedCategories中,再将实现主类将分类内容添加到主类,map_images中最后调用的read_images中也是先将分类添加到unattachedCategories中,再将实现主类将分类内容添加到主类,其实从map_images处理分类那段代码的上方注释就可以理解,我们的load_images中走的添加分类到主类处理的都是非懒加载的分类,而read_images中走的添加分类到主类处理的都是懒加载的分类,而且都是清一色的先将分类添加到unattachedCategories中与主类产生关联并存放分类到内存中,然后再等到后面调用realizeClassWithoutSwift函数实现(初始化)主类的时候,再调用methodizeClass实现的具体的分类内容添加到主类

简言之就是加载分类有两个路径,一个是处理非懒加载分类的load_images中的路径,一个是处理懒加载分类的map_imagesread_images处理的路径。

整个map_images的流程大致如下图:

有趣的是,上面的第10步,初始化懒加载类,实际上也是调用我们上方说的realizeClassWithoutSwift进行的。

另外,对于懒加载类与非懒加载类的区别是:当前类是否实现 load 方法,实现了load方法就是非懒加载类,反之亦然,还有,懒加载类的数据加载推迟到第一次接收到消息的时候才开始加载,非懒加载类在map_images执行中就加载了所有类的数据

然后我们言归正传,回到load_categories_nolock函数上,其整个函数的流程其实是将所有分类添加到runtime_init中初始化的unattachedCategories表中或者调用attachCategories直接向主类中添加分类中的内容,注意这里的unattachedCategories表,意思是未将分类内容粘贴到主类的那些分类的表,说明后面就需要进行attachCategories操作来向主类粘贴。

然后我们来看一下unattachedCategories表:

c 复制代码
class UnattachedCategories : public ExplicitInitDenseMap<Class, category_list>
{
public:
	//将分类和主类关联起来
    void addForClass(locstamped_category_t lc, Class cls)
    {
        runtimeLock.assertLocked();

        if (slowpath(PrintConnecting)) {
            _objc_inform("CLASS: found category %c%s(%s)",
                         cls->isMetaClassMaybeUnrealized() ? '+' : '-',
                         cls->nameForLogging(), lc.cat->name);
        }

        auto result = get().try_emplace(cls, lc);
        if (!result.second) {
            result.first->second.append(lc);
        }
    }
	//这个是向本类粘贴分类内容的方法
    void attachToClass(Class cls, Class previously, int flags)
    {
        runtimeLock.assertLocked();
        ASSERT((flags & ATTACH_CLASS) ||
               (flags & ATTACH_METACLASS) ||
               (flags & ATTACH_CLASS_AND_METACLASS));

        auto &map = get();
        auto it = map.find(previously);

        if (it != map.end()) {
            category_list &list = it->second;
            if (flags & ATTACH_CLASS_AND_METACLASS) {
                int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
                //可以看到调用了attachCategories加载分类
                attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
                attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
            } else {
                attachCategories(cls, list.array(), list.count(), flags);
            }
            map.erase(it);
        }
    }

    void eraseCategoryForClass(category_t *cat, Class cls)
    {
        runtimeLock.assertLocked();

        auto &map = get();
        auto it = map.find(cls);
        if (it != map.end()) {
            category_list &list = it->second;
            list.erase(cat);
            if (list.count() == 0) {
                map.erase(it);
            }
        }
    }

    void eraseClass(Class cls)
    {
        runtimeLock.assertLocked();

        get().erase(cls);
    }
};

其中的addForClass其实就是将分类加载到内存的,里面我们发现有一个try_emplace方法,其代码如下:

cpp 复制代码
  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

这个创建一个存储桶的结构(这是一个键值对形式的结构),向里面存内容,结合上面的函数调用get().try_emplace(cls, lc)得知,以clskeylcvalue进行存储。

接着我们来看一下向主类中添加分类中内容的attachCategories函数的源码:

cpp 复制代码
// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order, 
// oldest categories first.
//将方法列表、属性和协议从类别附加到一个类。
//假设猫的类别都是加载的,并按加载顺序排序,
//最古老的类别先开始。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    if (slowpath(PrintReplacedMethods)) {
        printReplacements(cls, cats_list, cats_count);
    }
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
                     cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
                     cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
    }

    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids 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.
     */
     /*
	*只有少数类在启动时拥有超过64个类别。
	*这使用了一个小堆栈,并避免了malloc。
	*
	*类别必须以正确的顺序添加,这是回来
	*前面。为了使用分块实现这一点,我们需要迭代cats_list
	*从前面到后面,向后建立本地缓冲区,
	并在区块上调用attachLists。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;// 记录是否是从 bundle 中取的
    bool isMeta = (flags & ATTACH_METACLASS);
    //取出当前类 cls 的 class_rwe_t 数据
    auto rwe = cls->data()->extAllocIfNeeded();
	
	//遍历分类
    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) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;// 将方法列表放入 mlists 方法列表数组中
            fromBundle |= entry.hi->isBundle();// 分类的头部信息中存储了是否是 bundle,将其记住
        }

		// 取出分类中的属性列表,如果是元类,取得的是 nil
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            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) {
    	// 存储方法、属性、协议数组到 rwe 中
    	// 准备方法列表 mlists 中的方法【为什么需要准备方法列表这一步?】
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        // 将新方法列表添加到 rwe 中的方法列表中
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
        	// 清除 cls 的缓存列表
            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);
}
  • 先创建方法列表、属性列表、协议列表的新列表并且给它们分配内存,然后存储该cls所有的分类的方法、属性、协议,然后转交给了attachLists方法(就是后面的那几行代码)

为什么需要准备方法列表这一步呢?

方法的查找算法是通过二分查找算法,说明sel-imp是有排序的,那么是如何排序的呢?

perpareMethodLists中主要调用了fixup方法

fixupMethodList 方法中会遍历 mlist,把 sel 中的名字跟地址设置到 meth,然后根据地址对 mlist 进行重新排序

这也就意味着 remethodizeClass方法中实现类中方法(协议等)的序列化

  • attachLists方法保证其添加到列表的前面:
cpp 复制代码
    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;
            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();
        }
    }

具体的保证新添加的数组在大数组前面的实现上方代码已经体现地十分清晰了,就是对数组元素的简单插入,先将原来的元素后移我们需要新添加的元素的数量,然后将需要新添加的元素从下标0开始依次插入就实现了新添加的在前面。

下图很生动地表现了上述三种插入情况:

然后我们又回到load_images中,可以看到其中还调用了prepare_load_methods函数和call_load_methods函数,一个是用来 找到 所有非懒加载类和非懒加载分类的load方法的函数,一个是调用load方法进行最后 加载类和分类 的函数。

接下来我们先来看一下prepare_load_methods的实现:

调用load方法准备 (prepare_load_methods)

cpp 复制代码
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;\
runtimeLock.assertLocked();

//获取所有非懒加载类
classref_t const *classlist = 
    _getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
    schedule_class_load(remapClass(classlist[i]));
}
//获取所有非懒加载分类
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
    category_t *cat = categorylist[i];
    Class cls = remapClass(cat->cls);
    if (!cls) continue;  // category for ignored weak-linked class
    //swift没有load方法
    if (cls->isSwiftStable()) {
        _objc_fatal("Swift class extensions and categories on Swift "
                    "classes are not allowed to have +load methods");
    }
    //实现类
    realizeClassWithoutSwift(cls, nil);
    ASSERT(cls->ISA()->isRealized());
    add_category_to_loadable_list(cat);
}

调用load方法

cpp 复制代码
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;
    //加锁:线程安全
    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE  加载分类
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

可以是调用了call_category_loads();函数里面调用了分类的load方法对其进行了最后分类的加载。

上面一大堆过程肯定看的迷迷糊糊,不妨按照下方总结的精简过程看上方的具体实现,会好看很多。

总结分类的加载

首先分类加载分为两种情况:非懒加载分类和懒加载分类,所以分类就有两条加载流程。

先讲非懒加载分类的加载流程:

  1. 进入load_images,执行loadAllCategories,在loadAllCategories中调用了load_categories_nolock,再到load_categories_nolock中调用了addForClass
    (流程就是:load_images --> loadAllCategories --> load_categories_nolock --> objc::unattachedCategories.addForClass

此时的状态是 : 分类对应的主类都还没有实现(没有被初始化),我们只是调用了load_categories_nolock中的objc::unattachedCategories.addForClass分支将分类和主类关联并将分类加载进了内存。

  1. 执行完loadAllCategories之后,我们回到load_images中执行后面的内容,接着需要执行的是:prepare_load_methods方法获取类和分类的load方法,在其中调用了realizeClassWithoutSwift方法来实现(初始化)分类所对应的主类,在其中又调用了methodizeClass方法向主类中附加分类,在这之中又调用了objc::unattachedCategories.attachToClass,在这里面又调用了attachCategories来正式向主类中添加分类中的内容
    (流程就是: prepare_load_methods --> realizeClassWithoutSwift --> methodizeClass --> objc::unattachedCategories.attachToClass --> attachCategories

此时的状态是: 分类对应的主类已经实现,并已经将分类中的内容添加到了主类当中

  1. 执行完prepare_load_methods之后,我们又回到load_images中执行后面的内容,接着需要执行的是:call_load_methods方法用来调用所有的类和分类的load方法,让这些非懒加载的类和分类正式加载到程序中去。
    (最后的流程就是调用了: call_load_methods

此时的状态是: 非懒加载类和其分类加载完毕

再讲懒加载分类的加载流程

  1. 进入map_images,执行map_images_nolock,再执行其中的_read_images_read_images执行到与分类相关的部分是一个判断,判断是否执行过一次load_images,如果执行过load_images的话那个判断的参数didInitialAttachCategories的值就会是YES,然后就可以执行那个if中的代码,那些代码是循环调用load_categories_nolock,对于懒加载的分类,它们对应的懒加载主类还没有实现,所以又会调用load_categories_nolock中的objc::unattachedCategories.addForClass分支将分类和主类关联并将分类加载进了内存
    (流程就是: map_images --> map_images_nolock --> _read_images --> load_categories_nolock --> objc::unattachedCategories.addForClass

此时的状态 : 懒加载分类对应的主类还没有实现(初始化),我们只是调用了load_categories_nolock中的objc::unattachedCategories.addForClass分支将分类和主类关联并将分类加载进了内存。

  1. 循环执行完load_categories_nolock之后,我们又回到_read_images之中,接下来需要执行的是非懒加载类的实现(初始化),我们会调用到realizeClassWithoutSwift方法,不过由于我们在load_images当中已经调用过realizeClassWithoutSwift方法并实现了非懒加载类,所以这次刚刚进入realizeClassWithoutSwift就会返回nil而不执行任何操作。接着我们继续在_read_images中执行后续的代码,我们现在需要执行的是对懒加载类的实现(初始化),我们依然调用的是realizeClassWithoutSwift对懒加载类进行实现,并在其中调用了methodizeClass方法向主类中附加分类,在这之中又调用了objc::unattachedCategories.attachToClass,在这里面又调用了attachCategories来正式向主类中添加分类中的内容
    (流程就是:realizeClassWithoutSwift --> realizeClassWithoutSwift --> methodizeClass --> objc::unattachedCategories.attachToClass --> attachCategories

此时的状态: 懒加载分类的主类已经得到了实现(初始化),且懒加载分类中的内容已经添加到了懒加载主类之中。

以上就是本人对于整个分类的加载的总结叙述,如有问题望大家指正。

-- 未完

相关推荐
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
Stark-C3 小时前
万物皆可Docker,在NAS上一键部署最新苹果MacOS 15系统
macos·docker·策略模式
Roc.Chang3 小时前
macos 使用 nvm 管理 node 并自定义安装目录
macos·node.js·nvm
三劫散仙7 小时前
Mac vscode 激活列编辑模式
macos
程序猿看视界9 小时前
如何在 UniApp 中实现 iOS 版本更新检测
ios·uniapp·版本更新
endingCode11 小时前
45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题
javascript·macos·typescript
dr李四维12 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神12 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
soulteary13 小时前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
小江村儿的文杰1 天前
XCode Build时遇到 .entitlements could not be opened 的问题
ide·macos·ue4·xcode