CocoaPods使用指南

news2024/11/18 0:32:29

CocoaPods使用指南 作者

前言

对于大多数软件开发团队来说,依赖管理工具必不可少,它能针对开源和私有依赖进行安装与管理,从而提升开发效率,降低维护成本。针对不同的语言与平台,其依赖管理工具也各有不同,例如 npm 管理 Javascript、Gradle 、Maven 管理 Jar 包、pip 管理 Python 包,Bundler、RubyGems 等等。本文聚焦于 iOS 方面,对 CocoaPods 的使用和部分原理进行阐述。

简单易用的 CocoaPods

对于 iOSer 来说,CocoaPods 并不陌生,几乎所有的 iOS 工程都会有它的身影。CocoaPods 采用 Ruby 构建,它是 Swift 和 Objective-C Cocoa 项目的依赖管理工具。在 MacOS 上,推荐使用默认的 Ruby 进行安装 (以下操作均在 CocoaPods 1.10.1、Ruby 2.7.2 进行):

sudo gem install cocoapods

如果安装成功,便可以使用 pod 的相关命令了。针对一个简单的项目来说,只需三步便可引入其他的依赖:

创建 Podfile 文件( CocoaPods 提供了 pod init 命令创建)
对 Podfile 文件进行编写,添加依赖的库,版本等信息。
在命令行执行 pod install 命令

顺利的话,这时在项目目录下会出现以下文件:

  • .xcworkspace:CocoaPods 将项目分为了主工程与依赖工程(Pods)。与 .xcodeproj 相比 .xcworkspace 对于管理多个项目的能力更强,你也可以将复杂的大型应用转换为以 .xcworkspace 构建的多个兄弟项目,从而更轻松的维护和共享功能。
  • Podfile.lock:记录并跟踪依赖库版本,将依赖库锁定于某个版本。
  • Pods 文件夹:存放依赖库代码。
  • Pods/Manifest.lock:每次 pod install 时创建的 Podfile.lock 的副本,用于比较这两个文件。一般来说,Podfile.lock 会纳入版本控制管理,而 Pods 文件夹则不会纳入版本控制变更;这意味着 Podfile.lock 表示项目应该依赖的库版本信息,而 Manifest.lock 则代表本地 Pods 的依赖库版本信息。在 pod install 后会将脚本插入到 Build Phases,名为 [CP] Check Pods Manifest.lock,从而保证开发者在运行 app 之前能够更新 Pods,以确保代码是最新的。

pod install vs. pod update

  • pod install:在每一次编辑 Podfile 以添加、更新或删除 pod 时使用。它会下载并安装新的 Pod,并将其版本信息写入 Podfile.lock 中。
  • pod outdated:列出所有比 Podfile.lock 中当前记录的版本 newer 版本的 pod。
  • pod update [PODNAME]:CocoaPods 会查找 newer 版本的 PODNAME,同时将 pod 更新到可能的最新版本(须符合 Podfile 限制)。若没有 PODNAME,则会将每一个 pod 更新到可能的最新版本。

一般来说,每次编辑 Podfile 时使用 pod install,仅在需要更新某个 pod 版本(所有版本)时才使用 pod update。同时,需提交 Podfile.lock 文件而不是 Pods 文件夹来达到同步所有 pod 版本的目的。

ps: newer 代表更加新的,若采用中文理解起来比较别扭。

Podfile 语法规范

Podfile 描述了一个或多个 Xcode 项目的 target 依赖关系,它是一种 DSL,了解它对我们使用好 CocoaPods 是一个必不可少的步骤。下面列出其相关的语法规范:

Root Options

install!:指定 CocoaPods 安装 Podfile 时使用的安装方法和选项。如:

install! 'cocoapods',
         :deterministic_uuids => false,
         :integrate_targets => false
  • :clean:根据 podspec 和项目支持平台的指定,清理所有不被 pod 使用的文件,默认为 true。
  • :deduplicate_targets:是否对 pod target 进行重复数据删除,默认为 true。
  • :deterministic_uuids:创建 pod project 是否产生确定性 UUID,默认为 true。
  • :integrate_targets:是否继承到用户项目中,为 false 会将 Pod 下载并安装到到 project_path/Pods 目录下,默认为 true。
  • :lock_pos_sources:是否锁定 pod 的源文件,当 Xcode 尝试修改时会提示解锁文件,默认为 true。
  • :warn_for_multiple_pod_sources:当多个 source 包含同名同版本 pod 时是否发出警告,默认为 true。
  • :warn_for_unused_master_specs_repo:如果没有明确指出 master specs repo 的 git 是否发出警告,默认为 true。
  • :share_schemes_for_development_pods:是否为开发中的 pod 分享 schemes,默认为 false。
  • :disable_input_output_paths:是否禁用 CocoaPods 脚本阶段的输入输出路径(Copy Frameworks 和 Copy Resources),默认为 false。
  • :preserve_pod_file_structure:是否保留所有 pod 的文件结构,默认为 false。
  • :generate_multiple_pod_projects:是否为每一个 pod target 生成 一个 project,生成与 Pods/Pods 文件夹中,默认为 false。
  • :incremental_installation:仅对自上次安装的 target 与其关联的 project 的变更部分进行重新生成,默认为 false。
  • :skip_pods_project_generation:是否跳过生成 Pods.xcodeproj 并仅进行依赖项解析与下载,默认为 false。 ensure_bundler!:当 bundler 版本不匹配时发出警告。
ensure_bundler! '~> 2.0.0'

Dependencies

