feature: App 逆向防护

This commit is contained in:
杭城小刘
2024-07-17 23:30:50 +08:00
parent 83fefff66b
commit e3fde7a1df
15 changed files with 380 additions and 38 deletions

View File

@@ -57,13 +57,13 @@ Tips`[UIView setNeedsDisplay]` 之后并不会立马调用 `[view.layer setN
## 渲染机制
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RenderStructure.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RenderStructure.png)
iOS 渲染框架可以分为4层顶层是 UIKit包括图形界面的高级 API 和常用的各种 UI 控件。UIKit 下层是 Core Animation不要被名字误解了它不光是处理动画相关也在做图形渲染相关的事情(比如 UIView 的 CALayer 就处于 Core Animation 中)。Core Animation 之下就是由 OpenGL ES 和 CoreGraphics 组成的图形渲染层OpenGL ES 主要操作 GPU 进行图形渲染CoreGraphics 主要操作 CPU 进行图形渲染。上面3层都属于渲染图形软件层再下层就是图形显示硬件层。
iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core Graphics、Core Image 有 CPU 预处理,最终通过 OpenGL ES 将数据传输给 GPU最终显示到屏幕上。
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/RenderPipeline.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/RenderPipeline.png)
- Core Animation 提交会话(事务),包括自己和子树(view hierarchy) 的布局状态
@@ -75,13 +75,13 @@ iOS 图形界面的显示是一个复杂的流程,一部分数据通过 Core G
## Core Animation
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/APM-CoreAnimationPipeline.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/APM-CoreAnimationPipeline.png)
可以看到 Core Animation pipeline 由4部分组成Application 层的 Core Animation 部分、Render Server 中的 Core Animation 部分、GPU 渲染、显示器显示。
### Application 层 Core Animation 部分
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CoreAnimationCommit.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/CoreAnimationCommit.png)
- 布局(Layout)`layoutSubviews``addSubview`,这里通常是 CPU、IO 繁忙
@@ -107,7 +107,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
## UIView 绘制流程
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/UIRenderPipeline.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIRenderPipeline.png)
- 每个 UIView 都有一个 CALayerlayer 属性都有 contentscontents 其实是一块缓存,叫做 backing store
@@ -115,7 +115,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
- 当 backing store 写完后,通过 Render Server 交给 GPU 去渲染,最后显示到屏幕上
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/UIViewRenderPipeline.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/UIViewRenderPipeline.png)
- 调用 `[UIView setNeedsDisplay]` 方法时,并没有立即执行绘制工作,而是马上调用 `[view.layer setNeedsDisplay]` 方法,给当前 layer 打上标记
@@ -131,7 +131,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
### 系统绘制流程
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/iOSRenderProcess.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSRenderProcess.png)
- 首先 CALayer 内部会创建一个 CGContextRef在 drwaRect 方法中,可以通过上下文堆栈取出 context拿到当前视图渲染上下文也就是 backing store
@@ -143,7 +143,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
### 异步绘制流程
![](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/iOSAsyncRender.png)
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/iOSAsyncRender.png)
- 如果 layer 有代理对象,且代理对象实现了代理方法,则可以进入异步绘制流程
@@ -170,7 +170,7 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
这个流程看起来没有什么问题但是注意Core Animation库自身虽然支持异步线程渲染在macOS上可以手动开启但是UIKit的这套内建的pipeline全部都是发生在主线程的。
因此当一个CGImage是采取了惰性解码通过Image/IO生成出来的那么将会在主线程触发先前提到的惰性解码callback实际上Core Animation的调用触发了一个`CGDataProviderRetainBytePtr`这时候Image/IO的具体解码器会根据先前的图像元信息去分配内存创建Bitmap Buffer这一步骤也发生在主线程。
因此当一个CGImage是采取了惰性解码通过Image/IO生成出来的那么将会在主线程触发先前提到的惰性解码callback实际上Core Animation的调用触发了一个`CGDataProviderRetainBytePtr`这时候Image/IO的具体解码器会根据先前的图像元信息去分配内存创建Bitmap Buffer这一步骤也发生在主线程。
这个流程带来的问题在于主线程过多的频繁操作会造成渲染帧率的下降。实验可以看出通过原生这一套流程对于一个1000*1000的PNG图片第一次滚动帧率大概会降低5-6帧iPhone 5S上当年有人的测试。后续帧率不受影响因为是惰性解码解码完成后的Bitmap Buffer会复用。
@@ -187,3 +187,212 @@ Render Server 是一个独立的渲染进程,当收到来自 Application 的 (
缺点提前解码会立即分配Bitmap Buffer的内存增加了内存压力。举例子对于一张大图2048*2048像素32位色来说就会立即分配16MB(2048 * 2048 * 4 Bytes)的内存。
由此可见这是一个拿空间换时间的策略。但是实际上iOS设备早期的内存都是非常有限的UIKit整套渲染机制很多地方采取的都是时间换空间因此最终苹果没有使用这套Pipeline而是依赖于高性能的硬件解码器+其他优化来保证内存开销稳定。当然作为图片库和开发者这就属于仁者见仁的策略了。如大量小图渲染的时候开启Force Decode能明显提升帧率同时内存开销也比较稳定。
## iOS 图片解压缩到渲染过程
- 假设我们使用 `+imageWithContentsOfFile:` 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩
- 然后将生成的 `UIImage` 赋值给 `UIImageView`
- 接着一个隐式的 `CATransaction` 捕获到了 `UIImageView` 图层树的变化
- 在主线程的下一个 `runloop` 到来时,`Core Animation` 提交了这个隐式的 `transaction` ,这个过程可能会对图片进行 `copy` 操作,而受图片是否字节对齐等因素的影响,这个 `copy` 操作可能会涉及以下部分或全部步骤
- 分配内存缓冲区用于管理文件 IO 和解压缩操作
- 将文件数据从磁盘读到内存中
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作
- 最后 `Core Animation``CALayer` 使用未压缩的位图数据渲染 `UIImageView` 的图层
- CPU 计算好图片的 Frame,对图片解压之后.就会交给 GPU 来做图片渲染
- 渲染流程
- GPU 获取图片的坐标
- 将坐标交给顶点着色器(顶点计算)
- 将图片光栅化(获取图片对应屏幕上的像素点)
- 片元着色器计算(计算每个像素点的最终显示颜色值)
- 从帧缓存区中渲染到屏幕上
我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出
## 为什么要解压缩图片
既然图片的解压缩很耗费 CPU 时间,那么为什么还要对图片进行解压缩?是否可以不解压缩直接显示图片?不能
其实位图,就是一个像素数组,数组中的每个像素就代表图片中的一个点。平时遇到的 png、jpeg 就是位图。
```objective-c
UIImage *image = [UIImage imageNamed:@"text.png"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
```
rawData 就是图片原始数据。
jpg、png 都是一种压缩格式,只不过 png 是无损压缩,支持 alpha 通道。而 jpeg 是有损压缩可以指定0100%压缩比。iOS 提供2个函数来生成 png、jpeg 图片。
```objective-c
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);
// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);
```
所以,在磁盘的图片渲染到屏幕之前,必须先得到图片的原始像素数据,才可以执行后续的操作。所以必须先解压缩。
## 图片解压缩原理
既然图片的解压缩不可避免,而我们也不想让它在主线程执行,影响 App 性能,那么是否有比较好的解决方案呢?
我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在**子线程提前对图片进行强制解压缩**。
而强制解压缩的原理就是**对图片进行重新绘制,得到一张新的解压缩后的位图**。其中,用到的最核心的函数是 `CGBitmapContextCreate`
```objective-c
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
```
参数说明:
- data如果不为 `NULL` ,那么它应该指向一块大小至少为 `bytesPerRow * height` 字节的内存;如果 为 `NULL`,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 `NULL` 即可;
- width 和 height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
- bitsPerComponent像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
- bytesPerRow :位图的每一行使用的字节数,大小至少为 `width * bytes per pixel` 字节。当我们指定 0/NULL 时,系统不仅会为我们自动计算,而且还会进行 `cache line alignment` 的优化
- space :就是我们前面提到的颜色空间,一般使用 RGB 即可;
- bitmapInfo :位图的布局信息.`kCGImageAlphaPremultipliedFirst`
参考 YYImage/SDWebImage 都有图片解压缩的实现
```objective-c
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
...
if (decodeForDisplay) { // decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage;
} else {
...
}
}
```
自己也可以实现
```objective-c
- (void)setImage {
SP_BEGIN_LOG(custome, gl_log, imageSet);
[self decodeImage:[UIImage imageNamed:@"peacock"] completion:^(UIImage *image) {
self.imageView.image = image;
SP_END_LOG(imageSet);
}];
}
- (void)decodeImage:(UIImage *)image completion:(void(^)(UIImage *image))completionHandler {
if (!image) return;
//在子线程执行解码操作
dispatch_async(dispatch_get_global_queue(0, 0), ^{
CGImageRef imageRef = image.CGImage;
//获取像素宽和像素高
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
if (width == 0 || height == 0) return ;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
//判断颜色是否含有alpha通道
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
//在iOS中使用的是小端模式在mac中使用的是大端模式为了兼容我们使用kCGBitmapByteOrder32Host32位字节顺序该宏在不同的平台上面会自动组装换成不同的模式。
/*
#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else //Little endian.
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
*/
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
//根据是否含有alpha通道如果有则使用kCGImageAlphaPremultipliedFirstARGB否则使用kCGImageAlphaNoneSkipFirstRGB
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
//创建一个位图上下文
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
if (!context) return;
//将原始图片绘制到上下文当中
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
//创建一张新的解压后的位图
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
UIImage *originImage =[UIImage imageWithCGImage:newImage scale:[UIScreen mainScreen].scale orientation:image.imageOrientation];
//回到主线程回调
dispatch_async(dispatch_get_main_queue(), ^{
!completionHandler ?: completionHandler(originImage);
});
});
}
```