小白也能看懂的UE_LOG机制

不明觉厉的开始

不明觉厉的开始
一直在用,但是一直不敢点进去,这到底是啥玩意?

作者本人是小白,因此本篇也只面向小白,不会涉及源码或OS底层实现,不会使用过多概念性术语,只会讲实现逻辑,如果挑起了你的兴趣,可以自行去深入发掘。
截至发稿,使用UE5.1学习。本篇配合源码使用味道更佳。

第一层

UE_5.1\Engine\Source\Runtime\Core\Private\Logging\LogMacros.cpp

UE_LOG中包裹了一层又一层的宏命令,这里嵌套的宏,用于处理及优化编译条件、Fatal异常抛出、类似于LogTemp这种Condition条件检查、处理MSVC对于宏里Lambda函数的编译、对于Debug的Log分离处理等。可见,一个Log要去涵盖和处理的程序运行情况还是非常复杂的,不要小看这些,有了UE干完这些脏活累活,我们才能心无旁骛地放心调用。
如果一层一层拨开,还能看到一堆_Internal和_Impl函数,这与平台(Platform)的处理有关。最终发现,调用了FMsg::Logf_InternalImpl来执行log操作。

void FMsg::Logf_InternalImpl(
	const ANSICHAR* File, int32 Line, 
	const FLogCategoryName& Category, 
	ELogVerbosity::Type Verbosity, 
	const TCHAR* Fmt, ...)

可以看到一些关键参数出现了,比如Category==Verbosity的Pair对,TCHAR类型的数据。
Category就是某个模块或代码的Scope中分好的类型,是Log森林的一部分。这个函数处理UE的ELogVerbosity枚举的三个基本类型,erro,warn,display(实际上对应了不同的线程状态和错误抛出),根据不同的类型获取值,分别传数据给到对应的地方去输出。

这个地方就是FOutputDevice,比如Windows,Linux,ISO等,UE告诉我们,它是通过调用对应平台的API来打印log的。
那么怎么知道应该让哪个设备去执行Output呢?主要通过FOutputDeviceRedirector 来转发重定向,这个类也是属于 CORE_API的核心。

class CORE_API FOutputDeviceRedirector final : public FOutputDevice

FOutputDeviceRedirector 也重写了很多父类的虚函数。

第二层

\UE_5.1\Engine\Source\Runtime\Core\Public\Misc\OutputDevice.h
\UE_5.1\Engine\Source\Runtime\Core\Public\Misc\OutputDeviceRedirector.h

你可能早就注意到了,UE在Log的时候,会在Saved文件夹也保存一份,这其实就是序列化(Serialize())的结果,实际上,Log函数也是在调用Serialize()函数。

void FOutputDeviceRedirector::Serialize(
	const TCHAR* const Data, 
	const ELogVerbosity::Type Verbosity, 
	const FName& Category, 
	const double Time)

我们知道Log一般都很多,而且频繁更新,这个过程在底层是通过多线程的方式实现的,有很多Lock读写的操作。还要区分不同平台的特征,非常繁琐,因此,UE定义了一个结构体FOutputDeviceRedirectorState来管理这些东西。主要包括

  1. 自定义的Lock和一堆TheadID来区分和管理一堆OutputDevice,
  2. Log的结构体定义,相关参数
  3. 线程函数,开闭Log,循环Log读写等

一些你可能想到的基本操作,比如,开启Log,Log写入,Log刷新,Device添加删除等都在这里。这时,你对UE_LOG的实现逻辑基本就有了了解。

现在继续往下探。

第三层

这里面有个东西你有没有想过,对于正常情况下,你去打印“hello world”,只要把字符串传过去就可以。但是,对于要报告ERRO Log情况下,程序是面临崩溃的(PanicThread),它不一定能留下遗言。这里面就有很多问题,可以深入理解一下Verbosity level。

实际上,对于Warning和ERRO类型的Log,是在引擎的EngineLoop.cpp中设置的,用于捕获一些异常。当然这个枚举你也可以自己设置。

对于一般的Log,UE定义了两种Log的结构体,FBufferedLine & BufferedLines,它们都有相似的成员变量,主要在内存管理上不同。为了应对大量数据的收集,UE采用了一种多生产者唯一消费者的模板类来管理BufferedLinesFBufferedLine 就只用了TArray。

