OCLint 實現(xiàn) Code Review - 給你的代碼提提質(zhì)量

工程代碼質(zhì)量,一個永恒的話題。好的質(zhì)量的好處不言而喻,團(tuán)隊成員間除了保持統(tǒng)一的風(fēng)格和較高的自我約束力之外,還需要一些工具來統(tǒng)計分析代碼質(zhì)量問題。

本文就是針對 OC 項目,提出的一個思路和實踐步驟的記錄,最后形成了一個可以直接用的腳本。如果覺得文章篇幅過長,則直接可以下載腳本

OCLint is a static code analysis tool for improving quality and reducing defects by inspecting C, C++ and Objective-C code and looking for potential problems ...

從官方的解釋來看,它通過檢查 C、C++、Objective-C 代碼來尋找潛在問題,來提高代碼質(zhì)量并減少缺陷的靜態(tài)代碼分析工具

一、OCLint 的下載和安裝

有3種方式安裝,分別為 Homebrew、源代碼編譯安裝、下載安裝包安裝。

區(qū)別:

如果需要自定義 Lint 規(guī)則,則需要下載源碼編譯安裝

如果僅僅是使用自帶的規(guī)則來 Lint,那么以上3種安裝方式都可以

1. Homebrew 安裝

在安裝前,確保安裝了 homebrew。步驟簡單快捷

brew tap oclint/formulae?

brew install oclint

2. 安裝包安裝

進(jìn)入 OCLint 在 Github 中的地址,選擇 Release。選擇最新版本的安裝包(目前最新版本為:oclint-0.13.1-x86_64-darwin-17.4.0.tar.gz)

解壓下載文件。將文件存放到一個合適的位置。(比如我選擇將這些需要的源代碼存放到 Document 目錄下)

在終端編輯當(dāng)前環(huán)境的配置文件,我使用的是 zsh,所以編輯 .zshrc 文件。(如果使用系統(tǒng)的終端則編輯 .bash_profile 文件)

OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release

export PATH=$OCLint_PATH/bin:$PATH

將配置文件 source 一下。

source .zshrc // 如果你使用系統(tǒng)的終端則執(zhí)行 soucer .bash_profile

驗證是否安裝成功。在終端輸入oclint --version

3. 源碼編譯安裝

homebrew 安裝 CMake 和 Ninja 這2個編譯工具

brew install cmake ninja

進(jìn)入 Github 搜索 OCLint,clone 源碼

gc https://github.com/oclint/oclint

或者:git clone https://github.com/oclint/oclint.git

進(jìn)入 oclint-scripts 目錄,執(zhí)行 ./make 命令。這一步的時間非常長。會下載 oclint-json-compilation-database、oclint-xcodebuild、llvm 源碼以及 clang 源碼。并進(jìn)行相關(guān)的編譯得到 oclint。且必須使用翻墻環(huán)境不然會報 timeout。如果你的電腦支持翻墻環(huán)境,但是在終端下不支持翻墻,可以查看我的這篇文章

./make

編譯結(jié)束,進(jìn)入同級 build 文件夾,該文件夾下的內(nèi)容即為 oclint??梢钥吹絙uild/oclint-release。方式2下載的安裝包的內(nèi)容就是該文件夾下的內(nèi)容。

cd 到根目錄,編輯環(huán)境文件,比如我 zsh 對應(yīng)的 .zshrc 文件。編輯下面的內(nèi)容

? OCLint_PATH=/Users/liubinpeng/Desktop/oclint/build/oclint-release

? export PATH=$OCLint_PATH/bin:$PATH

source 下 .zshrc 文件

source .zshrc // source .bash_profile

進(jìn)入oclint/build/oclint-release目錄執(zhí)行腳本

cp ~/Documents/oclint/build/oclint-release/bin/oclint* /usr/local/bin/

ln -s ~/Documents/oclint/build/oclint-release/lib/oclint /usr/local/lib

ln -s ~/Documents/oclint/build/oclint-release/lib/clang /usr/local/lib

