VOLLEY 超详细源码解析

为什么需要阅读Volley的源码

Volley是Google在2013年推出的一个网络库,用于解决复杂网络环境下网络请求问题。「Google出品,必属精品」,而且Volley被使用在包括「Google Plus」的一系列Google产品中,久经考验。因此我们通过学习Volley的源代码,可以学得很多Android网络处理方面的知识,同时可以看看Google 在设计Volley体系结构的时候,所使用的技巧。

在多如箭雨的情形下,Volley是如何帮你搞定一切的

Java HashMap 源码解析终极版

序言

ConCurrentHashMap 是一个被忽视的Java Concurrent包下面的类,在满足并发的「安全性」,和「活跃性」的前提下,做到了与不考虑线程安全的 HashMap 同等效率. 作者是大名鼎鼎的Doug Lea,他老人家在Java 并发领域做的贡献,确实是我们的榜样。下篇文章,对ConCurrentHashMap做一个分析,希望这个代码中的闪光点,能够对各位读者产生启发。这里先介绍HashMap做的实现,便于后面我们理解2者的差异,以及[Doug Lea]完成的ConCurrentHashMap类具有那些惊为天人的地方。

JDK 是如何定义Map接口的

在设计一个通用的模块和功能的时候,我们需要静下心来分析下根本需求是什么?根据这个需求来理清我们的思路。

Map,就是Key-Value对,通过Key可以快速找到对应的Value,核心的需求是Put和Get方法。

public void put(K,V);
public V get(K);

在实际的需求里面,虽然不同于Collection,但是一些基础功能和需求是共通的,所以需要额外地加上一些基础方法,比如isEmpty(),size()等。于是而后在原来的基础上添加了其他基础抽象,最后形成的接口大体可以如下

public void clear();
public boolean containsKey(Object key);
public boolean containsValue(Object value);
public Set<Map.Entry<K,V>> entrySet();
public boolean equals(Object object);
...
public V remove(Object key);
public int size();

在这个时候,可能会有2个问题

1)为什么Map接口没有继承自Collection?

这个在JDK的问题里面所说的是Map,和Collection根本是两个东西,具体可以参考下面的引用

This was by design. We feel that mappings are not collections and collections are not mappings. Thus, it makes little sense for Map to extend the Collection interface (or vice versa). If a Map is a Collection, what are the elements? The only reasonable answer is “Key-value pairs”, but this provides a very limited (and not particularly useful) Map abstraction. You can’t ask what value a given key maps to, nor can you delete the entry for a given key without knowing what value it maps to. Collection could be made to extend Map, but this raises the question: what are the keys? There’s no really satisfactory answer, and forcing one leads to an unnatural interface. Maps can be viewed as Collections (of keys, values, or pairs), and this fact is reflected in the three “Collection view operations” on Maps (keySet, entrySet, and values). While it is, in principle, possible to view a List as a Map mapping indices to elements, this has the nasty property that deleting an element from the List changes the Key associated with every element before the deleted element. That’s why we don’t have a map view operation on Lists.

Map可以用Collection来实现,但不一定意味着Map就一定是Collection,Collecton也不一定是Map。

2)为什么没有实现Iterator接口?

Map在定义上面就没有要求一定是可以迭代的,有人可能疑问用EntrySet来实现迭代啊?但是从设计的角度上去理解,如果用EntrySet的方式在接口里面申明Iterator接口,就意味着把内部的实现细节暴露出去了。所以宁愿提供 entrySet() 的接口。

/**
 * Returns a {@code Set} containing all of the mappings in this {@code Map}. Each mapping is
 * an instance of {@link Map.Entry}. As the {@code Set} is backed by this {@code Map},
 * changes in one will be reflected in the other.
 *
 * @return a set of the mappings
 */
public Set<Map.Entry<K,V>> entrySet();

Java是如何实现HashMap的

HashMap 基础单元

HashMap作为我们经常用的类,几乎没有程序猿不熟悉Map的用法, 没有道理不去熟悉下HashMap的实现原理。

