Object-C
采用"消息結構"而非”函數(shù)調(diào)用“昵济。對于函數(shù)調(diào)用的語言,是由編譯器決定的。而消息結構的語言访忿,其運行所執(zhí)行的代碼由運行環(huán)境來決定瞧栗。 而這個運行環(huán)境,就是Runtime
海铆。
一. 消息機制
1.1. 消息傳遞
消息機制是Runtime
的核心迹恐,方法調(diào)用的過程可以看做是消息傳遞的過程。
先來熟悉下類的基本結構卧斟,在iOS
中殴边,基本上所有類都直接或者間接繼承于NSObject
(也有NSProxy
這種例外),那么來看下NSObject
:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
NSObject
中持有一個Class
類型的isa
指針,那么這個Class
是什么呢珍语?來看一下:
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // 指向metaclass
Class _Nullable super_class OBJC2_UNAVAILABLE; // 指向其父類
const char * _Nonnull name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息锤岸,初始化默認為0,可以通過runtime函數(shù)class_setVersion和class_getVersion進行修改板乙、讀取
long info OBJC2_UNAVAILABLE; // 一些標識信息,如CLS_CLASS (0x1L) 表示該類為普通 class 是偷,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;
long instance_size OBJC2_UNAVAILABLE; // 該類的實例變量大小(包括從父類繼承下來的實例變量);
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 用于存儲每個成員變量的地址
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
// 與 info 的一些標志位有關,如CLS_CLASS (0x1L),則存儲對象方法募逞,如CLS_META (0x2L)蛋铆,則存儲類方法;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; //指向最近使用的方法的指針,用于提升效率凡辱;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 存儲該類遵守的協(xié)議
}
可以看到戒职,objc_class
中也有多個元素栗恩,除了類型父類等能夠理解顧名思義
的元素透乾,特別需要注意的是isa
和cache
兩個元素。cache
是將用過的方法存儲到其內(nèi)磕秤,優(yōu)先查找乳乌,是典型的時空裝換。而對于類的isa
, 它是指向元類的市咆,也就是說:
mateClass
(元類)生成Class
(類/類對象), Class
(類)生成obj
(對象)汉操。用一張經(jīng)典圖來說明:
有了以上的基礎,那么消息傳遞就會容易理解很多蒙兰。例如我們調(diào)用一個實例方法:
[obj test];
轉化為匯編代碼:
objc_msgSend(obj,sel_registerName("test"));
接下來會調(diào)用_class_lookupMethodAndLoadCache3
方法磷瘤,看下其具體實現(xiàn):
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
retry:
runtimeLock.assertLocked();
imp = cache_getImp(cls, sel);
if (imp) goto done;
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
即開始先從cache
查找:
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
如果緩存命中,直接返回imp
搜变。如果沒有命中采缚,繼續(xù)往下走,先判斷類有沒有加載到內(nèi)存挠他,如果沒有扳抽,先加載類:
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
判斷是否實現(xiàn)了initialize
,如果有實現(xiàn),先調(diào)用initialize
:
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
在類對象的方法列表查找imp
:
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
如果沒有找到贸呢,繼續(xù)在父類的緩存的方法列表中查找imp
镰烧。
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
imp
還沒有找到,則嘗試做一次動態(tài)方法解析:
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);//這里做一次動態(tài)方法解析楞陷。
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
最終沒有找到imp
怔鳖,并且方法解析也沒有處理,那么則進入消息轉發(fā)流程:
imp = (IMP)_objc_msgForward_impcache;
1.2. 消息轉發(fā)
在調(diào)用對象拿到對應的selector
之后固蛾,如果自己無法執(zhí)行這個方法败砂,那么該條消息要被轉發(fā)∥呵Γ或者臨時動態(tài)的添加方法實現(xiàn)昌犹。如果轉發(fā)到最后依舊沒法處理,程序就會崩潰览芳。
如以下例子:
新建一個Person
類繼承于NSObject
,并聲明一個msgTest
方法(不實現(xiàn));
@interface Person : NSObject
- (void)msgTest;
@end
調(diào)用該方法:
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [Person new];
[p msgTest];
}
此時我們將項目跑起來就會發(fā)現(xiàn)斜姥,項目是能通過編譯的,但是會崩潰掉:
-[Person msgTest]: unrecognized selector sent to instance 0x6000020543e0
在方法在調(diào)用時沧竟,系統(tǒng)會查看這個對象能否接收這個消息(沒有實現(xiàn)這個方法)铸敏,如果不能接收,就會調(diào)用下面這幾個方法悟泵,會采用拯救模式杈笔,給你“補救”的機會。
第一次補救: 動態(tài)方法解析
/*
cls:要添加方法的類
name:選擇器
imp:方法實現(xiàn),IMP在objc.h中的定義是:typedef id (*IMP)(id, SEL, ...);該方法至少有兩個參數(shù),self(id)和_cmd(SEL)
types:方法,參數(shù)和返回值的描述,"v@:"表示返回值為void,沒有參數(shù)
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(msgTest)){
return class_addMethod([self class],sel, (IMP)reTest, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void reTest(id self, SEL _cmd) {
NSLog(@"test");
}
可看到打印數(shù)據(jù):
learn[47237:860941] test
注: resolveInstanceMethod
處理對象方法糕非,resolveClassMethod
處理類方法蒙具。
第二次補救: 消息重定向
我們繼續(xù)以實例方法舉例:
創(chuàng)建一個新的類RePerson
,該類包含有msgTest
的實現(xiàn)方法朽肥。
#import "RePerson.h"
@implementation RePerson
- (void)msgTest{
NSLog(@"rePerson");
}
@end
在Person
類中進行下兩步操作:
- resolveInstanceMethod返回值設為NO禁筏。
- forwardingTargetForSelector返回值為RePerson對象。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(msgTest)){
return NO;
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(msgTest)){
return [RePerson new];
}
return [super forwardingTargetForSelector:aSelector];
}
這樣就可以得到結果:
learn[47519:906599] rePerson
第三次補救: 消息轉發(fā)
關于消息轉發(fā)衡招,希望您對Type Encodings 篱昔、NSMethodSignature 、NSInvocation
已經(jīng)有基本的認知始腾,可查看本人呢另一篇文章Type Encodings 州刽、NSMethodSignature 、NSInvocation三部曲浪箭。
也是改變調(diào)用對象,使該消息在新對象上調(diào)用;不同是forwardInvocation
方法帶有一個NSInvocation
對象,這個對象保存了這個方法調(diào)用的所有信息,包括SEL
穗椅,參數(shù)和返回值描述等。
同樣的山林,我們利用上文中描述的RePerson
類房待,實現(xiàn)以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(msgTest)){
[anInvocation invokeWithTarget:[RePerson new]];
return;
}
[super forwardInvocation:anInvocation];
}
同樣的邢羔,我們也可以拿到如下答案:
learn[47574:911006] rePerson
經(jīng)典圖:
消息轉發(fā)也是我們處理unrecognized selector crash
的主要方案,減少對應的崩潰桑孩。
1.3. 關于NSProxy
說到消息轉發(fā)這一問題拜鹤,NSProxy
才是消息轉發(fā)、消息分發(fā)的終極答案流椒。
對比上面的一套消息查找過程敏簿,NSProxy
就簡單多了,接收到 unkonwn selector
后宣虾,直接調(diào)用- (NSMethodSignature *)methodSignatureForSelector:
和 - (void)forwardInvocation:
進行消息轉發(fā)惯裕。看下YYWeakProxy
的源碼:
@implementation YYWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
- (BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
- (NSUInteger)hash {
return [_target hash];
}
- (Class)superclass {
return [_target superclass];
}
- (Class)class {
return [_target class];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
- (BOOL)isProxy {
return YES;
}
- (NSString *)description {
return [_target description];
}
- (NSString *)debugDescription {
return [_target debugDescription];
}
@end
其實就是簡單的實現(xiàn)這兩種方法而已绣硝。
它的主要功能之一就是避免循環(huán)引用:
@implementation MyView {
NSTimer *_timer;
}
- (void)initTimer {
YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
_timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
}
- (void)tick:(NSTimer *)timer {...}
@end
如上例子蜻势, MyView
持有Timer
, Timer
強引用Proxy
, Proxy
雖然能發(fā)送消息到MyView
卻不會形成強引用。
二. Runtime 應用
Runtime
的是iOS
中的高頻詞鹉胖,具體的使用大致分為以下幾個類別:
- 關聯(lián)對象(
Objective-C Associated Objects
)添加對象握玛。 - 方法交換
Method Swizzling
。 - 字典和模型的自動轉換甫菠。
2.1. 關聯(lián)對象
首先拋出一個問題:分類Category
為什么不能直接添加屬性挠铲。
從邏輯角度來說,Category
本來就不是一個真實的類寂诱,是在Runtime
期間拂苹,動態(tài)的為相關類添加方法。在編譯期間連相關對象都沒拿到痰洒,如何添加屬性瓢棒?
另一方面,從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; // 協(xié)議
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);
};
雖然其中包括了屬性的list
,但是并不包含成員變量的list
, 屬性是要自動合成相關的成員變量的带迟,而其明顯不具備這一特點音羞。so囱桨,該如何做呢 ? 當然還是回到Runtime
仓犬。
Runtime
提供了三個函數(shù)進行屬性關聯(lián):
// 關聯(lián)對象 setter
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// objec: 被關聯(lián)對象。key:關聯(lián)key, 唯一標識舍肠。 value:關聯(lián)的對象搀继。policy: 內(nèi)存管理的策略。
// 獲取關聯(lián)的對象 getter
id objc_getAssociatedObject(id object, const void *key);
// 移除關聯(lián)對象 delloc
void objc_removeAssociatedObjects(id object);
內(nèi)存策略:
OBJC_ASSOCIATION_ASSIGN, //等價于 @property(assign)翠语。
OBJC_ASSOCIATION_RETAIN_NONATOMIC, //等價于 @property(strong, nonatomic)叽躯。
OBJC_ASSOCIATION_COPY_NONATOMIC, //等價于 @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN //等價于@property(strong,atomic)肌括。
OBJC_ASSOCIATION_COPY //等價于@property(copy, atomic)点骑。
如我們給一個UIViewController
分類添加一個params
字典用戶接受傳遞過來的參數(shù):
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (Base)
@property (nonatomic, strong) NSDictionary * params;
@end
NS_ASSUME_NONNULL_END
#import "UIViewController+Base.h"
#import <objc/runtime.h>
static const void * jParamsKey = &jParamsKey;
@implementation UIViewController (Base)
- (void)setParams:(NSDictionary *)params{
objc_setAssociatedObject(self, jParamsKey, params, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary *)params{
return objc_getAssociatedObject(self, jParamsKey);
}
@end
2.2. 方法交換 (Method Swizzling)
Method Swizzling
被稱為黑魔法, 在iOS
編程具有不可動搖的核心地位,修改原有方法指向的特性使其能夠十分出色完成以下任務:
-
hook
系統(tǒng)方法黑滴,例如hook
系統(tǒng)字體設置動態(tài)修改不同屏幕下字體大小憨募,hook
系統(tǒng)生命周期方法達到埋點統(tǒng)計的目的。 - 在
debug
過程中hook
原方法來進行bug
修復袁辈。hook
例如NSArray
的indexof
去防崩潰菜谣。 - 實現(xiàn)
KVO
類的觀察者方案。
先看代碼吧晚缩,如果我們實現(xiàn)UIFont
的動態(tài)方案:
#import "UIFont+Adapt.h"
@implementation UIFont (Adapt)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self exchangeMethod];
});
}
+ (void)exchangeMethod{
Class class = [self class];
SEL originalSelector = @selector(systemFontOfSize:);
SEL swizzledSelector = @selector(runTimeFitFont:);
Method systemMethod = class_getClassMethod(class, originalSelector );
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
method_exchangeImplementations(systemMethod, swizzledMethod);
}
+ (UIFont *)runTimeFitFont:(CGFloat)fontSize{
UIFont *fitFont = nil;
//這里并不會造成循環(huán)調(diào)用,方法已經(jīng)被交換
fitFont = [UIFont runTimeFitFont:fontSize * (Main_Screen_Width / 375 )];
return fitFont;
}
@end
這里解釋下這些代碼:
一般情況下尾膊,都會寫一個分類來實現(xiàn)Method Swizzling
。 一般情況下會在load
方法里調(diào)用荞彼,保證在該方法調(diào)用之前冈敛,已經(jīng)完成了方法交換。
load
方法在不同系統(tǒng)下有不同表現(xiàn)鸣皂,在iOS10
或者其它情況下莺债,會出現(xiàn)多次調(diào)用的情況,所以使用dispatch_once
方案保證方法交換只實現(xiàn)一次签夭。
在hook
完成后齐邦,我們調(diào)用原方法,最終就會調(diào)用到交換后的方法第租,而在交換方法如需調(diào)用原方法措拇,類似上面的本來該調(diào)用systemFontOfSize:
的,但是systemFontOfSize:
已經(jīng)被交換了慎宾,所以調(diào)用runTimeFitFont:(CGFloat)fontSize
就是調(diào)用systemFontOfSize:
丐吓,并不會引起循環(huán)調(diào)用。
另一個需要注意點是在hook
父類的方法時候存在的問題趟据,比如我們有一個HookViewController
繼承于 BaseViewController
繼承于UIViewController
券犁,如果我們想hook
它的viewDidAppear
,如果我們直接hook
:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替換方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
// 原方法結構體和替換方法結構體
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 調(diào)用交互兩個方法的實現(xiàn)
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
就會報錯:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JTabbarController swizzle_viewDidAppear:]: unrecognized selector sent to instance 0x7fb191811400'
修改成:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替換方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
// 原方法結構體和替換方法結構體
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 如果當前類沒有原方法的實現(xiàn)IMP汹碱,先調(diào)用class_addMethod來給原方法添加默認的方法實現(xiàn)IMP
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 添加方法實現(xiàn)IMP成功后粘衬,修改替換方法結構體內(nèi)的方法實現(xiàn)IMP和方法類型編碼TypeEncoding
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 添加失敗,調(diào)用交互兩個方法的實現(xiàn)
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
當然如果我們重寫這個方法咳促,也是可以的稚新。
為什么會這樣呢?
根據(jù)方法的的查找路徑跪腹,沒有重寫的話實質(zhì)會去調(diào)用父類的方法褂删,但是父類沒有實現(xiàn)Imp
,就會失敗。
2.3. 字典和模型的自動轉換
根據(jù)上文冲茸,我們已經(jīng)明白屯阀, 類的結構體中包含了成員變量的list
, 那么在這個前提下缅帘,我們就很輕松的做到字典到模型或者說json
到模型的轉換。
具體方案如下:
+ (instancetype)modelWithDict:(NSDictionary *)dict{
id objc = [[self alloc] init];
//1.獲取成員變量
unsigned int count = 0;
//獲取成員變量數(shù)組
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
//獲取成員變量
Ivar ivar = ivarList[i];
//獲取成員變量名稱
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
//獲取成員變量類型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
//獲取key
NSString *key = [ivarName substringFromIndex:1];
id value = dict[key];
// 二級轉換:判斷下value是否是字典,如果是,字典轉換層對應的模型
// 并且是自定義對象才需要轉換
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]){
//獲取class
Class modelClass = NSClassFromString(ivarType);
value = [modelClass modelWithDict:value];
}
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
搭配Type Encodings 难衰、NSMethodSignature 股毫、NSInvocation三部曲,相信就能輕松理解這段代碼召衔,就不多敘铃诬。
三. 總結
這篇文章算是寫的比較快的,大致就是想到哪就寫一寫苍凛,Runtime
這個話題其實也有無數(shù)人寫過了趣席,我只是想用自己的思路把這個話題順一下,有什么問題醇蝴,歡迎留言宣肚。