热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android主流三方库源码分析(七、深入理解ButterKnife源码)

前言成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。不知不觉,笔者已

前言


成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

不知不觉,笔者已经对Android主流三方库中的网络框架OkHttp、Retrofit,图片加载框架Glide、数据库框架GreenDao、响应式编程框架RxJava、内存泄露框架LeakCanary进行了详细的分析,如果有朋友对这些开源框架的内部实现机制感兴趣的话,可以在笔者的个人主页选择相应的文章阅读。这篇,我将会对Android中的依赖注入框架ButterKnife的源码实现机制进行详细地讲解。


一、简单示例

首先,我们先来看一下ButterKnife的基本使用(取自Awesome-WanAndroid),如下所示:

public class CollectFragment extends BaseRootFragment implements CollectContract.View {@BindView(R.id.normal_view)SmartRefreshLayout mRefreshLayout;@BindView(R.id.collect_recycler_view)RecyclerView mRecyclerView;@BindView(R.id.collect_floating_action_btn)FloatingActionButton mFloatingActionButton;@Nullable@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {View view = inflater.inflate(getLayoutId(), container, false);unBinder = ButterKnife.bind(this, view);initView();return view;}@OnClick({R.id.collect_floating_action_btn})void onClick(View view) {switch (view.getId()) {case R.id.collect_floating_action_btn:mRecyclerView.smoothScrollToPosition(0);break;default:break;}}@Overridepublic void onDestroyView() {super.onDestroyView();if (unBinder != null && unBinder != Unbinder.EMPTY) {unBinder.unbind();unBinder = null;}}

可以看到,我们使用了@BindView()替代了findViewById()方法,然后使用了@OnClick替代了setOnClickListener()方法。ButterKnife的初期版本是通过使用注解+反射这样的运行时解析的方式实现上述功能的,后面,为了改善性能,便使用了注解+APT编译时解析技术并从中生成配套模板代码的方式来实现。

在开始分析之前,可能有同学对APT不是很了解,我这里普及一下,APT是Annotation Processing Tool的缩写,即注解处理工具。它的使用步骤通常为如下三个步骤:


  • 1、首先,声明注解的生命周期为CLASS,即@Retention(CLASS)
  • 2、然后,通过继承AbstractProcessor自定义一个注解处理器
  • 3、最后,在编译的时候,编译器会扫描所有带有你要处理的注解的类,最后再调用AbstractProcessor的process方法,对注解进行处理

下面,我们正式来解剖一下ButterKnife的心脏。


二、源码分析


1、模板代码解析

首先,在我们编写好上述的示例代码之后,调用 gradle build 命令,在app/build/generated/source/apt下将可以找到APT为我们生产的配套模板代码CollectFragment_ViewBinding,如下所示:

public class CollectFragment_ViewBinding implements Unbinder {private CollectFragment target;private View view2131230812;@UiThreadpublic CollectFragment_ViewBinding(final CollectFragment target, View source) {this.target = target;View view;// 1target.mRefreshLayout = Utils.findRequiredViewAsType(source, R.id.normal_view, "field 'mRefreshLayout'", SmartRefreshLayout.class);target.mRecyclerView = Utils.findRequiredViewAsType(source, R.id.collect_recycler_view, "field 'mRecyclerView'", RecyclerView.class);view = Utils.findRequiredView(source, R.id.collect_floating_action_btn, "field 'mFloatingActionButton' and method 'onClick'");target.mFloatingActionButton = Utils.castView(view, R.id.collect_floating_action_btn, "field 'mFloatingActionButton'", FloatingActionButton.class);view2131230812 = view;// 2view.setOnClickListener(new DebouncingOnClickListener() {@Overridepublic void doClick(View p0) {target.onClick(p0);}});}@Override@CallSuperpublic void unbind() {CollectFragment target = this.target;if (target == null) throw newIllegalStateException("Bindings already cleared.");this.target = null;target.mRefreshLayout = null;target.mRecyclerView = null;target.mFloatingActionButton = null;view2131230812.setOnClickListener(null);view2131230812 = null;}
}

生成的配套模板CollectFragment_ViewBinding中,在注释1处,使用了ButterKnife内部的工具类Utils的findRequiredViewAsType()方法来寻找控件。在注释2处,使用了view的setOnClickListener()方法来添加了一个去抖动的DebouncingOnClickListener,这样便可以防止重复点击,在重写的doClick()方法内部,直接调用了CollectFragment的onClick方法。最后,我们在深入看下Utils的findRequiredViewAsType()方法内部的实现。

public static T findRequiredViewAsType(View source, @IdRes int id, String who,Class cls) {// 1View view = findRequiredView(source, id, who);// 2return castView(view, id, who, cls);
}public static View findRequiredView(View source, @IdRes int id, String who) {View view = source.findViewById(id);if (view != null) {return view;}...
}public static T castView(View view, @IdRes int id, String who, Class cls) {try {return cls.cast(view);} catch (ClassCastException e) {...}
}

在注释1处,最终也是通过View的findViewById()方法找到相应的控件,在注释2处,通过相应Class对象的cast方法强转成对应的控件类型


2、ButterKnife 是怎样实现代码注入的

接下来,为了使用这套模板代码,我们必须调用ButterKnife的bind()方法实现代码注入,即自动帮我们执行重复繁琐的findViewById和setOnClicklistener操作。下面我们来分析下bind()方法是如何实现注入的。

@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {return createBinding(target, source);
}

在bind()方法中调用了createBinding(),

@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {Class targetClass = target.getClass();// 1Constructor constructor = findBindingConstructorForClass(targetClass);if (constructor == null) {return Unbinder.EMPTY;}try {// 2return constructor.newInstance(target, source);// 3} catch (IllegalAccessException e) {...
}

首先,在注释1处,通过 findBindingConstructorForClass() 方法从 Class 中查找 constructor,这里constructor即上文生成的CollectFragment_ViewBinding类。然后,在注释2处,利用反射来新建 constructor 对象。最后,如果新建 constructor 对象失败,则会在注释3后面捕获一系列对应的异常进行自定义异常抛出处理。

下面,我们来详细分析下
findBindingConstructorForClass() 方法的实现逻辑。

@VisibleForTesting
static final Map, Constructor> BINDINGS &#61; new LinkedHashMap<>();&#64;Nullable &#64;CheckResult &#64;UiThread
private static Constructor findBindingConstructorForClass(Class cls) {// 1Constructor bindingCtor &#61; BINDINGS.get(cls);if (bindingCtor !&#61; null || BINDINGS.containsKey(cls)) {return bindingCtor;}// 2String clsName &#61; cls.getName();if (clsName.startsWith("android.") || clsName.startsWith("java.")|| clsName.startsWith("androidx.")) {return null;}try {// 3Class bindingClass &#61; cls.getClassLoader().loadClass(clsName &#43; "_ViewBinding");bindingCtor &#61; (Constructor) bindingClass.getConstructor(cls, View.class);} catch (ClassNotFoundException e) {// 4bindingCtor &#61; findBindingConstructorForClass(cls.getSuperclass());} catch (NoSuchMethodException e) {throw new RuntimeException("Unable to find binding constructor for " &#43; clsName, e);}// 5BINDINGS.put(cls, bindingCtor);return bindingCtor;
}

这里&#xff0c;我把多余的log代码删除并把代码格式优化了一下&#xff0c;可以看到&#xff0c;findBindingConstructorForClass() 这个方法中的逻辑瞬间清晰不少&#xff0c;这里建议以后大家自己在分析源码的时候可以进行这样的优化重整&#xff0c;会带来不少好处。

重新看到 findBindingConstructorForClass() 方法&#xff0c;在注释1处&#xff0c;我们首先从缓存BINDINGS中获取CollectFragment类对象对应的模块类CollectFragment_ViewBinding的构造器对象&#xff0c;这里的BINDINGS是一个LinkedHashMap对象&#xff0c;它保存了上述两者的映射关系。在注释2处&#xff0c;如果是 android&#xff0c;androidx&#xff0c;java 原生的文件&#xff0c;不进行处理。在注释3处&#xff0c;先通过CollectFragment类对象的类加载器加载出对应的模块类CollectFragment_ViewBinding的类对象&#xff0c;再通过自身的getConstructor()方法获得相应的构造对象。如果在步骤3中加载不出对应的模板类对象&#xff0c;则会在注释4处使用类似递归的方法重新执行findBindingConstructorForClass()方法。最后&#xff0c;如果找到了bindingCtor模板构造对象&#xff0c;则将它保存在BINDINGS这个LinkedHashMap对象中。

这里总结一下findBindingConstructorForClass()方法的处理&#xff1a;


