Luakit跨平台的客户端开发框架

Luakit的历史渊源

最近发布了一个跨平台的app开发框架Luakit 。那怎么会想到做这样一个东西呢?这要先说一下我参与过的一些项目,和在项目中接触到的一些技术点和对项目开发体检了,因为Luakit是集合了几个重要技术才能做到用Lua脚本来实现跨平台app开发的。

我主要参与的项目是QQMail的IOS版。在2017年下半年,由于机缘巧合,我参与开发了企业微信的一个分支版本,appstore上叫政务微信。QQMail的历史比较悠久了,在QQMail项目里我们使用了两项技术是比较特殊的,其他项目团队接触得比较少,一个是Lua脚本化技术,一个是orm技术。而在政务微信开发过程中是企业微信团队的跨平台开发技术给我留下很深印象,下面我首先简单介绍这几项技术。

  • 当时QQMail的Lua脚本化技术我们是基于wax来做的,只能在IOS上跑,不具备跨平台的能力。QQMail里面有几个版本中,整个记事本模块从底层逻辑层到界面渲染全部都用Lua来实现,脚本化过程中我们也克服了很多技术难点,例如如何在Lua脚本实现竞争式多线程,如何高效方便地在Lua环境实现数据存储之类的这些业界难题,当然了,脚本化之后我们也第一次吃到脚本化的甜头,最大的优点就是对代码的掌控能力大大提升,一个是可以随时上线,另外就是可以给不同的用户下发不同的代码。这个对于发现问题有很大的好处,当有用户投诉的时候,给用户下发特殊的debug代码,基本没有发现不了的问题。

  • orm技术我们组内同事的研究成果GYDataCenter,这个orm框架确实简单易用,可以大大减少数据库相关的开发量,当我们后来做政务微信的时候,项目里没有引入GYDataCenter,我们对直接裸写sql都非常的不适应,也极大的抵触。

  • 在深入接触政务微信后,我们感到企业微信客户端团队最有价值的技术是跨平台开发的技术,企业微信是基于chromium这套google开源的跨平台开发框架实现的业务跨平台的。跨平台的业务代码包括,线程模型,http短连接请求,请求调度,tcp长链接,数据库存储,数据包加解密等等,基本上除了界面,其他都放到了底层c++来实现了。当我们刚接触这种c++写的业务代码时,我们十分抵触,因为用c++开发会使复杂度大大提高,内存管理问题也是使用其他高级语言开发所不会碰到的。但是当项目继续下去,我们做了几个版本的业务的时候,慢慢的我们感觉到跨平台带来的好处了,虽然开发复杂,但是参考其他业务的代码,我们修改一下做新业务也不是太大的问题,最大的好处是只要开发一次,IOS和android就都work了,确实很高效。业务代码只有一份,bug也只有一份,一个平台修复了,另一个平台也可以享受到。

深入接触这几个框架后,我发现Lua跟chromium真是绝配,chromium提供跨平台的消息循环机制可以完美解决lua实现竞争式多线程的问题,在lua环境实现竞争式多线程(注意,不是单单线程安全)是使用lua开发的一个普遍性的难题,cocos2d-x的lua-binding也没解决这个问题,所以基于cocos2d-x lua版开发的游戏也很难做到全脚本化,因为Lua只能单线程。有了Luakit后,这类问题都有解决方案了。而lua的内存管理机制也可以很好的解决chromium用c++开发,内存管理和不适合函数式编程的最大的弊端,两者解合可以产生很好的效果。有了lua的多线程模型后,参考GYDataCenter的实现原理,我们可以实现一套lua版的orm框架,GYDataCenter只能在ios使用,现在lua版的orm框架可以具有跨平台的特性。

Luakit的功能简介

Luakit提供的很多强大的功能,这些功能都是可以跨平台运行的,Luakit主要包括以下功能接口

  • 多线程接口
  • orm模型接口
  • 文件操作接口
  • http请求
  • 异步socket接口
  • 全局通知机制
  • Lua代码加解密

下面简单介绍多线程接口,orm接口,http请求,异步socket接口和全局通知接口。

多线程模型

如何在Lua实现竞争式多线程我会再发一篇文章专门讲讲,因为这个问题是Lua领域的普遍存在的问题,有一定的技术意义。这里我先简单带过一下实现思路,一个lua解析器本身是不具备多线程能力,甚至不是线程安全的,但是在服务器开发上已经有人尝试起多条线程然后给每条线程配置独立的Lua解析器,然后多条线程通过一定的数据通道传输数据,通过这样的方式实现真正的多线程,但是这个思路一直没有延伸到客户端开发,主要原因是因为客户端通常把真正的线程隐藏起来,无论IOS或者android,都不能轻易地接触真正的线程,但是由于chromium提供了开源的线程模型,通过修改chromium的底层源码,生成消息循环时的给每个消息循环配置独立的lua解析器,这样最大的问题就得到了解决,下面看一下Luakit 提供的多线程接口。

