前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >工程化(一)——Xcode工程探究

工程化(一)——Xcode工程探究

作者头像
拉维
发布2023-03-09 15:10:28
1.9K0
发布2023-03-09 15:10:28
举报
文章被收录于专栏:iOS小生活iOS小生活

一、Xcode工程说明‍‍

1,创建Workspace

如果我们是通过 CocoaPods 引入第三方,那么在命令行执行 pod install 之后,查看项目目录就可以看到多了一个 xcworkspace 文件,如下:

这会给我们产生一个错觉,那就是workspace只可以在pod install之后才能生成。但是实际上不是的,其实本质上,所有的Xcode工程(project)都是通过workspace进行管理的。为什么我会这样说?我们随便找到一个Xcode工程,然后对xcodeproj进行右键显示包内容,就可以看到,其实xcodeproj里面也是有一个xcworkspace的,如下:

workspace其实就是为了管理多个project的

我们先来创建一个workspace,打开Xcode,File->New->Workspace,然后修改名称和路径,就创建成功了,如下:

右键显示包内容:

contents.xcworkspacedata文件是workspace的配置文件,它是一个XML文本文件,这里面承载了workspace管理的所有project的路径

xcshareddata文件夹里面存储的是要分享给别人的配置文件

比如,我们自定义的Scheme中,就有一个shared配置选项,如果选中这个shared,那么该配置就会被放在xcshareddata文件夹里面

xcuserdata文件夹里面存储的是只能自己看到的配置文件。

2,创建Project

先创建一个空的project:

然后修改名称和路径,就创建成功了,如下:

接下来我将NormanProject添加到workspace中

①打开Norman.xcworkspace

②点击左下角加号➕按钮,选择【Add Files to “Norman”】

③选择所要添加的NormanProject.xcodeproj文件

添加完了之后就可以在workspace中看到project了,此时打开该workspace中的contents.xcworkspacedata文件,可以看到project的路径信息也被自动添加了进来:

可以看到,project的路径是由前缀和后缀组成,前后缀之间通过冒号隔开,前缀有如下几种选项:

  • self,在.xcworkspace所在目录下有同名、并以.pbxproj结尾的文件的路径。对新创建的.xcodeproj显示包内容后,再对看到的project.xcworkspace显示包内容,然后在contents.xcworkspacedata中看到的project路径中就是self。
  • group,指定目录下以.xcodeproj结尾的文件的路径
  • container,在.xcworkspace所在目录下有不同名、并且以.xcodeproj结尾的文件的路径。
  • absolute,绝对路径。

我们知道,workspace可以用来组合管理多个project,通过上面这个规则,workspace就可以将多个不同路径下的project组合到一起。

我们新创建一个Xcode工程(project),配置好对应的podfile文件,然后pod install,之后就可以生成一个管理原project和pods project的workspace,其实这就是通过上面这个规则来实现的。

现在我将project的工程文件移动到workspace的同一层级,如下:

然后再打开workspace,发现其管理的NormanProject.xcodeproj报红了,如下:

这是因为在workspace中找不到对应的project了,按照上面介绍的规则,我在workspace的contents.xcworkspacedata中将project的路径修改一下:

然后重新打开workspace,就能看到对应的project了。

3,创建Target

一个Xcode工程(project),是可以构建多种产物的,具体是构建哪一种产物,就是通过Target来指定配置的。实际上,project只是用户来管理各种代码、资源源文件的,它并不会产出任何产物,真正产出产物是通过target来实现的

接下来我给NormanProject添加一个Target:

给这个Target命名为NormanTarget,就可以看到对应的Target了:

4,Scheme

我们还会注意到,在Xcode工程中还会有一个Scheme配置,如下:

我们通过Target构建产物一定需要Scheme吗?答案是不需要。验证流程如下:

①通过Manage Schemes将对应的scheme给删除

此时Target就没有任何Scheme了。

②进入workspace所在目录,运行如下指令:

代码语言:javascript
复制
xcodebuild -project NormanProject.xcodeproj -target NormanTarget -showBuildSettings -json

xcodebuild是可以指定action的,其默认action就是building

