线程

这个末节的方针是略微熟悉一下c言语体系下的多线程操作,防止看到类似代码一脸懵逼 不知道为啥

运用pthread 创建一个线程

留意这儿要改一下 cmake文件 不然,pthread 相关依赖是找不到的

add_executable(cstudy12 pthread_test.c)
target_link_libraries(cstudy12 pthread)
//
#include <pthread.h>
#include <stdio.h>
void *SayHello(void *name) {
  printf("hello from thread %s", name);
  pthread_exit(NULL);
}
int main() {
  //声明了一个pthread_t类型的变量tid来存储线程ID
  pthread_t tid;
  // ret!=0 代表创建线程失败了
  int ret;
  //榜首个参数是指向pthread_t变量的指针,用于存储新线程的ID。第二个参数是线程的特点,能够运用NULL表明默许特点。
  // 第三个参数是指向线程进口点函数的指针。最终一个参数是传递给线程进口点函数的参数,能够运用NULL表明没有参数。
  ret = pthread_create(&tid, NULL, SayHello, "wuyue");
  if (ret) {
    printf("error create pthraed");
    return 1;
  }
  //咱们运用pthread_join函数等候新线程完成履行。pthread_join函数的榜首个参数是要等候的线程的ID,
  // 第二个参数是指向线程返回值的指针,假如不需求获取线程返回值,能够运用NULL表明不关心返回值。
  pthread_join(tid, NULL);
  return 0;
}

锁操作

#include <pthread.h>
#include <stdio.h>
int global_value=0;
pthread_mutex_t lock;
void *thread_func(void* arg){
  int i;
  for (i = 0; i < 100; ++i) {
    //locl和unlock要成对出现
    pthread_mutex_lock(&lock);
    global_value++;
    pthread_mutex_unlock(&lock);
  }
  return NULL;
}
int main() {
  // 留意init和destroy 要成对出现
  pthread_mutex_init(&lock,NULL);
  pthread_t t1,t2;
  pthread_create(&t1,NULL, thread_func,NULL);
  pthread_create(&t2,NULL, thread_func,NULL);
  pthread_join(t1,NULL);
  pthread_join(t2,NULL);
  pthread_mutex_destroy(&lock);
  printf("value:%d",global_value);
  return 0;
}

threadlocal

#include <pthread.h>
#include <stdio.h>
//__thread关键字告诉编译器为thread_local_var变量分配线程本地存储空间,这样每个线程都能够具有自己的thread_local_var变量实例。
// 这意味着在不同的线程中拜访thread_local_var变量时,每个线程都会拜访自己的thread_local_var变量实例,从而防止了线程间的竞争和同步开支。
__thread int thread_local_var;
//在线程函数中,咱们能够像运用一般变量一样运用thread_local_var变量,而且每个线程都能够具有自己的变量实例
void *thread_func(void *arg){
  thread_local_var = (int) arg;
  printf("Thread %d: thread_local_var=%d\n", (int) pthread_self(), thread_local_var);
  pthread_exit(NULL);
}
int main() {
  pthread_t tid[2];
  int i;
  //创建两个线程
  for (i = 0; i < 2; i++) {
    if (pthread_create(&tid[i], NULL, thread_func, (void*)i) != 0) {
      perror("pthread_create");
    }
  }
  //等候线程完毕
  for (i = 0; i < 2; i++) {
    pthread_join(tid[i], NULL);
  }
  return 0;
}

C言语的编译过程

这一末节 首要是为了后面静态库和动态库理解用的

预处理器

这一步履行完今后便是宏 替换后的源代码

gcc -E helloworld.c -o helloworld.i

能够看下这个helloworld.i 是啥

内容挺长的,咱们能够自行看看,其实也是源代码 只不过比你写的源代码要复杂多了,宏被一致替换成源代码了

Android JNI 编程 - C语言基础知识 (三)

编译器

这一步履行完今后便是中间文件 也便是汇编指令

gcc -S helloworld.i -o helloworld.s

Android JNI 编程 - C语言基础知识 (三)

也能够直接编译成方针文件

方针文件是计算机可履行的二进制文件,它是编译器生成的中间文件,包含编译后的机器代码和未解析的符号引证。 方针文件能够用于生成最终的可履行文件或许库文件。

gcc -C helloworld.s -o helloworld.o

咱们能够看一下这个 方针文件的类型

Android JNI 编程 - C语言基础知识 (三)

未解析的符号引证 这概念要好好把握:

未解析的符号引证(Unresolved symbol reference)指的是在方针文件中引证的一个符号(通常是函数或变量)没有在该文件中界说。这个符号被标记为“未解析”,由于编译器无法确认该符号所代表的实践地址,由于该符号的界说在其他文件中。在链接器(Linker)将多个方针文件组合成可履行文件或许库文件时,需求解析这些未解析的符号引证,将其与正确的界说进行匹配,以确保程序能够正常运转。

