阅读视图

发现新文章,点击刷新页面。

宝可梦宇宙全揭秘:从皮卡丘到千种精灵,入门玩法、进化对战与全球现象深度解析

卷首词
紫电青霜藏芥子,灵珠玉露幻奇缘。
千般妙相云中隐,一卷丹青掌上悬。
且看乾坤藏袖底,莫言造化戏樽前。
银屏漫展逍遥境,万类精灵共此天。

前言:一封来自宝可梦宇宙的邀请函

当熟悉的旋律在耳畔响起,当“皮卡丘!”的呼喊划破次元,你是否也会瞬间被拉回那个充满奇遇与梦想的童年?最近,随着 Delta ,Manic EMU ,xone等一众游戏模拟器的涌现,尘封的记忆再度鲜活,无数经典老游戏重焕生机,其中,经久不衰的“宝可梦”系列,无疑是无数玩家心中最柔软的角落。特别是当3DS模拟技术日渐成熟,我们终于能在指尖重温那些更加立体、更加鲜活的宝可梦冒险。

我和柠檬,在我们的播客“铭莉双收”中,也曾数次聊了宝可梦的魅力。柠檬很喜欢宝可梦游戏,还有相关宝可梦的漫画,日常刷短视频都离不开这些小可爱们。为此,我特意搜集整理了海量资料,尤其是bulbapedia52pokeThe PokéCommunity Forums这些宝库,它们如同宝可梦图鉴般包罗万象,令人叹为观止。如今,我将这些心得体会与资料梳理成篇,亦同步更新至小册子中,希望能为同好们提供一份更便捷的查阅指南。

以下是本篇正文。

宝可梦的魅力就是是“一直在进化,从未被超越”!从最早在 Game Boy 上那“糊你一脸”的“红绿蓝黄”像素风,到 GBA 上让人眼前一亮的斑斓色彩,再到 NDS 让人惊呼“哇塞”的双屏互动,3DS 的裸眼3D更是酷到没朋友,直到现在 Switch 上的开放世界,每一代主机上的宝可梦,都像是换了身新衣服,带着新技能闪亮登场,总能给你新的惊喜。更别提那经典的“双版本大法”(逼死收集癖)、时不时来个情怀满满的“复刻版”,还有每个地区都风情万种的设计,花式掏空你钱包啊!

1996年,当《宝可梦 红/绿》在 Game Boy 这个“老古董”掌机上悄然登场,谁也没想到,一个传奇就此拉开序幕。1997年开播的动画片,火遍了全球192个国家和地区(“皮卡皮卡”的声音谁不熟悉?);电影系列票房累计超过10亿美元,赚得盆满钵满;还有漫画书、交换式卡牌(这个更厉害,全球卖出超过648亿张!一张稀有的“皮卡丘插画师”卡牌,居然拍出了527.5万美元的天价!),再加上数不清的毛绒玩具、手办周边……整个游戏系列累计销量更是突破了4.8亿套,授权商品的总收入将近一千亿美元!稳稳地坐在“全球最赚钱媒体系列”的头把交椅上。

直到今天,宝可梦系列还在不断搞事情,什么Mega进化、极巨化这些新机制啦,还有源源不断的新宝可梦(官方认证的都超过1008种了,而且还在疯狂增加中!),从 Game Boy 上的像素小人到 Switch 上的3D高清大片,一直在吸引着从90后到Z世代的全球粉丝。它在各个市场的表现都强劲得不行(比如2024年在欧洲市场,销售额居然还同比增长了30%!),而且每年2月27号的“宝可梦日”,全世界的粉丝都会一起庆祝,这活力,杠杠的!

作为任天堂家的三大台柱子IP之一(另外俩是马力欧和塞尔达,个个都是重量级),宝可梦可以说是一路为任天堂的历代主机保驾护航,一起书写了游戏史上的辉煌篇章。快三十年了,它还在不断拓展自己的产业边界,用实力证明:一个真正的文化现象,那是技术、情感和社会价值的完美结合,缺一不可!

宝可梦的诞生:从昆虫少年的梦想到达成全球现象

宝可梦这颗神奇的种子,最早是在创始人田尻智的童年时光里悄悄埋下的。这位1965年出生在东京的哥们儿,小时候可不是什么乖乖仔,而是个痴迷于在自家附近田埂边、树林里捉昆虫的“昆虫博士”。每天抓了放,放了抓,乐此不疲。这段经历,就像给后来的宝可梦核心理念——“收集、养成”——打下了最早的草稿。随着城市化的滚滚车轮碾过,他童年的秘密乐园也渐渐消失了。于是,田尻智把阵地转移到了当时新兴的街机厅,在游戏上还真有两把刷子,很快就成了远近闻名的“街机小王子”。

1981年,田尻智干了件大事儿——他自己捣鼓出了一本叫做《Game Freak》的游戏同人杂志。在杂志里,他针砭时弊(主要是吐槽游戏烂尾或者难度变态),分享各种独门攻略,迅速在玩家圈子里打响了名号。也正是因为这本杂志,他认识了两位日后宝可梦系列的“左膀右臂”——艺术总监杉森建和音乐总监增田顺一。杂志搞得有声有色,田尻智心也野了,寻思着:“光说不练假把式,我也要自己做游戏!”于是,Game Freak 公司就这么诞生了。当1990年,任天堂推出了那台划时代的、带联机功能的 Game Boy 掌机时,田尻智的眼睛噌地就亮了!他突然想起了小时候跟小伙伴们交换昆虫标本的快乐时光,一个基于“收集、培养、交换、对战”神奇生物的社交游戏概念——最初还起了个名字叫“胶囊怪兽”(Capumon)——在他脑海里逐渐清晰起来。

六年磨一剑,差点磨断气了呀!

田尻智的灵感,一方面来自大自然,比如昆虫的变态发育就对应宝可梦的进化,属性相克就是生态链的翻版;另一方面也深受日本特摄片《赛文奥特曼》里那些可以装在胶囊里的怪兽设定的启发。在他看来,Game Boy 那根神奇的联机线,就是实现“交换”乐趣、让游戏活起来的关键钥匙。然而,在机能弱得可怜的 Game Boy 上实现这么宏大的构想,对于刚刚起步的 Game Freak 来说,简直是地狱级难度。

这条开发之路,一走就是六年,远远超过了最初乐观估计的半年。那过程,怎一个“惨”字了得:

  • 技术太难,钱包太扁: 想想看,要在只有4.19MHz CPU 和可怜的8KB内存的 Game Boy 上,塞进151种不同的宝可梦数据、复杂的战斗系统,还要实现顺畅的联机交换功能,简直是“螺蛳壳里做道场”!没办法,团队只能硬着头皮用像素画风、地图块移动这些土办法来节省宝贵的资源。为了强化社交性,顺便也绕开一些技术难题,田尻智灵机一动,设计了某些宝可梦必须通过联机交换才能进化的机制,这招可真绝!开发期间,Game Freak 一度穷得叮当响,差点就关门大吉了。田尻智自己都停发工资了,公司全靠接点外包项目(比如给任天堂做了个叫《耀西的蛋》的游戏)和 Creatures 公司的救命钱才勉强撑了下来。
  • 任天堂的“您再瞅瞅?”与宫本茂的“我看行!”: 任天堂一开始对这个听起来就特复杂、特不靠谱的项目其实是有点打退堂鼓的。幸运的是,创造了马力欧的“游戏大神”宫本茂慧眼识珠!他力排众议,非常欣赏田尻智的创意,并且给与了关键性的支持。比如,他建议搞“双版本”发行(就是后来的《红》与《绿》,这样大家为了集齐图鉴就不得不互相交换,高明!),还帮忙加入了属性相克系统,甚至亲自下场协助优化联机技术,硬是把交换时间从让人等到花儿都谢了的3分钟缩短到了嗖嗖的20秒!后来的宝可梦公司CEO石原恒和,在市场推广上也帮了大忙。

最终,在1996年2月27日,历经磨难的《宝可梦 红/绿》终于在日本发售了。游戏里的主角“小智”(Satoshi)和他的劲敌“小茂”(Shigeru)这两个名字,其实就是田尻智和宫本茂两位大佬互相“商业互吹”,啊不,是致敬对方呢!

初代逆袭:从无人问津到“现象级”的华丽转身

《宝可梦 红/绿》刚上市那会儿,Game Boy 在当时已经算是“爷爷辈”的掌机了,所以初期的销量嘛,只能说“emmm,一般般”(首周大概卖了12万份)。再加上游戏里还有不少Bug,宣传也不给力,一度面临着“凉凉”的风险。真正的转机,出现在玩家之间的口耳相传、《CoroCoro漫画》的给力连载,以及一个堪称里程碑的“意外惊喜”——编号151的隐藏宝可梦“梦幻”(Mew)的揭秘!这只由程序员森本茂树“偷偷摸摸”塞进游戏里的“彩蛋”,经过杂志搞的限量赠送活动(据说只送了20只!),一下子引爆了玩家们的探索热情和社交讨论,简直是病毒式传播!销量就像坐了火箭一样奇迹般地往上蹿,到了1997年,居然突破了365万份,把同期的大作《最终幻想VII》都给甩在了身后,成了当年的销量冠军!最终,初代在日本本土就卖出了超过1600万套。后来,修复了Bug还优化了内容的《蓝》版(1998年)和跟动画片联动、皮卡丘能屁颠屁颠跟在主角后面的全彩《黄》版(1999年),更是把这股热潮推向了顶峰。初代全球销量高达3140万份,不仅成功地给 Game Boy 这位“老将”续了一波命,也为宝可梦日后走向世界打下了坚实的基础。

幕后大佬

《宝可梦》能有今天的辉煌,可不是某一个人的功劳,而是 Game Freak、Creatures Inc. 和任天堂这“吉祥三宝”紧密合作、各显神通的结果。他们还一起投资成立了 The Pokémon Company 这个“大管家”来进行统一指挥,整个运作模式就像一套设计精密的属性相克系统一样高效,共同构建了这个全球最具价值的IP生态圈。

Game Freak (株式会社ゲームフリーク)由田尻智、杉森建、增田顺一在1989年创立,前身就是那本同人杂志《GAME FREAK》。他们的核心使命,就是把田尻智小时候捉虫的乐趣,变成能让全世界都玩上头的数字娱乐。所以,《宝可梦》系列最最核心的那些角色扮演游戏(RPG),从策划、设计、敲代码到配音乐,全都是他们一手包办的。从1996年的《红/绿》到最新的正统续作,没跑!

田尻智 (Satoshi Tajiri) 这位就是“宝可梦之父”本尊啦,Game Freak的创始人。他那“收集、养成、交换、对战”的核心理念,都源于他自己的童年经历和对掌机联机功能的敏锐洞察。

杉森建 (Ken Sugimori) 长年担任艺术总监和角色设计师,初代那151只宝可梦以及后来许许多多深入人心的形象,都出自他的神来之笔,奠定了整个系列的视觉风格。可以说,我们看到的宝可梦长啥样,他说了算!

增田顺一 (Junichi Masuda) 一开始是负责音乐和音效的(那些经典的BGM很多都是他写的!),后来逐渐成长为游戏总监和制作人,对系列的发展方向和核心玩法(比如属性相克系统)都有着举足轻重的影响。

Creatures Inc. (株式会社クリーチャーズ)这家公司是石原恒和在1995年创立的,当时还获得了岩田聪(时任HAL研究所社长,也是个传奇人物)的资金支持。作为IP的共同持有人之一(占了三分之一的版权呢),他们主要负责《宝可梦》相关产品的制作和管理,尤其是在那个风靡全球、堪称“纸片黄金”的集换式卡牌游戏(Pokémon TCG)的开发生产上,绝对是挑大梁的角色!除此之外,他们还参与游戏3D模型的制作、数据压缩技术(省卡带空间就靠它了!)、品牌管理,并且还开发了一些衍生游戏(比如《宝可梦巡护员》、《名侦探皮卡丘》这些脑洞作品)和各种让你忍不住剁手的周边产品。

石原恒和 (Tsunekazu Ishihara) 这位大佬在《宝可梦 红/绿》开发后期以及后来 The Pokémon Company 的成立过程中都发挥了至关重要的作用,并且长期担任CEO,是宝可梦商业帝国的掌舵人之一。2023年他从Creatures社长的位置上退下来,更专注于TPC的全球战略了。

Creatures 主导的宝可梦TCG,全球销量已经超过了惊人的529亿张!他们还采用了各种先进的防伪技术,那些稀有的卡牌在拍卖会上屡屡创下天价纪录,简直比真金白银还值钱!在新任社长北野祐司的带领下,这家公司现在还积极布局元宇宙和NFT这些新潮领域,看来是要搞波大的!

Nintendo (任天堂株式会社)作为游戏发行商和平台持有者(就是做游戏机的那个!),任天堂负责《宝可梦》系列游戏在全球的发行、市场营销和销售。我们玩的那些承载着宝可梦作品的 Game Boy、DS、Switch 等游戏机,全都是任天堂家的。同时,任天堂还拥有“宝可梦”商标权

宫本茂 (Shigeru Miyamoto) 前面提过啦,在《红/绿》开发最困难的时候,是他给出了关键指导(比如双版本发行、属性相克这些点子)。

横井军平 (Gunpei Yokoi) Game Boy 的设计者,正是他设计的联机功能启发了田尻智。

岩田聪 (Satoru Iwata) 前任天堂社长,不仅是商业奇才,也是个技术大神。当年他曾亲自下场,帮 Game Freak 解决了《金/银》开发时遇到的技术难题,并且一直持续支持着宝可梦系列的发展。

任天堂的硬件一代代升级(从 Game Boy 的联机功能到 Switch 的便携性和强大性能),为宝可梦系列的技术进步提供了坚实的基础。通过《宝可梦GO》这款现象级手游,利用LBS+AR技术,更是成功地把宝可梦文化输出到了全世界的每一个角落。他们这套“硬件+IP+社群”的组合拳,简直是业界典范!任天堂通过其子公司对 The Pokémon Company 实现了控股,确保了宝可梦这个超级IP能够得到统一的管理和全球化的运营,包括跨媒体讲故事(游戏、动画、电影多管齐下)和针对不同地区的本土化策略。当然,对于那些想蹭热度搞盗版的,任天堂的法务部也不是吃素的!

为了更有效地管理和推广宝可梦这个越来越大的品牌,上面那三家在1998年一合计,共同出资成立了TPC(最初的名字叫Pokémon Center Co., Ltd.),并请石原恒和出任CEO,负责统筹全局。TPC就像是宝可梦品牌的总指挥部和授权中心。除了最核心的游戏开发(那是Game Freak的事儿),其他所有跟宝可梦品牌相关的事情,比如品牌授权、市场营销、动画电影的制作发行、TCG卡牌的全球运营、各种衍生游戏的管理,还有官方周边商品的策划销售等等,几乎都由TPC来全面负责。TPC整合了三家母公司的资源,大力推动宝可梦在全球范围内的跨媒体发展,目标就是把它打造成一个永不过时、代代相传的文化符号。TPC除了要操心怎么让宝可梦更好玩、更受欢迎,还得积极应对各种盗版侵权问题,保护好这个金字招牌。展望未来,TPC还计划在元宇宙和AI技术这些前沿领域搞点新花样,比如开发虚拟偶像啦,或者利用生成式AI来设计新的宝可梦形象啦,想想都觉得挺酷的!

初代像素风暴,一切的“真香”从这里开始! (1996-1999) —— 红、绿、蓝、黄,时代的眼泪啊!

想当年,咱们还是个孩子(或者你现在就是个孩子,那更得好好听听老前辈的故事了!),在一个叫做“关都地区”(原型就是日本的关东啦)的地方,我们的宝可梦传奇就这么拉开了序幕!目标?那必须是成为最牛的宝可梦联盟冠军,顺便把那本厚厚的图鉴给它填满了!

  • 当年那些“小霸王”上的神作 (Game Boy平台,暴露年龄系列):

    • 《红/绿》(日本本土版):1996年2月27号,历史性的一天!
    • 《蓝》(日本本土版):同年10月15号,一开始还是限定邮购的稀罕货,到了1999年才公开发售,吊足了胃口!
    • 《红/蓝》(国际版,咱们接触到的版本):北美人民1998年9月28号先玩上,欧洲小伙伴们则是1999年10月5号。
    • 《皮卡丘》(黄版,谁的童年没有一只“皮神”跟着跑?):日本1998年9月12号,北美1999年10月19号,欧洲更是等到了2000年6月16号。
  • 为啥这么火?火到没朋友!

    • 口碑销量齐上天:全球卖了超过3100万份!啥概念?就是人手一份还绰绰有余!Bug满天飞,但快乐也起飞! 虽然当年的游戏里有不少奇奇怪怪的Bug(比如那个能让你道具无限的“MissingNo.”,当年谁没手贱试过?虽然有搞坏存档的风险,但刺激啊!),但这丝毫挡不住大家的热情!“收集、养成、交换、对战”这四大法宝一出,谁与争锋?特别是那个跟动画片神同步的《黄》版,皮卡丘在你屁股后面“皮卡皮卡”地叫,简直萌翻了,人气更是高到突破天际!
    • 奠基石,影响后世一百年(可能不止)
      • “抓、养、打、换”这套核心玩法,就是从这儿定下来的!
      • 创造了一个让我们魂牵梦萦的宝可梦世界,还有那些一眼就能认出来的经典形象(皮卡丘?那已经是全球通用的文化符号了好吗!)。
      • “Gotta Catch ‘Em All!” 这句口号,喊遍了五湖四海!
      • 直接引爆了全球性的宝可梦狂潮,各种玩具、卡牌、动画片像雨后春笋一样冒出来,一个跨媒体的超级IP就这么诞生了!
      • 特别是那个关于隐藏宝可梦“梦幻”的都市传说和官方的“饥饿营销”。

版本之间有啥不一样?“换汤不换药”的小心机!

  • 《红/绿》(日版元祖): 双版本传统的开山鼻祖!主要区别就是有些宝可梦是你这个版本死活抓不到的(比如红版有阿柏蛇,绿版才有穿山鼠),还有些地图细节上有点小差异。早期的Bug也是层出不穷(MissingNo.:没错,正是在下!)。
  • 《蓝》(日版优化版): 算是《红/绿》的“导演剪辑修复版”,修了些Bug,宝可梦的画像和图鉴说明也更新了,音乐和宝可梦的分布也做了些调整(比如蓝版就能抓到杰尼龟了,美滋滋!)。咱们玩的国际版《红/蓝》,程序和图像是基于日版《蓝》的,但宝可梦的出现列表又对应日版的《红/绿》,有点绕是吧?
  • 《皮卡丘》(黄版,动画党的狂欢!):
    • 最大的不同?那可太多了!
      • 开局直接送你一只皮卡丘当伙伴,而且它还特傲娇,不肯进精灵球,就喜欢跟在你屁股后面跑,时不时还给你个表情包,甚至有专属配音!你的老对头,也就是劲敌,开局则会拿到一只伊布。
      • 剧情、NPC(比如君莎小姐、乔伊小姐)都更贴近动画片,火箭队的武藏、小次郎还有那只会说话的喵喵,也成了固定来找茬的Boss。
      • 御三家(妙蛙种子、小火龙、杰尼龟)不用愁,路上会有好心的NPC送给你,或者通过特定事件就能拿到。
      • 画面也升级了,部分场景实现了全彩,角色之间还有动态交互,甚至还贴心地加入了“属性克制提示”(妈妈再也不用担心我被克制了!)。不过,有些宝可梦(比如皮卡丘的进化型雷丘)在这个版本里是得不到的,想进化?老老实实跟别的版本换吧!
      • 还有个独家福利:在19号路的别墅里,如果你的皮卡丘学会了“冲浪”(对,你没看错,电气老鼠会冲浪!),就能玩一个冲浪小游戏!
    • 为啥这么设计?逼你社交啊!:版本限定的宝可梦,就是为了让你拉着小伙伴,用那根珍贵的联机线交换,不然你的图鉴永远都少那么几块!《黄》版里的皮卡丘更是个“钉子户”,死活不肯进化(除非你把它交换到别的版本再用雷之石),这不明摆着让你跟它培养深厚感情嘛!所以,《黄》版也成了初代里卖得最好的版本,皮神牛逼!

初代精华:151只像素小可爱,复古关都地图,奠定一切的玩法基石——咱们的第一个数字生态园!

  • 151只宝可梦,个个都是心头肉: 从最初的妙蛙种子到传说中的梦幻,每一只的设计灵感都来源于生活中的昆虫、动物,甚至还有神话传说。
  • 关都地区,梦开始的地方: 以日本的关东地区为蓝本,城镇的名字都跟颜色有关(比如枯叶市、彩虹市),很多地方都能找到现实中地标的影子。主线剧情会带你穿梭在森林、山洞、水路这些不同的生态环境中,还有些隐藏地点(比如华蓝洞窟、紫苑镇那个阴森森的宝可梦塔)等着你去探索。
  • 基础玩法,经典永流传:
    • 捕捉: 遇到野生的宝可梦,先把它打个半残,然后潇洒地扔出精灵球!能不能抓住,那得看脸,也看各种姿势对不对。
    • 培育: 带着你的宝可梦去战斗,赢了就能赚经验升级,还能学到新的招式。
    • 进化: 等级到了,或者用了特定的道具(比如雷之石),你的宝可梦就能“Duang”一下进化成更酷炫的形态!有些宝可梦还特别傲娇,非要通过联机交换才能进化(比如勇基拉进化成胡地),这可是开创了把线下社交和游戏进度绑定的先河!
    • 对战: 经典的回合制,15种属性互相克制(不过初代的时候平衡性嘛,只能说“尽力了”,比如超能系强得不像话,暴风雪技能100%冰冻对手,简直是耍赖!)。
    • 交换: 用联机线把小伙伴的Game Boy连起来,互相交换宝可梦,为了集齐图鉴或者让特定的宝可梦进化,这是最重要的社交环节!
    • 挑战联盟: 干掉8个道馆的馆主,拿到他们的徽章,最后去挑战牛逼轰轰的四天王和冠军,成为最强训练家!

剧情大概是这么个事儿:一个愣头青少年的冠军之路,顺便粉碎了火箭队的阴谋!

你扮演一个从真新镇(多么淳朴的名字!)出发的少年,从大木博士那里领到你的第一只宝可梦(《红/绿/蓝》版是三选一;《黄》版直接就是皮卡丘,没得选!)和一本空荡荡的图鉴。你的好基友(也是老对头)青绿(就是大木博士的孙子,关系户啊!)也跟你一起踏上了冒险之旅。你们的目标,就是成为宝可梦联盟的冠军,顺便帮博士把图鉴填满。一路上,你们还得跟一个叫做“火箭队”的邪恶组织斗智斗勇,他们老想利用宝可梦干坏事。最后,你还得干掉火箭队的老大坂木(这家伙居然还是常磐市的道馆馆主,隐藏得够深!)。通关之后,还有更刺激的挑战等着你——去华蓝洞窟抓那只传说中的、强到变态的宝可梦“超梦”(这背后其实也暗示了一些关于科学伦理的思考)。而那只神龙见首不见尾的“梦幻”,则象征着那些还未被人类驯服的神秘自然力量。

通关小秘籍(当年我们都是这么过来的):

  1. 开局选哪个?(红/绿/蓝版限定烦恼): 妙蛙种子(前期打道馆比较轻松),小火龙(后期进化成喷火龙帅炸,输出也高),杰尼龟(比较均衡,水箭龟也挺猛)。《黄》版没得选,就是皮卡丘,好好待它!早期在常磐森林可以抓只比比鸟(会飞,交通工具预定),尼多王/尼多后也是万金油打手,啥都能干。
  2. 道馆挑战(那些年被虐过的馆主): 尼比市的小刚(岩石系,被水和草克得死死的),枯叶市的马志士(电系,打赢他能拿到“冲浪”学习机,重要!),浅红市的阿桔(毒系,有点恶心),金黄市的娜姿(超能系,初代Bug级存在,打她之前你得先拿到“西尔佛检视镜”才能看见她的幽灵宝可梦)。
  3. 重要地点/剧情节点,别错过: 常磐森林(早期练级圣地),月见山(会遇到火箭队,还能二选一拿个化石,复活远古宝可梦),圣特安努号(船上能拿到“居合斩”学习机,砍树必备),宝可梦塔(在紫苑镇,气氛阴森,BGM更是经典中的经典,还有幽灵剧情),野生原野区(可以抓到很多稀有宝可梦,还能拿到“冲浪”学习机),双子岛/无人发电厂/冠军之路(这三个地方分别藏着三只传说中的神鸟,后面细说)。
  4. 传说宝可梦在哪抓? 急冻鸟(在双子岛的深处,迷宫有点绕),闪电鸟(在无人发电厂,要小心那些烦人的电磁波),火焰鸟(在冠军之路或者红莲岛的宝可梦屋里,版本不一样位置可能也不同)。超梦(通关之后才能去华蓝洞窟抓,70级的大佬,没点策略和耐心,外加疯狂SL大法,基本没戏)。至于梦幻嘛,正常途径是抓不到的,得利用一些特定的BUG才能搞到手,当年可是都市传说级别的存在!
  5. 战斗/培育小技巧,萌新必备: 属性克制一定要记牢,打不过就上状态异常(睡眠、麻痹、中毒轮番轰炸)。队伍搭配要合理,别全是攻击手。利用“学习装置”或者“幸运蛋”(如果有的话)来快速练级。彩虹市的百货大楼是购物天堂,啥都有。打重要Boss之前,记得存档!存档!存档!重要的事情说三遍!
  6. 著名的BUG,玩的就是心跳: MissingNo.(前面提过,可以复制道具,但有风险),梦幻BUG(通过一系列复杂的操作可以抓到梦幻,当年可是技术活儿!),等级BUG(利用NPC战斗的间隙,可以让你的宝可梦等级坐火箭一样升到100级,爽歪歪!)。
  7. 最终的考验:四天王与冠军,车轮战的噩梦! 科拿(冰系美女),希巴(格斗猛男),菊子(幽灵老太,打她还是需要西尔佛检视镜),阿渡(龙系大师,其实他队伍里飞龙系更多),最后就是你的老对手——冠军青绿(这家伙的队伍那叫一个强大,属性搭配也很多样,不好对付!)。

第二世代(1999-2002):金、银、水晶 —— 彩色时代的文艺复兴

如果说初代是开天辟地,那第二世代的《金、银、水晶》简直就是宝可梦系列的“文艺复兴”!不仅画面从黑白变成了彩色,更是在玩法和世界观上搞了个大升级。直到今天,还有无数老玩家把它奉为系列巅峰,心中的“白月光”,提起来就俩字:“完美!”

  • GBC上的彩色新纪元 (Game Boy Color平台):

    • 《金/银》:日本1999年11月21号率先发售,北美玩家2000年10月15号跟上,欧洲则是2001年4月6号。
    • 《水晶》(闪亮亮的加强版):日本2000年12月14号,北美2001年7月29号,欧洲2001年11月2日。
  • 为啥吹爆?因为它值得!

    • 口碑销量双炸裂:在初代的基础上进行了翻天覆地的扩展和优化,全球又卖疯了,高达2910万份!昼夜系统、生蛋机制、新的属性等等,一下子把可玩性和策略深度拉高了好几个档次,被很多老粉誉为“最完整的宝可梦体验”,没有之一!
    • 奠定系列基石,影响不是一点点
      • 成功地把宝可梦世界的魅力又提升了一个level,用实力证明了这个系列不是昙花一现,而是能打持久战的!
      • 引入的那些核心系统,比如生蛋、携带道具、性别、昼夜变化、新的恶系和钢系属性、还有亲密度,基本上都成了后续作品的标配,一直沿用至今。
      • 双地区设计(城都地区 + 初代的关都地区,地图面积直接翻了2.5倍!)。这也是系列里唯一一个可以让你完完整整地重游前作地图的世代。
      • 《水晶》版里可以选择女主角,宝可梦出场还有了动画。作为第一款彩色的、并且能跟前代联动的作品,《水晶》也给后来的重制版打了个样板。

版本有啥不同?

  • 《金/银》版本差异: 老规矩,主要还是版本限定的宝可梦不一样(比如金版才能抓到凤王,银版则是洛奇亚,大概有12%的宝可梦是版本限定),继续逼你跟小伙伴交换!
  • 第二代核心新增内容
    • 彩色画面:得益于GBC的机能提升,游戏终于从黑白变成了彩色。
    • 时间流逝系统(白天黑夜,周一到周日,安排得明明白白!): 游戏里有了真实的时间流逝!白天、黄昏、夜晚的背景会变,NPC的活动、商店的营业时间、特定事件的触发,甚至某些宝可梦的出现和进化(比如伊布要进化成太阳伊布还是月亮伊布,就看是白天还是晚上了),都跟时间挂钩了!
    • 培育系统,孵蛋带娃一条龙!:引入了宝可梦生蛋机制!只要是同蛋群的宝可梦(或者找万能的百变怪帮忙),就能生出小宝宝,还能遗传老爸老妈的招式和一部分个体值(IV),运气好的话,还能孵出颜色不一样的闪光宝可梦(虽然概率低到令人发指)!给宝可梦带上“不变之石”,还能遗传性格呢!
    • 携带道具,打架更有套路!:宝可梦终于可以带东西上战场了!这些道具可以在战斗中发挥各种效果,或者产生其他神奇的作用(比如各种效果的树果系统,就是从这代开始登场的)。
    • 新属性登场(恶系 & 钢系),平衡性调整!:为了平衡初代过于强势的超能系、幽灵系等属性,新增了恶系和钢系,属性克制关系一下子复杂了不少,也更有意思了。
    • 进化方式五花八门!:除了传统的等级进化和道具进化,还新增了亲密度进化(比如伊布、波克比,你得对它好,它才肯进化)、特定时间进化、带着特定道具通讯进化(比如呆呆兽带着王者之证交换就能进化成呆呆王)等等,花样百出!
    • 新式精灵球,抓宠更有针对性!:可以用不同颜色的球果制作出各种有特殊效果的精灵球(比如诱饵球更容易钓到水里的宝可梦)。
    • “宝可装置”(Pokégear),高科技腕表!:这玩意儿可厉害了,集成了地图、电话(可以跟打败过的NPC约架,或者获取稀有宝可梦的情报)、收音机(有些频道还有任务提示)等多种功能,简直是训练家出门必备神器!
    • 性别系统,终于分公母了!:大部分宝可梦都有了性别之分,这不仅影响培育(同种宝可梦一公一母才能生蛋),有些招式也跟性别有关。
    • “特殊”能力细分,更科学了!:初代笼统的“特殊”能力值,在这代被细分成了“特殊攻击”和“特殊防御”,招式也开始区分是物理攻击还是特殊攻击了(虽然还是雏形阶段)。
    • 闪光宝可梦,blingbling惹人爱!:首次正式出现了颜色跟普通宝可梦不一样的“闪光宝可梦”!在野外遇到的概率极低(1/8192,比中彩票还难!),不过在愤怒之湖,剧情会必定让你遇到一只红色的闪光暴鲤龙。
    • 宝可病毒,是福不是祸!:你的宝可梦有极小的概率会感染一种叫做“宝可病毒”的东西(概率大概是3/65536,比闪光还稀有!),别慌,这可是个好东西!感染了宝可病毒的宝可梦,在战斗中获得的努力值会翻倍,练级神Buff!
  • 《水晶》版又加了啥料?更香了!
    • 终于可以选择女主角“克丽丝”了! 女玩家狂喜!
    • 宝可梦战斗登场的时候有了专属动画! 更酷炫了!
    • 对战塔,挑战你的极限!:这里有等级限制,你需要连续打赢7场对战,赢了就能拿到营养饮料。
    • 水君专属剧情线,神兽更有排面!:你需要追踪传说中的水君,跟一个神秘的NPC“水京”互动,最终可以在圆朱市的铃铛塔固定捕捉到它。
    • 定点教学招式,学到就是赚到!:有些特定的NPC会教你的宝可梦一些强力招式,可别错过了!
    • 手机上网功能(日本版专属的Mobile System GB,可惜现在已经停服了): 当年可是黑科技!支持联网交换、对战,还能接收官方活动送的蛋(比如著名的GS球,可以触发捕捉时拉比的剧情)。
    • 地图、天气效果优化,导航也改进了! 细节更到位!
    • 部分宝可梦的出现率和捕捉方式也做了调整。
    • 时间胶囊系统,穿越时空的联动!:可以通过这个系统,把第一世代的宝可梦传送到第二世代来,老伙计又能并肩作战了!

第二代特色:100只新伙伴,和风满满的城都,日月星辰的轮转,生儿育女的乐趣——让宝可梦世界真正“活”了起来!

  • 100种新宝可梦,阵容大扩充: 编号从152到251,总数一下子达到了251只!包括了新的御三家(菊草叶、火球鼠、小锯鳄),传说中的凤王、洛奇亚、三圣兽,还有幻之宝可梦时拉比。设计上融入了更多东方神话元素和现代科技感,还引入了“进化分支”的概念(比如伊布在这代又多了两种进化形态)。
  • 城都地区(Johto),关西风情画卷: 以日本的关西地区为原型,到处都充满了日式传统文化的气息(比如圆朱市那古色古香的烧焦塔和铃铛塔)。最牛的是,通关城都地区的剧情后,你居然可以坐船回到第一世代的关都地区,挑战三年后的道馆馆主们!最后还能在白银山跟前作的主角赤红来一场史诗级的对决!双地图的总面积是初代的2.5倍,各种隐藏要素多到让你眼花缭乱(比如阿露福遗迹里那些神秘的未知图腾)。
  • 昼夜与星期系统,世界不再一成不变: 宝可梦的行动、NPC的活动、场景的互动、甚至进化条件都跟时间和星期挂钩了,让整个世界感觉更加动态和真实。
  • 培育系统,深度玩家的狂欢: 极大地丰富了游戏的玩法,你可以通过生蛋来培育出拥有特定招式或者能力更强的宝可梦。万能的百变怪成了培育的核心工具。亲密度不仅影响某些招式的威力,还是某些宝可梦进化的关键。《水晶》版还首次引入了25种不同的性格,这些性格会影响宝可梦的能力值成长,让每一只宝可梦都更加独一无二。

剧情大概是这么个事儿:跨越时空的救赎与传承,新的少年再次踏上征途!

故事发生在一代剧情结束的三年后,你扮演一个从若叶镇出发的新人训练家。一开始,你的任务是追捕那个偷走了空木博士宝可梦的红发劲敌(这家伙性格可不怎么好)。然后,你就要开始挑战城都地区的八个道馆了。一路上,你还得粉碎火箭队残余势力的复兴阴谋(他们甚至想利用广播塔来联络失踪已久的老大坂木!),并且会邂逅传说中的宝可梦(比如凤王或洛奇亚,还有在城都地区四处游荡的三圣兽)。当你击败了城都地区的四天王和冠军阿渡之后,别以为就结束了!你还可以前往第一世代的关都地区,挑战那里的八个道馆。当你集齐了全部16个徽章后,就可以前往白银山,在山顶挑战那个传说中的、前代游戏的主角——赤红!这场对决,象征着两代训练家之间的传承与超越。

通关小秘籍(二代玩家的智慧结晶):

  1. 开局选哪个?新的烦恼又来了: 菊草叶(早期比较肉,能抗),火球鼠(输出高,进化成火暴兽很猛),小锯鳄(比较均衡,大力鳄也很强力)。早期在野外可以抓到像赫拉克罗斯(格斗/虫,强力打手)、快泳蛙(水/格斗,也很实用)这样的好帮手。
  2. 城都道馆(那些让人头疼的家伙): 桧皮镇的阿笔(虫系,打他之前得先去呆呆兽之井解决火箭队的麻烦),满金市的小茜(普通系,她那只滚来滚去的大奶罐是多少人的噩梦!打她之前,记得去地下通道逛逛,能拿到王者之证,还要完成电台塔的事件),圆朱市的小松(幽灵系,有点难缠),卡吉镇的柳伯(冰系,打他之前要先去愤怒之湖解决红色暴鲤龙的事件,打赢他能拿到“潮旋”秘传学习器)。
  3. 神兽在哪抓?一个都不能少! 红色暴鲤龙(在愤怒之湖,剧情必定会让你抓到,而且还是闪光的!)。三圣兽(雷公、炎帝、水君,在烧焦塔事件发生后,它们就会在城都地区的草丛里随机游荡了,想抓住它们得用“黑色目光”这样的技能防止它们逃跑。《水晶》版里水君有固定的剧情,更容易抓到)。凤王/洛奇亚(金版需要集齐虹色之羽后去铃铛塔顶层/银版则需要带着银色之羽去漩涡列岛的深处)。时拉比(这可是幻之宝可梦,通常需要通过官方活动才能获得,或者在《水晶》版里通过GS球事件触发剧情捕捉)。
  4. 培育进阶,肝帝的自我修养: 利用拥有“火焰之躯”特性的宝可梦(比如熔岩蜗牛)来加速孵蛋。通过走路、喂食、给宝可梦按摩等方式提升亲密度。没事就去对战塔挑战一下,练练手。利用“幸运蛋”或者“学习装置”来快速练级。
  5. 关都地区与最终的史诗对决: 通关城都地区的剧情后,就可以前往关都地区挑战那里的道馆了。当你集齐16个徽章后,就可以去白银山挑战赤红了!他的队伍可是相当强悍,等级也很高,包含了皮卡丘、喷火龙这些经典的老伙计,绝对是一场硬仗!

GBA时代,宝可梦的“文艺复兴”Plus!第三世代(2002-2006):红宝石、蓝宝石、绿宝石、火红、叶绿

进入GBA时代,宝可梦系列简直是打了鸡血一样,画面和玩法都来了个双重飞跃!咱们正式从“傻乎乎收集养成”进化到了需要动脑筋的“战术对抗”新纪元!这一代被很多玩家誉为“宝可梦的文艺复兴”(虽然二代也被这么叫过,但三代更“复兴”!),宝可梦的总数也一下子扩展到了386种,钱包君,你准备好了吗?

  • GBA上的视觉盛宴与玩法革新:

    • 《红宝石/蓝宝石》:日本2002年11月21日首发,北美则是2003年3月19日。
    • 《火红/叶绿》(初代高清重制,情怀满满!):日本2004年1月29日,北美2004年9月9日。
    • 《绿宝石》(红蓝宝石的究极加强版):日本2004年9月16日,北美2005年5月1日。
  • 口碑与影响?那是相当炸裂!

    • 画面和玩法齐飞:GBA的机能让画面提升了一大截,色彩更鲜艳,细节更丰富。“特性”和“双打对战”这两个新玩意儿,直接把对战系统给颠覆了,变得超级有策略性!全球销量超过2100万份,妥妥的!特别是《绿宝石》,因为内容超级丰富,还有那个让人肝到天昏地暗的“对战开拓区”,简直是好评如潮!不过嘛,初期因为不能跟前两代的游戏直接联机,图鉴也不全,也引起了一些老玩家的小小不满(不过后来通过《火红/叶绿》这些给补上了)。
    • 奠定现代对战的基石,影响老深远了!
      • “特性”这玩意儿,成了对战的核心要素,让每一只宝可梦都变得独一无二,不再只是看种族值和技能了。
      • “双打对战”模式一出来,就成了官方比赛的主流,策略性、观赏性直接拉满!
      • “性格”系统让宝可梦的个体差异化培养更加深入,想培养出完美打手?有的你研究了!
      • 《火红/叶绿》开创了“官方高清重制”的先河,而且通过“时间胶囊”实现了跟同代作品的数据互通,老伙计又能穿越了!
      • 那个“对战开拓区”,给通关后的玩家提供了极具挑战性的内容,让你刷到停不下来!
      • 从探讨生态环保的主题,到深入挖掘策略深度,再到把经典用新技术重新演绎一遍,第三世代通过技术和玩法的双重创新,把宝可梦的魅力又巩固和拓展了一大圈!

主要游戏版本和重制版,个个都经典!

  • 《红宝石/蓝宝石》: 咱们的冒险舞台换到了全新的“丰缘地区”,这里充满了热带岛屿风情,有13个城镇,还有复杂得让人想哭的水路(比如琉璃市那个海流谜题,当年绕晕了多少人!)。红宝石版的封面神兽是地面系的固拉多,反派是熔岩队,想让陆地变多;蓝宝石版的封面神兽是水系的盖欧卡,反派是海洋队,想让海洋变多。老规矩,不同版本有各自限定的宝可梦。这一代开始支持GBA的无线适配器了,可以进行双打对战和交换,也是第一次支持跨版本联机合作打Boss!
  • 《火红/叶绿》: 初代《红/绿》的重制版,也是宝可梦系列第一次搞官方重制!用了《红蓝宝石》改良过的引擎,把关都地区重新打造了一遍,还加入了一些昼夜循环和动态天气效果,场景细节也丰富了不少。更重要的是,把第三代的特性、性格系统和新的技能教学都加了进去,属性平衡也做了优化。还新增了“七岛”冒险剧情,里面有火箭队残党的搞事,还能抓到一些第二代和丰缘地区的宝可梦,实现了跨代联动!版本限定宝可梦?那必须有啊!
  • 《绿宝石》: 《红蓝宝石》的资料片,或者说是“终极威力加强版”,用了《火红/叶绿》的引擎。这次,熔岩队和海洋队同时出来搞事情,玩家得去唤醒传说中的烈空坐,才能平息固拉多和盖欧卡的世纪大战。还新增了水都兄妹(拉帝亚斯/拉帝欧斯)在丰缘地区随机游荡的剧情。天气效果、秘密基地的装饰、华丽大赛(新增了大师级别)都做了完善,道馆和地图细节也调整了,通关后你甚至可以同时抓到固拉多和盖欧卡,爽!
  • 后来的重制,情怀永不落幕: 《欧米伽红宝石/阿尔法蓝宝石》(2014年在3DS上推出),用了《X/Y》的3D引擎,画面更上一层楼!

第三代特色:135只新面孔,热带风情丰缘,双人开黑对战,特性与性格系统——奠定系列深度与竞技性的里程碑!

  • 135只新宝可梦,阵容再壮大: 编号从#252的木守宫一直到#386的代欧奇希斯。引入了新的钢系宝可梦(比如帅炸的巨金怪)和恶系宝可梦(比如酷酷的阿勃梭鲁),还有一些跟天气有特殊关系的宝可梦(比如大嘴鸥的“降雨”特性),进一步平衡了属性克制关系。固拉多和盖欧卡的海陆大战、烈空坐从天而降平息纷争、还有解谜才能抓到的三神柱,都成了经典剧情。幻之宝可梦(比如时拉比、基拉祈)还是老样子,得通过线下的官方活动限量发放,想要?看缘分吧!
  • 丰缘地区(Hoenn Region),全新的冒险舞台: 这里有火山、沙漠、海洋、热带雨林等各种各样的地貌,原型是日本的九州和冲绳。琉璃市那个让人抓狂的海流谜题、觉醒神殿里那些需要特定宝可梦才能打开的岩石机关,都大大提升了探索的深度和乐趣。
  • 双人对战(Double Battles),2v2开黑更刺激!:首次引入了2对2的战斗模式!这下可热闹了,有些招式的攻击范围也做了调整(比如“地震”会打到自己队友,得小心!),策略复杂度直接翻倍!经典的组合比如暴鲤龙(出场“威吓”降低对手攻击)配上钻角犀兽(“避雷针”特性吸引电系攻击保护队友),玩的就是联防和配合!
  • 特性(Abilities)与性格(Natures),你的宝可梦独一无二!
    • 特性: 每只宝可梦都有至少一种特性(总共有77种!),这些特性会自动生效,可以增强自己、削弱对手、改变天气或场地,甚至影响状态,一下子让对战充满了变数!特性之间的联动(比如巨沼怪的“湿气”特性可以防止对手自爆)和后来出现的隐藏特性(比如蚊香泳士的“轻快”特性在雨天速度翻倍),更是增加了策略的深度。
    • 性格: 每只宝可梦天生就有一种性格,这个性格会影响它的能力值成长(通常是一项能力提升10%,另一项降低10%)。再结合个体值(IV)和努力值(EV),你就可以精细化地培养出专属于你的强力打手了!
  • 宝可梦华丽大赛(Pokémon Contests),不打架也能当明星!:除了打打杀杀,你还可以带着你的宝可梦去参加华丽大赛,展示它们的魅力!比赛会按照帅气、美丽、可爱这些不同的项目来评分。
  • 秘密基地(Secret Base),打造你的专属小窝!:你可以在地图上找到一些特定的地点,创建并装饰自己的秘密基地,还能邀请NPC或者其他玩家(通过联机)来参观,算是早期的社交雏形了。有些家具还能帮助你的宝可梦练级呢!
  • 天气博弈,看天吃饭的对战!:新增了冰雹天气,跟之前的沙暴、晴天、雨天一起构成了四大天气体系。传说中的烈空坐还有个“空气锁”特性,可以直接封锁所有的天气效果,霸道!
  • 树果系统优化,数据还能继承(时间胶囊): 可以把第一世代的宝可梦传送到第三世代来,老朋友又能见面了!

剧情大概是这么个事儿:生态危机,跨时空冒险,还有经典的英雄救世!

  • 《红宝石/蓝宝石/绿宝石》: 你扮演一个刚刚搬到丰缘地区未白镇的少年/少女,目标还是挑战道馆和联盟。结果呢,遇上了两个神经兮兮的组织:熔岩队(在红宝石版里)想利用固拉多的力量扩张陆地,海洋队(在蓝宝石版里)则想利用盖欧卡的力量扩张海洋。结果搞得气候失衡,世界大乱。
    • 在《绿宝石》里,这两个组织更是同时搞事情,你得去天空之柱唤醒沉睡的烈空坐,让它用一招“画龙点睛”平息固拉多和盖欧卡的争斗,恢复自然平衡。整个故事都在探讨人与自然和谐共处这个深刻的主题。通关后还新增了一个叫做“Delta篇章”的剧情,引入了平行宇宙的设定,脑洞大开!对战开拓区的剧情也跟世界观有些联系。
  • 《火红/叶绿》: 剧情基本上还是沿袭了初代,你从真新镇出发,目标是成为冠军,顺便粉碎火箭队的阴谋。新增的“七岛”冒险剧情里,你需要协助四天王之一的科拿对抗火箭队的残余势力,还要找回红蓝宝石,这样才能跟《红蓝绿宝石》联动,最终目标还是收集所有的宝可梦。阿露福遗迹里还能抓到那些神秘的未知图腾。

通关小秘籍(三代老司机的经验之谈):

  1. 开局选哪个?丰缘御三家,各有千秋: 木守宫(草系,速度快),火稚鸡(火系,进化后是火/格斗,输出暴力),水跃鱼(水/地面,弱点少,巨沼怪非常强力)。在《火红/叶绿》里还是关都御三家。早期可以抓到像傲骨燕(飞行系,早期好用)、拉鲁拉丝(超能/妖精,沙奈朵颜值与实力并存)、泥泥鳅(水/地面,进化成鲶鱼王也不错)这些。
  2. 丰缘道馆(那些印象深刻的馆主): 卡那兹市的杜鹃(岩石系),紫堇市的铁旋(电系,打他之前你得先拿到自行车),茵郁市的娜琪(飞行系,打赢她能拿到“飞空”秘传学习器,终于可以到处飞了!)。
  3. 神兽在哪抓?一个都不能放过! 三神柱(雷吉洛克、雷吉艾斯、雷吉斯奇鲁,在丰缘地区的特定地点,需要解开复杂的谜题,还得带上古空棘鱼和吼鲸王这两只特定的宝可梦才能触发机关)。水都兄妹(拉帝亚斯/拉帝欧斯,在《绿宝石》里通关后会在丰缘地区随机游荡,看你运气了)。超梦和三圣鸟(在《火红/叶绿》里通关后,可以在七岛的剧情中抓到)。代欧奇希斯(在诞生之岛,需要解开一个三角形的谜题,还得有官方活动的船票才能去)。时拉比、基拉祈(幻之宝可梦,老规矩,活动限定,一般人拿不到)。
  4. 培育进阶,打造你的梦之队! 利用等级、道具、特殊条件让宝可梦进化。带上学习装置、幸运蛋快速练级。合理搭配技能,别忘了属性克制。可以尝试组建天气队(比如雨天队、沙暴队)。掌握双打对战的联防和配合技巧。规划好努力值的分配(比如经典的“252大法”,把努力值集中加在两项关键能力上)。
  5. 通关后干啥?肝就完事了! 挑战丰缘地区的四天王和冠军(大吾或者米可利,看版本)。《绿宝石》里有那个让人欲罢不能的对战开拓区,挑战里面的开拓头脑,奖励丰厚!《火红/叶绿》里有七岛的后续剧情,还能抓到城都地区的三圣兽呢!

NDS时代,双屏新体验,第四世代(2006-2010):钻石、珍珠、白金、心金、魂银

当任天堂的双屏掌机 NDS 横空出世,宝可梦系列也迎来了又一个黄金时代!第四世代不仅给我们带来了一个全新的大陆去冒险,还把经典的第二世代进行了堪称完美的复刻。更重要的是,它搞了个革命性的改革——“物理招式和特殊招式彻底分家”,直接为现代宝可梦电子竞技打下了坚实的基础!宝可梦的总数也扩充到了493种,收集党的噩梦(也是天堂)!

  • NDS上的双倍快乐 (Nintendo DS平台):

    • 《钻石/珍珠》:日本2006年9月28日,北美2007年4月22日。
    • 《白金》(钻石珍珠的究极无敌豪华加强版):日本2008年9月13日,北美2009年3月22日。
    • 《心金/魂银》(金银的完美复刻,吹爆!):日本2009年9月12日,北美2010年3月14日。
  • 口碑炸裂!核心改动人人夸!

    • “物理/特殊招式分家”这个改动,简直是神来之笔!一下子让对战的策略性和平衡性提升了好几个档次,玩家们纷纷点赞(《钻石/珍珠》在Metacritic上拿了85分,《心金/魂银》更是高达87分,牛逼!)。Wi-Fi功能的加入,让全世界的玩家都能一起玩耍,交换宝可梦、在线对战,不亦乐乎!《白金》版修复了《钻石/珍珠》早期版本节奏有点慢的问题,还加了不少新内容,评价自然更高。《心金/魂银》因为内容超级丰富,既忠实还原了原作的精髓,又在很多方面超越了原作,被誉为系列最强复刻之一,至今无人能敌!3D化的场景尝试也得到了大家的认可。
    • 奠定现代宝可梦的基石,影响深远到没边儿!
      • “物特分家”绝对是宝可梦对战史上最重要的变革之一,没有它,就没有后来的所有世代,更别提什么电子竞技了!
      • Wi-Fi功能的全面应用,真正把宝可梦的交换和对战推向了全球化。
      • 《心金/魂银》给后来的复刻游戏树立了一个几乎不可能被超越的标杆。
      • 这一代还诞生了像路卡利欧、烈咬陆鲨这些帅气又强力,至今仍在对战中叱咤风云的高人气宝可梦。也推动了系列向3D化的转型。全球销量超过2470万份,实力说明一切!

主要游戏版本和重制版,个个都是精品中的精品!

  • 《钻石/珍珠》: 第四世代的开山之作,第一次登陆NDS平台,用了3D贴图技术,让场景看起来更有立体感了。冒险的舞台换到了全新的“神奥地区”。钻石版的封面神兽是钢/龙属性的帝牙卢卡(时间之神),珍珠版的封面神兽是水/龙属性的帕路奇亚(空间之神)。老规矩,版本限定的宝可梦各有不同(比如钻石版能抓到可爱的圆陆鲨,珍珠版才有帅气的泳气鼬)。
  • 《白金》: 《钻石/珍珠》的强化版,也是很多玩家心中的“神奥完全体”。封面宝可梦换成了鬼/龙属性的骑拉帝纳(反转世界之王),它在剧情里的地位也大大提升。新增了好多剧情(比如那个物理规则都跟现实世界不一样的“毁坏的世界/反转世界”,酷毙了!)、新的角色(比如帅气的国际刑警帅哥)、还有那个让人肝到爆的“对战开拓区”(美版叫对战边疆)。主角和劲敌的衣服也换成了冬装款式,细节满满。神奥地区的图鉴也扩充了不少。
  • 《心金/魂银》: 第二代《金/银》的重制版,前面说了,系列评价最高的复刻之一,没有争议!原汁原味地重制了城都和关都两个大地图,还融入了《水晶》版的一些剧情(比如水君的故事线),并且把第四代的特性系统和各种创新玩法都加了进来。心金版的封面是凤王,魂银版的封面是洛奇亚(主线剧情会先遇到对应版本的封面神兽,后期也能抓到另一只,良心!)。还新增了所有宝可梦都能跟在主角屁股后面跑的系统(萌出血!)、新的小游戏设施“宝可梦全能竞技赛”、以及一个捆绑销售的配件“宝可计步器”(走路就能养宝可梦,黑科技!)。

第四世代特色:107只新丁,冰天雪地的神奥大冒险,物特分家闹革命,全球网友一线牵——战斗和生态环境全面升级!

  1. 107只新宝可梦,老伙计也能焕发第二春: 总数达到了493只!其中有29种是以前那些老宝可梦的新进化形态(比如天蝎进化成天蝎王,电击兽进化成电击魔兽),一下子让队伍的选择丰富了不少。传说中的创世神阿尔宙斯(编号#493,带着不同的石板就能切换属性,逼格满满!)也是在这一代首次登场的。还有像准神级别的烈咬陆鲨(龙/地面属性,实力强到犯规!)、帅气的路卡利欧(格斗/钢属性,能使用波导之力),都是这一代备受关注的热门宝可梦。
  2. 神奥地区(Sinnoh Region),充满神话色彩的立体世界与多样地貌: 原型是日本的北海道,中间被一座叫做“天冠山”的巨大山脉分割开来,导致东西两边的气候截然不同。这里有美丽的湖泊(三圣菇就住在这里)、广阔的雪原、泥泞的湿地等等。
    • 地下通路(Underground),挖宝探险新乐趣: 你可以在地下挖掘化石、各种珍贵的宝玉,还能布置自己的秘密基地,跟朋友联机玩各种小游戏,算是早期的沙盒玩法雏形了。
    • 后来的《宝可梦传说 阿尔宙斯》的故事,就发生在这个地区的古代形态——洗翠地区。
  3. 划时代大事件:“拳脚功夫”和“魔法攻击”终于分家啦! (Physical/Special Split) 这可是战斗系统的一场大革命!:攻击招式被明确地分成了“物理”或者“特殊”两类,不再像以前那样主要由招式的属性来决定了(比如以前火系招式大多算特殊攻击,格斗系招式大多算物理攻击)。从这一代开始,每个攻击招式都有自己独立的分类!比如说,格斗系的“劈瓦”那是实打实的物理拳脚,但同样是格斗系的“波导弹”,那就是biubiubiu的特殊气功波!这样做的好处?那可太大了!宝可梦的攻击、特攻、防御、特防这些能力值终于能派上真正的用场了,配招也一下子从“哪个威力大用哪个”变成了需要深思熟虑的策略活儿。对战的公平性、趣味性和深度,直接原地起飞!
  4. 在线交易与对战(Wi-Fi Connection),全世界玩家手拉手!:《钻石/珍珠》是第一批支持通过互联网进行全球交换和对战的主系列游戏!
    • 全球贸易站(Global Trade Station, GTS): 你可以把自己想交换的宝可梦上传到GTS,设定好你想要的交换条件,或者去搜索别人发布的交换请求,实现了跨地区、匿名的宝可梦交换。
    • Wi-Fi对战: 支持单打和双打模式,还引入了随机匹配对战和跟好友约战的功能。
    • 时间胶囊,三代四代一家亲: 通过NDS的GBA插槽,可以把第三代游戏的宝可梦数据传到第四代来。
    • 官方也从这一代开始,正式举办起了官方的电子竞技赛事——宝可梦世界锦标赛(Pokémon World Championships, VGC)(从2009年开始),采用的是“神战规则”(就是允许使用一些传说中的神兽),推动了宝可梦对战向职业化发展。
  5. 其他重要特性与系统深化,细节决定成败:
    • 特性和道具再强化: 新增了77种特性(比如“沙之力”、“引火”这些),还有各种新的强力道具(比如“生命宝珠”、“气势披带”),让特性之间的组合和道具的选择博弈更加重要。
    • 昼夜系统回归并且强化了。 世界更加生动。
    • NDS双屏大显神通: 下屏可以显示各种辅助信息(比如那个功能超多的“宝可梦手表Pokétch”,里面有计步器、计算器等各种实用小应用)、战斗指令菜单等等,大大提升了操作的便捷性。
    • 宝可梦超级华丽大赛(《钻石/珍珠/白金》): 分成了视觉、舞蹈、演技三个阶段来评审,想当明星也不容易啊!
    • 宝可梦跟随系统(《心金/魂银》专属福利!): 队伍里的第一只宝可梦可以跟在主角屁股后面跑!这个设计简直太棒了,代入感和互动性爆棚!
    • 宝可梦全能竞技赛(Pokéathlon,《心金/魂银》): 有各种各样的小项目,考验你宝可梦的综合能力,比如速度、力量、技巧等等。
    • 宝可计步器(Pokéwalker,《心金/魂银》捆绑配件): 把这个小玩意儿带在身上,你现实中走的步数可以给计步器里的宝可梦赚取经验、道具,甚至还能抓到一些特殊的宝可梦!运动健身两不误!
    • 神奇齿轮(Pokégear,《心金/魂银》)也升级了: 里面有地图、电话(可以跟打败过的NPC再约一架,或者获取商店打折的情报)等功能,构建了一个虚实结合的社交体验。

剧情大概是这么个事儿:神话与科技的激烈碰撞,经典与创新的完美交响!

  • 《钻石/珍珠/白金》:神奥地区的时空创世大危机!:你扮演一个从双叶镇出发的新人训练家,跟你的劲敌一起,从山梨博士那里领到了初始伙伴(草苗龟、小火焰猴、波加曼三选一),然后就踏上了收集图鉴和挑战冠军之路。一路上,你们会遇到一个叫做“银河队”的神秘组织,他们的老大赤日野心勃勃,想利用传说中的帝牙卢卡(时间之神)或者帕路奇亚(空间之神)来创造一个所谓的“完美新宇宙”。你需要借助三圣菇(由克希、艾姆利多、亚克诺姆,分别象征知识、情感和意志)的力量,在天冠山顶的枪之柱阻止银河队的阴谋。
    • 在《白金》版里,骑拉帝纳(反物质世界之王)的剧情地位大大提升,还引入了一个叫做“毁坏的世界/反转世界”的神秘空间(里面的重力是颠倒的,迷宫设计很有意思!),骑拉帝纳在这个世界里会以它的起源形态登场。你需要更深入地调查银河队的阴谋,还会遇到国际刑警帅哥和银河队的新科学家冥王。故事的高潮是帝牙卢卡和帕路奇亚在枪之柱发生激烈碰撞,导致维度撕裂,你会被卷入毁坏的世界,挑战骑拉帝纳,最终调和时空之间的矛盾。创世神阿尔宙斯的神话贯穿了整个故事,如果你能集齐所有的石板,就能揭示创世的记忆,最终在神都遗迹(需要特定条件才能触发)面临“神权”与“人性”的抉择。
  • 《心金/魂银》:重温城都与关都的经典之旅,还新增了神话篇章!:忠实地再现了《金/银》的剧情,你从若叶镇出发,挑战城都地区的八个道馆,粉碎火箭队的复辟阴谋。包含了与凤王或洛奇亚相遇的剧情,还有强化版的水君追寻故事。通关城都地区后,前往关都地区挑战那里的八个道馆,最终在白银山顶挑战那个男人——赤红(他的皮卡丘等级高达88级,恐怖如斯!)。还新增了桐树林时拉比穿越时空的事件(揭示了你的劲敌和火箭队老大坂木之间不为人知的往事,不过需要官方活动送的宝可梦才能触发)、神都遗迹事件(如果你带着官方活动送的阿尔宙斯来到这里,就能获得帝牙卢卡、帕路奇亚、骑拉帝纳中的一只,三选一!)。

通关小秘籍(四代高手的压箱底绝活):

  1. 开局选哪个?神奥御三家,各有特色: 草苗龟(最终进化是草/地面,弱点比较多但打击面广),小火焰猴(最终进化是火/格斗,输出暴力,人气很高),波加曼(最终进化是水/钢,抗性优秀)。在《心金/魂银》里还是城都御三家。早期可以抓到像姆克儿(进化成姆克鹰后是强力飞行系打手)、拉鲁拉丝(沙奈朵和艾路雷朵都是好用的超能系)、圆陆鲨(在《白金》版里早期就能获得,进化成烈咬陆鲨后就是神挡杀神佛挡杀佛的存在!)这些。工具宠方面,大尾狸因为能学很多秘传学习器而备受青睐,热带龙也是不错的飞天和清雾工具人。
  2. 神奥道馆(那些让你卡关的馆主): 百代市的菜种(草系,她的罗丝雷朵有点东西),缘之道馆的梅丽莎(幽灵系,她的梦妖魔也很难缠),水脉市的东瓜(钢系,防御超高),切锋市的小菘(冰系,她的暴雪王会开暴风雪天气,很烦)。
  3. 神兽在哪抓?一个都不能少!(部分列举): 帝牙卢卡/帕路奇亚/骑拉帝纳(这仨是《钻/珍/白》剧情的核心,肯定能抓到)。三圣菇(由克希、艾姆利多、亚克诺姆,分别在神奥地区的三个湖里,其中艾姆利多会到处跑,比较难抓)。席多蓝恩(在严酷山深处)。雷吉奇嘉斯(需要通过GBA联动把前代的三神柱传过来,才能在切锋市的神殿唤醒它)。克雷色利亚(在美央市完成剧情后会在神奥地区随机游荡,是只漂亮的月神)。凤王/洛奇亚(《心/魂》剧情的核心,肯定能遇到)。三圣兽(雷公、炎帝、水君,在《心/魂》里烧焦塔事件后会在城都地区随机游荡,水君有固定的剧情线)。阿尔宙斯(通常需要官方活动送的道具“天界之笛”才能在枪之柱遇到,或者通过《心/魂》的神都遗迹事件获得)。达克莱伊(噩梦神,在新月岛,也需要官方活动送的道具才能触发事件)。红色暴鲤龙(《心/魂》愤怒之湖剧情必抓,闪光福利!)。
  4. 培育进阶,从萌新到大神: 活用“物特分家”的原则来配招,合理规划努力值(EVs)和个体值(IVs)的培养。利用拥有“火焰之躯”等特性的宝可梦来加速孵蛋。多去地下通路挖宝,能获得不少好东西。通关后去挑战对战开拓区或者对战边疆,那里才是真正检验你实力的地方!
  5. BUG与小技巧,前人栽树后人乘凉: 早期的版本曾经出现过一些利用GTS复制道具的BUG,还有透视蛋的BUG(可以看到蛋里是什么宝可梦),不过这些后来都被修复了。在《心/魂》的满金市百货大楼里可以买到各种有用的战斗道具,别错过了!

NDS末期的大胆革新之作!第五世代(2010-2013):黑、白、黑2、白2 —— 剧情深刻引人深思,宝可梦也开始探讨哲学了!

在NDS掌机生命周期的尾声,第五世代的《黑、白》以及它们的续作《黑2、白2》横空出世,以其极其大胆的剧情(是的,你没看错,宝可梦的剧情也能深刻!)和机制上的革新,成为了宝可梦系列中最具“想法”、也最具争议的一代。甚至可以说,这是系列从“给小孩子玩的”开始向“引发成人化思考”转折的一个重要节点。

  • NDS上的最后绝唱 (Nintendo DS平台):

    • 《黑/白》:日本2010年9月18日,北美2011年3月6日。
    • 《黑2/白2》(正统续作,不是加强版哦!):日本2012年6月23日,北美2012年10月7日。
  • 评价与影响?争议与深度齐飞,有人爱死有人愁!

    • 口碑两极分化,但深度广受好评:因为一些非常大胆的革新(比如通关前你几乎见不到一只老宝可梦,全是新面孔!还有些新宝可梦的设计风格也挺独特的,比如那个垃圾袋成精的……),以及极其深刻、甚至有点黑暗的剧情(深入探讨了宝可梦与人类之间的关系,还成功塑造了像N这样极具魅力的反派角色),导致玩家评价两极分化严重。不过,Metacritic评分还是挺高的,《黑/白》拿了87分,《黑2/白2》也有80分。《黑2/白2》作为系列首次的正统剧情续作,在内容和可玩性上都有了很大的提升,获得了普遍的好评。
    • 开创了系列叙事的新高度,影响不是一般的大!
      • 通关前几乎全是新宝可梦的设计,给玩家带来了一种前所未有的、纯粹的探索体验,仿佛真的来到了一个全新的世界。
      • 剧情和角色塑造都比以往更加成熟和复杂,甚至把宝可梦系列提升到了“引人深思的社会寓言”这样的高度,让人刮目相看!
      • 首次推出了正统的剧情续作,扩展了世界观,开创了系列叙事的新纪元。
      • 那个叫做“宝可梦世界锦标赛(PWT)”的设施,能让你跟历代游戏里的道馆馆主和冠军们对战,简直是情怀党的狂欢盛宴!
      • 销量也相当不错,超过了1700万份。三人对战和各种特性组合的策略性,也为后续的作品奠定了基础。

游戏版本与独特的续作模式:合众地区的风云变幻!

  • 《黑/白》:颠覆传统的全新冒险,让你眼前一亮(或一黑)!:彻底打破了以往“资料片”的传统,展现出了强烈的独立性和创新精神。黑版的封面神兽是电/龙属性的捷克罗姆(代表“理想”),白版的封面神兽是火/龙属性的莱希拉姆(代表“真实”)。版本限定的宝可梦自然也各不相同。剧情的核心围绕着一个叫做“等离子队”的组织和他们的首领N(一个能与宝可梦心灵沟通、致力于“解放宝可梦”的神秘少年)展开,深入探讨了“理想”与“真实”之间的冲突。冒险的舞台换到了一个全新的“合众地区”(灵感来源于美国纽约市),充满了国际大都市的现代气息。还引入了一个叫做“隐藏洞穴”的系统(这些洞穴会随机生成,里面藏着稀有的宝可梦或者道具,看你运气了!)。
  • 《黑2/白2》:两年后的合众,故事仍在继续!:这次玩家扮演的是新的主角,探索的是两年后焕然一新的合众地区。前作里的那些重要角色(比如白露、切莲、N、阿戴克等等)都会悉数登场,并且展现出了各自的成长和转变。新增了一些区域(比如P2实验室、桧扇市),部分道馆馆主和反派的剧情也重新设定了。剧情承接《黑/白》两年之后,原来的等离子队发生了分裂,N的养父魁奇思贼心不死,卷土重来,企图利用传说中的酋雷姆将整个合众地区冰封起来!主线剧情会推进等离子队的事件,支线“N的城堡”还会揭示N不为人知的过去。
    • 记忆链接系统,情怀联动!:如果你的DS里有前作《黑/白》的存档,通过这个系统联动,就能在《黑2/白2》里解锁一些特殊的剧情、对话或者道具,大大增强了代入感和两部作品之间的联系。
    • 新增内容与优化,诚意满满!:加入了新的交通工具“滑板”,还有前面提到的那个能跟历代大佬对战的“宝可梦世界锦标赛(PWT)”,甚至还加入了难度分级系统(普通模式和挑战模式,满足不同玩家的需求)。

第五世代特色:156张全新面孔,纽约风情合众地区,春夏秋冬四季轮转,三人混战大乱斗——生态环境和对战深度再次起飞!

  1. 156只全新宝可梦,属性组合眼花缭乱: 图鉴编号从#494的比克提尼一直到#649的盖诺赛克特,总数达到了649只!在《黑/白》里,通关前你看到的几乎全是这一代新设计的宝可梦,老面孔少得可怜!引入了不少新的复合属性组合(比如虫/岩石这种以前没有的搭配)。核心的传说神兽是捷克罗姆、莱希拉姆,还有能跟它俩合体的酋雷姆(合体后变成焰白酋雷姆或暗黑酋雷姆,种族值高达700,强到离谱!)。还有三只“云神”(龙卷云、雷电云、土地云),需要特定的天气事件才能捕捉到。这一代也诞生了不少人气新星,比如帅气的索罗亚克(恶属性,拥有独特的“幻影”特性,能变成队伍里最后一只宝可梦的样子迷惑对手)、还有虽然是前代但依然强势的烈咬陆鲨(它在这一代获得了“破格”特性,能无视对手的一些防御特性,更猛了!)。
  2. 合众地区(Unova Region),现代都市与原始自然的激情碰撞: 灵感来源于美国纽约市,所以整个地区都充满了国际大都市的现代气息。这里有像飞云市(繁华的港口城市)、雷文市(热闹的娱乐中心)、双龙市(充满东方韵味的龙系圣地)这样的现代化都市,也有像古代城堡、螺旋山这样的原始自然景观,对比鲜明。
  3. 四季变化(Seasons),动态的世界等你探索: 首次引入了季节系统!随着现实生活中的时间流逝(游戏里大概是一个月一换季),合众地区也会经历春夏秋冬的轮换。季节的变化会影响野生宝可梦的种类和出现概率,甚至连地图的样貌都会改变(比如冬天有些道路会被积雪覆盖)。有些宝可梦(比如四季鹿)的外形也会随着季节的变化而改变,非常有趣!
  4. 三人混战(Triple Battles)与轮盘对战(Rotation Battles),战斗维度再扩展!
    • 三人混战: 双方各派出三只宝可梦同时在场上进行战斗!宝可梦的站位会影响它们的攻击范围(比如站在中间的可以攻击到对方全场,而站在两侧的则会受到一些限制),非常考验团队配合、联防轮换和集火秒杀的策略。
    • 轮盘对战: 也是三对三的战斗,但每回合你只能选择场上的一只宝可梦进行行动或者替换下场,非常考验临场应变能力和对对手行动的预判。
  5. 其他重要特性与系统革新,细节之处见真章:
    • “梦特性”(隐藏特性/Hidden Abilities),让老宝可梦焕发新生!:通过一个叫做“宝可梦梦境世界”的在线服务(可惜后来停服了),或者在《黑2/白2》新增的“隐藏洞穴”里,你可以获得拥有特殊“梦特性”的宝可梦。这些梦特性往往比普通特性更强力或者更有趣,一下子丰富了对战环境(比如大嘴鸥获得了“降雨”特性,直接晋升雨天队核心!)。
    • 全局动画,宝可梦动起来了!:在战斗中,宝可梦不再是静止的图片了,而是会持续地做出各种动态效果,招式的演出也更加具有动感和魄力。
    • TM(招式学习器)可以无限使用了! 这可是个天大的好消息!以前那些珍贵的招式学习器用一次就没了,现在可以随便用了,妈妈再也不用担心我学错技能了!
    • 练级也优化了: 等级低的宝可梦在击败等级高的对手时,会获得更多的经验值(最高可以获得150%的加成!),练级效率大大提升。
    • 在线社交与全球对战,一个都不能少!
      • 宝可梦全球连接(PGL)与GTS: 支持跨版本的宝可梦交换,还可以指定交换的条件。PGL以前还提供“梦境世界”的服务,让你在梦里抓宝可梦。
      • Wi-Fi对战: 包含了随机匹配对战和跟好友约战的功能。官方的VGC电竞赛事也继续举办(这一代的规则是6v6单打,禁止使用一级神兽)。
    • 宝可梦好莱坞(Pokéstar Studios,《黑2/白2》专属): 你可以带着你的宝可梦去拍电影!选择不同的剧本和台词,会导向不同的结局,还能获得一些稀有的道具,非常有意思的小游戏。
    • 加盟大道(Join Avenue,《黑2/白2》专属): 你可以发展一条属于自己的商业街!通过跟其他玩家通信,可以招募各种店铺(比如道具店、努力值训练店、美容店等等)并给它们升级,解锁更高级的服务(比如可以直接鉴定宝可梦的个体值!)。
    • 钥匙系统(《黑2/白2》专属): 通关后会解锁一些特殊的“钥匙”,用这些钥匙可以改变游戏的难度(比如开启“挑战模式”让NPC的宝可梦等级更高、AI更强,或者开启“援助模式”降低难度),甚至可以改变特定区域的样貌。

剧情大概是这么个事儿:理想与真实的哲学思辨,宝可梦世界的“成人礼”!

第五世代的剧情,那绝对是系列里数一数二的深刻和复杂!核心是围绕着“宝可梦是否应该被人类束缚和利用”这个极具争议性的话题展开的,塑造了许多令人印象深刻的角色和两难的道德困境。

  • 《黑/白》:等离子队的弥天大谎与N王子的迷茫与觉醒!:你和你的两个好朋友白露、切莲一起在合众地区冒险。很快,你们就遇到了一个倡导“解放宝可梦”、让它们回归自然的神秘组织——等离子队,以及他们的首领,一个能够与宝可梦进行心灵沟通的绿发少年N。N被他的养父,等离子队的真正幕后黑手魁奇思,从小就塑造成了一个“宝可梦解放者”的形象,但实际上,他只是被魁奇思利用,来操控传说中的捷克罗姆(如果你玩的是黑版)或莱希拉姆(如果你玩的是白版),以实现魁奇思自己统治世界的野心。整个剧情都在探讨“理想”与“真实”这两个核心主题,你和N之间的对话和选择,也会影响你们之间的关系。最终,你唤醒了对应版本的传说神兽,在N那座象征着极权统治的城堡里,与他展开了一场宿命的对决。因为受到了你和你的宝可梦之间深厚羁绊的感染,N的信仰开始动摇,他最终选择了自我流放,去寻找真正的答案。这个结局并没有简单的正邪之分,而是留给玩家去思考人与宝可梦之间应该如何共存这个深刻的问题。
  • 《黑2/白2》:分裂与救赎的两年后,酋雷姆的冰封危机与新英雄的崛起!:故事发生在《黑/白》剧情结束的两年后,合众地区因为气候的剧烈变化而分裂成了不同的区域。你扮演新的主角,从宁静的桧扇市开始你的旅程。原来的等离子队已经分裂成了两派:一派是由N领导、追求人类与宝可梦和平共处的“新等离子队”;另一派则是由贼心不死的魁奇思领导、企图复辟并再次统治世界的旧等离子队。魁奇思这次的目标是利用传说中的酋雷姆(它甚至能跟捷克罗姆或莱希拉姆合体,变得更强!)将整个合众地区冰封起来。你需要和新的劲敌以及前作中的老朋友们(比如已经成为博士助手的白露、成为道馆馆主的切莲,还有再次出现的N)一起,粉碎魁奇思的阴谋,探索酋雷姆以及N身世的秘密,最终在巨人洞窟调和两派之间的冲突。前作的冠军阿戴克已经辞去了冠军的职务,去研究生态问题了,而新的冠军则变成了可爱的艾莉丝。

通关小秘籍(五代老鸟的经验之谈):

  1. 开局选哪个?合众御三家,各有千秋: 藤藤蛇(草系,速度快,最终进化君主蛇颜值高),暖暖猪(火系,进化后是火/格斗,输出暴力),水水獭(水系,进化后大剑鬼也很帅气)。早期可以抓到像探探鼠(进化后是步哨鼠,早期好用)、索罗亚(剧情会送你一只,能变成队伍最后一只宝可梦的样子,很有趣)、达摩狒狒(火系,特性“不倒翁模式”很特别)这些。
  2. 合众道馆(《黑/白》里那些印象深刻的馆主): 唐草镇(在这里你会第一次跟N交手)。三曜市(这里有三个馆主天桐、伯特、寇恩,你会根据你初始选择的御三家,对战克制你属性的那个馆主)。七宝市的芦荟(普通系,她的步哨鼠会用“催眠术”和“黑色目光”,有点烦)。飞云市的亚堤(虫系,艺术家气质)。雷文市的小菊儿(电系,模特出身,她的电飞鼠会用“伏特替换”换来换去)。在《黑2/白2》里,道馆馆主有一些变动(比如桧扇市的馆主变成了你的老朋友切莲,是普通系;立涌市的馆主是摇滚少女霍米加,是毒系)。
  3. 神兽与隐藏要素,一个都不能少!:捷克罗姆/莱希拉姆(剧情核心,肯定能抓到)。酋雷姆(在巨人洞窟,在《黑2/白2》里还能跟捷克罗姆/莱希拉姆合体)。三圣剑士(勾帕路翁、代拉基翁、毕力吉翁,分别在特定的地点或者会在地图上随机游荡,需要你去寻找和捕捉)。三圣云(龙卷云、雷电云、土地云,在《黑/白》里会在地图上随机游荡,或者在丰饶之社触发事件;在《黑2/白2》里则需要用到AR搜索器或者跟其他游戏联动才能获得)。幻之宝可梦(比如凯路迪欧、美洛耶塔、盖诺赛克特,老规矩,官方活动限定)。别忘了去探索那些“隐藏洞穴”,里面可能会有拥有梦特性的稀有宝可梦!在《黑2/白2》里,通关后一定要去挑战“宝可梦世界锦标赛(PWT)”,跟历代的道馆馆主和冠军们切磋一下!还有N的城堡遗迹,在《黑2/白2》通关后可以去那里,跟N进行一场特殊的对战。在《黑2/白2》的奇幻桥,带着新月之羽和现形镜,还能遇到前代的传说宝可梦克雷色利亚!
  4. 战斗与培育,细节决定成败: 掌握好三人对战的站位技巧。活用那些强力的隐藏特性。闪光宝可梦依然非常稀有(概率还是1/8192),不过可以通过在晃动的草丛里遇敌,或者在“大量出现”事件中,稍微提高一点遇到闪光的几率。宝可病毒仍然是练努力值的神Buff,遇到了千万别治好!

3DS时代的全3D进化!第六世代(2013-2016):X、Y、欧米伽红宝石、阿尔法蓝宝石 —— Mega进化帅炸天!

当宝可梦系列昂首阔步迈入任天堂3DS时代,那可真是发生了一场翻天覆地的变化!最直观的感受就是——哇塞!全3D了!再也不是纸片人了!还引入了酷到没朋友的“超级进化”(Mega Evolution)机制和全新的“妖精属性”,让对战环境焕然一新。更牛的是,这一代首次实现了全球同步发售!全世界的训练家们终于可以站在同一起跑线上,一起冒险,一起“剁手”了!这标志着宝可梦系列正式开启了现代化和全球化的新纪元。主要的扛鼎之作就是《宝可梦 X/Y》(2013年发售)和第三代经典重制的《宝可梦 欧米伽红宝石/阿尔法蓝宝石》(2014年发售)。

  • 3DS上的视觉盛宴与玩法革新 (Nintendo 3DS平台):

    • 《X/Y》:全球同步发售!2013年10月12日,全世界玩家一起狂欢!
    • 《欧米伽红宝石/阿尔法蓝宝石》(ORAS,三代老粉的福音!):日本、北美、澳洲是2014年11月21日,欧洲稍微晚一点,是2014年11月28日。
  • 口碑炸裂!创新点人人爱!

    • 画面和机制都太顶了!:全3D的画面和那个帅到掉渣的“超级进化”机制,简直是广受好评(《X/Y》在Metacritic上拿了88分的高分,《ORAS》也有78分,都很不错!)。新加入的妖精属性也成功地平衡了对战环境(龙系宝可梦终于有爹了!)。全球同步发售更是极大地提升了玩家的游戏体验,再也不用眼巴巴地等汉化或者担心被剧透了!《ORAS》作为复刻作品,既保留了原作的精华,又融入了各种新的特性和玩法,评价也是相当高。不过嘛,《X/Y》新增的宝可梦数量比较少(只有72种),而且在某些场景下会出现掉帧的情况,也引起了一些小小的讨论。
    • 开启了宝可梦的新时代,影响深远没得说!
      • “超级进化”成了宝可梦系列的一个经典机制,不仅让战斗场面更华丽,也大大丰富了对战的观赏性和策略性。
      • “妖精属性”的加入是对战环境的一次重要调整和优化。
      • 全3D化是系列视觉表现上的一个重要里程碑,为后续作品的技术发展奠定了坚实的基础。
      • 玩家终于可以自定义主角的形象了(肤色、发型、衣服随便换!),个性化体验大大提升!
      • 那个叫做PSS的系统,让在线互动变得超级简单方便。
      • 全球同步发售,更是为宝可梦系列奠定了其全球顶级IP的地位。销量也相当惊人,超过了1600万份!

游戏版本与历史性的全球同步发售!

《X/Y》是宝可梦系列第一次在全球主要地区实现完全同步发售的,这意义可不一般:

  • 全球玩家社区一起嗨翻天!:促进了全球范围内的联机对战和交换(通过“奇迹交换”和那个超级方便的“玩家搜索系统”P.S.S.),消除了因为发售时间不同步导致的等待和信息不对称,也大大增强了整个IP的凝聚力。
  • 商业上也是大获成功!:有效地打击了盗版和跨区购买的行为,首周全球销量就突破了400万份,成为了3DS平台上最畅销的游戏之一(到2022年,《X/Y》的全球销量已经达到了1662万份,在3DS游戏销量榜上排第二,仅次于马车!)。
  • 文化输出也更给力了!:卡洛斯地区那种充满法式风情的美学设计,迅速成为了全球玩家共同的文化记忆。
    《ORAS》也延续了这个成功的策略,大部分地区都实现了同步发售。

第六世代特色:72只新丁加盟,法式风情卡洛斯,超级进化变身!全3D视觉盛宴等你来!

  • 平台与画面大革命(全3D图形,告别纸片人!): 这是第一款在3DS平台上的正统续作,宝可梦、人物、环境全都变成了3D多边形建模,战斗场景(还引入了动态视角和酷炫的“天空对战”)和技能动画都变得更加生动和华丽,带来了里程碑式的视觉体验!在核心城市密阿雷市,你甚至可以360度自由旋转视角,宝可梦的动作也更加逼真了。
  • 72只新宝可梦与浪漫的卡洛斯地区: 新增了72种宝可梦(总数达到了721种!),包括了传说中的哲尔尼亚斯(X神,代表生命)、伊裴尔塔尔(Y神,代表死亡)、还有基格尔德(Z神,负责维护生态平衡)。冒险的舞台换到了一个以法国为原型的卡洛斯地区(Kalos Region),到处都充满了法式的浪漫风情,整个地区的地理结构呈现出一个漂亮的五角星形状,包含了各种各样的地貌。核心城市密阿雷市(Lumiose City)是以巴黎为蓝本设计的,标志性建筑“棱镜塔”简直就是埃菲尔铁塔的翻版,完美还原了巴黎的时尚艺术气息。整个地区都强调人文与科技的结合。
  • 超级进化(Mega Evolution),战斗中变身,帅就完事了!:特定的宝可梦(总共有48种可以Mega进化,包括了ORAS里新增的那些)只要携带了对应的“超级石”,并且训练家手持“钥石”,就能在战斗中临时进行超级进化!进化后不仅外观变得更加酷炫,种族值总和也会提升整整100点,能力、特性甚至连属性都可能发生改变(比如喷火龙X会变成火/龙属性,超帅!)。每场战斗只能让一只宝可梦进行一次超级进化,非常考验训练家的战术选择和使用时机。传说中的烈空坐更是个例外,它不需要超级石,只要学会了招式“画龙点睛”,就能直接进行超级进化,还拥有专属的特性“德尔塔气流”。
  • 妖精属性(Fairy Type),龙系宝可梦的噩梦来了!:为了平衡之前过于强势的龙系宝可梦,新加入了妖精属性。这个属性对格斗系、龙系、恶系效果绝佳,还能抵抗虫系、格斗系、恶系的攻击,并且免疫龙系招式!不过它也怕毒属性和钢属性的攻击。一些以前的老宝可梦也追加了妖精属性,或者直接改成了妖精属性。代表性的新妖精属性宝可梦有可爱的绵绵泡芙和粉香香。
  • 系统与玩法大优化,体验更上一层楼!
    • 抓怪也能得经验了!:成功捕捉到野生宝可梦后,你的队伍也能获得经验值了。
    • 学习装置全队共享经验,练级更轻松!:只要打开学习装置,队伍里所有的宝可梦(除了出战的)都能分享到50%的经验值,大大降低了练级的难度。
    • 轮滑鞋,新的代步工具!:新增了轮滑鞋,比跑步鞋更快,操控也更灵活,在城市里穿梭自如!
    • 宝可梦友友乐(Pokémon-Amie),跟你的宝可梦亲密互动!:通过触摸屏,你可以跟你的宝可梦进行各种互动(比如喂食、抚摸、玩小游戏等等),提升它们和你的“牵绊度”。牵绊度高了,在战斗中可能会触发一些特殊效果(比如闪避对手的攻击、提高会心一击的几率、甚至在残血的时候坚持住不倒下!)。
    • 超级特别训练(Super Training),可视化练努力值!:通过一些小游戏的方式,可以直观地培养宝可梦的基础点数(也就是努力值),不用再像以前那样盲目地刷怪了。
    • 玩家自定义形象,打造专属训练家!:终于可以自定义主角的肤色、发型、服装了!想怎么搭配就怎么搭配,打造属于你自己的个性训练家!
    • 群聚对战(Horde Encounters),1V5的挑战!:在野外有可能会一次性遇到多只(通常是5只)野生宝可梦,这种群聚对战非常适合刷努力值。
    • 空中对战(Sky Battles),天空才是极限!:一种特殊的对战模式,只有飞行属性的宝可梦或者拥有“飘浮”特性的宝可梦才能参加。训练家会穿着飞翼服在空中进行对战,战斗场景还会动态变化。

剧情大概是这么个事儿:生命、死亡与生态平衡的宏大史诗,经典与创新的完美交融!

  • 《X/Y》:生命、毁灭与卡洛斯地区流传千年的传说!:你在美丽的卡洛斯地区冒险,目标是挑战道馆和联盟。结果呢,遇上了一个叫做“闪焰队”的神经病组织,他们的老大弗拉达利想利用一个叫做“最终兵器”的古代武器,以及传说中的哲尔尼亚斯(如果你玩的是X版)或伊裴尔塔尔(如果你玩的是Y版)的力量,消灭掉除了他们自己以外的所有生命,创造一个所谓的“美丽”新世界(这脑回路也是清奇)。你需要粉碎他们的阴谋,并且会了解到卡洛斯地区古老的历史,以及一个活了整整3000年的国王AZ和他的宝可梦花叶蒂之间那段悲伤又感人的故事(这也是宝可梦系列第一次如此深刻地把宝可梦和人类历史上的悲剧联系起来)。结局是哲尔尼亚斯和伊裴尔塔尔的力量达到了平衡,卡洛斯地区恢复了和平,AZ也终于和他的花叶蒂重逢,并且亲手摧毁了最终兵器,象征着救赎和对生命的珍爱。生态平衡的守护者基格尔德也会登场,警示着人类要与自然和谐共处。
  • 《ORAS》:丰缘地区的重生与原始回归,平行宇宙的初次探索!:这是第三代《红宝石/蓝宝石》在3DS上的完全重制版,带我们重回那个充满热带风情的丰缘地区。核心剧情还是围绕着熔岩队(在OR版里,他们想利用原始固拉多扩张陆地)和海洋队(在AS版里,他们想利用原始盖欧卡扩张海洋)这两个老对头展开。
    • 原始回归(Primal Reversion),神兽变身更霸气!:固拉多和盖欧卡只要携带了对应的“朱红色宝珠”或“靛蓝色宝珠”,就能在战斗中进行原始回归!回归后的形态更加霸气,特性(固拉多的“终结大地”和盖欧卡的“始源之海”,能改变天气并且让对应属性的招式无效!)和能力都会大幅增强。
    • 通关后新增了一个叫做“Episode Delta”(德尔塔篇章)的剧情章节:这个章节深入讲述了烈空坐、超级进化的起源以及宇宙宝可梦代欧奇希斯的故事,揭示了丰缘地区和卡洛斯地区之间一些不为人知的历史渊源,甚至还引入了平行宇宙的概念,信息量巨大!你需要和新的角色希嘉娜、还有老朋友大吾一起,解决一场由陨石引发的危机,捕捉烈空坐并让它超级进化,然后骑着它飞向宇宙摧毁陨石,途中还会跟代欧奇希斯进行一场激烈的战斗!
    • 翱翔高空(Soaring in the Sky),骑着神兽去旅游!:拿到无限船票后,你可以召唤超级拉帝亚斯或超级拉帝欧斯,在丰缘地区的上空自由飞行!飞行途中还会发现一些叫做“幻之地点”的隐藏区域,在那里可以抓到许多历代游戏里的传说宝可梦!
    • 图鉴导航器(DexNav),抓宠神器!:下屏的一个实用工具,可以帮助你搜索特定的野生宝可梦,这些宝可梦可能拥有隐藏特性、特殊的招式或者更高的个体值!连续遇到同一种宝可梦还能提高遇到闪光的几率!
    • 超级秘密基地(Super-Secret Bases),装修你的秘密据点!:秘密基地系统也得到了强化,你可以更自由地装饰你的基地,通过QR码分享给其他玩家,还可以去访问别人的基地,跟他们对战或者招募他们当你的伙伴。
    • 华丽大赛(Pokémon Contest Spectaculars)也回归并且强化了!

通关小秘籍(六代老司机的指路明灯):

  1. 开局选哪个?御三家都很强力!:在《X/Y》里,卡洛斯地区的御三家是哈力栗(草/格斗)、火狐狸(火/超能)、呱呱泡蛙(水/恶)。剧情还会送你一只关都地区的御三家。一般来说,推荐火狐狸(进化后妖火红狐颜值高,特攻也强)搭配妙蛙种子(妙蛙花能抗能打)。早期还能获得皮卡丘和路卡利欧(能Mega进化,超帅!)。在《ORAS》里,还是丰缘地区的老三样:木守宫(草)、火稚鸡(火/格斗)、水跃鱼(水/地面)。建议优先培养水跃鱼,进化成巨沼怪后非常好用,弱点少。工具宠方面,《ORAS》里的蛇纹熊因为有“捡拾”特性(能捡到各种好东西)而备受青睐,热带龙能学很多秘传学习器,也是不错的选择。
  2. 道馆(那些让你印象深刻的馆主): 《X/Y》里,密阿雷市的希特隆(电系发明家),香薰市的可尔妮(格斗系轮滑少女,也是第一个教你Mega进化的)。《ORAS》里,卡那兹市的杜鹃(岩石系优等生),紫堇市的铁旋(电系乐呵呵大叔),茵郁市的娜琪(飞行系优雅大姐姐)。
  3. Mega进化与原始回归,用好了就是神技!:Mega石一部分会在剧情里送给你,另一部分则需要你自己去探索或者完成特定任务才能获得。优先强化那些速度快、攻击高的宝可梦(比如喷火龙X/Y、超梦Y、烈空坐等等)。盖欧卡和固拉多只要携带了对应的宝珠就能在战斗中进行原始回归,改变天气并且大幅提升能力。
  4. 神兽在哪抓?一个都不能少!(部分列举): 《X/Y》里,哲尔尼亚斯/伊裴尔塔尔是剧情核心,肯定能抓到。基格尔德的50%形态藏在终结洞窟里。超梦在无名洞窟等着你。在《ORAS》里,固拉多/盖欧卡也是剧情核心。烈空坐则在天空之柱。三神柱还是老样子,需要解谜才能遇到。代欧奇希斯在德尔塔篇章结束后,会再次出现在天空之柱。拉帝欧斯/拉帝亚斯会作为剧情的一部分加入你的队伍。还有大量历代游戏里的传说宝可梦,可以通过翱翔高空在那些“幻之地点”遇到。在《X/Y》里,还有一个叫做“朋友狩猎区”的地方,可以刷到高个体值或者拥有隐藏特性的宝可梦。
  5. 通关后干啥?内容多到玩不过来!:《X/Y》通关后有帅哥的侦探任务,还有对战公馆让你挑战。别忘了在地图上找找那些隐藏的道具,皮卡丘的尾巴会提示你附近有东西哦!在《ORAS》里,通关后有精彩的德尔塔篇章,还有回归的对战开拓区(虽然规模不如以前大了),超级秘密基地也能让你玩上好一阵子。还有各种幻影岛屿和古代坟墓的谜题等着你去探索。《ORAS》的试玩版如果玩过,还能联动到正式版里获得一些Mega石和其他道具,里面还包含了一个隐藏的“幻影洞窟”任务。不同版本的游戏之间,可以通过“宝可梦银行”这个服务来传送数据。

官方中文终于来了!第七世代(2016-2019):太阳、月亮、究极之日/月,还有Switch上的试水作Let’s Go! 皮卡丘/伊布

第七世代绝对是宝可梦系列历史上一个极其重要的里程碑!它不仅横跨了3DS和Switch两大平台,给我们带来了充满热带风情的奇妙冒险和全新的战斗机制,还在Switch上用一种全新的方式重温了初代的感动。最最最重要的是——这一代首次内置了官方简体中文和繁体中文! 并且统一了译名,正式叫做“宝可梦”!这对于广大的中文区玩家来说,简直是天大的好消息,游戏体验直接原地起飞!

  • 3DS与Switch双平台齐开花 (Nintendo 3DS & Switch):

    • 《太阳/月亮》(3DS):日本、北美、澳洲是2016年11月18日,欧洲是2016年11月23日。
    • 《究极之日/究极之月》(3DS,《太阳/月亮》的平行世界威力加强版):全球同步!2017年11月17日。
    • 《Let’s Go! 皮卡丘/伊布》(Switch,初代《黄版》的温馨重制):全球同步!2018年11月16日。
  • 评价与影响?中文加持,创新与情怀两手抓,两手都很硬!

    • 口碑相当不错,特色玩法广受好评!:独特的阿罗拉地区热带风情、有趣的“地区形态”宝可梦以及对传统道馆模式的大胆改变,都获得了玩家们的好评(《太阳/月亮》在Metacritic上拿了87分的高分,《究极之日/月》也有83分,都很棒!)。那个演出效果超级华丽的“Z招式”,更是让人眼前一亮!剧情和角色塑造(尤其是莉莉艾这个角色)也相当出色,赚足了玩家的眼泪。不过嘛,《太阳/月亮》因为教学内容有点过多,导致游戏节奏偏慢;而《究极之日/究极之月》对剧情的一些改动,也引发了一些讨论。《Let’s Go!》系列作为一款面向新玩家和轻度用户的作品,成功吸引了大量新用户(全球销量超过了1500万份,厉害了!),但因为内容深度不足,也受到了一些核心玩家的质疑。
    • 全球化的里程碑,影响不是一般的大!
      • 首次官方中文化! 这点必须加粗强调!统一了译名“宝可梦”,对于中文市场来说意义非凡,极大地推动了系列在全球范围内的推广。
      • “地区形态”这个设定大受欢迎,给后续的作品提供了很多新的设计思路。
      • “Z招式”给战斗提供了一个全新的爆发点,增加了战斗的刺激性和变数。
      • “诸岛巡礼”这个设定,是对传统道馆模式的一次大胆革新和尝试。
      • 取消了烦人的秘传学习器,改用更方便的“宝可可骑行”,大大提升了游戏的便利性。
      • 《Let’s Go!》系列的成功,也为宝可梦在Switch平台上的发展进行了成功的试水,为后来的《剑/盾》实现全3D化以及与《Pokémon GO》的联动铺平了道路。

游戏版本与特色玩法:热情似火的阿罗拉,温馨重现的经典关都!

  • 《太阳/月亮》(3DS): 2016年11月18日全球同步发售。关键的传说宝可梦是索尔迦雷欧(太阳版,像狮子一样威武)和露奈雅拉(月亮版,像蝙蝠一样优雅)。比较特别的是,《月亮》版的游戏内时间跟现实生活中的时间会有12个小时的时差,这会影响到某些宝可梦的出现和一些剧情的触发。
  • 《究极之日/究极之月》(3DS): 这是《太阳/月亮》的平行世界故事,也可以看作是加强版,在2017年11月17日发售。故事的核心围绕着传说宝可梦奈克洛兹玛以及它的不同形态(黄昏之鬃、拂晓之翼、究极奈克洛兹玛)展开。补充了原作的剧情,让角色形象更加丰满。还新增了一个叫做“究极调查队”的组织(他们来自一个被奈克洛兹玛夺走了光明的异世界)。玩家可以通过“究极之洞”,骑着索尔迦雷欧或露奈雅拉进行究极空间跳跃,去捕捉历代的传说宝可梦和闪光宝可梦!
    • 新增玩法,乐趣加倍!:可以收集“霸主贴纸”(收集到一定数量可以兑换体型巨大的霸主宝可梦)、去阿罗拉摄影俱乐部给你的宝可梦拍照留念、体验紧张刺激的代理人对战、还有在海上进行巨翅飞鱼冲浪小游戏。洛托姆图鉴的功能也增强了(它会时不时给你一些Buff,比如“洛托碰碰”,甚至还有“Z洛托姆之力”,能让在一场战斗中使用两次Z招式!)。阿罗拉地区的图鉴也扩充到了400多只,新增了一些究极异兽和幻之宝可梦捷拉奥拉。岩狗狗还新增了一个帅气的黄昏形态。通关后还有一个叫做“彩虹火箭队”的篇章,火箭队老大坂木居然集结了历代游戏里的所有反派首领,要搞波大的!
  • 《Let’s Go! 皮卡丘/伊布》(Switch): 2018年11月16日推出,是初代《黄版》的温馨重制版,主要面向的是新玩家以及那些因为《Pokémon GO》而入坑的用户。
    • 核心创新与简化,轻松上手无压力!
      • 捕捉方式大革新!:取消了跟野生宝可梦的对战(只有特定的传说宝可梦除外),改成了类似《Pokémon GO》那样,通过体感操作来投掷精灵球进行捕捉(支持用Joy-Con或者那个酷酷的精灵球Plus外设)。连续捕捉同一种宝可梦(也就是连锁捕捉),可以提高遇到高个体值和闪光宝可梦的几率!
      • 你的专属搭档!:开局的搭档是皮卡丘或者伊布(看你买的是哪个版本啦),它们是不能进化的,但拥有非常强力的专属招式,而且个体值天生就是全满的!你还可以跟它们进行各种互动,给它们换装打扮。
      • 系统大简化!:取消了特性、持有物、天气这些比较复杂的系统。秘传学习器也整合成了搭档的“秘术”,不需要再占用宝可梦的技能格子了。
      • 明雷遇敌,看得见才抓得准!:野外的宝可梦在地图上是直接可见的,再也不用盲目地在草丛里转圈圈了。
      • 跟《Pokémon GO》梦幻联动!:你可以把自己在《Pokémon GO》里抓到的初代宝可梦(包括它们的阿罗拉形态)传送到《Let’s Go!》里的一个叫做“GO Park”的地方,然后再去捕捉它们(不过是单向传送,传过来就回不去了哦!)。
      • 双人合作,乐趣加倍!:支持用两个Joy-Con进行双人冒险,小伙伴可以帮你一起捕捉宝可梦,或者在对战中助你一臂之力。
      • 精灵球 Plus,训练家的浪漫!:这是一个全新的外设,不仅可以作为游戏控制器使用,里面还预装了一只幻之宝可梦“梦幻”!
      • 挑战大师训练家,证明你的实力!:通关后,你可以去挑战像赤红这些经典角色,他们被称为“大师训练家”。
    • 版本差异: 除了初始搭档不一样,某些野生宝可梦的出现概率和种类也会有所区别。

第七世代特色:88位新朋友驾到,热情似火的阿罗拉,毁天灭地的Z招式,还有老朋友的新面貌——地区形态!

  • 88只新宝可梦,阿罗拉的独特生态圈: 总共新增了88种宝可梦(到《究极之日/月》为止,图鉴编号从#722的木木枭到#809的美录梅塔,宝可梦总数达到了807种!)。
    • 新的御三家,各有萌点: 木木梟(草/飞行属性,最终进化是狙射树枭,草/幽灵属性,帅气弓箭手!)、火斑喵(火属性,最终进化是炽焰咆哮虎,火/恶属性,霸气摔跤手!)、球球海狮(水属性,最终进化是西狮海壬,水/妖精属性,优雅歌唱家!),它们的最终进化型都有专属的Z招式!
    • 传说宝可梦与神秘的究极异兽: 核心的传说神兽是索尔迦雷欧(钢/超能属性)和露奈雅拉(幽灵/超能属性),还有奈克洛兹玛(超能属性)。还引入了一群来自异次元的“究极异兽(UB)”(比如虚吾伊德、爆肌蚊这些,设计风格非常独特,甚至有点诡异)。
  • 阿罗拉地区(Alola Region),热带岛屿风情与独特的文化重构: 以夏威夷为原型,由四个自然岛屿和一个人工岛屿组成,气候湿热,到处都充满了热带风情,强调生态的多样性。
    • “诸岛巡礼”取代了传统的道馆挑战: 在阿罗拉地区,你需要完成各个岛屿上队长们设置的考验,然后挑战该岛的岛屿之王或岛屿女王,进行一场叫做“大考验”的对战。每个岛屿还有各自的守护神“卡璞”。
    • “宝可骑行”取代了烦人的秘传学习器: 你可以召唤特定的宝可梦(比如骑着喷火龙飞天,骑着巨牙鲨冲浪)来跨越各种障碍或者进行快速移动,再也不用为了学秘传技而牺牲一个技能格子了!
    • 其他特色玩法,乐趣多多: 可以在宝可梦学校里和莉莉艾这些小伙伴们一起学习成长;用宝可搜寻镜在特定的地点给宝可梦拍照;还有一个会说话的洛托姆图鉴陪你冒险;通过宝可清爽乐跟你的宝可梦互动,提升它们的友好度,还能治疗异常状态。
  • Z招式(Z-Moves),属性大招毁天灭地,威力惊人还带Pose!:训练家只要佩戴了Z手环或者Z强力手环,并且让宝可梦携带了同种属性的Z纯晶,就能在战斗中发动一次威力超强的Z招式(发动的时候还得摆个酷炫的Pose!)。
    • 机制是这样的: 每场战斗只能使用一次Z招式。攻击型的Z招式威力巨大,但会失去原有招式的附加效果;变化型的Z招式则会在原有效果的基础上增加一些额外的增益效果。Z招式还能无视对手的“守住”技能,造成25%的伤害。
    • 种类还挺多: 有通用的、对应18种属性的Z招式(比如火系的“超强极限爆焰弹”,有些还能攻击到多个目标!),也有特定宝可梦才能使用的专属Z招式(比如卡比兽的“认真起来大爆击”,简直萌翻了!)。在《究极之日/月》里,还新增了一些Z招式和更酷的Z强力手环。
  • 地区形态(Regional Forms),老朋友换新装,环境造就大不同!:一些我们熟悉的老宝可梦,为了适应阿罗拉地区独特的自然环境,进化出了完全不同的样貌、属性和生活习性!比如阿罗拉地区的椰蛋树(草/龙属性,脖子长到屏幕都装不下!)、阿罗拉地区的九尾(冰/妖精属性,仙气飘飘!)、阿罗拉地区的雷丘(电/超能属性,居然用尾巴当冲浪板!),这个设定大受欢迎,也为后续作品的地区形态设计开创了先河。

剧情大概是这么个事儿:光明与黑暗的救赎史诗,亲情与友情的守护之旅,还有跨越维度的终极较量!

  • 《太阳/月亮》:守护光与暗的平衡,一个关于亲情守护的温暖故事: 你扮演一个刚刚搬到阿罗拉地区的新人训练家,开始了独特的“诸岛巡礼”冒险。你会遇到一个神秘的少女莉莉艾(她曾经被究极异兽虚吾伊德袭击过,所以非常害怕接触宝可梦)和她偷偷带着的一只叫做“小星云”的神秘宝可梦(其实是科斯莫古)。你还会和阳光开朗的劲敌哈乌、以及帅气又有点不靠谱的库库伊博士一起冒险。主要的敌人是一个叫做“以太基金会”的组织和他们的理事长露莎米奈(她其实是莉莉艾的妈妈!)。露莎米奈痴迷于研究究极异兽,甚至想利用小星云的力量打开究极之洞,把究极异兽引到阿罗拉地区。你需要阻止她的计划,并且会亲眼见证小星云进化成传说中的索尔迦雷欧(如果你玩的是太阳版)或露奈雅拉(如果你玩的是月亮版)。最终,你会在究极空间里与受到虚吾伊德影响而变得疯狂的露莎米奈进行一场激烈的对战。结局是莉莉艾和她的家庭达成了和解,阿罗拉地区也正式建立了宝可梦联盟,而你,则成为了阿罗拉地区的第一任冠军!
  • 《究极之日/究极之月》:奈克洛兹玛的光明危机与平行世界的救赎之路!:这是《太阳/月亮》的平行世界故事,主线流程大体相似,但是很多事件、人物对话和剧情细节都有所不同。故事的核心围绕着传说宝可梦奈克洛兹玛(它曾经是究极之都的光源,后来因为某些原因变得狂暴)展开,揭示了关于光与暗的古老传说。新增了一个叫做“究极调查队”的组织(他们来自一个被奈克洛兹玛夺走了光明的异世界),你需要和他们合作,对抗同样想要利用奈克洛兹玛力量的露莎米奈(不过这次她的动机和行为都有所调整)。奈克洛兹玛会吸收索尔迦雷欧或露奈雅拉,进化成“黄昏之鬃”或“拂晓之翼”形态,最终还会进行“究极爆发”,变成帅到炸裂的“究极奈克洛兹玛”!你需要击败它,才能让阿罗拉地区重获光明。通关后还有一个叫做“彩虹火箭队”的篇章,火箭队老大坂木居然集结了历代游戏里的所有反派首领,妄图利用究极异兽的力量统治所有的平行宇宙!
  • 《Let’s Go! 皮卡丘/伊布》:重温关都地区的经典冒险,与你的搭档开启一场温馨之旅!:故事发生在经典的关都地区,基本上沿袭了初代《黄版》的剧情。你和你的搭档皮卡丘或伊布一起冒险,挑战八个道馆,击败四天王和冠军。一路上会遇到各种各样的事件,还会跟火箭队(武藏、小次郎、喵喵这对活宝也会登场!)进行战斗。新增了一些感人的剧情(比如帮助可拉可拉寻找它失踪的妈妈),火箭队的基地也增加了一些电梯解谜的元素。你的劲敌是一个青梅竹马的小伙伴。通关后,你还可以去挑战像赤红这样的大师训练家。

通关小秘籍(七代老鸟的友情提示):

  1. 《太阳/月亮》与《究极之日/究极之月》阿罗拉冒险指南: 熟悉每个岛屿考验的内容,优先解决霸主宝可梦召唤出来的小弟,关键时刻别忘了用Z招式!活用属性克制提示,能让你事半功倍。多利用“宝可骑行”探索地图,收集隐藏的道具、Z纯晶、还有那些霸主贴纸。通关后,《太阳/月亮》可以去挑战对战树(在那里可以获得Mega石);《究极之日/月》除了对战树,还可以通过“究极空间跳跃骑行”去捕捉历代的传说宝可梦和闪光宝可梦,还能体验紧张刺激的代理人对战。那个“彩虹火箭队”篇章,需要你有非常强力的队伍才能应对哦!记得通过“宝可梦评定”(孵化20个蛋就能解锁)来查看宝可梦的个体值,利用“红线”和“不变之石”来进行高效的培育。在对战树获得的BP点数,可以用来兑换“金色王冠”(能把一只宝可梦的个体值直接提升到最大!)和各种Mega石。
  2. 《Let’s Go! 皮卡丘/伊布》轻松愉快的关都之旅: 熟悉体感投掷精灵球的操作,用树果可以提高捕捉的成功率。通过连锁捕捉来刷高个体值的宝可梦和闪光宝可梦,效率会高很多。多跟你的搭档皮卡丘或伊布互动,提升它们的亲密度,它们会学会一些非常强力的搭档专属招式。留意每个道馆的进入条件,别白跑一趟。跟NPC进行交换,可以获得一些拥有阿罗拉形态的宝可梦。通关后去挑战那些大师训练家,他们每个人都只会使用一只特定的宝可梦跟你进行1v1的对战,需要你培养出对应的克制宝可梦才能获胜。

Switch时代全面降临!第八世代(2019-2022):剑、盾、晶灿钻石/明亮珍珠、传说 阿尔宙斯

第八世代,宝可梦系列在Nintendo Switch这个性能更强的平台上火力全开!不仅带来了核心RPG体验的全面提升,还在玩法上进行了各种各样大胆的创新和尝试,推动着整个系列向着更高清的画面和更自由的开放世界探索大步迈进!

  • Switch上的宝可梦新纪元 (Nintendo Switch平台):

    • 《剑/盾》:全球同步!2019年11月15日;后来还出了两个付费DLC,《铠之孤岛》是2020年6月17日,《冠之雪原》是2020年10月23日。
    • 《晶灿钻石/明亮珍珠》(第四代《钻石/珍珠》的复刻版):全球同步!2021年11月19日。
    • 《传说 阿尔宙斯》(颠覆传统的全新玩法!):全球同步!2022年1月28日。
  • 评价与影响?创新与争议并存,但未来绝对可期!

    • 口碑嘛,可以说是各有千秋,亮点也不少
      • 《剑/盾》(Metacritic评分80分,销量更是突破了2616万份,卖疯了!):高清的画面、流畅的联机体验、广阔的“旷野地带”以及霸气的“极巨化”战斗,都给玩家带来了不少新鲜感,获得了普遍的认可。但是呢,因为删减了全国图鉴(不是所有宝可梦都能传到这一代了)、剧情流程比较短、以及地图设计上的一些问题,也引发了不少争议和核心玩家的不满。
      • 《晶灿钻石/明亮珍珠》:对于很多老玩家来说,能忠实地还原原作已经很开心了。但是,因为它在玩法上缺乏创新、Q版的画风不太讨喜、以及一些技术上的小毛病,也受到了不少批评。
      • 《传说 阿尔宙斯》:这款游戏简直是颠覆性的玩法革新!那种沉浸式的探索和全新的捕捉体验获得了玩家们的高度评价,被看作是系列一次非常大胆而且成功的尝试!虽然画面技术力还是有点跟不上时代,但瑕不掩瑜。
    • 为系列的未来指明了方向,影响深远没得跑!
      • 《剑/盾》里的“旷野地带”,为后续作品的开放世界设计提供了宝贵的探索经验,而DLC付费扩展内容的模式,也成为了系列内容更新的一种新方式。
      • 《传说 阿尔宙斯》的巨大成功,为宝可梦系列未来的发展(尤其是在动作性和世界探索方面)提供了非常重要的参考,充分展示了这个老牌IP在玩法创新上的巨大潜力,也为后来的第九代《朱/紫》实现真正的开放世界奠定了技术基础。

主要游戏版本与大胆的创新尝试:Switch上的宝可梦,玩的就是心跳!

  • 《剑/盾》:在伽勒尔地区展开冒险,体验霸气的极巨化!:第八世代的开篇之作,继承了宝可梦系列的核心玩法,冒险的舞台换到了一个全新的、充满英伦风情的“伽勒尔地区”。玩家的目标还是老样子,挑战道馆,最终成为冠军。游戏的画面有了显著的提升,伽勒尔地区的风光和宝可梦的细节都更加生动了。并且首次尝试设计了一个叫做“旷野地带”的开放区域。后来还推出了两个付费DLC——《铠之孤岛》和《冠之雪原》,进一步扩展了游戏的世界、增加了新的宝可梦(比如帅气的武道熊师和神秘的蕾冠王)以及全新的剧情故事。
  • 《晶灿钻石/明亮珍珠》:第四代经典的完美复刻?Q版画风重温神奥!:这是第四代《钻石/珍珠》的重制版,比较特别的是,这次不是由Game Freak本社开发,而是交给了ILCA这家公司(当然,Game Freak还是负责监制的)。游戏保留了原版的故事和核心玩法,画面进行了3D重制,不过人物设计采用的是比较可爱的Q版二头身风格。也加入了一些现代化的改进,比如“宝可梦秘巢”(就是地下大洞窟,可以在里面摆放石像来影响出现的宝可梦种类)、宝可梦跟随系统、经验分享、极限特训、薄荷这些方便培育的系统。还新增了一个叫做Ramanas Park的地方,可以在那里镶嵌石板来召唤以前的传说宝可梦。
  • 《传说 阿尔宙斯》:穿越到古代的神奥,体验前所未有的动作RPG!:这款游戏绝对是宝可梦系列玩法上的一次重大创新和突破!它是系列第一部真正的动作RPG。故事发生在比《钻石/珍珠》更古老的“洗翠地区”(也就是神奥地区的前身),你扮演一名穿越而来的调查员,加入了银河队的调查组,目标是完成洗翠地区第一本宝可梦图鉴。游戏引入了半开放世界的元素(由多个大型的开放区域组成),你可以自由地探索、收集素材、自己制作各种道具。战斗系统也进行了大刀阔斧的调整,引入了即时捕捉、“迅疾”攻击模式和“刚猛”攻击模式、以及无缝衔接的即时战斗。游戏非常强调对生态环境的观察和与宝可梦的互动。图鉴系统也不再是简单地抓到就行了,而是需要完成各种各样的调查课题来提升研究等级。还引入了一个叫做“时空歪曲”的随机机制(在特定的区域会随机出现时空裂缝,里面会有一些稀有的宝可梦和道具)。

第八世代特色:96位新朋友加盟,英伦风情伽勒尔,霸气侧漏极巨化,开放世界初体验!

  • 96只新宝可梦与酷炫的地区形态: 新增了96种宝可梦(编号从810的敲音猴到905的眷恋云,包含了DLC和《阿尔宙斯》里新增的那些),让宝可梦大家庭更加壮大了。
    • 新的御三家,你选哪个?:敲音猴(草属性,最终进化是轰擂金刚猩,草/岩石属性,打鼓的猩猩!)、炎兔儿(火属性,最终进化是闪焰王牌,火/格斗属性,踢足球的兔子!)、泪眼蜥(水属性,最终进化是千面避役,水/恶属性,耍帅的特工蜥蜴!)。
    • 伽勒尔形态,一方水土养一方宝可梦!:一些我们熟悉的老宝可梦,在伽勒尔地区也拥有了适应当地环境的独特“伽勒尔形态”(比如伽勒尔地区的呆呆兽是超能属性,伽勒尔地区的蛇纹熊是恶属性),有些甚至还有了新的进化形态(比如伽勒尔地区的大葱鸭可以进化成帅气的葱游兵!)。
    • 洗翠形态(《传说 阿尔宙斯》专属): 在古代的洗翠地区,一些宝可梦也拥有着和现代不同的独特形态和进化方式(比如洗翠地区的索罗亚是普通/幽灵属性,非常特别!)。
  • 伽勒尔地区(Galar Region)与广阔的旷野地带: 这是《剑/盾》的冒险舞台,灵感来源于英国。这里有各种各样的地貌,道馆挑战也被设计成了盛大的体育赛事,非常有仪式感。
    • 旷野地带(Wild Area),自由探索新天地!:这是一片非常广阔的开放区域,连接着伽勒尔地区的多个城镇。在这里,你可以自由地调整视角进行探索,遇到的宝可梦种类会随着天气(天气是动态变化的哦!)和地点的不同而改变。旷野地带还分布着许多“宝可梦巢穴”,你可以和其他玩家组队进行“极巨团体战”,挑战强大的极巨化宝可梦。
  • 极巨化(Dynamax)与超极巨化(Gigantamax),宝可梦变身奥特曼!:这是伽勒尔地区特有的一种现象,只有在特定的地点(比如道馆、宝可梦巢穴,或者使用了极巨腕带)才能发生。
    • 极巨化: 所有的宝可梦都可以进行极巨化!极巨化后,宝可梦的体型会变得无比巨大,HP也会提升,能力得到增强,所有的招式都会变成威力更强的“极巨招式”(有些极巨招式还能改变天气或者场地效果!)。极巨化状态会持续三个回合,每场战斗只能使用一次,而且极巨化后的宝可梦免疫强制退场的技能。
    • 超极巨化: 只有一部分特殊的宝可梦才能进行超极巨化。超极巨化后,不仅体型会变大,外形也会发生独特的变化(比如喷火龙超极巨化后,火焰会呈现出漩涡的形状!),并且会拥有专属的“超极巨招式”。超极巨化的宝可梦比较稀有,通常需要通过极巨团体战才能捕捉到。
  • 开放世界元素的尝试与玩法的巨大革新!
    • 《剑/盾》的旷野地带和DLC: 引入了更加广阔和自由的探索空间,你可以骑着自行车在旷野里尽情驰骋。
    • 《传说 阿尔宙斯》的颠覆性变革!:采用了半开放式的地图设计(由黑曜原野等五个大型的开放区域组成),给予了玩家前所未有的高度自由去探索。你可以骑着特定的宝可梦上天入海,收集各种素材,甚至可以直接和野外的宝可梦进行互动和战斗!取消了传统的道馆挑战,改为了以完成各种调查任务为核心的玩法。还引入了强大的头目宝可梦和需要特殊仪式才能平息的“王”或“女王”宝可梦。宝可梦的进化方式也变得更加独特(比如黏美龙需要在雨天升级才能进化,月月熊则需要使用稀有的“泥炭块”才能进化)。

剧情大概是这么个事儿:挑战冠军之路,揭露能源危机,重温神奥传说,探索古代创世之谜!

  • 《剑/盾》:踏上冠军之路,揭露伽勒尔地区的能源危机!:你和你的好朋友赫普受到了现任冠军丹帝的推荐,一起踏上了挑战伽勒尔地区各个道馆的旅程,目标是成为新的冠军!一路上,你们会遇到强劲的劲敌彼特和玛俐,以及他们有点狂热的粉丝团“呐喊队”(他们其实是为了给玛俐加油的,算不上真正的反派)。后来,你们会卷入能源公司“马洛科蒙”的会长洛兹为了解决伽勒尔地区能源危机而引发的一系列事件,甚至还涉及到了唤醒传说中的宝可梦无极汰那。最终,你需要和丹帝、赫普以及传说中的英雄宝可梦苍响(如果你玩的是剑版)或藏玛然特(如果你玩的是盾版)联手,平息因为无极汰那失控的无极巨化而引发的“暗夜”灾难,最后堂堂正正地挑战丹帝,成为新的伽勒尔冠军!
    • DLC剧情,精彩继续!:《铠之孤岛》的剧情主题是“成长”,你会在铠岛上和可爱的熊徒弟一起进行各种修行,最终让它成长为帅气的武道熊师。《冠之雪原》的剧情主题则是“探索”,你会在冰天雪地的王冠雪原追寻传说中的蕾冠王以及它的坐骑雪暴马或灵幽马的古老故事。
  • 《晶灿钻石/明亮珍珠》:重温神奥地区的经典传说,再次挑战时空双神!:剧情基本上和原作《钻石/珍珠》一模一样。你和你的劲敌一起在神奥地区冒险,目标是完成图鉴,成为神奥联盟的冠军。途中,你需要阻止邪恶组织银河队和他们的首领赤日利用帝牙卢卡(晶灿钻石版)或帕路奇亚(明亮珍珠版)的力量来创造一个所谓的“完美新世界”的阴谋。你还会了解到关于骑拉帝纳和反转世界的秘密。在挫败了银河队的阴谋后,你就可以去挑战四天王和冠军竹兰了。值得一提的是,如果你拥有《传说 阿尔宙斯》的存档,并且完成了特定的条件,就能在枪之柱通过“天界之笛”遇到创世神阿尔宙斯!
  • 《传说 阿尔宙斯》:穿越到古代的洗翠地区,揭开创世神话与时空裂缝之谜!:你意外地从现代穿越到了比神奥地区更古老的“洗翠地区”,加入了银河队的调查组,肩负起了制作洗翠地区第一本宝可梦图鉴的重任。你需要去平息那些因为时空裂缝的影响而变得狂暴的“王”或“女王”宝可梦。你会和崇敬帝牙卢卡的金刚队以及崇敬帕路奇亚的珍珠队进行各种互动。最终,你会揭开时空裂缝出现的真相——原来是反派望罗在幕后操控骑拉帝纳,企图篡改历史!你需要和创世神阿尔宙斯相遇,探寻这个世界的奥秘,并见证它与人类达成和解。在二周目,你需要捕捉到237种不同的宝可梦(除了几只幻之宝可梦),才能真正挑战阿尔宙斯,解锁最终的剧情和奖励——闪耀护符(提高遇到闪光宝可梦的几率!)。

通关小秘籍(八代老司机的独家秘方):

  1. 《剑/盾》伽勒尔冒险指南: 初始御三家里,炎兔儿(最终进化闪焰王牌,火/格斗)因为速度快、攻击高而比较受欢迎。早期探索旷野地带的时候要小心一点,等级高的野生宝可梦可能会教你做人,拿到自行车后再去深入探索会比较安全。留意旷野地带的天气变化,不同天气下出现的宝可梦也不同。积极参与极巨团体战,这是获取经验糖果和稀有宝可梦(甚至超极巨化宝可梦)的重要途径。在道馆战或者关键战斗中,合理运用极巨化往往能起到奇效。针对不同道馆的属性调整自己的队伍配置。冠军丹帝的王牌是超极巨化的喷火龙,非常强力!DLC里的“制约比试”和“极巨大冒险”是获取强力宝可梦和珍贵道具的重要途径(在极巨大冒险里,捕捉到的传说神兽的成功率是100%,但每只神兽只能抓一次哦!)。
  2. 《晶灿钻石/明亮珍珠》神奥怀旧之旅: 初始御三家里,小火焰猴(最终进化烈焰猴,火/格斗)因为早期能克制不少道馆而比较容易开荒。尽早去探索地下大洞窟,里面能挖到很多有用的东西(比如化石、进化石、心之鳞片等等),在里面摆放石像还能影响秘密基地里出现的宝可梦种类(有些稀有宝可梦需要全国图鉴解锁后才能通过石像吸引出来)。收集好各种秘传学习器,方便在地图上行动。想抓阿尔宙斯的话,你需要拥有《传说 阿尔宙斯》的存档并且完成了主线剧情,然后把《晶灿钻石/明亮珍珠》更新到1.3.0版本,回到双叶镇自己家的二楼,就能拿到关键道具“天界之笛”,通关名人堂之后,再去枪之柱就能触发捕捉阿尔宙斯的剧情了。
  3. 《传说 阿尔宙斯》洗翠开拓生存手册: 掌握好投掷精灵球的技巧,从背后偷袭(也就是“背刺”)可以提高捕捉的成功率,善用烟雾弹和各种诱饵能让你事半功倍。积极完成图鉴上的各种调查课题,提升自己的队员星级,这样才能解锁更高级的道具配方和前往新的区域。灵活运用战斗中的“迅疾”模式(出手快但威力稍低)和“刚猛”模式(出手慢但威力巨大)。在面对那些强大的头目宝可梦以及“王”或“女王”宝可梦时,注意躲避它们的攻击,等它们力竭或者露出破绽的时候再进行对战或者投掷镇宝。记得去收集散落在地图各处的107个幽火(在祝庆村接任务后开启),收集齐了就能在红莲湿地的楔石旁边捕捉到花岩怪。还有散落在各地的28个未知图腾也别忘了收集(可以参考随意遗迹里的线索)。完成那个有点烦人的“顽皮大牙狸”系列任务,可以获得“奋斗岩石”(用来提升宝可梦的努力值)。如果你有《剑/盾》的存档,可以解锁捕捉谢米的调查任务;如果你有《晶灿钻石/明亮珍珠》的存档,则可以解锁捕捉达克莱伊的调查任务。

Switch上的真·开放世界来了!第九世代(2022年至今):朱、紫已上线,传说 Z-A 敬请期待!

第九世代,宝可梦系列在Nintendo Switch平台上终于迈入了玩家们期待已久的真·开放世界时代!这带来了前所未有的自由冒险体验,全新的宝可梦和酷炫的战斗机制,也预示着这个经典系列未来拥有更加广阔的探索边界!准备好迎接帕底亚地区的热情了吗?还有未来密阿雷市的都市传说!

  • Switch上的开放世界新篇章 (Nintendo Switch平台):

    • 《朱/紫》:全球同步!2022年11月18日。
    • DLC《零之秘宝》也陆续上线:“前篇:碧之假面”是2023年9月13日;“后篇:蓝之圆盘”是2023年12月14日;还有一个“番外篇:麻吉麻吉团团乱”在2024年1月11日。内容管饱!
    • 《传说 Z-A》(Mega进化回归!):预计2025年秋季发售,平台是Switch(或者任天堂的下一代主机?搓手期待!)。
  • 评价与影响?开放世界香疯了!但技术优化还得加把劲啊!

    • 口碑与销量,冰火两重天?
      • 《朱/紫》(Metacritic评分在71-72分之间,首周销量就突破了1000万份,成为了系列第二畅销的作品,仅次于初代!):大胆的开放世界设计、多线叙事的自由度(IGN甚至给了9分的高分!)以及魅力十足的角色塑造都获得了玩家们的好评。全新的“太晶化”系统也为对战策略增添了不少乐趣。但是!发售初期因为大量的技术问题、糟糕的性能表现(掉帧、卡顿、各种BUG满天飞)以及画面细节上的不足,也受到了玩家们猛烈的批评。可以说是爱恨交加了。
      • 《传说 Z-A》:目前还只是个饼,但光是“Mega进化回归”这一点,就足以让无数老玩家热泪盈眶,期待值拉满了!如果还能延续《阿尔宙斯》那种颠覆性的实时战斗系统,那简直是王炸!
    • 奠定了未来的基调,影响深远没跑了!
      • 真·开放世界的成功尝试,为宝可梦系列未来的发展确立了一个全新的方向。
      • 多线叙事和更加成熟的剧情主题,进一步拓展了宝可梦故事的深度和广度。
      • “太晶化”系统成功地革新了对战环境,带来了更多变数。
      • 《朱/紫》在商业上的巨大成功,也证明了玩家们对于核心体验的追求和对大胆创新的渴望。
      • 如果《传说 Z-A》真的能完美融合实时战斗和Mega进化这两大亮点,那将标志着宝可梦系列玩法上的又一次重大突破!
      • 第九世代通过真正的开放世界和未来可能实现的更自由的实时战斗,为后续的作品(我们有理由期待画面更好、玩法更自由、动作元素更强的全3D大作!)奠定了坚实的基础。

游戏版本与激动人心的未来展望:帕底亚地区的自由奔放与密阿雷市的华丽重塑!

  • 《朱/紫》:在帕底亚地区展开自由探险,感受过去与未来的激烈碰撞!:2022年11月18日全球同步发售。这是系列首次采用真正意义上的无缝开放世界设计,你可以自由决定冒险的顺序,想先打哪个道馆,想先走哪条路,全凭你心意!
    • 版本差异与鲜明主题: 《朱》版的主题是“过去”(你的坐骑是原始形态的故勒顿,遇到的悖谬宝可梦也都是古代种);《紫》版的主题则是“未来”(你的坐骑是充满科技感的密勒顿,遇到的悖谬宝可梦也都是未来种)。你所就读的学院(橘子学院或葡萄学院)和遇到的关键教授(奥琳博士或弗图博士)也会因版本而异。你的坐骑宝可梦会通过完成“传说之路”的剧情,逐步解锁冲刺、爬墙、滑翔、游泳这些非常实用的骑乘能力。
    • 付费DLC《零之秘宝》,内容丰富到爆炸!
      • “前篇:碧之假面”:带你前往一个充满日式乡村风情的“北上乡”参加夏令营,在那里你会遇到全新的宝可梦(比如可爱的斯魔茶、还有被称为“宝伴”的够赞狗、愿增猿、吉雉鸡,以及核心的传说宝可梦厄鬼椪),并揭开一段类似桃太郎的古老传说。还新增了“毒锁链”这个强力的新特性。
      • “后篇:蓝之圆盘”:你将以交换生的身份,前往一个建在海底的巨大生态巨蛋——“蓝莓学园”进行学习和冒险。这里有全新的海底生态环境(分成了四个不同的区域),还有更强大的训练家等着你去挑战。传说中的宝可梦“太乐巴戈斯”(普通属性,拥有三种不同的形态)也会在这里登场。还新增了超过230只历代宝可梦回归,以及全新的悖谬宝可梦。
      • “番外篇:麻吉麻吉团团乱”:你会和妮莫这些好朋友一起重返北上乡,揭开幻之宝可梦“桃歹郎”的神秘面纱。
  • 《传说 Z-A》:重塑卡洛斯地区的繁华都市,Mega进化的荣光再次降临!:这是《宝可梦传说》系列的第二部作品,计划在2025年秋季登陆Switch(或者任天堂的下一代主机?)。
    • 舞台与主题,令人期待: 故事设定在第六世代的卡洛斯地区,但聚焦于“都市再开发计划”下的核心城市“密阿雷市”,目标是打造一个人与宝可梦和谐共存的未来都市。一家叫做“阔星公司”的企业正在推动这项开发计划,并且设立了“野生特区”。这预示着游戏可能会展现城市建筑与自然生态空间的深度融合。
    • 核心机制大胆猜想: Mega进化已经确认回归! 这绝对是最大的惊喜!而且很可能还会有新的调整和变化(有传言说可能会包含基格尔德、雪妖女等27种全新的Mega形态,卡洛斯地区的御三家或许也能Mega进化了!)。战斗系统方面,可能会引入训练家和宝可梦都可以进行实时行动的模式,你可以操控角色移动、释放招式,甚至利用地形来进行战斗,策略性将大大提升!城市建设和生态系统的发展,也可能会影响剧情的走向。联机模式或许还支持合作建设城市,动态的天气系统也可能会影响宝可梦的捕捉。
    • 剧情与角色猜想,脑洞大开: 故事可能会从一个Z级训练家开始,你需要参加“ZA登峰战”,目标是成为A级训练家,途中可能会挑战一个叫做“类星队”的组织(据说里面有前闪焰队的成员茉蜜姬)。登顶后或许可以实现一个愿望,而类星队的目的可能与城市的能源危机有关。目前已知的角色有AZ(那个活了3000年的国王)、劲敌盖伊/塔霓、企业家捷朵以及她的秘书马斯卡托。
    • 初始伙伴会是谁?拭目以待: 可能会是来自不同世代的经典御三家,比如菊草叶、火球鼠(或者暖暖猪)、小锯鳄(或者波加曼、藤藤蛇)等等,一切皆有可能!

第九世代特色:103+位新伙伴闪亮登场,热情奔放的帕底亚,闪闪惹人爱的太晶化!

  • 全新的宝可梦阵容与更加真实的生态设计: 《朱/紫》首发就带来了103只全新的宝可梦(新的御三家是可爱的“新叶喵”、憨憨的“呆火鳄”、以及优雅的“润水鸭”),后续的DLC还在持续增加新的伙伴。整个第九世代总共新增了120只宝可梦,包括了帕底亚地区的地区形态(比如毒/地面属性的乌波,跟原来的水/地面属性完全不同!)、还有一些被称为“趋同进化”的奇特宝可梦(比如海地鼠,长得像地鼠但其实是水属性的鳗鱼!)、以及来自过去或未来的神秘“悖谬宝可梦”(比如古代种的吼叫尾,未来种的铁斑叶,设计都非常独特!)。野生宝可梦的行为也更加贴近它们的生活习性,会根据周围环境做出动态的反应,族群的首领还会保护弱小的同伴,让整个世界感觉更加生动和真实。代表性的新宝可梦有可以当做交通工具的摩托蜥、电系道馆馆主奇树的王牌电肚蛙、象征着双生与陪伴的墓仔狗和狗仔包、拥有四种不同面具形态的厄鬼椪、以及拥有三种形态的太乐巴戈斯(它的星晶形态甚至可以消除天气和场地效果!)。
  • 帕底亚地区(Paldea Region),充满伊比利亚半岛的热情与神秘: 灵感来源于现实生活中的伊比利亚半岛(也就是西班牙和葡萄牙所在的地区),所以整个地区的城镇名称大多都跟厨具有关,充满了生活气息。这里有广阔的草原、美丽的湖泊、险峻的高山、荒凉的土地等多种多样的地貌,不同区域的气候差异也非常显著,甚至会影响到某些宝可梦的形态(比如四季鹿在帕底亚地区就有新的样子)。整个世界是无缝连接的开放世界,你可以自由地探索每一个角落。位于地区中央的巨大坑洞——“帕底亚巨坑”(也被称为第零区),是古代帝国的禁区,蕴藏着太晶化的秘密,也是人类第一次发现太晶化现象的地方。地区内还有十大美景等着你去打卡,解锁后还能作为传送点,非常方便。
  • 太晶化现象(Terastal Phenomenon),宝可梦变身闪亮水晶灯,属性大挪移!:这是第九世代全新的战斗机制,超级酷炫!所有的宝可梦都可以进行太晶化。太晶化后,宝可梦的身体会呈现出宝石般的光泽,头顶还会出现一个代表太晶属性的华丽“太晶宝石”。
    • 机制是这样的: 每只宝可梦天生就有一个固定的“太晶属性”(总共有18种,对应18种基本属性,而且这个太晶属性可以跟宝可梦原来的属性完全不同!)。当宝可梦进行太晶化后,它的属性会临时变成它的太晶属性,从而改变自身的弱点,并且能大幅强化与太晶属性相同属性的招式威力(还有一个叫做“太晶爆发”的特殊招式,威力固定为80,属性会随着你的太晶属性而改变)。比如说,一只原本是火属性的宝可梦,如果它的太晶属性是飞行系,那么它太晶化后就能利用飞行系的招式“杂技”打出非常高的伤害,出奇制胜!
    • 怎么发动呢?:你需要使用一个叫做“太晶珠”的道具,在一场战斗结束后,太晶珠的能量就会耗尽,需要去宝可梦中心或者在野外找到太晶能量结晶的地方进行充能(不过在某些特殊的战斗中,太晶珠会自动充能)。
    • 太晶团体战,组队打Boss!:你可以和最多三个其他玩家联机合作,一起挑战野外那些强大的、太晶化了的野生宝可梦。成功击败它们后,不仅能获得各种稀有的道具,还有机会捕捉到拥有特殊太晶属性的宝可梦。
    • “星晶属性”,第19种神秘太晶!:在DLC《蓝之圆盘》中,引入了第19种全新的太晶属性——星晶属性!当宝可梦太晶化为星晶属性后,它原来的属性不会改变,但是它所有属性的招式在第一次使用时威力都会得到提升(如果是和自身原有属性相同的招式,威力会翻倍;如果是不相同的招式,威力也会提升1.2倍!),而且在攻击已经太晶化了的对手时,造成的伤害还会翻倍!传说中的太乐巴戈斯的星晶形态,甚至可能无视招式的使用次数限制,持续强化自身!
  • “Let’s Go”功能与温馨的野餐时光: 队伍里的第一只宝可梦可以跟在你屁股后面跑,或者你可以让它开启“Let’s Go”模式,自主地去攻击野外的宝可梦或者捡拾地上的道具。你还可以随时随地在野外进行野餐,和你的宝可梦们进行各种互动,制作各种口味的三明治(不同的三明治会有不同的增益效果,比如提高遇到特定属性宝可梦的几率,或者提高遇到闪光宝可梦的几率!),还能在野餐的时候孵蛋!
  • 联盟集友圈,和小伙伴一起浪!:最多可以有四个玩家在同一个世界里进行联机探索,你们可以一起拍照留念,一起挑战太晶团体战,共享开放世界的乐趣。

剧情大概是这么个事儿(《朱/紫》):一场自由的寻宝之旅,揭开第零区的神秘面纱!

你扮演一名刚刚转学到帕底亚地区橘子学院(如果你玩的是朱版)或葡萄学院(如果你玩的是紫版)的学生,参加了一项叫做“寻宝”的特别课外活动。在这个活动中,你可以自由地探索整个帕底亚地区,并且有三条可以并行推进的主线故事等着你:

  1. 冠军之路: 挑战分布在帕底亚地区各地的八个道馆(这一代的道馆测试花样百出,比如有滚橄榄球的,有捉迷藏的,非常有意思!),通过最后的冠军测验,成为一名真正的冠军级训练家,最终挑战帕底亚地区实力最强的首席冠军也慈。你的劲敌妮莫会一直陪伴着你,和你一起成长,共同进步。
  2. 传说之路: 你会遇到一位叫做派帕的学长,他正在寻找一种叫做“秘传调味料”的珍贵食材,为了治好他受伤的宝可梦獒教父。你需要帮助他一起挑战守护着这些秘传调味料的、体型巨大的“宝主宝可梦”。在这个过程中,你会了解到派帕和他心爱的獒教父之间那段感人至深的故事。完成这条主线后,你的坐骑故勒顿或密勒顿就能解锁全部的骑乘能力,让你在帕底亚地区上天入地,无所不能!
  3. 星尘★之路: 对抗在校园里制造麻烦的“天星队”(他们其实是一群因为受到校园霸凌而逃学的学生组成的,在帕底亚各地设立了五个不同属性的帮班据点)。你需要逐个挑战这些据点的老大(在挑战老大之前,可以用“Let’s Go”模式快速清理掉他们的小喽啰),并最终揭开天星队成立的真相以及他们幕后的大姐头“仙后”(其实就是你的同班同学牡丹)的真实身份。你们的校长克拉韦尔还会化名为“聂凯”,偷偷地在暗中帮助你。结局是天星队的成员们获得了大家的理解和接纳,并且运营起了一个叫做“天星训练中心”的地方,帮助其他有困难的学生。

当你完成了这三条主线故事后,就会开启最终的篇章——“归乡之路”:你会和妮莫、派帕、牡丹这三位好朋友一起,前往帕底亚地区中央那个神秘的巨大坑洞——“第零区”。在那里,你们会遇到来自古代或未来的悖谬宝可梦,并最终揭开奥琳博士(朱版)或弗图博士(紫版)研究的真相(他们企图通过太晶技术复活生活在古代或未来的宝可梦,但因为能量失控而引发了巨大的危机)以及太晶化现象的真正起源。你需要化解由博士的人工智能引发的危机,守护帕底亚地区的和平。整个剧情深刻地刻画了成长、友情和亲情这些动人的主题。

后续的DLC《零之秘宝》则会延续主线的剧情,带你前往全新的地区“北上乡”和海底的“蓝莓学园”,在那里你会遇到更多新的宝可梦和角色,并解开关于厄鬼椪、太乐巴戈斯这些传说宝可梦的更多谜团。

通关小秘籍(九代萌新和老鸟都看过来!):

  1. 《朱/紫》帕底亚冒险指南: 建议优先推进“传说之路”的剧情,尽早解锁坐骑的全部能力,这样探索地图会方便很多。初始的御三家(新叶喵-草,呆火鳄-火,润水鸭-水)都各有各的优点,选哪个看你喜好啦!一周目通关的话,推荐培养像骨纹巨声鳄(呆火鳄最终进化,火/幽灵,特攻高)、海豚侠(波普海豚进化,水,特性变身后种族值爆炸)、弃世猴(火爆猴使用特定招式后升级进化,格斗/幽灵,非常强力)这些比较好用的宝可梦。太晶属性如果和宝可梦原来的属性一致,招式威力会更强;当然,你也可以选择一些能够改变自身弱点的太晶属性来出奇制胜。“太晶爆发”这个招式可以补充队伍的打击面,非常实用。在锦汇镇的食堂里,可以消耗太晶碎片来改变宝可梦的太晶属性。别忘了关注宝可梦的个体值(可以通过极限特训来提升)、努力值(可以通过定向击败特定的野生宝可梦来刷)和性格修正(可以使用薄荷来改变性格带来的能力值变化)。有些宝可梦的进化方式比较特别(比如赛富豪需要收集999个索财灵的硬币才能进化,弃世猴则需要使用20次“愤怒之拳”这个招式后再升级才能进化)。建议在游戏设置里关闭自动存档功能,这样方便在关键时刻进行SL(存档/读档)。多利用地图上的飞行出租车和帕底亚十景这些传送点,可以节省不少跑路的时间。留意地图上出现的“大量出现”事件,这是刷稀有宝可梦和闪光宝可梦的好机会!
  2. 《传说 Z-A》前瞻(大胆猜想,不一定对!): 如果真的采用了实时战斗系统,那么灵活的走位和对地形的利用可能会变得非常重要。Mega进化的回归,无疑会极大地改变对战的战术格局。城市建设的元素如果加入,可能会影响剧情的走向以及某些宝可梦的出现。密阿雷市标志性的棱镜塔挑战,可能会设计成分层的高难度挑战,需要你同时考虑Mega进化和属性克制。羁绊值(类似《X/Y》里的友友乐系统)或许会影响Mega进化的效果或者在战斗中触发一些特殊的爆发。
  3. 版本选择建议,总有一款适合你!
    • 《朱/紫》: 朱版更侧重“过去/复古”的主题(坐骑是故勒顿,遇到的悖谬宝可梦也都是古代种);紫版则更侧重“未来/科技”的主题(坐骑是密勒顿,遇到的悖谬宝可梦也都是未来种)。不同版本会有一些限定的宝可梦,需要通过联机交换才能集齐图鉴。这款游戏非常适合喜欢自由探索和体验全新机制的玩家。
    • 《传说 Z-A》: 如果实时战斗和城市建设真的能实现,那绝对会带来前所未有的全新体验。Mega进化的回归,对于老玩家来说更是情怀满满。
    • 新手玩家: 建议可以先从《朱/紫》入手,熟悉一下宝可梦的基本玩法和太晶化这个新系统,然后再去挑战《传说 Z-A》这种可能操作要求更高的实时战斗游戏。
    • 对战爱好者: 深入研究《朱/紫》的个体值、努力值、太晶属性系统,打造你的最强战队!密切关注《传说 Z-A》关于Mega进化的各种信息,提前构思新的战术和队伍配置。
    • 剧情爱好者: 《传说 Z-A》关于密阿雷市重建的剧情以及可能会出现的新老角色的支线故事,都非常值得期待!

核心玩法

每个训练家的冒险故事,都从那个激动人心的时刻开始——从慈祥的博士或者某个重要人物手里,接过你人生中的第一只宝可梦!通常呢,会有三只小可爱摆在你面前,分别是草属性、火属性、水属性的“御三家”。选哪个?这可不只是挑个顺眼的那么简单!你的选择,不仅会影响你游戏初期的“开荒”难度(比如选了草系,出门遇到一堆虫系,那可就酸爽了!),还会通过属性克制,跟你那个相爱相杀的劲敌建立起一种“不是你秒我,就是我秒你”的奇妙关系。从这一刻起,你就得开始琢磨队伍的属性搭配了,不然出门就得挨揍!当然,有些作品也玩点新花样,比如《皮卡丘》版直接塞你一只皮神当初始伙伴,没得选;或者像《朱/紫》那样,得先在学院里上完几节课才能正式领养。但不管怎么说,核心目的都是一样的:让你跟你第一只宝可梦建立起深厚的情感羁绊(毕竟是“初恋”嘛!),也为你后续那漫长又刺激的捕获冒险打下坚实的基础!

抓啊抓啊我的宝可梦,填满那本厚厚的图鉴:Gotta Catch ‘Em All! 这口号可不是白喊的!

满世界溜达,发现并抓住那些数以百计、千奇百怪的野生宝可梦,这绝对是宝可梦最核心的玩法之一!这也是你扩充队伍、完成图鉴,最终成为宝可梦大师的关键一步!

抓宠也是技术活儿,精灵球可不是随便扔的!

  • 怎么才能遇到它们?:早期的时候,你得在草丛里、山洞里这些地方瞎转悠,随机遇敌,跟开盲盒似的。后来就进步了,搞了个“明雷”机制,宝可梦直接在地图上大摇大摆地走来走去,你可以看准了再上去“碰瓷儿”进入战斗,省了不少事儿!
  • 想抓它?先把它打个半死!:遇到心仪的野生宝可梦,别急着扔球!先跟它大战三百回合,把它的HP削到残血状态(血条变红了就差不多了),但千万别手抖把它给打死了,不然就白忙活了!
  • 瞅准时机,扔球!:把它打得奄奄一息之后,就可以潇洒地掏出精灵球,瞄准,发射!能不能抓住,那就看天意,也看你的技术和策略了。
  • 啥玩意儿影响抓宠成功率?玄学还是科学?
    • 宝可梦的“身价”:每种宝可梦都有个隐藏的“捕获率”数值,这个数值越高,就越容易抓住。那些传说中的神兽,捕获率通常低到令人发指,没点耐心和运气基本没戏。
    • 血量越低,机会越大:道理很简单,把它打得越惨,它就越没力气反抗,自然就更容易被你收服。
    • 给它点“颜色”看看!:如果能让它陷入睡眠或者冰冻状态,捕获率通常能翻倍!中毒、烧伤、麻痹这些异常状态也能提升大概1.5倍的成功率。所以,带上会催眠或者麻痹技能的宝可梦,绝对是抓宠路上的好帮手!
    • 球不对,努力白费!:不同的精灵球效果可差远了!最基础的是“精灵球”(1倍捕获率),稍微好点的是“超级球”(1.5倍),再高级点的是“高级球”(2倍)。还有那个传说中的“大师球”,100%成功,一球超人,但通常整个游戏里也就那么一两个,珍贵得很,用在谁身上可得想好了!除此之外,还有各种针对特定情况的特殊球,比如“捕网球”抓水系和虫系宝可梦效果更好,“黑暗球”在山洞里或者晚上用成功率更高,“先机球”在战斗开始的第一回合扔出去有高达5倍的捕获率,简直是偷袭神器!
  • 宝可梦图鉴,你的“集邮册”!:每当你成功捕捉到一种新的宝可梦,它的详细信息就会被记录到你的图鉴里,包括它的属性、特性、生活习性等等。把图鉴填满,收集齐所有的宝可梦,是很多训练家毕生的追求和终极目标之一!
  • 抓宠也有新花样!:有些作品还引入了一些特殊的捕获机制。比如在《Let’s Go!》系列里,搞了个“连锁捕捉”,连续捕捉同一种宝可梦,就能提高遇到闪光宝可梦和高个体值宝可梦的概率。在《传说 阿尔宙斯》里,下雨天更容易遇到闪光的头目宝可梦。说到闪光宝可梦(就是颜色跟普通不一样的稀有品种),那可是可遇不可求的存在!遇到的概率极低(大概是1/8192,后来有些版本提高到了1/4096),很多肝帝为了刷一只闪光,不惜SL(存档/读档)大法用到天荒地老!

养育与进化:见证奇迹的时刻,丑小鸭也能变天鹅!

抓到手可不是结束,那仅仅是开始!通过精心的训练和培养,提升你宝可梦的能力,让它们变得更强,并且在某个激动人心的时刻进化成更加强大、更加帅气的形态,这绝对是养成玩法中最核心的乐趣所在!

  • 经验与升级,打怪就能变强!:带着你的宝可梦去参加战斗,只要打赢了对手(或者成功捕捉到野生宝可梦),就能获得经验值(EXP)。经验值积累到一定程度,你的宝可梦就会升级啦!升级不仅能提升它们的基础能力(HP、攻击、防御、特攻、特防、速度),还有可能学会新的、更强力的招式。如果想让它们升级快一点,可以给它们带上“经验分享器”,或者直接喂它们吃“神奇糖果”(这玩意儿能直接升一级,但别乱用哦!)。
  • 能力值解密,你的宝可梦到底有多强?:一只宝可梦最终能有多强,可不是简单看等级就行的,它受到好几种隐藏因素的影响:
    • 种族值,爹妈给的天赋上限!:每一种宝可梦天生就有一个固定的“种族值”,这个数值决定了它们在各项能力上的基础强度和成长潜力。比如快龙的种族值总和就比鲤鱼王高得多,这是娘胎里带的,没办法。
    • 个体值(IVs),天生的骨骼惊奇度!:同一窝生的宝可梦,能力也可能有天壤之别,这就是个体值的功劳!每只宝可梦从蛋里孵出来的那一刻,它的六项能力(HP、攻击、防御、特攻、特防、速度)就有个隐藏的“天赋点”,范围是从0到31。这个数字越高,就代表它在这项能力上的潜力越大,上限也越高。所谓“6V”,指的就是六项个体值全是31的极品中的极品,可遇不可求啊!想追求完美?那就得靠疯狂孵蛋(记得带上“红线”这个遗传神器!),或者在后期的游戏里用珍贵的“金色王冠”给它“强行镀金”了。
    • 努力值(EVs),后天的肌肉锻炼!:光有天赋还不行,后天的努力更重要!你的宝可梦通过打败特定的对手,或者吃一些特殊的“补品”(比如各种口味的营养饮料、或者那些神奇的小羽毛),就能获得努力值。这些努力值可以直接提升对应的能力(在100级的时候,通常每4点努力值就能提升1点具体的能力值)。不过要注意,每项能力的努力值上限是252点,所有能力加起来的总上限是510点。所以,怎么合理分配这些努力值,是扬长避短还是全面发展,就看你的战术需求了!比如经典的“252攻速+6HP”加点法,就是要把优势发挥到极致!
    • 性格,天生的脾气秉性,也影响战斗力!:大部分宝可梦天生就有一种性格,这个性格会影响它的能力值成长(通常是一项能力提升10%,另一项降低10%)。选对了性格,能让你的宝可梦在战斗中如虎添翼!
  • 进化!进化!见证奇迹的时刻!:当你的宝可梦满足了特定的条件后,就会在一阵华丽的光芒中进化成全新的形态!进化后不仅能力会大幅提升,有时候连属性和特性都可能发生改变,简直是脱胎换骨!
    • 等级进化,最普遍的方式: 很多宝可梦只要等级达到一定程度就会自动进化,比如我们熟悉的小火龙16级就能进化成火恐龙。
    • 道具进化,一块石头引发的质变: 有些宝可梦需要使用特定的进化道具才能进化,比如给卡蒂狗用一块“火之石”,它就能进化成威武的风速狗。
    • 通信交换进化,友谊的见证(也是奸商的套路): 有些宝可梦特别傲娇,非要跟朋友交换一下才肯进化,比如鬼斯通交换后就能进化成强大的耿鬼。还有些更麻烦,不仅要交换,还得让它带着特定的道具才能进化,比如大岩蛇带着“金属膜”交换后就能进化成帅气的钢系大钢蛇。
    • 亲密度进化,你对它好,它就用进化回报你!:有些宝可梦需要跟你的亲密度达到一定的程度后,再升一级才能进化,比如可爱的利欧路进化成帅气的路卡利欧,就得看你平时对它好不好了!
    • 特定地点/条件进化,花样百出: 有些宝可梦的进化条件非常奇葩,比如伊布需要在特定的岩石(青苔岩石或冰雪岩石)附近升级才能进化成叶伊布或冰伊布;有些则需要学会某个特定的招式后再升级才能进化(比如甜舞妮学会“踩踏”后就能进化成甜冷美后);甚至还有些需要满足一些匪夷所思的动作(比如伽勒尔地区的大葱鸭,在一场战斗中打出三次会心一击后就能进化成葱游兵,这操作也是绝了!)。
    • 分支进化,选择困难症的噩梦: 有些宝可梦有多条不同的进化路径,最典型的就是伊布了,它能进化成各种不同属性的形态,让你挑花了眼!

属性克制是王道

把辛辛苦苦培养出来的宝可梦拉出来练练手,检验一下它们的实力和你的战术水平,这绝对是宝可梦游戏中最刺激、也最核心的环节!无论是跟野生的愣头青打,还是跟其他雄心勃勃的训练家一决高下,都充满了变数和乐趣!

  • 经典的回合制战斗,你来我往: 双方轮流选择招式进行攻击,或者选择使用道具、替换宝可梦等其他行动。通常情况下,速度快的宝可梦会先出手(不过有些招式比如“电光一闪”具有优先攻击的特性)。当一只宝可梦的HP降到0的时候,它就“濒死”了,需要换上队伍里的其他宝可梦继续战斗。如果己方所有的宝可梦都濒死了,那很遗憾,你就输了。
  • 招式花样繁多,效果各异: 每只宝可梦最多只能同时掌握4个招式,这些招式不仅能造成伤害,还可能附带降低对手能力、造成各种异常状态(比如中毒、麻痹、烧伤、睡眠、冰冻)、改变天气或场地效果等等,组合起来千变万化!
  • 属性克制,以弱胜强的法宝!:宝可梦世界里总共有18种不同的属性,每只宝可梦和每个招式都有自己的属性。这些属性之间存在着非常复杂又有趣的克制关系,理解并运用好属性克制,是取得胜利的关键!
    • 效果绝佳,打你个措手不及!:如果你的招式属性克制对手的宝可梦属性,就能造成双倍的伤害!如果对手的宝可梦是双属性,而且两个属性都被你的招式克制,那伤害更是高达四倍!比如火系招式打草系宝可梦,那叫一个疼!
    • 效果不理想,刮痧都没这么温柔!:如果你的招式属性被对手的宝可梦属性抵抗,那造成的伤害就会减半。如果对手是双属性,而且两个属性都抵抗你的招式,那伤害就只剩四分之一了,简直是给它挠痒痒。比如用水系招式打草系宝可梦,效果就很差。
    • 没有效果,完全免疫!:有些属性的招式对特定属性的宝可梦是完全无效的,比如地面系的招式就打不到飞行系的宝可梦(除非用了某些特殊的招式或者道具)。
    • 双属性叠加计算,烧脑又刺激!:在计算伤害的时候,需要同时判断招式的属性和对手宝可梦的两个属性(如果它是双属性的话)之间的克制关系,最终的伤害倍率是叠加计算的,非常考验你的数学能力和临场判断!
  • 特性(Abilities),每个宝可梦的“被动技能”!:每只宝可梦都有自己独特的特性,这些特性就像是被动技能一样,可以在战斗中自动生效,比如提升招式的威力、免疫某些异常状态、改变自身或对手的能力等等,是战斗策略中非常重要的一环!
  • 特殊战斗机制,让战斗更酷炫!(部分世代限定)
    • 极巨化(第八代《剑/盾》): 宝可梦可以变得像奥特曼一样巨大!HP大幅提升,招式也变得更强力!不过只能持续三个回合,而且每场战斗只能用一次,用在谁身上,什么时候用,可得想好了!
    • Z招式(第七代): 消耗一块Z纯晶,就能发动一次威力毁天灭地的超强Z招式!每个属性都有对应的Z招式,有些宝可梦还有专属的Z招式,演出效果超级华丽!同样是每场战斗只能用一次。
    • 太晶化(第九代《朱/紫》): 宝可梦可以临时改变成预先设定好的“太晶属性”,并且全身散发出宝石般的光芒!太晶化后不仅能强化该属性的招式威力,还能用出一个叫做“太晶爆发”的特殊招式,属性会随着你的太晶属性而改变,非常灵活!
  • 进阶战术,从萌新到大神之路!:除了基本的属性克制,还有很多更高级的战术玩法,比如用“接力棒”招式把强化效果传递给队友;组建“天气队”来控制战场环境;利用“戏法空间”来反转速度的顺序,让慢速宝可梦也能先手出招等等,玩法深度无穷无尽!

想让你的宝可梦从“路人甲”变成“赛场明星”?光知道属性克制那可远远不够!你必须掌握那些决定宝可梦真正潜能的高级隐藏机制!它们才是你构筑强大战队、在PVP对战中运筹帷幄、最终制霸全场的基石!想从入门到精通?那就赶紧往下看!

  • 个体值 (IV),江湖人称“天生的骨骼惊奇度”!:每只宝可梦从蛋里孵出来的那一刻,它的六项能力(HP、攻击、防御、特攻、特防、速度)就有个隐藏的“天赋点”,范围是从0到31。这个数字越高,就代表它在这项能力上的潜力越大,上限也越高。所谓“6V”,指的就是六项个体值全是31的极品中的极品,可遇不可求啊!想追求完美?那就得靠疯狂孵蛋(记得带上“红线”这个遗传神器!),或者在后期的游戏里用珍贵的“金色王冠”给它“强行镀金”了。
  • 努力值 (EV),俗称“后天的肌肉锻炼”!:光有天赋还不行,后天的努力更重要!你的宝可梦通过打败特定的对手,或者吃一些特殊的“补品”(比如各种口味的营养饮料、或者那些神奇的小羽毛),就能获得努力值。这些努力值可以直接提升对应的能力(在100级的时候,通常每4点努力值就能提升1点具体的能力值)。不过要注意,每项能力的努力值上限是252点,所有能力加起来的总上限是510点。所以,怎么合理分配这些努力值,是扬长避短还是全面发展,就看你的战术需求了!比如经典的“252攻速+6HP”加点法,就是要把优势发挥到极致!

简单来说,个体值决定了你的宝可梦能跳多高(潜力上限),努力值决定了它往哪个方向跳,以及跳得有多努力(能力强化)!

  • 性格 (Nature),宝可梦天生的“脾气秉性”,也是能力的催化剂!:总共有25种不同的性格,大部分性格都会让宝可梦的一项能力提升10%,同时让另一项能力降低10%(当然也有一些性格是不增不减的“白板”)。选对了性格,比如给物攻手选“固执”(加物攻,减特攻)或者给速度型选手选“爽朗”(加速度,减特攻),就能让宝可梦的优势更加突出,在关键时刻先发制人或者一击制胜!
  • 特性 (Ability),每个宝可梦独特的“被动天赋”和“独门绝技”!:每只宝可梦都拥有至少一种特性,这些特性会在战斗中自动生效,效果五花八门!比如有些特性像“威吓”,出场就能降低对手的物攻;有些特性像“引火”,不仅能免疫火系招式,还能强化自身的火系招式威力!更有一些非常强力或者效果独特的隐藏特性(也叫梦特性),往往能带来意想不到的战术价值!理解并巧妙地活用各种特性,是你在对战中扭转战局、出奇制胜的法宝!

性格和特性,就像是宝可梦的“出厂个性设置”和“自带的独门武功秘籍”,直接影响着你的战术选择和它在战场上的发挥!

当你拥有了一只潜力无限的宝可梦之后,合理的招式搭配灵活的战术运用才是让你立于不败之地的王道!

  • 招式配置,打造你的专属“技能库”!:每只宝可梦最多只能学4个招式,怎么选可是个大学问!首先要考虑本系加成 (STAB),就是让宝可梦使用和自身属性相同的招式,这样威力会更大!其次要追求打击面,就是让你的招式组合能克制尽可能多的不同属性的对手。除此之外,还可以搭配一些强化招式(比如“剑舞”大幅提升物攻,“诡计”大幅提升特攻)或者变化招式(比如让对手中毒的“剧毒”,提升全队速度的“顺风”)来主导战局的节奏。
  • 竞技核心,预测与博弈的心理战场!:PVP对战可不仅仅是数值上的比拼,更是一场激烈的心理战!你需要根据对手的阵容和可能的行动,来预测他的下一步操作,并做出相应的应对。常见的对战流派有追求速度和爆发的速攻流、依靠回复和异常状态慢慢磨死对手的消耗流(也叫受队)、利用特定天气或场地效果来强化队伍的天气队/场地队,还有颠覆速度规则、让慢速宝可梦也能先手攻击的空间队等等。在构建队伍的时候,你需要仔细考虑属性之间的互补关系、每个宝可梦在队伍中的角色定位(比如是物攻手、特防盾牌、还是辅助手等等),以及携带什么道具(比如能提升招式威力的“生命宝珠”、能保证存活一回合的“气势披带”等等)才能发挥出最大的效果。

商业版图

宝可梦,到底是怎么从一个简简单单的抓宠游戏,“进化”成一个席卷全球、让无数人心甘情愿掏空钱包的文化符号的?它那惊掉下巴的商业版图、无孔不入的文化渗透力、还有那群数量庞大、能量爆棚的粉丝团,以及越来越火爆的电子竞技生态……这一切的一切,都在告诉我们:这只电气老鼠(以及它的小伙伴们)长盛不衰的秘密,可深着呢!

  • 钞能力MAX!宝可梦的“印钞机”是怎么炼成的?
    说到赚钱,宝可梦说第二,谁敢说第一?这小东西的IP总收入已经超过了一千亿美元!啥概念?就是富可敌国,让多少世界五百强都得跪下唱征服!光是咱们玩的主系列游戏,就卖出去了超过4.8亿份,简直是人手N份的节奏!还有那些让人欲罢不能的衍生手游,像什么出门抓精灵的《Pokémon GO》、还有最近刚出的能让你在线开包的《TCG Pocket》,那吸金能力也是百亿美元级别的,恐怖如斯!更别提那个风靡全球的集换式卡牌游戏 (PTCG)了,全球发行量已经超过了648亿张!有些稀有的卡牌,那价值简直比黄金还贵,一张就能换套房,形成了一个巨大的收藏和交易市场。至于那些毛绒玩具、手办、衣服、主题商店……各种各样的周边商品和授权产品,几乎渗透到了我们生活的方方面面,每年的零售额都超过百亿美元,简直是行走的印钞机!

  • 文化病毒式传播!宝可梦已经无处不在了!
    皮卡丘、喷火龙这些经典形象,早就成了全球通用的文化符号,你能在各种大型活动上看到它们的身影,各种大牌也抢着跟它们合作。那部播了超过25年的动画片,更是陪伴了几代人的成长,风靡全球没商量!还有各种电影(比如那部真人版的《大侦探皮卡丘》,票房也相当不错呢!)、各种主题音乐(甚至还跟世界顶流的明星合作出歌!),简直是全面开花!更牛的是,它还成功跨界到了时尚圈(优衣库、Fendi这些大牌都出过联名款)、艺术圈(比如跟当代艺术家Daniel Arsham的合作,把宝可梦变成了“文物”!),引领着社交媒体上的潮流。宝可梦所代表的那种探索精神、合作精神,以及它在AR这些新技术上的应用,已经让它不仅仅是一个游戏,更变成了一种生活方式!

宝可梦能这么成功,背后那股庞大而且活跃到不行的全球粉丝社群,绝对是功不可没!

  • 粉丝力量大过天!“用爱发电”的宝可梦宇宙共建者!
    宝可梦能有今天,离不开全球那群爱到骨子里、“用爱发电”的粉丝们!
    • 脑洞大开的“二创”大军,官方都得喊佩服!:你敢信?粉丝们画的同人画、写的小说、做的手工艺品、甚至自己开发的独立小游戏,那创意和质量,有时候连官方都得竖起大拇指!这些“二创”作品不仅极大丰富了宝可梦的文化内涵,形成了一个庞大到你无法想象的“粉丝生态圈”,有时候甚至还能反过来给官方提供灵感呢!
    • 网上冲浪,全球训练家“一线牵”!:全世界的宝可梦粉丝们,通过各种各样的在线社群(比如国外的Reddit、咱们国内的贴吧、还有各种宝可梦维基百科网站,像什么Bulbapedia、52Poke Wiki这种资料库,以及Discord这种语音聊天频道,还有像Pokémon Showdown这样的在线对战平台)热火朝天地交流心得、分享喜悦、切磋技艺,构建起了一个无比紧密的全球大家庭。
    • 线下狂欢,一起为爱发电,共享热爱时刻!:官方举办的那些大型赛事(比如宝可梦世界锦标赛WCS、各种大师赛)、每年夏天让无数人走出家门的Pokémon GO Fest,还有粉丝们自发组织的各种聚会、同人展,都把线上的那份热情原汁原味地延伸到了线下,形成了一种沉浸式的社群体验。这种体验,让粉丝们不再仅仅是游戏的消费者,更成为了这个庞大IP生态的共同建设者和守护者!

说到宝可梦的竞技场景,那现在也已经发展成了一个具有全球影响力的、多层次的完整体系了!

  • 官方赛事,从草根到世界冠军,人人都有机会!:官方通过一个叫做“Play! Pokémon”的赛事体系,构建起了一条从地方小比赛一直到最高荣誉的宝可梦世界锦标赛 (WCS) 的完整竞技通路。比赛项目也多种多样,涵盖了咱们玩的视频游戏(VGC)、集换式卡牌游戏(PTCG)、还有《Pokémon GO》以及《宝可梦大集结》这些热门衍生游戏。像WCS这样的大型赛事,每年都能吸引到全球数千名顶尖选手和数百万的观众在线观看,比赛现场还会结合各种嘉年华活动,形成了一种独特的“电竞+文旅”现象。
  • 电竞直播,把宝可梦竞技的火烧得更旺!:像Twitch、YouTube这些主流的直播平台,对宝可梦电竞的普及起到了巨大的推动作用!通过直播,不仅提升了赛事本身和参赛选手的知名度,也大大降低了新玩家了解和参与竞技宝可梦的门槛,让整个社群更加活跃。同时,电竞直播还催生了大量丰富的衍生内容(比如战术分析、精彩集锦、主播整活等等)和独特的互动文化。

宝可梦风靡全球这么多年,有可爱的形象和好玩的游戏,还有精心构建的商业模式、深入人心的文化渗透、粉丝的凝聚力,竞技生态!这简直就是一个文化帝国。

憋了个大招!《戴铭的小册子》6.6.7 版:不止搞开发,这次玩儿大了!

嘿,朋友们!你那个贴心的小伙伴——《戴铭的小册子》App,最近可是搞了个大动作,迎来了 6.6.7 版本 的重磅更新!这次不只是给老本行(Apple 开发那点事儿)来了个豪华升级,更是塞进了一大堆让你意想不到的新鲜玩意儿。诸君且拭目细观:方寸天地藏乾坤,指尖轻点现琳琅,旧时雕栏玉砌处,化作九曲回廊步步生莲;寻常笔墨丹青间,彩蝶衔来异宝奇珍。似那瑶池蟠桃宴上,既有琼浆润喉,又添仙乐绕梁。

小册子的程序的代码依然是开源的:SwiftPamphletApp

所以,这个“知识宇宙”到底多了些啥新星星呢?

一开始,小册子确实是咱们 Apple 开发者的“秘密基地”,专供些弄璋的巧匠推敲些金石之语。但这次,我寻思着得“搞点事情”,于是大刀阔斧地拓展了一番。现在这 App 啊,简直是从‘小学’直接跳级到了‘大学’!内容上来了个 乾坤大挪移,从硬核的技术前沿,一路逛到温润的人文艺术。

所以啊,任你是醉心雕琢玲珑代码的玉工,痴迷破译千年密语的探骊客,还是对历史文化两眼放光的‘挖宝人’,亦或是追求生活小情调的品味家,管教你左手拈着金刚钻,右手拾得夜光珠,袖底还藏了缕未散的茶烟,新版小册子都能让你翻着翻着就嘴角上扬,找到心头好。

名字都改了,你说变化大不大?

正是因为内容不再‘偏科’,小册子的名字也理所当然地从原来的‘开发小册子’,”开发”二字轻轻卸却,正式更名为——《小册子》!莫道名号减,须知意蕴添。嗯,就是这么简洁自信。目的嘛,就是希望更多朋友能在这里发现点有用的、有趣的,不虚此‘下’(载)。倒似春雪融尽后,去尽铅华存本真。不为别的,只愿诸君掀卷时,或见智语如珠,或遇妙趣横生,总不枉此番殷勤探看。

内容嘛,墨痕犹带露,字字自生春,一直“新鲜生长”!

放心,小册子会是个‘活物’,卷中天地时时异,字里烟霞日日鲜。自有时雨润新枝,内容持续更新中!最近我不是跟家人一起录播客了嘛,聊嗨了之后留下的那些干货和思考,比如咱们一起‘侦破’的推理女王阿加莎·克里斯蒂的案子,还有对《孤独的美食家》那种‘一人食’境界的探讨,静参五郎独食禅意,我都会整理好放进小册子里。相当于给你多了个 播客伴侣+拓展阅读,声韵绕梁后的余香。

欢迎关注我们的《铭莉双收》播客小宇宙和Apple的Podcasts上都能搜到。

历史迷?这里有“时间机器”!

我还特意梳理了 中国历史的大脉络,用时间轴给你串起来,织就一幅千年锦绣图。君试观:自岐山凤鸣定周礼,至列国争锋百家立言;秦皇挥鞭收六合,汉室开疆绘鸿图;及至三分鼎立风云会,俱化作游龙戏珠跃然素卷。更有妙笔藏机杼,竟使八荒共此时——君见楚汉垓下月,可知罗马城头正扬鹰旗?觉得不过瘾?别急,若嫌纸上烟云浅,后头还备着琅嬛福地引路签。那些个太史公未尽的墨、希罗多德暗藏的卷,皆在此间候着慧眼人。一堆 参考书单,够你慢慢啃!

一梭织尽兴亡事,半卷收来天地春。欲解沧桑真面目,书山径幽待君耘。

技术宅?“硬菜”管够!

当然,老本行技术这块儿也不能落下!给技术控们也准备了‘硬菜’。这次新增了 计算机基础知识全家桶,专为解那数字乾坤的连环扣,且看时髦的玄妙机关如何参透天机:时髦的 AI 是怎么回事?老派的精微符咒(汇编)怎样金针度与绣娘;更说那长青不老松(C言)岁岁抽新枝,巧匠神器(GCC)凿玉成器的门道,乃至统筹六宫的掌印太监(操作系统)如何调理阴阳——连带着丹青妙笔(图形学)、缩地成寸术(压缩)、天涯若比邻(网络)的根基,俱化作玲珑九连环。

你可能会问,我又不做程序员,学这些干嘛?嘿,此言差矣!在这个走到哪都离不开‘数字’的时代,懂点计算机的‘底层逻辑’,绝对能让你看世界都清晰几分。不光是计算机专业的同学和上班族觉得有用,就算你只是想搞明白手机、电脑背后那点‘小九九’,这些知识也能帮你开开窍,少踩坑。虽然不是说人人都得成为编程大佬,但懂点基础,无论是理解新玩意儿、解决怪问题,还是给自己的职业道路添块砖,都贼有帮助!可以说,从萌新到想查漏补缺的大佬,都能找到自己的菜。

总之,这次更新,诚意满满,字字皆是心血熬成!

绝不是简单地把内容‘复制粘贴’进来堆一块儿。我心里门儿清:只有真正有料、有趣、有用的东西,才能让大家用得开心,须得灵犀妙笔生花,方能使诸君展颜常顾。

现在好了,一个 App 在手,从代码敲到历史书,从追剧聊到硬核科普,知识、趣味一网打尽!敲得代码玄机,解得史海钩沉,观得梨园幻梦,参得造化真章。再也不用在十几个 App 之间反复横跳了,是不是感觉省时省力,还能顺便拓展一下认知边界?

快去更新体验吧!保证有料!那字缝里渗的皆是天地菁华,章节间藏的尽是烟霞文章,断不会辜负了青眼垂顾的雅意。

接下来是我前段时间去美团分享AI辅助开发的一些内容。这些内容也收录在小册子中了。

对于使用AI辅助iOS开发,我的做法是通用规则定义为:

Always respond in 中文编写新代码前先检查是否有可以用上项目中已有的 Swift Package Manager 的库里的能力。## 技术栈你是一位专注于 SwiftUI 开发的 AI 助手。你的任务是帮助开发者编写简洁、高效、现代化的 SwiftUI 代码。在所有回答中遵循以下指导方针:- 您始终使用最新版本的 SwiftUI 和 Swift 的 API,并熟悉最新的特性和最佳实践。对于异步操作,使用 async/await。持久化存储使用 SwiftData,小数据使用 @AppStorage。图表使用 Swift Charts。- 使用 @Observable 注解视图模型,例如 @Observable final class MyModel。- 使用 Observation 框架(@Observable、@State 和 @Binding)构建响应式视图。- iOS 使用 NavigationStack 管理页面导航, macOS 使用  NavigationSplitView。- 使用 async/await 和 MainActor 处理异步操作- 遵循Swift6的并发严格模式,使用`@Sendable`。  - 确保所有可能在并发环境中共享的类型都符合 Sendable 协议  - 使用 @MainActor 隔离 UI 和需要在主线程执行的操作  - 标记可变状态的访问操作,增强线程安全性  - 为 SwiftData 模型添加 @unchecked Sendable 支持- 保持简短、可读性强、结构良好的可运行的代码。- 独立功能组件都按照 Swift Package Manager 的方式进行组件化开发,为每个组件编写单元测试,确保功能正常。- 同时支持 iOS 和 macOS 平台。## 开发流程对于每个 Swift Package Manager 组件,遵循以下测试驱动开发流程:编写测试:首先编写测试,描述组件应该具有的行为和功能。运行测试:运行测试,确认测试失败(因为还没有实现功能)。实现功能:实现最小功能,使测试通过。重构代码:优化代码结构和设计,确保代码质量。重复:继续添加更多测试和功能,直到组件完成。依赖管理对于模块间的依赖关系,遵循以下原则:最小依赖:每个模块只依赖必要的其他模块。避免循环依赖:确保模块之间没有循环依赖。接口隔离:通过接口隔离依赖,减少模块间的耦合。依赖注入:使用依赖注入,而不是直接创建依赖对象。## 规范遵循 Apple 的文档获取详细的实施指导。

让 Cursor 等开发工具保持记忆能力的方法是,创建一个mdc文件,任务开始和结束时使用和更新:

# 记忆## 0.Document/memory 文件夹- 检查 0.Document/memory 文件夹,如果这些文件不存在,请创建它们(整体功能和界面设计参考 @design.md 文档,进度和实施计划参考 @roadmap250417.md 文档)。如果存在,请阅读它们以了解项目背景。0.Document/memory 文件夹包含必需的核心文件和可选的上下文文件,均为 Markdown 格式。文件之间以清晰的层级关系相互依存:mermaidflowchart TD    PB[projectbrief.md] --> PC[productContext.md]    PB --> SP[systemPatterns.md]    PB --> TC[techContext.md]    PC --> AC[activeContext.md]    SP --> AC    TC --> AC    AC --> P[progress.md]### 核心文件(必需)1. `projectbrief.md`   - 项目基础文档,塑造所有其他文件   - 在项目开始时创建(如果不存在)   - 定义核心需求和目标   - 项目范围的真实来源2. `productContext.md`   - 为什么这个项目存在   - 解决的问题   - 应该如何工作   - 用户体验目标3. `activeContext.md`   - 当前工作重点   - 最近的变更   - 下一步计划   - 活跃的决策和考虑因素4. `systemPatterns.md`   - 系统架构   - 关键技术决策   - 使用中的设计模式   - 组件关系5. `techContext.md`   - 使用的技术   - 开发设置   - 技术约束   - 依赖关系6. `progress.md`   - 已经完成的功能   - 待构建的内容   - 当前状态   - 已知问题### 附加上下文在 memory/ 内创建额外的文件/文件夹,帮助组织:- 复杂功能文档- 集成规范- API 文档- 测试策略- 部署程序## 核心工作流### 计划模式mermaidflowchart TD    Start[开始] --> ReadFiles[阅读记忆银行]    ReadFiles --> CheckFiles{文件完整?}    CheckFiles -->|否| Plan[创建计划]    Plan --> Document[在聊天中记录]    CheckFiles -->|是| Verify[验证上下文]    Verify --> Strategy[制定策略]    Strategy --> Present[提出方法]### 执行模式mermaidflowchart TD    Start[开始] --> Context[检查记忆银行]    Context --> Update[更新文档]    Update --> Rules[必要时更新 .cursor/rules]    Rules --> Execute[执行任务]    Execute --> Document[记录变更]## 文档更新记忆银行在以下情况下更新:1. 发现新的项目模式2. 实施重大变更后3. 当用户使用 **update memory** 请求时(必须审查所有文件)4. 当上下文需要澄清时mermaidflowchart TD    Start[更新流程]    subgraph Process[处理过程]        P1[审查所有文件]        P2[记录当前状态]        P3[明确下一步]        P4[更新 .cursor/rules]        P1 --> P2 --> P3 --> P4    end    Start --> Process注意:当由 **update memory** 触发时,我必须审查每个记忆银行文件,即使有些不需要更新。特别关注 activeContext.md 和 progress.md,因为它们跟踪当前状态。## 项目智能 (.cursor/rules).cursor/rules 文件夹是每个项目的学习日志。它捕获重要的模式、偏好和项目智能,帮助我更有效地工作。随着与项目的合作,我将发现并记录从代码中不明显的关键见解。mermaidflowchart TD    Start{发现新模式}    subgraph Learn [学习过程]        D1[识别模式]        D2[与用户验证]        D3[记录在 .cursor/rules 中]    end    subgraph Apply [应用]        A1[阅读 .cursor/rules]        A2[应用学习的模式]        A3[改进未来工作]    end    Start --> Learn    Learn --> Apply### 需要捕获的内容- 关键实现路径- 用户偏好和工作流- 项目特定模式- 已知挑战- 项目决策的演变- 工具使用模式格式灵活 - 专注于捕获有价值的见解,帮助我更有效地与项目合作。将 .cursor/rules 视为随着我们一起工作而变得更智能的活文档。记住:每次记忆重置后,我完全重新开始。记忆银行是我与以前工作的唯一联系。它必须以精确和清晰的方式维护,因为我的有效性完全取决于其准确性。## Swift 特定指南在维护记忆银行时,特别注意记录以下 Swift 和 SwiftUI 相关信息:1. **SwiftUI 架构模式**   - 项目中使用的视图架构(MVVM、MVC 等)   - 状态管理策略(@Observable、@State、@Binding 等)   - 关键视图组合和复用模式2. **并发安全实践**   - 使用 @Sendable 标记的关键类型和函数   - 使用 @MainActor 隔离的 UI 组件   - 用于共享状态的同步机制(actor、锁等)3. **SwiftData 实现**   - 数据模型结构和关系   - 查询和过滤策略   - 性能优化技术4. **跨平台兼容性**   - iOS 和 macOS 特定的适配   - 条件性编译和平台检测技术   - 共享与平台特定的组件差异

不管是从头开发一个新项目还是从大型项目中迭代新功能,我会拟定一份对应需求文档,先让 AI 通过记忆去分析输出实施计划。计划中每个步骤都是能够完成并可测试的,完成后让 AI 更新记忆,如此循环直到需求完成。

工具目前优先 Cursor,其次是 Augment Code,步兵用 Copilot 和 Trae。模型我会先 Claude 3.7,fix 不了的用 Gemini 2.5 pro 和自己 review 修改。

分享使用的 PPT 如下(小册子这次内容更新的风格也是下面的样子,不重复,丰富、形象和耐看):

使用 AI 突破 iOS 开发者能力边界

之前在 KWDC 和 iOS Conf SG 用英文做过性能优化进化启动优化的技术分享。以下是我这次在上海举行的 Let’s Vision 25 大会上分享的内容。

前言

从变形金刚的机械美学启蒙,到用 Objective-C 构建第一个 iOS 应用,每个技术突破都带来新的创作可能。AI带来的变革尤为特殊——它首次实现了”所想即所得”的开发体验。过去需要数周研究的 CoreAnimation 动画,现在通过自然语言描述即可生成基础实现;曾经需要阅读大量 RFC 文档的网络协议,如今通过 AI 助手可以快速理解关键点。

根据GitHub官方统计,使用 Copilot 的开发者代码完成速度提升55%,而我的实践数据显示,在SwiftUI动画开发场景中,AI工具可减少70%的 API 查阅时间。

AI 编程工具

工具类型 我的选择 核心优势 适用场景 候选方案
本地推理 Ollama+Deepseek-r1 32B参数平衡性能/显存 代码审查 LM Studio
知识管理 AnythingLLM 支持PDF/代码库索引 技术文档检索 OpenWebUI
云端开发 Cursor+Claude3.7 实时联网/多文件分析 全栈调试 VSCode+Copilot

推荐在 Xcode 中配置 Copilot 双面板布局:左侧编写业务逻辑,右侧自动生成单元测试用例。对于 CoreData 等 ORM 操作,尝试用”逆向Prompt”技巧:先让AI生成代码,再要求它解释可能出现的问题。

使用本地大模型的好处是可以使用 Swift 项目代码作为训练集,进行模型微调。

提示词

提示词非常重要,现在推理模型出来后,我觉得最重要的是描述清楚想要得到结果是什么样的。然后再设定好范围和边界。

比如

1/ 角色设定你是有10年经验的CoreAnimation专家,擅长用显式动画优化交互体验2/ 任务描述我需要实现类似Apple Music专辑封面的3D翻转效果,要求:- 使用CATransform3D实现透视投影- 支持手势控制翻转角度- 优化iPad多任务场景下的性能3/ 约束条件- 目标系统iOS15+- 避免使用第三方库- 优先考虑Metal加速方案4/ 输出格式分步骤给出实现方案,标记出需要特别注意的点

有了工具,了解了提示词,接下来就可以做一些能够开心的事情了。对于我来说提升技能是最能让我开心的事。

想提升的技能

我感兴趣的事情其实挺多,比如画漫画,做幻灯片,游戏相关的 Blender 、Godot 和 Vision OS。

开发相关的有密码技术、网络技术、编译器和虚机等。

另外开发各类的 APP 技能也是我想要掌握的。比如日历、日记、天气、健康、拍照、漫画阅读器和音视频播放器等等。

当然,我当前眼前最想提升的就是幻灯片的质量,因为我要用它在会上来做分享呀。

开发幻灯片 APP 需要什么知识

以前我为了能够更好的分享内容,我做了个幻灯片软件。

接下来我想完善这个幻灯片软件,于是我这样问了 AI

我想开发一个 PPT 这样的 macOS 应用,这个应用有哪些功能,分别需要什么方面的技术知识

AI 的回答分为基础功能、进阶、高级、技术架构、挑战等部分。基础部分主要包括了界面、文档和幻灯片编辑的功能和使用的技术。

进阶里有模板、动画和多媒体的功能和技术。

  • 基础功能
    • 界面
      • 功能:窗口、菜单栏、工具栏、幻灯片缩略图、编辑区
      • 技术:Swift/SwiftUI、AppKit、NSWindowController、NSDocument、NSDraggingDestination
    • 文档
      • 功能:新建、打开、保存、自动保存、版本恢复
      • 技术:FileManager、NSSavePanel、NSOpenPanel
    • 幻灯片编辑
      • 功能:添加/删除幻灯片、调整顺序、编辑文本/形状/图片
      • 技术:NSView 或 CALayer 实现画布、NSTextView 或自定义文本渲染、NSBezierPath 或 Core Graphics 进行图形绘制。
  • 进阶
    • 模版与主题
      • 功能:预置模板、自定义主题
      • 技术:NSUserDefaults、动态渲染实时应用到幻灯片元素
    • 动画与过渡效果
      • 功能:元素入场动画、幻灯片切换效果
      • 技术:Core Animation、CAAnimation 和 CALayer 实现关键帧动画、时间轴控制
    • 协作
      • 功能:多人实时编辑、版本历史、云端存储
      • 技术:WebSocket、冲突解决(OT)算法、CloudKit、NSUbiquitousKeyValueStore
    • 多媒体支持
      • 功能:插入图片、音频、视频、形状、图标
      • 技术:AVFundation、NSImageView、Core Plot
  • 高级
    • 导出和分享
      • 功能:导出 PDF、视频、HTML、图片、Markdown
      • 技术:NSPrintOperation 或 PDFKit,AVAssetWriter 逐帧渲染幻灯片
    • 智能建议
      • 功能:自动排版、配色建议、AI 生成内容
      • 技术:Core ML、NaturalLanguage、LLM
    • 插件系统
      • 功能:第三方插件扩展(图表库、AI 工具)
      • 技术:NSBundle 加载插件、定义插件接口协议
  • 技术架构
    • 设计模式:MVVM、响应式编程
    • 数据存储:SwiftData、NSCache 缓存
  • 适配 macOS 特性
    • Dark Mode:通过 NSAppearance 适配深色主题
    • 沙盒机制:处理文件访问权限和安全性
  • 关键挑战
    • 复杂动画:高性能渲染需要 Metal 或 Core Animation 优化

目前我最希望能够学习到的就是其中的动画部分,因为我希望 PPT 能够通过动画看起来更生动些。

AI 能够解决传统动画开发的一些痛点:

功能模块 传统开发痛点 AI解决方案 效率提升
动画系统 关键帧参数调试耗时 自然语言转CAKeyframeAnimation代码 65%
布局引擎 多设备适配复杂 深度学习预测最佳constraint组合 80%
协作同步 冲突解决算法实现难度高 生成OT算法Swift实现模板 70%
性能优化 Metal Shader调试困难 自动生成性能分析报告及优化建议 90%

一些操作技巧:

  • 逆向工程法:将Keynote动画导出为视频,用Vision框架分析帧差异生成CAAnimation参数

    Screen Recording → (Vision分析) → CoreAnimation指令集 → (AI转换) → SwiftUI可编辑代码
  • 提示词演进

    • 初级:”如何实现页面翻转动画”
    • 进阶:”生成支持手势控制的CATransform3D动画,要求60fps流畅运行”
    • 专家:”创建可中断的物理动画系统,模拟纸张的弯曲刚度和空气阻力”
  • 调试技巧

    • 在Xcode中配置CA_DEBUG_TRANSACTIONS环境变量
    • 使用Instruments的Core Animation分析模板
    • 通过AI解释CAMediaTimingFunction的贝塞尔曲线参数

基础提示词

接下来就看看我是怎么使用 AI 的来学习动画的吧。

我们先看看基础提示词,框定一些基本技术,这里列出是我常用的技术,比如 SwiftUI、Swift Concurrency、SwiftData 和 Observation。

动画描述提示词

下面我对我要实现的动画做一个描述,现有幻灯片有个比较大的痛点,就是当我想在幻灯片上指出某个区域时,需要用到银光笔。如果能够直接在幻灯片中实现点击,然后通过一些效果表示点击的地方那不是更好。

于是我做了这样一个描述的提示词。让其在点击拖动时会产生变色的光晕效果,这个光晕会随着时间渐渐消失,可以多点触控,能够有多条轨迹,发光效果使用模糊滤镜实现的,颜色会随时间自动的变化。

代码可以看到颜色随时间变化这个需求主要是通过 truncatingRemainder 这个方法来实现的。使用 Canvas 的上下 addFilter 方法设置模糊效果,然后形状设置成圆形。

下一个例子。提示词是点击或拖动会产生随机的形状,这些形状包括圆形、矩形、圆角矩形、胶囊形、椭圆形和三角形,每个形状都有随机的颜色和大小,形状出现时有缩放动画,然后渐渐消失。最多同时显示50个形状,形状之间保持最小间距。

得到了代码。

ForEach(shapes) { shape in    // 描边和尺寸设置    shape.view        .stroke(shape.color, lineWidth: 2) // 描边样式        .frame(width: shape.size * shape.scale, height: shape.size * shape.scale) // 动态尺寸        .position(shape.position)        .opacity(shape.opacity)        .animation(.easeOut(duration: animationDuration), value: shape.scale)}// 随机图形static func randomShape() -> some Shape {    let shapes: [AnyShape] = [        AnyShape(Circle()),         AnyShape(Rectangle()),        AnyShape(RoundedRectangle(cornerRadius: 25)),        AnyShape(Capsule()),        AnyShape(Ellipse()),        AnyShape(Triangle()) // Added Triangle    ]    return shapes.randomElement()!}// 三角形状 Shapestruct Triangle: Shape {    func path(in rect: CGRect) -> Path {        var path = Path()        path.move(to: CGPoint(x: rect.midX, y: rect.minY))        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))        path.closeSubpath()        return path    }}extension Color {    static var random: Color {        Color(red: .random(in: 0...1),              green: .random(in: 0...1),              blue: .random(in: 0...1))    }}

从代码中可以看出颜色是通过 rgb 三个值随机组合获得的随机颜色,在 randomShape 这个函数中实现了各个形状,基本都是内置的 Shape 形状。

点击运行可以看到效果。

从其它语言代码获取提示词

这是一个 Rect 的动画代码,完整代码地址在这里。代码不少,如果对 CSS 动画不熟还挺难看懂,那么我们可以让 AI 帮我们获取这段代码的动画描述,让他通过描述转化成 SwiftUI 动画的提示词。这就是得到的提示词。

可以看到 AI 生成的提示词比我们写的更有条理,他会先说最终的视觉效果是什么样的,然后会对动画的特点做详细的说明,比如是按照什么速度和方向旋转,完成一圈的时间,是否循环。还生成了技术要点,比如环形渐变用 AngularGradient ,边框效果用 mask 和 strokeBorder,blur 效果模拟发光。用 GeometryReader 来确保边框大小。还有布局上一些值的设置。可以说是非常精确和详细了。

通过这些提示词得到的 SwiftUI 代码我们就容易看懂了。比如 GlowingCardBorder 这个边框视图的颜色设置,尺寸的计算等,还有 TimelineView 如何计算时间。

运行这段代码的效果如下图:

我们可以学到 CSS → SwiftUI 转换法则

protocol AnimationTranspiler {    func convert(keyframes: CSSKeyframe) -> CAKeyframeAnimation    func translate(easing: BezierCurve) -> CAMediaTimingFunction    func adapt(layout: FlexBox) -> SwiftUI Layout}// AI生成的转换中间件示例class CSS2SwiftAITranspiler {    func parseTransform(transform: String) -> CATransform3D {        // 使用NLP识别translateX/rotate等操作        // 生成优化后的CATransform3D链式调用    }}

代码复用

这是一个会发光和改变形状的动效,左边是代码,可以看到很多值的设置都是写在各处表达式中的,如果想复用这段动画代码进行一些调整就很费劲。于是我写出这样的提示词。先让他将动画的部分进行复用,实现更多自定义,然后利用提示词再优化下代码。比如减少重复的部分,编写已用接口等。

AI 依据提示会帮着生成一个接口配置结构体,里面可以对动效做详细的设置。比如角的弧度,变换的幅度,颜色和阴影等。还可以对不同参数组合进行配置,方便复用。

这里是一个新的设置,主要是将背景设置为黄色,改变了角的弧度还有阴影部分。拖动的时候会发现,对拖动后的弹性做了些参数调整,弹起来更加流畅些了。

学习技术

这个效果,可以看到一群散开的小圆,慢慢聚合点缀成文字。

如果我想了解这个动画效果后面的技术,可以这样问 AI。先让他系统性的检查代码中的动画实现,然后说下每个动画的技术。最后说下每个技术的最佳实践。

AI 的回答如下:

主要使用了以下几种动画技术:## **SpriteKit 粒子动画**:- 使用 `SKShapeNode` 创建粒子节点- 通过 `SKAction` 实现粒子的移动动画let move = SKAction.move(to: targetPosition, duration: duration)let scale = SKAction.scale(to: 1.0, duration: duration)particle.run(SKAction.group([move, scale]))## **粒子动画效果**:- 初始化时粒子随机分布- 文字组装动画(1.5-3.0秒)- 触摸爆炸效果(0.3秒)- 重新组装动画(1.5-2.0秒)## **SwiftUI 动画**:- 使用 `withAnimation` 制作按钮滑入动画withAnimation(.spring(duration: 0.6)) {    buttonOffset = 0}## **核心动画技术**:- 使用 Core Text (`CTLine`, `CTRun`) 创建文字路径- 使用 `CGPath` 进行路径采样,确定粒子目标位置## **动画时序控制**:- 使用 `DispatchQueue.main.asyncAfter` 控制动画延时- 设置动画缓动效果:`.easeOut`- 使用不同的动画持续时间创造自然的视觉效果这个示例综合运用了 SpriteKit、SwiftUI 和 Core Graphics 的动画能力,创造了一个复杂的交互式文字粒子动画效果。

通过 AI 的回答可以看到动画中的粒子节点时使用 SpriteKit 创建的。粒子不同时段的动画的参数也给了出来。还有文字形状使用 Core Text 的 CTLine 和 CTRun 创建的文字路径,用 CGPath 进行了路径的采样确定粒子目标位置。

从开源库学习

背景的这个颜色会变的效果来自一个开源库,你可以看到颜色的变化的节奏并不死板,而且颜色融合的效果也很好。我们可以通过 AI 来了解背后是怎么做到的。

AI 分析后,会告诉我各个动画的效果是在哪个文件实现的。

我们能够一目了然用了哪些动画,以及是在哪个文件中写的。比如弹性动画效果是在 BlobLayer.swift 里用 CASpringAnimation 创建的,用到了哪些属性。定时器是在 FluidGradientView.swift 里通过 Combine 创建的,触发时机是随机的。

渐变动画是基于 CAGradientLayer 创建的子类 BlobLayer 来创建的径向渐变。

如何使用这个库,也可以让 AI 提供范例,这样应用到自己项目也会很容易。下面就是它提供的使用范例。

可以看到这个库的接口非常清晰,除了基础颜色可以定义外,还能够添加叠加高亮色,其中加白是为了让颜色不要太艳丽,加黄是为了让颜色偏暖些。

接口使用

下面我想说说接口的使用,比如 SF Symbol 新增了 symbolEffect 来给 SF Symbol 做动效,第一个是不断的晃动,第二个是给晃动加了延时,第三个是在两个 SF Symbol 切换时加上过渡效果。那么怎么全面掌握 symbolEffect 接口所有动效及用法呢?

我们可以直接这样问 AI。它会整理好所有的基础动画效果。

在 SwiftUI 中,`symbolEffect` 修饰符可以为 SF Symbols 添加动画效果。以下是主要的 symbolEffect 类型:## 基础动画效果:.symbolEffect(.bounce) // 弹跳.symbolEffect(.scale) // 缩放.symbolEffect(.pulse) // 脉冲.symbolEffect(.wiggle) // 摇摆.symbolEffect(.appear) // 出现.symbolEffect(.disappear) // 消失.symbolEffect(.replace) // 替换## 变体动画效果:.symbolEffect(.bounce.up) // 向上弹跳.symbolEffect(.bounce.down) // 向下弹跳.symbolEffect(.variableColor) // 变色.symbolEffect(.iterative) // 迭代动画## 常用选项设置:// 重复选项.symbolEffect(.bounce, options: .repeat(3)) // 重复3次.symbolEffect(.bounce, options: .repeat(.forever)) // 无限重复.symbolEffect(.bounce, options: .repeat(.continuous)) // 连续重复// 速度选项.symbolEffect(.bounce, options: .speed(2)) // 2倍速## 组合效果:// 可以组合多个效果.symbolEffect(.bounce).symbolEffect(.variableColor)你当前代码中使用的是:.symbolEffect(.wiggle, options: .repeat(.continuous))这会让图标持续不断地进行摇摆动画。注意:某些效果可能需要 iOS 17.0+ 或 macOS 14.0+ 及以上版本才能使用。结合 transition 来使用import SwiftUIstruct ContentView: View {    @State private var isPresented = false    var body: some View {        if isPresented {            Image(systemName: "swift")                .font(.system(size: 100))                .transition(.symbolEffect(.appear))        } else {            Button("Show symbol", action: { isPresented.toggle() })        }    }}// contentTransitionimport SwiftUIstruct ContentView: View {    @State private var isPresented = false        var body: some View {        VStack(spacing: 50){            Image(systemName: isPresented ? "checkmark.circle.fill" : "faceid")                .font(.system(size: 100))                .contentTransition(.symbolEffect(.replace))                        Button("Show symbol", action: { isPresented.toggle() })        }    }}

会说变体动画有哪些,组合效果,有哪些选项,比如设置重复次数,速度等。最后还会举例怎么使用。

修改动画效果

下面我们看看如何通过 AI 来修改动画的效果,左边代码会显示一个火焰的动效,现在我们让颜色有变化。提示词指明增加颜色的随机性,然后让粒子大小也能够有变化。

可以看到右侧火焰的颜色变成随机的来了。

接下来再看看怎么让粒子的轨迹能够有变化,提示 AI 让运动轨迹变成螺旋运动,带些波浪效果,然后让运动的中心点移到画布的中心。

会生成对应的运算表达式。

// 添加螺旋运动let spiralRadius = canvasSize.width/3 * (1-time)let spiralAngle = rotations * time * .pi * 2 + startingRotation// 添加波浪效果let waveAmplitude = canvasSize.width/6 * Darwin.sin(time * .pi * 4)let x = canvasSize.width/2 + Darwin.cos(spiralAngle) * spiralRadius + waveAmplitudelet y = canvasSize.height/2 + Darwin.sin(spiralAngle) * spiralRadius * 0.8// 使用非线性alpha衰减let alpha = Darwin.pow(1-time, 1.5)

运行后看到粒子的轨迹已经变了。

编辑器的开发

接下来我要讲的是这个用来展示代码的编辑器,以前用的三方的,定制起来不方便,还需要考虑升级和兼容问题。

左侧代码是我幻灯片中标题文字动画选择的部分,目前这个编辑器不光可以浏览代码,还能够修改代码,比如更改 animationType 的值,就可以更改文字动画效果。

这个其中一种动画效果,也是我用在提示词上的文字动画。我们用这个编辑器来改改文字内容,文字大小。

那么这个编辑器的提示词是什么呢?我希望这个编辑器是原生的,但是 SwiftUI 的 TextEditor 现在还无法进行深入的定制,我发现很多好的原生编辑器都是使用 AppKit 的控件来做的。所以我希望它是在 SwiftUI 下用 NSViewRepresentable 来包装现有的 AppKit。

功能上我需要的是能够显示语法高亮,而且是多语言的,Swift 语法用来显示开发代码,Markdown 语法用来显示提示词问题和答案之类。高亮逻辑会在文本改变时触发。这样能够保证修改代码后依旧能够正确显示高亮。确保 NSTextView 和 SwiftUI 的数据流能够双向绑定。再让其支持 UndoManager 撤销操作和快捷键,因为代码会经常敲错,需要快速回退。

另外是对语法高亮支持的说明,说明需要支持多语言。支持配色主题等。

最后就是换行到语法格式对应的地方。

总的来说

这几年用 AI 的工具以来,我最大的感触就是用 AI 去学习喜欢却不擅长的,用 AI 去做必要却枯燥繁琐的。这样就有更多的时间去做很少的事情,因为

只有很少的书值得去读,
很少的作者值得关注,
很少的朋友值得交往,
很少的事情值得投入,
很少的道理值得明白,
很少的资产值得投资,
很少的目标值得追逐。

2025 年我正在使用的 macOS 应用

这篇文章是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 的演讲

大会前,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 也会有很多有意思的国内外嘉宾过来,真是非常期待。

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

最近,我的颜气 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 做的技术分享

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

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 聊了点技术问题,他也是喵神的同事。

我是怎么做个人知识管理(PKM)的,从史前时代备忘录、Sublime,经历了Evernote,Markdown兴起,Bear的优雅,Notion革命,飞书语雀,Obsidian、Flomo,到最后使用卢曼卡片盒笔记法串起了流程

前言

前段时间,我发布的几篇和动漫相关的公众号文章《鸟山明和他的龙珠》、《圣斗士星矢的前世今生,车田正美的坚持,城户光政的阴谋》得到很多阅读和关注。有些人感叹我涉猎广,其实是我平日爱收集各种奇奇怪怪的文章和资料。今天我就跟大家说说我知识管理的心路历程。

本文会从我所遇到知识管理的困境以及如何构建自己的知识管理流程说起,然后详细聊些知识管理各流程中的笔记工具和一些笔记记录的方法理论,比如卡片盒笔记法、P.A.R.A.、费曼学习法和 CODE 等。

在信息爆炸的时代,个人信息管理已经成为不可忽视的挑战。为了高效收集、整理、消化海量信息,也出现了很多工具。从史前的系统自带备忘录、Sublime、Word、Vim,Emacs orgmode,到 Evernote、Bear、Devonthink、Drafts、Ulysses,还有很多线上笔记管理平台比如语雀、飞书,最后时依赖各种高效笔记记录方法的 APP,Roam Research、Obsidian、Flomo、Craft。还有很多很多,这些工具提供了不同的管理方式和理念。

我最开始使用知识管理是 Evernote,开始尝试使用数字化工具来管理日常信息和笔记。然而,这并不是一次一劳永逸的解决方案。随着信息量的增长,Evernote 和 OneNote 等传统笔记工具逐渐显得力不从心。于是,我开始探索更多的工具,从 Bear、Noted,再到 Notion、Roam Research、Obsidian 等更具结构化思维和知识管理理念的工具,试图构建一个完整的信息管理系统。与此同时,我也引入了 NetNewsWire、Reeder、Readwise、Pocket、Hypothesis 等工具,将外部资料(如书籍标注、文章收藏)引入我的信息生态系统中。期间,我一度搭建了一个复杂的知识系统,囊括了项目管理、信息整理、笔记复盘、阅读记录等各个方面。然而,尽管工具愈发复杂和多样,我逐渐意识到,工具本身并不能解决一个问题,这个问题是如何高效消化这些海量信息并真正转化为可以使用的知识。

在长时间的实践过程中,我发现自己渐渐陷入了“信息囤积”的陷阱。无论是微信公众号文章、头条文章,还是深思好文,我收藏了大量内容,却很少有时间真正阅读和消化。同样的情况也出现在视频资源的消化上。不断下载、收藏,而这些内容大多只是躺在硬盘里。从教程、电子书到 PDF 文档,堆积的资料让我感到压力倍增,却又无法轻松地找到一个合适的时机来打开它们。

更糟糕的是,我在各种平台上收藏的笔记,如小红书、B站、RSS中的上千条收藏内容,已然变得难以追溯。每当需要查找某些信息时,我常常陷入找不到、看不见、用不上的窘境。尽管心知这些信息很可能再也不会有用,我依然继续收藏,仿佛只要“拥有”它们,就能够缓解对信息错失的焦虑。

在面对大量囤积的信息时,我逐渐意识到,真正的问题并不在于收集,而在于消化。收集信息的过程可能只需要短短几秒,但要真正理解并内化这些信息,则需要更多时间和精力。这种“藏品如山积,消化路漫漫”的现象说明了信息处理的关键并不在于收集多少,而在于如何高效地使用这些信息。

最早的时候,我的资料整理方式相对简单,按项目划分文件夹,将所有相关资料、需求和产出放在一个地方。当我遇到类似的项目时,会从旧的文件夹中查找资料。然而,随着项目增多,文件夹中的资料也越来越多,查找变得愈加困难。后来,我开始使用系统自带的备忘录,将日常遇到的可能有用的信息随手记录下来,但随着时间的推移,备忘录中的信息数量爆发式增长,最终变得难以管理和检索。

随着项目工作的减少,我更多地参与到已有项目的维护中,积累的信息越发显得冗余。尤其是长期在特定领域的深入学习,我不断发现许多新的、感兴趣的内容,但缺乏机会去实践和应用,于是这些信息就一直沉积在备忘录中。时间一长,这些信息变得难以追踪,也无法有效利用。

为了应对不断增长的信息压力,我决定将注意力从“收集信息”转移到“使用信息”上。我认识到,仅仅依赖工具本身是不够的,重要的是建立起一个高效的系统来帮助我处理这些信息。为此,我还开发了一款小册子 APP,并在其中加入了资料整理的功能。这个系统让我能够更好地分类、归档、回顾和消化资料,且不再依赖多个工具来切换工作流程。

与此并行,我调整了信息管理的方式。与其简单地收集信息,我更加注重系统化的消化流程。通过设定清晰的目标和计划,我能够优先处理对当前工作或生活最有价值的内容。那些暂时无用的信息则被归档存储,并定期进行复盘和清理。这一优化使得我能够减少信息焦虑,从“拥有大量信息”的负担中解脱出来,真正做到高效利用和整合信息资源。

我是怎么用小册子 APP 做知识管理的

我的知识管理流程分为两个并行的部分:日常收集和项目整理。

在日常的知识收集中,我的重点是快速、便捷地记录信息。无论是看到的文章、读到的书籍、自己的灵感,还是电影的感悟,我都会将它们整理成卡片并录入到我的知识管理工具中。我根据不同领域为这些卡片设置分类,比如有Apple技术、计算机科学、艺术、生活,还有当前项目。如果收集的信息和我正在进行的项目相关,我会为其打上一些粗力度的标签,以便日后整理。收集的过程非常高效:对于文章,我通常只需贴上 URL,程序会自动解析出标题;对于灵感,我直接在文本框中输入内容;书籍和电影则通过豆瓣链接快速录入。这样的操作使得整个录入过程简单而快捷,极大提升了我记录知识的效率。

与日常收集相对应,我在整理项目时会更为仔细。这一流程是对收集到的卡片进行进一步的分类和消化。我通常会从项目相关的标签入手,深入研究卡片的内容,并对其进行精细化分类。与日常收集阶段相比,在这个阶段我会为卡片设置更多、更细致的标签,并剔除重复或无用的信息。此外,已经消化并应用过的卡片会被归档,作为日后的参考。归档会沉到对应标签列表的最下面而不是移到另一处,方便以后的回顾。

标签管理是我知识整理中的核心要素之一。我采用纯文本的方式来管理标签,这与许多其他知识管理应用的标签管理方式不同。选择这种方式的原因在于文本标签具有极大的灵活性,特别是在标签数量众多的情况下。我可以通过简单的文本编辑,避免不相关领域的标签干扰到我当前的项目整理。当某个标签不再需要时,我可以直接从文本中删除,存入一个独立文档,待日后需要时再复制回来。这样的操作不仅简化了标签管理流程,也保证了标签系统的灵活性和可操作性。

标签的顺序管理同样依赖于文本编辑,这种方式虽然简单,但极为高效,特别是当需要对标签进行大规模调整时,文本编辑可以轻松完成。

在知识被消化和分类后,我会将其以树状结构呈现在我的知识手册中。这种结构清晰地展示了知识点的层次和相互关系,使得我在后续学习中能够快速定位到相关知识点。

在手册中的每个知识点右上角都有一个“相关资料管理”的按钮,点击该按钮,我可以将新的卡片与当前的知识点关联。这个功能不仅帮助我巩固已有的知识,还为未来的知识再生提供了条件。新的卡片在合适的时间会被重新整理并融入到现有知识体系中,进一步完善我的知识结构。这一过程形成了一个持续的知识再循环,确保了知识的更新与扩展。

在处理和管理个人知识时,我曾遇到一个常见但令人困扰的问题:很多资料的链接在我回顾时已经失效。尽管我尝试通过标题重新搜索这些内容,往往还是一无所获。这让我意识到,单靠链接保存是不够的。链接失效并不是个例,它反映了一个更广泛的互联网现象——“数字衰变”。皮尤研究中心(Pew Research Center)的一项最新研究揭示了互联网内容消失的严重性。研究表明,2013 年发布的网页中,有 38% 在十年后的今天已经无法访问。这些失效的链接覆盖了多个重要领域,包括政府网站、新闻平台、维基百科和社交媒体等,广泛而深远地影响了信息的持久性和可访问性。尤其值得注意的是,新闻网站的失效率达到了 23%。这些数据表明,即使是高流量、重要性较高的网站,也不能幸免于这一问题。这不仅限于普通的网络内容,连维基百科这样的全球最大在线百科全书中,也有 54% 的页面存在至少一个失效的引用链接。对于依赖这些引用来源的用户而言,失效的链接让信息的可靠性大打折扣,无法再追溯到原始的参考资料,这在知识传播和信息验证中是一个严重的问题。

为了应对这个问题我尝试了直接保存网页、转为 Markdown、保存为 PDF等方式,这些方式的痛点是保存和管理会耗费精力,最后我在小册子 APP 中实现了一键保存,这样内容会离线存在本地,无网情况也能看,避免了网页所属平台关闭后无法访问的问题。落得一个安心。

下面我会详细具体的说下我对个人知识管理的理解,各种工具以及一些方法论的介绍,讲讲我是怎么一步一步形成如今的的个人知识管理工作流的。

也不知道你现在处在什么样的时期,只是希望下面的内容能够引起你的共鸣。

PKM 是什么,为什么要有它

个人知识管理(Personal Knowledge Management,简称 PKM)是当今信息时代的热门话题。随着信息量的爆炸和数据的迅猛增长,如何有效管理、组织和利用这些知识成为每个人必须面对的问题。PKM 的核心不仅仅在于工具或系统,更在于思维方式和对信息的主动掌控。

工具无法决定输出,驱动力才是关键

工具本身并不能决定最终的输出效果。正如有人所言,即便只有纸和笔,依然能够达到想要的结果。工具的作用在于让你在输出的过程中减少痛苦,降低摩擦成本,从而让你更专注于核心的知识管理任务。然而,真正促使个人持续进行知识管理的并非工具本身,而是内心深处的兴趣和热爱。

例如,一位绘画爱好者可能在寒风中等待数小时,只为等待一张在夕阳照射下绝美的风景;一位跑步爱好者可能在跑步中,在风拂过面颊的瞬间不自觉地微笑。这些经历带来的内在反馈远比任何打卡、点赞等外部激励更能促使人坚持。

大脑擅长思考,但不擅长记忆

在个人知识管理的过程中,理解大脑的功能是关键。大脑擅长复杂的思考和逻辑推理,但并不擅长记忆大量的信息。这也是为什么建立一个有效的知识库如此重要。知识库擅长存储和记忆,让我们从记忆琐碎信息的负担中解脱出来,腾出更多精力进行创造性思考。

PKM 的目标是为大脑减轻负担,让知识和信息在需要的时候能够快速、准确地被调取,而不是依赖于记忆。通过构建一个系统化的知识库,我们可以将大脑从信息存储的压力中解放出来,专注于思考和创造。

PKM 中的常见争论:标签、文件夹与反向链接

在 PKM 的讨论中,关于工具和方法的选择也充满了争议。诸如“标签与文件夹之争”、“反向链接的价值”这类话题已是老生常谈,甚至已经进入了学术研究领域。而一些相对冷门的议题,如“笔记大小及其优缺点”则鲜有人提及,只有少数来自 Zettelkasten 用户的讨论。

事实上,PKM 领域内的大多数讨论都集中在如何组织和呈现知识这一点上。不同的知识组织方式适用于不同的使用场景和个人习惯。例如,反向链接可以增强知识的关联性,而文件夹和标签系统则更加适合线性思维的人群。然而,无论选择哪种方式,重要的是找到适合自己的组织方式,并坚持使用。

信息膨胀与管理挑战:数量是最大难点

PKM 面临的最大挑战之一是信息的数量。在我们日常生活中,信息膨胀的现象也随处可见。读者只需简单查看自己关注的公众号数量和近三个月的推文量,便能直观感受到信息的急速增长。虽然我们可以通过筛选优质信息源来精简管理,但随着时间的推移,信息依然会逐渐积累,形成庞大的知识库。现代社会中,每个人每天都会面对大量的信息涌入,如何筛选、存储并最终利用这些信息是一个巨大的挑战。一个简单的例子是,假如有一名喜好读书的人有一百本书时,可能通过简单的记忆便能轻松找到某本书的位置。然而,当这个数字扩大到万本时,如果没有一个有效的管理系统,几乎不可能在短时间内找到所需的书籍。

更多知识管理的研究可以参看这个 Github 上的仓库 Awesome Knowledge Management

工欲善其事,必先利其器

下面我们继续说说这些适合知识管理的工具们。

工具

工具的使用我觉得没必要被一个工具的各种功能束缚了,一个工具可能只有一个比其他工具好用的,那么你就用那个最好的功能就可以了。工具是可以组合起来用的,因此多了解些工具也没啥坏处。

史前时代的 Windows的记事本、Mac的备忘录、Word等。因为我是做开发的,所以我还会用 SublimeText、CotEditor、Emacs orgmode 等开发用的编辑器来写笔记,这些软件对大文件的支持都很好。Evernote 的出现带来了笔记整体管理的风潮,随之诞生出 Onenote、有道云、为知笔记等笔记应用。Markdown 格式笔记的流行,出现了 Typora、Notable、Ulysses 和 Bear 等 App。最后我还是留下了 Bear。主要是 Bear 的一些特色我很喜欢,无需特别了解 Markdown,即可自然使用。折叠功能支持对象有各级标题、次级列表、待办清单。图片大小调整,图片本地存储。简约美观。

飞书、语雀、Confluence、Thoughts 这样的将内容放在云端的应用解决了笔记同步的问题,开始流行起来。
Craft、Wolai、FlowUS 等类 Notion 应用,将笔记更加数据库化了,而且自定义能力也很强。功能不断迭代丰富,感觉就跟 CMS 系统一样。这样吸引了很多对定制要求高的用户。后面还出现了白板类 APP,比如无边记。完成了石板、黑板、白板再到数字画布的进化。

笔记记录法的流行诞生了 Roam Research、Obsidian 和 Flomo 这些主打方法论的 APP,有主云端的也有主本地的。

下面是按照在特定场景下工具组合使用来看。

日常信息获取时,对于聚合信息,会使用 RSS 订阅软件还有网页收藏夹。热点事件通过知乎、微博、X 等平台。艺术我通常会通过订阅和关注一些账号来发现自己喜欢的作品。一些期刊文献使用 Zotero,Calibra 来管理,MarginNote 标注,然后 obsidian 整理笔记。杂志书籍用 Apple Books,漫画用可达漫画。图片管理用 Eagle。

一些会议笔记用 Notability 或 GoodNotes,研究型的用 Obsidian。知识回顾用 Anki。提纲用 MindNode

另外还有些精品 APP 也值得一试:

  • DEVONthink - 官网(买断制,macOS & iOS)老牌知识管理软件,支持储存文件、管理档案、撰写笔记与文章,具备AI功能。
  • Heptabase - 官网(订阅制,macOS)由卡片、白板、文字编辑器组成的生产力工具,易于组织内容并建立关联性。
  • OmniFocus - 官网(买断制/订阅制,macOS & iOS)老牌GTD软件,支持拆解复杂任务,设定不同情境下的任务显示。

绝妙的个人生产力(Awesome Productivity 中文版) 里面很全面按分类的列出了各种工具。

接下来我打算按照输入、整理、消化、输出这个我使用的流程再详细说说。

输入

有效的信息输入不仅依赖于优质的信息源选择,更需要我们有意识地简化、系统化管理这些信息。通过借助合适的工具、培养良好的习惯,以及采用像卡片写作法这样的整理方式,我们可以在信息洪流中建立起属于自己的知识体系。这不仅能帮助我们在日常学习中游刃有余,还能为我们的长期成长打下坚实的基础。

二八原则(帕累托原则)告诉我们,80%的结果源自20%的输入。这一原理不仅适用于经济和管理,还可以延伸至学习和知识管理。当我们回顾积累的信息时,真正能够帮助我们成长的往往是少部分重要的内容。因此,做笔记时,要学会识别和提取关键信息,而不是事无巨细地记录一切。因此,选择优质的信息源,减少无效信息的输入,是优化信息输入的重要第一步。

优质的信息源不但能节约时间,还能提高信息的深度和可靠性。比如一些报纸杂志等传统媒体,此外,书籍、播客也是不可忽视的信息来源。了解信息的来源,并确保其权威性,不仅有助于提升知识质量,还能在引用时提高信息的可信度。

有效简化输入,提升思考深度。

过多的输入常常会稀释我们的注意力,使思考变得浅显。因此,简化输入变得尤为重要。我们需要有意识地控制信息的广度,从而为深度思考腾出空间。输入广度的减少,能帮助我们集中精力,深入思考。举例来说,在订阅新闻和学术论文时,关注核心主题的深度研究,而不是试图了解每一个细节。同时,保持思维的“留白”也是一个关键策略,为深入思考留下空间,而非被无尽的信息所淹没。

从不同渠道获取信息。

信息的输入并不仅限于文字或数据。灵感和想法有时会突然出现,或来源于日常生活中的对话与观察;我们还可以从社交媒体的信息流中获取某些时效性较强的观点。

播客资源近年来逐渐成为获取深度信息的热门选择,其中不少优质的中文播客资源都提供了非常多元的视角。下面是一些播客资源的整理:

还有一些 NewsLetter 资源的整理:

RSS 订阅同样是不可忽视的工具,可以通过 RSS Source 发现感兴趣的订阅源,并通过 Inoreader 等工具将其整合起来,进行高效的管理。

信息的有效管理需要借助工具。我使用了多个工具,结合它们的优势来实现系统化的收集与整理。例如:

  • CuboxNetNewsWire 是我常用的 RSS 阅读工具,能帮助我快速筛选并收集有价值的信息;
  • Readwise 用于同步和高亮阅读笔记,并可以将微信读书、Calibre、Apple Books 等平台的内容整合起来,方便跨平台管理;
  • DraftsThe Archive 则是用来记录和存储灵感的利器,特别是当灵感突然出现时,可以随时记录下这些碎片化的想法。

信息的收集只是第一步,吸收和整理才是信息输入的核心。无论是通过写作来总结,还是通过复盘来回顾,都可以帮助我们将碎片化的信息转化为系统化的知识。比如,卡片写作法对我来说是一种极其有效的方式。通过将思考过程拆解为一张张小卡片,每张卡片涵盖一个明确的主题或观点,降低了思维的门槛,使写作和总结变得更加轻松。原子笔记(Atomic Notes)的概念正是建立在这种写作方式之上,每个笔记单位仅涵盖一个明确的信息点,既独立又简洁,便于日后整理和复盘。

这种方法不仅适用于写作,还能帮助我们管理思维和时间。每次积累一点点,最后汇聚成千字的成果,这种日积月累的方式既高效又能产生实质性的进展。就像我们刷短视频时,总想着“再看几条”,结果一不小心,花的时间比看一部电影还多。因此,小步积累能带来长远的收获

除了系统化的信息输入,我也培养了一些帮助维持效率的小习惯。例如,每天早上起床就喝水;步行时会戴上耳机开始听播客;坐地铁时打开读书 APP;到办公桌上第一件事是写下接下来要做的事项。这些小习惯不仅能帮助我保持良好的时间管理,还能使信息输入变得自然。

笔记记录方法

对于想要长期保存的信息,笔记的撰写方式至关重要。一种极简的记录方法是只回答一个问题:「读了这篇文章,我最大的收获是什么?」这一方法看似简单,但实际上蕴含着深刻的洞察力。它通过提炼出文章的核心要点,既能帮助建立记录习惯,也要求对信息进行深入思考和概括。

笔记记录方法可以分为两大类:注重笔记静态结构重视笔记动态结构。前者主要关注笔记的层次和内容的组织形式,后者则更强调笔记之间的联系和思维的流动性。常见的静态结构笔记法包括康奈尔笔记法、麦肯锡笔记法、大纲笔记法、子弹笔记法和一元笔记法。这些方法往往以明确的层次结构、分类条理为特点,适合用于总结、归纳和整理知识点。

近年来,随着知识管理需求的变化,越来越多的人开始探索更加灵活、动态的笔记方式,强调想法的关联和思维过程。这种笔记法侧重笔记内容之间的关联性,以构建知识网络为核心,代表方法包括渐进式归纳法卢曼卡片盒笔记法。渐进式归纳法提倡逐步丰富笔记内容,确保笔记随着时间的推移不断演变和扩展,而卢曼的卡片盒笔记法则通过卡片之间的连接,构建出高度关联的知识网络。相比静态结构笔记法,这些方法更符合做笔记的理念,即在记录过程中不断推动思维深化和知识创新。

在这种动态笔记法的基础上,许多知识工作者也在进行个性化探索,发展出适合自身需求的系统。例如,Andy Matuschak 的长青笔记系统(Evergreen Notes)强调信息的长期价值和持续更新,通过不断复盘和优化,保证知识的鲜活性;而Maggie Appleton 的数字花园(Digital Garden)则通过图像化、模块化设计,鼓励思维的自由生长和创意的涌现。

下面我具体介绍下这些方法。

卡片盒笔记法

卢曼卡片盒笔记法,由德国社会学家尼可拉斯·卢曼(Niklas Luhmann)首创,是一种旨在捕捉想法、促进思考和提升写作效率的笔记方法论。卢曼通过这种方法积累了大量的学术成果,出版了50本专著和600篇论文,而这一切的背后离不开他所依赖的笔记系统——卡片盒笔记法(Zettelkasten)。卢曼卡片盒笔记法不仅是一种笔记方法,更是一种思维方式。通过卡片之间的非线性关联,它帮助用户构建属于自己的知识网络,实现从知识积累到创造性输出的跃升。随着技术的发展,这一方法论已融入到现代知识管理工具中,为更多人所用。在当今信息爆炸的时代,Zettelkasten 系统无疑为那些渴望深度思考、持续创新的人提供了一条独特的路径。

卢曼卡片盒笔记法的核心概念。

卢曼笔记法的核心在于建立一个由相互关联的知识卡片组成的网络,即所谓的“思想之网”(Web of Thoughts)。每张卡片记录一个独立的想法,并通过超文本链接形式将不同的卡片联系在一起,形成非线性的知识结构。这一系统不仅帮助卢曼理清思路,更激发了新的创意,成为他与自己长期智性交流的重要工具。

卢曼笔记法包含以下几类笔记:

  1. 闪念笔记:任何突发的想法都应立即记录下来,形式不拘,记录方式灵活,但应在一两天内进行整理,以免遗忘或混乱。

  2. 文献笔记:无论是读书还是阅读文献,随时记录下不想忘记的内容,尤其是那些可能对未来的思考或写作有用的知识点。这类笔记应尽量用自己的语言进行总结,并保留参考文献信息,以便后续引用。

  3. 永久笔记:这是基于闪念笔记和文献笔记的深度思考和凝练。这类笔记力求简洁,并在卡片之间建立链接,以便于构建系统化的知识网络。

通过这些笔记的链接和积累,卢曼的卡片盒系统能够自然生长,逐渐发展出完整的主题和观点。这种有机生长使得知识网络不仅在规模上扩展,还在深度和广度上不断完善。这种系统让笔记不再是孤立的存在,而是一张由想法交织而成的知识网络。它在促进创意生成、增强记忆和理解、以及支持长期项目管理方面,展现了独特的优势。

卡片盒是卢曼笔记法的基础工具。这一系统强调笔记的独立性和相互之间的连接性,而非传统的线性笔记方式。每条笔记被视为一个原子化的知识单元,能够随时被重新组合和调用。通过这些连接,知识逐步沉淀,并从下而上形成主题,帮助用户进行深度的思考和写作。

卡片盒系统的要素有下面几个要素:

  • 原子性:每条笔记仅包含一个清晰的、独立的想法(thought)。这样的笔记结构便于信息的重新组合,并且能够灵活地应用于不同的主题。
  • 高可见性:通过笔记之间的相互连接,卡片盒系统能够迅速复现信息。每一个想法都嵌入了丰富的上下文,便于用户在需要时迅速找到相关信息。
  • 可传递性:卡片盒笔记系统中的每条笔记不仅是对信息的简单记录,它们经过深度加工,具备高度的实用性和复用性。通过打破传统笔记的“存完即弃”困境,它为未来的思考和写作提供了持续的支撑。

卡片盒系统强调非线性的关联性,这使得看似不相关的想法能够通过笔记的连接生成新的见解和创意。每条笔记独立存在,要求用户在记录和关联过程中进行深度思考。这种思考有助于增强对信息的理解和记忆。卡片盒系统特别适合处理需要长期积累的项目。用户可以随着时间的推移不断补充和完善笔记,最终形成系统化的知识成果。

卡片盒系统通过这种有机的知识生长能够在解决具体问题时,自动调整规模和结构,逐步呈现出一个自洽的思想体系。

使用卡片盒系统的流程如下:

  1. 创建笔记:每条笔记应简洁明了,并且包含一个独立的想法。为每条笔记分配一个唯一的标识符,便于后续引用和关联。

  2. 建立关联:将相关的笔记通过链接相互连接,形成网络结构。关联的方式可以是通过标识符引用,也可以是索引卡片。

  3. 维护与扩展:定期复查笔记,更新或者删除不再相关的内容。随时添加新笔记,并在已有的知识网络中寻找新的关联。

每条笔记都有其独特的结构:

  • 唯一标识符:为每条笔记提供唯一的引用地址。
  • 笔记正文:简短而独立的思想记录。
  • 参考文献:如果笔记来源于外部,需在底部注明引用来源。

卢曼在《How to take smart notes》一书中描述了他的笔记流程:
他首先记录下临时的闪念笔记,随后根据文献笔记进行深度思考,并最终撰写永久笔记。在撰写永久笔记前,卢曼会先回顾已有的相关笔记,判断是否需要撰写新的笔记或更新已有内容。这一过程本身就是信息的综合和创新的过程。

卢曼卡片盒笔记法的成功并不仅仅依赖于其系统本身,更重要的是长期的坚持与智性交流的投入。

随着 Roam Research 的发布,卢曼卡片盒笔记法在全球范围内再度引发关注。如今,Obsidian、Notion、Logseq 等众多笔记应用纷纷将卡片盒笔记法引入其中,并成为知识管理领域的重要工具。这些工具通过双向链接和图谱化笔记展示,延续并扩展了卢曼的思想之网。

尽管现代软件让笔记系统的操作更加便利,卢曼的成就依然更多源自他长期的智性投入,而非工具本身。卡片盒笔记法为他提供了持续智性交流的基础,但真正驱动这一切的是卢曼对知识的持续探索和真诚对话。

P.A.R.A.

在《Building a Second Brain》一书中,Tiago Forte 提出了一个高效的笔记组织框架——P.A.R.A.,即 Projects(项目)、Areas(领域)、Resources(资源)和 Archives(档案)。P.A.R.A. 框架为笔记提供了一个清晰的组织结构,确保我们可以系统性地管理和利用自己的知识储备。通过将笔记分类到 Projects、Areas、Resources 和 Archives,我们不仅能够有效管理当前的工作,还能为未来的项目做好充分准备。这种方法让我们不仅着眼于眼前,还能在长远的时间跨度中,更高效地利用笔记资源。P.A.R.A. 也是指导我打标签的一个很好的理论依据。

Projects:正在进行的项目

Projects 是指当前正在进行的具体项目或任务。例如,你正在学习的一门课程,或是开发中的某个产品,甚至是工作中需要处理的某项任务。任何具有明确目标和时间限制的活动都可以被归为项目。它们是动态的,随时会有进展,也会在某个时间点结束。因此,将与项目相关的笔记进行专门整理,可以让我们时刻聚焦在当下的工作目标上,避免混乱。

Areas:长期关注的领域

与项目不同,Areas 是长期关注的领域或兴趣点,这些领域没有特定的终点,而是贯穿个人发展和生活的各个阶段。例如,理财、编程、职业规划、商业等,都可以视为 Areas。每个领域下可能会有多个项目,但它们共同的特点是长期存在且没有明确的时间限制。通过将笔记归类到不同的领域,我们可以更好地跟踪和维护个人的长期兴趣与发展,确保在这些方面的知识积累和成长。

Resources:潜在的参考资料

Resources 是指那些在未来可能会用到的参考资料。它们是为了长远的项目和兴趣领域做准备,具有较高的参考价值,但并不一定会立即用到。例如,虽然你目前不从事画画相关的工作,但你可能会意识到这是一个未来会从事的领域,因此可以将与画画相关的资料和笔记归类到 Resources 中,方便将来需要时快速查阅。

这种资源型笔记的核心价值在于为未来的工作和学习打下基础。尽管当前未必立刻能用到,但这些知识储备可以帮助你在将来更快速地找到所需的信息,避免被大量无关内容所淹没。

Archives:已完成的项目

Archives 是指那些已经完成或不再活跃的项目。这些笔记虽然不再是工作重点,但它们的价值在于历史记录和参考。如果未来某个新项目需要借鉴之前的经验或成果,我们可以迅速从 Archives 中提取出有用的信息。将完成的项目归档有助于清晰区分当前的任务和过去的成果,避免无关内容干扰当前工作。

P.A.R.A. 框架中有一个核心理念:笔记应保持独立性,不应过度依赖于特定项目。也就是说,尽管笔记可能源自某个项目,它们应该具备跨项目的适用性。这种独立性意味着每一条笔记都有可能在未来的不同情境下重新发挥价值,而不仅仅局限于当前的使用场景。

知识的挑战在于其跨时间的应用。我们记录笔记,不仅是为了当前的需求,更是为未来的自己铺路。通过精心的标签和分类,我们能够为未来的项目储备知识,并迅速从过往的笔记中提取信息,避免重复劳动。

笔记管理的目标,不仅是积累知识,更是为了在需要时迅速找到相关内容。通过 P.A.R.A. 的结构化管理,我们为笔记增加了跨时空的联系。即便某条笔记当前无法用于手头的工作,它也可能在未来某个项目中发挥重要作用。而得益于 P.A.R.A. 框架的有序分类,我们可以轻松地从相关领域或资源中找到这些笔记,从而提升工作效率和知识处理的深度。

费曼学习法

费曼学习法,又称为费曼技巧(Feynman Technique),起源于诺贝尔物理学奖得主理查德·费曼(Richard Feynman)的教学风格和学习方法。费曼以其通俗易懂、深入浅出的讲解风格闻名,被誉为“伟大的解释者”。费曼学习法的精髓在于简化复杂、输出内化。它并不是简单地重复记忆,而是通过解释和简化来发现知识中的盲点并弥补这些不足,最终达到真正掌握的目标。通过这种不断迭代的学习方法,学习者不仅可以在短期内取得良好的学习效果,还能够在长期项目中构建起更加深厚的知识基础。这种方法对任何学习领域都适用,帮助我们将所学知识融会贯通,真正掌握其核心内容。

费曼能够在多个领域取得显著成就,这与他独特的学习方法密不可分。费曼曾分享了他在学习和思考中的一个技巧:他会在心中存有十几个自己感兴趣的问题。尽管这些问题不会时刻占据他的思维,但每当他看到新的研究成果或发现时,都会主动将其与自己关心的问题联系起来,看看这些新的知识能否为自己已有的问题提供答案。此外,他会将学习过程中的笔记归类到这些问题中,日积月累地丰富自己的知识体系。随着时间的推移,他的问题下的笔记越来越多,不同的知识点之间也逐渐建立起联系,这大大加深了他对问题的理解。

费曼学习法的核心思想是通过简化和输出知识来促进深度理解,避免“假学习”带来的错觉。具体而言,这一方法可以分为以下四个步骤:

  1. 确定目标并开始学习:首先,明确要学习的知识点或概念,并将其写在纸的最上方。此时,学习者应该尽可能主动地获取相关信息,学习新知识,并将其记录下来。这里的记录不仅仅是机械地抄写,而是要通过自己的语言对知识进行总结和梳理,确保内容能够被自己清楚理解。
  2. 教别人:费曼学习法的核心在于输出。为了检验自己是否真正掌握所学内容,学习者应该尝试将知识讲解给他人。最好选择一个知识储备较少的对象,比如一名小学生或者没有相关领域背景的人,确保讲解时使用简单的语言和易懂的表达。如果能让对方听懂并理解,说明自己对这部分知识的掌握已经相对深入。
  3. 遇到问题,返回学习:在讲解的过程中,学习者可能会遇到无法清楚解释或讲解的问题。这通常意味着对该部分知识的理解并不透彻。此时需要返回第一步,重新学习这一部分内容,找到理解中的盲点,并弥补知识上的缺漏,直到能够顺畅地解释。
  4. 不断简化:一旦能够顺畅讲解,学习者需要进行进一步的精简。目标是使用最简单的语言和最直观的表达方式来描述复杂的概念。通过不断简化和优化表达,学习者不仅能加深对知识的理解,还能内化这些知识,形成自己的认知框架。

费曼技巧不仅适用于短期的学习项目,也可以应用于长期、深度学习的过程中。具体步骤如下:

  1. 目标设定:首先,确定学习对象和学习目标,明确自己想要掌握的知识领域,并通过设定目标来增强专注力。
  2. 知识理解与分类:在学习过程中,将所学知识进行归类、结构化,尝试对不同的知识点进行对比、筛选,找到其中的关联性和关键点。这个过程可以帮助学习者建立一个更加系统的知识结构,提升对学习材料的掌握程度。
  3. 输出和解释:费曼特别强调了“输出”的重要性。在这里,学习者可以尝试用最简单的语言将复杂的概念解释给一个词汇量和认知水平较低的人,例如一个六年级的学生。六年级的学生只是一个象征,指的是讲解对象需要有基本的理解能力,但不能过于复杂。通过简化语言来解释知识,有助于学习者发现自己对概念的掌握是否足够深刻。
  4. 回顾和反思:在解释的过程中,学习者可能会发现一些自己没有理解透彻的地方。这时,需要对这些问题进行回顾和反思,进一步深入学习,确保对所有知识点都有透彻的理解。
  5. 内化知识:简化和反思的最终目标是内化知识,形成自己的知识体系。内化不仅仅是记住知识点,而是能够自如地运用这些知识,并灵活应对不同的情境。通过不断迭代和优化,学习者可以将复杂的概念融入自己的认知框架,达到深度掌握的状态。

CODE 外脑扩展的有效方法论

CODE(Capture, Organize, Distill, Express)方法论通过四个步骤帮助我们构建外脑(ExoBrain),将海量的外部信息转化为有价值的内部知识。

这些步骤是帮助我们管理、整理和应用外部信息的关键工具。

  1. 捕获(Capture)
    捕获阶段的核心目标是收集一切引发共鸣、激发灵感的信息。无论是阅读的文章、听到的播客、看到的有趣图片,还是日常生活中的随想,这些信息都值得被记录和保留。通过有效的捕获,我们可以避免有价值的信息流失,建立起一个丰富的知识库,随时准备后续的进一步处理。

  2. 组织(Organize)
    捕获的信息一旦过多,如果不加以整理,就会形成混乱的信息堆积。组织步骤正是为了将这些杂乱的信息进行分类、存储和管理。PARA 模型(Projects, Areas, Resources, Archives)是这一阶段的重要工具,它将信息按照项目、职责领域、参考资料和档案等四个类别进行归类。通过这种方式,知识库变得有条理,便于日后检索和使用。

  3. 提炼(Distill)
    捕获和组织后的信息并非最终形态。提炼阶段的目标是将这些信息进一步筛选、精简,提取其中最有价值的部分,形成知识的核心。提炼并不仅仅是简单地总结,而是通过对信息的深度理解,找到最为精华的内容,以便未来在不同的应用场景中高效使用。

  4. 表达(Express)
    知识的真正价值不仅在于被吸收,更在于被运用。表达步骤是整个流程的最后一个环节,也是至关重要的一步。如果我们仅仅停留在被动的输入阶段,而没有主动去使用这些知识,捕获、组织和提炼的工作都将是徒劳。表达可以通过写作、教学、项目实践等形式实现,只有在实际应用中,知识才能在大脑中内化,真正转化为自己的认知体系。

CODE方法的核心目标是增强创造力。

CODE 帮助我们通过结构化的捕获和组织,避免无序的信息输入,减轻信息过载的压力。这样,我们能够在有条理的环境中,逐步消化和处理信息,而不是被无用信息淹没。我们不需要记住每一个细节,CODE 允许我们保存所有接触到的有价值的资源,如文章、播客、书籍等,并能够在需要时快速检索这些信息。这不仅节省了我们大脑的记忆容量,也提高了信息获取的效率。通过组织和提炼,我们可以快速找到之前阅读、学习或思考过的内容。这使得信息的重复利用变得更加高效,无需浪费时间在无止境的搜索和重复整理上。CODE 的逻辑化结构不仅使信息存储更加高效,同时也支持我们进行创意工作。因为我们将更多的时间花在了想法的连接和发散上,而不是信息的整理和寻找上,这就为创新提供了更多的空间。通过表达环节不断实践,这些外部信息会逐渐内化,形成我们独特的洞见。这一过程也是费曼技巧中强调的“通过教学强化学习”的重要体现。

按网状和树状整理笔记

如何有效地组织和分类笔记内容一直是一个重要的课题。不同工具提供了不同的解决方案,从传统的文件夹结构到更加灵活的标签和链接连接方式,各自都有其优势与适用场景。

目前,笔记的连接主要有三种方式:

  1. 文件夹连接:通过树状结构将笔记放入预定义的文件夹中,适合用于管理层次清晰的内容。
  2. 标签连接:通过为笔记添加标签,将相关的内容进行分类。BearObsidian 等工具都依赖这种方式来组织笔记。
  3. 链接连接:通过双向链接建立笔记之间的直接关联,网状结构中这种连接方式尤其常见,Roam Research 等工具对此提供了强大的支持。

传统的笔记管理工具大多依赖文件夹系统,将笔记按照预设的类别和层级进行组织。然而,这种“树状结构”在面对动态、未确定的内容时往往显得过于僵化。许多用户在初期使用这类工具时会陷入这样的困境:为了将信息放置在“正确”的位置,他们不得不花费大量时间去思考如何分类,而这种预设的分类标准往往会随着知识的增加而变得不再适用。正如“即兴挥毫卡片乐,后续整理泪成河,整理之时心焦灼,犹如置身火中燎。”,过早地进行结构化可能会导致后期的整理变得困难且低效。

Roam Research 为代表的工具取消了传统的文件夹体系,允许用户在记录初期不必考虑如何归类,直接创建笔记并通过“链接”在笔记之间建立联系。这种网状结构的设计让用户在不确定分类标准的情况下也能高效地记录想法,随着时间的推移,笔记之间的关联和结构自然浮现。这种灵活性极大地提高了知识积累的效率,特别是在应对碎片化信息时显得尤为突出。

工具如 The Archive 也遵循相似的理念,主张通过建立笔记之间的相互联系来代替传统的文件夹分类。Bear 更是完全放弃了文件夹系统,转而依赖标签来组织内容。

尽管网状结构适合存储那些零散、非线性的思维碎片,例如阅读论文或文章时产生的想法,然而对于某些特定学科的系统化学习,如学习一门编程语言或阅读某本书时所产生的笔记,树状结构仍然具有重要的作用。树状结构提供了更清晰的层级和逻辑顺序,能够帮助用户按主题或学科对知识进行归类。这种结构对于那些需要线性逻辑的知识管理尤其适用,例如教程、课程笔记或研究项目等。

通过结合网状结构和树状结构,用户可以根据内容的特点选择适当的组织方式。碎片化的知识可以通过链接或标签形成灵活的网络,而系统化的知识则可以通过文件夹或目录形成层次分明的树状结构。

CraftObsidian 依然保留了文件夹体系,以帮助用户按照传统方式组织内容。Craft 甚至支持将整个文件夹导出,并将附件单独储存在对应的文件夹中,便于离线访问。

为了在网状和树状结构之间实现平衡,维护子级目录和索引是一个有效的策略。用户可以自下而上建立目录,随着笔记数量的增加,不断更新目录,确保内容的逻辑性和条理性。同时,可以通过整理相同主题下的笔记链接,建立专题索引,便于后期的高效查阅。通过这种方法,网状结构和树状结构可以有机结合,帮助用户既保留灵活的笔记结构,又能够确保内容的系统化管理。

消化

收藏是信息管理的起点,但许多人却将其误认为终点。我们习惯于囤积信息,却很少真正去消化和利用它。好比看到美食时,我们不会只将其拍照收藏,而是选择食用。而对于信息,我们却常常囤积,而不是将其内化为知识。这样的行为,像是画饼充饥,最终只能带来焦虑和无用的负担。

现代人越来越依赖于快速信息消费,如短视频、播客解说等,试图在有限时间内处理更多内容。然而,这种方式并未真正提升信息的有效处理,反而带来了更多纷扰和焦虑。大量信息快速流过,反而让我们感到空虚与不满足。信息的快速获取并不等于知识的积累。人类大脑有遗忘机制,唯有通过理解和重复,才能将信息真正转化为知识。尝试通过工具和方法论快速清理信息,只会加剧内心的焦虑感,真正的解决之道在于深度学习和反思,而非追求广度和数量。通过深入思考和识别真正的需求,才能突破信息积压。思考的过程需要时间,一周、一个月甚至一年,都是值得的。一旦想清楚了,不仅可以解决信息囤积,还可以释放心理负担,带来身心的轻松和通透。

真正的难题并不是将想法写出来或做笔记,而是如何有效地思考。思考是一种能力,需要通过控制思维形式和内容来加以锻炼。人类大脑有过滤机制,自动忽略与我们现有观念相悖的信息。这种选择性过滤是大脑的局限性,然而,它也阻碍了认知的扩展。因此,真正有效的知识管理,不只是收集信息,而是通过思考来内化新的认知。

心理学家科特勒提出,大脑的认知系统可以分为三大网络:

  1. 注意网络(Central Executive Network):专注于当前任务,帮助锁定思考的焦点,如同聚光灯一样引导我们的注意力。
  2. 想象网络(Default Mode Network):负责发散思维,产生创造力的来源。这一网络允许我们随机浮现想法,将分散的概念连接在一起。
  3. 突显网络(Salience Network):监控和评估各种想法,决定哪些值得进一步深入思考。强大的突显网络能帮助发现其他人未注意到的灵感。

这些网络的协同工作,决定了我们的创造力和思维质量。

法国社会学家布鲁诺·拉图尔指出,“不加质疑地相信或使用某个事实,会强化其合理性。”这与心理学家丹尼尔·卡尼曼的观点一致:人类大脑会将熟悉度等同于合理性,重复的观点更容易被大脑认同。商家利用这一特性进行广告营销,而我们可以通过反复引用和巩固记忆来更好地内化知识。

20世纪50年代,芝加哥大学教授布鲁姆提出的教育目标分类理论,将知识的认知过程分为六个层次:记忆、理解、应用、分析、评价和创造。学习的最终目标在于达到最高层次的“创造”,即通过对已有知识的重组,产生新的想法和模式。

第二大脑的概念,可以理解为一个外置的存储系统,帮助大脑减少负担。大脑就像计算机的CPU,而第二大脑则类似于内存和硬盘。它存储那些我们暂时不需要处理的内容,从而让第一大脑专注于当前任务。有效使用第二大脑,能让我们更高效地管理知识,提升思维的深度和广度。

有效的记忆策略,如间隔重复(Spaced Repetition),能够显著提升记忆效果。基于艾宾浩斯遗忘曲线的理念,使用如SuperMemo和Anki等工具进行渐进式阅读,能够帮助信息的有效吸收和转化。

位置记忆法(也称为记忆宫殿法)是一种古老且有效的记忆技巧。通过将记忆对象与特定空间位置进行关联,能够大幅提升记忆效果。这种方法可以追溯到古希腊时期,现如今被广泛应用于各种学习和知识管理场景中。

输出

在我们日常学习中,做笔记的根本目的,是为了提升产出力。而写作作为一种极其重要的产出形式,必须遵循一套清晰且结构化的流程。这个流程并不仅仅局限于文字的创作,它还包括了如何将生活中的灵感、阅读中的思考转化为最终的成果。

写作是一项有条不紊的工作,通常可以分为六个关键步骤:

  1. 灵感捕捉:随时随地收集日常生活中的写作灵感。这是写作的源头,灵感往往来自我们对周围环境的观察、对问题的思考,甚至是一些闪念。

  2. 阅读与内化:通过阅读他人的作品,获取知识,并将其转化为属于自己的永久笔记。这些笔记并非简单地记下原始信息,而是经过自己的思考和理解之后的产物。

  3. 思想碰撞:将新获得的知识与已有的认知相结合,在思考过程中产生新的想法。这种思想的碰撞有助于形成新的主题或独特的观点。

  4. 选题与规划:从众多灵感中筛选出可行的主题,并根据主题构思大纲,整合可以使用的素材和信息。

  5. 起草初稿:开始写作时,要允许灵感在过程中随时出现并调整原有的大纲。这是创作的灵活性所在。写作是动态的过程,初稿的产生往往伴随着对原有思路的修改和丰富。

  6. 校对与定稿:以读者和批评家的视角审视作品,修改错别字、调整不通顺的句子,并对不合理的地方进行修正和补充,最终形成定稿。

预写:从构想到初稿。

写作不只是动笔的瞬间,它的准备工作——预写,同样至关重要。Donald M. Murray 在其著作《Write before Writing》中提出,预写是正式写作之前的思维积累过程,虽然这种准备工作常常被忽视,但它实际上占据了作家大量的时间和精力。预写阶段的思考可以发生在脑海中,也可以记录在各种手稿和笔记中,这为正式写作打下了基础。

常见的预写策略包括:

  • 头脑风暴(brainstorming):快速列出可能的写作主题或思路。
  • 列提纲(outlining):框定文章的整体结构。
  • 自由写作(focused free-writing):不拘形式地写下与主题相关的想法。
  • 日志写作(journaling):通过记录日常的感悟,为未来的写作提供素材。
  • 思维导图(clustering/mapping):以图表形式呈现主题和关联内容,便于梳理逻辑。
  • 5W1H 分析法:通过“谁、什么、何时、何地、为什么、如何”来分析主题。

这些预写策略帮助作家将零散的灵感和笔记转化为连贯的写作思路,它们是从想法到初稿的桥梁。

重写:雕琢作品的过程。

写作并不只是一次性的输出,重写是其中不可或缺的部分。米开朗琪罗曾形容他雕刻《大卫》像的过程是“去掉多余的石头”,写作的重写过程亦然。重写意味着在已有的草稿基础上,进行内容的修改、优化和精炼。这是一个从粗糙到精细的过程,通过不断打磨,最终形成符合预期的成品。

写作并没有固定的模式,不同的写作方法适用于不同的创作需求。比如:

  • 雪花写作法:从一个简单的概念开始,逐步扩展和细化。
  • 分轨写作法:将不同内容模块分开处理,再合并为一个整体。
  • 细胞写作法:将文章拆分为若干个独立的细胞,再将这些细胞组织成文章。
  • 九宫格写作法:通过图表形式列出不同主题或想法,并在其中找到最具潜力的部分展开写作。
  • 卡片写作法:利用卡片记录灵感和笔记,并通过组合和重新排列形成新的写作思路。
  • 渐进式写作:逐步完善文章,从初稿到定稿,逐层递进。

无论采用哪种写作方法,最重要的理念是“写在当下”:立即写、随时写、多写。通过这种不断的输出,灵感才能真正转化为文字,积累为知识,最终实现有价值的产出。

现代科技提供了多种强大的工具来辅助写作和组织想法。其中常见的有:

  • Workflowy:这是一款列出和整理想法的工具,支持双向链接和镜像功能,非常适合处理灵感和构思阶段的内容。
  • OmniOutliner:稳定且功能强大的大纲编辑软件,尤其适用于构建工作相关的文件或复杂的文档结构。
  • iA Writer & Ulysses:这两款文字处理器适合撰写复杂的项目文件,iA Writer 因其对 Markdown 格式的完美支持、打字机滚动和专注模式,成为众多作家的首选工具。
  • PDF Expert:一款功能强大的 PDF 处理软件,支持阅读、注释、重点标记及文件处理功能,适合在写作过程中处理文档和参考资料。

特别是 iA Writer 的稳定性、开放性以及对 Markdown 格式的支持,使其在众多候选者中脱颖而出。通过直接在 Obsidian 的库路径中建立写作文件夹,并在 iA Writer 中打开,可以轻松实现写作和文件管理的统一。

以最终目的为导向的工作流

由于每个人的目标不同,所需的工作流模式也会有所区别。学生、研究者和职场人士的目标各不相同,因此他们的工作流系统对笔记和信息管理的需求也截然不同。

学生以考试为目标,强调记忆系统和概念结构的构建。他们更关注如何将信息分门别类地整理,以便在短时间内高效记忆。研究者以发表论文为目标,重点在于文献引用和观点交互,确保能够基于已有的理论框架创新和推进学术讨论。职场人士以推进工作进展为目标,重视的是决策过程中的思考和项目行动的有效推进,他们需要快速将信息转化为可执行的项目方案。

在知识管理领域,不断有新的概念和工具被创造出来,诸如常青笔记、PARA体系、卡片笔记法、双链系统、MOC等。然而,真正有效的方案并非盲目追随这些技术名词,而是在更高的层面上,独立思考自己的需求,创建一个稳定可靠的个人工作流。

笔记方法和工具都是锦上添花的辅助工具,真正重要的是我们内心对于目标的驱动力,即对自己的incentivepassion的认知。这是任何外在工具都无法给予的核心动力。正因为如此,很多人对工具的无用论产生共鸣:当一个人内心目标明确坚定时,无论使用何种工具或方法,他们都会找到实现目标的途径。对我而言,正是在明确了自己未来职业发展的目标之后,才真正有了针对性地创造自己的工作方法。这就是独立的意义所在。

因此,当你陷入迷茫,不确定工具或方法是否对你有帮助时,不妨停下来问问自己:我究竟想要做什么? 一旦你明确了这个问题,工具和方法的选择将会变得清晰无比。它们将自然而然地围绕你的目标,帮助你在各个环节实现最优效果。卢曼的卡片笔记法是一个典型的例子。在他的体系中,卡片笔记充当了项目和原始材料之间的中间层。如果没有具体的项目作为目标,笔记本身也就没有意义。笔记只有在与现实中的问题和目标相关联时,才会发挥出它的价值。

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

本文主要想说说 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 模块,供其他项目复用。

海贼王之感人名场面

这次播客是我,柠檬和田阳一起聊了下空岛篇以及之前的故事。田师傅渊博的海贼王知识非常值得一听。可在小宇宙收听,或扫码下图。

海贼王为什么有那么多令人感动的看一次哭一次的关于友情的名场面,我想可能是因为这些都是生活中所难以获得的美好与渴望吧。

下面让我看一次感动一次的名场面。

第一个是香克斯为救路飞丢掉一只手臂,让我感动的是香克斯的气度,也是因为他的气度一只影响着路飞后面走的路。

接下来是索隆的回忆,回忆中他的竞争对手,榜样和目标霜月克伊娜向他展露了柔软的一面,可能索隆还小不能理解她,但却让索隆因此更加坚定了自己的目标,同时带上了克伊娜的那份。

只是一只小狗,没有能力守护什么,却不言放弃,还默默接受着结果。这感动了路飞,也让我了解到了海贼王的意义。

一个人几十年一直在追求追寻自己想得到的宝箱,在得到后发现是虚无。他可能不会埋怨了,只会感谢珍惜身边那些充满善意,善良的人吧。

路飞和黑猫队长对战,发现黑猫队长是个只顾自己,只会利用伙伴的人,于是发出感叹,说他能力再强又怎样,还不如乌索普。这是因为什么呢?是气度,乌索普志气是保护身边的人,远望着无边无际的大海,有了这样的气度才能够有远航,冒险和称霸海洋的权利。

仪式感的解散,代表着乌索普和孩童时代的告别,告别舒适、安全,去面对挫折,煎熬。还好有着相互信赖的伙伴。

当两个人面对未知未来的时,是自保还是成全对方,这是对人性的考验。哲夫的选择换来了山治的感恩。

成年后的山治表面上看着吊儿郎当,但对哲夫的第二次施恩,却是发至肺腑的感激。哲夫真切的关心,和山治的下跪,让我深深感到他们之间的羁绊有多深。

不管娜美如何为了自己村子而背叛了路飞他们,路飞都是一只相信着她,娜美一只都不敢轻易去信任别人,直到最后无路可走时,才向路飞求助,路飞一如既往的相信着娜美,因为他从一开始就知道娜美是善良的。这种无条件的付出,赢得了娜美的信任。

在和路飞分别时的这段非常感人。海军的迫近,让此时无声更胜有声,这就是伙伴的标记。

当罗宾下决心要通过牺牲自己来保护大家,但大家全然不顾的追来了,她的身世决定她的未来是暗淡的,她也担心未来大家会抛弃她,于是她想死。但是与世界为敌又怎样,一直以来都是牺牲小我成全大我,但是路飞和乌索普的行为让罗宾明白,再渺小的人为了珍爱的人也可以和世界为敌。

有一群愿意和你一起走下去的伙伴,那么大海也就是你的。这一段是看一遍哭一遍,令人难忘。这部漫画在我心中永存。

鸟山明和他的龙珠

前言

上次录了期圣斗士的播客,还写了篇文章。这次录了期龙珠话题的播客,聊下龙珠,说下龙珠的故事,鸟山明与龙珠的那些事,比如他的助手、编辑以及他平日的一些爱好。作为一个老的漫画爱好者,我还会说说以前海南版七龙珠和画书大王杂志的一些事。本期播客地址,或扫码下图中二维码收听。铭莉双收播客已经有了 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年那会儿,《画书大王》,大家都叫它“画王”,咱心里的一道光!那感觉,就像是大家坐一块儿,平等交流,一块儿进步,全靠一腔热爱撑着。这杂志虽然薄薄的,不到百页,但里头啥都有,从鸟山明、北条司到高桥留美子这些国际大咖的作品,再到咱们国内第一代漫画家王庸声、谭晓春、陈翔他们的原创故事,那叫一个丰富多彩。

最让我开眼界的是,它不光有漫画看,还时不时来点法国漫画、纸雕漫画,世界各地的风格都能在这上面找到,学校里那美术课本可没这待遇!

画王连载漫画的同时,还教你咋画漫画,有套连载叫《漫画研究所》,鸟山明亲自上阵,从零开始,一步步教你。虽然最后几页讲怎么给集英社投稿,对国内来说用处不大,但人家还是完完整整给咱们搬来了,这份诚意,没得说!

我看画书大王的起因主要是因为杂志里面有刊登打败沙鲁之后的内容。每期我都有跟,同时也看到了更多的漫画,以及漫画背后作者画漫画的事情,这些点燃了我对漫画这个行业的热爱。

杂志是王庸声老师创办,初心是为了中国漫画的未来。可惜,画王只坚持了两年,就赶上那时候对漫画的“风波”,没了它,中国漫画就像突然被掐了脖子,艰难前行。就算有其他杂志接着干,但总感觉少了那么一股子劲儿。

那时,总会有那么群人,带着纯纯的热爱,艰难却很快乐的坚守者这座小城堡。

圣斗士星矢的前世今生,车田正美的坚持,城户光政的阴谋

前言

我最近和家人们一起做了一个播客,名叫《铭莉双收》,本文内容是对最新一期播客“还有人看圣斗士星矢吗?”的一个补充,欢迎大家订阅收听。

提到圣斗士星矢,大部分人都是通过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年代,那么肯定经历了抗日战争,日本人在庐山烧杀抢掠时童虎无动于衷么,难道是因为超过了参军年龄。

紫龙的父亲是城户光政,母亲是庐山人,应该是叫照香炉,紫龙从小跟随母亲,随母姓,人称庐山照紫龙,他还有个妹妹叫紫烟,有诗为证:
日照香炉生紫烟,遥看瀑布挂前川。
飞流直下三千尺,疑是银河落九天。

梵高灵感的来自江户浮世绘,颜气江户APP来自江户的色彩

前言

看过我前面博客的同学会发现我很喜欢自己画些配图,每次给画配色都会有些新的体会,却不知道是什么,于是每次都要经过一轮新的摸爬滚打才能调到自己满意的颜色,最近我为了能够提高自己的配色效率,就专门的看了一些色彩相关的书籍,学习了点色彩的理论,还专门的手写了一款 APP,提高了上色的效率呢。这个 APP 叫颜气江户,我已经发到苹果商店(28元),iOS、iPad 和 macOS 商店都可以找到,搜索“颜气江户”。下面我会聊一点色彩的理论,以及怎么将其融入到 APP 中的。

颜气江户 APP 有一些色彩 APP 的基本功能,200个江户色卡,色卡配图和配色表,以及识别图片配色等功能。

舒服的配色

首先,无论你是否画画,你都会觉得一些配色看起来很舒服,也会觉得有些配色看起来很丑。这些其实是有一定规律可循的。你的眼睛会寻求中等灰色才会达到一种平衡状态,互补色调合后会变成灰色,所以灰色和互补色很容易让配色舒服。因此苹果笔记本常年都是主打银灰色,macOS 操作系统的窗口和 Sidebar 一直以来也都是灰色的,非常耐看。有颜色的手机,单个不耐看,放在一起看着就很舒服,也是这个道理。

色彩即是物理的、化学的,还是生理的和心理的。物理主要是关于光谱和色彩光波,化学就是染色和载色剂相关的东西,对于艺术美学来说,主要是生理和心理方面的作用,看着舒服就是生理方面的,生理学家埃瓦尔德·赫林就说过眼睛和大脑需要中等灰色,没有的话大脑会处于不安状态。

这种灰色是可以通过色彩调合而成,最简单的调合方式就是互补色加白色。

互补色

那么什么是互补色呢。灰色好理解,互补色可能就比较复杂些了。严谨点的理论上通过色轮可以获取不同数量的互补颜色组合。比如双色互补是色轮上的直线段两端的颜色,三色互补是色轮上构成等边三角形火等腰三角形的三种色彩,四色互补是色轮上构成正方形或长方形的四种色彩。

虽然互补色会让画面配色看起来和谐舒服,但是有些艺术作品是需要有冲击性的,又或者有鲜明个性的,不然就不好玩了。因此创作时是不用严格按照互补色来配色,根据个人主观喜好来用色就好,当觉得配的不好时再运用互补色的理论调整好了。

对比色

另外,通过对比色可以突出你想突出的内容,有五种常用的色彩对比方法

  • 色相对比:十二色轮的主色
  • 明暗对比:黄色最亮,紫色最暗
  • 冷暖对比:红色和橙色最暖,蓝绿色最冷,风景画中远处色彩较冷
  • 补色对比:调合后成灰色的颜色对比
  • 面积对比:颜色所占比例对比

色彩的意义

色彩的组合其实和音乐的音阶一样,了解音阶的理论并不能创作出美妙的旋律,了解配色原理和每个颜色也不并能创作出美丽的作品。

了解色彩只是一切美好的基础,也是好的开端。

同时色彩的变迁和演变也伴随着历史、文化和艺术的演变,了解更多色彩的知识也能够探索到更多未知的领域,让日子意义感更足些呢。

江户色

19世纪50年代,江户时期的艺术品传入欧洲,在欧洲流行起来,巴黎一些前卫的艺术家很喜欢这些艺术品。1886年梵高来到巴黎见到江户浮世绘后对其产生了极大兴趣。

梵高起初收集浮世绘作品是想在咖啡馆里卖这些作品,结果没人买。梵高作品《铃鼓咖啡馆里的阿戈斯蒂娜·塞迦陀利》中可以看到咖啡馆上挂着浮世绘艺伎图。

虽然梵高没有从浮世绘中获取收入,却从其中获取了创作灵感。他临摹了歌川广重的《开花的李树园》和《大桥安宅骤雨》,还有溪斋英泉的《身穿云龙打挂的花魁》。

梵高《包扎着耳朵的自画像》这张自画像中墙上挂着的是佐藤虎清的《艺者与富士》。梵高曾表示自己很喜欢葛饰北斋的《神奈川冲浪里》,而后创作了最著名的《星月夜》,其中旋转的星云和神奈川冲浪里的浪非常神似。

梵高曾经说过:“如果你研究日本艺术,你看到的是一个充满智慧、哲思和悟性的人将时间用来做什么呢?研究地球与月亮的距离?不。研究俾斯麦政策?不。他研究一片草叶。这难道不是日本人教给我们的真正的宗教吗?他们简单地生活在自然之中如同花儿一样。”

我喜欢梵高的配色,同时也喜欢江户时代浮世绘画中的颜色,因此我打算先从江户时代的颜色入手,先做好研究。

我找了200种当时流行的颜色,颜气江户 APP 目前就内置了这些颜色,并加入了颜色的名字的来源。江户色名主要取自植物、动物、自然现象以及歌舞伎演员等。透过这些色名的来源,我发现还能够了解到江户时期的人们生活文化自然的方方面面。

举几个由歌舞伎演员名命名的颜色的例子,比如団十郎茶、梅幸茶、路考茶、芝翫茶、璃寛茶。

団十郎茶是歌舞伎役者市川團十郎爱使用的颜色,成田屋的标志,是一种偏红的棕色。艺荒事(武戏)代表剧“暂”里所用的服装颜色,还有温泉猴子的脸和柿饼也是这种颜色。

梅幸茶是歌舞伎大师初代尾上菊五郎最喜欢的颜色,颜色名来自菊五郎的俳句名梅幸。

路考茶是江戸的歌舞伎役者二代目瀬川菊之丞襲名王子路考喜欢的颜色,江户各地女性争相模仿,浮世絵美人衣服很多用的也是这个颜色。

芝翫茶是大阪花形役者三代目中村歌右衛門喜欢的颜色,呈偏淡的红棕色,芝翫是他的俳名。

璃寛茶是歌舞伎演员二世嵐吉三郎最喜欢的舞台衣裳的颜色,他的俳句叫瑠寛。虎河豚是一种高级鱼,就是璃寛茶色。

歌舞伎的演员成了明星和时尚的领导者,庶民喜欢把歌舞伎演员喜欢的颜色叫做役者色,将这些颜色衣裳穿在自己身上,会觉得自己很时尚。

另外,江户时代颜色有个最显著的特色,那就是茶色和鼠色很多,统称四十八茶百鼠。

江户中期富裕的商人越来越多,生活变得奢侈起来,更多钱用在了华丽颜色的服饰上,幕府为了避免攀比和浪费,因此提倡节俭,并颁布了奢侈禁止令,让更多的钱用在江户的城市发展上。这样一些以前代表低身份的茶色和鼠色被玩出了花,出现了很多新的颜色,这些颜色变成了流行时尚色。

小册子之 SwiftUI 动画

以下内容已整理到小册子中,本文会随着系统更新和我更多的实践而新增和更新,你可以下载“戴铭的开发小册子”应用,来跟踪查看本文内容新增和更新。小册子应用的代码可以在 Github 上查看。

本文属于小册子系列中的一篇,已发布系列文章有:

SwiftUI动画

SwiftUI 里实现动画的方式包括有 .animation 隐式动画、withAnimation 和 withTransaction 显示动画、matchedGeometryEffect Hero 动画和 TimelineView 等。

示例代码如下:

struct PlayAnimation: View {    @State private var isChange = false    private var anis:[String: Animation] = [        "p1": .default,        "p2": .linear(duration: 1),        "p3": .interpolatingSpring(stiffness: 5, damping: 3),        "p4": .easeInOut(duration: 1),        "p5": .easeIn(duration: 1),        "p6": .easeOut(duration: 1),        "p7": .interactiveSpring(response: 3, dampingFraction: 2, blendDuration: 1),        "p8": .spring(),        "p9": .default.repeatCount(3)    ]    @State private var selection = 1        var body: some View {        // animation 隐式动画和 withAnimation 显示动画        Text(isChange ? "另一种状态" : "一种状态")            .font(.headline)            .padding()            .animation(.easeInOut, value: isChange) // 受限的隐式动画,只绑定某个值。            .onTapGesture {                // 使用 withAnimation 就是显式动画,效果等同 withTransaction(Transaction(animation: .default))                withAnimation {                    isChange.toggle()                }                // 设置 Transaction。和隐式动画共存时,优先执行 withAnimation 或 Transaction。                var t = Transaction(animation: .linear(duration: 2))                t.disablesAnimations = true // 用来禁用隐式动画                withTransaction(t) {                    isChange.toggle()                }            } // end onHover                LazyVGrid(columns: [GridItem(.adaptive(minimum: isChange ? 60 : 30), spacing: 60)]) {            ForEach(Array(anis.keys), id: \.self) { s in                Image(s)                    .resizable()                    .scaledToFit()                    .animation(anis[s], value: isChange)                    .scaleEffect()            }        }        .padding()        Button {            isChange.toggle()        } label: {            Image(systemName: isChange ? "pause.fill" : "play.fill")                .renderingMode(.original)        }                // matchedGeometryEffect 的使用        VStack {            Text("后台")                .font(.headline)            placeStayView            Text("前台")                .font(.headline)            placeShowView        }        .padding(50)                // 通过使用相同 matchedGeometryEffect 的 id,绑定两个元素变化。        HStack {            if isChange {                Rectangle()                    .fill(.pink)                    .matchedGeometryEffect(id: "g1", in: mgeStore)                    .frame(width: 100, height: 100)            }            Spacer()            Button("转换") {                withAnimation(.linear(duration: 2.0)) {                    isChange.toggle()                }            }            Spacer()            if !isChange {                Circle()                    .fill(.orange)                    .matchedGeometryEffect(id: "g1", in: mgeStore)                    .frame(width: 70, height: 70)            }            HStack {                Image("p1")                    .resizable()                    .scaledToFit()                    .frame(width: 50, height: 50)                if !isChange {                    Image("p19")                        .resizable()                        .scaledToFit()                        .frame(width: 50, height: 50)                        .matchedGeometryEffect(id: "g1", in: mgeStore)                }                Image("p1")                    .resizable()                    .scaledToFit()                    .frame(width: 50, height: 50)            }        }        .padding()                // 使用 isSource,作为移动到相同 matchedGeometryEffect id 的方法。        HStack {            Image("p19")                .resizable()                .scaledToFit()                .frame(width: isChange ? 100 : 50, height: isChange ? 100 : 50)                .matchedGeometryEffect(id: isChange ? "g2" : "", in: mgeStore, isSource: false)                        Image("p19")                .resizable()                .scaledToFit()                .frame(width: 100, height: 100)                .matchedGeometryEffect(id: "g2", in: mgeStore)                .opacity(0)        }                                // 点击跟随的效果        HStack {            ForEach(Array(1...4), id: \.self) { i in                Image("p\(i)")                    .resizable()                    .scaledToFit()                    .frame(width: i == selection ? 200 : 50)                    .matchedGeometryEffect(id: "h\(i)", in: mgeStore)                    .onTapGesture {                        withAnimation {                            selection = i                        }                    }                    .shadow(color: .black, radius: 3, x: 2, y: 3)            }        }        .background(            RoundedRectangle(cornerRadius: 8).fill(.pink)                .matchedGeometryEffect(id: "h\(selection)", in: mgeStore, isSource: false)        )                // matchedGeometryEffect 还可以应用到 List 中,通过 Array enumerated 获得 index 作为 matchedGeometryEffect 的 id。右侧固定按钮可以直接让对应 id 的视图滚动到固定按钮的位置                        // TimelineView        TimelineView(.periodic(from: .now, by: 1)) { t in            Text("\(t.date)")            HStack(spacing: 20) {                let e = "p\(Int.random(in: 1...30))"                Image(e)                    .resizable()                    .scaledToFit()                    .frame(height: 40)                    .animation(.default.repeatCount(3), value: e)                                TimelineSubView(date: t.date) // 需要传入 timeline 的时间给子视图才能够起作用。                                }            .padding()        }                // matchedGeometryEffect        /// TimelineScheduler 的使用,TimelineScheduler 有以下类型        /// .animation:制定更新的频率,可以控制暂停        /// .everyMinute:每分钟更新一次        /// .explicit:所有要更新的放到一个数组里        /// .periodic:设置开始时间和更新频率        /// 也可以自定义 TimelineScheduler        TimelineView(.everySecond) { t in            let e = "p\(Int.random(in: 1...30))"            Image(e)                .resizable()                .scaledToFit()                .frame(height: 40)        }                // 自定义的 TimelineScheduler        TimelineView(.everyLoop(timeOffsets: [0.2, 0.7, 1, 0.5, 2])) { t in            TimelineSubView(date: t.date)        }    }        // MARK: - TimelineSubView    struct TimelineSubView: View {        let date : Date        @State private var s = "let's go"        // 顺序从数组中取值,取完再重头开始        @State private var idx: Int = 1        func advanceIndex(count: Int) {            idx = (idx + 1) % count            if idx == 0 { idx = 1 }        }                var body: some View {            HStack(spacing: 20) {                Image("p\(idx)")                    .resizable()                    .scaledToFit()                    .frame(height: 40)                    .animation(.easeIn(duration: 1), value: date)                    .onChange(of: date) { newValue in                        advanceIndex(count: 30)                        s = "\(date.hour):\(date.minute):\(date.second)"                    }                    .onAppear {                        advanceIndex(count: 30)                    }                                    Text(s)            }        }    }        // MARK: - 用 matchedGeometryEffect 做动画    /// matchedGeometryEffect 可以无缝的将一个图像变成另外一个图像。    @State private var placeStayItems = ["p1", "p2", "p3", "p4"]    @State private var placeShowItems: [String] = []        @Namespace private var mgeStore        private var placeStayView: some View {        LazyVGrid(columns: [GridItem(.adaptive(minimum: 30), spacing: 10)]) {            ForEach(placeStayItems, id: \.self) { s in                Image(s)                    .resizable()                    .scaledToFit()                    .matchedGeometryEffect(id: s, in: mgeStore)                    .onTapGesture {                        withAnimation {                            placeStayItems.removeAll { $0 == s }                            placeShowItems.append(s)                        }                    }                    .shadow(color: .black, radius: 2, x: 2, y: 4)            } // end ForEach        } // end LazyVGrid    } // private var placeStayView        private var placeShowView: some View {        LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: 10)]) {            ForEach(placeShowItems, id: \.self) { s in                Image(s)                    .resizable()                    .scaledToFit()                    .matchedGeometryEffect(id: s, in: mgeStore)                    .onTapGesture {                        withAnimation {                            placeShowItems.removeAll { $0 == s }                            placeStayItems.append(s)                        }                    }                    .shadow(color: .black, radius: 2, x: 0, y: 2)                    .shadow(color: .white, radius: 5, x: 0, y: 2)            } // end ForEach        } // end LazyVGrid    } // end private var placeShowView    } // end struct PlayAnimation// MARK: - 扩展 TimelineScheduleextension TimelineSchedule where Self == PeriodicTimelineSchedule {    static var everySecond: PeriodicTimelineSchedule {        get {            .init(from: .now, by: 1)        }    }}// MARK: - 自定义一个 TimelineSchedule// timeOffsets 用完,就会再重头重新再来一遍struct PCLoopTimelineSchedule: TimelineSchedule {    let timeOffsets: [TimeInterval]        func entries(from startDate: Date, mode: TimelineScheduleMode) -> Entries {        Entries(last: startDate, offsets: timeOffsets)    }        struct Entries: Sequence, IteratorProtocol {        var last: Date        let offsets: [TimeInterval]        var idx: Int = -1        mutating func next() -> Date? {            idx = (idx + 1) % offsets.count            last = last.addingTimeInterval(offsets[idx])            return last        }    } // end Struct Entries}// 为自定义的 PCLoopTimelineSchedule 做一个 TimelineSchedule 的扩展函数,方便使用extension TimelineSchedule where Self == PCLoopTimelineSchedule {    static func everyLoop(timeOffsets: [TimeInterval]) -> PCLoopTimelineSchedule {        .init(timeOffsets: timeOffsets)    }}

contentTransition

.contentTransition(.numericText()) 修饰符用于在视图内容发生变化时,以数字动画的方式进行过渡。

struct ContentView: View {    @State private var filmNumber: Int = 0        var body: some View {        VStack(spacing: 20) {            Text("\(filmNumber)")                .contentTransition(.numericText())                .animation(.easeIn, value: filmNumber)            Stepper("电影数量", value: $filmNumber, in: 0...100)        }        .font(.largeTitle)        .foregroundColor(.indigo)    }}

animation修饰符

基本用法

在 SwiftUI 中,创建一个动画需要以下三个组成部分:

  • 一个时间曲线函数
  • 一个声明将状态(或特定的依赖项)与该时间曲线函数关联起来
  • 一个依赖于该状态(或特定的依赖项)的可动画组件

动画的接口定义为 Animation(timingFunction:property:duration:delay)

  • timingFunction 是时间曲线函数,可以是线性、缓动、弹簧等
  • property 是动画属性,可以是颜色、大小、位置等
  • duration 是动画持续时间
  • delay 是动画延迟时间

三种写法

  • withAnimation(_:_:) 全局应用
  • animation(_:value:) 应用于 View
  • animation(_:) 应用于绑定的变量

第一种

withAnimation(.easeInOut(duration: 1.5).delay(1.0)) {    myProperty = newValue}

第二种

View().animation(.easeInOut(duration: 1.5).delay(1.0), value: myProperty)

第三种

struct ContentView: View {    @State private var scale: CGFloat = 1.0    var body: some View {        PosterView(scale: $scale.animation(.linear(duration: 1)))    }}struct PosterView: View {    @Binding var scale: CGFloat    var body: some View {        Image("evermore")            .resizable()            .scaledToFit()            .scaleEffect(scale)            .onAppear {                scale = 1.5            }    }}

在这个示例中,我们创建了一个 MovieView,它有一个状态变量 scale。当 scale 的值改变时,PosterView 中的海报图片会以线性动画的方式进行缩放。当 PosterView 出现时,scale 的值会改变为 1.5,因此海报图片会以线性动画的方式放大到 1.5 倍。

在 SwiftUI 中,我们也可以创建一个自定义的 AnimatableModifier 来实现对图文卡片大小的动画处理。

struct ContentView: View {    @State private var isSmall = false    var body: some View {        VStack {            Image("evermore")                .resizable()                .aspectRatio(contentMode: .fit)                .clipShape(.rect(cornerSize: CGSize(width: 16, height: 16)))            Text("电影标题")                .font(.title)                .fontWeight(.bold)        }        .animatableCard(size: isSmall ? CGSize(width: 200, height: 300) : CGSize(width: 400, height: 600))        .onTapGesture {            withAnimation(.easeInOut(duration: 1)){                isSmall.toggle()            }        }    }}struct AnimatableCardModifier: AnimatableModifier {    var size: CGSize    var color: Color = .white        var animatableData: CGSize.AnimatableData {        get { CGSize.AnimatableData(size.width, size.height) }        set { size = CGSize(width: newValue.first, height: newValue.second) }    }        func body(content: Content) -> some View {        content            .frame(width: size.width, height: size.height)            .background(color)            .cornerRadius(10)    }}extension View {    func animatableCard(size: CGSize,                        color: Color = .white) -> some View {        self.modifier(AnimatableCardModifier(size: size,                                             color: color))    }}

SwiftUI 内置了许多动画过渡函数,主要分为四类:

  • 时间曲线动画函数
  • 弹簧动画函数
  • 高阶动画函数
  • 自定义动画函数

时间曲线动画函数

时间曲线函数决定了动画的速度如何随时间变化,这对于动画的自然感觉非常重要。

SwiftUI 提供了以下几种预设的时间曲线函数:

  • linear:线性动画,动画速度始终保持不变。
  • easeIn:动画开始时速度较慢,然后逐渐加速。
  • easeOut:动画开始时速度较快,然后逐渐减速。
  • easeInOut:动画开始和结束时速度较慢,中间阶段速度较快。

除此之外,SwiftUI 还提供了 timingCurve 函数,可以通过二次曲线或 Bézier 曲线来自定义插值函数,实现更复杂的动画效果。

以下是代码示例:

struct ContentView: View {    @State private var scale: CGFloat = 1.0    var body: some View {        VStack {            Text("电影标题")                .font(.title)                .padding()            Image("evermore")                .resizable()                .scaledToFit()                .scaleEffect(scale)        }        .onAppear {            withAnimation(.easeInOut(duration: 1.0)) {                scale = 1.5            }        }    }}

在这个示例中,我们创建了一个 MovieView,它包含一个电影标题和一个电影海报。当视图出现时,海报的大小会以 easeInOut 的方式在 1 秒内放大到 1.5 倍。

弹簧动画函数

弹簧动画函数可以模拟物理世界中的弹簧运动,使动画看起来更加自然和生动。

SwiftUI 提供了以下几种预设的弹簧动画函数:

  • smooth:平滑的弹簧动画,动画速度逐渐减慢,直到停止。
  • snappy:快速的弹簧动画,动画速度快速减慢,然后停止。
  • bouncy:弹跳的弹簧动画,动画在结束时会有一些弹跳效果。

除此之外,SwiftUI 还提供了 spring 函数,可以自定义弹簧动画的持续时间、弹跳度和混合持续时间,实现更复杂的弹簧动画效果。

以下是代码示例:

struct ContentView: View {    @State private var scale: CGFloat = 1.0    var body: some View {        VStack {            Text("电影标题")                .font(.title)                .padding()            Image("evermore")                .resizable()                .scaledToFit()                .scaleEffect(scale)        }        .onAppear {            withAnimation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 1)) {                scale = 1.5            }        }    }}

在这个示例中,我们创建了一个 MovieView,它包含一个电影标题和一个电影海报。当视图出现时,海报的大小会以自定义的弹簧动画方式在 0.5 秒内放大到 1.5 倍。

高阶动画函数

高级动画函数可以在基础动画函数的基础上,添加延迟、重复、翻转和速度等功能,使动画效果更加丰富和复杂。

以下是这些函数的简单介绍:

  • func delay(TimeInterval) -> Animation:此函数可以使动画在指定的时间间隔后开始。
  • func repeatCount(Int, autoreverses: Bool) -> Animation:此函数可以使动画重复指定的次数。如果 autoreverses 参数为 true,则每次重复时动画都会翻转。
  • func repeatForever(autoreverses: Bool) -> Animation:此函数可以使动画无限次重复。如果 autoreverses 参数为 true,则每次重复时动画都会翻转。
  • func speed(Double) -> Animation:此函数可以调整动画的速度,使其比默认速度快或慢。

以下是代码示例:

struct MovieView: View {    @State private var scale: CGFloat = 1.0    var body: some View {        VStack {            Text("电影标题")                .font(.title)                .padding()            Image("movie_poster")                .resizable()                .scaledToFit()                .scaleEffect(scale)        }        .onAppear {            withAnimation(Animation.easeInOut(duration: 1.0).delay(0.5).repeatCount(3, autoreverses: true).speed(2)) {                scale = 1.5            }        }    }}

在这个示例中,我们创建了一个 MovieView,它包含一个电影标题和一个电影海报。当视图出现时,海报的大小会以 easeInOut 的方式在 1 秒内放大到 1.5 倍,然后在 0.5 秒后开始,重复 3 次,每次重复都会翻转,速度是默认速度的 2 倍。

自定义动画函数

SwiftUI 可以通过实现 CustomAnimation 协议来完全自定义插值算法。

以下是一个简单的 Linear 动画函数的实现:

struct ContentView: View {    @State private var scale: CGFloat = 1.0    var body: some View {        VStack {            Text("电影标题")                .font(.title)                .padding()            Image("evermore")                .resizable()                .scaledToFit()                .scaleEffect(scale)                .animation(.myLinear(duration: 1), value: scale) // use myLinear animation        }        .onAppear {            scale = 1.5        }    }}struct MyLinearAnimation: CustomAnimation {  var duration: TimeInterval  func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {    if time <= duration {      value.scaled(by: time / duration)    } else {      nil    }  }  func velocity<V: VectorArithmetic>(    value: V, time: TimeInterval, context: AnimationContext<V>  ) -> V? {    value.scaled(by: 1.0 / duration)  }  func shouldMerge<V>(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>) -> Bool where V : VectorArithmetic {    true  }}extension Animation {  public static func myLinear(duration: TimeInterval) -> Animation { // define function like linear    return Animation(MyLinearAnimation(duration: duration))  }}

Transaction

Transaction 使用指南

这段内容主要介绍了 SwiftUI 中的 TransactionwithTransactionTransaction 是 SwiftUI 中用于控制动画的一种方式,它可以用来定义动画的详细参数,如动画类型、持续时间等。withTransaction 是一个函数,它接受一个 Transaction 实例和一个闭包作为参数,闭包中的代码将在这个 Transaction 的上下文中执行。

以下是一个使用 TransactionwithTransaction 的代码示例:

struct ContentView: View {    @State private var scale: CGFloat = 1.0    var body: some View {        VStack {            Text("电影标题")                .font(.title)                .padding()            Image("evermore")                .resizable()                .scaledToFit()                .scaleEffect(scale)        }        .onAppear {            let transaction = Transaction(animation: .easeInOut(duration: 1))            withTransaction(transaction) {                scale = 1.5            }        }    }}

在这个示例中,我们创建了一个 MovieView,它包含一个电影标题和一个电影海报。当视图出现时,我们创建了一个 Transaction,并设置了动画类型为 easeInOut,持续时间为 1 秒。然后我们在 withTransaction 的闭包中改变 scale 的值,这样海报的大小就会以 easeInOut 的方式在 1 秒内放大到 1.5 倍。

使用 TransactionwithTransaction

SwiftUI 中 TransactiondisablesAnimationsisContinuous 属性,以及 transaction(_:) 方法怎么使用?

disablesAnimations 属性可以用来禁止动画,isContinuous 属性可以用来标识一个连续的交互(例如拖动)。transaction(_:) 方法可以用来创建一个新的 Transaction 并在其闭包中设置动画参数。

以下是一个使用这些特性的代码示例:

struct ContentView: View {    @State var size: CGFloat = 100    @GestureState var dragSize: CGSize = .zero    var body: some View {        VStack {            Image("fearless")                .resizable()                .scaledToFit()                .frame(width: size, height: size) // 使用 size 控制尺寸,而非位置                .animation(.spring(), value: size) // 使用弹簧动画                .transaction {                    if $0.isContinuous {                        $0.animation = nil // 拖动时,不设置动画                    } else {                        $0.animation = .spring() // 使用弹簧动画                    }                }                .gesture(                    DragGesture()                        .updating($dragSize, body: { current, state, transaction in                            state = .init(width: current.translation.width, height: current.translation.height)                            transaction.isContinuous = true // 拖动时,设置标识                        })                )            Stepper("尺寸: \(size)", value: $size, in: 50...200) // 使用 Stepper 替代 Slider            Button("开始动画") {                var transaction = Transaction()                if size < 150 { transaction.disablesAnimations = true }                withTransaction(transaction) {                    size = 50                }            }        }    }}

在这个示例中,当 size 小于 150 时,我们禁用动画。通过 .isContinuous 属性,我们可以标识一个连续的交互(例如拖动)。在这个示例中,当拖动时,我们禁用动画。通过 transaction(_:) 方法,我们可以创建一个新的 Transaction 并在其中设置动画参数。

用于视图组件

大部分 SwiftUI 视图组件都有 transaction(_:) 方法,可以用来设置动画参数。比如 NavigationStack, Sheet, Alert 等。

Transaction 也可以用于 BindingFetchRequest

看下面的例子:

struct ContentView: View {    @State var size: CGFloat = 100    @State var isBold: Bool = false    let animation: Animation? = .spring    var sizeBinding: Binding<CGFloat> {        let transaction = Transaction(animation: animation)        return $size.transaction(transaction)    }    var isBoldBinding: Binding<Bool> {        let transaction = Transaction(animation: animation)        return $isBold.transaction(transaction)    }    var body: some View {        VStack {            Image(systemName: "film")                .resizable()                .scaledToFit()                .frame(width: size, height: size) // 使用 size 控制尺寸,而非位置                .font(.system(size: size, weight: isBold ? .bold : .regular)) // 使用 isBold 控制粗细            Stepper("尺寸: \(size)", value: sizeBinding, in: 50...200)            Toggle("粗细", isOn: isBoldBinding)        }        .padding()    }}

传播行为

Transaction 可以用于控制动画的传播行为。在 SwiftUI 中,动画可以在视图层次结构中传播,这意味着一个视图的动画效果可能会影响到其子视图。Transaction 可以用来控制动画的传播行为,例如禁用动画、设置动画类型等。

以下是一个使用 Transaction 控制动画传播行为的代码示例:

enum BookStatus {    case small, medium, large, extraLarge}extension View {    @ViewBuilder func debugAnimation() -> some View {        transaction {            debugPrint($0.animation ?? "")        }    }}struct ContentView: View {    @State var status: BookStatus = .small    var animation: Animation? {        switch status {        case .small:            return .linear        case .medium:            return .easeIn        case .large:            return .easeOut        case .extraLarge:            return .spring()        }    }    var size: CGFloat {        switch status {        case .small:            return 100        case .medium:            return 200        case .large:            return 300        case .extraLarge:            return 400        }    }    var body: some View {        VStack {            Image(systemName: "book")                .resizable()                .scaledToFit()                .frame(width: size, height: size)                .debugAnimation() // 查看动画变化信息            Button("改变状态") {                var transaction = Transaction(animation: animation)                withTransaction(transaction) {                    switch self.status {                    case .small:                        self.status = .medium                    case .medium:                        self.status = .large                    case .large:                        self.status = .extraLarge                    case .extraLarge:                        self.status = .small                    }                }            }        }    }}

这个示例中,我们创建了一个 BookView,它包含一个书籍图标。我们通过 BookStatus 枚举来控制书籍的大小,通过 animation 计算属性来根据状态返回不同的动画类型。在 withTransaction 中,我们根据状态创建一个新的 Transaction,并在其中设置动画类型。通过 debugAnimation 修饰符,我们可以查看动画的变化信息。

TransactionKey

TransactionKey 是一种在 SwiftUI 的视图更新过程中传递额外信息的机制,它可以让你在不同的视图和视图更新之间共享数据。

struct ContentView: View {    @State private var store = MovieStore()    var body: some View {        VStack {            Image("evermore")                .resizable()                .scaledToFit()                .frame(width: 300, height: 300)                .saturation(store.isPlaying ? 1 : 0) // 滤镜变化                .transaction {                    $0.animation = $0[StatusKey.self].animation                }            PlayView(store: store)            PauseView(store: store)        }    }}struct PlayView: View {    let store: MovieStore    var body: some View {        Button("播放") {            withTransaction(\.status, .playing) {                store.isPlaying.toggle()            }        }    }}struct PauseView: View {    let store: MovieStore    var body: some View {        Button("暂停") {            withTransaction(\.status, .paused) {                store.isPlaying.toggle()            }        }    }}@Observableclass MovieStore {    var isPlaying = false}enum MovieStatus {    case playing    case paused    case stopped    var animation: Animation? {        switch self {        case .playing:            Animation.linear(duration: 2)        case .paused:            nil        case .stopped:            Animation.easeInOut(duration: 1)        }    }}struct StatusKey: TransactionKey {    static var defaultValue: MovieStatus = .stopped}extension Transaction {    var status: MovieStatus {        get { self[StatusKey.self] }        set { self[StatusKey.self] = newValue }    }}

以上代码中,我们创建了一个 MovieStore 类,用于存储电影播放状态。我们通过 PlayViewPauseView 分别创建了播放和暂停按钮,点击按钮时,我们通过 withTransaction 函数改变了 MovieStoreisPlaying 属性,并根据状态设置了动画类型。在 ContentView 中,我们通过 transaction 修饰符设置了动画类型为 MovieStatus 中的动画类型。

AnyTransition

AnyTransition 是一个用于创建自定义过渡效果的类型,它可以让你定义视图之间的过渡动画。你可以使用 AnyTransitionmodifier 方法将自定义过渡效果应用到视图上。

struct ContentView: View {        @StateObject var musicViewModel = MusicViewModel()        var body: some View {        VStack {            ForEach(musicViewModel.musicNames, id: \.description) { musicName in                if musicName == musicViewModel.currentMusic {                    Image(musicName)                        .resizable()                        .frame(width: 250, height: 250)                        .ignoresSafeArea()                        .transition(.glitch.combined(with: .opacity))                }            }                        Button("Next Music") {                musicViewModel.selectNextMusic()            }            .buttonStyle(.borderedProminent)            .padding()            .background(Color.blue)            .foregroundColor(.white)            .cornerRadius(10)        }    }}struct MyTransition: ViewModifier {    let active: Bool    func body(content: Content) -> some View {        content            .rotationEffect(active ? .degrees(Double.random(in: -10...10)) : .zero)            .offset(x: active ? CGFloat.random(in: -10...10) : 0, y: active ? CGFloat.random(in: -10...10) : 0)    }}extension AnyTransition {    static var glitch: AnyTransition {        AnyTransition.modifier(            active: MyTransition(active: true),            identity: MyTransition(active: false)        )    }}class MusicViewModel: ObservableObject {    @Published var currentMusic = ""        let musicNames = ["fearless", "evermore", "red", "speaknow", "lover"]        init() {        currentMusic = musicNames.first ?? "fearless"    }        func selectNextMusic() {        guard let currentIndex = musicNames.firstIndex(of: currentMusic) else {            return        }                let nextIndex = currentIndex + 1 < musicNames.count ? currentIndex + 1 : 0                withAnimation(.easeInOut(duration: 2)) {            currentMusic = musicNames[nextIndex]        }    }}

以上代码中,我们创建了一个 MusicViewModel 类,用于存储音乐播放状态。我们通过 MyTransition 自定义了一个过渡效果,通过 AnyTransitionmodifier 方法将自定义过渡效果应用到视图上。在 ContentView 中,我们通过 transition 修饰符设置了过渡效果为 glitch,并在点击按钮时切换音乐。

Matched Geometry Effect

位置变化

Matched Geometry Effect 是一种特殊的动画效果。当你有两个视图,并且你想在一个视图消失,另一个视图出现时,创建一个平滑的过渡动画,你就可以使用这个效果。你只需要给这两个视图添加同样的标识符和命名空间,然后当你删除一个视图并添加另一个视图时,就会自动创建一个动画,让一个视图看起来像是滑动到另一个视图的位置。

示例代码如下:

struct ContentView: View {    @StateObject var viewModel = ViewModel()    @Namespace var musicSelectionNamespace    var body: some View {        VStack {            HStack {                ForEach(viewModel.topMusic) { item in                    Button(action: { viewModel.selectTopMusic(item) }) {                        ZStack {                            Image(item.name)                                .resizable()                                .frame(width: 60, height: 60)                            Text(item.name)                                .fontDesign(.rounded)                                .foregroundColor(.white)                                .shadow(radius: 10)                        }                    }                    .matchedGeometryEffect(id: item.id, in: musicSelectionNamespace)                }            }            .frame(minHeight: 150)            Spacer()                .frame(height: 250)            HStack {                ForEach(viewModel.bottomMusic) { item in                    Button(action: { viewModel.selectBottomMusic(item) }) {                        ZStack {                            Image(item.name)                                .resizable()                                .frame(width: 90, height: 90)                            Text(item.name)                                .font(.title3)                                .fontWeight(.bold)                                .foregroundColor(.white)                                .shadow(radius: 10)                        }                    }                    .matchedGeometryEffect(id: item.id, in: musicSelectionNamespace)                }            }            .frame(minHeight: 150)        }    }}

以上代码中,我们创建了一个 ContentView 视图,其中包含两个 HStack 视图,分别展示了 viewModel 中的 topMusicbottomMusic 数组。我们为每个 topMusicbottomMusic 元素创建了一个 Button 视图,当用户点击按钮时,会调用 viewModel 中的 selectTopMusicselectBottomMusic 方法。我们使用 matchedGeometryEffect 修饰符为每个 Button 视图添加了一个标识符,这样当用户点击按钮时,就会自动创建一个动画,让一个视图看起来像是滑动到另一个视图的位置。

大小变化

Matched Geometry Effect 在大小和位置上都可以进行动画过渡,这样可以让你创建更加复杂的动画效果。

以下是一个视图大小切换的示例:

struct ContentView: View {    @State var isExpanded: Bool = false        private var albumId = "Album"        @Namespace var expansionAnimation        var body: some View {        VStack {            albumView(isExpanded: isExpanded)        }        .padding()        .onTapGesture {            withAnimation {                isExpanded.toggle()            }        }    }        @ViewBuilder    func albumView(isExpanded: Bool) -> some View {        let imageSize = isExpanded ? CGSize(width: 300, height: 450) : CGSize(width: 100, height: 150)        Image(isExpanded ? "evermore" : "fearless")            .resizable()            .aspectRatio(contentMode: .fill)            .frame(width: imageSize.width, height: imageSize.height)            .clipped()            .matchedGeometryEffect(id: albumId, in: expansionAnimation)            .overlay {                Text("Taylor Swift")                    .font(isExpanded ? .largeTitle : .headline)                    .fontDesign(.monospaced)                    .fontDesign(.rounded)                    .foregroundStyle(.white)            }    }}

内容位置变化

内容位置变化的动画效果。以下是一个内容位置变化的示例:

struct ContentView: View {    @State var show = false    @Namespace var placeHolder    @State var albumCoverSize: CGSize = .zero    @State var songListSize: CGSize = .zero    var body: some View {        ZStack {            VStack {                Text("Taylor Swift,1989年12月13日出生于美国宾夕法尼亚州,美国乡村音乐、流行音乐女歌手、词曲创作人、演员、慈善家。")                    .font(.title)                    .fontDesign(.monospaced)                    .fontDesign(.rounded)                    .padding(20)                Spacer()            }            Color.clear                // AlbumCover placeholder                .overlay(alignment: .bottom) {                    Color.clear // AlbumCoverView().opacity(0.01)                        .frame(height: albumCoverSize.height)                        .matchedGeometryEffect(id: "bottom", in: placeHolder, anchor: .bottom, isSource: true)                        .matchedGeometryEffect(id: "top", in: placeHolder, anchor: .top, isSource: true)                }                .overlay(                    AlbumCoverView()                        .sizeInfo($albumCoverSize)                        .matchedGeometryEffect(id: "bottom", in: placeHolder, anchor: show ? .bottom : .top, isSource: false)                )                .overlay(                    SongListView()                        .matchedGeometryEffect(id: "top", in: placeHolder, anchor: show ? .bottom : .top, isSource: false)                )                .animation(.default, value: show)                .ignoresSafeArea()                .overlayButton(show: $show)        }    }}struct AlbumCoverView: View {    var body: some View {        Image("evermore")            .resizable()            .aspectRatio(contentMode: .fill)    }}struct SongListView: View {    var body: some View {        List {            Text("Fearless")            Text("Speak Now")            Text("Red")            // ...        }    }}extension View {    func overlayButton(show: Binding<Bool>) -> some View {        self.overlay(            Button(action: {                withAnimation {                    show.wrappedValue.toggle()                }            }) {                Image(systemName: "arrow.up.arrow.down.square")                    .font(.largeTitle)                    .padding()                    .background(Color.white.opacity(0.75))                    .clipShape(Circle())            }            .padding()            , alignment: .topTrailing        )    }        func sizeInfo(_ size: Binding<CGSize>) -> some View {        self.background(            GeometryReader { geometry in                Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)            }        )        .onPreferenceChange(SizePreferenceKey.self) { size.wrappedValue = $0 }    }}struct SizePreferenceKey: PreferenceKey {    static var defaultValue: CGSize = .zero    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {        value = nextValue()    }}

我们使用 matchedGeometryEffect 修饰符为 AlbumCoverViewSongListView 添加了一个标识符,这样当用户点击按钮时,就会自动创建一个动画,让 AlbumCoverViewSongListView 看起来像是从一个位置切换到另一个位置。

点击显示详细信息

点击显示详细信息的动画效果。

struct ContentView: View {    @Namespace var animation    @State var showDetail = false            var body: some View {        ZStack {            if (!showDetail) {                VStack {                    Text("Taylor Swift")                            .matchedGeometryEffect(id: "artist", in: animation)                            .font(.largeTitle.bold())                            .foregroundColor(Color.white)                                        Text("美国歌手")                        .matchedGeometryEffect(id: "description", in: animation)                        .font(.title3.bold())                        .foregroundColor(Color.white)                }                .padding(30)                .background(                    Rectangle().fill(.black.gradient)                        .matchedGeometryEffect(id: "background", in: animation)                )            } else {                SingerView(animation: animation)            }        }        .onTapGesture {            withAnimation {                showDetail.toggle()            }        }    }}struct SingerView: View {    var animation: Namespace.ID    var body: some View {        VStack{            Text("Taylor Swift")                    .matchedGeometryEffect(id: "artist", in: animation)                    .font(.largeTitle.bold())                    .foregroundColor(Color.white)                        Text("美国歌手")                .matchedGeometryEffect(id: "description", in: animation)                .font(.title3.bold())                .foregroundColor(Color.white)            Spacer()                .frame(height: 30)            Text("泰勒·阿利森·斯威夫特(Taylor Swift),1989年12月13日出生于美国宾夕法尼亚州,美国乡村音乐、流行音乐女歌手、词曲创作人、演员、慈善家。")                .font(.subheadline.bold())                .foregroundColor(Color.white)                        Spacer()                .frame(height: 30)            Image("evermore")                .resizable()                .scaledToFit()                .clipShape(.rect(cornerSize: CGSize(width: 16, height: 16)))                        Text("Evermore 是 Taylor Swift 的最新专辑,这是她在 2020 年的第二张专辑,也是她的第九张录音室专辑。")                .font(.subheadline.bold())                .foregroundColor(Color.white)                    }        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)        .padding(.all, 20)        .background(            Rectangle().fill(.black.gradient)                .matchedGeometryEffect(id: "background", in: animation)                .ignoresSafeArea(.all)        )    }}

导航动画

以下是一个导航动画的示例:

struct ContentView: View {    @Namespace var animation    @State var selectedManga: String? = nil            var body: some View {        ZStack {            if (selectedManga == nil) {                MangaListView(animation: animation, selectedManga: $selectedManga)            } else {                MangaDetailView(selectedManga: $selectedManga, animation: animation)            }        }    }}struct MangaDetailView: View {    @Binding var selectedManga: String?    var animation: Namespace.ID        var body: some View {        VStack {            Text( "\(selectedManga ?? "")")                    .matchedGeometryEffect(id: "mangaTitle", in: animation)                    .font(.title3.bold())                    .foregroundColor(Color.black)                        Spacer()                .frame(height: 50)            Button(action: {                withAnimation {                    selectedManga = nil                }            }, label: {                Text( "返回")                    .font(.title3.bold())                    .foregroundColor(Color.black)            })            .foregroundColor(Color.red)            .padding(.all, 8)            .background(                RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))                    .fill(Color.white.gradient)            )        }        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)        .padding(.all, 20)        .background(            Color(UIColor.systemTeal)                .matchedGeometryEffect(id: "background", in: animation)                .ignoresSafeArea(.all)        )    }}struct MangaListView: View {    var animation: Namespace.ID    @Binding var selectedManga: String?    var body: some View {        VStack {            Button(action: {                withAnimation {                    selectedManga = "海贼王"                }            }, label: {                Text( "海贼王")                    .matchedGeometryEffect(id: "mangaTitle", in: animation)                    .font(.title3.bold())                    .foregroundColor(Color.black)            })            .foregroundColor(Color.black)            .padding(.all, 8)            .background(                RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))                    .fill(Color.teal)            )                        Button(action: {                withAnimation {                    selectedManga = "火影忍者"                }            }, label: {                Text( "火影忍者")                    .font(.title3.bold())                    .foregroundColor(Color.black)            })            .foregroundColor(Color.black)            .padding(.all, 8)            .background(                RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))                    .fill(Color.mint)                    .matchedGeometryEffect(id: "background", in: animation)            )            Button(action: {                withAnimation {                    selectedManga = "进击的巨人"                }            }, label: {                Text( "进击的巨人")                    .font(.title3.bold())                    .foregroundColor(Color.black)            })            .foregroundColor(Color.black)            .padding(.all, 8)            .background(                RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))                    .fill(Color.orange)            )            Button(action: {                withAnimation {                    selectedManga = "鬼灭之刃"                }            }, label: {                Text( "鬼灭之刃")                    .font(.title3.bold())                    .foregroundColor(Color.black)            })            .foregroundColor(Color.black)            .padding(.all, 8)            .background(                RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))                    .fill(Color.purple)            )            Button(action: {                withAnimation {                    selectedManga = "我的英雄学院"                }            }, label: {                Text( "我的英雄学院")                    .font(.title3.bold())                    .foregroundColor(Color.black)            })            .foregroundColor(Color.black)            .padding(.all, 8)            .background(                RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))                    .fill(Color.green)            )        }    }}

geometryGroup

.geometryGroup() 主要用于处理一组视图动画变化时不协调的问题。如果你有一组视图,它们的位置和大小会随着动画变化,你可以使用 .geometryGroup() 修饰符来确保它们的位置和大小保持一致。

PhaseAnimator

PhaseAnimator

以下代码示例演示了如何使用 PhaseAnimator 视图修饰符创建一个动画,该动画通过循环遍历所有动画步骤来连续运行。在这个例子中,我们使用 PhaseAnimator 来创建一个简单的动画,该动画通过循环遍历所有动画步骤来连续运行。当观测值发生变化时,动画会触发一次。

enum AlbumAnimationPhase: String, CaseIterable, Comparable {    case evermore, fearless, folklore, lover, midnights, red, speaknow    static func < (lhs: AlbumAnimationPhase, rhs: AlbumAnimationPhase) -> Bool {        lhs.rawValue < rhs.rawValue    }}struct ContentView: View {    @State var animate: Bool = false    var body: some View {        ScrollView {            PhaseAnimator(                AlbumAnimationPhase.allCases,                trigger: animate,                content: { phase in                    VStack {                        ForEach(AlbumAnimationPhase.allCases, id: \.self) { album in                            if phase >= album {                                VStack {                                    Image(album.rawValue)                                        .resizable()                                        .frame(width: 100, height: 100)                                    Text(album.rawValue.capitalized)                                        .font(.title)                                }                                .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))                            }                        }                    }                    .padding()                }, animation: { phase in                    .spring(duration: 0.5)                }            )        } // end ScrollView        Button(action: {            animate.toggle()        }, label: {            Text("开始")                .font(.largeTitle)                .bold()        })    }}

在上面的代码中,我们首先定义了一个枚举类型 AlbumAnimationPhase,用于表示专辑的不同阶段。然后,我们在 ContentView 视图中创建了一个 PhaseAnimator 视图修饰符,该修饰符接受一个观测值 trigger,用于触发动画。在 content 闭包中,我们遍历所有专辑,并根据当前阶段 phase 来决定是否显示专辑。在 animation 闭包中,我们使用 .spring(duration: 0.5) 创建了一个弹簧动画效果。

KeyframeAnimator

KeyframeAnimator是一个在SwiftUI中创建关键帧动画的工具。关键帧动画是一种动画类型,其中定义了动画开始和结束的关键帧,以及可能的一些中间关键帧,然后动画系统会在这些关键帧之间进行插值以创建平滑的动画。

KeyframeAnimator接受一个初始值,一个内容闭包,以及一个关键帧闭包。初始值是一个包含了动画所需的所有属性的结构(在这个例子中是scalerotationoffset)。内容闭包接受一个这样的结构实例,并返回一个视图。这个视图将使用结构中的值进行配置,以便它可以根据这些值进行动画。关键帧闭包接受一个这样的结构实例,并定义了一系列的关键帧轨道。每个轨道都对应于结构中的一个属性,并定义了一系列的关键帧。每个关键帧都定义了一个值和一个时间点,动画系统将在这些关键帧之间进行插值。

此外,SwiftUI提供了四种不同类型的关键帧:LinearKeyframeSpringKeyframeCubicKeyframeMoveKeyframe。前三种关键帧使用不同的动画过渡函数进行插值,而MoveKeyframe则立即跳转到指定值,无需插值。

KeyframeAnimator可以用于创建各种复杂的动画效果,例如根据滚动位置调整关键帧驱动的效果,或者根据时间进行更新。

struct ContentView: View {    @State var animationTrigger: Bool = false    var body: some View {        VStack {            KeyframeAnimator(                initialValue: AnimatedMovie(),                content: { movie in                    Image("evermore")                        .resizable()                        .frame(width: 100, height: 150)                        .scaleEffect(movie.scaleRatio)                        .rotationEffect(movie.rotationAngle)                        .offset(y: movie.verticalOffset)                }, keyframes: { movie in                    KeyframeTrack(\.scaleRatio) {                        LinearKeyframe(1.0, duration: 0.36)                        SpringKeyframe(1.5, duration: 0.8, spring: .bouncy)                        SpringKeyframe(1.0, spring: .bouncy)                    }                    KeyframeTrack(\.rotationAngle) {                        CubicKeyframe(.degrees(-30), duration: 1.0)                        CubicKeyframe(.zero, duration: 1.0)                    }                    KeyframeTrack(\.verticalOffset) {                        LinearKeyframe(0.0, duration: 0.1)                        SpringKeyframe(20.0, duration: 0.15, spring: .bouncy)                        CubicKeyframe(-60.0, duration: 0.2)                        MoveKeyframe(0.0)                    }                }            )        }    }}struct AnimatedMovie {    var scaleRatio: Double = 1    var rotationAngle = Angle.zero    var verticalOffset: Double = 0}

以上代码中,我们首先定义了一个AnimatedMovie结构,它包含了动画所需的所有属性。然后,我们在ContentView视图中创建了一个KeyframeAnimator,该修饰符接受一个观测值animationTrigger,用于触发动画。在content闭包中,我们使用Image视图创建了一个电影海报,并根据AnimatedMovie结构中的值对其进行配置。在keyframes闭包中,我们为每个属性定义了一系列的关键帧轨道。例如,我们为scaleRatio属性定义了三个关键帧,分别使用LinearKeyframeSpringKeyframe进行插值。我们还为rotationAngleverticalOffset属性定义了两个关键帧轨道,分别使用CubicKeyframeMoveKeyframe进行插值。

也可以使用 .keyframeAnimator 修饰符来创建关键帧动画。以下是一个示例,演示了如何使用 .keyframeAnimator 修饰符创建一个关键帧动画,该动画在用户点击时触发。

struct ContentView: View {    @State var animationTrigger: Bool = false        var body: some View {        Image("evermore")            .resizable()            .frame(width: 100, height: 150)            .scaleEffect(animationTrigger ? 1.5 : 1.0)            .rotationEffect(animationTrigger ? .degrees(-30) : .zero)            .offset(y: animationTrigger ? -60.0 : 0.0)            .keyframeAnimator(initialValue: AnimatedMovie(),                              trigger: animationTrigger,                              content: { view, value in                view                    .scaleEffect(value.scaleRatio)                    .rotationEffect(value.rotationAngle)            },                              keyframes: { value in                KeyframeTrack(\.scaleRatio) {                    LinearKeyframe(1.5, duration: 0.36)                    SpringKeyframe(1.0, duration: 0.8, spring: .bouncy)                    SpringKeyframe(1.5, spring: .bouncy)                }                                KeyframeTrack(\.rotationAngle) {                    CubicKeyframe(.degrees(-30), duration: 1.0)                    CubicKeyframe(.zero, duration: 1.0)                }                                KeyframeTrack(\.verticalOffset) {                    LinearKeyframe(-60.0, duration: 0.1)                    SpringKeyframe(0.0, duration: 0.15, spring: .bouncy)                    CubicKeyframe(-60.0, duration: 0.2)                    MoveKeyframe(0.0)                }            })                    .onTapGesture {                withAnimation {                    animationTrigger.toggle()                }            }    }}struct AnimatedMovie {    var scaleRatio: Double = 1    var rotationAngle = Angle.zero    var verticalOffset: Double = 0}

在这个例子中,我们创建了一个 AnimatedMovie 结构,它包含了动画所需的所有属性。然后,我们在 ContentView 视图中创建了一个 KeyframeAnimator,该修饰符接受一个观测值 animationTrigger,用于触发动画。在 content 闭包中,我们使用 Image 视图创建了一个电影海报,并根据 AnimatedMovie 结构中的值对其进行配置。在 keyframes 闭包中,我们为每个属性定义了一系列的关键帧轨道。例如,我们为 scaleRatio 属性定义了三个关键帧,分别使用 LinearKeyframe 和 SpringKeyframe 进行插值。我们还为 rotationAngle 和 verticalOffset 属性定义了两个关键帧轨道,分别使用 CubicKeyframe 和 MoveKeyframe 进行插值。

布局动画

import SwiftUIstruct AnimateLayout: View { @State var changeLayout: Bool = true @Namespace var namespace var body: some View {  VStack(spacing: 30) {   if changeLayout {    HStack { items }   } else {    VStack { items }   }   Button("切换布局") {    withAnimation { changeLayout.toggle() }   }  }  .padding() } @ViewBuilder var items: some View {  Text("one")   .matchedGeometryEffect(id: "one", in: namespace)  Text("Two")   .matchedGeometryEffect(id: "Two", in: namespace)  Text("Three")   .matchedGeometryEffect(id: "Three", in: namespace) }}

动画-例子

动画的例子有很多。准备中… 请期待。

小册子之 Form、Picker、Toggle、Slider 和 Stepper 表单相关 SwiftUI 视图

以下内容已整理到小册子中,本文会随着系统更新和我更多的实践而新增和更新,你可以下载“戴铭的开发小册子”应用,来跟踪查看本文内容新增和更新。小册子应用的代码可以在 Github 上查看。

本文属于小册子系列中的一篇,已发布系列文章有:

Form

控件视图 说明 Style
Button 触发操作的按钮 .bordered, .borderless, .borderedProminent, .plain
Picker 提供多选项供选择 .wheel, .inline, .segmented, .menu, .radioGroup
DatePicker and MultiDatePicker 选择日期的工具 .compact, .wheel, .graphical
Toggle 切换两种状态的开关 .switch, .botton, .checkbox
Stepper 调整数值的步进器 无样式选项
Menu 显示选项列表的菜单 .borderlessButton, .button

Form 有 ColumnFormStyle 还有 GroupedFormStyle。使用 buttonStyle 修饰符:

Form {   ...}.formStyle(.grouped)

Form 新版也得到了增强,示例如下:

struct SimpleFormView: View {    @State private var date = Date()    @State private var eventDescription = ""    @State private var accent = Color.red    @State private var scheme = ColorScheme.light    var body: some View {        Form {            Section {                DatePicker("Date", selection: $date)                TextField("Description", text: $eventDescription)                    .lineLimit(3)            }                        Section("Vibe") {                Picker("Accent color", selection: $accent) {                    ForEach(Color.accentColors, id: \.self) { color in                        Text(color.description.capitalized).tag(color)                    }                }                Picker("Color scheme", selection: $scheme) {                    Text("Light").tag(ColorScheme.light)                    Text("Dark").tag(ColorScheme.dark)                }            }        }        .formStyle(.grouped)    }}extension Color {    static let accentColors: [Color] = [.red, .green, .blue, .orange, .pink, .purple, .yellow]}

Form 的样式除了 .formStyle(.grouped) 还有 .formStyle(..columns)

关于 Form 字体、单元、背景颜色设置,参看下面代码:

struct ContentView: View {    @State private var movieTitle = ""    @State private var isWatched = false    @State private var rating = 1    @State private var watchDate = Date()    var body: some View {        Form {            Section {                TextField("电影标题", text: $movieTitle)                LabeledContent("导演", value: "克里斯托弗·诺兰")            } header: {                Text("关于电影")            }            .listRowBackground(Color.gray.opacity(0.1))            Section {                Toggle("已观看", isOn: $isWatched)                Picker("评分", selection: $rating) {                    ForEach(1...5, id: \.self) { number in                        Text("\(number) 星")                    }                }            } header: {                Text("电影详情")            }            .listRowBackground(Color.gray.opacity(0.1))                        Section {                DatePicker("观看日期", selection: $watchDate)            }            .listRowBackground(Color.gray.opacity(0.1))                        Section {                Button("重置所有电影数据") {                    resetAllData()                }            }            .listRowBackground(Color.white)        }        .foregroundColor(.black)        .tint(.indigo)        .background(Color.yellow)        .scrollContentBackground(.hidden)        .navigationBarTitle("电影追踪器")    }        private func resetAllData() {        movieTitle = ""        isWatched = false        rating = 1        watchDate = Date()    }}struct LabeledContent: View {    let label: String    let value: String    init(_ label: String, value: String) {        self.label = label        self.value = value    }    var body: some View {        HStack {            Text(label)            Spacer()            Text(value)        }    }}

Picker选择器

Picker

SwiftUI 中的 Picker 视图是一个用于选择列表中的一个选项的用户界面元素。你可以使用 Picker 视图来创建各种类型的选择器,包括滚动选择器、弹出菜单和分段控制。

示例代码如下:

struct PlayPickerView: View {    @State private var select = 1    @State private var color = Color.red.opacity(0.3)        var dateFt: DateFormatter {        let ft = DateFormatter()        ft.dateStyle = .long        return ft    }    @State private var date = Date()        var body: some View {                // 默认是下拉的风格        Form {            Section("选区") {                Picker("选一个", selection: $select) {                    Text("1")                        .tag(1)                    Text("2")                        .tag(2)                }            }        }        .padding()                // Segment 风格,        Picker("选一个", selection: $select) {            Text("one")                .tag(1)            Text("two")                .tag(2)        }        .pickerStyle(SegmentedPickerStyle())        .padding()                // 颜色选择器        ColorPicker("选一个颜色", selection: $color, supportsOpacity: false)            .padding()                RoundedRectangle(cornerRadius: 8)            .fill(color)            .frame(width: 50, height: 50)                // 时间选择器        VStack {            DatePicker(selection: $date, in: ...Date(), displayedComponents: .date) {                Text("选时间")            }                        DatePicker("选时间", selection: $date)                .datePickerStyle(GraphicalDatePickerStyle())                .frame(maxHeight: 400)                        Text("时间:\(date, formatter: dateFt)")        }        .padding()    }}

上面的代码中,有三种类型的 Picker 视图:

  1. 默认的下拉风格 Picker 视图。这种类型的 Picker 视图在 Form 中使用,用户可以点击选择器来打开一个下拉菜单,然后从菜单中选择一个选项。
Form {    Section("选区") {        Picker("选一个", selection: $select) {            Text("1")                .tag(1)            Text("2")                .tag(2)        }    }}
  1. 分段控制风格 Picker 视图。这种类型的 Picker 视图使用 SegmentedPickerStyle() 修饰符,它将选择器显示为一组水平排列的按钮,用户可以点击按钮来选择一个选项。
Picker("选一个", selection: $select) {    Text("one")        .tag(1)    Text("two")        .tag(2)}.pickerStyle(SegmentedPickerStyle())
  1. ColorPickerDatePicker 视图。这两种类型的视图是 Picker 视图的特殊形式,它们分别用于选择颜色和日期。
ColorPicker("选一个颜色", selection: $color, supportsOpacity: false)DatePicker("选时间", selection: $date)    .datePickerStyle(GraphicalDatePickerStyle())

在所有这些 Picker 视图中,你都需要提供一个绑定的选择状态,这个状态会在用户选择一个新的选项时更新。你还需要为每个选项提供一个视图和一个唯一的标签。

文字Picker

基本使用

文字 Picker 示例:

struct StaticDataPickerView: View {    @State private var selectedCategory = "动作"    var body: some View {        VStack {            Text("选择的类别: \(selectedCategory)")            Picker("电影类别",                 selection: $selectedCategory) {                Text("动作")                    .tag("动作")                Text("喜剧")                    .tag("喜剧")                Text("剧情")                    .tag("剧情")                Text("恐怖")                    .tag("恐怖")            }        }    }}

使用枚举

使用枚举来创建选取器的示例:

enum MovieCategory: String, CaseIterable, Identifiable {    case action = "动作"    case comedy = "喜剧"    case drama = "剧情"    case horror = "恐怖"    var id: MovieCategory { self }}struct MoviePicker: View {   @State private var selectedCategory: MovieCategory = .action  var body: some View {     Picker("电影类别", selection: $selectedCategory) {        ForEach(MovieCategory.allCases) { category in             Text(category.rawValue).tag(category)       }     }   }}

样式

SwiftUI 提供了多种内置的 Picker 样式,以改变 Picker 的外观和行为。以下是一些主要的 Picker 样式及其使用示例:

  • DefaultPickerStyle:根据平台和环境自动调整样式。这是默认的 Picker 样式。
Picker("Label", selection: $selection) {    ForEach(0..<options.count) {        Text(self.options[$0])    }}
  • WheelPickerStyle:以旋转轮的形式展示选项。在 iOS 上,这种样式会显示一个滚动的选择器。
Picker("Label", selection: $selection) {    ForEach(0..<options.count) {        Text(self.options[$0])    }}.pickerStyle(WheelPickerStyle())
  • SegmentedPickerStyle:将选项以分段控件的形式展示。这种样式会显示一个分段控制,用户可以在其中选择一个选项。
Picker("Label", selection: $selection) {    ForEach(0..<options.count) {        Text(self.options[$0])    }}.pickerStyle(SegmentedPickerStyle())
  • InlinePickerStyle:在列表或表格中内联展示选项。这种样式会在 FormList 中显示一个内联的选择器。
Form {    Picker("Label", selection: $selection) {        ForEach(0..<options.count) {            Text(self.options[$0])        }    }    .pickerStyle(InlinePickerStyle())}
  • MenuPickerStyle:点击时以菜单的形式展示选项。这种样式会显示一个菜单,用户可以在其中选择一个选项。
Picker("Label", selection: $selection) {    ForEach(0..<options.count) {        Text(self.options[$0])    }}.pickerStyle(MenuPickerStyle())
  • .navigationLink:在 iOS 16+ 中,点击后进入下一个页面。这种样式会显示一个导航链接,用户可以点击它来打开一个新的视图。
  • .radioGrouped:仅在 macOS 中可用,以单选按钮组的形式展示选项。这种样式会显示一个单选按钮组,用户可以在其中选择一个选项。

ColorPicker

ColorPicker 是一个允许用户选择颜色的视图。以下是一个 ColorPicker 的使用示例:

import SwiftUIstruct ContentView: View {    @State private var selectedColor = Color.white    var body: some View {        VStack {            ColorPicker("选择一个颜色", selection: $selectedColor)            Text("你选择的颜色")                .foregroundColor(selectedColor)        }    }}

在这个示例中,我们创建了一个 ColorPicker 视图,用户可以通过这个视图选择一个颜色。我们使用 @State 属性包装器来创建一个可以绑定到 ColorPickerselectedColor 状态。当用户选择一个新的颜色时,selectedColor 状态会自动更新,Text 视图的前景色也会相应地更新。

DatePicker

基本使用

struct ContentView: View {    @State private var releaseDate: Date = Date()    var body: some View {        VStack(spacing: 30) {            DatePicker("选择电影发布日期", selection: $releaseDate, displayedComponents: .date)            Text("选择的发布日期: \(releaseDate, formatter: DateFormatter.dateMedium)")        }        .padding()    }}

选择多个日期

在 iOS 16 中,您现在可以允许用户选择多个日期,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()    }}

指定日期范围

指定日期的范围,例如只能选择当前日期之后的日期,示例如下:

DatePicker(    "选择日期",    selection: $selectedDate,    in: Date()...,    displayedComponents: [.date]).datePickerStyle(WheelDatePickerStyle()).labelsHidden()

在这个示例中:

  • selection: $selectedDate 表示选定的日期和时间。
  • in: Date()... 表示可选日期的范围。在这个例子中,用户只能选择当前日期之后的日期。你也可以使用 ...Date() 来限制用户只能选择当前日期之前的日期,或者使用 Date().addingTimeInterval(86400*7) 来限制用户只能选择从当前日期开始的接下来一周内的日期。
  • displayedComponents: [.date] 表示 DatePicker 应该显示哪些组件。在这个例子中,我们只显示日期组件。你也可以使用 .hourAndMinute 来显示小时和分钟组件,或者同时显示日期和时间组件。
  • .datePickerStyle(WheelDatePickerStyle()) 表示 DatePicker 的样式。在这个例子中,我们使用滚轮样式。你也可以使用 GraphicalDatePickerStyle() 来应用图形样式。
  • .labelsHidden() 表示隐藏 DatePicker 的标签。

PhotoPicker

PhotoPicker 使用示例

import SwiftUIimport PhotosUIstruct ContentView: View {    @State private var selectedItem: PhotosPickerItem?    @State private var selectedPhotoData: Data?    var body: some View {        NavigationView {            VStack {                if let item = selectedItem, let data = selectedPhotoData, let image = UIImage(data: data) {                    Image(uiImage: image)                        .resizable()                        .scaledToFit()                } else {                    Text("选择电影海报")                }            }            .navigationTitle("电影海报")            .toolbar {                ToolbarItem(placement: .navigationBarTrailing) {                    PhotosPicker(selection: $selectedItem, matching: .images) {                        Label("选择照片", systemImage: "photo")                    }                    .tint(.indigo)                    .controlSize(.extraLarge)                    .buttonStyle(.borderedProminent)                }            }            .onChange(of: selectedItem, { oldValue, newValue in                Task {                    if let data = try? await newValue?.loadTransferable(type: Data.self) {                        selectedPhotoData = data                    }                }            })        }    }}

限制选择媒体类型

我们可以使用 matching 参数来过滤 PhotosPicker 中显示的媒体类型。这个参数接受一个 PHAssetMediaType 枚举值,可以是 .images.videos.audio.any 等。

例如,如果我们只想显示图片,可以这样设置:

PhotosPicker(selection: $selectedItem, matching: .images) {    Label("选择照片", systemImage: "photo")}

如果我们想同时显示图片和视频,可以使用 .any(of:) 方法:

PhotosPicker(selection: $selectedItem, matching: .any(of: [.images, .videos])) {    Label("选择照片", systemImage: "photo")}

此外,我们还可以使用 .not(_:) 方法来排除某种类型的媒体。例如,如果我们想显示所有的图片,但是不包括 Live Photo,可以这样设置:

PhotosPicker(selection: $selectedItem, matching: .any(of: [.images, .not(.livePhotos)])) {    Label("选择照片", systemImage: "photo")}

这些设置可以让我们更精确地控制 PhotosPicker 中显示的媒体类型。

选择多张图片

以下示例演示了如何使用 PhotosPicker 选择多张图片,并将它们显示在一个 LazyVGrid 中:

import SwiftUIimport PhotosUIstruct ContentView: View {    @State private var selectedItems: [PhotosPickerItem] = [PhotosPickerItem]()    @State private var selectedPhotosData: [Data] = [Data]()    var body: some View {        NavigationStack {            ScrollView {                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {                    ForEach(selectedPhotosData, id: \.self) { photoData in                        if let image = UIImage(data: photoData) {                            Image(uiImage: image)                                .resizable()                                .scaledToFit()                                .cornerRadius(10.0)                                .padding(.horizontal)                        }                    }                }            }            .navigationTitle("书籍")            .toolbar {                ToolbarItem(placement: .navigationBarTrailing) {                    PhotosPicker(selection: $selectedItems, maxSelectionCount: 5, matching: .images) {                        Image(systemName: "book.fill")                            .foregroundColor(.brown)                    }                    .onChange(of: selectedItems, { oldValue, newValue in                        for newItem in newValue {                            Task {                                if let data = try? await newItem.loadTransferable(type: Data.self) {                                    selectedPhotosData.append(data)                                }                            }                        }                    })                }            }        }    }}

以上示例中,我们使用了 PhotosPickermaxSelectionCount 参数来限制用户最多只能选择 5 张图片。当用户选择图片后,我们将图片数据保存在 selectedPhotosData 数组中,并在 LazyVGrid 中显示这些图片。

字体Picker

这段代码实现了一个字体选择器的功能,用户可以在其中选择和查看自己喜欢的字体。

struct ContentView: View {    @State private var fontFamily: String = ""    var body: some View {        VStack {            Text("选择字体:")            FontPicker(fontFamily: $fontFamily)                .equatable()        }    }}struct FontPicker: View, Equatable {    @Binding var fontFamily: String    var body: some View {        VStack {            Text("\(fontFamily)")                .font(.custom(fontFamily, size: 20))            Picker("", selection: $fontFamily) {                ForEach(NSFontManager.shared.availableFontFamilies, id: \.self) { family in                    Text(family)                        .tag(family)                }            }            Spacer()        }        .padding()    }    static func == (l: FontPicker, r: FontPicker) -> Bool {        l.fontFamily == r.fontFamily    }}

WheelPicker

本示例是一个可折叠的滚轮选择器 CollapsibleWheelPicker。这个选择器允许用户从一组书籍中选择一本。

struct ContentView: View {  @State private var selection = 0  let items = ["Book 1", "Book 2", "Book 3", "Book 4", "Book 5"]  var body: some View {    NavigationStack {      Form {        CollapsibleWheelPicker(selection: $selection) {          ForEach(items, id: \.self) { item in            Text("\(item)")          }        } label: {          Text("Books")          Spacer()          Text("\(items[selection])")        }      }    }  }}struct CollapsibleWheelPicker<SelectionValue, Content, Label>: View where SelectionValue: Hashable, Content: View, Label: View {    @Binding var selection: SelectionValue    @ViewBuilder let content: () -> Content    @ViewBuilder let label: () -> Label    var body: some View {        CollapsibleView(label: label) {            Picker(selection: $selection, content: content) {                EmptyView()            }            .pickerStyle(.wheel)        }    }}struct CollapsibleView<Label, Content>: View where Label: View, Content: View {  @State private var isSecondaryViewVisible = false  @ViewBuilder let label: () -> Label  @ViewBuilder let content: () -> Content  var body: some View {    Group {      Button(action: { isSecondaryViewVisible.toggle() }, label: label)        .buttonStyle(.plain)      if isSecondaryViewVisible {        content()      }    }  }}

ContentView 中,我们创建了一个 CollapsibleWheelPicker 视图。这个视图包含一个滚轮样式的选择器,用户可以从中选择一本书。选择的书籍会绑定到 selection 变量。

CollapsibleWheelPicker 视图是一个可折叠的滚轮选择器,它接受一个绑定的选择变量、一个内容视图和一个标签视图。内容视图是一个 Picker 视图,用于显示可供选择的书籍。标签视图是一个 Text 视图,显示当前选择的书籍。

Toggle

示例

使用示例如下

struct PlayToggleView: View {    @State private var isEnable = false    var body: some View {        // 普通样式        Toggle(isOn: $isEnable) {            Text("\(isEnable ? "开了" : "关了")")        }        .padding()                // 按钮样式        Toggle(isOn: $isEnable) {            Label("\(isEnable ? "打开了" : "关闭了")", systemImage: "cloud.moon")        }        .padding()        .tint(.pink)        .controlSize(.large)        .toggleStyle(.button)                // Switch 样式        Toggle(isOn: $isEnable) {            Text("\(isEnable ? "开了" : "关了")")        }        .toggleStyle(SwitchToggleStyle(tint: .orange))        .padding()                // 自定义样式        Toggle(isOn: $isEnable) {            Text(isEnable ? "录音中" : "已静音")        }        .toggleStyle(PCToggleStyle())            }}// MARK: - 自定义样式struct PCToggleStyle: ToggleStyle {    func makeBody(configuration: Configuration) -> some View {        return HStack {            configuration.label            Image(systemName: configuration.isOn ? "mic.square.fill" : "mic.slash.circle.fill")                .renderingMode(.original)                .resizable()                .frame(width: 30, height: 30)                .onTapGesture {                    configuration.isOn.toggle()                }        }    }}

样式

Toggle 可以设置 toggleStyle,可以自定义样式。

下表是不同平台支持的样式

  • DefaultToggleStyle:iOS 表现的是 Switch,macOS 是 Checkbox
  • SwitchToggleStyle:iOS 和 macOS 都支持
  • CheckboxToggleStyle:只支持 macOS

纯图像的 Toggle

struct ContentView: View {    @State private var isMuted = false    var body: some View {        Toggle(isOn: $isMuted) {            Image(systemName: isMuted ? "speaker.slash.fill" : "speaker.fill")                .font(.system(size: 50))        }        .tint(.red)        .toggleStyle(.button)        .clipShape(Circle())    }}

自定义 ToggleStyle

做一个自定义的切换按钮 OfflineModeToggleStyle。这个切换按钮允许用户控制是否开启离线模式。代码如下:

struct ContentView: View {    @State private var isOfflineMode = false    var body: some View {        Toggle(isOn: $isOfflineMode) {            Text("Offline Mode")        }        .toggleStyle(OfflineModeToggleStyle(systemImage: isOfflineMode ? "wifi.slash" : "wifi", activeColor: .blue))    }}struct OfflineModeToggleStyle: ToggleStyle {    var systemImage: String    var activeColor: Color    func makeBody(configuration: Configuration) -> some View {        HStack {            configuration.label            Spacer()            RoundedRectangle(cornerRadius: 16)                .fill(configuration.isOn ? activeColor : Color(.systemGray5))                .overlay {                    Circle()                        .fill(.white)                        .padding(2)                        .overlay {                            Image(systemName: systemImage)                                .foregroundColor(configuration.isOn ? activeColor : Color(.systemGray5))                        }                        .offset(x: configuration.isOn ? 8 : -8)                }                .frame(width: 50, height: 32)                .onTapGesture {                    withAnimation(.spring()) {                        configuration.isOn.toggle()                    }                }        }    }}

以上代码中,我们定义了一个 OfflineModeToggleStyle,它接受两个参数:systemImage 和 activeColor。systemImage 是一个字符串,表示图像的系统名称。activeColor 是一个颜色,表示激活状态的颜色。

动画化的 Toggle

以下是一个自定义的切换按钮 MuteToggleStyle。这个切换按钮允许用户控制是否开启静音模式。

struct ContentView: View {    @State private var isMuted = false    var body: some View {        VStack {            Toggle(isOn: $isMuted) {                Text("Mute Mode")                    .foregroundColor(isMuted ? .white : .black)            }            .toggleStyle(MuteToggleStyle())            .padding()        }        .frame(maxWidth: .infinity, maxHeight: .infinity)    }}struct MuteToggleStyle: ToggleStyle {    var onImage = "speaker.slash.fill"    var offImage = "speaker.2.fill"    func makeBody(configuration: Configuration) -> some View {        HStack {            configuration.label            Spacer()            RoundedRectangle(cornerRadius: 30)                .fill(configuration.isOn ? Color(.systemGray6) : .yellow)                .overlay {                    Image(systemName: configuration.isOn ? onImage : offImage)                        .resizable()                        .scaledToFit()                        .clipShape(Circle())                        .padding(5)                        .rotationEffect(.degrees(configuration.isOn ? 0 : 180))                        .offset(x: configuration.isOn ? 10 : -10)                }                .frame(width: 50, height: 32)                .onTapGesture {                    withAnimation(.easeInOut(duration: 0.2)) {                        configuration.isOn.toggle()                    }                }        }    }}extension ToggleStyle where Self == MuteToggleStyle {    static var mute: MuteToggleStyle { .init() }}

以上代码中,我们定义了一个 MuteToggleStyle,它接受两个参数:onImage 和 offImage。onImage 是一个字符串,表示激活状态的图像的系统名称。offImage 是一个字符串,表示非激活状态的图像的系统名称。

两个标签的 Toggle

以下是一个自定义的切换按钮,它有两个标签。这个切换按钮允许用户控制是否开启静音模式。

Toggle(isOn: $mute) {  Text("静音")  Text("这将关闭所有声音")}

Slider

简单示例

struct PlaySliderView: View {    @State var count: Double = 0    var body: some View {        Slider(value: $count, in: 0...100)            .padding()        Text("\(Int(count))")    }}

以下代码演示了如何创建一个自定义的 Slider 控件,用于调整亮度。

struct ContentView: View {    @State private var brightness: Double = 50    @State private var isEditing: Bool = false    var body: some View {        VStack {            Text("Brightness Control")                .font(.title)                .padding()            BrightnessSlider(value: $brightness, range: 0...100, step: 5, isEditing: $isEditing)            Text("Brightness: \(Int(brightness)), is changing: \(isEditing)")                .font(.footnote)                .padding()        }    }}struct BrightnessSlider: View {    @Binding var value: Double    var range: ClosedRange<Double>    var step: Double    @Binding var isEditing: Bool    var body: some View {        Slider(value: $value, in: range, step: step) {            Label("亮度", systemImage: "light.max")        } minimumValueLabel: {            Text("\(Int(range.lowerBound))")        } maximumValueLabel: {            Text("\(Int(range.upperBound))")        } onEditingChanged: {            print($0)        }    }}

以上代码中,我们创建了一个 BrightnessSlider 控件,它是一个自定义的 Slider 控件,用于调整亮度。BrightnessSlider 接受一个 value 绑定,一个 range 范围,一个 step 步长,以及一个 isEditing 绑定。在 BrightnessSlider 中,我们使用 Slider 控件来显示亮度调整器。我们还使用 Label 来显示亮度调整器的标题,并使用 minimumValueLabelmaximumValueLabel 来显示亮度调整器的最小值和最大值。最后,我们使用 onEditingChanged 修饰符来监听亮度调整器的编辑状态。

Stepper

Stepper 控件允许用户通过点击按钮来增加或减少数值。

struct ContentView: View {    @State private var count: Int = 2    var body: some View {        Stepper(value: $count, in: 2...20, step: 2) {            Text("共\(count)")        } onEditingChanged: { b in            print(b)        } // end Stepper    }}

ContentView 中,我们定义了一个状态变量 count,并将其初始化为 2。然后,我们创建了一个 Stepper 视图,并将其绑定到 count 状态变量。

Stepper 视图的值范围为 2 到 20,步进值为 2,这意味着每次点击按钮,count 的值会增加或减少 2。我们还添加了一个标签,显示当前的 count 值。

我们还添加了 onEditingChanged 回调,当 Stepper 的值改变时,会打印出一个布尔值,表示 Stepper 是否正在被编辑。

小册子之详说 Navigation、ViewThatFits、Layout 协议等布局 SwiftUI 组件

以下内容已整理到小册子中,本文会随着系统更新和我更多的实践而新增和更新,你可以下载“戴铭的开发小册子”应用,来跟踪查看本文内容新增和更新。小册子应用的代码可以在 Github 上查看。

本文属于小册子系列中的一篇,已发布系列文章有:

Navigation导航

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")            }        }    }}

对 toolbar 的自定义,示例如下:

.toolbar(id: "toolbar") {    ToolbarItem(id: "new", placement: .secondaryAction) {        Button(action: {}) {            Label("New Invitation", systemImage: "envelope")        }    }}.toolbarRole(.editor)

以下是废弃的 NavigationView 的用法。

对应代码如下:

struct PlayNavigationView: View {    let lData = 1...10    var body: some View {        NavigationView {            ZStack {                LinearGradient(colors: [.pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)                    .ignoresSafeArea()                                List(lData, id: \.self) { i in                    NavigationLink {                        PNavDetailView(contentStr: "\(i)")                    } label: {                        Text("\(i)")                    }                }            }                        ZStack {                LinearGradient(colors: [.mint, .yellow], startPoint: .topLeading, endPoint: .bottomTrailing)                    .ignoresSafeArea()                                VStack {                    Text("一个 NavigationView 的示例")                        .bold()                        .font(.largeTitle)                        .shadow(color: .white, radius: 9, x: 0, y: 0)                        .scaleEffect(2)                }            }            .safeAreaInset(edge: .bottom) {                HStack {                    Button("bottom1") {}                    .font(.headline)                    Button("bottom2") {}                    Button("bottom3") {}                    Spacer()                }                .padding(5)                .background(LinearGradient(colors: [.purple, .blue], startPoint: .topLeading, endPoint: .bottomTrailing))            }        }        .foregroundColor(.white)        .navigationTitle("数字列表")        .toolbar {            // placement 共有 keyboard、destructiveAction、cancellationAction、confirmationAction、status、primaryAction、navigation、principal、automatic 这些            ToolbarItem(placement: .primaryAction) {                Button("primaryAction") {}                .background(.ultraThinMaterial)                .font(.headline)            }            // 通过 ToolbarItemGroup 可以简化相同位置 ToolbarItem 的编写。            ToolbarItemGroup(placement: .navigation) {                Button("返回") {}                Button("前进") {}            }            PCToolbar(doDestruct: {                print("删除了")            }, doCancel: {                print("取消了")            }, doConfirm: {                print("确认了")            })            ToolbarItem(placement: .status) {                Button("status") {}            }            ToolbarItem(placement: .principal) {                Button("principal") {                                    }            }            ToolbarItem(placement: .keyboard) {                Button("Touch Bar Button") {}            }        } // end toolbar    }}// MARK: - NavigationView 的目的页面struct PNavDetailView: View {    @Environment(\.presentationMode) var pMode: Binding<PresentationMode>    var contentStr: String    var body: some View {        ZStack {            LinearGradient(colors: [.purple, .blue], startPoint: .topLeading, endPoint: .bottomTrailing)                .ignoresSafeArea()            VStack {                Text(contentStr)                Button("返回") {                    pMode.wrappedValue.dismiss()                }            }        } // end ZStack    } // end body}// MARK: - 自定义 toolbar// 通过 ToolbarContent 创建可重复使用的 toolbar 组struct PCToolbar: ToolbarContent {    let doDestruct: () -> Void    let doCancel: () -> Void    let doConfirm: () -> Void        var body: some ToolbarContent {        ToolbarItem(placement: .destructiveAction) {            Button("删除", action: doDestruct)        }        ToolbarItem(placement: .cancellationAction) {            Button("取消", action: doCancel)        }        ToolbarItem(placement: .confirmationAction) {            Button("确定", action: doConfirm)        }    }}

toolbar 的位置设置可选项如下:

  • primaryAction:放置到最主要位置,macOS 就是放在 toolbar 的最左边
  • automatic:根据平台不同放到默认位置
  • confirmationAction:一些确定的动作
  • cancellationAction:取消动作
  • destructiveAction:删除的动作
  • status:状态变化,比如检查更新等动作
  • navigation:导航动作,比如浏览器的前进后退
  • principal:突出的位置,iOS 和 macOS 会出现在中间的位置
  • keyboard:macOS 会出现在 Touch Bar 里。iOS 会出现在弹出的虚拟键盘上。

NavigationStack

使用示例

假设我们有一个 TVShow 结构体和一个 Book 结构体,它们分别包含电视剧和书籍的名字。当用户点击一个电视剧或书籍的名字时,他们会被导航到相应的详细信息页面。

以下是一个例子:

struct TVShow: Hashable {    let name: String}struct Book: Hashable {    let name: String}struct ContentView: View {    @State var tvShows = [TVShow(name: "Game of Thrones"), TVShow(name: "Breaking Bad")]    @State var books = [Book(name: "1984"), Book(name: "To Kill a Mockingbird")]    var body: some View {        NavigationStack {            List {                Section(header: Text("Best TV Shows"))  {                    ForEach(tvShows, id: \.name) { show in                        NavigationLink(value: show, label: {                            Text(show.name)                        })                    }                }                Section(header: Text("Books"))  {                    ForEach(books, id: \.name) { book in                        NavigationLink(value: book, label: {                            Text(book.name)                        })                    }                }            }            .navigationDestination(for: TVShow.self) { show in                TVShowView(show: show)            }            .navigationDestination(for: Book.self) { book in                BookView(book: book)            }            .navigationTitle(Text("Media"))        }    }}struct TVShowView: View {    let show: TVShow    var body: some View {        Text("Details for \(show.name)")    }}struct BookView: View {    let book: Book    var body: some View {        Text("Details for \(book.name)")    }}

全局路由

先写个路由的枚举

enum Route: Hashable {    case all    case add(Book)    case detail(Book)}struct Book {    let name: String    let des: String}

在 App 中设置好全局路由

@mainstruct LearnNavApp: App {    var body: some Scene {        WindowGroup {            NavigationStack {                ContentView()                    .navigationDestination(for: Route.self) { route in                        switch route {                            case .all:                                Text("显示所有图书")                            case .create(let book):                                Text("添加书 \(book.name)")                            case .detail(let book):                                Text("详细 \(book.des)")                        }                    }            }                        }    }}

所有视图都可调用,调用方式如下:

NavigationLink("查看书籍详细说明", value: Route.detail(Book(name: "1984", des: "1984 Detail")))

NavigationPath

NavigationPath 是一个用于管理 SwiftUI 中导航路径的工具。它可以帮助你在 SwiftUI 中实现更复杂的导航逻辑。

在 SwiftUI 中,我们通常使用 NavigationLink 来实现导航。然而,NavigationLink 只能实现简单的前进导航,如果你需要实现更复杂的导航逻辑,例如后退、跳转到任意页面等,你就需要使用 NavigationPath

NavigationPath 的工作原理是,它维护了一个路径数组,每个元素代表一个页面。当你需要导航到一个新的页面时,你只需要将这个页面添加到路径数组中。当你需要后退时,你只需要从路径数组中移除最后一个元素。这样,你就可以实现任意复杂的导航逻辑。

看个例子

假设我们有一个 TVShow 结构体,它包含电视剧的名字。当用户点击一个电视剧的名字时,他们会被导航到这个电视剧的详细信息页面。

struct ContentView: View {    @State private var path = NavigationPath()    @State private var tvShows = [ TVShow(name: "Game of Thrones"), TVShow(name: "Breaking Bad"), TVShow(name: "The Witcher") ]    var body: some View {        NavigationStack(path: $path) {            List {                Text("Select a TV show to get started.")                    .font(.subheadline.weight(.semibold))                ForEach(tvShows, id: \.name) { show in                    NavigationLink(value: show, label: {                        Text(show.name)                            .font(.subheadline.weight(.medium))                    })                }                Button(action: showFriends) {                    Text("This isn't navigation")                }            }            .navigationDestination(for: TVShow.self, destination: { show in                TVShowView(onSelectReset: { popToRoot() }, show: show, otherShows: tvShows)            })            .navigationTitle(Text("Select your show"))        }        .onChange(of: path.count) { oldValue, newValue in            print(newValue)        }    }    func showFriends() {        let show = TVShow(name: "Friends")        path.append(show)    }        func popToRoot() {        path.removeLast(path.count)    }}struct TVShowView: View {    var onSelectReset: () -> Void    var show: TVShow    var otherShows: [TVShow]    var body: some View {        VStack {            Text(show.name)                .font(.title)                .padding(.bottom)            Button(action: onSelectReset) {                Text("Reset Selection")            }            List(otherShows, id: \.name) { otherShow in                Text(otherShow.name)            }        }        .padding()    }}struct TVShow: Hashable {    let name: String    let premiereDate: Date = Date.now    var description: String = "detail"}

代码中,NavigationPath 被用作一个 @State 变量,这意味着它会自动响应变化,并更新视图。当你修改 NavigationPath 中的路径数组时,视图会自动更新,显示新的页面。

NavigationSplitView

以下是一个基于 NavigationSplitView 的三栏视图的示例。这个示例包含了一个主视图,一个次级视图和一个详细视图。

struct ContentView: View {    @State var books: [Book] = [        Book(title: "Book 1", author: "Author 1", description: "Description 1"),        Book(title: "Book 2", author: "Author 2", description: "Description 2"),        Book(title: "Book 3", author: "Author 3", description: "Description 3")    ]    @State var selectedBook: Book?    @State var splitVisibility: NavigationSplitViewVisibility = .all    var body: some View {        NavigationSplitView(columnVisibility: $splitVisibility, sidebar: {            List(books) { book in                Button(action: { selectedBook = book }) {                    Text(book.title)                }            }        }, content: {            if let book = selectedBook {                Text("Author: \(book.author)")            } else {                Text("Select a Book")            }        }, detail: {            if let book = selectedBook {                Text(book.description)            } else {                Text("Book details will appear here")            }        })        .onChange(of: selectedBook) { oldValue, newValue in            //...        }    }}struct Book: Identifiable, Equatable {    var id = UUID()    var title: String    var author: String    var description: String}

示例中,sidebar 是主视图,它显示了一个图书列表。当用户选择一个图书时,content 视图会显示图书的作者,detail 视图会显示图书的详细信息。NavigationSplitView 会根据 splitVisibility 的值来决定显示哪些视图。

自定义导航栏

交互样式

使用 navigationSplitViewStyle(_:) 修饰符

改变标签栏背景色

.toolbarBackground(.yellow.gradient, for: .automatic).toolbarBackground(.visible, for: .automatic)

列宽

navigationSplitViewColumnWidth(_:) 修饰符用于指定列宽。

设置列的最小、最大和理想大小,使用 navigationSplitViewColumnWidth(min:ideal:max:)。可以修饰于不同的列上。

自定返回按钮

先通过修饰符隐藏系统返回按钮 .navigationBarBackButtonHidden(true)。然后通过 ToolbarItem(placement: .navigationBarLeading) 来添加自定义的返回按钮。

struct BookDetailView: View {    var book: Book    @Binding var isDetailShown: Bool    var body: some View {        VStack {            Text(book.title).font(.largeTitle)            Text("Author: \(book.author)").font(.title)            Text(book.description).padding()        }        .navigationBarBackButtonHidden(true)        .navigationTitle(book.title)        .toolbar {            ToolbarItem(placement: .navigationBarLeading) {                Button {                    isDetailShown = false                } label: {                    HStack {                        Image(systemName: "chevron.backward")                        Text("Back to Books")                    }                }            }        }    }}

Inspectors右侧多出一栏

Inspector 的示例

struct Book: Identifiable {    var id = UUID()    var title: String    var author: String    var description: String}struct ContentView: View {    @State var books: [Book] = [        Book(title: "Book 1", author: "Author 1", description: "Description 1"),        Book(title: "Book 2", author: "Author 2", description: "Description 2"),        Book(title: "Book 3", author: "Author 3", description: "Description 3")    ]    @State var selectedBook: Book?    @State var showInspector: Bool = false    @State var splitVisibility: NavigationSplitViewVisibility = .all        var body: some View {        NavigationSplitView(columnVisibility: $splitVisibility, sidebar: {            List(books) { book in                Button(action: { selectedBook = book }) {                    Text(book.title)                }            }        }, content: {            if let book = selectedBook {                Text("Author: \(book.author)")            } else {                Text("Select a Book")            }        }, detail: {            Button("Inspector 开关") {                showInspector.toggle()            }            if let book = selectedBook {                Text(book.description)            } else {                Text("Book details will appear here")            }        })        .inspector(isPresented: $showInspector) {            if let book = selectedBook {                InspectorView(book: book)            }        }    }}struct InspectorView: View {    var book: Book    var body: some View {        VStack {            Text(book.title).font(.largeTitle)            Text("Author: \(book.author)").font(.title)            Text(book.description).padding()        }        .inspectorColumnWidth(200)        .presentationDetents([.medium, .large])    }}

它显示了一个图书列表。当用户选择一个图书时,会显示 InspectorView,这是辅助视图,它显示了图书的详细信息。inspector 方法用于显示和隐藏 InspectorView,inspectorColumnWidth 方法用于设置辅助视图的宽度,presentationDetents 方法用于设置辅助视图的大小。

导航状态保存和还原

通过 SceneStorage 保存导航路径,程序终止时会持久化存储路径,重启时恢复路径。

protocol URLProcessor<RouteType> {    associatedtype RouteType: Hashable    func process(_ url: URL, mutating: inout [RouteType])}protocol UserActivityProcessor<RouteType> {    associatedtype RouteType: Hashable    func process(_ activity: NSUserActivity, mutating: inout [RouteType])}@Observable@MainActor final class RouteManager<RouteType: Hashable> {    var navigationPath: [RouteType] = []    private let jsonDecoder = JSONDecoder()    private let jsonEncoder = JSONEncoder()    private let urlProcessor: any URLProcessor<RouteType>    private let activityProcessor: any UserActivityProcessor<RouteType>    init(        urlProcessor: some URLProcessor<RouteType>,        activityProcessor: some UserActivityProcessor<RouteType>    ) {        self.urlProcessor = urlProcessor        self.activityProcessor = activityProcessor    }    func process(_ activity: NSUserActivity) {        activityProcessor.process(activity, mutating: &navigationPath)    }    func process(_ url: URL) {        urlProcessor.process(url, mutating: &navigationPath)    }}extension RouteManager where RouteType: Codable {    func toData() -> Data? {        try? jsonEncoder.encode(navigationPath)    }        func restore(from data: Data) {        do {            navigationPath = try jsonDecoder.decode([RouteType].self, from: data)        } catch {            navigationPath = []        }    }}

这段代码定义了一个名为 RouteManager 的类,它用于处理和管理导航路径。这个类使用了 SwiftUI 的 @MainActor@Observable 属性包装器,以确保它的操作在主线程上执行,并且当 navigationPath 发生变化时,会自动更新相关的 UI。

RouteManager 类有两个协议类型的属性:urlProcessoractivityProcessor。这两个属性分别用于处理 URL 和用户活动(NSUserActivity)。这两个处理器的任务是根据给定的 URL 或用户活动,更新 navigationPath

RouteManager 类还有两个方法:process(_ activity: NSUserActivity)process(_ url: URL)。这两个方法分别用于处理用户活动和 URL。处理的方式是调用相应的处理器的 process 方法。

此外,RouteManager 类还有一个扩展,这个扩展只适用于 RouteTypeCodable 的情况。这个扩展提供了两个方法:toData()restore(from data: Data)toData() 方法将 navigationPath 转换为 Datarestore(from data: Data) 方法则将 Data 转换回 navigationPath。这两个方法可以用于将 navigationPath 保存到磁盘,并在需要时从磁盘恢复。

struct MainView: View {    @SceneStorage("navigationState") private var navigationData: Data?    @State private var dataStore = DataStore()    @State private var routeManager = RouteManager<Route>(        urlProcessor: SomeURLProcessor(),        activityProcessor: SomeUserActivityProcessor()    )        var body: some View {        NavigationStack(path: $routeManager.navigationPath) {            SomeView(categories: dataStore.categories)                .task { await dataStore.fetch() }                .navigationDestination(for: Route.self) { route in                    // ...                }                .onOpenURL { routeManager.process($0) }        }        .task {            if let navigationData = navigationData {                routeManager.restore(from: navigationData)            }                        for await _ in routeManager.$navigationPath.values {                navigationData = routeManager.toData()            }        }    }}

@SceneStorage("navigationState") 是用来保存和恢复导航状态的。当应用程序被挂起时,它会自动将 navigationData 保存到磁盘,当应用程序重新启动时,它会自动从磁盘恢复 navigationData

@State private var dataStore = DataStore()@State private var routeManager = RouteManager<Route>(...) 是用来存储数据和路由管理器的。DataStore 是用来获取和存储数据的,RouteManager 是用来处理和管理导航路径的。

body 属性定义了视图的内容。它首先创建了一个 NavigationStack,然后在这个 NavigationStack 中创建了一个 SomeViewSomeView 使用了 dataStore.categories 作为它的参数,并且在被创建后立即执行 dataStore.fetch() 来获取数据。

body 属性还定义了一个任务,这个任务在视图被创建后立即执行。这个任务首先检查 navigationData 是否存在,如果存在,就使用 routeManager.restore(from: navigationData) 来恢复导航路径。然后,它监听 routeManager.$navigationPath.values,每当 navigationPath 发生变化时,就使用 routeManager.toData() 来将 navigationPath 转换为 Data,并将结果保存到 navigationData 中。

布局基础

布局-基础

基本元素样式

通过 .font(.title) 设置字体大小。

.stroke(Color.blue) 设置描边。举个例子:

struct ContentView: View {    var body: some View {        Rectangle()            .stroke(Color.orange, style: StrokeStyle(lineWidth: 10, lineCap: .round, dash: [30]))            .padding(30)    }}

StrokeStyle(lineWidth: 10, lineCap: .round, dash: [30]) 定义了描边的样式,其中 lineWidth: 10 表示线宽为 10,lineCap: .round 表示线帽样式为圆形,dash: [30] 表示虚线模式,数组中的数字表示虚线和间隙的交替长度。

frame

.frame(width: 200, height:100, alignment: .topLeading)

  • width: 200 表示视图的宽度为 200 点。
  • height: 100 表示视图的高度为 100 点。
  • alignment: .topLeading 表示视图的内容应该在视图的左上角对齐。.topLeading 是 SwiftUI 中的一个对齐方式,表示左上角对齐。

Stack

多个视图通过 Stack 视图进行对齐排列。这些 Stack 视图主要是:

  • ZStack:Z轴排列
  • VStack:垂直排列
  • HStack:横向排列

间隔

视图之间的间隔可以用 Space(),它可以在各种布局视图中使用。

布局-留白

Space

Spacer 是一个灵活的空间,它会尽可能地占用多的空间,从而将其周围的视图推向堆栈的两边。因此,第一个 Text 视图会被推到左边,第二个 Text 视图会被推到中间,第三个 Text 视图会被推到右边。

struct ContentView: View {    var body: some View {        HStack {            Text("左边")            Spacer()            Text("中间")            Spacer()            Text("右边")        }    }}

下面这个例子是用 Space() 让三个视图都居右。

struct ContentView: View {    var body: some View {        HStack {            Spacer()            Text("视图1")            Text("视图2")            Text("视图3")        }    }}

布局-对齐

frame 对齐

.frame(width: 100, height: 50, alignment: .topLeading)

可设置对齐的视图

在 SwiftUI 中,许多视图都接受 alignment 参数,用于控制其子视图的对齐方式。以下是一些常见的接受 alignment 参数的视图:

  • HStack(alignment: .bottom):水平堆栈视图,可以控制其子视图在垂直方向上的对齐方式。
  • VStack(alignment: .trailing):垂直堆栈视图,可以控制其子视图在水平方向上的对齐方式。
  • ZStack(alignment: .center):深度堆栈视图,可以控制其子视图在水平和垂直方向上的对齐方式。
  • GridRow(alignment: .firstTextBaseline):用于定义网格的行或列的大小,可以设置行或列中的内容的对齐方式。。

基线对齐

你可以使用 alignment 参数来设置视图的对齐方式,包括基线对齐。以下是一个例子:

HStack(alignment: .firstTextBaseline) {    Text("Hello")    Text("World").font(.largeTitle)}

在这个例子中,HStack 是一个水平堆栈视图,它会将其子视图水平排列。alignment: .firstTextBaseline 是一个参数,用于设置堆栈中的内容的对齐方式。.firstTextBaseline 表示所有文本视图都应该根据它们的第一行的基线对齐。基线是文本字符的底部线。

因此,这个 HStack 中的两个 Text 视图会根据它们的第一行的基线对齐,即使它们的字体大小不同。

布局-居中

在 SwiftUI 中,有多种方法可以使视图居中:

Spacer

使用 SpacerSpacer 是一个灵活的空间,它会尽可能地占用多的空间,从而将其周围的视图推向堆栈的两边。如果在一个视图的两边都放置一个 Spacer,那么这个视图就会被推到中间。

HStack {    Spacer()    Text("居中")    Spacer()}

alignment

使用 alignment 参数:许多 SwiftUI 视图都接受 alignment 参数,用于控制其子视图的对齐方式。例如,VStackHStack 都接受 alignment 参数。

VStack(alignment: .center) {    Text("居中")}

frame

使用 frame 方法:frame 方法可以设置视图的尺寸和对齐方式。如果你想让一个视图在其父视图中居中,你可以使用 frame(maxWidth: .infinity, maxHeight: .infinity) 来使视图尽可能地占用多的空间,然后使用 alignment: .center 来使视图在这个空间中居中。

Text("居中")    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)

布局-offset偏移

struct OffsetDemo: View {    @State var offset: CGFloat = 0    var body: some View {        VStack {            Text("Hello, World!")                .font(.largeTitle)                .offset(y: offset)            Slider(value: $offset, in: -100...100)                .padding()        }    }}

我们创建了一个 Text 视图和一个 SliderText 视图使用了 .offset(y: offset) 修饰符,这意味着它的 y 偏移量会根据 offset 的值改变。Slider 则用于改变 offset 的值。当你移动滑块时,Text 视图的位置也会相应地上下移动。

Safe Area

ignoresSafeArea 忽略安全区域

使用 .ignoresSafeArea() 可以忽略安全区域。默认是所有方向都忽略。

如果只忽略部分方向,可以按照下面方法做:

// 默认会同时包含 .keyboard 和 .container。.ignoresSafeArea(edges: .top).ignoresSafeArea(edges: .vertical).ignoresSafeArea(edges: [.leading, .trailing])// 可以对安全区域分别指定.ignoresSafeArea(.keyboard, edges: .top).ignoresSafeArea(.container, edges: [.leading, .trailing])

safeAreaInset

safeAreaInset 是 SwiftUI 中的一个属性,它允许你将视图放置在安全区域内。”安全区域”是指设备屏幕上的一块区域,这块区域不会被系统界面(如状态栏、导航栏、工具栏、Tab栏等)遮挡。

例如,你可以使用 safeAreaInset 将一个视图放置在屏幕底部的安全区域内,代码如下:

VStack {    Text("Hello, World!")}.safeAreaInset(edge: .bottom, spacing: 10) {    Button("Press me") {        print("Button pressed")    }}

在这个例子中,”Press me” 按钮会被放置在屏幕底部的安全区域内,而且距离底部有 10 个点的间距。

下面是更完整点的例子:

struct ContentView: View {    @State var tasks: [TaskModel] = (0...10).map { TaskModel(name: "Task \($0)") }    @State var taskName = ""    @State var isFocused: Bool = false    var body: some View {        NavigationView {            VStack {                List {                    ForEach(tasks) { task in                        Text(task.name)                    }                }                .listStyle(PlainListStyle())                .safeAreaInset(edge: .bottom) {                    HStack {                        TextField("Add task", text: $taskName, onCommit: {                            addTask()                        })                        .textFieldStyle(RoundedBorderTextFieldStyle())                        .padding(.leading, 10)                                                Button(action: {                            addTask()                        }) {                            Image(systemName: "plus")                        }                        .padding(.trailing, 10)                    }                    .padding(.bottom, isFocused ? 0 : 10)                    .background(Color.white)                }                .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in                    withAnimation {                        isFocused = true                    }                }                .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in                    withAnimation {                        isFocused = false                    }                }            }            .navigationBarTitle("Task List Demo")        }    }    func addTask() {        if !taskName.isEmpty {            withAnimation {                tasks.append(TaskModel(name: taskName))            }            taskName = ""        }    }}struct TaskModel: Identifiable {    let id = UUID()    let name: String}

用户可以在底部的输入框中输入任务名称,然后点击 “+” 按钮将任务添加到任务清单中。添加的任务会显示在屏幕的上方。当键盘出现或消失时,底部的输入框会相应地移动,以确保不会被键盘遮挡。

布局原理

SwiftUI 的布局系统是一个两阶段的协商过程,涉及到父视图和子视图之间的交互。

建议阶段:在这个阶段,父视图会向子视图提出一个建议尺寸。这个建议尺寸是父视图希望子视图的大小。例如,如果父视图是一个 VStack,那么它可能会向子视图提出一个具有明确高度、宽度未指定的建议尺寸。

需求阶段:在这个阶段,子视图会根据父视图的建议尺寸来确定自己的需求尺寸。子视图可以选择接受父视图的建议尺寸,也可以选择返回一个不同的尺寸。例如,一个 Text 视图可能会返回一个刚好能够容纳其文本的尺寸。

在这个过程中,父视图和子视图都有可能影响最终的布局结果。父视图可以通过调整建议尺寸来影响子视图的大小,而子视图可以通过返回不同的需求尺寸来影响自己的大小。

在一些复杂的布局场景中,可能需要进行多轮的协商才能得到最终的布局结果。例如,如果一个视图使用了 GeometryReader 来获取其在父视图中的位置和尺寸,那么 GeometryReader 可能会在布局稳定之前,多次向子视图发送新的几何信息。

总的来说 SwiftUI 它允许父视图和子视图之间进行协商,以达到最佳的布局效果。

布局进阶

AnyLayout

使用 AnyLayout 包装布局组件,可以在布局之间进行切换,同时保持动画效果。

struct WeatherLayout: View {    @State private var changeLayout = false    var body: some View {        let layout = changeLayout ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())        layout {            WeatherView(icon: "sun.max.fill", temperature: 25, color: .yellow)            WeatherView(icon: "cloud.rain.fill", temperature: 18, color: .blue)            WeatherView(icon: "snow", temperature: -5, color: .white)        }        .animation(.default, value: changeLayout)        .onTapGesture {            changeLayout.toggle()        }    }}struct WeatherView: View {    let icon: String    let temperature: Int    let color: Color    var body: some View {        VStack {            Image(systemName: icon)                .font(.system(size: 80))                .foregroundColor(color)            Text("\(temperature)°")                .font(.system(size: 50))                .foregroundColor(color)        }        .frame(width: 120, height: 120)    }}

代码中,我们创建了一个 WeatherView 视图,它包含一个天气图标和一个温度标签。然后,我们在 WeatherLayout 视图中使用 AnyLayout 来动态改变布局。用户可以通过点击视图来在水平布局和垂直布局之间切换。

ViewThatFits

ViewThatFits 是一个自动选择最适合当前屏幕大小的子视图进行显示的视图。它会根据可用空间的大小来决定如何布局和显示子视图。

ViewThatFits 是一个在 SwiftUI 中用于选择最适合显示的视图的组件。它的工作原理如下:

  • 首先,ViewThatFits 会测量在特定轴(水平或垂直)或两个轴(水平和垂直)上的可用空间。这是通过 SwiftUI 的布局系统来完成的,该系统提供了当前视图的大小和位置信息。

  • 接着,ViewThatFits 会测量第一个视图的大小。这是通过调用视图的 measure(in:) 方法来完成的,该方法返回一个包含视图理想大小的 CGSize 值。

  • 如果第一个视图的大小适合可用空间,ViewThatFits 就会选择并放置这个视图。放置视图是通过调用视图的 layout(in:) 方法来完成的,该方法接受一个 CGRect 值,该值定义了视图在其父视图中的位置和大小。

  • 如果第一个视图的大小不适合可用空间,ViewThatFits 会继续测量第二个视图的大小。如果第二个视图的大小适合可用空间,ViewThatFits 就会选择并放置这个视图。

  • 如果所有视图的大小都不适合可用空间,ViewThatFits 会选择并放置 ViewBuilder 闭包中的最后一个视图。ViewBuilder 是一个特殊的闭包,它可以根据其内容动态创建视图。

ViewThatFits(in: .horizontal) {    Text("晴天,气温25°") // 宽度在200到300之间        .font(.title)        .foregroundColor(.yellow)    Text("晴天,25°") // 宽度在150到200之间        .font(.title)        .foregroundColor(.gray)    Text("晴25") // 宽度在100到150之间        .font(.title)        .foregroundColor(.white)}.border(Color.green) // ViewThatFits所需的大小.frame(width:200).border(Color.orange) // 父视图提议的大小

在不同的宽度下,ViewThatFits 会选择不同的视图进行显示。在上面的示例中,当父视图的宽度在100到150之间时,ViewThatFits 会选择显示 “晴25” 这个视图。

通过 ViewThatFits 来确定内容是否可滚动。

struct ContentView: View {    @State var step: CGFloat = 3    var count: Int {        Int(step)    }    var body: some View {        VStack(alignment:.leading) {            Text("数量: \(count)")                .font(.title)                .foregroundColor(.blue)            Stepper("数量", value: $step, in: 3...20)            ViewThatFits {                content                ScrollView(.horizontal,showsIndicators: true) {                    content                }            }        }        .padding()    }    var content: some View {        HStack {            ForEach(0 ..< count, id: \.self) { i in                Rectangle()                    .fill(Color.green)                    .frame(width: 30, height: 30)                    .overlay(                        Text("\(i)")                            .font(.headline)                            .foregroundColor(.white)                    )            }        }    }}

Layout协议

通过实现 Layout 协议,创建一个水平堆栈布局,其中所有子视图的宽度都相等。

struct OptimizedEqualWidthHStack: Layout {  func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {    if subviews.isEmpty { return .zero }    let maxSubviewSize = calculateMaxSize(subviews: subviews)    let totalSpacing = calculateSpacing(subviews: subviews).reduce(0, +)    return CGSize(width: maxSubviewSize.width * CGFloat(subviews.count) + totalSpacing, height: maxSubviewSize.height)  }  func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {    if subviews.isEmpty { return }    let maxSubviewSize = calculateMaxSize(subviews: subviews)    let spacings = calculateSpacing(subviews: subviews)    let placementProposal = ProposedViewSize(width: maxSubviewSize.width, height: maxSubviewSize.height)    var nextX = bounds.minX + maxSubviewSize.width / 2    for index in subviews.indices {      subviews[index].place(at: CGPoint(x: nextX, y: bounds.midY), anchor: .center, proposal: placementProposal)      nextX += maxSubviewSize.width + spacings[index]    }  }  private func calculateMaxSize(subviews: Subviews) -> CGSize {    return subviews.map { $0.sizeThatFits(.unspecified) }.reduce(.zero) { CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height)) }  }  private func calculateSpacing(subviews: Subviews) -> [CGFloat] {    return subviews.indices.map { $0 < subviews.count - 1 ? subviews[$0].spacing.distance(to: subviews[$0 + 1].spacing, along: .horizontal) : 0 }  }}

上面这段代码中 sizeThatFits 方法计算并返回布局容器需要的大小,以便排列其子视图。它首先检查子视图数组是否为空,如果为空则返回 .zero。然后,它计算子视图的最大尺寸和总间距,最后返回一个 CGSize 对象,其宽度等于最大子视图宽度乘以子视图数量加上总间距,高度等于最大子视图高度。

placeSubviews 方法将子视图放置在布局容器中。它首先检查子视图数组是否为空,如果为空则返回。然后,它计算子视图的最大尺寸和间距,然后遍历子视图数组,将每个子视图放置在布局容器中的适当位置。

calculateMaxSize 和 calculateSpacing 是两个私有方法,用于计算子视图的最大尺寸和间距。

GeometryReader

在 SwiftUI 中,有多种方法可以获取和控制视图的尺寸:

  • frame(width:60, height:60):这个方法会为子视图提供一个建议的尺寸,这里是 60 x 60。
  • fixedSize():这个方法会为子视图提供一个未指定模式的建议尺寸,这意味着视图会尽可能地大以适应其内容。
  • frame(minWidth: 120, maxWidth: 360):这个方法会将子视图的需求尺寸控制在指定的范围中,这里是宽度在 120 到 360 之间。
  • frame(idealWidth: 120, idealHeight: 120):这个方法会返回一个需求尺寸,如果当前视图收到为未指定模式的建议尺寸,那么它会返回 120 x 120 的尺寸。
  • GeometryReaderGeometryReader 会将建议尺寸作为需求尺寸直接返回,这意味着它会充满全部可用区域。你可以使用 GeometryReader 来获取其内容的尺寸和位置。

GeometryReader 可以获取其内容的尺寸和位置。在这个例子中,我们使用 GeometryReader 来获取视图的尺寸,然后打印出来。这对于理解 SwiftUI 的布局系统和调试布局问题非常有用。

extension View {    func logSizeInfo(_ label: String = "") -> some View {        background(            GeometryReader { proxy in                Color.clear                    .onAppear(perform: {                        debugPrint("\(label) Size: \(proxy.size)")                    })            }        )    }}struct ContentView: View {    var body: some View {        VStack {            Text("大标题")                .font(.largeTitle)                .logSizeInfo("大标题视图") // 打印视图尺寸            Text("正文")                .logSizeInfo("正文视图")        }    }}

这段代码首先定义了一个 View 的扩展,添加了一个 logSizeInfo(_:) 方法。这个方法接受一个标签字符串作为参数,然后返回一个新的视图。这个新的视图在背景中使用 GeometryReader 来获取并打印视图的尺寸。

然后,我们创建了一个 VStack 视图,其中包含一个 Text 视图。我们为 Text 视图调用了 logSizeInfo(_:) 方法,以打印其尺寸。

如何利用 GeometryReader 来绘制一个圆形?

struct CircleView: View {    var body: some View {        GeometryReader { proxy in            Path { path in                let radius = min(proxy.size.width, proxy.size.height) / 2                let center = CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2)                path.addArc(center: center, radius: radius, startAngle: .zero, endAngle: .init(degrees: 360), clockwise: false)            }            .fill(Color.blue)        }    }}

在这个例子中,我们首先获取 GeometryReader 的尺寸,然后计算出半径和中心点的位置。然后,我们使用 PathaddArc(center:radius:startAngle:endAngle:clockwise:) 方法来添加一个圆形路径。最后,我们使用 fill(_:) 方法来填充路径,颜色为蓝色。

关于 GeometryReader 性能问题

GeometryReader 是 SwiftUI 中的一个工具,它可以帮助我们获取视图的大小和位置。但是,它在获取这些信息时,需要等待视图被评估、布局和渲染完成。这就好比你在装修房子时,需要等待墙壁砌好、油漆干燥后,才能测量墙壁的尺寸。这个过程可能需要等待一段时间,而且可能需要多次重复,因为每次墙壁的尺寸改变,都需要重新测量。

这就是 GeometryReader 可能会影响性能的原因。它需要等待视图完成一轮的评估、布局和渲染,然后才能获取到尺寸数据,然后可能需要根据这些数据重新调整布局,这就需要再次进行评估、布局和渲染。这个过程可能需要重复多次,导致视图被多次重新评估和布局。

但是,随着 SwiftUI 的更新,这个问题已经有所改善。现在,我们可以创建自定义的布局容器,这些容器可以在布局阶段就获取到父视图的建议尺寸和所有子视图的需求尺寸,这样就可以避免反复传递尺寸数据,减少了视图的反复更新。

alignmentGuide

alignmentGuide是SwiftUI中的一个修饰符,它允许你自定义视图的对齐方式。你可以使用它来调整视图在其父视图或同级视图中的位置。

当你在一个视图上应用alignmentGuide修饰符时,你需要提供一个对齐标识符和一个闭包。对齐标识符定义了你想要调整的对齐方式(例如,.leading.trailing.center等)。闭包接收一个参数,这个参数包含了视图的尺寸,你可以使用这个参数来计算对齐指南的偏移量。

举个例子:

struct ContentView: View {    var body: some View {        HStack(alignment: .top) {            CircleView()                .alignmentGuide(.top) { vd in                    vd[.top] + 50                }            CircleView()        }        .padding()        .border(Color.gray)    }    struct CircleView: View {        var body: some View {            Circle()                .fill(Color.mint)                .frame(width: 50, height: 50)        }    }}

在HStack中,第一个CircleView使用了.alignmentGuide修饰符,这使得它在顶部对齐时向下偏移了50个单位。

布局进阶-参考资料

WWDC

23

22

20

官方接口文档

Stack

Stack View 有 VStack、HStack 和 ZStack

struct PlayStackView: View {    var body: some View {        // 默认是 VStack 竖排                // 横排        HStack {            Text("左")            Spacer()            Text("右")        }        .padding()                // Z 轴排        ZStack(alignment: .top) {            Image("logo")            Text("戴铭的开发小册子")                .font(.title)                .bold()                .foregroundColor(.white)                .shadow(color: .black, radius: 1, x: 0, y: 2)                .padding()        }                Color.cyan            .cornerRadius(10)            .frame(width: 100, height: 100)            .overlay(                Text("一段文字")            )    }}

GroupBox

struct PlayGroupBoxView: View {    var body: some View {        GroupBox {            Text("这是 GroupBox 的内容")        } label: {            Label("标题一", systemImage: "t.square.fill")        }        .padding()                GroupBox {            Text("还是 GroupBox 的内容")        } label: {            Label("标题二", systemImage: "t.square.fill")        }        .padding()        .groupBoxStyle(PCGroupBoxStyle())    }}struct PCGroupBoxStyle: GroupBoxStyle {    func makeBody(configuration: Configuration) -> some View {        VStack(alignment: .leading) {            configuration.label                .font(.title)            configuration.content        }        .padding()        .background(.pink)        .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))    }}

叠加 GroupBox 颜色会有区分

GroupBox {    Text("电视剧名称: 人民的名义")    GroupBox {        Text("播放时间: 每周一至周五")    }}

最后,您还可以 GroupBox 使用 Label .将 Label 定位为 GroupBox 容器的标题。

GroupBox(label: Label("电视剧", systemImage: "tv")) {    HStack {        Text("播放时间: 每周一至周五")            .padding()        Spacer()    }}

GroupBox 也可以用于创建自定义的按钮组,如下所示:

struct TVShowCardView: View {    var body: some View {        VStack(alignment: .leading) {            // The header of the card            // - Photo, Show Name and Genre            HStack {                Circle()                    .frame(width: 40, height: 40)                    .foregroundColor(.gray)                VStack(alignment: .leading, spacing: 3) {                    Text("权力的游戏")                        .font(.headline)                        .fontWeight(.semibold)                    Text("奇幻剧")                        .font(.caption)                }                Spacer()            }                        Divider()                .foregroundColor(Color(uiColor: UIColor.systemGray6))                .padding([.top, .bottom], 8)                        // The description of the show in a few lines            Text("《权力的游戏》是一部改编自乔治·马丁的奇幻小说系列《冰与火之歌》的电视剧。")                .font(.body)                        // Buttons to watch, share or save the show            HStack {                actionGroupBox(imageName: "play.rectangle", actionName: "观看", action: { print("Watching...") })                actionGroupBox(imageName: "square.and.arrow.up", actionName: "分享", action: { print("Sharing...") })                actionGroupBox(imageName: "bookmark", actionName: "保存", action: { print("Saving...") })            }        }        .padding()        .background(Color.white)        .cornerRadius(10)    }        // A function to create a GroupBox for an action    func actionGroupBox(imageName: String, actionName: String, action: @escaping () -> Void) -> some View {        GroupBox {            VStack(spacing: 5) {                Image(systemName: imageName)                    .font(.headline)                Text(actionName)                    .font(.caption)            }            .foregroundColor(.red)            .frame(maxWidth: .infinity)        }.onTapGesture {            action()        }    }}struct ContentView: View {    var body: some View {        NavigationView {            ScrollView {                TVShowCardView()                                Spacer()            }            .padding()            .background(Color(UIColor.systemGray6))            .navigationTitle("电视剧")            .shadow(color: Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 0.25), radius: 10, x: 0, y: 0)        }    }}

TabView

基本用法

struct PlayTabView: View {    @State private var selection = 0        var body: some View {        ZStack(alignment: .bottom) {            TabView(selection: $selection) {                Text("one")                    .tabItem {                        Text("首页")                            .hidden()                    }                    .tag(0)                Text("two")                    .tabItem {                        Text("二栏")                    }                    .tag(1)                Text("three")                    .tabItem {                        Text("三栏")                    }                    .tag(2)                Text("four")                    .tag(3)                Text("five")                    .tag(4)                Text("six")                    .tag(5)                Text("seven")                    .tag(6)                Text("eight")                    .tag(7)                Text("nine")                    .tag(8)                Text("ten")                    .tag(9)            } // end TabView                                    HStack {                Button("上一页") {                    if selection > 0 {                        selection -= 1                    }                }                .keyboardShortcut(.cancelAction)                Button("下一页") {                    if selection < 9 {                        selection += 1                    }                }                .keyboardShortcut(.defaultAction)            } // end HStack            .padding()        }    }}

.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) 可以实现 UIPageViewController 的效果,如果要给小白点加上背景,可以多添加一个 .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) 修改器。

添加提醒

struct ContentView: View {    @State private var bookVm: BooksViewModel        init() {        bookVm = BooksViewModel()    }        var body: some View {        TabView {            BookListView(bookVm: bookVm)                .tabItem {                    Image(systemName: "list.bullet.rectangle.fill")                    Text("Book List")                }            SelectedBooksView(bookVm: bookVm)                .badge(bookVm.selectedBooks.count)                .tabItem {                    Image(systemName: "book.fill")                    Text("Selected Books")                }        }    }}

自定义样式

iOS 14 和 macOS 11 开始可以使用 tabViewStyle 修饰符更改 TabView 样式。比如有页面指示器的水平滚动图片。

显示页面指示器:

.tabViewStyle(.page(indexDisplayMode: .always))

.tabViewStyle(.page(indexDisplayMode: .never)) 修饰符隐藏页面指示器。

水平滚动图片:

struct ContentView: View {    let images = ["pencil", "scribble", "highlighter"]    var body: some View {        VStack {            TabView {                ForEach(images, id: \.self) { imageName in                    Image(systemName: imageName)                        .resizable()                        .scaledToFit()                }            }            .tabViewStyle(.page(indexDisplayMode: .always))            .frame(height: 100)        }    }}

分页视图

struct OnboardingView: View {    var body: some View {        TabView {            OnboardingPageView(imageName: "figure.mixed.cardio",                               title: "Welcome",                               description: "Welcome to MyApp! Get started by exploring our amazing features.")            OnboardingPageView(imageName: "figure.archery",                               title: "Discover",                               description: "Discover new content and stay up-to-date with the latest news and updates.")            OnboardingPageView(imageName: "figure.yoga",                               title: "Connect",                               description: "Connect with friends and share your experiences with the community.")        }        .tabViewStyle(.page(indexDisplayMode: .always))        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))    }}

.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) 修饰符添加了背景。这将在点周围添加一个背景,使其在任何背景下都更容易看到。

背景颜色

iOS 16 和 macOS 13 开始可以更改 TabView 的背景颜色。

struct MainScreen: View {    var body: some View {        TabView {            NavigationView {                BookListView()                    .navigationTitle("图书列表")                    .toolbarBackground(.yellow, for: .navigationBar)                    .toolbarBackground(.visible, for: .navigationBar)            }            .tabItem {                Label("图书", systemImage: "book.closed")            }            UserPreferencesView()                .tabItem {                    Label("设置", systemImage: "gearshape")                }            .toolbarBackground(.indigo, for: .tabBar)            .toolbarBackground(.visible, for: .tabBar)            .toolbarColorScheme(.dark, for: .tabBar)        }    }}struct BookListView: View {    var body: some View {        Text("这里是图书列表")    }}struct UserPreferencesView: View {    var body: some View {        Text("这里是用户设置")    }}

ControlGroup

struct PlayControlGroupView: View {    var body: some View {        ControlGroup {            Button {                print("plus")            } label: {                Image(systemName: "plus")            }            Button {                print("minus")            } label: {                Image(systemName: "minus")            }        }        .padding()        .controlGroupStyle(.automatic) // .automatic 是默认样式,还有 .navigation    }}

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))    }}

ContentUnavailableView

基本用法

struct ArchivedInfosView: View {    @Environment(\.modelContext) var modelContext    @Query var infos: [IOInfo]    ...        var body: some View {        List(selection: $selectInfo) {            ForEach(infos) { info in                ...            }        }        .overlay {            if infos.isEmpty {                ContentUnavailableView {                    Label("无归档", systemImage: "archivebox")                } description: {                    Text("点击下方按钮添加一个归档资料")                } actions: {                    Button("新增") {                        addInfo()                    }                }            }        }    }    ...}

搜索

struct ContentView: View {    @Bindable var vm: VModel    ...    var body: some View {        NavigationStack {            List(vm.items, id: \.self) { item in                ...            }            .navigationTitle("Products")            .overlay {                if vm.items.isEmpty {                    ContentUnavailableView.search(text: vm.query)                }            }            .searchable(text: $vm.query)        }        ...    }}

小册子之 List、Lazy 容器、ScrollView、Grid 和 Table 数据集合 SwiftUI 视图

以下内容已整理到小册子中,小册子代码在 Github 上,本文会随着系统更新和我更多的实践而新增和更新,你可以购买“戴铭的开发小册子”应用(98元),来跟踪查看本文内容新增和更新。

本文属于小册子系列中的一篇,已发布系列文章有:

ForEach

使用

在 SwiftUI 中,ForEach 是一个结构体,它可以创建一组视图,每个视图都有一个与数据集中的元素相对应的唯一标识符。这对于在列表或其他集合视图中显示数据非常有用。

以下视图集会用到 ForEach:

  • List
  • ScrollView
  • LazyVStack / LazyHStack
  • Picker
  • Grids (LazyVGrid / LazyHGrid)

例如,如果你有一个 BookmarkModel 的数组,并且你想为每个书签创建一个文本视图,你可以这样做:

struct ContentView: View {    var bookmarks: [BookmarkModel]    var body: some View {        List {            ForEach(bookmarks) { bookmark in                Text(bookmark.name)            }        }    }}

ForEach 遍历 bookmarks 数组,并为每个 BookmarkModel 对象创建一个 Text 视图。bookmark 参数是当前遍历的 BookmarkModel 对象。

BookmarkModel 必须遵循 Identifiable 协议,这样 SwiftUI 才能知道如何唯一地标识每个视图。在你的代码中,BookmarkModel 已经有一个 id 属性,所以你只需要让 BookmarkModel 遵循 Identifiable 协议即可:

final class BookmarkModel: Identifiable {    // your code here}

使用索引范围进行编号

你可以使用 ForEach 结构体的另一个版本,它接受一个范围作为其数据源。这个范围可以是一个索引范围,这样你就可以为每个项目编号。

例如,如果你有一个 BookmarkModel 的数组,并且你想为每个书签创建一个文本视图,并在前面添加一个编号,你可以这样做:

struct ContentView: View {    var bookmarks: [BookmarkModel]    var body: some View {        List {            ForEach(bookmarks.indices, id: \.self) { index in                Text("\(index + 1). \(bookmarks[index].name)")            }        }    }}

在这个例子中,ForEach 遍历 bookmarks 数组的索引,并为每个 BookmarkModel 对象创建一个 Text 视图。index 参数是当前遍历的索引。我们使用 \(index + 1). \(bookmarks[index].name) 来创建一个带有编号的文本视图。请注意,我们使用 index + 1 而不是 index,因为数组的索引是从 0 开始的,但我们通常希望编号是从 1 开始的。

使用 enumerated 编号

 enumerated() 

以下是一个例子:

struct ContentView: View {    var bookmarks: [BookmarkModel]    var body: some View {        List {            ForEach(Array(bookmarks.enumerated()), id: \.element.id) { index, bookmark in                Text("\(index). \(bookmark.name)")            }        }    }}

我们使用 Array(bookmarks.enumerated()) 来创建一个元组数组,每个元组包含一个索引和一个 BookmarkModel 对象。然后,我们使用 ForEach 遍历这个元组数组,并为每个元组创建一个 Text 视图。index 参数是当前遍历的索引,bookmark 参数是当前遍历的 BookmarkModel 对象。

使用 zip 编号

zip(_:_:) 函数可以将两个序列合并为一个元组序列。你可以使用这个函数和 ForEach 结构体来为数组中的每个元素添加一个编号。

例如,如果你有一个 BookmarkModel 的数组,并且你想为每个书签创建一个文本视图,并在前面添加一个编号,你可以这样做:

struct ContentView: View {    var bookmarks: [BookmarkModel]    var body: some View {        List {            ForEach(Array(zip(1..., bookmarks)), id: \.1.id) { index, bookmark in                Text("\(index). \(bookmark.name)")            }        }    }}

写出扩展,方便调用

@dynamicMemberLookupstruct Numbered<Element> {    var number: Int    var element: Element        subscript<T>(dynamicMember keyPath: WritableKeyPath<Element, T>) -> T {        get { element[keyPath: keyPath] }        set { element[keyPath: keyPath] = newValue }    }}extension Sequence {    func numbered(startingAt start: Int = 1) -> [Numbered<Element>] {        zip(start..., self)            .map { Numbered(number: $0.0, element: $0.1) }    }}extension Numbered: Identifiable where Element: Identifiable {    var id: Element.ID { element.id }}

使用:

ForEach(bookmark.numbered()) { numberedBookmark in    Text("\(numberedBookmark.number). \(numberedBookmark.name)")}

Scroll视图

ScrollView

新增 modifier

ScrollView {    ForEach(0..<300) { i in        Text("\(i)")            .id(i)    }}.scrollDisabled(false) // 设置是否可滚动.scrollDismissesKeyboard(.interactively) // 关闭键盘.scrollIndicators(.visible) // 设置滚动指示器是否可见

ScrollViewReader

ScrollView 使用 scrollTo 可以直接滚动到指定的位置。ScrollView 还可以透出偏移量,利用偏移量可以定义自己的动态视图,比如向下向上滚动视图时有不同效果,到顶部显示标题视图等。

示例代码如下:

struct PlayScrollView: View {    @State private var scrollOffset: CGFloat = .zero        var infoView: some View {        GeometryReader { g in            Text("移动了 \(Double(scrollOffset).formatted(.number.precision(.fractionLength(1)).rounded()))")                .padding()        }    }        var body: some View {        // 标准用法        ScrollViewReader { s in            ScrollView {                ForEach(0..<300) { i in                    Text("\(i)")                        .id(i)                }            }            Button("跳到150") {                withAnimation {                    s.scrollTo(150, anchor: .top)                }            } // end Button        } // end ScrollViewReader                // 自定义的 ScrollView 透出 offset 供使用        ZStack {            PCScrollView {                ForEach(0..<100) { i in                    Text("\(i)")                }            } whenMoved: { d in                scrollOffset = d            }            infoView                    } // end ZStack    } // end body}// MARK: - 自定义 ScrollViewstruct PCScrollView<C: View>: View {    let c: () -> C    let whenMoved: (CGFloat) -> Void        init(@ViewBuilder c: @escaping () -> C, whenMoved: @escaping (CGFloat) -> Void) {        self.c = c        self.whenMoved = whenMoved    }        var offsetReader: some View {        GeometryReader { g in            Color.clear                .preference(key: OffsetPreferenceKey.self, value: g.frame(in: .named("frameLayer")).minY)        }        .frame(height:0)    }        var body: some View {        ScrollView {            offsetReader            c()                .padding(.top, -8)        }        .coordinateSpace(name: "frameLayer")        .onPreferenceChange(OffsetPreferenceKey.self, perform: whenMoved)    } // end body}private struct OffsetPreferenceKey: PreferenceKey {  static var defaultValue: CGFloat = .zero  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}}

固定到滚动视图的顶部

LazyVStack 有个参数 pinnedViews 可以用于固定滚动视图的顶部。

ScrollView {    LazyVStack(alignment: .leading, spacing: 10, pinnedViews: .sectionHeaders) {        Section {            ForEach(books) { book in                BookRowView(book: book)            }        } header: {            HeaderView(title: "小说")        }        ....    }}

滚动到特定的位置

scrollPostion 版本

scrollPositon(id:) 比 ScrollViewReader 简单,但是只适用于 ScrollView。数据源遵循 Identifiable,不用显式使用 id 修饰符

struct ContentView: View {    @State private var id: Int?    var body: some View {        VStack {            Button("Scroll to Bookmark 3") {                withAnimation {                    id = 3                }            }            Button("Scroll to Bookmark 13") {                withAnimation {                    id = 13                }            }            ScrollView {                ScrollViewReader { scrollView in                    LazyVStack {                        ForEach(Bookmark.simpleData()) { bookmark in                            Text("\(bookmark.index)")                                .id(bookmark.index)                        }                                            }                }            }            .scrollPosition(id: $id)            .scrollTargetLayout()        }    }        struct Bookmark: Identifiable,Hashable {        let id = UUID()        let index: Int                static func simpleData() -> [Bookmark] {            var re = [Bookmark]()            for i in 0...100 {                re.append(Bookmark(index: i))            }            return re        }    }}

scrollTargetLayout 可以获得当前滚动位置。锚点不可配,默认是 center。

ScrollViewReader 版本

ScrollViewReader 这个版本可以适用于 List,也可以配置锚点

你可以使用 ScrollViewReaderscrollTo(_:anchor:) 方法来滚动到特定的元素。以下是一个例子:

struct ContentView: View {    var bookmarks: [Int] = Array(1...100)    @State private var selectedBookmarkId: Int?    var body: some View {        VStack {            Button("Scroll to Bookmark 3") {                selectedBookmarkId = 3            }            Button("Scroll to Bookmark 13") {                selectedBookmarkId = 13            }            ScrollView {                ScrollViewReader { scrollView in                    LazyVStack {                        ForEach(bookmarks.indices, id: \.self) { index in                            Text("\(bookmarks[index])")                                .id(index)                        }                        .onChange(of: selectedBookmarkId) { oldValue, newValue in                            if let newValue = newValue {                                withAnimation {                                    scrollView.scrollTo(newValue, anchor: .top)                                }                            }                        }                    }                }            }        }    }}

在这个例子中,我们首先创建了一个 Button,当点击这个按钮时,selectedBookmarkId 的值会被设置为 3。然后,我们创建了一个 ScrollView,并在 ScrollView 中添加了一个 ScrollViewReader。我们在 ScrollViewReader 中添加了一个 LazyVStack,并使用 ForEach 遍历 bookmarks 数组的索引,为每个索引创建一个 Text 视图。我们使用 id(_:) 方法为每个 Text 视图设置了一个唯一的 ID。

我们使用 onChange(of:perform:) 方法来监听 selectedBookmarkId 的变化。当 selectedBookmarkId 的值改变时,我们会调用 scrollTo(_:anchor:) 方法来滚动到特定的元素。anchor: .top 参数表示我们希望滚动到的元素位于滚动视图的顶部。

scrollTargetBehavior分页滚动

按可视尺寸分页

.scrollTargetBehavior(.paging) 可以让 ScrollView 滚动,滚动一页的范围是 ScrollView 的可视尺寸。 

struct ContentView: View {    var body: some View {        ScrollView(.horizontal) {            LazyHStack {                ForEach(0...20, id: \.self) { i in                    colorView()                        .frame(width: 300, height: 200)                }            }        }        .scrollTargetBehavior(.paging)    }        @ViewBuilder    func colorView() -> some View {        [Color.red, Color.yellow, Color.blue, Color.mint, Color.indigo, Color.green].randomElement()    }}

按容器元素对齐分页

使用 .scrollTargetBehavior(.viewAligned) 配合 scrollTargetLayout。示例代码如下:

struct ContentView: View {    var body: some View {        ScrollView(.horizontal) {            LazyHStack {                ForEach(0...20, id: \.self) { i in                    colorView()                        .frame(width: 300, height: 200)                }            }            .scrollTargetLayout(isEnabled: true)        }        .scrollTargetBehavior(.viewAligned)    }        @ViewBuilder    func colorView() -> some View {        [Color.red, Color.yellow, Color.blue, Color.mint, Color.indigo, Color.green].randomElement()    }}

scrollTransition视觉效果

iOS 17 新推出 .scrollTransition,用于处理滚动时的动画。

.transition 用于视图插入和移除视图树时的动画。

.scrollTransition 会和滚动联合起来进行平滑的过渡动画处理。.scrollTransition 可以修改很多属性,比如大小,可见性还有旋转等。

.scrollTransition 可以针对不同阶段进行处理,目前有三个阶段:

  • topLeading: 视图进入 ScrollView 可见区域
  • identity: 在可见区域中
  • bottomTrailing: 视图离开 ScrollView 可见区域
struct ContentView: View {    var body: some View {        ScrollView(.horizontal) {            LazyHStack {                ForEach(0...20, id: \.self) { i in                    colorView()                        .frame(width: 300, height: 200)                        .scrollTransition { content, phase in                             content                                .scaleEffect(phase.isIdentity ? 1 : 0.4)                        }                }            }        }    }        @ViewBuilder    func colorView() -> some View {        [Color.red, Color.yellow, Color.blue, Color.mint, Color.indigo, Color.green].randomElement()    }}

使用阶段的值

.scrollTransition(.animated(.bouncy)) { content, phase in    content        .scaleEffect(phase.isIdentity ? 1 : phase.value)}

不同阶段的产生效果设置

.scrollTransition(    topLeading: .animated,    bottomTrailing: .interactive) { content, phase in    content.rotationEffect(.radians(phase.value))}

.rotation3DEffect 也是支持的。

.scrollTransition(.interactive) { content, phase in    content        .rotation3DEffect(            Angle.degrees(phase.isIdentity ? 0: 120),            axis: (x: 0.9, y: 0.0, z: 0.1))        .offset(x: phase.value * -300)}

ScrollView-参考资料

文档

WWDC

23

List列表

List

List 除了能够展示数据外,还有下拉刷新、过滤搜索和侧滑 Swipe 动作提供更多 Cell 操作的能力。

通过 List 的可选子项参数提供数据模型的关键路径来制定子项路劲,还可以实现大纲视图,使用 DisclosureGroup 和 OutlineGroup 可以进一步定制大纲视图。

使用 .listRowSeparator(.hidden, edges: .all) 可以隐藏分割线。

下面是 List 使用,包括了 DisclosureGroup 和 OutlineGroup 的演示代码:

struct PlayListView: View {    @StateObject var l: PLVM = PLVM()    @State private var s: String = ""        var outlineModel = [        POutlineModel(title: "文件夹一", iconName: "folder.fill", children: [            POutlineModel(title: "个人", iconName: "person.crop.circle.fill"),            POutlineModel(title: "群组", iconName: "person.2.circle.fill"),            POutlineModel(title: "加好友", iconName: "person.badge.plus")        ]),        POutlineModel(title: "文件夹二", iconName: "folder.fill", children: [            POutlineModel(title: "晴天", iconName: "sun.max.fill"),            POutlineModel(title: "夜间", iconName: "moon.fill"),            POutlineModel(title: "雨天", iconName: "cloud.rain.fill", children: [                POutlineModel(title: "雷加雨", iconName: "cloud.bolt.rain.fill"),                POutlineModel(title: "太阳雨", iconName: "cloud.sun.rain.fill")            ])        ]),        POutlineModel(title: "文件夹三", iconName: "folder.fill", children: [            POutlineModel(title: "电话", iconName: "phone"),            POutlineModel(title: "拍照", iconName: "camera.circle.fill"),            POutlineModel(title: "提醒", iconName: "bell")        ])    ]        var body: some View {        HStack {            // List 通过$语法可以将集合的元素转换成可绑定的值            List {                ForEach($l.ls) { $d in                    PRowView(s: d.s, i: d.i)                        .listRowInsets(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))                        .listRowBackground(Color.black.opacity(0.2))                }            }            .refreshable {                // 下拉刷新            }            .searchable(text: $s) // 搜索            .onChange(of: s) { newValue in                print("搜索关键字:\(s)")            }                        Divider()                        // 自定义 List            VStack {                PCustomListView($l.ls) { $d in                    PRowView(s: d.s, i: d.i)                }                // 添加数据                Button {                    l.ls.append(PLModel(s: "More", i: 0))                } label: {                    Text("添加")                }            }            .padding()                        Divider()                        // 使用大纲            List(outlineModel, children: \.children) { i in                Label(i.title, systemImage: i.iconName)            }                        Divider()                        // 自定义大纲视图            VStack {                Text("可点击标题展开")                    .font(.headline)                PCOutlineListView(d: outlineModel, c: \.children) { i in                    Label(i.title, systemImage: i.iconName)                }            }            .padding()                        Divider()                        // 使用 OutlineGroup 实现大纲视图            VStack {                Text("OutlineGroup 实现大纲")                                OutlineGroup(outlineModel, children: \.children) { i in                    Label(i.title, systemImage: i.iconName)                }                                // OutlineGroup 和 List 结合                Text("OutlineGroup 和 List 结合")                List {                    ForEach(outlineModel) { s in                        Section {                            OutlineGroup(s.children ?? [], children: \.children) { i in                                Label(i.title, systemImage: i.iconName)                            }                        } header: {                            Label(s.title, systemImage: s.iconName)                        }                    } // end ForEach                } // end List            } // end VStack        } // end HStack    } // end body}// MARK: - 自定义大纲视图struct PCOutlineListView<D, Content>: View where D: RandomAccessCollection, D.Element: Identifiable, Content: View {    private let v: PCOutlineView<D, Content>        init(d: D, c: KeyPath<D.Element, D?>, content: @escaping (D.Element) -> Content) {        self.v = PCOutlineView(d: d, c: c, content: content)    }        var body: some View {        List {            v        }    }}struct PCOutlineView<D, Content>: View where D: RandomAccessCollection, D.Element: Identifiable, Content: View {    let d: D    let c: KeyPath<D.Element, D?>    let content: (D.Element) -> Content    @State var isExpanded = true // 控制初始是否展开的状态        var body: some View {        ForEach(d) { i in            if let sub = i[keyPath: c] {                PCDisclosureGroup(content: PCOutlineView(d: sub, c: c, content: content), label: content(i))            } else {                content(i)            } // end if        } // end ForEach    } // end body}struct PCDisclosureGroup<C, L>: View where C: View, L: View {    @State var isExpanded = false    var content: C    var label: L    var body: some View {        DisclosureGroup(isExpanded: $isExpanded) {            content        } label: {            Button {                isExpanded.toggle()            } label: {                label            }            .buttonStyle(.plain)        }    }}// MARK: - 大纲模式数据模型struct POutlineModel: Hashable, Identifiable {    var id = UUID()    var title: String    var iconName: String    var children: [POutlineModel]?}// MARK: - List 的抽象,数据兼容任何集合类型struct PCustomListView<D: RandomAccessCollection & MutableCollection & RangeReplaceableCollection, Content: View>: View where D.Element: Identifiable {    @Binding var data: D    var content: (Binding<D.Element>) -> Content        init(_ data: Binding<D>, content: @escaping (Binding<D.Element>) -> Content) {        self._data = data        self.content = content    }        var body: some View {        List {            Section {                ForEach($data, content: content)                    .onMove { indexSet, offset in                        data.move(fromOffsets: indexSet, toOffset: offset)                    }                    .onDelete { indexSet in                        data.remove(atOffsets: indexSet) // macOS 暂不支持                    }            } header: {                Text("第一栏,共 \(data.count) 项")            } footer: {                Text("The End")            }        }        .listStyle(.plain) // 有.automatic、.inset、.plain、sidebar,macOS 暂不支持的有.grouped 和 .insetGrouped    }}// MARK: - Cell 视图struct PRowView: View {    var s: String    var i: Int    var body: some View {        HStack {            Text("\(i):")            Text(s)        }    }}// MARK: - 数据模型设计struct PLModel: Hashable, Identifiable {    let id = UUID()    var s: String    var i: Int}final class PLVM: ObservableObject {    @Published var ls: [PLModel]    init() {        ls = [PLModel]()        for i in 0...20 {            ls.append(PLModel(s: "\(i)", i: i))        }    }}

list 支持 Section footer。

list 分隔符可以自定义,使用 HorizontalEdge.leadingHorizontalEdge.trailing

list 不使用 UITableView 了。

今年 list 还新增了一个 EditOperation 可以自动生成移动和删除,新增了 edits 参数,传入 [.delete, .move] 数组即可。这也是一个演示如何更好扩展和配置功能的方式。

.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)            }    }}

List-设置样式

内置样式

通过 .listStyle 修饰符可以用系统内置样式更改 List 外观。

List {   ...}.listStyle(.sidebar)

不同平台有不同的选项

ListStyle iOS macOS watchOS tvOS
plain iOS 13+ macOS 10.15+ watchOS 6+ tvOS 13+
sidebar iOS 14+ macOS 10.15+ - -
inset iOS 13+ macOS 11.15+ - -
grouped iOS 13+ - - tvOS 13+
insetGrouped iOS 14+ - - -
bordered - macOS 12+ - -
carousel - - watchOS 6+ -
elliptical - - watchOS 7+ -

行高

List {  ...}.environment(\.defaultMinListRowHeight, 100).environment(\.defaultMinListHeaderHeight, 50)

分隔符

listSectionSeparator 和 listRowSeparator 隐藏行和 Section 分隔符。

listRowSeparatorTint 和 listSectionSeparatorTint 更改分隔符颜色

例如:

.listRowSeparatorTint(.cyan, edges: .bottom)

背景

.alternatingRowBackgrounds() 可以让 List 的行底色有区分。

listRowBackground 调整行的背景颜色

更改背景颜色前需要隐藏内容背景

List {  ...}.scrollContentBackground(.hidden).background(Color.cyan)

这个方法同样可用于 ScrollView 和 TextEditor。

你可以使用 .listRowBackground() 修饰符来更改列表行的背景。以下是一个例子:

struct ContentView: View {    var body: some View {        List {            ForEach(0..<5) { index in                Text("Row \(index)")                    .listRowBackground(index % 2 == 0 ? Color.blue : Color.green)            }        }    }}

在这个例子中,我们创建了一个包含五个元素的 List。我们使用 .listRowBackground() 修饰符来更改每个元素的背景颜色。如果元素的索引是偶数,我们将背景颜色设置为蓝色,否则我们将背景颜色设置为绿色。

Section

你可以使用 Section 视图的 headerfooter 参数来添加头部和尾部。以下是一个例子:

struct ContentView: View {    var body: some View {        List {            Section {                ForEach(0..<5) { index in                    Text("Row \(index)")                }            } header: {                Text("Header").font(.title)            } footer: {                Text("Footer").font(.caption)            }        }    }}

headerProminence(.increase) 可以增加 Section Header 的大小。

safeAreaInset

你可以使用 .safeAreaInset() 修饰符来调整视图的安全区域插入。以下是一个例子:

struct ContentView: View {    var body: some View {        List {            ForEach(0..<5) { index in                Text("Row \(index)")            }        }        .safeAreaInset(edge: .top, spacing: 20) {            Text("Header")                .frame(maxWidth: .infinity, alignment: .center)                .background(Color.blue)                .foregroundColor(.white)        }    }}

在这个例子中,我们创建了一个包含五个元素的 List。然后我们使用 .safeAreaInset() 修饰符来在 List 的顶部添加一个 Header。我们将 edge 参数设置为 .top,将 spacing 参数设置为 20,然后提供一个视图作为 Header。这个 Header 是一个文本视图,它的背景颜色是蓝色,前景颜色是白色,它被居中对齐,并且它的宽度和 List 的宽度相同。

List-移动元素

你可以使用 .onMove(perform:) 修饰符来允许用户移动 List 中的元素。以下是一个例子:

struct ContentView: View {    @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]    var body: some View {        NavigationView {            List {                ForEach(items, id: \.self) { item in                    Text(item)                }                .onMove(perform: move)            }            .toolbar {                EditButton()            }        }    }    private func move(from source: IndexSet, to destination: Int) {        items.move(fromOffsets: source, toOffset: destination)    }}

在这个例子中,我们创建了一个包含五个元素的 List。我们使用 .onMove(perform:) 修饰符来允许用户移动这些元素,并提供了一个 move(from:to:) 方法来处理移动操作。我们还添加了一个 EditButton,用户可以点击它来进入编辑模式,然后就可以移动元素了。

List-搜索

搜索和搜索建议

你可以使用 .searchable() 修饰符的 suggestions 参数来提供搜索建议。以下是一个例子:

struct ContentView: View {    @State private var searchText = ""    @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]    var body: some View {        NavigationView {            List {                ForEach(items.filter({ searchText.isEmpty ? true : $0.contains(searchText) }), id: \.self) { item in                    Text(item)                }            }            .searchable(text: $searchText, suggestions: {                 Button(action: {                    searchText = "Item 1"                }) {                    Text("Item 1")                }                Button(action: {                    searchText = "Item 2"                }) {                    Text("Item 2")                }            })            .navigationBarTitle("Items")        }    }}

在这个例子中,我们创建了一个包含五个元素的 List,并添加了一个搜索框。当用户在搜索框中输入文本时,List 会自动更新以显示匹配的元素。同时,我们提供了两个搜索建议 “Item 1” 和 “Item 2”,用户可以点击这些建议来快速填充搜索框。

在列表中显示搜索建议

struct ContentView: View {    @Environment(\.searchSuggestionsPlacement) var placement    @State private var searchText = ""    @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]        var body: some View {        NavigationView {            List {                SearchSuggestionView()                ForEach(items.filter({ searchText.isEmpty ? true : $0.contains(searchText) }), id: \.self) { item in                    Text(item)                }            }            .searchable(text: $searchText, suggestions: {                VStack {                    Button(action: {                        searchText = "Item 1"                    }) {                        Text("Item 1")                    }                    Button(action: {                        searchText = "Item 2"                    }) {                        Text("Item 2")                    }                }                .searchSuggestions(.hidden, for: .content)            })            .navigationBarTitle("Items")        }    }        @ViewBuilder    func SearchSuggestionView() -> some View {        if placement == .content {            Button(action: {                searchText = "Item 1"            }) {                Text("Item 1")            }            Button(action: {                searchText = "Item 2"            }) {                Text("Item 2")            }        }    }}

搜索状态

搜索中

@Environment(\.isSearching) var isSearching

关闭搜索

@Environment(\.dismissSearch) var dismissSearch

提交搜索

List {    ...}.searchable(text: $vm.searchTerm).onSubmit(of: .search) {    //...}

搜索栏外观

占位文字说明

.searchable(text: $wwdcVM.searchText, prompt: "搜索 WWDC Session 内容")

一直显示搜索栏

.searchable(text: $wwdcVM.searchText,             placement: .navigationBarDrawer(displayMode:.always))

更改搜索栏的位置

.searchable(text: $wwdcVM.searchText, placement: .sidebar)

搜索去抖动

你可以使用 Combine 框架来实现搜索的去抖动功能。以下是一个例子:

import SwiftUIimport Combineclass SearchViewModel: ObservableObject {    @Published var searchText = ""    @Published var searchResults: [String] = []    private var cancellables = Set<AnyCancellable>()    init() {        $searchText            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)            .sink { [weak self] in self?.search($0) }            .store(in: &cancellables)    }    private func search(_ text: String) {        // 这里是你的搜索逻辑        // 例如,你可以从一个数组中过滤出匹配的元素        let items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]        searchResults = items.filter { $0.contains(text) }    }}struct ContentView: View {    @StateObject private var viewModel = SearchViewModel()    var body: some View {        VStack {            TextField("Search", text: $viewModel.searchText)                .padding()            List(viewModel.searchResults, id: \.self) { result in                Text(result)            }        }    }}

在这个例子中,我们创建了一个 SearchViewModel 类,它有一个 searchText 属性和一个 searchResults 属性。当 searchText 属性的值发生变化时,我们使用 Combine 的 debounce(for:scheduler:) 方法来延迟执行搜索操作,从而实现去抖动功能。然后我们在 ContentView 中使用这个 SearchViewModel 来显示搜索框和搜索结果。

List-下拉刷新

你可以使用 .refreshable() 修饰符来添加下拉刷新功能。以下是一个例子:

struct ContentView: View {    @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]    var body: some View {        List {            ForEach(items, id: \.self) { item in                Text(item)            }        }        .refreshable {            await refresh()        }    }    func refresh() async {        // 这里是你的刷新逻辑        // 例如,你可以从网络获取新的数据,然后更新 items 数组        // 这里我们只是简单地将 items 数组反转        items.reverse()    }}

在这个例子中,我们创建了一个包含五个元素的 List,并添加了下拉刷新功能。当用户下拉 List 时,refresh() 方法会被调用,然后我们将 items 数组反转,从而模拟刷新操作。注意,refresh() 方法需要是一个异步方法,因为刷新操作通常需要一些时间来完成。

List-轻扫操作

你可以使用 .swipeActions() 修饰符来添加轻扫操作。以下是一个例子:

struct ContentView: View {    @State private var items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]    var body: some View {        List {            ForEach(items, id: \.self) { item in                Text(item)                .swipeActions {                    Button(action: {                        // 这里是你的删除操作                        if let index = items.firstIndex(of: item) {                            items.remove(at: index)                        }                    }) {                        Label("Delete", systemImage: "trash")                    }                    .tint(.red)                }            }        }    }}

在这个例子中,我们创建了一个包含五个元素的 List,并为每个元素添加了一个滑动操作。当用户向左轻扫一个元素时,会显示一个 “Delete” 按钮,用户可以点击这个按钮来删除该元素。

List-大纲视图

List 树状结构

通过 children 参数指定子树路径。

List(outlineModel, children: \.children) { i in    Label(i.title, systemImage: i.iconName)}

DisclosureGroup 实现展开和折叠

DisclosureGroup 视图可以用来创建一个可以展开和折叠的内容区域。以下是一个例子:

struct ContentView: View {    @State private var isExpanded = false    var body: some View {        DisclosureGroup("Options", isExpanded: $isExpanded) {            Text("Option 1")            Text("Option 2")            Text("Option 3")        }    }}

在这个例子中,我们创建了一个 DisclosureGroup 视图,它的标题是 “Options”,并且它包含三个选项。我们使用一个 @State 属性 isExpanded 来控制 DisclosureGroup 视图是否展开。当用户点击标题时,DisclosureGroup 视图会自动展开或折叠,同时 isExpanded 属性的值也会相应地改变。

OutlineGroup 创建大纲视图

可以使用 OutlineGroup 视图来创建一个大纲视图。以下是一个例子:

struct ContentView: View {    var body: some View {        List {            OutlineGroup(sampleData, id: \.self) { item in                Text(item.name)            }        }    }}struct Item: Identifiable {    var id = UUID()    var name: String    var children: [Item]?}let sampleData: [Item] = [    Item(name: "Parent 1", children: [        Item(name: "Child 1"),        Item(name: "Child 2")    ]),    Item(name: "Parent 2", children: [        Item(name: "Child 3"),        Item(name: "Child 4")    ])]

在这个例子中,我们创建了一个 Item 结构体,它有一个 name 属性和一个 children 属性。然后我们创建了一个 sampleData 数组,它包含两个父项,每个父项都有两个子项。最后我们在 ContentView 中使用 OutlineGroup 视图来显示这个数组,每个父项和子项都显示为一个文本视图。

结合 OutlineGroup 和 DisclosureGroup 实现自定义可折叠大纲视图

代码如下:

struct SPOutlineListView<D, Content>: View where D: RandomAccessCollection, D.Element: Identifiable, Content: View {    private let v: SPOutlineView<D, Content>        init(d: D, c: KeyPath<D.Element, D?>, content: @escaping (D.Element) -> Content) {        self.v = SPOutlineView(d: d, c: c, content: content)    }        var body: some View {        List {            v        }    }}struct SPOutlineView<D, Content>: View where D: RandomAccessCollection, D.Element: Identifiable, Content: View {    let d: D    let c: KeyPath<D.Element, D?>    let content: (D.Element) -> Content    @State var isExpanded = true // 控制初始是否展开的状态        var body: some View {        ForEach(d) { i in            if let sub = i[keyPath: c] {                SPDisclosureGroup(content: SPOutlineView(d: sub, c: c, content: content), label: content(i))            } else {                content(i)            } // end if        } // end ForEach    } // end body}struct SPDisclosureGroup<C, L>: View where C: View, L: View {    @State var isExpanded = false    var content: C    var label: L    var body: some View {        DisclosureGroup(isExpanded: $isExpanded) {            content        } label: {            Button {                withAnimation {                    isExpanded.toggle()                }            } label: {                label            }            .buttonStyle(.plain)        }            }}

List-完全可点击的行

使用 .contentShape(Rectangle()) 可以使整个区域都可点击

struct ContentView: View {    var body: some View {        List {            ForEach(1..<50) { num in                HStack {                    Text("\(num)")                    Spacer()                }                .contentShape(Rectangle())                .onTapGesture {                    print("Clicked \(num)")                }            }        } // end list    }}

List-索引标题

这个代码是在创建一个带有索引标题的列表,用户可以通过拖动索引标题来快速滚动列表。

import SwiftUI...struct ContentView: View {  ...  var body: some View {    ScrollViewReader { proxy in      List {        ArticleListView      }      .listStyle(InsetGroupedListStyle())      .overlay(IndexView(proxy: proxy))    }  }  ...}struct IndexView: View {  let proxy: ScrollViewProxy  let titles: [String]  @GestureState private var dragLocation: CGPoint = .zero  var body: some View {    VStack {      ForEach(titles, id: \.self) { title in        TitleView()          .background(drag(title: title))      }    }    .gesture(      DragGesture(minimumDistance: 0, coordinateSpace: .global)        .updating($dragLocation) { value, state, _ in          state = value.location        }    )  }  func drag(title: String) -> some View {    GeometryReader { geometry in      drag(geometry: geometry, title: title)    }  }  func drag(geometry: GeometryProxy, title: String) -> some View {    if geometry.frame(in: .global).contains(dragLocation) {      DispatchQueue.main.async {        proxy.scrollTo(title, anchor: .center)      }    }    return Rectangle().fill(Color.clear)  }  ...}...

上面代码中 ContentView 是主视图,它包含一个 List 和一个 IndexViewList 中的内容由 ArticleListView 提供。IndexView 是一个自定义视图,它显示了所有的索引标题。

IndexView 接受一个 ScrollViewProxy 和一个标题数组。它使用 VStackForEach 来创建一个垂直的索引标题列表。每个标题都是一个 TitleView,并且它有一个背景,这个背景是通过 drag(title:) 方法创建的。

drag(title:) 方法接受一个标题,并返回一个视图。这个视图是一个 GeometryReader,它可以获取其包含的视图的几何信息。然后,这个 GeometryReader 使用 drag(geometry:title:) 方法来创建一个新的视图。

drag(geometry:title:) 方法接受一个 GeometryProxy 和一个标题,并返回一个视图。如果 GeometryProxy 的全局帧包含当前的拖动位置,那么这个方法将返回一个特定的视图。

IndexView 还有一个手势,这个手势是一个 DragGesture。当用户拖动索引标题时,这个手势会更新 dragLocation 属性的值,这个属性是一个 @GestureState 属性,它表示当前的拖动位置。

List-加载更多

你可以通过检测列表滚动到底部来实现加载更多的功能。以下是一个简单的例子:

struct ContentView: View {    @State private var items = Array(0..<20)    var body: some View {        List {            ForEach(items, id: \.self) { item in                Text("Item \(item)")                    .onAppear {                        if item == items.last {                            loadMore()                        }                    }            }        }        .onAppear(perform: loadMore)    }    func loadMore() {        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {            let newItems = Array(self.items.count..<self.items.count + 20)            self.items.append(contentsOf: newItems)        }    }}

在这个例子中,我们创建了一个包含多个元素的 List。当 List 出现最后一项时,我们调用 loadMore 方法来加载更多的元素。在 loadMore 方法中,模拟在一秒后添加新的元素到 items 数组中。

请注意,这只是一个基本的使用示例,实际的使用方式可能会根据你的需求而变化。例如,你可能需要从网络获取新的元素,而不是像这个例子中那样直接创建新的元素。

Lazy容器

LazyVStack和LazyHStack

LazyVStack 和 LazyHStack 里的视图只有在滚到时才会被创建。

struct PlayLazyVStackAndLazyHStackView: View {    var body: some View {        ScrollView {            LazyVStack {                ForEach(1...300, id: \.self) { i in                    PLHSRowView(i: i)                }            }        }    }}struct PLHSRowView: View {    let i: Int    var body: some View {        Text("第 \(i) 个")    }    init(i: Int) {        print("第 \(i) 个初始化了") // 用来查看什么时候创建的。        self.i = i    }}

LazyVGrid和LazyHGrid

列的设置有三种,这三种也可以组合用。

  • GridItem(.fixed(10)) 会固定设置有多少列。
  • GridItem(.flexible()) 会充满没有使用的空间。
  • GridItem(.adaptive(minimum: 10)) 表示会根据设置大小自动设置有多少列展示。

示例:

struct PlayLazyVGridAndLazyHGridView: View {    @State private var colors: [String:Color] = [        "red" : .red,        "orange" : .orange,        "yellow" : .yellow,        "green" : .green,        "mint" : .mint,        "teal" : .teal,        "cyan" : .cyan,        "blue" : .blue,        "indigo" : .indigo,        "purple" : .purple,        "pink" : .pink,        "brown" : .brown,        "gray" : .gray,        "black" : .black    ]        var body: some View {        ScrollView {            LazyVGrid(columns: [                GridItem(.adaptive(minimum: 50), spacing: 10)            ], pinnedViews: [.sectionHeaders]) {                Section(header:                            Text("🎨调色板")                            .font(.title)                            .frame(maxWidth: .infinity, maxHeight: .infinity)                                .background(RoundedRectangle(cornerRadius: 0)                                                .fill(.black.opacity(0.1)))                ) {                    ForEach(Array(colors.keys), id: \.self) { k in                        colors[k].frame(height:Double(Int.random(in: 50...150)))                            .overlay(                                Text(k)                            )                            .shadow(color: .black, radius: 2, x: 0, y: 2)                    }                }            }            .padding()                        LazyVGrid(columns: [                GridItem(.adaptive(minimum: 20), spacing: 10)            ]) {                Section(header: Text("图标集").font(.title)) {                    ForEach(1...30, id: \.self) { i in                        Image("p\(i)")                            .resizable()                            .aspectRatio(contentMode: .fit)                            .shadow(color: .black, radius: 2, x: 0, y: 2)                    }                }            }            .padding()        }    }}

Grid

Grid 会将最大的一个单元格大小应用于所有单元格

代码例子:

struct ContentView: View {    var body: some View {        Grid(alignment: .center,             horizontalSpacing: 30,             verticalSpacing: 8) {            GridRow {                Text("Tropical")                Text("Mango")                Text("Pineapple")                    .gridCellColumns(2)            }            GridRow(alignment: .bottom) {                Text("Leafy")                Text("Spinach")                Text("Kale")                Text("Lettuce")            }        }    }}

gridCellAnchor 可以让 GridRow 给自己设置对齐方式。

gridCellColumns() modifier 可以让一个单元格跨多列。

GridRow 的间距通过 Grid 的 horizontalSpacingverticalSpacing 参数来控制。

struct ContentView: View {    let numbers: [[Int]] = [        [1, 2, 3],        [4, 5, 6],        [7, 8, 9]    ]    var body: some View {        Grid(horizontalSpacing: 0, verticalSpacing: 0) {            ForEach(numbers.indices, id: \.self) { i in                GridRow {                    ForEach(numbers[i].indices, id: \.self) { j in                        Text("\(numbers[i][j])")                            .frame(maxWidth: .infinity, maxHeight: .infinity)                            .background(Color.gray.opacity(0.2))                            .border(Color.gray, width: 0.5)                    }                }            }        }    }}

按照以上代码这样写,每个数字 GridRow 之间的间隔就是0了。

空白的单元格可以这样写:

Color.clear    .gridCellUnsizedAxes([.horizontal, .vertical])

Table表格

Table

今年 iOS 和 iPadOS 也可以使用去年只能在 macOS 上使用的 Table了,据 digital lounges 里说,iOS table 的性能和 list 差不多,table 默认为 plian list。我想 iOS 上加上 table 只是为了兼容 macOS 代码吧。

table 使用示例如下:

struct ContentView: View {    var body: some View {        Table(Fruit.simpleData()) {            TableColumn("名字", value: \.name)            TableColumn("颜色", value: \.color)            TableColumn("颜色") {                Text("\($0.name)")                    .font(.footnote)                    .foregroundStyle(.cyan)            }        }        .contextMenu(forSelectionType: Fruit.ID.self) { selection in            if selection.isEmpty {                Button("添加") {                    // ...                }            } else if selection.count == 1 {                Button("收藏") {                    // ...                }            } else {                Button("收藏多个") {                    // ...                }            }        }    }        struct Fruit:Identifiable {        let id = UUID()        let name: String        let color: String                static func simpleData() -> [Fruit] {            var re = [Fruit]()            re.append(Fruit(name: "Apple", color: "Red"))            re.append(Fruit(name: "Banana", color: "Yellow"))            re.append(Fruit(name: "Cherry", color: "Red"))            re.append(Fruit(name: "Date", color: "Brown"))            re.append(Fruit(name: "Elderberry", color: "Purple"))            return re        }    }}

Table-样式

在 SwiftUI 中,Table 视图的 .tableStyle 修改器可以用来设置表格的样式。目前,SwiftUI 提供了以下几种表格样式:

  • inset:默认
  • inset(alternatesRowBackgrounds: Bool):是否开启行交错背景
  • bordered:加边框
  • bordered(alternatesRowBackgrounds: Bool): 是否开启行交错背景

你可以使用 .tableStyle 修改器来设置表格的样式,例如:

Table(data) {    // ...}.tableStyle(InsetGroupedListStyle())

这段代码会将表格的样式设置为 InsetGroupedListStyle

Table-行的选择

你可以使用 Table 视图的 selection 参数来实现单选和多选。selection 参数接受一个绑定到一个可选的 Set 的变量,这个 Set 包含了被选中的元素的标识。

以下是一个使用 Table 视图实现单选和多选的例子:

struct ContentView: View {    @State private var selectionOne: UUID? // 单选    @State private var selection: Set<UUID> = [] // 多选    let data = [        Fruit(name: "Apple", color: "Red"),        Fruit(name: "Banana", color: "Yellow"),        Fruit(name: "Cherry", color: "Red"),        Fruit(name: "Date", color: "Brown"),        Fruit(name: "Elderberry", color: "Purple")    ]    var body: some View {        Table(data, selection: $selectionOne) {            TableColumn("Fruit") { item in                Text(item.name)            }            TableColumn("Color") { item in                Text(item.color)            }        }    }}struct Fruit: Identifiable {    let id = UUID()    let name: String    let color: String}

在这个例子中,我们首先定义了一个 @State 变量 selection,它是一个 Set,包含了被选中的元素的标识。然后,我们将这个变量绑定到 Table 视图的 selection 参数。

现在,当用户选择或取消选择一个元素时,selection 变量就会被更新。你可以使用这个变量来判断哪些元素被选中,或者实现其他的交互功能。

Table-多属性排序

你可以使用 Table 视图的 sortOrder 参数来实现多属性排序。sortOrder 参数接受一个绑定到一个 SortDescriptor 数组的变量,这个数组定义了排序的顺序和方式。

以下是一个使用 Table 视图实现多属性排序的例子:

struct ContentView: View {    @State private var sortOrder: [KeyPathComparator<Fruit>] = [.init(\.name, order: .reverse)]    @State var data = [        Fruit(name: "Apple", color: "Red"),        Fruit(name: "Banana", color: "Yellow"),        Fruit(name: "Cherry", color: "Red"),        Fruit(name: "Date", color: "Brown"),        Fruit(name: "Elderberry", color: "Purple")    ]    var body: some View {        sortKeyPathView() // 排序状态        Table(data, sortOrder: $sortOrder) {            TableColumn("Fruit", value: \.name)            TableColumn("Color", value: \.color)            // 不含 value 参数的不支持排序            TableColumn("ColorNoOrder") {                Text("\($0.color)")                    .font(.footnote)                    .foregroundStyle(.mint)            }        }        .task {            data.sort(using: sortOrder)        }        .onChange(of: sortOrder) { oldValue, newValue in            data.sort(using: newValue)        }        .padding()    }        @ViewBuilder    func sortKeyPathView() -> some View {        HStack {            ForEach(sortOrder, id: \.self) { order in                Text(order.keyPath == \Fruit.name ? "名字" : "颜色")                Image(systemName: order.order == .reverse ? "chevron.down" : "chevron.up")            }        }        .padding(.top)    }}struct Fruit: Identifiable {    let id = UUID()    let name: String    let color: String}

在这个例子中,我们首先定义了一个 @State 变量 sortOrder,它是一个 SortDescriptor 数组,定义了排序的顺序和方式。然后,我们将这个变量绑定到 Table 视图的 sortOrder 参数。

现在,当用户点击表头来排序一个列时,sortOrder 变量就会被更新。你可以使用这个变量来实现多属性排序,或者实现其他的交互功能。

Table-contextMenu

struct ContentView: View {    @State private var selection: Set<UUID> = []    var body: some View {        Table(Fruit.simpleData(), selection: $selection) {            ...        }        .contextMenu(forSelectionType: Fruit.ID.self) { selection in            if selection.isEmpty {                Button("添加") {                    // ...                }            } else if selection.count == 1 {                Button("收藏") {                    // ...                }            } else {                Button("收藏多个") {                    // ...                }            }        } primaryAction: { items in            // 双击某一行时            debugPrint(items)        }    }    ...}

小册子之简说 Widget 小组件

以下内容已整理到小册子中,小册子代码在 Github 上,本文会随着系统更新和我更多的实践而新增和更新,你可以购买“戴铭的开发小册子”应用(98元),来跟踪查看本文内容新增和更新。

本文属于小册子系列中的一篇,已发布系列文章有:

Widge 允许开发者在用户的主屏幕或通知中心展示应用的信息。Widget 可以提供快速的信息预览,或者提供快速访问应用的方式。

开发 Widget 的基本步骤如下:

  1. 创建 Widget Extension:在 Xcode 中,你需要创建一个新的 Widget Extension。这将会生成一个新的 target,包含了创建 Widget 所需的基本代码。

  2. 定义 Timeline Entry:Timeline Entry 是 Widget 数据的模型。你需要创建一个遵循 TimelineEntry 协议的结构体,定义你的 Widget 所需的数据。

  3. 创建 Widget View:Widget View 是 Widget 的用户界面。你需要创建一个 View,展示你的 Widget 的内容。

  4. 实现 Timeline Provider:Timeline Provider 是 Widget 数据的提供者。你需要创建一个遵循 TimelineProvider 协议的结构体,提供 Widget 的数据。

  5. 配置 Widget:在 Widget 的主结构体中,你需要配置你的 Widget,包括它的类型(静态或者动态)、数据提供者、视图等。

  6. 测试 Widget:在模拟器或者真机上测试你的 Widget,确保它的数据和视图都按预期工作。

接下来,我们将详细介绍 Widget 的开发流程。

小组件-StaticConfiguration 静态配置

在 Xcode 中,File -> New -> Target,选择 Widget Extension。这将会生成一个新的 target,包含了创建 Widget 所需的基本代码。

以下是一个简单的小组件代码示例:

import WidgetKitimport SwiftUI// Timeline Entrystruct ArticleEntry: TimelineEntry {    let date: Date    let title: String}// Widget Viewstruct ArticleWidgetView : View {    let entry: ArticleEntry    var body: some View {        Text(entry.title)    }}// Timeline Providerstruct ArticleTimelineProvider: TimelineProvider {    typealias Entry = ArticleEntry        func placeholder(in context: Context) -> Entry {        // 占位大小,内容不会显示        return ArticleEntry(date: Date(), title: "Placeholder")    }    func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {        let entry = ArticleEntry(date: Date(), title: "Snapshot")        completion(entry)    }    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {        let entry = ArticleEntry(date: Date(), title: "Timeline")        let timeline = Timeline(entries: [entry], policy: .never)        completion(timeline)    }}// Widget Configuration@mainstruct ArticleWidget: Widget {        var body: some WidgetConfiguration {        StaticConfiguration(            kind: "com.starming.articleWidget",            provider: ArticleTimelineProvider()        ) { entry in            ArticleWidgetView(entry: entry)        }        .configurationDisplayName("Article Widget")        .description("这是一个 Article Widget.")        .supportedFamilies([            .systemSmall,            .systemMedium,            .systemLarge,        ])    }}

在上面的代码中,我们定义了一个 ArticleWidget 小组件,它包含了一个 ArticleEntry 数据模型、一个 ArticleWidgetView 视图、一个 ArticleTimelineProvider 数据提供者和一个 ArticleWidget 配置。

小组件-AppIntentConfiguration

iOS 17 开始可以使用 AppIntentConfiguration 来配置小组件,这样可以让小组件和 AppIntent 交互。这样可以让小组件和 App 之间的进行交互。

下面是一个简单的小组件代码示例,展示了如何使用 AppIntentConfiguration 来配置小组件和 AppIntent 交互

import SwiftUIimport WidgetKitimport AppIntentsstruct ArticleWidget: Widget {    var body: some WidgetConfiguration {        AppIntentConfiguration(            kind: "com.starming.articleWidget",            intent: ArticleIntent.self,            provider: ArticleIntentProvider()        ) { entry in            ArticleWidgetView(entry: entry)        }        .configurationDisplayName("Article Widget")        .description("这是一个 Article Widget.")        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])    }}struct ArticleWidgetView: View {    var entry: IntentProvider.Entry    var body: some View {        Text(entry.author)    }}struct ArticleIntentProvider: AppIntentTimelineProvider {    func snapshot(for configuration: ArticleIntent, in context: Context) async -> ArticleEntry {        return .init(            date: Date(),            author: "snapshot"        )    }    func placeholder(in context: Context) -> ArticleEntry {        return .init(            date: Date(),            author: "某人"        )    }    func timeline(for configuration: ArticleIntent, in context: Context) async -> Timeline<ArticleEntry> {        return Timeline(            entries: [                .init(date: Date(),                      author: configuration.author,                      rate: await ArticleStore().rate())],            policy: .never)    }}struct ArticleEntry: TimelineEntry {    let date: Date    let author: String    var rate: Int = 0    //...}// 放在主应用中和小组件交互struct ArticleIntent: WidgetConfigurationIntent {        static var title: LocalizedStringResource  = "文章"    var author: String = "某某某"    func perform() async throws -> some IntentResult {        //...        return .result()    }}class ArticleStore {    //... SwiftData 相关配置    @MainActor    func rate() async -> Int {        //... 获取        return 5    }}

如上代码所示,我们定义了一个 ArticleWidget 小组件,它包含了一个 ArticleIntent 数据模型、一个 ArticleWidgetView 视图、一个 ArticleIntentProvider 数据提供者和一个 ArticleWidget 配置。

小组件-配置选项

显示区域

iOS 17 新增显示区域配置,有下面四种

  • homeScreen:主屏幕
  • lockScreen:锁屏
  • standBy:待机
  • iPhoneWidgetsOnMac:iPhone 上的 Mac 小组件

设置小组件不在哪个区域显示某尺寸。

struct SomeWidget: Widget {    ...    var body: some WidgetConfiguration {        AppIntentConfiguration(            ... { entry in            ...        }        // 在 StandBy 中取消显示 systemSmall 尺寸        .disfavoredLocations([.standBy], for: [.systemSmall])    }}

取消内容边距

使用 .contentMarginsDisabled() 取消内容边距。

struct SomeWidget: Widget {    ...    var body: some WidgetConfiguration {        AppIntentConfiguration(            ... { entry in            ...        }        // 使 Content margin 失效        .contentMarginsDisabled()    }}

每个平台内容边距大小不同,环境变量 \.widgetContentMargins 可以读取内容边距的大小。

取消背景删除

在 StandBy 和 LockScreen 的某些情况,小组件的背景是会被自动删除的。

使用 containerBackgroundRemovable() 修饰符可以取消背景删除。

struct SomeWidget: Widget {    ...    var body: some WidgetConfiguration {        AppIntentConfiguration(            ... { entry in            ...        }        // 取消背景删除        .containerBackgroundRemovable(false)        // 让自己的背景可以全覆盖        .contentMarginsDisabled()    }}

后台网络处理

.onBackgroundURLSessionEvents { (identifier, completion) in    //...}

AppIntentTimelineProvider

AppIntentConfiguration 需要 AppIntentTimelineProvider,AppIntentTimelineProvider 需要实现 snapshotplaceholdertimeline 三个方法来确定小组件在展示和实际运行时间线时的视图和数据。

struct ArticleIntentProvider: AppIntentTimelineProvider {    func snapshot(for configuration: ArticleIntent, in context: Context) async -> ArticleEntry {        return .init(            date: Date(),            author: "snapshot"        )    }    func placeholder(in context: Context) -> ArticleEntry {        return .init(            date: Date(),            author: "某人"        )    }    func timeline(for configuration: ArticleIntent, in context: Context) async -> Timeline<ArticleEntry> {        return Timeline(            entries: [                .init(date: Date(),                      author: configuration.author,                      rate: await ArticleStore().rate())],            policy: .never)    }}struct ArticleEntry: TimelineEntry {    let date: Date    let author: String    var rate: Int = 0    //...}````## Widget View### 不同的大小设置不同视图```swiftstruct ArticleWidgetView: View {  var entry: Provider.Entry  @Environment(\.widgetFamily) var family  @ViewBuilder  var body: some View {    switch family {    case .systemSmall:        SomeViewSmall()    default:      SomeViewDefault()    }  }}

锁屏小组件

让小组件支持锁屏

struct ArticleWidget: Widget {    var body: some WidgetConfiguration {        StaticConfiguration(            ...        ) { entry in            ...        }        ...        .supportedFamilies([            .systemSmall,            .systemMedium,            .systemLarge,            // 添加支持到 Lock Screen widgets            .accessoryCircular,            .accessoryRectangular,            .accessoryInline,        ])    }}

不同类型 widgetFamily 实现不同视图

struct ArticleWidgetView : View {       let entry: ViewSizeEntry    // 获取 widget family 值    @Environment(\.widgetFamily) var family    var body: some View {        switch family {        case .accessoryRectangular:            RectangularWidgetView()        case .accessoryCircular:            CircularWidgetView()        case .accessoryInline:            InlineWidgetView()        default:            ArticleWidgetView(entry: entry)        }    }}

不同渲染模式实现不同视图

小组件有三种不同的渲染模式:

  • Full-color:主屏用
  • Vibrant:用于待机模式和锁屏
  • The accented:用于手表
struct ArticleWidgetView: View {    let entry: Entry        @Environment(\.widgetRenderingMode) private var renderingMode        var body: some View {        switch renderingMode {        case .accented:            AccentedWidgetView(entry: entry)        case .fullColor:            FullColorWidgetView(entry: entry)        case .vibrant:            VibrantWidgetView(entry: entry)        default:            DefaultView()        }    }}

视图交互

使用 AppIntent

struct ArticleWidgetView : View {    var entry: IntentProvider.Entry    var body: some View {        VStack(spacing: 20) {            ...            Button(intent: RunIntent(rate: entry.rate), label: {                ...            })        }    }}

刷新小组件

通过 Text 视图更新

倒计时

let futureDate = Calendar.current.date(byAdding: components, to: Date())!// 日期会在 Text 视图中动态变化
struct CountdownWidgetView: View {        var body: some View {        Text(futureDate(), style: .timer)    }        private func futureDate() -> Date {        let components = DateComponents(second: 10)        let futureDate = Calendar.current.date(byAdding: components, to: Date())!        return futureDate    }}

Timeline Provider 更新

在 timeline 方法中实现,entries 包含了不同更新的数据。

func timeline(for configuration: ArticleIntent, in context: Context) async -> Timeline<ArticleEntry> {    return Timeline(        entries: [            .init(date: Date(),                  author: configuration.author,                  rate: await ArticleStore().rate())],        policy: .never)}

更新策略

3 种类型的刷新策略:

  • atEnd:上个刷新完成直接进入下个刷新,但是进入下一个刷新的时间由系统决定。
  • after(Date):指定进入下个刷新的时间,但是具体时间还是由系统说了算,因此可以理解为是指定的是最早进入下个刷新的时间。
  • never:不会进入下个刷新,除非显式调用 reloadTimelines(ofKind:)

举例,指定下个刷新周期至少是上个周期结束10秒后:

let lastUpdateDate = entries.last!.datelet nextUpdateDate = Calendar.current.date(byAdding: DateComponents(second: 10), to: lastUpdate)!let timeline = Timeline(entries: entries, policy: .after(nextUpdate))

Relevance 优先级

App 自定义刷新 Timeline 的优先级,使用 Relevance。先在 TimelineEntry 里定义:

struct ArticleEntry: TimelineEntry {    let date: Date    ...    let relevance: TimelineEntryRelevance?}

在 timeline 方法中根据必要刷新程序,定义不同 relevance 的值。

App 主动刷新

// 刷新单个小组件WidgetCenter.shared.reloadTimelines(ofKind: "CountryWidget")// 刷新所有小组件WidgetCenter.shared.reloadAllTimelines()

刷新小组件的最佳实践

调试时刷新率不会有限制,生产环境每天最多40到70次,相当于每15到60分钟刷新一次。

小组件动画

Text 视图动态时间

利用 Text 的动态时间能力

timeline 动画

timeline 是由一组时间和数据组成的,每次刷新时,小组件通过和上次数据不一致加入动画效果。

默认情况小组件使用的是弹簧动画。我们也可以添加转场(Transition)、动画(Animation)和内容过渡(Content Transition)动画效果。

文本内容过渡动画效果

.contentTransition(.numericText(value: rate))

从底部翻上来的专场

.transition(.push(from: .bottom))

小组件-远程定时获取数据

在 TimelineProvider 中的 timeline 方法中加入请求逻辑

func timeline(for configuration: RunIntent, in context: Context) -> Void) async -> Timeline<ArticleEntry> {    guard let article = try? await ArticleFetch.fetchNewestArticle() else {        return    }    let entry = ArticleEntry(date: Date(), article: article)        // 下次在 30 分钟后再请求    let afterDate = Calendar.current.date(byAdding: DateComponents(minute: 30), to: Date())!    return Timeline(entries: [entry], policy: .after(afterDate))}

以上代码中,我们在 timeline 方法中请求了最新的文章数据,并且设置了下次请求的时间是当前时间的 30 分钟后。

小组件-获取位置权限更新内容

小组件获取位置权限和主应用 target 里获取方式很类似,步骤:

  • 在 info 里添加 NSWidgetUseLocation = ture
  • 使用 CLLocationManager 来获取位置信息,设置较低的精度。
  • isAuthorizedForWidgetUpdates 请求位置权限。

支持多个小组件

widget bundle 可以支持多个小组件。

@mainstruct FirstWidgetBundle: WidgetBundle {        @WidgetBundleBuilder    var body: some Widget {        FirstWidget()        SecondWidget()        ...        SecondWidgetBundle().body    }}struct SecondWidgetBundle: WidgetBundle {    @WidgetBundleBuilder    var body: some Widget {        SomeWidgetOne()        SomeWidgetTwo()        ...    }}

获取小组件形状

不同设备小组件大小和形状都不同,比如要加个边框,就很困难。这就需要使用 ContainerRelativeShape 来获取 Shape 视图容器。

var body: some View {  ZStack {    ContainerRelativeShape()        .inset(by: 2)        .fill(.pink)    Text("Hello world")    ...  }}

小组件-Deep link

medium 和 large 的小组件可以使用 Link,small 小组件使用 .widgetURL 修饰符。

小组件访问SwiftData

Wdiget target 访问主应用 target 的 SwiftData 数据步骤如下:

  • 对主应用和 Widget 的 target 中的 Signing & Capabilities 都添加 App Groups,并创建一个新组,名字相同。
  • SwiftData 的模型同时在主应用和 Widget 的 target 中。
  • StaticConfiguration 或 AppIntentConfiguration 中添加 modelContainer() 修饰符,让 SwiftData 的容器可用。

小组件-参考资料

WWDC

23

22

21

20

小册子之如何使用 SwiftData 开发 SwiftUI 应用

以下内容已整理到小册子中,小册子代码在 Github 上,本文会随着系统更新和我更多的实践而新增和更新,你可以购买“戴铭的开发小册子”应用(98元),来跟踪查看本文内容新增和更新。

本文属于小册子系列中的一篇,已发布系列文章有:

小册子代码里有大量 SwiftData 实际使用实践的代码。

在 Swift 中,有许多库可以用于处理数据,包括但不限于 SwiftData、CoreData、Realm、SQLite.swift 等。这些库各有优势。

但,如果使用 SwiftData,你可以在 Swift 中更加方便地处理数据。SwiftData 是 Apple 在 WWDC23 上推出的一个新的数据持久化框架,它是 CoreData 的替代品,提供了更简单、更易用的 API。

创建@Model模型

先说说如何创建 SwiftData 模型。

创建

@Model 宏装饰类

@Modelfinal class Article {    let title: String    let author: String    let content: String    let publishedDate: Date        init(title: String, author: String, content: String, publishedDate: Date) {        self.title = title        self.author = author        self.content = content        self.publishedDate = publishedDate    }}

以上代码创建了一个 Article 模型,包含了标题、作者、内容和发布日期。

以下数据类型默认支持:

  • 基础类型:Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, Float, Double, Bool, String, Date, Data 等
  • 复杂的类型:Array, Dictionary, Set, Optional, Enum, Struct, Codable 等
  • 模型关系:一对一、一对多、多对多

默认数据库路径: Data/Library/Application Support/default.store

@Attribute

接下来说说如何使用 @Attribute 宏。

一些常用的:

  • spotlight:使其能出现在 Spotlight 搜索结果里
  • unique:值是唯一的
  • externalStorage:值存储为二进制数据
  • transient:值不存储
  • encrypt:加密存储

使用方法

@Attribute(.externalStorage) var imgData: Data? = nil

二进制会将其存储为单独的文件,然后在数据库中引用文件名。文件会存到 Data/Library/Application Support/.default_SUPPORT/_EXTERNAL_DATA 目录下。

@Transient 不存

如果有的属性不希望进行存储,可以使用 @Transient

@Modelfinal class Article {    let title: String    let author: String    @Transient var content: String    ...}

transformable

SwiftData 除了能够存储字符串和整数这样基本类型,还可以存储更复杂的自定义类型。要存储自定义类型,可用 transformable。

@Modelfinal class Article {    let title: String    let author: String    let content: String    let publishedDate: Date    @Attribute(.transformable(by: UIColorValueTransformer.self)) var bgColor: UIColor    ...}

UIColorValueTransformer 类的实现

class UIColorValueTransformer: ValueTransformer {        // return data    override func transformedValue(_ value: Any?) -> Any? {        guard let color = value as? UIColor else { return nil }        do {            let data = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true)            return data        } catch {            return nil        }    }        // return UIColor    override func reverseTransformedValue(_ value: Any?) -> Any? {        guard let data = value as? Data else { return nil }                do {            let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data)            return color        } catch {            return nil         }    }}

注册

struct SwiftPamphletAppApp: App {    init() {        ValueTransformer.setValueTransformer(UIColorValueTransformer(), forName: NSValueTransformerName("UIColorValueTransformer"))    }        var body: some Scene {        WindowGroup {            ContentView()                .modelContainer(for: [Article.self])        }    }}

SwiftData-模型关系

使用 ``@Relationship` 添加关系,但是不加这个宏也可以,SwiftData 会自动添加模型之间的关系。

@Modelfinal class Author {    var name: String    @Relationship(deleteRule: .cascade, inverse: \Brew.brewer)    var articles: [Article] = []}@Modelfinal class Article {    ...    var author: Author}

默认情况 deleteRule 是 .nullify,这个删除后只会删除引用关系。.cascade 会在删除用户后删除其所有文章。

SwiftData 可以添加一对一,一对多,多对多的关系。

限制关系表数量

@Relationship(maximumModelCount: 5)    var articles: [Article] = []

容器配置modelContainer

多模型

配置方法

@mainstruct SomeApp: App {    var body: some Scene {        WindowGroup {            ContentView()        }        .modelContainer(for: [Article.self, Author.self])    }}

有关系的两个模型,只需要加父模型,SwiftData 会推断出子模型。

数据存内存

let configuration = ModelConfiguration(inMemory: true)let container = try ModelContainer(for: schema, configurations: [configuration])

数据只读

let config = ModelConfiguration(allowsSave: false)

自定义存储文件和位置

如果要指定数据库存储的位置,可以按下面写法:

@mainstruct SomeApp: App {    var container: ModelContainer    init() {        do {            let storeURL = URL.documentsDirectory.appending(path: "database.sqlite")            let config = ModelConfiguration(url: storeURL)            container = try ModelContainer(for: Article.self, configurations: config)        } catch {            fatalError("Failed")        }    }    var body: some Scene {        WindowGroup {            ContentView()        }        .modelContainer(container)    }}

iCloud 支持

如果要添加 iCloud 支持,需要先确定模型满足以下条件:

  • 没有唯一约束
  • 关系是可选的
  • 有所值有默认值

iCloud 支持操作步骤:

  • 进入 Signing & Capabilities 中,在 Capability 里选择 iCloud
  • 选中 CloudKit 旁边的框
  • 设置 bundle identifier
  • 再按 Capability,选择 Background Modes
  • 选择 Remote Notifications

指定部分表同步到 iCloud

使用多个 ModelConfiguration 对象来配置,这样可以指定哪个配置成同步到 iCloud,哪些不同步。

添加多个配置

@mainstruct SomeApp: App {    var container: ModelContainer    init() {        do {            let c1 = ModelConfiguration(for: Article.self)            let c2 = ModelConfiguration(for: Author.self, isStoredInMemoryOnly: true)            container = try ModelContainer(for: Article.self, Author.self, configurations: c1, c2)        } catch {            fatalError("Failed")        }    }    var body: some Scene {        WindowGroup {            ContentView()        }        .modelContainer(container)    }}

撤销和重做

创建容器时进行指定

.modelContainer(for: Article.self, isUndoEnabled: true)

这样 modelContext 就可以调用撤销和重做函数。

struct SomeView: View {    @Environment(\.modelContext) private var context    var body: some View {        Button(action: {            context.undoManager?.undo()        }, label: {            Text("撤销")        })    }}

context

View 之外的地方,可以通过 ModelContainer 的 context 属性来获取 modelContext。

let context = container.mainContextlet context = ModelContext(container)

预先导入数据

方法如下:

.modelContainer(for: Article.self) { result in    do {        let container = try result.get()        // 先检查有没数据        let descriptor = FetchDescriptor<Article>()        let existingArticles = try container.mainContext.fetchCount(descriptor)        guard existingArticles == 0 else { return }        // 读取 bundle 里的文件        guard let url = Bundle.main.url(forResource: "articles", withExtension: "json") else {            fatalError("Failed")        }        let data = try Data(contentsOf: url)        let articles = try JSONDecoder().decode([Article].self, from: data)        for article in articles {            container.mainContext.insert(article)        }    } catch {        print("Failed")    }}

增删modelContext

添加保存数据

struct SomeView: View {   @Environment(\.modelContext) var context   ...   var body: some View {         ...         Button(action: {             self.add()         }, label: {             Text("添加")         })   }   func add() {      ...      context.insert(article)   }}

默认不用使用 context.save(),SwiftData 会自动进行保存,如果不想自动保存,可以在容器中设置

var body: some Scene {   WindowGroup {      ContentView()   }   .modelContainer(for: Article.self, isAutosaveEnabled: false)       }

编辑和删除数据

编辑数据使用 @Bindable

struct SomeView: View {    @Bindable var article: Article    @Environment(\.modelContext) private var modelContext    ...        var body: some View {        Form {            TextField("文章标题", text: $article.title)            ...        }        .toolbar {            ToolbarItem(placement: .destructiveAction) {                Button("删除") {                    modelContext.delete(article)                }            }        }        ...    }}

SwiftData-检索

@Query

使用 @Query 会从数据库中获取数据。

@Query private var articles: [Article]

@Query 还支持 filter、sort、order 和 animation 等参数。

@Query(sort: \Article.title, order: .forward) private var articles: [Article]

sort 可支持多个 SortDescriptor,SwiftData 会按顺序处理。

@Query(sort: [SortDescriptor(\Article.isArchived, order: .forward),SortDescriptor(\Article.updateDate, order: .reverse)]) var articles: [Article]

Predicate

filter 使用的是 #Predicate

static var now: Date { Date.now }@Query(filter: #Predicate<Article> { article in    article.releaseDate > now}) var draftArticles: [Article]

Predicate 支持的内置方法主要有 containsallSatisfyflatMapfiltersubscriptstartsminmaxlocalizedStandardContainslocalizedComparecaseInsensitiveCompare 等。

@Query(filter: #Predicate<Article> { article in    article.title.starts(with: "苹果发布会")}) var articles: [Article]

需要注意的是 .isEmpty 不能使用 article.title.isEmpty == false ,否则会崩溃。

FetchDescriptor

FetchDescriptor 可以在模型中查找数据,而不必在视图层做。

@Modelfinal class Article {    var title: String    ...    static var all: FetchDescriptor<Article> {        FetchDescriptor(sortBy: [SortDescriptor(\Article.updateDate, order: .reverse)])    }}struct SomeView: View {       @Query(Article.all) private var articles: [Article]    ...}

获取数量而不加载

使用 fetchCount() 方法,可完成整个计数,且很快,内存占用少。

let descriptor = FetchDescriptor<Article>(predicate: #Predicate { $0.words > 50 })let count = (try? modelContext.fetchCount(descriptor)) ?? 0

fetchLimit 限制获取数量

var descriptor = FetchDescriptor<Article>(  predicate: #Predicate { $0.read },  sortBy: [SortDescriptor(\Article.updateDate,           order: .reverse)])descriptor.fetchLimit = 30let articles = try context.fetch(descriptor)// 翻页let pSize = 30let pNumber = 1var fetchDescriptor = FetchDescriptor<Article>(sortBy: [SortDescriptor(\Article.updateDate, order: .reverse)])fetchDescriptor.fetchOffset = pNumber * pSizefetchDescriptor.fetchLimit = pSize

限制获取的属性

只请求要用的属性

var fetchDescriptor = FetchDescriptor<Article>(sortBy: [SortDescriptor(\.updateDate, order: .reverse)])fetchDescriptor.propertiesToFetch = [\.title, \.updateDate]

SwiftData-处理大量数据

SwiftData 模型上下文有个方法叫 enumerate(),可以高效遍历大量数据。

let descriptor = FetchDescriptor<Article>()...do {    try modelContext.enumerate(descriptor, batchSize: 1000) { article in        ...    }} catch {    print("Failed.")}

其中 batchSize 参数是调整批量处理的数量,也就是一次加载多少对象。因此可以通过这个值来权衡内存和IO数量。这个值默认是 5000。

SwiftData多线程

创建一个 Actor,然后 SwiftData 上下文在其中执行操作。

@ModelActoractor DataHandler {}extension DataHandler {    func addInfo() throws -> IOInfo {        let info = IOInfo()        modelContext.insert(info)        try modelContext.save()        return info    }    ...}

使用

Task.detached {    let handler = DataHandler()    let item = try await handler.addInfo()       ...}

SwiftData-版本迁移

以下的小改动 SwiftData 会自动执行轻量迁移:

  • 增加模型
  • 增加有默认值的新属性
  • 重命名属性
  • 删除属性
  • 增加或删除 .externalStorage.allowsCloudEncryption 属性。
  • 增加所有值都是唯一属性为 .unique
  • 调整关系的删除规则

其他情况需要用到版本迁移,版本迁移步骤如下:

  • 用 VersionedSchema 创建 SwiftData 模型的版本
  • 用 SchemaMigrationPlan 对创建的版本进行排序
  • 为每个迁移定义一个迁移阶段

设置版本

enum ArticleV1Schema: VersionedSchema {    static var versionIdentifier: String? = "v1"    static var models: [any PersistentModel.Type] { [Article.self] }    @Model    final class Article {        ...    }}

SchemaMigrationPlan 轻量迁移

enum ArticleMigrationPlan: SchemaMigrationPlan {    static var schemas: [any VersionedSchema.Type] {        [ArticleV1Schema.self, ArticleV2Schema.self]    }    static var stages: [MigrationStage] {        [migrateV1toV2]    }    static let migrateV1toV2 = MigrationStage.lightweight(        fromVersion: ArticleV1Schema.self,        toVersion: ArticleV2Schema.self    )}

自定义迁移

static let migrateV1toV2 = MigrationStage.custom(    fromVersion: ArticleV1Schema.self,    toVersion: ArticleV2Schema.self,    willMigrate: { context in        // 合并前的处理    },    didMigrate: { context in        // 合并后的处理    })

SwiftData-调试

CoreData 的调试方式依然适用于 SwiftData。

你可以设置启动参数来让 CoreData 打印出执行的 SQL 语句。在你的项目中,选择 “Product” -> “Scheme” -> “Edit Scheme”,然后在 “Arguments” 标签下的 “Arguments Passed On Launch” 中添加 -com.apple.CoreData.SQLDebug 1。这样,每当 CoreData 执行 SQL 语句时,都会在控制台中打印出来。

使用 -com.apple.CoreData.SQLDebug 3 获取后台更多信息。

SwiftData-资料

WWDC

23

❌