创建线程 ,demo code

-- Parma1 is the thread type ,there are five types of thread you can create.
-- BusinessThreadUI
-- BusinessThreadDB
-- BusinessThreadLOGIC
-- BusinessThreadFILE
-- BusinessThreadIO
-- Param2 is the thread name
-- Result is new threadId which is the token you should hold to do further action
local newThreadId = lua.thread.createThread(BusinessThreadLOGIC,"newThread")

异步调用方法,类似IOS gcd中的 dispatch_async , demo code

-- Parma1 is the threadId for which you want to perform method
-- Parma2 is the modelName
-- Parma3 is the methodName
-- The result is just like you run the below code on a specified thread async
-- require(modelName).methodName("params", 1.1, {1,2,3}, function (p)
-- end)
lua.thread.postToThread(threadId,modelName,methodName,"params", 1.1, {1,2,3}, function (p)
    -- do something here
end)

同步调用方法,类似IOS gcd中的 dispatch_sync , demo code

-- Parma1 is the threadId for which you want to perform method
-- Parma2 is the modelName
-- Parma3 is the methodName
-- The result is just like you run the below code on a specified thread sync
-- local result = require(modelName).methodName("params", 1.1, {1,2,3}, function (p)
-- end)
local result = lua.thread.postToThreadSync(threadId,modelName,methodName,"params", 1.1, {1,2,3}, function (p)
    -- do something here
end)

orm接口

orm 模型的实现方法是参考IOS orm 开源库GYDataCenter的实现方法,GYDataCenter很依赖IOS gcd 的机制,Luakit中可以用新的lua多线程接口取代,可以做到同样的效果,下面罗列一下 orm demo code

Luakit 提供的orm框架有如下特征

  • 面向对象接口
  • 自动建表自动更新表结构和索引
  • 自带cache功能
  • 定时transaction
  • 线程安全,可以在任何线程发起数据库操作

定义数据模型, demo code

-- Add the define table to dbData.lua
-- Luakit provide 7 colum types
-- IntegerField to sqlite integer 
-- RealField to sqlite real 
-- BlobField to sqlite blob 
-- CharField to sqlite varchar 
-- TextField to sqlite text 
-- BooleandField to sqlite bool
-- DateTimeField to sqlite integer
user = {
    __dbname__ = "test.db",
    __tablename__ = "user",
    username = {"CharField",{max_length = 100, unique = true, primary_key = true}},
    password = {"CharField",{max_length = 50, unique = true}},
    age = {"IntegerField",{null = true}},
    job = {"CharField",{max_length = 50, null = true}},
    des = {"TextField",{null = true}},
    time_create = {"DateTimeField",{null = true}}
    },
-- when you use, you can do just like below
local Table = require('orm.class.table')
local userTable = Table("user")

插入数据, demo code

local userTable = Table("user")
local user = userTable({
    username = "user1",
    password = "abc",
    time_create = os.time()
})
user:save()

更新数据 demo code

local userTable = Table("user")
local user = userTable.get:primaryKey({"user1"}):first()
user.password = "efg"
user.time_create = os.time()
user:save()

select 数据,demo code

local userTable = Table("user")
local users = userTable.get:all()
print("select all -----------")
local user = userTable.get:first()
print("select first -----------")
users = userTable.get:limit(3):offset(2):all()
print("select limit offset -----------")
users = userTable.get:order_by({desc('age'), asc('username')}):all()
print("select order_by -----------")
users = userTable.get:where({ age__lt = 30,
    age__lte = 30,
    age__gt = 10,
    age__gte = 10,
    username__in = {"first", "second", "creator"},
    password__notin = {"testpasswd", "new", "hello"},
    username__null = false
    }):all()
print("select where -----------")
users = userTable.get:where({"scrt_tw",30},"password = ? AND age < ?"):all()
print("select where customs -----------")
users = userTable.get:primaryKey({"first","randomusername"}):all()
print("select primaryKey -----------")

联表查询,demo code