這里使用 ln -s,把 lib 中的 clang 和 oclint 鏈接到 /usr/local/bin 目錄下。這樣做的目的是為了后面如果編寫了自己創(chuàng)建的 lint 規(guī)則,不必要每次更新自定義的 rule 庫,必須手動復(fù)制到 /usr/local/bin 目錄下。

驗證下 OCLint 是否安裝成功。輸入 oclint --version


驗證下 OCLint 是否安裝成功

注意:如果你采用源碼編譯的時候直接 clone 官方的源碼會有問題,編譯不過,所以提供了一個可以編譯過的版本。分支切換到 llvm-7.0。

4. xcodebuild 的安裝

xcode 下載安裝好就已經(jīng)成功安裝了

5. xcpretty 的安裝

先決條件,你的機(jī)器已經(jīng)安裝好了 Ruby gem.

gem install xcpretty

二、 自定義 Rule

OClint 提供了 70+ 項的檢查規(guī)則,你可以直接去使用。但是某些時候你需要制作自己的檢測規(guī)則,接下來就說說如何自定義 lint 規(guī)則。

1、進(jìn)入 ~/Document/oclint 目錄,執(zhí)行下面的腳本

oclint-scripts/scaffoldRule CustomLintRules -t ASTVisitor

其中,CustomLintRules就是定義的檢查規(guī)則的名字,ASTVisitor就是你繼承的 lint 規(guī)則

可以繼承的規(guī)則有:ASTVisitor、SourceCodeReader、ASTMatcher。

2、執(zhí)行上面的腳本,會生成下面的文件

Documents/oclint/oclint-rules/rules/custom/CustomLintRulesRule.cpp

Documents/oclint/oclint-rules/test/custom/CustomLintRulesRuleTest.cpp

3、要方便的開發(fā)自定義的 lint 規(guī)則,則需要生成一個 xcodeproj 項目。切換到項目根目錄,也就是 Documents/oclint,執(zhí)行下面的命令

mkdir Lint-XcodeProject

cd Lint-XcodeProject

touch generate-lint-rules.sh

chmod +x generate-lint-rules.sh

給上面的 generate-lint-rules.sh 里面添加下面的腳本

#! /bin/sh -ecmake -G Xcode \? -D CMAKE_CXX_COMPILER=../build/llvm-install/bin/clang++? \? -D CMAKE_C_COMPILER=../build/llvm-install/bin/clang \? -D OCLINT_BUILD_DIR=../build/oclint-core \? -D OCLINT_SOURCE_DIR=../oclint-core \? -D OCLINT_METRICS_SOURCE_DIR=../oclint-metrics \? -D OCLINT_METRICS_BUILD_DIR=../build/oclint-metrics \? -D LLVM_ROOT=../build/llvm-install/ ../oclint-rules

4、執(zhí)行 generate-lint-rules.sh 腳本(./generate-lint-rules.sh)。如果出現(xiàn)下面的 Log 則說明生成 xcodeproj 項目成功

生成 xcodeproj 項目成功
執(zhí)行 generate-lint-rules.sh 腳本的log

5、打開步驟4生成的項目,看到有很多文件夾,代表 oclint 自帶的 lint 規(guī)則,我們自定義的 lint 規(guī)則在最下面。

看到有很多文件夾,代表 oclint 自帶的 lint 規(guī)則,我們自定義的 lint 規(guī)則在最下面

關(guān)于如何自定義 lint 規(guī)則的具體還沒有深入研究,這里給個例子

<details>

<summary>點擊查看示例代碼</summary>

#include"oclint/AbstractASTVisitorRule.h"

#include"oclint/RuleSet.h"

using namespace std;

using namespace clang;

using namespace oclint;

#include <iostream>

class MVVMRule : public AbstractASTVisitorRule<MVVMRule>

