iOS runtime原理以及KVO实现

前言

做开发做了好几年,前几年写iOS代码,去年写Web端的代码。然后发现学到的东西有限,了解的东西确实很多但是都不够深入。今年重新做iOS开发,痛定思痛,来研究或者说来熟悉下iOS中的各项技术。后面的博客可能会参照各个大佬的博客加上我自己的一些理解。

技术更新

后面更新的博客主要为iOS开发内容,当然有空的时候我会去研究下前端和后端以及数据库、大数据、AI相关的东西。
废话不多说,先看下这一篇博客要说的东西,可能内容有点多。


runtime

runtime版本

OC2.0 之后的版本为Modern版本:运行与64位系统中
OC2.0以前是 legacy版本:老的32位程序
runtime 是用c和汇编写的

原理

  • 消息传递机制

[obj method]被转化为objc_msgSend(obj,method)
1.通过obj的isa指针找到它的class
2.在class的methodList里面找到method
3.如果没找到,就到它的superclass里面去找
4.一旦找到这个函数,name就去执行这个函数的IMP

  • objc_cache

每次去找methodlist然后找一遍会出现效率低的问题
objc_class的objc_cache来实现对常用方法的缓存。
意思就是将常用的方法缓存下来,运行时,先去找cache中是否有方法,然后再重复上述步骤。
那么这个cache是怎么实现的呢,大致就是将一个key和函数的IMP存起来,然后在执行的时候找到key去执行IMP,然后算法更新这个方法是不是常用的。

  • 结构体

1.objc_object
isa就是这个结构体的指针

objc_object
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

2.objc_class

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

结构体说明:
isa -》class的isa指针
name - 》class 的名字
version - 》class 的版本
info - 》包含的信息
instance_size-》 实例大小
ivars-》属性列表
methodlists-》方法列表
cache -》方法cache
prtocols-》协议列表
3.obj_mehod

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

结构体说明:
method_name-》方法的名字
method_types-》方法的类型
method_imp-》方法的IMP

runtime应用

  • 给分类关联属性
  • 给类动态添加方法
  • 方法交换
  • 获取类的详细列表
  • 其他

1.给分类关联属性
给分类添加属性主要用到下面三个方法

  • 根据key设置关联对象的value
    objc_setAssociatedObject(id object, void * key, id value, <objc_AssociationPolicy policy);
  • 根据key取出关联对象的value
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
  • 移除所有关联对象
    objc_removeAssociatedObjects(id _Nonnull object);

代码
给ViewController添加一个分类,命名为Extension
创建的分类如下

image0.png

分类的.h代码:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIViewController (Extension)

@property(nonatomic, copy)NSString *contentView;

@end

NS_ASSUME_NONNULL_END

分类的.m代码

#import "UIViewController+Extension.h"
#import <objc/runtime.h>

static const void *key = &key;

@implementation UIViewController (Extension)

- (void)setContentView:(UIView *)contentView{
    
    objc_setAssociatedObject(self, &key, contentView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}


- (UIView *)contentView{
    return objc_getAssociatedObject(self, &key);
}
@end

在UIViewController中调用

self.contentView = @"hello";
NSLog(@"%@",self.contentView);

代码以及相关说明
这个key值一定要定义全局静态变量 一个key对应一个value
声明:static void *key = "key";
用法1:&key作为参数
用法2:写成@selector(value)作为参数,其中value就是你属性的名字,这样写也获取了一个静态无返回值的地址。

2.给类动态添加方法

1.直接给当前类动态添加一个方法
传过来的方法是不是想要的方法,如果是就添加
+(BOOL)resolveInstanceMethod:(SEL)sel;
给类添加一个方法,参数说明 cls:给那个类添加 name:方法名字 IMP:指针(可以根据IMP的定义定义一个IMP的指针)types:”v@:“(编码表示(v)无返回值 (@)对象 (:)选择器)
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types);
添加成功 return YES;

代码
在ViewController.m中

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(work)) {
        class_addMethod([self class], sel,(IMP)work,"v@:");
    }
    return NO;
}

void work(id objct, SEL sel){
    NSLog(@"i am working");
}

调用

[self performSelector:@selector(work)];

2.转给备用接收者
第一种方案:
-(id)forwardingTargetForSelector:(SEL)aSelector;
声明一个对象,return这个对象,让另外的对象去执行这个方法
代码:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
//    if (sel == @selector(work)) {
//        class_addMethod([self class], sel,(IMP)work,"v@:");
//    }
    return NO;
}
void work(id objct, SEL sel){
    NSLog(@"i am working");
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(work)) {
        Person *person = [Person new];
        return person;
    }
    return nil;
}

