Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.swiftpm/
.DS_Store
Package.resolved
docs/superpowers
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/regulusleow/astrolabe-protocol.git",
exact: "2.0.0"
exact: "2.1.0"
)
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ the Astrolabe Host during development. Linking the dynamic framework is the
complete integration: the Runtime starts automatically when an eligible
application process loads it.

Current package release: `2.1.0`.
Current package release: `2.2.0`.

## Requirements

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Astrolabe Runtime for iOS 在开发期间向 Astrolabe Host 暴露 UIKit 和 Cor
检查数据。链接 dynamic Framework 即完成全部接入;符合条件的 App 进程加载 Framework
后,Runtime 会自动启动。

当前 Package 版本:`2.1.0`。
当前 Package 版本:`2.2.0`。

## 环境要求

Expand Down
2 changes: 1 addition & 1 deletion Sources/AstrolabeRuntime/AstrolabeRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import AstrolabeProtocol

package enum AstrolabeRuntimeSDK {
/// Release version advertised by the embedded runtime.
package static let runtimeVersion = "2.1.0"
package static let runtimeVersion = "2.2.0"

package static var protocolVersion: RuntimeProtocolVersion {
.v2
Expand Down
2 changes: 1 addition & 1 deletion Sources/AstrolabeRuntime/AutomaticRuntimeInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ package final class AutomaticRuntimeInstaller {
},
failureReporter: { error in
logger.error(
"Astrolabe Runtime 启动失败:\(error.localizedDescription, privacy: .public)"
"Astrolabe Runtime startup failed: \(error.localizedDescription, privacy: .public)"
)
}
)
Expand Down
9 changes: 8 additions & 1 deletion Sources/AstrolabeRuntimeCore/RuntimeInspectionServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// Created by 轩辕十四 on 2026/7/10.
//

import AstrolabeProtocol

package struct RuntimeInspectionServices: Sendable {
/// Provider exposing application, device, and screen metadata.
package let appInfoProvider: any RuntimeAppInfoProviding
Expand All @@ -18,16 +20,21 @@ package struct RuntimeInspectionServices: Sendable {
/// Optional provider managing Debug-only temporary attribute patches.
package let attributePatchProvider: (any RuntimeAttributePatchProviding)?

/// Capabilities describing payload extensions rather than request handlers.
package let additionalCapabilities: Set<RuntimeCapability>

package init(
appInfoProvider: any RuntimeAppInfoProviding,
hierarchyProvider: any RuntimeHierarchyProviding,
nodeDetailProvider: any RuntimeNodeDetailProviding,
attributePatchProvider: (any RuntimeAttributePatchProviding)? = nil
attributePatchProvider: (any RuntimeAttributePatchProviding)? = nil,
additionalCapabilities: Set<RuntimeCapability> = []
) {
self.appInfoProvider = appInfoProvider
self.hierarchyProvider = hierarchyProvider
self.nodeDetailProvider = nodeDetailProvider
self.attributePatchProvider = attributePatchProvider
self.additionalCapabilities = additionalCapabilities
}
}

Expand Down
6 changes: 5 additions & 1 deletion Sources/AstrolabeRuntimeCore/RuntimeServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,11 @@ package actor RuntimeServer {
Feature(capability: .attributePatching, handler: clearPatchesHandler)
])
}
let capabilities = Array(Set(features.map(\.capability))).sorted {
let capabilities = Array(
Set(features.map(\.capability)).union(
inspectionServices.additionalCapabilities
)
).sorted {
$0.rawValue < $1.rawValue
}
let handshakeHandler = TypedRuntimeRequestHandler<
Expand Down
150 changes: 143 additions & 7 deletions Sources/AstrolabeRuntimeUIKit/UIKitHierarchyCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ import AstrolabeRuntimeObjC
import QuartzCore
import UIKit

private struct UIKitCapturedLayerContext {
/// Layer captured from the authoritative window layer hierarchy.
let layer: CALayer

/// Window that owns the authoritative layer hierarchy.
let window: UIWindow

/// Whether the owner or one of its ancestors hides dependent layers.
let dependencyHiddenByAncestor: Bool

/// Effective opacity inherited by dependent layers.
let dependencyAncestorAlpha: Double

/// Visible screen rect inherited by dependent layers.
let dependencyAncestorVisibleRect: CGRect
}

private enum UIKitLayerCaptureMode {
/// Capture from the authoritative layer hierarchy.
case hierarchy

/// Capture a detached mask tree using its owner as the coordinate anchor.
case detachedMask(owner: CALayer)
}

@MainActor
final class UIKitHierarchyCollector {
private let nodeRegistry: RuntimeNodeRegistry
Expand All @@ -20,21 +45,24 @@ final class UIKitHierarchyCollector {
private let colorMapper: UIKitRuntimeColorMapper
private let accessibilityMapper: UIKitRuntimeAccessibilityMapper
private let attributeCollectorRegistry: UIKitRuntimeAttributeCollectorRegistry
private let relationProviderRegistry: UIKitRuntimeNodeRelationProviderRegistry

init(
nodeRegistry: RuntimeNodeRegistry,
targetIdentifier: RuntimeOpaqueIdentifier,
screenMapper: UIKitRuntimeScreenMapper,
colorMapper: UIKitRuntimeColorMapper,
accessibilityMapper: UIKitRuntimeAccessibilityMapper,
attributeCollectorRegistry: UIKitRuntimeAttributeCollectorRegistry
attributeCollectorRegistry: UIKitRuntimeAttributeCollectorRegistry,
relationProviderRegistry: UIKitRuntimeNodeRelationProviderRegistry = .init()
) {
self.nodeRegistry = nodeRegistry
self.targetIdentifier = targetIdentifier
self.screenMapper = screenMapper
self.colorMapper = colorMapper
self.accessibilityMapper = accessibilityMapper
self.attributeCollectorRegistry = attributeCollectorRegistry
self.relationProviderRegistry = relationProviderRegistry
}

func capture(
Expand Down Expand Up @@ -70,6 +98,7 @@ final class UIKitHierarchyCollector {
)
}
let layerRootOffset = roots.count
var capturedLayerContexts = [UIKitCapturedLayerContext]()
roots.append(contentsOf: try windows.enumerated().map { index, window in
try layerNode(
for: window.layer,
Expand All @@ -79,9 +108,21 @@ final class UIKitHierarchyCollector {
screen: screen,
ancestorHidden: false,
ancestorAlpha: 1,
ancestorVisibleRect: screen.coordinateSpace.bounds
ancestorVisibleRect: screen.coordinateSpace.bounds,
captureMode: .hierarchy,
capturedLayerContexts: &capturedLayerContexts
)
})
try appendDetachedMaskRoots(
to: &roots,
capturedLayerContexts: capturedLayerContexts,
screen: screen
)
let relationContext = UIKitRuntimeNodeRelationCollectionContext(
windows: windows,
layers: capturedLayerContexts.map(\.layer),
nodeIDsByObjectIdentity: capturedNodeIDs(in: roots)
)

return RuntimeHierarchySnapshotPayload(
snapshotID: try RuntimeOpaqueIdentifier(
Expand All @@ -96,10 +137,43 @@ final class UIKitHierarchyCollector {
space: .screen
),
roots: roots,
relations: try relationProviderRegistry.relations(
in: relationContext
),
extensions: nil
)
}

private func capturedNodeIDs(
in roots: [RuntimeNode]
) -> [ObjectIdentifier: RuntimeOpaqueIdentifier] {
var nodeIDsByObjectIdentity =
[ObjectIdentifier: RuntimeOpaqueIdentifier]()
for root in roots {
collectCapturedNodeIDs(
from: root,
into: &nodeIDsByObjectIdentity
)
}
return nodeIDsByObjectIdentity
}

private func collectCapturedNodeIDs(
from node: RuntimeNode,
into nodeIDsByObjectIdentity:
inout [ObjectIdentifier: RuntimeOpaqueIdentifier]
) {
if let object = nodeRegistry.object(for: node.nodeID) {
nodeIDsByObjectIdentity[ObjectIdentifier(object)] = node.nodeID
}
for child in node.children {
collectCapturedNodeIDs(
from: child,
into: &nodeIDsByObjectIdentity
)
}
}

private func viewNode(
for view: UIView,
parentID: RuntimeOpaqueIdentifier?,
Expand Down Expand Up @@ -185,7 +259,9 @@ final class UIKitHierarchyCollector {
screen: UIScreen,
ancestorHidden: Bool,
ancestorAlpha: Double,
ancestorVisibleRect: CGRect
ancestorVisibleRect: CGRect,
captureMode: UIKitLayerCaptureMode,
capturedLayerContexts: inout [UIKitCapturedLayerContext]
) throws -> RuntimeNode {
let nodeID = nodeRegistry.nodeID(for: layer)
let hiddenByAncestor = ancestorHidden
Expand All @@ -194,7 +270,8 @@ final class UIKitHierarchyCollector {
layer,
isRoot: parentID == nil,
window: window,
screen: screen
screen: screen,
captureMode: captureMode
)
let frameInScreen = cgRect(geometry.frameInScreen)
let visibleRect = ancestorVisibleRect.intersection(frameInScreen)
Expand All @@ -211,6 +288,15 @@ final class UIKitHierarchyCollector {
let childVisibleRect = layer.masksToBounds ?
visibleRect :
ancestorVisibleRect
if case .hierarchy = captureMode {
capturedLayerContexts.append(UIKitCapturedLayerContext(
layer: layer,
window: window,
dependencyHiddenByAncestor: hiddenByAncestor || layer.isHidden,
dependencyAncestorAlpha: effectiveAlpha,
dependencyAncestorVisibleRect: visibleRect
))
}
let children = try (layer.sublayers ?? []).enumerated().map {
index, sublayer in
try layerNode(
Expand All @@ -221,7 +307,9 @@ final class UIKitHierarchyCollector {
screen: screen,
ancestorHidden: hiddenByAncestor || layer.isHidden,
ancestorAlpha: effectiveAlpha,
ancestorVisibleRect: childVisibleRect
ancestorVisibleRect: childVisibleRect,
captureMode: captureMode,
capturedLayerContexts: &capturedLayerContexts
)
}

Expand Down Expand Up @@ -282,9 +370,17 @@ final class UIKitHierarchyCollector {
_ layer: CALayer,
isRoot: Bool,
window: UIWindow,
screen: UIScreen
screen: UIScreen,
captureMode: UIKitLayerCaptureMode
) -> RuntimeNodeGeometry {
let rectInWindow = layer.convert(layer.bounds, to: window.layer)
let rectInWindow: CGRect
switch captureMode {
case .hierarchy:
rectInWindow = layer.convert(layer.bounds, to: window.layer)
case let .detachedMask(owner):
let rectInOwner = layer.convert(layer.bounds, to: owner)
rectInWindow = owner.convert(rectInOwner, to: window.layer)
}
let rectInScreen = window.convert(
rectInWindow,
to: screen.coordinateSpace
Expand All @@ -299,6 +395,46 @@ final class UIKitHierarchyCollector {
)
}

private func appendDetachedMaskRoots(
to roots: inout [RuntimeNode],
capturedLayerContexts: [UIKitCapturedLayerContext],
screen: UIScreen
) throws {
var capturedLayerIdentities = Set(
capturedLayerContexts.map { ObjectIdentifier($0.layer) }
)
var ignoredLayerContexts = [UIKitCapturedLayerContext]()
for context in capturedLayerContexts {
guard let mask = context.layer.mask,
capturedLayerIdentities.insert(ObjectIdentifier(mask)).inserted
else {
continue
}
capturedLayerIdentities.formUnion(layerIdentities(in: mask))
roots.append(try layerNode(
for: mask,
parentID: nil,
siblingIndex: roots.count,
window: context.window,
screen: screen,
ancestorHidden: context.dependencyHiddenByAncestor,
ancestorAlpha: context.dependencyAncestorAlpha,
ancestorVisibleRect: context.dependencyAncestorVisibleRect,
captureMode: .detachedMask(owner: context.layer),
capturedLayerContexts: &ignoredLayerContexts
))
}
}

private func layerIdentities(
in root: CALayer
) -> Set<ObjectIdentifier> {
(root.sublayers ?? []).reduce(into: [ObjectIdentifier(root)]) {
identities, layer in
identities.formUnion(layerIdentities(in: layer))
}
}

private func runtimeType(for object: AnyObject) -> RuntimeType {
let classChain = RuntimeMetadataAdapter.classChain(for: object)
return RuntimeType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ final class UIKitRuntimeAttributeCollectorRegistry {
private static var defaultCollectors: [any UIKitRuntimeAttributeCollecting] {
[
UIKitLayoutAttributeCollector(),
UIKitNormalizedLayoutRelationAttributeCollector(),
UIKitViewAttributeCollector(),
UIKitLayerAttributeCollector(),
UIKitGradientLayerAttributeCollector(),
UIKitShapeLayerAttributeCollector(),
UIKitAccessibilityAttributeCollector(),
UIKitLabelAttributeCollector(),
UIKitImageViewAttributeCollector(),
Expand Down
Loading