Android--在Android应用中愉快地写C/C++代码(转)
1 前言
一直想在android层面写c进程,然后java可以与c进程交互,以前在android源码中想玩就可以直接在init.rc中加上交叉编译好的c进程就可以了,而在ide中,也就是ndk编译后各种权限问题就有点不得而知了。花了几天时间研究实践,也终于实现了。再者这个也可以为后期做进程间通信和守护进程做准备。进程间通过一个中转daemon来处理分发,各个进程交互的接口也可以通过jni暴露给java层,而内部实现在c中也可以防止反编译,安全性上也有一定的保障。说了这么多,还是开始吧。
2 CMake JNI开发
Android studio逐渐用CMake来开发JNI层的c代码,也就是生成动态库so文件,不管是调试还是补全都是非常不错的。那就先来写一个jni吧,在新建工程的时候,有个选项( Include C++ support)勾选上就会生成对应的jni的支持,
2.1 build.gradle中加了如下的配置:
cppFlags支持c++11
externalNativeBuild {
            cmake {
                cppFlags "-std=c++11"
            }
        }
2.2 编译的配置文件CMakeLists.txt
 externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
看下CMakeLists.txt文件,之前有一篇关于Android Studio cmake编译ffmpeg的文章也做了讲解。
cmake_minimum_required(VERSION 3.4.) # Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK. add_library( # Sets the name of the library.
native-lib # Sets the library as a shared library.
SHARED # Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp
) # Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build. find_library( # Sets the name of the path variable.
log-lib # Specifies the name of the NDK library that
# you want CMake to locate.
log ) # Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries. target_link_libraries( # Specifies the target library.
native-lib # Links the target library to the log library
# included in the NDK.
${log-lib} )
其实看下也能理解,native-lib是库的名字,log 是打印需要依赖的android中的动态so库,src/main/cpp/native-lib.cpp是对应的源代码。
2.3 熟悉JNI代码
从CMakeLists.txt中我们可以看到代码路径在src/main/cpp文件夹下,看下代码如下:
#include <jni.h>
#include <string> extern "C"
JNIEXPORT jstring JNICALL
Java_com_jared_jnidaemon_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str());
}
比较简单,返回一个Hello from C++的字符串,我们再来看下java层怎么调用的。
调用其实很简单,首先是一个native的接口:
public native String stringFromJNI();
一个静态的加载so库的系统方法:
static {
        System.loadLibrary("native-lib");
    }
