diff --git a/reader/browser/BrowserPage.cpp b/reader/browser/BrowserPage.cpp index 05122aadb..cf0a37c4f 100644 --- a/reader/browser/BrowserPage.cpp +++ b/reader/browser/BrowserPage.cpp @@ -33,10 +33,15 @@ const int ICON_SIZE = 23; +// 主线程专用的存活页注册表:入队时的 page 裸指针仅在回包时由主线程 handler 解引用, +// handler 必须先经 existPage 校验,避免向已析构页面回包(UAF) +static QSet g_alivePages; + BrowserPage::BrowserPage(SheetBrowser *parent, int index, DocSheet *sheet) : QGraphicsItem(), m_sheet(sheet), m_parent(parent), m_index(index) { qCDebug(appLog) << "BrowserPage created, index:" << index; + g_alivePages.insert(this); setAcceptHoverEvents(true); setFlag(QGraphicsItem::ItemIsPanel); @@ -48,7 +53,9 @@ BrowserPage::BrowserPage(SheetBrowser *parent, int index, DocSheet *sheet) : BrowserPage::~BrowserPage() { // qCDebug(appLog) << "BrowserPage destroyed, index:" << m_index; + g_alivePages.remove(this); PageRenderThread::clearImageTasks(m_sheet, this); + PageRenderThread::clearAllTasksForPage(this); // 断开并销毁夜间异步任务 watcher:后台滤镜任务持有的都是副本,安全丢弃 delete m_nightWatcher; @@ -62,6 +69,12 @@ BrowserPage::~BrowserPage() // qCDebug(appLog) << "BrowserPage::~BrowserPage() - Destructor completed"; } +bool BrowserPage::existPage(const BrowserPage *page) +{ + // 仅主线程调用:注册表只在主线程增删,无需加锁 + return g_alivePages.contains(page); +} + QRectF BrowserPage::boundingRect() const { // qCDebug(appLog) << "BrowserPage::boundingRect() - Calculating bounding rectangle"; @@ -329,6 +342,10 @@ void BrowserPage::render(const double &scaleFactor, const Dr::Rotation &rotation task.page = this; + task.renderer = m_sheet ? m_sheet->rendererPtr() : nullptr; + task.uuid = m_sheet ? m_sheet->uuid() : QString(); + task.pageIndex = itemIndex(); + task.pixmapId = m_pixmapId; const qreal deviceRatio = dApp ? dApp->devicePixelRatio() : 1.0; @@ -345,6 +362,12 @@ void BrowserPage::render(const double &scaleFactor, const Dr::Rotation &rotation task.page = this; + task.renderer = m_sheet ? m_sheet->rendererPtr() : nullptr; + task.uuid = m_sheet ? m_sheet->uuid() : QString(); + task.pageIndex = itemIndex(); + task.scaleFactor = m_scaleFactor; + task.originSize = m_originSizeF; + task.pixmapId = m_pixmapId; const qreal deviceRatio = dApp ? dApp->devicePixelRatio() : 1.0; @@ -365,6 +388,10 @@ void BrowserPage::render(const double &scaleFactor, const Dr::Rotation &rotation task.page = this; + task.renderer = m_sheet ? m_sheet->rendererPtr() : nullptr; + task.uuid = m_sheet ? m_sheet->uuid() : QString(); + task.pageIndex = itemIndex(); + PageRenderThread::appendTask(task); } } @@ -394,6 +421,10 @@ void BrowserPage::renderRect(const QRectF &rect) task.page = this; + task.renderer = m_sheet ? m_sheet->rendererPtr() : nullptr; + task.uuid = m_sheet ? m_sheet->uuid() : QString(); + task.pageIndex = itemIndex(); + task.pixmapId = m_pixmapId; task.whole = QRect(0, 0, @@ -659,6 +690,10 @@ void BrowserPage::loadWords() task.page = this; + task.renderer = m_sheet ? m_sheet->rendererPtr() : nullptr; + task.uuid = m_sheet ? m_sheet->uuid() : QString(); + task.pageIndex = itemIndex(); + PageRenderThread::appendTask(task); m_wordHasRendered = false; diff --git a/reader/browser/BrowserPage.h b/reader/browser/BrowserPage.h index 9e2198db8..b2a9d5986 100644 --- a/reader/browser/BrowserPage.h +++ b/reader/browser/BrowserPage.h @@ -41,6 +41,15 @@ class BrowserPage : public QGraphicsItem ~BrowserPage() override; + /** + * @brief existPage + * 判断页面是否存活(仅主线程调用)。渲染回包 handler 解引用 task.page + * 之前必须校验,避免向已析构页面回包 + * @param page 待校验页面 + * @return 存活返回 true + */ + static bool existPage(const BrowserPage *page); + /** * @brief 文档页缩放后的原区域 不受旋转影响 * @return diff --git a/reader/browser/PageRenderThread.cpp b/reader/browser/PageRenderThread.cpp index 6dcdfb043..61ba9aa27 100644 --- a/reader/browser/PageRenderThread.cpp +++ b/reader/browser/PageRenderThread.cpp @@ -133,6 +133,79 @@ bool PageRenderThread::clearImageTasks(DocSheet *sheet, BrowserPage *page, int p // qCDebug(appLog) << "PageRenderThread::clearImageTasks() - Clear image tasks completed"; return true; } +void PageRenderThread::clearAllTasksForSheet(DocSheet *sheet) +{ + PageRenderThread *inst = instance(); + if (nullptr == inst) + return; + + // 排空所有引用该 sheet 的待处理任务,避免析构后后台线程悬空访问 + inst->m_pageNormalImageMutex.lock(); + for (int i = inst->m_pageNormalImageTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageNormalImageTasks[i].sheet == sheet) + inst->m_pageNormalImageTasks.removeAt(i); + } + inst->m_pageNormalImageMutex.unlock(); + + inst->m_pageSliceImageMutex.lock(); + for (int i = inst->m_pageSliceImageTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageSliceImageTasks[i].sheet == sheet) + inst->m_pageSliceImageTasks.removeAt(i); + } + inst->m_pageSliceImageMutex.unlock(); + + inst->m_pageBigImageMutex.lock(); + for (int i = inst->m_pageBigImageTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageBigImageTasks[i].sheet == sheet) + inst->m_pageBigImageTasks.removeAt(i); + } + inst->m_pageBigImageMutex.unlock(); + + inst->m_pageWordMutex.lock(); + for (int i = inst->m_pageWordTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageWordTasks[i].sheet == sheet) + inst->m_pageWordTasks.removeAt(i); + } + inst->m_pageWordMutex.unlock(); + + inst->m_pageAnnotationMutex.lock(); + for (int i = inst->m_pageAnnotationTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageAnnotationTasks[i].sheet == sheet) + inst->m_pageAnnotationTasks.removeAt(i); + } + inst->m_pageAnnotationMutex.unlock(); + + inst->m_pageThumbnailMutex.lock(); + for (int i = inst->m_pageThumbnailTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageThumbnailTasks[i].sheet == sheet) + inst->m_pageThumbnailTasks.removeAt(i); + } + inst->m_pageThumbnailMutex.unlock(); +} + +void PageRenderThread::clearAllTasksForPage(const BrowserPage *page) +{ + PageRenderThread *inst = instance(); + if (nullptr == inst || nullptr == page) + return; + + // BrowserPage 析构时调用:文字/注释任务不在 clearImageTasks 覆盖范围内, + // 若不清除,worker 完成后回包将解引用已析构的 page(主线程 handler 崩溃) + inst->m_pageWordMutex.lock(); + for (int i = inst->m_pageWordTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageWordTasks[i].page == page) + inst->m_pageWordTasks.removeAt(i); + } + inst->m_pageWordMutex.unlock(); + + inst->m_pageAnnotationMutex.lock(); + for (int i = inst->m_pageAnnotationTasks.count() - 1; i >= 0; --i) { + if (inst->m_pageAnnotationTasks[i].page == page) + inst->m_pageAnnotationTasks.removeAt(i); + } + inst->m_pageAnnotationMutex.unlock(); +} + void PageRenderThread::appendTask(DocPageNormalImageTask task) { @@ -356,9 +429,13 @@ void PageRenderThread::run() continue; } + if (task.renderer.isNull()) { + continue; + } + QList renderRects; - if (task.sheet->renderer()->hasWidgetAnnots(task.page->itemIndex())) { + if (task.renderer->hasWidgetAnnots(task.pageIndex)) { //if has signature,render whole rect renderRects.append(task.rect); } else { @@ -377,12 +454,12 @@ void PageRenderThread::run() if (m_quit) break; - //外部删除了此处不判断会导致崩溃 - if (!DocSheet::existSheet(task.sheet)) + //uuid失效说明sheet已销毁(或新文档已接管),仅用于避免为将死文档做无用渲染; + //正确性由任务中的renderer共享引用保证,此处解引用不再有UAF风险 + if (!DocSheet::existSheetByUuid(task.uuid)) break; - //判断page存在之后 使用page之前,也就是此处,如果主线程先一步进入page被删流程,【理论上会导致崩溃】,目前概率非常低,未发现 - QImage image = task.sheet->getImage(task.page->itemIndex(), task.rect.width(), task.rect.height(), + QImage image = task.renderer->getImage(task.pageIndex, task.rect.width(), task.rect.height(), QRect(static_cast(rect.x()), static_cast(rect.y()), static_cast(rect.width()), @@ -421,9 +498,9 @@ void PageRenderThread::run() break; // 预取图片对象 bbox(夜间蒙版用):在工作线程取,避免 UI 线程与渲染争文档锁 - if (DocSheet::existSheet(task.sheet) && task.sheet->renderer()->opened()) { - task.imageRects = task.sheet->renderer()->getImageObjectRects( - task.page->itemIndex(), task.rect.width(), task.rect.height()); + if (DocSheet::existSheetByUuid(task.uuid) && task.renderer->opened()) { + task.imageRects = task.renderer->getImageObjectRects( + task.pageIndex, task.rect.width(), task.rect.height()); } emit sigDocPageBigImageTaskFinished(task, pixmap); @@ -599,7 +676,7 @@ bool PageRenderThread::execNextDocPageNormalImageTask() return false; } - if (!DocSheet::existSheet(task.sheet)) { + if (task.renderer.isNull() || !DocSheet::existSheetByUuid(task.uuid)) { qCWarning(appLog) << "Sheet no longer exists, skip task"; return true; } @@ -608,24 +685,22 @@ bool PageRenderThread::execNextDocPageNormalImageTask() int targetHeight = task.rect.height(); if (targetWidth <= 0 || targetHeight <= 0) { const qreal deviceRatio = dApp ? dApp->devicePixelRatio() : 1.0; - const double pageScale = (task.page && task.page->m_scaleFactor > 0.0) - ? task.page->m_scaleFactor - : (task.sheet ? task.sheet->operation().scaleFactor : 1.0); - const QSizeF pageSize = task.page ? task.page->m_originSizeF : QSizeF(); + const double pageScale = task.scaleFactor > 0.0 ? task.scaleFactor : 1.0; + const QSizeF pageSize = task.originSize; targetWidth = qMax(1, qRound(pageSize.width() * pageScale * deviceRatio)); targetHeight = qMax(1, qRound(pageSize.height() * pageScale * deviceRatio)); task.rect = QRect(0, 0, targetWidth, targetHeight); } - QImage image = task.sheet->getImage(task.page->itemIndex(), targetWidth, targetHeight); + QImage image = task.renderer->getImage(task.pageIndex, targetWidth, targetHeight); if (image.isNull()) { - qCWarning(appLog) << "Failed to get image for page:" << task.page->itemIndex(); + qCWarning(appLog) << "Failed to get image for page:" << task.pageIndex; } else { - qCDebug(appLog) << "Image rendered successfully for page:" << task.page->itemIndex(); + qCDebug(appLog) << "Image rendered successfully for page:" << task.pageIndex; // 预取图片对象 bbox(夜间蒙版用):在工作线程取,避免 UI 线程与渲染争文档锁 - task.imageRects = task.sheet->renderer()->getImageObjectRects( - task.page->itemIndex(), targetWidth, targetHeight); + task.imageRects = task.renderer->getImageObjectRects( + task.pageIndex, targetWidth, targetHeight); emit sigDocPageNormalImageTaskFinished(task, QPixmap::fromImage(image)); } @@ -648,12 +723,12 @@ bool PageRenderThread::execNextDocPageSliceImageTask() } - if (!DocSheet::existSheet(task.sheet)) { + if (task.renderer.isNull() || !DocSheet::existSheetByUuid(task.uuid)) { qCDebug(appLog) << "文档不存在,取切片任务已结束"; return true; } - QImage image = task.sheet->getImage(task.page->itemIndex(), task.whole.width(), task.whole.height(), task.slice); + QImage image = task.renderer->getImage(task.pageIndex, task.whole.width(), task.whole.height(), task.slice); if (!image.isNull()) emit sigDocPageSliceImageTaskFinished(task, QPixmap::fromImage(image)); @@ -677,13 +752,13 @@ bool PageRenderThread::execNextDocPageWordTask() return false; } - if (!DocSheet::existSheet(task.sheet)) { + if (task.renderer.isNull() || !DocSheet::existSheetByUuid(task.uuid)) { qCDebug(appLog) << "文档不存在,取页码文字任务已结束"; return true; } - const QList &words = task.sheet->renderer()->getWords(task.page->itemIndex()); + const QList &words = task.renderer->getWords(task.pageIndex); emit sigDocPageWordTaskFinished(task, words); @@ -706,12 +781,12 @@ bool PageRenderThread::execNextDocPageAnnotationTask() return false; } - if (!DocSheet::existSheet(task.sheet)) { + if (task.renderer.isNull() || !DocSheet::existSheetByUuid(task.uuid)) { qCDebug(appLog) << "文档不存在,取页码注释任务已结束"; return true; } - const QList annots = task.sheet->renderer()->getAnnotations(task.page->itemIndex()); + const QList annots = task.renderer->getAnnotations(task.pageIndex); emit sigDocPageAnnotationTaskFinished(task, annots); @@ -734,15 +809,16 @@ bool PageRenderThread::execNextDocPageThumbnailTask() return false; } - if (!DocSheet::existSheet(task.sheet)) { + if (task.renderer.isNull() || !DocSheet::existSheetByUuid(task.uuid)) { qCDebug(appLog) << "文档不存在,缩略图任务已结束"; return true; } - QImage image = task.sheet->getImage(task.index, 174, 174); + QImage image = task.renderer->getImage(task.index, 174, 174); - if (!image.isNull()) + if (!image.isNull()) { emit sigDocPageThumbnailTaskFinished(task, QPixmap::fromImage(image)); + } qCDebug(appLog) << "执行缩略图任务已完成"; return true; } @@ -762,19 +838,22 @@ bool PageRenderThread::execNextDocOpenTask() return false;//false 为不用再继续循环调用 } - if (!DocSheet::existSheet(task.sheet)) { + if (task.uuid.isEmpty() || !DocSheet::existSheetByUuid(task.uuid)) { qCDebug(appLog) << "文档不存在,文档打开任务已结束"; return true; } - QString filePath = task.sheet->filePath(); + QString filePath = task.filePath; PERF_PRINT_BEGIN("POINT-03", QString("filename=%1,filesize=%2").arg(QFileInfo(filePath).fileName()).arg(QFileInfo(filePath).size())); deepin_reader::Document::Error error = deepin_reader::Document::NoError; - qCDebug(appLog) << "PageRenderThread::execNextDocOpenTask" << task.sheet->convertedFileDir(); - deepin_reader::Document *document = deepin_reader::DocumentFactory::getDocument(task.sheet->fileType(), filePath, task.sheet->convertedFileDir(), task.password, &(task.sheet->m_process), error); + qCDebug(appLog) << "PageRenderThread::execNextDocOpenTask" << task.convertedFileDir; + //getDocument出参改为局部变量,由worker带回、主线程回调写回sheet,避免跨线程写sheet成员 + QProcess *process = nullptr; + deepin_reader::Document *document = deepin_reader::DocumentFactory::getDocument(task.fileType, filePath, task.convertedFileDir, task.password, &process, error); + task.process = process; if (nullptr == document) { emit sigDocOpenTask(task, error, nullptr, QList()); @@ -832,6 +911,8 @@ void PageRenderThread::onDocPageNormalImageTaskFinished(DocPageNormalImageTask t { // qCDebug(appLog) << "PageRenderThread::onDocPageNormalImageTaskFinished() - Starting on doc page normal image task finished"; if (DocSheet::existSheet(task.sheet)) { + if (nullptr != task.page && !BrowserPage::existPage(task.page)) + return; // 页面已析构,丢弃残留回包(task.page 非空时必须存活才可解引用) task.page->setImageObjectRects(task.imageRects, task.rect.width(), task.rect.height()); task.page->handleRenderFinished(task.pixmapId, pixmap); } @@ -842,6 +923,8 @@ void PageRenderThread::onDocPageSliceImageTaskFinished(DocPageSliceImageTask tas { // qCDebug(appLog) << "PageRenderThread::onDocPageSliceImageTaskFinished() - Starting on doc page slice image task finished"; if (DocSheet::existSheet(task.sheet)) { + if (nullptr != task.page && !BrowserPage::existPage(task.page)) + return; // 页面已析构,丢弃残留回包 task.page->handleRenderFinished(task.pixmapId, pixmap, task.slice); } // qCDebug(appLog) << "PageRenderThread::onDocPageSliceImageTaskFinished() - On doc page slice image task finished completed"; @@ -851,6 +934,8 @@ void PageRenderThread::onDocPageBigImageTaskFinished(DocPageBigImageTask task, Q { // qCDebug(appLog) << "PageRenderThread::onDocPageBigImageTaskFinished() - Starting on doc page big image task finished"; if (DocSheet::existSheet(task.sheet)) { + if (nullptr != task.page && !BrowserPage::existPage(task.page)) + return; // 页面已析构,丢弃残留回包 task.page->setImageObjectRects(task.imageRects, task.rect.width(), task.rect.height()); task.page->handleRenderFinished(task.pixmapId, pixmap); } @@ -861,6 +946,8 @@ void PageRenderThread::onDocPageWordTaskFinished(DocPageWordTask task, QListhandleWordLoaded(words); } // qCDebug(appLog) << "PageRenderThread::onDocPageWordTaskFinished() - On doc page word task finished completed"; @@ -870,6 +957,8 @@ void PageRenderThread::onDocPageAnnotationTaskFinished(DocPageAnnotationTask tas { // qCDebug(appLog) << "PageRenderThread::onDocPageAnnotationTaskFinished() - Starting on doc page annotation task finished"; if (DocSheet::existSheet(task.sheet)) { + if (nullptr != task.page && !BrowserPage::existPage(task.page)) + return; // 页面已析构,丢弃残留回包 task.page->handleAnnotationLoaded(annots); } // qCDebug(appLog) << "PageRenderThread::onDocPageAnnotationTaskFinished() - On doc page annotation task finished completed"; @@ -895,6 +984,9 @@ void PageRenderThread::onDocOpenTask(DocOpenTask task, deepin_reader::Document:: return; } + //getDocument在worker线程创建的QProcess,此处(主线程)写回sheet,消除worker对sheet成员的跨线程写 + sheet->m_process = task.process; + sheet->renderer()->handleOpened(error, document, pages); // qCDebug(appLog) << "PageRenderThread::onDocOpenTask() - On doc open task completed"; } diff --git a/reader/browser/PageRenderThread.h b/reader/browser/PageRenderThread.h index 13a6945a7..12b4a6df5 100644 --- a/reader/browser/PageRenderThread.h +++ b/reader/browser/PageRenderThread.h @@ -14,13 +14,24 @@ #include #include #include +#include class DocSheet; class BrowserPage; class SheetRenderer; class SideBarImageViewModel; +class QProcess; +// worker 线程约定:任务结构体中仅允许解引用 renderer(共享引用,生命周期安全)与 +// 入队时快照的值类型数据;sheet/page 等裸指针仅供主线程回调使用,worker 禁止访问。 struct DocPageNormalImageTask {//正常取图 + // ---- worker 线程只读数据 ---- + QSharedPointer renderer; //渲染器共享引用 + QString uuid; //入队时sheet唯一标识,worker据此跳过已销毁文档的任务(仅优化) + int pageIndex = -1; //入队时快照,worker不再解引用BrowserPage + qreal scaleFactor = 0.0; //入队时快照,兜底尺寸计算用 + QSizeF originSize; //入队时快照,兜底尺寸计算用 + // ---- 仅供主线程回调使用 ---- DocSheet *sheet = nullptr; BrowserPage *page = nullptr; int pixmapId = 0; //任务艾迪 @@ -29,6 +40,9 @@ struct DocPageNormalImageTask {//正常取图 }; struct DocPageSliceImageTask {//取切片 + QSharedPointer renderer; + QString uuid; + int pageIndex = -1; DocSheet *sheet = nullptr; BrowserPage *page = nullptr; int pixmapId = 0; //任务艾迪 @@ -37,6 +51,9 @@ struct DocPageSliceImageTask {//取切片 }; struct DocPageBigImageTask {//取大图 + QSharedPointer renderer; + QString uuid; + int pageIndex = -1; DocSheet *sheet = nullptr; BrowserPage *page = nullptr; int pixmapId = 0; //任务艾迪 @@ -45,26 +62,37 @@ struct DocPageBigImageTask {//取大图 }; struct DocPageWordTask {//取页码文字 + QSharedPointer renderer; + QString uuid; + int pageIndex = -1; DocSheet *sheet = nullptr; BrowserPage *page = nullptr; }; struct DocPageAnnotationTask {//取页码注释 + QSharedPointer renderer; + QString uuid; + int pageIndex = -1; DocSheet *sheet = nullptr; BrowserPage *page = nullptr; }; struct DocPageThumbnailTask {//缩略图 + QSharedPointer renderer; + QString uuid; DocSheet *sheet = nullptr; SideBarImageViewModel *model = nullptr; int index = -1; }; struct DocOpenTask {//打开文档 - DocSheet *sheet = nullptr; + DocSheet *sheet = nullptr; //仅供主线程回调使用 QString password; - SheetRenderer *renderer = nullptr; QString uuid; //排队时的sheet唯一标识,防止地址复用误判存活 + QString filePath; //入队时快照,worker不再访问sheet + QString convertedFileDir; + int fileType = 0; //Dr::FileType + QProcess *process = nullptr; //getDocument出参,由主线程回调写回sheet->m_process }; struct DocCloseTask {//关闭文档 @@ -89,6 +117,22 @@ class PageRenderThread : public QThread * @return 是否成功 */ static bool clearImageTasks(DocSheet *sheet, BrowserPage *page, int pixmapId = -1); + /** + * @brief clearAllTasksForSheet + * 清除指定 sheet 的所有待处理任务(图片/文字/注释/缩略图)。 + * 用于 DocSheet 析构前排空引用该 sheet 的后台任务,避免悬空访问。 + * @param sheet 目标 sheet 指针 + */ + static void clearAllTasksForSheet(DocSheet *sheet); + + /** + * @brief clearAllTasksForPage + * 排空队列中所有引用指定 page 的文字/注释任务(图像任务由 clearImageTasks 负责); + * 仅主线程调用(BrowserPage 析构时),与 worker 通过各队列互斥锁互斥 + * @param page 页面对象 + */ + static void clearAllTasksForPage(const BrowserPage *page); + /** * @brief appendTask diff --git a/reader/browser/SheetBrowser.cpp b/reader/browser/SheetBrowser.cpp index fd533784e..74adbbd78 100644 --- a/reader/browser/SheetBrowser.cpp +++ b/reader/browser/SheetBrowser.cpp @@ -983,7 +983,8 @@ void SheetBrowser::pinchTriggered(QPinchGesture *gesture) if (gesture->state() == Qt::GestureFinished) { // qCDebug(appLog) << "SheetBrowser::pinchTriggered() - Gesture finished"; this->setProperty("pinchgetsturing", false); - QTimer::singleShot(10, [this]() { + // 必须传 receiver=this:否则 browser 在定时器触发前被销毁时,lambda 仍会执行并写已死对象(UAF) + QTimer::singleShot(10, this, [this]() { //稍微延迟下,不然还是会引起mouse事件触发 m_startPinch = false; }); diff --git a/reader/sidebar/SideBarImageViewModel.cpp b/reader/sidebar/SideBarImageViewModel.cpp index b493450fa..f8ab61d49 100644 --- a/reader/sidebar/SideBarImageViewModel.cpp +++ b/reader/sidebar/SideBarImageViewModel.cpp @@ -129,6 +129,8 @@ QVariant SideBarImageViewModel::data(const QModelIndex &index, int role) const task.sheet = m_sheet; task.index = nRow; task.model = const_cast(this); + task.renderer = m_sheet ? m_sheet->rendererPtr() : nullptr; + task.uuid = m_sheet ? m_sheet->uuid() : QString(); PageRenderThread::appendTask(task); } @@ -198,6 +200,8 @@ void SideBarImageViewModel::onUpdateImage(int index) task.sheet = m_sheet; task.index = index; task.model = const_cast(this); + task.renderer = m_sheet ? m_sheet->rendererPtr() : nullptr; + task.uuid = m_sheet ? m_sheet->uuid() : QString(); PageRenderThread::appendTask(task); qCDebug(appLog) << "Updating image for index:" << index << "end"; } diff --git a/reader/uiframe/DocSheet.cpp b/reader/uiframe/DocSheet.cpp index 9a9ce3029..f0fd3c77b 100644 --- a/reader/uiframe/DocSheet.cpp +++ b/reader/uiframe/DocSheet.cpp @@ -72,8 +72,8 @@ DocSheet::DocSheet(const Dr::FileType &fileType, const QString &filePath, QWidg connect(m_searchTask, &PageSearchThread::finished, this, &DocSheet::onSearchFinished, Qt::QueuedConnection); connect(m_searchTask, &PageSearchThread::sigSearchResultNotEmpty, this, &DocSheet::onSearchResultNotEmpty, Qt::QueuedConnection); - m_renderer = new SheetRenderer(this); - connect(m_renderer, &SheetRenderer::sigOpened, this, &DocSheet::onOpened); + m_renderer = QSharedPointer::create(); + connect(m_renderer.data(), &SheetRenderer::sigOpened, this, &DocSheet::onOpened); m_browser = new SheetBrowser(this); m_browser->setMinimumWidth(481); @@ -157,12 +157,14 @@ DocSheet::~DocSheet() setAlive(false); + // 排空引用本 sheet 的所有待处理渲染任务(仅优化,避免为将死文档做无用渲染); + // 正确性由任务中捕获的renderer共享引用与uuid校验保证,不再依赖析构与线程间的时序 + PageRenderThread::clearAllTasksForSheet(this); + delete m_browser; delete m_sidebar; - delete m_renderer; - delete m_searchTask; delete m_encryPage; @@ -235,6 +237,17 @@ bool DocSheet::existSheet(DocSheet *sheet) return result; } +bool DocSheet::existSheetByUuid(const QString &uuid) +{ + g_lock.lockForRead(); + + bool result = !uuid.isEmpty() && g_uuidList.contains(uuid); + + g_lock.unlock(); + + return result; +} + DocSheet *DocSheet::getSheet(QString uuid) { qCDebug(appLog) << "getSheet"; @@ -279,7 +292,7 @@ bool DocSheet::openFileExec(const QString &password) qCDebug(appLog) << "Executing file open synchronously"; m_password = password; - bool result = m_renderer->openFileExec(password); + bool result = m_renderer->openFileExec(password, m_filePath, convertedFileDir(), m_uuid, static_cast(m_fileType), this); if (!result) { qCWarning(appLog) << "Failed to open file synchronously"; } @@ -293,7 +306,7 @@ void DocSheet::openFileAsync(const QString &password) m_password = password; qCInfo(appLog) << "添加异步打开任务..."; - m_renderer->openFileAsync(m_password); + m_renderer->openFileAsync(m_password, m_filePath, convertedFileDir(), m_uuid, static_cast(m_fileType), this); } void DocSheet::jumpToPage(int page) @@ -1880,7 +1893,7 @@ void DocSheet::onExtractPassword(const QString &password) qCDebug(appLog) << "Extracted password, attempting to open file"; m_password = password; - m_renderer->openFileAsync(m_password); + m_renderer->openFileAsync(m_password, m_filePath, convertedFileDir(), m_uuid, static_cast(m_fileType), this); } void DocSheet::saveCurrentViewState() @@ -2008,7 +2021,7 @@ float DocSheet::currentScrollPosition() const SheetRenderer *DocSheet::renderer() { // qCDebug(appLog) << "renderer"; - return m_renderer; + return m_renderer.data(); } void DocSheet::onPopPrintDialog() diff --git a/reader/uiframe/DocSheet.h b/reader/uiframe/DocSheet.h index c6b9f4f0f..c8244acaf 100644 --- a/reader/uiframe/DocSheet.h +++ b/reader/uiframe/DocSheet.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,14 @@ class DocSheet : public Dtk::Widget::DSplitter */ static bool existSheet(DocSheet *sheet); + /** + * @brief existSheetByUuid + * 进程是否存在该uuid对应的文档(worker线程安全,仅读锁+串查找) + * @param uuid + * @return + */ + static bool existSheetByUuid(const QString &uuid); + /** * @brief getSheet * 根据uuid返回sheet @@ -709,6 +718,12 @@ class DocSheet : public Dtk::Widget::DSplitter */ SheetRenderer *renderer(); + /** + * @brief rendererPtr + * 获取渲染器共享引用(入队时捕获,任务在worker线程使用期间生命周期安全) + */ + QSharedPointer rendererPtr() const { return m_renderer; } + QString uuid() const { return m_uuid; } public slots: @@ -960,7 +975,7 @@ private slots: SheetSidebar *m_sidebar = nullptr; //操作左侧ui SheetBrowser *m_browser = nullptr; //操作右侧ui - SheetRenderer *m_renderer = nullptr; //数据渲染器 + QSharedPointer m_renderer; //数据渲染器(共享所有权:worker持有引用期间不因析构悬空) QString m_filePath; QString m_password; diff --git a/reader/uiframe/SheetRenderer.cpp b/reader/uiframe/SheetRenderer.cpp index 08a5f891a..e285e838c 100644 --- a/reader/uiframe/SheetRenderer.cpp +++ b/reader/uiframe/SheetRenderer.cpp @@ -9,9 +9,11 @@ #include -SheetRenderer::SheetRenderer(DocSheet *parent) : QObject(parent), m_sheet(parent) +// 无父对象:由DocSheet以QSharedPointer持有,去除了对DocSheet的反向依赖, +// 使本对象可被worker线程通过共享引用安全使用 +SheetRenderer::SheetRenderer() : QObject(nullptr) { - qCDebug(appLog) << "Creating SheetRenderer for sheet:" << (parent ? parent->filePath() : "null"); + qCDebug(appLog) << "Creating SheetRenderer (parentless, shared ownership)"; } SheetRenderer::~SheetRenderer() @@ -27,14 +29,15 @@ SheetRenderer::~SheetRenderer() qCDebug(appLog) << "关闭文档任务已添加"; } -bool SheetRenderer::openFileExec(const QString &password) +bool SheetRenderer::openFileExec(const QString &password, const QString &filePath, + const QString &convertedFileDir, const QString &uuid, int fileType, DocSheet *sheet) { qCDebug(appLog) << "Executing synchronous file open"; QEventLoop loop; connect(this, &SheetRenderer::sigOpened, &loop, &QEventLoop::quit); - openFileAsync(password); + openFileAsync(password, filePath, convertedFileDir, uuid, fileType, sheet); loop.exec(); @@ -45,19 +48,23 @@ bool SheetRenderer::openFileExec(const QString &password) return success; } -void SheetRenderer::openFileAsync(const QString &password) +void SheetRenderer::openFileAsync(const QString &password, const QString &filePath, + const QString &convertedFileDir, const QString &uuid, int fileType, DocSheet *sheet) { qCDebug(appLog) << "Starting asynchronous file open"; DocOpenTask task; - task.sheet = m_sheet; - task.password = password; - task.renderer = this; + task.uuid = uuid; + + task.filePath = filePath; + + task.convertedFileDir = convertedFileDir; + + task.fileType = fileType; - if (nullptr != m_sheet) - task.uuid = m_sheet->uuid(); + task.sheet = sheet; //仅供主线程回调路由,worker线程不访问 PageRenderThread::appendTask(task); qCDebug(appLog) << "SheetRenderer::openFileAsync end"; diff --git a/reader/uiframe/SheetRenderer.h b/reader/uiframe/SheetRenderer.h index d920b8963..75ceaf85c 100644 --- a/reader/uiframe/SheetRenderer.h +++ b/reader/uiframe/SheetRenderer.h @@ -18,7 +18,7 @@ class SheetRenderer : public QObject { Q_OBJECT public: - explicit SheetRenderer(DocSheet *parent); + explicit SheetRenderer(); ~SheetRenderer(); @@ -26,16 +26,28 @@ class SheetRenderer : public QObject * @brief openFileExec * 阻塞式打开文档 * @param password + * @param filePath 文档路径(入队时快照,worker不再访问DocSheet) + * @param convertedFileDir 转换目录(入队时快照) + * @param uuid 所属DocSheet唯一标识 + * @param fileType 文档类型(入队时快照) + * @param sheet 所属DocSheet,仅供主线程回调路由使用 */ - bool openFileExec(const QString &password); + bool openFileExec(const QString &password, const QString &filePath, + const QString &convertedFileDir, const QString &uuid, int fileType, DocSheet *sheet); /** * @brief openFileAsync * 异步式打开文档,完成后会发出sigFileOpened * @param password 文档密码 + * @param filePath 文档路径(入队时快照,worker不再访问DocSheet) + * @param convertedFileDir 转换目录(入队时快照) + * @param uuid 所属DocSheet唯一标识(主线程回调路由用) + * @param fileType 文档类型(入队时快照) + * @param sheet 所属DocSheet,仅供主线程回调路由使用(worker禁止解引用) * @return */ - void openFileAsync(const QString &password); + void openFileAsync(const QString &password, const QString &filePath, + const QString &convertedFileDir, const QString &uuid, int fileType, DocSheet *sheet); /** * @brief opened @@ -222,7 +234,8 @@ class SheetRenderer : public QObject void sigOpened(deepin_reader::Document::Error error); private: - DocSheet *m_sheet = nullptr; + // 注意:无m_sheet反向指针、无QObject父对象;由DocSheet以QSharedPointer持有, + // worker线程持有共享引用期间生命周期安全,可跨线程安全使用 deepin_reader::Document::Error m_error = deepin_reader::Document::NoError; bool m_pageLabelLoaded = false; //是否已经加载page label QMap m_lable2Page; // 文档下标页码 diff --git a/tests/browser/ut_browserpage.cpp b/tests/browser/ut_browserpage.cpp index f098ff600..f5171d532 100644 --- a/tests/browser/ut_browserpage.cpp +++ b/tests/browser/ut_browserpage.cpp @@ -538,6 +538,7 @@ TEST_F(TestBrowserPage, UT_BrowserPage_addHighlightAnnotation_001) s.set(ADDR(SheetRenderer, getWords), getWords_stub); s.set(ADDR(QGraphicsItem, isSelected), isSelected_stub); s.set(ADDR(SheetRenderer, addHighlightAnnotation), addHighlightAnnotation_stub); + s.set(ADDR(BrowserPage, renderRect), renderRect_stub); BrowserWord *w1 = new BrowserWord(nullptr, Word("first", QRectF(0, 0, 20, 10))); BrowserWord *w2 = new BrowserWord(nullptr, Word("second", QRectF(20, 0, 40, 10))); m_tester->m_words.append(w1); diff --git a/tests/browser/ut_pagerenderthread.cpp b/tests/browser/ut_pagerenderthread.cpp index ee5f853f6..8f0294f75 100644 --- a/tests/browser/ut_pagerenderthread.cpp +++ b/tests/browser/ut_pagerenderthread.cpp @@ -101,7 +101,7 @@ static QString uuid_stub() // DocSheet::renderer 档:占位渲染器(handleOpened已stub)。堆分配不释放,避免静态对象在 main 返回后析构 static SheetRenderer *renderer_stub() { - static SheetRenderer *dummy = new SheetRenderer(nullptr); + static SheetRenderer *dummy = new SheetRenderer(); return dummy; } @@ -216,7 +216,6 @@ TEST_F(TestPageRenderThread, UT_PageRenderThread_onDocOpenTask_001) { DocOpenTask task; task.sheet = nullptr; - task.renderer = nullptr; QList pages; m_tester->onDocOpenTask(task, deepin_reader::Document::NoError, nullptr, pages); SUCCEED(); @@ -340,9 +339,12 @@ TEST_F(TestPageRenderThread, UT_PageRenderThread_onDocOpenTask_002) s.set(ADDR(DocSheet, renderer), renderer_stub); s.set(ADDR(SheetRenderer, handleOpened), handleOpened_stub); + // onDocOpenTask会直接写 sheet->m_process(非函数调用,stub拦截不到), + // 因此用足够大的可写静态存储充当假sheet,而非非法地址 + static unsigned char fake_sheet_storage[8192] = {}; + DocOpenTask task; - task.sheet = reinterpret_cast(0x1); //成员调用均已被stub - task.renderer = nullptr; + task.sheet = reinterpret_cast(fake_sheet_storage); //成员调用均已被stub task.uuid = "ut-sheet-uuid"; //与uuid_stub一致,校验通过 QList pages; m_tester->onDocOpenTask(task, deepin_reader::Document::NoError, nullptr, pages); @@ -361,7 +363,6 @@ TEST_F(TestPageRenderThread, UT_PageRenderThread_onDocOpenTask_003) DocOpenTask task; task.sheet = reinterpret_cast(0x1); - task.renderer = reinterpret_cast(0x1); //悬空,不应被解引用 task.uuid = "stale-uuid"; //与uuid_stub不一致 QList pages; m_tester->onDocOpenTask(task, deepin_reader::Document::NoError, nullptr, pages); @@ -466,7 +467,6 @@ TEST_F(TestPageRenderThread, UT_PageRenderThread_appendTask_Open) DocOpenTask task; task.sheet = nullptr; - task.renderer = nullptr; PageRenderThread::appendTask(task); EXPECT_FALSE(m_tester->m_openTasks.isEmpty()); m_tester->m_openTasks.clear(); diff --git a/tests/browser/ut_sheetbrowser.cpp b/tests/browser/ut_sheetbrowser.cpp index 7ca19eb62..f6a957fa4 100644 --- a/tests/browser/ut_sheetbrowser.cpp +++ b/tests/browser/ut_sheetbrowser.cpp @@ -208,8 +208,10 @@ Qt::MouseEventSource source_stub2() static BrowserPage *g_pBrowserPage2 = nullptr; BrowserPage *getBrowserPageForPoint_stub(QPointF &) { - DocSheet sheet(Dr::FileType::PDF, "1.pdf", nullptr); - g_pBrowserPage2 = new BrowserPage(nullptr, 0, &sheet); + // 用堆上静态sheet而非栈对象:返回的BrowserPage持有其指针, + // 若绑定栈对象会在函数返回后悬空(loadWords等路径解引用即崩) + static DocSheet *s_stubSheet = new DocSheet(Dr::FileType::PDF, "1.pdf", nullptr); + g_pBrowserPage2 = new BrowserPage(nullptr, 0, s_stubSheet); g_pBrowserPage2->m_index = 3; return g_pBrowserPage2; } diff --git a/tests/uiframe/ut_docsheet.cpp b/tests/uiframe/ut_docsheet.cpp index e586cf728..9049bd468 100644 --- a/tests/uiframe/ut_docsheet.cpp +++ b/tests/uiframe/ut_docsheet.cpp @@ -73,13 +73,13 @@ QImage firstThumbnail_stub(const QString &) return QImage(100, 100, QImage::Format_ARGB32); } -bool openFileExec_stub(const QString &) +bool openFileExec_stub(const QString &, const QString &, const QString &, const QString &, int, DocSheet *) { g_funcName = __FUNCTION__; return true; } -void openFileAsync_stub(const QString &) +void openFileAsync_stub(const QString &, const QString &, const QString &, const QString &, int, DocSheet *) { g_funcName = __FUNCTION__; } @@ -1493,7 +1493,7 @@ QSizeF getPageSize_stub2(int) return QSizeF(100.0, 200.0); } -void openFileAsync_stub2(const QString &) +void openFileAsync_stub2(const QString &, const QString &, const QString &, const QString &, int, DocSheet *) { g_funcName = __FUNCTION__; } diff --git a/tests/uiframe/ut_sheetrenderer.cpp b/tests/uiframe/ut_sheetrenderer.cpp index 8ab8f178e..a3fb987dd 100644 --- a/tests/uiframe/ut_sheetrenderer.cpp +++ b/tests/uiframe/ut_sheetrenderer.cpp @@ -56,7 +56,7 @@ void TestSheetRenderer::SetUp() QString strPath = UTSOURCEDIR; strPath += "/files/1.pdf"; m_sheet = new DocSheet(Dr::FileType::PDF, strPath, m_parent); - m_tester = m_sheet->m_renderer; + m_tester = m_sheet->m_renderer.data(); ASSERT_NE(m_tester, nullptr); } @@ -275,7 +275,7 @@ TEST_F(TestSheetRenderer, testOpenFileAsync) // otherwise a queued sigDocOpenTask referencing this renderer outlives the test // and gets delivered to freed memory later. QSignalSpy spy(m_tester, &SheetRenderer::sigOpened); - m_tester->openFileAsync("test"); + m_tester->openFileAsync("test", UTSOURCEDIR "/files/normal.pdf", QString(), m_sheet->uuid(), static_cast(Dr::PDF), m_sheet); QTRY_COMPARE_WITH_TIMEOUT(spy.count(), 1, 30000); SUCCEED(); } @@ -283,6 +283,6 @@ TEST_F(TestSheetRenderer, testOpenFileAsync) TEST_F(TestSheetRenderer, testOpenFileExec) { // Let openFileExec wait for the REAL sigOpened of the actual open task. - bool result = m_tester->openFileExec("test"); + bool result = m_tester->openFileExec("test", UTSOURCEDIR "/files/normal.pdf", QString(), m_sheet->uuid(), static_cast(Dr::PDF), m_sheet); EXPECT_TRUE(result); } diff --git a/tests/ut_mainwindow.cpp b/tests/ut_mainwindow.cpp index 456473ea9..7beade3d4 100644 --- a/tests/ut_mainwindow.cpp +++ b/tests/ut_mainwindow.cpp @@ -21,7 +21,7 @@ #include "ut_compat.h" #include -static void openFileAsync_stub(const QString &) +static void openFileAsync_stub(const QString &, const QString &, const QString &, const QString &, int, DocSheet *) { return; }