金沙国际官网_金沙国际平台登录

因为这个金沙国际官网_金沙国际平台登录网站与很多的大型澳门赌场都有合作,金沙国际官网_金沙国际平台登录尽职尽责,高效执行,保持好奇心,不断学习,追求卓越,点击进入金沙国际官网_金沙国际平台登录马上体验吧,所以现在也正式地开始了营业。

您的位置:金沙国际官网 > 编程 > ZKWeb网站框架的动态编译的实现原理

ZKWeb网站框架的动态编译的实现原理

发布时间:2019-11-04 13:08编辑:编程浏览(94)

    python 3.x

    IDisposable 接口

    托管资源和非托管资源

    • 托管资源
      • CLR 控制和管理的内存资源,如程序中在 Heap 上分配的对象、作用域内的变量等;
      • GC 机制实现自动内存管理和托管堆的全权管理;
    • 非托管资源
      • CLR 不能控制管理的部分,如文件流Stream/数据库连接coonection/窗口句柄/组件COM等;
      • Finalize 方法(析构函数) GC 隐式自动调用,Dispose 方法手动强制显式调用;
      • 尽量避免使用 Finalize() 方法清理资源,推荐实现 Dispose() 方法供显式调用;

    注:MSDN - 实现 Finalize() 方法或析构函数对性能可能会有负面影响。用 Finalize() 方法回收对象占用的内存至少需要两次垃圾回收,第一次调用析构函数,第二次删除对象。 GC 机制在回收托管对象内存之前,会先调用对象的析构函数。   

    析构函数(Finalize方法) .vs. Dispose方法

    Finalize 方法用于释放非托管资源,Dispose 方法用于清理或释放由类占用的非托管和托管资源。IDisposable 接口定义见上,自定义类应实现 IDisposable 接口,设计原则:

    • 可以重复调用 Dispose() 方法;
    • 析构函数应该调用 Dispose() 方法;
    • Dispose() 方法应该调用 GC.SuppressFinalize() 方法,指示垃圾回收器不再重复回收该对象;

    在一个包含非托管资源的类中,资源清理和释放的标准模式是:

    1. 继承 IDisposable 接口;
    2. 实现 Dispose() 方法,在其中释放托管和非托管资源,并将对象从垃圾回收器链表中移除;
    3. 实现类的析构函数,在其中释放非托管资源;

    其中,变量 "isDisposing" 来区分手动显式调用(true)还是GC隐式调用(false)。

      public class MyDispose : IDisposable
      {
          public MyDispose() { }
          ~MyDispose() { 
              Dispose(false); 
          }
    
          private bool isDisposed = false;
          public void Dispose(){
              Dispose(true);
              System.GC.SuppressFinalize(this);
          }        
          protected virtual void Dispose(bool isDisposing)  // 子类可重写
          {
              if (false == this.isDisposed)
              {
                  if (true == isDisposing){
                      OtherManagedObject.Dispose();      // 释放托管资源 ...
                  }
                  OtherUnManagedObjectDisposeOrClose();  // 释放非托管资源 ...             
                  this.isDisposed = true;
              }         
          }
      }
    

    析构函数执行在类的实例被销毁之前需要的清理或释放非托管资源的行为,注意不能在析构函数中释放托管资源。类的析构函数被编译后自动生成 protected void Finalize() 方法,GC 垃圾回收时会调用该方法并对继承链中的所有实例递归地调用 Finalize() 方法。

    Object.Finalize() 方法不可重写。

    • 类的析构函数不可继承和重载、不能带访问修饰符,一个类至多有一个析构函数;
    • 析构函数只针对类的实例对象,没有静态析构函数;

      protected void Finalize(){

       try{
           // 
       }
       finally{
           base.Finalize();
       }
      

      }

    Finalize() 方法被调用的情况:

    • 显式调用System.GC 的 Collect方法(不建议);
    • Windows 内存不足、第G0代对象充满;
    • 应用程序被关闭或 CLR 被关闭;

    Dispose() 方法的调用分 2 种:

    • 使用 using 语句会自动调用:using( MyDispose myObj = new MyDispose() ) {…}
    • 显式调用:myObj.Dispose();

    一个资源安全的类,都应实现 IDisposable 接口和析构函数,提供手动释放资源和系统自动释放资源的双保险。(1)若一个类A有一个实现了 IDisposable 接口类型的成员并创建(创建而不是接收,必须是由类A创建)它的实例对象,则类A也应该实现 IDisposable 接口并在 Dispose 方法中调用所有实现了 IDisposable 接口的成员的 Dispose 方法;(2)如果基类实现了 IDisposable 接口,那么其派生类也要实现 IDisposable 接口,并在其 Dispose 方法中调用基类中 Dispose 方法;只有这样才能保证所有实现了 IDisposable 接口的类的对象的 Dispose 方法能被调用到、手动释放任何需要释放的资源。

    参考

    为什么 IEnumerator 接口没有继承 IDisposable 接口;
    托管资源和非托管资源; IDisposable接口的一个典型例子;
    Finalize - Dispose - SuppressFinalize; IDisposable和Finalize的区别和联系;
    对.Net 垃圾回收 Finalize 和 Dispose 的理解;
    深刻理解 C# 中资源释放;

    ZKWeb网站框架是一个自主开发的网页框架,实现了动态插件和自动编译功能。
    ZKWeb把一个文件夹当成是一个插件,无需使用csproj或xproj等形式的项目文件管理,并且支持修改插件代码后自动重新编译加载。

    安装 Flask

    GC 垃圾回收

    本质:跟踪所有被引用到的对象,整理不再被引用的对象并回收相应内存。

    优点

    • 减少由于内存运用不当产生的Bug,降低编程复杂度;
    • 高效的内存管理;
    • 提高软件系统的内聚;

    代 Generation

    NET 垃圾回收器将 CLR 托管堆内的对象分为三代:G0、G1、G2,代龄机制支持有选择地查询,提高垃圾回收性能,避免回收整个托管堆。

    • G0:小对象(Size<85000Byte),最近被分配内存的对象,支持快速存取对象;
    • G1:在GC中幸存下来的G0对象,CLR 检查过一次未被回收的G0对象;
    • G2:大对象(Size>=85000Byte),CLR 检查过二次及以上仍未被回收的G1/G2对象;

    通过 GC.GetGeneration() 方法可以返回对象所处的代。当第0代对象已满时,自动进行垃圾回收,第0代中未被释放的对象成为第1代,新创建的对象成为第0代,以此类推,当第0代再次充满时会再次执行垃圾回收,未被释放的对象被添加到第1代。随着程序的执行,第1代对象会产生垃圾,此时垃圾回收器并不会立即执行回收操作,而是等第1代被充满回收并整理内存,第1代中未被释放的对象成为第2代。当第1代收集时,第0代也需要收集,当第2代收集时,第1和第0代也需要收集。

    根 root

    每个应用程序都包含一组根,每个根都是一个存储位置,包含一个指针或引用托管堆上的一个对象或为null,由 JIT编译器 和 CLR运行时 维护根(指针)列表。

    工作原理

    基于代的垃圾回收器如下假设:

    • 对象越新,生存期越短,最近分配内存空间的对象最有可能被释放,搜索最近分配的对象集合有助于花费最少的代价来尽可能多地释放内存空间;
    • 对象越老,生存期越长,被释放的可能性越小,经过几轮GC后,对象仍然存在,搜索代价大、释放内存空间小;
    • 程序的局部性原理 :同时分配的内存对象通常同时使用,将它们彼此相连有助于提高缓存性能和回收效率;
    • 回收堆的一部分速度快于回收整个堆;

    标记和清除 (Mark & Sweep) 收集算法:避免出现 "环引用" 造成内存泄露
    利用内部结构的 终止队列(Finalization Queue) 跟踪保存具有 Finalize 方法(定义了析构函数)的对象。

    • ReRegisterForFinalize():将对象的指针重新添加到Finalization队列中;(允许系统执行Finalize方法)
    • SuppressFinalize():将对象的指针从Finalization 队列中移除;(拒绝系统执行Finalize方法)

    程序创建具有 Finalize 方法的对象时,垃圾回收器会在终止队列中添加一个指向该对象的项(引用或指针)。当对象不可达时,没有定义析构函数的不可达对象直接由 GC 回收,定义了析构函数的不可达对象从终止队列中移除到 终止化-可达队列(F-reachable Queue)中。在一个特殊的专用线程上,垃圾回收器会依次调用该队列中对象的 Finalize 方法并将其从队列中移除,执行后该对象和没有Finalize方法的垃圾对象一样,然后在下一次 GC 中被回收。(GC线程 和 Finalizer线程 不同)
    算法分 2 步:

    • 标记阶段:垃圾识别。从应用程序的 root 出发,利用相互引用关系,递归标记(DFS),存活对象被标记,维护一张树图:"根-对象可达图"; 
    • 压缩阶段:内存回收。利用 Compact 压缩算法,移动内存中的存活对象(大对象除外)并修改根中的指针,使内存连续、解决内存碎片问题,有利于提高内存再次分配的速度和高速缓存的性能;  

    参考

    C#基础知识梳理系列十一:垃圾回收机制; 步步为营 C# 技术漫谈 四、垃圾回收机制(GC);
    垃圾回收机制 - Generation的原理分析;
    详解 Finalization队列与 F-reachable队列; 深入浅出理解 GC 机制;
    垃圾回收GC:.Net自动内存管理系列;

    下面将说明ZKWeb如何实现这个功能,您也可以参考下面的代码和流程在自己的项目中实现。
    ZKWeb的开源协议是MIT,有需要的代码可以直接搬,不需要担心协议问题。

    pip install Flask

    内存泄漏

    按照编译原理,内存分配策略有3种:

    • 静态存储区(方法区):编译时即分配好,程序整个运行期间都存在,主要存放静态数据、全局static数据和常量
    • 栈区:局部变量,自动释放
    • 堆区:malloc或new的动态分配区,需手动释放

    推荐使用 .Net 内存分析工具:CLR Profiler,用来观察托管堆内存分配和研究垃圾回收行为的一种工具。

    附注:

    该处提供一个狂降内存的方法(摘自网上),可以极大优化程序内存占用。
    这个函数是将程序的物理内存尽可能转换为虚拟内存,大大增加硬盘读写,是不好的,慎用!!
    使用方法:在程序中用一个计时器,每隔几秒钟调用一次该函数,打开任务管理器

        [DllImport("kernel32.dll", EntryPoint = "SetProcessWorkingSetSize")]
        public static extern int SetProcessWorkingSetSize(IntPtr process, int minSize, int maxSize);
        /// <summary>    
        /// 释放内存    
        /// </summary>    
        public static void ClearMemory()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            if (Environment.OSVersion.Platform == PlatformID.Win32NT)
            {
                SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
            }
        }
    

      

     

    实现动态编译依赖的主要技术

    编译: Roslyn Compiler
    Roslyn是微软提供的开源的c# 6.0编译工具,可以通过Roslyn来支持自宿主编译功能。
    要使用Roslyn可以安装nuget包Microsoft.CodeAnalysis.CSharp
    微软还提供了更简单的Microsoft.CodeAnalysis.CSharp.Scripting包,这个包只需简单几行就能实现c#的动态脚本。

    加载dll: System.Runtime.Loader
    在.Net Framework中动态加载一个dll程序集可以使用Assembly.LoadFile,但是在.Net Core中这个函数被移除了。
    微软为.Net Core提供了一套全新的程序集管理机制,要求使用AssemblyLoadContext来加载程序集。
    遗憾的是我还没有找到微软官方关于这方面的说明。

    生成pdb: Microsoft.DiaSymReader.Native, Microsoft.DiaSymReader.PortablePdb
    为了支持调试编译出来的程序集,还需要生成pdb调试文件。
    在.Net Core中,Roslyn并不包含生成pdb的功能,还需要安装Microsoft.DiaSymReader.NativeMicrosoft.DiaSymReader.PortablePdb才能支持生成pdb文件。
    安装了这个包以后Roslyn会自动识别并使用。

     

    实现动态编译插件系统的流程

    在ZKWeb框架中,插件是一个文件夹,网站的配置文件中的插件列表就是文件夹的列表。
    在网站启动时,会查找每个文件夹下的*.cs文件对比文件列表和修改时间是否与上次编译的不同,如果不同则重新编译该文件夹下的代码。
    网站启动后,会监视*.cs*.dll文件是否有变化,如果有变化则重新启动网站以重新编译。
    ZKWeb的插件文件夹结构如下

    • 插件文件夹
      • bin:程序集文件夹
        • net: .Net Framework编译的程序集
          • 插件名称.dll: 编译出来的程序集
          • 插件名称.pdb: 调试文件
          • CompileInfo.txt: 储存了文件列表和修改时间
        • netstandard: .Net Core编译的程序集
          • 同net文件夹下的内容
      • src 源代码文件夹
      • static 静态文件的文件夹
      • 其他文件夹……

     

    通过Roslyn编译代码文件到程序集dll

    在网站启动时,插件管理器在得到插件文件夹列表后会使用Directory.EnumerateFiles递归查找该文件夹下的所有*.cs文件。
    在得到这些代码文件路径后,我们就可以传给Roslyn让它编译出dll程序集。
    ZKWeb调用Roslyn编译的完整代码可以查看这里,下面说明编译的流程:

    首先调用CSharpSyntaxTree.ParseText来解析代码列表到语法树列表,我们可以从源代码列表得出List<SyntaxTree>
    parseOptions是解析选项,ZKWeb会在.Net Core编译时定义NETCORE标记,这样插件代码中可以使用#if NETCORE来定义.Net Core专用的处理。
    path是文件路径,必须传入文件路径才能调试生成出来的程序集,否则即使生成了pdb也不能捕捉断点。

    // Parse source files into syntax trees
    // Also define NETCORE for .Net Core
    var parseOptions = CSharpParseOptions.Default;
    #if NETCORE
    parseOptions = parseOptions.WithPreprocessorSymbols("NETCORE");
    #endif
    var syntaxTrees = sourceFiles
        .Select(path => CSharpSyntaxTree.ParseText(
            File.ReadAllText(path), parseOptions, path, Encoding.UTF8))
    .ToList();
    

    接下来需要分析代码中的using来找出代码依赖了哪些程序集,并逐一载入这些程序集。
    例如遇到using System.Threading;会尝试载入SystemSystem.Threading程序集。

    // Find all using directive and load the namespace as assembly
    // It's for resolve assembly dependencies of plugin
    LoadAssembliesFromUsings(syntaxTrees);
    

    LoadAssembliesFromUsings的代码如下,虽然比较长但是逻辑并不复杂。
    关于IAssemblyLoader将在后面阐述,这里只需要知道它可以按名称载入程序集。

    /// <summary>
    /// Find all using directive
    /// And try to load the namespace as assembly
    /// </summary>
    /// <param name="syntaxTrees">Syntax trees</param>
    protected void LoadAssembliesFromUsings(IList<SyntaxTree> syntaxTrees) {
        // Find all using directive
        var assemblyLoader = Application.Ioc.Resolve<IAssemblyLoader>();
        foreach (var tree in syntaxTrees) {
            foreach (var usingSyntax in ((CompilationUnitSyntax)tree.GetRoot()).Usings) {
                var name = usingSyntax.Name;
                var names = new List<string>();
                while (name != null) {
                    // The type is "IdentifierNameSyntax" if it's single identifier
                    // eg: System
                    // The type is "QualifiedNameSyntax" if it's contains more than one identifier
                    // eg: System.Threading
                    if (name is QualifiedNameSyntax) {
                        var qualifiedName = (QualifiedNameSyntax)name;
                        var identifierName = (IdentifierNameSyntax)qualifiedName.Right;
                        names.Add(identifierName.Identifier.Text);
                        name = qualifiedName.Left;
                    } else if (name is IdentifierNameSyntax) {
                        var identifierName = (IdentifierNameSyntax)name;
                        names.Add(identifierName.Identifier.Text);
                        name = null;
                    }
                }
                if (names.Contains("src")) {
                    // Ignore if it looks like a namespace from plugin 
                    continue;
                }
                names.Reverse();
                for (int c = 1; c <= names.Count; ++c) {
                    // Try to load the namespace as assembly
                    // eg: will try "System" and "System.Threading" from "System.Threading"
                    var usingName = string.Join(".", names.Take(c));
                    if (LoadedNamespaces.Contains(usingName)) {
                        continue;
                    }
                    try {
                        assemblyLoader.Load(usingName);
                    } catch {
                    }
                    LoadedNamespaces.Add(usingName);
                }
            }
        }
    }
    

    经过上面这一步后,代码依赖的所有程序集应该都载入到当前进程中了,
    我们需要找出这些程序集并且传给Roslyn,在编译代码时引用这些程序集文件。
    下面的代码生成了一个List<PortableExecutableReference>对象。

    // Add loaded assemblies to compile references
    var assemblyLoader = Application.Ioc.Resolve<IAssemblyLoader>();
    var references = assemblyLoader.GetLoadedAssemblies()
        .Select(assembly => assembly.Location)
        .Select(path => MetadataReference.CreateFromFile(path))
        .ToList();
    

    构建编译选项
    这里需要调用微软非公开的函数WithTopLevelBinderFlags来设置IgnoreCorLibraryDuplicatedTypes。
    这个标志让Roslyn可以忽略System.Runtime.Extensions和System.Private.CoreLib中重复的类型。
    如果需要让Roslyn正常工作在windows和linux上,必须设置这个标志,具体可以看
    Roslyn Scripting默认会使用这个标志,操蛋的微软

    // Create compilation options and set IgnoreCorLibraryDuplicatedTypes flag
    // To avoid error like The type 'Path' exists in both
    // 'System.Runtime.Extensions, Version=4.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
    // and
    // 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
    var compilationOptions = new CSharpCompilationOptions(
        OutputKind.DynamicallyLinkedLibrary,
        optimizationLevel: optimizationLevel);
    var withTopLevelBinderFlagsMethod = compilationOptions.GetType()
        .FastGetMethod("WithTopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
    var binderFlagsType = withTopLevelBinderFlagsMethod.GetParameters()[0].ParameterType;
    compilationOptions = (CSharpCompilationOptions)withTopLevelBinderFlagsMethod.FastInvoke(
        compilationOptions,
        binderFlagsType.GetField("IgnoreCorLibraryDuplicatedTypes").GetValue(binderFlagsType));
    

    最后调用Roslyn编译,传入语法树列表和引用程序集列表可以得到目标程序集。
    使用Emit函数编译后会返回一个EmitResult对象,里面保存了编译中出现的错误和警告信息。
    注意编译出错时Emit不会抛出例外,需要手动检查EmitResult中的Success属性。

    // Compile to assembly, throw exception if error occurred
    var compilation = CSharpCompilation.Create(assemblyName)
        .WithOptions(compilationOptions)
        .AddReferences(references)
        .AddSyntaxTrees(syntaxTrees);
    var emitResult = compilation.Emit(assemblyPath, pdbPath);
    if (!emitResult.Success) {
        throw new CompilationException(string.Join("rn",
            emitResult.Diagnostics.Where(d => d.WarningLevel == 0)));
    }
    

    到此已经完成了代码文件(cs)到程序集(dll)的编译,下面来看如何载入这个程序集。

    本文由金沙国际官网发布于编程,转载请注明出处:ZKWeb网站框架的动态编译的实现原理

    关键词:

上一篇:没有了

下一篇:没有了