神策分析 iOS SDK 代碼埋點解析 | 數(shù)據(jù)采集

一、前言

所謂埋點,是數(shù)據(jù)采集領(lǐng)域(尤其是用戶行為數(shù)據(jù)采集領(lǐng)域)的術(shù)語,指的是針對特定用戶行為或事件進行捕獲、處理和發(fā)送的相關(guān)技術(shù)及其實施過程,為進一步優(yōu)化產(chǎn)品或制定有針對性的運營計劃提供數(shù)據(jù)支撐。

?埋點的實質(zhì),是先監(jiān)聽軟件應(yīng)用運行過程中的關(guān)鍵節(jié)點,當(dāng)需要關(guān)注的事件發(fā)生時進行判斷和捕獲,獲取必要的上下文信息,最后將信息整理后發(fā)送至指定的服務(wù)端。

神策分析 iOS SDK,是一款輕量級用于 iOS 端的數(shù)據(jù)采集埋點 SDK。神策分析 iOS SDK 不僅有代碼埋點功能,還有通過使用運行時機制(Runtime)中的相關(guān)技術(shù)實現(xiàn) iOS 端的全埋點(無埋點、無碼埋點、無痕埋點、自動埋點)、點擊圖、可視化全埋點等功能。

其中,代碼埋點是最基本也是最重要的埋點方式,適用于需要精準(zhǔn)控制埋點位置、靈活的自定義事件和屬性等精細化需求的場景。下面針對神策分析 iOS SDK 代碼埋點進行詳細的介紹,希望能夠給大家提供一些參考。

二、實現(xiàn)原理

在介紹代碼埋點的實現(xiàn)原理之前,我們先來看下完整的數(shù)據(jù)采集流程,希望大家可以了解代碼埋點在數(shù)據(jù)采集流程中的作用。

2.1 數(shù)據(jù)采集流程

數(shù)據(jù)采集流程中主要包括事件采集、添加屬性、事件入庫、讀取上報等流程,詳細的步驟如下所示:

  1. 在產(chǎn)品、服務(wù)轉(zhuǎn)化的某些關(guān)鍵點,調(diào)用埋點相關(guān)接口采集事件;
  2. 獲取有意義的屬性豐富該事件,保證數(shù)據(jù)的廣度與深度;
  3. 數(shù)據(jù)采集完成,轉(zhuǎn)換成標(biāo)準(zhǔn) JSON 數(shù)據(jù)格式,以隊列的形式存儲到 SDK 的數(shù)據(jù)庫內(nèi);
  4. 定時讀取數(shù)據(jù)庫中的數(shù)據(jù),封裝請求并上報數(shù)據(jù),并在上報成功后,刪除數(shù)據(jù)庫內(nèi)存儲的已上報數(shù)據(jù)。

整體流程如圖 2-1 所示:

圖 2-1 數(shù)據(jù)采集流程圖

從圖中可以看出,代碼埋點位于數(shù)據(jù)采集流程的第一步,是數(shù)據(jù)采集流程中最關(guān)鍵的步驟。數(shù)據(jù)采集是否豐富、準(zhǔn)確、及時,都直接影響整個數(shù)據(jù)分析平臺的使用效果。

2.2 原理介紹

代碼埋點的實現(xiàn)原理比較簡單,主要是初始化 SDK 之后,在某個事件發(fā)生時調(diào)用 - track: 或 - track:withProperties: 等相關(guān)接口,將觸發(fā)的事件和屬性保存到數(shù)據(jù)模型中(SDK 中使用的是 NSDictionary 類型的數(shù)據(jù)模型)。并將數(shù)據(jù)模型轉(zhuǎn)化為 JSON 串,存儲到本地數(shù)據(jù)庫中。然后,按照發(fā)送策略將數(shù)據(jù)發(fā)送到指定的服務(wù)端。例如:我們想統(tǒng)計 App 里面某個按鈕的點擊次數(shù),可以在這個按鈕對應(yīng)的點擊方法里面調(diào)用 SDK 提供的接口來采集事件。

三、具體實現(xiàn)

