转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:【张鸿洋的博客】

上一篇带大家初步了解了EventBus的使用方式,详见:Android EventBus实战 没听过你就out了,本篇博客将解析EventBus的源码,相信能够让大家深入理解该框架的实现,也能解决很多在使用中的疑问:为什么可以这么做?为什么这么做不好呢?

1、概述

一般使用EventBus的组件类,类似下面这种方式:

  1. public class SampleComponent extends Fragment
  2. {
  3. @Override
  4. public void onCreate(Bundle savedInstanceState)
  5. {
  6. super.onCreate(savedInstanceState);
  7. EventBus.getDefault().register(this);
  8. }
  9. public void onEventMainThread(param)
  10. {
  11. }
  12. public void onEventPostThread(param)
  13. {
  14. }
  15. public void onEventBackgroundThread(param)
  16. {
  17. }
  18. public void onEventAsync(param)
  19. {
  20. }
  21. @Override
  22. public void onDestroy()
  23. {
  24. super.onDestroy();
  25. EventBus.getDefault().unregister(this);
  26. }
  27. }

大多情况下,都会在onCreate中进行register,在onDestory中进行unregister ;

看完代码大家或许会有一些疑问:

1、代码中还有一些以onEvent开头的方法,这些方法是干嘛的呢?

在回答这个问题之前,我有一个问题,你咋不问register(this)是干嘛的呢?其实register(this)就是去当前类,遍历所有的方法,找到onEvent开头的然后进行存储。现在知道onEvent开头的方法是干嘛的了吧。

2、那onEvent后面的那些MainThread应该是什么标志吧?

嗯,是的,onEvent后面可以写四种,也就是上面出现的四个方法,决定了当前的方法最终在什么线程运行,怎么运行,可以参考上一篇博客或者细细往下看。

既然register了,那么肯定得说怎么调用是吧。

  1. EventBus.getDefault().post(param);

调用很简单,一句话,你也可以叫发布,只要把这个param发布出去,EventBus会在它内部存储的方法中,进行扫描,找到参数匹配的,就使用反射进行调用。

现在有没有觉得,撇开专业术语:其实EventBus就是在内部存储了一堆onEvent开头的方法,然后post的时候,根据post传入的参数,去找到匹配的方法,反射调用之。

那么,我告诉你,它内部使用了Map进行存储,键就是参数的Class类型。知道是这个类型,那么你觉得根据post传入的参数进行查找还是个事么?

下面我们就去看看EventBus的register和post真面目。

2、register

EventBus.getDefault().register(this);

首先:

EventBus.getDefault()其实就是个单例,和我们传统的getInstance一个意思:

  1. /** Convenience singleton for apps using a process-wide EventBus instance. */
  2. public static EventBus getDefault() {
  3. if (defaultInstance == null) {
  4. synchronized (EventBus.class) {
  5. if (defaultInstance == null) {
  6. defaultInstance = new EventBus();
  7. }
  8. }
  9. }
  10. return defaultInstance;
  11. }

使用了双重判断的方式,防止并发的问题,还能极大的提高效率。

然后register应该是一个普通的方法,我们去看看:

register公布给我们使用的有4个:

  1. public void register(Object subscriber) {
  2. register(subscriber, DEFAULT_METHOD_NAME, false, 0);
  3. }
  4. public void register(Object subscriber, int priority) {
  5. register(subscriber, DEFAULT_METHOD_NAME, false, priority);
  6. }
  7. public void registerSticky(Object subscriber) {
  8. register(subscriber, DEFAULT_METHOD_NAME, true, 0);
  9. }
  10. public void registerSticky(Object subscriber, int priority) {
  11. register(subscriber, DEFAULT_METHOD_NAME, true, priority);
  12. }

本质上就调用了同一个:

  1. private synchronized void register(Object subscriber, String methodName, boolean sticky, int priority) {
  2. List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass(),
  3. methodName);
  4. for (SubscriberMethod subscriberMethod : subscriberMethods) {
  5. subscribe(subscriber, subscriberMethod, sticky, priority);
  6. }
  7. }

四个参数

