修复 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 文件。