HashMap使用java提供的基础类型中的数组,用 HashMapEntry<K, V>[] table 来进行Key-Value键值对的存储。Hash需要完成的工作即是将Key通过Hash的算法,将Key hash到某一个小于数组长度的数值 i 上面,从而在这个位置 i 记录下对应的Value,亦即table[i] = value,这样调用者在调用get(key)方法时,先通过hash计算出i,调用table[i]可以迅速找到相应的Value,这是HashMap查询速度快的原因。

JDK首先构建了对基础单元的抽象-HashMapEntry,里面的核心成员变量如下代码,其中hash值一个用于缓存,便于进行比较,而next字段则实现了C++的指针,通过这个指针可以链式地找到下一位。这个链式结构的设计目的是为了解决Hash冲突的情况。

final K key;
V value;
final int hash;
HashMapEntry<K, V> next;

HashMap 如何避免冲突和解决冲突的

当两个Key同时hash到一个值时,就会出现这样的冲突。这个冲突主要有2种解决方法。

  1. 开放地址,亦即如果hash冲突,则在空闲的位置进行插入
  2. hash复用,同一个hash值,链式地加入多个value

HashMap 选择了第二种方式来解决冲突的问题。

整个HashMap的核心部分是hash方法,我们先从最核心的方法看起,HashMap 是如何实现将hashCode值映射到table数组里面的索引里面的。

@Override public V put(K key, V value) {
    if (key == null) {
        return putValueForNullKey(value);
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }

    // No entry for (non-null) key is present; create one
    modCount++;
    if (size++ > threshold) {
        tab = doubleCapacity();
        index = hash & (tab.length - 1);
    }
    addNewEntry(key, value, hash, index);
    return null;
}
// Collections.secondaryHash(key)方法里面使用了
int hash = Collections.secondaryHash(key);
int index = hash & (tab.length - 1);

在这里面可以看到Hash的代码主要是Wang/Jenkins hash方法,这个方法具有两个属性

  1. 雪崩性(更改输入参数的任何一位,就将引起输出有一半以上的位发生变化)
  2. 可逆性(input ==> hash ==> inverse_hash ==> input)

Collections.secondaryHash能够使得hash过后的值的分布更加均匀,尽可能地避免冲突,具体原理点连接,注意这里的前提是table的长度为2的幂次方,从构造函数对capacity的改造可以看出来。hash & (tab.length - 1) 当tab.length为2^n-1的时候,可以保证结果不大于tab.length。

public HashMap(int capacity) {
    // ...
    if (capacity < MINIMUM_CAPACITY) {
        capacity = MINIMUM_CAPACITY;
    } else if (capacity > MAXIMUM_CAPACITY) {
        capacity = MAXIMUM_CAPACITY;
    } else {
        capacity = Collections.roundUpToPowerOfTwo(capacity);
    }
    makeTable(capacity);
    // ...
}

这里举一个例子,如果现在Table的容量是16,如果最后的结果是 hash & (16-1),也就是 hash & (00001111)。现在我们试着对hash值是31(00011111), 63(00111111),95(01011111)进行操作,理论上映射到的index值应该是不尽相同的,然而实际的情况确实如下的情形:

31=00011111 ==& 00001111==> 1111=15

63=00111111 ==& 00001111==> 1111=15

95=01011111 ==& 00001111==> 1111=15

因此Collections.secondaryHash需要解决的问题,就是避免上面的情况,效果见下面的例子:

31=00011111 ==secondaryHash==> 00011110==& 00001110==> 14

63=00111111 ==secondaryHash==> 00111100==& 00001100==> 12

95=01011111 ==secondaryHash==> 01011010==& 00001010==> 10

HashMap 如何解决容量不足的问题

前面解决了hash冲突的问题,那么现在如何解决容量不足的问题了。考虑这样的情况,如果用户大量地调用put方法,在这种情况下,如果容量不变,那么势必会出现大量的冲突,调用get方法时,可能需要很长长度的遍历才能得到答案,性能损失严重,因此,我们可以发现源码中有这样一种方法:

if (size++ > threshold) {
	tab = doubleCapacity();
	index = hash & (tab.length - 1);
}

JDK给出的方案是扩容,doubleCapacity()方法,比如原来容量为16,当现在的使用量大于 DEFAULT_LOAD_FACTOR*Capacity,下次直接把容量扩展到32. 这段代码注解说的是这整个类的精华,我们必须好好研究下.

Rehash the bucket using the minimum number of field writes, and this is the most subtle and delicate code in the class.

/**
 * Doubles the capacity of the hash table. Existing entries are placed in
 * the correct bucket on the enlarged table. If the current capacity is,
 * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
 * will be new unless we were already at MAXIMUM_CAPACITY.
 */
private HashMapEntry<K, V>[] doubleCapacity() {
    HashMapEntry<K, V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        return oldTable;
    }
    int newCapacity = oldCapacity * 2;
    HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
    if (size == 0) {
        return newTable;
    }

    for (int j = 0; j < oldCapacity; j++) {
        /*
         * Rehash the bucket using the minimum number of field writes.
         * This is the most subtle and delicate code in the class.
         */
        HashMapEntry<K, V> e = oldTable[j];
        if (e == null) {
            continue;
        }
        int highBit = e.hash & oldCapacity;
        HashMapEntry<K, V> broken = null;
        newTable[j | highBit] = e;
        for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
            int nextHighBit = n.hash & oldCapacity;
            if (nextHighBit != highBit) {
                if (broken == null)
                    newTable[j | nextHighBit] = n;
                else
                    broken.next = n;
                broken = e;
                highBit = nextHighBit;
            }
        }
        if (broken != null)
            broken.next = null;
    }
    return newTable;
}

我们注意其中这部分的代码

int highBit = e.hash & oldCapacity;
newTable[j | highBit] = e;

我们现在来说明,oldCapacity假设为16(00010000), int highBit = e.hash & oldCapacity 能够得到高位的值,因为低位全为0,经过与操作过后,低位一定是0。J 在这里是index,J 与 高位的值进行与操作过后,就能得到在扩容后面的新的index值。

设想一下,理论上我们得到的新的值应该是 newValue = hash & (newCapacity - 1) ,与 oldValue = hash & (oldCapacity - 1) 的区别仅在于高位上。 因此我们用 J | highBit 就可以得到新的index值。

HashMap 是如何解决迭代问题

首先HashMap提供了3种形式的迭代方法,分别是针对Entry,Key和Value的迭代器,这里实现的代码就比较简单,看下具体的例子就可以知道。实现方式主要是依赖于对HashEntity数组进行遍历即可实现。

问题在于如何保证迭代的时候,基于正确的输出,聪明的你一定看出来了,难度在于「多线程情况的处理」。在迭代的时候,外部可以通过调用put和remove的方法,来改变正在迭代的对象。但从设计之处,HashMap自身就不是线程安全的,因此HashMap在迭代的时候使用了一种Fast—Fail的实现方式,在HashIterator里面维持了一个 expectedModCount 的变量,在每次调用的时候如果发现 ModCount != expectedModCount,则抛出 ConcurrentModificationException 异常。但本身这种检验不能保证在发生错误的情况下,一定能抛出异常,所以我们需要在使用HashMap的时候,心里知道这是「非线程安全」的。

HashMapEntry<K, V> nextEntry() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (nextEntry == null)
        throw new NoSuchElementException();

    HashMapEntry<K, V> entryToReturn = nextEntry;
    HashMapEntry<K, V>[] tab = table;
    HashMapEntry<K, V> next = entryToReturn.next;
    while (next == null && nextIndex < tab.length) {
        next = tab[nextIndex++];
    }
    nextEntry = next;
    return lastEntryReturned = entryToReturn;
}

HashMap 是如何实现序列化接口的

