【视频文稿】车载Android应用开发与分析 - AOSP的下载与编译

本期内容的视频地址:https://www.bilibili.com/video/BV1kR4y1v7BK/

Hello各位好,最近工作比较忙导致这期视频拖更了,而且由于博主新冠康复的这几个月里,嗓子还是始终有异物感,说话口水多,嗓子也容易疼,个人配音效果并不好,本期视频开始使用腾讯智影工具进行配音。

本期视频我们介绍学习车载Android应用开发的第一步,如何下载、编译 AOSP源码,启动原生的车载Android模拟器 ,同时也会介绍一些常见的编译指令,以及实际工作中可能会出现的编译场景。


AOSP 概述

AOSP 概述

AOSP 安卓开源项目(Android Open Source Project),是一项旨在指导Android移动平台开发的计划 。

Android 是一个适用于移动设备的开源操作系统,也是由 Google 主导的对应开源项目。作为一个开源项目,Android 的目标是避免出现任何集中瓶颈,即没有任何行业参与者可一手限制或控制其他任何参与者的创新。为此,Android 被打造成了一个适用于消费类产品的完整高品质操作系统,并配有可自定义并运用到几乎所有设备的源代码,以及所有用户均可访问的公开文档(英文网址:source.android.com;简体中文网址:source.android.google.cn)。

学习AOSP的意义

有过一些车载应用开发经验的同学,可能会有困惑,为什么要下载 Android 源码?

确实从工作的角度来说,我们在实际的车载应用开发中往往并不会下载完整的Android源码,而是仅仅下载每个人负责的一个应用模块,但是我们在学习车载应用开发时,依然建议下载完整Android的源码进行学习。从UP自己的经历来说,学习AOSP有以下优势:

  • 参考系统应用的实现

车载Android应用开发,主要难点都集中在如何开发Android系统应用上。例如,定制Launcher时,我们首先需要理解原生的Launcher的实现方式,才能游刃有余地定制出符合产品需求的Android桌面。

  • 了解Android系统的运行原理

AOSP包含了完整的Application、Framework、Native、HAL等各各层级的源码,我们在学习时可以修改源码、添加输出日志,再编译运行查看结果,这样可以方便我们直观的理解Android系统的运行机制。

  • 工作需要

有的公司在开发车载项目时会给开发开通整个源码权限,这样方便开发在本地编译Android源码进行烧机测试,那么就需要我们掌握整个Android源码的编译方式。


源码管理工具

下载Android源码之前,要先安装其构建工具 Repo 来初始化源码。Repo是Android用来辅助Git工作的一个工具。

Repo

Repo 简介

Repo是Google使用python脚本编写的用于调用Git的脚本,主要用来下载、管理Android项目的软件仓库。

Repo不会取代Git,但是它可以让开发者在Android环境中更加轻松的使用Git。

在大多数情况下,确实可以仅使用 Git(不必使用 Repo),或结合使用 Repo 和 Git 命令以组成复杂的命令。不过,使用 Repo 执行基本的跨网络操作可大大简化我们的工作。

安装 Repo

第 1 步,创建一个bin目录,并将这个目录添加到系统的环境变量中

mkdir ~/bin
PATH=~/bin:$PATH

第 2 步,下载 repo

curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

安装完repo后需要使用source指令重新加载linux的配置环境,这里我们简单一些直接重启操作系统就行。


Git

Git 简介

Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。也是目前使用范围最广的版本控制系统。

Android 使用 Git 执行本地操作,例如建立本地分支、提交、对比差异、修改。Google 最初决定使用一种分布式修订版本控制系统,经过筛选最后选中了Git。

对于分支要求不高的项目会采用SVN或其它在线办公软件管理。例如,在UP的公司,车载项目的文档类资料以前采用SVN进行管理,现在已经统一切换到飞书系统上。

安装 Git

第 1 步,执行安装指令

sudo apt install git

第 2 步,配置 Git 全局环境

git config --global user.name "用户名"
git config --global user.email "邮箱"

第 3 步, 生成 ssh 秘钥(可选配置)

ssh-keygen

