推荐阅读:

滴滴Booster移动App质量优化框架-学习之旅 一

Android 模块Api化演练

不一样视角的Glide剖析(一)

续写滴滴Booster移动APP质量优化框架学习之旅,上篇文章分析内置的transform:booster-transform-shrink booster-transform-shared-preferences,今天分析booster-task-compression以及定制task对资源索引文件resource.asrc进行优化(重复资源优化、无用资源优化)。

booster-task-compression

该task对针对资源的压缩和删除,减少了apk包的大小。原理主要对资源编译打包过程进行了hook干涉,添加了三个Task:

removeRedundantResources、compressAssets、compressResources,其先相关task依赖关系图谱如下:

且在processResources任务 doTask阶段进行compressProcessedRes。

这四个动作作用依次如下:

①:removeRedundantResources   删除多余图片,保留最大尺寸

②:compressResources                 对可编译资源目录res下的图片进行压缩

③:compressAssets                       对不可编译资源asserts目录下的图片进行压缩

④:compressProcessedRes          对resource*._ap进行压缩

执行顺序:① > ② > ④,③ > ④  动作②和③无相关,动作④在动作②和③都完成后才执行。

①:removeRedundantResources 

AndroidStudio3.0以后是默认开启aapt2,Booster对没有开启aapt2的情况没有进行冗余图片的操作。在aapt2的模式下,mergeResource Task的产物是一些.flat文件,如下图:

removeRedundantResources 主要的逻辑代码实现如下:

override fun run() {
//搜索满足AAPT2 container format :
//magic = 0x54504141 && version > 0 && count > 0 && type = RES_FILE
val resources = sources().parallelStream().map {
it to it.metadata
}.collect(Collectors.toSet()) resources.filter {
//过滤情况 ResourcesInternal.CompileFile = null的情况
it.second != null
}.groupBy({
//根据资源名分组
it.second!!.resourceName.substringBeforeLast('/')
}, {
it.first to it.second
}).forEach { entry ->
//根据资源名分组
entry.value.groupBy({
it.second!!.resourceName.substringAfterLast('/')
}, {
it.first to it.second!!
}).map { group ->
//组内根据density降序排序,再排除最高density的资源
group.value.sortedByDescending {
it.second.config.density
}.takeLast(group.value.size - )
}.flatten().parallelStream().forEach {
try {
            //对除最高density的其他density资源文件进行删除
if (it.first.delete()) {
val original = File(it.second.sourcePath)
results.add(CompressionResult(it.first, original.length(), , original))
} else {
logger.error("Cannot delete file `${it.first}`")
}
} catch (e: IOException) {
logger.error("Cannot delete file `${it.first}`", e)
}
}
}
} }

该task的逻辑为搜索满足条件magic = 0x54504141 && version > 0 && count > 0 && type = RES_FILE的资源文件,根据资源名分组,组内根据config.density排序,排除掉最高的density的资源,其他density的资源文件都删除。

也许有些同学对AAPT2 Container format不熟悉,见 APPT2源码文档 便知悉该Task的搜索过滤条件了,从文档介绍,RES_FILE类型可以是PNG file, binary XML, PNG file, binary XML, or aapt.pb.XmlNode,那是不是搜索过滤条件最好加上后缀为png.flat,以免造成误删。

集成该task,进行打包,console打印文件删除失败log,如下图:

猜测跟文件流操作有关系,前面读取.flat文件元数据信息(magic、count、type等),已在github提了issue39

②:compressResources

有损压缩图片资源,内置两种压缩方案: 

1.pngquant 有损压缩(需要自行安装 pngquant 命令行工具)

2.cwebp 有损压缩(已内置)

在构建过程中,Booster会根据配置智能选择合适的压缩器Compressor

 /**
* Select the best compressor
*/
fun get(project: Project): CompressionTool? {
val pngquant = project.findProperty(PROPERTY_PNGQUANT)?.toString()
val compressor = project.findProperty(PROPERTY_COMPRESSOR)?.toString()
val binDir = project.buildDir.file(SdkConstants.FD_OUTPUT).absolutePath
val minSdkVersion = project.getAndroid<BaseExtension>().defaultConfig.minSdkVersion.apiLevel project.logger.info("minSdkVersion: $minSdkVersion$")
project.logger.info("$PROPERTY_COMPRESSOR: $compressor")
project.logger.info("$PROPERTY_PNGQUANT: $pngquant") return when (compressor) {
Cwebp.PROGRAM -> Cwebp(binDir)
Pngquant.PROGRAM -> Pngquant(pngquant)
else -> when {
minSdkVersion >= -> Cwebp(binDir)
minSdkVersion in .. -> Cwebp(binDir, true)
else -> Pngquant(pngquant).let {
if (it.isInstalled) it else null
}
}
}
}