pod:指定项目的依赖项

  • 依赖版本控制:=、>、>=、<、<= 为字面意思;~> 0.1.2 表示 0.1.2 <= currVersion < 0.2 之间的符合要求的最新版本版本。
  • Build configurations:默认依赖安装在所有的构建配置中,但也可仅在指定构建配置中启用。
  • Modular Headers:用于将 pod 转换为 module 以支持模块,这时在 Swift 中可以不用借助 bridging-header 桥接就可以直接导入,简化了 Swift 引用 Objective-C 的方式;也可以采用 use_modular_headers! 进行全局的变更。
  • Source:指定具有依赖项的源,同时会忽略全局源。
  • Subspecs:默认会安装所有的 subspecs,但可制定安装某些 subspecs。
  • Test Specs:默认不会安装 test specs,但可选择性安装 test specs。
  • Local path:将开发的 pod 与其客户端一起使用,可采用 path。
  • 指定某个特殊或者更为先进的 pod 版本
# 依赖版本控制
pod 'Objection', '~> 0.9' 
# Build configurations
pod 'PonyDebugger', :configurations => ['Debug', 'Beta'] 
# Modular Headers
pod 'SSZipArchive', :modular_headers => true 
# Source
pod 'PonyDebugger', :source => 'https://github.com/CocoaPods/Specs.git'
# Subspecs
pod 'QueryKit', :subspecs => ['Attribute', 'QuerySet'] 
# Test Specs
pod 'AFNetworking', :testspecs => ['UnitTests', 'SomeOtherTests']
# Local path
pod 'AFNetworking', :path => '~/Documents/AFNetworking'
# 指定某个特殊或者更为先进的 Pod 版本
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0'
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af'
# 指定某个 podspec
pod 'JSONKit', :podspec => 'https://example.com/JSONKit.podspec'

inherit:设置当前 target 的继承模式。

  • :complete 继承父级 target 的所有行为,:none 不继承父级 target 的任何行为,:search_paths 仅继承父级的搜索路径。
target 'App' do
  target 'AppTests' do
    inherit! :search_paths
  end
end

target:与 Xcode 中的 target 相对应,block 中是 target 的依赖项。
默认情况下,target 包含在父级 target 定义的依赖项,也即 inherit!:complete。关于 :complete:search_paths:complete 会拷贝父级 target 的 pod 副本,而 :search_paths 则只进行 FRAMEWORK_SEARCH_PATHSHEADER_SEARCH_PATHS 的相关拷贝,具体可通过比对 Pods/Target Support Files 的相关文件得以验证,一般在 UnitTests 中使用,以减少多余的 install_framework 过程。

target 'ShowsApp' do
  pod 'ShowsKit'
  # 拥有 ShowsKit 和 ShowTVAuth 的拷贝
  target 'ShowsTV' do
    pod 'ShowTVAuth'
  end
  # 拥有 Specta 和 Expecta 的拷贝
  # 并且能够通过 ShowsApp 进行访问 ShowsKit, 相当于 ShowsApp 是 ShowsTests 的宿主APP
  target 'ShowsTests' do
    inherit! :search_paths
    pod 'Specta'
    pod 'Expecta'
  end
end

abstract_target:定义 abstract_target,方便 target 进行依赖继承,在 CocoaPods 1.0 版本之前为 link_with

abstract_target 'Networking' do
  pod 'AlamoFire'
  target 'Networking App 1'
  target 'Networking App 2'
end

abstract:表示当前 target 是抽象的,不会链接到 Xcode 的 target 中。
script_phase:添加脚本阶段。
在执行完 pod install 之后 CocoaPods 会将脚本添加到对应的 target build phases。

target 'App' do
script_phase {
:name => 'scriptName' # 脚本名称,
        :script => 'echo "nihao"' # 脚本内容,
        :execution_position => :before_compile / :after_compile
        :shell_path => '/usr/bin/ruby' # 脚本路径
        :input_files => ['/input/filePath'], # 输入文件
        :output_files => ['/outpput/filePath'] # 输出文件
}
end

Target configuration

platform:指定其构建平台。
默认值为 iOS 4.3、OSX 10.6、tvOS 9.0 和 watchOS 2.0。CocoaPods 1.0 之前的版本为 xcodeproj

platform :ios, '4.0'

project:指定包含 target 的 Xcode project。这一般在 workspace 存在多个 xcode project 中使用:

# 在 FastGPS Project 中可以找到一个名为 MyGPSApp 的 target
target 'MyGPSApp' do
  project 'FastGPS'
  ...
end

inhibit_all_warnings!:禁止所有警告。如果针对单个 Pod,则可以采用:

pod 'SSZipArchive', :inhibit_warnings => true
pod 'SSZipArchive', :inhibit_warnings => true

user_modular_headers!:将所有 Pod 模块化。如果针对单个 Pod,则可以采用:

pod 'SSZipArchive', :modular_headers => true
pod 'SSZipArchive', :modular_headers => false

user_frameworks!:采用 framework 而不是 .a 文件的静态库。
可以通过 :linkage 指定使用静态库还是动态库:

use_frameworks!:linkage => :dynamic / :static

supports_swift_versions:指定 target definition 支持的 swift 版本要求

 supports_swift_versions '>= 3.0', '< 4.0' 

Workspace

workspace:指定包含所有项目的 Xcode workspace。

workspace 'MyWorkspace'

Sources

sources:Podfile 从指定的源列表中进行检索。sources 默认存储在 ~/.cocoapods/repos 中,是全局的而非按 target definition 存储。当有多个相同的 Pod 时,优先采用检索到的 Pod 的第一个源,因此当指定另一个来源时,则需显示指定 CocoaPods 的源。

source 'https://github.com/artsy/Specs.git'
source 'https://github.com/CocoaPods/Specs.git'

Hooks

plugin:指定在安装期间使用的插件。

plugin 'cocoapods-keys', :keyring => 'Eidolon'
plugin 'slather'

pre_install:在下载后和在安装 Pod 前进行更改。

