NDK开发

概述

全称:Native Development Kit,可让您使用C和C++等语言以原生代码实现应用的各个部分.

https://developer.android.google.cn/ndk

文件类型:

1
2
3
.o 等同于.Obj
.a 等同于.lib
.so等同于.dll

编译器

目前主要是clang编译器,Gcc编译器已被移除.

Gcc已被移除

下载NDK,里面有Clang编译器.

Clang编译器

编译示例:

将Clang编译器所在目录加入到环境变量path中,这个目录中有很多做好的cmd文件(如:i686-linux-android16-clang.cmd).

假如说编译Hello.c

  1. 编译
1
i686-linux-android16-clang -c -o Hello.o Hello.c

编译示例1

  1. 链接
1
i686-linux-android16-clang -o Hello Hello.o

编译示例2

生成的Hello文件格式为Elf.

  1. 将生成的文件传到手机上运行
1
2
3
4
5
6
7
//传文件到手机上
adb push Hello /data/local/tmp
//进入shell,修改/data/local/tmp/Hello路径权限为可执行
adb shell
chmod a+x /data/local/tmp/Hello
//运行程序
/data/local/tmp/Hello
注意
若要在手机上运行Hello这个程序,需要使用shell命令.若手机有Root权限,该Hello文件执行时也会有Root权限,但是apk文件不会具有Root权限,它只能得到一个带root权限的shell.

通用编译脚本

Makefile

假如说10个人开发一个软件,用VS是不行的.

Android NDK工具包中有makefile解释器.

makefile解释器

效率:

当生成的某个obj文件没有发生变化时,不重新生成,提高编译效率,可通过判断obj生成时间和.c文件的生成时间,若前者大于后者,则说明不需要重新生成,否则就需要重新生成obj文件.

语法参考:

https://github.com/seisman/how-to-write-makefile

Makefile语法参考.pdf

ndk-build脚本:

ndk-build脚本

手工编译示例:

  1. 将ndk-build.cmd所在的路径添加到系统环境变量中.

手工编译示例1

  1. 将要编译的文件放到一个名为jni或cpp的文件夹中.

手工编译示例2

  1. 新建一个Android.mk文件.

手工编译示例3

手工编译示例4

手工编译示例5

  1. 新建一个Application.mk文件.

手工编译示例6

手工编译示例7

  1. 编译.
1
ndk-build

手工编译示例8

手工编译示例9

  1. 编译清理.
1
ndk-build clean

ndk-build手工编译.7z

利用AndroidStudio来编译:

  1. AS新建一个空工程.
  2. 将上面的jni或cpp文件夹拷贝到新建的工程中.

利用AndroidStudio来编译1

  1. 在AS中选择Link C++ Project.

利用AndroidStudio来编译2

利用AndroidStudio来编译3

这个地方的右键,其实是在编译脚本中加了一段话.

利用AndroidStudio来编译4

  1. 指明NDK路径,下载了NDK,默认即可.
  2. 指定编译的平台.

利用AndroidStudio来编译5

  1. Ctrl + F9 编译工程即可.

利用AndroidStudio来编译6

ndk-build_AS编译.7z

注意

用ndk-build编译C++程序需注意,Cmake编译无需注意.

需要设置STL C++异常 Rtti的开关.

ndk-build注意

Cmake编译(Android主推)

Cmake编译概述

  1. 新建一个工程,这个工程默认就是CMake编译.

Cmake编译1

  1. 将要编译的文件,放到工程的cpp目录中.

Cmake编译2

  1. 添加编译链接内容.

Cmake编译3

Cmake编译4

  1. 指定编译的平台.

Cmake编译5

  1. Ctrl + F9编译工程即可.

Cmake编译6

Cmake编译.7z

静态库

又称归档文件,当一个工程链接静态库时,只会链接需要的Obj.

编译

手工编译

编译静态库的程序为llvm-ar.exe.

1
2
3
//mou1.h
#include <stdio.h>
void mou1();
1
2
3
4
5
6
//mou1.c
#include "mou1.h"

