iteatimeteam / Friday-QA

iTeaTime |技术清谈 微信群每周五问答环节
MIT License
229 stars 19 forks source link

iTeaTime(技术清谈)【-002期】【代号:模仿游戏之寻龙诀】 #11

Open ChenYilong opened 5 years ago

ChenYilong commented 5 years ago

iTeaTime(技术清谈)【-002期】【代号:模仿游戏之寻龙诀】




本期特辑:iOS应用安全与逆向之基础原理 本期出品人:微博@iOS程序犭袁 本期代号:模仿游戏之寻龙诀 本期出题人(排名不分先后):

注:题目难度五星为满分,各个类目下题目从易到难依次排列。


逆向类


1【问题】【iOS】对称加密有哪些?描述其原理。 【难度🌟】【出题人:风扬-拍拍贷-SOi】


2【问题】【iOS】随便找一个正在运行的程序,给 objc_msgSend 下符号断点。程序断下之后,写一条 lldb 指令打印当前调用方法的 selector 【难度🌟🌟】【出题人:鹅喵-便利蜂移动端】

【回答】 要看具体 cpu 架构 读取第二个参数对应的寄存器:

objc_msgSend 两个固定参数, selector 第二个,存放在 x0x1 寄存器中。

【SAGESSE-iOS-深圳】给出答案:

平台 x86_64 arm 通用的
命令 po (char*)$rsi po (char*)$x1 po (char*)$arg2
参考 《x86-64传参规则》 《ARM子函数定义中的参数放入寄存器的规则》 所谓通用,是指在 arm 和在 x86_64 下用 po (char*)$arg2 都能得到预期效果。 $arg1-$argN 是第1到第n,无论什么框架和约定(lldb处理过了),:arg1 到 argN 是 Xcode9/10 添加的功能,在 Xcode8 或者之前需要用 rsix1

【鹅喵-便利蜂移动端】写成 x/s $arg2 也行, x/s 比较好打 po (char*)$arg2 或者 po x/s $arg2

/one more thing/

上述讨论与调用约定有关,相关概念: rdirsirdxrcxr8r9

参考文档:


3【问题】【iOS】应用中使用了一个外部动态库的符号,这个符号的具体实现被查找了几次?为什么? 【难度🌟🌟🌟】【出题人:鹅喵-便利蜂移动端】

【提示】看过 fishhook 原理应该都知道 lazy symbol binding


4【问题】【iOS】Objective-C 和 C 在 iOS 中的区别, 尝试从以下角度分析两者区别:

【难度🌟🌟🌟】【出题人:风扬-拍拍贷-SOi】

/one more thing/

【出题人提示】 汇编角度: 汇编调用OC和C的过程分别是什么 2、加载区别: OC:消息转发
自定义C函数:直接调用地址 系统C函数:共享缓存库 3、存储区别:MochO-中存储的区别

//TODO: 待讨论部分 第二点感觉覆盖不全,自己写的C代码就是调用库了吧?

【答案】

  1. OC 是 C 的超集,在 C 的基础上加入了面向对象编程范式的支持,为了支持这一范式,需要运行时支持。所以方法调用会在编译期转换为对运行时函数的调用,例如 objc_msgSend 系列函数。ARC 内存管理也需要运行时的支持,在编译期会自动插入对应调用。
  2. 所有包含了 OC 代码的可执行文件会依赖 libobjc.A.dylib,这个动态库中包含了处理嵌入在可执行文件内辅助运行时运行的一些数据结构(例如加载类、方法列表、+load的执行等)的函数。libobjc.A.dylib 在被 dyld 加载时会向 dyld 注册一个映像加载的钩子函数,使得被动态载入的可执行文件同样可以被 libobjc.A.dylib 预处理。
  3. 类与方法列表、CFString 对象等数据,被存放在了可执行文件中,例如各种 __objc 开头的区段。 内存布局方面,除了 TaggedPointer、通过各种方式 bridge 过来的其他库中创建的之外的对象,都只能通过 alloc 在堆上分配内存

5【问题】【汇编】arm64位系统里的通用寄存器有多少个,分别是干什么的?什么是状态寄存器? 【难度🌟🌟🌟】【出题人:风扬-拍拍贷-SOi】