pre_install do |installer|
  # Do something fancy!
end

pre_integrate:在 project 写入磁盘前进行更改。

pre_integrate do |installer|
  # perform some changes on dependencies
end

post_install:对生成 project 写入磁盘前进行最后的修改。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['GCC_ENABLE_OBJC_GC'] = 'supported'
    end
  end
end

post_integrate:在 project 写入磁盘后进行最后更改。

post_integrate do |installer|
  # some change after project write to disk
end

podspec 语法规范

podspec = pod Specification,意为 pod 规范,它是一个 Ruby 文件。包含了 Pod 的库版本详细信息,例如应从何处获取源、使用哪些文件、要应用构建设置等信息;也可以看作该文件是整个仓库的索引文件,了解它对我们知道 Pod 库是如何组织、运作的提供了很大帮助。podspec 的 DSL 提供了极大的灵活性,文件可通过 pod spec create 创建。

Root

名称用途必需
namepod 名称required
versionpod 版本,遵循语义化版本控制required
swift_version支持的 Swift 版本
cocoapods_version支持的 CocoaPods 版本
authorspod 维护者的姓名和电子邮件,用“, ”进行分割required
licensepod 的许可证required
homepagepod 主页的 URLrequired
source源地址,即源文件的存放地址,支持多种形式源required
summarypod 的简短描述required
prepare_command下载 pod 后执行的 bash 脚本
static_framework是否采用静态 framework 分发
deprecated该库是否已被弃用
deprecated_in_favor_of该库名称已被弃用,取而代之
Pod::Spec.new do |s|
  s.name             = 'CustomPod'
  s.version          = '0.1.0'
  s.summary          = 'A short description of CustomPod.'
  s.swift_versions   = ['3.0', '4.0', '4.2']
  s.cocoapods_version  =  '>= 0.36'
  s.author           = { 'nihao' => 'XXXX@qq.com' }
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.homepage         = 'https://github.com/XXX/CustomPod'

# Supported Key
# :git=> :tag, :branch, :commit,:submodules
# :svn=> :folder, :tag,:revision
# :hg=>:revision
# :http=> :flatten, :type, :sha256, :sha1,:headers
  s.source           = { :git => 'https://github.com/XX/CustomPod.git', :tag => s.version.to_s }
  s.prepare_command  =  'ruby build_files.rb'
  s.static_framework = true
  s.deprecated       = true
  s.deprecated_in_favor_of  =  'NewMoreAwesomePod'
end

Platform

platform:pod 支持的平台,留空意味着 pod 支持所有平台。当支持多平台时应该用 deployment_target 代替。

spec.platform = :osx, '10.8'

deployment_target:允许指定支持此 pod 的多个平台,为每个平台指定不同的部署目标。

spec.ios.deployment_target = '6.0'
spec.osx.deployment_target = '10.8'

Build settings

dependency:基于其他 pods 或子规范的依赖

spec.dependency 'AFNetworking', '~&gt; 1.0', :configurations =&gt; ['Debug']

info_plist:加入到生成的 Info.plist 的键值对,会对 CocoaPods 生成的默认值进行覆盖。仅对使用 framework 的框架有影响,对静态库无效。对于应用规范,这些值将合并到应用程序主机的 Info.plist;对于测试规范,这些值将合并到测试包的 Info.plist。

spec.info_plist = {
  'CFBundleIdentifier' =&gt; 'com.myorg.MyLib',
  'MY_VAR' =&gt; 'SOME_VALUE'
}

requires_arc:允许指定哪些 source_files 采用 ARC,不使用 ARC 的文件将具有 -fno-objc-arc 编译器标志

spec.requires_arc = false
spec.requires_arc = 'Classes/Arc'
spec.requires_arc = ['Classes/*ARC.m', 'Classes/ARC.mm']

frameworks:使用者 target 需要链接的系统框架列表

spec.ios.framework = 'CFNetwork'
spec.frameworks = 'QuartzCore', 'CoreData'

weak_frameworks:使用者 target 需要弱链接的框架列表

spec.weak_framework = 'Twitter'
spec.weak_frameworks = 'Twitter', 'SafariServices'

libraries:使用者 target 需要链接的系统库列表

spec.ios.library = 'xml2'
spec.libraries = 'xml2', 'z'

compiler_flags:应传递给编译器的 flags

spec.compiler_flags = '-DOS_OBJECT_USE_OBJC=0', '-Wno-format'

pod_target_xcconfig:将指定 flag 添加到最终 pod 的 xcconfig 文件

spec.pod_target_xcconfig = { 'OTHER_LDFLAGS' =&gt; '-lObjC' }

user_target_xcconfig:🙅 将指定 flag 添加到最终聚合的 target 的 xcconfig,不推荐使用此属性,因为会污染用户的构建设置,可能会导致冲突。

spec.user_target_xcconfig = { 'MY_SUBSPEC' =&gt; 'YES' }

prefix_header_contents:🙅 在 Pod 中注入的预编译内容,不推荐使用此属性,因为其会污染用户或者其他库的预编译头。

spec.prefix_header_contents = '#import &lt;UIKit/UIKit.h&gt;', '#import &lt;Foundation/Foundation.h&gt;'

prefix_header_file:预编译头文件,false 表示不生成默认的 CocoaPods 的与编译头文件。🙅 不推荐使用路径形式,因为其会污染用户或者其他库的预编译头。

spec.prefix_header_file = 'iphone/include/prefix.pch'
spec.prefix_header_file = false

module_name:生成的 framrwork / clang module 使用的名称,而非默认名称。

spec.module_name = 'Three20'

header_dir:存储头文件的目录,这样它们就不会被破坏。

spec.header_dir = 'Three20Core'

header_mappings_dir:用于保留头文件文件夹的目录。如未提供,头文件将被碾平。

spec.header_mappings_dir = 'src/include'

script_phases:该属性允许定义脚本在 pod 编译时执行,其作为 xcode build 命令的一部分执行,还可以利用编译期间所设置的环境变量。

spec.script_phases = [
    { :name =&gt; 'Hello World', :script =&gt; 'echo "Hello World"' },
    { :name =&gt; 'Hello Ruby World', :script =&gt; 'puts "Hello World"', :shell_path =&gt; '/usr/bin/ruby' },
  ]

File patterns

文件模式指定了库的所有文件管理方式,如源代码、头文件、framework、libaries、以及各种资源。其文件模式通配符形式可参考 LINK。

source_files:指定源文件

spec.source_files = 'Classes/**/*.{h,m}', 'More_Classes/**/*.{h,m}'

public_header_files:指定公共头文件,这些头文件与源文件匹配,并生成文档向用户提供。如果未指定,则将 source_files 中的所有头文件都包含生成。

spec.public_header_files = 'Headers/Public/*.h'

project_header_files:指定项目头文件,与公共头文件相对应,以排除不应向用户项目公开且不应用于生成文档的标头,且不会出现在构建目录中。

spec.project_header_files = 'Headers/Project/*.h'

private_header_files:私有头文件,与公共头文件对应,以排除不应向用户项目公开且不应用于生成文档的标头,这些头文件会出现在产物中的 PrivateHeader 文件夹中。

spec.private_header_files = 'Headers/Private/*.h'

vendered_frameworks:pod 附加的 framework 路径

spec.ios.vendored_frameworks = 'Frameworks/MyFramework.framework'
spec.vendored_frameworks = 'MyFramework.framework', 'TheirFramework.xcframework'

vendered_libraries:pod 附加的 libraries 路径

spec.ios.vendored_library = 'Libraries/libProj4.a'
spec.vendored_libraries = 'libProj4.a', 'libJavaScriptCore.a'

on_demand_resources:根据 Introducing On demand Resources 按需加载资源,不推荐与主工程共享标签,默认类别为 category =&gt; :download_on_demand

s.on_demand_resources = {
  'Tag1' =&gt; { :paths =&gt; ['file1.png', 'file2.png'], :category =&gt; :download_on_demand }
}
s.on_demand_resources = {
  'Tag1' =&gt; { :paths =&gt; ['file1.png', 'file2.png'], :category =&gt; :initial_install }
}

resources:为 pod 构建的 bundle 的名称和资源文件,其中 key 为 bundle 名称,值代表它们应用的文件模式。

spec.resource_bundles = {
'MapBox' =&gt; ['MapView/Map/Resources/*.png'],
    'MapBoxOtherResources' =&gt; ['MapView/Map/OtherResources/*.png']
}

exclude_files:排除的文件模式列表

spec.ios.exclude_files = 'Classes/osx'
spec.exclude_files = 'Classes/**/unused.{h,m}'

preserve_paths:下载后不应删除的文件。默认情况下,CocoaPods 会删除与其他文件模式不匹配的所有文件

spec.preserve_path = 'IMPORTANT.txt'
spec.preserve_paths = 'Frameworks/*.framework'

module_map:pod 继承为 framework 时使用的模块映射文件,默认为 true,CocoaPods 根据 公共头文件创建 module_map 文件。

spec.module_map = 'source/module.modulemap'
spec.module_map = false

Subspecs

subspec:子模块的规范;实行双重继承:specs 自动继承所有 subspec 作为依赖项(除非指定默认 spec);subspec 继承了父级的属性;

# 采用不同源文件的 Specs, CocoaPods 自动处理重复引用问题
subspec 'Twitter' do |sp|
  sp.source_files = 'Classes/Twitter'
end

subspec 'Pinboard' do |sp|
  sp.source_files = 'Classes/Pinboard'
end

# 引用其他子规范
s.subspec "Core" do |ss|
    ss.source_files  = "Sources/Moya/", "Sources/Moya/Plugins/"
    ss.dependency "Alamofire", "~&gt; 5.0"
    ss.framework  = "Foundation"
  end
  s.subspec "ReactiveSwift" do |ss|
    ss.source_files = "Sources/ReactiveMoya/"
    ss.dependency "Moya/Core"
    ss.dependency "ReactiveSwift", "~&gt; 6.0"
  end
  s.subspec "RxSwift" do |ss|
    ss.source_files = "Sources/RxMoya/"
    ss.dependency "Moya/Core"
    ss.dependency "RxSwift", "~&gt; 5.0"
  end
end

# 嵌套子规范
Pod::Spec.new do |s|
  s.name = 'Root'
  s.subspec 'Level_1' do |sp|
    sp.subspec 'Level_2' do |ssp|
    end
  end
end

default_subspecs:默认子规范数组名称,不指定将全部子规范作为默认子规范,:none 表示不需要任何子规范。

spec.default_subspec = 'Core'
spec.default_subspecs = 'Core', 'UI'
spec.default_subspecs = :none

scheme:用以给指定 scheme configuration 添加拓展

spec.scheme = { :launch_arguments =&gt; ['Arg1'] }
spec.scheme = { :launch_arguments =&gt; ['Arg1', 'Arg2'], :environment_variables =&gt; { 'Key1' =&gt; 'Val1'} }

test_spec:测试规范,在 1.8 版本支持。可参考:CocoaPods 1.8 Beta

requires_app_host:是否需要宿主 APP 运行测试,仅适用于测试规范。

app_host_name:必要时作用于应用程序的应用程序规范名称

app_spec:宿主 APP 规范