void mou1(){
	printf("Hello mou1\n");
}
1
2
3
4
5
6
7
//main.c
#include "mou1.h"

int main(){
	mou1();
	printf("Hello Main\n");
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Makefile
NDK_PATH := D:/AndroidSDK/ndk/21.4.7075529/toolchains/llvm/prebuilt/windows-x86_64/bin
BUILD_ABI := i686
BUILD_PLT_VERSION := android21
BUILD_CLANG := $(BUILD_ABI)-linux-$(BUILD_PLT_VERSION)-clang
BUILD_AR := $(BUILD_ABI)-linux-android-ar
BUILD_MODULE_NAME := main
BUILD_LIB_NAME := libmou.a
BUILD_FLAGS := -c
BUILD_LINKER_FLAGS := -o $(BUILD_MODULE_NAME) main.o mou1.o

all:
	$(BUILD_CLANG) $(BUILD_FLAGS) main.c mou1.c
	$(BUILD_CLANG) $(BUILD_LINKER_FLAGS)

lib:
	$(BUILD_AR) r $(BUILD_LIB_NAME) mou1.o

install:
	adb push $(BUILD_MODULE_NAME) /data/local/tmp
	adb shell chmod a+x /data/local/tmp/$(BUILD_MODULE_NAME)
	adb shell /data/local/tmp/$(BUILD_MODULE_NAME)

clean:
	del *.o $(BUILD_MODULE_NAME) $(BUILD_LIB_NAME)

静态库手工编译.rar

ndk-build

可以放到AS中进行编译.

  1. 将要编译的源文件放到jni文件夹中.
  2. 新建一个Android.mk文件.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#编译静态库
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
#存储要构建的模块的名称
LOCAL_MODULE := mou
#列举源文件
LOCAL_SRC_FILES := mou1.c mou2.c
#包含编译脚本
include $(BUILD_STATIC_LIBRARY)

#使用静态库
include $(CLEAR_VARS)
LOCAL_MODULE := main
LOCAL_SRC_FILES := main.c
LOCAL_STATIC_LIBRARIES := mou
include $(BUILD_EXECUTABLE)
  1. 新建一个Application.mk文件.
1
2
APP_PLATFORM := android-16
APP_ABI := x86_64
  1. cmd运行ndk-build进行编译. 静态库ndk-build

静态库ndk-build.7z

Cmake编译

假设编译的为Cpp文件,需要注意头文件加 extern “C”.

  1. 新建一个工程.

静态库Cmake编译1

  1. 在cpp文件夹新建源文件.

静态库Cmake编译2

  1. 指明要静态编译的文件.

静态库Cmake编译3

静态库Cmake编译4

  1. 修改编译脚本.

静态库Cmake编译5

  1. 直接编译即可.

静态库Cmake编译6

静态库Cmake编译7

Cmake使用第三方静态库

Cmake使用第三方静态库

优点

软件静态链接静态库的时候,只链接需要的代码.

缺点

某一个.o文件有Bug,其他已经链接了该静态库的软件需要重新编译链接,维护性差.

动态库

编译

手工编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Makefile
NDK_PATH := D:/AndroidSDK/ndk/21.4.7075529/toolchains/llvm/prebuilt/windows-x86_64/bin
BUILD_ABI := i686
BUILD_PLT_VERSION := android21
BUILD_CLANG := $(BUILD_ABI)-linux-$(BUILD_PLT_VERSION)-clang
BUILD_AR := $(BUILD_ABI)-linux-android-ar
BUILD_MODULE_NAME := main
BUILD_LIB_NAME := libmou.so
BUILD_FLAGS := -c
BUILD_LINKER_FLAGS := -o $(BUILD_MODULE_NAME) main.o $(BUILD_LIB_NAME)

all:
	$(BUILD_CLANG) $(BUILD_FLAGS) main.c
	$(BUILD_CLANG) $(BUILD_LINKER_FLAGS)

lib:
	$(BUILD_CLANG) -c mou1.c mou2.c
	$(BUILD_CLANG) -o $(BUILD_LIB_NAME) -fpic -shared mou1.o mou2.o

install:
	adb push $(BUILD_LIB_NAME) /data/local/tmp
	adb push $(BUILD_MODULE_NAME) /data/local/tmp
	adb shell chmod a+x /data/local/tmp/$(BUILD_MODULE_NAME)
	adb shell /data/local/tmp/$(BUILD_MODULE_NAME)

clean:
	del *.o $(BUILD_MODULE_NAME) $(BUILD_LIB_NAME)

-fpic 位置无关代码.

1
2
3
4
5
6
#编译动态库
make lib
#编译可执行文件
make
#安装可执行文件到Android中并运行
make install

动态库手工编译.7z

报错处理:

需了解Linux共享库搜索规则.

动态库报错1

共享库搜索路径:

  1. system/lib
  2. user/lib
  3. 环境变量,如果以apk运行,会将/data/data/packageName/lib设为环境变量

示例:

1
2
3
4
//查看环境变量
echo $LD_LIBRARY_PATH
//设置临时环境变量, 以:分割环境变量路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/data/local/tmp

动态库报错2

ndk-build

动态库ndk-build

cmd 输入ndk-build即可编译.

动态库NDK-BUILD.7z

Cmake编译

动态库Cmake编译

优点

某一个.so文件有Bug,只需重新编译该so文件即可.

缺点

  • 启动速度慢,但可以延迟加载.
  • 软件加载动态库的时候,Dll中的函数代码全部进内存.

查看导出函数

1
命令行 llvm-readelf -s so文件路径

看导出函数

函数不导出

1
2
//在Linux中 函数默认是导出的,若不导出需加前缀
__attribute__ ((visibility("hidden")))

函数不导出

初始化函数

不常用:

之所以说不常用,是因为不方便,每加一个功能都有可能会修改_init中的代码.

初始化函数1

常用:

方便,名字随意只要声明为构造或析构函数即可,数量不限.

初始化函数2

动态使用

相关函数:

1
2
3
4
void* dlopen(const char* __filename, int __flag);
void* dlsym(void* __handle, const char* __symbol);
int dladdr(const void* __addr, Dl_info* __info);
int dlclose(void* __handle);

示例:

动态使用

动态库使用.7z

Android API

基于Linux内核,遵循Posix标准,也有自己独有的API.

NDK

这是独有的API.

AndroidAPINDK

常用来打日志:

1
2
3
4
#include <android/log.h>
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "LuoHun", __VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, "LuoHun", __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "LuoHun", __VA_ARGS__);