一直按回车,生成的秘钥文件会在 ~/.ssh目录下载,我们将~/.ssh目录下的id_rsa.pub 里面的内容复制到仓库管理系统相应的SSH Key中,在后续的代码下载时就不需要再输入用户名和密码。

车载项目常见的源码仓库管理系统是Gerrit 和 GItLab。

在线阅读、检索AOSP源码

http://aospxref.com/ 是一个在线的 AOSP 源码阅读网站,在这个网站上我们可以很轻松,阅读、检索 Android 的源码,而且对于车载 Android 开发而言,阅读源码十分的重要。


下载 AOSP

硬件环境

AOSP 的下载、编译理论上支持 Mac和Linux,但是个人非常不建议在非 Linux 环境下尝试编译,其中要踩的坑实在太多。

下载 Android 12 及以后的版本,需要至少100GB的硬盘空间,编译Android 12 则需要300GB的硬盘空间和16GB以上的内存,低于这个配置在编译时大概率会报错。

个人的PC环境如右所示:

下载方式

下载 AOSP 有以下两种方式

  • 使用初始化源码包

中科大和清华大学提供了AOSP源码的压缩包,现在的包大约有60GB左右,可以使用命令行或迅雷等下载工具下载。

优点:下载速度快,支持断点续传。

缺点:不贴近工作环境,实际项目中不使用这种方式。

清华大学文档:https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/

中科大文档:https://mirrors.ustc.edu.cn/help/aosp.html

  • 使用 repo 直接同步源码

使用repo直接同步源码,是Android官方文档中使用的方式。

优点:更贴近工作环境。在实际项目中也是使用这种方式同步Android源码。

缺点:源码太大,外网不稳定、速度很慢,容易同步失败(实际项目中,我们使用公司内网不会存在这个问题)。

官方文档:https://source.android.google.cn/docs/setup/download/downloading?hl=zh-cn

由于下载Android源码非常消耗服务器的带宽和I/O,中科大和清华大学的文档中都强烈要求我们使用第一种方式同步源代码。

如果下载Android官方的AOSP的源码,UP建议使用下载压缩包的方式,如果是下载公司项目的Android源码,则只能使用repo同步。

下面我们分别演示这两种下载Android源码的方式:


第 1 种方式,使用Android源码包

第 1 步,建立工作目录

开始下载AOSP源码之前,需要在合适的位置创建一个目录用于放置源码,使用如下命令

mkdir AOSP
cd AOSP

第 2 步,下载AOSP初始化包

下载AOSP初始化也有两种方式

第一种,是使用CURL命令行工具。个人建议使用这种方式下载,如果中途意外中断,在执行一次同样的CURL指令,即可进行断点续传。

curl -OC - https://mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-latest.tar

第二种,使用下载工具下载源码包。下载工具可以是浏览器,也可以是迅雷等专用下载工具

下载初始化包,可以用任意操作系统,甚至只要你的手机闪存足够大,用手机下载也无所谓。之后可以拷贝到 Linux 电脑中编译。

第 3 步,解压初始化包

下载后的初始化包是一个.tar的压缩包,既可以使用命令行解压,也可以用操作系统的图形化工具解压。解压命令如下:

tar xf aosp-latest.tar

由于压缩包非常大,解压需要一定的时间,这一步建议使用图形化工具解压,方便我们查看进度。

解压完成后的初始化包,只保留了.repo目录,使用快捷键Ctrl+H显示隐藏文件。执行 repo sync 即可拉取Android主干分支的源码。

cd aosp
repo sync 
# 或 repo sync -l 仅checkout代码

此后,每次只需运行 repo sync 即可保持与主分支同步。

不过我们并不需要主干分支的源码,我们需要选择同步指定版本的Android源码,继续如下的操作

第 4 步,同步指定分支的源码

首先切换到.repo/manifests目录,再使用git fetch指令,拉取最新的远程代码库。

cd .repo/manifests
git fetch --all
git branch -r

使用git branch -r查看目前最新分支,这里我们选择android 13的分支,执行repo指令,将当前源码库从主干分支切换到Android 13的分支上。

repo init -b android-13.0.0_r20

