Android 内存优化——常见内存泄露及优化方案
看到了一篇关于内存泄漏的文章后,就想着分享给大家,最后一起学习,一起进步:
如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,这种情况下就是内存泄漏。
在Android开发中,一些不好的编程习惯会导致我们开发的app存在内存泄漏的情况,下面介绍一些在Android开发中常见的内存泄漏场景及优化方案。
单例导致内存泄漏
单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄漏。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄漏。
public class AppSettings {private static AppSettings sInstance;private Context mContext;private AppSettings(Context context) {this.mContext = context;}public static AppSettings getInstance(Context context) {if (sInstance == null) {sInstance = new AppSettings(context);}return sInstance;}
}
向上面代码中这样的单例,如果我们再调用getInstance(Context context)方法的时候传入的context参数是Activity,Service等上下文,就会导致内存泄漏。
以Activity为例,当我们启动一个Activity,并调用getInstance(Context context)方法去获取AppSettings的单例,传入Activity.this作为context,这样AppSettings类的单例sInstance就持有了Activity的引用,当我们退出Activity时,该Activity就没有了,但是因为sInstance作为静态单例,(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄漏。
为了避免这样单例导致内存泄漏,我们可以将context参数改为全局的上下文。
private AppSettings(Context context) {this.mContext = context.getApplicationContext();
}
全局的上下文Application Context就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。
单例模式对应 应用程序的生命周期,所以我们再构造单例的时候尽量避免使用Activity的上下文,而是使用Appliction的上下文。
静态变量导致内存泄漏
静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。
比如下面这样的情况,在Activity中为了避免重复的创建info,将info作为静态变量:
public class MainActivity extends AppCompatActivity {
private static Info sInfo;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);if (sInfo != null) {sInfo = new Info(this);}}
}
class Info {public Info(Activity activity) {}
}
Info作为Activity的静态成员,并且持有Activity的引用,但是sInfor作为静态变量,生命周期肯定比Activity长。所以当Activity退出后,sInfor仍然引用了Activity,Activity不能被回收,这就导致了内存泄漏。
在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄漏,所以我们再新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄漏。当然,我们也可以在适当的时候将静态变量重置为null,使其不再持用引用,这样也可以避免内存泄漏。
非静态内部内导致内存泄漏
非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄漏。
非静态内部类导致的内存泄漏在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);start();}private void start() {Message msg = Message.obtain();msg.what = 1;mHandler.sendMessage(msg);}private Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 1) {// 做相应逻辑}}};
}
也许有人会说,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不一定会导致内存泄漏呢?显然不是这样的!
熟悉Handler消息机制的都知道,mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息队列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄漏。
给大家看一下源码,如果想学习Handler消息机制可以看看这个博客https://blog.csdn.net/sjw890821sjw/article/details/142138517写的很好:
通常在Android开发中如果要使用内部类,但又要规避内存泄漏,一般都会采用静态内部类+弱引用的方式。
public class MainActivity extends AppCompatActivity {private Handler mHandler;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mHandler = new MyHandler(this);start();}private void start() {Message msg = Message.obtain();msg.what = 1;mHandler.sendMessage(msg);}private static class MyHandler extends Handler {private WeakReference<MainActivity> activityWeakReference;public MyHandler(MainActivity activity) {activityWeakReference = new WeakReference<>(activity);}@Overridepublic void handleMessage(Message msg) {MainActivity activity = activityWeakReference.get();if (activity != null) {if (msg.what == 1) {// 做相应逻辑}}}}
}
mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄漏了。
上面的做法确实避免了Activity导致的内存泄漏,发送的msg不再已经没有持有Activity的引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。
@Overrideprotected void onDestroy() {super.onDestroy();mHandler.removeCallbacksAndMessages(null);
}
非静态内部类造成内存泄漏还有一种情况就是使用Thread或者AsyncTask。
比如在Activity中直接new一个子线程Thread:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);new Thread(new Runnable() {@Overridepublic void run() {// 模拟相应耗时逻辑try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}
或者直接新建AsynTask异步任务:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);new AsyncTask<Void, Void, Void>() {@Overrideprotected Void doInBackground(Void... params) {// 模拟相应耗时逻辑try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return null;}}.execute();}
}
很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程Thread和AsynTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄漏。要避免内存泄漏的话还是需要像上面Handler一样使用静态内部类+弱引用的方式。
未取消注册或回调导致内存泄漏
比如我们在Activity中注册广播,如果在Activity销毁后不再取消注册,那么这个广播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄漏。因此注册广播后在Activity销毁后一定要取消注册。
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);this.registerReceiver(mReceiver, new IntentFilter());}private BroadcastReceiver mReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 接收到广播需要做的逻辑}};@Overrideprotected void onDestroy() {super.onDestroy();this.unregisterReceiver(mReceiver);}
}
在注册观察者模式的时候,如果不及时取消也会造成内存泄漏,比如使用Retrofit+RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。
Timer和TimerTask导致内存泄漏
Timer和TimerTask在Android中通常会被用来做一些计时器或者循环任务,比如实现无限轮播的ViewPager:
public class MainActivity extends AppCompatActivity {private ViewPager mViewPager;private PagerAdapter mAdapter;private Timer mTimer;private TimerTask mTimerTask;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);init();mTimer.schedule(mTimerTask, 3000, 3000);}private void init() {mViewPager = (ViewPager) findViewById(R.id.view_pager);mAdapter = new ViewPagerAdapter();mViewPager.setAdapter(mAdapter);mTimer = new Timer();mTimerTask = new TimerTask() {@Overridepublic void run() {MainActivity.this.runOnUiThread(new Runnable() {@Overridepublic void run() {loopViewpager();}});}};}private void loopViewpager() {if (mAdapter.getCount() > 0) {int curPos = mViewPager.getCurrentItem();curPos = (++curPos) % mAdapter.getCount();mViewPager.setCurrentItem(curPos);}}private void stopLoopViewPager() {if (mTimer != null) {mTimer.cancel();mTimer.purge();mTimer = null;}if (mTimerTask != null) {mTimerTask.cancel();mTimerTask = null;}}@Overrideprotected void onDestroy() {super.onDestroy();stopLoopViewPager();}
}
当我们Activity销毁的时候,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。
集合中的对象未清理造成内存泄漏
这个比较好理解,如果一个对象放入到ArrayList,HashMap等集合中,这个集合就会持有该对象的引用。当我们不在需要这个对象时,也没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄漏。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄漏了,所以在使用集合时要将不用的对象从集合remove,或者clear集合,以避免内存泄漏。
资源未关闭或释放导致内存泄漏
在使用IO,File流或者Sqlite,Cursor等资源时要及时关闭,这些资源在进行读写操作时通常都会使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄漏。因此我们再不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄漏。
属性动画造成内存泄漏
动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用Cancel方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在控件引用Activity,这就造成了Activity无法正常释放。因此同样要在Activity销毁的时候Cancel掉属性动画,避免发生内存泄漏。
@Override
protected void onDestroy() {super.onDestroy();mAnimator.cancel();
}
WebView造成内存泄漏
关于WebView的内存泄漏,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。
另外再查阅webView内存泄漏相关资料时看到这种情况:
WebView下面的Callback持有Activity引用,造成WebView内存无法释放,即使是调用了WebView.destory()等方法都无法解决问题(Android5.1之后)
最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。详细分析过程请参考这篇文章:https://blog.csdn.net/xygy8860/article/details/53334476?utm_source=itdadao&utm_medium=referral)(http://blog.csdn.net/xygy8860/article/details/53334476)[WebView
@Override
protected void onDestroy() {super.onDestroy();// 先从父控件中移除 WebViewmWebViewContainer.removeView(mWebView);mWebView.stopLoading();mWebView.getSettings().setJavaScriptEnabled(false);mWebView.clearHistory();mWebView.removeAllViews();mWebView.destroy();
}
总结:
内存泄漏在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄漏我们不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避免大多数情况下的内存泄漏:
构造单例的时候尽量别用Activity的引用;
静态引用时注意应用对象的置空或者少用静态引用;
使用静态内部类+软引用代替非静态内部类;
及时取消广播或者观察者注册;
耗时任务,属性动画在Activity销毁时记得Cancel;
文件流,Cursor等资源及时关闭;
Activity销毁时WebView的移除和销毁。
相关文章:
Android 内存优化——常见内存泄露及优化方案
看到了一篇关于内存泄漏的文章后,就想着分享给大家,最后一起学习,一起进步: 如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在…...
Qt6.7.2中使用OpenSSL的坑
最近编写Qt Quick项目,使用Qt6.7.2版本,CMAKE编译,开始QtCreator运行代码都没有问题,访问https也正常,但打出安装包后一试,发现https访问不了,尴尬!! 查看了相关日志发现…...
Mybatis-08.基础操作-删除
一.删除 接口方法 package com.gjw.mapper;import com.gjw.pojo.Emp; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper;import java.util.List;Mapper public interface EmpMapper {// 根据ID删除数据Delete("delete from e…...
通过FDM升级Firepower
1.基本说明 本文主要主要介绍如何通过FDM来升级Firepower设备,这里以FPR1010设备为例,那么什么事Firepower、FDM呢?在开始之前,进行简单的介绍。 Firepower:思科 Firepower 是网络安全和流量管理产品的集成套件&…...
使用 Kibana 将地理空间数据导入 Elasticsearch 以供 ES|QL 使用
作者:来自 Elastic Craig Taverner 如何使用 Kibana 和 csv 采集处理器将地理空间数据采集到 Elasticsearch 中,以便在 Elasticsearch 查询语言 (ES|QL) 中进行搜索。Elasticsearch 具有强大的地理空间搜索功能,现在 ES|QL 也具备这些功能&am…...
demo说明
代码说明 MDIO总线的注册和初始化:分配并注册MDIO总线,用于与PHY设备通信。 网络设备的创建:分配网络设备(如以太网接口)。 PHY设备连接:通过phy_connect函数连接PHY设备,指定回调函数my_adj…...
【c++篇】:从基础到实践--c++内存管理技巧与模版编程基础
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨个人主页:余辉zmh–CSDN博客 ✨ 文章所属专栏:c篇–CSDN博客 文章目录 前言一.c/c内存分布二.c/c的动态内存管理方式2.1.c语言的动态内存管…...
如何减小 Maven 项目生成的 JAR 包体积 提升运维效率
在使用 Maven 构建 Java 项目时,有时需要减小生成的 JAR 包的体积,以提高部署效率或减少资源消耗。以下是一些有效的方法来减小 JAR 包的体积: 排除不必要的依赖打包时,依赖jar包独立于应用jar包 1. 排除不必要的依赖 通过排除项目…...
Python自动化会议记录与摘要生成
前言 在现代工作环境中,会议是团队沟通和决策的重要方式。然而,整理会议记录和生成摘要往往是一项耗时且容易出错的任务。幸运的是,借助Python编程语言以及一些强大的库,我们可以自动化这一过程,让机器帮助我们完成这…...
SwiftUI 中 List 或 Form 子视图关联的 swipeAction 导致展开动画异常的解决
问题现象 小伙伴们都知道,在 SwiftUI 中更快捷的增强 List 或 Form 子视图(Cell)交互功能的方法是使用 swipeAction 修改器。不过,对其使用稍有不慎也会横生枝节。 如上图所示,不适当的设置 Cell 视图布局会使 swipeAction 无法生成正确的收缩和展开动画。对此我们有什么…...
Apache Paimon Catalog
Paimon Catalog可以持久化元数据,当前支持两种类型的metastore: 文件系统(默认):将元数据和表文件存储在文件系统中。hive:在 hive metastore中存储元数据。用户可以直接从 Hive 访问表。 2.2.1 文件系统…...
C++基础:三个字符串也能搞大小?
上一篇说了三个整数比较大小,按照顺序输入的,这次我们看看字符串的,顺便把那个简化以下: 题目:这次输入三个字符串。如果用户输入“Stenbeck", “Hemingway”,“Fitzgerald”,输出将是“Fitzgerald,Hemingway&…...
了解AIGC——自然语言处理与生成
AIGC——自然语言处理与生成:揭秘AI如何生成语言 近年来,AIGC(AI Generated Content)技术迅猛发展,自然语言处理(Natural Language Processing, NLP)与生成技术的结合,使得机器不仅…...
Modern CMake 简明教程(8)- 集成Qt
在项目中集成 Qt 库需要先使用 find_package 查找 Qt 的安装位置。对于 Qt4, CMake 使用 Module 模式进行查找(FindQt4.cmake 由 CMake 提供),而 对于 Qt5、Qt6,则是使用 Config 模式进行查找,相应的 config 文件位于类似下面的目录中 D:\Qt\5.15.2\msvc2019\lib\cmake。…...
人脸应用实例:性别年龄预测
在当今科技飞速发展的时代,人脸识别技术已经从科幻电影走进了我们的日常生活。通过算法来识别人脸的特征,进而判断身份、年龄和性别,这一技术正逐步改变着我们的生活方式。今天,我们就来探讨一下基于深度学习的人脸应用实例——性…...
学习threejs,通过THREE.Raycaster给模型绑定点击事件
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️THREE.Raycaster光线投射概…...
Jackson Json序列化反序列化的两个坑
Jackson is a suite of data-processing tools for Java (and the JVM platform) Jackson最常用的Json序列化功能,引入如下的包即可: <properties>...<!-- Use the latest version whenever possible. --><jackson.version>2.17.1<…...
k8s_Pod健康检查
Kubernetes 3种探针介绍 LivenessProbe(存活探针) LivenessProbe 用于检查容器是否仍然活着。如果探针检测到容器已经失去响应,Kubernetes 将重启该容器。这通常用来修复由于内部状态错误或死锁引起的程序失效问题。 作用:检测容器…...
基于DDPG算法的股票量化交易
项目源码获取方式见文章末尾! 回复暗号:13,免费获取600多个深度学习项目资料,快来加入社群一起学习吧。 **《------往期经典推荐------》**项目名称 1.【基于PyQTFaceNet卷积神经网络实现的学生人脸识别考勤系统】 2.【卫星图像道…...
eIQ笔记(UI介绍+Loss曲线+OpenART例程)
This is a very beginner-friendly article ^o^ 目录 🍂一、训练器设置 input size: learning rate: learning rate decay: Epochs: Decay Rate: Linear Decay: Batch Size: Epochs to Train: QAT(Quantization Aware Training)量化感知训练: Pruning剪枝…...
微信小程序——消息订阅
首先用到的就是wx.requestSubscribeMessage接口。 注意:用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面 requestSubscribeMessage() {uni.requestSubscribeMessage({tmplIds: [],//需要订阅的消息模板的id的集合,一次调用最多可…...
网络原理(传输层)->TCP协议解
前言 大家好!我是小帅,今天我们来学习TCP协议,个人主页 文章目录 1. TCP协议2. TCP的核心机制2.1TCP核心机制一:确认应答2.2 TCP核心机制二:超时重传2.3 TCP核心机制三:连接管理2.4 TCP核心机制四…...
oracle imp和exp 导入不同库的用户和表空间
参考: oracle 导入(imp)数据时的表空间(tablespace users)问题_imp tablespace-CSDN博客 网上的解决办法大概都是这种,但是实际测试19c数据库并不能成功,所以最后采取在导出文件上强行修改表空间的办法,改完后再继续执行导出导入…...
滚珠丝杆的精度级别如何分?
滚珠丝杆是一种常见的线性传动装置,广泛应用于各种机械设备和自动化系统中。滚珠丝杆的精度等级划分是评估其传动精度和运动平稳度的重要标准,滚珠丝杆的精度级别划分主要基于传动中实际移动距离与理想移动距离的偏差,偏差越小,精…...
ComfyUI初体验
ComfyUI 我就不过多介绍了,安装和基础使用可以看下面大佬的视频,感觉自己靠图文描述的效果不一定好,大家看视频比较方便。 ComfyUI全球爆红,AI绘画进入“工作流时代”?做最好懂的Comfy UI入门教程:Stable D…...
DPI-C动态库so的使用
文章目录 前言一、方法介绍二、demo演示2.1 文件准备2.2 执行仿真2.3 仿真结果 总结 前言 在做IC验证EDA仿真过程中,有时候需要调用C实现的参考模块,我们可以利用DPI-C的功能,实现SV侧调用C侧的函数。 在具体实现过程中,我们可以…...
Java避坑案例 - 高并发场景下的分布式缓存策略
文章目录 概述缓存常见问题及解决方案把 Redis 当作数据库常用的数据淘汰策略如何选择合适的驱逐算法 缓存雪崩问题复现解决方案 缓存击穿(热点缓存失效)问题复现解决方案 缓存穿透问题复现解决方案缓存穿透 vs 缓存击穿 缓存与数据库的一致性先更新缓存…...
Python中的字符串修剪:strip()、lstrip() 和 rstrip()
Python中的字符串修剪 Python 中的字符串修剪:strip()、lstrip() 和 rstrip()strip()lstrip()rstrip()应用场景结论 Python 中的字符串修剪:strip()、lstrip() 和 rstrip() 在 Python 开发中,我们经常需要处理字符串,其中一项常见…...
K8S配置storage-class
简介 Kubernetes支持NFS存储,需要安装nfs-subdir-external-provisioner,它是一个存储资源自动调配器,它可将现有的NFS服务器通过持久卷声明来支持Kubernetes持久卷的动态分配。该组件是对Kubernetes NFS-Client Provisioner的扩展࿰…...
多线程——线程池
目录 前言 一、什么是线程池 1.引入线程池的原因 2.线程池的介绍 二、标准库中的线程池 1.构造方法 2.方法参数 (1)corePoolSize 与 maximumPoolSize (2)keepAliveTime 与 unit (3)workQueue&am…...
wordpress广告管理插件/seo和sem的区别
Android 学习已有一年半有余,先后做过两款游戏、三款应用和搭建一台服务端,也了解过一些Android相关的源码(JDK、SDK和NDK) 后来想学深入点,搞过两款开源项目(LGame和AChartEngine),…...
乌镇网站建设投标书/百度推广如何计费
什么是Dynamic Island Dynamic Island 是一项功能,可调整 iPhone 14 Pro 和 iPhone 14 Pro Max 上药丸形凹口的大小。调整大小让黑色区域包含更多信息、交互式信息,您可以点击或长按以访问不同的功能。 脚步 我们需要创建一个带有 Widget Extension 的项目。你可以参考之前的…...
苏南建设集团网站/优化设计单元测试卷答案
摘要 本篇经验将和大家介绍Windows下安装和部署RabbitMQ消息队列服务器,希望对大家的工作和学习有所帮助! 目录 一、Erlang语言环境的搭建 二、RabbitMQ服务环境的搭建 三、RabbitMQ服务Web管理工具 一、Erlang语言环境的搭建 RabbitMQ开源消息队列服务是…...
我想做/seo诊断
HTMLTestRunner下载地址: python 2: http://tungwaiyip.info/software/HTMLTestRunner.html python 3:http://pan.baidu.com/s/1dEZQ0pz 下载后放到路径:${python}\Lib\site-packages\ 在cmd引入HTMLTestRunner 不报错说明成功 3.方法一: …...
开州区城乡建设委员会官方网站/网络营销推广服务商
近日"超越时空的紫禁城"([url]www.beyondspaceandtime.org[/url])已正式上线运营,这是一个历经数年颇受业内人士关注的项目。能够再一次以虚拟现实的方式寻访故宫,于我个人而言,感慨颇多,顺手写下…...
济南网站排名推广/企业营销
前言: 在前两篇的文章中,我们学会了给组件添加属性、事件,以及对这些属性和事件进行描述添加,今天,我们就来小试一把这个组件吧,如果你忘记了前两篇文章的内容,可以从这里回顾一下: …...