Objective C开发性能优化iOSruntime精选

[翻译]理解Objective-C Runtime

2016-05-19  本文已影响382人  dalianer

原文地址:https://cocoasamurai.blogspot.jp/2010/01/understanding-objective-c-runtime.html
原文作者:Colin Wheeler

引子

当人们谈到Objective-C/Cocoa时,Objective-C Runtime是一个非常容易被忽略掉的特点,这大概是因为OC语言本身是一门可以在一小段时间就能入门的语言,学习Cocoa的新手们往往也只是在Cocoa框架和怎么使用框架上埋头钻研。可是,OC Runtime的工作原理是每一个学习OC的人都至少应该了解的,而并不是仅限于知道[target doMethodWith:var1];在经过编译之后变成了objc_msgSend(target,@selector(doMethodWith:),var1);这么表层的东西。理解了OC Runtime之后,有助于我们更深入的理解OC语言本身和应用的运行机制。在我看来,不论开发经验多或少,每一位Mac/iOS开发者都能从本文获取到一点新的知识。

OC Runtime开源项目

OC Runtime是一项开源项目,你可以从http://opensource.apple.comn 随时获得源代码。实际上,当初我刚开始去探究OC Runtime的工作机制时,相反,正是通过阅读它的源代码,而不是官方文档开始的。你也可以通过以下链接下载针对Mac OS X 10.6.2版本的Runtime源代码objc4-437.1.tar.gz

动态语言VS静态语言

OC是一门运行时才定向的语言,这意味着到底该由哪个对象来执行消息是在经过编译、链接之后,待运行时才会被确定下来。这种动态性给我们提供了巨大而又灵活的应用空间,我们可以利用这个特点来实现一些平常不容易做到的操作,例如到运行时再将消息转发给需要其处理的对象,甚至还能交换两个对象的实现方法。正是由于这种灵活的动态性,我们在使用Runtime时,应该仔细检查每个对象所能处理或者不能处理但能正确转发的消息。而与此对应的C语言的运行机制则是:从main()函数开始,之后从上到下,按照写好的代码逻辑,顺序执行我们构造的功能函数,而且C语言中的结构体无法将函数的调用进行转发,例如如下所示的一段C语言代码:

#include < stdio.h >
int main(int argc, const char **argv[])
{
        printf("Hello World!");
        return 0;
} 

在经过编译器编译之后,会变成如下的汇编代码:

.text
 .align 4,0x90
 .globl _main
_main:
Leh_func_begin1:
 pushq %rbp
Llabel1:
 movq %rsp, %rbp
Llabel2:
 subq $16, %rsp
Llabel3:
 movq %rsi, %rax
 movl %edi, %ecx
 movl %ecx, -8(%rbp)
 movq %rax, -16(%rbp)
 xorb %al, %al
 leaq LC(%rip), %rcx
 movq %rcx, %rdi
 call _printf
 movl $0, -4(%rbp)
 movl -4(%rbp), %eax
 addq $16, %rsp
 popq %rbp
 ret
Leh_func_end1:
 .cstring
LC:
 .asciz "Hello World!"

然后又经过链接,最终会生成一个可执行文件。这个过程和OC依赖OC Runtime库编译、链接程序的过程类似。例如下边一段OC代码:

[self doSomethingWithVar:var1];

会被编译成:

objc_msgSend(self,@selector(doSomethingWithVar:),var1);

除了这一点,我们对于OC的Runtime的工作机制完全不知道。

什么是OC Runtime

OC Runtime是一个Runtime库,它主要以C和汇编语言为基础,使用面向对象的OC来编写。这意味着,它可以加载类,也可以对消息进行转发、分发等。总而言之,OC Runtime为OC这门面向对象的语言提供了基础性的结构支持。

OC Runtime相关术语

在我们进行更深一步的探讨之前,先让我们共同梳理一下关于OC Runtime的术语。

1.Runtime

Mac/iOS开发人员关心的有2个运行时:现代运行时(Modern Runtime)和传统运行时(Legacy Runtime)。现代运行时涵盖了所有64位的Mac OS X和iPhone应用,而传统运行时则包括剩下的所有的32位Mac OS X应用。

2.方法(Methods)

包括两类基本的方法:对象方法(以"-"开头,像- (void)doFoo);类方法(以"+"开头,像+(id)alloc)。方法看起来和C语言中的函数很像,它们内部都是一段需要执行的语句,像下面这样:

-(NSString *)movieTitle
{
    return @"Futurama: Into the Wild Green Yonder";
}

3.选择器(selector)

OC中的选择器有点类似于C语言中的数据结构体,它扮演着确定需要执行的OC方法的角色。在Runtime中它的定义类似于下面这样:

typedef struct objc_selector *SEL;

使用时:

SEL aSel = @selector(movieTitle);

4.消息(Message)

[target getMovieTitleForObject:obj];

OC中的消息其实就是2个中括号中间的东西。它由接受消息的目标对象、需要执行的方法名和需要传入的参数三部分组成。OC消息类似于C语言的函数,但它们又不相同,给一个目标对象发送消息并不代表该对象就一定会执行这个方法,接受对象可以根据消息的发送者决定具体执行的方法,或者是该对象本身并不执行,而是将此消息转发给另一个对象去执行。

5.类(class)

在Runtime中,我们可以发现如下定义:

typedef struct objc_class *Class; 
typedef struct objc_object {
    Class isa; 
} *id;

这里面传达出几点信息。首先这段代码中分别有类的结构体定义和对象的结构体定义;其次对象的结构体中有一个类指针isa,也就是我们平常所说的“isa指针”。isa指针的存在是为了在运行时检查一个对象的父类,然后在其父类对应的类方法列表中查询可以响应消息的方法。最后,代码的结尾处还有一个id指针。id指针默认是告诉我们这个对象只是一个OC对象,系统可以通过查询id指针指向对象所属的类,进而查询到它能否对消息做出响应。当然,如果id指针指向的对象一经确定,我们可以进行更多的操作。

6.块(Blocks)

在LLVM/Clang文档中,关于blocks的介绍:

struct Block_literal_1 {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 {
 unsigned long int reserved; // NULL
     unsigned long int size;  // sizeof(struct Block_literal_1)
 // optional helper functions
     void (*copy_helper)(void *dst, void *src);
     void (*dispose_helper)(void *src); 
    } *descriptor;
    // imported variables
};

代码块(blocks)是用来与OC Runtime相配合的,它也可看作是OC对象,所以也能对retain、release、copy等消息做出响应。

6.IMP实现方法(Method Implementations)

typedef id (*IMP)(id self,SEL _cmd,...); 

IMP是编译器为我们生成的指向实现方法的指针。如果是刚开始接触OC语言,并不需要了解IMP,不过我们现在讨论的是Runtime,稍后就可以看到Runtime中是如何执行IMP的。

7.OC类(Objective-C Classes)

OC Classes中具体都有什么呢?一个最基本的类的实现就像下边这样:

@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end

但是在Runtime中,系统需要记录更多信息:

#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 

可以看到,一个OC类有对以下对象的引用:它的父类、它的名称、实例变量、方法、缓存以及协议。当一个OC类或一个OC对象需要对消息做出响应时,Runtime需要上述信息才能完成工作。

是类还是其本身定义一个对象?如何实现的?

我在之前说过,其实类也是对象。Runtime会将类看作是元类(MetaClass)的对象来处理。当我们向类发送一个消息,类似于[NSObject alloc]这样,其实类是作为元类的一个对象来接收消息的。同时,元类又可看作是根元类(RootMetaClass)的对象,例如一个继承自NSObject的类,它的类指针指向NSObject。所有的元类的类指针都指向根元类作为其父类,而且元类中还保存着其能做出响应的方法列表,当我们给一个类发送消息,就像[NSObject alloc],其实是objc_msgSend()NSObjec的元类的方法列表中查询能够对alloc做出相应的方法,然后让其执行。

为什么我们要继承自苹果的类库?

刚开始接触Cocoa开发时,一般总会从继承NSObject类开始写代码。继承苹果的类库可以给我们的开发带来的极大方便,我们也享受着这种方便。令人惊奇的是,我们与Runtime打交道,其实这时候就开始了。当我们给自定义的类创建一个实例对象时,一般会这样做:

MyObject *object = [[MyObject alloc] init];

+alloc是第一个被执行的消息。在这个文档中,它是这样介绍这个过程的:新建对象的isa指针依据类的数据结构进行初始化,开辟新内存,将对象中其余的变量值置0。所以,继承苹果的类库时,我们不仅继承了一些很好的功能,同时也继承了类似上述可以轻松创建对象并初始化的过程,而且通过这个操作创建出来的对象都能和Runtime要求的数据结构相一致(例如对象的isa指针会自动指向自定义的类)。

什么是类缓存(Class Cache)?

