edmond

edmond 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 looseyi.github.io/ 编辑
编辑

一分耕耘,一分收获

个人动态

edmond 发布了文章 · 2020-10-08

2. 整体把握 CocoaPods 核心组件

image

CocoaPods历险记这个专题是Edmond冬瓜共同撰写,对于 iOS / macOS 工程中版本管理工具 CocoaPods 的实现细节、原理、源码、实践与经验的分享记录,旨在帮助大家能够更加了解这个依赖管理工具,而不仅局限于pod installpod update

本文知识目录

CocoaPods-Perview

引子

在上文 版本管理工具及 Ruby 工具链环境 中,我们聊到如何统一管理团队小伙伴的 CocoaPods 生产环境及使用到的 Ruby 工具链。今天让我们将目光转到 CocoaPods 身上,一起来聊聊它的主要构成,以及各个组件在整个 Pods 工作流的关系。

为了整体把握 CocoaPods 这个项目,建议大家去入门一下 Ruby 这门脚本语言。另外本文基于 CocoaPods 1.9.2 版本。

CocoaPods 的核心组件

作为包管理工具,CocoaPods 随着 Apple 生态的蓬勃发展也在不断迭代和进化,并且各部分核心功能也都演化出相对独立的组件。这些功能独立的组件,均拆分出一个个独立的 Gem 包,而 CocoaPods 则是这些组件的“集大成者”。

CocoaPods 依赖总览

我们知道在 Pod 管理的项目中,Podfile 文件里描述了它所依赖的 dependencies,类似的 Gem 的依赖可以在 Gemfile 中查看。那 CocoaPods 的 Gemfile 有哪些依赖呢?

SKIP_UNRELEASED_VERSIONS = false
# ...

source 'https://rubygems.org'
gemspec
gem 'json', :git => 'https://github.com/segiddins/json.git', :branch => 'seg-1.7.7-ruby-2.2'

group :development do
  cp_gem 'claide',                'CLAide'
  cp_gem 'cocoapods-core',        'Core', '1-9-stable'
  cp_gem 'cocoapods-deintegrate', 'cocoapods-deintegrate'
  cp_gem 'cocoapods-downloader',  'cocoapods-downloader'
  cp_gem 'cocoapods-plugins',     'cocoapods-plugins'
  cp_gem 'cocoapods-search',      'cocoapods-search'
  cp_gem 'cocoapods-stats',       'cocoapods-stats'
  cp_gem 'cocoapods-trunk',       'cocoapods-trunk'
  cp_gem 'cocoapods-try',         'cocoapods-try'
  cp_gem 'molinillo',             'Molinillo'
  cp_gem 'nanaimo',               'Nanaimo'
  cp_gem 'xcodeproj',             'Xcodeproj'
   
  gem 'cocoapods-dependencies', '~> 1.0.beta.1'
  # ...
  # Integration tests
  gem 'diffy'
  gem 'clintegracon'
  # Code Quality
  gem 'inch_by_inch'
  gem 'rubocop'
  gem 'danger'
end

group :debugging do
  gem 'cocoapods_debug'

  gem 'rb-fsevent'
  gem 'kicker'
  gem 'awesome_print'
  gem 'ruby-prof', :platforms => [:ruby]
end

上面的 Gemfile 中我们看到很多通过 cp_gem 装载的 Gem 库,其方法如下:

def cp_gem(name, repo_name, branch = 'master', path: false)
  return gem name if SKIP_UNRELEASED_VERSIONS
  opts = if path
           { :path => "../#{repo_name}" }
         else
           url = "https://github.com/CocoaPods/#{repo_name}.git"
           { :git => url, :branch => branch }
         end
  gem name, opts
end

它是用于方便开发和调试,当 **SKIP_UNRELEASED_VERSIONS**false && pathtrue 时会使用与本地的 CocoaPods 项目同级目录下的 git 仓库,否则会使用对应的项目直接通过 Gem 加载。

通过简单的目录分割和 Gemfile 管理,就实现了最基本又最直观的热插拔,对组件开发十分友好。所以你只要将多个仓库如下图方式排列,即可实现跨仓库组件开发:

$ ls -l
lrwxr-xr-x  1 gua  staff    31 Jul 30 21:34 CocoaPods
lrwxr-xr-x  1 gua  staff    26 Jul 31 13:27 Core 
lrwxr-xr-x  1 gua  staff    31 Jul 31 10:14 Molinillo 
lrwxr-xr-x  1 gua  staff    31 Aug 15 11:32 Xcodeproj 
lrwxr-xr-x  1 gua  staff    42 Jul 31 10:14 cocoapods-downloader

组件构成和对应职责

通过上面对于 Gemfile 的简单分析,可以看出 CocoaPods 不仅仅是一个仓库那么简单,它作为一个三方库版本管理工具,对自身组件的管理和组件化也是十分讲究的。我们继续来看这份 Gemfile 中的核心开发组件:

00-Core Components

CLAide

The CLAide gem is a simple command line parser, which provides an API that allows you to quickly create a full featured command-line interface.

CLAide 虽然是一个简单的命令行解释器,但它提供了功能齐全的命令行界面和 API。它不仅负责解析我们使用到的 Pods 命令,如:pod install, pod update 等,还可用于封装常用的一些脚本,将其打包成简单的命令行小工具。

PS: 所谓命令行解释器就是从标准输入或者文件中读取命令并执行的程序。详见 Wiki

cocoapods-core

The CocoaPods-Core gem provides support to work with the models of CocoaPods, for example the Podspecs or the Podfile.

CocoaPods-Core 用于 CocoaPods 中模板文件的解析,包括 Podfile.podspec,以及所有的 .lock 文件中特殊的 YAML 文件。

cocoapods-downloader

The Cocoapods-downloader gem is a small library that provides downloaders for various source control types (HTTP/SVN/Git/Mercurial). It can deal with tags, commits, revisions, branches, extracting files from zips and almost anything these source control system would use.

Cocoapods-Downloader 是用于下载源码的小工具,它支持各种类型的版本管理工具,包括 HTTP / SVN / Git / Mercurial。它可以提供 tagscommitesrevisionsbranches 以及 zips 文件的下载和解压缩操作。

Molinillo

The Molinillo gem is a generic dependency resolution algorithm, used in CocoaPods, Bundler and RubyGems.

Molinillo 是 CocoaPods 对于依赖仲裁算法的封装,它是一个具有前向检察的回溯算法。不仅在 Pods 中,BundlerRubyGems 也是使用的这一套仲裁算法。

Xcodeproj

The Xcodeproj gem lets you create and modify Xcode projects from Ruby. Script boring management tasks or build Xcode-friendly libraries. Also includes support for Xcode workspaces (.xcworkspace) and configuration files (.xcconfig).

Xcodeproj 可通过 Ruby 来操作 Xcode 项目的创建和编辑等。可友好的支持 Xcode 项目的脚本管理和 libraries 构建,以及 Xcode 工作空间 (.xcworkspace)  和配置文件 .xcconfig 的管理。

cocoapods-plugins

CocoaPods plugin which shows info about available CocoaPods plugins or helps you get started developing a new plugin. Yeah, it's very meta.

cocoapods-plugins 插件管理功能,其中有 pod plugin 全套命令,支持对于 CocoaPods 插件的列表一览(list)、搜索(search)、创建(create)功能。

当然,上面还有很多组件这里就不一一介绍了。通过查看 Gemfile 可以看出 Pod 对于组件的拆分粒度是比较细微的,通过对各种组件的组合达到现在的完整版本。这些组件中,笔者的了解也十分有限,不过我们会在之后的一系列文章来逐一介绍学习。

CocoaPods 初探

接下来,结合 pod install 安装流程来展示各个组件在 Pods 工作流中的上下游关系。

命令入口

每当我们输入 pod xxx 命令时,系统会首先调用 pod 命令。所有的命令都是在 /bin 目录下存放的脚本,当然 Ruby 环境的也不例外。我们可以通过 which pod 来查看命令所在位置:

$ which pod
/Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod
这里的显示路径不是 /usr/local/bin/pod 的原因是因为使用 RVM 进行版本控制的。

我们通过 cat 命令来查看一下这个入口脚本执行了什么

$ cat /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod

输出如下:

#!/usr/bin/env ruby_executable_hooks

require 'rubygems'
version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
    load Gem.activate_bin_path('cocoapods', 'pod', version)
else
    gem "cocoapods", version
    load Gem.bin_path("cocoapods", "pod", version)
end

程序 CocoaPods 是作为 Gem 被安装的,此脚本用于唤起 CocoaPods。逻辑比较简单,就是一个单纯的命令转发。Gem.activate_bin_pathGem.bin_path 用于找到 CocoaPods 的安装目录 cocoapods/bin,最终加载该目录下的 /pod 文件:

#!/usr/bin/env ruby
# ... 忽略一些对于编码处理的代码

require 'cocoapods'

# 这里手动输出一下调用栈,来关注一下
puts caller

# 如果环境配置中指定了 ruby-prof 配置文件,会对执行命令过程进行性能监控
if profile_filename = ENV['COCOAPODS_PROFILE']
  require 'ruby-prof'
  # 依据配置文件类型加载不同的 reporter 解析器
  # ...
  File.open(profile_filename, 'w') do |io|
    reporter.new(RubyProf.profile { Pod::Command.run(ARGV) }).print(io)
  end
else
  Pod::Command.run(ARGV)
end

一起来查看一下 pod 命令的输出结果:

$ pod
/Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `load'
/Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `<main>'
/Users/edmond/.rvm/gems/ruby-2.6.1/bin/ruby_executable_hooks:24:in `eval'
/Users/edmond/.rvm/gems/ruby-2.6.1/bin/ruby_executable_hooks:24:in `<main>'

ruby_executable_hooks 通过 bin 目录下的 pod 入口唤醒,再通过 eval 的手段调起我们需要的 CocoaPods 工程。这是 RVM 的自身行为,它利用了 executable-hook 来注入 Gems 插件来定制扩展。

PS:大多数动态语言都支持 eval 这一神奇的函数。打  Lisp 开始就支持了,它通过接受一个字符串类型作为参数,将其解析成语句并混合在当前作用域内运行。详细可以参考这篇 文章

在入口的最后部分,通过调用 Pod::Command.run(ARGV),实例化了一个 CLAide::Command 对象,开始我们的 CLAide 命令解析阶段。这里不对 CLAide 这个命令解析工具做过多的分析,这个是后面系列文章的内容。这里我们仅仅需要知道:

每个 CLAide  命令的执行,最终都会对应到具体 Command Class 的 run 方法。

Pod 命令对应的 run 方法实现如下:

module Pod
  class Command
    class Install < Command
      # ... 
      def run
        # 判断是否存在 Podfile 文件,如果存在则进行 Podfile 的初始化
        verify_podfile_exists!
        # 从 Config 中获取一个 Instraller 实例
        installer = installer_for_config
        # 默认是不执行 update
        installer.repo_update = repo_update?(:default => false)
        installer.update = false
        installer.deployment = @deployment
        # install 的真正过程
        installer.install!
      end
    end
  end
end

上述所见的 Command::Install 类对应的命令为 pod installpod install 过程是依赖于 Podfile 文件的,所以在入口处会做检测,如果不存在 Podfile 则直接抛出 No 'Podfile' found in the project directory 的异常 警告并结束命令。

执行功能主体

installer 实例组装完成之后,调用其 install! 方法,这时候才进入了我们 pod install 命令的主体部分,流程如下图:

