IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> [iOS开发]Category和Extension -> 正文阅读

[移动开发][iOS开发]Category和Extension

什么是Category?

  • Category是Objective-C 2.0之后添加的语言特性,分类、类别其实都是指的Category。
  • Category的主要作用是为已经存在的类添加方法。也可以说是将庞大的类代码按逻辑划入几个分区。
  • 分类的特性是可以在运行时阶段动态的为已有的类添加新行为
  • Objective-C 中Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

分类和扩展

  • 扩展(Extension)有时候被称为匿名分类。但是两者实质上不是一个内容。
  • 扩展是在编译阶段与该类同时编译的,是类的一部分。扩展中声明的方法只能在该类的@implementation中实现。所以这也就意味着我们无法对系统的类使用扩展。
  • 同时与分类不同,扩展不但可以声明方法,还可以声明成员变量,这是分类所做不到的。

Category的实质

Category结构体

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++文件来了解一下

我们先搞个分类

@interface NSObject (testCategory) <TestProtocol>

@property (nonatomic, copy) NSString *personName;

- (void)test;

+ (void)secondTest;

@end

声明一个实例方法、一个类方法、一个属性
分类遵循一个协议 协议里也是一个类方法和对象方法

@protocol TestProtocol <NSObject>

- (void)protocolMethod;

+ (void)protocolClassMethod;

@end

clang -rewrite-objc NSObject+testCategory.m转成C++

【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_NSObject_$_testCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"NSObject",
	0, // &OBJC_CLASS_$_NSObject,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_testCategory,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_testCategory,
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_NSObject_$_testCategory,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_testCategory,
};

【结构体数组】
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_NSObject_$_testCategory,
};
  • 我们可以看到重点的三个元素
    • category结构体
    • category结构体的赋值语句
    • category结构体数组

对象方法列表结构体

【对象方法的实现】
static void _I_NSObject_testCategory_test(NSObject * self, SEL _cmd) {
    printf("正在打印test方法");
}
static void _I_NSObject_testCategory_protocolMethod(NSObject * self, SEL _cmd) {
    printf("正在打印协议对象方法");
}

【对象方法列表结构体】
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_NSObject_$_testCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_NSObject_testCategory_test},
	{(struct objc_selector *)"protocolMethod", "v16@0:8", (void *)_I_NSObject_testCategory_protocolMethod}}
};
  • - (void)test- (void)protocolMethod方法的实现
  • 对象方法结构体列表结构体

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

类方法列表结构体

【类方法的实现】
static void _C_NSObject_testCategory_secondTest(Class self, SEL _cmd) {

    printf("正在打印secondTest方法");
}

static void _C_NSObject_testCategory_protocolClassMethod(Class self, SEL _cmd) {
    printf("正在打印类对象方法");
}

【类方法列表结构体】
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_NSObject_$_testCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"secondTest", "v16@0:8", (void *)_C_NSObject_testCategory_secondTest},
	{(struct objc_selector *)"protocolClassMethod", "v16@0:8", (void *)_C_NSObject_testCategory_protocolClassMethod}}
};
  • + (void)secondTest+protocolClassMethod类方法的实现
  • 类方法列表结构体

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

协议列表结构体

【协议结构体】
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];//为什么是1?
} _OBJC_PROTOCOL_REFS_TestProtocol __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_TestProtocol __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_TestProtocol __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"protocolClassMethod", "v16@0:8", 0}}
};

【结构体赋值】
struct _protocol_t _OBJC_PROTOCOL_TestProtocol __attribute__ ((used)) = {
	0,
	"TestProtocol",
	(const struct _protocol_list_t *)&_OBJC_PROTOCOL_REFS_TestProtocol,
	(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_TestProtocol,
	(const struct method_list_t *)&_OBJC_PROTOCOL_CLASS_METHODS_TestProtocol,
	0,
	0,
	0,
	sizeof(_protocol_t),
	0,
	(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_TestProtocol
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_TestProtocol = &_OBJC_PROTOCOL_TestProtocol;
  • 协议列表结构体
  • 协议列表 对象方法列表结构体
  • 协议列表 类方法列表结构体
  • 结构体赋值语句

属性列表结构体

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_NSObject_$_testCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"personName","T@\"NSString\",C,N"}}
};

从【属性列表结构体】源码中我们可以看到:只有person分类中添加的属性列表结构体_OBJC_$_PROP_LIST_NSObject_$_testCategory,没有成员变量结构体_ivar_list_t结构体。更没有对应的set/get方法相关的内容。
这也说明了Category中不能添加成员变量这一事实。

category总结

主要包含下面几部分内容

  1. _method_list_t 类型的【对象方法列表结构体】;
  2. _method_list_t 类型的【类方法列表结构体】;
  3. _protocol_list_t 类型的【协议列表结构体】;
  4. _prop_list_t 类型的【属性列表结构体】。

_category_t结构体中并不包含_ivar_list_t类型,也就是不包含【成员变量结构体】

分类在运行期做了什么

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

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

_objc_init

/***********************************************************************
* _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();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

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

_read_images

// Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
  • 获取category列表list
  • 遍历category list中的每一个category
  • 获取category的对应的主类cls,如果没有cls就跳过(continue)这个继续获取下一个
  • 如果其有对应的主类,并其有实例方法、协议、属性,则调用addUnattachedCategoryForClass,同时如果cls中有实现的话,进一步调用remethodizeClass方法
  • 如果其有对应的主类,并其有类方法、协议,则调用addUnattachedCategoryForClass,同时如果cls的元类有实现的话,就进一步调用remethodizeClass方法

4、5主要是分类对应的主类是元类对象还是类对象

看一下对应的addUnattachedCategoryForClass的方法

addUnattachedCategoryForClass

static void addUnattachedCategoryForClass(category_t *cat, Class cls, 
                                          header_info *catHeader)
{
    runtimeLock.assertWriting();

    // DO NOT use cat->cls! cls may be cat->cls->isa instead
    NXMapTable *cats = unattachedCategories();
    category_list *list;

    list = (category_list *)NXMapGet(cats, cls);
    if (!list) {
        list = (category_list *)
            calloc(sizeof(*list) + sizeof(list->list[0]), 1);
    } else {
        list = (category_list *)
            realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
    }
    list->list[list->count++] = (locstamped_category_t){cat, catHeader};
    NXMapInsert(cats, cls, list);
}
static NXMapTable *unattachedCategories(void)
{
    runtimeLock.assertWriting();
    //全局对象
    static NXMapTable *category_map = nil;
    if (category_map) return category_map;
    // fixme initial map size
    category_map = NXCreateMapTable(NXPtrValueMapPrototype, 16);
    return category_map;
}
  • 通过unattachedCategories()函数生成一个全局对象cats
  • 我们从这个单例对象中查找cls,获取一个category_list *list列表
  • 要是没有list指针。那么我们就生成一个category_list空间
  • 要是有list指针,我们就在该指针的基础上再分配处category_list大小的空间
  • 在这新分配好的空间,将这个catcatHeader写入
  • 将数据插入到cats中,key----->cls value------>list

这段代码对于我们来说,对分类的实现部分关系并不大,其仅仅是把类和category做一个关联映射,而remethodizeClass才是真正去处理添加事宜的功臣

remethodizeClass

static void remethodizeClass(Class cls)
{
    //分类数组
    category_list *cats;
    bool isMeta;

    runtimeLock.assertWriting();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

还是没有得到我们需要的信息,其核心是调用了attachCategories函数把我们的分类信息附加到该类中

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // 创建方法列表、属性列表、协议列表,用来存储分类的方法、属性、协议
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;           // 记录方法的数量
    int propcount = 0;        // 记录属性的数量
    int protocount = 0;       // 记录协议的数量
    int i = cats->count;      // 从分类数组最后开始遍历,保证先取的是最新的分类
    bool fromBundle = NO;     // 记录是否是从 bundle 中取的
    while (i--) { // 从后往前依次遍历
        auto& entry = cats->list[i];  // 取出当前分类
    
        // 取出分类中的方法列表。如果是元类,取得的是类方法列表;否则取得的是对象方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;            // 将方法列表放入 mlists 方法列表数组中
            fromBundle |= entry.hi->isBundle();  // 分类的头部信息中存储了是否是 bundle,将其记住
        }

        // 取出分类中的属性列表,如果是元类,取得的是 nil
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        // 取出分类中遵循的协议列表
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    上面部分的代码仅仅是将分类中的方法、属性、协议插入到各自对应的大数组中
    注意是从后往前加入的(为啥???)
    --------------

    // 取出当前类 cls 的 class_rw_t 数据
    auto rw = cls->data();

    // 存储方法、属性、协议数组到 rw 中【注意是rw哦】
    // 准备方法列表 mlists 中的方法【为什么需要准备方法列表这一步?】
   
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    // 将新方法列表添加到 rw 中的方法列表中
    rw->methods.attachLists(mlists, mcount);
    // 释放方法列表 mlists
    free(mlists);
    // 清除 cls 的缓存列表
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    // 将新属性列表添加到 rw 中的属性列表中
    rw->properties.attachLists(proplists, propcount);
    // 释放属性列表
    free(proplists);

    // 将新协议列表添加到 rw 中的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    // 释放协议列表
    free(protolists);
}
  • 先创建方法列表、属性列表、协议列表的新列表并且给它们分配内存,然后存储该cls所有的分类的方法、属性、协议,然后转交给了attachMethodLists方法(就是后面的那些写在一起了)

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

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

perpareMethodLists中主要调用了fixup方法
在 fixupMethodList 方法中会遍历 mlist,把 sel 中的名字跟地址设置到 meth,然后根据地址对 mlist 进行重新排序。

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

  • 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;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            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;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }
  • array()->lists: 类对象原来的方法列表,属性列表,协议列表,比如Person中的那些方法等
  • addedLists:传入所有分类的方法列表,属性列表,协议列表,比如Person(Eat)、Person(Drink)中的那些方法等。

上面代码的作用就是通过memmove将原来的类找那个的方法、属性、协议列表分别进行后移,然后通过memcpy将传入的方法、属性、协议列表填充到开始的位置。

  • 分类的方法、属性、协议只是添加到原有类上,并没有将原有类的方法、属性、协议进行完全替换
    • 举个例子说明就是:假设原有类拥有 MethodA方法,分类也拥有 MethodA 方法,那> 么加载完分类之后,类的方法列表中会拥有两个 MethodA方法。
  • 分类的方法、属性、协议会添加到原有类的方法列表、属性列表、协议列表的最前面,而原有类的方法列表、属性列表、协议列表则被移动到了列表的后面
    • 因为运行时查找方法是顺着方法列表的顺序进行依次查找的,所以Category的方法会先被搜索到,然后直接指向,而原有类的方法则不被指向。这也是分类中的方法会覆盖掉原有类的方法最直接的原因。

在这里插入图片描述

分类的实现部分说起来很简单,不过源码确实有点恶心

load方法和initialize方法

load方法

我们知道,在类和category中都可以有+load方法,那么有两个问题:

1)、在类的+load方法调用的时候,我们可以调用category中声明的方法么?

2)、这么些个+load方法,调用顺序是咋样的呢?

在这里插入图片描述
并且在每个类中的load方法都添加了load方法

当在build Phases的Compile Sources中设置编译顺序如下时:
在这里插入图片描述
结果是
在这里插入图片描述

更换1和2的顺序
在这里插入图片描述
在这里插入图片描述

再当我们把主类放到这两个分类的编译顺序下面,是不是还会按照编译顺序运行?
在这里插入图片描述
可以看到并不是
在这里插入图片描述

带上子类呢?
在这里插入图片描述

在这里插入图片描述

对于load方法:

  • 先调用类的load方法
    • 按照编译先后顺序调用
    • 调用子类的load之前会先调用父类的load
  • 再调用分类的load
    • 按照编译的先后顺序调用

对于“覆盖”的方法,会找到最后一个编译的方法,和我们上面理解的分类实现的方法一样

initialize

不带子类

initialize是在类的方法第一次被调用时执行
覆盖类中的方法 只执行一次
分类2的test在前面,先调用分类2,所以分类2的initialize方法先执行
在这里插入图片描述

带上子类
在这里插入图片描述
调用子类的分类的test方法,可以看到创建了两遍父类的initialize方法
为啥呢???
在这里插入图片描述

因为我在initialize方法中写了[super initialize],由于是系统自动调用,也不需要再调用 [super initialize] ,否则父类的initialize会被多次执行
在这里插入图片描述

假如写了[super initialize]这时,系统就会再回到父类,再打印一遍。所以会打印两遍
请添加图片描述

关联对象

虽然在分类中可以写@property添加属性,但是不会自动生成私有属性,也不会生成set,get方法的实现,只会生成set,get的声明,需要我们自己去实现。

具体怎么实现,就涉及到关联对象了
请添加图片描述
就像这样

对于关联对象我们应该知道什么?
下次补充吧

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-14 14:11:29  更:2021-08-14 14:12:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 2:03:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码