HashMap是实现了Serializable接口的,这种Hash的结构如何实现序列化?按照最朴素的想法,按照默认的实现即可,但是细想之下,一个16位的数组如果只存了一个数据,却要把16位的数组到序列化进去,这本身并不可取。于是HashMap 将其他数据申明为transient,比如:

transient int modCount;

避免这些常量参与到序列化的细节里面去,在另一方面重写 writeObject()readObject() 方法,通过这样的方式来实现序列化,在代码里面添加了注释,便于大家理解。

private void writeObject(ObjectOutputStream stream) throws IOException {
    // Emulate loadFactor field for other implementations to read
    ObjectOutputStream.PutField fields = stream.putFields();
    fields.put("loadFactor", DEFAULT_LOAD_FACTOR);
    stream.writeFields();

    // 写入table的容量
    stream.writeInt(table.length); // Capacity
    // 写入目前的Entry数量
    stream.writeInt(size);
    for (Entry<K, V> e : entrySet()) {
    	// 迭代地写入Key和Value
        stream.writeObject(e.getKey());
        stream.writeObject(e.getValue());
    }
}
private void readObject(ObjectInputStream stream) throws IOException,
            ClassNotFoundException {
    stream.defaultReadObject();
    int capacity = stream.readInt();
    if (capacity < 0) {
        throw new InvalidObjectException("Capacity: " + capacity);
    }
    if (capacity < MINIMUM_CAPACITY) {
        capacity = MINIMUM_CAPACITY;
    } else if (capacity > MAXIMUM_CAPACITY) {
        capacity = MAXIMUM_CAPACITY;
    } else {
        capacity = Collections.roundUpToPowerOfTwo(capacity);
    }
    makeTable(capacity);

    // 得到size大小
    int size = stream.readInt();
    if (size < 0) {
        throw new InvalidObjectException("Size: " + size);
    }

    init(); // Give subclass (LinkedHashMap) a chance to initialize itself
    for (int i = 0; i < size; i++) {
        @SuppressWarnings("unchecked") K key = (K) stream.readObject();
        @SuppressWarnings("unchecked") V val = (V) stream.readObject();
        // 构造函数里面,会计算hash放置到响应的地方
        constructorPut(key, val);
    }
}

读薄《Java 并发实践》

并发编程的目的

线程的优势

发挥多处理器的优势,异步事件的处理,高响应性

线程需要解决的问题

  • 安全性问题(永远不发生糟糕的事情)
  • 活跃性问题(某件正确的事情最终会发生)
  • 性能问题 (线程创建和切换的开销)

线程安全性

线程安全类:当多个线程访问某个类的时候,不管运行环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为

无状态的对象一定是线程安全的

原子性

竞态条件与复合操作

由于不恰当的执行时序而出现不正确的结果

实例:错误的延迟初始化

@NotThreadSafe
public class LazyRace<T> {
	private T instance;
	public T getInstance() {
		// 判断和申明实例的复合操作不是原子的,可能引发问题
		if (instance == null) {
			instance = T.class.newInstance();
		}
		return instance;
	}
}

保证状态的一致性,就需要在单个原子操作中更新所有相关的状态变量

java的基础锁机制

synchronized (lock){

}

synchronized 是可以重入的,换言之某个线程试图获得一个已经由自己持有的锁,那么这个请求就会成功。获得锁的基础单位是「线程」 对于可能被多个线程同时访问的可变状态变量,在访问它的时候都需要持有同一个锁,在这种情况下,我们称为状态变量是由这个锁来保护的。@GuardedBy

一种常见的加锁约定是,把所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得该对象不会发生并发访问。


##对象的共享

可见性

在没有进行同步的情况下,编译器,处理器以及Java 运行时都可能对操作的顺序进行调整,在缺乏足够同步的多线程程序里面,想要对内存操作顺序进行判断,是不可能的

当一个线程修改了某些值,这个改变并不是马上对其他线程可见的。

