iOS開發中Runloop和Runtime淺談


Runloop

做了一年多的IOS開發,對IOS和Objective-C深層次的了解還十分有限,大多還停留在會用API的級別,這是件挺可悲的事情。想學好一門語言

還是需要深層次的了解它,這樣才能在使用的時候得心應手,出現各種怪異的問題時不至於不知所措。廢話少說,進入今天的正題。

不知道大家有沒有想過這個問題,一個應用開始運行以后放在那里,如果不對它進行任何操作,這個應用就像靜止了一樣,不會自發的有任何動作發生,但是如果我

們點擊界面上的一個按鈕,這個時候就會有對應的按鈕響應事件發生。給我們的感覺就像應用一直處於隨時待命的狀態,在沒人操作的時候它一直在休息,在讓它干

活的時候,它就能立刻響應。其實,這就是run loop的功勞。

一、線程與run loop

1.1 線程任務的類型

再來說說線程。有些線程執行的任務是一條直線,起點到終點;而另一些線程要干的活則是一個圓,不斷循環,直到通過某種方式將它終止。直線線程如簡單的

Hello

World,運行打印完,它的生命周期便結束了,像曇花一現那樣;圓類型的如操作系統,一直運行直到你關機。在IOS中,圓型的線程就是通過run

loop不停的循環實現的。

1.2 線程與run loop的關系

Run loop,正如其名,loop表示某種循環,和run放在一起就表示一直在運行着的循環。實際上,run

loop和線程是緊密相連的,可以這樣說run loop是為了線程而生,沒有線程,它就沒有存在的必要。Run

loops是線程的基礎架構部分,Cocoa和CoreFundation都提供了run loop對象方便配置和管理線程的run

loop(以下都已Cocoa為例)。每個線程,包括程序的主線程(main thread)都有與之相應的run loop對象。

1.2.1 主線程的run loop默認是啟動的。

iOS的應用程序里面,程序啟動后會有一個如下的main() 函數:

intmain(intargc,char*argv[])

{

@autoreleasepool{

returnUIApplicationMain(argc, argv,nil,NSStringFromClass([appDelegateclass]));

}

}

重點是UIApplicationMain() 函數,這個方法會為main thread 設置一個NSRunLoop 對象,這就解釋了本文開始說的為什么我們的應用可以在無人操作的時候休息,需要讓它干活的時候又能立馬響應。

1.2.2 對其它線程來說,run loop默認是沒有啟動的,如果你需要更多的線程交互則可以手動配置和啟動,如果線程只是去執行一個長時間的已確定的任務則不需要。

1.2.3 在任何一個Cocoa程序的線程中,都可以通過:

NSRunLoop*runloop = [NSRunLoopcurrentRunLoop];

來獲取到當前線程的run loop。

1.3 關於run loop的幾點說明

1.3.1 Cocoa中的NSRunLoop類並不是線程安全的

我們不能再一個線程中去操作另外一個線程的run

loop對象,那很可能會造成意想不到的后果。不過幸運的是CoreFundation中的不透明類CFRunLoopRef是線程安全的,而且兩種類型

的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過實例方法:

- (CFRunLoopRef)getCFRunLoop;

獲取對應的CFRunLoopRef類,來達到線程安全的目的。

1.3.2 Run loop的管理並不完全是自動的。

我們仍必須設計線程代碼以在適當的時候啟動run loop並正確響應輸入事件,當然前提是線程中需要用到run loop。而且,我們還需要使用while/for語句來驅動run loop能夠循環運行,下面的代碼就成功驅動了一個run loop:

BOOLisRunning =NO;

do{

isRunning = [[NSRunLoopcurrentRunLoop]runMode:NSDefaultRunLoopModebeforeDate:[NSDatedistantFuture]];

}while(isRunning);

1.3.3 Run loop同時也負責autorelease pool的創建和釋放

在使用手動的內存管理方式的項目中,會經常用到很多自動釋放的對象,如果這些對象不能夠被即時釋放掉,會造成內存占用量急劇增大。Run

