關於Android開發中JNI/NDK使用的一點總結

咳咳,作爲一名android愛好者(其實是爲了錢錢),之前一直在使用Android Sdk進行開發,同時也一直知道有個ndk的開發方式,知道全名是native development kit,基原生開發工具集,模糊的知道應該是和c/c++開發有關係的,然後就沒有深入一點的瞭解了。

目前階段想系統性的收一下自己的android技能,整理成一個比較系統的知識體系,於是乎ndk就成了一個繞不過去的技術,這篇文章就簡單的說說android開發中jni/ndk技術的使用。

1、關於JNI技術的說明

JNI全稱Java Native Interface,其實也不是啥新鮮的東西,簡單的說也就是java中調用c/c++類庫的技術,也可以理解爲”java + c”的一門技術,至於爲什麼要使用”java + c”的技術,難道c能實現的功能還有java做不到的麼,其實不也全不是這樣,在實際項目中使用JNI的主要目的有

a、增強安全性,防止被反編譯後被不法分子分析應用的邏輯(增強安全性是相對的);

b、java不能直接與硬件交互的時候,需要c/c++上場幹活;

c、有些功能可以通過c/c++打包後,供多種平臺和語言調用,也就提高了程序的可移植性;

而JNI的開發流程大致分爲六步

第一步: 編寫聲明瞭 native 方法的 Java 類
第二步: 將 Java 源代碼編譯成 class 字節碼文件
第三步: 用 javah -jni 命令生成.h頭文件(javah 是 jdk 自帶的一個命令,-jni 參數表示將 class 中用native 聲明的函數生成 JNI 規則的函數)
第四步: 用本地代碼實現.h頭文件中的函數
第五步: 將本地代碼編譯成動態庫(Windows:\*.dll,linux/unix:\*.so,mac os x:\*.jnilib)
第六步: 拷貝動態庫至 java.library.path 本地庫搜索目錄下,並運行 Java 程序

關於這六步的具體實現,JNI 開發流程這篇文章中有很好的講解,這裏就不多說了。

2、關於NDK技術的說明

第一小節講了JNI,那麼這個NDK又是什麼東西呢,其實這個NDK不是什麼新鮮技術,它只是一個Native開發的工具集,讓開發者更方便的進行JNI開發而已。

JNI的不方便之處可以用下面的話來表述

Android SDK 文檔裏,找不到任何 JNI 方面的幫助。即使第三方應用開發者使用 JNI 完成了自己的 C 動態鏈接庫(so)開發,但是 so 如何和應用程序一起打包成 apk 併發布?這裏面也存在技術障礙。比如程序更加複雜,兼容性難以保障,無法訪問Framework API,Debug 難度更大等。

而NDK的出現就是爲了解決上面描述的一些問題的,NDK作爲工具集,有以下功能

NDK 集成了交叉編譯器,並提供了相應的 mk 文件隔離 CPU、平臺、ABI 等差異,開發人員只需要簡單修改 mk 文件(指出“哪些文件需要編譯”、“編譯特性要求”等),就可以創建出 so。

NDK 可以自動地將 so 和 Java 應用一起打包,極大地減輕了開發人員的打包工作。

NDK 提供了一份穩定、功能有限的 API 頭文件聲明

根據第一小節我們知道,JNI開發有六個步驟,而當前的NDK版本提供便利表現在第五和第六個步驟,第五步的便利表現在ndk提供了ndk-build命令可以直接將c/c++代碼編譯爲so包,而第六步則體現在可通過gradle中的ndk配置來直接加載對應的so包(這裏不懂彆着急,後面會講ndk的使用,到時便可以體會)。

3、關於ABI的說明

可能有童鞋到這裏不明白了,好好的講JNI/NDK,怎麼又講到ABI了,這是個什麼玩意。這裏就來說說爲什麼要說這個ABI,ABI的全稱是Application Binary Interface,知道全稱其實也不知道是幹嘛的吧。

ABI(應用程序二進制接口)定義了二進制文件(尤其是.so文件)如何運行在相應的系統平臺上,從使用的指令集,內存對齊到可用的系統函數庫。而Andriod作爲一種操作系統,它得去適應不同的CPU架構,早期的Android系統幾乎只支持ARMv5的CPU架構,現在Android已經支持7中CPU架構了,分別是ARMv5,ARMv7 (從2010年起),x86 (從2011年起),MIPS (從2012年起),ARMv8,MIPS64和x86_64 (從2014年起),每一種CPU架構都關聯着一個相應的ABI。學會彙編的同學都知道CPU不一樣,對應的指令集就不一樣,這裏的ABI就可以理解爲指令集。那麼不同的CPU架構自然就需要不同的ABI,所以與7種CPU架構相對應的就有其中ABI,分別是armeabi,armeabi-v7a,x86,mips,arm64- v8a,mips64,x86_64。

還是有同學說了,你說這麼多還是沒有說清楚爲啥講JNI/SDK要講這個ABI啊。童鞋們,我們編寫的so包最終是要打包到android手機上的也就是會執行在android cpu上,那麼必然需要不同的指令集,所以我們寫的c/c++代碼就得根據不同的ABI進行編譯,最後編譯出針對不同cpu的so包來,也就是我們寫的一份c/c++代碼,最多可以編譯出7個so包,然後一起打包到apk中,apk會根據當前的cpu架構去調用對應的so包。

這裏寫圖片描述

我想到這裏算是說清楚了吧。

4、NDK的使用

我爲了嚐鮮kotlin已經把開發工具升級到android studio 3.0版本了,但是感覺這個ndk開發和ide版本沒有太大關係。

實際上使用NDK開發JNI還是蠻簡單的,但是我還是走了不少彎路,剛開始便是依據NDK-JNI實戰教程(一) 在Android Studio運行第一個NDK程序這篇文章進行開發,但是怎麼都不成功。

