Android NDK开发:打包so库及jar包供他人使用

Android的NDK开发相信各位已经精通各种姿势了。不过基本上都是那种native代码和java代码都在同一个工程中,因为应用从头到脚都是我们自己的,也不需要分离。但有时候可能需要我们自己把某些库打包起来供别人使用,或者使用别人提供给我们的库。本篇文章及下篇文章就讲讲so库如何打包。

一、目标及方式

这篇文章会讲打包so库,这种方式是基于jni层的,需要我们同时提供接口的jar包来配合使用,适用于对方从java层调用我们的库。因为jni中的函数名是有特殊要求的,它会指定jni的java接口的路径,如果不提供jar包,那么使用者就要按照我们在jni头文件中声明的函数名来建立java文件,这将是非常痛苦的。

二、新建调用方工程

按照正常流程新建工程即可,众所周知,新建一个工程,它会自己建一个名为app的module。它是作为我们的调用方,因此不需要什么特殊的操作。
为了避免剩下的操作报错,调用方工程先到此为止,在准备好了被调用方(也就是jar包和so库)后,再继续完善。

三、新建库module

我们仍然在这个工程中,新建一个module(AndroidStudio的组织结构是一个project可以包含多个module,每个module都可以独立编译为一个apk)。这个module就是我们的库模块。

File > New > New module > Android Library
填好相关信息等就可以finish了。目前我在用的是AndroidStudio3.4.1,在新建工程的时候已经找不到添加c++ support的选项了,因此一会儿也免不了要自己去改build.gradle文件来添加c++支持。

新建完后的工程结构如图


在这里插入图片描述

可以看到我们sdk模块和app是同级的。

四、为库模块添加c++支持

首先要做一些准备工作。为了顺应时代潮流,此处使用cmake构建。

4.1 准备CMakeLists文件

如果你会自己写的话最好,不会的话,可以新建一个支持c++的工程,在我现在的AndroidStudio3.4.1版本上,在Chose Project这一步时,选择Native C++就会有了,更早的版本更加简单,会有很显眼的选项供你选择。然后复制这个工程的CMakeLists文件过来就好了。或者你可以参考以前有NDK的工程,我这里选择最后一种。

OK无论哪种方式,得到了CMakeLists文件。

cmake_minimum_required(VERSION 3.4.1)

# 这一句用来导出编译生成的so库
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})

include_directories(src/main/cpp)

file(GLOB CPP_FILES "src/main/cpp/*.cpp")

add_library(
    native-lib
    SHARED
    ${CPP_FILES}
)

通常操作,我就不解释CMakeLists里都是啥意思了。注意我注释的那条语句,它会负责把编译出的so库导出到我们指定的位置。这里的位置就是sdk/jniLibs/{ANDROID_ABI}。
然后将这个文件放在库模块的根目录下(虽然我这库模块的名字是sdk,但是并不是AndroidSDK,切勿搞混,需要AndroidSDK的时候我会特别指明的)。放在别的地方也行,因为在gradle文件里这个位置是自己定的。文件结构如下:


在这里插入图片描述

这个文件结构和你在文件管理器中看到的是一样的。如果你看不到这个界面,点击图中绿色线的下拉菜单选择Project Files即可(一般都是在Android结构下)。

4.2 修改sdk的gradle脚本

此处修改的是sdk的build.gradle文件(不是AndroidSDK!!!)。在defaultConfig节点下指定要编译的cpu架构,在android节点下指定CMakeLists的位置。最后文件如下:

apply plugin: 'com.android.library'