Pod::Spec.new do |s|
  s.name         = 'CannonPodder'
  s.version      = '1.0.0'
  # ...rest of attributes here
  s.app_spec 'DemoApp' do |app_spec|
    app_spec.source_files = 'DemoApp/**/*.swift'
    # Dependency used only by this app spec.
    app_spec.dependency 'Alamofire'
  end
  s.test_spec 'Tests' do |test_spec|
    test_spec.requires_app_host = true
    # Use 'DemoApp' as the app host.
    test_spec.app_host_name = 'CannonPodder/DemoApp'
    # ...rest of attributes here
    # This is required since 'DemoApp' is specified as the app host.
    test_spec.dependency 'CannonPodder/DemoApp'
  end
end

Multi-Platform support

存储特定于某一个平台的值,分别为 ios、osx、macOS、tvos、watchos:

spec.resources = 'Resources/**/*.png'
spec.ios.resources = 'Resources_ios/**/*.png'

Pod 的开发流程

了解完 Podfile 和 podspec 的相关的规范之后,那么开发自己的 pod 应该是一件驾轻就熟的事。

Spec Repo

Spec Repo 是 podspec 的仓库,即是存储相关的 podspec 文件的地方。本地源存储于 ~/.cocoapods/repos中,它从 git 上拉取并完全保留目录结构。可以发现, Master Specs Repo 的现在目录结构有些特殊;以往版本的 Master Spec Repo 是完全在同一目录下的,但若大量文件在同一目录中会导致了 Github 下载慢 的问题。为解决这个问题,采用散列表形式处理。具体方式为对名称进行 MD5 计算得到散列值,取前三位作为目录前缀,以对文件分散化。初次之外,CocoaPods 后续还采用 CDN 以及 trunk 进一步加快下载速度,有兴趣可以参考 CocoaPods Source 管理机制。

如:md5("CJFoundation") =&gt; 044d913fdd5a52b303222c357521f744CJFoundation 则在 /Specs/0/4/4 目录中

Screenshot 2022-12-15 at 17.15.49.png

Create

只需利用 pod lib create [PodName] 命令便可以快速创建一个自己的 pod 。填写好使用平台、使用语言、是否包含 Demo、测试框架等信息,CocoaPods 会从默认的 Git 地址中拉取一份 pod 模版,同时也可以通过 --template-url=URL 指定模版地址。在执行完后,整个文件结构如下:

tree CustomPod -L 2
CustomPod
├── CustomPod
│   ├── Assets // 存放资源文件
│   └── Classes
│       └── RemoveMe.[swift/m] // 单一文件以确保最初编译工作
├── CustomPod.podspec // Pod 的 spec 文件, 是一个 Pod 依赖的索引以及规范信息
├── Example // 用作演示/测试的示例项目
│   ├── CustomPod
│   ├── CustomPod.xcodeproj
│   ├── CustomPod.xcworkspace
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Pods
│   └── Tests
├── _Pods.xcodeproj -&gt; Example/Pods/Pods.xcodeproj // 指向 Pods 项目的以获得 Carthage 支持
├── LICENSE // 许可证
└── README.md  // 自述文件

Development

将源文件和资源分别放入 Classes / Assets 文件夹中,或者按你喜欢的方式组织文件,并在 podspec 文件中编辑相应项。如果你有任何想使用的配置项,可参考前面的podsepc 语法规范 。

一般来说,开发 Pod 一般都是作为本地 Pod 被其他 Project 所依赖进行开发,无论是使用 example 文件夹的 project 或者其他的 Project。

pod 'Name', :path =&gt; '~/CustomPod/'

Testing

通过 pod lib lint 以验证 Pod 仓库的使用是否正常。

Release

前面提到过 podspec 可以看作是整个仓库的索引文件,有了这个文件也就能组织起一个 Pod。因此官方的源以及私有源都只需要 podspec 即可,而其他文件则应推送到 podspec 中 source 中指定仓库,这个仓库应该是你自创建的。

在准备发布推送源代码时,需要更新版本号以及在 git 上打上 tag,这是为了进行版本号匹配,因为默认情况下的 podspec 文件中:

s.source = { :git =&gt; 'https://github.com/XXX/CustomPod.git', :tag =&gt; s.version.to_s }

可能你的工作流操作如下:

$ cd ~/code/Pods/NAME
$ edit NAME.podspec
# set the new version to 0.0.1
# set the new tag to 0.0.1
$ pod lib lint
$ git add -A &amp;&amp; git commit -m "Release 0.0.1."
$ git tag '0.0.1'
$ git push --tags

存有几种方式推送 podspec 文件:

  1. 推送到公共仓库,需要用到的 trunk 子命令,更多可以参考 Getting setup with Trunk:
# 通过电子邮箱进行注册
pod trunk register orta@cocoapods.org 'Orta Therox' --description='macbook air' 
# 将指定podspec文件推送到公共仓库中
pod trunk push [NAME.podspec] 
# 添加其他人作为协作者
pod trunk add-owner ARAnalytics kyle@cocoapods.org 
  1. 推送到私有源,例如 Artsy/Specs,需要用到 repo 子命令,更多可以参考 Private Pods:
# 将私有源地址添加到本地
pod repo add REPO_NAME SOURCE_URL 
# 检查私有源是否安装成功并准备就绪
cd ~/.cocoapods/repos/REPO_NAME
pod repo lint .
# 将Pod的podspec添加到指定REPO_NAME中
pod repo push REPO_NAME SPEC_NAME.podspec
  1. 不推送到任何源中,若能存在以 URL 方式检索到 podspec文件,则可用该 URL,一般采用仓库地址,例如:
pod 'AFNetworking', :git =&gt; 'https://github.com/XXX/CustomPod.git'

Semantic Versioning

