使用 Go 语言开发 Android 应用的正确姿势探索

Android系统是基于linux,但开发框架和系统api是基于java语言的。

因此使用java或是kottin开发Android应用是自然的,是原生的应用且速度也是很快的。

考虑到需要支持其他系统如IOS苹果系统需要重复开发APP,或是基于java原生的app不能很好的支持热更新,

或如电商APP等前端业务复杂的场景,于是又出现了如Weex,React Native等使用node,html5和javaScript等前端技术开发跨平台app的方式。

无论哪种方式的都是基于需求和特定的场景决定的。

能否使用go语言开发Android应用?

当然也是可以的,可以在特定场景下局部的使用。但要是全部?包括界面?真不想折腾。

擅长的领域使用擅长的技术做它擅长的事,提高效率和满足需求才是根本目的。

使用java做Android的原生界面已经很顺溜了,且也很简单。还折腾用go去做Android界面意义何在?

不过这也是仁者见仁,智者见智的事情。撇开了特定的场景和需求谈这些无意义!

假若界面真的很简单,或者界面简单但有点儿个性,即便用原生java去做也得去画。

权衡下利弊得失,用go去尝试也未尝不可。

本文描述的场景也只是针对于Android应用需要调用本地Native层占比很高的场景。

避开了这种场景,这种尝试的意义也不大!若native层不多,也没必要。

比如说使用Reaect Native技术开发应用很火,你就要去用吗?

假若本来用原生java就很容易实现的,你不考虑你的使用场景也要去盲目追风非得去用?

那不就是舍近求远,舍本逐末吗?有点儿得不偿失划不来。

java高级语言面向对象,能够提供你好的灵活的封装和复用。

各种开源 java库一大堆,无论是网络通信,还是数据库存储等,都有很多强大的开源库使用。

那么go来开发Android应用可以用在哪?还有必要用go吗?

有,有一个地方可以尝试用go!

那就是java通过JNI调用c或c++的部分,可以用go来替代

原来的那种方式,实在是太繁琐了。可以使用go做这部分native层的工作

使用JNI太繁琐了,尽管我用的很熟了,封装动态库.so很溜了,但是封装吐了。

参数传递和接口封装写的真的很累人!

但是用go语言,一下子清爽多了!

go把底层的c的驱动调用封起来,go调c的接口很简单。

部分需要放在Native层的功能,使用go来提供接口,供java层调用。

界面,教给擅长的java的原生调用去负责,毕竟它擅长,擅长的就干擅长的事。

甚至,可以把业务也用go来做,如网络通信和数据存储等功能。

甚至可以让Android应用的Java层只负责界面。

这些尝试都提供了另外一种选择。

无论是java的原生开发,还是React Native还是Flutter,本身都有自己的完整生态

比如单独使用Flutter,它的体系内使用Dart语言,无论是存储还是网络通信等功能都涵盖。

如果只用Flutter的界面或者java原生的只做界面层。业务都用 go来做呢?

是否也能满足需求?满足跨平台?提高效率?

能否用go作为主流完整的开发移动应用?就目前来说希望不大。

google现在主推的移动端开发是Flutter,且现在开发Android应用的方式够多了,生态已经建立起来了。

使用JNI去封装c的接口供java层调用有多繁琐?知道有多繁琐就知道这块多希望能用go来取代。

例如这个,得有个java类文件声明本地接口,且包名不能搞错。

package com.newcapec.tycard.jni;