android {
    compileSdkVersion 28

    defaultConfig {
        minSdkVersion 23
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        //这里指定cpu架构
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    //这里指定CMakeLists的位置,默认根目录是从sdk目录开始的。
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:28.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

4.3 编写c++及java代码

我们从库里返回一个字符串"Hello from NDK"。
注意看我们的CMakeLists文件中指定的src位置,是src/main/cpp,那么去检查一下有无这个文件。没有的话就新建一个。然后在里面新建一个cpp文件(如果你是c的话,注意修改一下CMakeLists里相关的地方,因为这个目前仅包含了cpp文件)。点击Build > Make module "sdk",等模块编译完成后,我们就可以看到项目中出现了cpp文件夹和里面的源代码,并且被AndroidStudio认定为源代码文件夹。如果看不到,再点击一下Build > Refresh linked C++ project,就可以看到了。

在这里插入图片描述

编写一个jni函数。假设我在我的包里要建立一个MyStringUtil.java文件,那么这个文件的完整路径就是com.zu.sdk.MyStringUtil
那么在这个c++文件中,我们这样写:

#include 
#include 
#include 

using namespace std;

extern "C"
JNIEXPORT jstring
JNICALL
Java_com_zu_sdk_MyStringUtil_getStringNDK(JNIEnv *env, jobject instance)
{
    string result = "Hello from NDK";
    return env->NewStringUTF(result.c_str());
}

extern "C"是个比较迷惑的点,因为我们现在使用的是cpp文件,使用c++编译器编译后,函数名会改变,但是c的不会,因此我们要在作为jni接口的函数上加上这句。否则jni会找不到接口。
然后按照我们之前说的,在包下建立名为MyStringUtil.java的文件。之后的文件结构如图:

在这里插入图片描述

在java文件中加载ndk库,然后声明函数,要和c++文件中的对应起来。

package com.zu.sdk;

public class MyStringUtil {

    public static String ndkString()
    {
        return getStringNDK();
    }

    private static native String getStringNDK();

    static{
        System.loadLibrary("native-lib");
    }
}

注意如果你的java的函数和jni中的函数能对应起来的话,AndroidStudio会有相应提示:


在这里插入图片描述
在这里插入图片描述

标红的不要管它,因为现在我还没有build。而且现在AndroidStudio越来越难用,很多bug出现。

4.4 测试库

至此我们已经把库的部分完成了,但是还是要测试一下能不能正常运行。这里我们选择直接把库的module作为app module的项目依赖,这样一来,每次我们运行app module时,都是以sdk module作为依赖实时编译的,不必我们每次都要导出so库和jar包。方便修改。
在app module下已经有一个初始化的MainActivity。直接用它显示NDK过来的字符串。
接下来要将sdk作为app的依赖。在app上点击右键,点击Open module settings,点开Dependencies,选中app。

在这里插入图片描述

点击红圈里的加号,选择Module Dependency,会出来目前这个工程里的所有module,选择sdk这个module。


在这里插入图片描述

点击OK之后,再点击OK,这个时候依赖就添加好了。那么到底什么地方发生了改变呢?其实就是在app的build.gradle文件中的dependencies节点下加了一句:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation project(path: ':sdk')//就是这句
}

所以不想这么麻烦你也可以直接把这句加进去在sync一下gradle就可以了。

然后在app的MainActivity里就可以调用sdk里MyStringUtils里的ndkString方法获取字符串。

package com.zu.ndktest

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
//现在可以导入sdk里的内容了
import com.zu.sdk.MyStringUtil.ndkString

class MainActivity : AppCompatActivity() {

    private lateinit var tvNDK: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        tvNDK = findViewById(R.id.tv_ndk)
        //这句调用方法
        tvNDK.text = ndkString()
    }
}

app里的代码都是Kotlin。
然后运行。


在这里插入图片描述

nice,字符串没有错误,说明成功了。

五、打包so库

so库其实不用打包,因为我们已经在CMakeLists文件中指定了so库的输出路径。点击Build > Make module "sdk",然后去文件管理器里看一下,sdk目录下会生成一个jniLibs文件夹,里面包含了指定cpu架构的so文件。

在这里插入图片描述

六、打包jar包

