每次把做好的MATLAB界面发给同事,我都要先补一句:你电脑上装MATLAB了吗?没装?那你先去装个MATLAB,再装对应工具箱,再把路径配好,再把你那个.mlapp拖进运行……这一套下来,光教环境就耗掉半小时。更麻烦的是,你辛辛苦苦做好的GUI程序,交到对方手里明明只是点几个按钮、看几张图的小功能,却要让人家为一个工具去安十多个G的开发环境,这搁谁都觉得不划算。
所以很多工程师做到一定阶段都会主动研究打包,把MATLAB程序编译成不带MATLAB环境也能跑的独立exe。这事官方提供了一套挺成熟的东西,就是Application Compiler。我在R2020a和R2020b上都实测过完整流程,从GUI代码编写、依赖检查、编译打包,到拿到一台没有MATLAB的机器上部署运行,整个过程跑通之后,我最大的感受就是:这个功能真的把“开发”和“使用”彻底分开了,但这中间坑也确实不少。
这篇文章我按自己实际操作的顺序来写,从为什么选这套方案、打包前要准备什么,到逐个按钮怎么点,再到最后一份问题排查清单,尽量把每个细节都说到位。用的是2020R系列版本,其他版本界面略有差异,但核心逻辑是一样的。
1. 为什么要把GUI打包成独立exe,以及这套方案是怎么工作的
1.1 从一次“给同事发程序”说起
大概一年多前,我给测试组同事做了个信号预览GUI,功能不复杂,就是从文件夹里批量读数据文件,画个波形,能缩放能导图。当时我直接把代码发过去,让他自己跑。结果他打开MATLAB发现缺工具箱,装了半天工具箱又说License不对,折腾一上午,最后跑起来一看,路径里带中文,数据文件夹又读不到。那次之后我就下决心,以后再给人交东西,绝不给源码和MATLAB环境要求,只给一个能双击打开的exe。
不只是同事之间协作,很多场景都有这个需求:给甲方做演示工具、给实验室搭数据采集界面、给非技术背景的客户交付算法验证程序、甚至给自己保存一个“以后不用开MATLAB就能用”的版本。这时候一个独立exe的意义就很直接:收程序的人不需要知道MATLAB是什么,双击图标,界面出来,事情就结束了。
1.2 exe里面到底打包了什么:它不是一个自包含的绿色软件
这里要先说清楚一个常见误解。用Application Compiler生成的exe,并不是把所有MATLAB函数都编译成机器码塞进一个文件里,然后这台机器就什么依赖都没有了。它的真实模型是:MATLAB的编译工具把你的代码(包括GUI里的回调函数、工具箱调用)转换成一堆受保护的中间代码,再配上你用到的那些工具箱对应的动态库,最后放进一个包里。exe本身更像一个“启动器”,真正干活的计算引擎是MATLAB Runtime,也就是以前叫MCR的东西。
Runtime可以理解成一个“只运行、不开发”的MATLAB运行环境,体积不小,R2020a对应版本是9.8,安装完差不多2到3个GB。但它可以在一台机器上装好之后供多个打包程序共用,很多公司会给公用电脑装一次Runtime,之后所有同事发的exe就都能跑了。这也是为什么同样是打包,有些选项要带Runtime一起装,有些选项不带,后面实操部分我会详细说。
正因为有这层结构,你在判断“打包成功”的时候,也要多一步验证:同样的exe在你自己机器上能跑,不算数。一定要复制到一台干净、没装过MATLAB的机器上测一遍,这步才是真正的验收。
1.3 可选方案横向对比
其实想把MATLAB程序给别人用,思路不止打包exe一种,我在确定方案前也对比过几条路。
| 方案 | 目标机器要求 | 交付体积 | 优点 | 明显缺点 |
|---|---|---|---|---|
| 让对方装完整MATLAB | 需要完整License | 10GB以上 | 源码透明,方便二次修改 | 安装成本高,授权贵,看着就不专业 |
| 打包exe + 安装Runtime | 只需装Runtime,无需License | Runtime约2-3GB + exe包几十MB | 一次Runtime多次使用,运行速度快 | 首次安装Runtime略麻烦 |
| 打包exe,目标机器已有Runtime | 只需拷贝exe和dll | 几十MB | 分发最简单 | 要求对方机器Runtime版本与编译版本兼容 |
| Web App Server(R2020b+) | 浏览器访问 | 取决于服务器 | 不需要安装任何东西 | 需要一台常驻服务器,配置与运维成本高 |
| 给源码让用户自己处理 | 需要MATLAB环境 | 源码文件很小 | 最省事 | 对接收方极不友好,问题最多 |
我自己最后一直是“exe + Runtime”这条路。R2020b之后官方还提供Web App Server方案,能把GUI发成网页版,这适合给多人用的内部工具,但如果你只是交付给固定几个人,打包exe仍然是最省心、最可控的做法。
2. 打包前准备:90%的新手都在这一步卡住
2.1 先确认你的License和工具链是完整的
很多人打开Application Compiler发现按钮是灰的,或者打包到一半提示没有Compiler授权,问题通常出在MATLAB Compiler工具箱没装。检查方法很简单,在MATLAB命令窗口输入:
license('test', 'Compiler')返回1就说明授权可用,返回0说明当前License不支持编译。R2020a和R2020b对MATLAB Compiler的授权逻辑基本一致,就是要确保购买或部署的License里包含这个组件。
另外一个很容易被忽略的问题是:你代码里用到的工具箱函数,编译授权也要覆盖。绝大多数打包失败不是语法错误,而是某个函数对应的工具箱虽然在开发机上能用,但该工具箱没有被Compiler支持,或者License不允许被部署。比如MATLAB Coder、Simulink这类组件,Application Compiler是不能直接处理的,后者需要的是Simulink Compiler,标题里写的“GUI程序打包”一般用不到这些,但如果你在GUI里调用了Simulink模型,就要特别注意。
2.2 学会用代码查看依赖:requiredFilesAndProducts
我强烈建议不要等到点Package了才发现缺东西。打包之前先跑一条命令,让MATLAB自己列出程序依赖的所有文件和工具箱:
[files, products] = matlab.codetools.requiredFilesAndProducts('myApp.m'); files' products这里的myApp.m换成你的GUI主函数文件名。执行之后,files会列出你这个程序真正依赖的.m文件,products会列出依赖的工具箱及对应Matlab版本号。如果你发现某个你自己写的辅助函数没有出现在列表里,说明你的主程序里没有静态调用它,编译器很可能也不会自动带上它,这时候就要手动补。
这个命令对我们的打包思路是一个很好的体检:它告诉我程序“真正需要什么”,避免我凭印象猜。我在一次实际项目里就是用它发现GUI里动态加载的一个数据解析脚本没被列进依赖,当时要是不查,打包出来的exe到别人机器上一跑,点按钮就报“未定义函数或变量”,那场面很难收场。
2.3 把GUI工程整理成适合编译的结构
我踩过不少次坑之后,总结出一个比较保险的工程目录结构,现在每次打包前都先把代码整理成这个形态:
project_root/ ├── mainApp.m % 主函数,入口 ├── helper_funcs/ % 其他自定义函数 │ ├── loadData.m │ ├── plotWave.m │ └── exportFig.m ├── resources/ % 界面用的图标、示例数据、配置文件 │ ├── logo.png │ └── config.ini └── package_out/ % 打包输出目录,这个提前建好如果你用的是App Designer,主程序是一个.mlapp文件,那结构也可以把.mlapp当作入口文件放到根目录,手动添加资源文件夹的时候把resources一起加进去就行。
为什么这么强调结构?因为Application Compiler虽然会自动扫描依赖,但它扫描的规则是基于主程序里的“静态引用”——也就是代码里直接写明的那些函数调用。如果代码里用了eval、feval、str2func这种动态调用,或者脚本运行时才拼接函数名,编译器是没法自动发现并打包的。把这些文件统一放在一个明确目录下,打包时手动添加整个目录,就不容易漏。
2.4 主文件必须是函数,不是脚本
这是所有新手最容易翻车的一步。Application Compiler接受的主程序,需要一个以function开头的文件。如果你习惯写脚本(从头到尾只要变量,没有函数声明),直接添加是没问题的,但编译器会提示你主程序必须是一个函数。
解决办法很简单,套一层壳。比如你的脚本叫process.m,那就新建一个run_process.m:
function run_process % 这里直接调用原本的脚本逻辑 process; end这样打包入口就是run_process.m。
如果是App Designer设计的GUI,本身就是类定义加函数,天然满足要求。如果是GUIDE时代的老工程,主文件通常也是一对.fig和.m,打包时把.m文件作为主程序,.fig作为资源一并添加,同样能正常编译。但2020R这个时间点,GUIDE已经开始被官方标注为“不建议新项目使用”了,新项目我建议直接用App Designer。
3.Application Compiler完整打包实测过程
3.1 打开App和界面导览
在MATLAB命令窗口输入:
applicationCompilerR2020a和R2020b会打开一个独立窗口,整个界面左侧主要功能是“打包应用程序”,对应的是Standalone Application,这正是我们要的。旁边还有“打包库”等选项,那是给其他语言调用用的,属于MATLAB Compiler SDK的范畴,先不用管。
进入“打包应用程序”页面后,界面从上到下大概是这样的结构:
- 主文件区:需要指定一个主程序文件,不要选多个,主程序只能有一个。
- 自定义打包文件区:可以手动添加额外的文件夹或文件,比如数据文件、图标文件、工具函数。
- 应用程序信息区:设置名称、版本号、公司名、图标、附加说明文字。
- Runtime包含方式:这里的问题比较关键,我单独在3.3里说。
- Package按钮:最终执行编译。
标题里提到“2020R版本实测”,有一点需要说明:R2020a的Application Compiler窗口相对朴素,R2020b在此基础上做了一些界面调整和Web App引导,但核心选项位置基本一致。我下面写的步骤是R2020b里实拍的逻辑,R2020a用户按相同名称找控件即可。
3.2 添加主文件与依赖文件
点“添加主文件”,选择你的GUI主函数,比如mainApp.m。如果入口是.mlapp,这里也能直接选。选完主文件后,Application Compiler会立刻做一次依赖分析,在“包含的包”或“运行时设置”里列出它自动发现的文件。
这时候回头打开2.2节里我们跑requiredFilesAndProducts的列表,对比一下自动发现的结果,有没有漏掉什么。如果漏了,就在“自定义打包文件”里手动添加。注意一个细节:添加文件夹时,编译器默认是“递归添加”,也就是把子目录里的文件也包含进去。如果你的工具函数里有一个子目录,忘了这一条,运行时还是会报找不到文件。
资源文件的添加也一样。比如GUI用到了一个图标文件logo.png,你就把它加到自定义文件里。之后在代码里引用这个资源的时候,需要按打包后实际运行的路径逻辑来写,这一块在第5节会展开,因为它是运行时最容易出问题的地方。
3.3 Runtime分发方式的选择:包含还是不包含
Application Compiler界面里有一个选项,名字类似“运行时下载”或“包含MATLAB Runtime”。它通常有几种选择:
- 不包含:只生成exe和依赖dll,目标机器上必须已经装过同版本的MATLAB Runtime。
- 包含Runtime安装程序:输出里会附带一个Runtime安装程序,对方可以先装Runtime再运行exe。
- 生成完整安装包/安装器:编译器会把exe和Runtime安装程序做成一个安装向导,接收方双击后一路“下一步”即可。
选哪种取决于接收方的情况。如果接收方是一台公用机器,以后会有不少人给他发MATLAB编译程序,那第一次选“包含Runtime安装程序”装一次,以后别人再给他发exe,就可以相互用同一个Runtime了。如果接收方是完全不懂技术的业务人员,那就直接生成完整安装包,让他一路点下去,体验最友好。
有一点要提醒,R2020a和R2020b的Runtime不能混用版本支撑,比如R2020a编译的程序,对应的Runtime必须是9.8,R2020b对应9.9。不过同一台机器上可以同时装多个版本的Runtime,互不干扰。非要图省事的话,我就建议所有同学都统一用R2020b或更新版本,以后组内互相发程序也方便管理。
3.4 点Package之后到底发生了什么
上面都填好后,点“Package”,然后等待。编译过程短则两三分钟,长则十几分钟,取决于程序规模和机器性能。
打包过程中,Application Compiler会在背后调用mcc命令,把你的代码编译成中间表示,把用到的工具箱函数和资源文件统一打包成CTF归档文件,再生成exe可执行文件和一组dll。R2020a打包后通常会在输出目录里得到一个exe加一个同名的ctf文件,R2020b里这些文件通常被自动组织在输出目录的结构中。注意,这些dll和你代码里的函数库不是一回事,它们本质上是Runtime组件,缺少任何一个都会导致启动失败,所以分发时不要只拷exe,要把整个输出目录一起拷走。
打包过程中如果报错,错误信息会出现在MATLAB命令窗口。最常见的错误是“找不到编译器”,有时候是这个提示:
Error using mcc The MATLAB Compiler requires a supported compiler.很多人一看这个就去装Visual Studio,其实在R2020a和R2020b里,用Application Compiler打包纯MATLAB代码并不需要额外安装C/C++编译器;只有你的程序里包含MEX函数或外部C代码时才需要。真遇到这个报错,先检查一下MATLAB Compiler这个工具箱本身的License是否完整,而不是浪费时间装编译器。
3.5 输出目录里那三个文件夹分别派什么用场
打包完成后,输出目录下通常会出现三个文件夹:for_testing、for_redistribution、for_redistribution_files_only。很多人第一次看到这三个文件夹就晕了。我解释一下它们各自的实际用途:
| 文件夹 | 内容 | 我的使用建议 |
|---|---|---|
for_testing | exe、dll、ctf文件,可直接在本地运行 | 打包后第一件事,用它做快速自测,跑通再说 |
for_redistribution | 完整的安装包(如MyAppInstaller_web.exe) | 交付给技术小白用户时用这个,双击安装覆盖Runtime |
for_redistribution_files_only | 只有exe和dll,没有Runtime安装器 | 交付给已经装了同版本Runtime的机器,拷过去就能跑 |
在实际交付时,我一般把for_redistribution_files_only里的东西压缩成一个zip,发给对方的同时,写清楚一条要求:“机器上已经装过MATLAB Runtime R2020a就直接解压运行;没装过就先装这里附带的Runtime安装包。”如果对方不是我这种能折腾的人,就直接发完整安装包,体验最好。
补充一个细节:for_testing目录下面有个readme.txt,我每次都会打开看一眼,里面写了这个程序依赖的Runtime版本和运行注意事项。这比自己记靠谱得多,建议你也养成习惯。
3.6 不想开界面?命令行mcc一条龙
Application Compiler毕竟是图形界面,手动点几个按钮能满足大多数场景。如果你的打包流程要走自动化,或者要把打包步骤写进CI脚本,那就得用命令行方式。以我的一个工具为例,主程序是mainApp.m,我想打包成一个叫MyTool的独立exe,命令是:
mcc -e -m -o MyTool -W main:MyTool -T link:exe mainApp.m helper_funcs/loadData.m resources/logo.png参数拆开解释一下:
-e:生成独立应用程序,不需要接收方装MATLAB。-m:告诉编译器生成的类型是“独立应用”而非库。-o MyTool:指定输出文件名。-W main:MyTool:设置exe入口主函数名。-T link:exe:指定目标类型为exe可执行文件。
命令行方式和界面方式的底层一致,但命令行方式不会自动帮你把“自定义文件”里的非代码文件(如图片、数据文件)编排好,需要你手动把资源文件加到参数里。所以如果工程里有大量资源文件,我还是更推荐用图形界面勾选,省得命令行里一长串路径看着头晕。
4. 把exe搬到没有MATLAB的机器上
4.1 完整部署流程:装Runtime再跑exe
打包完成后,找一台干净机器做验收。这台机器最好是一台没有装过任何MATLAB组件的Windows,或者虚拟机也行。部署步骤是这样:
- 从
for_redistribution目录里拿到完整安装包,比如MyAppInstaller_web.exe,双击运行。 - 安装程序会先安装MATLAB Runtime,按提示选择安装路径,默认路径就好,不建议改到带空格或中文的目录。
- 安装完成后再把exe和dll文件拷贝到某个固定目录,双击exe运行。
如果你手里是for_redistribution_files_only,那就跳过安装步骤,前提是机器上已经有R2020a(Runtime 9.8)或R2020b(Runtime 9.9)。判断机器装没装Runtime,最简单的办法是看“控制面板-程序和功能”,搜索“MATLAB Runtime”,看到对应版本号就说明有。
一个很容易踩的误区是:把exe拷贝过去了,但和它同目录的那几个dll没有一起拷,或者只拷了exe而忘记了旁边的ctf文件。结果双击后程序闪退,毫无提示。所以我一再强调,拷贝时要拷整个for_redistribution_files_only目录里的内容,而不是只挑一个exe文件。
4.2 在目标机器上的GUI验收清单
部署到干净机器上,不要只双击一次发现能开界面就算完。GUI程序的验收要多测几个关键点,我自己习惯按这个清单来:
- 双击exe,界面能否正常打开,是否有报错弹窗。
- 触发几个主要按钮功能,尤其是涉及读文件、写文件、调用外部程序的回调。
- 切换不同工作目录再运行,看看程序是否依赖“exe所在目录”才能找到数据文件。
- 看看任务管理器里进程是否正常退出,不留下残留进程。
其中第三点最常见。很多程序在开发机里直接在代码里写相对路径,比如readtable('data.csv'),在开发机上跑没问题,因为当年当前目录就是工程目录。但打包后双击exe,当前工作目录通常是exe所在目录或系统用户目录,如果程序恰好在启动时读配置文件,就会找错路径。在验收清单里加上这一项,能提前发现问题。
4.3 Runtime版本冲突与多版本共存
一台机器上如果先后装过R2019b、R2020a、R2023b的Runtime,它们会各自独立安装,互不影响。MATLAB Runtime的安装机制做得还行,不会互相覆盖。但有一个麻烦点是:如果你编译用的是R2020a,而目标机器只有R2020b的Runtime,程序是跑不起来的。Runtime版本和编译版本必须严格对应,至少对R2020a和R2020b是这样。
所以你在给多个同事分发程序时,最好在说明文档里写清楚:“本程序基于MATLAB R2020a编译,需要安装MATLAB Runtime 9.8。”如果不确定对方是否装过,可以直接让他安装包里的Runtime安装程序走一遍。不用担心重复安装,Runtime如果已经存在,安装程序会提示修复或直接跳过,不会把系统搞乱。
5. 打包与部署阶段最常见的坑,以及对应的排查方法
5.1 路径问题:不是代码的问题,是运行时的工作目录变了
这个问题压到第一个说,因为它影响面太大。打包后的程序和开发机上的MATLAB环境有个本质区别:开发时,你的当前工作目录是工程目录,能直接访问相对路径;打包后,程序没有“当前脚本目录”这一说,它的工作目录取决于你怎么启动它。
如果代码里用pwd来定位资源文件,打包后十有八九会翻车。正确的做法是,编译程序内需要引用的资源文件,要在代码里用ctfroot获取打包归档的解压目录:
resPath = fullfile(ctfroot, 'logo.png');ctfroot是打包后程序运行时,MATLAB Runtime把CTF归档内容解压到的临时目录。代码里通过它就能稳定找到打包进去的资源。如果你想定位的是“用户当前在某些GUI里选中的文件”,那应该用uigetfile返回的路径,而不是猜测相对路径。
还要小心一点:不要往ctfroot目录里写文件。这个目录对程序来说是只读的,每次启动都会临时解压,退出后可能清理掉。需要写日志、写配置、写结果的时候,应该写到pwd或用户文档目录。
5.2 动态函数调用导致的“运行时报错,编译时一切正常”
Application Compiler的依赖分析是静态的。如果你的代码里写了:
funcName = 'myFunc'; feval(funcName, data);或者用str2func拼接回调函数名,编译器在打包时根本看不到myFunc这个名字,更不会主动把它加进去。结果就是编译顺利通过,但你到目标机器上一跑,点某个按钮就报“未定义函数或变量'myFunc'”。
排查方法:检查代码里所有eval、feval、str2func、evalin调用,凡是动态调用的函数,都要在打包时手动添加到自定义文件里,或者改用静态调用写法:
myFunc(data); % 直接调用,编译器能识别实在要动态调用,就在代码里加一行“假引用”骗过依赖分析,比如在文件开头写上:
if false myFunc(); end这样编译器会把myFunc包含进去,且运行时不会真正执行。这个技巧不优雅但好用,我把它留在了很多老项目的代码里。
5.3 杀毒软件误报与Windows SmartScreen拦截
MATLAB编译出来的exe启动时会释放dll、读注册表、链接Runtime,行为和很多商业软件的首次启动差不多,部分杀毒软件或者Windows SmartScreen会对它产生“不认识”的提示。我在Windows 10和Windows 11上都遇到过“Windows已保护你的电脑”弹窗,需要点“更多信息-仍要运行”才能继续。
如果你的接收方是业务部门,这种弹窗会非常有杀伤力,对方可能直接觉得程序有问题不敢点。解决方法是把生成的安装包做代码签名,但个人开发者搞到代码签名证书成本不低。折中方案是提前告知接收方,这是MATLAB Runtime的常见提示,让他放心点“仍要运行”。另外,在公司内部杀毒软件的管理后台,可以把exe所在目录加白名单,但这通常需要IT权限,我一般只在文档里提示一下。
5.4 常见问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 打包时提示缺少编译器 | MATLAB Compiler授权不完整,或代码里含MEX文件 | 检查License;若有MEX则安装对应C编译器 |
| 双击exe闪退,任务管理器一闪而过 | 缺少dll或ctf文件,或Runtime版本不匹配 | 从for_redistribution_files_only整个目录拷贝;确认Runtime版本 |
| 界面能打开,点按钮报“未定义函数” | 动态调用函数没被打包 | 手动添加文件或用静态调用写法 |
| 读取不到配置文件或图片 | 使用了相对路径 | 改用ctfroot拼路径 |
| 程序第一次启动慢 | Runtime首次解压CTF归档 | 属于正常现象,后续启动会加快 |
| 杀毒软件拦截 | Runtime行为类似商业软件 | 提示接收方放行,或做代码签名 |
| 中文路径下运行异常 | 部分老代码对中文路径支持差 | 建议安装路径和数据路径都避免中文 |
6. 打包之后的日常维护与升级建议
打包不是一次性工作,程序后续要改Bug、加功能,就会涉及重新编译和重新分发。这里有几个经验值得说说。
第一,建议给每次打包留下“编译记录”。我在工程目录里放了一个build_log.txt,每次打包都记一下编译时间、MATLAB版本、Runtime版本、输出目录大小、测试结果。这样过两个月用户报了一个老版本的Bug,我能快速知道那个版本是用什么环境打的,能复现还是不能复现,心里有数。
第二,升级MATLAB主版本时,要重新编译一遍所有已交付的程序。不要把R2020a编译的exe指望到R2023b的Runtime上跑,版本对不上绝对跑不了。像从R2020a升到R2020b这种小版本升级,也建议重新编译一次,旧版Runtime虽然还能装,但总归多维护一个版本不是好事。
第三,新项目建议直接用App Designer而不是GUIDE。GUIDE在R2020a里还能正常用,但官方已经逐步淘汰它,新版本MATLAB里已经很难找到完整支持。App Designer做的.mlapp不仅能正常被Application Compiler打包,界面风格也更现代,对高分屏支持也更好。
我自己的习惯是:每次代码改动后,先在开发机上用for_testing里的exe跑一遍;确认没问题,再重新生成for_redistribution安装包;最后把新包发给测试的同事,并请他确认是不是干净机器上测试的。这套流程看着多,但能避免绝大多数“在我电脑上明明好的”这种尴尬。
最后再分享一个小技巧:打包完的exe不要直接放在桌面或临时文件夹里就给人家,压缩成一个zip再发给对方,zip里放一个README.txt,把Runtime版本要求、首次运行注意事项写清楚。接收方体验会差很多,你后续被问的问题也少很多。