使用NDK方式调用jni将密码保存到so文件中

要使用NDK首先要了解NDK到底是什么?

(英语:native development kit,简称NDK)是一种基于原生程序接口的软件开发工具。通过此工具开发的程序直接以本地语言运行,而非虚拟机。因此只有java等基于虚拟机运行的语言的程序才会有原生开发工具包。[维基百科]

NDK是一系列工具的集合

NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的.

NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。

NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

为什么要使用?

1、代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。能够阻挡一定的开发者进行逆向。

2、可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。

3、提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。

4、便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。

NDK和CMake 的下载和安装

打开android studio 找到Sdk管理器,进行下载安装即可。

下面开发正式开发:

1、创建jni文件夹用来存放调用C++相关的文件,有两个配置文件和头文件以及源文件

配置文件:Android.mk

作用:命令行cd到src/main/jni文件夹下,使用命令ndk-build生成.so文件,这里边定义了生成.so库的文件名、指定编译的c++源文件和头文件,用于向构建系统描述源文件和共享库,让ndk去按照指定的方式编译c++源文件和头文件。

编译目标目录声明

LOCAL_PATH:=$(call my-dir)   LOCAL_PATH需要编译源文件所在的目录,$(call my-dir)使用宏定义,my-dir是返回值对应Android.mk所在的目录。

重置全局变量

include $(CLEAR_VARS)          CLEAR_VARS变量指向特殊 GNU Makefile, 可为您清除许多LOCAL_XXX变量.不包括LOCAL_PATH因为系统在单一 GNU Make 执行环境(其中所有变量都是全局的)中解析所有构建控制文件. 在描述每个模块之前, 必须声明(重新声明)此变量

LOCAL_MODULE:=secretkey 指定模块的名称

LOCAL_MODULE, 指定模块的名称,唯一且不含空格, 之后会编译出librecorder-jni.so, 如果模块名已包含前缀lib, 则不会自动添加lib前缀.

LOCAL_SRC_FILES:=secretkey.cpp secretkey.h 指定编译c++源文件和头文件

待编译的源文件

LOCAL_SRC_FILES,指定源文件列表, 多个文件使用空格分割.可以使用相对文件路径(指向 LOCAL_PATH)和绝对文件路径

LOCAL_LDLIBS :=-llog 打印log

include $(BUILD_SHARED_LIBRARY)整合

BUILD_SHARED_LIBRARY变量指向GNU Makefile脚本, 用于收集您自最近 include 后在 LOCAL_XXX 变量中定义的所有信息.其实就是让上一次include到这里之间的内容生效

编译模块输出名称

LOCAL_MODULE_FILENAME, 真正的库输出文件名. 如果不喜欢系统自动生成的文件名, 可以指定这个值

LOCAL_MODULE := foo

LOCAL_MODULE_FILENAME := libnewfoo

Application.mk

此文件枚举并描述您的应用需要的模块。Android.mk有效的前提是依靠该文件的保证位于jni的目录下.包含下面几个方面的内容:

用于针对特定平台进行编译的 ABI。

工具链。

要包含的标准库(静态和动态 STLport 或默认系统)。

APP_STL := stlport_static 静态标准库

Android NDK 默认使用的是最小支持的C++运行库,如果你需要你的NDK程序中使用STL,则可以设置APP_STL := stlport_static,APP_STL有表二中的几种取值。

NameExplanation

system(default)系统默认的C++运行库

stlport_static以静态链接方式使用的sttport版本的STL

stlport_shared以动态链接方式使用的sttport版本的STL

gnustl_static以静态链接方式使用的gnustl版本的STL

gnustl_shared以动态链接方式使用的gnustl版本的STL

gabi++_static以静态链接方式使用的gabi++

gabi++_shared以动态链接方式使用的gabi++

c++_static以静态链接方式使用的LLVM libc++

c++_shared以动态链接方式使用的LLVM libc++

表二:NDK运行库

若APK中有多个SO文件用到STL,建议都使用动态方式链接STL,这样可以减小整个APK文件大小。

