iOS 基礎知識概述

iOS 基礎知識概述

基本修飾屬性

  • assion
    -基本用于修飾基本數據類型 如 int 等 是弱引用
  • copy
    • copy 修飾不可變對象 和strong 修飾符 一樣對當前的對象進行一個強引用 copy 修飾可變對象 會對當前對象 進行深拷貝 生成一個不可變對象
    • 追問?用strong 修飾 會有什么問題 ?用strong 修飾可變對象 在某些可能會發生數據被修改風險 這個根據需求進行判斷
    • 還可用用于對于block 進行修飾 本質意義要從棧上復制到堆上
    • 為什么 要把block 從棧上 復制到堆上?延長block的生命周期
  • strong
    • strong 是一個強引用 會對當前對象保證在合適的生命周期不會銷毀
  • weak
    • weak 弱引用 不會增加引用計數 一般用于防止循環引用使用
  • notatomic
    • 非原子性 就不會保證線程讀寫安全
  • atomic
    • 原子性 在修飾對象時 會對當前對象set 和 get 方法進行加鎖 保存讀寫安全

weak 實現原理

  • 當我們用weak 修飾對象時 會調用initWeak方法 判斷對象是否為nil 不為nil 初始化一個新的weak指針對象的地址 調用storeWeak方法 更新指針指向 創建對應的弱引用表 釋放時 調用clearDeallocating 函數 會根據對象的地址獲取所有weak指針地址數組 ,然后遍歷這個數據把期中的數據設置為nil 最后把這個從數據從這個weak表中 清理對象記錄。
  • weak 相關問題

**Block **

  • 本質是一個oc 對象
  • Block 變量截取
    • 自動變量值被Block 截獲 只能執行Block語法瞬間值。保存后就不能修改此值。Block 中使用自動變量后 在Block 的結構體實例中重寫改自動變量也不會改變原先截獲的自動變量
  • 截獲對象
    • 而在ARC環境下,對于聲明為__block的外部對象,在block內部會進行retain,以至于在block環境內能安全的引用外部對象。對于沒有聲明__block的外部對象,在block中也會被retain。

