JNI学习-android下JNI开发

android下JNI开发

what 什么是JNI

  • JNI java native interface native本地 java本地接口
  • 通过JNI可以实现java和本地代码之间相互调用
  • jni可以看做是翻译 实际上就是一套协议

这里写图片描述

这里写图片描述

why 为什么要用JNI

  • Java 一处编译到处运行
    • ①java运行在虚拟机上 JNI可以扩展java虚拟机的能力 让java代码可以调用驱动
    • ②java是解释型语言 运行效率相对较低 C/C++的效率要高很多 通过jni把耗时操作方法C/C++可以提高java运行效率
    • ③ java代码编译成的.class 文件安全性较差, 可以通过jni 把重要的业务逻辑放到c/c++去实现,c/c++反编译比较困难 安全性较高
  • C历史悠久 1972年就出现了C,40多年的历史。 通过JNI可以调用优秀的C开源类库

How 怎么用JNI

  • java
  • JNI开发流程 NDK native develop kit

C基本语法

CHelloWorld

#include<stdio.h>    // 相当于 java的import .h c的头文件 stdio.h standard io 标准输入输出 
#include<stdlib.h>   // stdlib standard library 标准函数库    java.lang 
/**
*/
main(){    // public static void main(String[] args)
       printf("helloworld!\n");  //System.out.println();   "\n"换行符 
       system("javac Hello.java");
       system("java Hello");
       system("notepad");
       system("pause"); //system执行windows的系统命令 
       } 

C的基本数据类型

  • java基本数据类型
    boolean 1
    byte 1
    char 2 char 1个字节
    short 2 short 2
    int 4 int 4
    long 8 long 4
    float 4 float 4
    double 8 double 8
C语言基本类型

char, int, float, double, long,short, signed, unsigned, void
计算类型的长度:sizeof(“类型”)返回int类型的长度
占位符:%d
printf(“内容”);
java基本数据类和C语言的一些区别
1.Java中char类型的长度为2个字节,C语言中的长度为1个字节
2.Java中long类型的长度为8个字节,C语言中的长度为4个字节
C99标准规定:long类型的规定,不小于整形。
3.C语言中没有byte
4.C语言中 boolean类型,0表示flase,非零表示true
signed : 有符号:-128~127 = -2^7~ 2^7-1
unsigned:无符号 0~255 = 0~2^8-1

void: 无类型,代表任意类型
* signed 有符号数 最高位是符号位 可以表示负数 但是表示的最大值相对要小
* unsigned 无符号数 最高位是数值位 不可以表示负数 表示的最大值相对要大
* signed unsigned 只能用来修饰整形变量 char short int long
* C没有 boolean byte C用0和非0表示false true

C的输出函数

%d  -  int
%ld – long int
%lld - long long
%hd – 短整型
%c  - char
%f -  float
%lf – double
%u – 无符号数
%x – 十六进制输出 int 或者long int 或者short int
%o -  八进制输出
%s – 字符串
  • 占位符不要乱用 要选择正确的对应类型 否则可能会损失精度
  • C字符串

    • C没有String类型 C的字符串实际就是字符数组
    • C数组定义 [ ]只能再变量名之后
    • C字符串两种定义方式

      char str[] = {'h','e','l','l','o','\0'};//注意'\0'字符串结束符
      char str[] = "你好"; //这种定义方式不用写结束符 可以表示汉字
      

C的输入函数

  • scanf(“占位符”, &地址);
  • & 取地址符
  • C字符串不检查下标越界 使用时要注意

内存地址的概念

  • 声明一个变量,就会立即为这个变量申请内存,一定会有一个对应的内存地址
  • 没有地址的内存是无法使用的
  • 内存的每一个字节都有一个对应的地址
  • 内存地址用一个16进制数来表示
  • 32位操作系统最大可以支持4G内存
    • 32位系统的地址总线为32位,也就是说系统有2^32个数字可以分配给内存作为地址使用

指针入门 **

  • 指针和指针变量的关系
  • 指针就是地址,地址就是指针
    地址就是内存单元的编号
    指针变量是存放地址的变量
    指针和指针变量是两个不同的概念
    但是要注意: 通常我们叙述时会把指针变量简称为指针,实际它们含义并不一样