local userTable = Table("user")
local newsTable = Table("news")
local user_group = newsTable.get:join(userTable):all()
print("join foreign_key")
user_group = newsTable.get:join(userTable,"news.create_user_id = user.username AND user.age < ?", {20}):all()
print("join where ")
user_group = newsTable.get:join(userTable,nil,nil,nil,{create_user_id = "username", title = "username"}):all()
print("join matchColumns ")

http请求
Luakit提供了http请求接口,包括了请求队列调度控制, 实现代码, demo code

-- url , the request url
-- isPost, boolean value represent post or get
-- uploadContent, string value represent the post data
-- uploadPath,  string value represent the file path to post
-- downloadPath, string value to tell where to save the response
-- headers, tables to tell the http header
-- socketWatcherTimeout, int value represent the socketTimeout
-- onResponse, function value represent the response callback
-- onProgress, function value represent the onProgress callback
lua.http.request({ url  = "http://tj.nineton.cn/Heart/index/all?city=CHSH000000",
    onResponse = function (response)
    end})

异步socket接口
Luakit 提供了非阻塞的socket调用接口, demo code

local socket = lua.asyncSocket.create("127.0.0.1",4001)

socket.connectCallback = function (rv)
    if rv >= 0 then
        print("Connected")
        socket:read()
    end
end
    
socket.readCallback = function (str)
    print(str)
    timer = lua.timer.createTimer(0)
    timer:start(2000,function ()
        socket:write(str)
    end)
    socket:read()
end

socket.writeCallback = function (rv)
    print("write" .. rv)
end

socket:connect()

通知接口

app开发中经常会遇到需要一对多的通知场景,例如ios有系统提供Notification Center 来提供,为了跨平台的实现通知,Luakit也提供通知接口

Lua register and post notification, demo code

lua.notification.createListener(function (l)
    local listener = l
    listener:AddObserver(3,
        function (data)
            print("lua Observer")
            if data then
                for k,v in pairs(data) do
                    print("lua Observer"..k..v)
                end
            end
        end
    )
end);

lua.notification.postNotification(3,
{
    lua1 = "lua123",
    lua2 = "lua234"
})

Android register and post notification, demo code

LuaNotificationListener  listener = new LuaNotificationListener();
INotificationObserver  observer = new INotificationObserver() {
    @Override
    public void onObserve(int type, Object info) {
        HashMap<String, Integer> map = (HashMap<String, Integer>)info;
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            Log.i("business", "android onObserve");
            Log.i("business", entry.getKey());
            Log.i("business",""+entry.getValue());
        }
    }
};
listener.addObserver(3, observer);

HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("row", new Integer(2));
NotificationHelper.postNotification(3, map);

IOS register and post notification, demo code

_notification_observer.reset(new NotificationProxyObserver(self));
_notification_observer->AddObserver(3);
- (void)onNotification:(int)type data:(id)data
{
    NSLog(@"object-c onNotification type = %d data = %@", type , data);
}

post_notification(3, @{@"row":@(2)});

结语

在腾讯我也接触过不少项目(参与开发或者了解代码),每个项目都会发展出一套属于自己的基础架构,基础架构的选择通常都会根据自己原有的知识体系搭建,这个基本无一例外,习惯用chromium的团队,所有由那个团队建立的项目都会基于chromium做基础架构,如果没有特别熟悉的,就会使用原生提供的接口搭建自己的基础架构,这个无可厚非,但是选择完基础架构后,基本上app的素质就已经定下来,能不能跨平台,数据能不能支持orm,代码能不能热更新,所有这些基本能力都已经定下来了,后续加入团队的人无论多牛都只是在原有基础上添砖加瓦,修修补补,大动筋骨通常都有很大代价的。所有我认为对项目的技术负责人来说,选择什么基础架构这件事是再重要不过了,项目中后期花无数个晚上来解决不知从何查起的bug,对投诉无能为力,没有足够的工具来快速响应,大量重复代码,不断反复的bug,这些问题有可能看似是一个刚入职的工程师的疏忽或者设计不当,其实大部分的原因从选择基础架构的时候已经注定了,日后代码的复杂度,app具有的能力,早就已经定下了。

Luakit 是我暂时知道的最高效的基础架构,因为它具有以下特点

  • 跨平台(千万别小看这特性,效率是成倍提升的,企业微信底层代码可以跨平台运行才能如此高效的完成几个平台的开发并迅速推出市场)
  • 支持orm存储
  • 脚本化(脚本化的优势在于可以随时发布,可以给不同的用户下发不一样的代码,这点对定位问题有很大好处)

最后,希望大家可以多了解,试用Luakit ,有问题可以发邮件到williamwen1986@gmail.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容