找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 2273|回复: 3

[教程源码] 按键精灵手机版插件的开发标准草案与简易教程

[复制链接]
  • 打卡等级:行侠仗义

1080

主题

2100

回帖

4280

积分

院长

鲜花
6
猫粮
7394
QQ
发表于 2022-2-17 21:06:08 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
什么是按键精灵手机版插件?
按键精灵手机版可以通过Lua编程语言实现更加高级的功能扩展,例如大家熟知的紫猫插件就是通过Lua编程语言实现的。

为什么要制定插件开发标准草案?
一个优秀健壮的插件应该在不影响他人插件,不影响脚本自身等情况下提供可靠的功能扩展。
紫猫插件手机版在多年开发的过程中,踩过各种各样的坑,深知一个优秀插件开发的不易。
故本贴主旨是为了帮助大家少走弯路,通过统一的插件开发标准实现稳健的功能扩展。

本帖子适合哪些人阅读?
本帖子并非Lua编程教学,不适合对Lua编程语言完全不懂的人阅读。
如果你想要学习Lua编程,可以报名我们Lua课程学习,也可以查阅各种Lua文档资料学习。
本帖子主要以插件开发标准规范为主,适合已经会写一些Lua代码实现插件制作的人阅读。
如无特殊说明,本帖子中的代码均为Lua代码。




1. 按键精灵手机版插件开发简易教程

通过一个table类型的QMPlugin变量实现按键精灵调用Lua函数,例如

[Lua] 纯文本查看 复制代码
-- Lua代码
-- 实现两数相加求和
function QMPlugin.Add(a, b)
    return a + b
end


将以上代码保存成.lua文件,例如test.lua后,放入按键精灵手机助手的plugin文件夹内。
再重启按键精灵手机助手或者刷新插件列表,即可看到自己写的test插件了。
在按键精灵中调用代码如下

[按键精灵] 纯文本查看 复制代码
//按键精灵代码
Import "test.lua"   //导入插件

Dim 结果 = test.Add(11, 22)
TracePrint 结果 //输出结果为33


更多插件Lua源码例子可以参考按键精灵手机助手Plugin目录下的zmFile.lua文件。

2. 函数作用域的标准草案

所有变量与函数均使用local关键字定义成局部作用域(除了自带的QMPlugin外)。

因为多个插件是共用同一个全局作用域,假如插件A定义了一个全局函数Add,然后插件B也定义了同名但不同功能的全局函数Add,当脚本里同时加载这两个插件时,有一定概率会造成某一个插件内部Add函数被覆盖,导致该插件功能异常。例如紫猫插件V1版时期与山海师插件就造成过函数覆盖冲突,当然,现在的紫猫插件是不会与任何插件冲突了,因为插件内部全是局部作用域的函数。

一个简易的冲突例子演示,一个是相加功能的add,一个是返回第一个参数的add,但是由于全局冲突原因,导致有一定概率只返回一个功能。如果两个add函数前面加上local关键字即可完美解决该问题。
[Lua] 纯文本查看 复制代码
-- test1.lua插件代码
function add(a, b)
    return a + b
end

function QMPlugin.add(a, b)
    return add(a, b)
end

[Lua] 纯文本查看 复制代码
-- test2.lua插件代码
function add(a)
    return a
end

function QMPlugin.add(a)
    return add(a)
end

按键精灵调用出现异常的截图。
b1 (1).png

3. 函数定义的标准草案

定义两个局部作用域的table数据,一个用来存放对外公开函数,一个用来存放对内私有函数,注意不要跟插件文件同名。

例如以下插件源码
[Lua] 纯文本查看 复制代码
 --插件文件名请勿保存为zimao.lua, 否则会无法调用函数
local _zimao = {} --这是内部私有table函数
local zimao = {} --这是对外公开table函数
QMPlugin = zimao --通过这行代码, 实现将zimao表中所有函数对外公开

_zimao.ver = "20220217" --内部私有变量赋值, 

function _zimao.add(a, b) --内部私有函数定义, 按键精灵中无法使用该函数
    return a + b
end

function zimao.LuaVer() --定义对外公开函数, 即插件命令
    return _VERSION --返回按键精灵使用的Lua版本5.2
end


尽可能的减少其他多余的插件级局部变量,这里的插件级局部变量是指与以上源码中_zimaozimao相同作用域的变量。因为同级局部变量最多只能存放200个,这是紫猫插件命令功能首次超过200个时,遇到了错误:too many local variables (limit is 200) in main function near '='后,查阅资料得知,Lua使用一个栈存放它的寄存器,每个运行中的函数都有各自的一份活动记录,这些活动记录保存在栈中,内部存放每一个函数对应的寄存器。每条指令中只有8个bit用来标志寄存器,所以每个函数最多能够使用250个寄存器。为了空间与执行效率,Lua在lpaser.c源文件中定义了默认最多200个局部变量。但通过借助table数据类型,可以突破整个限制,减少局部变量总数。

所以这里我们推荐通过两个table变量来存放数据,适当的减少局部变量。当然,function函数内部的局部变量一般不会超过200个,所以函数中不用担心这个问题。

4. 函数结构的标准草案

为了避免插件内部各种意外错误导致脚本终止运行,推荐使用保护运行模式执行函数代码。这里提供一份Lua版的自定义 try...catch...finally 函数,利用这三个函数可以实现异常捕获处理,下面是这3个函数的用法,可以单独只用try,也可以使用try+catch,也可以try+catch+finally

[Lua] 纯文本查看 复制代码
local function try(block) -- 保护执行函数
    local tablejoin = function (...)
        local result = {}
        for _, t in ipairs({...}) do
            if type(t) == "table" then
                for k, v in pairs(t) do
                    if type(k) == "number" then table.insert(result, v)
                    else result[k] = v end
                end
            else
                table.insert(result, t)
            end
        end
        return result
    end
        
    -- get the try function
    local try = block[1]
    assert(try)
    
    -- get catch and finally functions
    local funcs = tablejoin(block[2] or {}, block[3] or {})
    
    -- try to call it
    local result_error = {}
    local results = {pcall(try)}
    if not results[1] then
        -- run the catch function
        if funcs and funcs.catch then
            result_error = {funcs.catch(results[2])}
        end
    end

    -- run the finally function
    if funcs and funcs.finally then
        local result_fin = {funcs.finally(table.unpack(results))}
        if #result_fin > 0 then
            return table.unpack(result_fin)
        end
    end

    -- ok?
    if results[1] and #results > 1 then
        return table.unpack(results, 2, #results)
    else
        if #result_error > 0 then
            return table.unpack(result_error)
        else
            return nil
        end
    end
end
local function catch(block) --异常捕获函数
    return {catch = block[1]}
end
local function finally(block) --最终必定执行函数
    return {finally = block[1]}
end

function test()
    return try {
        -- try 代码块
        function ()
            --这里写你的函数功能代码
            error("出错了")
        end,
 
        -- catch 代码块
        catch 
        {
            -- 发生异常后,被执行
            function (errors)
                --这里写异常处理代码
                print("执行错误:", errors)
            end
        },
 
        -- finally 代码块
        finally 
        {
            -- 最后都会执行到这里
            function (ok, errors)
                -- 如果try{}中存在异常,ok为true,errors为错误信息,否则为false,errors为try中的返回值
                print("必定执行内容:", ok, errors)
            end
        }
    }
end
 
test()


下面我们把这个保护执行加到我们的插件代码中,以下是一个完整的插件例子

[Lua] 纯文本查看 复制代码
local _zimao = {} --这是内部私有table函数
local zimao = {} --这是对外公开table函数
QMPlugin = zimao --通过这行代码, 实现将zimao表中所有函数对外公开

_zimao.ver = "20220217" --内部私有变量赋值, 

--插件规范仅要求少定义插件级局部变量,并非禁止定义,故可以适当定义一些常用的局部变量函数,减少代码量
local function traceprint(...)  --调用按键精灵的调试输出命令
    if QMPlugin then
        print(...)
    else
        local line = select(1, ...)
        if type(line) == "string" and line:match("^%_%d+$") then
            LuaAuxLib.TracePrint(line .. ":", table.concat({...}, " ", 2, select("#", ...)))
        elseif line == ":" and #{...} > 1 then
            LuaAuxLib.TracePrint(":", table.concat({...}, " ", 2, select("#", ...)))
        else
            LuaAuxLib.TracePrint(":","紫猫学院测试插件:", ...)
        end
    end
end

local function try(block) -- 保护执行函数
    local tablejoin = function (...)
        local result = {}
        for _, t in ipairs({...}) do
            if type(t) == "table" then
                for k, v in pairs(t) do
                    if type(k) == "number" then table.insert(result, v)
                    else result[k] = v end
                end
            else
                table.insert(result, t)
            end
        end
        return result
    end
        
    -- get the try function
    local try = block[1]
    assert(try)
    
    -- get catch and finally functions
    local funcs = tablejoin(block[2] or {}, block[3] or {})
    
    -- try to call it
    local result_error = {}
    local results = {pcall(try)}
    if not results[1] then
        -- run the catch function
        if funcs and funcs.catch then
            result_error = {funcs.catch(results[2])}
        end
    end

    -- run the finally function
    if funcs and funcs.finally then
        local result_fin = {funcs.finally(table.unpack(results))}
        if #result_fin > 0 then
            return table.unpack(result_fin)
        end
    end

    -- ok?
    if results[1] and #results > 1 then
        return table.unpack(results, 2, #results)
    else
        if #result_error > 0 then
            return table.unpack(result_error)
        else
            return nil
        end
    end
end
local function catch(block) --异常捕获函数
    return {catch = block[1]}
end
local function finally(block) --最终必定执行函数
    return {finally = block[1]}
end

function _zimao.add(a, b) --内部私有函数定义, 按键精灵中无法使用该函数
    return a + b
end


--以下是公开函数
--这是随机函数被try...catch...finally调用的例子
--由于随机函数不需要finally,故省略了,只写了try与catch
function zimao.RndNum(m, n)
    return try {
        function()
            --这里是随机函数的正式功能代码
            --大家自己插件的一个函数代码也可以从这里开始写
            --为了方便测试错误,注释掉了数据类型转换
            -- m = tonumber(m) or 0
            -- n = tonumber(n) or 0
            local max = ( m > n ) and m or n
            local min = ( m < n ) and m or n
            return math.random(min, max)
            --随机函数功能代码到这里就结束了
        end,
        catch {
            function (errors)
                --这里是当上面函数执行出现异常错误时候将会执行的代码
                --errors是上面函数执行后得到的错误信息
                --你也可以自己写一些其他错误处理代码
                traceprint("发生运行时错误!错误代码:RndNum(),错误信息:", errors)
            end
        }
    }
end

--这是一个读取文件的函数
--打开文件后不管是否出错,都要关闭文件,所以这里用到了finally
function zimao.Read(path)
    local f --由于文件句柄需要跨3个函数执行,所以这里定义放到了外面
    return try {
        function()
            -- 打开文件
            f = io.open(path, "r")
            if f == nil then
                return nil
            end
            -- 读取文件
            local ret = f:read("*all")
            -- 关闭文件
            f:close()
            return ret
        end,
        catch {
            function (errors)
                --如果出错了,将会执行这段代码
                --你也可以改成执行其他任何代码
                traceprint("发生运行时错误!错误代码:Read(),错误信息:", errors)
            end
        },
        finally {
            function ()
                --这里的代码是必定会被执行的
                --也就是不管读取函数是否会出错,这里都会进行一次判断是否需要关闭文件句柄。
                --为了证明本函数会执行, 加了一句输出, 正常不需要加
                if io.type(f) == "file" then
                    traceprint("文件未正常关闭, 现尝试重新关闭")
                    f:close()
                else
                    traceprint("文件已正常关闭, 不做任何操作!")
                end
            end
        }
    }
end

按键精灵执行效果截图
b2 (1).png

评分

参与人数 1猫粮 +5 收起 理由
szmy + 5 很给力!

查看全部评分

楼主热帖
  • 打卡等级:初涉江湖

0

主题

23

回帖

23

积分

按键电脑&手机班学员

鲜花
0
猫粮
189
发表于 2022-5-1 13:48:52 | 显示全部楼层
好 不赖 老花样新知识 再次总结 玩出新高度 打下江山 !
可难为我了 回复点什么 才不会重复呢 ~

0

主题

1

回帖

1

积分

学前班

鲜花
0
猫粮
1
发表于 2022-12-12 12:00:30 | 显示全部楼层
♪(^∇^*)

0

主题

3

回帖

3

积分

学前班

鲜花
0
猫粮
6
发表于 2023-5-6 10:23:40 | 显示全部楼层

好 不赖 老花样新知识 再次总结 玩出新高度 打下江山 !
可难为我了 回复点什么 才不会重复呢 ~
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|紫猫编程学园

GMT+8, 2024-3-29 03:42

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表