【Android 内存优化】KOOM 快手开源框架线上内存监控方案-源码剖析

news2024/10/5 21:21:18

文章目录

  • 前言
  • OOMMonitorInitTask.INSTANCE.init
  • OOMMonitor.INSTANCE.startLoop
  • super.startLoop
    • call() == LoopState.Terminate
  • dumpAndAnalysis
  • dump
  • startAnalysisService
  • 回到startLoop方法
  • 总结

前言

这篇文章主要剖析KOOM的Java层源码设计逻辑。

使用篇请看上一篇:
【Android KOOM】KOOM java leak使用全解析

OOMMonitorInitTask.INSTANCE.init

OOMMonitorInitTask.INSTANCE.init(JavaLeakTestActivity.this.getApplication());

这里进行初始化,来看看init里面做了什么:

object OOMMonitorInitTask : InitTask {

  override fun init(application: Application) {
    val config = OOMMonitorConfig.Builder()
      .setThreadThreshold(50) //50 only for test! Please use default value!
      .setFdThreshold(300) // 300 only for test! Please use default value!
      .setHeapThreshold(0.9f) // 0.9f for test! Please use default value!
      .setVssSizeThreshold(1_000_000) // 1_000_000 for test! Please use default value!
      .setMaxOverThresholdCount(1) // 1 for test! Please use default value!
      .setAnalysisMaxTimesPerVersion(3) // Consider use default value!
      .setAnalysisPeriodPerVersion(15 * 24 * 60 * 60 * 1000) // Consider use default value!
      .setLoopInterval(5_000) // 5_000 for test! Please use default value!
      .setEnableHprofDumpAnalysis(true)
      .setHprofUploader(object : OOMHprofUploader {
        override fun upload(file: File, type: OOMHprofUploader.HprofType) {
          MonitorLog.e("OOMMonitor", "todo, upload hprof ${file.name} if necessary")
        }
      })
      .setReportUploader(object : OOMReportUploader {
        override fun upload(file: File, content: String) {
          MonitorLog.i("OOMMonitor", content)
          MonitorLog.e("OOMMonitor", "todo, upload report ${file.name} if necessary")
        }
      })
      .build()

    MonitorManager.addMonitorConfig(config)
  }
}

可以看到里面做了各种参数的配置,包括上传hprof和报告的上传回调。
使用了构建者模式来进行参数设置,接着通过MonitorManager.addMonitorConfig(config) 添加到MonitorManager中,可见MonitorManager这个类就是监控器管理用的。

interface InitTask {
  fun init(application: Application)
}

定义了一个接口,用来初始化内存监控任务。参数是需要传递Application,但是这里没有看到有使用到。

OOMMonitor.INSTANCE.startLoop

        OOMMonitor.INSTANCE.startLoop(true, false,5_000L);

上面配置好咯参数和回调,这里就是开始循环。下面来看看里面做了什么。


object OOMMonitor : LoopMonitor<OOMMonitorConfig>(), LifecycleEventObserver {
@Volatile
  private var mIsLoopStarted = false
...
  override fun startLoop(clearQueue: Boolean, postAtFront: Boolean, delayMillis: Long) {
    throwIfNotInitialized { return }

    if (!isMainProcess()) {
      return
    }

    MonitorLog.i(TAG, "startLoop()")

    if (mIsLoopStarted) {
      return
    }
    mIsLoopStarted = true

    super.startLoop(clearQueue, postAtFront, delayMillis)
    getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)
  }
  ...
  }

判断下,假如非主线程,立刻返回。这里可以看出来,调用的地方必须是主线程,不然它就不会执行。
来看下mIsLoopStarted,它被Volatile修饰。Volatile的作用是可以把对应的变量刷新到Cpu缓存中,保证了多线程环境变量的可见性。假如有其他线程修改了这个变量,那么其他线程可以立刻知道。
而这里判断假如loop已经开始,那么也return掉。这些属于健壮性代码。

super.startLoop