在神策分析中,我們使用事件模型(Event)來描述用戶在產(chǎn)品上的各種行為,這也是神策分析中所有接口和功能設(shè)計的核心依據(jù)。簡單來說,一個 Event 就是描述了一個用戶在某個時間點、某個地方、以某種方式完成了某個具體的事情??梢钥闯?,一個完整的 Event,包含如下的幾個關(guān)鍵因素:

  • Who:參與事件的用戶是誰;

  • When:事件發(fā)生的實際時間;

  • Where:事件發(fā)生的地點;

  • How:用戶從事事件的方式;

  • What:描述用戶所做事件的具體內(nèi)容。

對于 SDK 來說,記錄用戶行為數(shù)據(jù)的接口主要考慮的就是上面這五個因素。不難看出,接口的主要功能是:在業(yè)務(wù)特定時機被調(diào)用,傳入事件名與想要記錄的屬性或者其他必要參數(shù),然后將事件記錄下來。

3.1 接口設(shè)計

一個設(shè)計良好的接口,應(yīng)該在輸入一組合理的數(shù)據(jù)時,能夠在有限的運行時間內(nèi)得到正確的結(jié)果;對不合理的數(shù)據(jù)輸入,有足夠的反應(yīng)和處理能力。參照這個思想,我們來設(shè)計記錄用戶行為數(shù)據(jù)的接口。

首先考慮接口暴露的部分。開發(fā)者在使用一個接口的時候,主要會注意以下幾點:

  1. 接口名:接口名要足夠精確,能用言簡意賅的語言描繪出該接口的功能。針對要實現(xiàn)的功能,我們把這個接口命名為 - track:withProperties: ;

  2. 參數(shù)列表:通過上述的介紹可以知道,方法調(diào)用時機可以作為事件(Event)的發(fā)生時間(When),此外我們?nèi)孕柰饨缣峁┑氖鞘录唧w內(nèi)容(What)與從事方式(How),即事件名(參數(shù) event 表示)與事件屬性(參數(shù) properties 表示);

  3. 返回值:通過該接口記錄的用戶行為數(shù)據(jù)最終需要上報到指定的服務(wù)端,所以該方法的返回值應(yīng)符合指定的服務(wù)端所要求的格式。一般來說,數(shù)據(jù)為 JSON 格式,物理上對應(yīng)一條數(shù)據(jù),邏輯上對應(yīng)一個描述了用戶行為的事件。

基于上述三點,我們的接口定義如下:

- (NSString *)track:(NSString *)event withProperties:(NSDictionary *)properties;

3.2 事件模型的關(guān)鍵因素

通過上述的介紹可以知道,事件模型(Event)中包含五個關(guān)鍵因素,下面就詳細介紹下在代碼埋點中如何獲取這五個關(guān)鍵因素。

3.2.1 用戶標(biāo)識

用戶的唯一標(biāo)識,這里用 distinct_id 表示。簡單來說,在用戶未登錄的情況下,SDK 會選取設(shè)備 ID 作為唯一標(biāo)識,而登錄狀態(tài)下會選取登錄 ID 作為唯一標(biāo)識,即一個用戶既有設(shè)備 ID(亦稱作 “匿名 ID”)又有登錄 ID,通過 “用戶關(guān)聯(lián)” 可以將同一個用戶的設(shè)備 ID 和登錄 ID 關(guān)聯(lián)到一起。這樣,不管用戶是匿名狀態(tài)還是登錄狀態(tài)發(fā)生的行為,我們都能準(zhǔn)確識別到是同一個用戶,這是目前為止較為通用且準(zhǔn)確的用戶標(biāo)識方式。

1.設(shè)備 ID