调用

[self performSelector:@selector(work)];

新建Person类,在Person类中添加一个work方法即可

第二种方案:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
通过签名发送给anInvocation
-(void)forwardInvocation:(NSInvocation *)anInvocation;
[anInvocation invokeWithTarget:target];
在上面的方法中如果 target类中有anInvocation.selector,name就交给target类去执行这个方法,否则就执行[self doesNotRecognizeSelector:sel];

上面也称之为方法重定向

代码

+ (BOOL)resolveInstanceMethod:(SEL)sel{
//    if (sel == @selector(work)) {
//        class_addMethod([self class], sel,(IMP)work,"v@:");
//    }
    return NO;
}
void work(id objct, SEL sel){
    NSLog(@"i am working");
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
//    if (aSelector == @selector(work)) {
//        Person *person = [Person new];
//        return person;
//    }
    return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(work)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [NSMethodSignature methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    Person *p = [Person new];
    if ([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }else{
        [self doesNotRecognizeSelector:sel];
    }
}

调用

[self performSelector:@selector(work)];

3.方法交换
1.直接方法交换
获取类的方法 返回值为Method
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
交换方法
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

2.截取系统方法交换
在+(void)load{}中截取系统的方法并且交换 例如不想用系统的某个方法 需要自己实现,那么可以分类然后在分类中自定义一个方法 并且替换系统的方法。单例执行一次。
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

代码

- (void)exchageMethod{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method m1 = class_getInstanceMethod([self class], @selector(printA));
        Method m2 = class_getInstanceMethod([self class], @selector(printB));
        method_exchangeImplementations(m1, m2);
    });
}

- (void)printA{
    NSLog(@"A");
}
- (void)printB{
    NSLog(@"B");
}

调用

[self exchageMethod];
[self printA];

结果会打印“B”;

4.获取类各种详细列表

  • 获取属性名字
    代码:
- (void)obtainPropertyName{
   unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList([Person class], &count);
    for (NSInteger i = 0; i<count; i++) {
        NSString *name = @(property_getName(propertyList[i]));
        NSString *attribute = @(property_getAttributes(propertyList[i]));
        NSLog(@"%@ %@",name,attribute);
    }
}
  • 获取成员变量名字
- (void)obtainIvars{
    unsigned int outCount = 0;
    Ivar *ivarList = class_copyIvarList([Person class], &outCount);
    for (NSInteger i = 0; i < outCount; i++) {
        NSLog(@"%@",@(ivar_getName(ivarList[i])));
    }  
}
  • 获取方法名字
- (void)obtainMthods{
    unsigned int outCount = 0;
    Method *methods = class_copyMethodList([Person class], &outCount);
    for (NSInteger i = 0; i < outCount; i++) {
        NSLog(@"%@", NSStringFromSelector(method_getName(methods[i])));
    }
}
  • 获取遵守的协议名字
- (void)obtainProtocols{
    unsigned int outCount = 0;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([Person class], &outCount);
    for (NSInteger i = 0; i < outCount; i++) {
        Protocol *pro = protocolList[i];
        NSLog(@"%@", @(protocol_getName(pro)));
    }
}

当然你如果要获取协议名字,那么你首先应该在这个类中遵守某个协议。

5.其他

  • 字典转模型
    代码(这个代码只是自己简单的实现,具体的字典转模型还需要考虑很多,以后有机会单独再写一篇博客)
+ (instancetype)modelWithDic:(NSDictionary *)dictionary{
    unsigned int outCount = 0;
    Ivar *ivars =  class_copyIvarList([self class], &outCount);
    id p = [[self alloc]init];
    for (int i = 0 ; i < outCount ; i ++ ) {
        NSString *name = @(ivar_getName(ivars[i]));
        if ([dictionary objectForKey:[name substringFromIndex:1]]) {
            [p setValue:[dictionary objectForKey:[name substringFromIndex:1]] forKey:[name substringFromIndex:1]];
        }
    }
    return p;
}

调用:

NSDictionary *dic = @{@"name":@"zhaoqian",@"sex":@"man",@"age":@"15"};
Person * nep = [Person modelWithDic:dic];
NSLog(@"%@\n%@\n%ld\n",nep.name,nep.sex,nep.age);
  • 同一方法高频率调
    代码:
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
  • 编码解码(只要重写这两个方法就行,当然也必须遵守NSCoding协议)
    代码:
- (void)encodeWithCoder:(nonnull NSCoder *)aCoder {
    unsigned int outCout = 0;
    Ivar *ivars = class_copyIvarList([self class], &outCout);
    for (int i = 0 ; i < outCout; i++) {
        NSString *name = @(ivar_getName(ivars[i]));
        [aCoder encodeObject:[self valueForKey:name] forKey:name];
    }
    free(ivars);
}

- (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        unsigned int outCout = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCout);
        for (int i = 0 ; i < outCout; i++) {
            NSString *name = @(ivar_getName(ivars[i]));
            [aDecoder decodeObjectForKey:name];
        }
        free(ivars);
    }
    return self;
}

KVO

KVO 一对一
NSNotificationCenter 一对多
KVO可以监听单个属性的变化,也可以监听集合对象的变化

KVO原理

实际上是创建了一个 NSKVONotifying_XXX的子类,然后用runtime的isa-swizzling技术实现。

KVO实现

1.简单理解的kvo
kvo意思就是当属性的值变化的时候,监听这个变化的值,并且得到监听结果。那么可以在某个类中重写该属性的setter方法,然后回调回来接收,但是我写的不能动态的实现监听 ,要在每个属性的setter方法中去实现回调,实现了简单的kvo。
代码
Person.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
typedef void(^callBack)(id);

@interface Person : NSObject

@property (nonatomic, copy)NSString *name;
@property (nonatomic, assign)NSInteger age;
@property (nonatomic, copy)callBack block;

- (void)addObserverForkeyPath:(NSString *)keyPath;


@end

NS_ASSUME_NONNULL_END

Person.m

#import "Person.h"
#import <objc/runtime.h>
@interface Person(){
    NSString *_name;
    NSInteger _age;
}
@property (nonatomic, strong)NSString *keyPath;
@end

@implementation Person


- (void)addObserverForkeyPath:(NSString *)keyPath{
    if (!self.keyPath) {
        return;
    }
    self.keyPath = keyPath;
}
- (void)setName:(NSString *)name{
    if (_name!=name) {
         _name = name;
        if (self.block) {
            self.block(name);
        }
    }
}

- (void)setAge:(NSInteger)age{
    if (_age!=age) {
        _age = age;
    }
    if (self.block) {
        self.block(@(age));
    }
}

调用:

Person *p = [Person new];
    [p addObserverForkeyPath:@"name"];
    p.block = ^(id info) {
        //TODO
 };

2.利用runtime的方法实现KVO
我将代码说明全部注释在代码中了,就直接贴代码了
创建一个NSObect的分类 分类中重新添加代码
NSObject+Extension.h

#import <Foundation/Foundation.h>


NS_ASSUME_NONNULL_BEGIN

typedef void(^ValueChangeBlock)(id obeserverObject, NSString *key, id oldValue, id newValue);

@interface NSObject (Extension)

// 添加kvo并回调
-(void)addObserver:(NSObject *)observer forKey:(NSString *)key options:(NSKeyValueObservingOptions)options block:(ValueChangeBlock)callBack;

// 移除kvo
- (void)removeObserver:(NSObject *)observer forKey:(NSString *)key;


@end

NS_ASSUME_NONNULL_END

NSObject+Extension.m

#import "NSObject+Extension.h"
#import <objc/runtime.h>
#import <objc/message.h>

static char const * kObservers= "ZHAOQIAN";

// ObserverInfo Model
@interface ObserverInfo : NSObject

// 监听的对象
@property (nonatomic, copy) NSString *observerName;
// 监听的健
@property (nonatomic, copy) NSString *key;

// 回调
@property (nonatomic, copy) ValueChangeBlock block;

@end

@implementation ObserverInfo

// 实例方法 
- (instancetype)initWithObserver:(NSString *)observerName key:(NSString *)key block:(ValueChangeBlock)block
{
    self = [super init];
    if (self) {
        _observerName = observerName;
        _key = key;
        _block = block;
    }
    return self;
}

@end

@implementation NSObject (Extension)


