Android修行手册 - 一篇文章从0到1搞一个Android Studio插件。

news2024/11/20 8:38:52
Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总
游戏脚本-辅助自动化Android控件全解手册再战Android系列
Scratch编程案例软考全系列Unity3D学习专栏
蓝桥系列ChatGPT和AIGC

👉关于作者

专注于Android/Unity和各种游戏开发技巧,以及各种资源分享(网站、工具、素材、源码、游戏等)
有什么需要欢迎底部卡片私我,交流让学习不再孤单

在这里插入图片描述

👉实践过程

最近项目试了一下Android组件化架构,感觉坑还是蛮多的,首先ButterKnife就用不了了,各种R和R2文件的切换就烦死,刚开始看了下ButterKnife Zelezny插件的源码,增加了R文件的选择,感觉在组件化中还是不太好用,最后还是用回了痛苦的findViewById,正好也看了看android studio编写插件的相关知识,今天就和大家一起撸一个findViewById插件!

😜环境配置

Android Studio是基于IntelliJ专门为Android定制的IDE,是没有办法编写IDE的插件的,所以我们首先要下载一个开发Java用的IntelliJ IDEA。具体下载过程就不赘述了,网上教程一大堆,咱们也不是专门开发Java,随便下载一个就好。

下载好打开后,我们看到了一个熟悉的页面,和android studio差不多,选择新建一个项目。左边选择IntelliJ Platform Plugin,右上方Project SDK第一次进入应该是没有配置的。
这里写图片描述
我们选择New,选择一个SDK。这里系统一般Idea的根目录,我们直接确定即可。接下来系统会让你选择一个JDK,也就是java环境,同样也会定位到相应位置,如果没有定位到,我们使用开发Android时JDK的路径就可以了。查看Android Studio中的JDK路径:
查看Android Studio中的JDK路径

配置好环境后,我们就可以愉快的编写插件啦!

😜项目目录结构

新建好的项目目录结构比较简单,没有什么多余的文件。大概长这样。
这里写图片描述
其中com.xxx.xxx刚创建好时是没有的,需要自己建包。

  • .idea: idea的一些配置信息。

  • out: 编译生成的一些.class文件,有点类似于android的build文件夹。

  • resources/META-INF/plugin.xml: 插件的一些描述信息,和我们接下来要写的插件操作“Action”的配置。类似android中的Manifest文件。

  • src: 这里就是我们要写代码的地方啦。

  • .iml: 项目的一些配置信息,一般不用去管,和android的.iml一样。

  • External Libraries: 这个也和android一样,时引用的第三方库。

整体看下来,编写插件代码和我们平时写android代码的时候非常类似。还是非常容易理解的。使用的语言也就是java语言,学习成本很低,但是可以开发出一些非常好玩的插件。

😜配置插件信息

好,各个文件的作用我们已经大概了解了,接下来,我们先来配置一下我们的插件信息,也就是配置我们的resources/META-INF/plugin.xml文件。配置文件里有很详细的英文描述。这里只简单的说一下。
这里写图片描述

  • id: 插件唯一的id。

  • name: 插件显示的名字。

  • version: 插件版本。

  • vendor: 里面分别是你的邮箱,公司网站或个人网站,公司名。

  • description: 插件的描述。

  • change-notes: 更新文档。

  • extensions defaultExtensionNs: 默认依赖的库。

  • actions: “注册”一会编写的动作Action类。

具体填写的东西展示出来是什么样子,大家可以去android studio的插件仓库中看看,对应填写相应的内容就好。如ButterKnife Zelezny填写的配置信息长这样。
这里写图片描述

😜获取资源文件名

好,接下来是大家最喜欢的敲代码了!其实android studio中,每个按钮都相当于一个系统写好的插件,点击这些按钮执行的动作,都是在对应的Action中写好的。我们要做的,就是给IDE添加一个我们自己的按钮,并且写一个做我们想要操作的Action。

