定义

注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。

分类

注解分为标准注解和元注解;

标准注解

@Override:对覆盖超类中的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编译器会发出错误警告;

@Deprecated:对不鼓励使用或者已过时的方法添加注解,当编程人员使用这些方法时,将会在编译时显示提示信息;

@SuppressWarnings:选择性地取消特定代码段中的警告;

@SafeVarargs:JDK7新增,用来声明使用了可变长度参数的方法,其在与泛型类一起使用时不会出现类型安全问题;

元注解

用来注解其他注解,从而创建新的注解;

@Target:注解所修饰的对象范围,取值是一个ElementType类型的数组,copy了下ElemenType的代码,以下是可以修饰的类型;

public enum ElementType {/** 能修饰类,接口(包含注解类型),或者枚举类型 */TYPE,/** 修饰成员变量 */FIELD,/** 修饰方法 */METHOD,/** 修饰参数 */PARAMETER,/** 修饰构造方法 */CONSTRUCTOR,/** 修饰局部变量 */LOCAL_VARIABLE,/** 修饰注解 */ANNOTATION_TYPE,/** 修饰包 */PACKAGE,/** 类型参数声明 */TYPE_PARAMETER,/** 使用类型 */TYPE_USE
}

@Retention:用来声明注解的保留策略 , @Retention注解有三种取值,分别代表不同级别的保留策略;

  • RetentionPolicy.SOURCE:源码级注解;
    注解信息只会保留在.java源码中,源码在编译后,注解信息被丢弃,不会保留在.class中;
  • RetentionPolicy.CLASS:编译时注解;
    注解信息回保留在.java源码以及.class中,当运行Java程序时,JVM会丢弃该注解信息,不会保留在JVM中;
  • RetentionPolicy.RUNTIME:运行时注解;
    当运行Java程序时,JVM也会保留该注解信息,可以通过反射获取该注解信息;

@Inherited:表示注解可以被继承;

@Documented:表示这个注解应该被JavaDoc工具记录;

@Repeatable:JDK8新增,允许一个注解在同一声明类型(类、属性或方法)上多次使用;

验证三种保留策略

  • RetentionPolicy.SOURCE(源码级注解):

    虽然书上说,源码级注解,在编译后注解信息会被丢掉,但是我还是自己写了个例子试了下

    上代码,先看两个注解;

    我们常用的@Override(刚刚提到的标准注解之一),先看源码:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)//源码级注解
    public @interface Override {}
    

    如果需要自定义一个注解,格式应该是:public @interface 注解名,接下来自定义一个编译时注解@ClassAnnotation

    public class SuperTest {void methodA(){System.out.println("SuperTest,method methodA");}
    }public class Test extends SuperTest{@Overridevoid methodA() {super.methodA();System.out.println("Test,method methodA");}@ClassAnnotationvoid methodB(){}
    }
    

    使用javac编译后,会得到Test.class文件,如下:

    public class Test extends SuperTest {public Test() {}//Override没啦void methodA() {super.methodA();System.out.println("Test,method methodA");}@ClassAnnotation//编译时注解还在void methodB() {}
    }
    

    可以看到,在编译后的.class文件中,@Override没了,但@ClassAnnotation还在,那运行时注解(RetentionPolicy.RUNTIME)呢:

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;@Target({ElementType.METHOD})
    @Retention(RetentionPolicy.CLASS)
    public @interface ClassAnnotation {}
    

    这是@ClassAnnotation编译后的文件,可以看到,@Target注解还是在的,而@Target便是一个运行时注解(RetentionPolicy.RUNTIME);

自定义注解

自定义运行时注解(RetentionPolicy.RUNTIME)

刚刚也提到的@ClassAnnotation便是一个RetentionPolicy.RUNTIME(运行时注解),不过比较简单;通常呢,我们会把RetentionPolicy.RUNTIME(运行时注解)和反射一块儿用;

举个小栗子,先自定义一个注解@GetViewTo,设置其修饰对象为成员变量,是一个运行时注解;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetViewTo {int value() default -1;
}

然后用这个注解去标注Activity的成员变量mTextViewmButton,这里有个点,就是当注解类只有一个方法且方法名为“value"时,可以直接在注解后的括号里写明它的值,如下:

@GetViewTo(value = R.id.text_view)
private TextView mTextView;@GetViewTo(R.id.button)
private Button mButton;