subscriber 是我们扫描类的对象,也就是我们代码中常见的this;

methodName 这个是写死的:“onEvent”,用于确定扫描什么开头的方法,可见我们的类中都是以这个开头。

sticky 这个参数,解释源码的时候解释,暂时不用管

priority 优先级,优先级越高,在调用的时候会越先调用。

下面开始看代码:

  1. List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass(),
  2. methodName);

调用内部类SubscriberMethodFinder的findSubscriberMethods方法,传入了subscriber 的class,以及methodName,返回一个List<SubscriberMethod>。

那么不用说,肯定是去遍历该类内部所有方法,然后根据methodName去匹配,匹配成功的封装成SubscriberMethod,最后返回一个List。下面看代码:

  1. List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass, String eventMethodName) {
  2. String key = subscriberClass.getName() + '.' + eventMethodName;
  3. List<SubscriberMethod> subscriberMethods;
  4. synchronized (methodCache) {
  5. subscriberMethods = methodCache.get(key);
  6. }
  7. if (subscriberMethods != null) {
  8. return subscriberMethods;
  9. }
  10. subscriberMethods = new ArrayList<SubscriberMethod>();
  11. Class<?> clazz = subscriberClass;
  12. HashSet<String> eventTypesFound = new HashSet<String>();
  13. StringBuilder methodKeyBuilder = new StringBuilder();
  14. while (clazz != null) {
  15. String name = clazz.getName();
  16. if (name.startsWith("<a href="http://lib.csdn.net
    /base/17" class="replace_word" title="Java EE知识
    库" target="_blank" style="color:#df3434; font-weight:bold;">java</a>.") || name.startsWith("javax.") || name.startsWith("android.")) {
  17. // Skip system classes, this just degrades performance
  18. break;
  19. }
  20. // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
  21. Method[] methods = clazz.getMethods();
  22. for (Method method : methods) {
  23. String methodName = method.getName();
  24. if (methodName.startsWith(eventMethodName)) {
  25. int modifiers = method.getModifiers();
  26. if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
  27. Class<?>[] parameterTypes = method.getParameterTypes();
  28. if (parameterTypes.length == 1) {
  29. String modifierString = methodName.substring(eventMethodName.length());
  30. ThreadMode threadMode;
  31. if (modifierString.length() == 0) {
  32. threadMode = ThreadMode.PostThread;
  33. } else if (modifierString.equals("MainThread")) {
  34. threadMode = ThreadMode.MainThread;
  35. } else if (modifierString.equals("BackgroundThread")) {
  36. threadMode = ThreadMode.BackgroundThread;
  37. } else if (modifierString.equals("Async")) {
  38. threadMode = ThreadMode.Async;
  39. } else {
  40. if (skipMethodVerificationForClasses.containsKey(clazz)) {
  41. continue;
  42. } else {
  43. throw new EventBusException("Illegal onEvent method, check for typos: " + method);
  44. }
  45. }
  46. Class<?> eventType = parameterTypes[0];
  47. methodKeyBuilder.setLength(0);
  48. methodKeyBuilder.append(methodName);
  49. methodKeyBuilder.append('>').append(eventType.getName());
  50. String methodKey = methodKeyBuilder.toString();
  51. if (eventTypesFound.add(methodKey)) {
  52. // Only add if not already found in a sub class
  53. subscriberMethods.add(new SubscriberMethod(method, threadMode, eventType));
  54. }
  55. }
  56. } else if (!skipMethodVerificationForClasses.containsKey(clazz)) {
  57. Log.d(EventBus.TAG, "Skipping method (not public, static or abstract): " + clazz + "."
  58. + methodName);
  59. }
  60. }
  61. }
  62. clazz = clazz.getSuperclass();
  63. }
  64. if (subscriberMethods.isEmpty()) {
  65. throw new EventBusException("Subscriber " + subscriberClass + " has no public methods called "
  66. + eventMethodName);
  67. } else {
  68. synchronized (methodCache) {
  69. methodCache.put(key, subscriberMethods);
  70. }
  71. return subscriberMethods;
  72. }
  73. }

