├── .gitignore ├── AppPerformance.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── AppPerformance ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── cat.imageset │ │ ├── Contents.json │ │ └── cat.png │ └── city.imageset │ │ ├── Contents.json │ │ └── city.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Classes │ ├── AppPerformance-Bridging-Header.h │ ├── cpu │ │ ├── LSLCpuUsage.h │ │ └── LSLCpuUsage.m │ ├── fps │ │ ├── LSLFPSMonitor.swift │ │ ├── LSLFPSTableViewController.swift │ │ ├── LSLWeakProxy.h │ │ └── LSLWeakProxy.m │ ├── memory │ │ ├── LSLApplicationMemory.h │ │ └── LSLApplicationMemory.m │ └── 卡顿 │ │ ├── LSLAppFluencyMonitor.h │ │ ├── LSLAppFluencyMonitor.m │ │ ├── LSLBacktraceLogger.h │ │ └── LSLBacktraceLogger.m ├── Info.plist ├── ViewController.swift └── main.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /AppPerformance.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 99144B41211C4DA500D59060 /* LSLWeakProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 99144B40211C4DA500D59060 /* LSLWeakProxy.m */; }; 11 | 9992231221251D4F00131C34 /* LSLBacktraceLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 9992231121251D4F00131C34 /* LSLBacktraceLogger.m */; }; 12 | 9992231721251F1000131C34 /* LSLAppFluencyMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 9992231621251F1000131C34 /* LSLAppFluencyMonitor.m */; }; 13 | 99ABCCD921194DA10074A0D5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99ABCCD821194DA10074A0D5 /* AppDelegate.swift */; }; 14 | 99ABCCDB21194DA10074A0D5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99ABCCDA21194DA10074A0D5 /* ViewController.swift */; }; 15 | 99ABCCDE21194DA10074A0D5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 99ABCCDC21194DA10074A0D5 /* Main.storyboard */; }; 16 | 99ABCCE021194DA30074A0D5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 99ABCCDF21194DA30074A0D5 /* Assets.xcassets */; }; 17 | 99ABCCE321194DA30074A0D5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 99ABCCE121194DA30074A0D5 /* LaunchScreen.storyboard */; }; 18 | 99ABCCF0211971300074A0D5 /* LSLCpuUsage.m in Sources */ = {isa = PBXBuildFile; fileRef = 99ABCCEF211971300074A0D5 /* LSLCpuUsage.m */; }; 19 | 99B8EE7F21197BB6007647F1 /* LSLApplicationMemory.m in Sources */ = {isa = PBXBuildFile; fileRef = 99B8EE7E21197BB6007647F1 /* LSLApplicationMemory.m */; }; 20 | 99CEFA6D211BF02E0018F23C /* LSLFPSMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99CEFA6C211BF02E0018F23C /* LSLFPSMonitor.swift */; }; 21 | 99CEFA72211C2EC80018F23C /* LSLFPSTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99CEFA71211C2EC80018F23C /* LSLFPSTableViewController.swift */; }; 22 | 99E6A2E7211AE1F1004B8BBD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99E6A2E6211AE1F1004B8BBD /* main.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 99144B3F211C4DA500D59060 /* LSLWeakProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LSLWeakProxy.h; sourceTree = ""; }; 27 | 99144B40211C4DA500D59060 /* LSLWeakProxy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSLWeakProxy.m; sourceTree = ""; }; 28 | 9992231021251D4E00131C34 /* LSLBacktraceLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSLBacktraceLogger.h; sourceTree = ""; }; 29 | 9992231121251D4F00131C34 /* LSLBacktraceLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LSLBacktraceLogger.m; sourceTree = ""; }; 30 | 9992231521251F1000131C34 /* LSLAppFluencyMonitor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LSLAppFluencyMonitor.h; sourceTree = ""; }; 31 | 9992231621251F1000131C34 /* LSLAppFluencyMonitor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSLAppFluencyMonitor.m; sourceTree = ""; }; 32 | 99ABCCD521194DA10074A0D5 /* AppPerformance.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppPerformance.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 99ABCCD821194DA10074A0D5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | 99ABCCDA21194DA10074A0D5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 35 | 99ABCCDD21194DA10074A0D5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 36 | 99ABCCDF21194DA30074A0D5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 99ABCCE221194DA30074A0D5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 38 | 99ABCCE421194DA30074A0D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | 99ABCCED211971300074A0D5 /* AppPerformance-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AppPerformance-Bridging-Header.h"; sourceTree = ""; }; 40 | 99ABCCEE211971300074A0D5 /* LSLCpuUsage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LSLCpuUsage.h; sourceTree = ""; }; 41 | 99ABCCEF211971300074A0D5 /* LSLCpuUsage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSLCpuUsage.m; sourceTree = ""; }; 42 | 99B8EE7D21197BB6007647F1 /* LSLApplicationMemory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LSLApplicationMemory.h; sourceTree = ""; }; 43 | 99B8EE7E21197BB6007647F1 /* LSLApplicationMemory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LSLApplicationMemory.m; sourceTree = ""; }; 44 | 99CEFA6C211BF02E0018F23C /* LSLFPSMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSLFPSMonitor.swift; sourceTree = ""; }; 45 | 99CEFA71211C2EC80018F23C /* LSLFPSTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LSLFPSTableViewController.swift; sourceTree = ""; }; 46 | 99E6A2E6211AE1F1004B8BBD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 99ABCCD221194DA10074A0D5 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXFrameworksBuildPhase section */ 58 | 59 | /* Begin PBXGroup section */ 60 | 9992230C212518A800131C34 /* 卡顿 */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 9992231021251D4E00131C34 /* LSLBacktraceLogger.h */, 64 | 9992231121251D4F00131C34 /* LSLBacktraceLogger.m */, 65 | 9992231521251F1000131C34 /* LSLAppFluencyMonitor.h */, 66 | 9992231621251F1000131C34 /* LSLAppFluencyMonitor.m */, 67 | ); 68 | path = "卡顿"; 69 | sourceTree = ""; 70 | }; 71 | 99ABCCCC21194DA10074A0D5 = { 72 | isa = PBXGroup; 73 | children = ( 74 | 99ABCCD721194DA10074A0D5 /* AppPerformance */, 75 | 99ABCCD621194DA10074A0D5 /* Products */, 76 | ); 77 | sourceTree = ""; 78 | }; 79 | 99ABCCD621194DA10074A0D5 /* Products */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 99ABCCD521194DA10074A0D5 /* AppPerformance.app */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | 99ABCCD721194DA10074A0D5 /* AppPerformance */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 99ABCCEA21194DA90074A0D5 /* Classes */, 91 | 99ABCCD821194DA10074A0D5 /* AppDelegate.swift */, 92 | 99ABCCDA21194DA10074A0D5 /* ViewController.swift */, 93 | 99E6A2E6211AE1F1004B8BBD /* main.swift */, 94 | 99ABCCDC21194DA10074A0D5 /* Main.storyboard */, 95 | 99ABCCDF21194DA30074A0D5 /* Assets.xcassets */, 96 | 99ABCCE121194DA30074A0D5 /* LaunchScreen.storyboard */, 97 | 99ABCCE421194DA30074A0D5 /* Info.plist */, 98 | ); 99 | path = AppPerformance; 100 | sourceTree = ""; 101 | }; 102 | 99ABCCEA21194DA90074A0D5 /* Classes */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 9992230C212518A800131C34 /* 卡顿 */, 106 | 99CEFA70211C2DF60018F23C /* fps */, 107 | 99CEFA6F211C2DEF0018F23C /* memory */, 108 | 99CEFA6E211C2DE50018F23C /* cpu */, 109 | 99ABCCED211971300074A0D5 /* AppPerformance-Bridging-Header.h */, 110 | ); 111 | path = Classes; 112 | sourceTree = ""; 113 | }; 114 | 99CEFA6E211C2DE50018F23C /* cpu */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 99ABCCEE211971300074A0D5 /* LSLCpuUsage.h */, 118 | 99ABCCEF211971300074A0D5 /* LSLCpuUsage.m */, 119 | ); 120 | path = cpu; 121 | sourceTree = ""; 122 | }; 123 | 99CEFA6F211C2DEF0018F23C /* memory */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 99B8EE7D21197BB6007647F1 /* LSLApplicationMemory.h */, 127 | 99B8EE7E21197BB6007647F1 /* LSLApplicationMemory.m */, 128 | ); 129 | path = memory; 130 | sourceTree = ""; 131 | }; 132 | 99CEFA70211C2DF60018F23C /* fps */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 99CEFA71211C2EC80018F23C /* LSLFPSTableViewController.swift */, 136 | 99CEFA6C211BF02E0018F23C /* LSLFPSMonitor.swift */, 137 | 99144B3F211C4DA500D59060 /* LSLWeakProxy.h */, 138 | 99144B40211C4DA500D59060 /* LSLWeakProxy.m */, 139 | ); 140 | path = fps; 141 | sourceTree = ""; 142 | }; 143 | /* End PBXGroup section */ 144 | 145 | /* Begin PBXNativeTarget section */ 146 | 99ABCCD421194DA10074A0D5 /* AppPerformance */ = { 147 | isa = PBXNativeTarget; 148 | buildConfigurationList = 99ABCCE721194DA30074A0D5 /* Build configuration list for PBXNativeTarget "AppPerformance" */; 149 | buildPhases = ( 150 | 99ABCCD121194DA10074A0D5 /* Sources */, 151 | 99ABCCD221194DA10074A0D5 /* Frameworks */, 152 | 99ABCCD321194DA10074A0D5 /* Resources */, 153 | ); 154 | buildRules = ( 155 | ); 156 | dependencies = ( 157 | ); 158 | name = AppPerformance; 159 | productName = AppPerformance; 160 | productReference = 99ABCCD521194DA10074A0D5 /* AppPerformance.app */; 161 | productType = "com.apple.product-type.application"; 162 | }; 163 | /* End PBXNativeTarget section */ 164 | 165 | /* Begin PBXProject section */ 166 | 99ABCCCD21194DA10074A0D5 /* Project object */ = { 167 | isa = PBXProject; 168 | attributes = { 169 | CLASSPREFIX = LSL; 170 | LastSwiftUpdateCheck = 0940; 171 | LastUpgradeCheck = 0940; 172 | ORGANIZATIONNAME = lisilong; 173 | TargetAttributes = { 174 | 99ABCCD421194DA10074A0D5 = { 175 | CreatedOnToolsVersion = 9.4.1; 176 | LastSwiftMigration = 0940; 177 | }; 178 | }; 179 | }; 180 | buildConfigurationList = 99ABCCD021194DA10074A0D5 /* Build configuration list for PBXProject "AppPerformance" */; 181 | compatibilityVersion = "Xcode 9.3"; 182 | developmentRegion = en; 183 | hasScannedForEncodings = 0; 184 | knownRegions = ( 185 | en, 186 | Base, 187 | ); 188 | mainGroup = 99ABCCCC21194DA10074A0D5; 189 | productRefGroup = 99ABCCD621194DA10074A0D5 /* Products */; 190 | projectDirPath = ""; 191 | projectRoot = ""; 192 | targets = ( 193 | 99ABCCD421194DA10074A0D5 /* AppPerformance */, 194 | ); 195 | }; 196 | /* End PBXProject section */ 197 | 198 | /* Begin PBXResourcesBuildPhase section */ 199 | 99ABCCD321194DA10074A0D5 /* Resources */ = { 200 | isa = PBXResourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | 99ABCCE321194DA30074A0D5 /* LaunchScreen.storyboard in Resources */, 204 | 99ABCCE021194DA30074A0D5 /* Assets.xcassets in Resources */, 205 | 99ABCCDE21194DA10074A0D5 /* Main.storyboard in Resources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXResourcesBuildPhase section */ 210 | 211 | /* Begin PBXSourcesBuildPhase section */ 212 | 99ABCCD121194DA10074A0D5 /* Sources */ = { 213 | isa = PBXSourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | 99CEFA6D211BF02E0018F23C /* LSLFPSMonitor.swift in Sources */, 217 | 99ABCCF0211971300074A0D5 /* LSLCpuUsage.m in Sources */, 218 | 99CEFA72211C2EC80018F23C /* LSLFPSTableViewController.swift in Sources */, 219 | 99E6A2E7211AE1F1004B8BBD /* main.swift in Sources */, 220 | 9992231221251D4F00131C34 /* LSLBacktraceLogger.m in Sources */, 221 | 99ABCCDB21194DA10074A0D5 /* ViewController.swift in Sources */, 222 | 9992231721251F1000131C34 /* LSLAppFluencyMonitor.m in Sources */, 223 | 99B8EE7F21197BB6007647F1 /* LSLApplicationMemory.m in Sources */, 224 | 99144B41211C4DA500D59060 /* LSLWeakProxy.m in Sources */, 225 | 99ABCCD921194DA10074A0D5 /* AppDelegate.swift in Sources */, 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXSourcesBuildPhase section */ 230 | 231 | /* Begin PBXVariantGroup section */ 232 | 99ABCCDC21194DA10074A0D5 /* Main.storyboard */ = { 233 | isa = PBXVariantGroup; 234 | children = ( 235 | 99ABCCDD21194DA10074A0D5 /* Base */, 236 | ); 237 | name = Main.storyboard; 238 | sourceTree = ""; 239 | }; 240 | 99ABCCE121194DA30074A0D5 /* LaunchScreen.storyboard */ = { 241 | isa = PBXVariantGroup; 242 | children = ( 243 | 99ABCCE221194DA30074A0D5 /* Base */, 244 | ); 245 | name = LaunchScreen.storyboard; 246 | sourceTree = ""; 247 | }; 248 | /* End PBXVariantGroup section */ 249 | 250 | /* Begin XCBuildConfiguration section */ 251 | 99ABCCE521194DA30074A0D5 /* Debug */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | ALWAYS_SEARCH_USER_PATHS = NO; 255 | CLANG_ANALYZER_NONNULL = YES; 256 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 257 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 258 | CLANG_CXX_LIBRARY = "libc++"; 259 | CLANG_ENABLE_MODULES = YES; 260 | CLANG_ENABLE_OBJC_ARC = YES; 261 | CLANG_ENABLE_OBJC_WEAK = YES; 262 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 263 | CLANG_WARN_BOOL_CONVERSION = YES; 264 | CLANG_WARN_COMMA = YES; 265 | CLANG_WARN_CONSTANT_CONVERSION = YES; 266 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 267 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 268 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 269 | CLANG_WARN_EMPTY_BODY = YES; 270 | CLANG_WARN_ENUM_CONVERSION = YES; 271 | CLANG_WARN_INFINITE_RECURSION = YES; 272 | CLANG_WARN_INT_CONVERSION = YES; 273 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 274 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 275 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 277 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 278 | CLANG_WARN_STRICT_PROTOTYPES = YES; 279 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 280 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 281 | CLANG_WARN_UNREACHABLE_CODE = YES; 282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 | CODE_SIGN_IDENTITY = "iPhone Developer"; 284 | COPY_PHASE_STRIP = NO; 285 | DEBUG_INFORMATION_FORMAT = dwarf; 286 | ENABLE_STRICT_OBJC_MSGSEND = YES; 287 | ENABLE_TESTABILITY = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu11; 289 | GCC_DYNAMIC_NO_PIC = NO; 290 | GCC_NO_COMMON_BLOCKS = YES; 291 | GCC_OPTIMIZATION_LEVEL = 0; 292 | GCC_PREPROCESSOR_DEFINITIONS = ( 293 | "DEBUG=1", 294 | "$(inherited)", 295 | ); 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 303 | MTL_ENABLE_DEBUG_INFO = YES; 304 | ONLY_ACTIVE_ARCH = YES; 305 | SDKROOT = iphoneos; 306 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 307 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 308 | }; 309 | name = Debug; 310 | }; 311 | 99ABCCE621194DA30074A0D5 /* Release */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | ALWAYS_SEARCH_USER_PATHS = NO; 315 | CLANG_ANALYZER_NONNULL = YES; 316 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 317 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 318 | CLANG_CXX_LIBRARY = "libc++"; 319 | CLANG_ENABLE_MODULES = YES; 320 | CLANG_ENABLE_OBJC_ARC = YES; 321 | CLANG_ENABLE_OBJC_WEAK = YES; 322 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 323 | CLANG_WARN_BOOL_CONVERSION = YES; 324 | CLANG_WARN_COMMA = YES; 325 | CLANG_WARN_CONSTANT_CONVERSION = YES; 326 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 327 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 328 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 329 | CLANG_WARN_EMPTY_BODY = YES; 330 | CLANG_WARN_ENUM_CONVERSION = YES; 331 | CLANG_WARN_INFINITE_RECURSION = YES; 332 | CLANG_WARN_INT_CONVERSION = YES; 333 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 334 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 335 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 336 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 337 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 338 | CLANG_WARN_STRICT_PROTOTYPES = YES; 339 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 340 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | CODE_SIGN_IDENTITY = "iPhone Developer"; 344 | COPY_PHASE_STRIP = NO; 345 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 346 | ENABLE_NS_ASSERTIONS = NO; 347 | ENABLE_STRICT_OBJC_MSGSEND = YES; 348 | GCC_C_LANGUAGE_STANDARD = gnu11; 349 | GCC_NO_COMMON_BLOCKS = YES; 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 357 | MTL_ENABLE_DEBUG_INFO = NO; 358 | SDKROOT = iphoneos; 359 | SWIFT_COMPILATION_MODE = wholemodule; 360 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 361 | VALIDATE_PRODUCT = YES; 362 | }; 363 | name = Release; 364 | }; 365 | 99ABCCE821194DA30074A0D5 /* Debug */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | CLANG_ENABLE_MODULES = YES; 370 | CODE_SIGN_STYLE = Automatic; 371 | DEVELOPMENT_TEAM = 9LB893D6J3; 372 | INFOPLIST_FILE = AppPerformance/Info.plist; 373 | LD_RUNPATH_SEARCH_PATHS = ( 374 | "$(inherited)", 375 | "@executable_path/Frameworks", 376 | ); 377 | PRODUCT_BUNDLE_IDENTIFIER = com.dream.AppPerformance; 378 | PRODUCT_NAME = "$(TARGET_NAME)"; 379 | SWIFT_OBJC_BRIDGING_HEADER = "AppPerformance/Classes/AppPerformance-Bridging-Header.h"; 380 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 381 | SWIFT_VERSION = 4.0; 382 | TARGETED_DEVICE_FAMILY = "1,2"; 383 | }; 384 | name = Debug; 385 | }; 386 | 99ABCCE921194DA30074A0D5 /* Release */ = { 387 | isa = XCBuildConfiguration; 388 | buildSettings = { 389 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 390 | CLANG_ENABLE_MODULES = YES; 391 | CODE_SIGN_STYLE = Automatic; 392 | DEVELOPMENT_TEAM = 9LB893D6J3; 393 | INFOPLIST_FILE = AppPerformance/Info.plist; 394 | LD_RUNPATH_SEARCH_PATHS = ( 395 | "$(inherited)", 396 | "@executable_path/Frameworks", 397 | ); 398 | PRODUCT_BUNDLE_IDENTIFIER = com.dream.AppPerformance; 399 | PRODUCT_NAME = "$(TARGET_NAME)"; 400 | SWIFT_OBJC_BRIDGING_HEADER = "AppPerformance/Classes/AppPerformance-Bridging-Header.h"; 401 | SWIFT_VERSION = 4.0; 402 | TARGETED_DEVICE_FAMILY = "1,2"; 403 | }; 404 | name = Release; 405 | }; 406 | /* End XCBuildConfiguration section */ 407 | 408 | /* Begin XCConfigurationList section */ 409 | 99ABCCD021194DA10074A0D5 /* Build configuration list for PBXProject "AppPerformance" */ = { 410 | isa = XCConfigurationList; 411 | buildConfigurations = ( 412 | 99ABCCE521194DA30074A0D5 /* Debug */, 413 | 99ABCCE621194DA30074A0D5 /* Release */, 414 | ); 415 | defaultConfigurationIsVisible = 0; 416 | defaultConfigurationName = Release; 417 | }; 418 | 99ABCCE721194DA30074A0D5 /* Build configuration list for PBXNativeTarget "AppPerformance" */ = { 419 | isa = XCConfigurationList; 420 | buildConfigurations = ( 421 | 99ABCCE821194DA30074A0D5 /* Debug */, 422 | 99ABCCE921194DA30074A0D5 /* Release */, 423 | ); 424 | defaultConfigurationIsVisible = 0; 425 | defaultConfigurationName = Release; 426 | }; 427 | /* End XCConfigurationList section */ 428 | }; 429 | rootObject = 99ABCCCD21194DA10074A0D5 /* Project object */; 430 | } 431 | -------------------------------------------------------------------------------- /AppPerformance.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AppPerformance.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AppPerformance/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/7. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | //@UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | 19 | // APP启动时间耗时,从mian函数开始到didFinishLaunchingWithOptions方法结束 20 | DispatchQueue.main.async { 21 | print("APP启动时间耗时,从mian函数开始到didFinishLaunchingWithOptions方法:\(CFAbsoluteTimeGetCurrent() - appStartLaunchTime)。") 22 | } 23 | return true 24 | } 25 | 26 | func applicationWillResignActive(_ application: UIApplication) { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 28 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 29 | } 30 | 31 | func applicationDidEnterBackground(_ application: UIApplication) { 32 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | } 35 | 36 | func applicationWillEnterForeground(_ application: UIApplication) { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | func applicationDidBecomeActive(_ application: UIApplication) { 41 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 42 | } 43 | 44 | func applicationWillTerminate(_ application: UIApplication) { 45 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 46 | } 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /AppPerformance/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /AppPerformance/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /AppPerformance/Assets.xcassets/cat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /AppPerformance/Assets.xcassets/cat.imageset/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilongLi/AppPerformance/473be027fbebabea5e05ebd596ad0051d019fd9f/AppPerformance/Assets.xcassets/cat.imageset/cat.png -------------------------------------------------------------------------------- /AppPerformance/Assets.xcassets/city.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "city.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /AppPerformance/Assets.xcassets/city.imageset/city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilongLi/AppPerformance/473be027fbebabea5e05ebd596ad0051d019fd9f/AppPerformance/Assets.xcassets/city.imageset/city.png -------------------------------------------------------------------------------- /AppPerformance/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /AppPerformance/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /AppPerformance/Classes/AppPerformance-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "LSLCpuUsage.h" 6 | #import "LSLApplicationMemory.h" 7 | #import "LSLWeakProxy.h" 8 | #import "LSLBacktraceLogger.h" 9 | #import "LSLAppFluencyMonitor.h" 10 | 11 | -------------------------------------------------------------------------------- /AppPerformance/Classes/cpu/LSLCpuUsage.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSLCpuUsage.h 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/7. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LSLCpuUsage : NSObject 12 | 13 | // 获取当前应用在CPU中的占有率 14 | + (double)getCpuUsage; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /AppPerformance/Classes/cpu/LSLCpuUsage.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSLCpuUsage.m 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/7. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | #import "LSLCpuUsage.h" 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | 16 | @implementation LSLCpuUsage 17 | 18 | + (double)getCpuUsage { 19 | kern_return_t kr; 20 | thread_array_t threadList; // 保存当前Mach task的线程列表 21 | mach_msg_type_number_t threadCount; // 保存当前Mach task的线程个数 22 | thread_info_data_t threadInfo; // 保存单个线程的信息列表 23 | mach_msg_type_number_t threadInfoCount; // 保存当前线程的信息列表大小 24 | thread_basic_info_t threadBasicInfo; // 线程的基本信息 25 | 26 | // 通过“task_threads”API调用获取指定 task 的线程列表 27 | // mach_task_self_,表示获取当前的 Mach task 28 | kr = task_threads(mach_task_self(), &threadList, &threadCount); 29 | if (kr != KERN_SUCCESS) { 30 | return -1; 31 | } 32 | double cpuUsage = 0; 33 | for (int i = 0; i < threadCount; i++) { 34 | threadInfoCount = THREAD_INFO_MAX; 35 | // 通过“thread_info”API调用来查询指定线程的信息 36 | // flavor参数传的是THREAD_BASIC_INFO,使用这个类型会返回线程的基本信息, 37 | // 定义在 thread_basic_info_t 结构体,包含了用户和系统的运行时间、运行状态和调度优先级等 38 | kr = thread_info(threadList[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount); 39 | if (kr != KERN_SUCCESS) { 40 | return -1; 41 | } 42 | 43 | threadBasicInfo = (thread_basic_info_t)threadInfo; 44 | if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) { 45 | cpuUsage += threadBasicInfo->cpu_usage; 46 | } 47 | } 48 | 49 | // 回收内存,防止内存泄漏 50 | vm_deallocate(mach_task_self(), (vm_offset_t)threadList, threadCount * sizeof(thread_t)); 51 | 52 | return cpuUsage / (double)TH_USAGE_SCALE * 100.0; 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /AppPerformance/Classes/fps/LSLFPSMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LSLFPSMonitor.swift 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/9. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LSLFPSMonitor: UILabel { 12 | 13 | private var link: CADisplayLink = CADisplayLink.init() 14 | private var count: NSInteger = 0 15 | private var lastTime: TimeInterval = 0.0 16 | private var fpsColor: UIColor = UIColor.green 17 | public var fps: Double = 0.0 18 | 19 | // MARK: - init 20 | 21 | override init(frame: CGRect) { 22 | var f = frame 23 | if f.size == CGSize.zero { 24 | f.size = CGSize(width: 55.0, height: 22.0) 25 | } 26 | super.init(frame: f) 27 | 28 | self.textColor = UIColor.white 29 | self.textAlignment = .center 30 | self.font = UIFont.init(name: "Menlo", size: 12.0) 31 | self.backgroundColor = UIColor.black 32 | 33 | link = CADisplayLink.init(target: LSLWeakProxy(target: self), selector: #selector(tick)) 34 | link.add(to: RunLoop.current, forMode: RunLoopMode.commonModes) 35 | } 36 | 37 | deinit { 38 | link.invalidate() 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | // MARK: - actions 46 | 47 | @objc func tick(link: CADisplayLink) { 48 | guard lastTime != 0 else { 49 | lastTime = link.timestamp 50 | return 51 | } 52 | 53 | count += 1 54 | let delta = link.timestamp - lastTime 55 | guard delta >= 1.0 else { 56 | return 57 | } 58 | 59 | lastTime = link.timestamp 60 | fps = Double(count) / delta 61 | let fpsText = "\(String.init(format: "%.3f", fps)) FPS" 62 | count = 0 63 | 64 | let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText)) 65 | if fps > 55.0{ 66 | fpsColor = UIColor.green 67 | } else if(fps >= 50.0 && fps <= 55.0) { 68 | fpsColor = UIColor.yellow 69 | } else { 70 | fpsColor = UIColor.red 71 | } 72 | attrMStr.setAttributes([NSAttributedStringKey.foregroundColor:fpsColor], range: NSMakeRange(0, attrMStr.length - 3)) 73 | attrMStr.setAttributes([NSAttributedStringKey.foregroundColor:UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3)) 74 | DispatchQueue.main.async { 75 | self.attributedText = attrMStr 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /AppPerformance/Classes/fps/LSLFPSTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LSLFPSTableViewController.swift 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/9. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let FPSCellKey = "FPSCell" 12 | 13 | class LSLFPSTableViewController: UITableViewController { 14 | 15 | var closeBtn: UIButton = { 16 | let btn = UIButton.init(frame: CGRect.init(x: UIScreen.main.bounds.width - 70, y: 20, width: 60, height: 36)) 17 | btn.addTarget(self, action: #selector(closeAction), for: UIControlEvents.touchUpInside) 18 | btn.layer.borderWidth = 1 19 | btn.layer.borderColor = UIColor.lightGray.cgColor 20 | btn.setTitle("close", for: UIControlState.normal) 21 | btn.setTitleColor(UIColor.black, for: UIControlState.normal) 22 | return btn 23 | }() 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: FPSCellKey) 29 | LSLAppFluencyMonitor().startMonitoring() 30 | } 31 | 32 | // MARK: - actions 33 | 34 | @objc func closeAction() { 35 | self.dismiss(animated: true, completion: nil) 36 | } 37 | 38 | // MARK: - Table view data source 39 | 40 | override func numberOfSections(in tableView: UITableView) -> Int { 41 | return 1 42 | } 43 | 44 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 45 | return 200 46 | } 47 | 48 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 49 | let cell = tableView.dequeueReusableCell(withIdentifier: FPSCellKey, for: indexPath) 50 | cell.textLabel?.text = "卡顿" 51 | cell.imageView?.image = UIImage.init(named: "city") 52 | if (indexPath.row > 0 && indexPath.row % 10 == 0) { 53 | usleep(100000); 54 | } 55 | return cell 56 | } 57 | 58 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 59 | return 100.0 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /AppPerformance/Classes/fps/LSLWeakProxy.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSLWeakProxy.h 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/9. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /** 14 | A proxy used to hold a weak object. 15 | It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink. 16 | 17 | sample code: 18 | 19 | @implementation MyView { 20 | NSTimer *_timer; 21 | } 22 | 23 | - (void)initTimer { 24 | LSLWeakProxy *proxy = [LSLWeakProxy proxyWithTarget:self]; 25 | _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES]; 26 | } 27 | 28 | - (void)tick:(NSTimer *)timer {...} 29 | @end 30 | */ 31 | @interface LSLWeakProxy : NSProxy 32 | 33 | /** 34 | The proxy target. 35 | */ 36 | @property (nonatomic, weak, readonly) id target; 37 | 38 | /** 39 | Creates a new weak proxy for target. 40 | 41 | @param target Target object. 42 | 43 | @return A new proxy object. 44 | */ 45 | - (instancetype)initWithTarget:(id)target; 46 | 47 | /** 48 | Creates a new weak proxy for target. 49 | 50 | @param target Target object. 51 | 52 | @return A new proxy object. 53 | */ 54 | + (instancetype)proxyWithTarget:(id)target; 55 | 56 | @end 57 | 58 | NS_ASSUME_NONNULL_END 59 | 60 | -------------------------------------------------------------------------------- /AppPerformance/Classes/fps/LSLWeakProxy.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSLWeakProxy.m 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/9. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | #import "LSLWeakProxy.h" 10 | 11 | /** 12 | 实现的原理: 使用 NSProxy 持有 NSTimer 的 target 13 | 不再用 NSTimer 直接持有 self,就不会导致 timer 对 self 的循环强引用了 14 | */ 15 | @implementation LSLWeakProxy 16 | 17 | - (instancetype)initWithTarget:(id)target { 18 | _target = target; 19 | return self; 20 | } 21 | 22 | //类方法 23 | + (instancetype)proxyWithTarget:(id)target { 24 | return [[LSLWeakProxy alloc] initWithTarget:target]; 25 | } 26 | 27 | #pragma mark - private 28 | 29 | - (id)forwardingTargetForSelector:(SEL)selector { 30 | return _target; 31 | } 32 | 33 | #pragma mark - over write 34 | 35 | //重写NSProxy如下两个方法,在处理消息转发时,将消息转发给真正的Target处理 36 | - (void)forwardInvocation:(NSInvocation *)invocation { 37 | void *null = NULL; 38 | [invocation setReturnValue:&null]; 39 | } 40 | 41 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { 42 | return [NSObject instanceMethodSignatureForSelector:@selector(init)]; 43 | } 44 | 45 | - (BOOL)respondsToSelector:(SEL)aSelector { 46 | return [_target respondsToSelector:aSelector]; 47 | } 48 | 49 | #pragma mark - 50 | 51 | - (BOOL)isEqual:(id)object { 52 | return [_target isEqual:object]; 53 | } 54 | 55 | - (NSUInteger)hash { 56 | return [_target hash]; 57 | } 58 | 59 | - (Class)superclass { 60 | return [_target superclass]; 61 | } 62 | 63 | - (Class)class { 64 | return [_target class]; 65 | } 66 | 67 | - (BOOL)isKindOfClass:(Class)aClass { 68 | return [_target isKindOfClass:aClass]; 69 | } 70 | 71 | - (BOOL)isMemberOfClass:(Class)aClass { 72 | return [_target isMemberOfClass:aClass]; 73 | } 74 | 75 | - (BOOL)conformsToProtocol:(Protocol *)aProtocol { 76 | return [_target conformsToProtocol:aProtocol]; 77 | } 78 | 79 | - (BOOL)isProxy { 80 | return YES; 81 | } 82 | 83 | - (NSString *)description { 84 | return [_target description]; 85 | } 86 | 87 | - (NSString *)debugDescription { 88 | return [_target debugDescription]; 89 | } 90 | 91 | @end 92 | -------------------------------------------------------------------------------- /AppPerformance/Classes/memory/LSLApplicationMemory.h: -------------------------------------------------------------------------------- 1 | // 2 | // LSLApplicationMemory.h 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/7. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface LSLApplicationMemory : NSObject 12 | 13 | // 获取当前应用的内存占用情况,和Xcode数值相差较大 14 | + (double)getResidentMemory; 15 | 16 | // 获取当前应用的内存占用情况,和Xcode数值相近 17 | + (double)getMemoryUsage; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /AppPerformance/Classes/memory/LSLApplicationMemory.m: -------------------------------------------------------------------------------- 1 | // 2 | // LSLApplicationMemory.m 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/7. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | 10 | #import "LSLApplicationMemory.h" 11 | #import 12 | #import 13 | 14 | @implementation LSLApplicationMemory 15 | 16 | // 获取当前应用的内存占用情况,和Xcode数值相差较大 17 | + (double)getResidentMemory { 18 | struct mach_task_basic_info info; 19 | mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; 20 | if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) == KERN_SUCCESS) { 21 | return info.resident_size / (1024 * 1024); 22 | } else { 23 | return -1.0; 24 | } 25 | } 26 | 27 | // 获取当前应用的内存占用情况,和Xcode数值相近 28 | + (double)getMemoryUsage { 29 | task_vm_info_data_t vmInfo; 30 | mach_msg_type_number_t count = TASK_VM_INFO_COUNT; 31 | if(task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count) == KERN_SUCCESS) { 32 | return (double)vmInfo.phys_footprint / (1024 * 1024); 33 | } else { 34 | return -1.0; 35 | } 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /AppPerformance/Classes/卡顿/LSLAppFluencyMonitor.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppPerformance 3 | // 4 | // Created by lisilong on 2018/8/9. 5 | // Copyright © 2018 lisilong. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface LSLAppFluencyMonitor : NSObject 13 | 14 | 15 | + (instancetype)monitor; 16 | 17 | - (void)startMonitoring; 18 | - (void)stopMonitoring; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /AppPerformance/Classes/卡顿/LSLAppFluencyMonitor.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppPerformance 3 | // 4 | // Created by lisilong on 2018/8/9. 5 | // Copyright © 2018 lisilong. All rights reserved. 6 | // 7 | 8 | #import "LSLAppFluencyMonitor.h" 9 | #import "LSLBacktraceLogger.h" 10 | 11 | 12 | #define lsl_SEMAPHORE_SUCCESS 0 13 | static BOOL lsl_is_monitoring = NO; 14 | static dispatch_semaphore_t lsl_semaphore; 15 | static NSTimeInterval lsl_time_out_interval = 0.05; 16 | 17 | 18 | @implementation LSLAppFluencyMonitor 19 | 20 | static inline dispatch_queue_t __lsl_fluecy_monitor_queue() { 21 | static dispatch_queue_t lsl_fluecy_monitor_queue; 22 | static dispatch_once_t once; 23 | dispatch_once(&once, ^{ 24 | lsl_fluecy_monitor_queue = dispatch_queue_create("com.dream.lsl_monitor_queue", NULL); 25 | }); 26 | return lsl_fluecy_monitor_queue; 27 | } 28 | 29 | static inline void __lsl_monitor_init() { 30 | static dispatch_once_t onceToken; 31 | dispatch_once(&onceToken, ^{ 32 | lsl_semaphore = dispatch_semaphore_create(0); 33 | }); 34 | } 35 | 36 | #pragma mark - Public 37 | + (instancetype)monitor { 38 | return [LSLAppFluencyMonitor new]; 39 | } 40 | 41 | - (void)startMonitoring { 42 | if (lsl_is_monitoring) { return; } 43 | lsl_is_monitoring = YES; 44 | __lsl_monitor_init(); 45 | dispatch_async(__lsl_fluecy_monitor_queue(), ^{ 46 | while (lsl_is_monitoring) { 47 | __block BOOL timeOut = YES; 48 | dispatch_async(dispatch_get_main_queue(), ^{ 49 | timeOut = NO; 50 | dispatch_semaphore_signal(lsl_semaphore); 51 | }); 52 | [NSThread sleepForTimeInterval: lsl_time_out_interval]; 53 | if (timeOut) { 54 | [LSLBacktraceLogger lsl_logMain]; // 打印主线程调用栈 55 | // [LSLBacktraceLogger lsl_logCurrent]; // 打印当前线程的调用栈 56 | // [LSLBacktraceLogger lsl_logAllThread]; // 打印所有线程的调用栈 57 | } 58 | dispatch_wait(lsl_semaphore, DISPATCH_TIME_FOREVER); 59 | } 60 | }); 61 | } 62 | 63 | - (void)stopMonitoring { 64 | if (!lsl_is_monitoring) { return; } 65 | lsl_is_monitoring = NO; 66 | } 67 | 68 | 69 | @end 70 | 71 | -------------------------------------------------------------------------------- /AppPerformance/Classes/卡顿/LSLBacktraceLogger.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppPerformance 3 | // 4 | // Created by lisilong on 2018/8/9. 5 | // Copyright © 2018 lisilong. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | /*! 11 | * @brief 线程堆栈上下文输出 12 | */ 13 | @interface LSLBacktraceLogger : NSObject 14 | 15 | + (NSString *)lsl_backtraceOfAllThread; 16 | + (NSString *)lsl_backtraceOfMainThread; 17 | + (NSString *)lsl_backtraceOfCurrentThread; 18 | + (NSString *)lsl_backtraceOfNSThread:(NSThread *)thread; 19 | 20 | + (void)lsl_logMain; 21 | + (void)lsl_logCurrent; 22 | + (void)lsl_logAllThread; 23 | 24 | + (NSString *)backtraceLogFilePath; 25 | + (void)recordLoggerWithFileName: (NSString *)fileName; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /AppPerformance/Classes/卡顿/LSLBacktraceLogger.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppPerformance 3 | // 4 | // Created by lisilong on 2018/8/9. 5 | // Copyright © 2018 lisilong. All rights reserved. 6 | // 7 | 8 | #import "LSLBacktraceLogger.h" 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | 18 | /*! 19 | * @brief 适配不同CPU的宏定义 20 | */ 21 | #if defined(__arm64__) 22 | #define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(3UL)) 23 | #define LSL_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT 24 | #define LSL_THREAD_STATE ARM_THREAD_STATE64 25 | #define LSL_FRAME_POINTER __fp 26 | #define LSL_STACK_POINTER __sp 27 | #define LSL_INSTRUCTION_ADDRESS __pc 28 | 29 | #elif defined(__arm__) 30 | #define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(1UL)) 31 | #define LSL_THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT 32 | #define LSL_THREAD_STATE ARM_THREAD_STATE 33 | #define LSL_FRAME_POINTER __r[7] 34 | #define LSL_STACK_POINTER __sp 35 | #define LSL_INSTRUCTION_ADDRESS __pc 36 | 37 | #elif defined(__x86_64__) 38 | #define DETAG_INSTRUCTION_ADDRESS(A) (A) 39 | #define LSL_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT 40 | #define LSL_THREAD_STATE x86_THREAD_STATE64 41 | #define LSL_FRAME_POINTER __rbp 42 | #define LSL_STACK_POINTER __rsp 43 | #define LSL_INSTRUCTION_ADDRESS __rip 44 | 45 | #elif defined(__i386__) 46 | #define DETAG_INSTRUCTION_ADDRESS(A) (A) 47 | #define LSL_THREAD_STATE_COUNT x86_THREAD_STATE32_COUNT 48 | #define LSL_THREAD_STATE x86_THREAD_STATE32 49 | #define LSL_FRAME_POINTER __ebp 50 | #define LSL_STACK_POINTER __esp 51 | #define LSL_INSTRUCTION_ADDRESS __eip 52 | 53 | #endif 54 | 55 | #if defined(__LP64__) 56 | #define TRACE_FMT "%-4d%-31s 0x%016lx %s + %lu" 57 | #define POINTER_FMT "0x%016lx" 58 | #define POINTER_SHORT_FMT "0x%lx" 59 | #define LSL_NLIST struct nlist_64 60 | #else 61 | #define TRACE_FMT "%-4d%-31s 0x%08lx %s + %lu" 62 | #define POINTER_FMT "0x%08lx" 63 | #define POINTER_SHORT_FMT "0x%lx" 64 | #define LSL_NLIST struct nlist 65 | 66 | #endif 67 | 68 | #define MAX_FRAME_NUMBER 30 69 | #define LOG_SEPERATE printf("\n"); 70 | #define FAILED_UINT_PTR_ADDRESS 0 71 | #define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1) 72 | 73 | 74 | typedef struct LSLStackFrameEntry{ 75 | const struct LSLStackFrameEntry * const previous; 76 | const uintptr_t return_address; 77 | } LSLStackFrameEntry; 78 | 79 | static mach_port_t main_thread_id; 80 | 81 | static inline dispatch_queue_t lsl_log_IO_queue() { 82 | static dispatch_queue_t lsl_log_IO_queue; 83 | static dispatch_once_t once; 84 | dispatch_once(&once, ^{ 85 | lsl_log_IO_queue = dispatch_queue_create("com.sindrilin.lsl_log_IO_queue", NULL); 86 | }); 87 | return lsl_log_IO_queue; 88 | } 89 | 90 | 91 | 92 | @implementation LSLBacktraceLogger 93 | 94 | + (void)load { 95 | main_thread_id = mach_thread_self(); 96 | } 97 | 98 | 99 | #pragma mark - Public 100 | + (NSString *)lsl_backtraceOfAllThread { 101 | thread_act_array_t threads; 102 | mach_msg_type_number_t thread_count = 0; 103 | 104 | kern_return_t kr = task_threads(mach_task_self(), &threads, &thread_count); 105 | if (kr != KERN_SUCCESS) { 106 | return @"Failed to get information of all threads"; 107 | } 108 | NSMutableString * result = @"".mutableCopy; 109 | for (int idx = 0; idx < thread_count; idx++) { 110 | [result appendString: _lsl_backtraceOfThread(threads[idx])]; 111 | } 112 | return result.copy; 113 | } 114 | 115 | + (NSString *)lsl_backtraceOfMainThread { 116 | return [self lsl_backtraceOfNSThread: [NSThread mainThread]]; 117 | } 118 | 119 | + (NSString *)lsl_backtraceOfCurrentThread { 120 | return [self lsl_backtraceOfNSThread: [NSThread currentThread]]; 121 | } 122 | 123 | + (NSString *)lsl_backtraceOfNSThread:(NSThread *)thread { 124 | return _lsl_backtraceOfThread(lsl_machThreadFromNSThread(thread)); 125 | } 126 | 127 | + (void)lsl_logMain { 128 | LOG_SEPERATE 129 | NSLog(@"%@", [self lsl_backtraceOfMainThread]); 130 | LOG_SEPERATE 131 | } 132 | 133 | + (void)lsl_logCurrent { 134 | LOG_SEPERATE 135 | NSLog(@"%@", [self lsl_backtraceOfCurrentThread]); 136 | LOG_SEPERATE 137 | } 138 | 139 | + (void)lsl_logAllThread { 140 | LOG_SEPERATE 141 | NSLog(@"%@", [self lsl_backtraceOfAllThread]); 142 | LOG_SEPERATE 143 | } 144 | 145 | 146 | #pragma mark - Generate 147 | thread_t lsl_machThreadFromNSThread(NSThread * nsthread) { 148 | char name[256]; 149 | thread_act_array_t list; 150 | mach_msg_type_number_t count; 151 | task_threads(mach_task_self(), &list, &count); 152 | 153 | NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970]; 154 | NSString * originName = nsthread.name; 155 | [nsthread setName: [NSString stringWithFormat: @"%f", timeStamp]]; 156 | 157 | if ([nsthread isMainThread]) { return (thread_t)main_thread_id; } 158 | 159 | for (int idx = 0; idx < count; idx++) { 160 | pthread_t pt = pthread_from_mach_thread_np(list[idx]); 161 | if ([nsthread isMainThread] && list[idx] == main_thread_id) { return list[idx]; } 162 | if (pt) { 163 | name[0] = '\0'; 164 | pthread_getname_np(pt, name, sizeof(name)); 165 | if (!strcmp(name, [nsthread name].UTF8String)) { 166 | [nsthread setName: originName]; 167 | return list[idx]; 168 | } 169 | } 170 | } 171 | [nsthread setName: originName]; 172 | return mach_thread_self(); 173 | } 174 | 175 | NSString * _lsl_backtraceOfThread(thread_t thread) { 176 | uintptr_t backtraceBuffer[MAX_FRAME_NUMBER]; 177 | int idx = 0; 178 | NSMutableString * result = [NSString stringWithFormat: @"Backtrace of Thread %u:\n======================================================================================\n", thread].mutableCopy; 179 | 180 | _STRUCT_MCONTEXT machineContext; 181 | if (!lsl_fillThreadStateIntoMachineContext(thread, &machineContext)) { 182 | return [NSString stringWithFormat: @"Failed to get information abount thread: %u", thread]; 183 | } 184 | const uintptr_t instructionAddress = lsl_mach_instructionAddress(&machineContext); 185 | backtraceBuffer[idx++] = instructionAddress; 186 | 187 | uintptr_t linkRegister = lsl_mach_linkRegister(&machineContext); 188 | if (linkRegister) { 189 | backtraceBuffer[idx++] = linkRegister; 190 | } 191 | if (instructionAddress == FAILED_UINT_PTR_ADDRESS) { return @"Failed to get instruction address"; } 192 | 193 | LSLStackFrameEntry frame = { 0 }; 194 | const uintptr_t framePtr = lsl_mach_framePointer(&machineContext); 195 | if (framePtr == FAILED_UINT_PTR_ADDRESS || 196 | lsl_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) { 197 | return @"failed to get frame pointer"; 198 | } 199 | 200 | for (; idx < MAX_FRAME_NUMBER; idx++) { 201 | backtraceBuffer[idx] = frame.return_address; 202 | if (backtraceBuffer[idx] == FAILED_UINT_PTR_ADDRESS || 203 | frame.previous == NULL || 204 | lsl_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) { 205 | break; 206 | } 207 | } 208 | 209 | int backtraceLength = idx; 210 | Dl_info symbolicated[backtraceLength]; 211 | lsl_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0); 212 | for (int idx = 0; idx < backtraceLength; idx++) { 213 | [result appendFormat: @"%@", lsl_logBacktraceEntry(idx, backtraceBuffer[idx], &symbolicated[idx])]; 214 | } 215 | [result appendString: @"\n"]; 216 | [result appendString: @"======================================================================================"]; 217 | return result.copy; 218 | } 219 | 220 | 221 | #pragma mark - operate machine context 222 | bool lsl_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT * machineContext) { 223 | mach_msg_type_number_t state_count = LSL_THREAD_STATE_COUNT; 224 | kern_return_t kr = thread_get_state(thread, LSL_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count); 225 | return (kr == KERN_SUCCESS); 226 | } 227 | 228 | uintptr_t lsl_mach_linkRegister(_STRUCT_MCONTEXT * const machineContext){ 229 | #if defined(__i386__) || defined(__x86_64__) 230 | return FAILED_UINT_PTR_ADDRESS; 231 | #else 232 | return machineContext->__ss.__lr; 233 | #endif 234 | } 235 | 236 | uintptr_t lsl_mach_framePointer(_STRUCT_MCONTEXT * const machineContext) { 237 | return machineContext->__ss.LSL_FRAME_POINTER; 238 | } 239 | 240 | uintptr_t lsl_mach_instructionAddress(_STRUCT_MCONTEXT * const machineContext) { 241 | return machineContext->__ss.LSL_INSTRUCTION_ADDRESS; 242 | } 243 | 244 | kern_return_t lsl_mach_copyMem(const void * src, const void * dst, const size_t numBytes) { 245 | vm_size_t bytesCopied = 0; 246 | return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied); 247 | } 248 | 249 | 250 | #pragma mark - handle symbolicate 251 | void lsl_symbolicate(const uintptr_t * const backtraceBuffer, Dl_info * const symbolsBuffer, const int numEntries, const int skippedEntries) { 252 | int idx = 0; 253 | if (!skippedEntries && idx < numEntries) { 254 | lsl_dladdr(backtraceBuffer[idx], &symbolsBuffer[idx]); 255 | idx++; 256 | } 257 | 258 | for (; idx < numEntries; idx++) { 259 | lsl_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[idx]), &symbolsBuffer[idx]); 260 | } 261 | } 262 | 263 | bool lsl_dladdr(const uintptr_t address, Dl_info * const info) { 264 | info->dli_fname = NULL; 265 | info->dli_fbase = NULL; 266 | info->dli_sname = NULL; 267 | info->dli_saddr = NULL; 268 | 269 | const uint32_t idx = lsl_imageIndexContainingAddress(address); 270 | if (idx == UINT_MAX) { return false; } 271 | 272 | const struct mach_header * header = _dyld_get_image_header(idx); 273 | const uintptr_t imageVMAddressSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx); 274 | const uintptr_t addressWithSlide = address - imageVMAddressSlide; 275 | const uintptr_t segmentBase = lsl_segmentBaseOfImageIndex(idx) + imageVMAddressSlide; 276 | if (segmentBase == FAILED_UINT_PTR_ADDRESS) { return false; } 277 | 278 | info->dli_fbase = (void *)header; 279 | info->dli_fname = _dyld_get_image_name(idx); 280 | 281 | const LSL_NLIST * bestMatch = NULL; 282 | uintptr_t bestDistance = ULONG_MAX; 283 | uintptr_t cmdPtr = lsl_firstCmdAfterHeader(header); 284 | if (cmdPtr == FAILED_UINT_PTR_ADDRESS) { return false; } 285 | 286 | for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) { 287 | const struct load_command * loadCmd = (struct load_command *)cmdPtr; 288 | if (loadCmd->cmd == LC_SYMTAB) { 289 | const struct symtab_command * symtabCmd = (struct symtab_command *)cmdPtr; 290 | const LSL_NLIST * symbolTable = (LSL_NLIST *)(segmentBase + symtabCmd->symoff); 291 | const uintptr_t stringTable = segmentBase + symtabCmd->stroff; 292 | 293 | for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) { 294 | if (symbolTable[iSym].n_value == FAILED_UINT_PTR_ADDRESS) { continue; } 295 | uintptr_t symbolBase = symbolTable[iSym].n_value; 296 | uintptr_t currentDistance = addressWithSlide - symbolBase; 297 | if ( (addressWithSlide >= symbolBase && currentDistance <= bestDistance) ) { 298 | bestMatch = symbolTable + iSym; 299 | bestDistance = currentDistance; 300 | } 301 | } 302 | if (bestMatch != NULL) { 303 | info->dli_saddr = (void *)(bestMatch->n_value + imageVMAddressSlide); 304 | info->dli_sname = (char *)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx); 305 | if (*info->dli_sname == '_') { 306 | info->dli_sname++; 307 | } 308 | if (info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) { 309 | info->dli_sname = NULL; 310 | } 311 | break; 312 | } 313 | } 314 | cmdPtr += loadCmd->cmdsize; 315 | } 316 | return true; 317 | } 318 | 319 | uintptr_t lsl_firstCmdAfterHeader(const struct mach_header * const header) { 320 | switch (header->magic) { 321 | case MH_MAGIC: 322 | case MH_CIGAM: 323 | return (uintptr_t)(header + 1); 324 | case MH_MAGIC_64: 325 | case MH_CIGAM_64: 326 | return (uintptr_t)(((struct mach_header_64*)header) + 1); 327 | default: 328 | return 0; 329 | } 330 | } 331 | 332 | uintptr_t lsl_segmentBaseOfImageIndex(const uint32_t idx) { 333 | const struct mach_header * header = _dyld_get_image_header(idx); 334 | 335 | uintptr_t cmdPtr = lsl_firstCmdAfterHeader(header); 336 | if (cmdPtr == FAILED_UINT_PTR_ADDRESS) { return FAILED_UINT_PTR_ADDRESS; } 337 | for (uint32_t idx = 0; idx < header->ncmds; idx++) { 338 | const struct load_command * loadCmd = (struct load_command *)cmdPtr; 339 | if (loadCmd->cmd == LC_SEGMENT) { 340 | const struct segment_command * segCmd = (struct segment_command *)cmdPtr; 341 | if (strcmp(segCmd->segname, SEG_LINKEDIT) == 0) { 342 | return segCmd->vmaddr - segCmd->fileoff; 343 | } 344 | } else if (loadCmd->cmd == LC_SEGMENT_64) { 345 | const struct segment_command_64 * segCmd = (struct segment_command_64 *)cmdPtr; 346 | if (strcmp(segCmd->segname, SEG_LINKEDIT) == 0) { 347 | return (uintptr_t)(segCmd->vmaddr - segCmd->fileoff); 348 | } 349 | } 350 | cmdPtr += loadCmd->cmdsize; 351 | } 352 | return FAILED_UINT_PTR_ADDRESS; 353 | } 354 | 355 | uint32_t lsl_imageIndexContainingAddress(const uintptr_t address) { 356 | const uint32_t imageCount = _dyld_image_count(); 357 | const struct mach_header * header = FAILED_UINT_PTR_ADDRESS; 358 | 359 | for (uint32_t iImg = 0; iImg < imageCount; iImg++) { 360 | header = _dyld_get_image_header(iImg); 361 | if (header != NULL) { 362 | uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg); 363 | uintptr_t cmdPtr = lsl_firstCmdAfterHeader(header); 364 | if (cmdPtr == FAILED_UINT_PTR_ADDRESS) { continue; } 365 | 366 | for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) { 367 | const struct load_command * loadCmd = (struct load_command *)cmdPtr; 368 | if (loadCmd->cmd == LC_SEGMENT) { 369 | const struct segment_command * segCmd = (struct segment_command *)cmdPtr; 370 | if (addressWSlide >= segCmd->vmaddr && 371 | addressWSlide < segCmd->vmaddr + segCmd->vmsize) { 372 | return iImg; 373 | } 374 | } else if (loadCmd->cmd == LC_SEGMENT_64) { 375 | const struct segment_command_64 * segCmd = (struct segment_command_64 *)cmdPtr; 376 | if (addressWSlide >= segCmd->vmaddr && 377 | addressWSlide < segCmd->vmaddr + segCmd->vmsize) { 378 | return iImg; 379 | } 380 | } 381 | cmdPtr += loadCmd->cmdsize; 382 | } 383 | } 384 | } 385 | return UINT_MAX; 386 | } 387 | 388 | 389 | #pragma mark - generate backtrace entry 390 | const char * lsl_lastPathEntry(const char * const path) { 391 | if (path == NULL) { return NULL; } 392 | char * lastFile = strrchr(path, '/'); 393 | return lastFile == NULL ? path: lastFile + 1; 394 | } 395 | 396 | NSString * lsl_logBacktraceEntry(const int entryNum, const uintptr_t address, const Dl_info * const dlInfo) { 397 | char faddrBuffer[20]; 398 | char saddrBuffer[20]; 399 | 400 | const char * fname = lsl_lastPathEntry(dlInfo->dli_fname); 401 | if (fname == NULL) { 402 | sprintf(faddrBuffer, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase); 403 | fname = faddrBuffer; 404 | } 405 | 406 | uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr; 407 | const char * sname = dlInfo->dli_sname; 408 | if (sname == NULL) { 409 | sprintf(saddrBuffer, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase); 410 | sname = saddrBuffer; 411 | offset = address - (uintptr_t)dlInfo->dli_fbase; 412 | } 413 | return [NSString stringWithFormat: @"%-30s 0x%08" PRIxPTR " %s + %lu\n", fname, (uintptr_t)address, sname, offset]; 414 | } 415 | 416 | + (NSString *)backtraceLogFilePath { 417 | static NSString * const fileDirectoryName = @"lsl_backtrace"; 418 | NSString * filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent: fileDirectoryName]; 419 | NSFileManager * manager = [NSFileManager defaultManager]; 420 | if (![manager fileExistsAtPath: filePath]) { 421 | [manager createDirectoryAtPath: filePath withIntermediateDirectories: YES attributes: nil error: nil]; 422 | } 423 | return filePath; 424 | } 425 | 426 | + (void)recordLoggerWithFileName: (NSString *)fileName { 427 | NSParameterAssert(fileName); 428 | dispatch_async(lsl_log_IO_queue(), ^{ 429 | NSDateFormatter * formatter = [NSDateFormatter new]; 430 | formatter.dateFormat = @"mmssS"; 431 | NSString * filePath = [[self backtraceLogFilePath] stringByAppendingString: [formatter stringFromDate: [NSDate date]]]; 432 | NSString * backtraceStackInfo = [self lsl_backtraceOfMainThread]; 433 | [backtraceStackInfo writeToFile: filePath atomically: YES encoding: NSUTF8StringEncoding error: nil]; 434 | }); 435 | } 436 | 437 | 438 | @end 439 | -------------------------------------------------------------------------------- /AppPerformance/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | 性能监控 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /AppPerformance/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/7. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | var timer: DispatchSourceTimer? // GIF播放定时器 13 | let infoLabel: UILabel = { // 展示cpu、内存信息 14 | let label = UILabel.init(frame: CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.size.width - 100, height: 30)) 15 | label.textColor = UIColor.white 16 | label.textAlignment = .center 17 | label.font = UIFont.init(name: "Menlo", size: 12.0) 18 | label.backgroundColor = UIColor.black 19 | return label 20 | }() 21 | 22 | let window: UIWindow = { 23 | let window = UIWindow.init(frame: CGRect.init(x: 0, y: 64, width: UIScreen.main.bounds.size.width, height: 30)) 24 | window.rootViewController = UIViewController() 25 | window.backgroundColor = UIColor.black 26 | window.makeKeyAndVisible() 27 | window.windowLevel = UIWindowLevelAlert 28 | return window 29 | }() 30 | 31 | var showFPSBtn: UIButton = { 32 | let btn = UIButton.init(frame: CGRect.init(x: 80, y: 200, width: 100, height: 30)) 33 | btn.addTarget(self, action: #selector(showFPSVC), for: UIControlEvents.touchUpInside) 34 | btn.layer.borderWidth = 1 35 | btn.layer.borderColor = UIColor.lightGray.cgColor 36 | btn.setTitle("卡顿", for: UIControlState.normal) 37 | btn.setTitleColor(UIColor.black, for: UIControlState.normal) 38 | return btn 39 | }() 40 | 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | window.center.x = self.view.center.x 46 | // CPU、内存 47 | window.rootViewController?.view.addSubview(infoLabel) 48 | // FPS 49 | let fpsView = LSLFPSMonitor.init(frame: CGRect.init(x: UIScreen.main.bounds.size.width - 100, y: 0, width: 100, height: 30)) 50 | window.rootViewController?.view.addSubview(fpsView) 51 | 52 | view.addSubview(showFPSBtn) 53 | 54 | showUsageInfo() 55 | } 56 | 57 | // MARK: - actions 58 | 59 | func showUsageInfo() { 60 | timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global()) 61 | timer?.schedule(deadline: .now(), repeating: 1) 62 | timer?.setEventHandler(handler: { [weak self] in 63 | DispatchQueue.main.async { 64 | let cpu = String.init(format: "%.2f", LSLCpuUsage.getCpuUsage()) 65 | let memory = String.init(format: "%.2f", LSLApplicationMemory.getUsage()) 66 | self?.infoLabel.text = "CPU: \(cpu)% Memory: \(memory) MB" 67 | } 68 | }) 69 | timer?.resume() 70 | } 71 | 72 | @objc func showFPSVC() { 73 | let fpsVC = LSLFPSTableViewController.init(style: .plain) 74 | self.navigationController?.pushViewController(fpsVC, animated: true) 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /AppPerformance/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // AppPerformance 4 | // 5 | // Created by lisilong on 2018/8/8. 6 | // Copyright © 2018 lisilong. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | var appStartLaunchTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent() 12 | 13 | UIApplicationMain( 14 | CommandLine.argc, 15 | UnsafeMutableRawPointer(CommandLine.unsafeArgv) 16 | .bindMemory( 17 | to: UnsafeMutablePointer.self, 18 | capacity: Int(CommandLine.argc)), 19 | nil, 20 | NSStringFromClass(AppDelegate.self) 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bruce Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App性能监控 2 | 3 | APP的性能监控包括: `CPU 占用率`、`内存使用情况`、`网络状况监控`、`启动时闪退`、`卡顿`、`FPS`、`使用时崩溃`、`耗电量监控`、`流量监控`等等。 4 | 5 | [博客地址](https://www.jianshu.com/u/b534ce5f8fae) 6 | 7 | 文中所有代码都已同步到[github](https://github.com/SilongLi/AppPerformance)中,有兴趣的可以`clone `下来一起探讨下。 8 | 9 | ## 环境 10 | > Xcode 10.0+ 11 | > 12 | > Swift 4.2 13 | 14 | ## 1 . CPU 占用率 15 | 16 | CPU作为手机的中央处理器,可以说是手机最关键的组成部分,所有应用程序都需要它来调度运行,资源有限。所以当我们的APP因设计不当,使 CPU 持续以高负载运行,将会出现APP卡顿、手机发热发烫、电量消耗过快等等严重影响用户体验的现象。 17 | 18 | 因此我们对应用在`CPU `中占用率的监控,将变得尤为重要。那么我们应该如何来获取CPU的占有率呢?! 19 | 20 | 我们都知道,我们的APP在运行的时候,会对应一个`Mach Task`,而Task下可能有多条线程同时执行任务,每个线程都是作为利用CPU的基本单位。所以我们可以通过获取当前`Mach Task`下,所有线程占用 CPU 的情况,来计算APP的 CPU 占用率。 21 | 22 | 在《OS X and iOS Kernel Programming》是这样描述 Mach task 的: 23 | > 任务(task)是一种容器(container)对象,虚拟内存空间和其他资源都是通过这个容器对象管理的,这些资源包括设备和其他句柄。严格地说,Mach 的任务并不是其他操作系统中所谓的进程,因为 Mach 作为一个微内核的操作系统,并没有提供“进程”的逻辑,而只是提供了最基本的实现。不过在 BSD 的模型中,这两个概念有1:1的简单映射,每一个 BSD 进程(也就是 OS X 进程)都在底层关联了一个 Mach 任务对象。 24 | 25 | ![Mac OS X 中进程子系统组成的概念图](https://upload-images.jianshu.io/upload_images/877439-18e562c2a9f7612d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 26 | 27 | > iOS 是基于 `Apple Darwin` 内核,由`kernel `、`XNU `和`Runtime ` 组成,而`XNU ` 是`Darwin ` 的内核,它是“X is not UNIX”的缩写,是一个混合内核,由 Mach 微内核和 BSD 组成。Mach 内核是轻量级的平台,只能完成操作系统最基本的职责,比如:进程和线程、虚拟内存管理、任务调度、进程通信和消息传递机制等。其他的工作,例如文件操作和设备访问,都由 BSD 层实现。 28 | 29 | iOS 的线程技术与Mac OS X类似,也是基于 Mach 线程技术实现的,在 Mach 层中`thread_basic_info ` 结构体封装了单个线程的基本信息: 30 | ~~~C 31 | struct thread_basic_info { 32 | time_value_t user_time; /* user run time */ 33 | time_value_t system_time; /* system run time */ 34 | integer_t cpu_usage; /* scaled cpu usage percentage */ 35 | policy_t policy; /* scheduling policy in effect */ 36 | integer_t run_state; /* run state (see below) */ 37 | integer_t flags; /* various flags (see below) */ 38 | integer_t suspend_count; /* suspend count for thread */ 39 | integer_t sleep_time; /* number of seconds that thread has been sleeping */ 40 | } 41 | ~~~ 42 | 43 | 一个`Mach Task`包含它的线程列表。内核提供了`task_threads ` API 调用获取指定 task 的线程列表,然后可以通过`thread_info ` API调用来查询指定线程的信息,在 thread_act.h 中有相关定义。 44 | 45 | `task_threads ` 将`target_task ` 任务中的所有线程保存在`act_list `数组中,act_listCnt表示线程个数: 46 | 47 | ~~~C 48 | kern_return_t task_threads 49 | ( 50 | task_t target_task, 51 | thread_act_array_t *act_list, 52 | mach_msg_type_number_t *act_listCnt 53 | ); 54 | ~~~ 55 | 56 | `thread_info `结构如下: 57 | 58 | ~~~C 59 | kern_return_t thread_info 60 | ( 61 | thread_act_t target_act, 62 | thread_flavor_t flavor, // 传入不同的宏定义获取不同的线程信息 63 | thread_info_t thread_info_out, // 查询到的线程信息 64 | mach_msg_type_number_t *thread_info_outCnt // 信息的大小 65 | ); 66 | ~~~ 67 | 68 | 所以我们如下来获取CPU的占有率: 69 | 70 | ~~~Object-C 71 | #import "LSLCpuUsage.h" 72 | #import 73 | #import 74 | #import 75 | #import 76 | #import 77 | 78 | @implementation LSLCpuUsage 79 | 80 | + (double)getCpuUsage { 81 | kern_return_t kr; 82 | thread_array_t threadList; // 保存当前Mach task的线程列表 83 | mach_msg_type_number_t threadCount; // 保存当前Mach task的线程个数 84 | thread_info_data_t threadInfo; // 保存单个线程的信息列表 85 | mach_msg_type_number_t threadInfoCount; // 保存当前线程的信息列表大小 86 | thread_basic_info_t threadBasicInfo; // 线程的基本信息 87 | 88 | // 通过“task_threads”API调用获取指定 task 的线程列表 89 | // mach_task_self_,表示获取当前的 Mach task 90 | kr = task_threads(mach_task_self(), &threadList, &threadCount); 91 | if (kr != KERN_SUCCESS) { 92 | return -1; 93 | } 94 | double cpuUsage = 0; 95 | for (int i = 0; i < threadCount; i++) { 96 | threadInfoCount = THREAD_INFO_MAX; 97 | // 通过“thread_info”API调用来查询指定线程的信息 98 | // flavor参数传的是THREAD_BASIC_INFO,使用这个类型会返回线程的基本信息, 99 | // 定义在 thread_basic_info_t 结构体,包含了用户和系统的运行时间、运行状态和调度优先级等 100 | kr = thread_info(threadList[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount); 101 | if (kr != KERN_SUCCESS) { 102 | return -1; 103 | } 104 | 105 | threadBasicInfo = (thread_basic_info_t)threadInfo; 106 | if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) { 107 | cpuUsage += threadBasicInfo->cpu_usage; 108 | } 109 | } 110 | 111 | // 回收内存,防止内存泄漏 112 | vm_deallocate(mach_task_self(), (vm_offset_t)threadList, threadCount * sizeof(thread_t)); 113 | 114 | return cpuUsage / (double)TH_USAGE_SCALE * 100.0; 115 | } 116 | @end 117 | ~~~ 118 | 119 | ---------- 120 | 121 | ## 2. 内存 122 | 123 | 虽然现在的手机内存越来越大,但毕竟是有限的,如果因为我们的应用设计不当造成内存过高,可能面临被系统“干掉”的风险,这对用户来说是毁灭性的体验。 124 | 125 | Mach task 的内存使用信息存放在`mach_task_basic_info `结构体中 ,其中`resident_size ` 为应用使用的物理内存大小,`virtual_size `为虚拟内存大小,在`task_info.h`中: 126 | ~~~C 127 | #define MACH_TASK_BASIC_INFO 20 /* always 64-bit basic info */ 128 | struct mach_task_basic_info { 129 | mach_vm_size_t virtual_size; /* virtual memory size (bytes) */ 130 | mach_vm_size_t resident_size; /* resident memory size (bytes) */ 131 | mach_vm_size_t resident_size_max; /* maximum resident memory size (bytes) */ 132 | time_value_t user_time; /* total user run time for 133 | terminated threads */ 134 | time_value_t system_time; /* total system run time for 135 | terminated threads */ 136 | policy_t policy; /* default policy for new threads */ 137 | integer_t suspend_count; /* suspend count for task */ 138 | }; 139 | ~~~ 140 | 141 | 获取方式是通过`task_info `API 根据指定的 flavor 类型,返回 target_task 的信息,在`task.h`中: 142 | ~~~C 143 | kern_return_t task_info 144 | ( 145 | task_name_t target_task, 146 | task_flavor_t flavor, 147 | task_info_t task_info_out, 148 | mach_msg_type_number_t *task_info_outCnt 149 | ); 150 | ~~~ 151 | 152 | 笔者尝试过使用如下方式获取内存情况,基本和腾讯的[GT](https://github.com/Tencent/GT)的相近,但是和Xcode和Instruments的值有较大差距: 153 | 154 | ~~~C 155 | // 获取当前应用的内存占用情况,和Xcode数值相差较大 156 | + (double)getResidentMemory { 157 | struct mach_task_basic_info info; 158 | mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; 159 | if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) == KERN_SUCCESS) { 160 | return info.resident_size / (1024 * 1024); 161 | } else { 162 | return -1.0; 163 | } 164 | } 165 | ~~~ 166 | 167 | 后来看了一篇博主讨论了这个问题,说使用`phys_footprint `才是正解,[博客地址](http://www.samirchen.com/ios-app-memory-usage/)。亲测,基本和Xcode的数值相近。 168 | 169 | ~~~C 170 | // 获取当前应用的内存占用情况,和Xcode数值相近 171 | + (double)getMemoryUsage { 172 | task_vm_info_data_t vmInfo; 173 | mach_msg_type_number_t count = TASK_VM_INFO_COUNT; 174 | if(task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count) == KERN_SUCCESS) { 175 | return (double)vmInfo.phys_footprint / (1024 * 1024); 176 | } else { 177 | return -1.0; 178 | } 179 | } 180 | ~~~ 181 | 182 | 博主文中提到:关于 `phys_footprint` 的定义可以在 [XNU](https://github.com/apple/darwin-xnu) 源码中,找到 `osfmk/kern/task.c` 里对于 `phys_footprint` 的注释,博主认为注释里提到的公式计算的应该才是应用实际使用的物理内存。 183 | 184 | ~~~C 185 | /* 186 | * phys_footprint 187 | * Physical footprint: This is the sum of: 188 | * + (internal - alternate_accounting) 189 | * + (internal_compressed - alternate_accounting_compressed) 190 | * + iokit_mapped 191 | * + purgeable_nonvolatile 192 | * + purgeable_nonvolatile_compressed 193 | * + page_table 194 | * 195 | * internal 196 | * The task's anonymous memory, which on iOS is always resident. 197 | * 198 | * internal_compressed 199 | * Amount of this task's internal memory which is held by the compressor. 200 | * Such memory is no longer actually resident for the task [i.e., resident in its pmap], 201 | * and could be either decompressed back into memory, or paged out to storage, depending 202 | * on our implementation. 203 | * 204 | * iokit_mapped 205 | * IOKit mappings: The total size of all IOKit mappings in this task, regardless of 206 | clean/dirty or internal/external state]. 207 | * 208 | * alternate_accounting 209 | * The number of internal dirty pages which are part of IOKit mappings. By definition, these pages 210 | * are counted in both internal *and* iokit_mapped, so we must subtract them from the total to avoid 211 | * double counting. 212 | */ 213 | ~~~ 214 | 215 | **当然我也是赞同这点的>.<**。 216 | 217 | 218 | ------- 219 | 220 | ## 3. 启动时间 221 | 222 | APP的启动时间,直接影响用户对你的APP的第一体验和判断。如果启动时间过长,不单单体验直线下降,而且可能会激发苹果的watch dog机制kill掉你的APP,那就悲剧了,用户会觉得APP怎么一启动就卡死然后崩溃了,不能用,然后长按APP点击删除键。(Xcode在debug模式下是没有开启watch dog的,所以我们一定要连接真机测试我们的APP) 223 | 224 | 在衡量APP的启动时间之前我们先了解下,APP的启动流程: 225 | 226 | ![APP启动过程](https://upload-images.jianshu.io/upload_images/877439-77c0062f78b28b87.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 227 | 228 | APP的启动可以分为两个阶段,即`main()`执行之前和`main()`执行之后。总结如下: 229 | 230 | > t(App 总启动时间) = t1( `main()`之前的加载时间 ) + t2( `main()`之后的加载时间 )。 231 | > - t1 = 系统的 dylib (动态链接库)和 App 可执行文件的加载时间; 232 | > - t2 = `main()`函数执行之后到`AppDelegate `类中的`applicationDidFinishLaunching:withOptions:`方法执行结束前这段时间。 233 | 234 | 所以我们对APP启动时间的获取和优化都是从这两个阶段着手,下面先看看`main()`函数执行之前如何获取启动时间。 235 | 236 | ### 衡量main()函数执行之前的耗时 237 | 238 | 对于衡量main()之前也就是time1的耗时,苹果官方提供了一种方法,即在真机调试的时候,勾选`DYLD_PRINT_STATISTICS `选项(如果想获取更详细的信息可以使用`DYLD_PRINT_STATISTICS_DETAILS `),如下图: 239 | 240 | ![main()函数之前](https://upload-images.jianshu.io/upload_images/877439-f31da849c9cae6b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 241 | 242 | 输出结果如下: 243 | ~~~Swift 244 | Total pre-main time: 34.22 milliseconds (100.0%) 245 | dylib loading time: 14.43 milliseconds (42.1%) 246 | rebase/binding time: 1.82 milliseconds (5.3%) 247 | ObjC setup time: 3.89 milliseconds (11.3%) 248 | initializer time: 13.99 milliseconds (40.9%) 249 | slowest intializers : 250 | libSystem.B.dylib : 2.20 milliseconds (6.4%) 251 | libBacktraceRecording.dylib : 2.90 milliseconds (8.4%) 252 | libMainThreadChecker.dylib : 6.55 milliseconds (19.1%) 253 | libswiftCoreImage.dylib : 0.71 milliseconds (2.0%) 254 | ~~~ 255 | 系统级别的动态链接库,因为苹果做了优化,所以耗时并不多,而大多数时候,t1的时间大部分会消耗在我们自身App中的代码上和链接第三方库上。 256 | 257 | 所以我们应如何减少main()调用之前的耗时呢,我们可以优化的点有: 258 | > 1. 减少不必要的`framework `,特别是第三方的,因为动态链接比较耗时; 259 | > 2. `check framework `应设为`optional `和`required `,如果该`framework `在当前App支持的所有iOS系统版本都存在,那么就设为`required `,否则就设为`optional `,因为`optional `会有些额外的检查; 260 | > 3. 合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具: 261 | > - 删减一些无用的静态变量 262 | > - 删减没有被调用到或者已经废弃的方法 263 | > - 将不必须在`+load`方法中做的事情延迟到`+initialize`中 264 | > - 尽量不要用C++虚函数(创建虚函数表有开销) 265 | 266 | ### 衡量main()函数执行之后的耗时 267 | 第二阶段的耗时统计,我们认为是从`main ()`执行之后到`applicationDidFinishLaunching:withOptions:`方法最后,那么我们可以通过打点的方式进行统计。 268 | Objective-C项目因为有main文件,所以我么直接可以通过添加代码获取: 269 | 270 | ~~~Swift 271 | // 1. 在 main.m 添加如下代码: 272 | CFAbsoluteTime AppStartLaunchTime; 273 | 274 | int main(int argc, char * argv[]) { 275 | AppStartLaunchTime = CFAbsoluteTimeGetCurrent(); 276 | ..... 277 | } 278 | 279 | // 2. 在 AppDelegate.m 的开头声明 280 | extern CFAbsoluteTime AppStartLaunchTime; 281 | 282 | // 3. 最后在AppDelegate.m 的 didFinishLaunchingWithOptions 中添加 283 | dispatch_async(dispatch_get_main_queue(), ^{ 284 | NSLog(@"App启动时间--%f",(CFAbsoluteTimeGetCurrent()-AppStartLaunchTime)); 285 | }); 286 | ~~~ 287 | 288 | 大家都知道Swift项目是没有main文件,官方给了如下解释: 289 | > In Xcode, Mac templates default to including a “main.swift” file, but for iOS apps the default for new iOS project templates is to add @UIApplicationMain to a regular Swift file. This causes the compiler to synthesize a mainentry point for your iOS app, and eliminates the need for a “main.swift” file. 290 | 291 | 也就是说,通过添加`@UIApplicationMain`标志的方式,帮我们添加了mian函数了。所以如果是我们需要在mian函数中做一些其它操作的话,需要我们自己来创建main.swift文件,这个也是苹果允许的。 292 | 293 | - 1. 删除`AppDelegate`类中的 `@UIApplicationMain`标志; 294 | - 2. 自行创建main.swift文件,并添加程序入口: 295 | ~~~Swift 296 | import UIKit 297 | 298 | var appStartLaunchTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent() 299 | 300 | UIApplicationMain( 301 | CommandLine.argc, 302 | UnsafeMutableRawPointer(CommandLine.unsafeArgv) 303 | .bindMemory( 304 | to: UnsafeMutablePointer.self, 305 | capacity: Int(CommandLine.argc)), 306 | nil, 307 | NSStringFromClass(AppDelegate.self) 308 | ) 309 | ~~~ 310 | 311 | - 3. 在AppDelegate的`didFinishLaunchingWithOptions :`方法最后添加: 312 | ~~~Swift 313 | // APP启动时间耗时,从mian函数开始到didFinishLaunchingWithOptions方法结束 314 | DispatchQueue.main.async { 315 | print("APP启动时间耗时,从mian函数开始到didFinishLaunchingWithOptions方法:\(CFAbsoluteTimeGetCurrent() - appStartLaunchTime)。") 316 | } 317 | ~~~ 318 | 319 | main函数之后的优化: 320 | > - 尽量使用纯代码编写,减少xib的使用; 321 | > - 启动阶段的网络请求,是否都放到异步请求; 322 | > - 一些耗时的操作是否可以放到后面去执行,或异步执行等。 323 | 324 | ## 4. FPS 325 | 326 | 通过维基百科我们知道,`FPS `是`Frames Per Second` 的简称缩写,意思是每秒传输帧数,也就是我们常说的“刷新率(单位为Hz)。 327 | 328 | `FPS `是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的画面就会愈流畅,`FPS `值越低就越卡顿,所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。一般我们的APP的`FPS `只要保持在 50-60之间,用户体验都是比较流畅的。 329 | 330 | 苹果手机屏幕的正常刷新频率是每秒60次,即可以理解为`FPS`值为60。我们都知道`CADisplayLink `是和屏幕刷新频率保存一致,所以我们是否可以通过它来监控我们的`FPS`呢?! 331 | 332 | 首先`CADisplayLink `是什么 333 | > `CADisplayLink `是`CoreAnimation `提供的另一个类似于`NSTimer `的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和`NSTimer `很类似,所以它实际上就是一个内置实现的替代,但是和`timeInterval `以秒为单位不同,`CADisplayLink `有一个整型的`frameInterval `属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定`frameInterval `为2,就是说动画每隔一帧执行一次(一秒钟30帧)。 334 | 335 | 使用`CADisplayLink `监控界面的`FPS`值,参考自[YYFPSLabel](https://github.com/ibireme/YYKit/blob/master/Demo/YYKitDemo/YYFPSLabel.m): 336 | 337 | ~~~Swift 338 | // 详情代码可以clone demo查看,或查看作者的博客 339 | ~~~ 340 | 341 | 通过`CADisplayLink `的实现方式,并真机测试之后,确实是可以在很大程度上满足了监控`FPS`的业务需求和为提高用户体验提供参考,但是和Instruments的值可能会有些出入。下面我们来讨论下使用`CADisplayLink `的方式,可能存在的问题。 342 | 343 | - (1). 和Instruments值对比有出入,原因如下: 344 | 345 | >`CADisplayLink `运行在被添加的那个`RunLoop `之中(一般是在主线程中),因此它只能检测出当前`RunLoop `下的帧率。`RunLoop `中所管理的任务的调度时机,受任务所处的`RunLoopMode `和CPU的繁忙程度所影响。所以想要真正定位到准确的性能问题所在,最好还是通过Instrument来确认。 346 | 347 | - (2). 使用`CADisplayLink `可能存在的**循环引用**问题。 348 | 349 | 例如以下写法: 350 | ~~~Swift 351 | let link = CADisplayLink.init(target: self, selector: #selector(tick)) 352 | 353 | let timer = Timer.init(timeInterval: 1.0, target: self, selector: #selector(tick), userInfo: nil, repeats: true) 354 | 355 | ~~~ 356 | 357 | **原因**:以上两种用法,都会对 self 强引用,此时 timer持有 self,self 也持有 timer,循环引用导致页面 dismiss 时,双方都无法释放,造成循环引用。此时使用 weak 也不能有效解决: 358 | ~~~Swift 359 | weak var weakSelf = self 360 | let link = CADisplayLink.init(target: weakSelf, selector: #selector(tick)) 361 | ~~~ 362 | 363 | 那么我们应该怎样解决这个问题,有人会说在`deinit `(或`dealloc `)中调用定时器的`invalidate`方法,但是这是无效的,因为已经造成循环引用了,不会走到这个方法的。 364 | 365 | `YYKit`作者提供的解决方案是使用 [YYWeakProxy](https://github.com/ibireme/YYKit/blob/master/YYKit/Utility/YYWeakProxy.m),这个`YYWeakProxy `不是继承自`NSObject`而是继承`NSProxy `。 366 | 367 | > ### NSProxy 368 | > An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. 369 | 370 | `NSProxy `是一个为对象定义接口的抽象父类,并且为其它对象或者一些不存在的对象扮演了替身角色。[具体的可以看下NSProxy的官方文档](https://developer.apple.com/documentation/foundation/nsproxy) 371 | 修改后代码如下,亲测定时器如愿释放,`LSLWeakProxy `的具体实现代码已经同步到[github](https://github.com/SilongLi/AppPerformance)中。 372 | 373 | ~~~Swift 374 | let link = CADisplayLink.init(target: LSLWeakProxy(target: self), selector: #selector(tick)) 375 | ~~~ 376 | 377 | ## 5. 卡顿 378 | 379 | 在了解卡顿产生的原因之前,先看下屏幕显示图像的原理。 380 | 381 | #### 屏幕显示图像的原理: 382 | 383 | ![屏幕绘制原理](https://upload-images.jianshu.io/upload_images/877439-d8f58796bc648a9d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 384 | 385 | 现在的手机设备基本都是采用双缓存+垂直同步(即V-Sync)屏幕显示技术。 386 | 387 | 如上图所示,系统内`CPU `、`GPU `和显示器是协同完成显示工作的。其中`CPU `负责计算显示的内容,例如视图创建、布局计算、图片解码、文本绘制等等。随后`CPU `将计算好的内容提交给`GPU `,由`GPU `进行变换、合成、渲染。`GPU `会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,`GPU `会直接将视频控制器的指针指向第二个容器(双缓存原理)。这里,`GPU `会等待显示器的`VSync `(即垂直同步)信号发出后,才进行新的一帧渲染和缓冲区更新(这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟)。 388 | 389 | #### 卡顿的原因: 390 | 391 | ![掉帧](https://upload-images.jianshu.io/upload_images/877439-13fba20b4f543bbb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 392 | 393 | 由上面屏幕显示的原理,采用了垂直同步机制的手机设备。如果在一个`VSync ` 时间内,`CPU ` 或`GPU ` 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。例如在主线程里添加了阻碍主线程去响应点击、滑动事件、以及阻碍主线程的UI绘制等的代码,都是造成卡顿的常见原因。 394 | 395 | #### 卡顿监控: 396 | 卡顿监控一般有两种实现方案: 397 | - (1). **主线程卡顿监控**。通过子线程监测主线程的`runLoop `,判断两个状态区域之间的耗时是否达到一定阈值。 398 | 399 | - (2). **`FPS`监控**。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。`FPS`的监控实现原理,上面已经探讨过这里略过。 400 | 401 | 在使用`FPS `监控性能的实践过程中,发现 `FPS ` 值抖动较大,造成侦测卡顿比较困难。为了解决这个问题,**通过采用检测主线程每次执行消息循环的时间,当这一时间大于规定的阈值时,就记为发生了一次卡顿的方式来监控**。 402 | 这也是美团的移动端采用的性能监控[Hertz ](https://tech.meituan.com/hertz.html)方案,微信团队也在实践过程中提出来类似的方案--[微信读书 iOS 性能优化总结](https://wereadteam.github.io/2016/05/03/WeRead-Performance/)。 403 | 404 | ![美团Hertz方案流程图](https://upload-images.jianshu.io/upload_images/877439-a61af10b3a84c76f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 405 | 406 | 方案的提出,是根据滚动引发的Sources事件或其它交互事件总是被快速的执行完成,然后进入到kCFRunLoopBeforeWaiting状态下;假如在滚动过程中发生了卡顿现象,那么RunLoop必然会保持kCFRunLoopAfterWaiting或者kCFRunLoopBeforeSources这两个状态之一。 407 | 408 | #### 所以监控主线程卡顿的方案一: 409 | 开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况。 410 | 但是由于主线程的RunLoop在闲置时基本处于Before Waiting状态,这就导致了即便没有发生任何卡顿,这种检测方式也总能认定主线程处在卡顿状态。 411 | 412 | 为了解决这个问题寒神([南栀倾寒](https://www.jianshu.com/u/cc1e4faec5f7))给出了自己的解决方案,`Swift`的卡顿检测第三方[ANREye](https://link.jianshu.com?t=https://github.com/zixun/ANREye)。这套卡顿监控方案大致思路为:创建一个子线程进行循环检测,每次检测时设置标记位为`YES`,然后派发任务到主线程中将标记位设置为`NO`。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成`NO`,如果没有说明主线程发生了卡顿。 413 | 414 | 结合这套方案,当主线程处在Before Waiting状态的时候,通过派发任务到主线程来设置标记位的方式处理常态下的卡顿检测: 415 | 416 | ~~~Objective-C 417 | #define lsl_SEMAPHORE_SUCCESS 0 418 | static BOOL lsl_is_monitoring = NO; 419 | static dispatch_semaphore_t lsl_semaphore; 420 | static NSTimeInterval lsl_time_out_interval = 0.05; 421 | 422 | 423 | @implementation LSLAppFluencyMonitor 424 | 425 | static inline dispatch_queue_t __lsl_fluecy_monitor_queue() { 426 | static dispatch_queue_t lsl_fluecy_monitor_queue; 427 | static dispatch_once_t once; 428 | dispatch_once(&once, ^{ 429 | lsl_fluecy_monitor_queue = dispatch_queue_create("com.dream.lsl_monitor_queue", NULL); 430 | }); 431 | return lsl_fluecy_monitor_queue; 432 | } 433 | 434 | static inline void __lsl_monitor_init() { 435 | static dispatch_once_t onceToken; 436 | dispatch_once(&onceToken, ^{ 437 | lsl_semaphore = dispatch_semaphore_create(0); 438 | }); 439 | } 440 | 441 | #pragma mark - Public 442 | + (instancetype)monitor { 443 | return [LSLAppFluencyMonitor new]; 444 | } 445 | 446 | - (void)startMonitoring { 447 | if (lsl_is_monitoring) { return; } 448 | lsl_is_monitoring = YES; 449 | __lsl_monitor_init(); 450 | dispatch_async(__lsl_fluecy_monitor_queue(), ^{ 451 | while (lsl_is_monitoring) { 452 | __block BOOL timeOut = YES; 453 | dispatch_async(dispatch_get_main_queue(), ^{ 454 | timeOut = NO; 455 | dispatch_semaphore_signal(lsl_semaphore); 456 | }); 457 | [NSThread sleepForTimeInterval: lsl_time_out_interval]; 458 | if (timeOut) { 459 | [LSLBacktraceLogger lsl_logMain]; // 打印主线程调用栈 460 | // [LSLBacktraceLogger lsl_logCurrent]; // 打印当前线程的调用栈 461 | // [LSLBacktraceLogger lsl_logAllThread]; // 打印所有线程的调用栈 462 | } 463 | dispatch_wait(lsl_semaphore, DISPATCH_TIME_FOREVER); 464 | } 465 | }); 466 | } 467 | 468 | - (void)stopMonitoring { 469 | if (!lsl_is_monitoring) { return; } 470 | lsl_is_monitoring = NO; 471 | } 472 | 473 | @end 474 | ~~~ 475 | 476 | 其中`LSLBacktraceLogger `是获取堆栈信息的类,详情见代码[Github](https://github.com/SilongLi/AppPerformance)。 477 | 478 | demo卡顿堆栈打印日志如下: 479 | 480 | ~~~Swift 481 | 2018-08-16 12:36:33.910491+0800 AppPerformance[4802:171145] Backtrace of Thread 771: 482 | ====================================================================================== 483 | libsystem_kernel.dylib 0x10d089bce __semwait_signal + 10 484 | libsystem_c.dylib 0x10ce55d10 usleep + 53 485 | AppPerformance 0x108b8b478 $S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtF + 1144 486 | AppPerformance 0x108b8b60b $S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtFTo + 155 487 | UIKitCore 0x1135b104f -[_UIFilteredDataSource tableView:cellForRowAtIndexPath:] + 95 488 | UIKitCore 0x1131ed34d -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 765 489 | UIKitCore 0x1131ed8da -[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 73 490 | UIKitCore 0x1131b4b1e -[UITableView _updateVisibleCellsNow:isRecursive:] + 2863 491 | UIKitCore 0x1131d57eb -[UITableView layoutSubviews] + 165 492 | UIKitCore 0x1133921ee -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1501 493 | QuartzCore 0x10ab72eb1 -[CALayer layoutSublayers] + 175 494 | QuartzCore 0x10ab77d8b _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 395 495 | QuartzCore 0x10aaf3b45 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 349 496 | QuartzCore 0x10ab285b0 _ZN2CA11Transaction6commitEv + 576 497 | QuartzCore 0x10ab29374 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 76 498 | CoreFoundation 0x109dc3757 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23 499 | CoreFoundation 0x109dbdbde __CFRunLoopDoObservers + 430 500 | CoreFoundation 0x109dbe271 __CFRunLoopRun + 1537 501 | CoreFoundation 0x109dbd931 CFRunLoopRunSpecific + 625 502 | GraphicsServices 0x10f5981b5 GSEventRunModal + 62 503 | UIKitCore 0x112c812ce UIApplicationMain + 140 504 | AppPerformance 0x108b8c1f0 main + 224 505 | libdyld.dylib 0x10cd4dc9d start + 1 506 | 507 | ====================================================================================== 508 | ~~~ 509 | 510 | #### 方案二是结合`CADisplayLink `的方式实现 511 | 在检测FPS值的时候,我们就详细介绍了`CADisplayLink `的使用方式,在这里也可以通过FPS值是否连续低于某个值开进行监控。 512 | 513 | ## PS 514 | 515 | 更多App性能监控的内容,可查阅作者[博客](https://www.jianshu.com/u/b534ce5f8fae): 516 | 517 | [iOS开发--APP性能检测方案汇总(一)](https://www.jianshu.com/p/95df83780c8f) 518 | 519 | 520 | 521 | 522 | --------------------------------------------------------------------------------