01-pod install.png

对应的实现如下:

def install!
    prepare
    resolve_dependencies
    download_dependencies
    validate_targets
    if installation_options.skip_pods_project_generation?
        show_skip_pods_project_generation_message
    else
        integrate
    end
    write_lockfiles
    perform_post_install_actions
end

def integrate
    generate_pods_project
    if installation_options.integrate_targets?
        integrate_user_project
    else
        UI.section 'Skipping User Project Integration'
    end
end

0x1 Install 环境准备(prepare)

def prepare
  # 如果检测出当前目录是 Pods,直接 raise 终止
  if Dir.pwd.start_with?(sandbox.root.to_path)
    message = 'Command should be run from a directory outside Pods directory.'
    message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n"
    raise Informative, message
  end
  UI.message 'Preparing' do
    # 如果 lock 文件的 CocoaPods 主版本和当前版本不同,将以新版本的配置对 xcodeproj 工程文件进行更新
    deintegrate_if_different_major_version
    # 对 sandbox(Pods) 目录建立子目录结构
    sandbox.prepare
    # 检测 PluginManager 是否有 pre-install 的 plugin
    ensure_plugins_are_installed!
    # 执行插件中 pre-install 的所有 hooks 方法
    run_plugins_pre_install_hooks
  end
end

prepare 阶段会将 pod install 的环境准备完成,包括版本一致性目录结构以及将 pre-install 的装载插件脚本全部取出,并执行对应的 pre_install hook。

0x2 解决依赖冲突(resolve_dependencies)

def resolve_dependencies
    # 获取 Sources
    plugin_sources = run_source_provider_hooks
    # 创建一个 Analyzer
    analyzer = create_analyzer(plugin_sources)

    # 如果带有 repo_update **标记**
    UI.section 'Updating local specs repositories' do
        # 执行 Analyzer 的更新 Repo 操作
        analyzer.update_repositories
    end if repo_update?

    UI.section 'Analyzing dependencies' do
        # 从 analyzer 取出最新的分析结果,@analysis_result,@aggregate_targets,@pod_targets
        analyze(analyzer)
        # 拼写错误降级识别,白名单过滤
        validate_build_configurations
    end

    # 如果 deployment? 为 true,会验证 podfile & lockfile 是否需要更新
    UI.section 'Verifying no changes' do
        verify_no_podfile_changes!
        verify_no_lockfile_changes!
    end if deployment?
 
    analyzer
end

依赖解析过程就是通过 PodfilePodfile.lock 以及沙盒中的 manifest 生成 Analyzer 对象。_Analyzer_ 内部会使用 Molinillo (具体的是 Molinillo::DependencyGraph 图算法)解析得到一张依赖关系表。

PS:通过 Analyzer 能获取到很多依赖信息,例如 Podfile 文件的依赖分析结果,也可以从 specs_by_target 来查看各个 target 相关的 specs。

另外,需要注意的是 analyze 的过程中有一个 pre_download 的阶段,即在 --verbose 下看到的 Fetching external sources 过程。这个 pre_download 阶段不属于依赖下载过程,而是在当前的依赖分析阶段。

PS:该过程主要是解决当我们在通过 Git 地址引入的 Pod 仓库的情况下,系统无法从默认的 Source 拿到对应的 Spec,需要直接访问我们的 Git 地址下载仓库的 zip 包,并取出对应的 podspec 文件,从而进行对比分析。

0x3 下载依赖文件(download_dependencies)

def download_dependencies
  UI.section 'Downloading dependencies' do
    # 初始化 sandbox 文件访问器
    create_file_accessors
    # 构造 Pod Source Installer
    install_pod_sources
    # 执行 podfile 定义的 pre install 的 hooks
    run_podfile_pre_install_hooks
    # 根据配置清理 pod sources 信息,主要是清理无用 platform 相关内容
    clean_pod_sources
  end
end

create_file_accessors 中会创建沙盒目录的文件访问器,通过构造 FileAccessor 实例来解析沙盒中的各种文件。接着是最重要的 install_pod_sources 过程,它会调用对应 Pod 的 install! 方法进行资源下载。

先来看看 install_pod_sources 方法的实现:

def install_pod_sources
 @installed_specs = []
 # install 的 Pod 只需要这两种状态,added 和 changed 状态的并集
 pods_to_install = sandbox_state.added | sandbox_state.changed
 title_options = { :verbose_prefix => '-> '.green }
 puts "root_specs"
 root_specs.each do |item|
   puts item
 end
 # 将 Podfile 解析后排序处理
 root_specs.sort_by(&:name).each do |spec|
   # 如果是 added 或 changed 状态的 Pod
   if pods_to_install.include?(spec.name)
     # 如果是 changed 状态并且 manifest 已经有记录
     if sandbox_state.changed.include?(spec.name) && sandbox.manifest
       # 版本更新
       current_version = spec.version
       # 被更新版本记录
       previous_version = sandbox.manifest.version(spec.name)
       # 变动记录
       has_changed_version = current_version != previous_version
       # 找到第一个包含 spec.name 的 Pod,获取对应的 Repo,其实就是 find 方法
       current_repo = analysis_result.specs_by_source.detect { |key, values| break key if values.map(&:name).include?(spec.name) }
       # 获取当前仓库
       current_repo &&= current_repo.url || current_repo.name
       # 获取之前该仓库的信息
       previous_spec_repo = sandbox.manifest.spec_repo(spec.name)
       # 是否仓库有变动
       has_changed_repo = !previous_spec_repo.nil? && current_repo && (current_repo != previous_spec_repo)

       # 通过 title 输出上面的详细变更信息
       title = ...
     else
       # 非 changed 状态,展示 Installing 这个是经常见的 log
       title = "Installing #{spec}"
     end
     UI.titled_section(title.green, title_options) do
       # 通过 name 拿到对应的 installer,记录到 @pod_installers 中
       install_source_of_pod(spec.name)
     end
   else
     # 如果没有 changed 情况,直接展示 Using,也是经常见到的 log
     UI.titled_section("Using #{spec}", title_options) do
       # # 通过 sandbox, specs 的 platform 信息生成 Installer 实例,记录到 @pod_installers 中
       create_pod_installer(spec.name)
     end
   end
 end
end

# 通过缓存返回 PodSourceInstaller 实例
def create_pod_installer(pod_name)
    specs_by_platform = specs_for_pod(pod_name)
 
    # 当通过 pod_name 无法找到对应的 pod_target 或 platform 配置,主动抛出错误信息
    if specs_by_platform.empty?
        requiring_targets = pod_targets.select { |pt| pt.recursive_dependent_targets.any? { |dt| dt.pod_name == pod_name } }
        # message = "..."
        raise StandardError, message
    end
    # 通过 sandbox, specs 的 platform 信息生成 Installer 实例
    pod_installer = PodSourceInstaller.new(sandbox, podfile, specs_by_platform, :can_cache => installation_options.clean?)
    pod_installers << pod_installer
    pod_installer
end

# 如果 resolver 声明一个 Pod 已经安装或者已经存在,将会将其删除并重新安装。如果不存在则直接安装。
def install_source_of_pod(pod_name)
  pod_installer = create_pod_installer(pod_name)
  pod_installer.install!
  @installed_specs.concat(pod_installer.specs_by_platform.values.flatten.uniq)
end

在方法的开始,root_specs 方法是通过 analysis_result 拿出所有根 spec

def root_specs
  analysis_result.specifications.map(&:root).uniq
end

下面再来看看 pod_installer 中的 install! 方法,主要是通过调用 cocoapods-downloader 组件,将 Pod 对应的 Source 下载到本地。实现如下:

def install!
    download_source unless predownloaded? || local?
    PodSourcePreparer.new(root_spec, root).prepare! if local?
    sandbox.remove_local_podspec(name) unless predownloaded? || local? || external?
end

0x4 验证 targets (validate_targets)

用来验证之前流程中的产物 (pod 所生成的 Targets) 的合法性。主要作用就是构造 TargetValidator,并执行 validate! 方法:

def validate_targets
    validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options)
    validator.validate!
end

def validate!
    verify_no_duplicate_framework_and_library_names
    verify_no_static_framework_transitive_dependencies
    verify_swift_pods_swift_version
    verify_swift_pods_have_module_dependencies
    verify_no_multiple_project_names if installation_options.generate_multiple_pod_projects?
end

验证环节在整个 Install 过程中仅占很小的一部分。因为只是验证部分,是完全解耦的。

  1. verify_no_duplicate_framework_and_library_names

验证是否有重名的 framework,如果有冲突会直接抛出 frameworks with conflicting names 异常。

  1. verify_no_static_framework_transitive_dependencies

验证动态库中是否有静态链接库 (.a 或者 .framework) 依赖,如果存在则会触发 transitive dependencies that include static binaries... 错误。假设存在以下场景:

  1. 组件 A 和组件 B 同时依赖了组件 C,C 为静态库,如 Weibo_SDK
  2. 组件 A 依赖组件 B,而组件 B 的 .podspec 文件中存在以下设置时,组件 B 将被判定为存在静态库依赖:

    1. podspec 设置了 s.static_framework = true
    2. podspec 以 s.dependency 'xxx_SDK 依赖了静态库 xxx_SDK
    3. podspec 以 s.vendored_libraries = 'libxxx.a' 方式内嵌了静态库 libxxx

此时如果项目的 Podfile 设置了 use_framework! 以动态链接方式打包的时,则会触发该错误。
问题原因
Podfile 中不使用 use_frameworks! 时,每个 pod 是会生成相应的 .a(静态链接库)文件,然后通过 static libraries 来管理 pod 代码,在 Linked 时会包含该 pod 引用的其他的 pod 的 .a 文件。
Podfile 中使用 use_frameworks! 时是会生成相应的 .framework 文件,然后通过 dynamic frameworks 的方式来管理 pod 代码,在 Linked 时会包含该 pod 引用的其他的 pod 的 .framework 文件。
上述场景中虽然以 framework 的方式引用了 B 组件,然而 B 组件实际上是一个静态库,需要拷贝并链接到该 pod 中,然而 dynamic frameworks 方式并不会这么做,所以就报错了。
解决方案

  1. 修改 pod 库中 podspec,增加 pod_target_xcconfig,定义好 FRAMEWORK_SEARCH_PATHSOTHER_LDFLAGS 两个环境变量;
  2. hook verify_no_static_framework_transitive_dependencies 的方法,将其干掉!对应 issue
  3. 修改 pod 库中 podspec,开启 static_framework 配置 s.static_framework = true
  1. verify_swift_pods_swift_version

确保 Swift Pod 的 Swift 版本正确配置且互相兼容的。

  1. verify_swift_pods_have_module_dependencies

检测 Swift 库的依赖库是否支持了 module,这里的 module 主要是针对 Objective-C 库而言。
首先,Swift 是天然支持 module 系统来管理代码的,Swift Module 是构建在 LLVM Module 之上的模块系统。Swift 库在解析后会生成对应的 modulemapumbrella.h 文件,这是 LLVM Module 的标配,同样 Objective-C 也是支持 LLVM Module。当我们以 Dynamic Framework 的方式引入 Objective-C 库时,Xcode 支持配置并生成 header,而静态库 .a 需要自己编写对应的 umbrella.hmodulemap
其次,如果你的 Swift Pod 依赖了 Objective-C 库,又希望以静态链接的方式来打包 Swift Pod 时,就需要保证 Objective-C 库启用了 modular_headers,这样 CocoaPods 会为我们生成对应 modulemapumbrella.h 来支持 LLVM Module。你可以从这个地址 - http://blog.cocoapods.org/CocoaPods-1.5.0/ 查看到更多细节。

  1. verify_no_pods_used_with_multiple_swift_versions

检测是否所有的 Pod Target 中版本一致性问题。

用一个流程图来概括这一验证环节:
validate targets

0x5 生成工程 (Integrate)

工程文件的生成是 pod install 的最后一步,他会将之前版本仲裁后的所有组件通过 Project 文件的形式组织起来,并且会对 Project 中做一些用户指定的配置。

def integrate
    generate_pods_project
    if installation_options.integrate_targets?
        # 集成用户配置,读取依赖项,使用 xcconfig 来配置
        integrate_user_project
    else
        UI.section 'Skipping User Project Integration'
    end
end

def generate_pods_project
    # 创建 stage sanbox 用于保存安装前的沙盒状态,以支持增量编译的对比
    stage_sandbox(sandbox, pod_targets)
    # 检查是否支持增量编译,如果支持将返回 cache result
    cache_analysis_result = analyze_project_cache
    # 需要重新生成的 target
    pod_targets_to_generate = cache_analysis_result.pod_targets_to_generate
    # 需要重新生成的 aggregate target
    aggregate_targets_to_generate = cache_analysis_result.aggregate_targets_to_generate

    # 清理需要重新生成 target 的 header 和 pod folders
    clean_sandbox(pod_targets_to_generate)
    # 生成 Pod Project,组装 sandbox 中所有 Pod 的 path、build setting、源文件引用、静态库文件、资源文件等
    create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate,
                                cache_analysis_result.build_configurations, cache_analysis_result.project_object_version)

    # SandboxDirCleaner 用于清理增量 pod 安装中的无用 headers、target support files 目录
    SandboxDirCleaner.new(sandbox, pod_targets, aggregate_targets).clean!
    # 更新安装后的 cache 结果到目录 `Pods/.project_cache` 下
    update_project_cache(cache_analysis_result, target_installation_results)
end

install 过程中,除去依赖仲裁部分和下载部分的时间消耗,在工程文件生成也会有相对较大的时间开销。这里往往也是速度优化核心位置。

0x6 写入依赖 (write_lockfiles)

将依赖更新写入 Podfile.lockManifest.lock

0x7 结束回调(perform_post_install_action)

最后一步收尾工作,为所有插件提供 post-installation 操作以及 hook。

def perform_post_install_actions
    # 调用 HooksManager 执行每个插件的 post_install 方法 
    run_plugins_post_install_hooks
    # 打印过期 pod target 警告
    warn_for_deprecations
    # 如果 pod 配置了 script phases 脚本,会主动输出一条提示消息
    warn_for_installed_script_phases
    # 输出结束信息 `Pod installation complete!`
    print_post_install_message
end

核心组件在 pod install 各阶段的作用如下:

02-pod install integrate

总结

当我们知道 CocoaPods 在 install 的大致过程后,我们可以对其做一些修改和控制。例如知道了插件的 pre_installpost_install 的具体时机,我们就可以在 Podfile 中执行对应的 Ruby 脚本,达到我们的预期。同时了解 install 过程也有助于我们进行每个阶段的性能分析,以优化和提高 Install 效率。

后续,将学习 CocoaPods 中每一个组件的实现,将所有的问题在代码中找到答案。

知识点问题梳理

这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:

  1. 简单概述 CocoaPods 的核心模块?
  2. pod 命令是如何找到并启动 CocoaPods 程序的?
  3. 简述 pod install 流程?
  4. resolve_dependencies 阶段中的 pre_download 是为了解决什么问题?
  5. validate_targets 都做了哪些校验工作?
查看原文

赞 0 收藏 0 评论 0

edmond 发布了文章 · 2020-08-21

1. 版本管理工具及 Ruby 工具链环境

ruby tool tour

CocoaPods 历险记这个专题是 Edmond冬瓜 是对于 iOS / macOS 工程中版本管理工具 CocoaPods 的实现细节、原理、源码、实践与经验的分享记录,旨在帮助大家能够更加了解这个依赖管理工具,而不仅局限于 pod installpod update

本文知识目录

版本管理工具及 Ruby 工具链

背景

CocoaPods 作为业界标准,各位 iOS 开发同学应该都不陌生。不过很多同学对 CocoaPods 的使用基本停留在 pod installpod update 上。一旦项目组件化,各业务线逻辑拆分到独立的 Pod 中后,光了解几个简单 Pod 命令是无法满足需求的,同时还面临开发环境的一致性,Pod 命令执行中的各种异常错误,都要求我们对其有更深层的认知和 🤔。

关于 CocoaPods 深入的文章有很多,推荐 ObjC China 的这篇,深入理解 CocoaPods,而本文希望从依赖管理工具的角度来谈谈 CocoaPods 的管理理念。

Version Control System (VCS)

Version control systems are a category of software tools that help a software team manage changes to source code over time. Version control software keeps track of every modification to the code in a special kind of database.

软件工程中,版本控制系统是敏捷开发的重要一环,为后续的持续集成提供了保障。Source Code Manager (SCM) 源码管理就属于 VCS 的范围之中,熟知的工具有如 Git 。而 CocoaPods 这种针对各种语言所提供的 Package Manger (PM)也可以看作是 SCM 的一种。

而像 GitSVN 是针对项目的单个文件的进行版本控制,而 PM 则是以每个独立的 Package 作为最小的管理单元。包管理工具都是结合 SCM 来完成管理工作,对于被 PM 接管的依赖库的文件,通常会在 Git.ignore 文件中选择忽略它们。

例如:在 Node 项目中一般会把 node_modules 目录下的文件 ignore 掉,在 iOS / macOS 项目则是 Pods

Git Submodule

Git Submodule

Git submodules allow you to keep a git repository as a subdirectory of another git repository. Git submodules are simply a reference to another repository at a particular snapshot in time. Git submodules enable a Git repository to incorporate and track version history of external code.

Git Submodules 可以算是 PM 的“青春版”,它将单独的 git 仓库以子目录的形式嵌入在工作目录中。它不具备 PM 工具所特有的语义化版本管理、无法处理依赖共享与冲突等。只能保存每个依赖仓库的文件状态。

Git 在提交更新时,会对所有文件制作一个快照并将其存在数据库中。Git 管理的文件存在 3 种状态:

  • working director: 工作目录,即我们肉眼可见的文件
  • stage area: 暂存区 (或称 index area ),存在 .git/index 目录下,保存的是执行 git add 相关命令后从工作目录添加的文件。
  • commit history: 提交历史,存在 .git/ 目录下,到这个状态的文件改动算是入库成功,基本不会丢失了。

Git submodule 是依赖 .gitmodules 文件来记录子模块的。

[submodule "ReactNative"]
    path = ReactNative
    url = https://github.com/facebook/ReactNative.git

.gitmodules 仅记录了 path 和 url 以及模块名称的基本信息, 但是我们还需要记录每个 Submodule Repo 的 commit 信息,而这 commit 信息是记录在 .git/modules 目录下。同时被添加到 .gitmodules 中的 path 也会被 git 直接 ignore 掉。

Package Manger

作为 Git Submodule 的强化版,PM 基本都具备了语义化的版本检查能力,依赖递归查找,依赖冲突解决,以及针对具体依赖的构建能力和二进制包等。简单对比如下:

Key FileGit submoduleCocoaPodsSPMnpm
描述文件.gitmodulesPodfilePackage.swiftPackage.json
锁存文件.git/modulesPodfile.lockPackage.resolvedpackage-lock.json

从 👆 可见,PM 工具基本围绕这个两个文件来现实包管理:

  • 描述文件:声明了项目中存在哪些依赖,版本限制;
  • 锁存文件(Lock 文件):记录了依赖包最后一次更新时的全版本列表。

除了这两个文件之外,中心化的 PM 一般会提供依赖包的托管服务,比如 npm 提供的 npmjs.com 可以集中查找和下载 npm 包。如果是去中心化的 PM 比如 iOSCarthageSPM 就只能通过 Git 仓库的地址了。

CocoaPods

image.png

CocoaPods  是开发 iOS/macOS 应用程序的一个第三方库的依赖管理工具。 利用 CocoaPods,可以定义自己的依赖关系(简称 Pods),以及在整个开发环境中对第三方库的版本管理非常方便。

下面我们以 CocoaPods 为例。

Podfile

Podfile 是一个文件,以 DSL(其实直接用了 Ruby 的语法)来描述依赖关系,用于定义项目所需要使用的第三方库。该文件支持高度定制,你可以根据个人喜好对其做出定制。更多相关信息,请查阅 Podfile 指南

Podfile.lock

这是 CocoaPods 创建的最重要的文件之一。它记录了需要被安装的 Pod 的每个已安装的版本。如果你想知道已安装的 Pod 是哪个版本,可以查看这个文件。推荐将 Podfile.lock 文件加入到版本控制中,这有助于整个团队的一致性。

Manifest.lock

这是每次运行 pod install 命令时创建的 Podfile.lock 文件的副本。如果你遇见过这样的错误 沙盒文件与 Podfile.lock 文件不同步 (The sandbox is not in sync with the Podfile.lock),这是因为 Manifest.lock 文件和 Podfile.lock 文件不一致所引起。由于 Pods 所在的目录并不总在版本控制之下,这样可以保证开发者运行 App 之前都能更新他们的 Pods,否则 App 可能会 crash,或者在一些不太明显的地方编译失败。

Master Specs Repo

Ultimately, the goal is to improve discoverability of, and engagement in, third party open-source libraries, by creating a more centralized ecosystem.

作为包管理工具,CocoaPods 的目标是为我们提供一个更加集中的生态系统,来提高依赖库的可发现性和参与度。本质上是为了提供更好的检索和查询功能,可惜成为了它的问题之一。因为 CocoaPods 通过官方的 Spec 仓库来管理这些注册的依赖库。随着不断新增的依赖库导致 Spec 的更新和维护成为了使用者的包袱。

好在这个问题在 1.7.2 版本中已经解决了,CocoaPods 提供了 Mater Repo CDN ,可以直接 CDN 到对应的 Pod 地址而无需在通过本地的 Spec 仓库了。同时在 1.8 版本中,官方默认的 Spec 仓库已替换为 CDN,其地址为  https://cdn.cocoapods.org

Spec 仓静态页

Ruby 生态及工具链

对于一部分仅接触过 CocoaPods 的同学,其 PM 可能并不熟悉。其实 CocoaPods 的思想借鉴了其他语言的 PM 工具,例:RubyGems, Bundler, npmGradle

我们知道 CocoaPods 是通过 Ruby 语言实现的。它本身就是一个 Gem 包。理解了 Ruby 的依赖管理有助于我们更好的管理不同版本的 CocoaPods 和其他 Gem。同时能够保证团队中的所有同事的工具是在同一个版本,这也算是敏捷开发的保证吧。

RVM & rbenv

RVM vs rbenv

RVMrbenv 都是管理多个 Ruby 环境的工具,它们都能提供不同版本的 Ruby 环境管理和切换。

具体哪个更好要看个人习惯。 当然 rbenv 官方是这么说的 Why rbenv 。本文后续的实验也都是是使用 rbenv 进行演示。

层级关系

RubyGems

