cocos2dx加载网络图片
在浏览网页时,我们常常会看到图片以渐进(从模糊到清晰)的方式显示。在游戏开发中,比如显示高清卡牌时,采用这种方式展示图片,能为玩家带来更好的体验。
相关知识
png interlaced
PNG图片在导出时可选择 interlaced (Adam7) 模式。采用这种模式存储的PNG图片在网页上会渐进显示。该模式由Adam开发,分为7段扫描,具体方式可参考下面的GIF图。
jpg progressive
在Web浏览器中,很多图片都采用这种模式。
png解码
Cocos2d-x本身未对 interlaced 模式提供支持,但 libpng 是支持的。对于 interlaced 模式的PNG图片,必须使用 png_progressive_combine_row 逐行读取,同时 libpng 也支持非 interlaced 模式的PNG图片解析。在使用 libpng 解析时,首先要初始化 png_structp,所有解析信息都存储在这个结构体中。
以下是PNG解码准备函数的代码:
bool PNGCodec::PrepareDecode() {
png_reader_.png_struct_ptr_ = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_reader_.png_struct_ptr_)
return false;
png_reader_.png_info_ptr_ = png_create_info_struct(png_reader_.png_struct_ptr_);
if (!png_reader_.png_info_ptr_) {
png_destroy_read_struct(&png_reader_.png_struct_ptr_, NULL, NULL);
return false;
}
if (setjmp(png_jmpbuf(png_reader_.png_struct_ptr_))) {
png_destroy_read_struct(&png_reader_.png_struct_ptr_, &png_reader_.png_info_ptr_, (png_infopp)NULL);
return false;
}
png_set_error_fn(png_reader_.png_struct_ptr_, NULL, LogLibPNGDecodeError, LogLibPNGDecodeWarning);
png_set_progressive_read_fn(png_reader_.png_struct_ptr_, &png_reader_, &DecodeInfoCallback, &DecodeRowCallback, &DecodeEndCallback);
png_reader_.decode_state_ = PNGCodec::DecodeState::DECODE_READY;
return true;
}
这里的关键函数是 png_set_progressive_read_fn,通过设置回调函数来处理不同阶段的解析工作。该函数的第3个参数是读完PNG头信息的回调,第4个参数是每行数据读入的回调,第5个参数是解析结束的回调。通过这些回调函数,我们可以更新Sprite的纹理。
png_set_progressive_read_fn 函数原型如下:
/*
@parm1:png_structp
@parm2:自定义指针
@parm3:void *png_progressive_info_ptr (png_struct* png_ptr, png_info* info_ptr)
@parm4:void *png_progressive_row_ptr (png_struct* png_ptr, png_byte* new_row, png_uint_32 row_num, int pass)
@parm5:void png_progressive_end_ptr(png_struct* png_ptr, png_info* info)
*/
void png_set_progressive_read_fn(png_structrp png_ptr, png_voidp progressive_ptr, png_progressive_info_ptr info_fn,
png_progressive_row_ptr row_fn, png_progressive_end_ptr end_fn);
思路
加载网络图片的主要思路是:先从网络下载PNG数据,使用 curl 将数据传递给PNG解析器,通过PNG的回调函数更新Sprite的纹理。为避免阻塞主线程,将下载和解析操作放在一个线程中执行。
为实现这一思路,我们实现了四个类:
- PNGCoder:主要负责对PNG图片进行解析。
- HttpConnection:对 curl进行封装,用于网络数据的下载。
- CCInterlacedImage:用于缓存PNG解析后的数据。
- WebSprite:主要提供 initWithFileUrl接口。用户通过创建WebSprite实例并调用initWithFileUrl方法,将其添加到场景中。WebSprite负责创建线程和HttpConnection实例。
具体操作流程为:用户创建 WebSprite 并调用 initWithFileUrl 方法,WebSprite 会创建线程和 HttpConnection 实例,从网络下载PNG数据,将数据传递给 PNGCoder 进行解析,通过PNG的回调函数更新Sprite的纹理。
碰到的问题
线程通信
在使用 boost 库时,可通过 boost::asio::io_service 实现线程通信,它实际上是一个线程安全的函数队列。但在C++11中没有直接对应的实现,因此在 WebSprite 中创建了一个类似的队列,并通过锁来保证线程安全。
终止线程
当释放 WebSprite 时,线程处于分离状态,无法直接强制终止。std::thread 没有提供相关接口,且 curl_easy_perform 是阻塞的。为解决这个问题,可在 size_t writeData ( void * ptr , size_t size , size_t nmemb , void * stream ) 函数中返回0,此时 curl_easy_perform 会终止并返回错误。
内存释放
由于涉及跨线程操作,数据的安全释放需要格外小心。通常可设置某个标志位在两个线程间通知相关指针是否已经失效。使用 std::shared_ptr 可以很好地解决线程间的内存释放问题,其引用计数是线程安全的。
代码实现
以下是具体的代码实现,该代码基于Cocos2d 3.0开发,将下面的文件替换掉 ccp-empty-test 即可使用。
#include "CCWebSprite.h"
#include "CCInterlacedPngImage.h"
#include "http_connection.h"
#include "png_codec.h"
#include <future>
namespace cocos2d {
// Callback function used by libcurl for collect response data
size_t WebSprite::DataBridge::WriteData(void *ptr, size_t size, size_t nmemb, void *stream) {
if (stream == nullptr) {
return 0;
}
WebSprite* web_sprite = static_cast<WebSprite*>(stream);
if (web_sprite == nullptr) {
return 0;
}
size_t sizes = size * nmemb;
web_sprite->reciverData((unsigned char*)ptr, sizes);
return sizes;
}
void WebSprite::DataBridge::ReadHeaderCompleteCallBack(void *ptr) {
WebSprite* web_sprite = static_cast<WebSprite*>(ptr);
web_sprite->readHeaderComplete();
}
void WebSprite::DataBridge::ReadRowCompleteCallBack(void *ptr, int pass) {
WebSprite* web_sprite = static_cast<WebSprite*>(ptr);
web_sprite->readRowComplete(pass);
}
void WebSprite::DataBridge::ReadAllCompleteCallBack(void *ptr) {
WebSprite* web_sprite = static_cast<WebSprite*>(ptr);
web_sprite->readAllComplete();
}
WebSprite::WebSprite() : http_connection_(nullptr),
png_coder_(std::make_shared<util::PNGCodec>()),
interlaced_png_image_buff_(new InterlacedPngImage()),
code_pass_(-1) {
}
WebSprite::~WebSprite() {
if (http_connection_ != nullptr) {
http_connection_->SetWriteCallBack(nullptr, WebSprite::DataBridge::WriteData);
}
png_coder_->SetReadCallBack(nullptr, nullptr, nullptr, nullptr);
CC_SAFE_RELEASE(interlaced_png_image_buff_);
}
WebSprite* WebSprite::create() {
WebSprite* sprite = new WebSprite();
if (sprite && sprite->init()) {
sprite->autorelease();
return sprite;
}
CC_SAFE_DELETE(sprite);
return nullptr;
}
WebSprite* WebSprite::createWithFileUrl(const char *file_url) {
WebSprite* sprite = new WebSprite();
if (sprite && sprite->initWithFileUrl(file_url)) {
sprite->autorelease();
return sprite;
}
CC_SAFE_DELETE(sprite);
return nullptr;
}
bool WebSprite::initWithFileUrl(const char *file_url) {
Sprite::init();
file_url_ = file_url;
if (isRemotoeFileUrl(file_url)) {
return initWithRemoteFile();
} else {
return initWithLocalFile();
}
}
bool WebSprite::initWithRemoteFile() {
assert(http_connection_ == nullptr);
http_connection_ = std::make_shared<HttpConnection>();
http_connection_->Init(file_url_.c_str());
png_coder_->PrepareDecode();
png_coder_->SetReadCallBack(this, WebSprite::DataBridge::ReadHeaderCompleteCallBack,
WebSprite::DataBridge::ReadRowCompleteCallBack,
WebSprite::DataBridge::ReadAllCompleteCallBack);
http_connection_->SetWriteCallBack(this, WebSprite::DataBridge::WriteData);
this->scheduleUpdate();
std::thread http_thread = std::thread(std::bind(&HttpConnection::PerformGet, http_connection_));
http_thread.detach();
return true;
}
bool WebSprite::initWithLocalFile() {
auto filePath = FileUtils::getInstance()->fullPathForFilename(file_url_);
std::shared_ptr<Data> data = std::make_shared<Data>(FileUtils::getInstance()->getDataFromFile(filePath));
png_coder_->PrepareDecode();
png_coder_->SetReadCallBack(this, &WebSprite::DataBridge::ReadHeaderCompleteCallBack,
WebSprite::DataBridge::ReadRowCompleteCallBack,
WebSprite::DataBridge::ReadAllCompleteCallBack);
std::thread http_thread = std::thread(std::bind([=]() {
png_coder_->Decoding(data->getBytes(), data->getSize());
}));
http_thread.detach();
this->scheduleUpdate();
return true;
}
bool WebSprite::isRemotoeFileUrl(const char *file_url) {
if (strlen(file_url) > 7 && (strncmp(file_url, "http://", 7) == 0)) {
return true;
}
return false;
}
void WebSprite::reciverData(unsigned char *data, size_t data_size) {
png_coder_->Decoding(data, data_size);
}
void WebSprite::updateTexture() {
cocos2d::Texture2D* texture = cocos2d::Director::getInstance()->getTextureCache()->addImage(interlaced_png_image_buff_, file_url_);
texture->updateWithData(interlaced_png_image_buff_->getData(), 0, 0, interlaced_png_image_buff_->getWidth(),
interlaced_png_image_buff_->getHeight());
SpriteFrame* sprite_frame = cocos2d::SpriteFrame::createWithTexture(texture,
CCRectMake(0, 0, texture->getContentSize().width,
texture->getContentSize().height));
Sprite::setSpriteFrame(sprite_frame);
}
void WebSprite::readHeaderComplete() {
interlaced_png_image_buff_->setImageHeader(png_coder_->png_width(), png_coder_->png_height(),
png_coder_->png_color_type(), png_coder_->png_output_channels());
}
void WebSprite::readRowComplete(int pass) {
if (code_pass_ < pass) {
perform_mutex_.lock();
interlaced_png_image_buff_->setImageBodyData((char*)png_coder_->png_data_buffer(), png_coder_->png_data_size());
perform_main_thread_functions_.push_back(std::bind(&WebSprite::updateTexture, this));
perform_mutex_.unlock();
code_pass_ = pass;
}
}
// run on sub thread
void WebSprite::readAllComplete() {
perform_mutex_.lock();
interlaced_png_image_buff_->setImageBodyData((char*)png_coder_->png_data_buffer(), png_coder_->png_data_size());
perform_main_thread_functions_.push_back(std::bind(&WebSprite::updateTexture, this));
perform_mutex_.unlock();
}
void WebSprite::update(float fDelta) {
Sprite::update(fDelta);
perform_mutex_.lock();
for (std::vector<std::function<void()>>::iterator it = perform_main_thread_functions_.begin();
it != perform_main_thread_functions_.end(); ++it) {
(*it)();
}
perform_main_thread_functions_.clear();
perform_mutex_.unlock();
}
}
通过上述实现,我们可以在Cocos2d-x中实现网络图片的渐进加载功能,为游戏带来更好的视觉体验。