怎么做呢?首先,我们在我们创建好的包中new一个Action。
这里写图片描述

点击后出现如下弹窗,让我们配置Action的一些信息。

这里写图片描述

其中,Action Id,Class Name就不多说了,Name为显示给用户的动作名称,Description为操作的描述。

Groups是比较重要的,他代表了我们按钮展示的位置。比如选择GenerateGroup,就是在Generate中显示(Windows中快捷键alt+insert,Mac快捷键control+enter)。还有build、code(显示在菜单栏上build、code按钮中)等等一系列Groups的位置,大家根据需要自己选择。不知道意思的网上查一下就好。
这里写图片描述
右边Actions是选择按钮位置的,First和Last分别为菜单最上方和最下方,点击Actions中的按钮,可以选择在该按钮的下方和上方。我这里模仿了ButterKnife Zelezny选择了GenerateGroup,并且放在了最下方。运行时的效果是这样的:
这里写图片描述
后面的Keyboard Shortcuts中的First和Second就是我们自定义的快捷键了,这里注意快捷键不要和其他系统的快捷键冲突。

配置好后,我们点击ok,就能看到我们新建好的类了。

public class FindViewsAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent anActionEvent) {

    }
}

同时,我们的plugin.xml中也自动帮我们注册好了Action。在action标签中,我们还可以给action增加一个icon字段来设置按钮前面的图标。


一步一步来,Acton已经创建好了,接下来就是写我们的方法了,我们先看一下自动继承的这个AnAction类有什么我们可以用的方法。

看完我就更懵逼了。除了一个自动重写的actionPerformed大概能看出来是按钮被点击的操作外,似乎没有用的上的方法啊。AnActionEvent里也就是有个getProject方法感觉对我们有点用。
这里写图片描述

到这里,我是彻底不知道咋弄了。慢慢来,我们先来捋一捋需求。我们要做的是一键findViewById,首先要获取到光标所在的layout文件,然后读取出layout.xml文件里的所有vieiw的id,最后把再代码中生成全局的变量名,并且绑定findViewById找到的控件。

那第一步就是找到光标所在的layout.xml文件。那肯定要用到光标了。根据需求找方法,我发现anActionEvent中有一个getData方法,这个方法的参数中正好有一个DataKeys.EDITOR,这个似乎是我们想要的啊,得到之后,果然有一个光标的单词caret

@Override
public void actionPerformed(AnActionEvent anActionEvent) {
    Editor editor = anActionEvent.getData(DataKeys.EDITOR);
    if (editor != null) {
        //得到编辑器的光标类
        CaretModel caret = editor.getCaretModel();
    }
}

得到光标之后,我们应该就可以找到我们需要的资源文件了。但是,看了半天方法。。也没找到得到光标所在文件的方法。。没办法,看一下ButterKnifeZelezny的源码吧。

在源码里,我发现了PsiUtilBase.getPsiFileInEditor()这个方法。并且很多文件操作都用到了PsiFile,这个是干什么的呢?还是看一下官网吧。本人英语捉急,不过文档也比较简单,大概还是能看出点东西的。附上官网地址:IDEA插开发工具SDK文档

进入官网后,我们可以左上角搜索一下psi,然后找到psi files,看一下英文全称我们概可以了解到,这是一个表示文件结构的接口,PsiFile是一个基类,里面还有PsiJavaFileXmlFile。那我们获取xml文件中的id,要拿到的肯定是XMLFile这个类。
这里写图片描述
我们再往下翻,其中有两个标题比较重要。分别是,我们怎么得到这个类,还有我们能用这个类做什么。
这里写图片描述
这里写图片描述
我们看到三个比较重要的方法。
psiElement.getContainingFile(): Element我们都知道是元素的意思,通过这个方法,我们大概了解到,用光标获取文件中选中的词,大概率需要用到元素psiElement
FilenameIndex.getFilesByName(project, name,scope): 通过文件名获取文件,这个我们一会肯定也会用到。
psiFile.accept(new PsiRecursiveElementWalkingVisitor()…): 递归递归元素,我们获取id的时候肯定要递归xml文件的,这里IDEA已经帮我们写好了递归的方法。
正好搜索栏下面有一个PSI Elements的介绍,不需要多看,我们只看文档标出来的两个方法。
这里写图片描述
一个是anActionEvent.getData(LangDataKeys.PSI_ELEMENT),一个是psiPfile.findElementAt()