未解析的符号引证通常发生在运用库文件时,由于库文件是预编译好的二进制代码,编译器无法在编译时确认库中函数或变量的界说。当链接器链接方针文件时,它会搜索库文件,以找到符号的界说,并将其与未解析的引证进行匹配。

假如链接器无法找到符号的界说,它将生成一个“未界说符号”过错并停止链接过程。因而,处理未解析的符号引证问题十分重要,通常需求运用正确的编译器选项和库文件来确保正确的链接。

咱们能够运用指令来检查 有哪些未解析的符号引证

nm -u helloworld.o

Android JNI 编程 - C语言基础知识 (三)

这儿有的人奇怪 怎么会有个puts函数,咱们写的是printf啊, 这是由于咱们printf里边 是个纯字符串比较简略,所以gcc的编译器主动给咱们优化了

检查下汇编代码就真相大白了

Android JNI 编程 - C语言基础知识 (三)

链接器

这一步履行完今后便是可履行文件了

咱们能够把这一步打出来看一下

gcc -v helloworld.o -o helloworld

代码我就不贴了,太长了,基本上都跟一个叫collect的程序有关。

能够看下生成的可履行文件类型是什么

Android JNI 编程 - C语言基础知识 (三)

静态链接库与动态链接库

静态库被运用方针代码最终和可履行文件在一起(它只会有自己用到的),而动态库与它相反,它的方针代码在运转时或许加载时链接。

静态链接的可履行文件要比动态链接的可履行文件要大得多,由于它将需求用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是仿制了一些重定位和符号表信息。

静态链接的可履行文件不需求依赖其他的内容即可运转,而动态链接的可履行文件有必要依赖动态库的存在。所以假如你在装置一些软件的时分,提示某个动态库不存在的时分也就不奇怪了。

即便如此,体系中一般存在一些大量共用的库,所以运用动态库并不会有什么问题。

静态链接库

已知 咱们有如下 一段c 代码

Android JNI 编程 - C语言基础知识 (三)

add.c 和 subtract.c 代码就不放了 很简略,咱们猜也能猜到 啥意思

现在咱们想打出一个可履行程序

咱们首要打出2个方针文件

gcc -c add.c subtract.c

Android JNI 编程 - C语言基础知识 (三)

然后咱们运用ar指令将这两个方针文件打包成静态链接库 libmath.a

ar rcs libmath.a add.o subtract.o

Android JNI 编程 - C语言基础知识 (三)

然后

gcc -o main main.c -L. -lmath

Android JNI 编程 - C语言基础知识 (三)

即可编译出咱们的可履行文件了

动态链接库

留意动态库的命名有必要是lib最初

其实你看 就算方才咱们编译出来的 main 可履行文件 里边也是需求有动态链接库信息的 这个libc 应该很熟悉吧。。

Android JNI 编程 - C语言基础知识 (三)

仍是上面那个比如

gcc -v -fPIC -shared -o libmath.so add.c subtract.c

-v 能够具体打出编译的过程日志信息,有兴趣的能够自己看下

Android JNI 编程 - C语言基础知识 (三)

能够看出来 这个so 文件现已出来了

Android JNI 编程 - C语言基础知识 (三)

继续编咱们的可履行程序:

gcc -o main main.c -L. -lmath

Android JNI 编程 - C语言基础知识 (三)

可是当你履行的时分 你会发现报错了

Android JNI 编程 - C语言基础知识 (三)

他说这个 找不到这个so文件在哪里

此刻咱们再检查一下

Android JNI 编程 - C语言基础知识 (三)

要处理这个办法也很简略

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/library

再看 即可恢复正常

Android JNI 编程 - C语言基础知识 (三)

当然你也能够把这个so 文件拷贝到 /usr/lib 这个文件夹下, 其实许多程序的装置 便是把动态库 放到/usr/lib下

mmap 技能

mmkv 用的便是这个作为根底, 这儿也用c言语来做一些演示,加深一下理解。讲白了关于andorid程序员来说,其实便是 用c言语和linux的接口 以及 android 提供的一些c言语的库,来做一些编程

长处:

在mmap技能中,操作体系将文件映射到进程的虚拟地址空间中,然后进程能够像拜访内存一样拜访文件的内容。这使得文件的拜访变得愈加高效,由于操作体系能够通过页面映射技能将文件内容读入物理内存中,而且在需求时将其刷回磁盘,而不需求频繁地进行文件I/O操作

