我的编程空间,编程开发者的网络收藏夹
学习永远不晚

怎样浅析ButterKnife

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

怎样浅析ButterKnife

怎样浅析ButterKnife,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

不管是Android开发的老司机也好,新司机也罢,想必大家都对findViewById这种样板代码感到了厌倦,特别是进行复杂的UI界面开发的时候,这种代码就会显的非常的臃肿,既影响开发时的效率,又影响美观。
俗话说,不想偷懒的程序猿不叫工程师,那有什么方法可以让我们写这样的代码更加的有效率呢?

使用依赖注入框架

如果你不想写那些无聊的样板代码,那么你可以尝试一下现有的依赖注入库。ButterKnife作为Jake Wharton大神写的开源框架,号称在编译期间就可以实现依赖注入,没有用到反射,不会降低程序性能等。那么问题来了,它到底是怎么做到的呢?

初探ButterKnife

ButterKnife是Jake Wharton写的开源依赖注入框架,它和Android Annotations比较类似,都是用到了Java Annotation Tool来在编译期间生成辅助代码来达到View注入的目的。

注解处理器是Java1.5引入的工具,它提供了在程序编译期间扫描和处理注解的能力。它的原理就是在编译期间读取Java代码,解析注解,然后动态生成Java代码。下图是Java编译代码的流程,可以看到,我们的注解处理器的工作在Annotation Processing阶段,最终通过注解处理器生成的代码会和源代码一起被编译成Java字节码。不过比较遗憾的是你不能修改已经存在的Java文件,比如在已经存在的类中添加新的方法,所以通过Java Annotation Tool只能通过辅助类的方式来实现View的依赖注入,这样会略微增加项目的方法数和类数,不过只要控制好,不会对项目有太大的影响

怎样浅析ButterKnife

ButterKnife在业务层的使用我就不介绍了,各位老司机肯定是轻车熟路。假如是我们自己写类似于ButterKnife这样的框架,那么我们的思路是这样:定义注解,扫描注解,生成代码。同时,我们需要用到以下这几个工具:JavaPoet(你当然可以直接用Java Annotation Tool,然后直接通过字符串拼接的方式去生成java源码,如果你生无可恋的话),Java Annotation Tool以及APT插件。为了后续更好的阅读ButterKnife的源码,我们先来介绍一下JavaPoet的基础知识。

JavaPoet生成代码

JavaPoet是一个可以生成.java源代码的开源项目,也是出自JakeWharton之手,我们可以结合注解处理器在程序编译阶段动态生成我们需要的代码。先介绍一个使用JavaPoet最基本的例子:
怎样浅析ButterKnife
其中:

  • MethodSpec:代表一个构造函数或者方法声明

  • TypeSpec:代表一个类、接口或者枚举声明

  • FieldSpec:代表一个成员变量声明

  • JavaFile:代表一个顶级的JAVA文件

运行结果:
怎样浅析ButterKnife

是不是很神奇?我们的例子只是把生成的代码写到了输出台,ButterKnife通过Java Annotation Tool的Filer可以帮助我们以文件的形式输出JAVA源码。问:那如果我要生成下面这段代码,我们会怎么写?

怎样浅析ButterKnife

很简单嘛,依葫芦画瓢,只要把MethodSpec替换成下面这段:
怎样浅析ButterKnife

然后代码华丽的生成了:
怎样浅析ButterKnife

唉,等等,好像哪里不对啊,生成代码的格式怎么这么奇怪!难道我要这样写嘛:
怎样浅析ButterKnife

这样写肯定是能达到我们的要求,但是未免也太麻烦了一点。其实JavaPoet提供了一个addStatement接口,可以自动帮我们换行以及添加分号,那么我们的代码就可以写成这个样子:
怎样浅析ButterKnife

生成的代码:
怎样浅析ButterKnife

好吧,其实格式也不是那么好看对不对?而且还要addStatement还需要夹杂addCode一起使用。为什么写个for循环都这么难(哭泣脸)。其实JavaPoet早考虑到这个问题,它提供了beginControlFlow() + endControlFlow()两个接口提供换行和缩进,再结合负责分号和换行的addStatement(),我们的代码就可以写成这样子:
怎样浅析ButterKnife
生成的代码相当的顺眼:
怎样浅析ButterKnife
其实JavaPoet还提供了很多有用的接口来帮我们更方便的生成代码。更加详细的用法请访问https://github.com/square/javapoet,这里我就不赘述了。

