查看: 1078|回复: 1

[原创发布] 内存泄漏检测脚本V3

[复制链接]
梦石
0
星屑
280
在线时间
25 小时
注册时间
2025-6-8
回帖
1
发表于 2025-7-20 10:15:30 | 显示全部楼层 |阅读模式

加入我们,或者,欢迎回来。

您需要 登录 才可以下载或查看,没有账号?注册会员

×
创意来源:https://rpg.blue/thread-369217-1-1.html
非常感谢原作者给我提供了思路,但是原本的脚本存在几个问题:数据收集不全,统计方式错误,实际用起来效果并不理想
所以我请DeepSeek帮我拓展了一下脚本,修复了原本脚本的bug基础上,多了一些花里胡哨但其实没太大用的统计功能
不要问我为什么是V3

如何判断发生了内存泄漏:
在游戏运行一段时间后,明明没干什么特别的事情,游戏突然变得卡顿,而且越来越卡,最后闪退,就很可能是内存泄漏
如果同时还导致了其他程序的崩溃,那就基本可以确定了

为什么会发生内存泄漏:
Rpgmaker中一部分资源是需要使用dispose手动清理的,如果没人清理,即使使用资源的变量已经销毁,也不会释放内存

如何修复内存泄漏:
我唯一能给出的建议就是拿着内存泄漏的脚本去问AI

如何找到发生内存泄漏的脚本:
[pre lang="ruby"]#==============================================================================
# ■ RGSS3 检测内存泄漏V3 by SaiCateDoan
#------------------------------------------------------------------------------
# 创意来源:taroxd
#
# 使用方法:
# 1.插入到Main脚本的上面,所有其他脚本的下面
# 2.正常玩游戏一段时间,进行几次战斗
# 3.在地图界面按下Alt,建议多按几次
# 4.重复2-3数次,或重复到游戏卡顿(当然你也可以偷懒)
# 5.检查未释放对象数量是否异常增长
#   具体来说,如果有数百个未释放对象,那其实是正常的(来源于脚本的Cache)
#   但如果未释放对象数量上千,并且还在不断增加,那就很可能发生了内存泄漏
# 6.在控制台中检查调用栈,寻找其中重复出现的关键词
# 7.将关键词写到下面的配置中,然后重复以上步骤(可选)
#   如果想追踪内存泄漏发生的时间,就修改TRIGGER_KEYWORD,然后时刻检查控制台
#   如果想追踪内存泄漏的速度,就修改STAT_KEYWORDS,检查不同脚本泄漏的数量
# 8.在脚本编辑器界面按下Ctrl+Shift+F,全局搜索关键词,找到发生内存泄漏的脚本
# 9.修复它!(如果看不懂,就去问AI吧)
#------------------------------------------------------------------------------

# 内存泄露检测系统配置(可修改)
TRIGGER_KEYWORD = '我是关键词'  # 触发详细调用栈打印的关键词
STAT_KEYWORDS = [:我是关键词, :我也是关键词]  # 需要统计的关键词列表

# 扩展Viewport类添加内存释放标记功能
class Viewport
  def_before(:dispose) { @__disposed__ = true }
  def disposed?; @__disposed__; end
end

need_dispose = [Bitmap, Sprite, Window, Plane, Tilemap, Viewport]  # 需要监控的对象类型
$caller_stack_db = {}        # 存储对象创建信息的全局数据库
not_disposed = []            # 记录当前未释放的对象列表

# 为所有监控类添加创建追踪功能
need_dispose.each do |klass|
  klass.class_eval do
    # 追踪initialize操作创建的对象
    def_after(:initialize) do |*|
      record_creation_info(self)
    end
   
    alias_method :original_clone, :clone
    alias_method :original_dup, :dup
   
    # 追踪clone操作创建的对象
    def clone
      obj = original_clone
      record_creation_info(obj)
      obj
    end
   
    # 追踪dup操作创建的对象
    def dup
      obj = original_dup
      record_creation_info(obj)
      obj
    end
  end
end

# 记录对象创建时的调用栈和相关信息
def record_creation_info(obj)
  stack = Kernel.caller
  contains_keyword = stack.any? { |line| line.include?(TRIGGER_KEYWORD) }
  
  # 触发关键词命中时打印完整调用栈
  if contains_keyword
    puts "[#{TRIGGER_KEYWORD}触发] 对象类型: #{obj.class}"
    puts "完整调用栈:"
    stack.each { |line| puts "> #{line}" }
    puts "-" * 20
  end

  # 保存对象创建信息到全局数据库
  $caller_stack_db[obj] = {
    :stack => stack,
    :timestamp => Time.now,
    :location => stack.first.to_s
  }
end

# 在场景切换时捕获未释放对象
Scene_Base.class_eval do
  def_after :terminate do
    current_not_disposed = []
    need_dispose.each do |klass|
      ObjectSpace.each_object(klass) do |obj|
        current_not_disposed << obj unless obj.disposed?
      end
    end
    not_disposed.replace(current_not_disposed)
  end

  # ALT键触发内存分析报告
  def_after :update do
    return unless Input.trigger?(:ALT)
   
    # 1. 随机对象分析:显示单个对象的详细信息
    target_obj = not_disposed.sample
    puts target_obj
    puts "对象类型: #{target_obj.class}"
    if target_obj && $caller_stack_db[target_obj]
      puts "创建方式: #{ $caller_stack_db[target_obj][:stack].any? { |line| line.include?('clone') } ? 'clone' :
              $caller_stack_db[target_obj][:stack].any? { |line| line.include?('dup') } ? 'dup' : 'new' }"
      puts "创建时间: #{$caller_stack_db[target_obj][:timestamp]}"
      puts "创建位置: #{$caller_stack_db[target_obj][:location]}"
      puts "调用栈:"
      $caller_stack_db[target_obj][:stack].each { |line| puts "> #{line}" }
    end

    # 2. 关键词统计:显示特定关键词的出现频率
    keyword_stats = Hash.new(0)
    not_disposed.each do |obj|
      next unless $caller_stack_db[obj]
      found_keywords = {}
      $caller_stack_db[obj][:stack].each do |line|
        STAT_KEYWORDS.each do |kw|
          if !found_keywords[kw] && line.include?(kw.to_s)
            found_keywords[kw] = true
            keyword_stats[kw] += 1
          end
        end
        break if found_keywords.size == STAT_KEYWORDS.size
      end
    end
   
    keyword_stats.each { |kw, count| puts "包含关键词 #{kw} 的对象数量: #{count}" }

    # 3. 全局统计:对象类型和创建方式的分布情况
    type_stats = Hash.new(0)
    create_method_stats = Hash.new(0)
    not_disposed.each do |obj|
      next unless $caller_stack_db[obj]
      type_stats[obj.class] += 1
      stack = $caller_stack_db[obj][:stack]
      create_method_stats[stack.any? { |line| line =~ /`clone'/ || line.include?('clone') } ? 'clone' :
                         stack.any? { |line| line =~ /`dup'/ || line.include?('dup') } ? 'dup' : 'new'] += 1
    end
   
    puts "\n未释放对象类型统计:"
    type_stats.each { |klass, count| puts "#{klass.name}: #{count}" }
   
    puts "\n对象创建方式统计:"
    create_method_stats.each { |method, count| puts "#{method}: #{count}" }
  end
end[/pre]

评分

参与人数 2星屑 +30 赞 +2 收起 理由
Ruigi + 1 精品文章
寂静的夜里 + 30 + 1 塞糖

查看全部评分

梦石
0
星屑
280
在线时间
25 小时
注册时间
2025-6-8
回帖
1
 楼主| 发表于 2025-7-20 10:19:37 | 显示全部楼层
另外,由于内存泄漏发生在joiplay上时后果通常更加严重,而且joiplay缺少获取控制台输出的手段
所以就有了下面这个便宜脚本,方便在joiplay上调试游戏
  1. #==============================================================================
  2. # ■ RGSS3 控制台重定向到文件 by SaiCateDoan
  3. #------------------------------------------------------------------------------
  4. # 日志文件路径(默认在游戏根目录)
  5. log_file = "debug_log.txt"

  6. # 备份原始标准输出
  7. $original_stdout = $stdout.dup if $original_stdout.nil?

  8. # 重定向输出到文件(追加模式)
  9. $stdout.reopen(log_file, "a")
  10. $stdout.sync = true  # 实时写入

  11. # 添加时间戳和分隔符
  12. puts "\n\n===== Log Started: #{Time.now} ====="

  13. # 确保游戏退出时关闭文件流
  14. at_exit do
  15.   puts "===== Log Ended: #{Time.now} ====="
  16.   $stdout.close
  17.   $stdout = $original_stdout  # 恢复原始输出
  18. end
复制代码
回复 1 0

使用道具 举报

您需要登录后才可以回帖 登录 | 注册会员

本版积分规则

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖返回顶部