├── Imgs ├── class-diagram.jpg ├── cover.jpeg ├── runloop.png ├── weak_init.png └── weak_store.png ├── README.md ├── demo ├── KVO │ ├── KVO.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ ├── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcuserdata │ │ │ │ └── yxyt.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcuserdata │ │ │ └── yxyt.xcuserdatad │ │ │ ├── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── KVO │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── NSObject+SKVO.h │ │ ├── NSObject+SKVO.m │ │ ├── SceneDelegate.h │ │ ├── SceneDelegate.m │ │ ├── Student.h │ │ ├── Student.m │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ └── main.m │ ├── KVOTests │ │ ├── Info.plist │ │ └── KVOTests.m │ └── KVOUITests │ │ ├── Info.plist │ │ └── KVOUITests.m └── Notification │ ├── Notification.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── yxyt.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── yxyt.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── Notification │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── NotificationCenter.h │ ├── NotificationCenter.m │ ├── SceneDelegate.h │ ├── SceneDelegate.m │ ├── ViewController.h │ ├── ViewController.m │ └── main.m │ ├── NotificationTests │ ├── Info.plist │ └── NotificationTests.m │ └── NotificationUITests │ ├── Info.plist │ └── NotificationUITests.m └── iOS面试题难点集锦 ├── iOS面试题难点集锦(一).md ├── iOS面试题难点集锦(三).md └── iOS面试题难点集锦(二).md /Imgs/class-diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiuFuBo/iOSInterviewQuestions/265cb18cc24950446dc9d7b4157ed4fcdb5ba0f8/Imgs/class-diagram.jpg -------------------------------------------------------------------------------- /Imgs/cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiuFuBo/iOSInterviewQuestions/265cb18cc24950446dc9d7b4157ed4fcdb5ba0f8/Imgs/cover.jpeg -------------------------------------------------------------------------------- /Imgs/runloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiuFuBo/iOSInterviewQuestions/265cb18cc24950446dc9d7b4157ed4fcdb5ba0f8/Imgs/runloop.png -------------------------------------------------------------------------------- /Imgs/weak_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiuFuBo/iOSInterviewQuestions/265cb18cc24950446dc9d7b4157ed4fcdb5ba0f8/Imgs/weak_init.png -------------------------------------------------------------------------------- /Imgs/weak_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiuFuBo/iOSInterviewQuestions/265cb18cc24950446dc9d7b4157ed4fcdb5ba0f8/Imgs/weak_store.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOSInterviewQuestions 2 |
3 | 4 |
5 | 6 | ## iOS面试题难点集锦(附答案) 7 | 8 | 第一篇:[《iOS面试题难点集锦一》 --参考答案](https://github.com/LiuFuBo/iOSInterviewQuestions/blob/master/iOS面试题难点集锦/iOS面试题难点集锦(一).md)
9 | 第二篇:[《iOS面试题难点集锦二》 --参考答案](https://github.com/LiuFuBo/iOSInterviewQuestions/blob/master/iOS面试题难点集锦/iOS面试题难点集锦(二).md) 10 | 第三篇:[《iOS面试题难点集锦三》 --参考答案](https://github.com/LiuFuBo/iOSInterviewQuestions/blob/master/iOS面试题难点集锦/iOS面试题难点集锦(三).md) 11 | 12 | 面试题的来源主要是自日常工作的总结,以及阅读runtime源码并参考一些知名技术大佬的博客以后的记录,文章中每一道题我都有给出答案,有些地方有疑惑的我也有标记出来,如果你对于我的文章中某些地方有更好的解答或者疑问都欢迎你联系我,大家可以互相探讨,再加以修正。 -------------------------------------------------------------------------------- /demo/KVO/KVO.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 13796407255BE5C90036D75F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13796406255BE5C90036D75F /* AppDelegate.m */; }; 11 | 1379640A255BE5C90036D75F /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13796409255BE5C90036D75F /* SceneDelegate.m */; }; 12 | 1379640D255BE5C90036D75F /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1379640C255BE5C90036D75F /* ViewController.m */; }; 13 | 13796410255BE5C90036D75F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1379640E255BE5C90036D75F /* Main.storyboard */; }; 14 | 13796412255BE5D80036D75F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13796411255BE5D80036D75F /* Assets.xcassets */; }; 15 | 13796415255BE5D80036D75F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 13796413255BE5D80036D75F /* LaunchScreen.storyboard */; }; 16 | 13796418255BE5D80036D75F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13796417255BE5D80036D75F /* main.m */; }; 17 | 13796422255BE5D80036D75F /* KVOTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13796421255BE5D80036D75F /* KVOTests.m */; }; 18 | 1379642D255BE5D80036D75F /* KVOUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1379642C255BE5D80036D75F /* KVOUITests.m */; }; 19 | 1379643C255BE6110036D75F /* NSObject+SKVO.m in Sources */ = {isa = PBXBuildFile; fileRef = 1379643B255BE6110036D75F /* NSObject+SKVO.m */; }; 20 | 13E7CC83255D0A4E00B5BDE6 /* Student.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E7CC82255D0A4E00B5BDE6 /* Student.m */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 1379641E255BE5D80036D75F /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 137963FA255BE5C90036D75F /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 13796401255BE5C90036D75F; 29 | remoteInfo = KVO; 30 | }; 31 | 13796429255BE5D80036D75F /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 137963FA255BE5C90036D75F /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = 13796401255BE5C90036D75F; 36 | remoteInfo = KVO; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 13796402255BE5C90036D75F /* KVO.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KVO.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 13796405255BE5C90036D75F /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 43 | 13796406255BE5C90036D75F /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 44 | 13796408255BE5C90036D75F /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 45 | 13796409255BE5C90036D75F /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 46 | 1379640B255BE5C90036D75F /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 47 | 1379640C255BE5C90036D75F /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 48 | 1379640F255BE5C90036D75F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 49 | 13796411255BE5D80036D75F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50 | 13796414255BE5D80036D75F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 51 | 13796416255BE5D80036D75F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52 | 13796417255BE5D80036D75F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 53 | 1379641D255BE5D80036D75F /* KVOTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KVOTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 13796421255BE5D80036D75F /* KVOTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KVOTests.m; sourceTree = ""; }; 55 | 13796423255BE5D80036D75F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 13796428255BE5D80036D75F /* KVOUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KVOUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 1379642C255BE5D80036D75F /* KVOUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KVOUITests.m; sourceTree = ""; }; 58 | 1379642E255BE5D80036D75F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | 1379643A255BE6110036D75F /* NSObject+SKVO.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+SKVO.h"; sourceTree = ""; }; 60 | 1379643B255BE6110036D75F /* NSObject+SKVO.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+SKVO.m"; sourceTree = ""; }; 61 | 13E7CC81255D0A4E00B5BDE6 /* Student.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Student.h; sourceTree = ""; }; 62 | 13E7CC82255D0A4E00B5BDE6 /* Student.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Student.m; sourceTree = ""; }; 63 | /* End PBXFileReference section */ 64 | 65 | /* Begin PBXFrameworksBuildPhase section */ 66 | 137963FF255BE5C90036D75F /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | 1379641A255BE5D80036D75F /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | 13796425255BE5D80036D75F /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | /* End PBXFrameworksBuildPhase section */ 88 | 89 | /* Begin PBXGroup section */ 90 | 137963F9255BE5C90036D75F = { 91 | isa = PBXGroup; 92 | children = ( 93 | 13796404255BE5C90036D75F /* KVO */, 94 | 13796420255BE5D80036D75F /* KVOTests */, 95 | 1379642B255BE5D80036D75F /* KVOUITests */, 96 | 13796403255BE5C90036D75F /* Products */, 97 | ); 98 | sourceTree = ""; 99 | }; 100 | 13796403255BE5C90036D75F /* Products */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 13796402255BE5C90036D75F /* KVO.app */, 104 | 1379641D255BE5D80036D75F /* KVOTests.xctest */, 105 | 13796428255BE5D80036D75F /* KVOUITests.xctest */, 106 | ); 107 | name = Products; 108 | sourceTree = ""; 109 | }; 110 | 13796404255BE5C90036D75F /* KVO */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 13796405255BE5C90036D75F /* AppDelegate.h */, 114 | 13796406255BE5C90036D75F /* AppDelegate.m */, 115 | 13796408255BE5C90036D75F /* SceneDelegate.h */, 116 | 13796409255BE5C90036D75F /* SceneDelegate.m */, 117 | 1379640B255BE5C90036D75F /* ViewController.h */, 118 | 1379640C255BE5C90036D75F /* ViewController.m */, 119 | 13E7CC81255D0A4E00B5BDE6 /* Student.h */, 120 | 13E7CC82255D0A4E00B5BDE6 /* Student.m */, 121 | 1379643A255BE6110036D75F /* NSObject+SKVO.h */, 122 | 1379643B255BE6110036D75F /* NSObject+SKVO.m */, 123 | 1379640E255BE5C90036D75F /* Main.storyboard */, 124 | 13796411255BE5D80036D75F /* Assets.xcassets */, 125 | 13796413255BE5D80036D75F /* LaunchScreen.storyboard */, 126 | 13796416255BE5D80036D75F /* Info.plist */, 127 | 13796417255BE5D80036D75F /* main.m */, 128 | ); 129 | path = KVO; 130 | sourceTree = ""; 131 | }; 132 | 13796420255BE5D80036D75F /* KVOTests */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 13796421255BE5D80036D75F /* KVOTests.m */, 136 | 13796423255BE5D80036D75F /* Info.plist */, 137 | ); 138 | path = KVOTests; 139 | sourceTree = ""; 140 | }; 141 | 1379642B255BE5D80036D75F /* KVOUITests */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 1379642C255BE5D80036D75F /* KVOUITests.m */, 145 | 1379642E255BE5D80036D75F /* Info.plist */, 146 | ); 147 | path = KVOUITests; 148 | sourceTree = ""; 149 | }; 150 | /* End PBXGroup section */ 151 | 152 | /* Begin PBXNativeTarget section */ 153 | 13796401255BE5C90036D75F /* KVO */ = { 154 | isa = PBXNativeTarget; 155 | buildConfigurationList = 13796431255BE5D80036D75F /* Build configuration list for PBXNativeTarget "KVO" */; 156 | buildPhases = ( 157 | 137963FE255BE5C90036D75F /* Sources */, 158 | 137963FF255BE5C90036D75F /* Frameworks */, 159 | 13796400255BE5C90036D75F /* Resources */, 160 | ); 161 | buildRules = ( 162 | ); 163 | dependencies = ( 164 | ); 165 | name = KVO; 166 | productName = KVO; 167 | productReference = 13796402255BE5C90036D75F /* KVO.app */; 168 | productType = "com.apple.product-type.application"; 169 | }; 170 | 1379641C255BE5D80036D75F /* KVOTests */ = { 171 | isa = PBXNativeTarget; 172 | buildConfigurationList = 13796434255BE5D80036D75F /* Build configuration list for PBXNativeTarget "KVOTests" */; 173 | buildPhases = ( 174 | 13796419255BE5D80036D75F /* Sources */, 175 | 1379641A255BE5D80036D75F /* Frameworks */, 176 | 1379641B255BE5D80036D75F /* Resources */, 177 | ); 178 | buildRules = ( 179 | ); 180 | dependencies = ( 181 | 1379641F255BE5D80036D75F /* PBXTargetDependency */, 182 | ); 183 | name = KVOTests; 184 | productName = KVOTests; 185 | productReference = 1379641D255BE5D80036D75F /* KVOTests.xctest */; 186 | productType = "com.apple.product-type.bundle.unit-test"; 187 | }; 188 | 13796427255BE5D80036D75F /* KVOUITests */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 13796437255BE5D80036D75F /* Build configuration list for PBXNativeTarget "KVOUITests" */; 191 | buildPhases = ( 192 | 13796424255BE5D80036D75F /* Sources */, 193 | 13796425255BE5D80036D75F /* Frameworks */, 194 | 13796426255BE5D80036D75F /* Resources */, 195 | ); 196 | buildRules = ( 197 | ); 198 | dependencies = ( 199 | 1379642A255BE5D80036D75F /* PBXTargetDependency */, 200 | ); 201 | name = KVOUITests; 202 | productName = KVOUITests; 203 | productReference = 13796428255BE5D80036D75F /* KVOUITests.xctest */; 204 | productType = "com.apple.product-type.bundle.ui-testing"; 205 | }; 206 | /* End PBXNativeTarget section */ 207 | 208 | /* Begin PBXProject section */ 209 | 137963FA255BE5C90036D75F /* Project object */ = { 210 | isa = PBXProject; 211 | attributes = { 212 | LastUpgradeCheck = 1160; 213 | ORGANIZATIONNAME = watermark; 214 | TargetAttributes = { 215 | 13796401255BE5C90036D75F = { 216 | CreatedOnToolsVersion = 11.6; 217 | }; 218 | 1379641C255BE5D80036D75F = { 219 | CreatedOnToolsVersion = 11.6; 220 | TestTargetID = 13796401255BE5C90036D75F; 221 | }; 222 | 13796427255BE5D80036D75F = { 223 | CreatedOnToolsVersion = 11.6; 224 | TestTargetID = 13796401255BE5C90036D75F; 225 | }; 226 | }; 227 | }; 228 | buildConfigurationList = 137963FD255BE5C90036D75F /* Build configuration list for PBXProject "KVO" */; 229 | compatibilityVersion = "Xcode 9.3"; 230 | developmentRegion = en; 231 | hasScannedForEncodings = 0; 232 | knownRegions = ( 233 | en, 234 | Base, 235 | ); 236 | mainGroup = 137963F9255BE5C90036D75F; 237 | productRefGroup = 13796403255BE5C90036D75F /* Products */; 238 | projectDirPath = ""; 239 | projectRoot = ""; 240 | targets = ( 241 | 13796401255BE5C90036D75F /* KVO */, 242 | 1379641C255BE5D80036D75F /* KVOTests */, 243 | 13796427255BE5D80036D75F /* KVOUITests */, 244 | ); 245 | }; 246 | /* End PBXProject section */ 247 | 248 | /* Begin PBXResourcesBuildPhase section */ 249 | 13796400255BE5C90036D75F /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | 13796415255BE5D80036D75F /* LaunchScreen.storyboard in Resources */, 254 | 13796412255BE5D80036D75F /* Assets.xcassets in Resources */, 255 | 13796410255BE5C90036D75F /* Main.storyboard in Resources */, 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | 1379641B255BE5D80036D75F /* Resources */ = { 260 | isa = PBXResourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | 13796426255BE5D80036D75F /* Resources */ = { 267 | isa = PBXResourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | /* End PBXResourcesBuildPhase section */ 274 | 275 | /* Begin PBXSourcesBuildPhase section */ 276 | 137963FE255BE5C90036D75F /* Sources */ = { 277 | isa = PBXSourcesBuildPhase; 278 | buildActionMask = 2147483647; 279 | files = ( 280 | 1379640D255BE5C90036D75F /* ViewController.m in Sources */, 281 | 13796407255BE5C90036D75F /* AppDelegate.m in Sources */, 282 | 13796418255BE5D80036D75F /* main.m in Sources */, 283 | 1379643C255BE6110036D75F /* NSObject+SKVO.m in Sources */, 284 | 1379640A255BE5C90036D75F /* SceneDelegate.m in Sources */, 285 | 13E7CC83255D0A4E00B5BDE6 /* Student.m in Sources */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | 13796419255BE5D80036D75F /* Sources */ = { 290 | isa = PBXSourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | 13796422255BE5D80036D75F /* KVOTests.m in Sources */, 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | }; 297 | 13796424255BE5D80036D75F /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 1379642D255BE5D80036D75F /* KVOUITests.m in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | /* End PBXSourcesBuildPhase section */ 306 | 307 | /* Begin PBXTargetDependency section */ 308 | 1379641F255BE5D80036D75F /* PBXTargetDependency */ = { 309 | isa = PBXTargetDependency; 310 | target = 13796401255BE5C90036D75F /* KVO */; 311 | targetProxy = 1379641E255BE5D80036D75F /* PBXContainerItemProxy */; 312 | }; 313 | 1379642A255BE5D80036D75F /* PBXTargetDependency */ = { 314 | isa = PBXTargetDependency; 315 | target = 13796401255BE5C90036D75F /* KVO */; 316 | targetProxy = 13796429255BE5D80036D75F /* PBXContainerItemProxy */; 317 | }; 318 | /* End PBXTargetDependency section */ 319 | 320 | /* Begin PBXVariantGroup section */ 321 | 1379640E255BE5C90036D75F /* Main.storyboard */ = { 322 | isa = PBXVariantGroup; 323 | children = ( 324 | 1379640F255BE5C90036D75F /* Base */, 325 | ); 326 | name = Main.storyboard; 327 | sourceTree = ""; 328 | }; 329 | 13796413255BE5D80036D75F /* LaunchScreen.storyboard */ = { 330 | isa = PBXVariantGroup; 331 | children = ( 332 | 13796414255BE5D80036D75F /* Base */, 333 | ); 334 | name = LaunchScreen.storyboard; 335 | sourceTree = ""; 336 | }; 337 | /* End PBXVariantGroup section */ 338 | 339 | /* Begin XCBuildConfiguration section */ 340 | 1379642F255BE5D80036D75F /* Debug */ = { 341 | isa = XCBuildConfiguration; 342 | buildSettings = { 343 | ALWAYS_SEARCH_USER_PATHS = NO; 344 | CLANG_ANALYZER_NONNULL = YES; 345 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 346 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 347 | CLANG_CXX_LIBRARY = "libc++"; 348 | CLANG_ENABLE_MODULES = YES; 349 | CLANG_ENABLE_OBJC_ARC = YES; 350 | CLANG_ENABLE_OBJC_WEAK = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 358 | CLANG_WARN_EMPTY_BODY = YES; 359 | CLANG_WARN_ENUM_CONVERSION = YES; 360 | CLANG_WARN_INFINITE_RECURSION = YES; 361 | CLANG_WARN_INT_CONVERSION = YES; 362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 367 | CLANG_WARN_STRICT_PROTOTYPES = YES; 368 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 369 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | COPY_PHASE_STRIP = NO; 373 | DEBUG_INFORMATION_FORMAT = dwarf; 374 | ENABLE_STRICT_OBJC_MSGSEND = YES; 375 | ENABLE_TESTABILITY = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu11; 377 | GCC_DYNAMIC_NO_PIC = NO; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_OPTIMIZATION_LEVEL = 0; 380 | GCC_PREPROCESSOR_DEFINITIONS = ( 381 | "DEBUG=1", 382 | "$(inherited)", 383 | ); 384 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 385 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 386 | GCC_WARN_UNDECLARED_SELECTOR = YES; 387 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 388 | GCC_WARN_UNUSED_FUNCTION = YES; 389 | GCC_WARN_UNUSED_VARIABLE = YES; 390 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 391 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 392 | MTL_FAST_MATH = YES; 393 | ONLY_ACTIVE_ARCH = YES; 394 | SDKROOT = iphoneos; 395 | }; 396 | name = Debug; 397 | }; 398 | 13796430255BE5D80036D75F /* Release */ = { 399 | isa = XCBuildConfiguration; 400 | buildSettings = { 401 | ALWAYS_SEARCH_USER_PATHS = NO; 402 | CLANG_ANALYZER_NONNULL = YES; 403 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 404 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 405 | CLANG_CXX_LIBRARY = "libc++"; 406 | CLANG_ENABLE_MODULES = YES; 407 | CLANG_ENABLE_OBJC_ARC = YES; 408 | CLANG_ENABLE_OBJC_WEAK = YES; 409 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 410 | CLANG_WARN_BOOL_CONVERSION = YES; 411 | CLANG_WARN_COMMA = YES; 412 | CLANG_WARN_CONSTANT_CONVERSION = YES; 413 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 414 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 415 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 416 | CLANG_WARN_EMPTY_BODY = YES; 417 | CLANG_WARN_ENUM_CONVERSION = YES; 418 | CLANG_WARN_INFINITE_RECURSION = YES; 419 | CLANG_WARN_INT_CONVERSION = YES; 420 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 421 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 422 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 423 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 424 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 425 | CLANG_WARN_STRICT_PROTOTYPES = YES; 426 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 427 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 428 | CLANG_WARN_UNREACHABLE_CODE = YES; 429 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 430 | COPY_PHASE_STRIP = NO; 431 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 432 | ENABLE_NS_ASSERTIONS = NO; 433 | ENABLE_STRICT_OBJC_MSGSEND = YES; 434 | GCC_C_LANGUAGE_STANDARD = gnu11; 435 | GCC_NO_COMMON_BLOCKS = YES; 436 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNDECLARED_SELECTOR = YES; 439 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 440 | GCC_WARN_UNUSED_FUNCTION = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 443 | MTL_ENABLE_DEBUG_INFO = NO; 444 | MTL_FAST_MATH = YES; 445 | SDKROOT = iphoneos; 446 | VALIDATE_PRODUCT = YES; 447 | }; 448 | name = Release; 449 | }; 450 | 13796432255BE5D80036D75F /* Debug */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 454 | CODE_SIGN_STYLE = Automatic; 455 | ENABLE_STRICT_OBJC_MSGSEND = NO; 456 | INFOPLIST_FILE = KVO/Info.plist; 457 | LD_RUNPATH_SEARCH_PATHS = ( 458 | "$(inherited)", 459 | "@executable_path/Frameworks", 460 | ); 461 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.KVO; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | TARGETED_DEVICE_FAMILY = "1,2"; 464 | }; 465 | name = Debug; 466 | }; 467 | 13796433255BE5D80036D75F /* Release */ = { 468 | isa = XCBuildConfiguration; 469 | buildSettings = { 470 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 471 | CODE_SIGN_STYLE = Automatic; 472 | ENABLE_STRICT_OBJC_MSGSEND = NO; 473 | INFOPLIST_FILE = KVO/Info.plist; 474 | LD_RUNPATH_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "@executable_path/Frameworks", 477 | ); 478 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.KVO; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | TARGETED_DEVICE_FAMILY = "1,2"; 481 | }; 482 | name = Release; 483 | }; 484 | 13796435255BE5D80036D75F /* Debug */ = { 485 | isa = XCBuildConfiguration; 486 | buildSettings = { 487 | BUNDLE_LOADER = "$(TEST_HOST)"; 488 | CODE_SIGN_STYLE = Automatic; 489 | INFOPLIST_FILE = KVOTests/Info.plist; 490 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 491 | LD_RUNPATH_SEARCH_PATHS = ( 492 | "$(inherited)", 493 | "@executable_path/Frameworks", 494 | "@loader_path/Frameworks", 495 | ); 496 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.KVOTests; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | TARGETED_DEVICE_FAMILY = "1,2"; 499 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KVO.app/KVO"; 500 | }; 501 | name = Debug; 502 | }; 503 | 13796436255BE5D80036D75F /* Release */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | BUNDLE_LOADER = "$(TEST_HOST)"; 507 | CODE_SIGN_STYLE = Automatic; 508 | INFOPLIST_FILE = KVOTests/Info.plist; 509 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 510 | LD_RUNPATH_SEARCH_PATHS = ( 511 | "$(inherited)", 512 | "@executable_path/Frameworks", 513 | "@loader_path/Frameworks", 514 | ); 515 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.KVOTests; 516 | PRODUCT_NAME = "$(TARGET_NAME)"; 517 | TARGETED_DEVICE_FAMILY = "1,2"; 518 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KVO.app/KVO"; 519 | }; 520 | name = Release; 521 | }; 522 | 13796438255BE5D80036D75F /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | CODE_SIGN_STYLE = Automatic; 526 | INFOPLIST_FILE = KVOUITests/Info.plist; 527 | LD_RUNPATH_SEARCH_PATHS = ( 528 | "$(inherited)", 529 | "@executable_path/Frameworks", 530 | "@loader_path/Frameworks", 531 | ); 532 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.KVOUITests; 533 | PRODUCT_NAME = "$(TARGET_NAME)"; 534 | TARGETED_DEVICE_FAMILY = "1,2"; 535 | TEST_TARGET_NAME = KVO; 536 | }; 537 | name = Debug; 538 | }; 539 | 13796439255BE5D80036D75F /* Release */ = { 540 | isa = XCBuildConfiguration; 541 | buildSettings = { 542 | CODE_SIGN_STYLE = Automatic; 543 | INFOPLIST_FILE = KVOUITests/Info.plist; 544 | LD_RUNPATH_SEARCH_PATHS = ( 545 | "$(inherited)", 546 | "@executable_path/Frameworks", 547 | "@loader_path/Frameworks", 548 | ); 549 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.KVOUITests; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | TARGETED_DEVICE_FAMILY = "1,2"; 552 | TEST_TARGET_NAME = KVO; 553 | }; 554 | name = Release; 555 | }; 556 | /* End XCBuildConfiguration section */ 557 | 558 | /* Begin XCConfigurationList section */ 559 | 137963FD255BE5C90036D75F /* Build configuration list for PBXProject "KVO" */ = { 560 | isa = XCConfigurationList; 561 | buildConfigurations = ( 562 | 1379642F255BE5D80036D75F /* Debug */, 563 | 13796430255BE5D80036D75F /* Release */, 564 | ); 565 | defaultConfigurationIsVisible = 0; 566 | defaultConfigurationName = Release; 567 | }; 568 | 13796431255BE5D80036D75F /* Build configuration list for PBXNativeTarget "KVO" */ = { 569 | isa = XCConfigurationList; 570 | buildConfigurations = ( 571 | 13796432255BE5D80036D75F /* Debug */, 572 | 13796433255BE5D80036D75F /* Release */, 573 | ); 574 | defaultConfigurationIsVisible = 0; 575 | defaultConfigurationName = Release; 576 | }; 577 | 13796434255BE5D80036D75F /* Build configuration list for PBXNativeTarget "KVOTests" */ = { 578 | isa = XCConfigurationList; 579 | buildConfigurations = ( 580 | 13796435255BE5D80036D75F /* Debug */, 581 | 13796436255BE5D80036D75F /* Release */, 582 | ); 583 | defaultConfigurationIsVisible = 0; 584 | defaultConfigurationName = Release; 585 | }; 586 | 13796437255BE5D80036D75F /* Build configuration list for PBXNativeTarget "KVOUITests" */ = { 587 | isa = XCConfigurationList; 588 | buildConfigurations = ( 589 | 13796438255BE5D80036D75F /* Debug */, 590 | 13796439255BE5D80036D75F /* Release */, 591 | ); 592 | defaultConfigurationIsVisible = 0; 593 | defaultConfigurationName = Release; 594 | }; 595 | /* End XCConfigurationList section */ 596 | }; 597 | rootObject = 137963FA255BE5C90036D75F /* Project object */; 598 | } 599 | -------------------------------------------------------------------------------- /demo/KVO/KVO.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/KVO/KVO.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/KVO/KVO.xcodeproj/project.xcworkspace/xcuserdata/yxyt.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiuFuBo/iOSInterviewQuestions/265cb18cc24950446dc9d7b4157ed4fcdb5ba0f8/demo/KVO/KVO.xcodeproj/project.xcworkspace/xcuserdata/yxyt.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /demo/KVO/KVO.xcodeproj/xcuserdata/yxyt.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /demo/KVO/KVO.xcodeproj/xcuserdata/yxyt.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | KVO.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/KVO/KVO/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /demo/KVO/KVO/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | #pragma mark - UISceneSession lifecycle 25 | 26 | 27 | - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { 28 | // Called when a new scene session is being created. 29 | // Use this method to select a configuration to create the new scene with. 30 | return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; 31 | } 32 | 33 | 34 | - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { 35 | // Called when the user discards a scene session. 36 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 37 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 38 | } 39 | 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /demo/KVO/KVO/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /demo/KVO/KVO/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/KVO/KVO/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 | -------------------------------------------------------------------------------- /demo/KVO/KVO/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 | -------------------------------------------------------------------------------- /demo/KVO/KVO/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /demo/KVO/KVO/NSObject+SKVO.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+SKVO.h 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef void(^SKVOBlock)(NSDictionary *change); 12 | 13 | @interface NSObject (SKVO) 14 | 15 | - (void)s_addObserver:(NSObject *)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(SKVOBlock)block; 16 | 17 | @end 18 | 19 | 20 | -------------------------------------------------------------------------------- /demo/KVO/KVO/NSObject+SKVO.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+SKVO.m 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "NSObject+SKVO.h" 10 | #import 11 | 12 | static const char *SKVO_observer_key = "SKVO_observer_key"; 13 | static const char *SKVO_setter_key = "SKVO_setter_key"; 14 | static const char *SKVO_getter_key = "SKVO_getter_key"; 15 | static const char *SKVO_block_key = "SKVO_block_key"; 16 | 17 | @implementation NSObject (SKVO) 18 | 19 | - (void)s_addObserver:(NSObject *)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(SKVOBlock)block { 20 | 21 | //创建、注册子类 22 | NSString *oldClassName = NSStringFromClass([self class]); 23 | NSString *newClassName = [NSString stringWithFormat:@"SKVONotifying_%@",oldClassName]; 24 | 25 | Class clazz = objc_getClass(newClassName.UTF8String); 26 | if (!clazz) { 27 | clazz = objc_allocateClassPair([self class], newClassName.UTF8String, 0); 28 | objc_registerClassPair(clazz); 29 | } 30 | 31 | //set方法名 32 | if (keyPath.length <= 0)return; 33 | NSString *fChar = [[keyPath substringToIndex:1] uppercaseString];//第一个char 34 | NSString *rChar = [keyPath substringFromIndex:1];//第二到最后char 35 | NSString *setterChar = [NSString stringWithFormat:@"set%@%@:",fChar,rChar]; 36 | SEL setSEL = NSSelectorFromString(setterChar); 37 | 38 | //添加set方法 39 | Method getMethod = class_getInstanceMethod([self class], setSEL); 40 | const char *types = method_getTypeEncoding(getMethod); 41 | class_addMethod(clazz, setSEL, (IMP)setterMethod, types); 42 | 43 | 44 | //改变isa指针,指向新建的子类 45 | object_setClass(self, clazz); 46 | 47 | //保存set方法名 48 | objc_setAssociatedObject(self, SKVO_setter_key, setterChar, OBJC_ASSOCIATION_COPY_NONATOMIC); 49 | 50 | //保存getter方法名 51 | objc_setAssociatedObject(self, SKVO_getter_key, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC); 52 | 53 | //保存observer 54 | objc_setAssociatedObject(self, SKVO_observer_key, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 55 | 56 | //保存block 57 | objc_setAssociatedObject(self, SKVO_block_key, block, OBJC_ASSOCIATION_COPY); 58 | } 59 | 60 | void setterMethod(id self, SEL _cmd, id newValue){ 61 | 62 | //获取set、get犯法名 63 | NSString *setterChar = objc_getAssociatedObject(self, SKVO_setter_key); 64 | NSString *getterChar = objc_getAssociatedObject(self, SKVO_getter_key); 65 | 66 | //保存子类类型 67 | Class clazz = [self class]; 68 | 69 | //isa 指向原类 70 | object_setClass(self, class_getSuperclass(clazz)); 71 | 72 | //调用原类get方法,获取oldValue 73 | id oldValue = objc_msgSend(self, NSSelectorFromString(getterChar)); 74 | 75 | //调用原类set方法 76 | objc_msgSend(self, NSSelectorFromString(setterChar),newValue); 77 | 78 | NSMutableDictionary *change = [[NSMutableDictionary alloc]init]; 79 | if (newValue) { 80 | change[NSKeyValueChangeNewKey] = newValue; 81 | } 82 | if (oldValue) { 83 | change[NSKeyValueChangeOldKey] = oldValue; 84 | } 85 | 86 | //原类响应消息更新方法 87 | NSObject *observer = objc_getAssociatedObject(self, SKVO_observer_key); 88 | 89 | objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),getterChar,self,change,nil); 90 | 91 | SKVOBlock block = objc_getAssociatedObject(self, SKVO_block_key); 92 | if (block) { 93 | block(change); 94 | } 95 | 96 | //isa改回子类类型 97 | object_setClass(self, clazz); 98 | } 99 | 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /demo/KVO/KVO/SceneDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.h 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SceneDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow * window; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /demo/KVO/KVO/SceneDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.m 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "SceneDelegate.h" 10 | 11 | @interface SceneDelegate () 12 | 13 | @end 14 | 15 | @implementation SceneDelegate 16 | 17 | 18 | - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { 19 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 20 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 21 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 22 | } 23 | 24 | 25 | - (void)sceneDidDisconnect:(UIScene *)scene { 26 | // Called as the scene is being released by the system. 27 | // This occurs shortly after the scene enters the background, or when its session is discarded. 28 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 29 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 30 | } 31 | 32 | 33 | - (void)sceneDidBecomeActive:(UIScene *)scene { 34 | // Called when the scene has moved from an inactive state to an active state. 35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 36 | } 37 | 38 | 39 | - (void)sceneWillResignActive:(UIScene *)scene { 40 | // Called when the scene will move from an active state to an inactive state. 41 | // This may occur due to temporary interruptions (ex. an incoming phone call). 42 | } 43 | 44 | 45 | - (void)sceneWillEnterForeground:(UIScene *)scene { 46 | // Called as the scene transitions from the background to the foreground. 47 | // Use this method to undo the changes made on entering the background. 48 | } 49 | 50 | 51 | - (void)sceneDidEnterBackground:(UIScene *)scene { 52 | // Called as the scene transitions from the foreground to the background. 53 | // Use this method to save data, release shared resources, and store enough scene-specific state information 54 | // to restore the scene back to its current state. 55 | } 56 | 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /demo/KVO/KVO/Student.h: -------------------------------------------------------------------------------- 1 | // 2 | // Student.h 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/12. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | @interface Student : NSObject 13 | 14 | @property (nonatomic, copy) NSNumber *score; 15 | 16 | @end 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/KVO/KVO/Student.m: -------------------------------------------------------------------------------- 1 | // 2 | // Student.m 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/12. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "Student.h" 10 | 11 | @implementation Student 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /demo/KVO/KVO/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /demo/KVO/KVO/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "NSObject+SKVO.h" 11 | #import "Student.h" 12 | 13 | @interface ViewController () 14 | @property (nonatomic, strong) Student *s1; 15 | 16 | @end 17 | 18 | @implementation ViewController 19 | 20 | - (void)viewDidLoad { 21 | [super viewDidLoad]; 22 | _s1 = [[Student alloc]init]; 23 | [_s1 s_addObserver:self keyPath:@"score" options:NSKeyValueObservingOptionNew block:^(NSDictionary *change) { 24 | NSLog(@"KVO结果:%@",change); 25 | }]; 26 | } 27 | 28 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 29 | NSLog(@"keyPath=%@ old=%@ new=%@", keyPath, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]); 30 | } 31 | 32 | 33 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 34 | static NSInteger num = 0; 35 | num++; 36 | _s1.score = @(num); 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /demo/KVO/KVO/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // KVO 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | NSString * appDelegateClassName; 14 | @autoreleasepool { 15 | // Setup code that might create autoreleased objects goes here. 16 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 17 | } 18 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 19 | } 20 | -------------------------------------------------------------------------------- /demo/KVO/KVOTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/KVO/KVOTests/KVOTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // KVOTests.m 3 | // KVOTests 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface KVOTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation KVOTests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | - (void)tearDown { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | - (void)testExample { 26 | // This is an example of a functional test case. 27 | // Use XCTAssert and related functions to verify your tests produce the correct results. 28 | } 29 | 30 | - (void)testPerformanceExample { 31 | // This is an example of a performance test case. 32 | [self measureBlock:^{ 33 | // Put the code you want to measure the time of here. 34 | }]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /demo/KVO/KVOUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/KVO/KVOUITests/KVOUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // KVOUITests.m 3 | // KVOUITests 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface KVOUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation KVOUITests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | 20 | // In UI tests it is usually best to stop immediately when a failure occurs. 21 | self.continueAfterFailure = NO; 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | - (void)tearDown { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | } 29 | 30 | - (void)testExample { 31 | // UI tests must launch the application that they test. 32 | XCUIApplication *app = [[XCUIApplication alloc] init]; 33 | [app launch]; 34 | 35 | // Use recording to get started writing UI tests. 36 | // Use XCTAssert and related functions to verify your tests produce the correct results. 37 | } 38 | 39 | - (void)testLaunchPerformance { 40 | if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { 41 | // This measures how long it takes to launch your application. 42 | [self measureWithMetrics:@[XCTOSSignpostMetric.applicationLaunchMetric] block:^{ 43 | [[[XCUIApplication alloc] init] launch]; 44 | }]; 45 | } 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /demo/Notification/Notification.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 137963C3255BC01F0036D75F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 137963C2255BC01F0036D75F /* AppDelegate.m */; }; 11 | 137963C6255BC01F0036D75F /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 137963C5255BC01F0036D75F /* SceneDelegate.m */; }; 12 | 137963C9255BC01F0036D75F /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 137963C8255BC01F0036D75F /* ViewController.m */; }; 13 | 137963CC255BC01F0036D75F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 137963CA255BC01F0036D75F /* Main.storyboard */; }; 14 | 137963CE255BC0310036D75F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 137963CD255BC0310036D75F /* Assets.xcassets */; }; 15 | 137963D1255BC0310036D75F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 137963CF255BC0310036D75F /* LaunchScreen.storyboard */; }; 16 | 137963D4255BC0310036D75F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 137963D3255BC0310036D75F /* main.m */; }; 17 | 137963DE255BC0310036D75F /* NotificationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 137963DD255BC0310036D75F /* NotificationTests.m */; }; 18 | 137963E9255BC0310036D75F /* NotificationUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 137963E8255BC0310036D75F /* NotificationUITests.m */; }; 19 | 137963F8255BC1D40036D75F /* NotificationCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 137963F7255BC1D40036D75F /* NotificationCenter.m */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 137963DA255BC0310036D75F /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 137963B6255BC01F0036D75F /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 137963BD255BC01F0036D75F; 28 | remoteInfo = Notification; 29 | }; 30 | 137963E5255BC0310036D75F /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 137963B6255BC01F0036D75F /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 137963BD255BC01F0036D75F; 35 | remoteInfo = Notification; 36 | }; 37 | /* End PBXContainerItemProxy section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 137963BE255BC01F0036D75F /* Notification.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Notification.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 137963C1255BC01F0036D75F /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 42 | 137963C2255BC01F0036D75F /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 43 | 137963C4255BC01F0036D75F /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 44 | 137963C5255BC01F0036D75F /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 45 | 137963C7255BC01F0036D75F /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 46 | 137963C8255BC01F0036D75F /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 47 | 137963CB255BC01F0036D75F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 48 | 137963CD255BC0310036D75F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | 137963D0255BC0310036D75F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 50 | 137963D2255BC0310036D75F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 137963D3255BC0310036D75F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 52 | 137963D9255BC0310036D75F /* NotificationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NotificationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 137963DD255BC0310036D75F /* NotificationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationTests.m; sourceTree = ""; }; 54 | 137963DF255BC0310036D75F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | 137963E4255BC0310036D75F /* NotificationUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NotificationUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 137963E8255BC0310036D75F /* NotificationUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationUITests.m; sourceTree = ""; }; 57 | 137963EA255BC0310036D75F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | 137963F6255BC1D40036D75F /* NotificationCenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationCenter.h; sourceTree = ""; }; 59 | 137963F7255BC1D40036D75F /* NotificationCenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationCenter.m; sourceTree = ""; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | 137963BB255BC01F0036D75F /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | 137963D6255BC0310036D75F /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | 137963E1255BC0310036D75F /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | 137963B5255BC01F0036D75F = { 88 | isa = PBXGroup; 89 | children = ( 90 | 137963C0255BC01F0036D75F /* Notification */, 91 | 137963DC255BC0310036D75F /* NotificationTests */, 92 | 137963E7255BC0310036D75F /* NotificationUITests */, 93 | 137963BF255BC01F0036D75F /* Products */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 137963BF255BC01F0036D75F /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 137963BE255BC01F0036D75F /* Notification.app */, 101 | 137963D9255BC0310036D75F /* NotificationTests.xctest */, 102 | 137963E4255BC0310036D75F /* NotificationUITests.xctest */, 103 | ); 104 | name = Products; 105 | sourceTree = ""; 106 | }; 107 | 137963C0255BC01F0036D75F /* Notification */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 137963C1255BC01F0036D75F /* AppDelegate.h */, 111 | 137963C2255BC01F0036D75F /* AppDelegate.m */, 112 | 137963C4255BC01F0036D75F /* SceneDelegate.h */, 113 | 137963C5255BC01F0036D75F /* SceneDelegate.m */, 114 | 137963C7255BC01F0036D75F /* ViewController.h */, 115 | 137963C8255BC01F0036D75F /* ViewController.m */, 116 | 137963F6255BC1D40036D75F /* NotificationCenter.h */, 117 | 137963F7255BC1D40036D75F /* NotificationCenter.m */, 118 | 137963CA255BC01F0036D75F /* Main.storyboard */, 119 | 137963CD255BC0310036D75F /* Assets.xcassets */, 120 | 137963CF255BC0310036D75F /* LaunchScreen.storyboard */, 121 | 137963D2255BC0310036D75F /* Info.plist */, 122 | 137963D3255BC0310036D75F /* main.m */, 123 | ); 124 | path = Notification; 125 | sourceTree = ""; 126 | }; 127 | 137963DC255BC0310036D75F /* NotificationTests */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 137963DD255BC0310036D75F /* NotificationTests.m */, 131 | 137963DF255BC0310036D75F /* Info.plist */, 132 | ); 133 | path = NotificationTests; 134 | sourceTree = ""; 135 | }; 136 | 137963E7255BC0310036D75F /* NotificationUITests */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 137963E8255BC0310036D75F /* NotificationUITests.m */, 140 | 137963EA255BC0310036D75F /* Info.plist */, 141 | ); 142 | path = NotificationUITests; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXNativeTarget section */ 148 | 137963BD255BC01F0036D75F /* Notification */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = 137963ED255BC0310036D75F /* Build configuration list for PBXNativeTarget "Notification" */; 151 | buildPhases = ( 152 | 137963BA255BC01F0036D75F /* Sources */, 153 | 137963BB255BC01F0036D75F /* Frameworks */, 154 | 137963BC255BC01F0036D75F /* Resources */, 155 | ); 156 | buildRules = ( 157 | ); 158 | dependencies = ( 159 | ); 160 | name = Notification; 161 | productName = Notification; 162 | productReference = 137963BE255BC01F0036D75F /* Notification.app */; 163 | productType = "com.apple.product-type.application"; 164 | }; 165 | 137963D8255BC0310036D75F /* NotificationTests */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = 137963F0255BC0310036D75F /* Build configuration list for PBXNativeTarget "NotificationTests" */; 168 | buildPhases = ( 169 | 137963D5255BC0310036D75F /* Sources */, 170 | 137963D6255BC0310036D75F /* Frameworks */, 171 | 137963D7255BC0310036D75F /* Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | 137963DB255BC0310036D75F /* PBXTargetDependency */, 177 | ); 178 | name = NotificationTests; 179 | productName = NotificationTests; 180 | productReference = 137963D9255BC0310036D75F /* NotificationTests.xctest */; 181 | productType = "com.apple.product-type.bundle.unit-test"; 182 | }; 183 | 137963E3255BC0310036D75F /* NotificationUITests */ = { 184 | isa = PBXNativeTarget; 185 | buildConfigurationList = 137963F3255BC0310036D75F /* Build configuration list for PBXNativeTarget "NotificationUITests" */; 186 | buildPhases = ( 187 | 137963E0255BC0310036D75F /* Sources */, 188 | 137963E1255BC0310036D75F /* Frameworks */, 189 | 137963E2255BC0310036D75F /* Resources */, 190 | ); 191 | buildRules = ( 192 | ); 193 | dependencies = ( 194 | 137963E6255BC0310036D75F /* PBXTargetDependency */, 195 | ); 196 | name = NotificationUITests; 197 | productName = NotificationUITests; 198 | productReference = 137963E4255BC0310036D75F /* NotificationUITests.xctest */; 199 | productType = "com.apple.product-type.bundle.ui-testing"; 200 | }; 201 | /* End PBXNativeTarget section */ 202 | 203 | /* Begin PBXProject section */ 204 | 137963B6255BC01F0036D75F /* Project object */ = { 205 | isa = PBXProject; 206 | attributes = { 207 | LastUpgradeCheck = 1160; 208 | ORGANIZATIONNAME = watermark; 209 | TargetAttributes = { 210 | 137963BD255BC01F0036D75F = { 211 | CreatedOnToolsVersion = 11.6; 212 | }; 213 | 137963D8255BC0310036D75F = { 214 | CreatedOnToolsVersion = 11.6; 215 | TestTargetID = 137963BD255BC01F0036D75F; 216 | }; 217 | 137963E3255BC0310036D75F = { 218 | CreatedOnToolsVersion = 11.6; 219 | TestTargetID = 137963BD255BC01F0036D75F; 220 | }; 221 | }; 222 | }; 223 | buildConfigurationList = 137963B9255BC01F0036D75F /* Build configuration list for PBXProject "Notification" */; 224 | compatibilityVersion = "Xcode 9.3"; 225 | developmentRegion = en; 226 | hasScannedForEncodings = 0; 227 | knownRegions = ( 228 | en, 229 | Base, 230 | ); 231 | mainGroup = 137963B5255BC01F0036D75F; 232 | productRefGroup = 137963BF255BC01F0036D75F /* Products */; 233 | projectDirPath = ""; 234 | projectRoot = ""; 235 | targets = ( 236 | 137963BD255BC01F0036D75F /* Notification */, 237 | 137963D8255BC0310036D75F /* NotificationTests */, 238 | 137963E3255BC0310036D75F /* NotificationUITests */, 239 | ); 240 | }; 241 | /* End PBXProject section */ 242 | 243 | /* Begin PBXResourcesBuildPhase section */ 244 | 137963BC255BC01F0036D75F /* Resources */ = { 245 | isa = PBXResourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 137963D1255BC0310036D75F /* LaunchScreen.storyboard in Resources */, 249 | 137963CE255BC0310036D75F /* Assets.xcassets in Resources */, 250 | 137963CC255BC01F0036D75F /* Main.storyboard in Resources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | 137963D7255BC0310036D75F /* Resources */ = { 255 | isa = PBXResourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | ); 259 | runOnlyForDeploymentPostprocessing = 0; 260 | }; 261 | 137963E2255BC0310036D75F /* Resources */ = { 262 | isa = PBXResourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXResourcesBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 137963BA255BC01F0036D75F /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 137963C9255BC01F0036D75F /* ViewController.m in Sources */, 276 | 137963F8255BC1D40036D75F /* NotificationCenter.m in Sources */, 277 | 137963C3255BC01F0036D75F /* AppDelegate.m in Sources */, 278 | 137963D4255BC0310036D75F /* main.m in Sources */, 279 | 137963C6255BC01F0036D75F /* SceneDelegate.m in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | 137963D5255BC0310036D75F /* Sources */ = { 284 | isa = PBXSourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | 137963DE255BC0310036D75F /* NotificationTests.m in Sources */, 288 | ); 289 | runOnlyForDeploymentPostprocessing = 0; 290 | }; 291 | 137963E0255BC0310036D75F /* Sources */ = { 292 | isa = PBXSourcesBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | 137963E9255BC0310036D75F /* NotificationUITests.m in Sources */, 296 | ); 297 | runOnlyForDeploymentPostprocessing = 0; 298 | }; 299 | /* End PBXSourcesBuildPhase section */ 300 | 301 | /* Begin PBXTargetDependency section */ 302 | 137963DB255BC0310036D75F /* PBXTargetDependency */ = { 303 | isa = PBXTargetDependency; 304 | target = 137963BD255BC01F0036D75F /* Notification */; 305 | targetProxy = 137963DA255BC0310036D75F /* PBXContainerItemProxy */; 306 | }; 307 | 137963E6255BC0310036D75F /* PBXTargetDependency */ = { 308 | isa = PBXTargetDependency; 309 | target = 137963BD255BC01F0036D75F /* Notification */; 310 | targetProxy = 137963E5255BC0310036D75F /* PBXContainerItemProxy */; 311 | }; 312 | /* End PBXTargetDependency section */ 313 | 314 | /* Begin PBXVariantGroup section */ 315 | 137963CA255BC01F0036D75F /* Main.storyboard */ = { 316 | isa = PBXVariantGroup; 317 | children = ( 318 | 137963CB255BC01F0036D75F /* Base */, 319 | ); 320 | name = Main.storyboard; 321 | sourceTree = ""; 322 | }; 323 | 137963CF255BC0310036D75F /* LaunchScreen.storyboard */ = { 324 | isa = PBXVariantGroup; 325 | children = ( 326 | 137963D0255BC0310036D75F /* Base */, 327 | ); 328 | name = LaunchScreen.storyboard; 329 | sourceTree = ""; 330 | }; 331 | /* End PBXVariantGroup section */ 332 | 333 | /* Begin XCBuildConfiguration section */ 334 | 137963EB255BC0310036D75F /* Debug */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ALWAYS_SEARCH_USER_PATHS = NO; 338 | CLANG_ANALYZER_NONNULL = YES; 339 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 340 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 341 | CLANG_CXX_LIBRARY = "libc++"; 342 | CLANG_ENABLE_MODULES = YES; 343 | CLANG_ENABLE_OBJC_ARC = YES; 344 | CLANG_ENABLE_OBJC_WEAK = YES; 345 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 346 | CLANG_WARN_BOOL_CONVERSION = YES; 347 | CLANG_WARN_COMMA = YES; 348 | CLANG_WARN_CONSTANT_CONVERSION = YES; 349 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 350 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 351 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 352 | CLANG_WARN_EMPTY_BODY = YES; 353 | CLANG_WARN_ENUM_CONVERSION = YES; 354 | CLANG_WARN_INFINITE_RECURSION = YES; 355 | CLANG_WARN_INT_CONVERSION = YES; 356 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 357 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 358 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 359 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 360 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 361 | CLANG_WARN_STRICT_PROTOTYPES = YES; 362 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 363 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 364 | CLANG_WARN_UNREACHABLE_CODE = YES; 365 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 366 | COPY_PHASE_STRIP = NO; 367 | DEBUG_INFORMATION_FORMAT = dwarf; 368 | ENABLE_STRICT_OBJC_MSGSEND = YES; 369 | ENABLE_TESTABILITY = YES; 370 | GCC_C_LANGUAGE_STANDARD = gnu11; 371 | GCC_DYNAMIC_NO_PIC = NO; 372 | GCC_NO_COMMON_BLOCKS = YES; 373 | GCC_OPTIMIZATION_LEVEL = 0; 374 | GCC_PREPROCESSOR_DEFINITIONS = ( 375 | "DEBUG=1", 376 | "$(inherited)", 377 | ); 378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNUSED_FUNCTION = YES; 383 | GCC_WARN_UNUSED_VARIABLE = YES; 384 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 385 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 386 | MTL_FAST_MATH = YES; 387 | ONLY_ACTIVE_ARCH = YES; 388 | SDKROOT = iphoneos; 389 | }; 390 | name = Debug; 391 | }; 392 | 137963EC255BC0310036D75F /* Release */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | ALWAYS_SEARCH_USER_PATHS = NO; 396 | CLANG_ANALYZER_NONNULL = YES; 397 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 398 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 399 | CLANG_CXX_LIBRARY = "libc++"; 400 | CLANG_ENABLE_MODULES = YES; 401 | CLANG_ENABLE_OBJC_ARC = YES; 402 | CLANG_ENABLE_OBJC_WEAK = YES; 403 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 404 | CLANG_WARN_BOOL_CONVERSION = YES; 405 | CLANG_WARN_COMMA = YES; 406 | CLANG_WARN_CONSTANT_CONVERSION = YES; 407 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 408 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 409 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 410 | CLANG_WARN_EMPTY_BODY = YES; 411 | CLANG_WARN_ENUM_CONVERSION = YES; 412 | CLANG_WARN_INFINITE_RECURSION = YES; 413 | CLANG_WARN_INT_CONVERSION = YES; 414 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 415 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 416 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 418 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 419 | CLANG_WARN_STRICT_PROTOTYPES = YES; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 422 | CLANG_WARN_UNREACHABLE_CODE = YES; 423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 424 | COPY_PHASE_STRIP = NO; 425 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 426 | ENABLE_NS_ASSERTIONS = NO; 427 | ENABLE_STRICT_OBJC_MSGSEND = YES; 428 | GCC_C_LANGUAGE_STANDARD = gnu11; 429 | GCC_NO_COMMON_BLOCKS = YES; 430 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 431 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 432 | GCC_WARN_UNDECLARED_SELECTOR = YES; 433 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 434 | GCC_WARN_UNUSED_FUNCTION = YES; 435 | GCC_WARN_UNUSED_VARIABLE = YES; 436 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 437 | MTL_ENABLE_DEBUG_INFO = NO; 438 | MTL_FAST_MATH = YES; 439 | SDKROOT = iphoneos; 440 | VALIDATE_PRODUCT = YES; 441 | }; 442 | name = Release; 443 | }; 444 | 137963EE255BC0310036D75F /* Debug */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 448 | CODE_SIGN_STYLE = Automatic; 449 | INFOPLIST_FILE = Notification/Info.plist; 450 | LD_RUNPATH_SEARCH_PATHS = ( 451 | "$(inherited)", 452 | "@executable_path/Frameworks", 453 | ); 454 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.Notification; 455 | PRODUCT_NAME = "$(TARGET_NAME)"; 456 | TARGETED_DEVICE_FAMILY = "1,2"; 457 | }; 458 | name = Debug; 459 | }; 460 | 137963EF255BC0310036D75F /* Release */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 464 | CODE_SIGN_STYLE = Automatic; 465 | INFOPLIST_FILE = Notification/Info.plist; 466 | LD_RUNPATH_SEARCH_PATHS = ( 467 | "$(inherited)", 468 | "@executable_path/Frameworks", 469 | ); 470 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.Notification; 471 | PRODUCT_NAME = "$(TARGET_NAME)"; 472 | TARGETED_DEVICE_FAMILY = "1,2"; 473 | }; 474 | name = Release; 475 | }; 476 | 137963F1255BC0310036D75F /* Debug */ = { 477 | isa = XCBuildConfiguration; 478 | buildSettings = { 479 | BUNDLE_LOADER = "$(TEST_HOST)"; 480 | CODE_SIGN_STYLE = Automatic; 481 | INFOPLIST_FILE = NotificationTests/Info.plist; 482 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 483 | LD_RUNPATH_SEARCH_PATHS = ( 484 | "$(inherited)", 485 | "@executable_path/Frameworks", 486 | "@loader_path/Frameworks", 487 | ); 488 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.NotificationTests; 489 | PRODUCT_NAME = "$(TARGET_NAME)"; 490 | TARGETED_DEVICE_FAMILY = "1,2"; 491 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Notification.app/Notification"; 492 | }; 493 | name = Debug; 494 | }; 495 | 137963F2255BC0310036D75F /* Release */ = { 496 | isa = XCBuildConfiguration; 497 | buildSettings = { 498 | BUNDLE_LOADER = "$(TEST_HOST)"; 499 | CODE_SIGN_STYLE = Automatic; 500 | INFOPLIST_FILE = NotificationTests/Info.plist; 501 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 502 | LD_RUNPATH_SEARCH_PATHS = ( 503 | "$(inherited)", 504 | "@executable_path/Frameworks", 505 | "@loader_path/Frameworks", 506 | ); 507 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.NotificationTests; 508 | PRODUCT_NAME = "$(TARGET_NAME)"; 509 | TARGETED_DEVICE_FAMILY = "1,2"; 510 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Notification.app/Notification"; 511 | }; 512 | name = Release; 513 | }; 514 | 137963F4255BC0310036D75F /* Debug */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | CODE_SIGN_STYLE = Automatic; 518 | INFOPLIST_FILE = NotificationUITests/Info.plist; 519 | LD_RUNPATH_SEARCH_PATHS = ( 520 | "$(inherited)", 521 | "@executable_path/Frameworks", 522 | "@loader_path/Frameworks", 523 | ); 524 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.NotificationUITests; 525 | PRODUCT_NAME = "$(TARGET_NAME)"; 526 | TARGETED_DEVICE_FAMILY = "1,2"; 527 | TEST_TARGET_NAME = Notification; 528 | }; 529 | name = Debug; 530 | }; 531 | 137963F5255BC0310036D75F /* Release */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | CODE_SIGN_STYLE = Automatic; 535 | INFOPLIST_FILE = NotificationUITests/Info.plist; 536 | LD_RUNPATH_SEARCH_PATHS = ( 537 | "$(inherited)", 538 | "@executable_path/Frameworks", 539 | "@loader_path/Frameworks", 540 | ); 541 | PRODUCT_BUNDLE_IDENTIFIER = com.watermark.NotificationUITests; 542 | PRODUCT_NAME = "$(TARGET_NAME)"; 543 | TARGETED_DEVICE_FAMILY = "1,2"; 544 | TEST_TARGET_NAME = Notification; 545 | }; 546 | name = Release; 547 | }; 548 | /* End XCBuildConfiguration section */ 549 | 550 | /* Begin XCConfigurationList section */ 551 | 137963B9255BC01F0036D75F /* Build configuration list for PBXProject "Notification" */ = { 552 | isa = XCConfigurationList; 553 | buildConfigurations = ( 554 | 137963EB255BC0310036D75F /* Debug */, 555 | 137963EC255BC0310036D75F /* Release */, 556 | ); 557 | defaultConfigurationIsVisible = 0; 558 | defaultConfigurationName = Release; 559 | }; 560 | 137963ED255BC0310036D75F /* Build configuration list for PBXNativeTarget "Notification" */ = { 561 | isa = XCConfigurationList; 562 | buildConfigurations = ( 563 | 137963EE255BC0310036D75F /* Debug */, 564 | 137963EF255BC0310036D75F /* Release */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | 137963F0255BC0310036D75F /* Build configuration list for PBXNativeTarget "NotificationTests" */ = { 570 | isa = XCConfigurationList; 571 | buildConfigurations = ( 572 | 137963F1255BC0310036D75F /* Debug */, 573 | 137963F2255BC0310036D75F /* Release */, 574 | ); 575 | defaultConfigurationIsVisible = 0; 576 | defaultConfigurationName = Release; 577 | }; 578 | 137963F3255BC0310036D75F /* Build configuration list for PBXNativeTarget "NotificationUITests" */ = { 579 | isa = XCConfigurationList; 580 | buildConfigurations = ( 581 | 137963F4255BC0310036D75F /* Debug */, 582 | 137963F5255BC0310036D75F /* Release */, 583 | ); 584 | defaultConfigurationIsVisible = 0; 585 | defaultConfigurationName = Release; 586 | }; 587 | /* End XCConfigurationList section */ 588 | }; 589 | rootObject = 137963B6255BC01F0036D75F /* Project object */; 590 | } 591 | -------------------------------------------------------------------------------- /demo/Notification/Notification.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/Notification/Notification.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/Notification/Notification.xcodeproj/project.xcworkspace/xcuserdata/yxyt.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiuFuBo/iOSInterviewQuestions/265cb18cc24950446dc9d7b4157ed4fcdb5ba0f8/demo/Notification/Notification.xcodeproj/project.xcworkspace/xcuserdata/yxyt.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /demo/Notification/Notification.xcodeproj/xcuserdata/yxyt.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /demo/Notification/Notification.xcodeproj/xcuserdata/yxyt.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Notification.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/Notification/Notification/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /demo/Notification/Notification/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | #pragma mark - UISceneSession lifecycle 25 | 26 | 27 | - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { 28 | // Called when a new scene session is being created. 29 | // Use this method to select a configuration to create the new scene with. 30 | return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; 31 | } 32 | 33 | 34 | - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { 35 | // Called when the user discards a scene session. 36 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 37 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 38 | } 39 | 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /demo/Notification/Notification/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /demo/Notification/Notification/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/Notification/Notification/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 | -------------------------------------------------------------------------------- /demo/Notification/Notification/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 | -------------------------------------------------------------------------------- /demo/Notification/Notification/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /demo/Notification/Notification/NotificationCenter.h: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenter.h 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface Notification : NSObject 12 | @property (nonatomic, strong, readwrite) NSDictionary *userInfo; 13 | @property (nonatomic, assign) id object; 14 | @property (nonatomic, assign) id observer; 15 | @property (nonatomic, copy) NSString *name; 16 | @property (nonatomic, copy) void(^callBack)(void); 17 | @property (nonatomic, assign) SEL aSelector; 18 | 19 | - (NSString *)name; 20 | - (id)object; 21 | - (NSDictionary *)userInfo; 22 | 23 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject; 24 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject userInfo:(NSDictionary *)aUserInfo; 25 | - (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo; 26 | 27 | 28 | @end 29 | 30 | 31 | @interface NotificationCenter : NSObject 32 | 33 | + (instancetype)defaultCenter; 34 | - (void)addObserver:(id)observer callBack:(void(^)(void))callBack name:(NSString *)aName object:(id)anObject; 35 | - (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject; 36 | 37 | - (void)postNotification:(Notification *)notification; 38 | - (void)postNotificationName:(NSString *)aName object:(id)anObject; 39 | - (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo; 40 | 41 | @end 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo/Notification/Notification/NotificationCenter.m: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenter.m 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "NotificationCenter.h" 10 | 11 | @implementation Notification 12 | 13 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject { 14 | return [Notification notificationWithName:aname object:anObject userInfo:nil]; 15 | } 16 | 17 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject userInfo:(NSDictionary *)aUserInfo { 18 | 19 | Notification *nofi = [[Notification alloc]init]; 20 | nofi.name = aname; 21 | nofi.object = anObject; 22 | nofi.userInfo = aUserInfo; 23 | return nofi; 24 | } 25 | 26 | - (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo { 27 | return [Notification notificationWithName:name object:object userInfo:userInfo]; 28 | } 29 | 30 | 31 | @end 32 | 33 | @implementation NotificationCenter{ 34 | NSMutableArray *_nofiArray; 35 | } 36 | 37 | + (instancetype)defaultCenter { 38 | static NotificationCenter *_infoCenter = nil; 39 | static dispatch_once_t onceToken; 40 | dispatch_once(&onceToken, ^{ 41 | _infoCenter = [[NotificationCenter alloc]init]; 42 | }); 43 | return _infoCenter; 44 | } 45 | 46 | - (instancetype)init 47 | { 48 | self = [super init]; 49 | if (self) { 50 | _nofiArray = [[NSMutableArray alloc]init]; 51 | } 52 | return self; 53 | } 54 | 55 | - (void)addObserver:(id)observer selector:(SEL)aSelector callBack:(void (^)(void))callBack name:(NSString *)aName object:(id)anObject { 56 | 57 | Notification *nofi = [[Notification alloc]init]; 58 | nofi.callBack = callBack; 59 | nofi.name = aName; 60 | nofi.aSelector = aSelector; 61 | nofi.observer = observer; 62 | [_nofiArray addObject:nofi]; 63 | } 64 | 65 | - (void)addObserver:(id)observer callBack:(void (^)(void))callBack name:(NSString *)aName object:(id)anObject { 66 | [self addObserver:observer selector:nil callBack:callBack name:aName object:anObject]; 67 | } 68 | 69 | - (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject { 70 | [self addObserver:observer selector:aSelector callBack:nil name:aName object:anObject]; 71 | } 72 | 73 | - (void)postNotificationName:(NSString *)aName object:(id)anObject { 74 | 75 | for (Notification *nofi in _nofiArray) { 76 | if ([nofi.name isEqualToString:aName]) { 77 | 78 | nofi.object = anObject ? : anObject; 79 | 80 | if (nofi.callBack) { 81 | nofi.callBack(); 82 | } 83 | if (nofi.aSelector) { 84 | if ([nofi.observer respondsToSelector:nofi.aSelector]) { 85 | // [nofi.observer performSelector:nofi.aSelector withObject:nofi]; 86 | IMP imp = [nofi.observer methodForSelector:nofi.aSelector]; 87 | void(*func)(id, SEL,Notification *) = (void *)imp; 88 | func(nofi.observer,nofi.aSelector,nofi); 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | - (void)postNotification:(Notification *)notification { 96 | for (Notification *nofi in _nofiArray) { 97 | if ([nofi.name isEqualToString:notification.name]) { 98 | nofi.callBack = notification.callBack; 99 | nofi.object = notification.object; 100 | nofi.aSelector = notification.aSelector; 101 | nofi.observer = notification.observer; 102 | nofi.userInfo = notification.userInfo; 103 | break; 104 | } 105 | } 106 | [self postNotificationName:notification.name object:nil]; 107 | } 108 | 109 | - (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo { 110 | 111 | for (Notification *nofi in _nofiArray) { 112 | 113 | if ([nofi.name isEqualToString:aName]) { 114 | nofi.object = anObject; 115 | nofi.userInfo = aUserInfo; 116 | break; 117 | } 118 | } 119 | [self postNotificationName:aName object:nil]; 120 | } 121 | 122 | @end 123 | -------------------------------------------------------------------------------- /demo/Notification/Notification/SceneDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.h 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface SceneDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow * window; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /demo/Notification/Notification/SceneDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.m 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "SceneDelegate.h" 10 | 11 | @interface SceneDelegate () 12 | 13 | @end 14 | 15 | @implementation SceneDelegate 16 | 17 | 18 | - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { 19 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 20 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 21 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 22 | } 23 | 24 | 25 | - (void)sceneDidDisconnect:(UIScene *)scene { 26 | // Called as the scene is being released by the system. 27 | // This occurs shortly after the scene enters the background, or when its session is discarded. 28 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 29 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 30 | } 31 | 32 | 33 | - (void)sceneDidBecomeActive:(UIScene *)scene { 34 | // Called when the scene has moved from an inactive state to an active state. 35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 36 | } 37 | 38 | 39 | - (void)sceneWillResignActive:(UIScene *)scene { 40 | // Called when the scene will move from an active state to an inactive state. 41 | // This may occur due to temporary interruptions (ex. an incoming phone call). 42 | } 43 | 44 | 45 | - (void)sceneWillEnterForeground:(UIScene *)scene { 46 | // Called as the scene transitions from the background to the foreground. 47 | // Use this method to undo the changes made on entering the background. 48 | } 49 | 50 | 51 | - (void)sceneDidEnterBackground:(UIScene *)scene { 52 | // Called as the scene transitions from the foreground to the background. 53 | // Use this method to save data, release shared resources, and store enough scene-specific state information 54 | // to restore the scene back to its current state. 55 | } 56 | 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /demo/Notification/Notification/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /demo/Notification/Notification/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "NotificationCenter.h" 11 | 12 | @interface ViewController () 13 | 14 | @end 15 | 16 | @implementation ViewController 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | [[NotificationCenter defaultCenter] addObserver:self selector:@selector(targetAction:) name:@"notificationTargetActionName" object:nil]; 21 | [[NotificationCenter defaultCenter] postNotificationName:@"notificationTargetActionName" object:nil userInfo:@{@"name":@"zhangsan",@"hobby":@"hiking"}]; 22 | } 23 | 24 | - (void)targetAction:(Notification *)noti { 25 | NSLog(@"%@,%@",noti.name,noti.userInfo); 26 | } 27 | 28 | 29 | 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /demo/Notification/Notification/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Notification 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | NSString * appDelegateClassName; 14 | @autoreleasepool { 15 | // Setup code that might create autoreleased objects goes here. 16 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 17 | } 18 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 19 | } 20 | -------------------------------------------------------------------------------- /demo/Notification/NotificationTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/Notification/NotificationTests/NotificationTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationTests.m 3 | // NotificationTests 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NotificationTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation NotificationTests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | - (void)tearDown { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | - (void)testExample { 26 | // This is an example of a functional test case. 27 | // Use XCTAssert and related functions to verify your tests produce the correct results. 28 | } 29 | 30 | - (void)testPerformanceExample { 31 | // This is an example of a performance test case. 32 | [self measureBlock:^{ 33 | // Put the code you want to measure the time of here. 34 | }]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /demo/Notification/NotificationUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/Notification/NotificationUITests/NotificationUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationUITests.m 3 | // NotificationUITests 4 | // 5 | // Created by liufubo on 2020/11/11. 6 | // Copyright © 2020 watermark. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NotificationUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation NotificationUITests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | 20 | // In UI tests it is usually best to stop immediately when a failure occurs. 21 | self.continueAfterFailure = NO; 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | - (void)tearDown { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | } 29 | 30 | - (void)testExample { 31 | // UI tests must launch the application that they test. 32 | XCUIApplication *app = [[XCUIApplication alloc] init]; 33 | [app launch]; 34 | 35 | // Use recording to get started writing UI tests. 36 | // Use XCTAssert and related functions to verify your tests produce the correct results. 37 | } 38 | 39 | - (void)testLaunchPerformance { 40 | if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { 41 | // This measures how long it takes to launch your application. 42 | [self measureWithMetrics:@[XCTOSSignpostMetric.applicationLaunchMetric] block:^{ 43 | [[[XCUIApplication alloc] init] launch]; 44 | }]; 45 | } 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /iOS面试题难点集锦/iOS面试题难点集锦(一).md: -------------------------------------------------------------------------------- 1 | ## iOS面试题难点集锦(一)---参考答案 2 | 3 | 说明:面试题来源于自己知识点的总结以及一些博客大佬例如:[玉令天下的博客](http://yulingtianxia.com)的博文学习记录。文章问题答案如有出入的地方,欢迎联系我加以校正。 4 | 5 | # 索引 6 | 7 | 8 | 1. [什么是Runtime?Runtime方法是如何在缓存中寻找的?](#什么是runtime?runtime方法是如何在缓存中寻找的) 9 | 10 | 2. [在Runtime中,是如何在消息列表中查找方法的?](#在runtime中是如何在消息列表中查找方法的) 11 | 12 | 3. [Runtime在项目中的运用](#Runtime在项目中的运用) 13 | 14 | 4. [Class继承链关系](#Class继承链关系) 15 | 16 | 5. [以上代码打印结果是什么?](#以上代码打印结果是什么) 17 | 18 | 5. [RunLoop和屏幕点击事件传递链以及响应链关系](#RunLoop和屏幕点击事件传递链以及响应链关系) 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ### 什么是Runtime?Runtime方法是如何在缓存中寻找的? 27 | 28 | OC语言是一门动态语言,会将程序的一些决定工作从编译期推迟到运行期。由于OC语言运行时的特性,所以其不仅需要依赖编译期,还需要依赖运行时环境,这就是Objective-C Runtime(运行时环境)系统存在的意义。Runtime基本是由一套C、C++、以及汇编编写的,可见苹果为了动态系统的高效而作出的努力。你可以在[这里下载](https://opensource.apple.com/source/objc4/)到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。 29 | 30 | OC􏰈􏰉􏰈􏰉语言在编译期都会被编译为C语言的Runtime代码,二进制执行过程中执行的都是C语言代码。而OC的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。 31 | 32 | 根据Apple官方文档的描述,目前OC运行时分为两个版本,Modern和Legacy,我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 maxOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于Legacy在实例变量发生改变后,需要重新编译其子类。Modern在实例变量发生改变后,不需要重新编译其子类。 33 | 34 | ## Runtime的交互 35 | 36 | 我们在编写Objective-C代码的时候,无论是直接的还是间接的都会使用到Runtime,总的来说Runtime系统发生交互的方式主要有三种分别如下: 37 | 38 | 1.Objective-C源码直接使用上层Objective-C源码,然后底层会通过Runtime为其提供运行支持,上层不需要关心Runtime的运行。 39 | 2.通过Foundation框架的NSObject类定义的方法来访问Runtime。NSObject在OC代码中绝大多数类都是继承自NSObject的,NSProxy类例外。Runtime在NSObject中定义了一些基础操作,NSObject的子类也具备这些特性。 40 | 3.通过直接调用Runtime函数,不过我们一般情况下不需要直接调用Runtime函数,咱们直接和Objective-C代码打交道就好了。 41 | 42 | ## Runtime方法的调用 43 | 44 | 除了 `+(void)load` 以外,所有的OC方法调用在底层都会被转化为`objc_msgSend:`函数的调用,它是这样的结构: 45 | ``` 46 | id objc_msgSend ( id self, SEL op, ... ); 47 | 48 | ``` 49 | 下面咱们先逐一的对该函数参数做一个详尽的讲解。看过Runtime源码的可以直接跳过。 50 | 51 | ### id 52 | 53 | `objc_msgSend`函数第一个参数类型为`id`,它实际是指向类实例的指针,具体结构如下: 54 | 55 | ``` 56 | typedef struct objc_object *id; 57 | 58 | ``` 59 | 那么`objc_object`又是什么东西呢?参考Runtime部分源码: 60 | 61 | ``` 62 | struct objc_object { 63 | private: 64 | isa_t isa; 65 | 66 | public: 67 | 68 | // ISA() assumes this is NOT a tagged pointer object 69 | Class ISA(); 70 | 71 | // getIsa() allows this to be a tagged pointer object 72 | Class getIsa(); 73 | ... 此处省略其他方法声明 74 | } 75 | 76 | ``` 77 | `objc_object`结构体包含了一个`isa`指针,类型为`isa_t`联合体。根据`isa`就可以顺藤摸瓜找到对象所属的类。因为`isa_t`使用`union`实现,所以可能表示多种形态,既可以当成指针,也可以作为存储标志位。它跟`tagged pointer`都涉及到引用计数管理。有关`isa_t`联合体更多的内容可以查看[Objective-C 引用计数原理](https://www.jianshu.com/p/12d6e64c07bb)。 78 | 79 | 注:`isa`指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用 class 方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的 `isa` 指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见[官方文档](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOImplementation.html) 80 | 81 | ### SEL 82 | 83 | `objc_msgSend`函数第二个参数类型为`SEL`,它是`selector`在Objc中的表示类型(Swift中是`Selector`类)。`selector`是方法选择器, 84 | 可以理解为区分方法的 ID,而这个 ID 的数据结构是`SEL`: 85 | ``` 86 | typedef struct objc_selector *SEL; 87 | 88 | ``` 89 | 其实它也就是一个映射方法的C字符串,你可以用 Objc 编译器命令`@selector()`或者 Runtime 系统的 `sel_registerName` 函数来获得一个 `SEL` 类型的方法选择器。在OC中不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是OC方法命名中有时候会带上参数类型。 90 | 91 | PS:`objc_msgSend`方法第三个参数`...`表示的是函数的一些参数,这里就不再做具体的讲解。 92 | 93 | 上面我们讲到`objc_msgSend`函数的各个参数的作用,同时了解到`isa`指针不能用来确定类型,必须用`class`方法来确定实例对象的类。那么`class`又究竟是个什么呢?咱们通过Runtime源码可以了解到它是一个结构体指针,具体结构如下: 94 | ``` 95 | typedef struct objc_class *Class; 96 | 97 | ``` 98 | 而咱们的`objc_class`又是继承自`objc_object`结构体的,`objc_class`包含了很多成员,主要如下: 99 | 100 | ``` 101 | struct objc_class : objc_object { 102 | // Class ISA; 103 | Class superclass; 104 | cache_t cache; // formerly cache pointer and vtable 105 | class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 106 | class_rw_t *data() { 107 | return bits.data(); 108 | } 109 | ... 省略其他方法 110 | } 111 | 112 | ``` 113 | 114 | `objc_class`它是继承自`objc_object`的,也就是说`Class`本身同时也是一个对象,为了处理类和对象的关系,runtime库创建了一种叫做元类(Meta Class)的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似 `[NSObject alloc] `的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 `[NSObject alloc]` 这条消息发给类对象的时候,objc_msgSend() 会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。 115 | 116 |
117 | 118 |
119 | 120 | 121 | 122 | 上图中实线是`superclass`指针,虚线是`isa`指针。 有趣的是根元类的超类是 `NSObject`,而 `isa` 指向了自己,而 `NSObject` 的超类为 nil,也就是它没有超类。 123 | 124 | 可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。 125 | 126 | 上面我们讲到OC方法调用实际转化为了 `objc_msgSend`函数调用,而通过方法选择器`SEL`字符串去寻找方法实现`IMP`则是先去当前对象的`cache`缓存中寻找的,也就是上面`objc_class`结构体对应的`cache_t`结构体内部实现。`cache_t`结构体的结构如下: 127 | ``` 128 | struct cache_t { 129 | struct bucket_t *_buckets; 130 | mask_t _mask; 131 | mask_t _occupied; 132 | ... 省略其他方法 133 | } 134 | 135 | ``` 136 | `mask`存储了散列表的长度,`occupied`则存储了缓存方法的数量。 137 | 138 | `buckets`则是存储了`IMP`和`SEL`映射的散列表(Hash表)。 通过Runtime源码查看`buckets`结构体如下: 139 | 140 | ``` 141 | struct bucket_t { 142 | private: 143 | cache_key_t _key; 144 | IMP _imp; 145 | 146 | public: 147 | inline cache_key_t key() const { return _key; } 148 | inline IMP imp() const { return (IMP)_imp; } 149 | inline void setKey(cache_key_t newKey) { _key = newKey; } 150 | inline void setImp(IMP newImp) { _imp = newImp; } 151 | 152 | void set(cache_key_t newKey, IMP newImp); 153 | }; 154 | 155 | ``` 156 | 157 | 通过阅读Runtime源码,我们可以找到方法缓存的实现函数为`cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)`具体实现如下: 158 | 159 | ``` 160 | static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) 161 | { //线程安全操作 162 | cacheUpdateLock.assertLocked(); 163 | //系统要求在类初始化完成之前,不能进行方法缓存,因此如果类还没有完成初始化就返回 164 | // Never cache before +initialize is done 165 | if (!cls->isInitialized()) return; 166 | 167 | // Make sure the entry wasn't added to the cache by some other thread 168 | // before we grabbed the cacheUpdateLock. 169 | //因为有可能其他线程已经抢先把该方法缓存进来,因此这里还需要检查一次缓存,如果cache_t找到该方法,就返回 170 | if (cache_getImp(cls, sel)) return; 171 | //通过类对象获取到cache_t 172 | cache_t *cache = getCache(cls); 173 | 174 | // Use the cache as-is if it is less than 3/4 full 175 | //拿到散列表中已经缓存方法数并在此基础上+1,看假设存好这个方法后会不会占用空间超过3/4,超过的话就不在这里缓存,二十对散列表进行扩容处理,所以这里自身并没有+1而是看+1后是否超过容量的3/4 176 | mask_t newOccupied = cache->occupied() + 1; 177 | //拿到散列表的容量,看散列表可以存储多少个bucket_t结构体 178 | mask_t capacity = cache->capacity(); 179 | if (cache->isConstantEmptyCache()) { 180 | // Cache is read-only. Replace it. 如果散列表还没有空间,则为起分配空间 181 | cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); 182 | } 183 | else if (newOccupied <= capacity / 4 * 3) { 184 | // Cache is less than 3/4 full. Use it as-is. 185 | //如果缓存小于总容量的3/4,则继续使用他 186 | } 187 | else { 188 | // Cache is too full. Expand it.如果大于3/4则进行扩容 189 | cache->expand(); 190 | } 191 | 192 | // Scan for the first unused slot(位置,狭槽) and insert there. 193 | // There is guaranteed to be an empty slot because the 194 | // minimum size is 4 and we resized at 3/4 full. 195 | //调用cache->find函数(在find有一个条件是散列表某个索引的数据为空或者key等于当前要找的这个key,则返回这个索引对应对应的方法实现,这个l的作用是看这个key&mask对应的索引是否还有其他内容,没有的话就拿到这个索引方法存储数据,有的话,则判断是否为__x86_64__(x86架构的64拓展向后兼容16位或者32位x86架构)或者__i386__(i386即Intel 80386。其实i386通常被用来作为对Intel(英特尔)32位微处理器的统称)则向上取整,如果为__arm64__则向下取整,有空的话就返回空的bucket_t来存储这方法的key和imp,) 196 | bucket_t *bucket = cache->find(sel, receiver); 197 | //拿到了散列表中的一个空位置将散列表中的已经缓存方法数+1 198 | if (bucket->sel() == 0) cache->incrementOccupied(); 199 | //将要缓存的方法的imp和Key放进去 200 | bucket->set(sel, imp); 201 | } 202 | 203 | ``` 204 | 205 | 从上面代码备注我们可以了解到在缓存过程,我们需要经历以下几个步骤: 206 | 207 | 1.线程上锁,确保线程安全的情况下对方法进行缓存操作。 208 | 2.在系统初始化完成之前,不能进行方法缓存,若当前类还没有完成初始化操作就直接return返回。 209 | 3.其他线程也是已经将当前方法缓存进来,因此需要检查一遍是否已经缓存过该方法,如果之前已经缓存过该方法,则直接返回。 210 | 4.判断散列表容量是否能继续存储该方法,如果超过当前类存储方法散列表内存的3/4,就需要对散列表进行扩容,扩容大小为原来的散列表内存2倍,并且直接扔掉以前缓存的方法,仅缓存当前方法。具体原由后面再详细讲解。如果散列表能够存储下当前方法,则判断是否已经散列表分配空间,如果未分配空间则需要为散列表分配空间。 211 | 5.通过调用`cache->find(sel,receiver)`函数获取适合的内存区域存储`imp`。 212 | 6.判断获取的散列表`SEL`所对应位置是否已经存储有数据了,如果没有存储过数据,则存储数据量+1。 213 | 7.将需要缓存方法存入散列表。 214 | 215 | 216 | ### 散列表扩容 217 | 218 | 通过上面步骤流程咱们了解到当散列表存储容量达到3/4以后,再存入新的方法则需要对散列表进行扩容,通过上面方法缓存入口函数可知,系统是调用`cache->expand()`函数来实现扩容的,具体实现可参考Runtime源码 219 | 220 | ``` 221 | //当散列表的空间占用超过3/4的时候,散列表会调用expand()函数进行扩展 222 | void cache_t::expand() 223 | { //线程安全操作,上锁 224 | cacheUpdateLock.assertLocked(); 225 | //旧散列表占用的存储空间 226 | uint32_t oldCapacity = capacity(); 227 | //将新散列表占用的内存空间扩展到老内存空间的2倍 228 | uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; 229 | 230 | if ((uint32_t)(mask_t)newCapacity != newCapacity) { 231 | // mask overflow - can't grow further处理溢出情况 232 | // fixme this wastes one bit of mask 233 | newCapacity = oldCapacity; 234 | } 235 | //释放旧的内存,开辟新的内存 236 | reallocate(oldCapacity, newCapacity); 237 | } 238 | 239 | ``` 240 | 通过`cache->expand()`函数我们了解到扩容过程实现步骤也有五步: 241 | 242 | 1.将当前线程上锁,确保线程安全 243 | 2.获取旧的散列表占用空间 244 | 3.将新的散列表占用空间设置为旧散列表的2倍 245 | 4.判断新散列表内存是否存在溢出,如果溢出,则内存还是设置为原来的内存大小 246 | 5.释放旧的散列表,创建新的散列表 247 | 248 | 咱们继续查看散列表内存释放函数`cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)`实现代码如下: 249 | 250 | ``` 251 | void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) 252 | { 253 | bool freeOld = canBeFreed();//这里仅仅判断旧的缓存容量池是否为空,为空则不需要释放 254 | 255 | bucket_t *oldBuckets = buckets(); 256 | bucket_t *newBuckets = allocateBuckets(newCapacity); 257 | 258 | // Cache's old contents are not propagated. 259 | // This is thought to save cache memory at the cost of extra cache fills. 260 | // fixme re-measure this 261 | 262 | assert(newCapacity > 0); 263 | assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); 264 | 265 | setBucketsAndMask(newBuckets, newCapacity - 1); 266 | 267 | if (freeOld) { 268 | cache_collect_free(oldBuckets, oldCapacity); 269 | cache_collect(false); 270 | } 271 | } 272 | 273 | ``` 274 | 从实现代码我们看到新的散列表数组被创建,实际可以存储大小为`newCapacity-1`,旧的散列表最后就会被释放,因为读写操作非常耗时,缓存的目的是节省时间,所以创建新缓存池没有将老的内存copy过来,而且这种操作也会清理掉长时间没有调用的方法。 275 | 276 | 处理完缓存散列表扩容问题以后,咱们需要关注一下,方法缓存入口函数`cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)`内部`cache->find(SEL s, id receiver)`函数,该函数用于查询`SEL`所对应的`bucket`。具体实现函数如下: 277 | 278 | ``` 279 | bucket_t * cache_t::find(SEL s, id receiver) 280 | { 281 | //当方法选择器为空,则断言打印错误 282 | assert(s != 0); 283 | //获取散列表 284 | bucket_t *b = buckets(); 285 | //获取mask 286 | mask_t m = mask(); 287 | //通过key找到key在散列表中存储的下标 288 | mask_t begin = cache_hash(s, m); 289 | //将下标赋值给i 290 | mask_t i = begin; 291 | //这里采用do{}while()并不是全部循环了,如果do{}内部第一次被执行时,如果key和i下标取出的key是对应的,或者key下标对应的方法为空,则直接返回, 292 | do { 293 | // 如果下标i中存储的bucket的key==0说明当前没有存储相应的key,将b[i]返回出去进行存储 294 | // 如果下标i中存储的bucket的key==s,说明当前空间内已经存储了相应key,将b[i]返回出去进行存储 295 | if (b[i].sel() == 0 || b[i].sel() == s) { 296 | return &b[i]; 297 | } 298 | //如果都不满足,则调用cache_nex(i,m)去寻找合适的i下标来存储imp 299 | } while ((i = cache_next(i, m)) != begin); 300 | 301 | 302 | Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); 303 | cache_t::bad_cache(receiver, (SEL)s, cls); 304 | } 305 | 306 | ``` 307 | 308 | `cache->find(SEL s, id receiver)`函数,当存储 `IMP` 之前需要查询散列表数组中的 `bucket_t`,采用 do{}while()只是为了处理 key 值所 对应的`hash`产生碰撞的问题。总结函数实现步骤如下: 309 | 1.获取散列表(Hash表) 310 | 2.获取mask,它是一个uint32_t类型的常量 311 | 3.通过hash函数`cache_hash(s,m)`获取散列表中SEL存储的下标 312 | 4.判断当前下标是否有值,如果没有值则直接返回该位置缓存新加入方法,如果之前如果有值,缓存的方法选择器字符串是否是当前需要缓存的字符串,如果是,则直接返回,如果获取的当前散列表下标对应的位置已经有内容了,则遇到了Hash碰撞,需要采用`cache_next(i,m)`函数获取适合的位置返回存储内容,这里涉及到两个函数,一个是`cache_hash(s,m)`,一个是`cache_next(i,m)`函数。 313 | 314 | ``` 315 | static inline mask_t cache_hash(SEL sel, mask_t mask) 316 | { 317 | return (mask_t)(uintptr_t)sel & mask; 318 | } 319 | 320 | ``` 321 | 322 | 在拿到key以后,`cache_hash(s,m)`采用SEL按位与mask得到一个小于等于mask的常量,任何一个数值&mask得到的数值最大就是mask。 323 | 324 | ``` 325 | static inline mask_t cache_next(mask_t i, mask_t mask) { 326 | return (i+1) & mask; 327 | } 328 | 329 | ``` 330 | `cache_next(mask_t i, mask_t mask)`方法内部则判断硬件系统如果是__x86_64__或者__i386系列的产品则向上取整 `-1` ,如果是__arm64__则向下取整 `+1` 331 | 332 | 333 | 334 | 335 | ### 在Runtime中是如何在消息列表中查找方法的? 336 | 337 | 注:通过以上讲解,我们知道了OC方法在缓存中查找方法实现的过程,但是如果缓存列表中没有找到相关方法实现,则需要进入方法列表中寻找。那么具体是如何寻找的呢?我们通过打开objc-msg-x86_64.s文件找到一段代码如下: 338 | 339 | ``` 340 | /******************************************************************** 341 | * 342 | * _objc_msgSend_uncached 343 | * _objc_msgSend_stret_uncached 344 | * _objc_msgLookup_uncached 345 | * _objc_msgLookup_stret_uncached 346 | * 347 | * The uncached method lookup. 348 | * 349 | ********************************************************************/ 350 | 351 | STATIC_ENTRY __objc_msgSend_uncached 352 | UNWIND __objc_msgSend_uncached, FrameWithNoSaves 353 | 354 | // THIS IS NOT A CALLABLE C FUNCTION 355 | // Out-of-band r10 is the searched class 356 | 357 | // r10 is already the class to search 358 | MethodTableLookup NORMAL // r11 = IMP 359 | jmp *%r11 // goto *imp 360 | 361 | END_ENTRY __objc_msgSend_uncached 362 | 363 | 364 | STATIC_ENTRY __objc_msgSend_stret_uncached 365 | UNWIND __objc_msgSend_stret_uncached, FrameWithNoSaves 366 | 367 | // THIS IS NOT A CALLABLE C FUNCTION 368 | // Out-of-band r10 is the searched class 369 | 370 | // r10 is already the class to search 371 | MethodTableLookup STRET // r11 = IMP 372 | jmp *%r11 // goto *imp 373 | 374 | END_ENTRY __objc_msgSend_stret_uncached 375 | 376 | 377 | STATIC_ENTRY __objc_msgLookup_uncached 378 | UNWIND __objc_msgLookup_uncached, FrameWithNoSaves 379 | 380 | ``` 381 | 从上面我们可以了解到,如果缓存中没有找到方法,则会执行一个宏 `MethodTableLookup STRET` 这个方法可以看到它后面的注释说明它将 `IMP` 函数指针存放到了r11寄存器中,之后再通过jmp%r11 找到`IMP`完成方法调用。`MethodTableLookup`宏函数实现如下: 382 | 383 | ``` 384 | .macro MethodTableLookup 385 | 386 | push %rbp 387 | mov %rsp, %rbp 388 | 389 | sub $$0x80+8, %rsp // +8 for alignment 390 | 391 | movdqa %xmm0, -0x80(%rbp) 392 | push %rax // might be xmm parameter count 393 | movdqa %xmm1, -0x70(%rbp) 394 | push %a1 395 | movdqa %xmm2, -0x60(%rbp) 396 | push %a2 397 | movdqa %xmm3, -0x50(%rbp) 398 | push %a3 399 | movdqa %xmm4, -0x40(%rbp) 400 | push %a4 401 | movdqa %xmm5, -0x30(%rbp) 402 | push %a5 403 | movdqa %xmm6, -0x20(%rbp) 404 | push %a6 405 | movdqa %xmm7, -0x10(%rbp) 406 | 407 | // _class_lookupMethodAndLoadCache3(receiver, selector, class) 408 | 409 | .if $0 == NORMAL 410 | // receiver already in a1 411 | // selector already in a2 412 | .else 413 | movq %a2, %a1 414 | movq %a3, %a2 415 | .endif 416 | movq %r10, %a3 417 | call __class_lookupMethodAndLoadCache3 418 | 419 | // IMP is now in %rax 420 | movq %rax, %r11 421 | 422 | movdqa -0x80(%rbp), %xmm0 423 | pop %a6 424 | movdqa -0x70(%rbp), %xmm1 425 | pop %a5 426 | movdqa -0x60(%rbp), %xmm2 427 | pop %a4 428 | movdqa -0x50(%rbp), %xmm3 429 | pop %a3 430 | movdqa -0x40(%rbp), %xmm4 431 | pop %a2 432 | movdqa -0x30(%rbp), %xmm5 433 | pop %a1 434 | movdqa -0x20(%rbp), %xmm6 435 | pop %rax 436 | movdqa -0x10(%rbp), %xmm7 437 | 438 | .if $0 == NORMAL 439 | cmp %r11, %r11 // set eq for nonstret forwarding 440 | .else 441 | test %r11, %r11 // set ne for stret forwarding 442 | .endif 443 | 444 | leave 445 | 446 | .endmacro 447 | 448 | ``` 449 | 我们可以观察到宏在内部调用了 `_class_lookupMethodAndLoadCache3()` 函数,而在该函数内部,它又调用了 `lookUpImpOrForward()`函数,在这个函数中,要么找到方法并执行,要么找不到方法,调用消息转发流程,下面咱们来详细讲讲这个函数实现原理。 450 | 451 | ``` 452 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 453 | bool initialize, bool cache, bool resolver) 454 | { 455 | IMP imp = nil; 456 | bool triedResolver = NO; 457 | 458 | runtimeLock.assertUnlocked(); 459 | 460 | // Optimistic cache lookup 461 | if (cache) { 462 | imp = cache_getImp(cls, sel); 463 | if (imp) return imp; 464 | } 465 | 466 | runtimeLock.lock(); 467 | checkIsKnownClass(cls); 468 | 469 | if (!cls->isRealized()) { 470 | cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock); 471 | // runtimeLock may have been dropped but is now locked again 472 | } 473 | 474 | if (initialize && !cls->isInitialized()) { 475 | cls = initializeAndLeaveLocked(cls, inst, runtimeLock); 476 | } 477 | 478 | retry: 479 | runtimeLock.assertLocked(); 480 | 481 | // Try this class's cache. 482 | 483 | imp = cache_getImp(cls, sel); 484 | if (imp) goto done; 485 | 486 | // Try this class's method lists. 487 | { 488 | Method meth = getMethodNoSuper_nolock(cls, sel); 489 | if (meth) { 490 | log_and_fill_cache(cls, meth->imp, sel, inst, cls); 491 | imp = meth->imp; 492 | goto done; 493 | } 494 | } 495 | 496 | // Try superclass caches and method lists. 497 | { 498 | unsigned attempts = unreasonableClassCount(); 499 | for (Class curClass = cls->superclass; 500 | curClass != nil; 501 | curClass = curClass->superclass) 502 | { 503 | // Halt if there is a cycle in the superclass chain. 504 | if (--attempts == 0) { 505 | _objc_fatal("Memory corruption in class list."); 506 | } 507 | 508 | // Superclass cache. 509 | imp = cache_getImp(curClass, sel); 510 | if (imp) { 511 | if (imp != (IMP)_objc_msgForward_impcache) { 512 | // Found the method in a superclass. Cache it in this class. 513 | log_and_fill_cache(cls, imp, sel, inst, curClass); 514 | goto done; 515 | } 516 | else { 517 | // Found a forward:: entry in a superclass. 518 | // Stop searching, but don't cache yet; call method 519 | // resolver for this class first. 520 | break; 521 | } 522 | } 523 | 524 | // Superclass method list. 525 | Method meth = getMethodNoSuper_nolock(curClass, sel); 526 | if (meth) { 527 | log_and_fill_cache(cls, meth->imp, sel, inst, curClass); 528 | imp = meth->imp; 529 | goto done; 530 | } 531 | } 532 | } 533 | 534 | // No implementation found. Try method resolver once. 535 | 536 | if (resolver && !triedResolver) { 537 | runtimeLock.unlock(); 538 | resolveMethod(cls, sel, inst); 539 | runtimeLock.lock(); 540 | } 541 | 542 | // No implementation found, and method resolver didn't help. 543 | // Use forwarding. 544 | 545 | imp = (IMP)_objc_msgForward_impcache; 546 | cache_fill(cls, sel, imp, inst); 547 | 548 | done: 549 | runtimeLock.unlock(); 550 | 551 | return imp; 552 | } 553 | 554 | ``` 555 | 556 | 上面代码比较长,咱们将主要实现功能罗列如下: 557 | 558 | 1.Runtime不加锁。 559 | 2.如果存在缓存,则读取缓存内容,因为方法缓存列表内没有找到对应方法实现,所以这一步应该不可能缓存,不过当前函数顶部有一段系统注释 "cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)"讲述了为何需要写上这段代码,因为其他地方会用到缓存。 560 | 3.Runtime加上锁,并且检查Class是否合法。 561 | 4.如果类未加载,则在此刻加载,这里通常是处理懒加载的。 562 | 5.如果需要初始化,但是当前Class并没有初始化,此时需要进行初始化操作。 563 | 6. 再次去缓存中再次查找一遍,如果没有找到则会通过SEL去方法列表查找,并且找到以后存入缓存中。 564 | 7.如果当前类还是找不到方法实现,则会去父类缓存中查找,如果缓存中命中,但是当前类没有该方法,则会将命中的方法实现存入当前类的缓存中,如果父类的缓存中没有,就会去父类的方法列表中查找,如果找到同样存入子类也就是当前类的缓存中。 565 | 8.最后,如果还是没有找到方法,则会执行消息转发流程。 566 | 567 | 568 | 咱们接着来看上面去方法列表查找方法实现的入口代码如下: 569 | ``` 570 | // Try this class's method lists. 571 | { 572 | Method meth = getMethodNoSuper_nolock(cls, sel); 573 | if (meth) { 574 | log_and_fill_cache(cls, meth->imp, sel, inst, cls); 575 | imp = meth->imp; 576 | goto done; 577 | } 578 | } 579 | ``` 580 | 从以上代码我们可以了解到方法列表获取方法是通过'getMethodNoSuper_nolock(cls, sel);'函数来实现的。咱们接下来看一下内部实现代码: 581 | ``` 582 | static method_t * 583 | getMethodNoSuper_nolock(Class cls, SEL sel) 584 | { 585 | runtimeLock.assertLocked(); 586 | 587 | assert(cls->isRealized()); 588 | // fixme nil cls? 589 | // fixme nil sel? 590 | //遍历方法,判断方法是否是自己选择的那个 591 | for (auto mlists = cls->data()->methods.beginLists(), 592 | end = cls->data()->methods.endLists(); 593 | mlists != end; 594 | ++mlists) 595 | { 596 | method_t *m = search_method_list(*mlists, sel); 597 | if (m) return m; 598 | } 599 | 600 | return nil; 601 | } 602 | ``` 603 | 总结流程如下: 604 | 1.先通过runtime上锁。 605 | 2.如果没有没有初始化类,则直接断言崩溃。 606 | 3.for循环取出方法method结构体,判断是否是sel对应的method。如果是,则直接范围。 607 | 4.如果没有找到则直接返回nil。 608 | 609 | 接下来我们看一下'search_method_list(*mlists, sel);'函数内部是如何通过sel找到method: 610 | ``` 611 | static method_t *search_method_list(const method_list_t *mlist, SEL sel) 612 | { 613 | int methodListIsFixedUp = mlist->isFixedUp();是否排序好的 614 | int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);//获取方法列表size 615 | 616 | if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) { 617 | return findMethodInSortedMethodList(sel, mlist); 618 | } else { 619 | // Linear search of unsorted method list 620 | for (auto& meth : *mlist) { 621 | if (meth.name == sel) return &meth; 622 | } 623 | } 624 | ``` 625 | 626 | 627 | 628 | 总结流程如下: 629 | 1.先通过isFixedUp()函数判断是否方法列表是已经编译阶段就排序好的。 630 | 2.获取方法列表的size大小 631 | 3.通过__builtin_expect指令,优化执行逻辑,CPU是流水线方式实现的,即取指->执行->输出结果,第一条指令在执行的过程中,第二条指令可能已经完成了取指。但是如果第一条执行的执行结果使得程序发生了跳转,取到的第二条指令相当于做了无用功。因为此时要执行跳转位置的指令。使用__builtin_expect指令,如LIKELY宏,是告诉CPU X的值更大概率是1。在执行下一次取指操作的时候,执行结果是1成立的情况下的指令。这样减少了CPU做无用功的次数,提高执行效率。这里是如果方法列表已经排序好的,并且方法列表size大小已经获取到,就会去已存在的方法列表中通过'findMethodInSortedMethodList(sel, mlist);'函数二分查找更加快速查找到对应method结构体。 632 | 4.如果方法列表没排序好,并且方法列表size大小无法获取到,则直接遍历方法列表。判断sel是否等于name,找到对应的method结构体。 633 | 634 | 通过上面我们知道已经排好序的方法列表会通过二分查找的形式进行method结构体寻找,我们进入'findMethodInSortedMethodList(sel, mlist);'函数内部看一下实现过程。 635 | 636 | ``` 637 | static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list) 638 | { 639 | assert(list); 640 | //拿到第一个method结构体 641 | const method_t * const first = &list->first; 642 | const method_t *base = first; 643 | const method_t *probe;//折半位置的method结构体 644 | uintptr_t keyValue = (uintptr_t)key; 645 | uint32_t count; 646 | 647 | for (count = list->count; count != 0; count >>= 1) { 648 | probe = base + (count >> 1); 649 | 650 | uintptr_t probeValue = (uintptr_t)probe->name; 651 | 652 | if (keyValue == probeValue) { 653 | // `probe` is a match. 654 | // Rewind looking for the *first* occurrence of this value. 655 | // This is required for correct category overrides. 656 | while (probe > first && keyValue == (uintptr_t)probe[-1].name) { 657 | probe--; 658 | } 659 | return (method_t *)probe; 660 | } 661 | 662 | if (keyValue > probeValue) { 663 | base = probe + 1; 664 | count--; 665 | } 666 | } 667 | 668 | return nil; 669 | } 670 | ``` 671 | 672 | 总结流程如下: 673 | 1.首先断言判断方法列表是否为空,如果为空,则直接崩溃。 674 | 2.通过递减的形式进行for循环,count>>=1,相当于每循环一次除2。 675 | 3.probe 先取一半的位置的mehtod的name判断是否为需要寻找的method结构体。 676 | 4.如果是需要找的结构体,还得考虑分类添加的跟本类同样方法时的情况。 677 | 5.如果分类也添加了跟本类同样的方法,那么分类的方法一定是排在本类方法前面的,而且这里是已经排序好的方法,那么相同的方法肯定会被排序到相邻位置。所以通过while循环判断数组上一个位置method的name是否也是跟keyValue相同,如果相同则是分类添加的方法,而且开发者可能同时添加了多个跟本类一样的方法在不同的分类,这里只需要遍历找到最前面那个匹配的method结构体,就是我们需要寻找的method,再直接返回即可。 678 | 6.当keyValue位置大于一半的时候,则直接把base = probe +1 ,通过后一半数据进行比较,最终找出匹配的method结构体。 679 | 680 | 通过上面代码,我们了解到方法列表在编译器有可能优化以后,会对方法列表进行排序,那么方法列表是通过什么方式来进行排序的呢?咱们接着来探索。 681 | 682 | 683 | ``` 684 | static void 685 | prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount, 686 | bool baseMethods, bool methodsFromBundle) 687 | { 688 | 689 | for (int i = 0; i < addedCount; i++) { 690 | method_list_t *mlist = addedLists[i]; 691 | assert(mlist); 692 | 693 | // 如果没有排序 694 | if (!mlist->isFixedUp()) { 695 | fixupMethodList(mlist, methodsFromBundle, true/*sort*/); 696 | } 697 | } 698 | } 699 | ``` 700 | 接着咱们进入'fixupMethodList(mlist, methodsFromBundle, true/*sort*/);'函数内部查看实现过程: 701 | 702 | ``` 703 | static void 704 | fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort) 705 | { 706 | //为方便查看,仅保留关键代码 707 | for (auto& meth : *mlist) { 708 | const char *name = sel_cname(meth.name); 709 | 710 | SEL sel = sel_registerNameNoLock(name, bundleCopy); 711 | meth.name = sel; 712 | 713 | if (ignoreSelector(sel)) { 714 | meth.imp = (IMP)&_objc_ignored_method; 715 | } 716 | } 717 | 718 | //根据地址排序 719 | if (sort) { 720 | method_t::SortBySELAddress sorter; 721 | std::stable_sort(mlist->begin(), mlist->end(), sorter); 722 | } 723 | 724 | //标记方法列表已经被统一排序 725 | mlist->setFixedUp(); 726 | } 727 | ``` 728 | 经过这里,我们大概已经知道是通过方法名地址来排序的,我们进去看一下排序的实现: 729 | ``` 730 | struct method_t { 731 | SEL name; 732 | const char *types; 733 | IMP imp; 734 | 735 | struct SortBySELAddress : 736 | public std::binary_function 738 | { 739 | bool operator() (const method_t& lhs, 740 | const method_t& rhs) 741 | { return lhs.name < rhs.name; } 742 | }; 743 | }; 744 | ``` 745 | 通过这里我们看到,其实地址比较的方法是放在method结构体内部实现的。可以清晰的看到,他们是通过地址比较来进行的排序。 746 | 747 | 748 | 749 | ### Runtime在项目中的运用 750 | 751 | 1.消息转发 当需要动态指定某一个对象来实现某一个方法 752 | 2.方法交换。用户埋点的时候,写一个工具实现方法交换 添加 `VC` 分类,`load` 中把系统方法 替换。 753 | 3.字典转模型 `YYModel`。 754 | 4.`KVO` 实现。 755 | 5.处理崩溃(NSDictionary, NSMutableDictionary, NSArray, NSMutableArray 的处理) 在开发过程中, 有时候会出现 set object for key 的时候 object 为 Nil 或者 Key 为 Nil, 又或 者初始化array, dic的时候由于数据个数与指定的长度不一致造成崩溃。此时利用runtime 对异常情况进行捕捉,提前 return 或者抛弃多余的长度。 756 | 6.通过写这些对象的分类,在 `load` 中交换方法 处理按钮的重复点击方法交换。 757 | 758 | 759 | 760 | ### Class继承链关系 761 | 762 | > 假如现在我们有一个继承链 A->B->C->NSObject, A * a = [[A alloc]init]; 那么这里实例对象 'a' 的父类是谁? 类对象A的父类又是谁? 763 | 764 | 为了验证上面的结果,我们建立了一个Model类继承自Father类,Father类继承自NSObject,我们打印看看他们的地址 765 | ``` 766 | Model *a = [[Model alloc]init]; 767 | NSLog(@"小a的父类:%@,地址:%p",[a superclass],[a superclass]); 768 | NSLog(@"Model的父类:%@,地址:%p",[Model superclass],[Model superclass]); 769 | ``` 770 | 打印结果如下: 771 | 772 | ``` 773 | 2020-11-20 10:31:41.655137+0800 AAATest[2007:90242] 小a的父类:Father,地址:0x101e27570 774 | 2020-11-20 10:31:41.655308+0800 AAATest[2007:90242] Model的父类:Father,地址:0x101e27570 775 | ``` 776 | 通过结果我们可以知道看到a实例对象的父类和Model类对象的父类都是Father 777 | 778 | > 那么我们再次思考 a 的isa 指针是指向Model的,那么Model的isa指针指向的是Model的元类,那么Model的元类的父类又是谁呢? 779 | 780 | 接下来我们为了充分的验证,我们分别打印Model的元类,Model的元类的父类,Father的元类,Father的元类的父类,NSObject的元类,NSObject元类的元类,NSObject的元类的父类 781 | ``` 782 | NSLog(@"Model的地址:%p",[Model class]); 783 | Class clazz = objc_getMetaClass(NSStringFromClass([Model class]).UTF8String); 784 | NSLog(@"Model的元类:%@,地址:%p",clazz,clazz); 785 | NSLog(@"Model的元类的父类:%p",objc_getMetaClass(NSStringFromClass([clazz superclass]).UTF8String)); 786 | Class fatherClazz = objc_getMetaClass(NSStringFromClass([Father class]).UTF8String); 787 | NSLog(@"Father的地址:%p,Father的元类地址:%p",[Father class],fatherClazz); 788 | NSLog(@"Father的元类的父类的地址:%p",[fatherClazz superclass]); 789 | Class objctClass = objc_getMetaClass(NSStringFromClass([NSObject class]).UTF8String); 790 | NSLog(@"NSObject的元类地址:%p,NSobject的地址:%p",objctClass,[NSObject class]); 791 | NSLog(@"NSObject的元类的元类地址:%p",objc_getMetaClass(NSStringFromClass([objctClass class]).UTF8String)); 792 | NSLog(@"NSObject的元类的父类地址:%p",[objctClass superclass]); 793 | NSLog(@"NSObject的父类地址:%p",[NSObject superclass]); 794 | ``` 795 | 打印结果如下: 796 | ``` 797 | 2020-11-20 11:19:18.887960+0800 AAATest[2691:147165] Model的地址:0x105eae658 798 | 2020-11-20 11:19:18.888548+0800 AAATest[2691:147165] Model的元类:Model,地址:0x105eae630 799 | 2020-11-20 11:19:18.888779+0800 AAATest[2691:147165] Model的元类的父类:0x105eae540 800 | 2020-11-20 11:19:18.889115+0800 AAATest[2691:147165] Father的地址:0x105eae568,Father的元类地址:0x105eae540 801 | 2020-11-20 11:19:18.889451+0800 AAATest[2691:147165] Father的元类的父类的地址:0x10678b1d8 802 | 2020-11-20 11:19:18.890022+0800 AAATest[2691:147165] NSObject的元类地址:0x10678b1d8,NSobject的地址:0x10678b200 803 | 2020-11-20 11:19:18.890173+0800 AAATest[2691:147165] NSObject的元类的元类地址:0x10678b1d8 804 | 2020-11-20 11:19:18.890776+0800 AAATest[2691:147165] NSObject的元类的父类地址:0x10678b200 805 | 2020-11-20 11:46:56.007279+0800 AAATest[4208:196636] NSObject的父类地址:0x0 806 | ``` 807 | 通过打印结果,我们发现Model的元类是一个单独的内存地址,Model的元类的父类就是Father的元类,而因为Father是继承自NSObject的,Father的元类的父类就是NSObject的元类,而NSObject的元类的元类是指向NSObject元类的,也就是说NSObject的元类就是NSobject元类本身,而NSObject元类的父类就是NSObject本身, NSObject的父类是nil。 808 | 809 | 具体的继承关系图如下: 810 | 811 |
812 | 813 |
814 | 815 | 816 | 该图咱们在讲解class类结构的时候已经分析过。 817 | 818 | 819 | 820 | ### 以上代码打印结果是什么 821 | 822 | 有以下代码,它们执行以后打印结果是怎么样的?为什么? 823 | 824 | ``` 825 | #import "ViewController.h" 826 | 827 | @interface NSObject (objc) 828 | 829 | + (void)funTest; 830 | 831 | @end 832 | @implementation NSObject (objc) 833 | 834 | -(void)funTest { 835 | NSLog(@"this is function test"); 836 | } 837 | 838 | @end 839 | @interface ViewController () 840 | 841 | @end 842 | 843 | @implementation ViewController 844 | - (void)viewDidLoad { 845 | [super viewDidLoad]; 846 | [NSObject funTest]; 847 | NSObject *objc = [NSObject new]; 848 | [objc funTest]; 849 | } 850 | 851 | @end 852 | ``` 853 | 以上代码运行结果如下: 854 | ``` 855 | 2020-12-07 17:26:26.718361+0800 AAATest[5920:304985] this is function test 856 | 2020-12-07 17:26:26.718559+0800 AAATest[5920:304985] this is function test 857 | ``` 858 | 这里我们观察到NSObject的 ’funTest‘ 类方法并没有方法的实现,为何最终调用了实例方法呢?咱们来一步步分析。 859 | 860 | > NSObject的类方法是存储在NSObject的元类中的 861 | > NSObject的元类中并没有 ’funTest‘ 类方法 862 | > NSObject的元类中找不到类方法实现的时候,就需要去NSObject元类的父类中寻找 863 | > NSObject的元类的父类指向的是NSObject类对象 864 | > 而咱们 objc 这个NSObject的实例对象的实例方法也是在NSObject类对象中寻找的 865 | > 在咱们NSObject类对象中方法是存放在专门的hash散列表中,通过sel作为key,可以获取到对象的imp方法函数指针 866 | > 而这里咱们的类方法和实例方法的方法名是一样的,所以找到的imp就是实例方法的imp 867 | 868 | 因此,上面的代码类方法和实例方法打印结果是一样的 869 | 870 | 这里我们猜想一下,假设上面是一个继承自NSObject的Model类,添加同样的方法和实现,最后结果又是怎么样的呢? 871 | 872 | 我们再次来整理一下整个查找过程如下: 873 | > Model中类方法是存储在Model的元类中的 874 | > Model的元类中并没有 ’funTest‘ 类方法的实现imp 875 | > Model的元类找不到的话就会去Model的元类的父类中寻找 876 | > Model的元类的父类指向的是NSObject的元类(根元类) 877 | > 而NSObject根元类中也没有 'funTest' 类方法 878 | > 这个时候就需要去NSObject根元类的父类中寻找 879 | > 而NSObject的根元类的父类又是指向的NSObject类对象本身 880 | > 而NSObject类对象中并没有 'funTest' 类方法的实现,所以程序会直接崩溃 881 | 882 | 因此,如果是Model中同样代码,则会造成整个程序直接崩溃,都无法执行到实例方法的执行。 883 | 884 | ### RunLoop和屏幕点击事件传递链以及响应链关系 885 | 886 | 887 | 在讲解RunLoop和屏幕点击事件传递链以及响应链关系之前,我们先来看一下下面这张图: 888 | 889 |
890 | 891 |
892 | 893 | 通过上图,我们可以总结屏幕点击触发流程如下: 894 | 895 | > 1.手指点击屏幕,传感器获取到信号,在内部由 `IOKit.framework` 框架将点击电信号封装成IOHIDEvent事件 896 | > 2.`springboard` 桌面操作系统接收事件,并通过 `mach port` 端口转发(PIC进程间通信)将 `IOHIDEvent` 事件传递给主线程处理 897 | > 3.主线程 RunLoop 此时注册的`source1`(专门处理系统级事件)回调被触发,然后`source1`通过内部将信息转发给了`source0`处理(专门处理应用内事件) 898 | > 4.然后事件被传递到UIApplication 899 | > 5.通过UIApplication触发`sendEvent`事件转发消息,将消息扔给UIWindow 900 | > 6.UIWindow通过调用 `hitTest:withEvent` 和 `pointInside:withEvent` 层层传递将事件传递给能够响应的控件 901 | > 7.系统判断找到的控件是否能够响应该事件,如果能响应则走响应流程,如果不能响应则采用回溯方法,寻找能够响应该事件的控件 902 | > 8.如果在回溯过程中找到能响应该事件的控件则进行响应,如果事件回溯到`UIApplication`还是没能找到响应该事件的控件,则直接丢弃事件 903 | 904 | 905 | 在寻找能够响应该事件的控件过程中有两个方法被频繁的调用了: 906 | 907 | ``` 908 | // 先判断点是否在View内部,然后遍历subViews 909 | - (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; 910 | //判断点是否在这个View内部 911 | - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; 912 | 913 | ``` 914 | 具体搜寻过程就是系统通过遍历屏幕范围内的view(注:如果遇到view内部还有subview,则会采用倒序遍历方法,加快遍历的速度)。然后调用 `hitTest:withEvent:` 判断屏幕点击事件是否在该view内部,实现流程如下: 915 | 916 | > 1.判断该层级是否能够响应事件,包括透明度是否大于0.01,是否用户交互事件为YES,是否隐藏了当前view 917 | > 2.判断该点是否在当前view范围内。具体是通过调用 `pointInside:withEvent:`来实现 918 | > 3.如果存在则遍历子view,直到找到被点击的view,如果该view能够响应事件则由该view执行响应过程,如果不能响应则调用 `nextResponder` 方法将事件传递给下一个响应者来响应 919 | 920 | 921 | 922 | 923 | -------------------------------------------------------------------------------- /iOS面试题难点集锦/iOS面试题难点集锦(三).md: -------------------------------------------------------------------------------- 1 | ## iOS面试题难点集锦(二)---参考答案 2 | 3 | 引言:通过上篇文章[iOS面试题难点集锦(二)](https://github.com/LiuFuBo/iOSInterviewQuestions/blob/master/iOS面试题难点集锦/iOS面试题难点集锦(二).md),我们了解了一些散碎的iOS知识点,接下来的篇幅我们会着重讲解一些体系内文章。 4 | 5 | # 索引 6 | 7 | 1. [HTTP工作原理图](#HTTP工作原理图 ) 8 | 2. [HTTP/0.9版本篇](#HTTP/0.9版本篇 ) 9 | 3. [HTTP/1.0版本篇](#HTTP/1.0版本篇 ) 10 | 4. [HTTP/1.1版本篇](#HTTP/1.1版本篇 ) 11 | 5. [HTTP/2.0版本篇](#HTTP/2.0版本篇 ) 12 | 6. [HTTP/3.0版本篇](#HTTP/3.0版本篇 ) 13 | 6. [HTTPS版本篇](#HTTPS版本篇 ) 14 | 7. [SSL/TLS协议介绍](#SSL/TLS协议介绍 ) 15 | 16 | 17 | 18 | 19 | ### HTTP发展历程? 20 | 21 | HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(www.world wide web)服务器传输超文本到本地浏览器的传输协议。HTTP是基于TCP/IP的应用层协议。不涉及到数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用80端口。 22 | 23 | 24 | 25 | #### HTTP工作原理图 26 | 27 | ![](http://brandonliu.pub/icon_blog_wanweiwang_http.jpg) 28 | 29 | 30 | 31 | #### HTTP/0.9版本篇 32 | 33 | 34 | 35 | #### 要点 36 | 37 | * 客户端向服务器请求网页,服务器只能回应HTML格式字符串,不能回应别的格式。 38 | * 只有GET请求方式。 39 | * 服务器发送完毕,就关闭TCP连接。 40 | * 任何格式内容都可以发送—(包括文字、图像、视频、二进制文件)。 41 | 42 | 43 | #### 缺点 44 | 45 | * 每个TCP连接只能发送一个请求;发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。 46 | * TCP连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢。 47 | * 网页加载的外部资源越多,性能就越差。 48 | * 只有GET这一种请求方式。 49 | 50 | 51 | #### HTTP/1.0版本篇 52 | 53 | 54 | 55 | #### 要点 56 | 57 | * 除了GET请求格式,还引入了POST和HEAD格式,丰富了浏览器与服务器交互方式。 58 | * HTTP请求和回应格式发生改变,除了数据部分,每次通信都包括头部分,用来描述数据。 59 | * 新增功能包括:状态码、多字符集支持、多部分发送、权限、缓存、内容编码等。 60 | 61 | 62 | ``` 63 | 64 | HTTP/1.0请求的例子: 65 | 66 | GET/HTTP/1.0 67 | USER-AGENT:MOZILLA/5.0(MACINTOSH;INTEL MAC OS X 10_10_5 ) 68 | ACCEPT:*/* 69 | 第一行为请求命令,必须在尾部添加协议版本(HTTP/1.0) 70 | 后面为多行头信息,描述客户端情况 71 | 72 | ``` 73 | 74 | 75 | ``` 76 | 77 | HTTP/1.0回应的例子: 78 | HTTP/1.0 200 OK /*协议版本+状态码+状态描述*/ 79 | CONTENT-TYPE: TEXT/PLAIN 80 | CONTENT-LENGTH: 137582 81 | EXPIRES: THU, 05 DEC 1997 16:00:00 GMT 82 | LAST-MODIFIED: WED, 5 AUGUST 1996 15:55:28 GMT 83 | SERVER: APACHE 0.84 84 | 85 | 86 | HELLO WORLD 87 | 88 | CONTENT-TYPE:字符编码,HTTP 1.0规定 头部必须是ASCII码,后面可以是任何格式, 89 | 因此,服务器回应时,CONTENT-TYPE的作用是:告诉客户端,数据是什么格式 90 | 91 | ``` 92 | 93 | 94 | 95 | #### 缺点 96 | 97 | * 每个TCP连接只能发送一个请求;发送数据完毕,连接关闭,如果还要请求其他资源,必须再新建一个连接。 98 | * TCP新建成功很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢。(为了解决这个问题,有些浏览器在请求时,用了一个非标准的Connection字段,即:CONNECTION:KEEP-ALIVE)请求服务器不要关闭TCP连接,以便其他请求复用,服务器同样回复这个字段;以实现TCP的复用,直到客户端或服务器主动关闭连接,但这不是标准字段,不同实现的行为可能导致不一致,因此不是根本的解决办法。 99 | * 网页加载外部资源越多,性能越差。 100 | 101 | 102 | #### HTTP/1.1版本篇 103 | 104 | 105 | 106 | ##### 要点 107 | 108 | 109 | * 引入持久连接 110 | 111 | ``` 112 | 113 | 1.持久连接定义:即TCP连接默认不关闭,可以被多个请求复用,不用声明。 114 | 2.引入方式:CONNECTION:KEEP-ALINE 115 | 3.关闭连接:客户端和服务器发现对方有单时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时明确要求服务器关闭TCP连接。CONNECTION:CLOSE 116 | 4.对于同一个域名,大多数浏览器允许同时建立6个持久连接。 117 | 118 | ``` 119 | 120 | * 引入管道机制 121 | 122 | 123 | ``` 124 | 1.管道机制:在同一个TCP连接里面,客户端可以同时发送多个请求。(提高HTTP协议的效率) 125 | 例:客户端需要请求两个资源,HTTP 1.0是在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发送B请求;管道机制是允许浏览器同时发生A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。 126 | 2.一个TCP连接可以传送多个回应,势必要有机制区分数据包属于哪个回应;则需要Content-length字段,它的作用就是声明本次回应数据长度。(服务器发送回应之前,必须知道回应数据长度) 127 | 128 | ``` 129 | 130 | * 采用分块传输编码 131 | 132 | ``` 133 | 1.使用Content-length字段前提是服务器发送回应之前,必须知道回应数据长度。但对于一些耗时的动态操作,意味着,服务器要等待所有操作完成,才能发送数据,显然效率不高。 134 | 2.更好的处理方式是:服务器每产生一块数据,就发送一块,采用"流模式 (stream)"取代"缓存模式(buffer)",因此,HTTP/1.1版本规定可以不使用Content-length字段,而使用"分块传输编码",只要请求或回应的头信息有Transfer-Encoding字段,就表明回应的数据将有数量未定的数据块组成,最后是一个大小为0的块,就标识本次回应的数据发送完毕。 135 | 136 | ``` 137 | 138 | 139 | * 新增 PUT、PATCH、HEAD、OPTIONS、DELECT.客户端的头信息增加HOST字段,用来自定指定服务器的域名。HOST:WWW.EXAMPLE.COM 有了HOST字段,就可以将请求发往同一台服务器的不同网站,为虚拟主机新起打下了基础。 140 | 141 | 142 | #### 缺点 143 | 144 | 145 | * HTTP层面队头阻塞: 虽然复用TCP连接,但是在同一个TCP连接里面,所有的数据通信都是按照次序进行的。服务器只有处理完一个回应,才能进行下一个回应。 146 | 147 | 148 | ``` 149 | 解决方案: 150 | 1.减少HTTP请求数。 151 | 2.同事多开持久连接。 152 | 153 | ``` 154 | 155 | #### HTTP/2.0版本篇 156 | 157 | 158 | 159 | #### 要点 160 | 161 | 162 | * 采用二进制协议 163 | 164 | 165 | ``` 166 | 1.HTTP/1.1的头信息是文本(ASCII编码), 数据体可以是文本(解析非常麻烦),也可以是二进制。 167 | 2.HTTP/2.0则是一个彻底的二进制协议,头信息和数据体都是二进制,统称为"帧(frame):头信息帧、数据帧"。二进制协议有一个好处是可以定义额外的帧,方便解析。 168 | 169 | ``` 170 | 171 | * 多路复用(双向,实时的通信) 172 | 173 | 174 | ``` 175 | HTTP/2.0复用TCP连接,在一个连接里,客户端和服务端都可以同时发送多个请求或回应,而不用按照顺序一一对应,这样就避免"队头阻塞"。 176 | 例:在一个TCP连接里面,服务器同时受到A请求和B请求,先回应A请求,结果发现处理过程非常耗时,于是就发送A请求已经处理好的部分,接着回应B请求,完成后,才发送B请求剩下的部分。 177 | 178 | ``` 179 | 180 | * 数据流 181 | 182 | 183 | ``` 184 | 1.HTTP/2.0数据流是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此必须要对数据包标记,指出它属于哪个回应。 185 | 2.HTTP/2.0将每一个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据ID,用于区分它数据哪一个数据流;另外规定:客户端发出的数据流,ID一律为奇数,服务器发送的,ID为偶数。 186 | 3.数据流发送一半时,客户端和服务器都可以发送信号,取消这个数据流。即HTTP/2.0可以取消某一个请求,同事保证TCP连接还开着,可以被其他请求使用。客户端还可以指定数据流的优先级,优先级越高,服务器就越早回应。 187 | 188 | ```` 189 | 190 | 191 | * 头信息压缩 192 | 193 | 194 | ``` 195 | 1.HTTP/2.0以前的版本协议不带有状态,每次请求都必须附带上所有信息。所以,请求的很多字段都是重复的,比如Cookie和User Agent一模一样,每次请求都必须附带,这很浪费宽带,也影响速度。 196 | 2.HTPP/2.0优化了这一点,引入了投信息压缩机制,一方面头信息使用gzip或者compress压缩后再发送,另一方面,客户端和服务端同事维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段,只发送索引号,提高速度。 197 | 198 | ``` 199 | 200 | * 服务器推送 201 | 202 | 203 | ``` 204 | 1.HTTP/2.0允许服务器未经允许,主动向客户端发送资源。 205 | 例:客户端请求一个网页,这个网页里面包含静态资源。正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发送静态资源请求;其实服务器可以预期到客户端请求网页后,很可能再请求静态资源,所以主动把这些静态资源随网页一起发送给客户端。 206 | 207 | ```` 208 | 209 | 210 | #### 缺点 211 | 212 | 213 | * TCP层面发生队头阻塞:将数组分为帧来传输,当一个请求出现了一个数据包丢失,TCP协议会发生阻塞整个TCP连接就会暂停,然后进行数据的重传。这样就造成了队头阻塞。 214 | 215 | 216 | 217 | 218 | #### HTTP/3.0版本篇 219 | 220 | HTTP2.0是2015年推出的,还是比较年轻的,其重要的二进制分帧协议、多路复用、头部压缩、服务端推送等重要优化使HTTP协议真正上了一个新台阶。 221 | ![](http://brandonliu.pub/icon_blog_http2.0_function.png) 222 | 像谷歌这种重要的公司并没有满足于此,而且想继续提升HTTP的性能,花最少的时间和资源获取极致体验。 223 | 那么肯定要问HTTP2.0虽然性能已经不错了,还有什么不足吗? 224 | * 建立连接时间长(本质上是TCP的问题) 225 | * 队头阻塞问题 226 | * ...... 227 | 熟悉HTTP2.0协议的同学应该知道,这些缺点基本都是由于TCP协议引起的,水能载舟亦能覆舟,其实TCP也很无辜呀! 228 | ![](http://brandonliu.pub/icon_blog_http2.0_three_hand.png) 229 | 在我们眼里,TCP是面向连接、可靠的传输层协议,当前几乎所有重要的协议和应用都是基于TCP来实现的。 230 | 网络环境的改变速度很快,但是TCP协议相对缓慢,正是这种矛盾促使谷歌做出了一个看似出乎意料的决定-基于UDP来开发新一代HTTP协议。 231 | 232 | 233 | 234 | #### 谷歌为什么选择UDP 235 | 上文提到,谷歌选择UDP是看似出乎意料的,仔细想一想其实很有道理。 236 | 我们单纯地看看TCP协议的不足和UDP的一些优点: 237 | * 基于TCP开发的设备和协议非常多,兼容困难 238 | * TCP协议栈是Linux内部的重要部分,修改和升级成本很大 239 | * UDP本身是无连接的、没有建链和拆链成本 240 | * UDP的数据包无队头阻塞问题 241 | * UDP改造成本小 242 | 从上面的对比可以知道,谷歌要想从TCP上进行改造升级绝非易事,但是UDP虽然没有TCP为了保证可靠连接而引发的问题,但是UDP本身不可靠,又不能直接用。 243 | ![](http://brandonliu.pub/icon_blog_udp_gaizao.jpeg) 244 | 综合而知,谷歌决定在UDP基础上改造一个具备TCP协议优点的新协议也就顺理成章了,这个新协议就是QUIC协议。 245 | 246 | 247 | #### QUIC协议和HTTP3.0 248 | QUIC其实是Quick UDP Internet Connections的缩写,直译为快速UDP互联网连接。 249 | * QUIC协议最初由Google的Jim Roskind设计,实施并于2012年部署,在2013年随着实验的扩大而公开宣布,并向IETF进行了描述。 250 | * QUIC提高了当前正在使用TCP的面向连接的Web应用程序的性能。它在两个端点之间使用用户数据报协议(UDP)建立多个复用连接来实现此目的。 251 | * QUIC的次要目标包括减少连接和传输延迟,在每个方向进行带宽估计以避免拥塞。它还将拥塞控制算法移动到用户空间,而不是内核空间,此外使用前向纠错(FEC)进行扩展,以在出现错误时进一步提高性能。 252 | HTTP3.0又称为HTTP Over QUIC,其弃用TCP协议,改为使用基于UDP协议的QUIC协议来实现。 253 | ![](http://brandonliu.pub/icon_blog_upd_quic.jpeg) 254 | 255 | 256 | #### QUIC协议详解 257 | HTTP3.0既然选择了QUIC协议,也就意味着HTTP3.0基本继承了HTTP2.0的强大功能,并且进一步解决了HTTP2.0存在的一些问题,同时必然引入了新的问题。 258 | ![](http://brandonliu.pub/icon_blog_http2.0_question.png) 259 | QUIC协议必须要实现HTTP2.0在TCP协议上的重要功能,同时解决遗留问题,我们来看看QUIC是如何实现的。 260 | 261 | 262 | 263 | #### 队头阻塞问题 264 | 队头阻塞 Head-of-line blocking(缩写为HOL blocking)是计算机网络中是一种性能受限的现象,通俗来说就是:一个数据包影响了一堆数据包,它不来大家都走不了。 265 | 队头阻塞问题可能存在于HTTP层和TCP层,在HTTP1.x时两个层次都存在该问题。 266 | ![](http://brandonliu.pub/icon_blog_duitou_zusai.png) 267 | 268 | HTTP2.0协议的多路复用机制解决了HTTP层的队头阻塞问题,但是在TCP层仍然存在队头阻塞问题。 269 | TCP协议在收到数据包之后,这部分数据可能是乱序到达的,但是TCP必须将所有数据收集排序整合后给上层使用,如果其中某个包丢失了,就必须等待重传,从而出现某个丢包数据阻塞整个连接的数据使用。 270 | QUIC协议是基于UDP协议实现的,在一条链接上可以有多个流,流与流之间是互不影响的,当一个流出现丢包影响范围非常小,从而解决队头阻塞问题。 271 | 272 | 273 | 274 | #### 0RTT 建链 275 | 衡量网络建链的常用指标是RTT Round-Trip Time,也就是数据包一来一回的时间消耗。 276 | ![](http://brandonliu.pub/icon_blog_round_rt.jpeg) 277 | RTT包括三部分:往返传播时延、网络设备内排队时延、应用程序数据处理时延。 278 | ![](http://brandonliu.pub/icon_blog_Rt_time.jpeg) 279 | 一般来说HTTPS协议要建立完整链接包括:TCP握手和TLS握手,总计需要至少2-3个RTT,普通的HTTP协议也需要至少1个RTT才可以完成握手。 280 | 然而,QUIC协议可以实现在第一个包就可以包含有效的应用数据,从而实现0RTT,但这也是有条件的。 281 | ![](http://brandonliu.pub/icon_blog_woshou_liucheng.gif) 282 | 简单来说,基于TCP协议和TLS协议的HTTP2.0在真正发送数据包之前需要花费一些时间来完成握手和加密协商,完成之后才可以真正传输业务数据。 283 | 但是QUIC则第一个数据包就可以发业务数据,从而在连接延时有很大优势,可以节约数百毫秒的时间。 284 | ![](http://brandonliu.pub/icon_blog_quic_jiami_pre.png) 285 | QUIC的0RTT也是需要条件的,对于第一次交互的客户端和服务端0RTT也是做不到的,毕竟双方完全陌生。 286 | 因此,QUIC协议可以分为首次连接和非首次连接,两种情况进行讨论。 287 | 288 | 289 | 290 | #### 首次连接和非首次连接 291 | 使用QUIC协议的客户端和服务端要使用1RTT进行密钥交换,使用的交换算法是DH(Diffie-Hellman)迪菲-赫尔曼算法。 292 | DH算法开辟了密钥交换的新思路,在之前的文章中提到的RSA算法也是基于这种思想实现的,但是DH算法和RSA的密钥交换不完全一样,感兴趣的读者可以看看DH算法的数学原理。 293 | 294 | 295 | 296 | #### 首次连接 297 | 简单来说一下,首次连接时客户端和服务端的密钥协商和数据传输过程,其中涉及了DH算法的基本过程: 298 | * 客户端对于首次连接的服务端先发送client hello请求。 299 | * 服务端生成一个素数p(除了1和他本身,不能被其他数整除)和一个整数g,同时生成一个随机数 (笔误-此处应该是Ks_pri)为私钥,然后计算出公钥 = mod p,服务端将,p,g三个元素打包称为config,后续发送给客户端。 300 | * 客户端随机生成一个自己的私钥,再从config中读取g和p,计算客户端公钥 = mod p。 301 | * 客户端使用自己的私钥和服务端发来的config中读取的服务端公钥,生成后续数据加密用的密钥K = mod p。 302 | * 客户端使用密钥K加密业务数据,并追加自己的公钥,都传递给服务端。 303 | * 服务端根据自己的私钥和客户端公钥生成客户端加密用的密钥K = mod p。 304 | * 为了保证数据安全,上述生成的密钥K只会生成使用1次,后续服务端会按照相同的规则生成一套全新的公钥和私钥,并使用这组公私钥生成新的密钥M。 305 | * 服务端将新公钥和新密钥M加密的数据发给客户端,客户端根据新的服务端公钥和自己原来的私钥计算出本次的密钥M,进行解密。 306 | * 之后的客户端和服务端数据交互都使用密钥M来完成,密钥K只使用1次。 307 | ![](http://brandonliu.pub/icon_blog_jiami_guocheng.png) 308 | 309 | #### 非首次连接 310 | 前面提到客户端和服务端首次连接时服务端传递了config包,里面包含了服务端公钥和两个随机数,客户端会将config存储下来,后续再连接时可以直接使用,从而跳过这个1RTT,实现0RTT的业务数据交互。 311 | 客户端保存config是有时间期限的,在config失效之后仍然需要进行首次连接时的密钥交换。 312 | 313 | 314 | 315 | #### 前向安全问题 316 | 通俗来说,前向安全指的是密钥泄漏也不会让之前加密的数据被泄漏,影响的只有当前,对之前的数据无影响。 317 | 前面提到QUIC协议首次连接时先后生成了两个加密密钥,由于config被客户端存储了,如果期间服务端私钥泄漏,那么可以根据K = mod p计算出密钥K。 前面提到QUIC协议首次连接时先后生成了两个加密密钥,由于config被客户端存储了,如果期间服务端私钥泄漏,那么可以根据K = mod p计算出密钥K。 318 | ![](http://brandonliu.pub/icon_blog_xiangqian_jiucuo.png) 319 | 320 | #### 前向纠错 321 | 听这段描述就是做校验的,看看QUIC协议是如何实现的: 322 | * QUIC每发送一组数据就对这组数据进行异或运算,并将结果作为一个FEC包发送出去,接收方收到这一组数据后根据数据包和FEC包即可进行校验和纠错。 323 | 324 | 325 | 326 | #### 连接迁移 327 | 网络切换几乎无时无刻不在发生。 328 | TCP协议使用五元组来表示一条唯一的连接,当我们从4G环境切换到wifi环境时,手机的IP地址就会发生变化,这时必须创建新的TCP连接才能继续传输数据。 329 | QUIC协议基于UDP实现摒弃了五元组的概念,使用64位的随机数作为连接的ID,并使用该ID表示连接。 330 | 基于QUIC协议之下,我们在日常wifi和4G切换时,或者不同基站之间切换都不会重连,从而提高业务层的体验。 331 | ![](http://brandonliu.pub/icon_blog_jiyu_udp_quic.png) 332 | 333 | 334 | 335 | #### QUIC的应用和前景 336 | 通过前面的一些介绍我们看出来QUIC协议虽然是基于UDP来实现的,但是它将TCP的重要功能都进行了实现和优化,否则使用者是不会买账的。 337 | QUIC协议的核心思想是将TCP协议在内核实现的诸如可靠传输、流量控制、拥塞控制等功能转移到用户态来实现,同时在加密传输方向的尝试也推动了TLS1.3的发展。 338 | 但是TCP协议的势力过于强大,很多网络设备甚至对于UDP数据包做了很多不友好的策略,进行拦截从而导致成功连接率下降。 339 | 主导者谷歌在自家产品做了很多尝试,国内腾讯公司也做了很多关于QUIC协议的尝试。 340 | 其中腾讯云对QUIC协议表现了很大的兴趣,并做了一些优化然后在一些重点产品中对连接迁移、QUIC成功率、弱网环境耗时等进行了实验,给出了来自生产环境的诸多宝贵数据。 341 | 简单看一组腾讯云在移动互联网场景下的不同丢包率下的请求耗时分布: 342 | ![](http://brandonliu.pub/icon_blog_diu_bao.png) 343 | ![](http://brandonliu.pub/icon_blog_diubao_table.png) 344 | 345 | 346 | 347 | #### HTTPS版本篇 348 | 349 | 350 | 351 | #### 要点 352 | 353 | 354 | * HTTPS 是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 355 | * HTTPS协议的主要作用是:建立一个信息安全通道,来确保数组的传输,确保网站的真实性。 356 | * HTTPS的SSL加密是在传输层实现的。 357 | 358 | 359 | 360 | #### 握手流程图: 361 | 362 | 363 | ![](http://brandonliu.pub/icon_blog_woshou_http.jpeg) 364 | 365 | 366 | 367 | #### 工作原理 368 | 369 | * 客户端-->服务器 发起请求 370 | 371 | ``` 372 | 1.客户端发起请求到服务器。主要参数是支持的协议版本,加密方法以及一个随机数n1。 373 | 374 | ``` 375 | 376 | * 服务器-->客户端 发送证书,客户端验证证书。 377 | 378 | ``` 379 | 1.服务器收到请求并确认加密方法,然后返回公钥,以及一个由服务器生成的随机数n2;在这个阶段iOS中的CA认证的认证的证书会自动验证,而私有的证书则需要手动验证放行,否则拒绝链接 380 | 381 | ``` 382 | 383 | * 客户端-->服务器 发送消息 384 | 385 | 386 | ``` 387 | 1.客户端验证证书成功后会生成第三个随机数n3,并用第2步服务器返回的证书对该随机数加密,并发送给服务器,同时也会发送一些其他信息,比如:编码信息和客户端握手结束的通知。 388 | 389 | ```` 390 | 391 | 392 | * 服务器-->客户端 发送信息 393 | 394 | ``` 395 | 服务器用私钥解密后,得到客户端传来的第三个随机数n3,两端使用这三个随机数n1、n2、n3来生成Session Key。服务器向客户端发送编码信息和服务器握手结束通知。 396 | 397 | ``` 398 | 399 | * 完整性验证 400 | 401 | 402 | ``` 403 | 完整性验证以后,后面的信息传输就靠这个Session key进行对称加密了。 404 | 405 | ``` 406 | 407 | 408 | 409 | #### SSL在握手的过程中主要交换了以下三个信息: 410 | 411 | 412 | * 加密通信协议:就是双方商量使用哪一种加密方式,假如两者支持的加密方式不匹配,则无法进行通信。 413 | * 数字证书:该证书包含了公钥等信息,一般是由服务器发给客户端,接收方通过验证这个证书是不是由信赖的CA签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证。 414 | * 三个随机数n1,n2,n3:这三个随机数构成了后续通信过程中用来对数据进行对称加密解密的对话密钥。 415 | 416 | 417 | 418 | #### 优点 419 | 420 | 421 | * 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器。 422 | * HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。 423 | * HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。 424 | 425 | 426 | 427 | 428 | #### 缺点 429 | 430 | 431 | * HTTPS握手阶段比较费时,会使页面加载时间延长50%,增加10%-20%的耗电。 432 | * HTTPS缓存不如HTTP高效,会增加数据开销。 433 | * SSL证书也需要钱,功能越强大的证书费用越高。 434 | * SSL证书需要绑定IP,不能再同一个IP上绑定多个域名,IPV4资源支持不了这种消耗。 435 | 436 | 437 | 438 | #### SSL/TLS协议介绍 439 | 440 | 互联网的通信安全是建立在SSL/TLS协议之上。不使用SSL/TLS的HTTP协议,就是不加密的通信;会带来三大风险: 441 | 442 | * 窃听风险:第三方可以获取通信内容。—(截取银行卡、各种密码、手机号等) 443 | * 篡改风险:第三方可以修改通信内容。 —(劫持插入广告) 444 | * 冒失风险:第三方可以冒充他人身份参与通信。—(遭遇伪装服务器通信) 445 | 446 | 447 | 448 | #### SSL/TLS就是为了解决这三大风险而设计的,希望达到: 449 | 450 | * 所有信息都是加密传播,第三方无法窃听。 451 | * 具有校验机制,一旦被篡改,通信双方立即发现。 452 | * 配备身份证书,防止身份被冒充。 453 | 454 | 455 | 456 | #### SSL/TLS协议的基本思路 457 | 458 | 459 | * 公钥加密法:即客户端先向服务端索要公钥,然后用公钥加密信息,客户端收到密文后,用自己的私钥解密。 460 | 461 | 462 | #### 如何保证公钥不被篡改? 463 | 464 | 465 | * 解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。 466 | 467 | 468 | #### 公钥加密计算量太大,如何减少耗用的时间? 469 | 470 | 471 | * 解决方法:每一次对话(SESSION),客户端和服务器端都生成一个"对话密钥"(SESSION KEY),用它来加密信息。由于"对话密钥"是对称加密,所以运算速度非常快,而服务器公钥只用于加密"对话密钥"本身,这样就减少了加密运算的消耗时间。 472 | 473 | #### HTTPS协议耗时原因? 474 | 475 | * HTTP耗时 = TCP握手;HTTPS耗时 = TCP握手 + SSL握手 所以,HTTPS肯定比HTTP耗时,这就叫SSL延迟 476 | 477 | 478 | #### 中间人攻击 479 | 480 | 481 | 中间人攻击是通过与客户端、服务器分别建立连接,来获得了明文的信息攻击方式。在这个过程中,客户端与服务器的通信被第三方解密、查看、修改。 482 | 483 | 484 | #### 原理图: 485 | 486 | 487 | ![](http://brandonliu.pub/icon_blog_gongji_http.jpeg) 488 | 489 | 490 | 491 | #### 为什么有些APP即便使用了HTTPS,还是会被中间人攻击呢? 492 | 493 | 494 | 基本都是因为没做客户端验证,特别是在使用未经CA认证的证书时,更容易中招。关于黑客是如何在协议握手初期劫持服务,从而实现中间人攻击,可以做如下推演: 495 | 496 | * 黑客劫持到服务器公钥,并冒充客户端与服务器连接。 497 | * 黑客自己生成公钥,冒充服务器公钥返回给真正的客户端。 498 | 499 | 500 | 如果客户端未做验证的话,就不会发现证书被替换,于是客户端会用黑客的公钥发送数据,黑客劫持数据后,用自己的私钥解密。 501 | 502 | 503 | 504 | #### 如何防止中间人攻击? 505 | 506 | 507 | * 把证书打包进APP,然后与服务器返回的证书对比验证,验证成功以后才允许连接。当然是用这种方式会导致一些问题,比如:当证书过期时APP连不上服务器,这时需要我们将新证书提前打包进APP。 508 | * 通过Socket通道从服务器传送证书进APP中,来实现无缝对接。 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | -------------------------------------------------------------------------------- /iOS面试题难点集锦/iOS面试题难点集锦(二).md: -------------------------------------------------------------------------------- 1 | ## iOS面试题难点集锦(二)---参考答案 2 | 3 | 引言:通过上篇文章[iOS面试题难点集锦(一)](https://github.com/LiuFuBo/iOSInterviewQuestions/blob/master/iOS面试题难点集锦/iOS面试题难点集锦(一).md),我们讲解了一些关于Runtime的知识,由于篇幅过长问题,其他一些内容咱们会放到后续一些章节来讲解。 4 | 5 | # 索引 6 | 7 | 1. [main函数之前程序做了些什么?](#main函数之前程序做了些什么) 8 | 2. [NSArray 和 NSMutableArray使用Copy和MutableCopy有何不同?](#nsarray-和-nsmutablearray使用copy和mutablecopy有何不同) 9 | 3. [initialize 和 init 以及 load 调用时机?](#initialize-和-init-以及-load-调用时机) 10 | 4. [GCD 全称是啥?什么时候使用 GCD?](gcd-全称是啥-什么时候使用-gcd) 11 | 5. [NSURLSession 和 NSURLConnection 区别?](#nsurlsession-和-nsurlconnection-区别) 12 | 6. [引用计数设计原理](#引用计数设计原理) 13 | 7. [Hash表扩容问题](#hash表扩容问题) 14 | 8. [深入了解+load方法的执行顺序](#深入了解+load方法的执行顺序) 15 | 9. [load 和 initialize 方法的区别](#load-和-initialize-方法的区别) 16 | 10. [RunLoop 的 CFRunLoopModeRef 结构体有啥内容? ](#runLoop-的-cfrunloopmoderef-结构体有啥内容) 17 | 11. [Category为现有的类提供拓展性,为何它可以提供拓展性?](#category为现有的类提供拓展性-为何它可以提供拓展性) 18 | 12. [Category为何不能直接添加属性?](#Category为何不能直接添加属性) 19 | 13. [Category和Extension的区别?](#Category和Extension的区别) 20 | 14. [NSObject对象苹果增加了一些内容,为何不会覆盖咱们自定义的属性?](#NSObject对象苹果增加了一些内容-为何不会覆盖咱们自定义的属性) 21 | 15. [load方法里如果有大量对象的创建的操作是,是否需要自动释放池?](#load方法里如果有大量对象的创建的操作-是否需要自动释放池) 22 | 16. [AppDelegate 各个常用代理方法都是何时调用的?](#AppDelegate-各个常用代理方法都是何时调用的) 23 | 17. [UIViewController 生命周期方法调用顺序?](#UIViewController-生命周期方法调用顺序) 24 | 18. [浅谈iOS中weak的底层实现原理](#浅谈iOS中weak的底层实现原理) 25 | 19. [多线程死锁原因?](#多线程死锁原因) 26 | 20. [单核处理器和多核处理器的区别?](#单核处理器和多核处理器的区别) 27 | 21. [KVO实现原理?如何自己实现KVO?](#KVO实现原理-如何自己实现KVO) 28 | 22. [通知实现原理?如何自定义通知?](#通知实现原理-如何自定义通知) 29 | 23. [一个只读的属性 为什么不能实现KVO](#一个只读的属性-为什么不能实现KVO) 30 | 24. [UIWebView和WKWebView的区别?](#UIWebView和WKWebView的区别?) 31 | 25. [一张图片渲染到屏幕上经过了什么过程?](#一张图片渲染到屏幕上经过了什么过程) 32 | 26. [离屏渲染是什么?什么场景会触发离屏渲染?都有什么具体的优化?](#离屏渲染是什么-什么场景会触发离屏渲染-都有什么具体的优化) 33 | 27. [LRU算法原理?以及如何优化?](#LRU算法原理以及如何优化) 34 | 28. [NSMutableArray是线程安全的么?如何创建线程安全的NSMutableArray?](#NSMutableArray是线程安全的么-如何创建线程安全的NSMutableArray) 35 | 29. [UITableView性能优化汇总](#UITableView性能优化汇总) 36 | 30. [有两个视图A/B,A有一部分内容覆盖在B上面,如果点击A让B视图响应?](#有两个视图A/B-A有一部分内容覆盖在B上面-如果点击A让B视图响应) 37 | 31. [折半查找为何不能用于存储结构是链式的有序查找表?](#折半查找为何不能用于存储结构是链式的有序查找表) 38 | 32. [同步、异步与串行、并行的关系?](#同步-异步与串行-并行的关系) 39 | 33. [类目为何不能添加属性?](#类目为何不能添加属性) 40 | 34. [字典,一般是用字符串来当做Key的,可以用对象来做key么?要怎么做?](#字典一般是用字符串来当做Key的-可以用对象来做key么-要怎么做) 41 | 35. [苹果公司为什么要设计元类?](#苹果公司为什么要设计元类) 42 | 36. [结构体和联合区的区别?](#结构体和联合区的区别) 43 | 37. [CALayer和UIView的区别?](#CALayer和UIView的区别) 44 | 38. [一个NSObject对象占用多少内存?](#一个NSObject对象占用多少内存) 45 | 39. [通过self和super两种方式分别调用class方法,得到结果是啥?调用区别是什么?](#通过self和super两种方式分别调用class方法-得到结果是啥-调用区别是什么) 46 | 40. [如何监听非UI主线程添加视图?](#如何监听非UI主线程添加视图) 47 | 41. [假如给你一张50M的本地图片,你如何保证图片能够流畅的在手机上显示出来,并且内存不暴涨?](#假如给你一张50M的本地图片-你如何保证图片能够流畅的在手机上显示出来-并且内存不暴涨) 48 | 42. [修改成员变量会触发KVO吗?KVC会触发KVO吗?](#修改成员变量会触发KVO吗-KVC会触发KVO吗) 49 | 43. [NSTimer计时不准的原因?](#NSTimer计时不准的原因) 50 | 44. [线程池的作用有哪些?](#线程池的作用有哪些) 51 | 45. [什么是函数式编程?](#什么是函数式编程) 52 | 46. [WKWebView和JS交互的方式有哪些?](#WKWebView和JS交互的方式有哪些) 53 | 54 | 55 | 56 | 57 | ### main函数之前程序做了些什么? 58 | 59 | 我们很少关注应用在启动前,系统会为我们做哪些事情,首先系统会先读取App 的可执行文件(Mach-O 文件),从里面获得 `dyld`(动态链接库) 的路径,然后加载 `dyld`(动态链接库)并进行以下流程加载: 60 | 61 | 1、设置运行环境 62 | 2、加载共享缓存 63 | 3、实例化主程序 64 | 4、加载插入的动态库 65 | 5、链接主程序 66 | 6、链接插入的动态库 67 | 7、执行弱符号绑定 68 | 8、执行初始化方法 69 | 9、查找入口点并返回 70 | 71 | 72 | #### 1.设置运行环境 73 | 74 | 这一步主要是设置运行参数、环境变量等。代码在开始的时候,将入参 `mainExecutableMH` 赋值给了 `sMainExecutableMachHeader`,这是一个 `macho_header` 结构体,表示的是当前主程 序的 `Mach-O` 头部信息,加载器依据 `Mach-O` 头部信息就可以解析整个 `Mach-O` 文件信息。 接着调用 `setContext()`设置上下文信息,包括一些回调函数、参数、标志信息等。 75 | 76 | #### 2.加载共享缓存 77 | 78 | 这一步先调用 `checkSharedRegionDisable()` 检查共享缓存是否禁用。推断 iOS 必须开启共享缓 存才能正常工作仅加载到当前进程,调用 `mapCachePrivate()`。共享缓存已加载,不做任何处理。当前进程首次加载共享缓存,调用 `mapCacheSystemWide()`。 79 | 80 | 81 | #### 3.实例化主程序 82 | 83 | 这一步将主程序的 Mach-O 加载进内存,并实例化一个 `ImageLoader`。 84 | 85 | 86 | #### 4.加载插入的动态库 87 | 88 | 89 | 这一步是加载环境变量 `DYLD_INSERT_LIBRARIES` 中配置的动态库,先判断环境变量 `DYLD_INSERT_LIBRARIES` 中是否存在要加载的动态库,如果存在则调用 `loadInsertedDylib()` 依次加载,会先在共享缓存中搜寻,打开文件并读取数据到内存后,最后调用 `checkandAddImage()` 验证镜像并将其加入到全局镜像列表中。 90 | 91 | 92 | #### 5.链接主程序 93 | 94 | 这一步调用 `link()` 函数将实例化后的主程序进行动态修正,让二进制变为可正常执行的状态。递归调用刷新层级。 95 | 96 | 97 | #### 6.链接插入的动态库 98 | 99 | 这一步与链接主程序一样,将前面调用 `addImage()` 函数保存在 `sAllImages` 中的动态库列表循 环取出并调用 `link()` 进行链接,需要注意的是,`sAllImages` 中保存的第一项是主程序的镜像, 所以要从 `i+1` 的位置开始,取到的才是动态库的 `ImageLoader`。 100 | 101 | #### 7.执行弱符号绑定 102 | 103 | `weakBind()`首先通过 `getCoalescedImages()`合并所有动态库的弱符号到一个列表里,然后调用 `initializeCoalIterator()`对需要绑定的弱符号进行排序,接着调用 `incrementCoalIterator()`读取 `dyld_info_command` 结构的 `weak_bind_off` 和 `weak_bind_size` 字段,确定弱符号的数据偏移 与大小,最终进行弱符号绑定。 104 | 105 | 106 | #### 8.执行初始化方法 107 | 108 | 这一步由 `initializeMainExecutable()` 完成。`dyld` 会优先初始化动态库,然后初始化主程序。该函数首先执行 `runInitializers()` , 内部再依次调用 `processInitializers()` 、 `recursiveInitialization()` runtime在这个时间被初始化,runtime初始化后不会闲着,在`_objc_init` 中注册了几个通知,从 `dyld` 这里接手了几个活,其中包括负责初始化相应依赖库里的类结构,调用依赖库里所有的 `load` 方法。这里注册的 `init` 回调函数就是 `load_images()`,回调里 面调用了 call_load_methods()来执行所有的+ load 方法。但由于 lazy bind 机制,依赖库多数 都是在使用时才进行 `bind`,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。 109 | 110 | 111 | #### 9.查找入口点并返回 112 | 113 | 这一步调用主程序镜像的 `getThreadPC()`,从加载命令读取 `LC_MAIN` 入口,如果没有 `LC_MAIN` 就调用 `getMain()`读取 `LC_UNIXTHREAD`,找到后就跳到入口点指定的地址并返回。 至此,整个 `dyld` 的加载过程就分析完成了。\ 114 | 115 | 116 | 117 | 118 | ### NSArray 和 NSMutableArray使用Copy和MutableCopy有何不同? 119 | 120 | #### 1.深拷贝和浅拷贝区别 121 | 122 | NSArray Copy出来的是浅拷贝,MutableCopy出来的是深拷贝。 123 | NSMutableArray 使用Copy 和 MutableCopy 都是深拷贝。 124 | 125 | 126 | #### 2.拷贝出来的对象是可变数组还是非可变数组 127 | 128 | NSArray `Copy`拷贝出来的是一个不可变数组, `MutableCopy`拷贝出来的是一个可变数组。 129 | NSMutableArray `Copy`拷贝出来的是一个不可变数组, `MutableCopy`拷贝出来的是一个可变数组。 130 | 131 | 132 | ### initialize 和 init 以及 load 调用时机? 133 | 134 | `Initialize` 是在这个类第一次被调用的时候调用,比如`[[class alloc]init]`,之后不管创建多少次这个类,都不会再调用这个方法,它被用来初始化静态变量,`init`是只要调用这个类就会触发 `init` 方法,`load` 方法是在 `main` 函数之前调用。 135 | 136 | 137 | ### GCD 全称是啥 什么时候使用 GCD? 138 | 139 | GCD 全称 `Grand Central Dispatch` 优秀的中央调度器 140 | 141 | GCD 优势: 142 | 1.GCD 是苹果为多核的并行运算提出的解决方案。 143 | 2.GCD 会自动利用更多的 CPU 内核。 144 | 3.GCD 可以自动管理线程生命周期。 145 | 146 | GCD 使用场景: 147 | GCD 定时器、切换线程、耗时操作、多个异步操作完成后再更新UI。 148 | 149 | 1、dispatch_barrier_sync 需要等待自己的任务(barrier)结束之后,才会继续添加并执行写 在 barrier 后面的任务(4、5、6),然后执行后面的任务。 150 | 2、dispatch_barrier_async 将自己的任务(barrier)插入到 queue 之后,不会等待自己的任务结束,它会继续把后面的任务(4、5、6)插入到 queue,然后执行任务。 151 | 152 | ### NSURLSession 和 NSURLConnection 区别? 153 | 154 | 1.使用现状 155 | 156 | NSURLSession 是 NSURLConnection 的替代者。 157 | 158 | 2.普通任务和上传 159 | 160 | NSURLSession 针对下载/上传等复杂的网络操作提供了专门的解决方案,针对普通、上传和 下载分别对应三种不同的网络请求任务:NSURLSessionDataTask, NSURLSessionUploadTask 和 NSURLSessionDownloadTask。创建的 task 都是挂起状态,需要 resume 才能执行。当服务器返回的数据较小时,NSURLSession 与 NSURLConnection 执行普通任务的操作步骤没有区别。 执行上传任务时,NSURLSession 与 NSURLConnection 一样都需要设置 POST 请求的请求体再进行上传。 161 | 162 | 3.下载任务 163 | 164 | NSURLConnection 下载文件时,先将整个文件下载到内存,然后再写入沙盒,如果文件比较大,就会出现内存暴涨的情况。而使用 NSURLSessionDownloadTask 下载文件,会默认下载到沙盒中的 tem 文件夹中,不会出现内存暴涨的情况,但在下载完成后会将 tem 中的临时文件 删除,需要在初始化任务方法时,在 completionHandler 回调中增加保存文件的代码。 165 | 166 | 4.配置信息 167 | 168 | NSURLSession 的构造方法(sessionWithConfiguration:delegate:delegateQueue)中有一个 NSURLSessionConfiguration 类的参数可以设置配置信息,其决定了 cookie,安全和高速缓存 策略,最大主机连接数,资源管理,网络超时等配置。NSURLConnection 不能进行这个配置, 相比于 NSURLConnection 依赖于一个全局的配置对象,缺乏灵活性而言,NSURLSession 有 很大的改进了。 169 | 170 | 171 | ### 引用计数设计原理 172 | 173 | 引用计数设计不仅我们的系统是否支持`Tagged Pointer`有关系,而且还跟系统`isa`指针是否优化过有关系,具体的实现原理可参考这篇文章[Objective-C 引用计数原理](https://www.jianshu.com/p/12d6e64c07bb)。 174 | 175 | 176 | ### hash表扩容问题 177 | 178 | 179 | 180 | 哈希表是一个散列表,里面存储的是键值对(key-value)映射。它是一种根据关键码 key 来寻找值 value 的数据映射结构。 181 | 182 | 183 | `装载因子`,也叫负载因子(load factor),它表示散列表的装满程度。当当前表的实际装载因 子达到默认的负载因子值(负载极限)时,就会触发哈希表的扩容。 184 | 185 | 186 | 一般情况下,默认的负载因子值不能太大,因为其虽然减少了空间开销,但是增加了查询的时间成本;也不能太小,因为这样还会增加 `rehash` 的次数,性能较低。 187 | 188 | 189 | `Hashcode` 190 | 哈希码是一种算法,尽量为不同的对象生成不同的哈希码。(但不代表不同对象的哈希码一 定不同。)它可以作为相同对象判断的依据。同一对象如果没有经过修改,前后不同时刻生 成的哈希码应该是一致的。 191 | 192 | 不过我们知道,判断是否相同已经有了 `equals()` 方法,那为什么还需要 `Hashcode()`方法呢? 这是因为 `equals()`方法的效率远不如 `Hashcode()`方法。 193 | 194 | 同样的问题,既然 `Hashcode()`性能那么高,那为什么还需要 `equals()`方法呢? 这是因为 `equals()`方法是完全可靠的,而仅仅基于哈希码比较是不完全可靠的。 如果两个对象相同,`Hashcode()` 一定相同。 195 | 196 | 但是 `Hashcode()` 相同的两个对象不一定相同。 而如果两个对象相同,`equals()`方法得到的一定为 `true`。所以说 `Java` 中的 `HashMap` 既提供对 `equals()`的重写,也提供对 `Hashcode()`的重写。 于是,对于这种有着大量且快速的对象对比需求的 `hash` 容器,我们将两种方法结合起来用。 先使用 `Hashcode()`方法,如果两个对象产生的哈希码不相同,那么这两个对象一定不同,不 再进行后续比较; 而如果两个对象产生的哈希码相同,那么这两个对象有可能相同,于是再使用 equals()方法 进行比较。 197 | 198 | 199 | 哈希冲突 200 | 哈希冲突是指,不同的 key 经由哈希函数,映射到了相同的位置上,造成的冲突。哈希冲突是不可避免的,但是如果冲突较严重就会影响哈希表的性能。 201 | 202 | 203 | 我们一般采用四种方式来解决哈希冲突:开放定址法、链地址法、再哈希法、建立公共溢出区。 204 | 205 | 这里列举几种解决哈希冲突的几种办法(举例推演)。 206 | 207 | 开放定址法: 208 | 209 | 这种方法的意思是当关键字 Key 的哈希地址 p=H(key)出现冲突是,以 p 为基础, 产生另一个哈希地址 `p1`,如果 `p1` 仍然冲突,产生另外一个哈希地址 P2,直到找出不冲突的哈 希地址 Pi,将相应元素存入其中。 210 | 211 | 线性探测再散列 212 | 213 | 当发生冲突的时候,顺序的查看下一个单元 214 | 215 | 二次(平方)探测再散列 216 | 217 | 当发生冲突的时候,在表的左右进行跳跃式探测 (+1^2 如果有冲突就-1^2 如果发生冲 突,+2^2 进行验证) 218 | 219 | 伪随机探测再散列 220 | 221 | 建立一个伪随机数发生器,并给一个随机数作为起点,再hash法 这种方式是同时构造多个哈希函数,当产生冲突时,计算另一个哈希函数的值。 这种方法不易产生聚集,但增加了计算时间。 222 | 223 | 双哈希函数探测法 224 | 225 | Hi = (Hash(key) + i·ReHash(key))mod m (i=1,2,...,m-1) 226 | 其中,Hash(key),ReHash(key)是两个哈希函数,m为哈希函表的长度。 227 | 双哈希函数探测法先用第一个函数Hash(key)对关键字计算哈希地址,一旦产生地址冲突,再用第二个函数ReHash(key) 确定移动步长因子,最后,通过步长因子序列由探测函数寻找空的哈希地址。 228 | 例如: 229 | Hash(key) = a 产生地址冲突,就计算ReHash(key) = b,则探测的地址序列为 H1 = (a+b)mod m, H2 = (a+2b) mod m,..., Hm-1 = (a + (m-1)b) mod m。 230 | 231 | 232 | 链地址法 233 | 234 | 将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中 进行。链地址法适用于经常进行插入和删除的情况。HashMap 就是用此方法解决冲突的。 此种方法必须要求采用链表来实现hash表。具体实现方法为将所有关键字为同义词的记录存储在一个单链表中以后,采用一维数组存放头指针。链地址法每个单元不是存储对应元素,而是存储响应单链表的表头指针,单链表中的每个节点动态分配产生。 235 | 236 | 建立一个公共溢出区 237 | 238 | 设哈希函数产生的哈希地址集为[0,...,m-1],则分配两个表: 239 | 一个是基本表ElemType base_tb[m],其每个单元只能存放一个元素。另一个是溢出表ElemType over_tb[k],只要关键字对应的哈希地址在基本表上产生冲突,则所有这样的元素一律存入该表中。查找时,对给定值kx通过哈希函数计算出哈希地址I,先与基本表的base_tb[i]单元比较,若相等,查找成功;否则,再到溢出表中进行查找。 240 | 241 | 242 | 采取分摊转移的方式 243 | 即当插入一个新元素 x 触发了扩容时,先转移第一个不为空的桶 到新的哈希表,然后将该元素插入。而下一次再次插入时,继续转移旧哈希表中第一个不为 空的桶,再插入元素。直至旧哈希表为空为止。这样一来,理想情况下,插入的时间复杂度 是 O(1)。在 Redis 的实现中,新插入的键值对会放在箱子中链表的头部,而不是在尾部继续插入。 244 | 245 | 这种方案是基于两点考虑: 246 | 247 | 一是由于找到链表尾部的时间复杂度为 O(n),且需要额外的内存地址来保存链表的尾部位置, 而头插法的时间复杂度为 O(1)。 248 | 二是处于 Redis 的实际应用场景来考虑。对于一个数据库系统来说,最新插入的数据往往更 可能频繁地被获取,所以这样也能节省查找的耗时。 249 | 250 | 251 | ### 深入了解+load方法的执行顺序 252 | 253 | 254 | 1、`+load` 方法是在 `dyld` 阶段的执行初始化方法步骤中执行的,其调用为 `load_images->call_load_methods`。 255 | 2、一个类在代码中不主动调用+load 方法的情况下,其类、子类实现的`+load` 方法都会分别执行一次。 256 | 3、父类的 `+load` 方法执行在前,子类的 `+load` 方法在后。 257 | 4、在同一镜像中,所有类的+load 方法执行在前,所有分类的 `+load` 方法执行在后。 258 | 5、同一镜像中,没有关系的两个类的执行顺序与编译顺序有关(Compile Sources 中的顺序)。 259 | 6、同一镜像中所有的分类的+load方法的执行顺序与编译顺序有关(Compile Sources中的顺 序),与是谁的分类,同一个类有几个分类无关。 260 | 7、同一镜像中主工程的 `+load` 方法执行在前,静态库的 `+load` 方法执行在后。有多个静态库时, 静态库之间的执行顺序与编译顺序有关(Link Binary With Libraries 中的顺序)。 261 | 8、不同镜像中,动态库的 `+load` 方法执行在前,主工程的 `+load` 执行在后,多个动态库的 `+load` 方法的执行顺序编译顺序有关(Link Binary With Libraries 中的顺序)。 262 | 263 | ### load 和 initialize 方法的区别 264 | 265 | 266 | `load` 方法采用的是 `IMP` 指针地址直接调用,`initialize` 采用的是消息转发机制。 267 | 268 | `load` 是只要类所在文件被引用就会被调用,而 `initialize` 是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有`load`调用;但即使类文件被引用进来, 但是没有使用,那么 `initialize` 也不会被调用。 269 | 270 | 关于load方法特点 271 | 272 | 1、只要程序启动就会将所有类的代码加载到内存中(在 `main` 函数执行之前), 放到代码区 (无论该类有没有被使用到都会被调用)。 273 | 2、`+load` 方法会在当前类被加载到内存的时候调用, 有且仅会调用一次。 274 | 3、当父类和子类都实现+load 方法时, 会先调用父类的 `+load` 方法, 再调用子类的 `+load`方法。 275 | 4、先加载原始类,再加载分类的 `+load` 方法。 276 | 5、当子类未实现 `+load` 方法时,不会调用多一次父类的 `+load` 方法(因为 `load` 采用 `IM`P 指针直接调用的)。 277 | 6、多个类都实现 `+load` 方法,`+load` 方法的调用顺序,与 Compile Sources 中出现的顺序一致。 278 | 279 | 关于initialize特点 280 | 281 | 1、当类第一次被使用的时候就会调用(创建类对象的时候)。 282 | 2、`initialize` 方法在整个程序的运行过程中只会被调用一次, 无论你使用多少次这个类都只会调用一次。 283 | 3、`initialize` 用于对某一个类进行一次性的初始化。 284 | 4、先调用父类的 `initialize` 再调用子类的 `initialize`。 285 | 5、当子类未实现 `initialize` 方法时,会把父类的实现继承过来调用一遍,再次之前父类的 `initialize` 方法会被优先调用一次。 286 | 6、当有多个 `Category` 都实现了 `initialize` 方法,会覆盖类中的方法,只执行一个(会执行 Compile Sources 列表中最后一个 `Category` 的 `initialize` 方法)。 287 | 288 | 289 | ### RunLoop 的 CFRunLoopModeRef 结构体有啥内容? 290 | 291 | 292 | 主要包含了两个Source 既`source0` 和 `source1` 293 | 294 | ``` 295 | CFMutableSetRef _sources0; CFMutableSetRef _sources1; 296 | 一个观察者 observers 297 | CFMutableArrayRef _observers; 一个 timers 298 | CFMutableArrayRef _timers; 一个 portSet 299 | __CFPortSet _portSet; 300 | ``` 301 | 其实 `NSRunLoop` 的本质是一个消息机制的处理模式,`runloop` 的运行,其实就是不停的通过 `observer` 监听各种事件,包含各种 `source` 事件,`timer`,`port` 等等,如果有这些事件,那就 处理,没有事件,就会进入休眠,不停的重复上述过程。`RunLoop` 是一种观察者模式 `AutoreleasePool` 与 `RunLoop` 并没有直接的关系,之所以将两个话题放到一起讨论最主要的原 因是因为在 iOS 应用启动后会注册两个 `Observer` 管理和维护 `AutoreleasePool` 第一个 `Observer` 会监听 `RunLoop` 的进入,它会回调 `objc_autoreleasePoolPush()`向当前的 `AutoreleasePoolPage` 增加一个哨兵对象标志创建自动释放池。这个` Observer `的 `order` 是 `-2147483647` 优先级最高, 确保发生在所有回调操作之前。 302 | 303 | 第二个 `Observer` 会监听 `RunLoop` 的进入休眠和即将退出 `RunLoop` 两种状态,在即将进入休 眠时会调用 `objc_autoreleasePoolPop()` 和 `objc_autoreleasePoolPush()` 根据情况从最新加入的 对象一直往前清理直到遇到哨兵对象。而在即将退出 `RunLoop` 时会调用 `objc_autoreleasePoolPop()` 释放自动自动释放池内对象。这个 `Observer` 的 `order` 是 `2147483647`, 优先级最低,确保发生在所有回调操作之后。 304 | 305 | 306 | ### Category为现有的类提供拓展性 为何它可以提供拓展性? 307 | 308 | 309 | 在App启动加载镜像文件时,会在`_read_images` 函数中调用 `remethodizeClass` 函数,然后再调用 `attachCategories` 函数,完成向类中添加 `Category` 的工作。原理就是向 `class_rw_t` 中的 `method_array_t`,`property_array_t`,`protocol_array_t` 数组中分别添加 `method_list_t`,`property_list_t`,`protocol_list_t` 指针。`xxx_array_t` 可以存储对应的 `xxx_list_t` 的指针数组。 310 | 311 | 在调用 `attachCategories` 函数之前,会先调用 `unttachedCategoriesForClass` 函数获取类中还未添加的类别列表。这个列表类型为 `locstamped_category_list_t`,它封装了 `category_t` 以及对应的 `header_info`。`header_info` 存储了实体在镜像中的加载和初始化状态,以及一些偏移量, 在加载 `Mach-O` 文件相关函数中经常用到。 312 | 313 | ``` 314 | struct locstamped_category_t { category_t *cat; 315 | struct header_info *hi; }; 316 | struct locstamped_category_list_t { uint32_t count; 317 | #if __LP64__ 318 | uint32_t reserved; 319 | #endif 320 | locstamped_category_t list[0]; 321 | }; 322 | ``` 323 | 所以更具体来说 `attachCategories` 做的就是将 `locstamped_category_list_t.list` 列表中每个 `Locstamped_category_t.cat` 中那方法、协议和属性分别添加到类的 `class_rw_t` 对应列表中。 `header_info` 中的信息决定了是否是元类,从而选择应该是添加实例方法还是类方法、实例 属性还是类属性等。 324 | 其实编译器会根据情况在 `objc_msgSend`,`objc_msgSend_stret`,`objc_msgSendSuper`,或 `objc_msgSendSuper_stret` 四个方法中选择一个来调用。如果消息是传递给超类,那么会调用 名字带有 `super` 的函数,如果是在 `i386` 平台处理返回类型为浮点数的消息事,需要用到 `objc_msgSend_fpret` 函数处理 `fpret` 就是`fp"+"ret` 分别代表`floating-point`和`return`。 325 | 326 | 327 | 328 | #### Category为何不能直接添加属性 329 | 330 | 首先Category系统没有生成setter方法,而且Category并没有成员变量列表,成员变量列表本身也是一个类才该有的东西,分类本身不是一个类,所以他不能添加成员变量。另外类的变量布局在编译后就已经完成。在objc_class结构体中,ivars是objc_ivar_list(成员变量列表)指针;methodLists是指向objc_method_list指针的指针。在Runtime中,objc_class结构体大小是固定的,不可能往这个结构体中添加数据,只能修改。所以ivars指向的是一个固定区域,只能修改成员变量值,不能增加成员变量个数。methodList是一个二维数组,所以可以修改methodLists里面指针的值来增加成员方法,虽没办法扩展methodLists指向的内存区域,却可以改变这个内存区域的值(存储的是指针)。因此,可以动态添加方法,不能添加成员变量。 331 | 332 | 333 | #### Category和Extension的区别 334 | Category是为现有的类添加新的方法的方式,是运行时决议的,Extension是特殊的分类,也叫匿名分类,主要是为类添加私有变量和私有方法的,他是编译时决议的,是类的一部分。 335 | 336 | 337 | ### NSObject对象苹果增加了一些内容 为何不会覆盖咱们自定义的属性? 338 | 339 | 340 | `objc_class` 包含了 `class_data_bits_t`,`class_data_bits_t` 存储了 `class_rw_t` 的指针,而 `class_rw_t` 结构体又包含 `class_ro_t` 的指针。 341 | 342 | ``` 343 | struct class_ro_t { 344 | uint32_t flags; 345 | uint32_t instanceStart; 346 | uint32_t instanceSize; #ifdef __LP64__ 347 | uint32_t reserved; #endif 348 | const uint8_t * ivarLayout; 349 | const char * name; 350 | method_list_t * baseMethodList; 351 | protocol_list_t * baseProtocols; 352 | const ivar_list_t * ivars; 353 | const uint8_t * weakIvarLayout; 354 | property_list_t *baseProperties; 355 | method_list_t *baseMethods() const { 356 | return baseMethodList; 357 | } 358 | }; 359 | ``` 360 | 361 | 假如苹果在新版本的 SDK 中向 NSObject 类增加了一些内容,NSObject 的占据的内存区域 会扩大,开发者以前编译出的二进制中的子类就会与新的 NSObject 内存有重叠部分。于是在编译期会给 `instanceStart` 和 `instanceSize` 赋值,确定好编译时每个类的所占内存区域起 始偏移量和大小,这样只需将子类与基类的这两个变量作对比即可知道子类是否与基类有重叠,如果有,也可知道子类需要挪多少偏移量。 362 | 363 | 364 | 365 | ### load方法里如果有大量对象的创建的操作 是否需要自动释放池? 366 | 367 | 368 | `load`方法外部有一个 `runtime`。但是 他会调完 所有的`+load` 才会结束。。所以对于局部的峰值来说并不能优化。 369 | 370 | 371 | ### AppDelegate 各个常用代理方法都是何时调用的? 372 | 373 | 374 | 1. didFinishLaunchingWithOptions 375 | 当应用程序正常启动时(不包括已在后台转到前台的情况),调用此回调。launchOptions 是 启动参数,假如用户通过点击 push 通知启动的应用,(这是非正常启动的情况,包括本地通 知和远程通知),这个参数里会存储一些 push 通知的信息。 376 | 377 | 2.applicationWillResignActive 378 | 当应用程序即将从活动状态移动到非活动状态时发送。对于某些类型的临时中断(例如来电 或 SMS 消息),或者当用户退出应用程序并开始转换到后台状态时,可能会出现这种情况。 使用此方法暂停正在进行的任务,禁用计时器,并使图形呈现回调无效。游戏应该使用这种 方法暂停游戏。调用时机可能有以下几种:锁屏、单击 HOME 键、下拉状态栏、双击 HOME 弹出底部状态栏等情况 379 | 380 | 3.applicationDidBecomeActive 381 | 当应用程序全新启动,或者在后台转到前台,完全激活时,都会调用这个方法。它会重新启 动应用程序处于非活动状态时暂停(或尚未启动)的任何任务,如果应用程序是以前运行在后 台,这时可以选择刷新用户界面。 382 | 383 | 4.applicationDidEnterBackground 384 | 使用此方法释放共享资源、保存用户数据、使计时器失效,并存储足够的应用程序状态信息, 以便在以后终止应用程序时将其恢复到当前状态。如果你的应用程序支持后台执行,这个方 法会被调用,而不是 applicationWillTerminate:当用户退出时。 385 | 386 | 5.applicationWillEnterForeground 387 | 被调用作为从后台到活动状态转换的一部分;在这里,您可以撤消在进入后台时所做的许多 更改。如果应用不在后台状态,而是直接启动,则不会回调此方法。 388 | 389 | 6.applicationWillTerminate 390 | 当应用退出,并且进程即将结束时会调到这个方法,一般很少主动调到,更多是内存不足时 是被迫调到的,我们应该在这个方法里做一些数据存储操作。 391 | 392 | 7.application:openURL: 393 | 从其他应用回到当前应用回调。 394 | 395 | 396 | ### UIViewController 生命周期方法调用顺序? 397 | 398 | 399 | 1.init方法 400 | 这里包含了非 storyBoard 创建 UIViewController 调用 initWithNibName:bundle: 如果用 storyBoard 进行视图管理会调用 initWithCoder。 401 | 402 | 2.loadView 403 | 当执行到 loadView 方法时,如果视图控制器是通过 nib 创建,那么视图控制器已经从 nib 文 件中被解档并创建好了,接下来任务就是对 view 进行初始化。 404 | 405 | 3.viewDidload 406 | 当 loadView 将 view 载入内存中,会进一步调用 viewDidLoad 方法来进行进一步设置。此时, 视图层次已经放到内存中,通常,我们对于各种初始化数据的载入,初始设定、修改约束、 移除视图等很多操作都可以这个方法中实现。 407 | 408 | 4.viewWillAppear 409 | 系统在载入所有的数据后,将会在屏幕上显示视图,这时会先调用这个方法,通常我们会在 这个方法对即将显示的视图做进一步的设置。比如,设置设备不同方向时该如何显示;设置 状态栏方向、设置视图显示样式等。 410 | 411 | 5.viewWillLayoutSubviews 412 | view 即将布局其 Subviews。 比如 view 的 bounds 改变了(例如:状态栏从不显示到显示,视图 方向变化),要调整 Subviews 的位置,在调整之前要做的工作可以放在该方法中实现。 413 | 414 | 6.viewDidAppear 415 | 在 view 被添加到视图层级中以及多视图,上下级视图切换时调用这个方法,在这里可以对 正在显示的视图做进一步的设置。 416 | 417 | 7.viewWillDisappear 418 | 在视图切换时,当前视图在即将被移除、或被覆盖是,会调用该方法,此时还没有调用 removeFromSuperview。 419 | 420 | 8.viewDidDisappear 421 | view 已经消失或被覆盖,此时已经调用 removeFromSuperView。 422 | 423 | 9.dealloc 424 | 视图被销毁,此次需要对你在 init 和 viewDidLoad 中创建的对象进行释放。 425 | 426 | 10.didReceiveMemoryWarning 427 | 在内存足够的情况下,app的视图通常会一直保存在内存中,但是如果内存不够,一些没有 正在显示的 viewController 就会收到内存不足的警告,然后就会释放自己拥有的视图,以达到释放内存的目的。但是系统只会释放内存,并不会释放对象的所有权,所以通常我们需要 在这里将不需要显示在内存中保留的对象释放它的所有权,将其指针置 nil。 428 | 429 | 430 | ### 浅谈iOS中weak的底层实现原理 431 | 432 | 使用场景:在iOS开发中经常会用到"weak"关键词,具体的使用场景就是用于一些对象互相引用的时候,为了避免造成循环引用。weak 关键字的为弱引用,所以引用对象的引用计数不会+1,并且在引用对象被释放的时候,会自动将引用对象设置为nil。 433 | 434 | 原理概括:苹果为了管理所有对象的计数器和weak指针,苹果创建了一个全局的哈希表,我们暂且叫它SideTables,里面装的是的名为SideTable的结构体。用对象的地址作为key,可以取出sideTable结构体,这个结构体用来管理引用计数和weak指针。 435 | 436 | 下面是SideTables结构体: 437 | 438 | ``` 439 | struct weak_table_t { 440 | weak_entry_t *weak_entries; // 保存了所有指向指定对象的weak指针数组 441 | size_t num_entries; // weak对象的存储空间 442 | uintptr_t mask; //参与判断引用计数辅助量 443 | uintptr_t max_hash_displacement; //hash key 最大偏移值 444 | }; 445 | 446 | ``` 447 | 448 | 449 | 例如:__weak NSObject *objc = [[NSObject alloc] init]; 根据上面weak表的结构可以看出,这里通过objc这个对象的地址作为key,然后再全局weak哈希表中获取到objc该对象下面维护的所有弱引用的对象的指针数组。也就是weak_entry_t。 450 | 451 | 那么为何value这里是一个数组呢?因为objc对象可能引用了多个weak关键字的属性 452 | 453 | 454 | #### 具体步骤是怎么实现的呢?主要可以分为三步 455 | 456 | > 1. 创建一个weak对象时,runtime会调用一个objc_initWeak函数,初始化一个新的weak指针指向该对象的地址 457 | ![image](http://brandonliu.pub/weak_init.png) 458 | 459 | > 2.在objc_initWeak函数中会继续调用objc_storeWeak函数,在这个过程是用来更新weak指针的指向,同时创建对应的弱引用表 460 | 461 | ![image](http://brandonliu.pub/weak_store.png) 462 | 463 | > 3.释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。 464 | 465 | 466 | 拓展:weak和__unsafe_unretained以及unowned 与 assign区别是什么? 467 | 468 | >1.unsafe_unretained: 不会对对象进行retain,当对象销毁时,会依然指向之前的内存空间(野指针)。 469 | 470 | >2.weak: 不会对对象进行retain,当对象销毁时,会自动指向nil。 471 | 472 | >3.assign: 实质与__unsafe_unretained等同。 473 | 474 | >4.unsafe_unretained也可以修饰代表简单数据类型的property,weak也不能修饰用来代表简单数据类型的property。 475 | 476 | >5.unsafe_unretained 与 weak 比较,使用 weak是有代价的,因为通过上面的原理可知,weak需要检查对象是否已经消亡,而为了知道是否已经消亡,自然也需要一些信息去跟踪对象的使用情况。也正因此,unsafe_unretained 比weak快,所以当明确知道对象的生命期时,选择unsafe_unretained 会有一些性能提升,这种性能提升是很微小的。但当很清楚的情况下,unsafe_unretained 也是安全的,自然能快一点是一点。而当情况不确定的时候,应该优先选用weak。 477 | 478 | >6.unowned使用在Swift中,也会分 weak 和 unowned。unowned 的含义跟 unsafe_unretained 差不多。假如很明确的知道对象的生命期,也可以选择unowned。 479 | 480 | 481 | #### 多线程死锁原因? 482 | 483 | 484 | 属于临界资源的硬件有打印机、磁带机等,软件有消息缓冲队列、变量、数组、缓冲区等。诸进程间应采取互斥方式,实现对这种资源的共享。 485 | 486 | 487 | 当我们在使用两个不同的线程访问同一个临界资源时就会出现如下情况: 488 | ![image](http://brandonliu.pub/icon_blog_jingzhengziyuan.png) 489 | 490 | 491 | 线程A优先被创建出来并优先去获得对临界资源的操作权限,线程A里有一个循环代码会循环对该临界资源进行操作,因此就会操作系统内核在进程里的线程之间调度时会出现这样一种情况:线程A在对该临界资源操作时,线程B呼唤操作系统取的CPU控制权时,会有一个线程调用之间的现场保护,会对线程里的代码执行到了哪一步或者循环次数的记录保存到寄存器里,下次获取CPU控制权时会读取该记录,此时如果线程A没有结束的情况下会一直占用着该临界资源,导致线程B无法对该临界资源做写操作,从而进入无限的阻塞等待,从而导致了死锁的情况! 492 | 493 | 如何避免死锁 494 | 495 | 在有些情况下死锁是可以避免的。三种用于避免死锁的技术: 496 | 497 | > 1.加锁顺序(线程按照一定的顺序加锁) 498 | > 2.加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁) 499 | > 2.死锁检测 500 | 501 | 加锁顺序 502 | 503 | 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。 504 | 505 | 如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子: 506 | 507 | ``` 508 | Thread 1: 509 | lock A 510 | lock B 511 | 512 | Thread 2: 513 | wait for A 514 | lock C (when A locked) 515 | 516 | Thread 3: 517 | wait for A 518 | wait for B 519 | wait for C 520 | 521 | ``` 522 | 523 | 如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。 524 | 525 | 例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。 526 | 527 | 按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。 528 | 529 | 530 | 加锁时限 531 | 532 | 另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。 533 | 534 | 以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景: 535 | 536 | ``` 537 | Thread 1 locks A 538 | Thread 2 locks B 539 | 540 | Thread 1 attempts to lock B but is blocked 541 | Thread 2 attempts to lock A but is blocked 542 | 543 | Thread 1's lock attempt on B times out 544 | Thread 1 backs up and releases A as well 545 | Thread 1 waits randomly (e.g. 257 millis) before retrying. 546 | 547 | Thread 2's lock attempt on A times out 548 | Thread 2 backs up and releases B as well 549 | Thread 2 waits randomly (e.g. 43 millis) before retrying. 550 | 551 | ``` 552 | 553 | 在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁。 554 | 555 | 需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。 556 | 557 | 此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。 558 | (译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。) 559 | 560 | 这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。 561 | 562 | 死锁检测 563 | 564 | 死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。 565 | 566 | 每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。 567 | 568 | 当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。 569 | 570 | 当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。 571 | 572 | 573 | 那么当检测出死锁时,这些线程该做些什么呢? 574 | 575 | 一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。 576 | 577 | 一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。 578 | 579 | 580 | 581 | #### 单核处理器和多核处理器的区别? 582 | 583 | 单核cpu并不是一个长久以来存在的概念,在近年来多核心处理器逐步普及之后,单核心的处理器为了与双核和四核对应而提出。多核是指一个CPU有多个核心处理器,处理器之间通过CPU内部总线进行通讯,而多CPU是指简单的多个CPU工作在同一个系统上,多个CPU之间的通讯是通过主板上的总线进行的。 584 | 585 | 586 | #### KVO实现原理 如何自己实现KVO? 587 | 588 | 实现原理 589 | 590 | KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。[具体实现,请参考demo](https://github.com/LiuFuBo/iOSInterviewQuestions/blob/master/demo) 591 | 592 | 593 | 自定义KVO 594 | 595 | 创建一个分类新增一个方法addObserver,在方法中创建子类注册并指向子类,再为子类添加set方法既可。 596 | 597 | 主要使用函数如下: 598 | 599 | >1.创建一个子类 600 | 601 | ``` 602 | objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, 603 | size_t extraBytes) 604 | superclass:设置新类的父类 605 | name:新类名称 606 | extraBytes:额外字节数设置为0 607 | 608 | ``` 609 | 610 | >2.注册该类 611 | 612 | ``` 613 | objc_registerClassPair(Class _Nonnull cls) 614 | cls:当前要注册的类,注册后才可以使用 615 | 616 | ``` 617 | 618 | >3.设置当前对象指向其他类 619 | 620 | ``` 621 | object_setClass(id _Nullable obj, Class _Nonnull cls) 622 | obj:要设置的对象 623 | cls:指向的类 624 | ``` 625 | 626 | >4.动态添加一个方法 627 | 628 | ``` 629 | class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 630 | const char * _Nullable types) 631 | cls:设置添加方法对应的类 632 | name:选择子(选择器)名称,描述了方法的格式,并不会指向方法 633 | imp:函数名称(函数指针),和选择子一一对应,指向方法实现的地址 634 | 635 | ``` 636 | 637 | >5.通过分类添加新的观察者添加方法 638 | 639 | ``` 640 | -(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{ 641 | 642 | //创建、注册子类 643 | NSString *oldClassName = NSStringFromClass([self class]); 644 | NSString *newClassName = [NSString stringWithFormat:@"SKVONotifying_%@",oldClassName]; 645 | 646 | Class clazz = objc_getClass(newClassName.UTF8String); 647 | if (!clazz) { 648 | clazz = objc_allocateClassPair([self class], newClassName.UTF8String, 0); 649 | objc_registerClassPair(clazz); 650 | } 651 | 652 | //set方法名 653 | if (keyPath.length <= 0)return; 654 | NSString *fChar = [[keyPath substringToIndex:1] uppercaseString];//第一个char 655 | NSString *rChar = [keyPath substringFromIndex:1];//第二到最后char 656 | NSString *setterChar = [NSString stringWithFormat:@"set%@%@:",fChar,rChar]; 657 | SEL setSEL = NSSelectorFromString(setterChar); 658 | 659 | //添加set方法 660 | Method getMethod = class_getInstanceMethod([self class], setSEL); 661 | const char *types = method_getTypeEncoding(getMethod); 662 | class_addMethod(clazz, setSEL, (IMP)setterMethod, types); 663 | 664 | //改变isa指针,指向新建的子类 665 | object_setClass(self, clazz); 666 | 667 | 668 | //保存getter方法名,获取旧值的时候使用 669 | objc_setAssociatedObject(self, "getterKey", keyPath, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 670 | 671 | //保存setter方法名,设置新值的时候使用 672 | objc_setAssociatedObject(self, "setterKey", setName, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 673 | 674 | //通知值变化 675 | objc_setAssociatedObject(self, "observerKey", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 676 | 677 | //传进来的内容需要回传 678 | objc_setAssociatedObject(self, "contextKey", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 679 | } 680 | ``` 681 | 682 | >6.设置属性值的新调用方法 683 | 684 | ``` 685 | void setterMethod(id self, SEL _cmd, id newValue){ 686 | 687 | NSString *setterChar = objc_getAssociatedObject(self, "setterKey"); 688 | NSString *getterChar = objc_getAssociatedObject(self, "getterKey"); 689 | 690 | //保存子类类型 691 | Class clazz = [self class]; 692 | 693 | //isa 指向原类 694 | object_setClass(self, class_getSuperclass(clazz)); 695 | 696 | //调用原类get方法,获取oldValue 697 | id oldValue = objc_msgSend(self, NSSelectorFromString(getterChar)); 698 | 699 | //调用原类set方法 700 | objc_msgSend(self, NSSelectorFromString(setterChar),newValue); 701 | 702 | NSMutableDictionary *change = [[NSMutableDictionary alloc]init]; 703 | if (newValue) { 704 | change[NSKeyValueChangeNewKey] = newValue; 705 | } 706 | if (oldValue) { 707 | change[NSKeyValueChangeOldKey] = oldValue; 708 | } 709 | 710 | //原类观察者 711 | NSObject *observer = objc_getAssociatedObject(self, "observerKey"); 712 | 713 | //原类存储的上下文 714 | id context = objc_getAssociatedObject(self, "contextKey"); 715 | 716 | //调用observer的回调方法 717 | objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),getterChar,observer,change,context); 718 | 719 | //操作完成后指回动态创建的新类 720 | object_setClass(self, clazz); 721 | } 722 | 723 | ``` 724 | 725 | #### 通知实现原理 如何自定义通知 726 | 727 | 728 | 实现原理 729 | 730 | 自定义通知可以先创建一个Notification对象,将注册消息的observer、通知名name、通知触发的方法选择器等写入Notification,然后再创建一个NotificationCenter类单例,并且在单例内部创建一个数组,用来存储所有的Notification,每当需要注册通知时,就将需要注册的信息绑定到Notification对象上,放到单例全局数组中,在发送通知消息的时候,根据通知名遍历单例数组匹配对应的Notification,再通过IMP直接调用注册通知的对象的响应方法即可。[具体实现,请参考demo](https://github.com/LiuFuBo/iOSInterviewQuestions/blob/master/demo) 731 | 732 | 733 | 734 | 735 | > 创建Notification类,用于保存observer、通知名name,通知触发方法名、block回调等信息 736 | 737 | * 声明文件部分 738 | 739 | ``` 740 | @interface Notification : NSObject 741 | @property (nonatomic, strong, readwrite) NSDictionary *userInfo; 742 | @property (nonatomic, assign) id object; 743 | @property (nonatomic, assign) id observer; 744 | @property (nonatomic, copy) NSString *name; 745 | @property (nonatomic, copy) void(^callBack)(void); 746 | @property (nonatomic, assign) SEL aSelector; 747 | 748 | - (NSString *)name; 749 | - (id)object; 750 | - (NSDictionary *)userInfo; 751 | 752 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject; 753 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject userInfo:(NSDictionary *)aUserInfo; 754 | - (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo; 755 | 756 | 757 | @end 758 | 759 | ``` 760 | 761 | * 实现文件部分 762 | 763 | ``` 764 | @implementation Notification 765 | 766 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject { 767 | return [Notification notificationWithName:aname object:anObject userInfo:nil]; 768 | } 769 | 770 | + (instancetype)notificationWithName:(NSString *)aname object:(id)anObject userInfo:(NSDictionary *)aUserInfo { 771 | 772 | Notification *nofi = [[Notification alloc]init]; 773 | nofi.name = aname; 774 | nofi.object = anObject; 775 | nofi.userInfo = aUserInfo; 776 | return nofi; 777 | } 778 | 779 | - (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo { 780 | return [Notification notificationWithName:name object:object userInfo:userInfo]; 781 | } 782 | 783 | @end 784 | 785 | ``` 786 | 787 | 788 | 789 | 790 | > 创建NOtificationCenter类进行通知的管理,包括注册信息的数组保存,发送消息对象查找,以及IMP调用 791 | 792 | 793 | * 声明文件添加注册和发送通知两类方法 794 | 795 | ``` 796 | @interface NotificationCenter : NSObject 797 | 798 | + (instancetype)defaultCenter; 799 | - (void)addObserver:(id)observer callBack:(void(^)(void))callBack name:(NSString *)aName object:(id)anObject; 800 | - (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject; 801 | 802 | - (void)postNotification:(Notification *)notification; 803 | - (void)postNotificationName:(NSString *)aName object:(id)anObject; 804 | - (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo; 805 | 806 | @end 807 | 808 | ``` 809 | 810 | * 实现文件单例的初始化、数组的创建 811 | 812 | ``` 813 | @implementation NotificationCenter{ 814 | NSMutableArray *_nofiArray; 815 | } 816 | 817 | + (instancetype)defaultCenter { 818 | static NotificationCenter *_infoCenter = nil; 819 | static dispatch_once_t onceToken; 820 | dispatch_once(&onceToken, ^{ 821 | _infoCenter = [[NotificationCenter alloc]init]; 822 | }); 823 | return _infoCenter; 824 | } 825 | 826 | - (instancetype)init 827 | { 828 | self = [super init]; 829 | if (self) { 830 | _nofiArray = [[NSMutableArray alloc]init]; 831 | } 832 | return self; 833 | } 834 | 835 | ``` 836 | 837 | * 对通知消息进行注册,该步骤主要是将注册信息绑定到Notification对象,并存储到全局数组 838 | 839 | ``` 840 | - (void)addObserver:(id)observer selector:(SEL)aSelector callBack:(void (^)(void))callBack name:(NSString *)aName object:(id)anObject { 841 | 842 | Notification *nofi = [[Notification alloc]init]; 843 | nofi.callBack = callBack; 844 | nofi.name = aName; 845 | nofi.aSelector = aSelector; 846 | nofi.observer = observer; 847 | [_nofiArray addObject:nofi]; 848 | } 849 | 850 | - (void)addObserver:(id)observer callBack:(void (^)(void))callBack name:(NSString *)aName object:(id)anObject { 851 | [self addObserver:observer selector:nil callBack:callBack name:aName object:anObject]; 852 | } 853 | 854 | - (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject { 855 | [self addObserver:observer selector:aSelector callBack:nil name:aName object:anObject]; 856 | } 857 | 858 | ``` 859 | 860 | * 发送通知消息,采用遍历数组,通过通知名命中对应能响应该通知的对象,并调用其对象对应的消息响应方法或者block回调 861 | 862 | ``` 863 | - (void)postNotificationName:(NSString *)aName object:(id)anObject { 864 | 865 | for (Notification *nofi in _nofiArray) { 866 | if ([nofi.name isEqualToString:aName]) { 867 | 868 | nofi.object = anObject ? : anObject; 869 | 870 | if (nofi.callBack) { 871 | nofi.callBack(); 872 | } 873 | if (nofi.aSelector) { 874 | if ([nofi.observer respondsToSelector:nofi.aSelector]) { 875 | IMP imp = [nofi.observer methodForSelector:nofi.aSelector]; 876 | void(*func)(id, SEL,Notification *) = (void *)imp; 877 | func(nofi.observer,nofi.aSelector,nofi); 878 | } 879 | } 880 | } 881 | } 882 | } 883 | 884 | - (void)postNotification:(Notification *)notification { 885 | for (Notification *nofi in _nofiArray) { 886 | if ([nofi.name isEqualToString:notification.name]) { 887 | nofi.callBack = notification.callBack; 888 | nofi.object = notification.object; 889 | nofi.aSelector = notification.aSelector; 890 | nofi.observer = notification.observer; 891 | nofi.userInfo = notification.userInfo; 892 | break; 893 | } 894 | } 895 | [self postNotificationName:notification.name object:nil]; 896 | } 897 | 898 | - (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo { 899 | 900 | for (Notification *nofi in _nofiArray) { 901 | 902 | if ([nofi.name isEqualToString:aName]) { 903 | nofi.object = anObject; 904 | nofi.userInfo = aUserInfo; 905 | break; 906 | } 907 | } 908 | [self postNotificationName:aName object:nil]; 909 | } 910 | 911 | @end 912 | 913 | ``` 914 | 915 | #### 一个只读的属性 为什么不能实现KVO? 916 | 917 | >因为readonly对象没有setter方法,isa指向的派生类NSKVONotifying_XXX,也是readonly的,所以没有setter方法 918 | 919 | 920 | #### UIWebView和WKWebView的区别? 921 | 922 | WKWebView的优劣势 923 | 924 | >1.内存占用是UIWebView的20%-30%; 925 | >2.页面加载速度有提升,有的文章说它的加载速度比UIWebView提升了一倍左右。 926 | >3.更为细致地拆分了 UIWebViewDelegate 中的方法 927 | >4.自带进度条。不需要像UIWebView一样自己做假进度条(通过NJKWebViewProgress和双层代理技术实现),技术复杂度和代码量,根贴近实际加载进度优化好的多。 928 | >5.允许JavaScript的Nitro库加载并使用(UIWebView中限制) 929 | >6.可以和js直接互调函数,不像UIWebView需要第三方库WebViewJavascriptBridge来协助处理和js的交互。 930 | >7.不支持页面缓存,需要自己注入cookie,而UIWebView是自动注入cookie。 931 | >8.无法发送POST参数问题。 932 | 933 | 934 | #### 一张图片渲染到屏幕上经过了什么过程 935 | 936 | 对应应用来说,图片是最占用手机内存的资源,将一张图片从磁盘中加载出来,并最终显示到屏幕上,中间其实经过了一系列复杂的处理过程。 937 | 938 | 具体工作流程: 939 | 940 | >1、假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩; 941 | >2、然后将生成的 UIImage 赋值给 UIImageView ; 942 | >3、接着一个隐式的 CATransaction(Transactions是CoreAnimation的用于将多个layer tree操作批量化为渲染树的原子更新的机制。 对layer tree的每个修改都需要事务作为其一部分) 捕获到了 UIImageView 图层树的变化; 943 | >4、在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤: 944 | >分配内存缓冲区用于管理文件 IO(输入/输出) 和解压缩操作; 945 | >将文件数据从磁盘读到内存中; 946 | >将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作; 947 | >最后 Core Animation 中CALayer使用未压缩的位图数据渲染 UIImageView 的图层。 948 | 949 | 950 | 渲染流程: 951 | 952 | >GPU获取获取图片的坐标 953 | >将坐标交给顶点着色器(顶点计算) 954 | >将图片光栅化(获取图片对应屏幕上的像素点) 955 | >片元着色器计算(计算每个像素点的最终显示的颜色值) 956 | >从帧缓存区中渲染到屏幕上 957 | >我们提到了图片的解压缩是一个非常耗时的 CPU操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。 958 | 959 | 为什么要解压缩图片? 960 | 961 | 既然图片的解压缩需要消耗大量的 CPU 时间,那么我们为什么还要对图片进行解压缩呢?是否可以不经过解压缩,而直接将图片显示到屏幕上呢?答案是否定的。要想弄明白这个问题,我们首先需要知道什么是位图 962 | 其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用到的 JPEG 和 PNG 图片就是位图 963 | 964 | 例如: 965 | 966 | ``` 967 | UIImage *image = [UIImage imageNamed:@"file.png"]; 968 | CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage)); 969 | ``` 970 | 971 | 打印rawData,这里就是图片的原始数据. 972 | 事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。值得一提的是,在苹果的 SDK 中专门提供了两个函数用来生成 PNG 和 JPEG 图片: 973 | 974 | ``` 975 | // return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format 976 | UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image); 977 | 978 | // return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least) 979 | UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality); 980 | 981 | ``` 982 | 983 | 因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解压缩的原因。 984 | 985 | 986 | 解压缩原理 987 | 988 | 既然图片的解压缩不可避免,而我们也不想让它在主线程执行,影响我们应用的响应性,那么是否有比较好的解决方案呢? 989 | 我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。 990 | 而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate: 991 | 992 | 993 | ``` 994 | CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data, 995 | size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, 996 | CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo) 997 | CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0); 998 | 999 | ``` 1000 | 1001 | >data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可; 1002 | >width 和height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可; 1003 | >bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可; 1004 | >bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。当我们指定 0/NULL 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化; 1005 | >space :就是我们前面提到的颜色空间,一般使用 RGB 即可; 1006 | >bitmapInfo :位图的布局信息.kCGImageAlphaPremultipliedFirst; 1007 | 1008 | 1009 | 总结 1010 | 1011 | >1、图片文件只有在确认要显示时,CPU才会对齐进行解压缩.因为解压是非常消耗性能的事情.解压过的图片就不会重复解压,会缓存起来。 1012 | >2、图片渲染到屏幕的过程:读取文件->计算Frame->图片解码->解码后纹理图片位图数据通过数据总线交给GPU->GPU获取图片Frame->顶点变换计算->光栅化->根据纹理坐标获取每个像素点的颜色值(如果出现透明值需要将每个像素点的颜色透明度值)->渲染到帧缓存区->渲染到屏幕。 1013 | 1014 | 1015 | #### 离屏渲染是什么 什么场景会触发离屏渲染 都有什么具体的优化 1016 | 1017 | 首先,什么是离屏渲染? 1018 | 1019 | 离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作。 1020 | 1021 | 离屏渲染出发的场景有以下: 1022 | 1023 | >1.圆角 (maskToBounds并用才会触发) 1024 | >2.图层蒙版 1025 | >3.阴影 1026 | >4.光栅化 1027 | 1028 | 1029 | 为什么要避免离屏渲染? 1030 | 1031 | CPU、GPU 在绘制渲染视图时做了大量的工作。离屏渲染发生在 GPU 层面上,会创建新的渲染缓冲区,会触发 OpenGL的多通道渲染管线,图形上下文的切换会造成额外的开销,增加 GPU 工作量。如果 CPU GPU 累计耗时 16.67 毫秒还没有完成,就会造成卡顿掉帧。 1032 | 1033 | 圆角属性、蒙层遮罩 都会触发离屏渲染。指定了以上属性,标记了它在新的图形上下文中,在未愈合之前,不可以用于显示的时候就出发了离屏渲染。 1034 | 1035 | 在OpenGL中,GPU有2种渲染方式 1036 | 1037 | >On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作 1038 | >Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作 1039 | 1040 | 离屏渲染消耗性能的原因 1041 | 1042 | >需要创建新的缓冲区 1043 | >离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。 1044 | 1045 | 1046 | 哪些操作会触发离屏渲染? 1047 | 1048 | >光栅化,layer.shouldRasterize = YES 1049 | >遮罩,layer.mask 1050 | >圆角,同时设置 layer.masksToBounds = YES、layer.cornerRadius大于0 1051 | >考虑通过 CoreGraphics 绘制裁剪圆角,或者叫美工提供圆角图片阴影,layer.shadowXXX,如果设置了 layer.shadowPath 就不会产生离屏渲染 1052 | 1053 | 1054 | 什么是光栅化? 1055 | 1056 | 光栅化是将一个图元转变为一个二维图像的过程。二维图像上每个点都包含了颜色、深度和纹理数据。将该点和相关信息叫做一个片元,光栅化的目的,是找出一个几何单元(比如三角形)所覆盖的像素。你模型的那些顶点在经过各种矩阵变换后也仅仅是顶点。而由顶点构成的三角形要在屏幕上显示出来,除了需要三个顶点的信息以外,还需要确定构成这个三角形的所有像素的信息。光栅化会根据三角形顶点的位置,来确定需要多少个像素点才能构成这个三角形,以及每个像素点都应该得到哪些信息. 1057 | 1058 | 1059 | 1060 | #### LRU算法原理以及如何优化? 1061 | 1062 | LRU设计原理 1063 | 1064 | LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。 1065 | 1066 | 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下: 1067 | 1068 | ![image](http://brandonliu.pub/icon_blog_lru.png) 1069 | 1070 | 1071 | >1.新数据插入到链表头部; 1072 | >2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部; 1073 | >3.当链表满的时候,将链表尾部的数据丢弃。 1074 | 1075 | 命中率 1076 | 1077 | >当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。 1078 | 复杂度 1079 | >实现简单。 1080 | 代价 1081 | >命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。 1082 | 1083 | 1084 | LRU-K原理 1085 | 1086 | LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。 1087 | 1088 | 相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下: 1089 | 1090 | ![image](http://brandonliu.pub/icon_blog_lruK.png) 1091 | 1092 | 1093 | >1.数据第一次被访问,加入到访问历史列表; 1094 | >2.如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰; 1095 | >3.当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序; 1096 | >4.缓存数据队列中被再次访问后,重新排序; 1097 | >5.需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。 1098 | LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。 1099 | 1100 | 1101 | 命中率 1102 | >LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。 1103 | 1104 | 1105 | 复杂度 1106 | >LRU-K队列是一个优先级队列,算法复杂度和代价比较高。 1107 | 1108 | 1109 | 代价 1110 | >由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多;当数据量很大的时候,内存消耗会比较可观。LRU-K需要基于时间进行排序(可以需要淘汰时再排序,也可以即时排序),CPU消耗比LRU要高。 1111 | 1112 | 1113 | 关于 `NSDictionary + 双向链表 ` 实现LRU缓存淘汰算法方案 1114 | 1115 | 1116 | 度过YYCache的同学应该知道YYCache内部对于内存缓存部分就是采用 `NSDictionary + 双向链表` 来实现的,具体的实现思路如下: 1117 | 1118 | 1119 | 设计思路: 1120 | 1121 | 1122 | 使用 `NSDictionary` 存储 `key`,这样可以做到 Save 和 Get key的时间都是 O(1),而 NSDictionary 的 Value 指向双向链表实现的 `LRU` 的 `Node` 节点,利用空间hash_map的空间换来快速如图所示的访问; 1123 | 1124 | ![image](http://brandonliu.pub/icon_lru_cache.png) 1125 | 1126 | 1127 | LRU 存储是基于双向链表实现的。其中 `head` 代表双向链表的表头,`tail` 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。 1128 | 1129 | 关于存取实现细节如下: 1130 | 1131 | >1.存储数据,首先在NSDictionary找到Key对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。 1132 | 1133 | >2.获取数据,通过 NSDictionary 找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。 1134 | 1135 | demo样例: 1136 | 1137 | ``` 1138 | 1139 | struct DlinkdNode { 1140 | int key; 1141 | int val; 1142 | DlinkdNode* pre; 1143 | DlinkdNode* next; 1144 | 1145 | }; 1146 | 1147 | class LRUCache { 1148 | private: 1149 | unordered_mapcache; 1150 | int capacity; 1151 | int size; 1152 | DlinkdNode*head; 1153 | DlinkdNode*tail; 1154 | 1155 | //链表中添加节点 1156 | void addNode(DlinkdNode* node) 1157 | { 1158 | node->pre = head; 1159 | node->next = head->next; 1160 | head->next->pre = node; 1161 | head->next = node; 1162 | } 1163 | //链表中删除节点 1164 | void remove(DlinkdNode*node) 1165 | { 1166 | node->pre->next = node->next; 1167 | node->next->pre = node->pre; 1168 | //delete node;此处不能删除,节点的删除交给hash_map进行,否则后序hash_map无法访问此节点 1169 | } 1170 | //链表中将节点调制头结点,数据变为最热的数据 1171 | void moveTohead(DlinkdNode*node) 1172 | { 1173 | this->remove(node); 1174 | this->addNode(node); 1175 | } 1176 | 1177 | //删除链表尾节点 1178 | DlinkdNode* popTail() 1179 | { 1180 | DlinkdNode*res = tail->pre; 1181 | this->remove(res); 1182 | return res; 1183 | } 1184 | 1185 | 1186 | public: 1187 | LRUCache(int capacity) { 1188 | size = 0; 1189 | this->capacity = capacity; 1190 | 1191 | head = new DlinkdNode(); 1192 | head->pre = nullptr; 1193 | 1194 | tail = new DlinkdNode(); 1195 | tail->next = nullptr; 1196 | 1197 | head->next = tail; 1198 | tail->pre = head; 1199 | } 1200 | 1201 | int get(int key) { 1202 | unordered_map::iterator it = cache.find(key); 1203 | if (it == cache.end()) 1204 | return -1; 1205 | else { 1206 | moveTohead(it->second); 1207 | return it->second->val; 1208 | } 1209 | } 1210 | 1211 | 1212 | void put(int key, int value) { 1213 | unordered_map::iterator it = cache.find(key); 1214 | if (it == cache.end()) { 1215 | DlinkdNode*Node = new DlinkdNode(); 1216 | Node->key = key; 1217 | Node->val = value; 1218 | this->cache.insert({ key,Node }); 1219 | addNode(Node); 1220 | 1221 | ++size; 1222 | if (size > capacity)//超出容量将尾部最冷数据删除 1223 | { 1224 | DlinkdNode* TailNode = popTail(); 1225 | cache.erase(TailNode->key); 1226 | --size; 1227 | } 1228 | } 1229 | else { 1230 | it->second->val = value;//更新缓存key对应的val,并移动到链表头,最热 1231 | moveTohead(it->second); 1232 | } 1233 | 1234 | } 1235 | }; 1236 | 1237 | ``` 1238 | 1239 | 1240 | #### NSMutableArray是线程安全的么 如何创建线程安全的NSMutableArray 1241 | 1242 | >首先,NSMutableArray是线程不安全的,当有多个线程同时对数组进行操作的时候可能导致崩溃或数据错误 1243 | 1244 | >对数组的读写都加锁,虽然数组是线程安全了,但失去了多线程的优势 1245 | 1246 | > 然后又想可以只对写操作加锁然后定义一个全局变量来表示现在有没有写操作,如果有写操作就等写完了在读,那么问题来了如果一个线程先读取数据紧接着一个线程对数组写的操作,读的时候还没有加锁同样会导致崩溃或数据错误,这个方案pass掉. 1247 | 1248 | >第三种方案说之前先介绍一下dispatch_barrier_async,dispatch_barrier_async 追加到 并行队列 queue 中后,会等待并行队列 queue 中的任务都结束后,再执行 dispatch_barrier_async 的任务,等 dispatch_barrier_async 的任务结束后,才恢复任务执行, 用dispatch_async和dispatch_barrier_async结合保证NSMutableArray的线程安全,用dispatch_async读和dispatch_barrier_async写(add,remove,replace),当有任务在读的时候写操作会等到所有的读操作都结束了才会写,同样当有写任务时,读任务会等写操作完了才会读,既保证了线程安全又发挥了多线程的优势,但还是有个不足,当我们重写读的方法时dispatch_async是另开辟线程去执行的而且是立马返回的,所以我们不能拿到执行结果,需要去另写一个方法来返回读的结果,但是我们又不想改变调用者的习惯于是又想到了一下方案 1249 | 1250 | >用dispatch_sync和dispatch_barrier_async以及一个并行队列queue结合保证NSMutableArray的线程安全,dispatch_sync是在当前线程上执行不会另开辟新的线程,当线程返回的时候就可以拿到读取的结果,我认为这个方案是最完美的选择,既保证的线程安全有发挥了多线程的优势还不用另写方法返回结果. 1251 | 1252 | 1253 | 1254 | #### UITableView性能优化汇总 1255 | 1256 | 一、Cell重用 1257 | 1258 | 优化数据源方法: 1259 | 1260 | ``` 1261 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; 1262 | 1263 | ``` 1264 | 1265 | 在可⻅的⻚⾯会重复绘制⻚⾯,每次刷新显示都会去创建新的Cell,非常耗费性能。 1266 | 1267 | 解决方案: 创建⼀个静态变量reuseID(代理方法返回Cell会调用很多次,防⽌重复创建,static保证只会被创建⼀次,提⾼性能),然后,从缓存池中取相应 identifier的Cell并更新数据,如果没有,才开始alloc新的Cell,并用identifier标识 Cell。每个Cell都会注册⼀个identifier(重用标识符)放⼊入缓存池,当需要调⽤的时候就直接从缓存池里找对应的id,当不需要时就放入缓存池等待调⽤。(移出屏幕的Cell才会放⼊入缓存池中,并不会被release) 1268 | 1269 | 优化方案如下: 1270 | 1271 | ``` 1272 | static NSString *reuseID = “reuseCellID”; 1273 | // 缓存池中取已经创建的cell 1274 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseID]; 1275 | 1276 | ``` 1277 | 1278 | 缓存池内部原理: 1279 | 1280 | 当Cell要alloc时,UITableView会在堆中开辟一段内存以供Cell缓存之用。Cell的重 用通过identifier标识不同类型的Cell,由此可以推断出,缓存池外层可能是⼀个可变字典,通过key来取出内部的Cell,而缓存池为存储不同高度、不同类型(包含图片、 Label等)的Cell,可以推断出缓存池的字典内部可能是一个可变数组,用来存放不同类型的Cell,缓存池中只会保存已经被移出屏幕的不同类型的Cell。 1281 | 1282 | 1283 | 缓存池获取可重用Cell的两个方法区别 1284 | 1285 | ``` 1286 | -(nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier: (NSString *)identifier; 1287 | 这个⽅法会查询可重用Cell,如果注册了原型Cell,能够查询到,否则,返回nil;⽽且需要判断if(cell == nil),才会创建Cell,不推荐 1288 | -(__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0); 1289 | 使⽤这个⽅法之前,必须通过xib(storyboard)或是Class(纯代码)注册可重用 Cell,而且这个方法⼀定会返回一个Cell 1290 | //注册Cell 1291 | - (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString 1292 | *)identifier NS_AVAILABLE_IOS(5_0); 1293 | - (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0); 1294 | 1295 | ``` 1296 | 1297 | 二、在同一个UITableView中尽量少定义Cell类型,并且需要善用hidden隐藏或者显示subviews 1298 | 1299 | 1300 | 尽量少的定义Cell类型 1301 | 1302 | 分析Cell结构,尽可能的将 相同内容的抽取到⼀种样式Cell中,前面已经提到了Cell的重⽤机制,这样就能保证UITbaleView要显示多少内容,真正创建出的Cell可能 只比屏幕显示的Cell多⼀点。虽然Cell的’体积’可能会大点,但是因为Cell的数量不会很多,完全可以接受的。 1303 | 1304 | 好处: 1305 | >减少代码量,减少Nib⽂件的数量,统⼀一个Nib文件定义Cell,容易修改、维护。 1306 | >基于Cell的重⽤,真正运⾏时铺满屏幕所需的Cell数量⼤致是固定的,设为N个。所以如果如果只有一种Cell,那就是只有N个Cell的实例;但是如果有M种Cell,那么运行时最多可能会是“M x N = MN”个Cell的实例例,虽然可能并不会占⽤用太多内存,但是能少点不是更好吗。 1307 | 1308 | 1309 | 善用hidden隐藏或显示subviews 1310 | 1311 | 只定义⼀种Cell,那该如何显示不同类型的内容呢?答案就是,把所有不同类型的view都定义好,放在cell⾥⾯,通过hidden显示、隐藏,来显示不同类型的内容。毕竟,在⽤户快速滑动中,只是单纯的显示、隐藏subview比实时创建要快得多。 1312 | 1313 | 1314 | 三、提前计算并缓存Cell的⾼高度 1315 | 1316 | 在iOS中,不设UITableViewCell的预估行高的情况下,会优先调用 ”tableView:heightForRowAtIndexPath:”⽅法,获取每个Cell的即将显示的高度, 从而确定UITableView的布局,实际就是要获取contentSize(UITableView继承⾃UIScrollView,只有获取滚动区域,才能实现滚动),然后才调用”tableView:cellForRowAtIndexPath”,获取每个Cell,进⾏赋值。如果项⽬中模块有 10000个Cell需要显示,可想⽽知. 1317 | 1318 | 1319 | 解决方案如下: 1320 | 1321 | 可以将计算Cell的⾼高度放⼊入数据模型,但这与MVC设计模式可能稍微有点冲突,这个时候我就想到MVVM这种设计模式,这个时候才能稍微有点MVVM这种设计模式的优点,可 以讲计算Cell⾼高度放入ViewModel(视图模型)中,让Model(数据模型)只负责处理数据。 1322 | 1323 | 1324 | 四、异步绘制(自定义Cell绘制) 1325 | 1326 | 遇到⽐较复杂的界面的时候,如复杂点的图文混排采用异步绘制Cell 1327 | 1328 | 1329 | 五、滑动时,按需加载 1330 | 1331 | 开发的过程中自定义Cell的种类千奇百怪,但Cell本来就是⽤来显示数据的,不说100%带有图片,也差不多,这个时候就要考虑,下滑的过程中可能会有点卡顿,尤其网络不好的时候,异步加载图片是个程序员都会想到,但是如果给每个循环对象都加上异步加载,开启的线程太多,⼀样会卡顿,我记得好像线程条数⼀般3-5条,最多也就6条吧。 1332 | 1333 | 解决方案: 1334 | 1335 | cell每次被渲染时,判断当前tableView是否处于滚动状态,是的话,不加载图片,cell 滚动结束的时候,获取当前界面内可见的所有cell. 1336 | ``` 1337 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 1338 | 1339 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; 1340 | if (!cell) { 1341 | cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; 1342 | } 1343 | 1344 | DemoModel *model = self.datas[indexPath.row]; 1345 | cell.textLabel.text = model.text; 1346 | 1347 | //不在直接让cell.imageView loadYYWebImage 1348 | if (model.iconImage) { 1349 | cell.imageView.image = model.iconImage; 1350 | }else{ 1351 | cell.imageView.image = [UIImage imageNamed:@"placeholder"]; 1352 | /** 1353 | runloop - 滚动时候 - trackingMode, 1354 | - 默认情况 - defaultRunLoopMode 1355 | ==> 滚动的时候,进入`trackingMode`,defaultMode下的任务会暂停 1356 | 停止滚动的时候 - 进入`defaultMode` - 继续执行`trackingMode`下的任务 - 例如这里的loadImage 1357 | */ 1358 | [self performSelector:@selector(p_loadImgeWithIndexPath:) 1359 | withObject:indexPath 1360 | afterDelay:0.0 1361 | inModes:@[NSDefaultRunLoopMode]]; 1362 | } 1363 | } 1364 | 1365 | //下载图片,并渲染到cell上显示 1366 | - (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{ 1367 | 1368 | DemoModel *model = self.datas[indexPath.row]; 1369 | UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; 1370 | 1371 | [ImageDownload loadImageWithModel:model success:^{ 1372 | //主线程刷新UI 1373 | dispatch_async(dispatch_get_main_queue(), ^{ 1374 | cell.imageView.image = model.iconImage; 1375 | }); 1376 | }]; 1377 | } 1378 | 1379 | ``` 1380 | 1381 | 六、避免⼤量的图片缩放、颜⾊渐变等,尽量显示“大⼩刚好合适的图片资源 1382 | 1383 | 七、避免同步的从网络、文件获取数据,Cell内实现的内容来自web,使用异步加载,缓存请求结果 1384 | 1385 | 1386 | 八、减少渲染复杂度 1387 | 1388 | 解决方案如下: 1389 | 1390 | >减少subviews的个数和层级,子控件的层级越深,渲染到屏幕上所需要的计算量就越大;如多用drawRect绘制元素,替代⽤view显示. 1391 | 1392 | >少⽤用subviews的半透明图层,不透明的View,设置opaque为YES,这样在绘制该View时,就不需要考虑被View覆盖的其他内容(尽量设置Cell的view为opaque,避免GPU对Cell下⾯的内容 也进行绘制) 1393 | 1394 | >避免CALayer特效(shadowPath) 给Cell中View加阴影会引起性能问题,如下面代码会导致滚动时有明显的卡顿: 1395 | 1396 | 1397 | #### 有两个视图A/B A有一部分内容覆盖在B上面 如果点击A让B视图响应 1398 | 1399 | 1400 | 对于这个问题,仅仅需要按照以下方法 1401 | 1402 | ``` 1403 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 1404 | { 1405 | UIView *view = [super hitTest:point withEvent:event]; 1406 | if (view == nil) { 1407 | // 转换坐标系 1408 | CGPoint newPoint = [self.subButton convertPoint:point fromView:self]; 1409 | // 判断触摸点是否在button上 1410 | if (CGRectContainsPoint(self.A, newPoint)) { 1411 | view = self.B; 1412 | } 1413 | } 1414 | return view; 1415 | } 1416 | 1417 | ``` 1418 | 1419 | 从上面可以看到,仅仅只需要在里面加一个判断,当点击的点在A视图里面,则将响应对象返回为B,即可实现该功能。 1420 | 1421 | 1422 | #### 折半查找为何不能用于存储结构是链式的有序查找表 1423 | 1424 | > 这是由链表的特性决定的,链表是典型的顺序存取结构,数据在链表中的位置只能通过从头到尾的顺序检索得到,即使是有序的,要操作其中的某个数据也必须从头开始。这和数组有本质的区别。数组中的元素是通过下标来确定的,只要知道了下标,就可以直接存储真个元素,比如a[0]是可以直接存取的,链表没有这个,所以折半查找只能在数组上进行。 1425 | 1426 | 1427 | 1428 | #### 同步 异步与串行 并行的关系 1429 | 1430 | > 这块知识在我的简书博客[这篇文章](https://www.jianshu.com/p/29f1cee0aef7)中已经阐述的很清楚了,有不明白的同学欢迎前往自取。 1431 | 1432 | 1433 | #### 类目为何不能添加属性 1434 | 1435 | > 首先咱们的类目并不会生成setter和getter方法,其次类目没有成员变量列表。 1436 | 1437 | 1438 | #### 字典一般是用字符串来当做Key的 可以用对象来做key么 要怎么做 1439 | 1440 | 字典是可以用对象来做key的,不过需要满足几个条件 1441 | > 1.NSDictionary的key实际是使用了copy方法,所以key必须要遵守NSCopying协议 1442 | > 2.实现 ‘copyWithZone:’ 方法 1443 | > 3.必须要保证 'copyWithZone: ' 返回对象为同一个地址对象,如果不能保证,则必须重写hashCode和isEqual方法,用来保证让存进去的对象key的地址不发生变化。 1444 | 1445 | 1446 | #### 苹果公司为什么要设计元类 1447 | 1448 | > 这个问题是个开放性的问题。不同阶段的解读也会不一样。因为这个知识点大家都会去学,主要是希望能够通过面试者的回答,加上追问方式来看他是不是会带着思考去学习。比如元类和类的结构体非常类似,他有没有想过为什么不合在一起用一个结构体?(结构体设计能力)元类和类创建的时机是不是一样的,为什么?(用过 runtime 接口开发没)元类的 flag 字段里记录了什么?(是否有深入探究的意识)。 1449 | 1450 | * 个人认为元类和类的数据结构是同一个,只是运行时使用的字段不一样。实例方法调用是通过objc_msgSend来调用,它的第一个入参就是实例对象,其流程是查找实例对象的isa指针,找到类对象,然后找到method_t的IMP,bl直接跳转调用。 1451 | * 类方法的调用和实例方法调用一致,它的第一个入参对象是类对象,类对象的isa指向的是元类。 1452 | * 所以,没有元类的话,类方法是没有办法调用的。objc_msgSend的调用流程是一定要isa指针的。 1453 | * 如果实例方法和类方法都放在类对象上,那类对象的isa指针只能指向自己了,那一旦类方法和实例方法重名,就没法搞了! 1454 | 1455 | 1456 | #### 结构体和联合区的区别 1457 | 1458 | * 联合体 (union) 1459 | > 各成员变量共用同一块内存空间,并且同事只有一个成员变量可以得到这块内存的使用权,当联合体对不同成员变量赋值,就会对其他成员重写,原来成员的值就不存在了。一个联合体变量的总长度等于最长的成员长度。 1460 | 1461 | * 结构体 (struct) 1462 | > 各成员变量拥有各自的内存,互不干涉,遵守内存对齐原则,一个结构体的总长度等于所有成员的长度之和。 1463 | 1464 | 1465 | #### CALayer和UIView的区别 1466 | * UIView可以响应时间,CALayer却不能,UIView继承自UIResponder,UIResponder定义了很多处理各种事件和事件传递的接口,而CALayer直接继承自NSObject,所以并没有响应的处理事件的接口。 1467 | * UIView的frame仅简单通过CALayer的frame来显示,但是CALayer的frame同时由anchorPoint,position,bounds和transform共同决定。 1468 | * UIView主要负责对显示内容的管理而CALayer主要侧重于显示内容的绘制。 1469 | 1470 | 1471 | #### 一个NSObject对象占用多少内存 1472 | 1473 | 首先回答问题: 1474 | * 系统分配了16个字节给NSObject对象 (通过malloc_size 函数获得) 1475 | * 但NSObject对象内部只使用了8个字节的空间 (64bit环境下,可以通过class_getInstanceSize函数获得) 1476 | 1477 | 为什么是8个字节呢? 1478 | 我们打开NSObject类进去查看实现: 1479 | ``` 1480 | @interface NSObject { 1481 | 1482 | Class isa OBJC_ISA_AVAILABILITY; 1483 | } 1484 | ``` 1485 | NSObject包含一个结构体指针。在64位系统上指针占用8个字节(32位系统4个字节),所以 NSObject 在64位上占用 8 字节。 1486 | 1487 | 为何又是16字节? 1488 | 但是在前文,我们通过 malloc_size 发现系统输出的内存大小又是 16 字节。这又是为何呢?通过 objc4 源码,我们可以追踪下 NSObject 的 alloc 的过程。 1489 | ``` 1490 | alloc -> _objc_rootAlloc -> callAlloc -> class_createInstance -> _class_createInstanceFromZone 1491 | ``` 1492 | 在_class_createInstanceFromZone函数中实现如下: 1493 | 1494 | ``` 1495 | _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 1496 | bool cxxConstruct = true, 1497 | size_t *outAllocatedSize = nil) 1498 | { 1499 | if (!cls) return nil; 1500 | 1501 | assert(cls->isRealized()); 1502 | 1503 | // Read class's info bits all at once for performance 1504 | bool hasCxxCtor = cls->hasCxxCtor(); 1505 | bool hasCxxDtor = cls->hasCxxDtor(); 1506 | bool fast = cls->canAllocNonpointer(); 1507 | 1508 | size_t size = cls->instanceSize(extraBytes); 1509 | if (outAllocatedSize) *outAllocatedSize = size; 1510 | 1511 | id obj; 1512 | if (!zone && fast) { 1513 | obj = (id)calloc(1, size); 1514 | if (!obj) return nil; 1515 | obj->initInstanceIsa(cls, hasCxxDtor); 1516 | } 1517 | else { 1518 | if (zone) { 1519 | obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size); 1520 | } else { 1521 | obj = (id)calloc(1, size); 1522 | } 1523 | if (!obj) return nil; 1524 | 1525 | // Use raw pointer isa on the assumption that they might be 1526 | // doing something weird with the zone or RR. 1527 | obj->initIsa(cls); 1528 | } 1529 | 1530 | if (cxxConstruct && hasCxxCtor) { 1531 | obj = _objc_constructOrFree(obj, cls); 1532 | } 1533 | 1534 | return obj; 1535 | } 1536 | 1537 | ``` 1538 | 这里我们重点关注 size_t size = cls->instanceSize(extraBytes); 这个函数调用计算需要分配空间的大小。 1539 | 1540 | ``` 1541 | size_t instanceSize(size_t extraBytes) { 1542 | size_t size = alignedInstanceSize() + extraBytes; 1543 | // CF requires all objects be at least 16 bytes. 1544 | if (size < 16) size = 16; 1545 | return size; 1546 | } 1547 | 1548 | ``` 1549 | 可以看到。系统会给小于16字节的对象也会分配 16 字节的内存大小。 所以在前文中 malloc_size 的结果是 16,相当于OC中的对象最小内存空间大小是16字节。 1550 | 1551 | > 注意:在instanceSize函数中 alignedInstanceSize 会对结构体进行字节对齐的计算。 1552 | 1553 | ``` 1554 | #ifdef __LP64__ 1555 | # define WORD_MASK 7UL 1556 | #else 1557 | # define WORD_MASK 3UL 1558 | #end 1559 | 1560 | static inline size_t word_align(size_t x) { 1561 | return (x + WORD_MASK) & ~WORD_MASK; 1562 | } 1563 | 1564 | /** 1565 | 如: x = 17; 即(17 + 7 & ~7) 1566 | 1567 | 0001 0001 1568 | + 0000 0111 1569 | ------------ 1570 | 0001 1000// 24 1571 | & 1111 1000 // ~7 1572 | ------------ 1573 | 0001 1000 // 24 1574 | 1575 | 实际上需要 17 字节的大小,但系统会自动字节对齐到 24 字节。 1576 | */ 1577 | 1578 | ``` 1579 | 除此之外,libmalloc源码中查看到 malloc_zone_calloc / calloc源代码,系统还会进行一次内存对齐的操作,以 16 字节的倍数分配内存大小。 1580 | 1581 | ``` 1582 | #define SHIFT_NANO_QUANTUM 4 1583 | #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16 1584 | 1585 | static MALLOC_INLINE size_t 1586 | segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey) 1587 | { 1588 | size_t k, slot_bytes; 1589 | 1590 | // 最小 16 字节 1591 | if (0 == size) { 1592 | size = NANO_REGIME_QUANTA_SIZE; // Historical behavior 1593 | } 1594 | // 确保内存大小为 16 的倍数 1595 | k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta 1596 | slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size 1597 | *pKey = k - 1; // Zero-based! 1598 | 1599 | return slot_bytes; 1600 | } 1601 | 1602 | /** 1603 | 1604 | 如果size 为 24 1605 | (24 + 16 - 1) >> 4 1606 | 39 >> 4 1607 | 1608 | 0010 0101 >> 4 = 0000 0010 1609 | 0000 0010 << 4 = 0010 0000 // 32 1610 | 最终分配了 32 字节的内存。 1611 | */ 1612 | 1613 | ``` 1614 | 所以,最终iOS分配的内存大小会是16的倍数大小。 1615 | 1616 | 1617 | #### 通过self和super两种方式分别调用class方法-得到结果是啥-调用区别是什么 1618 | 1619 | ``` 1620 | @interface TMMeditor : NSObject 1621 | 1622 | 1623 | @end 1624 | 1625 | @implementation TMMeditor 1626 | 1627 | - (instancetype)init 1628 | { 1629 | self = [super init]; 1630 | if (self) { 1631 | NSLog(@"%@",NSStringFromClass([self class])); 1632 | NSLog(@"%@",NSStringFromClass([super class])); 1633 | } 1634 | return self; 1635 | } 1636 | ``` 1637 | * 第一行打印结果是TMMeditor。[self class] isa指针会先从当前类调用class方法,消息接收者是self,所以得到类名TMMeditor。 1638 | * 第二行打印结果也是TMMeditor。[super class] isa指针会去先去super父类中调用class方法,但是消息接收者都是self,所以得到TMMeditor类名。 1639 | 1640 | 1641 | #### 如何监听非UI主线程添加视图 1642 | 当前视图如果加入了新的视图,如果当前类重写了didAddSubView:方法,则会自动调用该方法,咱们可以在该方法中打印当前线程,从而判断是否是在非主线程进行UI操作。 1643 | 1644 | 1645 | ### 假如给你一张50M的本地图片-你如何保证图片能够流畅的在手机上显示出来-并且内存不暴涨 1646 | 1647 | 首先,这里可以对图片先进行切片,再采用多线程对各个切片进行解码,转交给GPU渲染以后,回到主线程显示,其次还可以将最终得到的完整图片进行一次无损压缩。 1648 | 1649 | 1650 | #### 修改成员变量会触发KVO吗-KVC会触发KVO吗 1651 | 直接修改成员变量不会触发KVO,因为他不会触发setter方法,KVC方法能够触发KVO,KVC在赋值的时候会进行方法调用,首先调用_setKey,setKey,如果没有setter方法,则会内部设置属性key或者下划线key。 1652 | 1653 | 1654 | #### NSTimer计时不准的原因 1655 | 导致NSTimer计时不准的原因主要有以下几点: 1656 | * NSTimer实例是被加入到当前RunLoop中的,模式为NSDefaultRunLoopModel,RunLoop模式如果在滑动等情况下可能会切换模式,导致计时器暂停。 1657 | * NSTimer因为加入的是应用程序的 Main RunLoop ,这个RunLoop负责了所有主线程事件,这其中包括了UI界面的各种事件。当主线程进行复杂运算、或者进行UI界面操作,因为它是同步交付的,会被阻塞。 1658 | 1659 | 解决这种误差的方法有以下几种: 1660 | * 在子线程中进行NSTimer的操作,再在主线程中修改UI界面显示操作结果。 1661 | * 在主线程中进行 NSTimer操作,但是将NSTimer实例加到main runloop的特定mode(模式)中。避免被复杂运算操作或者UI界面刷新所干扰。 1662 | 1663 | 1664 | #### 线程池的作用有哪些 1665 | * 复用线程:通过重复利用现有线程来执行任务,避免了多次创建和销毁线程。 1666 | * 提升执行速度:因为省去了创先线程这个步骤,所以拿到任务,可以立刻执行。 1667 | * 拓展性:线程池的可拓展性使得我们可以自己加入新的功能,比如定时、延时来执行某些线程任务。 1668 | 1669 | 1670 | #### 什么是函数式编程 1671 | * 用函数的思想解决问题,而不是对象的思想。对于函数来说就只有(入参 和 返回值)那么把一个复杂的问题,拆解为多个小问题,并用函数计算的方式,去解决。 1672 | 1673 | 1674 | #### WKWebView和JS交互的方式有哪些 1675 | 系统原生方式调用: 1676 | ``` 1677 | // 1. webview调用JS函数, JS代码可根据需要拼接好。 1678 | NSString *JSFunc = xxx; 1679 | [self.webView evaluateJavaScript:JSFunc completionHandler:^(id _Nullable result, NSError * _Nullable error) { 1680 | NSLog(@"evaluateJavaScript:\n result = %@ error = %@",result, error); 1681 | }]; 1682 | 1683 | // 2. 网页JS调原生: 1684 | // 1> 需要先设置Webview. config 的WKUserContentController 1685 | // 2> 注册方法名 [userCC addScriptMessageHandler:self name:]; 1686 | // 3> 遵守协议,实现其方法. 1687 | // 4> 在控制器销毁时,需要移除方法名注册 1688 | WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; 1689 | WKUserContentController *userCC = [WKUserContentController new]; 1690 | config.userContentController = userCC; 1691 | //JS调用OC 添加处理脚本 1692 | [userCC addScriptMessageHandler:self name:JSMessageName_Register]; 1693 | WKWebView *webView = [[WKWebView alloc] initWithFrame:webFrame configuration:config]; 1694 | 1695 | #pragma mark - WKScriptMessageHandler 1696 | - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { 1697 | if ([message.name isEqualToString:JSMessageName_Register]) { 1698 | RegisterViewController *controller = [RegisterViewController new]; 1699 | [self.navigationController pushViewController:controller animated:YES]; 1700 | } 1701 | } 1702 | - (void)dealloc 1703 | { 1704 | WKUserContentController *userCC = self.webView.configuration.userContentController; 1705 | [userCC removeScriptMessageHandlerForName:JSMessageName_Register]; // 不移除,怕是会造成webview无法释放 1706 | } 1707 | 1708 | ``` 1709 | 1710 | 采用桥接方式调用: 1711 | ``` 1712 | // 1.JS调用原生:假设方法名是getUserInfo 1713 | [_bridge registerHandler:@"getUserInfo" handler:^(id data, WVJBResponseCallback responseCallback) { 1714 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 1715 | dict[@"custName"] = [UserModel shareUserModel].custName; 1716 | dict[@"idCard"] = [UserModel shareUserModel].certifino; 1717 | responseCallback([JSONKit jsonStringWithObject:dict]); 1718 | }]; 1719 | 1720 | // 2. webview调用JS: 假设JS的方法名是redirect 1721 | [_bridge callHandler:@"redirect" data:@"test" responseCallback:^(id responseData) { 1722 | NSLog(@"%@", responseData); 1723 | }]; 1724 | 1725 | ``` 1726 | 通过方法名与对应的操作放在一起的方式更有利于维护;除此之外,这个提供了一份JS模板文件,安卓端也可以是同样的原理实现。 1727 | 1728 | 1729 | 1730 | 1731 | 1732 | 1733 | 1734 | 1735 | 1736 | 1737 | 1738 | 1739 | 1740 | 1741 | 1742 | 1743 | 1744 | 1745 | --------------------------------------------------------------------------------