iOS Develope

Golang 开发跨平台SDK(iOS, Android),Part 2

    上一篇简单的介绍了下Golang Mobile的工作原理,这一篇讲介绍如何使用Golang 做一个跨平台的RSS SDK。

RSS和HTTP

    RSS是基于XML标准,在互联网上被广泛采用的内容包装和投递协议,主要用来描述和同步网站内容。既然RSS是一种网络协议,那我们就需要使用Golang的http模块了。整个流程可以归结为,RSS的SDK提供一个方法去请求RSS源获得数据,然后通过callback获得数据,而这些数据最好是Android/iOS都可以直接使用的model集合。

    作为一个http请求在移动端最好是在子进程中执行,通过异步回调返回给主进程或UI 进程,这其中带来的明显好处就是我们的http任务不会阻塞整个UI,用户在请求RSS源获取最新网站内容的同时依然可以继续对应用进行操作。Golang 本身并没有提供一个异步执行http的库,但是我们可以利用Golang routine 简单的封装一个异步执行http的库。

func (t *Task) Runtask() {  
    r := t.create()      // 这里create是创建channel
    go func() {
        res, err := http.Get(t.Url)
        if err != nil {
            r.err <- err
        } else {
            buf, err := ioutil.ReadAll(res.Body)
            if err != nil {
                r.err <- err
            } else {
                r.data <- buf
            }
        }
    }()
}

上面的代码段摘自我实现的库go-async,其原理就是将http任务在 golang routine中去执行,当执行结束是通过channel 返回。

接口

    通过简单的继承上面的go-async我们就可以实现一个简单的RSS http请求。

func RSS() string {  
    return "http://rss.cnbeta.com/rss"
}
type Listener interface {  
    OnSuccess(buf []byte)
    OnFailure(err string)
    OnEnd()
}
type CnBeta struct {  
    listener Listener
}
func (cb *CnBeta) Run() {  
    t := async.NewTask(RSS(), cb)
    t.Runtask()
}
func (cb *CnBeta) Success(buf []byte) {  
}

func (cb *CnBeta) Failure(err error) {  
}

因为Golang现在不支持倒出constant类型的数据结构,所以我们需要定义一个function,然后利用function的返回值取得这个常量。SuccessFailure是从go-async继承的方法,当异步http callback的时候就会执行其中一个方法。Listener是定义的一个外部接口,当然在Java/swift/Objective-C中实现这个接口在应用中拿到异步callback的数据。

Model

    上面我实现了请求RSS源并且异步获取了byte数据,但是我们的目标是设计一个SDk,只提供方法并不能满足一个完整的SDK的需要,SDK需要接口返回的是Model或者Model的集合。

func NewRssXml() *RssXml {  
    return &RssXml{
        Channel: &RssFeed{},
    }
}

type RssXml struct {  
    Version string
    Channel *RssFeed
}

type RssFeed struct {  
    Title       string
    Link        string
    Description string
    Language    string
    Copyright   string
    PubDate     string

    Items []*RssItem
}

type RssItem struct {  
    Title       string
    Link        string
    Description string
    PubDate     string
}

func (rss *RssXml) Feed() *RssFeed {  
    return rss.Channel
}

上面就是使用Golang实现的RSS的model,由于Golang mobile 本身的限制只能export基本的数据类型,所以我只export需要的类型,但是我也想要直接export Items []*RssItem,这时我们可以通过实现一个类型方法来实现

type RssFeed struct {  
    Title       string
    Link        string
    Description string
    Language    string
    Copyright   string
    PubDate     string

    items []*RssItem
}

func (feed *RssFeed) Length() int64 {  
    return int64(len(feed.items))
}

func (feed *RssFeed) Item(p int64) *RssItem {  
    if p >= feed.Length() {
        return &RssItem{}
    }
    return feed.items[p]
}

通过 rssfeed.Length() 我们获得总共有多少RssItem,而通过rssfeed.Item(1) 我们变可以得到第二个RssItem,变相的实现了直接使用rssfee.Items[1]

整合

    实现了Rss Model这时我们便可以将接口改写下

type Listener interface {  
    OnSuccess(rss *RssXml)
    OnFailure(err string)
    OnEnd()
}

func (cb *CnBeta) Success(buf []byte) {  
    doc := xmlx.New()
    if err := doc.LoadBytes(buf, nil); err != nil {
        cb.Failure(err)
        return
    }
    cb.listener.OnSuccess(parser(doc))
    cb.listener.OnEnd()
}