6【问题】【iOS】动态下发一个经过苹果签名的动态库是否可以加载(允许使用dlopen), 为什么? 【难度🌟🌟🌟🌟】【出题人:SAGESSE-iOS-深圳】

【出题人提示】 1: 首先,动态下载肯定不在bundle里, 只能在沙盒里面 2: 从题目条件得知是经过苹果签名的,所以代码签名这一步是通过的 3: 可以通过lldb测试


7【问题】【iOS】iOS 是如何通过代码签名确保应用安全的? 【难度🌟🌟🌟🌟】【出题人:SAGESSE-iOS-深圳】


8【问题】【iOS】分别给 objc_msgSend 和你要调用的方法下符号断点,第一次断下的时候可以在调用栈中看到 objc_msgSend。第二次断下的时候,调用栈里只有你要调用的方法了。objc_msgSend 哪儿去了? 【难度🌟🌟🌟🌟】【出题人:鹅喵-便利蜂移动端】

【提示】其实是想讨论一下如何控制堆栈平衡以及 backtrace 背后的原理:

  1. 调试器中看到的调用栈是怎么来的
  2. 如何通过汇编做到不带栈帧的调用

【答案】

跳板函数/蹦床函数/trampoline function 正解。

首先,如果用了 bl 指令的话 lr 里面会存储返回地址的,但是串成串还要靠栈帧结构。 x86_64 有个一个 rbp , 每一次 call 都会压入 rbp , 就会形成一帧帧调用栈, 然后通过 rbp 可以朔到最开始。所以展示 backtrace 只要遍历栈帧就可以了,首个栈帧靠 ip 确定。

普通的函数是call调用, 而objc_msgSend 是长跳转(jump),估计是为了减轻栈溢出的压力, 另外尾递归也是长跳转


Q-A环节 Q:[腾讯-刘翅鹏]第8题是指定cachelookup吗? A:[鹅喵-便利蜂移动端]不是,CacheLookup 里面应该是顺带做了这个事

参考 《[腾讯-刘翅鹏]的笔记》

Q:是否如下图所言:

A: [鹅喵-便利蜂移动端]是的,不过 cdecl 调用约定是被调用者清理栈空间,所以纯汇编写的时候要注意平衡

A:【欧阳大哥】上面那个红框中的结论有待商榷吧。 如果函数调用发生在最后一条指令时不能用bl而只用用b的原因是因为:执行bl指令时会把当前指令的下一条指令保存到LR寄存器中。问题是因为这是最后一条指令了,下一条指令是一条未知指令,所以如果仍然用bl指令的话,那么函数返回时所跳转的地址将可能是一条无效的地址了。。而不是所谓的栈溢出的现象。

Q:[SAGESSE-iOS-深圳] 我突然想到个问题,栈顶的第一个条数据是rbp吗, A:[鹅喵-便利蜂移动端]应该是栈上最后一个申请的变量,rbp 是当前栈帧的顶端,所以函数退出清理栈的时候不需要记住你申请了多少栈空间,只要 movq %rbp, %rsp 就可以了。*(rbp + 8) 是上个栈帧的 rbp。

*(void*)($rbp + 0) 上一个栈帧的地址
*(void*)($rbp + 8) 返回地址

然后通过返回地址可以就可得到函数的信息

Q: 也就是说:第一个调用的函数,当前栈的rbp是啥? *(rbp)

A: 刚才调试了一下,是个0.

Q-A结束



常规类


9【iOS】kengny 是一名产品经理,他们的 app 是一款类似美团的产品,最近他和一些店家进行了PY交易,要求用户到他们家店附近的时候,立即收到通知。 小地和大风哥,会上听到需求后,小地立即说:这个需求做不了。大风哥会上没说话,产品经理说,明天上线,怎么实现我不管,散会。

会后,大风哥悄悄说对小地说要做也可以,可以这样做:___

请补充填空,要求给出详细理由,包括技术实现细节,如有必要贴出示例代码。

【 难度🌟】【出题人 微博@iOS程序犭袁】

已知 iOS 定位方法有:GPS定位、基站蜂窝定位、Wi-Fi定位等多种定位方法,

精准度优先级可以为:

如果结合以上多种定位方法,这四个方案是同时的,组合起来可有效命中率。

蓝牙相关的例子: 好多超市有蓝牙定位,还有商场的室内定位,都是基于蓝牙