语义化版本控制顾名思义是一种语义上的版本控制,它不要求强制遵循,只是希望开发者能够尽量遵守。如果库之间依赖关系过高,可能面临版本控制被锁死的风险(可能需要对每一个依赖库改版才能完成某次升级);如果库之间依赖关系过于松散,又将无法避免版本的混乱(可能库兼容性不再能支持以往版本),语义化版本控制正是作为这个问题的解决方案之一。无论在 CocoaPods 中,还是 Swift Packager Manager 上,官方都希望库开发者的的版本号能遵循这一原则:

例如,给定版本号 MAJOR.MINOR.PATCH

  1. MAJOR:进行不兼容的 API 更改时进行修改
  2. MINOR:向后兼容的方式添加新功能时进行修改
  3. PATCH:进行向后兼容的错误修复时进行修改

先行版本号以及版本编译信息可以添加到 MAJOR.MINOR.PATCH 后面以作为延伸。

CocoaPods 原理浅析

CococaPods 核心组件

CocoaPods 被 Ruby 管理,其核心部分也被分为一个一个组件。下载源码,可以看到 Gemfile 文件如下,其依赖了若干个 gem,有意思的是 cp_gem 函数,通过 SKIP_UNRELEASED_VERSIONSpath 来控制是否采用本地的 gem 路径,实现了 DEVELOPMENT 与 RELEASE 环境的切换。

SKIP_UNRELEASED_VERSIONS = false
# Declares a dependency to the git repo of CocoaPods gem. This declaration is
# compatible with the local git repos feature of Bundler.
def cp_gem(name, repo_name, branch = 'master', path: false)
  return gem name if SKIP_UNRELEASED_VERSIONS
  opts = if path
           { :path =&gt; "../#{repo_name}" }
         else
           url = "https://github.com/CocoaPods/#{repo_name}.git"
           { :git =&gt; url, :branch =&gt; branch }
         end
  gem name, opts
end

source 'https://rubygems.org'

gemspec

group :development do
  cp_gem 'claide',                'CLAide'
  cp_gem 'cocoapods-core',        'Core'
  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-trunk',       'cocoapods-trunk'
  cp_gem 'cocoapods-try',         'cocoapods-try'
  cp_gem 'molinillo',             'Molinillo'
  cp_gem 'nanaimo',               'Nanaimo'
  cp_gem 'xcodeproj',             'Xcodeproj'
  gem 'cocoapods-dependencies', '~&gt; 1.0.beta.1'
  ...
end

这些组件相对独立,被分成一个一个 Gem 包,在 Core Components 中,可以找到对这些组件的简要描述。同时也可以到 CocoaPods 的 Github 中去看详细文档。

  • CocoaPods:命令行支持与安装程序,也会处理 CocoaPods 的所有用户交互。
  • cocoapods-core:对模版文件的解析,如 Podfile、.podspec 等文件。
  • CLAide:一个简单的命令解析器,它提供了一个快速创建功能齐全的命令行界面的 API。
  • cocoapods-downloader:用于下载源码,为各种类型的源代码控制器(HTTP/SVN/Git/Mercurial) 提供下载器。它提供 tags、commites、revisions、branches 以及 zips 文件的下载与解压缩操作。
  • Monlinillo:CocoaPods:对于依赖仲裁算法的封装,它是一个具有前项检察的回溯算法。不仅在 pods 中,Bundler 和 RubyGems 也是使用这一套仲裁算法。
  • Xcodeproj:通过 Ruby 来对 Xcode projects 进行创建于修改。如:脚本管理、libraries 构建、Xcode workspece 和配置文件的管理。
  • cocoapods-plugins:插件管理,其中有 pod plugins 命令帮助你获取的可用插件列表以及开发一个新插件等功能,具体可用 pod plugins --help 了解。

pod install 做了什么

执行 pod install --verbose,会显示 pod install 过程中的更多 debugging 信息。下文主要参考:整体把握 CocoaPods 核心组件

经过消息转发与 CLAide 命令解析,最终调用了 CocoaPods/lib/cocoapods/installer.rb 的 install! 函数,主要流程图如下:

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

    write_lockfiles
    perform_post_install_actions
end

1. 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 &lt;&lt; "\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 的 hook。