其实吧,注解就是一种标记,如果我们只是标记了,但对标记不做任何处理,也是没什么用的,下面来对@GetViewTo这个注解做点’处理’:

private void getAnnotations() {try {//获取MainActivity的成员变量Field[] fields = getClass().getDeclaredFields();for (Field field : fields) {if (null != field.getAnnotations()) {//确定注解类型if (field.isAnnotationPresent(GetViewTo.class)) {//允许修改反射属性field.setAccessible(true);GetViewTo getViewTo = field.getAnnotation(GetViewTo.class);//findViewById将注解的id,找到View注入成员变量中field.set(this, findViewById(getViewTo.value()));}}}} catch (IllegalAccessException e) {e.printStackTrace();}
}

然后设置一下Button的点击事件,编译运行,可以看到,mButtonmTextView的id,通过@GetViewTo绑定成功了;

@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);getAnnotations();mButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this,"text  on textview is :" + mTextView.getText(), Toast.LENGTH_SHORT).show();}});
}

自定义编译时注解(RetentionPolicy.CLASS)

在自定义编译时注解之前,我们先了解以下注解处理器,通常,我们在处理运行时注解时,会采用反射机制处理,如上述的@GetViewTo注解这个栗子;

而在处理编译时注解时,我们通常会采用注解处理器AbstractProcessor去处理,AbstractProcessor是个抽象类,这里面会有4个比较重要的方法,通常我们自定义注解,都会重写这四个方法,注释里表明了它们的作用,如下:

 /*** 初始化处理器,一般在这里获取我们需要的工具类* @param processingEnvironment*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);}/*** 指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合* @return*/@Overridepublic Set<String> getSupportedAnnotationTypes() {return super.getSupportedAnnotationTypes();}/*** 指定java版本* @return*/@Overridepublic SourceVersion getSupportedSourceVersion() {return super.getSupportedSourceVersion();}/*** 处理器实际处理逻辑入口* @param set* @param roundEnvironment* @return*/@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {return false;}

我们平常去餐厅,会点餐,然后根据所点食物的名称,餐厅服务员会告诉我们多少钱,就拿这个作为栗子吧;

这里定义一个ChineseFood接口,以及它的两个实现类Dumplings和Noodles,还有Restaurant类(负责点餐),看代码,相信不用过多解释了;

public interface ChineseFood {float getPrice();
}public class Dumplings implements ChineseFood {@Overridepublic float getPrice() {return 16f;}
}public class Noodles implements ChineseFood {@Overridepublic float getPrice() {return 10.5f;}
}public class Restaurant {private ChineseFood order(String name){if ("noodles".equals(name)){return new Noodles();}if ("dumplings".equals(name)){return new Dumplings();}throw new  IllegalArgumentException("Unknown ChineseFood name : " + name);}public static void main(String[] args) throws IOException {Restaurant restaurant = new Restaurant();ChineseFood food = restaurant.order("dumplings");System.out.println("dumplings's price is " + food.getPrice());}
}

我们试着用注解来解决这个问题;

step1

先新建一个Java Library,命名为annotations,在这里定义一个注解@Factory,这里有个type字段,比如,如果我们去餐厅,并不是点的ChineseFood,而是Pizza或者Hamburger,那它们便不属于ChineseFood,而是WesternFood,而id这个字段,便是我们为每种ChineseFood指定的唯一id,譬如"noodles"

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Factory {/*** 所属的type* @return*/Class type();/*** 唯一的id* @return*/String id();
}

故,我们给@Factory这个注解定义一些规则:

1. 只有类可以被@Factory注解,因为接口或者抽象类并不能用new操作实例化;
2. 被@Factory注解的类必须直接或者间接的继承于type()指定的类型;
3. id只能是String类型,并且在同一个type组中必须唯一;
4. 具有相同的type的注解类,将被聚合在一起生成一个工厂类。这个生成的类使用Factory后缀,例如type = ChineseFood.class,将生成ChineseFoodFactory工厂类;

step2

新建一个Java Library,命名为processor,新建一个类FactoryProcessor,继承AbstractProcessor,然后设置指定的注解和java版本,Init方法一般用来获取一些工具类;