loop就為我們做了這樣的工作,每當一個運行循環結束的時候,它都會釋放一次autorelease

pool,同時pool中的所有自動釋放類型變量都會被釋放掉。

1.3.4 Run loop的優點

一個run

loop就是一個事件處理循環,用來不停的監聽和處理輸入事件並將其分配到對應的目標上進行處理。如果僅僅是想實現這個功能,你可能會想一個簡單的

while循環不就可以實現了嗎,用得着費老大勁來做個那么復雜的機制?顯然,蘋果的架構設計師不是吃干飯的,你想到的他們早就想過了。

首先,NSRunLoop是一種更加高明的消息處理模式,他就高明在對消息處理過程進行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的

具體消息的處理,在NSRunLoop中每一個消息就被打包在input source或者是timer source(見后文)中了。

其次,也是很重要的一點,使用run loop可以使你的線程在有工作的時候工作,沒有工作的時候休眠,這可以大大節省系統資源。

二、Run loop相關知識點

2.1輸入事件來源

Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。兩種源都使用程序的某一特定的處理例程來處理到達的事件。圖-1顯示了run loop的概念結構以及各種源。

需要說明的是,當你創建輸入源,你需要將其分配給run loop中的一個或多個模式(什么是模式,下文將會講到)。模式只會在特定事件影響監聽的源。大多數情況下,run loop運行在默認模式下,但是你也可以使其運行在自定義模式。若某一源在當前模式下不被監聽,那么任何其生成的消息只在run loop運行在其關聯的模式下才會被傳遞。

圖-1  Runloop的結構和輸入源類型

2.1.1輸入源(input source)

傳遞異步事件,通常消息來自於其他線程或程序。輸入源傳遞異步消息給相應的處理例程,並調用runUntilDate:方法來退出(在線程里面相關的NSRunLoop對象調用)。

2.1.1.1基於端口的輸入源

基於端口的輸入源由內核自動發送。

Cocoa和Core

Foundation內置支持使用端口相關的對象和函數來創建的基於端口的源。例如,在Cocoa里面你從來不需要直接創建輸入源。你只要簡單的創建端口

對象,並使用NSPort的方法把該端口添加到run loop。端口對象會自己處理創建和配置輸入源。

在Core Foundation,你必須人工創建端口和它的run

loop源。我們可以使用端口相關的函數(CFMachPortRef,CFMessagePortRef,CFSocketRef)來創建合適的對象。

下面的例子展示了如何創建一個基於端口的輸入源,將其添加到run loop並啟動:

voidcreatePortSource()

{

CFMessagePortRefport =CFMessagePortCreateLocal(kCFAllocatorDefault,CFSTR("com.someport"),myCallbackFunc,NULL,NULL);

CFRunLoopSourceRefsource =CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port,0);

CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopCommonModes);

while(pageStillLoading) {

NSAutoreleasePool*pool = [[NSAutoreleasePoolalloc]init];

CFRunLoopRun();

[poolrelease];

}

CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);

CFRelease(source);

}

2.1.1.2自定義輸入源

自定義的輸入源需要人工從其他線程發送。

為了創建自定義輸入源,必須使用Core

Foundation里面的CFRunLoopSourceRef類型相關的函數來創建。你可以使用回調函數來配置自定義輸入源。Core

Fundation會在配置源的不同地方調用回調函數,處理輸入事件,在源從run loop移除的時候清理它。

除了定義在事件到達時自定義輸入源的行為,你也必須定義消息傳遞機制。源的這部分運行在單獨的線程里面,並負責在數據等待處理的時候傳遞數據給源並通知它處理數據。消息傳遞機制的定義取決於你,但最好不要過於復雜。創建並啟動自定義輸入源的示例如下:

voidcreateCustomSource()

{

CFRunLoopSourceContextcontext = {0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL};

CFRunLoopSourceRefsource =CFRunLoopSourceCreate(kCFAllocatorDefault,0, &context);

CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);

while(pageStillLoading) {

NSAutoreleasePool*pool = [[NSAutoreleasePoolalloc]init];

CFRunLoopRun();

[poolrelease];

}

CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);

CFRelease(source);

}

2.1.1.3Cocoa上的Selector源

除了基於端口的源,Cocoa定義了自定義輸入源,允許你在任何線程執行selector方法。和基於端口的源一樣,執行selector請求會在目標線

程上序列化,減緩許多在線程上允許多個方法容易引起的同步問題。不像基於端口的源,一個selector執行完后會自動從run loop里面移除。

當在其他線程上面執行selector時,目標線程須有一個活動的run loop。對於你創建的線程,這意味着線程在你顯式的啟動run loop之前是不會執行selector方法的,而是一直處於休眠狀態。

NSObject類提供了類似如下的selector方法:

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray*)array;

2.1.2定時源(timer source)

定時源在預設的時間點同步方式傳遞消息,這些消息都會發生在特定時間或者重復的時間間隔。定時源則直接傳遞消息給處理例程,不會立即退出run loop。

需要注意的是,盡管定時器可以產生基於時間的通知,但它並不是實時機制。和輸入源一樣,定時器也和你的run

loop的特定模式相關。如果定時器所在的模式當前未被run loop監視,那么定時器將不會開始直到run

loop運行在相應的模式下。類似的,如果定時器在run loop處理某一事件期間開始,定時器會一直等待直到下次run

loop開始相應的處理程序。如果run loop不再運行,那定時器也將永遠不啟動。

創建定時器源有兩種方法,

方法一:

NSTimer *timer = [NSTimerscheduledTimerWithTimeInterval:4.0

target:self

selector:@selector(backgroundThreadFire:) userInfo:nil

repeats:YES];

[[NSRunLoop currentRunLoop]addTimer:timerforMode:NSDefaultRunLoopMode];

方法二:

[NSTimerscheduledTimerWithTimeInterval:10

target:self

selector:@selector(backgroundThreadFire:)

userInfo:nil

repeats:YES];

2.2 RunLoop觀察者

源是在合適的同步或異步事件發生時觸發,而run loop觀察者則是在run loop本身運行的特定時候觸發。你可以使用run loop觀察者來為處理某一特定事件或是進入休眠的線程做准備。你可以將run loop觀察者和以下事件關聯:

1.  Runloop入口

2.  Runloop何時處理一個定時器

3.  Runloop何時處理一個輸入源

4.  Runloop何時進入睡眠狀態

5.  Runloop何時被喚醒,但在喚醒之前要處理的事件

6.  Runloop終止

和定時器類似,在創建的時候你可以指定run loop觀察者可以只用一次或循環使用。若只用一次,那么在它啟動后,會把它自己從run

loop里面移除,而循環的觀察者則不會。定義觀察者並把它添加到run loop,只能使用Core

Fundation。下面的例子演示了如何創建run loop的觀察者:

- (void)addObserverToCurrentRunloop

{

// The application uses garbage collection, so noautorelease pool is needed.

NSRunLoop*myRunLoop = [NSRunLoop currentRunLoop];

// Create a run loop observer and attach it to the runloop.

CFRunLoopObserverContextcontext = {0,self,NULL,NULL,NULL};

CFRunLoopObserverRef    observer =CFRunLoopObserverCreate(kCFAllocatorDefault,

kCFRunLoopBeforeTimers,YES,0, &myRunLoopObserver, &context);

if(observer)

{

CFRunLoopRefcfLoop = [myRunLoopgetCFRunLoop];

CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);

}

}

其中,kCFRunLoopBeforeTimers表示選擇監聽定時器觸發前處理事件,后面的YES表示循環監聽。

2.3 RunLoop的事件隊列

每次運行run loop,你線程的run loop對會自動處理之前未處理的消息,並通知相關的觀察者。具體的順序如下:

通知觀察者run loop已經啟動

通知觀察者任何即將要開始的定時器

通知觀察者任何即將啟動的非基於端口的源

啟動任何准備好的非基於端口的源

如果基於端口的源准備好並處於等待狀態,立即啟動;並進入步驟9。

通知觀察者線程進入休眠

