Flutter 混合开发 - 动态下发 libflutter.so libapp.so
背景
最近在做包体积优化,在完成代码混淆、压缩,裁剪ndk支持架构,以及资源压缩(如图片转webp、mp3压缩等)后发现安装包的中占比较大的仍是 so 动态库依赖。
具体查看发现 libflutter.so 和 libapp.so 的体积是最大的,这两个动态库都是 flutter 集成进来的。结合项目中 Flutter 的应用,Flutter 页面都是作为二级页面使用,而且页面使用频率很低,所以是不是可以把这两个 so 从 apk 中剔除,在应用启动后再动态下发呢?
如果可以实现,那么包体积又可以缩减 13.8 M,包体积在原基础上立减一半,收益非常可观!开搞!
实战
libflutter.so & libapp.so 如何引入项目的?
项目是以远程依赖方式引入 flutter,即 flutter 开发完成后打包 aar 发布到公司 maven。通过解压已打包的 aar 发现,aar 中仅有 libapp.so,并没有 libflutter.so。而唯一提到 libflutter.so 的只有打包时生成的 pom 文件。
那么就从宿主项目入手。要远程依赖 flutter,需要指定 repositories{} 。通过配置发现,除了公司 maven 仓库地址,还需要额外配置一个 "https://storage.flutter-io.cn/download.flutter.io",结合打包时生成的 pom 文件,可以猜测 libflutter.so 是在依赖解析过程中引入到项目中的。
allprojects {repositories {google()mavenCentral()//flutter 需要的仓库配置:maven {url '******' //公司 maven 仓库地址}maven {url 'https://storage.flutter-io.cn/download.flutter.io'}}
}
如何剔除与上传 libflutter.so & libapp.so
知道了这两个 so 文件如何引入到项目中的,那么接下来就要考虑怎么剔除与上传。剔除的时机有两个时间节点:打包 aar 时,打包 apk 时。结合已了解的 so 文件引入时机,打包 aar 时只能剔除 libapp.so,显然这个时机不合适,那么下面就来看打包 apk 时怎么实现剔除并上传这两个 so 文件。
既然要在打包 apk 时剔除并上传,毫无疑问需要自定义 Gradle Plugin 和 Gradle Task。如何自定义不细讲,网上相关文章太多,自行查看。
这里考虑只在项目中使用,所以直接在项目中新建 buildSrc Module,在里面实现 Gradle Plugin。
自定义 Gradle Plugin
- 明确只在打 release 包时才需要剔除(因为谁关心 debug 包包体积呀!)
- 确定剔除 Task 执行的时机。剔除要在 merge 所有 so 之后才行,通过查看 task 列表,发现 “mergeReleaseNativeLibs” 就是非常不错的时机。
public class FlutterDynamicPlugin implements Plugin<Project> {@Overridepublic void apply(Project project) {if (project.getPlugins().hasPlugin("com.android.application")) {project.afterEvaluate(project1 -> {AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);appExtension.getApplicationVariants().all(variant -> {String variantName = StringUtil.capitalize(variant.getName());//只在 release 变体下生效if (!variantName.equalsIgnoreCase("release")) return;//自定义 Gradle TaskEngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);//指定自定义 Task 执行时机:mergeReleaseNativeLibs -> flutterSoDynamicReleaseTask mergeSOTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");mergeSOTask.finalizedBy(engineSoDynamicTask);});});}}
}
自定义 Gradle Task
- 找到 libflutter.so
- 上传
- 剔除
- 记录上传信息(用于运行时下载)
public class EngineSoDynamicTask extends DefaultTask {@Inputpublic String mergeNativeLibsOutputPath;@TaskActionpublic void optimizeEngineSo() {//从 app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a 中找到 libflutter.soFile soFile = FileUtil.findSpecificFile(mergeNativeLibsOutputPath, "arm64-v8a", "libflutter.so");if (soFile == null || !soFile.exists()) return;//上传String url = HttpUtil.getInstance().upload(soFile);if (url != null){//记录上传信息write2Assets(url);//剔除soFile.delete();}}private void write2Assets(String url) {String content = "\"flutterSoUrl\":\"" + url + "\"";Write2AssetsUtil.getInstance().writeContent(content);}
}
这里以剔除 libflutter.so 为例,由于项目中只支持 arm64-v8a,所以只剔除了该架构下的。
坑点: 记录上传信息是通过向 assets 中插入 json 文件实现的,而上面只指定了自定义 Task 在 mergeReleaseNativeLibs Task 之后执行,这里就会偶现 assets 插入成功了,但打出的 apk 的 asstes 中并没有 json 文件。
原因: mergeReleaseNativeLibs Task 与 mergeReleaseAssets Task 没有指定的先后顺序,这就导致 assets 插入成功了,但被后续的 mergeReleaseAssets Task 覆盖掉了。
解决办法: 指定自定义 Task 、mergeReleaseNativeLibs Task、mergeReleaseAssets Task 三者先后顺序
EngineSoDynamicTask engineSoDynamicTask = project.getTasks().create("flutterSoDynamic" + variantName, EngineSoDynamicTask.class);
Task mergeNativeLibsTask = project.getTasks().findByName("merge" + variantName + "NativeLibs");
Task mergeAssetsTask = project.getTasks().findByName("merge" + variantName + "Assets"); // mergeReleaseNativeLibs -> flutterSoDynamicRelease -> mergeReleaseAssets
mergeNativeLibsTask.finalizedBy(engineSoDynamicTask);
mergeAssetsTask.dependsOn(engineSoDynamicTask);
运行时动态加载
libflutter.so & libapp.so 使用时机
要实现动态加载,先明确这两个 so 文件在何时用到,找到这个时间点,只要在其之前下载完成就,理论上就实现了运行时动态加载。
项目中使用的是官方多引擎方案(即 EngineGroup),所以先看它的构造函数中有何逻辑。
public class FlutterEngineGroup {public FlutterEngineGroup(@NonNull Context context) {this(context, null);}public FlutterEngineGroup(@NonNull Context context, @Nullable String[] dartVmArgs) {// FlutterInjector.instance() 该方法会创建一个 FlutterInjector 单例,// FlutterInjector 实例创建过程中会创建 FlutterLoader 对象并赋值给 flutterLoader 变量FlutterLoader loader = FlutterInjector.instance().flutterLoader();if (!loader.initialized()) {loader.startInitialization(context.getApplicationContext());loader.ensureInitializationComplete(context.getApplicationContext(), dartVmArgs);}}
}
FlutterEngineGroup 构造函数中直接创建获取 FlutterLoader 对象,然后调用其 startInitialization() 和 ensureInitializationComplete()。限于篇幅,这里直接说结论:
- startInitialization() 最终会执行 FlutterJNI#loadLibrary(),其内部调用 System.loadLibrary(“flutter”),实现加载 libflutter.so。
- ensureInitializationComplete() 内部会准备一个 shellArgs 配置,最终调用 FlutterJNI#init() 执行。shellArgs 中有两条是关于 libapp.so 的。
public void ensureInitializationComplete(){//...List<String> shellArgs = new ArrayList<>();//...shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName);shellArgs.add("--"+ AOT_SHARED_LIBRARY_NAME+ "="+ flutterApplicationInfo.nativeLibraryDir+ File.separator+ flutterApplicationInfo.aotSharedLibraryName);//...
}
通过上面可知,libflutter.so 和 libapp.so 都是在 FlutterEngineGroup 构造时调用的,那么只要在 FlutterEngineGroup 构造之前下载完成即可。
动态加载 libflutter.so
查看 FlutterEngineGroup 构造函数源码可知,libflutter.so 是通过 System.loadLibrary(“flutter”) 来实现加载的。结合 so 加载流程可知,将自定义的 so 文件路径注入到 classLoader#pathList#nativeLibraryDirectories 就可以实现优先加载,就可以实现 so 的动态加载了。这里我们直接复用 Tinker 的 TinkerLoadLibrary#installNativeLibraryPath() 。
动态加载 libapp.so
查看 FlutterEngineGroup 构造函数源码可知,libapp.so 是添加到一个配置中,然后调用 native 方法执行,所以无法想 libflutter.so 来实现。首先能想到的是能不能 hook 方法来自己实现配置,再次查看 FlutterEngineGroup 代码。
首先拿到 FlutterLoader 对象,那么看下 FlutterLoader 是怎么来的。
FlutterLoader loader = FlutterInjector.instance().flutterLoader();
public final class FlutterInjector {public static void setInstance(@NonNull FlutterInjector injector) {instance = injector;}public static FlutterInjector instance() {accessed = true;if (instance == null) {instance = new Builder().build();}return instance;}public static final class Builder {public Builder setFlutterJNIFactory(@NonNull FlutterJNI.Factory factory) {this.flutterJniFactory = factory;return this;}private void fillDefaults() {if (flutterJniFactory == null) {flutterJniFactory = new FlutterJNI.Factory();}if (executorService == null) {executorService = Executors.newCachedThreadPool(new NamedThreadFactory());}if (flutterLoader == null) {flutterLoader = new FlutterLoader(flutterJniFactory.provideFlutterJNI(), executorService);}}public FlutterInjector build() {fillDefaults();return new FlutterInjector(flutterLoader, deferredComponentManager, flutterJniFactory, executorService);}}
}
通过上面的代码可知,FlutterLoader 时在 FlutterInjector 构造时默认创建。同时值得注意的两点:
- FlutterInjector 是单例模式,并提供 setInstance() 自行创建。
- FlutterInjector 通过构造模式构建,并提供自行创建 FlutterJNI.Factory、FlutterLoader 等。
有这两点完全可以 hook FlutterLoader#ensureInitializationComplete()了,但实操下来发现代码量太大,实现难度太高。虽然没法 hook ensureInitializationComplete() 来修改配置,但在实操过程中发现重要信息。
大致意思是,下面的配置是为上面做兜底。如果我们把 libapp.so 剔除,那么这俩配置都无法生效,那我们可以再加一条来兜底啊,即把下载后 libapp.so 的存储路径配置上去。
结合之前的代码逻辑,shellArgs 最终会在 FlutterJNI#init() 中使用,而 FlutterJNI 又可以在 FlutterInjector 自行创建,那么问题不就简单了:
- 新建自定义的 FlutterJNI 继承自 FlutterJNI,内部重写 init(),将下载后下载后 libapp.so 的存储路径添加到 shellArgs 中。
- 在调用 FlutterEngineGroup 构造之前调用 FlutterInjector#setInstance() 将自定义的 FlutterJNI 注入进去。
class CustomFlutterJNI(private val appSOSavePath: String) : FlutterJNI(){override fun init(context: Context,args: Array<out String>,bundlePath: String?,appStoragePath: String,engineCachesPath: String,initTimeMillis: Long) {val hookArgs = args.toMutableList().run {add("--aot-shared-library-name=$appSOSavePath")toTypedArray()}super.init(context, hookArgs, bundlePath, appStoragePath, engineCachesPath, initTimeMillis)}class CustomFactory(private val appSOSavePath: String) : Factory(){override fun provideFlutterJNI(): FlutterJNI {return CustomFlutterJNI(appSOSavePath)}}
}
val appSOSavePath = "******" // libapp.so 下载保存的存储路径
FlutterInjector.setInstance(FlutterInjector.Builder().setFlutterJNIFactory(CustomFlutterJNI.CustomFactory(appSOSavePath)).build())
val engineGroup = FlutterEngineGroup(context)
小结
通过如下几步实现了 libflutter.so 和 libapp.so 的剔除、上传、动态加载:
- 自定义 GradleTask 实现在 merged_native_libs/ 中查找指定 so 文件、上传、记录上传信息(写入 assets 中)、剔除。
- 自定义 GradlePlugin 指定仅在 release 打包中使用,并指定自定义 GradleTask 执行时机。
- 读取 asstes 信息并下载,下载完成后通过注入 so 加载目录和 hook FlutterJNI 实现动态加载 so 文件,最后调用 FlutterEngineGroup 实现 Flutter 初始化。
实现后的效果非常显著:
完整代码(仅供参考)
GitHub - StefanShan/flutterSoDynamic: 从 apk 中剔除 libflutter.so 和 libapp.so,并动态下发加载
优化
上面把所有流程跑通了,但有些地方还需要优化:
- libflutter.so 是根据 flutter 版本生成的,libapp.so 为业务代码生成,所以需要区分上传,即做版本控制,减少重复上传。
- 同样在下载时,也要根据版本判断,避免重复下载。
- 动态加载失败时,需要做兜底处理,例如用 H5 页面来替代。
文章来源(更多文章请点击) 青杉
参考资料
到家Flutter动态化瘦身方案的探索 - 墨天轮
Android 重构之旅:动态下发 SO 库
Android 动态链接库 So 的加载
Android编译期动态添加assets
Hi,我是“青杉”,您可以通过如下方式关注我:
相关文章:
Flutter 混合开发 - 动态下发 libflutter.so libapp.so
背景 最近在做包体积优化,在完成代码混淆、压缩,裁剪ndk支持架构,以及资源压缩(如图片转webp、mp3压缩等)后发现安装包的中占比较大的仍是 so 动态库依赖。 具体查看发现 libflutter.so 和 libapp.so 的体积是最大的&…...
Peter算法小课堂—动态规划
Peter推荐算法书:《算法导论》 图示: 目录 钢条切割 打字怪人 钢条切割 算法导论(第四版)第十四章第一节:钢条切割 题目描述: 给定一根长度为 n 英寸的钢条和一个价格表 ,其中 i1,2,…,n …...
2022–2023学年2021级计算机科学与技术专业数据库原理 (A)卷
一、单项选择题(每小题1.5分,共30分) 1、构成E—R模型的三个基本要素是( B )。 A.实体、属性值、关系 B.实体、属性、联系 C.实体、实体集、联系 D.实体、实体…...
Clojure 实战(4):编写 Hadoop MapReduce 脚本
Hadoop简介 众所周知,我们已经进入了大数据时代,每天都有PB级的数据需要处理、分析,从中提取出有用的信息。Hadoop就是这一时代背景下的产物。它是Apache基金会下的开源项目,受Google两篇论文的启发,采用分布式的文件…...
Django 分页(表单)
目录 一、手动分页二、分页器分页 一、手动分页 1、概念 页码:很容易理解,就是一本书的页码每页数量:就是一本书中某一页中的内容(数据量,比如第二页有15行内容),这 15 就是该页的数据量 每一…...
socket实现视频通话-WebRTC
最近喜欢研究视频流,所以思考了双向通信socket,接下来我们就一起来看看本地如何实现双向视频通讯的功能吧~ 客户端获取视频流 首先思考如何获取视频流呢? 其实跟录音的功能差不多,都是查询电脑上是否有媒体设备,如果…...
simulink代码生成(九)—— 串口显示数据(纸飞机联合调试)
纸飞机里面的协议是固定的,必须按照协议配置; (1)使用EasyHEX协议,测试int16数据类型 测试串口发出的数据是否符合? 串口接收数据为: 打开纸飞机绘图侧: (1)…...
Mysql数据库(中)——增删改查的学习(全面,详细)
上一篇主要对查询操作进行了详细的总结,本篇主要对增删改操作以及一些常用的函数进行总结,包括流程控制等;以下的代码可以直接复制到数据库可视化软件中,便于理解和练习; 常用的操作: #函数: S…...
test dbtest-03-对比 Liquibase、flyway、dbDeploy、dbsetup
详细对比 Liquibase、flyway、dbDeploy、dbsetup,给出对比表格 下面是一个简要的对比表格,涵盖了 Liquibase、Flyway、dbDeploy 和 DbSetup 这四个数据库变更管理工具的一些主要特点。 特点/工具LiquibaseFlywaydbDeployDbSetup开发语言Java࿰…...
力导向图与矩阵排序
Graph-layout force directed(力导向图布局)是一种用于可视化网络图的布局算法。它基于物理模型,模拟了图中节点之间的相互排斥和连接弹性,以生成具有良好可读性和美观性的图形布局。 在力导向图布局中,每个节点被视为…...
word 常用功能记录
word手册 多行文字对齐标题调整文字间距打钩方框插入三线表插入参考文献自动生成目录 多行文字对齐 标题调整文字间距 打钩方框 插入三线表 插入一个最基本的表格把整个表格设置为无框线设置上框线【实线1.5磅】设置下框线【实线1.5磅】选中第一行,设置下框线【实线…...
C#线程基础(线程启动和停止)
目录 一、关于线程 二、示例 三、生成效果 一、关于线程 在使用多线程前要先引用命名空间System.Threading,引用命名空间后就可以在需要的地方方便地创建并使用线程。 创建线程对象的构造方法中使用了ThreadStart()委托,当线程开始执行时,…...
如何利用ChatGPT来提高编程效率
如何利用ChatGPT来提高编程效率 在当今这个信息爆炸和技术快速发展的时代,程序员们面临着巨大的压力,既要保证代码的质量,又要提高工作效率。幸运的是,人工智能(AI)正在改变我们编写和维护代码的方式,而OpenAI的ChatGPT是其中的佼佼者。本文将讨论如何利用ChatGPT以及结合…...
java智慧工地源码,互联网+建筑工地,实现对工程项目内人员、车辆、安全、设备、材料等的智能化管理
智慧工地全套源码,微服务JavaSpring Cloud UniApp MySql;支持多端展示(大屏端、PC端、手机端、平板端)演示自主版权。 智慧工地概念: 智慧工地就是互联网建筑工地,是将互联网的理念和技术引入建筑工地&…...
创建并使用自己的C++模块(Windows10+MSVC)
module是C20种新引入的特性,关于module的介绍和好处,网上已有大量的文章,此处也不再赘述,本文仅记录在个人的环境上创建一个简单的module并使用这个module。 环境同上一篇文章( windows10,MSVC C工具链&am…...
Spring Boot 2.7.11 集成 GraphQL
GraphQL介绍 GraphQL(Graph Query Language)是一种用于API的查询语言和运行时环境,由Facebook于2012年创建并在2015年公开发布。与传统的RESTful API相比,GraphQL提供了更灵活、高效和强大的数据查询和操作方式。 以下是GraphQL…...
软件工程期末总结
软件工程期末总结 软件危机出现的原因软件生命周期软件生命周期的概念生命周期的各个阶段 软件开发模型极限编程 可行性研究与项目开发计划需求分析结构化分析的方法结构化分析的图形工具软件设计的原则用户界面设计结构化软件设计面向对象面向对象建模 软件危机出现的原因 忽视…...
MidTool图文创作-GPT-4与DALL·E 3的结合
GPT-4与DALLE 3的结合 GPT-4是由OpenAI开发的最新一代语言预测模型,它在前代模型的基础上进行了大幅度的改进,不仅在文本生成的连贯性、准确性上有了显著提升,还在理解复杂语境和执行多步骤指令方面表现出了更高的能力。而DALLE 3则是一个创…...
Python将两个或多个列表合并为一个列表,并根据每个输入列表中的元素的位置将其组合在一起
将两个或多个列表合并为一个列表,并根据每个输入列表中的元素的位置将其组合在一起。 这个需求在实际开发过程中应该说非常常见,当然python也给我们内置了相关方法! zip(*iterables, strictFalse) 在多个迭代器上并行迭代,从每…...
数模混合SoC芯片中LEF2Milkyway的golden flow
在数模混合芯片中的项目中,特别是数字模块很少甚至只有一个简单的数字控制逻辑时,我们要做数字模块的后端实现时,通常模拟那边会问我们实现需要他们提供哪些数据。 通常来说,我们可以让模拟设计提供数字模块的GDS或LEF文件即可。…...
Five tips to make your essay flow
This post was written by Sydney Nicholson, a second-year master’s student in the English Department. Dear writer, Have you ever wondered what it takes to make an essay “flow”? In my time as a writing center tutor, I’ve noticed that this is one of th…...
linux驱动(二):led补
本文主要探讨s5pv210的led驱动相关知识,包括驱动主次设备注册和取消,udev(mdev)机制,静态和动态映射操作寄存器。 字符设备驱动注册 老接口(register_chrdev) static inline int register_chrdev(unsigned int major, const char *n…...
性能测试-jmeter:安装 / 基础使用
一、理解jmeter 官网-Apache JMeter-Apache JMeter™ JMeter是一款开源的性能测试工具,主要用于模拟大量用户并发访问目标服务器,以评估服务器的性能和稳定性。 JMeter可以执行以下任务序号用途描述1性能测试通过模拟多个用户在同一时间对服务器进行请…...
数据仓库-数仓优化小厂实践
一、背景 由于公司规模较小,大数据相关没有实现平台化,相关的架构都是原生的Apache组件,所以集群的维护和优化都需要人工的参与。根据自己的实践整理一些数仓相关的优化。 二、优化 1、简易架构图 2、ODS层优化 2.1 分段式解析 随着业务增长…...
uniapp中uview组件丰富的Code 验证码输入框的使用方法
目录 基本使用 #自定义提示语 #保持倒计时 API #Props #Methods #Event 基本使用 通过ref获取组件对象,再执行后面的操作,见下方示例。 通过seconds设置需要倒计的秒数(默认60)通过ref调用组件内部的start方法,开始倒计时通过监听cha…...
md文件图片上传方案:Github+PicGo 搭建图床
文章目录 1. PicGo 下载2. 配置Github3. 配置PicGo4. PicGo集成Typora4.1 picGo监听端口设置 5. 测试 1. PicGo 下载 下载地址:https://molunerfinn.com/PicGo/ 尽量下载稳定版本 2. 配置Github 1. 创建一个新仓库,用于存放图片 2. 生成一个token&a…...
从零开始 - 在Python中构建和训练生成对抗网络(GAN)模型
生成对抗网络(GANs)是一种强大的生成模型,可以合成新的逼真图像。通过完整的实现过程,读者将对GANs在幕后的工作原理有深刻的理解。本教程首先导入必要的库并加载将用于训练GAN的Fashion-MNIST数据集。然后,提供了构建…...
OfficeWeb365 Indexs 任意文件读取漏洞复现
0x01 产品简介 OfficeWeb365 是专注于 Office 文档在线预览及PDF文档在线预览云服务,包括 Microsoft Word 文档在线预览、Excel 表格在线预览、Powerpoint 演示文档在线预览,WPS 文字处理、WPS 表格、WPS 演示及 Adobe PDF 文档在线预览。 0x02 漏洞概述 OfficeWeb365 /Pi…...
Crypto的简单应用-前后端加密传输
最近遇到一个数据脱敏处理的需求,想要用一种轻量级的技术实现,必须足够简单并且适用于所有场合如前后端加密传输、路由加密、数据脱敏等。抽时间研究了一下Crypto加密库的一些API,发现完全符合上述需求,扩展也比较容易。 1、前端加…...
Vue3-32-路由-重定向路由
什么是重定向 路由的重定向 :将匹配到的路由 【替换】 为另一个路由。 redirect : 重定向的关键字。 重定向的特点 1、重定向是路由的直接替换,路由的地址是直接改变的; 2、在没有子路由配置的情况下,重定向的路由可以省略 component 属性的配…...
备案没有商城可以做商城网站吗/今日中央新闻
boost生成和解析json[转] 原文:《boost生成和解析json》 目录 boost生成和解析json[转] 什么是property_tree? 生成 解析 遍历读取属性 删除节点 修改值 抛出异常 boost json使用过程中需要注意的问题 参考: 《boost::property_tree讲解》《…...
网络培训法/seo外链推广工具下载
原标题:一加6T被曝将预装Android P 这一点其它品牌比不了近年来国产手机群雄崛起,不仅在国内获得了很高的呼声,一些品牌在国外也拥有着很高的热度,从以往的三星、苹果手中抢占了很多的份额,如今三星在国内的销量非常惨…...
刘素云网站脱孝怎样做/企业推广网
点击菜单栏“粉丝福利”拿小度,兑手机!在Java中,想必所有人都不会对main方法感到陌生,main方法是Java应用程序的入口方法。程序运行时,要执行的第一个方法就是main方法。在使用Java写下第一个hello world的时候&#x…...
wordpress 命令执行时间/营销策划公司介绍
文/黑水泛金波 问题一、套头是不是都很差? 答:不是。肯定不是。 事实上,厂家在决定用什么样的头来作为“套头”、跟机身组成“套机”一起销售的时候,的确就已经先考虑好了、用户最可能需要的镜头会是哪一个。这往往跟机身的“气质…...
网站建设网站徒手整形培训/seo全称
前言 默认的jdk不支持DESeee的算法,本地化的JDK中配置有拦截规则,可以通过使用bouncycastle的jar包中的DESEngine类来进行DESeee算法的运算。 DES的8字节加解密 DESEngine可以进行8字节数据的加解密,具体的调用方法如下: byte[] k…...
asp.net网站开发视频/网站制作网站推广
吴裕雄--天生自然Django框架开发笔记--Django 模板使用 django.http.HttpResponse() 来输出 "Hello World!"。该方式将数据与视图混合在一起,不符合 Django 的 MVC 思想。将介绍 Django 模板的应用,模板是一个文本,用于…...