<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>liang2kl's Blog</title><link>https://yeshe.ng/blog/</link><description>Recent content on liang2kl's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>liang2kl@outlook.com (Liang Yesheng)</managingEditor><webMaster>liang2kl@outlook.com (Liang Yesheng)</webMaster><copyright>from 2021, to infinity and beyond...</copyright><lastBuildDate>Fri, 21 Mar 2025 21:44:00 -0700</lastBuildDate><atom:link href="https://yeshe.ng/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>死亡谷国家公园自驾游记</title><link>https://yeshe.ng/blog/posts/2025/03/death-valley-trip/</link><pubDate>Fri, 21 Mar 2025 21:44:00 -0700</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2025/03/death-valley-trip/</guid><description>圣地亚哥 -> 拉斯维加斯 -> 死亡谷的三天两夜</description><content>&lt;p>最近在计划暑假找时间走一走美西国家公园环线，本来考虑在环线的最后穿过死亡谷回圣地亚哥，但是夏天死亡谷的平均气温接近地狱，不太现实。但死亡谷是一个非常特别的国家公园，而且距离圣地亚哥近，不去有点可惜。于是说走就走，提前不到一个星期极限用脚本抢到了 Furnace Creek 的&lt;a href="https://www.recreation.gov/camping/campgrounds/232496">露营地&lt;/a>，买了各种装备，在考完试的第二天就开始了这次自驾。&lt;/p>
&lt;p>这次行程没有找人一起，一个原因是我打算在死亡谷住车里露营，超过一个人睡不下；更加根本的原因是我比较喜欢实现自己一些奇怪的想法，与其他人出行必定需要妥协。反正距离不远，而且有 FSD，开车几乎不需要消耗精力，一个人自驾完全没有问题。&lt;/p>
&lt;h2 id="准备">准备&lt;/h2>
&lt;p>第一次自驾+露营没什么经验，因此计划做得比较详细。最头疼的是车的续航问题：我的车是 Model 3 标续，电池衰减后之后只有大概 250 英里的续航，面对长途旅行有点捉襟见肘，因此所有计划都得考虑到两点之间的距离。&lt;/p>
&lt;p>首先是对开车和休息的计划。从圣地亚哥直接去死亡谷太远，第一天几乎没有可以游玩的时间，因此最终决定在拉斯维加斯住一晚，第二天大概开 2 个小时就能到死亡谷游客中心，中途经停 Pahrump 充电；最后一天直接从死亡谷开车回家。&lt;/p>
&lt;p>其次是确定在死亡谷要玩的几个地方。我根据 &lt;a href="https://stoneroadpress.com/ygnp/">Your Guide&lt;/a> 的推荐选择了靠近 Furnace Creek 露营地（也是游客中心）附近的五六个观景点和徒步路线，从而避免电量耗尽，因此也没办法到死亡谷的其他区域玩。不过，最热门的景点基本都集中在 Furnace Creek 附近，就两天的行程而言已经是非常充足了。路线确定后需要大概预估一下续航能不能撑得住。我当时预估续航是直接通过 Google Maps 的距离算的，预留了 10 英里的容错空间。最后发现这种方法不是很准确，因为上坡消耗的电量远大于平地，而下坡由于动能回收反而能够增加续航，但整个行程算下来也没有太大误差。最准确的方法是通过特斯拉的 app 或者&lt;a href="https://www.tesla.com/trips">网页&lt;/a>来预估，这个预估考虑了海拔高度的变化，实际测量下来误差大概在 5% 以内。&lt;/p>
&lt;p>最后是物资的准备。我带了三张被子，一张当作床垫，另两张来盖。为了体验露营，我还买了一捆木柴生火；事实证明这是一个错误的选择，暂且按下不表。另外，露营灯和手电筒非常重要，因为露营地漆黑一片，伸手不见五指。其他一些极大改善露营体验的物品包括：耳塞、眼罩、露营椅，等等。总之装备多多益善。&lt;/p>
&lt;p>出发前还订阅了每月 99 刀的 FSD，并从 v12.5 升级到了 v12.6，表现有非常大的提升。事实证明这 99 刀非常值：这次路上 95% 以上由 FSD 驾驶，在高速和城市道路上几乎没有任何问题；在国家公园里晚上驾驶时偶尔有幽灵刹车的问题，不过因为周围几乎没有车，因此影响不大。总的来说 FSD 极大程度上解放了驾驶员，在 roadtrip 中能够为司机节省宝贵的精力用于游玩。&lt;/p>
&lt;h2 id="第一天圣地亚哥到拉斯维加斯">第一天：圣地亚哥到拉斯维加斯&lt;/h2>
&lt;p>第一天早上 10 点从圣地亚哥出发，上 15 号公路就可以直达拉斯维加斯，全程在高速最左道上巡航，没有任何心智负担。算上充电，全程大概 5 个半小时，一共 326 英里（521 公里），不算太远。15 号公路上的风景很不错，途径雪山、沙漠、峡谷，第一次开这条路线不会觉得无聊。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2025/03/i15-view.webp" />
&lt;figcaption class="left" >I-15 沿途的雪山&lt;/figcaption>
&lt;/figure>
&lt;p>我对拉斯维加斯本身没有太多兴趣，到酒店之后直接点了 Panda Express 在房间里吃。之后去隔壁的 Park MGM 看了 Maroon 5 的演唱会，体验、氛围和 Adam 的状态比预期好不少。看完演唱会就回去睡觉了。&lt;/p>
&lt;figure class="center" style="width:100%">
&lt;video style="object-fit:fill;width:100%" controls preload="none" poster="/blog/2025/03/maroon5-screenshot.webp">
&lt;source src="https://yeshe.ng/blog/2025/03/maroon5-stereo-hearts.mov#t=0.1">
&lt;/video>
&lt;figcaption class="left">Maroon 5 演唱会&lt;/figcaption>
&lt;/figure>
&lt;h2 id="第二天游玩死亡谷">第二天：游玩死亡谷&lt;/h2>
&lt;p>早上 10 点准时从酒店出发，1 小时 10 分钟后到了 Pahrump 充电；30 分钟后充满，再开 40 分钟就到了 visitor center。在里面按惯例买了冰箱贴，拿了地图和指南后就开始去各个地方玩。&lt;/p>
&lt;p>第一站去了 Artist&amp;rsquo;s Palette。去这里的路叫做 Artist&amp;rsquo;s Drive，是一条先上坡后下坡的单行道，弯道很急，并不是特别好开。这里的地貌由很多小山坡组成，每个山坡由于不同矿物质沉积的缘故显示出迥异的颜色，看上去就像调色盘一般，而 Artist&amp;rsquo;s Palette 这种特点最为突出，尤为漂亮，因而得此名。&lt;/p>
&lt;p>也许是因为下午 3 点的光线没到最佳，现场看到的像是低饱和度的版本，整体偏灰褐色，但仍然很好看。眯上眼睛，朦胧中看到了《千里江山图》的色调。在远处拍完照之后，走到了山丘上近距离观察了一会不同颜色的岩石，像是置身画中一般。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/gallery/preview/DSC03098.webp" />
&lt;figcaption class="left" >Artist's Palette&lt;/figcaption>
&lt;/figure>
&lt;p>沿着路继续往低处开，之前从高处俯视的白色盐碱地逐渐与路面平齐。这片盐碱地叫 Badwater Basin，是北美大陆最低点，海拔 -82 米。后方的岩壁高高耸起，半山腰上却立起了一块标示海平面高度的牌子，十分魔幻，可见此处地势之低。因此，水流进入这片盆地后无法流出，河水累积在盆地底部；而这里极高的蒸发速率使得水分无法长存，残余的盐便形成了一片广阔无垠、纯净的白色盐碱地。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2025/03/sea-level-sign.webp" />
&lt;figcaption class="left" >Badwater Basin 的海平面标志&lt;/figcaption>
&lt;/figure>
&lt;p>此时气温已经达到 30 度左右，越往盐碱地中心走人越少，到最后只剩下我和一个头发花白但精神矍铄的大叔。来美国半年已经习惯了迎面相遇时跟对方打招呼，上前互相问候一番后，我们开始聊了起来。&lt;/p>
&lt;p>我们聊起了死亡谷。他说他上一次来是 20 年前，我问他这里 20 年来有什么变化，他说完全没有。景观没变，但人却经历了 20 年的时光：他上次跟妻子——当时还是女朋友——一起来这里，没钱住公园里的酒店，但 20 年后的现在他住进去了。我打趣说 20 年后我有钱了也回来住酒店，他笑着说一定会的。&lt;/p>
&lt;p>他说自己在 18 年去中国出差，去了北京、上海和西安旅游。他说，自己世界各地飞，即使是出差时，也一定要抽出时间在当地转转。他的同事和他一起出差时总是说“这次算了，下次再玩”，但每次都是“next time”，从来没有实现过。他告诫说，要抓住机会去体验世界，人生一眨眼就过去了。我点点头，深以为然。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/gallery/preview/DSC03114.webp" />
&lt;figcaption class="left" >走在 Badwater Basin 上&lt;br/>拍完这张照片后，跟这位大叔聊了会天&lt;/figcaption>
&lt;/figure>
&lt;p>跟大叔话别，我往回开，前往今天的最后一站：Mesquite Flat Sand Dunes。走进沙丘，我有意避开人群，朝着另一个方向走。在独自一人的时候，沙丘仿佛有种魔力，驱使着我不自觉地跑了起来，像个小孩子跌跌撞撞地反复冲上冲下一个又一个斜坡，尽管脚下的沙子不断打滑。&lt;/p>
&lt;p>置身沙丘最独特的体验，是从不同角度欣赏光与影投射出的杰作。在落日的照射下，沙丘明暗两面有坚决而果断的分界，而暗面的渐变却像丝绸一样光滑。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2025/03/dune-wallpaper.webp" />
&lt;figcaption class="left" >走进 macOS Mojave 壁纸&lt;/figcaption>
&lt;/figure>
&lt;p>将近日落，我终于走到了附近最高的一个沙丘。沙丘很滑，因此登顶最好的方法是找一个坡度较缓的角度登上顶部的沙脊，然后沿着沙脊走向最高点。在登顶的过程中，踩在完全没有其他人脚印的沙脊上一步一步向上走，整个沙丘为我独占，耳中只有呼呼的风声和自己的脚步声，心中突然涌现一种复杂而莫名的感动。&lt;/p>
&lt;figure class="center" style="width:100%;max-width:500px;">
&lt;video style="object-fit:fill;width:100%;margin-botton:0" controls preload="none" poster="/blog/2025/03/walking-dune-screenshot.webp">
&lt;source src="https://yeshe.ng/blog/2025/03/walking-on-top-of-dune.mov">
&lt;/video>
&lt;figcaption class="left">走在沙丘顶上&lt;/figcaption>
&lt;/figure>
&lt;p>到了最高点，拍了几张照片之后，我把相机和手机放在一边，躺在沙脊上，头侧过一边，看着太阳缓缓落下。我没有拍照——相机只能记录下静态的画面，在这种时刻显得力不从心。&lt;/p>
&lt;figure class="center" style="width:100%;max-width:500px;">
&lt;img src="https://yeshe.ng/blog/2025/03/dune-lying.webp">&lt;/img>
&lt;figcaption class="left">躺在沙丘顶上&lt;/figcaption>
&lt;/figure>
&lt;p>待到日落后，我继续坐了 10 来分钟就离开了。其实这里的星空是最好看的，我本想坐到星空出现，但无奈接下来还要去 40 分钟以外的 Beatty 充电，一来一回晚上回到露营地已经很晚，就此作罢。充电回来的路上有一件趣事：我的耳朵因为气压的变化产生了和飞机下降过程中同样的不适，可见 Furnace Creek 区域地势之低。&lt;/p>
&lt;p>回到营地后，我便开始准备晚上的露营。我首先生了火，在旁边坐着烤火，非常舒服。但是有一个致命的问题：晚上风很大，而且风往车的方向吹，以至于最后睡觉的时候车里面全是碳火的味道。另外，我买的木柴非常不禁烧，大概过半小时就快熄灭了；但烟还持续地散发。总之这篝火非常失败，也是这次旅途唯一一个不太完美的地方。&lt;/p>
&lt;p>之后开始铺床。Model 3 的后排座椅不能完全放平，有一个小角度，但影响不大。我买的一张 twin size 的床垫保护罩刚好铺满了后备箱和后座，可见车内的空间足以睡一个安稳觉。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2025/03/car-camping.webp" />
&lt;figcaption class="left" >睡车里露营&lt;/figcaption>
&lt;/figure>
&lt;p>铺完床之后就做到车外面看星星。死亡谷完全没有光污染，观星效果非常好。我没带三脚架，因此把相机架在椅子上随便拍了几张长曝光之后就单纯用肉眼看了。我完全不懂观星，只认得小学课上介绍的北斗七星；但完全不认得星星也能够轻易地被漫天的繁星震撼。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2025/03/stargazing.webp" />
&lt;figcaption class="left" >露营地的星空&lt;/figcaption>
&lt;/figure>
&lt;p>看着看着就累了，于是钻进车里面，摇下车窗躺下来对着星空发呆。在车里面放了几首十分应景的音乐；这些唱得滚瓜烂熟的歌词在此情此景之下第一次产生了具体的含义。&lt;/p>
&lt;blockquote>
&lt;p>Cause all of the stars / are fading away&lt;/p>
&lt;p>Just try not to worry / we&amp;rsquo;ll see them some day&lt;/p>
&lt;p>Take what you need / and be on your way&lt;/p>
&lt;p>And stop crying your heart out&lt;/p>
&lt;p>&lt;em>&lt;strong>&amp;ndash; Stop Crying Your Heart Out, Oasis&lt;/strong>&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>之后顺便看了 F1 中国站的冲刺排位赛，看完后已经是 1 点半了，实在是太累，一闭眼就睡着了。&lt;/p>
&lt;h2 id="第三天返程">第三天：返程&lt;/h2>
&lt;p>为了第二天不用再花 2 个小时去 Beatty 充电，我晚上睡觉的时候没有开空调，而是把车窗开了一条缝。刚睡觉的时候气温在 15 度左右，车内很舒服；到了后半夜沙漠开始展现它可怕的一面，气温骤降，导致我在 5 点的时候被冷醒了。醒来后转念一想，不如直接起床去看日出。这意味着我晚上只睡了 3 个半小时，却要开 6 个小时的车回家；但我做想做的事的时候就是这么疯狂，从不考虑后果。于是，穿上所有最厚的衣服，收拾好床铺，把放在外面的物资全部收拾齐全，就出发了。&lt;/p>
&lt;p>大概 6 点到了 Zebriskie Point。这是死亡谷公认最好的日出观赏地，人出乎意料的多，以摄影佬居多，不到六点半就有接近 10 台相机架在三脚架上准备好拍摄了。日出时太阳将远处的高山染成金色，而山脚、山谷底部以及近景的金黄色岩石还处于所谓的蓝调时间内，冷暖对比鲜明，十分漂亮。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/gallery/preview/DSC03265.webp" />
&lt;figcaption class="left" >从 Zebriskie Point 看向染上金色的群山&lt;/figcaption>
&lt;/figure>
&lt;p>本来计划中这一天去 Golden Canyon 进行徒步，但由于起得太早，完全没有体力徒步，就此作罢，于是便准备返程。在离开死亡谷之前，沿着 CA-190 的方向走一段距离后转向右侧，在开了一段不短的距离，并不断爬升高度后，来到了最后一个观景点：Dante&amp;rsquo;s View。这里可以俯瞰整个死亡谷全貌，底下就是前一天去的 Badwater Basin。有意思的是，这个角度正是《星球大战：新希望》里拍摄塔图因星球上莫斯艾斯利（就是 Han Solo 首次出场的地方）全景的取景地，也算是给这里稍显平淡的景观增添了一番趣味。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2025/03/dantes-view.webp" />
&lt;figcaption class="left" >在 Dante's View 俯瞰 Badwater Basin&lt;/figcaption>
&lt;/figure>
&lt;p>从 Dante&amp;rsquo;s View 下去，就正式开始返程。回去的路程稍显无聊，在 CA-190 上绝大多数时间内前后没有一辆车，非常容易犯困，特别是在前一天睡眠时间严重不足的情况下——我就好几次差点合上眼。幸好有 FSD 全程驾驶，不然还是非常危险。&lt;/p>
&lt;p>6 个小时后，FSD 把我送回到了圣地亚哥，为这次接近完美的旅途划上了句号。&lt;/p></content></item><item><title>2023 年终总结</title><link>https://yeshe.ng/blog/posts/2023/12/2023-review/</link><pubDate>Sat, 30 Dec 2023 00:40:35 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2023/12/2023-review/</guid><description>2023 年终总结</description><content>&lt;p>又到了翻相册写总结的时候了。今年做了很多事，去了很多地方；遇到了很多新朋友，也告别了一些老朋友；有了新的目标，也多了新的迷茫。&lt;/p>
&lt;h2 id="照片中的-2023">照片中的 2023&lt;/h2>
&lt;h3 id="感染新冠">感染新冠&lt;/h3>
&lt;p>新冠成功防了三年半，终于在今年的五一假期的前几天感染了。到这个时候大家对疫情都已经麻木了，我也没带过口罩，因此感染也是迟早的事情。因为短时间内大量感染，只要在校医院发热门诊发现的阳性病例都强制要到近春园（就在学校里面）隔离。我在去校医院之前就知道这事，但是为了不感染室友就还是去了，然后不出所料被隔离了。&lt;/p>
&lt;p>隔离的环境还算可以，没有想象中的单人单间，而是和另一个同学被分到了一个双床房里面。隔离的待遇很好，除了进去的时候发的一个零食大礼包，每天的伙食也非常好，都是一肉两素、加一个鸡蛋、再加一杯饮料的配置。这几天胃口竟然完全没有影响，每天都吃得非常饱，到最后都不舍得走了。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/quarantine-food.webp" />
&lt;figcaption class="left" >隔离餐 · 2023/4/29&lt;/figcaption>
&lt;/figure>
&lt;p>感染期间其实并没有非常严重的症状，发烧两天就退了，第四天开始转阴。中间主要是咳嗽比较厉害，但做其他事情并没有很大的影响。第五天，隔离结束。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/covid-test.webp" />
&lt;figcaption class="left" >抗原检测 · 2023/5/3&lt;/figcaption>
&lt;/figure>
&lt;p>好了之后，逐渐感受到了新冠的后遗症，最主要的症状是心动过速，静息心率经常上到 90 - 100，到现在都还没有完全恢复。新冠确实是名不虚传。现在新加坡疫情严重，为了不要再有新的后遗症，我在人多的地方还是选择乖乖地带上口罩，毕竟健康第一，牺牲一点方便和舒适还是值得的。&lt;/p>
&lt;h3 id="九字班毕业">九字班毕业&lt;/h3>
&lt;p>因为大二的时候降转到了零字班，今年的毕业季我原来的同学就都毕业了。我借了同学的学士服，找了个时间跟室友和高中同学拍了毕业照。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/graduation.webp" />
&lt;figcaption class="left" >#608B 毕业照 · 2023/6/25&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/graduation-pku.webp" />
&lt;figcaption class="left" >在未名湖拍毕业照 · 2023/6/25&lt;/figcaption>
&lt;/figure>
&lt;p>室友一个个搬出宿舍的时候，不禁鼻子一酸。四年的时间这么快就过去了，原来每天醒来就能看见的面庞，之后还能见多少次呢？&lt;/p>
&lt;h3 id="暑研--新加坡之旅">暑研 &amp;amp; 新加坡之旅&lt;/h3>
&lt;p>今年 8 月，我来到新加坡在 NUS 进行 6 个月的暑研。这是我第一次出国，出发之前多少还是有点忐忑，选择新加坡也是因为生活饮食习惯比较接近，而且可以讲中文，比较容易适应。从广州出发，四个半小时就到了新加坡，其实和去北京差不了多少，完全没有国际旅行的感觉。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/sg-arrival.webp" />
&lt;figcaption class="left" >到达新加坡 · 2023/7/30&lt;/figcaption>
&lt;/figure>
&lt;h4 id="关于气候">关于气候&lt;/h4>
&lt;p>新加坡的气候非常有特点（槽点）。我刚来的时候是旱季，基本不怎么下雨，这种时候基本上过了十点之后太阳底下就没法呆了，晒一会儿就有灼痛的感觉；大概 11 月之后到了雨季，这时候又天天下雨，非常烦人。不过如果不下雨的话，新加坡的晚上非常舒服，不太潮湿的空气、轻柔的晚风以及 25 度左右的气温非常宜人。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/sg-sunset.webp" />
&lt;figcaption class="left" >家附近的日落 · 2023/9/26&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/sg-rain.webp" />
&lt;figcaption class="left" >圣诞节大雨 · 2023/12/25&lt;/figcaption>
&lt;/figure>
&lt;h4 id="到处玩玩">到处玩玩&lt;/h4>
&lt;p>新加坡的 work-life balance 很好，周六日和公共假期不用去实验室，于是就有很多时间到处溜达。和 LT 一起去了新加坡的几乎所有景点：滨海湾花园、鱼尾狮公园、植物园、国家博物馆、圣淘沙、樟宜机场、动物园、牛车水、小印度，等等。新加坡就这么点地方，逛到后面都没地方去了，只能重新去之前去过的地方逛，其实也挺无聊的。&lt;/p>
&lt;p>来之前买了个微单，拍了&lt;a href="https://yeshe.ng/blog/galleries/singapore">很多照片&lt;/a>。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/sg-cbd.webp" style="width:500px;max-width:100%" />
&lt;figcaption class="left" >新加坡 CBD（手机拍的）· 2023/8/7&lt;/figcaption>
&lt;/figure>
&lt;h4 id="各种活动">各种活动&lt;/h4>
&lt;p>新加坡的活动非常多。国庆日在滨海湾有烟花和空军阅兵，去现场凑了凑热闹。人实在是非常多，距离烟花三个小时基本上现场就都满了，过了一会儿就不允许人进来了。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/ndp-crowd.webp" />
&lt;figcaption class="left" >国庆日的人群 · 2023/8/9&lt;/figcaption>
&lt;/figure>
&lt;p>暑研正好赶上 F1 新加坡站的比赛，三天的比赛去了两天。比赛前一天参加了维修区，近距离参观了各个车队的 P 房，并且十分幸运地拿到了周冠宇的签名。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/singaporegp-selfie.webp" style="width:500px;max-width:100%" />
&lt;figcaption class="left" >F1 维修区参观 · 2023/9/14&lt;/figcaption>
&lt;/figure>
&lt;p>平安夜的时候去乌节路凑了个热闹，也是非常多人。其实新加坡没有什么圣诞的气氛，华人为主本来就不怎么过圣诞，另外在热带 20 多度的晚上过圣诞也没有圣诞的感觉。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/orchard-xmas.webp" />
&lt;figcaption class="left" >乌节路圣诞游行 · 2023/12/25&lt;/figcaption>
&lt;/figure>
&lt;h4 id="朋友们">朋友们&lt;/h4>
&lt;p>一到新加坡就赶上了何老师很长时间以来组织的第一次聚会，在老师家的 condo 烧烤。（全是中国人的组也太友好了😜）&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/xtra-party.webp" />
&lt;figcaption class="left" >何老师家聚会 · 2023/8/5&lt;/figcaption>
&lt;/figure>
&lt;p>我并没有和组里面的同学在同一个实验室里面，而是坐在提供 funding 的 Institute of Data Science。除了可以自由摸鱼（划掉）之外，还认识了一群非常可爱的师兄师姐们（是的，我是实验室里面严格最小的）。非常幸运能够有这么一群哥哥姐姐们帮我融入新加坡的生活，带我到处吃喝玩乐，为我提供帮助和建议，这也让我这个社恐对之后的求学之路中的社交多了一点信心。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/ids-birthday-party.webp" />
&lt;figcaption class="left" >IDS 生日会 · 2023/8/29&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/12/ids-gathering.webp" />
&lt;figcaption class="left" >IDS 年会 · 2023/12/21&lt;/figcaption>
&lt;/figure>
&lt;h2 id="关于出国暑研">关于出国/暑研&lt;/h2>
&lt;p>今年过年的时候下定了决心出国读研。之前有过几次出国的打算，但最后都因为还对保研心存幻想而无疾而终。我其实是不太喜欢走出舒适区的。倾向于保研，很大程度上是因为出国的准备非常麻烦，而且要面对一个完全陌生的环境。但是，原地踏步的绩点以及对这种过于功利的内卷的厌恶让我不得不考虑其他选择。我并不太想去国内其他地方（比如中科院或者上交）读博，除了内心还是有学历崇拜的思想作祟，也是出于这样一种心理：我在清华已经看到了国内学校的天花板，那么究竟比清华更好的学校是怎么样的？于是，就决定出国看看。&lt;/p>
&lt;p>之后的大半年基本都是在围绕出国而做准备。考雅思比较轻松，虽然考口语还是非常紧张，最后也只有 6.5，但是总分 7.5 基本上所有的项目都够了。GRE 一直拖着没考，到最后发现 ETH 今年不需要 GRE 了，其他要求 GRE 的项目也不多，就懒得学了。&lt;/p>
&lt;p>因为想要一封海外的推荐信，联系了 NUS 的何老师做暑研，老师要求实习 6 个月以上，想了想下学期如果休学不上课的话也能毕业，就这么定下来了。暑研的收获基本上符合我的预期。我对暑研的期望是经历一段完整的科研，并且对读 PhD 有更深入的感受和认识。我和 Qinbin 在 2 个月内弄出了一篇 LLM 相关的 paper，算是有了第一段完整的科研经历。有意思的是，这反而让我坚定了以后不搞 AI 的决心。这个领域的科研和我的想象相去甚远，我还是更加喜欢踏踏实实地去实现自己想要的东西的感觉——这是我在这次科研中没有体会到的。&lt;/p>
&lt;p>对于要不要读 PhD 的问题，我跟很多人聊过，也算是体验过了 PhD 的生活，但是最后还是没有一个定论。我还没有在心理上准备好进行高强度的研究，也不一定喜欢做研究，而且还没有完全找到自己感兴趣的方向。所以，先读个 Master 或许是最合理的选择。&lt;/p>
&lt;h2 id="关于游戏">关于游戏&lt;/h2>
&lt;p>今年玩游戏挺多的。年初买了一台 Xbox Series S 来玩一些 3A 大作，后来又买了一台 3DS 重温任天堂平台上的旧作。&lt;/p>
&lt;p>这些是今年新玩的游戏，大致按照喜爱程度排序：&lt;/p>
&lt;p>&lt;strong>塞尔达传说：王国之泪&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：Switch&lt;/li>
&lt;li>时长：150h&lt;/li>
&lt;/ul>
&lt;p>成功超过已经是神的前作，在我心目中是我玩过游戏里面的 No.1。究极手、通天术和时间回溯使得与世界的交互在旷野之息基础上更加富有想象力，剧情的情感共鸣更是系列最佳。最终战我是一边颤抖一边哭着打完的，有段时间的一大乐趣是看各路主播在通关时泪奔的场景。&lt;/p>
&lt;p>&lt;strong>博德之门 3&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：PC&lt;/li>
&lt;li>时长：100h (未通关)&lt;/li>
&lt;/ul>
&lt;p>非常好玩，不仅是因为自由度极高的玩法，极度充实的内容和精细打磨的剧情更是让人惊叹，无愧于年度游戏。玩的时候想得最多的是：这么复杂、这么细致的游戏怎么可能做得出来？&lt;/p>
&lt;p>&lt;strong>幽灵诡计&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：3DS (NDS)&lt;/li>
&lt;li>时长：约 12h&lt;/li>
&lt;/ul>
&lt;p>玩法和剧情完美统一的神作，游戏节奏非常好，美术设计无可挑剔。剧情几度转折，最后一刻的反转让人潸然泪下。&lt;/p>
&lt;p>&lt;strong>塞尔达传说：众神的三角力量 2&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：3DS&lt;/li>
&lt;li>时长：约 20h&lt;/li>
&lt;/ul>
&lt;p>玩的第一部 2D 塞尔达，能够进入墙壁的设计使得解谜设计多了一个维度，非常惊艳。难度还行，就是最后打盖侬的时候手快打断了。&lt;/p>
&lt;p>&lt;strong>超级马力欧兄弟：惊奇&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：Switch&lt;/li>
&lt;li>时长：5h (未通关)&lt;/li>
&lt;/ul>
&lt;p>用任亏券换的游戏，和之前的马力欧相比耳目一新，但在开始玩博德之门之后就搁置了。&lt;/p>
&lt;p>&lt;strong>皮克敏 4&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：Switch&lt;/li>
&lt;li>时长：约 10h (未通关)&lt;/li>
&lt;/ul>
&lt;p>皮克敏 3 我玩了一会就没完了，这次皮克敏 4 画面玩法升级了之后更能玩下去了。游戏本身很有意思，但游戏重复的游玩要素不少，我也实在是没有耐心全收集，没有通关。&lt;/p>
&lt;p>&lt;strong>Hi-Fi Rush&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：Xbox&lt;/li>
&lt;li>时长：5h (未通关)&lt;/li>
&lt;/ul>
&lt;p>音游玩法的动作游戏，很惊艳。&lt;/p>
&lt;p>&lt;strong>极限竞速：地平线 5&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：Xbox&lt;/li>
&lt;li>时长：56h&lt;/li>
&lt;/ul>
&lt;p>最好的赛车游戏，手柄震动的体验非常让人沉浸。开放世界赛车游戏的概念挺有意思，不过世界并没有和赛车比赛真正做到融合。通关之后没有什么动力继续折腾改装之类的东西，就没再玩了。&lt;/p>
&lt;p>&lt;strong>逆转裁判 3&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：3DS&lt;/li>
&lt;li>时长：约 10h (未通关)&lt;/li>
&lt;/ul>
&lt;p>在 3DS 上体验双屏的逆转裁判，但玩着确实是太累了，过了一个案件后就搁置了。&lt;/p>
&lt;p>&lt;strong>High On Life&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>平台：Xbox&lt;/li>
&lt;li>时长：11h&lt;/li>
&lt;/ul>
&lt;p>噱头是枪会说话，游戏其实一般，没有什么新意。&lt;/p>
&lt;h2 id="关于音乐">关于音乐&lt;/h2>
&lt;p>今年还是没有听什么新歌。随着我年龄增大，我喜欢的歌手/乐队都在老去，越来越难听到他们的新歌了。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2023/12/music-records.webp" alt="">&lt;/p>
&lt;h2 id="总结的总结">总结的总结&lt;/h2>
&lt;p>今年第一次出了国。“到国外去见识不一样的世界”，意味着有机会去见识和之前的经历完全不同的东西，去体验其他文化，理解其他社会的运行逻辑，观察其他国家的运行方式，和具有不同文化背景的人打交道。这往往会带来新的世界观、人生观。&lt;/p>
&lt;p>比如，在了解了新加坡的建国历史之后，在对新加坡的国家治理感到赞叹的同时，我反而更对治理中国这样一个幅员辽阔的国家的巨大难度和所面临的挑战有了新的认识。又比如，在和一个新加坡出生的华人大爷聊到中国的发展时，他对“中国永远不可能在科技上超过美国”的坚持让我重新思考，我究竟要在国家发展的过程中扮演什么角色？&lt;/p>
&lt;p>在新加坡的这段经历的另一个意义是，在某种程度上第一次离开了清华、特别是贵系的环境，去拥有一种不一样的心态和眼光来审视自己的未来。来这里半年，我已经忘记了之前对于与同辈对比之下的焦虑和被迫内卷的迷茫，也意识到人生远远不止一条“正确”的路。&lt;/p>
&lt;p>2024 年，我想去更多的地方看看，去体验不一样的生活，去更加深刻地理解这个世界。&lt;/p>
&lt;p>今年 12 月交完申请之后，申请的结果其实就已经决定了。明年的现在，我会在哪里写 2024 年的年终总结呢？&lt;/p></content></item><item><title>2023 F1 新加坡大奖赛体验</title><link>https://yeshe.ng/blog/posts/2023/09/singaporegp/</link><pubDate>Tue, 19 Sep 2023 22:34:57 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2023/09/singaporegp/</guid><description>第一次现场观看 F1，非常圆满的 3 天体验</description><content>&lt;p>看完 Netflix 的 Strive to Survive 之后就开始看 F1 比赛了，正好今年新加坡站的比赛在我的暑研时间段内，就决定去现场体验一次比赛。&lt;/p>
&lt;p>最开始只买了周日的 Premier Walkabout 票，价格是 418 新币。这张票能够在全场自由走动，好处是便宜（除了只能在 Zone 4 活动的 Walkabout 票之外最便宜的），但缺点是是不能上看台，也就意味着没有固定的座位。这带来了一些不好的体验，下面会详细展开。Anyway，买到票之后就确定可以去现场看了，然后就开始关注相关的活动。&lt;/p>
&lt;h2 id="周四pit-lane-experience">周四：Pit Lane Experience&lt;/h2>
&lt;p>逛大奖赛官网的时候看到有一个 Pit Lane Experience 的活动，只针对新加坡居民和长期签证的持有者，抽中的人可以免费在周四晚上到维修区和赛道上参观。我参加了抽奖，但没有中。开奖的当天不太甘心，google 了一下发现新加坡最常用的二手交易网站 Carosell 上有很多人在卖票（这其实违反了不能转让或出售门票的规定，但是其实最后并没有任何的检查），就着手买一张票。&lt;/p>
&lt;p>Carosell 和闲鱼不一样，是类似于 ebay 的竞价平台，也就是说上面要买到票不只是看卖家想要卖多少钱，还要看其他人愿意出多少钱。一开始的价格还算合理，卖家标的价格大概 S$50 一张，但随着大量买家的竞价，最高的时候炒到了 S$200 一张。这价格就是明抢了，我就没有再尝试买票。之后在小红书上面看到有人抽到票后想找人一起去（抽到的人有 2 张票），就尝试私信找个愿意带我去的人，但是最后都没有回复。&lt;/p>
&lt;p>在活动的前一天，Carousell 上面之前联系过的一个卖家突然发消息说可以以 S$85 的价格卖给我，虽然仍然偏高，但是想着机会难得，就买下来了。&lt;/p>
&lt;p>当天提前一个小时到现场排队，人已经非常多了。进场之后被告知不能直接进入维修区，继续排队等了一个半小时之后才终于允许进入（这是我唯一一个对新加坡站组织不满意的地方）。放开入场之后前面的人都冲着进场，以期待在人少的时候能够遇到一些车手/占据靠近赛车的位置。&lt;/p>
&lt;p>我是最前面冲进去的，到维修区的时候基本上没什么人。快速地走过十家车队的 P 房，看到了阿隆索和汉密尔顿在房间里面。我当时觉得他们应该不会出来和车迷互动，拍了几张照就继续往前走了。尽头是红牛的 P 房，维斯塔潘和佩雷斯都不在，但是正好车队在练习换胎。他们练习的方式是：几个人推着赛车往前，模拟比赛时赛车的行进，到了换胎区之后就是正常的换胎过程。换胎的动静比想象中大得多，把我吓了一跳。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/gallery/preview/DSC01262.webp" />
&lt;figcaption class="left" >红牛换胎练习&lt;/figcaption>
&lt;/figure>
&lt;p>看完换胎后近距离观察了下各家 Pit Wall 的设备，因为不能靠太近，拍了几张照就走了。之后翻到威廉姆斯设备的照片的时候发现了一个有意思的地方：他们有专门的监听阿罗和哈斯无线电的频道：&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-pitwall.webp" />
&lt;figcaption class="left" >威廉姆斯偷听阿罗和哈斯的无线电&lt;/figcaption>
&lt;/figure>
&lt;p>走了一个来回，突然听到阿罗的 P 房传来一阵尖叫，一看是周冠宇出来了，马上从书包里拿出纸和笔冲过去。因为我距离近而且阿罗人气确实比不上大车队，我挤了一下就挤到了最前面。我挤到前面去的时候小周已经走到另一边了，本来已经对拿到签名不抱期望，但是他到另一侧签完名之后又往这边走过来了。我一边喊着“冠宇，冠宇”，一边&lt;del>利用手长的优势&lt;/del>递出了笔，他看到之后就拿起笔签了名。我当时还算镇定，跟他说了声“恭喜”，因为他刚在几个小时之前成功续约。他非常亲和，认真地回了句“谢谢”，之后走开了。这时候我满脑子都是“钱没白花！”。旁边有一花钱买票的哥们在成功合照之后喊着“It&amp;rsquo;s not S$50, it&amp;rsquo;s worth S$5000!”，我听到之后跟着笑了起来。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-zhou-autograph.webp" />
&lt;figcaption class="left" >周冠宇的签名&lt;/figcaption>
&lt;/figure>
&lt;p>之后汉密尔顿和勒克莱尔陆续出来签名，因为围在周围的人非常多，而且我有小周签名已经非常满足了，就只是去附近凑了下热闹。&lt;/p>
&lt;p>之后来回逛了几圈，因为人实在是太多了，就出到赛道外面了。这时遇到威廉姆斯的人正在把赛车推到指定地点给 FiA 检查，我就跟着他们走了一段路，近距离地观察了赛车，还跟他们说了声“Good luck”。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-williams-car.webp" />
&lt;figcaption class="left" >威廉姆斯赛车&lt;/figcaption>
&lt;/figure>
&lt;p>因为实在是热得慌，并且已经很累了，就走了出去，在一号弯坐了下来。一号弯非常漂亮，地上是大面积的新航蓝黄白配色的油漆，天上高架桥横穿而过，配上金黄色的打光，非常特别。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-on-turn1.webp" />
&lt;figcaption class="left" >一号弯&lt;/figcaption>
&lt;/figure>
&lt;h2 id="周六三练演唱会--排位赛">周六：三练，演唱会 &amp;amp; 排位赛&lt;/h2>
&lt;p>周五的练习赛我是在家看的直播。到周六的时候憋不住，就坐地铁到了赛道附近，一方面是想找一下有没有好的免费观赛点，一方面是想在最后时刻淘到一些低价票。&lt;/p>
&lt;p>怀着一丝幻想，我走到了横跨一号弯附近赛道的 Benjamin Sheares Bridge，但人行道果不其然被封上了。走这一趟倒不是完全没有收获，在这附近刚好可以看到里面车手的 Meet and Greet 环节的大屏幕，就停下来听了几分钟。&lt;/p>
&lt;p>之后我在闲鱼上刷到了一个 600 多块的 zone 4 walkabout 票（原价大约 1100 块），价格非常好，想着既然都来现场了，稍微砍了砍价就买下，接着就进场了。我的第一次 F1 体验就这样提前了一天。&lt;/p>
&lt;p>Zone 4 票的好处是非常便宜，但是缺点是没有太多很好的观赛点。想起之前在 Reddit 上看到的 7 号弯是高速直道后的弯道，比较好看，就到了这儿的平台。Walkabout 票是没有座位的，只能和其他人挤在一些观赛平台上看比赛。观赛平台很小，而且只有很矮的几层，在练习赛开始前就挤满了人，我们只能站着，又热又累。旁边的看台上只坐了大概一半的人，但我们不能上去，只能羡慕地干看着。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-turn7.webp" />
&lt;figcaption class="left" >7 号弯的观赛平台&lt;/figcaption>
&lt;/figure>
&lt;p>练习赛开始之后发现这个位置其实很不理想。一方面只能看到弯道的很小一部分，另一方面没有屏幕，无从得知赛道其他地方的情况。不过，第一次看到 F1 赛车在赛道上飞驰是很震撼的：震耳欲聋的引擎声，轮胎融化的橡胶味，以及观众们的欢呼声，都是现场独一无二的体验。&lt;/p>
&lt;figure class="center" style="width:100%;">
&lt;video style="object-fit:fill;max-width:100%" controls preload="metadata">
&lt;source src="https://yeshe.ng/blog/2023/09/f1-turn7.mov#t=0.1" type="video/mp4">
&lt;/video>
&lt;figcaption class="left">练习赛&lt;/figcaption>
&lt;/figure>
&lt;p>一个小时的练习赛结束，我决定换个地方看晚上的排位赛。经过 Padang Stage 的时候 Kings of Leon 的演唱会刚好开始，想着应该不需要这么早去赛道占位置，就进去了。在现场演唱会的音浪、震动和攒动的人头下，我们即使不会跟着唱，没听过，甚至不一定欣赏得来那些歌曲，却都在缓缓降临的夜幕下五光十色的舞台融为一体，不约而同地尽情摇摆。新加坡站的特点是大牌云集的演唱会，它们将比赛推向了一个更加令人沉浸和放松的高潮。&lt;/p>
&lt;figure class="center" style="width:100%;">
&lt;video style="object-fit:fill;max-width:100%" controls preload="metadata">
&lt;source src="https://yeshe.ng/blog/2023/09/f1-concert.mov#t=0.1" type="video/mp4">
&lt;/video>
&lt;figcaption class="left">夜幕下的 Kings of Leon 演唱会&lt;/figcaption>
&lt;/figure>
&lt;p>排位赛我选的位置是 12 号弯附近、进入 Anderson Bridge 前的平台。这里可以看到 11 和 12 两个弯，并且正对一个大屏幕，可以很清晰地看到排位赛的情况。车手在 11 号弯刹车，加速通过 12 号弯并一路穿过 Anderson Bridge，观赏性十足。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-turn12.webp" />
&lt;figcaption class="left" >12 号弯的观赛平台&lt;/figcaption>
&lt;/figure>
&lt;p>现场的车迷组成很有意思，大部分是法拉利和梅奔的车迷，红牛的车迷不少但是明显少于另两强。红牛的垄断地位让他们自然成为了公敌，当红牛在 Q2 中双车被淘汰时，现场爆发了比赛期间最响亮的掌声。此外，斯托尔撞车、角田短暂排第一和赛恩斯夺杆时现场也是一片热闹。虽然在现场对比赛进程的认知不如看直播，但是伴随着赛车驶过声浪的热烈气氛是只有在现场才能享受的体验。&lt;/p>
&lt;p>排位赛结束后有 Post Malone 的演唱会，我没什么兴趣，把票挂在 Carosell 上面，很快就以 S$50 卖出去了，这样一来这张票（等于花了 300 多块钱）拥有了无敌的性价比。&lt;/p>
&lt;h2 id="周日正赛">周日：正赛&lt;/h2>
&lt;p>周日的门票是 premier walkabout，可以在所有区域内活动。由于之前新加坡站一号弯附近出现过很多事故，因此就选择了靠近维修区出口和一号弯的平台。这个平台能看到主直道末端、维修区出口、一号弯以及二和三号弯的部分。这个位置不仅视野好，而且头顶就是立交桥，下午的大部分时间在桥的阴影下，同时正对面就有一块大屏幕。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-turn1.webp" />
&lt;figcaption class="left" >1 号弯附近的观赛平台&lt;/figcaption>
&lt;/figure>
&lt;p>有了前一天的经验，我在入场的最早时间之后大概 15 分钟就到了现场。距离比赛还有五个多小时，但是平台已经几乎快被占满了。看来不只是中国人在卷 :)&lt;/p>
&lt;p>到比赛快开始的时候发生了一些冲突。一些后到的观众钻到了前后两排的空间里面，这样一来在那里占了一下午座的观众不乐意了，吵着让他们走开。双方争执了很久，期间有人一直在礼貌地劝双方冷静，最后没有变成太大的冲突，以后来者的离开而结束。这就是 walkabout 票最大的缺点：没有固定的座位，比较好的观赛位置必须早早去占座，导致无法在比赛前去享受各种各样的活动。&lt;/p>
&lt;p>比赛开始之前我跟左边的一群统一着装的红牛车迷聊了起来。他们是维斯塔潘的老乡，跟着他参加了很多场大奖赛。跟其中一个车迷聊到红牛前一天不太顺利的排位赛，他脸色一下子就沉了下去。我赶紧打趣说根据传统没准今晚两辆法拉利撞一起 Max 就能拿冠军了，我们一起哈哈大笑起来。&lt;/p>
&lt;p>正赛之前赛道上有两场低级别的车赛和车手巡游来消磨时间，五个小时很快就过去了。之后就是本赛季最精彩的一场比赛了：角田退赛、萨金特撞车安全车出动、维斯塔潘连续被超车、维斯塔潘连续超车回去、阿隆索超长的 20 多秒换胎、史诗般的前四名追逐战、拉塞尔最后时刻的撞车。赛恩斯冲线、烟花升起的一刻，所有人无论是否支持法拉利都在为他的完美表现欢呼鼓掌，沉浸在夜色下完美的氛围中回味着比赛，久久没回过神来。&lt;/p>
&lt;figure class="center" style="width:100%;">
&lt;video style="object-fit:fill;max-width:100%" controls preload="metadata">
&lt;source src="https://yeshe.ng/blog/2023/09/f1-ending.mov#t=0.1" type="video/mp4">
&lt;/video>
&lt;figcaption class="left">Carlos Sainz P1!&lt;/figcaption>
&lt;/figure>
&lt;p>离场的时候，我追上之前聊天的红牛老哥，说“Welcome to China next year!”他虽然略有失望，但还是开心地说“Sure!”。我无比期待明年的中国大奖赛，再一次与这些素不相识但志同道合的朋友们一起享受 F1 给我们带来的兴奋、刺激与感动。&lt;/p></content></item><item><title>记一次溜入 F1 新加坡大奖赛封闭区域的经历</title><link>https://yeshe.ng/blog/posts/2023/09/singaporegp-intrusion/</link><pubDate>Sat, 09 Sep 2023 19:36:40 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2023/09/singaporegp-intrusion/</guid><description>在 2023 年新加坡大奖赛比赛日前一个星期前成功溜入已经封闭的 F1 Village，而且在赛道和维修区大摇大摆地逛了一圈的神奇经历</description><content>&lt;h2 id="起因">起因&lt;/h2>
&lt;p>自从一个月前通过网飞的纪录片入坑 F1 之后，亲身体验 F1 就加入了我的“不做就会遗憾终身”的列表中（列表的上一个是去 Coldplay 的演唱会，这个愿望如不出意外将在明年离开新加坡前达成）。正好新加坡站的比赛在 9 月进行，于是就买了张周日的门票。我总是觉得只在观众区看比赛不够特别，当看到官方组织的在比赛日前参观维修区通道（pit lane）的活动之后就兴致勃勃地参与了抽奖。&lt;/p>
&lt;p>可惜最后没有中奖。不甘心的我打算去现场转转，而我本来也打算这周末去现场考察一下下周比赛的观赛位置，于是在开了一个实验在后台跑之后就出发去现场了。&lt;/p>
&lt;h2 id="封闭的赛道">封闭的赛道&lt;/h2>
&lt;p>一出地铁站（Promenade 站）就感受到了大奖赛临近的氛围。新加坡站是街道赛，很大一部分赛道是平时的街道，因此这一些街道有明显的特点：道路两侧有非常高的防护栏，防护栏上每隔几米就有一张反光带。随着比赛日的临近，街道附近也开始搭建一些临时设施，如看台、灯光和遮挡没买票的人视线的围栏。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-street.webp" />
&lt;figcaption class="left" >用作赛道的街道，已经搭好了看台，防护栏倒是永久设施&lt;/figcaption>
&lt;/figure>
&lt;p>虽然这些街道不可能提前一个星期就封起来，但是赛道的核心部分——主直道、维修区和围场（Paddock）是早早（8月底）就用围栏封闭、不对外开放的。我一开始想尝试进入，但是看到门口有保安看守，且没有发现明显的漏洞后，就作罢了。进入无果后我就只能去最近的新加坡摩天轮考察一下我打算去的观赛点附近的情况了。那里的角度倒是挺好，赛道看得很清楚，而且还有一段看台可以上去。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-paddock-entry.webp" />
&lt;figcaption class="left" >Paddock 入口，有若干个保安看着&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-flyer-grandstand.webp" />
&lt;figcaption class="left" >摩天轮附近的看台&lt;/figcaption>
&lt;/figure>
&lt;h2 id="溜入封闭区域">溜入封闭区域&lt;/h2>
&lt;p>只是看一眼赛道当然算不上什么收获，趁着时间还早，我沿着围栏，想碰碰运去看能不能进去。一路上遇到了两个有保安的门口，我抱着试一试的心态问保安能不能进去，在知道我不是里面的 staff 之后都很礼貌地拒绝了我。我也不好意思进一步试探，两次都乖乖地离开了。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-satellite.webp" />
&lt;figcaption class="left" >两个入口，下面的是三号弯的出口&lt;/figcaption>
&lt;/figure>
&lt;p>一路走到 Nicoll Highway 地铁站，我本来已经有点疲惫，要进站回去了，但是还是不甘心，又往回走再找一找有没有机会。&lt;/p>
&lt;p>在走到上面图片靠上的圆圈的入口，我终于发现了破绽。这个入口的右边是一条机动车的地下通道，没有人看着，与入口之间由路障隔着，而沿着通道走几步有一些路障之间有空隙，我趁着保安不注意（他们应该根本没有注意这个地方），一侧身就进去了。我撑着伞走了一段路，心脏狂跳，又不敢回头看，好在没有人发现异常。就这样，我成功进入了封闭的区域。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-intrusion-route.webp" />
&lt;figcaption class="left" >溜进去的路径&lt;/figcaption>
&lt;/figure>
&lt;h2 id="赛道维修区与围场">赛道、维修区与围场&lt;/h2>
&lt;p>封闭区域里面的所有人都挂着证件，我心惊胆战地跟着前面的人走。后来发现其实根本没有人管证件的事儿，我胆子就大了起来。&lt;/p>
&lt;p>我进去之后的地方是维修区对面的看台，而我最想去的地方是维修区和它后面的围场。我在防护栏外绕着赛道走了一会儿（这也是比赛时观众活动的区域），走到了和外界的隔离处也没找到进入维修区的合法路径，只好回头。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2023/09/f1-outside-fence.webp" />
&lt;figcaption class="left" >防护栏外&lt;/figcaption>
&lt;/figure>
&lt;p>回去的路上发现最后一个弯道的防护栏是开着的（应该是为了让吊车进入赛道），四处张望发现附近人不多之后，我迅速地穿过赛道，终于来到了维修区。&lt;/p>
&lt;figure class="center" style="width:100%;max-width:500px;">
&lt;video style="object-fit:fill;width:100%" controls preload="metadata">
&lt;source src="https://yeshe.ng/blog/2023/09/f1-on-track.mov#t=0.1" type="video/mp4">
&lt;/video>
&lt;figcaption class="left">走入赛道&lt;/figcaption>
&lt;/figure>
&lt;p>维修区摆满了集装箱，里面是各种配件、器材（赛车本身应该还没运来），不过现场车队的人员还没来，只有本地的工作人员在进行装修。维修区后面是围场，现在已经布置好了鲜花，不过建筑里面还没有东西。&lt;/p>
&lt;p>在里面走一圈，想象着在这些地方发生过的那么多戏剧性的事件，还是很有感觉的。&lt;/p>
&lt;figure class="center" style="width:100%;max-width:500px;">
&lt;video style="object-fit:fill;max-width:100%" controls preload="metadata">
&lt;source src="https://yeshe.ng/blog/2023/09/f1-pit-lane.mov#t=0.1" type="video/mp4">
&lt;/video>
&lt;figcaption class="left">维修区&lt;/figcaption>
&lt;/figure>
&lt;figure class="center" style="width:100%;max-width:500px;">
&lt;video style="object-fit:fill;max-width:100%" controls preload="metadata">
&lt;source src="https://yeshe.ng/blog/2023/09/f1-paddock.mov#t=0.1" type="video/mp4">
&lt;/video>
&lt;figcaption class="left">围场&lt;/figcaption>
&lt;/figure>
&lt;p>本来还想到一号弯附近看一下，不过那里在施工的人比较多，最后没有过去。没有什么其他的地方可以看了，就走到围场的出口，疲惫但是满意地离开了。&lt;/p>
&lt;p>下周比赛见！&lt;/p></content></item><item><title>2022 年的一些记忆</title><link>https://yeshe.ng/blog/posts/2022/12/2022-clips/</link><pubDate>Fri, 30 Dec 2022 22:38:09 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/12/2022-clips/</guid><description>2022 年对于我来说波澜不惊，但好像又有很多不愿丢弃的记忆……</description><content>&lt;p>2022 年对于我来说波澜不惊，似乎不值得像去年一样写一篇“年终总结”，那就用流水账的形式把这普通但又不平凡的一年作一个小小的总结。&lt;/p>
&lt;h2 id="编程">编程&lt;/h2>
&lt;p>编程时间从去年 12 月开始用 &lt;a href="https://wakatime.com">wakatime&lt;/a> 记录，到现在正好一年。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/12/programming-time.webp" alt="">&lt;/p>
&lt;p>今年仍然是“码力全开”的一年，但更多是完成课内繁杂且繁重的大作业以及做一个目前为止半死不活的科研，并不像去年（和更早之前）那样大部分时间用在自己感兴趣的一些杂七杂八的大小玩意上。主要投入的有：&lt;/p>
&lt;ul>
&lt;li>春季学期
&lt;ul>
&lt;li>软件工程：一个动态配置管理系统&lt;/li>
&lt;li>人工智能导论：输入法、四子棋、情感分类&lt;/li>
&lt;li>搜索引擎技术基础：一个简陋的图片搜索引擎&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>秋季学期
&lt;ul>
&lt;li>计算机组成原理：Risc-V 五级流水 CPU，支持 uCore 操作系统&lt;/li>
&lt;li>编译原理：一个简单的编译器&lt;/li>
&lt;li>数据库系统概论：一个支持基本 SQL 的 DBMS&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/ucore.webp" />
&lt;figcaption class="left" >年度最佳瞬间：验收前 1 小时把 bug 完全修好，uCore 跑出来了&lt;/figcaption>
&lt;/figure>
&lt;p>虽然这些大作业大部分都是“标准动作”，并没有很多自由发挥的空间，但在其中的收获感觉非常多，比如：&lt;/p>
&lt;ul>
&lt;li>写了/看了几百个小时 C++，对 C++ 的理解深入了很多&lt;/li>
&lt;li>在软件工程/科研中学到了不少工程上的规范，并在写数据库的时候实践了一番&lt;/li>
&lt;li>造机补全了对硬件的理解空白，以前遇到的很多关于底层的困惑豁然开朗&lt;/li>
&lt;/ul>
&lt;p>我觉得，这些课程最重要的作用是对我代码能力的进一步提高。之前自己瞎搞的很多程序，包括 Whiz 和树洞 app，要么没有工程上的规范，要么对计算机系统不够理解而导致写出很多性能上完全不靠谱的代码。在这学期写数据库的过程中，明显感受到开发过程更加严谨规范，性能上也更有把握。&lt;/p>
&lt;p>虽然话是这么说，但是这些大作业没有一个是好啃的骨头。客观上说，某些课程一言难尽的文档是造成这一结果的重要原因，但在写这些东西的过程中，在偶尔获得成就感的同时，的确不时感受到一种无力感，内心的想法在这几个状态间不断转移：自己怎么什么都不懂，我真菜/为什么大佬们都能自己搞懂，我真菜/啊我自己搞明白了，我真牛X/啊我实现出来了，我好牛X……现在看来，自己水平还是有的，但也的确和同辈的最高水平有着相当的差距（当然，这是每个清华学生最终都要学会承认的 :)&lt;/p>
&lt;h2 id="学习与科研">学习与科研&lt;/h2>
&lt;p>学习上，一句话：想象不到自己能这么摆。不说了，总之就是一塌糊涂，课没怎么去上，但好像该理解的基本上都理解了……&lt;/p>
&lt;p>没准这就是大学学习呢……&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/db-course.webp" />
&lt;figcaption class="left" >《清华大学同学在睡觉》PS：冯老师讲课不差，就是周一早八实在是阴间&lt;/figcaption>
&lt;/figure>
&lt;p>另外，由于自己时间管理实在是太烂，科研已经拖了两个月没有动了，也的确不知道该如何继续下去，等考完试再慢慢捋一捋吧……&lt;/p>
&lt;h2 id="游戏">游戏&lt;/h2>
&lt;p>自从今年暑假上写沟进行了&lt;a href="https://yeshe.ng/blog/posts/2022/07/media-game/">关于游戏主题的研究&lt;/a>之后，之前一直以不打游戏的好学生自居的我突然走上了玩游戏的不归路。本来不怎么玩游戏要归功于我的国行 Switch 买不到什么游戏 + 主力机是 Mac，但自从暑假买了台日版的 Switch Lite（之后又淘了一台二手的普通版 Switch）之后，一切都变了。&lt;del>是时候给年度总结加上游戏了。&lt;/del>&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/games.webp" style="max-width:100%;width:400px" />
&lt;/figure>
&lt;p>今年主要玩的游戏有：&lt;/p>
&lt;ul>
&lt;li>Splatoon 3：之前很早就被它吸引，“通过墨水占地的射击游戏”这一概念对于习惯于传统射击游戏的玩家来说非常新颖，实际上手之后更觉得惊艳。占据了我绝大部分空闲时间的罪魁祸首。&lt;/li>
&lt;li>Baba is You：一款颠覆游戏玩法的神作：通过改变游戏的规则，而不是在规则的约束下达成目标，非常考验思维，很难，现在大概通关了 70%。&lt;/li>
&lt;li>Super Mario Maker 2（马力欧制造2）：由于看桀哥直播的缘故，对 2D 马力欧产生了兴趣（之前只玩过 3D 的奥德赛）。可以玩一辈子的马里奥。&lt;/li>
&lt;li>逆转裁判：买的二手 Switch 上还保留着的数字版游戏，玩了几十个小时，案件推理十分精彩。之前对这种文字类游戏不太感冒，没想到这么好玩。可惜太花时间，只玩了一半左右。&lt;/li>
&lt;li>塞尔达传说：旷野之息：有了日版 Switch 之后把之前没能玩的 DLC 补了一下。&lt;/li>
&lt;/ul>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/splatoon-1.webp" />
&lt;figcaption class="left" >Splatoon 3&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/botw.webp" />
&lt;figcaption class="left" >100 年前的合照（泪目）&lt;/figcaption>
&lt;/figure>
&lt;h2 id="音乐">音乐&lt;/h2>
&lt;p>9 月的时候换了个 Apple Music 账户，所以只有几个月的数据：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/12/top-songs.webp" alt="">&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/12/top-artists.webp" alt="">&lt;/p>
&lt;p>今年常听的基本上都是老歌，相比于之前听粤语歌多了不少。之前总觉得粤语歌语调比较低沉，但现在越来越觉得有味道，可能是相比英文歌，还是母语的歌更贴近我的内心吧。另外，还听了很多 Sondheim 的音乐剧。&lt;/p>
&lt;p>换了美区的账户之后可以听一些被禁的歌曲了，包括之前很喜欢的、但由于支持港独被大陆封禁的 RubberBand，当时去香港露营现场听过他们的表演。我一直觉得在艺术本身不被用于政治用途的情况下，艺术不应与政治相关，它承载了人的不可封杀的记忆、情感。&lt;/p>
&lt;h2 id="一些值得留念的瞬间">一些值得留念的瞬间&lt;/h2>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/food-after-covid.webp" />
&lt;figcaption class="left" >6 月 15 日，学校解封后宿舍一起吃的第一次夜宵&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/travel-1.webp" />
&lt;figcaption class="left" >小七孔，疫情下看到这样单纯的场景竟有些恍惚&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/travel-2.webp" />
&lt;figcaption class="left" >镇远古镇&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/travel-3.webp" />
&lt;figcaption class="left" >梵净山&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/make-cpu.webp" />
&lt;figcaption class="left" >造机（在床上）开始！&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/ymy.webp" />
&lt;figcaption class="left" >圆明园“金秋游”——这竟然是我第一次去圆明园&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/lockdown-food.webp" />
&lt;figcaption class="left" >第一次封楼，分发的晚餐&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/leaving-school.webp" />
&lt;figcaption class="left" >凌晨五点半的 C 楼：疫情严重，被迫回家，又一个不完整的学期&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/airport-sunrise.webp" />
&lt;figcaption class="left" >当我们到达登机口的时候，刚好赶上日出——也许今天，一切都会好起来吧？&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/quarantine.webp" />
&lt;figcaption class="left" >在飞机上成为密接，从集中隔离点望向阳江一中&lt;/figcaption>
&lt;/figure>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/cooking.webp" />
&lt;figcaption class="left" >一个人困在家，被迫尝试自己煮饭——还不赖&lt;/figcaption>
&lt;/figure>
&lt;p>最后一个瞬间，留给梅西。我错过了他的巅峰，又见证了他的所有至暗时刻。如童话般的结局，是对他绝代无双的球技和人品一份迟到的礼物，我的青春也终于没有了遗憾。&lt;/p>
&lt;figure class="left" >
&lt;img src="https://yeshe.ng/blog/2022/12/messi.webp" />
&lt;/figure></content></item><item><title>中国主流媒体视角与游戏产业发展的割裂——以《人民日报》游戏相关报道（1989-2021）为例</title><link>https://yeshe.ng/blog/posts/2022/07/media-game/</link><pubDate>Thu, 28 Jul 2022 23:42:15 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/07/media-game/</guid><description>写作与沟通游戏主题长文终稿</description><content>&lt;p>&lt;em>本文为 2022 年夏季学期程祥钰老师《写作与沟通》课程游戏主题长文终稿&lt;/em>&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>摘要&lt;/strong> 主流媒体几乎参与了中国游戏产业发展的全过程，但其报道相较于游戏产业的发展而言存在着缺失和偏差，存在着明显的割裂。纵观中国游戏产业发展史，这种割裂长期存在，总体上在缓慢弥合，但最近又有重新加大的趋势。由于主流媒体宣传官方主张、传播主流声音的性质，其与游戏产业的发展存在着产生割裂的必然性，而游戏相关政策与主流舆论对割裂的产生具有重要作用。二者之间割裂的影响具有两面性，游戏产业的健康发展需要主流媒体保持适当的距离。&lt;/p>
&lt;p>&lt;strong>关键词&lt;/strong> 游戏；游戏产业；主流媒体；游戏报道；人民日报&lt;/p>
&lt;/blockquote>
&lt;p>主流媒体对游戏&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>的报道自其进入中国以来就一直存在。《人民日报》对游戏的间接报道最早可追溯到 1981 年&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup>；此后，随着中国游戏产业的不断发展，《人民日报》对游戏的直接报道数量基本呈递增趋势，目前游戏已成为其热门话题之一。可见，主流媒体几乎参与了中国游戏产业发展的全过程。&lt;/p>
&lt;p>然而，尽管主流媒体似乎并未缺席中国游戏行业的发展历史，且主流媒体的视角与中国游戏产业的发展大体上步调一致，但其在某些历史阶段的报道忽略了游戏产业发展的重要历程，且立场存在明显的偏向性；主流媒体的视角与游戏产业的发展之间存在着缺失和偏差。例如，对于 20 世纪 80 年代末到 21 世纪初之间的历史时期，在游戏产业发展史的叙事中，“20 世纪 90 年代新出现的游戏杂志群开始频繁讨论游戏中的文化议题，其核心主张便是如何生产作为文化抵抗媒介的国产游戏。”，自此产生了第一批国产游戏&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>；而在《人民日报》的叙事中，没有一篇文章提及此历史进程，反之在重复讨论“电子游戏热中的负效应”&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>、“电子游戏的暴力内容对孩子不利”&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>、“青少年沉迷电子游戏现象”&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>等游戏的负面形象。主流媒体的视角并不总是与游戏产业的发展相统一，二者之间存在明显的割裂。&lt;/p>
&lt;p>对于媒体报道对游戏的舆论评价所产生的影响，何威等在研究《人民日报》游戏报道的话语变迁时指出，“不论普罗大众、文化精英或政府官员，多少都依赖媒体报道提供素材、主题、框架、观点，来理解和对待游戏及游戏玩家”&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>。具体到主流媒体，汉娜·沃曼在研究西方媒体对中国游戏文化和商业的报道时指出，“官方媒体成为了一种精英象征，代表社会上‘最有发言权’的群体，也因此可以优先影响‘大众的思想’”&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup>。正因为《人民日报》等主流媒体反映了官方的态度，且有着巨大的舆论影响力，主流媒体的视角与游戏产业发展之间的割裂，以及这种割裂对中国游戏产业带来的深刻影响，值得关注和反思。&lt;/p>
&lt;p>本文将以《人民日报》以游戏为主题的文章&lt;sup id="fnref:9">&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref">9&lt;/a>&lt;/sup>为例，从中国主流媒体游戏报道话语演变及游戏产业发展的历史入手，揭示二者之间的割裂性，分析这种关系背后的影响因素，进而探讨这种关系对中国游戏产业的影响。&lt;/p>
&lt;h2 id="中国游戏产业发展的过程中主流媒体的视角看向何处">中国游戏产业发展的过程中，主流媒体的视角看向何处&lt;/h2>
&lt;p>中国游戏产业发展的历史可以清晰地划分为若干个阶段，每个发展阶段既有标志性的事件作为开端，又有明确的主题贯穿其中。《人民日报》的视角是否能够覆盖中国游戏发展的重大事件，其报道是否能够呈现当时游戏产业发展阶段的现状？我们以中国游戏发展的标志性事件为点、以发展阶段中的主要趋势为线，对《人民日报》游戏报道的视角进行分析。&lt;/p>
&lt;p>1981 年北京第一轻工业研究研制出 YQ-1 型游戏机，标志着当代中国游戏史的开端。虽然同年《人民日报》第一次在报道中提到了电子游戏，但距离《人民日报》第一篇以游戏为主题的文章发表还有 8 年时间。在这段时间里，中国游戏产业尚未开始全面发展；电子游戏以学习机的面目出现，其游戏的本质被压抑，与大众尚有一定距离，游戏并未产生足够的社会影响。因此，游戏产业并不足以得到主流媒体的关注，这导致了主流媒体叙事中这段历史中的缺失。&lt;/p>
&lt;p>1980 年代末，中国东南沿海逐渐具备规模化的任天堂兼容机生产能力，电子游戏开始成为社会现象&lt;sup id="fnref:10">&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref">10&lt;/a>&lt;/sup>，自然也逐渐被主流媒体所关注。《人民日报》对游戏的第一篇直接报道（1989 年）便出现在此时期：这篇报道聚焦上海大量游戏机涌现而导致学生沉迷的问题，呼吁有关部门加强管理&lt;sup id="fnref:11">&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref">11&lt;/a>&lt;/sup>，可见此时电子游戏已具有相当的社会影响。值得注意的是，此后 10 年《人民日报》对于游戏的 15 篇报道均停留在其消极的社会影响上，并未关注电子游戏本身或其背后产业的发展，直到1999 年才出现第一篇介绍电子游戏的文章&lt;sup id="fnref:12">&lt;a href="#fn:12" class="footnote-ref" role="doc-noteref">12&lt;/a>&lt;/sup>。可见，此时期《人民日报》的游戏报道虽然逐渐增多，但其视角极其局限。这一结论在同一发展阶段的另一个标志性事件及其带来的发展趋势中可以得到印证：1994 年 10 月，金盘公司游戏部发表首款国产游戏《神鹰突击队》；一年后，《电子游戏软件》一篇题为《乌鸦·乌鸦·叫：该谈大陆电子游戏业了》的文章&lt;sup id="fnref:13">&lt;a href="#fn:13" class="footnote-ref" role="doc-noteref">13&lt;/a>&lt;/sup>批评了电子游戏中中国文化元素的缺失，引发了游戏界的广泛讨论，从此直到 21 世纪初，游戏国产化成为了中国游戏产业发展的主旋律；然而，如前文所述，这一阶段《人民日报》的视角仍局限在电子游戏产生的影响上，对游戏产业这一关键发展历程完全没有涉及。在强调电子游戏的危害的同时忽视游戏本身及游戏行业的发展现状，主流媒体的报道几乎与游戏产业的发展完全割裂。&lt;/p>
&lt;p>2000 年 7 月到 9 月，全国开展电子游戏经营场所的专项治理，出台了持续 14 年之久的“游戏机禁令”&lt;sup id="fnref:14">&lt;a href="#fn:14" class="footnote-ref" role="doc-noteref">14&lt;/a>&lt;/sup>，从此中国失去游戏软件产业升级的硬件基础，转向代理海外网游的新模式，从此进入长达十几年的网游时代&lt;sup id="fnref:15">&lt;a href="#fn:15" class="footnote-ref" role="doc-noteref">15&lt;/a>&lt;/sup>。与之前的缺位不同，《人民日报》在此次转变中不仅在场，还起到了十分重要的推动作用：从 2003 年 3 月 3 日开始，《人民日报》以前所未有的频率关注游戏，在专项治理开始前的 3 个月连续刊发 8 篇关于游戏厅现状及其危害的文章，从时间上看这些报道导致了政策的制定和实行；随后的 4 个月中，《人民日报》又刊发了 4 篇文章介绍专项治理的效果；这一年，《人民日报》的全部 13 篇游戏报道均与专项治理相关。值得关注的是，虽然主流媒体的报道仍然局限在游戏的负面影响上，其在游戏产业的此次转折中扮演了一个完全不同的角色，在以往的旁观者、批判者之外，短暂地成为了游戏产业发展的推动者，尽管游戏产业本身在其报道中并未在场。&lt;/p>
&lt;p>2001 年开始，中国游戏厂商开始代理国外网络游戏，开启了中国的网游时代。《人民日报》在 2002 年和 2003 年分别对网易推出的国产新网游《大话西游》&lt;sup id="fnref:16">&lt;a href="#fn:16" class="footnote-ref" role="doc-noteref">16&lt;/a>&lt;/sup>和金山推出的新网游《剑侠情缘网络版》&lt;sup id="fnref:17">&lt;a href="#fn:17" class="footnote-ref" role="doc-noteref">17&lt;/a>&lt;/sup>进行的报道，是其最早对具体游戏的主题报道；2003 年，《人民日报》发表了一篇解读网络游戏的文章，详细介绍了网友相关的政策和产业发展情况，并强调辩证地看待游戏的影响&lt;sup id="fnref:18">&lt;a href="#fn:18" class="footnote-ref" role="doc-noteref">18&lt;/a>&lt;/sup>。虽然这只是这个时代开端的几篇报道，但却反映了这个阶段主流媒体与游戏产业关系的明显变化：主流媒体终于主动向游戏产业靠近，游戏产业开始作为主体在主流媒体的报道中出现；主流媒体与游戏产业的对立开始得到缓解，其对游戏产业发展报道的缺失逐渐得到弥补。尽管如此，2009 年之前的主流媒体报道仍然带有对游戏的严重偏见。例如，“网瘾治疗专家”杨永信作为“靠着一颗爱心，挽救深陷泥潭的学子”的“好人”出现在人民日报的报道中&lt;sup id="fnref:19">&lt;a href="#fn:19" class="footnote-ref" role="doc-noteref">19&lt;/a>&lt;/sup>，尽管此人在当时饱受争议。主流媒体报道立场偏向性严重的问题导致其对游戏产业的报道不够全面客观，主流媒体与游戏产业发展之间的割裂继续存在。&lt;/p>
&lt;p>2009 年后，随着“转变经济发展方式”、“科技强国”、“中国文化走出去”等主流叙事的流行，游戏的科技与文化属性成为这一时期《人民日报》报道游戏的主要主题：2009 至 2011 年，“产业经济”框架在游戏报道中占据绝对主流；而 2012 年到 2017 年，“文娱新方式”框架占比陡增，维持在26%至36%之间波动&lt;sup id="fnref:20">&lt;a href="#fn:20" class="footnote-ref" role="doc-noteref">20&lt;/a>&lt;/sup>。可以看到，《人民日报》在这个时期从以往的游戏产业的旁观者变成了参与者，利用自身的舆论影响力服务于国家发展需要塑造舆论话语，有意识地引导游戏产业与国家发展战略接轨。为了达到此目标，《人民日报》对电子游戏危害的讨论相比以往几乎销声匿迹，报道角度也更加多元。以上种种变化，在客观上促进了主流媒体与游戏产业之间的交流，主流舆论与游戏产业之间更加平衡，同时一定程度上缓解了由于立场偏向而导致的割裂。&lt;/p>
&lt;p>2017 年，《王者荣耀》风靡全国，带来了新的一轮网络手游热，由此沉寂已久的游戏成瘾话题又重新成为主流议题；相比之下科技创新、文化输出等报道框架虽仍然存在，但明显有所减少。2018 年世卫组织首次将游戏成瘾列为疾病&lt;sup id="fnref:21">&lt;a href="#fn:21" class="footnote-ref" role="doc-noteref">21&lt;/a>&lt;/sup>后，《人民日报》在游戏成瘾以外的报道框架进一步被压缩，直到 2021 年仍是如此。主流媒体与游戏产业之间的关系在当下又重新变得紧张，二者之间的割裂有重新加大的趋势。&lt;/p>
&lt;h2 id="什么造成了主流媒体与游戏产业发展的割裂">什么造成了主流媒体与游戏产业发展的割裂&lt;/h2>
&lt;p>如前文所述，主流媒体与游戏产业发展之间的关系在40多年的中国游戏史中经历了明显的起伏。主流媒体与游戏产业发展之间的割裂，一方面表现在主流媒体对游戏的报道中游戏产业作为主体的缺失，另一方面表现在主流媒体报道话语的偏向性；这种割裂长期存在，总体上在缓慢地弥合，但有重新加大的趋势。这种关系变化的背后，有什么因素对二者之间的割裂产生影响？&lt;/p>
&lt;p>首先需要指出的是，以《人民日报》为首的官方媒体的报道角度与游戏产业的发展之间存在产生割裂的必然性，所有对主流媒体与游戏产业关系的影响因素的讨论都必须基于这一前提。《人民日报》是党中央的机关报，其作用是“上连党心，下接民心”&lt;sup id="fnref:22">&lt;a href="#fn:22" class="footnote-ref" role="doc-noteref">22&lt;/a>&lt;/sup>；《人民日报》并不是为游戏产业服务的，其目的是宣传国家中央主张、传播社会的主流声音，这就导致一旦游戏产业的发展与中央政策导向或主流舆论发生冲突，《人民日报》必然会在一定程度上站在游戏产业的对立面，其报道与游戏产业自身的发展历程必然会产生割裂，这与持小众、个人观点的游戏媒体有着本质的不同。&lt;/p>
&lt;p>在这一前提下，通过前文对游戏发展过程的归纳，不难发现游戏相关政策对二者关系的重要影响。一个鲜明的例子是《人民日报》对游戏产业在上世纪 90 年代以及 2012 至 2017 年这两段发展历史截然不同的态度：这两段时期中，游戏产业都致力于将中国文化意象融合到游戏中，然而《人民日报》在前一段时期中完全忽视了游戏产业发展进程，将游戏妖魔化，而在后一段时期中却积极地推动着此进程的发展，游戏的负面报道几乎不再存在。难道 90 年代中国游戏产业的探索没有意义，而 2012 年以后游戏的负面影响就完全消失了吗？显然不是。政策的因素在其中起到了至关重要的作用：第一部与促进国产游戏发展的政策“民族网络游戏出版工程”在 2004 年的网游时代才面世，90 年代主流媒体的报道更多基于社会主流舆论产生；而在 2010 年，“讲好中国故事”、“中国文化走出去”已成为中国文化工作的主流导向&lt;sup id="fnref:23">&lt;a href="#fn:23" class="footnote-ref" role="doc-noteref">23&lt;/a>&lt;/sup>，同时文化部指导网游厂商发起“家长监护工程”，意在引导未成年人“玩健康的游戏”和“健康地玩游戏”，游戏产业的发展得到了国家战略与政策的支持。尽管此时网游成瘾问题依然严重，对游戏的主流舆论评价仍然呈负面，但显然政策导向对主流媒体话语塑造具有更大的影响力。&lt;/p>
&lt;p>另外，由于主流媒体传播的是社会的主流声音，其对游戏产业的报道受社会主流舆论影响较大。这种影响不仅如前文所述体现在报道内容和价值取向上，更隐含在由不同群体舆论话语权的不同而导致的主流媒体报道立场偏向性中。根据何威等的研究，在其所统计的《人民日报》历年对游戏负面报道中，引用专家学者观点的有 49 篇，引用管理部门观点的有 40 篇，而引用玩家的观点仅有 11 篇&lt;sup id="fnref:24">&lt;a href="#fn:24" class="footnote-ref" role="doc-noteref">24&lt;/a>&lt;/sup>。可以看到，在“游戏的负面影响”这一主题下，与游戏产业直接相关的主体的话语权是缺失的，无论是游戏从业人员还是游戏玩家；他们不满与自身话语无法得到关注，却又对此无能为力。游戏杂志《家用电脑与游戏机》在 2000 年 7 月刊登了一篇反驳《光明日报》将游戏称作“电子海洛因”的文章，作者在其中愤怒地抱怨道：&lt;/p>
&lt;blockquote>
&lt;p>为什么作者宁愿“暗访”地下游戏室三次，却不愿意去一趟正大光明的游戏公司或游戏媒体，甚至多走访一些真正热爱电脑游戏的普通人呢？看来是“非不能也，实不愿也”，既然早就定下了“电子游戏是瞄准孩子的‘电子海洛因’”的标题（或者说是结论），一切就要围绕这个中心服务，如此而已。&lt;sup id="fnref:25">&lt;a href="#fn:25" class="footnote-ref" role="doc-noteref">25&lt;/a>&lt;/sup>&lt;/p>
&lt;/blockquote>
&lt;p>这篇文章是主流媒体与游戏产业之间冲突的直接表达，虽然已有 20 多年的历史，但即使是现在，由舆论话语权不平等导致的主流媒体与游戏产业发展之间割裂的问题也还没有得到较好的改善。这是因为，中国电子游戏在上世纪 90 年代刚刚兴起，而其影响力进一步扩大则是在十年后开始的网游时代。绝大部分出生于游戏流行年代的“游戏原住民”出生于 90 年代或 21 世纪，目前大多为 20 多岁的青少年，对主流舆论的影响力还远未成型，而他们恰恰是与游戏产业联系最为密切、最为庞大的群体，因此长期作为无法发声的、被批评的对象出现在游戏报道中；另一方面，40 多年来塑造主流舆论的群体大多作为旁观者的身份目睹游戏产业的发展，与游戏产业之间存在着天然的距离；即使对于部分人而言这种距离可能随着了解的不断深入而缩短，但大多数人由于主流媒体对游戏形象持续的污名化构建，其与游戏产业的距离只会越来越远，从而进一步加剧主流媒体与游戏产业发展之间的割裂。&lt;/p>
&lt;h2 id="总结如何看待主流媒体与游戏产业发展的割裂">总结：如何看待主流媒体与游戏产业发展的割裂&lt;/h2>
&lt;p>如前所述，主流媒体与游戏产业发展之间的割裂主要表现在主流媒体对游戏的报道中游戏产业作为主体的缺失，以及主流媒体报道话语的偏向性。随着历史进程的推进，主流媒体报道逐渐向游戏产业靠近，报道的价值取向取向也逐渐多元化，但是主流媒体与游戏产业发展之间的割裂是必然存在，而且将长期存在的：游戏易成瘾等特性导致大众舆论关于游戏社会影响的警惕，以及国家意志关于游戏对国家发展的影响的考虑一直存在，而这两者是影响主流媒体报道话语的最重要因素。尽管舆论对游戏的警惕可能会随着主流话语权的变更而放松，有关政策可能会因为游戏与国家发展向契合的属性而对游戏发展更加宽容，但它们背后的决定因素——游戏的消极影响是始终存在的。&lt;/p>
&lt;p>与游戏媒体的报道与游戏产业发展较为同步的关系相比，主流媒体与游戏产业之间保持适当的距离在某些方面是有益的。主流媒体对游戏消极影响的关注催生了游戏防沉迷、实名制等机制的产生，在客观上对规范游戏产业的发展起到了一定的作用；同时，主流媒体对游戏产业发展的参与程度越来越高，利用自身舆论影响力引导游戏产业与国家战略接轨，在一定程度上促进了游戏产业的发展。但是，主流媒体对游戏的报道需要避免重新回到以预设立场完全否定游戏价值、无视游戏产业发展历程的状态，这对游戏产业发展，以及游戏与人之间的关系存在着不可忽视的消极影响。&lt;/p>
&lt;p>游戏作为复杂的文化、社会现象，其影响必定是多元的。游戏产业的发展需要具有社会和舆论影响力的主流媒体起到化解不同群体之间冲突与矛盾、促进游戏产业与社会发展相统一的作用。保持一定的距离，批判地看待游戏，给出有建设性的意见，不失为达到这一目标的一种行之有效的方法。&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>如无特别说明，本文中“游戏”专指“电子游戏”，即一切“依靠电子媒体平台而运行的互动游戏”。&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>戚休：《广州东方宾馆利用外资更新客房效果显著》，1981年05月25日《人民日报》第2版。&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>邓剑：《中国电子游戏文化的源流与考辨》，《上海文化》2020年第12期，第45页。&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>刘建华：《电子游戏热中的负效应》，《人民日报》1990年4月4日第6版。&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>夷水寒：《电子游戏的暴力内容对孩子不利》，《人民日报》1991年2月12日第5版。&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>马永梅，吴光志，周开荣：《游戏岂能“犯规”——青少年沉迷电子游戏现象扫描》，《人民日报》1993年12月21日第10版。&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>何威，曹书乐:《从“电子海洛因”到“中国创造”: 〈人民日报〉游戏报道（1981-2017）的话语变迁》，《国际新闻界》第40期，2018年，第58页。&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>汉娜·沃曼：《西方新闻媒体的“中国-东方主义”：被漫画化的游戏文化和商业》，宋雅文译，见何威、刘梦霏编：《游戏研究读本》，上海：华东师范大学出版社，2020年，第220页。&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:9">
&lt;p>使用“人民日报图文数据库”对“游戏、网游、网瘾、手游、电竞、电子竞技”进行标题检索，包括新闻、评论、读者来信等所有类别，并剔除无关文章，得到1989年至2021年刊登的共194篇相关文章。&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:10">
&lt;p>邓剑：《中国当代游戏史的思想谱系：从本土现代化到资本与市场逻辑》，《探索与争鸣》2022年第2期，第166页。&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:11">
&lt;p>朱丹：《学生入迷，家长忧心——上海街头游戏机成灾》，《人民日报》1989年4月17日第2版。&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:12">
&lt;p>《电子游戏的分类》，《人民日报》1999年3月14日第7版。&amp;#160;&lt;a href="#fnref:12" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:13">
&lt;p>本刊评论员：《乌鸦·乌鸦·叫:该谈大陆电子游戏业了》，《电子游戏软件》1995 年第 4 期。&amp;#160;&lt;a href="#fnref:13" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:14">
&lt;p>国务院办公厅等：《国务院办公厅转发文化部等部门关于开展电子游戏经营场所专项治理意见的通知》，2000年6月15日，http://www.gov.cn/gongbao/content/2000/content_60240.htm，2022年7月21日。&amp;#160;&lt;a href="#fnref:14" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:15">
&lt;p>邓剑：《中国当代游戏史的思想谱系：从本土现代化到资本与市场逻辑》，《探索与争鸣》2022年第2期，第169页。&amp;#160;&lt;a href="#fnref:15" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:16">
&lt;p>周萍：《网易推出网络游戏〈大话西游〉》，《人民日报》2002年1月1日第5版。&amp;#160;&lt;a href="#fnref:16" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:17">
&lt;p>崔扬：《国产游戏新出网络版》，《人民日报》2003年7月30日第11版。&amp;#160;&lt;a href="#fnref:17" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:18">
&lt;p>任建民：《网络游戏：超越电影票房之后（热点解读）》，《人民日报》2003年12月15日第5版。&amp;#160;&lt;a href="#fnref:18" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:19">
&lt;p>刘成友：《道德力量汇聚孔孟之乡——全国道德模范与身边好人现场交流活动侧记》，《人民日报》2009年4月4日第3版。&amp;#160;&lt;a href="#fnref:19" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:20">
&lt;p>何威，曹书乐:《从“电子海洛因”到“中国创造”: 〈人民日报〉游戏报道（1981-2017）的话语变迁》，《国际新闻界》第40期，2018年，第71-72页。&amp;#160;&lt;a href="#fnref:20" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:21">
&lt;p>世界卫生组织：《国际疾病分类》第11次修订本，https://www.who.int/zh/news-room/spotlight/international-classification-of-diseases。&amp;#160;&lt;a href="#fnref:21" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:22">
&lt;p>汪晓东：《让党心和民心贴得更紧——写在习近平总书记“2·19”重要讲话发表3周年之际》，《人民日报》2019年2月19日第1版。&amp;#160;&lt;a href="#fnref:22" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:23">
&lt;p>温红彦，李舫：《向世界讲述好中国故事——文化部副部长赵少华谈中华文化“走出去”》，2010年9月30日。&amp;#160;&lt;a href="#fnref:23" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:24">
&lt;p>何威，曹书乐:《从“电子海洛因”到“中国创造”: 〈人民日报〉游戏报道（1981-2017）的话语变迁》，《国际新闻界》第40期，2018年，第77页。&amp;#160;&lt;a href="#fnref:24" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:25">
&lt;p>王亚晖：《中国游戏风云》，北京：中国发展出版社，2018年11月，第113至114页。&amp;#160;&lt;a href="#fnref:25" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content></item><item><title>2022 春季学期总结</title><link>https://yeshe.ng/blog/posts/2022/06/2022-spring-summary/</link><pubDate>Mon, 20 Jun 2022 16:26:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/06/2022-spring-summary/</guid><description>2022 春季学期总结</description><content>&lt;p>春季学期对我来说有一个定律，或者说一个魔咒：学业一塌糊涂，学业之外收获甚多。&lt;/p>
&lt;p>20 年的春季学期在家里上课，鬼使神差般地接触了 iOS 开发，从此一整个学期都在写一个 app，上课和下课都在写代码，导致那个学期的绩点异常整齐，除了体育之外全部都是 3.6，排名刷刷地往下掉，还退了微积分和概统。21 年春季学期进行转系，同时在高强度写树洞 app，没什么心思听课，最后离散 (2) 喜提 3.0，绩点进一步雪崩，并且又退了一次微积分（于是大学总共修了三次微积分 A2，唉！）。&lt;/p>
&lt;p>但是，这两个学期对于我大学四年有着非常深远的影响。最重要的当然是开始写程序之后逐渐喜欢上了 CS，并直接导致最终转到计算机系；写树洞是第一次接触正经的 C/S 架构的开发，积累了大量的网络、git、前后端等等知识，这也使得我现在的技术能力放在贵系也还算可以（？）。&lt;/p>
&lt;p>与之相反，秋季学期在经过前一个春季学期的“惨痛”经历后，往往走向另一个极端：抑制自己在其他地方的兴趣，以 GPA 为首要目标。这当然是有效的，我秋季学期的 GPA 都挺不错；但同时也是无聊的，天天跑图书馆写作业刷题对某些同学来说可能是享受，但我真的不喜欢以提高分数为唯一目的的学习。&lt;/p>
&lt;p>我也曾妄想过“小孩子才做选择，我全都要”，每个学期之前都想着如何能够更高效地利用时间，把学业和其他的所有事情都做到极致。然而，这种想法未曾成为现实，主要有几个原因：第一，我时间管理极差，无法利用正常作息下白天和晚上大部分可以利用的时间。第二，我对于我感兴趣的事情往往有着极高的热情，而这种热情往往导致时间安排上极不均衡的优先级，进而导致一些目前阶段可能（或者说表面上）较为重要的任务没有充足的时间完成（用计算机的专业术语来说，starvation /笑）。&lt;/p>
&lt;p>尽管在这个学期开学前有想过避免以前春季学期的问题，但很不幸，课内学习不够投入的问题并没有得到解决。现在想来，主要的原因是软工占用了大部分时间，其他课程上得也不舒服：数电 tp 老师讲课风格实在是不喜欢，人智导讲的内容实在是没兴趣，搜索引擎实在是太水，而我又比较喜欢捣鼓软工所用的一些技术……因此很多课都用来写软工了（数电前几周的课、搜索引擎后面的课、人智导大部分的课）。微积分倒是坚持认真听了十几周，但耐不住记 P/F 这一重大利好（终于能在不影响 GPA 的情况下修完微积分了 /哭），在记了 P/F 之后就没听了。&lt;/p>
&lt;p>另一个原因，与之前相似，是课外的工作占用了大量的时间，这个学期主要是科研。这个学期开始跟着学长做科研项目，从寒假开始一直在恶补操作系统的知识，后面换方向后又从零开始学习了数据库的知识。等到终于开始上手代码了，花了很多时间调试和修改以前论文的代码，然后考试周就来了，只能搁置。总的来说，这一段科研经历的开端并不是特别顺利，也还没有完全找到自己感兴趣的方向，但总算是开了个头，也算是迈出了一小步。&lt;/p>
&lt;p>总之这个学期就这么不太明白地过去了。有很多不好的经历：软工开题后焦虑、软工进展不顺、数电听不懂、数电实验遇到各种问题、人智导没兴趣、科研进展缓慢……同时也有很多美好的回忆：软工助教和用户代表高度评价我们的项目、软工做了一个（自认为）非常精彩的展示、科研开了一个还算可以的头……&lt;/p>
&lt;p>和&lt;a href="https://yeshe.ng/blog/posts/2022/01/2021-fall-summary">一个学期前&lt;/a>相比，我的乐观精神显然消退了不少。或许是因为第一次遇到了对于计算机不太感兴趣或不太擅长的方面，或许是发现在某些方面自己没办法做到某些同学的高度，或许是在读文章时感受到自己的水平与顶尖水平的巨大差距……总之，发现有些事情是自己做不到的，这对于我自己能力的认识，以及对未来的规划都产生了不小的影响。当然，我也时常想起软工被用户代表和 rls 夸上天的时候的那种兴奋感。或许，我还是能够在一些地方找到自己的独特之处的。&lt;/p>
&lt;p>至于暑假和下学期，还是花多点时间在科研上吧。如果这条路能走通，那就坚定走下去；如果走不通，那就要及时考虑一下其他道路了。&lt;/p></content></item><item><title>WWDC22 Sessions 记录</title><link>https://yeshe.ng/blog/posts/2022/06/wwdc22/</link><pubDate>Wed, 08 Jun 2022 22:27:07 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/06/wwdc22/</guid><description>记录 WWDC22 上发布的新 API 和新技术</description><content>&lt;p>虽然 iOS 16 和 macOS 13 并没有太大的变化，但是 SDK（特别是 SwiftUI）更新的东西相当多，解决了很多以前开发中的痛点。&lt;/p>
&lt;p>目录（根据观看时间顺序，大致按照发布顺序）：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="#meet-swift-regex">Meet Swift Regex&lt;/a>&lt;/li>
&lt;li>&lt;a href="#meet-desktop-class-ipad">Meet desktop class iPad&lt;/a>&lt;/li>
&lt;li>&lt;a href="#the-swiftui-cookbook-for-navigation">The SwiftUI cookbook for navigation&lt;/a>&lt;/li>
&lt;li>&lt;a href="#hello-swift-charts">Hello Swift Charts&lt;/a>&lt;/li>
&lt;li>&lt;a href="#whats-new-in-swiftui">What&amp;rsquo;s new in SwiftUI&lt;/a>&lt;/li>
&lt;li>&lt;a href="#whats-new-in-uikit">What&amp;rsquo;s new in UIKit&lt;/a>&lt;/li>
&lt;li>&lt;a href="#compose-custom-layouts-with-swiftui">Compose custom layouts with SwiftUI&lt;/a>&lt;/li>
&lt;li>&lt;a href="#swift-charts-raise-the-bar">Swift Charts: Raise the bar&lt;/a>&lt;/li>
&lt;li>&lt;a href="#embrace-swift-generics">Embrace Swift generics&lt;/a>&lt;/li>
&lt;li>&lt;a href="#design-protocol-interfaces-in-swift">Design protocol interfaces in Swift&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="meet-swift-regex">Meet Swift Regex&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110357/">https://developer.apple.com/videos/play/wwdc2022/110357/&lt;/a>&lt;/p>
&lt;p>Swift 5.7 新增了强类型的正则表达式类型，自动将字符串转化为需要的类型，支持编译器检查。另外还提供了 &lt;code>RegexBuilder&lt;/code> 框架，使用 SwiftUI 所使用的 result builder 语法来写正则表达式，非常具有表达力。编译器帮忙检查的 regex 和强类型的 capture 太香了。&lt;/p>
&lt;p>Regex literal，强类型静态正则表达式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Regex literals&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> digits = &lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d&lt;span style="color:#f92672">+/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// digits: Regex&amp;lt;Substring&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Regex Builder，使用 result builder 构建正则表达式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// CREDIT 03/02/2022 Payroll from employer $200.23&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// CREDIT 03/03/2022 Suspect A $2,000,000.00&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// DEBIT 03/03/2022 Ted&amp;#39;s Pet Rock Sanctuary $2,000,000.00&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// DEBIT 03/05/2022 Doug&amp;#39;s Dugout Dogs $33.27&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> &lt;span style="color:#a6e22e">RegexBuilder&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> fieldSeparator = &lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>s{&lt;span style="color:#ae81ff">2&lt;/span>,}&lt;span style="color:#f92672">|&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>t&lt;span style="color:#f92672">/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> transactionMatcher = Regex {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { &lt;span style="color:#f92672">/&lt;/span>CREDIT&lt;span style="color:#f92672">|&lt;/span>DEBIT&lt;span style="color:#f92672">/&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { One(.date(.numeric, locale: Locale(identifier: &lt;span style="color:#e6db74">&amp;#34;en_US&amp;#34;&lt;/span>), timeZone: .gmt)) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OneOrMore {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NegativeLookahead { fieldSeparator }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CharacterClass.any
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fieldSeparator
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Capture { One(.localizedCurrency(code: &lt;span style="color:#e6db74">&amp;#34;USD&amp;#34;&lt;/span>).locale(Locale(identifier: &lt;span style="color:#e6db74">&amp;#34;en_US&amp;#34;&lt;/span>))) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// transactionMatcher: Regex&amp;lt;(Substring, Substring, Date, Substring, Decimal)&amp;gt;}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Named captures，给各个 group 起名（返回的结果是一个 &lt;code>Tuple&lt;/code>，因此可以在编译期加上标识）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> regex = #&lt;span style="color:#f92672">/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;date&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">2&lt;/span>} &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">2&lt;/span>} &lt;span style="color:#f92672">/&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>d{&lt;span style="color:#ae81ff">4&lt;/span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;middle&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>P{currencySymbol}&lt;span style="color:#f92672">+&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (?&amp;lt;currency&amp;gt; &lt;span style="color:#960050;background-color:#1e0010">\&lt;/span>p{currencySymbol})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">/&lt;/span>#
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Regex&amp;lt;(Substring, date: Substring, middle: Substring, currency: Substring)&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="meet-desktop-class-ipad">Meet desktop class iPad&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10069/">https://developer.apple.com/videos/play/wwdc2022/10069/&lt;/a>&lt;/p>
&lt;p>针对 iPad 生产力问题作的改进，推出了更像 macOS 的 toolbar。不过，这远远不能解决 iPad 缺少足够 productive 的应用的问题。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-ipad.webp" alt="">&lt;/p>
&lt;h2 id="the-swiftui-cookbook-for-navigation">The SwiftUI cookbook for navigation&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10054/">https://developer.apple.com/videos/play/wwdc2022/10054/&lt;/a>&lt;/p>
&lt;p>苹果终于改进了 SwiftUI 上视图导航的逻辑。简单来说就是以前只能傻傻地打开另一个视图而无法获取之前的视图的情况，现在可以获取并可以用程序控制当前所在的层级了。&lt;/p>
&lt;p>官方的示例代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> &lt;span style="color:#a6e22e">SwiftUI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Pushable stack&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PushableStack&lt;/span>: View {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @State &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> path: [Recipe] = []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @StateObject &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> dataModel = DataModel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some View {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NavigationStack(path: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>path) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List(Category.allCases) { category &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Section(category.localizedName) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(dataModel.recipes(&lt;span style="color:#66d9ef">in&lt;/span>: category)) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NavigationLink(recipe.name, value: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .navigationTitle(&lt;span style="color:#e6db74">&amp;#34;Categories&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .navigationDestination(&lt;span style="color:#66d9ef">for&lt;/span>: Recipe.&lt;span style="color:#66d9ef">self&lt;/span>) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RecipeDetail(recipe: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .environmentObject(dataModel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>相比于之前 &lt;code>NavigationLink&lt;/code> 提供一个 &lt;code>View&lt;/code>，现在可以提供一个用于区分不同目标的值来代替（这样就可以记录栈上的内容）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>NavigationLink(recipe.name, value: recipe)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，使用 &lt;code>navigationDestination()&lt;/code> 来返回对应的 &lt;code>View&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>.navigationDestination(&lt;span style="color:#66d9ef">for&lt;/span>: Recipe.&lt;span style="color:#66d9ef">self&lt;/span>) { recipe &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RecipeDetail(recipe: recipe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用新的 &lt;code>NavigationStack&lt;/code>，我们传递一个 &lt;code>path&lt;/code> 来获取当前导航栈上的所有 destination 所对应的值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>NavigationStack(path: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>path)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，我们可以通过更改 &lt;code>path&lt;/code> 来随意更改导航栈：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>path.removeAll()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，还可以通过 &lt;code>SceneStorage&lt;/code> 持久化当前的状态。&lt;/p>
&lt;h2 id="hello-swift-charts">Hello Swift Charts&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10136/">https://developer.apple.com/videos/play/wwdc2022/10136/&lt;/a>&lt;/p>
&lt;p>官方的图表框架，使用 SwiftUI 快速构建图表，非常简洁。比如，创建折线图：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>Chart(partyTasksRemaining) { element &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LineMark(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> x: .value(&lt;span style="color:#e6db74">&amp;#34;Date&amp;#34;&lt;/span>, element.date, unit: .day),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> y: .value(&lt;span style="color:#e6db74">&amp;#34;Tasks Remaining&amp;#34;&lt;/span>, element.remainingCount)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .foregroundStyle(by: .value(&lt;span style="color:#e6db74">&amp;#34;Category&amp;#34;&lt;/span>, element.category))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .symbol(by: .value(&lt;span style="color:#e6db74">&amp;#34;Category&amp;#34;&lt;/span>, element.category))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后效果很不错（也很苹果）：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart.webp" alt="">&lt;/p>
&lt;p>声明式语法真是太适合做图表了，matplotlib 虽然强大但是画个图挺麻烦的，在想着以后科研能不能用 Swift 画图（&lt;/p>
&lt;h2 id="whats-new-in-swiftui">What&amp;rsquo;s new in SwiftUI&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10052/">https://developer.apple.com/videos/play/wwdc2022/10052/&lt;/a>&lt;/p>
&lt;p>除了上面提到的 Swift Charts 和 navigation 之外，这次 SwiftUI 的主要更新还有：&lt;/p>
&lt;h3 id="window-和-menu-bar-extras">Window 和 Menu bar extras&lt;/h3>
&lt;p>macOS 可以创建单独的、可以控制显示/关闭的窗口了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@main
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PartyPlanner&lt;/span>: App {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some Scene {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WindowGroup(&lt;span style="color:#e6db74">&amp;#34;Party Planner&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PartyPlannerHome()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Window(&lt;span style="color:#e6db74">&amp;#34;Party Budget&amp;#34;&lt;/span>, id: &lt;span style="color:#e6db74">&amp;#34;budget&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Budget View&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .keyboardShortcut(&lt;span style="color:#e6db74">&amp;#34;0&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，终于支持状态栏 app 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@main
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">PartyPlanner&lt;/span>: App {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> body: some Scene {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MenuBarExtra(&lt;span style="color:#e6db74">&amp;#34;Bulletin Board&amp;#34;&lt;/span>, systemImage: &lt;span style="color:#e6db74">&amp;#34;quote.bubble&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BulletinBoard()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .menuBarExtraStyle(.window)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="resizable-sheets">Resizable sheets&lt;/h3>
&lt;p>加入了去年 UIKit 新增的可变高度的 sheet：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>.sheet(isPresented: &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>presented) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Budget View&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .presentationDetents([.height(&lt;span style="color:#ae81ff">250&lt;/span>), .medium])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .presentationDragIndicator(.visible)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="其他">其他&lt;/h3>
&lt;p>其他的更新包括：&lt;/p>
&lt;ul>
&lt;li>macOS 上 &lt;code>Form&lt;/code> 新的外观&lt;/li>
&lt;li>支持多行的 &lt;code>TextField&lt;/code> 和限定行高范围&lt;/li>
&lt;li>iPad 支持 &lt;code>Table&lt;/code>&lt;/li>
&lt;li>可自定义的 toolbar&lt;/li>
&lt;li>可自定义的 layout&lt;/li>
&lt;/ul>
&lt;h2 id="whats-new-in-uikit">What&amp;rsquo;s new in UIKit&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10068/">https://developer.apple.com/videos/play/wwdc2022/10068/&lt;/a>&lt;/p>
&lt;p>现在基本不写 UIKit 了，这部分就没怎么记录。比较有意思的是，现在创建 &lt;code>UICollectionView&lt;/code> 和
&lt;code>UITableView&lt;/code> 可以用 SwiftUI 来写 cell 的 UI 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>cell.contentConfiguration = UIHostingConfiguration {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Image(systemName: &lt;span style="color:#e6db74">&amp;#34;wand.and.stars&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .font(.title)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;Like magic!&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .font(.title2).bold()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .foregroundStyle(Color.purple)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其他的更新，如新的 toolbar，到暑假可以用来改进一下 Whiz Reader（不过可能用 SwiftUI 重写而不是继续用 UIKit）。&lt;/p>
&lt;h2 id="compose-custom-layouts-with-swiftui">Compose custom layouts with SwiftUI&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10056/">https://developer.apple.com/videos/play/wwdc2022/10056/&lt;/a>&lt;/p>
&lt;p>支持自定义布局应该是本次 SwiftUI 最重要的更新了。现在我们可以访问到一些以前获取不到的信息来放置子视图了。&lt;/p>
&lt;h3 id="grid">Grid&lt;/h3>
&lt;p>在介绍自定义布局之前，首先介绍了新增的 &lt;code>Grid&lt;/code> 类型，可以构建静态的对齐与网格的布局。&lt;/p>
&lt;p>与之前的 &lt;code>LazyHGrid&lt;/code> 和 &lt;code>LazyVGrid&lt;/code> 不同，&lt;code>Grid&lt;/code> 不是懒加载的，因此高/宽是确定的，适合于静态 grid 的构建。&lt;/p>
&lt;p>与 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 的组合相比，&lt;code>Grid&lt;/code> 的各行/列可以自动根据最大元素调整大小，而 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 不行。比如，现在可以实现这样一个列表：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-grid.webp" alt="">&lt;/p>
&lt;p>左侧的名称一栏自动将宽度调整到适合于最大的 &lt;code>Goldfish&lt;/code>，右侧同理。&lt;/p>
&lt;blockquote>
&lt;p>在以前，使用 &lt;code>VStack&lt;/code> 和 &lt;code>HStack&lt;/code> 想要实现各列大小一致，只能给各列指定固定的大小。比如，&lt;a href="https://github.com/treehollow/treehollow-v3-ios/blob/main/Source/Hollow/View/Hierarchy/Hollow/Content/HollowCommentContentView.swift">树洞的评论界面&lt;/a>大概是这样实现的：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(comments) { comment &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(comment.name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .fixedSize(.horizontal, &lt;span style="color:#ae81ff">20&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(comment.content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当 &lt;code>comment.name&lt;/code> 过长时，就会变成两行，非常影响体验。&lt;/p>
&lt;/blockquote>
&lt;h3 id="自定义布局">自定义布局&lt;/h3>
&lt;p>自定义布局对应一个新的协议：&lt;code>Layout&lt;/code>。&lt;/p>
&lt;p>&lt;code>Layout&lt;/code> 通过两个函数参与布局过程：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">sizeThatFits&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> proposal: ProposedViewSize,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subviews: Subviews,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cache: &lt;span style="color:#66d9ef">inout&lt;/span> Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) -&amp;gt; CGSize
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">placeSubviews&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">in&lt;/span> bounds: CGRect,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> proposal: ProposedViewSize,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subviews: Subviews,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cache: &lt;span style="color:#66d9ef">inout&lt;/span> Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第一个函数 &lt;code>sizeThatFits()&lt;/code> 通过返回一个大小来告诉父 &lt;code>View&lt;/code> 我们需要多少空间；第二个函数 &lt;code>placeSubviews()&lt;/code> 将所有的子 &lt;code>View&lt;/code> 放置在合适的位置（使用我们在 &lt;code>sizeThatFits&lt;/code> 中要求的空间）。&lt;/p>
&lt;h4 id="sizethatfits">sizeThatFits&lt;/h4>
&lt;p>我们先来看 &lt;code>sizeThatFits()&lt;/code>。它有三个参数：&lt;/p>
&lt;ul>
&lt;li>&lt;code>proposal&lt;/code>：父 &lt;code>View&lt;/code> 所期望这个布局的大小。我们（可以）根据 &lt;code>proposal&lt;/code> 的大小来决定我们最终要占据多少空间，当然也可以忽略&lt;/li>
&lt;li>&lt;code>subviews&lt;/code>：布局中所有的子 &lt;code>View&lt;/code>。这并不是 &lt;code>View&lt;/code> 本身，而是它的“代理”（proxy），仅给我们提供必要的信息和接口&lt;/li>
&lt;li>&lt;code>cache&lt;/code>：可用来缓存中间计算结果&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>关于 &lt;code>proposal&lt;/code>，官方文档是这样介绍的：&lt;/p>
&lt;p>A proposed size for the subview. In SwiftUI, views choose their own size, but can take a size proposal from their parent view into account when doing so.&lt;/p>
&lt;p>可以看到，&lt;code>proposal&lt;/code> 是父视图对子视图大小的“建议”。父视图在进行布局时，将建议的大小传入每个子视图，子视图经过计算返回最终的大小（可以是 &lt;code>proposal&lt;/code> 的大小，也可以不是），父视图最终根据子视图返回的大小来进行布局的计算（而不是它所认为的大小，即 &lt;code>proposal&lt;/code>）。&lt;/p>
&lt;/blockquote>
&lt;p>我们以官方的例子为例。这个例子实现了一个 &lt;code>EqualWidthHStack&lt;/code>，有几个需求：&lt;/p>
&lt;ul>
&lt;li>水平布局&lt;/li>
&lt;li>每个子 &lt;code>View&lt;/code> 大小相同&lt;/li>
&lt;li>每个子 &lt;code>View&lt;/code> 的大小都等于最大的子 &lt;code>View&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>对于这个 layout，&lt;code>sizeThatFits()&lt;/code> 如下：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-sizethatfits.webp" alt="">&lt;/p>
&lt;p>原理很简单：计算子 &lt;code>View&lt;/code> 的最大的 size，加上之间的 spacing，即是布局需要的大小。我们通过 &lt;code>LayoutSubview&lt;/code> 的 &lt;code>sizeThatFits()&lt;/code> 获取每个子 &lt;code>View&lt;/code> 的大小，然后计算最大值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">maxSize&lt;/span>(subviews: Subviews) -&amp;gt; CGSize {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> subviewSizes = subviews.map { $0.sizeThatFits(.unspecified) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> maxSize: CGSize = subviewSizes.reduce(.zero) { currentMax, subviewSize &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CGSize(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> width: max(currentMax.width, subviewSize.width),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> height: max(currentMax.height, subviewSize.height))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> maxSize
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 &lt;code>sizeThatFits()&lt;/code> 中，我们忽略了 &lt;code>proposal&lt;/code>，这样在父视图的任何大小下这个布局所占的空间都是一样的。如果需要更灵活的布局，如空间不足时自动缩小，可以根据不同的 &lt;code>proposal&lt;/code> 返回不同的大小。&lt;/p>
&lt;h4 id="placesubviews">placeSubviews&lt;/h4>
&lt;p>&lt;code>placeSubviews()&lt;/code> 相较于 &lt;code>sizeThatFits()&lt;/code> 多了一个参数 &lt;code>bounds&lt;/code>，其余参数均相同；我们通过调用 &lt;code>LayoutSubview&lt;/code> 的方法来放置子视图。其实完全可以理解为两个函数是同一个函数的两个部分，&lt;code>sizeThatFits()&lt;/code> 用于计算视图大小，&lt;code>placeSubviews()&lt;/code> 用我们计算好的大小来放置视图；只是因为上层调用是两个不连续的过程，才将它们分开为两个函数。&lt;/p>
&lt;p>因此，这两个函数一般具有部分相同的逻辑，比如上面的 &lt;code>maxSize()&lt;/code> 和 &lt;code>spacing()&lt;/code>，最好将其写到一个新的函数中以便复用，或者直接将中间结果放到 &lt;code>cache&lt;/code> 中（待考证）。&lt;/p>
&lt;p>实现 &lt;code>EqualWidthHStack&lt;/code> 的 &lt;code>placeSubviews()&lt;/code> 很简单，我们只需要通过相同的方法计算 &lt;code>maxSize&lt;/code> 和 &lt;code>spacing&lt;/code>，然后将每个 &lt;code>subView&lt;/code> 放置在合适的位置就可以了。这个过程通过 &lt;code>LayoutSubView&lt;/code> 的 &lt;code>place()&lt;/code> 方法完成。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-placesubviews.webp" alt="">&lt;/p>
&lt;h4 id="传递额外信息">传递额外信息&lt;/h4>
&lt;p>这种方式有一个问题：传入的 &lt;code>subviews&lt;/code> 只是视图的代理，不带有视图的信息。如果我们需要传递子视图的额外信息用于布局的计算，官方提供了一种（强类型的）方式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Rank&lt;/span>: LayoutValueKey {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> defaultValue: Int = &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MyLayout(&lt;span style="color:#ae81ff">1.&lt;/span>..&lt;span style="color:#ae81ff">10&lt;/span>) { rank &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Text(&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#e6db74">\(&lt;/span>rank&lt;span style="color:#e6db74">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .layoutValue(key: Rank.&lt;span style="color:#66d9ef">self&lt;/span>, value: rank)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用下标运算符，我们可以从 &lt;code>subview&lt;/code> 中获取对应的值：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> ranks = subviews.map { subview &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subview[Rank.&lt;span style="color:#66d9ef">self&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样就实现了额外信息的传递。&lt;/p>
&lt;h3 id="自适应布局">自适应布局&lt;/h3>
&lt;p>有意思的是，官方还实现了一个能够根据可用空间的大小自动调整布局的布局 &lt;code>ViewThatFits&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>ViewThatFits {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EqualWidthHStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Buttons(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> VStack {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Buttons(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>当空间足够时，选择第一个 &lt;code>EqualWidthHStack&lt;/code>；空间不够时，选择所需空间更少的 &lt;code>VStack&lt;/code>。&lt;/p>
&lt;p>其实这个非常 fancy 的布局的实现应该很简单：根据 &lt;code>EqualWidthHStack&lt;/code> 和 &lt;code>VStack&lt;/code> 的 &lt;code>sizeThatFits()&lt;/code> 的返回值，按顺序选择能够在可用空间内放置的一个。当然，我们没办法在自定义排布中选择哪些子视图不显示，因此是没办法完美地手动实现一个类似的容器的（可以通过不给子视图分配空间来实现，不过这样子视图仍然会加载）。&lt;/p>
&lt;h3 id="anylayout">AnyLayout&lt;/h3>
&lt;p>最后，官方提供了一个 type-erased 的 &lt;code>Layout&lt;/code>，即 &lt;code>AnyLayout&lt;/code>，可以用于动态改变布局类型：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> layout = isThreeWayTie ? AnyLayout(HStack())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : AnyLayout(MyRadialLayout())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>layout {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ForEach(pets) { pet &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Avatar(pet: pet)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.animation(...)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，由于 &lt;code>layout&lt;/code> 中的 &lt;code>View&lt;/code>（在这里是若干个 &lt;code>Avatar&lt;/code>）在不同的布局下具有相同的 view identity，因此布局变化前后子视图是不变的。通过这一点，我们甚至可以为不同布局的变化加上动画。&lt;/p>
&lt;h2 id="swift-charts-raise-the-bar">Swift Charts: Raise the bar&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10137/">https://developer.apple.com/videos/play/wwdc2022/10137/&lt;/a>&lt;/p>
&lt;p>Swift Charts 框架的进阶介绍，主要介绍了一些自定义方法。&lt;/p>
&lt;p>增加标注：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-annotation.webp" alt="">&lt;/p>
&lt;p>指定图表颜色：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-color.webp" alt="">&lt;/p>
&lt;p>自定义刻度：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-axis.webp" alt="">&lt;/p>
&lt;p>自定义图表区域：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-area.webp" alt="">&lt;/p>
&lt;p>在图表上添加一层 overlay，可用于交互：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-proxy.webp" alt="">
&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-chart-proxy-2.webp" alt="">&lt;/p>
&lt;h2 id="embrace-swift-generics">Embrace Swift generics&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110352/">https://developer.apple.com/videos/play/wwdc2022/110352/&lt;/a>&lt;/p>
&lt;p>介绍了 Swift 范型的更新，主要针对 protocol。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Animal&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Feed: AnimalFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">eat&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> feed: Feed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Farm&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feed&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animal: some Animal) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> produce = crop.harvest()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> animal.eat(produce)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feedAll&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animals: [any Animal]) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> animal &lt;span style="color:#66d9ef">in&lt;/span> animals {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> feed(animal)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>上面的代码中有几个关键点：&lt;/p>
&lt;ul>
&lt;li>协议限制在 Swift 5.7 中可以用 opaque type，即 &lt;code>some&lt;/code> 关键字表示&lt;/li>
&lt;li>可以用 &lt;code>type(of:)&lt;/code> 来获取确定的具体类型&lt;/li>
&lt;li>在 Swift 5.7 中可使用 &lt;code>any&lt;/code> 实现针对 protocol 的类型消除（type erasure）&lt;/li>
&lt;/ul>
&lt;p>&lt;code>any&lt;/code> 非常实用。之前，数组只能储存同一具体类型的数据，不能储存遵循相同协议而类型不同的数据（主要原因应该是内存大小不同）；在 Swift 5.7 中，使用 &lt;code>any&lt;/code> 可以储存遵循某协议的任意类型的数据。现在，数组就不一定直接储存对象本身了。如果对象大小合适（这里不太清楚“合适”的含义，估计是指针大小以内？），可以直接存到数组里，否则存在外面：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-generics-type-erasure.webp" alt="">&lt;/p>
&lt;p>这种方式有点像存了一堆指针，但是又不完全是（较小的对象可以直接放在数组内），可能是为了减少虚函数查表的开销？&lt;/p>
&lt;p>另外，&lt;code>any&lt;/code> 类型可以用于 &lt;code>some&lt;/code>，具体类型的获取在运行时完成。这种转换叫做“opening boxes”，即“打开盒子看究竟是什么动物”：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-generics-opening-boxes.webp" alt="">&lt;/p>
&lt;p>最后，官方建议：&lt;/p>
&lt;blockquote>
&lt;p>Write &lt;strong>some&lt;/strong> by default&lt;/p>
&lt;/blockquote>
&lt;p>道理很简单，直接调用带 &lt;code>some&lt;/code> 参数的函数实际上在编译期是确定了类型的，而 &lt;code>any&lt;/code> 在编译期去掉了具体类型的信息，在运行期才能确定，相比之下效率较低。&lt;/p>
&lt;h2 id="design-protocol-interfaces-in-swift">Design protocol interfaces in Swift&lt;/h2>
&lt;p>&lt;a href="https://developer.apple.com/videos/play/wwdc2022/110353/">https://developer.apple.com/videos/play/wwdc2022/110353/&lt;/a>&lt;/p>
&lt;p>在上面 Embrace Swift generics 的基础上，进一步介绍使用新的语言特性设计协议。&lt;/p>
&lt;h3 id="type-erasure">Type erasure&lt;/h3>
&lt;p>和上面类似的例子。当我们使用类型消除时，所有类型变成“上界”（不太明白这个名词的意思&amp;hellip;），与之相关的所有泛型都变成对应的上界。&lt;/p>
&lt;p>比如，&lt;code>any Animal&lt;/code> 调用 &lt;code>produce()&lt;/code> 的结果类型为 &lt;code>any Food&lt;/code>：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-type-erasure-sematics.webp" alt="">&lt;/p>
&lt;p>但是，反过来，不能够将某一个符合 associatedtype 的类型作为参数传入一个上界类型的函数中。&lt;/p>
&lt;p>比如，&lt;code>Crop()&lt;/code> 不能作为 &lt;code>any Animal&lt;/code> 的 &lt;code>eat()&lt;/code> 的参数：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-type-erasure-sematics-2.webp" alt="">&lt;/p>
&lt;p>这里的关键在于，一个具体类型总是可以安全地转换为 &lt;code>any&lt;/code>，而 &lt;code>any&lt;/code> 不能反过来安全地转换为某一个具体的类型。&lt;/p>
&lt;h3 id="hide-implementation-details">Hide implementation details&lt;/h3>
&lt;p>有时候，我们不希望暴露太多实现细节，比如下面的这个例子：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-lazy-filter.webp" alt="">&lt;/p>
&lt;p>我们希望使用 &lt;code>LazyFilterSequence&lt;/code> 来提高性能，然而并不希望将这个信息暴露在 API 中。Swift 为了解决这个问题，增加了一个叫做 primary associated types 的语法：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/06/wwdc22/wwdc22-lazy-filter-opaque.webp" alt="">&lt;/p>
&lt;p>Primary associated types 大致的意思是，将 protocol 中与实现细节相关的 associatedtype 使用尖括号暴露出来，从而能够使用 &lt;code>some&lt;/code> 或者 &lt;code>any&lt;/code> 来表示满足这个 associatedtype 要求的类型。&lt;/p>
&lt;p>比如，标准库中的 &lt;code>Collection&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Collection&lt;/span>&amp;lt;Element&amp;gt;: Sequence {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Element
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="关于-typeof">关于 &lt;code>type(of:)&lt;/code>&lt;/h3>
&lt;p>看完视频最主要的疑问是：&lt;code>type(of:)&lt;/code> 究竟是完全由运行时决定的，还是有编译器参与的。比如上面的例子：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Animal&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Feed: AnimalFeed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">eat&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> feed: Feed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">feed&lt;/span>(&lt;span style="color:#66d9ef">_&lt;/span> animal: some Animal) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> produce = crop.harvest()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> animal.eat(produce)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编译器知道 &lt;code>some Animal&lt;/code> 是一个具体的、遵循 &lt;code>Animal&lt;/code> 的类型（包括运行时才能确定类型的 &lt;code>any Animal&lt;/code>），因此可以确定 &lt;code>type(of: animal).Feed&lt;/code> 的类型就是具体类型的 &lt;code>Feed&lt;/code>，上面的代码得以通过编译。&lt;/p>
&lt;p>但是，脱离了 &lt;code>some&lt;/code> 的语境，&lt;code>any&lt;/code> 就没有这种机制了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> animal: any Animal = Cow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> crop = type(of: animal).Feed.grow()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// error: type of expression is ambiguous without more context&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// let crop = type(of: animal).Feed.grow()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// ~~~~~~~~~~~~~~~~~^^^^~~~~~~~&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编译器实际上通过 &lt;code>some&lt;/code> 给 &lt;code>any&lt;/code> 提供了便利，即虽然不能在编译期知道具体的类型，但是可以保证 associatedtype 的正确性，这也是上面所说的“Open boxing”实现的效果。要想利用这种便利，我们需要使用带有 &lt;code>some&lt;/code> 参数的函数。&lt;/p></content></item><item><title>音乐剧 Come From Away 赏析</title><link>https://yeshe.ng/blog/posts/2022/05/come-from-away-review/</link><pubDate>Sat, 28 May 2022 14:27:27 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/05/come-from-away-review/</guid><description>\"Tonight, we honor what was lost, but we also commemorate what we found.\"</description><content>&lt;blockquote>
&lt;p>本文为 2022 年春季学期《西方音乐剧史》课程期末作业&lt;/p>
&lt;/blockquote>
&lt;p>《Come From Away》的故事原型是，在 911 事件发生时，加拿大的一个小镇接纳了超过 7000 个因飞行禁令临时降落在此地的乘客。故事本身并不复杂，主要讲述了当地居民尽其所能为滞留乘客提供住宿等帮助、乘客从一开始的恐慌、焦虑到最终融入当地人的生活中，并获得了继续前行的动力。&lt;/p>
&lt;p>下面主要从舞台设计和主题表达两个方面进行赏析。&lt;/p>
&lt;h2 id="舞台设计">舞台设计&lt;/h2>
&lt;p>这部音乐剧最令人印象深刻的是对转场、道具和灯光的令人惊叹的设计，可以说充分展现了音乐剧作为舞台表演艺术的魅力。&lt;/p>
&lt;h3 id="场景切换">场景切换&lt;/h3>
&lt;p>本部音乐剧总体上是刻画群像，涉及到当地居民和乘客等多个群体，并且群体内部又分成很多不同的角色。如何在 Cast 有限的条件下（只有 12 人）演出这么多不同的角色，并且营造本剧“来的人很多”的背景氛围呢？方法很简单：一个演员演多个角色，并在不同场景中切换。但是这种方法有一个最大的问题：如何让观众清楚地知道这个时刻舞台上的演员演的是哪个角色、现在是在哪个场景？&lt;/p>
&lt;p>《Come From Away》的方法是，利用服装、道具、灯光等，来主动地为人物提供“背景”，并结合演员的动作和台词，来进行快速的场景切换。&lt;/p>
&lt;p>本剧角色和场景切换的速度之快令人瞠目结舌，同时又达到了非常好的效果。比如，在下面这短短三分钟中，一共表现了 6 个场景的情形。&lt;/p>
&lt;p>首先，场景是在小镇的酒吧内，大家刚刚得知恐怖袭击的消息：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/1.webp" alt="">&lt;/p>
&lt;p>此时，灯光熄灭，演员打开手电筒，瞬间切换另一个场景：&lt;/p>
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/2.webp">
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/3.webp">
&lt;p>这里演员使用手电筒营造了光线不强、主要发光源是屏幕的环境，结合台词观众很容易就知道现在场景切换到了塔台上。&lt;/p>
&lt;p>接下来的场景是塔台和机长进行交流。首先是塔台上管制员在繁忙地向机长传达必须马上降落的信息：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/4.webp" alt="">&lt;/p>
&lt;p>然后，椅子一转，所有演员面朝观众。这时候，演员以机长的身份分别与塔台交流（这里同时有多架飞机），我们因此可以知道现在场景切换到了飞机的驾驶舱内。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/5.webp" alt="">&lt;/p>
&lt;p>突然，灯光一暗，演员向右转 90 度，瞬间切换到了机舱内乘客们的场景，同时乘务员开始讲话。我们可以看到这里其实还有原先的桌子，实际上是不符合机舱的情况的，但是并不会造成观众的误解。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/6.webp" alt="">&lt;/p>
&lt;p>过了一段时间，演员站起来，抬头向上看，开始在数降落的飞机的数量。这里，场景又切换到了机场的地面上。我们可以看到，后面的部分演员仍然保持之前的坐姿，但由于他们都在向上看，并逐渐站起来，因此这两个场景的过渡非常自然。之前的乘务员脱下帽子和外套，拿起麦克风，摇身一变成为在地面进行报道的记者，毫无违和感。&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/7.webp" alt="">&lt;/p>
&lt;p>最后，灯光由冷变暖，场景重新回到室内的酒吧。这里有个细节，切换前各个演员是分散的，而一切换到酒吧后，演员们都朝着镇长聚集，这也加强了对场景的构建。&lt;/p>
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/8.webp">
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/9.webp">
&lt;p>总的来看，本剧进行场景切换的主要方法有：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>演员表演&lt;/strong>：通过台词、动作和表情演绎不同角色，如机舱内乘客拨动头顶的开关、管制员用严肃的语气进行无线电通话&lt;/li>
&lt;li>&lt;strong>服装&lt;/strong>：通过有职业/角色特点的服装区分同一个演员饰演的不同角色&lt;/li>
&lt;li>&lt;strong>灯光&lt;/strong>：通过灯光区分不同场景，如室内的酒吧是暖色的，机舱和地面是冷色的，而塔台和驾驶舱是暗的&lt;/li>
&lt;li>&lt;strong>道具&lt;/strong>：通过道具（这里主要是椅子）表现空间的特点&lt;/li>
&lt;/ul>
&lt;p>上面的片段使用非线性叙事的方法，将“下达降落命令”这一时刻前后不同地点、不同角色的情况，包括角色的身份、心情等串联起来，立体地表现了不同人群在这一时间对事件的反应，颇有概念音乐剧的影子。&lt;/p>
&lt;p>同时，演员所表现的是群像特征，而不是特定角色的特点，因此成功地表现了各个群体总体在此刻的境遇。这也使得观众无需将演员与特定角色绑定（除了酒吧的场景），使得场景切换非常自然。&lt;/p>
&lt;p>有意思的是，剧组在剧中还故意展示了他们通过这种方式“省人手”的技巧。同一个人戴上眼镜，长了胡子，或者戴上帽子，配上不同的表情，就是不同性格的、不同镇的镇长，引来阵阵笑声：&lt;/p>
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/10.webp">
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/11.webp">
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/12.webp">
&lt;h3 id="道具">道具&lt;/h3>
&lt;p>这部剧除了十几张椅子外几乎没有任何道具。但就是这十几张椅子，剧组把它们玩出了花。这些椅子的最大作用，如前所述，是用于场景构建。下面这两张图非常明显地展示了这点：&lt;/p>
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/13.webp">
&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/14.webp">
&lt;p>将椅子拍成两排，就变成了机舱的座位。&lt;/p>
&lt;p>又如，机长在讲述自己的故事时，其余的人围成一圈：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/15.webp" alt="">&lt;/p>
&lt;p>情侣在野外登高时，用凳子构成一条栈道：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/16.webp" alt="">&lt;/p>
&lt;p>这种方式非常有效，因为不同场景的设施具有迥异的几何排布特征，使用凳子表现这些几何特征，就可以让观众迅速识别并沉浸入场景中（同时非常省经费！）。&lt;/p>
&lt;p>另外，灯光和服装等设计，以及演员的表演也对舞台效果起了非常大的作用，在这里限于篇幅不再进行深入分析。让我印象比较深刻的有这些场景：&lt;/p>
&lt;p>众人进行默哀，此时一束光穿过树林投下来，渲染了庄严的气氛：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/18.webp" alt="">&lt;/p>
&lt;p>这个是乘客和司机坐在校车上，司机脸前有亮光，而乘客的区域是暗的；同时乘客头瘫向一边非常生动地表现了乘客此时是在车上（急转弯），并且非常疲惫：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/19.webp" alt="">&lt;/p>
&lt;p>你很难想象在舞台上能够表演出的比这个更生动形象的场景了。&lt;/p>
&lt;h2 id="主题表现">主题表现&lt;/h2>
&lt;p>本剧最主要的主题是表现人们在面对灾难时互相帮助的精神，但又不只于此。&lt;/p>
&lt;p>剧组使用相同的演员来饰演不同的角色。这里固然有演员人数的考虑，但我相信这其中蕴含了另一层深意。在剧中，同一个演员既是需要住宿的乘客，也是提供帮助的本地人；既是与事件有直接关系的遇难者家属，也是因看到电视画面而产生心理创伤的普通人……在恐怖主义面前，全体人类都是需要帮助的对象。你就是我，我就是你。帮助他人，从另一个角度来看，也是帮助自己。&lt;/p>
&lt;p>通过将灾难下的所有人视为一个整体，恐怖主义被放置在了全体人类的对立面。这以一种非常温和，却非常有力的方式蔑视了恐怖主义，即使全剧几乎没有一句台词直接谴责恐怖主义行为。&lt;/p>
&lt;p>这也是本剧的主调，即以普通人的视角来审视 911 事件给人们带来的创伤，以及我们从灾难后人们的互相帮助中所看到的人类本性中最珍贵的特质：善良，乐于助人，等等。本剧之所以有很强的感染力，在于它不仅仅赞颂了人类的珍贵特质——这样的话就成“主旋律”“唱颂歌”类型的剧了；它同时通过去世消防员的母亲等角色提醒我们恐怖主义给我们带来的巨大创伤，提醒我们不要忘记每一个亲历者以及背后无数个家庭的痛苦经历。正如镇长的唱段中所言：&lt;/p>
&lt;blockquote>
&lt;p>Tonight, we honor what was lost, but we also commemorate what we found.&lt;/p>
&lt;/blockquote>
&lt;p>这里有个小插曲，在《Something&amp;rsquo;s Missing》中，消防员的母亲在得知儿子去世后开口唱“You are here / At the end of the moment&amp;hellip;” 的时候，背后拉小提琴的小姐姐忍不住哭了：&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/come-from-away/17.webp" alt="">&lt;/p>
&lt;p>大概，共情是人类能够站在一起最重要的原因吧。&lt;/p>
&lt;h2 id="其他">其他&lt;/h2>
&lt;p>这部音乐剧有比较浓的商业音乐剧的气息，比如无处不在的政治正确，以及时不时的玩梗和抛笑话，非常的 entertaining；这部剧也非常“小清新”，并没有激烈的冲突，也没有非常深入的主题挖掘。另一方面，近乎完美的编排、令人眼花缭乱的舞台表现也反映了当下音乐剧商业化、产业化的趋势。当然，这些并不妨碍这部剧成为一部非常优秀的音乐剧作品。&lt;/p>
&lt;h2 id="后记">后记&lt;/h2>
&lt;p>《Come From Away》实际上是我看过的第一部音乐剧。去年官摄版上架 Apple TV+，恰巧我有订阅，之前也或多或少感受过音乐剧的魅力，于是便找了一个周末看了，之后就爱上了音乐剧（并选了《西方音乐剧史》课程 ^_^）。现在感觉自己挺幸运，第一次就能看到这样一部能够将音乐剧的魅力展现得淋漓尽致的剧（并且有 4k 画质和 Dolby Atmos 音效，which is 非常难得）。&lt;/p>
&lt;p>当然，上完课之后再重新看一遍这部音乐剧，获得了完全不一样的体验。与之前只能看剧情不同，现在能够看懂一些表现手法了，也能在上面有模有样地分析它究竟好在哪里。希望以后能够继续深入音乐剧的世界。感谢罗老师的指导！&lt;/p></content></item><item><title>协程上下文切换</title><link>https://yeshe.ng/blog/posts/2022/05/context-switch/</link><pubDate>Thu, 26 May 2022 13:02:01 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/05/context-switch/</guid><description>在用户态进行上下文切换</description><content>&lt;p>最近在研究 &lt;a href="https://github.com/PlatformLab/Arachne">Arachne&lt;/a> 做用户态线程调度的方法。&lt;/p>
&lt;p>众所周知，一个线程的状态包括：&lt;/p>
&lt;ul>
&lt;li>PC&lt;/li>
&lt;li>一些寄存器（即“上下文”）&lt;/li>
&lt;/ul>
&lt;p>切换线程时，需要切换上下文；为了将来能够恢复上下文，在切换时我们需要将当前的上下文保存在某个地方。我们尝试用 C 写一个简单的上下文切换。&lt;/p>
&lt;h2 id="上下文切换">上下文切换&lt;/h2>
&lt;p>进行上下文切换的汇编代码在上汇编课时就讲过：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-assembly" data-lang="assembly">swapcontext:
# save current context
pushq %r12
pushq %r13
pushq %r14
pushq %r15
pushq %rbx
pushq %rbp
movq %rsp, (%rsi)
# restore previously saved context
movq (%rdi), %rsp
popq %rbp
popq %rbx
popq %r15
popq %r14
popq %r13
popq %r12
&lt;/code>&lt;/pre>&lt;p>上面的代码做了几件事情：&lt;/p>
&lt;ol>
&lt;li>保存当前上下文
&lt;ol>
&lt;li>将被调用者保存寄存器保存到当前的调用栈上&lt;/li>
&lt;li>将当前的栈指针 &lt;code>%rsp&lt;/code> 保存到 &lt;code>%rsi&lt;/code> 所指向的内存地址上&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>恢复之前保存的上下文
&lt;ol>
&lt;li>将栈指针恢复为 &lt;code>%rdi&lt;/code> 所保存的另一个协程的栈顶&lt;/li>
&lt;li>恢复另一个协程的栈上面保存的被调用者保存寄存器&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ol>
&lt;p>这里需要理解一个问题：&lt;strong>为什么需要保存被调用者保存寄存器&lt;/strong>？&lt;/p>
&lt;p>“被调用者保存寄存器”指的是在调用子过程前后不发生改变的寄存器，即子过程需要在返回前将这些寄存器的值恢复。上面的 &lt;code>swapcontext&lt;/code> 是一个过程调用，因此需要遵循这一惯例。然而，与一般的过程调用不同，我们在这里实际上是切换到了另一个上下文中，因此我们实际上是恢复了另一个协程的上下文。&lt;/p>
&lt;p>这里可能会产生疑惑：调用者保存寄存器呢？实际上，调用 &lt;code>swapcontext&lt;/code> 的过程在调用它之前就已经把调用者保存寄存器保存好了（如果有必要的话）。理解的关键是：&lt;code>swapcontext&lt;/code> 是一个过程，而不是内联的汇编代码。&lt;/p>
&lt;h2 id="调用栈结构">调用栈结构&lt;/h2>
&lt;p>由于 &lt;code>swapcontext&lt;/code> 中会将当前的上下文保存到某个内存地址，我们之后只需要将这个内存地址传入 &lt;code>swapcontext&lt;/code> 就可以切换回来了。在这里，我们主要考虑在一开始如何切换到一个指定的函数上；我们需要初始化调用栈，使得 &lt;code>swapcontext&lt;/code> 后能够跳转到指定的函数。&lt;/p>
&lt;p>不难画出这样的一个调用栈：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Return Address |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Registers |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sp-&amp;gt; | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们将需要切换到的函数的地址保存在 &lt;code>Return Address&lt;/code> 中。在使用 &lt;code>popq&lt;/code> 指令恢复所有保存的寄存器后，&lt;code>rsp&lt;/code> 指向此函数地址，&lt;code>swapcontext&lt;/code> 返回（&lt;code>ret&lt;/code> 指令）后便跳转到函数的入口地址。需要注意的是 &lt;code>Return Address&lt;/code> 上方需要保留 8 字节的空间。&lt;/p>
&lt;p>栈的初始化过程如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define SPACE_FOR_SAVED_REGISTERS 48UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define STACK_SIZE 1024UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine2&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>sp2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">char&lt;/span> stack[&lt;span style="color:#ae81ff">8&lt;/span> &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Initialize stack for routine2
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> stack &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">sizeof&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Set return address
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">*&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">**&lt;/span>)sp2 &lt;span style="color:#f92672">=&lt;/span> routine2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Point to bottom of the register stack
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#66d9ef">char&lt;/span> &lt;span style="color:#f92672">*&lt;/span>)sp2 &lt;span style="color:#f92672">-&lt;/span> SPACE_FOR_SAVED_REGISTERS;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">//...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在这里，我们创建一个栈，并将 &lt;code>routine2&lt;/code> 的地址写到相应位置；&lt;code>sp2&lt;/code> 指向最后一个储存寄存器值的位置。&lt;/p>
&lt;p>此时，&lt;code>stack&lt;/code>、&lt;code>sp2&lt;/code> 和调用栈的结构如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Address of `routine2` |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Registers |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sp2-&amp;gt; | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | Stack Space |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>stack-&amp;gt; | |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> +-----------------------+
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="程序">程序&lt;/h2>
&lt;p>最后，我们写几个简单的上下文切换的 demo。&lt;/p>
&lt;h3 id="例子-1">例子 #1&lt;/h3>
&lt;p>在这里，我们进行两次上下文切换，从 &lt;code>routine&lt;/code> 切换到 &lt;code>routine2&lt;/code>，再切换回 &lt;code>routine&lt;/code>。&lt;/p>
&lt;p>其中，&lt;code>swapcontext&lt;/code> 需要加上 &lt;code>noinline&lt;/code> 的 attribute，这是为了保证 &lt;code>swapcontext&lt;/code> 作为过程被调用。&lt;/p>
&lt;p>另外，编译时需要加上 &lt;code>-fomit-frame-pointer&lt;/code>，否则编译器在编译时会在过程前后分别加上 &lt;code>pushq %rbp&lt;/code> 和 &lt;code>popq %rbp&lt;/code>，导致栈结构遭到破坏。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#include&lt;/span> &lt;span style="color:#75715e">&amp;lt;stdio.h&amp;gt;&lt;/span>&lt;span style="color:#75715e">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define SPACE_FOR_SAVED_REGISTERS 48UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define STACK_SIZE 1024UL
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">__attribute__&lt;/span>((noinline)) swapcontext(&lt;span style="color:#66d9ef">void&lt;/span>&lt;span style="color:#f92672">**&lt;/span> saved, &lt;span style="color:#66d9ef">void&lt;/span>&lt;span style="color:#f92672">**&lt;/span> target) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">asm&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;pushq %r12&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %r13&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %r14&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %r15&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %rbx&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pushq %rbp&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;movq %rsp, (%rsi)&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">asm&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;movq (%rdi), %rsp&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %rbp&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %rbx&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r15&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r14&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r13&lt;/span>&lt;span style="color:#ae81ff">\n\t&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;popq %r12&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>sp2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>sp;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">char&lt;/span> stack[&lt;span style="color:#ae81ff">8&lt;/span> &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Switch to routine2
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine() end&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine2&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine2()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Switch back
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Unreachable
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> printf(&lt;span style="color:#e6db74">&amp;#34;routine2() end&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Initialize stack for routine2
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> stack &lt;span style="color:#f92672">+&lt;/span> SPACE_FOR_SAVED_REGISTERS &lt;span style="color:#f92672">+&lt;/span> STACK_SIZE &lt;span style="color:#f92672">-&lt;/span> &lt;span style="color:#ae81ff">2&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">sizeof&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">*&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Set return address
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">*&lt;/span>(&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#f92672">**&lt;/span>)sp2 &lt;span style="color:#f92672">=&lt;/span> routine2;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Point to bottom of the register stack
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> sp2 &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#66d9ef">char&lt;/span> &lt;span style="color:#f92672">*&lt;/span>)sp2 &lt;span style="color:#f92672">-&lt;/span> SPACE_FOR_SAVED_REGISTERS;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> routine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;main routine&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>routine()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine() end
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>main routine
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="例子-2">例子 #2&lt;/h3>
&lt;p>继续尝试一些其他的用法。在这里，我们计算 0 累加至 9 的结果，并在每次循环时都切换一次上下文，然后立刻切换回来。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> sum &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> (&lt;span style="color:#66d9ef">int&lt;/span> i &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>; i &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">10&lt;/span>; i&lt;span style="color:#f92672">++&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sum &lt;span style="color:#f92672">+=&lt;/span> i;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;sum: %d&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>, sum);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">routine2&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> (;;) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;routine2()&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> swapcontext(&lt;span style="color:#f92672">&amp;amp;&lt;/span>sp, &lt;span style="color:#f92672">&amp;amp;&lt;/span>sp2);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>routine()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>routine2()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sum: 45
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="汇编结果">汇编结果&lt;/h3>
&lt;p>使用 &lt;code>-O1&lt;/code> 编译上面第二个例子（去掉所有 &lt;code>printf&lt;/code>），&lt;code>routine&lt;/code> 的汇编代码如下：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-assembly" data-lang="assembly">0000000000000639 &amp;lt;routine&amp;gt;:
639: 55 push %rbp
63a: 53 push %rbx
63b: bb 0a 00 00 00 mov $0xa,%ebx
640: 48 8d 2d f9 09 20 00 lea 0x2009f9(%rip),%rbp # 201040 &amp;lt;sp&amp;gt;
647: 48 89 ee mov %rbp,%rsi
64a: 48 8d 3d 47 0e 20 00 lea 0x200e47(%rip),%rdi # 201498 &amp;lt;sp2&amp;gt;
651: e8 af ff ff ff callq 605 &amp;lt;swapcontext&amp;gt;
656: 83 eb 01 sub $0x1,%ebx
659: 75 ec jne 647 &amp;lt;routine+0xe&amp;gt;
65b: 5b pop %rbx
65c: 5d pop %rbp
65d: c3 retq
&lt;/code>&lt;/pre>&lt;p>可以看到，&lt;code>for&lt;/code> 循环使用 &lt;code>%ebx&lt;/code>（被调用者保存）进行计数，在调用 &lt;code>swapcontext&lt;/code> 前后并没有保存其值。保存这个寄存器是 &lt;code>swapcontext&lt;/code> 的责任。&lt;/p></content></item><item><title>Operating Systems: Three Easy Pieces 学习笔记（五）</title><link>https://yeshe.ng/blog/posts/2022/05/ostep-concurrency/</link><pubDate>Wed, 25 May 2022 11:44:11 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/05/ostep-concurrency/</guid><description>Concurrency</description><content>&lt;h2 id="26-concurrency-an-introduction">26 Concurrency: An Introduction&lt;/h2>
&lt;p>Each thread is very much like a separate process, except for one difference: they share the same address space and thus can access the same data.&lt;/p>
&lt;p>State of a single thread:&lt;/p>
&lt;ul>
&lt;li>program counter&lt;/li>
&lt;li>private set of registers (context switch is needed)&lt;/li>
&lt;/ul>
&lt;p>which are stored in &lt;strong>process control block&lt;/strong>.&lt;/p>
&lt;p>Any stack-allocated variables, parameters, return values, and other things that we put on the stack will be placed in what is sometimes called &lt;strong>thread-local&lt;/strong> storage.&lt;/p>
&lt;h3 id="261-why-use-threads">26.1 Why Use Threads?&lt;/h3>
&lt;ul>
&lt;li>parallelism: speed up using multi-core processors&lt;/li>
&lt;li>avoid blocking during I/O&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Computers are hard enough to understand without concurrency. Unfortunately, with concurrency, it simply gets worse. Much worse.&lt;/p>
&lt;/blockquote>
&lt;h3 id="264-the-heart-of-the-problem-uncontrolled-scheduling">26.4 The Heart Of The Problem: Uncontrolled Scheduling&lt;/h3>
&lt;p>&lt;strong>Race condition&lt;/strong>: the results depend on the timing execution of the code.&lt;/p>
&lt;p>&lt;strong>Critical section&lt;/strong>: a piece of code that accesses a shared variable (or more generally, a shared resource) and must not be concurrently executed by more than one thread.&lt;/p>
&lt;p>&lt;strong>Mutal exclusion&lt;/strong>: if one thread is executing within the critical section, the others will be prevented from doing so.&lt;/p>
&lt;h2 id="27-interlude-thread-api">27 Interlude: Thread API&lt;/h2>
&lt;h3 id="274-condition-variables">27.4 Condition Variables&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">pthread_cond_wait&lt;/span>(pthread_cond_t &lt;span style="color:#f92672">*&lt;/span>cond, pthread_mutex_t &lt;span style="color:#f92672">*&lt;/span>mutex);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">pthread_cond_signal&lt;/span>(pthread_cond_t &lt;span style="color:#f92672">*&lt;/span>cond);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To wait for a signal:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>pthread_mutex_lock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">while&lt;/span> (ready &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pthread_cond_wait(&lt;span style="color:#f92672">&amp;amp;&lt;/span>cond, &lt;span style="color:#f92672">&amp;amp;&lt;/span>lock); &lt;span style="color:#75715e">// will release the lock when sleep
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// and re-acquire before wakeup
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>pthread_mutex_unlock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To send a signal:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>pthread_mutex_lock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ready &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pthread_cond_signal(&lt;span style="color:#f92672">&amp;amp;&lt;/span>cond);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pthread_mutex_unlock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="28-locks">28 Locks&lt;/h2>
&lt;h3 id="284-evaluating-locks">28.4 Evaluating Locks&lt;/h3>
&lt;p>Criteria:&lt;/p>
&lt;ul>
&lt;li>mutal exclusion&lt;/li>
&lt;li>fairness&lt;/li>
&lt;li>performance&lt;/li>
&lt;/ul>
&lt;h3 id="286-a-failed-attempt-just-using-loadsstores">28.6 A Failed Attempt: Just Using Loads/Stores&lt;/h3>
&lt;p>Problems:&lt;/p>
&lt;ul>
&lt;li>No mutal exclusion&lt;/li>
&lt;li>Poor performance: spin-waiting&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/lock-flag.webp" alt="">&lt;/p>
&lt;p>The problem: set after test (maybe after an interrupt).&lt;/p>
&lt;h3 id="287-building-working-spin-locks-with-test-and-set">28.7 Building Working Spin Locks with Test-And-Set&lt;/h3>
&lt;p>Test-and-set instruction (&lt;code>xchg&lt;/code> on x86) is something like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">testAndSet&lt;/span>(&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#f92672">*&lt;/span>old_ptr, &lt;span style="color:#66d9ef">int&lt;/span> new) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> old &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">*&lt;/span>old_ptr; &lt;span style="color:#75715e">// fetch old value at old_ptr
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">*&lt;/span>old_ptr &lt;span style="color:#f92672">=&lt;/span> new; &lt;span style="color:#75715e">// store ’new’ into old_ptr
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">return&lt;/span> old; &lt;span style="color:#75715e">// return the old value
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lock and unlock:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">lock&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>lock) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (testAndSet(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag, &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ; &lt;span style="color:#75715e">// spin-wait (do nothing)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">unlock&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>lock) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="288-evaluating-spin-locks">28.8 Evaluating Spin Locks&lt;/h3>
&lt;ul>
&lt;li>correctness: yes&lt;/li>
&lt;li>fairness: no, maybe spinning forever (starvation)&lt;/li>
&lt;li>performance: bad in single CPU case&lt;/li>
&lt;/ul>
&lt;h3 id="289-compare-and-swap">28.9 Compare-And-Swap&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">compareAndSwap&lt;/span>(&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#f92672">*&lt;/span>ptr, &lt;span style="color:#66d9ef">int&lt;/span> expected, &lt;span style="color:#66d9ef">int&lt;/span> new) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> original &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">*&lt;/span>ptr;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (original &lt;span style="color:#f92672">==&lt;/span> expected)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>ptr &lt;span style="color:#f92672">=&lt;/span> new;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> original;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2810-load-linked-and-store-conditional">28.10 Load-Linked and Store-Conditional&lt;/h3>
&lt;p>&lt;strong>Store-conditional&lt;/strong>: only succeeds (and updates the value stored at the address just load-linked from) if no intervening store to the address has taken place.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">lock&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>lock) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (loadLinked(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#f92672">||&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> storeConditional(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag, &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2811-fetch-and-add">28.11 Fetch-And-Add&lt;/h3>
&lt;p>The pseudo C code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">fetchAndAdd&lt;/span>(&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#f92672">*&lt;/span>ptr) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> old &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">*&lt;/span>ptr;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>ptr &lt;span style="color:#f92672">=&lt;/span> old &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> old;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lock and unlock:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">lock&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>lock) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> myTurn &lt;span style="color:#f92672">=&lt;/span> fetchAndAdd(&lt;span style="color:#f92672">&amp;amp;&lt;/span>lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>ticket);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>turn &lt;span style="color:#f92672">!=&lt;/span> myTurn)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">unlock&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>lock) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>turn &lt;span style="color:#f92672">=&lt;/span> lock&lt;span style="color:#f92672">-&amp;gt;&lt;/span>turn &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This ensures progress for all threads.&lt;/p>
&lt;h3 id="2813-a-simple-approach-just-yield-baby">28.13 A Simple Approach: Just Yield, Baby&lt;/h3>
&lt;p>&lt;strong>Yield&lt;/strong> is simply a system call that moves the caller from the &lt;strong>running&lt;/strong> state to the &lt;strong>ready&lt;/strong> state, and thus promotes another thread to running. Thus, the yielding thread essentially &lt;strong>deschedules&lt;/strong> itself.&lt;/p>
&lt;ul>
&lt;li>Still costly: context switches.&lt;/li>
&lt;li>Still unfair: endless yield loop.&lt;/li>
&lt;/ul>
&lt;h3 id="2814-using-queues-sleeping-instead-of-spinning">28.14 Using Queues: Sleeping Instead Of Spinning&lt;/h3>
&lt;blockquote>
&lt;p>The real problem with our previous approaches is that they leave too much to chance.&lt;/p>
&lt;/blockquote>
&lt;p>Solaris provides two calls: &lt;code>park()&lt;/code> to put a calling thread to sleep, and &lt;code>unpark(threadID)&lt;/code> to wake a particular thread as designated by &lt;code>threadID&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">typedef&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> __lock_t {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> flag;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> guard;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queue_t &lt;span style="color:#f92672">*&lt;/span>q;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>} lock_t;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">lock_init&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>m) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>guard &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queue_init(m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>q);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">lock&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>m) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (testAndSet(&lt;span style="color:#f92672">&amp;amp;&lt;/span>m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>guard, &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ; &lt;span style="color:#75715e">// acquire guard lock by spinning
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> (m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>; &lt;span style="color:#75715e">// lock is acquired
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>guard &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queue_add(m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>q, gettid());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> setpark();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>guard &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> park();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">unlock&lt;/span>(lock_t &lt;span style="color:#f92672">*&lt;/span>m) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (testAndSet(&lt;span style="color:#f92672">&amp;amp;&lt;/span>m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>guard, &lt;span style="color:#ae81ff">1&lt;/span>) &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ; &lt;span style="color:#75715e">// acquire guard lock by spinning
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> (queue_empty(m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>q))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>flag &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>; &lt;span style="color:#75715e">// let go of lock; no one wants it
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> unpark(queue_remove(m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>q)); &lt;span style="color:#75715e">// hold lock
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#75715e">// (for next thread!)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> m&lt;span style="color:#f92672">-&amp;gt;&lt;/span>guard &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="29-lock-based-concurrent-data-structures">29 Lock-based Concurrent Data Structures&lt;/h2>
&lt;h3 id="291-concurrent-counters">29.1 Concurrent Counters&lt;/h3>
&lt;p>Approximate counting: record counter values locally, and periodically transfer them to the global counter.&lt;/p>
&lt;h3 id="292-concurrent-linked-lists">29.2 Concurrent Linked Lists&lt;/h3>
&lt;p>A hand-over-hand linked list may not be faster than the simple single lock approach, as the overhead of acquiring and releasing every node along the traversal path is high.&lt;/p>
&lt;h3 id="293-concurrent-queues">29.3 Concurrent Queues&lt;/h3>
&lt;h3 id="294-concurrent-hash-table">29.4 Concurrent Hash Table&lt;/h3>
&lt;h2 id="30-condition-variables">30 Condition Variables&lt;/h2>
&lt;h3 id="301-definition-and-routines">30.1 Definition and Routines&lt;/h3>
&lt;p>A &lt;strong>condition variable&lt;/strong> is an explicit queue that threads can put themselves on when some state of execution (i.e., some &lt;strong>condition&lt;/strong>) is not as desired (by &lt;strong>waiting&lt;/strong> on the condition); some other thread, when it changes said state, can then wake one (or more) of those waiting threads and thus allow them to continue (by &lt;strong>signaling&lt;/strong> on the condition).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">pthread_cond_wait&lt;/span>(pthread_cond_t &lt;span style="color:#f92672">*&lt;/span>cond, pthread_mutex_t &lt;span style="color:#f92672">*&lt;/span>mutex);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">pthread_cond_signal&lt;/span>(pthread_cond_t &lt;span style="color:#f92672">*&lt;/span>cond);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>wait()&lt;/code> unlocks a mutex (assumed to be locked) and put the calling thread to sleep;
it re-acquires the lock before returning.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> done &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pthread_mutex_t m &lt;span style="color:#f92672">=&lt;/span> PTHREAD_MUTEX_INITIALIZER;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pthread_cond_t c &lt;span style="color:#f92672">=&lt;/span> PTHREAD_COND_INITIALIZER;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">thr_exit&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pthread_mutex_lock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>m);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> done &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pthread_cond_signal(&lt;span style="color:#f92672">&amp;amp;&lt;/span>c);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pthread_mutex_unlock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>m);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">thr_join&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pthread_mutex_lock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>m);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">while&lt;/span> (done &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pthread_cond_wait(&lt;span style="color:#f92672">&amp;amp;&lt;/span>c, &lt;span style="color:#f92672">&amp;amp;&lt;/span>m);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pthread_mutex_unlock(&lt;span style="color:#f92672">&amp;amp;&lt;/span>m);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Some questions:&lt;/p>
&lt;p>&lt;strong>Why is the &lt;code>done&lt;/code> state needed?&lt;/strong>&lt;/p>
&lt;p>If the child runs immediately and calls &lt;code>thr_exit()&lt;/code> immediately, then although the
child will signal, there is no thread asleep. When the parent calls &lt;code>wait()&lt;/code>, it will
be stuck.&lt;/p>
&lt;p>&lt;strong>Why is the mutex needed?&lt;/strong>&lt;/p>
&lt;p>The child might change the state &lt;em>before&lt;/em> the parent goes to sleep, and the parent will
sleep forever. If there is a mutex, the child can only alter the state after the parent goes
to sleep (and thus releasing the mutex).&lt;/p>
&lt;blockquote>
&lt;p>Always hold the lock while signaling!&lt;/p>
&lt;/blockquote>
&lt;h3 id="302-the-producerconsumer-bounded-buffer-problem">30.2 The Producer/Consumer (Bounded Buffer) Problem&lt;/h3>
&lt;blockquote>
&lt;p>Thanks to Mesa semantics, a simple rule to remember with condition variables is to &lt;strong>always use while loops&lt;/strong>.&lt;/p>
&lt;/blockquote>
&lt;h3 id="303-covering-conditions">30.3 Covering Conditions&lt;/h3>
&lt;p>&lt;code>pthread_cond_broadcast()&lt;/code>: waking up all waiting threads; downside: negative performace impact.&lt;/p></content></item><item><title>Read Phenomena and Isolation Levels</title><link>https://yeshe.ng/blog/posts/2022/05/isolation/</link><pubDate>Thu, 05 May 2022 23:11:19 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/05/isolation/</guid><description>In database systems, isolation determines how transaction integrity is visible to other users and systems.</description><content>&lt;p>Reference: &lt;a href="https://en.wikipedia.org/wiki/Isolation_(database_systems)">https://en.wikipedia.org/wiki/Isolation_(database_systems)&lt;/a>&lt;/p>
&lt;p>Isolation is typically defined at database level as a property that defines how or when the changes made by one operation become visible to others.&lt;/p>
&lt;h2 id="read-phenomena">Read phenomena&lt;/h2>
&lt;h3 id="dirty-reads">Dirty reads&lt;/h3>
&lt;p>A dirty read (aka uncommitted dependency) occurs when a transaction is allowed to read data from a row that has been modified by another running transaction and &lt;em>not yet committed&lt;/em>.&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/dirty-read.webp" alt="">&lt;/p>
&lt;h3 id="non-repetable-reads">Non-repetable reads&lt;/h3>
&lt;p>A non-repeatable read occurs when, during the course of a transaction, a row is &lt;em>retrieved twice&lt;/em> and the values within the row &lt;em>differ&lt;/em> between reads.&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/non-repeatable-read.webp" alt="">&lt;/p>
&lt;h3 id="phantom-reads">Phantom reads&lt;/h3>
&lt;p>A phantom read occurs when, in the course of a transaction, &lt;em>new rows are added or removed&lt;/em> by another transaction to the records being read.&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/05/phantom-read.webp" alt="">&lt;/p>
&lt;h2 id="isolation-levels">Isolation levels&lt;/h2>
&lt;blockquote>
&lt;p>Of the four ACID properties in a DBMS (Database Management System), the isolation property is the one most often relaxed.&lt;/p>
&lt;p>For many database applications, the majority of database transactions can be constructed to avoid requiring high isolation levels (e.g. SERIALIZABLE level), thus reducing the locking overhead for the system.&lt;/p>
&lt;/blockquote>
&lt;h3 id="serializable">Serializable&lt;/h3>
&lt;p>The execution of concurrent SQL-transactions at isolation level SERIALIZABLE is guaranteed to be serializable.&lt;/p>
&lt;p>When using non-lock based concurrency control&amp;hellip; And also range-locks must be acquired when a SELECT query uses a ranged WHERE clause, especially to avoid the phantom reads phenomenon.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Snapshot isolation&lt;/strong>&lt;/p>
&lt;p>snapshot isolation is a guarantee that all reads made in a transaction will see a consistent snapshot of the database (in practice it reads the last committed values that existed at the time it started), and the transaction itself will successfully commit only if no updates it has made conflict with any concurrent updates made since that snapshot.&lt;/p>
&lt;/blockquote>
&lt;h3 id="repeatable-reads">Repeatable reads&lt;/h3>
&lt;p>Range-locks are not managed, so &lt;em>phantom reads&lt;/em> can occur.&lt;/p>
&lt;h3 id="read-committed">Read committed&lt;/h3>
&lt;p>Read locks are released as soon as the SELECT operation is performed, so the &lt;em>non-repeatable reads&lt;/em> phenomenon can occur in this isolation level.&lt;/p>
&lt;h3 id="read-uncommitted">Read uncommitted&lt;/h3>
&lt;p>No read locks. Dirty reads are allowed, so one transaction may see not-yet-committed changes made by other transactions.&lt;/p></content></item><item><title>Concurrency Control for Database Transactions</title><link>https://yeshe.ng/blog/posts/2022/04/database/</link><pubDate>Mon, 11 Apr 2022 23:27:56 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/04/database/</guid><description>DATABASE SYSTEMS: The Complete Book</description><content>&lt;p>来自 &lt;em>DATABASE SYSTEMS: The Complete Book&lt;/em>&lt;/p>
&lt;h2 id="serial-and-serializable-schedules">Serial and Serializable Schedules&lt;/h2>
&lt;p>&lt;strong>Serial schedule&lt;/strong>: no mixing of actions from different transactions.&lt;/p>
&lt;p>&lt;strong>Serializable schedule&lt;/strong>: if there is a &lt;em>serial&lt;/em> schedule S&amp;rsquo; such that for
every initial database state, the &lt;em>effects&lt;/em> of S and S&amp;rsquo; are the same.&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/serializable.webp" alt="">&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/nonserializable.webp" alt="">&lt;/p>
&lt;p>The details of the transactions do matter, but it is not realistic for the
scheduler to concern itself with the details of computation undertaken by
transactions.&lt;/p>
&lt;p>We only care about the reads $r_T(X)$ and writes $w_T(X)$ performed by the transaction $T$.&lt;/p>
&lt;h2 id="conflict-serializability">Conflict-Serializability&lt;/h2>
&lt;p>Actions that conflict (i.e. their order may not be swapped):&lt;/p>
&lt;ol>
&lt;li>Two actions of the same transaction.&lt;/li>
&lt;li>Two writes of the same database element by different transactions.&lt;/li>
&lt;li>A read and a write of the same database element by different transactions.&lt;/li>
&lt;/ol>
&lt;p>In other words, for actions from &lt;em>different&lt;/em> transactions, they conflict when:&lt;/p>
&lt;ol>
&lt;li>They involve the same database element, and&lt;/li>
&lt;li>At least one is a write.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Conflict-equivalent&lt;/strong>: two schedules can be turned one into the other by
a sequence of nonconflicting swaps of adjacent actions.&lt;/p>
&lt;p>&lt;strong>Conflict-serializable&lt;/strong>: the schedule is conflict-equivalent to a &lt;em>serial&lt;/em> schedule&lt;/p>
&lt;p>conflict-serializability $\Rightarrow$ serializability&lt;/p>
&lt;p>&lt;strong>Precedence&lt;/strong>: if there are actions $A_1$ of $T_1$ and $A_2$ of $T_2$, such that:&lt;/p>
&lt;ul>
&lt;li>$A_1$ is ahead of $A_2$ in the schedule $S$,&lt;/li>
&lt;li>Both $A_1$ and $A_2$ involve the same database element, and&lt;/li>
&lt;li>At least one of $A_1$ and $A_2$ is a write action.&lt;/li>
&lt;/ul>
&lt;p>then we say that $T_1$ takes precedence over $T_2$ in $S$ ($T_1&amp;lt;_ST_2$).&lt;/p>
&lt;p>Any conflict-equivalent serial schedule must have $T_1$ before $T_2$.&lt;/p>
&lt;p>If the precedence graph is acyclic, then the schedule is conflict-equivalent.&lt;/p>
&lt;p>Proof: for $n$, arrange the schedule by (Actions of $T_i$)(Actions of the other $n-1$ transactions) where $T_i$ is one node without an in arc.&lt;/p>
&lt;h2 id="enforcing-serializability-by-locks">Enforcing Serializability by Locks&lt;/h2>
&lt;p>The use of lock:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Consistency of Transactions&lt;/strong>: Actions and locks must relate in the expected ways:
&lt;ol>
&lt;li>A transaction can only read or write an element if it previously was granted a lock on that element and hasn’t yet released the lock.&lt;/li>
&lt;li>If a transaction locks an element, it must later unlock that element.&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>&lt;strong>Legality of Schedules&lt;/strong>: Locks must have their intended meaning: no two transactions may have locked the same element without one having first released the lock.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>two-phase locking&lt;/strong> condition than can ensure conflict-serializability:&lt;/p>
&lt;ul>
&lt;li>In every transaction, all lock actions precede all unlock actions.&lt;/li>
&lt;/ul>
&lt;p>A transaction that obeys the 2PL condition is said to be a &lt;strong>two-phase-locked transaction&lt;/strong>, or &lt;strong>2PL transaction&lt;/strong>.&lt;/p>
&lt;p>Proof: for $n$, arrange the schedule by (Actions of $T_i$)(Actions of the other $n-1$ transactions) where $T_i$ is the transaction with the first &lt;em>unlock&lt;/em> action $u_iX$.&lt;/p>
&lt;p>The risk of deadlock:&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/deadlock.webp" alt="">&lt;/p>
&lt;h2 id="locking-systems-with-several-lock-modes">Locking Systems With Several Lock Modes&lt;/h2>
&lt;p>Lock modes:&lt;/p>
&lt;ul>
&lt;li>shared&lt;/li>
&lt;li>exclusive&lt;/li>
&lt;/ul>
&lt;p>The compatibility matrix:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">&lt;/th>
&lt;th style="text-align:center">Request S&lt;/th>
&lt;th style="text-align:center">Request X&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">Currently S&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">Currently X&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The deadlock problem caused by upgrading locks:&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/upgrading-deadlock.webp" alt="">&lt;/p>
&lt;p>Avoid the problem with a third lock mode: &lt;strong>update locks&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>An update lock $ul_i(X)$ gives transaction $T_j$ only the privilege to read X, not to write X;&lt;/li>
&lt;li>Only the update lock can be upgraded to a write lock later; a read lock cannot be upgraded.&lt;/li>
&lt;/ul>
&lt;p>The compatibility matrix:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">&lt;/th>
&lt;th style="text-align:center">Request S&lt;/th>
&lt;th style="text-align:center">Request X&lt;/th>
&lt;th style="text-align:center">Request U&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">Currently S&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">Currently X&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">Currently U&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The columns for U and S locks are the same, and the rows for U and X locks are the same.&lt;/p>
&lt;p>And now:&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/upgrading-updatelock.webp" alt="">&lt;/p>
&lt;h2 id="an-architecture-for-a-locking-scheduler">An Architecture for a Locking Scheduler&lt;/h2>
&lt;p>The architecture discussed here: the scheduler insert and release locks.&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/scheduler-insert-lock.webp" alt="">&lt;/p>
&lt;ol>
&lt;li>Part 1: inserts appropriate lock actions ahead of all database-access operations&lt;/li>
&lt;li>Part 2: for received action,
&lt;ol>
&lt;li>if the issuing transcation is already delayed, then delay this action&lt;/li>
&lt;li>otherwise,
&lt;ol>
&lt;li>if the action is db access (&lt;em>the required locks are already been granted&lt;/em>), execute it&lt;/li>
&lt;li>if is lock action
&lt;ol>
&lt;li>if the lock can be granted: modify the lock table&lt;/li>
&lt;li>otherwise, request the lock and delay&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ol>
&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>When a transcation commits or aborts, Part 1 releases all locks held by the transaction; if any transactions are waiting for any of these locks, notify Part 2&lt;/li>
&lt;li>Part 2 checks which actions can be executed now&lt;/li>
&lt;/ol>
&lt;p>The lock table entry:&lt;/p>
&lt;ol>
&lt;li>The &lt;em>group mode&lt;/em>: the most stringent conditions&lt;/li>
&lt;li>The &lt;em>waiting bit&lt;/em>: at least one transaction is waiting&lt;/li>
&lt;li>A list of transactions that hold or waiting for the lock&lt;/li>
&lt;/ol>
&lt;h2 id="hierarchies-of-database-elements">Hierarchies of Database Elements&lt;/h2>
&lt;p>&lt;strong>Warning locks&lt;/strong>: useful for hierarchical structure.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">&lt;/th>
&lt;th style="text-align:center">IS&lt;/th>
&lt;th style="text-align:center">IX&lt;/th>
&lt;th style="text-align:center">S&lt;/th>
&lt;th style="text-align:center">X&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">IS&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">IX&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">S&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">Yes&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">X&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;td style="text-align:center">No&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;strong>phantom tuple&lt;/strong>: one that should have been locked but wasn’t, because it didn’t exist at the time the locks were taken.&lt;/p>
&lt;p>Solution: acquire an X lock on the entire relation.&lt;/p>
&lt;h2 id="the-tree-protocol">The Tree Protocol&lt;/h2>
&lt;p>The problem: only one transaction that is not read-only can access the B-tree at any time, because the root node must be locked (at least with an update lock).&lt;/p>
&lt;p>Solution: as soon as a transaction moves to a child of the root and observes the (quite usual) situation that rules out a rewrite of the root, we would like to release the lock on the root; but it violates 2PL, so a new protocol is needed.&lt;/p>
&lt;p>&lt;strong>The tree protocol&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>The schedule is legal (i.e. does not violate lock schemes).&lt;/li>
&lt;li>A transaction’s first lock may be at any node of the tree.&lt;/li>
&lt;li>Subsequent locks may only be acquired if the transaction currently has a lock on the parent node.&lt;/li>
&lt;li>Nodes may be unlocked at any time.&lt;/li>
&lt;li>A transaction may not relock a node on which it has released a lock, even if it still holds a lock on the node’s parent.&lt;/li>
&lt;/ol>
&lt;p>Conclusion: the precedence graph must always be acyclic if the tree protocol is obeyed. Proof:&lt;/p>
&lt;ul>
&lt;li>If two transactions lock several elements in common, then they are all locked in the same order.&lt;/li>
&lt;li>If $T_i$ locks the root before $T_j$, then $T_i$ locks every node in common with $T_j$ before $T_j$ does.&lt;/li>
&lt;/ul>
&lt;h2 id="concurrency-control-by-timestamps">Concurrency Control by Timestamps&lt;/h2>
&lt;p>Two approaches to generate timestamp:&lt;/p>
&lt;ol>
&lt;li>use the system clock&lt;/li>
&lt;li>the scheduler maintaining a counter&lt;/li>
&lt;/ol>
&lt;p>For the database element $X$:&lt;/p>
&lt;ol>
&lt;li>$RT(X)$: the read time of $X$&lt;/li>
&lt;li>$WT(X)$: the write time of $X$&lt;/li>
&lt;li>$C(X)$: the commit bit fot $X$, true when the most recent transaction to write $X$ has already commited.&lt;/li>
&lt;/ol>
&lt;p>Two kinds of unrealizable behaviours:&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/read-too-late.webp" alt="">&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/write-too-late.webp" alt="">&lt;/p>
&lt;p>The problem with dirty data:&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/dirty-read.webp" alt="">&lt;/p>
&lt;p>$T$ better delays until $U$ commits.&lt;/p>
&lt;p>The commit bit will be false.&lt;/p>
&lt;p>The &lt;strong>Thomas write rule&lt;/strong>: the writes can be skipped when a write with a later write-time is already in place (read intention for value written by T is too-late, as the value is written by a logically latter transaction).&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/thomas-write-rule.webp" alt="">&lt;/p>
&lt;p>The scheduler, in response to a read or write request from a transaction T has the choice of:&lt;/p>
&lt;ul>
&lt;li>Granting the request,&lt;/li>
&lt;li>Aborting T and restarting T with a new timestamp&lt;/li>
&lt;li>Delaying T and later deciding whether to abort or grant the request.&lt;/li>
&lt;/ul>
&lt;p>The rules for timestamp-based scheduling:&lt;/p>
&lt;ol>
&lt;li>The scheduler receives a request $r_T(X)$.
&lt;ol>
&lt;li>$TS(T)\ge WT(X)$: physically realizable.
&lt;ol>
&lt;li>$C(X)$ is true: grant the request; set $RT(X)$ if $TS(T)&amp;gt;RT(X)$.&lt;/li>
&lt;li>$C(X)$ is false: delay until becomes true, or that transaction aborts (to avoid dirty reads).&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>$TS(T)&amp;lt;WT(X)$: physically unrealizable, rollback $T$.&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>The scheduler receives a request $w_T(X)$.
&lt;ol>
&lt;li>$TS(T) \ge RT(X)$ and $TS(T) &amp;gt; WT(X)$: physically realizable and must be performed (?).
&lt;ol>
&lt;li>Write $X$,&lt;/li>
&lt;li>Set $WT(X) := TS(T)$, and&lt;/li>
&lt;li>Set $C(X) := \texttt{false}$.&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>$TS(T) \ge RT(X)$ but $TS(T) &amp;lt; WT(X)$: physically realizable, and
&lt;ol>
&lt;li>If $C(X)$ is true, ignore the write by T (the thomas write rule);&lt;/li>
&lt;li>otherwise, delay T until the previous write operation commits or aborts.&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>$TS(T) &amp;lt; RT(X)$: physically unrealizable, roll back T.&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>The scheduler receives a request to &lt;em>commit T&lt;/em>: find all elements written by T, and set $C(X) := \texttt{true}$; the delayed transactions are allowed to proceed.&lt;/li>
&lt;li>The scheduler receives a request to &lt;em>abort T&lt;/em>, or to rollback: check all delaying transcation waiting for any element that T wrote to see if they can proceed.&lt;/li>
&lt;/ol>
&lt;p>The use of &lt;strong>multiversion timestamps&lt;/strong>: to provide an appropriate version for a read action (the version of X written immediately before T theoretically executed), even if the latest version is written after the transcation.&lt;/p>
&lt;p>Comparision of locking and timestamp schemes:&lt;/p>
&lt;p>Generally, timestamping is superior in situations where either most transactions are read-only, or it is rare that concurrent transactions will try to read and write the same element. In high-conflict situations, locking performs better.&lt;/p>
&lt;ul>
&lt;li>Locking will frequently delay transactions as they wait for locks.&lt;/li>
&lt;li>If concurrent transactions frequently read and write elements in common, then &lt;em>rollbacks&lt;/em> will be frequent in a timestamp scheduler.&lt;/li>
&lt;/ul>
&lt;h2 id="concurrency-control-by-validation">Concurrency Control by Validation&lt;/h2>
&lt;p>Validation differs from timestamping principally in that the scheduler maintains a record of what &lt;em>active&lt;/em> transactions are doing, rather than keeping read and write times for all database elements.&lt;/p>
&lt;p>Denote the write set and read set as $WS(T)$ and $RS(T)$. Transactions are executed in three phases:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Read&lt;/strong>. The transaction reads from the database all the elements in its read set, and also computes (locally) the results it is going to write.&lt;/li>
&lt;li>&lt;strong>Validate&lt;/strong>. The transaction compares its read and write sets with those of other transactions; rollback if fails.&lt;/li>
&lt;li>&lt;strong>Write&lt;/strong>. The transaction writes to the database the values for the elements in its write set.&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>Thus, the validation-based scheduler has an assumed serial order of the transactions to work with, and it bases its decision to validate or not on whether the transactions’ behaviors are consistent with this serial order.&lt;/p>
&lt;/blockquote>
&lt;p>The scheduler maintains three sets:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>START&lt;/strong>: the set of transactions that have started, but not yet completed validation.&lt;/li>
&lt;li>&lt;strong>VAL&lt;/strong>: the set of transactions that have been validated but not yet finished the writing in phase 3.&lt;/li>
&lt;li>&lt;strong>FIN&lt;/strong>: the set of transactions that have completed phase 3.&lt;/li>
&lt;/ol>
&lt;p>Some examples:&lt;/p>
&lt;p>A later transaction might read a value before an earlier transaction writes the value.&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/validate-1.webp" alt="">&lt;/p>
&lt;p>A later transaction might write before a validated, earlier transaction, as the write of the later one might occur earlier.&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/04/validate-2.webp" alt="">&lt;/p>
&lt;p>Summarization: to validate a transaction T:&lt;/p>
&lt;ul>
&lt;li>Check that $RS(T) \cap WS(U) = \emptyset$ for any previously validated U that did not finish before T &lt;em>started&lt;/em>, i.e., if $FIN(U) &amp;gt; START(T)$.&lt;/li>
&lt;li>Check that $WS(T) \cap WS(U) = \emptyset$ for any previously validated U that did not finish before T &lt;em>validated&lt;/em>, i.e., if $FIN(U) &amp;gt; VAL(T)$.&lt;/li>
&lt;/ul>
&lt;h2 id="comparison-of-three-concurrency-control-mechanisms">Comparison of Three Concurrency-Control Mechanisms&lt;/h2>
&lt;p>Storage: all schemes (can be optimized to) use storage space proportional to the sum over all active transactions of the number of database elements the transaction accesses.&lt;/p>
&lt;p>Performace:&lt;/p>
&lt;ul>
&lt;li>Locking delays transactions but avoids rollbacks, even when interaction is high.&lt;/li>
&lt;li>Timestamps and validation do not delay transactions, but can cause them to rollback.&lt;/li>
&lt;li>If interference is low, then neither timestamps nor validation will cause many rollbacks, and may be preferable to locking because they generally have lower overhead than a locking scheduler.&lt;/li>
&lt;li>When a rollback is necessary, timestamps catch some problems earlier than validation, which always lets a transaction do all its internal work before considering whether the transaction must rollback.&lt;/li>
&lt;/ul>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>同时执行多个事务时，需要保证每个事务的原子性，同时还必须保持一定的先后次序。&lt;/p>
&lt;p>对 schedule 最严格的要求是串行性（serial），当然这并不能做到并发。退一步，我们不需要严格保证不同事务处理的时间不互相重叠，但需要保证最终的效果和串行执行一致，即“可串行化”（serializable schedule）。当然，这个条件覆盖的范围太大，很难判断 schedule 是否满足可串行化条件。因此，我们需要增加更严格的条件，使得我们可以有简单的方法判断。当然，这样一来，满足条件的 schedule 只是可串行化 schedule 的一个真子集。&lt;/p>
&lt;p>第一大类办法是使用锁，并加上 2PL 条件，使得 schedule 满足“冲突可串行化”（confilict-serializable）。上锁有两个需要解决的问题：锁的类型及其兼容性，以及锁的粒度。&lt;/p>
&lt;p>第二大类办法是“乐观的”（optimistic），即假设不会发生不可串行化的事件，仅当的确出现违反可串行化条件时采取措施。Optimistic 方法包括两种：by timestamp，即通过比较事务开始的时间与元素被读或写的时间，判断某个操作是否可以进行；by validation，即在验证阶段比较与之前事务的读写集合判断是否可以提交，验证成功后再写入数据库。&lt;/p>
&lt;p>上锁的方法避免了重新执行，但事务会受到延迟，在冲突严重时较优；乐观方法由于可以进行一定操作，只在出现问题时取消或重新执行，因此延迟较低，但在冲突严重时会造成大量的重新执行而影响性能。&lt;/p></content></item><item><title>软件工程：The Missing Semester</title><link>https://yeshe.ng/blog/posts/2022/03/se-missing-semester/</link><pubDate>Tue, 22 Mar 2022 23:01:03 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/03/se-missing-semester/</guid><description>软件工程项目缺失的亿点文档</description><content>&lt;p>要是让我总结一个“在清华体验不太好的课程列表”，软件工程一定位列其中。这并不意味着我不喜欢软工项目中的环境配置、CI/CD、单元测试、部署等等工作；相反，我在其中找到了很多乐趣——毕竟，我是一个爱折腾的人。&lt;/p>
&lt;p>问题出在作用接近于 0 的文档上：对于很多依赖于详细的文档才能上手的工作，这门课几乎没有提供任何有用的文档。对于这些工作，课程组只提供了没有任何注释的“样例”让我们照猫画虎。虽然说面向 Google 是计算机专业最好的学习方式，但是将一堆东西扔给学生自学，还几乎不提供任何的引导，这是这门课最大的问题。虽然其中很多知识我以前都接触过，但是每当我站在没有开发经验的同学的角度上思考这些复杂的技术栈对于同学们来说意味着什么，就不禁感到愤怒。&lt;/p>
&lt;p>在摸索了几天、踩了无数坑之后，我总算是把我们项目的代码部署到了软工提供的服务器上。在这里，我将把一些在软工平台上部署的经验写成文档，作为软工课程文档的补充。&lt;/p>
&lt;p>By the way，“The Missing Semester”在这里有两个含义：一是致敬 CMU 的“The Missing Semester”课程，一是表达我对于软工试图仅用一学期就让我们成为全栈工程师的不满。在我看来，软工至少需要两个学期；在这两个学期中，教学团队的引导和同学们的开发应当是相辅相成的，而不能只是学生自己独自面对一个完整的、陌生的、无从下手的、令人恐惧的项目，即使可能有很多有开发经验的同学并不这么觉得。&lt;/p>
&lt;p>感谢 &lt;a href="https://github.com/prnake">@prnake&lt;/a> 学长在 GitHub 上公开他们的项目 &lt;a href="https://github.com/prnake/Quick-Red">Quick-Red&lt;/a>，下面的很多例子源于此项目。没有它，很多问题我根本无从下手。&lt;/p>
&lt;h2 id="创建项目">创建项目&lt;/h2>
&lt;h4 id="在软工平台上新建仓库">在软工平台上新建仓库&lt;/h4>
&lt;p>如果需要部署仓库内容到服务器上，必须要在软工平台（SECoder）上的 &lt;code>项目管理 - 仓库管理&lt;/code> 中新建，否则之后无法使用 &lt;code>deployer&lt;/code> 部署到提供的服务器上。&lt;/p>
&lt;h4 id="分支管理">分支管理&lt;/h4>
&lt;p>仓库中应该有如下分支：&lt;/p>
&lt;ul>
&lt;li>&lt;code>master&lt;/code>（或 &lt;code>main&lt;/code>）：主分支，用于部署生产环境&lt;/li>
&lt;li>&lt;code>dev&lt;/code>：开发分支，所有最新的改动合并到此分支，定期合并到 &lt;code>master&lt;/code> 分支&lt;/li>
&lt;li>其他开发分支（例如 &lt;code>feat/login&lt;/code>）：开发某一新功能时新建的分支，开发完成后合并到 &lt;code>dev&lt;/code> 分支&lt;/li>
&lt;/ul>
&lt;p>需要注意 &lt;code>master&lt;/code> 和 &lt;code>dev&lt;/code> 只能通过 merge request 由其他分支合并，不能直接 push。另外，新建开发分支时一定要从 &lt;code>dev&lt;/code> 分支上新建，不能从其他开发分支上新建。&lt;/p>
&lt;h4 id="仓库配置">仓库配置&lt;/h4>
&lt;p>GitLab 中有几项设置对后续协作开发有不小的帮助，值得花点时间配置一下：&lt;/p>
&lt;ol>
&lt;li>在 &lt;code>General - Merge request&lt;/code> 中取消选择 &lt;code>Merge options&lt;/code> 下的 &lt;code>Enable 'Delete source branch' option by default&lt;/code>。课程需要保留开发过程中的分支，需要避免不小心删除分支的情况。&lt;/li>
&lt;li>在 &lt;code>Repository - Default Branch&lt;/code> 中将 &lt;code>Default Branch&lt;/code> 设置为 &lt;code>dev&lt;/code>，使得进行 merge request 时默认合并到 &lt;code>dev&lt;/code> 分支。&lt;/li>
&lt;li>在 &lt;code>Repository - Protected Branches&lt;/code> 中添加 &lt;code>master&lt;/code> 和 &lt;code>dev&lt;/code> 都设置为受保护的分支，并禁止任何人直接 push 到这两个分支上。&lt;/li>
&lt;/ol>
&lt;h4 id="增加-issue-template-和-merge-request-template">增加 issue template 和 merge request template&lt;/h4>
&lt;p>在仓库目录下新建文件夹 &lt;code>.gitlab/issue_templates/&lt;/code>，并在其中添加若干 Markdown 文件，这些文件将可用作新建 issue 时作为模版供选择。例如，针对 bug 的 issue：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&amp;lt;!-- Title 格式：[bug] Bug 描述 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## 复现过程
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## 当前的行为
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#75715e">&amp;lt;!-- 当前版本存在的问题行为 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">## 预期的行为
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#75715e">&amp;lt;!-- 正常应当有怎样的行为 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>添加模版之后，在新建 issue 时可以选择其中之一作为模版，保证 issue 的规范和质量。&lt;/p>
&lt;p>同理，可以为 merge request 添加模版，可以参考 &lt;a href="https://github.com/prnake/Quick-Red/blob/master/frontend/.gitlab/merge_request_templates/CodeReview.md">Quick-Red 的模版&lt;/a>。&lt;/p>
&lt;h2 id="cicd">CI/CD&lt;/h2>
&lt;p>CI/CD 简单来说就是在服务器上执行一系列命令，从而自动化地在每次提交代码之后完成测试、检查、构建等工作，并在需要时完成部署。&lt;/p>
&lt;h4 id="stage-和-job">Stage 和 job&lt;/h4>
&lt;p>Stage 指的是 CI/CD 的不同阶段，用来完成不同阶段的工作，一般情况下&lt;strong>顺序执行&lt;/strong>。你可以使用 GitLab 的 DAG 功能使得没有依赖关系的 stage 并行运行以加快速度，具体可以参考 &lt;a href="https://docs.gitlab.com/ee/ci/directed_acyclic_graph/">官方文档&lt;/a>。&lt;/p>
&lt;p>在软工项目中的 stage 通常包括：&lt;/p>
&lt;ul>
&lt;li>pre：准备阶段，完成依赖库缓存的拉取和保存等&lt;/li>
&lt;li>build：构建 Docker 镜像&lt;/li>
&lt;li>test：单元测试、代码风格检查、上传代码质量报告（sonar）&lt;/li>
&lt;li>deploy：将镜像部署到服务器上&lt;/li>
&lt;/ul>
&lt;p>在 &lt;code>.gitlab-ci.yml&lt;/code> 开头声明 stage：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">stages&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">pre&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">build&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">test&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">deploy&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>需要注意的是，不同 stage 之间的环境是相互隔离的。比如，在前面的 stage 中如果安装了项目依赖（如 &lt;code>npm install&lt;/code>），在后面的 stage 中仍然需要安装这些依赖。在后面将会介绍加快这一过程的方法。&lt;/p>
&lt;p>Job 指的是 stage 内部的若干个任务单元，执行具体的命令，同一个 stage 的所有 jobs 是&lt;strong>并行执行&lt;/strong>的，环境也是相互隔离的。&lt;/p>
&lt;p>Job 一些常用的属性有：&lt;/p>
&lt;ul>
&lt;li>&lt;code>stage&lt;/code>：指定这个 job 所属的 stage；若不指定，默认为 &lt;code>test&lt;/code>&lt;/li>
&lt;li>&lt;code>script&lt;/code>：这个 job 执行的命令行命令&lt;/li>
&lt;li>&lt;code>before_script&lt;/code>：在执行 &lt;code>script&lt;/code> 之前执行的命令，一般用于环境配置&lt;/li>
&lt;li>&lt;code>after_script&lt;/code>：在执行 &lt;code>script&lt;/code> 之后执行的指令，比如使用 &lt;code>sonar-scanner&lt;/code> 上传在 &lt;code>script&lt;/code> 中得到的覆盖率报告&lt;/li>
&lt;li>&lt;code>only&lt;/code> 和 &lt;code>except&lt;/code>：用于指定运行此 job 的条件，比如生产环境的部署对应的 job 只在 &lt;code>master&lt;/code> 分支上进行&lt;/li>
&lt;/ul>
&lt;p>具体的用法可以参考 GitLab 文档。&lt;/p>
&lt;h4 id="hidden-jobs-与继承">Hidden jobs 与继承&lt;/h4>
&lt;p>如果多个 job 有大量相同的配置，可以使用继承的方法避免重复编写相同配置。比如，我们对 Node.js 项目分别进行单元测试和代码风格检查，而它们都需要安装相同的依赖、都属于 &lt;code>test&lt;/code> stage。&lt;/p>
&lt;p>首先，定义一个以 &lt;code>.&lt;/code> 开头的 job，这个 job 为 hidden job，不会被执行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">.test&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">stage&lt;/span>: &lt;span style="color:#ae81ff">test&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">before_script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm config set registry http://mirrors.cloud.tencent.com/npm/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm install&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>第一条命令用于将 npm 使用的源设置为国内腾讯云镜像，这将大大加快加载速度。&lt;/p>
&lt;/blockquote>
&lt;p>然后，在真正的 job 中，使用 &lt;code>extends&lt;/code> 继承此 job：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">lint-test&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">extends&lt;/span>: &lt;span style="color:#ae81ff">.test&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm run lint&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">unit-test&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">extends&lt;/span>: &lt;span style="color:#ae81ff">.test&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm run test:ci&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这两个 job 因此拥有了与 &lt;code>.test&lt;/code> 相同的 &lt;code>stage&lt;/code> 和 &lt;code>before_script&lt;/code> 属性。&lt;/p>
&lt;p>你还可以继承多个 hidden job，具体行为可以查阅有关文档。&lt;/p>
&lt;h4 id="加快速度cache">加快速度：cache&lt;/h4>
&lt;p>对于使用如 Node.js 这样的需要安装大量依赖库的项目，如果在每个 stage 中、每次 CI/CD 都重新安装一次依赖，CI/CD 的效率将会非常低。对这个问题，我们可以使用 GitLab 的 cache 功能将这些依赖缓存起来供重复利用。&lt;/p>
&lt;p>因为每个 job 的环境都是独立的，因此每个 job 都需要定义缓存。我们使用 hidden job 完成统一配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">.cache&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cache&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">key&lt;/span>: &lt;span style="color:#ae81ff">${CI_COMMIT_REF_SLUG}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">paths&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">node_modules/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">package-lock.json&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">policy&lt;/span>: &lt;span style="color:#ae81ff">pull&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中，&lt;code>key&lt;/code> 使用分支名（&lt;code>${CI_COMMIT_REF_SLUG}&lt;/code>），即不同的分支使用不同的缓存，而同一分支下的所有 job 共享这份缓存。&lt;code>paths&lt;/code> 指定了缓存项的路径，这里我们将 Node.js 的依赖文件缓存起来。&lt;code>policy&lt;/code> 设置为 &lt;code>pull&lt;/code> 的目的是，每次只从服务器上拉取缓存，而不需要在结束之后将此缓存上传到服务器上。这很重要，因为我们不希望每个 job 都重复地上传相同的缓存。&lt;/p>
&lt;p>我们在 &lt;code>pre&lt;/code> 阶段中添加一个 job 用来更新缓存：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">node-cache&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">stage&lt;/span>: &lt;span style="color:#ae81ff">pre&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">extends&lt;/span>: &lt;span style="color:#ae81ff">.cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cache&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">policy&lt;/span>: &lt;span style="color:#ae81ff">pull-push&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm config set registry http://mirrors.cloud.tencent.com/npm/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm install&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意到，我们将 cache policy 更改为了 &lt;code>pull-push&lt;/code>，这是因为我们需要将此缓存上传到服务器上供其他 job 和 pipeline 使用。&lt;/p>
&lt;p>然后，在需要用到此缓存的 job 中，继承 &lt;code>.cache&lt;/code> 即可：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">stage&lt;/span>: &lt;span style="color:#ae81ff">build&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">extends&lt;/span>: &lt;span style="color:#ae81ff">.cache&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm config set registry http://mirrors.cloud.tencent.com/npm/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">npm install&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">...&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个 job 将会从服务器上拉取最新的缓存，从而无需重新下载。&lt;/p>
&lt;p>当然，以防万一，还是加上一句 &lt;code>npm install&lt;/code>，虽然正常情况下这条命令并不会从网络上下载依赖，因为在 &lt;code>pre&lt;/code> 阶段我们已经下载好了需要的依赖。&lt;/p>
&lt;h2 id="sonar-与单元测试的配置">Sonar 与单元测试的配置&lt;/h2>
&lt;p>课程要求使用 Sonar 对源代码进行静态分析。既然是静态分析，那么 Sonar 需要测试工具执行测试生成测试报告，才能进行分析。因此，测试与报告的流程为：&lt;/p>
&lt;ol>
&lt;li>测试工具执行测试，并输出测试结果&lt;/li>
&lt;li>使用 &lt;code>sonar-scanner&lt;/code> 对测试结果和源代码进行分析并上传分析结果&lt;/li>
&lt;/ol>
&lt;h4 id="使用测试工具生成测试报告">使用测试工具生成测试报告&lt;/h4>
&lt;p>Sonar 需要提供 xml 格式的测试报告，对于不同语言、框架的方法不同，可以到搜索引擎上搜索生成报告的方法。在这里分享一下我们项目使用的框架（Django、React（create-react-app）、Go）适用的方法。&lt;/p>
&lt;p>&lt;strong>Django&lt;/strong>&lt;/p>
&lt;p>如果直接使用 &lt;code>pytest&lt;/code>，可以参考 &lt;a href="https://github.com/prnake/Quick-Red/tree/master/backend">Quick-Red&lt;/a>。&lt;/p>
&lt;p>如果使用 Django 自带的测试框架 &lt;code>django.test&lt;/code>：&lt;/p>
&lt;p>需要安装以下依赖：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>pytest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>coverage
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>unittest-xml-reporting
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 &lt;code>settings.py&lt;/code> 中，添加如下配置：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 指定 test runner 为 xmlrunner&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TEST_RUNNER &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;xmlrunner.extra.djangotestrunner.XMLTestRunner&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 指定报告输出目录&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TEST_OUTPUT_DIR &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;xunit-reports/&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 指定报告文件名&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TEST_OUTPUT_FILE_NAME &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#39;xunit-result.xml&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，运行&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>python -m coverage run --source path1,path2,...,pathn manage.py test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>python -m coverage xml -o coverage-reports/coverage.xml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中，&lt;code>path1&lt;/code>、&lt;code>path2&lt;/code> 等替换为包括在覆盖率报告中的文件/文件夹路径。&lt;/p>
&lt;p>这样，分别生成了测试报告 &lt;code>xunit-reports/xunit-result.xml&lt;/code> 和覆盖报告 &lt;code>coverage-reports/coverage.xml&lt;/code>。&lt;/p>
&lt;p>这些命令仅完成了最基本的测试报告的配置。关于可选的命令行参数，请参考 &lt;a href="https://coverage.readthedocs.io/en/6.3.2/cmd.html">官方文档&lt;/a>。&lt;/p>
&lt;p>&lt;strong>React（create-react-app）&lt;/strong>&lt;/p>
&lt;p>create-react-app 预置了 Jest，无需手动安装。&lt;/p>
&lt;p>安装 &lt;code>jest-sonar&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npm install --save-dev jest-sonar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 &lt;code>package.json&lt;/code> 的 &lt;code>scripts&lt;/code> 中添加：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74">&amp;#34;test:ci&amp;#34;&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">:&lt;/span> &lt;span style="color:#e6db74">&amp;#34;CI=true react-scripts test --coverage --reporters=jest-sonar --collectCoverageFrom=src/**/*.js&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，执行：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>npm run test:ci
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，在 &lt;code>coverage&lt;/code> 目录下会生成测试报告。&lt;/p>
&lt;p>这条命令仅完成了最基本的测试报告的配置。关于可选的命令行参数，请参考 &lt;a href="https://jestjs.io/docs/cli#options">官方文档&lt;/a>。&lt;/p>
&lt;h4 id="golang">Golang&lt;/h4>
&lt;p>执行&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>go test -cover -race -v -coverprofile&lt;span style="color:#f92672">=&lt;/span>coverage.out ./...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="配置-sonar-projectproperties-文件">配置 sonar-project.properties 文件&lt;/h4>
&lt;p>我们通过项目根目录下的 &lt;code>sonar-project.properties&lt;/code> 文件对 Sonar 进行配置。一些配置如下：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf"># 源代码所在目录
sonar.sources=src
# 包含测试文件的路径，sonar 将会寻找其中的所有测试文件
sonar.tests=src
# 包含在测试分析内的测试文件
sonar.test.inclusions=src/**/__tests__/*.test.js
# 排除在所有分析之外的路径
sonar.exclusions=node_modules/**,coverage/**
# 覆盖率分析时排除在外的路径
sonar.coverage.exclusions=src/utils/**/*.js
&lt;/code>&lt;/pre>&lt;p>另外，还需要提供已经得到的测试报告的路径。对于 JavaScript：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.testExecutionReportPaths=coverage/sonar-report.xml
&lt;/code>&lt;/pre>&lt;p>对于使用 &lt;code>coverage&lt;/code> 进行分析的 Python 代码：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">sonar.python.coverage.reportPaths=coverage-reports/coverage.xml
sonar.python.xunit.reportPath=xunit-reports/xunit-result.xml
&lt;/code>&lt;/pre>&lt;p>对于 Go：&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">sonar.go.coverage.reportPaths=coverage.out
&lt;/code>&lt;/pre>&lt;p>这里路径需要改成上面一步所设置的覆盖报告和测试报告的路径。&lt;/p>
&lt;h4 id="设置-ci">设置 CI&lt;/h4>
&lt;p>在 CI 中，定义一个 job 用来执行测试和上传报告：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">unit-test&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># 执行测试的命令&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">after_script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">SUFFIX=$RANDOM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">curl &amp;#34;http://10.0.0.11/sonar-scanner.tar.gz&amp;#34; -s -o &amp;#34;/tmp/sonar-$SUFFIX.tar.gz&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">tar -xf &amp;#34;/tmp/sonar-$SUFFIX.tar.gz&amp;#34; -C /opt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">/opt/sonar-scanner/bin/sonar-scanner&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="dockerfile">Dockerfile&lt;/h2>
&lt;p>将代码部署在服务器上需要配置环境、编译应用、安装应用、开启服务等一系列操作，而 Docker 使得我们可以预先完成或指定这些操作，从而能够实现自动部署；Dockerfile 就是我们定义这些操作的地方。&lt;/p>
&lt;p>在软工平台上使用 &lt;code>deployer&lt;/code> 构建、上传 Docker 镜像，并使用此镜像进行部署。大概的逻辑是：&lt;/p>
&lt;ol>
&lt;li>通过 Dockerfile 内的一系列语句预先配置好所需的环境并打包成镜像&lt;/li>
&lt;li>将镜像上传到软工平台的镜像站上&lt;/li>
&lt;li>部署时，拉取之前构建好的镜像，并在一个容器中运行，这样就完成了部署&lt;/li>
&lt;/ol>
&lt;p>这个过程在 &lt;code>.gitlab-ci.yml&lt;/code> 中使用多个 job 完成：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 构建 Docker 镜像&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">stage&lt;/span>: &lt;span style="color:#ae81ff">build&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">export BUILD_IMAGE_NAME=$CI_REGISTRY_IMAGE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">export BUILD_IMAGE_TAG=$CI_COMMIT_REF_SLUG&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">export BUILD_IMAGE_USERNAME=$CI_REGISTRY_USER&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">export BUILD_IMAGE_PASSWORD=$CI_REGISTRY_PASSWORD&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">deployer build&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 使用镜像部署&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">deploy&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">stage&lt;/span>: &lt;span style="color:#ae81ff">deploy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">deployer dyno replace $CI_PROJECT_NAME &amp;#34;$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG&amp;#34; &amp;#34;$REGISTRY_USER&amp;#34; &amp;#34;$REGISTRY_PWD&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>build&lt;/code> 的多个 &lt;code>export&lt;/code> 指定了上传时使用的镜像名、tag、使用的用户名和密码；&lt;code>deployer build&lt;/code> 命令完成了 (1) 使用 &lt;code>docker build&lt;/code> 根据 Dockerfile 进行镜像构建，(2) 将镜像上传到平台上；&lt;code>deploy&lt;/code> 工作将 &lt;code>build&lt;/code> 中上传的镜像部署到服务器上。这样就完成了整个部署过程。&lt;/p>
&lt;p>所以，我们需要做的，就是在 Dockerfile 中描述配置容器的过程。&lt;/p>
&lt;h4 id="配置-dockerfile">配置 Dockerfile&lt;/h4>
&lt;p>首先，指定一个“基础镜像”，即使用一个预先配置好的镜像，在这个镜像的基础上配置我们的镜像。比如，对于 Node.js 应用，我们使用官方的 &lt;code>node&lt;/code> 镜像：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span>&lt;span style="color:#e6db74"> node:latest&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在这之后，我们就在一个配置好了 Node.js 的环境中了。&lt;/p>
&lt;p>接下来，无非就是执行一系列语句来编译我们的应用，并将其拷贝到合适的位置。这部分内容可以参考软工课程给出的样例文件。&lt;/p>
&lt;p>比如，编译 React 应用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 第一个阶段：使用 node 镜像编译 React 应用；将此阶段命名为 build&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">FROM&lt;/span>&lt;span style="color:#e6db74"> node:latest as build&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 设置环境变量 HOME&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">ENV&lt;/span> HOME&lt;span style="color:#f92672">=&lt;/span>/opt/app&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 将工作目录设为 HOME，并拷贝当前目录（项目代码）到 HOME 中&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">WORKDIR&lt;/span>&lt;span style="color:#e6db74"> $HOME&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">COPY&lt;/span> . $HOME&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 安装依赖&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> npm config set registry http://mirrors.cloud.tencent.com/npm/&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> npm install&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 执行 React 的 build，获得网页文件&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">RUN&lt;/span> npm run build&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 第二个阶段：配置使用 nginx 容器，使得可以通过 HTTP 访问&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">FROM&lt;/span>&lt;span style="color:#e6db74"> nginx:1.19&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 从第一个阶段（build）拷贝文件&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 拷贝编译后的网页文件到 nginx 的目录下&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">COPY&lt;/span> --from&lt;span style="color:#f92672">=&lt;/span>build /opt/app/build /usr/share/nginx/html&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 拷贝 nginx 配置文件（储存在源代码中）&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 对于 nginx 的配置和使用，你可能需要查阅相关文档，也可以直接使用已有的样例&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">COPY&lt;/span> --from&lt;span style="color:#f92672">=&lt;/span>build /opt/app/nginx.conf /etc/nginx/nginx.conf&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 暴露容器的 80 端口&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">EXPOSE&lt;/span>&lt;span style="color:#e6db74"> 80&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#75715e"># 这里定义了容器启动时执行的命令（开启 Nginx 服务），注意并不是在构建镜像时执行&lt;/span>&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#960050;background-color:#1e0010">&lt;/span>&lt;span style="color:#66d9ef">CMD&lt;/span> [&lt;span style="color:#e6db74">&amp;#34;nginx&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;-g&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;daemon off;&amp;#34;&lt;/span>]&lt;span style="color:#960050;background-color:#1e0010">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>有关 Dockerfile 的语句，可以参考 &lt;a href="https://yeasy.gitbook.io/docker_practice/image/build">Docker - 从入门到实践&lt;/a>（中文）或 &lt;a href="https://docs.docker.com/engine/reference/builder/">Dockerfile 官方文档&lt;/a>（英文）。&lt;/p>
&lt;h4 id="在-ci-中向-dockerfile-传递环境变量">在 CI 中向 Dockerfile 传递环境变量&lt;/h4>
&lt;p>有时候，我们需要向 Dockerfile 中传递环境变量，比如使用不同的环境部署生产环境和开发环境的代码。但是，&lt;code>deployer&lt;/code> 并不会向 &lt;code>docker build&lt;/code> 传递环境变量，因此在 Dockerfile 中是无法获取到我们希望传递的环境变量的。&lt;/p>
&lt;p>一种不太优雅的解决办法是：使用不同的 Dockerfile，直接在 Dockerfile 中用 &lt;code>ENV&lt;/code> 语句设置环境变量。&lt;/p>
&lt;p>比如，当前项目根目录下有两个文件：用于生产环境的 &lt;code>Dockerfile&lt;/code>，以及用于开发环境的 &lt;code>Dockerfile-dev&lt;/code>，它们的唯一区别是，&lt;code>Dockerfile-dev&lt;/code> 设置了一个名为 &lt;code>REACT_APP_IS_DEV_SERVER&lt;/code> 的环境变量，在我们的 React 应用中需要根据此环境变量判断所处的是否为生产环境：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ENV&lt;/span> REACT_APP_IS_DEV_SERVER&lt;span style="color:#f92672">=&lt;/span>true
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是，因为 &lt;code>deployer&lt;/code> 无法指定构建镜像所使用的 Dockerfile 而只能使用文件名为 &lt;code>Dockerfile&lt;/code> 的文件，我们需要将文件更名为 &lt;code>Dockerfile&lt;/code>。在 CI 中，分别为生产环境和开发环境定义 build 的 job：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">build&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">extends&lt;/span>: &lt;span style="color:#ae81ff">.build&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">only&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">master&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">build-dev&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">extends&lt;/span>: &lt;span style="color:#ae81ff">.build&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">before_script&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">rm Dockerfile&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">mv Dockerfile-dev Dockerfile&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">except&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">master&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们用 &lt;code>Dockerfile-dev&lt;/code> 替代了 &lt;code>Dockerfile&lt;/code>，实现了根据条件传递环境变量的目的。&lt;/p>
&lt;h2 id="数据库配置">数据库配置&lt;/h2>
&lt;p>对于需要使用 Client/Server 通信模式数据库的情况，需要部署一个独立的数据库服务器。下面以 MySQL 为例。&lt;/p>
&lt;p>对于其他数据库，过程大同小异，只需要完成：&lt;/p>
&lt;ul>
&lt;li>创建仓库并设置好 CI/CD&lt;/li>
&lt;li>将持久卷挂载到数据库储存文件的位置&lt;/li>
&lt;li>设置好数据库访问的用户名和密码、创建数据库、授予权限等初始化工作&lt;/li>
&lt;li>在后端上配置好访问数据库的信息&lt;/li>
&lt;/ul>
&lt;h4 id="建立仓库配置持久卷和端口">建立仓库、配置持久卷和端口&lt;/h4>
&lt;p>在软工平台上创建数据库所用的仓库，并在软工平台 &lt;code>项目管理 - 部署管理 - 持久储存&lt;/code> 中新建一个持久卷。&lt;/p>
&lt;p>之后，在上方 &lt;code>容器&lt;/code> 中打开数据库容器，在 &lt;code>挂载&lt;/code> 一栏中选择此持久卷，挂载点设置为 &lt;code>/var/lib/mysql&lt;/code>。这是因为 MySQL 所有的数据库文件均储存在此目录下，将持久卷挂载到此目录上可以保持数据库文件不因重新部署而丢失。&lt;/p>
&lt;p>在容器的 &lt;code>端口&lt;/code> 中添加 MySQL 使用的 &lt;code>3306&lt;/code> 端口。&lt;/p>
&lt;h4 id="配置-cicd">配置 CI/CD&lt;/h4>
&lt;p>&lt;code>.gitlab-ci.yml&lt;/code> 和 Dockerfile 直接使用 &lt;a href="https://github.com/prnake/Quick-Red/tree/master/mysql">Quick-Red&lt;/a> 的配置即可。&lt;/p>
&lt;h4 id="初始化数据库">初始化数据库&lt;/h4>
&lt;p>初次部署完成后，需要进入容器初始化 MySQL。在软工平台数据库容器下打开 &lt;code>管理 - 终端&lt;/code>，进入容器。&lt;/p>
&lt;p>然后，执行 &lt;code>mysql&lt;/code> 命令进入 MySQL。&lt;/p>
&lt;p>首先，设置数据库 &lt;code>root&lt;/code> 用户的密码（&lt;code>&amp;lt;password_root&amp;gt;&lt;/code> 设置为你想要设置的密码）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ALTER&lt;/span> &lt;span style="color:#66d9ef">USER&lt;/span> &lt;span style="color:#e6db74">&amp;#39;root&amp;#39;&lt;/span>&lt;span style="color:#f92672">@&lt;/span>&lt;span style="color:#e6db74">&amp;#39;localhost&amp;#39;&lt;/span> IDENTIFIED &lt;span style="color:#66d9ef">BY&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;lt;password_root&amp;gt;&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>接下来，创建供外部访问的账号（&lt;code>&amp;lt;password_remote&amp;gt;&lt;/code> 设置为外部访问时使用的密码）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">USER&lt;/span> &lt;span style="color:#e6db74">&amp;#39;django&amp;#39;&lt;/span>&lt;span style="color:#f92672">@&lt;/span>&lt;span style="color:#e6db74">&amp;#39;%&amp;#39;&lt;/span> IDENTIFIED &lt;span style="color:#66d9ef">BY&lt;/span> &lt;span style="color:#e6db74">&amp;#39;&amp;lt;password_remote&amp;gt;&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在这里，&lt;code>'django'@'%'&lt;/code> 的意思是，创建一个从任意 IP 访问（&lt;code>%&lt;/code>）、用户名为 &lt;code>django&lt;/code> 的账户。&lt;code>django&lt;/code> 可以设置为你想设置的用户名。注意用户名尽量不要使用 &lt;code>root&lt;/code>，会产生冲突。
然后，创建一个空的数据库（&lt;code>db_name&lt;/code> 更改为你想要设置的名称）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">DATABASE&lt;/span> db_name;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>将此数据库的访问权限授予远程用户（注意修改 &lt;code>db_name&lt;/code> 和 &lt;code>django&lt;/code>）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">GRANT&lt;/span> &lt;span style="color:#66d9ef">ALL&lt;/span> &lt;span style="color:#66d9ef">PRIVILEGES&lt;/span> &lt;span style="color:#66d9ef">ON&lt;/span> db_name.&lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">TO&lt;/span> &lt;span style="color:#e6db74">&amp;#39;django&amp;#39;&lt;/span>&lt;span style="color:#f92672">@&lt;/span>&lt;span style="color:#e6db74">&amp;#39;%&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最后，更新权限：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>FLUSH &lt;span style="color:#66d9ef">PRIVILEGES&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，我们就可以在后端服务器上访问此数据库了。&lt;/p>
&lt;h4 id="配置-django">配置 Django&lt;/h4>
&lt;p>这里以 Django 为例配置使用上面已经配置好的 MySQL 数据库。&lt;/p>
&lt;p>在软工平台中，创建一个“配置”，并上传一个包含数据库访问信息的 JSON 文件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 在 MySQL 中新建的数据库名
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;MYSQL_NAME&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;db_name&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 配置数据库时设置的远程访问用户名
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;MYSQL_USER&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;root&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 配置数据库时设置的远程访问密码
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;MYSQL_PASSWORD&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;&amp;lt;password_remote&amp;gt;&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 数据库服务器的本地地址，在软工平台上可以找到
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;MYSQL_HOST&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;example-app.secoder.local&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// MySQL 端口
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">&amp;#34;MYSQL_PORT&amp;#34;&lt;/span>: &lt;span style="color:#ae81ff">3306&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，在软工平台的后端容器中将此文件（配置）挂载到源代码所在的子目录下（这里你可能需要查看你的 Dockerfile 来获取容器中源代码所在的目录）。注意，这个操作会将此目录的所有内容替换为上面配置的内容，因此&lt;strong>不要将其挂载到源代码根目录下，也不要将其挂载到任何一个原有的子目录下&lt;/strong>。&lt;/p>
&lt;p>在 settings.py 中，读取配置文件（这里，我们把配置文件目录挂载到了 代码目录/config，文件名为 ‌config.json）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> os&lt;span style="color:#f92672">.&lt;/span>path&lt;span style="color:#f92672">.&lt;/span>isfile(&lt;span style="color:#e6db74">&amp;#34;config/config.json&amp;#34;&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">with&lt;/span> open(&lt;span style="color:#e6db74">&amp;#34;config/config.json&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;r&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">as&lt;/span> file:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> env &lt;span style="color:#f92672">=&lt;/span> json&lt;span style="color:#f92672">.&lt;/span>load(file)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，将 &lt;code>DATABASES&lt;/code> 更改为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>DATABASES &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;default&amp;#39;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;ENGINE&amp;#39;&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;django.db.backends.mysql&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;NAME&amp;#39;&lt;/span>: env[&lt;span style="color:#e6db74">&amp;#34;MYSQL_NAME&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;USER&amp;#39;&lt;/span>: env[&lt;span style="color:#e6db74">&amp;#34;MYSQL_USER&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;PASSWORD&amp;#39;&lt;/span>: env[&lt;span style="color:#e6db74">&amp;#34;MYSQL_PASSWORD&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;HOST&amp;#39;&lt;/span>: env[&lt;span style="color:#e6db74">&amp;#34;MYSQL_HOST&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;PORT&amp;#39;&lt;/span>: env[&lt;span style="color:#e6db74">&amp;#34;MYSQL_PORT&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;OPTIONS&amp;#39;&lt;/span>: {&lt;span style="color:#e6db74">&amp;#39;charset&amp;#39;&lt;/span>: &lt;span style="color:#e6db74">&amp;#39;utf8mb4&amp;#39;&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>至此，Django 就可以使用 MySQL 服务器了。注意，你应该避免在开发环境中使用生产环境的 MySQL 服务器，这个问题可以通过挂载不同的配置文件解决。&lt;/p></content></item><item><title>Operating Systems: Three Easy Pieces 学习笔记（四）</title><link>https://yeshe.ng/blog/posts/2022/03/ostep-virtualization-2/</link><pubDate>Sun, 06 Mar 2022 17:24:23 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/03/ostep-virtualization-2/</guid><description>Virtual Memory</description><content>&lt;h2 id="13-the-abstraction-address-spaces">13 The Abstraction: Address Spaces&lt;/h2>
&lt;h3 id="133-the-address-space">13.3 The Address Space&lt;/h3>
&lt;p>The address space of a process contains all of the memory state of the running program (e.g. code, stack, heap&amp;hellip;)&lt;/p>
&lt;h3 id="134-goals">13.4 Goals&lt;/h3>
&lt;p>Goals of the design:&lt;/p>
&lt;ul>
&lt;li>Transparency&lt;/li>
&lt;li>Efficiency&lt;/li>
&lt;li>Protection&lt;/li>
&lt;/ul>
&lt;h2 id="14-interlude-memory-api">14 Interlude: Memory API&lt;/h2>
&lt;h2 id="15-mechanism-address-translation">15 Mechanism: Address Translation&lt;/h2>
&lt;h3 id="153-dynamic-hardware-based-relocation">15.3 Dynamic (Hardware-based) Relocation&lt;/h3>
&lt;p>When a program is loaded, the OS sets &lt;code>base&lt;/code> and &lt;code>bounds&lt;/code> registers.&lt;/p>
&lt;pre tabindex="0">&lt;code>physical address = virtual address + base
&lt;/code>&lt;/pre>&lt;h3 id="154-hardware-support-a-summary">15.4 Hardware Support: A Summary&lt;/h3>
&lt;p>The required hardware support:&lt;/p>
&lt;ul>
&lt;li>privileged mode&lt;/li>
&lt;li>base/bounds registers&lt;/li>
&lt;li>translate virtual address and check if within bounds&lt;/li>
&lt;li>privileged instructions to update base/bounds registers&lt;/li>
&lt;li>privileged instructions to register exception handlers&lt;/li>
&lt;li>ability to raise exceptions&lt;/li>
&lt;/ul>
&lt;h3 id="156-summary">15.6 Summary&lt;/h3>
&lt;p>The problem: all the space between stack and heap is wasted (called &lt;strong>internal fragmentation&lt;/strong>)&lt;/p>
&lt;h2 id="16-segmentation">16 Segmentation&lt;/h2>
&lt;h3 id="161-segmentation-generalized-basebounds">16.1 Segmentation: Generalized Base/Bounds&lt;/h3>
&lt;p>&lt;strong>Segment&lt;/strong>: contiguous portion of the address space of a particular length.&lt;/p>
&lt;p>Segmentation allows the OS to place segments in differnet parts of physical memory.&lt;/p>
&lt;p>The hardware structure in the MMU: three &lt;code>base&lt;/code> and &lt;code>bounds&lt;/code> register pairs.&lt;/p>
&lt;h3 id="162-which-segment-are-we-referring-to">16.2 Which Segment Are We Referring To?&lt;/h3>
&lt;p>The explicit approach: chop up the address space into segments based on the top few bits of the virtual address.&lt;/p>
&lt;p>The implicit approcah: the hardware determins the segment by noticing how the address was formed.&lt;/p>
&lt;h3 id="163-what-about-the-stack">16.3 What About The Stack?&lt;/h3>
&lt;p>The difference: &lt;em>it grows backwords&lt;/em>.&lt;/p>
&lt;p>Use a bit in the hardware to indicate the sign of the offset.&lt;/p>
&lt;h3 id="164-support-for-sharing">16.4 Support for Sharing&lt;/h3>
&lt;h3 id="165-fine-grained-vs-coarse-grained-segmentation">16.5 Fine-grained vs. Coarse-grained Segmentation&lt;/h3>
&lt;h3 id="166-os-support">16.6 OS Support&lt;/h3>
&lt;p>The operation to support segmentation:&lt;/p>
&lt;ul>
&lt;li>On context switch: save and store segment registers&lt;/li>
&lt;li>Support growing heap segment (&lt;code>sbrk()&lt;/code>)&lt;/li>
&lt;li>Handle &lt;strong>external fragmentation&lt;/strong> (physical memory quickly becomes full of little holes of free space, making it difficult to allocate new segments, or to grow existing ones)&lt;/li>
&lt;/ul>
&lt;h2 id="17-free-space-management">17 Free-Space Management&lt;/h2>
&lt;h3 id="171-assumptions">17.1 Assumptions&lt;/h3>
&lt;p>&lt;strong>Internal fragmentation&lt;/strong>: the allocator hands out chunks of memory bigger than that requested&lt;/p>
&lt;h3 id="172-low-level-mechanisms">17.2 Low-level Mechanisms&lt;/h3>
&lt;h4 id="splitting-and-coalescing">Splitting and Coalescing&lt;/h4>
&lt;p>&lt;strong>Coalescing&lt;/strong>: coalesce spliited fragments when they are contiguous&lt;/p>
&lt;h4 id="tracking-the-size-of-allocated-regions">Tracking The Size Of Allocated Regions&lt;/h4>
&lt;p>Most allocators store a little bit of extra information in a header block which is kept in memory to keep track of the size of the allocated region.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">typedef&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> size;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> magic; &lt;span style="color:#75715e">/* for integrity checking */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>} header_t;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="embedding-a-free-list">Embedding A Free List&lt;/h4>
&lt;h4 id="growing-the-heap">Growing The Heap&lt;/h4>
&lt;h3 id="173-basic-strategies">17.3 Basic Strategies&lt;/h3>
&lt;blockquote>
&lt;p>Unfortunately, because the stream of allocation and free requests can be arbitrary (after all, they are determined by the programmer), any particular strategy can do quite badly given the wrong set of inputs.&lt;/p>
&lt;/blockquote>
&lt;h4 id="best-fit">Best Fit&lt;/h4>
&lt;p>The &lt;strong>best chunk&lt;/strong>: the smallest in candidates.&lt;/p>
&lt;p>Cost: heavy performance overhead&lt;/p>
&lt;h4 id="worst-fit">Worst Fit&lt;/h4>
&lt;p>The opposite to the best fit strategy.&lt;/p>
&lt;h4 id="first-fit">First Fit&lt;/h4>
&lt;p>Returns the first qualified block.&lt;/p>
&lt;p>Problem: pollution at the beginning of the free list; approach: address-based ordering.&lt;/p>
&lt;p>Advantage: speed.&lt;/p>
&lt;h4 id="next-fit">Next Fit&lt;/h4>
&lt;p>Keep an eatra pointer to the last-searched location; the blocks are distributed more evenly than first-fit strategy.&lt;/p>
&lt;h3 id="174-other-approaches">17.4 Other Approaches&lt;/h3>
&lt;h4 id="segregated-lists">Segregated Lists&lt;/h4>
&lt;p>Keep a separate list just to manage objects of some popular sizes; all other requests are forwarded to a more general memory allocator.&lt;/p>
&lt;h4 id="buddy-allocation">Buddy Allocation&lt;/h4>
&lt;p>The &lt;strong>binary buddy allocator&lt;/strong>: free memory is thought of as one big space of $2^N$; the search for free space recursively divides free space by two.&lt;/p>
&lt;p>Suffers from &lt;strong>internal fragmentation&lt;/strong>.&lt;/p>
&lt;p>Free process: recursively coalescing blocks upwards.&lt;/p>
&lt;p>Simple to determine the buddy of a particular block: each buddy pair (at the same level) only differs by a single bit.&lt;/p>
&lt;h2 id="18-paging-introduction">18 Paging: Introduction&lt;/h2>
&lt;h3 id="181-a-simple-example-and-overview">18.1 A Simple Example And Overview&lt;/h3>
&lt;p>&lt;strong>Page table&lt;/strong>: a &lt;em>per-process&lt;/em> data structure to store &lt;strong>address translations&lt;/strong>.&lt;/p>
&lt;h3 id="183-whats-actually-in-the-page-table">18.3 What’s Actually In The Page Table?&lt;/h3>
&lt;p>Some bits:&lt;/p>
&lt;ul>
&lt;li>valid bit &lt;em>(supporting a sparse address space; x86 doesn&amp;rsquo;t have it)&lt;/em>&lt;/li>
&lt;li>protection bit&lt;/li>
&lt;li>present bit (whether a page has been swapped out)&lt;/li>
&lt;li>dirty bit (whether a page has been modified)&lt;/li>
&lt;li>refence bit (whether a page has been accessed)&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Don’t worry: it deﬁnitely gets worse, because the mechanisms we are about to introduce only complicate this already complex machinery. Sorry!&lt;/p>
&lt;/blockquote>
&lt;h2 id="19-paging-faster-translations-tlbs">19 Paging: Faster Translations (TLBs)&lt;/h2>
&lt;blockquote>
&lt;p>When we want to make things fast, the OS usually needs some help. And help often comes from the OS’s old friend: the hardware.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Translation-lookaside buffer&lt;/strong> (or &lt;strong>TLB&lt;/strong>)&lt;/p>
&lt;h3 id="193-who-handles-the-tlb-miss">19.3 Who Handles The TLB Miss?&lt;/h3>
&lt;ul>
&lt;li>CISC: uses &lt;strong>hardware-managed TLBs&lt;/strong>&lt;/li>
&lt;li>RISC: uses &lt;strong>software-managed TLBs&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>The software approach: more flexible and simple; need to avoid infinite chain of TLB misses.&lt;/p>
&lt;h3 id="194-tlb-contents-whats-in-there">19.4 TLB Contents: What’s In There?&lt;/h3>
&lt;p>The TLB typically has 32, 64 or 128 entries, and is &lt;strong>fully associated&lt;/strong> (any given translation can be anywhere in the TLB).&lt;/p>
&lt;h3 id="195-tlb-issue-context-switches">19.5 TLB Issue: Context Switches&lt;/h3>
&lt;p>Some hardware systems provide an &lt;strong>address space identifier&lt;/strong> (&lt;strong>ASID&lt;/strong>) in the TLB to identify the associated process.&lt;/p>
&lt;h3 id="196-issue-replacement-policy">19.6 Issue: Replacement Policy&lt;/h3>
&lt;p>One common approach is to evict the &lt;strong>least-recently-used&lt;/strong> or &lt;strong>LRU&lt;/strong> entry.&lt;/p>
&lt;p>Another typical approach is to use a &lt;strong>random&lt;/strong> policy, which evicts a TLB mapping at random.&lt;/p>
&lt;h3 id="197-a-real-tlb-entry">19.7 A Real TLB Entry&lt;/h3>
&lt;p>&lt;a href="https://stackoverflow.com/questions/52813239/how-many-bits-there-are-in-a-tlb-asid-tag-for-intel-processors-and-how-to-handl">How to handle &amp;lsquo;ASID overflow&amp;rsquo;?&lt;/a>&lt;/p>
&lt;p>In MIPS, some TLB slots can be reserved for the OS.&lt;/p>
&lt;h2 id="20-paging-smaller-tables">20 Paging: Smaller Tables&lt;/h2>
&lt;blockquote>
&lt;p>Here, the problem should be clear: simple linear (array-based) page tables are too big.&lt;/p>
&lt;/blockquote>
&lt;h3 id="202-hybrid-approach-paging-and-segments">20.2 Hybrid Approach: Paging and Segments&lt;/h3>
&lt;blockquote>
&lt;p>Whenever you have two reasonable but different approaches to something in life, you should always examine the combination of the two to see if you can obtain the best of both worlds.&lt;/p>
&lt;/blockquote></content></item><item><title>Operating Systems: Three Easy Pieces 学习笔记（三）</title><link>https://yeshe.ng/blog/posts/2022/02/ostep-virtualization/</link><pubDate>Sat, 26 Feb 2022 13:26:41 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/02/ostep-virtualization/</guid><description>Process、Scheduling</description><content>&lt;h2 id="4-the-abstraction-the-process">4 The Abstraction: The Process&lt;/h2>
&lt;p>&lt;strong>Process&lt;/strong>: a running program&lt;/p>
&lt;h3 id="42-process-api">4.2 Process API&lt;/h3>
&lt;ul>
&lt;li>create&lt;/li>
&lt;li>destroy&lt;/li>
&lt;li>wait&lt;/li>
&lt;li>miscellaneous control&lt;/li>
&lt;li>status&lt;/li>
&lt;/ul>
&lt;h3 id="44-process-states">4.4 Process States&lt;/h3>
&lt;p>A process can be in one of three states:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>running&lt;/strong>: executing instructions&lt;/li>
&lt;li>&lt;strong>ready&lt;/strong>: ready to run but the OS has chosen not to run at this moment&lt;/li>
&lt;li>&lt;strong>blocked&lt;/strong>: a process has performed some kind of operation that makes it not ready to run until some other event takes place&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/02/ostep-process-state.webp" alt="">&lt;/p>
&lt;h2 id="5-interlude-process-api">5 Interlude: Process API&lt;/h2>
&lt;h3 id="54-why-motivating-the-api">5.4 Why? Motivating The API&lt;/h3>
&lt;p>The separation of &lt;code>fork()&lt;/code> and &lt;code>exec()&lt;/code> is essential in building a UNIX shell, because it lets the shell run code after the call to &lt;code>fork()&lt;/code> but before the call to &lt;code>exec()&lt;/code>; this code can alter the environment of the about-to-be-run program, and thus enables a variety of interesting features to be readily built.&lt;/p>
&lt;h3 id="55-process-control-and-users">5.5 Process Control And Users&lt;/h3>
&lt;p>The &lt;strong>user&lt;/strong>, after entering a password to establish credentials, logs in to gain access to system resources (and as a result, can send &lt;strong>signals&lt;/strong> to programs).&lt;/p>
&lt;h2 id="6-mechanism-limited-direct-execution">6 Mechanism: Limited Direct Execution&lt;/h2>
&lt;p>Use &lt;strong>time sharing&lt;/strong> to achieve virtualization&lt;/p>
&lt;p>Some challenges:&lt;/p>
&lt;ul>
&lt;li>performance&lt;/li>
&lt;li>control&lt;/li>
&lt;/ul>
&lt;h3 id="61-basic-technique-limited-direct-execution">6.1 Basic Technique: Limited Direct Execution&lt;/h3>
&lt;p>Direct execution: run the program directly on the CPU&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/02/ostep-direct-exec.webp" alt="">&lt;/p>
&lt;h3 id="62-problem-1-restricted-operations">6.2 Problem #1: Restricted Operations&lt;/h3>
&lt;p>The approach to restrict certain operations: introducing &lt;strong>user mode&lt;/strong> (and &lt;strong>kernel mode&lt;/strong>)&lt;/p>
&lt;p>&lt;strong>System calls&lt;/strong> allow the kernel to carefully expose certain key pieces of functionality to user programs, setting up a &lt;strong>trap table&lt;/strong> for the hardware to jump to &lt;strong>trap handlers&lt;/strong>&lt;/p>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/02/ostep-direct-exec-limited.webp" alt="">&lt;/p>
&lt;h3 id="63-problem-2-switching-between-processes">6.3 Problem #2: Switching Between Processes&lt;/h3>
&lt;blockquote>
&lt;p>The curx: how to regain control of the CPU?&lt;/p>
&lt;/blockquote>
&lt;h4 id="a-cooperative-approach-wait-for-system-calls">A Cooperative Approach: Wait For System Calls&lt;/h4>
&lt;p>The OS regains control of the CPU by waiting for a system call or an illegal operation of some kind to take place.&lt;/p>
&lt;h4 id="a-non-cooperative-approach-the-os-takes-control">A Non-Cooperative Approach: The OS Takes Control&lt;/h4>
&lt;p>A timer device can be programmed to raise an interrupt (&lt;strong>timer interrupt&lt;/strong>) every so many milliseconds; when the interrupt is raised, the currently running process is halted, and a pre-configured &lt;strong>interrupt handler&lt;/strong> in the OS runs.&lt;/p>
&lt;h4 id="saving-and-restoring-context">Saving and Restoring Context&lt;/h4>
&lt;p>Two types of register saves/restores:&lt;/p>
&lt;ul>
&lt;li>when timer interrupt occurs: the &lt;em>user registers&lt;/em> are implicitly saved by the &lt;em>hardware&lt;/em> to the kernel stack&lt;/li>
&lt;li>when the OS decides to switch to another process: the &lt;em>kernel registers&lt;/em> are explicitly saved by the &lt;em>software&lt;/em> to the memory of the process structure (Still in the kernel mode so should kernel registers be saved/restored; the user registers are restored later during return-from-trap)&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="https://yeshe.ng/blog/2022/02/ostep-timer-interrupt.webp" alt="">&lt;/p>
&lt;h3 id="64-worried-about-concurrency">6.4 Worried About Concurrency?&lt;/h3>
&lt;p>OS might disable interrupts during interrupt processing (but can lead to lost of interrupts), and use locking to protect concurrent access to internal data structures.&lt;/p>
&lt;h2 id="7-scheduling-introduction">7 Scheduling: Introduction&lt;/h2>
&lt;h3 id="71-workload-assumptions">7.1 Workload Assumptions&lt;/h3>
&lt;p>&lt;em>Unrealistic&lt;/em> assumptions:&lt;/p>
&lt;ol>
&lt;li>each job runs for the same amount of time&lt;/li>
&lt;li>all jobs arrive at the same time&lt;/li>
&lt;li>once started, each job runs to completion&lt;/li>
&lt;li>all jobs only use the CPU (i.e., they perform no I/O)&lt;/li>
&lt;li>the run-time of each job is known (more unrealistic!)&lt;/li>
&lt;/ol>
&lt;h3 id="72-scheduling-metrics">7.2 Scheduling Metrics&lt;/h3>
&lt;p>Simplify with one single metric: &lt;strong>turnaround time&lt;/strong>&lt;/p>
&lt;p>$$
T_\mathrm{turnaround}=T_\mathrm{completion}-T_\mathrm{arrival}
$$&lt;/p>
&lt;p>In our case:&lt;/p>
&lt;p>$$
T_\mathrm{turnaround}=T_\mathrm{completion}
$$&lt;/p>
&lt;h3 id="73-first-in-first-out-fifo">7.3 First In, First Out (FIFO)&lt;/h3>
&lt;p>The &lt;strong>First In, First Out&lt;/strong> scheduling, or &lt;strong>First Come, First Served&lt;/strong>&lt;/p>
&lt;p>&lt;strong>Convoy effect&lt;/strong>: a number of relatively-short potential consumers of a resource get queued behind a heavyweight resource consumer.&lt;/p>
&lt;h3 id="74-shortest-job-first-sjf">7.4 Shortest Job First (SJF)&lt;/h3>
&lt;p>&lt;strong>Shortest Job First&lt;/strong>: runs the shortest job first, then the next shortest, and so on; optimal.&lt;/p>
&lt;p>Bad without assumption 2 (all jobs arrive at the same time)&lt;/p>
&lt;h3 id="75-shortest-time-to-completion-first-stcf">7.5 Shortest Time-to-Completion First (STCF)&lt;/h3>
&lt;p>Relax assumption 3 (each job runs to completion)&lt;/p>
&lt;p>The scheduler can &lt;strong>preempt&lt;/strong> the current job; adding this to SJF, we get the &lt;strong>Shortest Time-to-Completion First&lt;/strong> (&lt;strong>STCF&lt;/strong>) or &lt;strong>Preemptive Shortest Job First&lt;/strong> (&lt;strong>PSJF&lt;/strong>) scheduler&lt;/p>
&lt;h3 id="76-a-new-metric-response-time">7.6 A New Metric: Response Time&lt;/h3>
&lt;p>The &lt;strong>response time&lt;/strong>:&lt;/p>
&lt;p>$$
T_\mathrm{response}=T_\mathrm{firstrun}-T_\mathrm{arrival}
$$&lt;/p>
&lt;h3 id="77-round-robin">7.7 Round Robin&lt;/h3>
&lt;p>&lt;strong>Round-Robin&lt;/strong>: instead of running jobs to completion, RR runs a job for a &lt;strong>time slice&lt;/strong> (sometimes called a &lt;strong>scheduling quantum&lt;/strong>) and then switches to the next job in the run queue&lt;/p>
&lt;p>The length of the time slice is critical for RR; a trade-off between response time and the cost of context switching&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The cost of context switch&lt;/strong>&lt;/p>
&lt;p>When programs run, they build up a great deal of state in CPU caches, TLBs, branch predictors, and other on-chip hardware. Switching to another job causes this state to be flushed and new state relevant to the currently-running job to be brought in, which may exact a noticeable performance cost.&lt;/p>
&lt;/blockquote>
&lt;p>RR is pretty awful for turnaround time&lt;/p>
&lt;blockquote>
&lt;p>More generally, any policy (such as RR) that is fair, i.e., that evenly divides the CPU among active processes on a small time scale, will perform poorly on metrics such as turnaround time.&lt;/p>
&lt;/blockquote>
&lt;h3 id="78-incorporating-io">7.8 Incorporating I/O&lt;/h3>
&lt;p>Relax assumption 4 (all jobs only use the CPU)&lt;/p>
&lt;p>Overlap will occur if using STCF&lt;/p>
&lt;h3 id="79-no-more-oracle">7.9 No More Oracle&lt;/h3>
&lt;p>Relax our final assumption (the run-time of each job is known)&lt;/p>
&lt;h2 id="8-scheduling-the-multi-level-feedback-queue">8 Scheduling: The Multi-Level Feedback Queue&lt;/h2>
&lt;p>The problem we try to address: given that we in general do not know anything about a process, how to&lt;/p>
&lt;ul>
&lt;li>optimize turnaround time&lt;/li>
&lt;li>minimize response time&lt;/li>
&lt;/ul>
&lt;h3 id="81-mlfq-basic-rules">8.1 MLFQ: Basic Rules&lt;/h3>
&lt;p>The MLFQ has a number of distinct &lt;strong>queues&lt;/strong>, each assigned a different &lt;strong>priority level&lt;/strong>. At any given time, a job that is ready to run is on a single queue.&lt;/p>
&lt;p>A job with higher priority (i.e., a job on a higher queue) is chosen to run; use round-robin scheduling among jobs with the same priority.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>rule 1&lt;/strong>: if Priority(A) &amp;gt; Priority(B), A runs (B doesn’t)&lt;/li>
&lt;li>&lt;strong>rule 2&lt;/strong>: if Priority(A) = Priority(B), A &amp;amp; B run in RR&lt;/li>
&lt;/ul>
&lt;p>To set the priority: MLFQ will try to learn about processes as they run, and thus use the history (&lt;em>long-running&lt;/em> or &lt;em>interactive&lt;/em>) of the job to predict its future behavior.&lt;/p>
&lt;h3 id="82-attempt-1-how-to-change-priority">8.2 Attempt #1: How To Change Priority&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>rule 3&lt;/strong>: when a job enters the system, it is placed at the highest priority&lt;/li>
&lt;li>&lt;strong>rule 4&lt;/strong>
&lt;ul>
&lt;li>a: if a job uses up an entire time slice while running, its priority is reduced&lt;/li>
&lt;li>b: if a job gives up the CPU before the time slice is up (&lt;em>therefore jobs with lower priority may have a chance to run&lt;/em>), it stays at the same priority level&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h4 id="example-1-a-single-long-running-job">Example 1: A Single Long-Running Job&lt;/h4>
&lt;h4 id="example-2-along-came-a-short-job">Example 2: Along Came A Short Job&lt;/h4>
&lt;p>The idea: first assumes a job to be short, otherwise move it down; this is an approximization of SJF&lt;/p>
&lt;h4 id="example-3-what-about-io">Example 3: What About I/O?&lt;/h4>
&lt;h4 id="problems-with-our-current-mlfq">Problems With Our Current MLFQ&lt;/h4>
&lt;ul>
&lt;li>&lt;strong>starvation&lt;/strong>: too many interactive jobs will consume all CPU time, thus long-running jobs will never receive any CPU time&lt;/li>
&lt;li>can be attacked by first using most of the time and then perform an I/O&lt;/li>
&lt;li>a program might change its behaviour&lt;/li>
&lt;/ul>
&lt;h3 id="83-attempt-2-the-priority-boost">8.3 Attempt #2: The Priority Boost&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>rule 5&lt;/strong>: after some time period $S$, move all the jobs in the system to the topmost queue&lt;/li>
&lt;/ul>
&lt;p>If $S$ is set too high, long-running jobs could starve; too low, and interactive jobs may not get a proper share of the CPU.&lt;/p>
&lt;h3 id="84-attempt-3-better-accounting">8.4 Attempt #3: Better Accounting&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>rule 4&lt;/strong>: once a job uses up its time allotment (分配) at a given level (regardless of how many times it has given up the CPU), its priority is reduced (i.e., it moves down one queue)&lt;/li>
&lt;/ul>
&lt;h3 id="85-tuning-mlfq-and-other-issues">8.5 Tuning MLFQ And Other Issues&lt;/h3>
&lt;h2 id="9-scheduling-proportional-share">9 Scheduling: Proportional Share&lt;/h2>
&lt;p>The concept: instead of optimizing for turnaround or response time, a scheduler might instead try to guarantee that each job obtain a certain percentage of CPU time.&lt;/p>
&lt;h3 id="91-basic-concept-tickets-represent-your-share">9.1 Basic Concept: Tickets Represent Your Share&lt;/h3>
&lt;p>&lt;strong>Tickets&lt;/strong>: the share of a resource that a process (or user or whatever) should receive.&lt;/p>
&lt;p>The system picks a ticket (randomly) whose owner process can run at this point&lt;/p>
&lt;blockquote>
&lt;p>If you are ever in need of a mechanism to represent a proportion of ownership, this concept just might be &amp;hellip; (wait for it) &amp;hellip; the ticket.&lt;/p>
&lt;/blockquote>
&lt;h3 id="92-ticket-mechanisms">9.2 Ticket Mechanisms&lt;/h3>
&lt;ul>
&lt;li>ticket currency&lt;/li>
&lt;li>ticket transfer&lt;/li>
&lt;li>ticket inflation (通货膨胀)&lt;/li>
&lt;/ul>
&lt;h3 id="93-implementation">9.3 Implementation&lt;/h3>
&lt;p>Best practice: order the list from the highest number of tickets to the lowest&lt;/p>
&lt;h3 id="94-an-example">9.4 An Example&lt;/h3>
&lt;p>Overall fairness is higher with higher job length&lt;/p>
&lt;h3 id="95-how-to-assign-tickets">9.5 How To Assign Tickets?&lt;/h3>
&lt;p>Given a set of jobs, the “ticket-assignment problem” remains open.&lt;/p>
&lt;h3 id="96-stride-scheduling">9.6 Stride Scheduling&lt;/h3>
&lt;p>Stride: inverse in proportion to the number of tickets a job has&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>curr &lt;span style="color:#f92672">=&lt;/span> remove_min(queue); &lt;span style="color:#75715e">// pick client with min pass
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>schedule(curr); &lt;span style="color:#75715e">// run for quantum
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>curr&lt;span style="color:#f92672">-&amp;gt;&lt;/span>pass &lt;span style="color:#f92672">+=&lt;/span> curr&lt;span style="color:#f92672">-&amp;gt;&lt;/span>stride; &lt;span style="color:#75715e">// update pass using stride
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>insert(queue, curr); &lt;span style="color:#75715e">// return curr to queue
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Stride scheduling is a deterministic fair-share scheduler.&lt;/p>
&lt;p>Lottery scheduling has no global state, so that new processes are easier to be incorporated&lt;/p>
&lt;h3 id="97-the-linux-completely-fair-scheduler-cfs">9.7 The Linux Completely Fair Scheduler (CFS)&lt;/h3>
&lt;blockquote>
&lt;p>Recent studies have shown that scheduler efficiency is surprisingly important; specifically, in a study of Google datacenters, Kanev et al. show that even after aggressive optimization, scheduling uses about 5% of overall datacenter CPU time.&lt;/p>
&lt;/blockquote>
&lt;h4 id="basic-operation">Basic Operation&lt;/h4>
&lt;p>Goal: to fairly divide a CPU evenly among all competing processes&lt;/p>
&lt;p>In the basic case, each process&amp;rsquo;s &lt;code>vruntime&lt;/code> increases at the same rate, propotional to the physical time. Pick the process with the lowest &lt;code>vruntime&lt;/code> to run next.&lt;/p>
&lt;p>Divide &lt;code>sched_latency&lt;/code> by process number to calculate the time slice; use &lt;code>min_granularity&lt;/code> to constrain the minimum possible time slice.&lt;/p>
&lt;h4 id="weighting-niceness">Weighting (Niceness)&lt;/h4>
&lt;p>Use &lt;strong>nice&lt;/strong> level to control the priority of a process. Positive nice values imply lower priority and negative values imply higher priority&lt;/p>
&lt;blockquote>
&lt;p>When you’re too nice, you just don’t get as much (scheduling) attention, alas.&lt;/p>
&lt;/blockquote>
&lt;p>Calculation of the time slice:&lt;/p>
&lt;p>$$
\texttt{time_slice}_k=\cfrac{\texttt{weight}_k}{\sum_{i=0}^{n-1}\texttt{weight}_i}\cdot\texttt{sched_latency}
$$&lt;/p>
&lt;p>Also adjusting the calculation of virtual runtime:&lt;/p>
&lt;p>$$
\texttt{vruntime}_i=\texttt{vruntime}_i+\cfrac{\texttt{weight}_0}{\texttt{weight}_i}\cdot\texttt{runtime}_i
$$&lt;/p>
&lt;p>The table preserves CPU proportionality ratios when the difference in nice values is constant&lt;/p>
&lt;h4 id="using-red-black-trees">Using Red-Black Trees&lt;/h4>
&lt;p>Use a red-black tree to maintain running (or runnable) processes; if a process goes to sleep (say, waiting fot an I/O to complete), it is removed.&lt;/p>
&lt;h4 id="dealing-with-io-and-sleeping-processes">Dealing With I/O And Sleeping Processes&lt;/h4>
&lt;p>A just-awaken process will monopolize the CPU; solution: set the &lt;code>vruntime&lt;/code> of that job to the min value found in the tree (which only keeps track of &lt;em>running&lt;/em> jobs!).&lt;/p>
&lt;h2 id="10-multiprocessor-scheduling-advanced">10 Multiprocessor Scheduling (Advanced)&lt;/h2>
&lt;h3 id="101-background-multiprocessor-architecture">10.1 Background: Multiprocessor Architecture&lt;/h3>
&lt;p>Two kinds of locality:&lt;/p>
&lt;ul>
&lt;li>temporal locality&lt;/li>
&lt;li>spatial locality&lt;/li>
&lt;/ul>
&lt;p>Problem with multi-code processors: &lt;strong>cache coherence&lt;/strong>; the hardware uses &lt;strong>bus snooping&lt;/strong> to update/invalidate outdated cache&lt;/p>
&lt;h3 id="102-dont-forget-synchronization">10.2 Don’t Forget Synchronization&lt;/h3>
&lt;h3 id="103-one-final-issue-cache-affinity">10.3 One Final Issue: Cache Affinity&lt;/h3>
&lt;p>A multiprocessor scheduler should consider cache affinity when making its scheduling decisions, perhaps preferring to keep a process on the same CPU if at all possible.&lt;/p>
&lt;h3 id="104-single-queue-scheduling">10.4 Single-Queue Scheduling&lt;/h3>
&lt;p>&lt;strong>Singlequeue multiprocessor scheduling&lt;/strong> (or &lt;strong>SQMS&lt;/strong>): take an existing policy that picks the best job to run next and adapt it to work on more than one CPU&lt;/p>
&lt;p>Problems:&lt;/p>
&lt;ul>
&lt;li>performance: the overhead of locking is high&lt;/li>
&lt;li>cache affinity: each job ends up bouncing around from CPU to CPU&lt;/li>
&lt;/ul>
&lt;h3 id="105-multi-queue-scheduling">10.5 Multi-Queue Scheduling&lt;/h3>
&lt;p>Each CPU has a queue of tasks to be performed.&lt;/p>
&lt;p>To solve &lt;strong>load imbalance&lt;/strong>: using &lt;strong>migration&lt;/strong>; a basic approach is to use &lt;strong>work stealing&lt;/strong>&lt;/p></content></item><item><title>Operating Systems: Three Easy Pieces 学习笔记（二）</title><link>https://yeshe.ng/blog/posts/2022/02/ostep-persistence-2/</link><pubDate>Wed, 23 Feb 2022 21:37:07 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/02/ostep-persistence-2/</guid><description>FSCK and Journaling、LFS、Flash-based SSD</description><content>&lt;h2 id="42-crash-consistency-fsck-and-journaling">42 Crash Consistency: FSCK and Journaling&lt;/h2>
&lt;blockquote>
&lt;p>One major challenge faced by a file system is how to update persistent data structures despite the presence of a power loss or system crash.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Crash-consistency problem&lt;/strong>: On-disk state may only partially get updated on system crash or power loss&lt;/p>
&lt;h3 id="421-a-detailed-example">42.1 A Detailed Example&lt;/h3>
&lt;h4 id="crash-scenarios">Crash Scenarios&lt;/h4>
&lt;p>Three possible outcomes if an error occured when a data block is allocated and written for a file (which should allocate a data block (and write data to it), update the data bitmap, and update the file inode):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>only the data block&lt;/strong> is written: no problem at all (from the perspective of file-system crash consistency)&lt;/li>
&lt;li>&lt;strong>only the updated inode&lt;/strong> is written: garbage data will be read; file-system inconsistency: the allocated status of the data block is different in the bitmap and the inode&lt;/li>
&lt;li>&lt;strong>only the bitmap&lt;/strong> is written: space leak&lt;/li>
&lt;li>&lt;strong>only the inode and the bitmap&lt;/strong> are written: garbage data&lt;/li>
&lt;li>&lt;strong>only the inode and the data block&lt;/strong>: inconsistency between the inode and the bitmap&lt;/li>
&lt;li>&lt;strong>only the bitmap and the data block&lt;/strong>: space leak again&lt;/li>
&lt;/ul>
&lt;h4 id="the-crash-consistency-problem">The Crash Consistency Problem&lt;/h4>
&lt;p>&lt;strong>Crash-consistency problem&lt;/strong> (or &lt;strong>consistent-update problem&lt;/strong>): the disk can only commit one write at a time, so that an update cannot be done atomically&lt;/p>
&lt;h3 id="422-solution-1-the-file-system-checker">42.2 Solution #1: The File System Checker&lt;/h3>
&lt;p>Early systems&amp;rsquo; approach: fix inconsistencies later (when rebooting)&lt;/p>
&lt;p>&lt;code>fsck&lt;/code>&amp;rsquo;s phases:&lt;/p>
&lt;ul>
&lt;li>check superblock&lt;/li>
&lt;li>scans inodes, indirect blocks, etc. to make consistency between inodes and bitmaps (by trusting inodes)&lt;/li>
&lt;li>check inodes for corruption and other problems&lt;/li>
&lt;li>verify link count&lt;/li>
&lt;li>check for duplicate pointers (two different inodes refer to the same block) (clear some inodes or make copies of the duplicate data)&lt;/li>
&lt;li>check for bad block pointers (and remove them)&lt;/li>
&lt;li>check directories&lt;/li>
&lt;/ul>
&lt;p>Fundamental problems of &lt;code>fsck&lt;/code>: too slow&lt;/p>
&lt;blockquote>
&lt;p>At a higher level, the basic premise of &lt;code>fsck&lt;/code> seems just a tad irrational. Consider our example above, where just three blocks are written to the disk; it is incredibly expensive to scan the entire disk to fix problems that occurred during an update of just three blocks.&lt;/p>
&lt;/blockquote>
&lt;h3 id="423-solution-2-journaling-or-write-ahead-logging">42.3 Solution #2: Journaling (or Write-Ahead Logging)&lt;/h3>
&lt;p>&lt;strong>Write-ahead logging&lt;/strong> (or &lt;strong>journaling&lt;/strong>): write logs before overwriting the structures to be able to recover from errors&lt;/p>
&lt;h4 id="data-journaling">Data Journaling&lt;/h4>
&lt;p>&lt;strong>Journal write&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>transaction begin block&lt;/li>
&lt;li>blocks containing the exact contents of the updating blocks (physical logging) or some more compact forms (logical logging)&lt;/li>
&lt;li>transaction end block&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Checkpointing&lt;/strong>: override the old structures in the file system&lt;/p>
&lt;p>Problems if a crach occurs during the write to the journal: if writing all the log blocks at once, as the disk might perform scheduling, they might not complete in order, and thus some blocks might not be correct (e.g. invalid data blocks within valid TxB and TxE blocks).&lt;/p>
&lt;p>Solution: write all blocks except the TxE at once, and write TxE block after these writes. &lt;strong>With atomicity guatantee!&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>Performance problem: the transaction-end block can only be written after previous blocks are written.&lt;/p>
&lt;p>Optimizing log writes: using checksum&lt;/p>
&lt;/blockquote>
&lt;p>Three phases:&lt;/p>
&lt;ul>
&lt;li>journal write (TxB, metadata and data)&lt;/li>
&lt;li>journal commit (TxE)&lt;/li>
&lt;li>checkpoint (write the contents to final on-disk locations)&lt;/li>
&lt;/ul>
&lt;h4 id="recovery">Recovery&lt;/h4>
&lt;p>Replay the transactions that are commited to the disk (&lt;strong>redo logging&lt;/strong>)&lt;/p>
&lt;h4 id="batching-log-updates">Batching Log Updates&lt;/h4>
&lt;p>Buffer all updates into a global transaction to avoid writing some same blocks many times.&lt;/p>
&lt;h4 id="making-the-log-finite">Making The Log Finite&lt;/h4>
&lt;p>Journaling file systems treat the log as a circular data structure (the journal is referred to as &lt;strong>circular log&lt;/strong>).&lt;/p>
&lt;p>Mark the transaction free in the journal by updating the journal superblock some time after checkpoint.&lt;/p>
&lt;p>Now the four phases:&lt;/p>
&lt;ul>
&lt;li>journal write&lt;/li>
&lt;li>journal commit&lt;/li>
&lt;li>checkpoint&lt;/li>
&lt;li>free&lt;/li>
&lt;/ul>
&lt;h4 id="metadata-journaling">Metadata journaling&lt;/h4>
&lt;p>&lt;strong>Ordered journaling&lt;/strong> (or &lt;strong>metadata journaling&lt;/strong>): the user data is not written to the journal&lt;/p>
&lt;p>One possible order (to avoid garbage data):&lt;/p>
&lt;ul>
&lt;li>data write (1)&lt;/li>
&lt;li>journal metadata write (2)&lt;/li>
&lt;li>journal commit&lt;/li>
&lt;li>checkpoint metadata&lt;/li>
&lt;li>free&lt;/li>
&lt;/ul>
&lt;p>(1) and (2) can be written concurrently&lt;/p>
&lt;h2 id="43-log-structured-file-systems">43 Log-structured File Systems&lt;/h2>
&lt;blockquote>
&lt;p>&lt;strong>The motivation&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>system memories are growing so that more data can be cached, and disk traffic increasingly consists of writes&lt;/li>
&lt;li>a large gap between random and sequential I/O performace&lt;/li>
&lt;li>exising file systems perform poorly on many common workloads&lt;/li>
&lt;li>file systems are not RAID-aware&lt;/li>
&lt;/ul>
&lt;p>An ideal file system would thus focus on write performance, and try to make use of the sequential bandwidth of the disk. Further, it would perform well on common workloads that not only write out data but also update on-disk metadata structures frequently. Finally, it would work well on RAIDs as well as single disks.&lt;/p>
&lt;/blockquote>
&lt;h3 id="431-writing-to-disk-sequentially">43.1 Writing To Disk Sequentially&lt;/h3>
&lt;p>The basic idea: writing all updates (such as data blocks, inodes, etc.) to the disk sequentially&lt;/p>
&lt;h3 id="432-writing-sequentially-and-effectively">43.2 Writing Sequentially And Effectively&lt;/h3>
&lt;p>Sequential writes is not enough; to achieve good performance we must issue contiguous writes (or one large write)&lt;/p>
&lt;p>&lt;strong>Write buffering&lt;/strong>: keeps track of updates &lt;em>in memory&lt;/em>, and write the buffered data at once when gets sufficient updates (called a &lt;strong>segment&lt;/strong>)&lt;/p>
&lt;h3 id="433-how-much-to-buffer">43.3 How Much To Buffer?&lt;/h3>
&lt;p>$$
D=\cfrac{F}{1-F}\times R_\mathrm{peak}\times T_\mathrm{position}
$$&lt;/p>
&lt;h3 id="434-problem-finding-inodes">43.4 Problem: Finding Inodes&lt;/h3>
&lt;h3 id="435-solution-through-indirection-the-inode-map">43.5 Solution Through Indirection: The Inode Map&lt;/h3>
&lt;p>The &lt;strong>inode map&lt;/strong>: takes an inode number as input and returns the disk address of the &lt;em>most recent&lt;/em> version of the inode&lt;/p>
&lt;p>LFS places chunks of the inode map right &lt;em>next to&lt;/em> where it is writing all of the other new information.&lt;/p>
&lt;blockquote>
&lt;p>Inode maps are compact enough to keep the active portions cached in main memory: inode map lookups rarely require disk accesses.&lt;/p>
&lt;p>(From &lt;em>The Design and Implementation of a Log-Structured File System&lt;/em>)&lt;/p>
&lt;/blockquote>
&lt;h3 id="436-completing-the-solution-the-checkpoint-region">43.6 Completing The Solution: The Checkpoint Region&lt;/h3>
&lt;p>The &lt;strong>checkpoint region&lt;/strong> (or &lt;strong>CR&lt;/strong>) contains pointers to the latest pieces of the inode map; it is only updated periodically for performance&lt;/p>
&lt;h3 id="437-reading-a-file-from-disk-a-recap">43.7 Reading A File From Disk: A Recap&lt;/h3>
&lt;p>The steps of reading a file (from start):&lt;/p>
&lt;ul>
&lt;li>read the CR and cache it in memory&lt;/li>
&lt;li>lookup the CR to find the address of a given inode&lt;/li>
&lt;li>(the rest is pretty much the same as a typical FS)&lt;/li>
&lt;/ul>
&lt;h3 id="438-what-about-directories">43.8 What About Directories?&lt;/h3>
&lt;p>Steps:&lt;/p>
&lt;ul>
&lt;li>look in the imap for the location of the inode of the directory&lt;/li>
&lt;li>find inode number of the file in the inode of the directory&lt;/li>
&lt;li>look in the imap for the location of the inode of the file&lt;/li>
&lt;/ul>
&lt;p>The imap also solves the &lt;strong>recursive update problem&lt;/strong> through indirection.&lt;/p>
&lt;h3 id="439-a-new-problem-garbage-collection">43.9 A New Problem: Garbage Collection&lt;/h3>
&lt;p>The cleaner assures to preserve contiguous free regions by compacting existing segments to new, contiguous segments&lt;/p>
&lt;h3 id="4310-determining-block-liveness">43.10 Determining Block Liveness&lt;/h3>
&lt;p>Use &lt;strong>segment summary block&lt;/strong> at the head of the segment to record the inode number and offset (which block of the file) for each data block&lt;/p>
&lt;p>To determine whether a block is dead:&lt;/p>
&lt;ul>
&lt;li>look in the segment summary block and find its inode number and offset&lt;/li>
&lt;li>look in the imap to find and read the inode&lt;/li>
&lt;li>look in the inode using the offset to compare the disk location&lt;/li>
&lt;/ul>
&lt;p>LFS can also use &lt;strong>version number&lt;/strong> to determine this, avoiding extra reads.&lt;/p>
&lt;h3 id="4311-a-policy-question-which-blocks-to-clean-and-when">43.11 A Policy Question: Which Blocks To Clean, And When?&lt;/h3>
&lt;p>The time to clean can be:&lt;/p>
&lt;ul>
&lt;li>perioducally&lt;/li>
&lt;li>during idle time&lt;/li>
&lt;li>when the disk is full&lt;/li>
&lt;/ul>
&lt;h3 id="4312-crash-recovery-and-the-log">43.12 Crash Recovery And The Log&lt;/h3>
&lt;p>Use two CRs containing update timestamps to recover from crash during CR updates&lt;/p>
&lt;p>The last consistent snapshot of the file system may be quite old as LFS only writes the CR every 30 seconds or so; use &lt;strong>roll forward&lt;/strong> to read through the next segments of the end of the log to see if there are any valid updates, and updates the file system accordingly.&lt;/p>
&lt;h2 id="44-flash-based-ssds">44 Flash-based SSDs&lt;/h2>
&lt;p>&lt;strong>Solid-state storage&lt;/strong>: random access, retains information on power loss&lt;/p>
&lt;h3 id="441-storing-a-single-bit">44.1 Storing a Single Bit&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>single-level cell&lt;/strong> (&lt;strong>SLC&lt;/strong>) flash: a single bit per transistor&lt;/li>
&lt;li>&lt;strong>multi-level cell&lt;/strong> (&lt;strong>MLC&lt;/strong>) flash: two bits per transistor (00, 01, 10, 11 represented by different levels of charge)&lt;/li>
&lt;li>&lt;strong>triple-level cell&lt;/strong> (&lt;strong>TLC&lt;/strong>) flash: three bits per cell&lt;/li>
&lt;/ul>
&lt;p>SLC chips achieve higher performance and are more expensive.&lt;/p>
&lt;h3 id="442-from-bits-to-banksplanes">44.2 From Bits to Banks/Planes&lt;/h3>
&lt;p>Flash chips are organized into &lt;strong>banks&lt;/strong> or &lt;strong>planes&lt;/strong> which consist of a large number of cells.&lt;/p>
&lt;p>A bank is accessed in two different sized units:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>block&lt;/strong> (or &lt;strong>erase block&lt;/strong>): typically of size 128KB or 256KB&lt;/li>
&lt;li>&lt;strong>page&lt;/strong>: a few KB in size (e.g. 4KB)&lt;/li>
&lt;/ul>
&lt;h3 id="443-basic-flash-operations">44.3 Basic Flash Operations&lt;/h3>
&lt;p>Three low-level operations:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>read&lt;/strong> (a &lt;strong>page&lt;/strong>): random access; fast, 10s of $\mathrm{\mu s}$ or so&lt;/li>
&lt;li>&lt;strong>erase&lt;/strong> (a &lt;strong>block&lt;/strong>): setting each bit to 1; quite expensive, takes a few ms; need to copy data of care somewhere else&lt;/li>
&lt;li>&lt;strong>program&lt;/strong> (a &lt;strong>page&lt;/strong>): After a block is erased, change some of the 1&amp;rsquo;s within a page to 0&amp;rsquo;s; usually taking around $100\mathrm{\mu s}$&lt;/li>
&lt;/ul>
&lt;h3 id="444-flash-performance-and-reliability">44.4 Flash Performance And Reliability&lt;/h3>
&lt;p>&lt;strong>Wear out&lt;/strong>: erasing and programming accure a little bit of extra charge, 0 and 1 are increasingly difficult to be differentiated.&lt;/p>
&lt;p>The typical lifetime:&lt;/p>
&lt;ul>
&lt;li>MLC: 10,000 P/E (Program/Erase) cycles&lt;/li>
&lt;li>SLC: 100,000 P/E cycles&lt;/li>
&lt;li>actual lifetimes are much longer&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Disturbance&lt;/strong>: some bits get flipped in neighboring pages when accessing (read/program) a particular page&lt;/p>
&lt;h3 id="445-from-raw-flash-to-flash-based-ssds">44.5 From Raw Flash to Flash-Based SSDs&lt;/h3>
&lt;p>&lt;strong>Flash translation layer&lt;/strong> (or &lt;strong>FTL&lt;/strong>): translates read and write requests on &lt;em>logical blocks&lt;/em> into low-level read, erase and program commands on underlying &lt;em>physical blocks&lt;/em> and &lt;em>physical pages&lt;/em>&lt;/p>
&lt;blockquote>
&lt;p>The FTL should accomplish this task with the goal of delivering excellent performance and high reliability.&lt;/p>
&lt;/blockquote>
&lt;p>To achieve performance:&lt;/p>
&lt;ul>
&lt;li>parallel&lt;/li>
&lt;li>reduce write amplification&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Write amplification&lt;/strong>: total write traffic issued to the flash chips (by the FTL) divided by the total write traffic issued to the SSD (by the client)&lt;/p>
&lt;p>To achieve high reliability:&lt;/p>
&lt;ul>
&lt;li>for wear out: &lt;strong>wear leveling&lt;/strong>&lt;/li>
&lt;li>for program disturbance: a sequential-programming approach&lt;/li>
&lt;/ul>
&lt;h3 id="446-ftl-organization-a-bad-approach">44.6 FTL Organization: A Bad Approach&lt;/h3>
&lt;p>&lt;strong>Direct mapped organization&lt;/strong>: a read to a logical page is mapped directly to the physical page with the same page number&lt;/p>
&lt;p>The problems with the direct-mapped FTL:&lt;/p>
&lt;ul>
&lt;li>performance: each write needs to
&lt;ul>
&lt;li>read in the entire block (costly)&lt;/li>
&lt;li>erase the entire block (quite costly)&lt;/li>
&lt;li>program the block (costly)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>reliability: the same block can be erased and programmed over and over (e.g., file system metadata is overritten repeatedly)&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>The direct mapped approach simply &lt;strong>gives too much control over wear out to the client workload&lt;/strong>; if the workload does not spread write load evenly across its logical blocks, the underlying physical blocks containing popular data will quickly wear out.&lt;/p>
&lt;/blockquote>
&lt;h3 id="447-a-log-structured-ftl">44.7 A Log-Structured FTL&lt;/h3>
&lt;p>Use a &lt;strong>mapping table&lt;/strong> to map the logical block to the physical address&lt;/p>
&lt;p>The log-based approach improves performance in:&lt;/p>
&lt;ul>
&lt;li>erases being required once in a while&lt;/li>
&lt;li>spread writes across pages (&lt;strong>wear leveling&lt;/strong>)&lt;/li>
&lt;/ul>
&lt;p>Downsides:&lt;/p>
&lt;ul>
&lt;li>garbage collection may drives up write amplification&lt;/li>
&lt;li>in-memory mapping tables might be large&lt;/li>
&lt;/ul>
&lt;h3 id="448-garbage-collection">44.8 Garbage Collection&lt;/h3>
&lt;p>To reduce GC costs, &lt;strong>overprovisioning&lt;/strong> can be performed: adding extra flash capacity and delay cleaning, which will be performed in background.&lt;/p>
&lt;h3 id="449-mapping-table-size">44.9 Mapping Table Size&lt;/h3>
&lt;h4 id="block-based-mapping">Block-Based Mapping&lt;/h4>
&lt;h4 id="hybrid-mapping">Hybrid Mapping&lt;/h4>
&lt;p>Two types of mapping table:&lt;/p>
&lt;ul>
&lt;li>log table (per-page mappings)&lt;/li>
&lt;li>data table (per-block mappings)&lt;/li>
&lt;/ul>
&lt;p>To read:&lt;/p>
&lt;ul>
&lt;li>first find in the log table&lt;/li>
&lt;li>if not found, find in the data table&lt;/li>
&lt;/ul>
&lt;p>The key to the hybrid mapping strategy is keeping the number of log blocks small. To do that, the FTL has to periodically examine log blocks and switch them into blocks that &lt;em>can&lt;/em> be pointed to by only a single block pointer.&lt;/p>
&lt;p>Three types of merge:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>switch merge&lt;/strong>: all pointers of a block in the log table can be switched into a pointer pointing to this block&lt;/li>
&lt;li>&lt;strong>partial merge&lt;/strong>&lt;/li>
&lt;li>&lt;strong>full merge&lt;/strong>&lt;/li>
&lt;/ul>
&lt;h4 id="page-mapping-plus-caching">Page Mapping Plus Caching&lt;/h4>
&lt;h3 id="4410-wear-leveling">44.10 Wear Leveling&lt;/h3>
&lt;p>One problem of the log-structured approach: blocks filled with long-lived data do not get overwritten; solution: periodically read and rewrite elsewhere&lt;/p></content></item><item><title>Operating Systems: Three Easy Pieces 学习笔记（一）</title><link>https://yeshe.ng/blog/posts/2022/01/ostep-persistence/</link><pubDate>Thu, 13 Jan 2022 20:38:28 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/01/ostep-persistence/</guid><description>I/O Devices、Hard Disk Drives、RAID、Files and Directories、File System Implementation、FFS</description><content>&lt;h2 id="36-io-devices">36 I/O Devices&lt;/h2>
&lt;h3 id="361-system-architecture">36.1 System Architecture&lt;/h3>
&lt;p>Devices are connected to the system via:&lt;/p>
&lt;ul>
&lt;li>memory bus (CPU - main memory)&lt;/li>
&lt;li>general I/O bus: PCI (graphics, high performance devices)&lt;/li>
&lt;li>peripheral bus: SCSI, SATA, USB (slow devices)&lt;/li>
&lt;/ul>
&lt;p>In modern systems:&lt;/p>
&lt;ul>
&lt;li>PCIe Graphics&lt;/li>
&lt;li>memory interconnect&lt;/li>
&lt;li>DMI (Direct Media Interface)&lt;/li>
&lt;li>PCIe&lt;/li>
&lt;li>eSATA&lt;/li>
&lt;/ul>
&lt;h3 id="362-a-canonical-device">36.2 A Canonical Device&lt;/h3>
&lt;p>A device has two important components:&lt;/p>
&lt;ul>
&lt;li>interface
&lt;ul>
&lt;li>status&lt;/li>
&lt;li>command&lt;/li>
&lt;li>data&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>internal structure&lt;/li>
&lt;/ul>
&lt;h3 id="363-the-canonical-protocol">36.3 The Canonical Protocol&lt;/h3>
&lt;p>&lt;strong>Programmed I/O&lt;/strong> (&lt;strong>PIO&lt;/strong>): the main CPU is involved with the data movement&lt;/p>
&lt;h3 id="364-lowering-cpu-overhead-with-interrupts">36.4 Lowering CPU Overhead With Interrupts&lt;/h3>
&lt;p>Utilize CPU:&lt;/p>
&lt;ul>
&lt;li>interrupt&lt;/li>
&lt;li>a two-phased approach: polls first, using interrupts if not yet finished&lt;/li>
&lt;li>coalescing&lt;/li>
&lt;/ul>
&lt;h3 id="365-more-efficient-data-movement-with-dma">36.5 More Efficient Data Movement With DMA&lt;/h3>
&lt;p>&lt;strong>Direct Memory Access&lt;/strong> (DMA): transfer data between devices and main memory &lt;strong>without much CPU intervention&lt;/strong>&lt;/p>
&lt;h3 id="366-methods-of-device-interaction">36.6 Methods Of Device Interaction&lt;/h3>
&lt;ul>
&lt;li>explicit I/O instructions (on x86, the in and out instructions), usually privileged&lt;/li>
&lt;li>memory-mapped I/O (no new instructions are needed)&lt;/li>
&lt;/ul>
&lt;h3 id="367-fitting-into-the-os-the-device-driver">36.7 Fitting Into The OS: The Device Driver&lt;/h3>
&lt;p>Read/write blocks:&lt;/p>
&lt;ul>
&lt;li>file system&lt;/li>
&lt;li>raw interface (support low-level storage management)&lt;/li>
&lt;/ul>
&lt;h3 id="368-case-study-a-simple-ide-disk-driver">36.8 Case Study: A Simple IDE Disk Driver&lt;/h3>
&lt;p>Registers are available by reading or writing to specific “I/O addresses” using (on x86) the &lt;code>in&lt;/code> and &lt;code>out&lt;/code> I/O instructions.&lt;/p>
&lt;ul>
&lt;li>wait for drive to be ready&lt;/li>
&lt;li>write parameters to command registers&lt;/li>
&lt;li>start the I/O: issuing read/write to command register&lt;/li>
&lt;li>data transfer (for writes): write data to data port&lt;/li>
&lt;li>handle interrupts&lt;/li>
&lt;/ul>
&lt;h2 id="37-hard-disk-drives">37 Hard Disk Drives&lt;/h2>
&lt;h3 id="371-the-interface">37.1 The Interface&lt;/h3>
&lt;p>The drive consists of a large number of sectors (512-byte blocks), each of which can be read or written&lt;/p>
&lt;p>View the disk as an array of sectors (扇区)&lt;/p>
&lt;ul>
&lt;li>multi-sector operations are possible&lt;/li>
&lt;li>only guarantee a single 512-byte write is atomic&lt;/li>
&lt;/ul>
&lt;p>Efficiency:&lt;/p>
&lt;ul>
&lt;li>access closer blocks are faster than farther ones&lt;/li>
&lt;li>sequential read or write are faster than the random access manner&lt;/li>
&lt;/ul>
&lt;h3 id="372-basic-geometry">37.2 Basic Geometry&lt;/h3>
&lt;ul>
&lt;li>platter: 磁盘&lt;/li>
&lt;li>surface: the 2 sides of a platter&lt;/li>
&lt;li>spindle: 轴&lt;/li>
&lt;li>track: 道, concentric circles of sectors&lt;/li>
&lt;li>disk head&lt;/li>
&lt;li>disk arm&lt;/li>
&lt;/ul>
&lt;h3 id="373-a-simple-disk-drive">37.3 A Simple Disk Drive&lt;/h3>
&lt;p>I/O time:&lt;/p>
&lt;ul>
&lt;li>rotational delay&lt;/li>
&lt;li>seek time
&lt;ul>
&lt;li>acceleration&lt;/li>
&lt;li>coasting&lt;/li>
&lt;li>deceleration&lt;/li>
&lt;li>settling (significant)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>transfer&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Track skew&lt;/strong>: sequential reads can be properly serviced when crossing track boundaries&lt;/p>
&lt;p>&lt;strong>Multi-zoned&lt;/strong> disk drives:&lt;/p>
&lt;ul>
&lt;li>a zone is a consecutive set of tracks on a surface&lt;/li>
&lt;li>each zone has the same number of sectors per track&lt;/li>
&lt;li>outer zones have more sectors&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Cache&lt;/strong> (track buffer):&lt;/p>
&lt;ul>
&lt;li>read: to read in all of the sectors on that track and cache them in its memory&lt;/li>
&lt;li>write
&lt;ul>
&lt;li>write back: acknowledge when puts into memory&lt;/li>
&lt;li>write through: acknowledge after actual writes&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="374-io-time-doing-the-math">37.4 I/O Time: Doing The Math&lt;/h3>
&lt;p>The &lt;strong>rate of I/O&lt;/strong>:&lt;/p>
&lt;p>$$
T_\mathrm{I/O} = T_\mathrm{seek}+T_\mathrm{rotation}+T_\mathrm{transfer}
$$&lt;/p>
&lt;p>$$
R_\mathrm{I/O}=\cfrac{Size_\mathrm{transfer}}{T_\mathrm{I/O}}
$$&lt;/p>
&lt;ul>
&lt;li>a huge gap in drive performance between random and sequential workloads&lt;/li>
&lt;li>a large difference in performance between high-end “performance” drives and low-end “capacity” drives&lt;/li>
&lt;/ul>
&lt;h3 id="375-disk-scheduling">37.5 Disk Scheduling&lt;/h3>
&lt;p>Principle: &lt;strong>shortest job first&lt;/strong>, &lt;strong>SJF&lt;/strong>&lt;/p>
&lt;p>&lt;strong>Shortest-seek-time-first&lt;/strong> (&lt;strong>SSTF&lt;/strong>, also shortest-seek-first or &lt;strong>SSF&lt;/strong>): picking requests on nearest track to complete first&lt;/p>
&lt;p>problems:&lt;/p>
&lt;ul>
&lt;li>the drive geometry is not available to the OS (fix: nearest-block-first, NBF)&lt;/li>
&lt;li>starvation: a steady stream to the closest track will block the access to other tracks&lt;/li>
&lt;/ul>
&lt;h4 id="elevator-algorithm">Elevator algorithm&lt;/h4>
&lt;p>&lt;strong>SCAN&lt;/strong>: simply moves back and forth across the disks&lt;/p>
&lt;p>&lt;strong>F-SCAN&lt;/strong>: freezes the queue when doing a sweep (avoid starvation of far-away requests)&lt;/p>
&lt;p>&lt;strong>C-SCAN (Circular SCAN)&lt;/strong>: only sweeps from outer-to-inner (more fair to the inner and outer tracks)&lt;/p>
&lt;blockquote>
&lt;p>Prevent fights from taking place on elevators!&lt;/p>
&lt;/blockquote>
&lt;p>SCAN and SSTF does not adhere to SJF closly: they ignore rotation&lt;/p>
&lt;h4 id="sptf-shortest-positioning-time-first">SPTF: Shortest positioning time first&lt;/h4>
&lt;p>Also called &lt;strong>shortest access time first&lt;/strong>, &lt;strong>SATF&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>In engineering, it turns out “it depends” is almost always the answer, reflecting that trade-offs are part of the life of the engineer.&lt;/p>
&lt;/blockquote>
&lt;p>On modern drives both seek and rotation are roughly equivalent&lt;/p>
&lt;p>SPTF is usually performed inside a drive&lt;/p>
&lt;h4 id="other-scheduling-issues">Other scheduling issues&lt;/h4>
&lt;p>&lt;strong>Where to perform scheduling&lt;/strong>: The OS scheduler usually picks (what it thinks) some best few requests (say 16) and issues them all to disk; the disk then uses its internal knowledge to service said requests in the best possible (SPTF) order.&lt;/p>
&lt;p>&lt;strong>I/O merging&lt;/strong>: merge I/O requests by the OS&lt;/p>
&lt;p>&lt;strong>Non-work-conserving&lt;/strong>: waiting for some time for a possible better request&lt;/p>
&lt;h2 id="38-redundant-arrays-of-inexpensive-disks-raids">38 Redundant Arrays of Inexpensive Disks (RAIDs)&lt;/h2>
&lt;p>&lt;strong>RAID&lt;/strong>: a technique to use multiple disks in concert to build a faster, bigger, and more reliable disk system&lt;/p>
&lt;ul>
&lt;li>externally looks like a disk&lt;/li>
&lt;li>internally consisting multiple disks, memory and processor(s)&lt;/li>
&lt;/ul>
&lt;h3 id="381-interface-and-raid-internals">38.1 Interface And RAID Internals&lt;/h3>
&lt;p>To perform a logical I/O:&lt;/p>
&lt;ul>
&lt;li>calculate the actual disk(s)&lt;/li>
&lt;li>perform one or more physical I/Os&lt;/li>
&lt;/ul>
&lt;h3 id="382-fault-model">38.2 Fault Model&lt;/h3>
&lt;blockquote>
&lt;p>RAIDs are designed to detect and recover from certain kinds of disk faults.&lt;/p>
&lt;/blockquote>
&lt;p>The &lt;strong>fail-stop&lt;/strong> fault model: a not-working disk is assumed permanently lost&lt;/p>
&lt;h3 id="383-how-to-evaluate-a-raid">38.3 How To Evaluate A RAID&lt;/h3>
&lt;p>Characteristics:&lt;/p>
&lt;ul>
&lt;li>capacity&lt;/li>
&lt;li>reliability&lt;/li>
&lt;li>performance&lt;/li>
&lt;/ul>
&lt;h3 id="384-raid-level-0-striping">38.4 RAID Level 0: Striping&lt;/h3>
&lt;blockquote>
&lt;p>RAID level 0, or striping as it is better known, serves as an excellent upper-bound on performance and capacity.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Striping&lt;/strong>: no redundancy; spread the blocks of the array across the disks in a round-robin fashion to get the most parrallelism.&lt;/p>
&lt;p>Simple striping:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">Disk 0&lt;/th>
&lt;th style="text-align:center">Disk 1&lt;/th>
&lt;th style="text-align:center">Disk 2&lt;/th>
&lt;th style="text-align:center">Disk 3&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">1&lt;/td>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">4&lt;/td>
&lt;td style="text-align:center">5&lt;/td>
&lt;td style="text-align:center">6&lt;/td>
&lt;td style="text-align:center">7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">8&lt;/td>
&lt;td style="text-align:center">9&lt;/td>
&lt;td style="text-align:center">10&lt;/td>
&lt;td style="text-align:center">11&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">12&lt;/td>
&lt;td style="text-align:center">13&lt;/td>
&lt;td style="text-align:center">14&lt;/td>
&lt;td style="text-align:center">15&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Stripe&lt;/strong>: blocks in the same row (0, 1, 2, 3)&lt;/p>
&lt;p>Striping with a bigger chunk size (8KB, 2 blocks, a stripe consists of 32KB of data):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">Disk 0&lt;/th>
&lt;th style="text-align:center">Disk 1&lt;/th>
&lt;th style="text-align:center">Disk 2&lt;/th>
&lt;th style="text-align:center">Disk 3&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">4&lt;/td>
&lt;td style="text-align:center">&lt;strong>6&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">1&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;td style="text-align:center">5&lt;/td>
&lt;td style="text-align:center">&lt;strong>7&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">8&lt;/td>
&lt;td style="text-align:center">10&lt;/td>
&lt;td style="text-align:center">12&lt;/td>
&lt;td style="text-align:center">14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">9&lt;/td>
&lt;td style="text-align:center">11&lt;/td>
&lt;td style="text-align:center">13&lt;/td>
&lt;td style="text-align:center">15&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h4 id="chunk-sizes">Chunk sizes&lt;/h4>
&lt;ul>
&lt;li>small chunk size: on large files, positioning more on a single disk&lt;/li>
&lt;li>large chunk size: less parallelism, but also less posisioning time&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Thus, determining the “best” chunk size is hard to do, as it requires a great deal of knowledge about the workload presented to the disk system.&lt;/p>
&lt;/blockquote>
&lt;h4 id="raid-0-analysis">RAID-0 Analysis&lt;/h4>
&lt;ul>
&lt;li>capacity: perfect&lt;/li>
&lt;li>reliability: any disk failure will lead to data loss&lt;/li>
&lt;li>performance: all disks are utilized, often in parallel&lt;/li>
&lt;/ul>
&lt;h4 id="evaluating-raid-performances">Evaluating RAID Performances&lt;/h4>
&lt;p>Two performance metrics:&lt;/p>
&lt;ul>
&lt;li>single-request latency&lt;/li>
&lt;li>steady-state throughput (critical in high-perfromance environments)&lt;/li>
&lt;/ul>
&lt;p>Two types of workloads (considered here):&lt;/p>
&lt;ul>
&lt;li>sequential: large contiguous chunks
&lt;ul>
&lt;li>searching through a large file for a keyword&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>random: samll requests, to different random locations on disk
&lt;ul>
&lt;li>transactional workloads on a database management system&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Denote the sequential and random workload&amp;rsquo;s throughput as:&lt;/p>
&lt;ul>
&lt;li>$S$ for sequential&lt;/li>
&lt;li>$R$ for random&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;h3 id="385-raid-level-1-mirroring">38.5 RAID Level 1: Mirroring&lt;/h3>
&lt;p>&lt;strong>RAID-10&lt;/strong> (or &lt;strong>RAID 1+0&lt;/strong>, &lt;strong>stripe of mirrors&lt;/strong>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">Disk 0&lt;/th>
&lt;th style="text-align:center">Disk 1&lt;/th>
&lt;th style="text-align:center">Disk 2&lt;/th>
&lt;th style="text-align:center">Disk 3&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">1&lt;/td>
&lt;td style="text-align:center">1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">4&lt;/td>
&lt;td style="text-align:center">4&lt;/td>
&lt;td style="text-align:center">5&lt;/td>
&lt;td style="text-align:center">5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">6&lt;/td>
&lt;td style="text-align:center">6&lt;/td>
&lt;td style="text-align:center">7&lt;/td>
&lt;td style="text-align:center">7&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>RAID-01&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">Disk 0&lt;/th>
&lt;th style="text-align:center">Disk 1&lt;/th>
&lt;th style="text-align:center">Disk 2&lt;/th>
&lt;th style="text-align:center">Disk 3&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">1&lt;/td>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">4&lt;/td>
&lt;td style="text-align:center">5&lt;/td>
&lt;td style="text-align:center">4&lt;/td>
&lt;td style="text-align:center">5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">6&lt;/td>
&lt;td style="text-align:center">7&lt;/td>
&lt;td style="text-align:center">6&lt;/td>
&lt;td style="text-align:center">7&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;/blockquote>
&lt;ul>
&lt;li>read: either copy&lt;/li>
&lt;li>write: update both&lt;/li>
&lt;/ul>
&lt;h4 id="analysis">Analysis&lt;/h4>
&lt;blockquote>
&lt;p>&lt;strong>Consistent-update problem&lt;/strong>&lt;/p>
&lt;p>untimely failures on write: inconsistent copies&lt;/p>
&lt;p>solution: &lt;strong>write-ahead log&lt;/strong> and &lt;strong>recovery&lt;/strong> procedure&lt;/p>
&lt;/blockquote>
&lt;ul>
&lt;li>capacity: expensive, $N\cdot B/2$ for mirroring level = 2&lt;/li>
&lt;li>reliability: tolerate failure on any disk, up to $N/2$ disks can fail&lt;/li>
&lt;li>single request performance
&lt;ul>
&lt;li>read: sames as on a single disk ($R$, writes to two blocks)&lt;/li>
&lt;li>write: slightly higher than writing to a single disk (worst-case seek and rotational delay)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>steady-state throughput
&lt;ul>
&lt;li>write: ($\frac{N}{2}\cdot S$, writes to $N$ blocks)&lt;/li>
&lt;li>read: same as write ($\frac{N}{2}\cdot S$, each disk receives a request for every other block)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>random reads/writes
&lt;ul>
&lt;li>read: $N\cdot R$&lt;/li>
&lt;li>write: $\frac{N}{2}\cdot R$&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="386-raid-level-4-saving-space-with-parity">38.6 RAID Level 4: Saving Space With Parity&lt;/h3>
&lt;blockquote>
&lt;p>Parity-based approaches attempt to use less capacity and thus overcome the huge space penalty paid by mirrored systems. They do so at a cost, however: performance.&lt;/p>
&lt;/blockquote>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">Disk 0&lt;/th>
&lt;th style="text-align:center">Disk 1&lt;/th>
&lt;th style="text-align:center">Disk 2&lt;/th>
&lt;th style="text-align:center">Disk 3&lt;/th>
&lt;th style="text-align:center">Disk 4&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">1&lt;/td>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P0&lt;/strong>&lt;/em>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">4&lt;/td>
&lt;td style="text-align:center">5&lt;/td>
&lt;td style="text-align:center">6&lt;/td>
&lt;td style="text-align:center">7&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P1&lt;/strong>&lt;/em>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">8&lt;/td>
&lt;td style="text-align:center">9&lt;/td>
&lt;td style="text-align:center">10&lt;/td>
&lt;td style="text-align:center">11&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P2&lt;/strong>&lt;/em>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">12&lt;/td>
&lt;td style="text-align:center">13&lt;/td>
&lt;td style="text-align:center">14&lt;/td>
&lt;td style="text-align:center">15&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P3&lt;/strong>&lt;/em>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Simple function: &lt;strong>XOR&lt;/strong>, which makes a row having even number of 1&lt;/p>
&lt;h4 id="analysis-1">Analysis&lt;/h4>
&lt;ul>
&lt;li>capacity: $(N-1)\cdot B$&lt;/li>
&lt;li>reliability: tolerates 1 disk failure&lt;/li>
&lt;li>performance
&lt;ul>
&lt;li>steady-state: $(N-1)\cdot S$ (full-stripe write: calculate parity bits first then write all disks in parallel)&lt;/li>
&lt;li>random
&lt;ul>
&lt;li>read: $(N-1)\cdot R$&lt;/li>
&lt;li>write: $R/2$ (the parity disk is the bottleneck)
&lt;ul>
&lt;li>additive parity: read in parallel, calculate XOR, and write in parallel (larger RAIDs require a high number of reads)&lt;/li>
&lt;li>subtractive parity: $P_\mathrm{new}=(C_\mathrm{old}\oplus C_\mathrm{new})\oplus P_\mathrm{old}$ (&lt;em>Why not always better?&amp;ndash; because it has to read the parity disk?&lt;/em>)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Small-write problem&lt;/strong>: the parity disk is a bottleneck under small writes which prevents parallelism&lt;/p>
&lt;h3 id="387-raid-level-5-rotating-parity">38.7 RAID Level 5: Rotating Parity&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">Disk 0&lt;/th>
&lt;th style="text-align:center">Disk 1&lt;/th>
&lt;th style="text-align:center">Disk 2&lt;/th>
&lt;th style="text-align:center">Disk 3&lt;/th>
&lt;th style="text-align:center">Disk 4&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">0&lt;/td>
&lt;td style="text-align:center">1&lt;/td>
&lt;td style="text-align:center">2&lt;/td>
&lt;td style="text-align:center">3&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P0&lt;/strong>&lt;/em>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">5&lt;/td>
&lt;td style="text-align:center">6&lt;/td>
&lt;td style="text-align:center">7&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P1&lt;/strong>&lt;/em>&lt;/td>
&lt;td style="text-align:center">4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">10&lt;/td>
&lt;td style="text-align:center">11&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P2&lt;/strong>&lt;/em>&lt;/td>
&lt;td style="text-align:center">8&lt;/td>
&lt;td style="text-align:center">9&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">15&lt;/td>
&lt;td style="text-align:center">&lt;em>&lt;strong>P3&lt;/strong>&lt;/em>&lt;/td>
&lt;td style="text-align:center">12&lt;/td>
&lt;td style="text-align:center">13&lt;/td>
&lt;td style="text-align:center">14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">&lt;em>&lt;strong>P4&lt;/strong>&lt;/em>&lt;/td>
&lt;td style="text-align:center">16&lt;/td>
&lt;td style="text-align:center">17&lt;/td>
&lt;td style="text-align:center">18&lt;/td>
&lt;td style="text-align:center">19&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h4 id="analysis-2">Analysis&lt;/h4>
&lt;p>random write: $\frac{N}{4}\cdot R$ (four I/O operations) (&lt;em>aren&amp;rsquo;t the reads/writes parallel?&amp;ndash;the data disk itself serves as parity disk as well&lt;/em>)&lt;/p>
&lt;h2 id="39-interlude-files-and-directories">39 Interlude: Files and Directories&lt;/h2>
&lt;blockquote>
&lt;p>Thus, the OS must take extra care with such a device: this is where users keep data that they really care about.&lt;/p>
&lt;/blockquote>
&lt;h3 id="391-files-and-directories">39.1 Files And Directories&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>file&lt;/strong>: linear array of bytes
&lt;ul>
&lt;li>low-level name (inode number)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>directory&lt;/strong>
&lt;ul>
&lt;li>low-level name (inode number)&lt;/li>
&lt;li>a list of &lt;code>(user-readable name, low-level name)&lt;/code> pairs&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="393-creating-files">39.3 Creating Files&lt;/h3>
&lt;p>Create a new file: call &lt;code>open()&lt;/code> and pass &lt;code>O_CREAT&lt;/code> flag:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> fd &lt;span style="color:#f92672">=&lt;/span> open(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>, O_CREAT&lt;span style="color:#f92672">|&lt;/span>O_WRONLY&lt;span style="color:#f92672">|&lt;/span>O_TRUNC, S_IRUSR&lt;span style="color:#f92672">|&lt;/span>S_IWUSR);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>O_CREAT&lt;/code>: create the file if it does not exist&lt;/li>
&lt;li>&lt;code>O_WRONLY&lt;/code>: read only&lt;/li>
&lt;li>&lt;code>O_TRUNC&lt;/code>: if exists already, truncates it to a size of zero bytes&lt;/li>
&lt;/ul>
&lt;h3 id="395-reading-and-writing-but-not-sequentially">39.5 Reading And Writing, But Not Sequentially&lt;/h3>
&lt;p>Reading some random offsets within the document:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>off_t &lt;span style="color:#a6e22e">lseek&lt;/span>(&lt;span style="color:#66d9ef">int&lt;/span> fildes, off_t offset, &lt;span style="color:#66d9ef">int&lt;/span> whence);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>fildes&lt;/code>: file descriptor&lt;/li>
&lt;li>&lt;code>offset&lt;/code>: the file offset to a particular location&lt;/li>
&lt;li>&lt;code>whence&lt;/code>
&lt;ul>
&lt;li>&lt;code>SEEK_SET&lt;/code>: the offset is set to offset bytes&lt;/li>
&lt;li>&lt;code>SEEK_CUR&lt;/code>: the offset is set to its current location plus offset bytes&lt;/li>
&lt;li>&lt;code>SEEK_END&lt;/code>: the offset is set to the size of the file plus offset bytes&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Calling &lt;code>lseek()&lt;/code> does not perform a disk seek&lt;/p>
&lt;/blockquote>
&lt;p>The &amp;ldquo;current offset&amp;rdquo; tracked by the OS:&lt;/p>
&lt;ul>
&lt;li>implicitly updates when read or write&lt;/li>
&lt;li>explicitly updates with &lt;code>lseek&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>The file structure:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> file {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> ref;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">char&lt;/span> readable;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">char&lt;/span> writable;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> inode &lt;span style="color:#f92672">*&lt;/span> ip;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uint off;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;strong>open file table&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> spinlock lock;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> file file[NFILE];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>} ftable;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="396-shared-file-table-entries-fork-and-dup">39.6 Shared File Table Entries: &lt;code>fork()&lt;/code> And &lt;code>dup()&lt;/code>&lt;/h3>
&lt;p>The open file table entries are shared by 1. the parent and child processes created by &lt;code>fork()&lt;/code>; 2. one file descriptor and its duplicates created by &lt;code>dup()&lt;/code>, with the reference count incremented.&lt;/p>
&lt;h3 id="397-writing-immediately-with-fsync">39.7 Writing Immediately With &lt;code>fsync()&lt;/code>&lt;/h3>
&lt;p>Calls &lt;code>fsync()&lt;/code> to force all dirty (not yet written) data to disk.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> rc &lt;span style="color:#f92672">=&lt;/span> write(fd, buffer, size); assert(rc &lt;span style="color:#f92672">==&lt;/span> size);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In some cases, it is also needed to &lt;code>fsync()&lt;/code> the containing directory to ensure that the file is part of the directory.&lt;/p>
&lt;blockquote>
&lt;p>Not surprisingly, this type of detail is often overlooked, leading to many application-level bugs.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>&lt;code>mmap()&lt;/code> and persistent memory&lt;/strong>&lt;/p>
&lt;p>By combining the persistence of files with the access semantics of memory, file-backed memory mappings support a software abstraction called persistent memory.&lt;/p>
&lt;/blockquote>
&lt;h3 id="398-renaming-files">39.8 Renaming Files&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>rename(&lt;span style="color:#66d9ef">char&lt;/span> &lt;span style="color:#f92672">*&lt;/span>old, &lt;span style="color:#66d9ef">char&lt;/span> &lt;span style="color:#f92672">*&lt;/span>new)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>rename()&lt;/code> call is usually implemented as an atomic call.&lt;/p>
&lt;p>An example to use this kind of guarantee:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">int&lt;/span> fd &lt;span style="color:#f92672">=&lt;/span> open(&lt;span style="color:#e6db74">&amp;#34;foo.txt.tmp&amp;#34;&lt;/span>, O_WRONLY&lt;span style="color:#f92672">|&lt;/span>O_CREAT&lt;span style="color:#f92672">|&lt;/span>O_TRUNC, S_IRUSR&lt;span style="color:#f92672">|&lt;/span>S_IWUSR);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>write(fd, buffer, size);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>fsync(fd);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>close(fd);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>rename(&lt;span style="color:#e6db74">&amp;#34;foo.txt.tmp&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;foo.txt&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="399-getting-information-about-files">39.9 Getting Information About Files&lt;/h3>
&lt;p>Use &lt;code>stat()&lt;/code> or &lt;code>fstat()&lt;/code> to access metadata of files or directories.&lt;/p>
&lt;h3 id="3910-removing-files">39.10 Removing Files&lt;/h3>
&lt;p>Use &lt;code>unlink()&lt;/code> to remove files.&lt;/p>
&lt;h3 id="3911-making-directories">39.11 Making Directories&lt;/h3>
&lt;p>Use &lt;code>mkdir()&lt;/code> to create a directory.&lt;/p>
&lt;p>An empty directory has two entries:&lt;/p>
&lt;ul>
&lt;li>refer to itself&lt;/li>
&lt;li>refer to its parent&lt;/li>
&lt;/ul>
&lt;h3 id="3912-reading-directories">39.12 Reading Directories&lt;/h3>
&lt;p>Use &lt;code>opendir()&lt;/code>, &lt;code>readdir()&lt;/code> and &lt;code>closedir()&lt;/code> to read contents of a directory.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>DIR &lt;span style="color:#f92672">*&lt;/span> dp &lt;span style="color:#f92672">=&lt;/span> opendir(&lt;span style="color:#e6db74">&amp;#34;.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>assert(dp &lt;span style="color:#f92672">!=&lt;/span> NULL);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> dirent &lt;span style="color:#f92672">*&lt;/span> d;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">while&lt;/span> ((d &lt;span style="color:#f92672">=&lt;/span> readdir(dp)) &lt;span style="color:#f92672">!=&lt;/span> NULL) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> printf(&lt;span style="color:#e6db74">&amp;#34;%lu %s&lt;/span>&lt;span style="color:#ae81ff">\n&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>, (&lt;span style="color:#66d9ef">unsigned&lt;/span> &lt;span style="color:#66d9ef">long&lt;/span>) d&lt;span style="color:#f92672">-&amp;gt;&lt;/span>d_ino, d&lt;span style="color:#f92672">-&amp;gt;&lt;/span>d_name);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>closedir(dp);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3913-deleting-directories">39.13 Deleting Directories&lt;/h3>
&lt;p>Call &lt;code>rmdir()&lt;/code> to delete a directory.&lt;/p>
&lt;h3 id="3914-hard-links">39.14 Hard Links&lt;/h3>
&lt;p>Call &lt;code>link()&lt;/code> to make an entry in the file system tree, simply creates another name in the directory and refers it to the &lt;strong>same inode number&lt;/strong>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ln file file2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>When creating a file, two things happened:&lt;/p>
&lt;ol>
&lt;li>making a structure (the inode) that tracks relevent information&lt;/li>
&lt;li>linking a human-readable name to that file, and put that link into a directory&lt;/li>
&lt;/ol>
&lt;/blockquote>
&lt;p>When the file system unlinks a file, it checks the reference count (link count) within the inode number, and only free the node and related data blocks when it reaches zero.&lt;/p>
&lt;h3 id="3915-symbolic-links">39.15 Symbolic Links&lt;/h3>
&lt;p>Call &lt;code>symlink()&lt;/code> to make an symbolic link (soft link) to a file.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ln -s file file2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Differences from hard links:&lt;/p>
&lt;ul>
&lt;li>symbolic links is actually a file itself of a different type&lt;/li>
&lt;li>possibility for dangling reference&lt;/li>
&lt;/ul>
&lt;h3 id="3916-permission-bits-and-access-control-lists">39.16 Permission Bits And Access Control Lists&lt;/h3>
&lt;p>&lt;strong>Permission bits&lt;/strong> consist of three groupings:&lt;/p>
&lt;ul>
&lt;li>owner&lt;/li>
&lt;li>group&lt;/li>
&lt;li>other (anyone)&lt;/li>
&lt;/ul>
&lt;p>The owner of the file can change the permissions.&lt;/p>
&lt;blockquote>
&lt;p>But now you have learned an even more important lesson: there can be no secrets in a good marriage, even within the file system.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>The &lt;em>Time Of Check To Time Of Use (TOCTTOU)&lt;/em> problem&lt;/strong>&lt;/p>
&lt;p>The problem is caused by the gap between the check and the update.&lt;/p>
&lt;/blockquote>
&lt;h3 id="3917-making-and-mounting-a-file-system">39.17 Making And Mounting A File System&lt;/h3>
&lt;blockquote>
&lt;p>And mkfs said, let there be a file system!&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Mount&lt;/strong>: to take an existing directory as a target &lt;strong>mount point&lt;/strong> and paste a new file system onto the directory tree at that point.&lt;/p>
&lt;blockquote>
&lt;p>Mount unfies all file systems into one tree, making naming uniform and convenient.&lt;/p>
&lt;/blockquote>
&lt;h2 id="40-file-system-implementation">40 File System Implementation&lt;/h2>
&lt;p>&lt;strong>vsfs&lt;/strong> (the &lt;strong>Very Simple File System&lt;/strong>)&lt;/p>
&lt;blockquote>
&lt;p>The file system is pure software; unlike our development of CPU and memory virtualization, we will not be adding hardware features to make some aspect of the file system work better (though we will want to pay attention to device characteristics to make sure the file system works well).&lt;/p>
&lt;/blockquote>
&lt;h3 id="402-overall-organization">40.2 Overall Organization&lt;/h3>
&lt;p>The disk is divided into:&lt;/p>
&lt;ul>
&lt;li>data blocks: user data&lt;/li>
&lt;li>inodes&lt;/li>
&lt;li>allocation structures: to track whether inodes or data blocks are free or allocated&lt;/li>
&lt;li>superblock: contains information about the fie system&lt;/li>
&lt;/ul>
&lt;h3 id="403-file-organization-the-inode">40.3 File Organization: The Inode&lt;/h3>
&lt;p>&lt;strong>Inode&lt;/strong> (&lt;strong>index node&lt;/strong>): the data structure that holds the metadata of a given file&lt;/p>
&lt;p>&lt;strong>i-number&lt;/strong> (&lt;strong>low-level name&lt;/strong>): the number that the inode is implicitly referred to; disk position can be calculated by the number&lt;/p>
&lt;p>Calculation:&lt;/p>
&lt;p>$$
block = (inumber\times sizeof(inode)) / blockSize
$$&lt;/p>
&lt;p>$$
sector = ((block\times blockSize) + inodeStartAddr) / sectorSize
$$&lt;/p>
&lt;p>Metadata in a inode:&lt;/p>
&lt;ul>
&lt;li>type: directory or file (kept in the directory entry and thus is not found in the inode)&lt;/li>
&lt;li>size&lt;/li>
&lt;li>number of blocks&lt;/li>
&lt;li>protection information&lt;/li>
&lt;li>time&lt;/li>
&lt;li>data block position on disk&lt;/li>
&lt;/ul>
&lt;h4 id="the-multi-level-index">The Multi-Level Index&lt;/h4>
&lt;p>&lt;strong>Indirect pointer&lt;/strong>: points to a block that contains pointers which each points to user data&lt;/p>
&lt;p>An inode may have some fixed number of direct pointers and a single indirect pointer.&lt;/p>
&lt;p>If a file grows large enough, an indirect block is allocated (from the data block) and the inode&amp;rsquo;s indirect pointer is set to point to it.&lt;/p>
&lt;p>&lt;strong>Extent-based approaches&lt;/strong>: a disk pointer plus a length; allow for more than one extent&lt;/p>
&lt;p>&lt;strong>Linked-based approaches&lt;/strong>: use a linked list to keep track all blocks of a file&lt;/p>
&lt;p>&lt;strong>Double indirect pointer&lt;/strong>: refer to a block that contains indirect pointers&lt;/p>
&lt;blockquote>
&lt;p>Most files are small: a small number of direct pointers is sufficient&lt;/p>
&lt;/blockquote>
&lt;h3 id="404-directory-organization">40.4 Directory Organization&lt;/h3>
&lt;p>Deleting a file can leave an empty space in the middle of the directory; a new entry may reuse an old, bigger, deleted entry.&lt;/p>
&lt;p>File systems often streat directories as a special type of file, and thus they have inodes in the inode table.&lt;/p>
&lt;h3 id="405-free-space-management">40.5 Free Space Management&lt;/h3>
&lt;p>To track free inode/data blocks:&lt;/p>
&lt;ul>
&lt;li>use bitmap&lt;/li>
&lt;li>use free list&lt;/li>
&lt;li>use more soplisticated ds, e.g. B-tree&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Pre-allocation&lt;/strong> policy: reserve a sequence of blocks for a newly-created file, to guarantee that a portion of the file is contiguous on the disk.&lt;/p>
&lt;h3 id="406-access-paths-reading-and-writing">40.6 Access Paths: Reading and Writing&lt;/h3>
&lt;h4 id="reading-a-file-from-disk">Reading A File From Disk&lt;/h4>
&lt;p>The timeline of reading a file &lt;code>/foo/bar&lt;/code>:&lt;/p>
&lt;p>&lt;strong>&lt;code>open(&amp;quot;/foo/bar&amp;quot;, O_RDONLY)&lt;/code>&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>read the root inode (&lt;code>/&lt;/code>) (whose i-number is well known; &lt;code>2&lt;/code> in most UNIX FSs)&lt;/li>
&lt;li>read &lt;code>/&lt;/code> data (to look for an entry for &lt;code>foo&lt;/code>)&lt;/li>
&lt;li>read &lt;code>foo&lt;/code> inode&lt;/li>
&lt;li>read &lt;code>foo&lt;/code> data (to look for an entry for &lt;code>bar&lt;/code>)&lt;/li>
&lt;li>read &lt;code>bar&lt;/code> inode&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>read(...)&lt;/code>&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>read &lt;code>bar&lt;/code> inode&lt;/li>
&lt;li>read specific block of &lt;code>bar&lt;/code>&amp;rsquo;s data&lt;/li>
&lt;li>write &lt;code>bar&lt;/code> inode for last access time&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Yes, life can get pretty bad when reading a file; as you’re about to find out, writing out a file (and especially, creating a new one) is even worse.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>Reads don&amp;rsquo;t access allocation structures (e.g. bitmaps); they are only accessed when allocation is needed. There is no need to make sure a block is allocated when the inode already points to it&lt;/p>
&lt;/blockquote>
&lt;h4 id="writing-a-file-to-disk">Writing A File To Disk&lt;/h4>
&lt;p>Each write to a file logically generates 5 I/Os:&lt;/p>
&lt;ul>
&lt;li>read file inode&lt;/li>
&lt;li>read data bitmap&lt;/li>
&lt;li>write data bitmap (to mark the newly-allocated block as used)&lt;/li>
&lt;li>write file data&lt;/li>
&lt;li>write file inode&lt;/li>
&lt;/ul>
&lt;p>For file creation:&lt;/p>
&lt;ul>
&lt;li>read inode and inode data traversly to find the containing directory&lt;/li>
&lt;li>read containing directory data&lt;/li>
&lt;li>read inode bitmap (find a free inode)&lt;/li>
&lt;li>write inode bitmap (to mark the inode allocated)&lt;/li>
&lt;li>write directory data (link)&lt;/li>
&lt;li>read/write directory inode&lt;/li>
&lt;li>read/write inode of the new file (initialize)&lt;/li>
&lt;li>additional I/O might be needed if the directory needs to grow&lt;/li>
&lt;/ul>
&lt;h3 id="407-caching-and-buffering">40.7 Caching and Buffering&lt;/h3>
&lt;blockquote>
&lt;p>Most file systems aggressively use system memory (DRAM) to cache important blocks.&lt;/p>
&lt;/blockquote>
&lt;p>Early file systems use a fixed-size cache to hold popular blocks; this &lt;strong>static partitioning&lt;/strong> method can be wasteful.&lt;/p>
&lt;p>Modern systems use a &lt;strong>dynamic partitioning&lt;/strong> approach.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Static vs. dynamic partitioning&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>static: predictable performance&lt;/li>
&lt;li>dynamic: better utilization; more complex to implement&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;p>&lt;strong>Buffering&lt;/strong>: delay writes to reduce I/O&lt;/p>
&lt;blockquote>
&lt;p>In this case, laziness (in writing blocks to disk) is a virtue.&lt;/p>
&lt;/blockquote>
&lt;h2 id="41-locality-and-the-fast-file-system">41 Locality and The Fast File System&lt;/h2>
&lt;h3 id="411-the-problem-poor-performance">41.1 The Problem: Poor Performance&lt;/h3>
&lt;p>Main issue of the old UNIX file system: data was spread all over the place, thus had expensive positioning costs&lt;/p>
&lt;p>Some problems:&lt;/p>
&lt;ul>
&lt;li>the inode and the data block are often far away&lt;/li>
&lt;li>the file system is getting more and more fragmented&lt;/li>
&lt;li>the block size is too small, so that transferring data is inefficient&lt;/li>
&lt;/ul>
&lt;h3 id="413-organizing-structure-the-cylinder-group">41.3 Organizing Structure: The Cylinder Group&lt;/h3>
&lt;p>&lt;strong>Cylinder&lt;/strong> (柱面): a set of tracks on different surfaces that are the same distance from the center&lt;/p>
&lt;p>FFS divides the disk into a number of cylinder groups.&lt;/p>
&lt;p>As modern drives only export a logical address space of blocks, modern file systems instead organize the drive into &lt;strong>block groups&lt;/strong>, each of which is &lt;strong>a consecutive portion of the disk&amp;rsquo;s address space&lt;/strong>. By placing two files within the same group, FFS can ensure that accessing one after the other will not result in long seek time.&lt;/p>
&lt;p>FFS needs to have the ability to place files and directories into a groups and track relevant information, thus all the structures that a file system is expected to have is included in &lt;strong>each group&lt;/strong>.&lt;/p>
&lt;p>The components of a single cylinder group:&lt;/p>
&lt;ul>
&lt;li>a copy of the super block&lt;/li>
&lt;li>a per-group inode bitmap and data bitmap&lt;/li>
&lt;li>inode region&lt;/li>
&lt;li>data block region&lt;/li>
&lt;/ul>
&lt;h3 id="414-policies-how-to-allocate-files-and-directories">41.4 Policies: How To Allocate Files and Directories&lt;/h3>
&lt;blockquote>
&lt;p>The basic mantra is simple: keep related stuff together (and its corollary, keep unrelated stuff far apart).&lt;/p>
&lt;/blockquote>
&lt;p>A few placement heuristics:&lt;/p>
&lt;ul>
&lt;li>directories: find the group
&lt;ul>
&lt;li>with a low number of allocated directories (to balance directories across groups)&lt;/li>
&lt;li>with a high number of free inodes (to be able to allocate a bunch of files)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>files:
&lt;ul>
&lt;li>allocate the data blocks in the same group as its inode&lt;/li>
&lt;li>places all files that are in the same directory in the cylinder group of the directory they are in&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h3 id="416-the-large-file-exception">41.6 The Large-File Exception&lt;/h3>
&lt;p>Filling an entire block group with large files will prevents related files from being placed within the same group.&lt;/p>
&lt;p>FFS&amp;rsquo; policy for large files: after some number of blocks are allocated into the first block group, it places the next large chunk of the file in another block group.&lt;/p>
&lt;p>To address the performance problem: choosing chunk size carefully. By &lt;strong>amotization&lt;/strong>, if the chunk size is large enough, the seek time will be relatively little.&lt;/p>
&lt;p>&lt;strong>FFS&amp;rsquo;s approach&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>the first 12 blocks are placed in the same group as the inode&lt;/li>
&lt;li>each subsquent indirect block and the blocks it points to are placed in a different group&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Note that the trend in disk drives is that transfer rate improves fairly rapidly, as disk manufacturers are good at cramming more bits into the same surface, but the mechanical aspects of drives related to seeks (disk arm speed and the rate of rotation) improve rather slowly.&lt;/p>
&lt;/blockquote>
&lt;h3 id="417-a-few-other-things-about-ffs">41.7 A Few Other Things About FFS&lt;/h3>
&lt;p>&lt;strong>Internal fragmentation&lt;/strong>: 4KB blocks are used by many 2KB files&lt;/p>
&lt;p>&lt;strong>Sub-blocks&lt;/strong>: 512-byte blocks can be allocated to files; copy to a 4KB block when the size grows beyond 4KB&lt;/p>
&lt;p>To address the performance issue caused by copies: buffer writes, to avoid this in most cases.&lt;/p>
&lt;p>A refined disk layout (&lt;strong>parameterization&lt;/strong>): skipping a block when issuing a sequential read (e.g. 0 6 1 7 2 8 3 9 4 10 5 11); to issue the performance problem: using track buffer.&lt;/p>
&lt;p>FFS also supports:&lt;/p>
&lt;ul>
&lt;li>long file names&lt;/li>
&lt;li>symbolic links&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Certainly all modern systems account for the main lesson of FFS: treat the disk like it’s a disk.&lt;/p>
&lt;/blockquote></content></item><item><title>2021 秋季学期总结</title><link>https://yeshe.ng/blog/posts/2022/01/2021-fall-summary/</link><pubDate>Sat, 08 Jan 2022 01:05:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2022/01/2021-fall-summary/</guid><description>2021 秋季学期总结</description><content>&lt;h2 id="关于各课程">关于各课程&lt;/h2>
&lt;p>这个学期总共 17 学分，除了体育和信概，上了五门课：汇编、自动机、数据结构、网原、概统。&lt;/p>
&lt;p>汇编的收获挺大。老师其实讲得很一般，但课程内容很有意思，计算机系统的知识对于之前只接触过上层软件的我来说是一个全新的世界。计算机系统各个部分、各个层次的运转和协调非常精妙，下学期应该有机会继续深入研究。&lt;/p>
&lt;p>自动机属于个人感受先抑后扬的课程。前两节课上来就讲上下文无关文法无异于当头一棒，这部分内容过于抽象，而且老师并没有作很好的解释，体验极其不好。后来随着正规语言、DFA 这些更符合直觉的内容的引入，终于慢慢弄清楚了研究这些各种各样的语言有什么用，它们分别对应程序设计中的什么问题，也逐渐适应了其中的逻辑。老师非常温柔，也非常认真负责，但是讲课实在是有点无聊。&lt;/p>
&lt;p>邓公的数据结构可以说是清华课程的天花板，上课时都是抱着“上一节少一节”的虔诚心态来享受的。PPT 的组织、老师的讲解、课堂的演示几乎都达到了完美，每堂课听完都有酣畅淋漓的感觉。当然，课讲得好，不代表学生上完课水平就高。上课感觉啥都会了，开始做 PA 还是一脸懵逼，最后的考试更是直接把“你并没有学懂”写在了试卷上。虽然但是，数据结构的确是一门深刻改变编程思想的课程，经过一个学期的学习，受益匪浅。&lt;/p>
&lt;p>网原的学习体验并不好。我对网络还算挺感兴趣，计算机网络本身也很有意思，但是这课恨不得把每一层历经几十年发展积累的无论是过时的还是先进的技术都一股脑塞给你，知识过于繁杂，导致上课根本没有一个清晰的脉络。而且，自底向上的方法让人在前几章学数据通信、物理层和链路层时很难理解这些东西有什么用，这些协议这样设计的意义是什么。不过最后考试出的题目还挺有意思，这个出乎意料。&lt;/p>
&lt;p>概统选的是胡家信老师的课，全程很水，特别是和之前上的唐宏岩的课相比，知识的广度和深度都差太多，统计竟然只讲到参数估计，连假设检验都没讲。老师上课讲得很详细，但是统计部分基本念 PPT，也没有什么原理性、启发性的解释。考试挺难的，有几道题不会做。&lt;/p>
&lt;h2 id="关于项目">关于项目&lt;/h2>
&lt;p>这个学期做的项目最主要是&lt;a href="https://github.com/liang2kl/mkdocs-blogging-plugin">一个静态网站生成器的博客插件&lt;/a>，给 Mkdocs 增加了博客功能。维护用了不少时间，陆续加了不少功能，用的人也逐渐多了起来，感受还是挺不错的。不过用插件很难构建一个功能完善的博客，最后还是转向了 Hugo 来建博客。&lt;/p>
&lt;p>另外，还写了一点 SwiftUI 教程，主要想对 SwiftUI 一些难懂的概念、原理作一下介绍，不过写了一半就停下来了（的确太花时间），写的效果也没有达到预期，可能寒假会继续写下去。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>前几天和 lqw 吃饭的时候聊到，感觉这个学期过得比以往任何一个学期都要快。前几个学期要么有繁忙的社工，要么在玩命写项目，要么在紧张准备转系，相比之下这个学期显得过于平淡了。&lt;/p>
&lt;p>这个调子其实在开学前就定下了。我的 GPA 在机械还能勉强排到中上游，而在各路大神齐聚的贵系，这个分数对应的排名就变得非常难看，压力变得空前巨大。于是，暑假的时候我就先后退掉了宣中的社工、拒绝了一个创业团队的邀请。我给出的理由都是：成绩不好，之后只能专心学业。&lt;/p>
&lt;p>结果就是，大多数日子里，我基本上都在宿舍-教室-食堂-图书馆的循环中度过，有时候想做一些好玩的东西，也的确写了几个玩具项目，但最终还是一次次地告诫自己：不要让这些事情干扰到学习。&lt;/p>
&lt;p>现在回想，专心学习的确是有好处的。我在大一第一个学期之后第一次做到线下上课全勤，上课基本上都认真地听讲并做了笔记，每天课后也对内容做了回顾。这样坚持下来的最明显的好处是：我能够很清晰地掌握每个课程的脉络，清楚地知道自己在这些课程中学到了什么东西、这些课程的精髓在什么地方。之前的学习过于匆忙，没有回顾、思考的时间，囫囵吞枣，因此几乎没有对哪一门课有“学懂”了的感觉。这个学期，投入了更多的时间进行复习、思考，对知识的理解就有了深度。&lt;/p>
&lt;p>但是，毕竟学习只是生活的一部分，阅读、游戏、游玩、写程序等等都不应该因此而荒废。以后应当提高学习效率，减少浪费在漫无目的的刷手机上的时间，把更多的时间放在这些有意思的事情上面。&lt;/p>
&lt;p>总的来说，这个学期算是挺有收获。在贵系的第一个学期，（应该？但愿？）没有掉队，而且学习的内容都挺感兴趣，虽然学习的过程并不一直都那么顺利。下学期学习任务更重，希望到时候能够既保持课内学习的状态，同时在科研和其他方面有一定的发展吧。&lt;/p>
&lt;h2 id="更新">更新&lt;/h2>
&lt;p>今天（1 月 21 日）不经意间看到机械班群里面赵导发的群公告，才知道出成绩了，来这里更新一下。&lt;/p>
&lt;p>不得不说我的学习感觉和最终的成绩还是比较吻合的，几门比较用功并且学得比较透彻的专业课（汇编、自动机、网原）都得到了 4.0；花了比较多时间但并没有学得很透彻的数据结构，以及没怎么花时间的概统得了 3.6。&lt;/p>
&lt;p>其实考试过程中和考完试之后一直挺担心一件事，就是我自认为学得很透彻的几门课可能由于我考试时经常犯的计算错误、审题错误、脑子短路等问题而不能够取得一个比较好的成绩；考完汇编和概统之后我悲观地认为这个学期，甚至以后的学期一个 4.0 都拿不到了。事实证明我还是多虑了，大多数的考试都是所学内容的子集，如果已经学得比较透彻，那已经超过（大部分的）考试要求了，也基本上不会出现上面这些所谓的问题。虽然这是一个高中就已经非常清楚的常识，但说实话，在没有什么自信的情况下（特别是在贵系中）是很难相信这一点，也很难相信“有付出就有回报”的。&lt;/p>
&lt;p>这个学期的学习算是达到了预期，下学期继续努力吧。&lt;/p></content></item><item><title>2021 年终总结</title><link>https://yeshe.ng/blog/posts/2021/12/2021-summary/</link><pubDate>Fri, 31 Dec 2021 20:46:58 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/12/2021-summary/</guid><description>2021 年终总结</description><content>&lt;p>终于到了 12 月 31 日，可以赶在 ddl 前的最后的三个小时里总结一下过去的一年了。&lt;/p>
&lt;h2 id="转系">转系&lt;/h2>
&lt;p>今年最重要的事情，是从机械系降转到了计算机系。&lt;/p>
&lt;p>转系的想法是在前一年转系结果出来的那段时间萌发的。当时从来没有编程经验的我经过一个学期的学习、开发后，在 App Store 上架了第一个 app。在这个过程中，我惊讶地发现，我短暂地到达了刚上高中时思考人生时所设想的那种理想中的状态：每时每刻都全身心投入在自己所感兴趣的事情上。&lt;/p>
&lt;p>我的兴趣是什么？大学以来，我一直以一种回避的态度来面对这个问题。我之前的人生像是在应试教育的框条下的一条预设好的“最优道路”：上最好的小学，成绩拔尖；上最好的初中，成绩拔尖；上最好的高中，成绩拔尖。因此，当由于高考发挥不佳，只能通过领军进入清华而在有限的专业里面选择机械时，我也就理所应当地将其视为这条“预设”道路的一部分，是“最好的安排”。我一直回避与专业选择相关的问题，总是给自己灌输“继续学下去，就一定能找到兴趣”的观念。因为高中的时候对做生物实验挺感兴趣，又了解到机械系有生物 3D 打印的方向，于是在分流的时候，我还很笃定地（现在看来是一种自我安慰）认为我感兴趣的方向就是生物 3D 打印，并在分流小作文里表达了自己对“机械和生物结合”浓厚的兴趣。&lt;/p>
&lt;p>这种想法在我接触编程后发生了改变。对于我来说，那是一种与之前的学习完全不同的感受：我愿意为与课内学习无关的知识付出时间，而不是之前“考试不考就没有动力学”；我不会因为越来越艰深的知识而感到枯燥、无聊，而是学得越多，越对更深入的知识产生更多的兴趣。原来这才是真正的兴趣，这才是我真正想做的事情。&lt;/p>
&lt;p>在当年的转系结果出来以后，看到周围不少同学都找到了自己所认为的合适的专业，我第一次认真考虑了转系这一选项。我找到培养方案：一边是理论力学、材料力学、工程材料这些我看课程名就有点抗拒的课程，另一边是汇编、编译原理、网络原理这些在我之前写程序的过程中一直想要深入探究的、听上去就很有意思的东西。在这番比较之后，答案就很显然了：我要转系。&lt;/p>
&lt;p>刚决定要转系的几天，也许是自己转系成功之前最紧张的几天。这种紧张并不是害怕转系失败，更多是一种站在十字路口上做选择的恐惧和迷茫。我从来没有自己做出对自己人生如此重大的决定，要是错了，怎么办？&lt;/p>
&lt;p>在经过了做出决定之后的最初的挣扎之后，接下来的事情就非常自然了。我选了计算机的专业课，报了一个软院的 SRT，一直在写各种各样的程序，这些经历也成为了我面试时个人志趣的证明。当然，过了一年之后，当转系真正开始时，我还是有点紧张的，毕竟我的排名并不高，虽然已经坚定了学习计算机的信念，但要是不能通过，之后还是有点麻烦的。&lt;/p>
&lt;p>非常幸运，最终转系成功。我第一次改变了自己的人生轨迹。&lt;/p>
&lt;h2 id="树洞">树洞&lt;/h2>
&lt;p>占据 2021 年故事的很大篇幅的另一个事情，是写树洞 app。寒假刚开始时，树洞在招募客户端开发者，我稍微犹豫了一会后加入了。犹豫是因为担心自己的能力不足以支持开发这样一个比较大的项目，但最终还是发邮件报了名。&lt;/p>
&lt;p>在大家对新功能作一番论证、UI 设计师把设计稿画出来之后，就开始了漫长的开发过程。进行 iOS 客户端开发的有我和另外一个北大的同学，但只有我有过 iOS 开发经验，所以我承担了大部分的开发工作。开发过程中学到了不少东西：git 的一些高级用法、HTTP 请求、iOS 开发和 SwiftUI 的很多未接触过的知识……当然还走了不少弯路，要么是由于经验不足，要么是由于 SwiftUI 不成熟导致的问题。&lt;/p>
&lt;p>写树洞 app 的投入程度比我之前写任何程序，包括 Whiz Reader，都要疯狂。除了正经坐在桌子前，在广州实践时住的酒店里、在高铁上、在春节回老家过年时、&lt;del>上 OOP 时&lt;/del>，都留下了一条条 commit 记录，只要打开 Xcode，就感受不到任何的困倦。由于高强度的开发，虽然 iOS 客户端开发人手远少于安卓，但我们比安卓早了将近两个月完成开发。&lt;/p>
&lt;p>这最终成为了我接触编程之后提升最大的一次项目经历。&lt;/p>
&lt;h2 id="告别">告别&lt;/h2>
&lt;p>今年有两次告别让我猝不及防：一是梅西离开巴萨，一是树洞关闭。&lt;/p>
&lt;p>这是我的两条朋友圈：&lt;/p>
&lt;blockquote>
&lt;p>十几年前，看一个大哥哥打实况，我说这个人怎么跑得这么慢，他笑了笑，“他可是梅西，速度最快的”&lt;/p>
&lt;p>接下来，在 CCTV5 的体育新闻里记下了，那个叫梅西的人是在一个叫巴塞罗那的俱乐部，队服是红蓝色的&lt;/p>
&lt;p>2012 年，开始关注巴萨的比赛，那年欧冠半决赛输给了切尔西，梅西射丢了点球&lt;/p>
&lt;p>2015 年，穿着梅西球衣看了欧冠决赛，那年 MSN 大杀四方，所向披靡&lt;/p>
&lt;p>2017 年，6-1 逆转巴黎的欧冠看一次哭一次，至今还记得那天在教室电脑看到比分时的疯狂&lt;/p>
&lt;p>2019 年，高考，几乎每场欧冠淘汰赛都看了，没看 3-0 利物浦，却看了 0-4 的那一场&lt;/p>
&lt;p>2020 年，梅西要走，焦虑了大半个月，最后庆幸梅西还在巴萨&lt;/p>
&lt;p>上个赛季，几乎每场球都看了，两点，三点，四点……梅西总能让伤身的熬夜显得那么值得&lt;/p>
&lt;p>梅西在巴萨的最后一场比赛，看了直播，梅西进了一个头球，最后被逆转&lt;/p>
&lt;p>没想到已成绝唱&lt;/p>
&lt;p>青春真的结束了&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>……&lt;/p>
&lt;p>为什么我们，包括其他开发者、设计人员和运营者都愿意牺牲自己的时间和精力投入在其中？&lt;/p>
&lt;p>因为它对你我，对每个人都有独特的意义。这份意义，可以是更多的信息来源，可以是洞友们的善意带来的温暖，也可以是缓解压力的那些有趣的树洞。虽然树洞是一个存在很多争议和负面情绪的地方，但并不妨碍它存在于每个清华学生的美好回忆中。&lt;/p>
&lt;p>今天下午花了几十分钟爬取树洞，爬着爬着到了每天8000个树洞的上限，只好作罢，不禁悲从中来，也许我们真的要跟树洞说再见了。&lt;/p>
&lt;/blockquote>
&lt;p>我之前并未经历多少离别，这两件事情对我来说是不小的打击。我逐渐意识到，再美好的事物都有逝去的一天。当它们真正离开的时候，我们几乎什么都做不了。&lt;/p>
&lt;p>但是，美好事物的意义，不正在于它存在时在我们生活中留下的痕迹吗？&lt;/p>
&lt;p>“Don&amp;rsquo;t be sad because it&amp;rsquo;s over. Smile because it happened.”&lt;/p>
&lt;p>Goodbye, 2021. &lt;strong>Hello, 2022.&lt;/strong>&lt;/p></content></item><item><title>Jinja 继承、函数和块</title><link>https://yeshe.ng/blog/posts/2021/09/jinja/</link><pubDate>Fri, 24 Sep 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/09/jinja/</guid><description>对 HTML 模版引擎 Jinja 的一些探索</description><content>&lt;p>最近在为 &lt;a href="https://github.com/liang2kl/mkdocs-blogging-plugin">mkdocs-blogging-plugin&lt;/a> 添加自定义的功能，因为本来就是用 Jinja 的 HTML 模版来生成 HTML，所以也想用 Jinja 给用户提供自定义的接口。&lt;/p>
&lt;p>大概的要求有：&lt;/p>
&lt;ul>
&lt;li>提供自定义单个 post 而保留大体框架的方法&lt;/li>
&lt;li>提供自定义 style sheet 的方法&lt;/li>
&lt;li>提供整体重写的方法&lt;/li>
&lt;/ul>
&lt;p>这篇文章只会覆盖到部分。&lt;/p>
&lt;h2 id="继承extends">继承：&lt;code>extends&lt;/code>&lt;/h2>
&lt;p>Jinja 给我们提供了一种类似于继承的方法，可以在保留原来模版所有内容的情况下增加、重写部分内容的方法。&lt;/p>
&lt;p>在“子模版”中，通过 &lt;code>extends&lt;/code> 引用“父模版”：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#e6db74">&amp;#34;blog.html&amp;#34;&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果只有这一句，相当于将原来 &lt;code>blog.html&lt;/code> 中所有内容原封不动地搬到新的模版中。至于增加和重写内容，稍后再谈。&lt;/p>
&lt;h2 id="函数macro">函数：&lt;code>macro&lt;/code>&lt;/h2>
&lt;p>在用户提供的覆写模版中，我们需要用户向父模版提供一个“函数”，用来生成单个博客的内容。&lt;/p>
&lt;p>Jinja 中的“函数”叫做 &lt;code>macro&lt;/code>，通过这样的方法定义：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">macro&lt;/span> render_blog&lt;span style="color:#f92672">(&lt;/span>title&lt;span style="color:#f92672">,&lt;/span> description&lt;span style="color:#f92672">,&lt;/span> time&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">)&lt;/span> -&lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{# content #}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span>- &lt;span style="color:#66d9ef">endmacro&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>以及，这样调用：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">call&lt;/span> render_blog&lt;span style="color:#f92672">(&lt;/span>title&lt;span style="color:#f92672">,&lt;/span> description&lt;span style="color:#f92672">,&lt;/span> time&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{# caller_content #}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endcall&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 &lt;code>call&lt;/code> 调用 &lt;code>macro&lt;/code> 后，将整个 &lt;code>call&lt;/code> 块 替换为 &lt;code>macro&lt;/code> 块里面的所有内容 &lt;code>{# content #}&lt;/code>，与 C 中的宏定义一致（这应该也是为什么叫做 &lt;code>macro&lt;/code> 的原因）。&lt;/p>
&lt;p>另外，&lt;code>macro&lt;/code> 有一个隐含参数 &lt;code>caller&lt;/code>，其作用是返回 &lt;code>call&lt;/code> 块中间的内容 &lt;code>{# caller_content #}&lt;/code>，如：&lt;/p>
&lt;p>&lt;code>macro&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">macro&lt;/span> foo&lt;span style="color:#f92672">(&lt;/span>name&lt;span style="color:#f92672">)&lt;/span> -&lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> name &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#e6db74">&amp;#34; says: &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> caller&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span>- &lt;span style="color:#66d9ef">endmacro&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>call&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">call&lt;/span> foo&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;liang2kl&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Hello, World!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endcall&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>call&lt;/code> 的结果为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">div&lt;/span>&amp;gt;liang2kl says: Hello, World!&amp;lt;/&lt;span style="color:#f92672">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>值得注意的是，如果不想传递 &lt;code>caller&lt;/code> 中的内容，也需要在 &lt;code>macro&lt;/code> 中调用 &lt;code>caller()&lt;/code>，否则会报错：&lt;/p>
&lt;p>TypeError: macro &amp;lsquo;foo&amp;rsquo; was invoked with two values for the special caller argument. This is most likely a bug.&lt;/p>
&lt;/blockquote>
&lt;p>现在，我们只需要用户提供一个产生博客内容的 macro 给我们的父模版就可以了。&lt;/p>
&lt;h2 id="重写block">重写：&lt;code>block&lt;/code>&lt;/h2>
&lt;p>Jinja 的继承机制允许我们在“子模版”中选择性地重写部分内容，而保持其他部分不变。实现的机制是 &lt;code>block&lt;/code>。&lt;/p>
&lt;p>在父模版中，我们用 &lt;code>block&lt;/code> 块划出允许重写的部分。如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;style&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/style&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在子模版中，重写：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>即可将父模版中的 &lt;code>style&lt;/code> 块替换为新的块。&lt;/p>
&lt;p>另外，可以用 &lt;code>super()&lt;/code> 保留原来内容：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{{&lt;/span> super&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="放在一起">放在一起&lt;/h2>
&lt;p>将上面几个功能放在一起，我们给用户提供这样的自定义方式：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">macro&lt;/span> render_blog&lt;span style="color:#f92672">(&lt;/span>title&lt;span style="color:#f92672">,&lt;/span> description&lt;span style="color:#f92672">,&lt;/span> time&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">)&lt;/span> -&lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;&lt;span style="color:#75715e">{{&lt;/span> url &lt;span style="color:#75715e">}}&lt;/span>&amp;#34;&amp;gt;&amp;lt;h3&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> title &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/h3&amp;gt;&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> description &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;&lt;span style="color:#75715e">{{&lt;/span> time &lt;span style="color:#75715e">}}&lt;/span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{{&lt;/span> caller&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span>- &lt;span style="color:#66d9ef">endmacro&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#e6db74">&amp;#34;blog.html&amp;#34;&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">block&lt;/span> style &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .md-typeset .blog-post-title {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> color: blue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endblock&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其作用：&lt;/p>
&lt;ul>
&lt;li>在引入 &lt;code>blog.html&lt;/code> 之前，定义 &lt;code>render_blog&lt;/code> 的 macro，以便在 &lt;code>blog.html&lt;/code> 中调用&lt;/li>
&lt;li>引入 &lt;code>blog.html&lt;/code>&lt;/li>
&lt;li>通过 &lt;code>block&lt;/code> 修改 &lt;code>blog.html&lt;/code> 的 &lt;code>style&lt;/code> 块&lt;/li>
&lt;/ul></content></item><item><title>做一个 MkDocs 博客插件</title><link>https://yeshe.ng/blog/posts/2021/09/making-mkdocs-plugin/</link><pubDate>Thu, 09 Sep 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/09/making-mkdocs-plugin/</guid><description>之前使用 MkDocs 构建博客，但是 MkDocs 本身并不是为博客设计的，于是着手写一个插件。（当然最后还是放弃了 Mkdocs（划掉））</description><content>&lt;p>&lt;a href="https://github.com/mkdocs/mkdocs">MkDocs&lt;/a> 是个好东西，非常适合用来写一些 tech 文章；而 MkDocs 的 &lt;a href="https://github.com/squidfunk/mkdocs-material">Material&lt;/a> 主题更是 geek 风格十足。第一次看到 Material for MkDocs，就一见钟情，马上搭了个博客网站。&lt;/p>
&lt;p>但是，仔细一看 Material for MkDocs 的介绍，感觉不太对劲：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Technical documentation that just works&lt;/strong>&lt;/p>
&lt;p>Create a branded static site from a set of Markdown files to host the documentation of
your Open Source or commercial project&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;p>MkDocs 本来就只是为了搭建项目文档网站设计的，很多功能和设计（如底部导航栏、侧边栏等）更适合文档，而非博客。这导致，MkDocs 并没有将所有文章列在一个地方的功能。没有博客界面，怎么能叫做一个博客呢……&lt;/p>
&lt;p>但是，实在舍不得这么好看的界面，就琢磨着自己魔改一番。正好小学期学了 Python，可以用来练练手。&lt;/p>
&lt;blockquote>
&lt;p>最终成果开源于 &lt;a href="https://github.com/liang2kl/mkdocs-blogging-plugin">mkdocs-blogging-plugin&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;h2 id="mkdocs-的插件是怎么工作的">MkDocs 的插件是怎么工作的&lt;/h2>
&lt;p>仔细读了读&lt;a href="https://www.mkdocs.org/dev-guide/plugins/">开发者文档&lt;/a>，MkDocs 插件的工作是在 MkDocs 处理文档的整个生命周期的不同阶段&lt;strong>拦截当前结果，并返回处理后的结果&lt;/strong>。比如，对生成的 html 进行修改，是在插件类的 &lt;code>on_page_content&lt;/code> 函数中实现的：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>on_page_content&lt;/strong>&lt;/p>
&lt;p>The &lt;code>page_content&lt;/code> event is called after the Markdown text is rendered to HTML (but before being passed to a template) and can be used to alter the HTML body of the page.&lt;/p>
&lt;p>Parameters:&lt;/p>
&lt;ul>
&lt;li>html: HTML rendered from Markdown source as string&lt;/li>
&lt;li>page: &lt;code>mkdocs.nav.Page&lt;/code> instance&lt;/li>
&lt;li>config: global configuration object&lt;/li>
&lt;li>files: global files collection&lt;/li>
&lt;/ul>
&lt;p>Returns:
HTML rendered from Markdown source as string&lt;/p>
&lt;/blockquote>
&lt;p>我们从函数参数中读取当前处理中的 html 字符串，在对其进行处理后，返回最终的 html 字符串，就完成了插件对页面内容的修改。&lt;/p>
&lt;p>实现这些函数，我们就可以读取需要的信息，并更改某些页面内容。&lt;/p>
&lt;h2 id="设计与实现">设计与实现&lt;/h2>
&lt;p>首先，考虑把博客内容加入什么地方。考虑到用户可能需要在博客列表的前后加上其他的信息，直接生成一个页面不太可行。参考 &lt;a href="https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/tree/master/mkdocs_git_revision_date_localized_plugin">mkdocs-git-revision-date-localized-plugin&lt;/a> 的做法，让用户在需要添加博客内容的地方加上一个指定的占位符 &lt;code>{{ blog_content }}&lt;/code>，然后在处理时用正则表达式替换即可。&lt;/p>
&lt;p>然后，考虑博客的排序。我们选择使用 git log 来获取博文的创建和更新时间，然后向用户提供配置的选项（从新到旧 + 根据创建时间 / 从旧到新 + 根据修改时间 等）。这部分代码可以直接抄 &lt;a href="https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/tree/master/mkdocs_git_revision_date_localized_plugin">mkdocs-git-revision-date-localized-plugin&lt;/a> 的实现，工作量&amp;ndash;。&lt;/p>
&lt;h3 id="插件类与用户配置">插件类与用户配置&lt;/h3>
&lt;p>根据文档，插件类需要继承 &lt;code>BasePlugin&lt;/code>。在 &lt;code>BasePlugin&lt;/code> 中，&lt;code>config_scheme&lt;/code> 为插件的可配置选项。比如，我们需要用户设置一个名叫 &lt;code>foo&lt;/code> 的参数，其类型为 &lt;code>str&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BloggingPlugin&lt;/span>(BasePlugin):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> config_scheme &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>, config_options&lt;span style="color:#f92672">.&lt;/span>Type(str, default&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">None&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，用户在 &lt;code>mkdocs.yml&lt;/code> 中，可以这样配置 &lt;code>foo&lt;/code>（假设我们的插件名为 &lt;code>blogging&lt;/code>）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">plugins&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">blogging&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">foo&lt;/span>: &lt;span style="color:#ae81ff">bar&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="获取博客文件">获取博客文件&lt;/h3>
&lt;p>我们需要给予用户自行设置哪些文件夹、哪些文件的途径。在 &lt;code>config_scheme&lt;/code> 中，加上一项 &lt;code>dirs&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>config_scheme &lt;span style="color:#f92672">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#e6db74">&amp;#34;dirs&amp;#34;&lt;/span>, config_options&lt;span style="color:#f92672">.&lt;/span>Type(list, default&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#66d9ef">None&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，在 &lt;code>on_page_content&lt;/code> 函数中，如果 &lt;code>page&lt;/code> 是在用户设置的 &lt;code>dirs&lt;/code> 里面，就将其储存下来：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_post_page&lt;/span>(self, output, page, config):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> dir &lt;span style="color:#f92672">in&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>docs_dirs:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>file&lt;span style="color:#f92672">.&lt;/span>src_path[:len(dir)] &lt;span style="color:#f92672">==&lt;/span> dir:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="color:#f92672">.&lt;/span>blog_pages&lt;span style="color:#f92672">.&lt;/span>append(page)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>另外，有些文章在 &lt;code>dirs&lt;/code> 中，但可能并不希望被包括进博客中（如一些 index page）。对于这些文件，我们要求用户在 markdown 的 meta 中加入一项 &lt;code>exclude_from_blog&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>exclude_from_blog: true
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然后，修改一下上面的判断条件：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>file&lt;span style="color:#f92672">.&lt;/span>src_path[:len(dir)] &lt;span style="color:#f92672">==&lt;/span> dir \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">and&lt;/span> (&lt;span style="color:#f92672">not&lt;/span> &lt;span style="color:#e6db74">&amp;#34;exclude_from_blog&amp;#34;&lt;/span> &lt;span style="color:#f92672">in&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>meta \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">or&lt;/span> &lt;span style="color:#f92672">not&lt;/span> page&lt;span style="color:#f92672">.&lt;/span>meta[&lt;span style="color:#e6db74">&amp;#34;exclude_from_blog&amp;#34;&lt;/span>]):
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>就可以实现页面的筛选。&lt;/p>
&lt;h3 id="页面生成">页面生成&lt;/h3>
&lt;p>我们需要生成博客的列表界面，最简单的办法就是用 html 模版引擎来为我们动态地生成静态页面，这里使用 MkDocs 使用的 &lt;code>jinja2&lt;/code> 来处理。&lt;/p>
&lt;p>生成博客列表不是难事，写一个 for 循环就可以了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> page &lt;span style="color:#66d9ef">in&lt;/span> pages &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;...&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endfor&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>比较棘手的是分页的处理。根据我们的设计，我们最终在&lt;strong>一个页面&lt;/strong>中插入博客内容，但是分页一般是&lt;strong>多个页面&lt;/strong>。&lt;/p>
&lt;p>最终采取了将所有博客信息塞入页面、使用 css 现实当前页面博客信息 / 隐藏其他页面的方法：&lt;/p>
&lt;p>首先，在 pagination 中，将地址设置为 &lt;code>#page{{当前页面}}&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#f92672">a&lt;/span> &lt;span style="color:#a6e22e">href&lt;/span>&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;#page{{ page_idx + 1 }}&amp;#34;&lt;/span>&amp;gt;{{ page_idx + 1 }}&amp;lt;/&lt;span style="color:#f92672">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 css 中，id 与 &lt;code>#&lt;/code> 后面字符相同的可以用 &lt;code>:target&lt;/code> 选择器选中。比如，当前页面是 &lt;code>/blog/#page1&lt;/code>，那么 &lt;code>:target&lt;/code> 选中的就是 &lt;code>id = &amp;quot;page1&amp;quot;&lt;/code> 的元素。我们只需要将 &lt;code>:target&lt;/code> 设为显示，其他设为隐藏就可以了。&lt;/p>
&lt;p>在 html 中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-jinja" data-lang="jinja">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&amp;#34;pages&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">for&lt;/span> page_idx &lt;span style="color:#66d9ef">in&lt;/span> range&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">0&lt;/span>&lt;span style="color:#f92672">,&lt;/span> page_num&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;page&amp;#34; id=&amp;#34;page&lt;span style="color:#75715e">{{&lt;/span> page_idx &lt;span style="color:#f92672">+&lt;/span> &lt;span style="color:#ae81ff">1&lt;/span> &lt;span style="color:#75715e">}}&lt;/span>&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;!-- 第 page_index + 1 页的内容 --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">{%&lt;/span> &lt;span style="color:#66d9ef">endfor&lt;/span> &lt;span style="color:#75715e">%}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在 css 中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">/* 将所有子元素隐藏 */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#a6e22e">pages&lt;/span> &lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#f92672">*&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">display&lt;/span>: &lt;span style="color:#66d9ef">none&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">/* 显示 :target */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#a6e22e">pages&lt;/span> &lt;span style="color:#f92672">&amp;gt;&lt;/span> :&lt;span style="color:#a6e22e">target&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">display&lt;/span>: &lt;span style="color:#66d9ef">block&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>由于 css 处于后面的声明会覆盖前面的声明，&lt;code>:target&lt;/code> 可以显示，而其他页面的元素将被隐藏。这样就实现了分页。&lt;/p>
&lt;p>另外，我们还需要 JavaScript 来处理 pagination 的高亮，在这里就不赘述了。&lt;/p>
&lt;blockquote>
&lt;p>这种解决方法其实有个问题：当我们点击链接跳转到 &lt;code>#pageX&lt;/code> 的时候，浏览器会自动跳转到 &lt;code>id = pageX&lt;/code> 的元素处，非常影响体验。如今已经抛弃这种方法，改为在 JavaScript 中利用点击事件获取页面、更改 &lt;code>page&lt;/code> 的 class 来实现页面的显示和隐藏。&lt;/p>
&lt;/blockquote>
&lt;h3 id="页面插入">页面插入&lt;/h3>
&lt;p>在插件的 &lt;code>on_post_page&lt;/code> 函数中，将原来文件中的 &lt;code>{{ placeholder }}&lt;/code> 替换为用 jinja 生成的 html 字符串：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> &lt;span style="color:#a6e22e">on_post_page&lt;/span>(self, output, page, config):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> additional_html &lt;span style="color:#f92672">=&lt;/span> self&lt;span style="color:#f92672">.&lt;/span>template&lt;span style="color:#f92672">.&lt;/span>render(pages&lt;span style="color:#f92672">=&lt;/span>self&lt;span style="color:#f92672">.&lt;/span>blog_pages, &lt;span style="color:#f92672">...&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output &lt;span style="color:#f92672">=&lt;/span> re&lt;span style="color:#f92672">.&lt;/span>sub(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">r&lt;/span>&lt;span style="color:#e6db74">&amp;#34;\{\{\s*blog_content\s*\}\}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> additional_html,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> output,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> flags&lt;span style="color:#f92672">=&lt;/span>re&lt;span style="color:#f92672">.&lt;/span>IGNORECASE,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> output
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>就基本上完成了所有的工作。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>上面所述的仅仅是插件开发中一些枝干的问题，实际上，做一个工程，而不仅是自己用的小软件，要考虑的问题远不止于怎么实现功能，不仅要尽可能满足用户的需求，还要处理各种 corner cases（&lt;del>测试工程师进酒吧&lt;/del>）。当然，造轮子虽然难，但是其中的快乐是无穷的。&lt;/p></content></item><item><title>用 Property Wrapper 包装网络请求参数</title><link>https://yeshe.ng/blog/posts/2021/08/property-wrapper-http/</link><pubDate>Sat, 28 Aug 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/08/property-wrapper-http/</guid><description>Swift 中的 HTTP 请求一般需要手动设置请求的参数和 Header，是否有一种优雅的方式将请求的数据与请求本身绑定，从而省去手动赋值的过程？</description><content>&lt;p>偶然看到 &lt;a href="https://square.github.io/retrofit/">Java 语言 Retrofit 库的文档&lt;/a>，对这种标注 HTTP 请求参数的方式十分感兴趣（其实我也没学过 Java）：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@FormUrlEncoded&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">@POST&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;user/edit&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Call&lt;span style="color:#f92672">&amp;lt;&lt;/span>User&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">updateUser&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#a6e22e">@Field&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;first_name&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span> String first&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#a6e22e">@Field&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;last_name&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span> String last&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>联想一下以前用 Swift 写的网络请求（参考 &lt;a href="https://yeshe.ng/blog/posts/2021/08/protocol-generic/">协议 + 泛型：威力巨大的组合&lt;/a>），都需要手动赋值参数，实在是不太优雅：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> headers: HTTPHeaders = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.token,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;Accept&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> parameters: [String : Encodable] = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pid&amp;#34;&lt;/span> : &lt;span style="color:#66d9ef">self&lt;/span>.configuration.postId,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;switch&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.switchToAttention ? &lt;span style="color:#ae81ff">1&lt;/span> : &lt;span style="color:#ae81ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Java 的这种语法与 Swift 的 &lt;code>Property Wrapper&lt;/code> 很像（实践证明 Swift 这个语法局限性还是挺大的），&lt;del>而且逼格也非常高&lt;/del> ，恰好也一直想自己写个 Property Wrapper 玩一玩研究一下，于是就动手写一个包装网络请求参数的库。&lt;/p>
&lt;h2 id="理想效果">理想效果&lt;/h2>
&lt;p>理想中，对参数的包装应该达到的效果有：&lt;/p>
&lt;ul>
&lt;li>用 Property Wrapper 包装实际的参数值和对应的 key
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">SomeRequest&lt;/span>: Request {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Header(&lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> accessToken
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;page&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> page: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>能够自动找到所有的参数，并将其放入 HTTP 请求中&lt;/li>
&lt;/ul>
&lt;p>目标有了，就开始写吧！&lt;/p>
&lt;h2 id="设计与实现">设计与实现&lt;/h2>
&lt;h3 id="包装参数">包装参数&lt;/h3>
&lt;p>我们的 Property Wrapper 中需要 key 和 value。尝试写一下 &lt;code>@Query&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@propertyWrapper
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Query&lt;/span>&amp;lt;Value: Encodable&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> key: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> wrappedValue: Value
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value, &lt;span style="color:#66d9ef">_&lt;/span> key: String) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.key = key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.wrappedValue = wrappedValue
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>@Query&lt;/code> 接受 &lt;code>String&lt;/code> 作为 key，和一个遵循 &lt;code>Encodable&lt;/code> 协议的值作为 value。&lt;/p>
&lt;p>可是，当我们尝试使用这个 Property Wrapper 的时候，却出现了问题：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样写会出现编译错误：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Missing argument for parameter &amp;#39;wrappedValue&amp;#39; in call
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>原因在于，&lt;code>@Query(&amp;quot;foo&amp;quot;) var bar: Int&lt;/code> 的声明其实是在调用 &lt;code>Query&amp;lt;Int&amp;gt;&lt;/code> 的 &lt;code>init(wrappedValue: Value, _ key: String)&lt;/code>，而 Swift 不允许存在未初始化的成员变量，所以必须同时初始化 &lt;code>key&lt;/code> 和 &lt;code>wrappedValue&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int = &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是，总不能要求调用者每种类型都给出个默认值……这样实在是太丑陋了，也不符合语义—— &lt;code>@Query(&amp;quot;foo&amp;quot;)&lt;/code> 声明 key，value 应该是之后赋值的。而且也不现实，如果 &lt;code>wrappedValue&lt;/code> 是一个自定义类型的话，提供默认值是一件匪夷所思的事情。&lt;/p>
&lt;p>没办法，Property Wrapper 括号中的参数并不是严格意义上静态的 attribute，而是一个函数的参数，是一个变量，这本来就是和我们的需求相背的。&lt;/p>
&lt;p>那么，退而求其次，既然不能要求提供默认值，那我们可以用一种不会产生歧义的、带有默认初始化方法的变量—— &lt;code>Optional&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@propertyWrapper
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Query&lt;/span>&amp;lt;Value: Encodable&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> key: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> wrappedValue: Value?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value?, &lt;span style="color:#66d9ef">_&lt;/span> key: String) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.key = key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.wrappedValue = wrappedValue
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样一来，参数的声明就可以稍微合理一点了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>虽然我们还是要提供一个初始值，但是 &lt;code>nil&lt;/code> 在这里刚好可以表达“未初始化”的意思，在以后解析所有参数的时候，如果这个值没有被设定，我们直接跳过这个请求参数就好了。&lt;/p>
&lt;p>到这里，自然会想到在 &lt;code>init&lt;/code> 中给 &lt;code>wrappedValue&lt;/code> 设定 &lt;code>nil&lt;/code> 的默认值，使得我们不需要手动写 &lt;code>= nil&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value? = &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#66d9ef">_&lt;/span> key: String)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这似乎解决了问题，我们可以这样写了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>@Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是又多了一个问题：我们不能直接用 &lt;code>wrappedValue&lt;/code> 初始化含有 &lt;code>@Query&lt;/code> 变量的类型了，如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Foo&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Query(&lt;span style="color:#e6db74">&amp;#34;foo&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> bar: Int?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// let foo = Foo(bar: 1)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Cannot convert value of type &amp;#39;Int&amp;#39; to expected argument type &amp;#39;Query&amp;lt;Int&amp;gt;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;blockquote>
&lt;p>用一个例子测试一下 Property Wrapper 初始化的原理：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">A&lt;/span>&amp;lt;Value: Encodable&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> key: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> value: Value?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">var&lt;/span> wrappedValue: Value? {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">get&lt;/span> { value }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">set&lt;/span> { value = newValue; print(newValue) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">init&lt;/span>(wrappedValue: Value?, &lt;span style="color:#66d9ef">_&lt;/span> key: String) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.key = key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.value = wrappedValue
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(&lt;span style="color:#e6db74">&amp;#34;Initializing with wrappedValue &lt;/span>&lt;span style="color:#e6db74">\(&lt;/span>wrappedValue&lt;span style="color:#e6db74">)&lt;/span>&lt;span style="color:#e6db74">, key &lt;/span>&lt;span style="color:#e6db74">\(&lt;/span>key&lt;span style="color:#e6db74">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Test&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @A(&lt;span style="color:#e6db74">&amp;#34;hello&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> a: Int? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> foo = Test()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> bar = Test(a: &lt;span style="color:#ae81ff">1&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Initializing with wrappedValue nil, key hello
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Initializing with wrappedValue Optional(1), key hello
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>原来 &lt;code>@A&lt;/code> 变量在 &lt;code>Test&lt;/code> 的构造函数中无论是否赋值，都只会调用一次 &lt;code>@A&lt;/code> 的 &lt;code>init&lt;/code>。当我们不指定 &lt;code>a&lt;/code> 的值的时候，使用我们提供的默认值 &lt;code>nil&lt;/code> 进行初始化；当我们指定 &lt;code>a&lt;/code> 的值的时候，使用我们指定的值。&lt;/p>
&lt;p>所以，&lt;code>@Query(&amp;quot;foo&amp;quot;) var bar: Int? = nil&lt;/code> 中 &lt;code>= nil&lt;/code> 其实就相当于 &lt;code>init&lt;/code> 的一个默认参数。这也解释了为何我们直接在 &lt;code>init&lt;/code> 中设参数默认值使得无法用 &lt;code>wrappedValue&lt;/code> 初始化。&lt;/p>
&lt;/blockquote>
&lt;p>所以，还是手动把 &lt;code>= nil&lt;/code> 加上为妙。&lt;/p>
&lt;h3 id="获取信息">获取信息&lt;/h3>
&lt;p>包装好了，怎么获取这些变量是另一个大问题。要知道，我们根本不知道传进来的对象是什么。例如，我们的 &lt;code>SomeRequest&lt;/code> 中有这些参数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">SomeRequest&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Query(&lt;span style="color:#e6db74">&amp;#34;page&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> page
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Query(&lt;span style="color:#e6db74">&amp;#34;bar&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> foo: Int? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们当然不知道 &lt;code>SomeRequest&lt;/code> 中有 &lt;code>page&lt;/code> 和 &lt;code>foo&lt;/code> 两个成员。&lt;/p>
&lt;p>幸好，我们有 &lt;code>Mirror&lt;/code> ——利用对象的元数据，能够向我们提供对象成员变量的信息。&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;p>用 &lt;code>Mirror&lt;/code> 尝试一下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> mirror = Mirror(reflecting: request) &lt;span style="color:#75715e">// request is `SomeRequest`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> (&lt;span style="color:#66d9ef">_&lt;/span>, property) &lt;span style="color:#66d9ef">in&lt;/span> mirror.children {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// property 是 request 的成员变量&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们可以通过这种方法访问 &lt;code>request&lt;/code> 对象的所有成员变量。接下来的问题是，我们怎么识别是不是 &lt;code>@Query&lt;/code>，以及怎么把 &lt;code>Query&lt;/code> 里面的 &lt;code>key&lt;/code> 和 &lt;code>wrappedValue&lt;/code> 取出来。&lt;/p>
&lt;p>一种很朴素，又很错误的想法是这样：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> property = property &lt;span style="color:#66d9ef">as&lt;/span>? Query {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样当然通过不了编译，&lt;code>Query&lt;/code> 只是个模版，不是具体的类。我们需要一种与 &lt;code>Query&lt;/code> 模版参数无关的方法。&lt;/p>
&lt;p>我们喜欢的 &lt;code>protocol&lt;/code> 这时可以发挥作用了。我们不需要获取“&lt;code>property&lt;/code> 具体的类型”这么强的信息，只需要这个 &lt;code>property&lt;/code> 能够向我们提供我们需要的 URL 编码的 &lt;code>value&lt;/code> 和 &lt;code>key&lt;/code> 就行了。马上可以写一个协议：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">QueryProtocol&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedKey&lt;/span>() -&amp;gt; String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedValue&lt;/span>() -&amp;gt; String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们给 &lt;code>Query&lt;/code> 加上 &lt;code>QueryProtocol&lt;/code> 的协议：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">extension&lt;/span> &lt;span style="color:#a6e22e">Query&lt;/span>: QueryProtocol {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedKey&lt;/span>() -&amp;gt; String {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> SomeURLStringEncoder().encode(key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">urlEncodedValue&lt;/span>() -&amp;gt; String {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> SomeURLStringEncoder().encode(wrappedValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，我们就可以用 &lt;code>Mirror&lt;/code> 访问 key 和 value 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> mirror = Mirror(reflecting: request) &lt;span style="color:#75715e">// request is `SomeRequest`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> (&lt;span style="color:#66d9ef">_&lt;/span>, property) &lt;span style="color:#66d9ef">in&lt;/span> mirror.children {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> query = property &lt;span style="color:#66d9ef">as&lt;/span>? QueryProtocol {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> key = query.urlEncodedKey()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> value = query.urlEncodedValue()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// &lt;/span>&lt;span style="color:#75715e">TODO:&lt;/span>&lt;span style="color:#75715e"> Add key, value to request URL.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，最后一个障碍——读取参数信息也扫清了。我们可以获取到一个类里面所有的 &lt;code>@Query&lt;/code> 参数了，将所有的 &lt;code>key&lt;/code> 和 &lt;code>value&lt;/code> 加到请求的 URL 即可。&lt;/p>
&lt;blockquote>
&lt;p>实际上的处理要稍微复杂一点，因为 &lt;code>Value&lt;/code> 可能是一个数组，这时候就不仅仅是一个 key 和 value 对了。&lt;/p>
&lt;/blockquote>
&lt;h2 id="后续">后续&lt;/h2>
&lt;p>根据相似的思路，我们可以为 Header、body 参数等定义类似的 Property Wrapper：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Property Wrapper&lt;/th>
&lt;th>Description&lt;/th>
&lt;th>Wrapped Value&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>@Header&lt;/code>&lt;/td>
&lt;td>HTTP Header&lt;/td>
&lt;td>&lt;code>String?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@HeaderDict&lt;/code>&lt;/td>
&lt;td>An dictionary of headers&lt;/td>
&lt;td>&lt;code>[String : String]?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@Query&lt;/code>&lt;/td>
&lt;td>Query parameters&lt;/td>
&lt;td>&lt;code>Encodable?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@QueryDict&lt;/code>&lt;/td>
&lt;td>An dictionary of query parameters&lt;/td>
&lt;td>&lt;code>[String : String]?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@KeyQuery&lt;/code>&lt;/td>
&lt;td>Query parameters with no value associated&lt;/td>
&lt;td>&lt;code>Bool&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@JSON&lt;/code>&lt;/td>
&lt;td>Body parameters with JSON encoding&lt;/td>
&lt;td>&lt;code>Encodable?&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>@Field&lt;/code>&lt;/td>
&lt;td>Form URL Encoded parameters&lt;/td>
&lt;td>&lt;code>Encodable?&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>我们可以在一个类声明中定义所有的请求参数了。最后的实现效果如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">SetPushRequest&lt;/span>: JSONDecodableRequest, RequestConfiguration {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Header(&lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> accessToken = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;push_system_msg&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> pushSystemMessage: Bool? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;push_reply_me&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> pushReplyMe: Bool? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Field(&lt;span style="color:#e6db74">&amp;#34;push_favorited&amp;#34;&lt;/span>) &lt;span style="color:#66d9ef">var&lt;/span> pushFavourite: Bool? = &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Response&lt;/span>: Codable {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> code: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> msg: String?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> base: URL = URL(string: &lt;span style="color:#e6db74">&amp;#34;https://dev-api.thuhole.com&amp;#34;&lt;/span>)&lt;span style="color:#f92672">!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> path: String = &lt;span style="color:#e6db74">&amp;#34;v3/config/set_push&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> method: HTTPMethod = .post
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> configuration: &lt;span style="color:#66d9ef">Self&lt;/span> { &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">self&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> request = SetPushRequest(accessToken: token, pushSystemMessage: &lt;span style="color:#66d9ef">true&lt;/span>, pushReplyMe: &lt;span style="color:#66d9ef">true&lt;/span>, pushFavourite: &lt;span style="color:#66d9ef">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>request.perform { result &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> result {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> .success(&lt;span style="color:#66d9ef">let&lt;/span> response):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(response)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> .failure(&lt;span style="color:#66d9ef">let&lt;/span> error):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print(error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>SetPushRequest&lt;/code> 的定义不计空行只用了 14 行。在 &lt;a href="https://github.com/liang2kl/HollowCore/blob/main/Sources/HollowCore/Network/Config/SetPushRequest.swift">原来树洞的代码&lt;/a> 中，完成上述功能写了 31 行（不包括上面代码不涉及的功能）。&lt;/p>
&lt;blockquote>
&lt;p>项目源代码参见 &lt;a href="https://github.com/liang2kl/APIKit">liang2kl/APIKit&lt;/a>（&lt;del>虽然还没写完&lt;/del>）。&lt;/p>
&lt;/blockquote>
&lt;p>利用 Property Wrapper 包装参数，加上一些基本信息，就可以定义一个 HTTP 请求了。最重要的是，我们再也不需要手动设置 parameters 了。&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://developer.apple.com/documentation/swift/mirror">Mirror | Apple Developer Documentation&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content></item><item><title>协议 + 泛型：威力巨大的组合</title><link>https://yeshe.ng/blog/posts/2021/08/protocol-generic/</link><pubDate>Thu, 26 Aug 2021 00:00:00 +0800</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/posts/2021/08/protocol-generic/</guid><description>在 Swift 的 protocol 中使用泛型，进行愉快的 Protocol-oriented Programming</description><content>&lt;p>写树洞客户端时，和一个 PKU 的同学一起写，他负责写调用树洞 HTTP API 获取数据的部分。既然涉及到协作，就得先规定下接口了。&lt;/p>
&lt;p>对于 Swift 来说，规定接口的最好的办法自然是 &lt;code>protocol&lt;/code> ——规定并强制实现所有的接口。于是，我着手写这个协议的声明。&lt;/p>
&lt;blockquote>
&lt;p>源代码参见 &lt;a href="https://github.com/liang2kl/HollowCore">liang2kl/HollowCore&lt;/a>。&lt;/p>
&lt;/blockquote>
&lt;h2 id="协议的设计">协议的设计&lt;/h2>
&lt;h3 id="目标">目标&lt;/h3>
&lt;p>协议的设计有两个目标：&lt;/p>
&lt;ul>
&lt;li>所有的请求均提供统一的接口，方便调用&lt;/li>
&lt;li>尽可能方便不同的请求类复用代码&lt;/li>
&lt;/ul>
&lt;h3 id="v10最初尝试">v1.0：最初尝试&lt;/h3>
&lt;p>接口无非是 &lt;code>输入&lt;/code> + &lt;code>输出&lt;/code>，对于网络请求，输入即是请求的参数，输出是得到的 response。很容易想到这样一个声明：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 用网络请求参数初始化请求&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(parameters: [String : String])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 开始请求&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (String?, Error?) -&amp;gt; Void)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中，&lt;code>init&lt;/code> 接受一个字典作为输入，&lt;code>performRequest&lt;/code> 接受一个回调函数，异步处理数据。&lt;/p>
&lt;p>乍看之下，似乎没什么问题——调用者只需要将参数的字典传进去，然后接收 JSON 格式的 &lt;code>String&lt;/code> 字符串就行了。不过，这个设计真的合理吗？&lt;/p>
&lt;p>仔细分析，这个版本的协议存在这些问题：&lt;/p>
&lt;ol>
&lt;li>调用者的输入 &lt;code>parameters: [String : String]&lt;/code> 没有受到任何限制，导致：
&lt;ul>
&lt;li>调用者在每次调用时都需要查看文档以确定需要传哪些参数&lt;/li>
&lt;li>被调用者需要繁琐地一一检查字典的 key 和 value 是否符合要求&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>直接返回原始的 JSON 字符串，每次调用后均需要解析&lt;/li>
&lt;/ol>
&lt;p>问题的根本在于，完成网络请求的类并不知道任何关于请求本身的具体信息。我们需要向类提供这些信息，使得类只接受&lt;strong>符合规定的输入&lt;/strong>，并提供&lt;strong>解析好的、符合规定的输出&lt;/strong>。而且，完成这个目标最好是依赖编译期进行静态的检查，避免可能出现的失误。朝着这个目标，我们来写 &lt;code>2.0&lt;/code> 版本。&lt;/p>
&lt;h3 id="v20添加请求信息">v2.0：添加请求信息&lt;/h3>
&lt;p>我们需要向 &lt;code>Request&lt;/code> 协议内添加参数、输出的信息。&lt;/p>
&lt;p>首先考虑传递参数的问题。一个朴素的想法是在 &lt;code>init&lt;/code> 中分别列出参数，如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">init&lt;/span>(token: String, page: Int, ...)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但是这就违背了我们**「提供一个统一的接口」**的宗旨。理想情况下，我们应该提供这样一个接口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>其中，&lt;code>Configuration&lt;/code> 包装了所有需要的参数，这通常使用 &lt;code>struct&lt;/code> 完成。&lt;/p>
&lt;p>但是，不同请求的 &lt;code>Configuration&lt;/code> 是不一样的，该怎么办？&lt;/p>
&lt;p>幸运的是，&lt;code>protocol&lt;/code> 支持“泛型”，我们只需要针对不同的请求定义不同的 &lt;code>Configuration&lt;/code> 类就可以了。同理，每个请求应该有对应的 &lt;code>Response&lt;/code> 类型。另外，为了方便处理错误，添加一个 &lt;code>Error&lt;/code> 类型，其遵循 Swift 中 &lt;code>Error&lt;/code> 协议，用来指定具体的错误。新的设计如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Response
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Error: Swift.Error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 储存参数信息&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> configuration: Configuration { &lt;span style="color:#66d9ef">get&lt;/span> &lt;span style="color:#66d9ef">set&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (Response?, Error?) -&amp;gt; Void)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们可以开始实现具体的请求了。比如，对于编辑收藏的请求：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">EditAttentionRequest&lt;/span>: Request {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Configuration&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> token: String
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> postId: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> switchToAttention: Bool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Response&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 暂时省略&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">enum&lt;/span> &lt;span style="color:#a6e22e">Error&lt;/span>: Swift.Error {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> tokenExpired,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> configuration: Configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">self&lt;/span>.configuration = configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (Response?, Error?) -&amp;gt; Void) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> urlPath = &lt;span style="color:#e6db74">&amp;#34;v3/edit/attention&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样，对于调用者来说就轻松很多了，不再需要翻文档来传参数，也不需要自己来解析 JSON 字符串，只需要根据 &lt;code>Configuration&lt;/code> 的构造函数传递参数，并利用得到的 &lt;code>Response&lt;/code> 即可：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> configuration = EditAttentionRequest.Configuration(...)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> request = EditAttentionRequest(configuration: configuration)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>request.performRequest { response, error &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这个版本的 &lt;code>Request&lt;/code> 基本上实现了我们设计一个统一接口的目标。&lt;/p>
&lt;h3 id="v21response-的界定">v2.1：Response 的界定&lt;/h3>
&lt;p>注意到上面的 &lt;code>EditAttentionRequest&lt;/code> 中省略了 &lt;code>Response&lt;/code> 的定义。我们需要明确 &lt;code>Response&lt;/code> 是什么。&lt;/p>
&lt;p>从调用者的角度，&lt;code>Response&lt;/code> 应该是可以直接利用，而不需要再进行处理的数据；而从请求本身的角度，&lt;code>Response&lt;/code> 指的是服务器返回的 JSON 字符串解析出来的对象。但是，这两个概念并不总是一样的：服务器返回的结果往往需要进一步处理，比如指示请求状态的 &lt;code>code&lt;/code>，错误信息 &lt;code>msg&lt;/code> 等。&lt;/p>
&lt;p>于是，&lt;code>Response&lt;/code> 的概念需要分成两部分：服务器返回的结果 &lt;code>Result&lt;/code>，和处理后最终返回的可以直接使用的数据 &lt;code>ResultData&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Configuration
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Result
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype ResultData
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> associatedtype Error: Swift.Error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(configuration: Configuration)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (ResultData?, Error?) -&amp;gt; Void)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>相对应地，&lt;code>EditAttentionRequest&lt;/code> 中 &lt;code>Result&lt;/code> 和 &lt;code>ResultData&lt;/code> 分别定义为：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">struct&lt;/span> &lt;span style="color:#a6e22e">Result&lt;/span>: DefaultRequestResult {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> code: Int
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> msg: String?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> data: Post?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">typealias&lt;/span> ResultData = Post
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在编辑完关注状态后，服务器会返回完整的树洞信息 &lt;code>Post&lt;/code>，以及 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code>。我们不需要 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code>（错误已经在请求内部处理），于是 &lt;code>Post&lt;/code> 作为 &lt;code>ResultData&lt;/code> 就可以了。&lt;/p>
&lt;blockquote>
&lt;p>定义 &lt;code>Result&lt;/code> 类型是很有必要的：我们一般直接使用 &lt;code>JSONDecoder&lt;/code> 将 JSON 反序列化为对象，这需要我们提供解析的模版，即 &lt;code>Result&lt;/code>。&lt;/p>
&lt;/blockquote>
&lt;p>至此，我们的协议声明已经基本上完成了。所有遵循 &lt;code>Request&lt;/code> 协议的类型都对外提供了统一的接口，并且利用泛型实现了对参数输入、服务器返回数据、最终输出和错误的严格规定。&lt;/p>
&lt;h2 id="代码复用与接口扩展">代码复用与接口扩展&lt;/h2>
&lt;p>上面我们花了大力气来规定 &lt;code>Request&lt;/code> 的各种类型，但除了统一了接口、约束输入输出外并没有什么特别的作用。实际上，协议 + 泛型的巨大威力体现在&lt;strong>代码复用&lt;/strong>和&lt;strong>接口扩展&lt;/strong>上。&lt;/p>
&lt;h3 id="代码复用">代码复用&lt;/h3>
&lt;p>上面的 &lt;code>Request&lt;/code> 只是规定了接口，我们还要逐一实现每个请求的 &lt;code>performRequest&lt;/code>。如果分别独立写 HTTP 请求代码，将十分繁琐，而且会大量重复相同代码。为此，我们需要进行一些默认实现。&lt;/p>
&lt;p>大多数请求满足两个共性：&lt;/p>
&lt;ul>
&lt;li>从 JSON 反序列化为对象的要求很简单：&lt;code>Result&lt;/code> 类型遵循 &lt;code>Decodable&lt;/code> 即可。对于这些请求，&lt;strong>由获得的 JSON 生成 &lt;code>Result&lt;/code> 的方法是固定的&lt;/strong>。&lt;/li>
&lt;li>返回包括 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code>，用来传递查询结果和错误信息。因此，&lt;strong>从 &lt;code>Result&lt;/code> 生成 &lt;code>Error&lt;/code> 的方法是固定的&lt;/strong>。而大多数的 &lt;code>Error&lt;/code> 类型也是相同的。&lt;/li>
&lt;/ul>
&lt;p>利用这两点，我们可以在 &lt;code>Request&lt;/code> 的基础上，针对这些请求进行 &lt;code>performRequest&lt;/code> 的默认实现。对这些请求，我们定义一个新的协议 &lt;code>DefaultRequest&lt;/code>，遵循 &lt;code>Request&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">DefaultRequest&lt;/span>: Request
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">where&lt;/span> Error == DefaultRequestError, Result: DefaultRequestResult { }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Error&lt;/code> 为 &lt;code>DefaultRequestError&lt;/code>，是一个涵盖了所有错误类型的 &lt;code>enum&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">enum&lt;/span> &lt;span style="color:#a6e22e">DefaultRequestError&lt;/span>: Swift.Error, LocalizedError {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> decodeFailed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> tokenExpiredError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> fileTooLarge
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> unknown
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> noSuchPost
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> other(description: String)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">init&lt;/span>(errorCode: Int, description: String?) {...}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Result&lt;/code> 遵循 &lt;code>DefaultRequestResult&lt;/code>，其定义如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">protocol&lt;/span> &lt;span style="color:#a6e22e">DefaultRequestResult&lt;/span>: Codable {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> code: Int { &lt;span style="color:#66d9ef">get&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">var&lt;/span> msg: String? { &lt;span style="color:#66d9ef">get&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分别对应上述两个共同点。&lt;/p>
&lt;p>于是，我们就可以进行 &lt;code>performRequest&lt;/code> 的默认实现了。在 &lt;code>DefaultRequest&lt;/code> 的 &lt;code>extension&lt;/code>&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> 中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">extension&lt;/span> &lt;span style="color:#a6e22e">DefaultRequest&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlPath: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: [String : Any]? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: HTTPHeaders? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: HTTPMethod,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> transformer: @escaping (Result) -&amp;gt; ResultData?,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion: @escaping (ResultData?, Error?) -&amp;gt; Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) {...}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在函数内，我们需要实现：&lt;/p>
&lt;ul>
&lt;li>将网络请求得到的 JSON 反序列化为对象，由 &lt;code>DefaultRequestResult&lt;/code> 遵循 &lt;code>Codable&lt;/code> 满足
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> jsonDecoder = JSONDecoder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> result = &lt;span style="color:#66d9ef">try&lt;/span> jsonDecoder.decode(Result.&lt;span style="color:#66d9ef">self&lt;/span>, from: data)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>将 &lt;code>Result&lt;/code> 中包含的错误信息转化为 &lt;code>Error&lt;/code> 返回，由 &lt;code>Error&lt;/code> 类型已知以及 &lt;code>DefaultRequestResult&lt;/code> 中要求 &lt;code>code&lt;/code> 和 &lt;code>msg&lt;/code> 满足
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">let&lt;/span> error = DefaultRequestError(errorCode: result.code, description: result.msg)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>将 &lt;code>Result&lt;/code> 转化为 &lt;code>ResultData&lt;/code>，由传入的参数 &lt;code>transformer&lt;/code> 提供
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> data = transformer(result) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(data, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>我们在对 &lt;code>protocol&lt;/code> 的泛型作了一定的约束后，获得了更多的信息，使得我们可以在不知道请求的具体类型的情况下进行默认实现&lt;/strong>。&lt;/p>
&lt;p>完整代码如下：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlPath: String,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: [String : Any]? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: HTTPHeaders? = &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: HTTPMethod,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> transformer: @escaping (Result) -&amp;gt; ResultData?,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion: @escaping (ResultData?, Error?) -&amp;gt; Void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AF.request(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot &lt;span style="color:#f92672">+&lt;/span> urlPath,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: method,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: parameters,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> encoding: URLEncoding.&lt;span style="color:#66d9ef">default&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: headers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .validate()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .responseJSON { response &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">switch&lt;/span> response.result {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> .success:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> jsonDecoder = JSONDecoder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">do&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">guard&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> data = response.data &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .unknown)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> result = &lt;span style="color:#66d9ef">try&lt;/span> jsonDecoder.decode(Result.&lt;span style="color:#66d9ef">self&lt;/span>, from: data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> result.code &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">0&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// result code &amp;gt;= 0 valid!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> data = transformer(result) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(data, &lt;span style="color:#66d9ef">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .unknown)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> error = DefaultRequestError(errorCode: result.code, description: result.msg)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">catch&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .decodeFailed)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> .failure(error):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#66d9ef">let&lt;/span> errorCode = error.responseCode, errorCode == &lt;span style="color:#ae81ff">413&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .fileTooLarge)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#66d9ef">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion(&lt;span style="color:#66d9ef">nil&lt;/span>, .other(description: error.localizedDescription))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>现在，我们可以轻松实现具体类型的 &lt;code>performRequest&lt;/code> 了，如 &lt;code>EditAttentionRequest&lt;/code>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">performRequest&lt;/span>(completion: @escaping (ResultData?, Error?) -&amp;gt; Void) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> urlPath = &lt;span style="color:#e6db74">&amp;#34;v3/edit/attention&amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> Constants.urlSuffix
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> headers: HTTPHeaders = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;TOKEN&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.token,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;Accept&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">let&lt;/span> parameters: [String : Encodable] = [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;pid&amp;#34;&lt;/span> : &lt;span style="color:#66d9ef">self&lt;/span>.configuration.postId,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;switch&amp;#34;&lt;/span>: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.switchToAttention ? &lt;span style="color:#ae81ff">1&lt;/span> : &lt;span style="color:#ae81ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> performRequest(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlRoot: &lt;span style="color:#66d9ef">self&lt;/span>.configuration.apiRoot,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urlPath: urlPath,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parameters: parameters,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers: headers,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> method: .post,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> transformer: { $0.data },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> completion: completion
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我们只需要将 &lt;code>Configuration&lt;/code> 转换为字典就可以了。在实际项目中，几乎所有的请求都可以遵循 &lt;code>DefaultRequest&lt;/code>，只需要写一次网络请求的代码，就可以实现所有的网络请求。这就是 &lt;strong>&lt;code>protocol&lt;/code> + 泛型 + 限制条件&lt;/strong>的魅力。&lt;/p>
&lt;h3 id="接口扩展">接口扩展&lt;/h3>
&lt;p>利用 &lt;code>protocol&lt;/code> 的泛型，还有个好处在于扩展接口十分方便。例如，针对 &lt;code>Swift 5.5&lt;/code> 的 &lt;code>async/await&lt;/code>，我们很容易为 &lt;code>Request&lt;/code> 添加异步接口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-swift" data-lang="swift">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">extension&lt;/span> &lt;span style="color:#a6e22e">Request&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">result&lt;/span>() async -&amp;gt; (ResultData?, Error?) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> await withCheckedContinuation { continuation &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> performRequest { result &lt;span style="color:#66d9ef">in&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> continuation.resume(returning: result)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>几乎不需要写任何代码。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>Swift 的 &lt;code>protocol&lt;/code> 相当于 C++ 的抽象类，不过相比 C++ 更加严谨、灵活。&lt;code>protocol&lt;/code> 本身有规定接口的功能，再加上泛型，能够很好地实现多态，实现高效的代码复用，同时保持良好的可维护性和可扩展性。&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a href="https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID521">Protocols — The Swift Programming Language (Swift 5.5)&lt;/a>&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/arches/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/arches/</guid><description>&amp;lt; All Photos
#arches 2025/6/21 - 2025/6/21
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Landscape ArchArches National Park, 2025/6/21</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="arches">#arches&lt;/h2>
&lt;p>2025/6/21 - 2025/6/21&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;b style="margin-right:1em">#arches&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03404">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03404.webp">
&lt;div class="image-caption ">Landscape Arch&lt;/br>&lt;span>Arches National Park, 2025/6/21&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/bayarea/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/bayarea/</guid><description>&amp;lt; All Photos
#bayarea 2026/5/24 - 2026/5/24
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Niles BoulevardNiles, 2026/5/24</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="bayarea">#bayarea&lt;/h2>
&lt;p>2026/5/24 - 2026/5/24&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;b style="margin-right:1em">#bayarea&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00221">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00221.webp">
&lt;div class="image-caption ">Niles Boulevard&lt;/br>&lt;span>Niles, 2026/5/24&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/bryce-canyon/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/bryce-canyon/</guid><description>&amp;lt; All Photos
#bryce-canyon 2025/6/20 - 2025/6/20
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Navajo Loop TrailBryce Canyon National Park, 2025/6/20</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="bryce-canyon">#bryce-canyon&lt;/h2>
&lt;p>2025/6/20 - 2025/6/20&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;b style="margin-right:1em">#bryce-canyon&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03361">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03361.webp">
&lt;div class="image-caption ">Navajo Loop Trail&lt;/br>&lt;span>Bryce Canyon National Park, 2025/6/20&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/death-valley/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/death-valley/</guid><description>&amp;lt; All Photos
#death-valley 2025/3/20 - 2025/3/21
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Zabriskie PointDeath Valley National Park, 2025/3/21 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Badwater BasinDeath Valley National Park, 2025/3/20 Artist's PaletteDeath Valley National Park, 2025/3/20</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="death-valley">#death-valley&lt;/h2>
&lt;p>2025/3/20 - 2025/3/21&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;b style="margin-right:1em">#death-valley&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03265">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03265.webp">
&lt;div class="image-caption ">Zabriskie Point&lt;/br>&lt;span>Death Valley National Park, 2025/3/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/polaroid-mesquite">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/polaroid-mesquite.webp">
&lt;div class="image-caption polaroid">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03172">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03172.webp">
&lt;div class="image-caption ">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03114">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03114.webp">
&lt;div class="image-caption ">Badwater Basin&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03098">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03098.webp">
&lt;div class="image-caption ">Artist's Palette&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc00150/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc00150/</guid><description> &amp;lt; All Photos&amp;lt; return
Big Ben (London) #travel#london
DateTimeOriginal 2026:04:18 00:27:42 OffsetTimeOriginal +08:00 Make SONY Model ILCE-7CM2 LensModel FE 20-70mm F4 G FocalLength 20.0 ExposureTime 0.001 FNumber 4.5 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="big-ben-london">Big Ben (London)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC00150.jpg" alt="Big Ben" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2026:04:18 00:27:42&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>OffsetTimeOriginal&lt;/b>&lt;/td>
&lt;td>+08:00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-7CM2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>FE 20-70mm F4 G&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>20.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.001&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>4.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=51.500729,-0.124625&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc00177/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc00177/</guid><description> &amp;lt; All Photos&amp;lt; return
McWay Falls (Highway 1) #travel#hwy1
DateTimeOriginal 2026:05:21 04:32:43 OffsetTimeOriginal +08:00 Make SONY Model ILCE-7CM2 LensModel FE 20-70mm F4 G FocalLength 39.0 ExposureTime 0.001 FNumber 4.5 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="mcway-falls-highway-1">McWay Falls (Highway 1)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC00177.jpg" alt="McWay Falls" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2026:05:21 04:32:43&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>OffsetTimeOriginal&lt;/b>&lt;/td>
&lt;td>+08:00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-7CM2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>FE 20-70mm F4 G&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>39.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.001&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>4.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.157826,-121.672163&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc00192/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc00192/</guid><description> &amp;lt; All Photos&amp;lt; return
Soberanes Point (Highway 1) #travel#hwy1
DateTimeOriginal 2026:05:21 05:54:13 OffsetTimeOriginal +08:00 Make SONY Model ILCE-7CM2 LensModel FE 20-70mm F4 G FocalLength 70.0 ExposureTime 0.000625 FNumber 4.5 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="soberanes-point-highway-1">Soberanes Point (Highway 1)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC00192.jpg" alt="Soberanes Point" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2026:05:21 05:54:13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>OffsetTimeOriginal&lt;/b>&lt;/td>
&lt;td>+08:00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-7CM2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>FE 20-70mm F4 G&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>70.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.000625&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>4.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.45283,-121.929376&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc00221/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc00221/</guid><description> &amp;lt; All Photos&amp;lt; return
Niles Boulevard (Niles) #travel#bayarea
DateTimeOriginal 2026:05:25 09:44:55 OffsetTimeOriginal +08:00 Make SONY Model ILCE-7CM2 LensModel FE 20-70mm F4 G FocalLength 51.0 ExposureTime 0.00625 FNumber 6.3 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="niles-boulevard-niles">Niles Boulevard (Niles)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC00221.jpg" alt="Niles Boulevard" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2026:05:25 09:44:55&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>OffsetTimeOriginal&lt;/b>&lt;/td>
&lt;td>+08:00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-7CM2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>FE 20-70mm F4 G&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>51.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.00625&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>6.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=37.579256,-121.98902&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc00381/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc00381/</guid><description> &amp;lt; All Photos&amp;lt; return
Convict Lake (Eastern Sierra) #travel#sierra
DateTimeOriginal 2026:09:13 15:06:01 OffsetTimeOriginal +08:00 Make SONY Model ILCE-7CM2 LensModel Sony FE 20-70mm F4.0 FocalLength 20.0 ExposureTime 20.0 FNumber 4.0 ISOSpeedRatings 3200 Software Adobe Photoshop Lightroom Classic 15.6 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="convict-lake-eastern-sierra">Convict Lake (Eastern Sierra)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC00381.jpg" alt="Convict Lake" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2026:09:13 15:06:01&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>OffsetTimeOriginal&lt;/b>&lt;/td>
&lt;td>+08:00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-7CM2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>Sony FE 20-70mm F4.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>20.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>20.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>4.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>3200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 15.6 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=37.589226,-118.859157&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc00558/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc00558/</guid><description> &amp;lt; All Photos&amp;lt; return
Gardens by the Bay (Singapore) #singapore#travel
DateTimeOriginal 2023:07:30 21:06:09 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 16.0 ExposureTime 0.125 FNumber 3.5 ISOSpeedRatings 3200 Software Adobe Photoshop Lightroom Classic 12.5 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="gardens-by-the-bay-singapore">Gardens by the Bay (Singapore)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC00558.jpg" alt="Gardens by the Bay" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2023:07:30 21:06:09&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>16.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.125&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>3.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>3200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 12.5 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=1.281568,103.863613&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc00821/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc00821/</guid><description> &amp;lt; All Photos&amp;lt; return
Merlion Park (Singapore) #singapore#travel
DateTimeOriginal 2023:08:09 16:53:52 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 50.0 ExposureTime 0.003125 FNumber 8.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 11.4.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="merlion-park-singapore">Merlion Park (Singapore)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC00821.jpg" alt="Merlion Park" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2023:08:09 16:53:52&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>50.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.003125&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 11.4.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=1.286745,103.854387&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc01262/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc01262/</guid><description> &amp;lt; All Photos&amp;lt; return
Singapore GP Pit Lane (Singapore) #singapore#formula1
DateTimeOriginal 2023:09:14 20:28:18 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 50.0 ExposureTime 0.0125 FNumber 5.6 ISOSpeedRatings 2000 Software Adobe Photoshop Lightroom Classic 12.5 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="singapore-gp-pit-lane-singapore">Singapore GP Pit Lane (Singapore)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC01262.jpg" alt="Singapore GP Pit Lane" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2023:09:14 20:28:18&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>50.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.0125&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>5.6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>2000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 12.5 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=1.291363,103.863892&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc01646/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc01646/</guid><description> &amp;lt; All Photos&amp;lt; return
Chinese GP 2024 Free Practice - GS H (Shanghai) #formula1
DateTimeOriginal 2024:04:19 11:35:34 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 203.0 ExposureTime 0.001 FNumber 6.3 ISOSpeedRatings 200 Software Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="chinese-gp-2024-free-practice---gs-h-shanghai">Chinese GP 2024 Free Practice - GS H (Shanghai)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC01646.jpg" alt="Chinese GP 2024 Free Practice - GS H" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2024:04:19 11:35:34&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>203.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.001&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>6.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=31.3371,121.2274&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc01869/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc01869/</guid><description> &amp;lt; All Photos&amp;lt; return
Chinese GP 2024 Sprint - GS A (Shanghai) #formula1
DateTimeOriginal 2024:04:20 10:40:22 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 195.0 ExposureTime 0.003125 FNumber 6.3 ISOSpeedRatings 400 Software Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="chinese-gp-2024-sprint---gs-a-shanghai">Chinese GP 2024 Sprint - GS A (Shanghai)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC01869.jpg" alt="Chinese GP 2024 Sprint - GS A" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2024:04:20 10:40:22&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>195.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.003125&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>6.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>400&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=31.3372,121.2206&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc02170/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc02170/</guid><description> &amp;lt; All Photos&amp;lt; return
Chinese GP 2024 Race - GS A (Shanghai) #formula1
DateTimeOriginal 2024:04:21 14:20:41 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 122.0 ExposureTime 0.005 FNumber 5.6 ISOSpeedRatings 160 Software Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="chinese-gp-2024-race---gs-a-shanghai">Chinese GP 2024 Race - GS A (Shanghai)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC02170.jpg" alt="Chinese GP 2024 Race - GS A" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2024:04:21 14:20:41&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>122.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.005&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>5.6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>160&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=31.3372,121.2206&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc02254/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc02254/</guid><description> &amp;lt; All Photos&amp;lt; return
Chinese GP 2024 Race - GS A (Shanghai) #formula1
DateTimeOriginal 2024:04:21 16:47:31 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 210.0 ExposureTime 0.0025 FNumber 6.3 ISOSpeedRatings 2000 Software Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="chinese-gp-2024-race---gs-a-shanghai">Chinese GP 2024 Race - GS A (Shanghai)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC02254.jpg" alt="Chinese GP 2024 Race - GS A" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2024:04:21 16:47:31&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>210.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.0025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>6.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>2000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 13.0.2 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=31.3372,121.2206&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc02980/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc02980/</guid><description> &amp;lt; All Photos&amp;lt; return
Torrey Pines State Reserve (San Diego) #san-diego#travel
DateTimeOriginal 2025:01:08 07:33:50 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 50.0 ExposureTime 0.01 FNumber 8.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="torrey-pines-state-reserve-san-diego">Torrey Pines State Reserve (San Diego)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC02980.jpg" alt="Torrey Pines State Reserve" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:01:08 07:33:50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>50.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.01&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=32.920935,-117.251444&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03005/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03005/</guid><description> &amp;lt; All Photos&amp;lt; return
Sunset Cliff (San Diego) #san-diego#travel
DateTimeOriginal 2025:01:15 08:55:14 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 165.0 ExposureTime 0.003125 FNumber 8.0 ISOSpeedRatings 640 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="sunset-cliff-san-diego">Sunset Cliff (San Diego)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03005.jpg" alt="Sunset Cliff" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:01:15 08:55:14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>165.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.003125&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>640&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=32.715915,-117.255168&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03054/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03054/</guid><description> &amp;lt; All Photos&amp;lt; return
Joshua Tree National Park (Twentynine Palms) #travel#national-park
DateTimeOriginal 2025:01:16 08:13:01 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 210.0 ExposureTime 0.0025 FNumber 8.0 ISOSpeedRatings 500 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="joshua-tree-national-park-twentynine-palms">Joshua Tree National Park (Twentynine Palms)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03054.jpg" alt="Joshua Tree National Park" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:01:16 08:13:01&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>210.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.0025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>500&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=33.873415,-115.900992&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03098/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03098/</guid><description> &amp;lt; All Photos&amp;lt; return
Artist&amp;rsquo;s Palette (Death Valley National Park) #travel#death-valley#national-park
DateTimeOriginal 2025:03:20 14:52:18 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 80.0 ExposureTime 0.002 FNumber 9.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="artists-palette-death-valley-national-park">Artist&amp;rsquo;s Palette (Death Valley National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03098.jpg" alt="Artist's Palette" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:03:20 14:52:18&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>80.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.002&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>9.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.367728,-116.803384&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03114/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03114/</guid><description> &amp;lt; All Photos&amp;lt; return
Badwater Basin (Death Valley National Park) #travel#death-valley#national-park
DateTimeOriginal 2025:03:20 16:04:10 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 55.0 ExposureTime 0.0025 FNumber 9.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="badwater-basin-death-valley-national-park">Badwater Basin (Death Valley National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03114.jpg" alt="Badwater Basin" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:03:20 16:04:10&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>55.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.0025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>9.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.246084,-116.818508&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03119/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03119/</guid><description> &amp;lt; All Photos
Badwater Basin (Death Valley National Park) #travel#death-valley#national-park
DateTimeOriginal 2025:03:20 16:06:28 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 55.0 ExposureTime 0.0025 FNumber 11.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="badwater-basin-death-valley-national-park">Badwater Basin (Death Valley National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03119.webp" alt="Badwater Basin" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:03:20 16:06:28&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>55.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.0025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>11.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03172/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03172/</guid><description> &amp;lt; All Photos&amp;lt; return
Mesquite Flat Sand Dunes (Death Valley National Park) #travel#death-valley#national-park
DateTimeOriginal 2025:03:20 18:24:41 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 55.0 ExposureTime 0.00025 FNumber 8.0 ISOSpeedRatings 3200 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="mesquite-flat-sand-dunes-death-valley-national-park">Mesquite Flat Sand Dunes (Death Valley National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03172.jpg" alt="Mesquite Flat Sand Dunes" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:03:20 18:24:41&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>55.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.00025&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>3200&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.615122,-117.113144&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03265/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03265/</guid><description> &amp;lt; All Photos&amp;lt; return
Zabriskie Point (Death Valley National Park) #travel#death-valley#national-park
DateTimeOriginal 2025:03:21 06:52:48 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 55.0 ExposureTime 0.05 FNumber 4.5 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="zabriskie-point-death-valley-national-park">Zabriskie Point (Death Valley National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03265.jpg" alt="Zabriskie Point" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:03:21 06:52:48&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>55.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.05&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>4.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.420067,-116.81223&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03277/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03277/</guid><description> &amp;lt; All Photos&amp;lt; return
Tunnel View (Yosemite National Park) #travel#yosemite#national-park#sierra
DateTimeOriginal 2025:06:15 07:48:50 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 26.0 ExposureTime 0.004 FNumber 8.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="tunnel-view-yosemite-national-park">Tunnel View (Yosemite National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03277.jpg" alt="Tunnel View" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:06:15 07:48:50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>26.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.004&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=37.715799,-119.677483&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03343/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03343/</guid><description> &amp;lt; All Photos&amp;lt; return
Point Imperial (Grand Canyon National Park) #travel#grand-canyon#national-park
DateTimeOriginal 2025:06:18 07:49:36 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 50.0 ExposureTime 0.003125 FNumber 8.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="point-imperial-grand-canyon-national-park">Point Imperial (Grand Canyon National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03343.jpg" alt="Point Imperial" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:06:18 07:49:36&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>50.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.003125&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.278957,-111.977646&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03361/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03361/</guid><description> &amp;lt; All Photos&amp;lt; return
Navajo Loop Trail (Bryce Canyon National Park) #travel#bryce-canyon#national-park
DateTimeOriginal 2025:06:20 12:23:01 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 16.0 ExposureTime 0.004 FNumber 7.1 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="navajo-loop-trail-bryce-canyon-national-park">Navajo Loop Trail (Bryce Canyon National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03361.jpg" alt="Navajo Loop Trail" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:06:20 12:23:01&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>16.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.004&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>7.1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=37.62064,-112.163771&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03404/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03404/</guid><description> &amp;lt; All Photos&amp;lt; return
Landscape Arch (Arches National Park) #travel#arches#national-park
DateTimeOriginal 2025:06:21 08:37:01 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 16.0 ExposureTime 0.003125 FNumber 7.1 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="landscape-arch-arches-national-park">Landscape Arch (Arches National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03404.jpg" alt="Landscape Arch" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:06:21 08:37:01&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>16.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.003125&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>7.1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=38.790473,-109.60713&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03541/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03541/</guid><description> &amp;lt; All Photos&amp;lt; return
Taggart Lake Loop Trail (Grand Teton National Park) #travel#grand-teton#national-park
DateTimeOriginal 2025:06:25 07:17:17 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 50.0 ExposureTime 0.005 FNumber 5.6 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="taggart-lake-loop-trail-grand-teton-national-park">Taggart Lake Loop Trail (Grand Teton National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03541.jpg" alt="Taggart Lake Loop Trail" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:06:25 07:17:17&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>50.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.005&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>5.6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=43.692957,-110.739584&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03551/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03551/</guid><description> &amp;lt; All Photos&amp;lt; return
Taggart Lake (Grand Teton National Park) #travel#grand-teton#national-park
DateTimeOriginal 2025:06:25 07:22:09 Make SONY Model ILCE-6300 LensModel E PZ 16-50mm F3.5-5.6 OSS FocalLength 20.0 ExposureTime 0.01 FNumber 8.0 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="taggart-lake-grand-teton-national-park">Taggart Lake (Grand Teton National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03551.jpg" alt="Taggart Lake" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:06:25 07:22:09&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E PZ 16-50mm F3.5-5.6 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>20.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.01&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>8.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=43.703718,-110.755502&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/dsc03640/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/dsc03640/</guid><description> &amp;lt; All Photos&amp;lt; return
Artist Point (Yellowstone National Park) #travel#yellowstone#national-park
DateTimeOriginal 2025:06:27 17:12:35 Make SONY Model ILCE-6300 LensModel E 55-210mm F4.5-6.3 OSS FocalLength 55.0 ExposureTime 0.005 FNumber 5.6 ISOSpeedRatings 100 Software Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh) approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="artist-point-yellowstone-national-park">Artist Point (Yellowstone National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/DSC03640.jpg" alt="Artist Point" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2025:06:27 17:12:35&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>SONY&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>ILCE-6300&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>E 55-210mm F4.5-6.3 OSS&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>55.0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.005&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>5.6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>Adobe Photoshop Lightroom Classic 14.1.1 (Macintosh)&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=44.721049,-110.47937&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/img_4061/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/img_4061/</guid><description> &amp;lt; All Photos&amp;lt; return
Torrey Pines State Reserve (San Diego) #san-diego#travel
DateTimeOriginal 2024:12:10 15:37:33 OffsetTimeOriginal -08:00 Make Apple Model iPhone 15 Pro Max LensModel iPhone 15 Pro Max back triple camera 15.66mm f/2.8 FocalLength 15.659999847383 ExposureTime 0.0007727975270479134 FNumber 2.8 ISOSpeedRatings 50 Software 18.1.1</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="torrey-pines-state-reserve-san-diego">Torrey Pines State Reserve (San Diego)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/IMG_4061.jpeg" alt="Torrey Pines State Reserve" class="detail-image">
&lt;table>
&lt;colgroup>
&lt;col style="width: min-content;">
&lt;col>
&lt;/colgroup>
&lt;tr>
&lt;td>&lt;b>DateTimeOriginal&lt;/b>&lt;/td>
&lt;td>2024:12:10 15:37:33&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>OffsetTimeOriginal&lt;/b>&lt;/td>
&lt;td>-08:00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Make&lt;/b>&lt;/td>
&lt;td>Apple&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Model&lt;/b>&lt;/td>
&lt;td>iPhone 15 Pro Max&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>LensModel&lt;/b>&lt;/td>
&lt;td>iPhone 15 Pro Max back triple camera 15.66mm f/2.8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FocalLength&lt;/b>&lt;/td>
&lt;td>15.659999847383&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ExposureTime&lt;/b>&lt;/td>
&lt;td>0.0007727975270479134&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>FNumber&lt;/b>&lt;/td>
&lt;td>2.8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>ISOSpeedRatings&lt;/b>&lt;/td>
&lt;td>50&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;b>Software&lt;/b>&lt;/td>
&lt;td>18.1.1&lt;/td>
&lt;/tr>
&lt;/table>
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=32.922342,-117.258842&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/detail/polaroid-mesquite/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/detail/polaroid-mesquite/</guid><description> &amp;lt; All Photos&amp;lt; return
Mesquite Flat Sand Dunes (Death Valley National Park) #travel#death-valley#national-park#polaroid
approximated</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-detail.css">
&lt;style>
table,
th,
td {
border: none;
}
&lt;p>td {
padding-left: 0;
padding-right: 20px;
}
&lt;/style>&lt;/p>
&lt;p>&lt;a href="https://yeshe.ng/blog/gallery" id="back-all">&amp;lt; All Photos&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/map" id="back-map" hidden>&amp;lt; return&lt;/a>&lt;/p>
&lt;script>
// opened from the map page
if (new URLSearchParams(location.search).get("return") === "map") {
document.getElementById("back-all").hidden = true;
document.getElementById("back-map").hidden = false;
}
&lt;/script>
&lt;h2 id="mesquite-flat-sand-dunes-death-valley-national-park">Mesquite Flat Sand Dunes (Death Valley National Park)&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;/p>
&lt;img src="https://yeshe.ng/blog/gallery/original/polaroid-mesquite.jpg" alt="Mesquite Flat Sand Dunes" class="detail-image">
&lt;div class="detail-map">&lt;iframe src="https://maps.google.com/maps?q=36.615122,-117.113144&amp;amp;z=5&amp;amp;output=embed" loading="lazy" title="Map">&lt;/iframe>&lt;/div>
&lt;div class="map-note">approximated&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/formula1/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/formula1/</guid><description>&amp;lt; All Photos
#formula1 2023/9/14 - 2024/4/21
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Chinese GP 2024 Race - GS AShanghai, 2024/4/21 Chinese GP 2024 Race - GS AShanghai, 2024/4/21 Chinese GP 2024 Sprint - GS AShanghai, 2024/4/20 Chinese GP 2024 Free Practice - GS HShanghai, 2024/4/19 Singapore GP Pit LaneSingapore, 2023/9/14</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="formula1">#formula1&lt;/h2>
&lt;p>2023/9/14 - 2024/4/21&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;b style="margin-right:1em">#formula1&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc02254">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC02254.webp">
&lt;div class="image-caption ">Chinese GP 2024 Race - GS A&lt;/br>&lt;span>Shanghai, 2024/4/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc02170">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC02170.webp">
&lt;div class="image-caption ">Chinese GP 2024 Race - GS A&lt;/br>&lt;span>Shanghai, 2024/4/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc01869">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC01869.webp">
&lt;div class="image-caption ">Chinese GP 2024 Sprint - GS A&lt;/br>&lt;span>Shanghai, 2024/4/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc01646">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC01646.webp">
&lt;div class="image-caption ">Chinese GP 2024 Free Practice - GS H&lt;/br>&lt;span>Shanghai, 2024/4/19&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc01262">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC01262.webp">
&lt;div class="image-caption ">Singapore GP Pit Lane&lt;/br>&lt;span>Singapore, 2023/9/14&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/grand-canyon/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/grand-canyon/</guid><description>&amp;lt; All Photos
#grand-canyon 2025/6/17 - 2025/6/17
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Point ImperialGrand Canyon National Park, 2025/6/17</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="grand-canyon">#grand-canyon&lt;/h2>
&lt;p>2025/6/17 - 2025/6/17&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;b style="margin-right:1em">#grand-canyon&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03343">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03343.webp">
&lt;div class="image-caption ">Point Imperial&lt;/br>&lt;span>Grand Canyon National Park, 2025/6/17&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/grand-teton/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/grand-teton/</guid><description>&amp;lt; All Photos
#grand-teton 2025/6/25 - 2025/6/25
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Taggart LakeGrand Teton National Park, 2025/6/25 Taggart Lake Loop TrailGrand Teton National Park, 2025/6/25</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="grand-teton">#grand-teton&lt;/h2>
&lt;p>2025/6/25 - 2025/6/25&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;b style="margin-right:1em">#grand-teton&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03551">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03551.webp">
&lt;div class="image-caption ">Taggart Lake&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03541">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03541.webp">
&lt;div class="image-caption ">Taggart Lake Loop Trail&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/hwy1/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/hwy1/</guid><description>&amp;lt; All Photos
#hwy1 2026/5/20 - 2026/5/20
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Soberanes PointHighway 1, 2026/5/20 McWay FallsHighway 1, 2026/5/20</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="hwy1">#hwy1&lt;/h2>
&lt;p>2026/5/20 - 2026/5/20&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;b style="margin-right:1em">#hwy1&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00192">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00192.webp">
&lt;div class="image-caption ">Soberanes Point&lt;/br>&lt;span>Highway 1, 2026/5/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00177">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00177.webp">
&lt;div class="image-caption ">McWay Falls&lt;/br>&lt;span>Highway 1, 2026/5/20&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/london/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/london/</guid><description>&amp;lt; All Photos
#london 2026/4/17 - 2026/4/17
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Big BenLondon, 2026/4/17</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="london">#london&lt;/h2>
&lt;p>2026/4/17 - 2026/4/17&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;b style="margin-right:1em">#london&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00150">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00150.webp">
&lt;div class="image-caption ">Big Ben&lt;/br>&lt;span>London, 2026/4/17&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/map/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/map/</guid><description> &amp;lt; return
Map</description><content>&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery-map.css">
&lt;p class="map-return">&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; return&lt;/a>&lt;/p>
&lt;h2 id="map">Map&lt;/h2>
&lt;div id="globe">&lt;/div>
&lt;div id="globe-photos" hidden>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc00381" data-lat="37.589226" data-lng="-118.859157" title="Convict Lake, Eastern Sierra">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC00381.webp" alt="Convict Lake">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc00221" data-lat="37.579256" data-lng="-121.98902" title="Niles Boulevard, Niles">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC00221.webp" alt="Niles Boulevard">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc00192" data-lat="36.45283" data-lng="-121.929376" title="Soberanes Point, Highway 1">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC00192.webp" alt="Soberanes Point">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc00177" data-lat="36.157826" data-lng="-121.672163" title="McWay Falls, Highway 1">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC00177.webp" alt="McWay Falls">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc00150" data-lat="51.500729" data-lng="-0.124625" title="Big Ben, London">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC00150.webp" alt="Big Ben">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03640" data-lat="44.721049" data-lng="-110.47937" title="Artist Point, Yellowstone National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03640.webp" alt="Artist Point">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03551" data-lat="43.703718" data-lng="-110.755502" title="Taggart Lake, Grand Teton National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03551.webp" alt="Taggart Lake">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03541" data-lat="43.692957" data-lng="-110.739584" title="Taggart Lake Loop Trail, Grand Teton National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03541.webp" alt="Taggart Lake Loop Trail">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03404" data-lat="38.790473" data-lng="-109.60713" title="Landscape Arch, Arches National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03404.webp" alt="Landscape Arch">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03361" data-lat="37.62064" data-lng="-112.163771" title="Navajo Loop Trail, Bryce Canyon National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03361.webp" alt="Navajo Loop Trail">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03343" data-lat="36.278957" data-lng="-111.977646" title="Point Imperial, Grand Canyon National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03343.webp" alt="Point Imperial">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03277" data-lat="37.715799" data-lng="-119.677483" title="Tunnel View, Yosemite National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03277.webp" alt="Tunnel View">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03265" data-lat="36.420067" data-lng="-116.81223" title="Zabriskie Point, Death Valley National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03265.webp" alt="Zabriskie Point">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/polaroid-mesquite" data-lat="36.615122" data-lng="-117.113144" title="Mesquite Flat Sand Dunes, Death Valley National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/polaroid-mesquite.webp" alt="Mesquite Flat Sand Dunes">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03172" data-lat="36.615122" data-lng="-117.113144" title="Mesquite Flat Sand Dunes, Death Valley National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03172.webp" alt="Mesquite Flat Sand Dunes">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03114" data-lat="36.246084" data-lng="-116.818508" title="Badwater Basin, Death Valley National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03114.webp" alt="Badwater Basin">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03098" data-lat="36.367728" data-lng="-116.803384" title="Artist&amp;#39;s Palette, Death Valley National Park">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03098.webp" alt="Artist&amp;#39;s Palette">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03054" data-lat="33.873415" data-lng="-115.900992" title="Joshua Tree National Park, Twentynine Palms">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03054.webp" alt="Joshua Tree National Park">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc03005" data-lat="32.715915" data-lng="-117.255168" title="Sunset Cliff, San Diego">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC03005.webp" alt="Sunset Cliff">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc02980" data-lat="32.920935" data-lng="-117.251444" title="Torrey Pines State Reserve, San Diego">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC02980.webp" alt="Torrey Pines State Reserve">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/img_4061" data-lat="32.922342" data-lng="-117.258842" title="Torrey Pines State Reserve, San Diego">&lt;img src="https://yeshe.ng/blog/gallery/thumb/IMG_4061.webp" alt="Torrey Pines State Reserve">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc02254" data-lat="31.3372" data-lng="121.2206" title="Chinese GP 2024 Race - GS A, Shanghai">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC02254.webp" alt="Chinese GP 2024 Race - GS A">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc02170" data-lat="31.3372" data-lng="121.2206" title="Chinese GP 2024 Race - GS A, Shanghai">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC02170.webp" alt="Chinese GP 2024 Race - GS A">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc01869" data-lat="31.3372" data-lng="121.2206" title="Chinese GP 2024 Sprint - GS A, Shanghai">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC01869.webp" alt="Chinese GP 2024 Sprint - GS A">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc01646" data-lat="31.3371" data-lng="121.2274" title="Chinese GP 2024 Free Practice - GS H, Shanghai">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC01646.webp" alt="Chinese GP 2024 Free Practice - GS H">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc01262" data-lat="1.291363" data-lng="103.863892" title="Singapore GP Pit Lane, Singapore">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC01262.webp" alt="Singapore GP Pit Lane">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc00821" data-lat="1.286745" data-lng="103.854387" title="Merlion Park, Singapore">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC00821.webp" alt="Merlion Park">&lt;/a>
&lt;a href="https://yeshe.ng/blog/galleries/detail/dsc00558" data-lat="1.281568" data-lng="103.863613" title="Gardens by the Bay, Singapore">&lt;img src="https://yeshe.ng/blog/gallery/thumb/DSC00558.webp" alt="Gardens by the Bay">&lt;/a>
&lt;/div>
&lt;script src="https://cdn.jsdelivr.net/npm/globe.gl@2.46.2/dist/globe.gl.min.js">&lt;/script>
&lt;script src="https://cdn.jsdelivr.net/npm/topojson-client@3.1.0/dist/topojson-client.min.js">&lt;/script>
&lt;script src="https://yeshe.ng/blog/js/gallery-map.js">&lt;/script></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/national-park/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/national-park/</guid><description>&amp;lt; All Photos
#national-park 2025/1/15 - 2025/6/27
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Artist PointYellowstone National Park, 2025/6/27 Taggart LakeGrand Teton National Park, 2025/6/25 Taggart Lake Loop TrailGrand Teton National Park, 2025/6/25 Landscape ArchArches National Park, 2025/6/21 Navajo Loop TrailBryce Canyon National Park, 2025/6/20 Point ImperialGrand Canyon National Park, 2025/6/17 Tunnel ViewYosemite National Park, 2025/6/14 Zabriskie PointDeath Valley National Park, 2025/3/21 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Badwater BasinDeath Valley National Park, 2025/3/20 Artist's PaletteDeath Valley National Park, 2025/3/20 Joshua Tree National ParkTwentynine Palms, 2025/1/15</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="national-park">#national-park&lt;/h2>
&lt;p>2025/1/15 - 2025/6/27&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;b style="margin-right:1em">#national-park&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03640">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03640.webp">
&lt;div class="image-caption ">Artist Point&lt;/br>&lt;span>Yellowstone National Park, 2025/6/27&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03551">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03551.webp">
&lt;div class="image-caption ">Taggart Lake&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03541">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03541.webp">
&lt;div class="image-caption ">Taggart Lake Loop Trail&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03404">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03404.webp">
&lt;div class="image-caption ">Landscape Arch&lt;/br>&lt;span>Arches National Park, 2025/6/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03361">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03361.webp">
&lt;div class="image-caption ">Navajo Loop Trail&lt;/br>&lt;span>Bryce Canyon National Park, 2025/6/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03343">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03343.webp">
&lt;div class="image-caption ">Point Imperial&lt;/br>&lt;span>Grand Canyon National Park, 2025/6/17&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03277">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03277.webp">
&lt;div class="image-caption ">Tunnel View&lt;/br>&lt;span>Yosemite National Park, 2025/6/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03265">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03265.webp">
&lt;div class="image-caption ">Zabriskie Point&lt;/br>&lt;span>Death Valley National Park, 2025/3/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/polaroid-mesquite">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/polaroid-mesquite.webp">
&lt;div class="image-caption polaroid">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03172">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03172.webp">
&lt;div class="image-caption ">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03114">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03114.webp">
&lt;div class="image-caption ">Badwater Basin&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03098">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03098.webp">
&lt;div class="image-caption ">Artist's Palette&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03054">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03054.webp">
&lt;div class="image-caption ">Joshua Tree National Park&lt;/br>&lt;span>Twentynine Palms, 2025/1/15&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/polaroid/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/polaroid/</guid><description>&amp;lt; All Photos
#polaroid 2025/3/20 - 2025/3/20
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="polaroid">#polaroid&lt;/h2>
&lt;p>2025/3/20 - 2025/3/20&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;b style="margin-right:1em">#polaroid&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/polaroid-mesquite">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/polaroid-mesquite.webp">
&lt;div class="image-caption polaroid">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/san-diego/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/san-diego/</guid><description>&amp;lt; All Photos
#san-diego 2024/12/10 - 2025/1/14
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Sunset CliffSan Diego, 2025/1/14 Torrey Pines State ReserveSan Diego, 2025/1/7 Torrey Pines State ReserveSan Diego, 2024/12/10</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="san-diego">#san-diego&lt;/h2>
&lt;p>2024/12/10 - 2025/1/14&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;b style="margin-right:1em">#san-diego&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03005">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03005.webp">
&lt;div class="image-caption ">Sunset Cliff&lt;/br>&lt;span>San Diego, 2025/1/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc02980">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC02980.webp">
&lt;div class="image-caption ">Torrey Pines State Reserve&lt;/br>&lt;span>San Diego, 2025/1/7&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/img_4061">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/IMG_4061.webp">
&lt;div class="image-caption ">Torrey Pines State Reserve&lt;/br>&lt;span>San Diego, 2024/12/10&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/sierra/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/sierra/</guid><description>&amp;lt; All Photos
#sierra 2025/6/14 - 2026/9/13
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Convict LakeEastern Sierra, 2026/9/13 Tunnel ViewYosemite National Park, 2025/6/14</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="sierra">#sierra&lt;/h2>
&lt;p>2025/6/14 - 2026/9/13&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;b style="margin-right:1em">#sierra&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00381">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00381.webp">
&lt;div class="image-caption ">Convict Lake&lt;/br>&lt;span>Eastern Sierra, 2026/9/13&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03277">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03277.webp">
&lt;div class="image-caption ">Tunnel View&lt;/br>&lt;span>Yosemite National Park, 2025/6/14&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/singapore/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/singapore/</guid><description>&amp;lt; All Photos
#singapore 2023/7/30 - 2023/9/14
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Singapore GP Pit LaneSingapore, 2023/9/14 Merlion ParkSingapore, 2023/8/9 Gardens by the BaySingapore, 2023/7/30</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="singapore">#singapore&lt;/h2>
&lt;p>2023/7/30 - 2023/9/14&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;b style="margin-right:1em">#singapore&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc01262">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC01262.webp">
&lt;div class="image-caption ">Singapore GP Pit Lane&lt;/br>&lt;span>Singapore, 2023/9/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00821">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00821.webp">
&lt;div class="image-caption ">Merlion Park&lt;/br>&lt;span>Singapore, 2023/8/9&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00558">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00558.webp">
&lt;div class="image-caption ">Gardens by the Bay&lt;/br>&lt;span>Singapore, 2023/7/30&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/travel/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/travel/</guid><description>&amp;lt; All Photos
#travel 2023/7/30 - 2026/9/13
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Convict LakeEastern Sierra, 2026/9/13 Niles BoulevardNiles, 2026/5/24 Soberanes PointHighway 1, 2026/5/20 McWay FallsHighway 1, 2026/5/20 Big BenLondon, 2026/4/17 Artist PointYellowstone National Park, 2025/6/27 Taggart LakeGrand Teton National Park, 2025/6/25 Taggart Lake Loop TrailGrand Teton National Park, 2025/6/25 Landscape ArchArches National Park, 2025/6/21 Navajo Loop TrailBryce Canyon National Park, 2025/6/20 Point ImperialGrand Canyon National Park, 2025/6/17 Tunnel ViewYosemite National Park, 2025/6/14 Zabriskie PointDeath Valley National Park, 2025/3/21 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Badwater BasinDeath Valley National Park, 2025/3/20 Artist's PaletteDeath Valley National Park, 2025/3/20 Joshua Tree National ParkTwentynine Palms, 2025/1/15 Sunset CliffSan Diego, 2025/1/14 Torrey Pines State ReserveSan Diego, 2025/1/7 Torrey Pines State ReserveSan Diego, 2024/12/10 Merlion ParkSingapore, 2023/8/9 Gardens by the BaySingapore, 2023/7/30</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="travel">#travel&lt;/h2>
&lt;p>2023/7/30 - 2026/9/13&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;b style="margin-right:1em">#travel&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00381">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00381.webp">
&lt;div class="image-caption ">Convict Lake&lt;/br>&lt;span>Eastern Sierra, 2026/9/13&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00221">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00221.webp">
&lt;div class="image-caption ">Niles Boulevard&lt;/br>&lt;span>Niles, 2026/5/24&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00192">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00192.webp">
&lt;div class="image-caption ">Soberanes Point&lt;/br>&lt;span>Highway 1, 2026/5/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00177">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00177.webp">
&lt;div class="image-caption ">McWay Falls&lt;/br>&lt;span>Highway 1, 2026/5/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00150">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00150.webp">
&lt;div class="image-caption ">Big Ben&lt;/br>&lt;span>London, 2026/4/17&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03640">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03640.webp">
&lt;div class="image-caption ">Artist Point&lt;/br>&lt;span>Yellowstone National Park, 2025/6/27&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03551">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03551.webp">
&lt;div class="image-caption ">Taggart Lake&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03541">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03541.webp">
&lt;div class="image-caption ">Taggart Lake Loop Trail&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03404">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03404.webp">
&lt;div class="image-caption ">Landscape Arch&lt;/br>&lt;span>Arches National Park, 2025/6/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03361">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03361.webp">
&lt;div class="image-caption ">Navajo Loop Trail&lt;/br>&lt;span>Bryce Canyon National Park, 2025/6/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03343">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03343.webp">
&lt;div class="image-caption ">Point Imperial&lt;/br>&lt;span>Grand Canyon National Park, 2025/6/17&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03277">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03277.webp">
&lt;div class="image-caption ">Tunnel View&lt;/br>&lt;span>Yosemite National Park, 2025/6/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03265">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03265.webp">
&lt;div class="image-caption ">Zabriskie Point&lt;/br>&lt;span>Death Valley National Park, 2025/3/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/polaroid-mesquite">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/polaroid-mesquite.webp">
&lt;div class="image-caption polaroid">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03172">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03172.webp">
&lt;div class="image-caption ">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03114">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03114.webp">
&lt;div class="image-caption ">Badwater Basin&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03098">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03098.webp">
&lt;div class="image-caption ">Artist's Palette&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03054">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03054.webp">
&lt;div class="image-caption ">Joshua Tree National Park&lt;/br>&lt;span>Twentynine Palms, 2025/1/15&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03005">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03005.webp">
&lt;div class="image-caption ">Sunset Cliff&lt;/br>&lt;span>San Diego, 2025/1/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc02980">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC02980.webp">
&lt;div class="image-caption ">Torrey Pines State Reserve&lt;/br>&lt;span>San Diego, 2025/1/7&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/img_4061">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/IMG_4061.webp">
&lt;div class="image-caption ">Torrey Pines State Reserve&lt;/br>&lt;span>San Diego, 2024/12/10&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00821">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00821.webp">
&lt;div class="image-caption ">Merlion Park&lt;/br>&lt;span>Singapore, 2023/8/9&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00558">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00558.webp">
&lt;div class="image-caption ">Gardens by the Bay&lt;/br>&lt;span>Singapore, 2023/7/30&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/yellowstone/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/yellowstone/</guid><description>&amp;lt; All Photos
#yellowstone 2025/6/27 - 2025/6/27
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Artist PointYellowstone National Park, 2025/6/27</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="yellowstone">#yellowstone&lt;/h2>
&lt;p>2025/6/27 - 2025/6/27&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;b style="margin-right:1em">#yellowstone&lt;/b>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03640">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03640.webp">
&lt;div class="image-caption ">Artist Point&lt;/br>&lt;span>Yellowstone National Park, 2025/6/27&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/galleries/yosemite/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/galleries/yosemite/</guid><description>&amp;lt; All Photos
#yosemite 2025/6/14 - 2025/6/14
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Tunnel ViewYosemite National Park, 2025/6/14</description><content>&lt;p>&lt;a href="https://yeshe.ng/blog/gallery">&amp;lt; All Photos&lt;/a>&lt;/p>
&lt;h2 id="yosemite">#yosemite&lt;/h2>
&lt;p>2025/6/14 - 2025/6/14&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;b style="margin-right:1em">#yosemite&lt;/b>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03277">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03277.webp">
&lt;div class="image-caption ">Tunnel View&lt;/br>&lt;span>Yosemite National Park, 2025/6/14&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>Gallery</title><link>https://yeshe.ng/blog/gallery/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/gallery/</guid><description>All Photos Map &amp;gt;
#arches#bayarea#bryce-canyon#death-valley#formula1#grand-canyon#grand-teton#hwy1#london#national-park#polaroid#san-diego#sierra#singapore#travel#yellowstone#yosemite
Convict LakeEastern Sierra, 2026/9/13 Niles BoulevardNiles, 2026/5/24 Soberanes PointHighway 1, 2026/5/20 McWay FallsHighway 1, 2026/5/20 Big BenLondon, 2026/4/17 Artist PointYellowstone National Park, 2025/6/27 Taggart LakeGrand Teton National Park, 2025/6/25 Taggart Lake Loop TrailGrand Teton National Park, 2025/6/25 Landscape ArchArches National Park, 2025/6/21 Navajo Loop TrailBryce Canyon National Park, 2025/6/20 Point ImperialGrand Canyon National Park, 2025/6/17 Tunnel ViewYosemite National Park, 2025/6/14 Zabriskie PointDeath Valley National Park, 2025/3/21 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Mesquite Flat Sand DunesDeath Valley National Park, 2025/3/20 Badwater BasinDeath Valley National Park, 2025/3/20 Artist's PaletteDeath Valley National Park, 2025/3/20 Joshua Tree National ParkTwentynine Palms, 2025/1/15 Sunset CliffSan Diego, 2025/1/14 Torrey Pines State ReserveSan Diego, 2025/1/7 Torrey Pines State ReserveSan Diego, 2024/12/10 Chinese GP 2024 Race - GS AShanghai, 2024/4/21 Chinese GP 2024 Race - GS AShanghai, 2024/4/21 Chinese GP 2024 Sprint - GS AShanghai, 2024/4/20 Chinese GP 2024 Free Practice - GS HShanghai, 2024/4/19 Singapore GP Pit LaneSingapore, 2023/9/14 Merlion ParkSingapore, 2023/8/9 Gardens by the BaySingapore, 2023/7/30</description><content>&lt;h2 id="all-photos">All Photos&lt;/h2>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/map">Map &amp;gt;&lt;/a>&lt;/p>
&lt;link rel="stylesheet" href="https://yeshe.ng/blog/css/gallery.css">
&lt;script>
const loadScript = (url) => {
return new Promise((resolve, reject) => {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = resolve;
script.onerror = reject;
head.appendChild(script);
});
};
document.addEventListener("DOMContentLoaded", async () => {
try {
// Load Masonry
await loadScript("/blog/js/masonry.pkgd.min.js");
// Load imagesLoaded plugin
await loadScript("/blog/js/imagesloaded.pkgd.min.js");
const grid = document.querySelector('.image-grid');
// Initialize Masonry but don't layout yet
const masonry = new Masonry(grid, {
itemSelector: '.gallery-container',
transitionDuration: 0,
gutter: 8
});
// Wait for all images to load, then layout
imagesLoaded(grid).on('progress', () => {
// Layout Masonry after each image loads
masonry.layout();
}).on('done', () => {
// Final layout when all images have loaded
masonry.layout();
});
} catch (error) {
console.error("Failed to load scripts:", error);
}
});
&lt;/script>
&lt;p>&lt;a href="https://yeshe.ng/blog/galleries/arches" style="margin-right:1em">#arches&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bayarea" style="margin-right:1em">#bayarea&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/bryce-canyon" style="margin-right:1em">#bryce-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/death-valley" style="margin-right:1em">#death-valley&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/formula1" style="margin-right:1em">#formula1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-canyon" style="margin-right:1em">#grand-canyon&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/grand-teton" style="margin-right:1em">#grand-teton&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/hwy1" style="margin-right:1em">#hwy1&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/london" style="margin-right:1em">#london&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/national-park" style="margin-right:1em">#national-park&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/polaroid" style="margin-right:1em">#polaroid&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/san-diego" style="margin-right:1em">#san-diego&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/sierra" style="margin-right:1em">#sierra&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/singapore" style="margin-right:1em">#singapore&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/travel" style="margin-right:1em">#travel&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yellowstone" style="margin-right:1em">#yellowstone&lt;/a>&lt;a href="https://yeshe.ng/blog/galleries/yosemite" style="margin-right:1em">#yosemite&lt;/a>&lt;/p>
&lt;div class="image-grid"
data-masonry='{ "itemSelector": ".gallery-container", "transitionDuration": 0, "horizontalOrder": true, "gutter": 8 }'>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00381">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00381.webp">
&lt;div class="image-caption ">Convict Lake&lt;/br>&lt;span>Eastern Sierra, 2026/9/13&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00221">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00221.webp">
&lt;div class="image-caption ">Niles Boulevard&lt;/br>&lt;span>Niles, 2026/5/24&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00192">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00192.webp">
&lt;div class="image-caption ">Soberanes Point&lt;/br>&lt;span>Highway 1, 2026/5/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00177">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00177.webp">
&lt;div class="image-caption ">McWay Falls&lt;/br>&lt;span>Highway 1, 2026/5/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00150">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00150.webp">
&lt;div class="image-caption ">Big Ben&lt;/br>&lt;span>London, 2026/4/17&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03640">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03640.webp">
&lt;div class="image-caption ">Artist Point&lt;/br>&lt;span>Yellowstone National Park, 2025/6/27&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03551">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03551.webp">
&lt;div class="image-caption ">Taggart Lake&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03541">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03541.webp">
&lt;div class="image-caption ">Taggart Lake Loop Trail&lt;/br>&lt;span>Grand Teton National Park, 2025/6/25&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03404">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03404.webp">
&lt;div class="image-caption ">Landscape Arch&lt;/br>&lt;span>Arches National Park, 2025/6/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03361">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03361.webp">
&lt;div class="image-caption ">Navajo Loop Trail&lt;/br>&lt;span>Bryce Canyon National Park, 2025/6/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03343">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03343.webp">
&lt;div class="image-caption ">Point Imperial&lt;/br>&lt;span>Grand Canyon National Park, 2025/6/17&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03277">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03277.webp">
&lt;div class="image-caption ">Tunnel View&lt;/br>&lt;span>Yosemite National Park, 2025/6/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03265">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03265.webp">
&lt;div class="image-caption ">Zabriskie Point&lt;/br>&lt;span>Death Valley National Park, 2025/3/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/polaroid-mesquite">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/polaroid-mesquite.webp">
&lt;div class="image-caption polaroid">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03172">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03172.webp">
&lt;div class="image-caption ">Mesquite Flat Sand Dunes&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03114">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03114.webp">
&lt;div class="image-caption ">Badwater Basin&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03098">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03098.webp">
&lt;div class="image-caption ">Artist's Palette&lt;/br>&lt;span>Death Valley National Park, 2025/3/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03054">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03054.webp">
&lt;div class="image-caption ">Joshua Tree National Park&lt;/br>&lt;span>Twentynine Palms, 2025/1/15&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc03005">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC03005.webp">
&lt;div class="image-caption ">Sunset Cliff&lt;/br>&lt;span>San Diego, 2025/1/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc02980">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC02980.webp">
&lt;div class="image-caption ">Torrey Pines State Reserve&lt;/br>&lt;span>San Diego, 2025/1/7&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/img_4061">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/IMG_4061.webp">
&lt;div class="image-caption ">Torrey Pines State Reserve&lt;/br>&lt;span>San Diego, 2024/12/10&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc02254">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC02254.webp">
&lt;div class="image-caption ">Chinese GP 2024 Race - GS A&lt;/br>&lt;span>Shanghai, 2024/4/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc02170">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC02170.webp">
&lt;div class="image-caption ">Chinese GP 2024 Race - GS A&lt;/br>&lt;span>Shanghai, 2024/4/21&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc01869">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC01869.webp">
&lt;div class="image-caption ">Chinese GP 2024 Sprint - GS A&lt;/br>&lt;span>Shanghai, 2024/4/20&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc01646">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC01646.webp">
&lt;div class="image-caption ">Chinese GP 2024 Free Practice - GS H&lt;/br>&lt;span>Shanghai, 2024/4/19&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc01262">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC01262.webp">
&lt;div class="image-caption ">Singapore GP Pit Lane&lt;/br>&lt;span>Singapore, 2023/9/14&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00821">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00821.webp">
&lt;div class="image-caption ">Merlion Park&lt;/br>&lt;span>Singapore, 2023/8/9&lt;/span>&lt;/div>
&lt;/a>
&lt;a class="gallery-container" href="https://yeshe.ng/blog/galleries/detail/dsc00558">
&lt;img class="gallery-image" src="https://yeshe.ng/blog/gallery/preview/DSC00558.webp">
&lt;div class="image-caption ">Gardens by the Bay&lt;/br>&lt;span>Singapore, 2023/7/30&lt;/span>&lt;/div>
&lt;/a>
&lt;/div></content></item><item><title>关于</title><link>https://yeshe.ng/blog/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>liang2kl@outlook.com (Liang Yesheng)</author><guid>https://yeshe.ng/blog/about/</guid><description>关于我 在这些地方读过书：
THU 计算机科学与技术，学士学位 UCSD Master in Computer Science and Engineering (in prgress) 喜欢：
iOS 开发、前后端、高性能计算、其他任何有意思的技术 看足球（巴萨） 看 F1（主要是看乐子） 玩游戏 听 Coldplay 皮克斯动画 一些美剧（如寻妈记、老友记、Severance） 一点点摄影 大多数 Apple 产品 编程语言：
非常熟练： 比较熟练： 项目 Whiz Reader iOS App 闭源｜App Store 链接 2020.4 至今（其实很久没有维护了） 第一个项目，C++ 都还没学会的时候的产物。一个英文阅读器，支持点击查词、划词实时翻译等，本来想的是写来看生物原版教材，没想到最终让我来到了计算机系。用的人还不少，也赚了一点小钱。
libftm Linux 程序 开源｜GitHub 链接 2020.12 - 2021.1 参加软院 SRT 时候写的程序，用 Wi-Fi 网卡进行测距/定位，花了不少时间。实现基本上是抄 iw 的。SRT 最后无疾而终，但是学会了不少 Linux 知识。
treehollow-v3-ios iOS App 开源｜GitHub 链接 2021.1 - 2021.12 树洞 iOS 客户端。写过的规模最大、用户最多的的项目，可惜无法继续更新了。写的过程中学会了不少东西，算是很有收获。</description><content>&lt;h2 id="关于我">关于我&lt;/h2>
&lt;style>
.lang-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
margin-top: 5px;
}
.inline {
height: 1.2em;
display: inline;
vertical-align:middle;
}
.white-bg {
background-color:white;
border-radius:0.3em;
padding:0.1em 0.1em 0.1em 0.1em;
}
&lt;/style>
&lt;p>在这些地方读过书：&lt;/p>
&lt;ul>
&lt;li>&lt;span class="white-bg" style="color:black;">THU  &lt;img class="inline" src="https://yeshe.ng/blog/icon/thu.svg"/>&lt;/span> 计算机科学与技术，学士学位&lt;/li>
&lt;li>&lt;span class="white-bg" style="color:black;">UCSD &lt;img class="inline" src="https://yeshe.ng/blog/icon/ucsd-seal.svg"/>&lt;/span> Master in Computer Science and Engineering (in prgress)&lt;/li>
&lt;/ul>
&lt;p>喜欢：&lt;/p>
&lt;ul>
&lt;li>iOS 开发、前后端、高性能计算、其他任何有意思的技术&lt;/li>
&lt;li>看足球（巴萨）&lt;/li>
&lt;li>看 F1（主要是看乐子）&lt;/li>
&lt;li>玩&lt;a href="https://backloggd.com/u/liang2kl/journal/activity/">游戏&lt;/a>&lt;/li>
&lt;li>听 Coldplay&lt;/li>
&lt;li>皮克斯动画&lt;/li>
&lt;li>一些美剧（如寻妈记、老友记、Severance）&lt;/li>
&lt;li>一点点&lt;a href="https://yeshe.ng/blog/gallery">摄影&lt;/a>&lt;/li>
&lt;li>大多数 Apple 产品&lt;/li>
&lt;/ul>
&lt;!-- 想要：
- 到世界各地旅游
- 一辈子做自己喜欢做的事情 -->
&lt;p>编程语言：&lt;/p>
&lt;ul>
&lt;li>
&lt;span class="lang-grid">
非常熟练：
&lt;img src="https://img.shields.io/badge/C-00599C?style=for-the-badge&amp;logo=c&amp;logoColor=white" alt="c">
&lt;img src="https://img.shields.io/badge/C%2B%2B-00599C?style=for-the-badge&amp;logo=c%2B%2B&amp;logoColor=white" alt="c++">
&lt;img src="https://img.shields.io/badge/Swift-FA7343?style=for-the-badge&amp;logo=swift&amp;logoColor=white" alt="swift">
&lt;img src="https://img.shields.io/badge/Python-3776AB?style=for-the-badge&amp;logo=python&amp;logoColor=white" alt="python">
&lt;/span>
&lt;/li>
&lt;li>
&lt;div class="lang-grid">
比较熟练：
&lt;img src="https://img.shields.io/badge/typescript-%23007ACC?style=for-the-badge&amp;logo=typescript&amp;logoColor=white" alt="typescript">
&lt;img src="https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&amp;logo=JavaScript&amp;logoColor=black" alt="javascript">
&lt;img src="https://img.shields.io/badge/Rust-000000?style=for-the-badge&amp;logo=rust&amp;logoColor=white" alt="rust">
&lt;img src="https://img.shields.io/badge/Go-00ADD8?style=for-the-badge&amp;logo=go&amp;logoColor=white" alt="go">
&lt;/div>
&lt;/li>
&lt;/ul>
&lt;h2 id="项目">项目&lt;/h2>
&lt;h4 id="whiz-reader">Whiz Reader&lt;/h4>
&lt;ul>
&lt;li>iOS App&lt;/li>
&lt;li>闭源｜&lt;a href="https://apps.apple.com/cn/app/whiz-reader/id1514342771">App Store 链接&lt;/a>&lt;/li>
&lt;li>2020.4 至今（其实很久没有维护了）&lt;/li>
&lt;/ul>
&lt;p>第一个项目，C++ 都还没学会的时候的产物。一个英文阅读器，支持点击查词、划词实时翻译等，本来想的是写来看生物原版教材，没想到最终让我来到了计算机系。用的人还不少，也赚了一点小钱。&lt;/p>
&lt;h4 id="libftm">libftm&lt;/h4>
&lt;ul>
&lt;li>Linux 程序&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/libftm">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2020.12 - 2021.1&lt;/li>
&lt;/ul>
&lt;p>参加软院 SRT 时候写的程序，用 Wi-Fi 网卡进行测距/定位，花了不少时间。实现基本上是抄 &lt;a href="https://wireless.wiki.kernel.org/en/users/documentation/iw">iw&lt;/a> 的。SRT 最后无疾而终，但是学会了不少 Linux 知识。&lt;/p>
&lt;h4 id="treehollow-v3-ios">treehollow-v3-ios&lt;/h4>
&lt;ul>
&lt;li>iOS App&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/treehollow/treehollow-v3-ios">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2021.1 - 2021.12&lt;/li>
&lt;/ul>
&lt;p>树洞 iOS 客户端。写过的规模最大、用户最多的的项目，可惜无法继续更新了。写的过程中学会了不少东西，算是很有收获。&lt;/p>
&lt;h4 id="wechat-avatar-frame">wechat-avatar-frame&lt;/h4>
&lt;ul>
&lt;li>微信小程序&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/wechat-avatar-frame">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2021.4&lt;/li>
&lt;/ul>
&lt;p>给机械系学生节做的头像框小程序，1 天速成。后来人文学院和航院也用了。微信提供的接口还挺好用，就是文档写得一塌糊涂。&lt;/p>
&lt;h4 id="wqxuetang-downloader">wqxuetang-downloader&lt;/h4>
&lt;ul>
&lt;li>Chrome 插件&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/wqxuetang-downloader">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2021.4&lt;/li>
&lt;/ul>
&lt;p>用来下载文泉学堂图片的 Chrome 插件。因为是直接操作浏览器，比一般的爬虫有效。&lt;/p>
&lt;h4 id="hollowcore">HollowCore&lt;/h4>
&lt;ul>
&lt;li>Swift Package&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/wqxuetang-downloader">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2021.6-2021.9&lt;/li>
&lt;/ul>
&lt;p>树洞网络请求库，目的是将网络请求代码和树洞客户端解耦。当时，为了用这个库，重构客户端花了不少时间。&lt;/p>
&lt;h4 id="museum">Museum+&lt;/h4>
&lt;ul>
&lt;li>iOS App&lt;/li>
&lt;li>闭源｜&lt;a href="https://liang2kl.github.io/museum-plus-docs-cn.pdf">中文文档链接&lt;/a>&lt;/li>
&lt;li>2021.7 - 2021.8&lt;/li>
&lt;/ul>
&lt;p>苹果移动应用创新赛的参赛作品，利用 AR 增强博物馆游览体验，可以通过手机屏幕对艺术品实现精确、详细、可互动的讲解。没想明白为什么只拿了赛区二等奖。&lt;/p>
&lt;h4 id="pasteboard">Pasteboard&lt;/h4>
&lt;ul>
&lt;li>macOS App&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/HollowCore">GitHub 链接&lt;/a>｜&lt;a href="https://apps.apple.com/cn/app/pastyboard/id1585942797">App Store 链接&lt;/a>&lt;/li>
&lt;li>2021.8&lt;/li>
&lt;/ul>
&lt;p>一个简单的记录剪贴板记录的状态栏程序，摆脱用备忘录临时记录的方法。起因是某同学在更新到 Windows 11 后很喜欢新的剪贴板功能，向我安利，然后我就想着给 macOS 写一个。实现很简陋，性能不太行，不过记录纯文本够用。&lt;/p>
&lt;h4 id="chess">chess&lt;/h4>
&lt;ul>
&lt;li>Qt App&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/2021-Summer-Programming-Chess">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2021.8&lt;/li>
&lt;/ul>
&lt;p>小学期 Qt 大作业，支持双人联机对战的军棋游戏。纯代码实现，使用 MVVM 架构。&lt;/p>
&lt;h4 id="bilibili-crawler">bilibili-crawler&lt;/h4>
&lt;ul>
&lt;li>Python + Django + React&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/2021-Summer-Programming-Web">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2021.8&lt;/li>
&lt;/ul>
&lt;p>小学期爬虫 + 后端 + 前端大作业，爬取 B 站上的视频信息然后展示出来。超级大杂烩。&lt;/p>
&lt;h4 id="mkdocs-blogging-plugin">mkdocs-blogging-plugin&lt;/h4>
&lt;ul>
&lt;li>Mkdocs 插件&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/mkdocs-blogging-plugin">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2021.9 至今&lt;/li>
&lt;/ul>
&lt;p>使用 Mkdocs 建博客的插件，支持自动生成博客页面、使用模版进行自定义等。目前为止在 GitHub 上 star 最多、issue 最多的项目。在 GitHub 上和外国友人/国内同胞交流还挺有意思的。&lt;/p>
&lt;h4 id="swiftui-tutorial">swiftui-tutorial&lt;/h4>
&lt;ul>
&lt;li>教程&lt;/li>
&lt;li>在线｜&lt;a href="https://docs.net9.org/frontend/swiftui/">网页链接&lt;/a>&lt;/li>
&lt;li>2022.1&lt;/li>
&lt;/ul>
&lt;p>为计算机系学生科协&lt;a href="https://docs.net9.org">技能引导文档&lt;/a>编写的 SwiftUI 入门教程。自我感觉写得还不错。&lt;/p>
&lt;h4 id="config-service">config-service&lt;/h4>
&lt;ul>
&lt;li>Go 服务器程序&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/2022-Spring-SE-Config-Service">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2022.3 - 2022.5&lt;/li>
&lt;/ul>
&lt;p>字节提供的软件工程课程大作业项目，实现一个通过执行动态语言程序来动态返回应用配置的平台。&lt;/p>
&lt;h4 id="deploy-tool">deploy-tool&lt;/h4>
&lt;ul>
&lt;li>Go 服务器程序&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/deploy-tool">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2022.5&lt;/li>
&lt;/ul>
&lt;p>软工最后一次组会之前软工平台不出所料地崩了，于是把我们的项目用 docker-compose 部署到了宿舍的电脑上，通过 ddns 访问（其实完全是想玩一下，因为我们当时已经写完代码，不需要再次部署在软工平台了）。想着每次 push 之后需要手动 &lt;code>git pull&lt;/code> 和 &lt;code>docker-compose up&lt;/code> 挺麻烦，于是写了个小后端来做这件事。&lt;/p>
&lt;h4 id="swiftui-wwdc22">swiftui-wwdc22&lt;/h4>
&lt;ul>
&lt;li>SwiftUI 程序&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/swiftui-wwdc22">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2022.6&lt;/li>
&lt;/ul>
&lt;p>对 WWDC22 发布的 SwiftUI 新 API 的探索。&lt;/p>
&lt;h4 id="docker-tutorial">docker-tutorial&lt;/h4>
&lt;ul>
&lt;li>教程&lt;/li>
&lt;li>在线｜&lt;a href="https://liang2kl.github.io/2022-summer-training-docker-tutorial">网页链接&lt;/a>&lt;/li>
&lt;li>2022.7&lt;/li>
&lt;/ul>
&lt;p>在计算机系学生科协暑培上作为讲师介绍 Docker 的基本使用及原理。&lt;/p>
&lt;h4 id="simpledb">simpledb&lt;/h4>
&lt;ul>
&lt;li>C++/Python 程序&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/simpledb">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2022.10 - 2023.1&lt;/li>
&lt;/ul>
&lt;p>数据库系统概论课程大作业，一个支持基础 SQL 的数据库管理系统（DBMS）。&lt;/p>
&lt;h4 id="riscv-lab">riscv-lab&lt;/h4>
&lt;ul>
&lt;li>SystemVerilog&lt;/li>
&lt;li>公开｜&lt;a href="https://github.com/wenqi-wang20/RISCV-LAB">GitHub 链接&lt;/a>&lt;/li>
&lt;li>合作者｜王文琦 田仁睿&lt;/li>
&lt;li>2022.11 - 2022.12&lt;/li>
&lt;/ul>
&lt;p>计算机组成原理大作业，支持运行 uCore 操作系统的五级流水 RISC-V CPU。&lt;/p>
&lt;h4 id="minidecaf-compiler">minidecaf-compiler&lt;/h4>
&lt;ul>
&lt;li>C++ 程序&lt;/li>
&lt;li>公开｜&lt;a href="https://github.com/liang2kl/minidecaf-compiler">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2022.9 - 2023.1&lt;/li>
&lt;/ul>
&lt;p>编译原理大作业，一个简单语言的简单编译器。&lt;/p>
&lt;h4 id="feiyue-database">feiyue-database&lt;/h4>
&lt;ul>
&lt;li>Python 程序&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/THU-feiyue/database">GitHub 链接&lt;/a>｜&lt;a href="https://database.feiyue.online">网页&lt;/a>&lt;/li>
&lt;li>2023.12 至今&lt;/li>
&lt;/ul>
&lt;p>把飞跃手册的案例结构化地存在数据库里，定期生成网页。这样的好处在于可以系统地整理申请情况，同时方便贡献。&lt;/p>
&lt;h4 id="juss-ticket">juss-ticket&lt;/h4>
&lt;ul>
&lt;li>Python 脚本｜Go 后端｜React 前端&lt;/li>
&lt;li>不公开｜不想被请喝茶&lt;/li>
&lt;li>2024.3 - 2024.4&lt;/li>
&lt;/ul>
&lt;p>黄牛猖獗下，为了抢 F1 中国站的票写的抢票脚本，自己买到后加上了一个简单的后台系统和前端用来帮别人抢。抢票无法全流程自动化，因此和室友忙活了半个月。赚了一点小钱（至少相当于免费的三天 F1 票 + 住宿 + 交通）。&lt;/p>
&lt;h4 id="launchmanager">LaunchManager&lt;/h4>
&lt;ul>
&lt;li>macOS App&lt;/li>
&lt;li>开源｜&lt;a href="https://github.com/liang2kl/LaunchManager">GitHub 链接&lt;/a>&lt;/li>
&lt;li>2024.12&lt;/li>
&lt;/ul>
&lt;p>一个方便管理 macOS 开机启动脚本的 GUI。&lt;/p></content></item></channel></rss>