Android Studio之NDK开发

Android Studio之NDK开发

一、 前言
NDK全称是Native Development Kit,是Android提供的一个开发工具包,能够快速将开发的C,C++的动态库,协议软件包,以及优秀的软件工具,用so和应用打包成APK,自由地在Android上运行。而NDK开发工具,就是将C/C++程序,编译成为Android环境可运行的程序,再者,通过NDK实现Android中实现JNI编程。那么JNI又是啥呢?JNI的全称是Java Native Interface,即Java的本地接口,JNI可以实现Java与C/C++语言进行交互,实现JAVA与C/C++之间的贯通,实现JAVA层的穿透,实现JAVA不法完成的一些底层的功能。这么一来,通过NDK和JNI结合,就可以很方便的在Android的开发环境中使用C/C++技术互通,方便地使用C/C++所提供的开源库,实现更加广泛的应用功能。
由此,我们不难看出,NDK开发工具在Android应用中有两个使用点,一是对C/C++开源库的编译,获得在Android环境下可运行的SO文件或Lib库;二是JNI程序的编译,完美实现JAVA与C/C++程序的结合。一般情况下我们使用了Android与C/C++的结合,必定需要JNI技术,也就必定需要使用NDK开发工具。所以,我们应掌握NDK编译,掌握Android之NDK开发。

二、 NDK安装
NDK开发工具的安装,可分C/C++开源库等编译环境的NDK开发工具的安装和Android Studio编程环境的安装。前者是基于OS环境交叉编译,制作在Android环境下可运行的SO文件或Lib文件,后者是编译JNI设计技术的C/C++文件,也可以编译Android Studio环境编写的Android下运行的C/C++工程,两个安装方法是不同的,我们在这里分别进行描述。

2.1、Ubuntu环境下安装NDK开发工具

2.1.1、下载NDK安装包
进入官网下载:android-ndk-r16d-linux-x86_64.bin
我们应当注意的问题,在Android NDK版本17以后,不再提供编译功能,我们下载的版本需满足我们能的需求。
在这里插入图片描述

2.1.2、安装

#cd /usr/local/
#chmod +x android-ndk-r16d-linux-x86_64.bin
#./android-ndk-r16d-linux-x86_64.bin

在这里插入图片描述

2.1.3、环境配置

#cd /root
#vim .bashrc

在文件的最后行添加代码:

export NDK_HOME=/usr/local/android-ndk-16r/
export PATH=$PATH:$NDK_HOME

环境配置完成后,运行下面命令,表示配置环境生效。

#source .bashrc

2.2、Android Studio环境安装NDK开发工具

2.2.1、安装
运行Android studio程序,进入主界面,在主界面工具条上选择SDK manager,点击进入。
在这里插入图片描述

选择SDK tool
在这里插入图片描述

选择NDK和CMake两项工具安装,点击“ok”按钮,将自动安装NDK。

2.2.2、项目配置NDK编译环境
点击程序主界面菜单的“File”项,选择“project structure…”,进入编译环境配置。
在这里插入图片描述

在“NDK version”栏选择安装的版本,点击”OK“按钮,即编译化境配置完成。再次提醒注意的问题,版本17以后的版本,不在提供编译功能,我们需要下载版本17或以前的版本。
在这里插入图片描述

三、 Ubuntu环境下NDK编译
在这里需要说明的一点,在linux环境下编译,搭建环境比较简单,如果在windows环境下,往往需要安装cygwin,既费资源,有难找到三方提供(可能不支持cygwin),不如直接在linux环境下操作。
Linux环境下实现编译,主要是NDK运行环境配置和解决configure的配参问题。

3.1、NDK运行环境配置

export NDK=/home/ndk_build/android-ndk-r14b
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
export CPU=arm
export PREFIX=$(pwd)/android/$CPU
export ADDI_CFLAGS="-marm"

3.2、configure配参

--target-os=linux
--arch=arm
CC=$NDK_HOME/bin/ arm-linux-androideabi-gcc
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi-

