NDK development: the first Jni practice
The project source code and original documents are in Github: [Forgo7ten / AndroidReversePractice]
1. Create a ndk project using AS
- new a project
- Select template Native C++
- Next, modify the project name, package name, and directory. Next, Finish
Directory structure:
Android Developer documentation: Add C and C + + code to your project | Android developers | Android Developers (google.cn)
2. create a new so source file and call it in java.
-
Create a new hello. In the CPP folder cpp
-
In hello Add code to CPP
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_forgotten_firstjni_MainActivity_stringFromHello( JNIEnv* env, jobject /* this */) { std::string hello = "He1·llo from Hello"; return env->NewStringUTF(hello.c_str()); }
-
Modify cmakelists txt
add_library( # Sets the name of the library. # The generated so file name needs to be in the java code at the same time Name of loadlibrary() hello # Sets the library as a shared library. The setting is STATIC or dynamic share SHARED # Provides a relative path to your source file(s). # so source file relative path hello.cpp ) ############# You can also compile multiple source files into the same so file ############# add_library( # Sets the name of the library. firstjni # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). native-lib.cpp hello.cpp)
-
Modify Java code
public class MainActivity extends AppCompatActivity { // Used to load the 'firstjni' library on application startup. static { System.loadLibrary("firstjni"); } private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); // Example of a call to a native method TextView tv = binding.sampleText; // tv.setText(stringFromJNI()); tv.setText(stringFormHello()); } /** * A native method that is implemented by the 'firstjni' native library, * which is packaged with this application. */ public native String stringFromJNI(); public native String stringFormHello(); }
Run successfully
JNI usage object
1. Create a new Person class as a test
public class Person { public static int sNumber; private static String country; private String mName; public int mAge; static { sNumber = 100; country = "China"; } }
2. In hello Write relevant methods in CPP for testing
Then reference the so Library in java, declare and call the native function
get and set of static field
extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_MainActivity_useObjectStaticField( JNIEnv *env, jobject /* this */) { // Find this class jclass person_clazz = env->FindClass("com/forgotten/firstjni/Person"); // Find (public) static field ID jfieldID number_fieldID = env->GetStaticFieldID(person_clazz, "sNumber", "I"); // Gets the value of the field jint number = env->GetStaticIntField(person_clazz, number_fieldID); __android_log_print(ANDROID_LOG_DEBUG, "useObjectStaticField", "before sNumber=%d", number); env->SetStaticIntField(person_clazz,number_fieldID,999); __android_log_print(ANDROID_LOG_DEBUG, "useObjectStaticField", "after sNumber=%d", number); // The search (private) static field ID is the same as that in the public | jni jfieldID country_fieldID = env->GetStaticFieldID(person_clazz,"country", "Ljava/lang/String;"); jstring country_jstr = static_cast<jstring>(env->GetStaticObjectField(person_clazz, country_fieldID)); const char* country_chars = env->GetStringUTFChars(country_jstr, nullptr); __android_log_print(ANDROID_LOG_DEBUG,"useObjectStaticField","country=%s",country_chars); }
get and set of field
You need to instantiate the object first. There are two ways to instantiate the object
The first method uses enc - > newobject() (common)
extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_MainActivity_useObjectField1( JNIEnv *env, jobject /* this */) { // Find this class jclass person_clazz = env->FindClass("com/forgotten/firstjni/Person"); // Find the methodID of the construction method jmethodID constructID1 = env->GetMethodID(person_clazz,"<init>", "()V"); // Instantiate objects jobject person1 = env->NewObject(person_clazz,constructID1); // Find fieldID of field name jfieldID nameID = env->GetFieldID(person_clazz,"mName", "Ljava/lang/String;"); // Gets the stored value from the object and fieldID jstring name_jstr = static_cast<jstring>(env->GetObjectField(person1, nameID)); // Convert jstring to char × type const char * name_chars = env->GetStringUTFChars(name_jstr, nullptr); __android_log_print(ANDROID_LOG_DEBUG, "useObjectField1", "name=%s", name_chars); // The string needs to be released after use. I'm not sure whether to use it this way env->ReleaseStringUTFChars(name_jstr, name_chars); __android_log_print(ANDROID_LOG_DEBUG, "useObjectField1", "name=%s", name_chars); // It seems that you can still use... Dizzy after release jmethodID constructID2 = env->GetMethodID(person_clazz,"<init>", "(Ljava/lang/String;)V"); // Set parameters to the parameterized constructor and execute to get the object jobject person2 = env->NewObject(person_clazz,constructID2,env->NewStringUTF("Hello")); jstring name_jstr2 = static_cast<jstring>(env->GetObjectField(person2, nameID)); __android_log_print(ANDROID_LOG_DEBUG, "useObjectField1", "param name=%s", env->GetStringUTFChars(name_jstr2, nullptr)); }
Use env - > callnonvirtualvoidmethod() to get the object
extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_MainActivity_useObjectField2( JNIEnv *env, jobject /* this */) { // Find this class jclass person_clazz = env->FindClass("com/forgotten/firstjni/Person"); // Find the methodID of the construction method jmethodID constructID = env->GetMethodID(person_clazz,"<init>", "()V"); // A new object was created but not initialized jobject person_obj = env->AllocObject(person_clazz); // Initialize the object. The fourth parameter is params env->CallNonvirtualVoidMethod(person_obj, person_clazz, constructID); // Find fieldID of field name jfieldID nameID = env->GetFieldID(person_clazz,"mName", "Ljava/lang/String;"); jstring name_jstr2 = static_cast<jstring>(env->GetObjectField(person_obj, nameID)); __android_log_print(ANDROID_LOG_DEBUG, "useObjectField2", "name=%s", env->GetStringUTFChars(name_jstr2, nullptr)); }
Processing of int [] array type
/** * Using array */ extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_MainActivity_useArray( JNIEnv *env, jobject /* this */) { // Find this class jclass person_clazz = env->FindClass("com/forgotten/firstjni/Person"); // Find (public) static field ID jfieldID array_fieldID = env->GetStaticFieldID(person_clazz, "testArray", "[I"); jintArray tarray = static_cast<jintArray>(env->GetStaticObjectField(person_clazz, array_fieldID)); // Get the length of the array int length = env->GetArrayLength(tarray); // Get int pointer to array int* p = env->GetIntArrayElements(tarray, nullptr); for(int i=0;i<length;++i){ // Loop print array __android_log_print(ANDROID_LOG_DEBUG, "useArray", "array[%d]=%d",i,p[i]); } int newarr[length]; for(int i =0;i<length;++i){ newarr[i] = 100-i; } // Modify the values in the array env->SetIntArrayRegion(tarray,0,length,newarr); }
Calling static and non static methods
/** * JNI Calling static and non static methods */ extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_MainActivity_useMethod(JNIEnv *env, jobject thiz) { // Find this class jclass person_clazz = env->FindClass("com/forgotten/firstjni/Person"); // Find the corresponding construction method jmethodID constructID = env->GetMethodID(person_clazz, "<init>", "(Ljava/lang/String;)V"); jstring hello_js = env->NewStringUTF("Hello"); // Set parameters to the parameterized constructor and execute to get the object jobject person_obj = env->NewObject(person_clazz, constructID, hello_js); // Get the static method ID in the Person class jmethodID sMethod_mid = env->GetStaticMethodID(person_clazz,"sMethod", "(Ljava/lang/String;)I"); // The corresponding methods include callstaticintmethoda and callstaticintmethodv, but the passed parameter forms are different // The int in the middle is the return value. If the return value of the method is void, callvoid method() should be called int hello_len = env->CallStaticIntMethod(person_clazz,sMethod_mid,hello_js); __android_log_print(ANDROID_LOG_DEBUG, "useMethod", "hello_len=%d", hello_len); // Get the non static method ID in the Person class jmethodID mMethod_mid = env->GetMethodID(person_clazz,"mMethod","(Ljava/lang/String;)I"); int hello_2len = env->CallIntMethod(person_obj,mMethod_mid,hello_js); __android_log_print(ANDROID_LOG_DEBUG, "useMethod", "hello_2len=%d", hello_2len); }
Call parent method
Take the onCreate method of the subclass as an example
-
Create a NativeActivityActivity
public class NativeActivity extends AppCompatActivity { static { System.loadLibrary("nactivity"); } protected native void onCreate(Bundle savedInstanceState); // @Override // protected void onCreate(Bundle savedInstanceState) { // super.onCreate(savedInstanceState); // setContentView(R.layout.activity_native); // } }
-
At cmakelists Txt
add_library( # Sets the name of the library. nactivity # Sets the library as a shared library. SHARED # Provides a relative path to your source file(s). nactivity.cpp)
-
Write activity cpp
#include <jni.h> extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_NativeActivity_onCreate(JNIEnv *env, jobject thiz, jobject Bundle_obj) { // super.onCreate(savedInstanceState); jclass AppCompatAcitvity_jclazz = env->FindClass("androidx/appcompat/app/AppCompatActivity"); jmethodID onCreate_mid = env->GetMethodID(AppCompatAcitvity_jclazz,"onCreate", "(Landroid/os/Bundle;)V"); /** * Call the method of the parent class (1. Current object; 2. Clazz of the parent class; 3. Method mid;4. Method parameter list) */ env->CallNonvirtualVoidMethod(thiz, AppCompatAcitvity_jclazz, onCreate_mid, Bundle_obj); // Print log d("NativeActivity","onCreate run.."); jclass Log_clazz = env->FindClass("android/util/Log"); jmethodID Log_d_mid = env->GetStaticMethodID(Log_clazz,"d","(Ljava/lang/String;Ljava/lang/String;)I"); jstring tag = env->NewStringUTF("NativeActivity"); jstring info = env->NewStringUTF("onCreate run.."); env->CallStaticIntMethod(Log_clazz,Log_d_mid,tag,info); }
Implement the onCreate() method
extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_NativeActivity_onCreate(JNIEnv *env, jobject thiz, jobject Bundle_obj) { /** super.onCreate(savedInstanceState); **/ // Get class clazz from object jclass NativeActivity_clazz = env->GetObjectClass(thiz); // Get parent class from child class jclass AppCompatAcitivity_clazz = env->GetSuperclass(NativeActivity_clazz); // Gets the onCreate method ID of the parent class jmethodID supper_onCreate_mid = env->GetMethodID(AppCompatAcitivity_clazz,"onCreate", "(Landroid/os/Bundle;)V"); // Execution method env->CallNonvirtualVoidMethod(thiz, AppCompatAcitivity_clazz, supper_onCreate_mid, Bundle_obj); /** setContentView(R.id.activity_native); **/ // Gets the method ID of setContentView jmethodID setContentView_mid = env->GetMethodID(NativeActivity_clazz,"setContentView", "(I)V"); jclass R_layout_clazz = env->FindClass("com/forgotten/firstjni/R$layout"); jfieldID activity_native_fid = env->GetStaticFieldID(R_layout_clazz,"activity_native","I"); // Get layout ID value jint activity_native_value = env->GetStaticIntField(R_layout_clazz,activity_native_fid); env->CallVoidMethod(thiz,setContentView_mid,activity_native_value); /** showText=findViewById(R.id.show_text); **/ jclass R_id_clazz = env->FindClass("com/forgotten/firstjni/R$id"); jmethodID findViewById_mid = env->GetMethodID(NativeActivity_clazz,"findViewById", "(I)Landroid/view/View;"); jfieldID showText_fid = env->GetStaticFieldID(R_id_clazz,"show_text","I"); jint showText_value = env->GetStaticIntField(R_id_clazz,showText_fid); jobject showText = env->CallObjectMethod(thiz,findViewById_mid,showText_value); /** showText.setText("From nactivity.cpp") **/ jclass TextView_clazz = env->FindClass("android/widget/TextView"); jmethodID setText_mid = env->GetMethodID(TextView_clazz,"setText", "(Ljava/lang/CharSequence;)V"); jstring text_s = env->NewStringUTF("From nactivity.cpp"); env->CallVoidMethod(showText,setText_mid,text_s); /** Log.d("NativeActivity","onCreate run.."); **/ jclass Log_clazz = env->FindClass("android/util/Log"); jmethodID Log_d_mid = env->GetStaticMethodID(Log_clazz, "d", "(Ljava/lang/String;Ljava/lang/String;)I"); jstring tag = env->NewStringUTF("NativeActivity"); jstring info = env->NewStringUTF("onCreate run.."); env->CallStaticIntMethod(Log_clazz, Log_d_mid, tag, info); }
JavaVM and JNIEnv
- JavaVM is the representation of the native layer of java virtual machine in jni. It provides the basis for java running and calling. A process shares JavaVM, and a process may have multiple threads, which share JavaVM.
- JNIEnv is both a JNI environment and a java execution environment. With JNIEnv, you can call a series of JNI APIs to call java layer or other. JNI is unique to a thread. The implementation of a function may have multiple threads, and JNIEnv is not common to them.
Get javaVM
- Through JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) function
- Via env - > getjavavm (& VM); Method acquisition
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ // JNI version number jint result = JNI_VERSION_1_6; // Get env through vm JNIEnv * env = nullptr; if(vm->GetEnv((void **)&env,result) == JNI_OK){ JavaVM* evm = nullptr; // Get vm via env env->GetJavaVM(&evm); if(evm == vm){ __android_log_print(ANDROID_LOG_DEBUG,"JNI_OnLoad","evm == vm"); }else{ __android_log_print(ANDROID_LOG_DEBUG,"JNI_OnLoad","evm != vm"); } } return result; }
Get JNIEnv
-
In the main thread, VM - > getenv ((void * *) & Env, result) = = JNI_ OK to get (same as the sample code)
-
The first parameter of other functions is JNIEnv *env
-
Get env in child thread
// Save global variables of vm JavaVM* global_vm = nullptr; void *thread_method(void * args){ JNIEnv * thread_env = nullptr; // Attach current process if(global_vm->AttachCurrentThread(&thread_env, nullptr)==JNI_OK){ __android_log_print(ANDROID_LOG_DEBUG,"thread_method","get env=%p",&thread_env); } // UN attach current process global_vm->DetachCurrentThread(); pthread_exit(0); } JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ // JNI version number jint result = JNI_VERSION_1_6; /** Thread Get env**/ global_vm = vm; pthread_t thread; pthread_create(&thread, nullptr,thread_method,nullptr); pthread_join(thread, nullptr); return result; }
Find classes in child threads
-
Method 1: use env - > findclass() in the main thread to find it and save it to the global variable
-
Method 2: find the ClassLoader object in the main thread and save it as a global variable for use
- At JNI_ Load a class in the onload() function, and get and save the ClassLoader object
- Write the encapsulated getEnv() and loadClass() functions
- To load, call the toString() method of the Person class
Local and global references
- Local reference: created through NewLocalRef and various JNI interfaces (FindClass, NewObject, GetObjectClass, NewCharArray, etc.). Prevents GC from reclaiming referenced objects. The local reference can only be used in the current function. After the function returns, the object referenced by the local reference will be automatically released by the VM or manually released by calling DeleteLocalRef. Therefore, local references cannot be used across functions or threads.
- Global reference: calling NewGlobalRef to create a reference based on a local reference will prevent GC from recycling the referenced object. Global references can be used across functions and threads. ART will not be released automatically. You must call DeleteGlobalRef to manually release DeleteGlobalRef(g_ cIs_ string), otherwise memory leakage will occur.
3. Weak global reference: calling NewWeakGlobalRef is created based on local reference or global reference. It will not prevent GC from recycling the referenced object. It can be used across methods and threads. However, unlike global references, weak references do not prevent GC from reclaiming the objects it references. However, the reference will not be released automatically. It will be released when ART thinks it should be recycled (such as when memory is tight), or it can be released manually by calling DeleteWeakGlobalRef.
When calling local jni code, Java layer functions will maintain a local reference table (the reference table is not infinite). Generally, ART will release the reference after jni function call. If it is a simple function, you don't need to pay attention to these problems and let it release itself. There is basically no problem, but if there are a large number of circular operations in the function, Then the program may have an exception due to too many local references.
PushLocalFrame can create a reference stack for local references needed in the current function; PopLocalFrame is responsible for destroying all references in the stack. Therefore, the Push/PopLocalFrame function pair provides more convenient management of the local reference life cycle, instead of always paying attention to obtaining a reference and then calling DeleteLocalRef to release the reference. Before calling PopLocalFrame to destroy all references in the current frame, if the second parameter result is not empty, a new local reference will be generated by result, and then the newly generated local reference will be stored in the previous frame
/** * Test local reference, global reference, weak global reference */ extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_MainActivity_testLGRef(JNIEnv *env, jobject thiz) { jstring hello = env->NewStringUTF("Hello"); // env->DeleteLocalRef(hello); // It cannot be used after being deleted __android_log_print(ANDROID_LOG_DEBUG, "testLGRef", "hello=%s",env->GetStringUTFChars(hello, nullptr)); // If JNI has 10 reference locations (10 reference variables can be defined), then if is true if(env->EnsureLocalCapacity(10)==0){ __android_log_print(ANDROID_LOG_DEBUG, "testLGRef", "has 10 yes"); }else{ __android_log_print(ANDROID_LOG_DEBUG, "testLGRef", "has 10 no"); } // Define a global reference that can be recycled across processes and functions // global_hello = static_cast<jstring>(env->NewGlobalRef(hello)); // Define a weak global reference. The difference is that it can be recycled // global_hello = static_cast<jstring>(env->NewWeakGlobalRef(hello)); // A reference stack with a length of 10 is generated, and the next 10 local references are managed by the stack if(env->PushLocalFrame(10)==0){ jstring s1 = env->NewStringUTF("s1"); jstring s2 = env->NewStringUTF("s2"); // Destroy references in the stack // env->PopLocalFrame(nullptr); // Destroy the reference in the stack, keep s2 and use it as the result jobject res = env->PopLocalFrame(s2); }else{ // Insufficient space } }
Dynamic registration
- Static registration: (passive) the Dalvik/ART virtual machine finds and completes the address binding before calling
- Dynamic registration: (actively) app completes the binding between java functions and addresses in so itself: register by calling RegisterNatives()
JNI dynamic registration
-
Write relevant methods in NativeActivity
private void javaOnCreate(){ int dynamicLen = dynamicGetLen("dynamicGetLen"); Log.d("javaOnCreate", "dynamic_len= "+dynamicLen); dynamicPrintNum(5); } private native int dynamicGetLen(String str); private native void dynamicPrintNum(int num);
-
In activity Written in CPP
extern "C" JNIEXPORT void JNICALL Java_com_forgotten_firstjni_NativeActivity_onCreate(JNIEnv *env, jobject thiz, jobject Bundle_obj) { . . . . . . // Execute the javaOnCreate method jmethodID javaOnCreate_mid = env->GetMethodID(NativeActivity_clazz,"javaOnCreate", "()V"); env->CallVoidMethod(thiz,javaOnCreate_mid); } jint dynamic_one(JNIEnv *env, jobject thiz,jstring str){ const char * s = env->GetStringUTFChars(str, nullptr); __android_log_print(ANDROID_LOG_DEBUG, "dynamic", "str: %s", s); int len = env->GetStringUTFLength(str); return len; } // You can make the name not exported __attribute__ ((visibility ("hidden"))) void dynamic_two(JNIEnv *env, jobject thiz,jint num){ for(int i=0;i<num;i++){ __android_log_print(ANDROID_LOG_DEBUG, "dynamic", "i=%d", i); } } JNIEXPORT void RegisterNatives(JNIEnv *env) { jclass clazz = env->FindClass("com/forgotten/firstjni/NativeActivity"); if(nullptr!=clazz){ JNINativeMethod methods[] = { // 1:java native method 2: method signature 3: function to bind {"dynamicGetLen","(Ljava/lang/String;)I",(void *)dynamic_one}, {"dynamicPrintNum","(I)V",(void *)dynamic_two} }; // Dynamically register methods for the clazz class. The method list is methods and the length is the number env->RegisterNatives(clazz,methods,sizeof (methods)/sizeof (JNINativeMethod)); } } JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ // JNI version number jint result = JNI_VERSION_1_6; // Get env through vm JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, result) == JNI_OK) { RegisterNatives(env); } return result; }
-
At the same time, in order to use < Android / log h> , need to be in cmakelists Txt to import the library
target_link_libraries( # Specifies the target library. nactivity # Links the target library to the log library # included in the NDK. ${log-lib})
supplement
- Calling java function in ndk has a very high consumption performance.
Than JNI_OnLoad functions executed earlier
Add in activity
// constructor(num) is the priority. The smaller the priority is, the higher the priority is, the earlier the execution is; May be omitted together with parentheses __attribute__ ((constructor(2),visibility("hidden"))) void initarray_2(void){ __android_log_print(ANDROID_LOG_DEBUG,"nactivity","initarray_2()"); } extern "C" void _init(void){ __android_log_print(ANDROID_LOG_DEBUG,"nactivity","_init()"); } __attribute__ ((constructor(1),visibility("hidden"))) void initarray_1(void){ __android_log_print(ANDROID_LOG_DEBUG,"nactivity","initarray_1()"); } __attribute__ ((constructor(3),visibility("hidden"))) void initarray_3(void){ __android_log_print(ANDROID_LOG_DEBUG,"nactivity","initarray_3()"); }
result: D/nactivity: _init() D/nactivity: initarray_1() D/nactivity: initarray_2() D/nactivity: initarray_3()