![c2d6c4200388ff572377e964088228f0.png](https://i-blog.csdnimg.cn/blog_migrate/be86f33127c1941d016ed111f1624b9f.jpeg)
WebRTC的日志不能算是系统,但是日志对于任何应用定位和解决问题都非常的重要。这里对其中的代码做一些解读,不仅介绍了日志的整个过程细节,而且对其中使用到的C++模板类和技巧做些交流。
日志文件主要是在rtc_base/logging.h和rtc_base/http://logging.cc文件里。
关键是要理解logging.h文件。在该文件的开头,有段注解,介绍如何使用log功能。
核心是RTC_LOG(sev),其他的变种可以打印更多的默认信息,比如__FUNCTION__, this等等。
另外RTC_LOG_V和RTC_LOG的区别是V版本,表示sev是个变量,而非V版本通常直接就写死了枚举值。V版本用在sev变量根据其他程序运行时条件计算出来的情况。我们SDK中一般不大用V版本。
我们知道要打印一段日志,需要比如如下的code:
RTC_LOG(LS_ERROR) << "Transaction id is not match。 sdptype:" << sdptype << " sdp: " << sdp;
这里是如何将日志最终输出到控制台或者文件的呢?让我们一点点来揭开。
- 首先看一下RTC_LOG宏定义:
sev是几个枚举值,表示本条日志的错误级别。这个定义在logging.h的开头处,就是通常的方式:从LS_VERBOSE, LS_INFO,...,到LS_NONE。
#define RTC_LOG_FILE_LINE(sev, file, line)
RTC_LOG_ENABLED() &&
::rtc::webrtc_logging_impl::LogCall() &
::rtc::webrtc_logging_impl::LogStreamer<>()
<< ::rtc::webrtc_logging_impl::LogMetadata(file, line, sev)
#define RTC_LOG(sev) RTC_LOG_FILE_LINE(::rtc::sev, __FILE__, __LINE__)
可以看到宏最终都是执行RTC_LOG_FILE_LINE
下面我们来逐行解释一下:
1. RTC_LOG_ENABLED() :全局校验,是否编译选项打开了日志功能。
2. && 这个是条件选项,我们通常是 A && B这样写,如果A是false的话,B就不会再执行。但这里就是我曾经迷惑的点,从C++的运算符优先级来看, 后面的&、<<运算符优先级更高,为什么后面的代码没有执行?但实际测试却是不执行!
3. 然后是& 和 <<,首先执行的是 <<运算符,所以先执行 ::rtc::webrtc_logging_impl::LogStreamer<>()
稍后详细讲解,这里先理解为获得一个临时的LogStreamer<>()对象
4. 然后是 << ::rtc::webrtc_logging_impl::LogMetadata(file, line, sev), 就是像上面的LogStreamer<>对象输出(存储)一个LogMetadata对象
4.1 注意一下,因为RTC_LOG(LS_ERROR) 宏后面我们的代码还会加很多 << "message" << varaible ... ,见上面打印一段日志的code例子;
4.2 所以这里实际上这里还会执行多个<<运算。把我们要输出的内容存储在(多个)LogStreamer当中。
4.3 稍后下面我们详细讲解这个过程
5. 最后执行&运算符: ::rtc::webrtc_logging_impl::LogCall() &。 这句可以看到创建一个临时的LogCall对象,并且把上面<<运算符“返回”的变量作为参数传递到&运算符重载函数中作为参数
6. 整体上再回顾一下:先获得一个LogStreamer,把LogMetadata输出(存储)到LogStreamer,最后用LogCall把(多个)LogStreamer存储的内容合并输出
在我们了解了大致过程之后,详细讲解LogStreamer对象前,我们需要先讲解一些struct Val模板类和其运用的一些相对简单的模板定义的方式。
struct Val, 它存储了每个<<之后要输出的内容,存储内容,并记录内容的类型。
我们直接在代码中注释讲解。
//模板定义:
template <LogArgType N, typename T>
struct Val {
static constexpr LogArgType Type() { return N; }
T GetVal() const { return val; }
T val;
};
//根据需要输出的变量x的类型,通过MakeVal函数生成不同的模板Val变量
//比如下面的int值输出时候,通过MakeVal 生成一个 Val<LogArgType::kInt, int>的 Val值。
inline Val<LogArgType::kInt, int> MakeVal(int x) {
return {x};
}
//同上,参数类型不同,返回不同的模板类实例
inline Val<LogArgType::kLong, long> MakeVal(long x) {
return {x};
}
inline Val<LogArgType::kLongLong, long long> MakeVal(long long x) {
return {x};
}
inline Val<LogArgType::kUInt, unsigned int> MakeVal(unsigned int x) {
return {x};
}
inline Val<LogArgType::kULong, unsigned long> MakeVal(unsigned long x) {
return {x};
}
inline Val<LogArgType::kULongLong, unsigned long long> MakeVal(
unsigned long long x) {
return {x};
}
inline Val<LogArgType::kDouble, double> MakeVal(double x) {
return {x};
}
inline Val<LogArgType::kLongDouble, long double> MakeVal(long double x) {
return {x};
}
inline Val<LogArgType::kCharP, const char*> MakeVal(const char* x) {
return {x};
}
//这里是常规输出<< "string messsage"这样的字符串时候,
//Val<LogArgType::kStdString, const std::string*>中T val成员变量只是只是临时变量的地址
inline Val<LogArgType::kStdString, const std::string*> MakeVal(
const std::string& x) {
return {&x};
}
inline Val<LogArgType::kStringView, const absl::string_view*> MakeVal(
const absl::string_view& x) {
return {&x};
}
inline Val<LogArgType::kVoidP, const void*> MakeVal(const void* x) {
return {x};
}
inline Val<LogArgType::kLogMetadata, LogMetadata> MakeVal(
const LogMetadata& x) {
return {x};
}
inline Val<LogArgType::kLogMetadataErr, LogMetadataErr> MakeVal(
const LogMetadataErr& x) {
return {x};
}
//对输出枚举类型内容时候,采用的MakeVal参数
// The enum class types are not implicitly convertible to arithmetic types.
template <typename T,
absl::enable_if_t<std::is_enum<T>::value &&
!std::is_arithmetic<T>::value>* = nullptr>
inline decltype(MakeVal(std::declval<absl::underlying_type_t<T>>())) MakeVal(
T x) {
return {static_cast<absl::underlying_type_t<T>>(x)};
}
/*
!!!这段初看不好懂,做些展开!!!
首先要了解一个知识:模板实例化时候替换的过程中遵循一个规则,替换出现错误时候,就不适用(使用)这个模板。
微软C++文档中如下描述:
在 C++ 中,模板参数的替换失败不是其本身的错误,这称为 SFINAE(替换失败不是错误)。 通常,enable_if 用于从重载决策中删除候选项(即剔除重载集),以便为了支持一个定义而拒绝另一个定义。 这符合 SFINAE 行为。 有关 SFINAE 的详细信息,请参阅 Wikipedia 上的替换失败不是错误。
可以自行google “ SFINAE(替换失败不是错误)”展开。
然后来看absl::enable_if_t这个如何理解:
就是template <typename T,
absl::enable_if_t<表达式A>* = nullptr>这段。
enable_if_t的定义如下:
注意两点:一个是整体上是一个typename T这样的替换;另一个是用到了enalbe_if类中的type声明
namespace absl{
template <bool B, typename T = void>
using enable_if_t = typename std::enable_if<B, T>::type;
}
//而enalbe_if类是如下定义的:
namespace std{
//默认是没有type声明的
template <bool _Test, class _Ty = void>
struct enable_if {}; // no member "type" when !_Test
//当第一个模板参数是true时候,定义了type
template <class _Ty>
struct enable_if<true, _Ty> { // type is _Ty for _Test
using type = _Ty;
};
}
综合结果就是:absl::enable_if_t<表达式A>* = nullptr,
在表达式A结果是false时候,type没有声明,出现了(语法)错误,
这个时候整个模板函数是不会被使用(就是调用不匹配,就是剔除,就是继续寻找其他函数)。
只有在表达式为true的时候,这个模板函数就会被使用,展开后是
template <typename T, typename T* = nullptr>,是没有语法错误的。
接着再看表达式A:std::is_enum<T>::value && !std::is_arithmetic<T>::value
这个就是正常的校验:表示如果T是枚举类型,并且T不是数字类型,返回true
(枚举形的似乎在vc下面都不是数字型,不知道为什么要 &&?)
就是说这个模板函数匹配枚举类型的参数!
接着看:返回值这个定义 decltype(MakeVal(std::declval<absl::underlying_type_t<T>>()))
主要用到了:decltype 和 declval
首先declval<Tv>的意思是生成一个编译期间的Tv临时变量。
declval的模板参数是absl::underlying_type_t<T>,
underlying_type_t是针对枚举类型的,它生成枚举类型的基础整型(可能是int,long等)类型
所以"std::declval<absl::underlying_type_t<T>>()"就是生成一个临时的如int x这样的参数。
然后"decltype(表达式)"的作用是获得表达式的类型,这里MakeVal是个函数,那就是函数返回值类型。
而MakeVal(int x)返回的类型是Val<LogArgType::kInt, int>,所以如果枚举底层存储类型是int的话,
整个返回值就能确定为“Val<LogArgType::kInt, int>”,如果是其他基础整形,类似推断。
到此:就是说这个模板函数匹配枚举类型的参数,并且将它转化为枚举的基础整形类型对应的Val模板实例
一句话,就是如果<<输出一个枚举值,那么匹配这个MakeVal模板函数,并转成int/long等Val
*/
//正常的<<"string message"我们在上面讲过是临时变量的指针。
//对于一些类,直接输出为string的时候,这里提供一个ToStringVal类型的val存储
//类的string输出,下面又分为两种情况,一种是提供了ToLogString(类)方法的和没有提供的
struct ToStringVal {
static constexpr LogArgType Type() { return LogArgType::kStdString; }
const std::string* GetVal() const { return &val; }
std::string val;
};
/*
这里顺便展开一下模板常见的"static constexpr" 这种形式的方法和成员的写法
static可以使其通过模板实例直接通过两个::访问,就是如ToStringVal::Type()
上面的讲到的enable_if<...>::Type也是,还有is_enum<T>::value也是静态的
constexpr这个是表明是常量表达式,此声明能够使编译器在编译阶段内部使用,固定不变值
*/
//下面这个has_to_log_string模板类是类似上面讲到的std::enable_if的写法
//首先默认的是定义::value是false的(其实是会有一个成员static constexptr value = false)
template <typename T, class = void>
struct has_to_log_string : std::false_type {};
//然后定义同名的另一个模板类,去测试是否能匹配,如果能匹配,这个vaule就是true
//如何匹配成功,就是上面讲到的“ SFINAE(替换失败不是错误)”规则,所以理解这个很重要
/*
decltype(ToLogString(std::declval<T>())) 这个我想如果理解了上面讲过得decltype、declval就能明白
首先std::declval<T>()生成一个T类型的临时变量,
这样就能去找ToLogString(T t)这个方法是否存在,
如果不存在就发生了错误,这个has_to_log_string模板就不会被匹配和实例化
如果存在(就是在我们代码中有一个ToLogString函数,能够把t变量作为入参),
那么就通过decltype来获得ToLogString函数的返回值作为模板定义的类型,
比如,实际中有一个
inline std::string ToLogString(TimeDelta value) {
return ToString(value);
}
这样的 ToLogString 函数,
就是能把当TimeDelta作为模板类型的时候,has_to_log_string<T1>::value有意义(没有编译错误)并且 == true
就是如下的定义:
templat <>
struct has_to_log_string<TimeDelta, std::string>: std::true_type {};
*/
template <typename T>
struct has_to_log_string<T, decltype(ToLogString(std::declval<T>()))>
: std::true_type {};
//总之,has_to_log_string现在可以用于判断一个输入的T类型是否具有一个叫ToLogString的函数
//Tips:其实这里还少一个条件,ToLogString输出必须是std::string,呵呵
//如果不存在ToLogString等,采用std::ostringstream方式把对象转为string
// 这里讲一下:
//"typename T1 = absl::decay_t<T>" 这句解释一下:就是获得T类型的原始类型,
//比如T是string*,那么T1就是string
//其他不多说了,所有点都讲过了,应该要能看懂这个函数了。
template <
typename T,
typename T1 = absl::decay_t<T>,
absl::enable_if_t<std::is_class<T1>::value &&
!std::is_same<T1, std::string>::value &&
!std::is_same<T1, LogMetadata>::value &&
!has_to_log_string<T1>::value &&
#ifdef WEBRTC_ANDROID
!std::is_same<T1, LogMetadataTag>::value &&
#endif
!std::is_same<T1, LogMetadataErr>::value>* = nullptr>
ToStringVal MakeVal(const T& x) {
std::ostringstream os; // no-presubmit-check TODO(webrtc:8982)
os << x;
return {os.str()};
}
//如果存在ToLogString可以转换T类型为string,则使用ToLogString方法转换
template <typename T, absl::enable_if_t<has_to_log_string<T>::value>* = nullptr>
ToStringVal MakeVal(const T& x) {
return {ToLogString(x)};
}
到这里Val和MakeVal都定义好了,他们能把不同类型的参数都存储到Val模板实力中,之后就能够使用它们了。
Val实际存储在LogStreamer当中,下面我们就来讲讲LogStreamer。
LogStreamer的工作过程
首先有一个印象,LogStream中重载了<<运算符,这个函数返回的是一个新的LogStreamer,所以<<可以串起来写。
先整体看一下LogStreamer,这是一个模板类:template <typename T, typename... Ts> class LogStreamer<T, Ts...>。
这里的T和Ts...是模板的类型参数,这里T实际上是我们一个输出内容的类型(Val模板实例)。
具体的直接在代码中做细节介绍。
// Ephemeral type that represents the result of the logging << operator.
template <typename... Ts>
class LogStreamer;
// Base case: Before the first << argument.
//再去看看RTC_LOG的定义中 用到的是"::rtc::webrtc_logging_impl::LogStreamer<>() "
//所以 RTC_LOG(sev) << ... ,第一个临时变量是下面这个LogStreamer<>空模板参数的实例化模板
template <>
class LogStreamer<> final {
public:
//下面有两个operator<<函数重载的模板方法,支持不同类型的参数
//参数类型是U
//返回类型是V,对应的MakeVal(U)的返回类型。
//比如U是int类型,那么V就是Val<LogArgType::kUInt, unsigned int> (tips:看MakeVal(int)函数)
//这里如果U是枚举和数值型,就采用operator<<(U arg) ,直接参数传值的方式(似乎可以不要)
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<std::is_arithmetic<U>::value ||
std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V> operator<<(U arg) const {
return LogStreamer<V>(MakeVal(arg), this);//注意
}
//这里如果U不是枚举和数值型,就采用operator<<(const U& arg) ,直接使用不可变引用传递参数
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<!std::is_arithmetic<U>::value &&
!std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V> operator<<(const U& arg) const {
return LogStreamer<V>(MakeVal(arg), this);//注意
}
//注意上面两个函数的返回值: return LogStreamer<V>(MakeVal(arg), this);
//首先返回的是LogStreamer<V>,所以后面可以继续 <<
//其次LogStreamer<V>,这里多了一个V的模板参数,V是当前arg转为Val<>之后的类型,
//就是当前要输出参数对应的Val类型
//这个Call后面再讲
template <typename... Us>
RTC_FORCE_INLINE static void Call(const Us&... args) {
static constexpr LogArgType t[] = {Us::Type()..., LogArgType::kEnd};
Log(t, args.GetVal()...);
}
//注意:这个没有模板参数的LogStreamer<>没有成员变量,它实际上只有一个Call函数
//系统默认构造函数
};
// Inductive case: We've already seen at least one << argument. The most recent
// one had type `T`, and the earlier ones had types `Ts`.
// 下面这个LogStreamer模板类,就是上面第一个LogStreamer中operator<<运算符的返回值
// 需要构建的新对象类型。
template <typename T, typename... Ts>
class LogStreamer<T, Ts...> final {
public:
//构造函数, arg要存储的数据,类型T,从上面我们可以知道是Val<>形式的值(MakeVal返回)
//prior是前一个LogStreamer,比如前一个如果是上面讲到的LogStreamer<>
//最终形成一个链表,
RTC_FORCE_INLINE LogStreamer(T arg, const LogStreamer<Ts...>* prior)
: arg_(arg), prior_(prior) {}
//下面两个operator<<和LogStreamer<>一样
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<std::is_arithmetic<U>::value ||
std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V, T, Ts...> operator<<(U arg) const {
return LogStreamer<V, T, Ts...>(MakeVal(arg), this);//注意
}
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<!std::is_arithmetic<U>::value &&
!std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V, T, Ts...> operator<<(const U& arg) const {
return LogStreamer<V, T, Ts...>(MakeVal(arg), this);//注意
}
//注意上面两个函数的返回值"return LogStreamer<V, T, Ts...>(MakeVal(arg), this);"
//通过返回值,把自己当作返回值的prev
//这个Call后面再讲
template <typename... Us>
RTC_FORCE_INLINE void Call(const Us&... args) const {
prior_->Call(arg_, args...);
}
private:
// The most recent argument.
T arg_;
// Earlier arguments.
const LogStreamer<Ts...>* prior_;
};
```
至此,我们已经看到LogStreamer<...>能够把多个<<的参数输出,转换为Val存储在内部,并形成一个反向链表
现在再来看看整个输出的过程:
```
RTC_LOG(LS_ERROR) << "Transaction id is not match。 sdptype:" << sdptype << " sdp: " << sdp;
```
RTC_LOG(LS_ERROR) 展开后插入 << ::rtc::webrtc_logging_impl::LogMetadata(file, line, sev) ,变成了
```
//我们姑且认为的命名临时变量为log1
LogStreamer<> log1 << LogMetadata(file, line, sev) << "Transaction id is not match。 sdptype:" << sdptype << " sdp: " << sdp;
根据上面的分析,大概如图:
![9ccdfe3b897f2593eef8c010464db77a.png](https://i-blog.csdnimg.cn/blog_migrate/4ce7b34949e3369aae47b43a3918240d.jpeg)
最后<<运算符计算完成后返回一个LogStreamer<T,Ts...>对象,
然后开始RTC_LOG(sev)中运算优先级别较低的& 运算符的计算,
" ::rtc::webrtc_logging_impl::LogCall() & "
待我在下篇中继续。
WebRTC日志功能解读(1)blog.pprtc.com![80ca171ec13d9a80a8c9ccabf9516d6e.png](https://i-blog.csdnimg.cn/blog_migrate/cc910efb6260ba9bf6c7267287188cf8.jpeg)