此时指令是可以正确运行的,没有任何报错,这说明可以正常编译,也就进一步说明了产物的构建并不是必须要依赖于Scheme

那么Scheme是做什么用的呢?

我先给NormanTarget新建一个Scheme,命名为NormanTarget-Dev,然后Edit Scheme,如下:

可以看到,我可以通过Scheme来配置产物构建过程中的各种详细参数,在对应的Action执行时机插入指定脚本动作等。通过Scheme来辅助Target进行产物的构建能够更加直观、更加方便控制。我们可以对一个Target来创建多个Scheme,这样就可以根据不同的环境进行不同的构建配置,达到一键切换构建配置的目的,更加方便,更加直观,更加清晰。

可以想象一下,如果没有Scheme的话,想要实现上面的这些功能该是多么麻烦。

现在考虑一下,Scheme是存在哪个地方呢?答案是存放在对应Project的xcshareddata或者xcuserdata中(具体存在哪里面要看是否share):

打开其中一个Scheme配置文件:

可以看到,我在Xcode可视化配置的Scheme,其实最终也是通过NormanTarget.xcscheme文件来存储的。如果我们熟悉配置文件的语法书写规则,其实我们也可以自己去写这个Scheme配置文件,然后通过脚本的方式读取该文件并且将读取到的配置内容应用到构建过程中去,如果所有的这些都是自己手动去做的话,那就会非常麻烦。所以Xcode提供的Scheme就是可以让开发者更方便、更直观地进行各项构建配置。

在workspace所在目录下,执行如下指令,都可以成功运行:

代码语言:javascript
复制
xcodebuild -workspace Norman.xcworkspace -scheme NormanTarget-Dev -showBuildSettings -json 
xcodebuild -project NormanProject.xcodeproj -scheme NormanTarget-Dev -showBuildSettings -json 
xcodebuild -project NormanProject.xcodeproj -target NormanTarget -showBuildSettings -json 
xcodebuild -project NormanProject.xcodeproj -scheme NormanTarget-Dev -showBuildSettings -json -configuration Debug -destination generic/ platform="iOS Simulator" 

这说明,workspace、project都可以直接通过scheme进行构建,project可以通过target进行构建

但是当我运行如下指令的之后就报错了:

代码语言:javascript
复制
xcodebuild -workspace Norman.xcworkspace -target NormanTarget -showBuildSettings -json

报错如下:

报错信息指出:不可以直接给workspace指定target

二、Xcode就是一个终端

Xcode可以在build phases中添加shell脚本,如下:

Run Script,顾名思义,就是运行脚本的意思。由此可见,Xcode内部也是内置了一个终端环境的

Xcode既然是一个终端,那么它就势必会使用到一些环境变量,Xcode终端环境使用的环境变量是如何定义的呢?Xcode会将生成产物所需要的各种参数(Build Settings中的各种参数),以定义shell环境变量的形式,定义在Xcode的Shell环境中(所谓的Shell环境,其实就是终端环境)。也就是说,Xcode工程中的BuildSettings中的各种参数其实就是Xcode的shell环境的环境变量

什么是环境变量呢?通过export关键字定义的变量都是环境变量,如下:

举个例子,我在Build Settings中去搜索“header search path”,就会搜到下图左侧红框内选项,它表示的是头文件的搜索路径。我在这里配置了之后,就可以在Xcode内置的shell环境中导出一个名为HEADER_SEARCH_PATHS的环境变量(如下图右侧红框所示)。当Clang进行编译的时候,就会直接拿到HEADER_SEARCH_PATHS环境变量来使用。

接下来再举一个例子来说明一下环境变量。

Edit Scheme,在Build的Pre-actions中去新增一个脚本动作(New Run Script Action),然后键入下面一条错误的shell指令:

然后重新编译,肯定就报错了,如下:

点开右侧的详细按钮:

可以看到,这里定义了很多环境变量。这里export出的这些变量,就是在执行当前shell脚本的环境中,所能够拿到的、Xcode给提供的环境变量。

Xcode会在编译的时候,将BuildSettings中配置的各种参数都导出为环境变量,提供给Xcode内置的Shell环境