添加库:

AndroidAPINDK添加库

Posix

可移植操作系统接口(Portable Operating System Interface),无窗口API.

Cygwin,该库在Win32系统下实现了POSIX系统调用的API.

errno

扩展了 C标准的errno,下面是C标准的errno.

C标准的errno

通过查文档可知该函数出错时,errno是否被设置.

errno设置

文件

相关函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//打开
int open(const char *pathname, int flags, mode_t mode);
//关闭
int close(int __fd);
//读
ssize_t read(int fd, void *buf, size_t count);
//写
ssize_t write(int fd, const void *buf, size_t count);   
//移动文件指针
off_t lseek(int fildes, off_t offset, int whence);   
//文件控制
int fcntl(int __fd, int __cmd, ...);
//指定位置读写
ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset); 
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
//文件属性
int stat(const char* __path, struct stat* __buf);
//检查文件权限
int access(const char* __path, int __mode);
//更改文件权限
int chmod(const char* __path, mode_t __mode);
int fchmod(int __fd, mode_t __mode);
//创建目录
int mkdir(const char* __path, mode_t __mode);
//遍历文件
DIR* opendir(const char* __path);

文件控制:

fcntl的用途之一是针对一个打开的文件,获取或修改访问模式和状态标识.

要获取这些设置,应将fcntl的cmd参数设置为F_GETFL.

