news 2026/9/29 2:02:15

从零打包APK:Android Studio完整流程与工程实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零打包APK:Android Studio完整流程与工程实践指南

1. 为什么我建议每个安卓初学者亲手做一个APK

先聊个很现实的问题:很多人学Android开发,跟着教程敲了一堆代码,项目也能在模拟器里跑起来,但一说到“APK”就觉得隔着一层纱。有人甚至以为APK是某种需要特殊工具才能生成的“安装包”,还有人把APK和“破解软件”画等号。实际上APK就是一个压缩包,里面装的是你的代码、资源文件和清单说明,就像一份打包好的外卖——你写了菜谱、买齐了食材、炒好了菜,APK就是那个装好饭菜的餐盒。

自己动手做一次APK,本质上不是为了学会“打包”这个动作,而是通过完整走一遍“写代码→编译→打包→安装→运行”的链路,把Android开发的整体流程串起来。很多初学者在IDE里点一下“Run”按钮,App就跑起来了,但这中间发生了什么完全不知道。一旦哪天遇到编译报错、签名冲突、包名被占用、ABI不匹配这类问题,就会完全抓瞎。

这篇文章的目标很明确:带着你用Android Studio从零创建一个项目,写一个能交互的App,然后打包出真正的APK文件,再装到手机上跑起来。整个过程中我会把每个关键步骤背后的原理和常见的坑一起讲清楚,适合完全没接触过Android开发的新手,也适合那些已经在写代码但从来没搞清楚过打包细节的朋友。

2. 环境准备里最容易被忽略的几个细节

2.1 JDK与Android Studio版本的匹配陷阱

先说环境。Android开发的基本工具链是JDK(Java开发环境)+ Android SDK(安卓软件开发工具包)+ Android Studio(集成开发环境)。很多新手卡在第一步,不是因为不会装,而是因为版本不匹配。

一个很常见的现象:你下载了最新版Android Studio,结果发现它要求JDK 17以上,而你的电脑上装的是JDK 8;或者你照着某个老教程操作,教程里说需要JDK 8,但新的Android Studio已经不认了。版本不匹配的直接后果是Gradle同步失败,或者编译时直接报错。

我给你的建议是:不要自己单独去下载JDK,直接安装Android Studio时让它自动捆绑的JBR(JetBrains Runtime)来用。Android Studio从Arctic Fox版本开始,内置了JDK 11,新版本甚至内置了JDK 17。你在项目配置里看到的“Gradle JDK”选项,选择“Embedded JDK”即可,这就避免了一大堆版本冲突问题。

2.2 SDK Manager里到底要装哪些东西

安装完Android Studio后,第一次启动会让你选择SDK组件。很多人看到那一长串列表就头大,全选吧,几十个GB下到怀疑人生;不选吧,又怕后面缺东西。这里我给出一个最少安装清单:

  • Android SDK Platform-Tools:必装,里面有adb(Android调试桥),后面真机调试和安装APK全靠它。
  • Android SDK Build-Tools:必装,打包APK时负责把资源文件和代码编译打包的工具。
  • 对应版本的Android SDK Platform:至少装一个,比如Android 14(API 34)或Android 15(API 35)。注意,这里的版本要和你的compileSdk版本对上。
  • Intel HAXM或AEHD:如果你打算用模拟器,Windows系统下需要装这个硬件加速组件,否则模拟器慢到怀疑人生。

至于各种系统镜像(System Images),除非你要用模拟器跑Android 8以下的旧系统,否则建议直接用真机调试。原因后面会说。

2.3 环境变量配置的一个常用命令验证方法

Windows和macOS的SDK路径默认位置不同,Windows通常在C:\Users\你的用户名\AppData\Local\Android\Sdk,macOS在~/Library/Android/sdk。不管装在哪里,我建议你配置环境变量ANDROID_HOME指向SDK目录,然后把platform-tools目录加入PATH。

配置完之后,打开命令行工具,输入adb --version,如果能看到版本号输出,说明环境配置成功。这个过程很多人忽略,等到真机连不上、找不到设备时才回来补配置,其实一开始花两分钟就能搞定。

3. 创建第一个项目:包名、SDK版本背后的门道

3.1 包名为什么不能乱起

打开Android Studio,点击“New Project”,选择一个空Activity模板,填写项目信息时有两个字段需要注意:一个是Package name(包名),一个是Minimum SDK(最低支持的安卓版本)。