配置了可识别的Compressor,就使用该Compressor,否则根据minSdkVersion选择合适的Compressor,Compressor再根据是否开启aapt2创建合适的CompressImage任务

//Cweb
override fun newCompressionTaskCreator() = SimpleCompressionTaskCreator(this) { aapt2 ->
when (aapt2) {
true -> when (opaque) {
true -> CwebpCompressOpaqueFlatImages::class
else -> CwebpCompressFlatImages::class
}
else -> when (opaque) {
true -> CwebpCompressOpaqueImages::class
else -> CwebpCompressImages::class
}
}
} //pngquant
override fun newCompressionTaskCreator() = SimpleCompressionTaskCreator(this) { aapt2 ->
if (aapt2) PngquantCompressFlatImages::class else PngquantCompressImages::class
}

当开启aapt2时,aapt2的compile阶段的产物为.flat文件,使用Cweb / pngquant 压缩原图片后,还需要通过aapt2编译压缩生成后的图片,生成对应的.flat文件

//PngquantCompressFlatImages
override fun run() {
... sources().parallelStream().map {
it to it.metadata
}.filter {
it.second != null
}.map {
val output = compressedRes.file("${it.second!!.resourcePath.substringBeforeLast('.')}$DOT_PNG")
Aapt2ActionData(it.first, it.second!!, output,
listOf(pngquant, "--strip", "--skip-if-larger", "-f", "-o", output.absolutePath, "-s", "", it.second!!.sourcePath),
listOf(aapt2, "compile", "-o"
, it.first.parent, output.absolutePath))
}.forEach {
it.output.parentFile.mkdirs()
val s0 = File(it.metadata.sourcePath).length()
val rc = project.exec { spec ->
spec.isIgnoreExitValue = true
spec.commandLine = it.cmdline
} ...
}
} //CwebpCompressFlatImages
override fun compress(filter: (File) -> Boolean) {
  ... sources().parallelStream().map {
it to it.metadata
}.filter {
      //在android15 - 17 还不支持透明的webp,过滤掉应用图标
it.second != null && isNotLauncherIcon(it.first, it.second!!) && filter(File(it.second!!.sourcePath))
}.map {
val output = compressedRes.file("${it.second!!.resourcePath.substringBeforeLast('.')}.webp")
Aapt2ActionData(it.first, it.second!!, output,
listOf(cwebp, "-mt", "-quiet", "-q", "80", it.second!!.sourcePath, "-o", output.absolutePath),
listOf(aapt2, "compile", "-o", it.first.parent, output.absolutePath))
}.forEach {
it.output.parentFile.mkdirs()
val s0 = File(it.metadata.sourcePath).length()
     //cwep压缩
val rc = project.exec { spec ->
spec.isIgnoreExitValue = true
spec.commandLine = it.cmdline
}
when (rc.exitValue) {
0 -> {
val s1 = it.output.length()
if (s1 > s0) {
            //cwebp压缩后的产物文件大小比原图片还大,则使用原图

results.add(CompressionResult(it.input, s0, s0, File(it.metadata.sourcePath),Compression))
it.output.delete()
} else {
...
}
}
...
}
}
}

③:compressAssets  

对不可编译资源资源的压缩,跟动作compressResources共用相同的compressor,选择没开启的aapt2的CompressImage任务处理。

④:compressProcessedRes

processResources任务完成生成的的产物:resources*.ap_,路径如下图:

resources*.ap_是个压缩文件,resources*.ap_中zip条目归档到apk中,通过 aapt l -v xxx.apk > log.txt  查看APK文件归档类信息 ,可以看到APK中很多资源是以Stored来存储的(),,根据Zip的文件格式中对压缩方式的描述Compression_methods可以看出这些文件是没有压缩的,那为什么它们没有被压缩呢?从AAPT的源码分别从aapt和aapt2中找到以下描述:

/********************aapt Package.cpp****************************/
/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
/********************aapt2 Link.cpp****************************/

// Populate some default no-compress extensions that are already compressed.
options.extensions_to_not_compress.insert(
{".jpg", ".jpeg", ".png", ".gif", ".wav", ".mp2", ".mp3", ".ogg",
".aac", ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl",
".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2",
".3gpp2", ".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"});

     


从描述中都没有找到webp格式,是不是webp图片可以以Deflate来存储在apk?那么看Booster做了什么?

compressProcessedRes逻辑关键代码如下:

//同aapt源码的no-compress格式同出一辙
private val NO_COMPRESS = setOf(
"jpg", "jpeg", "png", "gif",
"wav", "mp2", "mp3", "ogg", "aac",
"mpg", "mpeg", "mid", "midi", "smf", "jet",
"rtttl", "imy", "xmf", "mp4", "m4a",
"m4v", "3gp", "3gpp", "3g2", "3gpp2",
"amr", "awb", "wma", "wmv", "webm", "mkv"
)
private fun BaseVariant.compressProcessedRes(results: CompressionResults) {
//搜索resources-*.ap_文件
val files = scope.processedRes.search {
it.name.startsWith(SdkConstants.FN_RES_BASE) && it.extension == SdkConstants.EXT_RES
}
files.parallelStream().forEach { ap_ ->
val s0 = ap_.length()
ap_.repack {
//过滤掉 不压缩存储的图片格式
!NO_COMPRESS.contains(it.name.substringAfterLast('.'))
}
val s1 = ap_.length()
results.add(CompressionResult(ap_, s0, s1, ap_))
}
} private fun File.repack(shouldCompress: (ZipEntry) -> Boolean) {
val dest = File.createTempFile(SdkConstants.FN_RES_BASE + SdkConstants.RES_QUALIFIER_SEP, SdkConstants.DOT_RES) ZipOutputStream(dest.outputStream()).use { output ->
ZipFile(this).use { zip ->
zip.entries().asSequence().forEach { origin ->
val target = ZipEntry(origin.name).apply {
size = origin.size
crc = origin.crc
comment = origin.comment
extra = origin.extra
//不压缩的格式,保持原有的ZipEntry.method,显然webp格式的图片的method
method = if (shouldCompress(origin)) ZipEntry.DEFLATED else origin.method
} output.putNextEntry(target)
zip.getInputStream(origin).use {
it.copyTo(output)
}
output.closeEntry()
}
}
} if (this.delete()) {
if (!dest.renameTo(this)) {
dest.copyTo(this, true)
}
}
}

其逻辑为搜索resources*.ap_,保存NO_COMPRESS格式的条目method,其他格式的ZipEntry method修改为deflated,显然resources*.ap_文件中webp图片和resources.arsc资源索引文件的method改为了deflated,做了压缩存储,可以aapt命令查看经Booster compression处理的apk zip条目信息进行验证。

对于resources.arsc的压缩是否对apk运行时性能有影响,大佬们也有这样的讨论,见 resources.arsc压缩会影响性能吗? 、Google I/O 2016 笔记:APK 瘦身的正确姿势,尚未定论。

针对resources.arsc的优化,美团还提出如下手段:

1.开启资源混淆

2.对重复的资源优化

3.对被shrinkResources优化掉的资源进行处理

资源混淆见微信开源的资源混淆库AndResGuard

对重复的资源优化和对被shrinkResources优化掉的资源进行处理的原理见:美团博客 Android App包瘦身优化实践

这里根据美团讲述的原理在Booster定制task实现对重复的资源优化和对无用资源优化讨论,详见工程module TaskCompression

一、对重复的资源优化

重复资源的筛选条件为 资源的zipEntry.crc相等,最先出现的资源压缩包产物ap_文件是在processResTask中,尽可能早的删除重复资源, 可以减少后续task的执行时间,hook在processResTask之后,如下:

variant.processResTask?.doLast{
variant.removeRepeatResources(it.logger,results)
}

这里我按照同zipEntry.crc和同资源目录(不同资源目录可能有相同的crc资源,造成误删,不过可能性较小)去分类收集重复资源:

private fun File.findDuplicatedResources():Map<Key,ArrayList<DuplicatedOrUnusedEntry>>{
var duplicatedResources = HashMap<Key,ArrayList<DuplicatedOrUnusedEntry>>()
ZipFile(this).use { zip ->
zip.entries().asSequence().forEach { entry ->
val lastIndex : Int = entry.name.lastIndexOf('/')
val key = Key(entry.crc.toString(),if(lastIndex == -) "/" else entry.name.substring(,lastIndex))
if(!duplicatedResources.containsKey(key)){
val list : ArrayList<DuplicatedOrUnusedEntry> = ArrayList()
duplicatedResources[key] = list
} val list = duplicatedResources[key]
list?.add(DuplicatedOrUnusedEntry(entry.name,entry.size,entry.compressedSize,DuplicatedOrUnusedEntryType.duplicated)) }
} duplicatedResources.filter {
it.value.size >=
}.apply{
duplicatedResources = this as HashMap<Key, ArrayList<DuplicatedOrUnusedEntry>>
} return duplicatedResources
}

重复的资源优化的实现整体思路:

1.从ap_文件中解压出resources.arsc条目,并收集该条目的ZipEntry.method,为后续按照同ZipEntry.method 把改动后的resources.arsc添加到ap_文件中

2.收集重复资源

3.根据收集的重复资源,保留重复资源的第一个,从删除ap_文件中删除其他重复资源的zipEntry

4.使用通过[android-chunk-utils]修改resources.arsc,把把这些重复的资源都重定向到没有被删除的第一个资源

5.按照同ZipEntry.method把改动后的resources.arsc添加到ap_文件中

源码见:doRemoveRepeatResources方法

验证: 分别在App/lib module显示三张图片,重复资源如下:

查看没集成重复的资源优化的apk,如图:

使用工具查看集成重复的资源优化的apk,如图:

集成重复的资源优化打包,控制和输出报告都可以看到如下输出:

可以知道删除哪些重复资源,压缩包减少了多少kb。

二、无用资源优化

通过shrinkResources true来开启资源压缩,资源压缩工具会把无用的资源替换成预定义的版本而不是移除,那么google出于什么原因这样做了? ResourceUsageAnalyzer注释是这样说的的:

/**
* Whether we should create small/empty dummy files instead of actually
* removing file resources. This is to work around crashes on some devices
* where the device is traversing resources. See http://b.android.com/79325 for more.
*/

注释上说了适配解决某些设备crash问题,查看issue,发现发生crash的设备基本上都是三星手机,如果删除无用资源,需要考虑该issue问题。

如果采用人工移除的方式会带来后期的维护成本,在Android构建工具执行package${flavorName}Task之前通过修改Compiled Resources来实现自动去除无用资源。

具体流程如下: * 收集资源包(Compiled Resources的简称)中被替换的预定义版本的资源名称,通过查看资源包 (Zip格式)中每个ZipEntry的CRC-32 checksum来寻找被替换的预定义资源,预定义资源的CRC-32定义在ResourceUsageAnalyzer, 下面是它们的定义:

    // A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
public static final long TINY_PNG_CRC = 0x88b2a3b0L; // A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
public static final long TINY_9PNG_CRC = 0x1148f987L; // The XML document <x/> as binary-packed with AAPT
public static final long TINY_XML_CRC = 0xd7e65643L; // The XML document <x/> as a proto packed with AAPT2
public static final long TINY_PROTO_XML_CRC = 3204905971L;

从定义中没有看到webp、jpg、jpeg相关的crc,那么这些没有定义crc-32的资源在ZipEntry中crc为多少了,用预定义资源替换未使用的地方的实现如下:

private void replaceWithDummyEntry(JarOutputStream zos, ZipEntry entry, String name)throws IOException {
// Create a new entry so that the compressed len is recomputed.
byte[] bytes;
long crc;
if (name.endsWith(DOT_9PNG)) {
bytes = TINY_9PNG;
crc = TINY_9PNG_CRC;
} else if (name.endsWith(DOT_PNG)) {
bytes = TINY_PNG;
crc = TINY_PNG_CRC;
} else if (name.endsWith(DOT_XML)) {
switch (format) {
case BINARY:
bytes = TINY_BINARY_XML;
crc = TINY_BINARY_XML_CRC;
break;
case PROTO:
bytes = TINY_PROTO_XML;
crc = TINY_PROTO_XML_CRC;
break;
default:
throw new IllegalStateException("");
}
} else {
//没有预定资源格式,crc =0,数据为空
bytes = new byte[];
crc = 0L;
}
JarEntry outEntry = new JarEntry(name);
if (entry.getTime() != -1L) {
outEntry.setTime(entry.getTime());
}
if (entry.getMethod() == JarEntry.STORED) {
outEntry.setMethod(JarEntry.STORED);
outEntry.setSize(bytes.length);
outEntry.setCrc(crc);
}
zos.putNextEntry(outEntry);
zos.write(bytes);
zos.closeEntry(); ...
}

