.Net缓存管理框架CacheManager

Cache缓存在计算机领域是一个被普遍使用的概念。硬件中CPU有一级缓存,二级缓存, 浏览器中有缓存,软件开发中也有分布式缓存memcache, redis。缓存无处不在的原因是它能够极大地提高硬件和软件的运行速度。在项目开发中,性能慢的地方常常是IO操作频繁的地方,读取数据库是我们常见的消耗性能的地方。这个时候,如果将使用频繁的数据缓存到能够高速读取的介质中,下次访问时,不用再去请求数据库,直接从缓存中获取所需的数据,就能够大大提高性能。这篇文章主要讨论的是在.Net开发中,如何使用CacheManager框架方便的管理项目中的缓存。

一,CacheManager介绍以及优点

CacheManager是开源的.Net缓存管理框架。它不是具体的缓存实现,而是在缓存之上,方便开发人员配置和管理各种不同的缓存,为上层应用程序提供统一的缓存接口的中间层。

下面是CacheManager的一些优点:

  • 让开发人员的生活更容易处理和配资缓存,即使是非常复杂的缓存方案。
  • CacheManager能够管理多种缓存,包含 内存, appfabric, redis, couchbase, windows azure cache, memorycache等。
  • 提供了额外的功能,如缓存同步、并发更新、事件、性能计数器等…

二,CacheManager开始之旅

CacheManager上手还是非常简单的。下面使用内存缓存结合CacheManager的一个实例,能够帮助我们快速的熟悉CacheManager如何使用。

首先在Visual Studio中创建一个Console Application.

使用Nuget为项目添加CacheManager包引用。CacheManager包含了很多的Package. 其中CacheManager.Core是必须的,其它的针对不同缓存平台上有不同的对应Package.

这个Demo中,我们使用内存作为缓存,所以只是需要CacheManager.Core和CacheManager.SystemRuntimeCaching

接着在Main函数中配置好我们的缓存:

复制代码
 1 using System;
 2 using CacheManager.Core;
 3 namespace ConsoleApplication
 4 {
 5     class Program
 6     {
 7         static void Main(string[] args)
 8         {
 9             var cache = CacheFactory.Build("getStartedCache", settings =>
10             {
11                 settings.WithSystemRuntimeCacheHandle("handleName");
12             });
13         }
14     }
15 }
复制代码

上面代码中使用CacheFactory创建了一个名称为getStartedCache的缓存实例,这个缓存实例使用的是SystemRunTime Cache, 内存缓存。一个缓存实例是可以配置多个Handle的,我们可以使用内存来作为存储介质,也可以使用Redis分布式缓存作为存储介质,并且可以同时在一个缓存实例中使用,后面我们再介绍多级缓存的配置和使用。

接下来,我们添加一些测试缓存的代码

复制代码
 1 static void Main(string[] args)
 2 {
 3 
 4     var cache = CacheFactory.Build("getStartedCache", settings =>
 5     {
 6         settings.WithSystemRuntimeCacheHandle("handleName");
 7     });
 8 
 9     cache.Add("keyA", "valueA");
10     cache.Put("keyB", 23);
11     cache.Update("keyB", v => 42);
12     Console.WriteLine("KeyA is "   cache.Get("keyA"));      // should be valueA
13     Console.WriteLine("KeyB is "   cache.Get("keyB"));      // should be 42
14     cache.Remove("keyA");
15     Console.WriteLine("KeyA removed? "   (cache.Get("keyA") == null).ToString());
16     Console.WriteLine("We are done...");
17     Console.ReadKey();
18 }
复制代码

三,CacheManager多级缓存配置

实际开发中,我们常常会需要使用多级缓存。

一种常见的情况是,你有一个分布式式缓存服务器,例如redis,独立的缓存服务器能够让我们的多个系统应用程序都能够共享这些缓存的数据,因为这些缓存项的创建是昂贵的。

和访问数据库相比,分布式缓存速度较快,但是和内存相比,还是不够快。因为分布式缓存使用还需要序列化和网络传输的时间消耗。

这个时候里,做个分级缓存是个好的解决方案,将内存缓存结合分布式缓存使用,使用频率高的数据直接从内存中读取,这将大大提高应用程序的整体性能。

使用内存缓存的读取速度能够达到分布式缓存的100倍,甚至更高。

使用CacheManager, 配置多级缓存是一件非常容易的事情

复制代码
 1 var cache = CacheFactory.Build("myCache", settings =>
 2 {
 3     settings
 4         .WithSystemRuntimeCacheHandle("inProcessCache")//内存缓存Handle
 5         .And
 6         .WithRedisConfiguration("redis", config =>//Redis缓存配置
 7         {
 8             config.WithAllowAdmin()
 9                 .WithDatabase(0)
10                 .WithEndpoint("localhost", 6379);
11         })
12         .WithMaxRetries(1000)//尝试次数
13         .WithRetryTimeout(100)//尝试超时时间
14         .WithRedisBackPlate("redis")//redis使用Back Plate
15         .WithRedisCacheHandle("redis", true);//redis缓存handle
16 });
复制代码