1
2
3
4
5
6
//下述到吗可以测试文件是否以同步写方式打开
int nFlags;
nFlags = fcntl(fd, F_GETFL);
if(nFlags & O_SYNC){
   //...
}

文件属性:

利用系统调用stat(),lstat(),fstat(),可以获取文件相关的信息.

1
2
3
4
5
6
7
8
9
//返回文件的相关信息
int stat(const char* __path, struct stat* __buf);
//与stat类似,区别,如果文件属于符号链接,那么返回的信息针对是的符号链接自身
int lstat(const char* __path, struct stat* __buf);
//根据文件描述符获取文件的相关信息
int fstat(int __fd, struct stat* __buf);
/*
系统调用stat()和lstat()无需对其所操作的文件本身拥有任何的权限,但是针对指定的pathname的父目录要有执行(搜素)权限.而fstat()系统调用只要文件描述符有效,总是成功.
*/

st_mode:

st_mode

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct stat statBuf;
stat("/data/local/tmp/2.txt", &statBuf);
printf("st_uid = %d st_gid = %d st_size = %lld, st_mode = %o\n",
       statBuf.st_uid,
       statBuf.st_gid,
       statBuf.st_size,
       statBuf.st_mode);
if (S_ISREG(statBuf.st_mode)) {
    printf("常规文件\n");
}
if (statBuf.st_mode & S_IRUSR) {
    printf("拥有读权限\n");
}

目录权限:

目录与文件拥有相同的权限方案,只是对3种权限的含义另有所指.

  • 读权限:可列出目录之下的内容.
  • 写权限:可在目录创建,删除文件.
  • 可执行权限:可访问目录中的文件(搜索权限).

权限检查算法:

  • 对于特权级进程,授予所有访问权限.
  • 若进程的有效用户ID与文件的用户ID相同,内核会根据文件的属主权限,授予进程相应的访问权限.
  • 若进程的有效组ID与文件的组ID相匹配,内核会根据文件的属组权限,授予进程相应的访问权限.
  • 若以上3点都不满足,内核会根据文件的其他权限,授予进程相应的访问权限.

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <jni.h>
#include <string>
#include <stdio.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

#include <android/log.h>
#include <errno.h>

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "Luo", __VA_ARGS__);
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, "Luo", __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Luo", __VA_ARGS__);

int main() {

/*  FILE* fp = fopen("/data/local/tmp/1.txt", "r");
    if (fp == NULL){
        //当C库函数出错了,会将错误传给全局变量errno
        printf("fopen error %d\n", errno);
        //将错误转换为字符串
        perror("fopen");
        //传errno,将错误字符串返回,方便界面显示
        char* pErr =  strerror(errno);
        printf("%s\n", pErr);
    }*/

    int fd = open("/data/local/tmp/2.txt", O_RDWR | O_CREAT, S_IRWXU);
    if (fd < 0) {
        perror("open");
    }

    int bytes = write(fd, "Hello", 5);
    if (bytes < 0) {
        perror("write");
    } else {
        printf("write bytes: %d\n", bytes);
    }

    if (lseek(fd, 0, SEEK_SET) < 0) {
        close(fd);
        perror("lseek");
        return 0;
    }

    char szBuf[0x100] = {};
    ssize_t readBytes = read(fd, szBuf, sizeof szBuf);
    if (readBytes < 0) {
        close(fd);
        perror("read");
        return 0;
    }
    printf("szBuf = %s readBytes = %d\n", szBuf, readBytes);

    //获取文件属性
    struct stat statBuf;
    stat("/data/local/tmp/2.txt", &statBuf);
    printf("st_uid = %d st_gid = %d st_size = %lld, st_mode = %o\n",
           statBuf.st_uid,
           statBuf.st_gid,
           statBuf.st_size,
           statBuf.st_mode);
    if (S_ISREG(statBuf.st_mode)) {
        printf("常规文件\n");
    }
    if (statBuf.st_mode & S_IRUSR) {
        printf("拥有读权限\n");
    }

    //修改文件权限 相同Uid才可以修改权限 Root用户除外
    fchmod(fd, S_IRWXU);

    //检查文件权限
    if (!access("/data/local/tmp/2.txt", W_OK)){
        printf("拥有写权限\n");
    }

    close(fd);
    return 0;
}

