Android NDK development: the first Jni practice

Posted by gamma_raj on Wed, 05 Jan 2022 23:50:14 +0100

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

  1. new a project
  2. Select template Native C++
  3. 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.

  1. Create a new hello. In the CPP folder cpp

  2. 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());
    }
    
  3. 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)
    
  4. 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

  1. 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);
        // }
    }
    
  2. 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)
    
  3. 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

  1. Through JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) function
  2. 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

  1. In the main thread, VM - > getenv ((void * *) & Env, result) = = JNI_ OK to get (same as the sample code)

  2. The first parameter of other functions is JNIEnv *env

  3. 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

  1. Method 1: use env - > findclass() in the main thread to find it and save it to the global variable

  2. Method 2: find the ClassLoader object in the main thread and save it as a global variable for use

    1. At JNI_ Load a class in the onload() function, and get and save the ClassLoader object
    2. Write the encapsulated getEnv() and loadClass() functions
    3. To load, call the toString() method of the Person class

Local and global references

  1. 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.
  2. 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

  1. 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);
    
  2. 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;
    }
    
  3. 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

  1. 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()

Topics: Java Android Android Studio JNI NDK