可以得出筛选无使用资源的条件为crc in如下集合中:

val unusedResourceCrcs  = longArrayOf(
ResourceUsageAnalyzer.TINY_PNG_CRC,
ResourceUsageAnalyzer.TINY_9PNG_CRC,
ResourceUsageAnalyzer.TINY_BINARY_XML_CRC,
ResourceUsageAnalyzer.TINY_PROTO_XML_CRC,
//jpg、jpeg、webp等
)

打印packageAndroidTask的inputFiles,如下:

分别查看箭头目录下的文件,有*.ap_文件

而从上面两图中可以了解到shrinkResources 影响到packageAndroidTask的inputFiles,没有开启shrinkResources, packageAndroidTask从processedResTask
产物中读取ap_文件,开启shrinkResources,从res_stripped目录下读取ap_文件, 根据其stripped名,也猜测出ap_文件中已经进行了预定资源替换未使用资源了,
可以压缩软件查看未使用资源的zipEntry.crc 进行验证,如下图:

可以看到没有使用的webp、jpg资源的ZipEntry.crc为0;如果集成了Booster内置的booster-task-compression, 会把png格式转换成webp格式,没使用的png最后的crc会变为0。

删除无用资源方案想到两种:

方案一:删除所有无用资源文件,以及删除资源索引文件resources.arsc中global StringChunk有关无用资源的数据项。

缺点:删除了global StringChunk中的数据项,改变了后续数据项的索引值,好比删除List中的元素,后续的元素索引值减一一样,牵一发动全身,需要同步其他chunk索引到global StringChunk数据项的索引值。否则会出现资源显示混乱,甚至crash;同时需要考虑上述issue问题。对resources.arsc越大出现问题的概率越大

方案二:无用资源根据crc分类,再按照重复资源优化,没有删除global StringChunk数据项,没有改变数据项的索引值,不需要改动其他chunk,同时不会出现上述issue问题。

下面对方案二具体实现,方案一就不做讨论了。

无用资源优化的代码实现整体思路:

1.从ap_文件中解压出resources.arsc条目,并收集该条目的ZipEntry.method,为后续按照同ZipEntry.method 把改动后的resources.arsc添加到ap_文件中

2.收集无用资源

3.把收集的无用资源根据crc进行分类,在按照重复资源优化处理

源码见:doRemoveUnusedResources1方法

集成无用资源优化打包,控制和输出报告都可以看到如下输出:

可以知道删除哪些无用资源,压缩包减少了多少kb,无用资源优化减少的size并没有多少。

以上重复资源优化和无用资源优化,没有经过大量设备测试,仅供参考,源码传送门:Boosterstudy

参考阅读:

滴滴Booster学习之旅

Booster 官方文档

美团-Android App包瘦身优化实践

如果您对博主的更新内容持续感兴趣,请关注公众号!