The Java Memory Model A memory model describes when one thread’s actions are guaranteed to be visible to another. The Java memory model (JMM) is quite an achievement: previously, memory models were specific to each processor architecture. A cross-platform memory model takes portability well beyond being able to compile the same source code: you really can run it anywhere. It took until Java 5 (JSR 133) to get the JMM right. The JMM defines a partial ordering on program actions (read/write, lock/unlock, start/join threads) called happens-before. Basically, if action X happens-before Y, then X’s results are visible to Y. Within a thread, the order is basically the program order. It’s straightforward. But between threads, if you don’t use synchronized or volatile, there are no visibility guarantees. As far as visible results go, there is no guarantee that thread A will see them in the order that thread B executes them. Brian even invoked special relativity to describe the disorienting effects of relative views of reality. You need synchronization to get inter-thread visibility guarantees.

java的内存模型要求,变量的读取和写入是原子的,除了非volatitle类型的long和double。

加锁的意义不仅在于互斥行为,还包括内存可见性。

volatile

仅当Volatile变量能简化代码的实现以及同步策略的验证时,才应该使用他们。

  1. 对变量的写入操作不依赖与变量的当前值,或者只有单个线程在更新这个值
  2. 在访问变量时不需要加锁
  3. 该变量不与其他状态变量一起纳入不变性条件里面

发布与逸出

发布 - 对象能够在当前作用域之外的代码里面使用

this 逸出

public class ThisEscape {
	Public ThisEscape(EventSource source) {
		source.registeListener {
			// 发布EventListener时,也隐含发布了ThisEscape实例本身
			new EventListener() {
				public void onEvent(Event e) {
					doSomething(e);
				}
			}
		}
	}
}

线程封闭

所有操作都在同一个线程里面完成,这个技术称为线程封闭(Thread Confinement) 栈封闭 - 线程内部使用或者局部使用 ThreadLocal - 使线程中的某个值与保存的对象关联起来

不变性

不可变对象一定是线程安全的,Java内存模型里面,final域能够确保初始化过程的安全性。除非某个域是可变的,否则应该将他声明为Final

安全发布

public class UnSafeProgram {
	public Holder holder;

	public void init() {
		holder = new Holder(34);
	}
}

public class Holder {  
    private int n;  

    public Holder(int n) {
    	// Object会先于子类的构造函数执行,赋予默认值。
        this.n = n;
    }  

    public void assertSanity() {  
        if (n != n) {
        	// 除了发布的线程外,其他线程看到的Holder域是一个失效值,因此可能看到一个空引用
        	// 或者之前的旧值。
            throw new AssertionError("This statement is false.");  
        }
    }  
}

由于不可变对象是一种非常安全的对象,因此java内存模型为不可变对象的共享提供了一个特殊的初始化安全性保证。即便某个对象的引用对其他线程是可见的,也不意味着对象状态对使用该对象的线程一定是可见的。

  1. 在静态初始化函数中初始化一个对象引用
  2. 将对象的引用保存在volatile或者AtomicReferance对象中
  3. 保存在某个正确构建对象的Final类型域中
  4. 将对象的引用保存在由一个锁保护的域中

对象的组合

JAVA监视器模式

java 为每一个Object都关联着一种锁(Mutex),通过这个锁来保证在Sync模块内,同时只有一个线程在执行代码

基础构建模块

并发容器与同步工具类

结构化并发应用程序

Executor框架

为何用使用Executor框架

  1. 线程生命周期的开销非常高
  2. 资源消耗
  3. 稳定性

框架介绍

java类库中,任务执行的框架主要是Thread,而是Executor。

public interface Executor {
	void execute(Runnable command);
}

Executor 是基于生产者-消费者模式,提交任务的操作相当于生产者(生产待完成的工作单元),执行任务的线程相当于消费者(执行完这些工作单元)。需要实现生产者-消费者模式,最简单地就是使用Executor

常见的线程池

newFixedThreadPool
newCachedThreadPool
newSingleThreadPool
newScheduledThreadPool