呵,代码还真长;不过我们直接看核心部分:

22行:看到没,clazz.getMethods();去得到所有的方法:

23-62行:就开始遍历每一个方法了,去匹配封装了。

25-29行:分别判断了是否以onEvent开头,是否是public且非static和abstract方法,是否是一个参数。如果都复合,才进入封装的部分。

32-45行:也比较简单,根据方法的后缀,来确定threadMode,threadMode是个枚举类型:就四种情况。

最后在54行:将method, threadMode, eventType传入构造了:new SubscriberMethod(method, threadMode, eventType)。添加到List,最终放回。

注意下63行:clazz = clazz.getSuperclass();可以看到,会扫描所有的父类,不仅仅是当前类。

继续回到register:

  1. for (SubscriberMethod subscriberMethod : subscriberMethods) {
  2. subscribe(subscriber, subscriberMethod, sticky, priority);
  3. }

for循环扫描到的方法,然后去调用suscribe方法。

  1. // Must be called in synchronized block
  2. private void subscribe(Object subscriber, SubscriberMethod subscriberMethod, boolean sticky, int priority) {
  3. subscribed = true;
  4. Class<?> eventType = subscriberMethod.eventType;
  5. CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
  6. Subscription newSubscription = new Subscription(subscriber, subscriberMethod, priority);
  7. if (subscriptions == null) {
  8. subscriptions = new CopyOnWriteArrayList<Subscription>();
  9. subscriptionsByEventType.put(eventType, subscriptions);
  10. } else {
  11. for (Subscription subscription : subscriptions) {
  12. if (subscription.equals(newSubscription)) {
  13. throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
  14. + eventType);
  15. }
  16. }
  17. }
  18. // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
  19. // subscriberMethod.method.setAccessible(true);
  20. int size = subscriptions.size();
  21. for (int i = 0; i <= size; i++) {
  22. if (i == size || newSubscription.priority > subscriptions.get(i).priority) {
  23. subscriptions.add(i, newSubscription);
  24. break;
  25. }
  26. }
  27. List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
  28. if (subscribedEvents == null) {
  29. subscribedEvents = new ArrayList<Class<?>>();
  30. typesBySubscriber.put(subscriber, subscribedEvents);
  31. }
  32. subscribedEvents.add(eventType);
  33. if (sticky) {
  34. Object stickyEvent;
  35. synchronized (stickyEvents) {
  36. stickyEvent = stickyEvents.get(eventType);
  37. }
  38. if (stickyEvent != null) {
  39. // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
  40. // --> Strange corner case, which we don't take care of here.
  41. postToSubscription(newSubscription, stickyEvent, Looper.getMainLooper() == Looper.myLooper());
  42. }
  43. }
  44. }

我们的subscriberMethod中保存了method, threadMode, eventType,上面已经说了;

4-17行:根据subscriberMethod.eventType,去subscriptionsByEventType去查找一个CopyOnWriteArrayList<Subscription> ,如果没有则创建。

顺便把我们的传入的参数封装成了一个:Subscription(subscriber, subscriberMethod, priority);

这里的subscriptionsByEventType是个Map,key:eventType ; value:CopyOnWriteArrayList<Subscription> ; 这个Map其实就是EventBus存储方法的地方,一定要记住!

22-28行:实际上,就是添加newSubscription;并且是按照优先级添加的。可以看到,优先级越高,会插到在当前List的前面。

30-35行:根据subscriber存储它所有的eventType ; 依然是map;key:subscriber ,value:List<eventType> ;知道就行,非核心代码,主要用于isRegister的判断。

37-47行:判断sticky;如果为true,从stickyEvents中根据eventType去查找有没有stickyEvent,如果有则立即发布去执行。stickyEvent其实就是我们post时的参数。

postToSubscription这个方法,我们在post的时候会介绍。

到此,我们register就介绍完了。

你只要记得一件事:扫描了所有的方法,把匹配的方法最终保存在subscriptionsByEventType(Map,key:eventType ; value:CopyOnWriteArrayList<Subscription> )中;