滴滴Booster移动APP质量优化框架 学习之旅 二的更多相关文章

  1. 滴滴Booster移动APP质量优化框架 学习之旅 三

    推荐阅读: 滴滴Booster移动App质量优化框架-学习之旅 一 Android 模块Api化演练 不一样视角的Glide剖析(一) 滴滴Booster移动App质量优化框架-学习之旅 二对重复资源 ...

  2. 滴滴Booster移动APP质量优化框架 学习之旅

    推荐阅读: 滴滴Booster移动App质量优化框架-学习之旅 一 Android 模块Api化演练 不一样视角的Glide剖析(一) 一.Booster简介 Booster是滴滴最近开源一个的移动应 ...

  3. 滴滴 App 的质量优化框架 Booster,开源了!

    一. 序 当 App 达到一定体量的时候,肯定是要考虑质量优化.有些小问题,看似只有 0.01% 触发率,但是如果发生在 DAU 过千万的产品中,就很严重了. 滴滴这个独角兽的 DAU 早已过千万,自 ...

  4. Yii框架学习笔记(二)将html前端模板整合到框架中

    选择Yii 2.0版本框架的7个理由 http://blog.chedushi.com/archives/8988 刚接触Yii谈一下对Yii框架的看法和感受 http://bbs.csdn.net/ ...

  5. Spring框架学习之IOC(二)

    Spring框架学习之IOC(二) 接着上一篇的内容,下面开始IOC基于注解装配相关的内容 在 classpath 中扫描组件 <context:component-scan> 特定组件包 ...

  6. CLion之C++框架篇-优化框架,单元测试(二)

    背景   结合上一篇CLion之C++框架篇-安装工具,基础框架的搭建(一),继续进行框架优化!   googletest(GTest)是Google开源的C++测试框架,与CLion组合,对C++环 ...

  7. Omi框架学习之旅 - 之开篇扯蛋

    说实话, 我也不知道Omi是干啥的, 只因此框架是alloyTeam出的, dntzhang写的, 也有其他腾讯大神参与了, 还有一些其他贡献者, 以上我也不太清楚, 当我胡说八嘎. 因其写法有人说好 ...

  8. 01-Spring Security框架学习--入门(二)

    一.入门案例 Spring Security 自定义登录界面 通过之前的一节 01-Spring Security框架学习--入门(一)的简单演示,Spring security 使用框架自带的登录界 ...

  9. Hadoop学习之旅二:HDFS

    本文基于Hadoop1.X 概述 分布式文件系统主要用来解决如下几个问题: 读写大文件 加速运算 对于某些体积巨大的文件,比如其大小超过了计算机文件系统所能存放的最大限制或者是其大小甚至超过了计算机整 ...

随机推荐

  1. 怎么用cookie解决选项卡问题刷新后怎么保持原来的选项?

    什么是cookie? Cookies虽然一般都以英文名呈现,但是它还是有一个可爱的中文名“小甜饼”.Cookies是指服务器暂存放在你的电脑里的txt格式的文本文件资料,主要用于网络服务器辨别电脑使用 ...

  2. async & await (转载)

    async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们 编程埋下了一些 ...

  3. 九度OJ 1158:买房子 (基础题)

    时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:1801 解决:1096 题目描述: 某程序员开始工作,年薪N万,他希望在中关村公馆买一套60平米的房子,现在价格是200万,假设房子价格以每 ...

  4. 九度OJ 1154:Jungle Roads(丛林路径) (最小生成树)

    时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:832 解决:555 题目描述: The Head Elder of the tropical island of Lagrishan has ...

  5. Open-sourcing LogDevice, a distributed data store for sequential data

    https://logdevice.io/blog/2018/09/12/open-sourcing-announcement.html September 12, 2018   We are exc ...

  6. BCH硬分叉在即,Bitcoin ABC和NChain两大阵营PK

    混迹币圈,我们都知道,BTC分叉有了BCH,而近期BCH也将面临分叉,这次分叉将是Bitcoin ABC和NChain两大阵营的较量,最后谁能成为主导,我们拭目以待. 比特币现金(BCH)的价格自上周 ...

  7. log4net 初步使用

    自从知道了log4net之后,就一直使用的它,一直没有问题,最近由于项目变动,便将一部分的代码分离出来,然后咋UI项目中调用loghelper,便发现在本地测试一切正常,可是发布到服务器之后便不正常了 ...

  8. JS高级调试技巧:捕获和分析 JavaScript Error详解

    前端工程师都知道 JavaScript 有基本的异常处理能力.我们可以 throw new Error(),浏览器也会在我们调用 API 出错时抛出异常.但估计绝大多数前端工程师都没考虑过收集这些异常 ...

  9. Matlab之rand(), randn(), randi()函数的使用方法

    1.  rand()函数用于生成取值在(0~1)之间均匀分布的伪随机数.rand(n):生成n*n的0~1之间的满足均匀分布的伪随机矩阵:rand(m,n):生成m*n的伪随机数:rand(m,n,' ...

  10. 关于MVC模板渲染的一点小事type="text/template"

    先上一个demo,简单粗暴,请自便 <!DOCTYPE html> <html> <head lang="en"> <meta chars ...