使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

1.冷启动

1.1 什么是冷启动?

冷启动是指内存中不包含该应用程序相关的数据,必须要 从磁盘载入到内存中的启动过程。

注意: 重新打开 APP, 不一定就是冷启动。

  1. 当内存不足,APP被系统自动杀死后,再启动就是冷启动。
  2. 如果在重新打开 APP 之前,APP 的相关数据 还存储在内存中,这时再打开 APP,就是 热启动
  3. 冷启动与热启动是由 系统决定的,我们无法决定。
  4. 当然 设备重启以后,第一次打开 APP 的过程, 一定是冷启动

1.2 如何统计冷启动耗时?

一般来讲,统计 APP 启动时长,以 main 函数为节点 ,分两个大阶段:

  • main 函数之后的代码,是我们自己写的,我们可以 自行统计进入 main 函数到第一个界面显示的耗时

*
– 在 main 函数里打印一下当前的时间,
– 在第一个要显示的控制器的 viewDidLoad 方法中打印一下当前时间
– 两个时间的差值,即为 main函数后的加载时长

  • main 函数之前,为 pre-main 阶段,由于是系统在做事情,这段时间 耗时,我们没办法直接统计, 需要查 看系统给我们的反馈

1.2.1 pre-main阶段都做了什么?

接下来看一下项目中的 pre-main 阶段的耗时。

  • 查看系统给的反馈需要 增加一个环境变量
  • 增加路径:在 Xcode -> Edit Scheme -> Run -> Arguments -> Environment Variables 中,
  • 增加一个环境变量 *DYLD_PRINT_STATISTICS:1。

下图是我项目的加载耗时:

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

耗时过程分为以下4部分:

  1. dylib loading time : 是指 动态库加载的耗时, 系统的动态库做过优化,耗时较少。 苹果官方推荐最多 不要超过6个外部动态库,多余6个,需要考虑合并动态库, 合并动态库对于启动时期的优化,非常有效。 像微信的动态库早期有八九个,现在也优化成6个了。
  2. rebase/binding

  3. rebase:是指 地址的 偏移修正耗时。

*
在编译生成二进制文件的时候,每个 函数都有一个地址,这个地址 是相对于二进制文件的偏移地址
在启动时,也就是在二进制文件在加载到虚拟内存的时候,为了安全起见,苹果有个安全机制( ASLR), 会在整个二进制文件的最前面随机加一个偏移值
– 比如 A 函数,相对于二进制文件的偏移值是 0x003。 启动时,整个二进制文件被分配了一个随机值0x100。 那么 A 函数在内存中的实际地址是 0x003 + 0x100 = 0x103。
– 偏移修正指的就是 计算方法在虚拟内存中的地址的过程!

  • *binding: 动态库的方法绑定,是指将方法名字与方法的实现进行绑定过程的耗时。

*
– 比如 NSLog 方法,在加载的时候需要先找到Foundation库,再找到库里的NSLog的方法的实现,将 *方法名字和方法实现绑定在一起。

  1. Objc setup time: 注册所有 OC类 耗时, 类越多耗时越多,有人统计过2万个自定义的OC的类,大概耗时800毫秒。 删除不用的类,可以减少耗时。
  2. initializer time: load方法 和 C++构造函数的耗时. 减少重写load方法,尽量将事情延迟到 main 方法以后,可以减少耗时。
  3. slowest intializers : 指出了最耗时的几个库是下面的6个库(最后一个是我的项目)。

1.2.2 pre-main阶段耗时优化方法总结:

  • 减少外部动态库的数量
  • 不用的类和方法,删除
  • 类尽量使用懒加载,也就是尽量不要重写load方法。
  • 启动时加载的数据使用多线程
  • *使用纯代码。不用xib storyboard(要额外进行代码解析转换和页面的渲染)

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:6205024a-2e3c-4d3e-9783-4a5896a1c6ba

[En]

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:1a6ccf62-011d-4dcf-b7d0-a722a2e38942

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:8eba8888-475f-4edf-87d3-b64e662e83e1

[En]

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:44965a39-5a41-4c1b-bb01-7853074116d1

  1. 二进制重排

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:31d4f850-911e-42f3-9d5a-64195b6bdc06

[En]

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:074e015b-ba80-4ecf-b1f5-030fa81654ae

我们已经知道数据加载到内存的过程, 当虚拟内存页还没有对应的物理内存页时,会出现缺页异常(PageFault)。

当冷启动时,物理内存中是没有数据的,这时会出现大量的缺页异常,在iOS生产环境的app,在发生Page Fault进行重新加载时,iOS系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 比Debug环境下所产生的耗时更多

这里有没有优化空间呢?接下来就是优化方案:二进制重排!

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:d7d5d7b8-dcaf-4ea9-b16f-7536c219e6b1

[En]

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:87e51828-a624-4624-8aa6-5da76a9629ab

2.2.1 二进制文件中方法实现排序是什么样的?

  1. 在 viewController 中,先随便写几个方法。

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  1. 再看下源文件的编译顺序

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

接下来查看 Link Map文件查看符号顺序, 查看方式:

  1. 打开link map

****

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  1. 编译生成link map 文件
  2. 找到link map 文件

  3. 项目目录中,生成的 app 右键,show in Finder

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  • 找到 app 的上上级目录

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  • 进入Intermediates.noindex -> TraceDemo.build -> Debug-iphonesimulator -> TraceDemo.build -> TraceDemo-LinkMap-normal-x86_64.txt

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  1. 打开link map 文件,找到自己的类及方法的名字

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  1. 我们可以直观的看出 link map中符号的顺序,类是以源文件的编译顺序,从上到下按序排列。方法名是以类中方法的书写顺序,由上到下排序。

2.2.2 为什么需要二进制重排?

从源码的执行顺序上看,应该是 load -> test2 -> viewDidLoad -> test1.

但是二进制文件中 符号的顺序是方法从 上到下的书写顺序没有按照调用顺序去排列。

在冷启动分页 加载二进制文件时,发现 很多页中都有启动时需要用到的方法,那么即使页里面也存在启动时不需要的方法,但是由于内存是分页管理的,要加载就要 整页加载。这样就导致了 大量不需要在 pre-main 阶段执行的方法,也会被加载到内存中,增加了启动的耗时。

\

例如,启动需要加载100个页,每个页可以包含20个方法。但是每个页里只有2个方法是启动时后用到的。这样实际上启动时必须要的方法是2 * 100 = 200个,如果将这200个方法紧挨着放在一起,那么只需要2页。比100个页,减少了98页。这样耗时就会大大降低。

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

2.2.3 如何进行二进制重排?

1. 二进制重排的方法

在项目编译生成二进制文件的时候,找到启动时需要的方法,并且将它们放在一起 重新排序,这就是二进制重排。

两个关键点: 找到启动时需要方法 & 方法 的重排序

2.方法的重排序:

重排序其实很简单。 xcode已经为我们提供了这个机制,它使用的链接器叫做 ld, ld有一个参数叫做Order File, 我们可以通过 配置order文件,来使编译时生成的二进制的文件的Link Map种的符号顺序,按照我们指定的顺序排列生成。而且 libobjc 实际上也做了二进制重排

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

【第一步】在项目根目录下 建一个xxx.order的文件,里面写上按照自己想排列的顺序,写上方法或者函数的名字。(如果写了一个不存在的符号,也不会报错,会被自动过滤掉~)

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

【第二步】在 Build Settings 搜索order file 的文件。将项目根目录创建的文件,设置上去。

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

【第三步】 重新编译,查看 Link Map 文件的顺序,果然,按照我们指定的顺序排列啦!

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

3. 静态插桩 – 找到冷启动时的所有方法

接下来,需要做的就是 写入 order 文件里的符号了,我们不可能手写上所有的启动时需要的执行的符号,这里的所有符号包括, 调用的方法、函数、C++构造方法、swift方法、block。

这里使用 LLVM 内置的简单 代码覆盖率检测工具SanitizerCoverage)。它在 边缘、 函数、基本块 级别上插入对用户定义函数的调用。

  • edge (默认): 检测边缘(所有的指令跳转都会被插入对用户定义函数的调用, 如 循环、分支判断、方法函数等)。
  • bb检测基本块。
  • func:仅将检测 每个 功能的输入块(这个就是我们要重排序的符号)。

按照文档,

  • 【第1步】搜索并设置 Other C Flags/ Other C++ Flags-fsanitize-coverage=func,trace-pc-guard ( 这里要用func, 不能用默认的edge, 不然会造成死循环)。
  • 如果有swift ,需要设置 Other Swift Flags 设置为 * -sanitize-coverage=func -sanitize=undefined

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  • 【第2步】编译器将插入对 模块构造函数的调用,所以我们要实现这个方法:
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);

通过打印start, stop 地址的内容,从 start 地址开始,到 stop 地址的前4位,存储的是 uint32 的 1-19的数字。

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

我们可以从这个函数中知道, 当前项目中自定义的功能输入块的数量。

  • 【第3步】 编译器会在生成二进制文件的时候,在每个func调用之初,插入以下代码
__sanitizer_cov_trace_pc_guard(&guard_variable)

也就是说,每个方法在执行的时候,都会调用上面这个方法。 接下来:

*

1. 我们要实现这个方法,并在这个方法里, *获取到本方法结束后要返回的地址

// 获取到本方法结束后,要返回的地址去,这个地址包含在被hook的方法内部,但不是被hook 的方法的首地址
void *PC = __builtin_return_address(0);

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

*

1. 并将地址保存一个 系统的原子队列( ( 底层实际上是个栈结构 , 利用队列结构 + 原子性来保证顺序 ))中,使用原子队列,是为了 防止多线程资源抢夺。原子队列的存值方法如下:

// 将结构体存入到原子队列中。
// offsetof(type,member) 返回结构体中成员的偏移值,由于指针PC是8字节,所以这里返回8字节。
// 详见下图
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

每个 SYNode 首地址都距离上一个偏移 PC 所占的字节数。这样做的妙处就是,每个 SYNode 的 next 的地址,恰巧就是下一个结构体的地址。这样方便获取队列里面的所有数据。

  • 【第4步】我们在点击屏幕的事件中

*
– 把存储到原子队列中的地址遍历出来,
– 根据地址获取当前地址所在的方法的名称并存入数组中,

typedef struct dl_info {
        const char      *dli_fname;     /* 所在文件 */
        void            *dli_fbase;     /* 文件地址 */
        const char      *dli_sname;     /* 符号名称 */
        void            *dli_saddr;     /* 函数起始地址 */
} Dl_info;

//这个函数能通过函数内部地址找到函数符号
int dladdr(const void *, Dl_info *);

*
– 由于原子队列是栈结构,先进后出,所以我们需要将数组倒序排列
– 由于方法可能会被多次调用,我们需要去重
– 再将最后我们当前点击屏幕的方法删除掉
– 将方法名字的数组,转成字符串,写到沙盒文件中

完整代码如下:

//
//  ViewController.m
//  TraceDemo
//
//  Created by hank on 2020/3/16.

//  Copyright © 2020 hank. All rights reserved.

//

#import "ViewController.h"
#import <dlfcn.h>
#import <libkern osatomic.h>
#import "TraceDemo-Swift.h"

@interface ViewController ()

@end

@implementation ViewController

+(void)initialize
{

}

void(^block1)(void) = ^(void) {

};

void test(){
    block1();

}

+(void)load
{

}

- (void)viewDidLoad {
    [super viewDidLoad];
    [SwiftTest swiftTestLoad];
    test();
}

-(void)touchesBegan:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event
{
    NSMutableArray <nsstring *> * symbolNames = [NSMutableArray array];

    while (YES) {
        SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        if (node == NULL) {
            break;
        }
        Dl_info info;
        dladdr(node->pc, &info);
        NSString * name = @(info.dli_sname);
        BOOL  isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
        [symbolNames addObject:symbolName];
    }
    //&#x53D6;&#x53CD;
    NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
    //&#x53BB;&#x91CD;
    NSMutableArray<nsstring *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    while (name = [emt nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    //&#x79FB;&#x9664;&#x672C;&#x65B9;&#x6CD5;
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //&#x5C06;&#x6570;&#x7EC4;&#x53D8;&#x6210;&#x5B57;&#x7B26;&#x4E32;
    NSString * funcStr = [funcs  componentsJoinedByString:@"\n"];

    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"demo.order"];
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    NSLog(@"%@",funcStr);
}

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.

  if (start == stop || *start) return;  // Initialize only once.

  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.

}

//&#x539F;&#x5B50;&#x961F;&#x5217;
static  OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//&#x5B9A;&#x4E49;&#x7B26;&#x53F7;&#x7ED3;&#x6784;&#x4F53;
typedef struct {
    void *pc;
    void *next;
}SYNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    // &#x4F1A;&#x5BFC;&#x81F4;load &#x65B9;&#x6CD5;&#x88AB;return
//    if (!*guard) return;
    // &#x83B7;&#x53D6;&#x5230;&#x672C;&#x65B9;&#x6CD5;&#x7ED3;&#x675F;&#x540E;&#xFF0C;&#x8981;&#x8FD4;&#x56DE;&#x7684;&#x5730;&#x5740;&#x53BB;&#xFF0C;&#x8FD9;&#x4E2A;&#x5730;&#x5740;&#x5305;&#x542B;&#x5728;&#x88AB;hook&#x7684;&#x65B9;&#x6CD5;&#x5185;&#x90E8;&#xFF0C;&#x4F46;&#x4E0D;&#x662F;&#x88AB;hook &#x7684;&#x65B9;&#x6CD5;&#x7684;&#x9996;&#x5730;&#x5740;
    void *PC = __builtin_return_address(0);
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //&#x8FDB;&#x5165;
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}

@end

</nsstring></nsstring></uitouch></libkern></dlfcn.h>

2.2.4 如何验证二进制重排的效果?

1.查看缺页异常数量Page Fualt:

  1. 查看一下项目的 缺页异常数量。注意需要卸载 APP 或者重启手机,来保证这个APP完全没有被加载到内存中,因为如果物理内存中有该APP的数据,
  2. 打开 Instrument -> System Trace

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
  1. 选择真机、项目、点击启动,当第一个页面显示出来后,点击停止。

  2. xcode 12搜索 main thread, 选择 Virtual MemoryFile Backed Page in 就是缺页异常的数量

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

优化前:项目的缺页遗产数量是427

优化后:

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

优化前:项目的缺页遗产数量是286

减少了启动时大概40%的缺页异常~

3.自动更新order 文件

随着代码迭代,order文件需要更新,每次手动更新很麻烦,所以需要自动更新。

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化
brew install ios-deploy
APP_ORDER_DIR=appOrderDir
APP_ORDER=./$APP_ORDER_DIR/Documents/app.order
mkdir $APP_ORDER_DIR
ios-deploy --download=/Documents --bundle_id $PRODUCT_BUNDLE_IDENTIFIER --to ./$APP_ORDER_DIR

if [ -e $APP_ORDER ] ;then
cp -f $APP_ORDER ./Resource/app.order
fi
rm -r $APP_ORDER_DIR


【补充xcode13】查看缺页异常的方式

选择真机、项目、点击启动,当第一个页面显示出来后,点击停止。

使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:67ebd246-7f17-4b42-a46a-01e02c0237b5

[En]

[TencentCloudSDKException] code:FailedOperation.ServiceIsolate message:service is stopped due to arrears, please recharge your account in Tencent Cloud requestId:8bdf60e1-4c55-45e3-b76a-4990b63cc27c

Original: https://www.cnblogs.com/mysweetAngleBaby/p/16608427.html
Author: 一眼万年的星空
Title: 使用二进制重排 & Clang插桩技术点来进行iOS冷启动进行优化

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/563787/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • pyinstaller打包python文件(含第三方库)(以pandas和xlwings为例)

    首先安装该模块 pip install Pyinstaller (用清华源快一点) pip install Pyinstaller -i https://pypi.tuna.tsi…

    Python 2023年8月26日
    099
  • 一、目标检测概述

    一、目标检测概述 1.什么是目标检测 目标检测定义识别图片中有哪些物体以及物体的位置(坐标位置) 2.传统目标算法到深度学习 2.1目标检测算法的变迁 对于传统的:将待检测作为输入…

    Python 2023年10月25日
    090
  • 关于plt.scatter()的使用

    1.plt.scatter()的说明 来源: plt.scatter()是来源于matplotlib库中pyplot子库的一个方法 pyplot是matplotlib子库,用于绘制…

    Python 2023年8月1日
    061
  • 记python 链式比较的坑

    前两天,python交流群里有人问: python;gutter:true;”x” 这个表达式输出的是什么,脑子都没动,就觉得应该是True [TencentCloudSDKExc…

    Python 2023年6月3日
    067
  • Pandas求两个dataframe差集 详解

    1、交集 intersected=pd.merge(df1,df2,how=’inner’) 延伸(针对列求交集)intersected=pd.merge(…

    Python 2023年8月2日
    037
  • Unity游戏开发:对话系统的实现

    在解谜类游戏中,与npc的对话是一个基础且常用的功能。通常来说,在与npc的对话中玩家可以获取一些有价值的信息并对之后的游戏有一定的导向作用。此外,在玩家获取对应物品前后,与npc…

    Python 2023年9月29日
    082
  • 我也和 chatGPT 聊了聊

    大家好啊,我是董董灿。 我也和 chatGPT 聊了聊,都是因为最近 chatGPT 太火了! 这是一个大型的 AI 语言模型。你不仅可以和它聊天,问它各种各样的问题,还可以让它写…

    Python 2023年11月3日
    047
  • pytest学习和使用1-pytest安装和版本查看

    1 pytest安装和初步使用 1 学习来源 2 依赖的环境 3 本文学习环境 4 pytest安装 5 查看pytest版本 1 学习来源 https://docs.pytest…

    Python 2023年9月10日
    046
  • 利用Python监控儿子每天在电脑上面做了些什么

    在监测到玩游戏、看视频等钓鱼行为后,还会监测工人的离职倾向。 [En] After the fishing behaviors such as playing games and …

    Python 2023年5月24日
    048
  • python pandas 读取excel 去重某一列_使用Python Pandas读取excel并将列/行隔离到p

    下面是我如何绘制大型数据帧的第31行中的数据,将第0行设置为x轴。(更新答案)import pandas as pd import numpy as np import matpl…

    Python 2023年8月8日
    048
  • 可交互绘图-Plotly

    欢迎关注笔者的微信公众号 Plotly 是一个 Python 库,用于设计图形,尤其是交互式图形。它可以绘制各种图形和图表,如直方图、条形图、箱线图、展开图等等。它主要用于数据分析…

    Python 2023年9月6日
    052
  • 快速绘制流程图「GitHub 热点速览 v.22.47」

    画流程图一直是研发的一个难题,如何画得通俗易懂已经够让人头疼了,还要美观大方。用 d2 的语法描述下流程,d2 会自动帮你生成一张配色极佳的流程图。说到研发的选择,本周特推的 ch…

    Python 2023年10月13日
    044
  • Docker+Ubuntu+Nginx+uwsgi+Django+Python部署

    前言 用Python开发时,经常会用到Django框架,本地跑项目还是比较简单能跑起来,但在云服务器部署时,踩了不少坑,本文将记录部署的全过程,避免再踩同样的坑。文末附上打包好的完…

    Python 2023年8月4日
    039
  • plt python 自己制定cmap_带有自定义cmap颜色的matplotlib散点图不正确

    所以我按照一个模式为我的散点图创建一个自定义的颜色图和cbar。我创建了4个子图,每个子图覆盖了一个参数的不同范围,这个参数用来给点颜色。此参数的值范围从1e-10到1.0。在 我…

    Python 2023年9月4日
    051
  • pandas多重索引补全子索引缺失的方法

    当数据中的dataframe(df)是一个二重索引且某一层索引的第二层索引值并不是全部索引值时,我们应该如何在该层索引插入第二层索引没有的值呢?本文记录自己的学习遇到的情况~ 如以…

    Python 2023年8月18日
    042
  • 关于Retinex理论的一些理解

    目前一直在参与关于Retinex的相关课题,并完成了许多模型的构建,本文以个人的见解介绍Retinex的相关理论1. 基本原理Retinex理论是上世纪八十年代由land等人提出的…

    Python 2023年10月1日
    032
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球