ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

iOS底层原理(三)Category

2021-03-05 22:32:17  阅读:159  来源: 互联网

标签:Category load 调用 list iOS objc 底层 method cls


Category的本质

Category的底层结构

1.我们先给Person增加一个Person+Eat的分类

@interface Person (Eat) <NSCopying, NSCoding>

- (void)eat;

@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
@end

@implementation Person (Eat)

- (void)eat
{
    NSLog(@"eat");
}

- (void)eat1
{
    NSLog(@"eat1");
}

+ (void)eat2
{
    
}

+ (void)eat3
{
    
}

@end

2.然后通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Eat.m转换成Person+Eat.cpp文件,发现内部会生成一个_category_t类型的结构体

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; // 属性列表
};

3.我们还发现会生成一个_category_t结构体类型的变量,这个变量对应着该分类文件是Person+Eat,并且里面记录着所有的分类信息

// 变量名对应着分类文件名
static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"Person", // 类名
	0, // cls
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat, // 对象方法
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat, // 类方法
	(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat, // 协议列表
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat, // 属性列表
};

4._OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat这个变量里面记录着分类的对象方法eateat1

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_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_Eat_eat},
	{(struct objc_selector *)"eat1", "v16@0:8", (void *)_I_Person_Eat_eat1}}
};

5._OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat这个变量里面记录着分类的类方法eat2eat3

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_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	2,
	{{(struct objc_selector *)"eat2", "v16@0:8", (void *)_C_Person_Eat_eat2},
	{(struct objc_selector *)"eat3", "v16@0:8", (void *)_C_Person_Eat_eat3}}
};

6. _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat这个变量里面记录着NSCopyingNSCoding两个协议

static struct /*_protocol_list_t*/ {
	long protocol_count;  // Note, this is 32/64 bit
	struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	2,
	&_OBJC_PROTOCOL_NSCopying,
	&_OBJC_PROTOCOL_NSCoding
};

7._OBJC_$_PROP_LIST_Person_$_Eat这个变量里面记录着属性weightheight

static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	2,
	{{"weight","Ti,N"}, 
	{"height","Td,N"}}
	// Ti,N和Td,N对应着int和double两个类型
};

Category的加载处理过程

1.通过分析查找到objc-rumtime-new.mm文件里的attachCategories函数,将分类文件里的数据信息都附加到对应的类对象或者元类对象里,详细代码如下

// 附加上分类的核心操作
// cls:类对象或者元类对象,cats_list:分类列表
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)" : "");
    }

    // 先分配固定内存空间来存放方法列表、属性列表和协议列表
    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();

    for (uint32_t i = 0; i < cats_count; i++) {
        // 取出某个分类
        auto& entry = cats_list[i];

        // entry.cat就是category_t *cat
        // 根据isMeta属性取出每一个分类的类方法列表或者对象方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        
        // 如果有方法则添加mlist数组到mlists这个大的方法数组中
        // mlists是一个二维数组:[[method_t, method_t, ....], [method_t, method_t, ....]]
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
			// 将分类列表里先取出来的分类方法列表放到大数组mlists的最后面(ATTACH_BUFSIZ - ++mcount),所以最后编译的分类方法列表会放在整个方法列表大数组的最前面            
			mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 同上面一样取出的是分类中的属性列表proplist加到大数组proplists中
        // proplists是一个二维数组:[[property_t, property_t, ....], [property_t, property_t, ....]]
        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;
        }

        // 同上面一样取出的是分类中的协议列表protolist加到大数组protolists中
        // protolists是一个二维数组:[[protocol_ref_t, protocol_ref_t, ....], [protocol_ref_t, protocol_ref_t, ....]]
        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->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }

    // 将分类的所有属性附加到类对象的属性列表中
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

    // 将分类的所有协议附加到类对象的协议列表中
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

2.上述的每步操作都会调用attachLists方法来进行元素分配,详细代码如下

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

   if (hasArray()) {
       // 获取原本的个数
       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 { .... }
}

总结

  • 编译时
    • 每一个Category都会生成一个_category_t结构体对象,记录着所有的属性、方法和协议信息
  • 运行时
    • 通过Runtime加载某个类的所有Category数据
    • 把所有Category的方法、属性、协议数据,合并到一个大数组中,并且后面参与编译的Category数据,会在数组的前面
    • 将合并后的Category数据(方法、属性、协议),插入到类原来数据的前面