public class FactoryProcessor extends AbstractProcessor {private Messager mMessager;private Elements mElementUtils;private Types mTypeUtils;private Filer mFiler;/*** 初始化处理器,一般在这里获取我们需要的工具类* @param processingEnvironment*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);mMessager = processingEnvironment.getMessager();mElementUtils = processingEnvironment.getElementUtils();mTypeUtils = processingEnvironment.getTypeUtils();mFiler = processingEnvironment.getFiler();}/*** 指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合* @return*/@Overridepublic Set<String> getSupportedAnnotationTypes() {Set<String> annotations = new LinkedHashSet<>();annotations.add(Factory.class.getCanonicalName());return annotations;}/*** 指定java版本,一般返回最新版本即可* @return*/@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}/*** 处理器实际处理逻辑入口* @param set* @param roundEnvironment* @return*/@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {return true;}
}

我们看下通过init方法获得的四个工具类,这里先做简单的介绍:

  • Messager:提供给注解处理器一个报告错误、警告以及提示信息的途径,它能够打印非常优美的错误信息。除此之外,你还可以连接到出错的元素。在像IntelliJ这种现代的IDE(集成开发环境)中,第三方开发者可以直接点击错误信息,IDE将会直接跳转到第三方开发者项目的出错的源文件的相应的行;
  • Elements:一个用来处理Element的工具类;
  • Types:一个用来处理TypeMirror的工具类;
  • Filer:使用Filer我们可以创建文件;

step3

为了能够使用注解处理器,我们需要用一个服务文件来注册它。比较简单的处理方式,是使用Google开源的AutoService,在Project Structure里搜索“auto-service"查找该库并添加,最后再在注解处理器中添加@AutoService(Processor.class)就可以了;

@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor{}

接下来,需要在app/build.gradle下添加:

dependencies {implementation project(':annotations')annotationProcessor project(':processor')
}

step4
这里主要是process()的实现,刚刚定义的4条规则,在process里面我们会一一实现;

如若被注解的元素不满足规则,我们会抛出异常,这里自定义一个ProcessorException类,然后通过Messager(这是在init()方法中获得的工具类)来打印相关信息:

public class ProcessorException  extends Exception{private Element element;public ProcessorException(Element element, String msg, Object... args) {super(String.format(msg, args));this.element = element;}public Element getElement() {return element;}
}//在FactoryProcessor类中private void showError(ProcessorException e){mMessager.printMessage(Diagnostic.Kind.ERROR,e.getMessage(),e.getElement());}

第一条规则:
只有类可以被@Factory注解,因为接口或者抽象类并不能用new操作实例化;

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {try {//遍历所有注解了@Factory的元素for (Element element : roundEnvironment.getElementsAnnotatedWith(Factory.class)){//1.只有类可以被@Factory注解,因为接口或者抽象类并不能用new操作实例化;if (ElementKind.CLASS != element.getKind()){throw new ProcessorException(element, "Only classes can be annotated with @%s", Factory.class.getSimpleName());}//检查是否是抽象类checkIsAbstract(element);}}private void checkIsAbstract(Element element) throws ProcessorException {Set<Modifier> modifierSet = element.getModifiers();for (Modifier modifier : modifierSet){if (Modifier.ABSTRACT.toString().equals(modifier.toString()) ){throw new ProcessorException(element, "abstract classes can't be annotated with @%s", Factory.class.getSimpleName());}}
}

第二条规则:
@Factory注解的类必须直接或者间接的继承于type()指定的类型;

为了方便以后的操作,这里先定义一个FactoryAnnotationClass类,将被@Factory注解的类的关键信息保存下;

public class FactoryAnnotationClass {/*** 被Factory注解的元素*/private TypeElement element;/*** {@link Factory#id()} 注解时设置的id*/private String id;/*** {@link Factory#type()} 注解时设置的type类型的合法全名;*/private String superTypeName;public FactoryAnnotationClass(TypeElement element) {this.element = element;Factory factory = element.getAnnotation(Factory.class);id = factory.id();superTypeName = getRequiredTypeClassName(element);}private String getRequiredTypeClassName(TypeElement currentElement){Factory factory = currentElement.getAnnotation(Factory.class);String requiredTypeClassName;try {//这种情况是针对第三方jar包Class clazz = factory.type();requiredTypeClassName = clazz.getCanonicalName();}catch (MirroredTypeException e){//平常在源码上注解时,都会走到这里DeclaredType declaredType = (DeclaredType) e.getTypeMirror();TypeElement element = (TypeElement) declaredType.asElement();requiredTypeClassName = element.getQualifiedName().toString();}return requiredTypeClassName;}}

注释写的很清楚了,主要是去获得type成员的合法全名时,直接通过getCanonicalName()可能会失败;

  • 当type为第三方jar包时,这个类已经被编译,是可以直接通过Class去获取的;
  • 但如果是被@Factory注解的源码,即我们这个栗子的情况,会抛MirroredTypeException异常,此时便需要通过DeclaredType去获取type的合法全名;

接下来,在process方法中验证第二条规则

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {...//强转TypeElement typeElement = (TypeElement) element;FactoryAnnotationClass currentClass = new FactoryAnnotationClass(typeElement);//2.被@Factory注解的类必须直接或者间接的继承于type()指定的类型checkType(currentClass);
}private void checkType(FactoryAnnotationClass annotationClass) throws ProcessorException {String superTypeName = annotationClass.getSuperTypeName();TypeElement typeElement = annotationClass.getElement();//根据type的class名来获取TypeElementTypeElement requiredElement = mElementUtils.getTypeElement(superTypeName);//若type类是一个接口if (ElementKind.INTERFACE == requiredElement.getKind()){// 检查接口是否实现了if (!typeElement.getInterfaces().contains(requiredElement.asType())) {throw new ProcessorException(typeElement, "The class %s annotated with @%s must implement the interface %s",typeElement.getQualifiedName().toString(), Factory.class.getSimpleName(),superTypeName);}}else {// 检查子类TypeElement currentClass = typeElement;while (true) {TypeMirror superClassType = currentClass.getSuperclass();if (superClassType.getKind() == TypeKind.NONE) {// 到达了基本类型(java.lang.Object), 所以退出throw new ProcessorException(typeElement, "The class %s annotated with @%s must inherit from %s",typeElement.getQualifiedName().toString(), Factory.class.getSimpleName(),superTypeName);}if (superClassType.toString().equals(superTypeName)) {// 找到了要求的父类break;}// 在继承树上继续向上搜寻currentClass = (TypeElement) mTypeUtils.asElement(superClassType);}}
}

checkType()方法走完后,就代表当前的Element是符合前两条规则的,这时候,我们将这些复合规则的Element存起来;

在这里,定义FactoryGroup类,里面有个HashMap的成员变量,我们的逻辑是,先将具有相同type类型的Element放入一个set集合中,再把type类型的合法全名做key,将这个set集合做为value;

这里存储set集合的过程中,需要考虑到第三条规则,即:

id只能是String类型,并且在同一个type组中必须唯一;

public class FactoryGroup {/*** key:Factory type 类型的合法全名*/private Map<String,Set<FactoryAnnotationClass>> mGroupMap;FactoryGroup() {mGroupMap = new HashMap<>();}void add(FactoryAnnotationClass annotationClass) throws ProcessorException {Set<FactoryAnnotationClass> set = mGroupMap.get(annotationClass.getSuperTypeName());if (null == set){set = new HashSet<>();set.add(annotationClass);mGroupMap.put(annotationClass.getSuperTypeName(),set);}else {for (FactoryAnnotationClass factoryAnnotationClass : set){//3.id只能是String类型,并且在同一个type组中必须唯一;if (annotationClass.getId().equals(factoryAnnotationClass.getId())){throw new ProcessorException(annotationClass.getElement(),"Conflict: The class %s annotated with @%s with id ='%s' but %s already uses the same id",annotationClass.getElement().getQualifiedName().toString(),Factory.class.getSimpleName(),annotationClass.getId(),factoryAnnotationClass.getElement().getQualifiedName().toString());}}set.add(annotationClass);}}
}

接着,在process中调用FactoryGroup的add()方法

public class FactoryProcessor extends AbstractProcessor {private FactoryGroup mFactoryGroup;@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {for (Element element : roundEnvironment.getElementsAnnotatedWith(Factory.class)){//符合条件的加入到group中mFactoryGroup.add(currentClass);}...}
}

最后,看第四条规则:

具有相同的type的注解类,将被聚合在一起生成一个工厂类。这个生成的类使用Factory后缀,例如type = ChineseFood.class,将生成ChineseFoodFactory工厂类

也就是说,我们需要为相同type的注解类,生成一个类,这个类以type的简单名字加Factory结尾,譬如,在这个栗子,就应该是ChineseFoodFactory,具体生成类代码如下,在FactoryGroup类中:

/*** 具有相同的type的注解类,将被聚合在一起生成一个工厂类。* 这个生成的类使用*Factory*后缀,* 例如type = ChineseFood.class,将生成ChineseFoodFactory工厂类;* @param elementUtils* @param filer* @throws IOException*/
void createFactory(Elements elementUtils, Filer filer) throws IOException {if (null != mGroupMap){for (String typeName : mGroupMap.keySet()){TypeElement superTypeElement = elementUtils.getTypeElement(typeName);PackageElement pkg = elementUtils.getPackageOf(superTypeElement);//isUnnamed;如果此包是一个未命名的包,则返回 true,否则返回 falseString packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();//创建一个create方法MethodSpec.Builder method = MethodSpec.methodBuilder("create").addModifiers(Modifier.PUBLIC).addParameter(String.class, "id").returns(TypeName.get(superTypeElement.asType()));// 判断id是否为空method.beginControlFlow("if (id == null)").addStatement("throw new IllegalArgumentException($S)", "id is null!").endControlFlow();Set<FactoryAnnotationClass> classSet = mGroupMap.get(typeName);// 根据id去返回各自的实现类for (FactoryAnnotationClass item : classSet) {method.beginControlFlow("if ($S.equals(id))", item.getId()).addStatement("return new $L()", item.getElement().getQualifiedName().toString()).endControlFlow();}method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = ");TypeSpec typeSpec = TypeSpec.classBuilder(superTypeElement.getSimpleName().toString() + Factory.class.getSimpleName()).addMethod(method.build()).build();JavaFile.builder(packageName, typeSpec).build().writeTo(filer);}}
}

然后我们需要在FactoryProcessor的process中,在结束for循环后调用createFactory方法:

private FactoryGroup mFactoryGroup;@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {for (Element element : roundEnvironment.getElementsAnnotatedWith(Factory.class)){//符合条件的加入到group中mFactoryGroup.add(currentClass);}...mFactoryGroup.createFactory(mElementUtils,mFiler);mFactoryGroup.clear();
}

这里先埋个点,即在process中调用了mFactoryGroup.clear(),这是为啥?
ok,到这里,FactoryProcessor基本已经实现了。

step5
@Factory注解NoodlesDumplings两个类:

@Factory(id = "noodles", type = ChineseFood.class)
public class Noodles implements ChineseFood {@Overridepublic float getPrice() {return 10.5f;}
}@Factory(id = "dumplings", type = ChineseFood.class)
public class Dumplings implements ChineseFood {@Overridepublic float getPrice() {return 16f;}
}

step6

先clean project,再build一下,会看到生成了ChineseFoodFactory.java的文件
`
具体代码如下:

class ChineseFoodFactory {public ChineseFood create(String id) {if (id == null) {throw new IllegalArgumentException("id is null!");}if ("noodles".equals(id)) {return new com.example.annotation.food.Noodles();}if ("dumplings".equals(id)) {return new com.example.annotation.food.Dumplings();}throw new IllegalArgumentException("Unknown id = " + id);}
}

接下来,就可以"点餐"了,我点了个饺子…

public class Restaurant {public static void main(String[] args) throws IOException {ChineseFoodFactory factory = new ChineseFoodFactory();ChineseFood food = factory.create("dumplings");System.out.println("dumplings's price is " + food.getPrice());}
}

题外话:
还记得刚刚说的,在process中调用了mFactoryGroup.clear()吗?
是这样的,在这个栗子中,FactoryProcessor在处理注解时,process方法会运行3次:

  • 第一次时,处理了Dumplings.java和Noodles.java上的@Factory注解;同时也生成了ChineseFoodFactory.java文件,此时,ChineseFoodFactory上也有可能被注解;
  • 第二次时,处理ChineseFoodFactory.java上的注解;
  • 第三次时,没有被@Factory注解的元素,over;

*代码地址:https://github.com/suya1994/annotations.git

*参考资料:
1.https://www.jianshu.com/p/9ca78aa4ab4d
2.https://race604.com/annotation-processing/

Android注解之从入门到并没有放弃相关推荐

  1. android 注解和反射 (入门以及使用)

    先来看一看今天的效果: 代码效果: 效果不重要,重要的是代码: 注解 官方解释: 从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这 ...

  2. Android注解处理器APT技术简介

    Android注解处理器APT技术简介 APT是什么 例子 APT有什么用 (好处) APT原理 (为什么) APT实践 (怎么做) 参考 APT是什么 APT全称"Annotation P ...

  3. 开发自己的山寨Android注解框架

    目录 开发自己的山寨Android注解框架 开发自己的山寨Android注解框架 参考 Github黄油刀 Overview 在上一章我们学习了Java的注解(Annotation),但是我想大家可能 ...

  4. 自定义Android注解Part3:绑定

    上一节我们已经将自动生成注解代码部分介绍完毕,今天这篇文章是自定义Android注解系列的最后一篇文章.希望大家这一路走来有所收获. 经过前面的了解,我们三大部分:butterknife-annota ...

  5. flex+android+教程,android开发flex4.5入门教程.pdf

    android开发flex4.5入门教程 中国矿业大学教务部 教务通知(2013 )第33 号 关于做好各级"大学生创新训练计划" 项目中期检查和结题验收的通知 各学院: 为加强我 ...

  6. 自定义Android注解Part2:代码自动生成

    上一期我们已经把butterknife-annotations中的注解变量都已经定义好了,分别为BindView.OnClick与Keep. 如果你是第一次进入本系列文章,强烈推荐跳到文章末尾查看上篇 ...

  7. 玩转java(Android)注解

    2019独角兽企业重金招聘Python工程师标准>>> 玩转java(Android)注解 1. java标准(原生)注解概览 Java API 中,在java.lang.java. ...

  8. Android 音视频开发入门指南

    最近收到很多网友通过邮件或者留言说想学习音视频开发,该如何入门,我今天专门写篇文章统一回复下吧. 音视频这块,目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的,希望我后面能挤出时间整 ...

  9. android自定义美颜相机完整程序,Android OpenGL ES从入门到进阶(一)—— 五分钟开发一款美颜相机...

    源码链接:https://github.com/smzhldr/AGLFramework 一.前言 商店里有数十款的美颜相机类产品,其实现原理基本上都是以OpenGL ES为核心的特效处理,大神可以忽 ...

最新文章

  1. jq 读取office插件_800+页麦肯锡经典图示发送!让你不用插件,轻松搞定逻辑图...
  2. 多个不同的app应用间应该如何进行消息推送呢?
  3. java用线程插入一张图片_JAVA中怎么用线程实现图片的切换?
  4. python迭代法求解非线性方程_荐【数学知识】非线性方程求解的二分法以及牛顿迭代法...
  5. 小米路由器怎么连接无盘服务器,播放器+服务器的方法瞬间玩转小米路由方法图文介绍...
  6. Spring 框架基础(04):AOP切面编程概念,几种实现方式演示
  7. Laravel核心解读 -- Response
  8. 如何在npm上发布项目
  9. 《别做正常的傻瓜》读书笔记
  10. StarTeam 使用小记
  11. 19muduo_base库源码分析(十)
  12. 结构体C语言王者归来
  13. 微信经典飞机大战,承载多少人的回忆!
  14. AWS SQS, SWF and SNS
  15. Linux:CentOS 7 解压 7zip 压缩的文件
  16. 富文本编辑器 froala-editor
  17. QC1.0、QC2.0、QC3.0、QC4.0协议介绍
  18. Java学习总结与反思
  19. python模拟行星运动_动态模拟运行太阳系的行星运转
  20. 【论文】 Deep Learning Architecture for Short-Term Passenger Flow Forecasting in Urban Rail Transit

热门文章

  1. [Linux Kernel] memory-barriers 内存屏蔽 官方文档
  2. 七牛云 转码_七牛云试用指南-音视频基本处理
  3. 51 币圈里外的强者愈强----超级君扯淡录【2020-07-13 2100】
  4. PHP调用QQ互联接口实现QQ登录网站
  5. 石英晶振为何应用于风扇
  6. 彩虹六号mute影响服务器笑话,有哪些《彩虹六号》玩家才懂的笑话?
  7. C语言/771.宝石与石头
  8. jquery.form 异步上传文件(写的不是很好,望大家多海涵)
  9. 深度学习入门笔记(十八):卷积神经网络(一)
  10. 深度学习入门笔记(十一):权重初始化