Android 启动速度优化

news2024/11/19 23:13:14

Android 启动速度优化

  • 前序
  • 统计
    • adb测量
    • 手动打点
  • 方案
    • 预加载
      • class字节码的预加载
      • Activity预创建
      • Glide预初始化
      • WebView预加载
      • 数据预加载
    • 三方库初始化
    • 布局方面
      • ViewStub标签
      • 减少层级
        • 主题的选择
        • 约束布局使用
      • X2C方案
    • 过度绘制
      • 如何检测过度绘制
      • 如何监控页面的渲染速度
      • 移除多余背景
    • RecyclerView专项
    • 杂项
    • 黑科技
      • 三级目录

前序

启动速度优化是一个需要慢工出细活的工作。这里主要记录下几年前在做启动优化时的各种方案,和现在新技术下方案。当时从主流机型(小米8)4.5s左右优化到了1.8s左右,当然这里因为之前没做过优化,所以提升很明显。记录为主。
启动速度的业务定义:首先要明白你要优化的点,是要启动不黑屏等待;还是是要点击图标后启动快,出现闪屏页面就行,不用管后面显示的数据页面;还是要第一个展示功能的页面完全展示给客户的启动时间。是根据自身的业务定义,而我们公司的是支付类app,领导的明确要求就是,点击app启动到点击打开首页上面付款码或首页下面的生活缴费的功能页面控制在2s内完成。

统计

adb测量

该方案只能线下使用,不能带在线上。

  • adb shell am start -W 包名/启动类完整路径
  • ThisTime 表示一连串启动Activity的最后一个Activity的启动耗时
  • TotalTime 所有activity启动耗时
  • WaitTime AMS启动activity的耗时包括当前Activity的onPause()和自己Activity的启动)。

手动打点

可带线上,但是结束点比较相对,可有不同参考。

  • 开始点:在App的入口attachBaseContext开始计算开始。争议点就是,错过了包括zygote点fork进程的阶段和dex包加载时间。
  • 结束点:
    • 1,可以在onResume()打印。
    • 2, 监听绘制完后打印。
//view重绘时回调
view.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {		
	@Override
	public void onDraw() {
	// TODO Auto-generated method stub
	}
});

方案

预加载

class字节码的预加载

先讨论下java类的加载过程,如图:
在这里插入图片描述
java类的类加载生命周期包括:加载Loading、验证Verification,、准备Preparation、解析Resolution、 初始化Initialization、使用Using和卸载Unloading.
这里就需要了解下Class.forName(className),这个
我们在应用的启动阶段,Application的入口,去做class的预加载操作。而这个操作最好是放在attachBaseContext里面,这个方法调用时候的cpu还没上来,可以利用。

@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        ThreadUtils.getSinglePool().execute(new Runnable() {
            @Override
            public void run() {
            //是否开启线程,要看时间情况
                Log.d("test","执行class 预加载 开始");
                try {
                    Class.forName("com.xxxxx.xxxxx.TestActivity");
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                Log.d("test","执行class 预加载 结束");
            }
        });
    }

Activity预创建

根据上面字节码的预加载原理一样,对象第一次创建的时候,java虚拟机首先检查类对应的Class 对象是否已经加载。如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。

 @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        start = System.currentTimeMillis();
        ThreadUtils.getSinglePool().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Class.forName("com.hozonauto.manual.ui.fragment.TutorialFragment");
                    ManualActivity activity = new ManualActivity();                    
                    
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        });
    }

Glide预初始化

在我们实际用Profiler来看那一块比较耗时,发现Gilde的初始化流程非常耗时,启动速度的感观就是首页的能看见的速度,我们应用中占大头的就是首页的banner图片。同时、是在其他操作也在同时抢占CPU资源的时候会更慢。Glide的初始化会加载所有配置的Module,然后初始化RequestManager、包括网络层、工作线程池和配置项等,比较耗时,最后还要应用一些解码的选项Options。

所以,我们在应用的启动的阶段去初始化glide:

@Override
    protected void attachBaseContext(Context context) {
        super.attachBaseContext(context);

        ThreadUtils.getSinglePool().execute(new Runnable() {
            @Override
            public void run() {
                try {
          //地址假的就行
                    Glide.with(context).load("/sdcard/xxx.jpg").into(1, 1);
                    Glide.with(context).load("http://sdcard/xxx.jpg").into(1, 1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

WebView预加载

对于WebView的优化网上很多方案,简单的思路就是WebView第一次创建非常耗时,可以创建一个WebView的缓存池,提前去创建WebView,提前完成内核初始化。同时可以本地预置html和css,WebView创建的时候先预加载本地html,之后通过js脚本填充内容部分。具体优化方案参考:
webview优化方案参考一
webview优化方案参考二
当然这里,如果你的启动流程不存在使用webview的情况,请不用多余操作。

数据预加载

一般我们的页面展示的数据一般分三种数据:

  1. 默认数据(一般放在string.xml或者assets里面。有的没有默认)
  2. 缓存数据
  3. 网络数据

数据预加载的意义在于启动第一个展示的页面是否正确展示给用户,这是一个主观的启动速度感知。

比如第三方数据库,如果使用到GreenDao,有必要提前初始化并且放在工作线程中,获取获取一次greenDao的DaoSession实例化对象即可。注意这类型的数据库的升级和数据迁移是非常耗时的。

同时还可以在应用入口,提前获取到数据,使用静态变量保存。一种空间换时间的思路。

三方库初始化

这里主要是指启动阶段中,一些第三方框架的耗时。在业务上尽可能做到以下几点:

  1. 能异步的就异步,在工作线程去初始化
  2. 能用到再初始化是最好的
  3. 不要太多异步导致 业务逻辑剧增导致逻辑复杂
  4. 注意第三方库中ContentProvider的自动初始化
  5. 注意第三方库的方法总数超标导致分包太多

对于第3点提供一个方案:有向无环图

布局方面

这里要先要找到耗时点在哪?(XML文件的IO解析 和 反射创建View)。
我们能做有什么方案去优化?
我们先看setContentView的代码块:

 @Override
    public void setContentView(int resId) {
    	//1.优化点 自带的系统布局
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        //2.LayoutInflater.inflate 的耗时
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

代码块中的1点,看ensureSubDecor()方法的部分作用,解析主题style文件,根据style生成是否带title、ActionBar、Menu等控件。所以主题的选择会影响绘制流程的时间。

然后跟进看LayoutInflater.from(mContext).inflate方法:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
 		//0.parser 是xml解析的工具类,这里的重载方法已经调用类loadXmlResourceParser(id, "layout"),把resid塞到parser里面类
 		//1.保证View的状态可控性加锁
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
            //2.检查resid 的根root 是否合法性
                advanceToRootNode(parser);
                final String name = parser.getName();
			//3.merge标签使用是否合法
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    // 4.根据解析的标签名字去创建view
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    // Inflate all children under temp against its context.
                    //5.while循环去创建下一层级的view
                    rInflateChildren(parser, temp, attrs, true);

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                    	//6.添加到根布局上
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

简单的说下流程就是,通过XmlPullParser去解析xml布局文件,while循环和递归一层层往更深层级去创建view,层级越多,遍历时间越长。view的创建最后都是调用的createViewFromTag() 方法。
跟进createViewFromTag() 方法,最后会走到createView()方法(中间的Factory流程这里不做介绍):

public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Objects.requireNonNull(viewContext);
        Objects.requireNonNull(name);
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                //反射创建
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);

                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                                mContext.getClassLoader()).asSubclass(View.class);

                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, viewContext, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, viewContext, attrs);
                    }
                }
            }

            Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = viewContext;
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            try {
                final View view = constructor.newInstance(args);
                //2.ViewStub标签不解析
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        } catch (NoSuchMethodException e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(viewContext, attrs)
                    + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;

        } catch (ClassCastException e) {
            // If loaded class is not a View subclass
            final InflateException ie = new InflateException(
                    getParserStateDescription(viewContext, attrs)
                    + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
        } catch (Exception e) {
            final InflateException ie = new InflateException(
                    getParserStateDescription(viewContext, attrs) + ": Error inflating class "
                            + (clazz == null ? "<unknown>" : clazz.getName()), e);
            ie.setStackTrace(EMPTY_STACK_TRACE);
            throw ie;
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

根据上面的流程分析,总结下耗时的关键点在:

  1. 加载XML文件是一个IO流程,这个会耗时
  2. VIew的创建是一个反射过程

从而也能得到一些方案:

  • 减少布局层级
  • 主题的选择可以减少不需要的控件和布局嵌套
  • 不用LayoutInflater
  • 异步方案

studio自带布局层级查看工具:
在这里插入图片描述
打开View,点击Tool Windows 中 Layout Inspector 项,下方会显示:

在这里插入图片描述
就可以看到view的布局层级了,可以选择优化那些层级过深了。

ViewStub标签

ViewStub 是一种view,是不可见的,实际上是把宽高都设置为0。
根据之前分析的流程,ViewStub标签是不会参与解析和创建的。仅仅做一个占位使用。

 <ViewStub
        android:id="@+id/vs"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:inflatedId="@+id/inflated_id"
        android:layout="@layout/view_stub_layout" />

我们可以根据ViewStub标签来做一些布局的懒加载,android:layout指定的布局为真正的布局,在真正需要的时候调用ViewStub.infalte方法(不要重复调用)。

启动优化中ViewStub使用个人经验分享:

  • 对于闪屏页面的广告,可以使用ViewStub把广告布局包住,广告不是每次都有的
  • 首页不可见的部分(屏幕下面的内容)
  • 对于首页为ViewPager的布局,因为会默认预加载左右各一个fragment。而不是首屏显示的fragment的布局可以用ViewStub包裹住,这样那些非首屏的fragment的布局不会创建耗时,在真正显示的时候再去加载
  • 一些状态布局

减少层级

主题的选择

根据上面的源码分析,不同主题会让页面绘制不同的控件(title、bar、menu等)。
这里同一个activity分别测试了:Theme.AppCompat.NoActionBarTheme.AppCompat.DayNight.DarkActionBarTheme.AppCompat.Dialog 的style。
得到不同耗时结果从长到短为:Theme.AppCompat.DayNight.DarkActionBar > Theme.AppCompat.Dialog > Theme.AppCompat.NoActionBar

除了系统自带的style有不用的差异,单独设置的属性也存在差异:无背景的、无title的、无动画的会更快。

 <style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:colorBackgroundCacheHint">@null</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowAnimationStyle">@android:style/Animation</item>
    </style>

约束布局使用

ConstraintLayout是一个优化布局的利器,可以把之前的LinearLayoutRelativeLayout嵌套的复杂布局拍平为一个层级,很大程度上减少的布局的层级。使用参考文档:
约束布局的使用文档

X2C方案

先回顾下LayoutInflater加载的耗时点:IO和反射。而X2C方案直接是通过代码创建布局,不去读取xml文件和反射创建布局。
具体可以查看X2C的文档:X2C

小tips:项目中可以不用集成X2C框架,可以在测试demo中集成,然后同X2C框架去生成xml文件的布局代码,然后再拷贝到原项目中。

小tips2: 通过new 的方式创建布局,存在代码量比较多问题。重点这里会略过Factory流程,有的换肤框架会失效。

过度绘制

如何检测过度绘制

以小米手机为例子,在手机设置----》开发者选项----》-硬件加速渲染----》调试GPU过度绘制----》选择 显示过度绘制区域:
在这里插入图片描述

这里各种颜色代码什么意思了,选用官方贴图:
在这里插入图片描述
如果页面红色居多,表示同一个快区域存在大量的重复绘制了,不仅浪费了性能,而且会导致加载过长,除了非必要外,就需要检查下我们的代码是否应该如此。
我们应该尽可能的去保留原色或者蓝色,无过度绘制或者一次过度绘制。

如何监控页面的渲染速度

以小米手机为例子,在手机设置----》开发者选项----》监控----》HWUI呈现模式分析----》选择 在屏幕上显示为条线图:
在这里插入图片描述
上图中横轴的每一个竖条都代表一帧,而绿色的水平线表示16ms,而android要求每帧的绘制时间不超过16ms。(其他两根颜色水平线可能是高刷屏)
垂直的颜色还是以官方贴图解释下:
在这里插入图片描述
adb方式:adb shell setprop debug.hwui.profile true 开关打开 ; adb shell dumpsys gfxinfo 包名 用adb显示

从HWUI呈现模式分析和过度绘制可以看出来,我们能够进行优化的点主要就是测量、布局、绘制、动画和输入处理。

  • 测量(onMeasure)、布局(onLayout)、绘制(onDraw)过程是存在自顶而下遍历过程,父View到子View的过层,所以如果布局的层级过多,这会占用额外的CPU资源
  • 当屏幕上的某个像素在同一帧的时间内被绘制了多次(Overdraw),这会浪费大量的CPU以及GPU资源,而且是无意义的。
  • 在绘制过程中执行的onDraw()方法中,不要大量的创建局部变量和耗时操作,而onDraw()方法中会一直重复调用。
  • 小心使用动画和动画过程中产生的消耗(局部变量和耗时操作)

其实总结还是:减少布局层次、和绘制中的耗时操作、多余布局背景。

移除多余背景

  • 如果列表和子元素是相同的背景,可以移除子元素的背景
  • <item name="android:windowBackground">@android:color/transparent</item>主题可以使用透明背景或者无背景(@null)来代替
  • 减少透明度的使用:如果这个view带有透明度,它就需要知道它下一层的view的颜色,再进行Blend混色处理,至少绘制2次
  • 自定义view中,善于利用 clipRect() 来裁剪绘制,而不是画布的叠加

RecyclerView专项

杂项

黑科技

三级目录

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

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

相关文章

Linux系列 Linux常用命令(2)

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.Linux常用命令后续 1.创建目录和文件 &#xff08;1&#x…

DaVinci:Camera Raw(Blackmagic RAW)

本文主要介绍 Blackmagic Raw 格式&#xff08;.braw&#xff09;素材相关的 Camera Raw 参数。解码质量Decode Quality解码质量决定了图像解拜耳之后所呈现的素质。默认为“使用项目设置” Use project setting&#xff0c;表示使用项目设置对话框中的“Camera RAW”解码质量设…

PPT录制视频的方法有哪些?分享5种亲测好用的方法

PPT文稿可以通过图文或者动画的形式&#xff0c;直观形象地把内容展现给观众&#xff0c;从而给观众留下深刻的印象。比如老师讲课时会用到PPT&#xff0c;公司开会时也会用到PPT。除了需要使用到PPT文稿之外&#xff0c;有时还要对它进行录制。那你知道PPT录制视频的方法有哪些…

[JavaWeb]HTML

目录1.简介1.1 HTML基本结构1.2 标签使用细节2.常用标签使用2.1 font 字体标签2.2 字符实体2.3 标题标签2.4 超链接标签2.5 有序无序标签(1)无序列表ul/li(2)有序列表ol/li2.6 图像标签(img)2.7 表格(table)标签表格标签-跨行跨列表格2.8 form(表单)标签介绍2.9 input单选多选标…

Unity-ROS与Navigation 2(四)

0. 简介 对于Gazebo而言&#xff0c;我们知道其是可以通过与ROS的连接完成机器人建图导航的&#xff0c;那我们是否可以通过Unity来完成相同的工作呢&#xff0c;答案是肯定的。这一讲我们就来讲述使用Unity的“Turtlebot3”模拟环境&#xff0c;来运行ROS2中的“Navigation 2…

Rust机器学习之tch-rs

Rust机器学习之tch-rs tch-rs是PyTorch接口的Rust绑定&#xff0c;可以认为tch-rs是Rust版的PyTorch。本文将带领大家学习如何用tch-rs搭建深度神经网络识别MNIST数据集中的手写数字。 本文是“Rust替代Python进行机器学习”系列文章的第五篇&#xff0c;其他教程请参考下面表…

autodeauth:一款功能强大的自动化Deauth渗透测试工具

关于autodeauth autodeauth是一款功能强大的自动化Deauth渗透测试工具&#xff0c;该工具可以帮助广大研究人员以自动化的形式针对本地网络执行Deauth渗透测试&#xff0c;或者枚举公共网络。当前版本的autodeauth已在树莓派OS和Kali Linux平台上进行过测试&#xff0c;之后的…

Presidential靶机总结

Presidential靶机渗透总结 靶机下载地址: https://download.vulnhub.com/presidential/Presidential.ova 打开靶机,使用nmap扫描出靶机的ip和所有开放的端口 可以看到靶机开放了80端口和2082端口 使用-sV参数查看详细服务 80端口是http服务 2082端口是ssh服务 那么我们先根据…

双向链表实现简单的增删查改

前言&#xff1a;上次分享了单向链表的增删查改&#xff0c;这次要介绍双向链表的增删查改&#xff0c;其实双向链表也有多种&#xff0c;这次主要介绍结构最复杂但是实现起功能反而最简单的带头双向循环链表&#xff0c;希望我的分享对各位有些许帮助。学习这篇文章的内容最好…

[虾说IT]GIS与三高架构(一)什么是高性能

大家好&#xff0c;我是消失了一个年假的不愿意透露姓名的神秘虾神&#xff0c;这是癸卯兔年虾神的第一个系列&#xff0c;聊聊GIS中的架构设计&#xff0c;不过你如果是做其他架构的也差不多……总之是架构是虾神的本职工作之一&#xff0c;那么培养更多的架构设计者和爱好者&…

基于前馈补偿的PID控制算法及仿真

在高精度伺服控制中&#xff0c;前馈控制可用来提高系统的跟踪性能。经典控制理论中的前馈控制设计是基于复合控制思想&#xff0c;当闭环系统为连续系统时&#xff0c;使前馈环节与闭环系统的传递函数之积为1&#xff0c;从而实现输出完全复现输入。利用前馈控制的思想&#x…

剑指 Offer 05. 替换空格 [C语言]

目录题目思路1代码1结果1思路2代码2结果2该文章只是用于记录考研复试刷题题目 请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20"。 示例 1&#xff1a; 输入&#xff1a;s “We are happy.” 输出&#xff1a;“We%20are%20happy.” 限制&#xff…

pnpm 简介

本文引用自 摸鱼wiki 1. 与npm&#xff0c;yarn性能比较 actioncachelockfilenode_modulesnpmpnpmYarnYarn PnPinstall33.8s20.1s20.3s40.7sinstall✔✔✔2.1s1.4s2.6sn/ainstall✔✔9.1s5.3s7.8s1.7sinstall✔13.5s9.3s14.1s7.7sinstall✔15s17.2s14.2s33.4sinstall✔✔2.5s3s…

2.JSX

JSX(JavaScript XML) 是 JavaScript 的语法扩展&#xff0c;格式上比较像模板语言。React支持JSX 下面两个代码可以实现相同的功能&#xff0c;JSX看起来要简洁一些 目录 1 使用环境 2 React中的JSX 2.1 特殊的属性 2.2 没有子节点的标签 2.3 小括号包裹 3 JSX使用…

vue 实现动态路由

vue-router对象中的addRoutes&#xff0c;用它来动态添加路由配置格式&#xff1a;router.addRoutes([路由配置对象]) this.$router.addRoutes([路由配置对象])举个例子&#xff1a;// 按钮 <button click"hAddRoute">addRoute</button>// 回调 hAddRout…

感染了恶意软件怎么办?

近日&#xff0c;研究人员披露了一种恶意软件&#xff0c;这种恶意软件已经感染了一系列广泛的 Linux 和 Windows 设备。恶意软件攻击事件的频繁发生&#xff0c;除了黑客的恶意攻击外&#xff0c;还有企业内部自身的问题&#xff0c;下面列举了7种容易感染恶意软件的途径和解决…

2023年2月软考高级-信息系统项目管理师【报名入口】

信息系统项目管理师是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目之一&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资…

coresight(六) power requestor

power requestor power requestor属于coresight组件。这个组件用来控制系统的power domain&#xff0c;最多可以控制32个。 如果没有power requestor&#xff0c;通过DAP&#xff0c;只能对整个coresight系统进行上下电操作&#xff0c;但是有了power requestor&#xff0c;可…

2Pai半导体-推出π122E61双通道数字隔离器 智能分压技术 兼容代替Si8622ET-IS

2Pai半导体-推出π122E61双通道数字隔离器 智能分压技术 兼容代替Si8622ET-IS 电路简单、稳定性更高 &#xff0c;具有出色的性能特征和可靠性&#xff0c;整体性能优于光耦和基于其他原理的数字隔离器产品。 产品传输通道间彼此独立&#xff0c;可实现多种传输方向的配置&…

开源工作流可以解决什么问题?

要了解这个问题&#xff0c;就需要先弄清楚相关概念。为什么要使用开源工作流&#xff0c;可以解决什么问题&#xff1f;如果要实现某个业务目标&#xff0c;提高办公协作效率&#xff0c;就可以用开源工作流在多个参与者之间&#xff0c;借助计算机&#xff0c;按照某种预定规…