资讯动态

uni-app Android 平台内存泄漏排查与修复实战指南:基于 LeakCanary 自动检测的完整方案

发布时间:2026/9/19 23:26:53 来源:尧图企业网站定制
示例工程前端移动开发跨平台【免费下载链接】uni-appA cross-platform framework using Vue.js项目地址https://gitcode.com/gh_mirrors/un/uni-app点击查看免费下载uni-app 与 uvue 页面在 Android 平台长期运行时的内存占用问题往往源于 Activity、UI 元素或响应式数据被全局引用而无法被 GC 回收。HBuilderX 4.81 版本真机运行已集成 LeakCanary 自动检测内存泄漏本指南将带你理解内存泄漏的本质、读懂 LeakCanary 的引用链日志并通过 UTS 插件与 uvue 页面中的 4 个真实案例掌握从定位到修复的完整排查方法。内存泄漏概念与危害内存泄漏Memory Leak是指应用在申请内存后无法释放已申请的内存空间导致系统无法再次将该内存分配给其他应用使用。在 Android 应用中内存泄漏会导致应用内存占用不断增长随着时间推移应用占用的内存越来越多性能下降可用内存减少导致频繁的垃圾回收GC影响应用流畅度OOMOut Of Memory错误严重时会导致应用崩溃系统卡顿影响整个系统的性能表现内存泄漏的本质在 Java/Kotlin 中当一个对象不再被需要时垃圾回收器GC应该能够回收它占用的内存。但如果该对象仍然被其他对象持有引用GC 就无法回收它从而造成内存泄漏。简单来说只要有一条从 GC Root 出发、能够到达某个已失效对象的强引用链存在该对象及其关联对象就永远无法被回收。常见内存泄漏类型在 uni-app / uvue / UTS 开发中常见的 Android 内存泄漏类型包括静态变量持有 Activity/Context 引用UTS 插件的应用级全局变量模块顶层变量生命周期与应用相同一旦存入 Activity 便无法释放全局集合对象持有 UI 元素引用将 UniElement、View 等 UI 对象推入全局数组响应式数据Vue data被全局引用Vue 响应式数组被外部全局集合持有形成双层引用监听器、回调未及时注销全局事件监听回调捕获页面this形成无法切断的强引用链资源文件、网络连接未关闭HBuilderX 自动检测内存泄漏Android 平台的标准基座及自定义调试基座已集成内存泄漏检测工具 LeakCanarySquare 公司开源的 Android 内存泄漏检测库。使用 HBuilderX 4.81 版本真机运行时会基于 LeakCanary 自动检测内存泄漏无需开发者手动集成任何依赖。LeakCanary 的核心能力自动检测无需手动触发自动监控 Activity、Fragment 等组件的生命周期详细报告提供完整的引用链路径帮助定位泄漏源头实时通知发现泄漏时立即在 HBuilderX 控制台输出详细信息如果发现内存泄漏HBuilderX 控制台中会显示内存泄漏信息包括详细的引用链分析日志。开发者只需在真机运行过程中正常操作页面进入、退出LeakCanary 便会持续监控已销毁的 Activity 是否仍被引用一旦判定泄漏即输出日志。如何解决内存泄漏问题日志结构解析LeakCanary 输出的内存泄漏日志包含以下关键信息头部信息[retained bytes] bytes retained by leaking objectsretained bytes泄漏对象占用的内存大小该值越大说明泄漏越严重引用链分析┬─── │ GC Root: Input or output parameters in native code │ ├─ [引用链节点] │ Leaking: YES/NO (原因说明) │ ↓ 引用字段名 ╰→ [最终泄漏对象] Leaking: YES (泄漏原因)关键字段含义GC Root垃圾回收的根节点引用链的起点GC 从它出发判断可达性Leaking: YES/NO该对象是否泄漏↓引用方向表示从上一个对象指向下一个对象Retaining X kB in Y objects该对象持有的内存X kB和对象数量YmDestroyed trueActivity 已被销毁已执行 onDestroy但仍被引用泄漏日志分析技巧在分析内存泄漏日志时可以运用以下技巧快速定位问题关注包名定位泄漏源UTS 插件泄漏如果引用链中出现uts.sdk.modules开头的包名通常表示泄漏发生在某个 UTS 插件中。应重点排查该插件的静态变量模块级顶层变量、全局集合或未注销的监听器。在本仓库中所有官方 UTS 插件均位于 src/uni_modules例如uni-theme、uni-getSystemInfo等其编译产物在 Android 上的包名即为uts.sdk.modules.*形式排查时可在对应的插件源码目录中定位问题变量。uvue 页面泄漏如果引用链中出现uni.${appid}如uni.UNI511CEBA开头的类名这通常指向一个 uvue 页面。泄漏很可能与该页面的全局变量、data中被外部引用的数据或未在onUnload生命周期中清理的资源有关。页面源码位于项目的pages目录本仓库示例为 src/pages编译后的页面类即命名为此形式。识别关键引用字段LeakCanary 日志中带有下划波浪线~的变量例如↓ static IndexKt.leakActivitys下的~~~~~~~~~~~~~是核心线索。它明确指出了是哪个字段持有了对下一个对象的引用。沿着这些带波浪线的字段追溯就能完整地理解整个引用链找到泄漏的根源。内存泄漏问题案例分析案例 1UTS 插件静态变量持有 Activity 引用问题代码//UTS插件中 import Activity from android.app.Activity; const leakActivitys: Activity[] [] // 应用级全局变量 export function leakActivity() { const topActivity UTSAndroid.getTopPageActivity() if (topActivity ! null) { leakActivitys.push(topActivity) } }页面调用import { leakActivity } from /uni_modules/leak-leakcanary onReady(() { leakActivity() })泄漏日志分析┬─── │ GC Root: Input or output parameters in native code │ ├─ dalvik.system.PathClassLoader instance │ Leaking: NO (IndexKt↓ is not leaking and a ClassLoader is never leaking) │ ↓ ClassLoader.runtimeInternalObjects ├─ java.lang.Object[] array │ Leaking: NO (IndexKt↓ is not leaking) │ ↓ Object[890] ├─ uts.sdk.modules.leakLeakcanary.IndexKt class // UTS插件类 │ Leaking: NO (a class is never leaking) │ ↓ static IndexKt.leakActivitys // 问题根源静态变量 │ ~~~~~~~~~~~~~ ├─ io.dcloud.uts.UTSArray instance // UTS数组 │ Leaking: UNKNOWN │ Retaining 212.3 kB in 3987 objects │ ↓ ArrayList.elementData │ ~~~~~~~~~~~ ├─ java.lang.Object[] array │ Leaking: UNKNOWN │ Retaining 212.3 kB in 3986 objects │ ↓ Object[0] │ ~~~ ╰→ io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity instance Leaking: YES (ObjectWatcher was watching this because io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity received Activity#onDestroy() callback and Activity#mDestroyed is true) // Activity已销毁但仍被引用 Retaining 52.9 kB in 994 objects key 2abb5312-246c-4978-8f88-fd1a4aad7403 watchDurationMillis 12084 retainedDurationMillis 7084泄漏原因分析静态变量持有引用static IndexKt.leakActivitys是静态变量生命周期与应用相同Activity 已销毁Activity#mDestroyed is true表明 Activity 已经调用了 onDestroy()引用链完整从 GC Root → 类加载器 → UTS 插件类 → 静态数组 → Activity内存占用泄漏的 Activity 持有 52.9 kB 内存和 994 个对象补充说明UTSAndroid.getTopPageActivity()是 UTS 插件中获取当前顶层页面 Activity 的常用 API。在本仓库的多个官方 UTS 插件如 uni-getSystemInfo/utssdk/app-android/index.uts、uni-payment-wxpay/utssdk/app-android/src/Wxpay.uts中都有类似用法它返回的正是io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity这类页面 Activity。拿到 Activity 引用后务必只做临时使用严禁存入模块级静态变量或全局集合。解决方案UTS 模块顶层变量在编译到 Android 后即成为静态字段因此必须为全局集合补充移除与清理方法并在页面销毁时调用import Activity from android.app.Activity; const leakActivitys: Activity[] [] // 应用级全局变量 export function leakActivity() { const topActivity UTSAndroid.getTopPageActivity() if (topActivity ! null) { leakActivitys.push(topActivity) } } // 添加清理方法 - 移除特定Activity export function removeActivity(activity: Activity) { const index leakActivitys.indexOf(activity) if (index -1) { leakActivitys.splice(index, 1) } } // 页面销毁时移除当前Activity export function removeCurrentActivity() { const topActivity UTSAndroid.getTopPageActivity() if (topActivity ! null) { removeActivity(topActivity) } }页面中正确使用import { leakActivity, removeCurrentActivity } from /uni_modules/leak-leakcanary onReady(() { leakActivity() }) onUnload(() { // 页面销毁时清理引用 removeCurrentActivity() })案例 2全局变量持有 UniElement 引用问题代码//uvue中 let globalElement: UniElement[] [] // 应用级全局变量 const element refUniElement | null(null) onReady(() { element.value uni.getElementById(xx) const currentElement element.value if (currentElement ! null) { globalElement.push(currentElement) } })泄漏日志分析┬─── │ GC Root: Input or output parameters in native code │ ├─ dalvik.system.PathClassLoader instance │ Leaking: NO (IndexKt↓ is not leaking and a ClassLoader is never leaking) │ ↓ ClassLoader.runtimeInternalObjects ├─ java.lang.Object[] array │ Leaking: NO (IndexKt↓ is not leaking) │ ↓ Object[1658] ├─ uni.UNI511CEBA.IndexKt class // 页面编译后的类 │ Leaking: NO (a class is never leaking) │ ↓ static IndexKt.globalElement // 问题全局变量持有元素 │ ~~~~~~~~~~~~~ ├─ io.dcloud.uts.UTSArray instance │ Leaking: UNKNOWN │ Retaining 632.6 kB in 10655 objects // 大量内存被占用 │ ↓ ArrayList.elementData │ ~~~~~~~~~~~ ├─ java.lang.Object[] array │ Leaking: UNKNOWN │ Retaining 632.5 kB in 10654 objects │ ↓ Object[0] │ ~~~ ├─ io.dcloud.uniapp.runtime.UniTextElementImpl instance // UniElement实例 │ Leaking: UNKNOWN │ Retaining 105.6 kB in 1778 objects │ ↓ PropsNode.pageNode // 元素持有页面节点 │ ~~~~~~~~ ├─ io.dcloud.uniapp.dom.node.PageNode instance │ Leaking: UNKNOWN │ Retaining 102.9 kB in 1712 objects │ ↓ PageNode.frameView // 页面节点持有框架视图 │ ~~~~~~~~~ ├─ io.dcloud.uniapp.appframe.ui.PageFrameView instance │ Leaking: YES (View.mContext references a destroyed activity) // 视图持有已销毁的Activity │ Retaining 2.1 kB in 42 objects │ View not part of a window view hierarchy │ View.mAttachInfo is null (view detached) │ View.mID R.id.null │ View.mWindowAttachCount 1 │ mContext instance of io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity with mDestroyed true │ ↓ View.mContext ╰→ io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity instance Leaking: YES (ObjectWatcher was watching this because io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity received Activity#onDestroy() callback and Activity#mDestroyed is true) Retaining 48.3 kB in 971 objects泄漏原因分析全局变量持有 UI 元素static IndexKt.globalElement全局数组持有 UniElementUI 元素持有页面引用UniElement → PageNode → PageFrameView → Activity视图与 Activity 绑定View.mContext references a destroyed activity引用链路径全局数组 → UI 元素 → 页面视图 → 已销毁的 Activity补充说明uni.getElementById()返回的UniElement是 uni-app 的 DOM 元素抽象其实现类如日志中的UniTextElementImpl通过PropsNode.pageNode关联到整棵页面节点树最终经PageNode.frameView关联到承载页面的 Activity。getElementById的 API 说明可参考 docs/api/get-element-by-id.md它适合在页面生命周期内临时操作元素不适合跨页面长期保存。解决方案let globalElement: UniElement[] [] // 应用级全局变量 const element refUniElement | null(null) onReady(() { element.value uni.getElementById(xx) const currentElement element.value if (currentElement ! null) { globalElement.push(currentElement) } }) onUnload(() { // 页面销毁时移除特定元素引用 const currentElement element.value if (currentElement ! null) { const index globalElement.indexOf(currentElement) if (index -1) { globalElement.splice(index, 1) } element.value null } })案例 3响应式数据Vue data被全局引用问题代码let globalElement: UniElement[][] [] // 应用级全局变量 export default { data() { return { elements: [] as UniElement[] // Vue响应式数组 } }, onReady(){ const element1 uni.getElementById(xx) const element2 uni.getElementById(yy) if (element1 ! null) { this.elements.push(element1) } if (element2 ! null) { this.elements.push(element2) } globalElement.push(this.elements) // 将响应式数组推入全局数组 }, methods: {} }泄漏日志分析┬─── │ GC Root: Input or output parameters in native code │ ├─ dalvik.system.PathClassLoader instance │ Leaking: NO (IndexKt↓ is not leaking and A ClassLoader is never leaking) │ ↓ ClassLoader.runtimeInternalObjects ├─ java.lang.Object[] array │ Leaking: NO (IndexKt↓ is not leaking) │ ↓ Object[2146] ├─ uni.UNI511CEBA.IndexKt class │ Leaking: NO (a class is never leaking) │ ↓ static IndexKt.globalElement // 全局变量 │ ~~~~~~~~~~~~~ ├─ io.dcloud.uts.UTSArray instance │ Leaking: UNKNOWN │ Retaining 541.6 kB in 9236 objects │ ↓ ArrayList.elementData │ ~~~~~~~~~~~ ├─ java.lang.Object[] array │ Leaking: UNKNOWN │ Retaining 541.5 kB in 9235 objects │ ↓ Object[0] │ ~~~ ├─ io.dcloud.uniapp.vue.UTSReactiveArray instance // 关键Vue响应式数组 │ Leaking: UNKNOWN │ Retaining 108.1 kB in 1844 objects │ ↓ UTSReactiveArray.__v_raw // 响应式数组的原始数据 │ ~~~~~~~ ├─ io.dcloud.uts.UTSArray instance // 原始数组 │ Leaking: UNKNOWN │ Retaining 108.1 kB in 1843 objects │ ↓ ArrayList.elementData │ ~~~~~~~~~~~ ├─ java.lang.Object[] array │ Leaking: UNKNOWN │ Retaining 108.1 kB in 1842 objects │ ↓ Object[0] │ ~~~ ├─ io.dcloud.uniapp.runtime.UniTextElementImpl instance │ Leaking: UNKNOWN │ Retaining 2.7 kB in 66 objects │ ↓ PropsNode.pageNode │ ~~~~~~~~ ├─ io.dcloud.uniapp.dom.node.PageNode instance │ Leaking: UNKNOWN │ Retaining 102.6 kB in 1709 objects │ ↓ PageNode.frameView │ ~~~~~~~~~ ├─ io.dcloud.uniapp.appframe.ui.PageFrameView instance │ Leaking: YES (View.mContext references a destroyed activity) │ Retaining 2.0 kB in 41 objects │ View not part of a window view hierarchy │ View.mAttachInfo is null (view detached) │ View.mID R.id.null │ View.mWindowAttachCount 1 │ mContext instance of io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity with mDestroyed true │ ↓ View.mContext ╰→ io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity instance Leaking: YES (ObjectWatcher was watching this because io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity received Activity#onDestroy() callback and Activity#mDestroyed is true) Retaining 48.1 kB in 969 objects泄漏原因分析响应式数组被全局引用UTSReactiveArray是 Vue 的响应式数组双重引用结构响应式数组包含原始数组__v_raw形成双重持有复杂引用链全局数组 → 响应式数组 → 原始数组 → UI 元素 → ActivityVue 内部机制响应式数组内部持有原始数据的引用增加了泄漏风险解决方案关键点不要将响应式数组直接推入全局集合应推入其副本并在onUnload中同步清理。let globalElement: UniElement[][] [] // 应用级全局变量 export default { data() { return { elements: [] as UniElement[] } }, onReady(){ const element1 uni.getElementById(xx) const element2 uni.getElementById(yy) if (element1 ! null) { this.elements.push(element1) } if (element2 ! null) { this.elements.push(element2) } // 避免直接推入响应式数组使用副本 globalElement.push([...this.elements]) }, onUnload() { // 找到并移除当前页面的elements数组 const index globalElement.indexOf(this.elements) if (index -1) { globalElement.splice(index, 1) } // 清空当前页面的elements数组 this.elements.splice(0, this.elements.length) }, methods: {} }案例 4全局监听回调持有页面this引用问题代码//uvue中 export default { data() { return {} }, onReady() { // 注册一个全局应用主题变化监听器 // 回调函数隐式地捕获了当前页面的this实例 uni.onAppThemeChange((){ console.log(Theme changed, page this:, this); }) }, methods: {} }泄漏日志分析┬─── │ GC Root: Thread object │ ├─ android.os.HandlerThread instance │ Leaking: NO (PathClassLoader↓ is not leaking) │ Thread name: LeakCanary-Heap-Dump │ ↓ Thread.contextClassLoader ├─ dalvik.system.PathClassLoader instance │ Leaking: NO (UniAppThemeManager↓ is not leaking and A ClassLoader is never leaking) │ ↓ ClassLoader.runtimeInternalObjects ├─ java.lang.Object[] array │ Leaking: NO (UniAppThemeManager↓ is not leaking) │ ↓ Object[753] ├─ io.dcloud.uniapp.appframe.UniAppThemeManager class │ Leaking: NO (a class is never leaking) │ ↓ static UniAppThemeManager.appThemeChangeListeners // 问题根源静态的监听器集合 │ ~~~~~~~~~~~~~~~~~~~~~~~ ├─ java.util.concurrent.ConcurrentHashMap instance │ Leaking: UNKNOWN │ Retaining 333.6 kB in 5195 objects │ ↓ ConcurrentHashMap[instance 322967640 of java.lang.Integer] │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ├─ uts.sdk.modules.DCloudUniTheme.IndexKt$onAppThemeChange$1$1 instance │ Leaking: UNKNOWN │ Retaining 111.0 kB in 1727 objects │ Anonymous subclass of kotlin.jvm.internal.Lambda │ ↓ IndexKt$onAppThemeChange$1$1.$callback │ ~~~~~~~~~ ├─ uni.UNI511CEBA.GenPagesFourthFourth$1$1 instance // 页面生成的回调函数实例 │ Leaking: UNKNOWN │ Retaining 111.0 kB in 1726 objects │ Anonymous subclass of kotlin.jvm.internal.Lambda │ ↓ GenPagesFourthFourth$1$1.this$0 // 关键回调函数持有页面的this引用 │ ~~~~~~ ├─ uni.UNI511CEBA.GenPagesFourthFourth instance // 页面实例 │ Leaking: UNKNOWN │ Retaining 111.0 kB in 1725 objects │ ↓ Page.$nativePage │ ~~~~~~~~~~~ ├─ io.dcloud.uniapp.appframe.UniNativePageImpl instance │ Leaking: UNKNOWN │ Retaining 764 B in 23 objects │ ↓ UniNativePageImpl.container │ ~~~~~~~~~ ├─ io.dcloud.uniapp.appframe.ui.PageFrameView instance │ Leaking: YES (View.mContext references a destroyed activity) │ ↓ View.mContext ╰→ io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity instance Leaking: YES (ObjectWatcher was watching this because io.dcloud.uniapp.appframe.activity.UniPortraitPageActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)泄漏原因分析静态监听器集合uni.onAppThemeChange将回调函数存储在一个静态的、全局的UniAppThemeManager.appThemeChangeListeners集合中。这个集合的生命周期与整个应用相同。回调函数捕获this日志中的GenPagesFourthFourth$1$1.this$0字段明确指出传递给监听器的回调函数Lambda捕获了其所在的页面实例的this引用。形成强引用链一条无法被打破的强引用链形成静态管理器→静态监听器集合→回调函数实例→页面实例(this)→Activity。页面无法释放当页面关闭并销毁时由于这个全局的引用链依然存在垃圾回收器无法回收页面实例及其关联的 Activity导致内存泄漏。解决方案对于这种基于回调的监听器必须在页面销毁时手动注销监听切断引用链。export default { data() { return { callbackId: -1 } }, onReady() { // 注册监听 this.callbackId uni.onAppThemeChange(() { console.log(Theme changed, page this:, this); }) }, onUnload() { // 页面销毁时必须注销监听 uni.offAppThemeChange(this.callbackId) }, methods: {} }源码级佐证推荐范式本仓库的官方示例页 src/pages/API/theme-change/theme-change.uvue 正是采用这一规范写法——在onReady中通过uni.onAppThemeChange(callback)拿到监听 id 存入变量并在onUnload中调用uni.offAppThemeChange(appThemeChangeId.value)注销见该文件 onReady/onUnload 生命周期。同时可参考 docs/api/theme-change.md 了解该 API 的完整入参与返回值说明。在底层实现上官方uni-theme插件的 Android 实现src/uni_modules/uni-theme/utssdk/app-android/index.uts将onAppThemeChange转发给UTSAndroid.onAppThemeChanged并返回监听 idoffAppThemeChange则调用UTSAndroid.offAppThemeChanged(id)按 id 注销。这一注册返回 id、注销传 id的约定意味着只要在页面销毁时按 id 注销静态监听器集合中的回调即可被移除引用链随之切断——这正是日志中static UniAppThemeManager.appThemeChangeListeners指向的静态集合也是案例 4 泄漏能否被修复的关键所在。类似地onOsThemeChange/offOsThemeChange也遵循同一模式示例页中同时成对出现。内存泄漏问题实践总结1. 识别泄漏模式通过日志关键信息快速识别泄漏类型看到static字段静态变量泄漏模块顶层变量/全局集合看到UTSReactiveArrayVue 响应式数据泄漏看到static ...Listeners且引用链经过 Lambda 回调实例全局监听器未注销看到View.mContext references a destroyed activityUI 视图链持有已销毁 Activity通常是上面几种模式的最终表现2. 生命周期管理onReady中添加的引用在onUnload中移除凡是在页面生命周期内获取的 Activity、UniElement、监听器 id都要在页面销毁时成对清理——这正是案例 14 所有解决方案的共同原则。3. 避免常见陷阱不在全局变量中直接持有 UI 元素或 Activity注意 Vue 响应式数据的特殊性__v_raw双层结构避免将其推入全局集合确需保存时使用副本及时清理监听器、定时器等长生命周期资源注册与注销必须成对出现排查时优先看带波浪线~标注的引用字段并借助包名uts.sdk.modules对应 UTS 插件、uni.${appid}对应 uvue 页面快速锁定泄漏代码所在模块掌握以上日志解析方法与生命周期管理原则后配合 HBuilderX 真机运行集成的 LeakCanary 自动检测即可在开发阶段及时发现并修复 Android 平台的内存泄漏问题避免应用在上线后出现内存持续增长、卡顿乃至 OOM 崩溃。赞分享示例工程前端移动开发跨平台【免费下载链接】uni-appA cross-platform framework using Vue.js项目地址https://gitcode.com/gh_mirrors/un/uni-app点击查看免费下载相关推荐Android应用内存泄漏终极排查指南LeakCanary检测与修复方案Android应用内存泄漏终极排查指南LeakCanary检测与修复方案 Android应用开发中内存泄漏是导致应用卡顿、崩溃的常见元凶。本文将为你提供一套移动开发Obsidian-i18n终极指南3种智能翻译模式让英文插件秒变中文界面Obsidian i18n终极指南3种智能翻译模式让英文插件秒变中文界面 还在为Obsidian插件全是英文而头疼吗想要一键将你常用的插件变成熟悉的中文界面AI 应用开发工具大模型Unlicense深度解析动态 instrumentation 技术在 Themida 脱壳中的应用Unlicense深度解析动态 instrumentation 技术在 Themida 脱壳中的应用 Unlicense 是一款强大的 Python 工具专逆向工程应用安全创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价