|
// |
|
// 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 -") |
|
} |
|
} |