修复 QuickLook(2023)

Lobsters Hottest 工具

摘要

一篇技术指南,介绍如何使用 LLDB 对 macOS Finder 进行逆向工程,以解决在升级到 macOS Ventura 后 QuickLook 预览中出现 unwanted 圆角的问题。

<p><a href="https://lobste.rs/s/symlbk/fixing_quicklook_2023">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/09 18:43

# 修复 QuickLook:https://foon.uk/fixing-quicklook/ ## 在从 Mojave 升级到 Ventura 后,我注意到苹果对 QuickLook 进行了改动。QuickLook 是我最喜欢的软件功能之一。你只需在 Finder 中选中一个文件,按下空格键,该文件就会弹出现在屏幕上。再按一次空格键,它又会消失。焦点始终保留在 Finder 中,因此你可以使用方向键选择其他文件,而 QuickLook 窗口会随之更新以显示它们。它简单、设计精良、快速、优雅,整体表现非常出色。直到最近为止。 不知出于何种原因,现在的 QuickLook 在显示图像前会将其四角裁切掉。无论这些图像是照片、游戏资源还是你正在设计的 UI 元素,所有图片在显示之前都会被圆角处理。 自然,我四处寻找能关闭这一功能的秘密开关。苹果通常很擅长让我们选择是否使用他们最新的改进功能,所以肯定有办法吧?是辅助功能设置吗?还是某个 `defaults.write` 命令?据我所知,并没有这样的选项。 那么,我们是不是只能认命了?不。别这么想。虽然它是 Mac,但它仍然是你的电脑。让我们来修复这个问题。 ## QuickLook 头文件 如果我们想干预 QuickLook 显示内容的方式,就需要获取到它的窗口。然后我们 hopefully 可以探索其中的视图,并更改它们的绘制方式。QuickLook API 是公开且有文档记录的(https://developer.apple.com/documentation/quicklook?language=objc),查阅文档你会发现,这个窗口被称为 `QLPreviewPanel`,每个应用只有一个其实例,可以通过 `[QLPreviewPanel sharedPreviewPanel]` 获取。 ## 调试 Finder 我之前一直只在 Finder 中使用 QuickLook,所以我认为应该从这里开始。让我们启动调试器看看 Finder 在做什么: ```bash $ lldb -n Finder error: process exited with status -1 (attach failed (Not allowed to attach to process.)) ``` 实际上,首先我们需要处理 SIP(系统完整性保护)。SIP 是苹果的一项新技术,通过阻止你读写自己电脑的内存来“保护”你。为了修复此类系统进程中的问题,我们必须关闭它并重启。 好吧,让我们再试一次: ```bash $ lldb -n Finder Process 4040 stopped thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x000000018df1bf54 libsystem_kernel.dylib`mach_msg2_trap + 8 libsystem_kernel.dylib`mach_msg2_trap: -> 0x18df1bf54 <+8>: ret libsystem_kernel.dylib`macx_swapon: 0x18df1bf58 <+0>: mov x16, #-0x30 0x18df1bf5c <+4>: svc #0x80 0x18df1bf60 <+8>: ret Target 0: (Finder) stopped. Executable module set to "/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder". Architecture set to: arm64e-apple-macosx-. ``` 我们已经进入了系统。让我们尝试在 `sharedPreviewPanel` 上设置一个断点。 ```bash (lldb) break set -n sharedPreviewPanel Breakpoint 5: where = QuickLookUI`+[QLPreviewPanel sharedPreviewPanel], address = 0x00000001f81c025c (lldb) c ``` 导航到一个图像文件,按下空格键,然后: ```bash Process 4040 stopped thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1 frame #0: 0x00000001f81c025c QuickLookUI`+[QLPreviewPanel sharedPreviewPanel] QuickLookUI`+[QLPreviewPanel sharedPreviewPanel]: -> 0x1f81c025c <+0>: pacibsp 0x1f81c0260 <+4>: sub sp, sp, #0x40 0x1f81c0264 <+8>: stp x22, x21, [sp, #0x10] 0x1f81c0268 <+12>: stp x20, x19, [sp, #0x20] ``` LLDB 在 `sharedPreviewPanel` 函数中暂停了 Finder 进程。让我们获取返回值,这应该是预览面板的实例: ```bash (lldb) finish (lldb) po $x0 ``` 太好了!我们有了进展。让我们看看这个面板里有什么: ```bash (lldb) po [$x0 recursiveDescription] [ A w ] h=--- v=--- NSNextStepFrame 0x12cea29b0 f=(0,0,370,223) b=(-) => <_NSViewBackingLayer: 0x600003f7fcf0> [ A w ] h=-&- v=-&- QLPreviewBackgroundView 0x11e00df90 f=(0,0,370,223) b=(-) => <_NSViewBackingLayer: 0x600003f7fc60> [ A w ] h=--- v=--- NSView 0x11e040ec0 f=(0,0,370,223) b=(-) => <_NSViewBackingLayer: 0x600003f7fc00> [ A w ] h=--- v=--- NSView 0x11e047f50 f=(5,5,360,180) b=(-) => <_NSViewBackingLayer: 0x600003f7f9f0> [ A w ] h=-&- v=-&- NSView 0x11cf35780 f=(0,0,360,180) b=(-) => <_NSViewBackingLayer: 0x600003f7f960> [ A w ] h=-&- v=-&- NSView 0x11cf31c50 f=(0,0,360,180) b=(-) => <_NSViewBackingLayer: 0x600003f7f8d0> [ A w ] h=-&- v=-&- NSView 0x11cf31fb0 f=(0,0,360,180) b=(-) => <_NSViewBackingLayer: 0x600003f7f840> [ A w ] h=-&- v=-&- QLPanelPreviewView 0x11cf35d10 f=(0,0,360,180) b=(-) => <_NSViewBackingLayer: 0x600003f7f4e0> [ A W ] h=-&- v=-&- QLPreviewContainerView 0x11cf360d0 f=(0,0,360,180) b=(-) => [ A W ] h=--- v=--- NSRemoteView 0x119c3a040 f=(0,0,360,180) b=(-) => [ A W ] h=--- v=--- QLPreviewTitleBarView 0x11e0482b0 f=(8,189,354,30) b=(-) => <_NSViewBackingLayer: 0x600003fbaf70> [ AF w ] h=--- v=--- QLPreviewWindowButton 0x11cf35300 "Button" f=(4,9,12,12) b=(-) => <_NSViewBackingLayer: 0x600003f7fa80> [ A V W ] h=--- v=--- NSButtonImageView 0x11cfaeca0 f=(0,0,12,12) b=(-) => <_NSViewBackingLayer: 0x600003f7f2a0> [ AF w ] h=--- v=--- QLPreviewWindowButton 0x11cf31310 "Button" f=(24,9,12,12) b=(-) => <_NSViewBackingLayer: 0x600003f7fb40> [ A V W ] h=--- v=--- NSButtonImageView 0x11cfaef00 f=(0,0,12,12) b=(-) => <_NSViewBackingLayer: 0x600003f7f390> [ A w ] h=--- v=--- NSView 0x11e048d80 f=(114,4,240,22) b=(-) => <_NSViewBackingLayer: 0x600003f7a310> [ A w ] h=--- v=--- QLControlsCenteringView 0x11cfa9960 f=(0,0,240,22) b=(-) => <_NSViewBackingLayer: 0x600003f7a280> [ A w ] h=--- v=--- QLControlsContainerView 0x11cfa9ed0 f=(0,0,240,22) b=(-) => <_NSViewBackingLayer: 0x600003f7a220> [ AF w ] h=--- v=--- QLControlSegmentedControl 0x11cf630a0 f=(38,-2,33,25) b=(-) => <_NSViewBackingLayer: 0x600003ced470> [ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf63560 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003ced500> [ A V W ] h=--- v=--- NSSegmentItemBezelView 0x11cf63840 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003f80d80> [ A w ] h=--- v=--- NSSegmentItemImageView 0x11cf63bb0 f=(10,1.5,15,17) b=(-) => <_NSViewBackingLayer: 0x600003ced410> [ AF w ] h=--- v=--- QLControlSegmentedControl 0x11cf5b880 f=(-1,-2,33,25) b=(-) => <_NSViewBackingLayer: 0x600003cec690> [ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf5bd40 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003ced530> [ A V W ] h=--- v=--- NSSegmentItemBezelView 0x11cf5c020 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003f80060> [ A w ] h=--- v=--- NSSegmentItemImageView 0x11cf5c390 f=(9,4,15,15) b=(-) => <_NSViewBackingLayer: 0x600003ceccc0> [ AF w ] h=--- v=--- QLControlSegmentedControl 0x11cf53ee0 f=(77,-2,33,25) b=(-) => <_NSViewBackingLayer: 0x600003cee370> [ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf543a0 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003ced110> [ A V W ] h=--- v=--- NSSegmentItemBezelView 0x11cf54680 f=(0,0,33,25) b=(-) => <_NSViewBackingLayer: 0x600003f80270> [ A w ] h=--- v=--- NSSegmentItemImageView 0x11cf549f0 f=(9.5,2,15,17) b=(-) => <_NSViewBackingLayer: 0x600003cec9c0> [ AF w ] h=--- v=--- QLControlSegmentedControl 0x11cf4c8c0 f=(116,-2,125,25) b=(-) => <_NSViewBackingLayer: 0x600003f784e0> [ AF V W ] h=--- v=--- NSSegmentItemView 0x11cf4cc70 f=(0,0,125,25) b=(-) => <_NSViewBackingLayer: 0x600003f78720> [ A V W ] h=--- v=--- NSSegmentItemBezelView 0x11cf4cf50 f=(0,0,125,25) b=(-) => <_NSViewBackingLayer: 0x600003f83000> [ AF w ] h=--- v=--- NSSegmentItemLabelView 0x11cf4d650 "Open with Preview" f=(2,3,121,16) b=(-) => [ AF w ] h=--- v=--- NSTextField 0x11e04cda0 "Screenshot 2023-08-28 at 16.21.26" f=(42,8,66,16) b=(-) => A=autoresizesSubviews, C=canDrawConcurrently, D=needsDisplay, F=flipped, G=gstate, H=hidden (h=by ancestor), L=needsLayout (l=child needsLayout), U=needsUpdateConstraints (u=child needsUpdateConstraints), O=opaque, P=preservesContentDuringLiveResize, S=scaled/rotated, W=wantsLayer (w=ancestor wantsLayer), V=needsVibrancy (v=allowsVibrancy), #=has surface ``` 这信息量很大。我们可以忽略 `QLPreviewTitleBarView` 及其子视图,所以让我们关注树状结构的另一分支。这里有一个 `QLPreviewBackgroundView`,接着是一堆 `NSView`,然后是 `QLPanelPreviewView`、`QLPreviewContainerView`,最顶部是一个 `NSRemoteView`。 ## NSRemoteView 我之前从未见过 `NSRemoteView`,也不太确定它是什么。似乎没有公开的头文件供它使用。我想知道它是如何工作的? 好吧,我们可能有点跑题了。我们仍然不知道 `NSRemoteView` 是否与我们的图像有关。让我们把它隐藏起来,看看图像是否消失。 ```bash (lldb) po [(NSRemoteView *)0x119c3a040 setHidden:YES] (lldb) c ``` 确实消失了。所以,正是这个 `NSRemoteView` 在显示我们的图像。“Remote”(远程)这个词让我觉得它与另一个执行实际渲染的进程进行通信,所以我们可能需要在远程进程中查看它做了什么。但是是哪个进程呢?也许 `NSRemoteView` 可以告诉我们?让我们看看它能做什么: ```bash (lldb) po [[NSRemoteView class] fp_shortMethodDescription] :in NSRemoteView: Class Methods: + (void) initialize; (0xd465800195a3a894) + (void) observeValueForKeyPath:(id)arg1 ofObject:(id)arg2 change:(id)arg3 context:(void *)arg4; (0xea63800195a944bc) ... + (id) _remoteViewForIdentifier:(id)arg1; (0xda2a800195a94dc4) + (BOOL) anyRemoteViewAttachedToWindow:(id)arg1; (0x285f800195a94c14) ... Properties: @property (retain, nonatomic) NSAccessibilityRemoteUIElement* accessoryViewAccessibilityParent; @property (readonly, nonatomic) NSRemoteView* spawnedBy; ... Instance Methods: - (oneway void) release; (0xc97d800195a40a60) ... ``` 好多!在该方法列表中搜索“process”得到了 `_serviceProcessIdentifier`,所以让我们调用它。 ```bash (lldb) po [$v _serviceProcessIdentifier] 5959 ``` 那个进程是什么? ```bash $ ps -c 5959 PID TTY TIME CMD 5959 ?? 0:14.34 QuickLookUIService ``` 肯定是它了。调试 Finder 并不是我们想要的。我们需要调试 `QuickLookUIService`。 ## 调试 QuickLookUIService ```bash (lldb) detach Process 4040 detached (lldb) attach 5959 Process 5959 stopped thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP frame #0: 0x000000018df1bf54 libsystem_kernel.dylib`mach_msg2_trap + 8 libsystem_kernel.dylib`mach_msg2_trap: -> 0x18df1bf54 <+8>: ret libsystem_kernel.dylib`macx_swapon: 0x18df1bf58 <+0>: mov x16, #-0x30 0x18df1bf5c <+4>: svc #0x80 0x18df1bf60 <+8>: ret Target 0: (QuickLookUIService) stopped. ``` 所以,`NSRemoteView` 的另一端就在……这个进程中的某个地方。如果我们能遍历该进程中的所有视图,就能看看是否有任何视图看起来像 `NSRemoteViewDelegate` 的另一端。我在互联网上搜索获取所有视图的函数,但除了偶尔有人建议直接使用 Xcode 之外,什么也没找到 [^1]。 好吧,那我们直接用 Xcode 怎么样? 在打开 Xcode、创建项目、选择保存位置、输入团队名称、选择模板、输入 bundle identifier、创建 scheme 并设置 scheme 目标以允许我使用调试器之后,我将其附加到 `QuickLookUIService`,按下“Show View Hierarchy”,然后: *Xcode 的视图调试器已附加到 QuickLookUIService。* 哇,这太简单了!我们可以点击这些视图并获取关于它们的信息,包括它们的地址,以便我们在调试器中操作它们。而且我们可以立刻看到,最前面的视图(Xcode 告诉我们是一个 `QLBorderView`)是一个带圆角的边框!等等,有个边框? ## 边框 确实有。起初我没注意到,但如果放大看,你会发现除了圆角处理外,QuickLook 还叠加了一个略小于图像内容的边框: `QLBorderView` 原来非常简单;它只是一个图层具有 `borderWidth` 和 `borderColor` 的视图。让我们从 Xcode 获取它的地址,并让它停止这样做: ```bash (lldb) po [0x600000850180 setBorderWidth:0]; ``` 边框成功移除! ## 取消裁剪 所以,边框不见了,但角落仍然是圆形的。我们在视图调试器中可以看到图像视图的角落没有被圆角处理,所以一定有东西在裁剪它。让我们检查所有父视图的图层,看看是否有任何图层的 `cornerRadius` 非零。 你可以写一些代码来做到这一点,但我只是逐个点击并复制其图层地址到调试器中进行检查: ```bash (lldb) p [(CALayer *)0x60000283f5a0 cornerRadius] (double) $78 = 0 (lldb) p [(CALayer *)0x60000283f660 cornerRadius] (double) $79 = 0.43438914027149322 ``` 好吧,这没用多久。让我们试试…… ```bash (lldb) po [0x60000283f660 setMasksToBounds

相似文章

QuickRight

Product Hunt

QuickRight 为 macOS Finder 添加了缺失的右键功能,提升了工作效率。

Glance

Product Hunt

Glance 是一款工具,让用户能在 macOS 上通过 Quick Look 即时预览 .md 文件。

Marked QL

Product Hunt

Marked QL 是一款可在 Finder 中即时预览 Markdown 的工具。

我也锉平了我的MacBook边角

Hacker News Top

一位用户描述了如何将MacBook Air的尖锐边缘锉平,以便在腿上使用时更舒适,并通过照片和技巧记录了整个过程。