RubyGems

The RubyGems software allows you to easily download, install, and use ruby software packages on your system. The software package is called a “gem” which contains a packaged Ruby application or library.

RubyGems 是 Ruby 的一个包管理工具,这里面管理着用 Ruby 编写的工具或依赖我们称之为 Gem。

并且 RubyGems 还提供了 Ruby 组件的托管服务,可以集中式的查找和安装 library 和 apps。当我们使用 gem install xxx 时,会通过 rubygems.org 来查询对应的 Gem Package。而 iOS 日常中的很多工具都是 Gem 提供的,例:BundlerfastlanejazzyCocoaPods 等。

在默认情况下 Gems 总是下载 library 的最新版本,这无法确保所安装的 library 版本符合我们预期。因此我们还缺一个工具。

Bundler

Bundler

Bundler 是管理 Gem 依赖的工具,可以隔离不同项目中 Gem 的版本和依赖环境的差异,也是一个 Gem。

Bundler 通过读取项目中的依赖描述文件 Gemfile ,来确定各个 Gems 的版本号或者范围,来提供了稳定的应用环境。当我们使用 bundle install 它会生成 Gemfile.lock 将当前 librarys 使用的具体版本号写入其中。之后,他人再通过 bundle install 来安装 libaray 时则会读取 Gemfile.lock 中的 librarys、版本信息等。

Gemfile

可以说 CocoaPods 其实是 iOS 版的 RubyGems + Bundler 组合。Bundler 依据项目中的 Gemfile 文件来管理 Gem,而 CocoaPods 通过 Podfile 来管理 Pod。

Gemfile 配置如下:

source 'https://gems.example.com' do
  gem 'cocoapods', '1.8.4'是管理 Gem 依赖的工具
  gem 'another_gem', :git => 'https://looseyi.github.io.git', :branch => 'master'
end

可见,Podfile 的 DSL 写法和 Gemfile 如出一辙。那什么情况会用到 Gemfile 呢 ?

CocoaPods 每年都会有一些重大版本的升级,前面聊到过 CocoaPodsinstall 过程中会对项目的 .xcodeproj 文件进行修改,不同版本其有所不同,这些在变更都可能导致大量 conflicts,处理不好,项目就不能正常运行了。我想你一定不愿意去修改 .xcodeproj 的冲突。

如果项目是基于 fastlane 来进行持续集成的相关工作以及 App 的打包工作等,也需要其版本管理等功能。

如何安装一套可管控的 Ruby 工具链?

讲完了这些工具的分工,然后来说说实际的运用。我们可以使用 homebrew + rbenv + RubyGems + Bundler 这一整套工具链来控制一个工程中 Ruby 工具的版本依赖。

以下是我认为比较可控的 Ruby 工具链分层管理图。下面我们逐一讲述每一层的管理方式,以及实际的操作方法。

可管控工具链的分层结构

1. 使用 homebrew 安装 rbenv

$ brew install rbenv

安装成功后输入 rbenv  就可以看到相关提示:

$ rbenv

rbenv 1.1.2
Usage: rbenv <command> [<args>]

Some useful rbenv commands are:
   commands    List all available rbenv commands
   local       Set or show the local application-specific Ruby version
   global      Set or show the global Ruby version
   shell       Set or show the shell-specific Ruby version
   install     Install a Ruby version using ruby-build
   uninstall   Uninstall a specific Ruby version
   rehash      Rehash rbenv shims (run this after installing executables)
   version     Show the current Ruby version and its origin
   versions    List installed Ruby versions
   which       Display the full path to an executable
   whence      List all Ruby versions that contain the given executable

See `rbenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/rbenv/rbenv#readme

2. 使用 rbenv 管理 Ruby 版本

使用 rbenv  来安装一个 Ruby 版本,这里我使用刚刚 release Ruby 2.7:

$ rbenv install 2.7.0

这个安装过程有些长,因为要下载 openssl 和 Ruby 的解释器,大概要 20 分钟左右。

安装成功后,我们让其在本地环境中生效:

$ rbenv shell 2.7.0
输入上述命令后,可能会有报错。 rbenv  提示我们在 .zshrc  中增加一行 eval "$(rbenv init -)"  语句来对 rbenv  环境进行初始化。如果报错,我们增加并重启终端即可。
$ ruby --version
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]
$ which ruby
/Users/gua/.rbenv/shims/ruby

切换之后我们发现 Ruby 已经切换到 rbenv  的管理版本,并且其启动 PATH  也已经变成 rbenv  管理下的 Ruby。并且我们可以看一下 Ruby 捆绑的 Gem  的 PATH :

$ which gem
/Users/bytedance/.rbenv/shims/gem

对应的 Gem  也已经变成 rbenv  中的 PATH 。

3. 查询系统级 Gem 依赖

如此,我们使用 rbenv 已经对 Ruby 及其 Gem  环境在版本上进行了环境隔离。我们可以通过 gem list  命令来查询当前系统环境下所有的 Gem  依赖:

$ gem list

*** LOCAL GEMS ***

activesupport (4.2.11.3)
...
claide (1.0.3)
cocoapods (1.9.3)
cocoapods-core (1.9.3)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
cocoapods-plugins (1.0.0)
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.5.0)
cocoapods-try (1.2.0)

记住这里的 CocoaPods 版本,我们后面项目中还会查询。

如此我们已经完成了全部的 Ruby、Gem 环境的配置,我们通过一张漏斗图再来梳理一下:

操作安装流程

如何使用 Bundler 管理工程中的 Gem 环境

下面我们来实践一下,如何使用 Bundler 来锁定项目中的 Gem 环境,从而让整个团队统一 Gem 环境中的所有 Ruby 工具版本。从而避免文件冲突和不必要的错误。

下面是在工程中对于 Gem 环境的层级图,我们可以在项目中增加一个 Gemfile 描述,从而锁定当前项目中的 Gem 依赖环境。

工程中独立 Gem 环境示意图

以下也会逐一讲述每一层的管理方式,以及实际的操作方法。

1. 在 iOS 工程中初始化 Bundler 环境

首先我们有一个 iOS Demo 工程 - GuaDemo :

$ ls -al
total 0
drwxr-xr-x   4 gua  staff   128 Jun 10 14:47 .
drwxr-xr-x@ 52 gua  staff  1664 Jun 10 14:47 ..
drwxr-xr-x   8 gua  staff   256 Jun 10 14:47 GuaDemo
drwxr-xr-x@  5 gua  staff   160 Jun 10 14:47 GuaDemo.xcodeproj

首先先来初始化一个 Bundler  环境(其实就是自动创建一个 Gemfile 文件):

$ bundle init

Writing new Gemfile to /Users/Gua/GuaDemo/Gemfile

2. 在 Gemfile  中声明使用的 CocoaPods 版本并安装

之后我们编辑一下这个 Gemfile 文件,加入我们当前环境中需要使用 CocoaPods 1.5.3 这个版本,则使用 Gemfile  的 DSL 编写以下内容:

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem "cocoapods", "1.5.3"

编写之后执行一下 bundle install :

$ bundle install
Fetching gem metadata from https://gems.ruby-china.com/............
Resolving dependencies...
...
Fetching cocoapods 1.5.3
Installing cocoapods 1.5.3
Bundle complete! 1 Gemfile dependency, 30 gems now installed.

发现 CocoaPods 1.5.3  这个指定版本已经安装成功,并且还保存了一份 Gemfile.lock  文件用来锁存这次的依赖结果。

3. 使用当前环境下的 CocoaPods 版本操作 iOS 工程

此时我们可以检查一下当前 Bundler 环境下的 Gem  列表:

$ bundle exec gem list

*** LOCAL GEMS ***

activesupport (4.2.11.3)
atomos (0.1.3)
bundler (2.1.4)
CFPropertyList (3.0.2)
claide (1.0.3)
cocoapods (1.5.3)
...

发现相比于全局 Gem 列表,这个列表精简了许多,并且也只是基础 Gem 依赖和 CocoaPodsGem 依赖。此时我们使用 bundle exec pod install  来执行 Install 这个操作,就可以使用 CocoaPods 1.5.3  版本来执行 Pod  操作了(当然,前提是你还需要写一个 Podfile ,大家都是 iOSer 这里就略过了)。

$ bundle exec pod install
Analyzing dependencies
Downloading dependencies
Installing SnapKit (5.0.1)
Integrating client project
[!] Please close any current Xcode sessions and use `GuaDemo.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

可以再来看一下 Podfile.lock  文件:

cat Podfile.lock
PODS:
  - SnapKit (5.0.1)

DEPENDENCIES:
  - SnapKit (~> 5.0.0)

SPEC REPOS:
  https://github.com/cocoapods/specs.git:
    - SnapKit

SPEC CHECKSUMS:
  SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb

PODFILE CHECKSUM: 1a4b05aaf43554bc31c90f8dac5c2dc0490203e8

COCOAPODS: 1.5.3

发现使用的 CocoaPods 的版本确实是 1.5.3 。而当我们不使用 bundle exec  执行前缀,则会使用系统环境中的 CocoaPods 版本。如此我们也就验证了工程中的 Gem 环境和系统中的环境可以通过 Bundler 进行隔离。

总结

  • 通过版本管理工具演进的角度可以看出,CocoaPods 的诞生并非一蹴而就,也是不断地借鉴其他管理工具的优点,一点点的发展起来的。VCS 工具从早期的 SVNGit,再细分出 Git Submodule,再到各个语言的 Package Manager 也是一直在发展的。
  • 虽然 CocoaPods 作为包管理工具控制着 iOS 项目的各种依赖库,但其自身同样遵循着严格的版本控制并不断迭代。希望大家可以从本文中认识到版本管理的重要性。
  • 通过实操 Bundler 管理工程的全流程,学习了 Bundler 基础,并学习了如何控制一个项目中的 Gem 版本信息。

后续我们将会围绕 CocoaPods ,从工具链逐渐深入到细节,根据我们的使用经验,逐一讲解。

知识点问题梳理

这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:

  1. PM 是如何进行依赖库的版本管理?
  2. RubyRVM/rbenv 之间的关系是什么?
  3. GemBundlerCocaPods 之间的关系是什么?
  4. 如何通过 Bundler 来管理工程中的 Gem 环境?如何锁死工程内部的 CocoaPods 版本?
查看原文

赞 1 收藏 0 评论 0

edmond 关注了标签 · 2020-08-19

ios

iOS 是苹果公司为其移动产品开发的操作系统。它主要给 iPhone、iPod touch、iPad 以及 Apple TV 使用。原本这个系统名为 iPhone OS,直到2010年6月7日 WWDC 大会上宣布改名为 iOS。

系统结构

  iOS的系统结构分为以下四个层次:核心操作系统(the Core OS layer),核心服务层(the Core Services layer),媒体层(the Media layer),Cocoa 触摸框架层(the Cocoa Touch layer)。

发展历史

