【技术研究】NDK 动态注册以及自制APK分享

TEC

Posted by Corax on February 21, 2024

NDK 动态注册以及自制APK分享

前段时间家里有些事情,学习断了一段时间。

目前事情以及办完了,可以安下心来好好看移动安全了。

这几天看完了静态注册,jni部分函数,插桩,栈回溯,一些内容。

今天看了动态注册以及动态加载的一些原理,自己也手写了一个函数实现在native层的apk,拿来分享分享也自己巩固一下。

自制apk

界面

功能是很简单的,正好我最近车子开的多,想着自己搞一个油价计算器(后面会看看能不能自动获取实时油价)

image-20240222165806057

代码

其实我写的时候是没有什么调用思路的,单纯是跟着别人的项目,然后按照自己的需求更改功能,仅仅是这样而已。

但博客写出来的话易读性还是重要的,所以就以调用关系来分析我这个apk。

Java层

先是声明变量吧,毕竟布局里面的控件不和实际代码的变量联系起来,实现不了功能。(此处还没有进行绑定)

//声明文本和按钮 也就是上面的文本和按钮
    private EditText first;
    private EditText second;
    private EditText third;
    
    private Button button1;
    private Button button2;
    //作为运算的输入参数 等会获取文本框的内容,被放在这里
    private float one;
    private float two;
    private float three;

app进来会执行如下oncreate

里面加载布局,初始化,和调用运算方法。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化控件
        init();
        //调用运算方法
        Calcu();
    }

先是初始化控件,也很简单,就是把文本框,按钮先和代码的实际变量相互绑定。

private void init(){
        //把文本和按钮相绑定起来
        first = (EditText)findViewById(R.id.editText1);
        second =(EditText)findViewById(R.id.editText2);
        third =(EditText)findViewById(R.id.editText3);
        button1 = (Button)findViewById(R.id.Button1);
        button2 = (Button)findViewById(R.id.Button2);
    }

再是第二个Calcu

其实看着有点多,但看懂的关键在于setOnClickListener的使用方法就是会调用后面cl里面的onclick方法。

而cl被我们重写,详细的方法就是进行监听,然后用switchcase先得到此时文本框的值,再得到我们按的是什么button,从而进行不同按钮的不同方法。例如这里button1就是计算满油的价格,button2就是百公里价格

private void Calcu(){
        //按钮绑定监听 使用switch case简化代码 结果通过toast弹出
        final View.OnClickListener cl = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                switch (view.getId()) {
                    case R.id.Button1:
                        //获取文本框的值
                        one = Float.parseFloat(first.getText().toString());
                        two = Float.parseFloat(second.getText().toString());
                        three = Float.parseFloat(third.getText().toString());
                        Toast.makeText(MainActivity.this, FullCal(one, two)+"",Toast.LENGTH_LONG).show();
                        break;
                    case R.id.Button2:
                        //获取文本框的值
                        one = Float.parseFloat(first.getText().toString());
                        two = Float.parseFloat(second.getText().toString());
                        three = Float.parseFloat(third.getText().toString());
                        Toast.makeText(MainActivity.this, HundredCal(three, two)+"",Toast.LENGTH_LONG).show();
                        break;
                }
            }
        };
        //把监听事件传入button
        button1.setOnClickListener(cl);
        button2.setOnClickListener(cl);
    }

ok,到这边oncreate的内容暂时结束了,但后续更长的尾巴就在Calcu调用的FullCal和HundredCal两个方法里面,这两个方法我们做了如下声明

很明显对吧,是native方法。

    //定义native方法 实现so层
    public native float FullCal(float one,float two);
    public native float HundredCal(float three,float two);

ok至此我们java层工作到此位置了。

native层

其实关于native层 java层 jni的关系目前还不是很深入,但姑且我先这么说。(这部分后面一定会好好开一篇博客好好聊聊学学)

java实现java代码,但有些时候出于效率,保护代码等原因需要用到c,目光转向C代码。

native层实现c代码,但这个c和日常写的不太一样,要想和java层的代码相互配合使用,需要jni.h里面的一些写好的函数的方法,或者变量。

