暑期实习找到了A的安全开发,提前学习下NDK开发的简单流程。

项目创建

新建一个Empty Activity:

右键点击 app/src/ 下的 main 目录,然后New > Directory,为目录输入一个名称(例如 cpp)并点击 OK:

右键点击刚刚创建的cpp目录,然后 New > C/C++ Source File,输入一个名称,例如 test-ndk:

然后右键点击选中 app ,然后选择 New > File,输入CMakeLists.txt 作为文件名并点击 OK:

添加命令到 CMakeLists.txt 中:

cmake_minimum_required(VERSION 3.4.1)add_library( # Sets the name of the library.test-ndk# Sets the library as a shared library.SHARED# Provides a relative path to your source file(s).src/main/cpp/test-ndk.cpp )find_library( # Sets the name of the path variable.log-lib# Specifies the name of the NDK library that# you want CMake to locate.log )target_link_libraries( # Specifies the target library.test-ndk# Links the target library to the log library# included in the NDK.${log-lib} )include_directories(src/main/cpp/include/)

接着要将 Gradle 关联到原生库,右键app,点击 Link C++ Project with Gradle,配置 CMakeLists.txt 的路径,点击 OK。然后 app目录下的build.gradle文件会自动添加以下代码:

externalNativeBuild {cmake {path file('CMakeLists.txt')}}

然后需要配置Javah命令工具,以macos为例,点击左上角打开设置:

点击+ 配置添加外部工具:

配置完成后如下:

program:javah的路径
arguments:-classpath . -jni -d $SourcepathEntry$/src/main/cpp $FileClass$
working directory:$SourcepathEntry$

静态注册

我们使用静态注册来学习一些基础的用法。

JNI访问Java成员变量

编辑MainActivity文件,这里先以静态注册JNI访问Java成员变量为例,编辑 MainActivity:

public class MainActivity extends AppCompatActivity {public String showText = "Hello ";static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);String res = "before: " + showText;test();res += ", after:" + showText;textView.setText(res);}public native void test();
}

然后右键点击MainActivity,选择弹出框中的 External Tools 的 JavaH 。就会在src/main/java/src/main/cpp目录下生成 com_example_myapplication_MainActivity.h文件,将这个.h文件复制到cpp目录下(应该是可以直接设置生成路径的):

然后修改test-ndk.cpp代码,遵循以下步骤:

  • 通过env->GetObjectClass(jobject)获取Java 对象的 class 类,返回一个 jclass
  • 调用env->GetFieldID(jclazz, fieldName, signature)得到该实例域(变量)的 id,即 jfieldID;如果变量是静态 static 的,则调用的方法为 GetStaticFieldID
  • 然后如果是要获取变量的话通过调用env->Get{type}Field(jobject, fieldId) 得到该变量的值。其中{type}是变量的类型,例如int、Object等;如果变量是静态 static 的,则调用的方法是GetStatic{type}Field(jclass,fieldId),注意 static 的话, 是使用 jclass 作为参数。如果是要修改的话,就把前面的Get…改为Set{type}Field(jobject, fieldId, “新的字符串”),修改静态变量为SetStatic{type}Field(jclass, fieldId, “新的字符串”);

代码如下:

#include "jni.h"
#include "com_demo_ndktest_MainActivity.h"extern "C" JNIEXPORT void JNICALL Java_com_demo_ndktest_MainActivity_test(JNIEnv *env , jobject obj){jclass jcls = env->GetObjectClass(obj);jfieldID  jfid = env->GetFieldID(jcls,"showText", "Ljava/lang/String;");jstring js = env->NewStringUTF("Hello world");env->SetObjectField(obj,jfid,js);
}

然后编译运行即可:

JNI访问Java静态变量

如果是静态注册JNI访问Java静态变量的话:

public class MainActivity extends AppCompatActivity {public static String static_String = "static";static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);String res = "before: " + static_String;test();res += ", after:" + static_String;textView.setText(res);}public native void test();
}

cpp就是把非静态的改为GetStaticFieldID、SetStaticObjectField,编译运行后:

JNI访问Java非静态方法