大部分情況下,一個用戶只有一臺設(shè)備,因此可以獲取其設(shè)備的 ID 來作為用戶標(biāo)識。具體到 iOS,我們可用的是 IDFA、IDFV 或者 UUID。

  • IDFA:英文全稱是 Identifier For Advertising,是廣告標(biāo)識符的縮寫,主要用于廣告推廣、換量等跨應(yīng)用的設(shè)備追蹤等。在同一個 iOS 設(shè)備上,同一時刻,所有的應(yīng)用程序獲取到的 IDFA 都是相同的。在 iOS 10 之后,若用戶限制了廣告追蹤(【設(shè)置】→ 【隱私】→【廣告】→【限制廣告追蹤】),我們獲取到的 IDFA 將是固定的一串零:00000000-0000-0000-0000-000000000000;

  • IDFV:英文全稱是 Identifier For Vendor,是應(yīng)用開發(fā)商標(biāo)識符的縮寫,是給應(yīng)用開發(fā)商標(biāo)識用戶使用的,主要適用于分析用戶在同一應(yīng)用開發(fā)商不同應(yīng)用間的行為等。在重啟設(shè)備之后和解鎖設(shè)備之前,可能獲取不到此值;

  • UUID:英文全稱是 Universally Unique Identifier,是通用唯一標(biāo)識符的縮寫,能讓你在任何一個時刻,在不借助任何服務(wù)器的情況下生成唯一標(biāo)識符。也就是說,UUID 在某一特定的時空下是全球唯一的。若 IDFA 和 IDFV 都獲取不到,則我們會生成一個 UUID,作為該設(shè)備的 ID。

結(jié)合實際情況來看,對于常規(guī)數(shù)據(jù)分析中的設(shè)備 ID,可按照 IDFA → IDFV → UUID 優(yōu)先級順序獲取,基本上能滿足我們的業(yè)務(wù)需求。

另外,為了防止限制廣告追蹤、卸載重裝等可能導(dǎo)致設(shè)備 ID 改變的情況,SDK 會將設(shè)備 ID 存儲到 KeyChain 和沙盒中,在一定程度上避免這個問題。因此,獲取設(shè)備 ID 的流程如圖 3-1 所示:

圖 3-1 獲取設(shè)備 ID 的流程圖

2.登錄 ID

一般情況下,在業(yè)務(wù)后臺系統(tǒng)中會使用登錄 ID 來標(biāo)識用戶,它識別用戶非常準(zhǔn)確,但是無法識別未登錄狀態(tài)的用戶。

在 SDK 中,通過調(diào)用 - login: 接口并傳入登錄 ID,即可完成 “用戶關(guān)聯(lián)”,將同一個用戶的設(shè)備 ID 和登錄 ID 關(guān)聯(lián)到一起。

3.唯一標(biāo)識

在 SDK 中,我們將設(shè)備 ID 定義為 anonymousId,登錄 ID 定義為 loginId,用戶唯一標(biāo)識定義為 distinctId。 獲取 distinctId 的邏輯如下:

  1. 若 loginId 不為空且長度不為 0,則返回 loginId;

  2. 若 loginId 為空,則返回 anonymousId。

3.2.2 觸發(fā)時間

在 SDK 的埋點相關(guān)接口中,使用 time 字段來記錄事件發(fā)生的時間(單位是毫秒)。若傳入的 properties 中不包含 time 字段的話,則會自動獲取當(dāng)前時間作為 time 字段的值,如下面代碼所示:

NSNumber *timeStamp = @([[NSDate date] timeIntervalSince1970] * 1000);

3.2.3 觸發(fā)地點

可以從三個方面來采集位置信息:

  1. 神策系統(tǒng)會自動根據(jù)請求的 ip 來解析相應(yīng)的省份(province)和城市(city),因此 SDK 并不需要處理這兩個屬性;

  2. SDK 可通過 CoreLocation 框架自動采集經(jīng)度(longitude)和緯度(latitude),可以在初始化 SDK 后調(diào)用 - enableTrackGPSLocation: 方法進行開啟;

  3. 開發(fā)者也可以設(shè)置一些其它地域相關(guān)的字段。例如:國家(country)、社區(qū)(HousingEstate)等。

3.2.4 從事方式

用戶從事這個事件的方式。這個概念比較寬泛,包括用戶使用的設(shè)備、瀏覽器、App 版本、操作系統(tǒng)版本、進入的渠道、跳轉(zhuǎn)過來時的 referer 等。目前,神策分析預(yù)置了部分字段用來描述這類信息,稱為預(yù)置屬性。同時,開發(fā)者也可以根據(jù)自己的需要來增加相應(yīng)的自定義字段。

3.2.5 事件內(nèi)容

描述用戶所做事件的具體內(nèi)容。主要是使用事件名稱(event),來對用戶所做的內(nèi)容進行初步的分類。除了 event 這個至關(guān)重要的字段以外,我們并沒有設(shè)置太多預(yù)置字段,需要開發(fā)者根據(jù)每個產(chǎn)品以及每個事件的實際情況和分析的需求,來進行具體的設(shè)置。