後面換了個思路,改依據Android studio運行JNI程序以及生成.so文件(Windows下)這篇文章後成功。

這裏還是來說說吧。

4.1 基本配置

首先得配置好ndk,這裏的配置包括兩方面的,

一是在android stuido中配置好

project structure中的配置

這裏寫圖片描述

local.properties文件中的配置

這裏寫圖片描述

二是系統環境變量中的配置

配置NDK_HOME

在系統環境變量中添加NDK_HOME 然後值爲你的ndk路徑

配置Path

path環境變量中添加%NDK_HOME%\

配置好後,cmd中運行ndk-build,出來正常提示就表示配置成功了。

4.2 生成.h頭文件

在android 項目中添加NdkUtil.java文件

package com.yjing.ndk.utils;

public class NdkUtil {

    public native String helloFromJni();

    public native void callBackJavafromC();
}

然後編譯項目,到build\intermediates\classes\debug這個目錄下查看,是否成功編譯出NdkUitl.class文件,有的話表示編譯成功,然後繼續

cmd中cd到build\intermediates\classes\debug這個目錄下,然後執行

javah -jni com.yjing.ndk.utils.NdkUtil

或者

javah -classpath . -jni com.yjing.ndk.utils.NdkUtil

總之目的就是在debug目錄下生成一個針對NdkUtil.class文件的.h文件就是了。

4.3 jni文件夾中代碼完善

在main文件目錄下創建jni目錄

這裏寫圖片描述

將4.2中的頭文件,拷貝到jni目錄中,同時在jni目錄中創建hello.c文件,對該文件做實現

#include "com_yjing_ndk_utils_NdkUtil.h"

/*
 * Class:     com_yjing_ndk_utils_NdkUtil
 * Method:    helloFromJni
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_yjing_ndk_utils_NdkUtil_helloFromJni
  (JNIEnv *env, jobject obj){

    char* c ="hello from c";
    return (*env)->NewStringUTF(env, c);

  }


/*
 * Class:     com_yjing_ndk_utils_NdkUtil
 * Method:    callBackJavafromC
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_yjing_ndk_utils_NdkUtil_callBackJavafromC
  (JNIEnv *env, jobject obj){

    // 1.通過反射找到類
    jclass clazz = (*env) -> FindClass(env, "com/wangjin/hellondkdemo/MainActivity");
    // 2. 找到方法ID
    jmethodID methodId = (*env) -> GetMethodID(env, clazz, "logout", "()V");
    // 3.調用方法,obj就是調用的類實例,所以不用再次創建了
    (*env)->CallVoidMethod(env, obj, methodId);

  }

然後在jni目錄中創建Android.mk以及Application.mk兩個文件,內容分別爲

Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c

include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_ABI := all

這裏的Android.mk類似linux中的makefile文件用來指定c/c++文件之間的依賴關係,而Application.mk文件用來指定ABI類型,這裏指定我們的so包要適配所有的CPU架構。

Android.mk中的LOCAL_MODULE對應的是build.gradle文件中的ndk配置

這裏寫圖片描述

到這裏jni目錄下的東西就算是講完了

4.4 build.gradle文件中的配置

實際上4.3小節都提到了build.gradle文件的配置,但是不全面,這裏單獨用一小節來說明

build.gradle中首先要如4.3小節中的那樣配置ndk模塊,其次還需要指定so包編譯後的存放路徑

sourceSets{
    main{
        jni.srcDirs = ['libs']
    }
}

這裏指定so報的路徑爲main目錄下的libs目錄

4.5 使用ndk-build命令

我們配置好後,便到了生成so包的時候了,生成so包很簡單,只需要藉助ndk-build工具便可。

cmd 中cd到項目的main目錄路徑,然後執行ndk-build便能在main/libs目錄下生成各種CPU架構的so包了。

這裏寫圖片描述

4.6 項目中使用so包

編輯NdkUtil.java文件,添加加載so包的代碼後,NdkUtil.java內容如下

package com.yjing.ndk.utils;

public class NdkUtil {

    static{
        System.loadLibrary("hello");
    }

    public native String helloFromJni();

    public native void callBackJavafromC();
}

然後在代碼中條用helloFromJni以及callBackJavafromC方法便能夠成功調用到native代碼中去。

這裏給出測試的MainActivity代碼吧

package com.yjing.ndk;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.yjing.ndk.utils.NdkUtil;

public class MainActivity extends AppCompatActivity {
    private Button btnNdk;
    private TextView tvNdk;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.initViews();
        this.initEvents();
    }

    private void initViews(){
        this.btnNdk = (Button)findViewById(R.id.btnNdk);
        this.tvNdk = (TextView)findViewById(R.id.tvNdk);
    }

    private void initEvents(){
        this.btnNdk.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                NdkUtil ndkUtil = new NdkUtil();
                ndkUtil.callBackJavafromC();
            }
        });
    }

    public void logout() {
        System.out.println("hahahahahahahahahah===");
    }
}

運行後提示錯誤

couldn't find "libhello.so

只需要修改build.gradle文件配置如下,便能解決問題

sourceSets{
    main{
        jni.srcDirs = ['libs']
        jniLibs.srcDirs 'src/main/libs'
    }
}

從代碼中可以看出java能夠調用native方法,而native也能夠調用java方法。

到這裏關於Android開發中的JNI/NDK基本總結就算完成了。

5、參考文獻

1、JNI/NDK 開發指南

2、Android studio運行JNI程序以及生成.so文件(Windows下)

3、Android 中arm64-v8a、armeabi-v7a、armeabi、x86簡介~

4、Android Studio2.3NDK的簡單配置及快速開發

5、Getting Started with the NDK

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章