3.3、具体的实例
编辑android_build.sh文件

#!/bin/bash
make clean
export NDK=/home/ndk_build/android-ndk-r14b
export SYSROOT=$NDK/platforms/android-9/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
export CPU=arm
export PREFIX=$(pwd)/android/$CPU
export ADDI_CFLAGS="-marm"
./configure --target-os=linux \
--prefix=$PREFIX --arch=arm \
--disable-doc \
--enable-shared \
--disable-static \
--disable-yasm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-symver \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install

只要注意了NDK配置和configure配参问题,解决了NDK编译的主要问题。

四、 CMakelists.txt编译
CMakelists.txt编译的讨论,主要是为了在Android Studio环境下解决编译问题。
我们知道makefile是在Linux编译c或者c++代码的时候的一种脚本文件,但是每一个功能都要写一个makefile文件,这样如果这个工程很大,而且相关性比较强的话,makefile的书写就会变得相对繁琐,更要命的是如果以后需要添加新的功能或者是新人需要修改功能的话,看起来就会特别麻烦;因为介于此,cmake的出现就是为了解决这样的问题,cmake的入门相当容易,而且管理也特别方便简单,那我们开始吧。
cmake的所有语句都写在一个CMakeLists.txt的文件中,CMakeLists.txt文件确定后,直接使用cmake命令进行运行,但是这个命令要指向CMakeLists.txt所在的目录,cmake之后就会产生我们想要的makefile文件,然后再直接make就可以编译出我们需要的结果了。更简单的解释就是cmake是为了生成makefile而存在,这样我们就不需要再去写makefile了,只需要写简单的CMakeLists.txt即可。
cmake的执行流程很简单,我们的重点是如何编写CMakeLists.txt文件呢,我们通过例子来学习cmake的语法。
例子从这篇文章中学习http://blog.csdn.net/dbzhang800/article/details/6314073,大致如下:

4.1、一个单文件的简单的例子
文件名字为main.c 内容如下:

#include <stdio.h>
int main()
{
    printf("Hello World Test!\n");
    return 0;
}

编写CMakeLists.txt文件内容如下:

project(hello_jelly)
set(APP_SRC main.c)
add_executable(${PROJECT_NAME} main.c)
#print message
message(${PROJECT_SOURCE_DIR})

解释代码:
第一个行project不是强制性的,最好加上,这会引入两个变量:
HELLO_BINARY_DIR, HELLO_SOURCE_DIR
同时也会定义两个等价的变量:
PROJECT_BINARY_DIR, PROJECT_SOURCE_DIR
外部编译要时刻区分这两个变量对应的目录
可以通过message进行输出
message(${PROJECT_SOURCE_DIR})
set 命令用来设置变量
add_exectuable 告诉工程生成一个可执行文件。
add_library 则告诉生成一个库文件。
CMakeList.txt 文件中,命令名字是不区分大小写的,而参数和变量是大小写相关的。
然后将以上两个文件放在统一目录下面,注意编译产生时候分为两种,一种是直接在当前源码目录执行cmake命令#cmake ./,但是这样会在当前目录下产生很多临时文件和目录,另一种方式就是在当前目录新建一个build目录,然后我门进入到build目录,执行命令cmake …/,这样产生的所有临时文件都会生成在build目录下,而不影响源码目录的代码,此处我们采用第二种方法。我们进入到build目录,执行命令#cmake …/,然后在当前目录可以看到文件如下

drwxrwxr-x 3 zqq zqq 4096 928 17:12 CMakeFiles
-rw-rw-r-- 1 zqq zqq  993 928 17:12 cmake_install.cmake
-rw-rw-r-- 1 zqq zqq 5479 928 17:12 Makefile

最后再在此目录执行make即可生成相应的可执行程序。

4.2、多个源文件的操作
hello.h头文件内容如下

#ifndef JELLYHELLO
#define JELLYHELLO
void hello(const char* name);
#endif