3.3 事件屬性

事件觸發(fā)時除了傳入的自定義屬性以外,還有一些特殊的屬性,可由 SDK 預(yù)先采集。例如:頁面標(biāo)題(title)、屏幕寬高(screen_height、$screen_width )等,我們稱之為預(yù)置屬性。由于這些屬性是由 SDK 自動采集的,不需要開發(fā)者增加代碼,因此極大地增加了數(shù)據(jù)采集的范圍和便利性。而采集的預(yù)置屬性本身,是數(shù)據(jù)分析中涉及到的重要分析緯度,極大降低了開發(fā)和采集的成本,是可以拿來即用的部分。

另外,如果所有事件中都需要某些相同的屬性,則可以把這些屬性注冊為公共屬性。

以上兩種特殊的事件屬性,都可以在一定程度上節(jié)約埋點成本。接下來我們將介紹這兩個屬性的實現(xiàn)方案。

3.3.1 預(yù)置屬性

考慮到 SDK 的活躍期基本上可確定為 “初始化” 與 “事件觸發(fā)” 這兩個時機,所以預(yù)置屬性也根據(jù)采集時機,大致分為兩類:

  1. SDK 初始化時采集:初始化時即可確定該屬性的值,隨后在本次 App 生命周期中不會再改變;
  2. 事件觸發(fā)時采集:調(diào)用 - track:withProperties: 時才可確定的屬性。

1.初始化時采集的屬性

最容易想到的,也是最優(yōu)的方案:在 SDK 初始化時創(chuàng)建一個存儲屬性的模型(可以使用 NSDictionary 類型),命名為 automaticProperties,采集相應(yīng)屬性置入其中,并由 SDK 持有該模型。隨后,在每次事件觸發(fā)時,將該模型中的值添加入屬性中即可。采集的預(yù)置屬性如表 3-1 所示:

表 3-1 初始化時采集的預(yù)置屬性列表

2.觸發(fā)事件時采集的屬性

由于一些預(yù)置屬性,在 App 的整個生命周期中可能發(fā)生變化,更強調(diào)實時性,因此需要在事件觸發(fā)時采集,典型代表就是之前已介紹的事件觸發(fā)時間(When)與地點(Where)。事件觸發(fā)時采集的預(yù)置屬性如表 3-2 所示:

表 3-2 事件觸發(fā)時采集的預(yù)置屬性列表

3.3.2 公共屬性

有些屬性是我們希望每個事件都帶上,但不屬于預(yù)置屬性,相當(dāng)于公共的自定義屬性。對于這些屬性,SDK 提供了兩種不同的方式來設(shè)置,即 “靜態(tài)” 與 “動態(tài)” 公共屬性。

靜態(tài)公共屬性在一次 App 生命周期中一般都是固定的;而動態(tài)公共屬性則相反,只有事件觸發(fā)的那一刻采集到的值才有意義。這實際上也對應(yīng)了預(yù)置屬性的兩個采集時機。例如:

  • 應(yīng)用名稱,在一次 App 生命周期中一般都是固定的,因此可以設(shè)置為靜態(tài)公共屬性;

  • 當(dāng)前游戲等級、最新金幣余額。顯然每次采集時這些值都是變化的,但仍然屬于公共屬性的范疇。這時候就可以使用動態(tài)公共屬性。

1.靜態(tài)公共屬性

根據(jù)上面的分析,靜態(tài)公共屬性可以這樣實現(xiàn):對外提供一個注冊靜態(tài)公共屬性的接口,開發(fā)者在 SDK 初始化時通過該接口注冊靜態(tài)公共屬性,之后在事件觸發(fā)時,將靜態(tài)公共屬性添加進去。

根據(jù) “在一次 App 生命周期中一般都是固定的” 這個特點,靜態(tài)公共屬性存儲到內(nèi)存中即可。但是在實踐中,有些靜態(tài)公共屬性在 SDK 初始化時并不能確定,需要經(jīng)過網(wǎng)絡(luò)請求或者其他操作后才能被注冊。這樣也就導(dǎo)致在注冊靜態(tài)公共屬性之前的那部分事件,是沒有靜態(tài)公共屬性的。如果每次 App 啟動后都要重復(fù)一遍上述操作,會導(dǎo)致有大量的事件帶不上靜態(tài)公共屬性,這顯然是有問題的。因此 SDK 也將注冊的靜態(tài)公共屬性持久化,并在 SDK 初始化時取出持久化的這部分靜態(tài)公共屬性,提前了靜態(tài)公共屬性的注冊時間,解決了大部分問題。不過,刪除靜態(tài)公共屬性時也需要同步清除本地持久化的內(nèi)容。