KVO和KVC 實現原理

  • 當某個類的屬性對象第一次被觀察時,系統就會在運行期動態地創建該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的setter 方法。派生類在被重寫的setter方法內實現真正的通知機制
    如果原類為ClassName,那么生成的派生類名為NSKVONotifying_ClassName
    每個類對象中都有一個isa指針指向當前類,當一個類對象的第一次被觀察,那么系統會偷偷將isa指針指向動態生成的派生類,從而在給被監控屬性賦值時執行的是派生類的setter方法
    鍵值觀察通知依賴于NSObject 的兩個方法: willChangeValueForKey: 和 didChangevlueForKey:;在一個被觀察屬性發生改變之前, willChangeValueForKey:一定會被調用,這就 會記錄舊的值。而當改變發生后,didChangeValueForKey:會被調用,繼而 observeValueForKey:ofObject:change:context: 也會被調用。
    補充:KVO的這套實現機制中蘋果還偷偷重寫了class方法,讓我們誤認為還是使用的當前類,從而達到隱藏生成的派生類
  • kvc
    • KVC(key-Value coding) 鍵值編碼,指iOS開發中,可以允許開發者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。不需要調用明確的存取方法,這樣就可以在運行時動態訪問和修改對象的屬性,而不是在編譯時確定。
    • 程序優先調用setKey:屬性值方法,代碼通過setter方法完成設置。注意,這里的key是指成員變量名,首字母大小寫要符合KVC的命名規范,下同
      如果沒有找到setName:方法,KVC機制會檢查+(BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認返回的是YES,如果你重寫了該方法讓其返回NO,那么在這一步KVC會執行setValue: forUndefineKey:方法,不過一般不會這么做。所以KVC機制會搜索該類里面有沒有名為_key的成員變量,無論該變量是在.h,還是在.m文件里定義,也不論用什么樣的訪問修飾符,只要存在_key命名的變量,KVC都可以對該成員變量賦值。
      如果該類既沒有setKey:方法,也沒有_key成員變量,KVC機制會搜索_isKey的成員變量。
      同樣道理,如果該類沒有setKey:方法,也沒有_key和_isKey成員變量,KVC還會繼續搜索key和isKey的成員變量,再給他們賦值。
      如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。

runtime

對象:OC中的對象指向的是一個objc_object指針類型,typedef struct objc_object *id;從它的結構體中可以看出,它包括一個isa指針,指向的是這個對象的類對象,一個對象實例就是通過這個isa找到它自己的Class,而這個Class中存儲的就是這個實例的方法列表、屬性列表、成員變量列表等相關信息的

類:在OC中的類是用Class來表示的,實際上它指向的是一個objc_class的指針類型,typedef struct objc_class *Class

OC的Class類型包括如下數據(即:元數據metadata):super_class(父類類對象);name(類對象的名稱);version、info(版本和相關信息);instance_size(實例內存大小);ivars(實例變量列表);methodLists(方法列表);cache(緩存);protocols(實現的協議列表);

當然也包括一個isa指針,這說明Class也是一個對象類型,所以我們稱之為類對象,這里的isa指向的是元類對象(metaclass),元類中保存了創建類對象(Class)的類方法的全部信息。

為什么要設計metaclass
類對象、元類對象能夠復用消息發送流程機制;
單一職責原則

為什么對象方法沒有保存的對象結構體里,而是保存在類對象的結構體里?
方法是每個對象互相可以共用的,如果每個對象都存儲一份方法列表太浪費內存,由于對象的isa是指向類對象的,當調用的時候,直接去類對象中查找就行了。可以節約很多內存空間的

class_ro_t 和 class_rw_t 的區別?
class_rw_t提供了運行時對類拓展的能力,而class_ro_t存儲的大多是類在編譯時就已經確定的信息。二者都存有類的方法、屬性(成員變量)、協議等信息,不過存儲它們的列表實現方式不同。簡單的說class_rw_t存儲列表使用的二維數組,class_ro_t使用的一維數組。 class_ro_t存儲于class_rw_t結構體中,是不可改變的。保存著類的在編譯時就已經確定的信息。而運行時修改類的方法,屬性,協議等都存儲于class_rw_t中

category如何被加載的,兩個category的load方法的加載順序,兩個category的同名方法的加載順序

+load 方法是 images 加載的時候調用,假設有一個 XXXClass 類,其主類和所有分類的 +load 都會被調用,優先級是先調用主類,且如果主類有繼承鏈,那么加載順序還必須是基類的 +load ,接著是父類,最后是子類;category 的 +load 則是按照編譯順序來的,先編譯的先調用,后編譯的后調用,可在 Xcode 的 BuildPhase 中查看
分類添加到了 rw = cls->data() 中的 methods/properties/protocols 中,實際上并無覆蓋,只是查找到就返回了,導致本類函數無法加載。

initialize && Load

類第一次被使用到的時候會被調用,底層實現有個邏輯先判斷父類是否被初始化過,沒有則先調用父類,然后在調用當前類的 initialize 方法.

一個類 A 存在多個 category ,且 category中各自實現了 initialize 方法,這時候走的是 消息發送流程,也就說 initialize 方法只會調用一次,也就是最后編譯的那個category中的 initialize 方法。
如果+load 方法中調用了其他類:比如 B 的某個方法,其實就是走消息發送流程,由于 B 沒有初始化過,則會調用其 initialize 方法,但此刻 B 的 +load 方法可能還沒有被系統調用過。

方法查詢-> 動態解析-> 消息轉發
【self test】會轉換成 objc_megsend方法 檢測當前targte 是否為nil 如果為nil則忽略
- 不是 會從當前對象 方法列表里面查找方法 如果找到了 就直接調用執行 如果沒有找到就會去父類方法列表里面查找 如果還沒有找到 就根類方法列表查找 如果還沒有找到 就會走消息轉發流程
- 通過resolveInstanceMethod 得知方法是否動態添加,YES則通過 class_addMethod 動態添加方法,處理消息,否則進入下一步、
- forwardingTargetForSelect 用于指定那個對象來響應消息。如果返回nil 則進入第三步
- methodSignatureForSelector 進行方法簽名,可以將函數參數類型和返回值封裝。如果返回nil 說明消息無法處理并報錯
- 把 imp 指向_objc_msgForward函數指針 最后執行這個IMP
runLoop

線程和 RunLoop 之間是一一對應的,其關系是保存在一個全局的 Dictionary 里。線程剛創建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷毀是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。
mode
同時蘋果還提供了一個操作 Common 標記的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用這個字符串來操作 Common Items,或標記一個 Mode 為 “Common”。使用時注意區分這個字符串和其他 mode name。

  1. kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
  2. UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
  3. kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。

PerformSelecter
當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。

當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。

GCD

  • GCD中常用的函數有哪些及使用場景
    • dispatch_async 開啟一個異步的網絡請求
    • dispatch_after 簡單的延遲執行的方式
    • dispatch_once 只執行一次的代碼 創建單例
    • dispatch_group 維護一些異步任務的同步問題
    • dispatch_barrier_async 文件的讀寫操作時使用,保證讀操作的準確性。另外,有一點需要注意,dispatch_barrier_sync和dispatch_barrier_async只在自己創建的并發隊列上有效,在全局(Global)并發隊列、串行隊列上,效果跟dispatch_(a)sync效果一樣。
    • dispatch_semaphore_signal
  • 同步、異步、串行、并行的區別
    • 同步 不開啟線程
    • 異步 開啟線程
    • 同步 串行 不開啟線程 按順序執行
    • 異步 串行 開啟線程 按順序執行
    • 同步 并行 不會新建線程 按順序執行
    • 異步 并行 會開多條線程 操作無序
      11
    • dispatch_async(dispatch_get_main(), ^{})的實現原理
      - dispatch_async 指的是將指定的Block 異步的追加到指定的queue中 這個函數不做任何等待
      • dispatch_sync 將指定的block 同步追加到指定的queue 中在追加之后 dispatch——sync 會一直等待
    • performSelector:..afterDelay:的實現原理
      - 內部會創建一個Timer 并添加到當前線程的RunLoop 如果在子線程下 調用方法 是不會執行的 子線程Runloop 默認是不開啟的
    • runloop與線程的關系
      • 線程和Runloop 之間是一一對應的,其關系是保存在一個全局的字典里。線程剛創建沒有Runloop 。Runloop 的創建是發生在第一次獲取時 Runloop 的銷毀時發生在線程結束時 只能在一個線程內部獲取其Runloop(主線程除外)

當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執行這個 block。但這個邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。

自動釋放池(autoreleasepool)

Autorelease Pool 是由多個 AutoreleasePoolPage 對象以雙向鏈表的方式組織起來的數據結構。

每個 AutoreleasePoolPage 只能存儲有限個對象指針。當新的對象加入 Autorelease Pool 的時候,如果當前的 AutoreleasePoolPage 存儲空間不夠,會新初始化一個 AutoreleasePoolPage,加入到鏈表末端。

Autorelease Pool 可以被嵌套創建。創建一個新的 Autorelease Pool 的時候,會在當前 AutoreleasePoolPage 中插入邊界對象 POOL_BOUNDARY,以和上一個 Autorelease Pool 以區分。

當 Autorelease Pool 銷毀的時候,對 AutoreleasePoolPage 里存儲的所有對象依次從后往前調用 release,直到遇到對象 POOL_BOUNDARY,表明當前 Autorelease Pool 中的對象已經被全部釋放。

App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。

第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。

在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。

Autoreleasepool是由多個AutoreleasePoolPage以雙向鏈表的形式連接起來的,

Autoreleasepool的基本原理:在每個自動釋放池創建的時候,會在當前的AutoreleasePoolPage中設置一個標記位,在此期間,當有對象調用autorelsease時,會把對象添加到AutoreleasePoolPage中,若當前頁添加滿了,會初始化一個新頁,然后用雙向量表鏈接起來,并把新初始化的這一頁設置為hotPage,當自動釋放池pop時,從最下面依次往上pop,調用每個對象的release方法,直到遇到標志位。 AutoreleasePoolPage結構如下

class AutoreleasePoolPage {
magic_t const magic;
id *next;//下一個存放autorelease對象的地址
pthread_t const thread; //AutoreleasePoolPage 所在的線程
AutoreleasePoolPage * const parent;//父節點
AutoreleasePoolPage *child;//子節點
uint32_t const depth;//深度,也可以理解為當前page在鏈表中的位置
uint32_t hiwat;
}

GCD

  • dispatch_sync 將任務 block 通過 push 到隊列中,然后按照 FIFO 去執行。
  • dispatch_sync造成死鎖的主要原因是堵塞的tid和現在運行的tid為同一個
  • dispatch_async 會把任務包裝并保存,之后就會開辟相應線程去執行已保存的任務。
  • semaphore 主要在底層維護一個value的值,使用 signal 進行 + +1,wait進行-1。如果value的值大于或者等于0,則取消阻塞,否則根據timeout參數進行超時判斷
  • dispatch_group 底層也是維護了一個 value 的值,等待 group 完成實際上就是等待value恢復初始值。而notify的作用是將所有注冊的回調組裝成一個鏈表,在 dispatch_async 完成時判斷 value 是不是恢復初始值,如果是則調用dispatch_async異步執行所有注冊的回調。
  • dispatch_once 通過一個靜態變量來標記 block 是否已被執行,同時使用加鎖確保只有一個線程能執行,執行完 block 后會喚醒其他所有等待的線程。
    列舉你知道的線程同步策略?
    OSSpinLock 自旋鎖,已不再安全,除了這個鎖之外,下面寫的鎖,在等待時,都會進入線程休眠狀態,而非忙等
    os_unfair_lock atomic就是使用此鎖來保證原子性的
    pthread_mutex_t 互斥鎖,并且支持遞歸實現和條件實現
    NSLock,NSRecursiveLock,基本的互斥鎖,NSRecursiveLock支持遞歸調用,都是對pthread_mutex_t的封裝
    NSCondition,NSConditionLock,條件鎖,也都是對pthread_mutex_t的封裝
    dispatch_semaphore_t 信號量
    @synchronized 也是pthread_mutex_t的封裝
    有哪幾種鎖?各自的原理?它們之間的區別是什么?最好可以結合使用場景來說
    自旋鎖:自旋鎖在無法進行加鎖時,會不斷的進行嘗試,一般用于臨界區的執行時間較短的場景,不過iOS的自旋鎖OSSpinLock不再安全,主要原因發生在低優先級線程拿到鎖時,高優先級線程進入忙等(busy-wait)狀態,消耗大量 CPU 時間,從而導致低優先級線程拿不到 CPU 時間,也就無法完成任務并釋放鎖。這種問題被稱為優先級反轉。
    互斥鎖:對于某一資源同時只允許有一個訪問,無論讀寫,平常使用的NSLock就屬于互斥鎖
    讀寫鎖:對于某一資源同時只允許有一個寫訪問或者多個讀訪問,iOS中pthread_rwlock就是讀寫鎖
    條件鎖:在滿足某個條件的時候進行加鎖或者解鎖,iOS中可使用NSConditionLock來實現
    遞歸鎖:可以被一個線程多次獲得,而不會引起死鎖。它記錄了成功獲得鎖的次數,每一次成功的獲得鎖,必須有一個配套的釋放鎖和其對應,這樣才不會引起死鎖。只有當所有的鎖被釋放之后,其他線程才可以獲得鎖,iOS可使用NSRecursiveLock來實現

哪些場景可以觸發離屏渲染?(知道多少說多少)
添加遮罩mask
添加陰影shadow
設置圓角并且設置masksToBounds為true
設置allowsGroupOpacity為true并且layer.opacity小于1.0和有子layer或者背景不為空
開啟光柵化shouldRasterize=true

響應鏈
當一個事件發生后,事件會從父控件傳給子控件,也就是說由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的傳遞,也就是尋找最合適的view的過程。

2、接下來是事件的響應。首先看initial view能否處理這個事件,如果不能則會將事件傳遞給其上級視圖(inital view的superView);如果上級視圖仍然無法處理則會繼續往上傳遞;一直傳遞到視圖控制器view controller,首先判斷視圖控制器的根視圖view是否能處理此事件;如果不能則接著判斷該視圖控制器能否處理此事件,如果還是不能則繼續向上傳 遞;(對于第二個圖視圖控制器本身還在另一個視圖控制器中,則繼續交給父視圖控制器的根視圖,如果根視圖不能處理則交給父視圖控制器處理);一直到 window,如果window還是不能處理此事件則繼續交給application處理,如果最后application還是不能處理此事件則將其丟棄

MVC和MVVM的區別?MVVM和MVP的區別?
另一個 MVP 與 MVC 之間的重大區別就是,MVP(Passive View)中的視圖和模型是完全解耦的,它們對于對方的存在完全不知情,這也是區分 MVP 和 MVC 的一個比較容易的方法。

無論是 MVVM 還是 Presentation Model,其中最重要的不是如何同步視圖和展示模型/視圖模型之間的狀態,是使用觀察者模式、雙向綁定還是其它的機制都不是整個模式中最重要的部分,最為關鍵的是展示模型/視圖模型創建了一個視圖的抽象,將視圖中的狀態和行為抽離出一個新的抽象,這才是 MVVM 和 PM 中需要注意的。

面向對象的幾個設計原則了解么?最好可以結合場景來說。
對于設計模式的六大設計原則,單一職責原則主要說明類的職責要單一;里氏替換原則強調不要破壞繼承體系;依賴倒置原則描述要面向接口編程;接口隔離原則講解設計接口的時候要精簡;迪米特法則告訴我們要降低耦合;開閉原則講述的是對擴展開放,對修改關閉。

可以說幾個重構的技巧么?你覺得重構適合什么時候來做?

了解編譯的過程么?分為哪幾個步驟?
預編譯:主要處理以“#”開始的預編譯指令。
編譯:
詞法分析:將字符序列分割成一系列的記號。
語法分析:根據產生的記號進行語法分析生成語法樹。
語義分析:分析語法樹的語義,進行類型的匹配、轉換、標識等。
中間代碼生成:源碼級優化器將語法樹轉換成中間代碼,然后進行源碼級優化,比如把 1+2 優化為 3。中間代碼使得編譯器被分為前端和后端,不同的平臺可以利用不同的編譯器后端將中間代碼轉換為機器代碼,實現跨平臺。
目標代碼生成:此后的過程屬于編譯器后端,代碼生成器將中間代碼轉換成目標代碼(匯編代碼),其后目標代碼優化器對目標代碼進行優化,比如調整尋址方式、使用位移代替乘法、刪除多余指令、調整指令順序等。
匯編:匯編器將匯編代碼轉變成機器指令。
靜態鏈接:鏈接器將各個已經編譯成機器指令的目標文件鏈接起來,經過重定位過后輸出一個可執行文件。
裝載:裝載可執行文件、裝載其依賴的共享對象。
動態鏈接:動態鏈接器將可執行文件和共享對象中需要重定位的位置進行修正。
最后,進程的控制權轉交給程序入口,程序終于運行起來了。

靜態鏈接了解么?靜態庫和動態庫的區別?
靜態庫:鏈接時完整地拷貝至可執行文件中,被多次使用就有多份冗余拷貝。

動態庫:鏈接時不復制,程序運行時由系統動態加載到內存,供程序調用,系統只加載一次,多個程序共用,節省內存。

內存的幾大區域,各自的職能分別是什么?

棧區:有系統自動分配并釋放,一般存放函數的參數值,局部變量等
堆區:有程序員分配和釋放,若程序員未釋放,則在程序結束時有系統釋放,在iOS里創建出來的對象會放在堆區
數據段:字符串常量,全局變量,靜態變量
代碼段:編譯之后的代碼

TCP為什么要三次握手,四次揮手?

HTTPS是如何實現驗證身份和驗證完整性的?

使用數字證書和CA來驗證身份,首先服務端先向CA機構去申請證書,CA審核之后會給一個數字證書,里面包裹公鑰、簽名、有效期,用戶信息等各種信息,在客戶端發送請求時,服務端會把數字證書發給客戶端,然后客戶端會通過信任鏈來驗證數字證書是否是有效的,來驗證服務端的身份。

使用摘要算法來驗證完整性,也就是說在發送消息時,會對消息的內容通過摘要算法生成一段摘要,在收到收到消息時也使用同樣的算法生成摘要,來判斷摘要是否一致。

tabView的優化

  • TableViewCell 復用 在cellForRowAtIndexPath:回調的時候只創建實例,快速返回cell,不綁定數據。在willDisplayCell: forRowAtIndexPath:的時候綁定數據(賦值)。
  • 高度緩存 UITableView-FDTemplateLayoutCell
  • 減少多余的繪制操作 盡可能將多張圖片合成為一張進行顯示。 優化圖片大小,盡量不要動態縮放(contentMode)。
  • 減少離屏渲染 觸發離屏渲染: layer.shouldRasterize,光柵化 layer.mask,遮罩 layer.allowsGroupOpacity為YES,layer.opacity的值小于1.0 layer.cornerRadius,并且設置layer.masksToBounds為YES。可以使用剪切過的圖片,或者使用layer畫來解決。
  • 離屏渲染的優化建議
    使用ShadowPath指定layer陰影效果路徑。
    使用異步進行layer渲染(Facebook開源的異步繪制框架AsyncDisplayKit)。
    設置layer的opaque值為YES,減少復雜圖層合成。
    盡量使用不包含透明(alpha)通道的圖片資源。
    盡量設置layer的大小值為整形值。
    直接讓美工把圖片切成圓角進行顯示,這是效率最高的一種方案。
    很多情況下用戶上傳圖片進行顯示,可以在客戶端處理圓角。
    使用代碼手動生成圓角image設置到要顯示的View上,利用UIBezierPath(Core Graphics框架)畫出來圓角圖片。
  • 異步渲染
  • 按需加載
    利用runloop提高滑動流暢性,在滑動停止的時候再加載內容,像那種一閃而過的(快速滑動),就沒有必要加載,可以使用默認的占位符填充內容。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容