指针里存的是100, 指针: 地址–具体
指针里存的是地址, 指针: 指针变量 – 可变

  • 为什么要使用指针
    · 指针的重要性
    直接访问硬件 (opengl 显卡绘图)
    快速传递数据(指针表示地址)
    返回一个以上的值(返回一个数组或者结构体的指针)
    表示复杂的数据结构(结构体)
    方便处理字符串
    指针有助于理解面向对象

    int i = 123;
    //一般计算机中用16进制数来表示一个内存地址
    printf(“%#x\n”,&i);
    //int* int类型的指针变量 pointer指针 指针变量只能用来保存内存地址
    //用取地址符&i 把变量i的地址取出来 用指针变量pointer 保存了起来
    //此时我们可以说 指针pointer指向了 i的地址
    int* pointer = &i;
    printf(“pointer的值 = %#x\n”,pointer);
    printf(“*pointer的值%d\n”,*pointer);
    *pointer = 456;
    printf(“i的值是%d\n”,i);
    system(“pause”);

  • 指针常见错误
    • 声明了指针变量后 未初始化直接通过*p 进行赋值操作 运行时会报错
        • 未赋值的指针称为野指针
    • 指针类型错误 如int* p 指向了double类型的地址, 通过指针进行读取操作时,读取值会出错

指针的练习

  • 值传递和引用传递(交换两个数的值)

    • 引用传递本质是把地址传递过去
    • 所有传递其实本质都是值传递,引用传递其实也是传递一个值,但是这个值是一个内存地址

      void swap(int* p, int* p2){
          int temp = *p;
          *p = *p2;
          *p2 = temp; 
      }
      main(){
          int i = 123;
          int j = 456;
          //将i, j的地址传递过去
          swap(&i,&j);
          printf("i = %d, j = %d", i, j);
      }
      
  • 返回多个值
    • 把地址作为参数传入函数中,当函数执行完毕时,参数的值就已经被修改了

多级指针

  • int* p; int 类型的一级指针 int** p2; int 类型的二级指针
  • 二级指针变量只能保存一级指针变量的地址
  • 有几个* 就是几级指针 int*** 三级指针
  • 通过int类型三级指针 操作int类型变量的值 ***p

        int i = 123;
        //int类型一级指针 
        int* p = &i;
        //int 类型 二级指针 二级指针只能保存一级指针的地址 
        int** p2 = &p;
        //int 类型 三级指针  三级指针只能保存二级指针的地址 
        int*** p3 = &p2;
        //通过p3 取出 i的值
        printf("***p3 = %d\n", ***p3);
    
  • 多级指针案例 取出子函数中临时变量的地址

数组和指针的关系

  • 数组占用的内存空间是连续的
  • 数组变量保存的是第0个元素地址,也就是首地址
  • *(p + 1):指针位移一个单位,一个单位是多少个字节,取决于指针的类型

指针的长度

  • 不管变量的类型是什么,它的内存地址的长度一定是相同的
  • 类型不同只决定变量占用的内存空间不同
  • 32位环境下,内存地址长度都是4个字节,所以指针变量长度只需4个字节即可
  • 区分指针类型是为了指针位移运算方便

堆栈概念 静态内存分配 动态内存分配

  • 栈内存
    • 系统自动分配
    • 系统自动销毁
    • 连续的内存区域
    • 向低地址扩展
    • 大小固定
    • 栈上分配的内存称为静态内存
  • 静态内存分配
    • 子函数执行完,子函数中的所有局部变量都会被销毁,内存释放,但内存地址不可能被销毁,只是地址上的值没了
    • 特点:静态内存是程序编译执行后系统自动分配,由系统自动释放,静态内存是栈分配的;
#include<stdio.h>
#include<stdlib.h>
/**
  静态内存分配 
  在主函数中定义一个指针,指针传递给子函数,子函数给赋值 
  静态内存是系统是程序编译执行后系统自动分配,由系统自动释放
*/
func(int** pAddress){  
  int i = 110;    
  *pAddress = &i;     

}    

main()
{

     int* p;      

     func(&p);  

     printf("*p=%d\n",*p);  
     printf("*p=%d\n",*p);  
     printf("*p=%d\n",*p);  
     printf("*p=%d\n",*p);  
     printf("*p=%d\n",*p);   

     system("pause");           
}       
  • 堆内存
    • 程序员手动分配
      • java:new
      • c:malloc
    • 空间不连续
    • 大小取决于系统的虚拟内存
    • C:程序员手动回收free
    • java:自动回收
    • 堆上分配的内存称为动态内存
#include<stdio.h>
#include<stdlib.h>
/**
  动态内存分配 
*/
func(int** pAddress){  
         int i = 110;
         int* temp; 
         //动态的申请一块内存空间 
         temp =malloc(sizeof(int)); //返回的是在堆内存中的一块地址 

         //把申请出来的内存赋值为i;
         *temp = i; 
         //把堆内存中的一块地址赋值给 一级指针变量 pAddress
         *pAddress = temp;


         //把申请的内存回收掉
         free(temp); 


}    


main()
{

     int* p;    

     func(&p); 

     printf("*p=%d\n",*p);  
     printf("*p=%d\n",*p);  
     printf("*p=%d\n",*p); 
     printf("*p=%d\n",*p);  



     system("pause");           
}       
特点:申请完之后,只要不回收就会一直在内存中存在子函数的值,可以被主函数长时间的保留。
知识拓展

动态内存和静态内存
静态内存是程序编译执行后系统自动分配,由系统自动释放, 静态内存是栈分配的.
动态内存是开发者手动分配的, 是堆分配的.

(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多.
堆和栈的区别:
1.申请方式
栈:
由系统自动分配.例如,声明一个局部变量int b; 系统自动在栈中为b开辟空间.例如当在调用涵数时,需要保存的变量,最明显的是在递归调用时,要系统自动分配一个栈的空间,后进先出的,而后又由系统释放这个空间.
堆:
需要程序员自己申请,并指明大小,在c中用malloc函数
如char* p1 = (char*) malloc(10); //14byte
但是注意p1本身是在栈中的.
2 申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

3.申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(vc编译选项中可以设置,其实就是一个STACK参数,缺省2M),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

4.申请效率的比较:
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:由malloc/new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.

5.堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

6.内存的回收
栈上分配的内存,编译器会自动收回;堆上分配的内存,要通过free来显式地收回,否则会造成内存泄漏。
堆和栈的区别可以用如下的比喻来看出:
使用栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就像是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

# include <stdio.h>
# include <malloc.h>  //不能省  malloc 是 memory(内存) allocate(分配)的缩写(stdlib.h库里有
malloc这个库)
int main(void)
{
  int i = 5; //分配了4个字节 静态分配   11 行
  int * p = (int *)malloc(4); //12行
     /*
       1. 要使用malloc函数,必须添加malloc.h这个头文件
       2. malloc函数只有一个形参,并且形参是整型
       3. 4表示请求系统为本程序分配4个字节
       4. malloc函数只能返回第一个字节的地址
       5. 12行分配了8个字节, p变量占4个字节, p所指向的内存也占4个字节
       6. p本身所占的内存是静态分配的, p所指向的内存是动态分配的    
     */
  *p = 5; //*p 代表的就是一个int变量, 只不过*p这个整型变量的内存分配方式和11行的i变量的分配方式不同
  free(p); //freep(p)表示把p所指向的内存给释放掉  p本身的内存是静态的,不能由程序员手动释放,p本身的内存只能在p变量所在的函数运行终止时由系统自动释放 
  printf("大家好!\n");
  return 0;
}

动态数组的创建方式

动态数组的创建步骤
1、让用户输入一个长度
2、根据长度,分配内存空间
3、让用户把数组中的元素依次的赋值;
4、接收用户输入扩展数组长度
5、根据扩展的长度重新分配空间
6、把扩展长度的元素让用户赋值;
7、输出数组

#include<stdio.h>
#include<stdlib.h>
/**
  动态数组的创建 
  1、让用户输入一个长度
  2、根据长度,分配内存空间
  3、让用户把数组中的元素依次的赋值;
  4、接收用户输入扩展数组长度
  5、根据扩展的长度重新分配空间 
  6、把扩展长度的元素让用户赋值;
  7、输出数组 

  malloc(长度);

  realloc(数组,长度)重新分配空间  

*/



main()
{

  //1、让用户输入一个长度
   printf("请输入数组的长度:");
   int length;
   scanf("%d",&length); 
   printf("你输入的长度为:%d\n",length);
  //2、根据长度,分配内存空间
  // int* == int iArray[] 
  int* iArray = malloc(length * 4);//分配出指定元素的总空间:3个元素,3 * 4 = 12; 
  //3、让用户把数组中的元素依次的赋值;
  int i;
  for(i = 0;i < length;i++){
          printf("请输入第[%d]元素的值:",i);
          scanf("%d",iArray + i);
  }     
  //4、接收用户输入扩展数组长度
    printf("请输入扩展的长度:");
    int supportLength ;    
    scanf("%d",&supportLength); 
    printf("扩展的长度为:%d\n",supportLength);

  //5、根据扩展的长度重新分配空间 
    iArray = realloc(iArray,(length + supportLength) * 4);

  //6、把扩展长度的元素让用户赋值;
  for(i=length;i<(length + supportLength);i++){
        printf("请输入扩展第[%d]元素的值:",i);  
        scanf("%d",iArray + i);                 
  }                     

  //7、输出数组 
     for(i = 0;i < length + supportLength;i++){

              printf("iArray[%d]=%d\n",i,*(iArray +i));

      }  

     system("pause");     


}       

函数指针

#include<stdio.h>
#include<stdlib.h>
/**
  函数指针 

*/


int add(int x,int y){
   return x + y;     
}

main()
{

     //函数指针
     int (*pf)(int x,int y); 
     //函数指针赋值
     pf =  add;
     //调用函数指针
     int result =  pf(30,10);

     printf("ressult=%d\n",result);


     system("pause");         

}  

结构体

  • 结构体中的属性长度会被自动补齐,这是为了方便指针位移运算
  • 结构体中不能定义函数,可以定义函数指针
  • 程序运行时,函数也是保存在内存中的,也有一个地址
  • 结构体中只能定义变量
  • 函数指针其实也是变量,它是指针变量
  • 函数指针的定义 返回值类型(*变量名)(接收的参数);
  • 函数指针的赋值: 函数指针只能指向跟它返回值和接收的参数相同的函数

结构体指针

(*stuPoint).age 等价于 stuPoint->age

#include<stdio.h>
#include<stdlib.h>

//结构体指针 

//定义结构
struct student{
    int age;//4个字节 
    float score;//4个字节 
    char sex;   //1个字节 
} ;     
main(){
       //使用结构体 
       struct student stu = {18,98.9,'W'};

       //结构体指针
       struct student* point = &stu;

       struct student** point2 = &point;

       //取值运算(*point).age  等价于 point->age  
       printf("(*point).age ==%d\n",(*point).age ); 
       printf("point->age ==%d\n",point->age ); 
       printf("point->score ==%f\n",point->score ); 
       printf("point->sex ==%c\n",point->sex ); 
       //赋值运算
       point->age = 20; 
       point->score = 100;
       point->sex = 'M';
       printf("point->age ==%d\n",point->age ); 
        printf("point->age ==%d\n",point->age ); 
       printf("point->score ==%f\n",point->score ); 
       printf("point->sex ==%c\n",point->sex );  

       //二级结构体指针取值 (*point).age  等价于 point->age   所以  (**point).age 等价于 (*point)->age
        printf("(**point2).age ==%d\n",(**point2).age ); 
        printf("(*point2)->age ==%d\n",(*point2)->age ); 
        //二级指针赋值
         (**point2).age = 2000;
           printf("(**point2).age ==%d\n",(**point2).age ); 
        printf("(*point2)->age ==%d\n",(*point2)->age ); 
       system("pause"); 
}

联合体

  • 长度等于联合体中定义的变量当中最长的那个
  • 联合体只能保存一个变量的值
  • 联合体共用同一块内存
  • 应用场景-各个类型之间方便转换
#include<stdio.h>
#include<stdlib.h>
/*
联合体 


*/ 
//定义一个结构体 
struct Date {
      int year;
      int month;
      int day;
}; 
//定义一个联合体,特点,所有的字段都是使用同一块内存空间; 
union Mix {
     long i; //4个字节 
     int k; //4个字节 
     char ii;//1个字节 
};
main() { 
       printf("date:%d\n",sizeof(struct Date)); 
       printf("mix:%d\n",sizeof(union Mix)); 

       //实验 
        union Mix m;
        m.i = 100;
        m.k = 123;
        printf("m.i=%d\n",m.i);         
        printf("m.k=%d\n",m.k);            
       system("pause");
} 

枚举

  • 值是递增
  • 默认值是从0开始
#include<stdio.h>
#include<stdlib.h>
/*
枚举中的变量是递增的
默认是0开始 
*/
enum WeekDay {
     Monday=0,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
};


main() {
       enum WeekDay day = Wednesday;
       printf("%d\n",day);
       system("pause");
}

Typedef别名

定义:在类中把名字很长的方法用简写或者代替方式
声明自定义数据类型,配合各种原有数据类型来达到简化编程的目的的类型定义关键字。
在计算机编程语言中用来为复杂的声明定义简单的别名;

#include <stdio.h> 
#include <stdlib.h>
typedef int i;
typedef long l;
main() {
       i m = 10;
       l n = 123123123;
       printf("%d\n", m);
       printf("%ld\n", n);
       system("pause");       
}

NDK

JNI是一套协议,而NDK是一个工具包。英文全称Native Develop Kits(本地开发工具包)
作用:Android提供的用来做JNI开发的工具包

NDK开发详细流程
1. 安装配置NDK
1). 解压NDK的zip包到非中文目录
2). 配置path : 解压后NDK的根目录—–>ndk-build

  1. 给AS配置关联NDK
    1). local.properties中添加配置
    ndk.dir=G:\android-ndk-r10
    2). gradle.properties中添加配置
    android.useDeprecatedNdk=true

  2. 编写native方法:
    public class JNIS {
    public native String helloJNI();
    }

  3. 定义对应的JNI
    1). 在main下创建jni文件夹
    2). 生成native方法对应的JNI函数声明头文件: 命令窗口中, 进入java文件夹
    执行命令: javah com.zhonghao.jnitests2.JNIS
    生成头文件: com_zhonghao_jnitests2_JNIS.h
    函数声明: JNIEXPORT jstring JNICALL
    *****PS:或者使用AS的Terminal,进入java目录下
    cd /d D:\work\work_as\JNIDemo\app\src\main\java
    使用javah命令生成.h文件(javah -jni 包名.类名)
    >javah -jni com.chen.jnidemo.JNI

    刷新项目,你会发现在java目录下多了一个.h文件。
    C语言方法的格式:返回类型 Java_包名(包名用代替原来的.)类名
    Java_com_zhonghao_jnitests2_JNIS_helloJNI(JNIEnv *, jobject);
    3). 将生成的头文件转移到jni文件夹下
    4). 在jni下定义对应的函数文件: test.c
    #include “com_zhonghao_jnitests2_JNIS.h”
    JNIEXPORT jstring JNICALL Java_com_zhonghao_jnitests2_JNIS_helloJNI
    (JNIEnv * env, jobject jobj) {
    return (*env)->NewStringUTF(env, “Hello from C”);
    }
    ****Ps:或者将生成的.h文件中的 JNIEXPORT jstring JNICALL Java_com_chen_jnidemo_JNI_printHelloWorld (JNIEnv , jobject);方法复制到c文件中,实现该方法:
    /#include

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Android JNI学习路线可以按照以下步骤进行: 1. 了解JNI的基本概念和作用:JNIJava Native Interface)是Java提供的一种机制,用于实现Java与其他编程语言(如C、C++)之间的交互。它允许在Java代码中调用本地代码(Native Code),并且可以在本地代码中调用Java代码。 2. 学习JNI的基本语法和规则:JNI使用一组特定的函数和数据类型来实现Java与本地代码之间的交互。你需要学习如何声明本地方法、如何在Java代码中调用本地方法、如何在本地代码中调用Java方法等。 3. 学习JNI的数据类型映射:JNI提供了一套数据类型映射规则,用于将Java数据类型映射到本地代码中的数据类型。你需要学习如何处理基本数据类型、对象类型、数组类型等。 4. 学习JNI的异常处理:在JNI中,Java代码和本地代码之间的异常处理是非常重要的。你需要学习如何在本地代码中抛出异常、如何在Java代码中捕获异常等。 5. 学习JNI的线程处理:JNI允许在本地代码中创建和操作线程。你需要学习如何创建和销毁线程、如何在线程之间进行通信等。 6. 学习JNI的性能优化:JNI涉及到Java代码和本地代码之间的频繁切换,因此性能优化是非常重要的。你需要学习如何减少JNI调用的次数、如何避免不必要的数据拷贝等。 7. 学习JNI的调试和测试:在开发JNI程序时,调试和测试是非常重要的。你需要学习如何使用调试器调试本地代码、如何进行单元测试等。 8. 学习JNI的进阶主题:一旦掌握了基本的JNI知识,你可以进一步学习JNI的进阶主题,如JNIJava虚拟机的交互、JNI与动态链接库的交互、JNI与多线程的交互等。 总结起来,Android JNI学习路线包括了基本概念、基本语法、数据类型映射、异常处理、线程处理、性能优化、调试和测试以及进阶主题等内容。通过系统地学习这些知识,你将能够更好地理解和应用JNI技术。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting_Boss_Hao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值