这就引入了jni,在我理解,jni这一层提供了很多的api和重定义变量以连接native层和java层。

ok,理解还很浅薄,但是不影响这篇博客继续,我们进入代码。

流程从加载好so后(java层),进入onload

里面进行了获取env的操作和判断注册是否成功的操作

GetEnv还没看,registerNative是后面重要的

//动态注册 使用JNILOAD进行动态注册 其中使用到上面的registernative函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env;

    if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4)!=JNI_OK){
        return JNI_ERR;
    }
    if(registerNative(env)!=JNI_OK){
        return JNI_ERR;
    }
    return JNI_VERSION_1_4;
}

到了这里

先是获取类,然后用获取的类进行RegisterNatives的调用

RegisterNatives会遍历nativeMethod里面的方法集合

jint registerNative(JNIEnv* env){
    //获取类
    jclass clazz = (*env)->FindClass(env, "com/example/oilcalculater/MainActivity");
    //注册
    if((*env)->RegisterNatives(env, clazz, nativeMethod,sizeof(nativeMethod)/sizeof(nativeMethod[0]))!= JNI_OK){
        return JNI_ERR;
    }
    return JNI_OK;
}

以下就是方法集合 涉及到一个结构体

可以看到第一个是java层的名字,第二个是关于参数和返回 这里都是float所以是F,第三个函数指针,指向native层实现的jni函数

JNINativeMethod nativeMethod[]={
    {"FullCal", "(FF)F",(void*)FullCalc},
    {"HundredCal", "(FF)F",(void*)HundredCalc}
};

image-20240222172249782

ok

那后面一部就是FullCalc和HundredCalc的编写了,就是返回相乘。

jfloat FullCalc(JNIEnv* env,jobject obj,jfloat a,jfloat b){
    return a*b;
}

jfloat HundredCalc(JNIEnv* env,jobject obj,jfloat a,jfloat b){
    return a*b;
}

到这里,native的编写算是结束了,要ndk给build一下

image-20240222173942318

然后会有个坑,在于Android.mk和Application编译出来的玩意要和目标平台相符

在gradle

image-20240222174104999

以及再次建立jniLibs

image-20240222174227299

详细在此https://fq68l8ofjqw.feishu.cn/wiki/UIcVwvCXaixtBSkSVlHcu7e5nVd?from=from_copylink

最后的一布 system.loadlibrary

image-20240222174256335

就完成啦!!!

动态注册

一篇我觉着写的不错的blog

https://www.cnblogs.com/qixingchao/p/11911787.html

拿着个apk我画了张图

image-20240222175522757

上面其实不完全是调用关系,算是一个开发apk时候的思路把

下面更好一下

image-20240222175731447

流程1:是指执行 System.loadLibray函数;
流程2:是指底层默认调用so中的JNI_OnLoad函数;
流程3:是指开发人员在JNI_OnLoad中写的注册方法,例如: (*env)->RegisterNatives(env,.....)
流程4:需要重点讲解一下:
├── 在Android中,不管是Java函数还是Java Native函数,它在虚拟机中对应的都是一个Method*对象
├── 如果是Java Native函数,那么Method*对象的nativeFunc会指向一个bridge函数dvmCallJNIMethod
├── 当调用Java Native函数时,就会执行该bridge函数,bridge函数的作用是调用该Java Native方法对应的
JNI方法,即: method.insns

image-20240222175617212

image-20240222175642240

流程1Java代码中调用Java Native函数;
流程2:获得Method*对象,默认为该函数的Method*设置nativeFuncdvmResolveNativeMethod);
流程3dvmResolveNativeMethod函数中按照特定名称查找对应的C方法;
流程4:如果找到了对应的C方法,重新为该方法设置Method*属性

如果是动态注册的Java native函数,System.loadLibray时就已经设置好了Java native函数与C函数的对应关系,当Java代码中调用Java native方法时,直接执行dvmCallJNIMethod桥函数即可(该函数中执行C函数)

后面对于整个动态注册方面还会更加深入学习。

有错误欢迎师傅们更正。