讲道理这里我们应该用第一个方法拿到实体类的,但是第一个方法打印出来的是xml文件的id,所以这里我们只能用第二个方法,根据光标位置找到元素,然后用文件名找到对应的xml文件实体。

PsiFile psiFile = anActionEvent.getData(DataKeys.PSI_FILE);
Editor editor = anActionEvent.getData(DataKeys.EDITOR);
CaretModel caret = editor.getCaretModel();
PsiElement psiElementA = file.findElementAt(offset);
//(R.layout.activity_main)由于光标在‘n’和‘)’中间的时候会打印出')'
//所以这里必须获取两个,然后进行判断。
PsiElement psiElementB = file.findElementAt(offset - 1);
//System.out.println(psiElementA.getText());
//打印一下发现确实打印出了文件名。

接下来我们判断一下这两个element哪个是正确的文件名

//getParent()可以得到元素包括'.'在内的字符串。
//getFirstChil()则可以得到整个字符串开头的字符
String firstChild=psiElementA.getParent().getFirstChild().getText();
if ("R.layout".equals(firstChild)) {
    //psiElementA正确就用A,psiElementB正确就用B。
    //这里只写伪代码了,全部代码之后给出下载。
}

至此,我们得到了xml文件的名字psiElement.getText,把名字末尾拼接上后缀名,就能得到完整的文件名了。

String name = String.format("%s.xml", psiElement.getText());

😜获取xml文件实体对象

最近重构项目实在有点忙,两篇中间也是隔的时间有点久,尽量抽时间多写一下
吧!

我们先来整理一下我们手上有的“资源”。
上一篇文章,我们得到了PsiElement(光标选到的元素)Editor(光标等一写编辑上的操作)xxx.xml(资源文件的名字)

接下来,我们的任务是根据名字取到这个xml文件的实体。

上次我们通过官网,找到了一个方法

FilenameIndex.getFilesByName(project, name, scope);

很显然,这个方法可以通过文件名,得到PsiFile。不过,这个方法除了project和name之外,还需要一个scope。字面意思应该是个范围。我们用编辑器看一下这个方法,第三个参数需要一个GlobalSearchScope

官网搜了一下这个类,似乎并没有搜到介绍它的。我们先看一下这个类有没有什么静态方法可以得到它的实体。
这里写图片描述
看了一下,通过文件得到肯定是没办法了。看来看去,似乎也就module这个东西有点希望。通过编辑器一看,发现有个ModuleUtil,里面有一个findModuleForPsiElement()方法,所需的参数正好是我们有的psiElement。先不管这个能不能行了,反正有参数了,先试试再说。

Module moduleForPsiElement = ModuleUtil.findModuleForPsiElement(psiElement);
GlobalSearchScope searchScope = GlobalSearchScope.moduleScope(moduleForPsiElement);
Project project = anActionEvent.getData(DataKeys.PROJECT);
//得到所有名字为name的文件
PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, name, searchScope);
for (PsiFile file : psiFiles) {
    //得到的psiFiles长度为1,打印一下文件名name和内容text,发现名字为我们需要的xxxx.xml,内容也和文件里的内容一致。
    System.out.println(file.getName());
    System.out.println(file.getText());
}

通过这个方法,我们得到了我们要的xml文件实体类。


😜获取类名和id一一对应的对象集合

接下来就是遍历里面的id了。为了避免有bug,我们先多放几个控件,包括viewgroup的嵌套,还有include和自定义view。大概长这样。

<!-- 伪代码去除了无用代码,只保留了id -->
<!-- activity_main.xml -->
<RelativeLayout
    android:id="@+id/rlVidwGroup">

    <TextView
        android:id="@+id/tvHelloWorld"/>

    <ImageView
        android:id="@+id/ivIcon"/>

    <LinearLayout
        android:id="@+id/llViewGroup"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvInner"/>

        <include
            layout="@layout/include_plugin_test"/>

    </LinearLayout>

    <com.jarvis.myapplication.app.Custom
        android:id="@+id/custom"/>

</RelativeLayout>

<!-- include_plugin_test.xml -->
<LinearLayout
    android:id="@+id/llIncludeViewGroup"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tvInclude"/>

</LinearLayout>

不知道大家还记不记得,我们上一次再官网找到了一个能够递归遍历psiFile内元素的方法
这里写图片描述

其中参数PsiRecursiveElementWalkingVisitor有很多子类,其中就有XmlRecursiveElementVisitor,看名字正是我们需要的。(其实这里用PsiRecursiveElementWalkingVisitor也行,只不过需要把返回值手动强转成XML文件的元素)。我们调用一下这个方法,并且重写我们需要的方法。

很明显。我们可能用到的是visitXmlAttribute和visitXmlTag。但是由于我们得到id时还需要得到它对应的类,以便于我们生成参数类型,所以这里我们必须用visitXmlTag得到标签类。并且创建一个bean类,里面暂时存储我们一会得到的类名和id。

public class ResIdBean {
    String name;
    String id;
    public ResIdBean(String name, String id) {
        this.name = name;
        this.id = id;
    }
}

我们先考虑一般情况,也就是没有include的时候。这时候比较简单,类名就是标签名。自定义控件打出来的是完整的类名。

resFile.accept(new XmlRecursiveElementVisitor(true) {
    @Override
    public void visitXmlTag(XmlTag tag) {
        super.visitXmlTag(tag);
        String className = tag.getName();
    }
});

接下来我们要得到控件的id。通过tag获取名为”android:id”的attribute属性。然后分割一下字符串就可以得到对应的id了。我们打印一下,确实是我们想要的值,我们把类名和id存在一个List集合里备用。

ArrayList<ResIdBean> resIdBeans = new ArrayList<>();
resFile.accept(new XmlRecursiveElementVisitor(true) {
    @Override
    public void visitXmlTag(XmlTag tag) {
        super.visitXmlTag(tag);
        XmlAttribute attribute = tag.getAttribute("android:id");
        if (attribute != null) {
            String idValue = attribute.getValue();
            if (idValue != null && idValue.startsWith("@+id/")) {
                String[] split = idValue.split("/");
                String className = tag.getName();
                String id = split[1];
                System.out.println(className + "---" + id);
                resIdBeans.add(new ResIdBean(className, id));
            }
        }
    }
});

接下来就是获取include标签中的类名和id了。由于include中只有xml文件的名字,所以,和之前一样,我们需要先得到xml文件的名字,然后得到xml文件的实体类,在进行同样的操作得到类名和id,如果include中还有include,我们还需要进行这样的操作。显然这是一个递归。

我们完善一下代码,简单封装一下之前写的方法。如果tagName为include就继续通过文件名找到文件,然后遍历获得id,如果不是include就放入集合中。封装好的代码大概是这样。

