普通视图

发现新文章,点击刷新页面。
昨天以前戴铭的博客 - 星光社

2025 年我正在使用的 macOS 应用

作者 戴铭
2025年2月18日 12:56

这篇文章是10年前写的,如今使用的应用已经完全不同了。于是我更新了下:

  • 信息:
    • 新闻:Follow、微信、小红书
    • 翻译:沉浸式翻译、网易有道翻译
    • 播客:小宇宙
    • 视频:哔哩哔哩、YouTube、迅雷、DaVinci、IINA
    • 电子书:可达漫画、微信阅读、Calibre
    • 浏览器:Safari、Firefox
  • 创作
    • 知识管理:戴铭的小册子、Obsidian
    • 图片管理:Eagle、照片
    • AI:Ollama、AnythingLLM
    • 写作:Sublime、备忘录、便笺、Bear、MindNode
  • 开发
    • IDE:Xcode、VSCode、Cursor
    • Git:Fork、GitHub Desktop
    • FTP:Cyberduck、FileZilla

以下是我2015年整理的一份,可以感慨下时势变迁:

- 写作笔记:Bear、MWeb、Evernote、有道云笔记、Papers、有道词典、系统邮件、Reeder- 视觉:keynote、Photoshop、Pixave、Sip- 开发:Xcode、VSCode、AppCode、Sublime Text、Cornerstone、Transmit、iTerm、Dash、Reaveal、SourceTree、Graphviz- 通讯:钉钉、微信- 娱乐:网易云音乐、mpv、VLC、Thunder、The Unarchiver、百度网盘

我在 iOS Conf SG 25 的演讲

作者 戴铭
2025年1月21日 10:14

大会前,vina 跟我说 iOS Conf SG 的受众很大,她希望能够讲些可以让大家更加兴奋,可以在日常工作中应用的内容。因此,我也是专门写了些 Demo 和工具,共三个,123。那些难理解的内容我都去掉了。这次的画的图也是我花费时间最长的一次,学习了些时尚杂志的设计和布局。有些来不及调配色的图,我就参考媳妇买的巧克力包装配色。

下面是分享的内容。视频已放出点击查看

I’ll be talking about how to reduce app launch times.

I’ll first explain what app launch time is.

Then, I’ll cover how to collect launch time data using tools like Instruments, os_signpost, sysctl, MetricKit, and by hooking objc_msgSend and Swift functions.

I’ll also go over how to solve common performance issues.

Finally, we’ll dive into advanced ways to reduce launch times, with optimization strategies and code examples.

Let’s first understand launch time.

Launch time has two main parts: pre-main and post-main.

  • Pre-main happens before the main function. This is when the Mach-O file is loaded and dynamic libraries are read. To optimize here, we can reduce the size of the Mach-O file and cut down the number of dynamic libraries.

  • Post-main happens after the main function. This is when the UI is rendered and data is loaded until the app becomes interactive. Here, we can optimize task priority.

So, how can we measure the time spent during these stages?

We can use Xcode’s Instruments to analyze launch time.

The method is to use the App Launch template in Instruments, collect data for the first 20 seconds of the app launch, filter the data, and then analyze it.

Since the launch phase calls many system library methods, to get better results, it’s important to filter out system library data and track time usage per thread. Instruments can do this by setting up the Call Tree to filter system libraries and view data by thread.

Keep in mind that Instruments collects data through periodic sampling, so it may miss some details.

So, we need to do manual analysis. The benefit of this approach is that it lets us collect data automatically, gathering it daily.

It also allows us to customize time tracking, like measuring time at the function level, which gives us more detailed stats.

The methods for manual analysis include os_signpost and MetricKit.

Let’s first look at how to use os_signpost.

First, import os.signpost into your code. Then, where you want to track time, add start and end markers to log the duration.

Data collection with os_signpost is done through Xcode’s Profile feature, using the Instrument’s Logging template.

The limitation of os_signpost is that it can’t track pre-main timing. Another limitation is that it still relies on Instruments.

How do we solve these limitations?

To handle this, we can use the sysctl system interface to get pre-main timing.

And with MetricKit, we can gather launch time data without relying on Instruments.

Let’s talk about sysctl. sysctl provides an interface to fetch process information.

When a process is created, it initializes kernel data and records the creation time. This is the start time of the process.

To measure time with sysctl, we first get process info and then calculate the elapsed time.

We do this by setting up sysctl, creating an MIB array, and getting the p_starttime value from the kinfo_proc structure.

The p_starttime gives us the process start time. To get the elapsed time, we need the current time and then calculate the difference.

In the getProcessRunningTime function, we find the address offset for the current process’s PID in the process’s memory layout. This gives us detailed information about the current process, stored in kinfo.

We then get the current time when the function is called. By subtracting the process start time from the current time, we get the runtime since the process was created.

Now that we’ve solved the issue of not being able to track pre-main time, let’s move on to solving how to get this data without relying on Instruments.

To obtain the pre-main time, you need to first gather information about the process, extract the process creation time, and then calculate the app’s running time.

Now that we’ve solved the issue of not being able to track pre-main time, let’s move on to solving how to get this data without relying on Instruments.

To use MetricKit, you first create an MXMetricManager and add a subscriber to collect data.

Data is collected when the app enters the background or when the device is idle.

The data processing happens in MXMetricManagerSubscriber and supports batch processing.

You can view the collected data in Xcode’s Organizer, and it also supports custom analysis.

Manual analysis has many benefits, but it’s time-consuming, error-prone, and can lead to messy code. So, we need an automated solution.

The automated process involves using tools to parse the code, find method definitions, and insert timing logic. This saves development time and makes the code easier to maintain.

Tools available for this include source code analysis tools and build integration tools.

Next, I’ll cover some automated ways to measure time, including how to hook objc_msgSend to track the time of Objective-C function calls.

For Swift projects, I’ll also explain how to track the time of each Swift function.

Let’s first see how to track the time of Objective-C functions. Since all Objective-C functions are called through objc_msgSend, we can hook this method to track the time of all Objective-C functions.

The approach is to use fishhook to replace the objc_msgSend C function.

Since objc_msgSend is written in assembly, we also need to use assembly to do the method replacement.

In the replacement, we save the necessary registers before the method call and restore them afterward.

We track the time before and after the method call, save the time for each function, and generate a report.

You can view the full code at the link below.

Here is the code. In the smCallTraceStart function, we use fishhook‘s rebind_symbols to replace the method. The original objc_msgSend is saved as orig_objc_msgSend, and the hook logic is in hook_Objc_msgSend.

In the hook_Objc_msgSend method, we first save the method call parameters, then record the start time with before_objc_msgSend. After reading the parameters, we call the original objc_msgSend, save its return value, and calculate the function execution time.

Finally, we return the value from objc_msgSend and wrap everything in an interface for easy use.

After running it, you’ll see that the execution time of all functions is recorded.

The code summary is shown in the diagram. We first replace objc_msgSend and calculate function execution time in the replacement. Then, we save the data and generate a report.

This is the method we use in our company to check startup time.

This method only works for tracking the execution time of Objective-C functions. But what about Swift functions?

To track the runtime of Swift functions, I wrote a tool.

Simply drag your Swift project folder into the tool, click a button, and the tool will parse the Swift files in the project, find function definitions, and insert the time tracking code.

When your app runs, the tool starts collecting data, including function names, call counts, and execution times.

This is the tool’s interface. Just drag your project in. In the top right corner, there’s a button for time tracking. Click it, and it will insert the tracking code.

Once your project runs, the tool will sort the function’s execution time, showing the average time, call count, and total time for each function.

From what we’ve covered so far, we know how to identify where startup time is spent.

There are a few common issues that can impact launch time.

There are several common situations that can affect function execution time, as shown in the image.

The first one is expensive operations, like reading large files, making network requests, or querying the database.

The solution here is to move these operations to the background or break them into smaller tasks that run as needed.

The second issue is displaying large images. You can asynchronously load and decode large images using Swift Concurrency, or use more optimized formats to reduce I/O and memory usage.

The third issue is frequent UI updates. The solution is to use lazy loading to only update the UI visible on the screen, and use default placeholders for UI elements off-screen.

The last issue is DispatchSemaphore, which can block the main thread. The solution is to use Swift Concurrency’s Task Group, move the wait into async tasks, and free up the main thread.

The relevant examples and solutions are available in the link below.

In the demo app, I’ve included all the bad cases. The app’s launch time was huge, over 10 seconds.

After optimizing the code, the main thread finish time is down to just 1 second, and the async completion time is also much shorter.

You definitely want to download this demo and see the difference before and after optimization. The link is below on this page.

We’ve used tools to pinpoint startup issues, and now we’ve solved those costly problems.

But can we further reduce the startup time?

Next, I’ll introduce two more techniques that can reduce startup time even further: optimizing the launch path and merging libraries.

The principle of Launch Path optimization is that when an external launch is triggered, we bypass the home page’s reading and rendering, directly opening the target page.

The benefit of this approach is that it saves the overhead of reading and rendering the home page.

Next is the Mergeable Libraries optimization technique.

Traditionally, dynamic libraries were loaded one by one, processing symbols and then initializing each library.

With Mergeable Libraries, dynamic libraries are merged, removing redundant and duplicate symbols, and turning them into static libraries.

This is a new feature in Xcode that can be enabled through Build Settings.

In Build Settings, you can find the “Merged Binary” option and set it to “Automatic.”

At this point, we’ve identified the problems and understand how to address them. We also know how to further reduce startup time.

However, as the app evolves, the tasks that run during startup can become more complex and numerous.

We need a way to manage these tasks effectively, so we can control the system resource usage during startup and prevent the launch time from getting worse.

CPU and memory are limited resources.

If we don’t manage multithreading tasks properly, tasks can pile up at times, causing the CPU to switch between threads frequently, which wastes time.

When threads aren’t busy, the CPU isn’t fully utilized, causing delays and slowing down startup time.

The larger the codebase, the more obvious these issues become.

So, how can we better manage multithreading tasks and make full use of the CPU?

We divide tasks into high-priority and low-priority ones. High-priority tasks should run concurrently and can have dependencies managed.

Low-priority tasks can be delayed and run only when system resources are available.

I created two functions: executeTasksConcurrently and performLowPriorityTasks.

executeTasksConcurrently runs high-priority tasks concurrently using Swift Concurrency’s withTaskGroup, and the order of calling this function controls task dependencies.

performLowPriorityTasks runs low-priority tasks using Task.detached and sets the task’s priority to background.

Once we create three high-priority task groups, they will execute sequentially, and tasks within each group will run concurrently. Low-priority tasks will run when system resources are free.

So far, we’ve mostly covered Post-main optimizations. For Pre-main, we can optimize startup time by reducing the app size.

There are many ways to reduce app size, mainly through static analysis. Today, I’ll share how we can analyze at runtime to find unused code, expanding the scope of our optimizations.

Let me introduce a solution that can help identify which classes are not being used during runtime.

The process involves checking all classes when the app goes to the background and determining which ones have been initialized.

We use the objc_getClassList API to get a list of all classes, and NSClassFromString to find the metaclass of each class. The metaclass’s flag field, when shifted 29 bits, tells us if the class was initialized during runtime.

In the code, the metaClass struct’s data method returns a class_rw_t metaclass struct. The flag field is shifted left by 29 bits. A value of 0 means the class hasn’t been initialized, while 1 means it has.

In the initializedClassesInArray method, we use NSClassFromString to get class data, then call isInitialized to check if the class was initialized. We add initialized classes to an array, and the remaining classes are the ones not used during this app session.

Here, I’ve printed out all the initialized classes.

It’s also how we check for unused code in our company.

From the results of the analysis, this solution indeed detects a lot of unused code, especially older code.

However, there’s one issue. If a class contains many functions, as long as one of them is used, the entire class is considered “in use.”

So, we need to take it a step further and find even more unused code.

Do you remember the tool I created to collect Swift function data?

That tool can also collect data on all the functions in your app.

Every function your app calls during execution gets logged.

By subtracting the functions that are actually called from the total list, we can identify unused functions.

Click the button in the top-right corner of the tool, and it will show a list of all functions, with the ones that were executed marked.

We’ve gone over the built-in tools in Xcode for checking startup issues and how to create custom tools for automating the checks.

We also looked at some bad cases and discussed optimization techniques. To make every millisecond count, we shared more practical optimization tips.

I hope you found this helpful.

上面就是我分享的内容。另外这次主题是个大话题,还有很多相关知识可能需要花费更多时间学习,我也整理了些官方内容和一些工具。

很多嘉宾的博客我都订阅过,看过他们很多的分享。

这次也是 iOS Conf SG 大会的10周年。很多上次 KWDC 大会认识的韩国朋友也来了。思琦说这次上海的 Let’s Vision 25 也会有很多有意思的国内外嘉宾过来,真是非常期待。

全网最全的日本传统颜色指南,看完你会更懂日本

作者 戴铭
2024年11月4日 12:10

最近,我的颜气 APP 迎来了激动人心的升级,现在已正式发布 2.0 版本,审核也顺利通过,心情格外愉悦!在这一版本中,我新增了小组件功能,用户无需打开程序即可直接查看色彩。同时,小组件还可以显示重要节日和节气的提醒,增加天气信息及当日步数,完美地将日本传统色彩融入你的日常生活。

你可以通过这个地址下载颜气 APP,或者直接搜索我的名字“戴铭”来找到它。

开发2.0 Widget、WeatherKit 相关技术也都整理到了小册子中。

在开发这款 APP 的过程中,我查阅了大量资料,深入了解了日本传统颜色的丰富内涵。以前写过一篇文章,这次打算写的更详细些。接下来,我将与大家分享这些知识。我将从日本历史的角度出发,跨越绳纹时代、弥生时代、古坟时代、飞鸟时代、奈良时代、平安时代、镰仓时代、战国时代,一直到日本传统色彩的巅峰——江户时代,介绍这些时代中诞生的色彩及其意义与用途。同时,我会结合宗教、习俗、自然与人文的和谐共生,探讨这些色彩如何与日本人的生活息息相关。最后,我将通过日本文学艺术作品、美食、传统工艺、服饰及建筑中对色彩的运用,从全新的视角来了解日本的古代艺术。

以下是正文内容,希望你能够喜欢!

引言

在樱花盛开的季节,身着鲜艳和服的人们漫步在古色古香的街道上,那些色彩交织成一幅美丽的画卷。你是否好奇,为何日本的和服总是那么色彩斑斓?这些色彩背后又隐藏着怎样的文化寓意呢?

在日本,色彩不仅仅是视觉的享受,它们是故事的载体,是历史的见证者,更是文化灵魂的流露。当你漫步在京都的古老街道上,色彩静静地诉说着日本的过去与现在。颜色不仅仅是颜色,它是日本人对美的追求、对和谐生活的向往,以及与自然和谐共生的哲学思考。

每一抹色调都不仅是视觉的享受,更是文化的符号,是那份跨越千年的韵味与情怀,等待着我们去揭开它的秘密。准备好踏上一场探索那些隐藏在色彩背后的动人故事的色彩之旅,重新认识这个国度了吗?

绳纹时代(公元前14000年左右 – 公元前300年)

日本绳纹时代标志着日本从狩猎采集向早期农耕社会过渡的漫长时期。在这一时期,传统颜色的起源与发展显著反映了当时人们的生活方式、自然环境以及原始信仰。

在绳纹时代(约公元前14000年 – 公元前300年),日本人主要从自然界中获取颜色。 这一时期,颜色的提取和应用反映了当时人们的生活方式、自然环境以及原始信仰。当时,人们主要使用天然存在的物质来制造颜料,如红色的赤铁矿(氧化铁,日语称为“弁柄”,bengara)和黑色的锰矿石或炭黑。 这些颜料不仅用于陶器装饰,也可能用于身体彩绘或宗教仪式。

绳纹时代以其独特的陶器而闻名,“绳纹”即指陶器上常见的绳索压印图案。 这些陶器的颜色主要是烧制过程中泥土成分和温度不同自然形成的,多呈现黑褐色、红褐色、橘红色或土黄色等。 有些陶器表面还会涂上赤铁矿等颜料。

除了陶器,绳纹时代也出现了漆器工艺。 人们使用漆树的汁液,并常与红色颜料(如赤铁矿或朱砂)混合,制作红色的漆器,这些漆器被认为具有特殊的魔力或审美价值。 黑色的漆器也已出现。 考古发现包括红色的漆碗、梳子等。

关于纺织品染色,虽然直接证据较少,但有迹象表明绳纹人可能已经开始使用植物染料进行初步的染色尝试。 例如,有学者认为日本自绳纹时代起就开始利用天然染料。 当时衣物的材质主要是用植物纤维(如麻)编织而成的“アンギン”(angin)。

由于绳纹时代久远,且缺乏系统性的颜色命名体系,我们很难准确列出当时所有的颜色名称及其普遍认同的象征意义。不过,基于考古发现和后世的文化认知,我们可以推测一些基础颜色在当时可能具有的含义:

  • 赤(あか) - 红色:主要来源于赤铁矿(べんがら)和朱砂(辰砂)。 红色在绳纹文化中尤为重要,常用于陶器、漆器和墓葬中,可能象征生命力、火焰、血液或具有驱邪避凶的魔力。
  • 黒(くろ) - 黑色:来源于炭黑或锰矿石。 黑色与红色常一起出现在漆器上。
  • 白(しろ) - 白色:可能来源于白色粘土或贝壳粉末。在后世的日本文化中,白色常与神圣和纯洁相关联,这种观念的源头可能更为久远。
  • 其他自然色:如陶器本身的土黄色、褐色等。

总的来说,绳纹时代的色彩运用以红、黑两色以及陶土的自然本色为主,材料直接取自自然。 相较于后来的奈良、平安时代发展起来的复杂染色技术,绳纹时代的色彩提取和应用方式相对原始和直接。

弥生时代(公元前10世纪/公元前4世纪 – 公元3世纪中期)

弥生时代是日本历史上继绳纹时代之后的一个重要时期,其起始年代有多种说法,较早的说法可追溯至公元前10世纪,而一般通说则认为是公元前3、4世纪,结束于公元3世纪中期。 这一时期,水稻种植技术从亚洲大陆传入,带来了社会结构的深刻变革,日本从狩猎采集社会逐渐过渡到以稻作农业为基础的定居型社会。 青铜器和铁器的使用也标志着生产力的显著提升。

在色彩运用方面,弥生时代虽然尚未形成如后世般丰富和系统的色彩体系及染色技术,但人们依然从自然中获取灵感和材料。

根据考古发现和文献推测,当时可能认知或使用的颜色包括:

  • 与植物相关的颜色

    • 踯躅色(つつじいろ):即杜鹃花的颜色。虽然“踯躅色”这一名称的确立和广泛使用可能要到平安时代,但杜鹃花作为日本本土植物,其鲜艳的色彩在弥生时代的人们生活中是可以感知到的。
    • 桃色(ももいろ):来源于桃子或桃花的颜色。桃树在弥生时代已传入日本,其果实和花朵的颜色自然为人们所熟悉。 在后来的《万叶集》等早期文献中也有对桃花的记载,“桃染”一词也指染成桃花般的颜色。
    • 考古发现表明,弥生时代的人们可能已经开始利用身边的植物进行初步的染色尝试。 例如,吉野里遗址出土的织物上发现了使用日本茜(茜草,可染红色)和贝紫(可染紫色)染色的痕迹。 也有学者认为当时可能已经存在使用蓝草和红花进行染色的技术。 衣物的主要材料是麻和葛等植物纤维,其天然颜色多为米色、灰白色和浅褐色,反映了当时朴素的生活方式。
  • 陶器与矿物颜料

    • 弥生陶器是这一时期的重要代表,其颜色主要是通过烧制形成的,多为赤褐色、褐色或土黄色,少量呈黑色。 这种赤褐色调的陶器被认为与后来日本文化中对土色的偏好有一定渊源。
    • 考古发现显示,弥生时代会使用少量矿物颜料,如赤铁矿(弁柄,べんがら)产生的红色和木炭产生的黑色。 这些颜色可能用于祭祀装饰、绘画或涂在陶器上。 绳文时代对红色的重视在弥生时代依然延续,红色漆器依然存在,但到了弥生时代后期,黑漆逐渐成为主流,这可能反映了人们开始更注重器物的造型和功能,而非早期那种强烈的咒术意味。
  • 金属与自然环境色

    • 青铜器和铁器的引入带来了新的色彩元素。青铜器(如铜铎、铜镜)具有其独特的金属光泽和锈蚀后的颜色,而铁器的出现则带来了铁灰色。 虽然这不是染色,但这些金属色彩丰富了人们的视觉经验。
    • 由于稻作农业成为主要的生产方式,与水稻相关的颜色,如稻田的绿色、稻谷成熟时的金黄色以及稻草的淡黄色和干草的褐色,成为日常生活中常见的自然色系。
  • 珠饰与其他

    • 弥生时代的玉石饰品(如勾玉、管玉)以绿色为主,继承了绳文时代的偏好,主要材质有翡翠、碧玉和绿色凝灰岩。 弥生时代中期出现了青色的玻璃小玉,后期则有玻璃制勾玉和管玉。

弥生时代的色彩主要来源于自然材料的本色以及基础的加工。染色技术尚处于早期发展阶段,以植物染和矿物颜料为主。 与绳文时代相比,色彩的应用范围有所扩大,但仍以质朴的自然色调为主。

古坟时代(公元3世纪中期/后期 - 7世纪末左右)

日本古坟时代,其名称源于此期间遍布列岛的大型“古坟”(高地墓葬)。这一时期的色彩运用,受到了当时社会结构、原始信仰、自然环境以及与东亚大陆(尤其是中国和朝鲜半岛)文化交流的多重影响。权力逐渐集中,形成了更具影响力的首领阶层,这一点在古坟的规模和陪葬品中均有体现。

主要色彩表现

  • 埴轮(はにわ)与陶器

    • 古坟时代的代表性陶器是“埴轮”,这些素陶器多排列在古坟的顶部和四周,造型包括圆筒形以及人物、动物、房屋、器物等形象。 埴轮的色调通常是橙色、黄褐色、红褐色或土灰色,这些颜色主要来源于粘土本身的天然色泽以及烧制过程中的窑温和气氛控制。 大部分埴轮是中空的,通过泥条盘筑等方式制作。
    • 虽然大部分埴轮以素面为主,但也存在施加颜料的情况。例如,一些埴轮会涂以红色颜料(如赤铁矿)。 尤其在关东地区,出土过涂有多种颜色的造型埴轮。 埴轮的色彩和造型反映了古坟时代相对质朴但又富有表现力的美学风格。
  • 金属器的色彩

    • 随着与大陆的交流,更先进的金属加工技术被引入和发展,尤其是青铜器和铁器的制造。 这些金属器物本身具有自然的金属光泽。
    • 青铜器:如铜镜(仿制汉镜及和镜)、铜铃、铜箭头等。青铜器表面常因年代久远而带有青绿色的铜锈,这种色彩变化也成为古坟时代器物的一个特征。
    • 铁器:包括武器(刀剑、盔甲)、农具和工具等。铁器呈现出深灰或铁黑色,也易生锈变为红褐色。
    • 这些金属器具,特别是精美的铜镜和坚固的铁制武器,常作为权力与地位的象征,出现在大型古坟的随葬品中。
  • 珠饰与玉石

    • 古坟时代的装饰品中,珠饰是非常重要的一类,常见的材质有翡翠、碧玉、玛瑙、水晶、玻璃等。
    • 勾玉(まがたま)是一种独特的弯曲逗号形状的玉饰,是古坟时代极具代表性的饰品。 翡翠制成的绿色勾玉尤为珍贵,其产地有限(如新潟县糸鱼川地区),象征着富贵和权力。 除绿色外,还有红色(玛瑙等)、白色/透明(水晶)、蓝色(玻璃、部分碧玉)的勾玉。 红色勾玉可能与生命力、活力和血缘相关。
    • 管玉、小玉等其他形状的珠饰也常与勾玉组合成项链或腕饰。
    • 古坟时代中期以后,玻璃珠的制作技术从中原地区传入,出现了蓝色、黄绿色等多种颜色的玻璃玉。
  • 纺织品与染色

    • 古坟时代的纺织技术较弥生时代有所进步,衣物的主要材料仍然是麻、葛等植物纤维。 由于染色工艺相对复杂且成本较高,日常衣物可能仍以天然纤维的米色、浅褐色和灰白色为主。
    • 然而,考古发现表明,古坟时代已经开始使用一些天然染料进行染色。例如,墓葬中出土的少量染织品残片显示出红色(可能来自茜草、红花或辰砂等矿物颜料)、蓝色(可能来自蓼蓝或其他蓝草植物)的使用。
    • 有学者指出,蓼蓝(たであい)这种能够染出深邃蓝色的植物及其染色技术,可能在古坟时代后期经由丝绸之路或朝鲜半岛传入日本。 这种蓝色在后世被称为“日本蓝”,但此称谓的形成时间可能晚于古坟时代。
    • 随着与大陆的交流,特别是佛教文化的初步传入(约6世纪中叶),一些用于宗教仪式或高级阶层服饰的染料,如红花(べにばな)苏芳(すおう,一种可染红紫色系的苏木),可能也开始传入日本,丰富了红色系的色彩。 例如,苏芳染料在后来的奈良时代被视为珍贵物品。

  • 古坟壁画与石棺装饰

    • 一部分古坟的石室内发现了壁画,这些壁画是了解当时色彩运用、绘画题材和思想观念的重要资料。 早期古坟壁画颜色相对简单,以单色或红、黑等基础色描绘几何图案、人物、动物等。
    • 到了古坟时代后期,尤其是在受大陆文化影响较深的地区(如九州北部和畿内地区),壁画的色彩变得更加丰富,开始使用红、黄、绿、黑、白等多种颜色描绘更为复杂的图案,如骑马人物、船只、狩猎场景以及具有象征意义的圆形、三角形、菱形等纹饰。 这些颜色主要来自矿物颜料,如赤铁矿(红)、黄土(黄)、绿土(绿)、炭黑(黑)、白土(白)。
    • 大型古坟的石棺本身也可能利用石材的自然颜色和纹理,常见的有灰色、暗褐色和黄褐色。
  • 其他

    • 古坟时代还存在“黑齿”习俗,即用铁粉和五倍子等将牙齿染黑,这在一些埴轮的人物表现上也有所反映。


(图片说明:表现古坟时代武士形象的埴轮,其服饰和装备的细节为了解当时的风貌提供了参考。)

古坟时代的色彩运用在继承绳文、弥生时代的基础上,随着社会发展和对外交流的深入而有所进步。以埴轮的素朴色彩和土石的自然色为基调,同时在金属器、玉石饰品、壁画以及部分高级纺织品上,展现出对红、黑、绿、蓝等颜色的认知和运用。这一时期的色彩尚不具备后世那样系统化的等级象征意义,但已经开始与权力、信仰和区域文化特色产生联系。

飞鸟时代(592-710年)

飞鸟时代是日本历史上一个重要的转折期,此时日本开始积极吸收中国隋唐文化,佛教也正式传入并得到推广,社会、文化、艺术和工艺体系随之发生深刻变革。 这一时期,通过与大陆的交流,特别是遣隋使的派遣,先进的染色技术和色彩观念被引入日本,并逐渐形成了与社会等级相联系的色彩规范。

主要颜色及其象征意义

在飞鸟时代,日本的染色技术得到发展,一些特定的颜色因其染料的稀有性、制作工艺的复杂性以及文化象征意义而显得尤为重要。

颜色名称 日文名称 主要来源与染料 象征意义及用途
紫色 むらさき 紫草根(紫根) 高贵、权威,是皇室和最高等级官员的象征色。
茜色 あかねいろ 茜草根 象征生命力、活力,也用于佛教相关的装饰和高级服饰。
青色/靛蓝 あお 蓼蓝等蓝草植物 较为普及的颜色,也用于表示一定等级的官位,其深浅亦有区别。部分天然蓝染具有一定的防虫效果。
草色 みどりいろ 多种植物染料混合或特定植物(如刈安草) 代表自然、新生、平和,常用于装饰和一般服饰。
黄檗色 きはだいろ 黄檗树皮 具有一定地位的官员或僧侣服饰,也用于佛教相关物品。
朱色 しゅいろ 矿物朱砂(辰砂)或红土(如赤铁矿) 用于宫殿、神社佛寺建筑(如柱子和鸟居),具有神圣、辟邪的意义。
白色 しろ 未经染色的麻、丝等天然纤维的本色,或使用白土、贝壳粉 纯洁、神圣,常用于神道教和佛教仪式,也代表一定的官位等级。

随着佛教的传入和兴盛,飞鸟时代的色彩观念深受影响。 许多佛教造像、绘画、仪式用品和僧侣的袈裟常采用红色、金色(或黄色)、白色等,这些颜色在佛教中分别象征着生命、庄严、纯洁和智慧等。 紫色和一些深浓的颜色因其染料稀有、染色工艺复杂,成为贵族和高阶官员才能使用的颜色,体现了色彩的等级象征。 日常生活中,民众的衣物则更多采用易于获取的植物染料染出的蓝色、绿色和各种天然纤维的浅褐色、米色等。

冠位十二阶

“冠位十二阶”是公元603年由圣德太子制定的官位等级制度。 这一制度旨在打破传统的氏姓门阀限制,尝试根据个人的才能和功绩授予官位,是日本历史上一次重要的政治革新。 冠位十二阶的最大特色是通过官员所佩戴冠冕的颜色来明确区分其等级和地位。


(图片说明:描绘圣德太子及诸臣的绘画,冠冕的颜色示意了不同等级的官员。)

在“冠位十二阶”制度之前,日本的社会地位和官职主要由世袭的氏姓制度决定,这在一定程度上限制了人才的选拔和中央集权的加强。 圣德太子推行冠位十二阶,以儒家的“德、仁、礼、信、义、智”六种德目为基础,每种德目又分“大”和“小”两个等级,共计十二阶。 每一阶位对应一种特定颜色的冠帽。

以下是冠位十二阶的颜色与品级对应(根据《日本书纪》等史料记载):

冠位 颜色 德目 含义(根据德目推测)
大德 浓紫 最高等级,代表最高的道德与才能。
小德 薄紫 次于大德,代表较高的道德与才能。
大仁 浓青 代表仁爱与广博。
小仁 薄青 代表仁爱与广博。
大礼 浓赤 代表礼仪与规范。
小礼 薄赤 代表礼仪与规范。
大信 浓黄 代表诚信与真实。
小信 薄黄 代表诚信与真实。
大义 浓白 代表正义与公正。
小义 薄白 代表正义与公正。
大智 浓黑 代表智慧与知识。
小智 薄黑 代表智慧与知识。

冠位十二阶颜色的象征意义及背景

  • 紫色(むらさき):在当时是最高贵的颜色,其染料紫草根(紫根)产量稀少,染色工艺复杂,因此紫色冠位仅授予最高级别官员,象征至高无上的权威和尊荣。
  • 青色(あお):通常指蓝色系,有时也包含部分绿色调。在五行思想传入后,青色常与东方和春季相关联,象征生长与发展。
  • 红色(あか):象征太阳、火焰和生命力,是一种具有积极意义的颜色。
  • 黄色(きいろ):在五行思想中与中央土相关,也常与光明和丰饶联系。
  • 白色(しろ):象征纯洁、神圣和真实。
  • 黑色(くろ):在五行思想中与北方水相关,也具有庄重、肃穆的含义。

冠位十二阶的颜色规定,使得不同等级官员的服饰颜色有了明确区分,标志着“禁色”(きんじき)制度的雏形开始形成。 “禁色”是指特定阶层(尤其是皇室和高级贵族)专用的颜色,其他较低阶层的人禁止使用。 这种通过颜色来彰显身份等级的制度,对后世日本的服饰色彩规范产生了深远影响。

奈良时代(710–794年)

随着迁都平城京(今奈良),日本在奈良时代(710年-794年)开始建立起更为完善的朝廷体制,这一时期深受中国隋唐文化的深远影响。 日本的色彩文化在此时期得到进一步发展和丰富,传统颜色逐渐在社会生活中系统化。 朝廷对各阶层服饰的颜色做出了严格的规定,不同的颜色开始代表不同的身份和地位。 佛教在奈良时代发展迅速,深刻影响了诸多颜色的象征意义。

佛教文化的传播,极大地提升了色彩在宗教和艺术领域中的象征地位。 日本积极引进唐朝丰富的色彩文化,其中包括模仿唐三彩烧制出的“奈良三彩”,它以绿、白、褐为主要釉色,展现了与唐三彩既相似又具日本本土特色的色彩风格。

中国隋唐时期的文化对日本产生了深远影响。 在这个时期,中国的金、银等金属及其加工技术经朝鲜半岛传入日本,为日本的色彩世界增添了金属光泽。同时,中国的染料和染色技术也传入日本,例如红花、苏芳、紫草等染料,这些染料被用于制作各种颜色的衣物和装饰品。

奈良时代的颜色体系中,根据《衣服令》的规定,特定颜色与官位等级紧密相连,紫色、红色系(绯色/赤色)、绿色系、蓝色系(縹色)、黄色系以及白色和黑色成为社会各阶层和宗教仪式中的重要颜色。

主要颜色及其名称与象征意义

颜色名称 日文名称 来源 用途与象征意义
紫色 紫 (むらさき) 紫草根 至高无上的颜色,象征尊贵、皇权与最高德行,用于天皇、皇族及最高级别官员的服饰。
深紫色 濃紫 (こきむらさき/こむらさき) 紫草根 特别尊贵的紫色,多用于皇族及极少数高官,象征崇高地位。
赤色 赤 (あか) 茜草、红花 象征活力、生命力,也用于佛教仪式和部分官员服饰。
朱色 朱 (しゅ) 朱砂(硫化汞) 神圣、庄严,常用于神社、宫殿和佛寺建筑的涂装,具有驱邪避凶的意义。
黄色 黄 (き) 黄檗(きはだ)、栀子(くちなし)、刈安(かりやす)等 曾为天皇专用色(隋唐影响初期),后也用于特定身份官员服饰;佛教中象征神圣与智慧。
黄丹 黄丹 (おうに) 栀子与红花套染 皇太子礼服专用色,象征着太阳的光辉。
蓝色 青 (あお) 蓼蓝(たであい)等 曾为较高官阶服色,后也普及于普通服饰、日常生活,象征沉稳、洁净。
浅蓝 浅縹 (あさはなだ) 蓼蓝等 佛教装饰中象征平和与智慧,也用于官员服饰。
深蓝 深縹 (こきはなだ) 蓼蓝等 较为普遍的服饰颜色,象征坚韧、忠诚。
绿色 緑 (みどり) 刈安与蓝染套染、其他植物染料 自然、生命力、和平,常用于佛教艺术、装饰品及特定官阶服饰。
白色 白 (しろ) 未染色麻、丝,或用白土、胡粉等处理 象征纯洁、神圣,用于神道教及佛教仪式,天皇祭祀时的帛衣,也用于贵族服饰。
灰色 灰 (はいいろ) 橡木、墨染等,或为陶器自然烧成色 日常用品、僧侣服饰,体现朴素、寂静的美学。
深红/绯色 深緋 (こきひ/こきあけ) 茜草、苏芳等加深染色 高级官员服饰,象征地位与权力。
橙色 橙 (だいだいいろ) 柑橘类果皮、红花与黄檗套染等 日常生活和艺术装饰,佛教中有时象征火焰或光明。

详细说明:

紫色 (むらさき) 在奈良时代被视为最尊贵的颜色,根据《衣服令》,深紫和浅紫是亲王以及一位至三位官员的朝服颜色,象征着最高级别的权力和德行。 紫色染料主要从紫草 (むらさきそう) 的根中提取,这种染料稀少且染色工艺复杂,成本高昂,因此只有身份极高的人才能使用。 深紫色 (濃紫 - こきむらさき 或 こむらさき) 尤其尊贵,用于皇族和最高级别的官员。 地位崇高的僧侣有时也获赐紫色袈裟,以彰显其德行与地位。

红色系(赤、朱、绯)在奈良时代也扮演着重要角色。赤色 (あか) 通常由茜草 (あかね) 或红花 (べにばな) 提取,象征生命力与活力。 朱色 (しゅ) ,来源于矿物朱砂,因其鲜艳且具有不朽的特性,常用于宫殿、神社和佛寺等重要建筑的涂装,被认为具有驱邪避凶、彰显神圣庄严的作用。 绯色 (あけ/ひ) 则主要是指用茜草或苏芳 (すおう) 染出的红色,深绯 (こきひ/こきあけ) 是四位、五位官员的朝服颜色,象征权力和地位。

黄色系(黄、黄丹)在奈良时代具有特殊的象征意义。黄色 (き) 的染料来源有黄檗 (きはだ)、栀子 (くちなし)、刈安 (かりやす) 等。 在隋唐文化影响下,黄色一度是帝王的专用色,后来在日本的服色制度中,特定身份的官员也会使用黄色调的服饰。 佛教中,黄色袈裟是僧侣的常见服饰,象征出离心、智慧和神圣。 特别值得一提的是黄丹 (おうに) ,这是一种用栀子和红花套染出的偏红的黄色,被《衣服令》规定为皇太子礼服的专用颜色,象征着初升的太阳,寓意着储君的地位。

蓝色系(青、縹)是奈良时代常见的颜色。主要的蓝色染料是蓼蓝 (たであい)。 在《衣服令》中,縹色 (はなだいろ,一种明亮的蓝色) 分为深浅,是六位、七位官员的朝服颜色。 蓝色因其染制相对容易且具有一定的防虫效果,也广泛用于平民的日常服饰。 浅蓝色 (浅縹 - あさはなだ) 常用于佛教装饰,象征平和、清净与智慧。

绿色 (みどり) 在奈良时代象征自然、新生与和平。 其染料多为植物染料,如使用刈安等黄色染料与蓼蓝等蓝色染料进行套染得到。 根据《衣服令》,绿色也分为深浅,是介于绯色和縹色之间的官员服饰颜色。 绿色常用于佛教艺术的装饰、绘画以及一些饰品中,充满了自然的生机。

白色 (しろ) 在奈良时代象征纯洁、神圣与素朴。 它主要来源于未染色的麻或丝等天然纤维的本色,有时也使用白土等进行处理。 在神道教中,白色是神圣的颜色,常与神明和祭祀相关联,《古事记》等古籍中,神灵常以白色动物的形象出现,如白鹿、白鸟,被视为祥瑞。 天皇在一些重要的祭祀场合会穿着白色的“帛衣”。 佛教仪式和僧侣的服饰中也常使用白色,象征清净无垢。 同时,白色也是贵族日常服饰的颜色之一。

灰色 (はいいろ) 的染料来源可以是橡木等植物,或通过墨染得到,部分陶器烧制后也会呈现自然的灰色或灰褐色。 在奈良时代,灰色更多体现了朴素、沉静的审美意味,常见于日常用具以及部分僧侣的服饰。

橙色 (だいだいいろ) 的染料可以从柑橘类植物的果皮中提取,或通过红花与黄檗等黄色染料套染而成。 在奈良时代,橙色可见于日常生活用品和艺术装饰中。在佛教语境下,橙色或橘红色有时也与火焰相关联,象征光明或智慧。

通过对色彩的严格规范和广泛运用,奈良时代的日本社会不仅展现了其等级秩序,也反映了当时深受大陆文化影响并逐渐形成自身特色的文化面貌。

奈良时代的色彩不仅限于装饰用途,还被赋予了深刻的象征意义,不同的颜色代表不同的社会阶层和宗教象征。

  • 紫色:在奈良时代被视为最尊贵的颜色,象征皇族、贵族和高级官员的最高权力和德行。 佛教高僧也常穿着紫色袈裟,显示其地位的崇高。 紫色染料由紫草根制成,产量稀少且成本高昂。 当时还将金字抄写在紫色的纸上,例如《金光明最胜王经》,作为佛教镇护国家的象征。
  • 红色与朱色:红色象征活力与生命力,朱色则常用于神社、佛寺的建筑与装饰,象征神圣与庄严。 红色在佛教中也具有驱邪避恶的意义。
  • 黄色:在佛教文化中,黄色袈裟是僧侣的服饰,象征纯洁、智慧与神圣。 黄色和浅黄色也用于官员服饰,代表高贵和权威,例如黄丹是太子礼服的颜色。 [2. 12]
  • 蓝色:是奈良时代常见的颜色,靛蓝染料已广泛普及,多用于平民的日常服饰。 深蓝色象征坚韧与忠诚,浅蓝色也象征智慧与冷静,常用于佛教装饰品。
  • 绿色:象征自然、和平与生命力,主要用于佛教的装饰、雕塑和勾玉等饰品中。 其染料来源于艾蒿等天然植物。
  • 白色:象征纯洁和神圣,广泛应用于佛教仪式和宗教用品中。 贵族的日常服饰也使用白色的麻布和丝绸。 在日本古代,白色被认为是神圣纯洁之色,神灵常以白色动物形态出现。

延续下来的一些颜色名:

  • 青色(あお):青色可以指代天空和海洋的颜色,常用于描绘自然景观。在古代,青色有时也指更广泛的冷色调,包括绿色。
  • 茶色(ちゃいろ):茶色系在江户时代非常流行,有“四十八茶百鼠”之说,指茶色和鼠色(灰色)系的颜色种类繁多。 茶色是一种偏褐色,通常用于日常生活中的器皿和衣物。
  • 灰色(はいいろ):灰色在奈良时代的陶器和日常用品中有所体现,展现了朴素和低调的审美。 在佛教寺庙的建筑和装饰中,灰色象征朴素与平和。 鼠色(ねずみいろ)作为灰色的一种,在江户时代因幕府提倡节俭而流行。
  • 山吹色(やまぶきいろ):一种明亮的橘黄色,来源于棣棠花(山吹)的颜色。 自平安时代以来就很流行,象征着财富与繁荣,也常被用于文学作品中象征春天。 因其花色艳丽,也被称为“黄金色”。
  • 群青色(ぐんじょういろ):带有紫调的深蓝色,是日本画中描绘海洋、水流和天空的重要颜色。 其名称来源于日本传统岩石颜料“群青”,由蓝铜矿石研磨制成。 它的使用体现了日本人对自然美的感知和绘画中对色彩的精细运用。
  • 鼠色(ねずみいろ):取自老鼠毛皮的暗灰色。虽然在江户时代尤为流行,成为庶民阶层常用的颜色之一,但其作为颜色概念的起源可以追溯到更早的时期,平安时代称灰色系为“钝色”。 江户时代初期,为避讳与火灾相关的“灰”字,产生了“鼠色”的名称。
  • 茜色(あかねいろ):由茜草根提取的深红色,象征生命和活力。 在奈良时代,茜草是重要的红色染料来源,广泛用于贵族服饰。
  • 胡粉色(ごふんいろ):一种由牡蛎、扇贝等贝壳粉制成的白色颜料,带有柔和的光泽。象征纯洁,常用于绘画、佛教装饰和贵族家族的纹样。
  • 空五倍子色(うつぶしいろ):这种偏褐色的颜色来自五倍子(盐肤木上的虫癭)。 自平安时代起,常作为丧服的颜色,因此带有“凶色”的含义。

奈良时代的染色技术深受中国唐代文化的影响。 主要包括:

  • 绞染(しぼりぞめ):一种通过捆扎、缝合、折叠或用外力挤压布料,使其部分区域在染色时无法着色,从而形成各种花纹的传统防染技术。 日本的绞染历史可以追溯到8世纪的奈良时代。
  • 夹缬(きょうけち):也称夹染,是将织物对折夹在两块雕刻有对称镂空花纹的木板之间,浸入染缸进行染色,未被夹紧的部分着色,从而得到对称图案的技术。 这种技术在奈良时代与绞缬、蜡缬(臈缬)并称为“三缬”,均受到唐朝影响。

另外还有一些染色技术,比如腊接染,使用蜡液在布帛上描出图案,然后染色,蜡凝固后,未染色的部分显现出美丽的图案。纸型染,使用纸型进行印染,特别是在江户时代,这种技术被广泛用于武士的正式服装和商人的时装。

平安时代(794年-1185年)

平安时代(794年-1185年),宫廷生活以其华丽、优雅而著称,这一时期的色彩文化也得到了极大的发展和细化。 在贵族文化的鼎盛时期,色彩的运用达到了极致,贵族女性身着多层华美的服装(称为“十二单衣”),颜色丰富多变。 贵族阶层对色彩的运用极为考究,不同的颜色在宫廷礼仪、服饰、文学、艺术等方面都具有严格的象征意义和美学价值。

平安时代中期,藤原氏官员掌握国家实权,实行摄政统治,紫色因此成为统治阶级的象征色彩。 在此时期,社会上开始使用“禁色”(きんじき)和“许色”(ゆるしいろ)来区分社会等级。 禁色被严格限定给最高级别的官员使用,而许色则供普通人使用。

这个时期的色彩名称和分类方法逐渐成熟,并开始出现等级化的色彩系统:

  • 十二单衣(じゅうにひとえ):作为贵族女性的礼服,十二单衣的每一层都有特定的颜色搭配,这些搭配被称为“袭色目”(かさねのいろめ),用以表现季节、气候和节庆。 每个季节都有对应的配色方案,例如春季偏向淡粉和嫩绿(如“梅”或“樱”的配色),秋季则是红色与橙色的搭配(如“红叶袭”)。
  • 重色文化(かさねいろめぶんか):平安时代对颜色有严格的等级划分,许多颜色因其染料来源稀有而被赋予特权,成为地位的象征。 例如,“藤紫”(ふじむらさき)是一种由紫草根(しこん)染成的浅紫色,因“藤”字与当时的权贵藤原氏相关联,而被视为高贵色,不允许平民穿戴。

在这一时期的绘画作品如《源氏物语绘卷》中,许多场景和服饰的颜色描绘也反映了这一时期对色彩的高度重视和精细理解。

随着和风美学的逐步形成,平安时代的色彩系统在奈良时代的基础上得到了丰富和创新,许多传统颜色一直沿用至今。

平安时代的传统颜色体系非常庞大,有的颜色甚至根据季节、节气、时间而变换,并形成了独特的配色方案,即“襲の色目(かさねのいろめ)”(色层叠穿配色)。以下是平安时代常见的传统颜色及其用途和象征意义:

颜色名称 日文名称 来源 用途与象征意义
深紫色 濃紫 (こきむらさき / こむらさき) 紫草根 (しこん) 表示最高贵,皇族和最高级别贵族的专用色彩,象征权威与德行。
薄紫色 薄紫 (うすむらさき) 紫草根稀释 常见于贵族的春季服饰,柔和而优雅,象征柔美与品位。
红色 赤 (あか) 茜草 (あかね)、红花 (べにばな) 活力与激情,多用于贵族女子服饰及节庆场合。
薄红 薄紅 (うすくれない) 红花稀释 春天和樱花盛开时的颜色,象征青春、浪漫与喜悦。
朱色 朱 (しゅ) 朱砂 (辰砂) 宫廷建筑、器具装饰和祭祀仪式中常用,象征神圣、庄严与辟邪。
深红 紅 (くれない) 红花 贵族服饰,尤以女性为多,多见于秋冬季节,代表强烈的情感与华贵。
橙色 橙 (だいだいいろ) 柑橘类果皮染料、红花与栀子复合染 常见于秋季服饰,象征丰收、活力与温暖。
黄色 黄 (き) 栀子 (くちなし)、黄檗 (きはだ)、郁金/姜黄 (うこん) 等 高贵而神圣,曾为天皇专用(黄栌染),后也用于宫廷及僧侣服饰,尤其在夏季常见。
薄黄 浅黄色 (うすき) 栀子、黄檗等稀释 低调而优雅,用于贵族女性及年轻人的日常服饰,象征清新与柔和。
蓝色 青 (あお) 蓼蓝 (たであい) 典雅的颜色,象征智慧、沉静与高洁,亦见于贵族服饰及佛教僧侣服饰。
浅蓝 浅縹 (うすはなだ) 蓼蓝稀释 平和与纯净,夏季和春季服饰中常见,带来清凉感。
深蓝 濃縹 (こきはなだ) 蓼蓝 深沉与庄重,贵族服饰中的经典色彩,亦用于表现夜空或深水。
绿色 緑 (みどり) 刈安 (かりやす)、艾蒿 (よもぎ) 等植物染料 自然、生命的象征,主要在春夏季节服饰中使用,象征新生与活力。
翠绿色 萌黄 (もえぎ) 新芽的绿色,如刈安等植物染料 鲜嫩的黄绿色,表现初春的新生与希望,年轻人的服饰常用色。
白色 白 (しろ) 未染色麻、丝,或用白土、贝壳粉等处理 纯洁、神圣的象征,多见于神事、祭祀及贵族仪式服饰。
灰色 鈍色 (にびいろ) / 鼠色 (ねずみいろ) 橡子 (どんぐり)、墨 (すみ) 等染料,或指未染色材料的自然色泽 低调而朴素,用于僧侣服饰或丧服,也体现寂静、沉稳的审美。
藤色 藤色 (ふじいろ) 藤花 淡雅的紫色,高贵和神秘,贵族女性春夏季服饰常用,与藤原氏相关联。
青藤色 青藤 (あおふじ) 藤花,或指带青味的藤色 青紫色,象征春天和自然的优雅,常见于贵族女性的服饰。
桃色 桃色 (ももいろ) 苏木 (すおう)、红花等模拟桃花颜色 甜美与青春,多用于年轻女性和少女的春季服饰。
朽叶色 朽葉色 (くちばいろ) 模仿落叶的颜色,如栗皮、栀子等复合染 秋季服饰代表色,黄褐色、赤褐色等,象征成熟、寂寥与季节的变换。
海松色 海松色 (みるいろ) 海松藻 (みる) 的颜色,深橄榄绿 象征深海的宁静和深沉,也用于武家服饰及僧侣的袈裟色之一。
鴇羽色 鴇羽色 (ときはいろ) 朱鹮 (とき) 羽毛的颜色,浅粉红色带橙 高贵且稀有的颜色,与皇室相关,象征吉祥与雅致。
青磁色 青磁色 (せいじいろ) 模仿青瓷釉色 典雅、古朴的颜色,用于高阶贵族的装饰品和服饰,体现高雅品味。

紫色(尤其是深紫,こきむらさき)在平安时代被视为最高贵的颜色,象征权力和德行。紫色多用于天皇及高级贵族的服饰,特别是贵族女性的正式场合,显示身份的尊贵。紫色的淡化版“薄紫”则常见于春季,与藤花等意象结合,象征柔美与诗意。红色(赤)和深红色(紅)象征热情、活力和激情。平安贵族多在喜庆场合或特定季节(如秋季的红叶)穿着红色系的衣物,以彰显旺盛的生命力与华丽感。浅红色(薄紅)则是春季的代表色,多用于年轻女性,象征青春和浪漫。

黄色在平安时代象征神圣与高贵,一度为天皇专用色(如黄栌染 こうろぜん)。后来也广泛用于宫廷及高级僧侣的服饰。浅黄色则用在贵族女性的日常服饰中,显得温暖而清新。蓝色(青)在平安时代的色彩体系中,象征智慧和冷静,贵族服饰及佛教僧侣的袈裟中均有使用。浅蓝色(如浅縹)代表着春天的清新和夏日的平静,而深蓝色(如濃縹)则用于贵族男性的日常服饰或表现庄重沉稳的场合。绿色(緑)象征自然的生命力,尤其在春夏季节用于贵族的服饰中。鲜亮的“萌黄”色则常用于年轻人,充满生机。

白色在平安时代象征纯洁和神圣,多用于神事相关的服饰、贵族的正式礼服和宗教仪式的袍服,具有庄重的意义。灰色(如鈍色)是低调且沉稳的颜色,常用于僧侣的日常服饰或丧服,也体现朴素和寂静的和风美学。

藤色(藤色、青藤)是平安贵族非常喜爱的色彩,源自藤花,优雅而神秘,并与权势显赫的藤原氏相关联。春夏季的贵族女子常穿藤色服饰,象征贵族的独特气质。桃色象征青春和温柔,多见于年轻女性的春季服饰。朽叶色是秋季服饰中重要的颜色,模仿落叶的色彩,从黄褐色到赤褐色不等,象征成熟、丰收以及季节的变迁。海松色是深橄榄绿色,代表海的宁静与深邃,多用于僧侣的服饰或武家服饰。鴇羽色是一种稀有而高雅的浅粉红色,源于朱鹮的羽毛,常与高贵身份相关联。青磁色取自青瓷器物的釉色,色调古朴典雅,平安贵族中常用来作为装饰和服饰颜色,体现了高雅和内敛的美学追求。

平安时代的服饰配色讲究色彩的层叠,称为“襲の色目(かさねのいろめ)”,通常在同一套服饰上采用多种颜色的重叠,如“表”与“裏”的搭配,或多层服装的色彩组合。这些配色不仅考虑色彩本身的和谐,还需对应不同季节、节气,甚至根据贵族的身份而定。通过不同色层的组合,表达了季节的转换、自然的美以及宫廷的审美规范。以下是几种典型的“襲の色目”配色示例:

  1. 桜襲(さくらがさね - 春樱)

    • 色层组合:表:白色(白) / 裏:红色(赤)或苏芳(すおう - 一种暗红色)
    • 象征意义:春天樱花盛开的美景,外白内红,如同白樱透出红萼,清新柔美,表达青春与浪漫。
  2. 花菖蒲襲(はなしょうぶがさね - 夏菖蒲)

    • 色层组合:表:白 / 裏:青(蓝);或 表:青(蓝) / 裏:红紫(对应菖蒲花色)
    • 象征意义:夏季水边菖蒲花开的景象,表现出自然的清雅和季节感。
  3. 紅葉襲(もみじがさね - 秋枫)

    • 色层组合:表:红(くれない) / 裏:黄赤(黄丹おうに)或朽叶色(くちばいろ)
    • 象征意义:秋季枫叶由绿转黄再变红的层次感,象征成熟与季节的绚烂。
  4. 雪の下襲(ゆきのしたがさね - 冬雪下红梅)

    • 色层组合:表:白 / 裏:紅梅(こうばい - 红梅色)
    • 象征意义:表现冬雪之下红梅绽放的意境,白色象征纯洁的雪,红色象征寒冬中的生命力与高洁。
  5. 藤襲(ふじがさね - 藤层叠)

    • 色层组合:表:薄紫(うすむらさき) / 裏:青(あお - 通常指萌黄色或更深的绿色,代表藤叶)
    • 象征意义:藤花盛开,紫花与绿叶相映的美丽场景,是贵族女子春夏季喜爱的配色,清雅而高贵。
  6. 菊襲(きくがさね - 菊花层叠)

    • 色层组合:表:白 / 裏:黄;或 表:濃紫(こきむらさき) / 裏:青(緑系)等多种组合,对应不同品种菊花。
    • 象征意义:秋季盛开的菊花,象征高洁、长寿和吉祥,多用于秋季的贵族服饰。

日本平安时代 (794年-1185年),宫廷文化繁荣,人们生活相对安定,当时的贵族服饰颜色大多从四季的草木花卉、自然风光中汲取灵感,命名雅致,富有诗意。 衣物的色彩搭配非常讲究,不同颜色在宫廷礼仪、文学艺术中均有其特定的象征意义和审美规范。 以下是对该时代一些具有代表性的传统颜色的介绍:

  • **萌木色 (もえぎいろ)**:一种代表春日嫩芽的黄绿色,象征新生与活力,深受年轻人喜爱。 在平安时代的服饰配色中,萌黄色常与新绿之青组合,用于夏季服饰。 此外,萌木色也常用于年轻武士的铠甲,在镰仓时代的《平家物语》中就有相关描写。

  • **抚子色 (なでしこいろ)**:指石竹花那般娇艳的粉红色,常带有淡淡的紫色。 虽然抚子花期较长,且被列为秋季七草之一,但在平安时代的装束配色“袭(かさね)”中,“抚子袭”被视为夏季的色彩。 “大和抚子”一词也源于此花,用以赞美日本女性的纯洁美好,《古今和歌集》中便有将可爱女性比作“大和抚子”的和歌。 关于夏日祭女孩穿着抚子花浴衣、打“抚子结”的描述,虽然抚子花图案在浴衣中常见,象征优雅美丽,但浴衣作为日常穿着普及主要是在江户时代以后,平安时代的祭典穿着与后世有所不同。

  • **藤色 (ふじいろ)**:即紫藤花的颜色,是一种优雅的亮蓝而偏浅的紫色。 由于“藤”字与平安时代的权势贵族藤原氏相关联,藤色也被视为高贵的颜色。 淡雅柔和的藤色,能很好地衬托女性之美,常用于手帕、扇子等配饰,也是夏季贵族女性喜爱的服饰颜色。 在清少纳言的《枕草子》中,亦有记载用紫色和纸书写和歌,并系上藤花传递情意的浪漫场景。

  • **桔梗色 (ききょういろ)**:指桔梗花那般青中带紫的颜色,是平安时代以来就备受喜爱的传统色。 桔梗花是秋季七草之一。 在平安时代的文学作品如《源氏物語》、《枕草子》中均有提及,常作为秋季的代表花卉。 古时“朝颜”曾指桔梗花,与现代指牵牛花不同。 桔梗色予人浪漫神秘之感。

  • **梅色 (うめいろ)**:指梅花的颜色,尤指红梅色,是一种雅致的粉红色。 梅花被誉为“春告草”,象征高洁、坚韧。 平安时代的服饰配色中,春季常以梅、樱的印象为主,红梅色是高贵女性喜爱的外套颜色。

  • **莺色 (うぐいすいろ)**:如同日本树莺羽毛般的颜色,是一种暗哑的黄绿色。 需要注意的是,虽然“莺”是春鸟,但“莺色”作为一种流行色,其名称和普及主要是在江户时代。 江户时代流行饲养树莺,并盛行穿着茶色系的服饰,“莺茶”(更偏茶色的莺色)便是一种代表性的“四十八茶百鼠”(江户时代流行的茶色和鼠色系的泛称)之一。 因此,将莺色直接列为平安时代的代表色需谨慎,尽管描绘春季的色彩中可能有类似莺羽的色调。

  • **一斤染 (いっこんぞめ)**:一种极淡的、略带黄调的粉红色。 在平安时代,红花染料非常珍贵。 据载,染一匹浓红色的绢布需要耗费大量红花,因此浓艳的红色被定为“禁色”,非特定身份不得使用。 而“一斤染”仅需约一斤(日本古制,约600克)红花即可染出一匹绢布,属于“许色”(ゆるしいろ),即便是级别不高的官吏或平民也被允许使用。 这种浅淡的粉色温柔内敛,也被认为体现了穿着者的责任、关怀与善良。

这些美丽的传统色及其富有诗意的名称,不仅仅是对色彩的精准描绘,更承载了平安时代贵族文化的精致审美与对自然流转的细腻感知。 除此之外,平安时代的色彩体系还包括樱色、水色、露草色、堇色、踯躅色、女郎花色、山鸠色、雀茶和瓶覗等诸多典雅色彩。

镰仓时代(1185-1333年)

镰仓时代是日本由贵族统治向武士阶层统治过渡的重要时期,伴随着这一变化,色彩的使用和象征意义也发生了转变。与前一代平安时代的繁复和优雅不同,镰仓时代的颜色美学趋向于简约和稳重,以契合武士文化的务实精神。这一时期的色彩强调朴素、内敛和实用,受到禅宗思想的影响,展现出对简朴之美的崇尚。

镰仓时代(1185年-1333年)的色彩体系相较于平安时代的绚烂多彩,展现出更为质朴的特点,强调的是一种低调内敛的美感。这一时期,随着武士阶层的崛起,审美偏好也随之转变,深色和中性色开始占据主导地位。尤其是象征冷静与威严的深蓝色、代表力量与坚毅的铁灰色系、以及被赋予胜利含义的褐色(かちいろ)等,成为了武士阶层常用的色彩,这些颜色共同塑造了沉稳、勇气和力量的视觉意象,形成了独特的“武士之美”。

与此同时,禅宗在镰仓时代传入并对文化产生了深远影响。禅宗所崇尚的素雅、自然、不事雕琢的审美情趣,使得简约的色调成为新的流行趋势。许多僧侣的袍服以及武士的日常装束和铠甲,都体现出对深色系和自然色的偏爱。 禅宗的影响进一步强化了镰仓时代色彩文化中内敛、朴素和坚毅的特质,共同融入了武士阶层的生活与精神之中,逐渐形成了独树一帜的时代风格。

以下列举镰仓时代常见的颜色及其象征含义:

颜色名称 日文名称 来源 (主要传统染料) 用途与象征意义
深蓝色 濃紺 (こいこん) 蓝草 (蓼蓝) 武士服装和铠甲下摆的代表性颜色,象征冷静、威严和力量。
青色 青 (あお) 蓝草 (蓼蓝) 佛教僧侣常用色,也用于武士阶层,代表智慧和平静。
铁色 鉄色 (てついろ) 橡实、铁媒染 武士铠甲的常见色调,象征力量与坚毅。
灰色 灰色 (はいいろ) 墨、植物染料 低调和质朴的体现,受禅宗影响,僧侣服饰和日常装束常用。
褐色 褐色 (かちいろ) 蓝草 (蓼蓝) 深染 被认为是“胜色”(与“搗”同音,反复捶打染色使颜色加深),武士常用,象征胜利和吉祥。
茶色 茶色 (ちゃいろ) 茶叶、植物染料 朴素自然的象征,流行于各阶层的日常服饰和僧侣袍服。
薄茶色 薄茶 (うすちゃ) 茶叶、植物染料浅染 表现出低调的优雅,常见于日常便装。
深紫色 紫紺 (しこん) 紫草根 崇高和权威的象征,多用于高级武士、僧侣和贵族。
梅紫色 梅紫 (うめむらさき) 紫草根、苏木等 (调和出梅花般的红紫色) 武士的礼装或特定场合使用,象征忠诚和高雅。
枯草色 枯草色 (かれくさいろ) 模仿干草的植物染料 代表秋冬季节的颜色,象征坚韧和生命的循环。
黒色 黒 (くろ) 橡实、墨、植物染料 武士装束的经典色彩,象征庄严、力量和权威。
白色 白 (しろ) 未染色麻、丝 纯洁、神圣的象征,尤其在僧侣服饰和祭祀场合常见。
朱鷺色 朱鷺色 (ときいろ) 以日本朱鹭鸟羽毛命名的颜色,实际染色可能用红花等 温柔、高雅的浅粉橙色,可能用于女性或高雅场合的服饰点缀。
鼠色 鼠色 (ねずみいろ) 墨的淡染或植物染料 低调简约,象征平和和克制,江户时代更为流行,但镰仓亦有素朴之风。
焦茶色 焦茶色 (こげちゃ) 深染的茶叶或其它植物染料 表现出沉稳和厚重感,用于武士的日常装束。
金茶色 金茶色 (きんちゃ) 栀子与红花套染等 (模仿金色光泽的茶色) 带有光泽的茶色,象征富贵和吉祥,可能用于礼服或特殊场合。
羊羹色 羊羹色 (ようかんいろ) 以羊羹点心颜色命名的深红棕色,实际染色用相应植物染料 高级和质朴的结合,可见于武士的服饰或器物。

以下是一些在镰仓时代可能存在或开始形成的传统颜色名字:

  1. **萌木色 (もえぎいろ)**:鲜亮的黄绿色,如同春日新芽,象征新生与希望,自平安时代起即为流行色,镰仓时代依然沿用。
  2. **深蓝 (こいあい/のうこん)**:即浓绀,沉稳、内敛,常用于武士的服饰和铠甲缀线。
  3. **墨绿 (ぼくりょく/すみどり)**:深沉的绿色,常用于武士阶层的服饰或物品,体现沉静的力量。
  4. **茶色 (ちゃいろ)**:一种低调而自然的颜色,广泛用于各阶层,尤其是武士的日常服饰或家居装饰。
  5. **薄柿 (うすがき)**:淡雅的柿子橙色,可能用于和服等服饰,体现朴素审美。
  6. **朽叶色 (くちばいろ)**:秋季落叶的颜色(褐、黄、橙),富有季节感,符合镰仓时代崇尚自然的审美。
  7. **纳户色 (なんどいろ)**:略带灰调的蓝色或蓝绿色,作为一种实用且不张扬的颜色,可能在武士阶层中存在。“錆鉄御納戸”这样的复杂复合色名更常见于江户时代,但基础的纳户色符合镰仓的朴素风格。

战国时代(1467–1603年)

战国时代是日本历史上一个烽火连天、群雄割据的动荡时期。在这一背景下,武士阶层掌握了社会主导权,他们的实用主义与审美情趣深刻影响了当时的色彩文化。这一时期不仅是武士精神大放异彩的时代,也是日本传统色彩在实用与象征层面获得极大发展的阶段。战争与武家文化催生了更为丰富多样的色彩应用,颜色的象征意义与社会地位、军事策略及文化信仰紧密相连。

武士阶层的崛起,其独特的审美观推动了传统色彩的发展。武士的甲胄、旗印、阵羽织(战袍)等,广泛采用特定颜色以彰显其所属势力、勇武气概及家族荣耀:

  • 墨色(墨染 すみぞめ):通过墨染工艺获得的深灰至黑色,象征沉着、刚毅与力量,是武士常用的颜色之一,也体现了禅宗“空寂”美学的影响。
  • 赤色系(赤 Aka / 紅 Kurenai):包括绯色(緋色 ひいろ)茜色(茜色 あかねいろ)朱色(朱色 しゅいろ)等。深沉的红色常用于甲胄,象征勇猛无畏、克敌制胜。井伊家的“赤备”就是著名的例子。

在禅宗和武家简素枯淡审美的影响下,部分色彩运用趋向内敛与质朴,同时中国宋元水墨画的意境也对当时的审美产生影响。

以下列举战国时代常见的颜色及其象征含义:

颜色名称 日文名称 主要来源 用途与象征意义
浓红/深红 濃紅 (こいくれない) 茜草、红花等植物染料 代表勇武、斗志和权力,常见于武士的甲胄、旗帜,用以威慑敌人、鼓舞士气。
朱色 朱 (しゅ) / 朱色 (しゅいろ) 主要为辰砂(硫化汞)颜料,也指某些植物染出的亮红色 用于神社建筑、漆器,也用于武具装饰,象征驱邪、神圣、热情与胜利。
胜色/捣色 勝色 (かちいろ) 深い靛蓝染料 (Indigo) 因发音同“勝ち”,被武士视为吉祥色,象征胜利与刚健,广泛用于甲胄的缀绳、服装。
萌黄 萌黄 (もえぎ) 刈安(黄色)与靛蓝的复合染料,或直接从植物中提取 如新芽般的嫩绿色,象征新生、活力与希望,也用于年轻武士的服饰。
黑/玄 黒 (くろ) / 玄 (げん) 植物染料与铁媒染剂,或烟墨染 武士的甲胄、阵羽织和日常服饰常用色,象征力量、威严、神秘,亦有沉稳、不变之意。
浓紫/深紫 濃紫 (こいむらさき) 紫草根 (シコン) 染料 自古以来的高贵色,常用于身份较高的贵族和高级武士的服饰与用品,象征权威与雅致。
茶色系 茶色 (ちゃいろ) 橡实、栗壳、柿涩等植物染料 象征质朴、沉稳、内敛(侘寂美学),广泛用于僧侣、武士及平民的日常服装。
白 (しろ) 未经染色的麻、丝等天然纤维本色,或白色颜料(如胡粉) 象征神圣、纯洁、真实。在宗教仪式、特定礼仪场合使用,有时也与死亡和切腹关联。
空色 空色 (そらいろ) 浅淡的靛蓝染料 如晴空般的明亮蓝色,表现平和、广阔与清澄,用于衣物和工艺品。
缥色/花田色 縹 (はなだいろ) 中等浓度的靛蓝染料 一种清澈的蓝色,象征清爽、忠诚,常见于武士的便服和庶民的衣物。
藤色 藤色 (ふじいろ) 通常指紫草等植物染出的、类似藤花的淡雅紫色 象征优雅、高贵、沉静,多用于女性服饰、饰品,也为部分风雅武士所喜。
山吹色 山吹色 (やまぶきいろ) 栀子花、郁金等黄色系植物染料 如棣棠花般明亮的金黄色,象征财富、光明与活力,常用于礼装和装饰。
常磐色 常磐色 (ときわいろ) 指松柏等常绿树的深绿色,通过复合染色实现 象征不变、长寿、坚韧,是武家喜爱的吉祥色之一。
朽叶色 朽葉色 (くちばいろ) 模仿落叶的红黄色、黄褐色、赤褐色等一系列颜色,植物染料 体现了季节感和侘寂美学,象征自然、变迁与成熟,用于秋冬季服装。
灰色/鼠色 灰色 (はいいろ) / 鼠色 (ねずみいろ) 墨染的浅色,或特定植物染料与媒染剂 象征内省、朴素、克制,是僧侣、文人及部分武士喜爱的颜色,体现寂静、洗练的审美。
浅葱 浅葱 (あさぎ) 浅淡的靛蓝染料 清新明亮的浅蓝色,略带绿色调,象征年轻、活力,常见于年轻人的服饰及武士的里衬。
亜麻色 亜麻色 (あまいろ) 亚麻纤维的天然本色 象征自然、素朴、原始之美,常见于平民的衣物或作为底色。
赭 (しゃ) / 代赭色 (たいしゃいろ) 赭石等天然矿物颜料 源于土壤的红褐色,象征大地、稳固与坚韧,也用于绘画和部分器物。

在战国时代,色彩的运用极具战略意义。例如,武田信玄的军队以红色为基调(“武田赤备”),这不仅在战场上易于识别,也对敌军产生强烈的视觉冲击和心理威慑。

白色在历史上也具有重要意义。例如,在更早的源平合战中,源氏集团举白旗并最终获胜,这一传统使得白色在后来的武家文化中也象征着神圣、纯洁,有时也与胜利相关联。

深蓝或藏青色系,如“胜色(かちいろ)”,因其深沉且与“胜利”谐音,深受武士喜爱,常用于甲胄的缀线或服装。这种颜色在夜间或特定环境下也具有一定的隐蔽作用,可视为当时的一种实用选择。

在战国时代,颜色不仅仅是视觉的表现。各个大名使用特定颜色来标识其家族和地盘,使得颜色成为社会地位的象征。这一时期的颜色应用深刻影响了后世的和服设计、建筑装饰以及节日庆典等方面。

武士的服饰在战国时期尤为重要,不仅是个人风格的体现,也是地位和力量的象征。颜色的选择常常与家族的纹章(家纹)相结合,形成独特的视觉语言。例如,深红色和黑色的结合常用于战斗服饰,强调武士的勇敢与坚韧。

除了前述的主要颜色外,日本战国时代还运用了其他多种具有文化意涵的传统颜色。以下列举部分颜色及其在战国时代可能代表的意义或当时的认知情况:

(请注意:某些颜色的流行度和具体象征意义可能在不同时代有所侧重,战国时代的色彩更多与实用性、阶层以及特定大名的偏好相关。部分细腻的色彩名称和其广泛流行可能更集中于和平的江户时代,但在战国时期,这些颜色的基础染制技术和文化萌芽已经存在。)

颜色名称 日文名称 主要来源/描述 战国时代意义与联想(基于史料和文化推断)
黄金色/金色 黄金色 (こがねいろ) / 金色 (きんいろ) 金箔、金粉等金属颜料 象征财富、权力、权威和神圣。常用于高级武士的甲胄装饰、屏风、器物,以彰显地位与威严。
古代紫 古代紫 (こだいむらさき) 深浓的紫草染,带有红色调的紫色 作为紫色系的一种,继承了自古以来的高贵意涵。虽“古代紫”特指平安时代的特定浓紫,但深紫色在战国仍是身份的象征。
薄桜色 薄桜 (うすざくら) 模仿樱花颜色的极浅粉红色,可由少量红花或苏木染成 象征春日、短暂而绚烂的生命(与武士的生死观有暗合之处)、纯洁与雅致。多见于女性服饰或文学意象,武士文化中较少直接作为主色调。
枯茶色 枯茶 (からちゃ) 类似泡过的茶叶或枯叶的暗沉茶褐色,多种植物染料可得 体现“侘寂”美学,象征沉稳、内敛、朴素。可能用于僧侣、茶人或追求简素风格的武士的日常服饰。
青丹/暗绿 青丹 (あおに) / 暗緑 (あんりょく) 铜绿或植物染料(如蓝与黄的套染)形成的暗沉绿色 “青丹よし”是奈良的枕词,有古雅之意。暗绿色系在武具中可能出于实用(如伪装)或与其他颜色搭配,象征沉稳或与自然相关。
茄子紺 茄子紺 (なすこん) 如同茄子表皮的深紫蓝色,靛蓝与少量紫草或苏木混合 是一种深沉而富有光泽的颜色,兼具蓝色的稳重和紫色的高贵感。武士的服装或甲胄缀线可能采用此类深邃色彩。
鴇鼠 鴇鼠 (ときねずみ) 带有鴇羽(粉红)色调的灰色,属于鼠色系的一种 “鼠色”系在江户时代大为流行,代表素雅与粋(粹)。战国时期,灰色系更多体现朴素、隐忍,鴇鼠这种微妙的色调可能尚不普遍作为主流。
京紫 京紫 (きょうむらさき) 在京都染制的鲜明而纯正的紫色 作为高品质紫色的代表,与“江户紫”相对。象征高贵、雅致与文化中心(京都)的品味。高级武士和贵族所用。
土色/黄土色 土色 (つちいろ) / 黄土色 (おうどいろ) 天然的土黄色、黄褐色矿物颜料或相应植物染料 象征大地、质朴、稳固。可能用于一般士兵的服装(耐脏、易得),或建筑、陶器等。
芥子色 芥子色 (からしいろ) 如同芥末般的略带褐色的黄色,郁金、栀子等可染 象征明亮、温暖,但不如纯黄色鲜艳。是一种相对沉稳的暖色调,可见于服装或器物。
水浅葱 水浅葱 (みずあさぎ) 比浅葱色更浅、更清澈的淡蓝色,带有水样透明感 象征清澈、年轻、凉爽。是浅葱色的一种变体,用于夏季服装或表达清新感。
蒲萄色 蒲萄 (えびぞめ/えびいろ) 用山葡萄汁液或其皮与媒染剂染出的紫红色或红紫色 一种古老的染色,色调介于红与紫之间,带有野性与自然的韵味。其色调独特,象征成熟果实与自然的馈赠。

总的来说,战国时代的色彩运用一方面受到传统文化中颜色象征意义的影响(如紫色的高贵、白色的神圣),另一方面则更加注重实用性(如深色系的耐脏、特定颜色的战场识别)和阶层身份的体现。许多在后来江户时代大放异彩的细腻色彩,在战国时期可能已有雏形,但其大规模流行和文化意涵的丰富,则是在社会相对安定之后。

江户时代(1603年-1868年)

江户时代是日本色彩文化发展史上的一个黄金时期。在这一时期,武士阶层的审美情趣与新兴的町人(市民)文化相互交融,共同推动了色彩应用的空前繁荣。社会结构的转变,特别是商人及手工业者组成的町人阶层的崛起,使得色彩的运用不再局限于贵族阶层,而是广泛普及到平民百姓的日常生活中。这种普及在和服、浮世绘、陶瓷、漆器、室内装饰等领域表现得尤为突出,色彩的种类、命名以及搭配都达到了前所未有的细致与丰富。江户时代对颜色的精微细分和富有诗意的命名,在日本历史上独树一帜,色彩也成为了表达个性、社会地位、身份乃至细腻情感的重要媒介。由于幕府推行“奢侈禁止令”,限制了町人使用鲜艳色彩和高级面料,反而催生了诸如“四十八茶百鼠”(意指多种细致的茶色和鼠灰色)等低调而雅致的流行色系,体现了“粋(いき)”(精粹、潇洒)的独特审美意识。

以下列出江户时代一些具有代表性的传统颜色及其名称和应用:

颜色名称 日文名称 颜色描述 文化意义与应用
浅葱色 浅葱色 (あさぎいろ) 略带绿调的浅蓝色 年轻武士及町人喜爱的颜色,新撰组队服的颜色而闻名。象征清新、年轻与活力。
浅紫色 浅紫 (あさむらさき) 淡雅的紫色 女性服饰中常见的颜色,传递出温柔、雅致的气息。
藍色 藍 (あい) 靛蓝色,从深到浅有多种层次 日本最具代表性的传统色之一,因其亲民和实用性,被武士阶层和平民百姓都广泛使用,象征质朴、实用与沉稳。也被称为“日本蓝 (Japan Blue)”。
錆浅葱 錆浅葱 (さびあさぎ) 带灰调的暗浅葱色,如同生锈的金属光泽 “奢侈禁止令”下町人阶层中流行的“四十八茶百鼠”色系之一,带有寂静、成熟之美,符合“粋”的审美意识,常见于町人服饰。
鉛白 鉛白 (えんびゃく) 带有微黄的柔和白色 作为白色颜料,广泛用于绘画(尤其是浮世绘的美人画皮肤)和化妆品(白粉),是表现细腻肤色和绘制图案的基础色。
桔梗色 桔梗色 (ききょういろ) 如桔梗花般的蓝紫色 因其高雅的色调,常用于女性和服、小物及图案纹样中,尤其在秋季受到青睐,象征高雅、沉静。
群青色 群青色 (ぐんじょういろ) 深邃而鲜明的蓝色,源自矿物颜料群青 原本是贵重的绘画颜料,用于描绘天空、海洋等,也见于高级服饰中,象征高贵、深远与权威。
黄土色 黄土色 (おうどいろ) 温暖的赭黄色,如同天然的泥土色 广泛应用于陶器、染织品、建筑涂料等,象征自然、朴素与稳重。
朱色 朱色 (しゅいろ) 明亮鲜艳的橙红色 神社的鸟居、宫殿建筑、漆器以及庆典装饰中广泛使用,被认为有驱邪避凶的力量,象征神圣、权威、喜庆与活力。
紅梅色 紅梅色 (こうばいいろ) 如同红梅花初开时的娇嫩粉红色 象征初春、喜悦与娇美,常用于年轻女性和儿童的和服及饰品,也常出现在描绘春日景色的绘画中。
蘇芳色 蘇芳色 (すおういろ) 从苏木中提取的带有紫调的深红色或红褐色 自古以来就是高贵的染料颜色,平安时代起就为贵族所用,江户时代亦用于高级和服及织物,象征典雅、成熟与尊贵。
墨色 墨色 (すみいろ) 如同墨汁般的深黑色,有不同浓淡层次 武士的礼服(如黑纹付羽织袴)和僧侣的法衣常用此色,也为町人富裕阶层所喜爱,象征权威、端庄、沉稳,有时也与“粋”相关联。
浅藍 浅藍 (あさあい) 比藍色更浅一度的蓝色 日常和服及工作服中常见的颜色,给人清新、自然的印象,是蓝染中较易获得的颜色之一。
桃色 桃色 (ももいろ) 如同桃花般娇嫩明亮的粉红色 象征春天、年轻、幸福与女性的柔美,广泛用于女性及儿童的和服、节庆饰品以及描绘春季景物的艺术作品。
若草色 若草色 (わかくさいろ) 如同春天嫩草般的鲜亮黄绿色 充满生机的颜色,象征新生、希望与青春,常用于春季的和服、小物以及描绘自然风景的绘画中。
鳶色 鳶色 (とびいろ) 如同鸢鸟羽毛的红褐色或深棕色 江户时代庶民喜爱的茶色系(“四十八茶”之一),朴素而具有成熟韵味,广泛用于日常和服、外套(羽织)等。
利休鼠 利休鼠 (りきゅうねずみ) 带绿调的雅致灰色,与茶人千利休相关 茶道文化中备受推崇的颜色,体现了“侘寂(wabi-sabi)”美学,象征静谧、简素、内省与洗练。属于“百鼠”之一。
山吹色 山吹色 (やまぶきいろ) 如同山吹花(棣棠花)般鲜艳的金黄色或橙黄色 明亮而富有活力的颜色,常用于庆典服饰、儿童服装及工艺品中,象征财富、喜悦与繁荣。
空色 空色 (そらいろ) 清澈天空般的浅蓝色 象征晴朗、开阔与自由,常见于夏季和服(浴衣)、手帕以及浮世绘中天空的描绘。
白練 白練 (しろねり) 经过精炼、带有光泽的纯白色丝织品颜色 象征纯洁、神圣与高贵,多用于婚礼服(白无垢)、神道祭祀的服装以及高级和服的内衬。
深川鼠 深川鼠 (ふかがわねずみ) 略带青(或绿)味的时尚灰色 江户后期在深川地区(花街)的艺妓和粋人中流行的“鼠色”之一,代表低调的奢华与都会的时尚感。
銀鼠 銀鼠 (ぎんねずみ) 如同银般光泽的浅灰色 “百鼠”中具有代表性的高级灰色,给人以高雅、冷静、洗练的印象,常用于和服及配饰,深受文人雅士喜爱。
櫻色 櫻色 (さくらいろ) 如同染井吉野樱花瓣的极淡粉红色 日本最具代表性的花卉颜色,象征春天的到来、短暂而绚烂的美、以及纯洁的恋情。广泛用于春季和服、小物及各类设计。
鶸色 鶸色 (ひわいろ) 如同鶸鸟羽毛的明亮黄绿色 鲜嫩而富有春天气息的颜色,象征新绿、活力与希望,常见于春季的和服和儿童服饰。
江戸紫 江戸紫 (えどむらさき) 略带红(或蓝)调的明亮紫色 与京都的“京紫”(偏蓝调)相对,是江户町人文化的代表色之一。由当时著名的歌舞伎演员市川團十郎喜爱并推广,象征“粋”、高雅和都会的洗练感。
青磁色 青磁色 (せいじいろ) 如同青瓷器物釉色的淡雅蓝绿色 源自中国青瓷的颜色,给人以清凉、温润、宁静之感,常用于陶瓷器、夏季和服及室内装饰,象征优雅与平和。
黄緑 黄緑 (きみどり) 介于黄色与绿色之间的明快颜色 象征新芽、生机与活力,广泛用于表现春夏季节的服饰、儿童用品以及自然主题的图案设计中。
枯茶 枯茶 (からちゃ) 如同干枯茶叶的暗沉棕色或红褐色 “四十八茶”之一,带有秋冬的寂寥、成熟的韵味,常用于和服、茶道具及生活器物中,体现“侘寂”美学。

在江户时代,和服的颜色是身份、年龄、场合乃至季节感的重要体现。例如,武士阶层偏好沉稳的深色系如藍色、墨色,而町人则在“奢侈禁止令”的限制下,发展出诸如“四十八茶百鼠”这类内敛而富于变化的色彩体系,将朴素的色彩演绎出极致的雅致与“粋”的风格。江户紫、深川鼠等颜色的流行,正是江户町人文化成熟与自信的体现。

浮世绘作为江户时代最具代表性的艺术形式之一,其鲜明而丰富的色彩运用,生动地再现了当时的社会风俗与审美情趣。画家们大胆运用朱色、藍色、浅葱色、墨色以及各种间色,不仅捕捉了江户的繁华景象与人物风貌,其独特的配色方案和构图技巧也对后世乃至西方的艺术与设计产生了深远的影响。

江户时代的色彩文化,在不同层面展现出独特的审美意趣。茶道作为重要的文化代表,其色彩运用深受“侘寂”(わびさび)美学的影响。 诸多如利休鼠(りきゅうねず)、枯茶(からちゃ)和银鼠(ぎんねず)等色彩,均呈现出淡雅、朴素的质感。这些颜色传递出茶道中所追求的“简素”与“宁静”的精神境界,并体现了对自然的尊重和内敛之美。

与此同时,江户时代的市民文化也将丰富多彩的颜色融入节庆、婚礼等生活场景。 鲜艳明快的山吹色(やまぶきいろ)和若草色(わかくさいろ)等色彩,常常被用来象征生命的活力和喜庆的氛围。

在江户时代,茶色系(茶色系 - ちゃいろけい)的流行尤为引人注目,这是一组富有深厚文化内涵和自然质朴美感的颜色。 这些颜色深受“侘寂”美学的影响,崇尚自然、简约,强调在朴素和低调中发现美的真谛。 茶色系的广泛应用,与江户时期市民阶层的崛起息息相关。 当时幕府颁布的奢侈禁令,限制了过于华丽色彩的使用,这使得茶色系等低饱和度的色调备受推崇,成为平民和武士日常穿着及器物装饰的主要选择。 禁奢令的颁布,促使百姓的服饰颜色趋向朴素,但也催生了新的颜色名称和配色方法。 尽管如此,紫色(紫色 - むらさきいろ)在当时仍然是与贵族阶层相关联的颜色。 江户时代的人们更加注重色彩的搭配与调和,这使得日本的传统色彩体系愈发丰富和多样。 值得注意的是,尽管文中提及江户时代开始使用合成染料,但根据现有资料,江户时代的染色技术主要仍以天然染料和传统技法为主,如蓝染、友禅染等。 合成染料的广泛应用,实际上是在明治维新之后。

“茶色”,顾名思义,其色彩概念与茶紧密相连。茶文化源远流长,始于中国,兴于唐宋,并对周边国家产生了深远影响。“茶色”作为一类特定的色彩名称及其丰富的色系,在日本得到了显著的发展和普及,尤其在江户时代因其独特的审美和文化内涵而成为社会风尚。在日本,“茶色”指称一系列带有温暖、柔和情调的褐色系,从浅淡至浓郁,变化万千。

江户时代的茶色并非单一的棕褐色,而是衍生出所谓“四十八茶百鼠”的说法,意指茶色系与鼠色系(灰色系)拥有极为丰富细腻的色调变化。由于这些色彩质朴、低调,一方面符合当时“粹”(いき)的审美意识和伦理观念,另一方面也与幕府推行的奢侈禁止令有关(该法令限制了衣着等方面的奢华),促使民众转向内敛含蓄的色彩表达,茶色系因此逐渐成为社会各阶层广泛接受和喜爱的颜色。它们通常出现在和服、漆器、陶瓷和家居装饰等方面,象征着自然、稳重、质朴与内敛的气质。

以下列出江户时代常见的茶色系颜色及其名称、描述与象征意义:

颜色名称 日文名称 颜色描述 文化意义与应用
茶色 茶色 (ちゃいろ) 标准的棕褐色 江户时代最具代表性的庶民色彩之一,象征日常的朴素与踏实。
薄茶 薄茶 (うすちゃ) 浅棕色,比茶色更淡 平民和武士的休闲服饰色,亦用于茶道中的薄茶,象征自然与沉稳。
枯茶 枯茶 (からちゃ) 深沉的棕褐色,如同干枯的茶叶 体现茶道“侘寂”美学中“寂”(Sabi)的意境,象征自然的枯淡与深邃,常见于茶人所好。
黄枯茶 黄枯茶 (きがらちゃ) 带黄味的枯茶色,即略带黄色的深棕色 常见于平民服饰和日常用具,传递出朴素而不失温暖的感觉。
利休茶 利休茶 (りきゅうちゃ) 带柔和绿味或橄榄绿调的茶色 相传为茶道大师千利休所钟爱的颜色,传递出侘茶(わびちゃ)的静谧与深远意境,常见于茶道具及相关服饰。
利休鼠 利休鼠 (りきゅうねず) 略带绿味的暗灰色,或带灰的橄榄绿色 深受千利休审美影响的色彩,常用于茶道相关的服饰与器具,象征简素、朴拙,带有内敛寂静的审美情趣。
赤茶 赤茶 (あかちゃ) 带红味的深棕色 在男性,特别是武士的服饰中较为常见,象征强健和稳重。
胡桃色 胡桃色 (くるみいろ) 如同胡桃壳般的红棕色或深棕色 多用于染织品、漆器和家居装饰,色调沉稳,象征自然、传统与实用之美。
栗色 栗色 (くりいろ) 如同栗子壳或果实般的红棕色 常用于秋冬季的服饰和装饰,与自然的成熟与丰硕相关联,象征丰收与沉静。
煤竹色 煤竹 (すすたけいろ) 经烟熏的竹子所呈现的带黑的深棕色或红棕色 因其沉稳坚实的色调,常见于武士服饰、甲胄及日常器具,象征力量、坚韧与历经时间的美感。
木蘭色 木蘭色 (もくらんいろ) 带微红或微黄的浅棕色,柔和的淡褐色 在家居装饰、和服及其配饰中常见,给人以温暖、舒适且雅致的感觉。
黄茶 黄茶 (きちゃ) 明显的带黄调的棕色 常用于秋季节庆的服饰或装饰中,色感温暖明快,象征丰饶、喜悦与和谐。
鳶色 鳶色 (とびいろ) 如同鸢鸟羽毛般的红黑色或暗红棕色 江户时代男性间流行的“いき”(粹)的代表色之一,常见于消防员、工匠和侠客的服饰,象征洒脱、强韧与干练。
柿色 柿色 (かきいろ) 如同成熟柿子般的橙红色或红棕色 鲜明而温暖的色彩,在和服及带物(腰带)中常用,尤其能衬托女性之美,象征丰收、温暖、幸福与活力。
栗皮茶 栗皮茶 (くりかわちゃ) 接近栗子皮的深沉的红棕色或黑棕色 多见于武士阶层及男性的服饰,传递出稳重、质实有力的印象。
土器色 土器色 (かわらけいろ) 如同素烧陶器般的带土黄色调的茶色 质朴的陶器本色,象征着与土地的亲近、自然的本真与不加修饰的融合感。
黄櫨染 黄櫨染 (こうろぜん) 如旭日初升般的略带红味的明亮黄褐色 自平安时代起即为日本天皇的专用禁色(绝对禁色),用于天皇的礼服,象征最高权威、神圣与尊严。

茶色系与日本茶道文化有着密不可分的深厚联系。在茶道中,核心的“侘寂”(わびさび)美学推崇朴素、自然、非永恒及不完美的美感,茶色系的低调、沉稳与谦逊的特质恰恰完美地契合了这种审美追求。诸如利休茶、枯茶等特定的茶色调,不仅常见于茶室的壁土、障子、挂轴等空间装饰,也广泛应用于茶碗、水指、茶入等茶道具的设计制作中。这些色彩的选择,旨在引导人们在宁静、内敛的氛围中品味生活,感受超越外在华美的简朴之美与深邃意境。可以说,茶色在茶道仪式及相关美学构建中扮演着至关重要的角色,是体现茶道精神的视觉语言之一。

江户时代的茶色系颜色是平民百姓和低阶层武士在服饰上的主要选择之一。这与当时的社会背景和审美观念密切相关。

  • 奢侈禁止令与节俭美学:为了抑制社会奢侈风气,幕府多次颁布“奢侈禁止令”(贅沢禁止令),限制了衣物的材质、颜色和纹样。 这使得染制成本相对较低、色调不事张扬的茶色系(包括棕色、褐色、卡其色等一系列深浅不一的茶色调)成为了民众在服饰上的合规且经济的选择。 这种选择也契合了当时推崇的“侘寂”(わびさび)等强调朴素、自然的审美意识。
  • 自然亲和与市民生活:茶色系色调多源自自然界的泥土、树皮、枯叶、以及茶叶本身等,这些颜色给人以温暖、沉稳、亲切的感觉,与江户时期市民的生活方式和对自然的亲近感相契合。它们不像亮丽的色彩那样引人注目,但却散发出一种内敛的质感和舒适的氛围。
  • 歌舞伎演员的时尚引领:江户中期,歌舞伎演员作为当时的时尚引领者,对茶色系的流行起到了推波助澜的作用。例如,著名的歌舞伎演员市川团十郎家族偏爱柿色(一种茶色系),被称为“团十郎茶”,深受民众喜爱和模仿。 这些颜色不仅用于舞台表演,也成为普通民众日常穿着的流行色。
  • 家居装饰中的温暖格调:茶色系的温暖与质朴感也使其在家居装饰中备受欢迎。胡桃色、煤竹色等常见于家具、屏风、障子等物件,既能营造出沉静舒适的氛围,又带有自然的质感。

鼠色系,即各种不同色调的灰色,是江户时代与茶色系并驾齐驱的另一大流行色系。它并非单一的灰色,而是通过在灰色中微妙地混入其他颜色(如蓝、绿、紫、茶等)而形成的丰富多彩的色系,被称为“四十八茶百鼠”,意指茶色系有四十八种之多,而鼠色系更是有上百种变化。

  • 低调中的高雅:鼠色系具有宁静、稳重、内敛的特点,同时又不失细腻和雅致,能够营造出一种低调的高级感。在奢侈禁止令的背景下,人们开始在有限的色彩范围内追求细微的色彩变化和高级的审美趣味,鼠色系正满足了这种需求。
  • 丰富的色彩层次:鼠色系颜色的微妙之处在于其丰富的层次感。通过调整灰色的深浅以及所混合色彩的比例,可以创造出无穷无尽的灰色调,每一种都有其独特的韵味。

以下是一些典型的鼠色系颜色及其描述:

颜色名称 日文名称 颜色描述 文化意义与应用
鼠色 (ねずみいろ) 鼠色 (ねずみいろ) 标准的灰色 最基础和经典的鼠色,沉稳而不失雅致,广泛应用于各类和服及日常用品。
利休鼠 (りきゅうねず) 利休鼠 (りきゅうねず) 带些微绿调的灰色,或指略带茶色的灰色 据传为茶道大师千利休所喜爱的颜色,带有寂静、朴素的禅意,常用于和服、茶具及相关场合。
青鼠 (あおねず) 青鼠 (あおねず) 略带蓝色的灰色 给人以清爽、沉静之感,在男性和服中较为常见,也用于表现冷静、知性的气质。
鉛鼠 (なまりねず) 鉛鼠 (えんそ) / 鉛色 (なまりいろ) 接近铅的暗灰色,带有金属质感 深沉且具有重量感的灰色,常用于武士阶层或需要表现庄重感的服饰。
紅鼠 (べにねず) 紅鼠 (べにねず) 略带红紫色调的柔和灰色 在灰色中透出淡淡的红晕,显得柔和而雅致,常用于女性和服及小物,增添一丝温馨与妩媚。
葡萄鼠 (ぶどうねず) 葡萄鼠 (ぶどうねず) 带有葡萄紫调的灰色 混合了紫色的灰色,显得高雅而略带神秘感,适合较为正式或需要展现成熟韵味的场合。
藍鼠 (あいねず) 藍鼠 (あいねず) 略带蓝靛色的灰色 与“青鼠”接近但蓝色调更深沉,体现出冷静与知性,广泛用于男女和服。
錆鼠 (さびねず) 錆鼠 (さびねず) 带有些微铁锈红褐色的暗灰色 模仿铁锈的颜色,具有独特的岁月感和朴拙之美,常见于展现沉稳、内敛风格的服饰和工艺品中。
墨鼠 (すみねず) 墨鼠 (すみねず) 接近墨色的深灰色 非常深的灰色,接近黑色,给人以端庄、肃穆之感,常用于正式场合的服饰或表现力量感。
胡桃染 (くるみぞめ) / 胡桃皮色 (くるみかわいろ) 胡桃染 (くるみぞめ) / 胡桃皮色 (くるみかわいろ) 用胡桃树皮或果实染出的带黄褐色或红褐色的茶色系 虽然字面有“胡桃”,但更偏向茶色系,具有天然染料的质朴感,常用于日常和服及家居织物。 您的“胡桃鼠”更可能是指偏灰的胡桃色。
桔梗鼠 (ききょうねず) 桔梗鼠 (ききょうねず) 略带桔梗花紫色的灰色 带有优雅的紫色调,显得高贵而富有风情,常用于女性和服及高级织物。

江户时代的茶色与鼠色不仅仅是单纯的颜色选择,它们承载了当时的社会规范、审美情趣和文化心理,是理解江户时代庶民生活与文化的重要窗口。

鼠色系的低饱和度与江户时代的“侘寂”美学一脉相承。 这种美学追求自然、朴素与不完美之美,而鼠色系的柔和细腻恰是其体现。 在茶道、和服及建筑装饰等领域,鼠色系象征着对内在宁静与外在简约的追求。

江户幕府颁布“奢侈禁止令”后,对各阶层服饰的颜色、材质和图案进行了明确规范,例如平民不得穿着红色、紫色等鲜艳色彩。 在此背景下,鼠色系凭借其低调和优雅,成为符合当时审美观念的理想选择。 鼠色系服饰适合日常穿着,既能展现品味,又不过于张扬,反映了人们对简朴生活方式的认同。

鼠色系色调多为中性灰,不易显露污渍,因此在服饰、家居及器物装饰中均体现出实用价值。 特别是在武士服装和工匠的工作服中,深色调的鼠色常被赋予坚韧与力量的象征意义。 “鼠色”一词本身也取代了早期指代灰色的“钝色”,后者因与丧事关联而不受欢迎,而“鼠”则带有一种亲近感。

鼠色系既能表达庄重,又不失亲和力,成为当时市井生活的重要组成部分。 平民百姓在日常和正式场合均会穿着鼠色系和服,显得低调而不失庄重。 尤其在男性及中性服饰中,鼠色系既能保持朴素稳重的风格,又能展现优雅气质。

在家居装饰领域,鼠色系同样受到青睐,尤其常用于木制家具、屏风和墙面。 这种色系能赋予空间宁静感与层次感,非常适用于营造茶室、书房等需要安静氛围的场所。

“四十八茶百鼠”(しじゅうはっちゃひゃくねずみ)是江户时代对茶色系和鼠色系颜色的统称,是日本特有的传统色彩体系。 “四十八茶”指以棕色为基础的各种色调,如浅茶、深茶、黄茶等;“百鼠”则指以灰色为基础的各种色调,如青鼠、赤鼠、紫鼠等。 值得注意的是,这里的“四十八”和“百”并非确切数字,而是表示数量繁多之意,体现了当时人们在有限的色彩选择中追求细微变化的巧思。 实际上,茶色和鼠色都可以细分出超过百种的颜色。 这种对细微色差的敏锐捕捉和命名,也反映了日本人独特的色彩感知和审美情趣。

四十八茶可能是黄茶、浅黄茶、枯茶、利休茶、銀煤竹、焦茶、江户茶、紅茶、藁茶、渋茶、琥珀茶、狐茶、山吹茶、栗茶、朽葉茶、梅茶、亜麻茶、濃茶、榛茶、枯草茶、黄土茶、桑茶、胡桃茶、古代茶、利久茶、葡萄茶、伽羅茶、小豆茶、胡麻茶、栗皮茶、黒茶、花茶、土器茶、赤茶、鶯茶、涅茶、梔子茶、山吹茶、錆茶、木賊茶、藍茶、菜種茶、蜜柑茶、枇杷茶、茄子茶、葡萄皮茶、落栗茶和古代黄茶。

百鼠就是颜色中带鼠的颜色,颜色灰度比原色要低。

宗教与哲学

日本传统颜色在宗教(如佛教、神道教)和哲学思想(如茶道、花道)中有着独特而深远的象征意义,往往承载着宗教信仰、人生哲理和美学观念。

在神道教中,许多神社建筑,如著名的伏见稻荷大社,都涂有鲜艳的朱色(しゅいろ)。 朱色被认为具有驱邪辟恶、对抗魔力的力量,同时象征着生命的力量、丰饶和神圣。 朱色在日本传统文化中占有特殊地位,这不仅是美学上的选择,更是对神灵敬畏的体现。 关于伏见稻荷大社的朱色,有一种说法是,这种颜色代表了稻荷大神丰饶的力量。 此外,朱砂(朱色的原料)自古以来也被用作木材的防腐剂。 虽然有传说提及稻荷大神与朱色的关联(例如显现为朱色狐狸的说法),但更普遍的认知是狐狸作为稻荷大神的使者,常被称为“白狐”(透明之狐)。

红色在神道教中普遍象征着幸福、保护和生命力。 因此,许多神社的鸟居(神社入口处的门型建筑)都漆成红色,以标示神圣领域的入口,并有消灾除厄的寓意。 红色也常用于节庆活动,例如在“七五三”节这个为祈求孩童健康成长的传统节日里,孩子们有时会穿着包含红色的衣物以求吉祥。

佛教中的色彩象征

  • 青莲(青色莲花)——智慧与清净的象征:在佛教中,青莲花因其“出淤泥而不染”的特质,被视为智慧和清净的象征。 它代表修行者在纷扰尘世中保持内心纯净的信念。 青色(尤指群青色)也常用于佛教寺庙的装饰和壁画中,象征着对超脱尘世、追求精神觉悟的向往。 此外,青莲花亦被认为是慈悲的象征。

  • 黄色——智慧与启蒙的色彩:黄色在佛教中象征着智慧、启蒙和净化。 许多佛教寺庙采用黄色作为装饰色彩,以表达对佛法的崇敬以及追求智慧的决心。 例如,佛教徒在冥想时,有时会使用黄色的蜡烛或布料,以期营造宁静、专注的氛围,并引导修行者走向启蒙与解脱。 在藏传佛教中,黄色也与佛陀的法身紧密相关。

  • 红色——守护与成就的力量:在佛教寺庙以及日本神道教的地藏菩萨雕像上,经常可以看到红色的围巾或帽子。 红色被认为具有抵挡邪恶、保护弱者的力量,尤其用以祈求保护儿童免受疾病与灾祸。 家长们会为地藏菩萨穿戴红色衣物,祈愿子女平安健康成长。 在佛教中,红色也象征着成就、福德、生命力、慈悲和精进。

  • 金色——光明与佛性的光辉:金色在佛教中象征着光明、智慧、成就和佛陀的至高无上。 佛像和佛堂常常大量使用金色进行装饰,寓意佛光普照,破除无明黑暗。 日本京都著名的金阁寺(鹿苑寺),其外观便覆盖着金箔,象征着吉祥、福德与永恒,也体现了佛教中对净土的向往。 据信,镀金的建筑能够吸引神灵和祖先的护佑,使其成为重要的祈福之地。

  • 白色——纯洁与解脱的向往:白色在佛教中代表纯洁、清净和解脱。 白莲花是纯洁的象征,寓意在轮回中获得超脱。 在净土宗信仰中,白色莲花是往生极乐净土的标志。 白色也常用于佛像和修行用具上,以象征其净化心灵的特性。

在日本,紫色(むらさき)是一种高贵的颜色,历史上常用于贵族和高僧的服装。 紫色象征着权威、德行与高贵。 在佛教中,紫色的袈裟(けさ)被认为是高僧的象征,彰显其崇高地位。

自平安时代(794年-1185年)以来,紫色更是成为统治阶级的象征色彩。 根据记载,日本的紫衣(しえ),即紫色法衣或袈裟,其赐予源于中国唐代。 在日本,平安末期,鸟羽上皇将紫衣下赐给青莲院行玄大僧正,此后紫衣的授予与特定寺院及僧侣的权势相关联。 能够穿着紫色袈裟,不仅仅是地位的体现,也被认为是精神修为达到一定境界的象征。 律令制度中,紫色位列官阶颜色的首位,僧侣的紫衣也参照了这一规定。

关于冥想中观见紫光的说法,在佛教中,紫光具有象征意义,代表光明、希望和智慧。 有观点认为,修行者在修行过程中,通过观察内心的光明来达到解脱和觉悟,这种光明有时被描述为紫光,象征佛性、智慧和慈悲。 能够在冥想中观见紫光,可能被理解为与佛法有深厚因缘,并可能步入智慧之境的象征。

然而,需要注意的是,袈裟的颜色规定和象征意义在不同宗派和时代可能存在差异。 有些观点指出,颜色差异主要体现在法衣(袈裟下面穿着的衣物)上,而非袈裟本身,法衣的颜色因僧侣的阶级而异。 尽管如此,紫色和绯色(黄色加浓的红色)在很多宗派中仍被视为上位颜色,通常只有最高级别的僧侣才能穿着。

在神道教中,祭司和巫女的服饰主要采用白色(しろいろ)。 这种颜色被视为纯洁和神圣的象征,广泛应用于清净和祓除等仪式中。 神职人员身着白色祭服,以表达对神灵的崇高敬意。 有一种说法认为,在祭祀仪式中,白色能够吸引神灵降临,并帮助传递人们的祈愿,因此白色成为了神道教中极具代表性的颜色。

巫女的传统装束通常由白色的和服内衣“肌襦袢”、白色的和服外衣“白衣”以及红色的和服裙子“绯袴”组成。 这种搭配进一步凸显了白色在神道教仪式中的核心地位。

在佛教中,白莲花同样象征着纯洁,并进一步寓意着在轮回中获得超脱。 尤其在净土宗的信仰中,白色莲花被视为往生极乐净土的象征。 白莲花的清净之美,代表了修行者不受尘世污染的纯净心灵。 传说中,佛陀在说法时,常有白莲花随之开放,这也使得白色在佛教中被赋予了神圣的象征意义。

莲花因其“出淤泥而不染”的特性,被佛教视为能够同时体现过去、现在与未来的神圣花朵,象征着修行者在世俗生活中保持内心的清净与觉悟。 白色作为纯洁的代表,也常与佛陀联系在一起,例如传说中佛陀的母亲摩耶夫人曾梦见白象而受孕。 这种颜色在佛教中象征着知识、长寿,以及从极端(如冰雪的寒冷与金属的冶炼)中获得的纯净。

关于平安时期的紫式部与紫色

平安时期的女作家紫式部,她的名字来源并非因为钟爱紫色。实际上,“紫式部”这个名字与她创作的《源氏物语》中的角色“紫之上”以及她父亲曾担任“式部大丞”的官职有关。 《源氏物语》不仅传承了日本古典美学,更是日本文学史乃至世界文学史上的一颗璀璨明珠。在平安时代,紫色是象征高贵、优雅和神秘的颜色,深受贵族阶层的喜爱,并广泛应用于文学和日常生活中。 紫式部在作品中对色彩的描绘也体现了这种审美情趣。

茶道中的色彩哲学

茶道的核心精神是“和、敬、清、寂”。 茶色作为茶道中的重要色彩元素,象征着自然、朴素与沉静。茶具的选用往往偏向于体现自然本真和侘寂美学的色调。茶道师家们会根据季节的流转和心境的变化选择不同深浅色系的茶具。例如,春日里倾向于使用色调明快、轻盈的茶具,而秋冬则偏爱深沉、温暖的色调,以营造相应的氛围。茶道集大成者千利休推崇一种被称为“利休鼠”(りきゅうねずみ)的带有灰度的暗绿色或茶色,这种色彩精准地传达了茶道中“侘寂”——即在不完美、短暂和质朴中发现美的核心理念。 朴素的色彩,如枯茶色,成为了茶道美学的代表。据说,千利休曾在一次茶会中,特意使用了一件带有自然痕迹、未经刻意修饰的茶碗,以此来展现自然的本真之美和世事无常的哲思,启发人们在平凡事物中感知深邃的意境。茶庭(茶道园林)中常见的“枯山水”景观,也大量运用沙石的白、灰以及苔藓的绿、褐等自然色调,营造出宁静致远、引人冥想的氛围,象征着内心的平和与自然的和谐。 这些色彩的运用,既契合了“无为”的哲学思想,也体现了对生活本质的回归。在茶道中,人们通过欣赏这些质朴的色调,感悟“少即是多”的人生哲学。

江户时代的浅葱色与武士精神

浅葱色(あさぎいろ)是一种略带绿调的浅蓝色。 在江户时代,浅葱色因其清冽、沉静的特质,常被用于制作武士的服装,特别是新选组队服的颜色而闻名,象征着忠诚与坚韧。 有说法称德川家康也喜爱这种颜色。在武士的观念中,浅葱色有时也与武士面对死亡的从容与决绝精神相联系,体现了武士道的某些侧面。战场上,一些武士可能会使用特定颜色的旗帜或服饰以表明身份或信念。

花道中的色彩语言:绿色与红色

在花道(華道,Ikebana)中,绿色不仅是生命与自然的直接体现,更象征着生长、希望与活力。花道作品中广泛运用各种绿色植物的叶、茎、苔等元素,巧妙地展现四季的更迭和生命的力量。例如,春季的插花常选用嫩绿的新芽,传递春日萌发的喜悦;而秋冬季节则可能采用常绿的松枝或深沉的叶片,以表现不同时节的独特韵味和生命的坚韧。红色的花朵在花道中常常扮演着点燃热情、注入活力的角色。在作品中恰当地使用红花,如山茶、梅、玫瑰或芍药等,能够为整体构图增添视觉焦点,赋予作品蓬勃的生气和强烈的动感。

社会习俗

在日本传统婚礼中,新娘的经典礼服是纯白色的“白无垢”(しろむく)。自古以来,白色在日本被视为神圣的颜色,最初是神道教祭司服装的颜色,象征着纯洁和神圣。 后来,白色也被用于丧葬等特殊场合。关于白色在婚礼中的象征,一种说法是它代表新娘的纯洁无瑕,以及愿意像白纸一样染上夫家的色彩,融入新的家庭。 “白无垢”的配件包括“绵帽子”(わたぼうし)和“角隐”(つのかくし)。“绵帽子”如同西式婚纱的头纱,在婚礼仪式完成前遮挡新娘的面容,是日本古典婚礼的一项习俗,主要在神社仪式中佩戴。 “角隐”则是在新娘梳着日本传统发髻(通常是文金高岛田)时,覆盖在头顶上的一条带状白布,象征着新娘收起任性(“角”),成为温柔贤淑的妻子。

日本传统服装中的正礼服,如最正式的“大振袖”(おおふりそで),通常会有五枚代表家族的家纹,称为“五つ纹”(いつつもん)。整个和服的图案构成一幅完整的图画。不过,一些非正式场合穿着的大振袖可能会省略家纹。大振袖的历史可以追溯到江户时代,最初是未婚女性的最高规格礼服,也常被武士家庭用作婚嫁礼服,后来逐渐在民间普及。 和服的一种,其中纯白色的婚服被称为“白无垢”,而带有吉祥图案的彩色婚服则称为“色打褂”(いろうちかけ)。在日本的传统婚礼中,新娘通常先穿着白无垢举行仪式,之后在婚宴等场合换上色彩华丽的色打褂。 穿上色打褂,有时也象征着新娘正式成为夫家的一员。在婚礼仪式中,新娘的发饰、头饰和腰带也常采用白色,以突显其纯洁无瑕的姿态。婚礼中,红白两色的组合非常常见,被认为是吉祥与和谐的象征。新娘有时会在婚礼宴席的“お色直し”(おいろなおし,即中途换装)环节换上红色的和服,以祝福婚姻的幸福美满和长久。 据说红白这两种颜色的组合源于日本古代宫廷的喜庆装饰,象征着吉祥如意,这一习俗一直延续至今。

在传统婚礼中,新娘的“色打褂”上常常绣有象征富贵吉祥的花卉图案,如牡丹、菊花、鹤、龟等,这些图案寓意着繁荣、长寿与吉祥。 新郎则通常穿着带有家纹的黑色“纹付羽织袴”(もんつきはおりはかま),与新娘的白色或彩色礼服形成对比,象征着庄重与格调。

在日本传统葬礼中,逝者通常穿着白色的衣物,称为“死装束”(しにしょうぞく)。白色在这里象征着逝者踏上纯净的往生之路,前往净土。 相传,这一传统与佛教的葬仪有关,白色代表灵魂回归净土的清净。逝者的家属,在现代日本的葬礼中,无论是直系亲属还是前来吊唁的宾客,通常穿着黑色的丧服(ブラックフォーマル)或深色素服,以表达对逝者的哀悼与敬意。 这种以黑色为主要丧服颜色的习惯,据说是从明治时代开始,受到西方文化影响后逐渐普及的。 葬礼上,黑色的和服或西式礼服突显了场合的庄重肃穆,寄托了亲友对亡者的深情缅怀。

关于丧服颜色的历史演变:早期日本的染色技术可能不如后世发达,但在特定仪式中已有使用不同颜色的记载。隋唐时期,日本积极吸收中华文化,丧葬礼仪也受到影响。关于“贵族丧服因翻译失误由白转变为灰黑色”的说法,需要更具体的考证。历史上,丧服的颜色和材质会因时代、阶层以及与逝者关系的亲疏而有所不同。平安时代,贵族阶层发展出更为复杂的服丧规定,颜色使用也更为细致。虽然黑色作为丧服的主色调在近代得以确立,但白色在神道教葬礼和一些传统仪式中依然保留着其重要地位。

在葬礼的一些环节中,例如“香典”(奠仪)的信封,通常会使用白色或银色的纸张,并用水引绳结(通常是黑白色或双银色)捆扎,这表达了对逝者的哀思和对家属的慰问。 供奉给逝者的物品,如米、盐、水等,也是葬礼及后续祭祀中的常见元素,象征着对逝者的供养与怀念。

在新年和七五三节等喜庆场合,红色(あかいろ)被广泛用于装饰和服装。例如,在庆祝孩子成长的七五三节,父母会为孩子穿上鲜艳的和服,尤其是女孩的和服,常带有红色图案,以祈求孩子健康成长、驱邪避祸。红色在日本文化中象征着生命力、热情和吉祥。 金色(きんいろ)也常用于节庆装饰和礼品包装中,象征着富贵、繁荣和神圣。新年期间,人们常用红色和金色的绳结(如水引)装饰贺年卡(年賀状)和礼品,寓意新的一年繁荣昌盛。金色被认为能带来好运和财富。紫色(むらさきいろ)自古以来在日本被视为高贵、优雅的颜色。在一些庆祝场合,例如七五三节,有些女孩会选择穿着紫色的和服,象征着高贵、典雅以及对成长的祝福。 在新年的装饰中,紫色也可能被用于一些饰品或包装上,寄托着对长寿和家庭和睦的美好愿望。

新年时,人们会在家门口或神社悬挂用稻草编织的“注连绳”(しめなわ),其主要作用是标示圣洁的区域,驱邪纳福。注连绳本身是稻草的自然色,上面常会悬挂白色的“纸垂”(しで)。 而非整个注连绳是朱红色。年初一(元旦)并没有普遍穿着朱红色新衣的习俗,人们通常会穿着整洁得体的服装进行新年参拜(初詣),颜色选择较为自由,传统和服或现代服装均可。

在一些神社授予的护身符(お守り)中,金色是很受欢迎的颜色,常被用于祈求财运亨通、生意兴隆和整体好运。 金色象征财富和繁荣,尤其在商业繁盛的地区,金色护身符颇受欢迎,寄托了人们对家庭和睦与事业兴旺的期盼。

在日本的许多祭典(祭り)中,参与者会统一穿着称为“法被”(はっぴ)的传统短外套。法被的颜色多样,其中靛蓝色(藍色)是非常经典且常见的一种颜色。靛蓝色因其沉稳的特性,被认为象征着冷静、团结和集体精神。 尤其在一些社区祭典中,统一的靛蓝色法被能够增强参与者的归属感和团队凝聚力。

在端午节(在日本通常指阳历5月5日,也称“菖蒲の節句”),日本人有在门前悬挂菖蒲(しょうぶ)和艾草,以及在浴缸中放入菖蒲叶进行“菖蒲汤”(しょうぶゆ)沐浴的习俗。 菖蒲的绿色和其独特的香气被认为具有驱邪避灾、祈求健康的功效。这一习俗融合了祈求男孩健康成长和祛除瘟疫的愿望。

关于丰收祭典,日本各地有不同的庆祝方式。使用黄色来代表丰收是常见的,例如金黄的稻穗。提及“黄豆饭”作为祈求丰收的特定小吃,这可能是一些地区的习俗,但并非普遍全国性的固定搭配。日本在庆祝丰收时,新米和各种以米为原料的食物(如麻糬)是更具代表性的。

盂兰盆节(お盆)是祭奠祖先的重要节日。在此期间,人们会点燃“迎魂火”(迎え火)和“送魂火”(送り火),灯笼(提灯)是常见的器具,其颜色和样式多样。使用青色或淡雅色调的花卉装饰祭坛,营造宁静肃穆的氛围,以迎接和慰藉先人的灵魂,是很常见的做法。青色或浅蓝色调能带来清凉与宁静之感。

日本人对红色与白色的喜爱由来已久,其起源与多方面因素有关,源平合战是其中一个广为人知的说法。

源平合战发生于平安时代末期的1180年至1185年,是源氏和平氏两大武士家族集团之间一系列争夺权力的战争。 在这场大规模的战争中,为了区分敌我,源氏使用了白色旗帜,而平氏则使用了红色旗帜。 这段历史在日本影响深远,至今仍是人们津津乐道的话题。 因此,这被认为是当双方对阵时,人们习惯用“红白”来区分阵营的原因之一。

关于红白的含义,除了源平合战的说法外,还有其他的解释:

  • 生死与人生: 有一种说法认为,红色代表新生婴儿(日语中婴儿写作“赤ちゃん”),白色则代表死亡与离别。 将这两个颜色组合在一起,象征着生死,也代表了人的一生。
  • 吉祥与纯洁: 在日本文化中,红色常被视为吉祥的颜色,象征生命、活力、繁荣和幸运,常用于祭祀、节庆和婚礼等场合。 白色则代表纯洁、神圣、无暇和新的开始,常常与神道教相关联,用于祭祀神灵或祖先,以示尊重。 红白两色共存,被认为能创造出一种平衡之美。
  • 喜庆: 日本自古以来就有在喜庆之事时吃红豆饭和麻糬(白色)的习惯,一红一白,与喜庆紧密相连。 此外,从室町时代开始流行的新娘服“白无垢”,最初象征纯洁,到了江户时代末期出现了内衬为红色的款式,这也使得红白组合与喜庆的意义联系起来。

红白歌合战

如今,日本影响力最大的新年节目之一便是NHK红白歌合战。 该节目将女性艺人分为红组,男性艺人分为白组,以两组互相进行歌曲对抗赛的形式进行。 节目名中的“红白”也来源于日本剑道中红白对抗的概念。

其他文化现象

  • 源平咲き: 在日本,一棵树上同时开出红色和白色花朵的现象被称为“源平咲き”,其词汇由来也与源平合战相关。
  • 国旗: 日本国旗“日章旗”也是白底红日的图案。

情感表达

白色在日本文化中象征纯洁无瑕、新的开始。 例如,在日本神道教的婚礼中,新娘会穿着名为“白无垢”的纯白色和服,象征着新娘的纯洁以及作为婚姻生活新起点的决心。 禅宗的庭院设计中,白色砂石常用于象征海浪、山峦或云海,白色带来心灵上的清净和空灵。 参禅者坐于白色砂石构成的庭院前,感受宁静和空无,仿佛从日常烦恼中解脱出来。 禅宗中讲究“无”,白色的沙石则让人体验到无欲无求的心境,促使人进入冥想和心灵的纯净状态。

黑色在日本传统文化中不仅象征神秘,还带有庄严的力量。 在茶道中,黑色茶碗特别受到重视。 茶道大师千利休认为黑色茶碗能更好地衬托抹茶的绿色,将观者的注意力集中在茶的本质上。 黑色茶碗散发出一种沉稳而深邃的气息,让人感到平静和肃穆,尤其是在品茶时更能感受到禅意和静谧的氛围。 黑色的深邃也被视为对自我的内省和对宇宙的敬畏。

靛蓝色(藍色)在日本的工匠文化中占有重要地位,象征忠诚与冷静。 日本传统工匠的服装通常是靛蓝色,这种颜色具有镇静作用,帮助工匠在工作中保持专注。 据传,江户时代的工匠以靛蓝色为象征,以表示对技艺的忠诚和对工作的尊重。 靛蓝色服装让工匠感到安稳和踏实,在专注中追求技艺的精湛。 在江户时代的武士文化中,靛蓝也被认为与“胜利”的发音相近(勝ち色 Kachi-iro),因而受到武士的喜爱,代表勇敢与冷静。 据说一些著名的武士会选择穿戴靛蓝色的服饰或盔甲上战场,以冷静的心态迎接挑战。 靛蓝色给人一种可靠的感觉,帮助武士在战斗中保持冷静,并让人们对其信任和尊敬。 靛蓝色传达出一种稳重和不可动摇的精神力量。

在茶道中,抹茶的绿色象征自然之美和宁静心境,喝茶是一种与自然的亲密接触。 茶道师傅通过抹茶的颜色唤起人们的平静心情,抹茶的绿色让人感受到来自大自然的温柔。 绿色在茶道中成为连接人和自然的桥梁,参与者在绿茶的引导下,体会到大自然的宁静与和谐。

在日本神社中,金色护符象征财富和幸福,特别是在经济上代表着富贵之意。 传说金色的护符能吸引好运,帮助人们实现财富和幸福的愿望。 每当参拜者拿到金色护符时,会感到信心十足,仿佛未来充满光明和机会。 金色象征着富足,也让人感受到一种积极向上的力量,激励着人们对未来的期望。

与自然的和谐共生

日本传统颜色的使用体现了日本人对自然的敬畏和热爱,颜色名称常常与季节变化和自然景观相关联。日本传统颜色大多来源于自然,如植物、矿物和动物。 例如,“茜色”(akaneiro)来自茜草的根部,“群青色”(gunjouiro)则源于天然矿石蓝铜矿。 这些颜色的使用反映了日本人对自然的敬畏和热爱。颜色的命名也常常与季节变化和自然景观相关联,例如,「樱色」(さくらいろ)和「若草色」(わかくさいろ)是春天的象征,前者代表盛开的樱花,后者象征刚刚冒芽的嫩草;夏天的「空色」(そらいろ)是晴空的蓝色,而「若竹色」(わかたけいろ)则让人联想到竹林的清凉和生机。 这些颜色不仅捕捉了自然的瞬间,也反映了日本人对四季的敬意,季节的更替在颜色中得到了生动体现。

春季

春天是日本樱花盛开的季节,人们会举办赏樱活动(花見 Hanami),在樱花树下野餐、赏花,享受春天的美好。 樱色因此成为了春天的代表色之一。 樱色是以极淡的红色染料染出的颜色,象征着春天的到来。 在古代,由于鲜艳的大红色染料(如红花染)价格昂贵,一般庶民难以负担,因此相对廉价又美观的樱色受到广泛喜爱。 樱色让人联想到日本街头纷飞的樱花,是春天最具代表性的颜色之一。 日本人对樱花有着特别的感情,认为它象征着生命的美丽与短暂。 赏樱的习俗据说起源于奈良时代贵族观赏梅花,到了平安时代,赏樱逐渐流行。 平安时代的贵族会在樱花树下举行盛大的“花宴”,在赏花的同时饮酒赋诗。 樱花的粉色成为春天的象征,人们将其视为一年轮回的开始,象征着新的希望与重生。 至今,樱花色仍然唤起日本人对春天的期待和对短暂美好事物的珍惜。

萌木色(もえぎいろ)是日本传统色彩中代表嫩绿的颜色,通常指春季草木刚刚萌发时的浅绿色或黄绿色,象征着万物复苏的春天,寓意着朝气和活力。 这种颜色因其清新和充满生命力的意涵,常用在年轻武士的铠甲上。 镰仓时代的著名军事小说《平家物语》中,就有对年轻武士身着萌木色铠甲的描写,例如平家贵公子平敦盛以及源氏的弓箭高手那须与一。

梅色(うめいろ)是一种柔和的、略带紫调的粉红色,如同梅花的颜色,象征着春天的到来和生命的复苏。 梅花是冬末春初盛开的花朵,凌寒而开,常被视为春天的使者,预示着严冬的结束和温暖春日的降临。

山吹色(やまぶきいろ)是一种鲜艳的、略偏橘色的黄色,其色彩来源于日本常见的蔷薇科植物——棣棠花(日语:山吹/やまぶき)。 因其花色如同黄金般灿烂,棣棠花也被称为“黄金色”。 棣棠花主要在春季的4月到6月间盛开。 在日本江户时期,由于小判金币的颜色与山吹色相似,商人们在贿赂官员时,会将金币藏于点心盒内,并隐晦地称之为“山吹色的点心”(山吹色の菓子),因此“山吹色的点心”也成了贿赂的代名词。 在日本古代传说中,有这样一个故事:一位年轻的农夫在春天播种时,惊喜地发现田野中开满了金黄的棣棠花。 他认为这是丰收的吉兆,于是将山吹花作为祭品献给神明,祈求丰收和平安。 从此,山吹色便成为了丰收与希望的象征,常在节庆和庆典活动中使用。

据说,在平安时代,有位诗人(具体诗人姓名和诗歌原文已较难考证,但藤花在平安时代文学中确有重要地位,如《源氏物語》中就有藤壶的角色)深深喜爱盛开的藤花,并留下了动人的诗篇,以表达对藤花美丽与短暂生命的感慨。诗中描绘了藤花随风飘荡、落英缤纷的场景,令人感受到生命之美与无常。这些文学作品的流传,使得藤色逐渐成为了优雅与浪漫的象征。

在古代,日本人会在初春(通常指正月,特别是正月初七人日节前后)进行“若菜摘(若菜摘み)”的活动,即全家一起到野外采集新生的草药(如春之七草),用以制作祈求健康的料理。 若草色的清新淡绿让人联想到春日的温暖和生命的重生,也让人们在视觉上感受到春天的生机。若草色因此成为春天常见的服饰色彩,尤其在和服中,年轻女性穿着若草色和服象征着青春和纯洁。

夏季

紫藤花(藤の花)盛开于春末夏初(约4月下旬至5月上旬),其柔和的色调象征着谦逊与优雅。 古时一些贵族家族(如藤原氏)曾以藤为家徽,使得藤色也带有一丝贵族身份的象征。 藤色的和服常在春末夏初,藤花盛开的时节穿着,象征与自然的和谐,带给人安静和高贵的感受。 人们在藤花盛开时会举行赏藤活动,这也预示着夏日的临近。

抚子花(瞿麦)花期较长,可从春季开至秋季,但在传统上,抚子色(一种可爱的粉红色)常被认为是夏季的色彩。这或许与夏日祭典中,许多女孩穿着抚子花图案的浴衣有关。抚子花的图案象征着优雅、美丽和文静,女孩们甚至会将腰带打成“抚子结”,显得十分可爱,是非常适合年轻女孩的颜色。

夏天,人们喜欢在水边嬉戏、游泳,清澈的水色(みずいろ)因此成为了夏天的代表色之一。它给人一种清凉、舒适的感觉,常用于夏季和服(特别是浴衣)的染色。

夏初时节,嫩叶(若葉)的色彩依然清新。相传古时一些贵族在春末夏初穿着若葉色(わかばいろ)的服饰,象征自己如同刚萌发的嫩叶,充满活力与纯洁。人们相信这种颜色可以带来新生活的希望,给人一种清新与平静的感觉。若葉色至今依旧是春末夏初受欢迎的色彩之一,常用于和服和日用器具上。

青蓝色(一般指靛蓝/藍色系)在日本夏季的染织中非常常见,因为深蓝色不仅在视觉上带来清凉感,还象征着水的凉爽,有助于避开夏日炎热之感。据说,江户时代的人们会在夏天穿着青蓝色的浴衣以抵抗酷暑,尤其是在夏日祭典等盛会中。青蓝色让人们感到平静、清凉,仿佛置身于海边或水边,夏日的炎热在视觉上得以缓解,青蓝成为夏季舒适与清爽的象征。

秋季

桔梗花会开出美丽的紫色花朵,桔梗色(ききょういろ)也因此得名。桔梗是“秋之七草”之一。在《万叶集》著名的“秋之七草”和歌中,山上忆良所咏的“朝貌(あさがお/asagao)”之花,后世普遍认为指的便是桔梗,但这与我们现代所说的牵牛花(朝顔/asagao)是不同的植物。 桔梗色带有一种浪漫与神秘感,给人以无限遐想的空间。夏天转入秋天时,桔梗会开出像吊钟一样的紫色花,这就是桔梗色的来源。桔梗色是青紫系颜色中具有代表性的传统色。

女郎花(おみなえし)也是“秋之七草”之一,开出如其名的黄色小花,是一种给人以明快感的黄色。 它同样在平安时代的文学作品中已有记载,被认为是适合秋天穿着的颜色。“女郎”在古语中有身份高贵的女性或年轻女性的含义,人们便用此花在秋风中静静摇曳的样子来比喻她们。女郎花色因此成为了秋天的代表色之一,寓意着成熟和优雅。

在江户时代,秋茄是农家在秋季收获的作物之一,成熟后会被送到市场上售卖。当时,紫色系如“京紫色”和“青茄子色”是具有代表性的传统颜色,前者高贵神秘,后者则带有一种神秘感;在江户时代,色彩的运用从贵族普及到平民,成为表达个性和情感的方式之一。 虽然没有具体记载农夫直接用秋茄的深紫色装饰家来象征丰收和家庭幸福,但秋季的丰收喜悦与多彩的颜色运用的确是那个时代生活的一部分。

据说每到秋天,京都的寺庙和神社中常有观赏银杏的活动。 人们会在銀杏树下散步或拍照,享受秋日的美景和凉爽。银杏树在日本被视为神圣的植物,不仅是佛教寺院的标志性植物,还象征着长寿、坚韧和希望。 金黄的银杏叶装点着秋季,银杏色的和服和装饰品也成为秋季的流行元素,这种色彩与秋日赏景的习俗相映衬,带给人深秋的喜悦与宁静。

自古以来,日本人便有在秋天观赏枫叶的传统,称之为“红叶狩”(紅葉狩り)。 这一风俗起源于平安时代的贵族,他们会身着华服前往山林赏枫,《源氏物语》中也有相关描绘。 “红叶狩”中的“狩”字,古时亦有探访、寻觅之意。 紅葉的深红色象征着秋日的美好、自然的壮丽以及生命的韵律,也寄托了人们在秋季沉思自我、感悟生命的心境。 至今,紅葉色仍被广泛运用于秋季和服、配饰和家居装饰之中,深受喜爱。

朝霧色,一种带着朦胧美感的色彩,其灵感来源于秋冬清晨弥漫的雾气。在日本的平安时代,贵族阶层对服饰色彩的运用极为讲究,朝霧色因其独特的意境,常被用于和服设计中,象征着神秘与优雅。 穿着朝霧色的衣物,仿佛置身于清晨的薄雾之中,营造出一种宁静而引人遐想的氛围。时至今日,朝霧色凭借其独特的魅力,在服饰、家居等领域依然受到青睐,持续传递着其神秘与雅致的格调。

冬季

雪之白,其纯净之色总能唤起人们对冬季安宁与静谧景象的联想。 在日本的一些地区,例如北方的冬季祭典中,有点亮“雪灯笼”的习俗。这些雪灯笼承载着人们对和平与平安的祈愿,也寄托了对先人的感谢与追思。 白色在日本传统文化中具有神圣和纯洁的象征意义,常与神性相联系。 因此,雪之白色的和服也常出现在冬季的节庆或重要场合,其纯洁无瑕的色泽象征着心灵的纯净与美好的祈愿,也为寒冷的季节带来独特的宁静氛围。

在平安时代,梅花深受贵族喜爱,并被视为高雅品味的象征。当时,从中国传入的“唐风文化”对日本贵族的生活方式影响深远,赏梅也成为一项风雅的活动。

  • 梅花与高贵象征:平安时代的贵族的确非常推崇梅花。它不仅因其在寒冬中绽放的坚韧而被欣赏,也因其清雅的香气和优美的姿态成为诗歌、文学和艺术创作的重要题材。
  • 梅色和服:平安时代的女性会穿着各种颜色的和服,其中包括类似梅花花瓣的颜色,如淡粉色、红色系。这些颜色能够展现女性的优雅与品位。
  • 贵族女子插梅于发髻的传说:这个传说的具体出处难以考证,但在平安时代的文学作品如《源氏物語》等,常有对贵族女性用鲜花(包括梅花)装饰发髻或衣物的描写,这象征着她们对自然美的热爱和对季节变化的敏感。将梅花插入发髻,可以理解为对春天到来的期盼和对美好事物的向往。
  • 红梅的象征意义:红梅因其在严寒中绽放的特性,被赋予了不畏严寒、坚韧不拔的象征意义。这种精神力量也使得红梅及其相关颜色受到部分贵族的喜爱,并用于装饰。
  • 红梅与丰收吉兆:在传统文化中,特定植物在特定时节的生长状况常常与年景联系起来。寒冬中红梅的盛开,因其顽强的生命力,可能被一些人解读为克服困难、迎来丰收的好兆头。
  • 红梅色与冬季服装:红梅色因其温暖的色调和积极的象征意义,在冬季服饰中受到欢迎是符合情理的。它能在视觉上带来温暖感,并传递出希望与活力的信息,成为冬季服装中富有吸引力的色彩选择。

色彩的艺术表现

浮世绘

日本历史上有几位艺术家以其色彩运用和对日本传统色彩之美的独特诠释而闻名。

葛饰北斋(Katsushika Hokusai)

  • 《神奈川冲浪里》与“北斋蓝”/“普鲁士蓝”:葛饰北斋的《神奈川冲浪里》(亦称《巨浪》)是其代表作《富岳三十六景》系列中最著名的作品之一。画作中令人印象深刻的蓝色,主要使用的是当时从欧洲传入日本的化学颜料——普鲁士蓝 (Prussian Blue)。 这种颜色因其鲜艳且相对稳定,迅速被日本画师所接受和喜爱。由于葛饰北斋对此种蓝色的高超运用和推广,“普鲁士蓝”在日本也被一些人俗称为“北斋蓝”(Hokusai Blue)。
  • 普鲁士蓝的传入与特性:普鲁士蓝大约在18世纪末至19世纪初传入日本。 相比日本传统的天然蓝色颜料(如靛蓝和露草蓝),普鲁士蓝色彩更为鲜明、浓烈,且不易褪色,这使得画作能够长久保持鲜亮的色彩。 葛饰北斋敏锐地把握了这种新材料的特性,并将其大胆运用于创作中,极大地丰富了浮世绘的色彩表现力。
  • 色彩的融合与象征:葛饰北斋会将普鲁士蓝与日本传统的蓝色颜料(如“蓝”(Ai,主要指靛蓝)或“群青色”(Gunjo,一种矿物颜料))结合或对比使用,以创造出更富层次和深度的海洋色调。 《神奈川冲浪里》中的蓝色不仅仅是表现海水的颜色,更深刻地传递出大海的磅礴力量与动态之美,同时也可能蕴含着日本人审美意识中对于自然威力的一种敬畏与“物哀”(もののあはれ, mono no aware)的复杂情感。
  • 普鲁士蓝的流行与影响:普鲁士蓝凭借其优异的色彩表现,在日本迅速流行开来,不仅成为葛饰北斋作品的标志性色彩之一,也深刻影响了后来的浮世绘创作,推动了日本传统色彩与外来材料的融合与发展。
  • 《山下白雨》的色彩运用:在《富岳三十六景》的另一幅名作《山下白雨》(Sanka hakuu,英文常译为 “Rainstorm Beneath the Summit”)中,葛饰北斋运用了大胆的色彩对比。画面下方浓重的墨色描绘了雷雨云,与上方被日光照亮的赤富士形成鲜明对比,营造出风雨欲来的紧张氛围和自然的戏剧性。虽然蓝色在此幅作品中不如《神奈川冲浪里》突出,但对天空和远景的处理依然体现了其对色彩的精妙把握。作品通过色彩的对比和构图,生动地表现了自然景象的瞬息万变及其带来的视觉与情感冲击。

歌川广重(Utagawa Hiroshige,1797-1858)是日本江户时代晚期的浮世绘大师。 他的《东海道五十三次》系列是其成名作,以细腻的色彩描绘了从江户(今东京)到京都的沿途宿场风光。 广重尤为擅长捕捉不同季节、不同天气下的微妙变化,其作品充满了诗意之美。

在色彩运用上,广重偏爱使用特定的色调来营造氛围。例如,他作品中经常出现的“广重蓝”(一种基于普鲁士蓝调和而成的标志性蓝色)以及传统的“青磁色”(浅蓝绿)和“红丹色”(暗红),都极具特色,为画面增添了独特的韵味。

具体到作品:

  • 在《东海道五十三次》的《大井川》一图中,他运用了朴素的茶色和青绿色系,生动地再现了旅人涉水渡河的场景及其宁静的氛围。
  • 而在同一系列的《大津》中,则通过明暗对比,并运用“柿色”(红棕色)与“绀青色”(深蓝),巧妙地渲染出夕阳景色下的温暖与感动。
  • 《江户近郊八景之内,羽根田落雁》这幅画作,通过细腻的笔触和柔和的色彩,描绘了雨中田野与飞雁的景象,充分展现了自然的宁静与和谐之感。

歌川广重通过其精湛的技艺,将日本的自然景观与人文风情巧妙地融为一体,使画面充满了抒情意境。 他的作品不仅在日本国内广受欢迎,也对西方印象派画家产生了深远影响。 虽然关于他亲自收集动植物及矿物样本来研究颜料的说法尚待更多考证,但其作品色彩无疑展现了高超的技艺和对自然的深刻感悟,并通过与雕刻师、印刷师的紧密合作,创造出丰富的视觉效果。

喜多川歌麿(Kitagawa Utamaro)以其细腻入微的“美人画”闻名于世。他尤其擅长运用柔和的色彩,例如以“樱色”(淡粉色)、“薄红”(浅红色)等日本传统色彩,赋予画中女性柔美温婉的气质。歌麿的作品色调雅致,层次丰富,生动地展现了女性的优雅风韵与内在情感。

在创作“美人画”(特指其笔下的美人肖像系列)时,歌麿对色彩的运用和整体效果的把握极为讲究。例如,在其著名的《妇人相学十躰》(亦有《妇女人相十品》系列)中,他常运用柔和的粉色系与淡雅的褐色系来描绘人物的肌肤,并通过对发丝、衣物质感的精细刻画,以及背景的巧妙处理(如使用云母摺产生光泽效果),细致入微地展现不同女性的独特神情与温柔气质。为达到这种理想的艺术效果,歌麿在设计画稿、指定颜色时一丝不苟,与雕版师、拓印师紧密协作,确保最终作品的细腻美感。这些色彩的选择与表现方式,被认为是江户时代审美趣味的集中体现,既优雅含蓄,又不失生动,深刻反映了当时社会所欣赏的女性之美。

铃木春信(Suzuki Harunobu)是日本江户时代中期的浮世绘大师,以其优雅柔美的风格和对色彩的精妙运用而闻名。他是“锦绘”(多色印刷版画)发展初期的关键人物,对锦绘的普及和技术革新做出了重要贡献,因此常被认为是锦绘的创始人或早期最重要的推动者之一。

春信的作品色彩柔和典雅,尤其擅长运用中间色调,如淡粉色(日语中称“薄红”)、浅黄色、柔和的绿色(如“青藤色”)和灰色等,营造出一种温馨、细腻且富有诗意的氛围。这些色彩的运用,结合其对人物优美姿态的描绘,使其作品充满了抒情意味。

在其代表性作品中,例如《风流江户八景》系列,春信展现了江户市民的日常生活和细腻情感。他作品中的色彩选择,往往贴近当时江户市民的审美趣味和生活情调,这使得他的作品在当时广受欢迎。

春信的作品也常常表现人与自然的和谐之美。例如,在其描绘雪景的作品中,他会运用浅淡的色彩和柔和的笔触来表现雪的轻盈和宁静,人物的服饰色彩也与之协调,共同营造出一种宁静而温暖的意境。

东洲斋写乐以其富有戏剧性的演员肖像画闻名于世,他的作品以鲜明的色彩对比(尽管“丹色”和“墨色”的直接强调在搜索结果中不突出,但大胆色彩和对比是其特点)和夸张的人物表情为主要特征,旨在表现戏剧演员的独特神情与强烈个性。 他笔下的人物脸部线条棱角分明,表情夸张,以此传递强烈的舞台视觉冲击力。

在写乐的代表作《三世大谷鬼次之奴江户兵卫》中,他运用了强烈的色彩和对比(具体是否为浓烈的“丹色”和“黑色”需要进一步考证,但色彩的强烈性得到确认),有效地增强了画面的戏剧张力。 这种配色方式突破了传统浮世绘中相对柔和的色调,更加凸显了歌舞伎演员的角色特征和内在情感。 作品中演员的表情在鲜明色彩的衬托下,显得尤为生动和富有戏剧性。

关于“写乐的一幅肖像画《歌舞伎演员肖像》描绘了一位著名歌舞伎演员,他穿着华丽的服饰,面带夸张表情”这一描述,搜索结果中并未明确提及一幅标题为《歌舞伎演员肖像》的特定作品。东洲斋写乐本身就以创作歌舞伎演员肖像(役者绘)而闻名,他创作了大约一百四十余幅版画,其中绝大部分是役者绘。 他描绘了许多著名歌舞伎演员,并以夸张的手法和鲜明的色彩来捕捉他们在舞台上的独特魅力和角色个性。 例如,他为市川鰕藏(五代目市川团十郎)等著名演员创作过肖像。

东洲斋写乐的艺术风格在当时并不被主流审美所完全接受,甚至被一些人认为是“丑化”演员。 然而,他的作品后来被广泛认为是极具创新性的色彩应用和人物表现手法,展现了浮世绘中独特的力量感和对人物内心世界的深刻洞察。 他的艺术对后来的浮世绘画家也产生了一定的影响。 尽管他的创作活动异常短暂(约10个月),但他对浮世绘艺术,特别是役者绘领域,留下了强烈而独特的影响。

菱川师宣(Hishikawa Moronobu,1618年-1694年)被誉为“浮世绘的奠基者”,他的作品洋溢着浓郁的江户时代风情。 在色彩运用方面,他偏爱使用如“若草色”(嫩绿)与“山吹色”(金黄色)等明丽色彩,这些颜色在江户市民文化中常象征着生命的活力与喜庆氛围。 其绘画风格灵动洒脱,用笔轻快流畅,整体色彩丰富而不失雅致,细节描绘亦可精细绚烂。 以其代表性的美人画为例(如《回首美人图》),不仅展现了当时流行的服饰风尚,如带有精致刺绣图案的华美和服,也生动捕捉了人物的娇柔妩媚与江户市民的真实生活状态。 菱川师宣通过多样的题材和丰富的色彩搭配,淋漓尽致地展现了时人对生活的热忱以及江户时代贴近自然的生活意趣。 他的作品,尤其是开创性的单幅版画“一枚拓”,在江户时期广受欢迎,许多市民乐于将这些描绘着世俗百态与自然之美的画作悬挂于家中作为装饰。

河鍋暁斎以其大胆而鲜明的颜色组合而闻名,他在鬼怪图中使用了强烈对比的红、绿、黄等颜色。《鬼怪图》这幅作品展示了各种鬼怪形象,传说这些鬼怪源自日本古代民间故事。暁斎通过丰富多彩的表现手法,将这些神秘生物栩栩如生地呈现出来,使观者感受到日本传统文化中的神秘与奇幻。

文学作品

日本文学的诗歌与小说中,色彩常被用来表达人物情感、暗示情节发展,或展现季节与自然之美。

在《源氏物语》中,紫姬(紫之上)与藤壶中宫之间存在“红”与“白”的颜色隐喻。书中写道:“玉のような肌の白さが、上に何も纏っていない清らかな紅と重なり、まるで花びらのように。”(如玉般洁白的肌肤,与身上未着任何衣物的清净红色(指内衣或肌肤本身的光泽)相互映衬,宛如花瓣一般。)此处的白色象征女性的纯洁与高贵,而红色则可能暗示着她们内在的热情与生命的活力。紫姬象征着光源氏理想中的纯真爱情,而藤壶中宫则代表着深情与禁忌之恋。红白二色构成对比,共同塑造了人物的内在美。

紫色不仅象征人物的尊贵地位和优雅气质,更是女主角紫姬名字的来源。紫姬是光源氏一生挚爱的女性。她的名字源于光源氏偶然间遇到的一位酷似其逝去恋人藤壶中宫的年幼少女(若紫)。光源氏将她视为藤壶的“缘者”(ゆかり),精心培养她长大成人,并赋予其“紫”之名(紫姬,紫之上)。紫色在此象征着爱情的忠诚与延续,同时也暗示了光源氏对往昔恋情无法忘怀的执念。紫色贯穿全书,成为古典文学中爱与哀愁的象征之一。女郎花(ominaeshi)是一种明亮的黄绿色花朵,为秋季七草之一,也被称为“败酱”,在文学中常与思念、哀愁相关联。自《万叶集》以来,女郎花便常出现于诗歌中。在《源氏物语》中,有这样的描述:“帷子一重、紫苑色の花と女郎花を織り出したる、いとあざやかに重なりて、袖口より見えたり。”(单层帷帐上,紫苑色与女郎花色的花纹交织,色彩鲜明地层叠着,从袖口微微露出。)女郎花色在此不仅是对织物颜色的描绘,也可能寓意着女性的柔美与淡淡的哀愁。

《源氏物语》中对光源氏的服饰亦有色彩描写,例如“红梅のいと纹浮きたる葡萄染の御直衣に、下には浓き紫の御衣を奉りて”(《若紫》卷)。这里提及的“红梅”(淡红色)与“葡萄染”(深紫色)等色彩,均体现了光源氏的华贵与审美情趣。红绯色(鲜艳的红色)也常用于表现贵族的尊贵与热情。书中对自然景色的描绘也富含色彩:“春の日、庭の山吹が咲き乱れ、その色はまるで絵巻物に描かれたような美しさだった。”(春日里,庭院中的棣棠花(山吹)烂漫盛开,其金黄色泽宛如画卷般美丽。)山吹色(金黄色)在此象征着春日的新生与希望。

松尾芭蕉的俳句中也常以红色描绘秋景,例如他的一些咏红叶的俳句,便将红色与秋日之美联系起来。他感受到红叶短暂却绚丽的生命力,并将其与人生相比照。这种通过颜色来传达情感的方式成为了俳句的一大特色,使得红色不仅仅是自然景物的描述,更是对人生短暂美好的反思。

《枕草子》是平安时代女作家清少纳言的随笔集,她在其中以诗意的文字描绘了四季美景及宫中见闻。清少纳言在《枕草子》中,亦以色彩描绘四季变化的妙趣。例如,关于秋天,她写道:“秋は夕暮れ。夕日の差して山の端いと近うなりたるに、烏の寝どころへ行くとて、三つ四つ、二つ三つなど、飛び急ぐさへあはれなり。”(秋则黄昏。夕日的光辉,近映山际。(落日余晖中)乌鸦归巢,三点两点,急急展翅,很有情趣。)在这里,“夕暮れ”的景象常伴随着夕阳的橙黄与天空的微妙色彩变化,如山际可能呈现的淡紫色调,这种色彩组合象征着秋天的萧瑟与静谧,传达出一丝哀愁与思念。

在“冬はつとめて”(冬日早晨)一节中,她描绘冬日的雪景,以白色的雪来表现冬日的寒冷与静谧。《枕草子》中,金色也常用于描绘宫廷器物或服饰的华丽,象征着高贵与权威。例如描绘贵族们穿着饰有金线的衣物,或使用金饰的器物,在阳光下散发出耀眼的光芒。金色不仅体现出贵族的身份地位,更让人联想到平安时代奢华的生活。金色在平安文学中常作为地位和权力的象征,尤其在清少纳言的笔下,成为那个时代精致生活的点缀。

在江户时代的志怪小说集《雨月物语》中,青色(あお)常作为一种营造幽玄、神秘氛围的色彩,尤其在描绘鬼怪出没的场景时。书中的人物可能在幽暗的青色月光下遭遇幽灵,青色在此象征着不祥之兆和恐惧。它不仅代表夜晚的静谧,有时也暗示着阴阳两界之间的模糊地带。例如,故事中年轻武士在月夜下遇见的美丽女子,其真实身份是亡灵,青幽的月光便成为连接人世与幽冥的媒介。青色在此成为幽冥世界的象征之一,表现了江户时代人们对生死、鬼魂的深刻想象与敬畏之心。

《徒然草》是镰仓时代吉田兼好的随笔集,其中亦有通过描绘秋季黄叶飘零的景象来抒发萧索之感及对生命流逝的思考。例如,他笔下的秋日黄昏,遍地黄叶散落,引人浮想联翩,感叹人生如秋叶般短暂。黄色不仅是季节的体现,更是生命流逝、回顾往昔的象征,赋予作品一种深沉的哲思。

在宫泽贤治的文学作品《银河铁道之夜》中,作者曾这样描绘天空的颜色:“美しい桔梗色の広大な空の下”(在美丽的桔梗色的广阔天空下)。桔梗色(蓝紫色)在这里不仅是对自然景色的真实写照,也寓意着神秘、浪漫与无限的遐想空间。

《万叶集》中,柿本人麻吕的和歌「あかねさす 日は照らせれど ぬばたまの 夜渡る月の 隠らく惜しも」(夕阳茜光照,明月隐夜空,惜彼月影逝,寂寞在我胸。)以“茜草”(あかねさす)这一枕词引出日光(日),而以夜月(月)的隐没比喻人的逝去(此歌为悼念草壁皇子而作),茜色在此不仅描绘了夕阳的余晖,也营造出一种哀伤惋惜的氛围。 《万叶集》中亦有许多歌咏梅花的诗篇,例如描绘早春时节,白梅在青空下绽放,预示着春天的到来,青与白的对比鲜明,展现了自然之美。 《万叶集》中有一首和歌「若草の 新(にひ)芽ぐむころ あしひきの 山した光りて 春は来にけり」(嫩草初生芽,山脚泛光华,春日已来临。)(此为意译示例,原文中对“若草”的运用不尽相同,例如“若草の、つま(夫/妻)もこもれり、おしてる、難波の奥に”等)。“若草”意指春日初生的嫩绿,象征着生命的希望和新生。这种清新的绿色在诗中表达了对春日生机与活力的赞美。

《万叶集》中亦有对景色的色彩描绘,如:“青い海に浮かぶ白い帆は、まるで雲のように美しい。”(青色的海面上漂浮的白帆,宛如云朵般美丽。)这段话描绘了海上的景象,青色象征着自然和宁静。“黄金色の稲穂が風に揺れる様子は、まるで金の波が立っているようだ。”(金黄色的稻穗在风中摇曳,宛如金色的波浪起伏。)这段话描绘了秋天的丰收景象,黄色象征着丰收和富饶。

在镰仓时代的军记物语《平家物语》中,有描写平家贵公子平敦盛、源氏弓箭高手那须与一等人身着萌木色(嫩绿色)铠甲的情景。这种颜色象征着武士的朝气蓬勃与生命力。《平家物语》中“戦場には赤い旗が翻り、その色は血のように鮮やかだった。”(战场上赤旗翻飞,其色鲜红如血。)这段话描绘了战场的激烈景象,赤色象征着战斗和热血。“夜空には黒い雲が流れ、その色はまるで墨を流したようだった。”(夜空中乌云流动,其色如泼墨。)这段话描绘了夜晚的景象,黑色象征着神秘和压抑。

《枕草子》中频繁提到各种颜色,如藤色(淡紫色)、梅色(红梅色或与梅相关的颜色)和樱色(粉红色)等。清少纳言通过这些颜色描绘了自然景观和人们的情感。
在书中,清少纳言描述了春天樱花盛开的场景,她写道(意译):“樱色的花瓣如同轻柔的雪花般飘落,给大地披上了一层粉红色的轻纱。”这种描写不仅展现了春天的美丽,也传达了对生命短暂与美好瞬间的感慨。清少纳言通过这些传统色彩让读者感受到季节变化带来的情感共鸣。

《枕草子》中对色彩的运用非常细腻,例如:“桜の花びらが舞い散る様子は、まるで夢の中の光景のようで、桜色の花びらが空に舞い上がる。”(樱花花瓣飘舞散落的景象,宛如梦境一般,樱花色的花瓣在空中飞扬。)这段话描绘了春天的美景,樱色象征着春天和生命。“秋の夕暮れ、桔梗色の空が美しく、その色はまるで絵のように静かだった。”(秋日黄昏,桔梗色的天空十分美丽,那颜色如画般宁静。)这段话描绘了秋天的景色,桔梗色(蓝紫色)象征着秋天的成熟和宁静。

《古今和歌集》在一首描写秋天景象的和歌中,诗人可能写道:“红叶如火,映照着黄昏时分的天空。”(此为对常见题材的意译描述,具体和歌需查证)。这种描绘传达了诗人对生命无常和美好瞬间流逝的感慨。红色与黄色之间的对比,充满张力。

松尾芭蕉在一首著名的俳句中写道:“夏草や、兵どもが、夢の跡。”(夏草萋萋,乃是武士们,旧梦之遗迹。)这里的“夏草”是绿色的象征,代表了生命的循环往复与时间的流逝。绿色的夏草覆盖着昔日的战场,象征着历史的变迁与自然的永恒。此处绿色不仅仅是景物的颜色,更寄托了诗人对人世沧桑的深沉感慨。

和泉式部在《和泉式部日记》中通过紫色表现内心情感,她曾咏叹道:「紫草(むらさき)の色に心はあらねども深くぞ人を思ひそめてし」(我心并非紫草般易染,却已深深思念着你。)(此为和泉式部集中相关主题的和歌,原文中“紫草の色も、移りにけりな、いたづらに。”一句更常与小野小町联系,指花色易逝)。在此类歌中,紫色象征着爱恋的深切与复杂情感。紫草染出的颜色会随时间而变化,也可能暗示了感情的流转与无常,表现出对情感的珍视与感伤。紫色在此被赋予了丰富的象征意味,描绘出一种独特的美感和情感表达。

蓝色(青)在日本文学中常象征孤独、忧郁与深沉的情感。 例如《小仓百人一首》中权中纳言敦忠的和歌「逢ひ見ての 後の心に比ぶれば 昔はものを 思はざりけり」(相逢方知相思苦,不见先前总不如。) 虽未直接出现蓝色,但这首描绘相逢后别离的诗歌,其所抒发的愈发强烈的思念与孤寂之情,与蓝色所能承载的深沉情感有所共鸣。

在江户时代的《雨月物语》中,幽暗的青色(あお)除了前述的幽灵象征外,也常用于烘托恐怖与不可知的氛围。例如可以想象这样的场景:“青い月夜、木の葉がしとしと降り、怪しげな影が忍び寄る。”(青色的月夜,树叶沙沙飘落,诡异的影子悄然逼近。)青色在这里是夜晚的基调,神秘且带有寒意,表现出鬼怪故事的阴森恐怖,有效地烘托出惊悚氛围。青色象征着未知和阴森,具有非常强烈的心理暗示作用。

在《竹取物语》中,辉夜姬出现在竹林中的一瞬,她的容颜被描述为:“光満ちて美しきこと、世に類なし”(光华满溢,其美举世无双),有时也用“月光の如く輝く”(如月光般闪耀)来形容其非凡之美。这种描写带有清冷而圣洁的光辉,有时也令人联想到金色或银色的辉光,象征着她非凡的出身。这种光辉象征着辉夜姬的高贵与神秘身份,也预示了她并非凡人。在此,这种光辉表现出一种超凡脱俗的美,使得辉夜姬在整个故事中显得神圣而不可触及。

美食色彩

日本料理强调视觉与味觉的双重享受,尤其在“和食”中,色彩搭配具有重要的美学意义。和食的色彩美学以自然为灵感,不仅注重色彩的和谐与层次感,更通过食材的季节性选择与自然紧密联系。寿司、刺身等料理的颜色搭配讲究视觉美感,例如红色的鱼肉、绿色的海苔和白色的米饭。传统甜点和糕点的颜色选择也常常与季节变化相关联。

在春天,日本料理中常见粉色和绿色的组合,象征着大地复苏的季节。例如,樱花豆腐(桜豆腐)是一道用樱花花瓣或带有樱花风味的食材制作的豆腐类点心,呈现淡淡的粉色,可与山菜天妇罗的翠绿色形成对比,象征春日的美丽与生机。每年春天,京都许多老字号餐馆都会推出“花见料理”,即为观赏樱花而准备的精致菜肴。樱花豆腐常作为“花见料理”中的一道菜品,餐馆可能会将腌制的樱花融入豆腐中,或使用樱花提取物调味,使豆腐呈现粉色,花瓣在其中若隐若现,仿佛封存了春天的气息。有说法称,这类与樱花相关的料理可能与江户时代的赏樱习俗有关,人们在赏樱时品尝粉色料理,以此赞美春天。

樱饼诞生于江户时代,其创作灵感来源于日本山樱。关东地区的樱饼(如长命寺樱饼)多用小麦粉制成的薄皮包裹红豆馅,而关西地区的樱饼(如道明寺樱饼)则使用道明寺粉(蒸熟后干燥并粗磨的糯米)制作饼皮,同样包裹馅料,最后再用盐渍樱叶包裹。樱饼的粉色樱花造型,不仅体现了春天的气息,也展示了和食在色彩上的精致与讲究。莺饼是初春时节的点心,通常是在包裹着馅料的糯米团外裹上一层青大豆磨成的豆粉(莺粉)。莺粉呈现素雅的黄绿色,即“莺色”,其左右拉长的独特造型据说是模仿了日本树莺(ウグイス)的形态。莺饼诞生于安土桃山时代,相传是由丰臣秀吉命名的。

夏日的和食讲究清凉,色彩以能带来视觉凉爽的蓝、绿、白色系为主。

  • 冷荞麦面(ざるそば):作为夏季的代表性面食,冷荞麦面通常搭配碧绿的紫苏叶或青紫苏酱油,显得格外清爽宜人。点缀清脆的黄瓜片与淡雅的萝卜丝,更添一份绿意。相传在江户时代的京都贵族家庭中,夏季会使用特制的竹制餐具来盛放冷荞麦面,竹筐的清新竹绿与荞麦面的灰白形成和谐悦目的对比。更有传说,京都一些名门望族所用的竹制器皿乃是代代相传的手工珍品,他们以凉面款待宾客,不仅是美食的呈现,更是借此传递夏日清凉心意与雅致生活的情趣。

  • 刨冰(かき氷)与露草色饮品:无论是在热闹的夏日祭夜市摊位,还是午后宁静的咖啡店,刨冰都是备受欢迎的解暑佳品。在形形色色的刨冰口味中,草莓刨冰尤为经典,染上蔷薇色的雪白沙冰,单看“冰”字便足以让人的眼舌“品尝”到夏日的清凉风味。此外,还有一种名为“露草色”的刨冰糖浆,其灵感来源于鸭跖草科一年生草本植物——露草。此草在夏季开出青紫色的小花,古时人们将露草花瓣在纸或布上擦拭,即可得到这种清雅的“露草色”。用这种糖浆调制的露草色苏打水,沁人心脾,是夏日里不可或缺的视觉与味觉双重享受的清凉饮品。

秋季是红叶(もみじ)与橙色南瓜的季节,日本料理会大量运用这些富有秋日色彩的食材,展现丰收的喜悦与自然的绚烂。

  • 红叶天妇罗(紅葉の天ぷら):这是一道极具代表性的秋季特色小食,将精心挑选的红色枫叶裹上薄薄的天妇罗面糊,炸至金黄酥脆,象征着秋日落叶之美。橙色的南瓜、红色的胡萝卜等也常作为秋季料理的色彩点缀。红叶天妇罗起源于大阪的箕面市,传说这道特色料理已有上千年的历史。每年秋季,当地人会将枫叶清洗后,或用盐稍作腌渍,或直接裹上面糊油炸,以此表达对秋色的赞美与喜爱。这道菜肴不仅是盘中美味,更是一种庆祝秋季的传统,展现了料理与自然之间深厚的联系。

  • 柿子与秋刀鱼:明治二十八年(1895年)十月,俳人正冈子规拜访法隆寺时,曾作俳句赞颂茶店奉上的柿子及其映衬的秋日美景。那鲜艳亮丽的橙色柿子,正是丰收之秋的象征。而秋刀鱼,正如其名“秋刀”,鱼身细长如刀,秋季是其最为肥美的时节。此时的秋刀鱼眼珠清澈,鱼肉细腻,闪耀着独特的钝色光泽,油脂丰厚,新鲜可口。盐烤是最受欢迎的秋刀鱼烹饪方式,烤好后挤上几滴酸橘汁(すだち)或柠檬汁便可享用。品尝当季的秋刀鱼,被认为是生活在四季分明的日本才能体会到的幸福之一。

冬季的和食注重温暖与朴素,色彩上常以白色和绿色为主调,营造温馨宁静的氛围。

  • 大根火锅(おでん):这是一道经典的日式冬季料理,主要使用白萝卜(大根)、土豆等根茎类蔬菜,食材的白色与火锅浅黄色的汤底相映成趣。 锅中还会加入魔芋、鸡蛋、炸豆腐等,再撒上青葱或搭配海苔丝,点缀出清新的绿色。在日本的新年期间,许多家庭会煮上一锅热气腾腾的关东煮,与亲友共享,温暖身心。关于其起源,有说法称江户时期的大阪曾有一位商人,因生活贫困买不起肉类,便用萝卜煮汤待客,没想却意外地吸引了众多食客。这道朴实无华的料理因此逐渐流传开来,成为冬季日本料理中一道温暖人心的代表性菜肴。

  • 五色思想与五色饭(五色のご飯):日本的传统料理深受“五色”美学原则的影响,即:赤(红)、青(绿)、黄(黄)、白(白)、黒(黑)。例如,在新年等节庆场合,许多家庭会将米饭用天然食材染成这五种颜色:红色来自梅子或红米,绿色来自青豆或抹茶,黄色来自栀子果或蛋黄,白色即米饭原色,而黑色则常用黑芝麻或海苔来呈现。五色饭的传统可追溯至平安时代,当时的人们认为这五种颜色象征着构成世界的五行(木、火、土、金、水)以及儒家的五种美德(仁、义、礼、智、信)。这种色彩组合被认为能带来家庭的幸福与和谐。传说,五色饭最早是由宫廷御厨为祈求国泰民安、五谷丰登而创制。每种颜色也常被赋予特定的愿望:如绿色象征健康成长,黄色象征财富兴旺,红色代表喜庆平安,黑色则有驱邪避祸的寓意。至今,这一富有象征意义的传统仍在日本的一些节日和特殊场合中得以保留。

  • 精进料理(しょうじんりょうり):精进料理是日本佛教徒遵循戒律食用的素食料理,其色彩搭配以自然本色为主,常见“赤”(如红豆、胡萝卜)、“白”(如豆腐、白萝卜)、“绿”(如各类青菜)、“黒”(如海带、香菇、黑芝麻)、“黄”(如南瓜、黄豆制品)的组合。 这种配色不仅美观,也反映了佛教中关于五行调和、四季更迭的理念,并注重从不同颜色的食材中获取均衡的营养。 传说精进料理由一位高僧在山中修行时所创,他依靠采集山中野菜和制作豆腐维生,餐盘中五种颜色的菜肴成为他与自然和谐相处的写照。直至今日,在京都等地的寺庙中依然会提供遵循五色原则的素食料理,这些料理不仅让食客品尝美味,同时也能在清幽的环境中感受自然的静谧之美与禅宗的深远意境。

传统工艺

日本传统艺术中的色彩运用以其丰富的象征性、独特的审美价值和深刻的文化意蕴而闻名。无论是和纸的素雅、漆器的深邃光泽,还是织锦的华丽色彩,其背后都有着动人的故事和悠久的历史背景。

和纸的起源与佛教的传播紧密相关。纸张最初由中国经朝鲜半岛传入日本,在公元7世纪初,圣德太子下令推广造纸,主要用于抄写佛经。 由于这种手工制作的纸张极其耐用,颜色柔和且不易变色,因此备受青睐。和纸的素雅色调予人质朴自然之感,特别适合传统书画和纸艺,象征着对永恒之美的追求。现代也有使用天然植物染料(如茜草、靛蓝等)染色的和纸,其低饱和度的色彩呈现出宁静温柔的美感,深受人们喜爱。 在夏日祭典和秋季的收获节等各类庆典中,和纸灯笼被广泛用于点亮夜晚,柔和的灯光穿透纸面,在夜色中显得格外温馨。 据说,在江户时代,寺庙会在盂兰盆节期间挂上无数灯笼,以引导先祖的灵魂归来,这些灯笼也象征着人们对先人的思念。 柔和的灯光不仅照亮了夜空,更温暖了人心,成为家庭和睦的象征。

漆器在日本文化中占有重要地位。相传在平安时代,贵族们为了彰显身份的高贵,喜爱使用饰有“莳绘”(即利用金粉和银粉等金属粉末在漆器上绘制图案的技法)的漆器,这些漆器上常绘有家徽和吉祥图案。在武士时代,黑色因象征刚毅与力量,成为武士盔甲的主色调之一。 漆器中常见的黑红对比色调,不仅展现出色彩的浓烈张力,也传递出一种庄重之美。在茶道中,漆器常用于盛放点心,其深沉的色调能够衬托出茶道的静谧氛围。漆器在日本皇室和贵族生活中扮演着重要角色,尤其是在茶道和宴会等场合,是不可或缺的器具。例如,在茶道中,某些深色(如黑色)的茶碗能够很好地映衬出抹茶的翠绿,使茶色更显鲜明。茶道大师千利休(活躍于室町时代末期至安土桃山时代)提倡“侘寂(Wabi-sabi)”美学,强调质朴、素雅与自然的意境。 而漆器,尤其是红与黑等对比鲜明的色彩组合,在更广阔的茶道文化及其他传统仪式中,因其能营造庄重、深远(可联想到“幽玄”之美所指的深邃意境)的氛围而受到珍视。 即使在现代,日本漆器依旧常用于新年等重要节日,其经典的色彩搭配寄托着人们对吉祥与安宁的美好寓意。

茶道艺术在日本兴起后,日本茶人对青瓷陶器尤为钟爱,因其青绿色调蕴含着自然的清新气息。

“金缮”艺术的诞生,便与这种对不完美之美的欣赏息息相关。相传有著名茶人的珍爱茶碗意外损坏,他不忍舍弃,反而从缺口中体悟到“无常”与“自然”的意趣,于是尝试用金粉精心修补,赋予残缺新的生命与美感。 这种修复方式体现了接纳与再创造的哲学,尤其在茶道中备受推崇。

柿釉陶器的温暖色调,灵感源自秋季柿子成熟时的色彩,因此也常被用于制作茶道器具,为茶人营造出秋日特有的温馨感受。

这些富含意蕴的色调在日本陶器中的巧妙运用,使得陶器本身超越了实用容器的局限,升华为传递四季更迭之美与深邃禅意的艺术载体。

相传,著名茶道宗师千利休以追求简素、朴拙为核心美学理念,他摒弃外在的奢华装饰,倾向于运用内敛暗淡的色调来深层表达茶道的精神内涵。

在庄重雅致的茶席之上,选用那些色调浅淡、素朴无华的侘寂风格器具,其背后象征着对自然的无限谦卑以及对时间无情流逝的坦然接纳。 侘寂色调的器具,旨在引导品茶者在仪式过程中,深刻体悟人生无常之中的独特美感,从而唤起内心的宁静与平和。这种以低饱和度为特征的色彩美学,持续深刻地影响着日本的传统审美观念,并逐渐演变为简约与内敛之美的集中象征。

在日本茶碗的众多品类中,“曜变天目”茶碗被誉为极其稀有和珍贵的宝物。其独特的深色釉面上,自然窑变形成的斑点宛如夜空中闪烁的星辰,瑰丽异常,仿佛将整个星空再现于茶碗之中。 据闻,此类茶碗能引发茶人对宇宙浩渺、万物无限的深邃联想,因此被尊为茶道中最富禅思意境的器具之一。 陶瓷器釉色在烧制过程中所呈现的自然窑变与丰富变化,也象征着对时间流转和世事无常的接纳与包容;其釉彩的流动感与不可预测性,更使人领略到自然造化的无穷力量与原生美感。

平安时期(794年-1185年),日本社会相对安定,贵族文化繁荣发展。在这一时期,织锦成为了贵族服饰的重要材料。当时的贵族们对服饰色彩的选择极为讲究,其灵感往往来源于四季更迭的花卉和自然景致,充满了诗意与雅致。 例如,春季的萌木色(嫩绿色)象征新生喜悦,常用于年轻武士的铠甲。

每逢春季,贵族女子尤其喜爱穿着绣有樱花色调的织锦和服,以此表达对春天的赞美与喜爱。 紫色的淡化版“薄紫”也常在春季使用,与樱花共同象征柔美与诗意。 平安时代的赏樱习俗在贵族间盛行,嵯峨天皇甚至在公元812年举办了最早的官方赏樱会。 可以想见,身着樱花色和服的贵族女子漫步于樱花树下,是何等风雅的景象。

织锦的制作工艺复杂精细,其色彩和图案的选择不仅是对大自然细腻观察的体现,更象征着织工对自然的崇敬之心。 这种审美情趣与日本人对“物哀”的理解息息相关——即对自然之美和生命流转的深刻共鸣与感怀。 “物哀”是日本自古以来就有的美学思潮,强调在接触和认识自然万物与人生世相中产生感动与慨叹。

织锦和服因其色彩的华丽和优雅,以及其承载的文化内涵,常被视为家族传承的象征。 在重要的节庆和仪式场合,和服的色彩选择尤为关键,它不仅是美学的展现,更是生活哲学与文化信仰的传递。

例如,在新年这样的重要仪式上,红白相间的和服是常见的选择,象征着喜庆与吉祥。 红色在日本文化中常与庆典和节日相关联,象征喜庆快乐;而白色则代表纯洁与神圣。

  • 秋季婚礼的红叶情思: 在秋季的婚礼上,新娘有时会选择红叶色调的和服,象征着丰收的喜悦和新生活的开始。 秋季的红叶色、枫叶色和金黄色是和服的常用色调,寓意成熟与丰收。
  • 夏日烟火的清凉雅致: 夏日的烟火大会是日本重要的民俗活动。人们常会穿着青蓝色的浴衣(简便和服),上面点缀着水波或萤火虫等图案,寓意夏夜的静谧与美好。 青蓝色调带来视觉上的清凉感,也象征着夏日的悠闲与活力。 关于江户时代贵族在清晨穿着浅色浴衣观看第一场烟火的说法,搜索结果中提及江户时代的夏日烟火大会上,人们常穿着青蓝色和服,搭配水纹或萤火虫图案,但未明确指出是“清晨”和“浅色浴衣观看第一场烟火”的特定情境。

传统的和服配色讲究与大自然和季节的和谐统一,通过服饰的色彩与纹样,穿着者仿佛与自然融为一体,为生活增添了浓厚的仪式感。 和服的色彩运用,不仅仅停留在服饰美学的层面,更是日本人生活哲学与自然观念的一种深刻表达。

日本的蓝染(藍染,Aizome)拥有悠久的历史。尽管有说法称蓝染起源于江户时代,但实际上,用于染色的蓼蓝植物及其技术在更早的古坟时代(公元250年-592年)就已通过丝绸之路传入日本,当时染出的蓝色被称为“日本蓝”。 进入江户时代(1603年-1868年),蓝染因其耐用性而受到平民阶层的广泛喜爱,并普及开来,常被用作农民和工匠的工作服。

蓝染所呈现的“静蓝色”,色泽深邃,让人联想到大海与天空,充满了宁静的禅意。 关于蓝染,有这样一个传说:在江户时代,一位染织匠人运用蓝染技术,制作出一种色泽独特的深蓝服饰。

在日本的艺术长河中,屏风画占据着举足轻重的地位,其风格与题材也随着时代的变迁而演进。

据载,在辉煌壮丽的桃山时代(约1573-1603年),贵族和掌权者们倾向于在屏风上绘制象征祥瑞与力量的松树、竹林以及其他花木。 这些画作通常背景以金箔或金泥涂抹,营造出一种豪华绚烂的视觉效果,不仅彰显了当时统治阶级的高贵地位与雄厚财力,也反映了那个时代豪放壮丽的审美风尚。 例如,狩野派等画派的作品便是这一风格的杰出代表。

进入战国时代(约1467-1615年),武士阶层崛起并掌握实权。在这一时期,屏风画也深受武家文化的影响。武士们喜好在屏风上展现自然的雄伟壮丽,常见的题材包括山川草木以及象征勇猛的猛禽(如老鹰)等。 这些画作不仅体现了武士阶层对自然力量的敬畏与欣赏,也寄托了他们对武勇精神的追求。 在色彩运用上,除了桃山时代盛行的金色外,各种浓郁的色彩也被广泛使用,以表现自然的生机与画面的张力。虽然日本传统色彩体系中,绿色系(如“绿青”)常与自然意象相关联 [2 search result],但将某一特定颜色定义为战国时期大地与自然的唯一象征色,尚需更多考证。当时武士阶层的服饰和器物中,也常见象征力量与坚韧的色彩,如煤竹色。

无论是哪个时代,屏风画中的色彩运用都不仅仅是为了装饰室内空间,更重要的是它们能够让观赏者在视觉上感受到自然之美,并从中唤起对四季更迭、生命流转的感悟 ,这深刻体现了日本人独特的自然观和审美情趣。屏风本身作为一种兼具实用性与艺术性的家具,承载了丰富的文化内涵。

在贵族们的香道雅集中,香具的色彩选择极为考究,其微妙的色泽旨在象征香气那种难以捕捉的、空灵的特质。例如,“烟霞色”,一种富于柔和过渡的色调,因其能营造出仿佛置身于朦胧云雾之中的意境而被广泛采用,从而显著提升了香道体验的氛围感与沉浸感。据称,有香道大师对烟霞色的香具情有独钟,认为这种色彩有助于品香者在过程中放松心神,更快进入宁静致远的冥想状态。

竹编制品最初多见于古代农民和工匠的日常手工艺,但随着茶道的兴盛,逐渐演变为茶席上不可或缺的重要器具。特别是在夏季的茶会中,竹编茶具所特有的清新淡雅的绿色调,能为炎热的茶席带来一丝视觉上的清凉感。茶道大师们认为,竹子本身所具备的柔韧特性与清新的自然色彩,恰好象征了生命的平衡、坚韧与纯净,因此竹编器具在茶道中被视为清净之物,备受推崇。时至今日,现代日本人依然对竹编制品抱有深厚的情感,认为其天然去雕饰的色彩能够为生活空间带来宁静与和谐的氛围。

和伞(Wagasa)是日本传统节庆与婚礼等重要场合中常见的物品。其中,红色和伞因其鲜艳的色彩而被赋予了带来幸福与祝福的吉祥寓意,尤其在传统的日式婚礼中,新娘撑红伞象征着吉祥如意、趋吉避凶。而在秋日举行的传统庆典里,深蓝色或紫色的和伞常与金黄的银杏叶或火红的枫叶等秋季标志性景观相映衬,共同描绘出秋季特有的那份沉静悠远之美。雨天撑起色彩亮丽的和伞,其斑斓的色彩仿佛能驱散阴雨的沉闷,为雨中景致增添一抹亮色与趣味,令人心情也随之晴朗。

花道(华道)大师在进行插花创作时,会根据季节的特性来选择花材与色彩。春日里,常选用淡粉色的樱花与嫩绿色的柳枝,以此来呼应春天蓬勃的生机与活力。夏日则偏爱使用洁白的芍药、清雅的荷花以及青翠欲滴的枝叶,通过这样的色彩搭配,为观赏者带来清凉舒爽与宁静致远之感。据闻,曾有一位花道大师在初雪之际,选用纯白的菊花,配以深沉的黑色陶瓷花器,成功营造出一种“雪后寒山”般的幽寂意境,令观者深刻体会到冬季的寂寥与禅意。透过这些精心搭配的柔和色调,花道不仅淋漓尽致地展现了花卉草木的自然之美,更深层地象征了季节的往复流转以及人生变幻无常的哲思。

每年夏季在京都盛大举行的“祇园祭”是日本著名的传统祭典之一。届时,京都的街道,尤其是作为祭典核心的山鉾(彩车)会装饰上大量五彩斑斓的织物、绸缎和精美的悬装品(如著名的胴悬、水引等)。 这些色彩绚丽的装饰象征着祈福纳祥、祛除灾厄。当装饰着华丽织物的山鉾在街道缓行,彩绸随风轻拂,仿佛将自然界的灵动之气融入到了节庆的人潮之中,营造出欢快而热烈的氛围。传统上认为,这些装饰的颜色搭配与图案纹样能够协调阴阳,为民众带来好运与安宁。在夜晚灯光的映照下,这些彩绸与装饰所呈现出的瑰丽色彩,成为了祇园祭中最引人注目的亮丽风景线,也让参与其中的人们深刻感受到生活的多彩与美好。

服饰

在日本,和服的色彩选择非常考究,尤其是在季节和特定场合中,深刻体现了“侘寂(Wabi-sabi)”之美,即欣赏非永恒和不完美中的质朴与优雅之感。 不同季节、不同场合的和服颜色选择均有其特定的传统与含义。

春季

春季的和服色彩,常选用淡粉色、嫩绿色和浅紫色,象征万物复苏与新生。 樱花色(薄粉色)与嫩绿色被认为最能代表春天的生机与活力。 相传,在平安时代(794年-1185年),贵族们会在春日穿上樱花色的和服,与庭院中盛开的樱花相映成趣,营造出和谐雅致的氛围。

夏季

夏季天气炎热,人们偏爱清凉色调的和服,如水蓝色、浅绿色和白色。这些颜色不仅在视觉上带来清爽感,也与夏日的清新气息相契合。在江户时代(1603年-1867年)的夏日,人们常穿着水蓝色或靛蓝色的浴衣(夏季穿着的轻便和服),搭配水纹、萤火虫或紫阳花(绣球花)等图案,尤其是在烟火大会等场合,寓意清凉与夏夜的美好。

秋季

秋季是收获与成熟的季节,和服的色彩也相应地选用如红叶色(深红)、枫叶色(橙红)和金黄色等暖色调,象征着丰收的喜悦与生命的成熟。 京都等地的秋日祭典或赏枫活动中,人们喜爱穿着带有枫叶、菊花或秋草图案的和服,与烂漫的秋色融为一体。

冬季

冬季的和服色彩多选用深沉、温暖的色调,如墨色、深蓝色、深绿色以及雪白色。这些颜色不仅象征着冬季的宁静与稳重,亦能与雪景形成呼应。在一些正式场合如茶道仪式中,茶道师可能会选择素色或带有简约图案的和服,以体现茶道的寂静与纯粹精神。

婚礼

日本传统婚礼中,新娘的和服选择尤为重要。最具代表性的是“白无垢”(Shiromuku),这是一种纯白色的和服,象征新娘的纯洁无瑕以及愿意像白纸一样染上夫家的颜色。 婚礼仪式后或婚宴上,新娘可能会换上色彩艳丽的“色打褂”(Iro-uchikake),这是一种带有吉祥图案(如鹤、松竹梅等)的华丽和服,颜色以红色、金色居多,寓意喜庆、幸福和未来的美好生活。 新郎则通常穿着最正式的黑色纹付羽织袴。

葬礼

在葬礼上,和服的颜色以黑色(墨色)为主,称为“丧服”(Mofuku)。 这种选择表达了对逝者的哀悼与敬意,同时也体现了场合的庄重肃穆。 黑色被认为是避免色彩喧嚣,使人专注于悼念的颜色。 参加葬礼的亲属,尤其是近亲,会穿着带有五个家纹的黑色和服,以示最正式的哀悼。

节庆

在各种节庆场合,如新年、成人式等,和服的色彩选择趋向鲜艳和多样化。 红色、金色、以及其他明亮的色彩和吉祥图案(如松竹梅、鹤、樱花等)被广泛使用,以增添喜庆气氛,并寄托对幸福和好运的祈愿。 例如,在成人式上,年轻女性通常会穿着色彩鲜艳、袖子很长的“振袖”和服。

建筑

日本传统建筑的色彩设计深受自然环境、宗教信仰和美学原则的影响,不仅用于装饰,也带有象征意义和文化传承。

金阁寺(正式名称为鹿苑寺)位于京都,其外墙覆盖着闪亮的金箔,是“金色建筑”的典范。 金色不仅与佛教的智慧和净化之意相关,还象征了足利义满作为幕府将军的权力和财富。 这座寺庙最初建于1397年,由幕府将军足利义满所建,最初是他的别墅,名为北山殿。 足利义满去世后,遵循其遗愿,寺庙被改为禅寺。 金阁寺在1950年曾遭遇纵火,原建筑完全被毁,现今的建筑是在1955年重建的,并继续使用金箔装饰,成为京都的象征之一。 这种耀眼的金色让寺庙在湖面倒映下熠熠生辉。

清水寺作为京都的著名佛教建筑,其主堂(本堂)以“清水舞台”闻名,悬在悬崖之上,由139根巨大的榉木柱子支撑,未使用一根钉子。 其建筑群中,仁王门、西门和三重塔等色彩鲜艳,特别是三重塔,高约31米,是日本最大的三重塔之一,其朱红色的外观尤为引人注目。 清水寺整体建筑群色彩丰富,与周围的自然景观四季交融,展现出庄重而富有活力的景象。

日本神社中常见的朱红色,被称为“丹色”(丹色・にいろ),这种颜色鲜艳夺目,具有丰富的象征意义。

朱红色的渊源与象征意义:

  • 天然朱砂与驱邪避灾: 朱红色源于天然矿物朱砂(硫化汞)。 古代人们认为朱砂及其颜色具有驱除邪魔、辟邪消灾的力量。 此外,朱红色也象征着生命力与神圣。
  • 敬畏与结界: 在神社的鸟居和建筑上使用朱红色,除了表达对神明的敬畏之心,也有将神圣领域与世俗隔离开来的“结界”作用。
  • 防腐作用: 朱砂作为颜料涂抹在木质建筑上,还具有防虫、防腐的实际功效,有助于保护建筑。

著名神社的朱红色:

  • 平安神宫: 这座神社建于1895年(明治28年),是为了纪念平安京迁都1100周年而建。 其鲜艳的朱红色社殿建筑是仿造平安京时代的朝堂院(处理政务的正厅)建造的,象征着古都的繁荣与辉煌。 平安神宫主要祭祀的是迁都平安京的桓武天皇和和平安京最后的天皇孝明天皇。
  • 伏见稻荷大社: 以其“千本鸟居”闻名于世,无数座朱红色的鸟居层叠排列,形成通往稻荷山山顶的壮观长廊。 这种被称为“稻荷涂”的朱红色是稻荷神社的标志性色彩,使用的朱砂顔料承载着人们对于生命、大地、生产力量的稻荷大神“御灵”功绩的强烈信仰。 人们为表达祈求与感谢之心而供奉鸟居的习俗兴起于江户时代。 关于传说,稻荷神是谷物和丰收之神,而狐狸被视为稻荷神的使者。 朱红色本身在日本文化中也与希望和光明等积极含义相关联。 将鸟居漆成朱红色,除了上述的驱邪、防腐等意义外,也与稻荷信仰中对丰饶和神灵力量的祈愿紧密相连。

在宫崎县延冈市东海町海边的港神社,有一座罕见的蓝色鸟居。 这座鸟居的颜色灵感来源于大海,采用了鲜艳的蓝色调。 蓝色的鸟居通常出现在海边,呼应着海的颜色。 港神社祭拜的是“龙神”,因此来访者中渔民居多,他们在此祈求海上作业的安全以及渔获丰收。

京都岚山的野宫神社,以其古朴的“黑木鸟居”而闻名。这座鸟居直接使用未剥去树皮的天然木材搭建,因此呈现出木材本身的深暗色泽,而非人工涂刷的黑色。这种黑木鸟居是日本神社鸟居中最古老、最原始的样式之一,充满了庄重肃穆的氛围。

位于栃木县足利市的足利织姬神社,其标志性的鸟居为朱红色。神社本身以其绚丽多彩的氛围著称,尤其在夜间灯光的映照下更显梦幻。该神社以祈求“良缘”闻名,并特别强调七种“缘”的连结,包括:缘、健康智慧人生学业工作以及事业经营。许多游客慕名前来,希望能在此得到神明的庇佑,缔结美满的缘分,收获幸福。

京都龙安寺的“石庭”是日本枯山水庭园的杰出代表。庭园主要由15块精心布置的岩石耙制出纹理的白色砾石构成,岩石周围点缀着苔藓。这种设计并非以苔藓为主题,而是利用苔藓的深绿与岩石的灰褐、白砂的素净形成对比,共同营造出一种高度概括与凝练的微缩景观,引人冥想。
整个庭园设计体现了禅宗美学,尤其是“侘寂”(Wabi-sabi)的理念——在朴素、寂静、非永恒和不完美中发现美。它引导人们在静观中感悟自然、宇宙与生命的本质,体验宁静与空寂的境界。

伊势神宫被尊为日本神道信仰的中心。其建筑采用称为“神明造”(shinmei-zukuri)的独特古老样式,主要使用未经涂漆的桧木(日本扁柏)建成,呈现自然的原木色泽。这种设计不尚繁华装饰,追求极致的古朴与纯粹。

伊势神宫整体的素净感,也得益于铺设于神域内的白色卵石(お白石 - oshiraishi)。原木的淡雅与白石的洁净共同体现了神道教中“清净”(seijōshōjō)的核心理念,象征着无垢与神圣。

最为独特的传统是“式年迁宫”(Shikinen Sengū)制度。每隔20年,神宫的正殿及其他主要建筑会在预留的邻近空地上按照原样重新建造,旧殿则会被拆除。这一方面是为了保持社殿的崭新与神圣,确保神明拥有清净的居所;另一方面也是为了将古老的建筑技艺完整地传承下去,并蕴含着“常若”(tokowaka,意为永远年轻、充满生机)的祈愿,象征神威的不断更新与永续。这种对材质本色与极致简素的追求,以及周期性的更新,使伊势神宫在日本众多神社中展现出独一无二的庄严与神圣。

奈良的东大寺以其宏伟的木造结构和巨大的青铜大佛(卢舍那佛)著称。其南大门是日本最大的山门之一,以其雄伟的木造斗拱结构和古朴的深青绿色瓦片屋顶为特色,整体显得庄严而壮观,与寺庙的整体氛围相得益彰。

东大寺的建造始于公元8世纪的奈良时代,由圣武天皇下令修建,当时日本正受到佛教文化的深刻影响。东大寺作为华严宗的总本山,在其历史上一直是重要的宗教中心,承载着护佑国家和民众的祈愿。

其本尊卢舍那佛(大佛)是世界上最大的青铜佛像之一,象征着佛陀的慈悲与智慧。东大寺因此成为重要的佛教信仰中心和世界文化遗产,吸引了无数信徒和游客前来参拜和瞻仰。

松本城位于长野县,因其深黑色的外墙而被称为“乌城”。这座城堡的黑色墙体与白雪皑皑的冬季景象形成鲜明对比,显得庄严肃穆。松本城的黑色不仅带来视觉上的凝重感,也体现了武士的刚毅精神。据说,其黑色设计受到了当时致力于统一日本的掌权者丰臣秀吉偏爱黑色的影响,黑色也象征着威严与力量。

松本城天守阁的主体结构建于文禄年间(16世纪末),即战国时代末期至安土桃山时代。虽然深色在夜间有一定隐蔽效果,但其黑色外墙(具体为涂有黑漆的护墙板)更主要的原因被认为是出于美学考量、提升木材耐用性以及追随当时统治者的建筑风尚。其深沉的黑色主调与周围的自然景观形成强烈对比,是安土桃山时代受丰臣秀吉影响的城堡建筑风格的一个显著特征。

高野山金刚峰寺是佛教真言宗的总本山。其建筑群主要展现了传统的日本寺庙风格,以天然木材的温润色泽、白色的漆喰墙壁(白壁)以及深灰色的瓦顶为主要特征,而非以紫色为主色调。

虽然紫色在日本文化中确是高贵之色,并常与位高权重的僧侣相关联(例如高僧的袈裟颜色),但金刚峰寺的建筑本身并非以紫色为主调。其朴素而庄重的色彩组合营造出一种宁静、肃穆的氛围,与佛教的庄严相契合。这种环境有助于修行者沉淀心灵,专注于精神探索,追求内心的平和与觉悟。金刚峰寺整体的建筑设计与色彩运用,旨在体现佛教的深邃与引导信众在清净的环境中修行。

白川乡合掌村的传统民居以天然的木材原色和茅草的色泽为主调,展现出纯粹的自然之美,巧妙地融入周围的群山之中。这些房屋广泛采用被称为“合掌造”(合掌造り)的独特建筑风格,其最显著的特征是陡峭的茅草屋顶(茅葺き屋根),这种设计非常适应当地冬季豪雪的气候,有助于积雪自然滑落,减轻屋顶的承重压力。

房屋的墙壁多为木板墙(板壁),有些也会结合使用白色的灰泥墙(漆喰壁)。厚实的茅草屋顶本身就具有良好的隔热保温效果,冬暖夏凉。白川乡合掌村民居的整体设计,生动体现了日本人顺应自然、尊重自然的智慧。原木与茅草的色彩,以及建筑物与周围山林景观的和谐统一,共同营造出一种与自然浑然一体的独特景致。

姬路城因其覆盖着白灰浆(白漆喰)的优雅外墙和层叠的屋檐,远观宛如一只展翅欲飞的白鹭,因此被誉为“白鹭城”。姬路城的历史可追溯至14世纪中叶,而我们今天所见的主体天守阁则是在17世纪初(江户时代初期)由当时的城主池田辉政主持修建完成的,是日本最具代表性的城堡之一。

其白色外墙主要由具有防火功能的白灰浆涂抹而成,这种洁白亮丽的外观不仅是城堡显著的特征,也赋予了其优雅与庄严之感。姬路城在历史上奇迹般地躲过了多次战火与自然灾害的侵袭,始终保持着宏伟的姿态,是日本首批被列为世界文化遗产的古迹,也是日本文化遗产中极为重要的组成部分。

现代融合

日本的传统色彩在现代设计领域中持续焕发新生,并得到创新性的诠释与沿用。

富有盛名的时尚设计师三宅一生(Issey Miyake)巧妙地将日本的传统色彩理念与他标志性的褶皱工艺(如“一生褶”Pleats Please Issey Miyake系列)相融合,创造出极具辨识度的时尚作品。在他的设计中,常可见到对日本传统色谱的借鉴与创新运用,例如将“藤色”(淡紫色)、“茜色”(暗红色)以及“墨色”(近黑色)等传统色彩,通过其独特的面料技术和廓形赋予现代时尚的生命力。

在其设计中,三宅一生也常从樱花等富于季节感的自然意象中汲取灵感,并将与樱花相关的柔和色调融入创作,为时装增添了独特的东方韵味和现代美感。三宅一生曾在访谈中表示,他的创作深受日本自然景观及四季更迭之美的启发。他的设计中,色彩的运用非常广泛,既有鲜明强烈的色彩,也有许多作品通过运用相对低饱和度的色彩,巧妙地传达出一种内敛、温柔且不失优雅的日式美学意境。

日本的传统色彩在现代设计领域中持续焕发新生,并得到创新性的诠释与沿用,成为日本设计的重要标志之一。以下是几个具体的应用实例,展示了传统色彩及其蕴含的美学理念如何与现代设计相结合,体现出独特的和谐美感。

著名时尚设计师三宅一生(Issey Miyake)的设计,尤其是其标志性的褶皱系列,在深层次上呼应了日本传统美学中对材质、形态与时间关系的思考,例如“物哀”所蕴含的对稍纵即逝之美的感知,以及“侘寂”中对非完美和本真状态的欣赏。其褶皱面料在穿着过程中随身体动态展现出自然的形态变化,赋予服装以生命力。他从日本传统色谱中汲取灵感,通过简洁而富有张力的现代服装设计,赋予了这些色彩新的生命力。许多穿着者认为,三宅一生的服饰超越了单纯的时尚潮流,成为一种内敛而深刻的自我表达方式。

以运用自然材料和将建筑融入环境而著称的现代建筑师隈研吾(Kengo Kuma),在设计东京的GINZA SIX购物中心时,虽然建筑主体采用了玻璃与金属等现代材料,但在设计理念与细节处理上,巧妙地融入了对日本传统建筑元素和空间意境的理解。GINZA SIX的外立面设计灵感源自日本传统的“暖帘 (Noren)”与“庇 (Hisashi)”等商业店铺元素。隈研吾通过木材、金属等材料的精心搭配与现代化的演绎,结合光影效果,旨在营造出一种既具有现代都市感,又能与周边银座历史街区氛围相协调的亲和力,并传达出温暖的商业气息。这种将现代建筑语汇与传统文化意涵相结合的设计手法,成功吸引了众多游客,其营造的既摩登又富有文化底蕴的氛围,正是日本现代建筑中“新旧融合”理念的生动诠释。

在涩谷站的部分再开发项目中,设计团队借鉴了日本传统色彩的理念,例如运用温暖、沉静的色调,旨在为这个现代化的交通枢纽增添一份平和与温馨感。设计师希望通过对色彩的考量,让熙攘的车站空间体现“和”的精神,并向使用者传递日本的色彩美学。例如,一些商业设施或特定区域的设计,会融入让人联想到传统日式元素的色彩,营造出既现代又不失亲切感的氛围。

无印良品(MUJI)的设计风格以简约质朴著称,广泛采用天然木色、米白、以及不同层次的灰色、茶色等源于自然的色彩。其商品陈列与空间设计也常体现“空寂”与“素朴”的美学意识,将“无印”(无品牌标志)的理念与这些沉静的传统色系相结合,营造出朴素自然的生活美感。无印良品的重要推动者、时任西友社长的堤清二曾提出“反品牌”等核心理念,希望人们在使用产品时能感受到生活的本真。这些色彩的灵感多源于日本的自然风物,如大地、木材与未漂白的棉麻。无印良品的门店布置通常简洁统一,营造出宁静舒适的氛围,让许多顾客感到放松。

在日本的照明设计中,设计师们常巧妙运用和纸(Washi)本身的质感与传统色彩,如“生成色”(原色,接近米白)、“山吹色”(亮黄色)等,设计出光线柔和的灯具。和纸的透光性与这些温暖、自然的色彩相结合,为灯具带来了温馨、宁静的光感,非常适合用于营造舒适的家居氛围和雅致的公共空间。许多设计师希望通过这样的设计,让人们在光影的映照下感受到日本传统文化的温度与细腻。这类和纸灯具因其独特的材质与光效,有时被形容为能与环境“呼吸”的灯,其光影会随着环境产生微妙变化,为空间增添自然的韵味。这些灯具不仅在日本国内受到喜爱,也逐渐走向世界,让更多人体验到日式传统色彩与材质所带来的独特魅力。

东京2020奥运会及残奥会的会徽与核心视觉设计中,核心色彩选用了日本传统的“组市松纹”(Kumi Ichimatsu Mon)图案,其主色调为深邃的“靛蓝色”(藍色,Aiiro)。这种色彩在日本江户时代广泛流行,象征着日本的传统与雅致。会徽的设计理念是“多样性与融合”,体现了奥运精神。设计者野老朝雄选用的这种特定的蓝色,也承载着日本传统文化中沉稳、坚实的意涵。视觉系统中也包含了代表日本的“红色”(紅色,Akaneiro 或 Kurenai)等辅助色彩,这些传统色彩的运用既展现了现代感,又蕴含了丰富的历史韵味,向全球观众传递了日本的文化精髓。这些色彩的组合,不仅是体育赛事的视觉呈现,也成为了日本文化在国际舞台上的一次集中展示。

日本著名化妆品品牌资生堂(Shiseido)的品牌标志色之一是其特有的“资生堂红”(Shiseido Red)。这种鲜明而优雅的红色,虽然与日本传统色谱中的“茜色”(暗红色)等在视觉上有所区别,但其选择同样根植于对色彩力量的深刻理解和东方美学的现代演绎。资生堂创始人福原有信在创业初期选择红色作为品牌的核心色彩之一,是为了在当时以素雅包装为主流的市场中脱颖而出,并传递出活力、生命力与幸福感。资生堂将这种具有象征意义的红色巧妙融入其产品包装和品牌视觉中,使其产品既具有国际化的时尚感,又不失深植于日本文化的东方韵味,成为品牌传承与创新的重要视觉符号。

人物和作品命名的颜色

甚三紅 (Jinzamomi)

甚三紅是一种富有传奇色彩的红色,其历史可追溯至江户时代(1603年-1868年)中期。这种独特的红色相传由京城一位名为桔梗屋甚三郎的染坊主人所创制。甚三郎精通草木染技艺,在当时的织染界享有盛誉。甚三红的染色工艺尤为考究,主要染料采用红花(べにばな / Benibana)。通过独特的调和与染色工序,他将红花的色泽处理成一种更为鲜亮且略带柔和粉色调的红色。

甚三红的染色过程复杂精细。首先,需要从红花花瓣中精心提取红色素。传统做法通常是将红花在碱性溶液(如灰汁)中浸泡,以提取出红色素(黄色素通常在此过程中被分离或先期去除),之后再加入酸性物质(如乌梅的汁液或梅醋)使红色素沉淀并固着在纤维上。染匠需要精确控制浸泡时间、温度和酸碱度,以确保最终色泽鲜明而不暗沉。

除了红花,为了达到理想的色泽和提升色牢度,染色过程中可能还会加入少量乌梅(うめ / Ume,尤其是其汁液“梅酢”)等作为媒染剂或调色剂,以使颜色更为柔和并持久。这种工艺对环境的温度和湿度也较为敏感,因此理想的染色季节常选择在气候相对稳定的时期。

在江户时代的京都,甚三红深受上流社会,特别是女性的喜爱,常用于制作和服。这种色彩既娇美又不失品位,适合日常穿着与特定社交场合。尤其在京都的祇园(ぎおん / Gion)等花街,甚三红成为艺妓(げいこ / Geiko)与舞妓(まいこ / Maiko)常用的代表性色彩之一。她们的和服、腰带(帯 / Obi)以及和服衬里(襦袢 / Juban)等常常采用甚三红,以凸显其明艳与活力。

江户时代后期,甚三红的应用范围逐渐扩大,不仅限于和服,也流行于扇子、手袋等饰品配件,甚至一些工艺品上也能见到它的身影。

利休茶 (Rikyūcha)利休白茶 (Rikyū Shiracha)錆利休 (Sabi Rikyū)

“利休”系列色彩,如“利休茶”,其命名与16世纪日本茶道文化的集大成者千利休(Sen no Rikyū,1522-1591)密切相关。千利休是安土桃山时代的茶道大师,他所倡导的“侘寂(Wabi-sabi)”美学,强调简素、自然与非完美之美,对后世影响深远。这些以“利休”命名的色彩,通常都带有一种沉静、内敛的特质,正是为了纪念千利休在茶道以及日本美学发展中的重要地位。

  • **利休茶 (Rikyūcha)**:这是一种偏暗的、略带橄榄绿调的棕褐色或深沉的抹茶绿色。它体现了千利休所推崇的“寂(Sabi)”之趣,即从朴素、略带岁月痕迹的物件中发现美。这种色彩能与自然环境和谐相融,营造出宁静、谦和的空间氛围,符合千利休茶道的精神。传说他所偏爱的茶室壁土颜色或某些茶道具的釉色,便接近此类色调。

  • **利休白茶 (Rikyū Shiracha)**:这是一种在利休茶的基础上加入了更多白色的、更为浅淡的颜色,呈现为一种带有灰绿色调的浅茶色或米灰色。它保留了利休系的沉静感,但更为明亮柔和。

  • **錆利休 (Sabi Rikyū)**:“錆(Sabi)”在日语中有“寂び”之意,指古雅、幽静的意境,也常指金属生锈的颜色。錆利休是一种比标准利休茶更为深沉、暗哑,带有金属锈色的橄榄绿或灰绿色调,更富于古朴、幽玄的韵味。

梅幸茶 (Baikōcha)

“梅幸茶”是一种略带绿意的黄褐色,其命名与江户时代中后期著名的歌舞伎演员世家音羽屋,特别是初代尾上菊五郎(俳名:初代尾上梅幸,活跃于18世纪中期至后期)以及后来的三代目尾上菊五郎(俳名梅幸,后改为菊五郎,活跃于19世纪初)等习用“梅幸”之名的演员相关。这些名为“梅幸”的演员以其精湛的演技,尤其是在扮演不同类型角色时的深厚功力,赢得了观众的广泛赞誉。这种颜色因成为这些演员在舞台上所穿服饰的代表色之一而得名,并逐渐成为歌舞伎文化中一个为人熟知的色彩。

在这些“梅幸”演员出演的剧目中,梅幸茶色常被用于其角色服装,尤其是在塑造具有一定身份地位或特定性格的人物时,这种沉稳而不失雅致的色调能够有效衬托角色的气质与风度。

岩井茶 (Iwaicha)

“岩井茶”是一种略带灰调的柔和黄绿色,其名称源于江户时代后期极具人气的歌舞伎“女形”(扮演女性角色的男性演员)——五代目岩井半四郎(1776年-1847年)。他以其出色的演技和迷人的外貌风靡一时,其俊美的眼睛被誉为“目千両”(价值千金的眼睛)。

五代目岩井半四郎在其演出中所穿着的带有这种独特黄绿色的服饰,以及与他相关的饰品如“岩井櫛”(一种梳子款式)和印有“半四郎小紋”(特定小花纹图案)的和服,都在当时引领了时尚潮流。因此,“岩井茶”不仅在歌舞伎界广受欢迎,也成为当时女性和服设计中一种备受青睐的流行色。

璃寛茶 (Rikancha)

璃寛茶是一种略带绿意的暗黄褐色或涩味黄绿色,其名称与江户时代后期在上方(京都、大阪地区)极具人气的歌舞伎演员——初代嵐璃寛(Arashi Rikan I,1769年-1821年,其俳名也作璃寛)密切相关。嵐璃寛以其精湛的演技和独特的舞台魅力而闻名,是当时上方歌舞伎的代表性演员之一。

据传,初代嵐璃寛偏爱并经常穿着这种特定色调的服饰登台,因其独特的品味和巨大的影响力,这种茶色系的颜色便以他的俳名“璃寛”命名,被称为“璃寛茶”。这种颜色在舞台灯光下能展现出一种沉稳而富有格调的质感,与嵐璃寛所塑造的某些角色形象十分契合。

“璃寛茶”不仅在歌舞伎演员的服饰中流行,也作为一种时尚色彩影响了当时町人百姓的衣着审美,成为江户时代后期代表性的流行色之一。

芝翫茶 (Shikancha)

芝翫茶是一种带有红调的黄褐色,略显涩味。其名称与江户时代中期极具影响力的歌舞伎演员——初代中村歌右衛門(Shodai Nakamura Utaemon,1714年-1791年,其俳名也作**芝翫 (Shikan)**,后也被称为初代中村芝翫)密切相关。初代中村歌右衛門是上方歌舞伎的代表性演员,以其宽广的戏路和精湛的演技赢得了极高的声誉。

由于初代中村歌右衛門(芝翫)偏爱并经常穿着这种特定色调的服饰,这种颜色便以其俳名“芝翫”命名为“芝翫茶”。随着其在舞台上的广泛使用和演员本人的巨大声望,芝翫茶迅速在当时的歌舞伎爱好者和追求时尚的町人中流行开来,成为江户时代中期的一种流行色,并被应用于和服等服饰设计中,体现了当时民众对歌舞伎偶像的追捧和时尚潮流的演变。

光悦茶 (Kōetsucha)

光悦茶是一种略带赤味的黄褐色或偏暗的赤褐色,色调温和而沉稳。其名称源自江户时代初期(17世纪)日本一位杰出的文化巨擘与艺术家——本阿弥光悦(Hon’ami Kōetsu, 1558年-1637年)。光悦在书法、陶艺、漆艺、出版、茶道等多个领域均有非凡建树,并在京都的鷹ヶ峰(Takagamine)建立了艺术村(光悦村),汇聚了众多工艺美术家,对后世日本文化艺术产生了深远影响。

本阿弥光悦被誉为江户初期文化艺术的代表人物之一。他的书法风格雄浑大气,独具一格,与近卫信尹、松花堂昭乘并称为“寛永三笔”,其书法流派被称为“光悦流”。在艺术创作上,光悦与天才画家俵屋宗達(Tawaraya Sōtatsu)紧密合作,共同开创了装饰性强且富有设计感的“琳派”艺术风格的先河。

“光悦茶”这一色彩名称,反映了本阿弥光悦所推崇的审美意趣,特别是其作品中常见的朴素、深沉的色调。这种颜色常让人联想到他所制作的乐烧茶碗等陶器作品,这些器物往往呈现出自然、沉静的色彩,体现了“侘寂”的美学精神。例如,光悦的代表性茶碗(如被誉为国宝的“不二山”)的釉色就展现了类似的深邃与质朴。

因此,“光悦茶”不仅指一种具体的颜色,更承载了本阿弥光悦的艺术理念和其作品所散发出的独特韵味。这类沉稳的茶色也可见于日本传统工艺品如染织品、漆器以及部分和纸制品的设计中,体现了对这位艺术大师及其美学思想的致敬。

宗伝唐茶 (Sōden Karacha)

“宗伝唐茶”是一种带有赤味的暗褐色,属于日本传统色谱中的茶色系。“唐茶”本身泛指一系列带有中国(唐)风格或渊源的茶色调,通常是偏红或偏黄的褐色。而“宗伝唐茶”的命名,与安土桃山时代至江户时代初期的著名茶人津田宗凡(つだ そうぼん,Tsuda Sōbon,?-1623年,号**宗传 (Sōden)**)相关。

津田宗凡是堺(今大阪府堺市)的富商兼茶人津田宗及(つだ そうぎゅう,Tsuda Sōgyū,?-1591年)之子。津田宗及与千利休、今井宗久并称为“天下三宗匠”,是当时茶道界的领袖人物之一。津田宗凡(宗传)继承了其父的茶道事业与审美趣味,也是一位活跃的茶人。

据传,宗传偏爱并经常使用这种特定色调的茶道具或服饰,因此这种深沉而富有古雅韵味的“唐茶”便以其号“宗传”冠名,称为“宗伝唐茶”。这种颜色体现了桃山时代至江户初期茶人所推崇的沉静、内敛且富有历史感的审美情趣,与其茶道活动所追求的意境相符。

“唐茶”系列色彩,其名称中的“唐”字,在日本传统语境中常指广义的“中国传来之物”,并非严格限定于中国的唐代。其色调的形成,更多地被认为与宋元时期传入日本并备受珍视的中国陶瓷,尤其是深色釉(如建盏、天目釉等)茶道具的美学风格相关。这些器物对日本茶道审美产生了深远影响。

“宗伝唐茶”这一特定色调,与茶人津田宗传对茶道美学的理解和实践紧密相连。宗传在茶器的选择与使用上,可能偏爱那些展现了这类深沉、古雅色调的器物。这种偏好体现在他所使用或鉴赏的茶道具上,特别是那些被称为“唐物茶碗”(からものちゃわん,Karamono Chawan,指从中国传入的茶碗)或受其风格影响的日本国产茶碗。这些茶碗的釉色常呈现深褐色、赤褐色或带有微妙变化的暗色调,与“宗伝唐茶”的色彩意境相符。

“宗伝唐茶”的色彩,常见于一些茶道具的釉色或与茶道相关的染织品中,旨在营造一种沉静、质朴且富有历史感的氛围,这与茶道所追求的“侘寂”精神高度契合。这种颜色所传递的温暖而雅致的感觉,能够增强茶席空间的静谧与和谐。

団十郎茶 (Danjūrōcha)

団十郎茶是一种具有代表性的日本传统茶色,呈现为一种略带赤味的红褐色或柿色。这一色彩的名称与日本歌舞伎历史上声名显赫的演员世家——市川团十郎(いちかわ だんじゅうろう,Ichikawa Danjūrō)紧密相连,尤其是指其初代。市川团十郎家族是江户歌舞伎的代表性家族之一,历代团十郎均以其精湛的演技和对歌舞伎艺术的贡献而闻名。

初代市川团十郎(1660年-1704年)以其开创的“荒事”(あらごと,Aragoto)表演风格著称,这种风格的角色通常豪放磊落、充满力量。据传,初代团十郎偏爱穿着这种色调的服饰登台,其独特的舞台形象和巨大的声望使得这种红褐色在当时的江户民众中广为流行,并因此以“团十郎”之名命名,称为“团十郎茶”。此后,历代市川团十郎也多有沿用此色,进一步巩固了其作为市川宗家代表色的地位。

团十郎茶的色调,是通过传统的染色技术实现的,常使用如柿渋(かきしぶ,Kakishibu,柿子榨取的汁液发酵而成)或弁柄(べんがら,Bengara,一种红色氧化铁颜料)等天然染料,这些染料能赋予布料坚韧的质地和独特的红褐色调,非常适合表现“荒事”角色的阳刚与勇武。

在歌舞伎舞台上,团十郎茶常被用于主角或重要角色的服装,如“暫”(しばらく,Shibaraku)等经典剧目中,演员饰演的英雄人物的服装便常采用此色,以突显其威严与豪迈的气质。团十郎茶不仅是舞台服饰的色彩,也反映了江户时代民众对歌舞伎明星的崇拜以及由此产生的时尚潮流,成为日本传统色彩文化中一个富有故事性的代表色。在如《仮名手本忠臣蔵》(かなでほん ちゅうしんぐら,Kanadehon Chūshingura)等著名剧目中,若由市川团十郎家族的演员饰演重要角色,其服饰也可能融入这种具有家族象征意义的色彩,以彰显其身份与表演风格。

吉岡染 (Yoshiokazome)

“吉岡染”指的是位于京都的染织工房“染司吉岡(そめのつかさよしおか,Somenotsukasa Yoshioka)”所采用的纯天然植物染色技艺及其染出的色彩。染司吉岡的历史可以追溯到江户时代,至今已传承六代。近代致力于复兴和传承纯植物染色技艺的关键人物是第四代当主吉岡常雄(よしおか つねお,Yoshioka Tsuneo)。他从20世纪初期开始,便专注于研究与实践天然植物染色技术,摒弃化学染料,探索如何仅通过自然材料来再现日本古老的传统色彩。

其子、第五代当主吉岡幸雄(よしおか さちお,Yoshioka Sachio,1946年-2019年)继承并光大了家族事业。吉岡幸雄不仅是染织工艺家,也是一位杰出的染织史研究者。他深入研读古代文献,如《延喜式》、以及《源氏物语》等古典文学作品,依据古法不懈努力,成功恢复和再现了诸多失传的古代色彩。他曾表示:“我希望将这些珍贵的传统技艺传承下去,不让它们在我的时代消失。”他的贡献使得许多几近失传的日本古代表情丰富的色彩得以重现生机。

吉岡染所使用的颜色绚丽多彩,均通过天然植物染料染制而成,常用的染材包括紫草根(用于紫色)、红花(用于红色、粉色)、茜草根(用于茜色)、刈安(用于黄色)、蓼蓝(用于蓝色)等等。这些色彩不仅在视觉上呈现出深邃与温润之美,也蕴含着源于自然的生命力。例如,吉岡幸雄成功复原的“御大尝祭”中天皇所穿着的黄栌染(こうろぜん,Kōrozen,一种象征太阳的黄色)以及古代贵族使用的贝紫色(一种从特定海螺中提取的紫色,常与帝王紫相关联)等,都是其代表性成就。

染司吉岡的工房位于京都市伏见区(其店铺位于京都市中京区)。工房在染色过程中,尤为注重水源的品质,使用的是优质的地下井水,这种纯净的水质对植物染色的效果至关重要。

在实际应用中,吉岡染广泛用于制作和服、袈裟、以及各种工艺品和室内装饰品。吉岡幸雄生前曾长期为日本各地众多著名的寺庙和神社提供祭祀、法会活动中所使用的传统服饰与染织品,例如奈良的东大寺(用于修二会“取水节”的纸花染色)、药师寺以及伊势神宫等。通过这些实践,吉岡染不仅传承了古老的染色技艺,也让日本的传统色彩在当代重要的文化场合中持续焕发光彩。

我在韩国首尔 KWDC24 做的技术分享

作者 戴铭
2024年10月28日 10:58

韩国朋友真是太热情了。下面是这次分享的内容,文章后面我还会记录些这次首尔的见闻。

The topic I’ll be discussing is the evolution of iOS performance optimization. I hope you can take away some insights from my talk.

Let’s first talk about a few situations where an app becomes unusable, which can be simplified into app crashes or freezes. There are three main reasons, the first being OOM, meaning memory exhaustion.

When an app consumes too much memory, the system can no longer allocate more, leading to OOM. This issue doesn’t produce crash logs, making it tricky to trace.

The second reason is a null pointer, where the pointer points to an invalid memory address. The third common issue is accessing a nil element in an array, which is another frequent cause of crashes.

These are the three most common causes of crashes, with memory issues being the hardest to resolve. Next, I’ll focus on how to address memory issues.

In addition to crashes, performance issues can also affect the user experience, such as lagging or overheating.

  • Lag can be identified through Runloop monitoring to locate the part of the stack where execution takes too long;
  • Overheating can be addressed by monitoring CPU usage in threads to find the threads or methods causing CPU overload.

Slow app startup and large package sizes also impact user experience. As projects grow in complexity, solving these problems becomes increasingly challenging.

The above four issues lead to a poor user experience.

Upon analysis, these three problems are the hardest to solve: memory issues, slow startup, and large package sizes. I will focus on sharing some of the latest solutions to these problems next.

Memory issues fundamentally stem from improper memory usage. Memory is a finite resource, and if we misuse it, problems will inevitably arise.

The most common memory issues are threefold: the first is memory leaks, where memory is not released after being used, leading to increasing memory consumption.

The second issue is high memory peaks. When memory usage suddenly spikes at a certain point, the system may trigger the Jetsam mechanism, killing the app directly.

The third issue is memory thrashing, which refers to frequent garbage collection, causing performance corruption.

So, memory leaks, high memory peaks, and memory thrashing are the most common memory issues.

To solve memory issues, the first step is to understand memory usage. We can retrieve this information using system APIs, such as mach_task_basic_info, the physicalMemory property of NSProcessInfo, and the vm_statistics_data_t structure.

In addition to APIs, Xcode’s Memory Graph feature is very intuitive, allowing you to view the app’s memory usage in real-time, making it a very handy tool.

There are also some open-source libraries, such as KSCrash, which provide freeMemory and usableMemory functions to retrieve information about the system’s free and available memory.

Using these methods, we can clearly monitor the app’s memory usage.

What may seem like a small memory leak can accumulate over time, eventually causing system performance worse or even triggering an OOM crash.

The most common cause of memory leaks is retain cycles. Here are two open-source tools that can help us detect retain cycles.

The first is MLeaksFinder. It hooks the dealloc method to check whether an object still exists after being released, thereby determining if there is a memory leak.

The second tool is FBRetainCycleDetector. It traverses strong references between objects and builds a reference graph. If it detects a cycle, it indicates a retain cycle issue.

Retain cycles are relatively easy to detect. In addition to these open-source tools, Xcode’s tools can also help us detect memory leaks in a visual way.

In contrast, memory peaks and memory thrashing are like hide “little monsters” and are harder to detect. So, how do we track down these problems like detectives?

Here’s one method: by repeatedly sampling memory usage, we can calculate the differences and identify the objects with the fastest memory growth.

Rank the top 100 objects with the most significant growth. Specifically, this can be done by hooking the alloc and dealloc methods to track the allocation and release of objects.

Each time memory is allocated, we can maintain a counter—incrementing the counter on alloc and decrementing it on dealloc—this way, we can keep track of the number of currently active objects.

With this method, we can pinpoint the objects with the fastest memory growth, making it easier for further analysis.

Next, let’s introduce hook malloc, which allows us to capture every memory management operation. It’s like planting a “secret agent” to monitor each memory allocation action.

Below are some common methods to hook malloc, including macro definitions, symbol overriding, and function attributes. The most flexible method is using fishhook, which allows dynamic toggling.

fishhook is a technique that modifies Mach-O file symbols to achieve function replacement. We can use it to replace the malloc function.

In the code above, the purpose of rebind_symbol is to replace the malloc function with our custom-defined custom_malloc function. The second parameter, original_malloc, indicates that after replacing the function, the original function will continue to be executed.

This way, with each memory allocation, through the custom_malloc function, we can capture the size and address of every memory allocation.

Additionally, the system’s built-in malloc_logger tool can also comprehensively record the memory allocation process, offering a more straightforward solution.

malloc_logger is essentially a callback function. When memory is allocated or released, it will callback and log relevant information.

By tracking malloc and free operations, we can discover memory blocks that haven’t been correctly released.

After solving memory issues, remember to retest to ensure the problem is completely resolved.

Next, let’s look at how to customize this malloc_logger function to capture memory allocation and release information.

First, define a callback function with the same signature as malloc_logger, for example, custom_malloc_stack_logger.

The type indicates the type of memory operation, such as malloc, free, or realloc; arg1 represents the memory size, arg2 is the memory address, and result indicates the reallocated memory address.

Based on different type values, we can obtain this parameter information and record memory allocation details, especially for large memory allocations. We can also capture stack information to facilitate issue analysis.

Of course, a memory snapshot is also a comprehensive solution that captures complete memory information.

First, by traversing the process’s virtual memory space, we can identify all memory regions and log information like the start address and size of each region.

Using the malloc_get_all_zones function, we can retrieve all heap memory regions and analyze each region’s memory nodes one by one, ultimately identifying memory reference relationships.

With this more comprehensive information, we can resolve memory leaks, optimize memory usage, and prevent OOM crashes in one go.

Here is a code example for finding all memory regions. As you can see, the vm_region_recurse_64 function’s info parameter contains information like the memory region’s start address and size.

Using this information, we can construct a memory layout map to analyze the app’s memory state when issues occur, such as using the protection property to check if the app accessed unreadable or unwritable memory regions.

Compared to other methods, the benefit of malloc stack logging is that it automatically records data without needing to write code manually to capture memory information. You just need to enable it when necessary and disable it when not.

MallocStackLogging records every memory allocation, release, and reference count change. These logs can be analyzed with the system tool leaks to identify unreleased memory or with the malloc_history tool to translate stack IDs in the logs into readable stack trace information.

Here is an example code for using MallocStackLogging. We can use the enableStackLogging function to enable logging, disableStackLogging to disable logging, and getStackLoggingRecords to retrieve current memory operation details.

In the enableStackLogging function, turn_on_stack_logging is called to enable logging. disableStackLogging calls turn_off_stack_logging to disable logging. getStackLoggingRecords calls mach_stack_logging_enumerate_records and mach_stack_logging_frames_for_uniqued_stack to record the details of current memory operations.

The tools we used earlier, leak and malloc_history for analyzing MallocStackLogging logs, both come from the malloc library. The malloc library provides many tools for debugging memory.

In addition to MallocStackLogging, the system offers many tools for debugging memory, such as Guard Malloc and some environment variables and command-line tools.

The MallocScribble environment variable can detect memory corruption errors.

We’ve talked a lot about how to solve problems when they occur, but is there a way to optimize memory before problems even arise?

In fact, iOS itself evolves to optimize memory management. Especially in iOS, which is designed for mobile devices without swap partitions like desktop systems, it uses the Jetsam mechanism to help developers manage memory proactively when resources are tight.

Additionally, the system provides tools like thread-local storage and mmap(), which are methods that can improve memory efficiency.

Here are a few tips to help reduce unnecessary memory overhead:

  • Take advantage of the copy-on-write principle and avoid frequently modifying large strings.
  • Use value types as much as possible to avoid unnecessary object creation.
  • Make good use of caching and lazy loading.
  • Choose appropriate image formats and control image resolution and file size.

These are some of the optimizations the system does for you, but there are plenty of areas where we can optimize as well.

A slow app launch can be a frustrating experience. We all know that this is a big issue.

App launch actually happens in several stages. The first stage is called Pre-main, which refers to things the system does before the main() function executes, like loading app code, the dynamic linker working, Address Space Layout Randomization (ASLR), and some initialization operations.

After these preparations are done, the app truly starts running and enters the UI rendering stage, where tasks in didFinishLaunchingWithOptions begin executing. These tasks include both the main thread’s work and operations on other threads.

To summarize, app launch is a multi-stage process. From Pre-main to UI rendering, tasks must be properly arranged, and neither the main thread nor background threads should waste resources.

Next, let’s talk about factors affecting launch performance. In the Pre-main stage, the number of dynamic libraries, the number of ObjC classes, the number of C constructors, the number of C++ static objects, and ObjC’s +load methods all directly impact launch speed. Simply put, the fewer, the better.

After the main() function is executed, even more factors can affect the launch time, such as main() execution time, time spent in applicationWillFinishLaunching, view controller loading speed, business logic execution efficiency, the complexity of view hierarchy, number and speed of network requests, size of resource files, usage of locks, thread management, and time-consuming method calls—all of which can slow down the launch.

As you can see, many factors influence launch time, both before and after main(). However, this also means there are many opportunities for optimization.

For large apps, which are often developed by multiple teams, tasks executed at startup can change with each iteration. Therefore, we need an effective way to measure the time consumption of each task during startup to identify the “culprits” slowing down the launch, enabling targeted optimizations and checking the effectiveness of those optimizations.

Common measurement tools include Xcode Instruments’ Time Profiler, MetricKit’s os_signpost, hook initializers, hook objc_msgSend, and LLVM Pass.

Next, I’ll focus on hook objc_msgSend, which can record the execution time of each Objective-C method. For measuring the execution time of Swift functions, you can use LLVM Pass, which I’ll explain in detail when we discuss package size optimization.

By hooking objc_msgSend, we can record method call information, including method names, class names, and parameters. By inserting tracking code before and after method execution, we can calculate the execution time of each method.

The specific approach is to first allocate memory space for jumping, with the jump function being used to record the time. Then, save the register state: the x0 register can obtain the class name, the x1 register gets the method name, and the x2 to x7 registers can be used to get method parameters.

After completing the jump function call, restore the saved registers and use the br instruction to jump back to the original method and continue execution.

Although hook objc_msgSend uses assembly language, it’s not too complicated to write as long as you understand the roles of several registers and how the instructions work.

Next, I will introduce ten very useful startup optimization strategies:

  1. Reduce the use of +load methods.
  2. Reduce static initialization.
  3. Prefer static libraries over dynamic libraries to reduce the number of symbols.
  4. Control the number of dynamic libraries.
  5. Use the all_load compiler option.
  6. Perform binary reordering.

After the main function, we can do a lot more optimization, such as:

  • Optimizing business logic.
  • Using task scheduling frameworks to arrange tasks more efficiently.
  • Leveraging background mechanisms to handle non-essential tasks.
  • Refreshing regularly to fetch server data in a timely manner.

The final important topic is optimizing package size.

Optimizing package size has many benefits. For users, it improves download speed, saves device storage, and reduces resource consumption. For developers, it lowers development and maintenance costs while improving efficiency.

Through static analysis, we can identify some unused resources and code. Today, I will focus on how to discover unused code at runtime, starting with detecting unused classes.

In the meta-class, we can find the class_rw_t structure, which contains a flag that records the state of the class, including whether it has been initialized at runtime.

The code on the right shows how to access this flag and use it to determine whether a class has been initialized.

Next, let’s discuss how to determine which functions haven’t been executed at runtime.

This code shows how to customize an LLVM Pass to instrument each function and track whether they are called. The instrumentation code is written in the runOnFunction or runOnModule functions, where the former handles individual functions, and the latter handles the entire module.

Additionally, LLVM Pass can insert tracking code before and after function execution to record the execution time of each function.

以上就是分享的内容。下面是一些见闻。

KWDC 这次是在一所大学举办的。

这是我、徐驰和 falanke 的合影,会场有个大头照机器,很多人都在这里合影。

iOSConfSG 2025 组织团队负责人 Vina Melody 也来了,我分享结束后跟他们沟通了下明年我去新加坡 iOSConf 分享的内容。

第二天,KWDC团队组织我们在首尔 City walk,第一站是景福宫,我们玩起来 Cosplay。

freddi 是喵神的同事,在福岡。

River 是韩国的一名独立开发者,开发了很有品味的 APP Cherish。她不喜欢 KPOP,但她父母好像是从事表演的。

台湾最知名的 iOS Youtuber Jane 这次也来了。

中午我们吃了鸡肉火锅。

下午去了汉江野餐。晚上我们登上南山,看到了美丽的首尔夜景。

晚上,继续找地方喝酒。韩国晚上街上人依然很多。

giginet 聊了点技术问题,他也是喵神的同事。

二刷 iOS 性能与编译,简单点说

作者 戴铭
2024年9月5日 16:36

本文主要想说说 iOS 的性能问题的原因,如何监控发现问题,以及如何预防和解决这些问题。

为啥要说是二刷呢,因为以前我也写过好几篇性能相关的文章。有性能优化的深入剖析 iOS 性能优化,包体积相关的GMTC 上分享滴滴出行 iOS 端瘦身实践的 Slides用 Swift 编写的工程代码静态分析命令行工具 smck使用Swift3开发了个macOS的程序可以检测出objc项目中无用方法,然后一键全部清理使用 LLVM使用 LLVM 分享的幻灯片。还有启动速度相关的App 启动提速实践和一些想法如何对 iOS 启动阶段耗时进行分析。编译相关的深入剖析 iOS 编译 Clang / LLVM

这次我尽量绕开以前谈的,只简单提提,着重说些以前没提或者说的少的。来个互补吧。也加了些前段时间去深圳给平安做分享的内容。

这次内容也整理进了小册子方便下载后按目录日常查阅,小册子程序本身也是开源的,欢迎 Clone 查看。

由于 iOS 性能问题涉及面很多,我先做个分类,这样好一个一个的说。大概顺序是会先从造成用户体验损失最大的卡顿、内存爆掉来开头,然后说下启动和安装包体积怎么优化,说说性能分析的工具和方案,最后讲讲怎么使用 Bazel 提速编译。

卡顿

先了解下 iOS 视图和图像的显示原理。

介绍

我们了解的 UIKit 和 SwiftUI 都是提供了高层次的管理界面元素的 API。另外还有 ImageView 是专门用来显示图像的类。底层是 Core Graphics,也可以叫做 Quartz,这是 iOS 的 2D 绘图引擎,直接和硬件交互。Core Animation 是处理动画和图像渲染的框架,将图层内容提交到屏幕,并处理图层之间的动画。

底层图形渲染管线 iOS 用的是 Metal。Core Animation 会将要渲染的图层内容转换成 GPU 可以理解的命令,然后让 Metal 渲染到屏幕上。

大图

最容易造成掉帧的原因就是大图。由于大图数据量较大,对应渲染指令就比较多,会影响渲染的时间,造成卡顿。可以在显示大图前,先加载并显示较小尺寸的缩略图,等用户确实需要查看高清版本时,再加载完整图片。

举个例子:

import SwiftUIstruct ThumbnailImageView: View {    let thumbnailImage: UIImage    let fullSizeImageURL: URL        @State private var fullSizeImage: UIImage? = nil    var body: some View {        ZStack {            if let fullSizeImage = fullSizeImage {                Image(uiImage: fullSizeImage)                    .resizable()                    .scaledToFit()            } else {                Image(uiImage: thumbnailImage)                    .resizable()                    .scaledToFit()                    .onAppear(perform: loadFullSizeImage)            }        }    }    private func loadFullSizeImage() {        DispatchQueue.global().async {            if let data = try? Data(contentsOf: fullSizeImageURL),               let image = UIImage(data: data) {                DispatchQueue.main.async {                    self.fullSizeImage = image                }            }        }    }}

在加载大图时使用 CGImageSource 逐步解码图片,在低分辨率时减少内存占用。

import UIKitfunc loadImageWithLowMemoryUsage(url: URL) -> UIImage? {    guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {        return nil    }        let options: [NSString: Any] = [        kCGImageSourceShouldCache: false, // 避免直接缓存到内存        kCGImageSourceShouldAllowFloat: true    ]        return CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary).flatMap {        UIImage(cgImage: $0)    }}

异步绘制

系统资源方面,CPU 主要是计算视图层次结构,布局、文本的绘制、图像解码以及 Core Graphics 绘制。GPU 是处理图层合并、图像渲染、动画和 Metal 绘制。CPU 负责准备数据,GPU 负责渲染这些数据。

因此,CPU 方面需要注意过多的子视图会让 CPU 很累,需要简化视图层次。setNeedsDisplay 或 layoutSubviews 也不易过多调用,这样会让重新绘制不断发生。图像解码也不要放主线程。GPU 方面就是图片不要过大,主要是要合适,保持图片在一定分辨率下清晰就好,另外就是可以采用上面提到的大图优化方式让界面更流畅。

UIView 是界面元素的基础,用于响应用户输入,绘制流程是当视图内容或大小变化时会调用 setNeedsDisplay 或 setNeedsLayout 标记为要更新状态,下个循环会调用 drawRect: 进行绘制。绘制是 Core Graphics,也就是 CPU,显示靠的是 Core Animation,用的是 GPU。异步绘制就是将 Core Graphics 的动作放到主线程外,这样主线程就不会收到绘制计算量的影响。

Core Graphics 的异步绘制是使用 UIGraphicsBeginImageContextWithOptions 函数在后台线程中创建一个 CGContext。使用 GCD 或 NSOperationQueue 来在后台线程中进行绘制操作。完成绘制后,将结果返回主线程以更新 UI。

下面是一个异步绘制的示例代码:

import UIKitclass AsyncDrawingView: UIView {        private var asyncImage: UIImage?        override func draw(_ rect: CGRect) {        super.draw(rect)                // 如果有异步绘制的图片,直接绘制它        asyncImage?.draw(in: rect)    }        func drawAsync() {        Task {            // 创建图形上下文            let size = self.bounds.size            UIGraphicsBeginImageContextWithOptions(size, false, 0.0)            guard let context = UIGraphicsGetCurrentContext() else { return }                        // 进行绘制操作            context.setFillColor(UIColor.blue.cgColor)            context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))                        // 获取绘制结果            let image = UIGraphicsGetImageFromCurrentImageContext()            UIGraphicsEndImageContext()                        // 更新 UI,回到主线程            await MainActor.run {                self.asyncImage = image                self.setNeedsDisplay() // 触发 draw(_:) 方法重新绘制            }        }    }}

对于复杂的异步绘制,特别是涉及 UIView 的情况下,可以考虑这两个方法。首先是自定义 CALayer 并实现其 draw(in:) 方法来进行异步绘制。其次是使用 UIView 的 draw(:) 方法,在子类中重写 draw(:) 方法,并结合异步操作来更新绘制内容。

import UIKitclass AsyncDrawingLayer: CALayer {        override func draw(in ctx: CGContext) {        super.draw(in: ctx)                Task {            // 在子线程中执行绘制操作            await withCheckedContinuation { continuation in                Task.detached {                    // 执行绘制操作                    ctx.setFillColor(UIColor.red.cgColor)                    ctx.fill(self.bounds)                                        // 完成绘制操作后继续                    continuation.resume()                }            }                        // 回到主线程更新 UI            await MainActor.run {                self.setNeedsDisplay() // 触发 draw(in:) 重新绘制            }        }    }}

离屏渲染也容易掉帧,应该尽量的避免复杂的圆角、阴影效果,或者使用更简单的图形操作。如可能,减少对 layer 的属性设置,尤其是那些可能引起离屏渲染的属性。

运算转移到 GPU

CPU主要负责用户交互的处理,如果能够将运算转移到 GPU 上,就可以给 CPU 减压了。

以下是一些常见的方法和技术,可以在iOS中将计算任务从CPU转移到GPU:

通过Metal的计算管线(Compute Pipeline),可以编写计算着色器(Compute Shaders)在GPU上执行大量并行计算任务,如物理模拟、数据分析等。

// 使用Metal进行简单的计算操作let device = MTLCreateSystemDefaultDevice()let commandQueue = device?.makeCommandQueue()let shaderLibrary = device?.makeDefaultLibrary()let computeFunction = shaderLibrary?.makeFunction(name: "computeShader")let computePipelineState = try? device?.makeComputePipelineState(function: computeFunction!)

Core Image 是一个强大的图像处理框架,内置了许多优化的滤镜(Filters),并能够自动将图像处理任务分配到GPU上执行。

let ciImage = CIImage(image: inputImage)let filter = CIFilter(name: "CISepiaTone")filter?.setValue(ciImage, forKey: kCIInputImageKey)filter?.setValue(0.8, forKey: kCIInputIntensityKey)let outputImage = filter?.outputImage

Core Animation 是iOS的高效动画框架,它会将大部分动画的执行过程自动转移到GPU上。这包括视图的平移、缩放、旋转、淡入淡出等基本动画效果。通过使用CALayer和各种动画属性(如position、transform等),你可以创建平滑的动画,这些动画将在GPU上硬件加速执行。

let layer = CALayer()layer.position = CGPoint(x: 100, y: 100)let animation = CABasicAnimation(keyPath: "position")animation.toValue = CGPoint(x: 200, y: 200)animation.duration = 1.0layer.add(animation, forKey: "positionAnimation")

SpriteKit 和 SceneKit 是两个高层次的框架,分别用于2D和3D游戏开发。它们内部利用GPU进行图形渲染和物理模拟,极大地减少了CPU的负担。

let scene = SKScene(size: CGSize(width: 1024, height: 768))let spriteNode = SKSpriteNode(imageNamed: "Spaceship")spriteNode.position = CGPoint(x: scene.size.width/2, y: scene.size.height/2)scene.addChild(spriteNode)

线程死锁

线程操作稍不留神就会让主线程卡死,比如dispatch_once中同步访问主线程导致的死锁。子线程占用锁资源导致主线程卡死。dyld lock、selector lock和OC runtime lock互相等待。

同步原语(synchronization primitive)会阻塞读写任务执行。iOS 中常用的会阻塞读写任务执行的同步原语有 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、信号量(Dispatch Semaphore)、屏障(Dispatch Barrier)、读写锁(pthread_rwlock_t)、互斥锁(pthread_mutex_t)、@synchronized 指令os_unfair_lock、原子性属性(Atomic Properties)、NSOperationQueue 和 操作依赖(Dependencies)、Actors。

这些同步原语各有优缺点,选择合适的同步机制取决于具体的应用场景。例如,pthread_rwlock_t适用于读多写少的情况,而NSLock或@synchronized则适用于简单的互斥需求。GCD的信号量和屏障则提供了更高层次的并发控制手段。因此在使用同步原语时要特别注意了。检测卡死情况也要重点从同步原语来入手。

IO 过密

磁盘操作通常是阻塞性的,可以将磁盘 IO 操作放到后台线程中执行。

import SwiftUIstruct ContentView: View {    @State private var data: String = "Loading..." // `data` 用于存储从磁盘读取的数据,并在 UI 中显示。        var body: some View {        VStack {            Text(data)                .padding()            Button("Load Data") {                loadData()            }        }    }        func loadData() {        // 通过 `Task` 创建一个并发上下文来运行异步代码块。在这个代码块中执行耗时的磁盘 IO 操作。        Task {            // 在后台执行磁盘 IO 操作            let loadedData = await performDiskIO()            // 在主线程更新 UI            await MainActor.run {                data = loadedData            }        }    }        // 模拟一个磁盘 IO 操作,可能是从文件中读取大数据    func performDiskIO() async -> String {        // 模拟磁盘操作耗时        try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds delay                // 这里可以进行实际的磁盘读取操作        // 例如读取文件内容:        // let fileURL = ...        // let data = try? String(contentsOf: fileURL)                return "Data Loaded Successfully!"    }}@mainstruct DiskIOApp: App {    var body: some Scene {        WindowGroup {            ContentView()        }    }}

跨进程通信导致卡顿

进程间通信(IPC)是一种重要的机制,它允许不同的进程或应用程序之间交换信息。然而,某些系统API的调用可能会导致卡顿或性能问题,特别是在以下几种情况下:

  • CNCopyCurrentNetworkInfo 获取 WiFi 信息
  • 设置系统钥匙串 (Keychain) 中的值
  • NSUserDefaults 调用写操作
  • CLLocationManager 获取当前位置权限状态
  • UIPasteboard 设置和获取值
  • UIApplication 通过 openURL 打开其他应用

在执行以上操作时,心理上是要有预期的。能有替代方案的话那是最好的了。

卡顿监控

监控原理是注册runloop观察者,检测耗时,记录调用栈,上报后台分析。长时间卡顿后,若未进入下一个活跃状态,则标记为卡死崩溃上报。

以下是一个 iOS 卡死监控的代码示例:

#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>#import <execinfo.h>#import <sys/time.h>// 定义 Runloop 模式的枚举typedef enum {    eRunloopDefaultMode,  // 默认模式    eRunloopTrackingMode  // 追踪模式} RunloopMode;// 全局变量,用于记录 Runloop 的活动状态和模式static CFRunLoopActivity g_runLoopActivity;static RunloopMode g_runLoopMode;static BOOL g_bRun = NO;  // 标记 Runloop 是否在运行static struct timeval g_tvRun;  // 记录 Runloop 开始运行的时间// HangMonitor 类,用于监控卡死情况@interface HangMonitor : NSObject@property (nonatomic, assign) CFRunLoopObserverRef runLoopBeginObserver;  // Runloop 开始观察者@property (nonatomic, assign) CFRunLoopObserverRef runLoopEndObserver;    // Runloop 结束观察者@property (nonatomic, strong) dispatch_semaphore_t semaphore;  // 信号量,用于同步@property (nonatomic, assign) NSTimeInterval timeoutInterval;  // 超时时间- (void)addRunLoopObserver;  // 添加 Runloop 观察者的方法- (void)startMonitor;  // 启动监控的方法- (void)logStackTrace;  // 记录调用栈的方法- (void)reportHang;  // 上报卡死的方法@end@implementation HangMonitor// 单例模式,确保 HangMonitor 只有一个实例+ (instancetype)sharedInstance {    static HangMonitor *instance;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        instance = [[HangMonitor alloc] init];    });    return instance;}// 初始化方法- (instancetype)init {    self = [super init];    if (self) {        _timeoutInterval = 6.0;  // 设置超时时间为6秒        _semaphore = dispatch_semaphore_create(0);  // 创建信号量        [self addRunLoopObserver];  // 添加 Runloop 观察者        [self startMonitor];  // 启动监控    }    return self;}// 添加 Runloop 观察者的方法- (void)addRunLoopObserver {    NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];  // 获取当前 Runloop    // 创建第一个观察者,监控 Runloop 是否处于运行状态    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};    CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);    CFRetain(beginObserver);  // 保留观察者,防止被释放    self.runLoopBeginObserver = beginObserver;    // 创建第二个观察者,监控 Runloop 是否处于睡眠状态    CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);    CFRetain(endObserver);  // 保留观察者,防止被释放    self.runLoopEndObserver = endObserver;    // 将观察者添加到当前 Runloop 中    CFRunLoopRef runloop = [curRunLoop getCFRunLoop];    CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes);    CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes);}// 第一个观察者的回调函数,监控 Runloop 是否处于运行状态void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {    HangMonitor *monitor = (__bridge HangMonitor *)info;    g_runLoopActivity = activity;  // 更新全局变量,记录当前的 Runloop 活动状态    g_runLoopMode = eRunloopDefaultMode;  // 更新全局变量,记录当前的 Runloop 模式    switch (activity) {        case kCFRunLoopEntry:            g_bRun = YES;  // 标记 Runloop 进入运行状态            break;        case kCFRunLoopBeforeTimers:        case kCFRunLoopBeforeSources:        case kCFRunLoopAfterWaiting:            if (g_bRun == NO) {                gettimeofday(&g_tvRun, NULL);  // 记录 Runloop 开始运行的时间            }            g_bRun = YES;  // 标记 Runloop 处于运行状态            break;        case kCFRunLoopAllActivities:            break;        default:            break;    }    dispatch_semaphore_signal(monitor.semaphore);  // 发送信号量}// 第二个观察者的回调函数,监控 Runloop 是否处于睡眠状态void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {    HangMonitor *monitor = (__bridge HangMonitor *)info;    g_runLoopActivity = activity;  // 更新全局变量,记录当前的 Runloop 活动状态    g_runLoopMode = eRunloopDefaultMode;  // 更新全局变量,记录当前的 Runloop 模式    switch (activity) {        case kCFRunLoopBeforeWaiting:            gettimeofday(&g_tvRun, NULL);  // 记录 Runloop 进入睡眠状态的时间            g_bRun = NO;  // 标记 Runloop 进入睡眠状态            break;        case kCFRunLoopExit:            g_bRun = NO;  // 标记 Runloop 退出运行状态            break;        case kCFRunLoopAllActivities:            break;        default:            break;    }    dispatch_semaphore_signal(monitor.semaphore);  // 发送信号量}// 启动监控的方法- (void)startMonitor {    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{        while (YES) {            long result = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, self.timeoutInterval * NSEC_PER_SEC));            if (result != 0) {                if (g_runLoopActivity == kCFRunLoopBeforeSources || g_runLoopActivity == kCFRunLoopAfterWaiting) {                    [self logStackTrace];  // 记录调用栈                    [self reportHang];  // 上报卡死                }            }        }    });}// 记录调用栈的方法- (void)logStackTrace {    void *callstack[128];    int frames = backtrace(callstack, 128);    char **strs = backtrace_symbols(callstack, frames);    NSMutableString *stackTrace = [NSMutableString stringWithString:@"\n"];    for (int i = 0; i < frames; i++) {        [stackTrace appendFormat:@"%s\n", strs[i]];    }    free(strs);    NSLog(@"%@", stackTrace);}// 上报卡死的方法- (void)reportHang {    // 在这里实现上报后台分析的逻辑    NSLog(@"检测到卡死崩溃,进行上报");}@end// 主函数,程序入口int main(int argc, char * argv[]) {    @autoreleasepool {        HangMonitor *monitor = [HangMonitor sharedInstance];  // 获取 HangMonitor 单例        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));  // 启动应用程序    }}

以上代码中 HangMonitor 类会在主线程的 RunLoop 活动中检测是否有长时间的卡顿,并在检测到卡顿时记录调用栈并上报后台进行分析。超时时间设定为 6 秒,以覆盖大部分用户感知场景并减少性能损耗。

内存

引用计数

iOS 中用引用计数(ARC)来管理对象的生命周期。在ARC之前,开发者需要手动管理对象的内存,通过retain、release、autorelease等方法来控制对象的生命周期。SideTables 是一个包含8个 SideTable 的哈希数组,用于存储对象的引用计数和弱引用信息。每个 SideTable 对应多个对象。SideTable 包含三个主要成员:自旋锁(spinlock_t)、引用计数表(RefcountMap)、弱引用表(weak_table_t)。自旋锁用于防止多线程访问冲突,引用计数表存储对象的引用计数,弱引用表存储对象的弱引用信息。weak_table_t 是一个存储弱引用信息的哈希表,其元素是 weak_entry_t 类型。weak_entry_t 存储了弱引用该对象的指针的指针,即objc_object new_referrer。当对象被销毁时,weak引用的指针会被自动置为nil,防止野指针的出现。

当两个类互相持有对方的强引用时,会导致循环引用问题,导致内存无法正确释放,这会造成内存不断的增多。这类问题通常发生在闭包与类实例之间。为了打破这种循环引用,可以在闭包中使用捕获列表(capture list)将闭包中的引用声明为弱引用或无主引用。

import SwiftUIclass Element {    let title: String    let description: String?        lazy var convertToWeb: () -> String = { [unowned self] in        if let description = self.description {            return "<div class='line'><h2>\(self.title)</h2><p>\(description)</p></div>"        } else {            return "<div class='line'><h2>\(self.title)</h2></div>"        }    }        init(title: String, description: String? = nil) {        self.title = title        self.description = description    }        deinit {        print("\(title) is being deinitialized")    }}struct ContentView: View {    @State private var elm: Element? = Element(title: "Inception", description: "A mind-bending thriller by Christopher Nolan.")        var body: some View {        VStack {            if let html = elm?.convertToWeb() {                Text(html)                    .padding()                    .background(Color.yellow)                    .cornerRadius(10)            }            Button("Clear") {                elm = nil            }            .padding()            .background(Color.red)            .foregroundColor(.white)            .cornerRadius(10)        }        .padding()    }}

在这个示例中,convertToWeb 是一个闭包,使用了 [unowned self] 捕获列表,以避免闭包与 Element 实例之间的强引用循环。

Swift 通常通过引用计数和内存自动管理来保证内存安全,然而在某些高性能或特定底层操作中,开发者可能需要直接操作内存。这时就需要使用到 Swift 的 Unsafe 系列指针类型,例如 UnsafeMutablePointerUnsafePointerUnsafePointer 是一个指向某种类型的指针,它允许只读访问内存地址上的数据。这意味着你可以读取该地址的数据但不能修改它。相反,UnsafeMutablePointer 允许你修改指针指向的内存区域内的数据。使用 UnsafeMutablePointer 修改内存时,必须确保内存已经正确地分配且不会被其他代码同时访问。否则,可能会导致程序崩溃或出现难以调试的问题。Swift 提供的一些辅助工具 withUnsafePointer(to:_:)withUnsafeMutablePointer(to:_:),它们可以在有限的范围内确保内存操作的安全性。这些函数的使用可以帮助开发者避免一些常见的错误,确保指针的生命周期和作用域受到控制。

OOM

内存泄漏,难以监控。内存泄漏是指程序在运行过程中,由于设计错误或者代码实现不当,导致程序未能释放已经不再使用的内存,从而造成系统内存的浪费,严重的会导致程序崩溃。内存泄漏是一个非常严重的问题,因为它会导致程序运行速度变慢,甚至会导致程序崩溃。因此,我们在开发过程中,一定要注意内存泄漏的问题。

OOM(Out Of Memory)指的是iOS设备上应用因内存占用过高被系统强制终止的现象。iOS通过Jetsam机制管理内存资源,当设备内存紧张时,会终止优先级低或内存占用大的进程。分为FOOM(前台OOM)和BOOM(后台OOM),FOOM对用户体验影响更大。

Jetsam日志

包括pageSize(内存页大小)、states(应用状态)、rpages(占用的内存页数)、reason(终止原因)。通过pageSize和rpages可计算出应用崩溃时占用的内存大小。

在现代操作系统中,内存管理是一项关键任务。随着移动设备和桌面系统的复杂性增加,内存资源的高效使用变得更加重要。iOS和macOS通过引入“内存压力”(Memory Pressure)机制来优化内存管理,取代了传统的基于虚拟内存分页的管理方法。

虚拟内存系统允许操作系统将物理内存(RAM)和磁盘存储结合使用,以便在内存不足时将不常用的数据移至磁盘。分页(paging)是虚拟内存管理中的一种技术,它将内存划分为小块(页面),并根据需要将它们从物理内存交换到磁盘。然而,分页存在性能瓶颈,尤其是在存储访问速度远低于内存的情况下。

随着设备硬件的变化和用户体验要求的提高,苹果公司在iOS和macOS中引入了“内存压力”机制。内存压力是一种动态监测内存使用情况的技术,它能够实时评估系统内存的使用状态,并根据不同的压力级别采取相应的措施。

内存压力机制通过系统级别的反馈来管理内存。系统会监测内存的使用情况,并将压力分为四个级别:无压力(No Pressure)、轻度压力(Moderate Pressure)、重度压力(Critical Pressure)和紧急压力(Jetsam)。

压力级别的定义与响应:

  • 无压力(No Pressure):系统内存充足,没有特别的内存管理措施。
  • 轻度压力(Moderate Pressure):系统内存开始紧张,操作系统会建议应用程序释放缓存或非必要的资源。
  • 重度压力(Critical Pressure):系统内存非常紧张,操作系统可能会暂停后台任务或终止不活跃的应用程序。
  • 紧急压力(Jetsam):这是最严重的内存压力状态,系统可能会直接强制关闭占用大量内存的应用程序,以释放资源确保系统的稳定性。

系统对内存压力的应对措施

为了应对不同的内存压力,iOS和macOS系统采取了多种策略,包括:

  • 缓存管理:系统会首先清除可丢弃的缓存数据,以减轻内存负担。
  • 后台任务管理:在压力增加时,操作系统会优先暂停或终止低优先级的后台任务。
  • 应用程序终止:在紧急情况下,系统会选择性地关闭那些占用大量内存且当前不活跃的应用程序,这一过程被称为“Jetsam”。

使用系统提供的工具(如vm_statmemory_pressure等)监测应用程序的内存使用情况。这些工具可以帮助开发者识别内存泄漏、过度的缓存使用等问题。开发者可以通过这些机制感知内存压力的变化。例如,当系统发出UIApplicationDidReceiveMemoryWarningNotification通知时,应用程序应立即释放不必要的资源。

查看内存使用情况

在 iOS 中,可以使用 mach_task_basic_info 结构体来查看应用的实际内存使用情况。mach_task_basic_info 是一个 task_info 结构体的子集,它提供了关于任务(进程)的基本信息,包括内存使用情况。特别地,你可以通过 phys_footprint 字段来获取应用程序实际占用的物理内存量。

import Foundationfunc getMemoryUsage() -> UInt64? {    var info = mach_task_basic_info()    var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4        let kret = withUnsafeMutablePointer(to: &info) { infoPtr in        infoPtr.withMemoryRebound(to: integer_t.self, capacity: 1) { intPtr in            task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), intPtr, &count)        }    }        if kret == KERN_SUCCESS {        return info.phys_footprint    } else {        print("Failed to get task info with error code \(kret)")        return nil    }}// Usageif let memoryUsage = getMemoryUsage() {    print("Memory usage: \(memoryUsage / 1024 / 1024) MB")}

在这个示例中,mach_task_basic_info 结构体用于存储基本信息,task_info() 函数用来填充这些信息,phys_footprint 字段提供了物理内存占用的实际数据。使用这些底层 API 需要适当的权限,有时可能无法在应用程序的沙盒环境中访问所有内存信息。

在 iOS 中,NSProcessInfophysicalMemory 属性可以用来获取设备的总物理内存大小。这个属性返回一个 NSUInteger 类型的值,表示物理内存的大小(以字节为单位)。这个方法在 iOS 9 及更高版本中可用。

import Foundationfunc getPhysicalMemorySize() -> UInt64 {    let physicalMemory = ProcessInfo.processInfo.physicalMemory    return physicalMemory}// Usagelet memorySize = getPhysicalMemorySize()print("Total physical memory: \(memorySize / 1024 / 1024) MB")

vm_statistics_data_t 是一个与虚拟内存相关的数据结构,它提供了关于虚拟内存的统计信息,包括系统的内存使用情况。虽然它不能直接提供应用程序使用的内存,但它可以提供有关整个系统的虚拟内存状态的信息。使用 vm_statistics_data_t 可以获取有关系统内存的更详细的统计数据。

import Foundationimport MachOfunc getVMStatistics() -> (freeMemory: UInt64, usedMemory: UInt64)? {    var vmStats = vm_statistics_data_t()    var count = mach_msg_type_number_t(MemoryLayout<vm_statistics_data_t>.size) / 4    var hostPort: mach_port_t = mach_host_self()        let result = withUnsafeMutablePointer(to: &vmStats) { vmStatsPtr in        vmStatsPtr.withMemoryRebound(to: integer_t.self, capacity: 1) { intPtr in            // 用于获取主机的统计信息。通过指定 `HOST_VM_INFO`,可以获取虚拟内存相关的数据。            host_statistics(hostPort, HOST_VM_INFO, intPtr, &count)        }    }        if result == KERN_SUCCESS {        let pageSize = vm_kernel_page_size // 系统的页面大小(通常为 4096 字节)。        let freeMemory = UInt64(vmStats.free_count) * UInt64(pageSize)        let usedMemory = (UInt64(vmStats.active_count) + UInt64(vmStats.inactive_count) + UInt64(vmStats.wire_count)) * UInt64(pageSize)        return (freeMemory, usedMemory)    } else {        print("Failed to get VM statistics with error code \(result)")        return nil    }}// Usageif let vmStats = getVMStatistics() {    print("Free memory: \(vmStats.freeMemory / 1024 / 1024) MB")    print("Used memory: \(vmStats.usedMemory / 1024 / 1024) MB")}

vm_statistics_data_t 数据结构包含了有关虚拟内存的统计信息,如 free_count(自由页数)、active_count(活跃页数)、inactive_count(非活跃页数)和 wire_count(被锁定的页数)。

获取可用内存的方法如下:

import Foundationimport MachOfunc getAvailableMemory() -> UInt64? {    var vmStats = vm_statistics_data_t()    var count = mach_msg_type_number_t(MemoryLayout<vm_statistics_data_t>.size) / 4    var hostPort: mach_port_t = mach_host_self()        let result = withUnsafeMutablePointer(to: &vmStats) { vmStatsPtr in        vmStatsPtr.withMemoryRebound(to: integer_t.self, capacity: 1) { intPtr in            host_statistics(hostPort, HOST_VM_INFO, intPtr, &count)        }    }        if result == KERN_SUCCESS {        let pageSize = vm_kernel_page_size        let freeMemory = UInt64(vmStats.free_count) * UInt64(pageSize)        let inactiveMemory = UInt64(vmStats.inactive_count) * UInt64(pageSize)        return freeMemory + inactiveMemory    } else {        print("Failed to get VM statistics with error code \(result)")        return nil    }}// Usageif let availableMemory = getAvailableMemory() {    print("Available memory: \(availableMemory / 1024 / 1024) MB")}

free_count 表示系统中未使用的空闲内存页数。inactive_count 表示系统中未使用但可能会重新使用的内存页数。可用内存可以通过将空闲内存和非活跃内存的页数乘以页面大小来计算得到。

造成内存泄漏的常见原因

内存泄漏指的是程序中已动态分配的堆内存由于某些原因未能释放或无法释放,导致系统内存浪费,程序运行速度变慢甚至系统崩溃。

  • 循环引用:对象A强引用对象B,对象B又强引用对象A,或多个对象互相强引用形成闭环。使用Weak-Strong Dance、断开持有关系(如使用__block关键字、将self作为参数传入block)。
  • Block导致的内存泄漏:Block会对其内部的对象强引用,容易形成循环引用。使用Weak-Strong Dance、断开持有关系(如将self作为参数传入block)。
  • NSTimer导致的内存泄漏:NSTimer的target-action机制容易导致self与timer之间的循环引用。在合适的时机销毁NSTimer、使用GCD的定时器、借助中介者(如NSObject对象或NSProxy子类)断开循环引用、使用iOS 10后提供的block方式创建timer。
  • 委托模式中的内存泄漏:UITableView的delegate和dataSource、NSURLSession的delegate。根据具体场景选择使用weak或strong修饰delegate属性,或在请求结束时手动销毁session对象。
  • 非OC对象的内存管理:CoreFoundation框架下的对象(如CI、CG、CF开头的对象)和C语言中的malloc分配的内存。使用完毕后需手动释放(如CFRelease、free)。

Metrics

Metrics 和 XCTest 中的 memgraph 了解和诊断 Xcode 的内存性能问题。

内存泄漏检测工具原理

内存泄漏指的是程序在运行过程中,分配的内存未能及时释放,导致程序占用的内存持续增加。内存泄漏检测工具的基本原理是监控和管理对象的生命周期,检测那些在生命周期结束后仍未被释放的对象。

FBRetainCycleDetector

FBRetainCycleDetector 是由 Facebook 开源的一个用于检测 iOS 应用中的内存泄漏的工具。内存泄漏通常是由于对象之间的强引用循环导致的,FBRetainCycleDetector 的工作原理就是检测对象图中的强引用循环,进而帮助开发者识别和修复这些泄漏。

FBRetainCycleDetector 的核心思想是通过分析对象之间的引用关系来识别可能的循环引用。它通过以下步骤实现这一点:

  • 对象图构建FBRetainCycleDetector 首先会从一个指定的对象开始,递归地遍历该对象的所有属性和关联对象,构建一个引用图。这个图的节点是对象,边是对象之间的强引用。
  • **深度优先搜索 (DFS)**:在构建完对象图之后,FBRetainCycleDetector 会对图进行深度优先搜索,寻找从起始对象到自身的循环路径。换句话说,它会查找路径起始和终止于同一个对象的闭环。
  • 循环检测:当找到一个循环路径时,FBRetainCycleDetector 就会将其标记为潜在的内存泄漏。检测到的循环会以易于理解的方式输出,帮助开发者定位和解决问题。

为了避免不必要的检测,FBRetainCycleDetector 允许开发者定义一些属性过滤规则,忽略一些不会导致泄漏的引用。例如,可以跳过一些不可见的系统属性或自定义的非持有性引用。工具能够识别并忽略弱引用(weakunowned),因为这些引用不会导致内存泄漏。FBRetainCycleDetector 具有较高的灵活性,开发者可以通过扩展和定制对象图的遍历规则,使其适应不同的应用场景和复杂对象结构。由于对象图的遍历和循环检测可能会带来性能开销,FBRetainCycleDetector 主要用于开发和调试阶段,而不建议在生产环境中长期使用。

通常,FBRetainCycleDetector 会在调试时被使用。开发者可以通过简单的代码调用,检测指定对象是否存在循环引用。例如:

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];[detector addCandidate:someObject];NSSet *retainCycles = [detector findRetainCycles];

通过以上代码,可以查找someObject 是否存在循环引用,并返回检测到的循环路径。

在实际应用中,FBRetainCycleDetector 被广泛用于检测复杂的对象之间的引用关系,特别是在自定义控件、大型视图控制器、网络回调等场景下,容易产生强引用循环的问题。通过早期检测和解决这些循环引用,可以大大提高应用的内存管理效率,减少内存泄漏带来的问题。

MLeaksFinder

MLeaksFinder 是一款由腾讯 WeRead 团队开源的 iOS 内存泄漏检测工具,其原理主要基于对象生命周期的监控和延迟检测机制。

MLeaksFinder 通过为基类 NSObject 添加一个 -willDealloc 方法来监控对象的生命周期。当对象应该被释放时(例如,ViewController 被 pop 或 dismiss 后),该方法被调用。在 -willDealloc 方法中,MLeaksFinder 使用一个弱指针(weak pointer)指向待检测的对象,以避免因为对象已经被释放而导致的野指针访问问题。MLeaksFinder 通过检查视图控制器的生命周期来检测内存泄漏。每个 UIViewController 都有一个 viewDidDisappear 方法,这个方法会在视图控制器从屏幕上消失时被调用。MLeaksFinder 通过在 viewDidDisappear 被调用时,检测该视图控制器是否已经被释放,如果没有被释放则认为存在内存泄漏。对于视图 (UIView),MLeaksFinder 会在视图被从其父视图中移除时(即 removeFromSuperview 调用后)检查视图是否已经被释放。如果视图没有被释放,则认为存在内存泄漏。MLeaksFinder 通过扩展 NSObject 的功能(即为 NSObject 添加一个 Category)来追踪对象的生命周期。当对象的 dealloc 方法没有在预期的时间内被调用时,就可以判断该对象是否泄漏。

-willDealloc 方法中,MLeaksFinder 使用 dispatch_after 函数在 GCD(Grand Central Dispatch)的主队列上设置一个延迟(通常是2到3秒)执行的 block。这个 block 在延迟时间后执行,尝试通过之前设置的弱指针访问对象。如果对象已经被释放(即弱指针为 nil),则认为没有内存泄漏;如果对象仍然存活,则认为存在内存泄漏。MLeaksFinder 通过将对象的检测任务加入到下一个 Runloop 中执行,从而避免在当前线程中直接执行检测操作。这种方式确保了不会影响主线程的性能,同时能在适当的时间进行内存泄漏的检测。

如果在延迟时间后对象仍然存活,MLeaksFinder 会执行相应的检测逻辑,并可能通过断言(assertion)中断应用(具体行为可能根据配置和版本有所不同)。MLeaksFinder 会在应用运行时自动检测内存泄漏,不需要开发者手动触发。检测到内存泄漏后,MLeaksFinder 通常会弹出警告框(alert)或通过日志(log)输出相关信息,帮助开发者定位和解决内存泄漏问题。

MLeaksFinder 使用了方法交换技术替换如dismissViewControllerAnimated:completion:等方法,确保释放时触发检测。调用willDealloc方法,设置延时检查对象是否已释放。若未释放,则进入assertNotDealloc方法,中断言提醒开发者。

当 MLeaksFinder 检测到潜在的内存泄漏时,它还可以打印堆栈信息,帮助开发者找出导致对象无法释放的具体代码路径。通过willReleaseChildwillReleaseChildren方法构建子对象的释放堆栈信息。这通常通过递归遍历子对象,并将父对象和子对象的类名组合成视图堆栈(view stack)来实现。

MLeaksFinder 还可能集成了循环引用检测功能,使用如 Facebook 的 FBRetainCycleDetector 这样的工具来找出由 block 等造成的循环引用问题。MLeaksFinder 提供了一种白名单机制,允许开发者将一些特定的对象排除在泄漏检测之外。这在某些对象确实需要持久存在的场景下非常有用。MLeaksFinder 非常轻量,不会显著影响应用的性能。集成简单,自动化检测,极大地方便了开发者发现内存泄漏问题。在某些复杂的情况下,可能会有误报(即认为对象泄漏了,但实际上没有)。

PLeakSniffer

PLeakSniffer是一个用于检测iOS应用程序中内存泄漏的工具。PLeakSniffer的基本工作原理:通过对控制器和视图对象设置弱引用,并使用单例对象周期性地发送ping通知,如果对象在控制器已释放的情况下仍然响应通知,则可能存在内存泄漏。

PLeakSnifferCitizen协议的设计及其在NSObjectUIViewControllerUINavigationControllerUIView中的实现。每个类都通过实现prepareForSniffer方法来挂钩适当的生命周期方法(如viewDidAppearpushViewController等),在适当的时机调用markAlive方法,将代理对象附加到被监测的对象上,以便后续的ping操作能够检测到对象的存活状态。

代理对象PObjectProxy的功能,它主要负责接收ping通知并检查宿主对象是否应当被释放,如果检测到可能的内存泄漏,就会触发警报或打印日志。通过这种方式,PLeakSniffer能够在运行时检测到iOS应用中可能存在的内存泄漏问题。

其他内存泄漏检测工具

hook malloc方法

要在 iOS 上 hook malloc 方法可以监控内存分配。可以使用函数拦截技术。以下是一个示例,展示如何使用 Fishhook 库来 hook malloc 方法。

将 Fishhook 库添加到你的项目中。你可以通过 CocoaPods 或手动添加 Fishhook 源代码。

#import <Foundation/Foundation.h>#import <malloc/malloc.h>#import "fishhook.h"// 原始 malloc 函数指针static void* (*original_malloc)(size_t size);// 自定义 malloc 函数void* custom_malloc(size_t size) {    void *result = original_malloc(size);    NSLog(@"Allocated %zu bytes at %p", size, result);    return result;}// Hook 函数void hookMalloc() {    // 重新绑定 malloc 函数    rebind_symbols((struct rebinding[1]){{"malloc", custom_malloc, (void *)&original_malloc}}, 1);}int main(int argc, const char * argv[]) {    @autoreleasepool {        // Hook malloc        hookMalloc();                // 测试 malloc 和 free        void *ptr = malloc(1024);        free(ptr);    }    return 0;}

在实际项目中使用时,注意性能开销和日志记录的影响。

malloc logger

malloc_logger 是 iOS 和 macOS 中用于内存分配调试的一个工具。它允许开发者设置一个自定义的日志记录器函数,以便在内存分配和释放操作发生时记录相关信息。通过使用 malloc_logger,开发者可以更容易地检测和诊断内存问题,如内存泄漏、过度分配等。

以下是一个使用 Objective-C 实现的示例,展示如何设置和使用 malloc_logger

#import <Foundation/Foundation.h>#import <malloc/malloc.h>// 定义自定义的 malloc logger 函数void custom_malloc_logger(uint32_t type, uintptr_t arg1, uintptr_t arg2, uintptr_t result, uintptr_t num_bytes) {    switch (type) {        case MALLOC_LOG_TYPE_ALLOCATE:            NSLog(@"Allocated %lu bytes at %p", (unsigned long)num_bytes, (void *)result);            break;        case MALLOC_LOG_TYPE_DEALLOCATE:            NSLog(@"Deallocated memory at %p", (void *)arg1);            break;        case MALLOC_LOG_TYPE_HAS_ZONE:            NSLog(@"Memory operation with zone at %p", (void *)arg1);            break;        default:            break;    }}// 设置自定义的 malloc loggervoid setCustomMallocLogger() {    malloc_logger = custom_malloc_logger;}int main(int argc, const char * argv[]) {    @autoreleasepool {        // 设置自定义 malloc logger        setCustomMallocLogger();                // 测试 malloc 和 free        void *ptr = malloc(1024);        free(ptr);    }    return 0;}

在这个示例中,我们定义了一个自定义的 malloc_logger 函数 custom_malloc_logger,并在 setCustomMallocLogger 函数中将其设置为当前的 malloc_logger。然后,在 main 函数中,我们测试了内存的分配和释放操作,并通过日志记录器记录这些操作的信息。

通过这种方式,开发者可以在内存分配和释放时记录相关信息,从而更好地理解和优化应用程序的内存使用情况。

内存快照检测方案

扫描进程中所有Dirty内存,建立内存节点之间的引用关系有向图,用于内存问题的分析定位。

在 iOS 中,可以使用 vm_region_recurse_64 函数来获取所有内存区域的信息。

#include <stdio.h>  #include <stdlib.h>  #include <mach/mach.h>  #include <mach/vm_map.h>    int main(int argc, const char * argv[]) {      mach_port_t task = mach_task_self();      vm_address_t address = VM_MIN_ADDRESS;      vm_size_t size = VM_MAX_ADDRESS - VM_MIN_ADDRESS;      vm_region_basic_info_data_64_t info;      mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64;      memory_object_name_t object_name;      mach_port_t object_handle;        kern_return_t kr;        while (size > 0) {          kr = vm_region_recurse_64(task, &address, &size, VM_REGION_BASIC_INFO,                                    (vm_region_info_t)&info, &info_count, &object_name,                                    &object_handle);            if (kr != KERN_SUCCESS)              break;            printf("Address: 0x%llx, Size: 0x%llx, Protection: 0x%x, In Use: %s\n",                 (unsigned long long)info.protection,                 (unsigned long long)info.size,                 (unsigned int)info.protection,                 info.is_submap ? "Yes" : "No");            address += info.size;          size -= info.size;      }        if (kr != KERN_SUCCESS) {          char *err = mach_error_string(kr);          fprintf(stderr, "vm_region_recurse_64 failed: %s\n", err);          free(err);      }        return 0;  }

在iOS中,可以使用libmalloc库提供的malloc_get_all_zones函数来获取所有内存区域(zone)的信息。malloc_get_all_zones可以遍历所有的内存区域,并为每个区域执行一个回调函数,从而获取详细的内存分配信息。

以下是一个简单的代码示例,展示如何使用malloc_get_all_zones来获取并打印内存区域的信息:

#import <malloc/malloc.h>#import <mach/mach.h>// 自定义的回调函数,用于处理每个内存区域的块。该函数用于处理每个zone中的内存块,在这个例子中,它简单地打印出每个内存块的地址和大小。void my_zone_enumerator(task_t task, void *context, unsigned type_mask, vm_range_t *ranges, unsigned range_count) {    for (unsigned i = 0; i < range_count; i++) {        printf("Memory range: 0x%llx, Size: %llu\n", ranges[i].address, ranges[i].size);    }}void print_all_zones() {    // 获取当前任务的mach port。用于获取当前任务的Mach端口,这对于与Mach内核通信是必需的。    task_t task = mach_task_self();    unsigned int count;    // 这是`libmalloc`库中的一个结构体,表示内存区域。通过调用其`introspect`属性下的`enumerator`函数,可以遍历该zone中的所有内存块。    malloc_zone_t **zones = NULL;    // 获取所有的内存区域。这个函数返回当前任务的所有内存区域(zone),这些zone通常对应于不同的分配器或内存池。    kern_return_t kr = malloc_get_all_zones(task, NULL, &zones, &count);    if (kr != KERN_SUCCESS) {        fprintf(stderr, "Error: Unable to get all zones\n");        return;    }    // 遍历所有的zone    for (unsigned int i = 0; i < count; i++) {        malloc_zone_t *zone = zones[i];        if (zone != NULL) {            printf("Zone name: %s\n", zone->zone_name);            // 枚举zone中的内存块            zone->introspect->enumerator(task, NULL, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, my_zone_enumerator);        }    }}int main(int argc, const char * argv[]) {    print_all_zones();    return 0;}

使用单独的 malloc_zone 管理采集模块的内存使用,减少非法内存访问。遍历进程内所有VM Region(虚拟内存区域),获取Dirty和Swapped内存页数。重点关注libmalloc管理的堆内存,获取存活内存节点的指针和大小。

为内存节点赋予详细的类型名称,如Objective-C/Swift/C++实例类名等。通过运行时信息和mach-o、C++ ABI文档获取C++对象的类型信息。遍历内存节点,搜索并确认节点间的引用关系。对栈内存和Objective-C/Swift堆内存进行特殊处理,获取更详细的引用信息。

后台线程定时检测内存占用,超过设定的危险阈值后触发内存分析。内存分析过程中,对内存节点进行引用关系分析,生成内存节点之间的引用关系有向图。通过图算法,找到内存泄漏的根原因。

libmalloc 内存日志分析

通过代码控制内存日志开关,可以在内存泄漏发生时,输出内存日志。内存日志包括内存分配、释放、引用计数变化等信息,用于分析内存泄漏的原因。

在 iOS 开发中,libmalloc 提供了 turn_on_stack_loggingturn_off_stack_logging 方法,用于启用和禁用堆栈日志记录。这些方法可以帮助开发者在调试和分析内存问题时记录内存分配的堆栈信息。以下是一个使用这些方法的代码示例:

#import <Foundation/Foundation.h>#import <malloc/malloc.h>#import <mach/mach.h>#import <mach/mach_init.h>#import <mach/mach_vm.h>// 启用堆栈日志记录void enableStackLogging() {    turn_on_stack_logging(1);    NSLog(@"Stack logging turned on");}// 禁用堆栈日志记录void disableStackLogging() {    turn_off_stack_logging();    NSLog(@"Stack logging turned off");}// 获取堆栈日志记录void getStackLoggingRecords() {    // 获取当前任务    task_t task = mach_task_self();        // 获取所有堆栈日志记录    mach_vm_address_t *records;    uint32_t count;    kern_return_t kr = __mach_stack_logging_enumerate_records(task, &records, &count);        if (kr != KERN_SUCCESS) {        NSLog(@"Failed to enumerate stack logging records: %s", mach_error_string(kr));        return;    }        for (uint32_t i = 0; i < count; i++) {        mach_vm_address_t record = records[i];        NSLog(@"Record %u: %p", i, (void *)record);                // 定义堆栈帧数组        uint64_t frames[128];        // 获取堆栈帧信息        uint32_t frameCount = __mach_stack_logging_frames_for_uniqued_stack(task, record, frames, 128);                // 遍历堆栈帧,每次循环中,获取当前堆栈帧地址并打印地址信息        for (uint32_t j = 0; j < frameCount; j++) {            NSLog(@"Frame %u: %p", j, (void *)frames[j]);        }    }        // 释放记录数组    vm_deallocate(task, (vm_address_t)records, count * sizeof(mach_vm_address_t));}// 示例函数,分配一些内存void allocateMemory() {    void *ptr1 = malloc(1024);    void *ptr2 = malloc(2048);    free(ptr1);    free(ptr2);}// 主函数int main(int argc, const char * argv[]) {    @autoreleasepool {        // 启用堆栈日志记录        enableStackLogging();                // 分配内存        allocateMemory();                // 获取堆栈日志记录        getStackLoggingRecords();                // 禁用堆栈日志记录        disableStackLogging();    }    return 0;}

在这个示例中,我们首先调用 turn_on_stack_logging 方法来启用堆栈日志记录,然后进行一些内存分配和释放操作。接着,我们调用 __mach_stack_logging_enumerate_records 方法获取所有堆栈日志记录,并使用 __mach_stack_logging_frames_for_uniqued_stack 方法解析每个日志记录以获取堆栈帧信息。最后,我们调用 turn_off_stack_logging 方法来禁用堆栈日志记录。

通过这种方式,开发者可以在需要时启用和禁用堆栈日志记录,并解析这些日志记录以获取详细的堆栈信息。需要注意的是,这些函数在实际项目中使用时,需要确保在合适的时机启用和禁用堆栈日志记录,以避免性能开销和不必要的日志记录。

IO 性能

文件写操作常见但易出错。常见问题包括数据不一致、数据丢失、性能波动等。

读写的 API

文件读写系统调用的 API 有 read()write()read()从文件读取数据到应用内存。write()将数据从应用内存写入文件到内核缓存,但不保证立即写入磁盘。mmap()将文件映射到应用内存,直接访问,但写操作同样先进入内核缓存。fsync()fcntl(F_FULLSYNC) 会强制将文件写入磁盘。c标准库提供的文件读写 API 是 fwrite(buffer, sizeof(char), size, file_pointer)fflush(file_pointer)

iOS 提供了 NSFileManagerreplaceItemAtURL:withItemAtURL:backupItemName:options:resultingItemURL:error: 方法,可以实现原子性操作。

flockfcntl 使用文件锁防止多个进程或线程同时写入同一个文件,避免产生竞争条件,保证数据一致性。

iOS 提供了 NSFileManagerNSData 的封装方法,通常比直接使用 POSIX API 更安全和高效。

测试文件I/O性能时,应通过 fcntl(fd, F_NOCACHE, 1) 禁用统一缓冲缓存(UBC),以避免缓存影响测试结果。

文件缓存

文件缓存可以帮助优化应用性能、减少网络请求和延长电池续航。

iOS 提供了多个文件存储目录,选择合适的目录有助于管理缓存文件的生命周期。包括Caches 目录和tmp 目录。Caches 目录适合存储缓存文件。系统可能会在磁盘空间紧张时清除这个目录下的文件,因此不应存储重要数据。可以通过 NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) 获取路径。tmp 目录适用于临时文件。系统重启或应用未运行时,可能会清除这个目录下的文件。可以通过 NSTemporaryDirectory() 获取路径。

根据数据的重要性和更新频率,制定缓存策略。为缓存数据设置时间戳或过期时间。每次读取缓存时检查数据是否过期,及时更新。实现 LRU 算法,定期清理最久未使用的缓存文件。

为缓存文件生成唯一标识符(如使用哈希值),避免文件名冲突。可以将 URL 的 MD5 或 SHA1 哈希值作为缓存文件名。将缓存文件按类别或特定属性进行分类存储,方便管理。例如,将图片和JSON数据分别存储在不同的子目录中。

对于大型缓存数据,可以在写入文件时使用 GZIP 等压缩技术,减少存储空间占用。iOS 的 NSDataNSFileManager 支持数据的压缩和解压缩。避免在主线程上执行缓存读写操作,使用 Swift Concurrency 将缓存操作移到后台,保持 UI 的流畅性。减少频繁的写入操作,可以将多次写入合并为一次批量操作。

对于敏感数据(如用户信息),应在缓存时进行加密处理。iOS 提供了 Keychain 进行安全存储,也可以使用 CommonCrypto 框架进行自定义加密。

定期清理过期或不再使用的缓存文件,避免占用过多磁盘空间。可以使用 iOS 的 NSURLCache 设置缓存大小限制,自动管理缓存清理。提供手动清理缓存的选项,允许用户在应用内清理缓存数据。根据数据更新频率设置缓存失效时间,确保用户获得最新数据。可以通过 ETag 或 Last-Modified HTTP 头实现增量更新,避免每次都下载完整数据。尽量利用 iOS 自带的缓存机制,例如 NSURLCache,它自动管理 HTTP 请求的缓存,支持内存和磁盘缓存。对于图片缓存,使用 NSCache 或者第三方库,可以在内存和磁盘之间自动管理图片的缓存。

mmap

mmap 是一种内存映射文件的机制,允许用户态的程序像操作内存一样直接操作磁盘文件。通过 mmap,文件的内容被映射到进程的地址空间中,程序可以直接读写这段地址空间,操作系统会在背后处理实际的磁盘读写操作。标准IO(如read/write)涉及系统调用和内存拷贝开销,数据需要在内核态和用户态之间来回拷贝。mmap 避免了这些开销,因为它直接在用户态的内存中操作,操作系统只在需要时(如缺页中断)介入处理磁盘读写。

对于超过物理内存大小的大文件,mmap 可以利用虚拟内存的特性,在有限的物理内存中处理大文件。多个进程可以映射同一个文件到各自的地址空间,实现内存共享,这在动态链接库等场景中非常有用。在某些场景下,mmap 可以提供更好的性能,因为它减少了系统调用和内存拷贝的次数。但具体性能取决于应用场景和操作系统实现。在处理大文件时,mmap 可以避免频繁的内存拷贝和磁盘I/O操作。多个进程可以共享同一个动态链接库,节省内存和磁盘空间。可用于实现高效的内存文件交换,如数据库中的内存映射文件。

mmap 也有些问题需要注意。当访问的页面不在物理内存中时,会发生缺页中断,这会有一定的性能开销。为了维护地址空间与文件的映射关系,内核需要额外的数据结构,这也会带来一定的性能开销。

我们使用 mmap 将文件映射到内存中,并读取文件内容。示例如下:

#import <Foundation/Foundation.h>#import <sys/mman.h>#import <fcntl.h>#import <unistd.h>void mmapExample() {    // 文件路径    NSString *filePath = @"/path/to/your/file.txt";        // 打开文件    int fd = open([filePath UTF8String], O_RDONLY);    if (fd == -1) {        NSLog(@"Failed to open file");        return;    }        // 获取文件大小    off_t fileSize = lseek(fd, 0, SEEK_END);    if (fileSize == -1) {        NSLog(@"Failed to get file size");        close(fd);        return;    }        // 将文件映射到内存    void *mappedFile = mmap(NULL, fileSize, PROT_READ, MAP_PRIVATE, fd, 0);    if (mappedFile == MAP_FAILED) {        NSLog(@"Failed to map file");        close(fd);        return;    }        // 关闭文件描述符    close(fd);        // 读取文件内容    NSData *fileData = [NSData dataWithBytes:mappedFile length:fileSize];    NSString *fileContent = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];    NSLog(@"File content: %@", fileContent);        // 解除文件映射    if (munmap(mappedFile, fileSize) == -1) {        NSLog(@"Failed to unmap file");    }}int main(int argc, const char * argv[]) {    @autoreleasepool {        mmapExample();    }    return 0;}

MMKV 是腾讯开源的一个高性能通用键值对存储库,基于 mmap 内存映射机制,它提供了简单易用的接口,支持高效的读写操作,并且支持数据加密。

以下是一个在 iOS 项目中使用 MMKV 的示例代码:

import UIKitimport MMKV@UIApplicationMainclass AppDelegate: UIResponder, UIApplicationDelegate {    var window: UIWindow?    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        // 初始化 MMKV        MMKV.initialize(rootDir: MMKV.defaultMMKVPath)        return true    }}

使用 MMKV 存储和读取数据

import MMKVfunc mmkvExample() {    // 获取默认的 MMKV 实例    let mmkv = MMKV.default()    // 存储数据    mmkv?.set("Inception", forKey: "movieTitle")    mmkv?.set(8.8, forKey: "movieRating")    // 读取数据    if let movieTitle = mmkv?.string(forKey: "movieTitle") {        print("Movie Title: \(movieTitle)")    }    let movieRating = mmkv?.double(forKey: "movieRating")    print("Movie Rating: \(movieRating ?? 0.0)")}mmkvExample()

NSData 提供了三个与 mmap 相关的读取选项,它们分别是:

  • NSDataReadingUncached:这个选项表示不要缓存数据,如果文件只需要读取一次,使用这个选项可以提高性能。这个选项与 mmap 没有直接关系,因为它不涉及内存映射。
  • NSDataReadingMappedIfSafe:这个选项表示在保证安全的前提下,如果条件允许,则使用 mmap 进行内存映射。这意味着如果文件位于固定磁盘(非可移动磁盘或网络磁盘),则可能会使用 mmap 来优化读取性能。
  • NSDataReadingMappedAlways:这个选项表示总是使用 mmap 进行内存映射,不考虑文件的具体存储位置。但是,在 iOS 上,由于所有应用都运行在沙盒中,对 iOS 而言,NSDataReadingMappedIfSafeNSDataReadingMappedAlways 通常是等价的,因为 iOS 设备上的文件存储通常都是在固定磁盘上。

当你需要读取一个较大的文件,但又不想一次性将整个文件加载到内存中时,可以使用 NSDatadataWithContentsOfFile:options:error: 方法,并传入上述与 mmap 相关的选项之一。以下是一个示例代码,展示了如何使用 NSDataReadingMappedIfSafe 选项来读取文件:

NSError *error = nil;NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];if (data == nil) {    // 处理错误    NSLog(@"Error reading file: %@", error.localizedDescription);} else {    // 成功读取文件,可以处理 data}

在这个例子中,filePath 是你想要读取的文件的路径。通过使用 NSDataReadingMappedIfSafe,系统会在可能的情况下使用 mmap 来映射文件,这样就不需要在内存中为整个文件分配空间,从而减少了内存的使用。然而,需要注意的是,虽然 mmap 减少了物理内存的使用,但它仍然需要消耗虚拟内存地址空间。

在用 mmap 时要注意如果使用 mmap 映射了文件,那么在 NSData 的生命周期内,你不能删除或修改对应的文件,因为这可能会导致内存映射失效,进而引发不可预见的错误。mmap 适用于那些需要频繁读取、但不需要同时读取整个文件内容的场景,如视频加载、大日志文件读取等。mmap 映射的区域大小会占用相应大小的虚拟内存地址空间,因此对于非常大的文件,可能不适合将整个文件映射到内存中。

CPU

CPU 的高占用,会让手机耗电变快。

[NSProcessInfo processInfo].activeProcessorCount 可以获取 CPU 核数。获取 CPU 类型的方法有 sysctl、uname、hw.machine 和 NXArchInfo 几种方法。

怎么获取 CPU 使用率呢?

在 iOS 的 Mach 层中,thread_basic_info 结构体用于提供有关线程的一些基本信息,其中就有线程CPU使用率。这个结构体定义在 <mach/thread_info.h> 头文件中,其包含的字段提供了关于线程运行状态、执行时间和其他统计信息的基本数据。以下是 thread_basic_info 结构体的详细定义及其各字段的解释:

struct thread_basic_info {    time_value_t    user_time;       // 用户模式下线程运行的总时间    time_value_t    system_time;     // 内核模式下线程运行的总时间    integer_t       cpu_usage;       // CPU 使用率,以百分之一为单位    policy_t        policy;          // 调度策略(例如FIFO、Round Robin等)    integer_t       run_state;       // 线程的运行状态    integer_t       flags;           // 线程的标志位(例如是否正在被调度)    integer_t       suspend_count;   // 线程被挂起的次数    integer_t       sleep_time;      // 线程的睡眠时间};

字段解释

  • user_time: 该字段表示线程在用户模式下(即执行用户空间的代码)运行的总时间。time_value_t 是一个结构体,通常表示为秒和微秒。
  • system_time: 该字段表示线程在系统模式下(即执行内核空间的代码)运行的总时间。
  • cpu_usage: 该字段表示线程的 CPU 使用率,以百分之一为单位。例如,如果值为 100,表示线程使用了 1% 的 CPU 时间。
  • policy: 该字段表示线程的调度策略,如固定优先级调度(FIFO)或轮转调度(Round Robin)等。
  • run_state: 该字段表示线程当前的运行状态。可能的值包括:
    • TH_STATE_RUNNING: 正在运行
    • TH_STATE_STOPPED: 已停止
    • TH_STATE_WAITING: 正在等待资源
    • TH_STATE_UNINTERRUPTIBLE: 不可中断的等待
    • TH_STATE_HALTED: 已终止
  • flags: 该字段包含一些线程的标志位,用来表示线程的某些状态特性。例如,线程是否正在被调度等。
  • suspend_count: 该字段表示线程当前被挂起的次数。挂起次数大于 0 时,线程不会被调度执行。
  • sleep_time: 该字段表示线程处于睡眠状态的时间。

这些信息对于性能分析、调试以及获取系统中线程的运行状况非常有用。通过使用 thread_info 函数,可以获取到某个特定线程的 thread_basic_info 结构体实例。

要获取当前应用的 CPU 占用率,可以通过遍历当前应用的所有线程,利用 thread_info 函数获取每个线程的 CPU 使用情况。然后,将所有线程的 CPU 使用率汇总,就能得到整个应用的 CPU 占用率。

下面是一个使用 Objective-C 编写的示例代码,展示了如何获取当前应用的 CPU 占用率:

#import <mach/mach.h>#import <assert.h>float cpu_usage() {    kern_return_t kr;    thread_array_t thread_list;    mach_msg_type_number_t thread_count;    thread_info_data_t thread_info_data;    mach_msg_type_number_t thread_info_count;        // 获取当前任务    task_t task = mach_task_self();        // task_threads 这个函数用于获取当前任务的所有线程。`thread_list` 包含了所有线程的 ID,`thread_count` 是线程的数量。    kr = task_threads(task, &thread_list, &thread_count);    if (kr != KERN_SUCCESS) {        return -1;    }        float total_cpu = 0;        // 遍历所有线程    for (int i = 0; i < thread_count; i++) {        thread_info_count = THREAD_INFO_MAX;                // 通过 thread_info 获取每个线程的 `thread_basic_info`,其中包含了线程的 CPU 使用信息。        kr = thread_info(thread_list[i], THREAD_BASIC_INFO, (thread_info_t)thread_info_data, &thread_info_count);        if (kr != KERN_SUCCESS) {            return -1;        }                thread_basic_info_t thread_info = (thread_basic_info_t)thread_info_data;                if (!(thread_info->flags & TH_FLAGS_IDLE)) {            // 通过 `thread_basic_info` 结构体中的 `cpu_usage` 字段获取每个线程的 CPU 使用率,并将它们相加以得到整个应用的 CPU 使用率。            total_cpu += thread_info->cpu_usage / (float)TH_USAGE_SCALE * 100.0;        }    }        // 用于释放之前分配的线程列表内存。    kr = vm_deallocate(task, (vm_address_t)thread_list, thread_count * sizeof(thread_t));    assert(kr == KERN_SUCCESS);        return total_cpu;}

CPU 占用率是一个瞬时值,通常会波动,因此在实际应用中,可能需要多次采样并取平均值来得到更稳定的结果。这个方法会占用一定的 CPU 资源,尤其是在应用包含大量线程时,所以建议在非主线程或低优先级任务中执行这类操作。

对于总 CPU 占用率,使用 host_statistics 函数获取 host_cpu_load_info 结构体中的 cpu_ticks 值来计算总的 CPU 占用率。cpu_ticks 是一个数组,包含了 CPU 在各种状态(如用户模式、系统模式、空闲、Nice 等)下运行的时钟脉冲数量。通过计算这些脉冲数量的变化,可以得出总的 CPU 占用率。

以下是一个完整的示例代码,展示了如何使用 host_statistics 函数来计算总的 CPU 占用率:

#import <mach/mach.h>#import <stdio.h>float cpu_usage() {    // 获取 host 的 CPU load 信息    host_cpu_load_info_data_t cpuInfo;    mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT;    // `host_statistics` 这是一个用于获取主机统计信息的函数。通过传递 `HOST_CPU_LOAD_INFO` 作为参数,可以获取 `host_cpu_load_info_data_t` 结构体,该结构体包含了 CPU 在不同状态下的时钟脉冲数。    kern_return_t kr = host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, (host_info_t)&cpuInfo, &count);        if (kr != KERN_SUCCESS) {        return -1;    }    // 获取各个状态下的 CPU 时钟脉冲数。通过将 `cpu_ticks` 数组中的所有值相加,得到 CPU 所有状态下运行的总时钟脉冲数。    unsigned long long totalTicks = 0;    for (int i = 0; i < CPU_STATE_MAX; i++) {        totalTicks += cpuInfo.cpu_ticks[i];    }    // 计算 CPU 占用率    unsigned long long idleTicks = cpuInfo.cpu_ticks[CPU_STATE_IDLE]; // `cpu_ticks[CPU_STATE_IDLE]` 表示 CPU 在空闲状态下的时钟脉冲数。    float cpuUsage = (1.0 - ((float)idleTicks / (float)totalTicks)) * 100.0;    return cpuUsage;}

这种方法计算的是整个系统的 CPU 占用率,而不是某个具体应用的 CPU 占用率。如果需要获取具体应用的 CPU 使用情况,应该使用 thread_info 等方法。

启动优化

移动应用的启动时间是影响用户体验的重要方面。

启动时间

识别启动阶段各个步骤的耗时情况。

启动分为以下三种:

  • Cold Launch:应用完全从零开始加载,最耗时。
  • Warm Launch:应用仍在内存中,但由于系统资源紧张,部分内容可能被清理,需要重新加载。
  • Hot Launch:应用仍在后台,只需快速恢复。

治理主要是针对 Cold Landch。

示例:

import UIKitclass AppDelegate: UIResponder, UIApplicationDelegate {    var window: UIWindow?    var launchTime: CFAbsoluteTime?    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        // 记录应用启动的时间        launchTime = CFAbsoluteTimeGetCurrent()                // 在主线程完成所有启动任务后,计算应用启动时间        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {            if let launchTime = self.launchTime {                let launchDuration = CFAbsoluteTimeGetCurrent() - launchTime                print("App launch time: \(launchDuration) seconds")            }        }                return true    }}

另外也可获取完整加载使用时间。使用 DispatchQueue.main.asyncAfter 延迟执行,以确保所有启动任务(如 UI 渲染、网络请求等)已经完成。然后再使用 CFAbsoluteTimeGetCurrent() 获取当前时间,与记录的启动时间相减,得到启动耗时。

使用 mach_absolute_time() 来计算时间:

static uint64_t startTime;static uint64_t endTime = -1;static mach_timebase_info_data_t timebaseInfo;static inline NSTimeInterval MachTimeToSeconds(uint64_t machTime) {    return ((machTime / 1e9) * timebaseInfo.numer) / timebaseInfo.denom;}@implementation DurationTracker+ (void)load {    startTime = mach_absolute_time();    mach_timebase_info(&timebaseInfo);        @autoreleasepool {        __block id<NSObject> observer;        observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification                                                                object:nil queue:nil                                                            usingBlock:^(NSNotification *note) {            dispatch_async(dispatch_get_main_queue(), ^{                endTime = mach_absolute_time();                NSLog(@"StartupMeasurer: it took %f seconds until the app could respond to user interaction.", MachTimeToSeconds(endTime - startTime));            });            [[NSNotificationCenter defaultCenter] removeObserver:observer];        }];    }}

启动治理思路

减少初始加载的工作量主要有延迟初始化、按需加载数据和优化依赖注入。减少不必要的资源加载的方式有移除未使用的资源和使用延迟加载。减少动态库的数量,避免在启动时过度使用复杂的泛型或协议扩展,因为这些特性可能会增加编译器在运行时的解析开销。使用 Swift Concurrency 将耗时操作异步化,以并行处理更多任务,减少主线程的压力。减少初始界面上的复杂视图层次结构,优先加载并显示关键内容,延迟非关键内容的加载。在启动时尽量减少复杂的动画过渡,以提升首屏的渲染速度。

打法上:

  • 删:出最小集,减任务
  • 延:按需,延到首页后
  • 并:统一管理,编排,充分利用多核
  • 快:减 I/O,少并发,少计算(缓存)

经验:

  • 动态库转静态库
  • 不链用不到的系统库
  • 懒加载动态库,动态取类,dlopen 动态库
  • +load 里任务挪地
  • 减少视图数,少层级,懒加载
  • 主线程等待的子线程设高优先级
  • 子线程预加载
  • 文件大拆小,碎合并
  • 统计高频调用方法
  • 警惕隐藏的全局锁

包体积

影响和手段

包体积优化的必要性:

  • 下载转化率下降:每增加6M,应用下载转化率下降1%。
  • App Store限制:超过200MB的包,iOS 13以下用户无法通过蜂窝数据下载,iOS 13及以上用户需手动设置。
  • 磁盘占用:大包体积占用更多存储空间,影响低存储用户。
  • 用户下载意愿:大包体积减少用户下载意愿,尤其在蜂窝数据低数据模式下。
  • 性能影响:包体积大增加启动时间和SIGKILL风险,降低基础体验。

技术方案主要是以下几种:

  • 资源优化:优化大块资源、无用配置文件和重复资源。
  • 工程架构优化:建立体积检测流水线,控制体积增长。
  • 图片优化:无用图片优化、Asset Catalog优化、HEIC和WebP压缩优化、TinyPng压缩。
  • 编译器优化:使用LLVM编译选项,进行OC、C++、Swift等语言的编译优化。
  • 代码优化:无用类、方法、模块瘦身,精简重复代码,AB实验固化。

效果上讲,工程方向优化大于资源优化,资源优化大于代码优化。

系统提供的方式有

  • App Thinning:利用Apple提供的App Thinning功能,根据用户的设备自动下载适合该设备的资源包,有助于减少初装包的大小。
  • 按需下载资源:使用On-Demand Resources来按需下载资源,只下载用户实际需要的部分,从而减小初始安装包的大小。

包分析

iOS端安装包组成部分有:

  • Mach-O文件:iOS系统上的可执行文件。
  • Watch APP:带有小组件功能的WatchApp。
  • 自定义动态库:动态库推迟到运行时加载,节省代码段空间。
  • Swift系统库:高版本iOS系统自带,低版本需iPA包中自带。
  • Assets资源:Assets.car文件,包含图片资源。
  • 根目录下图片资源:直接添加进工程的图片文件。
  • bundle资源:管理图片和其他配置文件。
  • 其他配置文件:如plist、js、css、json等。

Mach-O是Mach Object文件格式的缩写,用于记录Mac及iOS系统上的可执行文件、目标代码、动态库和内存转储。使用MachOView和otool命令查看Mach-O文件信息,以及通过file和lipo命令查看文件格式和架构。Mach-O文件有Header、LoadCommands和Data部分,特别是LoadCommands中的关键cmd类型如LC_SEGMENT_64,及其段(__PAGEZERO、__TEXT、__DATA、__LINKEDIT)

APPAnalyze 是一款用于分析iOS ipa包的脚本工具,能够自动扫描并发现可修复的包体积问题,同时生成包体积数据用于查看。

资源优化

资源优化方案有图片压缩、资源清理、动态加载资源、使用 Assets.xcassets 等。

Asset Catalog是Xcode提供的资源管理工具,用于集中管理项目中的图片等资源。通过Xcode自带工具actool生成Assets.car文件,可使用assetutil工具分析文件内容。开发者在图片放入Asset Catalog前不要做无损压缩,因为actool会重新进行压缩处理。

Asset Catalog 的优点有:

  • 包体积瘦身:根据不同设备下载匹配的图片资源,减少下载包大小。
  • 统一的图片无损压缩:采用Apple Deep Pixel Image Compression技术,提高压缩比。
  • 便利的资源管理:将图片资源统一压缩成Assets.car文件,便于管理。
  • 高效的I/O操作:图片加载耗时减少两个数量级,提升应用性能。

代码优化

方案有:

  • 移除未使用的代码:查找并删除未使用的类、方法、变量等。审查业务逻辑,删除不再使用或已被废弃的代码模块。
  • 重构代码:对重复的代码进行重构,使用函数、类等方法来减少代码冗余。优化数据结构,减少内存占用和CPU消耗。
  • 编译策略调整:修改编译策略,如启用LTO(链接时优化)来优化跨模块调用代码。剥离符号表(Strip Linked Product),删除未引用的C/C++/Swift代码。精简编译产物,只保留必要的符号和导出信息。
  • 代码组件化:将常用代码文件打包成静态库,切断不同业务代码之间的依赖,减少每次编译的代码量。
  • 减少文件引用:能使用@class就使用@class,尽量减少文件之间的直接引用关系。
  • 减少Storyboard和XIB文件的使用:尽量使用代码布局,减少Storyboard和XIB文件的使用,这些文件在编译时会增加包体积。
  • 清理未使用的资源:清理项目中未使用的图片、音频等资源文件,以及未使用的类和合并重复功能的类。
  • 模块化设计:将App拆分成多个模块,每个模块独立编译和打包,可以根据需要动态加载或更新模块,减少主包的体积。
  • 依赖管理:合理使用CocoaPods、Carthage等依赖管理工具,管理项目的第三方库依赖,避免不必要的库被包含进最终的包中。

Periphery 是一个用于识别 Swift 项目中未使用代码的工具。Periphery 能够清除的无用代码种类有未使用的函数和方法,变量和常量,类或结构体,协议,枚举,全局和静态变量,导入语句和扩展。

需要注意的是,Periphery 可能会因为项目的特殊配置或动态特性(如反射、运行时类型检查等)而错过一些实际上在使用中的代码。

Periphery 不能自动清除或处理的代码有被间接引用的代码,未来可能使用的代码,跨项目共享的代码,特定构建配置下的使用,编译器特性或优化相关的代码。

Periphery 主要使用静态代码分析技术来识别 Swift 项目中未使用的代码。这种技术允许它在不实际运行代码的情况下,通过扫描代码库来查找潜在的问题,如未使用的变量、废弃的函数等。

Periphery 首先使用 xcodebuild 构建指定的 Xcode 工作区或项目,并通过 --schemes--targets 选项指定要构建的方案和目标。它索引这些目标中所有文件的声明和引用,生成一个包含这些信息的图形。在图形构建完成后,Periphery 对其执行大量的变异操作,并通过分析这些变异来识别未使用的声明。这些声明可能包括类、结构体、协议、函数、属性、构造函数、枚举、类型别名或关联类型等。Periphery 能够执行更高级的分析,例如识别协议函数中未使用的参数,但这需要在所有实现中也未使用时才会报告。类似地,重写函数的参数也只有在基函数和所有重写函数中也未使用时才会被报告为未使用。允许用户通过 YAML 配置文件来自定义排除规则,以避免误报。用户可以根据项目的需求,设置特定的排除路径或模式。可以与各种 CI/CD 工具集成,如 GitHub Actions、Jenkins 和 GitLab CI/CD,实现持续集成中的静态代码分析。通过自动运行代码扫描,Periphery 可以帮助团队在每次提交或拉取请求时发现和解决潜在的问题。Periphery 提供了两种扫描命令:scanscan-syntaxscan-syntax 命令只执行语法分析,因此速度更快,但可能无法提供与 scan 命令相同水平的准确性。用户可以根据项目的具体需求选择合适的命令。

Swift 代码静态分析的开源项目还有 SwiftLint 和 SourceKitten。

接下来具体说下运行时无用类检测方案。

静态检测,通过分析Mach-O文件中的__DATA __objc_classlist__DATA __objc_classrefs段,获取未使用的类信息。但存在无法检测反射调用类及方法的缺点。

动态检测的方法。在Objective-C(OC)中,每个类结构体内部都含有一个名为isa的指针,这个指针非常关键,因为它指向了该类对应的元类(meta-class)。元类本身也是一个类,用于存储类方法的实现等信息。

通过对元类(meta-class)的结构体进行深入分析,我们可以找到class_rw_t这样一个结构体,它是元类内部结构的一部分。在class_rw_t中,存在一个flag标志位,这个标志位用于记录类的各种状态信息。

通过检查这个flag标志位,我们可以进行一系列的计算或判断,从而得知当前类在运行时(runtime)环境中是否已经被初始化过。这种机制是Objective-C运行时系统的一个重要特性,它允许开发者在运行时动态地获取类的信息,包括类的初始化状态等。

也就是通过isa指针找到元类,再分析元类中的class_rw_t结构体中的flag标志位,我们可以得知OC中某个类是否已被初始化。

// class is initialized#define RW_INITIALIZED        (1<<29)struct objc_class : objc_object {    bool isInitialized() {    return getMeta()->data()->flags & RW_INITIALIZED;    }};

在Objective-C的运行时(runtime)机制中,类的内部结构和状态通常是由Objective-C运行时库管理的,而不是直接暴露给开发者在应用程序代码中调用的。不过,你可以通过Objective-C的runtime API来间接地获取这些信息。

关于类是否已被初始化的问题,通常不是直接通过objc_class结构体中的某个函数来判断的,因为objc_class结构体(及其元类)的细节和具体实现是私有的,并且不推荐开发者直接操作。然而,Objective-C运行时确实提供了一些工具和API来检查类的状态和行为。

为了检查一个类是否在当前应用程序的生命周期中被使用过(即“被初始化过”),开发者可能会采用一些间接的方法,而不是直接操作类结构体的内部函数。以下是一个简化的说明:

由于不能直接访问类的内部结构,开发者可能会通过其他方式来跟踪类的使用情况。例如,可以在类的初始化方法中设置一个静态标志位或计数器,以记录类是否已被初始化或实例化的次数。虽然不能直接调用objc_class结构体中的函数,但开发者可以使用Objective-C的runtime API(如objc_getClassclass_getInstanceSize等)来获取类的元信息和执行其他操作。然而,对于直接检查类是否“被初始化过”的需求,这些API可能并不直接提供所需的功能。在实际应用中,可能并不需要直接检查类是否“被初始化过”,而是可以通过检查该类的实例是否存在、类的某个特定方法是否被调用过等间接方式来判断。自定义与系统类相同的结构体并实现isInitialized()函数可能是一种模拟或抽象的方式。然而,在实际Objective-C开发中,这样的做法是不必要的,因为直接操作类的内部结构是违反封装原则且容易出错的。相反,开发者应该利用Objective-C提供的runtime API和其他设计模式来达成目标。提到通过赋值转换获取meta-class中的数据,这通常指的是利用Objective-C的runtime机制来查询类的元类信息。然而,直接“判断指定类是否在当前生命周期中是否被初始化过”并不是通过简单地查询元类数据就能实现的,因为这需要跟踪类的实例化过程,而不是仅仅查看元类的结构。

获取类结构体里面的数据

struct mock_objc_class : lazyFake_objc_object {    mock_objc_class* metaClass() {        #if __LP64__            return (mock_objc_class *)((long long)isa & ISA_MASK);        #else            return (mock_objc_class *)((long long)isa);        #endif    }    bool isInitialized() {        return metaClass()->data()->flags & RW_INITIALIZED;    }};

所有 OC 自定义类

Dl_info info;dladdr(&_mh_execute_header, &info);classes = objc_copyClassNamesForImage(info.dli_fname, &classCount);

是否初始化

struct mock_objc_class *objectClass = (__bridge struct mock_objc_class *)cls;BOOL isInitial = objectClass->isInitialized();

最后通过无用类占比指标(无用类数量/总类数量*100%)快速识别不再被使用的模块。对于无用类占比高的模块,进行下线或迁移处理,减少组件数量。

更细粒度无用方法检测方案有:

编译器优化

Xcode 14的编译器可能通过更智能的分析,识别并消除不必要的Retain和Release调用。这些调用在内存管理中是必要的,但在某些情况下,它们可能是多余的,因为对象的生命周期管理可以通过其他方式更有效地实现。在Objective-C的运行时层面,Xcode 14可能引入了更高效的内存管理策略。这些策略可能包括更快的对象引用计数更新、更智能的对象生命周期预测等,从而减少了Retain和Release操作的执行次数和开销。剥离了未使用的代码和库,包括那些与Retain和Release操作相关的部分。这种优化可以减少最终生成的二进制文件的大小。

一些配置对包体积的优化:

  • Generate Debug Symbols:在Levels选项内,将Generate Debug Symbols设置为NO,这可以减小安装包体积,但需要注意,这样设置后无法在断点处停下。
  • 舍弃老旧架构:舍弃不再支持的架构,如armv7,以减小安装包体积。
  • 编译优化选项:在Build Settings中,将Optimization Level设置为Fastest, Smallest [-Os],这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。同时,将Strip Debug Symbols During Copy和Symbols Hidden by Default在release版本设为yes,可以去除不必要的调试符号。
  • 预编译头文件:将Precompile Prefix Header设置为YES,预编译头文件可以加快编译速度,但需要注意,一旦PCH文件和引用的头文件内容发生变化,所有引用到PCH的源文件都需要重新编译。
  • 仅编译当前架构:在Debug模式下,将Build Active Architecture Only设置为YES,这样只编译当前架构的版本,可以加快编译速度。但在Release模式下,需要设置为NO以确保兼容性。
  • Debug Information Format:设置为DWARF,减少dSYM文件的生成,从而减少包体积。
  • Enable Index-While-Building Functionality:设置为NO,关闭Xcode在编译时建立代码索引的功能,以加快编译速度。

另外

还可以使用 -why_load 链接器标志来减少 iOS 应用程序的二进制文件大小, -why_load 标志的作用:它可以帮助开发者识别最终二进制文件中包含的不必要符号。

在 iOS 开发中,链接器负责将代码、库和资源结合成一个最终的可执行文件。在此过程中,可能会有一些不必要的代码被包含进去,例如未使用的库、重复的符号或模块。这些多余的代码会导致应用程序的二进制文件增大,进而影响应用的下载速度、安装时间以及设备的存储空间。

-ObjC 标志,它通常用于强制链接所有 Objective-C 代码到最终的二进制文件中。这在某些情况下是必要的,例如使用了某些需要反射的 Objective-C 代码时,但是它也会导致未使用的代码被包含进去。通过 -why_load,开发者可以识别出哪些代码是多余的,并通过删除 -ObjC 标志来减少文件大小。

性能分析

有些开源的工具可以直接用于性能分析。

  • XCTest XCTest 是 Apple 官方的单元测试框架,支持性能测试。开发者可以通过 measure 方法来衡量代码块的执行时间,从而发现性能瓶颈。适合需要在单元测试中添加性能测试的场景。
  • KSCrash KSCrash 是一个强大的崩溃报告框架,它不仅能够捕获崩溃信息,还能提供应用程序的性能数据,例如内存使用和 CPU 使用情况。适合需要深入了解崩溃原因并监控相关性能数据的场景。
  • GT (GDT, GodEye) GodEye 是一个开源的 iOS 性能监控工具包,提供了多种监控功能,包括 FPS、内存使用、CPU 使用率、网络请求、崩溃日志等。它有一个方便的 UI,可以实时显示性能数据。适合在开发过程中嵌入应用进行实时性能监控。
  • libimobiledevice libimobiledevice 是一个开源的库,提供了与 iOS 设备交互的 API,可以用来监控设备状态和性能,特别是对非越狱设备进行操作。

常用的 In-app Debug 工具有:

  • Flex 是一个功能强大的 In-app Debug 工具,允许开发者在应用内实时查看和修改视图层次结构、网络请求、用户默认设置等。它还支持动态调整 UI 以及调试其他 app 内部逻辑。无需重新编译代码即可直接调试;可以修改内存中的值来观察变化。
  • Chisel 是 Facebook 开发的一组 LLDB 命令集,专门用于在调试时提供更方便的操作。它能帮助开发者快速检查视图层次结构、查看控件信息等。与 Xcode LLDB 无缝集成,通过命令行调试视图、打印出布局相关信息等。
  • Reveal 是一个图形化的 In-app Debug 工具,它允许开发者在运行中的应用中实时查看和编辑视图层次结构,支持 2D 和 3D 的视图展示。提供直观的 UI 调试界面,可以轻松地查看和修改视图属性;支持 iOS 和 tvOS。
  • Lookin 是一个开源的 iOS 视觉调试工具,专门用于分析和检查 iOS 应用的界面结构。它提供类似于 Xcode 的 View Debugging 功能,但更加灵活和强大,尤其是在复杂 UI 布局的分析上。通过 Lookin,你可以轻松地获取 iOS 应用中的界面层级、布局信息,并进行实时的 UI 调试和调整。可以称之为开源版的 Reveal。

Bazel

介绍

Polyrepo(多仓库)在代码量不断增加,开发团队扩大后,会觉得不合适,比如配置 CI 工具的繁琐,容易出现冗余代码,构建出现问题滞后等。Monorepo 指的是将多个模块化的 package 或 library 放在一个高度模块化且可管理的单一代码仓库中。谷歌的 Blaze、Bazel,以及微软的 Lage 和 Rush 等工具都是典型的 Monorepo 工具。Bazel 是一个现代化的多语言构建和测试工具。

你可以理解为是现代化的 Make 工具,但更加强大。

Bazel 通过缓存和增量构建机制,可以有效减少重复构建时间。支持并行构建,能够利用多核处理器提高构建速度。这两个点应该就是最吸引人的地方了。

另外它还允许用户定义自己的构建规则。因此,Bazel 是很适合大型的项目,还有容器化的应用。

接下来我就详细的说下 Bazel 是怎么使用的。

Bazel 组织 iOS 工程结构的方式具有高度的模块化和可管理性。

  • WORKSPACE 文件:根目录的核心文件。每个使用 Bazel 的项目都会在项目根目录中包含一个 WORKSPACE 文件,这个文件定义了项目的整体环境和依赖项。它类似于项目的“入口点”,Bazel 通过它知道如何构建整个项目。
  • BUILD 文件:模块的定义。在 Bazel 中,每个独立的模块(如一个应用、库、测试等)都需要一个 BUILD 文件,这个文件定义了该模块的构建规则。通过 BUILD 文件,开发者可以指定模块的依赖项、构建方式(如编译源代码、生成静态库等),以及测试配置。
  • Targets(目标):构建单元。BUILD 文件中定义的每个构建任务被称为“Target”(目标),可以是一个 iOS 应用程序、一个静态库、或单元测试等。目标可以依赖其他目标,这样可以构建出复杂的依赖图,确保模块间的依赖关系被正确处理。
  • 模块化组织:模块隔离与复用。Bazel 鼓励将代码分解成多个模块,每个模块都可以独立构建和测试。这种模块化结构提高了代码的可复用性,也简化了依赖管理。
  • 依赖管理:声明式依赖。Bazel 使用声明式依赖管理,即通过 BUILD 文件明确指定每个模块依赖哪些其他模块。这种方式有助于避免传统 iOS 项目中常见的依赖冲突和版本管理问题。
  • 跨语言支持:对于使用多种编程语言的项目,Bazel 提供了原生支持。对于 iOS 工程,Bazel 既支持 Objective-C 和 Swift 的构建,也支持与其他语言(如 C++、Java)的集成。
  • 并行构建与缓存:增量构建和缓存。Bazel 的构建系统支持并行构建和缓存。它能够有效地重用已经构建的模块,避免重复构建,从而大幅缩短构建时间。
  • Xcode 集成:与 Xcode 协作。虽然 Bazel 可以独立执行构建任务,但它也提供了与 Xcode 的集成,开发者可以在 Xcode 中进行代码编辑和调试,同时使用 Bazel 进行构建和测试。

WORKSPACE 文件

WORKSPACE 文件是定义项目根目录的关键文件,它告诉 Bazel 项目依赖了哪些外部库和资源,并为整个构建过程提供了基础配置。下面是一个典型的 WORKSPACE 文件的结构和示例代码:

一个典型的 WORKSPACE 文件包括以下部分:

  • 加载 Bazel 提供的 iOS 相关规则集,如 rules_applerules_swift
  • 声明项目中使用的第三方库,通常使用 http_archivegit_repository 来加载外部依赖。
  • 配置目标平台、构建工具链等。
# WORKSPACE 文件的开头,定义需要加载的规则集# 引入苹果生态系统的 Bazel 规则load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")# 加载苹果的构建规则 (rules_apple)http_archive(    name = "build_bazel_rules_apple",    url = "https://github.com/bazelbuild/rules_apple/releases/download/1.0.0/rules_apple.1.0.0.tar.gz",    strip_prefix = "rules_apple-1.0.0",)# 加载 Swift 的构建规则 (rules_swift)http_archive(    name = "build_bazel_rules_swift",    url = "https://github.com/bazelbuild/rules_swift/releases/download/0.24.0/rules_swift.0.24.0.tar.gz",    strip_prefix = "rules_swift-0.24.0",)# 使用 rules_apple 提供的默认设置load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies")apple_rules_dependencies()# 使用 rules_swift 提供的默认设置load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies")swift_rules_dependencies()# 加载 CocoaPods 规则(如果项目中使用了 CocoaPods)http_archive(    name = "bazel_pod_rules",    url = "https://github.com/pinterest/PodToBUILD/releases/download/0.1.0/PodToBUILD.tar.gz",    strip_prefix = "PodToBUILD-0.1.0",)# 声明 Xcode 版本和 SDK 的目标设置(可选)load("@build_bazel_rules_apple//apple:config.bzl", "apple_common")apple_common.xcode_config(    name = "xcode_config",    default_ios_sdk_version = "14.5",    default_macos_sdk_version = "11.3",    default_watchos_sdk_version = "7.4",    default_tvos_sdk_version = "14.5",)# 声明项目中使用的第三方库(例如使用 gRPC 或其他库)load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")git_repository(    name = "com_github_grpc_grpc",    commit = "your_commit_hash",    remote = "https://github.com/grpc/grpc.git",)# 声明额外的外部依赖(例如 Swift Package Manager 包)load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_package")swift_package(    name = "swift_lib_example",    repository = "https://github.com/apple/swift-argument-parser",    revision = "0.4.4",)# 配置 BUILD.bazel 文件所在目录中的第三方依赖load("@bazel_pod_rules//:defs.bzl", "new_pod_repository")new_pod_repository(    name = "AFNetworking",    url = "https://github.com/AFNetworking/AFNetworking.git",    tag = "4.0.1",)

rules_applerules_swift 是 Bazel 提供的官方规则集,用于构建 iOS 和 Swift 项目。通过 http_archive 你可以指定需要的规则集版本。http_archivegit_repository 用于加载第三方库或工具集成。new_pod_repository 是专门为 CocoaPods 提供的规则,用于管理 iOS 项目中的 CocoaPods 依赖。apple_common.xcode_config 用于指定 iOS SDK 版本、Xcode 版本等,可以确保项目在正确的环境下构建。

BUILD 文件

编写 iOS 程序的 BUILD 文件时,需要使用 Bazel 提供的专门规则来构建 iOS 应用、库和测试。这些规则可以帮助你定义目标、依赖项和其他构建配置。

基本概念

  • ios_application: 用于定义一个 iOS 应用的目标。
  • objc_library: 用于定义一个 Objective-C 或 Swift 库。
  • ios_unit_testios_ui_test: 用于定义 iOS 的单元测试和 UI 测试目标。
  • apple_binary: 用于定义一个包含所有依赖的 iOS 可执行文件,通常与 ios_application 一起使用。

假设我们有一个简单的 iOS 项目,它包含一个应用和一个静态库,项目结构如下:

项目结构

my_ios_project/├── WORKSPACE├── BUILD├── App/│   ├── BUILD│   ├── AppDelegate.swift│   ├── ViewController.swift│   ├── Assets.xcassets│   └── Main.storyboard└── Libs/    ├── BUILD    ├── MyLib.swift    └── MyLib.h

Libs/BUILD 文件

首先,定义一个 Objective-C/Swift 库,这个库将在应用中使用:

# 用于定义一个 Objective-C 或 Swift 的库。objc_library(    name = "MyLib", # 库目标的名称。    srcs = ["MyLib.swift"], # 源文件列表(包括 Swift 和 Objective-C 文件)。    hdrs = ["MyLib.h"], // 头文件列表(如果有 Objective-C 文件)。    visibility = ["//visibility:public"],  # 公开可见,以供其他目标使用)

接下来,定义 iOS 应用目标,并指定它依赖于上面定义的库:

# 用于定义一个 iOS 应用目标。ios_application(    name = "MyApp", # 应用目标的名称。    bundle_id = "com.example.MyApp", # 应用的唯一标识符。    families = ["iphone", "ipad"], # 目标设备类型(如 iPhone 和 iPad)。    infoplists = ["Info.plist"], # 应用的 `Info.plist` 文件。    srcs = ["AppDelegate.swift", "ViewController.swift"], # 应用的源文件列表(Swift 和 Objective-C)。    storyboards = ["Main.storyboard"],     resources = glob(["Assets.xcassets/**/*"]), # 应用的资源文件,如图像、音效等,使用 `glob` 语法可以方便地将多个资源文件包含在 `BUILD` 文件中。    deps = ["//Libs:MyLib"],  # 依赖于 MyLib 库。 `deps` 参数用来定义该目标依赖的其他库或目标,Bazel 会自动处理这些依赖关系并确保它们的构建顺序正确。)

通常在项目的根目录也会有一个 BUILD 文件来聚合或定义一些全局目标,或仅作为入口文件:

# 设置包的默认可见性,这里设置为对所有目标公开可见。package(default_visibility = ["//visibility:public"]) # 创建别名,方便从顶层访问应用目标。alias(    name = "app",    actual = "//App:MyApp",)

Starlark 语言

Starlark 是一种由 Bazel 使用的嵌入式编程语言,用于定义构建规则和操作构建文件。它类似于 Python,专门设计用于 Bazel 的构建系统,允许用户扩展 Bazel 的功能。在 iOS 工程构建中,Starlark 主要用于编写自定义的规则、宏和函数。

Starlark 基础语法

Starlark 的语法类似 Python,包括变量、函数、条件、循环等基本结构。

变量与函数

# 定义变量message = "Hello, Starlark!"# 定义函数def greet(name):    return "Hello, " + name + "!"

条件与循环

# 条件语句def is_even(x):    if x % 2 == 0:        return True    else:        return False# 循环语句def sum_of_evens(limit):    sum = 0    for i in range(limit):        if is_even(i):            sum += i    return sum

使用 Starlark 自定义 iOS 构建

假设你想要定义一个自定义的 iOS 静态库规则,它能够简化库的定义并统一管理依赖。

项目结构

my_ios_project/├── WORKSPACE├── BUILD├── app/│   ├── BUILD│   ├── AppDelegate.swift│   └── ViewController.swift└── libs/    ├── BUILD    ├── mylib.swift    └── lib.bzl

编写 lib.bzl 文件

libs/ 目录下创建一个 lib.bzl 文件,定义自定义的 iOS 静态库规则。

# 这是一个宏,用于简化 `objc_library` 规则的定义。通过这种方式,你可以统一管理 ARC 选项、依赖等设置。def ios_static_library(name, srcs, hdrs = [], deps = []):    objc_library(        name = name,        srcs = srcs,        hdrs = hdrs,        deps = deps,        copts = ["-fobjc-arc"],  # 指定编译选项,如在此处启用 ARC。    )

使用 lib.bzl 文件中的宏

libs/BUILD 文件中使用上面定义的宏来创建一个 iOS 静态库。

# 用于加载 Starlark 文件中的宏或函数。在此例中,`//libs:lib.bzl` 表示加载 `libs` 目录中的 `lib.bzl` 文件。load("//libs:lib.bzl", "ios_static_library")# `ios_static_library` 宏会被调用来定义一个名为 `mylib` 的 iOS 静态库。ios_static_library(    name = "mylib",    srcs = ["mylib.swift"],)

app/BUILD 文件中,定义一个 iOS 应用目标,并依赖于上述的静态库:

ios_application(    name = "MyApp",    bundle_id = "com.example.MyApp",    families = ["iphone", "ipad"],    infoplists = ["Info.plist"],    srcs = ["AppDelegate.swift", "ViewController.swift"],    deps = ["//libs:mylib"],)

自定义 iOS Framework 构建的示例

你可以使用 Starlark 编写更复杂的规则,例如为 iOS 定制一个 Framework 的构建规则:

# 这是一个 Bazel 的内置规则,用于创建 iOS Framework。自定义的 `ios_framework` 宏将静态库打包成一个 Framework,简化了应用与库之间的集成。def ios_framework(name, srcs, hdrs = [], deps = [], bundle_id = None):    objc_library(        name = name + "_lib",        srcs = srcs,        hdrs = hdrs,        deps = deps,    )    apple_framework(        name = name,        bundle_id = bundle_id,        infoplists = ["Info.plist"],        deps = [":" + name + "_lib"],    )

运行

在终端中运行以下命令来构建 iOS 应用。

构建应用

bazel build //App:MyApp

运行应用

bazel run //App:MyApp

测试应用

bazel test //App:MyAppTests

rules_xcodeproj 生成 Xcode 工程

rules_xcodeproj 是一个用于生成 Xcode 工程文件 (.xcodeproj) 的 Bazel 插件。它允许你在使用 Bazel 构建系统的同时,仍然能够使用 Xcode 进行开发和调试。它目前支持两种主要的构建模式:BwB (Build with Bazel) 和 **BwX (Build with Xcode)**。
BwB 模式是将 Bazel 作为主要的构建工具,Xcode 项目仅用于 IDE 支持,而实际的构建过程完全由 Bazel 管理。BwX 模式官方后续支持会变弱,不建议使用。

首先,在你的 WORKSPACE 文件中添加 rules_xcodeproj 规则的依赖项。

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")http_archive(    name = "build_bazel_rules_xcodeproj",    sha256 = "<SHA256>",    url = "https://github.com/buildbuddy-io/rules_xcodeproj/releases/download/<version>/rules_xcodeproj-<version>.tar.gz",)load("@build_bazel_rules_xcodeproj//:workspace_setup.bzl", "rules_xcodeproj_workspace_setup")rules_xcodeproj_workspace_setup()

你需要替换 <SHA256><version> 为相应的值,可以从 rules_xcodeproj 的发布页面 获取。

在项目的 BUILD.bazel 文件中,使用 xcodeproj 规则生成 .xcodeproj 文件。例如:

load("@build_bazel_rules_xcodeproj//:defs.bzl", "xcodeproj")xcodeproj(    name = "MyApp_xcodeproj", # 定义生成的 `.xcodeproj` 的目标名称。    project_name = "MyApp", # 定义 Xcode 工程的名称。    targets = ["//app:MyApp"], # 指定 Bazel 中需要包含在 Xcode 工程中的目标。)

在命令行中,运行以下命令生成 Xcode 工程文件:

bazel run //:MyApp_xcodeproj

这将生成一个名为 MyApp.xcodeproj 的文件,位于你运行命令的目录中。你可以用 Xcode 打开这个工程文件,并在 Xcode 中调试和开发你的应用。

rules_xcodeproj 提供了多种配置选项,你可以根据需要进行自定义。例如,可以配置生成的 Xcode 工程中的编译设置、构建配置等。以下是一些常用的配置:

xcodeproj(    name = "MyApp_xcodeproj",    project_name = "MyApp",    targets = ["//app:MyApp"],    build_settings = {        "SWIFT_VERSION": "5.0",        "CODE_SIGN_IDENTITY": "",    }, # 指定 Xcode 工程的编译设置,例如 Swift 版本、代码签名等。    extra_generated_files = ["//path/to/resource"], #指定额外的生成文件,可能包括资源文件等。)

Build with Proxy 模式

rules_xcodeproj 新推出的 Build with Proxy 模式,是一种新的构建模式。在 “Build with Proxy” 模式下,Bazel 通过 XCBBuildServiceProxy 完全接管了整个构建过程。Xcode 在这个模式下只作为一个前端界面,所有的构建逻辑和执行都由 Bazel 来完成。在 “Build with Bazel” 模式下,Xcode 依然是主导构建过程的工具,但它在构建的关键步骤(如编译和链接)上调用 Bazel 来完成实际的工作。Xcode 会生成编译任务并将其委托给 Bazel,同时保持对构建过程的部分控制权。

流程是,当开发者在 Xcode 中触发构建时,XCBBuildServiceProxy 拦截 Xcode 的构建请求。构建请求被重定向到 Bazel,由 Bazel 完全控制构建过程,包括依赖管理、编译、链接等。构建结果通过 XCBBuildServiceProxy 返回给 Xcode,Xcode 仅作为显示界面。

Bazel 完全控制构建过程,提供更高效的构建性能和更一致的结果。由于 Xcode 不再控制构建过程,调试和查看构建日志可能需要适应 Bazel 的方式,还有更高的初始配置成本。

首先,你需要在 Bazel 的 WORKSPACE 文件中引入 rules_xcodeproj

http_archive(    name = "build_bazel_rules_xcodeproj",    url = "https://github.com/buildbuddy-io/rules_xcodeproj/releases/download/{version}/release.tar.gz",    sha256 = "{sha256}",)load("@build_bazel_rules_xcodeproj//xcodeproj:workspace.bzl", "xcodeproj_dependencies")xcodeproj_dependencies()

接着,在你的 BUILD 文件中配置 Xcode 项目生成规则,并启用 “Build with Proxy” 模式:

load("@build_bazel_rules_xcodeproj//xcodeproj:xcodeproj.bzl", "xcodeproj")xcodeproj(    name = "MyAppProject",    targets = ["//App:MyApp"],    build_mode = "build_with_proxy",  # 启用 "Build with Proxy" 模式    minimum_xcode_version = "14.0",    # 其他配置...)

生成 Xcode 项目文件:

bazel run //:MyAppProject

生成的 .xcodeproj 文件将会配置为使用 Bazel 进行构建。

XCBBuildServiceProxy 是核心代理组件,它通过拦截 Xcode 的构建请求并将其转发给 Bazel 进行处理。在 “Build with Proxy” 模式下,Xcode 的构建流程大致如下:

# 当你在 Xcode 中点击“构建”时,Xcode 会调用 XCBBuildServiceProxy。# XCBBuildServiceProxy 会将构建请求转发给 Bazel。bazel build //App:MyApp# Bazel 处理所有构建任务,包括编译、链接等。# 构建完成后,Bazel 将结果返回给 XCBBuildServiceProxy。# XCBBuildServiceProxy 将结果反馈给 Xcode,Xcode 显示构建输出。

为了确保 Xcode 在构建时使用 Bazel,你需要配置项目的 Scheme。在生成的 .xcodeproj 文件中,确保构建 Scheme 设置为使用 XCBBuildServiceProxy 调用 Bazel。

生成 IPA 包的过程

当你运行 bazel build //App:MyApp 这条命令时,Bazel 会从指定的目标 //App:MyApp 开始,递归解析其依赖树,执行构建过程,最终生成一个 IPA 文件。

//App:MyApp 是一个 Bazel 目标,它指向一个定义在 App/BUILD.bazel 文件中的构建规则。Bazel 首先会解析这个目标并确定其直接依赖项。

假设在 App/BUILD.bazel 文件中定义了一个 ios_application 规则:

ios_application(    name = "MyApp",    bundle_id = "com.example.myapp",    families = ["iphone", "ipad"],    infoplists = ["Info.plist"],    entitlements = "MyApp.entitlements",    provisioning_profile = "//:MyAppProfile",    app_icon = "AppIcon",    launch_images = ["LaunchImage"],    deps = [        "//App/Core:core_lib",        "//App/UI:ui_lib",    ],)

在这个例子中,MyApp 依赖于两个库 core_libui_lib

Bazel 会递归地解析 deps 字段中的依赖项,从而构建整个依赖树。在上面的例子中,Bazel 会进一步解析 //App/Core:core_lib//App/UI:ui_libBUILD.bazel 文件。

假设 core_libui_lib 是通过 objc_library 规则定义的:

# App/Core/BUILD.bazelobjc_library(    name = "core_lib",    srcs = ["CoreLib.m"],    hdrs = ["CoreLib.h"],    deps = [        "//third_party/some_lib:some_lib",    ],)
# App/UI/BUILD.bazelobjc_library(    name = "ui_lib",    srcs = ["UILib.m"],    hdrs = ["UILib.h"],    deps = [        "//App/Core:core_lib",    ],)

在这里,ui_lib 依赖于 core_lib,而 core_lib 依赖于一个第三方库 some_lib

在解析完依赖树后,Bazel 开始实际的构建过程。这包括编译源文件、链接目标文件、处理资源文件,并最终打包为一个 IPA 文件。

Bazel 会首先编译 objc_library 目标。比如,将 CoreLib.mUILib.m 文件编译为 .o 对象文件,并处理相应的头文件。之后,Bazel 将链接这些编译后的对象文件,生成静态库或可执行文件。Bazel 将所有编译结果(如可执行文件、静态库)、资源文件(如 Info.plist、图标)打包为一个 .app 目录。最后,Bazel 使用 ios_application 规则的配置,将 .app 目录压缩并签名为一个 IPA 文件。

Bazel 通过其强大的缓存和增量构建机制,只重新构建那些发生变化的目标。例如,如果只修改了 UILib.m 文件,那么 Bazel 只会重新编译 ui_lib 相关的目标,而不需要重新构建整个应用。

生成的 IPA 文件通常会保存在 bazel-bin 目录中,路径类似于 bazel-bin/App/MyApp.ipa

依赖分析

Bazel 的依赖分析(dependency analysis)是其构建系统中关键的一部分,用于决定哪些文件或目标需要重新构建,以及哪些可以重用之前的构建结果。这一过程高度依赖于 Bazel 的增量构建和缓存机制。

Bazel 依赖分析的核心步骤

  • 目标(Target)定义与依赖图:Bazel 使用 BUILD 文件定义构建目标(如库、应用、测试等)以及这些目标之间的依赖关系。这些依赖关系形成了一个有向无环图(DAG),用于描述项目的依赖结构。
  • 文件和目标的输入输出(Input/Output)追踪:Bazel 追踪每个目标的输入(源文件、依赖项)和输出(编译后的二进制文件、对象文件等)。任何影响输入的更改都会触发相应目标的重新构建。
  • 哈希校验与缓存:Bazel 对每个目标的输入文件进行哈希校验(如 MD5 或 SHA-256),并将其存储在缓存中。如果同一目标的输入哈希值未发生变化,则 Bazel 直接使用缓存中的构建结果,而不需要重新构建。
  • 增量构建:当 Bazel 发现输入文件发生了变化,它会自动标记该目标以及依赖于该目标的所有下游目标为“脏”(dirty),这些目标将在下一次构建时重新编译。
  • 依赖分析的递归性:Bazel 的依赖分析是递归进行的。如果一个目标的依赖发生变化,Bazel 将递归地检查其所有上游目标是否需要重建。

以下是一个简单的 Bazel 项目结构示例,展示了 Bazel 的依赖分析过程:

项目结构

my_project/├── WORKSPACE├── BUILD├── main/│   ├── BUILD│   ├── main.m│   └── AppDelegate.m└── libs/    ├── BUILD    ├── libA.m    ├── libA.h    ├── libB.m    └── libB.h

项目根目录的 BUILD 文件:

# 根目录下的 BUILD 文件ios_application(    name = "MyApp",    srcs = ["main/main.m", "main/AppDelegate.m"],    deps = [        "//libs:libA",        "//libs:libB",    ],)

libs/ 目录的 BUILD 文件:

# libs 目录下的 BUILD 文件objc_library(    name = "libA",    srcs = ["libA.m"],    hdrs = ["libA.h"],)objc_library(    name = "libB",    srcs = ["libB.m"],    hdrs = ["libB.h"],    deps = [":libA"],  # libB 依赖于 libA)

Bazel 的依赖分析过程

  • 依赖图的生成:MyApp 依赖于 libAlibB,而 libB 又依赖于 libA。Bazel 会根据这些依赖关系生成一个依赖图。
  • 输入输出追踪与哈希校验:在每次构建时,Bazel 会对 libA.mlibB.mmain.m 等输入文件进行哈希校验,并将结果与上次构建时的哈希值进行比较。例如,如果 libA.m 发生了变化,Bazel 会检测到其哈希值发生了变化,从而标记 libA 及依赖于它的 libBMyApp 为“脏”。
  • 增量构建:由于 libA.m 发生了变化,Bazel 将重新构建 libA,然后递归地重新构建依赖它的 libB,最终重新构建 MyApp
  • 缓存与重用:如果 libB.mmain.m 没有变化,Bazel 可以重用它们之前的编译结果(缓存),只需要重新构建那些受影响的目标。
  • 输出结果:最终,Bazel 生成一个新的 MyApp 二进制文件,包含了最新的代码改动,并保证所有依赖关系都得到了正确的处理。

Bazel 使用哈希校验来精确判断哪些输入文件发生了变化。只有当输入文件的哈希值变化时,才会触发相应目标的重新构建,这样可以最大程度地重用已有的构建结果,减少不必要的编译时间。Bazel 的依赖分析是递归的,这意味着任何下游依赖的变化都会向上递归地影响依赖它的所有目标。这确保了每次构建的结果都是一致且正确的。由于 Bazel 精确地追踪了目标的依赖关系和输入输出变化,它能够有效地执行增量构建,只重新编译那些受影响的模块。

不会影响依赖分析缓存的代码改动有哪些呢?

在 Bazel 中,构建系统的性能很大程度上依赖于其增量构建和缓存机制。Bazel 使用依赖分析(dependency analysis)来决定哪些部分的代码需要重新构建,哪些部分可以使用缓存结果。

以下是一些不会影响依赖分析缓存的代码改动类型,这些改动不会导致 Bazel 重新构建依赖的目标,因为它们不会改变编译输出或依赖图:

  • 注释的更改:添加、删除或修改代码中的注释不会影响构建输出,因为注释不参与代码编译。
  • 代码格式化:仅涉及代码格式(如缩进、空格、换行)的改动不会影响构建结果,格式化不会改变编译后的二进制文件。
  • 无实际影响的变量命名更改:在局部范围内(如函数内部)修改变量名称(而不影响函数签名)不会影响依赖分析缓存。
  • 无效或未使用代码的添加:添加从未使用的代码(如未调用的函数)在某些情况下不会触发 Bazel 的重构建,特别是在这些代码片段与已构建目标无关时。
  • 函数内部的逻辑更改:在某些情况下,对函数内部进行的改动可能不会影响其他模块的构建,具体取决于目标间的依赖关系和可见性(例如,私有函数内部的更改)。

以下是一个具体的代码示例,展示了不会影响 Bazel 依赖分析缓存的几种改动:

# 示例 BUILD 文件# 定义一个简单的 iOS 应用程序目标ios_application(    name = "MyApp",    srcs = ["main.m", "AppDelegate.m"],    deps = [":MyLibrary"],)objc_library(    name = "MyLibrary",    srcs = ["MyLibrary.m"],    hdrs = ["MyLibrary.h"],)

假设我们有以下 Objective-C 代码:

// MyLibrary.m#import "MyLibrary.h"// 1. 注释的改动// 添加一些注释,不会影响 Bazel 的依赖分析缓存// 例如:以下注释不会触发重新构建// This is a utility function@implementation MyLibrary// 2. 变量名更改(局部范围)。在函数内部修改变量名称不会影响其他目标或模块的编译结果,只要变量名的改变不影响接口或其他模块的依赖。- (void)performTask {    int localVar = 5;  // 如果将 localVar 改为 anotherVar,这不会触发重新构建    NSLog(@"Task performed");}// 3. 代码格式改动。如添加空行、调整缩进或更改代码对齐方式等纯粹的格式改动,不会改变源代码的语义,因此不会触发重新编译。- (void)doSomething {    int a = 10;    int b = 20;  // 对齐方式或空格的改变不会触发重新构建    NSLog(@"Sum: %d", a + b);}// 4. 添加未使用的代码。如果添加的代码从未被调用或引用,Bazel 可能不会重新构建该模块,尤其是在该代码片段没有影响编译输出时。- (void)unusedFunction {    NSLog(@"This function is never called.");}@end

在 Bazel 的构建过程中,操作图(Action Graph)是一个关键的概念,它定义了构建任务之间的依赖关系,并确保这些任务能够按照正确的顺序并行执行。Baziel 使用操作图来确定哪些任务可以并行执行,哪些任务需要依赖其他任务的结果。

操作图是一个有向无环图(DAG),其中每个节点代表一个操作(Action),每个边代表操作之间的依赖关系。操作可能包括编译源文件、链接对象文件、打包资源文件等。

操作图中的节点和边的关系如下:

  • 节点(Action):一个构建任务,如编译、链接或打包。
  • 边(Dependency): 表示一个操作依赖于另一个操作的输出。

Bazel 从指定的构建目标(如 bazel build //App:MyApp)开始,递归地解析 BUILD 文件中定义的目标和依赖关系,生成操作图。具体步骤如下:

  1. Bazel 解析 BUILD 文件,找到指定目标和其依赖项。
  2. 每个构建规则(如 objc_library, ios_application)会生成一组操作。这些操作可能包括编译源文件、链接目标文件等。
  3. Bazel 将生成的操作按照依赖关系连接起来,形成操作图。

Bazel 确保操作图中的操作按正确的顺序并行运行,遵循以下原则:

  • 一个操作只能在它所有的依赖操作完成后才能运行。
  • Bazel 会并行执行那些没有依赖关系或者依赖已经满足的操作。

假设我们有一个简单的项目,其中包含两个库和一个应用程序。每个库都有自己的源文件和头文件,应用程序依赖于这两个库。以下是 BUILD 文件的定义:

# App/Core/BUILD.bazelobjc_library(    name = "core_lib",    srcs = ["CoreLib.m"],    hdrs = ["CoreLib.h"],)# App/UI/BUILD.bazelobjc_library(    name = "ui_lib",    srcs = ["UILib.m"],    hdrs = ["UILib.h"],    deps = [        "//App/Core:core_lib",    ],)# App/BUILD.bazelios_application(    name = "MyApp",    bundle_id = "com.example.myapp",    families = ["iphone", "ipad"],    infoplists = ["Info.plist"],    deps = [        "//App/Core:core_lib",        "//App/UI:ui_lib",    ],)

对于上述项目,Bazel 会生成如下操作图:

  1. 编译操作:

    • CoreLib.m -> CoreLib.ocore_lib 的编译操作)
    • UILib.m -> UILib.oui_lib 的编译操作)
  2. 链接操作:

    • core_lib 编译完成后,可以立即编译 ui_lib,因为 ui_lib 依赖于 core_lib
    • core_libui_lib 都编译完成后,可以将它们链接到一起,生成 MyApp 的可执行文件。
  3. 打包操作:

    • 在所有链接操作完成后,将生成的二进制文件与资源文件(如 Info.plist)打包为 .app 目录,然后进一步打包为 IPA 文件。

在这个操作图中,CoreLib.oUILib.o 的编译操作可以并行执行,因为它们没有依赖关系。链接操作则需要等待所有编译操作完成后才能执行。

Bazel 在内部使用操作图来调度这些任务。通过分析操作图,Bazel 能够确定哪些任务可以并行执行,哪些任务需要等待依赖完成,从而最大化利用多核 CPU 的能力,加速构建过程。

query指令找依赖关系

Bazel 的 query 命令是一种强大的工具,用于在 Monorepo(单体代码库)中查找和分析目标之间的依赖关系。通过 query,你可以获取关于构建目标的详细信息,包括它们的依赖关系、反向依赖、测试等。

bazel query 命令的一般语法如下:

bazel query '<expression>'

<expression> 是你想要查询的表达式。Bazel 提供了一系列表达式来帮助你查找所需的信息。

以下是常见的 Bazel Query 表达式

列出工作区中所有可用的构建目标:

bazel query '//...'

//... 表示从当前工作区的根目录开始递归查找所有目标。

查找某个目标的所有直接和间接依赖:

bazel query 'deps(<target>)'

例如,查找 //app:main 目标的所有依赖:

bazel query 'deps(//app:main)'

查找哪些目标依赖于某个特定目标(即反向依赖):

bazel query 'rdeps(<scope>, <target>)'

例如,查找工作区中哪些目标依赖于 //lib:my_library

bazel query 'rdeps(//..., //lib:my_library)'

例如,列出所有的测试目标:

bazel query 'kind(test, //...)'

kind(test, //...) 将查找工作区中的所有测试目标。

如果只想查找目标的直接依赖而非递归依赖,可以使用:

bazel query 'deps(<target>, 1)'

例如:

bazel query 'deps(//app:main, 1)'

使用 attr 过滤带有特定属性的目标。例如,查找所有带有特定标签的目标:

bazel query 'attr(tags, "my_tag", //...)'

假设你有以下项目结构:

workspace/├── app/│   ├── BUILD│   ├── main.swift│   └── AppDelegate.swift├── lib/│   ├── BUILD│   ├── util.swift│   └── helper.swift└── third_party/    ├── BUILD    └── external_lib.swift

app/BUILD 文件中,你定义了一个 ios_application 目标:

ios_application(    name = "MyApp",    bundle_id = "com.example.MyApp",    srcs = ["main.swift", "AppDelegate.swift"],    deps = ["//lib:util"],)

lib/BUILD 文件中定义了一个 swift_library 目标:

swift_library(    name = "util",    srcs = ["util.swift", "helper.swift"],    deps = ["//third_party:external_lib"],)

你可以运行以下命令来查找 MyApp 的所有直接和间接依赖:

bazel query 'deps(//app:MyApp)'

这将输出:

//app:MyApp//lib:util//third_party:external_lib

查找依赖于 external_lib 的所有目标

你可以使用以下命令来查找反向依赖:

bazel query 'rdeps(//..., //third_party:external_lib)'

这将列出所有依赖于 external_lib 的目标,比如 //lib:util

你还可以生成图形化的依赖关系图,使用 dot 格式输出:

bazel query 'deps(//app:MyApp)' --output graph > graph.dot

然后使用 Graphviz 等工具将 graph.dot 文件转换为图形文件。

query 指令是理解和管理 Monorepo 中依赖关系的关键工具。它提供了多种强大的表达式,帮助你轻松地查找目标的依赖关系、反向依赖、过滤目标等。在大型代码库中,使用 query 可以大大简化依赖关系的管理,并且可以帮助你识别不必要的依赖或者循环依赖。

远程缓存

Bazel 的远程缓存功能允许你在不同的开发环境、构建机器或 CI 系统之间共享构建产物。这可以显著加快构建速度,因为已经构建好的产物可以被重复使用,而不需要重新编译。

Bazel 的远程缓存功能可以将构建产物(如编译后的二进制文件、对象文件等)存储在一个远程存储系统中。当你在不同环境或机器上构建同一个项目时,Bazel 会检查远程缓存,并下载已存在的构建产物,而不必重新构建。

Bazel 支持多种远程缓存后端,包括:

  • HTTP/HTTPS 服务器:可以使用支持 HTTP 的远程服务器作为缓存。
  • 云存储:如 Google Cloud Storage (GCS) 或 Amazon S3。
  • gRPC 缓存服务:可以通过 gRPC 接口进行缓存和检索。

在你的项目中,可以通过 ~/.bazelrc 文件或项目级别的 .bazelrc 文件来配置远程缓存。以下是如何配置不同类型远程缓存的示例。

配置 HTTP 远程缓存

build --remote_cache=http://my-cache-server.com/cache/

如果你使用 Google Cloud Storage (GCS) 作为远程缓存,你可以这样配置:

build --remote_cache=grpc://gcs.example.com/bucket-namebuild --google_credentials=/path/to/credentials.json

在这个例子中,grpc://gcs.example.com/bucket-name 是 GCS 的地址,/path/to/credentials.json 是你的 GCS 凭证文件。

配置 gRPC 远程缓存

build --remote_cache=grpc://my-grpc-cache-server.com

你可以使用 gRPC 缓存服务器,如 BuildBarn 或 BuildGrid 来搭建自己的 gRPC 远程缓存服务。

有些远程缓存服务需要身份认证,如 GCS 或 Amazon S3。对于 GCS,你可以配置 google_credentials 选项,或者使用 gcloud auth 命令登录:

gcloud auth application-default login

对于需要 AWS 认证的服务,你可以配置 AWS CLI,然后通过环境变量传递认证信息:

export AWS_ACCESS_KEY_ID="your-access-key-id"export AWS_SECRET_ACCESS_KEY="your-secret-access-key"

配置完成后,Bazel 会自动使用远程缓存。在运行构建命令时,如:

bazel build //App:MyApp

Bazel 会:

  1. 首先检查远程缓存,是否有匹配当前源代码和构建配置的缓存。
  2. 如果找到匹配的缓存,直接下载使用,而不重新编译。
  3. 如果没有找到匹配的缓存,正常编译并将结果上传到远程缓存,以便下次使用。

注意远程缓存和远程执行是不同的概念。远程缓存仅共享构建产物,而远程执行允许你在远程机器上执行整个构建过程。你可以根据需要选择合适的方案。

以下是一个项目级别的 .bazelrc 文件示例,它配置了远程缓存到一个 HTTP 服务器:

# .bazelrcbuild --remote_cache=http://cache.example.com/cache/build --disk_cache=/path/to/local/cachebuild --google_default_credentials

远程执行配置

Bazel 的远程执行功能允许你在远程服务器或集群上分布式执行构建任务,而不是在本地机器上执行。这种能力特别适用于大规模的项目,可以显著缩短构建时间,因为它利用了多台机器的计算资源。

远程执行让 Bazel 在远程执行环境中运行构建任务,例如编译、链接、测试等。Bazel 将构建任务分发到一个或多个远程执行节点,这些节点并行处理任务并将结果返回给本地 Bazel 客户端。

一个典型的远程执行环境由以下组件组成:

  • 远程执行服务器:处理来自 Bazel 的任务,并将它们分发给执行节点。
  • 远程工作节点:这些节点执行实际的构建任务。
  • Remote Cache(远程缓存):存储构建产物以便重复使用,避免重新执行相同任务。

要启用 Bazel 的远程执行功能,你需要配置 Bazel 来连接远程执行服务。配置通常在 .bazelrc 文件中完成。

假设你有一个远程执行服务器,它的地址是 remotebuild.example.com。你可以通过以下配置启用远程执行:

# .bazelrcbuild --remote_executor=grpc://remotebuild.example.com:443build --remote_cache=grpc://remotebuild.example.com:443build --remote_timeout=300build --spawn_strategy=remotebuild --strategy=Javac=remotebuild --strategy=CppCompile=remotebuild --strategy=Objc=remote
  • --remote_executor:指定远程执行服务器的地址。
  • --remote_cache:配置远程缓存的地址,这里可以和远程执行服务器一致。
  • --remote_timeout:设置远程执行的超时时间。
  • --spawn_strategy=remote:告诉 Bazel 使用远程策略执行所有构建任务。
  • --strategy=Javac=remote 等:为特定类型的任务指定使用远程执行。

如果远程执行服务器需要身份验证,你可能需要配置凭据。对于 Google Cloud Remote Build Execution (RBE) 服务,典型的配置如下:

build --google_credentials=/path/to/credentials.json

使用 gcloud 工具登录:

gcloud auth application-default login

设置远程执行服务(如 BuildFarm、BuildGrid 或 Google 的 Remote Build Execution (RBE))通常涉及以下步骤:

  1. 安装和配置 Remote Execution Server:这包括配置服务器的计算资源、执行策略等。
  2. 配置 Remote Workers:确保工作节点能够连接到服务器,并具备执行构建任务所需的环境和依赖。
  3. 配置 Remote Cache:搭建和配置远程缓存,以便存储和共享构建产物。

配置完成后,你可以运行 Bazel 命令进行远程执行,例如:

bazel build //App:MyApp

在这个过程中,Bazel 会:

  1. 将构建请求发送到远程执行服务器。
  2. 服务器将任务分发到远程工作节点,并行执行。
  3. 远程节点完成任务后,将结果和构建产物返回到本地。
  4. 本地 Bazel 客户端将最终产物(如可执行文件或 IPA 文件)生成。

使用远程执行的好处

  • 通过分布式构建,可以显著缩短构建时间。
  • 充分利用远程集群的计算资源,而不是依赖本地机器的性能。
  • 确保所有开发人员、CI/CD 系统在相同的环境中执行构建,减少“在我机器上正常”的问题。

假设你有一个项目 App,其中包括一个 BUILD 文件。以下是如何在远程执行环境中构建这个项目的完整配置。

.bazelrc 文件:

build --remote_executor=grpc://remotebuild.example.com:443build --remote_cache=grpc://remotebuild.example.com:443build --google_credentials=/path/to/credentials.jsonbuild --spawn_strategy=remotebuild --strategy=CppCompile=remotebuild --strategy=Javac=remotebuild --strategy=Objc=remote

然后你可以执行以下命令:

bazel build //App:MyApp

自定义构建规则

Bazel 的可扩展性是其强大功能之一,它允许开发者为尚未支持的编程语言或构建工具创建自定义的构建规则。通过编写自定义规则,你可以让 Bazel 识别、编译、链接特定语言的代码,并将它们集成到现有的 Bazel 构建系统中。

在自定义规则中,你可以指定输入、输出、依赖关系以及构建过程中的具体操作。

一个自定义的 Bazel 构建规则通常包括以下部分:

  • 规则定义:描述构建过程的逻辑和依赖关系。
  • 构建步骤:实际执行的命令,比如编译或链接操作。
  • 规则调用:在 BUILD 文件中调用自定义规则来应用于实际项目。

假设我们要为一个尚未被官方支持的编程语言 MyLang 创建一个简单的构建规则,该规则能够将 .mylang 源文件编译为可执行文件。

首先,在项目的根目录下创建一个 mylang_rules.bzl 文件,用于定义 MyLang 的构建规则。

# mylang_rules.bzldef _mylang_binary_impl(ctx):    # 输入文件    source = ctx.file.src        # 输出文件 (可执行文件)    output = ctx.actions.declare_file(ctx.label.name)        # 编译命令    ctx.actions.run(        inputs=[source],        outputs=[output],        arguments=[source.path, "-o", output.path],        executable="path/to/mylang_compiler",    )    return DefaultInfo(        executable=output,    )# 定义 mylang_binary 规则mylang_binary = rule(    implementation=_mylang_binary_impl,    attrs={        "src": attr.label(allow_single_file=True),  # 单个源文件    },    executable=True,  # 生成可执行文件)

_mylang_binary_impl 实现了 mylang_binary 规则的逻辑,它使用 Bazel 的 ctx.actions.run 来定义编译过程。mylang_binary定义了一个新的构建规则,允许我们在 BUILD 文件中使用 mylang_binary 规则来处理 MyLang 源文件。

在你的项目中,使用自定义的 mylang_binary 规则。比如,在 my_project/BUILD 文件中:

# my_project/BUILDload("//:mylang_rules.bzl", "mylang_binary")mylang_binary(    name = "my_program",    src = "main.mylang",)

这个 BUILD 文件表示使用 mylang_binary 规则编译 main.mylang 文件,并生成一个名为 my_program 的可执行文件。

你可以通过 Bazel 构建这个项目:

bazel build //my_project:my_program

这将使用 MyLang 编译器将 main.mylang 编译为 my_program 可执行文件。

自定义规则的功能可以进一步扩展。例如,你可以添加支持多个源文件、库依赖、资源文件等。如果你希望 mylang_binary 支持多个源文件,可以修改规则定义:

# mylang_rules.bzldef _mylang_binary_impl(ctx):    sources = ctx.files.srcs    output = ctx.actions.declare_file(ctx.label.name)    # 假设 mylang_compiler 能够接受多个源文件    args = [source.path for source in sources] + ["-o", output.path]    ctx.actions.run(        inputs=sources,        outputs=[output],        arguments=args,        executable="path/to/mylang_compiler",    )    return DefaultInfo(        executable=output,    )mylang_binary = rule(    implementation=_mylang_binary_impl,    attrs={        "srcs": attr.label_list(allow_files=True),  # 支持多个源文件    },    executable=True,)

BUILD 文件中:

# my_project/BUILDmylang_binary(    name = "my_program",    srcs = ["main.mylang", "utils.mylang"],)

通过创建自定义规则,你可以将 MyLang 与 Bazel 的其他功能(如远程缓存、远程执行、增量构建等)集成在一起。你还可以通过将规则打包为 Bazel 模块,供其他项目复用。

海贼王之感人名场面

作者 戴铭
2024年8月18日 21:24

这次播客是我,柠檬和田阳一起聊了下空岛篇以及之前的故事。田师傅渊博的海贼王知识非常值得一听。可在小宇宙收听,或扫码下图。

海贼王为什么有那么多令人感动的看一次哭一次的关于友情的名场面,我想可能是因为这些都是生活中所难以获得的美好与渴望吧。

下面让我看一次感动一次的名场面。

第一个是香克斯为救路飞丢掉一只手臂,让我感动的是香克斯的气度,也是因为他的气度一只影响着路飞后面走的路。

接下来是索隆的回忆,回忆中他的竞争对手,榜样和目标霜月克伊娜向他展露了柔软的一面,可能索隆还小不能理解她,但却让索隆因此更加坚定了自己的目标,同时带上了克伊娜的那份。

只是一只小狗,没有能力守护什么,却不言放弃,还默默接受着结果。这感动了路飞,也让我了解到了海贼王的意义。

一个人几十年一直在追求追寻自己想得到的宝箱,在得到后发现是虚无。他可能不会埋怨了,只会感谢珍惜身边那些充满善意,善良的人吧。

路飞和黑猫队长对战,发现黑猫队长是个只顾自己,只会利用伙伴的人,于是发出感叹,说他能力再强又怎样,还不如乌索普。这是因为什么呢?是气度,乌索普志气是保护身边的人,远望着无边无际的大海,有了这样的气度才能够有远航,冒险和称霸海洋的权利。

仪式感的解散,代表着乌索普和孩童时代的告别,告别舒适、安全,去面对挫折,煎熬。还好有着相互信赖的伙伴。

当两个人面对未知未来的时,是自保还是成全对方,这是对人性的考验。哲夫的选择换来了山治的感恩。

成年后的山治表面上看着吊儿郎当,但对哲夫的第二次施恩,却是发至肺腑的感激。哲夫真切的关心,和山治的下跪,让我深深感到他们之间的羁绊有多深。

不管娜美如何为了自己村子而背叛了路飞他们,路飞都是一只相信着她,娜美一只都不敢轻易去信任别人,直到最后无路可走时,才向路飞求助,路飞一如既往的相信着娜美,因为他从一开始就知道娜美是善良的。这种无条件的付出,赢得了娜美的信任。

在和路飞分别时的这段非常感人。海军的迫近,让此时无声更胜有声,这就是伙伴的标记。

当罗宾下决心要通过牺牲自己来保护大家,但大家全然不顾的追来了,她的身世决定她的未来是暗淡的,她也担心未来大家会抛弃她,于是她想死。但是与世界为敌又怎样,一直以来都是牺牲小我成全大我,但是路飞和乌索普的行为让罗宾明白,再渺小的人为了珍爱的人也可以和世界为敌。

有一群愿意和你一起走下去的伙伴,那么大海也就是你的。这一段是看一遍哭一遍,令人难忘。这部漫画在我心中永存。

鸟山明和他的龙珠

作者 戴铭
2024年8月10日 08:05

前言

上次录了期圣斗士的播客,还写了篇文章。这次录了期龙珠话题的播客,聊下龙珠,说下龙珠的故事,鸟山明与龙珠的那些事,比如他的助手、编辑以及他平日的一些爱好。作为一个老的漫画爱好者,我还会说说以前海南版七龙珠和画书大王杂志的一些事。本期播客地址,或扫码下图中二维码收听。铭莉双收播客已经有了 RSS 连接,可以通过泛用型客户端收听,也可以直接在苹果 Podcast 里搜索“铭莉双收”订阅收听,记得五星好评哦。以下内容是对播客内容的一点补充。

龙珠的影响力

龙珠的影响力不言而喻,下面用一些排行数据来直观感受下。

日本漫画全球销量排行榜,龙珠排在第二,第一是海贼王。火影第四,柯南第五。日本NHK官方排名前5的动漫,龙珠也是第二,第一是千与千寻,第三是海贼王。火影第四。日本最受欢迎漫画家排行榜中鸟山明位居第二,第一是手冢治虫。宫崎骏是第三。日本动漫协会民调评出最经典35部动漫中,龙珠是第一,海贼王第二,火影是第四。

可见龙珠在日本动漫界地位。

我所看的是情况是谁家要是有全套海南版七龙珠,一定会被全班羡慕,去他家蹭着看。因为七龙珠的火爆,一些其他的漫画也会被改成以七开头的,比如乱马就被改成七笑拳,连作者高桥留美子都被改成了鸟山明。同期还有很多漫画比如《哆啦A梦》、《圣斗士星矢》、《侠探寒羽良》,但是看得最多的还是龙珠。

龙珠和西游记的关系

鸟山明当初想创作一部带有《西游记》风格的冒险故事。

龙珠开始有点西游记冒险的那味,也用了一些西游记中元素,比如主角的名字,如意棒,筋斗云。还有乌龙的形象就是借用了猪八戒。

《西游记》最初是通过说书人和民间智慧口头流传下来的,后来由吴承恩在明朝将这些故事集结成书。到了民国时期,考证出吴承恩是《西游记》的整理者。几百年前写的《西游记》没有版权,即使是现代小说,在我国超过50年也进入公共领域,不再享有版权保护。进入公共领域意味着任何人都可以改编和商业使用这部作品,无需支付原作者费用。因此龙珠使用西游记中的元素也是没有问题的。

龙珠角色名字来源

因为龙珠流通的版本较多,本文主要取了个中,没有用最早的海南版和最新的台版,而是用的中国少年儿童出版社的翻译名,比如库林就是海南版中的小林,比克是短笛,亚姆查就是乐平。如有地方错了,可能是我看的版本太多导致混淆的缘故,请见谅。

下面是鸟山明怎么给龙珠里的人物命名的。基本都是根据生活中常见吃穿物品的音来起名的。

孙悟空以及初期冒险碰到的人都是有中国特色的,亚姆查是饮茶日语 yamcha 的发音。乌龙、普尔、天津饭、饺子、鹤仙人、桃白白等。悟饭的日语发音是 gouhan,在日语中也就是饭的意思。皮拉夫看起来没有中国味,但是确实中式杂烩炒饭的法语发音,他的手下阿修和小舞的是烧卖 shyumai 的发音。

布尔玛日语是ブルマ,读音 bulma,日语意思是女生短裤。布尔玛父亲布利夫日语意思是男性内裤,儿子特兰克斯意思是男性短裤,女儿布拉(Bra)是胸罩的意思。他们一家主题是内衣。

牛魔王的女儿琪琪的名字来自日语牛的读音 chichi。

库林的日语发音是 kuri,是光头的意思。小林女儿马萝是光头的法语发音。

红绸军里的人都是按照颜色来的,蓝将军、白将军、银大佐、紫曹长、紫罗兰大佐、黑副官、红总帅

比克大魔王是短笛的发音,手下都是乐器,皮亚诺、辛巴鲁、坦巴利、多拉姆分别对应钢琴 piano,钹 cymbal,铃鼓 tambourine,鼓 drum。

赛亚人 saiya 是日语蔬菜发音 yasai 倒着发。贝吉塔是蔬菜阴雨 vegetable 的前几个字母 vegeta 的发音。那巴是菜叶日语 nappa 发音。卡卡罗特是胡萝卜的英语发音 carotte,他衣服颜色也是胡萝卜的颜色,哥哥拉蒂兹是英语萝卜 radish 的发音,父亲巴达克是牛蒡英语 burdock 的发音。可以看出赛亚人都是蔬菜。

那美克星篇中弗利萨是能够装所有蔬菜的冰箱 freezer 的发音,他的父亲格尔多大王是 kingcold,也就是冷王的意思。冰箱还能够装水果,所以弗利萨的手下都是水果,比如第一个被贝吉塔杀掉的是丘夷是水果 kiwi,也就是猕猴桃的意思。萨博是日本水果朱栾 zabon 的发音,多多利亚是榴莲 durian 的发音。

基纽是牛奶日语 ginyu 发音,古尔多是酸奶德文 yoghurt 后半音,吉斯是奶酪英语 cheese 的发音,巴特是黄油英语 butter 的发音。简直就是奶制品特种部队。

人造人基本就简略到只用编号来表示了。

悟饭读高中时碰到的同学名字都是以学习用具命名的,比如莎普是铅笔刀英语 sharpener 的发音,伊雷莎是橡皮 eraser 的发音。

布欧篇更敷衍了,直接将一段灰姑娘电影中的咒语“比比迪巴比迪布欧”拆成不同人的名字,比比迪,巴菲迪,布欧。欧布就是布欧倒过来念。

下面按照时间线说说龙珠的内容。

龙珠时间线

正传前

宇宙中有个恐怖的魔物叫魔罗,它能吸收生命能量并用来增强自己,还能攻击敌人。魔罗摧毁了很多星球,最终大界王神耗费大量神力封印了它的魔力。魔导师比比迪创造了布欧,布欧在数年间摧毁了成千上万颗行星,并消灭了五位界王神中的四位,吸收了大界王神后变成肥布欧,西界王神艰难地将布欧封印在蛋壳内并抛入太空。

杏仁开始管理五行山上的八卦炉。小加力古的祖先从魔境星来到地球。邪道神化身为蛇道的公主并在路旁建了一座宫殿。占卜婆婆开始掌管人们的命运。阎魔大王穿越蛇道得到北界王的教导进行修炼,期间遇到蛇公主,蛇公主对他产生好感。龟仙人出生。比克在天神门下修炼。龟仙人18岁时在师父武泰斗的教导下修炼,并迷恋上少女芳芳。鹤仙人的兄弟桃白白出生。龟仙人在海滩捡到一颗三星龙珠。第一届天下第一武道会开幕。老孙悟饭出生。

那美克星遭遇可怕的风暴,许多娜美克星人丧生。卡达祖把儿子比克送上太空以保全性命,但太空船迫降地球。加力古卷土重来,企图夺取天神之位,天神被驱逐后,比克担任天神,但他的邪念分离出来,成为比克大魔王。大魔王开始破坏大地,但不久后被武泰斗用魔封波封印在电饭煲内,武泰斗也因此牺牲。

达普拉对地球进行了调研,为魔人布欧的苏醒做准备,但调研得太早了。

传说中的超级赛亚人在宇宙间大肆破坏。赛亚人集结占领了普兰特星,将其改名为贝吉塔星。祖福鲁族科学家赖知博士被杀,祖福鲁人被灭族,他们的科技被赛亚人夺走。赛亚人开始在宇宙间航行,与异星人接触并发展贸易。赛亚人通过占领星球并将资源卖给异星人,与弗利萨建立了伙伴关系。贝吉塔王与皇后结婚。贝吉塔王子、亚姆查、布尔玛、天津饭先后出生,地球新国王诞生。弗利萨开始对赛亚人感到不安,库林出生。弗利萨进攻贝吉塔星球,贝吉塔行星毁灭,孙悟空被父亲巴达克送往地球。巴达克试图挑战弗利萨但失败,卡卡罗特被送往地球。

卡卡罗特出生。老孙悟饭在竹林中发现赛亚人飞行器中的婴儿并收养了他,取名孙悟空。悟空不小心从山崖摔下,头部受伤后变得活泼开朗。龟仙人的不死鸟因食物中毒死去,他在海滨发现了一只小海龟。晚上悟空出去撒尿,看见圆月,第一次变身巨猿,无意中踩死了爷爷。

牛魔王之女琪琪出生,她的母亲不久后去世。牛魔王和琪琪在山脚野餐时,火焰让他们的城堡和整座山变为火海,他们迁居山脚,并将此山改名为火焰山。

悟空历险

艾纪748年(孙悟空11岁)

龙珠正传开始

布尔玛和孙悟空的命运相遇,开始了寻找七颗龙珠的冒险旅程。

布尔玛在家里整理仓库时发现了二星龙珠,接着在北方山洞里找到了五星龙珠,并决定利用暑假寻找剩下的龙珠。

她在路上遇见了悟空,发现悟空的爷爷遗物是四星龙珠,于是邀请他一起冒险。接下来,悟空发现了男女的不同。途中,小悟空救了深山里的老乌龟,将它送回海边,老乌龟带来老龟仙人,布尔玛用内裤从龟仙人那换来了三星龙珠,悟空还获得了筋斗云。

途中,悟空制服了变成怪物的乌龙,村里的老妇人给了他们六星龙珠。悟空还和亚姆查交手,最终成了平局。后来,悟空打败了亚姆查,在牛魔王的村子里遇见了琪琪。龟仙人用龟派气功扑灭了火焰山的大火,但也把大山吹走了,他们在废墟中找到了七星龙珠。在火焰山,悟空学会了龟派气功,战斗力达到10。去龟仙岛的路上,悟空和琪琪定下了婚约。

之后,兔子团长被送上月球做糖果,皮拉夫的手下舞和修偷走了龙珠。皮拉夫许愿时,乌龙抢先索要了一条女式内裤,悟空变成巨猿,把皮拉夫的城堡夷为平地。

同年,悟空拜龟仙人为师,结识了库林,一起习武。布尔玛和亚姆查开始了他们的恋爱长跑。

艾纪749年(12岁)

孙悟空和库林在龟仙人的安排下参加了“天下第一武道会”。库林在四强赛中输给了龟仙人化名的程龙,孙悟空也在决赛中惜败于他,获得亚军。比赛中,悟空因满月变成大猩猩,程龙无奈之下摧毁了月亮。

同年,孙悟空在寻找爷爷的四星龙珠时,顺手摧毁了“红绸军团”,阻止了首领瑞德想要变高的愿望,维护了地球治安。在这个过程中,悟空结识了斯诺、人造人8号、乌帕父子和卡林仙人,还击败了杀手桃白白。

在寻找最后一颗龙珠时,孙悟空得到占星婆婆的帮助,再次见到了已故的爷爷孙悟饭,完成了他的遗愿,看到了孙悟空的成长。同年,亚姆查拜入龟仙人门下。

第一次参加武道会

艾纪749

悟空和库林开始在龟仙人门下修炼,带来了可爱的兰琪。他们在小岛上接受了8个月的特训。

一天,他们吃了有毒的河豚鱼,只能躺在床上休息。几天后,悟空和库林正式开始训练,直到下一届天下第一武道会。

在训练期间,他们进行了负重40公斤的锻炼。红绸子军银司令也在寻找龙珠。

小悟空和库林练了8个月的基本功,然后直接参加了武道会。

有人认为几个月就能速成拿好名次,是爽文套路,取悦读者; 有人觉得龟仙人不肯教招式,是怕教会徒弟饿死师父,传武老毛病; 有人说龟仙人的理念是无招胜有招,基本功最重要,招式反而不重要; 也有人认为只靠日常劳作就能强身健体、逆袭,是投机取巧,老港片里常用的套路,不是因为合理,而是接地气,容易让观众代入。

不得不承认,龙珠确实用了不少爽文套路。8个月备战,两个十三四岁的孩子能顺利进入世界大赛四强,这有点离谱。打桃白白时更夸张,只用3天,通过卡林塔的攀爬和追逐训练,就全面碾压桃白白。比克大魔王篇更是充满武侠味,复仇、开挂、爽文元素全都有。

最终,龟仙人、悟空和库林离开小岛参加天下第一武道会。龟仙人化身“程龙”获得了冠军,悟空屈居第二。

红绸军

艾纪750

皮拉夫、红绸军和悟空同时开始找龙珠。红绸军在皮拉夫的地下城堡很快就找到了两颗龙珠。第二天,布尔玛给悟空修好了龙珠雷达。悟空和蓝将军在半空中打到了企鹅村上空,结果被阿拉蕾一记头槌打败。这也是悟空第一次来企鹅村,遇到了阿拉蕾。同一天,桃白白用舌头亲自杀了蓝将军。悟空在卡林塔下惨败给桃白白,决定爬上卡林塔找仙人帮忙。

过了两天,悟空爬上了卡林塔顶,得到了卡林仙人的引导,仙人给了他“超圣水”,其实就是普通的自来水。原来,真正的提升能力过程在于和卡林仙人的较量。又过了两天,经过三天的训练,悟空再战桃白白,取得了胜利,还一举歼灭了红绸军。这一天,悟空不仅为世界除了大害,还在水晶婆婆的格斗场上看到了已经去世的爷爷孙悟饭。

第二次武道会,天津饭

艾纪752年(15岁)

孙悟空在世界各地修炼之后,回来参加了这届天下第一武道会。鹤仙流也派人来参加了,龟仙流的亚姆查和鹤仙流的天津饭打了一场。但因为实力差太多,亚姆查被打败了,腿还受了伤,没能进八强。

程龙三年前因为打碎了月亮,导致人狼没法变回人,所以这次人狼来参加武道会想报仇。但程龙还是轻松地打败了他,还用库林的亮脑门和催眠术把人狼变回人了。

库林和饺子比赛时,库林这个数学天才靠着数数赢了饺子这个数学鬼才,给龟仙流挣回了面子。

孙悟空更是一招就打败了两个世界冠军的巴普特,轻松进了四强。四强战里,程龙和天津饭打得很激烈,但程龙想感化天津饭,所以主动认输了。孙悟空和库林打了一场,库林虽然尽力了,但还是输了。

总决赛是天津饭和孙悟空打,天津饭在战斗中觉醒了武道正义感,变成了好人。但最后因为运气稍差,孙悟空又得了亚军。

武道会结束后,库林在武道馆被神秘人杀了,孙悟空愤怒地去追,但因为体力耗尽,没能打过对方。龟仙人根据现场的东西,推断出大魔王又出现了。

孙悟空结识了亚奇洛贝,他们一起杀了几个魔族战士,结果引来了大魔王。孙悟空打不过,被打得晕了过去。龟仙人、天津饭、饺子在收集龙珠时,也遇到了大魔王。龟仙人挺身而出,想使用魔封波封印他,但失败了,龟仙人战死了。神龙出现后,饺子也战死了,大魔王恢复年轻,开始了他的恐怖统治。

天津饭想修炼魔封波来封印大魔王,但把封具弄坏了。孙悟空在亚奇洛贝的帮助下,再次见到了卡林仙人。他喝下蕴含剧毒的超神水,以命赌力,终于成功突破了!

突破后的孙悟空在国王宫殿旁击败了大魔王,再次拯救了地球!大魔王死前留下孙家保姆,说“魔族永远不会灭绝”。击败大魔王的孙悟空获得了面见天神的机会,他通过如意棒前往天界!在天界与波波交手之后,天神闪亮登场,一手弹开孙悟空,秀得一手好操作!天神复活神龙之后,库林、龟仙人、饺子等人也复活了。孙悟空则留在天界继续修行!

比克大魔王

艾纪753

第22届天下第一武道会正式开始了,最后天津饭赢了,悟空又得了亚军。可就在这时候,库林突然被杀,比克大魔王又出来了。后来,在地球国王掌政20周年纪念的第二天,悟空打败了比克大魔王,地球又恢复了和平,比克二代也出现了。然后,悟空就去了天神神殿,开始了神仙和波波先生指导下的三年修炼。

第三次武道会,比克大魔王

艾纪755年(18岁)

这段时间发生了好多大事!先是刚满18岁的孙悟空和琪琪完成了他们儿时的约定,结婚了。然后比克大魔王也来参加武道会,还打败了天神,嚣张得不行。不过最后孙悟空还是艰难地赢了他,粉碎了他想统治世界的野心。接着,孙悟空终于拿到了天下第一武道会的冠军,和琪琪一起在包子山定居了。哦对了,兰斯追着天津饭不知道跑到哪里去了。

再说说后来吧,第23届天下第一武道会又开始了,这次悟空打败了比克二代,终于第一次夺得了武道会的桂冠。比赛结束后,他们还订婚了。然后,艾纪756年,孙悟空19岁的时候,他们的儿子孙悟饭出生了。同年,孙悟饭的未来妻子比迪丽也出生了。这时间过得真快,事情也真多,真是让人感慨万千!

赛亚人地球战

艾纪760年(23岁)

有个外星神秘战士叫拉蒂兹的跑到地球来了,他一来就揭露了孙悟空的身世,原来孙悟空是赛亚人,这下赛亚人正式进入龙珠历史了。

拉蒂兹还想利用孙悟饭,拉拢孙悟空入伙。不过,比克和孙悟空联手对战拉蒂兹,结果也不占优势。

危急时刻,孙悟饭怒气爆发,竟然击伤了拉蒂兹。

拉蒂兹是孙悟空的亲哥哥,作为一名上级赛亚人战士,他来到地球寻找孙悟空,目的是拉拢孙悟空加入他们的行列,共同征服世界。然而,孙悟空拒绝了拉蒂兹的要求,这导致了双方的激烈冲突。

在与孙悟空和比克的战斗中,拉蒂兹展现了强大的实力,一度占据上风。然而,孙悟空在战斗中逐渐找到了拉蒂兹的弱点,并利用这一点展开反击。

在危急关头,孙悟空紧紧地抱住了拉蒂兹,使其无法动弹。同时,他请求比克使用绝招“魔贯光杀炮”来终结战斗。

比克听从了孙悟空的请求,发射出了强大的“魔贯光杀炮”,这一击直接贯穿了孙悟空和拉蒂兹的身体。

龙珠的信息也泄露了,听说更强的赛亚人一年后就要入侵地球。

比克赶紧带走孙悟饭去修练了,天津饭、库林、饺子、亚姆查、亚奇洛贝也跑到天界去修练了。

至于孙悟空嘛,他跑到阴间跟界王学习去了!这龙珠世界真是越来越热闹了!

艾纪761年

贝吉塔和那巴这俩人,他们降落到了一个叫阿鲁尼亚的星球上,结果一看,觉得没啥用,就直接把它给炸了。炸完之后,他俩就进入冬眠状态了,说是要睡一年,等到抵达地球的时候再醒过来。

再说孙悟空,他这时候也复活了,从界王星赶回了阎王殿。

后来,库林、天津饭、饺子、亚姆查和弥次郎兵卫这几个人,他们都跑到天神神殿去修行了。

悟空呢,他也没闲着,他到了北界王星,还在巨大的重力环境下抓住了巴布鲁斯。然后,他得用槌子敲中格雷哥利的脑袋,这事儿可不容易。不过呢,悟空最后还是做到了,用槌子打中了格雷哥利。

还有天津饭、亚姆查、库林和饺子他们,他们几个人还跑到“过去”的“贝吉塔星”上,跟赛亚人打了起来。结果呢,两个低等战士就差点把他们给“干掉”了。回到自己的身体后,他们发誓要发奋图强,一定要变得更强。最后,悟空也从界王那里完成了修业,重生了。

艾纪762年

贝吉塔跟那巴来到地球后,悟饭、比克、天津饭、库林、亚姆查还有饺子他们一起对抗这两个赛亚人。亚姆查这家伙,身先士卒,冲在最前面,结果第一个战死,算是博了个头筹吧。饺子也不甘示弱,紧跟着也自爆身亡了,真是惨烈!天津饭那时候说,不求同生,但求同死,双宿双栖,也是让人感慨万千。最后呢,悟空跟贝吉塔打了个平手,这场打斗才结束。贝吉塔乘坐太空船18日回到弗利萨星球。也是在那一天,悟空终于知道了他爷爷是怎么死的。同时,有个叫格罗博士的人,他派了个小机器人在这附近收集那些强大战士的细胞,想做个超强的生命体出来。

本篇中拉蒂兹战斗力情况如下:

  • 拉蒂兹:战斗力1500
  • 孙悟空:常态416,龟派神功924
  • 比克:战斗力408,魔贯光杀炮1330
  • 孙悟饭:愤怒1307

本篇中贝吉塔战斗力情况如下:

  • 天津饭:1830
  • 栽培人:1200
  • 那巴:4000,聚气8000
  • 亚姆查:1480
  • 饺子:610
  • 比克:3500,为打倒赛亚人提升
  • 库林:1770
  • 孙悟饭:愤怒2800
  • 孙悟空:常态8000,2倍界王拳1.6万,3倍2.4万,4倍界王拳3.2万
  • 贝吉塔:常态1.8万,闪光炮2.4万,虚弱7000,巨猿化7万

那美克星

艾纪762年

库林、悟饭和悟空打完架,伤得不轻,都被送去医院治伤了。过了没几天,库林和悟饭就康复出院了。紧接着,天神的飞船也修好了,速度快得很。

然后,布尔玛真是个天才,没几天就学会了娜美克星人的语言,厉害得不得了。她拉上库林和悟饭,三个人开着飞船就直奔娜美克星去了,用了34天时间,如果使用地球技术飞船那是需要四千年才能到。

另一边呢,贝吉塔在弗利萨星球的79区也受了重伤,但他恢复得也快,没多久就完全好了。一出疗养舱,他也二话不说,直接就往娜美克星赶。这几个人目标都是一样的,都奔着娜美克星去了。

布尔玛、孙悟饭、库林和贝吉塔一起降落到了娜美克星。同时,悟空也康复出院,急匆匆地赶往娜美克星。丹迪和库林去见大长老,而天津饭、亚姆查、饺子和比克则去了北界王星特训。

那美克星这边的代表是年纪小但能力超强的丹迪。这地方势力可复杂了,有像万年打野的贝吉塔,单人成团到处跑;有弗利萨主宰着,还带着一群手下;还有一群温和的那美克星人;最后就是咱们的主角们像防御塔一样守着各个地方。

贝吉塔先杀了多多利亚

后来,萨波击败了贝吉塔,把他带到了弗利萨的飞船里。赛亚人有个能力,濒死复活能增加战斗力。但贝吉塔不甘示弱,再次挑战萨波,终于把他干掉了。这时,库林和孙悟饭也见到了大长老。

另一方面,基纽战队原本要去雅路达星,但计划有变,他们被派去娜美克星协助弗利萨,他们乘坐飞船用了5天到达那美克星。而悟空乘坐了布尔玛父亲做的太空船在一百倍重力的环境下修炼,终于适应了那种极端条件。孙悟空在一百倍重力室修炼后,实力大涨,飞船用了6天时间到达那美克星,悟空打败了基纽特战队。但基纽战队的队友会换身技能,把孙悟空弄伤得很严重,他只好进疗养仓了,弗利萨星球疗伤技术很高,遍体鳞伤也只需要一个小时痊愈,如果是地球医疗技术的话,四个月也难痊愈。

趁着贝吉塔休息,库林、孙悟饭还有丹迪偷偷召唤神龙,救了比克,还把比克送到那美克星。第三个愿望还没许,大长老寿命到了,神龙就消失了。

弗利萨知道神龙死了,气坏了。贝吉塔、比克、库林、孙悟饭、丹迪五个人联手,还是打不过弗利萨,贝吉塔还战死了。

孙悟空养好伤回来,战斗力飙升,跟弗利萨苦战。后来库林被弗利萨杀了,悟空愤怒至极,他第一次变身成超级赛亚人,最终击败了弗利萨。

那美克星人被地球神龙复活,又由波仑伽(大长老复活短时间)传送到地球。最后那美克星爆炸,孙悟空坐着基纽特战队的飞船去雅德拉克星学瞬间移动了。

此阶段战斗力情况如下:

  • 库林:1500,大长老激发潜力1.3万
  • 悟饭:1500,大长老激发潜力1.4万,对弗利萨第三形态愤怒200万
  • 丘夷:1.8万
  • 贝吉塔:常态2.4万,萨博击败疗伤后3万,再次大难不死后12万,对弗利萨25万,对弗利萨第四形态250万
  • 多多利亚:2.2万
  • 萨博:2.2万,变身2.8万
  • 古尔多:1万,有超能力
  • 利库姆:4.2万
  • 巴特:4.5万,速度宇宙第一
  • 孙悟空:9万,2倍界王拳18万,重伤复活300万,10倍界王拳3000万,20倍界王拳6000万,超级赛亚人1.5亿
  • 吉斯:4.5万
  • 基纽:12万
  • 内鲁:4.2万
  • 比克:和内鲁同化后150万,胜于弗利萨第二形态
  • 弗利萨:第一形态53万,第二形态120万,第三形态220完胜比克,弗利萨最终形态1.2亿

特兰克斯

库林和亚姆查,这两个家伙,后来靠着那美克星那条神奇的神龙,嗖的一下,就从哪儿来回到地球了。

没过多久,饺子和天津饭也回来了,好事成双。那些剩下的娜美克星人,神龙也帮忙找了个新家,新娜美克星,听起来就不错。

然后沙鲁杀了未来的特兰克斯,偷偷搭了特兰克斯的时光机,成为一个蛋,进入地下孵化。在地底下猫了好多年,自己悄悄地吸收能量,准备搞事呢。

还有,弗利萨,也给修好了,没完没了。

转眼第二年,年初那会儿,贝吉塔可拼了,整天泡在重力室里修炼,就想变成超级赛亚人。练过头了,就会受伤,经布尔玛精心照料,两人就好上了,感情升温得跟坐火箭似的。

艾纪764年
弗利萨父子跑到地球来了。这时候未来特兰克斯出现啦!他是从 20 年后的未来来的,也就是艾纪 784 年。特兰克斯把弗利萨父子打败了,还给孙悟空带来了治心脏病的特效药,跟孙悟空说了人造人的那些事儿。

此阶段战斗力情况如下:

  • 机械弗利萨:1.4亿
  • 库尔德王:8000万,弗利萨的父亲
  • 特兰克斯:6亿
  • 孙悟空:8亿,亚德拉特星修炼结果,心脏病4亿以下
  • 人造人19号:4.2亿
  • 人造人20号:4.2亿
  • 贝吉塔:10亿
  • 比克:5亿,和天神合体15亿
  • 人造人18号:12亿
  • 人造人17号:15亿
  • 人造人16号:19亿

沙鲁

艾纪767年

弗利萨父子进攻地球的时候,遇到了来自未来的特兰克斯,这小伙子真不简单,一下子就把弗利萨父子给打败了。可紧接着,人造人出现了,悟空跟人造人19号、20号打起来,可是心脏病毒让他受不了,只好停下来。这时候,贝吉塔以超级赛亚人的身份出现了,真是帅啊!

没过多久,特兰克斯又来了,这次他从更远的未来回来,他发现在他那个时代的三年后,有个叫沙鲁的人造人出现了。哎呀,时间线就这么对上了。

库林和未来的特兰克斯找到了格罗博士的实验室,把那个还没完全成型的沙鲁给干掉了。

然后,人造人16、17、18号都冒出来了,沙鲁还吸收了17、18号,变得更强了。

孙悟空他们这边也不甘示弱,比克和天神合体,龙珠失效后,过了两天,悟空吃了特兰克斯带来的药,慢慢好起来了。孙悟空立马瞬移到新那美克星找新任天神丹迪,把龙珠给复活了。

贝吉塔和特兰克斯父子俩也进入精神时光屋去修炼了。

贝吉塔也突破了自己的极限,可惜还是没能打败完全体的沙鲁。特兰克斯也一样,虽然开发出了超级赛亚人一第三阶,但还是败给了沙鲁。

沙鲁变得更强了。然后悟空和悟饭也去精神时光屋修炼了。

后来沙鲁还搞了个沙鲁游戏,真是嚣张。孙悟空和孙悟饭进入精神时光屋修炼,出来后孙悟空虽然变强了,但还是没能打败沙鲁。最后,孙悟饭在沙鲁、16号和撒旦的帮助下,终于突破极限,变成了超级赛亚人二,成功打败了沙鲁。但因为骄傲自大,导致悟空又牺牲了,不过最后还是悟饭愤怒的一击,终于把沙鲁给消灭了,拯救了地球。可惜的是,孙悟空在这场战斗中牺牲了。

鸟山明曾说,第二阶段的沙鲁是他最喜欢的角色。大家都笑他是不是傻,尤其是编辑近藤裕。近藤以前做少女漫画杂志,看惯了帅哥,怎么能接受这种形象?不过这只是开玩笑,近藤其实很有深度。

近藤裕非常擅长设计人物形象,而且他总是根据大众心理来设计,效果往往非常好。比如弗利萨的形象,就是他提出要塑造成一个宇宙地产商的点子——在泡沫经济鼎盛时期,炒地皮的最招人恨。

近藤不喜欢二阶沙鲁,催着鸟山明赶快画出新形态,是因为他考虑到未来的胜负和故事整体。如果敌人太丑,大家会觉得主角赢是理所当然的结果;而敌人是帅哥的话,读者才会担心悟空能不能赢。事实证明,近藤的意见是对的——大部分人都认为完美沙鲁稳压悟空一头,沙鲁几乎是不可战胜的,这场决斗的胜负归属一直是龙珠中的热门话题。

那鸟山明为什么最喜欢二阶沙鲁呢?他的审美真的这么特别吗?一方面是因为他喜欢日本特摄剧,比起动漫,他更喜欢奥特曼、哥斯拉、超级战队这些特摄剧,并且在作品中致敬过。那么假面骑士怎么能落下呢?鸟山明在采访中表示,自己最喜欢的动物之一就是飞蝗(好在他没真在家里养这个),沙鲁的昆虫原型就是受到假面骑士的影响而诞生的。斑点难画?他心里乐意着呢。而且画斑点明显是助手的工作,鸟山明自嘲说斑点麻烦,只是习惯性地凡尔赛罢了。

此阶段战斗力情况如下:

  • 特兰克斯:超级赛亚人第二阶段150亿,超级赛亚人第三阶段225亿,超级赛亚人全功率250亿
  • 孙悟空:超级赛亚人全功率350亿
  • 贝吉塔:超级赛亚人第二阶段150亿,超级赛亚人全功率250亿
  • 沙鲁:第一形态11亿,第一形态吸收人类精华19亿和16号持平,第二形态95亿,沙鲁完全体初登场200亿,后期380亿,拳力500亿,闪电沙鲁1000亿
  • 小沙鲁:250亿
  • 孙悟饭:超级赛亚人全功率330亿,超级赛亚人全功率愤怒450亿,超级赛亚人2战斗力900亿

布欧

悟空的葬礼上,大家都参加了。之后,未来的特兰克斯回到自己的时代,消灭了未来的17号、18号和沙鲁。

接下来几个月,有一部关于撒旦先生的电视特别节目,详细介绍了他的生平,但没有提到沙鲁之战。撒旦被认为是地球上最强的格斗家。

库林和18号结婚了,他们的女儿玛伦也出生了。界王神和杰比特来到地球,寻找魔人布欧的蛋壳。悟饭升上了橙星高中的一年级。

一位金发战士保卫撒旦市的故事传遍了大街小巷。悟饭在橙星高中认识了撒旦的女儿比迪丽。下午三点左右,悟饭请布尔玛为他制作一件战斗服,布尔玛同意了。大约五点,超级赛亚蒙面超人诞生了。

比迪丽发现赛亚蒙面超人其实就是悟饭。接着,悟饭教比迪丽飞行,南界王见识到悟空惊人的修炼方式。小特兰克斯在他父亲面前第一次变成超级赛亚人。比迪丽终于掌握了舞空术。

第25届天下第一武道会召开,悟空获准离开阴间一天来参加比赛。贝吉塔通过魔导师巴比迪的控制,变身超级赛亚人2,并与悟空打斗,导致魔人布欧在地球上苏醒。悟饭拔出界王神剑,贝吉塔为了亲人自爆拯救了地球。胖布欧杀死了魔导师巴比迪。悟空展示超级赛亚人3的威力后提前返回阴间。悟天和特兰克斯开始练习合体,第三次才成功。悟饭把界王神剑折断,释放了老界王神,老界王神决定为悟饭引发出更深藏的潜能。

胖布欧在撒旦的感化下逐渐平静,但撒旦被杀后,胖布欧释放出瘦布欧,吃了胖布欧后变成大布欧。大布欧在精神时光屋与悟天克斯混战,打到下界。大布欧吸收了悟饭、悟天、特兰克斯和比克。在大布欧要杀光地球所有人的紧急关头,老界王神将他的性命送给悟空,让他再返人间作战。贝吉塔在水晶婆婆的协助下也回到了地球。两人第一次用耳环合体,变身为贝吉特,成功进入大布欧体内救出众人,使大布欧恢复成小布欧。小布欧炸毁了地球,但那美克星神龙让地球恢复原样。贝吉塔也因地球人全体复活而重生。悟空用一枚特大元气弹消灭了布欧。

神龙将人们心中对布欧的记忆完全抹掉。几年后,第26届天下第一武道会举行,撒旦先生夺得第一,胖布欧第二。

此阶段战斗力情况如下:

  • 孙悟天:19亿
  • 特兰克斯:20亿
  • 悟天克斯:超3战斗力8000亿
  • 悟饭:神秘悟饭1.2兆
  • 胖布欧:5000亿
  • 瘦布欧:3500亿
  • 大布欧:7000亿

龙珠超

破坏神比鲁斯来袭,悟空拼尽全力抵消了比鲁斯的“灼热弹”,比鲁斯很赞赏悟空,并决定不再破坏地球。

小芳出生。同年,弗利萨复活,修炼出金色形态,带军队来地球复仇。悟空击败弗利萨,弗利萨再次被送回“地狱”。第七宇宙和第六宇宙的破坏神比武大会开始,第七宇宙获胜。未来的扎马斯入侵,特兰克斯回到过去求助,最后合体扎马斯被未来全王消灭,未来全王和现世全王成为朋友。

布拉出生。同年,全王举办力之大会,失败的宇宙将被清除。悟空、贝吉塔、悟饭等十人代表第七宇宙参赛,悟空在大会上首次达成自在极意,第七宇宙获胜。人造人17号用超级龙珠许愿复活被清除的宇宙,大家回到正常生活。弗利萨的两个小兵在万帕星找到布罗利父子,悟空一伙寻找龙珠时遇到弗利萨军,悟空和达尔与布罗利交战,最后用美达摩融合术压制布罗利,但在消灭他的一瞬间,布罗利被神龙传送到万帕星。悟空送给布罗利物资,并表示想通过与他对战变强以超越比鲁斯。实习天使梅尔斯因违背天使中立准则而消失,孙悟空完全掌握自在极意,梅尔斯因大神官转生成人类而复活。

第27届天下第一武道会举行,撒旦先生夺得第一,“胖布欧”第二。第28届天下第一武道会举行,悟空在比赛场上将布欧的转世——欧布带走修炼。

龙珠GT

悟空被皮拉夫用黑星龙珠变成了8岁。悟空、小芳和特兰克斯乘宇宙飞船出发寻找龙珠。九个月后,他们遇上了贝比。贝比被击败后,潜入地球控制了几乎所有人,并变得更强大。贝比用黑星龙珠重建了祖福鲁星。悟空变身超级赛亚人4,与贝比展开大战,最终贝比被灭,祖福鲁星人也因地球人被治愈而灭绝。

黑星龙珠的诅咒导致地球爆炸,所有生命迁往祖福鲁星。那美克星龙珠将地球复原后,大家迁回地球。第30届天下第一武道会举行,撒旦再次获得冠军。

超级17号进化完成,但被悟空和18号联手击败。龙珠出现裂痕,邪恶龙肆虐世界。悟空最终击败一星龙,神龙重现并带走悟空,七颗龙珠融入悟空的身体,悟空离开了一百年。

悟空的玄孙出生,为纪念祖先取名小孙悟空。小悟空在独自寻找龙珠的冒险中激发了超级赛亚人潜能。在祖居门前,小孙悟空与显灵的祖父见面。

第63届天下第一武道会举行,小孙悟空与贝吉塔的玄孙角逐少年组决赛桂冠,结果未明。110岁的小芳在观众席上看到祖父悟空的身影,但未能追上。悟空和龙珠的故事到此画上完美的休止符。

龙珠中的道具清单

  • 龙珠:收集七颗龙珠能够实现一个愿望,丹迪制作的可以实现三个,可一次复活多人。只有那美克星龙族才能制作龙珠。那美克星大长老制作的龙珠能实现三个愿望,但是一次只能复活一个人,后期新的可以让多人复活。龙珠实现的愿望中,最让人感动的是库林让神龙拆掉18号和17号体内的爆炸装置。
  • 筋斗云:只有心灵纯洁的人才能坐上去。曾经坐上去的人有,孙悟空、悟饭、人鱼(悟空按龟仙人要求找的人)、琪琪、兰奇、欧布等。
  • 神奇胶囊:可以将房子汽车等物品装到一个瓶子里。打开瓶盖就能还原。由布尔玛父亲发明。
  • 如意棒:可以随意伸长,悟空就是用它直接伸到神殿。
  • PP糖:吃下的人听到PP就会拉肚子,效果会持续一个月,布尔玛用在了乌龙身上。
  • 龟壳:龟仙人所背,重量大,也用于悟空库林的训练。
  • 仙豆:重伤可痊愈。每次不可大量种植,数量很有限。但是仙豆不能治疗疾病,比如悟空心脏病发作时,仙豆不起作用。
  • 超圣水:里面只是普通的水。
  • 超神水:有剧毒,但是如果有强大的体能和意志力就能够引出潜能。悟空是唯一喝过没死的人。
  • 战斗机器人:红绸军黑参谋对付悟空的可操作的机器人。
  • 皮拉夫的机器人:皮拉夫三人组每人一个可操作的机器人,还可以合体。
  • 比克大魔王的封印:用来封住比克大魔王的咒印。将其贴在瓶子上,使用魔封波就可以将比克大魔王封印住。
  • 侦查器:按一下就可以知道对手的战斗力,还可以进行星际对话。旧款上限是2.2万,新款没有上限。
  • 弗利萨军战斗服:超级橡胶制成,柔软性和防御力都很高,很有弹性,什么身材都能穿进去,几乎感觉不到重量。简直就是最理想的宅男服装。
  • 栽培人套装:赛亚人的科学生物,播种后滴上栽培液,就会诞生栽培人,战斗力还不错,但不会说话。
  • 魔法飞毯:波波使用的交通工具,一瞬间将布尔玛带到天神的宇宙飞船那。
  • 光束枪:弗利萨的士兵使用的枪,布尔玛库林悟饭一行到那美克星的飞船被光束枪一枪击毙。
  • 治疗机:弗利萨军的治疗装置,最多四十分钟就能痊愈。
  • 特兰克斯的剑:一剑砍死悟空打了几十页漫画的弗利萨,非常的耀眼。
  • 紧急停止控制器:用于停止17号和18号活动的装置。需要在10米内使用。
  • 变身服装和手表:悟饭高中时,做好事为了隐藏身份所穿,悟饭拜托布尔玛制作的。
  • 测拳机:第24届天下第一武道会上开始使用的,最高纪录时撒旦打出的139分。
  • 能量吸收器:用于解开布欧封印的能量吸收器。
  • 封印蛋:封印魔人布欧的蛋。
  • 终极之剑:传说一拔出来就能够提升力量的剑。原因是老界王神被封印在剑内,他会帮助拔剑人提升力量。
  • 卡先钢:全宇宙最坚硬的金属。使用终极之剑都砍不开。
  • 天界神珠:带上的两人可以合体,合体后力量提升。

龙珠世界全地图

龙珠世界氛围天界和宇宙,天界包括阎王殿、蛇道、界王星、地狱和天国。宇宙包含了地球、那美克星、弗利萨星和贝吉塔星等星球。

天界位于宇宙之上,里面有裁判死者的阎王殿、天国和地狱,是神管理世界的地方。好人会去天国,坏人去地狱,如果被魔族所杀灵魂只能在宇宙飘浮。阎王殿是死者灵魂的入口,蛇道连接着阎王殿和界王星,界王星很小,只有界王和他的宠物阿布住在上面,重力是地球的十倍,界王的生活很简单很舒适,数数草,眺望天空,还可以看小便撒的多远。

那美克星科技是先进的,可以造出超光速宇宙飞船,但是那美克星人却因为天气问题几乎灭绝,为了恢复星球,仅存的那美克星人开始了种植花花草草的安稳生活。那美克星人分为龙族和战士两个,那美克星人没有性别,通过口中吐蛋进行繁殖。

贝吉塔星住着赛亚人,他们大部分都是弗利萨的雇佣兵,会去侵略其他星球。最后弗利萨害怕赛亚星会出现传说中的超级赛亚人,于是将贝吉塔星摧毁,赛亚人几乎灭绝。赛亚人是好战的名族,从小被灌输战斗的思想,使得他们天生就很享受战斗。

龙珠世界的地球只有一个国家,由国王统治,全国有四十三个区。地球的科技主要是胶囊公司带来的,也就是布尔玛他们家的公司。反重力装置交通工具很普及,地球人有人类,比如库林,动物类,比如乌龙,还有怪物类,比如皮拉夫。

地球北部地区有中都、东都、牧场、吉古鲁村、红绸军白队基地等地方。中都是国王的都城,比克大魔王就是在这里让国王屈服的。东都是东北部最大城市,贝吉塔和那巴就是在这里着陆的。拉蒂兹是在牧场着陆的。吉古鲁村是常年被大雪覆盖的城市,那里的人淳朴热情,小悟空帮他们打败了红绸军白队基地。

地球东部有兔子军团镇、乌龙的村庄、亚姆查的住处、皮拉夫城堡和海盗洞穴。

西部有圣地卡林,卡林塔、悟饭修行地、西都、胶囊公司和红绸军总部。卡林塔居住着卡林仙人,悟饭修行地是比克为了对抗赛亚人专门培训悟饭的地方。西都是地球科技最发达的地方,胶囊公司也在西部。

南部有龟仙人的小屋、企鹅村、火焰山、天下第一武道馆、占星婆婆宫殿。

鸟山明

家乡

鸟山明先生,家住 爱知县 名古屋市清州。名古屋是日本重工业基地,飞机汽车很繁荣,也导致鸟山明很喜欢一些机械的东西。但是清州很偏,导致鸟山明一直都只有一个助手,几乎所有创作和作画都是一个人完成。

1983年,他连载《阿拉蕾Q》时,创下“六亿四千七百四十五万日元”的漫画家纳税最高记录。阿拉蕾结束时鸟山明本打算去过自由自在的生活,游泳、赛车、玩模型和旅游,但是被鸟岛和彦告知业界残酷,让他很快回归业界,这才有了龙珠。

画画方法

作者通常会先画出NAME给责任编辑看,双方讨论后再修改细节。然后作者在稿纸上打草稿、勾线,助手负责涂黑、涂白和贴网点。

NAME可以画在普通笔记本上,只要有大概的分格和轮廓,作者和编辑能看懂就行。为了省力,画得潦草是正常的。

鸟山明连载时不画NAME,直接从底稿开始,改动很少。他曾说过:“为了少做修改,我会把稿子拖到最后一刻再交给编辑,实在没办法编辑也只能认了。”这种拖延战术我们都懂,但不是每个人都能像他一样一出手就是高质量底稿。

说到画画风格,龙珠的舞台总是很荒凉,鸟山明是觉得画街巷太复杂,阿拉蕾的背景也是用圈圈状的山和树木这种省事的方式来糊弄过去。因为住在乡村买网点纸很麻烦,也就用黑白做了基调。

鸟山明不会偷懒,他扎实地练习场景透视、人体比例、情节节奏和人物塑造。透视不过关,他就堆细节,用花纹和建筑填满画面,多分格,少画全景。人物比例画不好,就贴网点或用声效字遮挡,甚至用无意义的破格吸引读者。

人物成长和互动复杂,容易出错,他设计不同的价值观让角色自己动起来,而不是贴现成的性格标签。住在乡下,只有一名助手,背景不画建筑,头发不用涂黑,减轻助手负担。作画工具不高档,他只能大刀阔斧地画,时间有限,能推掉彩页就推掉,不讲与主线无关的故事,不乱埋伏笔,不无限拓展剧情。

鸟山明画漫画也不是一开始有很厉害的,周刊少年编辑鸟岛和彦说他第一次收到鸟山明漫画是临摹星球大战的作品,这类作品是不可刊登的,他是对漫画中的文字绘画感觉新鲜,这才有去联系鸟山明。

日本坚持黑白漫画,因为彩色漫画虽然好看,但成本高。JUMP每期有几页彩页和拉页海报,但大部分是黑白的,用的是便宜的纸张,定价低,小孩子都能买得起。

集英社的全彩版龙珠只关注色彩,不重视黑白基调,效果不好。鸟山明最初用普通的透明水彩,后来用签字笔融水涂色,效果不错。1981年,他在《りぼん》杂志的访谈中了解到彩色墨水的使用方法,后来常用Luma牌墨水。他还向动画导演和制作人员学习上色技巧。

助手

一个人又要拼命想故事,还得小心翼翼别踩坑,画技还得天天磨,连载的压力大得跟山似的,还得想着怎么快点火起来。能按时交稿,质量还不差,那已经是超人水平了!这时候再让他每周都给画上色,还没人帮忙分担,换谁不崩溃啊?

田中久志

说回鸟山明,他那时候可惨了,啥都得自己干,连个助手影儿都没有。可能他之前都不知道还有漫画助手这职业呢,毕竟以前都是画短篇的。他家那地儿偏得要命,想找人都找不着。要是在东京,鸟嶋和彦那哥们儿肯定能帮上忙,但鸟山明非要在家搞创作,那就只能自己想办法了。好在,他加入了个小圈子,里面有个叫田中久志的,后来成了他的第一任助手,不过这家伙一周才来一趟,帮不了太多忙。但人家可是厉害角色,参加过比赛还拿过亚军呢,现在都成大学教授了。

谷上

再来说说谷上,这位是东京来的机械天才,鸟山明都夸他。但不知道为啥,没多久就走了,存在感超低。那时候龙珠火得不行,如果是在大城市,来应征的人得挤破门,但鸟山明家那地儿太偏了。

松山孝司

还有松山孝司,这家伙跟鸟山明那叫一个默契,俩人兴趣相投,简直就是灵魂伴侣。松山不仅是助手,还是模型手办的高手,拿奖拿到手软。他从阿拉蕾后期就开始跟着鸟山明,一直到龙珠结束,整整12年!鸟山明还特地为松山减负,超级赛亚人的头发都不涂黑了,就是为了让他轻松点。俩人工作之余还一起抽烟、聊电影、打游戏、骑摩托,简直不要太爽!

松山结婚的时候,鸟山明还特地留言说以后不让他加班太晚,可见两人关系多铁了。总之,鸟山明能画出那么火的漫画,松山孝司功不可没!

一边要绞尽脑汁创作故事、一边要避免踩坑、一边要打磨画技、一边要适应连载的工作强度、一边还要争取尽快积攒人气,能保质保量地按时交稿就不错了,这时候再让他们每周都上色,又找不到人分担压力,换谁不得崩溃?

历任编辑

鸟岛和彦

鸟岛和彦这个人,大家可能不太熟悉,但说到龙珠里的马西利特博士和比克大魔王,动漫迷们应该都知道。他们的原型其实就是鸟岛和彦,他不仅是鸟山明的第一任编辑,还是个厉害的角色。80年代的时候,他就开始搞游戏业务,还推动了漫画和游戏的关系。就像那个《达伊大冒险》,其实就是为了展示游戏和漫画的紧密联系。他甚至想让鸟山明把龙珠的故事扩展到宇宙,还想让鸟山明和高桥留美子一起设计个RPG游戏。这哥们儿自己也创办了本超火的游戏杂志V-Jump,后来还当上了少年JUMP的主编,开始大刀阔斧地改革。

近藤裕则

然后说说近藤裕则,他是鸟山明的第二代编辑,也是弗利萨的原型。别看他彬彬有礼,严厉程度可不低。他喜欢帅哥,所以老是催鸟山明赶紧让沙鲁完全体出场。但鸟山明其实更喜欢画异形和昆虫这类的东西,像蓝将军和萨博这样的帅哥他画得并不多。

武田冬门

最后来聊聊武田冬门,这哥们儿是鸟山明的第三代编辑,也是胖布欧的原型。他可是鸟山明的超级粉丝,对《布欧篇》的创作,他基本上就是:“哥,你随便画,我都爱看。”所以,鸟山明就创造出了那个超可爱的天真胖布欧。

爱好模型

鸟山明工作房间有个大桌子用来拼模型,身后的另一个大桌子也是拼模型用的,比他在角落里画画的桌面大了三四倍不止。漫画只是糊口的手艺,模型才是真爱啊。

爱好摩托车

据说鸟山明的父亲曾参加过摩托车比赛,拥有一家汽车维修公司,鸟山明说自己一有空就会去摩托车改装店。

鸟山明还透露过他除了设计游戏还设计汽车,但因为保密协议,他没有透入是为哪家公司设计汽车,他觉得能够以门外汉的身份去设计可以好好乘坐的汽车是他生活的意义。

爱好游戏

鸟山明曾说自己是做事比较认真、一旦钻进去就很投入的那种人——从他的模型上就能看得出来——结果有了红白机之后,一下子迷上电子游戏,难以自拔,他说:“本来只是想随便买个红白机玩玩,结果却上瘾了,玩得手指都疼啦。甭管工作多么辛苦,我的手指从没长过茧。奇怪的是,一玩起红白机来,手上竟然长出了茧。”

老师曾说:“我的假期全都耗在《勇者斗恶龙III》上了。既高兴,又难过。但游戏本身确实非常有趣。白天去游泳池,晚上沉迷红白机上的《母亲》游戏,最后只能半夜工作。”

后来他还与堀井雄二、坂口博信这个「梦幻团队」开发的《时空之轮》。

爱好养宠物

鸟山明特别喜欢养小动物,尤其是狗和鸟。

他养的第一只狗叫“涡轮丸”,挺有个性的名字吧。后来呢,他又买了只哈士奇,给取了个名字叫“马特”,哦对,原名是“俄罗斯套娃”,但叫“马特”更亲切些。

再后来,他又看上了柯基这种狗,于是就把“马特”这两个字颠倒一下,叫“托马”了。

不仅仅是家里养的小动物,鸟山明家里还常常有些“不请自来”的客人,比如野猫、蜥蜴、乌龟、老鼠,甚至还有蛇!

野猫经常来找家里的锅巴打架,也是挺有趣的。蜥蜴和乌龟他最后都放生了,也没看到它们回来报答他。说到老鼠,那可真是让他头疼,晚上一过街老鼠都能把他吓得跳进田里。家里的老鼠更是麻烦,最后用了“灭鼠110”才解决。他家的猫还特别有趣,每次抓到老鼠都送到他面前,真是让人哭笑不得。

哦对了,还有毒蛇呢,像日本蝮、响尾蛇这种,他都好几次死里逃生,真是命大!不过说实话,他更烦那些到处跑的蟑螂和永远打不完的苍蝇蚊子。

这周边的小动物实在是太多了,有时候真的吵得他都没法专心画画。

作品

鸟山明出来长篇IQ博士和龙珠外还有很多短篇漫画,以下是按年排列的作品列表。

  • 1978年:「神秘的rainiack」完成后,「Awawaworld」角逐 Young Jump 新人赏,「WONDER ISLAND」「WONDER ISLAND2」(刊於WJ増1/25)
  • 1979年:「本日的HIRI岛」,「GAL刑事TOMATO」
  • 1980年:「IQ博士」
  • 1981年:「POLA & ROID」,「ESCAPE」
  • 1982年:「MAD MATIC」,「HETAPPI漫画研究所」,「PINK」
  • 1983年:「CНОВІТ」,「CHOBIT2」,「骑龙少年 其壹」,「骑龙少年 其贰」,「东风大冒险」
  • 1984年:「龙珠」
  • 1986年:「MI Hoo」
  • 1987年:「LADY RED」,「剑之介大人」
  • 1988年:「SONCHOH」,「豆次郎」
  • 1989年:「小忍者空丸」
  • 1990年:「WOLF」,「CASHMAN」
  • 1992年:「TRUNKS THE STORY 唯一的战士」,「DUB & PETER1」
  • 1993年:「GO!GOIACKMAN」
  • 1996年:「宇宙人PEKE」,「TOKIMECHA」
  • 1997年:「魔人村的BUBUL」,「COWAI」
  • 1998年:「河鹿」,「肺魚鯕鰍」
  • 1999年:「猫魔人在此」,「猫魔人在此2」
  • 2000年:「HYOUTAMU」,「SANDLAND」
  • 2001年:「猫魔人Z」
  • 2003年:「TOCCIO THE ANGEL」,「猫魔人Z2」,「三色猫魔人」

海南版七龙珠

海南摄影美术出版社,它是由海南省新闻出版局花了11万块钱建立起来的,是个挺正式的省级单位。刚开张那会儿,他们也不知道出啥书好,就尝试搞了些人体写真、美女挂历啊,还有些壮阳秘籍、鬼怪故事、养生菜谱之类的,当然也包括了介绍海南风情的画册和连环画。不过,这些书受众面可比漫画广多了。

说到漫画,海南版的《龙珠》那可是真牛,品质高,速度快,其他出版社都追不上,只能跟在后面抄作业。1991年1月,第一本《龙珠》就问世了,接着3月份第一卷就全套上架了。到了1992年2月,故事都快讲到人造人和未来战士那段了,一年之内就出了10卷,销量也是噌噌往上涨,最火的时候一卷能卖到12万册呢!

但是,从第11卷开始,出版速度就慢下来了,中间还隔了大半年才继续出。为啥呢?因为海南版快追上原作者鸟山明的进度了,他们不能这么赶了。这时候,市场上还出现了单本卖的情况,以前都是成套卖的。

到了1993年,海南社这边的情况就不太清楚了,反正沙鲁篇拖到了1994年才出完。那段时间,读者们可急坏了,等得花儿都谢了。

还有件事得提,1992年中国加入了《伯尔尼公约》,版权这事儿就严起来了。之前海南社出的那些日漫,其实算是打了个擦边球,不算完全的盗版。但新政一出,规矩就来了。

1994年,央视《新闻联播》连着三天讲打击盗版的事儿,还专门点名了日本漫画,说它们内容不健康,影响青少年。这下子,家长们都紧张了,孩子们看《龙珠》都得偷偷摸摸的。

最后,海南社还是没能继续出漫画了,不管读者怎么盼,后面的故事都只能留在想象中了。到了1997年,新闻出版署查了他们,发现违规出版了不少书,管理也乱,还被吊销了出版资格。从此,海南摄影美术出版社和《龙珠》的故事就告一段落了。

画书大王

记得94年那会儿,《画书大王》,大家都叫它“画王”,咱心里的一道光!那感觉,就像是大家坐一块儿,平等交流,一块儿进步,全靠一腔热爱撑着。这杂志虽然薄薄的,不到百页,但里头啥都有,从鸟山明、北条司到高桥留美子这些国际大咖的作品,再到咱们国内第一代漫画家王庸声、谭晓春、陈翔他们的原创故事,那叫一个丰富多彩。

最让我开眼界的是,它不光有漫画看,还时不时来点法国漫画、纸雕漫画,世界各地的风格都能在这上面找到,学校里那美术课本可没这待遇!

画王连载漫画的同时,还教你咋画漫画,有套连载叫《漫画研究所》,鸟山明亲自上阵,从零开始,一步步教你。虽然最后几页讲怎么给集英社投稿,对国内来说用处不大,但人家还是完完整整给咱们搬来了,这份诚意,没得说!

我看画书大王的起因主要是因为杂志里面有刊登打败沙鲁之后的内容。每期我都有跟,同时也看到了更多的漫画,以及漫画背后作者画漫画的事情,这些点燃了我对漫画这个行业的热爱。

杂志是王庸声老师创办,初心是为了中国漫画的未来。可惜,画王只坚持了两年,就赶上那时候对漫画的“风波”,没了它,中国漫画就像突然被掐了脖子,艰难前行。就算有其他杂志接着干,但总感觉少了那么一股子劲儿。

那时,总会有那么群人,带着纯纯的热爱,艰难却很快乐的坚守者这座小城堡。

圣斗士星矢的前世今生,车田正美的坚持,城户光政的阴谋

作者 戴铭
2024年7月20日 07:33

前言

我最近和家人们一起做了一个播客,名叫《铭莉双收》,本文内容是对最新一期播客“还有人看圣斗士星矢吗?”的一个补充,欢迎大家订阅收听。

提到圣斗士星矢,大部分人都是通过90年央视播出的《圣斗士星矢》这部动画片看到的。后来200多个地方电台每年轮番播放,我也是那时看了一遍又一遍。再后来圣斗士的风潮就结束了,被龙珠和灌篮高手等动画片所替代。

我相对更铁粉些,后来还看了圣斗士星矢的漫画,工作后还买了车田正美授权的手代木史织画的冥王神话LC。

圣斗士星矢在生活中的影响随处可见,像小宇宙爆发、天马流星拳、庐山升龙霸这样的词总是声声入耳。B站年会圣斗士主题演出也是常客。日本还有真人舞台音乐剧。在法国圣斗士也是非常流行,几个法国网友自制了一部十分钟左右的动画短片,这个动画在法国引起了轰动,动画还传到东映高层那,这是发生在圣斗士动画结束十年后的事情,东映因此重启了冥王篇动画。最近22年,法国还举办了一场圣斗士星矢的音乐会。在一些电视剧和电影中也会用到这些词。《爱情公寓》里关谷神奇总是会变身“圣斗士关谷”。韩寒的《飞驰人生》电影中,车手摸到以前赛车时将其比作打开圣衣箱的瞬间。

就算圣斗士星矢这个 IP 还一直人气尚在,但自从漫画冥王篇人气下滑,动画海皇篇收视率下降,还有后续的作品一直无法再续辉煌,即便是车田正美老师本人在五十多岁再次持笔续篇《圣斗士星矢NEXT DIMENSION 冥王神话》也没法重塑辉煌。如今 ND 刚完结,迎来最终回,官方随书赠了再多纪念品也没有破圈传播出来。

这也可以看出《周刊少年Jump》这个舞台的残酷,也正是有了这样的舞台,才会不断诞生出新的神作。

但对于车田正美来说他为圣斗士星矢搭建的巨大世界观还远没完成,关于漫画和动画为何双双落败,车田正美和周刊少年Jump还有东映动画之间发生了什么问题,车田正美到底是个什么样的人呢?

车田正美

车田正美出生在建筑工人家庭,生日是1953年12月6日,今年他已经71岁,是射手座,所以知道为什么星矢是射手座候选人了吧。小时候车田正美就是暴走族的一员,他和其他不良少年不同的是他高中时特别喜欢本宫宏志的《男儿当大将》,决定当像本宫宏志那样的漫画家。另外大家熟悉本宫宏志的作品是《吞食天地》,就是街机上那个三国志游戏的漫画原著。

车田正美高三时给周刊少年Jump的新人奖比赛投稿,结果入围未果,安慰奖中他的名字都被写错,写成了东田正美。还是不良少年的他直接跑到杂志社问责,结果杂志编辑为了安息民愤,给了他一个当本宫宏志助手的机会,本宫宏志可是他的偶像啊。可想当时车田正美的杀气有多大。

有了当漫画家的觉悟,学习起来是飞快的。车田正美很快就开始在周刊少年Jump上连载漫画了,《女强风暴》、《拳王创世纪》、《风魔小次郎》和《男坂》等作品不断推出。《风魔小次郎》我小时候看过,感觉和圣斗士的风格很像,只是少了圣衣。缺少了圣衣加持,里面的人物更难和圣斗士中人物区分开了。

这些作品中,《男坂》由于题材还是一群小混混对付黑道,已经过时,人气不断降低,最后被迫完结。但是车田正美本人和他作品中的热血男儿一样,不轻言放弃,于是在完结最后一页还写着未完二字。这种不放弃就是三十年,2014年《男坂》重开连载。

当时《男坂》的被迫停载让车田正美的小宇宙终于得到爆发,他曾表示,如果下部作品不能红他金盆洗手不干这行了。为了达成这个目标,他低下了他高傲的头颅,将他信奉的拳击和小混混题材放弃掉,给他们包上一层商业化的圣衣,侵泡在希腊神话中,拿出来的就是《圣斗士星矢》。

当年《圣斗士星矢》火到集英社大楼都被称为车田大楼,上一次被这么叫的还是鸟山明的《阿拉蕾》。东映动画制作了《圣斗士星矢》的TV动画,接着就是手办的热卖,手办火热程度一直持续到现在。车田正美当时在文化类纳税是排名第一的,他买了很多豪车,生活也过的豪起来了。

好景不长,在海皇篇时,他和编辑理念出现分歧,还打算用以前成功时的闯宫套路,小强们不升级,黄金圣斗士还是最厉害的,如今的他似乎更有底气,于是不再听从于编辑提出的人物成长,新对手更强的Jump成功学。到了冥王篇读者终于开始厌倦,车田正美的故事编排能力不足的缺陷也更加突出了。于是车田正美自断双臂,大量删减了冥王篇的内容,使其能很快的完结。同样的情景也发生在动画这,《剧场版 天界篇·序奏》里,编剧和车田正美的想法也出现了很大的冲突,这部动画口碑非常差,TV动画海皇篇收视率也出现了滑铁卢。

虽然漫画冥王篇中被删减的内容在续作《圣斗士星矢NEXT DIMENSION 冥王神话》中得到了补全,但圣斗士的故事还是没能回到公众视野中来。

自车田正美三十多岁完结《圣斗士星矢》后,他还一直在画新的漫画,包括《静斗士翔》、《魔矢》、《钢铁神兵》、《青鸟的神话》以及自传漫画《蓝之时代》。这些作品都没有流行起来。

流行就是这样,大家都在追求新鲜感的东西,东西再好,看多了就无趣了。即使是手冢治虫,鸟山明这样的顶流漫画家,后期的作品也难流行开来,但是这也不会妨碍他们成为经典。曾经流行过能够成为一段回忆,经典的作品却能够一直被关注,价值会更高些呢。

在车田正美画续作ND时,他已经五十多了,现在才完结,这一画就是十八年。老爷子真的和他笔下的角色一样,为了自己的理想,一直坚持着,努力着。

说完车田正美,接下来,我会说一说圣斗士星矢到底是个什么样的故事,还会包括圣斗士神话的起源,也就是车田正美创作的超神话。还有正篇中提到的前圣战的故事。

故事起源

圣斗士星矢的世界观是车田正美独创的超神话,和我们知道的希腊神话不一样,只是借鉴了希腊神话、印度和中国的一些神话故事。

起源要从大爆炸说起,大爆炸释放出众神意志,众神意志诞生出大地、天空、海洋和人,有些人会觉醒众神意志。最开始有三个人,分别是掌管天地的宙斯、冥界的哈迪斯和海洋的波塞冬。

人的欲望不断膨胀,掠夺、侵占,到处是罪恶。宙斯无法忍受,于是发起了大洪水作为惩罚。后来宙斯把大地交给了自己的女儿雅典娜,然后消失了。

波塞冬为了夺取雅典娜的大地,创建了亚特兰蒂斯和海斗士军团,使用特殊材料制作了鳞衣给海斗士。由于雅典娜不喜欢武器,所以大地斗士很难伤害身穿鳞衣的海斗士,还很容易被杀。雅典娜让穆大陆的炼金术士使用銀星砂等材料制作出圣衣,以保护这些斗士,这些斗士也被称为圣斗士。由于特殊材质,这些圣衣如果遇到小伤害放进圣衣箱里是会自修复的。天空的88个星座是雅典娜对圣衣做的设计图,因此斗士只能穿上和自己守护星座相对应的圣衣。

有了圣斗士,海皇落败,返回亚特兰蒂斯,利用神力发动洪水和地震,于是雅典娜让圣斗士将亚特兰蒂斯破坏,并把波塞冬封印于北极。常年看守的圣斗士后来成为冰战士。

神之间的战争叫圣战。第一次圣战后雅典娜创建了雅典娜神殿和十二宫,这片区域叫做圣域。这之后雅典娜和圣斗士还遭遇了巨人族的侵犯,战争结束后圣衣产地穆大陆沉没,很多炼金术士也死于这场战争。制作圣衣的技术失传,只剩下少数可以修复圣衣的人,比如牡羊座的穆。

后面还发生了很多次圣战,看起来大多数神是不爱和平的,这点还是比不上人类。最残暴的神是战神阿瑞斯,他还会煽动人类发动战争。他的斗士被叫做狂斗士。阿瑞斯发起的战争导致大量亡民成了哈迪斯的子民。阿瑞斯和雅典娜的圣战中,圣斗士不断死于狂斗士手下,于是雅典娜允许天秤座圣斗士可以使用武器,让他对付狂斗士,致使阿瑞斯败北。

前圣战

最近的圣战,也就是圣斗士星矢正篇里提到的前圣战,发生在两百多年前。和前圣战相关的作品有《圣斗士星矢NEXT DIMENSION 冥王神话》(后面简称 ND)和《圣斗士星矢 THE LOST CANVAS 冥王神话》(后面简称 LC)

ND 是继续着正篇讲的,里面纱织穿越到了前圣战,目的是毁掉哈迪斯之剑,以拯救被哈迪斯之剑伤害的星矢。LC 完全是讲的发生在正篇之前的事情,但是人物和 ND 不完全一样。

正篇前发生的事情

正篇主要分为以下部分:

  • 银河战争篇
  • 暗黑圣斗士
  • 白银圣斗士
  • 圣域十二宫
  • 北欧篇
  • 海王篇
  • 冥王十二宫
  • 冥王冥界篇

在银河战争篇之前,女神雅典娜降生于圣域,老教皇史昂在宣布雅典娜降生这个消息后准备退位给撒加和艾欧罗斯,但是艾欧罗斯被选中的可能性更高些,于是撒加的弟弟加隆提议杀掉雅典娜和老教皇,撒加将加隆关到舒尼恩岬牢狱内。

住在德国灵根的潘多拉家中一个有封条的盒子,使得睡眠之神修普诺斯和死亡之神塔纳托斯复活了。瞬和哈迪斯的灵魂同时出生,哈迪斯的灵魂是借由潘多拉的母亲生出,潘多拉家族城堡里的人在哈迪斯灵魂诞生之时全部死去,只留下潘多拉一人,这座城堡后来就是哈迪斯城堡。

加隆在牢狱中发现了海皇波塞冬的封印,他让海皇波塞冬附身在希腊船王家的继承人-朱利安·索罗体内,波塞冬再次进入沉睡。

在老教皇宣布艾欧罗斯为继承人后,那夜撒加杀害了老教皇。在撒加要杀雅典娜时被艾欧罗斯发现,艾欧罗斯带上雅典娜打算逃出圣域,撒加以教皇名义说艾欧里亚绑架雅典娜,是叛徒,于是一路被其他黄金圣斗士阻拦,最后垂死的艾欧里亚碰到来希腊旅游的财阀城户光政,并将雅典娜托付给了他。

接着城户光政开始策划“百子祭奠神”。

正篇

城户光政为了保护雅典娜,将100名孤儿派到世界各地进行修行,最后只有十个人成为了圣斗士。在银河擂台赛中,胜出的前四人是星矢、冰河、紫龙和瞬,后来经历由瞬的哥哥一辉领导的暗黑圣斗士之战后,一辉最终被四小强的友谊所感动,从而加入了他们,成为了五小强。五小强战胜了白银圣斗士,勇闯了黄金十二宫,铲除了海斗士和海皇波塞冬,死磕了冥王哈迪斯。

正篇中最出彩是黄金圣斗士和冥斗士,这也是圣斗士星矢IP最核心的部分,直到现在,即便圣斗士新作品无人问津,但是黄金圣斗士和冥斗士的手办依然火爆。


黄金圣斗士排名

另外,黄金圣斗士中谁更厉害也是永远是最受关注的话题。由于圣斗士星矢的作品太多,内容相互冲突,这里只限于车田正美自己的两部作品里的角色来比较。SS 代表正篇,ND 代表续作。

第一梯队有

  • SS的撒加,SS加隆,SS童虎,SS史昂
  • ND双子座的该隐和亚伯,ND狮子座凯撒

第二梯队有

  • SS沙加,SS穆
  • ND山羊座以藏,ND处女座释静摩,ND水瓶座米斯托利亚,ND天蝎座艾卡拉特

第三梯队有

  • SS米罗,SS卡妙,SS艾欧里亚,SS修罗
  • ND的射手座格式塔,ND天秤座童虎,ND白羊座史昂,ND巨蟹座迪斯托尔,ND金牛奥克斯

第四梯队有

  • SS金牛阿鲁迪巴
  • ND双鱼座卡迪纳尔

最差的

  • SS双鱼座阿布罗狄,SS巨蟹座迪斯

城户光政是谁

看了圣斗士星矢的人都会觉得剧情漏洞太多。但这里做一个假设,所有圣斗士星矢的剧情漏洞就都填上了。

这个假设是城户光政就是将大地交给雅典娜后就消失的宙斯。从头按照这个假设再看看剧情,宙斯应该是在每次圣战中按照帮雅典娜的人,不然为什么每次雅典娜都能够赢得圣战。正篇中,宙斯下凡投胎成为城户光政,以神力成为了大富豪,并在两年内在全世界到处留种,生了一百个孩子。一般人到了城户光政这个年龄是无法办到的。因此这只能是使用了神力。并且他没有将遗产给自己的孩子而是给了一个捡来的娃娃,这并不是人类的思维。

艾欧里亚将雅典娜交给城户光政这段,是不是和赵子龙救阿斗,交给孩子亲爹刘备一模一样。宙斯一定是将自己的身份告知了艾欧里亚,不然他怎么会把雅典娜交给他呢,宙斯将艾欧里亚的灵魂附着到射手座圣衣上,这样圣衣可以继续保护雅典娜。你看后来冥界篇中黄金圣斗士复活时怎么就没有艾欧里亚呢,这是因为艾欧里亚的灵魂并没有到冥界啊。

救回雅典娜后,城户光政又生了两个最关键的孩子,一个是星矢,一个是瞬,星矢是弑神者,瞬是给冥王作为转世体用。随后几年中宙斯将这些孩子的母亲都害死,致使这些孩子成为孤儿。

6年后,城户光政将一百个孩子聚到一起,和撒加达成协议,提供一百个人给他培训,并提供大量资金支持。撒加欣然答应了。为了表达感谢,撒加额外将几十万年都没用过的天鹅座和天龙座奉献了出来,并配备了黄金圣斗士卡妙和童虎作为导师,看来紫龙和冰河是内定的啊。撒加知道下面要面对对付神的战斗,于是将弑神者星矢叫到希腊重点培养。你看,星矢成为圣斗士时,教皇还亲自到场祝贺。

宙斯布好局,就到天上等着看好戏了。

成为冥王的转世体的条件是世界上最纯洁善良的人,这种人不会主动进入冲突,降低了被伤害的概率,但是一旦遇到危险难于自保,因此需要一个能够随时保护转世体的人。于是冥王就在死亡岛安排了一个导师,这个导师没有身份、圣衣和实体,只有一个面具,存在的目的就培养一个能够保护冥王转世体的人,将这个人培养成死不了,能够穿越生死空间,还越战越猛的人。能够让人不死和自由穿越空间的只有冥王。这样每次瞬遇到危险,一辉都能瞬间感到,无视雅典娜结界,或者直接降到冥界第四狱。一辉的导师很可能是前面某个双子座圣斗士的灵魂,凤翼天翔加幻魔拳招式和效果与双子座的银河星爆加幻朧魔皇拳如出一辙。

最终宙斯的阴谋得逞,他的儿子们和他的女儿一同战胜了波塞冬,能够弑神的儿子星矢最终帮女儿弑掉了总是威胁她的大爷冥王哈迪斯。

最后海皇篇主题曲有段歌词可以作为这段猜想的印证,“正如被选中的神之子”。

一些八卦图个乐

魔玲和星矢有着超越师徒的关系,这关系有点像杨过和小龙女。用漫画中魔玲自己说的话作证,“星矢一直把我当作他的姐姐,而且我们彼此问维持着超越师徒间的感情。如果可以的话,最后还是想再让他看一次我的真面目呢。”,待星矢背上圣衣走的时候,魔玲揭开了自己的面具。

邪武这个舔狗,给雅典娜做牛做马,雅典娜还是喜欢不服管教的星矢,这情节是不是也很熟悉,车田正美妥妥的看过神雕侠侣。

冰河恋母,成为圣斗士的动力就是为了下海。

死亡岛的斗士都是没有正式编制的,他们的圣衣是自产的,没被雅典娜采用的,黑暗斗士的招式是自己琢磨的,不像圣斗士都有名师指点。这妥妥的山寨工厂。

童虎的设定是中国人,童虎是从乾隆时一直到90年代,那么肯定经历了抗日战争,日本人在庐山烧杀抢掠时童虎无动于衷么,难道是因为超过了参军年龄。

紫龙的父亲是城户光政,母亲是庐山人,应该是叫照香炉,紫龙从小跟随母亲,随母姓,人称庐山照紫龙,他还有个妹妹叫紫烟,有诗为证:
日照香炉生紫烟,遥看瀑布挂前川。
飞流直下三千尺,疑是银河落九天。

给孩子小学的家长讲堂做了一个计算机科普分享

作者 戴铭
2023年5月31日 19:39

柠檬所在的学校举办了一个家长讲堂活动,家长们做了很多有意思的分享,柠檬也希望我能够去讲讲,因此我也专门准备了一些内容。下面是我在家长讲堂上所分享的内容。

分享的标题是《图灵对计算机的设想》,那么图灵是什么人?

阿兰·图灵,英国著名的数学家和计算机科学家,被誉为计算机科学之父、人工智能之父和密码学之父。

第二次世界大战中,阿兰·图灵是一位密码破译专家,协助英国政府破解了德国的密码,对盟军的胜利作出了贡献。

在1939年,英国参加了二战,他加入英国布莱切利园的一个密码破译组织,负责破解德军用的一种名为 Enigma 加密机的通信加密信息。

Enigma 看起来像一台打字机,有键盘、灯板、插线板和转子。键盘上按下一个字母键,灯板就会显示加密后的字母。

其中最重要的是转子,Enigma 的转子会轮换替代映射到密文。更改映射的能力很重要,因为一旦某人推导出一个字母替代规则,那么他将会知道密文中每个字母替换规则,因此需要将这些配对都改变,每次编码字母时都更改。

Enigma 实现方式是将所有布线嵌入到车轮/转子中。通过在保持字母静止的同时转动转子,字母之间的连接会发生变化。重复替换步骤,然后转动每个字母的转子。在转子中,每根导线的两端都有外部接触点。这允许这些转子中的多个并排放置,相邻触点接触。在内部,每个转子的接线方式不同,即每个转子都包含不同的密码。在一些Enigma机器中,有三个转子,最常用的是八个。每个转子还有一个附加的字母环,该字母环随转子转动并用于设置转子的初始位置。

每个转子都可以转动到任何位置。这意味着对于第一个转子,有26条可能的路径通过一个字母。但是一旦我们沿着导线穿过第一个转子,现在有26条可能的路径通过第二个转子。然后通过第三条路径还有26条可能的路径。因此,2条通过所有三个转子的路径总数为17576。如果是5个转子,我们可以从五个转子中选择用于左侧的转子,然后从剩余的四个转子中选择用于中间的转子,然后从三个转子中选择用于正确的转子。这提供了60种可能的方式来选择用于消息的三个转子。由于一个字母可以通过转子有17576条可能的路径,因此总共有1054560种可能性。

1930年,德国军队版本增加了一个插板,允许交换字母。由于有26个字母,最多可以进行13个掉期,但通常只有10个。计算连接插板的可能方法数量的数学有点复杂,但数字是150738274937250。乘以我们上面给出的其他可能的组合,我们得到一个字母可以采用的可能路径总数是158962555217826360000。

可能性超多的,在那个只能用真空管做布尔计算的时代,想要破译这些可能,是一件很难的事情。

那么当时盟军是怎么破译的呢?

早期替换加密规律很简单,比如凯撒加密把信件中的字母向前挪三个位置,还有玛丽女王密谋杀伊丽莎白女王的密文,通过统计字母出现频率之类的规则,当破解了一个字母替换方法就能找出通篇原文,没有计算机也能够手工破解出来,而 Enigma 每个字母的可能性都海量的,导致盟军在很长一段时间都没法破译 Enigma 加密的内容。

1932年波兰数学家马里安·雷耶夫斯基、杰尔兹·罗佐基和亨里克·佐加尔斯基按照法国情报人员秘密获取的 Enigma 的原理破解了 Enigma。由于波兰数学家们利用的漏洞不断被德军修复,算力无法及时算出结果,后来将破解方法告诉了英国。

图灵基于波兰破解方法,利用字母加密后一定会是一个和自己不同的字母这个缺陷,设计了一个叫 Bombe 的计算机,对加密消息尝试多种组合,如发现字母解密后和原先一样,这个组合就会被跳过,接着试另一组,因此 Bombe 大幅减少了搜索量,这样就能保证及时破解信息。

战争历史学家 Harry Hinsley 肯定了图灵和布莱切利园组织的工作,说由于他们的工作让战争缩短了两年多,挽救了1400万人的生命。

如今加密技术怎样了呢?进入民用了么?我们能够利用加密技术保护我们的数据安全吗?

加密技术从硬件转向了软件,早期加密算法是1977年的 DES。DES 是一种对称加密算法,它的原理是将明文分成64位的块,通过一系列的置换、替换和移位操作,使用一个56位的密钥对明文进行加密,得到64位的密文。意味着有2的56次方,或大约72千万亿个不同密钥。当时是没有计算能力可以暴力破解所有可能密钥的。

DES 加密算法的具体步骤如下:

  • 初始置换(IP):将明文按照一定的规则进行置换,得到一个新的64位明文。
  • 分组:将置换后的明文分成左右两个32位的块。
  • 轮函数:对右半部分进行一系列的置换、替换和移位操作,使用一个48位的子密钥对其进行加密。
  • 左右交换:将左半部分和右半部分进行交换。
  • 重复执行第3步和第4步,共进行16轮。
  • 合并:将左右两个32位的块合并成一个64位的块。
  • 末置换(FP):将合并后的块按照一定的规则进行置换,得到一个新的64位密文。

DES 解密算法的步骤与加密算法相反,主要是将加密算法中的子密钥按照相反的顺序使用,对密文进行解密。

DES 加密算法的安全性在当时是比较高的。

到了1999年,计算机芯片计算能力指数增加,一台计算机就能在几天内将 DES 的所有可能密钥都试一遍。因此,DES 已经不再被广泛使用,取而代之的是更加安全的加密算法,例如 AES。

2001年 AES 是一种对称加密算法,它的原理是将明文分成128位的块,通过一系列的置换、替换和移位操作,使用一个128位、192位或256位的密钥对明文进行加密,得到128位的密文。

AES 加密算法的具体步骤如下:

  • 密钥扩展:根据密钥长度,对密钥进行扩展,生成多个轮密钥。
  • 初始轮:将明文按照一定的规则进行置换,得到一个新的128位明文。
  • 轮函数:对明文进行一系列的置换、替换和移位操作,使用一个轮密钥对其进行加密。
  • 重复执行第3步,共进行多轮。
  • 末轮:对明文进行最后一轮的置换、替换和移位操作,使用最后一个轮密钥对其进行加密。
  • 得到密文。

AES 解密算法的步骤与加密算法相反,主要是将加密算法中的轮密钥按照相反的顺序使用,对密文进行解密。

AES 加密算法的安全性很高,主要基于其密钥长度和轮函数的复杂性。AES 支持三种密钥长度:128位、192位和256位,其中256位密钥的安全性最高。此外,AES 的轮函数使用了多种复杂的操作,例如有限域上的乘法和逆变换,使得密码破解变得更加困难。

AES 在性能和安全性间取得平衡。如今 AES 被广泛使用,比如 iPhone 上加密文件,访问 HTTPS 网站等。

进入互联网时代,以前加密技术中的密钥在网上传递过程中会被截获,截获到密钥就能够直接解密通信了。

那要怎么做才能够保证密钥不会被截获呢?

这就要用到密钥交换技术了。

密钥交换是一种不发送密钥,但依然让两台计算机在密钥上达成共识的算法。我们可以用单向函数来做。单向函数是一种数学操作,很容易算出结果,但想从结果逆向推算出输入非常困难。

密钥交换的原理是基于数学问题的难解性,例如离散对数问题。

其中,Diffie-Hellman 密钥交换协议是一种常见的密钥交换协议,在 Diffie-Hellman 里单向函数是模幂运算。意思是先做幂运算,拿一个数字当底数,拿一个数字当指数。其具体原理如下:

  • 选择两个大质数 p 和 g,其中 g 是 p 的原根。
  • 小明选择一个私钥 a,并计算 A=g^a(mod p),将 A 发送给小强。
  • 小强选择一个私钥 b,并计算 B=g^b(mod p),将 B 发送给小明。
  • 小明计算 s=B^a(mod p)
  • 小强计算 s=A^b(mod p)
  • 现在,小明和小强都拥有相同的密钥 s,可以在通信过程中使用它来加密和解密消息。

Diffie-Hellman 密钥交换协议的安全性基于离散对数问题的难解性,即使已知 p、g、A 和 B,也很难计算出 a 和 b。因此,Diffie-Hellman 密钥交换协议被广泛应用于安全通信和密钥交换等领域。

另外还可以用混色来比喻 Diffie-Hellman 密钥交换协议。

将颜色混合在一起很容易。但想知道混了什么颜色很难。要试很多种可能才知道,用这个比喻,那么我们的密钥是一种独特的颜色,首先,有一个公开的颜色 C,所有人都可以看到。然后小明和小强各自选一个秘密颜色 A 和颜色 C,只有自己知道,然后小明发给小强 A 和 C 的混色。小强也这样做,把他的秘密颜色 B 和公开颜色 C 混在一起,然后发给小明。小明收到小强的颜色后,把小明的秘密颜色 A 加进去,现在3种颜色混合在一起。小强也一样做。这样,小强和小明就有了一样的颜色。他们可以把这个颜色当密钥,尽管他们从来没有给对方发过这颜色。外部截获信息的人可以知道部分信息,但无法知道最终颜色。

Diffie-Hellman 密钥交换是建立共享密钥的一种方法。双方用一样的密钥加密和解密消息,这叫对称加密,因为密钥一样,凯撒加密,英格玛,AES 都是对称加密。

对称加密的内容两个人都能解密看到,如果加密的信息只想有一方可以解密查看就要用到非对称加密。非对称加密,有两个不同的密钥,一个是公开的,另一个是私有的,用公钥加密消息,只有有私钥的人能解密。

就好像把一个箱子和锁给你,你可以锁上箱子,但不能打开箱子,锁箱子就是公钥加密,能够打开箱子的是有钥匙的人,解锁就是私钥解密。

常见的非对称加密算法包括RSA、DSA和ECC等。目前最流行的非对称加密技术是 RSA。名字来自发明者:Rivest,Shamir,Adleman。

RSA 的原理是基于数学问题的难解性,例如大质数分解。RSA的具体原理如下:

  • 选择两个大质数 p 和 q,计算它们的乘积 n=p*q
  • 选择一个整数e,使得1<e<φ(n),且 e 与 φ(n) 互质,φ(n)=(p-1)*(q-1)
  • 计算 e 关于 φ(n) 的模反元素 d,即满足 e*d≡1(mod φ(n)) 的最小正整数 d。
  • 公钥为 (n,e),私钥为 (n,d)
  • 加密时,将明文 m 转换为整数 M,计算密文 C=M^e(mod n)
  • 解密时,将密文 C 计算出明文 m,即 M=C^d(mod n)

RSA 的安全性基于大质数分解的难度,即使已知公钥和密文,也很难计算出私钥。因此,RSA被广泛应用于数字签名、密钥交换和安全通信等领域。比如数字签名就是公钥来解密,大家都能公开看到签名内容,只有服务器端能够用私钥来加密,这样就能够证明签名是没有伪造的。

对称加密,密钥交换和公钥密码这些就是现代密码学。和图灵那个时代相比更加安全,加解密速度的提高让应用场景也更加地广泛了。

图灵除了密码破译外还做了一件对现代计算机影响深远的事情。

1935年,德国数学家大卫·希尔伯特提出的问题,就是“可判定性问题”,可判定性问题是指是否存在一种算法,输入逻辑语句,可以判断是和否。

图灵发明了一种叫做图灵机的东西,这个机器可以模拟任何其他的计算机,通过图灵机回答了可判定性问题,这个问题虽然看似简单,但是实际上却相当复杂,因为涉及了形式语言的理论、递归的原理等概念。

图灵机可以用于证明停机问题,即判断一个给定的程序是否会在有限时间内停止运行。停机问题是计算机科学中的一个经典问题,它在理论上是不可解的,即不存在一种通用的算法可以解决所有停机问题。这个图灵机可以接受一个程序集合作为输入,并输出一个程序,该程序与输入集合中的所有程序的行为都不同。通过对这个图灵机的构造和分析,图灵证明了停机问题的不可解性。

具体来说,当程序不递归自己,输出停机,测试程序就调用它,使其不停机;如果程序递归调用自己,输出不停机,测试程序不调用它,使其停机。那么问题是测试程序递归调用自己时。

另外还有个更形象的和停机问题一样的理发师悖论,具体说就是有个理发师他有个原则,有人不能刮胡子,他刮;有人刮胡子,他不能刮。无法回答的问题是,理发师会自己刮胡子么?因为他能自己刮,但根据他的原则他又不能刮,但他不能刮的话他又要刮。

图灵机是图灵对计算机的设想,他假设时间足够多,存储足够大,图灵机可以实现任何计算,另外通过停机问题也证明了并不是所有问题都能用计算来解决,也就是提前证明了计算机的极限。开启了可计算性理论,也就是丘奇-图灵论题。

图灵机工作过程和人处理问题的过程类似,获取外部信息,处理当前信息,将处理结果暂存,接下来再获取新的信息重复这个过程。为了完成这个过程,图灵设计的机器有用于输入信息的纸带,处理信息的状态规则,暂存结果的状态寄存器,以及用于获取信息和存储信息的读写器。图灵机的工作过程为:

  • 从纸带上读取信息
  • 通过状态规则查找状态并按规则执行
  • 状态寄存器存储结果
  • 进入新状态
  • 重复过程

现代计算机的设计和实现受到了图灵机的启发。计算机的核心部件包括中央处理器(CPU)、存储器、输入输出设备等,这些部件的设计和实现都是基于图灵机的模型。例如,CPU 可以看作是图灵机的控制器,存储器可以看作是图灵机的纸带,输入输出设备可以看作是图灵机的输入输出接口。

另外,现代计算机的编程语言和算法也受到了图灵机的影响。图灵机可以模拟任何可计算的问题,因此它可以用来证明某个问题是可计算的,也可以用来设计算法和编写程序。现代计算机的编程语言和算法都是基于图灵机的模型,它们可以用来描述和解决各种计算问题。

总的来说现代计算机实现了图灵对计算机的设想,也深入到了我们每个人的生活。一些本来机器解决不了而人类可以解决的问题,机器也可以通过大量数据学习人类来解决。

接下来,我先简单介绍下计算机最核心的计算处理控制器发展,是怎么从图灵时代的继电器发展到现代 CPU 的。

图灵所在二战时代最大的计算机叫哈佛一号,由哈佛大学和 IBM 公司合作研制,有76万5千个组件,300万个连接点和500英里长的导线。哈佛一号采用电子管和机械继电器作为计算元件,可以进行加、减、乘、除等基本运算,还可以进行对数、三角函数等高级运算。继电器是用电控制机械开关。可以把继电器控制线路想成水龙头,打开水龙头,水会流出来,关闭水龙头,水就没了。只不过继电器控制的是电子而不是水。机械开关速度有限,最好的继电器1秒翻转50次。

哈佛一号的体积庞大,重达 5 吨,占地面积达 51 平方米,需要 3 个人来操作。哈佛一号的设计和实现受到了图灵机的启发,它采用了分程序控制和存储程序的思想,可以根据不同的程序进行自动切换。哈佛一号的设计者之一霍华德·艾肯曾说过:“我们试图建造一台机器,它可以像人一样思考,但是我们失败了。相反,我们建造了一台机器,它可以像机器一样思考。”

哈佛一号的研制历时 11 年,耗资 500 万美元,是当时世界上最先进的计算机之一。

哈佛马克一号一秒3次加减,6秒乘法,15秒除法。更复杂操作比如三角函数需要1分钟以上。除了速度慢,齿轮也容易磨损,继电器数量多故障率也会增加,哈佛马克一号有3500个继电器。昆虫也会造成继电器故障,1947年9月操作员从故障继电器中拔出一只死虫,那时每当电脑出了问题,就说它出了 bug。这个就是术语 bug 的来源。

继电器的替代品是真空管。真空管是一种电子器件,它的工作原理基于热电子发射和电子在真空中的运动。真空管由阴极、阳极和控制网格等部件组成,其中阴极是一个加热的金属丝,当温度升高时,会发射出大量的自由电子。这些电子被加速器电场加速,穿过控制网格,最终撞击到阳极上,产生电流。真空管内通过电流控制开闭实现继电器功能,由于真空管内没有会动的组件,这样速度更快,磨损更少,每秒可以开闭数千次。

真空管的工作过程可以分为三个阶段:发射阶段、传输阶段和收集阶段。在发射阶段,阴极发射出大量的自由电子,这些电子被加速器电场加速,形成电子流。在传输阶段,电子流穿过控制网格,受到网格电场的控制,形成一个电子束。在收集阶段,电子束撞击到阳极上,产生电流。

真空管的工作原理与晶体管等现代电子器件不同,它需要加热阴极才能发射电子,因此功耗较大,体积较大,寿命较短。但是真空管具有高功率、高频率、高压等特点,在一些特殊的应用场合仍然得到广泛应用。

真空管很贵,收音机一般只用一个,但计算机可能要上百甚至上千个。一般只有政府才会使用真空管做计算机。第一个大规模用真空管做的计算机是巨人一号,由工程师 Tommy Flower 设计,1943年12月完成。巨人一号在英国的布莱切利园里,用来破解日本的通信。巨人一号是基于图灵机的原理设计的,它采用了存储程序的思想,可以自动执行多个程序。同在布莱切利园的图灵的 bombe 机器没有使用真空管,而是使用的机械装置。核心部件是旋转轮机,它通过模拟密码机的运行过程来破解密码。Bombe 机器的工作原理与真空管电子计算机不同,它不需要电子元件,而是通过机械装置来实现计算和控制。巨人一号和图灵的 bombe 机器在破解密码的方式上也存在一些区别。巨人一号主要使用了穷举法和字典攻击等方法,而图灵的 bombe 则主要使用了差分密码分析等方法。

计算机硬件技术真正实现突破沿用至今的时刻发生在1947年,当年为了降低计算机成本和大小,同时提高可靠性和速度,1947年贝尔实验室科学家 John Bardeen,Walter Brattain,William Shockley 发明了晶体管。晶体管由三个掺杂不同材料的半导体层构成,其中中间的层被称为基底,两侧的层被称为掺杂层。当掺杂层中注入电子或空穴时,它们会在基底中形成一个电子或空穴浓度较高的区域,这个区域被称为 PN 结。PN 结可以用来控制电流的流动,从而实现放大和开关电信号的功能。晶体管的发明是电子技术史上的重要里程碑,它的出现标志着电子器件从真空管时代进入了半导体时代。

晶体管的物理学相当复杂,牵扯到量子力学。晶体管有两个电极,电极之间有一种材料隔开他们,这种材料有时候导电,有时候不导电,叫半导体。半导体每秒可以开关10000次,与玻璃制作的真空管相比,晶体管是固态的,不容易坏,而且比真空管更小更便宜。

1957年 IBM 推出完全用晶体管的 IBM 608,由于便宜,消费者也可以买得到。它有3000个晶体管,每秒执行4500次加法,80次乘除法。IBM 将晶体管计算机带入千家万户。现在计算机里的晶体管小于50纳米,而一张纸的厚度大概是10万纳米。每秒可以切换上百万次,工作很多年。

晶体管和半导体的开发在圣克拉拉谷,半导体材料大部分是硅,硅很特别,它是半导体,它有时导电,有时不导电,我们可以控制导电时机,所以硅是做晶体管的绝佳材料。硅的蕴藏量丰富,占地壳四分之一,这个地方后来被称为硅谷。

1960年代,为了解决电子器件体积大、功耗高、可靠性差等问题。在德州仪器工作的 Jack Killby 把多个组件包在一起,变成一个新的独立组件,这个组件就是集成电路。Robert Noyce 的仙童半导体让集成电路变为现实。最开始一个 IC 只有几个晶体管,把简单电路,逻辑门封装成单独组件。

在集成电路中,数百万个晶体管、电容器、电阻器等元件被集成在一个芯片上,从而大大减小了电路的体积和功耗,提高了电路的可靠性和性能。

在集成电路出现之前,电子器件主要采用离散元件的方式进行组装。这种方式需要大量的电子元件,而且需要手工进行组装和连接,不仅体积大、功耗高,而且可靠性差。随着半导体技术的发展,人们开始尝试将多个晶体管、电容器、电阻器等元件集成在一个芯片上,从而形成了集成电路。

集成电路的出现极大地推动了电子技术的发展。它不仅使电子器件的体积和功耗大大减小,而且提高了电路的可靠性和性能。随着集成电路技术的不断发展,芯片上的晶体管数量不断增加,集成度不断提高。

为了创造更复杂的电路并能够大规模生产,出现了通过蚀刻金属线的方式,把零件连接到一起的印刷电路板技术,简称 PCB。是一种用于连接和支持电子元件的基板,它通过在表面覆盖一层导电材料(通常是铜)并在其上刻蚀出电路图案,从而实现电路的连接和布局。印刷电路板广泛应用于电子设备中,例如计算机、手机、电视等。

印刷电路板的制作过程通常包括以下几个步骤:

  • 设计电路图:首先需要根据电路的功能和布局设计电路图,通常使用电路设计软件进行设计。
  • 制作印刷电路板:将电路图转换为印刷电路板的图案,并使用光刻技术将图案转移到覆盖在基板上的光阻膜上。然后,使用化学蚀刻技术将未被光阻膜保护的铜层蚀刻掉,从而形成电路图案。
  • 镀金层:在印刷电路板表面镀上一层金属,通常是镀金,以提高电路板的导电性和耐腐蚀性。
  • 焊接元件:将电子元件焊接到印刷电路板上,通常使用表面贴装技术(Surface Mount Technology,SMT)或插件式技术(Through-Hole Technology,THT)。
  • 测试电路板:使用测试设备对印刷电路板进行测试,以确保电路板的功能和性能符合要求。

为了在相同体积下集成更多晶体管,全新的光刻工艺出现了,用光把复杂图案印到材料上,比如半导体。其基本原理是利用光敏材料对光的敏感性,通过光的照射和化学反应来形成所需的图案。光刻使用材料包括光掩膜,光刻胶,金属化,氧化层和晶圆。我们可以用晶圆做基础,把复杂金属电路放上面,集成所有东西,非常适合做集成电路。

光刻的基本步骤包括:

  • 涂覆光刻胶:将光刻胶涂覆在待加工的基板表面上,形成一层均匀的薄膜。
  • 曝光:将光刻胶暴露在紫外线下,通过掩膜将光刻胶暴露在特定的区域,形成所需的图案。
  • 显影:将光刻胶进行显影,将未暴露在紫外线下的光刻胶溶解掉,形成所需的图案。
  • 退光刻胶:使用退光刻胶剂将光刻胶进行退除,以便进行下一步的工艺步骤。

在曝光过程中,光刻胶中的光敏剂会吸收光子能量,从而发生化学反应,使得光刻胶在曝光区域发生物理或化学变化。在显影过程中,未曝光的光刻胶会被溶解掉,而曝光区域的光刻胶则会保留下来,形成所需的图案。在退光刻胶过程中,使用退光刻胶剂将光刻胶进行退除,以便进行下一步的工艺步骤。

用类似制作步骤,光刻可以制作其他电子元件,比如电阻和电容,都在一片硅上。而且互相连接的电路也做好了。现实中,光刻法一次会做上百万个细节。

有了光刻技术晶体管越来越小,密度也变得更高,戈登·摩尔发现了一个趋势,就是每两年相同空间所放晶体管数量会增加两倍,后来这个规律被称为摩尔定律。戈登·摩尔和罗伯特·诺伊斯联手成立了一家新公司,结合 Intergrated 和 Electronics 两个词,取名 Intel,是现在最大的芯片制造商。CPU 晶体管数量按摩尔定律一直在指数级地增长,1980年,一个芯片有3万晶体管。到1990年达到了100万,2010年一个芯片里已经可以放进10亿晶体管,现在苹果 M1 Ultra 的晶体管数量约为1140亿。英特尔说,到2030年,芯片将拥有约1万亿个晶体管。先进的芯片中晶体管的尺寸是以纳米为单位,小到2纳米,比血红细胞小2800倍。除了 CPU 还有内存,显卡,固态硬盘和摄像头感光元件等都得益于光刻带来的摩尔定律发展。现在的电路设计都是超大规模集成(VLSI)自动生成的设计。

目前由于光的波长精度已经接近极限,因此需要波长更短的光源来投射更小图案。另外晶体管小到一定程度电极之间可能只有原子长,会发生量子隧道贯穿,也就是电子会跳过间隙。不过相信只要有需求,这些技术问题终将被克服。

那么究竟都有什么样的需求一直推动着计算机技术爆发增长呢?

最早计算机的用途主要就是做数学计算,比如二战的炮手,需要根据射程和大气压力来计算近似多项式,多项式可以描述几个变量的关系,这些函数手算很麻烦耗时。Charles Babbage 提出一种新型机械装置叫差分机将欲求多项方程的前3个初始值输入到机器,推论出固定不变的差数,接下来每个值就可以将差数和前一个阶段的值相加得到。求多项方程的结果完全只需要用到加和减。

在19世纪末,美国人口10年一次普查,然而手工编制需要七年时间,编制完成已经过时了,1890年人口激增,手工编制普查数据需要13年之久。Herman Hollerith 发明了打孔卡片制表机,机器是电动机械的,用传统机械计数,用电动结构连接其他组件。用打孔来表示数据,每个孔代表一个二进制数码,机器会读取孔的位置将其转成数字,打孔卡片制表机的工作方式如下:

  • 使用打孔机将有关个人的数据记录在打孔卡片上。在卡片上打孔,代表一个人的姓名、年龄、职业等信息。
  • 打好的卡片被送入Hollerith的制表机。该机器有金属刷子,可以从卡片上的孔中穿过。
  • 当刷子经过一个开孔时,一个电路就会完成,一个计数器就会递增。计数器记录着有多少张牌具有某些特征。
  • 计数器还可以使用连接在机器上的打印机将结果打印在纸上。它将根据计数器的计数来打印数据的摘要。

与手工操作相比,Hollerith的系统加快了数据的统计过程,速度是手动的十倍。美国人口普查局在1890年采用了他的打孔卡系统,使他们能够在两年半内完成人口普查数据处理。

Herman Hollerith 后来成立了制表机器公司,服务于会计、保险评估和库存管理等数据密集行业。后来这家公司和其他公司合并后改名国际商业机器公司,简称 IBM。

二战时期及二战后冷战时期各国对计算机的需求达到了鼎盛,比如我前面提到图灵他们做的破译 Enigma 的机器。政府对计算机投入资源的时期是美国和苏联的冷战,这也得益于二战时计算机在曼哈顿计划和破解德军加密对自身价值的证明。其中阿波罗计划是投入经费最多的项目,雇了40多万人,还有2万多家大学和公司参与了其中。复杂轨道的计算需求是最大的,因此 NASA制造了阿波罗导航计算机,这台计算机首先使用了集成电路,当时首先使用了集成电路的价格是很贵的,一个芯片就需要五十多美元,而阿波罗导航计算机需要上千个这样的芯片。另外军事上,洲际导弹和核弹也促进了集成电路规模化生产。

随着冷战的结束,政府在计算机上的投入也逐渐减少,计算机迎来了家用消费级时代。

70年代初,计算机各个组件的成本都有大幅下降,可以做出低成本适用于个人使用的电脑,第一台取得商业成功的个人计算机是 Altair 8800,很多计算机爱好者都会购买,计算机的程序要用机器码编写,由于编写麻烦,比尔·盖茨编写了 BASIC 解释器,可以将 BASIC 代码转换成可执行机器码,这个解释器叫 Altair BASIC,也是微软的第一个产品。

24岁的 Steve Wozniak 受到 Altair 8800 的启发,做了一台自己的计算机,他的同学 Steve Jobs 看中了其中机会,1976年4月1日创立了苹果计算机公司,1976年7月开始将 Steve Wozniak 设计的计算机进行售卖,这也是苹果计算机公司的第一款产品。后来苹果的 Apple-II 卖了上百万套,苹果公司一战成名。

和苹果的封闭架构不同的是 IBM 发布的 IBM PC,IBM PC 采用的是开放式架构,这样每个公司都可以遵循这个标准做出自己的计算机,核心硬件和外设都可以有不同组合,这样的计算机也称为 IBM 兼容计算机。

开放的架构也繁荣了生态,更多公司比如康柏和戴尔加入了个人计算机领域。

让计算机进入更多普通人家庭的是交互上的革命。

1984年苹果发布了 Macintosh,使用图形界面取代了用命令行交互的终端。

更多用户对计算机的使用也带来视觉和听觉感官的诉求。那么图形和声音是怎么让计算机识别处理和保存的呢?

当一个图像以特定的格式保存时,构成图像的数字数据–像素和它们的颜色值–被编码并根据该格式的规范进行压缩。该文件还包含元数据,如图像大小、分辨率和色彩模式。

图像文件格式决定了数字数据的组织和压缩方式,图像文件格式的主要类型有:

  • JPEG:一种 “有损 “的压缩格式,通常用于照片。它压缩图像数据以减少文件大小,导致图像质量的一些损失。
  • PNG:一种 “无损 “的压缩格式,适用于带有文字、线条和图形的图像。用这种格式保存时,没有图像质量的损失。
  • GIF: 一种适用于颜色数量有限的图像的格式,通常用于网络上的简单图形和动画。
  • BMP: 一种未压缩的格式,存储图像的精确像素数据。BMP文件的尺寸往往很大。

数字音频文件是由代表音频波形的二进制数据组成。文件格式规定了这种二进制数据的结构和组织方式,以表示音频样本、比特深度、采样率、通道数量和其他元数据。像媒体播放器这样的计算机程序可以读取文件格式并解码二进制数据以播放音频。

常见的音频文件格式包括:

  • WAV:一种标准的未压缩的音频格式,由原始样本组成。WAV文件往往尺寸较大,但具有较高的音频质量。
  • MP3: 一种压缩的音频格式,使用有损压缩来减少文件大小。MP3文件较小,但与WAV相比,其音频质量略低。
  • AAC: 另一种压缩的音频格式,提供良好的压缩率,同时保持相对较高的音频质量。AAC文件通常用于iPod等设备。
  • FLAC: 一种无损压缩的音频格式,在保留所有原始音频信息和质量的同时压缩文件以减小尺寸。

如今,计算机已经可以大致模拟出我们所能感受到的东西,而图灵对计算机的构想也正随着硬件高速发展而逐步被实现,并走进每一个人的生活。关于图灵证明的计算机的极限,计算机已通过学习大量数据来模仿人类进行突破,学会根据情况忽略一些悖论来避免宕机。和计算机不同,我们的生命有限,记忆的容量有限,但也正因为如此,我们才能更好地享受和珍惜每一次对未知事物探索过程的回忆,而不是结果。

给孩子小学的家长讲堂做了一个计算机科普分享

作者 戴铭
2023年5月31日 19:39

柠檬所在的学校举办了一个家长讲堂活动,家长们做了很多有意思的分享,柠檬也希望我能够去讲讲,因此我也专门准备了一些内容。下面是我在家长讲堂上所分享的内容。

分享的标题是《图灵对计算机的设想》,那么图灵是什么人?

阿兰·图灵,英国著名的数学家和计算机科学家,被誉为计算机科学之父、人工智能之父和密码学之父。

第二次世界大战中,阿兰·图灵是一位密码破译专家,协助英国政府破解了德国的密码,对盟军的胜利作出了贡献。

在1939年,英国参加了二战,他加入英国布莱切利园的一个密码破译组织,负责破解德军用的一种名为 Enigma 加密机的通信加密信息。

Enigma 看起来像一台打字机,有键盘、灯板、插线板和转子。键盘上按下一个字母键,灯板就会显示加密后的字母。

其中最重要的是转子,Enigma 的转子会轮换替代映射到密文。更改映射的能力很重要,因为一旦某人推导出一个字母替代规则,那么他将会知道密文中每个字母替换规则,因此需要将这些配对都改变,每次编码字母时都更改。

Enigma 实现方式是将所有布线嵌入到车轮/转子中。通过在保持字母静止的同时转动转子,字母之间的连接会发生变化。重复替换步骤,然后转动每个字母的转子。在转子中,每根导线的两端都有外部接触点。这允许这些转子中的多个并排放置,相邻触点接触。在内部,每个转子的接线方式不同,即每个转子都包含不同的密码。在一些Enigma机器中,有三个转子,最常用的是八个。每个转子还有一个附加的字母环,该字母环随转子转动并用于设置转子的初始位置。

每个转子都可以转动到任何位置。这意味着对于第一个转子,有26条可能的路径通过一个字母。但是一旦我们沿着导线穿过第一个转子,现在有26条可能的路径通过第二个转子。然后通过第三条路径还有26条可能的路径。因此,2条通过所有三个转子的路径总数为17576。如果是5个转子,我们可以从五个转子中选择用于左侧的转子,然后从剩余的四个转子中选择用于中间的转子,然后从三个转子中选择用于正确的转子。这提供了60种可能的方式来选择用于消息的三个转子。由于一个字母可以通过转子有17576条可能的路径,因此总共有1054560种可能性。

1930年,德国军队版本增加了一个插板,允许交换字母。由于有26个字母,最多可以进行13个掉期,但通常只有10个。计算连接插板的可能方法数量的数学有点复杂,但数字是150738274937250。乘以我们上面给出的其他可能的组合,我们得到一个字母可以采用的可能路径总数是158962555217826360000。

可能性超多的,在那个只能用真空管做布尔计算的时代,想要破译这些可能,是一件很难的事情。

那么当时盟军是怎么破译的呢?

早期替换加密规律很简单,比如凯撒加密把信件中的字母向前挪三个位置,还有玛丽女王密谋杀伊丽莎白女王的密文,通过统计字母出现频率之类的规则,当破解了一个字母替换方法就能找出通篇原文,没有计算机也能够手工破解出来,而 Enigma 每个字母的可能性都海量的,导致盟军在很长一段时间都没法破译 Enigma 加密的内容。

1932年波兰数学家马里安·雷耶夫斯基、杰尔兹·罗佐基和亨里克·佐加尔斯基按照法国情报人员秘密获取的 Enigma 的原理破解了 Enigma。由于波兰数学家们利用的漏洞不断被德军修复,算力无法及时算出结果,后来将破解方法告诉了英国。

图灵基于波兰破解方法,利用字母加密后一定会是一个和自己不同的字母这个缺陷,设计了一个叫 Bombe 的计算机,对加密消息尝试多种组合,如发现字母解密后和原先一样,这个组合就会被跳过,接着试另一组,因此 Bombe 大幅减少了搜索量,这样就能保证及时破解信息。

战争历史学家 Harry Hinsley 肯定了图灵和布莱切利园组织的工作,说由于他们的工作让战争缩短了两年多,挽救了1400万人的生命。

如今加密技术怎样了呢?进入民用了么?我们能够利用加密技术保护我们的数据安全吗?

加密技术从硬件转向了软件,早期加密算法是1977年的 DES。DES 是一种对称加密算法,它的原理是将明文分成64位的块,通过一系列的置换、替换和移位操作,使用一个56位的密钥对明文进行加密,得到64位的密文。意味着有2的56次方,或大约72千万亿个不同密钥。当时是没有计算能力可以暴力破解所有可能密钥的。

DES 加密算法的具体步骤如下:

  • 初始置换(IP):将明文按照一定的规则进行置换,得到一个新的64位明文。
  • 分组:将置换后的明文分成左右两个32位的块。
  • 轮函数:对右半部分进行一系列的置换、替换和移位操作,使用一个48位的子密钥对其进行加密。
  • 左右交换:将左半部分和右半部分进行交换。
  • 重复执行第3步和第4步,共进行16轮。
  • 合并:将左右两个32位的块合并成一个64位的块。
  • 末置换(FP):将合并后的块按照一定的规则进行置换,得到一个新的64位密文。

DES 解密算法的步骤与加密算法相反,主要是将加密算法中的子密钥按照相反的顺序使用,对密文进行解密。

DES 加密算法的安全性在当时是比较高的。

到了1999年,计算机芯片计算能力指数增加,一台计算机就能在几天内将 DES 的所有可能密钥都试一遍。因此,DES 已经不再被广泛使用,取而代之的是更加安全的加密算法,例如 AES。

2001年 AES 是一种对称加密算法,它的原理是将明文分成128位的块,通过一系列的置换、替换和移位操作,使用一个128位、192位或256位的密钥对明文进行加密,得到128位的密文。

AES 加密算法的具体步骤如下:

  • 密钥扩展:根据密钥长度,对密钥进行扩展,生成多个轮密钥。
  • 初始轮:将明文按照一定的规则进行置换,得到一个新的128位明文。
  • 轮函数:对明文进行一系列的置换、替换和移位操作,使用一个轮密钥对其进行加密。
  • 重复执行第3步,共进行多轮。
  • 末轮:对明文进行最后一轮的置换、替换和移位操作,使用最后一个轮密钥对其进行加密。
  • 得到密文。

AES 解密算法的步骤与加密算法相反,主要是将加密算法中的轮密钥按照相反的顺序使用,对密文进行解密。

AES 加密算法的安全性很高,主要基于其密钥长度和轮函数的复杂性。AES 支持三种密钥长度:128位、192位和256位,其中256位密钥的安全性最高。此外,AES 的轮函数使用了多种复杂的操作,例如有限域上的乘法和逆变换,使得密码破解变得更加困难。

AES 在性能和安全性间取得平衡。如今 AES 被广泛使用,比如 iPhone 上加密文件,访问 HTTPS 网站等。

进入互联网时代,以前加密技术中的密钥在网上传递过程中会被截获,截获到密钥就能够直接解密通信了。

那要怎么做才能够保证密钥不会被截获呢?

这就要用到密钥交换技术了。

密钥交换是一种不发送密钥,但依然让两台计算机在密钥上达成共识的算法。我们可以用单向函数来做。单向函数是一种数学操作,很容易算出结果,但想从结果逆向推算出输入非常困难。

密钥交换的原理是基于数学问题的难解性,例如离散对数问题。

其中,Diffie-Hellman 密钥交换协议是一种常见的密钥交换协议,在 Diffie-Hellman 里单向函数是模幂运算。意思是先做幂运算,拿一个数字当底数,拿一个数字当指数。其具体原理如下:

  • 选择两个大质数 p 和 g,其中 g 是 p 的原根。
  • 小明选择一个私钥 a,并计算 A=g^a(mod p),将 A 发送给小强。
  • 小强选择一个私钥 b,并计算 B=g^b(mod p),将 B 发送给小明。
  • 小明计算 s=B^a(mod p)
  • 小强计算 s=A^b(mod p)
  • 现在,小明和小强都拥有相同的密钥 s,可以在通信过程中使用它来加密和解密消息。

Diffie-Hellman 密钥交换协议的安全性基于离散对数问题的难解性,即使已知 p、g、A 和 B,也很难计算出 a 和 b。因此,Diffie-Hellman 密钥交换协议被广泛应用于安全通信和密钥交换等领域。

另外还可以用混色来比喻 Diffie-Hellman 密钥交换协议。

将颜色混合在一起很容易。但想知道混了什么颜色很难。要试很多种可能才知道,用这个比喻,那么我们的密钥是一种独特的颜色,首先,有一个公开的颜色 C,所有人都可以看到。然后小明和小强各自选一个秘密颜色 A 和颜色 C,只有自己知道,然后小明发给小强 A 和 C 的混色。小强也这样做,把他的秘密颜色 B 和公开颜色 C 混在一起,然后发给小明。小明收到小强的颜色后,把小明的秘密颜色 A 加进去,现在3种颜色混合在一起。小强也一样做。这样,小强和小明就有了一样的颜色。他们可以把这个颜色当密钥,尽管他们从来没有给对方发过这颜色。外部截获信息的人可以知道部分信息,但无法知道最终颜色。

Diffie-Hellman 密钥交换是建立共享密钥的一种方法。双方用一样的密钥加密和解密消息,这叫对称加密,因为密钥一样,凯撒加密,英格玛,AES 都是对称加密。

对称加密的内容两个人都能解密看到,如果加密的信息只想有一方可以解密查看就要用到非对称加密。非对称加密,有两个不同的密钥,一个是公开的,另一个是私有的,用公钥加密消息,只有有私钥的人能解密。

就好像把一个箱子和锁给你,你可以锁上箱子,但不能打开箱子,锁箱子就是公钥加密,能够打开箱子的是有钥匙的人,解锁就是私钥解密。

常见的非对称加密算法包括RSA、DSA和ECC等。目前最流行的非对称加密技术是 RSA。名字来自发明者:Rivest,Shamir,Adleman。

RSA 的原理是基于数学问题的难解性,例如大质数分解。RSA的具体原理如下:

  • 选择两个大质数 p 和 q,计算它们的乘积 n=p*q
  • 选择一个整数e,使得1<e<φ(n),且 e 与 φ(n) 互质,φ(n)=(p-1)*(q-1)
  • 计算 e 关于 φ(n) 的模反元素 d,即满足 e*d≡1(mod φ(n)) 的最小正整数 d。
  • 公钥为 (n,e),私钥为 (n,d)
  • 加密时,将明文 m 转换为整数 M,计算密文 C=M^e(mod n)
  • 解密时,将密文 C 计算出明文 m,即 M=C^d(mod n)

RSA 的安全性基于大质数分解的难度,即使已知公钥和密文,也很难计算出私钥。因此,RSA被广泛应用于数字签名、密钥交换和安全通信等领域。比如数字签名就是公钥来解密,大家都能公开看到签名内容,只有服务器端能够用私钥来加密,这样就能够证明签名是没有伪造的。

对称加密,密钥交换和公钥密码这些就是现代密码学。和图灵那个时代相比更加安全,加解密速度的提高让应用场景也更加地广泛了。

图灵除了密码破译外还做了一件对现代计算机影响深远的事情。

1935年,德国数学家大卫·希尔伯特提出的问题,就是“可判定性问题”,可判定性问题是指是否存在一种算法,输入逻辑语句,可以判断是和否。

图灵发明了一种叫做图灵机的东西,这个机器可以模拟任何其他的计算机,通过图灵机回答了可判定性问题,这个问题虽然看似简单,但是实际上却相当复杂,因为涉及了形式语言的理论、递归的原理等概念。

图灵机可以用于证明停机问题,即判断一个给定的程序是否会在有限时间内停止运行。停机问题是计算机科学中的一个经典问题,它在理论上是不可解的,即不存在一种通用的算法可以解决所有停机问题。这个图灵机可以接受一个程序集合作为输入,并输出一个程序,该程序与输入集合中的所有程序的行为都不同。通过对这个图灵机的构造和分析,图灵证明了停机问题的不可解性。

具体来说,当程序不递归自己,输出停机,测试程序就调用它,使其不停机;如果程序递归调用自己,输出不停机,测试程序不调用它,使其停机。那么问题是测试程序递归调用自己时。

另外还有个更形象的和停机问题一样的理发师悖论,具体说就是有个理发师他有个原则,有人不能刮胡子,他刮;有人刮胡子,他不能刮。无法回答的问题是,理发师会自己刮胡子么?因为他能自己刮,但根据他的原则他又不能刮,但他不能刮的话他又要刮。

图灵机是图灵对计算机的设想,他假设时间足够多,存储足够大,图灵机可以实现任何计算,另外通过停机问题也证明了并不是所有问题都能用计算来解决,也就是提前证明了计算机的极限。开启了可计算性理论,也就是丘奇-图灵论题。

图灵机工作过程和人处理问题的过程类似,获取外部信息,处理当前信息,将处理结果暂存,接下来再获取新的信息重复这个过程。为了完成这个过程,图灵设计的机器有用于输入信息的纸带,处理信息的状态规则,暂存结果的状态寄存器,以及用于获取信息和存储信息的读写器。图灵机的工作过程为:

  • 从纸带上读取信息
  • 通过状态规则查找状态并按规则执行
  • 状态寄存器存储结果
  • 进入新状态
  • 重复过程

现代计算机的设计和实现受到了图灵机的启发。计算机的核心部件包括中央处理器(CPU)、存储器、输入输出设备等,这些部件的设计和实现都是基于图灵机的模型。例如,CPU 可以看作是图灵机的控制器,存储器可以看作是图灵机的纸带,输入输出设备可以看作是图灵机的输入输出接口。

另外,现代计算机的编程语言和算法也受到了图灵机的影响。图灵机可以模拟任何可计算的问题,因此它可以用来证明某个问题是可计算的,也可以用来设计算法和编写程序。现代计算机的编程语言和算法都是基于图灵机的模型,它们可以用来描述和解决各种计算问题。

总的来说现代计算机实现了图灵对计算机的设想,也深入到了我们每个人的生活。一些本来机器解决不了而人类可以解决的问题,机器也可以通过大量数据学习人类来解决。

接下来,我先简单介绍下计算机最核心的计算处理控制器发展,是怎么从图灵时代的继电器发展到现代 CPU 的。

图灵所在二战时代最大的计算机叫哈佛一号,由哈佛大学和 IBM 公司合作研制,有76万5千个组件,300万个连接点和500英里长的导线。哈佛一号采用电子管和机械继电器作为计算元件,可以进行加、减、乘、除等基本运算,还可以进行对数、三角函数等高级运算。继电器是用电控制机械开关。可以把继电器控制线路想成水龙头,打开水龙头,水会流出来,关闭水龙头,水就没了。只不过继电器控制的是电子而不是水。机械开关速度有限,最好的继电器1秒翻转50次。

哈佛一号的体积庞大,重达 5 吨,占地面积达 51 平方米,需要 3 个人来操作。哈佛一号的设计和实现受到了图灵机的启发,它采用了分程序控制和存储程序的思想,可以根据不同的程序进行自动切换。哈佛一号的设计者之一霍华德·艾肯曾说过:“我们试图建造一台机器,它可以像人一样思考,但是我们失败了。相反,我们建造了一台机器,它可以像机器一样思考。”

哈佛一号的研制历时 11 年,耗资 500 万美元,是当时世界上最先进的计算机之一。

哈佛马克一号一秒3次加减,6秒乘法,15秒除法。更复杂操作比如三角函数需要1分钟以上。除了速度慢,齿轮也容易磨损,继电器数量多故障率也会增加,哈佛马克一号有3500个继电器。昆虫也会造成继电器故障,1947年9月操作员从故障继电器中拔出一只死虫,那时每当电脑出了问题,就说它出了 bug。这个就是术语 bug 的来源。

继电器的替代品是真空管。真空管是一种电子器件,它的工作原理基于热电子发射和电子在真空中的运动。真空管由阴极、阳极和控制网格等部件组成,其中阴极是一个加热的金属丝,当温度升高时,会发射出大量的自由电子。这些电子被加速器电场加速,穿过控制网格,最终撞击到阳极上,产生电流。真空管内通过电流控制开闭实现继电器功能,由于真空管内没有会动的组件,这样速度更快,磨损更少,每秒可以开闭数千次。

真空管的工作过程可以分为三个阶段:发射阶段、传输阶段和收集阶段。在发射阶段,阴极发射出大量的自由电子,这些电子被加速器电场加速,形成电子流。在传输阶段,电子流穿过控制网格,受到网格电场的控制,形成一个电子束。在收集阶段,电子束撞击到阳极上,产生电流。

真空管的工作原理与晶体管等现代电子器件不同,它需要加热阴极才能发射电子,因此功耗较大,体积较大,寿命较短。但是真空管具有高功率、高频率、高压等特点,在一些特殊的应用场合仍然得到广泛应用。

真空管很贵,收音机一般只用一个,但计算机可能要上百甚至上千个。一般只有政府才会使用真空管做计算机。第一个大规模用真空管做的计算机是巨人一号,由工程师 Tommy Flower 设计,1943年12月完成。巨人一号在英国的布莱切利园里,用来破解日本的通信。巨人一号是基于图灵机的原理设计的,它采用了存储程序的思想,可以自动执行多个程序。同在布莱切利园的图灵的 bombe 机器没有使用真空管,而是使用的机械装置。核心部件是旋转轮机,它通过模拟密码机的运行过程来破解密码。Bombe 机器的工作原理与真空管电子计算机不同,它不需要电子元件,而是通过机械装置来实现计算和控制。巨人一号和图灵的 bombe 机器在破解密码的方式上也存在一些区别。巨人一号主要使用了穷举法和字典攻击等方法,而图灵的 bombe 则主要使用了差分密码分析等方法。

计算机硬件技术真正实现突破沿用至今的时刻发生在1947年,当年为了降低计算机成本和大小,同时提高可靠性和速度,1947年贝尔实验室科学家 John Bardeen,Walter Brattain,William Shockley 发明了晶体管。晶体管由三个掺杂不同材料的半导体层构成,其中中间的层被称为基底,两侧的层被称为掺杂层。当掺杂层中注入电子或空穴时,它们会在基底中形成一个电子或空穴浓度较高的区域,这个区域被称为 PN 结。PN 结可以用来控制电流的流动,从而实现放大和开关电信号的功能。晶体管的发明是电子技术史上的重要里程碑,它的出现标志着电子器件从真空管时代进入了半导体时代。

晶体管的物理学相当复杂,牵扯到量子力学。晶体管有两个电极,电极之间有一种材料隔开他们,这种材料有时候导电,有时候不导电,叫半导体。半导体每秒可以开关10000次,与玻璃制作的真空管相比,晶体管是固态的,不容易坏,而且比真空管更小更便宜。

1957年 IBM 推出完全用晶体管的 IBM 608,由于便宜,消费者也可以买得到。它有3000个晶体管,每秒执行4500次加法,80次乘除法。IBM 将晶体管计算机带入千家万户。现在计算机里的晶体管小于50纳米,而一张纸的厚度大概是10万纳米。每秒可以切换上百万次,工作很多年。

晶体管和半导体的开发在圣克拉拉谷,半导体材料大部分是硅,硅很特别,它是半导体,它有时导电,有时不导电,我们可以控制导电时机,所以硅是做晶体管的绝佳材料。硅的蕴藏量丰富,占地壳四分之一,这个地方后来被称为硅谷。

1960年代,为了解决电子器件体积大、功耗高、可靠性差等问题。在德州仪器工作的 Jack Killby 把多个组件包在一起,变成一个新的独立组件,这个组件就是集成电路。Robert Noyce 的仙童半导体让集成电路变为现实。最开始一个 IC 只有几个晶体管,把简单电路,逻辑门封装成单独组件。

在集成电路中,数百万个晶体管、电容器、电阻器等元件被集成在一个芯片上,从而大大减小了电路的体积和功耗,提高了电路的可靠性和性能。

在集成电路出现之前,电子器件主要采用离散元件的方式进行组装。这种方式需要大量的电子元件,而且需要手工进行组装和连接,不仅体积大、功耗高,而且可靠性差。随着半导体技术的发展,人们开始尝试将多个晶体管、电容器、电阻器等元件集成在一个芯片上,从而形成了集成电路。

集成电路的出现极大地推动了电子技术的发展。它不仅使电子器件的体积和功耗大大减小,而且提高了电路的可靠性和性能。随着集成电路技术的不断发展,芯片上的晶体管数量不断增加,集成度不断提高。

为了创造更复杂的电路并能够大规模生产,出现了通过蚀刻金属线的方式,把零件连接到一起的印刷电路板技术,简称 PCB。是一种用于连接和支持电子元件的基板,它通过在表面覆盖一层导电材料(通常是铜)并在其上刻蚀出电路图案,从而实现电路的连接和布局。印刷电路板广泛应用于电子设备中,例如计算机、手机、电视等。

印刷电路板的制作过程通常包括以下几个步骤:

  • 设计电路图:首先需要根据电路的功能和布局设计电路图,通常使用电路设计软件进行设计。
  • 制作印刷电路板:将电路图转换为印刷电路板的图案,并使用光刻技术将图案转移到覆盖在基板上的光阻膜上。然后,使用化学蚀刻技术将未被光阻膜保护的铜层蚀刻掉,从而形成电路图案。
  • 镀金层:在印刷电路板表面镀上一层金属,通常是镀金,以提高电路板的导电性和耐腐蚀性。
  • 焊接元件:将电子元件焊接到印刷电路板上,通常使用表面贴装技术(Surface Mount Technology,SMT)或插件式技术(Through-Hole Technology,THT)。
  • 测试电路板:使用测试设备对印刷电路板进行测试,以确保电路板的功能和性能符合要求。

为了在相同体积下集成更多晶体管,全新的光刻工艺出现了,用光把复杂图案印到材料上,比如半导体。其基本原理是利用光敏材料对光的敏感性,通过光的照射和化学反应来形成所需的图案。光刻使用材料包括光掩膜,光刻胶,金属化,氧化层和晶圆。我们可以用晶圆做基础,把复杂金属电路放上面,集成所有东西,非常适合做集成电路。

光刻的基本步骤包括:

  • 涂覆光刻胶:将光刻胶涂覆在待加工的基板表面上,形成一层均匀的薄膜。
  • 曝光:将光刻胶暴露在紫外线下,通过掩膜将光刻胶暴露在特定的区域,形成所需的图案。
  • 显影:将光刻胶进行显影,将未暴露在紫外线下的光刻胶溶解掉,形成所需的图案。
  • 退光刻胶:使用退光刻胶剂将光刻胶进行退除,以便进行下一步的工艺步骤。

在曝光过程中,光刻胶中的光敏剂会吸收光子能量,从而发生化学反应,使得光刻胶在曝光区域发生物理或化学变化。在显影过程中,未曝光的光刻胶会被溶解掉,而曝光区域的光刻胶则会保留下来,形成所需的图案。在退光刻胶过程中,使用退光刻胶剂将光刻胶进行退除,以便进行下一步的工艺步骤。

用类似制作步骤,光刻可以制作其他电子元件,比如电阻和电容,都在一片硅上。而且互相连接的电路也做好了。现实中,光刻法一次会做上百万个细节。

有了光刻技术晶体管越来越小,密度也变得更高,戈登·摩尔发现了一个趋势,就是每两年相同空间所放晶体管数量会增加两倍,后来这个规律被称为摩尔定律。戈登·摩尔和罗伯特·诺伊斯联手成立了一家新公司,结合 Intergrated 和 Electronics 两个词,取名 Intel,是现在最大的芯片制造商。CPU 晶体管数量按摩尔定律一直在指数级地增长,1980年,一个芯片有3万晶体管。到1990年达到了100万,2010年一个芯片里已经可以放进10亿晶体管,现在苹果 M1 Ultra 的晶体管数量约为1140亿。英特尔说,到2030年,芯片将拥有约1万亿个晶体管。先进的芯片中晶体管的尺寸是以纳米为单位,小到2纳米,比血红细胞小2800倍。除了 CPU 还有内存,显卡,固态硬盘和摄像头感光元件等都得益于光刻带来的摩尔定律发展。现在的电路设计都是超大规模集成(VLSI)自动生成的设计。

目前由于光的波长精度已经接近极限,因此需要波长更短的光源来投射更小图案。另外晶体管小到一定程度电极之间可能只有原子长,会发生量子隧道贯穿,也就是电子会跳过间隙。不过相信只要有需求,这些技术问题终将被克服。

那么究竟都有什么样的需求一直推动着计算机技术爆发增长呢?

最早计算机的用途主要就是做数学计算,比如二战的炮手,需要根据射程和大气压力来计算近似多项式,多项式可以描述几个变量的关系,这些函数手算很麻烦耗时。Charles Babbage 提出一种新型机械装置叫差分机将欲求多项方程的前3个初始值输入到机器,推论出固定不变的差数,接下来每个值就可以将差数和前一个阶段的值相加得到。求多项方程的结果完全只需要用到加和减。

在19世纪末,美国人口10年一次普查,然而手工编制需要七年时间,编制完成已经过时了,1890年人口激增,手工编制普查数据需要13年之久。Herman Hollerith 发明了打孔卡片制表机,机器是电动机械的,用传统机械计数,用电动结构连接其他组件。用打孔来表示数据,每个孔代表一个二进制数码,机器会读取孔的位置将其转成数字,打孔卡片制表机的工作方式如下:

  • 使用打孔机将有关个人的数据记录在打孔卡片上。在卡片上打孔,代表一个人的姓名、年龄、职业等信息。
  • 打好的卡片被送入Hollerith的制表机。该机器有金属刷子,可以从卡片上的孔中穿过。
  • 当刷子经过一个开孔时,一个电路就会完成,一个计数器就会递增。计数器记录着有多少张牌具有某些特征。
  • 计数器还可以使用连接在机器上的打印机将结果打印在纸上。它将根据计数器的计数来打印数据的摘要。

与手工操作相比,Hollerith的系统加快了数据的统计过程,速度是手动的十倍。美国人口普查局在1890年采用了他的打孔卡系统,使他们能够在两年半内完成人口普查数据处理。

Herman Hollerith 后来成立了制表机器公司,服务于会计、保险评估和库存管理等数据密集行业。后来这家公司和其他公司合并后改名国际商业机器公司,简称 IBM。

二战时期及二战后冷战时期各国对计算机的需求达到了鼎盛,比如我前面提到图灵他们做的破译 Enigma 的机器。政府对计算机投入资源的时期是美国和苏联的冷战,这也得益于二战时计算机在曼哈顿计划和破解德军加密对自身价值的证明。其中阿波罗计划是投入经费最多的项目,雇了40多万人,还有2万多家大学和公司参与了其中。复杂轨道的计算需求是最大的,因此 NASA制造了阿波罗导航计算机,这台计算机首先使用了集成电路,当时首先使用了集成电路的价格是很贵的,一个芯片就需要五十多美元,而阿波罗导航计算机需要上千个这样的芯片。另外军事上,洲际导弹和核弹也促进了集成电路规模化生产。

随着冷战的结束,政府在计算机上的投入也逐渐减少,计算机迎来了家用消费级时代。

70年代初,计算机各个组件的成本都有大幅下降,可以做出低成本适用于个人使用的电脑,第一台取得商业成功的个人计算机是 Altair 8800,很多计算机爱好者都会购买,计算机的程序要用机器码编写,由于编写麻烦,比尔·盖茨编写了 BASIC 解释器,可以将 BASIC 代码转换成可执行机器码,这个解释器叫 Altair BASIC,也是微软的第一个产品。

24岁的 Steve Wozniak 受到 Altair 8800 的启发,做了一台自己的计算机,他的同学 Steve Jobs 看中了其中机会,1976年4月1日创立了苹果计算机公司,1976年7月开始将 Steve Wozniak 设计的计算机进行售卖,这也是苹果计算机公司的第一款产品。后来苹果的 Apple-II 卖了上百万套,苹果公司一战成名。

和苹果的封闭架构不同的是 IBM 发布的 IBM PC,IBM PC 采用的是开放式架构,这样每个公司都可以遵循这个标准做出自己的计算机,核心硬件和外设都可以有不同组合,这样的计算机也称为 IBM 兼容计算机。

开放的架构也繁荣了生态,更多公司比如康柏和戴尔加入了个人计算机领域。

让计算机进入更多普通人家庭的是交互上的革命。

1984年苹果发布了 Macintosh,使用图形界面取代了用命令行交互的终端。

更多用户对计算机的使用也带来视觉和听觉感官的诉求。那么图形和声音是怎么让计算机识别处理和保存的呢?

当一个图像以特定的格式保存时,构成图像的数字数据–像素和它们的颜色值–被编码并根据该格式的规范进行压缩。该文件还包含元数据,如图像大小、分辨率和色彩模式。

图像文件格式决定了数字数据的组织和压缩方式,图像文件格式的主要类型有:

  • JPEG:一种 “有损 “的压缩格式,通常用于照片。它压缩图像数据以减少文件大小,导致图像质量的一些损失。
  • PNG:一种 “无损 “的压缩格式,适用于带有文字、线条和图形的图像。用这种格式保存时,没有图像质量的损失。
  • GIF: 一种适用于颜色数量有限的图像的格式,通常用于网络上的简单图形和动画。
  • BMP: 一种未压缩的格式,存储图像的精确像素数据。BMP文件的尺寸往往很大。

数字音频文件是由代表音频波形的二进制数据组成。文件格式规定了这种二进制数据的结构和组织方式,以表示音频样本、比特深度、采样率、通道数量和其他元数据。像媒体播放器这样的计算机程序可以读取文件格式并解码二进制数据以播放音频。

常见的音频文件格式包括:

  • WAV:一种标准的未压缩的音频格式,由原始样本组成。WAV文件往往尺寸较大,但具有较高的音频质量。
  • MP3: 一种压缩的音频格式,使用有损压缩来减少文件大小。MP3文件较小,但与WAV相比,其音频质量略低。
  • AAC: 另一种压缩的音频格式,提供良好的压缩率,同时保持相对较高的音频质量。AAC文件通常用于iPod等设备。
  • FLAC: 一种无损压缩的音频格式,在保留所有原始音频信息和质量的同时压缩文件以减小尺寸。

如今,计算机已经可以大致模拟出我们所能感受到的东西,而图灵对计算机的构想也正随着硬件高速发展而逐步被实现,并走进每一个人的生活。关于图灵证明的计算机的极限,计算机已通过学习大量数据来模仿人类进行突破,学会根据情况忽略一些悖论来避免宕机。和计算机不同,我们的生命有限,记忆的容量有限,但也正因为如此,我们才能更好地享受和珍惜每一次对未知事物探索过程的回忆,而不是结果。

WWDC22 笔记

作者 戴铭
2022年6月10日 12:13

第一天

今年是 WWDC 的第39个年头了。今年的 WWDC.playground 活动()是 SwiftGG、T 沙龙和老司机技术一起会和社区开发者们一起聊聊这次 WWDC。WWDC.playground 活动在节日期间每天都会有直播,我会和 61、13 他们参加 6月11日晚上8点那场直播。现在那场直播的录播已经放了出来,地址是 WWDC22.playground - Day 5:回顾 WWDC22

下面我整理了一份今年 WWDC 的指南,也算提供个方便的入口吧。

  1. WWDC22 直播地址微博直播WWDC22 YouTube 地址
  2. Apple WWDC22 页面
  3. Apple WWDC22 指南
  4. Apple Developer app 观看 Session 的 Apple 出的 App。
  5. Session 网页版
  6. Digital Lounge 注册感兴趣的主题,到时候就可以和 Apple 工程师在 Slack 上一起看 Session,交流。
  7. Labs 可以获得和 Apple 专家一对一指导。6号 keynote 完后就可以开始预约。
  8. Beyond WWDC22 和去年一样,这里是 Apple 制作的世界各地的社区活动。
  9. weak self Discord WWDC22 Keynote Watch Party 全球最多听众的 iOS 中文 Podcast 之一 weak self 的活动。
  10. Swiftly Rush WWDC22
  11. iOS Feeds 的 WWDC 2022 新闻聚合
  12. WWWDC.io App 社区的看 Session 的 App。
  13. Keynote 后的 Platforms State of the Union 这个主题是对后面一周 Session 的总结,开发者可以重点关注下。
  14. WWDC Notes 汇聚了大家的 Session 笔记,可以快速看到各个 Session 的重点。
  15. Technologies 这里是 Apple 框架 API 分类地址,看完 Session 可以直接在这里找对应 API 的更新。还有个网站 Apple Platform SDK API Differences 会列出新 SDK 里有哪些框架更新了。
  16. Apple Design Awards 提名作品

Apple Design Awards 提名作品,我先列几个我喜欢的:

  1. procreate
  2. Wylde Flowers
  3. 笼中窥梦
  4. Gibbon: Beyond the Trees
  5. Vectornator: Design Software
  6. Wylde Flowers
  7. Behind the Frame
  8. MD Clock - Live in the present
  9. 专注面条
  10. Townscaper

第二天

今天最让我印象深刻是 M2、Lock screen widgets、Stage manager、Swift Charts、WeatherKit、SwiftUI Navigation API、只要一个 1024x1024 App Icon、Sticky headers on Xcode scrolling、Xcode View Debugger 可以用于 SwiftUI 了,还有 iOS 16 原生的支持 Nintendo Switch Pro 手柄了。

后面我将更多内容使用点对点的分发,可以用 Planet 关注,我的 IPNS 是:k51qzi5uqu5dlorvgrleqaphsd1suegn8w40xwhxl0bgsyxw3zerivt59xbk74

Keynote 要点:

  • iOS 16
    • new lock screen
    • live activities
    • extend focus to lock screen
    • forcus filter for apps
    • dictation improvements
    • live text in video
    • visual lookup
    • maps
      • multistop routing
      • transit(add card to wallet)
      • new details
      • lookaround api
    • iCloud shared photo library
    • persanalized spatial audio
    • quick notes on iPhone
    • fitness app without watch
    • messages
      • edit messages
      • delete messages
      • mark as unread
      • share play
    • pay
      • tap to pay on iPhone
      • order tracking
    • carplay
      • widgets
      • more personalization
      • multi-screen
    • safety check
      • quickly remove access for others
    • home
      • introduce matter as new standard
      • redesign of app
  • M2
    • 15.8 trillion operations per seconds
    • 10-core GPU
    • macbook air and macbook pro 13”
    • better and faster
    • silent design
    • fast charge
    • new colors
    • magsafe
    • audio jack
  • macOS Ventura
    • improved spotlight
    • undo send and more
    • shared tab groups
    • passkeys
    • desk view
    • stage manager
    • continuity for facetime
    • use iPhone as camera on macbook
  • iPadOS 16
    • weather app
    • WeatherKit
    • collaborations api
    • freeform board
    • stage manager
  • WatchOS 9
    • four new watch faces
    • new ShareKit api
    • improved metrics for running
    • heart rate zones
    • create custom workouts

重要的几个信息:

大赞的库:

好用的功能和组件:

一些方便上手的例子:

一些感兴趣的 Session:

第三天


WWDC.playground 很精彩,怎么感觉昨天的 WWDC.playground 像是听了一期枫言枫语呢。预感 11 号可能会变成为一期 weak self 呢。

昨天老司机还整理了份 WWDC22 Session 观看介绍的列表

Apple 出的内容看不够的话,可使用 Follow WWDC 2022 News! 来看最新的 WWDC 相关的社区文章。

下面是我今天的一些记录。

Xcode

代码补全的更新。以前多个可选参数的体验很差,这次输入参数比如 frame 里的 maxWidth,会只显示当前要补全的参数。而且速度快了很多。

以前是编完源码再生成 module,然后 link编好的文件,最后再 link。现在整个过程改成并行执行,同时 link 还快了两倍。结果是比以前快了25%,核越多效果越明显。还有可可视化整个过程。

多平台以前是多个 tagets,现在是在一个 target 里管理。

Hangs 是官方线上主线程被卡了的检查工具,在 Organizer 里查看对应问题堆栈也很方便。

当然最爱的还是 sticky headers,秒杀其它编辑器 (虽然我还是觉得 Emacs 最好,由于会暴露年龄,一般我都不说)。

还有内存也好了很多,总体来说,这次 Xcode 更新很棒。

完整 Xcode release notes

WidgetKit

WidgetKit 将 WatchOS 上的 Circular、Rectangle 还有 Inline 带到了 iOS 和其他平台。

WeatherKit

安全方便获得用户位置信息,只用于天气。

VisionKit

Live Text API,感觉这类库都是为了以后出眼镜做铺垫的。

macOS

macOS 支持window,menuBar也支持了。

Swift

distrubuted actor 更安全,还可以在设备间(本地设备<->本地设备本地设备<->服务器)进行通信保护。

泛型新语法 some 和 any 关键字写起来真的简化了很多。

Swift 的更新了什么,除了 Session 外,还可以参看 Paul Hudson 这篇文章 What’s new in Swift 5.7 ,还有 Donny Wals 的这篇 What’s the difference between any and some in Swift 5.7?

SwiftUI

SwiftUI里没有用属性包装的属性也能够和视图变化绑定了。

关于 SwiftUI 的更新,Paul Hudson 写了很多例子 What’s new in SwiftUI for iOS 16

Reda Lemeden 整理了 WWDC22 SwiftUI 的所有相关内容 SwiftUI @ WWDC 2022 。可见社区对 SwiftUI 热情依然是最高的。

SPM

Swift Package Plugin,本来用其他语言,比如 ruby 、python 或 shell 做的事情,现在可以通过 Swift 语言来完成了,写的 plugin 还可以方便的在 Xcode 中使用。

虚机

使用 Virtualization 框架,享受 Rosetta 2 的优势,运行 x86-64 Linux 系统。

Apple 出虚机可运行 Linux 系统这点可以看得出 Apple 对开源的拥抱,原因还有一点是 Swift 也可以用在 Linux 服务器上了,Apple 用心良苦,也是想让开发者用本打算买其它硬件的钱来买 Apple 的硬件吧,更好的榨干 Apple 硬件过于优秀的性能,如同新出 Stage Manager 通过投到大屏来榨干 M1 的 iPad 性能。 不光是这样,还有文件,也就是存储设备也只需要一份了,更方便,还有苹果特有的 Trackpad 和 Magic mouse 也能够用于 Linux 系统中。

虚机运行 Linux 和 macOS 的区别是,启动 Linux 使用的是 EFI Boot Loader 来加载 Linux 文件,VirtioGraphicDevice 进行 Linux 系统图形界面的设置和渲染。使用Rosetta 运行 Linux 系统,运行 Linux 就是比其它虚机要快。

介绍的 session Create macOS or Linux virtual machines ,代码说明 Running GUI Linux in a virtual machine on a Mac,相关主题 Virtualization

第四天

今晚五神会现身 WWDC.playground 。内容涉及 SwiftUI 和 AR,不要错过。

今日零散记录

从 Apple 推出 WeatherKit 可以看出,Apple 喜欢把关键和有想象空间盈利价值的技术掌握在自己手上,WeatherKit 提供大量数据,包括分钟、小时、每日预报,还有提前警报,这些信息的商业价值本就很大。

今天看了 WeatherKit、Swift Chart 还有 SwiftUI 的 Layout,感觉 Apple 的接口设计能力很值得学习,可能具备了这些能力才能更好地沟通。

swift-algorithms 可以使用 .indexed() 来替代 zip。

Federico Zanetello 对 Platforms State of the Union 这个 Session 做的笔记

应用层面,今天还有好多 Swift Chart 的介绍。

Layout

Grid、Layout、ViewThatFits、AnyLayout,特别是 Grid 还统一了 HStack 和 VStack。这些布局方式,让先前复杂的要借助 GeometryReader,且容易出错的布局有了更易的写法。Layout 协议可以为 layout 创建自定义属性,另外布局计算也会被缓存。

Link

Link fast: Improve build and launch time 详细讲了 Apple 今年怎么改进了 link,思路很棒,很值得学习。

Static linking 和 Dynamic linking ,也就是静态链接和动态链接。

静态链接就是链接各个编译好的源文件以及链接源文件和编译好的库文件,通过将函数名放到符号表,链接新文件时确定先前是否有包含的 undefined 符号,给函数的数据指令分配地址,最后生成一个有 TEXT、DATA、LINKEDIT 段的可执行文件。

今年 Apple 通过利用多核优势让静态链接快了两倍。

具体做法是,并行的拷贝文件内容。并行构建 LINKEDIT 段的各个不同部分。并行改变 UUID 计算和 codesigning 哈希。然后是提高 exports-trie 构建器的算法。使用最新的 Crypto 库利用硬件加速的优势加速 UUID 计算。提高其它静态库处理算法库,debug-notes 生成也更快了。

Apple 推荐静态库最佳实践是:

使用 -all_load-force_load 可以让 .a 文件像 .o 文件那样并行处理,不过开启这个选项需要先处理重复的符号。另外一个副作用是会将一些被判断无用的代码也被链接进来,使包体变大,因此开启之前可以先使用静态分析工具分析处理,这个过程定期做就行,不用放到每次编译过程中。演讲者推荐使用 -dead_strip 选项,但是这样做并没有真实去掉费代码,以后这些代码还是会被编译分析,如果只是暂时不用,可以先注释掉。

使用 -no_exported_symbols 选项。链接器生成的 LINKEDIT 段的一部分是 exports trie,这是一个前缀树,对所有导出的符号名称、地址和标志进行编码。动态库 是会导出符号的,但运行的二进制文件其实是不用这些符号的,因此可以用 -no_exported_symbols 选项来跳过 LINKEDIT 中 trie 数据结构的创建,这样链接起来就快多了。如果程序导出符号是一百万个,这个选项就可以减少 2 到 3 秒的时间。但需要注意的是,如果要加载插件链接回主程序就需要所有的导出的 trie 数据,无法用这个选项。

另外一个是 -no_deduplicate 选项。先前 Apple 给链接器加了个 pass 用来合并函数的指令相同,函数名不相同,这个 pass 会对每个函数的指令进行递归散列,用这种方式来找重复指令,这样做比较费 CPU,由于调试时其实是不需要关注包大小,因此可以加上 -no_deduplicate 选项来跳过这个 pass。

这些选项在 Xcode 的 Other Linker Flags 里进行设置即可。

动态库也就是 dylib,其它平台就是 DSO 或 DLL。 动态链接器不是将代码从库里考到主二进制里,而是记录某种承诺,记录从动态库中使用符号名称,还有库路径。这样做好处就是好复用动态库,不用拷贝多份。虚拟内存看到多进程使用相同动态库,就会重新给这个动态库用相同的物理内存页。

动态库好处是构建快了,启动加载慢了,多个动态库不光要加载,还要在启动时链接。也就是把链接成本从本地构建换到了用户启动时。动态库还有个缺点是基于动态库的程序会有更多的 dirty 页,因为静态链接时会把全局数据放到主程序同一个 DATA 页中,动态库的话,每个都在自己的 DATA 页中。

动态库工作的原理是,可执行的二进制会有不同权限的段,至少会有 TEXT、DATA 和 LINKEDIT。分段总是操作系统页大小的倍数。TEXT 段有执行的权限,CPU 可以将页上的字节当做机器代码指令。运行时,dyld 会根据每个段权限将可执行文件 mmap() 到内存,这些段是页大小和页对齐的,虚拟内存系统可以直接将程序或动态库文件设置为 VM 范围的备份存储。在这些页的内存访问前是不会被加载到 RAM 里,就会触发一个页 fault,导致 VM 去读取文件的子范围,将内存填充到需要 RAM 页中。光映射不够,还要用某种方式“wired up”或绑到动态库上。比如要调用动态库上的某个函数,会转换成调用 site,调用 site 成为一个在相同 TEXT 段合成的 sub 的调用,相对地址在构建时就知道了,就意味着可以正确的形成 BL 指令。这样做的好处是,stub 从 DATA 加载一个指针并跳到对应的位置,不用在运行时修改 TEXT 段,dyld 只在运行时改 DATA 段。dyld 所进行的修改很简单,就是在 DATA 段里设置了一个指针而已。

当 dyld 或应用程序的指针指向自己时要 rebase,ASLR 使 dyld 以随机地址加载动态库,内部指针不能在构建时设置,dyld 在启动时 rebase 这些指针,磁盘上,如果动态库在地址零出被加载,这些指针包含它们的目标地址。LINKEDIT 需要记录的就是每个重定位的位置。然后,dyld 只需将动态库的实际加载地址添加到每个 rebase 位置。还有种修改方式是绑定,绑定就是符号引用,符号存储在 LINKEDIT 中,dyld 在动态库的 exports tire 中找实际地址,然后 dyld 将该值存储在绑定指定的位置。

今年 Apple 发布了一个新的修改方式 chained fixups。较前面两种的优势就是可以使 LINKEDIT 更小。新格式只存储每个 DATA 页中第一个 fixup 位置和一个导入的符号列表。其它信息编码到 DATA 段。iOS 13.4 就开始支持了。

下面先说下 dyld 原理介绍。

dyld 从主可执行文件开始,解析 mach-o 找依赖动态库,对动态库进行 mmap()。然后对每个动态库进行遍历并解析 mach-o 结构,根据需要加载其它动态库。加载完毕,dyld 会查找所有需要绑定符号,并在修改时使用这些地址。最后修改完,dyld 自下而上运行初始化程序。先前做的优化是只要程序和动态库,dyld 很多步骤都可以在首次启动时被缓存。

今年 Apple 做了更多的优化,这个优化叫 page-in linking,就是 dyld 在启动时做的 DATA 页面修改放到 page-in 时,也可以理解为懒修改。以前,在 mmap() 区域的某些页面中第一次使用某些地址会触发内核读入该页面。现在如果它是一个数据页,内核会应用改页需要的修改。这种机制减少了 dirty 内存和启动时间。意味着 DATA_CONST 也是干净的,可以像 TEXT 页一样被 evicted 和重新创建,以减少内存压力。需要注意的是 page-in linking 只用于启动,dlopen() 不支持。你看,Apple 优化启动的思路也是按需加载。

Apple 还提供了追踪 dyld 运行情况的 dyld_usage 工具。检查磁盘和 dyld 缓存中的二进制文件的 dyld_info 工具。

今日推荐 Session

除了 link 外,还有 Meet distributed actors in Swift 也是比看的,Mike Ash 和 Doug Gregor 一年的心血就在这了。

第五天

性能

性能的 Improve app size and runtime performance Session 值得一看。

今年苹果通过更有效的检查 Swift 协议,使 OC 消息发送调用更小,使 autorelease elision 更快更小这几个个方面来让 App 体积更小,性能更高。

Swift 协议检查。

一个协议通过 as 操作符检查传递值是否符合协议,这种检查会在编译器的构建时间被优化掉,所以往往需要在运行时借助之前计算协议检查元数据来看对象是否真的符合了协议。一些元数据是在编译时建的,但还有很多元数据只能在启动时建立,特别是使用泛型时。协议多了,会增加耗时,差不多会多一半启动时间。

今年 Apple 推出新的 Swift 运行时,可以提前计算 Swift 协议元数据,作为 App 可执行文件和它在启动时使用的任何动态库的 dyld 闭包的一部分。这个是在系统上的,因此,只要是使用了今年最新系统的 App 都会享受这个优化,可以理解为,新系统上启动老 App 也会快些。

消息发送。

Xcode 14 中新的编译器和链接器已经将 ARM64 的消息发送调用从 12 字节减少到 8 字节。因此如果你的 App 都是 OC 代码的话,使用 Xcode 14 编出来的二进制文件可以少 2%。老系统也有效。

使用 objc_stubs_small 选项可以只优化大小,获得最大的大小优化。objc_msgSend 调动有 8 个字节指令,也就是2个指令是专门用来准备 selector 的,对于任何特定的 selector,总是相同的代码,由于始终是相同的代码,那么就可以对其共享,每个 selector 只 emit 一次,而不是每次发送消息时都 emit。共享这段代码地方是一个叫 selector stub 的函数。

ARC 会在编译器插入大量的 c 的 retain/release 函数调用。这些调用遵守平台应用二进制接口(ABI)所定义的 c 语言 call convention。也就意味着我们要更多代码来完成这些调用,用来传递正确寄存器的指针。Apple 今年推出了自定义的 call convention 根据指针位置,适时使用正确变量而不用移动它,从而摆脱了调用里的多余代码。Apple 果然是坚持用户体验优先,为了更好体验不惜修改 c 的 ABI。

autorelease elision 。

App 今年对 objc 运行时进行了修改,使 autorelease elision 更小更快。deployment target 为 iOS 16 今年新系统时才可享用哦。

Apple 怎么做的呢?

ARC 在调用方插入一个 retain,在被调用的函数中插入一个 release。当我们返回我们的临时对象时,我们需要在函数中先释放它,因为它要离开 scope。在它还没有任何其它引用时还不能这么做,不然返回前他就会被销毁。Apple 现在使用一个新的 convention ,让其可以返回临时对象。做法是当返回一个自动释放值,编译器会发出一个特殊标记,这个标记会告诉运行时这是符合自动释放条件的。它的后面是 retain,我们会在后面执行。获取返回地址,也就是一个指针,将它先保存起来,然后离开运行时的自动释放调用。在运行时,可以将保留时得到的指正和先前做自动释放时保存的指针进行比较,这样标记指令不再是数据之间的比较,比较指针内存访问少。比较成功就可以省去 autorelease/retain。

autorelease elision 的优化同样也可以减少 2% 大小。感谢 Apple 为了用户和开发者 OKR 的付出。

SwiftUI

new navigation api,看完感觉我做的小册子还有幻灯应用要花些时间好好改改了。

接下来,有活干了。

WWDC.playground

明天的 WWDC.playground 嘉宾有谜底科技和 weak self,欢迎来捧场。

下面是按分类做的记录:

Swift

String Index 大升级 String Index Overhaul

参考

Regex

标准库多了个 Regex<Output> 类型,Regex 语法与 Perl、Python、Ruby、Java、NSRegularExpression 和许多其他语言兼容。可以用 let regex = try! Regex("a[bc]+")let regex = /a[bc]+/ 写法来使用。SE-0350 Regex Type and Overview 引入 Regex 类型。SE-0351 Regex builder DSL 使用 result builder 来构建正则表达式的 DSL。SE-0354 Regex Literals 简化的正则表达式。SE-0357 Regex-powered string processing algorithms 提案里有基于正则表达式的新字符串处理算法。

RegexBuilder 文档

session Meet Swift RegexSwift Regex: Beyond the basics

Regex 示例代码如下:

let s1 = "I am not a good painter"print(s1.ranges(of: /good/))do {    let regGood = try Regex("[a-z]ood")    print(s1.replacing(regGood, with: "bad"))} catch {    print(error)}print(s1.trimmingPrefix(/i am /.ignoresCase()))let reg1 = /(.+?) read (\d+) books./let reg2 = /(?<name>.+?) read (?<books>\d+) books./let s2 = "Jack read 3 books."do {    if let r1 = try reg1.wholeMatch(in: s2) {        print(r1.1)        print(r1.2)    }    if let r2 = try reg2.wholeMatch(in: s2) {        print("name:" + r2.name)        print("books:" + r2.books)    }} catch {    print(error)}

使用 regex builders 的官方示例:

// Text to parse:// CREDIT  03/02/2022  Payroll from employer     $200.23// CREDIT  03/03/2022  Suspect A           $2,000,000.00// DEBIT   03/03/2022  Ted's Pet Rock Sanctuary    $2,000,000.00// DEBIT   03/05/2022  Doug's Dugout Dogs      $33.27import RegexBuilderlet fieldSeparator = /\s{2,}|\t/let transactionMatcher = Regex {  /CREDIT|DEBIT/  fieldSeparator  One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) // 👈🏻 we define which data locale/timezone we want to use  fieldSeparator  OneOrMore {    NegativeLookahead { fieldSeparator } // 👈🏻 we stop as soon as we see one field separator    CharacterClass.any  }  fieldSeparator  One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US")))}

在正则表达式中捕获数据,使用 Capture:

let fieldSeparator = /\s{2,}|\t/let transactionMatcher = Regex {  Capture { /CREDIT|DEBIT/ } // 👈🏻  fieldSeparator  Capture { One(.date(.numeric, locale: Locale(identifier: "en_US"), timeZone: .gmt)) } // 👈🏻  fieldSeparator  Capture { // 👈🏻    OneOrMore {      NegativeLookahead { fieldSeparator }      CharacterClass.any    }  }  fieldSeparator  Capture { One(.localizedCurrency(code: "USD").locale(Locale(identifier: "en_US"))) } // 👈🏻}// transactionMatcher: Regex<(Substring, Substring, Date, Substring, Decimal)>

泛型与协议

session Embrace Swift genericsDesign protocol interfaces in Swift

swift 5.6 和之前编写泛型接口如下:

func feed<A>(_ animal: A) where A: Animal// 👆🏻👇🏻 Equivalentsfunc feed<A: Animal>(_ animal: A)

swift 5.7 可以这样写:

func feed(_ animal: some Animal)

some 关键字可以用于参数和结构类型。some 会保证类型关系,而 any 会持有任意具体类型,删除类型关系。

SE-0347 Type inference from default expressions 扩展 Swift 泛型参数类型的默认值能力。如下代码示例:

func suffledArray<T: Sequence>(from options: T = 1...100) -> [T.Element] {    Array(options.shuffled())}print(suffledArray())print(suffledArray(from: ["one", "two", "three"]))

SE-0341 Opaque Parameter Declarations 使用 some 参数简化泛型参数声明。SE-0328 Structural opaque result types 扩大不透明结果返回类型可以使用的范围。SE-0360 Opaque result types with limited availability 可用性有限的不透明结果类型,比如 if #available(macOS 13.0, *) {} 就可以根据系统不同版本返回不同类型,新版本出现新类型的 View 就可以和以前的 View 类型区别开。

SE-0309 Unlock existentials for all protocols 改进了 existentials 和 泛型的交互。这样就可以更方便的检查 Any 类型的两个值是否相等

any 关键字充当的是类型擦除的助手,是通过告知编译器你使用 existential 作为类型,此语法可兼容以前系统。

SE-0346 Lightweight same-type requirements for primary associated types 引入一种新语法,用于符合泛型参数并通过相同类型要求约束关联类型。SE-0358 Primary Associated Types in the Standard Library 引入主要关联类型概念,并将其带入了标准库。这些关联类型很像泛型,允许开发者将给定关联类型的类型指定为通用约束。

SE-0353 Constrained Existential Types 基于 SE-0309 和 SE-0346 提案,在 existential 类型的上下文中重用轻量关联类型的约束。

SE-0352 Implicitly Opened Existentials 允许 Swift 在很多情况下使用协议调用泛型函数。

Swift 论坛上一个对 any 和 some 关键字语法使用场景的讨论,Do any and some help with “Protocol Oriented Testing” at all?

Swift Concurrency

session Eliminate data races using Swift ConcurrencyVisualize and optimize Swift concurrencyMeet Swift Async Algorithms

表示持续时间有了新的放来来表达,对应提案是 SE-0329 Clock, Instant, and Duration ,continuous clock 是在系统睡眠状态还会增加时间,suspending clock 在系统睡眠状态不会增加时间。Instants 表示一个确定的时间。Duration 表示两个时间经历了多久。

新增 SE-0338 Clarify the Execution of Non-Actor-Isolated Async Functions 通过收紧可发送性检查的规则来避免潜在的数据竞争。

SE-0343 Concurrency in Top-level Code 这个提案主要是更好地支持命令行工具的开发,可以直接将 concurrency 代码写到 main.swift 文件里。

SE-0340 Unavailable From Async Attribute 提供 noasync 语法以允许我们将类型和函数标记为在异步上下文不可用。

Task 是按顺序执行的,是异步的,在 await 时可以暂停任意次数。task 是自包含的,有自己的资源,可以独立于任何其他 task 独立运行。task 通过在 body 末尾返回一个值来传递对象,值类型没问题,如果是引用类型有可能出现数据竞争。

通过 Sendable 协议 Swift 可以帮助告诉我们什么时候 task 之间共享数据是安全的。Sendable 描述的类型可以跨隔离 domain,不会有数据竞争,Swift 编译器会在构建时检查数据竞争。task 的返回类型要符合 Sendable。

引用类型只能在很少的情况下符合 Sendable。比如 final class 只有不可变的存储。对于自己内部同步的引用类型,比如锁,可以用 @unchecked Sendable

class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {  var lock: NSLock  var storage: [Key: Value]  // ...}

Actor 提供了一种隔离状态的方法可以消除数据竞争。使用 task 来执行 actor 定义的代码。一次只能在一个 actor 上执行一个 task。actor 也是依赖 Sendable。actor 是引用类型,但隔离了他们所有属性和代码来防止并发访问。@MainActor 表示的是主线程,你要在应用中更新 UI 时来用它。

@MainActor func updateView() {}Task { @MainActor in  // update UI here}

@MainActor 也可以用于类,类的属性和方法只能在主 main actor 上访问,除非标记为 nonisolated

@MainActorclass ChickenValley: Sendable {  var flock: [Chicken]  var food: [Pineapple]  func advanceTime() {    for chicken in flock {      chicken.eat(from: &food)    }  }}

Distributed Actors

actor 具有分布式形式工作能力,也就是可以 RPC 通过网络读取和写入属性或者调用方法。设计为保护在跨多个进程中的低级别数据竞争。Distributed actors 可以在两个进程间建立通道,隔离它们状态,并在它们之间异步通信。每个 distributed actors 在 actor 初始化时分配一个不可以手动创建的 id,在它所属整个 distributed actor 系统中唯一标识所指 actor,这样无论 distributed actors 在哪,都可以以相同的方式与之交互。

session Meet distributed actors in Swift 。这里有个 distributed actors 的代码示例 TicTacFish: Implementing a game using distributed actors

SE-0336 Distributed Actor IsolationSE-0344 Distributed Actor Runtime 是两个 Distributed Actors 的相关提案。

Apple 提供了一个参考的服务端 cluster actor 系统实现示例,cluster actor system implementation

Optional

SE-0345 if let shorthand for shadowing an existing optional variable 引入的新语法,用于 unwrapping optinal。

let s1: String? = "hey"let s2: String? = "u"if let s1 {    print(s1)}guard let s1, let s2 else { return }print(s1 + " " + s2)

类型推断

SE-0326 提高了 Swift 对闭包使用参数和类型推断的能力。如下代码:

let a = [1,2,3]let r = a.map { i in    if i >= 2 {        return "\(i) 大于等于2"    } else {        return "\(i) 小于2"    }}print(r)

Result Builders

SE-0348 buildPartialBlock for result builders 简化了实现复杂 result buiders 所需的重载。

Swift-DocC

现在支持 Swift、OC 和 C,文档标记一样。.doccarchive 包含可部署的网站内容,兼容大多数托管服务,比如 Github pages。部署到在线服务上可参考 Generating Documentation for Hosting OnlinePublishing to GitHub Pages 文档。

和 SPM 集成参看 SwiftDocCPlugin

session 有 What’s new in Swift -DocCImprove the discoverability of your Swift-DocC content

SE-0356 Swift Snippets 代码片段用于示例文档的提案。

调试

session Debug Swift debugging with LLDB

编译器编译 swift 文件生成 .o 文件会有 __debug_info 段,其中有可以映射到源文件和行号的地址。debug 信息可以链接到 .dSYM 包。debug 信息链接器叫 dsymutil,dsymutil 可以为每个动态库、framework 或 dylib 和可执行文件打包一个 debug 信息存档(.dSYM 包)。

image 和路径怎么重映射。使用 image list nameOfFramework 来检查 LLDB 是否找到了我们应用程序里嵌入的第三方框架的 debug dSYM。使用 image lookup 0xMemoryAddressHere 获取当前地址更多信息。要重新映射源文件 .dSYM 路径,使用 settings set target.source-map old/path new/path。每个 .dSYM 都有一个 UUID.plist,我们可以在其中设置 DBGSourcePathRemapping 这个字典。

Xcode 14 新增 swift-healthcheck 命令,这个命令可以了解 module 为何导入失败。

LLDB 怎么找到 Swift module?每个 .dSYM 包都可以包含二级制 swift module,其中可能包含桥头文件、swift 接口文件 .swiftinterface,还有 debug 信息。静态存档不是由链接器生成的,需要向链接器注册 swift module,使用 ld ... -add-ast-path /path/to/My.swiftmodule ,动态库和可执行文件的话,Xcode 会自动完成此操作。可以使用 dsymutil 来 dump 你可执行文件的符号表,并用 grep 找 swiftmodule,命令是 dsymutil -s MyApp | grep .swiftmodule

内存管理

相关提案包括 SE-0349 Unaligned Loads and Stores from Raw MemorySE-0334 Pointer API Usability ImprovementsSE-0333 Expand usability of withMemoryRebound

Set 使用新的 Temporary Buffers 功能,让 intersect 速度提升了 4 到 6 倍。

SwiftUI

介绍

Kuba Suder 做了一个 SwiftUI Index/Changelog ,从官方文档中提取版本信息,一目了然 SwiftUI 每个版本 view,modifier 还有属性做了哪些增加和改变。当然也包括这次 SwiftUI 4 的更新。还有份对今年更新整理的 cheat sheet What’s New In SwiftUI for iOS Cheat Sheet - WWDC22

SwiftUI 4 做了大量细节更新,比如添加了后台任务函数 backgroundTask(_:action:) 。List 改用 UICollectionView。AnyLayout 让 HStack 和 VStack 之间可以自由切换。scrollDismissesKeyboard() modifier 可以让键盘在滚动时自动 dismiss。scrollIndicators() modifier 可以隐藏 ScrollView 和 List 等视图的滚动指示。defersSystemGestures() modifier 允许我们的手势优先于系统的内置手势。颜色的 .gradient 可以获得很简单的渐变,Rectangle().fill(.red.gradient),还有 .shadow 用来创建投影 Rectangle().fill(.red.shadow(.drop(color: .black, radius: 10))),还有 .inner 内阴影。lineLimit() modifier 支持范围设置。还有一些 modifier 支持 toggle 参数,比如 .bold().italic() 等,这样利于运行时进行调整。

参考

session:

社区整理的和 SwiftUI 的 digital lounges 内容:

Navigation 接口

控制导航启动状态、管理 size class 之间的 transition 和响应 deep link。

Navigation bar 有新的默认行为,如果没有提供标题,导航栏默认为 inline title 显示模式。使用 navigationBarTitleDisplayMode(_:) 改变显示模式。如果 navigation bar 没有标题、工具栏项或搜索内容,它就会自动隐藏。使用 .toolbar(.visible) modifier 显示一个空 navigation bar。

参考:

NavigationStack 的示例:

struct PNavigationStack: View {    @State private var a = [1, 3, 9] // 深层链接    var body: some View {        NavigationStack(path: $a) {            List(1..<10) { i in                NavigationLink(value: i) {                    Label("第 \(i) 行", systemImage: "\(i).circle")                }            }            .navigationDestination(for: Int.self) { i in                Text("第 \(i) 行内容")            }            .navigationTitle("NavigationStack Demo")        }    }}

这里的 path 设置了 stack 的深度路径。

NavigationSplitView 两栏的例子:

struct PNavigationSplitViewTwoColumn: View {    @State private var a = ["one", "two", "three"]    @State private var choice: String?        var body: some View {        NavigationSplitView {            List(a, id: \.self, selection: $choice, rowContent: Text.init)        } detail: {            Text(choice ?? "选一个")        }    }}

NavigationSplitView 三栏的例子:

struct PNavigationSplitViewThreeColumn: View {    struct Group: Identifiable, Hashable {        let id = UUID()        var title: String        var subs: [String]    }        @State private var gps = [        Group(title: "One", subs: ["o1", "o2", "o3"]),        Group(title: "Two", subs: ["t1", "t2", "t3"])    ]        @State private var choiceGroup: Group?    @State private var choiceSub: String?        @State private var cv = NavigationSplitViewVisibility.automatic        var body: some View {        NavigationSplitView(columnVisibility: $cv) {            List(gps, selection: $choiceGroup) { g in                Text(g.title).tag(g)            }            .navigationSplitViewColumnWidth(250)        } content: {            List(choiceGroup?.subs ?? [], id: \.self, selection: $choiceSub) { s in                Text(s)            }        } detail: {            Text(choiceSub ?? "选一个")            Button("点击") {                cv = .all            }        }        .navigationSplitViewStyle(.prominentDetail)    }}

navigationSplitViewColumnWidth() 是用来自定义宽的,navigationSplitViewStyle 设置为 .prominentDetail 是让 detail 的视图尽量保持其大小。

SwiftUI 新加了个功能可以配置是否隐藏 Tabbar,这样在从主页进入下一级时就可以选择不显示底部标签栏了,示例代码如下:

ContentView().toolbar(.hidden, in: .tabBar)

相比较以前 NavigationView 增强的是 destination 可以根据值的不同类型展示不同的目的页面,示例代码如下:

struct PNavigationStackDestination: View {    var body: some View {        NavigationStack {            List {                NavigationLink(value: "字符串") {                    Text("字符串")                }                NavigationLink(value: Color.red) {                    Text("红色")                }            }            .navigationTitle("不同类型 Destination")            .navigationDestination(for: Color.self) { c in                c.clipShape(Circle())            }            .navigationDestination(for: String.self) { s in                Text("\(s) 的 detail")            }        }    }}

Swift Charts

可视化数据,使用 SwiftUI 语法来创建。还可以使用 ChartRenderer 接口将图标渲染成图。

官方文档 Swift Charts

入门参看 Hello Swift Charts

Apple 文章 Creating a chart using Swift Charts

高级定制和创建更精细图表,可以看这个 session Swift Charts: Raise the bar 这个 session 也会提到如何在图表中进行交互。这里是 session 对应的代码示例 Visualizing your app’s data

图表设计的 session,Design an effective chartDesign app experiences with charts

下面是一个简单的代码示例:

import Chartsstruct PChartModel: Hashable {    var day: String    var amount: Int = .random(in: 1..<100)}extension PChartModel {    static var data: [PChartModel] {        let calendar = Calendar(identifier: .gregorian)        let days = calendar.shortWeekdaySymbols        return days.map { day in            PChartModel(day: day)        }    }}struct PlayCharts: View {    var body: some View {        Chart(PChartModel.data, id: \.self) { v in            BarMark(x: .value("天", v.day), y: .value("数量", v.amount))                    }        .padding()    }}struct PSwiftCharts: View {    struct CData: Identifiable {        let id = UUID()        let i: Int        let v: Double    }        @State private var a: [CData] = [        .init(i: 0, v: 2),        .init(i: 1, v: 20),        .init(i: 2, v: 3),        .init(i: 3, v: 30),        .init(i: 4, v: 8),        .init(i: 5, v: 80)    ]        var body: some View {        Chart(a) { i in            LineMark(x: .value("Index", i.i), y: .value("Value", i.v))            BarMark(x: .value("Index", i.i), yStart: .value("开始", 0), yEnd: .value("结束", i.v))                .foregroundStyle(by: .value("Value", i.v))        } // end Chart    } // end body}

BarMark 用于创建条形图,LineMark 用于创建折线图。SwiftUI Charts 框架还提供 PointMark、AxisMarks、AreaMark、RectangularMark 和 RuleMark 用于创建不同类型的图表。注释使用 .annotation modifier,修改颜色可以使用 .foregroundStyle modifier。.lineStyle modifier 可以修改线宽。

AxisMarks 的示例如下:

struct MonthlySalesChart: View {    var body: some View {        Chart(data, id: \.month) {            BarMark(                x: .value("Month", $0.month, unit: .month),                y: .value("Sales", $0.sales)            )        }        .chartXAxis {            AxisMarks(values: .stride(by: .month)) { value in                if value.as(Date.self)!.isFirstMonthOfQuarter {                    AxisGridLine().foregroundStyle(.black)                    AxisTick().foregroundStyle(.black)                    AxisValueLabel(                        format: .dateTime.month(.narrow)                    )                } else {                    AxisGridLine()                }            }        }    }}

可交互图表示例如下:

struct InteractiveBrushingChart: View {    @State var range: (Date, Date)? = nil        var body: some View {        Chart {            ForEach(data, id: \.day) {                LineMark(                    x: .value("Month", $0.day, unit: .day),                    y: .value("Sales", $0.sales)                )                .interpolationMethod(.catmullRom)                .symbol(Circle().strokeBorder(lineWidth: 2))            }            if let (start, end) = range {                RectangleMark(                    xStart: .value("Selection Start", start),                    xEnd: .value("Selection End", end)                )                .foregroundStyle(.gray.opacity(0.2))            }        }        .chartOverlay { proxy in            GeometryReader { nthGeoItem in                Rectangle().fill(.clear).contentShape(Rectangle())                    .gesture(DragGesture()                        .onChanged { value in                            // Find the x-coordinates in the chart’s plot area.                            let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x                            let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x                            // Find the date values at the x-coordinates.                            if let dateStart: Date = proxy.value(atX: xStart),                               let dateCurrent: Date = proxy.value(atX: xCurrent) {                                range = (dateStart, dateCurrent)                            }                        }                        .onEnded { _ in range = nil } // Clear the state on gesture end.                    )            }        }    }}

社区做的更多 Swift Charts 范例 Swift Charts Examples

Advanced layout control

session Compose custom layouts with SwiftUI

提供了新的 Grid 视图来同时满足 VStack 和 HStack。还有一个更低级别 Layout 接口,可以完全控制构建应用所需的布局。另外还有 ViewThatFits 可以自动选择填充可用空间的方式。

Grid 示例代码如下:

Grid {    GridRow {        Text("One")        Text("One")        Text("One")    }    GridRow {        Text("Two")        Text("Two")    }    Divider()    GridRow {        Text("Three")        Text("Three")            .gridCellColumns(2)    }}

gridCellColumns() modifier 可以让一个单元格跨多列。

ViewThatFits 的新视图,允许根据适合的大小放视图。ViewThatFits 会自动选择对于当前屏幕大小合适的子视图进行显示。Ryan Lintott 的示例效果 ,对应示例代码 LayoutThatFits.swift

新的 Layout 协议可以观看 Swift Talk 第 308 期 The Layout Protocol

通过符合 Layout 协议,我们可以自定义一个自定义的布局容器,直接参与 SwiftUI 的布局过程。新的 ProposedViewSize 结构,它是容器视图提供的大小。 Layout.Subviews 是布局视图的子视图代理集合,我们可以在其中为每个子视图请求各种布局属性。

public protocol Layout: Animatable {  static var layoutProperties: LayoutProperties { get }  associatedtype Cache = Void  typealias Subviews = LayoutSubviews  func updateCache(_ cache: inout Self.Cache, subviews: Self.Subviews)  func spacing(subviews: Self.Subviews, cache: inout Self.Cache) -> ViewSpacing  /// We return our view size here, use the passed parameters for computing the  /// layout.  func sizeThatFits(    proposal: ProposedViewSize,     subviews: Self.Subviews,     cache: inout Self.Cache // 👈🏻 use this for calculated data shared among Layout methods  ) -> CGSize    /// Use this to tell your subviews where to appear.  func placeSubviews(    in bounds: CGRect, // 👈🏻 region where we need to place our subviews into, origin might not be .zero    proposal: ProposedViewSize,     subviews: Self.Subviews,     cache: inout Self.Cache  )    // ... there are more a couple more optional methods}

下面例子是一个自定义的水平 stack 视图,为其所有子视图提供其最大子视图的宽度:

struct MyEqualWidthHStack: Layout {  /// Returns a size that the layout container needs to arrange its subviews.  /// - Tag: sizeThatFitsHorizontal  func sizeThatFits(    proposal: ProposedViewSize,    subviews: Subviews,    cache: inout Void  ) -> CGSize {    guard !subviews.isEmpty else { return .zero }    let maxSize = maxSize(subviews: subviews)    let spacing = spacing(subviews: subviews)    let totalSpacing = spacing.reduce(0) { $0 + $1 }    return CGSize(      width: maxSize.width * CGFloat(subviews.count) + totalSpacing,      height: maxSize.height)  }  /// Places the stack's subviews.  /// - Tag: placeSubviewsHorizontal  func placeSubviews(    in bounds: CGRect,    proposal: ProposedViewSize,    subviews: Subviews,    cache: inout Void  ) {    guard !subviews.isEmpty else { return }    let maxSize = maxSize(subviews: subviews)    let spacing = spacing(subviews: subviews)    let placementProposal = ProposedViewSize(width: maxSize.width, height: maxSize.height)    var nextX = bounds.minX + maxSize.width / 2    for index in subviews.indices {      subviews[index].place(        at: CGPoint(x: nextX, y: bounds.midY),        anchor: .center,        proposal: placementProposal)      nextX += maxSize.width + spacing[index]    }  }  /// Finds the largest ideal size of the subviews.  private func maxSize(subviews: Subviews) -> CGSize {    let subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }    let maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize in      CGSize(        width: max(currentMax.width, subviewSize.width),        height: max(currentMax.height, subviewSize.height))    }    return maxSize  }  /// Gets an array of preferred spacing sizes between subviews in the  /// horizontal dimension.  private func spacing(subviews: Subviews) -> [CGFloat] {    subviews.indices.map { index in      guard index < subviews.count - 1 else { return 0 }      return subviews[index].spacing.distance(        to: subviews[index + 1].spacing,        along: .horizontal)    }  }}

自定义 layout 只能访问子视图代理 Layout.Subviews ,而不是视图或数据模型。我们可以通过 LayoutValueKey 在每个子视图上存储自定义值,通过 layoutValue(key:value:) modifier 设置。

private struct Rank: LayoutValueKey {  static let defaultValue: Int = 1}extension View {  func rank(_ value: Int) -> some View { // 👈🏻 convenience method    layoutValue(key: Rank.self, value: value) // 👈🏻 the new modifier  }}

然后,我们就可以通过 Layout 方法中的 Layout.Subviews 代理读取自定义 LayoutValueKey 值:

func placeSubviews(  in bounds: CGRect,  proposal: ProposedViewSize,  subviews: Subviews,  cache: inout Void) {  let ranks = subviews.map { subview in    subview[Rank.self] // 👈🏻  }  // ...}

要在布局之间变化使用动画,需要用 AnyLayout,代码示例如下:

struct PAnyLayout: View {    @State private var isVertical = false    var body: some View {        let layout = isVertical ? AnyLayout(VStack()) : AnyLayout(HStack())        layout {            Image(systemName: "star").foregroundColor(.yellow)            Text("Starming.com")            Text("戴铭")        }        Button("Click") {            withAnimation {                isVertical.toggle()            }        } // end button    } // end body}

同时 Text 和图片也支持了样式布局变化,代码示例如下:

struct PTextTransitionsView: View {    @State private var expandMessage = true    private let mintWithShadow: AnyShapeStyle = AnyShapeStyle(Color.mint.shadow(.drop(radius: 2)))    private let primaryWithoutShadow: AnyShapeStyle = AnyShapeStyle(Color.primary.shadow(.drop(radius: 0)))    var body: some View {        Text("Dai Ming Swift Pamphlet")            .font(expandMessage ? .largeTitle.weight(.heavy) : .body)            .foregroundStyle(expandMessage ? mintWithShadow : primaryWithoutShadow)            .onTapGesture { withAnimation { expandMessage.toggle() }}            .frame(maxWidth: expandMessage ? 150 : 250)            .drawingGroup()            .padding(20)            .background(.cyan.opacity(0.3), in: RoundedRectangle(cornerRadius: 6))    }}

分享接口

Transferable 协议使数据可以用于剪切板、拖放和 Share Sheet。

可以在自己应用程序之间或你的应用和其他应用之间发送或接受可传输项目。

支持 SwiftUI 来使用。

官方文档 Core Transferable

session Meet Transferable

新增一个专门用来接受 Transferable 的按钮视图 PasteButton,使用示例如下:

struct PPasteButton: View {    @State private var s = "戴铭"    var body: some View {        TextField("输入", text: $s)            .textFieldStyle(.roundedBorder)        PasteButton(payloadType: String.self) { str in            guard let first = str.first else { return }            s = first        }    }}

ShareLink

ShareLink 视图可以让你轻松共享数据。示例代码如下:

struct PShareLink: View {    let url = URL(string: "https://ming1016.github.io/")!    var body: some View {        ShareLink(item: url, message: Text("戴铭的博客"))        ShareLink("戴铭的博客", item: url)        ShareLink(item: url) {            Label("戴铭的博客", systemImage: "swift")        }    }}

锁屏的 Widget

和 WatchOS 一样,可以瞟一眼就获取信息。

官方指南 Creating Lock Screen Widgets and Watch Complications

Bottom Sheet

SwiftUI 新推出的 presentationDetents() modifier 可以创建一个可以定制的 bottom sheet。示例代码如下:

struct PSheet: View {    @State private var isShow = false    var body: some View {        Button("显示 Sheet") {            isShow.toggle()        }        .sheet(isPresented: $isShow) {            Text("这里是 Sheet 的内容")                .presentationDetents([.medium, .large])        }    }}

detent 默认值是 .large。也可以提供一个百分比,比如 .presentationDetents([.fraction(0.7)]),或者直接指定高度 .presentationDetents([.height(100)])

presentationDragIndicator modifier 可以用来显示隐藏拖动标识。

List

list 支持 Section footer。

list 分隔符可以自定义,使用 HorizontalEdge.leadingHorizontalEdge.trailing

list 不使用 UITableView 了。

今年 list 还新增了一个 EditOperation 可以自动生成移动和删除,新增了 edits 参数,传入 [.delete, .move] 数组即可。这也是一个演示如何更好扩展和配置功能的方式。

ScrollView

新增 modifier

ScrollView {    ForEach(0..<300) { i in        Text("\(i)")            .id(i)    }}.scrollDisabled(false).scrollDismissesKeyboard(.interactively).scrollIndicators(.visible)

TextField

支持多行,使用 Axis.vertical 以允许多行。TextField 超过行限制可以变成滚动视图。

今年 TextField 可以嵌到 .alert 里了。

Search

.searchable 支持 token 和 scope,示例如下:

struct PSearchTokensAndScopes: View {    enum AttendanceScope {        case inPerson, online    }    @State private var queryText: String    @State private var queryTokens: [InvitationToken]    @State private var scope: AttendanceScope        var body: some View {        invitationCountView()            .searchable(text: $queryText, tokens: $queryTokens, scope: $scope) { token in                Label(token.diplayName, systemImage: token.systemImage)            } scopes: {                Text("In Person").tag(AttendanceScope.inPerson)                Text("Online").tag(AttendanceScope.online)            }    }}

Gauge

SwiftUI 引入一个新显示进度的视图 Gauge。

简单示例如下:

struct PGauge: View {    @State private var progress = 0.45    var body: some View {        Gauge(value: progress) {            Text("进度")        } currentValueLabel: {            Text(progress.formatted(.percent))        } minimumValueLabel: {            Text(0.formatted(.percent))        } maximumValueLabel: {            Text(100.formatted(.percent))        }                Gauge(value: progress) {                    } currentValueLabel: {            Text(progress.formatted(.percent))                .font(.footnote)        }        .gaugeStyle(.accessoryCircularCapacity)        .tint(.cyan)    }}

Group Form

Form 今年也得到了增强,示例如下:

Form {    Section {        LabeledContent("Location") {            AddressView(location)        }        DatePicker("Date", selection: $date)        TextField("Description", text: $eventDescription, axis: .vertical)            .lineLimit(3, reservesSpace: true)    }        Section("Vibe") {        Picker("Accent color", selection: $accent) {            ForEach(Theme.allCases) { accent in                Text(accent.rawValue.capitalized).tag(accent)            }        }        Picker("Color scheme", selection: $scheme) {            Text("Light").tag(ColorScheme.light)            Text("Dark").tag(ColorScheme.dark)        }#if os(macOS)        .pickerStyle(.inline)#endif        Toggle(isOn: $extraGuests) {            Text("Allow extra guests")            Text("The more the merrier!")        }        if extraGuests {            Stepper("Guests limit", value: $spacesCount, format: .number)        }    }        Section("Decorations") {        Section {            List(selection: $selectedDecorations) {                DisclosureGroup {                    HStack {                        Toggle("Balloons 🎈", isOn: $includeBalloons)                        Spacer()                        decorationThemes[.balloon].map { $0.swatch }                    }                    .tag(Decoration.balloon)                                        HStack {                        Toggle("Confetti 🎊", isOn: $includeConfetti)                        Spacer()                        decorationThemes[.confetti].map { $0.swatch }                    }                    .tag(Decoration.confetti)                                        HStack {                        Toggle("Inflatables 🪅", isOn: $includeInflatables)                        Spacer()                        decorationThemes[.inflatables].map { $0.swatch }                    }                    .tag(Decoration.inflatables)                                        HStack {                        Toggle("Party Horns 🥳", isOn: $includeBlowers)                        Spacer()                        decorationThemes[.noisemakers].map { $0.swatch }                    }                    .tag(Decoration.noisemakers)                } label: {                    Toggle("All Decorations", isOn: [                        $includeBalloons, $includeConfetti,                        $includeInflatables, $includeBlowers                    ])                    .tag(Decoration.all)                }#if os(macOS)                .toggleStyle(.checkbox)#endif            }                        Picker("Decoration theme", selection: themes) {                Text("Blue").tag(Theme.blue)                Text("Black").tag(Theme.black)                Text("Gold").tag(Theme.gold)                Text("White").tag(Theme.white)            }#if os(macOS)            .pickerStyle(.radioGroup)#endif        }    }    }.formStyle(.grouped)

Button

.buttonStyle 可组合,示例如下:

struct PButtonStyleComposition: View {    @State private var isT = false    var body: some View {        Section("标签") {            VStack(alignment: .leading) {                HStack {                    Toggle("Swift", isOn: $isT)                    Toggle("SwiftUI", isOn: $isT)                }                HStack {                    Toggle("Swift Chart", isOn: $isT)                    Toggle("Navigation API", isOn: $isT)                }            }            .toggleStyle(.button)            .buttonStyle(.bordered)        }    }}

Tap Location

可以获取点击的位置,示例代码如下:

Rectangle()    .fill(.green)    .frame(width: 50, height: 50)    .onTapGesture(coordinateSpace: .global) { location in        print("Tap in \(location)")    }

其中 coordinateSpace 指定为 .global 表示位置是相对屏幕左上角,默认是相对当前视图的左上角的位置。

选择多个日期

MultiDatePicker 视图会显示一个日历,用户可以选择多个日期,可以设置选择范围。示例如下:

struct PMultiDatePicker: View {    @Environment(\.calendar) var cal    @State var dates: Set<DateComponents> = []    var body: some View {        MultiDatePicker("选择个日子", selection: $dates, in: Date.now...)        Text(s)    }    var s: String {        dates.compactMap { c in            cal.date(from:c)?.formatted(date: .long, time: .omitted)        }        .formatted()    }}

PhotosPick

支持图片选择,示例代码如下:

import PhotosUIimport CoreTransferablestruct ContentView: View {    @ObservedObject var viewModel: FilterModel = .shared        var body: some View {        NavigationStack {            Gallery()                .navigationTitle("Birthday Filter")                .toolbar {                    PhotosPicker(                        selection: $viewModel.imageSelection,                        matching: .images                    ) {                        Label("Pick a photo", systemImage: "plus.app")                    }                    Button {                        viewModel.applyFilter()                    } label: {                        Label("Apply Filter", systemImage: "camera.filters")                    }                }        }    }}

Table

今年 iOS 和 iPadOS 也可以使用去年只能在 macOS 上使用的 Table了,据 digital lounges 里说,iOS table 的性能和 list 差不多,table 默认为 plian list。我想 iOS 上加上 table 只是为了兼容 macOS 代码吧。

table 使用示例如下:

Table(attendeeStore.attendees) {    TableColumn("Name") { attendee in        AttendeeRow(attendee)    }    TableColumn("City", value: \.city)    TableColumn("Status") { attendee in        StatusRow(attendee)    }}.contextMenu(forSelectionType: Attendee.ID.self) { selection in    if selection.isEmpty {        Button("New Invitation") { addInvitation() }    } else if selection.count == 1 {        Button("Mark as VIP") { markVIPs(selection) }    } else {        Button("Mark as VIPs") { markVIPs(selection) }    }}

Toolbar

对 toolbar 的自定义,示例如下:

.toolbar(id: "toolbar") {    ToolbarItem(id: "new", placement: .secondaryAction) {        Button(action: {}) {            Label("New Invitation", systemImage: "envelope")        }    }}.toolbarRole(.editor)

SF Symbol

SF Symbol 支持变量值,可以通过设置 variableValue 来填充不同部分,比如 wifi 图标,不同值会亮不同部分,Image(systemName: "wifi", variableValue: 0.5)

Gradient 和 Shadow

下面是个简单示例:

struct PGradientAndShadow: View {    var body: some View {        Image(systemName: "bird")            .frame(width: 150, height: 150)            .background(in: Rectangle())            .backgroundStyle(.cyan.gradient)            .foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))            .font(.system(size: 60))    }}

Paul Hudson 使用 Core Motion 做了一个阴影随设备倾斜而变化的效果,非常棒,How to use inner shadows to simulate depth with SwiftUI and Core Motion

嵌入 UIKit

示例如下:

cell.contentConfiguration = UIHostingConfiguration {    VStack {        Image(systemName: "wand.and.stars")            .font(.title)        Text("Like magic!")            .font(.title2).bold()    }    .foregroundStyle(Color.purple)}

macOS

支持了 window,可以控制位置和大小。官方代码示例 Bringing multiple windows to your SwiftUI app

openWindow 代码示例如下:

struct PartyPlanner: App {    var body: some Scene {        WindowGroup("Party Planner") {            PartyPlannerHome()        }        Window("Party Budget", id: "budget") {            Text("Budget View")        }        .keyboardShortcut("0")        .defaultPosition(.topLeading)        .defaultSize(width: 220, height: 250)    }}struct DetailView: View {    @Environment(\.openWindow) var openWindow    var body: some View {        Text("Detail View")            .toolbar {                Button {                    openWindow(id: "budget")                } label: {                    Image(systemName: "dollarsign")                }            }    }}

session Bring multiple windows to your SwiftUI app 两个新 Scene 类型。WindowGroup 允许多 window。MenuBarExtra。可编程方式打开新 window 和 document。

MenuBarExtra 代码示例如下:

struct PartyPlanner: App {    var body: some Scene {        Window("Party Budget", id: "budget") {            Text("Budget View")        }        MenuBarExtra("Bulletin Board", systemImage: "quote.bubble") {            BulletinBoard()        }        .menuBarExtraStyle(.window)    }}

讲和 AppKit 混编的 session Use SwiftUI with AppKit

The craft of SwiftUI API design: Progressive disclosure 使用 windows 还有 MenuBarExtra,使用 modifier 来自定义应用程序 window 的 presentation 和行为。

使用 .dropDestination 来支持拖动。示例如下:

.dropDestination(payloadType: Image.self) { receivedImages, location in        guard let image = receivedImages.first else {            return false        }        viewModel.imageState = .success(image)        return true    }

今年有新的 FormStyle ,示例如下:

Form {    Picker("Notify Me About:", selection: $notifyMeAbout) {        Text("Direct Messages").tag(NotifyMeAboutType.directMessages)        Text("Mentions").tag(NotifyMeAboutType.mentions)        Text("Anything").tag(NotifyMeAboutType.anything)    }    Toggle("Play notification sounds", isOn: $playNotificationSounds)    Toggle("Send read receipts", isOn: $sendReadReceipts)    Picker("Profile Image Size:", selection: $profileImageSize) {        Text("Large").tag(ProfileImageSize.large)        Text("Medium").tag(ProfileImageSize.medium)        Text("Small").tag(ProfileImageSize.small)    }    .pickerStyle(.inline)}.formStyle(.columns)

Apple 自身在 macOS 系统中使用了多少 SwiftUI 呢?邮件、iWork 和 Keychain Access 的部分视图使用了,笔记、照片 和 Xcode 部分功能及新增功能的完整界面都是用的 SwiftUI,另外控制中心、字体册和系统设置的大部分都是用 SwiftUI 开发了。

ImageRenderer

可以将 SwiftUI 的 View 生成图片。

官方参考文档 ImageRenderer

后台任务

session Efficiency awaits: Background tasks in SwiftUI 了解如何使用 SwiftUI 后台任务 API 简洁地处理任务。展示如何使用 Swift Concurrency 来处理网络响应、后台刷新等——同时保持性能和功率。

Xcode 14

Xcode 14 里有新的 Swift 5.7,其中对泛型和协议有很大的改进。

参考

通用

编出来的二进制小 30%。

改进了并行性,构建提速 25%。

改进了在 iOS 设备上调试 Swift 程序的性能。

提供单一图标大小,Xcode 完成剩下的。

更智能的代码完成,滚动时置顶类、结构体和函数名。错误消息在重新处理时会变暗。

Xcode 搜索和替换栏中可以使用正则表达式。相信以后社区会出现很多好用的正则表达式分享。

Xcode Organizer 中新增 Hang 报告,用来提供主线程上发生挂起的调用堆栈信息,以及提供设备和 iOS 版本信息等统计信息。

Xcode 14 现在支持为 iPadOS 开发 DriverKit 驱动程序。

创建新 C++ 项目,Clang 默认使用 C++20。已经实现了几篇 C++20 和 C++2b 论文。

iOS、tvOS 和 watchOS 的构建默认不再包含 bitcode。

legacy 构建系统被删除,LLVM 14 也不再支持 legacy。

Xcode 中的 Swift-DocC 现支持 OC 和 C 的 API 构建文档。生成的 Swift-DocC 文档网站包括一个新的导航侧边栏,用于浏览和过滤文档。可将 Swift-DocC 部署到 GitHub Pages。

性能问题修复

代码完成不再自动导入模块。

提高了复杂表达式 SwiftUI 中代码完成的速度和准确性。

修复了包含大量错误或警告的文件时导致性能下降的问题。

修复了 minimap 在长文件时性能问题。

源码编辑器

滚动编辑器时,Xcode 会将代码结构的元素固定到编辑器顶部。

支持了 Regex 表达式语法高亮。Editor > Refactoring > Convert to Regex Builder 可以将正则文本转成等效 Regex builder。

可以输入匹配参数来选择代码完成中默认参数的任意组合。

Swift 中代码完成提供基于变量名的 map、filter 和 contains 的 snippet。

提高 Swift 代码完成的准确性。

SwiftUI 的代码完成,现在有了 List 和 ForEach 的 snippet。

Xcode 14 还要很多贴心代码完成改进,比如写 struct 的 init 可以自动完成。Codable 的 encode 也可以自动完成。

Xcode Preview

Preview 增强,默认是交互式的。

创建新项目会自动 resume。大量编辑时也不会暂停。会动态调整更新频率。

Swift Packages

引入新参数 moduleAliases 来为冲突的模块定义唯一名称,并以新名称构建而不用改代码。注意的是起别名的模块要是纯 Swift 模块。

允许使用 Swift Package command plugins。Xcode 为 Swift Package plugins 提供了 XcodeProjectPlugin 接口,这个接口扩展了 Swift Package Manager 的 PackagePlugin 接口。用这个接口可以获得 Xcode 项目结构的简化描述。

session 有 Meet Swift Package pluginsCreate Swift Package plugins

Instrument

Hang Tracing 工具,可以显示应用程序的主线程什么时候无法长时间处理传入事件,从而导致 UI 卡住。

Runloop 工具,显示 runloop 的使用和单独的迭代,视觉上区分了进程中所有 runloop 的 runloop sleep 和 busy interval。

Instrument 新模板更方便调试 distributed actors 和其它 Swift concurrency 特性。

memory graph 调试器可以显示 memory graph 的所有传入和传出引用。

Instrument 现有一个新的 Swift Concurrency 模板,用于跟踪 swift concurrency 的使用。这个模板包括 Swift Tasks 工具,可显示随时间变化的 task 的状态,总结 task 状态,提供详细的 task 描述,task 关系和 task 创建 callstacks 的调用树结构。还有 Swift Actors 工具,可以跟踪 actor 之间的 task 行为,显示每个 actor 的 task 队列,并帮助诊断 actor-isolated 代码等问题。

Instrument 里的代码查看更好显示包含了性能数据。Interleave 模式,可以同时查看源码和关联的反汇编。源码查看现在会在源码和反汇编判断显示 CPU 计数器,PMC 事件和动态公式。

修复了很多 Swift 相关显示不友好的问题。

多端

官方例子 Configuring a multiplatform app 。一个示例了 NavigationSplitView、Layout、Chart 和 WeatherKit 的运用的官方例子 Food Truck: Building a SwiftUI multiplatform app

Session 笔记

https://www.wwdcnotes.com/notes/wwdc22/110371/

下面是 App Intents、WidgetKit 相关内容,这些都属于 App Services,WWDC22 专门整理了 App Service 专题 。新系统服务比如 Messages collaboration、网络、CloudKit 的 System Service 主题

Widget

iOS 16 和 WatchOS 9 可以使用同一套代码编写 widget。iOS 新增场景是锁屏和 Live Activities(晚些时候推出)。

利用 Smart Stack,让 widget 出现到栈顶,可以使用 TimelineEntryRelevance

官方参考:

介绍怎么将 widgets 添加到 lock screen 的 session Complications and widgets: Reloaded 。对应的实例代码 Adding widgets to the Lock Screen and watch faces

App Intents

打通 App Shortcuts,从 Shortcuts 应用、Spotlight 和 Siri 运行你的 App 特定任务。

对应 Session

文档 App Intents

官方几篇 App Intents 文章:

对于 Shortcut 的使用少数派有篇很棒的文章 《iOS 快捷指令搭配 Notion API,更快速地编辑内容》 。

WeatherKit

Apple 收购 Dark Sky 后带来了 WeatherKit 和 WeatherKit REST API。有着易用的 Swift 接口,还有配套的 REST API。WeatherKit 内置了 async/await 支持。

WeatherKit 指南
WeatherKit 文档

session Meet WeatherKit 。一个 Apple 提供的天气代码示例 Fetching weather forecasts with WeatherKit

HealthKit

提供了更详细的睡眠和锻炼数据。session 介绍 What’s new in HealthKit

Vision

更新介绍 session What’s new in Vision

VisionKit 现在有一个结合 AVCapture 和 Vision 的数据扫描仪进行实时捕捉。 session Capture machine-readable codes and text with VisionKit

Live Text 接口

视觉库的应用接口。可以从照片和暂停视频中获取文本。

官方参考:

ScreenCaptureKit

creenCaptureKit 框架可以给你的 macOS 程序添加对高性能屏幕录制的支持。文档地址:ScreenCaptureKit

App Store

内购

可以将 App Store Connect 内购产品同步到 Xcode。

新测试功能,比如在沙盒和 Xcode 里请求测试通知和测试其它应用内购买场景。

官方参考:

这里有个 Kevin 开源的微信支付 SDK wechatpay-swift

全球化

session Build global apps: Localization by example

request review

你可以用 requestReview 这个 environment 键提示用户对你的 App 进行评论。示例代码如下:

struct PRequestReview: View {    @Environment(\.requestReview) var rr    var body: some View {        Button("来评论吧") {            rr()        }    }}

Apple 的最佳实践例子 Requesting App Store Reviews

参考

审核

这次审核,规则 4.2.3 中取消二进制要有启动时足够的内容,这可能是因为 Background Assets 的推出可以让用户更快更聪明的下载。另外 5.3.3 放宽了彩排等限制。

性能

Apple 除了做编译优化体积外,还提供了一个 Background Assets 在应用安装后、应用更新时以及应用保留在设备上时定期在后台下载资源,看起来类似 ODR。Background Assets 的 session Meet Background Assets

官方参考:

硬件和虚机

官方参考:

session 有:

虚机的应用可见 insidegui/VirtualBuddy 这个开源项目。

网络

session Reduce networking delays for a more responsive appBuild device-to-device interactions with Network Framework

Metal 3

利用多核优势,高分辨率图形渲染更快,资源加载更快。使用 GPU 训练机器学习网络。WWDC22 期间社区有个给背景添加雨水效果有些流行,作者放出了代码,介绍了如何将 Metal 引入 SwiftUI 工作流,Atmos

官方参考:

RoomPlan

ARKit 支持的新 Swift 接口。使用摄像头和 LiDAR 创建 3D 平面图。另外还有一个视觉库的代码例子很有趣,就是从视频中检测人物行为,Detecting Human Actions in a Live Video Feed

官方参考:

session Create parametric 3D room scans with RoomPlan 。官方示例代码 Create a 3D model of an interior room by guiding the user through an AR experience

Passkeys

身份验证,使用行业标准。

官方参考:

交互设计

Apple 的人机界面交互指南 Human Interface Guidelines 。内容超级详细,涉及程序界面方方面面。

官方参考:

资料

如何用 SwiftUI + Combine + Swift Concurrency Aysnc/Await Actor 欢畅开发

作者 戴铭
2022年1月3日 11:53

先说两句废话(Don’t blame me about my calculation)

为啥写这篇文章,简单说,这些日子以来,总觉着做事还是专注些好,于是也逐步减少了很多信息消费,缩减了些欲望吧。目前更加关注怎么能够让开发更快乐些,相信有了这个方向,其他事情就更容易见招拆招了,面对的挑战也不再是挑战,而是激发自己斗志的辅助工具,其实不用在乎那些看似权威的做法和打法,只要是没让你开心的,肯定是有改进空间的。思路和方向才是最重要的,比如《大侦探波洛》,每次破案之前波洛就已经通过利害关系找好了方向,他的推理都是基于认定的方向去寻找素材。

开心不是因为没有挑战,没有困难,没有煎熬,而是因为找到了方向,这个方向就是,快乐的 Coding,开心的工作,为了达成这个目标那些艰难挑战也就不算什么了。对于 Coding,经过实操,我觉得声明式 UI 响应式编程范式就是很好的提升工作愉悦程度的方式。代码在 GitHub 上,链接。后面我会详细跟你说说这个应用如何开发的及相关知识点,希望你也能够感受下这种 Happy 的开发模式。

这之前,我想先说下为什么我觉得快乐是很件重要的事情。这段时间,我接受了好几次采访,有关于工程师文化方面的,还有《时尚COSMOPOLITAN》杂志的采访,记者会问到一些以前的事情,在聊过往事情时我发现原来快乐才是每天自己存在着的最根本的原动力。为了能够让自己能够一直活着,就不要偏离快乐。摄影师是任欣羽,参与过《一代宗师》的拍摄,还是《时尚芭莎》的模特。以下是时尚 COSMOPOLITAN 的采访内容:

完整内容见:https://mp.weixin.qq.com/s/b5fj2b65xRv4mhFpftwNcg

视频可见这条微博地址:https://weibo.com/1351051897/KEdu5Fi1x?pagetype=profilefeed

视频有六十多万播放量,两百多评论和一千多转发。

话题还上了微博热搜,有六百多万阅读和三千多讨论。

你肯定会觉得很奇怪,我怎么会接受时尚杂志采访,其实我早在2006年就跟时尚娱乐圈有染了,那年张纪中版《神雕侠侣》刚热播完,刘亦菲演的小龙女,我特别的喜欢。有幸在一次活动中我成为她的御用摄影师,由于过于激动手抖,拍糊了好多张,蛮可惜的。私存这批里还是有些清晰的,这些照片最近在找资料时不小心被我翻了出来。挑几张看看十六年前的刘亦菲和我是什么样的吧。

我还很用心的置办了新家。也是希望能够让自己能够开心些。

那么,怎样高效开发,带来愉悦的呢?

看看做出来的样子

这是个 macOS 应用《戴铭的小册子》,能够方便的查看 Swift 语法,还有一些主要库的使用指南,内容还在完善中,选择的库主要就是开发小册子应用使用到的 SwitUI、Combine、Swift Concurrency。

除了这些速查和库的使用内容外,这个应用还有一些开发者的动态,当他们有新的动作,比如提交了代码、star 了什么项目,提交和留言了议题都会直接在程序坞中提醒你。

我对一些库做了分类,方便按需查找,库有新的提交也会在程序坞中提醒。

还能方便的查看库的议题。比如在阮一峰的《科技爱好者周刊》的议题中可以看到有很多人推荐和自荐了一些信息。保留议题有一千六百多个。

这个元旦假期,我又添加了博客动态的功能,可以跟进一些博客内容的更新。

由于 Swift 语言的简洁,这些库的先进,最近有同学做实验,5.5版本还有瘦体积的效果。这样的一个小册子应用程序累积开发的时间不多,就是很高效的嘛。特别是最后博客动态这个功能,七年前我用 Objective-C 做的一个RSS阅读器耗费了我两三周的时间。同样的功能用 Swift 这套来做元旦假期两天就完成了。声明式 UI 响应式范式配合上 Swift 简洁的语法真是蛮 Cool 的。

基础网络能力

小册子应用会大量使用网络,先看看怎么用 Swift Concurrency 来做吧。

func RSSReq(_ urlStr: String) async throws -> String? {  guard let url = URL(string: urlStr) else {    fatalError("wrong url")  }  let req = URLRequest(url: url)  let (data, res) = try await URLSession.shared.data(for: req)  guard (res as? HTTPURLResponse)?.statusCode == 200 else {    fatalError("wrong data")  }  let dataStr = String(data: data, encoding: .utf8)  return dataStr}

如上,通过 url 可以获取到 data 和 response,和其他网络请求的方式不同的是,使用 await 后就不用繁琐的代理或闭包来进行后续的处理,代码变得更好理解,即字面意思上的 await 后执行后面的行。举个例子,获取博客 RSS 时,如果希望处理完一个 RSS 后再处理后面一个 RSS,使用 await 语法看起来就非常简洁清爽易于理解了。

Task {    do {        let rssFeed = SPC.rssFeed() // 获取所有 rss 源的模型        for r in rssFeed {            let str = try await RSSReq(r.feedLink)            guard let str = str else {                break            }            RSSVM.handleFetchFeed(str: str, rssModel: r)            // 在 Main Actor 更新通知数            await rssUpdateNotis()        }    } catch {}}

如上,当出现数据获取错误就跳过后面逻辑直接去请求下个 RSS,获取成功会更新 Main Actor 处理通知逻辑,不同队列之间切换就是这么自然,短短几行代码就都讲清楚了。

Combine 来处理网络的优势就是能够将网络请求到数据处理,最后到数据绑定都负责了。也就是发布者、操作符和订阅者的组合。下面我通过开发指南功能的过程说明下 Combine 的用法。

怎么开发指南功能

指南的列表结构使用的是 JSON,我把列表的数据保存在仓库的议题中,通过 GitHub 的 REST API 获取议题进行展示,这样对于指南列表的内容修改丰富可以通过直接在议题中进行编辑即可,无需升级应用。

Combine 网络请求我写在 APIRequest.swift 文件里,主要代码如下:

final class APISev: APISevType {    private let rootUrl: URL        init(rootUrl: URL = URL(string: "https://api.github.com")!) {        self.rootUrl = rootUrl    }        func response<Request>(from req: Request) -> AnyPublisher<Request.Res, APISevError> where Request : APIReqType {        let path = URL(string: req.path, relativeTo: rootUrl)!        var comp = URLComponents(url: path, resolvingAgainstBaseURL: true)!        comp.queryItems = req.qItems        var req = URLRequest(url: comp.url!)        req.addValue("token \(SPC.gitHubAccessToken)", forHTTPHeaderField: "Authorization")        req.addValue("SwiftPamphletApp", forHTTPHeaderField: "User-Agent")        let de = JSONDecoder()        de.keyDecodingStrategy = .convertFromSnakeCase        let sch = DispatchQueue(label: "GitHub API Queue", qos: .default, attributes: .concurrent)        return URLSession.shared.dataTaskPublisher(for: req)            .retry(3)            .subscribe(on: sch)            .receive(on: sch)            .map { data, res in                return data            }            .mapError { _ in                APISevError.resError            }            .decode(type: Request.Res.self, decoder: de)            .mapError { _ in                APISevError.parseError            }            .receive(on: RunLoop.main)            .eraseToAnyPublisher()    }}

如上,Combine 有 decode 的操作符,能够直接指定 JSON 模型数据类型和 JSONDecoder 对象。还有重试、队列指定以及抛错误的操作符。

一个应用的生命周期内,相同的请求会发布很多次,需要定义一个发起请求的 Subject,还有请求完成响应的 Subject。定义如下:

private let apCustomIssuesSj = PassthroughSubject<Void, Never>()private let resCustomIssuesSj = PassthroughSubject<IssueModel, Never>()

apCustomIssuesSj 会发起网络请求,代码如下:

let resCustomIssuesSm = apCustomIssuesSj    .flatMap { [apiSev] in        apiSev.response(from: reqCustomIssues)            .catch { [weak self] error -> Empty<IssueModel, Never> in                self?.errSj.send(error)                return .init()            }    }    .share()    .subscribe(resCustomIssuesSj)

上面 .catch 里errSj 发布者就是嵌套发布者,.flatMap 会让每次返回都是新发布者。apiSev.response 返回的是被类型擦除到 AnyPublisher 上,这样不同类型的发布者能够被 .flatMap 处理。闭包内的 .catch 处理能区分发布者,仅对当前发布者有效,不会影响后面发布者,导致整个管道被取消。发布者失败类型是 Never,失败本身会被连贯的处理。

.flatMap 除了从它 map 函数里生产发布者,还有个可选参数 maxPublishers,通过这个参数可以限制一次生产的最大发布者数量,也就是你可以通过 .flatMap 对管道上游的发布者进行反压(Backpressure),maxPublishers 能有效的节流管道,按照管道内部实际上的发布速度进行反压,这个也是 Combine 相较于 RxSwift 来说的一个优势。比如当网络请求多时,你可以通过设置 .max(1) 来减轻请求对服务的压力,同时还能够保证结果到达的顺序和请求顺序的一致。

resCustomIssuesSj 会去处理网络请求成功的数据,最后通过 .assign 将处理的数据分配给遵循 ObservableObject 协议类的 @Published 属性包装的属性 customIssues,用于响应式的更新 SwiftUI 布局数据。实现代码如下:

let repCustomIssuesSm = resCustomIssuesSj    .map({ issueModel in        let str = issueModel.body?.base64Decoded() ?? ""        let data: Data        data = str.data(using: String.Encoding.utf8)!        do {            let decoder = JSONDecoder()            return try decoder.decode([CustomIssuesModel].self, from: data)        } catch {            return [CustomIssuesModel]()        }    })    .assign(to: \.customIssues, on: self)

如上,你会发现在 .map 中还会对数据进行 base64 decode,这是因为我在仓库议题中保存的是 base64 encode 的数据,decode 成 JSON 数据再用 JSONDecoder 转为 [CustomIssuesModel] 模型 数据分配给 customIssues。

使用 SwiftUI 写的指南列表视图,代码如下:

struct IssuesListFromCustomView: View {    @StateObject var vm: IssueVM    var body: some View {        List {            ForEach(vm.customIssues) { ci in                Section {                    ForEach(ci.issues) { i in                        NavigationLink {                            IssueView(vm: IssueVM(repoName: SPC.pamphletIssueRepoName, issueNumber: i.number))                        } label: {                            Text(i.title)                                .bold()                        }                    }                } header: {                    Text(ci.name).font(.title)                }            }        }        .alert(vm.errMsg, isPresented: $vm.errHint, actions: {})        .onAppear {            vm.doing(.customIssues)        }    }}

代码中的属性包装 @StateObject 会在当前视图生命周期中保持 vm 这个属性的数据,vm 需要遵循 ObservableObject 协议,其 @Published 发布属性的值会被 SwiftUI 自动进行管理,属性 vm 的发布属性数据变化时会自动触发布局依据新数据的更新。

上面代码中的 SwiftUI 写的布局界面效果如下:

界面主体是 List 视图,根据 List 的定义,要求的输入是一个数组,数组内元素需要遵循 Identifiable,每行的返回是被 @ViewBuilder 标记的 View。ForEach 根据数组中的元素会创建能够重复使用的视图,性能接近大家熟悉的 UITableView,但是写法上简洁的不要太多,真实完美解痛点案例,😄❤️。

指南的内容也会以 markdown 格式存在议题中,通过调用 GitHub API 的接口进行指南内容的读取。一个接口是议题接口,请求结构体定义如下:

struct IssueRequest: APIReqType {    typealias Res = IssueModel    var repoName: String    var issueNumber: Int    var path: String {        return "/repos/\(repoName)/issues/\(issueNumber)"    }    var qItems: [URLQueryItem]? {        return nil    }}

另一个是议题留言的接口,定义如下:

struct IssueRequest: APIReqType {    typealias Res = IssueModel    var repoName: String    var issueNumber: Int    var path: String {        return "/repos/\(repoName)/issues/\(issueNumber)"    }    var qItems: [URLQueryItem]? {        return nil    }}

实现效果如下图:

指南内容放在议题中,也是希望能够通过议题留言功能,让反馈和大家经验的补充被更多人看到。

除了语法速查的内容,关于 Swift 的一些特性,专题,还有 Combine、Concurrency、SwiftUI 这些库的使用指南内容都是采用的 GitHub API 接口读取议题方式获取的。

读取议题接口获取指南列表的模式,也用在了开发者和仓库动态列表中。接下来我跟你说下开发者和仓库动态怎么开发的吧。

开发者和仓库动态

显示开发者信息的页面代码在 UserView.swift 里,开发者介绍信息页面如下:

界面中的数据都来自 /users/(userName) 接口,获取数据逻辑在 UserVM.swift 里。数据多,但情况不复杂,布局上只要注意进行数据是否有的区分即可,布局代码如下:

HStack {    VStack(alignment: .leading, spacing: 10) {        HStack() {            AsyncImageWithPlaceholder(size: .normalSize, url: vm.user.avatarUrl)            VStack(alignment: .leading, spacing: 5) {                HStack {                    Text(vm.user.name ?? vm.user.login).font(.system(.title))                    Text("(\(vm.user.login))")                    Text("订阅者 \(vm.user.followers) 人,仓库 \(vm.user.publicRepos) 个")                }                HStack {                    ButtonGoGitHubWeb(url: vm.user.htmlUrl, text: "在 GitHub 上访问")                    if vm.user.location != nil {                        Text("居住:\(vm.user.location ?? "")").font(.system(.subheadline))                    }                }            } // end VStack        } // end HStack                if vm.user.bio != nil {            Text("简介:\(vm.user.bio ?? "")")        }        HStack {            if vm.user.blog != nil {                if !vm.user.blog!.isEmpty {                    Text("博客:\(vm.user.blog ?? "")")                    ButtonGoGitHubWeb(url: vm.user.blog ?? "", text: "访问")                }            }            if vm.user.twitterUsername != nil {                Text("Twitter:")                ButtonGoGitHubWeb(url: "https://twitter.com/\(vm.user.twitterUsername ?? "")", text: "@\(vm.user.twitterUsername ?? "")")            }        } // end HStack    } // end VStack    Spacer()}

上面代码可以看到,对于数据是否存在,SwiftUI 是可以使用 if 来进行判断是否展示视图的,这个条件判断也会存在于整个视图结构类型中被编译生成,因此更好的方式是将数据判断放到 ViewModifier 中,因为 ViewModifier 处理时机是在运行时,可以减少布局初始创建逻辑运算。

开发者的事件和接受事件部分的数据就比介绍部分复杂些,使得界面变化也多些,事件接口是 /users/(userName)/events,接受事件接口是 /users/(userName)/received_events 。数据的复杂体现在类型上,类型种类较多,我采用的是直接处理 payload 里的字段,如果其 issue.number 字段不为空,那么就表示这个开发者事件是和议题相关,会显示 issue.title 标题,有内容的话,也就是 issue.body 不为空,继续显示议题的内容。如果字段是 comment,就表示事件是议题的留言。如果字段是 commits,表示需要列出这个事件中所有的 commit 提交及标题和描述。pullRequest 字段不为空就显示这个 PR 的标题和内容描述。字段处理逻辑代码实现如下:

if event.payload.issue?.number != nil {    if event.payload.issue?.title != nil {        Text(event.payload.issue?.title ?? "").bold()    }    if event.payload.issue?.body != nil && event.type != "IssueCommentEvent" {        Markdown(Document(event.payload.issue?.body ?? ""))    }    if event.type == "IssueCommentEvent" && event.payload.comment?.body != nil {        Markdown(Document(event.payload.comment?.body ?? ""))    }}if event.payload.commits != nil {    ListCommits(event: event)}if event.payload.pullRequest != nil {    if event.payload.pullRequest?.title != nil {        Text(event.payload.pullRequest?.title ?? "").bold()    }    if event.payload.pullRequest?.body != nil {        Markdown(Document(event.payload.pullRequest?.body ?? ""))    }}if event.payload.description != nil {    Markdown(Document(event.payload.description ?? ""))}

上面代码中,对于不定数量的 commit 视图写在了一个单独的 ListCommits 视图中。只要是遵循了 View 协议,就可以作为自定义视图在其他视图中直接使用。ListCommits 代码如下:

struct ListCommits: View {    var event: EventModel    var body: some View {        ForEach(event.payload.commits ?? [PayloadCommitModel](), id: \.self) { c in            ButtonGoGitHubWeb(url: "https://github.com/\(event.repo.name)/commit/\(c.sha ?? "")", text: "提交")            Text(c.message ?? "")        }    }}

上面代码你会发现一个 ButtonGoGitHubWeb的视图,进入看会发现用到了一个自定义的 ButtonStyle:

.buttonStyle(FixAwfulPerformanceStyle())

FixAwfulPerformanceStyle() 的实现如下:

/// 列表加按钮性能问题,需观察官方后面是否解决/// https://twitter.com/fcbunn/status/1259078251340800000struct FixAwfulPerformanceStyle: ButtonStyle {    func makeBody(configuration: Self.Configuration) -> some View {        configuration.label            .font(.body)            .padding(EdgeInsets.init(top: 2, leading: 6, bottom: 2, trailing: 6))            .foregroundColor(configuration.isPressed ? Color(nsColor: NSColor.selectedControlTextColor) : Color(nsColor: NSColor.controlTextColor))            .background(configuration.isPressed ? Color(nsColor: NSColor.selectedControlColor) : Color(nsColor: NSColor.controlBackgroundColor))            .overlay(RoundedRectangle(cornerRadius: 6.0).stroke(Color(nsColor: NSColor.lightGray), lineWidth: 0.5))            .clipShape(RoundedRectangle(cornerRadius: 6.0))            .shadow(color: Color.gray, radius: 0.5, x: 0, y: 0.5)    }}

这是社区 @Kam-To 提的一个 PR,是解的 macOS 上的一个性能问题,也就是在 List 中直接使用 Button,在列表快速滚动时,流畅度会有损伤,加上上面的 ButtonStyle 代码就好了。

原推见 https://twitter.com/fcbunn/status/1259078251340800000

开发者接受事件和事件类似,只是会多显示事件的 actor 字段内容,表明开发者接受的是谁发出的事件。事件界面如下所示:

仓库整体处理和开发者类似,只是多了议题和 README 内容,数据复杂度比开发者要低。接下来我要跟你说的是如果开发者或仓库有新的提交,怎么能够获取到,并提示有更新。

动态有更新,怎么提醒的

我的思路是通过本地定时器,定期获取数据,本地记录上次浏览的位置,通过对比,看有多少新的动态没有查看,并通过 .badge 这个 ViewModifier 和 NSApp.dockTile.badgeLabel 来进行端内端外的提醒。

定时器

在 SwiftUI 中,可以使用 Combine 的 Timer.publish 发布器来设置一个定时属性,Timer.publish 指定好时间周期和队列模式等参数。比如设置一个开发者动态定时器属性,代码如下:

let timerForRepos = Timer.publish(every: SPC.timerForReposSec, on: .main, in: .common).autoconnect()

然后再在 .onReceive 中执行网络数据获取操作,就可以定时获取数据了。

.onReceive(timerForRepos, perform: { time in    if let repoName = appVM.timeForReposEvent() {        let vm = RepoVM(repoName: repoName)        vm.doing(.notiRepo)    }})

获取到的数据会跟本地已经存储的数据进行对比。

本地存储

本地数据存储,我用的是 SQLite.swift,这个库是使用 Swift 对 SQLite 做了一层封装,使用很简便,在 DBHandler.swift 里有数据库初始化和表的创建相关代码,DBDevNoti.swift 中的 DevsNotiDataHelper 有对数据操作的代码,DBDevNoti 定义了数据表的结构。如何使用可以参考 SQLite.swift 官方的指南,里面讲得非常详细清楚。

用 DB Browser for SQLite 应用可以查看本地的数据库。下面是用它查看记录的 RSS 的数据,如图:

更新未读数的判断逻辑,我封到了一个函数里,代码如下:

func updateDBDevsInfo(ems: [EventModel]) {    do {        if let f = try DevsNotiDataHelper.find(sLogin: userName) {            var i = 0            var lrid = f.lastReadId            for em in ems {                if i == 0 {                    lrid = em.id                }                if em.id == f.lastReadId {                    break                }                i += 1            }            i = f.unRead + i            do {                let _ = try DevsNotiDataHelper.update(i: DBDevNoti(login: userName, lastReadId: lrid, unRead: i))            } catch {}        } // end if let f    } catch {}} // end func updateDBDevsInfo

如上面代码所示,入参 ems 是获取到的最新数据,先从本地数据库中取到上次最新的阅读编号 lastReadId,迭代 ems,如果第一个 ems 的编号就和本地数据库 lastReadId 一样,那表示无新动态,如果没有就开始计数,直到找到相等的 lastReadId 位置,记了多少数就表示有多少新动态。

提醒

列表、Sidebar 还有 macOS 系统的 Dock 上都可以显示新状态数的提醒。列表和 Sidebar 直接使用 .badge ViewModifier 就可以展示未读数了,效果如下:

Dock 栏提示设置需要用到系统的 NSApp,代码如下:

NSApp.dockTile.showsApplicationBadge = trueNSApp.dockTile.badgeLabel = "\(count)"

小册子里还可以查看 Swift 社区里博主们博客更新动态。我接着跟你说说我怎么做的。

博客 RSS 更新动态

博客 RSS 的数据获取我在前面基础网络能力中已经说了。所有解析逻辑我都写在了工程 RSSReader/Parser/ 目录下的 ParseStandXMLTagTokens.swift、ParseStandXMLTags.swift、ParseStandXML.swift 三个文件中,实现思路我在先前《如何对 iOS 启动阶段耗时进行分析》文章的“优化后如何保持?”章节有详细说明。

根据 RSS 的 XML 结构,定义 Model 结构如下:

struct RSSModel: Identifiable {    var id = UUID()    var title = ""    var description = ""    var feedLink = ""    var siteLink = ""    var language = ""    var lastBuildDate = ""    var pubDate = ""    var items = [RSSItemModel]()    var unReadCount = 0}struct RSSItemModel: Identifiable {    var id = UUID()    var guid = ""    var title = ""    var description = ""    var link = ""    var pubDate = ""    var content = ""    var isRead = false}

根据这个结构,也会在本地数据库设计对应的两个表,两个表的增删改代码分别在 DBRSSFeed.swift 和 DBRSSItems.swift 里。表的结构和 Model 的结构基本一致,方便内存和磁盘进行切换。更新提醒逻辑和前面说的开发者动态更新逻辑区别在于,RSS 使用 isRead 标记有没有阅读过,直接在本地数据里 count 出 isRead 字段值为 false 的数量就是需要提醒的数。

新 RSS 的添加会先在本地数据库中查找是否有存在,依据的是文章的 url,如果不存在就会添加到数据库中设置为未读作为提醒。

RSS 里文章的内容是 HTML,显示内容使用的是 WebKit 库,要在 SwiftUI 中使用,需要封装下,代码如下:

import SwiftUIimport WebKitstruct WebUIView : NSViewRepresentable {    let html: String    func makeNSView(context: Context) -> some WKWebView {        return WKWebView()    }    func updateNSView(_ nsView: NSViewType, context: Context) {        nsView.loadHTMLString(html, baseURL: nil)    }}

效果如下图:

云打包

工程如果是本地编译,在 SwiftPamphletAppConfig.swift 的 gitHubAccessToken 中添上 token 就可以了,如果想快速打包使用小册子,使用 Github Action Workflow 编译,无需在本地操作、也无需开启 Xcode 设置个人开发帐号,只需设置 personal access token(PAT) 在 repository 设定中 action secrets,并命名为 PAT。Frok 此 repository,设置 PAT,手动启用 action,等候约3分钟即可下载档案,往后专案更新时,只需 fetch and merge,action 会自动进行。非常感谢社区 @powenn 开发的这个 Github Action。

推荐可以学习的开源仓库

为了避免闭门造车,可以多关注些开源项目,以下这些仓库是我放在小册子里可以关注到更新动态的项目,这里作为附录列下,也可以直接在小册子里查看。除了 Swift 也有些非常有趣的项目,希望可以丰富到你的开发生活。

好库

官方

新鲜事

封装易用功能

网络

图片

文字处理

动画

持久化存储

编程范式

路由

静态检查

系统能力

接口

macOS程序

性能和工程构建

音视频

服务器

探索库

SwiftUI扩展

接口应用

macOS

应用

  • Clendar SwiftUI 写的日历应用

游戏

新技术展示

新鲜事

聚合

知识管理

  • logseq 更好的知识管理工具

性能和工程构建

网络

图形

系统

Apple

待分类

❌
❌