hello.c文件内容

#include <stdio.h>
#include "hello.h"
void hello(const char* name)
{
    printf("Hello my name is %s\n",name);
}

main.c文件内容如下

#include <stdio.h>
#include "hello.h"
int main()
{
    printf("Hello World Test!\n");
    hello("jelly");
    return 0;
}

然后是编写CMakeLists.txt文件

project(hello_jelly)
set(APP_SRC main.c hello.c)
add_executable(${PROJECT_NAME} ${SRC_LIST})
#print message
message(${PROJECT_SOURCE_DIR})

然后保存使用上面的方法进行cmake和make,就可以生成需要的可执行文件。

4.3、将hello.c生成一个库来调用
如果将hello生成成一个库来调用的话只需要在2的基础上修改一下CMakeLists.txt文件再进行编译即可
修改的CMakeLists.txt如下:

project(hello_jelly)
set(LIB_SRC hello.c)
set(APP_SRC main.c)
add_library(hello ${LIB_SRC})
add_executable(${PROJECT_NAME} ${APP_SRC})
target_link_libraries(${PROJECT_NAME} hello)
#print message
message(${PROJECT_NAME})

相比之下,我们只是添加了一个新的目标hello库,并将其链接到我们的demo程序
然后同样的方法进行cmake和make进行编译。

4.4、工程分类文件夹编译
在前面,我们成功的使用了库,但是源代码都是在同一个路径下面,这样如果到时候代码量比较大的话,可能就会分类,形成多个文件夹,这样我们需要把代码分开放,此时我们需要些三个CMakeLists.txt文件,目录结构如下

drwxrwxr-x 2 zqq zqq 4096 928 17:32 app
drwxrwxr-x 5 zqq zqq 4096 928 17:12 build
-rw-rw-r-- 1 zqq zqq  487 927 14:42 CMakeLists.txt
drwxrwxr-x 2 zqq zqq 4096 928 17:19 libso

我们将main.c程序放在app目录下面,hello.c hello.h放在libso文件夹下面,然后该文件夹有一个CMakeLists.txt文件,app和libso文件夹下面也有CMakeLists.txt文件,这样就有三个CMakeLists.txt文件了,那么我们接下来来编辑这个三个文件吧。
首先是app文件夹的CMakeLists.txt

project(hello_jelly)
include_directories(${PROJECT_SOURCE_DIR}/../libso)
 
set(APP_SRC main.c)
add_executable(${PROJECT_NAME} main.c)
target_link_libraries(${PROJECT_NAME} helloso)
 
message(${PROJECT_SOURCE_DIR})

然后是libso文件夹的CMakeLists.txt,其中SHARED 表示是生成的动态库,如果把SHARED去掉的话就是生成静态库

project(helloso)
set(LIB_SRC hello.c)
add_library(${PROJECT_NAME} SHARED ${LIB_SRC})

最后是外面那个和app在同一目录下的CMakeLists.txt

cmake_minimum_required (VERSION 3.2)
project(jelly_cmake)
add_subdirectory(./app)
add_subdirectory(./libso)

其表示我们要到./app和./libso文件夹下面去寻找Cmake文件然后进行编译。
最后我们在build目录下面去执行上面的命令编译即可编译出我们需要的可执行文件。

#cmake ../
#make

4.5、Cmake的install简单使用
我的理解cmake中的install其实就是一个将编译好的可执行文件或者是生成的库文件将它放到系统对应的位置,比如说可执行文件直接要放到bin目录下面,so库文件要放在对应的lib目录下面,我在上面的例子的基础上修改CMakeLists.txt文件,编辑完成后编译的步骤如下,就是多了个install步骤,这样我们就可以在Linux上面使用该执行文件,执行文件会去调用so库。

#cmake ../
#make
#make install

app目录修改的CMakeLists.txt如下:只是在之前的基础上加了最后install一行