//伪代码,需要根据前面讲的自行修改。
private void getResIdBeans(PsiFile psiFile, ArrayList<ResIdBean> container) {
    psiFile.accept(new XmlRecursiveElementVisitor(true) {
        super.visitXmlTag(tag);
        if (tag.getName().equals("include")) {
            String xmlName = String.format("%s.xml", name);
            getResIdBeans(include, container);
            PsiFile fileByName = getFileByName(psiFile, xmlName);
            getResIdBeans(fileByName, container);
        }else{
            container.add(new ResIdBean(className, id));
        }
    }
}

最后我们往这个方法中传入的ArrayList<ResIdBean> container里面就放好了我们存的ResIdBean了。

现在我们已经得到了我们选中xml文件中所有的id集合了。

😜打印全局ID变量

好,今天写一下Android Studio编写插件的第三篇。

上一篇我们已经得到了类名和id一一对应的实体类。接下来就是把得到的这些参数写到我们的编辑器中了。

首先,我们要得到我们所在类的psiClass对象。我们之前有psiFile对象,但是如果往这个对象中添加元素的话,是会添加到文件最开始的,虽然有addBefore和addAfter方法,但是这样获取参数比较麻烦。所以,我们需要获取到psiClass这个层级为“类”的对象。
在这里插入图片描述

通过官网,我们可以看到这两个方法可以通过name得到class。因为我们已经有确定的psiFile了,所以这里我们用第二个方法。name通过psiFile.getName()方法就可以得到。但是需要注意,这里得到的name带有“.java”得到是文件名,我们得手动去掉.java,来得到类名。

GlobalSearchScope globalSearchScope = GlobalSearchScope.fileScope(psiFile);
String fullName = psiFile.getName();
String className = fullName.split("\\.")[0];
PsiClass psiClass = PsiShortNamesCache.getInstance(psiFile.getProject()).getClassesByName(className, globalSearchScope)[0];

得到psiClass验证一下没问题后,我们就可以开始把之前得到的类和id打印在这个类中了。

PsiElementFactory psiElementFactory = PsiElementFactory.SERVICE.getInstance(psiElement.getProject());
for (ResIdBean resIdBean : resIdBeans) {
//第一个参数为变量的字符串,第二个参数为写变量的所在类。
PsiField fieldFromText = psiElementFactory.createFieldFromText(
    "private"
    + " " 
    + resIdBean.getName() 
    + " "
    + resIdBean.getId() 
    + ";"
    , psiClass);
    psiClass.add(fieldFromText);
}

这里我又遇到了一个问题,当我add元素的时候,编辑器竟然报错了。看了一下官网,发现IntellJ是有一个读写锁的。
在这里插入图片描述

这里我理解的是。
写操作需要在ApplicationManager.getApplication().runWriteAction()方法中进行。可能是我英语太差理解的有问题。。我试着写了一下。运行,发现又报错了。
在这里插入图片描述

根据提示信息,我又用了一下WriteCommandAction()这个方法。

new WriteCommandAction(psiFile.getProject(), psiFile) {
    @Override
    protected void run(@NotNull Result result) throws Throwable {
        addFieldIds();
    }
}.execute();

呃,这次倒是成功了。不知道为什么runWriteAction()方法不行,英语好的小伙伴可以告知一下。

到这里,我们已经成功的打印了所有id对应的全局变量。如果没有开启自动导包功能的话,我们可以在类名前面加上包名,大部分都是android.widget包中的控件,少数其他包中控件,大家自己定制一下就好,这里就不多介绍了。


😜打印findViewById方法

接下来,我们要开始创建方法了。我希望方法是这样的。
在这里插入图片描述

因为系统是给出了createMethodFromText()方法的,所以我们不需要换行符和多余的字符串。定好了我们要生成的方法后,我们来拆一下,把换行删掉。大概是这样的。

private void findViews() { id = (cast) findViewById(R.id.id); }

然后我们用一个StringBuilder()拼接一下我们想要的字符串。然后打印在类中。

StringBuilder method = new StringBuilder();
method.append("private void findViews(){");
PsiElementFactory psiElementFactory = PsiElementFactory.SERVICE.getInstance(psiElement.getProject());
for (ResIdBean resIdBean : resIdBeans) {
    PsiField fieldElement = psiElementFactory.createFieldFromText(field, psiClass);
    psiClass.add(fieldElement);
    method.append(resIdBean.getId())
        .append(" = ")
        .append("(")
        .append(resIdBean.getName())
        .append(")")
        .append("findViewById(")
        .append("R.id.")
        .append(resIdBean.getId())
        .append(");");
}
method.append("}");
PsiMethod methodElement = psiElementFactory.createMethodFromText(method.toString(), psiClass);
psiClass.add(methodElement);

验证一下。果然都正常打印出来了。松了一口气。


😜判断所在类是否为Activity

接下来,我们判断一下所在的类,如果是Activity,那么这个方法就没问题,如果是Fragment,我们则需要在findViews()方法中加一个view的参数。

判断是fragment还是activity只需要看一下psiClass继承的是哪个psiClass就行了。之前看官网上有两个方法,第一个方法可以根据类名的全称(包括包名)来得到psiClass。然后通过psiClass的isInheritor()方法,就可以判断继承关系了。

GlobalSearchScope scope = GlobalSearchScope.allScope(psiFile.getProject());
PsiClass activityClass = JavaPsiFacade.getInstance(psiFile.getProject()).findClass(
                        "android.app.Activity",scope);
PsiClass fragmentClass = JavaPsiFacade.getInstance(psiFile.getProject()).findClass(
                        "android.app.Fragment", scope);
PsiClass supportFragmentClass = JavaPsiFacade.getInstance(psiFile.getProject()).findClass(
                        "android.support.v4.app.Fragment", scope);
                        if (activityClass != null && psiClass.isInheritor(activityClass, false)) {
    //当前类为activity       
}else if (fragmentClass != null && psiClass.isInheritor(fragmentClass, false)
|| supportFragmentClass != null && psiClass.isInheritor(supportFragmentClass, false)) {
    //当前类为fragment
}

具体在加个View参数的方法我就不写了,只需要在创建字符串方法的时候,多拼接一下就可以了。


😜判断变量和方法是否重复

接下来,我们还需要判断一下全局变量中是否已经创建了某个id,如果创建了,则不重复创建。我们只需要用变量名判断就可以了

if (psiClass.findFieldByName(resIdBean.getId(), false) == null) {
    //没创建过
}else{
    //创建过
}

还有findViews方法也需要判断一下,这个比较麻烦,我知道的只能是先删除之前的方法体,然后再重新生成一遍方法。

PsiMethod[] methods = psiClass.findMethodsByName("findViews", false);
PsiMethod findViewsMethod = methods.length > 0 ? methods[0] : null;
if (findViewsMethod != null) {
    //已经有这个方法了
    PsiCodeBlock body = findViewsMethod.getBody();
    if (body != null) {
        StringBuilder codeBlock = new StringBuilder(body.getText());
        body.delete();
        codeBlock.insert(codeBlock.length() - 1, method.toString());   
        findViewsMethod.add(psiElementFactory.createCodeBlockFromText(codeBlock.toString(), findViewsMethod));
    }
}else{
    //没生成过方法
}

简单的封装处理一下,生成findViewById的插件差不多就完成了~~

最后,我们点击build,prepare plugin module for deployment,就可以看到插件生成在我们的根目录啦。
在这里插入图片描述

使用的时候也很简单,进入setting中的plugin页面,点击install plugin from disk,然后选择刚才生成的jar包就可以使用了~~
在这里插入图片描述

不知道大家看的时候有没有一脸懵逼,我写的没有大量的代码,主要还是思路,希望看到的朋友能有收获。自己也会努力提高写作水平的~~

好了,大家根据自己的想法,编写适合自己的插件吧!

👉其他

📢作者:小空和小芝中的小空
📢转载说明-务必注明来源:https://zhima.blog.csdn.net/
📢这位道友请留步☁️,我观你气度不凡,谈吐间隐隐有王者霸气💚,日后定有一番大作为📝!!!旁边有点赞👍收藏🌟今日传你,点了吧,未来你成功☀️,我分文不取,若不成功⚡️,也好回来找我。

温馨提示点击下方卡片获取更多意想不到的资源。
空名先生

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

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

相关文章

Web安全漏洞分析-XSS(上)

随着互联网的迅猛发展&#xff0c;Web应用的普及程度也愈发广泛。然而&#xff0c;随之而来的是各种安全威胁的不断涌现&#xff0c;其中最为常见而危险的之一就是跨站脚本攻击&#xff08;Cross-Site Scripting&#xff0c;简称XSS&#xff09;。XSS攻击一直以来都是Web安全领…

一次Apollo Client升级导致的生产404 Not Found问题排查记录

概述 本文记录一次升级Apollo Client组件到1.7.0后遇到的重大生产事故。只想看结论的&#xff0c;可直接快进到文末。实际上&#xff0c;第一句话就是一个结论。 另&#xff0c;本文行文思路事后看起来可行略显思路清晰&#xff0c;实际上排查生产问题时如无头苍蝇&#xff0…

初学者如何入门深度学习:以手写数字字符识别为例看AI 的学习路径,一图胜千言!

文章大纲 深度神经网络机器学习,深度学习,数据发掘之间的关系理解深度神经网络最好的可视化工具深度学习基础概念能解决神马种类的问题?卷积池化以手写字符识别为例讲述深度学习的分类问题MNIST 数据集简介初学者入门 :生成式 AI -- generative-ai-for-beginners参考文献与…

【Web端CAD/CAE文字标注】webgl+canvas 2d实现文字标注功能

一、需求背景 在CAD/CAE领域经常会遇到显示节点编号这种需求&#xff0c;效果如下图&#xff1a; 本文介绍如何在WebGL中实现文字的显示&#xff0c;对于如何在OpenGL中实现请绕路。 二、实现原理 Canvas是HTML5提供的元素&#xff0c;用于在网页上绘制图形&#xff0c;其支…

计算机网络(超详解!) 第二节 物理层(上)

1.物理层的基本概念 物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输媒体。 物理层的作用是要尽可能地屏蔽掉不同传输媒体和通信手段的差异。 用于物理层的协议也常称为物理层规程(procedure)。 2.物理层的主要任务 主要…

Unity版本使用情况统计(更新至2023年10月)

本期UWA发布的内容是第十三期Unity版本使用统计&#xff0c;统计周期为2023年5月至2023年10月&#xff0c;数据来源于UWA网站&#xff08;www.uwa4d.com&#xff09;性能诊断提测的项目。希望给Unity开发者提供相关的行业趋势&#xff0c;了解近半年来哪些Unity版本的使用概率更…

Steps步骤条(antd-design组件库)简单用法

1.Steps步骤条 引导用户按照流程完成任务的导航条。 2.何时使用 当任务复杂或者存在先后关系时&#xff0c;将其分解成一系列步骤&#xff0c;从而简化任务。 组件代码来自&#xff1a; 步骤条 Steps - Ant Design 3.本地验证前的准备 参考文章【react项目antd组件-demo:hello-…

11.兔子生崽问题【2023.11.26】

1.问题描述 有一对兔子&#xff0c;从出生后第3个月起每个月都生一对兔子&#xff0c;小兔子长到第三个月后每个月又生一对兔子&#xff0c;假如兔子都不死&#xff0c;问 第二十个月的兔子对数为多少对&#xff1f; 2.解决思路 3.代码实现 #include<stdio.h> int mai…

单相直流电表和单相智能电表有哪些区别?

在众多的智能电表中&#xff0c;单相智能电表已成为家庭用电、工业用电等领域的重要组成部分。与此同时&#xff0c;单相直流电表也因其特性在某些特定场合受到关注。下面就来讲讲两者都有哪些区别&#xff0c;一起来看下吧&#xff01; 一、工作原理及性能差异 1.单相直流电表…

微信小程序本地和真机调试文件上传成功但体验版不成功

可能是微信小程序ip白名单的问题&#xff0c;去微信公众平台&#xff08;小程序&#xff09;上设置小程序的ip白名单 1、在本地中取消不校验 然后在本地去上传文件&#xff0c;就会发现控制台报错了&#xff0c;会提示一个https什么不在ip白名单&#xff0c;复制那个网址 2、…

5 面试题--redis

伪客户端&#xff1a; 伪客户端的 fd 属性值为 -1&#xff1b;伪客户端处理的命令请求来源于 AOF ⽂件或者 Lua 脚本&#xff0c;⽽不是⽹络&#xff0c;所以这种客户端不需要套接字连接&#xff0c;⾃然也不需要记录套接字描述符。⽬前 Redis 服务器会在两个地⽅ ⽤到伪客户端…

西工大网络空间安全学院计算机系统基础实验一(9, 10, 11, 12, 13)

还是那句话&#xff0c;专心做好你自己的&#xff0c;老老实实把基础打好&#xff0c;不要被其他人带跑节奏&#xff0c;不要跟他打&#xff0c;跟着这系列博客&#xff0c;稳扎稳打一步一步来。即使你VMware workstation没下载好&#xff0c;即使你Ubuntu虚拟机没配好&#xf…

nacos配置变更导致logback日志异常

问题背景: 线上的服务突然内存爆满&#xff0c;查服务器突然发现&#xff0c;日志全部打印到了/tmp/tomcat.xxx.port目录下&#xff0c;后来对应操作时间&#xff0c;和nacos修改配置是同一时间发生的&#xff0c;但是疑惑的点是&#xff0c;nacos配置变更为什么会引起logback的…

MySQL与其他数据库产品的比较,优势在哪里?

作为数据库管理领域的博主作家&#xff0c;我深知数据库在软件开发和数据管理中的重要性。在当今众多的数据库产品中&#xff0c;MySQL作为一种流行的开源关系型数据库管理系统&#xff0c;具有许多优势和特点。下面&#xff0c;我将通过对与其他数据库产品的比较以及MySQL的优…

【华为数通HCIP | 网络工程师】821刷题日记-IS-IS(2)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

mac截图Snagit 中文介绍

1.超越普通的屏幕截图 TechSmith Snagit 是唯一具有内置高级图像编辑和屏幕录制功能的屏幕捕获软件。因此&#xff0c;您可以在一个程序中轻松创建高质量的图像和视频。 2.最后&#xff0c;屏幕捕获软件可以完成您所做的一切 快速解释一个过程如果您正在努力清楚地沟通&…

电子学会C/C++编程等级考试2022年12月(三级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:鸡兔同笼 一个笼子里面关了鸡和兔子(鸡有2只脚,兔子有4只脚,没有例外)。已经知道了笼子里面脚的总数a,问笼子里面至少有多少只动物,至多有多少只动物。 时间限制:1000 内存限制:65536输入 一行,一个正整数a (a < 327…

HarmonyOS——UI开展前的阶段总结

当足够的了解了HarmonyOS的相关特性之后&#xff0c;再去介入UI&#xff0c;你会发现无比的轻松&#xff0c;特别当你有着其他的声明式UI开发的经验时&#xff0c;对于HarmonyOS的UI&#xff0c;大致一扫&#xff0c;也就会了。 如何把UI阐述的简单易懂&#xff0c;又能方便大…

全局异常处理类

全局异常处理类 创建步骤 定义一个自己的全局错误处理类GlobalExceptionHandler创建一个ExceptionHandler类&#xff0c;主要是用ControllerAdvice和 ExceptionHandler处理错误信息 以下说明各个注解的作用&#xff1a; ControllerAdvice(annotations {RestController.class…

【C++】单链表——单链表的基本操作

1、单链表的定义 由于顺序表的插入删除操作需要移动大量的元素&#xff0c;影响了运行效率&#xff0c;因此引入了线性表的链式存储——单链表。单链表通过一组任意的存储单元来存储线性表中的数据元素&#xff0c;不需要使用地址连续的存储单元&#xff0c;因此它不要求在逻辑…