如果是静态注册JNI访问Java非静态方法,可以看到MainActivity方法中是没有调用过getName()方法的,我们在so中调用,也就是test()方法中调用:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);textView.setText(test());}public String getName(String name){return "ndk"+name;}public native String test();
}

重新javah后,编辑cpp文件如下:

#include "jni.h"
#include "com_demo_ndktest_MainActivity.h"extern "C" JNIEXPORT jstring JNICALL Java_com_demo_ndktest_MainActivity_test(JNIEnv *env, jobject obj){jclass jcls = env->GetObjectClass(obj);jmethodID jmid = env->GetMethodID(jcls,"getName", "(Ljava/lang/String;)Ljava/lang/String;");jstring js = env->NewStringUTF("ndk");jobject job = env->CallObjectMethod(obj,jmid,js);return static_cast<jstring >(job);
}

然后编译运行:

JNI访问Java静态方法

如果是静态注册JNI访问Java静态方法的话,修改MainActivity:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);textView.setText(test(100)+"");}public static int getValue(int max){return new Random().nextInt(max);}public native int test(int max);
}

然后编辑cpp文件:

#include "jni.h"
#include "com_demo_ndktest_MainActivity.h"‘extern "C" JNIEXPORT jint JNICALL Java_com_demo_ndktest_MainActivity_test(JNIEnv *env, jobject obj,jint max){jclass jcls = env->GetObjectClass(obj);jmethodID methodID = env->GetStaticMethodID(jcls,"getValue", "(I)I");jint res = env->CallStaticIntMethod(jcls, methodID, max);return res;
}

编辑运行如下:

JNI访问Java构造方法

这里以访问Date类的构造方法为例,修改MainActivity:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);textView.setText(test().toString());}public native Date test();
}

修改test-ndk.cpp文件:

#include "jni.h"
#include "com_demo_ndktest_MainActivity.h"extern "C" JNIEXPORT jobject JNICALL Java_com_demo_ndktest_MainActivity_test(JNIEnv *env, jobject obj){jclass jcls = env->FindClass("java/util/Date");//反射获取jmethodID jmid = env->GetMethodID(jcls,"<init>", "()V");jobject jobj = env->NewObject(jcls,jmid);return  jobj;
}

编译运行后结果如下:

JNI操作Java数组

这里以创建数组以及对数组进行排序为例,修改MainActivity:

public class MainActivity extends AppCompatActivity {public String before = "";public String after = "";static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);TextView textView2 = findViewById(R.id.textview2);test();textView.setText(before);textView2.setText(after);}public void test(){int[] array = getIntArray(10);for(int i:array){before += i+ " ";}sortIntArray(array);for(int i:array){after += i+ " ";}}public native int[] getIntArray(int len);public native void sortIntArray(int[] arr);}

修改cpp文件:

#include "jni.h"
#include "com_demo_ndktest_MainActivity.h"
#include "random"/** Class:     com_demo_ndktest_MainActivity* Method:    getIntArray* Signature: (I)[I*/
extern "C" JNIEXPORT jintArray JNICALL Java_com_demo_ndktest_MainActivity_getIntArray(JNIEnv *env, jobject obj, jint len){jintArray array = env->NewIntArray(len);jint *arrPtr = env->GetIntArrayElements(array, nullptr);jint *start = arrPtr;for(;start<arrPtr+len;start++){*start = static_cast<jint>(random()%100);}//C中操作同步到java,并释放资源env->ReleaseIntArrayElements(array,arrPtr,0);return array;
}/** Class:     com_demo_ndktest_MainActivity* Method:    sortIntArray* Signature: ([I)V*/
int compare(const void *a,const void *b){return *(int *) a - *(int *) b;
}
extern "C" JNIEXPORT void JNICALL Java_com_demo_ndktest_MainActivity_sortIntArray(JNIEnv *env, jobject obj, jintArray intArr){//获取起始指针jint *arrPtr = env->GetIntArrayElements(intArr,nullptr);//获取数组长度jint len = env->GetArrayLength(intArr);qsort(arrPtr,len, sizeof(jint),compare);env->ReleaseIntArrayElements(intArr,arrPtr,0);
}

编译运行后结果如下:

JNI遍历文件夹