到这里,只要通过stringFromJNI()这个接口就可以调用c++层的代码获取对应的字符串了。是不是很简单,那就继续尝试下自己加一个方法吧。这里我们使用c++来开发吧。
2.4 动手写JNI代码
首先在java层定义下我们需要的接口:
public native int sumFromJNI(int a, int b);
然后通过”Show Intention Actions”快捷键来create对应的jni的方法
extern "C"
JNIEXPORT jint JNICALL
Java_com_jared_jnidaemon_MainActivity_sumFromJNI(JNIEnv *env, jobject instance, jint a, jint b) { // TODO }
然后我们就可以在这里返回 a + b的值就可以了。这里我们会新建一个c++文件,那样涉及的东西多点。cpp目录下新建Hello.cpp文件和Hello.h文件:
#ifndef JNIDAEMON_HELLO_H
#define JNIDAEMON_HELLO_H #include <string>
using namespace std; class Hello {
public:
Hello(); string getHello();
int sum(int a, int b);
}; #endif //JNIDAEMON_HELLO_H
新建一个Hello类,包含getHello方法和sum方法
#include <string>
#include "hello.h" using namespace std; Hello::Hello() {} string Hello::getHello() {
return "Hello eastmoon1117";
} int Hello::sum(int a, int b) {
return a + b;
}
实现Hello类的方法,比较简单。再看下native-lib.cpp:
extern "C"
JNIEXPORT jint JNICALL
Java_com_jared_jnidaemon_MainActivity_sumFromJNI(JNIEnv *env, jobject instance, jint a, jint b) { Hello *hello= new Hello();
return hello->sum(a, b);
}
这里返回了hello的sum方法。
都准备好了,那就把.cpp加入到CMake编译中吧
add_library( # Sets the name of the library.
native-lib # Sets the library as a shared library.
SHARED # Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp
src/main/cpp/hello.cpp
)
然后我们再activity中调用
Log.d(“MainActivity”, sumFromJNI(2, 6));就可以打印出8了。
3 NDK生成c可执行文件并执行
因为是c进程,可以说和主工程没有啥依赖,那就新建一个module来完成,这里新建Daemon模块。本来想着用cmake的,但是不知道怎么玩,就放弃了,直接用ndk编译吧还是。在新的Daemon模块中新建jni目录,在jni中新建daemon目录。
3.1 Makefile
首先在jni目录下建立两个文件,Android.mk和Application.mk 
先看下Android.mk文件:
include $(call all-subdir-makefiles)
意思就是包含素有子目录的makefile也就是所有子目录的Android.mk
再看下Application.mk文件:
APP_ABI := all
APP_PLATFORM := android-
APP_ABI是cpu相关的,比如arm的,mips的,x86的,这里就编译所有就好了。
APP_PLATFORM 是平台相关的
现在回到daemon文件,我们需要的东东,新建Android.mk,daemon.c和log.h文件。 
先看下Android.mk文件:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := daemon
LOCAL_SRC_FILES := daemon.c LOCAL_C_INCLUDES += \
$(LOCAL_PATH) \ LOCAL_LDLIBS := -lm -llog include $(BUILD_EXECUTABLE)
编译生成的文件名为daemon,源码是daemon.c,依赖m和log库,BUILD_EXECUTABLE表示生成可执行文件。
3.2 源码
再看下log.h文件:
#include <jni.h>
#include <android/log.h> #define TAG "Native" #define LOG_I(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOG_D(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOG_W(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
#define LOG_E(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define LOGI(tag, ...) __android_log_print(ANDROID_LOG_INFO, tag, __VA_ARGS__)
#define LOGD(tag, ...) __android_log_print(ANDROID_LOG_DEBUG, tag, __VA_ARGS__)
#define LOGW(tag, ...) __android_log_print(ANDROID_LOG_WARN, tag, __VA_ARGS__)
#define LOGE(tag, ...) __android_log_print(ANDROID_LOG_ERROR, tag, __VA_ARGS__)
这个就是对log的一个封装。
最后看下daemon.c文件:
#include <stdlib.h>
#include <stdio.h>
#include <signal.h> #include "log.h" #define LOG_TAG "Daemon" /* signal term handler */
static void sigterm_handler(int signo) {
LOGD(LOG_TAG, "handle signal: %d ", signo);
} int main(int argc, char *argv[]) { LOGI(LOG_TAG, "Copyright (c) 2018, eastmoon<chenjianneng1117@gmail.com>"); LOGI(LOG_TAG, "=========== daemon start ======="); /* add signal */
signal(SIGTERM, sigterm_handler); while () {
LOGI(LOG_TAG, "=========== daemon running ======");
sleep();
}
}
 主进程就是睡眠3s打印一条log。
3.3 build.gradle配置
建立一个ndk编译的task
task ndkBuild(type: Exec) {
    //def ndkDir = project.plugins.findPlugin('com.android.library').sdkHandler.getNdkFolder()
    def ndkDir = project.android.ndkDirectory
    commandLine "$ndkDir/ndk-build.cmd", '-C', 'src/main/jni',
            "NDK_OUT=$buildDir/ndk/obj",
            "NDK_APP_DST_DIR=$buildDir/ndk/libs/\$(TARGET_ARCH_ABI)"
}
因为最后需要把对应的c进程拷贝到asset中,所以需要加上
sourceSets {
        main {
            jni.srcDirs = []
            assets.srcDirs = ['src/main/assets']
        }
    }