面试题

1.如果几个分类中都有同样的方法,会调用哪个,调用顺序是什么

  • 有分类会先调用分类的方法,如果多个分类都有相同的方法,那么会根据编译顺序来决定执行哪个分类的方法,后参与编译的分类方法会放到整个方法列表数组的最前面,到时调用会遍历所有的方法列表数组,先找到的分类方法列表先执行。
  • 在Xcode中查看编译顺序:Build Phases->Compile Sources
  • 分类里面相同的方法会覆盖类原本的方法这种说法是错误的,根本就没有覆盖,只是最先遍历找到哪个就执行哪个,不存在覆盖的概念

2. Class Extension和Category的实现是一样的吗

  • 不一样。
  • 类扩展只是将.h文件中的声明放到.m中作为私有来使用,编译时就已经合并到该类中了。
  • 分类中的声明都是公开的,而且是利用运行时机制在程序运行时将分类里的数据合并到类中

3.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

  • 有load方法
  • load方法在Runtime加载类、分类的时候调用,而且只调用一次
  • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

4.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

load方法

  • load方法会在Runtime加载类、分类时调用- 每个类、分类的load,在程序运行过程中只调用一次
源码分析

1.在objc-runtime-new.mmload_images方法可以发现准备处理load方法和调用load方法的函数

void load_images(const char *path __unused, const struct mach_header *mh) {
   	....
    
    {
        mutex_locker_t lock2(runtimeLock);
        // 准备load方法
        prepare_load_methods((const headerType *)mh);
    }

   	// 调用load方法
    call_load_methods();
}

2.在prepare_load_methods中发现,调用load方法前的类的处理和分类的处理

void prepare_load_methods(const headerType *mhdr) {
    size_t count, i;

    runtimeLock.assertLocked();
    
	// 按编译顺序拿到所有的类的list
    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);
        
      	....
 
	   // 按顺序直接添加
       add_category_to_loadable_list(cat);
    }
}

3.递归调用schedule_class_load方法来优先添加父类放到列表中,然后再添加当前类,所以执行调用时肯定先执行父类的load方法

static void schedule_class_load(Class cls) {
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // 递归调用,找到传进来的类的父类添加到列表中
    schedule_class_load(cls->getSuperclass());

   // 然后再调用当前传进来的类添加到列表中,所以父类肯定是在前面
   add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}

4.在objc-loadmethod.mmcall_load_methods方法可以发现,程序运行时会先调用类的load方法,然后调用分类的load方法,详细代码如下

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 *po ol = objc_autoreleasePoolPush();

    do {
        // 1. 优先调用类的load方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 只调用一次分类的load方法
        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;
}

5.找到每个类的load方法的内存地址,然后直接调用

static void call_class_loads(void) {
    int i;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        
        // 取出类里面的load方法
        // load_method_t:指向函数地址的指针
        // 这里的method对应的结构体为loadable_class
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        
        // 直接调用load方法
        (*load_method)(cls, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

// 找到的method对应的结构体
struct loadable_class {
    Class cls;  // may be nil
    IMP method; // 这个就是load方法
};

6.找到每个分类的load方法的内存地址,然后直接调用

static bool call_category_loads(void) {
    int i, shift;
    bool new_categories_added = NO;
    
    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        
        // 取出每一个分类里的load方法
        // 这里的method对应的结构体为loadable_category
        load_method_t load_method = (load_method_t)cats[i].method;
        Class cls;
        if (!cat) continue;

        cls = _category_getClass(cat);
        if (cls  &&  cls->isLoadable()) {
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s(%s) load]\n", 
                             cls->nameForLogging(), 
                             _category_getName(cat));
            }
            
            // 直接调用load方法
            (*load_method)(cls, @selector(load));
            cats[i].cat = nil;
        }
    }

   ....
 }
 
// 找到的method对应的结构体
struct loadable_category {
    Category cat;  // may be nil
    IMP method; // 这个方法就是load方法
};
调用顺序
  • 先调用类的load
    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的load之前会先调用父类的load
  • 再调用分类的load
    • 按照编译先后顺序调用(先编译,先调用)
load方法系统调用和主动调用的区别
  • 系统调用load方法调用是直接找到类和分类中的方法的内存地址直接调用
  • 主动调用load方法是通过消息机制来发送消息的,会在对应的消息列表里按顺序遍历一层层查找,找到就调用