eventType是我们方法参数的Class,Subscription中则保存着subscriber, subscriberMethod(method, threadMode, eventType), priority;包含了执行改方法所需的一切。

3、post

register完毕,知道了EventBus如何存储我们的方法了,下面看看post它又是如何调用我们的方法的。

再看源码之前,我们猜测下:register时,把方法存在subscriptionsByEventType;那么post肯定会去subscriptionsByEventType去取方法,然后调用。

下面看源码:

  1. /** Posts the given event to the event bus. */
  2. public void post(Object event) {
  3. PostingThreadState postingState = currentPostingThreadState.get();
  4. List<Object> eventQueue = postingState.eventQueue;
  5. eventQueue.add(event);
  6. if (postingState.isPosting) {
  7. return;
  8. } else {
  9. postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
  10. postingState.isPosting = true;
  11. if (postingState.canceled) {
  12. throw new EventBusException("Internal error. Abort state was not reset");
  13. }
  14. try {
  15. while (!eventQueue.isEmpty()) {
  16. postSingleEvent(eventQueue.remove(0), postingState);
  17. }
  18. } finally {
  19. postingState.isPosting = false;
  20. postingState.isMainThread = false;
  21. }
  22. }
  23. }

currentPostingThreadState是一个ThreadLocal类型的,里面存储了PostingThreadState;PostingThreadState包含了一个eventQueue和一些标志位。

  1. private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
  2. @Override
  3. protected PostingThreadState initialValue() {
  4. return new PostingThreadState();
  5. }
  6. }

把我们传入的event,保存到了当前线程中的一个变量PostingThreadState的eventQueue中。

10行:判断当前是否是UI线程。

16-18行:遍历队列中的所有的event,调用postSingleEvent(eventQueue.remove(0), postingState)方法。

这里大家会不会有疑问,每次post都会去调用整个队列么,那么不会造成方法多次调用么?

可以看到第7-8行,有个判断,就是防止该问题的,isPosting=true了,就不会往下走了。

下面看postSingleEvent

  1. private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
  2. Class<? extends Object> eventClass = event.getClass();
  3. List<Class<?>> eventTypes = findEventTypes(eventClass);
  4. boolean subscriptionFound = false;
  5. int countTypes = eventTypes.size();
  6. for (int h = 0; h < countTypes; h++) {
  7. Class<?> clazz = eventTypes.get(h);
  8. CopyOnWriteArrayList<Subscription> subscriptions;
  9. synchronized (this) {
  10. subscriptions = subscriptionsByEventType.get(clazz);
  11. }
  12. if (subscriptions != null && !subscriptions.isEmpty()) {
  13. for (Subscription subscription : subscriptions) {
  14. postingState.event = event;
  15. postingState.subscription = subscription;
  16. boolean aborted = false;
  17. try {
  18. postToSubscription(subscription, event, postingState.isMainThread);
  19. aborted = postingState.canceled;
  20. } finally {
  21. postingState.event = null;
  22. postingState.subscription = null;
  23. postingState.canceled = false;
  24. }
  25. if (aborted) {
  26. break;
  27. }
  28. }
  29. subscriptionFound = true;
  30. }
  31. }
  32. if (!subscriptionFound) {
  33. Log.d(TAG, "No subscribers registered for event " + eventClass);
  34. if (eventClass != NoSubscriberEvent.class && eventClass != SubscriberExceptionEvent.class) {
  35. post(new NoSubscriberEvent(this, event));
  36. }
  37. }
  38. }

将我们的event,即post传入的实参;以及postingState传入到postSingleEvent中。

2-3
行:根据event的Class,去得到一个List<Class<?>>;其实就是得到event当前对象的Class,以及
父类和接口的Class类型;主要用于匹配,比如你传入Dog extends Dog,他会把Animal也装到该List中。

6-31行:遍历所有的Class,到subscriptionsByEventType去查找subscriptions;哈哈,熟不熟悉,还记得我们register里面把方法存哪了不?

是不是就是这个Map;

12-30行:遍历每个subscription,依次去调用postToSubscription(subscription, event, postingState.isMainThread);
这个方法就是去反射执行方法了,大家还记得在register,if(sticky)时,也会去执行这个方法。