线程

两种状态:

  • 连接状态,pthread_join,会阻塞,等待线程结束拿线程返回值.
  • 分离状态,pthread_detach,不会阻塞,节省内存开销.

终止线程:

1
2
3
4
5
6
7
8
void pthread_exit(void *value_ptr); 
return
/*
当一个线程设置了一个取消点,可使用下述API结束那个线程,
那个线程运行到取消点时,会结束线程
但遗憾的是,Android不允许使用此API
*/
int pthread_cancel(pthread_t thread); 

线程同步:

互斥体:

1
2
3
4
5
6
7
8
9
//初始化
//可以使用全局变量初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//也可以使用API初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//取消锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

信号量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//初始化
//跨进程使用sem_open
//不跨进程使用sem_init
int sem_init(sem_t* __sem, int __shared, unsigned int __value);
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, 
                mode_t mode, unsigned int value);
//释放信号量
int sem_post(sem_t *sem);
//等待信号量
int sem_wait(sem_t *sem);

示例:

线程

进程

Android将进程相关操作从原来的process.h移动到了unistd.h

相关函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//获取自身进程PID
pid_t  getpid(void);
//获取父进程PID
pid_t  getppid(void);
//创建进程
pid_t  fork(void);
//退进程
__noreturn void _exit(int __status);
int kill(pid_t __pid, int __signal);
//信号量
int sem_init(sem_t* __sem, int __shared, unsigned int __value);
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, 
                mode_t mode, unsigned int value);
int sem_close(sem_t *sem);
int sem_destroy(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
//内存映射
void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset);
int munmap(void* __addr, size_t __size);
int msync(void* __addr, size_t __size, int __flags);
//修改内存保护属性,需要给页首地址,否则会失败
int mprotect(void* __addr, size_t __size, int __prot);

创建进程:

系统调用fork(),一个进程(父进程)创建一个新进程(子进程),新的子进程获取父进程的栈,数据段,堆和执行文本段的拷贝.

Linux系统中,父子进程关系很密切,同生共死.

创建进程

虚拟文件系统:

Linux搞了一些假文件,通过操作这些假文件,可以访问内核的一些信息,Android中的虚拟文件在根目录的/proc中.

拿Cpu信息:

访问/proc/cpuinfo这个文件,就可以拿到Cpu信息.

拿Cpu信息

遍历进程:

/proc目录下的每个以数字命名的文件夹都是一个进程,这些数字代表的是进程PID

遍历进程1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void EnumProcess() {
    DIR *dirp;
    struct dirent *direntp;
    dirp = opendir("/proc");
    if (dirp == NULL) {
        perror("can't open /proc");
    } else {
        for (;;) {
            direntp = readdir(dirp);
            if (direntp == NULL) {
                break;
            }
            // /proc目录中以数字开头的是进程
            if (direntp->d_name[0] >= '0' && direntp->d_name[0] <= '9') {
                //取进程路径,在命令行的第一个参数中,有的系统进程可能无路径
                char szProcessCmdPath[0x1000] = {};
                char szProcessPath[0x1000] = {};
                sprintf(szProcessCmdPath, "/proc/%s/cmdline", direntp->d_name);
                FILE *fp = fopen(szProcessCmdPath, "r");
                fgets(szProcessPath, sizeof(szProcessPath), fp);
                fclose(fp);
                printf("PID:%s ProcessPath:%s\n", direntp->d_name, szProcessPath);
            }
        }
        closedir(dirp);
    }
}