注冊靜態(tài)公共屬性的代碼如下:

[[SensorsAnalyticsSDK sharedInstance] registerSuperProperties:@{@"superKey":@"superValue"}];

2.動態(tài)公共屬性

動態(tài)公共屬性會在每次事件觸發(fā)時采集,適用于會經(jīng)常發(fā)生變化的屬性。因此,在 SDK 中動態(tài)公共屬性是通過回調(diào)(block)來實現(xiàn)的。完整的流程如下:

  1. 在 SDK 初始化時,或者其他符合業(yè)務(wù)的時機,注冊回調(diào);

  2. 回調(diào)中實現(xiàn)屬性的采集邏輯,并返回采集的屬性;

  3. 在事件觸發(fā)時,調(diào)用該回調(diào)方法,并將其返回的屬性添加到事件屬性中。

由于動態(tài)公共屬性的回調(diào)方法在每次事件觸發(fā)時都會被調(diào)用,因此不建議在該回調(diào)方法中添加過多的業(yè)務(wù)邏輯。注冊動態(tài)公共屬性的代碼如下:

[[SensorsAnalyticsSDK sharedInstance] registerDynamicSuperProperties:^NSDictionary<NSString *,id> * _Nonnull{

3.3.3 屬性的優(yōu)先級

目前各種屬性按照優(yōu)先級從高到低的排序為:

  1. 事件觸發(fā)時傳入的自定義屬性;

  2. 動態(tài)公共屬性;

  3. 靜態(tài)公共屬性;

  4. 預(yù)置屬性。

不難看出,排序的核心思想是按照 “自定義” 的優(yōu)先級來排序:

  1. properties 僅代表本次觸發(fā)的事件,自定義程度最高;

  2. 動態(tài)公共屬性具有實時性,比靜態(tài)公共屬性優(yōu)先級高;

  3. 預(yù)置屬性是純粹的 SDK 行為,故優(yōu)先級最低。

3.4 數(shù)據(jù)校驗

數(shù)據(jù)校驗的內(nèi)容分為:

  1. 參數(shù)是否為空、類型是否正確等;

  2. 參數(shù)是否符合神策的數(shù)據(jù)格式要求。神策使用統(tǒng)一的數(shù)據(jù)格式,因此任何自定義內(nèi)容都應(yīng)經(jīng)過校驗來保證輸出的 JSON 是符合要求的。具體而言,是對事件名、自定義屬性、靜態(tài)公共屬性、動態(tài)公共屬性等做校驗。

數(shù)據(jù)校驗的時機分為:

  1. 靜態(tài)公共屬性應(yīng)在被注冊的時候檢查;

  2. 動態(tài)公共屬性與自定義屬性應(yīng)在事件觸發(fā)時檢查。

3.4.1 基本限制

事件名(event 的值)和 屬性名(properties 中 key 的取值)都需是合法的變量名,即不能以數(shù)字開頭,同時只能包含:大小寫字母、數(shù)字、下劃線和 $。另外,事件名和屬性名最大長度都為 100。上述限制條件在 SDK 中是通過正則表達式來實現(xiàn)的。

SDK 預(yù)留了部分字段作為預(yù)置事件與屬性名,自定義事件與屬性都需要避免相同,判斷事件名和屬性名是否合法的代碼如下所示:

- (BOOL)isValidName:(NSString *)name {

3.4.2 類型限制

SDK 的數(shù)據(jù)類型目前支持五種:數(shù)值型、布爾值、字符串、字符串?dāng)?shù)組、日期時間,對應(yīng)到代碼中即是 NSNumber、NSString、NSSet、NSArray、NSDate,其它類型的數(shù)據(jù)將會被拒絕。這里需要注意的是:

  • 在 SDK 中,布爾型與數(shù)值型一樣使用的是 NSNumber 類型。在轉(zhuǎn)為 JSON 后,布爾型的 NSNumber 會被轉(zhuǎn)為 true 或 false,而數(shù)值型的 NSNumber 會被轉(zhuǎn)為實際的數(shù)值;

  • NSSet 與 NSArray 都代表數(shù)據(jù)集合,只是無序與有序的區(qū)別。因此,這兩種類型都可代表字符串?dāng)?shù)組;

  • NSNull 類型會被單獨處理,它不會導(dǎo)致整條數(shù)據(jù)被丟棄,只會丟棄該鍵值對。