  • 1、首先从缓存BINDINGS中获取CollectFragment类对象对应的模块类CollectFragment_ViewBinding的构造器对象&#xff0c;获取不到&#xff0c;则继续执行下面的操作
  • 2、如果不是android&#xff0c;androidx&#xff0c;java 原生的文件&#xff0c;再进行后面的处理
  • 3、通过CollectFragment类对象的类加载器加载出对应的模块类CollectFragment_ViewBinding的类对象&#xff0c;再通过自身的getConstructor()方法获得相应的构造对象&#xff0c;如果获取不到&#xff0c;会抛出异常&#xff0c;在异常的处理中&#xff0c;我们会从当前 class 文件的父类中再去查找。如果找到了&#xff0c;最后会将bindingCtor对象缓存进在BINDINGS对象中

3、ButterKnife是如何在编译时生成代码的&#xff1f;

在编译的时候&#xff0c;ButterKnife会通过自定义的注解处理器ButterKnifeProcessor的process方法&#xff0c;对编译器扫描到的要处理的类中的注解进行处理&#xff0c;然后&#xff0c;通过javapoet这个库来动态生成绑定事件或者控件的模板代码&#xff0c;最后在运行的时候&#xff0c;直接调用bind方法完成绑定即可。

首先&#xff0c;我们先来分析下ButterKnifeProcessor的重写的入口方法init()。

&#64;Override public synchronized void init(ProcessingEnvironment env) {super.init(env);String sdk &#61; env.getOptions().get(OPTION_SDK_INT);if (sdk !&#61; null) {try {this.sdk &#61; Integer.parseInt(sdk);} catch (NumberFormatException e) {...}}typeUtils &#61; env.getTypeUtils();filer &#61; env.getFiler();...
}

可以看到&#xff0c;ProcessingEnviroment对象提供了两大工具类 typeUtils和filer。typeUtils的作用是用来处理TypeMirror&#xff0c;而Filer则是用来创建生成辅助文件

接着&#xff0c;我们再来看看被重写的getSupportedAnnotationTypes()方法&#xff0c;这个方法的作用主要是用于指定ButterknifeProcessor注册了哪些注解的。

&#64;Override public Set getSupportedAnnotationTypes() {Set types &#61; new LinkedHashSet<>();for (Class annotation : getSupportedAnnotations()) {types.add(annotation.getCanonicalName());}return types;
}

这里面首先创建了一个LinkedHashSet对象&#xff0c;然后将getSupportedAnnotations()方法返回的支持注解集合进行遍历一一并添加到types中返回。

接着我们看下getSupportedAnnotations()方法&#xff0c;

private Set> getSupportedAnnotations() {Set> annotations &#61; 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;
}

可以看到&#xff0c;这里注册了一系列的Bindxxx注解类和监听列表LISTENERS&#xff0c;接着看一下LISTENERS中包含的监听方法&#xff1a;

private static final List> LISTENERS &#61; Arrays.asList(OnCheckedChanged.class, OnClick.class, OnEditorAction.class, OnFocusChange.class, OnItemClick.class, OnItemLongClick.class, OnItemSelected.class, OnLongClick.class, OnPageChange.class, OnTextChanged.class, OnTouch.class
);

最后&#xff0c;我们来分析下整个ButterKnifeProcessor中最关键的方法process()。

&#64;Override public boolean process(Set elements, RoundEnvironment env) {// 1Map bindingMap &#61; findAndParseTargets(env);for (Map.Entry entry : bindingMap.entrySet()) {TypeElement typeElement &#61; entry.getKey();BindingSet binding &#61; entry.getValue();// 2JavaFile javaFile &#61; binding.brewJava(sdk, debuggable);try {javaFile.writeTo(filer);} catch (IOException e) {...}}return false;
}

首先&#xff0c;在注释1处通过findAndParseTargets()方法&#xff0c;知名见义&#xff0c;它应该就是找到并解析注解目标的关键方法了&#xff0c;继续看看它内部的处理&#xff1a;

private Map findAndParseTargets(RoundEnvironment env) {Map builderMap &#61; new LinkedHashMap<>();Set erasedTargetNames &#61; new LinkedHashSet<>();// 1、一系列处理每一个&#64;Bindxxx元素的for循环代码块...// Process each &#64;BindView element.for (Element element : env.getElementsAnnotatedWith(BindView.class)) {try {// 2parseBindView(element, builderMap, erasedTargetNames);} catch (Exception e) {logParsingError(element, BindView.class, e);}}// Process each &#64;BindViews element....// Process each annotation that corresponds to a listener.for (Class listener : LISTENERS) {findAndParseListener(env, listener, builderMap, erasedTargetNames);}// 2Deque> entries &#61;new ArrayDeque<>(builderMap.entrySet());Map bindingMap &#61; new LinkedHashMap<>();while (!entries.isEmpty()) {Map.Entry entry &#61; entries.removeFirst();TypeElement type &#61; entry.getKey();BindingSet.Builder builder &#61; entry.getValue();TypeElement parentType &#61; findParentType(type, erasedTargetNames);if (parentType &#61;&#61; null) {bindingMap.put(type, builder.build());} else {BindingSet parentBinding &#61; bindingMap.get(parentType);if (parentBinding !&#61; null) {builder.setParent(parentBinding);bindingMap.put(type, builder.build());} else {entries.addLast(entry);}}}return bindingMap;
}