project(hello_jelly)
include_directories(${PROJECT_SOURCE_DIR}/../libso)
 
set(APP_SRC main.c)
add_executable(${PROJECT_NAME} main.c)
target_link_libraries(${PROJECT_NAME} helloso)
 
message(${PROJECT_SOURCE_DIR})
 
install(TARGETS ${PROJECT_NAME} DESTINATION bin)

libso目录修改的CMakeLists.txt如下:只是在之前的基础上加了最后install一行

project(helloso)
set(LIB_SRC hello.c)
add_library(${PROJECT_NAME} SHARED ${LIB_SRC})
 
install(TARGETS ${PROJECT_NAME} DESTINATION ../lib)

在此需要解释下这个路径问题,install(TARGETS P R O J E C T N A M E D E S T I N A T I O N b i n ) 这 句 话 的 意 思 是 安 装 T A R G E R S h e l l o j e l l y 这 个 可 执 行 文 件 到 {PROJECT_NAME} DESTINATION bin)这句话的意思是安装TARGERS hello_jelly这个可执行文件到 PROJECTNAMEDESTINATIONbin)TARGERShellojelly{CMAKE_INSTALL_PREFIX}/bin目录下面,我测试打印我的 C M A K E I N S T A L L P R E F I X 路 径 是 / u s r / l o c a l 路 径 , b i n 前 面 不 能 有 / , 否 则 会 是 绝 对 路 径 , 它 不 再 会 去 获 取 {CMAKE_INSTALL_PREFIX}路径是/usr/local路径,bin前面不能有/,否则会是绝对路径,它不再会去获取 CMAKEINSTALLPREFIX/usr/localbin/{CMAKE_INSTALL_PREFIX}路径,
综上所述,可执行文件安装的路径是:
/usr/local/bin/
so库文件的安装路径是:
/usr/local/…/lib/
最后执行那三个命令就完了,此时你可以在你的Linux系统里面的任何目录执行./hello_jelly
注:如果执行make install的时候出现错误,可以加上sudo再次执行试试。

4.6、给出一个实际的例子

cmake_minimum_required(VERSION 3.4.1)

set(APP_SRC
    src/main/cpp/native-lib.cpp)

add_library( native-lib
             SHARED
             ${SRC_LIST})

find_library( log-lib
              log )

include_directories(libs/include)
set(DIR ../../../../libs)
add_library(avcodec-56
            SHARED
            IMPORTED)
set_target_properties(avcodec-56
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavcodec-56.so)

add_library(avdevice-56
            SHARED
            IMPORTED)
set_target_properties(avdevice-56
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavdevice-56.so)
add_library(avformat-56
            SHARED
            IMPORTED)
set_target_properties(avformat-56
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavformat-56.so)
add_library(avutil-54
            SHARED
            IMPORTED)
set_target_properties(avutil-54
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavutil-54.so)
add_library(postproc-53
            SHARED
            IMPORTED)
set_target_properties(postproc-53
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libpostproc-53.so)
add_library(swresample-1
             SHARED
             IMPORTED)
set_target_properties(swresample-1
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libswresample-1.so)
  add_library(swscale-3
              SHARED
              IMPORTED)
  set_target_properties(swscale-3
                        PROPERTIES IMPORTED_LOCATION
                        ${DIR}/armeabi/libswscale-3.so)
  add_library(avfilter-5
              SHARED
              IMPORTED)
  set_target_properties(avfilter-5
                        PROPERTIES IMPORTED_LOCATION
                        ${DIR}/armeabi/libavfilter-5.so)
target_link_libraries( native-lib
                       avfilter-5
                       avcodec-56
                       avdevice-56
                       avformat-56
                       avutil-54
                       postproc-53
                       swresample-1
                       swscale-3
                       ${log-lib}
                       android)

五、 JNI编程技术
我们为更好地掌握Android Studio JNI编程技术,我们首先介绍JAVA平台下JNI实现方法,了解JNI实现的基本要素。

