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

简介: 使用 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 <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:\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]();
}
相关文章
|
12天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
12天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
97 67
|
11天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
38 19
|
12天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
37 14
|
13天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
在数字时代,掌握安卓应用开发技能是进入IT行业的关键。本文将引导读者从零基础开始,逐步深入安卓开发的世界,通过实际案例和代码示例,展示如何构建自己的第一个安卓应用。我们将探讨基本概念、开发工具设置、用户界面设计、数据处理以及发布应用的全过程。无论你是编程新手还是有一定基础的开发者,这篇文章都将为你提供宝贵的知识和技能,帮助你在安卓开发的道路上迈出坚实的步伐。
25 5
|
11天前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
|
13天前
|
XML 搜索推荐 前端开发
安卓开发中的自定义视图:打造个性化UI组件
在安卓应用开发中,自定义视图是一种强大的工具,它允许开发者创造独一无二的用户界面元素,从而提升应用的外观和用户体验。本文将通过一个简单的自定义视图示例,引导你了解如何在安卓项目中实现自定义组件,并探讨其背后的技术原理。我们将从基础的View类讲起,逐步深入到绘图、事件处理以及性能优化等方面。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。
|
12天前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
4天前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数
|
12天前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
21 0