包名是整个应用的唯一标识,相当于应用的身份证号。Google Play和国内各应用商店都要求包名全局唯一,一旦发布后,包名不能随意更改,否则会被识别为一个全新的应用。所以不要起com.example.test这种名字,虽然系统允许你用,但发布时大概率会被商店拒绝。建议采用com.你的域名.项目名的格式,比如com.mingzi.firstapp。

命名规范方面,包名尽量只用小写字母和数字,不要用下划线。原因跟Java包名的历史遗留问题有关,虽然Android Studio不会拦你,但有些第三方SDK对下划线处理不够友好,能避免就尽量避免。

3.2 Minimum SDK选多少才合适

Minimum SDK代表你的App最低能在哪个安卓版本上运行。这个选项决定了你要兼容多少旧设备,也决定了你能用哪些新API。很多人喜欢直接选最高的Android 15,觉得用最新的一定最好,其实这是个误区。

选择Minimum SDK的核心逻辑是:你希望覆盖多少比例的安卓设备。目前Android 8.0(API 26)以下的设备占比已经非常低。对于练手项目,我建议选API 24(Android 7.0)或API 26(Android 8.0),这样能享受到apply colors等现代特性,同时覆盖了绝大多数存量设备。而且,模拟器跑高版本系统镜像本来就慢,选太低版本又要写一堆兼容代码,选一个中间的版本最省心。

3.3 第一次Gradle同步慢到崩溃怎么办

创建项目后,Android Studio会自动开始Gradle同步,也就是下载项目依赖的各种库。第一次同步慢,几乎每个新手都会遇到。这是因为Gradle工具本身和依赖包都在国外的服务器上,国内下载速度极慢,卡几个小时都有可能。

解决办法有两个:

方案一:修改Gradle的镜像源。找到项目根目录下的build.gradle.kts或settings.gradle.kts文件,把仓库地址替换成国内镜像。

dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url = uri("https://maven.aliyun.com/repository/public") } maven { url = uri("https://maven.aliyun.com/repository/google") } } }

方案二:使用Gradle发行版镜像。修改gradle/wrapper/gradle-wrapper.properties文件中的distributionUrl,把services.gradle.org换成腾讯或阿里云的镜像地址。

distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip

同步成功之后,界面右下角显示“Sync Finished”,左侧项目结构正常展开,这时候环境才算真正准备好。我遇到过不少人在这一步就放弃了,其实只要能熬过第一次同步,后面就顺了。

4. 工程结构拆解:不搞清楚文件职责就动手写代码,后面全是坑

4.1 一张图看懂App工程的核心目录

项目创建成功后,你会看到左侧有一棵目录树。很多新手不管三七二十一,只盯着MainActivity.kt改代码,改完发现运行报错,也不知道去哪里看日志。其实安卓项目的目录结构非常固定,它有明确的职责划分。

app/ ├─ src/ │ ├─ main/ │ │ ├─ java/com/mingzi/firstapp/ │ │ │ ├─ MainActivity.kt │ │ ├─ res/ │ │ │ ├─ layout/ │ │ │ │ └─ activity_main.xml │ │ │ ├─ values/ │ │ │ │ ├─ strings.xml │ │ │ │ ├─ colors.xml │ │ │ │ └─ themes.xml │ │ │ └─ mipmap/ │ │ ├─ AndroidManifest.xml ├─ build.gradle.kts

MainActivity.kt是应用的主界面逻辑代码,用Kotlin编写。activity_main.xml是布局文件,定义界面长什么样。AndroidManifest.xml是应用清单,相当于App的说明书,声明了有哪些页面、需要什么权限、入口Activity是哪个。drawable目录放图片等资源,values目录放字符串、颜色、主题等常量定义。mipmap目录放应用图标。

理解这些目录的职责后,你再去看教程和文档,就不会觉得人家说的是天书了。比如“把图片放到drawable里”,新手可能直接把图片拖到模拟器上,其实只需要在res/drawable目录右键粘贴即可。

4.2 build.gradle.kts里那几个版本号到底是什么意思

另一个让新手头皮发麻的文件是app/build.gradle.kts,里面有三个版本号:compileSdk(编译SDK版本)、minSdk(最低支持版本)、targetSdk(目标版本)。这三个版本号的区别是Android开发中最基础也最容易被忽略的知识点。

