在revit二次开发时,几乎都会遇到的一个问题就是,每次修改完代码,都必须重启revit,才能重新编译代码, 然后再次启动Revit进行调试。

这种开发方式太影响效率了,我一直在寻找一些热重载的办法,最近在逛jeremy博客的时候也发现了这一话题,也大概研究了一下。

目前能实现热重载的方式以下几种:

1. 使用解释性语言比如ruby、python来编写插件。

由于是解释性语言,本身就不需要先编译,而是在运行中编译,源头上直接解决了。但解释性语言性能不太行,而且资料也比较少,不太适合日常开发。但jeremy的博客也比较推荐使用这种,尤其是使用python,可以使用RevitPythonShell来编写python代码。这个需要对python代码和RevitPythonShell比较熟悉了,B站上也有相关的教程。之前折腾了一段时间,但感觉还是有点麻烦,主要是因为工作中的代码基本都不会用到python来开发Revit插件。所以实践比较少。

RevitpythonShell: https://github.com/architecture-building-systems/revitpythonshell

2. 使用宏。

revit内置的宏IDE是 SharpDevelop,这个修改完代码之后,直接编译,可以不用重启Revit。

这种开发方式基本就是:先使用宏进行代码的开发,开发完之后,移植成外部应用的代码。

这样做的话,你可能要维护两套代码, 一套是宏代码,一套是外部应用的代码。

而且SharpDevelop太老旧了,现在开发的IDE基本都是用VisualStudio,代码的移植还是很繁琐。

3. 使用AddinManager来加载、运行dll。

这种方式基本能胜任大部分的测试,也是我目前经常使用的,可以用到生产环境中来做开发。但这个也有一些问题。首先,先了解一下AddinManager的基本原理:

我们写的源代码会编译生成dll文件,然后revit调用这个dll文件实现相应的功能,就是我们开发的插件功能了。

而修改源代码之后,如果想重新编译,就会报错,因为Revit正在占用这个dll文件,除非把Revit给关闭了,这样就不会报被占用的错了。

AddinManager之所以不会报“被占用”的错误,因为使用AddinManager运行dll的时候,会先把这个dll复制到一个临时文件夹:%Temp%\RevitAddins下,这样Revit占用的就是%Temp%\RevitAddins临时文件夹下的dll,而你编译的生成的dll是在你源代码的目录下的,被占用的既然不是源代码目录下的dll,那当然可以随便编译运行了。但%Temp%\RevitAddins下的dll仍然会一直被占用,所以AddinManager每次点击运行,都会复制dll到新的临时文件夹,这样就实现了修改源代码不用重启Revit也能正常编译调试的效果了。

这个存在的问题:

  1. 由于处于同一个应用程序域(Revit程序域),所以一些静态的资源会在第一次使用AddinManager的时候就已经定死了,第二次使用AddinManager调试dll的时候,就会报错,说资源找不到之类的。

 这个问题,也可以修改,AddinManager是开源的,所以可以从源代码中找到一些端倪,AddinManaer在每次运行之前,都会清理%Temp%\RevitAddins这个临时文件夹。

其源代码如下:

public static string CreateTempFolder(string prefix)
{
    var folderPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
    var tempPath = Path.Combine(folderPath, "Temp");
    var directoryInfo = new DirectoryInfo(Path.Combine(tempPath, DefaultSetting.TempFolderName));
    if (!directoryInfo.Exists)
    {
        directoryInfo.Create();
    }
    // 下面注释的这里就是清理文件夹
    //foreach (var directoryInfo2 in directoryInfo.GetDirectories())
    //{
    //    try
    //    {
    //        Directory.Delete(directoryInfo2.FullName, true);
    //    }
    //    catch
    //    {
    //        // ignored
    //    }
    //}
    var str = $"{DateTime.Now:yyyyMMdd_HHmmss_ffff}";
    var path = Path.Combine(directoryInfo.FullName, prefix + str);
    var directoryInfo3 = new DirectoryInfo(path);
    directoryInfo3.Create();
    return directoryInfo3.FullName;
}rectoryInfo3.FullName;
}

所以我们只需要修改源代码,把上面这段给注释了,然后重新编译生成AddinManger.dll这样一些静态资源就能正确找到了。

当然这样的话,资源没有得到清理,你的C盘可能会无限膨胀。所以要在适当的时机给删除,比如在OnShutdown方法里面写下删除文件夹的代码(注意,是修改AddinManger插件的Onshudown方法,不是你自己写的插件)。这样虽然不能清理当前运行的,但确可以清理上一次Revit关闭之后的资源。也算一种清理机制。

当然这个也只能解决一部分,有些bug可能需要进一步修复。但这样的改动,基本以及能胜任大部分的开发工作了。

4. 使用VisualStudio的热重载

这种方式我也经常用,这种方式在Revit2020以上可以使用,Revit2018启动有点问题。基本步骤如下:

  1. 设置VisualStudio的项目属性,Debug时,设置启动的程序为C:\Program Files\Autodesk\Revit 2020\Revit.exe这里根据需要设置成对应版本的Revit。
  2. 以调试模式启动Revit。
  3. 正常修改源代码,点击热重载就行了。

这种方式也有缺点:

  1. 在某个程序集中添加新的方法、字段时,可能还是需要重启revit,重新编译。
  2. 在修改xaml代码的时候,不一定都能正常热重载成功
  3. 在调试打断点的时候,修改完代码,不要马上点热重载,而是要让程序执行完,再重新点热重载,不然有可能也会提示当前代码和调试的代码不匹配的问题,需要重启重新编译。

这种方式比较适用于不新增类、新增文件、方法之类的。适用已经有的方法,在上面进行修改调试。局限性其实也挺大的,而且debug状态下写代码,提示也不是很友好。鼠标会经常点到运行代码的悬浮按钮。

5.使用反射加载内存中的dll,而不是磁盘文件

这一点,算是一个思路,不能算是一个开箱就能用的方法。需要自己来写一个中间插件来调用。

Revit之所以会占用dll文件,因为用的是 Assembly.LoadFile(filePath)来直接读取磁盘上dll文件,这里有个思路就是不要直接读磁盘文件,而是先把dll读到内存中,读成字节数组(用File.ReadAllBytes(String) 方法),只读取的话,是不会占用dll太久的,把文件读成字节数组之后,文件占用的句柄会立马回收,所以下一次编译,就不会遇到文件被占用的问题。

而且Assembly类有个方法,可以直接读取字节数组的。

public static Assembly Load(byte[] rawAssembly)

就是写一个A插件,这个A插件的唯一功能就是调用其他插件。调用的时候,不是直接读取其他插件的dll文件, 而是先把其他插件的dll文件以流的形式先读到内存中,再用反射来运行其中的命令。AddinManager就是这么做的,只不过AddinManager也是读的磁盘文件,所以他先把dll复制到临时文件夹,然后占用那个临时文件夹下的文件。

直接加载磁盘文件的形式比较稳定,因为他能自动加载一些依赖,如果是先读到内存中,那很多dll并不一定能正常加载进去。