通过上一步我们已经打包出了so,但是只有so是很难使用的,因为jni接口是指定包名的,使用者的包名基本不可能和我们的一样,因此提供一个jar包来调用so库。
打包jar包实际上是一个不怎么常见的操作,这里我们要在sdk的build.gradle中添加一个task,通过这个task来打包。
为sdk的build.gradle文件添加如下节点

task makeJar(type: Copy) {
    delete 'libs/sdk.jar' //删除已经存在的jar包
    from('build/intermediates/packaged-classes/release/')//从该目录下加载要打包的文件,注意这个目录,不同版本的AndroidStudio是不一样的,比如在3.0版本是build/intermediates/bundles/release/,要自己去查一下。
    into('libs/')//jar包的保存目录
    include('classes.jar')//设置过滤,只打包classes文件
    rename('classes.jar', 'sdk.jar')//重命名,mylibrary.jar 根据自己的需求设置
}
makeJar.dependsOn(build)

注意这个节点是一个顶级节点,它不属于其他任何节点。放一个完整的sdk的build.gradle,如果有c++配置或者其他的一些问题可以参考这个gradle。


apply plugin: 'com.android.library'

android {
    compileSdkVersion 28

    defaultConfig {
        minSdkVersion 23
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

}
//该task用来打包jar包
task makeJar(type: Copy) {
    delete 'libs/sdk.jar' //删除已经存在的jar包
    from('build/intermediates/packaged-classes/release/')//从该目录下加载要打包的文件,注意这个目录,不同版本的AndroidStudio是不一样的,比如在3.0版本是build/intermediates/bundles/release/,要自己去查一下。
    into('libs/')//jar包的保存目录
    include('classes.jar')//设置过滤,只打包classes文件
    rename('classes.jar', 'sdk.jar')//重命名,mylibrary.jar 根据自己的需求设置
}

makeJar.dependsOn(build)

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:28.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

添加完之后,sync gradle一下。然后,打开Android Studio的Terminal,输入gradlew makeJar,然后你就会看到一堆输出。

在这里插入图片描述

最后看到build success,然后去sdk/libs目录下查看是否生成了名为sdk.jar的文件。如果没有,应该就是你的classes.jar路径写错了。

在这里插入图片描述

七、如何使用

如今我们已经打包好了so和jar,接下来就试一下能否正常使用。回到app,这次我们要修改一下app的依赖和其他一些东西。
首先是修改依赖,原来是直接依赖了sdk这个module的,现在要依赖生成的jar包。

修改之前,要把生成的jar包和so库复制到app目录下,app/libs/sdk.jarapp/jniLibs/{abi}/native-lib.so是最后应该存放的路径。当然路径是随便定的,只要你能找得到并且能在gradle文件里正确的指出。

在这里插入图片描述
在这里插入图片描述

然后修改app的build.gradle文件。
在依赖里,注释掉依赖sdk的那行,添加对jar包的依赖:

//implementation project(path: ':sdk')
implementation files('libs/sdk.jar')

这种写法是依赖一个具体包的写法,如果某些情况下你必须得使用已经存在的jar包,就按照这种方式即可。
然后在android节点下添加如下节点:

sourceSets {
    main {
        jniLibs.srcDirs = ['jniLibs']
    }
}

这个则是指定了so库所在路径。

需要注意的是,jar包依赖和项目依赖不可共存,否则会发生包冲突,如果你要依赖module,那你就把jar包依赖和sourceSets节点注释掉。相反亦是。

到这就可以运行了。

在这里插入图片描述

这种方式开发起来极为方便,但是局限性比较大,因为终究还是要使用android环境,并且对方只能从java调用(当然你要是愿意写java>jni的调用那我也没意见),调试起来也很麻烦,因为最终你还是要放到Android里去验证结果。下一篇会讲一种更类似于桌面端的开发方式,除了仅在打包时需要使用到NDK,其他时候都和普通开发c/c++程序没有区别,而且可以在电脑环境下充分验证程序之后再去打包,也更好分工。

你可能感兴趣的:(Android NDK开发:打包so库及jar包供他人使用)