不明觉厉的开始
一直在用,但是一直不敢点进去,这到底是啥玩意?
作者本人是小白,因此本篇也只面向小白,不会涉及源码或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
来管理这些东西。主要包括
- 自定义的Lock和一堆TheadID来区分和管理一堆OutputDevice,
- Log的结构体定义,相关参数
- 线程函数,开闭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采用了一种多生产者唯一消费者的模板类来管理BufferedLines
,FBufferedLine
就只用了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 入手。