上面代码中,内存缓存和Redis缓存配置部分很容易看明白。但是BackPlate是什么作用? 接下来,我们看看CacheManager中的BackPlate挡板机制。

四, BackPlate解决分布式缓存中的同步问题

对于大型的软件系统,常常都是分为很多独立的子项目,各个子项目为了节约成本或者是方便数据共享,常常会共用同一个分布缓存服务器。这样在使用多级缓存的时候,就有可能出现数据不一致的情况。

假设在系统A中的更新了缓存中的一个数据项,这个时候CacheManager会在A设置的所有的缓存handle中更新改数据,这里也包括了分布式缓存上的数据。但是在系统B中的内存缓存中,还是会存在着旧的未更新的数据。当系统B从缓存中取这条记录的时候,就会出现内存缓存和分布式缓存中的数据不一致的情况。

为了防止这一点,缓存管理器有一个功能叫做cachebackplate将尝试同步多个系统中的缓存。

上面设置的多级缓存中,我们就将redis作为BackPlate的源. 也就是说所有的数据都需要以redis中缓存的数据为蓝本。

在设置redis作为BackPlate之后,同样发生上面的数据不一致的情况的时候,只要redis中的数据被修改了,就会触发CacheManager更新所有系统中的内存缓存中的数据,和redis中的数据保持一致。

同步的工作是如何完成的?

每次一条缓存记录被删除或更新的时候,Cache Manager会发送一个消息,让BackPlate存储这次的数据变化信息。所有其它的系统将异步接收这些消息,并将相应地作出更新和删除操作,保证数据的一致性。

五,ExpirationMode和CacheUpdateMode

涉及到缓存,就必然有缓存过期的问题。CacheManager中提供了一些简单的缓存过期方式设置。

复制代码
1 public enum ExpirationMode
2 {
3     None = 0,
4     Sliding = 1,
5     Absolute = 2,
6 }
复制代码

同时CacheManager还为多级缓存之间设置不同的数据更新策略

复制代码
1 public enum CacheUpdateMode
2 {
3     None = 0,
4     Full = 1,
5     Up = 2,
6 }
复制代码

使用Sliding和Up, 我们我可以为多级缓存设置不同的缓存过期时间,这样使用频率高的数据就能够保存在访问速度更快的内存中,访问频率次高的放到分布式缓存中。当CacheManager在内存中找不到缓存数据的时候,就会尝试在分布式缓存中找。找到后,根据Up设置,会再将该缓存数据保存到内存缓存中。

具体的配置方式如下:

复制代码
 1 var cache = CacheFactory.Build("myCache", settings =>
 2 {
 3     settings.WithUpdateMode(CacheUpdateMode.Up)
 4         .WithSystemRuntimeCacheHandle("inProcessCache")//内存缓存Handle
 5         .WithExpiration(ExpirationMode.Sliding, TimeSpan.FromSeconds(60)))
 6         .And
 7         .WithRedisConfiguration("redis", config =>//Redis缓存配置
 8         {
 9             config.WithAllowAdmin()
10                 .WithDatabase(0)
11                 .WithEndpoint("localhost", 6379);
12         }).
13         .WithExpiration(ExpirationMode.Sliding, TimeSpan. FromHours  (24)))
14         .WithMaxRetries(1000)//尝试次数
15         .WithRetryTimeout(100)//尝试超时时间
16         .WithRedisBackPlate("redis")//redis使用Back Plate
17         .WithRedisCacheHandle("redis", true);//redis缓存handle
18 
19 });
复制代码

六,缓存使用分析

在缓存使用中,对于缓存hit和miss数据态比较关系,这些数据能够帮助我们分析和调整缓存的设置,帮助缓存使用地更加合理。

1 var cache = CacheFactory.Build("cacheName", settings => settings
2     .WithSystemRuntimeCacheHandle("handleName")
3         .EnableStatistics()
4         .EnablePerformanceCounters());

在配置好缓存的Statistic功能后,我们就能够跟踪到缓存的使用情况了, 下面就是分别打印各个缓存handle中的分析数据。

复制代码
 1 foreach (var handle in cache.CacheHandles)
 2 {
 3     var stats = handle.Stats;
 4     Console.WriteLine(string.Format(
 5             "Items: {0}, Hits: {1}, Miss: {2}, Remove: {3}, ClearRegion: {4}, Clear: {5}, Adds: {6}, Puts: {7}, Gets: {8}",
 6                 stats.GetStatistic(CacheStatsCounterType.Items),
 7                 stats.GetStatistic(CacheStatsCounterType.Hits),
 8                 stats.GetStatistic(CacheStatsCounterType.Misses),
 9                 stats.GetStatistic(CacheStatsCounterType.RemoveCalls),
10                 stats.GetStatistic(CacheStatsCounterType.ClearRegionCalls),
11                 stats.GetStatistic(CacheStatsCounterType.ClearCalls),
12                 stats.GetStatistic(CacheStatsCounterType.AddCalls),
13                 stats.GetStatistic(CacheStatsCounterType.PutCalls),
14                 stats.GetStatistic(CacheStatsCounterType.GetCalls)
15             ));
16 }
复制代码