func (cb *CnBeta) Failure(err error) {  
    cb.listener.OnFailure(err.Error())
    cb.listener.OnEnd()
}

这是一整个SDK 便是完成了,但是最关键的部分是编译后在应用开发时引入,以iOS为例通过前一篇的方法我们得到了rss.framework,这时我们只要通过XCode将这个rss.framework引入到项目中便可以使用了。

func loadRss() {  
    let cnbeta = GoCbNewCnBeta(self)
    cnbeta.run()
}

// MARK: - cnbeta lisener interface
func onEnd() {  
}

func onFailure(err: String!) {  
}

func onSuccess(rss: GoCbRssXml!) {  
    cbRss = rss

    dispatch_async(dispatch_get_main_queue(), {

        if (self.refreshControl!.refreshing) {
            self.refreshControl!.endRefreshing()
        }
        self.tableView.reloadData()
    })
}

下面的截图便是我写的一个Demo demo

结论

    作为一个跨平台的语言,Golang拥有其特有的优势,编译成机器码带来的高效的性能,而且作为一个编写SDK或者数据处理层的工具,其高效的开发效率也是值得考虑的。至于能不能用在production的项目上,我想说Google都在用。

refs

Golang 开发跨平台SDK(iOS, Android),Part 1

前一篇介绍了用React-Native做跨平台开发,其实Golang在1.5发布以后也是支持做移动端的跨平台开发的,Golang的移动端开发支持两种模式,一种是使用Golang开发整个应用,另外一种便是使用Golang开发common library。这两种各有优缺点,前者没有完善的UI库,如用来开发一个完整的应用需要的工作量着实有点不小,或者用来开发游戏可能也是一个不错的选择,亦或者寄望于Google可以开发出完善的UI库,至于后者想对于前者就方便多了,Google的GoMobile项目已经完善了大部分的工作,现在的缺点就是支持的数据类型还是远远不够,而且现只支持ARM架构。所以现阶段二者都还有些限制,但是作为一个跨平台的备选方案还是有其可取之处的。

How it works

在Google GoMobile的项目里Google提供了一个工具gobindgobind可以生成对于Java和Objective-C的bindings。下面是一段Golang代码

package mypkg

type Counter struct {  
    Value int
}

func (c *Counter) Inc() { c.Value++ }

func New() *Counter { return &Counter{ 5 } }  

生成的Java Bindings是

public abstract class Mypkg {  
    private Mypkg() {}
    public static final class Counter {
        public void Inc();
        public long GetValue();
        public void SetValue(long value);
    }
    public static Counter New();
}

Objective-C 代码如下:

@class GoMypkgCounter;

@interface GoMypkgCounter : NSObject {
}

@property(strong, readonly) GoSeqRef *ref;
- (void)Inc;
- (int64_t)Value;
- (void)setValue:(int64_t)v;
@end

FOUNDATION_EXPORT GoMypkgCounter* GoMypkgNewCounter();  

其实简单的来说就是将Golang翻译成Java和Objective-C,只不过bindings只是提供访问Golang编译后的二进制文件的接口。

How to build it

比如说你有个一个test.go文件

package mypkg  
type Counter struct {  
    Value int
}
func (c *Counter) Inc() { c.Value++ }  
func New() *Counter { return &Counter{ 5 } }  

1.在编译Android的library时可以使用
ANDROID_HOME=your_android_sdk_path gomobile bind -target=android -o file_path_for_android_library.aar -v test.go
这段代码会生成一个Java的二进制libray file_path_for_android_library.aar,其中包涵了ARM架构的.so二进制文件,.jniJava bindings classes等文件,在Android的项目中直接引用这个aar这个文件就可以用了。

Counter c = Mypkg.New();  
c.Inc();  
c.GetValue();  


2.在编译iOS的library时可以使用
gomobile bind -target=iOS -o file_path_for_android_library.framework -v test.go
这段代码会生成一个iOS可用的framework库,然后在iOS的项目中引用这个file_path_for_android_library.framework就可以了。

GoMypkgCounter *c = [GoMypkgCounter init];  
c.Inc();  
c.Value();  

更多信息可以看 https://godoc.org/golang.org/x/mobile/cmd/gobind

React-Native Native Module In Practise

React-Native 是Facebook 开发的一套移动端跨平台开发的框架,其脱胎于 ReactJS 。React-Native允许你使用JavaScript开发移动端的应用,相对于 PhoneGap React-Native 确实带来了原生应用的性能,如果之前有写过ReactJS,用React-Native开发一个移动端应用是非常容易的。不过虽然React-Native社区一天天壮大,但是仍旧可能需要自己开发一些React-Native的原生模块。