然后新建一个copyExeFile任务:
task copyExeFile(type: Copy) {
    from fileTree(dir: file(buildDir.absolutePath + '/ndk/libs'), include: '**/*')
    into file('src/main/assets/')
}
再右上角找到Daemon–>Tasks–>other–>ndkBuild执行就可以编译出对应的c可执行文件,然后右上角找到Daemon–>Tasks–>other–>copyExeFile执行把需要的c可执行文件拷贝到对应的assets文件夹下。
3.4 执行java层执行c进程
准备好了c可执行文件后,下一步就是执行这个进程。由于android的权限问题,对应的app会安装到/data文件夹下,而/data文件夹又需要root后才能进去,一般手机是没有root掉的,所以我们就通过读取assets中的c文件,写到/data文件夹的对应的包中,然后再执行该文件,那么不需要root的情况下就可以运行我们的c进程了。
3.4.1 CommandForNative
在java层新建CommandForNative类:代码有点多,这里不贴了,可以参考github的代码。简要介绍下: 
installBinary方法,就是根据cpu型号找到对应的c文件,从assets中拷贝到指定的目录下,然后设置权限成为可执行权限。 
restartBinary方法:先杀死已有的进程,然后再起这个进程。
3.4.2 Daemon
然后看下Daemon类:
private static final String BIN_DIR_NAME = "bin";
private static final String BINARY_NAME = "daemon"; /**
* 执行可执行文件
* @param context
* @param args 可执行文件的参数
*/
public static void run(final Context context, final String args) {
new Thread(new Runnable() {
@Override
public void run() {
CommandForNative.installBinary(context, BIN_DIR_NAME, BINARY_NAME);
//CommandForNative.execBinary(context, BIN_DIR_NAME, BINARY_NAME, args);
CommandForNative.restartBinary(context, BIN_DIR_NAME, BINARY_NAME, args);
}
}).start();
}
这里daemon就是对应的可执行文件名字,bin是需要新建的/data/data文件夹下的对应app文件夹的目录,也即是daemon文件的安装目录。
3.5 执行效果
看下效果,执行
adb shell
执行看下进程
shell@jflte:/ $ ps | grep jared
ps | grep jared
u0_a620 ffffffff S com.jared.jnidaemon
u0_a620 ffffffff S /data/data/com.jared.jnidaemon/app_bin/daemon
看下打印的log信息:
shell@jflte:/ $ logcat -s Daemon
logcat -s Daemon
--------- beginning of system
--------- beginning of main
I/Daemon (): Copyright (c) , eastmoon<chenjianneng1117@gmail.com>
I/Daemon (): =========== daemon start =======
I/Daemon (): =========== daemon running ======
I/Daemon (): =========== daemon running ======
I/Daemon (): =========== daemon running ======
I/Daemon (): =========== daemon running ======
I/Daemon (): =========== daemon running ======
就是我们所需要的。当然这个只是简单地例子,后续我们就可以基于这个愉快地写c/c++代码。
github代码:https://github.com/eastmoon1117/StudyTestCase/tree/master/JNIDaemon
Android--在Android应用中愉快地写C/C++代码(转)的更多相关文章
- 在 Android Studio 2.2 中愉快地使用 C/C++
		转载请注明出处:http://blog.csdn.net/wl9739/article/details/52607010 注:官网上面的技术文章也在不断地汉化中,只是进度有点慢.在我翻译本篇文章的时候 ... 
- Android:如何从堆栈中还原ProGuard混淆后的代码
		本文翻译自Android: How To Decode ProGuard's Obfuscated Code From Stack Trace 本篇文章是写给那些在他们的应用中使用ProGuard并且 ... 
- 在Android中调用C#写的WebService(附源代码)
		由于项目中要使用Android调用C#写的WebService,于是便有了这篇文章.在学习的过程中,发现在C#中直接调用WebService方便得多,直接添加一个引用,便可以直接使用将WebServi ... 