Java Annotation Tool

那么ButterKnife又是怎么通过Java Annotation Tool来生成我们的辅助代码呢?让我们以ButterKnife最新版本8.4.0的源代码为例。假如是我们自己写ButterKnife这样的框架,那么第一步肯定得先定义自己的注解。在ButterKnife源码的butterknife-annotations包中,我们可以看到ButterKnife自定义的所有的注解,如下图所示。
怎样浅析ButterKnife
有了自定义注解,那我们的下一步就是实现自己的注解处理器了。我们结合ButterKnifeButterKnifeProcessor类来学习一下注解处理器的相关知识。为了实现自定义注解处理器,必须先继承AbstractProcessor类。ButterKnifeProcessor通过继承AbstractProcessor,实现了四个方法,如下图所示:
怎样浅析ButterKnife
怎样浅析ButterKnife

  • init(ProcessingEnvironment env)
    通过输入ProcessingEnvironment参数,你可以在得到很多有用的工具类,比如ElementsTypesFiler等。
    Elements是可以用来处理Element的工具类,可以理解为Java Annotation Tool扫描过程中扫描到的所有的元素,比如包(PackageElement)、类(TypeElement)、方法(ExecuteableElement)等
    Types是可以用来处理TypeMirror的工具类,它代表在JAVA语言中的一种类型,我们可以通过TypeMirror配合Elements来判断某个元素是否是我们想要的类型
    Filer是生成JAVA源代码的工具类,能不能生成java源码就靠它啦

  • getSupportedAnnotationTypes()
    代表注解处理器可以支持的注解类型,由前面的分析可以知道,ButterKnife支持的注解有BindViewOnClick等。

  • getSupportedSourceVersion()
    支持的JDK版本,一般使用SourceVersion.latestSupported(),这里使用Collections.singleton(OPTION_SDK_INT)也是可以的。

  • process(Set<? extends TypeElement> elements, RoundEnvironment env)
    process是整个注解处理器的重头戏,你所有扫描和处理注解的代码以及生成Java源文件的代码都写在这里面,这个也是我们将要重点分析的方法。