2. 解决依赖冲突(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 &amp; lockfile 是否需要更新
    UI.section 'Verifying no changes' do
        verify_no_podfile_changes!
        verify_no_lockfile_changes!
    end if deployment?
    analyzer
end

通过 Podfile、Podfile.lock 以及 manifest.lock 等生成 Analyzer 对象,其内部会使用个 Molinillo 算法解析得到一张依赖关系表,进行一系列的分析与依赖冲突解决。

3. 下载依赖文件(download dependencies)

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

经过前面分析与解决依赖冲突后,这是会进行依赖下载。会根据依赖信息是否被新添加或者修改等信息进行下载,同时下载后也会在本地留有一份缓存,其目录在 ~/Library/Caches/CocoaPods 。

4. 验证 targets(validate targets)

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
  • verify_no_duplicate_framework_and_library_names:验证是否有重名的 framework / library
  • verify_no_static_framework_transitive_dependencies:验证动态库是否有静态链接库依赖。个人认为,这个验证是不必要的,起码不必要 error。
  • verify_swift_pods_swift_version:验证 Swift pod 的 Swift 版本配置且相互兼容
  • verify_swift_pods_have_module_dependencies:验证 Swift pod 是否支持 module
  • verify_no_multiple_project_names:验证没有重名的 project 名称

5. 生成工程(Integrate)

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

将之前版本仲裁的所有组件通过 project 文件的形式组织起来,并对 project 中做一些用户指定的配置。

6. 写入依赖(write lockfiles)

def write_lockfiles
  @lockfile = generate_lockfile
  UI.message "- Writing Lockfile in #{UI.path config.lockfile_path}" do
    # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the
    # contents of the file are the same.
    @lockfile.write_to_disk(config.lockfile_path)
  end
  UI.message "- Writing Manifest in #{UI.path sandbox.manifest_path}" do
    # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the
    # contents of the file are the same.
    @lockfile.write_to_disk(sandbox.manifest_path)
  end
end

将依赖更新写入 Podfile.lock 与 Manifest.lock

7. 结束回调(perform post install action)

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
  # 警告移除的 master specs repo 的 specs
  warn_for_removing_git_master_specs_repo
  # 输出结束信息 `Pod installation complete!`
  print_post_install_message
end

最后的收尾工作,进行 post install action 的 hook 执行以及一些 warning 打印。

CocoaPods + Plugins

早在 2013 年,CocoaPods 就添加了对插件的支持,以添加不符合依赖管理和生态系统增长为主要目标的功能。CocoaPods Plugins 可以:在 install 前后添加 hook、添加新命令到 pod、以及利用 Ruby 动态性做任何事。下面介绍一下常见的插件:

  • cocoapods-binary:一个比较早期的二进制插件库,是诸多二进制方案的灵感来源
  • cocoapods-repo-update:自动化 pod repo update
  • cocoapods-integrate-flutter:将 flutter 与现有 iOS 应用程序集成
  • cocoapods-uploader:上传文件/目录到远程仓库

ps:许多插件可能许久未维护,读者使用需自行斟酌。

不太常见概念

CocoaPods 的配置内容几乎包含了 Xcode Build 的方方面面,因此存在许多不太常见的概念,在此做一个链接聚合以供参考。

  • Clang Module / module_map / umbrella header:Clang Module 是 Clang 16.0.0 中引入的概念,用以解决 #include / #import 头文件引入导致的相关问题;module_map 是用以描述 clang module 与 header 的关系;umbrella header 则是 module_map 中的语法规范,表示指定目录中的头文件都应包含在模块中。

Modules

Clang Module

LLVM 中的 Module

  • Hmap / Xcode Header / CocoaPods Headers

Header Map 是一组头文件信息映射表,用 .hmap 后缀表示,整体结构以 Key-Value 形式存储;Key为头文件名称、Value 为 头文件物理地址。

Xcode Phases - Header 在构建配置中分为 public、private 与 project ,用以与 target 关联;其中 public 、private 就复制到最终产物的 header 和 PrivateHeaders 中,而 project 头文件不对外使用,则不会放到最终产物。

一款可以让大型iOS工程编译速度提升50%的工具

What are build phases?

  • Xcconfig:

一种配置文件,用以对构建设置进行声明与管理,比如区分不同的开发环境等。

Xcode Build Configuration Files

  • On demand resource:WWDC 2015 引入的概念,对资源文件的按需加载。

Introducing On Demand Resources

🔗:

[1] Cocoapods.org

[2] Xcode Workspace with multiple projects

[3] 深入理解 CocoaPods

[4] 系统理解 iOS 库与框架

[5] Cocoapods script phases

[6] CocoaPods Podfile 解析原理

[7] Semantic Versioning 2.0.0

[8] 一款可以让大型iOS工程编译速度提升50%的工具

[9] CocoaPods Source 管理机制

[10] 版本管理工具及 Ruby 工具链环境

[11] 整体把握 CocoaPods 核心组件

[12] 工程效率优化:CocoaPods优化

作者:nihao1011
链接:https://juejin.cn/post/7179231344147300412
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/349032.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LeetCode】剑指 Offer 06. 从尾到头打印链表 p58 -- Java Version

题目链接&#xff1a; https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/ 1. 题目介绍&#xff08;06. 从尾到头打印链表&#xff09; 输入一个链表的头节点&#xff0c;从尾到头反过来返回每个节点的值&#xff08;用数组返回&#xff09;。 【测试用例…

信息系统基本知识1

文章目录导论信息信息的定义:信息的特征信息分类系统定义&#xff1a;系统的组成:两种基本模式系统的特征信息系统定义信息系统的概念用户角度系统角度技术角度企业管理角度管理信息系统广义理解狭义理解信息系统工程定义信息系统的结构信息系统的功能信息系统的分类IS中人的作…

【并发编程】【3】Java线程 创建线程与线程运行

并发编程 3.Java线程 本章内容 创建和运行线程 查看线程 线程 API 线程状态 3.1 创建和运行线程 方法一&#xff0c;直接使用 Thread // 创建线程对象 Thread t new Thread() {public void run() {// 要执行的任务} }; // 启动线程 t.start();例如&#xff1a; // 构…

程序员必备的软技能-金字塔原理拆解

前言 日常工作中&#xff0c;常常因为思维、表达方式不对产生不想要的结果&#xff1a; 写了一个小时的周报&#xff0c;领导却不满意&#xff1f;跟团队讲了半天自己的想法&#xff0c;可别人就是没理解&#xff1f;看了很多知识、信息&#xff0c;却一点也没记住&#xff1…

【分享】如何通过集简云将ChatGPT人工智能接入到您的抖音中?

ChatGPT是一款非常强大的人工智能产品&#xff0c;可以有创造性的回复和创作文字&#xff0c;图片&#xff0c;适用于很多办公场景。这篇文章将介绍如何将ChatGPT接入到我们的抖音中。 在集简云中的ChatGPT应用 目前集简云提供了两个ChatGPT应用: OpenAI(ChatGPT&#xff09;…

OpenCV实战(11)——形态学变换详解

OpenCV实战&#xff08;11&#xff09;——形态学变换详解0. 前言1. 腐蚀和膨胀运算1.1 腐蚀和膨胀基础1.2 使用形态学滤波器执行图像腐蚀和膨胀运算2. 开运算和闭运算2.1 使用形态学滤波器执行图像开运算和闭运算3. 形态学变换应用3.1 使用形态学滤波器检测边缘3.2 使用形态学…

跨平台应用开发进阶(五十五):uni-app 实现内容分享

文章目录一、前言二、系统分享组件三、uniShare SDK调用四、拓展阅读一、前言 APP开发过程中&#xff0c;需要实现分享功能。 常用的分享实现方法包括&#xff1a; 系统分享组件&#xff1b;uniShare SDK调用&#xff1b; 二、系统分享组件 uni.shareWithSystem(OBJECT)调…

jvisualvm安装Visual GC插件以及连接远程应用监控jvm【杭州多测师_王sir】【杭州多测师】...

一)jvisualvm工具安装Visual GC插件 1、在本地jdk安装路径找到jvisualvm.exe双击打开 2、选择工具-插件-勾选visual GC 如果显示重试&#xff0c;先点击设置-编辑-选择你本地对应的JDK版本的URL&#xff1a;https://visualvm.github.io/pluginscenters.html 3、点击远程-添加远…

