Android NDK 作用主要用来编译本地库(由C,C++文件编译后运行于目标CPU的库)给Android java 调用,同时,可以用来打包编译动态库和静态库,但不能将本地库和java文件打包成APK(这是别的范畴,后面有简单实现这功能的方法). 本例子主要介绍如何使用NDK编译出动态库(.so)和静态库(.a),并且在编译它们的时候又调用其他的动态库或静态库,并且将其他的库打包成最终的一个库文件.so给java调用. 最后简单说明其他实现编译so文件的方法,打包so到apk里的方法. NDK内部变量说明请参考<【eoe特刊】第七期:NDK>.pdf 文件. 本例子是在ubuntu9.04上使用android-ndk-1.6_r1-linux-x86和android-sdk-linux_x86_16.
1. 安装. a) Tar –jxvf android-ndk-1.6_r1-linux-x86.tar.bz2 得到android-ndk-1.6_r1文件夹 b) 将android-ndk-1.6_r1文件夹放到你要的位置,进入android-ndk-1.6_r1文件夹,此时的pwd 就是NDK的根目录,我定义为$NDK_ROOT, c) 在$NDK_ROOT 执行命令 ./build/host-setup.sh ,能成功安装DNK环境.(如果有问题,请按提示添加系统缺失文件) 2. 编译一个动态库和一个静态库. a) 在$NDK_ROOT 目录下进入apps文件夹,创建jni文件夹和Application.mk文件,其内容如下 APP_PROJECT_PATH := $(call my-dir) APP_MODULES := testapi testapi 是生产模块的主要名字, ( 此版本系统默认编译$ APP_PROJECT_PATH 下jni文件夹下的c,c++文件) b) 进入jni文件夹,创建add.c,add.h,Android.mk内容分别如下: i. add.h: #ifndef ADD_H #define ADD_H extern int add(int x, int y); #endif /* ADD_H */ ii. add.c #include "add.h" int add(int x, int y){ return x + y; } iii. Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := testapi LOCAL_SRC_FILES := add.c LOCAL_PRELINK_MODULE := false include $(BUILD_SHARED_LIBRARY)
iv. 回到$NDK_ROOT目录,命令行输入 make APP=api 编译成功,在$NDK_ROOT/apps/api/libs/armeabi得到动态库 libtestapi.so, 3147Btye v. 将Android.mk 最后一样修改成 include $(BUILD_STATIC_LIBRARY) 在$NDK_ROOT目录,命令行输入 make APP=api 编译成功,在$NDK_ROOT/out/apps/api/得到静态库 libtestapi.a, 2572Btye 3. 利用Eclipse创建android 的java程序通过JNI调用动态库libapp.so,该so又调用步骤2.生产的库文件libtestapi.*. Android APK据我了解JNI只能调用动态库,不能把静态库添加到APK里面. a) 用eclipse在任意位置,创建工程apktest,创建test.jni包里jnitest的类,jnitest.java内容如下 package test.jni; public class jnitest { public native int appadd(int x, int y); } b) 然后进入工程的src目录制作JNI头文件 i. javac test/jni/jnitest.java javah test.jni.jnitest 产生得到test_jni_jnitest.h头文件 c) 在修改jnitest.java成如下形式 package test.jni; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class jnitest extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv = new TextView(this); int x = 77; int y = 88; System.loadLibrary("apptest"); int z = appadd(x, y); tv.setText( x + "+" + y + "=" + z ); setContentView(tv); } public native int appadd(int x, int y); } 其中主要参考了apps\two-libs下,因为这样简单让我们看起来容易理解. 没有语法错误后Eclipse在其工程的bin文件夹下自动产生apktest.apk 13.2k 此时,这模拟器运行apk会报错,因为动态链接库libapptest.so没有在apk里,也没在/system/lib d) 回到$NDK_ROOT目录下,进入apps目录执行一下命令 ln -s /home/clay/workspace/apktest/ app 其中/home/clay/workspace/apktest/是eclipse创建工程的位置, e) 因为ln –s的关系,app目录几乎等同/home/clay/workspace/apktest/,我用app目录代表/home/clay/workspace/apktest/目录.方便理解 f) 在app目录下创建jni文件夹和Application.mk文件,其内容如下 APP_PROJECT_PATH := $(call my-dir) APP_MODULES := apptest 其中apptest 要和java里System.loadLibrary("apptest");一致 g) 在上一步的jni文件夹里创建appcall.c 和Android.mk文件,内容如下 i. Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := apptest LOCAL_SRC_FILES := callapp.c LOCAL_PRELINK_MODULE := false include $(BUILD_SHARED_LIBRARY) ii. appcall.c 根据test_jni_jnitest.h #include <jni.h> JNIEXPORT jint JNICALL Java_test_jni_jnitest_appadd (JNIEnv *env, jobject obj, jint x, jint y){ return x + y; } 为检查JNI,我们先直接在libapptest.so实现,待验证后再修改call别的库文件. h) 回到$NDK_ROOT目录,执行make APP=app 得到 $NDK_ROOT /apps/app/libs/armeabi/libapptest.so 2146Byte 如果这时候,用 adb push /apps/app/libs/armeabi/libapptest.so /system/lib,然后从模拟菜单里运行刚才apk程序(名字也叫apk,建工程时候定下来的),此时可以运行,而且显示”77+88=165” i) 此时回到Eclipse 从菜单选择 project->clean,然后eclipse会自动(如果没设auto需手动)重新编译该工程,这时候你会发现工程里libs/armeabi/libapptest.so出现,而且,bin目录下的apk变成了14.7k,用zip或rar(windows下)你会发现你的apk文件里比之前多了lib文件夹,其子文件夹armeabi里有libapptest.so文件.用adb shell进入到虚拟机的/system/lib目录,删除之前libapptest.so,可以重启模拟器. 证明系统没有libapptest.so.后,运行后边编译的apk文件,这时候apk能正确运行,没有报错,而/system/lib目录下仍是没有libapptest.so,这说明:我们这时候的apk把libapptest.so打包进去,而且系统能解出libapptest.so并加载. 但是,这只是eclipse 通过ADT完成把so打包到apk里的.如果用源代码开发包mm命令方式产生的apk(不是NDK)似乎不行(之后再讨论,或许makefile没定义好). NDK绝对没有做这打包的动作. 4. 利用NDK 打印调试 a) 在3.基础上,修改android.mk连接liblog.so成为以下 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := apptest LOCAL_SRC_FILES := callapp.c LOCAL_LDLIBS := -llog LOCAL_PRELINK_MODULE := false include $(BUILD_SHARED_LIBRARY) b) 然后修改callapp.c,添加打印 #include <jni.h> #include <android/log.h> #define LOGD(...) __android_log_print(ANDROID_LOG_INFO, "Logtest", __VA_ARGS__); JNIEXPORT jint JNICALL Java_test_jni_jnitest_appadd (JNIEnv *env, jobject obj, jint x, jint y){ LOGD("TEST %d+%d=%d",x,y,x+y); __android_log_print(ANDROID_LOG_INFO, "Logtest","================"); return x + y; } c) 在$NDK_ROOT 下输入 make APP=app,重新编译动态库,eclipse clean rebuilt,运行,在eclipse的logcat 可以看到以下: I/Logtest ( 375): TEST 77+88=165 I/Logtest ( 375): ================ //注意,如果不用NDK,只有SDK开发,可以直接#include <utils/Log.h>后用LOGD(),类似c的printf,,另外,此时即使你include了<stdio.h>,c的printf仍是没有打印输出的 5. 为动态库libapptest.so链接别的静态库libXX.a a) 修改前,libapptest.so 为2829Byte,libtestapi.a 为2572Byte. b) 把api工程的add.h复制到$NDK_ROOT/apps/app/jni/下,修改callapp.c如下: #include <jni.h> #include <android/log.h> #include "add.h" #define LOGD(...) __android_log_print(ANDROID_LOG_INFO, "Logtest", __VA_ARGS__); JNIEXPORT jint JNICALL Java_test_jni_jnitest_appadd (JNIEnv *env, jobject obj, jint x, jint y){ LOGD("TEST %d+%d=%d",x,y,x+y); __android_log_print(ANDROID_LOG_INFO, "Logtest","================"); return add(x,y); } c) 修改$NDK_ROOT/apps/app/jni/Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := apptest LOCAL_SRC_FILES := callapp.c LOCAL_STATIC_LIBRARIES := libtestapi LOCAL_LDLIBS := -llog LOCAL_PRELINK_MODULE := false include $(BUILD_SHARED_LIBRARY) d) 把$NDK_ROOT/out/api/libtestapi.a复制到$NDK_ROOT/out/apps/app/,然后make APP=app,编译成功.$NDK_ROOT/apps/app/libs/armeabi/libapptest.so 3012Byte 如果把a文件放到jni文件里编译,会提示找不到No rule to make target `out/apps/app//libtestapi.a,我认为,LOCAL_STATIC_LIBRARIES, LOCAL_SHARED_LIBRARIES默认也是要自己编译出来 e) Eclipse 把工程clean rebuilt,run, 显示和打印都是正确的. 表是libtestapi.a的确是编进到libapptest.so文件里了 6. 为动态库libapptest.so连接别的动态库libXX.so a) 第4修改后,libapptest.so 为2829Byte,libtestapi.so 为3147Byte.(之前的记录) b) 维持第5修改的c文件, c) 修改$NDK_ROOT/apps/app/jni/Android.mk为 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := apptest LOCAL_SRC_FILES := callapp.c LOCAL_SHARED_LIBRARIES := libtestapi LOCAL_LDLIBS := -llog LOCAL_PRELINK_MODULE := false include $(BUILD_SHARED_LIBRARY) d) 同理,在$NDK_ROOT, cp out/apps/api/libtestapi.so out/apps/app/,make APP=app –B可以通过, 但是此时,eclipse clean后rebuilt,run 出现错误.因为动态库在编译的时候只做连接,并不包含到最终目标,这和静态库不一样.同时,你也会发现$NDK_ROOT/apps/app/libs/armeabi/下只有libapptest.so,没有libtestapi.so.请注意,这时候编译得到到libapptest.so 是2913Byte,和之前的不调用add()函数几乎差不多. e) 这时候,我们把libtestapi.so 复制到$NDK_ROOT/apps/app/libs/armeabi/,eclipse clean&rebuilt,run,结果仍是出错.虽然bin目录下的apktest.apk 的包libs/armeabi/里面有libtestapi.so和libapptest.so f) 如果这时候,把libtestapi.so 复制到$NDK_ROOT/apps/app/asset,eclipse clean&rebuilt,run,结果仍是出错.虽然bin目录下的apktest.apk包的 g) 即使我在jnitest.java里,添加了 System.loadLibrary("apptest"); System.loadLibrary("testapi");重复e和f,仍旧失败 h) 如果这时候,我们用adb remount(让模拟器系统可以改写),再adb push out/apps/app/libtestapi.so /system/lib,之后,运行eclipse修改的apk,发现可以正常正确的运行. 由此,我认为NDK能编译,但是,不能把so打包,同时也不能把编译中间的so文件(libtestapi.so)打包,但是NKD 却能以某种方式(我不知道)告诉APK打包的程序,所以eclipse 能把ibs/armeabi/下libapptest.so和libtestapi.so打包,但是系统跑起来却不能自动加载(能加载libapptest.so,不能加载libtestapi.so) 7. 为动态库libapptest.so连接别的动态库libXX.so,方法二. a) 第4修改后,libapptest.so 为2829Byte,libtestapi.so 为3147Byte.(之前的记录) b) 维持第5修改的c文件, c) 用adb shell 进入到/system/lib里删除第6加进去的libtestapi.so d) 修改$NDK_ROOT/apps/app/jni/Android.mk为 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := apptest LOCAL_SRC_FILES := callapp.c LOCAL_LDLIBS := -ldl -llog LOCAL_PRELINK_MODULE := false #LOCAL_ALLOW_UNDEFINED_SYMBOLS = true include $(BUILD_SHARED_LIBRARY) e) 这时候make APP=app,提示找add的原型,这时候 ,我们把LOCAL_ALLOW_UNDEFINED_SYMBOLS = true打开,这时候,能编译通过得到2.82k的libapptest.so,但是却出现正确的,,其实是刚好有一个有个lib 的函数是add,如果我们把我们add.c和add.h里的add函数改成,add22,重新编译api和app,运行就出错了.但是,这时候需要我们手工把libtestapi.so放到/system/lib里,仍旧不能运行,所以怀疑,该flag 是从全部的so里搜索(改成add22后,重复之前所以步骤都是可以得到按之前的说明的结果) f) 如果这时候我们只打开把Android.mk 修改成如下 LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := apptest LOCAL_SRC_FILES := callapp.c LOCAL_LDLIBS := -ltestapi -ldl -llog LOCAL_PRELINK_MODULE := false include $(BUILD_SHARED_LIBRARY) LOCAL_LDLIBS := -ltestapi -ldl –llog//有时候可以分成几行来写,但分开写,有时候编译不通过.(I don’t know why) 这是仿照我们之前添加log打印的方式一样,把我们的动态库当成NDK的库,所以,我把libtestapi.so放到NDK, cp apps/api/libs/armeabi/libtestapi.so build/platforms/android-4/arch-arm/usr/lib/, 在这之前,我们make APP=app -B 提示找不到libtestapi.so,把libtestapi.so放到NDK的lib后,编译可以通过.如果没通过,需要把libtestapi.so的mode变成777。 这时候,eclipse重新编译工程运行,提示错误,但是,我们用adb 把libtestapi.so push 弄到/system/lib后,重新运行,就ok了
关于NDK 的到此差不多告一段落. 由于android 的makefile的一大堆变量 刚开始就把人吓坏了.至今也没理解多少,这一切都是不断在网上查,自己摸索,不断看他的列子和其他人的建议,尝试的结果,其中反反复复好多次. ====================================================================== 其实通过JNI的方法来实现还有两种方式,这些都是我同事rock帮忙弄的,借花献佛,简单介绍一下. 1. linux 命令方式, a) 用gcc xxx.c -shared -o libXXX.so,生成 动态链接库. b) gcc xxx2.c -L . -l XXX -o APP,把xxx2c文件链接动态库libXXX.so,形成APP 可执行文件//注意,这个文件时在linux下运行的,是host c) 因为我的目标板(target board)是arm核心的,所以使用/usr/local/arm/4.3.2/bin/ 交叉编译器,模仿以上两步(自己写makefile),注意头文件时arm 的头文件就可以了,这样so和APP 可以在arm板上run了,可以使用gdb等工具调试. 2. Android原代码SDK 开发.需要有完整的code,并且会找到系统的编译器和头文件,把编译出来的中间so文件放到特定位置.详细参考<Android平台怎样使用第三方动态库> So文件放的位置主要根据编译时候提示在哪里找不到,就放到哪里(因为android 默认要完全有原代码开发,所以它默认我们的apk用的so需要有原文件,vendor下的例外) 3. 另外,很幸运,我们目标是ARM核心,有以上两步和NDK得到so文件几乎可以混用.SDK开发得到的so文件和NDK得到的so文件,分别push我目标板子和模拟器,都可以跑起来.而且三种方法得到的so,apk都可以在我的ARM目标板可以跑起来.并且,NDK得到的libapptest.so可以调用已经push 到/system/lib里 arm编译器编译出来的libtestapi.so文件. 但是,arm编译器编出来的libtestapi.so 不能放到NDK或SDK 里混合来编译出新的libapptest.so,提示” libthunder_api.so has EABI version 0, but target out/target/product/generic/obj/SHARED_LIBRARIES/libthunder_app_intermediates/LINKED/libthunder_app.so has EABI version 4”,也就是说,NDK和SDK里会检测版本.幸运的是我用的NDK 和SDK都是1.6的,而arm工具链编出来没有版本(EABI version 0) 另外,才傻傻发现,SDK里已经有NDK了…
另外,如果我们用SDK 开发,我们使用mm 得到的APK里是没有打包so文件的,这时候测试,只能手工把so文件push到系统里.而且,你会发现,如打包了so文件的apk文件用adb install 以后,在/system/lib目录,是找不到这so文件的.我曾经在/data/app或/data/system目录里见过,但是现在仍是没发现,不知道为什么?如果有谁知道请分享一下. 虽然mm命令方式生成的apk里没so文件,但是整个android 系统built通过以后,建成img文件的时候,so文件是会被放到/system/lib目录下的,例如lib3G等等. 我在网上查到,ant的开发方式可以把so打包到APK里,但是SDK开发用mm目前我没有试出来,曾经看过别人写的android.mk 里把so当成source文件去编译APK,但是我用NDK的时候却不成功,但是根据linux c的感觉,把so当成source(或中间文件) 一起编译打包理论是可以的.. ====================================================================== 这些事熬了半个通宵写了一部分边印证得出来的,平时上班的时间也尝试了很久很久,所以如此艰难弄了这说不定bug很多的半成品,虚荣一下,留个名. |
|
来自: techres > 《androidNative》