將線程置於休眠直到任一下面的事件發生:

某一事件到達基於端口的源

定時器啟動

Run loop設置的時間已經超時

run loop被顯式喚醒

通知觀察者線程將被喚醒。

處理未處理的事件

如果用戶定義的定時器啟動,處理定時器事件並重啟run loop。進入步驟2

如果輸入源啟動,傳遞相應的消息

如果run loop被顯式喚醒而且時間還沒超時,重啟run loop。進入步驟2

通知觀察者run loop結束。

因為定時器和輸入源的觀察者是在相應的事件發生之前傳遞消息,所以通知的時間和實際事件發生的時間之間可能存在誤差。如果需要精確時間控制,你可以使用休眠和喚醒通知來幫助你校對實際發生事件的時間。

因為當你運行run loop時定時器和其它周期性事件經常需要被傳遞,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標路徑追蹤。因為你的代碼直接獲取到消息而不是經由程序傳遞,因此活躍的定時器不會開始直到鼠標追蹤結束並將控制權交給程序。

Run loop可以由run loop對象顯式喚醒。其它消息也可以喚醒run loop。例如,添加新的非基於端口的源會喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發生后再處理。

從這個事件隊列中可以看出:

①如果是事件到達,消息會被傳遞給相應的處理程序來處理, runloop處理完當次事件后,run loop會退出,而不管之前預定的時間到了沒有。你可以重新啟動run loop來等待下一事件。

②如果線程中有需要處理的源,但是響應的事件沒有到來的時候,線程就會休眠等待相應事件的發生。這就是為什么run loop可以做到讓線程有工作的時候忙於工作,而沒工作的時候處於休眠狀態。

2.4什么時候使用run loop

僅當在為你的程序創建輔助線程的時候,你才需要顯式運行一個run loop。Run

loop是程序主線程基礎設施的關鍵部分。所以,Cocoa和Carbon程序提供了代碼運行主程序的循環並自動啟動run

loop。IOS程序中UIApplication的run方法(或Mac OS

X中的NSApplication)作為程序啟動步驟的一部分,它在程序正常啟動的時候就會啟動程序的主循環。類似

的,RunApplicationEventLoop函數為Carbon程序啟動主循環。如果你使用xcode提供的模板創建你的程序,那你永遠不需要自

己去顯式的調用這些例程。

對於輔助線程,你需要判斷一個run loop是否是必須的。如果是必須的,那么你要自己配置並啟動它。你不需要在任何情況下都去啟動一個線程的run

loop。比如,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動run loop。Run

loop在你要和線程有更多的交互時才需要,比如以下情況:

使用端口或自定義輸入源來和其他線程通信

使用線程的定時器

Cocoa中使用任何performSelector…的方法

使線程周期性工作

如果你決定在程序中使用run loop,那么它的配置和啟動都很簡單。和所有線程編程一樣,你需要計划好在輔助線程退出線程的情形。讓線程自然退出往往比強制關閉它更好。

-------- by wangzz

參考文檔:

http://developer.apple.com/library/ios/#documentation/cocoa/Conceptual/Multithreading/Introduction/Introduction.html#//apple_ref/doc/uid/10000057i

Runtime

RunTime簡稱運行時。就是系統在運行的時候的一些機制,其中最主要的是消息機制。對於C語言,函數的調用在編譯的時候會決定調用哪個函數( C語言的函數調用請看這里

)。編譯完成之后直接順序執行,無任何二義性。OC的函數調用成為消息發送。屬於動態調用過程。在編譯的時候並不能決定真正調用哪個函數(事實證明,在編

譯階段,OC可以調用任何函數,即使這個函數並未實現,只要申明過就不會報錯。而C語言在編譯階段就會報錯)。只有在真正運行的時候才會根據函數的名稱找

到對應的函數來調用。

那OC是怎么實現動態調用的呢?下面我們來看看OC通過發送消息來達到動態調用的秘密。假如在OC中寫了這樣的一個代碼:

[obj makeText];

其中obj是一個對象,makeText是一個函數名稱。對於這樣一個簡單的調用。在編譯時RunTime會將上述代碼轉化成