5.1、编写Native的JAVA接口
一个NetworkUtils.java程序, 文件路径:d:\jni\com\example\ administrator\testfirst\

package com.example.administrator.testfirst;


public class NetworkUtils {
    public native String GetMACAddressByIP(String ipAddr);

    static {
        System.loadLibrary("libnetutils");
    }

    public static void main(String[] args) {
        System.out.printf("Jni libnetutils");
    }
}

接口函数:
public native String GetMACAddressByIP(String ipAddr);

实现接口函数调用的C/C++封装的SO库:
System.loadLibrary(“libnetutils”);

Main函数是解决JAVA编译可能出现没有主函数引起的错误。

5.2、编写JAVA编译文件
编写Java编译的build.sh文件,文件路径:d:\jni\com\example\ administrator\testfirst\

#! /bin/bash
javac NetworkUtils.java

5.3、编写生成H头文件sh文件
编写Build_c.sh文件:文件路径:d:\jni\

#! /bin/bash
Javah -o jni_example.h -classpath D:\jni;D\Android- studio\platforms\android-28\android.jar -jni com.example.administrator.testfirst.NetworkUtils

根据生成的H头文件,建立C/C++文件。

5.4、编写C/C++生成SO文件
编写MakeFile文件,文件路径:d:\jni\

CC = arm-linux-gnueabi-gcc
CFLAGS = -Wall -g -O -fPIC
CXXFLAGS =
INCLUDE  = -I ./inc -I ../comm/inc -I/usr/include -I/usr/lib/jvm/java-7-openjdk-armel/include
TARGET   = libnetutils.so
LIBPATH  = ./libs
 
vpath %.h ./inc

OBJS = com_example_administrator_testfirst_GetMACAddressByIP.o
SRCS = com_example_administrator_testfirst_GetMACAddressByIP.c

all:$(OBJS)

$(OBJS):$(SRCS)
	$(CC) $(CFLAGS) $(INCLUDE) -c $^

	$(CC) -shared -fPIC -o $(TARGET) $(OBJS)
#	mv $(TARGET) $(LIBPATH)

clean:
	rm -rf $(TARGET) $(OBJS)

5.5、编写jar包生成文件
编写build_jar.sh文件,文件路径:D:\jni\

jar cvf libjni_example.jar -C .

六、 Android studio之NDK开发
我们在前面,已经讨论了NDK安装,CMaklists.txt的编写,JNI编程技术,这里编写JNI编程技术,主要是针对Android Studio环境下JNI编程,完整地介绍使用Android Studio实现JNI编程。
Android Studio JNI编程主要由:1)NDK安装;2)NDK配置;3)配置C/C++环境;4)配置CMakelists.txt编译;5)编写JNI接口程序;6)生成H头文件;7)编写C/C++程序。
前面我已介绍了NDK安装,以及NDK配置,这里,我们开始介绍如何编译JNI程序。

6.1、导入C/C++的SO及H文件
建立armeable和include两个目录,将SO文件拷贝到armeable目录里,将H头文件拷贝到include目录里。
在这里插入图片描述

6.2、建立C/C++编译环境
修改app级的build.gradle文件,插入:

externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                abiFilters 'armeabi'
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

在这里插入图片描述

我们只要看到图中三个点存在,表示JNI编译环境已经建立起来了。其布局位置:
在这里插入图片描述

abiFilters 完整表达格式是:

abiFilters "armeabi", "armeabi-v7a" , "arm64-v8a", "x86", "x86_64", "mips", "mips64"

具体表达描述请查阅abiFilters相关资料。
对了,还有一个值得注意的是:
在这里插入图片描述

6.3、编写CMakelista.txt程序

cmake_minimum_required(VERSION 3.4.1)

set(APP_SRC
    src/main/cpp/native-lib.cpp)

add_library( native-lib
             SHARED
             ${SRC_LIST})

find_library( log-lib
              log )


include_directories(libs/include)
set(DIR ../../../../libs)
add_library(avcodec-56
            SHARED
            IMPORTED)