七,结尾

缓存是个好东西,用好了能够极大的提高性能。缓存的使用本身是个很大的话题,这边文章只是从缓存管理这个角度介绍了CachManager的使用。

下面是CacheManager相关的资料和链接:

官方主页

http://cachemanager.net/

源代码

https://github.com/MichaCo/CacheManager

官方MVC项目的Sample

https://github.com/MichaCo/CacheManager/tree/master/samples/CacheManager.Samples.Mvc

最近在思考不同情况下缓存使用的区别问题。对于互联网项目来说,数据的一致性要求常常不太高,缓存管理中,关注点可能在缓存的命中率上。对于应用系统,访问请求不大,但是对于数据的一致性要求较高,缓存中的数据更新策略可能更加重要。

怎样才是好的适合应用系统的缓存设计呢? 如果大家有兴趣,欢迎探讨指教

正则表达式整理

 普通字符

符号 说明
. 除“\n”之外的任何单个字符。要匹配“\n”在内的任何字符,请使用像“(.|\n)”的模式。在中括号表达式时 [.] 只会匹配 .字符,等价于 .
\d 匹配一个数字字符。等价于[0-9]。
\w 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”
\s 匹配任意的空白符,包括空格、制表符、换页符等等。等价于 [\f\n\r\t\v]。
\f 匹配一个换页符。等价于\x0c和\cL。
\n 匹配一个换行符。等价于\x0a和\cJ。
\r 匹配一个回车符。等价于\x0d和\cM。
\t 匹配一个制表符。等价于\x09和\cI。
\v 匹配一个垂直制表符。等价于\x0b和\cK。

定位符

一个网站如果要求你填写的QQ号必须为5位到12位数字时,可以使用:^\d{5,12}$。因为使用了^和$,所以输入的整个字符串都要用来和\d{5,12}来匹配,也就是说整个输入必须是5到12个数字.

符号 说明
^ 匹配字符串的开始
$ 匹配字符串的结束
\b 匹配单词的开始或结束 例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。

字符集合

要想查找数字,字母或数字,空白是很简单的,因为已经有了对应这些字符集合的元字符,但是如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办?

符号 说明
x y
[xyz] 字符集合。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。
[a-z] 字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。

反义字符

有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义

符号 说明
\D 匹配一个非数字字符。等价于[^0-9]。
\W 匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。
\S 匹配任何非空白字符。等价于[^ \f\n\r\t\v]。
\B 匹配不是单词开头或结束的位置。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。
[^xyz] 负值字符集合。匹配未包含的任意字符。例如,“[^abc]”可以匹配“plain”中的“p”。

限定符

符号 说明
* 匹配前面的子表达式零次或多次。*等价于{0,}。
+ 匹配前面的子表达式一次或多次。+等价于{1,}。
? 匹配前面的子表达式零次或一次。?等价于{0,1}。
{n} 匹配确定的n次。
{n,} 至少匹配n次。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。
{n,m} 最少匹配n次且最多匹配m次。“o{0,1}”等价于“o?”。

贪婪与懒惰

非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。

? 紧跟在任何一个其他限制符 *,+,?,{n},{n,},{n,m} 后面时,匹配模式是非贪婪的。

例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”

符号 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

分组