objc_msgSend(obj,@selector(makeText));

首先我們來看看obj這個對象,iOS中的obj都繼承於NSObject。

@interface NSObject  {

Class isa  OBJC_ISA_AVAILABILITY;

}

在NSObjcet中存在一個Class的isa指針。然后我們看看Class:


typedef struct objc_class *Class;

struct objc_class {

Class isa;// 指向metaclass

Class super_class ;// 指向其父類

const char *name ;// 類名

long version ;// 類的版本信息,初始化默認為0,可以通過runtime函數class_setVersion和class_getVersion進行修改、讀取

long info;// 一些標識信息,如CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass,其中包含類方法;

long instance_size ;// 該類的實例變量大小(包括從父類繼承下來的實例變量);

struct objc_ivar_list *ivars;// 用於存儲每個成員變量的地址

struct objc_method_list **methodLists ;// 與 info 的一些標志位有關,如CLS_CLASS (0x1L),則存儲對象方法,如CLS_META (0x2L),則存儲類方法;

struct objc_cache *cache;// 指向最近使用的方法的指針,用於提升效率;

struct objc_protocol_list *protocols;// 存儲該類遵守的協議

}

我們可以看到,對於一個Class類中,存在很多東西,下面我來一一解釋一下:

Class

isa:指向metaclass,也就是靜態的Class。一般一個Obj對象中的isa會指向普通的Class,這個Class中存儲普通成員變量和對

象方法(“-”開頭的方法),普通Class中的isa指針指向靜態Class,靜態Class中存儲static類型成員變量和類方法(“+”開頭的方

法)。

Class super_class:指向父類,如果這個類是根類,則為NULL。

下面一張圖片很好的描述了類和對象的繼承關系:

注意:所有metaclass中isa指針都指向跟metaclass。而跟metaclass則指向自身。Root metaclass是通過繼承Root class產生的。與root class結構體成員一致,也就是前面提到的結構。不同的是Root metaclass的isa指針指向自身。

Class類中其他的成員這里就先不做過多解釋了,下面我們來看看:

@selector (makeText):這是一個SEL方法選擇器。SEL其主要作用是快速的通過方法名字(makeText)查找到對應方法的函數指針,然后調用其函數。SEL其本身是一個Int類型的一個地址,地址中存放着方法的名字。對於一個類中。每一個方法對應着一個SEL。所以iOS類中不能存在2個名稱相同的方法,即使參數類型不同,因為SEL是根據方法名字生成的,相同的方法名稱只能對應一個SEL。

下面我們就來看看具體消息發送之后是怎么來動態查找對應的方法的。

首先,編譯器將代碼[obj makeText];轉化為objc_msgSend(obj, @selector

(makeText));,在objc_msgSend函數中。首先通過obj的isa指針找到obj對應的class。在Class中先去cache中通過SEL查找對應函數method(猜測cache中method列表是以SEL為key通過hash表來存儲的,這樣能提高函數查找速度),若cache中未找到。再去methodList中查找,若methodlist中未找到,則取superClass中查找。若能找到,則將method加入到cache中,以方便下次查找,並通過method中的函數指針跳轉到對應的函數中去執行。

Runtime應用

1.什么是runtime?

runtime是一套底層的C語言API,包含很多強大實用的C語言數據類型和C語言函數,平時我們編寫的OC代碼,底層都是基於runtime實現的。

2.runtime有什么作用?

1.能動態產生一個類,一個成員變量,一個方法

2.能動態修改一個類,一個成員變量,一個方法

3.能動態刪除一個類,一個成員變量,一個方法

3.常用的頭文件

#import包含對類、成員變量、屬性、方法的操作#import包含消息機制

4.常用方法

class_copyIvarList()返回一個指向類的成員變量數組的指針

class_copyPropertyList()返回一個指向類的屬性數組的指針

注意:根據Apple官方runtime.h文檔所示,上面兩個方法返回的指針,在使用完畢之后必須free()。