最后,执行repo sync 同步源码,就可以得到完整的Android 13源码了。

repo sync

第 2 种方式,使用 repo 直接同步源码

第 1 步,建立工作目录

同样的,在开始下载AOSP源码之前,需要在合适的位置创建一个目录用于放置源码,使用如下命令:

mkdir AOSP
cd AOSP

第 2 步,初始化仓库

使用repo init 指令初始化源码仓库。

repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest

第 3 步,同步源码

执行repo sync 指令同步源码。

repo sync

如果提示无法连接到 gerrit.googlesource.com,可以将以下内容配置到系统的环境变量中,然后重启操作系统。

export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'

如果需要同步指定的Android版本,只需要在repo init之后加上-b带上分支名称即可。

repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-11.0.0.0_r40
repo sync

有关repo的更多使用方式,可以详细阅读中科大或清华大学的官方文档,这里不再赘述。


Android 源码文件目录结构

  • package

应用层(Application)源码。系统应用就在这里了,比如说系统设置,桌面,相机,电话之类的。

  • frameworks

系统框架(Framework)层源码。

  • hardware

硬件抽象(HAL)层源码。

  • out

输出目录。编译以后生成的目录,相关的产出就在这里了

  • build

用于构建Android系统的工具。也就是用于编译Android系统的

目录描述
abiApplication Binary Interface 应用程序二进制接口
artAndroid Runtime。 这个会提前把字节码编译成二进制机器码保存起来,执行的时候加载速度比较快。Dalvik虚拟机则是在加载以后再去编译的,所以速度上ART会比Dalvik快一点。牺牲空间来赢取时间
bionic基础库,Android系统与Linux内核的桥梁。Bionic 音标为 bīˈänik,翻译为"仿生"
bootable系统启动引导相关程序
ctsCompatibility Test Suite 兼容性测试
dalvikdalvik虚拟机,用于解析执行dex文件的虚拟机
developers开发者目录
developerment开发目录
devices设备相关的配置信息
docs文档
external开源模组相关文件
libcore核心库
libnativehelpernative帮助库,实现JNI的相关文件
ndknative development kit
pdkPlug-in Development Kit (PDK) is designed to help you build your own pattern projects
platform_testing平台测试
prebuiltsx86/arm架构下预编译的文件
sdksoftware development kit
system底层系统文件
toolchain工具链
tools工具文件
Makefilemk文件,用于控制编译

编译 AOSP

配置编译环境

编译Android源码会用到很多第三方库,我们需要先将这些库配置好,指令如下

sudo apt update
sudo apt install flex bison build-essential zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc fontconfig -y
sudo apt install make git-core gnupg zip unzip curl python3 openjdk-11-jdk -y
sudo apt clean && sudo apt autoremove -y

编译 Android 镜像

第 1 步,加载环境变量

在Android源码根目录执行source build/envsetup.sh,该指令会将lunch以及其他程序和变量添加到shell环境中。

cd aosp
source build/envsetup.sh

第 2 步,选择编译的目标

因为是在电脑上调试编译出的版本,所以这里我们选择 car_x86_64-userdebug。

执行lunch指令之前,当前窗口的shell环境必须已经执行过source build/envsetup.sh

lunch 打开选择菜单,选择aosp_car_x86_64-userdebug也就是 11。

lunch
11

也可以直接输入lunch 11/lunch aosp_car_x86_64-userdebug`跳过列表选择,不同版本的Android源码项目,目标数字对应的目标并不一样,要注意选择。

第 3 步,执行编译

make 指令是aosp的编译指令,支持并发编译。可以使用-j指定并发编译的线程数。电脑的CPU核心数越多,可以设定的线程数就越大,编译速度也就越快,一般可以设为CPU核心数*4。如果不指定,构建系统会自动选择当前操作系统最适合的并行线程数。

make

在调用 make 命令时,如果没有指定任何目标,则将使用默认的名称为“droid”目标,该目标会编译出完整的 Android 系统镜像。

第 5 步,启动模拟器

编译的时长取决于电脑的性能,首次编译约需要耗时约2-5个小时,控制台提示 build completed successfully则表示编译成功了。然后执行emulator就可以启动模拟器了。

emulator

等待开机动画播放完毕,看到Launcher界面,就表示编译成功了。如果编译后出现模拟器黑屏无法启动,可以再执行lunch sdk_car_x86_64-userdebug,然后再make一次。

编译产出

  • 目录结构

所有的编译产物都将位于/out目录下,该目录下主要有以下几个子目录:

路径描述
/out/host/该目录下包含了针对主机的 Android 开发工具的产物。即 SDK 中的各种工具,例如:emulator,adb,aapt 等
/out/target/common/该目录下包含了针对设备的共同的编译产物,主要是 Java 应用代码和 Java 库
/out/target/product/<product_name>/包含了针对特定设备的编译结果以及平台相关的 C/C++ 库和二进制文件。其中,<product_name>是具体目标设备的名称
/out/dist/包含了为多种分发而准备的包,通过“make disttarget”将文件拷贝到该目录,默认的编译目标不会产生该目录
  • 镜像文件

编译产生的镜像文件,它们都位于/out/target/product/<product_name>/目录下,以下是Android常见镜像文件的解释。

镜像描述
cache.img缓冲区,将被挂到/cache节点
vendor.imgOEM 厂商自定义和扩展的程序运行包。将被挂载到/vendor节点
ramdisk.img一个小型文件系统。在启动时将被 Linux 内核挂载为只读分区,它包含了 /init 文件和一些配置文件。它用来挂载其他系统镜像并启动 init 进程
system.imgAndroid程序运行包,包含了 Android OS 的系统文件,framework,可执行文件以及预置的应用程序,将被挂载到/system节点。
userdata.img各个程序的存储区,包含了应用程序相关的数据以及和用户相关的数据。将被挂载为 /data节点

以上介绍了编译 Android 镜像的过程,AOSP中还有很多实用的编译指令,我们接下来再一一介绍。


常用编译指令

在上面我们编译android镜像时有提到,执行lunch指令之前,当前窗口的shell环境必须已经执行过source build/envsetup.sh,这段指令就是把envsetup.sh里的命令载到当前的bash中,可以直接调用里面的命令。

env就是环境的意思,setup可以理解为设置,这个文件加载可以理解为编译准备

比如说里面有launch指令,有make指令等等,如果你没有source的话,这些指令就无法使用。

下面我们介绍一些常用的指令。

lunch 指令

使用lunch指令选择要构建的目标。所有构建目标都采用 BUILD-BUILDTYPE 形式,其中 BUILD 是表示特定功能组合的代号。BUILDTYPE 是以下类型之一:

构建类型使用情况
user权限受限;适用于生产环境
userdebug与“user”类似,但具有 root 权限和调试功能;是进行调试时的首选编译类型
eng具有额外调试工具的开发配置

编译指令

编译指令主要以make 和 make 指令的变体为主,其中最常用的是mm,他的作用是,编译当前目录中的所有模块及其依赖项。以及mmm,编译指定目录下的所有模块及其依赖项。

指令描述
m [module name]在源码树根目录执行编译行为,相当于make指令的简写。在调用 make 命令时,如果没有指定任何目标,则将使用默认的“droid”目标,该目标会编译出完整的 Android 系统镜像
mm编译当前目录中的所有模块及其依赖项
mma编译当前目录中的所有模块及其依赖项,当该目录新增或者删除文件可以使用该命令编译
mmm [module path]编译指定目录[module path]下所有模块及其依赖项
mmma [module path]编译指定指定目录[module path]下所有模块及其依赖项,当该目录新增或者删除文件可以使用该命令编译

用于编译各种模块的指令,这些指令都可以在最后跟上 -jn来指定编译时的并发线程数目。

指令描述
m snod从构建系统中快速编译出system.img
m vnod从构建系统中快速编译出vendor.img
m pnod从构建系统中快速编译出product.img
m senod从构建系统中快速编译出system_ext.img
m onod从构建系统中快速编译出odm.img
这些指令可以帮助我们快速编译出需要的镜像文件,而不需要全局整编。

清除指令

用清理的指令,最常用的是installclean它会把编译出的二进制文件都清理掉,make clean则是清理整个out目录。

指令描述
m clobber清除所有编译缓存,相当于rm -rf out/
m clean清除编译缓存,out/target/product/[product_name]
m installclean清除所有二进制文件

跳转指令

指令描述
croot定位到根目录
godir [file name]定位到包含指定文件的目录下,file name 采用模糊匹配,如果有多个目录包含指定的filename,则需要手动选择

进行快速定位的指令。

查找指令

指令描述
jgrep在所有的Java文件上执行grep指令
cgrep在所有的C/C++文件上执行grep指令
resgrep在所有的res/*.xml文件上执行grep指令
ggrep在所有的Gradle文件上执行grep指令
mangrep在所有的androidmanifest.xml中执行grep指令
mgrep在所有的Markfiles和*.bp中执行grep指令
sepgrep在所有的sepolicy中执行grep指令
sgrep在所有的文件中执行grep指令
例如,我想查找AOSP中有多少资源文件使用了android:orientation这个属性,就可以使用这种方式,这个指令的变体主要就是限定查找的范围。

其它指令

指令描述
printconfig显示当前编译环境的配置信息
allmod显示AOSP所有的module
pathmod [module name]显示 module name 所在的路径
refreshmod刷新 module 列表
gomod [module name]定位到指定的module name 目录下

以上就是一些常用指令的介绍,下面我们带入实际场景,演示基于源码开发环境的车载应用项目,可能会遇到哪些编译情形。

常见编译情景

编译多个目标产品

有时候我们需要使用一个源码环境编译出多个目标产品,并同时保持多个目标产品的编译结果,这个时候就可以使用lunch指令,在设置新的编译目标前指定另一个输出目录(默认输出目录是/out)。

export OUT_DIR=new_out

编译 android.ipr

编译Android.ipr可以让我们在Android studio中阅读Android源码,可以更高效的进行源码间的查找与跳转。

首先需要完整编译一次源码,再编译idegen模块

mmm development/tools/idegen/

编译成功后,在根目录执行idegen.sh脚本

development/tools/idegen/idegen.sh

执行成功后,在AOSP根目录下可以找到 android.imlandroid.ipr 两个文件,我们打开android.iml这个文件发现里面的配置项非常多,主要的标签有两类:

标签描述
sourceFolder需要包含的文件目录。需要包含的文件目录越多,导入Android Studio花费的时间就越久
excludeFolder灵活排除不需要的源码,可以加快导入速度

我们可以使用下面的配置文件替代原始的配置文件,加快导入速度。该配置文件要求Android Studio只引入package模块的源码。如果有需要也可以引入frameworks模块的源码。

<?xml version="1.0" encoding="UTF-8"?>
<module version="4" relativePaths="true" type="JAVA_MODULE">
  <component name="FacetManager">
    <facet type="android" name="Android">
      <configuration />
    </facet>
  </component>
  <component name="ModuleRootManager" />
    <component name="NewModuleRootManager" inherit-compiler-output="true">
    <exclude-output />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/packages" />
      <excludeFolder url="file://$MODULE_DIR$/frameworks" />
      <excludeFolder url="file://$MODULE_DIR$/.repo" />
      <excludeFolder url="file://$MODULE_DIR$/external/bluetooth" />
      <excludeFolder url="file://$MODULE_DIR$/external/chromium" />
      <excludeFolder url="file://$MODULE_DIR$/external/icu4c" />
      <excludeFolder url="file://$MODULE_DIR$/external/webkit" />
      <excludeFolder url="file://$MODULE_DIR$/frameworks/base/docs" />
      <excludeFolder url="file://$MODULE_DIR$/out/eclipse" />
      <excludeFolder url="file://$MODULE_DIR$/out/host" />
      <excludeFolder url="file://$MODULE_DIR$/out/target/common/docs" />
      <excludeFolder url="file://$MODULE_DIR$/out/target/common/obj/JAVA_LIBRARIES/android_stubs_current_intermediates" />
      <excludeFolder url="file://$MODULE_DIR$/out/target/product" />
      <excludeFolder url="file://$MODULE_DIR$/prebuilt" />
      <excludeFolder url="file://$MODULE_DIR$/external/emma" />
      <excludeFolder url="file://$MODULE_DIR$/external/jdiff" />
    </content>
    <orderEntry type="sourceFolder" forTests="false" />
    <orderEntry type="inheritedJdk" />
  </component>
</module>

最后,打开Android Studio,点击File --> Open,选中生成的 android.ipr 文件,等待一段时间后,就可以在Android studio中阅读Android源码了。

编译应用

使用 make 加 module name,可以直接编译出Android对应 module 的应用。

如果是编译我们自己的应用,则应该在应用的源码根目录(有 Android.bp 和 Android.mk的目录)使用mm编译,也可以使用mmm加源码路径进行编译。

编译后输出路径会显示在控制台上,我们进入相应的路径就可以找到我们的编译出的二进制文件。

编译Framework

编译framework源码有多种方式。使用make framework即可编译 framework 的源码。但是要注意 make 指令后跟的是 module name 而不是模块的路径,所以这里不能写成 frameworks。

或者使用mmm frameworks/base以及在/framework/base目录下执行mm都是可以的。

编译 Android12和以后版本,编译有所调整,使用 make framework-minus-apex

输出目录:out\target\product\generic\system\framework\framework.jar

上面的jar里面都是dex,如果想看代码,可以去下面目录下看,就好像平时看第三方jar一样,导入 Android Studio即可。

\out\soong.intermediates\frameworks\base\framework\android_common\combined\framework.jar

其它的一些指令,可以参考这张表格。

模块make 命令mmm 命令
initmake initmmm system/core/init
zygotemake app_processmmm frameworks/base/cmds/app_process
system_servermake servicesmmm frameworks/base/services
java frameworkmake frameworkmmm frameworks/base
res frameworkmake framework-resmmm frameworks/base/core/res
jni frameworkmake libandroid_runtimemmm frameworks/base/core/jni
bindermake libbindermmm frameworks/native/libs/binder

我们也可以使用 allmod 指令查看所有的 module,再使用 make 指令编译我们需要的模块。

编译Car API

编译Car API就是编译和 CarService 通信的接口库,有三种不同指令:

第一种,make android.car

编译成功后的jar存放在/out/soong/.intermediates/packages/services/Car/car-lib/android.car/android_common/javac/目录下。

这种方式编译出的CarLib库,包含Car API中定义的所有方法以及实现细节。导入到android studio中打开后,如下所示:

第二种,make android.car-system-stubs

编译成功后的jar存放在/out/soong/.intermediates/packages/services/Car/car-lib/android.car-system-stubs/android_common/javac/目录下。

这种方式编译出的CarLib库,包含CarAPI中定义的所有方法,但是不包含实现细节,一些与实现细节有关的变量也会被隐藏

实际项目中这种模式较为常用。导入到android studio中打开后,如下所示:

第三种,make android.car-stubs

编译成功后的jar存放在/out/soong/.intermediates/packages/services/Car/car-lib/android.car-stubs/android_common/javac/目录下。

这种方式编译出的CarLib库,仅包含没有被@SystemApi修饰的方法,而且方法同样不包含实现细节,是最严格的编译模式。

上面这三种指令可以放在一起执行,同时编译。

make android.car android.car-system-stubs android.car-stubs

总结

本期视频我们整理了车载Android应用开发者应该了解的Android源码的下载与常用编译方式,本视频的文字内容发布在我的个人微信公众号-『车载 Android』和我的个人博客中,视频中使用的 PPT 文件的发布在我的 github中,在视频的简介中你可以找到相应的地址。

上期视频发布后,有观众提问有没有某些应用的示例可以参考,这里就可以解答一下,应用的示例我们参考AOSP中的原生应用是如何实现的即可。既可以选择下载 AOSP 源码,也可以使用我们在第二节中提到的,在线AOSP源码阅读网站。如果是商用的源码,这涉及到了法律问题,博主无法提供。

好,以上就是本视频的全部内容了,感谢您的观看,我们下期视频再见。

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值