阿里ARouter 路由框架解析
一、简介
众所周知,在日常开发中,随着项目业务越来越复杂,项目中的代码量也越来越多,如果维护、扩展、解耦等成了一个非常头疼问题,随之孕育而生的诸如插件化、组件化、模块化等热门技术。 而其中组件化中一项的难点,就是实现各个组件之间的通讯,我们通常解决方案采用路由中间件,来实现页面之间跳转关系。 ARouter 是阿里开源路由框架,常被用于组件之间、模块之间的跳转,由于是国人团队开发的,所以你懂的,中文文档非常详细。
二、入门
2.1.添加依赖
android {defaultConfig {...javaCompileOptions {annotationProcessorOptions {arguments = [AROUTER_MODULE_NAME: project.getName()]}}}
}dependencies {api'com.alibaba:arouter-api:1.5.0'annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
2.2.初始化
在 Application 中初始化
if (BuildConfig.DEBUG) {ARouter.openLog() // 打印日志ARouter.openDebug() // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this);
2.3.添加注解
@Route(path = "/main/main_main")
public class MainActivity extend BaseActivity {...
}
2.4.发起路由
// 1.应用内简单的跳转(通过URL跳转在'进阶用法'中)
ARouter.getInstance().build("/main/main_main").navigation();// 2.跳转并携带参数
ARouter.getInstance().build("/main/main_main").withLong("id", 10086L).withString("name", "Test").navigation();
三、原理解析
在原理解析之前,我们先了解 ARouter 使用关键技术-APT(Annotation Processing Tool),它是 javac 的一个工具,中文译名为编译时注解处理器,说白了,APT 用来编译时,扫描和处理注解,获取注解和被注解类等相关信息,拿到这些信息之后,自动生成一些代码,核心是 AbstractProcessor 这个类,APT 运用非常广泛,诸如 ButterKnife、EventBus、Dagger2 等使用运用到了 APT,ARouter 也不例外。运用 APT 技术我们可以自己写一些注解处理器,例如处理网络、打印出错信息等。
3.1 分析
我们来看看 ARouter 初始化
//1.初始化
ARouter.init(application)//2.ARouter#init
public static void init(Application application) {if (!hasInit) {logger = _ARouter.logger;_ARouter.logger.info(Consts.TAG, "ARouter init start.");//委托给_ARouter去初始化hasInit = _ARouter.init(application);if (hasInit) {_ARouter.afterInit();}//初始化之后调用afterInit_ARouter.logger.info(Consts.TAG, "ARouter init over.");}}
从上面我们可以看到 ARouter 采用门面模式,实际上委托给 *ARouter 处理,*ARouter 是整个框架的路由中心控制器,负责处理控制整个路由的流程。接下来,我们看看 _ARouter 初始化化。
protected static synchronized boolean init(Application application) {mContext = application;//调用 LogisticsCenter 的初始化LogisticsCenter.init(mContext, executor);logger.info(Consts.TAG, "ARouter init success!");hasInit = true;mHandler = new Handler(Looper.getMainLooper());return true;
}//LogisticsCenter#init
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {mContext = context;executor = tpe;try {long startInit = System.currentTimeMillis();loadRouterMap();if (registerByPlugin) {logger.info(TAG, "Load router map by arouter-auto-register plugin.");} else {Set<String> routerMap;//1.如果是debug模式,或者有更新if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {logger.info(TAG, "Run with debug mode or new install, rebuild router map.");//1.扫描包,获取ROUTE_ROOT_PAKCAGE(com.alibaba.android.arouter.routes)包下的类routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);if (!routerMap.isEmpty()) {context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();}PackageUtils.updateVersion(context); // Save new version name when router map update finishes.} else {logger.info(TAG, "Load router map from cache.");//2.获取本地缓存routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));}logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");startInit = System.currentTimeMillis();for (String className : routerMap) {//3.将 IRouteRoot,IRouteGroup和IProviderGroup的实现类,//通过注解生成,并且加载 Warehouse(数据仓库)// Warehouse:数据仓库,存储路由配置信息和具体生成的IProvider对象if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {//1.加载 IRouteRoot,每个moudle下都会生成一个该类型的实现类,//通过module名来区分的,作用是将每个module下所有的分组按照组名//和对应分组的实现类(IRouteGroup接口的实现类)的 Class 对象做一个映射,//然后保存在一个全局的 groupIndex 的 map 表中。((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {//2.加载 IInterceptorGroup 作用是将各个module下的 自定义的i nterceptor 按照优先级和 interceptor的 // Class 对象做一个映射,然后保存在一个全局的 interceptorIndex 的 map 表中。((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {//3.加载 IProviderGroup 该类的作用的是将项目中自定义的提供序//列化功能的类的相关信息以RouteMeta 类的对象保存在全局的providerIndex的map表中((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);}}}logger.info(TAG, "Load root element finished, cost " + (System.currentTimeMillis() - startInit) + " ms.");if (Warehouse.groupsIndex.size() == 0) {logger.error(TAG, "No mapping files were found, check your configuration please!");}if (ARouter.debuggable()) {logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));}} catch (Exception e) {throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");}}
从上面代码我们可以看到调用 LogisticsCenter,后勤中心主要完成两件事
-
获取到 com.alibaba.android.arouter.routes 包下的所有 class 文件类名
-
根据找到的类名去加载相关的实例到Warehouse中,com.alibaba.android.arouter.routes 包下面的 class 是注解解析器自动生成,主要 IRouteRoot,IRouteGroup 和 IProviderGroup 的实现类,当我们使用 @Route 注解某个类时,会将这个类的信息注入的到自动生成的上述实现类中。 接下来调用 afterInit()。
//_ARouter#afterInit
static void afterInit() {// 调用 Postcard#build 方法,获取 InterceptorService 拦截服务控制器interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();}//build 方法实际上,将 path 和 group 信息封装到 Postcard,生成一个要跳转的信息表protected Postcard build(String path) {if (TextUtils.isEmpty(path)) {throw new HandlerException(Consts.TAG + "Parameter is invalid!");} else {//生成具体实例PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);if (null != pService) {path = pService.forString(path);}return build(path, extractGroup(path));}}//Postcard#navigation 这个方法实现了跳转,我们点进去会发现,最后调用 _ARouter#navigation
//_ARouter#navigation
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {// Pretreatment failed, navigation canceled.return null;}try {//1.调用 LogisticsCenter#completion 的方法LogisticsCenter.completion(postcard);......//不是绿色通道,通过拦截控制器依次调用不同拦截器处理信息 if (!postcard.isGreenChannel()) { //每个拦截器的拦截方法调用都是在子线程中执行的interceptorService.doInterceptions(postcard, new InterceptorCallback() {@Overridepublic void onContinue(Postcard postcard) {_navigation(context, postcard, requestCode, callback);}@Overridepublic void onInterrupt(Throwable exception) {if (null != callback) {//只要有一个拦截器拦截该包裹,则回调onInterrupt方法宣告本次路由被终止callback.onInterrupt(postcard);}}});} else {//2.如果是绿色通道,调用_navigation方法进行具体的导航return _navigation(context, postcard, requestCode, callback);}return null;} }//LogisticsCenter#completion
public synchronized static void completion(Postcard postcard) {......//1.Warehouse.routes获取RouteMeta,RouteMeta 路由信息描述类,存储目标地址的类型,路径,参数等信RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());if (null == routeMeta) { //如果没有路由信息,则尝试去数据仓库查找Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); if (null == groupMeta) {throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");} else {// Load route and cache it into memory, then delete from metas.try {if (ARouter.debuggable()) {logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));}IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();iGroupInstance.loadInto(Warehouse.routes);Warehouse.groupsIndex.remove(postcard.getGroup());if (ARouter.debuggable()) {logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));}} catch (Exception e) {throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");}completion(postcard); // Reload} else {//找到路由信息后,则将配置的路由信息填充到Postcard对象中postcard.setDestination(routeMeta.getDestination());//要跳转 Activity.class 路径postcard.setType(routeMeta.getType());postcard.setPriority(routeMeta.getPriority());postcard.setExtra(routeMeta.getExtra());Uri rawUri = postcard.getUri();if (null != rawUri) { //这里主要是完成参数的填充}//针对不同的路由类型进行处理switch (routeMeta.getType()) {case PROVIDER: //如果是服务提供者,则尝试获取其具体实例,如果没有,则根据路由信息构造一个实例,初始化并存储到数据仓库,Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();IProvider instance = Warehouse.providers.get(providerMeta);if (null == instance) { // There's no instance of this providerIProvider provider;provider = providerMeta.getConstructor().newInstance();provider.init(mContext);Warehouse.providers.put(providerMeta, provider);instance = provider;}postcard.setProvider(instance);//服务提供者被设置成绿色渠道,不用接受拦截检查postcard.greenChannel(); break;case FRAGMENT://fragment也不用拦截检查postcard.greenChannel(); default:break;}}//_ARouter#_navigation
//根据不同类型,路由处理和导航也不一样
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {final Context currentContext = null == context ? mContext : context;switch (postcard.getType()) {//是 ACTIVITY 数据填充到intent,并且调用 startActivitycase ACTIVITY:final Intent intent = new Intent(currentContext, postcard.getDestination());intent.putExtras(postcard.getExtras());// Set flags.int flags = postcard.getFlags();if (-1 != flags) {intent.setFlags(flags);} else if (!(currentContext instanceof Activity)) { // Non activity, need less one flag.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}// Set ActionsString action = postcard.getAction();if (!TextUtils.isEmpty(action)) {intent.setAction(action);}// Navigation in main looper.runInMainThread(new Runnable() {@Overridepublic void run() {startActivity(requestCode, currentContext, intent, postcard, callback);}});break;//PROVIDER类型,则直接返回其服务提供者case PROVIDER:return postcard.getProvider();//如果是BOARDCAST 、 CONTENT_PROVIDER 、 FRAGMENT,则创建其需要的实体,并填充数据,返回对象 case BOARDCAST:case CONTENT_PROVIDER:case FRAGMENT:Class fragmentMeta = postcard.getDestination();try {Object instance = fragmentMeta.getConstructor().newInstance();if (instance instanceof Fragment) {((Fragment) instance).setArguments(postcard.getExtras());} else if (instance instanceof android.support.v4.app.Fragment) {((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());}return instance;} catch (Exception ex) {logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));}case METHOD:case SERVICE:default:return null;}return null;}
上面的初始化代码其实就可以了解其原理,下面我们可以画出时序图,了解大致流程。
四、总结
ARouter 通过 apt 技术,生成保存路径(路由path)和被注解(@Router)的组件类的映射关系的类,利用这些保存了映射关系的类,根据用户的请求 postcard(明信片)寻找到要跳转的目标地址(class),使用 Intent 跳转。
ARouter的重要性主要体现在以下几个方面:
- 模块化开发:随着软件规模和复杂度的增加,越来越多的应用程序采用模块化开发方式。ARouter支持跨模块的调用,简化了模块间跳转的复杂度,降低了耦合性,提升了整个应用的可维护性和可扩展性。
- 代码解耦:在传统的Activity跳转方式中,一般是通过Intent来传递参数,不同页面之间的参数传递比较麻烦,还有可能导致代码冗余。ARouter采用注解方式传递参数,简化了代码的编写和阅读。
- 动态路由:路由是基于注解方式实现的,可以在代码中动态注册和删除路由。ARouter还支持路由重定向、降级等高级功能,可以根据不同的业务场景灵活处理路由逻辑。
- 统一管理:ARouter提供了统一管理路由配置的界面,可以直览整个项目的路由规则,方便开发人员进行维护和管理。
ARouter是一款非常实用的路由框架,可以帮助开发者在模块化开发、代码解耦、动态路由和统一管理等方面提高开发效率和应用质量。
下面整理了《Android 架构学习手册》+《深入理解Gradle框架》学习笔记,根据自己学习中所做的一些笔录来整的,主要也是方便后续好复习翻阅,省掉在去网上查找的时间,以免在度踩坑,如果大家有需要的可以直接 通过点击此处↓↓↓ 进行参考学习:https://qr21.cn/CaZQLo?BIZ=ECOMMERCE
Android 架构学习手册
深入理解Gradle框架
相关文章:

阿里ARouter 路由框架解析
一、简介 众所周知,在日常开发中,随着项目业务越来越复杂,项目中的代码量也越来越多,如果维护、扩展、解耦等成了一个非常头疼问题,随之孕育而生的诸如插件化、组件化、模块化等热门技术。 而其中组件化中一项的难点&…...

大型医院健康体检管理系统源码(PEIS)
一、体检管理系统(PEIS)概念 体检管理系统(PEIS)是以健康为中心的身体检查。一般医学家认为健康体检是指在身体尚未出现明显疾病时,对身体进行的全面检查。方便了解身体情况,筛查身体疾病。即应用体检手段对…...

java 获取时间的方法
Java的时间是通过字节码指令来控制的,所以 java程序的运行时间是通过字节码指令来控制的。但是由于 Java程序在运行时, JVM会产生一些状态,所以在执行 JVM指令时, JVM也会产生一些状态。 我们在执行 java程序时,主要是…...

Block原理(二)- 用白话说说底层源码,不扯代码
之前有一篇关于block的源码探究分析 Block原理(一),时至今日,总觉的那篇文章说得不够流畅,今天打算从顶层设计的角度试着拆解下block的设计思想,拗脑的源码部分就不必再次触碰了,尽量保障这篇文…...

springboot整合knife4j接口文档成公共模块使用
theme: smartblue 之前项目中一直用的是swagger-ui进行接口文档的调用和使用,最近前端一直反馈页面不美观,调用凌乱,等一系列问题。基于这个问题我决定将其进行更改调整,上网搜索了一下发现knife4j是目前接口文档不错的一款插件。…...

软件测试需要学什么
软件测试近些年也是比较热门的行业,薪资高、入门门槛低,让很多开发人员想纷纷加入软件开发这个行业,想要成为这一岗位的一员,想要进入软件测试行业,他们需要学习什么呢? 软件测试需要学习的还挺多的&#…...

【蓝桥杯省赛真题17】python删除字符串 青少年组蓝桥杯python编程省赛真题解析
目录 python删除字符串 一、题目要求 1、编程实现 2、输入输出 二、解题思路...

C# LINQ 查询语句和方法的区别及使用
C# LINQ(Language-Integrated Query)是一种强类型、编译时的查询技术,它可以通过统一的语法对多种数据源进行查询和操作,包括对象、集合、数据库等。LINQ 提供了两种查询方式:查询语句和扩展方法。 查询语句ÿ…...

【nacos配置中心】源码部分解析
启动初始化 SpringApplication.prepareContext applyInitializers 回调ApplicationContextInitializer的initialize方法 getInitializers()从applicationContext获取List<ApplicationContextInitializer<?>> initializers 这个集合是通过SpringApplication的…...

Kotlin 1.6.0 的新特性
1、稳定版对于枚举、密封类与布尔值主语穷尽 when 语句 一个详尽的when语句包含了所有主题可能的类型或值的分支,或者对于一些类型包含一个else分支。它覆盖了所有可能的情况,使代码更加安全。 即将禁止非详尽的when语句,以使行为与when表达…...

nextjs13临时笔记
动态路由 文件夹以中括号命名[id] -pages: --list: ---[id]: ----index.jsx(访问路径/list/1 即这种形式/list/:id) ---index.jsx(访问路径/list)[...params]gpt接口分析 初始化项目 npm install next react react-dom # or yarn add next react react-dom # or pnpm add n…...

云计算与区块链之间有什么区别?
区块链是一种去中心化的分布式数字账本,可实现安全透明的交易和数据存储。 它使用节点网络来验证和验证交易。 云计算通过互联网提供计算资源,例如服务器、存储和软件。区块链是一种分散且不可变的虚拟数据分类账,用于维护交易信息和监控网络…...

sed命令常用例子
替换文件中的文本 将文件file.txt中的所有"old_text"替换成"new_text": sed -i ‘s/old_text/new_text/g’ file.txt 删除文件中的某行 删除文件file.txt中的第5行: sed -i ‘5d’ file.txt 在文件中添加一行 在文件file.txt…...

MB510 3BSE002540R1在机器视觉工业领域最基本的应用
MB510 3BSE002540R1在机器视觉工业领域最基本的应用 大家都说人类感知外界信息的80%是通过眼睛获得的,图像包含的信息量是最巨大的。那么机器视觉技术的出现,就是为机器设备安上了感知外界的眼睛,使机器具有像人一样的视觉功能,…...

nightingale-0-介绍单机二进制部署
(一) 夜莺介绍 Nightingale | 夜莺监控,一款先进的开源云原生监控分析系统,采用 All-In-One 的设计,集数据采集、可视化、监控告警、数据分析于一体,与云原生生态紧密集成,提供开箱即用的企业级监控分析和告警能力。于…...

一个从培训学校走出来的测试工程师自述....
简单介绍一下我自己,1997年的,毕业一年了,本科生,专业是机械制造及其自动化。 在校度过了四年,毕业,找工作,填三方协议,体检,入职。我觉得我可能就这么度过我平平无奇的…...

关于pyqt的一些用法
QT原生是C,pyqt基于python语言。 关于插件: 安装一个PyUIC,一个Qt Designer 点击Qt Designer可以出来ui配置页面,和qt原生IDE基本一样 上面操作会生成.ui文件,选中此文件,点击PyUIC,会生成对…...

【Paper Note】ViViT: A Video Vision Transformer
ViViT: A Video Vision Transformer AbstractOverview of vision transformer 回顾ViTEmbedding video clips 视频编码方式Uniform frame sampling 均匀采样Tubelet embedding 时空管采样初始化3D卷积代码介绍视频编码输入到模型当中 Transformer Models for VideoSpatio-tempo…...

Java入坑之IO操作
目录 一、IO流的概念 二、字节流 2.1InputStream的方法 2.2Outputstream的方法 2.3资源对象的关闭: 2.4transferTo()方法 2.5readAllBytes() 方法 2.6BufferedReader 和 InputStreamReader 2.7BufferedWriter 和 OutputStreamWriter 三、路径:…...

校园小助手【GUI/Swing+MySQL】(Java课设)
系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设!!! 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址: 更多系统资源库地址:骚戴的博客_CSDN_更多系统资源 更多系统…...

String的不可变特性
1 问题 如何理解“String是不可变的,但是可以变”? 2 方法 (1)String的不可变特性体现在内容和长度 首先在idea中点开查看String这个类是如何定义的 可以看到这样一行代码:private final char value[]; 正是因为这个数…...

使用MMDeploy(预编译包)转换MMxx(MMDeploy支持库均可)pth权重到onnx,并使用python SDK进行部署验证
MMDeploy使用python部署实践记录 准备工作空间所需环境配置使用MMDeploy得到onnx使用MMDeploy加载onnx模型对单张图片进行推理使用python SDK对onnx模型进行验证 注意 mmdeploy C SDK的使用部署和实际操作过程请看本人另外一篇博文使用MMDeploy(预编译包)…...

Shiro安全框架简介
一、权限管理 1.1 什么是权限管理 基本上只要涉及到用户参数的系统都要进行权限管理,使用权限管理实现了对用户访问系统的控制,不同的用户访问不同的资源。按照安全规则或者安全策略控制用户访问资源,而且只能访问被授权的资源 权限管理包括认…...

三行Python代码,让数据处理速度提高2到6倍
本文可以教你仅使用 3 行代码,大大加快数据预处理的速度。 Python 是机器学习领域内的首选编程语言,它易于使用,也有很多出色的库来帮助你更快处理数据。但当我们面临大量数据时,一些问题就会显现…… 在默认情况下,…...

空间向量模长
// 空间向量模长 #include <stdio.h> #include <stdlib.h> #include <math.h> int main(int argc, char **argv) { float x, y, z; float mochang 0.0; x y z 0.0; if (argc ! 4) { printf("usage:%s x y z\n", argv[1]); …...

活动需求中灵活使用Redis提升生产力
抽奖 一堆用户参与进来,然后随机抽取几个幸运用户给予实物/虚拟的奖品;此时,开发人员就需要写上一个抽奖的算法,来实现幸运用户的抽取;其实我们完全可以利用Redis的集合(Set),就能轻…...

Java知识点学习(第16天)
Innodb是如何实现事务的? innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以一个update语句为例: innodb在收到一个update语句后,会先根据条件找到数据所在的页,并…...

ORA-1688: unable to extend table AUDSYS.AUD$UNIFIED
昨晚正在外滩玩,有个客户发过来一段报错,已经影响到业务了。一看就是12C以后版本才有的问题,,赶紧在手机中收到临时解决办法 报错如下 ORA-1688: unable to extend table AUDSYS.AUD$UNIFIED partition SYS_P42549 by 1024 in t…...

抖音滑块以及轨迹分析
声明 本文以教学为基准、本文提供的可操作性不得用于任何商业用途和违法违规场景。 本人对任何原因在使用本人中提供的代码和策略时可能对用户自己或他人造成的任何形式的损失和伤害不承担责任。 如有侵权,请联系我进行删除。 我们在web端打开用户主页的时候,时不时的会出现滑…...

C#生成单色bmp图片,转为单色bmp图片 任意语言完全用字节拼一张单色图,LCD取模 其它格式图片转为单色图
最终效果: V1.8.2 20230419 文字生成单色BMP图片4.exe 默认1280*720 如果显示不全,请把宽和高加大 字体加大。 首先,用windows画板生成一张1*1白色单色图作为标准,数据如下: 数据解析参考:BMP图像文件完…...