使用 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("xA0x00x00x00x03x86x98x07x01", 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 FilesJavajdk1.8.0_144/bin/javah.exe" -classpath . -jni -d E:ldpadmygittycardapp/src/main/jni com.newcapec.tycard.jni.JniCard
tool-->Externaltool->配置javah
JDKPath/bin/javah.exe
-classpath . -jni -d ModuleFileDir/src/main/jni FileClass
ModuleFileDirsrcmainjava
或者使用另一种写法如:
//jni_card_lib.c
#define _SYS_GLOBE_VAR_
#include "lib_includes.h"
#include <jni.h>
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:GitAsWorkfenghuangguchengFengHuangappsrcmainjniLibsarm64-v8alibcardnc.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 logn")
return fmt.Sprintf("Hello aaa, %s!", name)
}
func Test1(buf []byte) []byte{
fmt.Printf("in buf is:%xn",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 最好将目录
如果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]();
}
- 七牛镜像存储 WordPress 插件 的代码化
- WordPress 会自动压缩JPEG 格式的图像?
- jquery操作DOM 元素(2)
- WordPress 中部署真正的懒加载(Lazy Load)
- 图形化的2008R2 Server Core 配置管理工具
- 各种浏览器的userAgent
- WordPress 根据浏览器 user-agent 按需加载CSS 文件
- memcached的最新状态
- [程序设计语言]-01:引言
- ASP.NET Ajax 库
- ASP.NET进程优化
- 多说 提速:js内页页脚加载、静态文件CDN
- 微信小程序的王者时代
- [程序设计语言]-[核心概念]-02:名字、作用域和约束(Bindings)
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- Centos7.3服务器搭建LNMP环境的方法
- Linux中解除端口占用的方法
- ubuntu服务器环境下安装python的方法
- 带你入门Linux中size命令的6个例子
- 详解CentOS重启后resolv.conf被重置的解决方案
- scRNA-seq Clustering quality control(二)
- ubuntu环境下安装memcache及启动的方法
- Linux下批量修改服务器用户密码方法步骤
- Linux 常用文本处理命令和vim文本编辑器
- centos7利用yum安装lnmp的教程(linux+nginx+php7.1+mysql5.7)
- linux根据进程号PID查找启动程序的全路径
- linux服务器安装PHP扩展zip,zlib方法
- Lamp环境下设置绑定apache域名的方法分析
- Linux杀不死的进程之CPU使用率700%解决方法
- 解读Linux下ip命令展示的网络连接信息