什么是React-Native原生模块

React-Native 模块就是包含原生代码库的React-Native的模块(在Android 里是包含Java代码库,在iOS里是包含Objecive-C/Swift代码库),其工作原理是将原生代码库加载到React-Native的包管理器中,然后用JS通过React-Native的bridge调用原生库里的方法,最后返回结果。

Setup
创建React-Native项目
  1. 创建工作目录mkdir hello-rct
  2. 使用npm init 初始化项目
  3. 使用android sdk自带的cli工具初始化一个android library项目
android create lib-project \  
   --name hellorct \                # Project name
   --target android-23 \            # Android SDK version(target)
   --package com.xeodou.hellorct \  # Package name
   --gradle --gradle-version 1.3+ \ # Use gradle and 2.4 later version
   --path android                   # The project's directory

4.在android/build.gradle最后添加

dependencies {  
    compile 'com.facebook.react:react-native:0.13.+'
}

5.创建Java组件
添加HelloRCTManager.java

/*
* @Author: xeodou
* @Date:   2015
*/
package com.xeodou.hellorct

import com.facebook.react.ReactPackage;  
import com.facebook.react.bridge.JavaScriptModule;  
import com.facebook.react.bridge.NativeModule;  
import com.facebook.react.bridge.ReactApplicationContext;  
import com.facebook.react.uimanager.ViewManager;  
import java.util.ArrayList;  
import java.util.Collections;  
import java.util.List;

public class HelloRCTManager implements ReactPackage {

    @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
    @Override
    public List<NativeModule> createNativeModules(
            ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new HelloRCT(reactContext));
        return modules;
    }
}

创建HelloRCT.java

/*
* @Author: xeodou
* @Date:   2015
*/

package com.xeodou.hellorct

import com.facebook.react.bridge.ReactApplicationContext;  
import com.facebook.react.bridge.ReactContextBaseJavaModule;  
import com.facebook.react.bridge.ReactMethod;  
import com.facebook.react.bridge.Callback;

public class HelloRCT extends ReactContextBaseJavaModule {

    public static final String REACT_CLASS = "HelloRCT";

    public HelloRCT(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return REACT_CLASS;         //这里的getName方法是将你的原生模块注册
    }                               //到React-Native的模块包管理器中,然后
                                    //在使用JS使用相同的名字获得该原生模块
    @ReactMethod                    // ReactMethod表明该方法是暴露给JS的
    public void hello(Callback cb) {// 的外部方法,返回一个字符串`Hello RCT`
        cb.invoke("Hello RCT");     // React-Native的调用是异步的
    }                               // 所以这里我们用Callback返回结果
}

6.创建index.android.js

/*
* hello-rct - index.android.js
* Copyright(c) 2015 xeodou <[email protected]>
* MIT Licensed
*/

var { NativeModules } = require('react-native');  
module.exports = NativeModules.HelloRCT;  

7.使用react-native init Example创建一个React-Native的Example应用测试我们的模块

  • 修改Example/settings.gradle
include ':app','hellorct'  
project(':hellorct').projectDir = new File(rootProject.projectDir, '../../android')  
  • 修改Example/app/build.gradle
dependencies {  
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
    compile 'com.facebook.react:react-native:0.13.+'
    compile project(':hellorct')  // <- 添加这行
}
  • 修改Example/app/src/main/java/.../MainActivity.java
import com.xeodou.hellorct.HelloRCTManager; //<- import java class

public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {  
     ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mReactRootView = new ReactRootView(this);

        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setBundleAssetName("index.android.bundle")
                .setJSMainModuleName("index.android")
                .addPackage(new MainReactPackage())
                .addPackage(new HelloRCTManager())  //<- 注册模块
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();

        mReactRootView.startReactApplication(mReactInstanceManager, "Example", null);

        setContentView(mReactRootView);
    }
...
}
  • 修改Example/index.android.js
...
var { NativeModules } = require('react-native');  
var HelloRCT = NativeModules.HelloRCT;

var Example = React.createClass({  
  getInitialState: function() {
    return {};
  },
  click: function() {
    HelloRCT.hello( msg => {
      this.setState({
        hello: msg
      })
    })
  },

  render: function() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Hi, {this.state.hello}
        </Text>
        <TouchableHighlight onPress={this.click}>
            <Text >Press!</Text>
        </TouchableHighlight>
      </View>
    );
  }
});
...

8.cd Example && react-native run-android

Cheers!!