{

public:

? ? virtual conststring name() const override {

? ? ? ? return"Property in 'ViewModel' Class interface should be readonly.";

? ? }

? ? virtual intpriority() const override {

? ? ? ? return3;

? ? }

? ? virtual conststring category() const override {

? ? ? ? return"mvvm";

? ? }


? ? virtual unsigned intsupportedLanguages() const override {

? ? ? ? return LANG_OBJC;

? ? }

#ifdef DOCGEN

? ? virtual conststd::string since() const override {

? ? ? ? return"0.18.10";

? ? }

?virtual conststd::string description() const override {

? ? ? ? return"Property in 'ViewModel' Class interface should be readonly.";

? ? }

? ? virtual conststd::string example() const override {

? ? ? ? returnR"rst( .. code-block:: cpp @interface FooViewModel : NSObject // This is a "ViewModel" Class. @property (nonatomic, strong) NSObject *bar; // should be readonly. @end )rst";

? ? }

? ? virtual conststd::string fileName() const override {

? ? ? ? return"MVVMRule.cpp";

? ? }

#endif

? ? virtual voidsetUp() override {}

? ? virtual voidtearDown() override {}

? ? /* Visit ObjCImplementationDecl */

? ? boolVisitObjCImplementationDecl(ObjCImplementationDecl *node) {

? ? ? ? ObjCInterfaceDecl *interface = node->getClassInterface();


? ? ? ? bool isViewModel = interface->getName().endswith("ViewModel");

? ? ? ? if (!isViewModel) {

? ? ? ? ? ? returnfalse;

? ? ? ? }

? ? ? ? for (auto property = interface->instprop_begin(),

? ? ? ? ? ? propertyEnd = interface->instprop_end(); property != propertyEnd; property++)

? ? ? ? {

? ? ? ? ? ? clang::ObjCPropertyDecl *propertyDecl = (clang::ObjCPropertyDecl *)*property;

? ? ? ? ? ? if (propertyDecl->getName().startswith("UI")) {

? ? ? ? ? ? ? ? addViolation(propertyDecl, this);

? ? ? ? ? ? }

? ? ? ? ? ? auto attrs = propertyDecl->getPropertyAttributes();

? ? ? ? ? ? bool isReadwrite = (attrs & ObjCPropertyDecl::PropertyAttributeKind::OBJC_PR_readwrite) >0;

? ? ? ? ? ? if (isReadwrite && isViewModel) {

? ? ? ? ? ? ? ? addViolation(propertyDecl, this);

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? returntrue;

? ? }

};

staticRuleSetrules(newMVVMRule());

</details>

6、修改自定義規(guī)則后就需要編譯。

成功后在 Products 目錄下會看到對應(yīng)名稱的 CustomLintRulesRule.dylib 文件,就需要復(fù)制到 /Documents/oclint/oclint-release/lib/oclint/rules。講道理,生成新的 lint rule 文件,需要把新的 dylib 文件復(fù)制到 /usr/local/lib。因為我們在源代碼安裝的第4部,設(shè)置了 ln -s 鏈接,所以不需要每次復(fù)制到相應(yīng)文件夾。

但是還是比較麻煩,每次都需要編譯新的 lint rule 之后需要將相應(yīng)的 dylib 文件復(fù)制到源代碼目錄下的 oclint-release/lib/oclint/rules 目錄下,本著「可以偷懶絕不動手」的原則,在自定義的 rule 的 target 中,在 Build Phases 選項下 CMake PostBuild Rules 中的腳本下將下面的代碼復(fù)制進(jìn)去

cp /Users/liubinpeng/Documents/oclint/Lint-XcodeProject/rules.dl/Debug/libCustomLintRulesRule.dylib /Users/liubinpeng/Documents/oclint/build/oclint-release/lib/oclint/rules/libCustomLintRulesRule.dylib

7、規(guī)則限定的3個類說明:

RuleBase

?|

?|-AbstractASTRuleBase

?|? ? ? |_ AbstractASTVisitorRule

?|? ? ? ? ? ? |_AbstractASTMatcherRule

?|

?|-AbstractSourceCodeReaderRule

AbstractSourceCodeReaderRule:eachLine 方法,讀取每行的代碼,如果想編寫的規(guī)則是需要針對每行的代碼內(nèi)容,則可以繼承自該類

AbstractASTVisitorRule:可以訪問 AST 上特定類型的所有節(jié)點,可以檢查特定類型的所有節(jié)點是遞歸實現(xiàn)的。在?apply?方法內(nèi)可以看到代碼實現(xiàn)。開發(fā)者只需要重載 bool visit* 方法來訪問特定類型的節(jié)點。其值表明是否繼續(xù)遞歸檢查

AbstractASTMatcherRule:實現(xiàn) setUpMatcher 方法,在方法中添加 matcher,當(dāng)檢查發(fā)現(xiàn)匹配結(jié)果時會調(diào)用 callback 方法。然后通過 callback 方法來繼續(xù)對匹配到的結(jié)果進(jìn)行處理

8、知其所以然

oclint 依賴與源代碼的語法抽象樹(AST)。開源 clang 是 oclint 獲的語法抽象樹的依賴工具。你如果想對 AST 有個了解,可以查看這個視頻

如果想查看某個文件的 AST 結(jié)構(gòu),你可以進(jìn)入該文件的命令行,然后執(zhí)行下面的腳本

clang -Xclang -ast-dump -fsyntax-only main.m

三、 Homebrew 方式安裝的 oclint 如何使用自定義規(guī)則

查看 OCLint 安裝路徑

which oclint

// 輸出:/usr/local/bin/oclint

ls -al? /usr/local/bin/oclint

// 輸出:本機(jī)安裝路徑

把上面生成的新的 lint rule 下的 dylib 文件復(fù)制到步驟1得到的額本機(jī)安裝路徑下

四、 使用 oclint

在命令行中使用

如果項目使用了 Cocopod,則需要指定 -workspace xxx.workspace

每次編譯之前需要 clean

實操:

進(jìn)入項目cd /Workspace/Native/iOS/lianhua

查看項目基本信息xcodebuild -list

//輸出

information about project "BridgeLabiPhone":

? Targets:

? ? ? BridgeLabiPhone

? ? ? lint

? Build Configurations:

? ? ? Debug

? ? ? Release

? If no build configuration is specified and -scheme is not passed then "Release" is used.

? Schemes:

? ? ? BridgeLabiPhone

? ? ? lint

編譯xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json編譯成功后,會在項目的文件夾下出現(xiàn) compile_commands.json 文件

生成 html 報表oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html看到有報錯,但是報錯信息太多了,不好定位,利用下面的腳本則可以將報錯信息寫入 log 文件,方便查看oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html 2>&1 | tee 1.log報錯信息是:oclint: error: one compiler command contains multiple jobs:查找資料,解決方案如下

將 Project 和 Targets 中 Building Settings 下的 COMPILER_INDEX_STORE_ENABLE 設(shè)置為?NO

在 podfile 中 target 'xx' do 前面添加下面的腳本

post_install do |installer|

? installer.pods_project.targets.each do |target|

? ? ? target.build_configurations.each do |config|

? ? ? ? ? config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"

? ? ? end

? end

end

然后繼續(xù)嘗試編譯,發(fā)現(xiàn)還是報錯,但是報錯信息改變了,如下


報錯信息改變了

看到報錯信息是默認(rèn)的警告數(shù)量超過限制,則 lint 失敗。事實上 lint 后可以跟參數(shù),所以我們修改腳本如下

oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999

生成了 lint 的結(jié)果,查看 html 文件可以具體定位哪個代碼文件,哪一行哪一列有什么問題,方便修改。

![lint-result-html-report](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-05-23-oclint-result-html.png)

如果項目工程太大,整個 lint 會比較耗時,所幸 oclint 支持針對某個代碼文件夾進(jìn)行 lint

oclint-json-compilation-database -i 需要靜態(tài)分析的文件夾或文件 -- -report-type html -o oclintReport.html 其他的參數(shù)

參數(shù)說明

| 名稱 | 描述 | 默認(rèn)閾值 |

| ----------------------- | ---------------------------- | ---- |

| CYCLOMATIC_COMPLEXITY | 方法的循環(huán)復(fù)雜性(圈負(fù)責(zé)度) | 10 |

| LONG_CLASS | C類或Objective-C接口,類別,協(xié)議和實現(xiàn)的行數(shù) | 1000 |

| LONG_LINE | 一行代碼的字符數(shù) | 100 |

| LONG_METHOD | 方法或函數(shù)的行數(shù) | 50 |

| LONG_VARIABLE_NAME | 變量名稱的字符數(shù) | 20 |

| MAXIMUM_IF_LENGTH |if語句的行數(shù) | 15 |

| MINIMUM_CASES_IN_SWITCH | switch語句中的case數(shù) | 3 |

| NPATH_COMPLEXITY | 方法的NPath復(fù)雜性 | 200 |

| NCSS_METHOD | 一個沒有注釋的方法語句數(shù) | 30 |

| NESTED_BLOCK_DEPTH | 塊或復(fù)合語句的深度 | 5 |

| SHORT_VARIABLE_NAME | 變量名稱的字符數(shù) | 3 |

| TOO_MANY_FIELDS | 類的字段數(shù) | 20 |

| TOO_MANY_METHODS | 類的方法數(shù) | 30 |

| TOO_MANY_PARAMETERS | 方法的參數(shù)數(shù) | 10 |

在 Xcode 中使用

在項目的 TARGETS 下面,點擊下方的 "+" ,選擇 cross-platform 下面的 Aggregate。輸入名字,這里命名為 Lint


?Xcode 中配置

選擇對應(yīng)的 TARGET -> lint。在 Build Phases 下 Run Script 下寫下面的腳本代碼

export LC_CTYPE=en_US.UTF-8

cd ${SRCROOT}

xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace clean && xcodebuild -scheme BridgeLabiPhone -workspace BridgeLabiPhone.xcworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json && oclint-json-compilation-database -e Pods -- -report-type xcode

說明,雖然有時候沒有編譯通過,但是看到如下圖的關(guān)于代碼相關(guān)的 warning 則達(dá)到目的了。


看到如圖關(guān)于代碼相關(guān)的 warning 則達(dá)到目的了

lint 結(jié)果如下,根據(jù)相應(yīng)的提示信息對代碼進(jìn)行調(diào)整。當(dāng)然這只是一種參考,不一定要采納 lint 給的提示。


lint 結(jié)果如圖


五、腳本化

每次都在終端命令行去寫 lint 的腳本,效率很低,所以想做成 shell 腳本。需要的同學(xué)直接直接拷貝進(jìn)去,直接在工程的根目錄下使用,我這邊是一個 Cocopod 工程。拿走拿走別客氣

腳本文件多次保存,Confluence提示:Unable to communicate with server. Saving is not possible at the moment. 故放附件文件中,詳情請見下面腳本:

腳本化

每次都在終端命令行去寫 lint 的腳本,效率很低,所以想做成 shell 腳本。需要的同學(xué)直接直接拷貝進(jìn)去,直接在工程的根目錄下使用,我這邊是一個 Cocopod 工程。拿走拿走別客氣

#!/bin/bash

COLOR_ERR="\033[1;31m"? ? #出錯提示

COLOR_SUCC="\033[0;32m"? #成功提示

COLOR_QS="\033[1;37m"? #問題顏色

COLOR_AW="\033[0;37m"? #答案提示

COLOR_END="\033[1;34m"? ? #顏色結(jié)束符

# 尋找項目的 ProjectName

function searchProjectName () {

# maxdepth 查找文件夾的深度

? find . -maxdepth 1 -name "*.xcodeproj"

}

function oclintForProject () {

? ? # 預(yù)先檢測所需的安裝包是否存在

? ? if which xcodebuild 2>/dev/null; then

? ? ? ? echo 'xcodebuild exist'

? ? else

? ? ? ? echo '??? 連 xcodebuild 都沒有安裝,玩雞毛?。????'

? ? fi

? ? if which oclint 2>/dev/null; then

? ? ? ? echo 'oclint exist'

? ? else

? ? ? ? echo '?? 完蛋了你,玩 oclint 卻不安裝嗎,你要鬧哪樣 ??'

? ? ? ? echo '?? 乖乖按照博文:https://github.com/FantasticLBP/knowledge-kit/blob/master/第一部分%20iOS/1.63.md 安裝所需環(huán)境 ??'

? ? fi

? ? if which xcpretty 2>/dev/null; then

? ? ? ? echo 'xcpretty exist'

? ? else

? ? ? ? gem install xcpretty

? ? fi

? ? # 指定編碼

? ? export LANG="zh_CN.UTF-8"

? ? export LC_COLLATE="zh_CN.UTF-8"

? ? export LC_CTYPE="zh_CN.UTF-8"

? ? export LC_MESSAGES="zh_CN.UTF-8"

? ? export LC_MONETARY="zh_CN.UTF-8"

? ? export LC_NUMERIC="zh_CN.UTF-8"

? ? export LC_TIME="zh_CN.UTF-8"

? ? export xcpretty=/usr/local/bin/xcpretty # xcpretty 的安裝位置可以在終端用 which xcpretty找到

? ? searchFunctionName=`searchProjectName`

? ? path=${searchFunctionName}

? ? # 字符串替換函數(shù)。//表示全局替換 /表示匹配到的第一個結(jié)果替換。

? ? path=${path//.\//}? # ./BridgeLabiPhone.xcodeproj -> BridgeLabiPhone.xcodeproj

? ? path=${path//.xcodeproj/} # BridgeLabiPhone.xcodeproj -> BridgeLabiPhone


? ? myworkspace=$path".xcworkspace" # workspace名字

? ? myscheme=$path? # scheme名字

? ? # 清除上次編譯數(shù)據(jù)

? ? if [ -d ./derivedData ]; then

? ? ? ? echo -e $COLOR_SUCC'-----清除上次編譯數(shù)據(jù)derivedData-----'$COLOR_SUCC

? ? ? ? rm -rf ./derivedData

? ? fi

? ? # xcodebuild clean

? ? xcodebuild -scheme $myscheme -workspace $myworkspace clean

? ? # # 生成編譯數(shù)據(jù)

? ? xcodebuild -scheme $myscheme -workspace $myworkspace -configuration Debug | xcpretty -r json-compilation-database -o compile_commands.json

? ? if [ -f ./compile_commands.json ]; then

? ? ? ? echo -e $COLOR_SUCC'編譯數(shù)據(jù)生成完畢??????'$COLOR_SUCC

? ? else

? ? ? ? echo -e $COLOR_ERR'編譯數(shù)據(jù)生成失敗??????'$COLOR_ERR

? ? ? ? return -1

? ? fi

? ? # 生成報表

? ? oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \

? ? -rc LONG_LINE=200 \

? ? -disable-rule ShortVariableName \

? ? -disable-rule ObjCAssignIvarOutsideAccessors \

? ? -disable-rule AssignIvarOutsideAccessors \

? ? -max-priority-1=100000 \

? ? -max-priority-2=100000 \

? ? -max-priority-3=100000

? ? if [ -f ./oclintReport.html ]; then

? ? ? ? rm compile_commands.json

? ? ? ? echo -e $COLOR_SUCC'??分析完畢??'$COLOR_SUCC

? ? else

? ? ? ? echo -e $COLOR_ERR'??分析失敗??'$COLOR_ERR

? ? ? ? return -1

? ? fi

? ? echo -e $COLOR_AW'將為大爺自動打開 lint 的分析結(jié)果'$COLOR_AW

? ? # 用 safari 瀏覽器打開 oclint 的結(jié)果

? ? open -a "/Applications/Safari.app" oclintReport.html

}

oclintForProject

參考:

OCLint:https://oclint-docs.readthedocs.io/en/stable/

iOS 工程自動化 - OCLint:http://www.cocoachina.com/articles/20181

iOS持續(xù)集成(CI)——OCLint靜態(tài)代碼分析:https://cloud.tencent.com/developer/article/1438626

OCLint 自定義規(guī)則:http://www.itdecent.cn/p/383d4166bec5

OCLint靜態(tài)代碼檢測實踐:https://juejin.im/post/6844904017424809998

iOS 持續(xù)集成系列 - 自動化 Code Review:http://www.itdecent.cn/p/c8b3b515ccf3

OCLint靜態(tài)代碼分析:https://chenjiangchuan.gitbooks.io/ios-documentation/content/oclintjing-tai-dai-ma-fen-xi.html

?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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