public class JniCard {
    static
    {
        try {
            System.loadLibrary("ztcard");
            System.out.println("--------------------loadlibrary -- ztcard ok");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private native int Native_GetCardSn(byte cardsn[]);
}

c代码层: 

/*
 * Class:     com_newcapec_tycard_jni_ZtRfCard
 * Method:    Native_GetCardSn
 * Signature: ([B)I
 */
JNIEXPORT jint JNICALL Java_com_newcapec_tycard_jni_JniCard_Native_1GetCardSn
        (JNIEnv *env, jobject jobj, jbyteArray cardsn) {
    int ret = 0;
    char *tmpSN = NULL;
    int size = 0;

    assert_info;
    env_init(env);
    if (cardsn) {
        step_info;
        tmpSN = (*env)->GetByteArrayElements(env, cardsn, NULL);
        size  = (*env)->GetArrayLength(env, cardsn);
    }

    memset(tmpSN, 0, size);
    ret = ICF_SelectAID("\xA0\x00\x00\x00\x03\x86\x98\x07\x01", 9, Gich_Icc);
    if(ret != 0x9000){
        step_info_r(ret); return 1;
    }

    ret = ICF_ReadBinaryFile( 0x15, 0, 30, Gich_Icc );
    if(ret != 0x9000){
        step_info_r(ret); return 1;
    }

    memcpy(tmpSN, &gpRcvBuffer[12], 8);

    if( cardsn && tmpSN ) {
        (*env)->ReleaseByteArrayElements(env, cardsn, tmpSN, 0);
        step_info;
    }

    return 0;
}

首先是函数名,这么长的一串!!!,是可以使用javah自动生成,但是看着就别扭,使用起来还是麻烦。

取个参数吧,需要JNI的c层与java层转来转去的。GetByteArrayElements,分配的内存呢,还得不能忘了释放:

ReleaseByteArrayElements。

AndroidStdio的环境也得配,如:

sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            assets.srcDirs = ['src/main/assets', 'src/main/assets/']
        }
    }

externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
    }

还得组织好Android.mk文件,配置好环境。

#
# Copyright (C) 2010 The Android Open Source Project
#
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_SRC_FILES:= \
	   jni_Card.c \
	   CardPublic.c \
	 
#SRC_TAG := \
#    ./      \
#	AH_Driver  \
#	APP_Task \
#	JTB_Card \
#	NC_CurCalc_Lib/LIB \
#	protocol

#SRC_FILES := $(call all-cpp-files-under, $(SRC_TAG))
#SRC_FILES += $(call all-c-files-under, $(SRC_TAG))
#LOCAL_SRC_FILES := $(SRC_FILES)
	   
LOCAL_MODULE:= libztcard

LOCAL_SHARED_LIBRARIES := \
    libutils \
    libandroid_runtime \
    libnativehelper \
    libdl \
    liblog
	
LOCAL_LDLIBS := -llog

LOCAL_STATIC_LIBRARIES := \


#LOCAL_C_INCLUDES += $(JNI_H_INCLUDE)
	
#LOCAL_LDFLAGS += \
#	$(LOCAL_PATH)/bsp_lib/$(TARGET_ARCH_ABI)/libicc_interface.so \
#	$(LOCAL_PATH)/bsp_lib/$(TARGET_ARCH_ABI)/libpicc_interface.so 
	
#LOCAL_ALLOW_UNDEFINED_SYMBOLS := true
APP_ALLOW_MISSING_DEPS=true
LOCAL_MODULE_TAGS := optional
include $(BUILD_SHARED_LIBRARY)

"D:\Program Files\Java\jdk1.8.0_144/bin/javah.exe" -classpath . -jni -d E:\ldpad\mygit\tycard\app/src/main/jni com.newcapec.tycard.jni.JniCard

tool-->Externaltool->配置javah

$JDKPath$/bin/javah.exe

-classpath . -jni -d $ModuleFileDir$/src/main/jni $FileClass$

$ModuleFileDir$\src\main\java

或者使用另一种写法如:

//jni_card_lib.c
#define  _SYS_GLOBE_VAR_

#include "lib_includes.h"
#include 

static const char *TAG =	"CARDLIB_JNI";

//#define PATH_CLASS_NAME    "com/newcapec/jni/CardNc"