findAndParseTargets()方法的代码非常多&#xff0c;我这里尽可能做了精简。首先&#xff0c;在注释1处&#xff0c;扫描并处理所有具有&#64;Bindxxx注解和符合LISTENERS监听方法集合的代码&#xff0c;然后在每一个&#64;Bindxxx对应的for循环代码中的parseBindxxx()或findAndParseListener()方法中将解析出的信息放入builderMap这个LinkedHashMap对象中&#xff0c;其中builderMap是一个key为TypeElement&#xff0c;value为BindingSet.Builder的映射集合&#xff0c;这个 BindSet 是指的一个类型请求的所有绑定的集合。在注释3处&#xff0c;首先使用上面的builderMap对象去构建了一个entries对象&#xff0c;它是一个双向队列&#xff0c;能实现两端存取的操作。接着&#xff0c;又新建了一个key为TypeElement&#xff0c;value为BindingSet的LinkedHashMap对象&#xff0c;最后使用了一个while循环从entries的第一个元素开始&#xff0c;这里会判断当前元素类型是否有父类&#xff0c;如果没有&#xff0c;直接构建builder放入bindingMap中&#xff0c;如果有&#xff0c;则将parentBinding添加到BindingSet.Builder这个建造者对象中&#xff0c;然后再创建BindingSet再添加到bindingMap中。

接着&#xff0c;我们分析下注释2处parseBindView是如何对每一个&#64;BindView注解的元素进行处理。

private void parseBindView(Element element, Map builderMap,Set erasedTargetNames) {TypeElement enclosingElement &#61; (TypeElement) element.getEnclosingElement();// 1、首先验证生成的常见代码限制...// 2、验证目标类型是否继承自View。...// 3int id &#61; element.getAnnotation(BindView.class).value();BindingSet.Builder builder &#61; builderMap.get(enclosingElement);Id resourceId &#61; elementToId(element, BindView.class, id);if (builder !&#61; null) {String existingBindingName &#61; builder.findExistingBindingName(resourceId);if (existingBindingName !&#61; null) {...return;}} else {// 4builder &#61; getOrCreateBindingBuilder(builderMap, enclosingElement);}String name &#61; simpleName.toString();TypeName type &#61; TypeName.get(elementType);boolean required &#61; isFieldRequired(element);// 5builder.addField(resourceId, new FieldViewBinding(name, type, required));// Add the type-erased version to the valid binding targets set.erasedTargetNames.add(enclosingElement);
}

首先&#xff0c;在注释1、2处均是一些验证处理操作&#xff0c;如果不符合则会return。然后&#xff0c;我们看到注释3处&#xff0c;这里获取了BindView要绑定的View的id&#xff0c;然后先从builderMap中获取BindingSet.Builder对象&#xff0c;如果存在&#xff0c;直接return。如果不存在&#xff0c;则会在注释4处的
getOrCreateBindingBuilder()方法生成一个。我们看一下getOrCreateBindingBuilder()方法:

private BindingSet.Builder getOrCreateBindingBuilder(Map builderMap, TypeElement enclosingElement) {BindingSet.Builder builder &#61; builderMap.get(enclosingElement);if (builder &#61;&#61; null) {builder &#61; BindingSet.newBuilder(enclosingElement);builderMap.put(enclosingElement, builder);}return builder;
}

可以看到&#xff0c;这里会再次从buildMap中获取BindingSet.Builder对象&#xff0c;如果没有则直接调用BindingSet的newBuilder()方法新建一个BindingSet.Builder对象并保存在builderMap中&#xff0c;然后&#xff0c;再将新建的builder对象返回。

回到parseBindView()方法的注释5处&#xff0c;这里根据view的信息生成一个FieldViewBinding&#xff0c;最后添加到上边生成的builder对象中。