對于不同類型的屬性值,也會有各自單獨的校驗,如下所示:

  • NSString:對于字符串,需檢查其長度是否大于最大長度 8191。如果大于最大長度,會刪掉超出長度的部分,并拼接 $ 代表后續(xù)內(nèi)容已截斷。其中,App 崩潰事件(AppCrashed)的崩潰原因?qū)傩裕?app_crashed_reason)其值為崩潰的堆棧,通常都比較長,故其長度限制設(shè)定為常規(guī)值的兩倍;

  • NSSet 與 NSArray:代表字符串?dāng)?shù)組,會遍歷每個對象,檢查是否都為 NSString 類型,不是的話會刪掉該對象;

  • NSDate:由于 SDK 數(shù)據(jù)格式支持的日期時間實際為 JSON 中固定格式的字符串,所以對于 NSDate,會使用 NSDateFormatter 將其按格式序列化為字符串。

四、使用場景

要了解代碼埋點的使用場景,先來看下代碼埋點的優(yōu)缺點,盡量揚長避短。

優(yōu)點:

  • 原理簡單,學(xué)習(xí)成本較低;

  • 使用較為靈活,能夠根據(jù)業(yè)務(wù)特性自定義時機、屬性、事件,定制化獲取數(shù)據(jù)。

缺點:

  • 埋點成本高,每一個控件的埋點都需要添加相應(yīng)的代碼,不僅工作量大,而且限定了必須是技術(shù)人員才能完成;

  • 版本更新前后,容易發(fā)生數(shù)據(jù)紊亂;

  • 需要企業(yè)長期且穩(wěn)定地完善埋點,并不斷根據(jù)業(yè)務(wù)來更新。

根據(jù)上述的優(yōu)缺點可以知道:代碼埋點使用較為靈活,但是成本較高。因此,最好在全埋點、可視化全埋點等埋點方案無法解決問題時,或者更強調(diào)自定義的場景時來使用。例如:

  1. App 的整體日活,App 元素點擊的每日次數(shù),可使用全埋點;

  2. App 某個指定按鈕的點擊事件,某個特定頁面的頁面瀏覽事件,可使用可視化全埋點;

  3. 若對于業(yè)務(wù)統(tǒng)計要求非常準(zhǔn)確,安全性要求比較高的用戶數(shù)據(jù),例如注冊、支付成功,可使用服務(wù)器埋點;

  4. 以上方案解決不了,或者自定義的內(nèi)容較多,例如加入購物車、提交訂單等,可使用代碼埋點。

五、總結(jié)

代碼埋點是整個神策分析 iOS SDK 的基礎(chǔ)與核心,它足夠豐富穩(wěn)定,可以讓我們在使用全埋點與存儲上報等功能時無后顧之憂。希望大家通過這篇文章,能夠?qū)ι癫叻治?iOS SDK 的代碼埋點有一個全面的了解。

注:本文埋點釋義參考鏈接,如下

https://www.zhihu.com/question/36411025/answer/144973846

本文作者

image

張占凱神策數(shù)據(jù) | SDK 技術(shù)顧問

我是張占凱,熱愛技術(shù),目前專注于 iOS 領(lǐng)域。工作之余喜好讀書,對各個領(lǐng)域的知識都有所涉獵。希望與大家在開源社區(qū)中共同探討,共同進步。

本文著作權(quán)歸「神策數(shù)據(jù)開源社區(qū)」所有,商業(yè)轉(zhuǎn)載請聯(lián)系我們獲得授權(quán);非商業(yè)轉(zhuǎn)載請注明出處,并附上神策數(shù)據(jù)開源社區(qū)服務(wù)號二維碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容