Executor 只有在所有非守护线程全部终止后才会退出,为了解决这种生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于管理生命周期的方法。

一般用ScheduledThreadPool来代替Timer来执行一些与时间相关的任务。

携带结果的任务Callable与Future

Runnable 是一种很有局限性的抽象,Callable是一种更好的抽象,任务主入口有一个返回值,并可能抛出一个异常。相对于ExecutorService,Future是一个针对于任务生命周期的抽象。

public interface Callable<V> {
	V call() throws Exception;
}

Feature Interface

使用CompletionService来进行任务处理

Omitting many details:

  1. ExecutorService = incoming queue + worker threads
  2. CompletionService = incoming queue + worker threads + output queue

后续的内容将陆续以专门文章的形式推出,请期待

十年

不是陈奕迅的「十年」,是我们的十年。

十年很短,因为人生没几个十年;十年很长,因为你不会忘记任何一个十年;十年很珍贵,因为任何事情只要和十年扯上一点关系,就有了岁月的味道,比如亲情,爱情,还有现在要说的友情。

物以类聚,人以群分,能在一起的都是缘分。不说互相支持,就是能一直玩到一起,也是难能可贵。冉P,聪聪,元仔,基哥,老大和我,数数真是十年,快抵得上「歪嘴郎酒」的年份了。

想起几件轶事,写出来,没事可以乐一下。

「聪聪」 —————————————————————————————————————————————

今年,见他的时候,他已经开始抽烟了,开始唏嘘了几句后,递给我一支烟。烟是男人的印记,烟里往往有很多故事。他是这五个人里面,唯一做过同桌的一位,相处的时间也最久。

可以在他上面打很多TAG,比如「会事」,「贪玩」,「T-Mac」等等,我最中意的还是「老师」。有好些东西都是通过他,才开始了解和接触,没他,感觉现在的我还好多东西都不知道。

「T-Mac」,他喜欢「T-Mac」好多年,每每提起「T-Mac」,总是滔滔不绝,能从上世纪说到现在。我也是从哪个时候,开始打「篮球」和看「NBA」。高考前一个月,赶上火箭季后赛比赛,不能影响高考准备,更不能放弃看比赛。于是每个中午放学,都第一个冲出学校,到附近有电视的店里吃饭,边加油助威边吃饭。比赛一结束,就一边骂娘,一边回学校上自习。

现在也是有故事的男人,希望他的这个女朋友是他最后一个女朋友,白头到老。

「元仔」 —————————————————————————————————————————————

认识时间最久的哥们,和他的事情,能「留到以后,坐在摇椅上慢慢聊」。

他是典型的中国古典好青年,为人随和,且仗义。几乎没有什么缺点可以放在他身上,非要说的话,就是没有女朋友(给哥们憋坏了)。有一次,初中班主任当着全班的面说,在现在绝对引发歧义的一句话。「我就经常发现曾元和唐琪森在一起,优秀的人总是在一起互相督促和学习的。」尼玛,这放到现在不被说成搞基,就真是奇怪了。

还有略微有点怂,上次打着他去北京滑雪。到了「高级道」,我都已经做缆车快要出发了,临末,抖出一句「大哥,腿抖得厉害,就不上去了」。哈哈,也是太可爱了。

「基哥」 —————————————————————————————————————————————

和「元仔」是亲兄弟,不过外界看来,更像「异母异父」的兄弟,那个人唱的不太像~

「基哥」是永远对这世界充满了好奇的孩子,最想去的地方是遥远的阿富汗,偶像是共同的格瓦拉。 「基哥」什么都会点,会LOL,会台球,会篮球,会物理,会化学,会计算机。「什么都会一点,生活更美好」。 「基哥」永远在寻找下一个感兴趣的事情,一直在路上。

「老大」 —————————————————————————————————————————————

「老大」一直是老大,不是因为气势,是真的年纪比我们都大,经历的事情也比我们经历得多。

「老大」今年26了,我们都还在疑心他今年是不是又得光棍一年的时候,半年时间就找到一个可以见家长的女友,一下子就是我们这最早结婚的人了。 「老大」是一个颇具戏剧性的人物,当初填写自愿,我们都以为他如愿去厦门读「经济学」,结果「老大」脑子一充血,填了一个「川大」的基础化学,还真给录取了。后来本来只想读研,结果硬是换了个专业读博去了。 「老大」为了找女朋友,硬是从170斤减到了130斤;「老大」为了更好的高校,硬是复读了一年;「老大」为了更好的前途,硬是读博去了。

都知道,「老大」是为了更好的生活。

「冉P」 —————————————————————————————————————————————

是我们几个人里面唯一的本科生,中考和高考,即使复读了一年,都不算太理想,尽管冉P偶尔也会自嘲,但其实心里还是会苦闷。每个男人都想在自己20岁出头的时候,能风光满面,但大多数人也明白20岁出头是男人最难的日子。「没钱」,「没时间」,「没工作」的三无少年,总之那几年,是他人生里面难熬的一段日子。最能成为谈资的事情,也是在这一阶段里面。 「重庆机械学院」(具体名字有些忘记)和「重庆理工大学」是两所相差还不算太少的高校,如果都能去,脑子没问题的都会去「重庆理工大学」。那一年,冉P的脑子就出了点问题。当我们知道他选择了「重庆机械学院」,都我伙呆了。 「你呀,脑子傻吧?」 选择的原因其实我们都知道,他和她的女朋友都打算在重庆上大学,女朋友的分数只能到「重庆机械学院」里面去,为了不「异校」(但都在大学城里面),冉P把他的志愿填到了「重庆机械学院」。 这个问题,我问过他两次。 Q:怎么要选择「重庆机械学院」? A:我也不知道,就是想着近一点。 后来,不到一年时间,他和女朋友分手了,我再次问他。 Q:你后悔了吗? A:没啥好后悔了,这本身都不算什么事。

这事情,可以有很多种看法,很多种理解,可以为他感到骄傲,也可以说他傻。但我们都可以肯定的一点是,「他用他的方式,过好了这一辈子」。

「我」 —————————————————————————————————————————————

十年里面,「我」也是一个有故事的人了,就把故事留着,给未来吧。

以上。

2014年终总结

2015-2-17 夜深 阴

所有内容均来源于自己日常的思考,仅代表个人观点。

职业发展

这是在豌豆荚的第二个年头,而第一个年头是相当抑郁,遭受了许多质疑和否定。那一年里面,自己主要负责「Android端通用下载库」,这个项目旨在建立一个通用的下载库,能够支持多源的下载需求,无缝衔接视频,音乐和应用等下载类别。项目里的设计难度「架构知识」,和需要处理的细节「领域知识」都强于今年的项目,但收获远小于今年,细细对比下,能有如下的一些体会。

如果别人看不到,几乎等于没做

前一个项目由于是底层基础服务,做出一个有成效的东西,需要花费的时间和精力相对很长,这样使得结果很难在短时间内被人所看到。而另一方面,项目里面所涉及的技术难点我又采用了黑盒「一个闷头做」的做法,这样下来在长达3个月的时间里面,别人对我所做的内容,所需要完成的事情都不了解,做得好的,和做的不好的地方都无从知晓。如果公司看不到这个项目闪光的地方,那么也自然地否定了这个项目。即便后面下载库在公司已经服役2年多,累计贡献了亿级别的下载,当初公司看不到,那么现在也不会看到。

2014年的项目里面,从「Android Experience」,「UGC」到「Follow」,三个项目里面都是工程型项目,成果很明显,公司也会认同你的努力,所以在这一年,顺利完成了「转正」,「升级」和「管理转型」的milestone。

公司目前还是一个创业型公司,而且目前在国内IT圈「短平快」的氛围下,对技术和项目的耐心是远远不足的。公司都死了,技术再好有什么用?但另一方面,没有技术,公司很难长远。更合适的做法也许是,永远有人投入在基础项目的搭建中,尽管不一定是决定性因素。