缺点:

  1. 映射大文件时或许会耗费大量的虚拟内存。由于mmap技能将文件内容映射到进程的地址空间中,所以在映射大文件时会耗费大量的虚拟内存,或许会导致进程内存耗尽的问题。
  2. 映射文件时或许会导致文件锁定。当运用mmap技能将文件映射到进程的地址空间中时,文件或许会被锁定,导致其他进程无法对其进行拜访。
  3. 不能直接拜访文件体系。mmap技能只能拜访现已打开的文件,不能直接拜访文件体系,这或许会导致某些应用程序无法运用该技能来拜访文件。
  4. 不支持对文件结尾进行动态扩展 运用mmap技能将文件映射到进程的地址空间中时,假如需求对文件结尾进行动态扩展,则需求重新映射文件,这或许会导致额定的开支和复杂性。

在运用mmap技能时,通常需求满足一些要求,其间一个要求是要按照4K的倍数进行映射。这是由于操作体系将进程的地址空间分为多个页面,每个页面通常是4K字节巨细,因而假如要将文件映射到进程的地址空间中,就需求按照页面巨细的倍数进行映射。

假如按照不是4K的倍数进行映射,操作体系或许会主动进行调整,但这或许会导致额定的开支和功能问题。因而,在运用mmap技能时,最好按照4K的倍数进行映射,以取得最佳的功能和功率。

通过上述的描述,咱们要遵循一个运用办法, 当你运用mmap技能时,必定要适当的调整好自己的文件巨细,这个文件巨细一旦确认下来,后续就不要去改他了。

下面看一个比如

#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#define FILE_SIZE 1024
int main() {
  // 此处必定要留意 有读写文件的权限才能够
  int fd = open("file.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
  if (fd == -1) {
    perror("open");
    exit(EXIT_FAILURE);
  }
  if (ftruncate(fd, FILE_SIZE) == -1) {
    perror("ftruncate");
    exit(EXIT_FAILURE);
  }
  // 一般榜首个参数都是为null 这儿也是要有读写文件
  char *file_data = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (file_data == MAP_FAILED) {
    perror("mmap");
    exit(EXIT_FAILURE);
  }
  file_data[0] = 'H';
  file_data[1] = 'e';
  file_data[2] = 'l';
  file_data[3] = 'l';
  file_data[4] = 'o';
  // 将修正 同步回文件
  if (msync(file_data, FILE_SIZE, MS_SYNC) == -1) {
    perror("msync");
    exit(EXIT_FAILURE);
  }
  // 撤销映射
  if(munmap(file_data,FILE_SIZE)==-1){
    perror("munmap");
    exit(EXIT_FAILURE);
  }
  // 关闭文件
  if(close(fd)==-1){
    perror("close");
    exit(EXIT_FAILURE);
  }
  return 0;
}

假如是打开一个已存在的文件怎么做? 其实和上面的代码差不多,首要便是自己获取文件的实践巨细就能够了

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main() {
  // 此处必定要留意 有读写文件的权限才能够
  int fd = open("file.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
  if (fd == -1) {
    perror("open");
    exit(EXIT_FAILURE);
  }
  // 获取文件的巨细 
  struct stat st;
  if (fstat(fd, &st) == -1) {
    perror("fstat");
    exit(EXIT_FAILURE);
  }
  size_t file_size = st.st_size;
  // 一般榜首个参数都是为null 这儿也是要有读写文件
  char *file_data = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (file_data == MAP_FAILED) {
    perror("mmap");
    exit(EXIT_FAILURE);
  }
  file_data[0] = 'w';
  file_data[1] = 'u';
  file_data[2] = 'y';
  file_data[3] = 'u';
  file_data[4] = 'e';
  // 将修正 同步回文件
  if (msync(file_data, file_size, MS_SYNC) == -1) {
    perror("msync");
    exit(EXIT_FAILURE);
  }
  // 撤销映射
  if (munmap(file_data, file_size) == -1) {
    perror("munmap");
    exit(EXIT_FAILURE);
  }
  // 关闭文件
  if (close(fd) == -1) {
    perror("close");
    exit(EXIT_FAILURE);
  }
  return 0;
}

有兴趣的能够自己试着 用mmap 封装一层 简略的接口 给java 端调用,实现一个简略版别的mmkv。 简略版别的mmkv 咱们就用 mmap+文本的方法就能够了。 实践的mmkv 不过是把文本存储 替换成了 pb 协议二进制存储,功率更高,仅此而已

总结

jni 的榜首部分 常识到这儿就完毕了,首要便是简略介绍了下c言语以及 linux下的文件操作, 有了这些根底常识 现已能够让咱们看一下简略的 jni库了, 要真正的上手去写jni程序,最终仍是得学习一下c++,后续的根底常识就环绕c++打开, 一起交叉一些 重要的linux根底,例如 进程,权限,同享内存,信号 等等,这些常识对后续实践的android端 jni编程十分有帮主