遍历模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void EnumModule() {
    char buf[0x1000];
    //访问/proc每个进程下的maps文件,即可遍历模块
    //只要root权限的用户才可以遍历别人进程的模块
    //遍历自身模块,可以写/proc/self/maps
    FILE *fp = fopen("/proc/self/maps", "r");
    while (!feof(fp)) {
        fgets(buf, sizeof(buf), fp);
        printf("%s", buf);
    }
    fclose(fp);
}

读写内存:

访问/proc/pid/mem文件即可,文件偏移就是内存地址.

Andorid内存搜索工具:GG

进程状态:

访问/proc/pid/status文件即可.

进程状态

网络编程

常规Socket

耗费网络资源

服务端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <jni.h>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
    //初始化Socket
    int s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
        perror("socket");
        return 0;
    }
    printf("socket s = %d\n", s);
    fflush(stdout);

    //绑定
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5566);
    addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(s, (sockaddr *) &addr, sizeof addr) < 0) {
        perror("bind");
        return 0;
    }
    printf("bind s = %d ok\n", s);
    fflush(stdout);

    //监听
    if (listen(s, SOMAXCONN) < 0) {
        perror("listen");
        return 0;
    }
    printf("listen s = %d ok\n", s);
    fflush(stdout);

    //接受连接
    socklen_t len = sizeof addr;
    int cs = accept(s, (sockaddr *) &addr, &len);
    if (cs < 0) {
        perror("accept");
        return 0;
    }
    printf("accept cs = %d ip:%s port:%d\n", cs, inet_ntoa(addr.sin_addr), htons(addr.sin_port));
    fflush(stdout);

    char buf[260];
    int bytes;
    while (true) {
        bytes = recv(cs, buf, sizeof buf, 0);
        if (bytes < 0) {
            break;
        }
        printf("bytes:%d buf:%s\n", bytes, buf);
        fflush(stdout);
    }

    close(s);
    printf("close Socket\n");
    fflush(stdout);
    return 0;
}

客户端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <jni.h>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
    //初始化Socket
    int s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
        perror("socket");
        return 0;
    }
    printf("socket s = %d\n", s);
    fflush(stdout);

    //绑定
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5566);
    addr.sin_addr.s_addr = INADDR_ANY;
    if (connect(s, (sockaddr *) &addr, sizeof addr) < 0) {
        perror("connect");
        return 0;
    }
    printf("connect s = %d ok\n", s);
    fflush(stdout);

    //发送数据
    char buf[260];
    int bytes;
    while (true) {
        scanf("%s", buf);
        bytes = send(s, buf, sizeof buf, 0);
        printf("send bytes:%d buf:%s\n", bytes, buf);
        fflush(stdout);
    }

    close(s);
    printf("close Socket\n");
    fflush(stdout);
    return 0;
}
DoMain Socket
  • 不耗费网络资源.
  • 无IP和端口概念,用虚拟文件来代替IP地址.

服务端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <jni.h>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h>
#include <linux/un.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
    //初始化Socket
    int s = socket(AF_UNIX, SOCK_STREAM, 0);
    if (s < 0) {
        perror("socket");
        return 0;
    }
    printf("socket s = %d\n", s);
    fflush(stdout);

    //绑定
    //删除文件
    unlink("/data/local/tmp/myServer");

    sockaddr_un addr;
    //协议要用AF_UNIX
    addr.sun_family = AF_UNIX;
    //用文件来代替IP地址
    strcpy(addr.sun_path, "/data/local/tmp/myServer");

    if (bind(s, (sockaddr *) &addr, sizeof addr) < 0) {
        perror("bind");
        return 0;
    }
    printf("bind s = %d ok\n", s);
    fflush(stdout);

    //监听
    if (listen(s, SOMAXCONN) < 0) {
        perror("listen");
        return 0;
    }
    printf("listen s = %d ok\n", s);
    fflush(stdout);

    //接受连接
    socklen_t len = sizeof addr;
    int cs = accept(s, (sockaddr *) &addr, &len);
    if (cs < 0) {
        perror("accept");
        return 0;
    }
    printf("accept cs = %d ip:%s\n", cs, addr.sun_path);
    fflush(stdout);

    char buf[260];
    int bytes;
    while (true) {
        bytes = recv(cs, buf, sizeof buf, 0);
        if (bytes < 0) {
            break;
        }
        printf("bytes:%d buf:%s\n", bytes, buf);
        fflush(stdout);
    }

    close(s);

    printf("close Socket\n");
    fflush(stdout);
    return 0;
}