// 添加观察者
- (void)addObserver:(NSObject *)observer forKey:(NSString *)key options:(NSKeyValueObservingOptions)options block:(ValueChangeBlock)callBack{
    // 利用key 返回其setter方法的方法名,例如变为“setNmae”这样类型的
    NSString *str = private_setterForKey(key);
    // 根据方法名字获取方法
    Method setterMethod = class_getInstanceMethod([self class], NSSelectorFromString(str));
    // 获取父类的类名
    NSString *oldClassName = NSStringFromClass([self class]);
    // 创建子类的类名
    NSString *kvoClassName = [@"Notify_" stringByAppendingString:oldClassName];
    
    // 创建子类
    Class kvoClass;
    kvoClass = objc_lookUpClass(kvoClassName.UTF8String);
    if (!kvoClass) {
        kvoClass = objc_allocateClassPair([self class], kvoClassName.UTF8String, 0);
        objc_registerClassPair(kvoClass);
    }
    
    // 在子类中重新实现setter方法
    if (setterMethod) {
        class_addMethod(kvoClass, NSSelectorFromString(str), (IMP)setterIMP, "v@:@");
    }else{
        // 如果没有setter方法,那么方法交换下
        Method method1 = class_getInstanceMethod(self.class, @selector(setValue:forKey:));
        Method method2 = class_getInstanceMethod(self.class, @selector(swizz_setValue:forKey:));
        method_exchangeImplementations(method1, method2);
    }
    
    // 将self设置为它的子类
    object_setClass(self, kvoClass);
    
    // 创建模型
    ObserverInfo *info = [[ObserverInfo alloc]initWithObserver:observer.description key:key block:callBack];
    // 动态获取观察者属性
    NSMutableArray *observers = objc_getAssociatedObject(self, kObservers);
    if (!observers) {
        observers = [NSMutableArray array];
        // 添加观察者属性
        objc_setAssociatedObject(self, kObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

// 移除观察者
- (void)removeObserver:(NSObject *)observer forKey:(NSString *)key{
    NSMutableArray *observers = objc_getAssociatedObject(self, kObservers);
    ObserverInfo *info;
    for (ObserverInfo *temp in observers) {
        if ([temp.observerName isEqualToString:observer.description] && [temp.key isEqualToString:key]) {
            info = temp;
            break;
        }
    }
    if (info) {
        [observers removeObject:info];
    }else{
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%@ does not register observer for %@",observer.description, key] userInfo:nil];
    }
    
}

// 回调
- (void)swizz_setValue:(id)value forKey:(NSString *)key{
    id oldValue = [self valueForKey:key];
    [self swizz_setValue:value forKey:key];
    NSMutableArray *observers = objc_getAssociatedObject(self, kObservers);
    for (ObserverInfo *temp  in observers) {
        if ([temp.key isEqualToString:key]) {
            temp.block(self,key,oldValue,value);
        }
    }
}

// 指针交换的回调
void setterIMP(id self,SEL _cmd,id newValue){
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *temp = private_upperTolower([setterName substringFromIndex:@"set".length], 0);//去除set并将大写改成小写
    NSString *key = [temp substringToIndex:temp.length-1];//去除冒号
    id oldValue = [self valueForKey:key];
    // 父类结构体
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    // 方法发给父类
    ((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClazz, _cmd, newValue);
    // 回调
    NSMutableArray *observers = objc_getAssociatedObject(self, kObservers);
    for (ObserverInfo *temp in observers) {
        if ([temp.key isEqualToString:key]) {
            temp.block(self, key, oldValue, newValue);
        }
    }
}

// 内联函数

// 返回方法名字,例如“setName”这样类型的
static inline NSString * private_setterForKey(NSString *key){
    key = private_lowerToUpper(key, 0);
    return [NSString stringWithFormat:@"set%@:",key];
}

// 将某个位置的字母小写转大写
static inline NSString * private_lowerToUpper(NSString *str,NSInteger location){
    NSRange range = NSMakeRange(location, 1);
    NSString *lowerLetter = [str substringWithRange:range];
    return [str stringByReplacingCharactersInRange:range withString:lowerLetter.uppercaseString];
}
// 将某个位置的字母大写转小写
static inline NSString * private_upperTolower(NSString *str,NSInteger location){
    NSRange range = NSMakeRange(location, 1);
    NSString *lowerLetter = [str substringWithRange:range];
    return [str stringByReplacingCharactersInRange:range withString:lowerLetter.lowercaseString];
}

@end

写在结尾

很多内容还是不够深入,后面如果有更深入的研究我会更新在这个博客中。
如果内容能帮到和我一样的同学,随手点个小星星也是不错的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容