本文已授权微信公众号《非著名程序员》原创首发,转载请务必注明出处。

xUtils3源码解析系列

一. Android xUtils3源码解析之网络模块
二. Android xUtils3源码解析之图片模块
三. Android xUtils3源码解析之注解模块
四. Android xUtils3源码解析之数据库模块

初始化

public class BaseActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);x.view().inject(this);}
}public class BaseFragment extends Fragment {@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {return x.view().inject(this, inflater, container);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这里没有贴最开始的初始化x.Ext.init(this),因为这行代码的作用是获取ApplicationContext,而注解模块并不需要ApplicationContext。真正的初始化是在这里。实际上这里称作“初始化”有些不太合适,因为xUtils3中View注解都是@Retention(RetentionPolicy.RUNTIME)类型的,运行时才是真正的初始化,x.view().inject(this)是解析注解的地方。注解一共就这俩部分,先姑且这么称呼吧。下文以x.view().inject(this)为例进行分析,Fragment中和这个属于殊途同归,不再赘述。

View注解

注解的作用只能是“标志”,如果注解里定义的有属性,那么还能获取属性具体的值。属性的值没有default值,那么使用注解时此属性为必填项。反之亦反。我们先看下两个View注解ContentView和ViewInject的具体实现,之后统一查看注解解析相关代码。

ContentView标签

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {int value();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

ContentView注解修饰的对象范围为TYPE(用于描述类、接口或enum声明),保留的时间为RUNTIME(运行时有效),此外还定义了一个属性value,注意:是属性,不是方法。

ViewInject

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {int value();/* parent view id */int parentId() default 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

ViewInject注解修饰的对象范围为FIELD(用于描述属性),保留的时间为RUNTIME(运行时有效)。

View注解解析

在Activity或者Fragment中首先要做的就是初始化xUtils3注解,即x.view().inject(this)。前文也说过:这个过程实际是View注解解析的过程。下面就以这一过程跟进。

x.view()

public final class x {public static ViewInjector view() {if (Ext.viewInjector == null) {ViewInjectorImpl.registerInstance();}return Ext.viewInjector;}
}public final class ViewInjectorImpl implements ViewInjector {public static void registerInstance() {if (instance == null) {synchronized (lock) {if (instance == null) {instance = new ViewInjectorImpl();}}}x.Ext.setViewInjector(instance);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

获取ViewInjectorImpl唯一实例,并赋值给ViewInjector对象。之后调用ViewInjectorImpl.inject()方法解析上面两个View注解。

ViewInjectorImpl.inject()

public final class ViewInjectorImpl implements ViewInjector {@Overridepublic void inject(Activity activity) {Class<?> handlerType = activity.getClass();try {// 获取ContentView标签,主要是为了获取ContentView.value(),即R.layout.xxxContentView contentView = findContentView(handlerType);if (contentView != null) {// 获取R.layout.xxxint viewId = contentView.value();if (viewId > 0) {// 获取setContentView()方法实例Method setContentViewMethod = handlerType.getMethod("setContentView", int.class);// 反射调用setContentView(),并设置R.layout.xxxsetContentViewMethod.invoke(activity, viewId);}}} catch (Throwable ex) {LogUtil.e(ex.getMessage(), ex);}// 遍历被注解的属性和方法injectObject(activity, handlerType, new ViewFinder(activity));}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

几乎每行都添加了注释,应该比较清晰了,这里还是大概说下吧。在反射setContentView ()之后,ContentView注解的作用就结束了,毕竟ContentView注解的作用只有一个:设置Activity/Fragment布局。

ViewInjectorImpl.injectObject()

public final class ViewInjectorImpl implements ViewInjector {private static final HashSet<Class<?>> IGNORED = new HashSet<Class<?>>();static {IGNORED.add(Object.class);IGNORED.add(Activity.class);IGNORED.add(android.app.Fragment.class);try {IGNORED.add(Class.forName("android.support.v4.app.Fragment"));IGNORED.add(Class.forName("android.support.v4.app.FragmentActivity"));} catch (Throwable ignored) {}}private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {if (handlerType == null || IGNORED.contains(handlerType)) {return;}// 从父类到子类递归injectObject(handler, handlerType.getSuperclass(), finder);// 获取class中所有属性Field[] fields = handlerType.getDeclaredFields();if (fields != null && fields.length > 0) {for (Field field : fields) {// 获取字段类型Class<?> fieldType = field.getType();if (/* 不注入静态字段 */     Modifier.isStatic(field.getModifiers()) ||/* 不注入final字段 */    Modifier.isFinal(field.getModifiers()) ||/* 不注入基本类型字段 */  fieldType.isPrimitive() ||/* 不注入数组类型字段 */  fieldType.isArray()) {continue;}// 字段是否被ViewInject注解修饰ViewInject viewInject = field.getAnnotation(ViewInject.class);if (viewInject != null) {try {// 通过ViewFinder查找ViewView view = finder.findViewById(viewInject.value(), viewInject.parentId());if (view != null) {// 暴力反射,设置属性可使用field.setAccessible(true);// 关联被ViewInject修饰的属性和Viewfield.set(handler, view);} else {throw new RuntimeException("Invalid @ViewInject for "+ handlerType.getSimpleName() + "." + field.getName());}} catch (Throwable ex) {LogUtil.e(ex.getMessage(), ex);}}}} // end inject view// 方法注解Event的解析,下文会讲...}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

因为Activity/Fragment可能还有BaseActivity/BaseFragment。所以injectObject()是个递归方法,递归的出口在于最上面的判断,及父类不等于系统的那几个类。finder.findViewById(id,pid)参数id为R.id.xxx,pid默认为0。在ViewFinder中查找View的代码如下:

/*package*/ final class ViewFinder {public View findViewById(int id, int pid) {View pView = null;if (pid > 0) {pView = this.findViewById(pid);}View view = null;if (pView != null) {view = pView.findViewById(id);} else {view = this.findViewById(id);}return view;}public View findViewById(int id) {if (view != null) return view.findViewById(id);if (activity != null) return activity.findViewById(id);return null;}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

还是通过activity.findViewById(id)来查找控件的。View注解的作用是代替我们写了findViewById这行代码,一般用于敏捷开发。代价是增加了一次反射,每个控件都会。而反射是比较牺牲性能的做法,所以使用View注解算是有利有弊吧。

事件注解

Event

/*** 事件注解.* 被注解的方法必须具备以下形式:* 1. private 修饰* 2. 返回值类型没有要求* 3. 参数签名和type的接口要求的参数签名一致.*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {/** 控件的id集合, id小于1时不执行ui事件绑定. */int[] value();/** 控件的parent控件的id集合, 组合为(value[i], parentId[i] or 0). */int[] parentId() default 0;/** 事件的listener, 默认为点击事件. */Class<?> type() default View.OnClickListener.class;/** 事件的setter方法名, 默认为set+type#simpleName. */String setter() default "";/** 如果type的接口类型提供多个方法, 需要使用此参数指定方法名. */String method() default "";
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

Event中的属性,比View注解要多一些,毕竟Event也需要findViewById过程,并且还要处理参数,事件等等。默认type属性为View.OnClickListener.class,即点击事件。

public final class ViewInjectorImpl implements ViewInjector {private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {// 获取类中所有的方法Method[] methods = handlerType.getDeclaredMethods();if (methods != null && methods.length > 0) {for (Method method : methods) {// 方法是静态或者不是私有则验证不通过if (Modifier.isStatic(method.getModifiers())|| !Modifier.isPrivate(method.getModifiers())) {continue;}//检查当前方法是否是event注解的方法Event event = method.getAnnotation(Event.class);if (event != null) {try {// R.id.xxx数组(可能多个控件点击事件共用同一个方法)int[] values = event.value();int[] parentIds = event.parentId();int parentIdsLen = parentIds == null ? 0 : parentIds.length;//循环所有id,生成ViewInfo并添加代理反射for (int i = 0; i < values.length; i++) {int value = values[i];if (value > 0) {ViewInfo info = new ViewInfo();info.value = value;info.parentId = parentIdsLen > i ? parentIds[i] : 0;// 设置可反射访问method.setAccessible(true);EventListenerManager.addEventMethod(finder, info, event, handler, method);}}} catch (Throwable ex) {LogUtil.e(ex.getMessage(), ex);}}}} // end inject event}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

这里主要是查找被Event注解修饰的方法,之后设置可访问(method.setAccessible(true)),看样子还是反射调用咯。

EventListenerManager.addEventMethod(finder, info, event, handler, method)

/*package*/ final class EventListenerManager {public static void addEventMethod(//根据页面或view holder生成的ViewFinderViewFinder finder,//根据当前注解ID生成的ViewInfoViewInfo info,//注解对象Event event,//页面或view holder对象Object handler,//当前注解方法Method method) {try {// 查找指定控件View view = finder.findViewByInfo(info);if (view != null) {// 注解中定义的接口,比如Event注解默认的接口为View.OnClickListenerClass<?> listenerType = event.type();// 默认为空,注解接口对应的Set方法,比如setOnClickListener方法String listenerSetter = event.setter();if (TextUtils.isEmpty(listenerSetter)) {// 拼接set方法名,例如:setOnClickListenerlistenerSetter = "set" + listenerType.getSimpleName();}// 默认为""String methodName = event.method();boolean addNewMethod = false;DynamicHandler dynamicHandler = null;...// 如果还没有注册此代理if (!addNewMethod) {dynamicHandler = new DynamicHandler(handler);dynamicHandler.addMethod(methodName, method);// 生成的代理对象实例,比如View.OnClickListener的实例对象listener = Proxy.newProxyInstance(listenerType.getClassLoader(),new Class<?>[]{listenerType},dynamicHandler);listenerCache.put(info, listenerType, listener);}// 获取set方法,例如:setOnClickListenerMethod setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);// 反射调用set方法。例如setOnClickListener(new OnClicklistener)setEventListenerMethod.invoke(view, listener);}} catch (Throwable ex) {LogUtil.e(ex.getMessage(), ex);}}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

使用动态代理DynamicHandler实例化listenerType(例如:new OnClickListener),之后通过反射设置事件(例如点击事件,btn.setOnClickListener(new OnClickListener))。这么一套流程流程下来,我惊讶的发现,我们定义的方法好像完全没被调用!!

其实猫腻都在DynamicHandler这个动态代理中。注意一个细节,在实例化DynamicHandler的时候穿递的是Activity/Fragment。然后调用dynamicHandler.addMethod(methodName, method)方法的时候,将method(当前注解方法)传递进去了。完整类名有,方法名字有。齐活儿~

DynamicHandler

    public static class DynamicHandler implements InvocationHandler {// 存放代理对象,比如Fragment或view holderprivate WeakReference<Object> handlerRef;// 存放代理方法private final HashMap<String, Method> methodMap = new HashMap<String, Method>(1);private static long lastClickTime = 0;public DynamicHandler(Object handler) {this.handlerRef = new WeakReference<Object>(handler);}public void addMethod(String name, Method method) {methodMap.put(name, method);}public Object getHandler() {return handlerRef.get();}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Object handler = handlerRef.get();if (handler != null) {String eventMethod = method.getName();method = methodMap.get(eventMethod);if (method == null && methodMap.size() == 1) {for (Map.Entry<String, Method> entry : methodMap.entrySet()) {if (TextUtils.isEmpty(entry.getKey())) {method = entry.getValue();}break;}}if (method != null) {if (AVOID_QUICK_EVENT_SET.contains(eventMethod)) {long timeSpan = System.currentTimeMillis() - lastClickTime;if (timeSpan < QUICK_EVENT_TIME_SPAN) {LogUtil.d("onClick cancelled: " + timeSpan);return null;}lastClickTime = System.currentTimeMillis();}try {return method.invoke(handler, args);} catch (Throwable ex) {throw new RuntimeException("invoke method error:" +handler.getClass().getName() + "#" + method.getName(), ex);}} else {LogUtil.w("method not impl: " + eventMethod + "(" + handler.getClass().getSimpleName() + ")");}}return null;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

首先调用method = methodMap.get(eventMethod),由key查找方法名,之前我们传进来的是“”。以OnClickListener{ void onClick()}为例,由onClick为key查找,当然查找不到咯。然后遍历methodMap设置method为我们在Activity/Fragment中定义的方法名。if (AVOID_QUICK_EVENT_SET.contains(eventMethod))这行代码是防止快速双击的,设置间隔为300ms,最后通过反射调用在Activity/Fragment中特定被Event注解的方法。这里巧在没有调用OnClicklistener#onClick(),而是在调用OnClicklistener#onClick()的时候,真正调用的是我们在Activity/Fragment中定义的方法。体会一下这个过程。这里还需要注意一个地方,因为return method.invoke(handler, args),最后需要return返回值。所以在Activity/Fragment中定义方法的返回值,必须要和目标方法(例如:onClick())的返回值一样。

Android xUtils3源码解析之注解模块相关推荐

  1. Android xUtils3源码解析之图片模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

  2. Android xUtils3源码解析之数据库模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

  3. Android Lifecycle源码解析(一)

    Android Lifecycle源码解析(一) 首先我们看HomeActivity中我们添加到一行代码 public class HomeActivity extends AppCompatActi ...

  4. 【Android】Android Broadcast源码解析

    Android Broadcast源码解析 一.静态广播的注册 静态广播是通过PackageManagerService在启动的时候扫描已安装的应用去注册的. 在PackageManagerServi ...

  5. Spring源码深度解析(郝佳)-学习-源码解析-基于注解切面解析(一)

    我们知道,使用面积对象编程(OOP) 有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共的行为时,例如日志,安全检测等,我们只有在每个对象引用公共的行为,这样程序中能产生大量的重复代码,程序就 ...

  6. Spring源码深度解析(郝佳)-学习-源码解析-基于注解注入(二)

    在Spring源码深度解析(郝佳)-学习-源码解析-基于注解bean解析(一)博客中,己经对有注解的类进行了解析,得到了BeanDefinition,但是我们看到属性并没有封装到BeanDefinit ...

  7. 【Android】Android Parcelable 源码解析

    Android Parcelable 源码解析 大家都知道,要想在Intent里面传递一些非基本类型的数据,有两种方式,一种实现Parcelable,另一种是实现Serializable接口.今天先不 ...

  8. [Android] Handler源码解析 (Java层)

    之前写过一篇文章,概述了Android应用程序消息处理机制.本文在此文基础上,在源码级别上展开进行概述 简单用例 Handler的使用方法如下所示: Handler myHandler = new H ...

  9. android sdk 源码解析

    AndroidSdkSourceAnalysis:https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis 第一期 Class 分析 ...

最新文章

  1. Struts2漏洞的前因后果
  2. python中哈希是什么意思_在python中向量化特征哈希
  3. php 解析 saml协议,解出SAMLRequest的代码示例
  4. mysql flask-login_Flask web模板六–Flask-Login完成登录验证
  5. Linux内核深入理解中断和异常(7):中断下半部:Softirq, Tasklets and Workqueues
  6. Scikit-Learn 学得如何?程序员不容错过十大实用功能来袭
  7. python装饰器打印函数执行时间_使用python装饰器计算函数运行时间的实例
  8. Vijos 1041题:神风堂人数
  9. oracle字典在线查字手写,在线字典手写输入
  10. hibernate中持久化类的编写规则和主键生成策略
  11. android信鸽推送通知栏,【信鸽推送】点击推送通知后,默认会从程序Launcher进入,返回时会回到主界面的问题...
  12. Unity Mecanim动画的实现(一):基本程序
  13. python做淘宝客_python 做淘宝客程序(2)
  14. 写给这批≥30岁的测试工程师 。
  15. Android 实现仿微信朋友圈九宫格图片+NineGridView+ImageWatcher(图片查看:1.预览,2.拖动,3.放大,4.左右滑动,5.长按保存到手机)的功能
  16. HTML-浮动(float)
  17. 51nod 1298 圆与三角形
  18. oracle查询当天的数据(当年,当月,当日)
  19. 支持他们的应用程序突袭Cloudberry狂潮
  20. 【科普】十大科研好用软件

热门文章

  1. 杨辉三角 C语言(改)
  2. pngquant有损压缩png资源
  3. 精讲Spring得IOC和AOP
  4. 大学恋爱常见的几种心理因素?
  5. 论文阅读 【CVPR-2022】 A Simple Multi-Modality Transfer Learning Baseline for Sign Language Translation
  6. eclipse 解决启动慢、运行慢的方法总结(最全)
  7. Pink老师-简易计算器
  8. font-family对照表
  9. ARMv8之arm64架构汇编知识
  10. 关于s3c6410的SD卡启动