Android studio中如何使用Ndk(Jni)?
来源:互联网 发布:网络交友论坛 编辑:程序博客网 时间:2024/06/16 05:04
本文最初发表于2015-12-18 11:22,最后更新于2016年9月20日10:00。
Android studio中如何使用Ndk(Jni)?
因为公司工作的需要,最近研究了在android studio中如何使用ndk来进行app url签名加密算法。在这个过程中遇到了许多的问题,所以写下这篇博客来记录。在研究的过程中参考了网上许多资料,自己并未记下来这些参考资料的链接,如果作者您本人看见了我这篇博客,有侵犯您知识产权的地方,请告知我,我会进行修改。先在这里对你们无私的奉献表示感谢,谢谢你们。
本文demo在Github上。
- 1.android ndk 环境
- 2.自动生成头文件(.h)
- 3.创建ndk demo
- 4.调用c语言md5算法
- 5.导入eclipse中ndk demo
1.android ndk 环境
我将我自己工作中使用到的环境列举一下,如果跟你自己使用的环境不一样,请自行研究你所处环境ndk的搭建。
(作为一名android开发人员,我相信您是有能力进行搭建自己的开发环境!)
1. 操作系统: ubuntu 14.04 LTS 64位(linux发行版的一种) [命令:uname -a]
2. gcc version 4.8.5 (我平时用eclipse来进行c语言测试) [命令:gcc -v]
3. android studio 2.1.3 (64位和32位通用)
4. java version 1.8.0_65 (linux版本64位)
5. android sdk (compileSdkVersion: 24[7.0 Nougat] buildToolsVersion:”24.0.2”)
6. android ndk (r11c)
7. CMake 3.6
8. LLDB 2.2
9. SDK Patch Applier v1
linux下一些安装提示
一:关于1 - 4点请自行搜索,不再撰述;
二:android sdk请保持最新(API=24,buildtools=24.0.2);由于某些众所周知的原因导致您更新不了sdk,那么请你自己想办法解决。
这里说下我自己常用的两个镜像:
(1),大连东软镜像:sdkmanager->tools->options->server:mirrors.neusoft.edu.cn port:80 勾上force。貌似每隔一段时间它会清除android相关镜像,为什么呢?
(2),腾讯大师兄:https://dsx.bugly.qq.com/repository/1 打开网页可以查看使用说明,这个比较稳定就是速度比较慢。
三:android ndk 我这里用的是比较旧的版本,如果你要用最新的版本,那么请自行下载;[进入bin文件所处路径。(1). sudo su 切换到root; (2).sudo chmod +x xxx.bin; (3).sudo ./xxx.bin]
四:CMake是构建脚本程序,LLDB是与native调试相关,SDK Patch Applier v1是安装LLDB时必须安装的。下面图片是ndk官方网页对cmake的描述:
下面图片是我使用到sdk tools相关:
2.自动生成头文件(.h)
在你自己module中新建一个java文件;如我这里时Signature.java 并在其中定义你自己的native方法;
public class Signature { private Signature() { } private static Signature instance; public static Signature newInstance() { if (instance == null) { instance = new Signature(); } return instance; } //native method private native String getStringFromNative(String timeStamp,String randStr); public String getGenerateSignString(String timeStamp,String randStr) { return getStringFromNative(timeStamp,randStr); }}
这时候你会发现你的native方法对应后面有红色提示错误,不用担心,现在不用管它。
接着在studio中点击Build选择MakeProject(Ctrl+F9),这一步主要是为了生成与之对应的.class文件。如下面所示:
如果你公司的项目有多个渠道打包的需求,那么这里的路径会有所变化,上面路径会变为:
../build/intermediates/classes/baidu/debug/…..[你的包名]
下面打开终端,进入你module所在路径的main文件夹下,如下图所示:
在这里输入下面命令:
javah -classpath ../../build/intermediates/classes/debug -d jni org.tuzhao.demo.activity.Safe
解释一下:
1. javah 头文件(.h)生成命令;
2. -classpath ../../build/intermediates/classes/debug 指定你的classpath路径
3. -d jni 在当前目录生成jni文件夹
4. org.tuzhao.demo.activity.Safe 包名.类名 注意大小写
操作成功,这时你可以在你的module中看见如下图所示,
(请忽略除了Signature.h之外的三个文件)
3.创建ndk Demo
如果您顺利的完成了前面两个步骤,那么恭喜你,距离调用ndk demo只差一小部分了!
根绝头文件生成源文件;我这里是将源文件与头文件的命名一样,这个就看自己的处理了。
下面是头文件(Signature.h 自动生成的)的内容:
/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class org_tuzhao_demo_activity_Signature */#ifndef _Included_org_tuzhao_demo_activity_Signature#define _Included_org_tuzhao_demo_activity_Signature#ifdef __cplusplusextern "C" {#endif/* * Class: org_tuzhao_ demo_activity_Signature * Method: getStringFromNative * Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_org_tuzhao_demo_activity_Signature_getStringFromNative (JNIEnv *, jobject, jstring, jstring);#ifdef __cplusplus}#endif#endif
接着时源文件(Signature.c 这里暂只做一个简单的字符串返回)的内容:
//// Created by tuzhao on 15-12-14.//#include "org_tuzhao_demo_activity_Signature.h"#include "md5.h"#include <jni.h>#include <stdio.h>#include <string.h>#include <malloc.h>#include <android/log.h>#define LOG_TAG "md5"#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)JNIEXPORT jstring JNICALL Java_org_tuzhao_demo_activity_Signature_getStringFromNative(JNIEnv *env, jobject obj, jstring timeStamp, jstring randStr) { LOGI("ndk start...!"); char *cTimeStamp = NULL; char *cRandStr = NULL; LOGI("-------cTimeStamp-------"); cTimeStamp = (char *) (*env)->GetStringUTFChars(env, timeStamp, 0); LOGI("%s ", cTimeStamp); LOGI("-------cRandStr-------"); cRandStr = (char *) (*env)->GetStringUTFChars(env, randStr, 0); LOGI("%s ", cRandStr); (*env)->ReleaseStringUTFChars(env, randStr, cRandStr); (*env)->ReleaseStringUTFChars(env, timeStamp, cTimeStamp); return (*env)->NewStringUTF(env, "this is come from ndk str!");}
注意
一:源文件方法申明的方式要与头文件一致;Java_包名类名方法名(参数…),请仔细看一下方法名称的构成;
二:java中String在Jni中对应的是jstring,但是这还是不能被c语言使用,还必须经过转换,上面已经列出了转换的方法,请注意查看。因为c中涉及到了指针,用完jstring之后记得释放;
现在已经有了源文件和头文件,我们可以直接在android中调用native方法了吗?答案时否定的。
我们需要在类中加载我们的c库。
static { System.loadLibrary("my-Jni"); }
这里的my-Jni是自己随便取的,你可以更换为你自己想要的名字。这个是可配置的,但是在哪里配置呢?
在我们的module的build.gradle文件中进行配置,如下代码所示:
defaultConfig { applicationId "org.tuzhao.demo.activity" minSdkVersion 14 targetSdkVersion 23 versionCode 1 versionName "1.0" ndk { moduleName "my-Jni" ldLibs "log" abiFilters("armeabi", "armeabi-v7a", "x86", "mips", "arm64-v8a", "mips64", "x86_64") } }
可以看见在defaultConfig中有一个ndk块,主要是在这里面进行相关配置;这里不同于Eclipse中开发ndk。在Eclipse中需要手动编写Android.mk配置文件,这里studio中因为采用Gradle,所以就不需要我们手动去编写了。但实际上gradle也还是编写了这个.mk文件,您如果有兴趣可以自己在项目中找一找。
注意
1.如果你是按照我上面所说的步骤来,那么你得注意的是你得必须把你的项目运行经过编译一次才能找到这个.mk文件。
2.关于abiFilters,早期android支持的cpu架构只有两三种,现在随着技术演变,所以支持的cpu架构变多了。目前在我的项目中用到了这几种,请自己根据自己的需求去适配;记得去年刚弄ndk时不懂这个就只生成了armeabi这一种,那会儿用Eclipse,apk在其它手机上都是好的,但是在华为上一直打开奔溃,现在想起来好忧桑;如果你比较懂这个希望就能留言给我讲讲,在此先谢过了。
好了,到此为止,一个简单的ndk demo就成型了,自己运行一下吧。
4.调用C语言Md5算法
这里其实c语言不止可以计算MD5,这里只是举一个示例。你可以在c中进行一系列关于你公司的加密算法处理。这样对你自己写出来的apk也算是一个安全的保障,虽然没有破解不了的代码(或者apk),但是至少比直接用java写的代码破解系数高一点吧。
在这里我们看见在studio中编辑c文件这个提示一直都在,所以使用c是你值得商榷的一件事。
关于c加密处理与java加密处理效率的问题,这里不做探讨,我好像记得java的虚拟机和一些底层是用c写的。嗯,就是这样。
下面就例举一下我在项目中用到的MD5处理方式:
这是头文件,md5.h:
//// Created by root on 15-12-17.//#ifndef EBOXSIGNATURE_MD5_H#define EBOXSIGNATURE_MD5_Htypedef struct { unsigned int count[2]; unsigned int state[4]; unsigned char buffer[64];} MD5_CTX;#define F(x, y, z) ((x & y) | (~x & z))#define G(x, y, z) ((x & z) | (y & ~z))#define H(x, y, z) (x^y^z)#define I(x, y, z) (y ^ (x | ~z))#define ROTATE_LEFT(x, n) ((x << n) | (x >> (32-n)))#define FF(a, b, c, d, x, s, ac) \ { \ a += F(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ }#define GG(a, b, c, d, x, s, ac) \ { \ a += G(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ }#define HH(a, b, c, d, x, s, ac) \ { \ a += H(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ }#define II(a, b, c, d, x, s, ac) \ { \ a += I(b,c,d) + x + ac; \ a = ROTATE_LEFT(a,s); \ a += b; \ }void MD5Init(MD5_CTX *context);void MD5Update(MD5_CTX *context, unsigned char *input, unsigned int inputlen);void MD5Final(MD5_CTX *context,unsigned char digest[16]);void MD5Transform(unsigned int state[4], unsigned char block[64]);void MD5Encode(unsigned char *output, unsigned int *input, unsigned int len);void MD5Decode(unsigned int *output, unsigned char *input, unsigned int len);char *join(char *a, char *b);unsigned char *md5ToHex(unsigned char *md5Decimalism, int memLength, int lowercase);char *md5_calculate(char *randStr, char *timeStamp);#endif //EBOXSIGNATURE_MD5_H
这是源文件md5.c :
/* * md5.c * * Created on: 2015年12月16日 * Author: tuzhao */#include <memory.h>#include "md5.h"#include <android/log.h>#define LOG_TAG "md5"#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)unsigned char PADDING[] = {0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};void MD5Init(MD5_CTX *context) { context->count[0] = 0; context->count[1] = 0; context->state[0] = 0x67452301; context->state[1] = 0xEFCDAB89; context->state[2] = 0x98BADCFE; context->state[3] = 0x10325476;}void MD5Update(MD5_CTX *context, unsigned char *input, unsigned int inputlen) { unsigned int i = 0, index = 0, partlen = 0; index = (context->count[0] >> 3) & 0x3F; partlen = 64 - index; context->count[0] += inputlen << 3; if (context->count[0] < (inputlen << 3)) context->count[1]++; context->count[1] += inputlen >> 29; if (inputlen >= partlen) { memcpy(&context->buffer[index], input, partlen); MD5Transform(context->state, context->buffer); for (i = partlen; i + 64 <= inputlen; i += 64) MD5Transform(context->state, &input[i]); index = 0; } else { i = 0; } memcpy(&context->buffer[index], &input[i], inputlen - i);}void MD5Final(MD5_CTX *context, unsigned char digest[16]) { unsigned int index = 0, padlen = 0; unsigned char bits[8]; index = (context->count[0] >> 3) & 0x3F; padlen = (index < 56) ? (56 - index) : (120 - index); MD5Encode(bits, context->count, 8); MD5Update(context, PADDING, padlen); MD5Update(context, bits, 8); MD5Encode(digest, context->state, 16);}void MD5Encode(unsigned char *output, unsigned int *input, unsigned int len) { unsigned int i = 0, j = 0; while (j < len) { output[j] = input[i] & 0xFF; output[j + 1] = (input[i] >> 8) & 0xFF; output[j + 2] = (input[i] >> 16) & 0xFF; output[j + 3] = (input[i] >> 24) & 0xFF; i++; j += 4; }}void MD5Decode(unsigned int *output, unsigned char *input, unsigned int len) { unsigned int i = 0, j = 0; while (j < len) { output[i] = (input[j]) | (input[j + 1] << 8) | (input[j + 2] << 16) | (input[j + 3] << 24); i++; j += 4; }}void MD5Transform(unsigned int state[4], unsigned char block[64]) { unsigned int a = state[0]; unsigned int b = state[1]; unsigned int c = state[2]; unsigned int d = state[3]; unsigned int x[64]; MD5Decode(x, block, 64); FF(a, b, c, d, x[0], 7, 0xd76aa478); /* 1 */ FF(d, a, b, c, x[1], 12, 0xe8c7b756); /* 2 */ FF(c, d, a, b, x[2], 17, 0x242070db); /* 3 */ FF(b, c, d, a, x[3], 22, 0xc1bdceee); /* 4 */ FF(a, b, c, d, x[4], 7, 0xf57c0faf); /* 5 */ FF(d, a, b, c, x[5], 12, 0x4787c62a); /* 6 */ FF(c, d, a, b, x[6], 17, 0xa8304613); /* 7 */ FF(b, c, d, a, x[7], 22, 0xfd469501); /* 8 */ FF(a, b, c, d, x[8], 7, 0x698098d8); /* 9 */ FF(d, a, b, c, x[9], 12, 0x8b44f7af); /* 10 */ FF(c, d, a, b, x[10], 17, 0xffff5bb1); /* 11 */ FF(b, c, d, a, x[11], 22, 0x895cd7be); /* 12 */ FF(a, b, c, d, x[12], 7, 0x6b901122); /* 13 */ FF(d, a, b, c, x[13], 12, 0xfd987193); /* 14 */ FF(c, d, a, b, x[14], 17, 0xa679438e); /* 15 */ FF(b, c, d, a, x[15], 22, 0x49b40821); /* 16 */ /* Round 2 */ GG(a, b, c, d, x[1], 5, 0xf61e2562); /* 17 */ GG(d, a, b, c, x[6], 9, 0xc040b340); /* 18 */ GG(c, d, a, b, x[11], 14, 0x265e5a51); /* 19 */ GG(b, c, d, a, x[0], 20, 0xe9b6c7aa); /* 20 */ GG(a, b, c, d, x[5], 5, 0xd62f105d); /* 21 */ GG(d, a, b, c, x[10], 9, 0x2441453); /* 22 */ GG(c, d, a, b, x[15], 14, 0xd8a1e681); /* 23 */ GG(b, c, d, a, x[4], 20, 0xe7d3fbc8); /* 24 */ GG(a, b, c, d, x[9], 5, 0x21e1cde6); /* 25 */ GG(d, a, b, c, x[14], 9, 0xc33707d6); /* 26 */ GG(c, d, a, b, x[3], 14, 0xf4d50d87); /* 27 */ GG(b, c, d, a, x[8], 20, 0x455a14ed); /* 28 */ GG(a, b, c, d, x[13], 5, 0xa9e3e905); /* 29 */ GG(d, a, b, c, x[2], 9, 0xfcefa3f8); /* 30 */ GG(c, d, a, b, x[7], 14, 0x676f02d9); /* 31 */ GG(b, c, d, a, x[12], 20, 0x8d2a4c8a); /* 32 */ /* Round 3 */ HH(a, b, c, d, x[5], 4, 0xfffa3942); /* 33 */ HH(d, a, b, c, x[8], 11, 0x8771f681); /* 34 */ HH(c, d, a, b, x[11], 16, 0x6d9d6122); /* 35 */ HH(b, c, d, a, x[14], 23, 0xfde5380c); /* 36 */ HH(a, b, c, d, x[1], 4, 0xa4beea44); /* 37 */ HH(d, a, b, c, x[4], 11, 0x4bdecfa9); /* 38 */ HH(c, d, a, b, x[7], 16, 0xf6bb4b60); /* 39 */ HH(b, c, d, a, x[10], 23, 0xbebfbc70); /* 40 */ HH(a, b, c, d, x[13], 4, 0x289b7ec6); /* 41 */ HH(d, a, b, c, x[0], 11, 0xeaa127fa); /* 42 */ HH(c, d, a, b, x[3], 16, 0xd4ef3085); /* 43 */ HH(b, c, d, a, x[6], 23, 0x4881d05); /* 44 */ HH(a, b, c, d, x[9], 4, 0xd9d4d039); /* 45 */ HH(d, a, b, c, x[12], 11, 0xe6db99e5); /* 46 */ HH(c, d, a, b, x[15], 16, 0x1fa27cf8); /* 47 */ HH(b, c, d, a, x[2], 23, 0xc4ac5665); /* 48 */ /* Round 4 */ II(a, b, c, d, x[0], 6, 0xf4292244); /* 49 */ II(d, a, b, c, x[7], 10, 0x432aff97); /* 50 */ II(c, d, a, b, x[14], 15, 0xab9423a7); /* 51 */ II(b, c, d, a, x[5], 21, 0xfc93a039); /* 52 */ II(a, b, c, d, x[12], 6, 0x655b59c3); /* 53 */ II(d, a, b, c, x[3], 10, 0x8f0ccc92); /* 54 */ II(c, d, a, b, x[10], 15, 0xffeff47d); /* 55 */ II(b, c, d, a, x[1], 21, 0x85845dd1); /* 56 */ II(a, b, c, d, x[8], 6, 0x6fa87e4f); /* 57 */ II(d, a, b, c, x[15], 10, 0xfe2ce6e0); /* 58 */ II(c, d, a, b, x[6], 15, 0xa3014314); /* 59 */ II(b, c, d, a, x[13], 21, 0x4e0811a1); /* 60 */ II(a, b, c, d, x[4], 6, 0xf7537e82); /* 61 */ II(d, a, b, c, x[11], 10, 0xbd3af235); /* 62 */ II(c, d, a, b, x[2], 15, 0x2ad7d2bb); /* 63 */ II(b, c, d, a, x[9], 21, 0xeb86d391); /* 64 */ state[0] += a; state[1] += b; state[2] += c; state[3] += d;}/*不改变字符串a,b, 通过malloc,生成第三个字符串c, 返回局部指针变量*/char *join(char *a, char *b) { char *result = malloc(strlen(a) + strlen(b) + 1);//+1 for the zero-terminator strcpy(result, a); strcat(result, b); return result;}/* * 函数定义: * 用于把10进制表示的md5无符号字符数组,转换为16进制表示的md5字符串 * 参数 md5Decimalism 是指向10进制表示形式的md5无符号字符数组的指针 * 参数 memLength 是md5Decimalism指向的内存区域大小的字节表示 * 参数 lowercase表示16进制字母是否采用小写,非零值为采用小写 * 返回值 函数的返回值是一个指向字符串的指针,这个字符串是输入的md5的16进 * 制表示,占用的内存字节数为memLength*2+1,异常情况返回NULL,请务必检测 */unsigned char *md5ToHex(unsigned char *md5Decimalism, int memLength, int lowercase) { unsigned char *result = NULL; int i = 0; unsigned char offset = lowercase ? 87 : 55;/*16进制字母部分采用小写形式偏移87,采用大写形式偏移55*/ /*输入校验*/ if (memLength <= 0) { return result; } result = (unsigned char *) malloc(memLength * 2 + 1);/*为结果请求内存地址*/ if (result == NULL)return result; memset(result, 0, memLength * 2 + 1);/*重置内存结果*/ while (i < memLength) { memset( &result[i * 2], md5Decimalism[i] / 16 < 10 ? md5Decimalism[i] / 16 + 48/*16进制非字母部分偏移48*/ : md5Decimalism[i] / 16 + offset, sizeof(unsigned char));/*存储一字节8位的前四位*/ memset( &result[i * 2 + 1], md5Decimalism[i] % 16 < 10 ? md5Decimalism[i] % 16 + 48/*16进制非字母部分偏移48*/ : md5Decimalism[i] % 16 + offset, sizeof(unsigned char));/*存储一字节8位的后四位*/ i++; } return result;}/** * calculate md5 for string */char *md5_calculate(char *randStr, char *timeStamp) { unsigned char decrypt[16]; char *strTemp = NULL; strTemp = join(randStr, timeStamp); MD5_CTX md5; MD5Init(&md5); MD5Update(&md5, strTemp, strlen((char *) strTemp)); MD5Final(&md5, decrypt); unsigned char *b = NULL; b = md5ToHex(decrypt, 16 * sizeof(unsigned char), 1);/*函数调用*/ LOGI("%s", b); return b;}
注意:
1.md5.h和md5.c这两个文件参考自网上,自己做了一些调整。如果您是这两个文件的作者,请您与我联系,我好标明出处。如果您认为我侵犯你的知识权,那么请你通知我,我将将这两个文件从这篇博客中移除。
2.关于这个module请查看demo项目地址,module名称为:ndkDemoTwo;
3.我之前用的另外一个md5计算方式,如果位数超过了56位就出现计算不准确的现象,我也将那两个文件上传到我另外一个module中去了,名为ndkDemoOne;如果您有实践欢迎你来帮我解决这个难题,在
这里先对你表示感谢了!
具体请参考我在Github上项目地址。
5.导入eclipse中NnkDemo
其实在android studio 中导入Eclipse的ndk项目和导入普通的android项目一样。file->new->import module 即可。
导入后删除原项目中的.mk文件,在jni文件夹中只保留c文件,关于ndk的配置任然在defaultConfig中配置即可。
我目前是这样做的,如果你认为有不对的地方,欢迎指正。
在我的这个project中有一个从Eclipse中导入的ndk demo,module名称叫做ndkDemoFromEclipse ,这个demo是我的好友HaoYu所写。
具体请参考我在Github上项目地址。
本人才疏学浅,出错在所难免。如果文中有错误的地方望指正,谢谢!
欢迎关注我的Github账号:https://github.com/tuzhao
本文最初发表于2015-12-18 11:22,最后更新于2016年9月20日10:00。
- Android studio中如何使用Ndk(Jni)?
- Android Studio中使用NDK/JNI
- Android Studio中使用NDK/JNI
- 【笔记】Android Studio中使用NDK-JNI
- NDK 开发之 Android Studio 中使用 JNI
- 如何在Android Studio中使用JNI
- Android Studio中JNI NDK开发(一)
- Android Studio中JNI NDK开发(二)
- Android Studio中JNI NDK开发(三)
- Android studio 使用NDK ,jni调用
- Android studio 使用ndk开发JNI
- android studio JNI/NDK的简单使用
- android studio + NDK + JNI
- Android中JNI使用详解(2)---Android Studio中NDK环境配置
- Android Studio中使用NDK
- Android studio 中使用Jni
- Android NDK开发之旅(2):一篇文章搞定Android Studio中使用CMake进行NDK/JNI开发
- Android NDK开发之旅(2):一篇文章搞定Android Studio中使用CMake进行NDK/JNI开发
- SD卡分区创建详细教程
- spring mvc 框架项目tomcat 移植到WebSphere
- ios搭一个简易计算器(利用masonry布局)
- 奥迪A9霸气登场 一改低调形象被称凶悍
- JSON及其在项目中的使用
- Android studio中如何使用Ndk(Jni)?
- freeMarker使用记录
- 开始学习C和C++
- Linux指令--tar,gzip
- 冒泡排序、选择排序的区别
- select、poll、epoll之间的区别总结
- java解析和生成xml文件的补充
- BIRCH(Balanced Iterative Reducing and Clustering Using Hierarchies)
- 十大算法,让你轻松进阶高手