众里寻它千百度——ButterKnife源码完全解析

2024-02-15 08:59

本文主要是介绍众里寻它千百度——ButterKnife源码完全解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、概述

在很久很久之前,自从朋友推荐我用butterknife后, 从此的项目再也离不开butterknife了。问butterknife的原理,估计很多人都会回答注解加反射。我一开始也是以为是注解加反射,然而看了源码之后发现不单单用的注解加反射。那么下面我们就来分析一下ButterKnife的实现原理吧。本文基于ButterKnife8.6.0.

二、ButterKnife用法

做android开发应该大部分都用过ButterKnife(官方文档),没用过可以点击链接进去看看。就算没用过应该也听说过,毕竟是Jake Wharton出品的东西(话说现在Jake Wharton大神现在已经入职谷歌了)。要是没用过的话可以看看这篇文章,这是我自己用注解加反射实现的类似ButterKnife功能的代码。

三、ButterKnife原理

Butterknife用的APT(Annotation Processing Tool)编译时解析技术(现在已经改成了谷歌的更强大的annotationProcessor,APT已经停止更新了)。大概原理就是你声明的注解的生命周期为CLASS,然后继承AbstractProcessor类。继承这个类后,在编译的时候,编译器会扫描所有带有你要处理的注解的类,然后再调用AbstractProcessor的process方法,对注解进行处理,那么我们就可以在处理的时候,动态生成绑定事件或者控件的java代码,然后在运行的时候,直接调用方法完成绑定。

四、ButterKnife源码解析

说了这么多,我们还是直接从源码入手吧。

public class MainActivity extends AppCompatActivity {@BindView(R.id.tv)TextView tv;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ButterKnife.bind(this);tv.setText("点击");}@OnClick(R.id.tv)public void onViewClicked() {Toast.makeText(this,"111",Toast.LENGTH_SHORT).show();}
}

上边是我们在使用时候的代码,我们直接就从 ButterKnife.bind(this)入手吧,点进来看看:

  @NonNull @UiThreadpublic static Unbinder bind(@NonNull Activity target) {View sourceView = target.getWindow().getDecorView();return createBinding(target, sourceView);}

再点到createBinding(target, sourceView)里面看看:

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {Class<?> targetClass = target.getClass();if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);if (constructor == null) {return Unbinder.EMPTY;}//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.try {return constructor.newInstance(target, source);} catch (IllegalAccessException e) {throw new RuntimeException("Unable to invoke " + constructor, e);} catch (InstantiationException e) {throw new RuntimeException("Unable to invoke " + constructor, e);} catch (InvocationTargetException e) {Throwable cause = e.getCause();if (cause instanceof RuntimeException) {throw (RuntimeException) cause;}if (cause instanceof Error) {throw (Error) cause;}throw new RuntimeException("Unable to create binding instance.", cause);}}

createBinding()方法主要就是拿到我们绑定的Activity的Class,然后通过Constructor构造器获取一个Unbinder子类的构造方法,然后在调用newInstance(target, source)通过构造方法获取到Unbinder子类的一个实例,这里传入两个参数,说明构造方法里需要两个参数。我们打开findBindingConstructorForClass()方法。

@Nullable @CheckResult @UiThreadprivate static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);if (bindingCtor != null) {if (debug) Log.d(TAG, "HIT: Cached in binding map.");return bindingCtor;}String clsName = cls.getName();if (clsName.startsWith("android.") || clsName.startsWith("java.")) {if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");return null;}try {Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");//noinspection uncheckedbindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");} catch (ClassNotFoundException e) {if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());bindingCtor = findBindingConstructorForClass(cls.getSuperclass());} catch (NoSuchMethodException e) {throw new RuntimeException("Unable to find binding constructor for " + clsName, e);}BINDINGS.put(cls, bindingCtor);return bindingCtor;}
@VisibleForTestingstatic final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

