使用 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]();
}
相关文章
|
7天前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。
|
1天前
|
Shell 开发工具 Android开发
|
7天前
|
Android开发 开发者 UED
探索安卓应用开发中的UI设计趋势
随着移动应用市场的不断发展和用户需求的变化,安卓应用的UI设计趋势也在不断演进。本文将深入探讨当前安卓应用开发中的UI设计趋势,包括暗黑模式、原生化设计、动效设计等方面的发展趋势,为开发者提供参考和启发。
|
7天前
|
安全 网络安全 量子技术
网络安全与信息安全:漏洞、加密技术与安全意识的探索安卓应用开发中的内存管理策略
【5月更文挑战第31天】随着互联网的普及,网络安全问题日益严重。本文将深入探讨网络安全漏洞、加密技术以及安全意识等方面的问题,以期提高公众对网络安全的认识和防范能力。
|
7天前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第31天】在现代Android开发中,异步编程和性能优化成为关键要素。Kotlin协程作为一种在JVM上实现轻量级线程的方式,为开发者提供了简洁而强大的并发处理工具。本文深入探讨了如何在Android项目中利用Kotlin协程提升应用的响应性和效率,包括协程的基本概念、结构以及实际运用场景,旨在帮助开发者通过具体实例理解并掌握协程技术,从而构建更加流畅和高效的Android应用。
|
7天前
|
数据库 Android开发 开发者
构建高效Android应用:Kotlin协程的全面指南
【5月更文挑战第31天】 在移动开发领域,性能优化和流畅的用户体验是至关重要的。随着Kotlin语言在Android开发中的普及,其提供的协程功能已成为简化异步编程、提高应用响应性和效率的强大工具。本文将深入探讨Kotlin协程的概念、优势以及如何在Android应用中实现它们。通过实际案例分析,我们将展示如何利用协程提升数据处理能力,同时保持UI线程不被阻塞,确保用户界面流畅无阻。
|
7天前
|
机器学习/深度学习 算法 Android开发
安卓应用开发:打造高效通知管理系统
【5月更文挑战第31天】在移动应用的海洋中,用户经常被各种推送通知所困扰。一个精心设计的通知管理系统对于提升用户体验至关重要。本文将探讨如何在安卓平台上开发一个高效的通知管理系统,包括系统设计原则、实现机制以及性能优化策略,旨在为开发者提供一套可行的解决方案,以减少用户干扰并提高应用的用户留存率。
|
7天前
|
JSON Android开发 开发者
构建高效Android应用:采用Kotlin协程优化网络请求
【5月更文挑战第31天】 在移动开发领域,尤其是针对Android平台,网络请求的管理和性能优化一直是开发者关注的焦点。随着Kotlin语言的普及,其提供的协程特性为异步编程提供了全新的解决方案。本文将深入探讨如何利用Kotlin协程来优化Android应用中的网络请求,从而提升应用的响应速度和用户体验。我们将通过具体实例分析协程与传统异步处理方式的差异,并展示如何在现有项目中集成协程进行网络请求优化。
|
8天前
|
物联网 区块链 Android开发
构建高效Android应用:Kotlin与Jetpack的实践之路未来技术的融合潮流:区块链、物联网与虚拟现实的交汇点
【5月更文挑战第30天】 在移动开发领域,效率和性能始终是开发者追求的核心。随着技术的不断进步,Kotlin语言以其简洁性和现代化特性成为Android开发的新宠。与此同时,Jetpack组件为应用开发提供了一套经过实践检验的库、工具和指南,旨在简化复杂任务并帮助提高应用质量。本文将深入探索如何通过Kotlin结合Jetpack组件来构建一个既高效又稳定的Android应用,并分享在此过程中的最佳实践和常见陷阱。
|
8天前
|
缓存 监控 Android开发
Android 开发中的内存优化策略
【5月更文挑战第30天】在移动应用的开发过程中,性能和用户体验始终是核心关注点。对于基于Android平台的应用程序,有效的内存管理是确保流畅运行和优异性能的关键因素之一。本文将深入探讨Android开发中常见的内存问题,并提出一系列实用的内存优化策略。我们将从内存泄漏的识别与防止开始,到合理使用内存缓存技巧,以及高效的数据结构选择等方面进行详细阐述。通过这些策略的实施,开发者可以显著减少应用的内存占用,提升应用的稳定性和响应速度,进而改善最终用户的体验。