最后&#xff0c;再回到我们的process()方法中&#xff0c;现在所有的绑定的集合数据都放在了bindingMap对象中&#xff0c;这里使用for循环取出每一个BindingSet对象&#xff0c;调用它的brewJava()方法&#xff0c;看看它内部的处理&#xff1a;

JavaFile brewJava(int sdk, boolean debuggable) {TypeSpec bindingConfiguration &#61; createType(sdk, debuggable);return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration).addFileComment("Generated code from Butter Knife. Do not modify!").build();
}private TypeSpec createType(int sdk, boolean debuggable) {TypeSpec.Builder result &#61; TypeSpec.classBuilder(bindingClassName.simpleName()).addModifiers(PUBLIC);if (isFinal) {result.addModifiers(FINAL);}if (parentBinding !&#61; null) {result.superclass(parentBinding.bindingClassName);} else {result.addSuperinterface(UNBINDER);}if (hasTargetField()) {result.addField(targetTypeName, "target", PRIVATE);}if (isView) {result.addMethod(createBindingConstructorForView());} else if (isActivity) {result.addMethod(createBindingConstructorForActivity());} else if (isDialog) {result.addMethod(createBindingConstructorForDialog());}if (!constructorNeedsView()) {// Add a delegating constructor with a target type &#43; view signature for reflective use.result.addMethod(createBindingViewDelegateConstructor());}result.addMethod(createBindingConstructor(sdk, debuggable));if (hasViewBindings() || parentBinding &#61;&#61; null) {result.addMethod(createBindingUnbindMethod(result));}return result.build();
}

在createType()方法里面使用了java中的javapoet技术生成了一个bindingConfiguration对象&#xff0c;很显然&#xff0c;它里面保存了所有的绑定配置信息。然后&#xff0c;通过javapoet的builder构造器将上面得到的bindingConfiguration对象构建生成一个JavaFile对象&#xff0c;最终&#xff0c;通过javaFile.writeTo(filer)生成了java源文件

至此&#xff0c;ButterKnife的源码分析就结束了。


三、总结

从上面的源码分析来看&#xff0c;ButterKnife的执行流程总体可以分为如下两步&#xff1a;


  • 1、在编译的时候扫描注解&#xff0c;并通过自定义的ButterKnifeProcessor做相应的处理解析得到bindingMap对象&#xff0c;最后&#xff0c;调用 javapoet 库生成java模板代码
  • 2、当我们调用 ButterKnife的bind()方法的时候&#xff0c;它会根据类的全限定类型&#xff0c;找到相应的模板代码&#xff0c;并在其中完成 findViewById 和 setOnClick &#xff0c;setOnLongClick 等操作

接下来&#xff0c;笔者会对Android中的依赖注入框架Dagger2的源码实现流程进行详细的讲解&#xff0c;敬请期待~


参考链接&#xff1a;


1、ButterKnife V10.0.0 源码

2、Android进阶之光

3、ButterKnife源码分析

4、butterknife 源码分析


Contanct Me


● 微信&#xff1a;


欢迎关注我的微信&#xff1a;bcce5360



● 微信群&#xff1a;


微信群如果不能扫码加入&#xff0c;麻烦大家想进微信群的朋友们&#xff0c;加我微信拉你进群。





● QQ群&#xff1a;


2千人QQ群&#xff0c;Awesome-Android学习交流群&#xff0c;QQ群号&#xff1a;959936182&#xff0c; 欢迎大家加入~



About me


  • Email: chao.qu521&#64;gmail.com

  • Blog: https://jsonchao.github.io/

  • 掘金: https://juejin.im/user/5a3ba9375188252bca050ade


很感谢您阅读这篇文章&#xff0c;希望您能将它分享给您的朋友或技术群&#xff0c;这对我意义重大。


希望我们能成为朋友&#xff0c;在 Github、掘金上一起分享知识。


推荐阅读
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 拥抱Android Design Support Library新变化(导航视图、悬浮ActionBar)
    转载请注明明桑AndroidAndroid5.0Loollipop作为Android最重要的版本之一,为我们带来了全新的界面风格和设计语言。看起来很受欢迎࿰ ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • C# WPF自定义按钮的方法
    本文介绍了在C# WPF中实现自定义按钮的方法,包括使用图片作为按钮背景、自定义鼠标进入效果、自定义按压效果和自定义禁用效果。通过创建CustomButton.cs类和ButtonStyles.xaml资源文件,设计按钮的Style并添加所需的依赖属性,可以实现自定义按钮的效果。示例代码在ButtonStyles.xaml中给出。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
author-avatar
陆寥频
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有