通过对对象的isa指针进行追踪,Runtime可以找到对象所能响应的所有方法。然而我们经常调用的却往往只是这些方法中的一小部分,所以对象在响应消息时,就没必要每次都查询所有的方法,类缓存的概念也因此而来。类缓存的工作原理大致是:当对象对一个消息做出响应时,系统就会将这个方法存入到类缓存中,等objc_msgSend()下次查询时,就会优先检查类缓存,因为系统会认为调用完一个方法之后很有可能下次会再调用相同的方法。让我们以此为基础思考一下以下代码的执行过程:

MyObject *obj = [[MyObject alloc] init];
 
@implementation MyObject
-(id)init {
    if(self = [super init]){
        [self setVarA:@”blah”];
    }
    return self;
}
@end

上述代码大致分别执行了以下过程:

  1. [MyObject alloc]最先执行,然而MyObject类并没有+alloc对应的实现方法,所以系统接着就会查询MyObject的父类——NSObject;
  2. 经过查询,NSObject可以对+alloc做出响应,接着系统会检查MyObjec类,并在内存中开辟一块与其数据结构相一致的内存,并将isa指针指向MyObject,完成对象的创建过程,并把+alloc存入NSObject对应的类缓存中;
  3. 到目前为止,系统执行的都还是类方法,接下来就该执行-init或者其他初始化操作的对象方法了,同样系统也会将-init方法存入到类缓存中;
  4. 接下来该执行self = [super init]了。super是一个指向父类的关键字,在这里,系统会查询NSObject的方法列表并执行其中的init方法。这一步的操作是为了确保OOP继承模型能正确工作,其原理大致是:首先应该正确初始化父类的相关变量,然后我们自定义的类(也就是子类)的变量才能够得到正确的初始化。如果有需要,我们也可以重写父类。不过在这个例子中,NSObject似乎没什么太大的作用,当然这只是特殊情况,有些时候,NSObject则承担着十分重要的初始化角色,例如:
#import < Foundation/Foundation.h>
 
@interface MyObject : NSObject
{
 NSString *aString;
}
 
@property(retain) NSString *aString;
 
@end
 
@implementation MyObject
 
-(id)init
{
 if (self = [super init]) {
  [self setAString:nil];
 }
 return self;
}
 
@synthesize aString;
 
@end
 
 
 
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
 id obj1 = [NSMutableArray alloc];
 id obj2 = [[NSMutableArray alloc] init];
  
 id obj3 = [NSArray alloc];
 id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];
  
 NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class]));
 NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));
  
 NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class]));
 NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));
  
 id obj5 = [MyObject alloc];
 id obj6 = [[MyObject alloc] init];
  
 NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class]));
 NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));
  
 [pool drain];
    return 0;
}

上述代码的打印结果会是什么呢?如果你是刚接触Cocoa开发,有可能会这样回答:

NSMutableArray
NSMutableArray 
NSArray
NSArray
MyObject
MyObject

而实际却是这样:

obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject

原因是:在OC中,+alloc会返回一个类的对象,而-init则会返回另一个类对象。

objc_msgSend()执行了什么?

执行了很多过程,让我们还是以一个例子开始:

[self printMessageWithString:@"HelloWorld!"];

经过编译之后,上述代码实际会变成:

objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");

通过对isa指针的跟踪,系统会查询接受对象(或其父类)能否对选择器(selector)做出响应,假如系统在类的分发表(class dispatch table)或者类缓存中,找到了对应方法,则系统就会跳转到对应方法的地址并开始执行。不过objc_msgSend()并不会返回消息,它开始执行之后只是通过指针找到对应的实现方法,然后由实现方法执行并完成返回,这看起来就跟是objc_msgSend()返回一样。关于这一点,Bill通过三部分(part1part2&part3)讲解的更为细致,他的意思大致是:在OC代码中,

  1. 检查是否有可以被忽略或者被绕过不执行的选择器——显然,在运行有垃圾回收的机制下,类似于-retain/-release的操作都可以被忽略掉了;
  2. 检查有没有空(nil)对象。OC语言与其他语言不同,给nil对象发送消息完全合法,并且有些时候你也愿意这么做,不过在这里我们假设接收对象不为空,接下来……
  3. 系统在类中查询实现方法(IMP)。首先在类缓存中查询,如果找到了就直接跳转至对应的方法;
  4. 如果类缓存中没有要找的方法,系统就会转而查询类的分发表,如果在表中找到了就直接跳转至对应的方法;
  5. 如果在类缓存和分发表中都没有查询到对应的实现方法,系统就会启用消息转发机制。这意味着你的代码将会被编译器转换为C语言中的函数。假如你写了这样一个方法:
-(int)doComputeWithNum:(int)aNum 

它将会被转换为:

int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum) 

OC Runtime通过函数指针来调用你的函数,而你却不能直接调用这些被转换后的函数。不过Cocoa框架给我们提供了另外一个能获取到这些函数指针的方法:

//声明一个C的函数指针
int (computeNum *)(id,SEL,int);
 
//Cocoa中而不是OC Runtime中的方法
//取得和 objc_msgSend() 获取到的一样的函数指针
computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];
 
//执行Runtime返回的函数指针
computeNum(obj,@selector(doComputeWithNum:),aNum); 

通过这种方式,你可以直接获取到函数并在Runtime中调用它,甚至可以以此绕过Runtime的动态机制,达到确定想执行某一个方法的目的。其实,OC Runtime也是通过这种方法来获取函数的地址的,只不过它是利用objc_msgSend()而已。

OC消息转发

在OC中,给一个不确定其能否做出响应的对象发送消息是合法的,甚至有时会故意这样做,对此,苹果给出的解释是:为了模拟OC本身并不支持的多继承的特性。这一点也是Runtime机制所必须的,它的工作原理大致如下:

void fooMethod(id obj, SEL _cmd)
{
 NSLog(@"Doing Foo");
}

然后将其添加到类方法:

+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(doFoo:)){
        class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

class_addMethod最后的“v@”代表函数的返回类型和参数,可以通过Runtime手册中的TypeEncodings来查看具体字符代表的含义;

(void)forwardInvocation:(NSInvocation *)anInvocation;

不过在这一步中,我们可以这样实现:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

显然,不能再在这个方法里返回self,这会引起死循环。

-(void)forwardInvocation:(NSInvocation *)invocation
{
    SEL invSEL = invocation.selector;
 
    if([altObject respondsToSelector:invSEL]) {
        [invocation invokeWithTarget:altObject];
    } else {
        [self doesNotRecognizeSelector:invSEL];
    }
}
void fooMethod(id obj, SEL _cmd)
{
 NSLog(@"Doing Foo");
}

然后将其添加到类方法:

+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(doFoo:)){
        class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

class_addMethod最后的“v@”代表函数的返回类型和参数,可以通过Runtime手册中的TypeEncodings来查看具体字符代表的含义;如果自定义的类是继承自NSObject,则应实现的方法是:- (void)forwardInvocation:(NSInvocation *)anInvocation。最后,我们可以重写-doesNotRecognizeSelector:方法来做最后一点能做的事情,因为下一步程序就会崩溃。

译者注:这一步,一定要实现-methodSignatureForSelector:这个方法,返回函数的签名类型,即上一步中提到的“v@”,否则-(void)forwardInvocation不会执行!

并不脆弱的变量(现代运行时,ModernRuntime)

我们最近才从现代运行时中认识到的一点:并不脆弱的变量(Non Fragile ivars)。编译时,我们定义的变量是以在类中的偏移地址访问的,而且这些工作编译器能自动帮我们完成,这牵扯到底层的细节,大致类似于:先得到一个指针指向创建的对象,然后基于该对象的起始地址,再根据变量的偏移地址我们就可以访问到变量,最后根据变量的类型确定变量所占的内存空间,所以编译后变量的输出形式(ivar layout)类似于下边的表格,左边一列数字代表偏移地址:


变量输出

在苹果给出Mac OS X10.x更新之前,这一直都运行良好,可在更新之后,我们自定义的类中因为有些部分与父类发生了重叠,重叠的部分会被系统擦除,


变量擦除 唯一的解决办法是苹果还会到以前的布局方式,不过如果苹果这样做的话,意味着他们的框架就会因为其变量定义被冻结而不能与时俱进,在这种“脆弱变量”的机制下,我们不得不重新编译,以使自定义的类继承到父类已经保留的部分。

不过在“不脆弱变量”的机制下,又发生了什么呢?


不脆弱的变量

这种情况下,编译器会自动生成与“脆弱变量”机制下完全一样的布局,不过当Runtime检测到与父类有重叠的部分时,它会在自定义类中自动调整变量的偏移地址,从而保存自定义类的变量。

OC关联对象

在Mac OS X10.6中新引入的一个名词是——关联引用。OC并不支持像其他语言中的动态添加变量的功能,所以在这之前,我们不得不努力为将来有可能用到的变量预留出足够的空间,而从Mac OS X10.6开始,OC已经原生支持这一点了。假如我们想为现有的类,比如NSView添加变量,可以:

#import < Cocoa/Cocoa.h> //Cocoa
#include < objc/runtime.h> //objc runtime api’s
 
@interface NSView (CustomAdditions)
@property(retain) NSImage *customImage;
@end
 
@implementation NSView (CustomAdditions)
 
static char img_key; //has a unique address (identifier)
 
-(NSImage *)customImage
{
    return objc_getAssociatedObject(self,&img_key);
}
 
-(void)setCustomImage:(NSImage *)image
{
    objc_setAssociatedObject(self,&img_key,image,
                             OBJC_ASSOCIATION_RETAIN);
}
 
@end

在Runtime.h中我们可以找到传递给objc_setAssociatedObject()的选项,

/* Associated Object support. */
 
/* objc_setAssociatedObject() options */
enum {
    OBJC_ASSOCIATION_ASSIGN = 0,
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
    OBJC_ASSOCIATION_RETAIN = 01401,
    OBJC_ASSOCIATION_COPY = 01403
};

objc_setAssociatedObject()的参数和@property类似。

混合vTable分发

在现代运行时的源代码中,有以下代码:

/***********************************************************************
* vtable dispatch
* 
* Every class gets a vtable pointer. The vtable is an array of IMPs.
* The selectors represented in the vtable are the same for all classes
*   (i.e. no class has a bigger or smaller vtable).
* Each vtable index has an associated trampoline which dispatches to 
*   the IMP at that index for the receiver class's vtable (after 
*   checking for NULL). Dispatch fixup uses these trampolines instead 
*   of objc_msgSend.
* Fragility: The vtable size and list of selectors is chosen at launch 
*   time. No compiler-generated code depends on any particular vtable 
*   configuration, or even the use of vtable dispatch at all.
* Memory size: If a class's vtable is identical to its superclass's 
*   (i.e. the class overrides none of the vtable selectors), then 
*   the class points directly to its superclass's vtable. This means 
*   selectors to be included in the vtable should be chosen so they are 
*   (1) frequently called, but (2) not too frequently overridden. In 
*   particular, -dealloc is a bad choice.
* Forwarding: If a class doesn't implement some vtable selector, that 
*   selector's IMP is set to objc_msgSend in that class's vtable.
* +initialize: Each class keeps the default vtable (which always 
*   redirects to objc_msgSend) until its +initialize is completed.
*   Otherwise, the first message to a class could be a vtable dispatch, 
*   and the vtable trampoline doesn't include +initialize checking.
* Changes: Categories, addMethod, and setImplementation all force vtable 
*   reconstruction for the class and all of its subclasses, if the 
*   vtable selectors are affected.
**********************************************************************/

这背后的思想是:vTable中保存着最经常被调用的选择器,因为这是用比objc_msgSend()更少的指令,所以可以提高应用的运行速度。在vTable中保存着16个最经常被调用的选择器,再往下,我们会看到在默认的有垃圾回收机制的vTable和没有开启垃圾回收机制的vTable:

static const char * const defaultVtable[] = {
    "allocWithZone:", 
    "alloc", 
    "class", 
    "self", 
    "isKindOfClass:", 
    "respondsToSelector:", 
    "isFlipped", 
    "length", 
    "objectForKey:", 
    "count", 
    "objectAtIndex:", 
    "isEqualToString:", 
    "isEqual:", 
    "retain", 
    "release", 
    "autorelease", 
};
static const char * const defaultVtableGC[] = {
    "allocWithZone:", 
    "alloc", 
    "class", 
    "self", 
    "isKindOfClass:", 
    "respondsToSelector:", 
    "isFlipped", 
    "length", 
    "objectForKey:", 
    "count", 
    "objectAtIndex:", 
    "isEqualToString:", 
    "isEqual:", 
    "hash", 
    "addObject:", 
    "countByEnumeratingWithState:objects:count:", 
};

可我们怎么知道自己是不是从vTable中调用了这些方法呢?调试时,我们会看到以下几种方法:
objc_msgSend_fixup:代表该方法并没有从vTable中调用;
objc_msgSend_fixedup:代表调用了一开始在vTable中现在却已不存在的方法;
objc_msgSend_vtable[0-15]:代表调用了vTable中的某一个方法,后边的数字代表该方法在vTable中的序号。

Runtime会自动调整vTable中方法的顺序,所以这次有可能objc_msgSend_vtable10对应着-length方法,但下次运行时,不要指望它俩还是对应着的。

总结

我希望你能喜欢这篇文章,这也是我在Des Moines Cocoaheads演讲中的内容。OC Runtime是一项浩大的工程,它为我们的Cocoa/OC应用提供了动力,同时也让我们习以为常的功能得以实现,如果你还没有浏览过苹果的官方文档,希望你能浏览一下,以便能够更好的利用OC Runtime。再次感谢你的阅读!

上一篇下一篇

猜你喜欢

热点阅读