- 在Android中把内容写到XML文件中
		在Android中把内容写到XML文件中 saveXmlButton.setOnClickListener(new OnClickListener() { @Override public void ... 
- [Android]在Dagger 2中使用RxJava来进行异步注入(翻译)
		以下内容为原创,欢迎转载,转载请注明 来自天天博客: # 在Dagger 2中使用RxJava来进行异步注入 > 原文: 几星期前我写了一篇关于在Dagger 2中使用*Producers*进行 ... 
- Android在layout xml中使用include
		Android include与merge标签使用详解 - shuqiaoniu的博客 - 博客频道 - CSDN.NEThttp://blog.csdn.net/shuqiaoniu/article ... 
- Android  向系统日历中添加事件
		查了一天半,总算有点大概了.以下是自己的理解,有错误的地方望指正. android系统有日历功能,应用程序可以根据一些接口开发自己的功能,即使是日历app也是根据这些接口开发的,所以我们可以利用程序向 ... 
- Android中图像变换Matrix的原理、代码验证和应用(一)
		第一部分 Matrix的数学原理 在Android中,如果你用Matrix进行过图像处理,那么一定知道Matrix这个类.Android中的Matrix是一个3 x 3的矩阵,其内容如下: Matri ... 
- Android笔记——Android中数据的存储方式(二)
		我们在实际开发中,有的时候需要储存或者备份比较复杂的数据.这些数据的特点是,内容多.结构大,比如短信备份等.我们知道SharedPreferences和Files(文本文件)储存这种数据会非常的没有效 ... 
随机推荐
- C语言深度剖析-----最终的胜利
			进军C++ 初始OOP 抽象 封装 封装的好处,改名只需改封装 小结 面试题 指针运算 打印11,16,29,28,26 调试经验 printf定义,可变参数无法判断实际参数的类型 安全编程 数组 ... 
- OC学习篇之---类的定义
			OC中类的相关知识 OC和C的最大区别就是具有了面向对象的功能,那么说到面向对象,就不得不说类这个概念了,如果学过Java的话,那么对类和对象的概念就不陌生了,因为Java是非常纯正的面向对象设计语言 ... 
- 【转载】zookeeper数据模型
			[转载请注明作者和原文链接, 如有谬误, 欢迎在评论中指正. ] ZooKeeper的数据结构, 与普通的文件系统极为类似. 见下图: 图片引用自developerworks 图中的每个节点称为一个 ... 
- 5DXTPlayer串口调试小结
			小结 过程总是艰难. 首先是没有准备好. 没有安装vs2012,安装的时候,出现各种状况,因为先安装的2013高版本,造成12安装不正确,程序编译出问题.没有办法,只好卸载vs2012,2013及其各 ... 
- CleanCode代码整洁之道培训总结(2015-03-14)
			为期四天的CleanCode培训时间非常短.非常难准确掌握一些知识.但让我对代码有了一个又一次的认识和启示:之前也有看过设计模式.重构之类的书,看完之后也有一些感触,过后在写代码中还是不能应用进来,事 ... 
- php面试题5
			php面试题5 一.总结 二.php面试题5 1. 什么事面向对象?主要特征是什么?1) 面向对象是程序的一种设计方式,它利于提高程序的重用性,是程序结构更加清晰.2) 主要特征:封装.继承.多态 2 ... 
- 服务器svn 小乌龟 visualsvn server manager Tortoisesvn的部署使用
			这个主要说说实现hook,就是本地上传文件后,服务器svn将相应的文件也修改了,实现本地上传,可以及时在浏览器查看效果 首先安装visualsvn 可参考http://blog.csdn.net/zl ... 
- Mysql错误: ERROR 1205: Lock wait timeout exceeded解决办法(MySQL锁表、事物锁表的处理方法)
			Java执行一个SQL查询未提交,遇到1205错误. java.lang.Exception: ### Error updating database. Cause: java.sql.SQLExc ... 
- .NET Framework基础知识(四)(转载)
			.反射:是编程的读取与类型相关联的元数据的行为.通过读取元数据,可以了解它是什么类型以及类型的成员. 比如类中的属性,方法,事件等.所属命名空间System.Reflection. 例:using S ... 
- jquery插件课程1  幻灯片、城市选择、日期时间选择、拖放、方向拖动插件
			jquery插件课程1 幻灯片.城市选择.日期时间选择.拖放.方向拖动插件 一.总结 一句话总结:都是jquery插件,都还比较小,参数(配置参数.数据)一般都是通过json传递. 1.插件配置数据 ... 
