跨平台开发的那些事

跨平台架构

跨平台结构

  • oc中,直接引用c、c++代码的需要命名为mm格式
  • 自己开发的framework是静态库;Android的.so是共享库
    • os x中,.a表示静态库,.dylib表示动态库
    • linux中,.a表示静态库,.so表示动态库
    • windows中,.lib表示静态库,.dll表示动态库
  • 绿色表示库,黄色表示接口层,红色表示调用层

CMake

CMake是一个开源的跨平台自动化构建系统,用来管理软件构建的程序。

CMake常用命令

CMakeLists.txt文件用来配置构建参数,需要制定源文件、编译目标、include文件等信息。

一个例子

交叉编译

交叉编译:在一个平台上编译另一个平台上运行的程序(在Mac上编译Android和iPhone上的库,Android和iPhone又有多种CPU架构)

交叉编译链

Android

android.toolchain.cmake

  • CMAKE_SYSTEM_NAME
  • CMAKE_SYSTEM_PROCESSOR
  • ANDROID_C_COMPILER

iOS

ios.toolchain.cmake

  • 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

执行命令: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日志,有两种方式:

  1. 调用Java层的Log.i/v()之类的方法
  2. 使用liblog.so进行打印,和Log.i/v()底层原理相同

链接liblog.so库

cmake中加入如下语句:

find_library(log-lib log)  

target_link_libraries(${PROJECT_NAME} ${log-lib})

编写源代码

android/log.h

#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()。

参考

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页