让更多的人能看到你现在所做的工作,基本是百利无一害的。过程有问题,会督促你进步;过程很顺利,更多机会就来了。「单人完成WPS」这样的壮举现在会越来越难,反之透明的执行过程能帮你在职业阶梯上更加顺利。

正螺旋理论

如果帮事情做成功算作「正」,反而做失败算作「负」,正螺旋理论就是如果一件事情做成功后,会为后面事情的成功打下基础,如果一直接着成功,那么螺旋会越来越大,会越来越有信心,越来越成熟,这样,如果后续有一次失败,对你的螺旋结构不会有太大影响,你还是很有经验,很有信息,下一件事情成功的概率依然很大。

「下载库」失败为负,「Android Experience」,「UGC」和「Follow」都为正,目前趋势良好。都在说多少次的失败才能换来一次成功,这当然有道理,可目前IT行业是一个供大于求,快销的行业。当有的选的时候,行业会优先选择那些成功多的人,而不是还一直在失败中挣扎的人。毕竟你的时间和生命,也经不起失败的耗费。

流程化,必须流程化

这一部分,是在「Follow」项目里面才开始思考的问题,但也是非常重要的方面。IT行业节奏很快,开发人员在里面需要面对的事情越来越多,特别是在管理岗位的时候,跨团队合作需要处理的事情更是繁多。如果能把每天需要处理的事情流程化,那么就能从每天的疲于奔命的过程中转向自己把控的节奏里面。

IOC「控制反转」,是一个技术代名词,在流程化方面也是一个很有效的手段。可以想想,如果你每天需要处理的事情是如下四件事情: * 写代码 * 和其他开发沟通进度 * 讨论产品方案 * 跟进反馈 普通的做法是开始写代码,然后去找开发沟通进度,和PD沟通方案,和OPS跟进反馈,这些时间是不可控的,碎片化的,不高效的「把人聚到一起都得花不少时间」。利用IOC的做法,则情况大不一样,把这四件事情都写在日历里面,通知PD,开发,OPS人员在固定的时间开会,那么这些时间就是固定的,不用再花时间去找人了。另一方面,OPS有反馈需要给你说,那么在和OPS约定的时间里面反馈就好了,不会在你写代码的时候来打断你。

情感,感情

父母,年迈的父母

每年到家的时候,我都能感受到父母在老去,发福的身体,耳鬓的白发,即便现在他们还在给你支起一片天空,但他们真的在变老。前年春节,差点失去父亲的惊恐无助到现在还在提醒我,也许他们终于要老去。我尽可能地使自己保持理智,但唯独父母,爱情这两者,我很难做到,他们对我都太重要。

今年到家的火车是凌晨2点的,担心他们做生意太累,就让他们早点休息,不让他们来接我。本想安安静静地回到家里面,到家的时候,即使是2点多时候,周围除了一盏昏黄的路灯,别无其他亮光,但5楼家里的灯还亮着,注视着我。那晚他们在准备一顿凌晨2点的大餐。

我想,我成长的速度得打过他们老去的速度,该多分担些了。

爱,先是爱自己

爱,先是爱自己。老想着能为别人做些什么,忘了自己过着怎样的生活。很有有人会因为你过得不好,而喜欢上你「最多是怜悯」,到是你过得好,别人能在你这看到生活的美好,而和你接触。生活不是比谁过得不好的艺术,是一门怎么才能过得好的技术。

年初的时候,醉心于理财,想着自己也许能从中获利不少。别人的建议提醒了我,最大的投资,就是投资自己。如果身体好,那么能照顾别人;如果待遇好,那么能负担更多;如果去的地方多了,那么能让别人去更合适的地方游玩;如果你能爱自己,那么也能爱别人。

桂花性喜温暖,湿润,一般中秋前後開花,花香沁鼻,可佐茶入藥。遲桂花常見于山陵,經冬后花香更濃郁。