跨平台开发的那些事
跨平台架构
- oc中,直接引用c、c++代码的需要命名为mm格式
- 自己开发的framework是静态库;Android的.so是共享库
- os x中,.a表示静态库,.dylib表示动态库
- linux中,.a表示静态库,.so表示动态库
- windows中,.lib表示静态库,.dll表示动态库
- 绿色表示库,黄色表示接口层,红色表示调用层
CMake
CMake是一个开源的跨平台自动化构建系统,用来管理软件构建的程序。
CMake常用命令
CMakeLists.txt文件用来配置构建参数,需要制定源文件、编译目标、include文件等信息。
- file:文件操作,可以指定读、写、文件系统操作(重命名、移除、)等
- add_executable:生成一个可执行文件
- add_library:生成一个库,可以指定是STATIC、SHARED
- target_link_libraries:链接某个库
- add_subdirectory :添加一个构建子目录,目录中的构建规则遵循目录下的CMakeLists.txt
- include_directories:指定需要包含的头文件目录
- …
一个例子
交叉编译
交叉编译:在一个平台上编译另一个平台上运行的程序(在Mac上编译Android和iPhone上的库,Android和iPhone又有多种CPU架构)
交叉编译链
Android
- CMAKE_SYSTEM_NAME
- CMAKE_SYSTEM_PROCESSOR
- ANDROID_C_COMPILER
iOS
- CMAKE_SYSTEM_NAME
- CMAKE_SYSTEM_PROCESSOR
- ANDROID_C_COMPILER
交叉编译命令
Android
ANDROID_BUILD_CMD = 'cmake "%s" %s -DANDROID_ABI="%s" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=%s/build/cmake/android.toolchain.cmake -DANDROID_TOOLCHAIN=gcc -DANDROID_NDK=%s -DANDROID_PLATFORM=android-14 -DANDROID_STL="c++_shared" && cmake --build . %s --config Release -- -j8'
iOS
IOS_BUILD_OS_CMD = 'cmake ../.. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../../ios.toolchain.cmake -DIOS_PLATFORM=OS -DIOS_ARCH="armv7;arm64" -DENABLE_ARC=0 -DENABLE_BITCODE=0 -DENABLE_VISIBILITY=1 && make -j8 && make install'
跨平台构建
CMakeLists.txt可以根据不同平台指定需要构建的文件
比如:
if(ANDROID)
file(GLOB SELF_ANDROID_SRC_FILES RELATIVE ${PROJECT_SOURCE_DIR}
android/*.cc
android/*.c
jni/*.cc
jni/*.c
jni/util/*.cc)
list(APPEND SELF_SRC_FILES ${SELF_ANDROID_SRC_FILES})
elseif(APPLE)
file(GLOB SELF_TEMP_SRC_FILES RELATIVE ${PROJECT_SOURCE_DIR} objc/*.mm objc/*.h)
source_group(objc FILES ${SELF_TEMP_SRC_FILES})
list(APPEND SELF_SRC_FILES ${SELF_TEMP_SRC_FILES} debugger/debugger_utils.c)
endif()
Andorid NDK
NDK两种方式,一种是使用ndk-build,一种是使用CMake,Google推荐CMake。
不管使用哪种方式,最终都需要在build.gradle中指定CMakeLists.txt或Andorid.mk的路径 。
externalNativeBuild {
ndkBuild {
path file('src/main/jni/Android.mk')
}
cmake {
path "CMakeLists.txt"
}
}
Android Gradle Plugin会根据不同的参数最终使用不同的命令进行构建。
一点感悟:Android提供了很多命令行工具,gradle plugin或as中的图形化界面很多都是调用那些命令行的,封装了一下而已
ndk-build
- src/main/jni目录下编写c、c++源文件
- 编写Android.mk文件,用于向编译系统描述源文件和共享库。Android.mk文件用于定义Application.mk、编译系统和环境变量所未定义的项目范围设置
- 编写Application.mk,指定ABI、CFLAGS、CPPFLAGS等
- Linking C++ With Gradle
执行命令:ndk-build
CMake
- 在src/main/cpp目录下编写c、c++源文件
- 编写CMakeLists.txt目录,配置构建参数
- Linking C++ With Gradle
执行命令:cmake
编译apk过程中,会生成cmake_build_command.txt目录,这个里面是cmake构建的参数传递,内容如下:
Executable : /Users/wangli/Library/Android/sdk/cmake/3.6.4111459/bin/cmake
arguments :
-H/Users/wangli/AndroidStudioProjects/ClimbDemo/jnidemo/src/main/cpp
-B/Users/wangli/AndroidStudioProjects/ClimbDemo/jnidemo/.externalNativeBuild/cmake/debug/arm64-v8a
-DANDROID_ABI=arm64-v8a
-DANDROID_PLATFORM=android-21
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=/Users/wangli/AndroidStudioProjects/ClimbDemo/jnidemo/build/intermediates/cmake/debug/obj/arm64-v8a
-DCMAKE_BUILD_TYPE=Debug
-DANDROID_NDK=/Users/wangli/Library/Android/sdk/ndk-bundle
-DCMAKE_TOOLCHAIN_FILE=/Users/wangli/Library/Android/sdk/ndk-bundle/build/cmake/android.toolchain.cmake
-DCMAKE_MAKE_PROGRAM=/Users/wangli/Library/Android/sdk/cmake/3.6.4111459/bin/ninja
-GAndroid Gradle - Ninja
jvmArgs :
和上面android的交叉编译命令基本类似。
ndk-build转cmake
以xCrash为例,xCrash使用的是ndk-build方式,我们基于它进行了改造,实现了自己的Native崩溃捕获库。
ndk-build方式
xCrash的Android.mk文件如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_CFLAGS := -std=c11 -Weverything -Werror -O0
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_SRC_FILES := xc_test.c
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := xcrash
LOCAL_CFLAGS := -std=c11 -Weverything -Werror -fvisibility=hidden
LOCAL_C_INCLUDES := $(LOCAL_PATH) $(LOCAL_PATH)/../../common
LOCAL_STATIC_LIBRARIES := test
LOCAL_LDLIBS := -llog -ldl
LOCAL_SRC_FILES := xc_core.c \
xc_fallback.c \
xc_recorder.c \
xc_jni.c \
xc_util.c \
$(wildcard $(LOCAL_PATH)/../../common/*.c)
include $(BUILD_SHARED_LIBRARY)
主要包括两部分,构建了test静态库和xcrash共享库。
Application.mk的内容如下:
APP_ABI := armeabi armeabi-v7a arm64-v8a x86 x86_64
APP_PLATFORM := android-14
Application.mk指定了ABI和最低的Android的版本。
cmake
使用CMake方式改造,结果如下:
cmake_minimum_required(VERSION 3.4)
project(xcrash)
set(SELF_LIBS_OUT ${CMAKE_SYSTEM_NAME}.out)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -fvisibility=hidden -O0")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11")
include_directories(.)
include_directories(../common)
find_library(log-lib log)
find_library(dl-lib dl)
file(GLOB SELF_TEMP_SRC_FILES RELATIVE ${PROJECT_SOURCE_DIR} jni/*.h jni/*.c)
list(APPEND SELF_SRC_FILES ${SELF_TEMP_SRC_FILES})
file(GLOB SELF_TEMP_SRC_FILES RELATIVE ${PROJECT_SOURCE_DIR} ../common/*.h ../common/*.c)
source_group(common FILES ${SELF_TEMP_SRC_FILES})
list(APPEND SELF_SRC_FILES ${SELF_TEMP_SRC_FILES})
add_library(${PROJECT_NAME} SHARED ${SELF_SRC_FILES})
target_link_libraries(${PROJECT_NAME} ${log-lib} ${dl-lib})
JNI
静态注册与动态注册
静态注册:jni接口名称为Java_packagename_classname_methodname
动态注册:在JNI_OnLoad方法中注册jni方法和java方法的映射表 ,JNINativeMethod数据结构如下:
typedef struct {
const char* name; //Java端方法名
const char* signature; //Java端方法签名
void* fnPtr; //jni端的函数指针
} JNINativeMethod;
举个例子:
#include <jni.h>
#include <string.h>
jstring dynamic(JNIEnv *env,jobject thiz){
return env->NewStringUTF("Hello,this is from jni");
}
//方法对应表
const static JNINativeMethod methods[]={
{"dynamic","()Ljava/lang/String;",(void *)dynamic}
};
extern "C"{
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,void* reserved){
JNIEnv *env=NULL;
if(vm->GetEnv((void **) &env,JNI_VERSION_1_4)!=JNI_OK){
return JNI_FALSE;
}
jclass jclazz=env->FindClass("com/xingfeng/HelloWorld");
if(env-> RegisterNatives(jclazz,methods, sizeof(methods)/ sizeof(methods[0]))<0) {
return JNI_FALSE;
}
return JNI_VERSION_1_4;
}
}
不论是静态注册还是动态注册,都是与类名、方法名强关联的,因此jni层用到的类和方法是不能被混淆的
Java和JNI互相调用
JNI层调用Java层
JNI层调用Java层有点类似Java反射机制,需要首先找到类,再找到某个方法或字段,再进行调用。
有点需要注意的是,JNI层的方法严格区分了返回类型,返回类型是boolean的,会有CallBoolenMethodId,返回类型是Int的,会有CallIntMethodId;同理关于FieldId和数组的方法都是这样的,不能调用错。
举个例子:
Java层操作native对象
Java端调用创建c++对象,一般是保存对象的指针地址,然后传入native层,再强转到对应对象。
举个例子:
native层打印logcat日志
native层打印logcat日志,有两种方式:
- 调用Java层的Log.i/v()之类的方法
- 使用liblog.so进行打印,和Log.i/v()底层原理相同
链接liblog.so库
cmake中加入如下语句:
find_library(log-lib log)
target_link_libraries(${PROJECT_NAME} ${log-lib})
编写源代码
#include <android/log.h>
JNIEXPORT void JNICALL
Java_com_enniu_jnidemo_MainActivity_log(JNIEnv *env, jobject thiz, jstring tag, jstring log) {
const char *tag_chars = env->GetStringUTFChars(tag, NULL);
const char *log_chars = env->GetStringUTFChars(log, NULL);
__android_log_write(ANDROID_LOG_DEBUG, tag_chars, log_chars);
__android_log_print(ANDROID_LOG_DEBUG,tag_chars,"text from java : %s",log_chars);
env->ReleaseStringUTFChars(tag, tag_chars);
env->ReleaseStringUTFChars(log, log_chars);
}
以上代码,等同于Log.d()。
参考
下一篇: Java基础