用生活化的比喻解释:compileSdk是你做饭时用的菜谱版本,决定你能认识多少种新食材(新API);minSdk是你家的最低容忍度,决定你的菜能端到多老旧的饭桌上;targetSdk是你想迎合的口味标准,决定你按哪个年代的规矩来做事。用户看到的系统弹窗、权限规则都跟targetSdk相关,如果把targetSdk设得很高,但App的代码里没有适配新版权限模型,就很容易闪退或功能异常。

android { namespace = "com.mingzi.firstapp" compileSdk = 35 defaultConfig { applicationId = "com.mingzi.firstapp" minSdk = 24 targetSdk = 35 versionCode = 1 versionName = "1.0" } }

versionCode是给系统看的版本号,每次提交新版本必须递增;versionName是给用户看的版本号,比如“1.0.0”这种。这两兄弟容易搞混,记住一句话:versionCode管安装和升级判断,versionName管显示。

4.3 res目录里为什么没有drawable-hdpi就不舒服

res目录下有很多带后缀的子目录,比如drawable-mdpi、drawable-hdpi、drawable-xhdpi等。这是为了适配不同屏幕密度的设备。Android设备屏幕像素密度差异极大,同一张图片在低密度设备和高密度设备上显示大小会完全不同。通过把不同分辨率的图片放到对应目录,系统会自动选择合适的那一张。

对于练手项目,你只需要把图片放到drawable目录(不带后缀)即可,Android会自动按通用方式处理。知道这些目录的区别,主要是为了后面你做正式项目时不至于把放图片搞乱。

5. 亲手写一个能跑的App:UI界面和逻辑交互

5.1 用XML布局做一个计数器界面

理论说够了,开始写代码。我们做一个最经典的计数器App:界面上有一个数字文本、一个“加一”按钮和一个“清零”按钮。这个App麻雀虽小五脏俱全,包含界面布局、控件绑定和事件监听三大核心知识点。

打开res/layout/activity_main.xml,修改成下面的内容:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:padding="16dp"> <TextView android:id="@+id/tv_number" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" android:textSize="48sp" android:textStyle="bold" android:padding="24dp" /> <Button android:id="@+id/btn_increase" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="加一" android:paddingStart="32dp" android:paddingEnd="32dp" android:layout_marginTop="16dp" /> <Button android:id="@+id/btn_reset" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="清零" android:paddingStart="32dp" android:paddingEnd="32dp" android:layout_marginTop="8dp" /> </LinearLayout>

这里用LinearLayout作为根布局,属性android:orientation="vertical"表示子控件垂直排列,android:gravity="center"让所有子控件居中。match_parent让控件宽度填满父容器,wrap_content让控件大小跟随内容。每个控件都有一个id,这个id是Java/Kotlin代码里找到控件的凭证,相当于给控件贴了个标签。

layout_width和layout_height这两个属性是从Android诞生到现在最核心的布局概念,没有之一。所有布局问题、屏幕适配问题,追根溯源都是这两个属性没吃透。

5.2 Kotlin代码里绑定控件和事件

打开MainActivity.kt,我们写对应逻辑。先说明一下,新版Android Studio默认使用Kotlin,它比Java代码简洁得多。

package com.mingzi.firstapp import android.os.Bundle import android.widget.Button import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { private var count = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initViews() } private fun initViews() { val tvNumber = findViewById<TextView>(R.id.tv_number) val btnIncrease = findViewById<Button>(R.id.btn_increase) val btnReset = findViewById<Button>(R.id.btn_reset) btnIncrease.setOnClickListener { count++ tvNumber.text = count.toString() Toast.makeText(this, "当前计数:$count", Toast.LENGTH_SHORT).show() } btnReset.setOnClickListener { count = 0 tvNumber.text = "0" } } }

setContentView(R.layout.activity_main)这行代码把XML布局和Activity关联起来,相当于把设计稿搬到了舞台上。findViewById是寻找控件的入口,找到控件后给按钮设置setOnClickListener监听器,里面的代码就是点击时要执行的动作。

这里的count变量存计数值,点击“加一”时自增并刷新文本显示,同时弹一个Toast提示。点击“清零”时重新置零。

有个细节值得注意:onCreate方法中的savedInstanceState参数。当手机旋转屏幕或系统回收App时,Activity会重新创建,这个参数可以帮你保存和恢复状态。新手阶段可以忽略,但面试和实际项目中,这是必考必踩的点。

5.3 在模拟器还是真机上运行

写完代码后,点击Android Studio工具栏的绿色三角按钮(Run),会让你选择运行设备。两种选择各有优劣:

模拟器适合快速验证UI效果,但不适合测试相机、蓝牙、GPS等硬件相关功能,而且启动慢、占内存大。真机调试更接近真实用户环境,但需要手机开启开发者模式和USB调试。权衡下来,我建议新手优先真机调试,不仅省去模拟器一堆配置问题,而且能直观感受App安装到手机上的完整过程。

真机连接步骤:

  1. 手机进入“设置” → “关于手机”,连续点击“版本号”7次,直到提示“已进入开发者模式”。
  2. 返回设置页,进入“开发者选项”,打开“USB调试”。
  3. 用数据线连接电脑,手机弹出授权对话框,勾选“始终允许”,点击确定。
  4. 在Android Studio中点击Run,设备列表中会显示你的手机型号。

注意:USB调试授权弹窗只会在首次连接时出现,如果之前点掉了,可以在开发者选项里选择“撤销USB调试授权”后重新连接。

点击运行后,Android Studio会自动完成编译、打包、安装、启动的全流程。你会在Android Studio底部的Build窗口看到一堆日志信息,看到BUILD SUCCESSFUL就说明成功了。

6. 打包APK:搞清楚Debug签名和Release签名的区别

6.1 debug和release两种模式

在Android开发中,同样的代码可以打出两种格式的APK:Debug版和Release版。Android Studio点Run时自动安装的是Debug版,它使用系统自动生成的调试签名,方便开发调试,但缺点包含调试信息和泄漏检测逻辑,性能差一些,不适合发布。

要打出正式分发的APK,需要切换到Release构建类型,使用自己的签名文件签名。这就是很多新手的困惑点:为什么我从IDE里直接装到手机上能跑,但把app-debug.apk发给别人,别人装上却提示“应用未安装”或“解析包错误”?原因九成是签名不一致或没有签名。

6.2 生成自己的签名文件

签名文件是APK的身份证和防伪标识。没有签名的APK无法安装到安卓系统上,签名不一致的APK无法覆盖安装。生成签名文件的步骤如下:

点击Android Studio菜单栏的“Build” → “Generate Signed App Bundle / APK”,选择“APK”选项,然后点击“Create new...”。填写Key store路径、密码、别名等信息。这里提醒一点:密钥库文件和密码务必妥善保管,一旦丢失,你永远无法升级已经安装到用户手机上的App,只能换包名重发布。

推荐设置:

  • Key store path:放在项目目录外的安全位置,比如D:\keys\my-release.keystore,不要放进项目代码目录,避免提交代码时泄漏。
  • Validity(有效期):填写25年以上,因为安卓系统有规定,有效期截止日必须晚于App生命周期结束日,如果签名过期,用户将无法升级安装。

签名文件生成后会得到一个.keystore后缀的文件,这个文件大约2KB,看起来不起眼,却是你作为App开发者的“法人印章”。

6.3 正式打出Release版APK

生成签名文件后,继续向导,选择刚创建的keystore,填写密码和别名,勾选签名方式(推荐用V1和V2都勾选),点击Finish。Android Studio会在app/release/目录下生成app-release.apk文件。

生成后,记得在app/build.gradle.kts的buildTypes配置里区分签名配置,方便以后一条命令打包:

android { // 省略其他配置 signingConfigs { create("release") { storeFile = file("D:/keys/my-release.keystore") storePassword = "你的密码" keyAlias = "你的别名" keyPassword = "你的密码" } } buildTypes { release { signingConfig = signingConfigs.getByName("release") isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } }

isMinifyEnabled = true表示开启代码混淆和压缩。Release版的APK默认开启混淆,好处是代码体积变小、增加被逆向的难度;坏处是如果代码里用了反射或三方SDK,需要配置混淆规则,否则运行时可能报ClassNotFoundException。新手阶段可以把isMinifyEnabled设为false,等熟悉了再开启,否则排查问题时会额外多一个变量。

注意:千万不要把签名密码硬编码到build.gradle里然后提交到Git仓库,这是非常危险的习惯。正式项目中一般用local.properties或环境变量管理敏感信息,这个文件不会被版本控制追踪。

打包完成后,你会看到app/release/app-release.apk文件,把这个文件传到手机上,点击安装,你的第一个正式版APK就诞生了。

7. 安装到手机上:常见报错和解决链路

7.1 “应用未安装”的根因排查

当你把APK文件传到手机上点击安装时,最有可能遇到的问题是“应用未安装”或“未安装应用”,这个提示背后的原因和安装包本身、手机系统环境都有关系。用排查思路拆解一下:

第一步:检查签名是否完整。如果APK没有签名,或者签名损坏,系统直接拒绝安装。用下面的命令查看APK的签名信息:

apksigner verify --print-certs app-release.apk

第二步:检查包名冲突。当前手机已经安装了一个包名相同但签名不同的应用,直接覆盖安装会失败。卸载旧应用后重装即可。

第三步:检查Android版本兼容性。如果minSdk高于你手机的安卓版本,也会报“解析包错误”。下载安装包时要知道自己的手机安卓版本号,同时注意查看APK支持的版本范围。

第四步:检查targetSdk与系统权限策略冲突。如果targetSdk版本很高,而你传APK时通过的是不安全的文件管理器,系统可能直接拦截。

7.2 用adb命令绕过文件管理器直接安装

很多时候文件管理器安装失败,但用adb命令就能绕过去。这种方式也是开发者和高级用户最常用的安装方式。把手机连接到电脑,确保USB调试已打开,在命令行里执行:

adb install -r app-release.apk

-r参数表示覆盖安装,替换已有应用。如果安装成功,命令行会输出Success,手机上也会自动弹出安装完成的提示。如果安装失败,adb会给出具体的错误原因,比如INSTALL_FAILED_UPDATE_INCOMPATIBLE(签名不一致)、INSTALL_FAILED_OLDER_SDK(系统版本太低)、INSTALL_FAILED_INVALID_APK(APK文件损坏)等。

想卸载应用用adb uninstall 包名,想查看所有已安装应用用adb shell pm list packages | grep 包名关键字。这些命令不仅是日常调试的利器,也是排查问题的第一手工具。

7.3 从真机上查看崩溃日志

如果App装上了但一打开就闪退,不要慌。连接手机后,在命令行里执行:

adb logcat -s AndroidRuntime:E

这条命令会实时过滤并显示与Android运行时崩溃相关的错误日志。闪退时,日志里会输出一行FATAL EXCEPTION,下面跟着异常类型和出错的代码位置。新手最高频遇到的闪退原因有两个:一是布局文件中引用了不存在的资源ID,导致空指针异常;二是在主线程中做了耗时操作,导致ANR(Application Not Responding,应用无响应)。

排查这类问题的核心方法论是:先看日志,再猜原因。日志里的Caused by:那行会直接告诉你崩溃的根因,不用从头翻到尾。

8. 从第一个APK到规范开发:几个值得养成的习惯

打包出第一个APK只是起点。我在带过的很多新手身上发现,起步阶段最容易养成的坏习惯有三个:不读日志、不区分构建类型、代码和资源文件乱放。这里分享几个实操中沉淀下来的建议,对后续学习帮助很大。

第一,每次运行前先编译一遍。点击Run之前,先执行Build → Make Project,检查有没有编译错误。这样错误信息会单独呈现在Build窗口,不会混在运行日志里,排查起来清爽很多。

第二,尽早接触adb和命令行。图形界面的Android Studio能完成99%的操作,但那1%的命令行操作往往就是解决疑难杂症的钥匙。adb系列命令不仅是装包的工具,还可以查看设备信息、模拟点击、抓取系统日志。这些技能到后面做自动化测试、性能优化时都用得上。

第三,了解反编译的基本原理。很多人对“APK反编译”这个概念既好奇又恐惧。其实APK本身就是压缩包,把后缀改成.zip并解压,你能看到里面的资源文件和代码。编译后的Java/Kotlin字节码在classes.dex里,要用dex2jar和jadx这类工具才能还原成可读代码。理解这个原理很重要,因为它能帮你想明白“为什么Release版要加混淆”“为什么资源文件命名要规范”“为什么不要把密钥硬编码进代码”这些安全问题的本质。

从第一个能安装的APK开始,到能规范管理构建类型、签名配置、日志排查,这条路不算长,但每一步踩的坑都很有价值。我至今记得自己做第一个APK时,因为忘了开手机USB调试,折腾了一下午才装上,但那次折腾比任何教程都让人印象深刻。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:01:45

STM32CubeMX 6.14下载安装到工程生成完整避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:01:06

Linux下H3C iNode 7.3安装配置与802.1X认证排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:01:05

VirtualBox+Ubuntu 24.04安装与配置全攻略:虚拟机新手必看

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:59:46

Ubuntu 安装 Nvidia 显卡驱动全流程:版本选型、安装与排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:59:26

CNN人脸识别实战:从数据对齐到模型部署的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华