set_target_properties(avcodec-56
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavcodec-56.so)

add_library(avdevice-56
            SHARED
            IMPORTED)
set_target_properties(avdevice-56
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavdevice-56.so)
add_library(avformat-56
            SHARED
            IMPORTED)
set_target_properties(avformat-56
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavformat-56.so)
add_library(avutil-54
            SHARED
            IMPORTED)
set_target_properties(avutil-54
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libavutil-54.so)
add_library(postproc-53
            SHARED
            IMPORTED)
set_target_properties(postproc-53
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libpostproc-53.so)
add_library(swresample-1
             SHARED
             IMPORTED)
set_target_properties(swresample-1
                      PROPERTIES IMPORTED_LOCATION
                      ${DIR}/armeabi/libswresample-1.so)
  add_library(swscale-3
              SHARED
              IMPORTED)
  set_target_properties(swscale-3
                        PROPERTIES IMPORTED_LOCATION
                        ${DIR}/armeabi/libswscale-3.so)
  add_library(avfilter-5
              SHARED
              IMPORTED)
  set_target_properties(avfilter-5
                        PROPERTIES IMPORTED_LOCATION
                        ${DIR}/armeabi/libavfilter-5.so)
target_link_libraries( native-lib
                       avfilter-5
                       avcodec-56
                       avdevice-56
                       avformat-56
                       avutil-54
                       postproc-53
                       swresample-1
                       swscale-3
                       ${log-lib}
                       android)

将CMakelists.txt文件拷贝到app目录下。我们需要注意点,add_library( native-lib SHARED ${SRC_LIST})语句,是由SRC_LIST所有文件编译为native_lib.so文件。

6.4、编写JNI的JAVA程序

package com.example.ffmpegplay;

import android.view.Surface;

import java.util.Map;

public class FFmpegPlayer {
    public native int initNative();
    public native void stopNative();
    native void renderFrameStart();
    native void renderFrameStop();
    private native void seekNative(long positionUs) throws NotPlayingException;
    private native long getVideoDurationNative();
    public native void render(Surface surface);
    private native void deallocNative();
    private native int setDataSourceNative(String url, Map<String, String> dictionary, int videoStreamNo,
                                           int audioStreamNo, int subtitleStreamNo);
    private native void pauseNative() throws NotPlayingException;
    private native void resumeNative() throws NotPlayingException;

 static {
        System.loadLibrary("avcodec-56");
        System.loadLibrary("avdevice-56");
        System.loadLibrary("avfilter-5");
        System.loadLibrary("avformat-56");
        System.loadLibrary("avutil-54");
        System.loadLibrary("postproc-53");
        System.loadLibrary("swresample-1");
        System.loadLibrary("swscale-3");
        System.loadLibrary("native-lib");
    };
    //public static void main(String[] args) {
    //    System.out.printf("FFmpegPlayer\n");
    //}
}

文件路径按照工程路径,不需另造目录。

6.5、编写生成H文件的build.bat文件

javah -o FFmpegPlayer.h -classpath G:\VideoViewer\app\build\intermediates\javac\debug\classes;G:\Android-studio\platforms\android-28\android.jar -jni com.example.ffmpegplay.FFmpegPlayer  com.example.ffmpegplay.NotPlayingException

build.bat文件放在目录G:\VideoViewer\app\build\intermediates\javac\debug\classes下。一旦我们的工程编译成功,在该目录下会生成对应的class文件,我们就可以执行build.bat文件,生成H头文件了。运行build.bat文件:
在这里插入图片描述

注意:该文件最好备份一个到其它地方,防止丢失。

6.6、建C/C++工程,编写C\C++文件。
创建一个CPP目录于app/scrc/main/目录下,并将H头文件拷贝到CPP目录下,接下来就可以开启C/C++编程之旅了。
在这里插入图片描述

七、 Android studuio实例描述
八、

  • 4
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值