另外需要注意的是官方提供的NDK运行库除了默认的以外都支持RTTI和异常,然而默认是禁用的,将在下面的Android.mk中说明如何开启。

APP_OPTIM(编译模式)

“release”模式为默认的,生成的是优化后的二进制;也可以设置为“debug”模式,“debug”模式生成的是未优化二进制,提供很多BUG信息,便于调试和分析

APP_PLATFORM

指定当前程序支持android最低api水平,如APPP_PLATFORM:=16 最低支持到api16

编码实战

这里以我项目中使用ndk生成密匙一个例子进行讲解,实际项目中肯定会用到加密功能,有一些敏感的数据如用户的密码、账号等,需要将这些信息保存到本地一旦数据保存到磁盘上,如果不进行加密很有可能泄漏,造成不良的后果。

通过生成.so文件生成密匙,在一定程度上降低被盗风险,毕竟这里边设计到底层的东西比较多,安全性比硬编码之类的高了好几个档次。

1、所有文件存放的位置截图

jni存放位置

2、Android.mk配置详细信息

LOCAL_PATH:=$(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE:=secretkey

LOCAL_SRC_FILES:=secretkey.cpp secretkey.h

LOCAL_LDLIBS :=-llog

include $(BUILD_SHARED_LIBRARY)

3、Application.mk配置详细信息

APP_STL := stlport_static

APP_ABI := all

#app支持最小api level

APP_PLATFORM:= android-16

APP_OPTIM  := release

4、secretkey.h

#include

#defineUTF_8"UTF-8"

#ifdef__cplusplus

extern"C"{

//com.mine.cui.zxandroidlib.jni.com.mine.cui.zxandroidlib.security.SecretKeyHelper.createSecretKey

jstring

Java_com_test_lib_security_SecretKeyHelper_createSecretKey(JNIEnv*,jobject,jobject);

}

#endif

方法格式:jstring Java+全类名 (其中.用下划线分隔开)+java定义native方法的类名和native方法名

5、secretkey.app

#include

#include

#include"secretkey.h"

#include

#include

#defineLOG_TAG"robin_jni"// 自定义的LOG的标识

#defineLOGOPEN1//日志开关,1为开,其它为关

#defineLOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)

#defineLOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)

#defineLOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)

#defineLOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)

#defineLOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)

//char* to jstring

jstringtoString(JNIEnv* env,jbyteArraybyteArray) {

jclassstring_cls = env->FindClass("java/lang/String");

jmethodIDnew_string_mid = env->GetMethodID(string_cls,"",

"([BLjava/lang/String;)V");

return reinterpret_cast(env->NewObject(string_cls, new_string_mid,

byteArray, env->NewStringUTF(UTF_8)));

}

jbyteArraytoBytes(JNIEnv* env,const char* bytes) {

jclassstring_cls = env->FindClass("java/lang/String");

jmethodIDget_bytes_mid = env->GetMethodID(string_cls,"getBytes",

"(Ljava/lang/String;)[B");

return reinterpret_cast(env->CallObjectMethod(

env->NewStringUTF(bytes), get_bytes_mid, env->NewStringUTF(UTF_8)));

}

jbyteArraytoBytes(JNIEnv* env,jstringstring) {

jclassstring_cls = env->FindClass("java/lang/String");

jmethodIDget_bytes_mid = env->GetMethodID(string_cls,"getBytes",

"(Ljava/lang/String;)[B");

return reinterpret_cast(env->CallObjectMethod(string,

get_bytes_mid, env->NewStringUTF(UTF_8)));

}

jbyteArraygetDigestedBytes(JNIEnv* env,jbyteArraycomplex_bytes) {

staticjobjectsatic_message_digest_obj = __null;

jclassmessage_digest_cls = env->FindClass("java/security/MessageDigest");

jmethodIDget_instance_mid = env->GetStaticMethodID(message_digest_cls,

"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");

if(satic_message_digest_obj == __null) {

jobjectlocal_message_digest_obj = env->CallStaticObjectMethod(

message_digest_cls, get_instance_mid, env->NewStringUTF("MD5"));

satic_message_digest_obj = env->NewGlobalRef(local_message_digest_obj);

env->DeleteLocalRef(local_message_digest_obj);

}

jmethodIDdigest_mid = env->GetMethodID(message_digest_cls,"digest",

"([B)[B");

env->DeleteLocalRef(message_digest_cls);

return reinterpret_cast(env->CallObjectMethod(

satic_message_digest_obj, digest_mid, complex_bytes));

}

jstringtoHex(JNIEnv* env,jbyteArraydigested_bytes) {

jclassbig_integer_cls = env->FindClass("java/math/BigInteger");

jmethodIDnew_big_integer_mid = env->GetMethodID(big_integer_cls,"",

"(I[B)V");

jobjectbig_integer_obj = env->NewObject(big_integer_cls,

new_big_integer_mid,1, digested_bytes);

env->DeleteLocalRef(digested_bytes);

jmethodIDto_String_mid = env->GetMethodID(big_integer_cls,"toString",

"(I)Ljava/lang/String;");

env->DeleteLocalRef(big_integer_cls);

return reinterpret_cast(env->CallObjectMethod(big_integer_obj,

to_String_mid,16));

}

jstringgetMD5(JNIEnv* env,jstringjInfo) {

jbyteArraydigested_bytes = getDigestedBytes(env, toBytes(env, jInfo));

returntoHex(env, digested_bytes);

}

jstringgetAppendedString(JNIEnv* env,jobjectthiz,jstrings1,jstrings2) {

const char*s1x = (env)->GetStringUTFChars(s1,NULL);

const char*s2x = (env)->GetStringUTFChars(s2,NULL);

char*sall =new char[strlen(s1x) + strlen(s2x) +1];

strcpy(sall, s1x);

strcat(sall, s2x);

jstringretval = (env)->NewStringUTF(sall);

(env)->ReleaseStringUTFChars(s1, s1x);

(env)->ReleaseStringUTFChars(s2, s2x);

free(sall);

returnretval;

}

jobjectgetInstance(JNIEnv* env,jclassobj_class) {

jmethodIDconstruction_id = env->GetMethodID(obj_class,"","()V");

jobjectobj = env->NewObject(obj_class, construction_id);

returnobj;

}

//获取deviceid

jstringgetDeviceID(JNIEnv*env,jobjectthiz,jobjectmContext) {

jclasscls_context = (env)->FindClass("android/content/Context");

if(cls_context ==0) {

return(env)->NewStringUTF("unknown");

}

jmethodIDgetSystemService = (env)->GetMethodID(cls_context,

"getSystemService","(Ljava/lang/String;)Ljava/lang/Object;");

if(getSystemService ==0) {

return(env)->NewStringUTF("unknown");

}

jfieldIDTELEPHONY_SERVICE = (env)->GetStaticFieldID(cls_context,

"TELEPHONY_SERVICE","Ljava/lang/String;");

if(TELEPHONY_SERVICE ==0) {

return(env)->NewStringUTF("unknown");

}

jobjectstr = (env)->GetStaticObjectField(cls_context, TELEPHONY_SERVICE);

jobjecttelephonymanager = (env)->CallObjectMethod(mContext,

getSystemService, str);

if(telephonymanager ==0) {

return(env)->NewStringUTF("unknown");

}

jclasscls_tm = (env)->FindClass("android/telephony/TelephonyManager");

if(cls_tm ==0) {

return(env)->NewStringUTF("unknown");

}

jmethodIDgetDeviceId = (env)->GetMethodID(cls_tm,"getDeviceId",

"()Ljava/lang/String;");

if(getDeviceId ==0) {

return(env)->NewStringUTF("unknown");

}

jobjectdeviceid = (env)->CallObjectMethod(telephonymanager, getDeviceId);

return(jstring) deviceid;

}

//获取SerialNumber

jstringgetSerialNumber(JNIEnv*env,jobjectthiz,jobjectmContext) {

jclasscls_tm = (env)->FindClass("android/os/SystemProperties");

if(cls_tm ==0) {

return(env)->NewStringUTF("unknown");

}

jmethodIDgetDeviceId = (env)->GetStaticMethodID(cls_tm,"get",

"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");

if(getDeviceId ==0) {

return(env)->NewStringUTF("unknown");

}

jstringparam1 = (env)->NewStringUTF("ro.serialno");

jstringparam2 = (env)->NewStringUTF("unknown");

jobjectdeviceid = (env)->CallStaticObjectMethod(cls_tm, getDeviceId,

param1, param2);

return(jstring) deviceid;

}

jstringjint2jstring(JNIEnv*env,jintfirst) {

charbuf[64];// assumed large enough to cope with result

sprintf(buf,"%d", first);// error checking omitted

returnenv->NewStringUTF( buf);

}

//获取公钥

jstringgetPublicKey(JNIEnv* env,jobjectthiz,jobjectcontext) {

jclasscontext_cls = env->GetObjectClass(context);

jmethodIDget_package_manager_mid = env->GetMethodID(context_cls,

"getPackageManager","()Landroid/content/pm/PackageManager;");

jmethodIDget_package_name_mid = env->GetMethodID(context_cls,

"getPackageName","()Ljava/lang/String;");

env->DeleteLocalRef(context_cls);

jobjectpm_obj = env->CallObjectMethod(context, get_package_manager_mid);

jclasspm_cls = env->FindClass("android/content/pm/PackageManager");

jmethodIDget_package_info_mid = env->GetMethodID(pm_cls,"getPackageInfo",

"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");

jstringpackage_name =reinterpret_cast(env->CallObjectMethod(

context, get_package_name_mid));

jfieldIDflag_fid = env->GetStaticFieldID(pm_cls,"GET_SIGNATURES","I");

jintflag = env->GetStaticIntField(pm_cls, flag_fid);

env->DeleteLocalRef(pm_cls);

jobjectpi_obj = env->CallObjectMethod(pm_obj, get_package_info_mid,

package_name, flag);

env->DeleteLocalRef(package_name);

jclasspi_cls = env->FindClass("android/content/pm/PackageInfo");

jfieldIDsignatures_fid = env->GetFieldID(pi_cls,"signatures",

"[Landroid/content/pm/Signature;");

env->DeleteLocalRef(pi_cls);

jobjectsig_obj = env->GetObjectField(pi_obj, signatures_fid);

env->DeleteLocalRef(pi_obj);

jobjectArraysigs =reinterpret_cast(sig_obj);

jclasssignature_cls = env->FindClass("android/content/pm/Signature");

jmethodIDto_byte_array_mid = env->GetMethodID(signature_cls,"toByteArray",

"()[B");

jbyteArraysig_bytes =reinterpret_cast(env->CallObjectMethod(

env->GetObjectArrayElement(sigs,0), to_byte_array_mid));

jclasscertificate_factory_cls = env->FindClass(

"java/security/cert/CertificateFactory");

jmethodIDget_certificate_instance_mid = env->GetStaticMethodID(

certificate_factory_cls,"getInstance",

"(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");

jobjectcertificate_factory_obj = env->CallStaticObjectMethod(

certificate_factory_cls, get_certificate_instance_mid,

env->NewStringUTF("X509"));

jmethodIDgenerate_certificate_mid = env->GetMethodID(

certificate_factory_cls,"generateCertificate",

"(Ljava/io/InputStream;)Ljava/security/cert/Certificate;");

env->DeleteLocalRef(certificate_factory_cls);

jclasscertificate_cls = env->FindClass("java/security/cert/Certificate");

jclassbyte_input_stream_cls = env->FindClass(

"java/io/ByteArrayInputStream");

jmethodIDnew_sig_bytes_is_mid = env->GetMethodID(byte_input_stream_cls,

"","([B)V");

jobjectsig_bytes_is = env->NewObject(byte_input_stream_cls,

new_sig_bytes_is_mid, sig_bytes);

env->DeleteLocalRef(sig_bytes);

env->DeleteLocalRef(byte_input_stream_cls);

jobjectcert = env->CallObjectMethod(certificate_factory_obj,

generate_certificate_mid, sig_bytes_is);

env->DeleteLocalRef(sig_bytes_is);

env->DeleteLocalRef(certificate_factory_obj);

jmethodIDget_pubic_key_mid = env->GetMethodID(certificate_cls,

"getPublicKey","()Ljava/security/PublicKey;");

env->DeleteLocalRef(certificate_cls);

jobjectpublicKey  = env->CallObjectMethod(cert, get_pubic_key_mid);

jclasspublicKey_cls = env->GetObjectClass(publicKey);

jmethodIDtoString_mid = env->GetMethodID(publicKey_cls,"toString","()Ljava/lang/String;");

jstringpublicKey_str =static_cast(env->CallObjectMethod(publicKey,toString_mid));

env->DeleteLocalRef(cert);

env->DeleteLocalRef(publicKey_cls);

env->DeleteLocalRef(publicKey);

jclassstring_cls = env->GetObjectClass(publicKey_str);

jmethodIDindexOf_mid = env->GetMethodID(string_cls,"indexOf","(Ljava/lang/String;)I");

jstringparam = env->NewStringUTF("modulus");

jintaa = env->CallIntMethod(publicKey_str,indexOf_mid,param);

jstringparam2 = env->NewStringUTF("publicExponent");

jintbb = env->CallIntMethod(publicKey_str,indexOf_mid,param2);

jmethodIDsubstring_mid = env->GetMethodID(string_cls,"substring","(II)Ljava/lang/String;");

jstringpublicKey2_str =static_cast(env->CallObjectMethod(publicKey_str,substring_mid,aa+8,bb-1));

returnpublicKey2_str;

}

//获取签名

jstringgetSignatures(JNIEnv* env,jobjectthizz,

jobjectthiz) {

jclassnative_clazz = env->GetObjectClass(thiz);

// 得到 getPackageManager 方法的 ID

jmethodIDmethodID_func = env->GetMethodID(native_clazz,

"getPackageManager","()Landroid/content/pm/PackageManager;");

// 获得应用包的管理器

jobjectpackage_manager = env->CallObjectMethod(thiz, methodID_func);

// 获得 PackageManager 类

jclasspm_clazz = env->GetObjectClass(package_manager);

// 得到 getPackageInfo 方法的 ID

jmethodIDmethodID_pm = env->GetMethodID(pm_clazz,"getPackageInfo",

"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");

//获取包名

jmethodIDmethodID_packagename = env->GetMethodID(native_clazz,

"getPackageName","()Ljava/lang/String;");

jstringname_str =static_cast(env->CallObjectMethod(thiz,

methodID_packagename));

// 获得应用包的信息

jobjectpackage_info = env->CallObjectMethod(package_manager, methodID_pm,

name_str,64);//env->NewStringUTF("com.example.contasdf")

// 获得 PackageInfo 类

jclasspi_clazz = env->GetObjectClass(package_info);

// 获得签名数组属性的 ID

jfieldIDfieldID_signatures = env->GetFieldID(pi_clazz,"signatures",

"[Landroid/content/pm/Signature;");

// 得到签名数组,待修改

jobjectsignatur = env->GetObjectField(package_info, fieldID_signatures);

jobjectArraysignatures =reinterpret_cast(signatur);

// 得到签名

jobjectsignature = env->GetObjectArrayElement(signatures,0);

// 获得 Signature 类,待修改

jclasss_clazz = env->GetObjectClass(signature);

// 得到 hashCode 方法的 ID

jmethodIDmethodID_hc = env->GetMethodID(s_clazz,"hashCode","()I");

// 获得应用包的管理器,待修改

inthash_code = env->CallIntMethod(signature, methodID_hc);

charstr[100];

sprintf(str,"%u", hash_code);

jstringsign = env->NewStringUTF(str);

returnsign;

}

jstringgetPackageName(JNIEnv* env,jobjectthizz,jobjectthiz) {

jclassnative_clazz = env->GetObjectClass(thiz);

// 得到 getPackageManager 方法的 ID

jmethodIDmethodID_func = env->GetMethodID(native_clazz,

"getPackageManager","()Landroid/content/pm/PackageManager;");

// 获得应用包的管理器

jobjectpackage_manager = env->CallObjectMethod(thiz, methodID_func);

// 获得 PackageManager 类

jclasspm_clazz = env->GetObjectClass(package_manager);

// 得到 getPackageInfo 方法的 ID

jmethodIDmethodID_pm = env->GetMethodID(pm_clazz,"getPackageInfo",

"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");

//获取包名

jmethodIDmethodID_packagename = env->GetMethodID(native_clazz,

"getPackageName","()Ljava/lang/String;");

jstringname_str =static_cast(env->CallObjectMethod(thiz,

methodID_packagename));

returnname_str;

}

char* jstringTostring(JNIEnv* env,jstringjstr)

{

char* rtn =NULL;

jclassclsstring = env->FindClass("java/lang/String");

jstringstrencode = env->NewStringUTF("utf-8");

jmethodIDmid = env->GetMethodID(clsstring,"getBytes","(Ljava/lang/String;)[B");

jbyteArraybarr= (jbyteArray)env->CallObjectMethod(jstr, mid, strencode);

jsizealen = env->GetArrayLength(barr);

jbyte* ba = env->GetByteArrayElements(barr,JNI_FALSE);

if(alen >0)

{

rtn = (char*)malloc((size_t) (alen +1));

memcpy(rtn, ba, (size_t) alen);

rtn[alen] =0;

}

env->ReleaseByteArrayElements(barr, ba,0);

returnrtn;

}

jstring Java_com_test_lib_security_SecretKeyHelper_createSecretKey(JNIEnv* env,jobjectthizz,

jobjectthiz) {

jstringimei = getAppendedString(env, thizz, getDeviceID(env, thizz, thiz),getSerialNumber(env, thizz, thiz));

if(LOGOPEN==1){

LOGD("imei = %s",jstringTostring(env,imei));

}

jstringsign = getPublicKey(env, thizz, thiz);

if(LOGOPEN==1){

LOGD("sign = %s",jstringTostring(env,sign));

}

jstringimei_sign = getAppendedString(env, thizz, imei, sign);

if(LOGOPEN==1){

LOGD("imei_sign = %s",jstringTostring(env,imei_sign));

}

jstringpackage = getPackageName(env, thizz, thiz);

if(LOGOPEN==1){

LOGD("package = %s",jstringTostring(env,package));

}

jstringimei_sign_package = getAppendedString(env, thizz, imei_sign,package);

if(LOGOPEN==1){

LOGD("imei_sign_package = %s",jstringTostring(env,imei_sign_package));

}

//请再加入自己的移位或替换 或其他加密算法,例如我又append了一次imei

imei_sign_package = getAppendedString(env, thizz, imei_sign_package, imei);

if(LOGOPEN==1){

LOGD("imei_sign_package2 = %s",jstringTostring(env,imei_sign_package));

}

imei_sign_package = getAppendedString(env, thizz, imei_sign_package, sign);

if(LOGOPEN==1){

LOGD("imei_sign_package3 = %s",jstringTostring(env,imei_sign_package));

}

jstringsecretKey = getMD5(env, imei_sign_package);

if(LOGOPEN==1){

LOGD("secretKey = %s",jstringTostring(env,secretKey));

}

returnsecretKey;

}

6、、

public class SecretKeyHelper {

static{

System.loadLibrary("secretkey");

}

private staticStringmKey;

public staticString getSecretKey(Context context) {

if(mKey==null) {

mKey=createSecretKey(context);

}

returnmKey;

}

public static nativeString createSecretKey(Context context);

}

8、

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容