019-C++跨平台开发内存检测
《C++文章汇总》
上一篇介绍了《018-智能指针》,本文介绍实际开发中利用Xcode工具对内存进行检测。
在音视频开发中,不可避免要使用C++,需要我们自己管理内存,不像OC可通过自动释放池autoreleasepool管理内存,如何知道我们写的C++代码是否有内存忘记释放,内存泄露,内存破坏呢?可充分利用Xcode检测调试工具进行检测调试
1.开发中遇到的问题
图片.png
- 需求
图片.png
图片.png
- 现实开发中
图片.png
图片.png
内存问题
图片.png
I.内存破坏
class Cat{
public:
char name[4];
int age;
void show(){
std::cout << "cat name:" << name << ", age:" << age << std::endl;
}
};
class Dog{
public:
char tag[4];
char name[8];
int year;
void show(){
std::cout << "dog name:" << name << ",age:" << year << std::endl;
}
void setName(char *name_){
strcpy(name, name_);
}
};
int main(){
Cat cat = {
.name = "Tom",
.age = 6,
};
Dog *dog = (Dog *)&cat;
dog->setName((char *)"d");
cat.show();
return 0;
}
//输出
cat name:Tom, age:100
此时猫的age变为100,因为猫对象的内存地址被破坏了,dog指向了猫的内存地址,并将这块内存地址的第五个字节赋值为"d"
图片.png
II.内存泄露
现象:内存不断上涨或周期性上涨
模块析构后内存没有下降
图片.png
处理方法:
A.开启内存日志
B.使用内存泄漏工具发现泄漏内存
C.通过内存malloc日志找到创建的调用栈
D.跟踪内存使用情况
图片.png
#include <stdio.h>
#include <thread>
#include <unistd.h>
#include <iostream>
class MyFancyObject{
private:
size_t buffer_size_;
char *buffer_;
public:
MyFancyObject():buffer_size_(1024),buffer_((char*)malloc(buffer_size_)){};
void say(){
printf("buffer: %p,size:%ld\n",buffer_,(long)buffer_size_);
}
};
void TaskOne(){
auto object = MyFancyObject();
object.say();
}
int main(int argc,const char *argv[]){
std::cout << "App Started\n";
auto task1 = std::thread([]{
for (;;) {
TaskOne();
usleep(100);
}
});
task1.join();
for (;;);
return 0;
}
发现内存一直缓慢增加,通过Xcode查看堆栈信息
一、开启Allocation Log
- cmd+shift+< 编辑 Schema
-
打开 Malloc Stack Logging
图片.png
二、打开Memory Graph
图片.png
Memory Graph 使用方法
1.点过滤按钮查看问题内存列表
2.点击问题项查看
3.右侧显示创建时的调用栈
图片.png
图片.png
图片.png
MyFancyObject对象创建时申请了一块buffer_size_大小的内存空间,没有析构函数去释放
给MyFancyObject增加析构函数去释放即可
class MyFancyObject{
private:
size_t buffer_size_;
char *buffer_;
public:
MyFancyObject():buffer_size_(1024),buffer_((char*)malloc(buffer_size_)){};
void say(){
printf("buffer: %p,size:%ld\n",buffer_,(long)buffer_size_);
}
~MyFancyObject(){
free(buffer_);
}
};
图片.png
内存占比很平稳,一条平行线,没有内存泄露
III.内存忘记释放
现象:在模块重复使用过程中内存阶梯式上涨
模块析构后内存没有下降
图片.png
处理思路:使用功能后看残存的内存
开启 Profile 打开 Instrument
长按运行按钮选 Profile 或 Cmd + I
图片.png
选则 Allocations
点击 Choose
图片.png
点击录制按钮运行
图片.png
图片.png
图片.png
重复执行任务
如进出同一个界面,
重复执行同一个任务等
(示例代码中为重复玩游戏)
在每个周期前点击
“Mark Generation” 按钮
图片.png
展开后可以看snapshot中
当前仍然未释放的对象
右侧查看创建调用栈
2.gif
图片.png
图片.png
图片.png
game对象一直存在于队列games中未被释放,需在上报后将game对象从队列中移除games.pop_back();
class ScoreUploader{
public:
ScoreUploader(){
//上报线程,轮询上报队列上报
_thread = std::thread([this]{
for (;;) {
{
std::lock_guard<std::mutex> l(mu);
if (!games.empty()) {
//Upload last game result
//do_report(games.front())
games.pop_back();
sleep(2);
}
}
sleep(1);
}
});
}
~ScoreUploader(){
_thread.join();
}
void Upload(std::unique_ptr<GuessGame> game){
//将完成的游戏加入上报队列
std::lock_guard<std::mutex> l(mu);
games.push_back(std::move(game));
}
private:
std::thread _thread;
std::mutex mu;
std::vector<std::unique_ptr<GuessGame>> games;
};
图片.png
IV.内存被破坏
现象1.偶现异常Crash
图片.png现象2.奇怪的输出
如出现了不在上下文的字符串、
花屏、数值异常等
图片.png
这里随机打印四个字符串,但有奇怪的东西混进来了
一、开启Address Sanitizer,关闭Allocation Log(两者不能同时存在,都是hook malloc函数)1. cmd+shift+< 编辑 Schema
-
打开 Address Sanitizer
图片.png
运行后会挂在内存破坏的指令上
图片.png
控制台会输出原因及详细信息
图片.png
Xcode 会根据错误信息作展示
图片.png
如此处会展示访问的内存的
创建调用栈
析构调用栈
图片.png
二、为什么会有XXXXXXX的打印出现?
作用域{}结束后MyFancyObject对象被释放,buffer_指针指向的区域被释放,buffer_指针仍然指向这块区域,变为野指针,这块区域中的内容等着被覆盖,此时obj被干掉,指针还在
某一时刻,TwoTask刚刚申请一块内存区域给obj,并分配区域给buffer_指向,写入了四个随机字符串中的一个,此时调用了析构函数,buffer_变成了野指针,此时这块区域又被分配给了ThreeTask中的obj对象的成员变量buffer_指向,正好写入了"XXXXXXXXXX",覆盖了随机字符串,程序在TwoTask执行writer->present();打印"XXXXXXXXXX"
三、为什么会Crash?
在作用域{}中,obj对象MyFancyObject被创建,构造函数中会申请1024个字节的buffer_内存空间,在作用域{}结束,obj对象被释放,buffer_指向的1024个字节也被释放
Writer对象的构造方法中会对obj对象有一个引用,有个成员变量obj指向MyFancyObject对象obj的内存空间,此时obj对象已经被释放了,Writer对象中的指针obj指向的是一块被释放的区域,是一个野指针
这个时候再往buffer_指向的区域去写数据,会报错,工具能帮助定位偶现的crash,基本可以让一个偶现的问题变成必现的问题
四、工具的原理:在创建对象前或后加一些redzone标记,8个字节内存会增加标记位,如果8字节内存被析构掉会把标记位置为负数,编译器加一些代码,访问内存的时候去检查,这块内存是否被释放掉了
图片.png
图片.png
图片.png
Demo地址