// 离线包
curl -sSL https://github.com/rvm/rvm/tarball/stable -o rvm-stable.tar.gz
// 创建文件夹
mkdir rvm && cd rvm
// 解包
tar --strip-components=1 -xzf ../rvm-stable.tar.gz
// 安装
./install --auto-dotfiles
// 加载
source ~/.rvm/scripts/rvm
// if --path was specified when instaling rvm, use the specified path rather than '~/.rvm'
// 查询 ruby的版本
rvm list known
在查询 ruby的版本时可能会出现下面的错误:A RVM version () is installed yet 1.25.14 (master) is loaded.Please do one of the following:* 'rvm reload'* open a new shell* 'echo rvm_auto_reload_flag=1 >> ~/.rvmrc' # for auto reload with msg.* 'echo rvm_auto_reload_flag=2 >> ~/.rvmrc' # for silent auto reload.
解决办法: sudo rm -rf /users/your_username/.rvmThen close and reopen the terminal.
mViewLifecycleOwner = new LifecycleOwner() { @Override public Lifecycle getLifecycle(){ if (mViewLifecycleRegistry == null) {
mViewLifecycleRegistry = new LifecycleRegistry(mViewLifecycleOwner);
} return mViewLifecycleRegistry;
}
};
mViewLifecycleRegistry = null;
mView = onCreateView(inflater, container, savedInstanceState); if (mView != null) { // Initialize the LifecycleRegistry if needed
mViewLifecycleOwner.getLifecycle(); // Then inform any Observers of the new LifecycleOwner
mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner); //mViewLifecycleOwnerLiveData在后文介绍
} else { //...
}
}
基于 mViewLifecycleRegistry 创建 mViewLifecycleOwner,
@CallSuper publicvoidonViewStateRestored(@Nullable Bundle savedInstanceState){// called when onCreateView if (mView != null) {
mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
}
@CallSuper publicvoidonDestroyView(){ if (mView != null) {
mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
}
//Android val engine = FlutterEngine(this)
engine.dartExecutor.executeDartEntrypoin(DartExecutor.DartEntrypoint.createDefault())
FlutterEngineCache.getInstance().put(1,engine) val intent = FlutterActivity.withCacheEngine(1).build(this)
//iOS let engine =FlutterEngine()
engine.run() let vc =FlutterViewController(engine:engine,nibName:nil,bundle:nil)
Fluter2 引擎创建
//Android val engineGroup = FlutterEngineGroup(context) val engine1 = engineGroup.createAndRunDefaultEngine(context) val engine2 = engineGroup.createAndRunEngine(context,DartExecutor.DartEntrypoint(FlutterInjector.instance().flutterLoader().findAppBundlePath(),"anotherEntrypoint"))
//iOS let engineGroup =FlutterEngineGroup(name:"example",project:nil) let engine1 = engineGroup.makeEngine(withEntrypoint:nil,libraryURI:nil) let engine2 = engineGroup.makeEngine(withEntrypoint:"anotherEntrypoint",libraryURI:nil)
为什么突然想写一篇总结了呢,其实也是被虐的。今年 3 月份初期,我们商城接了一个 XX 银行的一分购活动(说白点就是薅羊毛),那时候是活动第一期,未曾想到活动入口开放时,流量能直接将 cpu 冲至 100%,导致服务短暂的 502 了。。期间采取了紧急方案到活动结束,但未曾想到还有活动二期,以及上周刚上线的活动三期。想着最近这段时间也做了一些事情,还有遇到的一些坑点,趁此机会,就不偷懒记录一下吧。
活动一期到三期具体做了些什么
技术背景&瓶颈
项目是基于 Vue+SSR 架构的,且没有做缓存处理,没做缓存的主要原因第一个是原本应用 tps 比较低,改造动力不强,并且页面渲染结果中包含了用户数据以及服务端时间,没法在不经过改造的情况下直接上缓存。所以当一期活动大流量冲击时,高并发情况下很容易将 cpu 打至 100%。
薅羊毛活动是真香现场吗~~6 月底产品就和我打了个招呼,说 XX 活动又要有三期了,但整体方案依旧和二期一样不变。我内心:还来???(小声说句打工人太苦了),由于最终时间没定下来,也有了二期的教训之后,和后端同学也一起商量了一下,把活动商品往配置化方向考虑,放在我们配置后台中文案模块且是可行的。针对商品详情页,考虑到不破坏动静分离,先确定下配置化接口返回的数据是静态的,可以放在服务端获取。以下具体三期做的事情:
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.
但更新到测试环境中,页面会失效,点击失效等。会报有如下错误: Failed to execute 'appendChild' on 'Node': This node type does not support this method
分析
Vue SSR 指南在客户端激活里刚好说到了一些需要注意的坑,使用「SSR + 客户端混合」时,需要了解的一件事是,浏览器可能会更改的一些特殊的 HTML 结构。例如 table 中漏写<tbody>,像以下几种情况也会导致导致服务端返回的静态 HTML 和浏览器端渲染的内容不一致:
UI 测试:为组件打快照,第一次运行测试命令会在目录下生成一个组件的 DOM 节点快照,在之后的测试命令中会与快照文件进行 diff 对照,避免在后面对组件进行了非期望的 UI 更改
关键行为:验证组件的基本行为(如:Checkbox 组件的勾选行为)
事件:测试各种事件的触发
属性:测试传入不同属性值是否得到与期望一致的结果
accordion 组件
// accordion.test.tsx
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import toJSON from 'enzyme-to-json';
import JestMock from 'jest-mock';
import React from 'react';
import { Accordion } from '..';
Enzyme.configure({ adapter: new Adapter() }); // 需要根据项目的react版本来配置适配
describe('Accordion', () => {
// 测试套件,通过 describe 块来将测试分组
let onChange: JestMock.Mock<any, any>; // Jest 提供的mock 函数,擦除函数的实际实现、捕获对函数的调用
let wrapper: Enzyme.ReactWrapper;
beforeEach(() => {
// 在运行测试前做的一些准备工作
onChange = jest.fn();
wrapper = mount(
<Accordion onChange={onChange}>
<Accordion.Item name='one' header='one'>
two
</Accordion.Item>
<Accordion.Item name='two' header='two' disabled={true}>
two
</Accordion.Item>
<Accordion.Item name='three' header='three' showIcon={false}>
three
</Accordion.Item>
<Accordion.Item name='four' header='four' active={true} icons={['custom']}>
four
</Accordion.Item>
</Accordion>
);
});
return YES;
} //当应用程序程序失去焦点的时候调用(系统自动调用) - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
//当程序进入后台的时候调用 //一般在这里保存应用程序的数据和状态 - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
//将要进入前台的是时候调用 //一般在该方法中恢复应用程序的数据,以及状态 - (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
//应用程序获得焦点 - (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
// 应用程序即将被销毁的时候会调用该方法 // 注意:如果应用程序处于挂起状态的时候无法调用该方法 - (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO #else //是否将目标对象设置为第一响应者的资格 - (BOOL)canBecomeFirstResponder; // default is NO #endif //成为第一响应者 - (BOOL)becomeFirstResponder;
// Generally, all responders which do custom touch handling should override all four of these methods. // Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each // touch it is handling (those touches it received in touchesBegan:withEvent:). // *** You must handle cancelled touches to ensure correct behavior in your application. Failure to // do so is very likely to lead to incorrect behavior or crashes. #UIResponder内部提供了以下方法来处理事件触摸事件 // UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件 // 一根或者多根手指开始触摸view,系统会自动调用view的下面方法 - (void)touchesBegan:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event; // 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法) - (void)touchesMoved:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event; // 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法 - (void)touchesEnded:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event; // 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法 - (void)touchesCancelled:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event;
// Generally, all responders which do custom press handling should override all four of these methods. // Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each // press it is handling (those presses it received in pressesBegan:withEvent:). // pressesChanged:withEvent: will be invoked for presses that provide an analog value // (like thumbsticks or analog push buttons) // *** You must handle cancelled presses to ensure correct behavior in your application. Failure to // do so is very likely to lead to incorrect behavior or crashes. - (void)pressesBegan:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); - (void)pressesChanged:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); - (void)pressesEnded:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); - (void)pressesCancelled:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0); //加速计事件 - (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); - (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); - (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0); //远程控制事件 - (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(3_0); // Allows an action to be forwarded to another target. By default checks -canPerformAction:withSender: to either return self, or go up the responder chain. - (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender NS_AVAILABLE_IOS(7_0);
typedefNS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
@interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp; @property(nonatomic,readonly) UITouchPhase phase; @property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time @property(nonatomic,readonly) UITouchType type API_AVAILABLE(ios(9.0));
// majorRadius and majorRadiusTolerance are in points // The majorRadius will be accurate +/- the majorRadiusTolerance @property(nonatomic,readonly) CGFloat majorRadius API_AVAILABLE(ios(8.0)); @property(nonatomic,readonly) CGFloat majorRadiusTolerance API_AVAILABLE(ios(8.0));
// Generally, all responders which do custom touch handling should override all four of these methods. // Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each // touch it is handling (those touches it received in touchesBegan:withEvent:). // *** You must handle cancelled touches to ensure correct behavior in your application. Failure to // do so is very likely to lead to incorrect behavior or crashes. - (void)touchesBegan:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event; - (void)touchesMoved:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event; - (void)touchesEnded:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event; - (void)touchesCancelled:(NSSet<UITouch *>*)touches withEvent:(nullable UIEvent *)event; - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *>*)touches API_AVAILABLE(ios(9.1));
// Generally, all responders which do custom press handling should override all four of these methods. // Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each // press it is handling (those presses it received in pressesBegan:withEvent:). // pressesChanged:withEvent: will be invoked for presses that provide an analog value // (like thumbsticks or analog push buttons) // *** You must handle cancelled presses to ensure correct behavior in your application. Failure to // do so is very likely to lead to incorrect behavior or crashes. - (void)pressesBegan:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0)); - (void)pressesChanged:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0)); - (void)pressesEnded:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0)); - (void)pressesCancelled:(NSSet<UIPress *>*)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
#pragma mark - 上下居中,图片在上,文字在下 - (void)verticalCenterImageAndTitle:(CGFloat)spacing
{ // get the size of the elements here for readability
CGSize imageSize =self.imageView.frame.size;
CGSize titleSize =self.titleLabel.frame.size;
// lower the text and push it left to center it self.titleEdgeInsets =UIEdgeInsetsMake(0.0, - imageSize.width, - (imageSize.height + spacing/2), 0.0);
// the text width might have changed (in case it was shortened before due to // lack of space and isn't anymore now), so we get the frame size again
titleSize =self.titleLabel.frame.size;
// raise the image and push it right to center it self.imageEdgeInsets =UIEdgeInsetsMake(- (titleSize.height + spacing/2), 0.0, 0.0, - titleSize.width);
}
#pragma mark - 左右居中,文字在左,图片在右 - (void)horizontalCenterTitleAndImage:(CGFloat)spacing
{ // get the size of the elements here for readability
CGSize imageSize =self.imageView.frame.size;
CGSize titleSize =self.titleLabel.frame.size;
// lower the text and push it left to center it self.titleEdgeInsets =UIEdgeInsetsMake(0.0, - imageSize.width, 0.0, imageSize.width + spacing/2);
// the text width might have changed (in case it was shortened before due to // lack of space and isn't anymore now), so we get the frame size again
titleSize =self.titleLabel.frame.size;
// raise the image and push it right to center it self.imageEdgeInsets =UIEdgeInsetsMake(0.0, titleSize.width + spacing/2, 0.0, - titleSize.width);
}
#pragma mark - 左右居中,图片在左,文字在右 - (void)horizontalCenterImageAndTitle:(CGFloat)spacing;
{ // get the size of the elements here for readability // CGSize imageSize = self.imageView.frame.size; // CGSize titleSize = self.titleLabel.frame.size;
#pragma mark - 文字居中,图片在左边 - (void)horizontalCenterTitleAndImageLeft:(CGFloat)spacing
{ // get the size of the elements here for readability // CGSize imageSize = self.imageView.frame.size; // CGSize titleSize = self.titleLabel.frame.size;
#pragma mark - 文字居中,图片在右边 - (void)horizontalCenterTitleAndImageRight:(CGFloat)spacing
{ // get the size of the elements here for readability
CGSize imageSize =self.imageView.frame.size;
CGSize titleSize =self.titleLabel.frame.size;
// lower the text and push it left to center it self.titleEdgeInsets =UIEdgeInsetsMake(0.0, - imageSize.width, 0.0, 0.0);
// the text width might have changed (in case it was shortened before due to // lack of space and isn't anymore now), so we get the frame size again
titleSize =self.titleLabel.frame.size;
// raise the image and push it right to center it self.imageEdgeInsets =UIEdgeInsetsMake(0.0, titleSize.width + imageSize.width + spacing, 0.0, - titleSize.width);
}
启动的时候,Mach-O 就是通过 mmap 映射到虚拟内存里的(如下图)。下图中部分页被标记为 zero fill,是因为全局变量的初始值往往都是 0,那么这些 0 就没必要存储在二进制里,增加文件大小。操作系统会识别出这些页,在 Page In 之后对其置为 0,这个行为叫做 zero fill。
Page In
启动的路径上会触发很多次 Page In,其实也比较容易理解,因为启动的会读写二进制中的很多内容。Page In 会占去启动耗时的很大一部分,我们来看看单个 Page In 的过程:
MMU 找到空闲的物理内存页面
触发磁盘 IO,把数据读入物理内存
如果是 TEXT 段的页,要进行解密
对解密后的页,进行签名验证
其中解密是大头,IO 其次。为什么要解密呢?
因为 iTunes Connect 会对上传 Mach-O 的 TEXT 段进行加密,防止 IPA 下载下来就直接可以看到代码。这也就是为什么逆向里会有个概念叫做“砸壳”,砸的就是这一层 TEXT 段加密。iOS 13 对这个过程进行了优化,Page In 的时候不需要解密了。
二进制重排
既然 Page In 耗时,有没有什么办法优化呢?
启动具有局部性特征,即只有少部分函数在启动的时候用到,这些函数在二进制中的分布是零散的,所以 Page In 读入的数据利用率并不高。如果我们可以把启动用到的函数排列到二进制的连续区间,那么就可以减少 Page In 的次数,从而优化启动时间:
fun getView(ctx: Context, filename: String): View? {
var name = filename
if(!filename.startsWith("assets/")){
name = "assets/$filename"
}
return LayoutInflater.from(ctx).inflate(am.openXmlResourceParser(name), null)
}
修改完代码后,你紧接着开始了第二波测试,却发现程序又抛出了异常:
java.io.FileNotFoundException: Corrupt XML binary file
function throttle(func, wait) {
let timer = 0;
return (...rest) => {
let now = Date.now();
let that = this;
if (now > timer + delay) {
fn.apply(that, rest);
timer = now;
}
};
}
// 定时器版本 节流函数
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
let throttleAjax = throttle(ajax, 1000)
let inputc = document.getElementById('throttle')
inputc.addEventListener('keyup', function(e) {
throttleAjax(e.target.value)
})
通过_NSKVONotifyingCreateInfoWithOriginalClass 的这段伪代码你会发现我们之前频繁提到 indexedIvars 原来就是在这里初始化生成的。objc_allocateClassPair 在 runtime.h 中的声明为 Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) ,苹果对 extraBytes 参数的解释为“The number of bytes to allocate for indexed ivars at the end of the class and metaclass objects.”,这就是说当我们在通过 objc_allocateClassPair 来生成一个新的类时可以通过指定 extraBytes 来为此类开辟额外的空间用于存储一些数据。系统在生成 KVO 类时会额外分配 0x68 字节的空间,其具体内存布局和用途我用一个结构体描述如下:
由于用到消息转发,我们会将 SD_NSKVONotifying_Test_abcd 的setNum:对应的实现指向_objc_msgForward,然后生成一个新的 SEL__sd_B_abcd_setNum:来指向其子类的原生实现,在我们这个例子中就是 NSKVONotifying_TestsetNum:实现的即void _NSSetIntValueAndNotify(id obj, SEL sel, int number)函数。当 test 实例收到setNum:消息时会先触发消息转发机制,然后 SDMagicHook 的消息调度系统会最终通过向 test 实例发送一个__sd_B_abcd_setNum:消息来实现对被 Hook 的原生方法的回调,而现在__sd_B_abcd_setNum:对应的实现函数正是void _NSSetIntValueAndNotify(id obj, SEL sel, int number),所以__sd_B_abcd_setNum:就会被作为 sel 参数传递到_NSSetIntValueAndNotify函数。然后当_NSSetIntValueAndNotify函数内部尝试从 indexedIvars 拿到原始类 Test 然后从 Test 上查找__sd_B_abcd_setNum:对应的方法并调用时由于找不到对应函数实现而发生 crash。为解决这个问题,我们还需要为 Test 类新增一个__sd_B_abcd_setNum:方法并将其实现指向setNum:的实现,代码如下:
目前还剩下一个问题“先调用 native-KVO 再调用 custom-KVO 再调用 native-KVO,native-KVO 运行正常,custom-KVO 失效,无 crash”。为什么会出现这个问题呢?这次我们依然以 Test 类为例,首先用 Test 类实例化了一个实例 test,然后对 test 的 num 属性进行 native-KVO 操作,这时 test 的 isa 指向了 NSKVONotifying_Test 类。然后我们再对 test 进行 custom-KVO 操作,这时我们的 custom-KVO 会基于 NSKVONotifying_Test 类再生成一个新的子类 SD_NSKVONotifying_Test_abcd,这时如果再对 test 的 num 属性进行 native-KVO 操作就会惊奇地发现 test 的 isa 又重新指向了 NSKVONotifying_Test 类然后 custom-KVO 就全部失效了。
想要弄明白这个问题首先需要研究清楚系统的 KVO 到底是如何实现的,而系统的 KVO 实现又相当复杂,我们该从哪里入手呢?想要弄清楚这个问题,我们首先需要了解下当对被 KVO 观察的目标属性进行赋值操作时到底发生了什么。这里我们以自建的 Test 类为例来说明,我们对 Test 类实例的 num 属性进行 KVO 操作:
当我们给 num 赋值时,可以看到断点命中了 KVO 类自定义的 setNum:的实现即_NSSetIntValueAndNotify 函数
states:当前应用的运行状态,对于Heimdallr-Example这个应用而言是正在前台运行的状态,这类崩溃我们称之为FOOM(Foreground Out Of Memory);与此相对应的也有应用程序在后台发生的 OOM 崩溃,这类崩溃我们称之为BOOM(Background Out Of Memory)。
大多数 VM Region 作为一个单独的内存节点,仅记录起始地址和 Dirty、Swapped 内存作为大小,以及与其他节点之间的引用关系;而 libmalloc 维护的堆内存所在的 VM Region 则由于往往包含大多数业务逻辑中的 Objective-C 对象、C/C++对象、buffer 等,可以获取更详细的引用信息,因此需要单独处理其内部节点、引用关系。
获取所有内存节点之后,我们需要为每个节点找到更加详细的类型名称,用于后续的分析。其中,对于 VM Region 内存节点,我们可以通过 user_tag 赋予它有意义的符号信息;而堆内存对象包含 raw buffer,Objective-C/Swift、C++等对象。对于 Objective-C/Swift、C++这部分,我们通过内存中的一些运行时信息,尝试符号化获取更加详细的信息。
Objective/Swift 对象的符号化相对比较简单,很多三方库都有类似实现,Swift在内存布局上兼容了Objective-C,也有isa指针,objc相关方法可以作用于两种语言的对象上。只要保证 isa 指针合法,对象实例大小满足条件即可认为正确。
C++对象根据是否包含虚表可以分成两类。对于不包含虚表的对象,因为缺乏运行时数据,无法进行处理。
对于对于包含虚表的对象,在调研 mach-o 和 C++的 ABI 文档后,可以通过 std::type_info 和以下几个 section 的信息获取对应的类型信息。
let a = '123456444565456.889'
let b = '121231456.32'
// a + b = '123456565796913.209'
function addTwo(a, b) {
//1.比较两个数长度 然后短的一方前面补0
if (a.length > b.length) {
let arr = Array(a.length - b.length).fill(0);
b = arr.join('') + b
} else if (a.length < b.length) {
let arr = Array(b.length - a.length).fill(0);
a = arr.join('') + a
}
//2.反转两个数 (这里是因为人习惯从左往右加 而数字相加是从右到左 因此反转一下比较好理解)
a = a.split('').reverse();
b = b.split('').reverse();
function base64ToBlob(base64, mimeType) {
let bytes = window.atob(base64);
let ab = new ArrayBuffer(bytes.length);
let ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: mimeType });
}
在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。如果服务器返回的是范围响应,需要使用 206 Partial Content 状态码。假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。服务器允许忽略 Range 首部,从而返回整个文件,状态码用 200 。
随着 Web 技术的不断发展,浏览器的功能也越来越强大。这些年出现了很多在线 Web 设计工具,比如在线 PS、在线海报设计器或在线自定义表单设计器等。这些 Web 设计器允许用户在完成设计之后,把生成的文件保存到本地,其中有一部分设计器就是利用浏览器提供的 Web API 来实现客户端文件下载。下面阿宝哥先来介绍客户端下载中,最常见的 a 标签下载 方案。
而图片下载的功能是借助 dataUrlToBlob 和 saveFile 这两个函数来实现。它们分别用于实现 Data URLs => Blob 的转换和文件的保存,具体的代码如下所示:
function dataUrlToBlob(base64, mimeType) {
let bytes = window.atob(base64.split(",")[1]);
let ab = new ArrayBuffer(bytes.length);
let ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: mimeType });
}
// 保存文件
function saveFile(blob, filename) {
const a = document.createElement("a");
a.download = filename;
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href)
}
在 文件上传,搞懂这8种场景就够了 这篇文章中,阿宝哥介绍了如何利用 JSZip 这个库提供的 API,把待上传目录下的所有文件压缩成 ZIP 文件,然后再把生成的 ZIP 文件上传到服务器。同样,利用 JSZip 这个库,我们可以实现在客户端同时下载多个文件,然后把已下载的文件压缩成 Zip 包,并下载到本地的功能。对应的操作流程如下图所示:
在以上 Gif 图中,阿宝哥演示了把 3 张素材图,打包成 Zip 文件并下载到本地的过程。接下来,我们来介绍如何使用 JSZip 这个库实现以上的功能。
//显示PopupWindow publicvoidshowAtLocation(View parent, int gravity, int x, int y) {
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
//显示PopupWindow publicvoidshowAtLocation(IBinder token, int gravity, int x, int y) { if (isShowing() || mContentView == null) { return;
}
platform :ios, '11.0' project 'iOSPlayground.xcodeproj' target 'iOSPlayground'do pod 'SDWebImage', '~> 5.6.0' pod 'SDWebImageLottieCoder', '~> 0.1.0' pod 'SDWebImageWebPCoder', '~> 0.6.1' end
然后执行 Pod install 命令 bundle exec pod install,CocoaPods 开始为你构建多依赖的开发环境;整个 Pod Install 流程最核心的就是 ::Pod::Installer 类,Pod Install 命令会初始化并配置 Installer,然后执行 install! 流程,install! 流程主要包括 6 个环节
def install! prepare resolve_dependencies # 依赖决议 download_dependencies # 依赖下载 validate_targets # Pods 校验 generate_pods_project # Pods Project 生成 if installation_options.integrate_targets? integrate_user_project # User Project 整合 else UI.section 'Skipping User Project Integration' end perform_post_install_actions # 收尾 end
下面会对这 5 个流程做一些简单分析,为了简单起见,我们会忽略一些细节。
准备阶段
这个流程主要是在 Pod Install 前做一些环境检查,并且初始化 Pod Install 的执行环境。
Pod Install 执行完成后,就将 User Target 整合到了 CocoaPods 环境中。User Target 依赖 Aggregate Target,Aggregate Target 依赖所有 Pod Targets,Pod Targets 按照 Pod 描述文件(Podspec)中的依赖关系进行依赖,这些依赖关系保证了编译顺序
iOSPlayground 工程中 User Target: iOSPlayground 依赖了 Aggregate Target 的产物 libPods-iOSPlayground.a
CocoaPods 的依赖版本决议流程是基于 Molinillo 的,Molinillo 是基于 DAG 来进行依赖解析的,通过构建图可以方便的进行依赖关系查找、依赖环查找、版本降级等。但是使用图来进行解析是有成本的,实际上大部分的本地依赖决议场景并不需要这么复杂,Podfile.lock 中的版本就是决议后的版本,大部分的研发流程直接使用 Podfile.lock 进行线性决议就可以,这可以大幅加快决议速度。
Specification 缓存
依赖分析流程中,CocoaPods 需要获取满足约束的 Specifications,1.7.5 上的流程是获取一个组件的所有版本的 Specifications 并缓存,然后从 Specifications 中筛选出满足约束的 Specifications。对于复杂的项目来说,往往对一个依赖的约束来自于多个组件,比如 A 依赖 F(>=0),B 依赖 F (>=0),在分析完 A 对 F 的依赖后,在处理 B 对 F 的依赖时,还是需要进行一次全量比较。通过优化 Specification 缓存层可以减少这部分耗时,直接返回。
module Pod::Specification::Set def all_specifications(warn_for_multiple_pod_sources, requirement) @all_specifications ||= {} @all_specifications[requirement] ||= begin #... end end end
module Pod::Requirement def eql?(other) @requirements.eql? other.requirements end end
循环依赖发现
当出现循环依赖时,CocoaPods 会报错,但报错信息只有谁和谁之间存在循环依赖,比如:
There is a circular dependency between A/S1 and D/S1
随着工程的复杂度提高,对于复杂的循环依赖关系,比如 A/S1 -> B -> C-> D/S2 -> D/S1 -> A/S1, 基于上面的信息我们很难找到真正的链路,而且循环依赖往往不止一条,subspec、default spec 等设置也提高了问题定位的复杂度。我们优化了循环依赖的报错,当出现循环依赖的时候,比如 A 和 D 之间有环,我们会查找 A -> D/S1 之前所有的路径,并打印出来:
There is a circular dependency between A/S1 and D/S1 Possible Paths:A/S1 -> B -> C-> D/S2 -> D/S1 -> A/S1 A/S1 -> B -> C -> C2 -> D/S2 -> D/S1 -> A/S1 A/S1 -> B -> C -> C3 -> C2 -> D/S2 -> D/S1 -> A/S1
在 Pods 工程生成流程中有三个流程会比较耗时,这些数据每次 Pod Install 都需要重新生成:
Pod 目录下的文件和目录列表,需要对目录下的所有节点做遍历;
Pod 目录下的动态库列表,需要分析二进制格式,判断是否为动态库;
Pod 文件的访问策略缓存 glob_cache,这个 glob_cache 是用于访问组件仓库中不同类型文件的,比如 source files、headers、frameworks、bundles 等。
但其实这些数据对固定版本的依赖都是唯一的,如果可以缓存一份就可以避免二次生成导致的额外耗时,我们补充了这个缓存层,以抖音为例子,使 Pod Clean Install 减少了 36%,Pod No-clean Install 减少了 42%
添加 FileAccessors 缓存层后,在效率上获得提升的同时,在稳定性上也获得了提升。因为在本地记录了 Pod 完整的文件结构,因此我们可以对 Pod 的内容做检查,避免 Pod 内容被删除导致构建失败。比如研发同学误删了缓存中的二进制库,CocoaPods 默认是难以发现的,需要延迟到链接阶段报 Symbol Not Found 的错误,但是基于 FileAccessors 缓存层,我们可以在 Pod Install 流程对 Pod 内容做检查,提前暴露出二进制库缺失,触发重新下载。
提高编译并发度
Pod Target 的依赖关系会保证 Target 按顺序编译,但是会导致 Target 编译的并发度下降,一定程度上降低了编译效率。其实生成静态库的 Pod Target 不需要按顺序进行编译,因为静态库编译不依赖产物,只是在最后进行链接。通过移除静态库的 Pod Target 对其他 Target 的依赖,可以提高整体的编译效率。
在 Multi Project 下,「Dependency Subproject」会导致索引混乱,移除静态库的 Pod Target 对其他 Target 的依赖后,我们也可以删除 Dependent Pod Subproject,减少 Xcode 检索问题。
Arguments Too Long
超大型工程在编译时稳定性降低,往往会因为工程放置的目录长产生一些未定义错误,其中错误比较大的来源就是 Arguments Too Long,表现为:
Build operation failed without specifying any errors ;Verify final result code for completed build operation
组件化的一个目标是业务代码按架构设计拆分成组件 Pod。但如果在一个组件中新增文件,比如在组件 A 中新增文件,依赖组件 A 的组件 B 是不能直接访问新增文件的头文件的,需要重新执行 Pod Install,这样会影响整体的研发效率。
为什么组件 B 不能够访问组件 A 的新增文件?在 Pod Install 后,组件 A 公共访问的头文件被索引在 Pods/Headers/Public/A/ 目录下,组件 B 的 HEADER_SEARCH_PATH 中配置了 Pods/Headers/Public/A/,因此就可以在组件 B 的代码里引入组件 A 的头文件。新增头文件的头文件没有在目录中索引,所以组件 B 就访问不到了。只需要在添加文件后,建立新增头文件的索引到 Pods/Headers/Public/A/目录下,就可以为组件 B 提供组件 A 新增文件的访问能力,这样就不需要重新 Pod Install 了。
Lockfile 生成
在依赖管理的部分场景中,我们只需要进行依赖决议,重新生成 Podfile.lock,但通过 Pod Install 生成是需要执行依赖下载及后续流程的,这些流程是比较耗时的,为了支持 Podfile.lock 的快速生成,可以对 install 命令做了简化,在依赖决议后就可以直接生成 Podfile.lock:
class Pod::Installer def quick_generate_lockfile! # 初始化 sandbox 环境 quick_prepare_env quick_resolve_dependencies quick_write_lockfiles end end
首先我们假设所有数据都是二维空间下的点,我们从图中这个R8区域说起,也就是那个shape of data object。别把那一块不规则图形看成一个数据,我们把它看作是多个数据围成的一个区域。为了实现R树结构,我们用一个最小边界矩形恰好框住这个不规则区域,这样,我们就构造出了一个区域:R8。R8的特点很明显,就是正正好好框住所有在此区域中的数据。其他实线包围住的区域,如R9,R10,R12等都是同样的道理。这样一来,我们一共得到了12个最最基本的最小矩形。这些矩形都将被存储在子结点中。