OpenHarmony内存泄漏指南 - 解决问题(综合)

本系列文章旨在提供定位与解决OpenHarmony应用与子系统内存泄露的常见手段与思路,将会分成几个部分来讲解。首先我们需要掌握发现内存泄漏问题的工具与方法,以及判断是否可能存在泄漏。接着需要掌握定位泄漏问题的工具,以及抓取trace、分析trace,以确定是否有泄漏问题。如果发现问题的场景过于复杂,需要通过分解问题来简化场景。最后根据trace来找到问题代码并尝试解决。

本篇提供了一些3.2 release内存泄漏的真实案例,旨在提供常见泄漏原因的解决办法。常见的泄漏问题主要分为Native代码泄漏、NAPI代码泄漏、JavaScript代码泄漏以及综合类问题。下面是综合类的案例,一般都是需要结合native、napi代码,与对应的JavaScript对象一起分析的类型。

OnJsRemoteRequest

该案例,是在进行rpc通信时,服务端的占用内容会不断增大,JavaScript代码如下:

class ServiceImpl extends rpc.RemoteObject {constructor() {super('test');}onRemoteMessageRequest(code, data, reply, option) {reply.writeString('Hello World');return true;}
}

代码中,仅仅只是像reply写入了一个字符串。trace显示如下函数中存在泄漏:

int NAPIRemoteObject::OnJsRemoteRequest(CallbackParam *jsParam)
{uv_loop_s *loop = nullptr;napi_get_uv_event_loop(env_, &loop);uv_work_t *work = new(std::nothrow) uv_work_t;work->data = reinterpret_cast<void *>(jsParam);ZLOGI(LOG_LABEL, "start nv queue work loop");uv_queue_work(loop, work, [](uv_work_t *work) {}, [](uv_work_t *work, int status) {ZLOGI(LOG_LABEL, "enter thread pool");CallbackParam *param = reinterpret_cast<CallbackParam *>(work->data);napi_value onRemoteRequest = nullptr;napi_value thisVar = nullptr;napi_get_reference_value(param->env, param->thisVarRef, &thisVar);napi_get_named_property(param->env, thisVar, "onRemoteMessageRequest", &onRemoteRequest);napi_valuetype type = napi_undefined;napi_typeof(param->env, onRemoteRequest, &type);bool isOnRemoteMessageRequest = true;napi_value jsCode;napi_create_uint32(param->env, param->code, &jsCode);napi_value global = nullptr;napi_get_global(param->env, &global);napi_value jsOptionConstructor = nullptr;napi_get_named_property(param->env, global, "IPCOptionConstructor_", &jsOptionConstructor);napi_value jsOption;size_t argc = 2;napi_value flags = nullptr;napi_create_int32(param->env, param->option->GetFlags(), &flags);napi_value waittime = nullptr;napi_create_int32(param->env, param->option->GetWaitTime(), &waittime);napi_value argv[2] = { flags, waittime };napi_new_instance(param->env, jsOptionConstructor, argc, argv, &jsOption);napi_value jsParcelConstructor = nullptr;if (isOnRemoteMessageRequest) {napi_get_named_property(param->env, global, "IPCSequenceConstructor_", &jsParcelConstructor);} else {napi_get_named_property(param->env, global, "IPCParcelConstructor_", &jsParcelConstructor);}napi_value jsData;napi_value dataParcel;napi_create_object(param->env, &dataParcel);napi_wrap(param->env, dataParcel, param->data,[](napi_env env, void *data, void *hint) {}, nullptr, nullptr);size_t argc3 = 1;napi_value argv3[1] = { dataParcel };napi_new_instance(param->env, jsParcelConstructor, argc3, argv3, &jsData);napi_value jsReply;napi_value replyParcel;napi_create_object(param->env, &replyParcel);napi_wrap(param->env, replyParcel, param->reply,[](napi_env env, void *data, void *hint) {}, nullptr, nullptr);size_t argc4 = 1;napi_value argv4[1] = { replyParcel };napi_new_instance(param->env, jsParcelConstructor, argc4, argv4, &jsReply);// start to call onRemoteRequestsize_t argc2 = 4;napi_value argv2[] = { jsCode, jsData, jsReply, jsOption };napi_value return_val;napi_status ret = napi_call_function(param->env, thisVar, onRemoteRequest, argc2, argv2, &return_val);// Reset old calling pid, uid, device idNAPI_RemoteObject_resetOldCallingInfo(param->env, oldCallingInfo);do {if (ret != napi_ok) {ZLOGE(LOG_LABEL, "OnRemoteRequest got exception");param->result = ERR_UNKNOWN_TRANSACTION;break;}ZLOGD(LOG_LABEL, "call js onRemoteRequest done");// Check whether return_val is Promisebool returnIsPromise = false;//napi_is_promise(param->env, return_val, &returnIsPromise);if (!returnIsPromise) {ZLOGD(LOG_LABEL, "onRemoteRequest is synchronous");bool result = false;napi_get_value_bool(param->env, return_val, &result);if (!result) {ZLOGE(LOG_LABEL, "OnRemoteRequest res:%{public}s", result ? "true" : "false");param->result = ERR_UNKNOWN_TRANSACTION;} else {param->result = ERR_NONE;}break;}...return;} while (0);std::unique_lock<std::mutex> lock(param->lockInfo->mutex);param->lockInfo->ready = true;param->lockInfo->condition.notify_all();});std::unique_lock<std::mutex> lock(jsParam->lockInfo->mutex);jsParam->lockInfo->condition.wait(lock, [&jsParam] { return jsParam->lockInfo->ready; });int ret = jsParam->result;delete jsParam;delete work;return ret;
}

代码比较长(有做删减),大致为:

  • 将下面代码通过uv_event_loop发送到js线程
  • 获取onRemoteMessageRequest函数
  • 创建code参数
  • 构造option参数
  • 构造data、reply参数
  • 调用JavaScript代码中的onRemoteMessageRequest函数,并将code、data、reply、option等参数传入
  • 获取onRemoteMessageRequest的返回值并唤醒线程

napi_handle_scope

首先,napi的各种函数如napi_create_object、napi_call_function等,在创建JavaScript Object或调用JavaScript函数的过程中,会创建各种NativeValue极其子类,如NativeObject、NativeFunction,还有NativeReference等

  • NativeValue等对象是通过NativeChunk创建并管理其内存。
  • NativeValue对象中,会将对应JS对象的作用域修改为global,也就是不会被gc回收。NativeValue被析构时,会将JS对象从global中移除。
  • NativeChunk会通过new与delete管理所有的NativeValue对象。
  • NativeValue对象不被回收,对应的JS对象就不会被回收。
  • 通过NativeChunk创建的对象不会被主动回收,需要使用napi_handle_scope。
  • napi_handle_scope的作用与LocalScope(createDate案例中)类似,只不过napi_handle_scope管理的napi的NativeValue系列对象,而LocalScope是管理Ark运行时中的JavaScript对象。

因此代码修改如下:

int NAPIRemoteObject::OnJsRemoteRequest(CallbackParam *jsParam)
{uv_loop_s *loop = nullptr;napi_get_uv_event_loop(env_, &loop);uv_work_t *work = new(std::nothrow) uv_work_t;work->data = reinterpret_cast<void *>(jsParam);ZLOGI(LOG_LABEL, "start nv queue work loop");uv_queue_work(loop, work, [](uv_work_t *work) {}, [](uv_work_t *work, int status) {ZLOGI(LOG_LABEL, "enter thread pool");CallbackParam *param = reinterpret_cast<CallbackParam *>(work->data);napi_handle_scope scope = nullptr;napi_open_handle_scope(param->env, &scope);...});std::unique_lock<std::mutex> lock(jsParam->lockInfo->mutex);jsParam->lockInfo->condition.wait(lock, [&jsParam] { return jsParam->lockInfo->ready; });int ret = jsParam->result;delete jsParam;delete work;return ret;
}

NAPI_MessageParcel

泄漏还未解决完,trace显示使用napi_new_instance构造data与reply时,有对象泄漏,即NAPI_MessageParcel对象。napi_new_instance函数在创建JavaScript对象时,会调用该类的构造函数,对应到NAPI_MessageParcel则是如下函数:

int NAPIRemoteObject::OnJsRemoteRequest(CallbackParam *jsParam)
{uv_loop_s *loop = nullptr;napi_get_uv_event_loop(env_, &loop);uv_work_t *work = new(std::nothrow) uv_work_t;work->data = reinterpret_cast<void *>(jsParam);ZLOGI(LOG_LABEL, "start nv queue work loop");uv_queue_work(loop, work, [](uv_work_t *work) {}, [](uv_work_t *work, int status) {ZLOGI(LOG_LABEL, "enter thread pool");CallbackParam *param = reinterpret_cast<CallbackParam *>(work->data);napi_handle_scope scope = nullptr;napi_open_handle_scope(param->env, &scope);...});std::unique_lock<std::mutex> lock(jsParam->lockInfo->mutex);jsParam->lockInfo->condition.wait(lock, [&jsParam] { return jsParam->lockInfo->ready; });int ret = jsParam->result;delete jsParam;delete work;return ret;
}

可以看到,在构造函数中,通过new关键字创建了NAPI_MessageParcel对象,但是在后续的OnJsRemoteRequest函数中,并未有delete的操作。那么问题就在于,客户端也有NAPI_MessageParcel对象,为何没有泄漏?

这里首先看看客户端的JavaScript代码:

let option = new rpc.MessageOption()
let data = rpc.MessageParcel.create()
let reply = rpc.MessageParcel.create()proxy.sendRequest(1, data, reply, option).then(function(result) {}).finally(() => {data.reclaim()reply.reclaim()})

在promise的finally中,调用了reclaim函数。其native实现为:

napi_value NAPI_MessageParcel::JS_reclaim(napi_env env, napi_callback_info info)
{size_t argc = 0;napi_value thisVar = nullptr;napi_get_cb_info(env, info, &argc, nullptr, &thisVar, nullptr);NAPI_MessageParcel *napiParcel = nullptr;napi_remove_wrap(env, thisVar, (void **)&napiParcel);NAPI_ASSERT(env, napiParcel != nullptr, "napiParcel is null");delete napiParcel;napi_value result = nullptr;napi_get_undefined(env, &result);return result;
}

在JS_reclaim函数中,有通过delete释放NAPI_MessageParcel对象。客户端没有泄漏的原因就在于调用了JavaScript的reclaim函数。那服务端是否可以调用呢?可以,但是不能让开发者来修改代码,那样后续维护代价太大。这里需要区分是服务端还是客户端,来判断是否要通过native来释放内存,修改NAPI_MessageParcel::JS_constructor函数如下:

napi_value NAPI_MessageParcel::JS_constructor(napi_env env, napi_callback_info info)
{...status = napi_wrap(env, thisVar, messageParcel,[](napi_env env, void *data, void *hint) {},[](napi_env env, void *data, void *hint) {NAPI_MessageParcel *messageParcel = reinterpret_cast<NAPI_MessageParcel *>(data);if (!messageParcel->owner) {delete messageParcel;}},NAPI_ASSERT(env, status == napi_ok, "napi wrap message parcel failed");return thisVar;
}

这样,服务端在JavaScript对象data、reply释放后,就能释放NAPI_MessageParcel对象的内存了。

CustomDialogController

在JavaScript中,使用CustomDialogController,会造成页面对象与CustomDialogController无法被销毁,代码如下:

private backDialogController: CustomDialogController = new CustomDialogController({builder: SimpleComponent({})
});

上述代码会被编译成:

this.backDialogController = new CustomDialogController({builder: () => {let jsDialog = new SimpleComponent_1.default("7", this, {});jsDialog.setController(this.backDialogController);View.create(jsDialog);}
}, this);

CustomDialogController对应的NAPI代码如下:

void JSCustomDialogController::JSBind(BindingTarget object)
{JSClass<JSCustomDialogController>::Declare("CustomDialogController");JSClass<JSCustomDialogController>::CustomMethod("open", &JSCustomDialogController::JsOpenDialog);JSClass<JSCustomDialogController>::CustomMethod("close", &JSCustomDialogController::JsCloseDialog);JSClass<JSCustomDialogController>::Bind(object, &JSCustomDialogController::ConstructorCallback, &JSCustomDialogController::DestructorCallback);
}

new CustomDialogController的实现

JavaScript代码new CustomDialogController会调用Native的JSCustomDialogController::ConstructorCallback函数,代码如下:

void JSCustomDialogController::JSBind(BindingTarget object)
{JSClass<JSCustomDialogController>::Declare("CustomDialogController");JSClass<JSCustomDialogController>::CustomMethod("open", &JSCustomDialogController::JsOpenDialog);JSClass<JSCustomDialogController>::CustomMethod("close", &JSCustomDialogController::JsCloseDialog);JSClass<JSCustomDialogController>::Bind(object, &JSCustomDialogController::ConstructorCallback, &JSCustomDialogController::DestructorCallback);
}
  • JSRef<JSObject> constructorArg = JSRef<JSObject>::Cast(info[0])获取的是第一个入参,即带builder函数的对象。
  • JSRef<JSObject> ownerObj = JSRef<JSObject>::Cast(info[1])获取的是第二个入参,即传入的this,也就是应用的页面View对象。
  • 接下来通过new关键字创建JSCustomDialogController对象instance。
  • 将第一个入参对象中的builder函数,使用instance对象的jsBuilderFunction_属性保存起来,供后续调用。该属性的类型是RefPtr<JsFunction>类型,会强持有对应的JavaScript对象。
  • 将instance对象通过SetReturnValue设置返回值给JSCallbackInfo对象。

也就是说,JavaScript代码new CustomDialogController会创建两个对象:

  1. JavaScript对象CustomDialogController
  2. Native对象JSCustomDialogController

这两个对象如何关联起来的呢?简单来说,在系统调用了JSCustomDialogController::ConstructorCallback函数后,通过JSCallbackInfo获取返回值,即JSCustomDialogController对象的指针,并将其通过NativePointer的形式,与JavaScript对象CustomDialogController关联。

JSCustomDialogController对象合适被回收呢?在JSCustomDialogController::DestructorCallback中,也就是JavaScript对象CustomDialogController销毁时:

void JSCustomDialogController::JSBind(BindingTarget object)
{JSClass<JSCustomDialogController>::Declare("CustomDialogController");JSClass<JSCustomDialogController>::CustomMethod("open", &JSCustomDialogController::JsOpenDialog);JSClass<JSCustomDialogController>::CustomMethod("close", &JSCustomDialogController::JsCloseDialog);JSClass<JSCustomDialogController>::Bind(object, &JSCustomDialogController::ConstructorCallback, &JSCustomDialogController::DestructorCallback);
}

对象之间的关系

这里涉及到三个对象,分别是:

  • View,ui页面对象,也就是ets代码中的this
  • CustomDialogController,JavaScript对象
  • JSCustomDialogController,native对象

三者关系如下:

  • View的成员backDialogController持有了CustomDialogController
  • CustomDialogController通过NativePointer与JSCustomDialogController关联
  • CustomDialogController销毁时会回收JSCustomDialogController

箭头函数

目前看起来一切正常,只要View能被正常销毁,就不会造成泄漏。那么问题出在哪了呢?我们回顾一下编译后的new CustomDialogController代码:

this.backDialogController = new CustomDialogController({builder: () => {...}
}, this);

注意这里builder函数被编译为了箭头函数,箭头函数的this会指向最近的上层this,即View。这样问题就来了,JSCustomDialogController对象的jsBuilderFunction_持有了builder函数,builder函数持有了View引用,相当于JSCustomDialogController持有了View的引用。

又因为CustomDialogController与JSCustomDialogController关联,生命周期保持一致,间接的可以看做CustomDialogController持有了View。同时View的成员backDialogController持有了CustomDialogController,造成了循环引用,两个JavaScript对象都无法被销毁。

如何解决呢?很简单,只需要在页面的aboutToDisappear函数中,将backDialogController与View的引用解除即可:

aboutToDisappear() {delete this.devicesDialogControllerthis.devicesDialogController = undefined
}

为了能让大家更好的学习鸿蒙 (Harmony OS) 开发技术,这边特意整理了《鸿蒙 (Harmony OS)开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙 (Harmony OS)开发学习手册》

入门必看:https://qr21.cn/FV7h05

  1. 应用开发导读(ArkTS)
  2. ……

HarmonyOS 概念:https://qr21.cn/FV7h05

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全

如何快速入门?:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. 构建第一个JS应用
  4. ……

开发基础知识:https://qr21.cn/FV7h05

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

基于ArkTS 开发:https://qr21.cn/FV7h05

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/606278.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Golang : Bson\Json互转

代码 package bson_jsonimport ("encoding/json""errors""fmt""gopkg.in/mgo.v2/bson""os""testing" )type User struct {Name string json:"name,omitempty" bson:"name,omitempty"CSD…

静态网页设计——天行九歌(HTML+CSS+JavaScript)(dw、sublime Text、webstorm、HBuilder X)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 感谢大佬的视频&#xff1a;https://www.bilibili.com/video/BV1de411m7y4/?vd_source5f425e0074a7f92921f53ab87712357b 源码&#xff1a;https://space.bilibili.com…

java回溯算法、最短路径算法、最小生成树算法

回溯算法 回溯算法实际上一个类似枚举的搜索尝试过程&#xff0c;主要是在搜索尝试过程中寻找问题的解&#xff0c;当发现已不满足求解条件时&#xff0c;就“回溯”返回&#xff0c;尝试别的路径。 最短路径算法 从某顶点出发&#xff0c;沿图的边到达另一顶点所经过的路径中…

高通开发系列 - toolchain交叉编译器编译kernel以及生成boot镜像

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 返回:专栏总目录 目录 背景概述分析过程generate_defconfig.sh脚本环境准备合并其他几个配置文件开始编译生成dtb镜像

Python-PyQt5树莓派上位机

Python-PyQt5树莓派上位机 一个使用PythonQT设计的树莓派的上位机&#xff0c;功能大概如下 1.笔记本电脑与树莓派的通讯是否成功显示&#xff08;给个信号显示判断是否通讯成功&#xff09;&#xff1b; 2.阈值的设置显示&#xff1b; 3.图像成像的显示&#xff1b; 4.是否发生…

【python爬虫】如何开始写爬虫?来给你一条清晰的学习路线吧~

记录一下我自己从零开始写python爬虫的心得吧&#xff01; 我刚开始对爬虫不是很了解&#xff0c;又没有任何的计算机、编程基础&#xff0c;确实有点懵逼。从哪里开始&#xff0c;哪些是最开始应该学的&#xff0c;哪些应该等到有一定基础之后再学&#xff0c;也没个清晰的概…

模糊综合评价

第一步&#xff1a;确定评语指标集 评语集 第二步&#xff1a;求出模糊评价矩阵P,往往结合具体数据 通常用频率法确定隶属度 用变异系数法确定权重 前几篇博客有讲变异系数法 移步 模糊合成

【hcie-cloud】【19】云原生详细介绍、华为云Stack中云原生服务简介

文章目录 前言云原生简介云计算和云原生云原生的特征云原生技术架构云原生的定义云原生关键技术 - 容器&不可变基础设施1、 容器2、微服务3、服务网格4、DevOps5、 不可变基础设施6、 声明式API 云原生关键技术 - 申明式API 华为云Stack中云原生服务简介构建以应用为中心的…

Day1Qt

1、实现登录窗口界面 头文件 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QIcon>//图标 #include <QLabel>//标签类 #include <QMovie>//动态类 #include <QLineEdit>//行编辑类 #include <QPushButton>…

龙芯+RT-Thread+LVGL实战笔记(29)——电子琴弹奏

【写在前面】临近期末,笔者工作繁忙,因此本系列教程的更新频率有所放缓,还望订阅本专栏的朋友理解,请勿催更。笔者在此也简要声明几点: 有些硬件模块笔者并没有,如LED点阵、压力传感模块、RFID模块等,因此这些模块的相关任务暂时无法给出经过验证的代码。其实,教程进行…

一文带你全面了解什么是自动化测试?

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;软件测试面试题分享&#xff1a; 1000道软件测试面试题及答案&#x1f4e2;软件测试实战项目分享&#xff1a; 纯接口项目-完…

打造专属个人或企业知识付费平台,核心功能设计

在当今信息爆炸的时代&#xff0c;知识管理已经成为了每个人必须面对的问题。然而&#xff0c;市面上的知识付费平台大多数都是通用的&#xff0c;无法满足个性化需求。 因此&#xff0c;明理信息科技提供了一款专属定制的适合个人的知识付费平台。核心产品能力如下&#xff1…

js逆向第14例:猿人学第7题动态字体,随风漂移

任务7:采集这5页中胜点列的数据,找出胜点最高的召唤师,将召唤师姓名填入答案中 此题采集的是胜点列表的数据如下 通过控制台审查元素查看,可以看到是乱码,记得几年前的快手,小红书,抖音也采用了此类反爬措施,html页面显示的是乱码,浏览器能正常显示数据,大概率就是…

基于ssm的双减后初小教育课外学习生活活动平台的设计与实现论文

双减后初小教育课外学习生活活动平台的设计与实现 摘 要 当下&#xff0c;正处于信息化的时代&#xff0c;许多行业顺应时代的变化&#xff0c;结合使用计算机技术向数字化、信息化建设迈进。以前学校对于课外学习活动信息的管理和控制&#xff0c;采用人工登记的方式保存相关…

基于海洋捕食者算法优化的Elman神经网络数据预测 - 附代码

基于海洋捕食者算法优化的Elman神经网络数据预测 - 附代码 文章目录 基于海洋捕食者算法优化的Elman神经网络数据预测 - 附代码1.Elman 神经网络结构2.Elman 神经用络学习过程3.电力负荷预测概述3.1 模型建立 4.基于海洋捕食者优化的Elman网络5.测试结果6.参考文献7.Matlab代码…

在Spring Cloud Config Github配置中心

关于Spring Cloud系列我们其实讲解了很多&#xff0c;但是这里我们介绍一下Spring Cloud Config&#xff0c;它是一个解决分布式系统的配置管理方案&#xff0c;他包含了Client 和 Server 两个部分&#xff0c;server提供配置文件的存储&#xff0c;以接口的方式将配置文件内容…

一天一个设计模式---工厂方法

概念 工厂模式是一种创建型设计模式&#xff0c;其主要目标是提供一个统一的接口来创建对象&#xff0c;而不必指定其具体类。工厂模式将对象的实例化过程抽象出来&#xff0c;使得客户端代码不需要知道实际创建的具体类&#xff0c;只需通过工厂接口或方法来获取所需的对象。…

《矩阵分析》笔记

来源&#xff1a;【《矩阵分析》期末速成 主讲人&#xff1a;苑长&#xff08;5小时冲上90&#xff09;】https://www.bilibili.com/video/BV1A24y1p76q?vd_sourcec4e1c57e5b6ca4824f87e74170ffa64d 这学期考矩阵论&#xff0c;使用教材是《矩阵论简明教程》&#xff0c;因为没…

全球企业绿色供应链数据(含CITI指数和CATI指数,2014-2023年)

数据简介&#xff1a;发文趋势与主题分布 数据来源&#xff1a;公众环境研究中心&#xff08;IPE&#xff09; 时间跨度 CITI指数&#xff1a;2014-2023年&#xff1b; CATI指数&#xff1a;2021-2023年 数据范围&#xff1a;品牌型企业&#xff0c;温室气体重点排放行业的上…

某市公共资源交易网

目标网站首页&#xff1a;aHR0cDovL2dnenkuendmd2IudGouZ292LmNu/ 分析页面&#xff1a;aHR0cDovL2dnenkuendmd2IudGouZ292LmNuL3h3engvaW5kZXhfMi5qaHRtbA 点击前URL http://ggzy.zwfwb.tj.gov.cn:80/zwyw/1030977.jhtml 点击后URL http://ggzy.zwfwb.tj.gov.cn/zwyw/PtP89W…