• 自动更新程序的制作方法


    自动更新程序的制作方法

    利用Microsoft的Application Blocks可以很简单地让程序可以自动更新。其基本原理是:利用Application Blocks软件包里面的一个.exe文件,(不妨命名为App.exe)作为长期运行的程序,这个程序不是真正的主体程序,而是会指向各个版本的程序,假如原来版本是1.0.0.0.exe,后来的版本是1.0.0.1.exe,那么运行App.exe时,它就先后指向这两个不同版本的exe。下面说说具体步骤:

    1.        安装Application Blocks

    记下安装路径,因为要用到里面的工程文件和dll等

    2.        在项目中加入代码和引用:

    把下列工程加入到你的工程所在的解决方案:

    Microsoft.ApplicationBlocks.ApplicationUpdater

    Microsoft.ApplicationBlocks.ApplicationUpdater.Interfaces

    Microsoft.ApplicationBlocks.ExceptionManagement

    Microsoft.ApplicationBlocks.ExceptionManagement.Interfaces

    如果你选择默认安装的话,它们的位置可能是: C:\Program Files\Microsoft ApplicationBlocksfor .NET\Updater\Code\CS\Microsoft.ApplicationBlocks.Updater

    (建议:把它们拷贝过来,放在同一个文件夹中,否则的话换了一台机器,或者改了路径的话就找不回这些工程了)

    在你的工程中(这里为 YourAppName)引用下列工程

    Microsoft.ApplicationBlocks.ApplicationUpdater

    Microsoft.ApplicationBlocks.ApplicationUpdater.Interfaces

    Microsoft.ApplicationBlocks.ExceptionManagement

    Microsoft.ApplicationBlocks.ExceptionManagement.Interfaces

    把下列命名空间加入到你Form的.cs文件中

    using System.Runtime.InteropServices;

    using System.Runtime.Serialization;

    using System.Threading;

    using System.Diagnostics;

    using System.IO;

    using System.Xml;

     

    特别提出关于引用的问题:

    在项目YourAppName中(自己原先写代码的那个项目)要引用下列文件:

    Microsoft.ApplicationBlocks.ApplicationUpdater.dll

    Microsoft.ApplicationBlocks.ApplicationUpdater.Interfaces .dll

    Microsoft.ApplicationBlocks.ExceptionManagement.dll

    Microsoft.ApplicationBlocks.ExceptionManagement.Interfaces .dll

     

     

    3.        把附录中的代码添加到你原来就有的的工程文件中去,这段代码是处理更新操作所用到的。在需要更新的时候调用函数InitializeAutoUpdate()即可。

     

    4.         生成你应用程序的发布目录结构并配置 AppStart.exe

    生成一个用于客户端程序安装的目录.  本例子中,我们用如下的目录:

    C:\Program Files\YourApp\1.0.0.0\

    现在复制 AppStart.exe 和 AppStart.exe.config 到类似如下的根目录中

       C:\Program Files\YourApp\AppStart.exe

     C:\Program Files\YourApp\AppStart.exe.config

    说明: 这两个文件你可以在如下目录中找到 “C:\Program Files\Microsoft Application Blocks for .NET\Updater\Code\CS\Microsoft.ApplicationBlocks.Updater\AppStart\bin\Debug“  

     

    5. 修改 AppStart.exe.config 文件

     

    AppStart.exe 会启动你的应用程序,如果更新文件下载完成之后还有可能要重启.  它需要知道启动你最新的程序的目录位置. 

    修改配置文件以配合当前的版本:

    <appStart>

      <ClientApplicationInfo>

        <appFolderName>C:\Program Files\YourApp\1.0.0.0</appFolderName>

        <appExeName>YourAppName.exe</appExeName>

        <installedVersion>1.0.0.0</installedVersion>

        <lastUpdated>2004-06-10T15:33:17.3745836-04:00</lastUpdated>

      </ClientApplicationInfo>

    </appStart>

     

    appFolderName 存放当前版本的exe所在的文件夹

    appExeName是运行的exe的文件名

    installedVersion是当前版本号

    lastUpdated是最新更新的时间

    更新成功以后这几个项都会自动更改

     

    6. 生成你的公钥和私钥

    运行

    "C:\ProgramFiles\MicrosoftApplicationBlocksfor .NET\Updater\Code\CS\Microsoft.ApplicationBlocks.Updater\ManifestUtility\bin\Debug\ManifestUtility.exe" 这个exe可能在软件包中会不存在,但是仍然可以在上述路径找到它的源文件,用C#.net编译运行即可。

    选择 “File..Generate Keys”  会提示你是否需要保存: PublicKey.xml 和 PrivateKey.xml  这两个密钥接下来就会用到. 

    这些密钥只要生成一次就可以了, 因为下面几个地方需要引用到RSA公钥和私钥.  你需要把这些密钥存放在一个安全的地方,因为在发布一个新的更新的时候会用到它。

     

    7. 创建IIS 虚拟目录

     

    在你的Web服务器上生成一个目录来存放你的更新文件.  在这两个目录中要放两样东西 1)  ServerManifest.xml 文件,包含最后版本的一些信息;2) 你的新程序的目录. 在这个目录里,生成一个目录来存放你的新版本程序.  在我们的例子中,我们用这两个目录, C:\Inetpub\AppUpdates  和C:\Inetpub\AppUpdates\1.0.0.1

    用 IIS 管理器生成一个虚拟目录指向刚才的实际目录.  记下你的 URL, 在上传步骤中我们需要用到它.  你必须要打开虚拟目录的“目录浏览”选项.

    ServerManifest.xml的生成要参考第11步,注意:这是最容易出错的地方了!

     

    8. 配置你的版本 1.0.0.0 的App.config 文件

     

    这里,我们会需要往里添加一些新东西.  首先, 我们需要加入一个configSections 元素来定义我们的 appUpdater 节:

     

    <configSections>

      <section name="appUpdater" type="Microsoft.ApplicationBlocks.ApplicationUpdater.UpdaterSectionHandler,Microsoft.ApplicationBlocks.ApplicationUpdater" />

    </configSections>

     

    接下来,我们需要添加一个 Version 键到我们的 appsettings 中, 我们首先设置我们的本地版本为 1.0.0.0, 这样我们就可以测试自动更新到版本 1.0.0.1

     

    <appSettings>

      <add key="VERSION" value="1.0.0.0" />

    </appSettings>

     

    最后,, 加入 appUpdater 节到你的配置文件中.  我这里用一对方括号把你要修改的值包含起来.  你可以直接从你上一步生成的 PublicKey.xml文件中复制 <RSAKeyValue> 元素.

     

    <xmlFile> 元素必须要指向你在Step #6创建的虚拟目录的 URL .

     

    <appUpdater>

      <UpdaterConfiguration>

       <polling type="Seconds" value="120" />

       <logListener logPath="C:\Program Files\YourApp\UpdaterLog.txt" />

       <downloader type="Microsoft.ApplicationBlocks.ApplicationUpdater.Downloaders.BITSDownloader"

    assembly="Microsoft.ApplicationBlocks.ApplicationUpdater,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"/>

       <validator type="Microsoft.ApplicationBlocks.ApplicationUpdater.Validators.RSAValidator" assembly="Microsoft.ApplicationBlocks.ApplicationUpdater,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null">

      <key>

       <RSAKeyValue>

      <Modulus>[YOUR MODULUS KEY]</Modulus>

      <Exponent>[YOUR EXPONENET]</Exponent>

      </RSAKeyValue>

      </key>

      </validator> 

      <application name="[YOUR APP NAME]" useValidation="true">

        <client>

          <baseDir>C:\Program Files\YourApp</baseDir>

          <xmlFile>C:\Program Files\YourApp\AppStart.exe.config</xmlFile>

          <tempDir>C:\Program Files\YourApp\temp</tempDir>

        </client>

        <server>

         <xmlFile>http://[YOUR URL]/ServerManifest.xml</xmlFile>

         <xmlFileDest>C:\Program Files\YourApp\ServerManifest.xml</xmlFileDest>

         <maxWaitXmlFile>60000</maxWaitXmlFile>

        </server>

      </application>

     </UpdaterConfiguration>

     </appUpdater>

     

    需要修改的地方只有[ ]括住的地方,

    <Modulus>[YOUR MODULUS KEY]</Modulus> 和

    <Exponent>[YOUR EXPONENET]</Exponent> 可以在PublicKey.xml中找到

    [YOUR URL ]就是ServerManifest.xml的URL地址

    <xmlFileDest>C:\Program Files\YourApp\ServerManifest.xml</xmlFileDest>中的路径就是更新成功后生成的ServerManifest.xml的路径

     

    9. 发布版本 1.0.0.0

     

    设置应用程序版本号.  可以通过设置在 AssemblyInfo.cs 文件中的版本属性来设置版本号.

    [assembly: AssemblyVersion("1.0.0.0")]

    编译应用程序并复制 1.0.0.0 版程序到你程序的 1.0.0.0 目录中. “C:\Program Files\YourApp\1.0.0.0“

    这里,你需要运行一下 AppStart.exe.  更新过程会失败,因为我们并没有把发布 ServerManifest XML 文件来指示应用程序新版本是否可用.  你可以检查日志文件,位置在 C:\Program Files\YourApp\ 目录中.

     

    10. 构建版本 1.0.0.1 

     

    首先, 通过更新应用程序的 AssemblyInfo.cs 和 App.config 文件内容来生成修订版本 1.0.0.1 .  编译程序, 然后复制文件到step 7生成的Web服务器目录中. 

    这里需要改动的地方很少。只要在AssemblyInfo.cs中修改[assembly: AssemblyVersion("1.0.0.1")]

    在App.config中修改<add key="VERSION" value="1.0.0.1" />

     

     

    11. 生成服务器的清单文件

     

    这个是最后一步.  如果你对本步骤中的.config文件作了任何修改的话,都必须把本步骤重来一遍.  做法如下:

     

    再次运行 ManifestUtility 程序. 

    在 “Update files folder“ 选择器中选择 1.0.0.1 目录 . 

    输入更新位置的 URL .  (这个是最容易出错的地方,不要漏掉/1.0.0.1)

    输入新版本号 1.0.0.1

    打开之前生成的 PrivateKey.xml 文件.

    选择验证类 “Microsoft.ApplicationBlocks.ApplicationUpdater.Validators.RSAValidator”

    鼠标点击 CreateManifest, 并保存 ServerManifest.xml 文件到你的虚拟服务器目录中. 它默认的文件名是Manifest.xml,要把它改过来的。

    从你的 C:\Program Files\YourApp\ 目录中运行你的 AppStart.exe .  你的程序就会被装入, 当你的程序运行的时候,你就会得到一个提示 “新版本可用” .  新版本会下载到目录 C:\Program Files\YourApp\1.0.0.1 中, 然后程序会自动重启.  如果有任何问题, 记得检查一下日志文件.  这些日志在诊断问题的时候会很有用的.

     

    细节方面:

    1.       除了使用BITS(background intellectual transmission service )方式传送之外还可以使用FTP方式传送。

    2.       假如在服务器端只需要放要更新的控件和可执行文件(*.exe),配置文件(*.exe.config)就行了。不需要把所有的文件都放进去,如果成功执行的话,程序会先把原先版本的所有文件复制到新版本的文件夹里面,并且会自动更新新的控件。

        

     

     

     

     

     

     

    附录:

     

         private ApplicationUpdateManager _updater = null;

             private Thread _updaterThread = null;

             private const int UPDATERTHREAD_JOIN_TIMEOUT = 3 * 1000;

             private delegate void MarshalEventDelegate( object sender, UpdaterActionEventArgs e );

             private void InitializeAutoUpdate()

             {

                  try

                  {

                       //  hook ProcessExit for a chance to clean up when closed peremptorily

                       AppDomain.CurrentDomain.ProcessExit +=new EventHandler(CurrentDomain_ProcessExit);

                       //  make an Updater for use in-process with us

                       _updater = new ApplicationUpdateManager();

                       //  hook Updater events

                       _updater.DownloadStarted +=new UpdaterActionEventHandler( OnUpdaterDownloadStarted );

                       _updater.FilesValidated +=new UpdaterActionEventHandler( OnUpdaterFilesValidated );

                       _updater.UpdateAvailable +=new UpdaterActionEventHandler( OnUpdaterUpdateAvailable );

                       _updater.DownloadCompleted +=new UpdaterActionEventHandler(OnUpdaterDownloadCompleted);

                       //  start the updater on a separate thread so that our UI remains responsive

                       _updaterThread = new Thread( new ThreadStart( _updater.StartUpdater ) );

                       _updaterThread.Start();

                  }

                  catch (Exception ex)

                  {MessageBox.Show(ex.Message);}

     

                  //  get version from config, set caption correctly

                  string version = System.Configuration.ConfigurationSettings.AppSettings["version"];

                   

     

                  this.Text = this.Text + String.Format(" v. {0}", version);

             }

             private void CurrentDomain_ProcessExit(object sender, EventArgs e)

             {

                  StopUpdater();

             }

             private void StopUpdater()

             {

                  //  tell updater to stop

                  _updater.StopUpdater();

                  if( null != _updaterThread )

                  {

                       //  join the updater thread with a suitable timeout

                       bool isThreadJoined = _updaterThread.Join( UPDATERTHREAD_JOIN_TIMEOUT );

                       //  check if we joined, if we didn't interrupt the thread

                       if( !isThreadJoined )

                       {    

                           _updaterThread.Interrupt();

                       }

                       _updaterThread = null;

                  }

             }

             /**//// <summary>

             /// This handler gets fired by the Windows UI thread that is the main STA thread for THIS FORM.  It takes the same 

             /// arguments as the event handler below it--sender, e--and acts on them using the main thread NOT the eventing thread

             /// </summary>

             /// <param name="sender">marshalled reference to the original event's sender argument</param>

             /// <param name="e">marshalled reference to the original event's args</param>

             private void OnUpdaterDownloadStartedHandler( object sender, UpdaterActionEventArgs e ) 

             {

                  Debug.WriteLine("Thread: " + Thread.CurrentThread.GetHashCode().ToString());

              

                  Debug.WriteLine(String.Format( "  DownloadStarted for application '{0}'", e.ApplicationName ));

             }

             /**//// <summary>

             /// Event handler for Updater event.  This event is fired by the originating thread from "inside" the Updater.  While it is

             /// possible for this same thread to act on our UI, it is NOT a good thing to do--UI is not threadsafe.  

             /// Therefore here we marshal from the Eventing thread (belongs to Updater) to our window thread using the synchronous Invoke

             /// mechanism.

             /// </summary>

             /// <param name="sender">event sender in this case ApplicationUpdaterManager</param>

             /// <param name="e">the UpdaterActionEventArgs packaged by Updater, which gives us access to update information</param>

             private void OnUpdaterDownloadStarted( object sender, UpdaterActionEventArgs e )

             { 

                  //  using the synchronous "Invoke".  This marshals from the eventing thread--which comes from the Updater and should not

                  //  be allowed to enter and "touch" the UI's window thread

                  //  so we use Invoke which allows us to block the Updater thread at will while only allowing window thread to update UI

                  Debug.WriteLine( String.Format( "[OnUpdaterDownloadStarted]Thread: {0}", Thread.CurrentThread.GetHashCode().ToString()) );

                  this.Invoke( 

                       new MarshalEventDelegate( this.OnUpdaterDownloadStartedHandler ), 

                       new object[] { sender, e } );

             }

             /**//// <summary>

             /// This handler gets fired by the Windows UI thread that is the main STA thread for THIS FORM.  It takes the same 

             /// arguments as the event handler below it--sender, e--and acts on them using the main thread NOT the eventing thread

             /// </summary>

             /// <param name="sender">marshalled reference to the original event's sender argument</param>

             /// <param name="e">marshalled reference to the original event's args</param>

             private void OnUpdaterFilesValidatedHandler( object sender, UpdaterActionEventArgs e )

             {

                  Debug.WriteLine(String.Format("FilesValidated successfully for application '{0}' ", e.ApplicationName));

        

     

                //hzh

                 GetPath(e.ServerInformation);

                  //  ask user to use new app

                //  DialogResult dialog = MessageBox.Show( 

                //  "Would you like to stop this application and open the new version?", "Open New Version?", MessageBoxButtons.YesNo );

                  DialogResult dialog = MessageBox.Show( 

                       "你想立即停用当前应用程序并运行新版本程序吗?", "运行新版本程序?", MessageBoxButtons.YesNo );

                  if( DialogResult.Yes == dialog )

                  {

                       StartNewVersion( e.ServerInformation );

                  }

             }

             /**//// <summary>

             /// Event handler for Updater event.  This event is fired by the originating thread from "inside" the Updater.  While it is

             /// possible for this same thread to act on our UI, it is NOT a good thing to do--UI is not threadsafe.  

             /// Therefore here we marshal from the Eventing thread (belongs to Updater) to our window thread using the synchronous Invoke

             /// mechanism.

             /// </summary>

             /// <param name="sender">event sender in this case ApplicationUpdaterManager</param>

             /// <param name="e">the UpdaterActionEventArgs packaged by Updater, which gives us access to update information</param>

             private void OnUpdaterFilesValidated( object sender, UpdaterActionEventArgs e )

             {

                  //  using the asynchronous "BeginInvoke".  

                  //  we don't need/want to block here

                  this.BeginInvoke( 

                       new MarshalEventDelegate( this.OnUpdaterFilesValidatedHandler ),

                       new object[] { sender, e } );

             }

             /**//// <summary>

             /// This handler gets fired by the Windows UI thread that is the main STA thread for THIS FORM.  It takes the same 

             /// arguments as the event handler below it--sender, e--and acts on them using the main thread NOT the eventing thread

             /// </summary>

             /// <param name="sender">marshalled reference to the original event's sender argument</param>

             /// <param name="e">marshalled reference to the original event's args</param>

             private void OnUpdaterUpdateAvailableHandler( object sender, UpdaterActionEventArgs e )

             {    

                  Debug.WriteLine("Thread: " + Thread.CurrentThread.GetHashCode().ToString());

    //            string message = String.Format( 

    //                 "Update available:  The new version on the server is {0} and current version is {1} would you like to upgrade?", 

    //                 e.ServerInformation.AvailableVersion,  

    //                 System.Configuration.ConfigurationSettings.AppSettings["version"] ) ;

                  string message = String.Format( 

                       "升级提示:服务器上最新的版本是 {0} ,你当前使用的版本是 {1} 。你希望进行升级吗?", 

                       e.ServerInformation.AvailableVersion,  

                       System.Configuration.ConfigurationSettings.AppSettings["version"] ) ;

                  //  for update available we actually WANT to block the downloading thread so we can refuse an update

                  //  and reset until next polling cycle;

                  //  NOTE that we don't block the thread _in the UI_, we have it blocked at the marshalling dispatcher "OnUpdaterUpdateAvailable"

    //            DialogResult dialog = MessageBox.Show( message, "Update Available", MessageBoxButtons.YesNo );

                 DialogResult dialog = MessageBox.Show( message, "升级提示", MessageBoxButtons.YesNo );

     

                  if( DialogResult.No == dialog )

                  {

                       //  if no, stop the updater for this app

                       _updater.StopUpdater( e.ApplicationName );

                       Debug.WriteLine("Update Cancelled.");

                  }

                  else

                  {

                       Debug.WriteLine("Update in progress.");

                  }

             }

             /**//// <summary>

             /// Event handler for Updater event.  This event is fired by the originating thread from "inside" the Updater.  While it is

             /// possible for this same thread to act on our UI, it is NOT a good thing to do--UI is not threadsafe.  

             /// Therefore here we marshal from the Eventing thread (belongs to Updater) to our window thread using the synchronous Invoke

             /// mechanism.

             /// </summary>

             /// <param name="sender">event sender in this case ApplicationUpdaterManager</param>

             /// <param name="e">the UpdaterActionEventArgs packaged by Updater, which gives us access to update information</param>

              private void OnUpdaterUpdateAvailable( object sender, UpdaterActionEventArgs e )

             {

                  //  using the synchronous "Invoke".  This marshals from the eventing thread--which comes from the Updater and should not

                  //  be allowed to enter and "touch" the UI's window thread

                  //  so we use Invoke which allows us to block the Updater thread at will while only allowing window thread to update UI

                  this.Invoke( 

                       new MarshalEventDelegate( this.OnUpdaterUpdateAvailableHandler ), 

                       new object[] { sender, e } );

             }

        

             /**//// <summary>

             /// This handler gets fired by the Windows UI thread that is the main STA thread for THIS FORM.  It takes the same 

             /// arguments as the event handler below it--sender, e--and acts on them using the main thread NOT the eventing thread

             /// </summary>

             /// <param name="sender">marshalled reference to the original event's sender argument</param>

             /// <param name="e">marshalled reference to the original event's args</param>

             private void OnUpdaterDownloadCompletedHandler( object sender, UpdaterActionEventArgs e )

             {

                  //  GetPath(e.ServerInformation);

                  Debug.WriteLine("Download Completed.");

             }

             /**//// <summary>

             /// Event handler for Updater event.  This event is fired by the originating thread from "inside" the Updater.  While it is

             /// possible for this same thread to act on our UI, it is NOT a good thing to do--UI is not threadsafe.  

             /// Therefore here we marshal from the Eventing thread (belongs to Updater) to our window thread using the synchronous Invoke

             /// mechanism.

             /// </summary>

             /// <param name="sender">event sender in this case ApplicationUpdaterManager</param>

             /// <param name="e">the UpdaterActionEventArgs packaged by Updater, which gives us access to update information</param>

             private void OnUpdaterDownloadCompleted( object sender, UpdaterActionEventArgs e )

             {

                  //  using the synchronous "Invoke".  This marshals from the eventing thread--which comes from the Updater and should not

                  //  be allowed to enter and "touch" the UI's window thread

                  //  so we use Invoke which allows us to block the Updater thread at will while only allowing window thread to update UI

                  this.Invoke( 

                       new MarshalEventDelegate( this.OnUpdaterDownloadCompletedHandler ), 

                       new object[] { sender, e } );

             }

             private void StartNewVersion( ServerApplicationInfo server )

             {

                  XmlDocument doc = new XmlDocument();

                  //  load config file to get base dir

                  doc.Load( AppDomain.CurrentDomain.SetupInformation.ConfigurationFile );

                  //  get the base dir

                  string baseDir = doc.SelectSingleNode("configuration/appUpdater/UpdaterConfiguration/application/client/baseDir").InnerText;

                  string newDir = Path.Combine( baseDir, "AppStart.exe" );

                MessageBox.Show(newDir);

                  ProcessStartInfo process = new ProcessStartInfo( newDir );

                  process.WorkingDirectory = Path.Combine( newDir , server.AvailableVersion );

                  //  launch new version (actually, launch AppStart.exe which HAS pointer to new version )

                  Process.Start( process );

                  //  tell updater to stop

                  CurrentDomain_ProcessExit( null, null );

                  //  leave this app

                  Environment.Exit( 0 );

             }

     

             //hzh

             private static void CopyDirRecurse( string sourcePath, string destinationPath )

             {

     

                  //  get dir info which may be file or dir info object

                  DirectoryInfo dirInfo = new DirectoryInfo( sourcePath );

     

                  foreach( FileSystemInfo fsi in dirInfo.GetFileSystemInfos() )

                  {

                       if ( fsi is FileInfo )

                       {

                           //  if file object just copy

                           if (File.Exists(destinationPath +@"\"+ fsi.Name)==false)

                           {

                                File.Copy( fsi.FullName,destinationPath+@"\" + fsi.Name,false);

                           }

                       }

                       else

                       {

                           //  must be a directory, create destination sub-folder and recurse to copy files

                           if (Directory.Exists(destinationPath +@"\"+ fsi.Name)==false)

                           {

                                Directory.CreateDirectory( destinationPath +@"\"+ fsi.Name );

                           }

                           CopyDirRecurse( fsi.FullName, destinationPath+@"\" + fsi.Name );

                       }

                  }

             }

     

             //hzh

             private void GetPath( ServerApplicationInfo server )

             {

                  XmlDocument doc = new XmlDocument();

                  //  load config file to get base dir

                  doc.Load( AppDomain.CurrentDomain.SetupInformation.ConfigurationFile );

                  //  get the base dir

                  string baseDir = doc.SelectSingleNode("configuration/appUpdater/UpdaterConfiguration/application/client/baseDir").InnerText;

                string sourcePath=baseDir+@"\"+System.Configuration.ConfigurationSettings.AppSettings["version"] ;

                 string destinationPath= baseDir+@"\"+server.AvailableVersion;

                  //  将旧版本的文件复制到新版本文件夹里

                  CopyDirRecurse(sourcePath,destinationPath);

             }

  • 相关阅读:
    又快又准的sql瓶颈诊断方法
    Qps从300到1500的优化过程
    Mysql性能优化全揭秘-庖丁解牛
    java学习笔记16-抽象类
    java学习笔记15-封装
    java学习笔记14-多态
    java学习笔记13-重写与重载
    Git学习笔记08-远程仓库
    Python3+Appium学习笔记09-元素定位android_uiautomator
    Python3+Appium学习笔记08-元素定位
  • 原文地址:https://www.cnblogs.com/cxy521/p/1238247.html
Copyright © 2020-2023  润新知