下面看它如何反射执行:

  1. private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
  2. switch (subscription.subscriberMethod.threadMode) {
  3. case PostThread:
  4. invokeSubscriber(subscription, event);
  5. break;
  6. case MainThread:
  7. if (isMainThread) {
  8. invokeSubscriber(subscription, event);
  9. } else {
  10. mainThreadPoster.enqueue(subscription, event);
  11. }
  12. break;
  13. case BackgroundThread:
  14. if (isMainThread) {
  15. backgroundPoster.enqueue(subscription, event);
  16. } else {
  17. invokeSubscriber(subscription, event);
  18. }
  19. break;
  20. case Async:
  21. asyncPoster.enqueue(subscription, event);
  22. break;
  23. default:
  24. throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
  25. }
  26. }

前面已经说过subscription包含了所有执行需要的东西,大致有:subscriber, subscriberMethod(method, threadMode, eventType), priority;

那么这个方法:第一步根据threadMode去判断应该在哪个线程去执行该方法;
case PostThread:

  1. void invokeSubscriber(Subscription subscription, Object event) throws Error {
  2. subscription.subscriberMethod.method.invoke(subscription.subscriber, event);

直接反射调用;也就是说在当前的线程直接调用该方法;

case MainThread:


先去判断当前如果是UI线程,则直接调用;否则: mainThreadPoster.enqueue(subscription,
event);把当前的方法加入到队列,然后直接通过handler去发送一个消息,在handler的handleMessage中,去执行我们的方
法。说白了就是通过Handler去发送消息,然后执行的。

case BackgroundThread:

如果当前非UI线程,则直接调用;如果是UI线程,则将任务加入到后台的一个队列,最终由Eventbus中的一个线程池去调用

executorService = Executors.newCachedThreadPool();。

case Async:将任务加入到后台的一个队列,最终由Eventbus中的一个线程池去调用;线程池与BackgroundThread用的是同一个。

这么说BackgroundThread和Async有什么区别呢?

BackgroundThread中的任务,一个接着一个去调用,中间使用了一个布尔型变量handlerActive进行的控制。

Async则会动态控制并发。

到此,我们完整的源码分析就结束了,总结一下:register会把当前类中匹配的方法,存入一个map,而post会根据实参去map查找进行反射调用。分析这么久,一句话就说完了~~

其实不用发布者,订阅者,事件,总线这几个词或许更好理解,以后大家问了EventBus,可以说,就是在一个单例内部维持着一个map对象存储了一堆的方法;post无非就是根据参数去查找方法,进行反射调用。

4、其余方法

介绍了register和post;大家获取还能想到一个词sticky,在register中,如何sticky为true,会去stickyEvents去查找事件,然后立即去post;

那么这个stickyEvents何时进行保存事件呢?

其实evevntbus中,除了post发布事件,还有一个方法也可以:

  1. public void postSticky(Object event) {
  2. synchronized (stickyEvents) {
  3. stickyEvents.put(event.getClass(), event);
  4. }
  5. // Should be posted after it is putted, in case the subscriber wants to remove immediately
  6. post(event);
  7. }

和post功能类似,但是会把方法存储到stickyEvents中去;

大家再去看看EventBus中所有的public方法,无非都是一些状态判断,获取事件,移除事件的方法;没什么好介绍的,基本见名知意。

好了,到此我们的源码解析就结束了,希望大家不仅能够了解这些优秀框架的内部机理,更能够体会到这些框架的很多细节之处,并发的处理,很多地方,为什么它这么做等等。

我建了一个QQ群,方便大家交流。群号:55032675

----------------------------------------------------------------------------------------------------------

博主部分视频已经上线,如果你不喜欢枯燥的文本,请猛戳(初录,期待您的支持):

1、高仿微信5.2.1主界面及消息提醒

2、高仿QQ5.0侧滑

3、Android智能机器人“小慕”的实现

EventBus (三) 源码解析 带你深入理解EventBus的更多相关文章

  1. Android EventBus源码解析 带你深入理解EventBus

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...

  2. Android EventBus源代码解析 带你深入理解EventBus

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus ...

  3. EventBus3.0源码解析

    本文主要介绍EventBus3.0的源码 EventBus是一个Android事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递. EventBus使用简单,并将事件发布和订阅充 ...

  4. LinkedList源码解析

    LinkedList是基于链表结构的一种List,在分析LinkedList源码前有必要对链表结构进行说明.1.链表的概念链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链表又分为单向链表和 ...

  5. Spring Security 解析(七) —— Spring Security Oauth2 源码解析

    Spring Security 解析(七) -- Spring Security Oauth2 源码解析   在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因 ...

  6. 给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析

    LinkedList是基于链表结构的一种List,在分析LinkedList源码前有必要对链表结构进行说明.   1.链表的概念      链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链 ...

  7. EventBus源码解析 源码阅读记录

    EventBus源码阅读记录 repo地址: greenrobot/EventBus EventBus的构造 双重加锁的单例. static volatile EventBus defaultInst ...

  8. 【Android】EventBus 源码解析

    EventBus 源码解析 本文为 Android 开源项目实现原理解析 中 EventBus 部分项目地址:EventBus,分析的版本:ccc2771,Demo 地址:EventBus Demo分 ...

  9. Tomcat源码分析三:Tomcat启动加载过程(一)的源码解析

    Tomcat启动加载过程(一)的源码解析 今天,我将分享用源码的方式讲解Tomcat启动的加载过程,关于Tomcat的架构请参阅<Tomcat源码分析二:先看看Tomcat的整体架构>一文 ...

随机推荐

  1. CMD和AMD

    CMD是国内玉伯在开发SeaJS的时候提出来的,属于CommonJS的一种规范,此外还有AMD,其对于的框架是RequireJS. 二者的异同之处: 二者都是异步(Asynchronuous Modu ...

  2. RHEL7软件包管理

    本文介绍RHEL7的软件包管理 RHEL7下主要有RPM和YUM这两种包管理: YUM使用简单但需要联网,YUM会去网上的YUM包源去获取所需要的软件包并获取该包依赖的其他包 RPM的需要的操作精度比 ...

  3. SharePoint 2013 中代码创建列表查阅项字段

    1.首先,打开VS创建两个List Definition,分别是Address和City,如下图: 2.City列表里修改Title为City Name,其实内部名称还是Title,注意一下: 3.给 ...

  4. tomcat已 .war 包的形式发布项目

    一:首相将写好的工程打成.war 文件包, 借助eclipse工具完成. 右键项目名称 --> Export --> WAR file  进入如下图 二: 进入到Tomcat的 webap ...

  5. Android 之 Intent(意图)

    Intent是 Android中重要的桥梁之一,它分为显式意图和隐式意图.接下来分别针对这两种意图进行讲解. 显式意图:通过指定一组数据或动作,激活应用内部的 activity:(相比隐式意图,此做法 ...

  6. HTML5 -1- 简介

    在介绍HTML5之前我们应该先了解一下HTML5 我想从下面开始简单了解一下HTML5 什么是HTML5??? HTML5 即HTML 5.0 版本 正式制定时间是在2014年 遵循着移动先行(前四个 ...

  7. 关于破解IDEA

    博客的意义就在于分享 哈哈 今天想装个 IDEA玩玩 去官网 下了个 安装包 想破解 结果度娘 帮解决了 直接po方法 很简单 就是安装好注册的时候 选择 License server ,填 http ...

  8. iOS之UI--微博个人详情页

    前言:微博个人详情页,和我常用的的QQ空间的详情页是同样的.要求能够融会贯通,做这一类的界面能够快速上手实现. 动态图效果展示: 直接使用UINavigationBar->UITableView ...

  9. Linux常用压缩和解压命令

    .tar 解包 tar xvf filename.tar.tar 打包 tar cvf filename.tar dirname.gz 解压1 gunzip filename.gz.gz 解压2 gz ...

  10. 数据结构--AC自动机--hdu 2896

    病毒侵袭 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submi ...