看下super.startLoop:

  open fun startLoop(
      clearQueue: Boolean = true,
      postAtFront: Boolean = false,
      delayMillis: Long = 0L
  ) {
    if (clearQueue) getLoopHandler().removeCallbacks(mLoopRunnable)

    if (postAtFront) {
      getLoopHandler().postAtFrontOfQueue(mLoopRunnable)
    } else {
      getLoopHandler().postDelayed(mLoopRunnable, delayMillis)
    }

    mIsLoopStopped = false
  }

这里可看到围绕着mLoopRunnable来做功夫。首先看看是否需要清理之前的mLoopRunnable,接着根据参数,决定把runable post到消息队列的哪种情况中,这个稍后研究。这里先看看哪里传入的Handler。

通过跳转,找到了这里:

package com.kwai.koom.base.loop

import android.os.Handler
import android.os.HandlerThread
import android.os.Process.THREAD_PRIORITY_BACKGROUND

internal object LoopThread : HandlerThread("LoopThread", THREAD_PRIORITY_BACKGROUND) {
  init {
    start()
  }

  internal val LOOP_HANDLER = Handler(LoopThread.looper)
}


这里是一个HandlerThread,至于HandlerThread。并且LoopThread它在初始化就执行start方法来启动线程。

接着看mLoopRunnable


  protected open fun getLoopInterval(): Long {
    return DEFAULT_LOOP_INTERVAL
  }
  
 companion object {
    private const val DEFAULT_LOOP_INTERVAL = 1000L
  }

private val mLoopRunnable = object : Runnable {
    override fun run() {
      if (call() == LoopState.Terminate) {
        return
      }

      if (mIsLoopStopped) {
        return
      }

      getLoopHandler().removeCallbacks(this)
      getLoopHandler().postDelayed(this, getLoopInterval())
    }
  }

这里就是拿到handler,执行postDelayed,间隔设置为1秒。

call() == LoopState.Terminate

这行代码是关键,假如LoopState.Terminate,是结束状态的话,那就执行call方法。
在这里插入图片描述
看下OOMMonitor的实现:

  override fun call(): LoopState {
    if (!sdkVersionMatch()) {
      return LoopState.Terminate
    }

    if (mHasDumped) {
      return LoopState.Terminate
    }

    return trackOOM()
  }

假如dump完成,就返回terminate状态。继续看trackOOM方法:

 private fun trackOOM(): LoopState {
    SystemInfo.refresh()

    mTrackReasons.clear()
    for (oomTracker in mOOMTrackers) {
      if (oomTracker.track()) {
        mTrackReasons.add(oomTracker.reason())
      }
    }

    if (mTrackReasons.isNotEmpty() && monitorConfig.enableHprofDumpAnalysis) {
      if (isExceedAnalysisPeriod() || isExceedAnalysisTimes()) {
        MonitorLog.e(TAG, "Triggered, but exceed analysis times or period!")
      } else {
        async {
          MonitorLog.i(TAG, "mTrackReasons:${mTrackReasons}")
          dumpAndAnalysis()
        }
      }

      return LoopState.Terminate
    }

    return LoopState.Continue
  }

看下refresh方法:


 var procStatus = ProcStatus()
  var lastProcStatus = ProcStatus()

  var memInfo = MemInfo()
  var lastMemInfo = MemInfo()

  var javaHeap = JavaHeap()
  var lastJavaHeap = JavaHeap()

  fun refresh() {
    lastJavaHeap = javaHeap
    lastMemInfo = memInfo
    lastProcStatus = procStatus

    javaHeap = JavaHeap()
    procStatus = ProcStatus()
    memInfo = MemInfo()

    javaHeap.max = Runtime.getRuntime().maxMemory()
    javaHeap.total = Runtime.getRuntime().totalMemory()
    javaHeap.free = Runtime.getRuntime().freeMemory()
    javaHeap.used = javaHeap.total - javaHeap.free
    javaHeap.rate = 1.0f * javaHeap.used / javaHeap.max

    File("/proc/self/status").forEachLineQuietly { line ->
      if (procStatus.vssInKb != 0 && procStatus.rssInKb != 0
          && procStatus.thread != 0) return@forEachLineQuietly

      when {
        line.startsWith("VmSize") -> {
          procStatus.vssInKb = VSS_REGEX.matchValue(line)
        }

        line.startsWith("VmRSS") -> {
          procStatus.rssInKb = RSS_REGEX.matchValue(line)
        }

        line.startsWith("Threads") -> {
          procStatus.thread = THREADS_REGEX.matchValue(line)
        }
      }
    }

    File("/proc/meminfo").forEachLineQuietly { line ->
      when {
        line.startsWith("MemTotal") -> {
          memInfo.totalInKb = MEM_TOTAL_REGEX.matchValue(line)
        }

        line.startsWith("MemFree") -> {
          memInfo.freeInKb = MEM_FREE_REGEX.matchValue(line)
        }

        line.startsWith("MemAvailable") -> {
          memInfo.availableInKb = MEM_AVA_REGEX.matchValue(line)
        }

        line.startsWith("CmaTotal") -> {
          memInfo.cmaTotal = MEM_CMA_REGEX.matchValue(line)
        }

        line.startsWith("ION_heap") -> {
          memInfo.IONHeap = MEM_ION_REGEX.matchValue(line)
        }
      }
    }

    memInfo.rate = 1.0f * memInfo.availableInKb / memInfo.totalInKb

    MonitorLog.i(TAG, "----OOM Monitor Memory----")
    MonitorLog.i(TAG,"[java] max:${javaHeap.max} used ratio:${(javaHeap.rate * 100).toInt()}%")
    MonitorLog.i(TAG,"[proc] VmSize:${procStatus.vssInKb}kB VmRss:${procStatus.rssInKb}kB " + "Threads:${procStatus.thread}")
    MonitorLog.i(TAG,"[meminfo] MemTotal:${memInfo.totalInKb}kB MemFree:${memInfo.freeInKb}kB " + "MemAvailable:${memInfo.availableInKb}kB")
    MonitorLog.i(TAG,"avaliable ratio:${(memInfo.rate * 100).toInt()}% CmaTotal:${memInfo.cmaTotal}kB ION_heap:${memInfo.IONHeap}kB")
  }

SystemInfo类里面有很多Java堆,内存信息,进程状态相关的类。这里面可以看出,这个类就是用来把一些监控到的数据刷新和写入文件里面的。当然,还有log输出。

再看mOOMTrackers,分别是各个跟踪器

  private val mOOMTrackers = mutableListOf(
    HeapOOMTracker(), ThreadOOMTracker(), FdOOMTracker(),
    PhysicalMemoryOOMTracker(), FastHugeMemoryOOMTracker()
  )

他们抽象父类是:

abstract class OOMTracker : Monitor<OOMMonitorConfig>() {
  /**
   * @return true 表示追踪到oom、 false 表示没有追踪到oom
   */
  abstract fun track(): Boolean

  /**
   * 重置track状态
   */
  abstract fun reset()

  /**
   * @return 追踪到的oom的标识
   */
  abstract fun reason(): String
}

至于具体怎么track,由于篇幅和内容方向问题,这篇文章先不进一步分析。留到后面的文章继续。

回到trackOOM方法:

   mTrackReasons.clear()
    for (oomTracker in mOOMTrackers) {
      if (oomTracker.track()) {
        mTrackReasons.add(oomTracker.reason())
      }
    }

  if (mTrackReasons.isNotEmpty() && monitorConfig.enableHprofDumpAnalysis) {
      if (isExceedAnalysisPeriod() || isExceedAnalysisTimes()) {
        MonitorLog.e(TAG, "Triggered, but exceed analysis times or period!")
      } else {
        async {
          MonitorLog.i(TAG, "mTrackReasons:${mTrackReasons}")
          dumpAndAnalysis()
        }
      }

假如track到了原因,它就添加mTrackReasons。
假如分析超过时间和次数,就打印error。其它正常情况就打印mTrackReasons,执行dumpAndAnalysis,然后返回LoopState.Terminate状态。

下面重点看看dumpAndAnalysis方法:

dumpAndAnalysis

 private fun dumpAndAnalysis() {
    MonitorLog.i(TAG, "dumpAndAnalysis");
    runCatching {
      if (!OOMFileManager.isSpaceEnough()) {
        MonitorLog.e(TAG, "available space not enough", true)
        return@runCatching
      }
      if (mHasDumped) {
        return
      }
      mHasDumped = true

      val date = Date()

      val jsonFile = OOMFileManager.createJsonAnalysisFile(date)
      val hprofFile = OOMFileManager.createHprofAnalysisFile(date).apply {
        createNewFile()
        setWritable(true)
        setReadable(true)
      }

      MonitorLog.i(TAG, "hprof analysis dir:$hprofAnalysisDir")

      ForkJvmHeapDumper.getInstance().run {
        dump(hprofFile.absolutePath)
      }

      MonitorLog.i(TAG, "end hprof dump", true)
      Thread.sleep(1000) // make sure file synced to disk.
      MonitorLog.i(TAG, "start hprof analysis")

      startAnalysisService(hprofFile, jsonFile, mTrackReasons.joinToString())
    }.onFailure {
      it.printStackTrace()

      MonitorLog.i(TAG, "onJvmThreshold Exception " + it.message, true)
    }
  }

这里面正式把track到的数据写入到文件中,包括json文件和hprof文件。重点看dump方法:

dump


  @Override
  public synchronized boolean dump(String path) {
    MonitorLog.i(TAG, "dump " + path);
    if (!sdkVersionMatch()) {
      throw new UnsupportedOperationException("dump failed caused by sdk version not supported!");
    }
    init();
    if (!mLoadSuccess) {
      MonitorLog.e(TAG, "dump failed caused by so not loaded!");
      return false;
    }

    boolean dumpRes = false;
    try {
      MonitorLog.i(TAG, "before suspend and fork.");
      int pid = suspendAndFork();
      if (pid == 0) {
        // Child process
        Debug.dumpHprofData(path);
        exitProcess();
      } else if (pid > 0) {
        // Parent process
        dumpRes = resumeAndWait(pid);
        MonitorLog.i(TAG, "dump " + dumpRes + ", notify from pid " + pid);
      }
    } catch (IOException e) {
      MonitorLog.e(TAG, "dump failed caused by " + e);
      e.printStackTrace();
    }
    return dumpRes;
  }

init方法:

  private void init () {
    if (mLoadSuccess) {
      return;
    }
    if (loadSoQuietly("koom-fast-dump")) {
      mLoadSuccess = true;
      nativeInit();
    }
  }

这里加载一个so库,可以看到还有这些native方法:

/**
   * Init before do dump.
   */
  private native void nativeInit();

  /**
   * Suspend the whole ART, and then fork a process for dumping hprof.
   *
   * @return return value of fork
   */
  private native int suspendAndFork();

  /**
   * Resume the whole ART, and then wait child process to notify.
   *
   * @param pid pid of child process.
   */
  private native boolean resumeAndWait(int pid);

  /**
   * Exit current process.
   */
  private native void exitProcess();

接着执行suspendAndFork,也是native方法。拿到进程pid之后,fork当前进程。然后dump hprof文件。
在这里插入图片描述
至于为什么需要fork一个进程出来dump,可以通过上面截图看出来原因,dump hprof 数据的时候会触发GC,而GC会出发STW,这无疑会造成APP卡顿。这也是LeakCanary不能做成线上内存监控的主要原因,而KOOM解决了这个问题。

子进程dump工作做完之后,接着exitProcess退出。

假如pid > 0,resumeAndWait,就恢复整个ART虚拟机,然后等待子线程唤醒。

这里逻辑我说的有点不清晰,由于看不到so的代码,无法确认。有知道的大佬可以指点一下,感激。

startAnalysisService

前面fork子进程后,执行了 Thread.sleep(1000) // make sure file synced to disk.
接着看是分析堆转信息工作:

private fun startAnalysisService(
    hprofFile: File,
    jsonFile: File,
    reason: String
  ) {
    if (hprofFile.length() == 0L) {
      hprofFile.delete()
      MonitorLog.i(TAG, "hprof file size 0", true)
      return
    }

    if (!getApplication().isForeground) {
      MonitorLog.e(TAG, "try startAnalysisService, but not foreground")
      mForegroundPendingRunnables.add(Runnable {
        startAnalysisService(
          hprofFile,
          jsonFile,
          reason
        )
      })
      return
    }

    OOMPreferenceManager.increaseAnalysisTimes()

    val extraData = AnalysisExtraData().apply {
      this.reason = reason
      this.currentPage = getApplication().currentActivity?.localClassName.orEmpty()
      this.usageSeconds = "${(SystemClock.elapsedRealtime() - mMonitorInitTime) / 1000}"
    }

    HeapAnalysisService.startAnalysisService(
      getApplication(),
      hprofFile.canonicalPath,
      jsonFile.canonicalPath,
      extraData,
      object : AnalysisReceiver.ResultCallBack {
        override fun onError() {
          MonitorLog.e(TAG, "heap analysis error, do file delete", true)

          hprofFile.delete()
          jsonFile.delete()
        }

        override fun onSuccess() {
          MonitorLog.i(TAG, "heap analysis success, do upload", true)

          val content = jsonFile.readText()

          MonitorLogger.addExceptionEvent(content, Logger.ExceptionType.OOM_STACKS)

          monitorConfig.reportUploader?.upload(jsonFile, content)
          monitorConfig.hprofUploader?.upload(hprofFile, OOMHprofUploader.HprofType.ORIGIN)
        }
      })
  }

这里就是进行针对一些dump数据进行解析、整理等工作,假如需要上传到服务器,这里也预留了接口供开发者使用,非常贴心。

到这里KOOM框架的Java层核心代码逻辑基本过完了。

回到startLoop方法

回到startLoop方法中super.startLoop 方法,下一行代码是:

    getLoopHandler().postDelayed({ async { processOldHprofFile() } }, delayMillis)

前面分析知道,getLoopHandler拿到的是HandlerThread,这里延时post一个runable消息给它。这里使用协程来执行。

重点需要关注的是processOldHprofFile。


object OOMMonitor : LoopMonitor<OOMMonitorConfig>(), LifecycleEventObserver {
  private const val TAG = "OOMMonitor"
  ...
  private fun processOldHprofFile() {
    MonitorLog.i(TAG, "processHprofFile")
    if (mHasProcessOldHprof) {
      return
    }
    mHasProcessOldHprof = true;
    reAnalysisHprof()
    manualDumpHprof()
  }
  ...

  private fun reAnalysisHprof() {
    for (file in hprofAnalysisDir.listFiles().orEmpty()) {
      if (!file.exists()) continue

      if (!file.name.startsWith(MonitorBuildConfig.VERSION_NAME)) {
        MonitorLog.i(TAG, "delete other version files ${file.name}")
        file.delete()
        continue
      }

      if (file.canonicalPath.endsWith(".hprof")) {
        val jsonFile = File(file.canonicalPath.replace(".hprof", ".json"))
        if (!jsonFile.exists()) {
          MonitorLog.i(TAG, "create json file and then start service")
          jsonFile.createNewFile()
          startAnalysisService(file, jsonFile, "reanalysis")
        } else {
          MonitorLog.i(
            TAG,
            if (jsonFile.length() == 0L) "last analysis isn't succeed, delete file"
            else "delete old files", true
          )
          jsonFile.delete()
          file.delete()
        }
      }
    }
  }

 private fun manualDumpHprof() {
    for (hprofFile in manualDumpDir.listFiles().orEmpty()) {
      MonitorLog.i(TAG, "manualDumpHprof upload:${hprofFile.absolutePath}")
      monitorConfig.hprofUploader?.upload(hprofFile, OOMHprofUploader.HprofType.STRIPPED)
    }
  }

}

里面就是操作dump出来的文件,判断当前的版本,假如是旧的,删掉重写等逻辑。

总结

截止到这里,我们开始监控的这两行代码分析完毕:

  /*
         * Init OOMMonitor
         */
        OOMMonitorInitTask.INSTANCE.init(JavaLeakTestActivity.this.getApplication());
        OOMMonitor.INSTANCE.startLoop(true, false,5_000L);

很简单的两行代码,里面包含了如此之多的业务逻辑和精彩的设计。

很多时候,我们使用越是简单的开源框架,越是能证明作者的厉害之处。他们把繁杂的逻辑内聚到了框架里面,让使用者能用简单一两行代码实现复杂的逻辑业务。

KOOM作为一个线上内存监控框架,有很多优秀的设计。这篇文章也只是在外层分析了一些表面的技术逻辑,至于更深入的内容,后续会继续更新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1502501.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LCR 132. 砍竹子 II

解题思路&#xff1a; 由于数量级较大&#xff0c;需要使用long以及快速幂求余 下图便于理解快速幂求余 class Solution {public int cuttingBamboo(int bamboo_len) {if(bamboo_len < 3) return bamboo_len - 1;int b bamboo_len % 3, p 1000000007;long rem 1, x 3;f…

计算机网络——23网络层导论

网络层导论 网络层服务 在发送主机和接收主机对之间传送段&#xff08;segment&#xff09;在发送端将段封装到数据报中在接收端&#xff0c;将段上交给传输层实体网络层协议存在于每一个主机和路由器路由器检查每一个经过它的IP数据报的头部 网络层的关键功能 网络层功能 …

【重温设计模式】备忘录模式及其Java示例

备忘录模式的概述 在软件设计的世界中&#xff0c;备忘录模式是一种行为设计模式&#xff0c;它的主要作用是保存对象的当前状态&#xff0c;以便在将来的某个时间点&#xff0c;可以将对象恢复到这个保存的状态。这种模式的命名源于生活中的备忘录&#xff0c;我们常常用它来…

图机器学习(4)-面向连接层面的人工特征工程

0 问题定义 通过已经连接去猜未知连接&#xff1a; 有两个思路&#xff1a; &#xff08;1&#xff09;直接提取link的特征&#xff0c;把link变成D维向量&#xff1b; &#xff08;2&#xff09;把link两端节点的D维向量拼在一起&#xff0c;缺点&#xff1a;丢失了link本身…

JavaScript极速入门-综合案例(3)

综合案例 猜数字 预期效果 代码实现 <button type"button" id"reset">重新开始一局游戏</button><br>请输入要猜的数字:<input type"text" id"number"><button type"button" id"button&q…

php使用ElasticSearch

ElasticSearch简介 Elasticsearch 是一个分布式的、开源的搜索分析引擎&#xff0c;支持各种数据类型&#xff0c;包括文本、数字、地理、结构化、非结构化。 Lucene与ElasticSearch Apache Lucene是一款高性能的、可扩展的信息检索&#xff08;IR&#xff09;工具库&#xf…

算法学习06:数组模拟:单/双链表,栈和队列,单调栈/队列

算法学习06&#xff1a;数组模拟&#xff1a;单/双链表&#xff0c;栈和队列&#xff0c;单调栈/队列 文章目录 算法学习06&#xff1a;数组模拟&#xff1a;单/双链表&#xff0c;栈和队列&#xff0c;单调栈/队列前言一、链表1.单链表2.双链表 二、栈和队列1.普通栈、队列2.单…

[LeetCode][102]二叉树的层序遍历——遍历结果中每一层明显区分

题目 102. 二叉树的层序遍历 给定二叉树的根节点 root&#xff0c;返回节点值的层序遍历结果。即逐层地&#xff0c;从左到右访问所有节点。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]] 示例 2&#xff1a; 输入…

【计算机网络】TCP 的三次握手与四次挥手

通常我们进行 HTTP 连接网络的时候会进行 TCP 的三次握手&#xff0c;然后传输数据&#xff0c;之后再释放连接。 TCP 传输如图1所示&#xff1a; 图1 TCP 传输 TCP三次握手的过程如下&#xff1a; 第一次握手&#xff1a;建立连接。客户端发送连接请求报文段&#xff0c;将 …

深度学习模型部署(四)常用模型及推理平台评估指标

判断选择什么模型&#xff0c;什么量化方案&#xff0c;什么推理框架&#xff0c;最基础的知识就是如何评估自己的模型以及推理平台。 模型衡量标准 衡量一个模型的最直接标准就是运算速度&#xff0c;但是运算速度是无法计算的&#xff0c;所以定义了一些间接标准来推测模型的…

探索数据可视化:Matplotlib 基础指南

图形绘制 import numpy as np import pandas as pd import matplotlib.pyplot as pltx np.linspace(0,2 * np.pi,100)# 说明&#xff1a;正弦波。x&#xff1a;NumPy数组 # 所有的数据&#xff0c;进行正弦计算 y np.sin(x)plt.plot(x,y)# 指定x轴范围 plt.xlim(-1,10) # 指…

99%的商业模式都是可借鉴,解读法国葡萄酒庄长盛不衰的经营模式?

99%的商业模式都是可借鉴&#xff0c;解读法国葡萄酒庄长盛不衰的经营模式&#xff1f; 文丨微三云营销总监胡佳东&#xff0c;点击上方“关注”&#xff0c;为你分享市场商业模式电商干货。 - 引言&#xff1a;很多朋友说到葡萄酒&#xff0c;有的第一印象就是“法国葡萄酒”…

全面对比Amazon DocumentDB 与 MongoDB

在云中部署 MongoDB 似乎有多种选择。例如&#xff0c;Amazon DocumentDB自称是完全支持 MongoDB API 的 AWS 原生数据库。虽然它支持一些 MongoDB 功能&#xff0c;但需要注意的是 DocumentDB 并不完全兼容 MongoDB。要在 AWS 上访问功能齐全的“MongoDB 即服务”&#xff0c;…

CCProxy代理服务器地址的设置步骤

目录 前言 一、下载和安装CCProxy 二、启动CCProxy并设置代理服务器地址 三、验证代理服务器设置是否生效 四、使用CCProxy进行代理设置的代码示例 总结 前言 CCProxy是一款常用的代理服务器软件&#xff0c;可以帮助用户实现网络共享和上网代理。本文将详细介绍CCProxy…

用Python实现一个简单的——人脸相似度对比

近几年来&#xff0c;兴起了一股人工智能热潮&#xff0c;让人们见到了AI的能力和强大&#xff0c;比如图像识别&#xff0c;语音识别&#xff0c;机器翻译&#xff0c;无人驾驶等等。总体来说&#xff0c;AI的门槛还是比较高&#xff0c;不仅要学会使用框架实现&#xff0c;更…

Windows上websocket客户端连接定时存储消息到文件并加载文件定时发送服务端工具实现

场景 在业务开发中&#xff0c;需要对接三方websocket协议数据或者连接并存储线上websocket协议数据&#xff0c;需要使用websocket客户端 连接线上的websocket服务端获取并存储数据&#xff0c;然后将数据存储成文件格式可移植&#xff0c;并将数据复制 到本地&#xff0c;…

15. C++泛型与符号重载

【泛型编程】 若多组类型不同的数据需要使用相同的代码处理&#xff0c;在C语言中需要编写多组代码分别处理&#xff0c;这样做显然太过繁琐&#xff0c;C增加了虚拟类型&#xff0c;使用虚拟类型可以实现一组代码处理多种类型的数据。 虚拟类型是暂时不确定的数据类型&#…

免费的文案二次创作软件,打造高质量原创文案

在当今数字化时代&#xff0c;内容创作已经成为企业和个人推广自身品牌、产品和服务的重要手段。然而&#xff0c;对于许多人来说&#xff0c;撰写高质量的原创文案并非易事。幸运的是&#xff0c;随着技术的发展&#xff0c;出现了许多文案二次创作免费软件&#xff0c;为那些…

怎么做不限扫码次数的文件活码?文件可长期扫描展现下载

如何制作不限扫码次数的文件二维码呢&#xff1f;将文件转二维码后分享给其他人&#xff0c;是现在非常方便的一种文件传输方式。很多小伙伴在制作文件二维码的时候&#xff0c;比较担心的一个问题&#xff0c;就是二维码可以扫码的次数&#xff0c;担心达不到自己预期的效果&a…

保姆级讲解 Stable Diffusion

目录 本文讲解思路介绍 一、引入 二、Diffusion Model 三、原文的摘要和简介 四、Stable Diffusion 4.1、组成模块 4.2、感知压缩 4.3、条件控制 五、图解 Stable Diffusion 5.1、潜在空间的扩散 5.2、条件控制 5.3、采样 5.4、Diffusion Model 与 Stable Diffusion …