聲明:本文是本人編程小翁原創(chuàng)档悠,轉(zhuǎn)載請(qǐng)注明左医。
用戶統(tǒng)計(jì).jpeg
用戶行為統(tǒng)計(jì)(User Behavior Statistics, UBS)一直是移動(dòng)互聯(lián)網(wǎng)產(chǎn)品中必不可少的環(huán)節(jié)牙言,也俗稱埋點(diǎn)艘儒。在保證移動(dòng)端流量不會(huì)受較大影響的前提下袜漩,PM們總是希望埋點(diǎn)覆蓋面越廣越好通殃。目前常規(guī)的做法是將埋點(diǎn)代碼封裝成工具類度液,但凡工程中需要埋點(diǎn)(如點(diǎn)擊事件、頁(yè)面跳轉(zhuǎn))的地方都插入埋點(diǎn)代碼画舌。一旦項(xiàng)目越來(lái)越復(fù)雜堕担,你會(huì)發(fā)現(xiàn)埋點(diǎn)的代碼散落在程序的各個(gè)角落,不利于維護(hù)以及復(fù)用曲聂。本文旨在探討利用iOS的運(yùn)行時(shí)機(jī)制實(shí)現(xiàn)一種可復(fù)霹购、解耦、容易維護(hù)的用戶統(tǒng)計(jì)方案朋腋。探討畢竟是探討齐疙,歡迎到在簡(jiǎn)書(shū)留言討論膜楷。本文雖有些長(zhǎng)卻是用心之作,希望你有耐心看完贞奋。
注:本文需要一些iOS的Runtime基礎(chǔ)
該方案的完成將會(huì)用到以下知識(shí):
Method Swizzling(Hook)
單元測(cè)試
一赌厅、常規(guī)埋點(diǎn)做法
接著開(kāi)頭的話題,我們先回顧一下主流的埋點(diǎn)是怎么做的轿塔。我粗糙地將埋點(diǎn)分為兩種:1特愿、頁(yè)面統(tǒng)計(jì),包括頁(yè)面停留時(shí)間勾缭、頁(yè)面進(jìn)入次數(shù)揍障;2、交互事件統(tǒng)計(jì)俩由,包括單擊毒嫡、雙擊、手勢(shì)交互等幻梯。
1)常規(guī)頁(yè)面統(tǒng)計(jì)埋點(diǎn)
以統(tǒng)計(jì)頁(yè)面進(jìn)入次數(shù)為例审胚,最簡(jiǎn)單粗暴的做法是在所有頁(yè)面的viewDidAppear:以及viewDidDisappear:中分別埋點(diǎn),將自己對(duì)應(yīng)的pageID上傳給服務(wù)端礼旅。代碼大概長(zhǎng)醬紫:
@implementationHomeViewController//...other methods- (void)viewDidAppear:(BOOL)animated{? ? [superviewWillAppear:animated];? ? [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_ENTER"];}- (void)viewDidDisappear:(BOOL)animated{? ? [superviewDidDisappear:animated];? ? [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_LEAVE"];}@end
+[WUserStatistics sendEventToServer:]封裝網(wǎng)絡(luò)請(qǐng)求膳叨,將ID上傳給服務(wù)器。上述方案有以下弊端:
1痘系、復(fù)用性差菲嘴。這部分埋點(diǎn)代碼很難給其他項(xiàng)目復(fù)用
2、工作量大汰翠。尤其當(dāng)頁(yè)面較多時(shí)龄坪,需要修改的代碼較多
3、引入“臟代碼”复唤,不易維護(hù)
第3點(diǎn)提到的“臟代碼”意思是用戶行為分析這種業(yè)務(wù)其實(shí)跟主業(yè)務(wù)沒(méi)太大關(guān)系健田,不應(yīng)該保持如此高的耦合度,因?yàn)檫@些代碼會(huì)干擾我們對(duì)項(xiàng)目主業(yè)務(wù)的維護(hù)佛纫。這個(gè)我個(gè)人看法妓局。
2)常規(guī)交互事件埋點(diǎn)
常規(guī)做法一般在交互事件的selector中獲取該事件的ID并上傳給服務(wù)端,代碼大概長(zhǎng)醬紫:
- (IBAction)onFavBtnPressed:(id)sender{? ? [WUserStatistics sendEventToServer:@"CTRL_EVENT_HOME_FAV"];//...do other things}
稍微大一點(diǎn)的APP如果采用這種方式呈宇,那諸如此類的埋點(diǎn)代碼將遍地都是好爬。它的缺點(diǎn)參考頁(yè)面統(tǒng)計(jì)埋點(diǎn)部分,其復(fù)用性基本為零甥啄,也就是在新項(xiàng)目中根本無(wú)法復(fù)用埋點(diǎn)代碼存炮。
小總結(jié)一下,采用常規(guī)的做法雖然直觀方便,但在可復(fù)用性穆桂、可維護(hù)性等方面有所欠缺宫盔。在我看來(lái),借助運(yùn)行時(shí)可以很好地避開(kāi)這些缺點(diǎn)享完。
二飘言、Method Swizzling、Hook與代碼注入
由于Runtime知識(shí)不屬于本文的重點(diǎn)驼侠,這里只簡(jiǎn)單介紹姿鸿。
在iOS中,我們可以在運(yùn)行時(shí)替換兩個(gè)方法的實(shí)現(xiàn)倒源,達(dá)到“勾住”某個(gè)方法并注入代碼的目的苛预。具體做法是:
重載類的“+(void)load”方法,在程序加載到內(nèi)存時(shí)利用Runtime的method_exchangeImplementations等接口將方法(設(shè)為M)的實(shí)現(xiàn)互相交換笋熬。當(dāng)方法M被調(diào)用時(shí)就會(huì)被勾住(Hook)热某,執(zhí)行我們的方法。
這種技術(shù)也稱為Method Swizzling胳螟,屬于面向切面編程(Aspect-Oriented Programming)的一種實(shí)現(xiàn)昔馋。
替換兩個(gè)方法的實(shí)現(xiàn),代碼一般長(zhǎng)醬紫:
@interfaceWHookUtility : NSObject+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;@end@implementationWHookUtility+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{? ? Classclass= cls;? ? Method originalMethod = class_getInstanceMethod(class,originalSelector);? ? Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);? ? ? ? BOOL didAddMethod =? ? class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if(didAddMethod) {? ? ? ? class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));? ? }else{? ? ? ? method_exchangeImplementations(originalMethod, swizzledMethod);? ? }}@end
這個(gè)WHookUtility工具類下文會(huì)用到糖耸。比如現(xiàn)在我們要勾住UIViewController的viewWillAppear:方法秘遏,可以這樣做:
@implementationUIViewController(userStastistics)+ (void)load {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{? ? ? ? SEL originalSelector =@selector(viewWillAppear:);? ? ? ? SEL swizzledSelector =@selector(swiz_viewWillAppear:);? ? ? ? [WHookUtility swizzlingInClass:[selfclass] originalSelector:originalSelector swizzledSelector:swizzledSelector];? ? });}#pragma mark - Method Swizzling- (void)swiz_viewWillAppear:(BOOL)animated{//插入需要執(zhí)行的代碼NSLog(@"我在viewWillAppear執(zhí)行前偷偷插入了一段代碼");//不能干擾原來(lái)的代碼流程,插入代碼結(jié)束后要讓本來(lái)該執(zhí)行的代碼繼續(xù)執(zhí)行[selfswiz_viewWillAppear:animated];}@end
更多關(guān)于Runtime嘉竟、method swizzling邦危、面向切面編程的介紹請(qǐng)參考這里
三、基于運(yùn)行時(shí)的埋點(diǎn)方案
為了便于下文敘述舍扰,先引入一個(gè)簡(jiǎn)單的項(xiàng)目倦蚪,共有兩個(gè)頁(yè)面(HomeViewController,DetailViewController)边苹,如下:
1.gif
需求是
統(tǒng)計(jì)兩個(gè)頁(yè)面的展示與離開(kāi)次數(shù)
統(tǒng)計(jì)收藏陵且、分享單擊事件的次數(shù)
對(duì)現(xiàn)有工程代碼影響越小越好
1)統(tǒng)計(jì)兩個(gè)頁(yè)面的展示與離開(kāi)次數(shù)
這部分應(yīng)該比較直觀了,摒棄掉在每個(gè)controller中埋點(diǎn)的方式个束,我們對(duì)UIViewController添加category從而Hook到viewWillAppear:與viewWillDisappear:慕购。在這兩個(gè)方法中注入埋點(diǎn)代碼:
埋點(diǎn)代碼注入.jpg
這時(shí)候問(wèn)題來(lái)了,項(xiàng)目中每個(gè)頁(yè)面都會(huì)有自己的頁(yè)面事件編號(hào)(pageEventID)播急,此處的埋點(diǎn)代碼如何知道要發(fā)送什么pageEventID給服務(wù)端呢脓钾?輕松祭出if-else神器:
- (NSString*)pageEventID:(BOOL)bEnterPage{NSString*selfClassName =NSStringFromClass([selfclass]);NSString*pageEventID =nil;if([selfClassName isEqualToString:@"HomeViewController"]) {? ? ? ? pageEventID = bEnterPage ?@"EVENT_HOME_ENTER_PAGE":@"EVENT_HOME_LEAVE_PAGE";? ? }elseif([selfClassName isEqualToString:@"DetailViewController"]) {? ? ? ? pageEventID = bEnterPage ?@"EVENT_DETAIL_ENTER_PAGE":@"EVENT_DETAIL_LEAVE_PAGE";? ? }//else if (<#expression#>)...}
當(dāng)然,我們可以有更優(yōu)雅的方式桩警,比如用一個(gè)配置表替代上面一長(zhǎng)串的if判斷,這樣無(wú)論頁(yè)面數(shù)怎么增加昌妹,代碼始終是那么一小段捶枢。我們新建一個(gè)WGlobalUserStatisticsConfig.plist的配置表來(lái)存放每個(gè)頁(yè)面在進(jìn)入以及離開(kāi)時(shí)的pageEventID握截,結(jié)構(gòu)如下:
配置表結(jié)構(gòu).png
因此爪幻,頁(yè)面進(jìn)出統(tǒng)計(jì)中獲取pageEventID的代碼始終是以下這幾句:
- (NSString*)pageEventID:(BOOL)bEnterPage{NSDictionary*configDict = [selfdictionaryFromUserStatisticsConfigPlist];NSString*selfClassName =NSStringFromClass([selfclass]);returnconfigDict[selfClassName][@"PageEventIDs"][bEnterPage ?@"Enter":@"Leave"];}- (NSDictionary*)dictionaryFromUserStatisticsConfigPlist{NSString*filePath = [[NSBundlemainBundle] pathForResource:@"WGlobalUserStatisticsConfig"ofType:@"plist"];NSDictionary*dic = [NSDictionarydictionaryWithContentsOfFile:filePath];returndic;}
效果如下:
頁(yè)面埋點(diǎn).gif
以上就是完成了頁(yè)面進(jìn)出統(tǒng)計(jì)的埋點(diǎn)堤器,并且達(dá)到了我們的第三點(diǎn)預(yù)期:對(duì)現(xiàn)有代碼基本無(wú)影響。通過(guò)Method Swizzling的方式現(xiàn)有的工程甚至不需要import任何文件躲胳!后期代碼變動(dòng)時(shí)需要維護(hù)的僅僅是plist配置表蒜鸡。
2)統(tǒng)計(jì)收藏胯努、分享單擊事件的次數(shù)
與上一節(jié)思路一致,要做到解耦顯然需要通過(guò)category+hook來(lái)實(shí)現(xiàn)逢防。本文demo中收藏跟分享都是UIButton類型叶沛,可以考慮添加UIButton的catogory。但更好的方式是添加UIControl的category忘朝,這樣可以讓埋點(diǎn)代碼覆蓋到所有UIControl的子類中去灰署,比如button、switch局嘁、segment等溉箕,提高復(fù)用性。
既然要hook悦昵,那就要清楚到底要hookUIControl的哪(幾)個(gè)方法肴茄,只有部分方法是滿足埋點(diǎn)需求的,最好是所hook的方法能提供target但指、actionName等信息独郎。這是個(gè)嘗試的過(guò)程。
UIControl的方法列表有以下:
UIControl方法列表.png
通過(guò)觀察方法名和參數(shù)枚赡,我們有理由懷疑是倒數(shù)第二個(gè)氓癌,因其攜帶了不少貌似有價(jià)值的信息:
- (void)sendAction:(SEL)action to:(nullableid)target forEvent:(nullableUIEvent*)event;
于是寫(xiě)出測(cè)試代碼看看:
@implementationUIControl(userStastistics)+ (void)load {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{? ? ? ? SEL originalSelector =@selector(sendAction:to:forEvent:);? ? ? ? SEL swizzledSelector =@selector(swiz_sendAction:to:forEvent:);? ? ? ? [WHookUtility swizzlingInClass:[selfclass] originalSelector:originalSelector swizzledSelector:swizzledSelector];? ? });}#pragma mark - Method Swizzling- (void)swiz_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event;{//插入埋點(diǎn)代碼[selfperformUserStastisticsAction:action to:target forEvent:event];? ? [selfswiz_sendAction:action to:target forEvent:event];}- (void)performUserStastisticsAction:(SEL)action to:(id)target forEvent:(UIEvent*)event;{NSLog(@"\n***hook success.\n[1]action:%@\n[2]target:%@ \n[3]event:%ld",NSStringFromSelector(action), target, (long)event);}@end
Log如下圖:
need-to-insert-img
Log.png
可以看到,通過(guò)category+method swizzling的方式在沒(méi)有修改現(xiàn)有工程任何代碼的情況下已經(jīng)成功Hook到所有點(diǎn)擊事件贫橙,在Hook代碼中我們知道了一個(gè)點(diǎn)擊事件的target也就是ViewController贪婉,也知道了點(diǎn)擊事件的響應(yīng)函數(shù)名,知道了點(diǎn)擊的TouchSet卢肃。這些信息已經(jīng)能滿足埋點(diǎn)需求了疲迂。
與頁(yè)面統(tǒng)計(jì)埋點(diǎn)類似,我們同樣采用plist配置表的方式避免一大長(zhǎng)串的if-else判斷:
need-to-insert-img
單擊事件配置表結(jié)構(gòu).png
有了這張配置表就很容易得到某次單擊事件的事件ID(ControlEventID):
NSString*actionString =NSStringFromSelector(action);//獲取SEL stringNSString*targetName =NSStringFromClass([targetclass]);//viewController nameNSDictionary*configDict = [selfdictionaryFromUserStatisticsConfigPlist];eventID = configDict[targetName][@"ControlEventIDs"][actionString];
事實(shí)上莫湘,我把某個(gè)頁(yè)面單元的所有事件ID分成了兩類:頁(yè)面事件ID(PageEventIDs尤蒿,頁(yè)面的進(jìn)出等)、交互事件ID(ControlEventIDs幅垮,單擊腰池、雙擊、手勢(shì)等)。分類有助于下文使用單元測(cè)試(Unit Test)進(jìn)行自動(dòng)化后期維護(hù)示弓。
埋點(diǎn)效果如圖:
need-to-insert-img
單擊埋點(diǎn)效果.gif
到這里先做了階段性的總結(jié)讳侨,本文提出的思路有以下優(yōu)越性:
與工程代碼基本解耦,避免引入“臟代碼”
即使后期工程代碼發(fā)生重構(gòu)奏属,需要修改的僅僅是plist配置表
維護(hù)配置表比維護(hù)散落在工程各個(gè)角落的代碼簡(jiǎn)單
四跨跨、基于單元測(cè)試的后期維護(hù)
俗話說(shuō),創(chuàng)業(yè)難守業(yè)更難囱皿。前面的思路基本可以完成初步的埋點(diǎn)需求勇婴。但是在實(shí)際項(xiàng)目中代碼重構(gòu)是很頻繁的。這意味著在多人協(xié)作開(kāi)發(fā)嘱腥、代碼重構(gòu)頻繁的項(xiàng)目中響應(yīng)事件方法甚至頁(yè)面名稱都可能被改掉耕渴,造成事件ID獲取不到導(dǎo)致埋點(diǎn)失效。
代碼變動(dòng)的情況無(wú)非以下幾種(這里只介紹響應(yīng)事件發(fā)生改變的情況):
1爹橱、響應(yīng)事件方法名稱改變或者刪除
比如收藏事件原先是onFavBtnPressed:萨螺,之后被改成onFavouriteBtnPressed:。代碼發(fā)生變動(dòng)但是plist配置表中由于開(kāi)發(fā)人員疏忽忘記同步修改了愧驱。這種疏忽在開(kāi)發(fā)壓力大進(jìn)度趕的情況下是有很大概率發(fā)生的慰技。由于代碼與配置表不匹配將導(dǎo)致eventID為nil。在這種情況下單元測(cè)試就很有必要了组砚,使用完備的測(cè)試用例能在發(fā)版前檢測(cè)到這種不匹配情況從而避免埋點(diǎn)失效吻商。
在單元測(cè)試中我們首先讀取plist配置文件,遍歷所有的頁(yè)面糟红。在一個(gè)頁(yè)面內(nèi)遍歷所有的ControlEventIDs艾帐,對(duì)每個(gè)響應(yīng)函數(shù)名進(jìn)行respondsToSelector:判斷:
need-to-insert-img
單元測(cè)試介紹.png
單測(cè)代碼如下:
- (void)testIfUserStatisticsConfigPlistValid{NSDictionary*configDict = [selfdictionaryFromUserStatisticsConfigPlist];XCTAssertNotNil(configDict,@"WGlobalUserStatisticsConfig.plist加載失敗");? ? ? ? [configDict enumerateKeysAndObjectsUsingBlock:^(NSString*? _Nonnull key,id_Nonnull obj,BOOL* _Nonnull stop) {XCTAssert([obj isKindOfClass:[NSDictionaryclass]],@"plist文件結(jié)構(gòu)可能已經(jīng)改變,請(qǐng)確認(rèn)");NSString*targetPageName = key;? ? ? ? Class pageClass =NSClassFromString(targetPageName);idpageInstance = [[pageClass alloc] init];//一個(gè)pageDict對(duì)應(yīng)一個(gè)頁(yè)面盆偿,存放pageID,所有的action及對(duì)應(yīng)的eventIDNSDictionary*pageDict = (NSDictionary*)obj;//頁(yè)面配置信息NSDictionary*pageEventIDDict = pageDict[@"PageEventIDs"];//交互配置信息NSDictionary*controlEventIDDict = pageDict[@"ControlEventIDs"];XCTAssert(pageEventIDDict,@"plist文件未包含PageID字段或者該字段值為空");XCTAssert(controlEventIDDict,@"plist文件未包含EventIDs字段或者該字段值為空");? ? ? ? ? ? ? ? [pageEventIDDict enumerateKeysAndObjectsUsingBlock:^(NSString*? _Nonnull key,id_Nonnull value,BOOL* _Nonnull stop) {XCTAssert([value isKindOfClass:[NSStringclass]],@"plist文件結(jié)構(gòu)可能已經(jīng)改變柒爸,請(qǐng)確認(rèn)");XCTAssertNotNil(value,@"EVENT_ID為空,請(qǐng)確認(rèn)");? ? ? ? }];? ? ? ? ? ? ? ? [controlEventIDDict enumerateKeysAndObjectsUsingBlock:^(NSString*? _Nonnull key,id_Nonnull value,BOOL* _Nonnull stop) {XCTAssert([value isKindOfClass:[NSStringclass]],@"plist文件結(jié)構(gòu)可能已經(jīng)改變事扭,請(qǐng)確認(rèn)");NSString*actionName = key;? ? ? ? ? ? SEL actionSel =NSSelectorFromString(actionName);XCTAssert([pageInstance respondsToSelector:actionSel],@"代碼與plist文件函數(shù)不匹配捎稚,請(qǐng)確認(rèn):-[%@ %@]", targetPageName, actionName);//EVENT_ID不能為空XCTAssertNotNil(value,@"EVENT_ID為空,請(qǐng)確認(rèn)");? ? ? ? }];? ? }];? ? }
我們來(lái)測(cè)試一下求橄,如果把HomeViewController的onFavBtnPressed:改成onMyFavBtnPressed:后單元測(cè)試的結(jié)果就是:
單元測(cè)試不通過(guò).png
這種改變給單測(cè)輕松捕捉到了今野,
只要XCTAssert的log夠詳細(xì),維護(hù)起來(lái)其實(shí)相當(dāng)輕松的罐农。
上圖中的log已經(jīng)明確指出-[HomeViewController onFavBtnPressed:]方法發(fā)生了改變条霜。
2、代碼中新增了響應(yīng)事件
這種情況常見(jiàn)于新版本中有新的埋點(diǎn)需求涵亏。如果代碼中新增了響應(yīng)事件并且該響應(yīng)事件是在PM要求的埋點(diǎn)列表中宰睡,但是plist有可能會(huì)漏掉該事件蒲凶。這種情況是比較棘手的。上一種情況是基于plist列表去校驗(yàn)代碼夹厌,這里就要反過(guò)來(lái)豹爹,根據(jù)代碼去校驗(yàn)plist是否有缺失裆悄。但問(wèn)題來(lái)了矛纹,一個(gè)項(xiàng)目中響應(yīng)函數(shù)往往是非常多的,并不是任何響應(yīng)函數(shù)都需要埋點(diǎn)光稼。需要埋點(diǎn)的響應(yīng)函數(shù)與其他響應(yīng)函數(shù)并沒(méi)有區(qū)別或南。
對(duì)于這種情況,一種方式是加強(qiáng)code review避免忘記往配置表中添加埋點(diǎn)(這簡(jiǎn)直就是廢話)艾君;一種是:要求埋點(diǎn)響應(yīng)函數(shù)的方法名中包含約定的字符串采够,比如收藏事件的方法名為onFavBtnPressed_UA:表示這個(gè)事件是需要埋點(diǎn)的。然后在單元測(cè)試中使用運(yùn)行時(shí)APIclass_copyMethodList取出標(biāo)記了_UA的所有函數(shù)冰垄,隨后到plist中校驗(yàn)是否存在蹬癌。不存在則表示測(cè)試用例不通過(guò),提示開(kāi)發(fā)人員校驗(yàn)虹茶。
代碼略逝薪。如果對(duì)單元測(cè)試不熟悉,可以參考單元測(cè)試
小總結(jié):
合理的單元測(cè)試可以為本文方案的后期維護(hù)減輕相當(dāng)大的負(fù)擔(dān)蝴罪,測(cè)試用例的完備性很重要董济,需要用心設(shè)計(jì)考慮周全。
五要门、結(jié)語(yǔ)
以上就是結(jié)合運(yùn)行時(shí)所設(shè)計(jì)出的用戶統(tǒng)計(jì)思路全部?jī)?nèi)容虏肾。應(yīng)該說(shuō)該方案的可復(fù)用性與解耦程度都是不錯(cuò)的,既適合于新建的工程欢搜,也適合于已經(jīng)創(chuàng)建的工程封豪。看起來(lái)內(nèi)容多炒瘟,其實(shí)總結(jié)起來(lái)無(wú)非幾個(gè)步驟:plist配置表+Hook+單元測(cè)試吹埠。利用Method Swizzling把埋點(diǎn)代碼集中管理其實(shí)也是合理的,有利于專人開(kāi)發(fā)唧领、跟蹤及維護(hù)藻雌。當(dāng)然以上思路只考慮簡(jiǎn)單的情形,更復(fù)雜的情況就需要變通了斩个,但總體思路就是如此胯杭。
思路可能不完美,但作為一種嘗試也未嘗不可受啥。路都是走出來(lái)的做个。
本文demo地址鸽心,記得star噢!
喜歡本文可以點(diǎn)一下喜歡并關(guān)注我居暖,或者留個(gè)言示個(gè)愛(ài)(拋媚眼中)
不喜歡可以留言提建議顽频,我必虛心接受
歡迎轉(zhuǎn)載
作者:編程小翁
鏈接:http://www.reibang.com/p/0497afdad36b
來(lái)源:簡(jiǎn)書(shū)
簡(jiǎn)書(shū)著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處太闺。