initialize方法

initialize方法会在类第一次接收到消息时调用

源码分析

1.由于在调用到这个类的时候才会执行initialize方法,那么说明是在发消息过程中来执行的,我们在objc-runtime-new.mm中调用class_getInstanceMethod或者class_getClassMethod方法,详细代码如下

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

    return _class_getMethod(cls, sel);
}

2.一步步调用最后找到initializeNonMetaClass函数,先递归找到父类调用initialize方法,然后当前类调用initialize方法。可以在callInitialize里发现本质都是通过Runtime的消息机制进行的发送

void initializeNonMetaClass(Class cls) {
	ASSERT(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // 如果存在父类,并且没有初始化父类,就去初始化父类
    supercls = cls->getSuperclass();
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
  
    SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
        
        // 未初始化的类通过setInitializing做了标记,下次就不会再调用了
            cls->setInitializing();
            reallyInitialize = YES;

            // Grab a copy of the will-initialize funcs with the lock held.
            localWillInitializeFuncs.initFrom(willInitializeFuncs);
        }
    }
    
    ....
    
    {
       // 调用初始化
       callInitialize(cls);
       ....
       
   	}
   	
   	....
}


// 调用initialize的函数
void callInitialize(Class cls) {
    // 通过runtime消息机制发送initialize消息
    ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
    asm("");
}
调用顺序
  • 先调用父类的initialize,再调用子类的initialize
  • (先初始化父类,再初始化子类,每个类只会初始化1次)
initialize和load的区别
  • initialize是通过objc_msgSend进行调用的,而load是找到函数地址直接调用的
  • 如果子类没有实现initialize,会调用父类的initialize(所以父类的initialize可能会被调用多次)
  • 如果分类实现了initialize,就覆盖类本身的initialize调用

5.Category能否添加成员变量?如果可以,如何给Category添加成员变量?

不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果

实现方案

通过objc_setAssociatedObject设置关联对象来实现

@interface Person (Test)

@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) int weight;
@end

@implementation Person (Test)

- (void)setName:(NSString *)name
{
    /*
     * object:关联的对象
     * value:关联的值
     * objc_AssociationPolicy:关联策略
     */
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name
{
    // 隐式参数
    // _cmd == @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setWeight:(int)weight
{
    objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)weight
{
    // _cmd == @selector(weight)
    return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
实现源码分析

1.在objc-references.mm中我们可以看到objc_setAssociatedObject的实现代码如下

void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) {
    if (!object && !value) return;

    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));
    
    // 通过传进来的object生成一个key
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

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

    bool isFirstAssociation = false;
    {
        AssociationsManager manager;
        // 取出AssociationsManager里的AssociationsHashMap这个属性
        AssociationsHashMap &associations(manager.get());

        // 如果value有值
        if (value) {
            // 根据传进来的object key的值disguised取出ObjectAssociationMap
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            // 根据refs_result的key存放进association
            // association就是ObjcAssociation
            // 总之就是对应的每一个key一层层的赋值
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else { // 如果value为空
            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()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        // 也会找到associations进行擦除
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

	....
}

2.关联对象的取值函数objc_getAssociatedObject的实现代码如下

void _object_get_associative_reference(id object, const void *key) {
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        
        // associations是manager里面所有的AssociationsHashMap的list
        AssociationsHashMap &associations(manager.get());
        // 根据object在list里找到对应的那个AssociationsHashMap类型的i
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            
            // 再取出所有的ObjectAssociationMap的list
            ObjectAssociationMap &refs = i->second;
            // 根据key在list里找到对应的ObjectAssociationMap类型的j
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                // 取出ObjectAssociation类型的值association
                association = j->second;
                // 取出association里的value和策略
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

3.关联对象的几个类的实现关系可以用下图表示

总结:
  • 关联对象并不是存储在被关联对象本身内存中
  • 关联对象存储在全局的统一的一个AssociationsManager
  • 设置关联对象为nil,就相当于是移除关联对象
  • 当object对象被释放,关联对象的值也会对应的从内存中移除(内存管理自动做了处理)

标签:Category,load,调用,list,iOS,objc,底层,method,cls
来源: https://www.cnblogs.com/funkyRay/p/ios-di-ceng-yuan-li-sancategory.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有