一个终端的命令是可以定位到另外一个终端的,这个定位就是通过终端标识符来完成的。

每一个终端都有自己唯一的标识符,在对应的终端环境下输入tty,就可以获取到当前Shell环境的的唯一标识符,如下:

代码语言:javascript
复制
➜  ~ tty
/dev/ttys001
➜  ~

然后我在另外一个终端通过如下指令定位到该终端,也就是说,可以进行编译信息的重定位。我在Xcode的Shell脚本中输入如下指令:

1> 代表的是将正确的结果重定向到某个地方

执行Xcode工程之后,就可以在对应终端中看到打印信息了,如下:

三、xcconfig文件简介

现在我想在终端中查看符号表信息,可以使用nm指令。

在终端输入如下指令来查看nm指令的作用:

代码语言:javascript
复制
➜  ~ man nm

终端显示如下:

可以看到,nm指令的作用就是显示符号表信息

如果我们现在想要查看某Xcode工程的构建产物的符号表信息,那么就需要依次手动执行如下操作:

①打开对应Xcode工程,Product->Show Build Folder In Finder

②在Build文件夹下面,找到Products文件夹,然后找到对应环境对应设备下面的构建产物

③对构建产物右击,显示包内容,就可以找到对应可执行文件了,如下:

④在终端输入nm之后,将上一步找到的可执行文件拖入到终端,如下:

⑤回车执行指令,就可以看到对应的符号表信息了,如下:

实际上,如果要查看一个二进制可执行文件的符号表信息,手动操作还是比较繁琐的。接下来我要做的事情就是利用Xcode的终端环境以及其环境变量来简化这些手动操作

我们现在应该知道如下几点了:Xcode本身就是一个大型的终端环境(Shell环境),在Build Setting中定义的各种变量其实就是Xcode终端环境的环境变量

除了可以修改系统提供的一些环境变量之外,我们也可以自定义环境变量。自定义环境变量的方式有如下两种:

①在Target的Build Settings的User-Defined中进行自定义,如下:

②通过xcconfig文件进行自定义。

xcconfig的创建如下:

创建完成之后,可以看到生成的文件的拓展名就是.xcconfig,如下:

还可以看到,在生成的文件中,提供了一个网址,该网址就是对该配置文件进行了详细介绍,在介绍中可以找到环境变量的详解文章,网址如下:

代码语言:javascript
复制
https://help.apple.com/xcode/mac/11.4/#/itcaec37c2a6

文章截图如下:

左侧的就是在Build Settings中看到的描述性字样,右侧小括号中的是Xcode导出的环境变量的真实名称。

通过上面的讲解我们已经了解到,在xcconfig文件中我们不仅可以修改系统提供的一些环境变量,也可以自定义环境变量。接下来我们就来详细介绍一下xcconfig配置文件。

1,xcconfig配置文件是跟随Configuration的,而Configurations又是在Project中进行管理的,如下:

对Configurations中的每一个configuration,我们都可以配置对应的xcconfig文件(将对应的路径配置好即可)。默认是有Debug和Release两个configuration,我们也可以自定义这里的configuration。

通过上图我们还可以看到,可以给Target配置单独的xcconfig,也可以给Project配置单独的xcconfig,Target中配置的xcconfig文件优先级高于Project中配置的xcconfig文件

2,实际上,我们既可以在Build Settings中对环境变量进行配置,也可以在xcconfig中对环境变量进行配置;既可以在Project的Build Settings或者xcconfig中进行配置,也可以在Targets的Build Settings或者xcconfig中进行配置。那么他们的优先级顺序是怎么样的呢?下面按照优先级由高到低依次排列:

  • ①手动配置Target Build Settings
  • ②Target中配置的xcconfig文件
  • ③手动配置Project Build Settings
  • ④Project中配置的xcconfig文件

这个顺序我们可以在Build Settings中的levels栏进行查看,如下:

Resolved就是最终生效的配置,从左往右优先级依次递减。

3,环境变量的名称定义是有一定的规则的,其仅由大写字母、数字和下划线(_)组成,原则上全部大写,但是你写成小写的也是可以的。字符串可以使用双引号也可以使用单引号。