iOS最早于2007年1月9日的苹果Macworld展览会上公布,随后于同年的6月发布的第一版iOS操作系统,当初的名称为“iPhone 运行 OS X”。最初,由于没有人了解“iPhone 运行 OS X”的潜在价值和发展前景,导致没有一家软件公司、没有一个软件开发者给“iPhone 运行 OS X”开发软件或者提供软件支持。于是,苹果公司时任CEO斯蒂夫.乔布斯说服各大软件公司以及开发者可以先搭建低成本的网络应用程序(WEB APP)来使得它们能像iPhone的本地化程序一样来测试“iPhone runs OS X”平台。 

  1. 2007年10月17日,苹果公司发布了第一个本地化IPhone应用程序开发包(SDK),并且计划在2月发送到每个开发者以及开发商手中。

  2. 2008年3月6日,苹果发布了第一个测试版开发包,并且将“iPhone runs OS X”改名为”iPhone OS“。 

  3. 2010年2月27日,苹果公司发布iPad,iPad同样搭载了”iPhone OS”。这年,苹果公司重新设计了“iPhone OS”的系统结构和自带程序。 

  4. 2010年6月,苹果公司将“iPhone OS”改名为“iOS”,同时还获得了思科iOS的名称授权。 

  5. 2010年第四季度,苹果公司的iOS占据了全球智能手机操作系统26%的市场份额。

  6. 2011年10月4日,苹果公司宣布iOS平台的应用程序已经突破50万个。

  7. 2012年2月,应用总量达到552,247个,其中游戏应用最多,达到95,324个,比重为17.26%;书籍类以60,604个排在第二,比重为10.97%;娱乐应用排在第三,总量为56,998个,比重为10.32%。

  8. 2012年6月,苹果公司在WWDC 2012 上宣布了iOS 6,提供了超过 200 项新功能。

  9. 2013年9月11日凌晨苹果在秋季发布会上宣布iOS 7于9月18日正式推出,2013年9月19日凌晨1点开放免费下载更新。

  10. iOS 8于2014年9月17号向用户推送正式版。

  11. iOS 9于2015年9月16日正式推出。iOS 9系统比iOS8更稳定,功能更全面,而且还更加开放。iOS 9加入了更多的新功能,包括更加智能的Siri,新加入的省电模式。iOS 9为开发者提供5000个全新的API。

  12. 2015年12月9日,苹果正式推送了iOS 9.2,更新内容相当之多,修复BUG改善稳定性自然不必多说,还增加了很多新功能,比如邮件增加了Mail Drop功能可以发送大附件、iBooks开始支持3D Touch、Apple News新闻中的“热门报道”等等

关注 50536

edmond 赞了文章 · 2020-05-18

不要跑,CRC没这么难!(简单易懂的CRC原理阐述)

不要跑,CRC没这么难!(简单易懂的CRC原理阐述)


网上大多的教材都是面向大佬的很多细节原理都没有讲清楚,对于我们这些新萌菜鸟们实在太不友好了。于是我写一篇相对轻松易懂的博客,希望能对学习CRC的朋友们有所帮助吧!

什么是CRC???


你说小学3年级的小明同学不知好歹喜欢村长女儿王大麻子,于是羞涩的他想到写一封情书已表心意。正所谓闺女似情人,爱女心切的村长凡是信件统统要过他之手。如果这份情书被她爸稍加“几笔”岂不悲剧了?

奇偶验证

如何验证情书是否被动过手脚(验证数据是否损坏),介于王大麻子数学不行,数数还行。作为数学课代表的小明同学立刻想到一个好主意:将所有的文字转化二进制,只要数一数1的数量是奇数还是偶数不就可以了吗!

比如我要传递M这个字符那么他的ASCII的值为0100 1101(M),就是数据末尾加上一个1或0,使其整个数据的1的数量可以凑齐奇数或偶数。

如果规定信件所有1的个数为奇数的话,那么0100 1101(M)才4个1,我们就数据的末尾加上一个1凑齐奇数(5个1):0100 1101 1;如果数据中的1刚好奇数,我们就在末尾加上一个0就可(这个方法叫做奇校验),当然相反如果规定的信件所有1的个数为偶数(偶校验),刚好处理的方法也是相反。

虽然这个方法简单到连他没有上学的弟弟都做得起(很容易通过硬件方式实现验证),但是这个的出错率是五五开啊(刚好少/多偶数个1),且永远检查不出0的错误(就算多100个零也看不出来)。

累加和校验

你说奇偶不行,哪我相加总该行吧?于是乎小明同学又推出第二号方案:将每一个文字(字节)的值相加来验证。

比如传递LOVE,我们就可以得到四个数字76(L) 79(O) 86(V) 69(E),他们相加可得310,如果这个数字嫌他太大了不好写,我们可以限制他在0~255(一个字节)这个范围之内,那么我们就得到一个验证数字55 =310 – 255(最简单的办法就是减去255的倍数)。然后将数字附加数据后面,对方接受到数据执行同样的操作对比验证数字是否一致。

虽说这个方法要比上面的方法要可靠一些,但凡事总有列外:

一、比如数据错误刚好等于验证数字
我们要传输的数据:76 79 86 69 310(验证数字)
发生错误的数据: 76 78(-1) 87(+1) 69 310(验证数字还是一样的)

二、单个字符的出错率为1/256

CRC验证原理

无数日夜小明同学冥思苦想,最终还剩最后三根头发之际他学会除法,头顶一凉想到一个绝佳主意:如果我将信件上的所有文字组合成一个长数字,然后用一个我和王大麻子都知道的数字(约定的多项式)去除以,将余数作为一个验证数字的话……

假设我要传输一个字符87(W),和王大麻子约定除数为6,那么结果很明显就是87 ÷ 6 = 14……3,那么我们可以将3作为验证数字附加原始数据的末尾发送。

但明显我们常规的借位除法大大超出了王大麻子的数学水平,于是小明同学决定不做人了发明一个二进制除法,并取名为“模二除法”。

所谓模二除法实际形式上和我们的平常使用的除法一样的(用于二进制运算),但是唯一不一同的是关于计算当中一切的加减法统统换成“异或”(XOR),一句话概括异或运算就是:异性相吸(返回真1),同性相斥(返回假0):1 Xor 0 = 1,0 Xor 1 = 1, 1 Xor 1 = 0, 0 Xor 0 = 0。

那么我们用上面列子来试一下模二除法(模二除法的结果不等于普通除法)

图片描述

王大麻子表示:我去!算完之后我还要核对余数?!不能再简单点吗?

于是乎小明同学又开始没日没夜苦想,最终当最后的狼牙山“三壮士”也离他而去时,他头顶一凉想到一个绝佳主意:在信件数据的末尾(即数据低位LSB)补上我和王大麻子都知道的数字的长度-1的0(生成项长度-1)然后在被同知数字相除,得到的余数再附加原始数据的末尾上。(这里说的原始数据是指没有补零的,且余数长度一定与补零的长度一致)

口说无凭,我们来重新用这个升级计算方法试一试。

图片描述

我们将余数10补在原始数据1010111的末尾得到了一个新数:101011110,然后我们再用110去除,神奇的事情发生了:没有余数了。(接受端只要将已修改的数据与生成项模二相除没有余数即代表数据无误)

这便是CRC(循环冗余校验,Cyclic Redundancy Check)是一种为了检测数据是否损坏处理办法。而当中的模二除法是无借位(不向高位借位的)的性质十分重要:意味我们计算CRC只需要简单的数据位移和Xor即可(到后面你就知道了)。

当然理论上讲CRC还是出错的可能(不过已经比程序猿能找到女朋友的几率要低了),所以选择一个好的除数就是一个非常重要的事情,关于CRC的除数有个专业的名称:生成多项式,简称生成项。如何选择生成项这是需要一定的代数编码知识(已经不是我这种咸鱼能搞懂的问题了)。好在我们可以直接用大佬计算好的生成项,不同的生成项有不同的CRC,如:CRC8、CRC16、CRC-CCITT、CRC32……。

从数学的角度来理解CRC(如果您只是了解如何计算CRC的话可以直接跳过本节)

我们不妨尝试将二进制转化一种多项式:数字中多少位1代表的是x的多少次方,0乘以任何数都是0所以不用管。

比如101010(42),可以转化为:x^5+x^3+x。(注意最低位0次方,任何数零次方都等于1)

假设用x^3+x^2+x除去x+1,可以得到如下式子:(这一段基本上完全搬运的循環冗餘校驗Wiki,写的真的好!)

图片描述

很好我们在两边再乘一个x+1:

图片描述

这里不难看出:得到一个商×除数+余数的式子,其中的(x^2+1)就是商,-1就是余数(我也不没懂为啥余数是负数)。我们还可以对左边式子再提取一次x,可得:

图片描述

我们可以理解这里提取的x是对于x^2+x+1(1011)进行补一次零变成了10110,实际上这个补零还有个说法叫做左移(要注意数据的方向高位在左边)。

如何理解补零的性质这个很简单,我们类比十进制的补零如:1要补两次零变成100,很明显补零就是乘与10的几次方。回到二进制中就是2的几次方,而多项式中的x就可以代表2。

通过以上的式子的整理可以得出通用公式即是:

图片描述

M(x)就代表的是原始数据,x^n代表的是数据末补n个0,K(x)代表的是Key也就是生成项,R(x)代表的余数也是上一节提到FCS。接收者接受到M(x)*x^n+R(x)看看是能被K(x)整除不,可以即为正确数据。

要注意的一点x^n的长度(补零长度)受限于R(x)(目的是可以直接追加在末尾,而不影响原始数据):他们长度一致,且一定比K(x)的长度少1。

关于R(x)为什么一定比K(x)的长度少1?我个人愚见是特殊的模二除法:K(x)的最高位一定1(不然没有意义啊),而数据处理过程中需要计算数据的最高位也是1(可以对照着除法的当中与除数对应的那个被除数的那部分数据),他们进行Xor就变成0,实际计算往往是剩下的部分(K(x)长度-1)(在程序设计中反正都会变成0干脆都不计算首位,这就是为啥网上的CRC生成多项式的简记码都是默认舍弃首位1的原因)。

CRC的原理实际上远比这些要复杂的多,涉及到群论这类我们这些吃瓜群众可望不可即的数理知识。以上也只是我对Wiki的搬运和理解瞎猜,希望大家能通过这个简单列子大概了解CRC的性质,如有不对之处有望大佬不惜赐教!

直接计算法


虽说上一节我们已经知道CRC是怎么计算的,但明显电脑可不会写除法公式(程序很难直接用这种方法)。且不说要对齐一个超长二进制数据进行逐位计算的困难不说,单单是像vbscript这种既没有几个位操作符又用起来蛋疼的语言基本上是很难实现(大佬:喵喵喵?)。不过可以转化一个思路:比如每次只取一部分数据来计算如何?

仔细观察计算的过程,可以发现其实每一次Xor都是固定不动的生成项与其对应的数据首位“消1”。那我们就可以假想出一个与生成项长度一致的“盒子”,取出一部分的数据出来若首位是1时就进行一次Xor,遇到0则左移到1为止,左移造成的右端的空缺用0补充。而这里0希望理解为一种“存储”,它“存储” 生成项中未和数据进行计算的那一部分,按顺序先后附加被计算数据的后面,当先一部分的数据全部计算之后,实际上“盒子”中剩下都是未和数据计算的部分的“和”11011 xor 10110 = 11011 xor ( 10000 xor 00100 xor 00010)(这里实际上就是Xor的交换律到后面就会体会到他的强大)

图片描述

通过以上的结论我们可以假装设计出一个算法(这里我用的是VBScript):

Const PLAY = &H1021&      '这里生成项(实际上就是CRC-CCITT),注意这里默认首位1是去掉的
Const TEXT = "123456789"  '这里就是原始数据

