Last active
August 8, 2024 08:47
-
-
Save steipete/2d50571f0fd4963ebe9b0062fec0976f to your computer and use it in GitHub Desktop.
Runtime Scanner to find large ObjC encodings. Inspired by https://medium.com/@dmaclach/objective-c-encoding-and-you-866624cc02de - this code scans the runtime to find excessive Objective-C type encodings, usually created when you mix it with C++. We were able to reduce the binary size by 100KB with rewriting just a few methods that were extremel…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Copyright © 2020 PSPDFKit GmbH, Peter Steinberger. MIT Licensed. | |
// | |
import Foundation | |
extension String { | |
fileprivate init?(maybeCString: UnsafePointer<CChar>?) { | |
guard let cString = maybeCString else { return nil } | |
self.init(cString: cString) | |
} | |
} | |
protocol ClassEntry { | |
var name: String { get } | |
var encoding: String { get } | |
var owningClass: RuntimeScanner.Class { get } | |
} | |
// Scans the runtime for large encodings | |
// Inspired by https://medium.com/@dmaclach/objective-c-encoding-and-you-866624cc02de | |
class RuntimeScanner { | |
struct Variable: ClassEntry { | |
let ivar: Ivar | |
let name: String | |
let encoding: String | |
let owningClass: Class | |
init?(ivar: Ivar, owningClass: Class) { | |
guard let name = String(maybeCString: ivar_getName(ivar)), | |
let typeEncoding = String(maybeCString: ivar_getTypeEncoding(ivar)) | |
else { return nil } | |
self.ivar = ivar | |
self.name = name | |
self.encoding = typeEncoding | |
self.owningClass = owningClass | |
} | |
} | |
struct Method: ClassEntry { | |
let method: Foundation.Method | |
let name: String | |
let encoding: String | |
let owningClass: Class | |
init?(method: Foundation.Method, owningClass: Class) { | |
guard let encoding = String(maybeCString: method_getTypeEncoding(method)) else { return nil } | |
self.method = method | |
self.encoding = encoding | |
self.name = String(cString: sel_getName(method_getName(method))) | |
self.owningClass = owningClass | |
} | |
} | |
/// Lightweight wrapper around AnyClass | |
struct Class: Hashable, Equatable { | |
let classType: AnyClass | |
let name: String | |
init?(_ classType: AnyClass) { | |
let className = String(cString: class_getName(classType)) | |
guard !className.isEmpty else { return nil } | |
self.classType = classType | |
self.name = className | |
} | |
var variables: [Variable] { | |
var variables = [Variable]() | |
let count = UnsafeMutablePointer<UInt32>.allocate(capacity: 0) | |
defer { count.deallocate() } | |
guard let ivarList = class_copyIvarList(self.classType, count) else { return variables } | |
defer { ivarList.deallocate() } | |
for ivarCount in 0..<count.pointee { | |
if let variable = Variable(ivar: ivarList[Int(ivarCount)], owningClass: self) { | |
variables.append(variable) | |
} | |
} | |
return variables | |
} | |
var methods: [Method] { | |
instanceMethods + classMethods | |
} | |
var instanceMethods: [Method] { | |
getMethods(of: self.classType) | |
} | |
var classMethods: [Method] { | |
guard let metaClass = object_getClass(self.classType) else { return [] } | |
return getMethods(of: metaClass) | |
} | |
private func getMethods(of klass: AnyClass) -> [Method] { | |
var methods = [Method]() | |
let count = UnsafeMutablePointer<UInt32>.allocate(capacity: 0) | |
defer { count.deallocate() } | |
guard let methodList = class_copyMethodList(klass, count) else { return methods } | |
defer { methodList.deallocate() } | |
for methodCount in 0..<count.pointee { | |
if let method = Method(method: methodList[Int(methodCount)], owningClass: self) { | |
methods.append(method) | |
} | |
} | |
return methods | |
} | |
static func == (lhs: Class, rhs: Class) -> Bool { | |
return lhs.name == rhs.name | |
} | |
func hash(into hasher: inout Hasher) { | |
hasher.combine(name) | |
} | |
} | |
static func getAllClasses() -> [Class] { | |
// Get number of classes, create buffer, then request them. | |
let expectedClassCount = objc_getClassList(nil, 0) | |
let allClasses = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(expectedClassCount)) | |
defer { allClasses.deallocate() } | |
let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(allClasses) | |
let actualClassCount = objc_getClassList(autoreleasingAllClasses, expectedClassCount) | |
var classes = [Class]() | |
for i in 0 ..< actualClassCount { | |
if let currentClass: AnyClass = allClasses[Int(i)], | |
let klass = Class(currentClass) { | |
classes.append(klass) | |
} | |
} | |
return classes | |
} | |
public func scan() { | |
let ENCODING_LIMIT = 100 | |
var offendingClasses = [Class: [ClassEntry]]() | |
// This includes all app classes, but no frameworks. | |
let classes = RuntimeScanner.getAllClasses().filter { | |
let bundle = Bundle(for: $0.classType) | |
return bundle == .main // possibility to add your custom bundles | |
} | |
// Alternatively, you can filter for prefixes | |
//let classes = RuntimeScanner.getAllClasses().filter { return $0.name.hasPrefix("PSPDF") || $0.name.hasPrefix("PDFC") } | |
for klass in classes { | |
let entries = klass.variables as [ClassEntry] + klass.methods | |
let filtered = entries.filter { $0.encoding.count > ENCODING_LIMIT } | |
if !filtered.isEmpty { | |
offendingClasses[klass] = filtered | |
} | |
} | |
let allEntries = offendingClasses.values.joined() | |
let sortedEntries = allEntries.sorted { $0.encoding.count > $1.encoding.count } | |
for entry in sortedEntries { | |
print("\n\(entry.owningClass.name).\(entry.name): (\(entry.encoding.count)) \(entry.encoding)") | |
} | |
print("- fin -") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment