Showing Posts From

Ios

CocoaPods对三方库的管理探究

CocoaPods是iOS开发中经常被用到的第三方库管理工具,我们有必要深入了解一下它对项目产生了什么影响,以及它是如何管理这些库的。使用pod安装三方库 我们新建一个不带测试模块的名为FFDemo的Swift项目,它的目录结构是这样的 ├── FFDemo │   ├── AppDelegate.swift │   ├── Assets.xcassets │   ├── Base.lproj │   ├── Info.plist │   ├── SceneDelegate.swift │   └── ViewController.swift └── FFDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace └── xcuserdata然后我们执行pod init创建一个Podfile模板,在里面引入这两个三方库: target 'FFDemo' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for FFDemo pod 'MJRefresh', '~> 3.5.0' pod 'Moya'end成功执行pod install之后我们就将这两个库引入到了项目,这时项目目录变成了这样: ├── FFDemo │   ├── AppDelegate.swift │   ├── Assets.xcassets │   ├── Base.lproj │   ├── Info.plist │   ├── SceneDelegate.swift │   └── ViewController.swift ├── FFDemo.xcodeproj │   ├── project.pbxproj │   ├── project.xcworkspace │   └── xcuserdata ├── FFDemo.xcworkspace │   └── contents.xcworkspacedata ├── Podfile ├── Podfile.lock └── Pods ├── Alamofire ├── Headers ├── Local\ Podspecs ├── MJRefresh ├── Manifest.lock ├── Moya ├── Pods.xcodeproj └── Target\ Support\ Files从目录看,除了pod init引入了Podfile,其余三部分内容:FFDemo.xcworkspace、Podfile.lock、Pods目录都是由pod install之后生成的。我们下面重点讲下这三部分内容。 CocoaPods安装的内容 xcworkspace文件 该文件下包含一个叫contents.xcworkspacedata的文件,它的内容是这样的: <?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "group:FFDemo.xcodeproj"> </FileRef> <FileRef location = "group:Pods/Pods.xcodeproj"> </FileRef> </Workspace>使用xml格式将依赖包含在标签内。 xcworkspace是一个项目容器,当有多个project需要相互依赖时可以用xcworkspace将它们组织起来。pod在首次安装三方库时会生成一个叫Pods.xcodeproj的project管理三方库,然后将该project和主项目的project通过workspace进行关联。这样我们就可以在主工程里引入三方库了,而且三方库由Pods.xcodeproj统一管理,不会对我们原项目产生任何干扰。 Podfile.lock Podfile.lock文件的内容是这样的: PODS: - Alamofire (5.3.0) - MJRefresh (3.5.0) - Moya (14.0.0): - Moya/Core (= 14.0.0) - Moya/Core (14.0.0): - Alamofire (~> 5.0)DEPENDENCIES: - MJRefresh (~> 3.5.0) - MoyaSPEC REPOS: trunk: - Alamofire - MJRefresh - MoyaSPEC CHECKSUMS: Alamofire: 2c792affbdc2f18016e08fdbcacd60aebe1ba593 MJRefresh: 6afc955813966afb08305477dd7a0d9ad5e79a16 Moya: 5b45dacb75adb009f97fde91c204c1e565d31916PODFILE CHECKSUM: 073f3d6d9f03e6a76838ca3719df48ae6cc01450COCOAPODS: 1.9.3因为Podfile文件里可以不指定版本号,而版本信息又很重要,于是就有了Podfile.lock,它里面记录完整的版本信息和依赖关系。它的内容包含以下几大块 PODS PODS是指当前引用库的具体版本号,可以发现我们并没有引入Alamofire,但在PODS里确有它。这是因为Moya中依赖了它,Moya里定义了一个subspec叫Core,这是Moya/Core写法的由来。pod是通过各个库的podspec文件找到对应依赖的,这里可以简单看下Moya的部分podspeec文件内容Moya.podspec: Pod::Spec.new do |s| s.default_subspecs = "Core" s.subspec "Core" do |ss| ss.source_files = "Sources/Moya/", "Sources/Moya/Plugins/" ss.dependency "Alamofire", "~> 5.0" ss.framework = "Foundation" end endDEPENDENCIES DEPENDENCIES为pod库的描述信息,这里内容是同Podfile里的写法。因为我们指定了MJRefresh的版本号,并没有指定Moya的版本号,所以这里内容也是一样的。 SPEC REPOS 这里描述的是仓库信息,即安装了哪些三方库,他们来自于哪个仓库。 trunk是共有仓库的名称,它的地址是https://github.com/CocoaPods/Specs.git,外部使用的三方库大都来自于这里。通常我们还会依赖一些公司内部的私有库,私有库的信息也会显示在这里。 SPEC CHECKSUM 这里描述的是各个三方库的校验和,校验和的算法是对当前安装版本的三方库的podspec文件求SHA1。比如MJRefresh的校验和:6afc955813966afb08305477dd7a0d9ad5e79a16。我们安装的MJRefresh的版本为3.5.0,它在本地的podspec文件路径为:~/.cocoapods/repos/trunk/Specs/0/f/b/MJRefresh/3.5.0/MJRefresh.podspec.json。 这个路径可以通过在安装库时增加 --verbose参数在输出日志里查看。我们对该文件内容通过openssl求sha1摘要: $ pod ipc spec ~/.cocoapods/repos/trunk/Specs/0/f/b/MJRefresh/3.5.0/MJRefresh.podspec.json | openssl sha1 $ 6afc955813966afb08305477dd7a0d9ad5e79a16因为是对podspec.json内容求sha1,所以只要内容发生一点变化,得出的校验和就将大不相同,而这也是校验和设计的目的:podspec文件发生变化意味着版本信息发生了变化,就需要重新同步代码。 大家可能注意到了,我们通常制作私有pod,控制配置信息的文件是podspec格式的,为什么本地文件变成了json格式? 这是因为json格式兼容性更高也更容易批量处理,官方Spec仓库的所有库配置文件都是被转成json格式的。在我们制作私有库的时候是可以直接以podspec的格式推到远程仓库的,但后续解析文件时pod内部检索还是会把它转成json格式。上面的命令是包含了podsepc转json的命令的,转json命令如下: $ pod ipc spec ModuleName.podspecPODFILE CHECKSUM 这个校验和是针对Podfile内容的校验和,如果Podfile内容改变了,该值也会跟着改变。计算方法为: $ openssl sha1 filePath/PodfileCOCOAPODS: 1.9.3 这个代表当前使用的CocoaPod版本号,远程版本管理应该要保证大家使用的pod版本号一致。 Pods Manifest.lock Manifest.lock是Podfile.lock的副本,它是在Pods目录里面。它的作用是这样的,我们通常是不把Pods文件放到版本管理里面,而把Podfile.lock放到版本管理里面。这时对于拉取代码之后是否需要更新pod,就可以通过对比本地的Manifest.lock和远程Podfile.lock是否相同即可。 Targets Support Files Pods安装的依赖是这样的组织形式一个Pods的Project下面有三个Targets,其中三个是安装的依赖库,最后一个Pods-FFDemo是关联三个库的Framework,也即是Pods这个Project的Targets。 Pods-Demo Framework 先看这个Demo的Framework,它会被用于工程项目的引用依赖这个库不会被打进包里,因为Do Not Embed代表并不是包含的关系。 这个工程下的配置文件有这些:许可协议文件 两个以acknowledgements命名的文件是用于管理pod库的许可协议,即三方库必须带有的LICENSE文件,这也是为什么我们在制作pod时会要求我们指定软件协议。 Framework文件 这里还包含了用于管理Module的modulemap和umbrella.h文件。modulemap是对Module的声明文件,制作Framework我们总是需要该文件,它的内容如下: framework module Pods_FFDemo { umbrella header "Pods-FFDemo-umbrella.h" export * module * { export * } }其指向了一个umbrella的头文件,这是制作Framework必须的头文件,modulemap和umbrella.h会在创建Module时自动生成,不建议手动修改其关系。 dummy.m文件 这其实是一个空的.m文件 #import <Foundation/Foundation.h> @interface PodsDummy_Pods_FFDemo : NSObject @end @implementation PodsDummy_Pods_FFDemo @end那为什么要有这个东西呢,包括所有的三方库的包里也会包含一个dummy文件。我在stackoverflow找到了一个解释:Xcode的编译是依赖.m文件的,如果一个库里没有.m文件,将不会被编译,为了防止这种情况就会在每个库里增加一个空的.m文件。 xcconfig文件 xcconfig文件是Build Setting配置项的文件形式,它的优先级大于Xcode内的Build Setting。看一个pod生成的debug模式下的xcconfig文件。 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/Moya" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Moya/Moya.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' OTHER_LDFLAGS = $(inherited) -framework "Alamofire" -framework "CFNetwork" -framework "Foundation" -framework "MJRefresh" -framework "Moya" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. PODS_ROOT = ${SRCROOT}/Pods USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YESxcconfig还有个作用是设置参数,比如我们比较熟悉的PODS_ROOT=${SRCROOT}/PODS,它代表项目根目录下的PODS文件目录。另外两项用于帮助我们在项目中查找三方库的FRAMEWORK_SEARCH_PATHS和HEADER_SEARCH_PATHS也是在该文件内部定义的,这些配置会体现到Build Settings里面:三方库的Framework 各个三方库也都有一些配置文件,他们文件格式基本一致,文件作用跟上面介绍的类似,下图是Moya的配置文件,Xcode中Pods > Pods > Moya > Support Files对应的文件就是该内容。我们可以想一个问题,当安装的第三方库需要依赖于别的库时它是如何去找这个库的呢?Moya是需要使用Alamofire的API的,会有import Alamofire的操作。凭借上面的内容,可以得知Framework的引用是需要在Build Setting里提前该Target,有哪些引用项的。所以这也是Framework里xcconfig文件的作用,可以在Moya的xcconfig文件里找到这个: FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire"而且引用的是跟主项目同一个Alamofire的路径。 Build Phases这里是设置编译阶段配置的地方,当首次pod install成功之后,这里会多几个[CP]开头的配置项(CP即CocoaPods缩写),它们都是由CocoPods添加的脚本内容,执行顺序从上到下。 New System Build 在讲编译脚本之前简单说下New Build System。 New Build System是Xcode10之后苹果推出的新的构建系统,新的构建系统对编译流程的优化做了很多工作,虽然到Xcode12仍兼容旧版的Legacy Build System,但其已经被标记为移除,我们的项目和库都应该使用新版的构建系统进行构建。和新的构建系统随之而来的是在运行脚本时增加的输入输出列表。这是为了控制是否每次编译都需要执行对应脚本,input和output文件可以是单个文件形式,如果文件过多可以放到格式为xcfilelist的文件列表里。 如果没有提供input和output,则每次构建都会运行该脚本。如果提供了,则会在以前从未运行过、某个输入文件被更改或某个输出文件丢失的情况下再次运行。 注意这些是构建脚本的默认逻辑,Xcode还提供了Run Scripts的自定义行为,默认勾选项:Based on dependency analysis,即代表上述逻辑。如果提供了输入输出还需要每次运行,关闭该选项即可。 [CP] Check Pods Manifest.lock 该脚本位于较上方,如果没有Dependencies,开始编译就会执行该脚本,它的内容如下: diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null if [ $? != 0 ] ; then # print error to STDERR echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2 exit 1 fi # This output is used by Xcode 'outputs' to avoid re-running this script phase. echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"作用是比较Podfile.lock和Manifest.lock文件是否相同,如果不同就输出错误信息:error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.,并执行退出,这会导致后续项目报错,无法继续编译。 该错误较常见,出现于拉取远端代码,远端pod依赖于本地不一致的情况。这时我们可以根据提示,执行pod install命令,根据Podfile及远端Podfile.lock生成新的Manifest.lock文件。 [CP] Copy Pods Resources 这个一般在以静态库引入的三方库切里面包含资源的话会添加该脚本,其作用是将三方库的资源文件拷贝至项目中。 它的完成是通过运行以下脚本进行的: "${PODS_ROOT}/Target Support Files/Pods-FFDemo/Pods-FFDemo-resources.sh"Pods-FFDemo-resources.sh文件在Pods目录内,该脚本内有个关键函数install_resource: install_resource() { if [[ "$1" = /* ]] ; then RESOURCE_PATH="$1" else RESOURCE_PATH="${PODS_ROOT}/$1" fi if [[ ! -e "$RESOURCE_PATH" ]] ; then cat << EOM error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. EOM exit 1 fi case $RESOURCE_PATH in *.storyboard) ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} ;; *.xib) ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} ;; *.framework) echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" ;; *.xcassets) ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") ;; *) echo "$RESOURCE_PATH" || true echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" ;; esac }删除了一部分日志内容,其内部主要是一个switch语句,根据资源文件的类型进行不同的同步操作。这里重点说下几种重要格式文件的处理方式。 storyboard和xib格式 这两项资源文件是需要编译处理的,利用ibtool命令分别转成sotryboardc和nib格式。 xcassets格式 这里的图片最终会被打包到Assets.car供程序使用,需要使用actool。 Bundle、plist、png等资源 其他类的资源是会走到switch语句最后出口,进行资源路径赋值给$RESOURCES_TO_COPY,在后面的代码中通过rsync命令,将资源同步到构建包的目录。 该脚本会打印很多日志,在使用CocoaPods时如果遇到资源相关的问题都可以遵循错误日志来这里推测定位错误原因。 [CP] Embed Pods Frameworks 该处脚本是直接运行Pods-FFDemo-frameworks.sh。 "${PODS_ROOT}/Target Support Files/Pods-FFDemo/Pods-FFDemo-frameworks.sh"可能你还记得上面说的pod会把多个库的依赖做成一个合并的库,但该库是以依赖的形式引入主工程,但是程序的运行时需要这些库,我们打包时就需要将各个库Embed到项目里,而做这个工作的就是该脚本。 # Copies and strips a vendored framework install_framework() { rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" # other code... # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" }脚本内容主要是调用install_framework函数,将framework内容同步到构建包里。在该函数里还有几个关键方法,strip_invalid_archs用于去除无用架构,code_sign_if_enabled用于framwork签名。

iOS面试总结(2020年6月)参考答案

上个月发了这篇iOS面试总结(2020年6月),没想到挺受大家欢迎,本来是没打算为它写答案,但有几个人建议我最好出一篇答案,提的人多了我就答应了下来。因为最近比较忙,断断续续总算补完了,就有了这篇文章,希望它对大家还有用处。这些都属于参考答案,如果大家感觉有不对不准确的地方也欢迎指出,我会及时更新。关于面试题 打个比方,如果把找工作理解成考大学,面试就是高考,市面上的“真题”就是模拟试卷。我们会很容易倾向于在面试前寻找对应公司的面试“真题”,重点准备,期待“押题”成功。但实际上,即使面试同一家公司,它会有不同部门,不同业务线,不同面试官,即使遇到同一面试官,他也不一定就每次考察完全一样的内容。想想高考中那些考的好的同学,他们肯定不是靠“押题”才能取得好成绩吧,他们大多靠的是平常积累及对知识点灵活掌握,那面试也一样啊。执着于搜题,把面试题当做重点进行“复习”,还不如自己划出“考纲”,各个知识点逐一检查掌握情况,复习的更全面呢。 我对于面试题的看法一直是相对保守的,这类文章一般只是内容搬运,它会存在一些偏差和误读,最重要的那就是几道题往那一扔,并没有产出有价值的东西。这也是为什么我上篇面试总结,会加了一些面试技巧,整理面试题时,也没提他们是出自哪家公司,就是不希望大家把题目区别看待。 说了这些并不是说面试题没用啊,而是希望大家不要迷信面试题,更多地去关注那些有质量有深度的技术文章。面试考核的是知识点而不是具体的某些题目,面试题的作用在于,衡量我们的知识掌握情况,便于我们查漏补缺,越说越像是针对一次“考试”了😄。 总结不易,希望这份参考答案能对你有所帮助,如果想持续关注我,欢迎订阅微信公众号:iOS成长之路。 面试题及参考答案 Swift 1、Swift中struct和class有什么区别? struct是值引用,更轻量,存放于栈区,class是类型引用,存放于堆区。struct无法继承,class可继承。 2、Swift中的方法调用有哪些形式? 答:直接派发、函数表派发、消息机制派发。派发方式受声明位置,引用类型,特定行为的影响。为什么Swift有这么多派发形式?为了效率。 参考文章:深入理解 Swift 派发机制3、Swift和OC有什么区别? Swift和OC的区别有很多,这里简要总结这几条:Swift Objective-C语言特性 静态语言,更加安全 动态语言,不那么安全语法 更精简 冗长命名空间 有 无方法调用 直接调用,函数表调用,消息转发 消息转发泛型/元组/高阶函数 有 无语言效率 性能更高,速度更快 略低文件特性 .swift 单文件 .h/.m包含头文件编程特性 可以更好的实现函数式编程/响应式编程 面向对象编程4、从OC向Swift迁移的时候遇到过什么问题? 可以参考这篇文章:OC项目转Swift指南 里的混编注意事项。 5、怎么理解面向协议编程? 面向对象是以对象的视角观察整体结构,万物皆为对象。 面向协议则是用协议的方式组织各个类的关系,Swift底层几乎所有类都构建在协议之上。 面向协议能够解决面向对象的菱形继承,横切关注点和动态派发的安全性等问题。 参考喵神的面向协议编程与 Cocoa 的邂逅 (上) OC语法 1、Block是如何实现的?Block对应的数据结构是什么样子的?__block的作用是什么?它对应的数据结构又是什么样子的? block本质是一个对象,底层用struct实现。 数据结构如下: struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); };struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ };isa 指针,所有对象都有该指针,用于实现对象相关的功能。flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。reserved,保留变量。invoke,函数指针,指向具体的 block 实现的函数调用地址。descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。__block的作用是让block可以捕获该变量,捕获之后的变量会进入到block内部,通过反编译的代码我们可以看到该对象是这样的: struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int val; //变量名 };对于block的深入了解,可以参考《Objective-C高级编程》第二章或者唐巧的这篇谈Objective-C block的实现 2、GCD中的Block是在堆上还是栈上? 堆上。可以通过block的isa指针确认。 3、NSCoding协议是干什么用的? 一种编码协议,归档时和解档时需要依赖该协议定义的编码和解码方法。Foundation和Cocoa Touch中的大部分类都遵循了这个协议,一般被NSKeyedArchiver做自定义对象持久化时使用。 4、KVO的实现原理 利用Runtime生成一个中间对象,让原对象的isa指针指向它,然后重写setter方法,插入willChangeValueForKey和didChangeValueForKey方法。当属性变化时会调用,会调用这两个方法通知到外界属性变化。 5、NSOperation有哪些特性,比着GCD有哪些优点,它有哪些API? NSOperation是对GCD的封装,具有面向对象的特点,可以更方便的进行封装,可以设置依赖关系。 API可以查看NSOperation文档。 6、NSNotificaiton是同步还是异步的,如果发通知时在子线程,接收在哪个线程? 同步。子线程。 UI 1、事件响应链是如何传递的? 手势的点击会发生两个重要事情,事件传递和事件响应。 事件传递:从UIApplication开始,到window,再逐步往下层(子视图)找,直到找到最深层的子视图,其为first responder。用到的判断方法是pointInside:withEvent和hitTest:withEvent。 事件响应:从识别到的视图(first responder)开始验证能否响应事件,如果不能就交给其上层(父视图)视图,如果能相应将不再往下传递,如果直到找到UIApplication层还没有相应,那就忽略盖茨点击。用到的判断方法是touchesBegan:withEvent、touchesMoved:withEvent等。 这两个过程大致的相反的。 2、什么是异步渲染? 异步渲染就是在子线程进行绘制,然后拿到主线程显示。 UIView的显示是通过CALayer实现的,CALayer的显示则是通过contents进行的。异步渲染的实现原理是当我们改变UIView的frame时,会调用layer的setNeedsDisplay,然后调用layer的display方法。我们不能在非主线程将内容绘制到layer的context上,但我们单独开一个子线程通过CGBitmapContextCreateImage()绘制内容,绘制完成之后切回主线程,将内容赋值到contents上。 这个步骤可以参照YYText中YYTextAsyncLayer.m文件中的实现方式。 3、layoutsubviews是在什么时机调用的?init初始化不会触发。addSubview时。设置frame且前后值变化,frame为zero且不添加到指定视图不会触发。旋转Screen会触发父视图的layoutSubviews。滚动UIScrollView引起View重新布局时会触发layoutSubviews。4、一张图片的展示经历了哪些步骤? 这个可以参考我之前写的一篇文章iOS开发图片格式选择 中的前半部分内容。 5、什么是离屏渲染,什么情况会导致离屏渲染? 如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。以阴影为例,为什么它会导致离屏渲染。因为GPU的渲染是遵循“画家算法”,一层一层绘制的,但阴影很特殊,它需要全部内容绘制完成,再根据外轮廓进行绘制。这就导致了,阴影这一层要一直占据一块内存区域,这就导致了离屏渲染。 类似导致离屏渲染的情况还有:cornerRadius+clipsToBounds group opacity 组透明度 mask 遮罩 UIBlurEffect 毛玻璃效果有一篇文章详细的讨论了这些情况:关于iOS离屏渲染的深入研究 6、CoreAnimation这个框架的作用什么,它跟UIKit的关系是什么? CoreAnimation虽然直译是核心动画,但它其实是一个图像渲染框架,动画实现只是它的一部分功能。看这张图我们可以知道,它是UIKit和AppKit的底层实现,位于Metal、Core Graphics和GPU之上之上。 苹果官方文档:About Core Animation 引用计数 1、ARC方案的原理是什么?它是在什么时候做的隐式添加release操作? ARC(Automatic Reference Cunting)自动引用计数,意即通过LLVM编译器自动管理对应的引用计数状态。ARC开启时无需再次键入retain或者release代码。 它是在编译阶段添加retain或者release代码的。 2、循环引用有哪些场景,如何避免? 循环引用及两个及以上对象出现引用环,导致对象无法释放的情况。一般在block,delegate,NSTimer时容易出现这个问题。 解决方案就是让环的其中一环节实现弱引用。 3、为什么当我们在使用block时外面是weak 声明一个weakSelf,还要在block内部使用strong再持有一下? block外界声明weak是为了实现block对对象的弱持有,而里面的作用是为了保证在进到block时不会发生释放。 4、Autoreleasepool是实现机制是什么?它是什么时候释放内部的对象的?它内部的数据结构是什么样的?当我提到哨兵对象时,会继续问哨兵对象的作用是什么,为什么要设计它? Autoreleasepool的原理是一个双向列表,它会对加入其中的对象实现延迟释放。当Autoreleasepool调用drain方法时会释放内部标记为autorelease的对象。 class AutoreleasePoolPage { magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; };哨兵对象类似一个指针,指向自动释放池的栈顶位置,它的作用就是用于标记当前自动释放池需要释放内部对象时,释放到那个地方结束,每次入栈时它用于确定添加的位置,然后再次移动到栈顶。 关于自动释放池的底层探究可以看draveness的这篇自动释放池的前世今生 ---- 深入解析 autoreleasepool 5、哪些对象会放入到Autoreleasepool中? 有两种情况生成的对象会加入到autoreleasepool中:非alloc/new/copy/mutablecopy 开始的方式初始化时。 id的指针或对象的指针在没有显示指定时引用计数带来的一次讨论 6、weak的实现原理是什么?当引用对象销毁是它是如何管理内部的Hash表的?(这里要参阅weak源码) runTime会把对weak修饰的对象放到一个全局的哈希表中,用weak修饰的对象的内存地址为key,weak指针为值,在对象进行销毁时,用通过自身地址去哈希表中查找到所有指向此对象的weak指针,并把所有的weak指针置位nil。 Runtime 1、消息发送的流程是怎样的? OC中的方法调用会转化成给对象发送消息,发送消息会调用这个方法: objc_msgSend(receiver, @selector(message))该过程有以下关键步骤:先确定调用方法的类已经都加载完毕,如果没加载完毕的话进行加载从cache中查找方法cache中没有找到对应的方法,则到方法列表中查,查到则缓存如果本类中查询到没有结果,则遍历所有父类重复上面的查找过程,直到NSObject2、关联对象时什么情况下会导致内存泄露? 关联对象可以理解就是持有了一个对象,如果是retain等方式的持有,而该对象也持有了本类,那就是导致了循环引用。 3、消息转发的流程是什么? 消息转发是发生在接收者(receiver)没有找到对应的方法(method)的时候,该步骤有如下几个关键步骤:消息转发的时候,如果是实例方法会走resolveInstanceMethod:,如果是类方法会走resolveClassMethod:,它们的返回值都是Bool,需要我们确定是否进行转发。 如果第一步返回YES,确定转发就会进到下个方法forwardingTargetForSelector,这个方法需要我们指定一个被用receiver。 methodSignatureForSelector用于指定方法签名,forwardInvocation用于处理Invocation,进行完整转发。 如果消息转发也没有处理即为无法处理,会调用doesNotRecognizeSelector,引发崩溃。更多了解可以参考iOS开发·runtime原理与实践: 消息转发篇(Message Forwarding) (消息机制,方法未实现+API不兼容奔溃,模拟多继承) 4、category能否添加属性,为什么?能否添加实例变量,为什么? 可以添加属性,这里的属性指@property,但跟类里的@property又不一样。正常的@property为:实例变量Ivar + Setter + Getter 方法,分类里的@property这三者都没有,需要我们手动实现。 分类是运行时被编译的,这时类的结构已经固定了,所以我们无法添加实例变量。 对于分类自定义Setter和Getter方法,我们可以通过关联对象(Associated Object)进行实现。 5、元类的作用是什么? 元类的作用是存储类方法,同时它也是为了让OC的类结构能够形成闭环。 对于为甚设计元类有以下原因;在OC的世界里一切皆对象(借鉴于Smalltalk),metaclass的设计就是要为满足这一点。在OC中Class也是一种对象,它对应的类就是metaclass,metaclass也是一种对象,它的类是root metaclass,在往上根元类(root metaclass)指向自己,形成了一个闭环,一个完备的设计。如果不要metaclass可不可以?也是可以的,在objc_class再加一个类方法指针。但是这样的设计会将消息传递的过程复杂化,所以为了消息传递流程的复用,为了一切皆对象的思想,就有了metaclass。 关于这一话题的深入讨论可以参考这两篇文章: 为什么要存在MetaClass 为什么要设计metaclass 6、类方法是存储到什么地方的?类属性呢? 类方法和类属性都是存储到元类中的。 类属性在Swift用的多些,OC中很少有人用到,但其实它也是有的,写法如下: @interface Person : NSObject // 在属性类别中加上class @property (class, nonatomic, copy) NSString *name; @end // 调用方式 NSString *temp = Person.name;需要注意的是跟实例属性不一样,类属性不会自动生成实例变量和setter,getter方法,需要我们手动实现。具体实现方法可以参考这个文章:Objective-C Class Properties 7、讲几个runtime的应用场景hook系统方法进行方法交换。 了解一个类(闭源)的私有属性和方法。 关联对象,实现添加分类属性的功能。 修改isa指针,自定义KVO。Runloop 1、讲一下对Runloop的理解? Runloop就是一个运行循环,它保证了在没有任务的时候线程不退出,有任务的时候即使响应。Runloop跟线程,事件响应,手势识别,页面更新,定时器都有着紧密联系。 深入了解推荐ibireme的这篇深入理解RunLoop 2、可以用Runloop实现什么功能?检测卡顿 线程包活 性能优化,将一些耗时操作放到runloop wait的情况处理。性能优化 1、对TableView进行性能优化有哪些方式?缓存高度 异步渲染 减少离屏渲染2、Xcode的Instruments都有哪些调试的工具?Activity Monitor(活动监视器):监控进程的CPU、内存、磁盘、网络使用情况。是程序在手机 运行真正占用内存大小Allocations(内存分配):跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史Core Animation(图形性能):显示程序显卡性能以及CPU使用情况Core Data:跟踪Core Data文件系统活动Energy Log:耗电量监控File Activity:检测文件创建、移动、变化、删除等Leaks(泄漏):一般的措施内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录Network:用链接工具分析你的程序如何使用TCP/IP和UDP/IP链接System Usage:记录关于文件读写,sockets,I/O系统活动,输入输出Time Profiler(时间探查):方法执行耗时分析Zombies:测量一般的内存使用,专注于检测过度释放的野指针对象。也提供对象分配统计以及主动分配的内存地址历史3、讲一下你做过的性能优化的事情。 这个根据自己情况来说吧。 4、如何检测卡顿,都有哪些方法?FPS,通过CADisplayLink计算1s内刷新次数,也可以利用Instruments里的Core Animation。 利用Runloop,实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值 子线程检测,每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿。参考ANREye的实现5、缩小包体积有哪些方案?图片压缩,无用图片删除 一些大图可以动态下发 删除无用类,无用方法 减少三方库的依赖计算机相关 1、项目编译的流程是什么?手机上的应用程序自点击图标开始到首屏内容展示都经历了哪些步骤? 编译流程:预处理:处理宏定义,删除注释,展开头文件。词法分析:把代码切成一个个token,比如大小括号等于号还有字符串语法分析:验证语法是否正确,合成抽象语法树AST静态分析:查找代码错误类型检查:动态和静态目标代码的生成与优化,包括删除多余指令,选择合适的寻址方式,如果开启了bitcode,会做进一步的优化汇编:由汇编器生成汇编语言机器码:由汇编语言转成机器码,生成.o文件应用启动的流程: 启动的前提是完成编译,运行程序即运行编译过后的目标程序,它分为main函数前和main函数后: main前加载可执行文件(App的.o文件集合)加载动态链接库(系统和应用的动态链接库),进行rebase指针调整和bind符号绑定Objc运行时的初始处理,包括Objc相关类的注册,category注册,selector唯一性检查初始化,包括执行+load()、attribute(constructor)修饰的函数的调用、创建C++静态全局变量main后首页初始化所需要配置文件的读写操作首页界面渲染2、对于基本数据类型,一般是存储到栈中的,它有没有可能存在堆上,什么情况下会存储到堆上? 栈和堆都是同属一块内存,只不过一个是高地址往低地址存储,一个从低地址往高地址存储,他们并没有严格的界限说一个值只能放在堆上或者栈上。所以基本数据类型也是可以存储到堆上的。 至于什么情况会存储到堆上,我没想到,有知道的同学可以告知一下。 3、数据库中的事务是什么意思? 事务就是访问并操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。如果其中一个步骤出错就要撤销整个操作,回滚到进入事务之前的状态。 4、使用过什么数据库(我回答的Sqlite,Realm),Realm在使用时有哪些注意事项,如何实现批量操作? 对于Realm感兴趣的同学可以看下其官方文档。 Realm需要注意的主要就是不能直接跨线程访问同一对象。 批量操作可以在一个单独的事务中执行多个数据库的修改。 5、LRU算法是否了解,如何实现一套LRU算法? LRU(Least recently used 最近最少使用)算法是一个缓存淘汰算法,其作用就是当缓存很多时,该淘汰哪些内容,见名知意,它的核心思想是淘汰最近使用最少的内容。实现它的关键步骤是:新数据插入到链表的头部每当缓存命中时,则将数据移动到链表头部链表满时,将尾部数据清除这个算法在SDWebImage和Kingfisher等需要处理缓存的库中都有实现。 6、知道哪些设计模式,怎么理解设计模式的作用? 工厂模式、观察者模式、中介者模式、单例模式。这个根据实际情况说吧。 7、如果有1000万个Int类型的数字,如何对他们排序? 这里的隐藏含义是,内存不够用时如何排序,还有一个隐藏含义是硬盘足够大。这是可以采用分而治之的方法,将数据分成若干块,使每一小块满足当前内容大小,然后对每块内容单独排序,最后采用归并排序对所有块进行排序,就得到了一个有序序列。 8、设计一套数据库方案,实现类似微信的搜索关键词能快速检索出包含该字符串的聊天信息,并展示对应数量(聊天记录的数据量较大) 可以对聊天记录的文本值加上索引。正常情况下数据库搜索都是全量检索的,加上索引之后只会检索满足条件的记录,大大降低检索量。 简历相关问题 1、Lottie实现动画效果的原理是什么? iOS里的动画基本都是基于CoreAnimation里的API实现的,Lottie也是如此。在AE上实现动画效果,通过插件导出对应的json文件,Lottie的库解析该json,转成对应的系统API方法。图片的引用可以使用Base64编到json里,也可以通过项目集成,通过路径引用。 2、OClint实现静态分析的原理是什么,它是如何做到的? 具体可以参考我之前写的如何通过静态分析提高iOS代码质量。 3、MVVM和MVC有什么区别? 对比架构时,可以从是否职责分离,可测试性,可易维护性三个维度对比。 更多对比可以参考我翻译的一篇文章:【译】iOS 架构模式--浅析MVC, MVP, MVVM 和 VIPER 4、静态库和动态库的区别是什么? 静态库:链接时被完整复制到可执行文件中,多次使用就多份拷贝。 动态库:链接时不复制,而是由系统动态加载到内存,内存中只会有一份该动态库。 5、了解Flutter吗?它有没有使用UIKit?它是如何渲染UI的?UIKit是基于CoreAnimation渲染的,而Flutter并没有用到它,而是自己基于C++实现了一套渲染框架。 6、二进制重排的核心依据是什么? 修改链接顺序,减少启动时的缺页中断。 实践步骤可以参考李斌同学的这篇iOS 优化篇 - 启动优化之Clang插桩实现二进制重排 7、如何设计一套切换主题的方案? 核心思路是观察者模式+协议(通知),当获取到主题切换时,通知各个实现了主题协议的类进行更新。 8、AVPlayer和IJKPlayer有什么区别?用IJKPlayer如何实现一个缓存视频列表每条视频前1s的内容? 因为对IJKPlayer和FFmpeg了解的不是很深,这个我也没有确切答案,如果有了解的小伙伴可以评论告知我。 9、类似微博的短视频列表,滑动停留播放,如何实现? 这个主要就是检测contentOffset和屏幕中间位置,设置一些边界条件,处理滑动过程中的切换行为。 10、使用python做过哪些事?如何理解脚本语言? 多语言管理,csv多语言文件读取,然后写入到项目Localizable.strings中;抓取项目中的多语言字符串。 脚本(script) 其实就是一系列指令,计算机看了指令就知道自己该做什么事情。像常见的Python,Shell,Ruby都是脚本语言,他们通常不需要编译,通过解释器运行。 数据结构与算法 1、什么是Hash表,什么是Hash碰撞,解决Hash碰撞有什么方法? 哈希表(Hash Table,也叫散列表),是根据关键码值 (Key-Value) 而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。我们常用的Dictionary就是一种Hash表。 那什么是Hash碰撞呢,我们知道Hash表的查找是通过键值进行定位的,当两个不同的输入对应一个输出时,即为Hash碰撞,也被称为Hash冲突。 如果使用字典的例子你可能联想不到冲突的情况,我们假设另一种情况:假设hash表的大小为9(即有9个槽),现在要把一串数据存到表里:5,28,19,15,20,33,12,17,10。我们使用的hash函数是对9取余。这样的话会出现hash(5)=5,hash(28)=1,hash(19)=1。28和19都对应一个地址,这就出现了Hash冲突。 解决Hash冲突的方式有开放定址法和链地址法。 2、如何遍历二叉树?二叉树的遍历有三种方式,对于上面这棵二叉树,他们的遍历结果为: 前序遍历:根节点 > 左子节点 > 右子节点。 10,6,4,8,14,12,16 中序遍历:左子节点 > 根节点 > 右子节点。 4,6,8,10,12,14,16 后序遍历:左子节点 > 右子节点 > 根节点。 4,8,6,12,16,14,10 3、简述下快速排序的过程,时间复杂度是多少? 快排的思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行。 一个简单的Swift实现方式如下: func quicksort<T: Comparable>(_ a: [T]) -> [T] { guard a.count > 1 else { return a } let pivot = a[a.count/2] let less = a.filter { $0 < pivot } let equal = a.filter { $0 == pivot } let greater = a.filter { $0 > pivot } return quicksort(less) + equal + quicksort(greater) }快速排序是有好几种的,他们的区别在于如何实现filter和分区基准值的选取。 快排的时间复杂度是O(nlogn),空间复杂度是O(logn) 4、有一个整数数组,如何只遍历一遍就实现让该数组奇数都在前面,偶数都在后面? 这个是《剑指offer》里的一道题,leedcode也有对应题目:剑指offer 21 这个相对比较简单,因为不要求有序,可以采用收尾遍历的方式,进行交换,我这有个参考答案: func sorted( _ nums: inout [Int]) -> [Int] { guard !nums.isEmpty else { return [] } var start = 0 var end = nums.count - 1 while start < end { if nums[start] % 2 != 0 { start += 1 continue } if nums[end] % 2 == 0 { end -= 1 continue } (nums[start], nums[end]) = (nums[end], nums[start]) } return nums }5、假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? leetcode 20 6、给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转 leetcode 7 7、有红、黄、蓝三种颜色的气球。在牛客王国,1个红气球+1个黄气球+1个蓝气球可以兑换一张彩票 2个红气球+1个黄气球可以兑换1个蓝气球。 2个黄气球+1个蓝气球可以兑换1个红气球。 2个蓝气球+1个红气球可以兑换1个黄气球。 现在牛牛有a个红气球,b个黄气球, c个蓝气球,牛牛想知道自己最多可以兑换多少张彩票。 这个是牛客网里的一道算法题,这里有个题解可以参考。

快手iOS面经

背景 过完年来北京之后,有准备看看机会,也是想了解下市场行情。简历没有投太多,只定向投了头条教育部门、抖音、快手、阿里,这些公司。头条和阿里的简历都没过,肯定是亮点太少吧。只有快手简历过了,快手是三轮技术面+一轮HR面,前两轮技术都比较顺利,到第三轮却栽了,很痛心o(╥﹏╥)o。目前就不考虑换工作了,等下半年再说了,接下来的时间再好好精炼一下。 快手是视频面试,不支持周末,但是可以选择晚上时间,我这几次都是定在了晚上九点。视频面试是通过牛客网进行的,以下是我还记得下来的各轮面试题,对于一些iOS基础知识就不做解答了。一面 1、用递归写一个算法,计算从1到100的和。 func sum(value: Int) -> Int { if value <= 0 { return 0 } var number = value return value + sum(value: number - 1) } // 计算过程 let result = sum(value: 100) print(result)写完算法之后又围绕着问了几个问题,都是算法基础:算法的时间复杂度是多少 递归会有什么缺点 不用递归能否实现,复杂度能否降到O(1)2、property的作用是什么,有哪些关键词,分别是什么含义? 3、父类的property是如何查找的? 4、NSArray、NSDictionary应该如何选关键词? 5、copy和muteCopy有什么区别,深复制和浅复制是什么意思,如何实现深复制? 6、用runtime做过什么事情?runtime中的方法交换是如何实现的? 7、讲一下对KVC合KVO的了解,KVC是否会调用setter方法? 8、__block有什么作用 9、说一下对GCD的了解,它有那些方法,分别是做什么用的? 10、对二叉树是否了解? 面试官是想接着问这方面的问题的。我当时说了不了解,然后就没有后续了。 二面 1、ARC和MRC的区别,iOS是如何管理引用计数的,什么情况下引用计数加1什么情况引用计数减一? 2、在MRC下执行[object autorelease]会发生什么,autorelease是如何实现的? 3、CoreAnimation是如何绘制图像的,动画过程中的frame能否获取到? 4、谈一下对Runlop的了解? 5、OC如何实现多继承? 这个当时没有答好。其实借助于消息转发,protocol和类别都可以间接实现多继承。 6、对设计模式有什么了解,讲一下其中一种是如何使用的。 7、有没有哪个开源库让你用的很舒服,讲一下让你舒服的地方。 我这里说了RxSwift中的观察者模式,和响应式编程。然后面试官问,如果要用OC实现一套RxSwift那样的逻辑应该怎么做。我回答的是结合KVO,将一些需要观察的属性,通过KVO进行监听,然后通过block回调出来。 8、一张100*100,RGBA的png图像解压之后占多大内存空间。 RGBA > FFFFFFFF > 4字节 所以会占用:(100 * 100 * 4) / 1024 = 39KB 9、算法题 题目:给定一个个数字arr,判断数组arr中是否所有的数字都只出现过一次。 这个并没有要求写出来,说是提供思路就行了。我当时给的方案是在便利数组的时候,用一个字典把便利的元素存起来,如果在后面的便利过程中新元素在字典中存在过就说明,有重复数字出现。时间复杂度是O(n)。 当时也问了有没有办法进行优化,我当时想到了将数组转成Set,然后和原数组比较,两个集合的数量是否变化。 10、因为我跟他介绍自己Swift用的多一些,然后问了些Swift跟OC的区别,各自的优缺点。 11、为什么离职,有什么职业规划。 三面 1、给定一个Int型数组,用里面的元素组成一个最大数,因为数字可能非常大,用字符串输出。 输入: [3,30,34,5,9] 输出: 9534330这个是leetcode的179题,难度中等。面试官让先说思路,再去做题。事先说一下这个题我没有做过。当时的思路是用冒泡法进行排序,排序的前提是将较少位数的数字进行循环补齐,例如3和30的比较,变成33和30的比较,34和4的比较变成34和44的比较,然后将结果从大到小整合成字符串输出。 但是做题是却发现没那么简单,位数的补齐对于2位和3位数的比较还需要求位数的最小公倍数,将他们都转成6位数才能比较。在挣扎了5分钟做了就做罢了。 后来再去做这道题,其实这就是一个排序而已,只不过他的规则是按高位优先级更高的原则,而这一点跟字符串的比较保持一致,如果再加一些Swift的高阶函数,就可以写成: func largestNumber(_ nums: [Int]) -> String { let sort = nums.map {"\($0)"}.sorted { (lStr, rStr) -> Bool in return lStr + rStr > rStr + lStr } let result = sort.joined() if result.prefix(1) == "0" { return "0" } else { return result } }2、项目中有这么一个方法func findfile(dir: String suffix: String) -> [String] ,可以通过输入文件夹目录,和后缀检索出所需的文件。 例如需要在某个文件中检索txt文件或者mp4文件,那就传入dir和suffix就行了。现在又有一些需求,例如需要检索utf8格式的txt或者h264编码的mp4,也会有一些例如查找最近一周更新过的文件这样的需求,你如何优化这个类,让它满足这些情况? 我首先想到的是这么多需求不可能一个方法就完成,需要根据不同场景拆出不同的方法,但是这些同属于文件操作,会有一个共同使用的方法就是检索文件。这个方法需要传入文件目录,然后递归的返回当前目录所有文件路径。外部不同场景的调用逻辑就用一个enum完成,不同值对应相同范围的不同种类。 面试官比较关注内部共用的文件检索怎么写,他说子文件如果过多怎么办,如何优化。我有点懵,查找文件至少是要遍历一遍的,子文件过多,这个应该是没法优化的啊。中间卡了一段时间,后来他给了提示说是不是可以用block实现,将文件路径返回出去,由外部决定当前文件是否可用,最终外部的调用类是这个样子。 //我的方案 //func findDir(_ dir: String) -> [String] //block方案 func findDir(_ dir: String, block: ((String) -> Bool))我想来确实没毛病,用block返回内容至少不会将该目录的所有文件都由一个对象持有,而前面一堆的铺垫其实也都是为验证block方案的好处。 其实事后想下这个问题没啥难的,这种写法自己也有写过,但当时就是没想起来,可能前面一圈的铺垫给我带偏了吧,说亏也不亏,以后多多努力吧。 总结 整体来看,快手的面试题跟我在别处看到的iOS面试题对比要简单些,一面主要是基础知识,二面考察更全面一些,更多让自己谈一些对技术的理解,三面则是更偏实践一些。 算法虽然三轮都有,但相对比较简单,即使写不出来,有思路也是可以的。当然写出来肯定是加分项,所以大家准备面试时,应该都看一下。算法相关的,排序,数组,二叉树,这几类是重点。

面试题总结(From J_Knight)

测试前一段时间看了J_Knight的2017年5月iOS找人心得(附面试题)。作为一个在编程前线奋斗了将近两年的iOS从业人员,面对这些题目时,有些竟感觉生疏,甚至答不上来,很是惭愧。个人感觉,像runtime、线程、信号量相关的偏底层知识虽然平时基本用不到。特别是很多人可能都没参与过稍复杂项目的开发,优化,这些内容对于很多新手iOS开发来说只存在于理论。但并不是说这些知识不重要,相反,它是我们进阶的必经之路。此篇文章的目的一方面自己整理,一方面希望和大家共同学习进步。以下内容多数为整理,时间仓促可能有不准确的地方,如果缺漏,欢迎指正。部分答案出处:iOSInterviewQuestions 基础 为什么说Objective-C是一门动态语言? 动态语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化。比如Ruby、Python等就是动态语言,而C、C++等语言则不属于动态语言。 所谓的动态类型语言,意思就是类型的检查是在运行时做的。 1、动态类型。 如id类型。实际上静态类型因为其固定性和可预知性而使用得更加广泛。静态类型是强类型,而动态类型属于弱类型。运行时决定接收者。 2、动态绑定。让代码在运行时判断需要调用什么方法,而不是在编译时。与其他面向对象语言一样,方法调用和代码并没有在编译时连接在一起,而是在消息发送时才进行连接。运行时决定调用哪个方法。 3、动态载入。让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。 讲一下MVC和MVVM,MVP iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构 为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别? 防止循环引用。 另外,不建议使用assign weak 当计数器为0 时对象被释放,地址指针就置为了nil 了。 assign 当计数器为0 时 对象被释放,地址指针还是指向那个地址,就会产生野指针 datasource协议里面东西是跟内容有关的,主要是cell的构造函数,各种属性 delegate协议里面的方法主要是操作相关的,移动编辑之类的,你都写上要用什么方法自己去翻就是了 delegate控制的是UI,是上层的东西;而datasource控制的是数据。他们本质都是回调,只是回调的对象不同。 block 和 delegate 都可以通知外面。block 更轻型,使用更简单,能够直接访问上下文,这样类中不需要存储临时数据,使用 block 的代码通常会在同一个地方,这样读代码也连贯。delegate 更重一些,需要实现接口,它的方法分离开来,很多时候需要存储一些临时数据,另外相关的代码会被分离到各处,没有 block 好读。 多个相关方法,避免循环引用,建议用delegate。 临时性的,只在栈中,需要存储,只调用一次,一个完成周期用block 属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的? 属性的本质就是实现实例变量和存取方法。 @property = ivar + getter + setter;对应基本数据类型默认关键字是atomic,readwrite,assign 对于普通的 Objective-C 对象atomic,readwrite,strong@property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var; @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。 @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。 NSString为什么要用copy关键字,如果用strong会有什么问题? 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性. 如何令自己所写的对象具有拷贝功能? 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。 可变集合类 和 不可变集合类的 copy 和 mutablecopy有什么区别?如果是集合是内容复制的话,集合里面的元素也是内容复制么? [immutableObject copy] // 浅复制 [immutableObject mutableCopy] //深复制 [mutableObject copy] //深复制 [mutableObject mutableCopy] //深复制但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制 为什么IBOutlet修饰的UIView也适用weak关键字? 因为既然有外链那么视图在xib或者storyboard中肯定存在,视图已经对它有一个强引用了。 不过这个回答漏了个重要知识,使用storyboard(xib不行)创建的vc,会有一个叫_topLevelObjectsToKeepAliveFromStoryboard的私有数组强引用所有top level的对象,所以这时即便outlet声明成weak也没关系 nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现? atomic会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明 nonatomic 可以节省这些虽然很小但是不必要额外开销。 一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全” ( thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。 UICollectionView自定义layout如何实现?新建一个类继承UICollectionViewFlowLayout,实现prepareLayout方法。 新建UICollectionViewFlowLayout类,设置属性。 实现UICollectionViewDelegateFlowLayout方法。用StoryBoard开发界面有什么弊端?如何避免?难以维护 性能瓶颈 错误定位不准确解决多Storyboard协作弊端,就是尽量将项目的界面分割在多个Storyboard文件中。一个最佳实践是,按照项目功能模块来区分故事板,例如Login.Storyboard,Chat.Storyboard,Person.Storyboard等。尽量把每个Storyboard的Scene数量控制在20个以内 进程和线程的区别?同步异步的区别?并行和并发的区别? 一个进程可以包括多个线程。一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。 一个程序至少有一个进程,一个进程至少有一个线程. 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。 同步会造成阻塞,异步非阻塞,网络请求操作。 一个形象的例子: 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』。 “并行”概念是“并发”概念的一个子集 线程间的通信 NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法 GCD的一些常用的函数?(group,barrier,信号量,线程同步)group 队列组通知监听函数(异步函数)dispatch_group_notify 队列组等待函数(同步函数)dispatch_group_wait 应用场景:下载两张图片,拼接图片后到主线程中刷新barrier 栅栏函数执行顺序:栅栏函数之前的任务(执行完毕)--> 栅栏函数的任务(执行完毕)--> 栅栏函数之后的任务 栅栏函数前面和后面追加的操作执行顺序都不固定,但是前面的三个输出操作必然先执行,然后再执行栅栏函数中的操作,最后执行后面的三个输出操作。 栅栏函数信号量 信号量大小是用于控制并发数量的 信号量就是一个资源计数器,对信号量有两个操作来达到互斥,分别是P和V操作。 一般情况是这样进行临界访问或互斥访问的: 设信号量值为1, 当一个进程1运行是,使用资源,进行P操作,即对信号量值减1,也就是资源数少了1个。这是信号量值为0。系统中规定当信号量值为0是,必须等待,知道信号量值不为零才能继续操作。 这时如果进程2想要运行,那么也必须进行P操作,但是此时信号量为0,所以无法减1,即不能P操作,也就阻塞。这样就到到了进程1排他访问。 当进程1运行结束后,释放资源,进行V操作。资源数重新加1,这是信号量的值变为1. 这时进程2发现资源数不为0,信号量能进行P操作了,立即执行P操作。信号量值又变为0.次数进程2咱有资源,排他访问资源。 这就是信号量来控制互斥的原理线程同步: 线程同步:@synchronized NSLock dispatch_semaphore(信号量设置为1)如何使用队列来避免资源抢夺? 当我们使用多线程来访问同一个数据的时候,就有可能造成数据的不准确性。这个时候我么可以使用线程锁的来来绑定。也是可以使用串行队列来完成。 如:fmdb就是使用FMDatabaseQueue,来解决多线程抢夺资源。 数据持久化的几个方案(fmdb用没用过)plist CoreData FMDB说一下AppDelegate的几个方法?从后台到前台调用了哪些方法?第一次启动调用了哪些方法?从前台到后台调用了哪些方法? ```//第一次启动: didFinishLaunchingWithOptions: applicationDidBecomeActive: //前台到后台: applicationWillResignActive: applicationDidEnterBackground: //后台到前台: applicationWillEnterForeground: applicationDidBecomeActive:```NSCache优于NSDictionary的几点?当系统资源将要耗尽时,NSCache具备自动删减缓冲的功能。并且还会先删减“最久未使用”的对象。 NSCache不拷贝键,而是保留键。因为并不是所有的键都遵从拷贝协议(字典的键是必须要支持拷贝协议的,有局限性)。 NSCache是线程安全的:不编写加锁代码的前提下,多个线程可以同时访问NSCache。知不知道Designated Initializer?使用它的时候有什么需要注意的问题? 便利初始化函数只能调用自己类中的其他初始化方法 指定初始化函数才有资格调用父类的指定初始化函数 构造便利函数 实现description方法能取到什么效果? description方法默认返回对象的描述信息(默认实现是返回类名和对象的内存地址),可定义输出自己想要的内容。 objc使用什么机制管理对象内存? 1.Objective-C中所有对象都在堆区建立,由程序员负责释放对象所占用的内存。内存管理机制由3种:垃圾回收、引用计数、C语言方式。 2.垃圾回收是Mac OS10.5提供的新方案,在系统存在一个垃圾收集器。如果发现某个对象没有被任何对象使用,该对象被自动释放。 3.C语言方式,原始内存管理方式。用户手动调用malloc、calloc函数分配内存,free回收内存。 4.引用计数机制:对象创建后,运行时系统通过对象维护的一个计数器来描述有多少个其他对象在使用自己,当计数器为0时,释放该对象占用的内存空间(该对象调用dealloc方法)。 5,内存管理规则:当使用alloc,new或copy创建一个对象时,对象的引用计数被设置为1.;向对象发送retain消息,对象引用计数加1;向对象发送release消息时,对象引用计数减1;当对象引用计数为0时,运行时系统向对象发送dealloc消息并回收对象所占用的内存。 中级 Block block的实质是什么?一共有几种block?都是什么情况下生成的? Block是iOS开发中一种比较特殊的数据结构,它可以保存一段代码,在合适的地方再调用,具有语法简介、回调方便、编程思路清晰、执行效率高等优点,受到众多猿猿的喜爱。_NSConcreteGlobalBlock: 存储在全局数据区 _NSConcreteStackBlock: 存储在栈区 _NSConcreteMallocBlock: 存储在堆区 其中,_NSConcreteGlobalBlock 和 _NSConcreteStackBlock 可以由程序创建,而 _NSConcreteMallocBlock 则无法由程序创建,只能由 _NSConcreteStackBlock 通过拷贝生成。为什么在默认情况下无法修改被block捕获的变量? __block都做了什么? Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。 又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。 将变量由栈区移到堆区。 模拟一下循环引用的一个情况?block实现界面反向传值如何实现? 一个对象中强引用了block,在block中又强引用了该对象,就会发射循环引用。 解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用. id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏 id __block weakSelf = self; 或者将其中一方强制制空 xxx = nil。 Runtime objc在向一个对象发送消息时,发生了什么?通过对象的isa指针获取类的结构体。 在结构体的方法表里查找方法的selector。 如果没有找到selector,则通过objc_msgSend结构体中指向父类的指针找到父类,并在父类的方法表里查找方法的selector。 依次会一直找到NSObject。 一旦找到selector,就会获取到方法实现IMP。 传入相应的参数来执行方法的具体实现。 如果最终没有定位到selector,就会走消息转发流程。什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步? 找不到执行方法。动态方法解析 对象接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。 备用接收者 如果这个方法返回一个对象,则这个对象会作为消息的新接收者。注意这个对象不能是self自身,否则就是出现无限循环。如果没有指定对象来处理aSelector,则应该 return [super forwardingTargetForSelector:aSelector]。 完整消息转发 这是最后一次机会将消息转发给其它对象。创建一个表示消息的NSInvocation对象,把与消息的有关全部细节封装在anInvocation中,包括selector,目标(target)和参数。在forwardInvocation 方法中将消息转发给其它对象。能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么? 不能向编译后得到的类中增加实例变量; 能向运行时创建的类中添加实例变量; 解释下: 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量; 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair之前,原因同上。 runtime如何实现weak变量的自动置nil? runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。 给类添加一个属性后,在类结构体里哪些元素会发生变化?instance_size :实例的内存大小 objc_ivar_list *ivars:属性列表 RunLoop runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢? 循环检测线程任务。为了在我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。主线程的run loop默认是启动的。对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。 NSRunLoop *runloop = [NSRunLoop currentRunLoop];runloop的mode是用来做什么的?有几种mode? model 主要是用来指定事件在运行循环中的优先级的,分为: NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态 UITrackingRunLoopMode:ScrollView滑动时 UIInitializationRunLoopMode:启动时 NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合 苹果公开提供的 Mode 有两个: NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes) 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了? RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。 如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。 同时因为mode还是可定制的,所以: Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。 //将timer添加到NSDefaultRunLoopMode中 [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; //然后再添加到NSRunLoopCommonModes里 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];苹果是如何实现Autorelease Pool的? autoreleasepool 以一个队列数组的形式实现,主要通过下列三个函数完成. objc_autoreleasepoolPush objc_autoreleasepoolPop objc_autorelease 看函数名就可以知道,对 autorelease 分别执行 push,和 pop 操作。销毁对象时执行release操作。 举例说明:我们都知道用类方法创建的对象都是 Autorelease 的,那么一旦 Person 出了作用域,当在 Person 的 dealloc 方法中打上断点,我们就可以看到这样的调用堆栈信息:备注:Objective-C Autorelease Pool 的实现原理 可能苹果在ARC的处理上又优化了,或者变更了。我自己测试的结果跟以上分析不一致,但原理可以参考。 类结构 isa指针?(对象的isa,类对象的isa,元类的isa都要说) 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中) 成员变量的列表, 属性列表, 它内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个superclass的指针,指向他的父类对象。 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。类方法和实例方法有什么区别?类方法: 类方法是属于类对象的 类方法只能通过类对象调用 类方法中的self是类对象 类方法可以调用其他的类方法 类方法中不能访问成员变量 类方法中不能直接调用对象方法实例方法: 实例方法是属于实例对象的 实例方法只能通过实例对象调用 实例方法中的self是实例对象 实例方法中可以访问成员变量 实例方法中直接调用实例方法 实例方法中也可以调用类方法(通过类名)介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法? 分类可以在不知道系统类源代码的情况下,为这个类添加新的方法。分类只能用来添加方法,不能添加成员变量。通过分类增加的方法,系统会认为是该类类型的一部分 Category实现原理 运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么? 可以添加属性,不可以添加成员变量。 OC类成员变量深度剖析 objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体) 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如: Person *motherInlaw = [[aPerson spouse] mother];如果 spouse 对象为 nil,那么发送给 nil 的消息 mother 也将返回 nil。 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量,发送给 nil 的消息将返回0。 如果方法返回值为结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。 如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。高级 UITableview的优化方法(缓存高度,异步绘制,减少层级,hide,避免离屏渲染) 一般在网络请求结束后,在更新界面之前就把每个 cell 的高度算好,缓存到相对应的 model 中。 另外绘制 cell 不建议使用 UIView,建议使用 CALayer。 简单的形式参考: ```//异步绘制 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ CGRect rect = CGRectMake(0, 0, 100, 100); UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0); CGContextRef context = UIGraphicsGetCurrentContext(); [[UIColor lightGrayColor] set]; CGContextFillRect(context, rect); //将绘制的内容以图片的形式返回,并调主线程显示 UIImage *temp = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); // 回到主线程 dispatch_async(dispatch_get_main_queue(), ^{ //code }); });```CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。