客户端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <jni.h>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h>
#include <linux/un.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
    //初始化Socket
    int s = socket(AF_UNIX, SOCK_STREAM, 0);
    if (s < 0) {
        perror("socket");
        return 0;
    }
    printf("socket s = %d\n", s);
    fflush(stdout);

    //绑定 可以不绑定 绑定是为了让服务器知道是谁连接的
    sockaddr_un clientAddr;
    //协议要用AF_UNIX
    clientAddr.sun_family = AF_UNIX;
    //用文件来代替IP地址 +Pid 防止文件冲突
    sprintf(clientAddr.sun_path, "/data/local/tmp/myClient_%d", getpid());
    if (bind(s, (sockaddr*)&clientAddr, sizeof clientAddr)){
        perror("bind");
        return 0;
    }

    sockaddr_un serverAddr;
    //协议要用AF_UNIX
    serverAddr.sun_family = AF_UNIX;
    //用文件来代替IP地址
    strcpy(serverAddr.sun_path, "/data/local/tmp/myServer");

    if (connect(s, (sockaddr *) &serverAddr, sizeof serverAddr) < 0) {
        perror("connect");
        return 0;
    }
    printf("connect s = %d ok\n", s);
    fflush(stdout);

    //发送数据
    char buf[260];
    int bytes;
    while (true) {
        scanf("%s", buf);
        bytes = send(s, buf, sizeof buf, 0);
        printf("send bytes:%d buf:%s\n", bytes, buf);
        fflush(stdout);
    }

    close(s);
    printf("close Socket\n");
    fflush(stdout);
    return 0;
}
注意
测试的时候,填写的虚拟Socket文件可能没有权限,需要进root测试,实际apk应用的时候,可以填写apk所在的目录,这样就没有权限问题了.

混合编程JNI

概述

Java Native Interface Java => JNI =>xxx(动态库 so) xxx(动态库 so) => JNI =>Java

两边互调应满足以下条件:

  • so文件应该导出函数,在函数前加JNIEXPORT.
  • 类型匹配,Java要调C++,首先类型应该匹配,在Jni.h中已定义好.
  • 调用约定,在函数前加JNICALL.

如果是C++,防止名称粉碎,在函数前加extern “C”

名称规范问题:

1
2
3
4
为了防止误调so中的导出函数,应以包名_类名_函数名来命名
可以使用javah.exe将编译好的class文件给这个exe,
它会扫描类声明,看到native会自动创建头文件
不过这样还是很麻烦的,AndroidStodio已集成该工具

Java调C++

  1. 在Java类中添加声明, 声明前加native关键字.

Java调C++1

  1. Alt + Enter,创建Jni方法.

Java调C++2

Java调C++3

Java反射

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//拿目标对象
//①
// Class cls = MainActivity.class;
//② 本身有对象
//Class cls = this.getClass();
//③ 常用, 通过类名拿
try {
    Class cls = Class.forName("org.example.luojni.MainActivity");

    //new 对象
    //不带构造new对象
    Object obj = cls.newInstance();
    //带构造new对象
    Constructor constructor = cls.getConstructor();
    //传构造参数即可
    obj = constructor.newInstance(10);

    //调方法 传参数是为了防止有歧义,因为函数可以重载
    //①拿方法
    Method method = cls.getMethod("MyAdd", int.class, int.class);
    //②拿方法 修改方法的访问权限 当方法为private时 第①种方式会拿不到
    method = cls.getDeclaredMethod("MyAdd", int.class, int.class);
    //修改方法的访问权限
    method.setAccessible(true);
    //为了统一 返回值为Object
    //如果为静态方法 第一个参数给null就可以了
    Object ret = method.invoke(obj, 1, 2);
    //若返回值为整型 就转为整型的包装类
    int nResult = ((Integer) ret).intValue();

    //反射字段
    //①拿字段
    Field field = cls.getField("mData");
    //②拿字段 修改字段的访问权限 当字段为private时 第①种方式会拿不到
    field = cls.getDeclaredField("mData");
    //修改字段的访问权限
    field.setAccessible(true);
    //修改字段值
    field.setInt(obj, 1);
    //获取字段值
    nResult = field.getInt(obj);


} catch (ClassNotFoundException | NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
}

Java反射的强大之处在于可以修改方法和字段的访问权限.

C++调Java

C++操作Java中的字符串以及数组:

  1. Java中声明函数,以及使用.

C++操作Java中的字符串以及数组1

C++操作Java中的字符串以及数组2

  1. C++中操作.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extern "C"
JNIEXPORT jstring JNICALL
Java_org_example_luojni_MainActivity_fun1(JNIEnv *env,
                                          jobject thiz,
                                          jstring str,
                                          jbyteArray ary) {
    //操作Java中的字符串
    int nLen = env->GetStringLength(str);
    //传false 说明不拷贝缓冲区,跟Java中用的是同一个缓冲区
    jboolean flag = false;
    const char* psz = env->GetStringUTFChars(str, &flag);
    LOGD("len = %d str = %s\n", nLen, psz);

   //操作Java中的数组
   int nCount = env->GetArrayLength(ary);
   jbyte * p = env->GetByteArrayElements(ary, &flag);
    for (int i = 0; i < nCount; ++i) {
        LOGD("%d ", p[i]);
    }

   //返回String
    return  env->NewStringUTF("Hello Ret");
}

C++反射调Java:

  1. 写一个Java类.

C++反射调Java1

  1. 在Java中声明native方法并调用.

C++反射调Java2

C++反射调Java3

  1. 在C++中反射调Java.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//参数信息描述
//I(int) F(float) D(double) J(long) Z(bool) C(char) S(short) Ljava/lang/String(String)
//一维数组 [i(int[])
//二维数组 [[i(int[][])

extern "C"
JNIEXPORT void JNICALL
Java_org_example_luojni_MainActivity_fun2(JNIEnv *env, jobject thiz) {
    //反射
    //拿Java类
    jclass cls = env->FindClass("org/example/luojni/MyClass");

    //new对象
    //传"<init>" 就是拿构造
    jmethodID construct = env->GetMethodID(cls, "<init>", "()V");
    //构造若是有参数就传参, 这个函数是不定参的
    jobject obj = env->NewObject(cls, construct);

    //调方法
    //非静态方法
    //()V 括号内写参数 后面写返回值
    jmethodID fun1 = env->GetMethodID(cls, "fun1", "()V");
    env->CallVoidMethod(obj, fun1);
    //静态方法
    jmethodID fun2 = env->GetStaticMethodID(cls, "fun2", "()V");
    env->CallStaticVoidMethod(cls, fun2);
    //示例
    jmethodID Add = env->GetMethodID(cls, "Add", "(II)I");
    //检查Java中的异常
    if (env->ExceptionCheck()){
        //打印异常栈信息
        env->ExceptionDescribe();
        //清除异常信息 为了不让程序崩
        env->ExceptionClear();
        return;
    }
    jint nRet = env->CallIntMethod(obj, Add, 1, 6);

    //修改字段
    jfieldID mData =  env->GetFieldID(cls, "mData", "I");
    env->SetIntField(obj, mData, 200);

    //引用计数 我们在C++中New了一个对象 Java虚拟机如何知道何时释放对象
    //释放字符串或数组对象 可以调env->Release...
    env->DeleteLocalRef(obj);
}

加密:

Java的逆向相对于C来说比较容易,将Java中的所有代码都用C反射来调用,然后对C++层进行加密.

注意
在C++中反射调Java无视权限(public private).

相关内容

0%