Skip to content

Instantly share code, notes, and snippets.

@steipete
Last active November 13, 2021 17:09
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save steipete/2d50571f0fd4963ebe9b0062fec0976f to your computer and use it in GitHub Desktop.
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…
//
// 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