`
sd6733531
  • 浏览: 66684 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

android PhoneGap源码详解

阅读更多

传送门:

PhoneGap源码详解一

PhoneGap源码详解二

PhoneGap源码详解三

PhoneGap 源码解析

之前有一位前辈已经写了 PhoneGap android 源码的解析。但是,前辈写得比较简单,只是把通信原理提了一提。本篇源码解析,会对 PhoneGap 做一个全面的介绍。

关于 Java/JS 互调,鄙人接触也有一段时间了。在 android sdk 文档中 , 也有用 JsInterface loadUrl 做到交互的示例。但令我惊讶的是 ,PhoneGap 并没有选择用 JsInterface ,而是使用拦截 prompt 这种 hack 做法。

PhoneGap android 源码写得稍稍有点凌乱和啰嗦,后面会详细解析。好了,不废话了。开始正文了

一、JS 层与 Native 层之间通信原理

在讲解这部分之前,我先解释 PhoneGap 中的插件的概念。

Plugin: 插件。插件具备标准 js 没有的功能,如打电话、查看电池状态。这部分功能需要通过本地代码调用实现。每个插件都会对外提供至少一个方法。

lib/common/notification.js 这个插件。它具备了 alert,confirm,vibrate( 震动 ),beep( 蜂鸣 ) 这几个方法。

很显然,编写插件有两个要点。首先 , 需要编写一个实现插件功能的本地代码。其次,需要编写一个暴露调用接口的 js 代码来供使用插件者调用。

当编写完插件后,问题就来了。 Js 接口代码怎么去调用本地代码 ? 本地代码执行完毕后,怎么去回调 Js? 如何处理同步回调和异步回调 ? 这些通信问题的解决才是 PhoneGap 框架的精华所在。

下面我们逐一看看 phoneGap 是如何解决这些问题的。

      1. Js 接口代码怎么去调用本地代码 ?

lib/android/exec.js, 我们找到一个称为 exec 的关键模块。它是 js 层调用本地代码的入口。

它的定义是 exec(success, fail, service, action, args) 。顺便多说一句 , 虽然 exec PhoneGap 的一个关键模块,但由于受到平台差异影响,各个平台 exec 的实现方式并不相同。

 

 

var r = prompt(JSON.stringify(args), "gap:"+JSON.stringify([service, action, callbackId, true]));
 

 

 

这句 prompt 便实现了本地代码调用。本地代码通过 WebChromeClient 拦截 onJsPrompt 回调,利用 gap: 开头标志得知是调用本地插件请求 , 然后向 PluginManager 转发该请求。 PluginManager 将会根据参数来查找并执行具体插件方法。 关于 PluginManager, 后面会做更详细的解释。

      2. 本地代码怎么去回调 Js?

PhoneGap 并没有简单的用 loadUrl 来实现回调,而是在本地层建立了一个 CallBackServer 。由 Js 层不断向 CallBackServer 请求回调语句 , 然后 eval 执行该回调。

CallBackServer 提供了两种模式 , 一种是基于 XMLHttpRequst ,一种是基于轮询。 XHR 的方式即 js 层不断向 CallBackServer 发送 XMLHttpRequest 请求 , CallBackServer 则将回调语句返回给 js 层。

轮询方式则是 js 层通过 prompt 向本地发送 poll 请求 , 本地将从 CallBackServer 中拿出下一个回调返回给 js 层。

Js 层相关的 XHR 和轮询实现请参考 lib/android/plugin/android/callback.js, 以及 lib/android/plugin/android/polling.js

通过阅读 CallBackServer 的源码可知,当 url 为本地路径时,默认将启用 XHR 方式。

           3.  如何处理同步回调和异步回调 ?

先说同步处理。从 js prompt WebChromeClient onJSPrompt 是一个跨线程的同步调用。图示如下




通过 prompt 便可以直接得到 Plugin 执行的结果。后续做同步回调便也非常简单了。

接着再说说异步回调是如何实现的。注意在 exec.js 的注释中 , 作者写道

 

The native side can return:
Synchronous: PluginResult object as a JSON string
Asynchrounous: Empty string ""

 

 

为了区别异步和同步。若 prompt 返回的是空字符串,那么将认为是异步调用。此时 PhoneGap 会在 JS 层保留回调函数,待本地层向 CallBackServer 发送回调后进行执行。

也许你会问 , 本地层怎么区别哪个回调啊 ?PhoneGap 对此的处理十分简单,在 cordova.js 中定义了一个 callbackId 的自增种子,并将每个 callBack 插入 callBacks 中去。无论同步异步,每个 plugin 调用都将得到一个流水号码作为回调标识。这个回调标识在 prompt 阶段便传递到了本地层。当本地层的 Plugin 异步结束后,便可以根据该 callbackId 找到回调。并向 CallBackServer 发送回调通知。图示如下



 

                           

二、 PhoneGap Native 层解析

与本地 Plugin 通信密切相关的是 :Plugin,PluginManager,PluginResult,CallbackServer

Plugin 是本地层所有插件的抽象基类,所有子插件都必须继承 Plugin 并实现 Plugin execute 方法。如下代码是一个极为简单的实现本地启动界面的插件。

 

package org.apache.cordova;
 
import org.apache.cordova.api.Plugin;
import org.apache.cordova.api.PluginResult;
import org.json.JSONArray;
 
public class SplashScreen extends Plugin {
 
    @Override
    public PluginResult execute(String action, JSONArray args, String callbackId) {
        PluginResult.Status status = PluginResult.Status.OK;
        String result = "";
 
        if (action.equals("hide")) {
            ((DroidGap)this.ctx).removeSplashScreen();
        }
        else {
            status = PluginResult.Status.INVALID_ACTION;
        }
        return new PluginResult(status, result);
    }
}

 

 

上述实例展现了一个典型的 execute 处理流程。首先 , 根据 action 判断插件需要执行的动作方法,处理后返回一个 PluginResult

 

PluginResult 表示插件执行结果的实体。它主要包含了三个字段,分别是 status: 状态码, message,keepCallBack

最基本的 status 状态码分别是 OK( 成功 ),NO_RESULT( 没有结果 ),Error( 失败 ) ,另外 status 还定义许多失败的具体异常码。

message 是返回的结果实体, message 将作为参数传入回调函数中。

keepCallBack 表示是否需要保持回调。如果该项为 false ,那么在 JS 层在执行回调后将立即删除回调以释放资源。

其两个工具方法 :toSuccessCallBackString toErrorCallbackString 将生成一个 JS 回调语句。配合 CallBackServer 实现了 Native JS 回调。

 

所有的 Plugin 都由 PluginManager 托管。 Js 端调用 Native 代码时 ,onJSPrompt 会将请求转发给 PluginManager, PluginManager 便会负责查找并执行 Plugin

 

从上面所说可以看出, PluginManager 非常重要。首先 , 从最重要的 exec 看起。

 

    public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
        PluginResult cr = null;
        boolean runAsync = async;
        try {
            final JSONArray args = new JSONArray(jsonArgs);
            final IPlugin plugin = this.getPlugin(service);
            final CordovaInterface ctx = this.ctx;
            if (plugin != null) {
                runAsync = async && !plugin.isSynch(action);
                if (runAsync) {
                    // Run this on a different thread so that this one can return back to JS
                    Thread thread = new Thread(new Runnable() {
                        public void run() {
                            try {
                                // Call execute on the plugin so that it can do it's thing
                                PluginResult cr = plugin.execute(action, args, callbackId);
                                int status = cr.getStatus();
 
                                // If no result to be sent and keeping callback, then no need to sent back to JavaScript
                                if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
                                }
 
                                // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
                                else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
                                    ctx.sendJavascript(cr.toSuccessCallbackString(callbackId));
                                }
 
                                // If error
                                else {
                                    ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
                                }
                            } catch (Exception e) {
                                PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
                                ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
                            }
                        }
                    });
                    thread.start();
                    return "";
                } else {
                    // Call execute on the plugin so that it can do it's thing
                    cr = plugin.execute(action, args, callbackId);
 
                    // If no result to be sent and keeping callback, then no need to sent back to JavaScript
                    if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
                        return "";
                    }
                }
            }
        } catch (JSONException e) {
            System.out.println("ERROR: " + e.toString());
            cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION);
        }
        // if async we have already returned at this point unless there was an error...
        if (runAsync) {
            if (cr == null) {
                cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
            }
            ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
        }
        return (cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }");
}

 

 

exec.js,PluginManager,Plugin 构成了经典的 Command/Action 模式。

以本篇日志中的图示为例 (http://www.cnblogs.com/springyangwc/archive/2011/04/13/2015456.html )



exec.js 便对应着玉皇大帝,其面向的是 client, 期望调用的是具体 plugin( 美猴王 ) 的具体方法 ( 上天 ) 。然而 exec.js 只管向 PluginManager( 太白金星 ) 发送指示。 PluginManager( 太白金星 ) 管理所有的 Plugin( 小仙 ) 。它接到通知后,将会根据指示向具体的 Plugin 发出通知。具体的 Plugin( 美猴王 ) 接到通知后,执行动作 (execute) ,并根据 action 来区分具体操作。

由于 PluginManager 自身对所有的 Plugin 进行了管理,因此其可以很轻松的通过 service 找到对应的 Plugin 。然后想 Plugin 转发该 action

其中的 asyn 参数比较特殊,其封装了 Plugin 的异步执行模式。要想 Plugin execute 在线程中执行,必须具备两个条件。其一是 js “下旨”给 PluginManager 的时候表示希望异步调用。其二是 Plugin 自身是允许异步执行的。通过查看源代码,可以发现 js 端默认都是希望异步调用,因此是否开启异步模式将由 Plugin isSync 决定。

PluginManager 载入 Plugin 的方式其实非常简单。主要是通过读取 plugins.xml 中的配置。配置中的 name service 对应 ,value Plugin 的类路径对应。 PluginManager 载入 Plugin 是通过反射空构造器实现,因此需要特别注意自定义的 Plugin 不要有带参构造器。

PluginManager Plugin 的管理还包含广播生命周期以及广播消息的功能。其中生命周期方法 onResume,onPause,onDestroy 其实是和 web 页面生命周期密切相关的。 ( 而不是 Activity, 注意与 js 层的 onResume,onPause 有很大区别 !) 这点从 DroidGap loadUrlIntoView 中可以看出。至于广播消息,则是 Plugin 框架的一个比较有趣的地方。

我们在 NetWorkManager 插件中看到这样一段代码 :

 

    /**
     * Create a new plugin result and send it back to JavaScript
     *
     * @param connection the network info to set as navigator.connection
     */
    private void sendUpdate(String type) {
        PluginResult result = new PluginResult(PluginResult.Status.OK, type);
        result.setKeepCallback(true);
        this.success(result, this.connectionCallbackId);
       
        // Send to all plugins
        this.ctx.postMessage("networkconnection", type);
}

 

 

DroidGap 代理了 PluginManager postMessage 方法,此处实际是请求 PluginManager 向所有的 Plugin 广播网络切换的事件。如果其他的 Plugin 关心网络切换事件 , 只需要覆盖 onMessage 方法即可。这样就实现了 Plugin 插件之间的交互。

 

最后一块硬骨头是 CallBackServer 。代码行数其实一点也吓不倒人,短短 400 行而已。首先从轮询模式开讲,当载入的 url 不是本地页面时,由于受到跨域限制,将强制切换成轮询模式。注意 getJavascript sendJavascript 这两个方法。

前面说过, CallBackServer 是异步回调的基础。我们来看看轮询下的异步回调究竟是怎么玩儿的。

来看看 BatteryListener 插件 , 下面是它的 execute 方法

注意 action start 时候 PluginResult 的返回。它返回了 NO_Result keepCallback PluginResult exec.js 接到该返回后将保持该回调。在 start 的同时 ,batteryListener 还保存了 callbackId 。那么 , 当接到 BroadCastReceiver 的通知后 , 怎么异步回调的呢 ?

  /**

     * Updates the JavaScript side whenever the battery changes
     *
     * @param batteryIntent the current battery information
     * @return
     */
    private void updateBatteryInfo(Intent batteryIntent) {   
        sendUpdate(this.getBatteryInfo(batteryIntent), true);
    }
   
    /**
     * Create a new plugin result and send it back to JavaScript
     *
     * @param connection the network info to set as navigator.connection
     */
    private void sendUpdate(JSONObject info, boolean keepCallback) {
              if (this.batteryCallbackId != null) {
                       PluginResult result = new PluginResult(PluginResult.Status.OK, info);
                       result.setKeepCallback(keepCallback);
                       this.success(result, this.batteryCallbackId);
              }
    }

 

其最终调用了 success 方法。 Success 方法将 PluginResult 包装成回调语句 , 并通过 DroidGap CallBackServer sendJavaScript

由此为止,本地层的异步 sendJavaScript 已经完成了。接下来的问题便是 ,JS 层如何 getJavaScript ? lib/android/plugin/android/polling.js , 可以看到 js 层获取回调的轮询实现。

 

 
   polling = function() {
      // Exit if shutting down app
      if (cordova.shuttingDown) {
          return;
      }
 
      // If polling flag was changed, stop using polling from now on and switch to XHR server / callback
      if (!cordova.UsePolling) {
          require('cordova/plugin/android/callback')();
          return;
      }
 
      var msg = prompt("", "gap_poll:");
      if (msg) {
          setTimeout(function() {
              try {
                  var t = eval(""+msg);
              }
              catch (e) {
                  console.log("JSCallbackPolling: Message from Server: " + msg);
                  console.log("JSCallbackPolling Error: "+e);
              }
          }, 1);
          setTimeout(polling, 1);
      }
      else {
          setTimeout(polling, period);
      }
};

 

 

通过 setTimeout 构成了一个死循环,通过 prompt 不断向本地层请求 gap_poll 。本地层收到 gap_poll 请求后,将会调用 CallBackServer getJavaScript 并同步返回给 polling Polling 接到回调后 , 通过 eval 便完成了 js 端回调代码的执行。

XHR 的方式与轮询其实类似 ,js 端的源码可以查看 lib/android/plugins/android/callback.js

最后给一张简单的静态结构图




 

CordovaInterface 中包含一些鸡肋的 url 白名单以及启动 Dialog 。虽然写得非常长 , 但如果了解整套 Plugin 机制的话,看下来还是小 case 的,这里就不赘述了。

三、 PhoneGap js 层源码

PhoneGap js 层源码的模块化机制和启动还是挺有趣的。下次码好字了传给大家看 J

 

  • 大小: 45.3 KB
  • 大小: 47.4 KB
  • 大小: 49.8 KB
  • 大小: 7.3 KB
分享到:
评论
6 楼 异域的梅 2013-01-06  
分析的很消耗,先学习着,不懂再问
5 楼 wmyzcs 2012-07-28  
源码下载地址是什么啊
4 楼 sd6733531 2012-05-07  
华天下 写道
我想清楚
sd6733531 写道
华天下 写道
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?



大哥的猜测我十分认同。xhr很可能是考虑性能比polling好



我想清楚了,由于javascript无法多线程,所以不断用polling,会造成其它插件执行时候有阻塞, 因为其它插件调用本地的时候跟polling用的是同一个入口。


大哥所言甚是。正解!待过两天码字将javascript层源码解析放上来后,欢迎再来讨论!
3 楼 华天下 2012-05-07  
我想清楚
sd6733531 写道
华天下 写道
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?



大哥的猜测我十分认同。xhr很可能是考虑性能比polling好



我想清楚了,由于javascript无法多线程,所以不断用polling,会造成其它插件执行时候有阻塞, 因为其它插件调用本地的时候跟polling用的是同一个入口。
2 楼 sd6733531 2012-05-07  
华天下 写道
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?



大哥的猜测我十分认同。xhr很可能是考虑性能比polling好
1 楼 华天下 2012-05-05  
少年? 你弄清楚为什么会有xhr和poll两种方式存在了吗? 单用poll我觉得是可以解决问题的。xhr显得有点打酱油,难道是用来避免用poll方式的时候,频繁调用onjsprompt造成的性能问题?

相关推荐

    第11讲:深入理解指针(1).pdf

    第11讲:深入理解指针(1)

    springboot整合 freemarker方法

    springboot整合 freemarker方法

    第14讲:深入理解指针(4).pdf

    第14讲:深入理解指针(4)

    同行者4.1.2语音助手

    《同行者4.1.2语音助手:车机版安装详解》 在现代科技日新月异的时代,智能车载设备已经成为了汽车生活的重要组成部分。"同行者4.1.2"便是这样一款专为车机设计的语音助手,旨在提供更为便捷、安全的驾驶体验。该版本针对掌讯全系列设备进行了兼容优化,让车主能够轻松实现语音控制,减少驾驶过程中的手动操作,提升行车安全性。 我们来了解下"同行者4.1.2"的核心功能。这款语音助手集成了智能语音识别技术,用户可以通过简单的语音指令完成导航、音乐播放、电话拨打等一系列操作,有效避免了因操作手机或车机带来的分心。此外,其强大的语义理解和自学习能力,使得它能逐步适应用户的口音和习惯,提供更个性化的服务。 在安装过程中,用户需要注意的是,"同行者4.1.2"包含了四个核心组件,分别是: 1. TXZCore.apk:这是同行者语音助手的基础框架,包含了语音识别和处理的核心算法,是整个应用运行的基础。 2. com.txznet.comm.base.BaseApplication.apk:这个文件可能包含了应用的公共模块和基础服务,为其他组件提供支持。 3. TXZsetting.apk:这

    市场拓展主管绩效考核表.xls

    市场拓展主管绩效考核表

    “线上购车3D全方位体验:汽车模型展示与个性化定制功能”,three.js案例- 线上购车3d展示(源码) 包含内容:1.汽车模型展示;2.汽车肤;3.轮毂部件更;4.开关车门动画;5.汽车尺寸测量

    “线上购车3D全方位体验:汽车模型展示与个性化定制功能”,three.js案例- 线上购车3d展示(源码) 包含内容:1.汽车模型展示;2.汽车肤;3.轮毂部件更;4.开关车门动画;5.汽车尺寸测量;6.自动驾驶;7.镜面倒影;8.hdr运用;9.移动端适配; 本为html+css+three.js源码 ,核心关键词:three.js案例; 线上购车3D展示; 汽车模型展示; 汽车换肤; 轮毂部件更换; 开关车门动画; 汽车尺寸测量; 自动驾驶; 镜面倒影; HDR运用; 移动端适配; HTML+CSS+three.js源码。,"Three.js源码:线上购车3D展示案例,含汽车模型、换肤、轮毂更换等九大功能"

    (数据权威)中国城市_县域统计面板数据二合一

    数据名称:2000-2022年各县市区主要社会经济发展指标面板数据 数据类型:dta格式 数据来源:中国县域统计

    120页-环卫车项目初步方案.pdf

    一、智慧环卫管理平台的建设背景与目标 智慧环卫管理平台的建设源于对环卫管理全面升级的需求。当前,城管局已拥有139辆配备车载GPS系统、摄像头和油耗传感器的环卫车辆,但环卫人员尚未配备智能移动终端,公厕也缺乏信息化系统和智能终端设备。为了提升环卫作业效率、实现精细化管理并节省开支,智慧环卫管理平台应运而生。该平台旨在通过信息化技术和软硬件设备,如车载智能终端和环卫手机App,实时了解环卫人员、车辆的工作状态、信息和历史记录,使环卫作业管理透明化、精细化。同时,平台还期望通过数据模型搭建和数据研读,实现更合理的环卫动态资源配置,为环卫工作的科学、健康、持续发展提供决策支持。 二、智慧环卫管理平台的建设内容与功能 智慧环卫管理平台的建设内容包括运行机制体制建设、业务流程设计、智慧公厕系统建设、网络建设、主机和储存平台需求、平台运维管理体系、硬件标准规范体系以及考核评价体系等多个方面。其中,智慧公厕系统建设尤为关键,它能实时监控公厕运行状态,保障公厕的清洁和正常运行。平台建设还充分利用了现有的电子政务网络资源,并考虑了有线和无线网络的需求。在功能上,平台通过普查、整合等手段全面收集环卫车辆、企业、人员、设施、设备等数据,建立智慧环卫基础数据库。利用智能传感、卫星定位等技术实现环卫作业的在线监管和远程监控,实现对道路、公共场所等的作业状况和卫生状况的全面监管。此外,平台还建立了环卫作业网格化管理责任机制,实现从作业过程到结果的全面监管,科学评价区域、部门、单位和人员的作业效果。 三、智慧环卫管理平台的效益与风险规避 智慧环卫管理平台的建设将带来显著的环境、经济和管理效益。环境方面,它将有力推进环境卫生监管服务工作,改善环境卫生状况,为人民群众创造更加清洁、卫生的工作和生活环境。经济方面,通过智慧化监管,大大降低了传统管理手段的成本,提高了监管的准确性和效率。管理方面,平台能够追踪溯源市民反映的问题,如公厕异味、渣土车辆抛洒等,并找到相应的责任单位进行处置,防止类似事件再次发生。同时,平台还拥有强大的预警机制功能,能够在很多环卫问题尚未出现前进行处置。然而,平台建设也面临一定的风险,如部门协调、配合问题,建设单位选择风险以及不可预测的自然灾害等。为了规避这些风险,需要加强领导、统一思想,选择优秀的系统集成商承接项目建设,并做好计算机和应用系统的培训工作。同时,也要注意标准制定工作和相关法律法规的制定工作,以保证系统建设完成后能够真正为环卫管理工作带来便利。

    36 -企业管理主管绩效考核表1.xlsx

    36 -企业管理主管绩效考核表1

    1.1 -1.4 工程代码

    1.1 -1.4 工程代码

    USDT合约,USDT智能合约

    USDT合约,USDT智能合约

    基于姿态估计三维人脸形状重建.pdf

    基于姿态估计三维人脸形状重建.pdf

    一般员工绩效考核表模板(通用版) (2).xls

    一般员工绩效考核表模板(通用版) (2)

    全国295个地级市2011-2022互联网宽带接入用户数互联网普及率(数据权威)

    全国各省295地级市互联网普及率、互联网用户数、每百人互联网宽带用户(2011-2022年) 数据年份:2011-2022年(2022存在部分缺失) 数据范围:全国各省295个地级市 数据来源:地方统计局

    (数据权威)碳排放、碳中和、碳交易、碳金融、碳计算、碳建模资料

    一、各省、分行业CO2排放、283个地级市碳排放及计算过程 2.分行业二氧化碳排放量 在这里插入图片描述 3、280多个地级市碳排放及计算过程 二、碳中和文献、最新政策、碳金融数据+数学建模 1.二氧化碳减排规划,碳金融数据收集及数学建模 2.碳中和政策和下载量最高的碳中和论文 三、碳排放+碳市场+碳交易+碳中和+碳排放核算Excel自动计算表 全行业碳排放核算Excel自动计算表 四、碳交易数据 五、主要能源碳排放计算参数

    第20讲:自定义类型:结构体.pdf

    第20讲:自定义类型:结构体

    视觉跟踪算法综述.pdf

    视觉跟踪算法综述.pdf

    MATLAB超效率SBM-DEA模型代码详解:简易操作指南及期望与非期望产出的超效率分析,附Malmquist指数与分解功能,MATLAB的超效率SBM-DEA模型代码(有安装教程和内容讲解之类的东西

    MATLAB超效率SBM-DEA模型代码详解:简易操作指南及期望与非期望产出的超效率分析,附Malmquist指数与分解功能,MATLAB的超效率SBM-DEA模型代码(有安装教程和内容讲解之类的东西),操作很简单 可以做期望产出和非期望产出的超效率和非超效率sbm模型和Malmquist指数和分解 ,MATLAB; SBM-DEA模型; 超效率SBM-DEA; 安装教程; 内容讲解; 期望产出; 非期望产出; 超效率与非超效率sbm模型; Malmquist指数; 分解。,"MATLAB超效SBM-DEA模型代码:非期望产出分析的便捷工具"

    人事行政主管绩效考核评分表.xls

    人事行政主管绩效考核评分表

    人力资源管理工具绩效考核excel模板.xlsx

    人力资源管理工具绩效考核excel模板

Global site tag (gtag.js) - Google Analytics