符号 说明
\num 匹配num,其中num是一个正整数。对所获取的匹配的引用。例如,“(.)\1”匹配两个连续的相同字符。
$num 替换匹配的引用
(pattern) 匹配exp,并捕获文本到自动命名的组里。在JScript 中则使用 $0…$9 属性
(?exp) 匹配exp,并捕获文本到名称为name的组里,也可以写成(?’name’exp)
(?:pattern) 匹配exp,不捕获匹配的文本,也不给此分组分配组号
(?=pattern) 正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95
(?<=pattern) 反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“(?<=95
(?!pattern) 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95
(?<!pattern) 反向否定预查,与正向否定预查类拟,只是方向相反。例如“(?<!95

 

正向预查

现在,我们假设需要仅匹配 Windows,不匹配后面的版本号,并且要求 Windows 后面的版本号只能是 数字类型,换言之,XP 和 Vista 不能被匹配,
在正则表达式中,可以使用 正向预查 来解决这个问题。本例中,写法是:“Windows(?= [\d.]+\b)”。
它的语法是在 子模式内部 前面加“?=”,表示的意思是:首先,要匹配的文本必须满足此子模式前面的表达式(本例,“Windows ”);其次,此子模式不参与匹配。

Text:
Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.
Windows 95 and Windows 98 are the successor.
Then Windows 2000 and Windows Xp appeared.
Windows Vista is the Latest version of the family.

RegEx:
Windows( ?=[\d.]+\b)

Result:(带下划线的为成功匹配的)
Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.
Windows 95 and Windows 98 are the successor.
Then Windows 2000 and Windows Xp appeared.
Windows Vista is the Latest version of the family.

可以将 正向预查 理解成为自定义的边界(\b),这个边界位于表达式末。

引用

mac 装双系统..

1.U盘格式化为ms-dos(fat)  主引导记录..注意U盘名字不能是中文..否则格式化失败

2.无法创建可引导的USB驱动器—-要先在磁盘工具里面移除 windows 的ISO!

3.按options选项..进入efi硬盘

4.不能装docker  说虚拟化不能开启…任务管理器.cpu可以看到

https://apple.stackexchange.com/questions/120361/how-to-turn-on-hardware-virtualization-on-late-2013-macbook-pro-for-windows-8-1?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

It sounds like you\’re running into the same issue I did, where after booting into Windows the VT-x shows as \’Disabled\’ in Task Manager.

Not sure how or why, but after going into

  • OS X
  • System Preferences
  • Target Disk
  • Select the BOOTCAMP disk as the startup disk

Everything was well after that and I could happily use Hyper-V, even from a cold boot.

If I cold booted using the Options-key, and then selecting Windows, VT-x was disabled in Task Manager.

Go figure. Could some Mac genius out there explain this one?

This thread explains that you have to boot using the CSM-BIOS layer. discussions.apple.com/thread/6720461?tstart=0 ; In addition it also provides a command line to permanently fix this problem. First use diskutil list to work out Windows partition, then sudo /usr/sbin/bless --device /dev/disk0s4 --setBoot --legacy --legacydrivehint /dev/disk0 – Chui Tey Dec 23 \’16 at 23:10

接着上面的说,这个在mac本上就没有BIOS主板系统,但是昂贵的Mac肯定也是有虚拟化服务的~只不过Mac本不是手动启动,而是每次启动完OSX系统自动启动~ 但是如果 第一次启动的是 bootcamp的Windows 系统 那么 这个 虚拟化是启动不了的。。。这时候有一个解决办法就是先启动OSX系统,再更具目标磁盘重启到bootcamp的Windows系统~ 参考下图~

通过这个启动盘重启的Windows虚拟化是 打开的状态~ 如下图

虚拟化状态打开后就可以下载安装 HoloLens 的模拟器了 ~ 而且在开发调试中 也一定要把虚拟化打开

typescript

Typescript 2.0之后,tsd和typings都可以去掉了。要获得lodash的类型定义文件只需要

npm install @types/lodash

这样一来,typescript的工作流就和普通的Node.js项目没什么区别了。
更重要的是Typescript 2.1之后,async/await可以直接编译到ES5。babel什么的,再见吧

https://github.com/typings/typings

关于持续集成打包平台的Jenkins配置和构建脚本实现细节

关于iOS的构建

对iOS源码进行构建,目标是要生成.ipa文件,即iOS应用安装包。

当前,构建方式主要包括两种:

  • 源码 -> .archive文件 -> .ipa文件
  • 源码 -> .app文件 -> .ipa文件

这两种方式的主要差异是生成的中间产物不同,对应的,两种构建方式采用的命令也不同。

源码 -> .archive -> .ipa

1
2
3
4
5
6
7
8
# build archive file from source code
xcodebuild \ # xctool
-workspace ${WORKSPACE_PATH} \
-scheme ${SCHEME} \
-configuration ${CONFIGURATION} \
-sdk ${SDK}
-archivePath ${archive_path}
archive

archive:对编译结果进行归档,会生成一个.xcarchive的文件,位于-archivePath指定的目录中。需要注意的是,对模拟器类型的sdk无法使用archive命令。

1
2
3
4
5
6
7
8
# export ipa file from .archive
xcodebuild -exportArchive \
-exportFormat format \
-archivePath xcarchivepath \
-exportPath destinationpath \
-exportProvisioningProfile profilename \
[-exportSigningIdentity identityname]
[-exportInstallerIdentity identityname]

源码 -> .app -> .ipa

1
2
3
4
5
6
7
# build .app file from source code
xcodebuild \ # xctool
-workspace ${WORKSPACE_PATH} \
-scheme ${SCHEME} \
-configuration ${CONFIGURATION} \
-sdk ${SDK}
-derivedDataPath ${OUTPUT_FOLDER}
1
2
3
4
5
6
# convert .app file to ipa file
xcrun \
-sdk iphoneos \
PackageApplication \
-v ${OUTPUT_FOLDER}/Release-iphoneos/xxx.app \
-o ${OUTPUT_FOLDER}/Release-iphoneos/xxx.ipa

参数说明

xcodebuild/xctool参数

  • -workspace:需要打包的workspace,后面接的文件一定要是.xcworkspace结尾的;
  • -scheme:需要打包的Scheme,一般与$project_name相同;
  • -sdk:区分iphone device和Simulator,可通过xcodebuild -showsdks获取,例如iphoneosiphonesimulator9.3
  • -configuration:需要打包的配置文件,我们一般在项目中添加多个配置,适合不同的环境,Release/Debug;
  • -exportFormat:导出的格式,通常填写为ipa
  • -archivePath.xcarchive文件的路径;
  • -exportPath:导出文件(.ipa)的路径;
  • -exportProvisioningProfile:profile文件证书;
  • -derivedDataPath:指定编译结果文件的存储路径;例如,指定-derivedDataPath ${OUTPUT_FOLDER}时,将在项目根目录下创建一个${OUTPUT_FOLDER}文件夹,生成的.app文件将位于${OUTPUT_FOLDER}/Build/Products/${CONFIGURATION}-iphoneos中。

除了采用官方的xcodebuild命令,还可以使用由Facebook开发维护的xctoolxctool命令的使用方法基本与xcodebuild一致,但是输出的日志会清晰很多,而且还有许多其它优化,详情请参考xctool的官方文档。

xcrun参数

  • -v:指定.app文件的路径
  • -o:指定生成.ipa文件的路径

补充说明

1、获取Targets、Schemes、Configurations参数

在填写target/workspace/scheme/configuration等参数时,如果不知道该怎么填写,可以在项目根目录下执行xcodebuild -list命令,它会列出当前项目的所有可选参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜ Store_iOS git:(NPED) ✗ xcodebuild -list
Information about project “Store”:
Targets:
Store
StoreCI
Build Configurations:
Debug
Release
If no build configuration is specified and -scheme is not passed then “Release” is used.
Schemes:
Store
StoreCI

2、清除缓存文件

在每次build之后,工程目录下会遗留一些缓存文件,以便下次build时减少编译时间。然而,若因为工程配置错误等问题造成编译失败后,下次再编译时就可能会受到缓存的影响。

因此,在持续集成构建脚本中,比较好的做法是在每次build之前都清理一下上一次编译遗留的缓存文件。

1
2
3
4
5
6
# clean before build
xctool \
-workspace ${WORKSPACE_PATH} \
-scheme ${SCHEME} \
-configuration ${CONFIGURATION} \
clean

clean:清除编译产生的问题,下次编译就是全新的编译了

3、处理Cocoapod依赖库

另外一个需要注意的是,若项目是采用Cocoapod管理项目依赖,每次拉取最新代码后直接编译可能会报错。这往往是因为其他同事更新了依赖库(新增了第三方库或升级了某些库),而本地还采用之前的第三方库进行编译,从而会出现依赖库缺失或版本不匹配等问题。

应对的做法是,在每次build之前都更新一下Cocoapod。

1
2
3
4
# Update pod repository
pod repo update
# Install pod dependencies
pod install

4、修改编译包的版本号

通过持续集成打包,我们会得到大量的安装包。为了便于区分,比较好的做法是在App中显示版本号,并将版本号与Jenkins的BUILD_NUMBER关联起来。

例如,当前项目的主版本号为2.6.0,本次构建的BUILD_NUMBER为130,那么我们就可以将本次构建的App版本号设置为2.6.0.130。通过这种方式,我们可以通过App中显示的版本号快速定位到具体到构建历史,从而对应到具体的代码提交记录。

要实现对App版本号的设置,只需要在打包前对Info.plist文件中的CFBundleVersionCFBundleShortVersionString进行修改即可。在Python中,利用plistlib库可以很方便地实现对Info.plist文件的读写。

5、模拟器运行

如果持续集成测试是要运行在iOS模拟器上,那么就需要构建生成.app文件。

在前面讲解的两种构建方式中,中间产物都包含了.app文件。对于以.xcarchive为中间产物的方式,生成的.app文件位于output_dir/StoreCI_Release.xcarchive/Products/Applications/目录中。

不过,这个.app文件在模拟器中还无法直接运行,还需要在Xcode中修改Supported Platforms,例如,将iphoneos更改为iOS。详细原因请参考《从0到1搭建移动App功能自动化测试平台(1):模拟器中运行iOS应用》

关于Android的构建

待续

关于构建脚本

对于构建脚本(build.py)本身,源码应该是最好的说明文档。

build.py脚本中,主要实现的功能就四点:

  • 执行构建命令,编译生成.ipa文件,这部分包含了关于iOS的构建部分的全部内容;
  • 构建时动态修改Info.plist,将编译包的版本号与Jenkins的BuildNumber关联起来;
  • 上传.ipa文件至pyger/fir.im平台,并且做了失败重试机制;
  • 解析pyger/fir.im平台页面中的二维码,将二维码图片保存到本地。

需要说明的是,对于构建任务中常用的可配置参数,例如BRANCH/SCHEME/CONFIGURATION/OUTPUT_FOLDER等,需要在构建脚本中通过OptionParser的方式实现可传参数机制。这样我们不仅可以命令行中通过传参的方式灵活地调用构建脚本,也可以在Jenkins中实现参数传递。

之所以强调常用的可配置参数,这是为了尽可能减少参数数目,降低脚本调用的复杂度。像PROVISIONING_PROFILEpgyer/fir.im账号这种比较固定的配置参数,就可以写死在脚本中。因此,在使用构建脚本(build.py)之前,需要先在脚本中配置下PROVISIONING_PROFILEpgyer/fir.im账号。

另外还想多说一句,pyger/fir.im这类第三方平台在为我们提供便利的同时,稳定性不可控也是一个不得不考虑的问题。在我使用pgyer平台期间,就遇到了平台服务变动、接口时而不稳定出现502等问题。因此,最好的方式还是自行搭建一套类似的服务,反正我是打算这么做了。

Jenkins的详细配置

对于Jenkins的详细配置,需要补充说明的有四点。

1、参数的传递

在构建脚本中,我们已经对常用的可配置参数实现了可传参机制。例如,在Terminal中可以通过如下形式调用构建脚本。

1
$ python build.py –scheme SCHEME –workspace Store.xcworkspace –configuration CONFIGURATION –output OUTPUT_FOLDER

那么我们在Jenkins中要怎样才能指定参数呢?

实际上,Jenkins针对项目具有参数化的功能。在项目的配置选项中,勾选This project is parameterized后,就可以为当前project添加多种类型的参数,包括:

  • Boolean Parameter
  • Choice Parameter
  • Credentials Parameter
  • File Parameter
  • Multi-line String Parameter
  • Password Parameter
  • Run Parameter
  • String Parameter

通常,我们可以选择使用String Parameter来定义自定义参数,并可对每个参数设置默认值。

当我们配置了BRANCHSCHEMECONFIGURATIONOUTPUT_FOLDERBUILD_VERSION这几个参数后,我们就可以在Build配置区域的Execute shell通过如下形式来进行参数传递。

1
2
3
4
5
6
$ python ${WORKSPACE}/Build_scripts/build.py \
–scheme ${SCHEME} \
–workspace ${WORKSPACE}/Store.xcworkspace \
–configuration ${CONFIGURATION} \
–output ${WORKSPACE}/${OUTPUT_FOLDER} \
–build_version ${BUILD_VERSION}.${BUILD_NUMBER}

可以看出,参数的传递方式很简单,只需要预先定义好了自定义参数,然后就可以通过${Param}的形式来进行调用了。

不过你也许会问,WORKSPACEBUILD_NUMBER这两个参数我们并未进行定义,为什么也能进行调用呢?这是因为Jenkins自带部分与项目相关的环境变量,例如BRANCH_NAMEJOB_NAME等,这部分参数可以在shell脚本中直接进行调用。完整的环境变量可在Jenkins_Url/env-vars.html/中查看。

配置完成后,就可以在Build with Parameters中通过如下形式手动触发构建。

Jenkins manul build

2、修改build名称

Build History列表中,构建任务的名称默认显示为按照build次数递增的BUILD_NUMBER。有时候我们可能想在build名称中包含更多的信息,例如包含当次构建的SCHEMECONFIGURATION,这时我们就可以通过修改BuildName实现。

Jenkins默认不支持BuildName设置,但可通过安装build-name-setter插件进行实现。安装build-name-setter插件后,在配置页面的Build Environment栏目下会出现Set Build Name配置项,然后在Build Name中就可以通过环境变量参数来设置build名称。

例如,要将build名称设置为上面截图中的StoreCI_Release_#130样式,就可以在Build Name中配置为${SCHEME}_${CONFIGURATION}_#${BUILD_NUMBER}

除了在Build Name中传递环境变量参数,build-name-setter还可以实现许多更加强大的自定义功能,大家可自行探索。

3、展示二维码图片

然后再说下如何在Build History列表中展示每次构建对应的二维码图片。

Jenkins build history

需要说明的是,在上图中,绿色框对应的内容是BuildName,我们可以通过build-name-setter插件来实现自定义配置;但是红色框已经不在BuildName的范围之内,而是对应的BuildDescription

同样地,Jenkins默认不支持在构建过程中自动修改BuildDescription,需要通过安装description setter plugin插件来辅助实现。安装description setter plugin插件后,在配置页面的Build栏目下,Add build step中会出现Set build description配置项,添加该配置项后就会出现如下配置框。

Jenkins set build description

该功能的强大之处在于,它可以在构建日志中通过正则表达式来匹配内容,并将匹配到的内容添加到BuildDescription中去。

例如,我们想要展示的二维码图片是在每次构建过程中生成的,因此我们首先要获取到二维码图片文件。

我的做法是,在build.py中将蒲公英平台返回的应用下载页面地址和二维码图片地址打印到log中。

1
2
3
appDownloadPage: https://www.pgyer.com/035aaf10acf5dd7c279c4fe423a57674
appQRCodeURL: https://o1wjx1evz.qnssl.com/app/qrcodeHistory/fe7a8c9051f0c7fc0affc78f40c20a4b5e4bdb4c77b91a29501f55fd9039c659
Save QRCode image to file: /Users/Leo/.jenkins/workspace/DebugTalk_Plus_Store_iOS/build_outputs/QRCode.png

然后,在Set build description配置项的Regular expression就可以按照如下正则表达式进行匹配:

1
appDownloadPage: (.*)$

接下来,就可以在Description中对匹配到的结果进行引用。

1

在这里,我们用到了HTML的标签,而Jenkins的Markup Formatter默认是采用Plain text模式,因此还需要对Jenkins对系统配置进行修改,在《使用Jenkins搭建iOS/Android持续集成打包平台》中已进行了详细说明,在此就不再重复。

通过以上方式,就可以实现前面图片中的效果。

4、收集编译成果物

在上面讲解的展示二维码图片一节中,用到了${BUILD_URL}artifact/build_outputs/QRCode.png一项,这里的URL就是用到了编译成果物收集后保存的路径。

Archives build artifacts是Jenkins默认自带的功能,无需安装插件。该功能在配置页面的Post-build Actions栏目下,在Add post-build action的列表中选择添加Archives build artifacts

添加后的配置页面如下图所示:

Jenkins archive the artifacts

通常,我们只需要配置Files to archive即可。定位文件时,可以通过正则表达式进行匹配,也可以调用项目的环境变量;多个文件通过逗号进行分隔。

例如,假如我们想收集QRCode.pngStoreCI_Release.ipaInfo.plist这三个文件,那么我们就可以通过如下表达式来进行指定。

1
${OUTPUT_FOLDER}/*.ipa,${OUTPUT_FOLDER}/QRCode.png,${OUTPUT_FOLDER}/*.xcarchive/Info.plist

当然,目标文件的具体位置是我们在构建脚本(build.py)中预先进行处理的。

通过这种方式,我们就可以实现在每次完成构建后将需要的文件收集起来进行存档,以便后续在Jenkins的任务页面中进行下载。

show artifacts of Jenkins

也可以直接通过归档文件的URL进行访问。例如,上图中QRCode.png的URL为Jenkins_Url/job/JenkinsJobName/131/artifact/build_outputs/QRCode.png,而Jenkins_Url/job/JenkinsJobName/131/即是${BUILD_URL},因此可以直接通过${BUILD_URL}artifact/build_outputs/QRCode.png引用。

总结

至此,《使用Jenkins搭建iOS/Android持续集成打包平台》一文中涉及到的Jenkins配置和构建脚本实现细节均已补充完毕了。相信大家结合这两篇文章,应该会对如何使用Jenkins搭建iOS/Android持续集成打包平台的基础概念和实现细节都有一个比较清晰的认识。

对于还未完善的部分,我后续将在博客中进行更新。

操作手册请参考文章末尾的【开箱即用】部分,祝大家玩得愉快!

开箱即用

GitHub地址:https://github.com/debugtalk/JenkinsTemplateForApp

1、添加构建脚本

  • 在构建脚本中配置PROVISIONING_PROFILEpgyer/fir.im账号;
  • 在目标构建代码库的根目录中,创建Build_scripts文件夹,并将build.py拷贝到Build_scripts中;
  • Build_scripts/build.py提交到项目中。

除了与Jenkins实现持续集成,构建脚本还可单独使用,使用方式如下:

1
2
3
4
5
$ python ${WORKSPACE}/Build_scripts/build.py \
true–scheme ${SCHEME} \
–workspace ${WORKSPACE}/Store.xcworkspace \
–configuration ${CONFIGURATION} \
–output ${WORKSPACE}/${OUTPUT_FOLDER}

2、运行jenkins,安装必备插件

1
$ nohup java -jar jenkins_located_path/jenkins.war &

3、创建Jenkins Job

  • 在Jenkins中创建一个Freestyle project类型的Job,先不进行任何配置;
  • 然后将config.xml文件拷贝到~/.jenkins/jobs/YourProject/中覆盖原有配置文件,重启Jenkins;
  • 完成配置文件替换和重启后,刚创建好的Job就已完成了大部分配置;
  • Job Configure中根据项目实际情况调整配置,其中Git Repositories是必须修改的,其它配置项可选择性地进行调整。

4、done!

Unityaction vs Method

 void Awake() {
         action = new UnityAction (Func);
         action += Funcb;
         gameObject.GetComponent<Button> ().onClick.AddListener (action);
         gameObject.GetComponent<Button> ().onClick.AddListener (Func);
         gameObject.GetComponent<Button> ().onClick.AddListener (Funcb);
         gameObject.GetComponent<Button> ().onClick.RemoveListener (Func);
         gameObject.GetComponent<Button> ().onClick.RemoveListener (Funcb);
     }

 

Unityaction可以绑定多个动作….如果是一个匿名函数..可以使用unityaction..方便remove

 

 

 

在unity向量空间内通过将极坐标转换为直角坐标,绘制阿基米德螺线,对数螺线与玫瑰线等几何图形

极坐标内的每个点都有两个参数: r, 与 θ。r为此点到极点(中心点)的距离,θ 为此点到极点的线段与极轴(类似x轴)的夹角。

很多几何图形公式都可以用极坐标简洁的表示,例如:

阿基米德螺旋线:(公式1) r=a b*θ
这里写图片描述

对数螺旋线:(公式2)r=aebθ
这里写图片描述

心脏线:(公式3)r=a(1sinθ)
这里写图片描述
玫瑰线:(公式4)r=cos(kθ) c
这里写图片描述

以上的a,b,c,k都是常数,用来控制图形的形状。
已知了r 与 θ 的关系,只要依次算出θ从0至2π相应的r的值,我们就能得出一个连续的极坐标(r, θ)数组。

极坐标与直角坐标的转换
公式:(公式5)x=rcos(θ) y=rsin(θ)

将以上信息转化为代码既是:

public Vector3[] GeneratePolarRoses(int points,Vector3 centre,float k,float c,float scale){

        Vector3[] coordinates=new Vector3[points]; 
        //将2π均分为points份,并依次赋予theta
        float theta=2f*Mathf.PI/points;
        float radius;
        for(int t=0;t<points;t  ){
            //公式4转换为代码,算出theta从0至2π每个角度的r值
            radius=Mathf.Cos(k*theta) c;
            //公式5转换为代码,既是将极坐标转换为直角坐标
            coordinates[t]=new Vector3(scale*radius*Mathf.Cos(theta) centre.x,scale*radius*Mathf.Sin(theta) centre.y,0f);
            theta =2f*Mathf.PI/points;
        }
        return coordinates;
    }

以上函数将会返回一个玫瑰线坐标点数组,参数points为数组长度,centre为图形中心点坐标。参数k必须为整数,当为奇数时,花瓣数量为k,当为偶数时,花瓣数量为2k。参数c也会影响图形形状,当为0-1之间时,将会产生类似花蕊的效果。参数scale可以设置图形整体大小。

阿基米德螺旋线与对数螺旋线的思路基本相同,唯一不同的是,需要增加一个参数设置螺旋的圈数。

    public Vector3[] GenerateArchimedeanSpirals(int points,Vector3 centre,int circles,float a,float b){

        Vector3[] coordinates=new Vector3[points]; 

        float radius;
        //调整角度的分配模式,circles为螺旋的圈数
        float theta=2f*Mathf.PI/points*circles;
        for(int t=0;t<points;t  ){
            //公式1
            radius=a b*theta;   
            //公式5   
            coordinates[t]=new Vector3(radius*Mathf.Cos(theta) centre.x,radius*Mathf.Sin(theta) centre.y,0f);
            theta =2f*Mathf.PI/points*circles;
        }
        return coordinates;
    }

对数螺线

    public Vector3[] GenerateLogarithmicSpirals(int points,Vector3 centre,int circles,float a,float b){

        Vector3[] coordinates=new Vector3[points]; 

        float radius;
        float theta=2f*Mathf.PI/points*circles;
        for(int t=0;t<points;t  ){
            //公式2
            radius=a*Mathf.Exp(b*theta);
            coordinates[t]=new Vector3(radius*Mathf.Cos(theta) centre.x,radius*Mathf.Sin(theta) centre.y,0f);
            theta =2f*Mathf.PI/points*circles;
            scales[t]=radius;
        }
        return coordinates;
    }

对数螺旋线
上图:一个a为0.1,b为0.1,共13圈的对数螺线。

Canvas控件——CanvasGroup

Canvas Group可以用来控制一组不需要个别控制的UI元素的某些方面,CanvasGroup的属性会影响他所有children的GameObject
img
其中有四个选项:
-Alpha:这个选项很多组件都有,用处也是一样的,在美术中,这个叫做Alpha通道的东东是用来控制透明度的,他的值从0到1.0是完全透明,1是完全不透明;
-Interactable确认该组件是否接受输入,当他被设置为false时,交互功能将被禁用;
-Block Raycasts是否让该组件像collider一样接受射线检测?你需要在依赖于Canvas的图形射线检测者上唤醒射线检测方法。这个不会作用于Physics.Raycast;
-Ignore Parent Groups(忽略父级团)是否响应父级group的方法
细节:
Canvas Group的经典使用:
-在窗口的GameObject上添加一个CanvasGroup,通过控制它的Alpha值来淡入或淡出整个窗口;
-通过给父级GameObject添加一个CanvasGroup并设置它的Interactable值为false来制作一整套没有交互(灰色)的控制;
-通过将元素或元素的一个父级添加CanvasGroup并设置BlockRaycasts值为false来制作一个或多个不阻止鼠标事件的UI元素
应用:(重要的地方写大字)
结合后面两点或者1,3点,都可以实现很牛叉的功能
比如说游戏里某些情况某个按钮(或者其他UI)是不能点的,而另外一些情况可以点,这样就可以通过动态改变这个组件的BlocksRaycasts值以及Interactable来实现
再比如说游戏里点击某个按钮要让这个按钮不可点并逐渐消失掉,当然啦,也可以让别的东西消失啦,这就可以通过改变alpha值来实现