ButterKnifeProcessorprocess方法看起来很简单,实际上做了很多事情,大致可以分为两个部分:

  1. 扫描所有的ButterKnife注解,并且生成以TypeElement为Key,BindingSet为键值的HashMap。TypeElement我们在前面知道属于类或者接口,BindingSet用来记录我们使用JavaPoet生成代码时的一些参数,最终把该HashMap返回。这些逻辑对应于源码中的findAndParseTargets(RoundEnvironment env)方法

  2. 生成辅助类。辅助类以_ViewBinding为后缀,比如在MainActivity中使用了ButterKnife注解,那么最终会生成MainActivity_ViewBinding辅助类。MainActivity_ViewBinding类中最终会生成对应于@BindView的findViewById等代码。
    第一步,我们先来分析findAndParseTargets(RoundEnvironment env)源码。由于方法太长,而且做的事情都差不多,我们只需要分析一小段即可

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();    --- 省略部分代码---    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {      if (!SuperficialValidation.validateElement(element)) continue;      try {        //遍历所有被BindView注解的类        parseBindView(element, targetClassMap, erasedTargetNames);      } catch (Exception e) {        logParsingError(element, BindView.class, e);      }    }    --- 省略部分代码---     // Try to find a parent binder for each.    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {      TypeElement parentType = findParentType(entry.getKey(), erasedTargetNames);      if (parentType != null) {        BindingClass bindingClass = entry.getValue();        BindingClass parentBindingClass = targetClassMap.get(parentType);        bindingClass.setParent(parentBindingClass);      }    }    return targetClassMap;  }

遍历找到被注解的Element之后,通过parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames)方法去解析各个Element。在parseBindView方法中,首先会去检测被注解的元素是不是View或者Interface,如果满足条件则去获取被注解元素的注解的值,如果相应的的BindingSet.Builder没有被绑定过,那么通过getOrCreateBindingBuilder方法生成或者直接从targetClassMap中获取(为了提高效率,生成的BindingSet.Builder会被存储在targetClassMap中)。getOrCreateBindingBuilder方法比较简单,我就不贴代码了,生成的BindingSet.Builder会记录一个值binderClassNameButterKnife最终会根据binderClassName作为辅助类的类名。

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,      Set<TypeElement> erasedTargetNames) {    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();    // Start by verifying common generated code restrictions.    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)        || isBindingInWrongPackage(BindView.class, element);    // Verify that the target type extends from View.    TypeMirror elementType = element.asType();    --- 省略类型校验逻辑的代码---     // 获取注解的值    int id = element.getAnnotation(BindView.class).value();    BindingSet.Builder builder = builderMap.get(enclosingElement);    if (builder != null) {      ViewBindings viewBindings = builder.getViewBinding(getId(id));      if (viewBindings != null && viewBindings.getFieldBinding() != null) {        FieldViewBinding existingBinding = viewBindings.getFieldBinding();        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",            BindView.class.getSimpleName(), id, existingBinding.getName(),            enclosingElement.getQualifiedName(), element.getSimpleName());        return;      }    } else {       //如果没有绑定过,那么通过该方法获得对应的builder并且返回。这里的targetClassMap会存储已经生成的builder,必要的时候提高效率         builder = getOrCreateBindingBuilder(builderMap, enclosingElement);    }    String name = element.getSimpleName().toString();    TypeName type = TypeName.get(elementType);    boolean required = isFieldRequired(element);    builder.addField(getId(id), new FieldViewBinding(name, type, required));    erasedTargetNames.add(enclosingElement);  }

parseBindView以及findAndParseTargets的解析工作完成后,所有的解析结果都会存放在targetClassMap中作为结果返回。我们现在来看process第二步的处理过程:遍历targetClassMap中所有的builder,并且通过Filer生成JAVA源文件。

---代码省略--- for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {      TypeElement typeElement = entry.getKey();      BindingSet binding = entry.getValue();      JavaFile javaFile = binding.brewJava(sdk);      try {        javaFile.writeTo(filer);      } catch (IOException e) {        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());      }    }

那么生成的代码都长什么样子呢?让我们打开BindingSetbrewJava(int sdk)方法一探究竟。

  JavaFile brewJava(int sdk) {    return JavaFile.builder(bindingClassName.packageName(), createType(sdk))        .addFileComment("Generated code from Butter Knife. Do not modify!")        .build();  }

怎样浅析ButterKnife
纳尼,竟然这么简单?我们观察到JavaFile的静态方法builder(String packageName, TypeSpec typeSpec)第二个参数为TypeSpec,前面提到过TypeSpec是JavaPoet提供的用来生成类的接口,打开createType(int sdk),霍霍,原来控制将要生成的代码的逻辑在这里:

private TypeSpec createType(int sdk) {     // 生成类名为bindingClassName的类    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())        .addModifiers(PUBLIC);     //ButterKnife的BindingSet初始化都是通过BindingSet的build方法初始化的,所以isFinal一般被初始化为false    if (isFinal) {      result.addModifiers(FINAL);    }    if (parentBinding != null) {       //如果有父类的话,那么注入该子类的时候,也会顺带注入其父类      result.superclass(parentBinding.bindingClassName);    } else {       //如果没有父类,那么实现Unbinder接口(所以所有生成的辅助类都会继承Unbinder接口)      result.addSuperinterface(UNBINDER);    }     //增加一个变量名为target,类型为targetTypeName的成员变量    if (hasTargetField()) {      result.addField(targetTypeName, "target", PRIVATE);    }    if (!constructorNeedsView()) {      // Add a delegating constructor with a target type + view signature for reflective use.      result.addMethod(createBindingViewDelegateConstructor(targetTypeName));    }    //核心方法,生成***_ViewBinding方法,我们控件的绑定比如findViewById之类的方法都在这里生成    result.addMethod(createBindingConstructor(targetTypeName, sdk));    if (hasViewBindings() || parentBinding == null) {     //生成unBind方法      result.addMethod(createBindingUnbindMethod(result, targetTypeName));    }    return result.build();  }

接下来让我们看看核心语句createBindingConstructor*_ViewBinding方法内到底干了什么:

private MethodSpec createBindingConstructor(TypeName targetType, int sdk) {    //方法修饰符为PUBLIC,并且添加注解为UiThread    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()        .addAnnotation(UI_THREAD)        .addModifiers(PUBLIC);    if (hasMethodBindings()) {       //如果有OnClick注解,那么添加方法参数为targetType final target      constructor.addParameter(targetType, "target", FINAL);    } else {       //如果没有OnClick注解,那么添加方法参数为targetType target      constructor.addParameter(targetType, "target");    }    if (constructorNeedsView()) {       //如果有注解的View控件,那么添加View source参数      constructor.addParameter(VIEW, "source");    } else {      //否则添加Context source参数      constructor.addParameter(CONTEXT, "context");    }    if (hasUnqualifiedResourceBindings()) {      constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)          .addMember("value", "$S", "ResourceType")          .build());    }     //如果有父类,那么会根据不同情况调用不同的super语句    if (parentBinding != null) {      if (parentBinding.constructorNeedsView()) {        constructor.addStatement("super(target, source)");      } else if (constructorNeedsView()) {        constructor.addStatement("super(target, source.getContext())");      } else {        constructor.addStatement("super(target, context)");      }      constructor.addCode("\n");    }     //如果有绑定过Field(不一定是View),那么添加this.target = target语句    if (hasTargetField()) {      constructor.addStatement("this.target = target");      constructor.addCode("\n");    }    if (hasViewBindings()) {      if (hasViewLocal()) {        // Local variable in which all views will be temporarily stored.        constructor.addStatement("$T view", VIEW);      }      for (ViewBindings bindings : viewBindings) {        //View绑定的最常用,也是最关键的语句,生成类似于findViewById之类的代码        addViewBindings(constructor, bindings);      }            for (FieldCollectionViewBinding binding : collectionBindings) {        constructor.addStatement("$L", binding.render());      }      if (!resourceBindings.isEmpty()) {        constructor.addCode("\n");      }    }---省略一些绑定resource资源的代码---}

addViewBindings我们简单看看就好。需要注意的是:

  • 因为生成代码时确实要根据不同条件来生成不同代码,所以使用了CodeBlock.Builder接口。CodeBlock.Builder也是JavaPoet提供的,该接口提供了类似字符串拼接的能力

  • 生成了类似于target.fieldBinding.getName() = .findViewById(bindings.getId().code)或者target.fieldBinding.getName() = .findRequiredView(bindings.getId().code)之类的代码,我们可以清楚的看到,这里没有用到反射,所以被@BindView注解的变量的修饰符不能为private。

private void addViewBindings(MethodSpec.Builder result, ViewBindings bindings) {  if (bindings.isSingleFieldBinding()) {    // Optimize the common case where there's a single binding directly to a field.    FieldViewBinding fieldBinding = bindings.getFieldBinding();        CodeBlock.Builder builder = CodeBlock.builder()        .add("target.$L = ", fieldBinding.getName());    boolean requiresCast = requiresCast(fieldBinding.getType());    if (!requiresCast && !fieldBinding.isRequired()) {      builder.add("source.findViewById($L)", bindings.getId().code);    } else {      builder.add("$T.find", UTILS);      builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");      if (requiresCast) {        builder.add("AsType");      }      builder.add("(source, $L", bindings.getId().code);      if (fieldBinding.isRequired() || requiresCast) {        builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));      }      if (requiresCast) {        builder.add(", $T.class", fieldBinding.getRawType());      }      builder.add(")");    }    result.addStatement("$L", builder.build());    return;  }  List<ViewBinding> requiredViewBindings = bindings.getRequiredBindings();  if (requiredViewBindings.isEmpty()) {    result.addStatement("view = source.findViewById($L)", bindings.getId().code);  } else if (!bindings.isBoundToRoot()) {    result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,        bindings.getId().code, asHumanDescription(requiredViewBindings));  }  addFieldBindings(result, bindings);   // 监听事件绑定  addMethodBindings(result, bindings);}

addMethodBindings(result, bindings)实现了监听事件的绑定,也通过MethodSpec.Builder来生成相应的方法,由于源码太长,这里就不贴源码了。

小结:createType方法到底做了什么?

  • 生成类名为className_ViewBing的类

  • className_ViewBing实现了Unbinder接口(如果有父类的话,那么会调用父类的构造函数,不需要实现Unbinder接口)

  • 根据条件生成className_ViewBing构造函数(实现了成员变量、方法的绑定)以及unbind方法(解除绑定)等

如果简单使用ButterKnife,比如我们的MainActivity长这样
怎样浅析ButterKnife

那么生成的最终MainActivity_ViewBinding类的代码就长下面这样子,和我们分析源码时预估的样子差不多。
怎样浅析ButterKnife

需要注意的是,Utils.findRequiredViewAsTypeUtils.findRequiredViewUtils.castView的区别。其实Utils.findRequiredViewAsType就是Utils.findRequiredView(相当于findViewById)+Utils.castView(强制转型,class类接口)。

  public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,Class<T> cls) {    View view = findRequiredView(source, id, who);    return castView(view, id, who, cls);  }

MainActivity_ViewBinding类的调用过程就比较简单了。MainActivity一般会调用ButterKnife.bind(this)来实现View的依赖注入,这个也是ButterKnife和Google亲儿子AndroidAnnotations的区别:AndroidAnnotations不需要自己手动调用ButterKnife.bind(this)等类似的方法就可以实现View的依赖注入,但是让人蛋疼的是编译的时候会生成一个子类,这个子类是使用了AndroidAnnotations类后面加了一个_,比如MainActivity你就要使用MainActivity_来代替,比如Activity的跳转就必须这样写:startActivity(new Intent(this,MyActivity_.class)),这两个开源库的原理基本差不多,哪种方法比较好看个人喜好去选择吧。
言归正传,辅助类生成后,最终的调用过程一般是ButterKnife.bind(this)开始,查看ButterKnife.bind(this)源码,最终会走到createBinding以及findBindingConstructorForClass这个方法中,源码如下图所示,这个方法就是根据你传入的类名找到对应的辅助类,最终通过调用constructor.newInstance(target, source)来实现View以及其他资源的绑定工作。这里需要注意的是在findBindingConstructorForClass使用辅助类的时候,其实是有用到反射的,这样第一次使用的时候会稍微降低程序性能,但是ButterKnife会把通过反射生成的实例保存到HashMap中,下一次直接从HashMap中取上次生成的实例,这样就极大的降低了反射导致的性能问题。当然ButterKnife.bind方法还允许传入其他不同的参数,原理基本差不多,最终都会用到我们生成的辅助类,这里就不赘述了。
怎样浅析ButterKnife
怎样浅析ButterKnife

执行注解处理器

注解处理器已经有了,比如ButterKnifeProcessor,那么怎么执行它呢?这个时候就需要用到android-apt这个插件了,使用它有两个目的:

  1. 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library

  2. 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用

这里把使用ButterKnifeandroid-apt的配置作为例子,在工程的build.gradle中添加android-apt插件

buildscript {  repositories {    mavenCentral()   }  dependencies {    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  }}

在项目的build.gradle中添加

apply plugin: 'android-apt'android {  ...}dependencies {  compile 'com.jakewharton:butterknife:8.4.0'  apt 'com.jakewharton:butterknife-compiler:8.4.0'}

ButterKnife作为一个被广泛使用的依赖注入库,有很多优点:

  • 没有使用反射,而是通过Java Annotation Tool动态生成辅助代码实现了View的依赖注入,提升了程序的性能

  • 提高开发效率,减少代码量

当然也有一些不太友好的地方:

  • 会额外生成新的类和方法数,主要是会加速触及65535方法数,当然,如果App已经有分dex了可以不用考虑

  • 也不是完全没有用到反射,比如第一次调用ButterKnife.bind(this)语句使用辅助类的时候就用到了,会稍微影响程序的性能(但是也仅仅是第一次)

关于怎样浅析ButterKnife问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注编程网行业资讯频道了解更多相关知识。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

怎样浅析ButterKnife

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

怎样浅析ButterKnife

怎样浅析ButterKnife,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。不管是Android开发的老司机也好,新司机也罢,想必大家都对findViewById这种样板代
2023-06-04

怎样浅析Python开发过程

这篇文章给大家介绍怎样浅析Python开发过程,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。Python是一门功能强大的高级脚本语言,它的强大不仅表现在其自身的功能上,而且还表现在其良好的可扩展性上,正因如此,Pyth
2023-06-17

浅析css怎么设置字体样式

CSS 是一种层叠样式表语言,它负责控制网页的样式和布局。在网页中,字体是一个非常重要的元素,可以让网站的内容更加清晰、易读、美观。在 CSS 中,我们可以使用多种方式来设置字体。在本文中,我将为大家详细介绍如何使用 CSS 样式设置字体。1. 字体系列字体系列指的是使用哪种字体来显示网页文本。在 CSS 中,我们可以通过 font-family 属性来设置字体系列。这个属性接
2023-05-14

怎样浅析Java单例设计模式

本篇文章给大家分享的是有关怎样浅析Java单例设计模式,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。单例模式特点1、构造器私有2、在一个Java应用程序中,可保证只有一个实例对
2023-06-22

浅析node怎样链接多个JS模块

有时候是不是会有这样的疑问:纷繁的功能文件,到最后是怎么组合成起来并且在浏览器中展示的?为什么需要 node 环境?下面本篇文章给大家介绍一下node是怎样把多个JS模块链接在一起的?希望对大家有所帮助!
2023-05-14

CSS怎么设置居中样式?方法浅析

CSS是网页设计中最常用的样式表语言之一,它不仅可以改变网页元素的颜色、字体、大小等属性,还能够实现居中、布局等功能。而在网页设计中,元素的居中是非常重要的,因此,本文将为您介绍如何使用CSS进行居中设置。一、文本居中在网页设计中,文本居中是一种非常常见的居中方式,可以使得网页排版更加整洁、美观,而CSS中设置文本居中的方法主要有以下两种:1. 水平居中CSS中设置文本水平居中
2023-05-14

怎样浅析Visual Studio中的特定领域开发

怎样浅析Visual Studio中的特定领域开发,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。将列数Visual Studio中的DSL,也就是特定领域开发的方面会用到的
2023-06-17

怎样浅析Java语言中两种异常的差别

怎样浅析Java语言中两种异常的差别,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。浅析Java语言中两种异常的差别  Java提供了两类主要的异常:runtime except
2023-06-03

怎么浅析Java的流

这篇文章给大家介绍怎么浅析Java的流,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。浅析Java的流 由于手头有个项目需要用的Java的输入输出操作, 所以,啃了几天书,对Java的流技术有些了解,不过,还是有很多不是
2023-06-03

浅析Angular项目中使用 SASS 样式的方法

Angular项目中怎么使用 SASS 样式?下面本篇文章给大家介绍一下Angular 中 SASS 样式的使用方法,希望对大家有所帮助!
2022-11-22

怎样浅谈Spark的多语言支持

怎样浅谈Spark的多语言支持,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。Spark 设计上的优秀无容置疑,甫一出道便抢了 Hadoop 的 C 位,在开源大数据的黄金十
2023-06-19

浅析node怎么实现ocr

怎么实现ocr(光学字符识别)?下面本篇文章给大家介绍一下使用node实现实现实现ocr的方法,希望对大家有所帮助!
2022-11-22

怎样浅谈Java访问控制机制

怎样浅谈Java访问控制机制,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。Java 访问控制机制的原理是:在某些策略配置文件中预定义好某些代码对某些资源具有某些操作权限,当
2023-06-17

浅析nodejs中怎么使用JWT?

本篇文章带大家了解一下JWT,介绍一下JWT在node中的应用,以及JWT的优缺点,希望对大家有所帮助!
2023-05-14

怎么浅析Python应用程序

怎么浅析Python应用程序,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。在第一次进行学习Python语言时,开发人员给Python应用程序创建图形用户界面未必复杂,Pyt
2023-06-17

github怎么使用?方法浅析

GitHub是一款基于Git的版本控制工具,它能够将代码托管到云端,并提供了一系列协作工具,帮助开发者更高效地管理和维护自己的代码。以下是GitHub的使用方法:一、 创建账户首先,用户需要在GitHub官网创建自己的账户。在创建账户的过程
2023-10-22

浅析xcode上怎么安装git

Git是一种非常流行的版本控制工具,许多开发人员都在使用它进行代码管理。如果你是一名iOS开发人员,那么你需要使用Xcode来进行代码编写和调试。为了将Git集成到你的Xcode项目中,你需要将Git安装到你的Mac上。本文将介绍如何从Xc
2023-10-22

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录