欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

跨平台开发的那些事

程序员文章站 2022-07-14 16:51:42
...

跨平台架构

跨平台开发的那些事

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

参考