首先封装一个打印相关的头文件LogUtils.h:

#ifndef _LOG_UTILS_H_
#define _LOG_UTILS_H_#define DEBUG // 可以通过 CmakeLists.txt 等方式来定义在这个宏,实现动态打开和关闭LOG// Windows 和 Linux 这两个宏是在 CMakeLists.txt 通过 ADD_DEFINITIONS 定义的
#ifdef Windows
#define __FILENAME__ (strrchr(__FILE__, '\\') + 1) // Windows下文件目录层级是'\\'
#elif Linux
#define __FILENAME__ (strrchr(__FILE__, '/') + 1) // Linux下文件目录层级是'/'
#else
#define __FILENAME__ (strrchr(__FILE__, '/') + 1) // 默认使用这种方式
#endif#ifdef DEBUG
#define TAG "JNI"
#define LOGV(format, ...) __android_log_print(ANDROID_LOG_VERBOSE, TAG,\"[%s][%d]: " format, __FILENAME__,  __LINE__, ##__VA_ARGS__);
#define LOGD(format, ...) __android_log_print(ANDROID_LOG_DEBUG, TAG,\"[%s][%d]: " format, __FILENAME__,  __LINE__, ##__VA_ARGS__);
#define LOGI(format, ...) __android_log_print(ANDROID_LOG_INFO, TAG,\"[%s][%d]: " format, __FILENAME__,  __LINE__, ##__VA_ARGS__);
#define LOGW(format, ...) __android_log_print(ANDROID_LOG_WARN, TAG,\"[%s][%d]: " format, __FILENAME__,  __LINE__, ##__VA_ARGS__);
#define LOGE(format, ...) __android_log_print(ANDROID_LOG_ERROR, TAG,\"[%s][%d]: " format, __FILENAME__,  __LINE__, ##__VA_ARGS__);
#else
#define LOGV(format, ...);
#define LOGD(format, ...);
#define LOGI(format, ...);
#define LOGW(format, ...);
#define LOGE(format, ...);
#endif // DEBUG#endif // _LOG_UTILS_H

修改MainActivity代码:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);test();}public void test(){listDirFIle("system/app/");}public native void listDirFIle(String dirPath);
}

修改cpp文件代码:

#include "jni.h"
#include "com_demo_ndktest_MainActivity.h"
#include "string"
#include "dirent.h"
#include "android/log.h"
#include "LogUtils.h"const int PATH_MAX_LENGTH = 256;extern "C" JNIEXPORT void JNICALL Java_com_demo_ndktest_MainActivity_listDirFIle(JNIEnv *env, jobject instance, jstring dirPath_){if (dirPath_ == nullptr) {LOGE("dirPath is null!");return;}const char *dirPath = env->GetStringUTFChars(dirPath_, nullptr);//长度判断if (strlen(dirPath) == 0) {LOGE("dirPath length is 0!");return;}//打开文件夹读取流DIR *dir = opendir(dirPath);if (nullptr == dir) {LOGE("can not open dir, check path or permission!")return;}struct dirent *file;while ((file = readdir(dir)) != nullptr) {//判断是不是 . 或者 .. 文件夹if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0) {continue;}if (file->d_type == DT_DIR) {//是文件夹则遍历//构建文件夹路径char *path = new char[PATH_MAX_LENGTH];memset(path, 0, PATH_MAX_LENGTH);strcpy(path, dirPath);strcat(path, "/");strcat(path, file->d_name);jstring tDir = env->NewStringUTF(path);//递归遍历Java_com_demo_ndktest_MainActivity_listDirFIle(env, instance, tDir);//释放文件夹路径内存free(path);} else {//打印文件名LOGD("### %s/%s", dirPath, file->d_name);}}//关闭读取流closedir(dir);env->ReleaseStringUTFChars(dirPath_, dirPath);
}

编译运行后:

调用父类方法

修改MainActivity:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);childJni cj = new childJni();textView.setText(cj.test());}public class SuperJni{public String hello(String name){return "Hello " + name;}}public  class  childJni extends SuperJni{public native String test();}
}

修改cpp:

extern "C" JNIEXPORT jstring JNICALL Java_com_demo_ndktest_MainActivity_00024childJni_test(JNIEnv *env, jobject obj){//需要通过反射FindClass获取父类实体类,FindClass需要传入完整的类名。而GetObjectClass只需一个jobject饮用即可jclass jcls = env->FindClass("com/demo/ndktest/MainActivity$SuperJni");if(jcls == nullptr){return env->NewStringUTF("error");}jmethodID jmid = env->GetMethodID(jcls,"hello", "(Ljava/lang/String;)Ljava/lang/String;");jstring js = env->NewStringUTF("World");//调用父类的方法是 CallNonvirtual{type}Method 函数return static_cast<jstring>(env->CallNonvirtualObjectMethod(obj, jcls, jmid, js));
}

编译运行后结果如下:

自定义对象参数的传递

新建Person类:

public class Person {private String name;private int age;public Person(){}public Person(int age,String name){this.age = age;this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Person :{ name: "+name+", age: "+age+"}";}
}

修改MainActivity代码:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);textView.setText(test(new Person()).toString());}public native Person test(Person person);
}

修改cpp代码:

extern "C" JNIEXPORT jobject JNICALL Java_com_demo_ndktest_MainActivity_test(JNIEnv *env, jobject obj, jobject person){jclass jcls = env->FindClass("com/demo/ndktest/Person");jmethodID jmid = env->GetMethodID(jcls,"<init>", "(ILjava/lang/String;)V");if(jmid == NULL){return env->NewStringUTF("Error");}jstring name = env->NewStringUTF("irirs");return env->NewObject(jcls,jmid,21,name);
}

编译运行结果如下:

自定义对象的集合参数的传递

修改MainActivity:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);TextView textView2 = findViewById(R.id.textview2);ArrayList<Person> arrayList = new ArrayList<>();for(int i=0;i<3;i++){arrayList.add(new Person(10+i,"iris"));}textView.setText("before:" + arrayList.toString());textView2.setText("after:" + test(arrayList).toString());}public native ArrayList<Person> test(ArrayList<Person> persons);
}

修改cpp:

extern "C" JNIEXPORT jobject JNICALL Java_com_demo_ndktest_MainActivity_test(JNIEnv *env, jobject obj, jobject personArrayList){jclass jcls = env->GetObjectClass(personArrayList);// //通过参数获取 ArrayList 对象的 classjmethodID jmid = env->GetMethodID(jcls,"<init>", "()V");//获取无参构造函数jobject arrayList = env->NewObject(jcls,jmid);//new一个 ArrayList 对象jmethodID addid = env->GetMethodID(jcls,"add", "(Ljava/lang/Object;)Z");jclass pjcls = env->FindClass("com/demo/ndktest/Person");jmethodID pjmid = env->GetMethodID(pjcls,"<init>", "(ILjava/lang/String;)V");jint i = 0;for(;i<3;i++){jstring name = env->NewStringUTF("refrain");jobject person = env->NewObject(pjcls,pjmid,20+i,name);env->CallBooleanMethod(arrayList,addid,person);}return arrayList;
}

编译运行后效果如下:

动态注册

可以看到静态注册java层方法和native层方法名称是相似的,安全性不高。并且函数名太长,文件、类名、变量或方法重构时,需要重新修改头文件或 C/C++ 内容代码。动态注册可以解决这个问题,其原理是直接告诉 native 方法其在JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系。具体步骤如下:
先编写 Java 的 native 方法;

  • 编写 JNI 函数的实现(函数名任意)
  • 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
  • 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
  • 在JNI_OnLoad 方法中调用注册方法;
  • 在Java中通过System.loadLibrary加载完JNI动态库之后,会自动调用JNI_OnLoad函数,完成动态注册;

先修改Mainactivity:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);textView.setText(test());}public native String test();
}

动态注册不再需要javah进行处理,直接编辑cpp文件:

#include "jni.h"
#include "string"extern "C" {static jstring jni_string(JNIEnv *env, jobject obj){jstring res = env->NewStringUTF("hello c++");return res;}static JNINativeMethod jNM_table[] = {{"test", "()Ljava/lang/String;",(void *)jni_string}//还有其他方法的话,直接后面,补就行};static int registerNativeMethods(JNIEnv *env){jclass clazz = env->FindClass("com/demo/ndktest/MainActivity");if(clazz == NULL){return JNI_ERR;}if(env->RegisterNatives(clazz,jNM_table, sizeof(jNM_table) / sizeof(jNM_table[0]))<0){return JNI_ERR;}return JNI_OK;}jint JNI_OnLoad(JavaVM *vm, void *reserved){JNIEnv *env = NULL;if(vm->GetEnv((void **) &env,JNI_VERSION_1_4)!=JNI_OK){return -1;}registerNativeMethods(env);return JNI_VERSION_1_4;}
}

编辑运行后结果如下:

多个类的动态注册

新建JNIAdd类,里面有个静态的native方法:

public class JNIAdd {public static native int add(int x, int y);
}

修改MainActivity:

public class MainActivity extends AppCompatActivity {static {System.loadLibrary("test-ndk");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);TextView textView = findViewById(R.id.textview);TextView textView2 = findViewById(R.id.textview2);TextView textView3 = findViewById(R.id.textview3);textView.setText(stringFromJNI());textView2.setText(setString("Hello"));textView3.setText(JNIAdd.add(5,3)+"");}public native String stringFromJNI();public native String setString(String str);
}

修改cpp:

#include "jni.h"
#include "string"extern "C" {static jstring jni_string(JNIEnv *env, jobject obj){//非static native方法的第二个参数为jobjectreturn env->NewStringUTF("hello ndk");}static jint sum(JNIEnv *env,jclass clzz,jint x,jint y){//static native方法的第二个参数为jclassreturn x+y;}static jstring jni_setstr(JNIEnv *env, jobject obj, jstring str){char *dirPath = const_cast<char *>(env->GetStringUTFChars(str, nullptr));strcat(dirPath," world!");jstring res = env->NewStringUTF(dirPath);return res;}static JNINativeMethod jNM_MainActivity[] = {{"stringFromJNI","()Ljava/lang/String;",(void *) jni_string},{"setString","(Ljava/lang/String;)Ljava/lang/String;",(void *) jni_setstr},};static JNINativeMethod JNM_JNIAdd[] = {{"add","(II)I",(void *) sum}};static int registerNativeMethods(JNIEnv *env){jclass clazz = env->FindClass("com/demo/ndktest/MainActivity");if(clazz == NULL){return JNI_ERR;}if(env->RegisterNatives(clazz,jNM_MainActivity, sizeof(jNM_MainActivity) / sizeof(jNM_MainActivity[0]))<0){return JNI_ERR;}return JNI_OK;}static int registerNativeMethods_JNIAdd(JNIEnv *env){jclass clazz = env->FindClass("com/demo/ndktest/JNIAdd");if(clazz == NULL){return JNI_ERR;}if(env->RegisterNatives(clazz,JNM_JNIAdd, sizeof(JNM_JNIAdd) / sizeof(JNM_JNIAdd[0]))<0){return JNI_ERR;}return JNI_OK;}jint JNI_OnLoad(JavaVM *vm, void *reserved){JNIEnv *env = NULL;if(vm->GetEnv((void **) &env,JNI_VERSION_1_4)!=JNI_OK){return -1;}registerNativeMethods(env);registerNativeMethods_JNIAdd(env);return JNI_VERSION_1_4;}
}

运行结果如下:

参考:

  • 103style博客
  • cfanrCoder博客
  • Android Studio3.0开发JNI流程------JNI静态注册和动态注册(多个类的native动态注册-经典篇)

