Android 性能优化:内存优化(实践篇)
1. 前言
前一篇文章Android性能优化:内存优化 (思路篇) 大概梳理了Android 内存原理和优化的必要性及应该如何优化,输出了一套短期和长期内存优化治理的SOP方案。
那么这一篇文章就总结下我最近在做内存优化如何实践的,本篇文章有参考了很多其他大佬的文章,站在巨人肩膀上确实更加省力,感谢~ ,这里会对大部分内存优化相关的操作都罗列进来,但是部分内容笔者研究有限,仅用于笔记记录和总结,有不对的地方可以指出,望海涵…
2. 获取内存信息
这里还是要先插入下Android App的内存构成
您在内存分析器顶部看到的数字,基于您的应用提交的所有专用内存页面(此数据由 Android 系统根据其记录提供)。此计数不包含与系统或其他应用共享的页面。
内存计数中的类别如下:
- Java:从 Java 或 Kotlin 代码分配的对象的内存。
- Native:从 C 或 C++ 代码分配的对象的内存。
即使您的应用中不使用 C++,您也可能会看到此处使用了一些原生内存,因为即使您编写的代码采用 Java 或 Kotlin 语言,Android 框架仍使用原生内存代表您处理各种任务,如处理图像资源和其他图形。
- Graphics:图形缓冲区队列为向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
- Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
- Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
- Others:您的应用使用的系统不确定如何分类的内存。
- Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。
3. 内存检测工具
常用的就是以下三种内存检测方式,这里简单罗列下优缺点
3.1 LeakCanary
线下使用,目前大厂内存检测的思路开辟者,集成简单 ,自动化内存泄漏检测神器。主要用于线下集成,虽然使用了idleHandler与多进程,但是 dumphprof 的 SuspendAll Thread 的特性依然会导致应用卡顿,不过只能够自动检测 Activity、Fragment 和其他常见组件的内存泄漏,但某些复杂情况下的泄漏可能无法被自动检测到,需要开发者手动检查和分析。
3.2 Memory Profiler
Android studio中内置的一个强大的工具,其中主要包括NetWork ,cpu 和 Memory Profiler部分,如果是Mac M2以上的开发,建议深度使用,使用起来还是比较流畅的。
Memory Profiler 显示堆内存的实时使用情况,包括 Java 堆、Native 堆和其他内存占用。你可以通过图表查看应用内存使用的波动情况,点击Capture heap dump可以输出当前堆的内存快照,也可以进行Java/Kotlin 或者 Native allocations 。
看下图,Memory Profiler 会展示出类的列表。对于每个类,Allocations 这一列显示的是它的实例数量。后边依次是 Native Size、Shallow Size 和 Retained Size:
Shallow Size 是 对象本身消耗的内存大小,即为红色节点自身所占内存:
Native Size 它是类对象所引用的 Native 对象 (蓝色节点) 所消耗的内存大小:
Retained Size 它是下图中所有橙色节点的大小,由于一旦删除红色节点,其余的橙色节点都将无法被访问,所以橙色节点是被红色节点所持有的,因此被命名为 Retained Size
3.3 Memory Analyzer
(MAT)是一个强大的分析工具,用于查找 Java 应用程序中的内存泄漏并分析内存使用情况。可以使用命令行或者 Android studio 中生成堆转储 (Heap Dump),可以在Leak Suspects Report中查看内存泄露点,Dominator Tree中查看哪些对象占用了大内存,并根据调用链向上排查。
如果使用Android studio profiler Memory视图中 export 生成的 .hprof文件,需要通过hprof工具进行类型转换,避免MAT 打开文件提示文件类型错误。
因为Android Studio保存的是Android Dalvik/ART格式的.hprof文件,所以需要转换成J2SE HPROF格式才能被MAT识别和分析。Android SDK自带了一个转换工具在SDK的platform-tools下,其中转换语句为
hprof-conv <input>memory-20241231T151336.hprof <output>memory1.hprof
在MAT窗口上,OverView是一个总体概览,显示总体的内存消耗情况和疑似问题。MAT提供了多种分析维度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的分析维度是不同的。
Histogram列出内存中的所有实例类型对象和其个数以及大小,并在顶部的regex区域支持正则表达式查找
Dominator Tree 列出最大的对象及其依赖存活的Object。相比Histogram直方图,能更方便地看出引用关系
Top Consumers 是通过图像列出最大的 Object
Leak Suspects 则是自动分析内存泄露的原因的整体报告
分析内存最常用的是Histogram和Dominator Tree这两个视图,视图中一共有四列:
- Class Name:类名
- Objects:对象实例个数
- Shallow Heap:对象自身占用的内存大小,不包括它引用的对象。非数组的常规对象的Shallow Heap Size由其成员变量的数量和类型决定,数组的Shallow Heap Size由数组元素的类型(对象类型、基本类型)和数组长度决定。真正的内存都在堆上,看起来是一堆原生的byte[]、char[]、int[],对象本身的内存都很小。因此Shallow Heap对分析内存泄漏意义不是很大。
- Retained Heap:是当前对象大小与当前对象可直接或间接引用到的对象的大小总和,包括被递归释放的。即:Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存大小。
相比于MAT的查找内存泄露,个人更喜欢使用 Android studio中的 profiler,查看更加直接,点击泄露对象,查看References 排查链路中可能得泄漏点。
4. OOM常见问题
其实笔者是在优化OOM问题过程中,针对内存优化做了优化,先从技术或代码维度优化写法,减少内存占用,但是最后发现是业务的内容太多,所以最后也落点到了业务层面。
导致App 发生OOM和内存占用过高主要分为以下几个方面 :
- Bitmap内存占用:大尺寸图片资源,未合理的对图片进行裁剪缩放和缓存,导致内存占用高
- 内存泄露 :内存中有对象的引用无法被释放,导致这些对象无法正常被回收,最终导致内存耗尽(导致OOM)
- 内存溢出 :Android 应用存在固定内存限制,超过一定数量会导致程序崩溃 (导致OOM)
- 内存抖动 :内存频繁分配和回收导致内存出现锯齿抖动的现象 (导致OOM)
- 过多线程开辟 :大量创建线程,或线程池资源未正确管理,可能会占用大量内存(导致OOM)
4.1 Bitmap内存占用
其实这里主要说的就是 Bitmap Native 内存占用,因为新版本模拟器 Bitmap 内存都会放在 Native 中,并且对于直播多媒体App,Bitmap在App的占比肯定是最高的,所以这里我把Bitmap内存占用放在第一位。
App端主要有两种形式加载 Bitmap 图片:本地图片和网络图片,本地图片推荐使用webp格式,缩小图片大小,尽量保证图片尺寸和展示View大小符合。网络图片则需要动态加载好bitmap大小,获取宽高进行截取,所以推荐直接使用Glide 或 Picasso,省心省力,但是还是大概说下不用工具的话,部分写法。
如果项目中没有使用 Glide 或 Picasso 等支持图片裁切加载和多级缓存的图片仓库的话,需要自己根据View展示大小,手动压缩图片大小和图片质量。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);
int outWidth = options.outWidth;
int outHeight = options.outHeight;
int inSampleSize = calculateInSampleSize(outWidth, outHeight, reqWidth, reqHeight);
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);
如果手写缓存的话,可以使用 LruCache 或 磁盘缓存 DiskLruCache
private static final int MAX_MEMORY = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final int CACHE_SIZE = MAX_MEMORY / 8;
LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>(CACHE_SIZE) {@Overrideprotected int sizeOf(String key, Bitmap bitmap) {// 计算 Bitmap 所占内存大小return bitmap.getRowBytes() * bitmap.getHeight();}
};
使用 Glide 或 Picasso 的话这里就不过多赘述两个图片加载工具在加载,异常处理异步执行和 多级缓存等原理了,在项目中碰到的使用了 Picasso 依然可能存在对于图片拉伸计算错误导致的实际 bitmap截取后比View大小稍大的问题,所以引出我们下边要说的 Bitmap大小监控
4.2 Bitmap图片大小监控
想到的第一个办法就是实现一个自定义 ImageView (AppCompatImageView 也继承于 ImageView) ,在View中区判断要加载的图片和实际View大小在进行尺寸上的压缩和优化,但是在实际多人并发开发中很难实现和代码收口。这里列举 业志陈 文章代码。
open class MonitorImageView @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) : android.widget.ImageView(context, attrs, defStyleAttr), MessageQueue.IdleHandler {//...override fun setImageBitmap(bm: Bitmap?) {super.setImageBitmap(bm)monitor()}private fun checkDrawable() {val mDrawable = drawable ?: returnval drawableWidth = mDrawable.intrinsicWidthval drawableHeight = mDrawable.intrinsicHeightval viewWidth = measuredWidthval viewHeight = measuredHeightval imageSize = calculateImageSize(mDrawable)if (imageSize > MAX_ALARM_IMAGE_SIZE) {log(log = "图片大小超标 -> $imageSize")}if (drawableWidth > viewWidth || drawableHeight > viewHeight) {log(log = "图片尺寸超标 -> drawable:$drawableWidth x $drawableHeight view:$viewWidth x $viewHeight")}}// other methods and logic go here...
}
如果项目中使用 Glide,那第二种办法就是加载成功后添加检测 Bitmap 尺寸是否过大,但是也存在一些弊端,如果更换图片加载库,必须要每次主动调用 onResourceReady 方法获取信息。
Glide.with(context).asBitmap().load(url).listener(object : RequestListener<Bitmap> {override fun onLoadFailed(e: GlideException?,model: Any?,target: Target<Bitmap>?,isFirstResource: Boolean): Boolean {// 图片加载失败处理// ...return false}override fun onResourceReady(resource: Bitmap?,model: Any?,target: Target<Bitmap>?,dataSource: DataSource?,isFirstResource: Boolean): Boolean {// 图片加载成功,检查是否为大图resource?.let {val imageSize = calculateImageSize(it)if (imageSize > LARGE_IMAGE_THRESHOLD) {// 处理大图逻辑,如压缩、裁剪或异步加载handleLargeImage(it)}}return false}}).into(target)
简单列举了没有使用图片加载库和 使用了图片加载库 Glide几种简单检测大图方式。下面列举下目前比较流行的几种监控Bitmap大小的方案:ASM字节码插桩和Native Hoook
4.2.1 ASM字节码插桩
ASM 是一种操作 Java 字节码的工具,允许在编译时或运行时动态修改 Java 类的字节码。通过 ASM,我们可以在编译阶段插入额外的逻辑代码,例如在 ImageView.setImageBitmap()
方法中插入检测逻辑。
class LegalBitmapTransform(private val config: LegalBitmapConfig) : BaseTransform() {companion object {private const val ImageViewClass = "android/widget/ImageView"}override fun modifyClass(byteArray: ByteArray): ByteArray {val classReader = ClassReader(byteArray)val className = classReader.classNameval superName = classReader.superNameLog.log("className: $className superName: $superName")// 判断是否是 ImageView 子类return if (className != config.formatMonitorImageViewClass && superName == ImageViewClass) {val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)val classVisitor = object : ClassVisitor(Opcodes.ASM6, classWriter) {override fun visit(version: Int,access: Int,name: String?,signature: String?,superName: String?,interfaces: Array<out String>?) {// 保留类信息,但修改父类为自定义监控类(如果提供)super.visit(version,access,name,signature,config.formatMonitorImageViewClass ?: superName,interfaces)}override fun visitMethod(access: Int,name: String?,descriptor: String?,signature: String?,exceptions: Array<out String>?): MethodVisitor {val mv = super.visitMethod(access, name, descriptor, signature, exceptions)// 判断目标方法 setImageBitmap(Bitmap)if ("setImageBitmap" == name && "(Landroid/graphics/Bitmap;)V" == descriptor) {Log.log("Hooking method: setImageBitmap in $className")// 返回自定义 MethodVisitorreturn object : MethodVisitor(Opcodes.ASM6, mv) {override fun visitCode() {// 方法开始时插入逻辑super.visitCode()// 1. 加载 this(即 ImageView 实例)mv.visitVarInsn(Opcodes.ALOAD, 0)// 2. 加载 Bitmap 参数mv.visitVarInsn(Opcodes.ALOAD, 1)// 3. 调用静态方法进行监控mv.visitMethodInsn(Opcodes.INVOKESTATIC,"com/example/BitmapValidator", // 静态工具类路径"checkBitmapSize", // 静态方法名称"(Landroid/widget/ImageView;Landroid/graphics/Bitmap;)V", // 方法签名false)}}}return mv}}classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)classWriter.toByteArray()} else {// 如果不匹配目标类,则返回原始字节码byteArray}}}
4.2.2 Java Hook 和 Inline-Hook
目前Native Hook 大部分都是使用 Epic等框架实现,基于 Android ART/Java 虚拟机的运行时方法替换技术,它工作在 Java 层 或 ART 层,通常利用 ART 虚拟机的 ArtMethod
结构,通过修改 Java 方法的指针或方法表来实现 Hook,以 Hook Bitmap为例,Epic Hook主要是修改Java层的 Bitmap.createBitmap()方法
项目中 Debug环境使用的 Inline-Hook 的方式,也是看了喜马同事 世欣 GitHub 的文章 才知道原来还可以这么玩。inline-hook
是基于 机器指令级别 的 Hook 技术,直接修改目标函数的指令、地址或者二进制代码,实现对函数的拦截或行为替换。它工作在 Native 层,主要通过重写函数的入口指令或跳转地址来实现。
jint do_hook_bitmap(long bitmap_recycle_check_interval,long get_stack_threshold,long restore_image_threshold,const char *restore_image_dir,bool notify_check_local_image_size) {g_recycle_check_interval_second = bitmap_recycle_check_interval;g_get_stack_threshold = get_stack_threshold;g_restore_image_threshold = restore_image_threshold;g_restore_image_dir = restore_image_dir;g_notify_check_local_image_size = notify_check_local_image_size;int api_level = get_api_level();if (api_level > 33) {return -2;}LOGI("hookBitmapNative called, printStackThreshold: %ld, restore_image_threshold: %ld, api_level: %d",get_stack_threshold, restore_image_threshold, api_level);// 根据Android 不同版本 Bitmap.nativeCreate所在的so文件名称auto so = api_level > API_LEVEL_10_0 ? BITMAP_CREATE_SYMBOL_SO_RUNTIME_AFTER_10 : BITMAP_CREATE_SYMBOL_SO_RUNTIME;// 获取 nativeCreate方法名auto symbol = api_level >= API_LEVEL_8_0 ? BITMAP_CREATE_SYMBOL_RUNTIME : BITMAP_CREATE_SYMBOL_BEFORE_8;// 借助字节的 shadowhook 进行 bitmap hookauto stub = shadowhook_hook_sym_name(so, symbol, (void *) create_bitmap_proxy,nullptr);if (stub != nullptr) {g_ctx.open_hook = true;g_ctx.shadowhook_stub = stub;JNIEnv *jni_env;if (g_ctx.java_vm->AttachCurrentThread(&jni_env, nullptr) == JNI_OK) {jclass bitmap_java_class = jni_env->FindClass("android/graphics/Bitmap");g_ctx.bitmap_recycled_method = jni_env->GetMethodID(bitmap_java_class, "isRecycled","()Z");jclass bitmap_info_jobject = jni_env->FindClass("com/xmly/ting/android/xmbitmapmonitor/BitmapMonitorData");g_ctx.bitmap_info_jclass = static_cast<jclass>(jni_env->NewGlobalRef(bitmap_info_jobject));g_ctx.report_bitmap_data_method = jni_env->GetStaticMethodID(g_ctx.bitmap_monitor_jclass,"reportBitmapInfo","(Lcom/xmly/ting/android/xmbitmapmonitor/BitmapMonitorData;)V");g_ctx.report_bitmap_file_method = jni_env->GetStaticMethodID(g_ctx.bitmap_monitor_jclass,"reportBitmapFile","(Ljava/lang/String;)V");}//hook 成功后,开启一个线程,定时轮训当前保存的数据,如果发现有被 recycle 的,移出去,更新总体数据 (世欣 SDK 逻辑)start_loop_check_recycle_thread();return 0;}g_ctx.open_hook = false;g_ctx.shadowhook_stub = nullptr;return -1;
}
相比Epic Hook 直接拦截底层实现函数,通过修改jni函数入口指令,达到替换逻辑,几乎没有额外性能开销,不需要担心 ART动态运行时环境的兼容, 不过相比Epic ,inline-hook需要对C++和底层逻辑更加熟悉,入门门槛比较高。
项目中基于 AndroidBitmpMonitor ,编入lib模块,方便后续修改,测试环境通过打开开关,可观察 Native 内存中 bitmap的占用大小和 图片大小占比,方便我们定位Top问题。
4.2.3 重复Bitmap监控
上面讲到的AndroidBitmpMonitor ,获取到bitmap信息后,也输出了图片调用地址,如果是网络图片加载,大多调用链会指向图片加载工具类,如果是本地图片可以查看具体名称,如果想要再深入的做,可以将重复Bitmap监控落地,本人大体思路是获取到 bitmap后,可以比较Bitmap的像素数据,但是这种必须要求图片一模一样,在项目中这种可能性很低,大多还是图片尺寸或压缩比不一样,另外如果项目中图片数据量很大,Md5比较也是很慢的 (原理上来看),另外一种办法就是 哈希感知。
可以借助opencv库实现,大概思路如下:
#include <opencv2/opencv.hpp>
#include <cmath>
#include <string>// 获取图片的感知哈希值
std::string calc_perceptual_hash(const cv::Mat &img) {if (img.empty()) return "";// 1. 转换为灰度图像cv::Mat gray;cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);// 2. 缩放到 8x8cv::Mat resized;cv::resize(gray, resized, cv::Size(8, 8), 0, 0, cv::INTER_AREA);// 3. 转换为 float 类型resized.convertTo(resized, CV_32F);// 4. 计算 DCT(离散余弦变换)cv::Mat dctImage;cv::dct(resized, dctImage);// 5. 从左上角取8x8的低频分量cv::Mat dctLow = dctImage(cv::Rect(0, 0, 8, 8)).clone();// 6. 计算均值float meanValue = cv::mean(dctLow)[0];// 7. 生成感知哈希值std::string hash;for (int i = 0; i < 8; i++) {for (int j = 0; j < 8; j++) {hash += (dctLow.at<float>(i, j) > meanValue) ? "1" : "0";}}// 确保 hash 长度为 64 位return hash;
}// 比较感知哈希值之间的海明距离(Hamming Distance)
int hamming_distance(const std::string &hash1, const std::string &hash2) {if (hash1.size() != hash2.size()) return -1;int dist = 0;for (size_t i = 0; i < hash1.size(); ++i) {if (hash1[i] != hash2[i]) {dist++;}}return dist;
}// 示例:比较两张图片是否重复
bool is_duplicate_image(const cv::Mat &img1, const cv::Mat &img2, int threshold = 5) {auto hash1 = calc_perceptual_hash(img1);auto hash2 = calc_perceptual_hash(img2);if (!hash1.empty() && !hash2.empty()) {int dist = hamming_distance(hash1, hash2);return dist >= 0 && dist <= threshold;}// 图片不合法return false;
}
关于图片的优化这里收个尾,这一块其实东西很多,网上不同的检测方式也很多,这里仅做部分方案记录
关于图片内存使用注意事项:
- 本地图片尽量压缩到极致,推荐webp格式,切图尺寸尽量和ui尺寸一致避免浪费
- 多项目组同步开发,避免引入多套图片加载工具库,基建层对通用工具进行收口和规范制定
- 合理规划缓存池大小,在OnTrimMemory / LowMemory后调中,根据系统状态去释放对应的缓存和内存
- 线下使用大图检测工具,超过尺寸图片应该提供堆栈和负责人信息,
4.3 内存泄露、内存抖动和内存溢出
还是啰嗦的说下内存泄露为什么会导致OOM,内存泄漏 就是 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小,直至最后没有更多内存可以分配,产生OOM。
关于内存溢出,Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM,所以长期的内存泄露也会导致内存溢出,不过如果是一次性开辟太大的数组或者加载过大的文件图片也容易导致OOM。
内存抖动,主要是内存波动图类似锯齿状,代表存在频繁地内存开辟和销毁,容易导致内存碎片,频繁的GC也容易导致卡顿,不过在最新的ART虚拟机上,针对内存管理和回收策略做了优化,所以除非代码中存在轮训添加Bitmap又频繁移除的场景,个人感觉很少遇到内存抖动问题了。
文章开篇讲了MAT 和 Android studio Profiler的使用,这两个工具都可以用来查看内存泄露问题,通过查看Profiler的 Memory 运行曲线,也可以发现内存抖动的趋势。上面说了内存泄露、内存抖动和内存溢出的原因,那么也列举下常见的内存代码优化方式:
4.3.1 内存抖动优化
容易频繁使用的对象,使用缓存池,减少对象频繁创建和销毁 。
减少不合理对象的创建,特别是嵌套for循环,注意对象创建的位置,尽量在for结构外侧。如果是Gson解析避免多处的重复创建。
使用合理的数据结构,SparseArray类,在清楚Map的个数的时候,可以手动设置大小个数,初始化个数都在16个,减少过多开辟
4.3.2 内存泄露优化
减少内类间接持有context或Fragment对象,比如匿名内部类和 普通的 Hanlder 都存在 this$0 间接引用 的问题, 这类问题可以使用 static和 WeakReference的方式,在外类销毁的时候调用移除方法
动画也可能导致内存泄露,比如启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。
注册对象未注销。如BraodcastReceiver、EventBus未注销造成的内存泄漏,要在Activity/Fragment销毁时及时注销。
WebView内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。 腾讯X5的思路是WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
类的静态变量导致泄露。静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放
public class MainActivity extends AppCompatActivity { public static Info sInfo; ...class Info { private Context mContext; public Info(Context context) { this.mContext = context; } }
}
4.4 过多线程开辟
OOM发生也可能是线程数超标导致的,Android 中每个线程默认分配 1MB 内存,如果存在大量线程,且并没有很好的利用,也容易导致内存不足,且多线程之间也可能出现竞争和互锁问题。
常见的关于线程优化方式:
- 全局使用统一线程池,提高线程复用,避免线程的重复创建和销毁,提高性能。不过在真实使用中,也不会只有一个线程池,线程池的知识面博大精深,这里不能再展开了,实际开发中一般分为 CPU线程池和IO线程池,根据子任务调用的频次和占用耗时,CPU线程池处理更加迅速的任务,一般都是核心线程,避免最大线程数超过核心线程数。IO线程池使用频次较低,就可以把核心线程数设置低一点 一般1就足够了,最大线程数可以大一点。
- 如果是Kotlin语言,有官方提供的 Kotlin Coroutines ,通过线程分区实现虚拟化线程,更加轻量
GlobalScope.launch(Dispatchers.IO) {val result = performBackgroundTask()withContext(Dispatchers.Main) {// 更新到主线程textView.text = result}
}suspend fun performBackgroundTask(): String {// 耗时操作delay(1000)return "Task Completed"
}
- 避免并发任务过多,根据业务特点,分批处理多线程任务,避免爆发式并发问题。
- (没有实践,大项目风险太高,搞不好提桶)Android默认创建线程开辟1MB内存,32位的话,微信有黑科技是利用PLT hook需改了创建线程时的栈空间大小。但是喜马目前32位系统的设备占比很低很低,可以忽略不计,也可以使用 赵子健 的文章方案,在创建Thread的时候 Thread 的构造函数中会将 stackSize 这个变量设置为 0,这个 stackSize 变量决定了线程栈大小
Thread(ThreadGroup group, String name, int priority, boolean daemon) {……this.stackSize = 0;
}public synchronized void start() {if (started)throw new IllegalThreadStateException();group.add(this);started = false;try {nativeCreate(this, stackSize, daemon);started = true;} finally {try {if (!started) {group.threadStartFailed(this);}} catch (Throwable ignore) {}}
}
看大佬是直接在线程工厂创建的地方收口
不过本人在真实项目处理线程收敛的时候,还是比较困难,第一 部分线程是业务绑定的倒计时任务,没办法去掉,毕竟业务才是根本。 第二就是第二方和第三方的线程滥用问题,因为代码或逻辑不在本地,需要推动其他同学优化,如果App中还有多渠道的广告SDK,那么恭喜你这几个广告SDK 基本要有二十个线程开辟数了,再加上APM和日志上报,OkHttp、Glide和Bugly等工具库的引入,线程数其实优化空间很小,比较难拿结果。
5. 业务优化
在以上技术维度进行内存优化外,可结合业务优化,多抓手形成组合拳帮助解决内存问题
5.1 设备分级
设备分级最早应该是 FaceBook 的轻量工具库 Device Year Class,主要是根据设备CPU 内存等硬件信息,通过计算划分一个版本年代,项目中也是按照这个思想,但是更新了最新的参数
参考了国外早期的案例,也可以看下目前市面上的 安兔兔的跑分策略,数据主要来自四个方面,分别为 :内存、CPU、GPU和IO速度。 不过这里我仅仅使用了内存和CPU ,并且自己添加上了实时的网络监控和电量监控。简单贴一部分代码
public class LiveDeviceLevel {public static final int DEVICE_LEVEL_HIGH = 3;public static final int DEVICE_LEVEL_MID = 2;public static final int DEVICE_LEVEL_LOW = 1;public static final int DEVICE_LEVEL_UNKNOWN = -1;/*** Level judgement based on current memory and CPU.* @param context - Context object.* @return int 设备等级*/public static int judgeDeviceLevel(Context context) {int level = DEVICE_LEVEL_UNKNOWN;int ramLevel = judgeMemory(context);int cpuLevel = judgeCPU();// 内存小于等于6G CPU刷新率小于等于2G 低端机if (ramLevel == 1 || cpuLevel == 1) {level = DEVICE_LEVEL_LOW;} else if (ramLevel == 2 && (cpuLevel >= 2)) {// 内存等于8G CPU刷新率大于等于2GHz 中端机level = DEVICE_LEVEL_MID;} else if (ramLevel > 2) {// 内存大于8G CPU刷新率大于等于2.5GHz 高端机if (cpuLevel > 2) {level = DEVICE_LEVEL_HIGH;} else {level = DEVICE_LEVEL_MID;}}return level;}/*** 评定内存的等级.* @return*/private static int judgeMemory(Context context) {long ramMB = LiveDeviceInfo.getTotalMemory(context) / (1024 * 1024);int level = 1;if (ramMB <= 6000) { //低端机level = 1;} else if (ramMB <= 8000) { // 中端机level = 2;} else { //8G以上 高端机level = 3;}return level;}/*** 评定CPU等级.(按频率和厂商型号综合判断)* @return*/private static int judgeCPU() {int level = 1;int freqMHz = LiveDeviceInfo.getCPUMaxFreqKHz() / 1000;if (freqMHz <= 2000) { //2GHz 低端level = 1;} else if (freqMHz <= 2500) { //2GHz - 2.5GHz 中端level = 2;} else { //高端level = 3;}return level;}}
上述代码中缺少的 获取内存和获取最大CPU刷新数 部分方法块代码
// 获取内存总量
public static long getTotalMemory(Context c) {if (sTotalMemory > 0) {return sTotalMemory;}// memInfo.totalMem not supported in pre-Jelly Bean APIs.ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();ActivityManager am = (ActivityManager) c.getSystemService(Context.ACTIVITY_SERVICE);am.getMemoryInfo(memInfo);sTotalMemory = memInfo.totalMem;return memInfo.totalMem;
}// 获取cpu最大刷新数
public static int getCPUMaxFreqKHz() {if (sCPUMaxFreqKHz > 0) {return sCPUMaxFreqKHz;}int maxFreq = DEVICEINFO_UNKNOWN;try {for (int i = 0; i < getNumberOfCPUCores(); i++) {String filename ="/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq";File cpuInfoMaxFreqFile = new File(filename);if (cpuInfoMaxFreqFile.exists() && cpuInfoMaxFreqFile.canRead()) {byte[] buffer = new byte[128];FileInputStream stream = new FileInputStream(cpuInfoMaxFreqFile);try {stream.read(buffer);int endIndex = 0;//Trim the first number out of the byte buffer.while (Character.isDigit(buffer[endIndex]) && endIndex < buffer.length) {endIndex++;}String str = new String(buffer, 0, endIndex);int freqBound = Integer.parseInt(str);if (freqBound > maxFreq) {maxFreq = freqBound;}} catch (NumberFormatException e) {//Fall through and use /proc/cpuinfo.} finally {stream.close();}}}if (maxFreq == DEVICEINFO_UNKNOWN) {FileInputStream stream = new FileInputStream("/proc/cpuinfo");try {int freqBound = parseFileForValue("cpu MHz", stream);freqBound *= 1000; //MHz -> kHzif (freqBound > maxFreq) maxFreq = freqBound;} finally {stream.close();}}} catch (IOException e) {maxFreq = DEVICEINFO_UNKNOWN; //Fall through and return unknown.}sCPUMaxFreqKHz = maxFreq;return maxFreq;
}
真实业务中也会对网络情况和电量进行判断,如果App物理内存占用超过多少占比或者电量低于阈值,就会提醒用户打开 流畅模式。
5.2 业务降级
设备分级其实每家大厂应该都有自己的衡量方式,重要的还是根据自己的业务特色 进行不同的设备等级定制化处理,以下只简单列举下自己业务中的部分降级场景
- 如果是低端机进入直播场景,默认打开直播流畅看播功能,屏蔽他人进房POP条和小心心互动动画等非主态的交互动画,但是商业化红包和礼物不做降级处理
- 如果用户手机是中低端机,且内存阈值超过上限或者剩余电量超过最低阈值,也提醒用户打开流畅模式,尝试减少APP功耗
- 针对不同设备分级,如图片的加载格式进行相应的降低,如资源的清晰度也可以动态调整等等
6. 线上监控和告警体系搭建
线下监控前面大概列举了一些工具,比如 MAT 、Android studio Profiler和 LeakCanary(GC会引起STW 导致卡顿只推荐线下,且内存泄露只能检测Activity级别。 dump内存快照,生成hprof文件也比较耗时),线上的方案和告警依然重要,我这里比较推荐 Koom 的线上内存泄漏检测方案。
Java Heap 泄漏监控
koom-java-leak
模块用于 Java Heap 泄漏监控:它利用 Copy-on-write 机制 fork 子进程 dump Java Heap,解决了 dump 过程中 app 长时间冻结的问题,详情参考 这里
Native Heap 泄漏监控
koom-native-leak
模块用于 Native Heap 泄漏监控:它利用 Tracing garbage collection 机制分析整个 Native Heap,直接输出泄漏内存信息「大小、分配堆栈等』;极大的降低了业务同学分析、解决内存泄漏的成本。详情可以参考 这里
Thread 泄漏监控
koom-thread-leak
模块用于 Thread 泄漏监控:它会 hook 线程的生命周期函数,周期性的上报泄漏线程信息。详情参考 这里
以上是简单贴了Koom的官网原理介绍,但是源码并没有深究,有了APM工具,就可以依据Koom的结果进行上报,根据上报日志,定制不同业务域的 内存泄露告警了。
总结
由于24年的kpi是关于OOM和内存优化治理,结果来看OOM降低了 70%,但是内存优化结果不太理想,线程数有一定控制,但是内存水位依然稳定,流畅看播的业务专项,在技术层面可一定程度遏制内存增高,但是对业务影响正相关性还未验证成功,实验组和对照组数据彼此纠缠。
由于很多笔记都是平时开发过程中临时笔记,最近是年关有空进行整理,部分段落可能比较跳跃,海涵,采百家花,酿自家蜜,稳重很多工具和优化手段都是站在前人肩膀上做了定制化落地,本篇文章更多是记录工作所学,温故知新,欢迎大家评论区讨论学习,大家蛇年快乐…
参考文章
# 深入探索 Android 内存优化
# 扒一扒抖音是如何做线程优化的
相关文章:
Android 性能优化:内存优化(实践篇)
1. 前言 前一篇文章Android性能优化:内存优化 (思路篇) 大概梳理了Android 内存原理和优化的必要性及应该如何优化,输出了一套短期和长期内存优化治理的SOP方案。 那么这一篇文章就总结下我最近在做内存优化如何实践的࿰…...
开源数据集成平台白皮书重磅发布《Apache SeaTunnel 2024用户案例合集》!
2025年新年临近,Apache SeaTunnel 社区用户案例精选📘也跟大家见面啦!在过去的时间里,SeaTunnel 社区持续成长,吸引了众多开发者的关注与支持。 为了致谢一路同行的伙伴,也为了激励更多人加入技术共创&…...
用python编写一个放烟花的小程序
import pygame import random # 代码解释及使用说明: # 首先,导入 pygame 和 random 库。pygame 用于创建游戏窗口和图形绘制,random 用于生成随机数。 # 初始化 pygame,并设置屏幕尺寸为 800x600 像素,设置窗口标题为…...
以一个实际例子来学习Linux驱动程序开发之“设备类”的相关知识【利用设备类实现对同一设备类下的多个LED灯实现点亮或关闭】
前言 对于一个设备的驱动程序来说,其实上层用户主要看到的、用到的就是设备文件和设备类,当然用得最多的是设备文件,虽然设备类用得不多,但也是每一个设备注册实例化时必须要用到的东西,本篇博文就以一个简单的例子说…...
培训机构Day22
今天主要还是围绕着jquery讲解的,没有什么可说的。 知识点: 常用事件类型: 1.click:单击事件。鼠标,或键盘都可以触发。 2.dblclick:双击事件。 3.contextmenu:右键事件。 4.键盘相关事件&…...
Synopsys软件基本使用方法
Synopsys软件基本使用方法 1 文件说明2 编译流程3 查看波形4 联合仿真 本文主要介绍Synopsys软件vcs、verdi的基本使用方法,相关文件可从 GitHub下载。 1 文件说明 创建verilog源文件add.v、mult.v、top.vmodule add (input signed [31:0] dina,input signed [3…...
信息科技伦理与道德1:研究方法
1 问题描述 1.1 讨论? 请挑一项信息技术,谈一谈为什么认为他是道德的/不道德的,或者根据使用场景才能判断是否道德。判断的依据是什么(自身的道德准则)?为什么你觉得你的道德准则是合理的,其他…...
手机租赁平台开发实用指南与市场趋势分析
内容概要 在当今快速变化的科技时代,手机租赁平台的发展如火如荼。随着越来越多的人希望使用最新款的智能手机,但又不愿意承担昂贵的购机成本,手机租赁平台应运而生。这种模式不仅为用户提供了灵活的选择,还为企业创造了新的商机…...
ABAQUS三维Voronoi晶体几何建模
材料晶体塑性理论与细观尺度上晶体几何模型相融合的模拟方法为探究材料在塑性变形过程中的行为机制以及晶体材料优化开辟了新途径。本案例演示在CAD软件内通过Voronoi建立晶体三维模型,并将模型导入到Abaqus CAE内,完成晶体材料的有限元建模。 在AutoC…...
职场常用Excel基础04-二维表转换
大家好,今天和大家一起分享一下excel的二维表转换相关内容~ 在Excel中,二维表(也称为矩阵或表格)是一种组织数据的方式,其中数据按照行和列的格式进行排列。然而,在实际的数据分析过程中,我们常…...
如何使用 ChatGPT Prompts 写学术论文?
第 1 部分:学术写作之旅:使用 ChatGPT Prompts 进行学术写作的结构化指南 踏上学术写作过程的结构化旅程,每个 ChatGPT 提示都旨在解决特定方面,确保对您的主题进行全面探索。 制定研究问题: “制定一个关于量子计算的社会影响的研究问题,确保清晰并与您的研究目标保持一…...
【深度学习】Java DL4J基于 LSTM 构建新能源预测模型
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探…...
Jetson系列部署YOLOv8模型教程
简介 NVIDIA Jetson系列是专为边缘计算设计的紧凑型计算模块,其目标用户为AI开发者、嵌入式系统工程师以及需要在设备端实时进行数据处理与AI推断的创新者。通过提供灵活的硬件平台,结合NVIDIA强大的GPU计算资源,Jetson系列能够支持复杂的机…...
【HAProxy】如何在Ubuntu下配置HAProxy服务器
HAProxy 是一款免费、开源且强大的反向代理程序,它为 HTTP 和 TCP 基础的应用提供了高可用性、负载均衡以及代理功能,因此对于管理高流量服务器(或 Web 应用)来说,通过将负载分散到多个节点服务器上,它是一…...
gesp(C++一级)(7)洛谷:B3863:[GESP202309 一级] 小明的幸运数
gesp(C一级)(7)洛谷:B3863:[GESP202309 一级] 小明的幸运数 题目描述 所有个位数为 k k k 的正整数,以及所有 k k k 的倍数,都被小明称为“ k k k 幸运数”。小明想知道正整数 L L L 和 R R R 之间&a…...
【数据库系统概论】数据库完整性与触发器--复习
在数据库系统概论中,数据库完整性是指确保数据库中数据的准确性、一致性和有效性的一组规则和约束。数据库完整性主要包括实体完整性、参照完整性和用户定义完整性。以下是详细的复习内容: 1. 数据库完整性概述 数据库完整性是指一组规则,这…...
【YOLOv8模型网络结构图理解】
YOLOv8模型网络结构图理解 1 YOLOv8的yaml配置文件2 YOLOv8网络结构2.1 Conv2.2 C3与C2f2.3 SPPF2.4 Upsample2.5 Detect层 1 YOLOv8的yaml配置文件 YOLOv8的配置文件定义了模型的关键参数和结构,包括类别数、模型尺寸、骨干(backbone)和头部…...
使用 commitlint 和 husky 检查提交描述是否符合规范要求
在上一小节中,我们了解了 Git hooks 的概念,那么接下来我们就使用 Git hooks 来去校验我们的提交信息。 要完成这么个目标,那么我们需要使用两个工具: 注意:npm 需要在 7.x 以上版本。 1. commitlint 用于检查提交信…...
QT集成IntelRealSense双目摄像头3,3D显示
前两篇文章,介绍了如何继承intel realsense相机和opengl。 这里介绍如何给深度数据和色彩数据一块显示到opengl里面。 首先,需要了解深度数据和彩色数据是如何存储的。先说彩色数据。彩色图像一般都是RGB,也就是每个像素有三个字节…...
Vue 中el-table-column 进行循环,页面没渲染成功
文章目录 前言效果图代码示例可能出现的问题及原因解决思路 前言 实现效果:el-table-column 进行循环,使之代码简化 遇到的问题: data进行默认赋值,操作列的删除都可以出来,其他表格里面的数据没出来 效果图 示例&am…...
渗透测试-非寻常漏洞案例
声明 本文章所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法. 此文章不允许未经授权转发至除先知社区以外的其它平台!࿰…...
Spring Boot 实战篇(四):实现用户登录与注册功能
目录 Spring Boot 实战篇(四):实现用户登录与注册功能 一、用户注册功能 (一)前端页面设计(简要提及) (二)后端实现 二、用户登录功能 (一)…...
VScode SSH 错误:Got bad result from install script 解決
之前vscode好好的,某天突然连接报错如下 尝试1. 服务器没有断开,ssh可以正常连接 2. 用管理员权限运行vscode,无效 3. 删除服务器上的~/.vscode-server 文件夹,无效 试过很多后,原来很可能是前一天anaconda卸载导致注册表项 步…...
openGauss与GaussDB系统架构对比
openGauss与GaussDB系统架构对比 系统架构对比openGauss架构GaussDB架构 GaussDB集群管理组件 系统架构对比 openGauss架构 openGauss是集中式数据库系统,业务数据存储在单个物理节点上,数据访问任务被推送到服务节点执行,通过服务器的高并…...
【ArcGISPro/GeoScenePro】检查并处理高程数据
数据 https://arcgis.com/sharing/rest/content/items/535efce0e3a04c8790ed7cc7ea96d02d/data 数字高程模型 (DEM) 是一种栅格,可显示地面或地形的高程。 数字表面模型 (DSM) 是另一种高程栅格,可显示表面的高度,例如建筑物或树冠的顶部。 您需要准备 DEM 和 DSM 以供分析…...
WebRTC的三大线程
WebRTC中的三个主要线程: signaling_thread,信号线程:用于与应用层交互worker_thread,工作线程(最核心):负责内部逻辑处理network_thread,网络线程:负责网络数据包的收发…...
HTML-文本标签
历史上,网页的主要功能是文本展示。所以,HTML 提供了大量的文本处理标签。 1.<div> <div>是一个通用标签,表示一个区块(division)。它没有语义,如果网页需要一个块级元素容器,又没…...
C# 在PDF中添加和删除水印注释 (Watermark Annotation)
目录 使用工具 C# 在PDF文档中添加水印注释 C# 在PDF文档中删除水印注释 PDF中的水印注释是一种独特的注释类型,它通常以透明的文本或图片形式叠加在页面内容之上,为文档添加标识或信息提示。与传统的静态水印不同,水印注释并不会永久嵌入…...
Unity2022接入Google广告与支付SDK、导出工程到Android Studio使用JDK17进行打包完整流程与过程中的相关错误及处理经验总结
注:因为本人也是第一次接入广告与支付SDK相关的操作,网上也查了很多教程,很多也都是只言片语或者缺少一些关键步骤的说明,导致本人也是花了很多时间与精力踩了很多的坑才搞定,发出来也是希望能帮助到其他人在遇到相似问…...
docker部署项目
docker部署项目 (加载tar包:docker image load -i mysql.tar) 一、jdk环境配置 1.jdk下载地址 --Java Archive | Oracle 中国 --选择好版本进入 --下载Linux x64 Compressed Archive的链接 2.解压 --创建文件夹:mkdir /ro…...
C# 设计模式(创建型模式):工厂模式
C# 设计模式(创建型模式):工厂模式 引言 在软件设计中,创建型模式是用来创建对象的设计模式,它们帮助我们将对象的创建过程从业务逻辑中分离出来,减少代码的重复性和耦合度。工厂模式作为创建型设计模式之…...
REMARK-LLM:用于生成大型语言模型的稳健且高效的水印框架
REMARK-LLM:用于生成大型语言模型的稳健且高效的水印框架 前言 提出这一模型的初衷为了应对大量计算资源和数据集出现伴随的知识产权问题。使用LLM合成类似人类的内容容易受到恶意利用,包括垃圾邮件和抄袭。 ChatGPT等大语言模型LLM的开发取得的进展标志着人机对话交互的范式…...
Lumos学习王佩丰Excel第二十三讲:Excel图表与PPT
一、双坐标柱形图的补充知识 1、主次坐标设置 2、主次坐标柱形避让(通过增加两个系列,挤压使得两个柱形挨在一起) 增加两个系列 将一个系列设置成主坐标轴,另一个设成次坐标轴 调整系列位置 二、饼图美化 1、饼图美化常见设置 …...
【Vue】v-if 和 :is 都是 Vue 中的指令,但它们用于不同的目的和场景
v-if v-if 是一个条件渲染指令,用于根据表达式的真假值来决定是否渲染一块内容。当 v-if 的表达式为真(truthy)时,Vue 会确保元素被渲染到 DOM 中;当表达式为假(falsy)时,元素不会被…...
private static final Logger log = LoggerFactory.getLogger()和@Slf4j的区别
一、代码方面 - private static final Logger log LoggerFactory.getLogger()方式 详细解释 这是一种传统的获取日志记录器(Logger)的方式。LoggerFactory是日志框架(如 Log4j、Logback 等)提供的工厂类,用于创建Lo…...
【项目】基于趋动云平台的Stable Diffusion开发
【项目】基于趋动云平台的Stable Diffusion开发 (一)登录趋动云(二)创建项目:(三)初始化开发环境:(四)运行代码(五)运行模型 …...
Git的.gitignore文件详解与常见用法
诸神缄默不语-个人CSDN博文目录 在日常使用 Git 进行版本控制时,我们经常会遇到一些不需要被提交到远程仓库的文件(例如日志文件、临时配置文件、环境变量文件等)。为了忽略这些文件的提交,Git 提供了一个非常有用的功能…...
客户端二维码优化居中和背景
原始 处理后...
Linux 安装运行gatk的教程
1.下载安装 wget https://github.com/broadinstitute/gatk/releases/download/4.1.8.1/gatk-4.1.8.1.zip2.解压 unzip *.zip3.查看 gatk --help 如下显示表示安装成功: 注意:仅限在该包所在位置的路径下能使用...
C++string类
1.为什么学习string类? 1.1C语言中的字符串 C语言中,字符串是以‘\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OO…...
下载linux aarch64版本的htop
htop代码网站似乎没有编译好的各平台的包,而自己编译需要下载一些工具,比较麻烦。这里找到了快速下载和使用的方法,记录一下。 先在linux电脑上执行: mkdir htop_exe cd htop_exe apt download htop:arm64 # 会直接下载到当前目…...
MYSQL---------支持数据类型
数值类型 整数类型 TINYINT:通常用于存储小范围的整数,范围是-128到127或0到255(无符号)。例如,存储年龄可以使用TINYINT类型。示例:CREATE TABLE users (age TINYINT);SMALLINT:范围比TINYINT…...
黑马JavaWeb开发跟学(十四).SpringBootWeb原理
黑马JavaWeb开发跟学 十四.SpringBootWeb原理 SpingBoot原理1. 配置优先级2. Bean管理2.1 获取Bean2.2 Bean作用域2.3 第三方Bean 3. SpringBoot原理3.1 起步依赖3.2 自动配置3.2.1 概述3.2.2 常见方案3.2.2.1 概述3.2.2.2 方案一3.2.2.3 方案二 3.2.3 原理分析3.2.3.1 源码跟踪…...
迅为RK3568开发板编译Android12源码包-设置屏幕配置
在源码编译之前首先要确定自己想要使用的屏幕并修改源码,在编译镜像,烧写镜像。如下图所示: 第一步:确定要使用的屏幕种类,屏幕种类选择如下所示: iTOP-3568 开发板支持以下种类屏幕: 迅为 LV…...
Spring Boot 中 TypeExcludeFilter 的作用及使用示例
在Spring Boot应用程序中,TypeExcludeFilter 是一个用于过滤特定类型的组件,使之不被Spring容器自动扫描和注册为bean的工具。这在你想要排除某些类或类型(如配置类、组件等)而不希望它们参与Spring的自动装配时非常有用。 作用 …...
Prometheus 采集 JVM 数据
Prometheus 采集 JVM 数据通常通过集成 JMX Exporter(Java Management Extensions Exporter)实现。以下是完整的介绍和操作步骤: 1. 原理概述 JVM 数据采集依赖于 JMX(Java Management Extensions),JVM 提…...
OpenNJet v3.2.0正式发布!
在这个版本中,NJet实现重大突破,提供了动态Upstream的能力。这是一个关键的特性,使得NJet可以按需动态管理上游服务器池,从而使得业务方可以按需配置资源池,实现业务分区、算法切换;结合动态路由技术&#…...
TinaCMS: 革命性的开源内容管理框架
在如今的数字时代,高效的内容管理系统(CMS)已成为构建内容丰富网站和应用程序的必需品。传统 CMS,如 WordPress 和 Drupal,功能丰富但复杂度高。而新一代 CMS,例如 TinaCMS,以其灵活性和开发者友…...
VuePress2配置unocss的闭坑指南
文章目录 1. 安装依赖:准备魔法材料2. 检查依赖版本一定要一致:确保魔法配方准确无误3. 新建uno.config.js:编写咒语书4. 配置config.js和client.js:完成仪式 1. 安装依赖:准备魔法材料 在开始我们的前端魔法之前&…...
SpringCloud(二)--SpringCloud服务注册与发现
一. 引言 前文简单介绍了SpringCloud的基本简介与特征,接下来介绍每个组成部分的功能以及经常使用的中间件。本文仅为学习所用,联系侵删。 二. SpringCloud概述 2.1 定义 Spring Cloud是一系列框架的有序集合,它巧妙地利用了Spring…...