上面的BINDINGS是一个保存了Class为key,Class_ViewBinding为Value的一个LinkedHashMap,主要是做一下缓存,提高下次再来bind的性能。
第14行clsName 是我们传入要绑定的Activity类名,第16行通过反射调用构造方法,这里相当于拿到了Activity_ViewBinding这个类的实例。其实从类名可以看出来,相当于Activity的一个辅助类,这时候我们就要问了,我们在用的时候没有声明这个类啊?从哪里来的? 不要方,其实它就是我们在之前讲原理的时候说到的AbstractProcessor在编译的时候生成的一个类,我们后面再来看它,现在我们继续往下面分析。
前面我们说到,这个方法里面用linkhashMap做了下缓存,所以在下边,就把刚刚反射的bindingCtor作为value,Class作为key加入这个LinkedHashMap,下次再绑定这个类的时候,就直接在方法的开始的时候取出来用。

下边我们就来找找Activity_ViewBinding这个类。

这里写图片描述

在app里面的build的classes文件夹下找到了它,点开看看:

public class MainActivity_ViewBinding implements Unbinder {private MainActivity target;private View view2131427422;@UiThreadpublic MainActivity_ViewBinding(MainActivity target) {this(target, target.getWindow().getDecorView());}@UiThreadpublic MainActivity_ViewBinding(final MainActivity target, View source) {this.target = target;View view = Utils.findRequiredView(source, 2131427422, "field \'tv\' and method \'onViewClicked\'");target.tv = (TextView)Utils.castView(view, 2131427422, "field \'tv\'", TextView.class);this.view2131427422 = view;view.setOnClickListener(new DebouncingOnClickListener() {public void doClick(View p0) {target.onViewClicked();}});}@CallSuperpublic void unbind() {MainActivity target = this.target;if(target == null) {throw new IllegalStateException("Bindings already cleared.");} else {this.target = null;target.tv = null;this.view2131427422.setOnClickListener((OnClickListener)null);this.view2131427422 = null;}}
}

刚才说的Unbinder的一个子类,看这里的类名。现在应该懂了。它刚好是实现了Unbinder接口。之前说了通过反射拿到了Activity_ViewBinding这个类的构造方法即通过调用getConstructor(cls, View.class)方法,然后通过newInstance(target, source)方法创建实例,这里传入的两个参数就是我们MainActivity_ViewBinding(final MainActivity target, View source)里面的两个参数。因为我们可以在Activity中使用butterknife,也可以在Fragment和Adapter等中使用butterknife,那么在不同的地方使用butterknife,这个target也就不同。我们接着看里面的findRequiredView方法:

public static View findRequiredView(View source, @IdRes int id, String who) {View view = source.findViewById(id);if (view != null) {return view;}String name = getResourceEntryName(source, id);throw new IllegalStateException("Required view '"+ name+ "' with ID "+ id+ " for "+ who+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"+ " (methods) annotation.");}

还是使用的findViewById。
返回上面的MainActivity_ViewBinding代码,首先调用了findRequiredView方法,其实这个方法最后经过处理就是调用了findViewById方法,拿到相应的view,然后再赋值给target.tv,刚说了target就是那个要绑定的Activity,这里通过 target.tv 这样的调用方式,说明了Activity中不能把TextView设置为private,不然会报错,其实这里可以用反射来拿到textView的,这里应该也是为了性能着想。最后setOnClickListener,DebouncingOnClickListener这个Listener其实也是实现了View.OnClickListener 方法,然后在OnClick里面调用了doClick方法。

五、Butterknife在编译的时候生成代码的原理

com.jakewharton:butterknife-compiler 就是自定义的注解处理器,我们在 Gradle 中注册使用它。
然而我在项目结构中找了很久也没有找到这个库的文件,有可能是在编译时才去访问的,如果需要可以在 GitHub 中找到:
butterknife-compiler

我们看看ButterKnifeProcessor这个类:

@Override public synchronized void init(ProcessingEnvironment env) {super.init(env);String sdk = env.getOptions().get(OPTION_SDK_INT);if (sdk != null) {try {this.sdk = Integer.parseInt(sdk);} catch (NumberFormatException e) {env.getMessager().printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"+ sdk+ "'. Falling back to API 1 support.");}}debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));elementUtils = env.getElementUtils();typeUtils = env.getTypeUtils();filer = env.getFiler();try {trees = Trees.instance(processingEnv);} catch (IllegalArgumentException ignored) {}}

int()方法里面进来判断了最低的支持的sdk版本。ProcessingEnviroment参数提供很多有用的工具类Elements, Types和Filer。Types是用来处理TypeMirror的工具类,Filer用来创建生成辅助文件。至于ElementUtils嘛,其实ButterKnifeProcessor在运行的时候,会扫描所有的Java源文件,然后每一个Java源文件的每一个部分都是一个Element,比如一个包、类或者方法。

@Override public Set<String> getSupportedAnnotationTypes() {Set<String> types = new LinkedHashSet<>();for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {types.add(annotation.getCanonicalName());}return types;}private Set<Class<? extends Annotation>> getSupportedAnnotations() {Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();annotations.add(BindAnim.class);annotations.add(BindArray.class);annotations.add(BindBitmap.class);annotations.add(BindBool.class);annotations.add(BindColor.class);annotations.add(BindDimen.class);annotations.add(BindDrawable.class);annotations.add(BindFloat.class);annotations.add(BindFont.class);annotations.add(BindInt.class);annotations.add(BindString.class);annotations.add(BindView.class);annotations.add(BindViews.class);annotations.addAll(LISTENERS);return annotations;}

getSupportedAnnotationTypes()方法主要是指定ButterknifeProcessor是注册给哪些注解的。我们可以看到,在源代码里面,作者一个一个地把Class文件加到那个LinkedHashSet里面,然后再把LISTENERS也全部加进去。

其实整个类最重要的是process方法:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {TypeElement typeElement = entry.getKey();BindingSet binding = entry.getValue();JavaFile javaFile = binding.brewJava(sdk, debuggable);try {javaFile.writeTo(filer);} catch (IOException e) {error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());}}return false;}

这个方法的作用主要是扫描、评估和处理我们程序中的注解,然后生成Java文件,也就是前面说的MainActivity_ViewBinding。首先一进这个函数就调用了findAndParseTargets方法,我们就去看看findAndParseTargets方法到底做了什么:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();scanForRClasses(env);// Process each @BindAnim element.for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {if (!SuperficialValidation.validateElement(element)) continue;try {parseResourceAnimation(element, builderMap, erasedTargetNames);} catch (Exception e) {logParsingError(element, BindAnim.class, e);}}
........
........
........
// Associate superclass binders with their subclass binders. This is a queue-based tree walk// which starts at the roots (superclasses) and walks to the leafs (subclasses).Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =new ArrayDeque<>(builderMap.entrySet());Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();while (!entries.isEmpty()) {Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();TypeElement type = entry.getKey();BindingSet.Builder builder = entry.getValue();TypeElement parentType = findParentType(type, erasedTargetNames);if (parentType == null) {bindingMap.put(type, builder.build());} else {BindingSet parentBinding = bindingMap.get(parentType);if (parentBinding != null) {builder.setParent(parentBinding);bindingMap.put(type, builder.build());} else {// Has a superclass binding but we haven't built it yet. Re-enqueue for later.entries.addLast(entry);}}}return bindingMap;

这个方法的代码非常多,这里只贴出一部分,这个方法的主要的流程如下:

扫描所有具有注解的类,然后根据这些类的信息生成BindingSet,最后生成以TypeElement为键,BindingSet为值的键值对。
循环遍历这个键值对,根据TypeElement和BindingSet里面的信息生成对应的java类。例如AnnotationActivity生成的类即为MainActivity_ViewBinding类。

这里我们可以看看BindingSet里面的代码:

final class BindingSet {static final ClassName UTILS = ClassName.get("butterknife.internal", "Utils");private static final ClassName VIEW = ClassName.get("android.view", "View");private static final ClassName CONTEXT = ClassName.get("android.content", "Context");private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");private static final ClassName UI_THREAD =ClassName.get("android.support.annotation", "UiThread");private static final ClassName CALL_SUPER =ClassName.get("android.support.annotation", "CallSuper");private static final ClassName SUPPRESS_LINT =ClassName.get("android.annotation", "SuppressLint");private static final ClassName UNBINDER = ClassName.get("butterknife", "Unbinder");static final ClassName BITMAP_FACTORY = ClassName.get("android.graphics", "BitmapFactory");static final ClassName CONTEXT_COMPAT =ClassName.get("android.support.v4.content", "ContextCompat");static final ClassName ANIMATION_UTILS =ClassName.get("android.view.animation", "AnimationUtils");private final TypeName targetTypeName;private final ClassName bindingClassName;private final boolean isFinal;private final boolean isView;private final boolean isActivity;private final boolean isDialog;private final ImmutableList<ViewBinding> viewBindings;private final ImmutableList<FieldCollectionViewBinding> collectionBindings;private final ImmutableList<ResourceBinding> resourceBindings;private final BindingSet parentBinding;private BindingSet(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,boolean isView, boolean isActivity, boolean isDialog, ImmutableList<ViewBinding> viewBindings,ImmutableList<FieldCollectionViewBinding> collectionBindings,ImmutableList<ResourceBinding> resourceBindings, BindingSet parentBinding) {this.isFinal = isFinal;this.targetTypeName = targetTypeName;this.bindingClassName = bindingClassName;this.isView = isView;this.isActivity = isActivity;this.isDialog = isDialog;this.viewBindings = viewBindings;this.collectionBindings = collectionBindings;this.resourceBindings = resourceBindings;this.parentBinding = parentBinding;}JavaFile brewJava(int sdk, boolean debuggable) {return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable)).addFileComment("Generated code from Butter Knife. Do not modify!").build();}private TypeSpec createType(int sdk, boolean debuggable) {TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName()).addModifiers(PUBLIC);if (isFinal) {result.addModifiers(FINAL);}........................static final class Builder {private final TypeName targetTypeName;private final ClassName bindingClassName;private final boolean isFinal;private final boolean isView;private final boolean isActivity;private final boolean isDialog;private BindingSet parentBinding;private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings =ImmutableList.builder();private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder();private Builder(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,boolean isView, boolean isActivity, boolean isDialog) {this.targetTypeName = targetTypeName;this.bindingClassName = bindingClassName;this.isFinal = isFinal;this.isView = isView;this.isActivity = isActivity;this.isDialog = isDialog;}void addField(Id id, FieldViewBinding binding) {getOrCreateViewBindings(id).setFieldBinding(binding);}void addFieldCollection(FieldCollectionViewBinding binding) {collectionBindings.add(binding);}boolean addMethod(Id id,ListenerClass listener,ListenerMethod method,MethodViewBinding binding) {ViewBinding.Builder viewBinding = getOrCreateViewBindings(id);if (viewBinding.hasMethodBinding(listener, method) && !"void".equals(method.returnType())) {return false;}viewBinding.addMethodBinding(listener, method, binding);return true;}void addResource(ResourceBinding binding) {resourceBindings.add(binding);}void setParent(BindingSet parent) {this.parentBinding = parent;}String findExistingBindingName(Id id) {ViewBinding.Builder builder = viewIdMap.get(id);if (builder == null) {return null;}FieldViewBinding fieldBinding = builder.fieldBinding;if (fieldBinding == null) {return null;}return fieldBinding.getName();}private ViewBinding.Builder getOrCreateViewBindings(Id id) {ViewBinding.Builder viewId = viewIdMap.get(id);if (viewId == null) {viewId = new ViewBinding.Builder(id);viewIdMap.put(id, viewId);}return viewId;}BindingSet build() {ImmutableList.Builder<ViewBinding> viewBindings = ImmutableList.builder();for (ViewBinding.Builder builder : viewIdMap.values()) {viewBindings.add(builder.build());}return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,viewBindings.build(), collectionBindings.build(), resourceBindings.build(),parentBinding);}}
}

这个类的代码也非常多,所以我们也只贴一部分,可以自己去看看源码,这个BindingSet是管理了所有关于这个注解的一些信息还有实例本身的信息。

因为我们之前用的例子是绑定的一个View,所以我们就只贴了解析View的代码。好吧,这里遍历了所有带有@BindView的Element,然后对每一个Element进行解析,也就进入了parseBindView这个方法中:

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();if (elementType.getKind() == TypeKind.TYPEVAR) {TypeVariable typeVariable = (TypeVariable) elementType;elementType = typeVariable.getUpperBound();}Name qualifiedName = enclosingElement.getQualifiedName();Name simpleName = element.getSimpleName();if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {if (elementType.getKind() == TypeKind.ERROR) {note(element, "@%s field with unresolved type (%s) "+ "must elsewhere be generated as a View or interface. (%s.%s)",BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);} else {error(element, "@%s fields must extend from View or be an interface. (%s.%s)",BindView.class.getSimpleName(), qualifiedName, simpleName);hasError = true;}}if (hasError) {return;}// Assemble information on the field.int id = element.getAnnotation(BindView.class).value();BindingSet.Builder builder = builderMap.get(enclosingElement);QualifiedId qualifiedId = elementToQualifiedId(element, id);if (builder != null) {String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));if (existingBindingName != null) {error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",BindView.class.getSimpleName(), id, existingBindingName,enclosingElement.getQualifiedName(), element.getSimpleName());return;}} else {builder = getOrCreateBindingBuilder(builderMap, enclosingElement);}String name = simpleName.toString();TypeName type = TypeName.get(elementType);boolean required = isFieldRequired(element);builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));// Add the type-erased version to the valid binding targets set.erasedTargetNames.add(enclosingElement);}

然后这里从一进入这个方法到

int id = element.getAnnotation(BindView.class).value();

都是在拿到注解信息,然后验证注解的target的类型是否继承自view,然后上面这一行代码获得我们要绑定的View的id,再从builderMap里面取出BindingSet.Builder对象(这个BindingSet是管理了所有关于这个注解的一些信息还有实例本身的信息,其实最后是通过BindingSet来生成java代码的,上面也已经看了BindingSet的代码),如果builderMap里面不存在的话,就在

builder = getOrCreateBindingBuilder(builderMap, enclosingElement);

这里生成一个,我们进去看一下getOrCreateBindingBuilder:

private BindingSet.Builder getOrCreateBindingBuilder(Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {BindingSet.Builder builder = builderMap.get(enclosingElement);if (builder == null) {builder = BindingSet.newBuilder(enclosingElement);builderMap.put(enclosingElement, builder);}return builder;}

这里面其实很简单,就是获取一些这个注解所修饰的变量的一些信息,然后把这个解析后的builder加入到builderMap里面。

返回刚刚的parseBindView中,根据view的信息生成一个FieldViewBinding,最后添加到上边生成的builder实例中。这里基本完成了解析工作。最后回到findAndParseTargets中:

// Associate superclass binders with their subclass binders. This is a queue-based tree walk// which starts at the roots (superclasses) and walks to the leafs (subclasses).Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =new ArrayDeque<>(builderMap.entrySet());Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();while (!entries.isEmpty()) {Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();TypeElement type = entry.getKey();BindingSet.Builder builder = entry.getValue();TypeElement parentType = findParentType(type, erasedTargetNames);if (parentType == null) {bindingMap.put(type, builder.build());} else {BindingSet parentBinding = bindingMap.get(parentType);if (parentBinding != null) {builder.setParent(parentBinding);bindingMap.put(type, builder.build());} else {// Has a superclass binding but we haven't built it yet. Re-enqueue for later.entries.addLast(entry);}}}

这里主要的工作是建立上面的绑定的所有的实例的解绑的关系,因为我们绑定了,最后在代码中还是会解绑的。这里预先处理好了这些关系。
回到我们的process中, 现在解析完了annotation,该生成java文件了,再看看代码:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {TypeElement typeElement = entry.getKey();BindingSet binding = entry.getValue();JavaFile javaFile = binding.brewJava(sdk, debuggable);try {javaFile.writeTo(filer);} catch (IOException e) {error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());}}return false;}

遍历刚刚得到的bindingMap,然后再一个一个地通过

javaFile.writeTo(filer);

来生成java文件。然而生成的java文件也是根据上面的信息来用字符串拼接起来的,然而这个工作在brewJava()中完成了:

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

最后通过writeTo(Filer filer)生成java源文件。

这篇关于众里寻它千百度——ButterKnife源码完全解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/710944

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

HDU 2159 二维完全背包

FATE 最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能

zoj 1721 判断2条线段(完全)相交

给出起点,终点,与一些障碍线段。 求起点到终点的最短路。 枚举2点的距离,然后最短路。 2点可达条件:没有线段与这2点所构成的线段(完全)相交。 const double eps = 1e-8 ;double add(double x , double y){if(fabs(x+y) < eps*(fabs(x) + fabs(y))) return 0 ;return x + y ;

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动