安卓开发之NDK开发基础(一)相关推荐

  1. 安卓开发之Handler、HandlerThread学习篇

    安卓开发之Handler.HandlerThread学习心得篇           开篇说明:本文采用的都是最基础最简单的例子,目的只有一个:希望大家将学习的焦点放在Handler的理解和使用上,我不 ...

  2. android studio开发工具介绍,Android应用开发之Android开发工具介绍、Android Studio配置...

    本文将带你了解Android应用开发之Android开发工具介绍.Android Studio配置,希望本文对大家学Android有所帮助. 2.1   Android Studio配置 2.1.1 ...

  3. iOS开发之Objective-C(基础篇)-李飞-专题视频课程

    iOS开发之Objective-C(基础篇)-232人已学习 课程介绍         该系列课程是iOS开发之Objective-C基础入门视频.课程中会详细的讲解OC语法特点,面向对象的使用,循环 ...

  4. Linux嵌入式系统开发之Led开发——应用篇(一)

    与Linux嵌入式系统开发之Led开发--驱动篇(一),对于的应用篇 看看咱们的开发板,有四个led灯,对吧,这次就是向办法用程序来点亮它,请看下边的代码: #include <stdlib.h ...

  5. 安卓开发之用RecyclerView做陈列式布局(仿小红书首页/淘宝商品浏览)

    安卓开发之用RecyclerView做陈列式布局 一.使用RecyclerView要先导入recyclerview-v7库 二.在layout文件夹内,新建一个xml文件,编写你要展示的item的样式 ...

  6. 安卓开发之WebView,进度条ProgressBar以及MediaPlayer和SonundPool的使用

    原 安卓开发之WebView,进度条ProgressBar以及MediaPlayer和SonundPool的使用 2018年06月06日 15:04:21 阅读数:106 内容比较简单,仅用作笔记,所 ...

  7. iOS开发之UI开发(UITableView)

    UITableView 继承自UIScrollView,性能极佳 UITableView的两种样式 UITableViewStylePlain列表样式 UITableViewStyleGrouped ...

  8. 安卓 App 库存系统开发 基础版本

    由于客户时间急迫,跟客户沟通后,只需要完成原先需求的安卓方面 而网页方面,客户自己直接用 Django 的 Admin 模版 安卓方面开发重点 用了4个 Activity 主 Activity 负责开 ...

  9. STM32WL开发之LORA开发环境及其Demo例程介绍

    前言:在前一篇<STM32WL开发之LM401评估板开箱及PingPong测试>中经过测试,认为STM32WL的LoRa通信和易智联的LM401评估板都是OK的,接下来就开始开发环境的准备 ...

  10. 安卓eclipse 的ndk开发

    谷歌改良了ndk的开发流程,对于Windows环境下NDK的开发,如果使用的NDK是r7之前的版本,必须要安装Cygwin才能使用NDK.而在NDKr7开始,Google的Windows版的NDK提供 ...

最新文章

  1. 酸爽! Intellij IDEA 神器居然还藏着这些实用小技巧 !
  2. Ubuntu 常见报错处理
  3. asp.net和javascript怎样结合
  4. JupyterNotebook随记(part1)--打开默认目录
  5. react dispatch_React系列自定义Hooks很简单
  6. [C++] - 类的构造函数constructor
  7. linux nginx安装php5.5,linux下搭建LNMP(linux+nginx+mysql+php)环境之mysql5.5安装
  8. 羽毛球:东南大学vs南京大学
  9. configure: error: Package requirements (commoncpp 6.2.2) were not met
  10. KEmulator与eclipse的集成
  11. 使用python画二元二次函数(笔记)
  12. Netty8# Netty之ByteBuf初探
  13. 农村有人收旧房梁,一根100多,破木头有啥用?
  14. redis 应用场景
  15. 14期《掬水月在手,弄花香满衣》1月刊
  16. win7进去提醒未能连接一个服务器,win7系统提示“未能连接一个windows服务”这个情况如何解决...
  17. windows用虚拟机vmWare安装黑苹果及注意事项
  18. ipa上架App Store流程
  19. CSS3+JS完美实现放大镜模式
  20. 软件需求规格说明书样例

热门文章

  1. Jemalloc安装
  2. 微信小程序MINA框架介绍
  3. windows10下超级好用的截屏自带快捷键
  4. 荣耀 android 5.0 root,华为EMUI5.0 可用的ROOT工具,我是作者!!!!——精华帖
  5. 如何制作毕业地图分布图_最简单的数据地图制作,一共6步搞定!
  6. MATLAB之Simulink基础
  7. Golang系列(四)之面向接口编程
  8. lqr matlab,MATLAB中的LQR函数用法
  9. mysql语句大全及例子_SQL语句大全实例教程.pdf
  10. (152)IES光源概述文件