还有基于Apple设备蓝牙配对效果的:

其中Wi-Fi SSID部分注意事项:

即使申请了权限,也只能在系统的Wi-Fi列表里获取所有Wi-Fi信息,APP内好像也是不能获取的。

正如 《iOS NetworkExtension 框架使用笔记》 所说:

ps1:如果你运行完,没看到打印。心想被坑了,那就拿 起你的手机进入到设置,打开【无线局域网】设置页 面。这时候你再看看控制 ps2:苹果这么搞也是不好玩,还要进入到他自己的设置 页面才能获取wifi列表,坑一~

此类的APP也是有引导用户这么做的,第一次不行,需要重新进一次吧: 而且有人反应该权限现在申请好像比较难,周期较长,半个月还不一定能申请好。

涉及的API:

GPS 部分采用地理围栏相关的API:

适合横向的大范围坐标,但比如写字楼、商场等纵向的情景,纯定位不是很理想。需要借助其他措施。

UNLocationNotificationTrigger 部分的限制:

Region-based notifications aren't always triggered immediately when the edge of the boundary is crossed. The system applies heuristics to ensure that the boundary crossing represents a deliberate event and is not the result of spurious location data. For more information about the heuristics that are applied, see Monitoring the User's Proximity to Geographic Regions.


10【问题】【C++】以下输出结果是什么?分析原因


class A{

public:
int m;
void foo1(){ cout<<“hello”<<endl;}
void foo2(){cout << “hello”<< m <<endl;}
virtual void foo3(){cout << “hello”<<endl;}

};

A*p =NULL;
p->foo1();
p->foo2();
p->foo3();

【难度🌟🌟】【出题人:欧阳大哥-美团-北京】

【答案】

【SAGESSE-iOS-深圳】:

错误分析: foo1和foo2是静态成员函数,调用时编译器会直接生成调用地址, 因为不会访问this指针所以调用不会出现问题;但foo2有访问成员m的操作,这就需要访问this指针了所以会出现段错误; foo3因为是虚函数,所以需要访问虚表,但this是空指针,所以调用也会出现段错误;

输出结果:

因为c++是一个跨平台的语言,在每个平台的 STL 实现都有可能不一样,所以输出结果会有所不同。 在foo2中的 cout << "hello" << m <<endl; 会被拆分成:

cout << "hello";
cout << m;
cout << endl;

当执行到 cout << m; 时,会因为 this->m 而发生段错误,这时的 "hello" 到底有没有输出呢?

第一种情况: 在 Xcode 中直接运行,这时 stdout 直接输出到 Xcode 的控制台,它的输出是即时的, 所以有溃之前就己经输出,所以最终输出结果是

hello
hello
段错误

第二种情况: 在 Xcode 中编译,然后在终端中运行,这时输出不是即时的,写入的数据是缓存在内存里,只有当调用 endl(flush) 时才会真正的去写入文件(io),所以最终输出结果是

hello
段错误

以下为其他同学的讨论部分:

//TODO: 待系统整理

[鹅喵-便利蜂移动端]:

hello
hello
段错误

原因:[鹅喵-便利蜂移动端]cpp的方法调用等价于method(this,args...),只要不访问成员变量或vtable就不会崩

[SAGESSE-iOS-深圳]foo1和foo2是静态成员函数,调用时编译器会直接生成调用地址, foo3因为是虚函数,所以需要访问虚表,但p是空指针,所以会直接段错误, 顺便一提如果foo1和foo2有访问成员m的操作结果又不一样了

【Never-成都-太合乐动-iOS】: foo1 和 foo2 直接地址 调用

【yx@美团北京】: foo2的hello确实也能正常输出 一直到尝试访问m实例变量挂了

看你用的g++ 编译器的行为可能不一样

编译器不重要,实现在STL库里面

xcode debugger的控制台输出是即时的

实测没有 重点是没有执行到endl

cout << "hello";
cout << m;
cout << endl;

foo3 因为是虚函数 会牵扯到 虚表操作

Xcode 可以输出,xcode debugger帮助flush了。 std::endl 一定会刷,其他情况一般不会立即刷。

以下为不同环境的输出结果:


11【iOS】CoreData中几个核心概念及关系阐述下,第三方库 MagicRecord 的读写操作是在什么线程中执行的?为何有人如此讨厌使用CoreData,CoreData适合什么样的项目?【 难度🌟🌟🌟】【出题人 微博@iOS程序犭袁】


12【iOS】kengny 是一名产品经理,他平时有两大爱好:第一,到处在各类群里求买企业证书,第二,运营着一款小成本的视频 app,迫于成本压力,一般只会有两个人参演。他向大风哥提出需求,说希望能够在用户退到后台后,上传日志记录用户什么时候进入的后台,便于记录用户使用时长。并要求退到后台后依然能够下载小视频,这样用户上班点击下载按钮,回到家躺床上打开 APP 就能看了。并且要求把后台下载成功率定为大风哥的KPI。

如果你是大风哥,你将如何应对。必要时贴出示例代码。

【 难度🌟🌟🌟】【出题人 微博@iOS程序犭袁】

【答案】

关于后台下载,我们研究的时候,很有必要把 iOS7 和 iOS7 之后的方案分清楚。 因为即使是现在,我们的 APP 最低版本都是iOS9+,但现在网上很多答案都还是iOS7之前的方案,给读者产生了混淆与误导。

现在分几个部分解答:

iOS7前的陈旧方案

iOS7 之前后台下载方案十分不灵活,现在已经基本不再使用。下面做下介绍:

下面做下详细介绍:

(一)使用 beginBackgroundTaskWithExpirationHandler 函数,向系统申请一段时间来执行需要后台运行的操作,这种方法的缺点是,后台操作最多只能运行10分钟,超过10分钟之后App会休眠。使用这种方法需要APPNAME-info.plist中设置 Application does not run in background 为NO,然后在适当的时间调用 beginBackgroundTaskWithExpirationHandler 函数。

(二)将 App 的后台运行模式设置为 audio 、VOIP、location、Newstand 等,无限制的在后台运行。修改 info.plist -> requried background modes-> App plays audio or streams audio/video using AirPlay , 进入后台,播放无声音乐。前台后台一套下载流程,下载完成,更新数据;下载失败,重新下载尝试。这种方案审核风险较大,不建议使用。因为审核时是可以通过静态分析知道使用了哪些API的,如果一个程序本来就不是音乐类的,却使用了播放音乐的API后台播音乐,有可能就被拒绝,如果想要绕过这个限制,可以向APP增加播放音乐的功能,但这样实际是增加了无用功能。

总结:

第二种,只有极少数 APP 能够用到,一般 APP 无法使用;第一种,运行时间无法保证,无法进行下载恢复等操作,毫无实用性可言。故现在已经很少 APP 在使用上述方式。下面介绍更为实用的方案:

Background Transfer service 特性实现大文件后台下载

注意: NSURLSession 是一个类蔟,不同系统版本实现上是有差异的。很多都要亲自实验一下,而且不同系统版本的行为也不太一样。建议不仅要阅读文档,而且要多多尝试调试。

当 App 使用了 Background Transfer service特性后,可以将一个下载任务交给系统的独立进程去下载,不管App在前台、休眠、以及crash,下载过程都在进行,因为是系统的独立进程在为App进行下载。

基本步骤:

使用 backgroundSessionConfigurationWithIdentifier: 初始化的 configure 初始化一个后台下载使用的 session。 实现 NSURLSessionDelegateNSURLSessionTaskDelegateNSURLSessionDownloadDelegate 中的 URLSession:task:didCompleteWithError: , URLSession:downloadTask:didFinishDownloadingToURL: , urlSessionDidFinishEvents(forBackgroundURLSession:) 和其他业务需求的 protocol 实现 application:handleEventsForBackgroundURLSession:completionHandler: 方法。

点击下载,创建 task 对象并开始:

NSURLSessionDownloadTask *downloadTask = [ session downloadTaskWithRequest:[NSURLRequest requestWithURL: downloadURL];

后台下载成功调用相关代理方法,实现数据和 UI 更新;下载失败从 error 中查找 resumeData,重新开始下载。


QA环节:

Q:下载能控制退到后台的下载速度么,在后台慢慢下,打开在前台全速

A:有的,configureation 里面有一个属性值 discretionary,就是控制的,在后台不占用设备性能的情况下进行下载。

Q:如果在前台,设了这个discretionary的值有用么,也会被系统控制下载速度?

A:没用了,里面强调的是 allows background tasks,如果需要具体限速的数值的话,是没有的,需要自己实现了。限速如果实现,应该是类似断点续传的思路。用suspend和resume做。

Q:后台下载是要用户开的吧?关掉了就只能用播放音乐了。URLSessionDownloadTask 和后台刷新开关有没有关系?

A:URLSessionDownloadTask 和后台刷新开关没有关系。同时,后台下载跟应用的进程没有关系了,是系统做维护的。,用第一种方案也是可以实现,即使用户手动关闭的app,最终也是可以下载成功的,但也不是说还会继续后台下载。

@property(getter=isDiscretionary) BOOL discretionary;

(Apple-Developer-Documentation-API-NSURLSessionConfiguration-discretionary )

此属性设置为 YES 时,系统根据当前性能自动处理后台任务的优先级,以获得最佳性能 (仅background session有效)。根据文档可知:allowsCellularAccessdiscretionary 被用于节省通过蜂窝连接的带宽。建议在使用后台传输的时候,使用 discretionary 属性,而不是 allowsCellularAccess 属性,因为它会把 Wi-Fi 和电源可用性考虑在内。

QA环节结束


用户主动关闭的app,会保存 resumedata ,在后面启动的时候调用didCompleteWithError方法, error 里有resumedata,可以持续下载。 这里有一个缺点就是,iOS11之前,如果因为没有网络导致系统下载失败了,系统即使唤醒了App,App也是没有办法下载的,然后App会进入休眠,即使后面有了网络,系统也不会继续下载,因为只要系统向App发出了失败的信号,除非App 调用resume函数来恢复下载过程,系统是不会自己恢复下载的。这里就需要用到本文提到的BAR ( Background App Refresh)模式,让App过一段时间被系统唤醒,然后App就可以去检查网络,当有网时恢复下载过程,恢复下载的原理类似于断点下载。

iOS11之后,可以采用下面API进行:

URLSession Adaptable Connectivity API

iOS11的重大更新,可以通过 urlSession(_:taskIsWaitingForConnectivity:) 让请求等待网络正常后再自动尝试。

URLSessionTask Scheduling API

通过 URLSessionTask Scheduling API 可以在 App 没有运行的时候下载内容,而手机也会结合实际电量,使用状态去决定是否执行。

参考:

用 BAR ( Background App Refresh)或 Remote notification 实现小文件后台下载

在iOS7以后,系统增加了两种后台的模式,一种是 Background fetch ,另一种是Remote notification,可以用于小文件下载。

BAR ( Background App Refresh)

之前讨论的方案,跟后台刷新没有关系的,没有使用 performFetchWithCompletionHandler 相关的功能

注意:BAR ( Background App Refresh)相关的,用户没有主动kill掉的app才会有 BAR 功能。 具体用法参考:

Remote notification

在iOS7以前,当系统收到推送消息后,会立即弹出消息提示用户,用户点击消息之后,就可以启动App,然后加载数据。使用了这种新的后台模式之后,当系统收到推送消息之后,会唤醒App,给App一个机会执行一部分操作,等操作之后才提醒用户,而且还支持 silent 模式,即执行完操作之后,完全不对用户做任何提醒,默默的就在后台把活干完了,此功能需要用户开启推送权限。


13【iOS】大风哥负责企业内部员工 APP 的iOS开发工作,产品经理 kengny 老师通知说,老板要求,发布2.0,对员工数据进行更新,在 iOS 原有数据库基础上,增加一个字段,用于记录用户 “是否是兄弟”。该字段只有老板有操作权限,如果打开APP后,发现不是兄弟,就弹出离职申请页面。服务端得知填写完成后,会发送指令要求手机原地爆炸。如果不能爆炸的话,远程删除APP,或将手机初始化也可以。

如果你是大风哥你将如何应对。要求数据库操作贴出示例代码,数据库类型不限。

【 难度🌟🌟🌟】【出题人 微博@iOS程序犭袁】


14【算法】请通过编程实现大数(亿位)的相加减乘除。(不限语言) 【 难度🌟🌟🌟】【出题人 消摇-金融-深圳iOSqp】 【提示】用人算的思路让电脑去算。

/one more thing/

多人提供答案:

[Lefex]提供以下答案:

《图解一道面试题 - 大数相加减乘除》

【SAGESSE-iOS-深圳】提供以下答案:

Swift版本:

除法使用移位减实现,不过会这么复杂是因为是以二进制保存的数:

static func + (lhs: XCNumber, rhs: XCNumber) -> XCNumber {
        var cf = 0
        var result = XCNumber()
        for pos in 0 ..< max(lhs.integers.count, rhs.integers.count) {
            let rax = lhs[pos] + rhs[pos] + cf
            result[pos] = rax
            cf = rax &>> XCNumber.nbits
        }
        let maximum = (lhs[.max] + rhs[.max] + cf) & XCNumber.mask
        result.isSigned = maximum & 0x80 != 0 // 最高位的值是否为1, 如果为1说明结果是负数
        if maximum != 0 {
            result.integers.append(.init(truncatingIfNeeded: maximum)) // 如果是结果是非负数,需要保存进位
        }
        return result
    }
    static func - (lhs: XCNumber, rhs: XCNumber) -> XCNumber {
        return lhs + -rhs
    }
    static func * (lhs: XCNumber, rhs: XCNumber) -> XCNumber {
        guard !lhs.isZero && !rhs.isZero else {
            return XCNumber() // 如果任何一个数为0都不需要计算
        }
        return XCNumber.calculator(lhs, rhs) { lhs, rhs in
            var result = XCNumber()
            for row in 0 ..< rhs.integers.count {
                var cf = 0
                var pos = row
                for column in 0 ..< lhs.integers.count {
                    let rax = result[pos] + lhs[column] * rhs[row] + cf
                    result[pos] = rax
                    pos += 1
                    cf = rax &>> XCNumber.nbits
                }
                while cf != 0 {
                    let rax = result[pos] + cf
                    result[pos] = rax
                    pos += 1
                    cf = rax &>> XCNumber.nbits
                }
            }
            return result
        }
    }
    static func / (lhs: XCNumber, rhs: XCNumber) -> XCNumber {
        guard !rhs.isZero else {
            fatalError("0不能作为除数")
        }
        guard !lhs.isZero else {
            return XCNumber() // 如果被除数为0, 不需要处理
        }
        return XCNumber.calculator(lhs, rhs) { lhs, rhs in
            var bit = lhs.integers.count * 8 - 1 // 因为是小于count * 8
            var result = XCNumber()
            var remainder = XCNumber(lhs)
            var divisor = rhs << bit  // 移动到最左边(乘N)
            while bit >= 0 {
                if divisor <= remainder {
                    result.set(1, at: bit) // 直接设置比特位
                    remainder -= divisor
                }
                bit = bit - 1
                divisor = divisor >> 1 // 向右移动(恢复)
            }
            return result
        }
    }

[李胜运-齐数-上海小程序]提供的JS版本,仅仅实现了正数的加 乘。

//TODO: 待完善。

function mergeStr(strA, strB, isIntegerPart){
   let arrA = strA ? strA.split("") : [0];
   let arrB = strB ? strB.split("") : [0];
   let shotArr = arrA.length > arrB.length ? arrB : arrA;
   let longArr = arrA.length <= arrB.length ? arrB : arrA;
   while(shotArr.length < longArr.length){
    if (isIntegerPart) {
     shotArr.unshift("0");
    } else {
     shotArr.push("0");
    }
   }

   let carry = 0;
   let sumArr = [];
   for(let i = longArr.length-1; i >=0; i--){
    let numA = Number(shotArr[i]);
    let numB = Number(longArr[i]);
    let sum = numA + numB + carry;
    sumArr.unshift(sum % 10);
    carry = Math.floor(sum / 10);
   }

   return {
    sumStr : sumArr.join(""),
    carry  : carry
   }
  }
  function add(strA, strB){
   let arrA = strA.split(".");
   let arrB = strB.split(".");
   let intrgerPartObj = mergeStr(arrA[0], arrB[0], true);
   let decimalPartObj = mergeStr(arrA[1], arrB[1], false);

   let carryStr = intrgerPartObj.carry ? intrgerPartObj.carry : "";
   let intrgerPart = carryStr + intrgerPartObj.sumStr;
   let carryIntrgerPartObj = mergeStr(intrgerPart, String(decimalPartObj.carry), true)
   carryStr = carryIntrgerPartObj.carry ? carryIntrgerPartObj.carry : "";

   let decimalPartStr = decimalPartObj.sumStr == "0" ? "" : "." + decimalPartObj.sumStr
   let res = carryStr + carryIntrgerPartObj.sumStr + decimalPartStr;
   return res;
  }

  function mul(strA, strB){
   let pointA =  strA.indexOf(".") == -1 ? 0 : strA.length - 1 - strA.indexOf(".");
   let pointB =  strB.indexOf(".") == -1 ? 0 : strB.length - 1 - strB.indexOf(".");
   let allPoint = pointA + pointB;
   let arrA = strA.replace(".", "").split("");
   let arrB = strB.replace(".", "").split("");
   let shotArr = arrA.length > arrB.length ? arrB : arrA;
   let longArr = arrA.length <= arrB.length ? arrB : arrA;
   while(shotArr.length < longArr.length){
    shotArr.unshift("0");
   }

   let all = [];
   for(let i = longArr.length-1; i >=0; i--){
    let sumArr = [];
    let carry = 0;
    for(let count = 0; count < longArr.length - i - 1; count++){
     sumArr.unshift("0");
    }
    for(let j = shotArr.length - 1; j >= 0; j--){
     let numA = Number(shotArr[i]);
     let numB = Number(longArr[j]);
     let sum = numA * numB + carry;
     sumArr.unshift(sum % 10);
     carry = Math.floor(sum / 10);
    }
    if (carry) {
     sumArr.unshift(carry);
    }
    all.push(sumArr.join(""));
   }

   var res = "0"
   for(let i = 0; i < all.length; i++){
    res = add(res, all[i]);
   }

   let resArr = res.split("");
   if (allPoint > 0) {
    while (allPoint >= resArr.length) {
     resArr.unshift("0");
    }
    resArr.splice(resArr.length - allPoint, 0, ".");
   }
   // return resArr.join("");
   return resArr.join("").replace(/^0+/g, "").replace(/^\./g, "0.");

  }

  console.log(mul("61", "1.0"))
  console.log(mul("88", "10.3"))
  console.log(mul("0.88", "0.103"))
  console.log(mul("12323736.453", "10.03"))
  console.log(mul("11111111", "55555555"))
  console.log(mul(".10", "13"))
  console.log(add("21.1233", "23.5"));
  console.log(add(".33", "5.5"));
  console.log(add("8.000", "23.5"));
  console.log(add("21.1233", "23.5"));
  console.log(add("999999999999999999999", "999999999999999999999"));

//TODO: 未完待续


15 【iOS】多线程操作中,读写操作一定要在同一线程中执行吗?给出原因,并至少给出两种场景佐证你的观点,以及实现方法。【难度🌟🌟】【出题人 微博@iOS程序犭袁】


16 【iOS】一个app中可能会产生几个 Autorelease Pool , Autorelease Pool 中的临时对象,何时会被dealloc 。给出原因。【难度🌟🌟】【出题人 微博@iOS程序犭袁】


17【iOS】For in 循环中频繁创建临时变量的场景下,如何使用 Autorelease Pool 优化, 着重讲下你放置pool的位置,以及这些临时变量的生命周期改变。并给出原因。【难度🌟🌟🌟】【出题人 微博@iOS程序犭袁】


18【算法】【iOS】在一个字典中含有,字符串,字典,数组。层层嵌套,可能十几层。现在想知道任意节点Value中是否含有某个字符串。【 难度🌟🌟🌟🌟】【出题人 BM-成都iOS】 【提示】广度优先,深度优先,为非常朴素的暴力搜索。 暴力搜索也有策略的,看到数组、字典就展开, 这是 深度优先 看到数据、字典先记下来,等这一层所有节点都查完了再展开下一层的,这是广度优先。从数据结构来分析,深度优先是维护一个栈,广度优先是维护一个队列


One more thing...

【非礼勿视】以下为彩蛋部分,建议28岁以上男性观看


/one more thing/

ChenYilong commented 2 years ago

后台定位的权限申请, 可以参考有一些app(已经在App Store上架的)监控你使用手机的频率的应用.

他们的核心原理就是开启无限保活功能, 然后监听锁屏.

使用的小技巧就是, 后台定位唤起后台任务(30s), 任务结束唤起定位. 循环操作.