Dim L,I,CRC
Do While L < Len(TEXT)
'通过循环取得原始数据的每一个字符来计算
    L = L + 1
    CRC = CRC Xor (Asc(Mid(TEXT,L,1)) * &H100&)
    '实际上文中提到的“盒子”专业的说法应该叫做:寄存器。
    '这里取出新的字符出来与CRC寄存器里上一次未和数据进行计算的多项式的部分进行Xor,作为新数据进行下一步处理。
    '机智的你也许发现了:1021这个生成式是16位与一个字节8位不符怎么办?别忘我们还有神器——补零!乘上H100 = 256 = 2 ^ 8
    For I = 0 To 7
        '判断数据最高位是否为 1
        '1 - 左移一位(去掉数据首位1),剩下的部分进行Xor
        '0 - 就左移一位(去掉多余的0)
        If (CRC And &H8000&) Then 
            CRC = (CRC * 2) And &HFFFF& '// 左移
            CRC = CRC Xor PLAY
        Else
            CRC = (CRC * 2) And &HFFFF& '// 左移
        End If
    Next
    CRC = CRC And &HFFFF&
    '限制寄存器内数据大小为16位。
Loop

WScript.Echo Hex(CRC)

运行之后成功就可以得出CRC-CCITT (XModem)的标准验证值:31C3(在线计算网站:On-line CRC calculation and free library

驱动表法


实际上CRC就像开心消消乐一样,就是不断消除首位数据。这时你想:要是能一口气消除一个字节(8bit)以上的数据那该多好!

一般来讲文艺青年会这么做(CRC的正常计算):

图片描述

但是2B青年会这么想:可不可以将生成项先Xor了?

图片描述

我们可以先提前计算出与数据前几位相符的生成项之和再Xor从而到达一口气消掉了多位数据的目的,Xor的乘法交换律允许我们提前计算出需要的数据A Xor B Xor C = A Xor ( B Xor C )

既然如此干脆直接将前八位0000 0000 ~ 1111 1111(0 ~ 255,一个字节)这个范围所有的生产项的和全部计算存储成表格,等计算的时候直接取出数据的首字节出来作为索引找到对应表格的中生存项的和与去掉首位字节的数据进行Xor不就可以了吗。

表的由来

虽说想法很美好,但是如何实现前8位从0~255所有的生成项之和了?我们来思考一下CRC计算的本质是什么?复读没错是不断地消除首位数据,那么在Xor运算下消除方法就是:数据一样!

那么我们将0~255这256个数字进行CRC逐位计算后剩下不就是已经消除掉前8位数据的生成项之和吗!(因为前8位数据一样所以被消除了)

用通俗点语言就是:我们提前将一个字节的CRC验证码计算出来。(实际上这一段语文老师死得早的我构思很久,担心没有上面这段过渡部分会造成读者理解困难,希望大家能Get我的点……Orz)

以下就是关于CRC16的正序表格计算代码:

Const PLAY = &H8005&     '这里生成项(CRC16),注意这里默认首位1是去掉的
ReDim Table(255)         '这里存储表格

'// 计算表格部分
Dim I,J,Temp
'对于0~255这256个数字进行CRC计算,并将计算好的CRC验证码按顺序存储在表格(数组)中
For I = 0 To 255
    Temp = (I * &H100&) And &HFFFF&
    For J = 0 To 7
        If (Temp And &H8000) Then
            Temp = (Temp * 2) And &HFFFF&
            Temp = Temp Xor PLAY
        Else
            Temp = (Temp * 2) And &HFFFF&
        End If
    Next
    Table(I) = Temp And &HFFFF&
Next

'// 输出CRC16表格代码(使用VbsEdit的调试)
Dim y,x,u
Debug.WriteLine "Dim CRC16_Table:CRC16_Table = Array( _"
For y = 1 To 64
    For x = 1 To 4
        Debug.Write "&H",String(4 - Len(Hex(Table(u))),"0"),Hex(Table(u)),"&"
        If u <> 255 Then Debug.Write ", " Else Debug.Write " "
        u = u + 1
    Next
    Debug.WriteLine "_"
Next
Debug.WriteLine ")"

代码无误的话,应该会在调试框中得到如下代码(作为验证可以看看):

Dim CRC16_Table:CRC16_Table = Array( _
&H0000&, &H8005&, &H800F&, &H000A&, _
&H801B&, &H001E&, &H0014&, &H8011&, _
&H8033&, &H0036&, &H003C&, &H8039&, _
&H0028&, &H802D&, &H8027&, &H0022&, _
&H8063&, &H0066&, &H006C&, &H8069&, _
&H0078&, &H807D&, &H8077&, &H0072&, _
&H0050&, &H8055&, &H805F&, &H005A&, _
&H804B&, &H004E&, &H0044&, &H8041&, _
&H80C3&, &H00C6&, &H00CC&, &H80C9&, _
&H00D8&, &H80DD&, &H80D7&, &H00D2&, _
&H00F0&, &H80F5&, &H80FF&, &H00FA&, _
&H80EB&, &H00EE&, &H00E4&, &H80E1&, _
&H00A0&, &H80A5&, &H80AF&, &H00AA&, _
&H80BB&, &H00BE&, &H00B4&, &H80B1&, _
&H8093&, &H0096&, &H009C&, &H8099&, _
&H0088&, &H808D&, &H8087&, &H0082&, _
&H8183&, &H0186&, &H018C&, &H8189&, _
&H0198&, &H819D&, &H8197&, &H0192&, _
&H01B0&, &H81B5&, &H81BF&, &H01BA&, _
&H81AB&, &H01AE&, &H01A4&, &H81A1&, _
&H01E0&, &H81E5&, &H81EF&, &H01EA&, _
&H81FB&, &H01FE&, &H01F4&, &H81F1&, _
&H81D3&, &H01D6&, &H01DC&, &H81D9&, _
&H01C8&, &H81CD&, &H81C7&, &H01C2&, _
&H0140&, &H8145&, &H814F&, &H014A&, _
&H815B&, &H015E&, &H0154&, &H8151&, _
&H8173&, &H0176&, &H017C&, &H8179&, _
&H0168&, &H816D&, &H8167&, &H0162&, _
&H8123&, &H0126&, &H012C&, &H8129&, _
&H0138&, &H813D&, &H8137&, &H0132&, _
&H0110&, &H8115&, &H811F&, &H011A&, _
&H810B&, &H010E&, &H0104&, &H8101&, _
&H8303&, &H0306&, &H030C&, &H8309&, _
&H0318&, &H831D&, &H8317&, &H0312&, _
&H0330&, &H8335&, &H833F&, &H033A&, _
&H832B&, &H032E&, &H0324&, &H8321&, _
&H0360&, &H8365&, &H836F&, &H036A&, _
&H837B&, &H037E&, &H0374&, &H8371&, _
&H8353&, &H0356&, &H035C&, &H8359&, _
&H0348&, &H834D&, &H8347&, &H0342&, _
&H03C0&, &H83C5&, &H83CF&, &H03CA&, _
&H83DB&, &H03DE&, &H03D4&, &H83D1&, _
&H83F3&, &H03F6&, &H03FC&, &H83F9&, _
&H03E8&, &H83ED&, &H83E7&, &H03E2&, _
&H83A3&, &H03A6&, &H03AC&, &H83A9&, _
&H03B8&, &H83BD&, &H83B7&, &H03B2&, _
&H0390&, &H8395&, &H839F&, &H039A&, _
&H838B&, &H038E&, &H0384&, &H8381&, _
&H0280&, &H8285&, &H828F&, &H028A&, _
&H829B&, &H029E&, &H0294&, &H8291&, _
&H82B3&, &H02B6&, &H02BC&, &H82B9&, _
&H02A8&, &H82AD&, &H82A7&, &H02A2&, _
&H82E3&, &H02E6&, &H02EC&, &H82E9&, _
&H02F8&, &H82FD&, &H82F7&, &H02F2&, _
&H02D0&, &H82D5&, &H82DF&, &H02DA&, _
&H82CB&, &H02CE&, &H02C4&, &H82C1&, _
&H8243&, &H0246&, &H024C&, &H8249&, _
&H0258&, &H825D&, &H8257&, &H0252&, _
&H0270&, &H8275&, &H827F&, &H027A&, _
&H826B&, &H026E&, &H0264&, &H8261&, _
&H0220&, &H8225&, &H822F&, &H022A&, _
&H823B&, &H023E&, &H0234&, &H8231&, _
&H8213&, &H0216&, &H021C&, &H8219&, _
&H0208&, &H820D&, &H8207&, &H0202& _
)

我们可以以空间换取时间,直接将CRC表格计算好作为一个常数数组使用。

得到表格之后回到驱动表法的计算:取出CRC寄存器中的首位字节,然后将CRC左移去掉首字节然后取出一字节新数据装入CRC低端空出字节中,根据取出首字节找到对应表格中的生成项之和与CRC寄存器进行Xor,然后重复这个步骤直到数据全部取完计算完。要注意的是:因为驱动表法是一个一个字节计算,所以他必须计算之前在原始数据上补零(不能像直接计算法那样通过逐位左移方式自己完成补零)。我们来看一下代码是如何实现的:

Dim CRC16_Table                     '这里表格我们直接就套用上一节我们计算好的数据
Const TEXT = "123456789"            '这里就是原始数据

'这里要注意驱动表法没有逐位补零,所以我们要手动在数据末尾增加两个字节的零
Dim str:str = TEXT & String(2,Chr(0))

Dim L,I,T,CRC
'初始化CRC寄存器值为0
CRC = 0
Do While L < Len(str)
    L = L + 1
    '取出CRC寄存器中的首字节
    T = (CRC And &HFF00&) / &H100
    '左移数据一个字节(就是去掉寄存器中的首字节),并在空出的字节放入新的数据,限制寄存器大小为两个字节。
    CRC = ((CRC * &H100&) Or Asc(Mid(str,L,1))) And &HFFFF&
    '将已经去掉首字节的CRC寄存器与对应的表格生成项之和进行Xor
    CRC = CRC Xor CRC16_Table(T)
    '当然也可以直接将上面三句缩写成一句:
    'CRC = (((CRC * 256) Or Asc(Mid(str,L,1))) And &HFFFF&) Xor Table16((CRC And &HFF00&) / &H100)
Loop

'限制CRC大小为两个字节
CRC = CRC And &HFFFF&

WScript.Echo Hex(CRC)

输出结果为:FEE8(注意这个值并不是标准的CRC16验证码,后面我们还会讲到如何修改它。)

直驱动法


这个部分的思路完全来自poiu_elab大佬的【脑冻结】CRC我就拿下了,感谢大佬的分享!

我们不妨用大佬的列子看看(丑不要脸的偷懒):

图片描述

我们针对31 32 33 34进行CRC-CCITT驱动表法的计算,着重观察每次查询表格的索引(也就是黄色部分),你会发现实际上索引就是原始数据与寄存器前2位数据Xor计算的结果。

比如第一次查询时,寄存器为0与原始数据31 Xor得到查表的索引31,查得26 72并存入寄存器内;遇到第二个原始数据32与寄存器内的26进行Xor得到14,查得52 B5由于寄存器左移一个字节于是上一次72移到高位与52进行Xor得到20,于是现在寄存器内就是20(72 Xor 52) B5;遇到第三个原始数据33与寄存器内20(72 xor 52)进行Xor,得到13……重复这个过程直到查到最后一个原始数据为止。

通过上面的步骤我们可以整理出直驱动表法的算法:

伪语言版:

循环:取得字符
寄存器 = 去掉首位字节的寄存器 Xor表格[寄存器的首字节 Xor 取出的原始数据]

C语言版 CRC32直驱表法:

While(len--)
r=(r<<8)^t[(r>>24)^*p++];

我个人的总结直驱动表法实际上就是将驱动表法再一次简化,将数据输入和表格查询有机的结合在一起,这么做好处可以不用在原始数据上补零,方便持续计算CRC(不停的加入新的数据)。

由于vbscript对于32位数据进行四则运算(乘法补零)会溢出,所以不妨为我们尝试一下寄存器分段处理(就是将寄存器分成两个部分处理来解决数据溢出的问题),代码如下:

Dim Table_M(255)         '表格的高位
Dim Table_L(255)         '表格的低位

'// CRC32正序表格计算
Const PLOY_M = &H04C1&   '生成项高位,CRC32生成项:04C11DB7
Const PLOY_L = &H1DB7&   '生成项低位
Dim I,J,CRC_M,CRC_L
For I = 0 To 255
    CRC_M = (I * &H100&) And &HFFFF&
    CRC_L = 0
    For J = 0 To 7
        If (CRC_M And &H8000) Then
            '// 左移
            CRC_M = (CRC_M And &H7FFF) * &H2
            CRC_M = CRC_M Xor ((CRC_L And &H8000&) / &H8000&) '低位寄存器中高位会移动到高位寄存器中的低位
            CRC_L = (CRC_L And &H7FFF) * &H2
            '// Xor计算
            CRC_M = CRC_M Xor PLOY_M
            CRC_L = CRC_L Xor PLOY_L
        Else
            '// 左移
            CRC_M = (CRC_M And &H7FFF) * &H2
            CRC_M = CRC_M Xor ((CRC_L And &H8000&) / &H8000&)
            CRC_L = (CRC_L And &H7FFF) * &H2
        End If
        Table_M(I) = CRC_M
        Table_L(I) = CRC_L
    Next
Next

'// CRC32计算部分
Const TEXT = "123456789"  '原始数据
CRC_M = 0
CRC_L = 0
Dim T
Do While L < Len(TEXT)
    L = L + 1
    '计算出查询表格的索引
    T = ((CRC_M And &HFF00&) / &H100&) Xor Asc(Mid(TEXT,L,1))
    '分别计算出高位、低位寄存器,要注意的是低位寄存器中高位会移动到高位寄存器中的低位。
    CRC_M = ((CRC_M And &HFF&) * &H100) Xor ((CRC_L And &HFF00&) / &H100&) Xor Table_M(T)
    CRC_L = ((CRC_L And &HFF&) * &H100) Xor Table_L(T)
Loop

'记得输出不足CRC寄存器长度的数据时在前面补零
Debug.WriteLine String(4 - Len(Hex(CRC_M)),"0"),Hex(CRC_M),String(4 - Len(Hex(CRC_L)),"0"),Hex(CRC_L)

输出结果为:89A1897F

CRC参数模型


细心的朋友可能发现我们上一节计算出的CRC32验证值是不对的,为了方便机器更好的计算CRC所以制定一些规则,称为CRC参数模型。我们一睹CRC32模型的芳容:

图片描述

Width:代表生成项长度
Poly:生成项
Init:寄存器计算前的初始值,其实初始值是为了保存数据之前零(CRC计算特性是省略开头的零)
RefIn:输入原始数据进行二进制数据反转,因为数据输入是一个字节一个字节,所以反转也是一个字节,如图所示:
图片描述
RefOut:最后输出的CRC数据进行数据反转,注意是整个CRC数据进行反转(其实就是对CRC寄存器进行反转)
XorOut:对已经RefOut的CRC数据进行Xor处理,方式和Init一样。
Check:是对字符串“123456789”CRC计算的验证值,作为参考看看自己的程序计算是否有误。

根据这个模型,我们对上一节的CRC32算法再次魔改:

Dim Table_M:Table_M = Array( _  ' CRC32表格高位,由于篇幅有限请自行补齐代码
Dim Table_L:Table_L = Array( _  ' CRC32表格低位,由于篇幅有限请自行补齐代码


'颠倒二进制函数
Public Function RevBin(ByVal Value,ByVal lLen)
    RevBin = 0
    If IsNumeric(Value) And Value Then
        lLen = lLen - 1
        Dim I,REG
        For I = 0 To lLen
            If ((2^I) And Value) Then REG = REG Or 2^(lLen - I)
        Next
        RevBin = REG
    End If
End Function

'原始数据
Const TEXT = "123456789" 

'Init:初始化寄存器
CRC_M = &HFFFF&
CRC_L = &HFFFF&

'计算CRC部分
Dim T
Do While L < Len(TEXT)
    L = L + 1
    'RefIn:注意这里的对一个字节(8bit)的反转(我曾经在这里被坑过)
    T = ((CRC_M And &HFF00&) / &H100) Xor RevBin(Asc(Mid(TEXT,L,1)),8)
    CRC_M = ((CRC_M And &HFF) * &H100) Xor ((CRC_L And &HFF00&) / &H100) Xor Table_M(T)
    CRC_L = ((CRC_L And &HFF) * &H100) Xor Table_L(T)
Loop

'RefOut:反转计算出来的CRC值
Dim Temp
Temp = RevBin(CRC_L,16)
CRC_L = RevBin(CRC_M,16)
CRC_M = Temp

'XorOut:最后一次Xor
CRC_M = CRC_M Xor &HFFFF&
CRC_L = CRC_L Xor &HFFFF&

'输出CRC验证值
Debug.WriteLine String(4 - Len(Hex(CRC_M)),"0"),Hex(CRC_M),String(4 - Len(Hex(CRC_L)),"0"),Hex(CRC_L)

终于得到正确的CRC32验证码:CBF43926

进一步的优化:镜像CRC直驱表法算法


虽然可以计算出CRC32,但是每次都要颠倒输入、输出的值这显十分浪费性能。我们可以不可以将这个算法直接镜像,从而避免对其输入、输出的逆转,只需要对表格进行逆转和相反的数据位移方向即可。

图片描述

那么新的问题已经出现,我们怎能停滞不前:这逆转的表格咋算啊?!

答案就是:要用魔法(逆转)去打败魔法(逆转):将生成项:04C11DB7逆转为EDB88320,新数据插入的方向为数据低位(新数据不需要逆转只需要通过它的值计算出对应的生成项之和),数据位移方向向右(这个就是逆转的核心)。我们可以达到以下代码:

Dim Table_M(255)         '表格的高位
Dim Table_L(255)         '表格的低位

'// CRC32逆序表格计算,CRC32生成项:EDB88320
Const PLOY_M = &HEDB8&   '生成项高位
Const PLOY_L = &H8320&   '生成项低位
Dim I,J,CRC_M,CRC_L
For I = 0 To 255
    CRC_M = 0
    CRC_L = I
    For J = 0 To 7
        If (CRC_L And &H1) Then
            '// 右移
            CRC_L = (CRC_L And &HFFFE&) / &H2
            CRC_L = CRC_L Xor ((CRC_M And &H1) * &H8000&) '高位寄存器中低位会移动到低位寄存器中的高位
            CRC_M = (CRC_M And &HFFFE&) / &H2
            '// Xor计算
            CRC_M = CRC_M Xor PLOY_M
            CRC_L = CRC_L Xor PLOY_L
        Else
            '// 右移
            CRC_L = (CRC_L And &HFFFE&) / &H2
            CRC_L = CRC_L Xor ((CRC_M And &H1) * &H8000&)  
            CRC_M = (CRC_M And &HFFFE&) / &H2
        End If
    Next
    Table_M(I) = CRC_M
    Table_L(I) = CRC_L
Next

'//输出逆序表格代码
Public Sub Print(ByVal Name,ByVal lTable)
    Dim y,x,u
    Debug.WriteLine Name & " = Array( _"
    For y = 1 To 32
        For x = 1 To 8
            Debug.Write "&H",String(4 - Len(Hex(lTable(u))),"0"),Hex(lTable(u)),"&"
            If u <> 255 Then Debug.Write ", " Else Debug.Write " "
            u = u + 1
        Next
        Debug.WriteLine "_"
    Next
    Debug.WriteLine ")"
End Sub

Call Print("CRC32Table_MSB",Table_M)
Call Print("CRC32Table_LSB",Table_L)

计算出表格代码根据上图的镜像算法,我们可以将新数据xor到寄存器最低位取得表格索引,然后两个寄存器左移一个字节计算索引对应的逆转的生成项之和。封装一下代码就是这样的:

Class C_CRC
    '初始化类载入数据
    Private m_CRC32Table_MSB
    Private m_CRC32Table_LSB
    Private Sub Class_Initialize()
        m_CRC32Table_MSB = Array( _
        &H0000&, &H7707&, &HEE0E&, &H9909&, &H076D&, &H706A&, &HE963&, &H9E64&, _
        &H0EDB&, &H79DC&, &HE0D5&, &H97D2&, &H09B6&, &H7EB1&, &HE7B8&, &H90BF&, _
        &H1DB7&, &H6AB0&, &HF3B9&, &H84BE&, &H1ADA&, &H6DDD&, &HF4D4&, &H83D3&, _
        &H136C&, &H646B&, &HFD62&, &H8A65&, &H1401&, &H6306&, &HFA0F&, &H8D08&, _
        &H3B6E&, &H4C69&, &HD560&, &HA267&, &H3C03&, &H4B04&, &HD20D&, &HA50A&, _
        &H35B5&, &H42B2&, &HDBBB&, &HACBC&, &H32D8&, &H45DF&, &HDCD6&, &HABD1&, _
        &H26D9&, &H51DE&, &HC8D7&, &HBFD0&, &H21B4&, &H56B3&, &HCFBA&, &HB8BD&, _
        &H2802&, &H5F05&, &HC60C&, &HB10B&, &H2F6F&, &H5868&, &HC161&, &HB666&, _
        &H76DC&, &H01DB&, &H98D2&, &HEFD5&, &H71B1&, &H06B6&, &H9FBF&, &HE8B8&, _
        &H7807&, &H0F00&, &H9609&, &HE10E&, &H7F6A&, &H086D&, &H9164&, &HE663&, _
        &H6B6B&, &H1C6C&, &H8565&, &HF262&, &H6C06&, &H1B01&, &H8208&, &HF50F&, _
        &H65B0&, &H12B7&, &H8BBE&, &HFCB9&, &H62DD&, &H15DA&, &H8CD3&, &HFBD4&, _
        &H4DB2&, &H3AB5&, &HA3BC&, &HD4BB&, &H4ADF&, &H3DD8&, &HA4D1&, &HD3D6&, _
        &H4369&, &H346E&, &HAD67&, &HDA60&, &H4404&, &H3303&, &HAA0A&, &HDD0D&, _
        &H5005&, &H2702&, &HBE0B&, &HC90C&, &H5768&, &H206F&, &HB966&, &HCE61&, _
        &H5EDE&, &H29D9&, &HB0D0&, &HC7D7&, &H59B3&, &H2EB4&, &HB7BD&, &HC0BA&, _
        &HEDB8&, &H9ABF&, &H03B6&, &H74B1&, &HEAD5&, &H9DD2&, &H04DB&, &H73DC&, _
        &HE363&, &H9464&, &H0D6D&, &H7A6A&, &HE40E&, &H9309&, &H0A00&, &H7D07&, _
        &HF00F&, &H8708&, &H1E01&, &H6906&, &HF762&, &H8065&, &H196C&, &H6E6B&, _
        &HFED4&, &H89D3&, &H10DA&, &H67DD&, &HF9B9&, &H8EBE&, &H17B7&, &H60B0&, _
        &HD6D6&, &HA1D1&, &H38D8&, &H4FDF&, &HD1BB&, &HA6BC&, &H3FB5&, &H48B2&, _
        &HD80D&, &HAF0A&, &H3603&, &H4104&, &HDF60&, &HA867&, &H316E&, &H4669&, _
        &HCB61&, &HBC66&, &H256F&, &H5268&, &HCC0C&, &HBB0B&, &H2202&, &H5505&, _
        &HC5BA&, &HB2BD&, &H2BB4&, &H5CB3&, &HC2D7&, &HB5D0&, &H2CD9&, &H5BDE&, _
        &H9B64&, &HEC63&, &H756A&, &H026D&, &H9C09&, &HEB0E&, &H7207&, &H0500&, _
        &H95BF&, &HE2B8&, &H7BB1&, &H0CB6&, &H92D2&, &HE5D5&, &H7CDC&, &H0BDB&, _
        &H86D3&, &HF1D4&, &H68DD&, &H1FDA&, &H81BE&, &HF6B9&, &H6FB0&, &H18B7&, _
        &H8808&, &HFF0F&, &H6606&, &H1101&, &H8F65&, &HF862&, &H616B&, &H166C&, _
        &HA00A&, &HD70D&, &H4E04&, &H3903&, &HA767&, &HD060&, &H4969&, &H3E6E&, _
        &HAED1&, &HD9D6&, &H40DF&, &H37D8&, &HA9BC&, &HDEBB&, &H47B2&, &H30B5&, _
        &HBDBD&, &HCABA&, &H53B3&, &H24B4&, &HBAD0&, &HCDD7&, &H54DE&, &H23D9&, _
        &HB366&, &HC461&, &H5D68&, &H2A6F&, &HB40B&, &HC30C&, &H5A05&, &H2D02& )
        m_CRC32Table_LSB = Array( _
        &H0000&, &H3096&, &H612C&, &H51BA&, &HC419&, &HF48F&, &HA535&, &H95A3&, _
        &H8832&, &HB8A4&, &HE91E&, &HD988&, &H4C2B&, &H7CBD&, &H2D07&, &H1D91&, _
        &H1064&, &H20F2&, &H7148&, &H41DE&, &HD47D&, &HE4EB&, &HB551&, &H85C7&, _
        &H9856&, &HA8C0&, &HF97A&, &HC9EC&, &H5C4F&, &H6CD9&, &H3D63&, &H0DF5&, _
        &H20C8&, &H105E&, &H41E4&, &H7172&, &HE4D1&, &HD447&, &H85FD&, &HB56B&, _
        &HA8FA&, &H986C&, &HC9D6&, &HF940&, &H6CE3&, &H5C75&, &H0DCF&, &H3D59&, _
        &H30AC&, &H003A&, &H5180&, &H6116&, &HF4B5&, &HC423&, &H9599&, &HA50F&, _
        &HB89E&, &H8808&, &HD9B2&, &HE924&, &H7C87&, &H4C11&, &H1DAB&, &H2D3D&, _
        &H4190&, &H7106&, &H20BC&, &H102A&, &H8589&, &HB51F&, &HE4A5&, &HD433&, _
        &HC9A2&, &HF934&, &HA88E&, &H9818&, &H0DBB&, &H3D2D&, &H6C97&, &H5C01&, _
        &H51F4&, &H6162&, &H30D8&, &H004E&, &H95ED&, &HA57B&, &HF4C1&, &HC457&, _
        &HD9C6&, &HE950&, &HB8EA&, &H887C&, &H1DDF&, &H2D49&, &H7CF3&, &H4C65&, _
        &H6158&, &H51CE&, &H0074&, &H30E2&, &HA541&, &H95D7&, &HC46D&, &HF4FB&, _
        &HE96A&, &HD9FC&, &H8846&, &HB8D0&, &H2D73&, &H1DE5&, &H4C5F&, &H7CC9&, _
        &H713C&, &H41AA&, &H1010&, &H2086&, &HB525&, &H85B3&, &HD409&, &HE49F&, _
        &HF90E&, &HC998&, &H9822&, &HA8B4&, &H3D17&, &H0D81&, &H5C3B&, &H6CAD&, _
        &H8320&, &HB3B6&, &HE20C&, &HD29A&, &H4739&, &H77AF&, &H2615&, &H1683&, _
        &H0B12&, &H3B84&, &H6A3E&, &H5AA8&, &HCF0B&, &HFF9D&, &HAE27&, &H9EB1&, _
        &H9344&, &HA3D2&, &HF268&, &HC2FE&, &H575D&, &H67CB&, &H3671&, &H06E7&, _
        &H1B76&, &H2BE0&, &H7A5A&, &H4ACC&, &HDF6F&, &HEFF9&, &HBE43&, &H8ED5&, _
        &HA3E8&, &H937E&, &HC2C4&, &HF252&, &H67F1&, &H5767&, &H06DD&, &H364B&, _
        &H2BDA&, &H1B4C&, &H4AF6&, &H7A60&, &HEFC3&, &HDF55&, &H8EEF&, &HBE79&, _
        &HB38C&, &H831A&, &HD2A0&, &HE236&, &H7795&, &H4703&, &H16B9&, &H262F&, _
        &H3BBE&, &H0B28&, &H5A92&, &H6A04&, &HFFA7&, &HCF31&, &H9E8B&, &HAE1D&, _
        &HC2B0&, &HF226&, &HA39C&, &H930A&, &H06A9&, &H363F&, &H6785&, &H5713&, _
        &H4A82&, &H7A14&, &H2BAE&, &H1B38&, &H8E9B&, &HBE0D&, &HEFB7&, &HDF21&, _
        &HD2D4&, &HE242&, &HB3F8&, &H836E&, &H16CD&, &H265B&, &H77E1&, &H4777&, _
        &H5AE6&, &H6A70&, &H3BCA&, &H0B5C&, &H9EFF&, &HAE69&, &HFFD3&, &HCF45&, _
        &HE278&, &HD2EE&, &H8354&, &HB3C2&, &H2661&, &H16F7&, &H474D&, &H77DB&, _
        &H6A4A&, &H5ADC&, &H0B66&, &H3BF0&, &HAE53&, &H9EC5&, &HCF7F&, &HFFE9&, _
        &HF21C&, &HC28A&, &H9330&, &HA3A6&, &H3605&, &H0693&, &H5729&, &H67BF&, _
        &H7A2E&, &H4AB8&, &H1B02&, &H2B94&, &HBE37&, &H8EA1&, &HDF1B&, &HEF8D& )
    End Sub
    
    '// 计算CRC部分
    Public Function CRC32(ByVal Value)
        Dim CRC_MSB,CRC_LSB
        CRC_MSB = &HFFFF&
        CRC_LSB = &HFFFF&
        
        Dim StrLen:StrLen = Len(Value)
        Dim FirstByte,L
        Do While L < StrLen
            L = L + 1
            FirstByte = (CRC_LSB And &HFF&) Xor Asc(Mid(Value,L,1))
            CRC_LSB = ((CRC_LSB And &HFF00&) / &H100) Xor ((CRC_MSB And &HFF&) * &H100&) Xor m_CRC32Table_LSB(FirstByte)
            CRC_MSB = ((CRC_MSB And &HFF00&) / &H100) Xor m_CRC32Table_MSB(FirstByte)
        Loop
        
        CRC_MSB = CRC_MSB Xor &HFFFF&
        CRC_LSB = CRC_LSB Xor &HFFFF&
        
        CRC32 = String(4 - Len(Hex(CRC_MSB)),"0") & Hex(CRC_MSB) & String(4 - Len(Hex(CRC_LSB)),"0") & Hex(CRC_LSB)
    End Function
End Class

Dim CRC:Set CRC = New C_CRC
WScript.Echo CRC.CRC32("123456789")

到这里教程就完全结束了,当然我这个代码完全没有效率,纯粹写出演示闹着玩(笑)。如果是想用vbs进行CRC计算的话,我推荐Demon大佬的这篇Blog:用VBS实现PHP的crc32函数。使用MSScriptControl.ScriptControl对象调用js就可以十分方便的实现数据数据左右移。可读性远比我这篇代码要高的多,推荐去读一读方便理解。

其他博客推荐

虽说我码这么多字,但我还是没有自信能保证各位一定能懂(强行语文老师背锅),所以我个人觉得写的不错的几篇博客(也是我学习的博客),希望能对大家有用。

循环冗余校验(CRC)算法入门引导:https://blog.csdn.net/liyuanb...
我学习CRC32、CRC16、CRC原理和算法的总结(与WINRAR结果一致):https://wenku.baidu.com/view/...
【脑冻结】CRC我就拿下了:https://www.cnblogs.com/poiu-...
CRC的基本原理详解:https://blog.csdn.net/dream_1...
循环冗余检验 (CRC) 算法原理:http://www.cnblogs.com/esestt...
你真的搞明白CRC的计算过程了吗?:http://blog.chinaaet.com/wuya...
CRC检错技术原理:https://www.cnblogs.com/forge...
CRC算法详解与c源码:https://wenku.baidu.com/view/...
最通俗的CRC校验原理剖析:http://blog.51cto.com/winda/1...
VB的CRC32校验代码:https://blog.csdn.net/zdingyu...
CRC校验码原理、实例、手动计算:https://www.cnblogs.com/bugut...
如何计算CRC32校验和?:https://codeday.me/bug/201708...
CRC算法与实现:https://blog.csdn.net/ncdawen...

再推荐两个在线计算网站方便验证程序:

CRC(循环冗余校验)在线计算:http://www.ip33.com/crc.html
On-line CRC calculation and free library:https://www.lammertbies.nl/co...

当然如果你E文比较好的话,还是推荐Ross Williams的《A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHMS》 http://www.ross.net/crc/downl...

哦,对了!故事的最后:王大麻子其实喜欢他们班的班长——小强

查看原文

赞 2 收藏 1 评论 2

edmond 关注了用户 · 2020-05-10

日拱一兵 @tanrigongyibing

欢迎关注,公众号「日拱一兵」,以读侦探小说思维趣味轻松学习Java技术

关注 22841

edmond 关注了专栏 · 2020-05-10

终身学习者

我要先坚持分享20年,大家来一起见证吧。

关注 48946

edmond 关注了专栏 · 2020-05-10

疯狂的技术宅

本专栏文章首发于公众号:前端先锋 。

关注 27201

edmond 关注了用户 · 2020-05-10

Java技术栈 @javastack

Java技术栈以 Java 后端技术为主,包括 Java 核心技术、多线程编程、Spring Boot、Spring Cloud、缓存、消息队列、架构设计等各种技术干货、Java 面试题、各种学习资料等。微信公众号:Java技术栈(id:javastack),搜索关注吧!

关注 8476

edmond 关注了专栏 · 2020-05-10

前端全栈开发者

专栏首发于公众号《前端全栈开发者》,订阅关注第一时间阅读好文

关注 2975

edmond 关注了专栏 · 2020-05-10

SegmentFault 行业快讯

第一时间为开发者提供行业相关的实时热点资讯

关注 55990

认证与成就

  • 获得 1 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-17
个人主页被 690 人浏览