#define LOGD(fmt, args...) \
		do{ if (debug_level >= 3) __android_log_print(ANDROID_LOG_DEBUG,  TAG, fmt, ##args); } while(0)

#define LOGI(fmt, args...) \
		do{ if (debug_level >= 2) __android_log_print(ANDROID_LOG_INFO,  TAG, fmt, ##args); } while(0)

#define LOGE(fmt, args...) \
		do{ if (debug_level >= 1) __android_log_print(ANDROID_LOG_ERROR,  TAG, fmt, ##args); } while(0)

#define LOGA(fmt, args...) \
		do{ if (debug_level >= 0) __android_log_print(ANDROID_LOG_ERROR,  TAG, fmt, ##args); } while(0)
struct TradeInfo_fields_t
{

	//=========需要返回给java层的变量定义
	//
	jfieldID	sDealTime;		//交易日期

	jfieldID    lDealMoney;     

	jfieldID	sPsamTID;		
    //省略。。。。。。
}

static jlong Jni_Card_Exch(JNIEnv *env, jobject obj,jlong money,jobject tradeInfo)
{

	U32 rcode = 0;

	char *date = NULL;

	jstring tmpString;

	LOGD(">>> ..%s..%d..enter",__FUNCTION__,__LINE__);

	TradeInformation.DealMoney  = money;//test

	//获取java层传递过来的值
	jlong jret = (jlong)(*env)->GetLongField(env,tradeInfo, TradeInfoFields.lIsUpdateRec);

	LOGD(">>> ..%s..%d..,IsUpdateRec:%ld,",__FUNCTION__,__LINE__,jret);
	IsUpdateRec = (U32)jret;

	if(TradeInformation.CardPhyType == DEF_PhyType_CPU)//CPU卡处理
	{
		rcode = Card_CPU_Exch_NC();
	}
	else
	{
		rcode = Card_M1_Exch();
	}


	SetTradeInfo(env,tradeInfo);
	LOGD(">>> ..%s..%d.exit",__FUNCTION__,__LINE__); 
	return rcode;
} 


//定义批量注册的数组,是注册的关键部分
static const JNINativeMethod gMethods[] = { 
    { "Native_JniTest","()J",	(void*)Jni_Test},
	{ "Native_Card_FindCard","([B)J",	(void*)Jni_Card_FindCard},
    ......
    //省略

};

static jint FindTradeInfoFields(JNIEnv *env)
{
	static const char *const kTradeInfoClassName = "com/newcapec/jni/CardNc$TradeInfo";
	jclass clazz;

	LOGD(">>> ..%s..%d..enter",__FUNCTION__,__LINE__);

	clazz = (*env)->FindClass(env,kTradeInfoClassName);
	if (clazz == NULL)
	{
		LOGD("Can't find class %s!\n", kTradeInfoClassName);
		return JNI_FALSE;
	}

	TradeInfoFields.sDealTime = (*env)->GetFieldID(env,clazz, "sDealTime", "Ljava/lang/String;");
	if (TradeInfoFields.sDealTime == NULL)
	{
		LOGE("com/newcapec/jni/CardNc$TradeInfoFields.sDealTime");
	}
      //省略......

	LOGD(">>> ..%s..%d.exit", __FUNCTION__, __LINE__);
	return 0;
}

// extern "C" {
JNIEXPORT jint JNI_OnLoad(JavaVM* vm,void *reserved)
{
	JNIEnv *env =NULL;
	jint result = -1;
	static const char* kClassName= "com/newcapec/jni/CardNc";
	jclass clazz;
	
	debug_level = 5;
	
	LOGD(">>> ..%s..%d..enter",__FUNCTION__,__LINE__);
	
	if( (*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_4) != JNI_OK )
	{
		return result;
	}

	clazz = (*env)->FindClass(env,kClassName);
	if( clazz == NULL )
	{
		LOGE("%d..Can't find class %s!\n",__LINE__, kClassName);
		return -1;
	}

	FindTradeInfoFields(env);

	if( (*env)->RegisterNatives( env,clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]) ) != JNI_OK )
	{
		LOGE("Failed registering methods for %s!\n", kClassName);
		return -1;
	}
	LOGD(">>> ..%s..%d.exit",__FUNCTION__,__LINE__);
	return JNI_VERSION_1_4;
}
//}

虽然函数名如Jni_Card_Exch看着不长,清爽很多,但是还要麻烦的函数签名,注册。

 { "Native_JniTest","()J",    (void*)Jni_Test},麻烦死了。

若接口少还好,若都得这样,要让人疯掉。

ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk

--copy--
cp ./libs/arm64-v8a/libcardnc.so D:\GitAsWork\fenghuanggucheng\FengHuang\app\src\main\jniLibs\arm64-v8a\libcardnc.so

这样的效率,能高吗?

但是,但是,如果用go,感觉一下子清爽了好多。

gomobile bind -target=android hello

生成hello.aar文件和hello-sources.jar文件,放到Android工程的libs目录里,aar文件供调用,source.jar可以看源码。

// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package hello is a trivial package for gomobile bind example.
package hello

import "fmt"

func Greetings(name string) string {
	fmt.Printf("hello go,this is log\n")
	return fmt.Sprintf("Hello aaa, %s!", name)
}

func Test1(buf []byte) []byte{

	fmt.Printf("in buf is:%x\n",buf)
	outbuf := []byte{0x31,0x32,0x33,0x34}
	return outbuf
   
}

然后应用里就可以很爽的调用:

如:

/*
 * Copyright 2015 The Go Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file.
 */

package org.golang.example.bind;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import hello.Hello;

public class MainActivity extends Activity {

    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.mytextview);

        // Call Go function. test String
        String greetings = Hello.greetings("Android and Gopher aaa11");
        mTextView.setText(greetings);
        // Call Go function. test byte[]
        byte[] inbuf = new byte[6];
        inbuf[0] = 0x39;
        inbuf[1] = 0x38;
        inbuf[2] = 0x37;
        inbuf[3] = 0x36;
        byte[] outbuf =  Hello.test1(inbuf);
       System.out.println(outbuf);
    }
}

 AndroidStdio的配置麻不麻烦呢?也不麻烦。配置下引用外部库即可。

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation fileTree(include: ['*.aar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:22.1.1'
    //implementation project(':hello')
    implementation files('libs/hello.aar')
}

最后再来说下环境,也很简单,只需配置一次即可。

使用了gomobile。

go get golang.org/x/mobile/cmd/...,

也下不下来,就去github上找,找到后下载下来放到指定位置即可。

https://github.com/golang/mobile/

总结下:

直接用AndroidNDK编写SDK,需要自己写JNI。而gomobile一个命令,把脏活累活都给弄好了。
可以一份代码支持Android和iOS,维护上比较方便。
体积上,gomobile的so最起码有2.8MB,比C要大不少,也还能接受。因为效率高啊。

如果再有人找我封装JNI层的.so?我想,我想用go来做!

至于执行的效率,可反编译过来看下,其实内部还是调的c的JNI,只不过gomobile命令把这些繁琐的事做了。

效率应差不了多少。至于稳定性,虽然gomobile是谷歌内部的一个实验性项目,但是你只使用gobind做native层的工作,这部分已经很稳定了。

gomobile 介绍

gomobile 可以让golang在移动设备中使用

  • bind 动态库方式native开发
  • build 直接生成移动应用
  • install 将生成的app,安装到设备或者模拟器
  • clean 清空缓存

一般使用bind方式开发,build方式还是试验性的

支持的类型
Signed integer and floating point types.
String and boolean types.
Byte slice types. Note that byte slices are passed by reference,
and support mutation.
Any function type all of whose parameters and results have
supported types. Functions must return either no results,
one result, or two results where the type of the second is
the built-in 'error' type.
Any interface type, all of whose exported methods have
supported function types.
Any struct type, all of whose exported methods have
supported function types and all of whose exported fields
have supported types.
https://godoc.org/golang.org/x/mobile/cmd/gobind

基本类型也就是

string(不支持string数组)
bool
int(java这边引用的时候会是long)
byte[]
传递返回值无法传递数组,可以将数据转成json格式然后通过string或者byte array传递过来,这边再解析。最好不要通过for循环频繁调用,因为他们之间的通讯是有代价的。

配置gomobile的环境
$ go get golang.org/x/mobile/cmd/gomobile
$ gomobile init # it might take a few minutes
最好将目录$GOPATH/bin加到环境变量,不然运行gomobile命令还需要进入到GOPATH/bin目录下。

如果go get不下来gomobile的话,可以将镜像工程:https://github.com/golang/mobileclone到GOPATH/src/golang.org/x目录下

gomobile init之前需要环境变量中配置了ndk环境,可把ndk环境加到系统环境变量,或者通过ndk标签指定ndk目录gomobile init -ndk 指定。注意,要求ndk版本是在19以上才行。

gomobile init

初始化会等几分钟,看网速,初始化后才可以正式使用!

Android 使用类似如下

import go.package.[GoPakcageName];
private void (){
  [GoPakcageName].[GoFunction]();
}

 

 

你可能感兴趣的:(Android)