环境变量有三种特殊的情况,下面一一进行说明。

(1)在xcconfig文件中定义的变量如果在Build Setting中也存在,那么就会存在优先级高的配置将优先级低的配置给覆盖掉的情况,如果不想要被覆盖,那么可以在优先级高的地方使用$(inherited)来让当前环境变量继承变量原有的值

比如我在Target的xcconfig文件中定义了OTHER_LDFLAGS环境变量,如下:

代码语言:javascript
复制
OTHER_LDFLAGS = -framework SDWebImage

然后我又在Target的Build Settings中定义了OTHER_LDFLAGS环境变量,如下:

代码语言:javascript
复制
OTHER_LDFLAGS = $(inherited) -framework AFNetworking

那么最终环境变量OTHER_LDFLAGS的取值是 -framework SDWebImage -framework AFNetworking

但是需要注意的是,有一部分环境变量是不能通过继承的方式由xcconfig文件配置到Build Settings中的,例如,配置PRODUCT_BUNDLE_IDENTIFIER就不会起作用。

(2)引用其他变量的使用,可以使用{},也可以使用(),如下:

代码语言:javascript
复制
FIRST_NAME = NORMAN
LAST_NAME = LEE
FULL_NAME = $(FIRST_NAME) ${LAST_NAME}

(3)可以通过Configuration、SDK和Arch来对环境变量的设置进行条件化。

可以指定Configuration是Debug、Release还是其他自定义的Configuration。关于Configuration的自定义新增,我在上面第1条已做介绍。

可以指定SDK是模拟器(iphonesimulator*),还是手机(iphoneos*),还是电脑(macosx*)。

还可以指定在哪个架构指令集Arch上面生效。

下面就是指定了OTHER_LDFLAGS环境变量是在Debug的Configuration下,并且运行在模拟器的时候,并且架构指令集是x86_64的时候才生效的:

代码语言:javascript
复制
OTHER_LDFLAGS[config=Debug][sdk=iphonesimulator*][arch=x86_64] = $(inherited) -framework AFNetworking

需要注意的是,在Xcode11.4及其之后的版本中,可以使用default来指定变量为空的时候的默认值,如下:

代码语言:javascript
复制
$(BUILD_SETTING_NAME:default=value)

它的意思就是,当环境变量BUILD_SETTING_NAME为空的时候就取默认值value

4,xcconfig文件的语法是比较简单的,每个配置文件都是由一系列的键值对组成,这些键值对具有如下语法:

代码语言:javascript
复制
BUILD_SETTING_NAME = value

在xcconfig文件中,只有一种注释方式 \\

5,在给Configuration配置xcconfig文件的时候,只能选择其中一个xcconfig文件。但是作为一个成熟的开发者,我们是有抽离和封装的思维的,对于一些公用的环境变量,我们习惯于去把它们抽离到单独的一个xcconfig文件中;或者可以根据功能将环境变量分配到不同的config文件中。也就是说,我们可以在Xcode工程中创建多个xcconfig文件,当需要在一个xcconfig文件中去使用另外一个xcconfig文件的内容的时候,就可以通过include关键字来导入其他xcconfig文件内的配置,其具体语法如下:

代码语言:javascript
复制
#include “Debug.config”

在搜索导入的文件的时候,如果是以/开头,那么就代表绝对路径,例如:

代码语言:javascript
复制
#include “/Users/liwei/Desktop/Xcode/NormanTarget/Config.xcconfig”

也可以通过相对路径来搜索,相对路径是相对${SRCROOT}路径的

代码语言:javascript
复制
#include “NormanTarget/Config.xcconfig”

6,可以在代码中去使用定义的环境变量吗?答案是可以的,但是不能直接使用,可以通过info.plist文件来做一层中间的传递,具体操作如下:

  1. 在info.plist中新增一个键值对,然后value设置为某个环境变量
  2. 在代码中拿到info.plist并将其转换成Dictionary,然后通过上一步新增的键值对的key进行取值即可。

接下来说一个我们在实际开发中的简单应用场景,在Xcode工程运行的时候执行一段脚本,这段脚本中需要使用到定义的环境变量,该场景的实现步骤如下:

①在Build Settings或者xcconfig文件中定义需要使用的环境变量,如下:

② 在Xcode中去执行对应的脚本,并将输出结果重定向到某一指定终端,如下:

③运行工程,并在指定终端下查看运行结果,如下:

接下来就来解决一开始提出来的那个问题:如何配置脚本去自动查看一个二进制可执行文件的符号表信息?

1,首先编写一个Shell脚本文件,内容如下:

代码语言:javascript
复制
#!/bin/sh
RunCommand() {
  #判断全局字符串VERBOSE_SCRIPT_LOGGING是否为空。-n string判断字符串是否非空
  #[[是 bash 程序语言的关键字,用于判断。当然也可以使用test关键字,【if [[ 条件语句 ]]】 与 【if test 条件语句】 的作用是一样的。
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
    #作为一个字符串输出所有参数。使用时加引号"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数
      if [[ -n "$TTY" ]]; then
          echo "♦ $@" 1>$TTY
      else
          echo "♦ $*"
      fi
      echo "------------------------------------------------------------------------------" 1>$TTY
  fi
  #与$*相同。但是使用时加引号,并在引号中返回每个参数。"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数
  if [[ -n "$TTY" ]]; then
        eval "$@" &>$TTY
  else
       "/bin/bash $@"
  fi
  #显示最后命令的退出状态。$? 代表的是上一条命令的执行结果,0表示没有错误,除0之外的其他任何值都表示有错误。
  return $?
}
EchoError() {
    #在shell脚本中,默认情况下,总是有三个文件处于打开状态,标准输入(键盘输入)、标准输出(输出到屏幕)、标准错误(也是输出到屏幕),它们分别对应的文件描述符是0,1,2
    # >  默认为标准输出重定向,与 1> 相同
    # 2>&1  意思是把 标准错误输出 重定向到 标准输出.
    # &>file  意思是把标准输出 和 标准错误输出 都重定向到文件file中
    # 1>&2 将标准输出重定向到标准错误输出。实际上就是打印所有参数已标准错误格式
    if [[ -n "$TTY" ]]; then
        echo "$@" 1>&2>$TTY
    else
        echo "$@" 1>&2
    fi
    
}
RunCMDToTTY() {
    if [[ ! -e "$TTY" ]]; then
        EchoError "=========================================="
        EchoError "ERROR: Not Config tty to output."
        exit -1
    fi
    # CMD = 运行到命令
    # CMD_FLAG = 运行到命令参数
    # TTY = 终端
    if [[ -n "$CMD" ]]; then
        RunCommand $CMD
    else
        EchoError "=========================================="
        EchoError "ERROR:Failed to run CMD. THE CMD must not null"
    fi
}
RunCMDToTTY

可以看到,该Shell脚本使用到了三个环境变量:VERBOSE_SCRIPT_LOGGINGTTYCMD

该脚本文件的作用就是,将CMD命令执行过程中出现的各种问题过程给打印出来。

2,在xcconfig文件中配置相关环境变量

这里需要说明的一点是,如果我们不知道某一个数据所对应的环境变量存不存在,那么可以首先在工程根目录(${SRCROOT})下执行如下指令来查看Build Settings中的各个环境变量:

代码语言:javascript
复制
xcodebuild -project NormanProject.xcodeproj -target NormanTarget -showBuildSettings -json

结果如下:

然后搜索相关的环境变量值,这样就可以查找到对应的环境变量了。

比如,我想要查看如下路径:

那么先打开对应文件夹,然后把该文件夹拖到终端,然后pwd查看其路径,如下:

复制该路径,然后在工程根目录(${SRCROOT})下执行如下指令来查看Build Settings中的各个环境变量:

代码语言:javascript
复制
xcodebuild -project NormanProject.xcodeproj -target NormanTarget -showBuildSettings -json

command + F,command + V,就能看到对应的环境变量了,如下:

在xcconfig文件中配置的环境变量如下:

代码语言:javascript
复制
TTY = /dev/ttys001 // 所要输出到的终端地址
MACHO_PATH = ${BUILD_DIR}/${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/${PRODUCT_NAME}${WRAPPER_SUFFIX}/${PRODUCT_NAME} // 二进制可执行文件的完整路径
CMD = nm ${MACHO_PATH} // 查看二进制可执行文件的符号表信息

3,将脚本文件放在对应目录下(本例中是直接放在项目根目录),运行脚本文件,然后就可以在对应终端中看到对应的符号信息了,如下:

四、Xcode脚本执行进阶

我们知道,Xcode本身就是一个Shell环境,在上面的讲解中,我们也多次使用到了Xcode中的Run Script来执行脚本,如下:

在这里可以执行Python、Ruby或者是Shell脚本。

但是不知道诸位有没有注意到Run Script中的一些配置选项,如下:

在Input Files或者Input File Lists中是配置输入文件的路径,在脚本执行过程中会使用到这些输入文件。

在Output Files或者Output File Lists中是配置输出文件的路径,会将脚本执行的结果输出到这些输出文件当中。

在Xcode11之后,提供了一个环境变量USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES,该环境变量是一个Bool类型,并且不能在Build Settings中进行设置,只能在xcconfig文件中进行设置。

如果将USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES设置为YES,那么在程序启动的时候就会递归检测输入文件是否有变更,在有变更的时候才会执行脚本。

那么这个在现实场景中有什么用呢?我们可以打开任何一个使用Pods管理第三方库或者其他组件的工程,然后来到Build Phases里面,找到[CP] Check Pods Mainfest.lock,点开之后,内容如下:

可以看到,在Input Files中定义了两个输入文件{PODS_PODFILE_DIR_PATH}/Podfile.lock和{PODS_ROOT}/Manifest.lock,在程序启动的时候会判断这两个输入文件是否有变动,如果有变动的话就会执行脚本。

在脚本中会比较这两个输入文件是否一致,如果不一致的话就会输出错误信息,并且退出脚本执行;如果一致的话,就会将SUCCESS信息输出到在Output Files中定义的输出文件中。需要注意的是,SCRIPT_OUTPUT_FILE_0表示的是在Output Files中定义的第一个输出文件。

这个脚本的作用就是在Pods库有更新的时候提醒你去pod install。

五、Target的依赖

如果TargetA要使用到TargetB的内容,则TargetA就会依赖TargetB。依赖分为显示依赖和隐式依赖。

隐式依赖,implicit dependencies,如果Target A 和 B 在同一个Project或者Workspace下面,那么Xcode会自动检测依赖关系,在构建A之前会自动构建B。

显示依赖,explicit dependencies,需要手动添加依赖关系。显示依赖的使用是有限制条件的,只有相同Project下面的Target才能在Build Phases下的Dependencies中添加显示依赖。

现在我们随便打开一个使用Pods管理的Xcode工程,如下:

可以看到,是有两个Project的,主工程Project下面的Target简称Target A,Pods Project下面的Target简称Target B,可以看到,A是依赖B的。而A和B是同属于一个WorkSpace,所以在构建A之前会首先构建B。

可以看到,在Frameworks文件夹下面标红的这种就是隐式依赖链接,这里的Pods_ShareLife就是隐式依赖链接,当上面的主工程Target编译的时候,会先去找其链接的隐式依赖,自动触发隐式依赖Target(Pods_ShareLife)的编译。

而Pods Project是管理了多个第三方库以及组件库的Target的,Pods Project管理的这些Target是可以通过隐式依赖的方式来进行依赖的,即拖入到主工程Project的Frameworks文件夹下面;但是Xcode采用了更为简洁的显示依赖的方式,如下:

在Build Phases的Dependencies下面添加的这些依赖都是显示依赖

现在总结一下,主工程ShareLife隐式依赖Pods_ShareLife Target,Pods_ShareLife Target又显示依赖了各个第三方库以及组件库的Target。因此,在编译构建主工程ShareLife,就会触发Pods_ShareLife Target,进而触发各个第三方库以及组件库的Targe的编译构建。这就是为什么主工程只引入了一个Pods_ShareLife Target就可以管理诸多组件Target的原理。

以上。‍

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-12-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档