CSS系统学习总结

目录 CSS边框 CSS背景 CSS3渐变 线性渐变&#xff08;Linear Gradients&#xff09;- 向下/向上/向左/向右/对角方向 语法 线性渐变&#xff08;从上到下&#xff09; 线性渐变&#xff08;从左到右&#xff09; 线性渐变&#xff08;对角&#xff09; 使用角度 使用多…

博视像元获近5000万元融资,主攻半导体前道及锂电高端部件供应

这两年各大车企与电池厂商都在快速新建产能&#xff0c;尤其上游原材料成本大增&#xff0c;反映到产业链上巨头都在寻求增效&#xff0c;高端制造技术投入也大幅增长。比如这家&#xff0c;高端工业相机提供商「博视像元」近期宣布完成近5000万的天使加轮融资&#xff0c;投资…

指针——“C”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰学习的内容是指针&#xff0c;这次只会讲一些很简单的知识点&#xff0c;更详细的指针知识会在以后的博客中逐步剖析清楚&#xff0c;那么现在&#xff0c;就让我们进入指针的世界吧 指针是什么 指针和指针类型 野指…

Spring 如何解决循环依赖?

什么是循环依赖 &#xff1f; 一个或多个对象之间存在直接或间接的依赖关系&#xff0c;这种依赖关系构成一个环形调用&#xff0c;有下面 3 种方式。 我们看一个简单的 Demo&#xff0c;对标“情况 2”。 Service public class Louzai1 {Autowiredprivate Louzai2 louzai2;…

基于OpenAI搭建自己的ChatGPT环境1

基于OpenAI搭建自己的ChatGPT环境1基于OpenAI搭建自己的ChatGPT环境注册账号生成访问密钥创建虚拟环境安装openai模块环境体验笔者初次接触人工智能领域&#xff0c;文章中错误的地方还望各位大佬指正&#xff01; 基于OpenAI搭建自己的ChatGPT环境 ChatGPT是OpenAI研发的人机…

Java基础之网络编程介绍详尽笔记

目录初识网络编程网络传输模型网络传输协议UDPUDP通信程序UDP的三种通信方式TCPTCP通信协议TCP的三次握手TCP的四次挥手初识网络编程 网络编程三要素 IP 设备在网络中的地址&#xff0c;是唯一的标识。 端口号 应用程序在设备中唯一的标识。 协议 数据在网络中传输的规则&…

童年回忆--扫雷(包括标记功能和递归展开)--万字讲解让你学会扫雷制作

魔王的介绍&#xff1a;&#x1f636;‍&#x1f32b;️一名双非本科大一小白。魔王的目标&#xff1a;&#x1f92f;努力赶上周围卷王的脚步。魔王的主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王.&#x1f525;&#x1f525;&#x1f525; ❤️‍&#x1…

第九章:创建用户和用户权限

Windows&#xff1a;创建用户&#xff1a;第一种方法创建用户&#xff1a;先点右上角的工具&#xff0c;然后点击AD用户和计算机双击skills.com打开目录&#xff0c;再双击Users&#xff0c;进入文件夹中在右框中右击空白处&#xff0c;新建用户填充好用户信息后点击下一步然后…

Sophos防火墙日志管理

每天&#xff0c;Sophos防火墙都会生成大量的syslog数据&#xff0c;很难独自监控它们。借助EventLog Analyzer&#xff0c;您可以存档系统日志以满足合规性要求&#xff0c;并进行彻底的取证调查&#xff0c;以在发生任何问题&#xff08;例如网络入侵&#xff09;时获得宝贵的…

MySQL用户管理

文章目录MySQL用户管理用户用户信息创建用户修改用户密码删除用户数据库的权限MySQL中的权限给用户授权回收权限MySQL用户管理 与Linux操作系统类似&#xff0c;MySQL中也有超级用户和普通用户之分。如果一个用户只需要访问MySQL中的某一个数据库&#xff0c;甚至数据库中的某…

Unity 资源插件 Agents Navigation 3.1.1.unitypackage

Unity 插件 Agents Navigation 3.1.1.unitypackage 描述 这个软件包包括高性能、模块化和可扩展的代理导航。它是以 DOTS 为核心开发的&#xff0c;因此充分利用了 Unity 的最新技术栈&#xff0c;如 SIMD 数学、Jobs、Burst 编译器和 EntityComponentSystem。此外&#xff0c;…

【ASP.NET】原生JavaScript加Asp.net实现多图片上传

记录一下&#xff0c;Javascript加asp.net实现多文件上传的方法。首先看一下要实现的功能&#xff0c;图片比文字描述更直观。 一、前台代码 前台代码代码分为三个部分&#xff0c;一是HTML代码&#xff0c;二是Style样式代码&#xff0c;三是Javascript代码。 1.html代码 …