这个是Log原本的样子。在运行时,不在IsPanicThread的时候,后台缓存backlog,逐步写入FBufferedLine,如果确认在IsPrimaryThread,则写入BufferedLines。当然,某些主机设备不允许log或不需要缓存,则也需要特别处理。所以你能发现,Log就是一个记录百官拉屎情况的太监,它需要领会各个主子(各种状态的线程)的说话方式和心思背景,然后才能干好自己的脏活。

第四层

究竟,UE是怎么把平台的输出信息拿出来呢?

UE会尝试拉起一个新的线程,通过WakeEvent来处理std::memory_order_relaxed中的信息,然后不断地FlushBufferedLines()就可以更新所有的log。Log这种数据是用queue容器来管理的,这里你会看到很多线程安全和lamda函数。

比如,在FOutputDeviceRedirectorState::ThreadLoop()里,先Lock住线程,然后调用BufferedLines.Deplete([]())来获取所有BufferedLines入队的数据,再发送出去。

这里面其实入参类型很多,log也很多,UE通过c++的Invoke机制去发送所有的可调用对象到OutputDevice。这个发送的操作,是UE定义的BroadcastTo模板,发送一系列定义的log内容,设备信息,线程信息,调用函数等。
具体的Invoke机制可以自行了解一下,非常有趣,结合UE的反射,可以动态去Invoke函数,最终实现蓝图VM,此处不在赘述。

应用层

直接应用,放上源码。

/** 
	 * A macro to declare a logging category as a C++ "extern", usually declared in the header and paired with DEFINE_LOG_CATEGORY in the source. Accessible by all files that include the header.
	 * @param CategoryName, category to declare
	 * @param DefaultVerbosity, default run time verbosity
	 * @param CompileTimeVerbosity, maximum verbosity to compile into the code
	 **/
	#define DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity) \
		extern struct FLogCategory##CategoryName : public FLogCategory<ELogVerbosity::DefaultVerbosity, ELogVerbosity::CompileTimeVerbosity> \
		{ \
			FORCEINLINE FLogCategory##CategoryName() : FLogCategory(TEXT(#CategoryName)) {} \
		} CategoryName;

	/** 
	 * A macro to define a logging category, usually paired with DECLARE_LOG_CATEGORY_EXTERN from the header.
	 * @param CategoryName, category to define
	**/
	#define DEFINE_LOG_CATEGORY(CategoryName) FLogCategory##CategoryName CategoryName;

这样你就可以定义一个属于自己的UE log 类了。
DECLARE_LOG_CATEGORY_EXTERN会把你的分类添加到UE的分类中,然后会自己TRACE_LOG_CATEGORY识别,在FLogCategoryBase构造函数中初始化。然后在UE_LOG中就可以查到你新定义的分类了。需要注意的是,该宏只能声明在head file中。
到这里,已经拥有了相当知识的问哦们,可以做一个自己的DebugHeader。

#pragma once
#include "CoreMinimal.h"

DEFINE_LOG_CATEGORY_STATIC(DebugHeader, Log, All);

#define DEBUG_TEXT(TextToShow) FString::Printf(TEXT("[DebugHeader] : %s"), *TextToShow)

enum class EBugSeverity : uint8
{
	EInfo,
	EWarning,
	EError
};


inline void CIMTOOLUNITY_API DebugThis(const FString& TextToShow, const EBugSeverity Severity)
{
	check(!TextToShow.IsEmpty())
	if(GEngine)
	{
		switch (Severity)
		{
		case EBugSeverity::EInfo:
			GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, DEBUG_TEXT(TextToShow));
			break;
		case EBugSeverity::EWarning:
			GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Yellow, DEBUG_TEXT(TextToShow));
			UE_LOG(DebugHeader, Warning, TEXT("%s"), *TextToShow);
			break;
		case EBugSeverity::EError:
			GEngine->AddOnScreenDebugMessage(-1, 15.f, FColor::Red, DEBUG_TEXT(TextToShow));
			UE_LOG(DebugHeader, Error, TEXT("%s"), *TextToShow);
			break;
		default:
			GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TextToShow);
			break;
		}

	}
	return;
}

第五层

如果感兴趣的话,可以继续往下思考。我也会逐步更新。

比如,UE_LOG在Debug的时
候是怎么做的,如何获取到你debug的内容呢?可以从Trace.h 和 TRACE_LOG_MESSAGE入手。UE自己有一套Trace系统和Debug系统。
对于处理崩溃错误,则可以从ProcessFatalError 入手。

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值