ivar_getName()獲取成員變量名-->C類型的字符串property_getName()獲取屬性名-->C類型的字符串-------------------------------------typedef struct objc_method *Method;class_getInstanceMethod() class_getClassMethod()以上兩個函數傳入返回Method類型---------------------------------------------------method_exchangeImplementations()交換兩個方法的實現

5.runtime在開發中的用途

1.動態的遍歷一個類的所有成員變量,用於字典轉模型,歸檔解檔操作

代碼如下:

- (void)viewDidLoad {      [superviewDidLoad];/** 利用runtime遍歷一個類的全部成員變量

1.導入頭文件    */unsignedintcount =0;/** Ivar:表示成員變量類型 */Ivar *ivars = class_copyIvarList([BDPerson class], &count);//獲得一個指向該類成員變量的指針for(inti =0; i < count; i ++) {//獲得IvarIvar ivar = ivars[i];//根據ivar獲得其成員變量的名稱--->C語言的字符串constchar*name = ivar_getName(ivar);NSString*key = [NSStringstringWithUTF8String:name];NSLog(@"%d----%@",i,key);}}

運行結果如下:

成員變量遍歷輸出結果.png

獲取一個類的全部屬性:

獲取類的屬性的代碼實現.png

結果如下:

輸出結果.png

應用場景:

可以利用遍歷類的屬性,來快速的進行歸檔操作。

將從網絡上下載的json數據進行字典轉模型。

注意:歸檔解檔需要遵守協議,實現以下兩個方法

- (void)encodeWithCoder:(NSCoder*)encoder{

//歸檔存儲自定義對象

unsignedintcount =0;

//獲得指向該類所有屬性的指針

objc_property_t *properties =    class_copyPropertyList([BDPerson class], &count);

for(inti =0; i < count; i ++) {

//獲得

objc_property_t property = properties[i];

//根據objc_property_t獲得其屬性的名稱--->C語言的字符串

constchar*name = property_getName(property);

NSString*key = [NSStringstringWithUTF8String:name];

//      編碼每個屬性,利用kVC取出每個屬性對應的數值

[encoder encodeObject:[selfvalueForKeyPath:key] forKey:key]; 

}

}

- (instancetype)initWithCoder:(NSCoder*)decoder{

//歸檔存儲自定義對象

unsignedintcount =0;

//獲得指向該類所有屬性的指針

objc_property_t *properties = class_copyPropertyList([BDPerson class], &count);

for(inti =0; i < count; i ++) {              

objc_property_t property = properties[i];

//根據objc_property_t獲得其屬性的名稱--->C語言的字符串

constchar*name = property_getName(property);

NSString*key = [NSStringstringWithUTF8String:name];

//解碼每個屬性,利用kVC取出每個屬性對應的數值

[selfsetValue:[decoder decodeObjectForKey:key] forKeyPath:key]; 

}

returnself;

}

二、交換方法

通過runtime的method_exchangeImplementations(Method m1, Method m2)方法,可以進行交換方法的實現;一般用自己寫的方法(常用在自己寫的框架中,添加某些防錯措施)來替換系統的方法實現,常用的地方有:

在數組中,越界訪問程序會崩,可以用自己的方法添加判斷防止程序出現崩潰數組或字典中不能添加nil,如果添加程序會崩,用自己的方法替換系統防止系統崩潰。

代碼實現如下:

運行程序崩潰.png

添加一個分類實現方法交換.png

再次運行剛才的程序:


最終運行結果圖.png

除了獲取屬性列表之外,還有方法調用,攔截調用,動態添加方法屬性,關聯對象(添加屬性),方法交換,根據屬性值獲取屬性名稱(反射機制)等應用。

總結

runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經常會被問到的, 所以大家有必要進行研究。

參考博客:

http://blog.csdn.net/ztp800201/article/details/9240913

http://www.jianshu.com/p/613916eea37f

http://www.jianshu.com/p/ebc6e20b84cf

http://www.cocoachina.com/ios/20141018/9960.html

http://www.jianshu.com/p/364eab29f4f5

http://www.jianshu.com/p/927c8384855a

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM