SegmentFault warmcheng技术小屋最新的文章
2019-05-21T09:34:38+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Rxjava2.x源码解析(二): 线程切换
https://segmentfault.com/a/1190000019243389
2019-05-21T09:34:38+08:00
2019-05-21T09:34:38+08:00
warmcheng
https://segmentfault.com/u/warmcheng
2
<p>上一篇文章<a href="https://segmentfault.com/a/1190000019243142">Rxjava2.x源码解析(一): 订阅流程</a>中我们讲了 RxJava2 的订阅部分的源码。但 RxJava2 最强大的部分其实是在异步。默认情况下,下游接收事件所在的线程和上游发送事件所在的线程是同一个线程。接下来我们在上一篇文章的示例代码中加入线程切换相关代码:</p>
<pre><code> // 上游 observable
Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "subscribe: ");
emitter.onNext(1);
emitter.onNext(2);
emitter.onComplete();
}
});
// 下游 observer
Observer<Integer> observer = new Observer<Integer>() {
@Override
public void onSubscribe(Disposable d) {
// onSubscribe 方法会最先被执行
Log.d(TAG, "onSubscribe: ");
}
@Override
public void onNext(Integer integer) {
Log.d(TAG, "onNext: ");
}
@Override
public void onError(Throwable e) {
Log.d(TAG, "onError: ");
}
@Override
public void onComplete() {
Log.d(TAG, "onComplete: ");
}
};
// 在子线程中进行事件的发送
observable.subscribeOn(Schedulers.newThread())
// 切换到UI线程进行监听
.observeOn(AndroidSchedulers.mainThread())
// 将上游和下游进行关联
.subscribe(observer);
</code></pre>
<p>我们通过<code>subscribeOn(Schedulers.newThread())</code>这行代码,就可以将我们上游的代码切换到子线程中去执行,通过<code>observeOn(AndroidSchedulers.mainThread())</code>又能指定下游监听的代码执行在主线程(这里的 AndroidSchedulers 并不是RxJava2 默认提供的,而是属于Android领域的,由RxAndroid这个库实现)。一行代码,就能自由切换上下游的代码执行的线程,这么骚的操作,到底是怎么实现的呢?</p>
<p>我们上面两个方法中传入的都是一个<code>Scheduler</code>实例,翻译过来就是“调度器”,负责线程相关的调度。</p>
<p>那接下来我们就先从上游相关的<code>subscribeOn(Schedulers.newThread())</code>开始分析。<br>先从参数入手,看看这个<code>Schedulers.newThread()</code>中执行了什么:</p>
<pre><code>public final class Schedulers {
static final Scheduler SINGLE;
static final Scheduler COMPUTATION;
static final Scheduler IO;
static final Scheduler TRAMPOLINE;
// 这里是 NEW_THREAD
static final Scheduler NEW_THREAD;
static final class SingleHolder {...}
static final class ComputationHolder {...}
static final class IoHolder {...}
// 初始化一个默认的 NewThreadScheduler
static final class NewThreadHolder {
static final Scheduler DEFAULT = new NewThreadScheduler();
}
static {
...
// 由一个新创建的 NewThreadTask 来初始化 NEW_THREAD
NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask());
}
@NonNull
public static Scheduler newThread() {
return RxJavaPlugins.onNewThreadScheduler(NEW_THREAD);
}
...
static final class IOTask implements Callable<Scheduler> {...}
// 这里是 NewThreadTask
static final class NewThreadTask implements Callable<Scheduler> {
@Override
public Scheduler call() throws Exception {
return NewThreadHolder.DEFAULT;
}
}
static final class SingleTask implements Callable<Scheduler> {...}
static final class ComputationTask implements Callable<Scheduler> {...}
}</code></pre>
<p>可以看到,<code>newThread(...)</code>方法会返回一个<code>Scheduler</code>类型的静态变量<code>NEW_THREAD</code>,而该变量的初始化是在如下的静态代码块中:</p>
<pre><code> static {
...
// 由一个新创建的 NewThreadTask 来初始化 NEW_THREAD,类型为 Scheduler
NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask());
}</code></pre>
<p>这里面创建了一个<code>NewThreadTask</code>实例,该类也比较简单,就是在<code>call()</code>方法中返回了<code>NewThreadHolder.DEFAULT</code>:</p>
<pre><code> static final class NewThreadTask implements Callable<Scheduler> {
@Override
public Scheduler call() throws Exception {
return NewThreadHolder.DEFAULT;
}
}</code></pre>
<p><code>NewThreadHolder.DEFAULT</code>则是一个<code>NewThreadScheduler</code>对象:</p>
<pre><code> // 初始化一个默认的 NewThreadScheduler
static final class NewThreadHolder {
static final Scheduler DEFAULT = new NewThreadScheduler();
}</code></pre>
<p>那我们不禁好奇,这个<code>call()</code>方法又是什么时候调用的呢?我们继续回到<code>RxJavaPlugins.initNewThreadScheduler(new NewThreadTask())</code>这行代码,从名称来看是初始化NewThreadScheduler对象的,那我们进去看下是如何进行的:</p>
<pre><code> public static Scheduler initNewThreadScheduler(@NonNull Callable<Scheduler> defaultScheduler) {
ObjectHelper.requireNonNull(defaultScheduler, "Scheduler Callable can't be null");
Function<? super Callable<Scheduler>, ? extends Scheduler> f = onInitNewThreadHandler;
if (f == null) {
// 直接看这里
return callRequireNonNull(defaultScheduler);
}
return applyRequireNonNull(f, defaultScheduler);
}</code></pre>
<p>作为聪明人,我们直接看<code>callRequireNonNull(defaultScheduler)</code>这行代码:</p>
<pre><code> static Scheduler callRequireNonNull(@NonNull Callable<Scheduler> s) {
try {
// 可以看到,这里调用了 s.call(),并将结果返回;若为空,则报异常
return ObjectHelper.requireNonNull(s.call(), "Scheduler Callable result can't be null");
} catch (Throwable ex) {
throw ExceptionHelper.wrapOrThrow(ex);
}
}</code></pre>
<p>可以看到,里面直接调用了传入的参数的<code>call()</code>方法,并返回。<br>到这里,就知道了,<code>RxJavaPlugins.initNewThreadScheduler(new NewThreadTask())</code>这行代码其实就是初始化一个<code>NewThreadScheduler</code>对象。</p>
<p>绕了这么远,其实<code>Schedulers.newThread()</code>这句就是创建了一个<code>NewThreadScheduler</code>对象,这里讲的比较细。</p>
<p>我们继续回来,看看<code>subscribeOn(Schedulers.newThread())</code>里面做了什么:</p>
<pre><code> public final Observable<T> subscribeOn(Scheduler scheduler) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
}</code></pre>
<p>根据第一篇文章里的经验,我们知道,这里又是将上一步生成的 Observable 进一步封装成一个<code>ObservableSubscribeOn</code>并返回。其实,RxJava之所以能进行链式调用,无外乎就是在每次调用操作符方法的时候,返回一个 Observable 的引用,但是这个 Observable 所具体指向的对象,可能是不同的。中间可能就创建了新的对象,经过了一层层的包装。RxJava 里装饰器模式用的还是比较厉害的,所以说,千万别觉的实际模式都是虚无缥缈的东西。</p>
<p>这里返回的是一个<code>ObservableSubscribeOn</code>对象(注意看命名哦!规律之前讲过的)</p>
<p>经过上篇文章分析,我们知道,使用 Observable 的 subscribe 方法进行订阅的时候,最终会调用到 Observable 的<code>subscribeActual(...)</code>方法,这里的<code>Observable</code>具体就是<code>ObservableSubscribeOn</code>:</p>
<pre><code> // ObservableSubscribeOn.java
public void subscribeActual(final Observer<? super T> observer) {
final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(observer);
observer.onSubscribe(parent);
parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
}</code></pre>
<p>可以看到,这里将 observer 也进行了包装,包装成<code>SubscribeOnObserver</code>对象。也相当于配套啦,haha。</p>
<p>然后又将这个封装后的对象传进了一个新建的 SubscribeTask 对象中。</p>
<p>???<br>这个<code>SubscribeTask</code>又是啥?<br>这个<code>SubscribeTask</code>是<code>ObservableSubscribeOn</code>这个类的内部类,其实就是一个<code>Runnable</code>实现类:</p>
<pre><code>public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {
final Scheduler scheduler;
public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) {
super(source);
this.scheduler = scheduler;
}
@Override
public void subscribeActual(final Observer<? super T> observer) {
// 创建一个新的 Observer
final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(observer);
observer.onSubscribe(parent);
// 进行线程任务的创建及分发
parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
}
...
// 是个 Runnable 实现类
final class SubscribeTask implements Runnable {
private final SubscribeOnObserver<T> parent;
SubscribeTask(SubscribeOnObserver<T> parent) {
this.parent = parent;
}
@Override
public void run() {
// 注意,此处是关键,正是从这里开始,上游(即:source)在新线程重新对下游进行订阅。
// 从而达到上游发送事件的线程进行切换的目的
// 这里提前提醒下,多次订阅,并不是只有第一次订阅指定的线程才有效,那只是普通使用场景下的“凑巧”
source.subscribe(parent);
}
}
}</code></pre>
<p>到这,我们总算看到了线程相关的东西了。Runnable 大家肯定都熟悉吧?在它的<code>run()</code>方法中,调用了<code>source.subscribe(parent)</code>,这里的 parent 我们知道,是封装之后的<code>SubscribeOnObserver</code>,但<code>source</code>又是啥?其实就是我们在 ObservableSubscribeOn 的构造函数中传进来的<code>this</code>,即上游的 Observable :</p>
<pre><code> // Observable.java
public final Observable<T> subscribeOn(Scheduler scheduler) {
ObjectHelper.requireNonNull(scheduler, "scheduler is null");
// 这里传进来的 this对象,就是上游 Observable 对象
return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
}</code></pre>
<p>抽象类 Observable 实现了 ObservableSource 接口,这个接口就是我们进行订阅时候用到的<code>subscribe(...)</code>:</p>
<pre><code>public interface ObservableSource<T> {
void subscribe(@NonNull Observer<? super T> observer);
}</code></pre>
<p>继续看这个 run() 方法,它相当于是把之前的上游通过<code>subscribe(...)</code>订阅到了新的下游。也就是说:</p>
<pre><code>subscribeOn(...)方法的本质是,在指定的线程中将上游和下游进行订阅`。</code></pre>
<p>这和我们链式调用中最后一步的订阅本质上是一样的。</p>
<p>明白了这点,也就能知道,这个线程一旦启动,新的 observer 接收和处理事件,也是在这个子线程里。即,默认情况下它会随着上游线程的切换而切换,二者始终在一个线程,除非它通过<code>observeOn(...)</code>自行指定。</p>
<p>我们现在明白了上游是如何通过一行代码就能运行在子线程里,但还没看到这个线程是什么时候、如何启动起来的。</p>
<p>那我们就回到之前的位置,继续看<code>scheduler.scheduleDirect(new SubscribeTask(parent))</code>这行代码,scheduler 具体指<code>NewThreadScheduler</code>,但<code>scheduleDirect(...)</code>这个方法是在父类中实现的,它没有进行重写(其他类型的 scheduler 有进行重写,比如 ComputationScheduler 等),那就进父类看看:</p>
<pre><code> // Scheduler.java
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
// createWorker()为抽象方法,由子类实现
final Worker w = createWorker();
final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);
DisposeTask task = new DisposeTask(decoratedRun, w);
w.schedule(task, delay, unit);
return task;
}</code></pre>
<p>这个方法的参数中有个 Runnable 对象,那我们直接启动个线程不就好了?当然是可以的。但是作为一个成熟的库,它一定要考虑更多的场景。需要考虑到线程安全问题,以及对线程的控制,比如,通过 Dispose 来截断上下游之间事件的事件流。</p>
<p>我们先看<code>final Worker w = createWorker();</code>这行代码,它创建了一个 Worker,具体点就是<code>NewThreadWorker</code>,这里贴下<code>NewThreadScheduler.java</code>的源码:</p>
<pre><code>/**
* Schedules work on a new thread.
*/
public final class NewThreadScheduler extends Scheduler {
final ThreadFactory threadFactory;
private static final String THREAD_NAME_PREFIX = "RxNewThreadScheduler";
private static final RxThreadFactory THREAD_FACTORY;
/** The name of the system property for setting the thread priority for this Scheduler. */
private static final String KEY_NEWTHREAD_PRIORITY = "rx2.newthread-priority";
static {
int priority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY,
Integer.getInteger(KEY_NEWTHREAD_PRIORITY, Thread.NORM_PRIORITY)));
THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX, priority);
}
public NewThreadScheduler() {
this(THREAD_FACTORY);
}
public NewThreadScheduler(ThreadFactory threadFactory) {
this.threadFactory = threadFactory;
}
@NonNull
@Override
public Worker createWorker() {
return new NewThreadWorker(threadFactory);
}
}</code></pre>
<p>继续回到<code>scheduleDirect(...)</code>方法的第 8 行:</p>
<pre><code> DisposeTask task = new DisposeTask(decoratedRun, w);</code></pre>
<p>它将我们要执行的 runnable 和 Worker,又封装进了一个<code>DisposeTask</code>中,便于对流进行控制。<code>DisposeTask</code>是 Scheduler 的静态内部类,实现了<code>Disposable</code>, <code>Runnable</code>, <code>SchedulerRunnableIntrospection</code>这三个接口:</p>
<pre><code>public abstract class Scheduler {
...
static final class DisposeTask implements Disposable, Runnable, SchedulerRunnableIntrospection {
@NonNull
final Runnable decoratedRun;
@NonNull
final Worker w;
@Nullable
Thread runner;
DisposeTask(@NonNull Runnable decoratedRun, @NonNull Worker w) {
this.decoratedRun = decoratedRun;
this.w = w;
}
@Override
public void run() {
runner = Thread.currentThread();
try {
decoratedRun.run();
} finally {
dispose();
runner = null;
}
}
@Override
public void dispose() {
if (runner == Thread.currentThread() && w instanceof NewThreadWorker) {
((NewThreadWorker)w).shutdown();
} else {
w.dispose();
}
}
@Override
public boolean isDisposed() {
return w.isDisposed();
}
@Override
public Runnable getWrappedRunnable() {
return this.decoratedRun;
}
}
}</code></pre>
<p>创建了 DisposeTask 之后,就将它传递给了<code>worker</code>执行:</p>
<pre><code>w.schedule(task, delay, unit);</code></pre>
<p>这行代码就是开始执行指定任务,我们可以进入<code>NewThreadWorker.java</code>源码中查看详细细节:</p>
<pre><code>public class NewThreadWorker extends Scheduler.Worker implements Disposable {
private final ScheduledExecutorService executor;
volatile boolean disposed;
public NewThreadWorker(ThreadFactory threadFactory) {
executor = SchedulerPoolFactory.create(threadFactory);
}
@NonNull
@Override
public Disposable schedule(@NonNull final Runnable run) {
return schedule(run, 0, null);
}
@NonNull
@Override
public Disposable schedule(@NonNull final Runnable action, long delayTime, @NonNull TimeUnit unit) {
if (disposed) {
return EmptyDisposable.INSTANCE;
}
// 最终会调用到 scheduleActual(...)方法
return scheduleActual(action, delayTime, unit, null);
}
public Disposable scheduleDirect(final Runnable run, long delayTime, TimeUnit unit) {
ScheduledDirectTask task = new ScheduledDirectTask(RxJavaPlugins.onSchedule(run));
try {
Future<?> f;
if (delayTime <= 0L) {
f = executor.submit(task);
} else {
f = executor.schedule(task, delayTime, unit);
}
task.setFuture(f);
return task;
} catch (RejectedExecutionException ex) {
RxJavaPlugins.onError(ex);
return EmptyDisposable.INSTANCE;
}
}
public Disposable schedulePeriodicallyDirect(Runnable run, long initialDelay, long period, TimeUnit unit) {...}
@NonNull
public ScheduledRunnable scheduleActual(final Runnable run, long delayTime, @NonNull TimeUnit unit, @Nullable DisposableContainer parent) {
Runnable decoratedRun = RxJavaPlugins.onSchedule(run);
/**********************************************
*** 将我们的runnable对象,又经过了一层封装 *****
*********************************************/
ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, parent);
if (parent != null) {
if (!parent.add(sr)) {
return sr;
}
}
/*********************************************************************************
*** 最终会通过 executor 线程池去执行相应的任务,通过Future,来获取线程执行后的返回值 *****
********************************************************************************/
Future<?> f;
try {
if (delayTime <= 0) {
f = executor.submit((Callable<Object>)sr);
} else {
f = executor.schedule((Callable<Object>)sr, delayTime, unit);
}
sr.setFuture(f);
} catch (RejectedExecutionException ex) {
if (parent != null) {
parent.remove(sr);
}
RxJavaPlugins.onError(ex);
}
return sr;
}
@Override
public void dispose() {
if (!disposed) {
disposed = true;
executor.shutdownNow();
}
}
/**
* Shuts down the underlying executor in a non-interrupting fashion.
*/
public void shutdown() {
if (!disposed) {
disposed = true;
executor.shutdown();
}
}
@Override
public boolean isDisposed() {
return disposed;
}
}
</code></pre>
<p><code>w.schedule(task, delay, unit)</code>最终会调用到第 46 行的<code>scheduleActual(...)</code>方法。在该方法中,又将新传进来的runnable对象封装进 ScheduledRunnable ,封装了这么多层...<del>~~(>_<)</del>~~。然后就直接将这个 <code>ScheduledRunnable</code>交给线程池去执行了。为了能在线程执行完之后,接收返回值,使用了<code>Future</code>。再往下,就完全是线程池相关的知识点了,此处不再赘述。</p>
<p>到这,我们就完全分析完了 RxJava2 是如何通过一行<code>subscribeOn(...)</code>代码切换上游发送事件所在线程的。接下来我们就来分析<code>observeOn(...)</code>是如何切换下游处理事件的线程的。</p>
<p>线程的创建,这里跟之前是相同的。该方法最终会调用到如下重载方法:</p>
<pre><code> public final Observable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {
...
// 创建了一个 ObservableObserveOn 并返回
return RxJavaPlugins.onAssembly(new ObservableObserveOn<T>(this, scheduler, delayError, bufferSize));
}</code></pre>
<p>直接进<code>ObservableObserveOn</code>的<code>subscribeActual(...)</code>方法:</p>
<pre><code> protected void subscribeActual(Observer<? super T> observer) {
if (scheduler instanceof TrampolineScheduler) {
source.subscribe(observer);
} else {
Scheduler.Worker w = scheduler.createWorker();
source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
}
}</code></pre>
<p>这个方法就比较简单了,直接将上游和新创建的<code>ObserveOnObserver</code>进行绑定。并且在创建的<code>ObserveOnObserver</code>的同时,也将 worker 传进去,进行线程任务的相关处理。到这里,我们可以猜想下,封装之后的新的 ObserveOnObserver 是如何做到使原observer中的任务在指定的线程中执行的。其实就是重写对应的方法,将之前的逻辑通过worker来指定执行线程。边追源码边猜想,才能更好的理解。</p>
<p>接下来就来看<code>ObservableObserveOn.java#ObserveOnObserver</code>的源码:</p>
<pre><code>static final class ObserveOnObserver<T> extends BasicIntQueueDisposable<T>
implements Observer<T>, Runnable {
private static final long serialVersionUID = 6576896619930983584L;
...
ObserveOnObserver(Observer<? super T> actual, Scheduler.Worker worker, boolean delayError, int bufferSize) {
this.downstream = actual;
this.worker = worker;
this.delayError = delayError;
this.bufferSize = bufferSize;
}
@Override
public void onSubscribe(Disposable d) {
if (DisposableHelper.validate(this.upstream, d)) {
this.upstream = d;
if (d instanceof QueueDisposable) {
@SuppressWarnings("unchecked")
QueueDisposable<T> qd = (QueueDisposable<T>) d;
// 注意,这里调用了 requestFusion 来获取 mode,之后会用到
int m = qd.requestFusion(QueueDisposable.ANY | QueueDisposable.BOUNDARY);
if (m == QueueDisposable.SYNC) {
sourceMode = m;
queue = qd;
done = true;
downstream.onSubscribe(this);
// 如果是sync,会立即调用 schedule()
// 执行线程任务,查看run方法
schedule();
return;
}
if (m == QueueDisposable.ASYNC) {
sourceMode = m;
queue = qd;
downstream.onSubscribe(this);
return;
}
}
queue = new SpscLinkedArrayQueue<T>(bufferSize);
downstream.onSubscribe(this);
}
}
@Override
public void onNext(T t) {
if (done) {
return;
}
if (sourceMode != QueueDisposable.ASYNC) {
queue.offer(t);
}
// 执行线程任务,查看run方法
schedule();
}
@Override
public void onError(Throwable t) {
if (done) {
RxJavaPlugins.onError(t);
return;
}
error = t;
done = true;
// 执行线程任务,查看run方法
schedule();
}
@Override
public void onComplete() {
if (done) {
return;
}
done = true;
// 执行线程任务,查看run方法
schedule();
}
@Override
public void dispose() {... }
@Override
public boolean isDisposed() {
return disposed;
}
void schedule() {
if (getAndIncrement() == 0) {
worker.schedule(this);
}
}
void drainNormal() {
int missed = 1;
final SimpleQueue<T> q = queue;
final Observer<? super T> a = downstream;
for (;;) {
// checkTerminated 方法会检查任务是否执行结束。
if (checkTerminated(done, q.isEmpty(), a)) {
return;
}
for (;;) {
boolean d = done;
T v;
try {
v = q.poll();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
disposed = true;
upstream.dispose();
q.clear();
a.onError(ex);
worker.dispose();
return;
}
boolean empty = v == null;
// checkTerminated 方法会检查任务是否执行结束。
if (checkTerminated(d, empty, a)) {
return;
}
if (empty) {
break;
}
a.onNext(v);
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}
void drainFused() {...}
@Override
public void run() {
if (outputFused) {
drainFused();
} else {
// outputFused 是跟背压及操作符相关,这里直接分析 drainNormal()
drainNormal();
}
}
boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a) {
if (disposed) {
queue.clear();
return true;
}
if (d) {
Throwable e = error;
// 是否设置了超时错误,是在 observeOn(scheduler, delayError, bufferSize()) 的第二个参数传入的,
// 默认传了false
if (delayError) {
if (empty) {
disposed = true;
if (e != null) {
a.onError(e);
} else {
a.onComplete();
}
worker.dispose();
return true;
}
} else {
// 根据是否报了异常,来决定是执行 onError 还是 onComplete
if (e != null) {
disposed = true;
queue.clear();
a.onError(e);
worker.dispose();
return true;
} else
if (empty) {
disposed = true;
a.onComplete();
worker.dispose();
return true;
}
}
}
return false;
}
...
}</code></pre>
<p>为了验证我们的猜想,我们看看在<code>onSubscribe/onNext/onError/onComplete</code>这些函数中都调用了什么。</p>
<p>我们发现,在这些函数中,差不多都调用了<code>schedule();</code>(调用 requestFusion(...)相关逻辑暂时忽略)。查看该函数的调用出,在第93行:</p>
<pre><code> void schedule() {
if (getAndIncrement() == 0) {
worker.schedule(this);
}
}</code></pre>
<p>这里直接将<code>this</code>传递给了 worker 进行线程任务的执行,这里的<code>this</code>指的就是<code>ObserveOnObserver</code>,上面说道,它实现了 runnable 接口。而<code>onSubscribe/onNext/onError/onComplete</code>这些函数中都调用了同一个函数<code>schedule();</code>,有理由猜想,对各个函数的区分处理,肯定就在重写的<code>run()</code>方法里了,查看第150行:</p>
<pre><code> public void run() {
if (outputFused) {
drainFused();
} else {
drainNormal();
}
}</code></pre>
<p>outputFused 涉及背压及操作符的相关处理,这里我们直接看<code>drainNormal();</code>:</p>
<pre><code> void drainNormal() {
int missed = 1;
final SimpleQueue<T> q = queue;
final Observer<? super T> a = downstream;
for (;;) {
// checkTerminated 方法会检查任务是否执行结束。
if (checkTerminated(done, q.isEmpty(), a)) {
return;
}
for (;;) {
boolean d = done;
T v;
try {
v = q.poll();
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
disposed = true;
upstream.dispose();
q.clear();
a.onError(ex);
worker.dispose();
return;
}
boolean empty = v == null;
// checkTerminated 方法会检查任务是否执行结束。
if (checkTerminated(d, empty, a)) {
return;
}
if (empty) {
break;
}
// 如果没结束,就调用新的Observer的 onNext方法
a.onNext(v);
}
missed = addAndGet(-missed);
if (missed == 0) {
break;
}
}
}</code></pre>
<p>在该方法中,首先通过<code>checkTerminated(...)</code>判断线程任务是否执行结束(complete或者error),如果没有,就去执行新的下游Observer的onNext()方法。如果执行完了,就直接返回。</p>
<p>那啥时候调用了新的下游Observer的<code>onComplete/onError</code>方法呢?当然是在<code>checkTerminated(...)</code>方法中啦:</p>
<pre><code> boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a) {
if (disposed) {
queue.clear();
return true;
}
if (d) {
Throwable e = error;
// 是否设置了超时错误,是在 observeOn(scheduler, delayError, bufferSize()) 的第二个参数传入的,
// 默认传了false
if (delayError) {
if (empty) {
disposed = true;
if (e != null) {
a.onError(e);
} else {
a.onComplete();
}
worker.dispose();
return true;
}
} else {
// 根据是否报了异常,来决定是执行 onError 还是 onComplete
if (e != null) {
disposed = true;
queue.clear();
// 执行 onError
a.onError(e);
worker.dispose();
return true;
} else
if (empty) {
disposed = true;
// 执行 onComplete
a.onComplete();
worker.dispose();
return true;
}
}
}
return false;
}</code></pre>
<p>在该方法里,我们就看到了对<code>onComplete()/onError</code>方法的调用了。</p>
<p>好了,到这里,我们就把rxjava2 中线程切换的知识讲完了,里面还有很多细节需要大家自己细细研究。</p>
<p><strong>总结</strong>:</p>
<ol>
<li>
<code>下游observer</code>的<code>onSubscribe(...)</code>方法一直是在它所在的线程调用的。即<code>observable.subscribe(observer)</code>这行代码所在的线程。</li>
<li>
<code>subscribeOn(...)</code>指定的是上游发送事件的线程, 比如<code>ObservableOnSubscribe</code>的<code>subscribe(ObservableEmitter<Integer> emitter){...}</code>方法执行的线程,在该方法里我们往往会调用<code>emitter.onNext(...)/onComplete()/onError(...)</code>来发送事件。</li>
<li>
<code>observeOn(...)</code> 指定的是下游接收事件的线程,即<code>onSubscribe(...)/ onNext(...)/onError(...)/onComplete()</code>这些回调方法的执行线程。</li>
<li>默认情况下,下游接收事件的线程和上游发送事件的线程,是同一个线程,下游与上游保持一致。上游通过<code>subscribeOn(...)</code>切换线程的时候,下游仍会自动与其保持一致。除非下游单独通过<code>observeOn(...)</code>来指定下游自己的线程。</li>
</ol>
<p>此外,还需要特别指出的一点就是,<code>多次指定上游的线程只有第一次指定的有效</code>这个结论是:<br><code>错误的 错误的 错误的</code></p>
<p>很多文章中也都是这么说的,但是很遗憾,是错误的,因为很多人都只是从表象出发,连续调用两次<code>subscribeOn</code>,然后在下游Observer的<code>onSubscribe</code>回调里打印线程名称,发现一直是第一次指定的那个线程,就开始想当然的总结结论了,他们的代码应该是下面这样的:</p>
<pre><code> // 上游 observable
Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "subscribe: ");
emitter.onNext(1);
emitter.onNext(2);
emitter.onComplete();
Log.d(TAG, "subscribe: 当前线程为: " + Thread.currentThread().getName());
}
});
// 下游 observer
Observer<Integer> observer = new Observer<Integer>() {...}
observable
// 第一次指定
.subscribeOn(AndroidSchedulers.mainThread())
// 第二次指定
.subscribeOn(Schedulers.newThread())
// 切换到UI线程进行监听
.observeOn(AndroidSchedulers.mainThread())
// 将上游和下游进行关联
.subscribe(observer);</code></pre>
<p>打印结果为:</p>
<p><img src="/img/remote/1460000019243392?w=702&h=290" alt="" title=""></p>
<p>你不断调整两个的位置,发现仍然是指定的第一个有效,似乎你是对的。不防试试下面的例子:</p>
<pre><code> // 上游 observable
Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "subscribe: ");
emitter.onNext(1);
emitter.onNext(2);
emitter.onComplete();
Log.d(TAG, "subscribe: 当前线程为: " + Thread.currentThread().getName());
}
});
// 下游 observer
Observer<Integer> observer = new Observer<Integer>() {...}
observable
// 第一次指定
.subscribeOn(AndroidSchedulers.mainThread())
// 创建第一个 onSubscribe
.doOnSubscribe(new Consumer<Disposable>() {
@Override
public void accept(Disposable disposable) throws Exception {
Log.d(TAG, "accept1: 当前线程为:" + Thread.currentThread().getName());
}
})
// 第二次指定
.subscribeOn(Schedulers.newThread())
// 创建第二个 onSubscribe
.doOnSubscribe(new Consumer<Disposable>() {
@Override
public void accept(Disposable disposable) throws Exception {
Log.d(TAG, "accept2: 当前线程为:" + Thread.currentThread().getName());
}
})
// 切换到UI线程进行监听
.observeOn(AndroidSchedulers.mainThread())
// 将上游和下游进行关联
.subscribe(observer);</code></pre>
<p>结果如下:</p>
<p><img src="/img/remote/1460000019243393" alt="" title=""><br>可以看到,每个<code>doOnSubscribe(...)</code>内的代码,运行在它上面离它最近的<code>subscribeOn()</code>指定的线程。也就是说,多次切换都生效了。这点也可以参考我们上面的总结里的第一条:</p>
<pre><code>下游observer的onSubscribe(...)方法一直是在它所在的线程调用的。即observable.subscribe(observer)这行代码所在的线程。</code></pre>
<p>对<code>doOnSubscribe</code>操作符就不展开讲了。</p>
<p>再仔细看上面的截图,发现我们在第二个<code>doOnSubscribe(...)</code>方法中的代码反而要比第一个先执行。Why?这其实是在向上回溯。希望你还能记得,我们前面说:</p>
<pre><code>subscribeOn(...)方法的本质是,在指定的线程中将上游和下游进行订阅`。</code></pre>
<p>这个“上游”是个相对概念,上游之上,还有上游,所以就不断回溯,最终调用到最开始指定的那个线程。</p>
<p>虽然表面上看,确实是第一个指定的有效,但是千万别被欺骗了。</p>
<p>好了,到这,本篇文章就结束了。文章较长,可以耐心点,反复看看。</p>
<p>通过对 RxJava2 的研究,发现里面涉及到很多知识,我也是一边读一遍补其他知识。比如里面涉及很多并发编程的知识,而并发编程又需要你对计算机组成原理、操作系统、编译原理这些有一定的了解,还好大学考软考的时候看过这些方面的书,拾起来相对容易点。</p>
<p>欠的技术债总是要还的,正面刚吧。</p>
<p>欢迎关注公众号来获取其他最新消息,有趣的灵魂在等你。</p>
<p><img src="/img/remote/1460000018337094?w=430&h=430" alt="" title=""></p>
Rxjava2.x源码解析(一): 订阅流程
https://segmentfault.com/a/1190000019243142
2019-05-21T09:18:03+08:00
2019-05-21T09:18:03+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>现在网上已经有大量的源码分析文章,各种技术的都有。但我觉得很多文章对初学者并不友好,让人读起来云里雾里的,比源码还源码。究其原因,是根本没有从学习者的角度去分析。在自己完成了源码阅读之后,却忘记了自己是如何一步步提出问题,进而走到这里的。</p>
<p>所以,我想在本篇及以后的文章中,花更多的精力去进行源码的分析,争取用浅显易懂的语言,用适合的逻辑去组织内容。这样不至于陷入源码里,导致文章难懂。尽量让更多的人愿意去读源码。</p>
<p>阅读本文,你需要对 RxJava2 的一些基本使用有所了解,不过不用太深。这里推荐下<code>Season_zlc</code>的<a href="https://link.segmentfault.com/?enc=y%2FLtZhhVq5i0EwTIehnumw%3D%3D.96SbLyCKSfLWkeXEtQktNGY0Sdv5%2FeXRM1ZL2ije2vTFoDXCURiQSS9PCQcNt0xT" rel="nofollow">给初学者的RxJava2.0教程(一)</a>,比较浅显易懂。</p>
<p>提到 RxJava,你第一个想到的词是什么?</p>
<p>“异步”。</p>
<p>RxJava 在 GitHub 上的官网主页也说了,“RxJava is a Java VM implementation of Reactive Extensions: a library for composing asynchronous and event-based programs by using observable sequences.”(RxJava是一个使用可观测序列来组建异步、基于事件的程序的库,它是 Reactive Extensions 在Java虚拟机上的一个实现)。它的优点嘛,用扔物线凯哥的话讲,就是“简洁”,并且“随着程序逻辑变得越来越复杂,它依然能够保持简洁”。</p>
<p>这里要注意一点,虽然对大多数人来讲,更多的是使用 RxJava 来配合 Retrofit、OkHttp 进行网络请求框架的封装及数据的异步处理,但是,RxJava和网络请求本质上没有半毛钱的关系。它的本质,官网已经说的很明白了,就是“异步”。</p>
<p>RxJava 基于观察者模式实现,基于事件流进行链式调用。</p>
<p>首先,我们需要添加必要的依赖,这里以最新的<code>2.2.8</code>版本为例:</p>
<pre><code> implementation "io.reactivex.rxjava2:rxjava:2.2.8"</code></pre>
<p>当然,对于 Android 项目来讲,我们一般还需要添加一个补充库:</p>
<pre><code> implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'</code></pre>
<p>这个库其实就是提供了 Android 相关的主线程的支持。</p>
<p>然后写个简单的代码,就可以开始我们的源码分析啦。</p>
<pre><code> // 上游 observable
Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "subscribe: ");
emitter.onNext(1);
emitter.onNext(2);
emitter.onComplete();
}
});
// 下游 observer
Observer<Integer> observer = new Observer<Integer>() {
@Override
public void onSubscribe(Disposable d) {
// onSubscribe 方法会最先被执行
Log.d(TAG, "onSubscribe: ");
}
@Override
public void onNext(Integer integer) {
Log.d(TAG, "onNext: ");
}
@Override
public void onError(Throwable e) {
Log.d(TAG, "onError: ");
}
@Override
public void onComplete() {
Log.d(TAG, "onComplete: ");
}
};
// 将上游和下游进行关联
observable.subscribe(observer);</code></pre>
<p>为便于理解,我故意将可以链式调用的代码,拆成了三部分。你完全可以写成下面的链式风格:</p>
<pre><code> Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "subscribe: ");
emitter.onNext(1);
emitter.onNext(2);
emitter.onComplete();
}
}).subscribe(new Observer<Integer>() {
@Override
public void onSubscribe(Disposable d) {
// onSubscribe 方法会最先被执行
Log.d(TAG, "onSubscribe: ");
}
@Override
public void onNext(Integer integer) {
Log.d(TAG, "onNext: ");
}
@Override
public void onError(Throwable e) {
Log.d(TAG, "onError: ");
}
@Override
public void onComplete() {
Log.d(TAG, "onComplete: ");
}
});</code></pre>
<p>同样,为了便于理解,我会借用<code>i/o</code>流里面经常用到的<code>水流</code>进行类比。将被观察者 <code>observable</code> 称为<code>上游(upstream)</code>,将观察者 <code>observer</code> 称为<code>下游(downstream)</code>。读源码其实也能看出,作者本身也正是这么类比的。</p>
<p>通过将整个过程拆分成三个步骤,能更清晰的理清逻辑。我们需要做的,本质上就是创建一个上游和一个下游,最终通过上游对象的<code>subscribe</code>方法将二者关联起来:</p>
<ol>
<li>创建一个 Observable 的实现类</li>
<li>创建一个 Observer 的实现类</li>
<li>将二者通过 Observable 的 subscribe(...) 方法将二者进行关联</li>
</ol>
<p>明白了这三点,以后我们就不会被各种实现类搞的眼花缭乱。</p>
<p>这三个步骤,里面的核心是第三部,也就是订阅过程,毕竟,这属于一个动作,而我们进行源码分析的时候,往往就是从动作开始的。这时候,我们<code>Ctrl/Command + 鼠标左键</code>,进入该方法看看,里面做了下什么。</p>
<pre><code> public final void subscribe(Observer<? super T> observer) {
ObjectHelper.requireNonNull(observer, "observer is null");
try {
// RxJavaPlugins是个钩子函数,用来在代码的执行前后插入进行一些操作
observer = RxJavaPlugins.onSubscribe(this, observer);
ObjectHelper.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins");
// 关键点是这行代码
subscribeActual(observer);
} catch (NullPointerException e) { // NOPMD
throw e;
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
// can't call onError because no way to know if a Disposable has been set or not
// can't call onSubscribe because the call might have set a Subscription already
RxJavaPlugins.onError(e);
NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS");
npe.initCause(e);
throw npe;
}
}</code></pre>
<p>这里将this(上游Observable类型)的和下游observer作为参数传给了 RxJavaPlugins 的 onSubscribe(...)方法,并返回一个Observer,同时,将原来的observer指向这个返回值,那么我们看看这个函数中到底进行了什么操作:</p>
<pre><code> // RxJavaPlugins.java
public static <T> Observer<? super T> onSubscribe(@NonNull Observable<T> source, @NonNull Observer<? super T> observer) {
BiFunction<? super Observable, ? super Observer, ? extends Observer> f = onObservableSubscribe;
if (f != null) {
return apply(f, source, observer);
}
return observer;
}</code></pre>
<p>这里判断<code>onObservableSubscribe</code>是否为 null,不为 null 则调用其 apply(...) 方法。若为 null ,则直接返回原来的observer。而该变量需要通过RxJavaPlugin的<code>setOnSingleSubscribe(...)</code>方法来指定的,显然,我们并没有指定,所以忽略不管(后面遇到类似问题,基本也都可以忽略)。</p>
<p>回到之前的订阅流程,就可以简化为下面这样:</p>
<pre><code> public final void subscribe(Observer<? super T> observer) {
ObjectHelper.requireNonNull(observer, "observer is null");
try {
...
// 调用到具体实现子类的 subscribeActual(observer) 方法
subscribeActual(observer);
} catch (
...
}
}</code></pre>
<p>从上面代码可以看出,订阅过程,即调用Observable的<code>subscribe(...)</code>的过程,其实就是直接调用了其实现类的<code>subscribeActual(observer)</code>方法(该方法在 Observable 中是个抽象方法)。以后我们遇到这个方法,就直接去 Observable 的实现类中找即可,就不会乱了。</p>
<p>一些熟悉RxJava的朋友可能会说,有时候我们通过<code>subscribe(...)</code>订阅的并不是Observer对象,而是consumer对象,有各种重载。如下:</p>
<p><img src="/img/remote/1460000019243145?w=1616&h=320" alt="" title=""></p>
<p>当你传入的是Consumer的时候,不管你传递了几个参数,最终都会代用到以下方法,那些你没传递的 onError或者 onComplete 回调等等,会自动使用默认创建的值。</p>
<pre><code> public final Disposable subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,
Action onComplete, Consumer<? super Disposable> onSubscribe) {
ObjectHelper.requireNonNull(onNext, "onNext is null");
ObjectHelper.requireNonNull(onError, "onError is null");
ObjectHelper.requireNonNull(onComplete, "onComplete is null");
ObjectHelper.requireNonNull(onSubscribe, "onSubscribe is null");
// 最终都会封装成一个 LambdaObserver,并作为参数传入subscribe(...)方法中
LambdaObserver<T> ls = new LambdaObserver<T>(onNext, onError, onComplete, onSubscribe);
subscribe(ls);
return ls;
}</code></pre>
<p>可以看出,这里最终还是将这些 <code>Consumer</code> 对象包装在了一个 LambdaObserver 类型的变量中,然后又调用了<code>subscribe(...)</code>方法,将其作为变量传入,之后的分析,就跟上面是一样的了。</p>
<p>订阅方法讲完了,我们也知道最终调用到了 Observable 的实现类的<code>subscribeActual(...)</code>方法。那接下来肯定就是要弄懂在这个方中做了什么事。我们例子中是使用<code>Observable.create(...)</code>方法创建的 observable:</p>
<pre><code> // 上游 observable
Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "subscribe: ");
emitter.onNext(1);
emitter.onNext(2);
emitter.onComplete();
}
});</code></pre>
<p>其中,<code>Observable.create(...)</code>方法的实现是这样的:</p>
<pre><code> public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {
ObjectHelper.requireNonNull(source, "source is null");
return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source));
}</code></pre>
<p>我们传进去了一个实现了<code>ObservableOnSubscribe</code>接口的匿名内部类,该接口类也很简单,就定义了一个<code> void subscribe(@NonNull ObservableEmitter<T> emitter) throws Exception</code>抽象方法。</p>
<p>然后我们将传进来的source(刚刚提到的匿名内部类ObservableOnSubscribe)封装进一个<code>ObservableCreate</code>对象中,又传进了<code>RxJavaPlugins.onAssembly(...)</code>中,这个RxJavaPlugins类刚才我们说过,其实就是一个hook类,暂时直接忽略,一般就是直接把传进来的参数返回了(不放心的话可以自己点进去,以后遇到该方法不再赘述)。</p>
<p>也就是说<code>Observable.create(...)</code>方法最终创建了一个<code>ObservableCreate</code>对象。注意,该对象是<code>Observable</code>抽象类的具体实现类。</p>
<p>特别注意!<br>特别注意!<br>特别注意!</p>
<p>重要事情说三遍。我们这里通过<code>create(...)</code>方法创建的<code>Observable</code>的具体实现子类是<code>ObservableCreate</code>。该子类的命名是有规律可言的。我在分析源码的时候有时候就想,这么多看起来名字都一样的类,RxJava的开发者本人不会懵逼吗?作为一个用户量这么大的库,肯定各种都有讲究,肯定有贵了。嗯。规律就是生成的子类的命名方法为<code>“Observable+创建该类的方法名”</code>,即:在创建该类的方法名称前面加上个Observable,以此来作为新的类的名称。</p>
<p>不信?</p>
<p>我们还可以通过<code>Observable.just(...)</code>这种方式来创建Observable,点进去看看具体子类名字是啥:</p>
<p><img src="/img/remote/1460000019243146" alt="" title=""></p>
<p>其他的自己就去验证吧。</p>
<p>所以,我们以后遇到<code>Observable</code>开头的类名,就可以猜测它是一个<code>Observable</code>类型的变量,类名后面的部分,就是创建该变量的方法(确保严谨,倒推可能不成立,要仔细确认)。</p>
<p>同样的,各种<code>Observer</code>的实现类也是类似,只不过各种它们是把创建的方法放在了前面,然后以<code>Observer</code>结尾而已,这点之后遇到的时候会再提及。</p>
<p>回到刚才讲的。我们通过create(...)方法,创建出来的是<code>ObservableCreate</code>,它是个Observable,那我们就直接看它的<code>subscribeActual(...)</code>方法究竟做了什么:</p>
<pre><code> protected void subscribeActual(Observer<? super T> observer) {
CreateEmitter<T> parent = new CreateEmitter<T>(observer);
// 首先调用下游 observer 的 onSubscribe方法
observer.onSubscribe(parent);
try {
source.subscribe(parent);
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
parent.onError(ex);
}
}</code></pre>
<p>首先,将<code>observer</code>包装进<code>CreateEmitter</code>对象中。</p>
<p>然后立即调用 Observer 的<code>onSubscribe(parent)</code>方法,表示订阅过程完成。(当我们通过<code>subscribe(...)</code>进行订阅的时候,会立即调用下游Observer 的<code>onSubscribe(...)</code>方法。通过查看其它实现类,可以总结出该结论)。</p>
<p>这里,会将我们的封装类<code>CreateEmitter</code>作为参数传进<code>onSubscribe(...)</code>方法中。</p>
<p>之后,又在代码<code>source.subscribe(parent)</code>中将其作为参数传递。这里的source,是源的意思,其实也就是上游。此例子中具体指我们传入<code>Observable.create(...)</code>中的<code>ObservableOnSubscribe</code>类型的匿名内部类。</p>
<p>而我们已经实现了该抽象方法:</p>
<pre><code> // 上游 observable
Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
@Override
public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
Log.d(TAG, "subscribe: ");
emitter.onNext(1);
emitter.onNext(2);
emitter.onComplete();
}
});</code></pre>
<p>我们之后就是调用的传进来的<code>ObservableEmitter</code>的<code>onNext/onError/OnComplete</code>来发送事件的。等等,我们创建的时候不是传进来的是<code>CreateEmitter</code>吗,怎么又变成了<code>ObservableEmitter</code>?其实,<code>CreateEmitter</code>是ObservableCreate的一个 static final 类型的内部类,并且实现了<code>ObservableEmitter</code>接口。因为是由<code>create</code>方法创建的,所以这样命名咯,同时,又作为内部类定义在 <code>ObservableCreate</code> 中,这样,用到的时候是不是就不那么凌乱啦?</p>
<p>到这里,我们知道了会通过回调<code>emitter</code>的各种方法来发送事件,这些事件又是怎么被observer 正确接收并处理的呢?</p>
<p>我们继续回到 ObservableCreate 的<code>subscribeActual(...)</code>方法:</p>
<pre><code> protected void subscribeActual(Observer<? super T> observer) {
CreateEmitter<T> parent = new CreateEmitter<T>(observer);
observer.onSubscribe(parent);
try {
source.subscribe(parent);
} catch (Throwable ex) {
Exceptions.throwIfFatal(ex);
parent.onError(ex);
}
}</code></pre>
<p>我们发送事件,最终调用的其实是 parent(即 CreateEmitter) 中的相应方法,而 CreateEmitter 里又封装了 observer。我们到 CreateEmitter 这个类的源码中,看看发送事件的时候,都干嘛了:</p>
<pre><code> static final class CreateEmitter<T>
extends AtomicReference<Disposable>
implements ObservableEmitter<T>, Disposable {
private static final long serialVersionUID = -3434801548987643227L;
final Observer<? super T> observer;
CreateEmitter(Observer<? super T> observer) {
this.observer = observer;
}
@Override
public void onNext(T t) {
if (t == null) {
onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
return;
}
if (!isDisposed()) {
observer.onNext(t);
}
}
@Override
public void onError(Throwable t) {
if (!tryOnError(t)) {
RxJavaPlugins.onError(t);
}
}
@Override
public boolean tryOnError(Throwable t) {
if (t == null) {
t = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources.");
}
if (!isDisposed()) {
try {
observer.onError(t);
} finally {
dispose();
}
return true;
}
return false;
}
@Override
public void onComplete() {
if (!isDisposed()) {
try {
observer.onComplete();
} finally {
dispose();
}
}
}
@Override
public void setDisposable(Disposable d) {
DisposableHelper.set(this, d);
}
@Override
public void setCancellable(Cancellable c) {
setDisposable(new CancellableDisposable(c));
}
@Override
public ObservableEmitter<T> serialize() {
// 这里返回了一个 SerializedEmitter,并传入 this,也就是 CreateEmitter 对象
return new SerializedEmitter<T>(this);
}
@Override
public void dispose() {
DisposableHelper.dispose(this);
}
@Override
public boolean isDisposed() {
// 这里判断,是否已经处于 disposed 状态,
// 注意 get() 是定义在 AtomicReference 中的方法
return DisposableHelper.isDisposed(get());
}
@Override
public String toString() {
return String.format("%s{%s}", getClass().getSimpleName(), super.toString());
}
}</code></pre>
<p>这里的代码也是比较简单的,就是将发送的事件中的参数直接传递给 observer 中的相应方法。只不过中间多了背压的判断(该类实现了Disposable 接口)。同时注意,该类还是 AtomicReference 的子类,可以实现原子操作。并且在覆写的 ObservableEmitter 的<code>serialize()</code>接口中创建并返回了一个<code>SerializedEmitter</code>,这些都是跟线程安全以及背压相关的,不是本文的重点。</p>
<p>还有一点,需要大家注意,从<code>RxJava2.x</code>开始,已经不允许向<code>onNext/onError</code>中传<code>null</code>值,否则会报空指针,这点在上面的源码中也能看到。这就会对封装网络请求的时候产生影响,比如请求<code>验证验证码</code>接口成功,但是后台返回的 result 字段为 null,我们此时可能仍然想要它调用 onNext 方法去执行成功的回调。那这就需要额外的处理了。网上也有一些解决方案,但是总觉得不够优雅,有大佬有比较好的建议,也可以指点下。</p>
<p>好啦,本篇文章就写到这里,带大家完成了订阅、事件的发送及处理的整个流程。</p>
<p>关于线程切换的内容,放在下一篇文章中讲。毕竟,不谈线程切换,谈什么 RxJava源码 分析,哈哈。</p>
<p>欢迎关注公众号来获取其他最新消息,有趣的灵魂在等你。</p>
<p><img src="/img/remote/1460000018337094?w=430&h=430" alt="" title=""></p>
工作、开源两不误:Git多账号管理
https://segmentfault.com/a/1190000018863655
2019-04-14T17:23:37+08:00
2019-04-14T17:23:37+08:00
warmcheng
https://segmentfault.com/u/warmcheng
17
<p>由于 Git 所具有的巨大优越性,越来越多的公司以及个人开始由 Svn 转向 Git 。一般来讲,每位员工都会被分配给一个公司内部的邮箱。比如一个 996 公司的员工 “张三”,获得的可能就是一个 “zhangsan@996icu.com” 的邮箱。比较规范的公司,就会要求我们使用自己的名字和公司所分配给自己的这个邮箱来配置 Git(姓名和邮箱可以不用引号括起来):</p>
<pre><code>git config --global user.name "张三"
git config --global user.email "zhangsan@996icu.com"</code></pre>
<p>但是这种配置是全局的,如果我们之前刚好有在 GitHub 上维护项目,那这样势必就会将之前所做的 Git 账户配置给覆盖了。那怎么解决呢?我们总不能来回覆盖,来回添加密钥吧。我们能不能同时配置多个 Git 账户呢?</p>
<p>当然能。</p>
<p>这里以 Mac 为例,如果我们之前配置过全局的用户名和邮箱,那么在用户目录下的<code>.gitconfig</code>文件中(如<code>/Users/zhangsan/.gitconfig</code>),会有类似如下的配置:</p>
<pre><code>[user]
name = 张三
email = zhangsan@gmail.com</code></pre>
<p>当然,我们也可以直接使用命令来查看:</p>
<pre><code>git config --global user.name
git config --global user.email</code></pre>
<p>如果设置了这两个全局属性,就会输出对应的值。若任何输出的话,则表示未设置。</p>
<p>如果设置过,我们就需要将用户名和邮箱这两个全局变量进行重置。使用如下命令:</p>
<pre><code>git config --global --unset user.name
git config --global --unset user.email </code></pre>
<p>我们知道,一般 Git 服务器为了安全,都会要求我们添加一个安全的 SSH 密钥。但是默认情况下,生成的密钥的文件名都是一样的。因而,不同的用户,必须设置不同文件名的密钥文件,否则会发生覆盖。所以,接下来千万别觉得太熟悉不过了,就一路回车,千万要悠着点手速。</p>
<p>以 “张三” 为例,首先,我们需要根据公司邮箱来生成密钥对:</p>
<pre><code>ssh-keygen -t rsa -C "zhangsan@996icu.com"</code></pre>
<p>回车后会出现下面这句话:</p>
<pre><code>Generating public/private rsa key pair.
Enter file in which to save the key (/Users/zhangsan/.ssh/id_rsa):</code></pre>
<p>这时候可千万别一路回车,注意看提示,这里要我们输入要保存的私钥的路径和文件名,为了以后易找,我们就仍然放在该路径下,只不过更改个跟平台相关的文件名,输入:</p>
<pre><code>Users/zhangsan/.ssh/996icu_id_rsa</code></pre>
<p>接着就可以一路回车了,默认密码为空即可。</p>
<p>生成完密钥之后,我们还需要使用<code>ssh-add</code>命令是把专用密钥添加到<code>ssh-agent</code>的高速缓存中。该命令位置在<code>/usr/bin/ssh-add</code>,用法如下:</p>
<pre><code>ssh-add -K ~/.ssh/996icu_id_rsa</code></pre>
<p>之后我们需要将生成的密钥对中的公钥里的内容用文本编辑器打开,复制下来,添加到对应的平台上面,比如公司的 GitLab 或者 GitHub 等。</p>
<p>Mac 下面可以直接使用如下命令来把公钥复制到剪切板:</p>
<pre><code>pbcopy < ~/.ssh/996icu_id_rsa.pub</code></pre>
<p>同样地,我们使用 “zhangsan@gmail.com” 这个邮箱,来生成供 GitHub 使用的账户的私钥<code>github_id_rsa</code>和公钥<code>github_id_rsa.pub</code>,并把公钥添加到 GitHub 平台上。</p>
<p>接下来我们还需要修改 Git 的本地配置,来将远程的服务器地址和本地的私钥文件进行关联。这样通过比较私钥和之前填在该平台上的公钥,就能进行权限验证。</p>
<p>在<code>Users/用户名/.ssh/</code>目录下面新建一个名为<code>config</code>的配置文件,添加如下内容:</p>
<pre><code># github email address
Host github
HostName github.com
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/github_id_rsa
# gitlab email address
# 公司内网地址
HostName 192.168.6.106
User git
PreferredAuthentications publickey
IdentityFile ~/.ssh/996icu_id_rsa</code></pre>
<p>这里就将远程地址和本地的私钥文件对应了起来。</p>
<p>配置文件中的 HostName 是远程仓库的访问地址,这里可以是 IP,也可以是域名。Host 是用来拉取的仓库的别名,配不配置都行。如果 HostName 没配置的话,那就必须把 Host 配置为仓库 IP 地址或者域名,而非别名。</p>
<p>配置了这些之后,我们就能够成功的从远程拉取仓库了,拉取之后,<code>cd</code>到仓库目录下,配置该仓库使用的用户名和邮箱:</p>
<pre><code>git config --local user.name 张三
git config --local user.email zhangsan@996icu.com</code></pre>
<p>当然,你也可以直接不用 <code>--local</code>参数</p>
<p>注意,这里的账户可以和我们开始时生成秘钥的邮箱不同。那个邮箱其实配置的是我们电脑针对某个 IP 的 “全局” 账户(注意,不是global参数指定的那个全局),这里配置的是某个仓库下的 “局部” 用户,当然,你把这个仓库再 copy 一份的话,就可以设置个其他的用户名和邮箱了,毕竟是局部的嘛,相当于多个用户在同一台电脑上进行工作。</p>
<p>还有,如果你未指用户名和邮箱的话,Git 会自动使用电脑登录的用户名,比如“zhangsan”,邮箱默认就是 “zhangsan@zhangsan.local”,这当然不是我们想要的,所以最好配置下吧。</p>
<p>到这里,还没完。如果你使用的是 IDEA,比如 AS,还需要设置 SSH 使用本地(native)的客户端,而非 AS 内嵌(build in)的 SSH 客户端 :</p>
<p><img src="/img/remote/1460000018863658" alt="" title=""></p>
<p>在AS 3.5 Canary 10 版本上,发现没有这个选项,应该是默认使用了本地的 SSH 客户端。</p>
<p>到这里仍然没有完,对于 Mac,可能会遇到升级系统或者重启系统之后,SSH私钥失效的问题,这时候我们可以通过在<code>~/.ssh/config</code>文件中添加如下内容来解决:</p>
<pre><code>Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/github_id_rsa
IdentityFile ~/.ssh/996icu_id_rsa</code></pre>
<p>如果还不生效,那就可以通过自己编写 shell 脚本<br>(这里我使用的是 zsh,所以配置的是<code>#! /bin/zsh</code>,若使用的是系统自带的终端,可以修改为<code>#! /bin/bash</code>):</p>
<pre><code>vim .ssh/ssh_add_private_keys.sh
#! /bin/zsh
# 添加 github 公钥
ssh-add ~/.ssh/github_id_rsa
# 添加 公司 gitlab 公钥
ssh-add ~/.ssh/996icu_id_rsa
// 赋予sh文件可运行权限
chmod +x .ssh/ssh_add_private_keys.sh</code></pre>
<p>然后把它添加到开机启动项中:<code>系统偏好设置>用户与群组>登录项</code>。</p>
<p><img src="/img/remote/1460000018863659" alt="" title=""></p>
<p>当然,我们也可以创建一个<code>Automator</code>任务,并将其添加到系统开机启动项中。</p>
<p><img src="/img/remote/1460000018863660" alt="" title=""></p>
<p>然后创建一个 App 应用程序:</p>
<p><img src="/img/remote/1460000018863661?w=1930&h=1332" alt="" title=""></p>
<p>添加 shell 脚本代码:</p>
<p><img src="/img/remote/1460000018863662" alt="" title=""></p>
<p>然后按<code>Command+S</code>保存,文件名另存为<code>ssh_add_private_keys.app</code>:</p>
<p><img src="/img/remote/1460000018863663" alt="" title=""></p>
<p>然后把刚才添加的这个APP添加到开机启动项中:</p>
<p><img src="/img/remote/1460000018863664" alt="" title=""></p>
<p>当然,上面这两种添加方式本质上都是一样的,只不过一个是创建的<code>.sh</code>的 shell 脚本文件,而一个是运行 shell 脚本的 App 文件形式。</p>
<p>欢迎关注公众号来获取其他最新消息,有趣的灵魂在等你。</p>
<p><img src="/img/remote/1460000018337094?w=430&h=430" alt="" title=""></p>
LiveData && ViewModel 使用详解
https://segmentfault.com/a/1190000018863550
2019-04-14T17:15:14+08:00
2019-04-14T17:15:14+08:00
warmcheng
https://segmentfault.com/u/warmcheng
2
<h2>前言</h2>
<p>在之前的文章中,我们讲了Android Architecture components 中的 Lifecycle 组件的详细使用以及源码解析。本篇将介绍另外AAC中另外两个组件:LiveData 和 ViewModel,它们的实现也都是利用了 Lifecycle。</p>
<h2>什么是 LiveData</h2>
<p>LiveData 是一个可观测的数据持有类,但是不同于通常的被观察者,LiveData 具有生命周期感知能力。通俗点说,LiveData 就是具有 “Live” 能力的 “Data” 持有类。当它所持有的数据发生改变的时候,并且 Lifecycle 对象(比如 Activity 或者 Fragment 等)处于活跃状态(STARTED 或者 RESUMED),LiveData 将立即通知观察者数据发生了变化。也就是说,比普通观察者多了个生命周期感知能力。</p>
<h3>LiveData 的优势</h3>
<ol><li>确保UI和数据状态匹配。</li></ol>
<p>当数据发生改变的时候,会自动通知UI进行更新。</p>
<ol><li>避免内存泄漏</li></ol>
<p>Observers 是绑定到 Lifecycle 对象上的,当与其关联的 lifecycle 被销毁的时候,它们会自动被清理。</p>
<ol><li>避免了由于 Activity 停止而导致的闪退</li></ol>
<p>当 Observer 所绑定的 Lifecycle 处于非活跃状态时,比如处于返回栈中的 Activity,它将不会收到任何 LiveData 事件。</p>
<ol><li>不再需要手动处理生命周期</li></ol>
<p>UI 组件只需要对相关的数据进行监听,不需要关心是否应该暂停或者恢复监听。LiveData 具有生命周期感知能力,它会自动对这些进行管理。</p>
<ol><li>数据总处于最新状态</li></ol>
<p>如果一个 Lifecycle 处于非活跃状态,那当它由非活跃状态变为活跃状态的时候,它将收到最新的数据。比如一个 Activity 由后台转为前台,这时候它将立即收到最新的数据</p>
<ol><li>系统配置更改时,进行数据的保存和恢复,及 UI 的恢复。</li></ol>
<p>当 Activity 或者 Fragment 由于配置更改而重新创建时(比如旋转屏幕等),它将收到最新的可用数据。这里简单提一点,这个有点是需要配合 ViewModel 使用的,严格来说,它主要是 ViewModel 的优点</p>
<ol><li>资源共享</li></ol>
<p>我们可以使用单例模式来扩展 LiveData,这样就能达到数据变化的时候,通知所有的观察者。</p>
<p>为了便于理解,关于 LiveData 和 ViewModel 的关系,我这里先说结论:</p>
<p>LiveData 的作用是在使得数据能具有生命周期感知能力,在 Activity 等变为活跃状态的时候,自动回调观察者中的回调方法。也就是说对数据的变化进行实时监听。而 ViewModel 的作用则是,当因系统配置发生改变导致 Activity 重建的时候(比如旋转屏幕),能对 LiveData 进行正确的保存和恢复。仅此而已。</p>
<h3>LiveData 的使用</h3>
<p>一般来讲,LiveData 是需要配合 ViewModel 来使用的,但千万不要觉得 LiveData 就一定结合 ViewModel。上面也说道二者只是功能互补。这里为了便于理解,我们先单独学习下 LiveData 的使用。</p>
<p>LiveData 的使用分三步:</p>
<ol>
<li>创建一个 LiveData 的实例,让它持有一种特定的数据类型,比如 String 或者 User .通常是将 LiveData 放在ViewModel中使用的(这里我们先单独使用)。</li>
<li>创建一个 Observer 对象,并实现其 onChanged(...) 方法,在这里定义当 LiveData 持有的数据发生改变的时候,应该做何操作。可以在这进行UI的更新,一般 Observer 是在 UI controller 中创建,比如 Activity 或者 Fragment 。</li>
<li>通过创建的 LiveData 实例的 observe(...)方法,将 Observer 对象添加进 LiveData 中。方法的原型为<code>observe( LifecycleOwner owner, Observer<? super T> observer)</code>,第一个参数是 LifecycleOwner对象,这也是 LiveData 能监听生命周期的能力来源。第二个参数就是我们的监听器对象 Observer 。</li>
</ol>
<p>添加 LiveData 和 ViewModel 的依赖:</p>
<pre><code> implementation "android.arch.lifecycle:extensions:1.1.1"</code></pre>
<p>当然,你也可以分别单独集成 LiveData 和 ViewModel:</p>
<pre><code>implementation "android.arch.lifecycle:livedata:1.1.1"</code></pre>
<pre><code>implementation "android.arch.lifecycle:viewmodel:1.1.1"</code></pre>
<p>接下来就对照上面讲的三步走战略,创建如下代码:</p>
<pre><code>public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "MainActivity";
private MutableLiveData<Integer> mNumberLiveData;
private TextView mTvNumber;
private Button mBtnStart;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTvNumber = findViewById(R.id.tv_number);
mBtnStart = findViewById(R.id.btn_start);
mBtnStart.setOnClickListener(this);
mNumberLiveData = new MutableLiveData<>();
mNumberLiveData.observe(this, new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
mTvNumber.setText("" + integer);
Log.d(TAG, "onChanged: " + integer);
}
});
}
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
super.run();
int number = 0;
while (number < 5) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
number++;
mNumberLiveData.postValue(number);
}
}
}.start();
}
}</code></pre>
<p>这里,我们在 onCreate 方法中创建了一个 MutableLiveData 类型的变量 mNumberLiveData ,并将其泛型指定为 Integer,通过其<code>observe(...)</code>方法把 this 传进去(this为 AppCompatActivity,实现了 LifecycleOwner 接口,支持包为 28.0.0),并传进去一个 Observer,在其<code>onChanged(...)</code>方法中,我们将变化后的数据 integer 设置给 TextView 显示。为了便于观察,我们同时在控制台打印一行对应的日志。</p>
<p><img src="/img/remote/1460000018863553" alt="" title=""></p>
<p>Demo 的界面很简单,就是一个按钮,一个 TextView ,点击按钮,开启一个子线程,每过3秒通过<code>postValue(...)</code>修改 LiveData 中的值(如果是在UI线程,可以直接通过 <code>setValue(...)</code>来修改)。</p>
<p>这里我们点击开始,并在数字还没变为 5 的时候,就按<code>Home键</code>进入后台,等过一段时间之后,在进入页面,会发现页面最终显示为数字 “5”,但是打印的结果并不是连续的1~5,而是有中断:</p>
<p><img src="/img/remote/1460000018863554" alt="-w785" title="-w785"></p>
<p>这也证明了当程序进入后台,变为 inactive 状态时,并不会收到数据更新的通知,而是在重新变为 active 状态的时候才会收到通知,并执行<code>onChanged(...)</code>方法。</p>
<p>上面可以看到,我们使用 LiveData 的时候,实际使用的是它的子类 MutableLiveData,LiveData 是一个接口,它并没有给我们暴露出来方法供我们对数据进行修改。如果我们需要对数据修改的时候,需要使用它的具体实现类 MutableLiveData,其实该类也只是简单的将 LiveData 的 <code>postValue(...)</code>和 <code>setValue(...)</code>暴露了出来:</p>
<pre><code>public class MutableLiveData<T> extends LiveData<T> {
@Override
public void postValue(T value) {
super.postValue(value);
}
@Override
public void setValue(T value) {
super.setValue(value);
}
}</code></pre>
<p><code>MutableLiveData<T></code>其实是对数据进行了一层包裹。在它的泛型中可以指定我们的数据类。可以存储任何数据,包括实现了 Collections 接口的类,比如 List 。</p>
<h3>扩展 LiveData</h3>
<p>有时候我们需要在 observer 的 lifecycle 处于 active 状态时做一些操作,那么我们就可以通过继承 LiveData 或者 MutableLiveData,然后覆写其<code>onActive()</code>和<code>onInactive()</code>方法。这两个方法的默认实现均为空。像下面这样:</p>
<pre><code>public class StockLiveData extends LiveData<BigDecimal> {
private StockManager stockManager;
private SimplePriceListener listener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};
public StockLiveData(String symbol) {
stockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
stockManager.requestPriceUpdates(listener);
}
@Override
protected void onInactive() {
stockManager.removeUpdates(listener);
}
}
</code></pre>
<p>LiveData 具有生命周期感知能力,能在 Activity 销毁的时候自动取消监听,这也意味着它可以用来在多个 Activity 间共享数据。我们可以借助单例来实现,这里直接饮用官方 Demo :</p>
<pre><code>public class StockLiveData extends LiveData<BigDecimal> {
private static StockLiveData sInstance;
private StockManager stockManager;
private SimplePriceListener listener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};
@MainThread
public static StockLiveData get(String symbol) {
if (sInstance == null) {
sInstance = new StockLiveData(symbol);
}
return sInstance;
}
private StockLiveData(String symbol) {
stockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
stockManager.requestPriceUpdates(listener);
}
@Override
protected void onInactive() {
stockManager.removeUpdates(listener);
}
}</code></pre>
<h3>转换 LiveData</h3>
<p>有时候,我们需要在将 LiveData 中存储的数据分发给 Observer 之前进行一些修改。比如我们例子中拿到的是 Integer 类型的返回值,我们设置进 TextView 的时候,直接使用<code>mTvNumber.setText(integer)</code>会报错,需要使用<code>mTvNumber.setText("" + integer)</code>这种形式,但我想在这里直接拿到已经处理过的 String 数据,拿到就能直接用,而不需要再在这里手动拼。我们可以通过<code>Transformations</code>类的 <code>map</code> 操作符来实现这个功能。</p>
<p>原始的代码为:</p>
<pre><code> mNumberLiveData = new MutableLiveData<>();
mNumberLiveData.observe(this, new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer integer) {
mTvNumber.setText("" + integer);
Log.d(TAG, "onChanged: " + integer);
}
});</code></pre>
<p>使用<code> Transformations.map(...)</code>改造之后的代码:</p>
<pre><code> mNumberLiveData = new MutableLiveData<Integer>();
Transformations.map(mNumberLiveData, new Function<Integer, String>() {
@Override
public String apply(Integer integer) {
return "" + integer;
}
}).observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
mTvNumber.setText(s);
Log.d(TAG, "onChanged: " + s);
}
});</code></pre>
<p>这就实现了将一种类型的数据转化为另一种类型的数据。map 操作符会返回一个改造之后的 LiveData,直接对这个 LiveData 进行监听即可。这里的<code>map</code>操作符类似于 RxJava 的<code>map</code>。</p>
<p>但有时候我们并不只是需要简单的把数据由一种类型转为另一种类型。我们可能需要的更高级一点。</p>
<p>比如,我们一方面需要一个存储 userId 的 LiveData,另一方面又需要维护一个存储 User 信息的 LiveData,而后者的 User 则是根据 userId 来从数据库中查找的,二者需要对应。这时候我们就可以使用<code>Transformations</code>类的<code>switchMap(...)</code>操作符。</p>
<pre><code>MutableLiveData<String> userIdLiveData = new MutableLiveData<>();
LiveData<User> userLiveData = Transformations.switchMap(userIdLiveData, new Function<String, LiveData<User>>() {
@Override
public LiveData<User> apply(String userId) {
// 根据 userId 返回一个 LiveData<User>,可以通过Room来获取
return getUser(userId);
}
});</code></pre>
<p>这里,我们在覆写的<code>apply(...)</code>方法中,每次 userId 发生变化之后,会自动通过 getUser(userId) 去获取一个封装有 User 对象的 LiveData。如果是从数据库获取的话,使用 Google 推出的配套的数据库组件 Room 会比较爽,因为它能直接返回一个 LiveData。关于 Room,有时间的话之后再写文章讲解。</p>
<p>从上面可以看出,LiveData 包中提供的 Transformations 非常有用,能让我们的整个调用过程变成链式。但 Transformations 只提供了<code>map(...)</code>和<code>switchMap(...)</code>两个方法,如果我们有其他更复杂的需求,就需要自己通过<code>MediatorLiveData</code>类来创建自己的<code>transformations</code>。话说回来,其实上面两个方法的内部,就是通过<code>MediatorLiveData</code>来实现的,通过 MediatorLiveData 进行了一次转发。这里贴出<code>Transformations</code>的源码:</p>
<pre><code>public class Transformations {
private Transformations() {
}
@MainThread
public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source,
@NonNull final Function<X, Y> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(source, new Observer<X>() {
@Override
public void onChanged(@Nullable X x) {
result.setValue(func.apply(x));
}
});
return result;
}
@MainThread
public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger,
@NonNull final Function<X, LiveData<Y>> func) {
final MediatorLiveData<Y> result = new MediatorLiveData<>();
result.addSource(trigger, new Observer<X>() {
LiveData<Y> mSource;
@Override
public void onChanged(@Nullable X x) {
LiveData<Y> newLiveData = func.apply(x);
if (mSource == newLiveData) {
return;
}
if (mSource != null) {
result.removeSource(mSource);
}
mSource = newLiveData;
if (mSource != null) {
result.addSource(mSource, new Observer<Y>() {
@Override
public void onChanged(@Nullable Y y) {
result.setValue(y);
}
});
}
}
});
return result;
}
}</code></pre>
<p>源码比较简单,不再详细讲解。</p>
<p>它里面其实主要用的就是<code>MediatorLiveData</code>,通过该类我们能组合多个 LiveData 源。当任何一个 LiveData 源发生改变的时候,<code>MediatorLiveData</code>的 Observers 都会被触发,这点比较实用。比如我们有两个 LiveData,一个是从数据库获取,一个是从网络获取。通过<code>MediatorLiveData</code>就能做到,当二者任何一个获取到最新数据,就去触发我们的监听。</p>
<p>顺便也贴下<code>MediatorLiveData</code>的源码,它继承自<code>MutableLiveData</code>:</p>
<pre><code>public class MediatorLiveData<T> extends MutableLiveData<T> {
private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}
@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote) {
Source<?> source = mSources.remove(toRemote);
if (source != null) {
source.unplug();
}
}
@CallSuper
@Override
protected void onActive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().plug();
}
}
@CallSuper
@Override
protected void onInactive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().unplug();
}
}
private static class Source<V> implements Observer<V> {
final LiveData<V> mLiveData;
final Observer<V> mObserver;
int mVersion = START_VERSION;
Source(LiveData<V> liveData, final Observer<V> observer) {
mLiveData = liveData;
mObserver = observer;
}
void plug() {
mLiveData.observeForever(this);
}
void unplug() {
mLiveData.removeObserver(this);
}
@Override
public void onChanged(@Nullable V v) {
if (mVersion != mLiveData.getVersion()) {
mVersion = mLiveData.getVersion();
mObserver.onChanged(v);
}
}
}
}</code></pre>
<p>这里顺便提一句,如果想在数据更新的时候让 Observer立即得到通知,也就是说忽略生命周期状态,这时候我们可以使用 LiveData 的<code>observeForever(Observer<T> observer)</code>方法。</p>
<p>LiveData 往往是需要结合 ViewModel才能发挥出更大的威力。下面就接着介绍 ViewModel 的知识,以及二者的搭配使用。</p>
<h2>什么是 ViewModel</h2>
<p>简单来讲,ViewModel 是一种用来存储和管理UI相关数据的类。但不同的是,它支持在系统配置发生改变的时候自动对数据进行保存。当然,这要配合 LiveData。</p>
<p>我们知道,在屏幕旋转的时候,会导致Activity/Fragment重绘,会导致我们之前的数据丢失。就比如,如果我们使用EditText,在里面输入了内容,但是屏幕旋转的时候,会发现其中的text内容被清空了。如果你发现没清空,可能使用的是 support 包下的控件,或者 Activity 继承自 AppCompatActivity,并且给该控件添加了 id。系统对一些简单的数据进行了恢复(其实是在EditText的父类TextView进行的恢复)。</p>
<p>对于一些简单的数据,我们可以通过在Activity的<code> onSaveInstanceState()</code>方法中存储,然后在<code>onCreate()</code>中进行恢复,但是这种方式只适合存储少量的数据,并且是能被序列化和反序列化的数据。而对那些大量的数据则不适用,比如一个 User 或者 Bitmap 的 List。</p>
<p>此外,它也使得 View 的数据持有者和 UI controller 逻辑更加分离,便于解耦和测试。</p>
<h2>LiveData 结合 ViewModel 使用</h2>
<p>之前我们是单独使用 LiveData,这里配合ViewModel使用:</p>
<pre><code>public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<List<User>>();
loadUsers();
}
return users;
}
private void loadUsers() {
// Do an asynchronous operation to fetch users.
}
}</code></pre>
<p>可以看到,这里我们创建一个类,继承自ViewModel,然后在里面存储我们需要的<code>MutableLiveData</code>字段。注意,getUsers()方法返回的类型是LiveData而非 MutableLiveData,因为我们一般不希望在ViewModel 外面对数据进行修改,所以返回的是一个不可变的 LiveData 引用。如果想对数据进行更改,我们可以暴露出来一个<code>setter</code>方法。</p>
<p>接下来可以按照如下的方式获取 ViewModel:</p>
<pre><code>public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {
// update UI
});
}
}</code></pre>
<p>我们在onCreate()方法中通过<code>ViewModelProviders.of(this).get(MyViewModel.class);</code>这行代码来获取一个<code>MyViewModel</code>实例。之后又通过该实例暴露出来的getter方法获取LiveData 实例。这里要注意,当Activity重建的时候,虽然 onCreate() 方法会重新走一遍,但是这个<code>MyViewModel</code>实例,仍然是第一次创建的那个实例,在<code>ViewModelProviders.of(this).get(***.class)</code>中的<code>get</code>方法中进行了缓存。之后进行源码解析的时候会详细讲解。先看下下面的一张图,了解下 ViewModel 的整个生命周期:</p>
<p><img src="/img/remote/1460000018863555" alt="viewmodel-lifecycle" title="viewmodel-lifecycle"></p>
<p>ViewModel 最终消亡是在 Activity 被销毁的时候,会执行它的<code>onCleared()</code>进行数据的清理。</p>
<h2>Fragment 间进行数据共享</h2>
<p>Fragment 间共享数据比较常见。一种典型的例子是屏幕左侧是一个 Fragment,其中存储了一个新闻标题列表,我们点击一个 item,在右侧的 Fragment 中显示该新闻的详细内容。这种场景在美团等订餐软件中也很常见。</p>
<p>通过 ViewModel 将使得数据在各 Fragment 之间的共享变得更加简单。</p>
<p>我们需要做的仅仅是在各 Fragment 的 onCreate() 方法中通过:</p>
<pre><code>ViewModelProviders.of(getActivity()).get(***ViewModel.class);</code></pre>
<p>来获取 ViewModel ,<strong>注意</strong>,<code>of(...)</code>方法中传入的是二者所在的<code>activity</code>。具体可以参考如下官方代码:</p>
<pre><code>public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 传入 activity
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 传入 activity
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// Update the UI.
});
}
}</code></pre>
<p>Android 3.0 中引入了 Loader 机制,让开发者能轻松在 Activity 和 Fragment 中异步加载数据。但事实上用的人并不多。现在,它几乎可以退出历史舞台了。ViewModel配合Room数据库以及LiveData,完全可以替代Loader,在SDK28里,也越来越多的用Loader也越来越多的被替代。</p>
<p>但要注意,ViewModel能用来替换Loader,但是它却并不是设计用来替换<code>onSaveInstanceState(...)</code>的。关于数据持久化以及恢复UI状态等,可以参考下Medium上的这篇文章,讲的简直不能再好了:<a href="https://link.segmentfault.com/?enc=9xycGoe7c0D99zRLLOU%2BFQ%3D%3D.WgVCHvtBV5mLDlvbRvxzZFJ8SIRQUwUWiDHTI4%2BXlVz2jUTykAyDSrT0735HIIKPFjMyht7UdA595zEJQgtOpLcCdFSw45c5xBklzJq7WEbjZ2JRM8PEhZDLHsZ8SVv9l42OWp2LPVgvkyhb%2F5CrenDMKL8dDNd5udAqcVV65PI%3D" rel="nofollow">ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders</a></p>
<h2>总结</h2>
<p>通常 LiveData 是需要配合 ViewModel 使用的。ViewModel 负责在系统配置更改时保存和恢复 LiveData,而 LiveData 则负责在生命周期状态发生改变的时候,对数据的变化进行监听。</p>
<p>写到这里算是把 LiveData 和 ViewModel 的使用讲完了。这里我在开篇故意单独把 LiveData 和 ViewModel 分开讲解,相比较官网更加容易理解。但如果想对二者进行详细了解,还是建议把官方文档认真的多阅读几遍。</p>
<p>欢迎关注公众号来获取最新消息。</p>
<p><img src="/img/remote/1460000018337094?w=430&h=430" alt="" title=""></p>
生命周期组件 Lifecycle 源码解析(二)
https://segmentfault.com/a/1190000018863482
2019-04-14T17:08:58+08:00
2019-04-14T17:08:58+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>上篇文章中我们以继承自 AppCompactActivity 这种情况来分析 Lifecycle 的源码。本篇,我们将一起来分析下继承自普通 Activity 这种情况下的源码分析。</p>
<h2>support library 27.0.0与 28.0.0 下使用的区别</h2>
<p>之前说道,我们如果不继承自 AppCompactActivity ,就只能自己手动在各 Activity 的生命周期方法中调用 markState(...) 函数来进行生命周期事件的分发。类似下图:</p>
<p><img src="/img/remote/1460000018863485?w=3264&h=2980" alt="28.0.0下继承普通Activity" title="28.0.0下继承普通Activity"></p>
<p>但是如果你是用的是 27.0.0 版本的 support library ,你会发现你这样实现的话是没有效果的。</p>
<p>你还需要自己再手动调用 LifecycleRegistry 类的<code>handleLifecycleEvent(...)</code>方法。</p>
<p><img src="/img/remote/1460000018863486" alt="27.0.0下继承自普通Activity" title="27.0.0下继承自普通Activity"></p>
<p>因为 support library 27.0.0 下引入的 Lifecycle 的 <code>common</code> 库的版本是 1.0.0(引入的 runtime 库会自动引入 common 等库),而 28.0.0版本引入的则是 1.1.1,二者的实现是有差别的,可以看下下面的源码。</p>
<p>先来看<code>1.1.1</code>版本下实现:</p>
<p><img src="/img/remote/1460000018863487?w=2960&h=2548" alt="" title=""></p>
<p>再来看下<code>1.0.0</code>版本下实现:</p>
<p><img src="/img/remote/1460000018863488" alt="" title=""></p>
<p>可以看出,在 1.0.0 版本中,<code>markState()</code>方法,仅仅是对 State 进行了赋值,而没有对事件进行分发,而在 1.1.1 版本中则是在标记 State 的时候,同时进行事件的分发。这就不用我们再像之前那样写那一行繁琐的代码,还要去根据生命周期方法来判断传进去什么 Event 作为参数。</p>
<p>那我们本篇所讲的继承自普通 Activity 情况下的源码解析,就是这个?当然不是。如果是这个,那就没有必要再讲了,因为这些在上篇中已经讲过了。继续往下看。</p>
<h2>引入 extensions 库之后的源码解析</h2>
<p>虽然通过重写 Activity 生命周期,并通过在各方法中仅添加一行<code>mLifecycleRegistry.markState()</code>代码就能实现生命周期的感知。但是,作为推动社会发展的“懒人” -- 程序员,自然想通过更简单的方式来解放右手。办法总比困难多。</p>
<p>从上一篇文章我们知道,通过继承 AppCompactActivity 这种实现方式中核心就是向 Activity 中注入一个空的 Fragment--ReportFragment。我们能不能也通过这种方式,动态的向 Activity 中注入这个 ReportFragment 呢?</p>
<p>我当时是这么想的。之前阅读<code>8.1</code>的系统源码的时候,了解到能通过 <code>Application</code> 的<code>registerActivityLifecycleCallbacks()</code> 方法监听 Activity 的生命周期。说明这条路是可行的。</p>
<p>当然,我们没必要自己进行实现,因为 Google 已经帮我们实现了。Google 为我们提供了一个<code>extensions</code>库,我们需要单独引入:</p>
<pre><code>implementation "android.arch.lifecycle:extensions:$lifecycle_version"</code></pre>
<p>该库同时也会自动引入 LiveData 和 ViewModel 相关库,关于这二者,我们之后的文章中会另行讲解。</p>
<p>引入该库之后,我们的使用方式,就跟继承自 AppCompactActivity 基本相同,唯一的不同点就是我们需要自己实现 LifecycleOwner :</p>
<p><img src="/img/remote/1460000018863489" alt="普通Activity添加extension库" title="普通Activity添加extension库"></p>
<p>引入该库之后,我们<code>Command/Ctrl+鼠标左键</code>,点击ReportFragment,会发现使用到它的有两个类:<code>LifecycleDispatcher.java</code> 和 <code>ProcessLifecycleOwner.java</code>这两个类,而这二者,就是<code>android.arch.lifecycle:extensions:1.1.0</code>这个库下的类:</p>
<p><img src="/img/remote/1460000018863490" alt="" title=""></p>
<p>那我们就先追踪<code>ReportFragment.injectIfNeededIn(activity);</code>在<code>LifecycleDispatcher.java</code>类中的调用:</p>
<p><img src="/img/remote/1460000018863491" alt="LifecycleDispatcher" title="LifecycleDispatcher"></p>
<p><code>ReportFragment.injectIfNeededIn(activity);</code> 这行代码是在 LifecycleDispatcher 的静态内部类<code>DispatcherActivityCallback</code>的 onActivityCreated(...) 方法中调用的。而<code>DispatcherActivityCallback</code>又继承自<code>EmptyActivityLifecycleCallbacks</code>,<code>EmptyActivityLifecycleCallbacks</code>是啥?它其实就是<code>Application.ActivityLifecycleCallbacks</code>接口的空实现类。</p>
<p><img src="/img/remote/1460000018863492" alt="EmptyActivityLifecycleCallbacks" title="EmptyActivityLifecycleCallbacks"></p>
<p>看到这就对上了,原来 Google 采用的就是我们前面提到的方式,通过<code>Application.ActivityLifecycleCallbacks</code>进行监听。</p>
<p>继续回到上面的 <code>LifecycleDispatcher</code> 的源码查看,发现静态内部类 <code>DispatcherActivityCallback</code> 的实例化是在<code>LifecycleDispatcher</code>类的<code>static</code>方法<code>init()</code>中,在该方法中进行监听器的注册:</p>
<p><img src="/img/remote/1460000018863493?w=3500&h=1252" alt="LifecycleDispatcher的init" title="LifecycleDispatcher的init"></p>
<p>这里面,就真正的看到了通过<code>Application</code>的<code>registerActivityLifecycleCallbacks</code>来注册监听器。</p>
<p>继续追踪<code> LifecycleDispatcher#init(...)</code>方法,就进入了<code>ProcessLifecycleOwnerInitializer</code>类的<code>onCreate()</code>方法:</p>
<p><img src="/img/remote/1460000018863494" alt="ProcessLifecycleOwnerInitializer" title="ProcessLifecycleOwnerInitializer"></p>
<p>在其 onCreate() 方法中,进行了LifecycleDispatcher 的初始化,并且也进行了ProcessLifecycleOwner 的初始化。关于ProcessLifecycleOwner ,这里我们简单点下,它也实现了 LifecycleOwner 接口,主要用来监听应用的前后台切换。</p>
<p>回过来继续看<code>ProcessLifecycleOwnerInitializer</code>,它继承自 ContentProvider ,也就是说,它是个 ContentProvider ,但通过看源码,发现它对 ContentProvider 的各种方法都进行了空实现。其实,这里就是利用了 ContentProvider 的隐式加载。它的 onCreate() 方法执行时机是在Application 的 onCreate()方法之前。这样它就能通过 Application 来监听 Activity 的创建,并判断是否已经添加过了一个空UI的 ReportFragment。若没有,就进行添加。这种设计真的是太妙了。</p>
<p>我们知道,四大组件都是要在 AndroidManifest.xml 文件中进行生命的。那这个 ContentProvider 类型的 ProcessLifecycleOwnerInitializer 又是在什么时候声明的呢?</p>
<p>我们找到<code>extensions</code>库,通过如下方式查看它的下载位置:<br><img src="/img/remote/1460000018863495" alt="" title=""></p>
<p><img src="/img/remote/1460000018863496" alt="" title=""></p>
<p>然后打开这里的<code>AndroidManifest.xml</code>,会发现,在这里进行了声明。</p>
<p><img src="/img/remote/1460000018863497" alt="ProcessLifecycleOwnerInitializer的AndroidManifest" title="ProcessLifecycleOwnerInitializer的AndroidManifest"></p>
<p>这里的<code>AndroidManifest.xml</code>最终会合并入我们<code>app module</code> 的<code>AndroidManifest.xml</code>文件中。</p>
<p>至此,我们就对 Lifecycle 的源码进行了完全的解析。包括继承自普通 Activity 和继承自AppCompactActivity这两种情况。</p>
<h2>补充</h2>
<p>这里,再进行一点补充。如果我们使用的是 Java8,我们可以通过依赖下面这个库,来避免使用<code>@OnLifecycleEvent(...)</code>注解的方式进行生命周期回调方法的声明:</p>
<pre><code>implementation "android.arch.lifecycle:common-java8:1.1.1"</code></pre>
<p>这个库其实就一个 <code>DefaultLifecycleObserver.java</code> 接口类。之后我们需要 LifecycleObserver 的时候,一般实现 DefaultLifecycleObserver 接口即可(不用再去直接实现 LifecycleObserver 接口),使用方式变成了下面这样:</p>
<p><img src="/img/remote/1460000018863498" alt="MainActivity的DefaultLifecycleObserver" title="MainActivity的DefaultLifecycleObserver"></p>
<p>可以看到,这里我们直接在覆写的生命周期对应回调方法中写入我们的逻辑代码即可。更加简洁。</p>
<p>后续文章会继续讲 LiveData 及 ViewModel 等Architecture Components,欢迎关注公众号类获取最新消息。</p>
<p><img src="/img/remote/1460000018337094?w=430&h=430" alt="" title=""></p>
生命周期组件 Lifecycle 源码解析(一)
https://segmentfault.com/a/1190000018392945
2019-03-05T14:35:02+08:00
2019-03-05T14:35:02+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>在上篇文章:<a href="https://segmentfault.com/a/1190000018337087">Android 生命周期组件 Lifecycle 使用详解</a> 中,我们讲了 Lifecycle 的简单使用,本篇我们来研究下它的源码。</p>
<h2>基础环境搭建</h2>
<p>首先,按照上篇文章所讲,快速搭建环境。</p>
<p>添加 Lifecycle 轻量级依赖库:</p>
<pre><code> implementation "android.arch.lifecycle:runtime:1.1.1"</code></pre>
<p>添加<code>support library 28.0.0</code>的支持库(希望大家能先保持一致,因为不同版本的源码是有区别的,后面会将到):</p>
<pre><code>implementation 'com.android.support:appcompat-v7:28.0.0'</code></pre>
<p>再添加个注解处理器相关的依赖,至于用处,后面会讲:</p>
<pre><code>annotationProcessor "android.arch.lifecycle:compiler:1.1.1"</code></pre>
<p>接下来创建实现了 LifecycleObserver 接口的 MyObserver 类:</p>
<p><img src="/img/remote/1460000018392948?w=2656&h=1900" alt="MyObserver" title="MyObserver"></p>
<p>让我们的 Activity 继承自 AppCompatActivity,并在 onCreate() 方法中通过 <code>getLifecycle().addObserver(new MyObserver())</code>绑定 MyObserver :</p>
<p><img src="/img/remote/1460000018392949" alt="MainActivity" title="MainActivity"></p>
<p>核心代码就一句,` getLifecycle().addObserver(new MyObserver())<br>`,就能让我们创建的 MyObserver 类,拥有生命周期感知能力。我们知道,这里主要的对象就两个。一个是 getLifecycle() 方法返回来的 LifecycleRegistry 对象(继承自抽象类 Lifecycle),一个是我们创建的需要监听生命周期的类 MyObserver。那我们不禁要问:LifecycleRegistry 是如何感知到生命周期的?它又是如何把生命周期事件分发给 LifecycleObserver 的?</p>
<p>我们先来解决第一个问题,LifecycleRegistry 是如何感知到生命周期的。</p>
<h2>LifecycleRegistry 是如何感知到生命周期的</h2>
<p>首先,我们<code>Command/Ctrl + 鼠标左键</code>跟踪 getLifecycle() 代码,发现它的具体实现是在 AppCompatActivity 的祖先类 SupportActivity 中,该类实现了 LifecycleOwner 接口。</p>
<p><img src="/img/remote/1460000018392950" alt="SupportActivity" title="SupportActivity"></p>
<p>在 onSaveInstanceState() 方法中将 mLifecycleRegistry 的状态置为了 Lifecycle.State.CREATED,这点我们在前篇也讲到过。但从这我们还是看不到跟生命周期有关的东西。此时,我们发现在 onCreate() 方法中有这一行代码:</p>
<pre><code>ReportFragment.injectIfNeededIn(this);</code></pre>
<p>ReportFragment 是做什么的?点进去看:</p>
<p><img src="/img/remote/1460000018392951" alt="ReportFragment2" title="ReportFragment2"></p>
<p>可以看到, ReportFragment 的 <code>injectIfNeededIn(Activity activity)</code>方法向 Activity 中添加了一个未设置布局的 Fragment :<br><img src="/img/remote/1460000018392952" alt="injectIfNeededIn2" title="injectIfNeededIn2"></p>
<p>然后又在重写的生命周期事件中调用<code>dispatch(Lifecycle.Event event)</code>方法,来分发生命周期事件,这就是“生命周期感知能力”的来源。这种通过一个空的 Activity 或者 Fragment 来实现特定功能的技巧还是挺常见的,比如权限请求库 RxPermission ,以及 airbnb 开源的用于URL跳转的 DeepLinkDispatch(前者是使用空的 Fragment,后者使用的是空的 Activity)</p>
<p><code>ReportFragment#dispatch(Lifecycle.Event event) </code></p>
<p><img src="/img/remote/1460000018392953" alt="dispatch2" title="dispatch2"></p>
<p>这里面,又调用了 LifecycleRegistry 的<code>handleLifecycleEvent(event)</code>方法。至此,就引入了第二个问题,事件是如何分发到 LifecycleObserver 的。</p>
<h2>事件是如何分发到 LifecycleObserver 的</h2>
<p>进入 <code>LifecycleRegistry#handleLifecycleEvent(Lifecycle.Event event)</code>方法,发现它又调用了 <code>moveToState(State next)</code> 方法:</p>
<p><img src="/img/remote/1460000018392954?w=2960&h=2620" alt="" title=""></p>
<p>而在 <code>sync()</code> 方法中,根据 state 的状态,最终会调用到<code>backwardPass(...)</code>或者<code>forwardPass(...)</code>:</p>
<p><img src="/img/remote/1460000018392955?w=3736&h=2260" alt="" title=""></p>
<p>以 <code>forwardPass(...)</code> 为例:</p>
<p><img src="/img/remote/1460000018392956" alt="" title=""></p>
<p>上图可以看到,通过 mObserverMap 最终获取到一个 ObserverWithState 类型的 observer 对象,并调用它的<code>dispatchEvent</code>进行事件分发:</p>
<pre><code> observer.dispatchEvent(lifecycleOwner, upEvent(observer.mState));</code></pre>
<p><code>ObserverWithState</code> 又是个什么鬼?我们继续追踪,发现 ObserverWithState 是 LifecycleRegistry 的一个静态内部类。</p>
<p><img src="/img/remote/1460000018392957?w=3128&h=2332" alt="LifecycleRegistry2" title="LifecycleRegistry2"></p>
<p>从名称上就能看出,该类封装了 Observer 对象和 State 对象(具体就是 <code>State</code> 和 <code>GenericLifecycleObserver</code>,GenericLifecycleObserver 是个接口,继承自 LifecycleObserver),在其 dispatchEvent 方法中,最终会回调 mLifecycleObserver 的 <code>onStateChanged(...) </code>方法。</p>
<p>追踪到这里,我们知道了,Lifecycle在监听到生命周期变化之后,最终会回调 GenericLifecycleObserver 的 onStateChanged() 方法。我们不由得疑惑,我们定义的 MyObserver 哪去了?没看到有调用我们定义的回调方法啊。它和 GenericLifecycleObserver 又有什么关系?</p>
<p>我们看到,ObserverWithState 的构造函数里传进来了一个 LifecycleObserver 类型的 observer 对象,这个参数是从哪传进来的?继续追踪,发现追到了<code>LifecycleRegistry#addObserver(LifecycleObserver observer)</code>方法。<br>而这个方法,就是我们在<code>MainActivity#onCreate(...)</code>方法中调用的:</p>
<pre><code> getLifecycle().addObserver(new MyObserver());</code></pre>
<p>到这里,总算跟我们的 MyObserver 关联上了。查看<code>LifecycleRegistry#addObserver(LifecycleObserver observer)</code>方法源码:</p>
<p><img src="/img/remote/1460000018392958" alt="" title=""></p>
<p>这里面的核心代码就两行,一行是:</p>
<pre><code> ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);</code></pre>
<p>这行代码,通过传进来的<code>Observer</code>对象,创建出 ObserverWithState 对象。还有一行是:</p>
<pre><code> ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);</code></pre>
<p>这行代码是将 LifecycleObserver 对象放入一个FastSafeIterableMap 中,以便进行迭代。</p>
<p>接下来我们就进入 ObserverWithState 的构造方法中看看:</p>
<p><img src="/img/remote/1460000018392957?w=3128&h=2332" alt="" title=""></p>
<p>在构造方法中,通过 <code>Lifecycling.getCallback(observer)</code>根据传进来的 observer ,构造了一个 GenericLifecycleObserver 类型的 mLifecycleObserver ,那秘密应该也就在这个方法里,继续跟进。<br><img src="/img/remote/1460000018392959?w=3432&h=2764" alt="getCallback2" title="getCallback2"></p>
<p>这个方法的本质,其实就是根据传进来的一个LifecycleObserver 对象,构造出来一个 GenericLifecycleObserver 对象(目前有四个子类:<code>FullLifecycleObserverAdapter</code>、<code>SingleGeneratedAdapterObserver</code>、<code>CompositeGeneratedAdaptersObserver</code>、<code>ReflectiveGenericLifecycleObserver</code>),而最终构造出来的对象,就包含了我们创建的 LifecycleObserver 的所有信息,包括各种回调方法等。</p>
<p>看到这里,就要提到文章开头要大家添加的一个注解处理器的依赖:</p>
<pre><code>annotationProcessor "android.arch.lifecycle:compiler:1.1.1"</code></pre>
<p>当我们通过注解的方式来自定义LifecycleObserver 的时候,按照传统方式,必定要通过反射来对注解进行解析,这样就会对性能造成影响。一方面,我们通过缓存,来避免每次都通过反射获取构造器。另一方面,又通过注解处理器,在编译时对那些被<code>@OnLifecycleEvent</code>注解标注的普通方法,进行预处理,生成以“类名_LifecycleAdapter”命名的类,将各种回调方法直接进行逻辑转换,避免反射,进而来提高性能。</p>
<p>明白了这点,再看<code>Lifecycling.getCallback(observer)</code>方法就比较容易理解了。</p>
<ol>
<li>如果传进来的的参数 object 是 FullLifecycleObserver 类型,就把它构造成FullLifecycleObserverAdapter 对象,并返回</li>
<li>如果传进来的的参数 object 是GenericLifecycleObserver类型,直接返回该对象</li>
<li>如果1,2都不满足,就解析该类的的构造器的Type(该类是反射获取的,还是通过注解处理器生成的)。如果是通过注解处理器生成的类来调用回调函数,就返回一个SingleGeneratedAdapterObserver/CompositeGeneratedAdaptersObserver 对象</li>
<li>如果以上条件都不满足,就通过反射来调用各回调函数。返回一个 ReflectiveGenericLifecycleObserver 对象</li>
</ol>
<p>现在我们在 app 目录下的 <code>bulid.gradle</code> 中添加上上面的注解处理器依赖,然后编译下项目,会发现在build目录下生成了对应的类:<code>MyObserver_LifecycleAdapter.java</code></p>
<p><img src="/img/remote/1460000018392960?w=422&h=388" alt="" title=""></p>
<p>点进去,看看生成的这个类的源码:</p>
<p><img src="/img/remote/1460000018392961?w=3468&h=3052" alt="" title=""><br>可以看到,我们在 MyObserver 中通过<code>@OnLifecycleEvent</code>注解标注的那些方法,在这里都根据条件进行判断了,而非通过注解。</p>
<p>这时候我们就能理清这个这个流程了,当添加了注解处理器之后,我们这里的<code>Lifecycling.getCallback(observer)</code>方法将会把我们的<code>MyObserver</code>对象构建成一个 <code>SingleGeneratedAdapterObserver</code>对象返回(因为这里只有一个构造器),之后的 <code>mLifecycleObserver.onStateChanged(owner, event);</code>其实调用的就是<code>SingleGeneratedAdapterObserver</code>的<code>onStateChanged(owner, event)</code>方法:</p>
<p><img src="/img/remote/1460000018392962?w=3332&h=1828" alt="SingleGeneratedAdapterObserver" title="SingleGeneratedAdapterObserver"></p>
<p>这里面就可以看到,它调用了内部包裹的类的<code>callMethods(...)</code>方法,也就是我们上面提到的<code>MyObserver_LifecycleAdapter</code>的<code>callMethonds(...)</code>方法。</p>
<p>到这里,就完成了 Lifecycle 源码的解析。</p>
<h2>通过反射获取注解信息</h2>
<p>这顺便提下通过注解的方式调用各回调方法的过程。主要相关类就是 <code>ReflectiveGenericLifecycleObserver.java</code><br><img src="/img/remote/1460000018392963" alt="ReflectiveGenericLifecycleObserver" title="ReflectiveGenericLifecycleObserver"></p>
<p>这里我们主要关注回调信息 <code>CallbackInfo</code> 的获取方式的代码: <code>mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());</code></p>
<p>因为反射的代价是比较大的,所以又通过 <code>ClassesInfoCache.java</code>这个单例类,为 ReflectiveGenericLifecycleObserver 类要调用的各种方法的相关信息进行了缓存。</p>
<p>点进去看下它的 <code>getInfo(...)</code> 方法内部,是如何获取方法信息的。</p>
<p><img src="/img/remote/1460000018392964?w=2488&h=1324" alt="getInfo" title="getInfo"></p>
<p>里面又调用了<code>createInfo()</code>方法:</p>
<p><img src="/img/remote/1460000018392965" alt="createInfo" title="createInfo"></p>
<p>这里,就能看到对注解进行处理的代码了。</p>
<p>到这,我们就算完成了继承自 AppCompactActivity 的情况下的源码解析,而继承自普通 Activity 这种情况下,原理是什么呢?</p>
<p>鉴于篇幅,将放在下篇文章。欢迎关注我的公众号获取。</p>
<p><img src="/img/remote/1460000018337094?w=430&h=430" alt="" title=""></p>
Android 生命周期组件 Lifecycle 使用详解
https://segmentfault.com/a/1190000018337087
2019-02-28T11:43:30+08:00
2019-02-28T11:43:30+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<h2>前言</h2>
<p>2018 年的 Google I/O 大会上,Google 发布了 Android Jetpack,并称其为下一代的 Android 组件,旨在帮助开发者加快应用开发速度。准确来讲,Jetpack 是一系列 Android 软件组件的集合,它包括基础组件、架构组件、行为组件、界面组件。其中的 Android Architecture Components 指的就是这里的 “架构组件”。</p>
<p>Android Architecture Components 是 Google 推荐的一个构建 APP 的应用架构,它包含了一些列架构相关组件。而本篇文章我们要介绍的 Lifecycle 就是其中的一个与生命周期相关的库,同时,Lifecycle 也跟 LiveData 和 ViewModel 两个库紧密联系,想要搞懂后两者,就必须先搞懂它。</p>
<p>具体各组件之间的关系,以及各自在 Jetpack 中的地位,可以参见下面两幅来源于官网的图片。</p>
<p><img src="/img/remote/1460000018337090?w=1283&h=1199" alt="" title=""></p>
<p><img src="/img/remote/1460000018337091?w=852&h=623" alt="" title=""></p>
<h2>Lifecycle 的作用</h2>
<p>Lifecycle 是具有生命周期感知能力的组件,也就是说,我们能在 Activity 或者 Fragment 的生命周期发生变化的时候得到通知。我们往往会在 Activity 的各种生命中周期方法里执行特定的方法,比如,进行广播的注册和解绑、Eventbus 的注册和解绑等:</p>
<pre><code>public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
@Override
protected void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
protected void onDestroy() {
EventBus.getDefault().unregister(this);
super.onDestroy();
}
}</code></pre>
<p>如果我们把很多这种需要跟生命周期相关的逻辑代码都直接放在 Activity 的生命周期方法中,Activity 将会变得难以维护。通过 Lifecycle,我们就能通过把这些逻辑抽离出来,进而避免这种问题。因为本质上我们需要的只是 Activity 或者 Fragment 的生命周期发生改变的时候能通知到我们,以便我们在对应生命周期中执行对应的方法。</p>
<h2>Lifecycle 的基本使用</h2>
<h3>2.0、 导入 Lifecycle 依赖</h3>
<p>Lifecycle 被包含在 support library 26.1.0 及之后的依赖包中,如果我们的项目依赖的支持库版本在 26.1.0及以上,那么不需要额外导入 Lifecycle 库,本篇例子中使用的支持库是 28.0.0 :</p>
<pre><code> implementation 'com.android.support:appcompat-v7:28.0.0'</code></pre>
<p>如果支持库版本小于 26.1.0 ,就需要单独导入 Lifecycle 库 :</p>
<pre><code> implementation "android.arch.lifecycle:runtime:1.1.1"</code></pre>
<p>当然,如果项目已经迁移到了 AndroidX,可以使用下面的方式引入 :</p>
<pre><code> implementation "androidx.lifecycle:lifecycle-runtime:2.0.0"</code></pre>
<p>还是建议大家尝试尽快把项目迁移为 AndroidX,因为很多更新,会最先在 AndroidX 中发布,逐渐摆脱传统的support包。比如这里要讲的 Lifecycle 在 AndroidX 中已经升级到了 2.x 版本,而支持库中还是 1.x 版本。</p>
<p>鉴于支持库一般都在 26.1.0 以上,并且尚有大部分用户未迁移到AndroidX,在本篇文章中,我们使用 <code>support library 28.0.0</code> 中默认包含的 Lifecycle 库。我们在项目的 app 目录下的 <code>build.gradle</code> 文件中添加以下依赖:</p>
<pre><code> implementation 'com.android.support:appcompat-v7:28.0.0'</code></pre>
<p>以 support library 版本在 26.1.0 及以上为前提,这里我们分两种情况来讲。一种是我们创建的Activity 继承自 AppCompatActivity(以Activity 为例,Fragment类似),另一种是创建的 Activity 继承自普通的 Activity,而非 AppCompatActivity。</p>
<p>这里要先说一点, Lifecycle 的实现机制是观察者模式,意识到这点,再讲它的使用过程及原理就比较容易理解了。 </p>
<p><strong>整体流程:</strong></p>
<ol>
<li>构建一个 Lifecycle 对象(通过一个实现了 LifecycleOwner 接口的对象的 <code>getLifecycle()</code>方法返回),这个对象就是一个被观察者,具有生命周期感知能力</li>
<li>构建一个 LifecycleObserver 对象,它对指定的 Lifecycle 对象进行监听</li>
<li>通过将 Lifecycle 对象的 addObserver(...) 方法,将 Lifecycle 对象和 LifecycleObserver 对象进行绑定</li>
</ol>
<h3>2.1、 方式一:继承自 AppCompatActivity</h3>
<p>首先,我们创建一个 MyObserver.java 类,让它实现 LifecycleObserver 接口( LifecycleObserver 接口是一个空接口,主要是给注解处理器使用),如下:</p>
<pre><code>public class MyObserver implements LifecycleObserver {
private static final String TAG = "MyObserver";
// 使用注解 @OnLifecycleEvent 来表明该方法需要监听指定的生命周期事件
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void connectListener() {
// ...
Log.d(TAG, "connectListener: -------- onResume" );
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void disconnectListener() {
// ...
Log.d(TAG, "disconnectListener: ------- onPause");
}
}</code></pre>
<p>可以看到,我们通过在方法上使用<code>@OnLifecycleEvent</code> 注解使得该方法具有了生命周期感知能力。括号里面的参数,表明需要监听的是什么生命周期事件。Lifecycle 主要就是通过 <code>Event</code> 和 <code>State</code> 这两个枚举类来跟踪所关联组件的生命周期状态。具体的 Event 和 State 之间的转换关系,可以参照下图:</p>
<p><img src="/img/remote/1460000018337092" alt="" title=""></p>
<p>接下来,让我们的 Activity 继承自 AppCompatActivity,然后在 onCreate(...) 方法中通过<code>getLifecycle().addObserver(new MyObserver())</code>完成 Lifecycle 和LifecycleObserver 的绑定。</p>
<pre><code>public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 就只需要这一行代码,简洁吧
getLifecycle().addObserver(new MyObserver());
}
}</code></pre>
<p>然后我们就可以运行下程序,跑起来之后按 Home 键或者按返回键进行操作。能看到,随着生命周期的变化,MyObserver() 中定义的方法在控制台中也被正确地打印了出来。</p>
<p><img src="/img/remote/1460000018337093?w=1822&h=158" alt="" title=""></p>
<p>是不是觉得特别简单。</p>
<p>但之所以毫不费力,是因为有人替你“负重前行”。在 <code>support library 26.1.0</code> 及以后的支持库中,AppCompatActivity 的祖先类 <code>SupportActivity</code>已经默认实现了 <code>LifecycleOwner</code> 接口,通过其 <code>getLifecycle()</code> 方法可以直接返回一个 <code>Lifecycle</code> 对象。之后我们就可以通过该对象的 addObserver(...) 方法将 Lifecycle 跟指定的 LifecycleObserver 进行绑定。</p>
<h3>2.2、 方式二:继承自普通的 Activity</h3>
<p>首先,我们仍然需要像上面的方式,来创建一个<code>MyObserver</code> 对象。</p>
<p>这次我们创建一个继承自普通的 Activity 的 Activity ,那自然无法直接使用 getLifecycle() 方法来获取 Lifecycle 。无法直接使用,那我们能否模仿 <code>AppCompatActivity</code>的实现,来自己创建 Lifecycle 对象呢?当然可以。这时候,我们就需要自己实现<code>LifecycleOwner</code>接口,并在具体的生命周期下通过 <code>LifecycleRegistry</code> 的 <code>markState(...)</code>方法来主动进行事件的分发。请看下面改造过的 <code>MainActivity.java</code> 代码 :</p>
<pre><code>public class MainActivity extends Activity implements LifecycleOwner {
private LifecycleRegistry mLifecycleRegistry;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLifecycleRegistry = new LifecycleRegistry(this);
getLifecycle().addObserver(new MyObserver());
mLifecycleRegistry.markState(Lifecycle.State.CREATED);
}
@Override
protected void onResume() {
super.onResume();
mLifecycleRegistry.markState(Lifecycle.State.RESUMED);
}
@Override
protected void onPause() {
super.onPause();
mLifecycleRegistry.markState(Lifecycle.State.STARTED);
}
@NonNull
@Override
public Lifecycle getLifecycle() {
return mLifecycleRegistry;
}
}</code></pre>
<p>然后运行代码,发现结果和上面的完全一样。</p>
<p>可以看到,<code>MainActivity</code>实现了<code>LifecycleOwner</code>接口(实现该接口的对象,即是 Lifecycle 的持有者),并在其 getLifecycle( ) 方法中返回了一个 <code>LifecycleRegistry</code>对象,而 LifecycleRegistry 是 Lifecycle 的实现类,能处理多个 Observer,我们自定义 LifecycleOwner的时候就可以直接使用它。其他使用方式,则完全相同。</p>
<p>为了让使用更加方便灵活,Lifecycle 还提供了查询当前组件所处的生命周期状态的方法:</p>
<pre><code>lifecycle.getCurrentState().isAtLeast(STARTED)</code></pre>
<h2>总结</h2>
<ol>
<li>实现了 LifecycleObserver 接口的类可以和实现了 LifecycleOwner 接口的类无缝工作,因为 LifecycleOwner 可以提供一个 Lifecycle 对象,而 LifecycleObserver 就正需要对这个 Lifecycle 对象进行监听呢。</li>
<li>LifecycleOwner 是从特定的类(比如 Activity 或者 Fragment 等)中抽象出来的Lifecycle 的持有者。</li>
<li>LifecycleRegistry 是 Lifecycle 的实现类,用于注册和反注册那些需要监听当前组件生命周期的 LifecycleObserver</li>
</ol>
<h2>注意</h2>
<p>从 1.0.0-rc1 版本的 Lifecycle 包开始,当 Activity 的 <code>onSaveInstanceState()</code> 方法调用结束之后,Lifecycle 将立刻被标记为 <code>CREATED</code> 和 <code>ON_STOP</code> ,而不是等 <code>onStop()</code> 方法调用结束。这点和 API level 26 或者更低版本上 Activity 的生命周期的调用顺序并不匹配,需要稍加注意。有具体需求的可以进一步查阅相关文档。</p>
<p>更多最新消息,欢迎关注我的公众号获取:</p>
<p><img src="/img/remote/1460000018337094?w=430&h=430" alt="" title=""></p>
论搜索方法,低效的你简直在浪费生命(三)(终结篇)
https://segmentfault.com/a/1190000018071435
2019-01-30T18:30:41+08:00
2019-01-30T18:30:41+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>本篇我们将学习逆向图片搜索和 Google 的高级搜索功能,同时,本篇也是该系列的完结篇。</p>
<h2>逆向图片搜索</h2>
<p>平时我们想要搜索图片,大都是在搜索主页切换到图片那个 Tab 项,然后输入图片的关键字,比如“ <code>美女</code> ”、“ <code>鸟</code> ”。何为逆向搜索呢?就是我们手里有一张图片(可以是本地的,也可以是已知的某个网络上的链接地址),反过来,想知道跟这张图片有关的信息。因为跟我们正常搜索图片的方式相反,所以称之为“ <code>图片逆向搜索</code> ”。之前去看完电影,在乘电梯回去下楼的时候有个阿姨掏出手机问我,有没有方法找到她屏保的那张图片的高清图,因为这张图片是她之前保存到手机上的,不太清晰。我就跟她讲回去试下在百度上搜“ <code>百度识图</code> ”或者在 Google 上搜索 “ <code>图片搜索</code> ”。还有次是朋友给我发了段会变色的鸟的视频,问我这是啥鸟,还真没法在他面前“无所不能”的装逼了。</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/bird.gif" alt="bird" title="bird"></p>
<p>于是我把视频截图,放到 Google 中搜索,得到了如下的答案:</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/search_bird2.gif" alt="search_bird2" title="search_bird2"></p>
<p>可以看到,GIF 图中我点击的那一项,那就是这鸟的名字,“ <strong>Anna</strong> ”,再多浏览几条,就能知道它是一种蜂鸟 (hummingbird) ,它的名字叫 “ <strong>安娜蜂鸟</strong> ”,被称为是 “ <strong>飞行的宝石</strong> ”</p>
<h2>高级搜索</h2>
<p>如果你觉得前面几篇文章,讲的挺实用的,但是你不想增加记忆负担,那有没有其他方法呢?当然有,你可以使用 Google 的高级搜索功能,高级搜索又分为两类,分别是<code>针对网站的高级搜索</code>和<code>针对图片的高级搜索</code>.</p>
<h3>针对网站的高级搜索</h3>
<p>我们可以直接访问网页 <a href="https://link.segmentfault.com/?enc=%2FBvD9KLfJ5LzNmlexU0xHw%3D%3D.wVbogGtx6mwyzHXW6JAw%2FstvlXPuj14iY%2FunkD1Kk5KEQ3PTEPUSERNYlwdhdhsJ" rel="nofollow">https://www.google.com/advanced_search</a> 进入高级搜索页,当然也可以在任意搜索结果页面,点击“设置 -> 高级搜索 ”</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/advanced_search.gif" alt="advanced_search" title="advanced_search"></p>
<p>这里我们可以看到,有各种选项可以填,填完后点击右下角“<code>高级搜索</code>”按钮即可。其实很多东西都是我们之前讲过的语法,只不过这里以一种更友好的方式展现出来了。其中你会注意到一栏有“字词出现的位置”,我们可以指定文字出现的位置,比如 标题、文本或者网址等,其实对应的语法就是“ <code>intitle:</code>”、“ <code>intext:</code> ”、“<code>inurl:</code>”等。如果想搜索标题中含有张三的结果,可以直接搜索“<code>intitle:张三</code>”,以此类推。</p>
<h3>针对图片的图片搜索</h3>
<p>直接打开网址 <a href="https://link.segmentfault.com/?enc=4DfkPTRE6HPOwTkvg6HSPA%3D%3D.ZCoe9g6m9kzJfv6%2BscTDuKkHKc7EbUOl3hsKaVqhjZOZVm7sN4M8Z4uLWm6dcnGQ" rel="nofollow">https://www.google.com/advanced_image_search</a> 进入高级图片搜索主页面</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/15485039385101.jpg" alt="" title=""></p>
<p>在这里我们可以根据图片的尺寸、宽高比以及文件格式进行搜索。其中我觉得比较重要的一点是“ <code>使用权限</code> ”这个条件,我们有时候需要在网上图片使用,或者用到个人网站,或者用到微信公众号,但很多图片都是有版权的,如果不加筛选的使用,很可能给自己惹上麻烦。这是后我们就可以搜索无版权图片来自由使用。</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/15485041034908.jpg" alt="" title=""></p>
<p>好了,写到这里,这个 Google 搜索系列的文章也就到尾声了,希望能帮助到大家,使大家能更畅快的抵达目的地。</p>
<p>想获取更多内容,可以关注我的公众号:</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/wechat_number_avator.jpg" alt="wechat_number_avator" title="wechat_number_avator"></p>
<p>或者直接加我的微信好友,一起交流,有趣的灵魂在等你:</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/wechat_my_avatar.jpeg" alt="wechat_my_avatar" title="wechat_my_avatar"></p>
论搜索方法,低效的你简直在浪费生命(二)
https://segmentfault.com/a/1190000018051469
2019-01-29T09:32:22+08:00
2019-01-29T09:32:22+08:00
warmcheng
https://segmentfault.com/u/warmcheng
2
<p>上篇文章中我们学习了 Google 的一些初级使用。这一篇文章我将带大家学习它的更多好玩儿的、进阶的用法。(除非特别说明,以下均不包含双信号,加粗部分即为输入搜索框中的内容),本篇开启多图流量预警。</p>
<h2>搜索小技巧</h2>
<h3>使用 “ $ ”,搜索特定价格</h3>
<p>比如我们搜索价格为400美元的相机 “<code>camera $400</code>”</p>
<p><img src="/img/remote/1460000018051472?w=1890&h=1904" alt="" title=""></p>
<h3>使用“ .. ”,搜索指定范围内的数字</h3>
<p>比如,搜索价格在 400~800 美元的相机 “<code>camera $400..$800</code>”(当然这类场景购物网站更擅长)</p>
<p><img src="/img/remote/1460000018051473?w=910&h=927" alt="" title=""></p>
<h3>使用“ filetype: ”,搜索指定类型的文件</h3>
<p>有时候我们需要在网上搜索指定类型的文件。如果你是现在刚好处于找工作时期,想搜下简历的模板下载下来,如果你直接搜“<code>简历</code>”,那么一定会有各种垃圾信息来干扰你。现在你可以直接搜索“<code>程序员简历 filetype:docx</code>”</p>
<p><img src="/img/remote/1460000018051474?w=896&h=893" alt="" title=""></p>
<p>可以看到,在每一项的开头都有个<code>【DOC】</code>标记,来告诉我们这是个doc文档,这时候你点击任何一项,就会直接把这个文档下载下来。是不是很方便。以此类推,你可以下载其他格式的文件的(通过文件后缀来搜索),比如 PDF、xlsx(Excle文件)等等</p>
<h3>使用“ * ”,搜索通配符或未知字词</h3>
<p>在一些情况下,我们并不一定需要很精确的搜索,可能只是一个有点模糊的概念或者框架,这时候我们就可以使用“*”通配符来进行搜索,比如我想知道中国的城市的各种排行,也就是“<code>城市之最</code>”,这时候我就可以搜索“<code>中国最 * 校园</code>”,注意,星号前后有空格</p>
<p><img src="/img/remote/1460000018051475?w=987&h=918" alt="" title=""></p>
<h3>使用“ site: ”,在指定网站上搜索</h3>
<p>有时候我们只关注某些网站上的内容,那就需要把其他网站上面的内容给过滤掉。如果你是一个程序员,只想搜索在CSDN上发表的跟 Glide 有关的文章,那就可以搜索“<code>Glide site:csdn.net</code>”</p>
<p><img src="/img/remote/1460000018051476?w=807&h=935" alt="" title=""></p>
<p>当然,如果你是个米粉,或许会搜索“<code>小米8 site:jd.com</code>”来获取在京东售卖的小米8的信息,而这并不会含有其他购物网站上的此商品信息。</p>
<p><img src="/img/remote/1460000018051477?w=818&h=932" alt="" title=""></p>
<h2>内置小工具</h2>
<p>这节我们聊点 Google 为我们提供的一些内置实用小工具,它能让我们的搜索变得有趣而高效。</p>
<h3>天气</h3>
<p>搜索框中直接搜索"<code>天气</code>",就会展示出来当前定位到的天气的温度走势图。当然你也可以手动指定要搜索的位置,比如“<code>天气 北京</code>”</p>
<p><img src="/img/remote/1460000018051478?w=789&h=861" alt="" title=""></p>
<p>我们可以在这里面查看温度、降水概率、风力等,稍微留意可以发现,这里打开的其实是“ <code>weather.com</code> ”网站,在搜索结果中直接进行了预览,以下各工具也类似</p>
<h3>计算器</h3>
<p>搜索框中直接搜索"<code>计算器"</code>,在搜索的出来的计算器页面就可以进行计算了。</p>
<p><img src="/img/remote/1460000018051479?w=816&h=732" alt="" title=""></p>
<p>当然,我们也可以直接在搜索框中输入一些简单的算式,比如“ <code>100+200</code> ”,然后 Google 就会直接在上面的计算器中给出计算之后的结果.</p>
<h3>颜色选择器</h3>
<p>有时候我们写代码,需要查看设计给的某些颜色是否标注正确或者自己去取色,那么就可以使用这个工具。在搜索栏里直接搜索“<code>颜色选择器</code>”</p>
<p><img src="/img/remote/1460000018051480?w=1251&h=647" alt="color_select" title="color_select"></p>
<p>我们可以拖动颜色滑块儿,也可以直接手动输入颜色值,来选择颜色</p>
<h3>货币兑换</h3>
<p>我们可以直接输入要兑换的货币,如“<code>美元兑换人民币</code>”,当然也可以直接输入上我们需要进行转化的数值,比如“<code> 10 美元兑换人民币 </code>”</p>
<p><img src="/img/remote/1460000018051481?w=1252&h=647" alt="money" title="money"></p>
<p>可以看到,这里我们可以选择我们要兑换的货币,上面也会显示最近的汇率变化情况</p>
<h3>翻译</h3>
<p>我们直接搜索“<code> 翻译 </code>”,就会出现下面的搜索框,我们能方便的进行各种语言的转换,而不需要再进行安装翻译软件,特别效率。</p>
<p><img src="/img/remote/1460000018051482?w=1780&h=1614" alt="" title=""></p>
<p>当然,我们也可以直接输入我们要翻译的语言,比如把“ <code>hello</code> ”这个单词翻译成汉语,我们就可以直接使用“ <code>hello in chinese </code> ”</p>
<p><img src="/img/remote/1460000018051483?w=1252&h=647" alt="hello" title="hello"></p>
<p>可以看到,当我们输入了 “<code>in</code>”这个单词之后,后面会自动对我们进行提示,我们选择要翻译的语言即可。</p>
<h3>根据数学公式绘制图形</h3>
<p>我们可以直接在浏览器中输入数学公式,直接查看该函数绘制出来的效果,挺厉害的,比如查看函数“<code>cos(3x)+sin(x)</code>”的图形</p>
<p><img src="/img/remote/1460000018051484?w=1252&h=647" alt="method" title="method"></p>
<p>当然,我们也可以在一个坐标中同时绘制多个函数,那么只需要用逗号把他们分割开就行,比如“<code>cos(3x)+sin(x), cos(7x)+sin(x),x</code>”</p>
<p><img src="/img/remote/1460000018051485?w=1788&h=1170" alt="" title=""></p>
<p>好啦,这次就讲到这里。这节相对就比较轻松啦。下一篇将是本系列文章的最后一篇,将会讲一点跟图片有关的搜索技巧,以及高级搜索的使用,不要走开。</p>
<p>也可以直接扫码关注我的公众号来关注最新消息:</p>
<p><img src="/img/remote/1460000018051486?w=430&h=430" alt="wechat_number_avator" title="wechat_number_avator"></p>
<p>或者直接加我的微信好友,一起交流,有趣的灵魂在等你:</p>
<p><img src="/img/remote/1460000018051487?w=512&h=512" alt="wechat_my_avatar" title="wechat_my_avatar"></p>
论搜索方法,低效的你简直在浪费生命(一)
https://segmentfault.com/a/1190000017953093
2019-01-19T21:21:29+08:00
2019-01-19T21:21:29+08:00
warmcheng
https://segmentfault.com/u/warmcheng
3
<p>现如今,我们无无时无刻不在互联网上进行着搜索行为,我觉得搜索是一个人最重要的技能。所谓搜索,“就是在正确的地方使用正确的工具和正确的方法寻找正确的内容”。学会搜索,能极大的让我们从互联网上庞大信息中快速定位到对我们有用的信息,提高效率和准确率。在实际生活中,我发现很多人并不懂得搜索,这也是我下决心写作本文的初衷。</p>
<p>这里我们使用最优秀的搜索引擎(没有之一)Google来学习搜索,其他搜索引擎也大同小异(比如那个说不怕“再赢”Google一次的**,咳咳...),PS:使用Google,目前来讲是需要科学上网。如果用不了,就先去铺路吧,这个话题就没法再发文说了。</p>
<p><img src="/img/bVbnuwc" alt="clipboard.png" title="clipboard.png"></p>
<p>我们很多人在搜索的时候,往往不对信息进行提炼,而是直接整句话进行搜索,这是比较低级、原始的方法,比如有的人就直接搜“从国贸到四惠东该怎么走”,我们可以通过在Google和百度上进行搜索,来感受下谷歌的威力。</p>
<p><img src="https://mmbiz.qpic.cn/mmbiz_png/Z3seruEnU8KnLZNQHx2xQ0icB3p2JHcibdNWNork7hFw3ialpbrIjkYcdOol6QwVBqoicWDY0tVl1EHydy0CibWl53g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="" title=""></p>
<p>可以看到,谷歌对自然语意做了很好的识别,准确的在第一条为我们展示出了地图预览,还有路线,而下面是百度的结果:</p>
<p><img src="/img/bVbnuw8" alt="图片描述" title="图片描述"></p>
<p>孰优孰劣立见分晓。到这里你可能要问,Google那么厉害,以后就直接整句搜索,为啥说这样搜很低级?</p>
<p>这次我们确实是搜到了很满意的效果,但这是因为它识别到了这是跟地理位置有关的,默认使用了内置的一些“工具”,后面我们还会具体介绍。</p>
<p>而普通的搜索,都是通过将句子拆分成一个个分词来进行搜索,会过滤掉那些常用的“的”字或者“*”等符号,然后对剩余词语进行关键词搜索。所以我们搜索的时候应该自己提取出要搜索的关键词,比如“国贸”、“四惠东”、“路线”,然后再进行搜索“国贸 四惠东 路线”(注意,不包含引号“”,在这个例子中,如果直接搜索这三个关键词,反而没有整句搜效果好,这个不必太较真)。</p>
<p>稍微初级一点的,知道用关键词进行搜索,他们采用的是空格来区分。接下来会讲解一些常用操作符。如果知道逻辑运算符的及比较容易理解,不过不知道也不影响,你也能很快学会。<strong>以下没有特殊说明的话,关键词外面的引号是不需要输入及搜索框的。而且Google是大小写不敏感的,QQ和qq搜索到的是同样的结果。</strong></p>
<h2>常用逻辑运算符</h2>
<h3>与运算,“ ”(空格)</h3>
<p>这是最常用的,它表示“与”的意思。当我们需要搜索结果包含两个或者两个以上的关键词的时候,我们可以直接使用空格来分割我们的关键字,比如“数码相机 选购”</p>
<h3>或运算,“ | ”</h3>
<p>“ | ”运算符表示逻辑“或”的意思,当然,你也可以用“OR”来搜索,二者完全等效,但是注意,这里的运算符都是大写的,小写的话,搜索引擎不会认为它是搜索运算符,下面其他运算符也是一样。格式为:“A|B”,“ | ”两边可以有空格也可以没有,Google会自动识别左右两个关键词。比如搜索尼康或者佳能的数码相机,“数码相机 尼康|佳能”<br><img src="/img/bVbnuyl" alt="图片描述" title="图片描述"></p>
<h3>非运算,“-”</h3>
<p>“-”运算符表示逻辑“非”操作,用来从搜索结果中排除指定信息。“A -B”表示搜索包含A内容,但是不包含B内容的网页。注意,这里“-”和“B”之间是没有空格的,如果用过命令行的可能比较容易理解,其实就是附加条件。比如“数码相机 -佳能”就表示搜索数码相机,但是不包括尼康这个品牌:</p>
<p><img src="/img/bVbnuyF" alt="图片描述" title="图片描述"><br><img src="/img/bVbnuyS" alt="图片描述" title="图片描述"></p>
<p>可以看出,与直接搜索“数码相机”相比,搜索“数码相机 -佳能”的结果中排除了所有跟佳能有关的信息。</p>
<h3>完全匹配,""</h3>
<p>前面说过,当我们使用Google搜索的时候,它会对我们输入的内容进行分词操作,这也是它能对我们的整句话搜索的关键所在。但有时候我们想要的是绝对搜索,这是后我们就可以使用""来把我们要搜索的关键词引起来,注意,这里是英文输入法下的双引号,而不是中文。比如直接搜索“最好看的电影”和搜索“"最好看的电影"”,搜索结果自己体会下:</p>
<p><img src="/img/bVbnuyY" alt="图片描述" title="图片描述"><br><img src="/img/bVbnuy2" alt="图片描述" title="图片描述"></p>
<p>这个对于汉语其实影响还不是很严重。如果你是对英文进行搜索,那就尬的不行了,因为英语里是用空格来分割两个单词的,像这样,"we are the world"。如果我们平时用英文搜索,特别是程序员,会这种搜索帮助还是比较大的,别上来就整个贴log。</p>
<h3>标在社交媒体上搜索,“@”符</h3>
<p>我们可以直接使用@符号在指定的社交媒体上进行搜索,比如我挺喜欢舒淇的,那我想在Instagram上搜索舒淇,应该怎么做呢?只需要搜索“舒淇 @Instagram”即可。</p>
<p><img src="/img/bVbnuy8" alt="图片描述" title="图片描述"></p>
<p>这里我们直接点开第一项,就可以进入她的Instagram主页了,她的网名是sqwhat,当然,如果知道网名的话搜索就更准确了</p>
<p><img src="/img/bVbnuzf" alt="图片描述" title="图片描述"></p>
<p>哈哈,停下来欣赏下女神照片吧。</p>
<p>当然,Twitter等等也是类似,只需要加上个@符号就行。其实类比下,我们经常用到的一些特殊符号也是有意义的,比如“#”,表示话题,我们就可以使用这个符号进行话题的搜索,比如“#metoo”,可以搜索下,这里就不再单独举例了。</p>
<p>O__O "…好吧,这次就先写到这,我要去女神圈子转转去啦...</p>
<p><img src="/img/bVbnuzu" alt="图片描述" title="图片描述"></p>
<h3>本篇结语:</h3>
<p><strong>PS:</strong> segmentfault排版比较费劲,想要更好的阅读体验,可以点击微信原文链接:<a href="https://link.segmentfault.com/?enc=fV5MsYr6L9a7pF70yt1s6g%3D%3D.rOpqbwZGPDbIa1kbYlzkH23kfeoua75gCUVvTsPBg1sJhC9Q0poI%2BAP5bkDeSHmXuQceUeL9OSBxfMgKcLLFH75rWhAk4krc5zMFYTBAaxwmx53evSIdUxF9duCo1bIsSrKtXV4EagFS7JXapOUoMc3r2o6yKzTqhifJY7nDYJmnjQaTkzRDMEU8deugFSzMxVoKvzn3D%2FcKaPzDhlR7f037ks0jQO%2FZIyMbvo94EwuFc35S9KdmHMsFXxYmO1PkQ1QZL5kerCc97KikcFAJ8JzuX7lA6Ts2HFtcDUowOUkcmuch9%2BdioTRRfnK7RMRc1DCdRgHpPQxbd8REUEskmQ%3D%3D" rel="nofollow">https://mp.weixin.qq.com/s?__...</a></p>
<p>也可以直接扫码关注我的公众号:</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/wechat_number_avator.jpg" alt="wechat_number_avator" title="wechat_number_avator"></p>
<p>或者直接加我的微信好友,一起交流,一起嗨:</p>
<p><img src="http://plmu4lksh.bkt.clouddn.com/wechat_my_avatar.jpeg" alt="wechat_my_avatar" title="wechat_my_avatar"></p>
<p>在接下来的文章里会分享更多关于Google使用的进阶技巧,欢迎关注哦,别走开,不然亏大发了,这才哪到哪。</p>
美团Walle多渠道打包+友盟统计+360加固
https://segmentfault.com/a/1190000015554496
2018-07-06T18:03:00+08:00
2018-07-06T18:03:00+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<h2>一、集成友盟统计</h2>
<p>在实际项目开发过程中,由于运营的需要,我们往往需要知道我们的APP在各大应用市场的下载和具体使用情况,这时候我们往往需要接入第三方统计,较常用的就是友盟统计。具体接入方式可以查看友盟统计的官方接入文档:</p>
<ol>
<li>基础组建集成:<a href="https://link.segmentfault.com/?enc=4d3x37szCVuv8bD6Jm8U%2Bw%3D%3D.MBoXepKuUbh3ReulaNxcXwqJTAok5tQpZdBq%2B%2Be1E2n110lHVH81oJukiB8uVrnr85k%2B%2BL3CRpDz4h6JZmbHXQ%3D%3D" rel="nofollow">https://developer.umeng.com/docs/66632/detail/66890</a>
</li>
<li>U-App统计集成:<a href="https://link.segmentfault.com/?enc=N5yFU%2BOx6Zz1xVGANDBZSg%3D%3D.dKxV5lTpNgPbIu4JiGfFjQhN1rGK3uwP3VrFDvRWYUfxhbpBhf7s4fNQV8%2Fb7%2FSfD7scDJCCYV48WObHfGwImQ%3D%3D" rel="nofollow">https://developer.umeng.com/docs/66632/detail/66889</a>
</li>
</ol>
<p>仔细观察文档可以发现,在旧版本中,我们是通过在<code>AndroidManifest.xml</code>文件中配置</p>
<pre><code><meta-data android:value="YOUR_APP_KEY" android:name="UMENG_APPKEY"/>
<meta-data android:value="Channel ID" android:name="UMENG_CHANNEL"/></code></pre>
<p>来实现渠道号的设置的,然后每次打新渠道包的时候,只需要手动修改这个<code>Channel ID</code>即可,当然,为了减少这种手动输入,我们可以通过<code>productFlavor</code>来实现多渠道打包,但这本质上还是每次都重新打一个包,只是解放了我们的双手,但并没有解决打包效率低、耗时的问题。其实我们需要的就是把我们要统计的App的渠道ID传给友盟,来让友盟进行渠道的统计,对此,友盟提供了新的接入方式:</p>
<pre><code>UMConfigure.init(Context context, String appkey, String channel, int deviceType, String pushSecret)</code></pre>
<p>这时候我们可以将之前<code>AndroidManifest.xml</code>中的<code>channelId</code>配置相关的<code><meta-data></code>删除,只需要提供channel即可,然后就可以通过美团Walle或者其他方式来获取到这个channel即可。</p>
<h2>二、接入美团多渠道打包方案Walle</h2>
<p>传统打包方案除了通过productFlavor方式外,还要通过apktools方式逆向来实现多渠道,这种方式原理也很简单,就是讲apk解压,然后通过修改AndroidManifest.xml中的渠道值,再重新进行打包和签名。这种方式我们每次打包时不再需要重新构建项目。只需先打包生成一个apk,然后在该apk的基础上生成其他渠道包即可。这种方式虽然比之前快了很多,但还能更快。</p>
<p>美团第一代多渠道打包方式提供了在<code>META-INF</code>目录内添加空文件这种方案,具体可以参考:<a href="https://link.segmentfault.com/?enc=ieJDVIonVX6UvdnsFt1YEw%3D%3D.c8DbPh1GqfA%2F8x1pOU%2BWX6oQ%2BbVSPajw8IsbZysv5eZSDxoijL8qKtNBl06iKL0t" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=UcWnEhi%2FSiqSFWI%2FNU%2F%2B7w%3D%3D.8wRnYWLao3VpopGdNeSfbS%2F5K6quCmhrVLrlUNjRfGdld5fNiK7DwXdNtbfTYzpU" rel="nofollow">https://tech.meituan.com/mt-a...</a>,这种方案快在,我们能直接修改apk的渠道号,而不需要再重新签名,这样能节省不少打包的时间。据美团将,这种打包方式速度非常快,900多个渠道不到一分钟就能打完。</p>
<p>但是好景不长,在Android 7.0(Nougat)推出了新的应用签名方案<code>APK Signature Scheme v2</code>后,之前快速生成渠道包的方式(美团Android自动化之旅—生成渠道包)已经行不通了(当然,我们可以暂时通过设置<code>v2SigningEnabled false</code>来继续通过之前的方式打包,但最好还是应该接受新的改变),因为之前的签名方案并不会校验<code>META-INF</code>目录,因而我们对该目录的修改并不需要重新签名。而新的方案下,我们对apk中包含的文件的任何修改,都会引起校验失败。这时美团推出了第二代多渠道打包方案Walle。</p>
<p>第二代多渠道打包方案Walle的思路其实就是从Zip文件本身入手,因为apk本质上还是一个Zip文件,美团通过在ZIP文件格式的 <code>Central Directory</code> 区块所在文件位置的前面添加一个<code>APK Signing Block</code>区块,然后把渠道信息写到这个区块中,之后再读出这个信息,就完成了。当然,实际细节比较复杂。具体可以参考<a href="https://link.segmentfault.com/?enc=G1ihct9qPDjPGv%2BS42MhIw%3D%3D.wmCYqdSVlhkAs%2FxSIWeOrGwiJeyk2Mji%2BgccDBZDeFVquBzerPl%2FYnOYe2fCVrrNhrp3fmp3f2AcBvswLyGryg%3D%3D" rel="nofollow">新一代开源Android渠道包生成工具Walle</a>,据说这种打包方式速度非常快,对一个30M大小的APK包只需要100多毫秒(包含文件复制时间)就能生成一个渠道包,而在运行时获取渠道信息只需要大约几毫秒的时间。</p>
<p>在接入Walle之前,我们需要创建好签名文件,并且在项目的build.gralde中进行配置,类似这种:</p>
<pre><code> signingConfigs {
myConfig {
storeFile file("../Walle.jks")
storePassword "abc12345"
keyAlias "Walle"
keyPassword "abc12345"
}
}
buildTypes {
debug {
signingConfig signingConfigs.myConfig
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
signingConfig signingConfigs.myConfig
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}</code></pre>
<p>具体关于如何创建签名文件,可以参考:<br><a href="https://segmentfault.com/a/1190000007579841">Android Studio下应用签名的方法以及获取 MD5、SHA1(签名)、SHA256 值</a>,关于keystone签名文件更详细的知识,可以参考:<a href="https://link.segmentfault.com/?enc=nWYmrVdaMjacjH%2FnQgZ6ug%3D%3D.WUxw%2BiFAbTasUaVWDTEBIkWSKD79T8wo2kjAvgkYtXRr2%2BKUXMDfNxKTqu54qdUM" rel="nofollow">Android Keystore漫谈</a><br>美团<code>Walle</code>具体接入方法可以参考:<a href="https://link.segmentfault.com/?enc=kICn8UME2f5ZdrQn9emeDw%3D%3D.drbg2s1876iD8lBTJXMrcK7jX9bYpUoAAYIcG%2BdBGAJIU4CDSKTigFqWnWqmSL%2BH" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=BQIe1GveCZNfh1lXWR8Xgg%3D%3D.DoHmnxyUyfJ%2BCAtIbZSDxxEtZ7jrmwa2V8QzZCQ246KdMjNwvL829vBrs8IYBuwH" rel="nofollow">https://github.com/Meituan-Di...</a><br>这里其他我们都按照默认即可,只需要根据自己的需要来编写<code>channel</code>文件即可,注意,这个文件不能有任何后缀。<br><img src="/img/bVbdpVB?w=2880&h=1800" alt="channel%E6%96%87%E4%BB%B6.png" title="channel%E6%96%87%E4%BB%B6.png"></p>
<p>集成<code>Walle</code>后我们就可以通过</p>
<pre><code>String channel = WalleChannelReader.getChannel(this.getApplicationContext());</code></pre>
<p>来获取channel,然后把该channel传给友盟进行渠道的统计:</p>
<pre><code>UMConfigure.init(Context context, String appkey, String channel, int deviceType, String pushSecret);</code></pre>
<p>然后我们需要进行多渠道打包的时候,只需要执行<code>./gradlew clean assembleReleaseChannels</code>即可来生成所有的渠道包<br><img src="/img/bVbdpWD?w=1210&h=1464" alt="%E6%B8%A0%E9%81%93%E5%8C%85.png" title="%E6%B8%A0%E9%81%93%E5%8C%85.png"><br>有时候执行该命令,我们可能会遇到R.java文件丢失的问题,但是通过AS的<code>clean</code>又能重新生成,这时候我们可以把上述命令中的clean去掉,变为通过命令<code>./gradlew assembleReleaseChannels</code>生成<br>所有渠道包。</p>
<p>这样,我们就完成了渠道包的生成,如果想验证是否能在代码中成功获取到渠道信息,我们可以在友盟的管理后台查看,也可以直接通过<code>Log</code>在代码中输出到<code>Logcat</code>中<br><img src="/img/bVbdpY0?w=2320&h=534" alt="%E5%8F%8B%E7%9B%9F%E5%90%8E%E5%8F%B0%E6%B8%A0%E9%81%93%E4%BF%A1%E6%81%AF.png" title="%E5%8F%8B%E7%9B%9F%E5%90%8E%E5%8F%B0%E6%B8%A0%E9%81%93%E4%BF%A1%E6%81%AF.png"></p>
<h2>三、进行360加固</h2>
<p>相信任何在公司从事过App开发的,都知道应用在上线之前应该进行加固,但是我还是在应用市场上见过没加固的,直接使用<code>jadx</code>反编译之后,每一行代码都能清清楚楚的看到,跟他们相关人员联系后,给我的答案是:“我们知道没有加固,这点我们很早就清楚。” “(⊙o⊙)…我.....”,然后还反问我一句:“你知道吗,你用加固的话,那些加固厂商可能会在你apk里加入些东西”...我都无fuck说了,那你还集成那么多的第三方服务干嘛,他们就不会添加?这就是裸奔的理由?好了,吐槽结束。我们在发布apk到应用市场的时候,一定要进行apk加固,如果实在是信不过加固厂商,自己又牛逼的很的话,就自己写吧。</p>
<p>这里用的比较多的就是360加固了,使用也很简单,就是讲自己打包好的apk上传上午,加固好之后再下载加固后的包即可。具体可以查看<a href="https://link.segmentfault.com/?enc=Pi8Jq4oc%2F1eo9t9JtFglog%3D%3D.S%2B8SIaq%2BehwcdJodnvrmF50KgwHfkKexyfT4Nrtsg357ERUyFg1lBAsJay5CRGnR" rel="nofollow">360加固官网</a></p>
<p>这看起来一切流程都已经完成了,我们就一个个把第二步中生成的那些渠道一个个上传到360进行加固,然后下载下来不就行了。首先,如果渠道太多的话,这个是要死人的。更要命的是,当你一个个加密之后,竟然发现这些都不能用,获取不到渠道信息的时候</p>
<p><img src="/img/bVbdp3O?w=180&h=200" alt="%E6%83%B3%E6%AD%BB.jpg" title="%E6%83%B3%E6%AD%BB.jpg"></p>
<p>其实,这里就涉及到一个问题,我们<strong>直接通过美团Walle多渠道方案打包生成的apk,在经过360加固之后,是会丢掉渠道信息的</strong>,那么如何解决呢,美团在GitHub上也提出了解决方案<a href="https://link.segmentfault.com/?enc=N3J3EhUCnwtHtZlDhB1zUg%3D%3D.CjIgqJzrDHQUN7BNRRA7O3c8d10fk4I%2Fu2%2BrIci6fd5r9QGpLlP9kYOaUPvxMZr8oB8oWL5CxDFP1cMPXD64n8v5hFXGa4VdAlFyokQmwe6NFgDRHpQW%2BoLoHCxFj7oi" rel="nofollow">360加固失效?</a></p>
<p>作为懒人,繁琐的一步步自己去做是不可能,这辈子都可能的。这里我们就直接采用<code>Jay-Goo</code>大佬提供的Python打包脚本<a href="https://link.segmentfault.com/?enc=YIE%2BDY14q8Yg7cK18dO99w%3D%3D.sXoe0dzJADhVHBAu6aOVxs1K8iFPxui0wxBq7u6pVDx4O9Ycs65Lct%2BqJbE%2BI%2BHAkDoBRUp0B1YMVfo1JdCkJg%3D%3D" rel="nofollow">ProtectedApkResignerForWalle</a>,这也是美团官方推荐的方案。接下来我们就讲解如果使用该脚本</p>
<h2>四、解决360加固丢失渠道信息的问题</h2>
<p>这里我们使用ProtectedApkResignerForWalle解决360加固丢失渠道信息的问题,可以直接去<a href="https://link.segmentfault.com/?enc=u4cIHe1tw90Hs2dcycMYkQ%3D%3D.L5YpA4XYYfe57iyBoEqtHMMSPq3bvQVFd26KCkGzzve%2FnAIwUrh9zc85SG1H4nfmS3R2vp2vMuluoQxVPB8rEg%3D%3D" rel="nofollow">ProtectedApkResignerForWalle</a>仓库查看,很简单。这里我只是将其他一些需要注意的事情讲下。</p>
<ol>
<li>生成一个发布版的包,可以通过执行<code>assembleRelease</code>这个<code>task</code>或者通过工具栏上面的Build下面的<code>Generated Signed Apk</code>
</li>
<li>将这个apk通过360网页版进行加固(通过其提供的桌面工具加固的话会自动使用V1方式签名),然后下载下来加固之后的apk,比如叫:<code>release.encrypted.apk</code>
</li>
<li>通过 <code>git clone https://github.com/Jay-Goo/ProtectedApkResignerForWalle.git</code>命令来将仓库<code>clone</code>下来,然后把步骤1中生成的<code>release.encrypted.apk</code>放入该项目的根目录,并根据按照config.py文件中的注释改成自己项目配置</li>
<li>根据项目情况修改channel这个文件中,如果之前配置Walle的时候已经写了,直接复制过来就好,之前位置的channel文件就没用了,以后我们只需要维护这个channel文件即可</li>
<li>运行命令 python ApkResigner.py,即可自动生成所有渠道包。之后我们即可在该仓库的channels文件夹下看到生成的各种渠道包了</li>
</ol>
<p>最后,我们需要通过<code>jadx</code>反编译利器来随意查看某个apk是否加固成功,再安装试试,看是否能正常获取渠道信息。没问题的话,就开心的加班发布去吧。</p>
Dagger2-终于学会了,还好没放弃(1)
https://segmentfault.com/a/1190000009570755
2017-05-26T19:06:25+08:00
2017-05-26T19:06:25+08:00
warmcheng
https://segmentfault.com/u/warmcheng
4
<p>MVP + RxJava + Retrofit + Dagger2 如今已经成了Android开发的主流框架,为了不落伍,我也开始了这个组合框架的学习,力求摆脱之前的那种简单粗暴的分包模式。于是也开始了 Dagger2 的学习之路。这几天断断续续的也看了很多文章,其中当然有很多优秀的文章,但是慢慢的觉得并没有哪一篇文章,是完全的能让人只需要看一篇就能完全让一个人从0开始入门的,Dagger2这个框架学习起来还是比较费劲的,往往是搜了很多文章,综合起来才慢慢的能把很多东西搞懂,这就使得很多人还没入门就多次放弃了(说实话,我也放弃了很多次)。在这里,我想力求做到从一个“白痴”的角度(我是不会承认是我的)进行讲解,以问题为导向来讲解,很多时候我们觉得写博客的人虽然写了很多,觉得很牛逼,但可恨的是,我们还是不懂。其实这并不是我们的错,只是作者忘记了当初自己是怎么过来的,各种问题是怎么一步步被提出来的,结果就是变成了教科书式的讲解。下面就让我们一切从头来过,这一次,决不放弃。</p>
<h2>什么是依赖注入,为什么我们需要Dagger2</h2>
<p>这里,我就不在粘贴那些很官方的描述了,直接大白话。“依赖注入”,从这个词来说,就是 “依赖” 和 “注入” 两个部分嘛。所谓“依赖”,和我们生活中理解的一样,比如说汽车和汽油的关系,汽车只有在有汽油存在的情况下才有意义,严重依赖汽油。而“注入”,很容易让我们想到注射器,将一种液体注入到另一种容器中,这里的“注入”也是一样的,只不过是讲对象绑定到另一个类中而已(简单点来讲就是通过引用进行赋值)。</p>
<p>那为什么需要依赖注入呢?其实就是为了简化无用操作,提高效率,并且对代码进行解耦,使得需求或者一个地方的代码变动,尽可能小的引起其他部分代码的修改。我们以箱子和箱子的水果这个场景为例,进行讲解。</p>
<p>首先创建一个Box类,在其中又依赖了两个类:Apple 和 Banana。</p>
<pre><code>public class Box {
Apple mApple;
public Box(Apple apple){
mApple = apple;
}
}</code></pre>
<p>箱子里装有苹果。下面看具体使用:</p>
<pre><code>public class MainActivity extends AppCompatActivity {
Apple mApple;
Box mBox;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mApple = new Apple(); // 先创建 Apple 对象
mBox = new Box(mApple, mBanana); // 将创建好了的 Apple 对象传入 Box 对象的构造函数中
}</code></pre>
<p>可以看到,我们要向得到一个Box对象,必须要先手动生成一个Apple对象和一个Banana对象,试想一下,如果在以后,我们修改了Apple类,在其中添加了对苹果的描述,改变了构造方法,比如变为:</p>
<pre><code>public class Apple {
public Apple(String desctiption){
Log.i("qc","苹果的描述为: ");
}
}</code></pre>
<p>那我们就需要再去修改 MainActivity.java 类,显然按,这种编程习惯是不好的。我们能不能通过其他方法,来降低这种影响呢?其实,在编程中我们很多时候是用到过的,只是我们没意识到,想想,在列表加载中,为适配器设置数据、为列表项加载头布局和尾布局的时候你是怎么设置的?Bingo,通过setXXX(...)方法,比如,在Adapter中设置一个public方法来设置数据集:</p>
<pre><code>public void setData(List data){
...
}</code></pre>
<p>代码编写的多了,就会更加的接近设计模式、编程思想的本质,毕竟设计模式源于实践并且指导实践的,我们可能再也不愿意去直接在创建Adapter的构造函数中直接传入数据集了。<br>如果觉得这点我说的不太清楚的话,可以参考这篇文章:<a href="https://link.segmentfault.com/?enc=JE5l0fcSGWP%2FVC84lUv60A%3D%3D.YSLdEfnscJehqcVi4v%2BX4H1YgRUfUtP3%2FdIbLHJ9qd6yhBEVUY4MyIzT5Ndj8JQy" rel="nofollow">使用Dagger2前你必须了解的一些设计原则</a><br>其实,Dagger2 的思想,跟这个就很像,都是为了解耦。使用 Dagger2 ,这些setXXX(...)的方法,完全就不需要我们自己去做了。</p>
<p>这里再补充一点,如果你熟悉MVP的话,就很容易能感受到 Dagger2 的魅力,MVP 模式中,View和 Presenter 互相持有对方的引用,而 Presenter 同时又持有 Model 的引用,这样,就造成了View和 Presenter 之间的耦合性比较大,使用 Dagger2 可以减弱这种耦合,直接将 Presenter 通过依赖注入的方式注入到 View 中(例如Activity),当然,如果你还不了解MVP的话,现在学习这篇文章,千万别再跑去看MVP,而丢了主次,完全不需要,那只是 Dagger2 的一个使用场景。</p>
<h2>Dagger2学习需要的基础:注解</h2>
<p>在学习Dagger2之前,你一定要了解的东西就是Java中的注解,可以说如果不懂注解的话,学习Dagger2 是非常痛苦的一件事,甚至完全不知道整个过程是怎么进行的。关于注解,并不难,一两篇文章就可以搞懂,不会花费你很多时间的,可以参考这篇文章:<a href="https://link.segmentfault.com/?enc=dBZ6vzClTyytG6pteMqJag%3D%3D.UADTSHqjSaXeQPCvfvdzeMXqFV2DgroVAPSUJRTQooGb3ouC1SVUPMUyqpxAjeSUlxZS8hrW8kqDEvfBHx0%2F%2BA%3D%3D" rel="nofollow">JAVA 注解的几大作用及使用方法详解</a></p>
<p>以前我们往往是在需要的地方手动的去通过 new 来生成对象,对象的生成和对象的使用是在同一个地方,而 Dagger2 将对象的是生成和其使用分开。说白了,Dagger2 的整个过程就两个:</p>
<ol>
<li><p>创建并初始化对象(被依赖的对象,比如 Apple)</p></li>
<li><p>将初始化好的对象通过“中间人”放到需要它的那个对象中(目标类,或者成为容器,比如 Box)</p></li>
</ol>
<p>那么就让我们直接通过具体的例子来进行讲解,让大家更直观的感受到整个过程。</p>
<h2>Dagger2 具体使用</h2>
<h3>1.使用Dagger2 需要的相关配置</h3>
<p>现在Dagger2抛弃了之前采用的apt自动生成代码的方式,而采用annotationProcessor,因而<br>如果你使用的Gradle插件版本在2.2及以上,只需要在应用对应的build.gradle文件中添加以下依赖即可:</p>
<pre><code>dependencies {
...
...
// Dagger2相关依赖
compile 'com.google.dagger:dagger:2.11-rc2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.11-rc2'
}</code></pre>
<p>如果用的是比较旧的版本,可以参考GitHub上项目帮助文档:<a href="https://link.segmentfault.com/?enc=F54hxAdn2%2B3Umex%2BEpyD%2FA%3D%3D.M%2BPYepkEDeHn8AcbULr48knG81aZ%2Bz%2F6rJ%2F5nkHA%2F%2FLwrpZfUl9VLzPZ5PSQxfYf" rel="nofollow">https://bitbucket.org/hvisser...</a></p>
<h3>2.如何生成对象</h3>
<p>既然是依赖注入,我们首先需要做的就是把对象生产出来。那怎么生成一个对象呢,很容易想到在构造函数前加一个 new ,如:new Apple(),但是既然 Dagger2 能让它自动生成,我们当然不需要自己动手,只需要做某种标记,比如可以直接加上@Inject注解。<br>这里我们首先提出来,生成所需对象的方式有两种:</p>
<ol>
<li><p>@Inject 注解提供方式:在所需类的构造函数中直接加上 @Inject 注解</p></li>
<li><p>@Module 注解提供方式:通过新建一个专门的提供这类来提供类实例,然后在类名上面添加 @Module 注解,在类中自己定义方法,手动通过 new 来进行创建,这种主要是针对第三方库中,我们无法直接在构造函数中添加 @Inject 注解的情况。</p></li>
</ol>
<h3>3.具体实施步骤</h3>
<p>这里,我们先针对第一种情况进行讲解,也就是 @Inject 注解,先跑通整个流程<br>下面一步步讲解:</p>
<p><strong>第一步,很简单:就是在Apple类的构造函数上添加 @Inject 注解标记</strong></p>
<pre><code>
public class Apple {
@Inject // 注意此处的标记是加在构造函数上面的,这个是无参构造函数,有参构造函数的情况下之后会讲
public Apple(){
}
public String getDescription(){
return "这个苹果真好吃";
}
}
</code></pre>
<p>这样,我们就完成了提供者端的操作了,很简单吧,仅仅只是在构造函数上面添加了一个 @Inject 注解。</p>
<p>这一步,是告诉编译器及其他处理程序,我们需要通过此处来生成对象,从而供以后进行依赖注入。</p>
<p><strong>第二步,在目标类中的对应成员变量上也添加相同的注解 @Inject (注意,在目标类中这个注解始终是 @Inject ,而不会因为之后使用 @Module 这种方式而改变)</strong></p>
<pre><code>public class MainActivity extends AppCompatActivity {
@Inject
Apple mApple; // 在这里,我们添加了@Inject 注解,告诉处理程序,我们需要将对象注入到此处,给此引用赋值
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}</code></pre>
<p>在这一步,我们通过在目标类中属性上添加 @Inject 注解,来告诉编译器和其他代码自动生成程序,我们需要将之前生成的对象注入到这里(赋值给这里的引用)。<br><strong>注意</strong>,属性的访问修饰符一定不能是private类型的,否则无法通过Dagger2进行依赖注入,因为我们其实是通过传入引用,然后通过引用来调用其中的属性,一旦声明为private,我们自然无法通过<code>mainActivity.mApple</code>这种方式来进行赋值操作了。<br>至此,我们就完成了最基本的两步了,我们知道了从哪里生成对象,也知道了生成之后的对象需要在哪里使用,但是,如何将二者联系起来呢,也就是说,我如何通过某种方式,将生成的对象送达到需要的地方呢?这就需要一个“中间人”来起到桥梁的作用,让二者产生实质上的关联。这就是接下来要讲到的 @Component 注解了。</p>
<p><strong>第三步,通过 @Component 进行牵线搭桥</strong><br>我们最好建立一个包,名叫component,以方便管理。在该包下新建一个接口类,建议统一以Component作为后缀,比如下面的 MainComponent.java 。</p>
<pre><code>@Component
public interface MainComponent {
void inject(MainActivity activity);
}</code></pre>
<p>我们在该接口上面添加了 @Component 注解,同时定义了一个抽象方法:<code>void inject(MainActivity activity);</code><br>注意,这个方法,返回值是<code>void</code>类型的,后面的方法名可以随便起,括号内的参数是我们需要将对象注入的类,一定要是具体的类,而不能是其父类。其实,之后我们使用的时候,就是将对应的<code>Activity</code>传进去,然后通过引用调用该对象的属性进行赋值,思想很简单。这里要强调一点,这里并不是一定要求是<code>void</code>类型的,可以有具体的返回值,这种情况我们会在之后具体讨论,这里先通过这个<code>void</code>简单类型来学习,避免复杂化。函数名也一般先命名为<code>inject</code>,便于理解。</p>
<p>其实,写到这里,我们的整个过程基本上就完成了,下面只需要通过代码,进行实际的注入就好了。可能你会问,这就完成了?我们上面只是定义了一个接口,并没有具体的实现啊,怎么就能注入呢?接下来,我们就来见证奇迹的时刻把,我们把项目编译一下,或者直接点击AS上的绿色的锤子图标:</p>
<p><img src="/img/bVOmen?w=204&h=170" alt="clipboard.png" title="clipboard.png"><br>然后你打开下图的目录,就会发现会自动生成一些类。</p>
<p><img src="/img/bVOmfL?w=412&h=510" alt="clipboard.png" title="clipboard.png"></p>
<p>这就是我们在build.gradley依赖中添加的注解处理器(AnnotationProcess)给我们自动生成的。之前是没有apt这整个目录的,这里面我们可以看到,它也按照我们分的包的结构来生成的,比如“component”包。这里面现在对我们来讲最重要的也是直接跟我们发生关系的就是<code>DaggerMainComponent.java</code>这个类,我们进行具体的依赖注入其实就是使用的这个类,这个类是Dagger2框架自动为我们生成的,可以看到,它是以Dagger开头的。当我们创建了一个Component,比如MainComponent之后,系统会自动为我们生成一个以Dagger为前缀,后面跟上我们的Component名的一个具体实现类。先满足一下你的好奇心,我们点进去,看一下这个类。</p>
<pre><code>public final class DaggerMainComponent implements MainComponent {
private MembersInjector<MainActivity> mainActivityMembersInjector;
private DaggerMainComponent(Builder builder) {
assert builder != null;
initialize(builder);
}
public static Builder builder() {
return new Builder();
}
public static MainComponent create() {
return new Builder().build();
}
@SuppressWarnings("unchecked")
private void initialize(final Builder builder) {
this.mainActivityMembersInjector = MainActivity_MembersInjector.create(Apple_Factory.create());
}
// 看到了吧,这个就是我们在之前定义的接口,此处系统自动为我们进行了实现
@Override
public void inject(MainActivity activity) {
mainActivityMembersInjector.injectMembers(activity);
}
public static final class Builder {
private Builder() {}
public MainComponent build() {
return new DaggerMainComponent(this);
}
}
}</code></pre>
<p>可以看到,它实现了我们之前写的MainComponent接口,里面也对我们之前定义的抽象方法进行了实现。</p>
<pre><code> @Override
public void inject(MainActivity activity) {
mainActivityMembersInjector.injectMembers(activity);
}</code></pre>
<p>到这里知道为啥我说名字可以随便命名了吧。</p>
<p>好啦,看到这里就够了,千万别陷进去,一会儿我们还会回来继续分析这个类的,千万别觉得我是在浅尝辄止啊。<br><strong>还是再提醒一下,编写完之后,一定要编译一下项目,否则这些不会生成的,下面你也没法用<code>Dagger***Component</code>来进行注入了,会提醒你找不到该类。</strong></p>
<p><strong>第四步,也是最后一步,使用<code>Dagger***Component</code>进行依赖注入</strong><br>我们只需要在我们的目标类中,调用注入代码就可以了,如下代码:</p>
<pre><code>public class MainActivity extends AppCompatActivity {
@Inject
Apple mApple;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 将依赖注入到该Activity中
DaggerMainComponent
.builder()
.build()
.inject(this);
// 打印测试消息
Timber.i(mApple.getDescription());
}
}</code></pre>
<p>运行项目,可以看到,在日志中已经打印出来了我们<code>Apple</code>类中的信息(这里使用的是<code>JakeWharton</code>大神的日志打印框架,你直接用 Log.i 打印就好)。</p>
<p><img src="/img/bVOmsG?w=1282&h=260" alt="clipboard.png" title="clipboard.png"></p>
<p>好了,到这里,我们算是将Dagger2 的一个流程整个跑完了,但这只是一种注入的方式,也就是直接通过在类的构造函数上面添加@Inject注解来完成依赖注入,在下一篇文章中我将会为大家讲解两一种提供对象的方式:@Module</p>
Android中LayoutInflate解析xml布局文件生成View树的过程(一)
https://segmentfault.com/a/1190000009280657
2017-05-03T19:27:34+08:00
2017-05-03T19:27:34+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>学习过自定义 View 的都知道,ViewGroup 的事件分发或者绘制都涉及到子 View 的遍历,在看ViewGroup 的源码的过程中发现了这个我们一直见到的 ViewGroup 的子 View 的集合,在这里是以数组的形式存储的:<code>View[] mChildren</code>。我就想,这个 mChildren 是在哪里赋值的呢?或者说是怎么被赋值的呢?这时候我想到,在平时我们想往一个 ViewGroup 中添加子 View 的时候,往往是调用的这个 ViewGroup 的 <code>addView(View child)</code> 方法,那么我们只要找到调用 <code>addView(View child)</code> 的地方,自然也就找到了。但是直接找这个方法的调用,可不是一件容易的事,看来我们要换种思路了。不急,这里我们先深入 <code>addView(View child)</code> 方法里看看具体是怎么赋值的,也算是给自己记下笔记吧。</p>
<h3>ViewGroup 的 <code>addView(View child)</code>过程,即 <code>mChildren</code> 的赋值</h3>
<p>用一个图来表示吧:</p>
<p><img src="/img/bVM6mu?w=459&h=411" alt="clipboard.png" title="clipboard.png"></p>
<p>在 <code>addView(View child)</code> 中调用了 <code>addViewInner(child, index, params, false)</code> ,在<br><code>addViewInner(child, index, params, false)</code> 中又调用了 <code>addInArray(child, index)</code>,在该方法中才是具体的实现了对 <code>mChildren</code> 的赋值(通过引用直接修改),下面贴下该方法的源码:</p>
<pre><code>private void addInArray(View child, int index) {
View[] children = mChildren;
final int count = mChildrenCount;
final int size = children.length;
if (index == count) {
if (size == count) {
mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
System.arraycopy(children, 0, mChildren, 0, size);
children = mChildren;
}
// 注意这里,这里就是对 mChildren 进行赋值的地方
children[mChildrenCount++] = child;
} else if (index < count) {
if (size == count) {
mChildren = new View[size + ARRAY_CAPACITY_INCREMENT];
System.arraycopy(children, 0, mChildren, 0, index);
System.arraycopy(children, index, mChildren, index + 1, count - index);
children = mChildren;
} else {
System.arraycopy(children, index, children, index + 1, count - index);
}
children[index] = child;
mChildrenCount++;
if (mLastTouchDownIndex >= index) {
mLastTouchDownIndex++;
}
} else {
throw new IndexOutOfBoundsException("index=" + index + " count=" + count);
}
}</code></pre>
<p>代码很简单,可以看到这里也是搞了一个不断增长的数组来更新 mChildren,每次增长的长度为:<code>ARRAY_CAPACITY_INCREMENT</code>,为常量 12,这里对该方法我们不过多讲解,毕竟,我们的重点不在这。</p>
<h3>布局文件 xxx.xml 是怎么生成 View 树,并显示出来的呢?</h3>
<p>我们已经习惯了直接在一个 <code>activity</code> 中的 <code>onCreate(...)</code> 方法中通过 <code>setContentView(R.layout.main)</code> 这种方式来给我们的 activity 指定布局,然后我们就可以通过修改布局文件,来决定<code>activity</code>的显示内容。显然,在 <code>setContentView(...)</code> 这个方法内部肯定有我们想要的答案,在其内部,一定涉及到 <code>xml</code> 布局文件与具体的 <code>View</code> 之间的转换,也就是 <code>xml</code> 文件的解析和 <code>View</code> 树的生成。那么就让我们深入到该方法里一探究竟。</p>
<p><strong>注意:这里我们创建的<code>activity</code>直接继承自 <code>Activity.Java</code> 而非 <code>AppCompatActivity.java</code> , 至于为什么强调这个,在后面的文章里我们会具体讲解(涉及到LayoutInflate.Factory 以及兼容性的处理策略)。</strong></p>
<p>按着 <code>Ctrl</code> 键,鼠标<code>左键</code>点击我们创建的 <code>activity</code> 的 <code>setContentView(...)</code> 方法,将会进入到 <code>Activity.java</code> 的 <code>setContentView(@LayoutRes int layoutResID)</code> 方法:</p>
<pre><code> public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}</code></pre>
<p>可以看到,这里又调用了 Window 的 <code>setContentView(layoutResID)</code> 方法,而 <code>Window</code> 是一个抽象类,因为调用的实际上是它的<strong>唯一实现类</strong> <strong>PhoneWindow.java</strong> 里的该方法:</p>
<pre><code> public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 请注意这句,这句才是真正的加载过程,是关键代码
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}</code></pre>
<p>别看这么多代码,关键的只有一句: <code>mLayoutInflater.inflate(layoutResID, mContentParent)</code>,这句才是解析 xml 文件的核心代码,在此, LayoutInflate现身了,经常在代码里使用 <code>LayoutInflate</code> ,这次终于找到系统中使用该方法的地方了吧。那么接下来我们就真正看看 <code>LayoutInflate</code> 的使用以及 <code>LayoutInflate.inflate(...)</code>的具体过程吧。</p>
<h2>LayoutInflate 的具体使用</h2>
<h3>一、如何获取 LayoutInflate</h3>
<p>获取 InflateInflate 有三种方式:</p>
<ol>
<li><p><code>LayoutInflater layoutInflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);</code></p></li>
<li><p><code>LayoutInflater layoutInflater = LayoutInflater.from(context);</code></p></li>
<li><p>在 <code>activity</code> 中直接调用 <code>LayoutInflater layoutInflater = getLayoutInflater();</code> 获得</p></li>
</ol>
<p>其实,前面两种方式是完全一样的,只不过第二种对第一种进行了封装,使用起来更加方便。</p>
<pre><code> public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}</code></pre>
<h3>二、LayoutInflate.inflate(...) 的过程</h3>
<p>知道怎么获取 LayoutInflate 之后,我们继续上面的讲,前面我们通过 深入 <code>setContentView(R.layout.xxx)</code>发现真正解析 xml 布局文件,并且生成 View 树的其实就是 <code>mLayoutInflater.inflate(layoutResID, mContentParent)</code> 这行代码。那么我们就来看看</p>
<pre><code> public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}</code></pre>
<p>在该方法里又调用了含有三个参数的重载方法 : <code>inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)</code>,这就是我们经常使用 LayoutInfalte 时候传递三个参数或者传递两个参数的两种情况,关于这两种情况的区别,其实就是是否保留最外层View的 layout_xxx 属性,具体细节可以在此不再赘述,我们进入该方法继续看<br>` public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {</p>
<pre><code> final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
// 注意这里,这里调用的是另一个重载方法,注意第一个参数的类型为 XmlResourceParser
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}`
</code></pre>
<p>需要注意,这里又调用了<code>inflate(...)</code>的另一个重载方法:<code>public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)</code>,仍然是三个参数,但要注意,第一个参数类型变为了 <code>XmlResourceParser</code>,这里就设计 xml 文档的 pull 解析了:</p>
<pre><code> public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 关键代码1,调用含有四个参数的 inflate(...)
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 关键代码2,根据传进来的各种参数,生成对应的 View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
// 关键代码3,递归的解析出以 temp 为父节点的所有子 View 或者子 ViewGroup
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
// 关键代码4,将temp添加到根View:root中
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}</code></pre>
<p>看着代码那么多,是不是傻眼了,哈哈,不过不用着急,只需要看上面我汉语注释了的那几句代码,下面我们结合图来具体说明,相信你也会更加明白。</p>
<p><img src="/img/bVM6TT?w=1019&h=636" alt="clipboard.png" title="clipboard.png"></p>
<p>方法 <code>rInflateChildren(...)</code>顾名思义就是递归的解析出所有的子View,我们看他的代码,里面是直接的调用了 <code>rInflate(...)</code>方法的</p>
<pre><code> final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}</code></pre>
<p>我们也直接在这里粘贴出来的源码,一起进行比较。</p>
<pre><code> void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
// 重点注意以下几行
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
// 此处是进行加载完成之后的回调,目前为止传进来的参数 finishInflate 均为 true ,因而当所有的子Viewz都加载完毕的时候,就会回调父容器的 onFinishInflate 方法,在 ViewGroup 中,该方法为空实现
if (finishInflate) {
parent.onFinishInflate();
}
}</code></pre>
<p>上面这么多代码看起来很乱,但是我们只要重点抓住几条,就可以让思路变得更加清晰,上面那么多代码,核心只有下面几句:</p>
<pre><code> final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);</code></pre>
<p>然后就只是递归的问题了。可以总结为下面的几句:</p>
<ol>
<li><p><code>createViewFromTag(...)</code>负责根据指定的 name 生成具体的 View</p></li>
<li><p><code>rInflateChildren(...)</code>负责递归调用 <code>rInflate(...)</code>,使得该过程反复被执行,直到最后遍历完所有的View,不再进入While循环,直接调用<code>parent.onFinishInflate()</code>,表明遍历完结,然后就会在栈中一步步的向上调用 <code>parent.onFinishInflate()</code>方法来通知父容器,本View已经解析完成.</p></li>
<li><p><code>viewGroup.addView(view, params)</code>,该句负责把每次解析出来的 View 都添加进本次的根 ViewGroup 中,这样最终会将其下所有的子 View 都添加进来。</p></li>
</ol>
<p>自此,我们就将 <code>LayoutInflate</code> 解析 <code>xml</code> ,从而生成 <code>View树</code> 的过程讲解完了,当然,具体的内容还有很多,接下来我会一步步的去讲解,在本篇中的一些迷惑,你也会一一解开,那么下一篇,就让我们从 <code>createViewFromTag(...)</code>讲起吧!</p>
PHP学习笔记
https://segmentfault.com/a/1190000008948456
2017-04-05T12:48:27+08:00
2017-04-05T12:48:27+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<ol><li><p>问题:运用 data("H") 函数的时候发现输出的时间值和系统的时间不相符,实际上出现该问题的原因是我们在PHP服务器中设置的时区不对。<br>解决方法:选择<code>wampserver -> PHP -> php.ini</code> ,在里面搜索 timezone,你会发现下面这行:</p></li></ol>
<p><img src="/img/bVLHVr?w=509&h=228" alt="clipboard.png" title="clipboard.png"><br>其中,UTC 指的是世界标准时间,也就是英国格林尼治天文台的时间,我们位于东8区,如果足够细心的话,就会发现输出的值和我们本地的时间刚好差了8个小时。我们可以将其设置为:</p>
<pre><code>Etc/GMT-8</code></pre>
使用.gitignore忽略编译自动生成的那些文件
https://segmentfault.com/a/1190000008517302
2017-02-28T17:57:56+08:00
2017-02-28T17:57:56+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<p>今天闲下来,终于有时间解决下一直以来困扰我的一个Git的问题。就是如果当我们每次编译Android项目之后,用<code>git status</code> 命令查看后,总会有很多跟编译相关的文件显示为红色(如果你之前没有添加后者修改.gitignore文件的话)。这对于像我一样患有重度强迫症的患者当然是不能接受的。那么我们就来简单的看下.gitignore文件是什么,以及如何正确使用它。</p>
<h2>.gitignore文件是什么</h2>
<p>在我们使用 git 的时候,我们将文件添加进暂存区往往用的不是<code>git add 单个文件</code>,我们更多的使用的是 <code>git add .</code> 来添加所有的文件,这样更加方便,毕竟某有多少情况下我们需要一个个添加文件。然而,当我们不需要添加某些文件的时候,怎么办呢?也有办法,比如,在Android studio 中,我们可以在项目中新建一个<code>.gitignore</code>文件(如下图中黄色标识处),然后在里面添加我们需要忽略的文件类型,添加进这里面的文件,我们在使用<code>git add .</code>命令的时候,并不会将他们添加进版本控制中,而且在使用<code>git status</code>命令的时候也不会显示出关于它们的任何信息,这样工作区 就变得干净清爽的多啦。</p>
<p><img src="/img/bVJTHH?w=215&h=385" alt="clipboard.png" title="clipboard.png"></p>
<p>不过,这里并不会教大家怎么写<code>.gitignore</code>文件,感兴趣的可以在网上搜下,其实很简单,就是忽略指定文件或者指定文件夹下的所有目录,又或者指定类型的文件,本质上其实就是正则表达式这里不详述。此处只是讲如何解决问题。</p>
<h2>何时会遇到<code>.gitignore</code>文件问题</h2>
<p>这里我假定你已经知道了如何编写<code>.gitignore</code>文件,我这里只说一些注意的东西,和如何解决我们遇到的问题。<br>诚然,如果你在新建项目的时候直接就编写好该文件,之后在版本控制中也不修改,而且也不会添加其他肯你那个你不想跟踪的文件进来,那你就不需要考虑这些问题了。然而往往我们是在开发的过程中,导入了很多库,这些库又生成了很多编译相关的文件。所以就涉及到修改.gitignore文件。如果你来来回回在本地和服务器之间<code>push</code>、<code>pull</code>了多次,或者跟别人写作的过程中突然想修改,那就会遇到一个问题,那就是:我们修改的<code>.gitignore</code>文件不生效。</p>
<h2>解决方法</h2>
<p>我们需要注意,如果那些文件我们已经加入了版本控制工具之后,我们要想重新设置.gitignore文件,需要将本地的仓库中的那些忽略的文件从 git 中取消追踪,可以使用以下命令来清空缓存<code>git rm -r --cached .</code> (不要忘记改命令后面的那个 "."),这时,我们会将所有文件都取消追踪,也就是说git不会再管理所有的文件,在命令行中可能还会显示很多文件被删除, 类似于下图:</p>
<p><img src="/img/bVJTN0?w=595&h=329" alt="clipboard.png" title="clipboard.png"></p>
<p>不过,不用害怕,这并没有从我们的工作控件之中删除,我们可以使用 <code>git add .</code>将他们重新加入 git 中,而不用担心那些想忽略的文件也添加进来了。之后再用<code>git commit -m "提交信息"</code>进行提交就行了。不过,接下来要注意了,我们最好不要在 <code>.gitignore</code>中添加以下这两行:</p>
<pre><code>/app/.gitignore
/.gitignore
</code></pre>
<p>添加这两行的话我们就<strong>不会</strong>将我们的<code>.gitignore</code>文件进行跟踪了,最好还是<strong>别加上</strong>这两句。</p>
<p>上面事情不大,可是,如果你加上了<code>项目名/</code>这句,比如<code>HelloWorld/</code>,相信你已经猜到了,如果这样的话,在你执行<code>git rm -r --cached .</code>后紧接着执行 <code>git add .</code>命令后,你会发现,你项目里的所有的东西都不会被 git 管理,当然很容易理解,因为它们已经 git 被忽略了。也许你会想,谁会那么傻,将这个添加进来。你当然不会,但是你不能保证某些情况下“被加入”,我就遇到过这种情况。因为我的 Android Studio 安装了 <code>.gitignore</code> 插件,而它有时候会询问你是否把某些不需要 git 追踪的文件加入进来 ,你点击“添加”的话,它会自动从网上将那些常用的文件加载进来,不过小心,有时候很可能不那么智能,比如像我们项目中有个文件夹,很不幸的就和我们的项目名重名了..然后就悲剧了...</p>
<p><strong>注意:</strong>通过以上命令只是将你自己的仓库里的修改设置生效了,如果你是多人协作,他人提交代码后,你还是会出错,这就需要那些跟你协作的人也都进行类似的操作,清除本地 git 缓存下来的东西,从而将那些东西也都从各自的本地仓库里取消追踪(不知道有没有其他方式来简单的解决,暂时只想到这种方式。)</p>
<p>参考:<br>1.关于Git忽略文件:<br><a href="https://link.segmentfault.com/?enc=6p7cMg0lrVmyGPyTd2FOwQ%3D%3D.svuI0UTwagf3cxgwzsZKtimnFHtApWNFvQoJseErMVOdXW8d8fegCZANhaTtaFLC7uBeGbYxdmEwXwiateysi9jLeR69oHbOYFnU0yj3yBEcDJwxfde94jIxCGGR7yq0tJwzfGL03U9iTyRs%2BIoQkn3wud2xyLbMtQXNxgOggFY%3D" rel="nofollow">http://reondz.github.io/%E6%8...</a></p>
android 四大组件之Service
https://segmentfault.com/a/1190000007685753
2016-12-03T22:57:58+08:00
2016-12-03T22:57:58+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<p>文章参考自:<br>1.郭神博客:<a href="https://link.segmentfault.com/?enc=XFUFsMR21EhaDo07naXY0g%3D%3D.H%2BpxDVWCi2DpUXUd7qTaxWoAQVrhdyp5n6ksUEoS9DuIHTSp8L%2F7hsAlUdovTCanYcnRiZ2RcCvIB3yaS32z3w%3D%3D" rel="nofollow">http://blog.csdn.net/guolin_b...</a><br>2.Android开发文档:<br><a href="https://link.segmentfault.com/?enc=SWK38SdHAKYlk%2BocXGkUJQ%3D%3D.uNxUqzAGZ7vhhoKWvdt8X0Kfn2OYVk4WKtu8BMi8VUgV8pkFey5ZorNhPjPiJo9BT%2FFSlXDcvIECVVbfyFjXm0TEwUjHwPORuzTeqmSxy%2Bo%3D" rel="nofollow">https://developer.android.com...</a><br><a href="https://link.segmentfault.com/?enc=NnmR6BmAsG6G8WXOPLPgRA%3D%3D.k0knhwhf8jTx1pnjyNfj2Ym1kjMlAfSziFE51MhpfWsP4zu2q%2BS2UC6tIXdRPaqvclUkthrB0MWn0VdY6fPosOyWa2N2%2F9A0yVOxqfiDikZ7rQS5VAXQl9QgKtVPJmwO" rel="nofollow">https://developer.android.com...</a></p>
<hr>
<p>最近趁着有时间,将以前的知识整理下,要不然总容易遗忘,今天就来讲解下Service的用法。<br>作为Android的四大组件之一,其重要性可想而知。在应用中我们主要是用来进行一些后台操作,不需与应用UI进行交互,执行耗时任务等。</p>
<p>官方文档中这样说:</p>
<blockquote><p>Service 是一个可以在后台执行长时间运行操作而不提供用户界面的应用组件。服务可由其他应用组件启动,而且即使用户切换到其他应用,服务仍将在后台继续运行。<br>此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O<br>或与内容提供程序交互,而所有这一切均可在后台进行。</p></blockquote>
<h2>Service的用途:</h2>
<p>1.在后台执行耗时操作,但不需要与用户进行交互。<br>2.一个应用暴露出来的一些供其他应用使用的功能。</p>
<p><strong>这里需要声明一点,Service是运行在主线程中,因而如果需要进行耗时操作或者访问网络等操作,需要在Service中再开启一个线程来执行(使用IntentService的话则不需要在自己手动开启线程)。</strong></p>
<h2>启动Service</h2>
<p>启动一个Service有两种方式:</p>
<pre><code>Context.startService()</code></pre>
<pre><code>Context.bindService()</code></pre>
<p><img src="/img/bVGp60?w=451&h=566" alt="clipboard.png" title="clipboard.png"></p>
<p>(图片截取自官方文档:<a href="https://link.segmentfault.com/?enc=EiXfgeXeJ%2BDue3bShbGm%2FA%3D%3D.jKwi8lfqy64tSon835VclolSDdrjSpoDknq%2FbshIv7ybcRbyVlJcURlZRPVXiPuEfMf2XUybQhdKieXGntU%2FlweOiSDBuTEYfQniKHrOkn6UFyKpIcJR7csh7%2F3KsOkZ" rel="nofollow">https://developer.android.com...</a>)</p>
<p>startService()方式启动Service,我们启动之后是没有办法再对Service进行控制的,而且启动之后该Service是一直在后台运行的,即使它里面的一些代码执行完毕,我们要想终止该Service,就需要在他的代码里面调用stopSelf()方法或者直接调用stopService() 方法。而通过bindService()方法启动的Service,客户端将获得一个到Service的持久连接,客户端会获取到一个由Service的onBind(Intent)方法返回来的IBinder对象,用来供客户端回调Service中的回调方法。</p>
<p>我们无论使用那种方法,都需要定义一个类,让它继承Service类,并重写其中的几个方法,如果我们是采用startService()方式启动的话,只需要重写onCreate() 、onStartCommand(Intent intent, int flags, int startId)、onDestroy()方法即可(其实我们也可以重写),而如果采用的是bindService()方法启动的话,我们就需要重写onCreate() 、onBind(Intent intent)、 onUnbind(Intent intent)方法.<strong>注意</strong>,作为四大组件之一,Service使用之前要在清单文件中进行配置。</p>
<pre><code> <application>
......
<service
android:name=".MyService">
</service>
</application></code></pre>
<h3>Context.startService()</h3>
<p>MyService.java的代码:</p>
<pre><code>public class MyService extends Service {
public MyService() {
}
@Override
public void onCreate() {
super.onCreate();
Log.i("test","onCrete executed !");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("test","onStartComand executed !");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("test","onDestroy executed !");
}
}</code></pre>
<p>MainActivity.java的代码如下:</p>
<pre><code>public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button btnStart,btnStop;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnStart = (Button) findViewById(R.id.btn_start);
btnStop = (Button) findViewById(R.id.btn_stop);
btnStart.setOnClickListener(this);
btnStop.setOnClickListener(this);
}
@Override
public void onClick(View view) {
Intent mIntent = new Intent(MainActivity.this,MyService.class);
switch (view.getId()){
case R.id.btn_start:
startService(mIntent);
break;
case R.id.btn_stop:
stopService(mIntent);
break;
}
}
}</code></pre>
<p>主界面就两个按钮,一个用来启动Service,一个用来停止Service:</p>
<p><img src="/img/bVGphg?w=256&h=422" alt="clipboard.png" title="clipboard.png"></p>
<p>下面我们先点击START按钮,Log信息如下:</p>
<p><img src="/img/bVGphl?w=1008&h=148" alt="clipboard.png" title="clipboard.png"><br>可以看出,onCreate()方法先执行,然后onStartCommand()方法紧接着执行,那么如果我们再次点击启动按钮呢?结果如下图:</p>
<p><img src="/img/bVGphK?w=1017&h=183" alt="clipboard.png" title="clipboard.png"><br>我们可以看到,这次onCreate()方法没有再执行,而是直接执行了onStartCommand()方法,这是因为Service只在第一次创建的时候才执行onCreate()方法,如果已经创建了,那之后再次调用startService()启动该Service的时候,只会去执行onStartCommand()方法方法,而不会再执行onCreate()方法。<br>接下来我们点击停止按钮,可以看到,onDestroy()方法被执行了:</p>
<p><img src="/img/bVGphX?w=1160&h=247" alt="clipboard.png" title="clipboard.png"></p>
<p>注意,如果我们不点击停止按钮手动停止该Service的话,该Service会一直在后台运行,即使它的onStartCommand()方法中的代码已经执行完毕,在下图中我们可以看到:</p>
<p>这时候我们的这个Service是一直在后台执行的,即使它的onStartCommand()方法中的代码已经执行完了。如果我们想要它自动停止的话,可以将onStartCommand()方法中的代码修改如下:</p>
<pre><code> @Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("test","onStartComand() executed !");
stopSelf();
return super.onStartCommand(intent, flags, startId);
}
</code></pre>
<h3>Context.bindService()</h3>
<p>采用该方法的代码就稍微比以前的多了,因为我们需要在客户端对Service进行控制,因而会在MainActivity中创建一个匿名内部类ServiceConnection,然后会在bindService()方法和unbindService()方法中将其传入。<br>MyService.java 中的代码如下:</p>
<pre><code>public class MyService extends Service {
private MyBinder myBinder = new MyBinder();
public MyService() {
}
@Override
public void onCreate() {
super.onCreate();
Log.i("test","onCreate() executed !");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("test","onDestroy() executed !");
}
@Override
public boolean onUnbind(Intent intent) {
Log.i("test","onUnbind executed !");
return super.onUnbind(intent);
}
@Override
public IBinder onBind(Intent intent) {
Log.i("test","onBind() executed !");
return myBinder;
}
class MyBinder extends Binder{
public void startDownload(){
Log.i("test", "MyBinder中的startDownload() executed !");
// 执行具体的下载任务,需开启一个子线程,在其中执行具体代码
}
}
}</code></pre>
<p>MainActivity.java 的代码如下:</p>
<pre><code>public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button btnBind,btnUnBind;
MyService.MyBinder myBinder ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnBind = (Button) findViewById(R.id.bind);
btnUnBind = (Button) findViewById(R.id.btn_unBind);
btnBind.setOnClickListener(this);
btnUnBind.setOnClickListener(this);
}
ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
// 将IBinder向下转型为我们的内部类MyBinder
myBinder = (MyService.MyBinder) iBinder;
// 执行下载任务
myBinder.startDownload();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
}
};
@Override
public void onClick(View view) {
Intent mIntent = new Intent(MainActivity.this,MyService.class);
switch (view.getId()){
case R.id.bind:
// 绑定Service
bindService(mIntent,mServiceConnection,BIND_AUTO_CREATE);
break;
case R.id.btn_unBind:
// 取消绑定Service
unbindService(mServiceConnection);
break;
}
}
}</code></pre>
<p>点击绑定按钮;</p>
<p><img src="/img/bVGppt?w=1269&h=211" alt="clipboard.png" title="clipboard.png"><br>点击取消绑定按钮:</p>
<p><img src="/img/bVGppK?w=1134&h=228" alt="clipboard.png" title="clipboard.png"><br>注意,如果我们没有先点击绑定,而是直接点击的取消绑定,程序会直接crash,报以下错误:</p>
<pre><code>java.lang.IllegalArgumentException: Service not registered: com.qc.admin.myserializableparceabledemo.MainActivity$1@8860e28
at android.app.LoadedApk.forgetServiceDispatcher(LoadedApk.java:1120)
at android.app.ContextImpl.unbindService(ContextImpl.java:1494)
at android.content.ContextWrapper.unbindService(ContextWrapper.java:616)
at com.qc.admin.myserializableparceabledemo.MainActivity.onClick(MainActivity.java:71)</code></pre>
<p><img src="/img/bVGpxH?w=953&h=160" alt="clipboard.png" title="clipboard.png"></p>
<p>细心的你也许早就发现了,Log中并没有打印"onServiceDisconnected executed !"这句,也就是说没有调用onServiceDisconnected()方法?从字面理解,onServiceConnected()方法是在Service建立连接的时候调用的,onServiceDisconnected()不就应该是在Service断开连接的时候调用的吗?其实不然,我们查看该方法的文档就知道了:</p>
<blockquote><p>Called when a connection to the Service has been lost. This typically happens when the process hosting the service has crashed or been killed. This does not remove the ServiceConnection itself -- this binding to the service will remain active, and you will receive a call to onServiceConnected(ComponentName, IBinder) when the Service is next running.</p></blockquote>
<p>意思就是:当绑定到该Service的连接丢失的时候,该方法会被调用,典型的情况就是持有该Service的进程crash掉了,或者被杀死了。但是这并不会移除ServiceConnection 自身--它仍然是保持活跃状态,当Service下次被执行的时候,onServiceConnected(ComponentName, IBinder) 方法仍然会被调用。</p>
<p>但是要<strong>注意</strong>,如果我们按照刚才说的,不是先点击 bindService()方法,而是直接点击unbindService()方法,程序虽然也是crash掉了,但onServiceDisconnected()方法并不会被调用,这个很容易理解,毕竟都没有建立连接呢,谈何断开连接啊。但是如果我们已经绑定了Service,然后在后台直接终止该Service呢?结果会怎样?答案是onServiceDisconnected()方法仍然不会调用。这里我觉得应该是只有在意外的情况下进程结束,是由系统自动调用的,而非我们手动停止的。我们可以查看该方法内部的注释:</p>
<blockquote><p>This is called when the connection with the service has been <br>unexpectedly disconnected -- that is, its process crashed.Because it<br>is running in our same process, we should never see this happen.</p></blockquote>
<p>这段文字清楚的说明了该方法执行的场景:异常情况下导致断开了连接。也就是进程crash掉了。因为它运行在我们应用程序所在的进程中,因而我们将永远不希望看到这种情况发生。</p>
<h3>Context.startService()和Context.bindService()同时使用</h3>
<p>这两种方式是可以同时使用的,但是要注意,startService()和stopService()方法是对应的,而bindService()和unBind()方法是对应的,也就是说如果我们先调用startService()之后调用bindService()方法,或者相反,那么我们如果只调用stopService()或者只调用bindService()都无法停止该Service,只有同时调用才可以。<br>下面来看下具体代码:<br>MainActivity.java</p>
<pre><code>public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button btnStart,btnStop,btnBind,btnUnBind;
MyService.MyBinder myBinder ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnStart = (Button) findViewById(R.id.btn_start);
btnStop = (Button) findViewById(R.id.btn_stop);
btnBind = (Button) findViewById(R.id.btn_bind);
btnUnBind = (Button) findViewById(R.id.btn_unBind);
btnStart.setOnClickListener(this);
btnStop.setOnClickListener(this);
btnBind.setOnClickListener(this);
btnUnBind.setOnClickListener(this);
}
ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
// 将IBinder向下转型为我们的内部类MyBinder
myBinder = (MyService.MyBinder) iBinder;
// 执行下载任务
myBinder.startDownload();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.i("test","onServiceDisconnected executed !");
}
};
@Override
public void onClick(View view) {
Intent mIntent = new Intent(MainActivity.this,MyService.class);
switch (view.getId()){
case R.id.btn_start:
// 启动Service
startService(mIntent);
break;
case R.id.btn_stop:
// 终止Service
stopService(mIntent);
break;
case R.id.btn_bind:
// 绑定Service
bindService(mIntent,mServiceConnection,BIND_AUTO_CREATE);
break;
case R.id.btn_unBind:
// 取消绑定Service
unbindService(mServiceConnection);
break;
}
}
}
</code></pre>
<p>MyService.java的代码:</p>
<pre><code>public class MyService extends Service {
private MyBinder myBinder = new MyBinder();
public MyService() {
}
@Override
public void onCreate() {
super.onCreate();
Log.i("test","onCreate() executed !");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("test","onStartComand() executed !");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("test","onDestroy() executed !");
}
@Override
public boolean onUnbind(Intent intent) {
Log.i("test","onUnbind executed !");
return super.onUnbind(intent);
}
@Override
public IBinder onBind(Intent intent) {
Log.i("test","onBind() executed !");
return myBinder;
}
class MyBinder extends Binder{
public void startDownload(){
Log.i("test", "MyBinder中的startDownload() executed !");
// 执行具体的下载任务
}
}
}</code></pre>
<p>a.下面是依次点击start、bind、stop、unBind 按钮的输出结果:</p>
<p><img src="/img/bVGpzm?w=1159&h=264" alt="clipboard.png" title="clipboard.png"></p>
<p>b.下面是依次点击start、bind、unbind、stop 按钮时的输出结果:</p>
<p><img src="/img/bVGpzv?w=1164&h=285" alt="clipboard.png" title="clipboard.png"></p>
<h2>在前台运行服务</h2>
<p>我们上面一直说Service一般是用来在后台执行耗时操作,但是要知道,Service也是可以运行在前台的。后台Service的优先级比较低,容在内存不足等情况下被系统杀死,通过将其设置为前台,可以大大降低其被杀死的机会。前台Service会在系统通知栏显示一个图标,我们可以在这里进行一些操作。前台Service比较常见的场景有音乐播放器和天气预报等:</p>
<p><img src="/img/bVGp9g?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p>那么接下来我们就直接上代码:</p>
<pre><code> @Override
public void onCreate() {
super.onCreate();
Log.i("test", "onCreate() executed !");
Intent mIntent = new Intent(this, SecondActivity.class);
PendingIntent mPendingIntent = PendingIntent.getActivity(this, 0, mIntent, 0);
Notification mNotification = new NotificationCompat.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("My Notification ")
.setContentText("Hello World ! ")
.setContentIntent(mPendingIntent)
.build();
// 注意:提供给 startForeground() 的整型 ID 不得为 0。
// 要从前台移除服务,请调用 stopForeground()。此方法采用一个布尔值,指示是否也移除状态栏通知。
// 然而stopForeground()不会停止服务。 但是,如果您在服务正在前台运行时将其停止,则通知也会被移除。
startForeground(1, mNotification);
}</code></pre>
<p>其实这里的实现很简单,就是将一个Notification通过startForeground(1, mNotification);传进去,从而将Notification与 Service建立起关联。<br>我们点击这个通知,就会跳转到第二个Activity(但是该Notification并不会消失),截图如下:</p>
<p><img src="/img/bVGqkd?w=768&h=1280" alt="clipboard.png" title="clipboard.png"></p>
对象的序列化存储:Serializable 和 Parceable
https://segmentfault.com/a/1190000007665988
2016-12-01T21:26:16+08:00
2016-12-01T21:26:16+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<p><strong>文章参考自任玉刚大神的书籍《Android开发艺术探索》,强烈推荐这本书。</strong></p>
<p>在进行Android开发的时候我们有时候需要用到数据的持久化存储,或者在进程之间传递数据。其中就可能需要用到对象的序列化,经过序列化的对象之后可以通过Intent或者Boundle来传输了。接下来还是想些介绍下吧。</p>
<h2>1.什么叫序列化,什么叫反序列化</h2>
<p>序列化: 将数据结构或对象转换成二进制串的过程。<br>反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。</p>
<p>简单来说,序列化就是将我们生成的对象进行存储起来(比如磁盘上),以用来将来使用或者在网络上进行传输,而反序列化呢,就是由我们的之前序列化生成的二进制串重新生成对象的过程。注意,这里我们反复说的序列化啦,反序列化啦,都是针对的<strong>对象</strong>,而非<strong>类</strong>。因为我们是针对对象进行存取与传输的,而非类,当我们需要重新获取之前的对象的时候,是直接读取出来的(从文件或网络中),而非根据类new出一个对象,这点是需要注意的。</p>
<h2>2.如何序列化</h2>
<p>序列话的方式有两种,一种是实现Serializable接口,一种是实现Parceable接口,下面会具体介绍这两种方式。</p>
<h3>a.实现Serializable接口</h3>
<p>这种序列化方式是Java提供的,它的优点是简单,其实Serializable接口是个空接口,因而我们并不需要实现什么抽象方法,但是我们却往往需要在类中声明一个静态变量标识(serialVersionUID),但这不是必须的,我们不声明,依然可以实现序列化,但是这样的话会对反序列化产生一定的影响,可能会在我们对类做了修改之后而造成对象的反序列化失败。声明方式如下:</p>
<pre><code>private static final long serialVersionUID = 8711368828010083044L;</code></pre>
<p>注意,这里的值可以是任意值。</p>
<p>下面我们来具体实现下。</p>
<pre><code>package com.qc.admin.myserializableparceabledemo;
import java.io.Serializable;
/**
* Created by admin on 2016/12/1.
*/
public class User implements Serializable {
private static final long serialVersionUID = 519067123721295773L;
public int userId;
public String userName;
public boolean isMale;
public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
@Override
public String toString() {
return "User{ " +
"userId = " + userId +
", userName = " + userName +
", isMale = " + isMale +
" }";
}
}
</code></pre>
<p>下面是序列化与反序列化过程:</p>
<pre><code> private void beginSerizable() throws IOException, ClassNotFoundException {
// 序列化
User user = new User(2016, "qian", true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File(getFilesDir(), "myfile.txt")));
out.writeObject(user);
out.close();
// 反序列化
// 注意,这里后面的“/myfile.txt”前面有个斜杠“/”,否则会报“FileNotFoundException”异常
ObjectInputStream in = new ObjectInputStream(new FileInputStream(getFilesDir() + "/myfile.txt"));
User mUser = (User) in.readObject();
textView.setText(mUser.toString());
in.close();
Log.i("test",mUser.toString());
}</code></pre>
<p>运行结果截图:</p>
<p><img src="/img/bVGjKR?w=1238&h=85" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>注意</strong>:如果是在Android项目中调用以上方法,别忘了在Manifest.xml文件中配置如下权限:</p>
<pre><code> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/></code></pre>
<h3>b.实现Parceable接口</h3>
<p>这种方式是Android提供的方式,相比较前面那种方式来讲,这种方式稍微有点复杂,我们需要自己尽享序列化与反序列化的操作,但是它却更加高效,并不需要执行大量的I/O操作。而且这种方式也是Android推荐的序列化方式,因此我们应该首选Parceable。只要实现了这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder进行传递了。下面请看实例:</p>
<pre><code>public class Book implements Parcelable {
public String bookTitle;
public int bookId;
protected Book(Parcel in) {
bookTitle = in.readString();
bookId = in.readInt();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(bookTitle);
parcel.writeInt(bookId);
}
}</code></pre>
<p>这里将Book这个类就实现了Parcelable接口,其实在Android Studio IDE中,上述过程很简单,我们只需要定义一个类,实现Parcelable接口,然后在里面定义我们的属性或者说是字段,根据提示的错误,按照它提示的方法覆盖相应的方法,之后的一切其实都可以自动生成(不过如果需要构造方法的话,那就需要自动生成了,toString()方法也是自己实现的),所以不用担心在Android开发中通过实现Parceable接口会比较麻烦,因为AS都会为你自动生成。<br>上面我们已经完整的将Book类实现了Parceable接口,那接下来如何序列化和反序列化呢?如果你说,刚才不是已经说过了吗,采用文件读取的方式不久可以了啦...当你那样做的时候,你会发现会报如下的错误:</p>
<p><img src="/img/bVGkjo?w=1047&h=251" alt="clipboard.png" title="clipboard.png"></p>
<p>Why???...什么情况?提示我们Book类没有实现序列化:</p>
<pre><code>/System.err: java.io.NotSerializableException: com.qc.admin.myserializableparceabledemo.Book</code></pre>
<p>好啦,之所以出现这种问题,并不是我们的实现过程有问题,而是使用该类的方式行不通。到这里我们就明白了Serializable和Parceable两种方式实现序列化还是有区别的,刚才我们也讲了,Parceable更加高效,不会像Serializable那样有大量的I/O操作,这句话的具体含义就道出了<strong>Serializable与Parcelable区别:虽然两者都是用于支持序列化、反序列化话操作,但是两者最大的区别在于存储媒介的不同,Serializable是将序列化后的对象存储在硬盘上,使用I/O读写的方式,而Parcelable是将其存储在内存中,是针对内存的读写,熟悉计算机组成原理的朋友都知道,内存的读写速度显然要远远大于I/O的读写速度,这也是为什么Android中推荐使用Parcelable这种方式来实现对象的序列化。</strong><br>那我们应该怎么使用通过实现Parcelable接口实现序列化的对象呢?答案是:通过Intent方式传递,除了基本类型外,Intent只能传输序列化之后的对象,对应这两种序列化方式,也有两种相应的方法:</p>
<pre><code> mIntent.getSerializableExtra(string name );</code></pre>
<pre><code> mIntent.getParcelableExtra(String name );</code></pre>
<p>当然,放入的操作就没有这种区分了,都是方法:</p>
<pre><code>mIntent.putExtra();</code></pre>
<p>我们可以在第一个Activity中将序列化对象放入Intent,在另一个Activity中取出,比如:<br>在另一端获取对象,例如:</p>
<pre><code> Bundle mBundle = getIntent().getExtras();
Book mBook = mBundle.getParcelable("book1");</code></pre>
<p>下面再看类User实现Parceable接口的过程,它内部包含了一个可序列化的类Book,具体细节跟上面的有点不同:</p>
<pre><code>package com.qc.admin.myserializableparceabledemo;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Created by admin on 2016/12/1.
*/
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public Book book;
public User(int userId, String userName, boolean isMale, Book book) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
this.book = book;
}
protected User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
// 此为不同之处1
// 也可以通过这种方式:book = in.readParcelable(Thread.currentThread().getContextClassLoader());
book = in.readParcelable(Book.class.getClassLoader());
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
// 几乎在所有的情况下都应该返回0,只有在当前对象中存在文件描述的时候,此方法返回CONTENTS_FILE_DESCRIPTOR(常量值为1)
@Override
public int describeContents() {
return 0;
}
// 将对象写入序列化结构中,其中i标识有两种值,0或者1(PARCELABLE_WRITE_RETURN_VALUE)
// 为1时表示当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeInt(userId);
parcel.writeString(userName);
// 注意这里,并不是直接写入boolean值,而是写入整数值
parcel.writeByte((byte) (isMale ? 1 : 0));
// 此为不同之处2
parcel.writeParcelable(book, i);
}
@Override
public String toString() {
return "User{ " +
"userId = " + userId +
", userName = " + userName +
", isMale = " + isMale +
"book = " + book.toString() +
" }";
}
}</code></pre>
<p>可以看出,结果已经正确的打印了出来了:</p>
<p><img src="/img/bVGkpx?w=1170&h=161" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>注意:在 Parcelable 中,我们无法直接写入 boolean 值,而是将其转化为整数值进行保存,这里为 Byte,当然,你也可以使用 Int 等。</strong></p>
Android Studio下应用签名的方法以及获取 MD5、SHA1(签名)、SHA256 值
https://segmentfault.com/a/1190000007579841
2016-11-23T20:18:45+08:00
2016-11-23T20:18:45+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>在开发中我们可能经常需要用到一些第三方公司的SDK,比如:百度地图SDK、腾讯的分享朋友圈的SDk等,我们使用这些SDK有个前提就是我们需要将我们应用的签名和包名添加进去,跟我们在这些第三方SDK公司网站里创建的应用联系起来(我们需要在里面填写我们的应用签名)</p>
<h2>1.生成签名</h2>
<p>首先,选择:Build->Generate Signed APK</p>
<p><img src="/img/bVFX4O?w=409&h=407" alt="clipboard.png" title="clipboard.png"></p>
<p>弹出如下的弹窗:</p>
<p><img src="/img/bVFX4Z?w=444&h=308" alt="clipboard.png" title="clipboard.png"></p>
<p>点击Create New 按钮,出现下面的对话框:</p>
<p><img src="/img/bVFX46?w=527&h=502" alt="clipboard.png" title="clipboard.png"></p>
<p>其中Alias为别名,对于下面“Certificate”的那几行,我们至少需要填一个,上面的都要填写清楚,重要的是要记清楚第二行输入的密码,之后需要用到。之后一路点击“OK”即可,这时候就会生成发布版的apk了:</p>
<p><img src="/img/bVFX51?w=212&h=210" alt="clipboard.png" title="clipboard.png"><br>上面的步骤只是生成了签名证书,我们还要用该证书对应用进行签名。<br>接下来我们需要在Moudle的build.gradle文件中添加下面的信息(将其中的内容替换为我们刚才填写的内容)</p>
<pre><code> signingConfigs {
config {
keyAlias 'friendsshare'
keyPassword '123456789'
storeFile file('E:/myfriendsshare.jks')
storePassword '123456789'
}
}</code></pre>
<p>上面代码的目的是:使用这里填写的别名(keyAlias)和密码,与签名证书里面的信息进行比对,如果成功,则表示我们是该证书的所有者,有权限使用其进行签名,这点是在进行签名的时候进行比对的。<br>我们可以在下图选择我们点击 “Run” 按钮时去 build 的类型,是“debug” 还是 “release”</p>
<p><img src="/img/bVNDl8?w=280&h=513" alt="clipboard.png" title="clipboard.png"><br>如果我们没有配置 “release”的话,在选择构建类型里选择 “release” 的时候会报如下错误(没配置的情况下选择“debug”不会报错,因为AS默认为我们创建了一个debug的签名,注意:更改过配置的话,最好重新 build 下项目,否则有时候会报错。):</p>
<p><img src="/img/bVNDn3?w=412&h=417" alt="clipboard.png" title="clipboard.png"></p>
<p>在上面配置里面任何一项都不能出错。如果出错,对应的 build 类型就无法通过。如果keyAlias错误的话,会报下面的错误:</p>
<p><img src="/img/bVNDlI?w=1156&h=55" alt="clipboard.png" title="clipboard.png"></p>
<p>如果其他两个密码有错误的话,会报如下错误:</p>
<p><img src="/img/bVNDlD?w=1138&h=84" alt="clipboard.png" title="clipboard.png"></p>
<p>当然,这里我们完全可以不用自己手动输入,可以采用下面的方法直接填写即可自动生成:<br>在项目上直接右键,选择Open Moudle Settings,点击红色框内的添加,填写刚才的信息,点击“OK”即可:</p>
<p><img src="/img/bVFX7a?w=285&h=529" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVFX7A?w=817&h=699" alt="clipboard.png" title="clipboard.png"></p>
<h2>2.查询签名</h2>
<p>在Android Studio下,我们可以在命令行中通过输入命令来查询我们的应用签名信息:</p>
<p><img src="/img/bVFX2f?w=323&h=103" alt="clipboard.png" title="clipboard.png"></p>
<p>查询命令为:</p>
<pre><code>keytool -list -v -keystore "E:\myfriendsshare.jks"</code></pre>
<p>其中<code>"E:\myfriendsshare.jks"</code>为我们刚才保存的签名相关文件的位置,在这里你直接替换成自己的文件位置即可。之后它会让你输入密码(注意,密码不会显示出来,输入之后点击回车就好)<br>查询结果如下:</p>
<p><img src="/img/bVFX4C?w=1354&h=1227" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>其中SHA1的值即为应用的签名</strong></p>
ViewPager实现轮播广告图
https://segmentfault.com/a/1190000007562144
2016-11-22T14:30:53+08:00
2016-11-22T14:30:53+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>轮播广告在现在的应用中比较常见,下面就来实现下该功能(文章参考了网上流传的黑马的视频教程)<br>先来看下具体的实现效果:</p>
<p><img src="/img/bVFTdE?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p>实现思路:<br>1.为ViewPager设置数据源,实现ViewPager的滚动<br>2.将圆点指示器与ViewPager的页面对应起来<br>3.实现左右滑动均能无限循环<br>4.实现自动播放<br>5.实现当手指滑动的时候取消自动播发</p>
<p><strong>首先需要说明的就是在这篇文章里并没有同时实现左右循环滑动和手指触碰自动停止的功能,这个问题还没有解决,留待以后再解决,也希望有知道的朋友在下面留言,先谢谢拉。</strong></p>
<h2>1.首先我们定义布局文件,用来显示Banner</h2>
<p>这里我们采用对的是相对布局RelativeLayout,当然也可以采用帧布局FrameLayout。布局文件activity_mian.xml代码:</p>
<pre><code><?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.qc.admin.mylunbotu.MainActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="160dp">
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v4.view.ViewPager>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:background="#66000000"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="这是图片描述"
android:textColor="@android:color/white" />
<!--用来填充圆点指示器的容器-->
<LinearLayout
android:id="@+id/point_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</code></pre>
<h2>2.初始化圆点指示器</h2>
<p>这里我们提供的图片数据和显示的文本数据都是静态的,直接从数组资源中获得的,如:</p>
<pre><code> //图片资源id数组
imageResIds = new int[]{R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e};
// 文本描述
contentDescs = new String[]{
"巩俐不低俗,我就不能低俗",
"扑树又回来啦!再唱经典老歌引万人大合唱",
"揭秘北京电影如何升级",
"乐视网TV版大派送",
"热血屌丝的反杀"
};</code></pre>
<p>我们可以自己定义两个shape,来显示圆点指示器的两种不同显示状态,然后再定义一个selector,将它设置为控件的背景(ImageView),通过其setEnable(Boolean bool)方法可以自动的决定使用哪个图片。代码分别如下:</p>
<p>a.正常状态下,灰色圆点,point_normal.xml:</p>
<pre><code><?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="5dp"
android:height="5dp" />
<solid android:color="#44000000" />
</shape></code></pre>
<p>b.点击状态下,白色圆点 point_pressed.xml:</p>
<pre><code><?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size android:width="5dp" android:height="5dp"/>
<solid android:color="#FFFFFFFF"/>
</shape></code></pre>
<p>c.设置背景时的xml文件, point_bg.xml:</p>
<pre><code><selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:drawable="@drawable/point_pressed"/>
<item android:state_enabled="false" android:drawable="@drawable/point_normal"/>
</selector></code></pre>
<p>具体代码:</p>
<pre><code> private void initData() {
//图片资源id数组
imageResIds = new int[]{R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e};
// 文本描述
contentDescs = new String[]{
"巩俐不低俗,我就不能低俗",
"扑树又回来啦!再唱经典老歌引万人大合唱",
"揭秘北京电影如何升级",
"乐视网TV版大派送",
"热血屌丝的反杀"
};
imageViewList = new ArrayList<>();
LinearLayout.LayoutParams mParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
mParams.leftMargin = 15;
mParams.topMargin = 2;
ImageView imageView;
ImageView pointView;
//初始化要展示的ImageView,并添加圆点指示器
for (int i = 0; i < imageResIds.length; i++) {
// 初始化图片
imageView = new ImageView(this);
imageView.setBackgroundResource(imageResIds[i]);
imageViewList.add(imageView);
// 初始化指示器
pointView = new ImageView(this);
pointView.setBackgroundResource(R.drawable.point_bg);
pointView.setLayoutParams(mParams);
if (i == 0) {
textView.setText(contentDescs[0]);
pointView.setEnabled(true);
} else {
pointView.setEnabled(false);
}
mPointsLayout.addView(pointView);
}
}</code></pre>
<p>注意:创建shape文件的时候,如果找不到,可以切换到project目录下,在drawable目录下右键,new->Drawable resource file ,得到如下界面:</p>
<p><img src="/img/bVFTgR?w=281&h=159" alt="clipboard.png" title="clipboard.png"></p>
<p>将selector直接改为shape即可</p>
<h2>3.为ViewPager设置适配器</h2>
<pre><code> class MyAdapter extends PagerAdapter {
// 1.返回条目的总数
@Override
public int getCount() {
//return imageViewList.size();
return Integer.MAX_VALUE;
}
// 2.返回要显示的条目,并创建条目
@Override
public Object instantiateItem(ViewGroup container, int position) {
//container:容器,其实也就是ViewPager
//position:当前要显示的条目的位置
int newPosition = position % imageViewList.size();
ImageView imageView = imageViewList.get(newPosition);
//a.将View对象添加到container容器中
container.addView(imageView);
//b.把View对象返回给框架,适配器
return imageView;
}
// 3.销毁条目,其实就是将要销毁的对象object从container中移除出去就好了
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
// 4.指定复用的判断逻辑(一般为固定写法)
@Override
public boolean isViewFromObject(View view, Object object) {
// 当滑动到新的条目之后,又返回回来,view是否可以被复用
return view == object;
}
}
</code></pre>
<p>在这里我们实现的是伪无限循环,其实就是将我们的第一项设置为一个很大的数的中间位置(Integer.MAX_VALUE),这样当我们向左向右滑动的时候,将返回的position对数据的大小进行取模运算%,根据相应的位置设置相应的图片或者文字即可。这样就实现了向右向左都是无限循环了。</p>
<h2>4.实现自动播放</h2>
<p>我们当然可以直接开启一个线程,在里面设置ViewPager的当前项,但是直接使用Handler更便于进行控机制,因为我们可以为ViewPager设置滚动监听器。<br>自动播放控制代码:</p>
<pre><code> private void startRun() {
mHandler = new Handler();
mHandler.postDelayed(mTaskRunnable, delayMillis);
}
//该线程一直运行着,知道activity被销毁,此时将isActivityAlive设置为false
final Runnable mTaskRunnable = new Runnable() {
@Override
public void run() {
// 如果activity未被销毁,就一直执行该线程
// 在ViewPager的OnPageChangeListener方法中决定是否将isAutoRun置反
if (isActivityAlive) {
if (isAutoRun) {
viewPager.setCurrentItem((viewPager.getCurrentItem() + 1) % imageViewList.size());
mHandler.postDelayed(mTaskRunnable, delayMillis);
} else {
mHandler.postDelayed(mTaskRunnable, delayMillis);
}
}
}
};</code></pre>
<p>为ViewPager设置滚动监听器,这里我们直接让当前类实现ViewPager.OnPageChangeListener接口,实现其中的抽象方法:</p>
<pre><code> @Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
int newPosition = position % imageViewList.size();
textView.setText(contentDescs[newPosition]);
// 先将上一个置位false,将当前位置置位true,这样可以使得初始化的时候就在第一个位置
// (因为previousSelectedItem的未赋值时候的初始值默认为0)
mPointsLayout.getChildAt(previousSelectedItem).setEnabled(false);
mPointsLayout.getChildAt(newPosition).setEnabled(true);
previousSelectedItem = newPosition;
}
@Override
public void onPageScrollStateChanged(int state) {
switch (state) {
// 静止状态
case SCROLL_STATE_IDLE:
isAutoRun = true;
break;
// 拖拽中
case SCROLL_STATE_DRAGGING:
isAutoRun = false;
break;
// 拖拽后松手,自动回到最终位置的过程
case SCROLL_STATE_SETTLING:
isAutoRun = true;
break;
}
}</code></pre>
<p>这里面我们就实现了当手指滑动的时候停止自动播放,当手指抬起的时候又开始了自动播放。但是,<strong>注意</strong>,你以为这些代码组合起来就能实现循环滚动的同时还能手指控制是否自动播放?好吧,我当时也是这样以为的,运行后会报错:</p>
<pre><code>java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.</code></pre>
<p><img src="/img/bVFTnJ?w=1162&h=433" alt="clipboard.png" title="clipboard.png"><br>意思是我们待加入的子视图已经有了一个父视图,需要调用它的父视图的removeView()方法来将其移除,之后再添加,然而事实证明并没有什么卵用。留待以后解决吧,在只能牺牲掉无线循环了,这里我们需要将getCount方法中的返回值改成我们数据的大小就好了:</p>
<pre><code> @Override
public int getCount() {
return imageViewList.size();
// return Integer.MAX_VALUE;
}</code></pre>
<p>然后将之前设置的默认的第一项:</p>
<pre><code> private void initAdapter() {
int firstPosition = Integer.MAX_VALUE / 2 - (Integer.MAX_VALUE / 2 % imageViewList.size());
//viewPager.setOffscreenPageLimit(imageViewList.size());
viewPager.setAdapter(new MyAdapter());
// 设置从中间的某个位置开始滑动,从而能够实现向左向右的循环滑动
viewPager.setCurrentItem(firstPosition);
}</code></pre>
<p>改为:</p>
<pre><code> private void initAdapter() {
viewPager.setAdapter(new MyAdapter());
// 设置从中间的某个位置开始滑动,从而能够实现向左向右的循环滑动
viewPager.setCurrentItem(0);
}</code></pre>
<p>可能说了这么多,看的都混了,在最后就贴一下完整的源码吧(支持自动播放、手指控制,但是不支持左右无限循环)。</p>
<pre><code>package com.qc.admin.mylunbotu;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
public class MainActivity extends AppCompatActivity implements ViewPager.OnPageChangeListener {
private ViewPager viewPager;
private int[] imageResIds;
private List<ImageView> imageViewList;
private LinearLayout mPointsLayout;
private String[] contentDescs;
private int previousSelectedItem;
private TextView textView;
private Handler mHandler;
boolean isAutoRun = true;
boolean isActivityAlive = true;
private int delayMillis = 2000;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化视图
initViews();
//初始化数据
initData();
//初始化适配器
initAdapter();
//开始自动播放
startRun();
}
private void startRun() {
mHandler = new Handler();
mHandler.postDelayed(mTaskRunnable, delayMillis);
}
//该线程一直运行着,知道activity被销毁,此时将isActivityAlive设置为false
final Runnable mTaskRunnable = new Runnable() {
@Override
public void run() {
// 如果activity未被销毁,就一直执行该线程
// 在ViewPager的OnPageChangeListener方法中决定是否将isAutoRun置反
if (isActivityAlive) {
if (isAutoRun) {
viewPager.setCurrentItem((viewPager.getCurrentItem() + 1) % imageViewList.size());
mHandler.postDelayed(mTaskRunnable, delayMillis);
} else {
mHandler.postDelayed(mTaskRunnable, delayMillis);
}
}
}
};
private void initAdapter() {
//int firstPosition = Integer.MAX_VALUE / 2 - (Integer.MAX_VALUE / 2 % imageViewList.size());
//viewPager.setOffscreenPageLimit(imageViewList.size());
viewPager.setAdapter(new MyAdapter());
// 设置从中间的某个位置开始滑动,从而能够实现向左向右的循环滑动
//viewPager.setCurrentItem(firstPosition);
viewPager.setCurrentItem(0);
}
private void initData() {
//图片资源id数组
imageResIds = new int[]{R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e};
// 文本描述
contentDescs = new String[]{
"巩俐不低俗,我就不能低俗",
"扑树又回来啦!再唱经典老歌引万人大合唱",
"揭秘北京电影如何升级",
"乐视网TV版大派送",
"热血屌丝的反杀"
};
imageViewList = new ArrayList<>();
LinearLayout.LayoutParams mParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
mParams.leftMargin = 15;
mParams.topMargin = 2;
ImageView imageView;
ImageView pointView;
//初始化要展示的ImageView,并添加圆点指示器
for (int i = 0; i < imageResIds.length; i++) {
// 初始化图片
imageView = new ImageView(this);
imageView.setBackgroundResource(imageResIds[i]);
imageViewList.add(imageView);
// 初始化指示器
pointView = new ImageView(this);
pointView.setBackgroundResource(R.drawable.point_bg);
pointView.setLayoutParams(mParams);
if (i == 0) {
textView.setText(contentDescs[0]);
pointView.setEnabled(true);
} else {
pointView.setEnabled(false);
}
mPointsLayout.addView(pointView);
}
}
private void initViews() {
viewPager = (ViewPager) findViewById(R.id.viewpager);
viewPager.addOnPageChangeListener(this);
textView = (TextView) findViewById(R.id.tv_desc);
// 用来添加圆点指示器
mPointsLayout = (LinearLayout) findViewById(R.id.point_container);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
int newPosition = position % imageViewList.size();
textView.setText(contentDescs[newPosition]);
// 先将上一个置位false,将当前位置置位true,这样可以使得初始化的时候就在第一个位置
// (因为previousSelectedItem的未赋值时候的初始值默认为0)
mPointsLayout.getChildAt(previousSelectedItem).setEnabled(false);
mPointsLayout.getChildAt(newPosition).setEnabled(true);
previousSelectedItem = newPosition;
}
@Override
public void onPageScrollStateChanged(int state) {
switch (state) {
// 静止状态
case SCROLL_STATE_IDLE:
isAutoRun = true;
break;
// 拖拽中
case SCROLL_STATE_DRAGGING:
isAutoRun = false;
break;
// 拖拽后松手,自动回到最终位置的过程
case SCROLL_STATE_SETTLING:
isAutoRun = true;
break;
}
}
// 创建一个MyAdapter类,继承自PagerAdapter来为ViewPager设置适配器
class MyAdapter extends PagerAdapter {
// 1.返回条目的总数
@Override
public int getCount() {
return imageViewList.size();
// return Integer.MAX_VALUE;
}
// 2.返回要显示的条目,并创建条目
@Override
public Object instantiateItem(ViewGroup container, int position) {
//container:容器,其实也就是ViewPager
//position:当前要显示的条目的位置
int newPosition = position % imageViewList.size();
ImageView imageView = imageViewList.get(newPosition);
//a.将View对象添加到container容器中
container.addView(imageView);
//b.把View对象返回给框架,适配器
return imageView;
}
// 3.销毁条目,其实就是将要销毁的对象object从container中移除出去就好了
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
// 4.指定复用的判断逻辑(一般为固定写法)
@Override
public boolean isViewFromObject(View view, Object object) {
// 当滑动到新的条目之后,又返回回来,view是否可以被复用
return view == object;
}
}
@Override
protected void onDestroy() {
isActivityAlive = false;
super.onDestroy();
}
}
</code></pre>
使用Java输出杨辉三角
https://segmentfault.com/a/1190000007466619
2016-11-13T11:36:22+08:00
2016-11-13T11:36:22+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<p>该篇文章用于输出杨辉三角,杨辉三角的定就是里面的某一个数等于其上边紧邻的两个数的和,效果如下:</p>
<pre><code> 1
1 1
1 2 1
1 3 3 1
1 4 6 4 1 </code></pre>
<p>具体代码:</p>
<pre><code>public void yanghuiFun() {
System.out.println("请输入需要打印的行数:");
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt();
if (num > 0) {
int[][] a = new int[num][num];
// 将每行的第一个数和最后一个数都赋为1
for (int i = 0; i < num; i++) {
a[i][0] = 1;
a[i][i] = 1;
}
// 当行数大于2的时候就可以使用递推公式
if (num > 2) {
// 依次将中间某个数的值赋为其上面紧邻着的两个数的和
for (int i = 2; i < num; i++) {
for (int j = 1; j < num - 1; j++) {
a[i][j] = a[i - 1][j - 1] + a[i - 1][j];
}
}
}
// 依次输出这些数
for (int i = 0; i < num; i++) {
// 输出数字前的空格,每行输出的空格数量为:num-1-i
for (int j = i; j < num - 1; j++) {
System.out.print(" ");
}
// 开始输出具体的数字以及数字之间的空格
for (int j = 0; j < i + 1; j++) {
System.out.print(a[i][j] + " ");
}
System.out.println();
}
}
}</code></pre>
单例模式
https://segmentfault.com/a/1190000007286846
2016-10-26T16:39:09+08:00
2016-10-26T16:39:09+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<h2>1.饿汉式单例模式</h2><p>所谓饿汉式单例模式,是指当类加载的时候就去创建该类的实例</p><pre><code>public class Singleton {
//访问修饰符需要设置为 private
private static Singleton instance = new Singleton();
//构造方法需要设置为 private
private Singleton(){
}
//访问修饰符需要设置为 public
public static Singleton getInstance(){
return instance;
}
}</code></pre><h2>2.懒汉式单例模式</h2><p>所谓懒汉式单例模式,是指我们只有在需要获取时才创建该类的实例,而饿汉式单例模式是在类加载的过程中就创建该类的实例。</p><pre><code>public class Singleton {
//访问修饰符需要设置为 private
private static Singleton instance = null;
//构造方法需要设置为 private
private Singleton(){
}
//访问修饰符需要设置为 public
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}</code></pre><p><strong>注意:</strong>在单线程环境下,上面两个单例模式都可以正常工作,但是当处于多线程环境下,仍然可能产生多个实例,这就违背了单例模式的设计初衷,这时候,就需要用到锁的机制,性能最好的,当然就是接下来要介绍的双检测锁机制的单例模式啦。</p><h2>3.双检测锁机制的单例模式</h2><pre><code>public class Singleton {
//访问修饰符需要设置为 private,并且是 volatile 的
private static volatile Singleton instance = null;
//构造方法需要设置为 private
private Singleton(){
}
//访问修饰符需要设置为 public
public static Singleton getInstance(){
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}</code></pre>
JPush消息推送的简单使用
https://segmentfault.com/a/1190000007282811
2016-10-26T11:29:48+08:00
2016-10-26T11:29:48+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<p>在本篇文章里我们以极光推送JPush为例,简单的介绍下消息推送的简单使用。<br>这里只是简单介绍下步骤及简单使用,作为本人的笔记,详细介绍可以参见极光官网文档:<br><a href="https://link.segmentfault.com/?enc=BC%2F8aaeB9R7LzL88%2FQCi1w%3D%3D.Ur0jDg%2FcHid3zccvh9Dq7FvZi2y7ScM82Uu5ganAw8yXIOLf4j2LAMRU7UavgYgLgT7o3DbQkmSPItYO80l%2FJg%3D%3D" rel="nofollow">http://docs.jiguang.cn/jpush/...</a></p>
<h2>1.申请开发者帐号</h2>
<p>在官网注册帐号,并进入后控制台</p>
<p><img src="/img/bVEIuZ?w=1295&h=614" alt="clipboard.png" title="clipboard.png"></p>
<h2>2.在自己的项目中进行配置(IDE为AndroidStudio)</h2>
<p>官网说有两种配置方法,一种是手动集成,一种是jcenter 自动集成步骤,而后者比较简单方便,一次我们就直接用后面一种,免去了繁琐的拷贝和修改,具体方法如下:</p>
<p>1.确认android studio的 Project 根目录的主 gradle 中配置了jcenter支持。(新建project默认配置就支持)</p>
<pre><code>buildscript {
repositories {
jcenter()
}
......
}
allprojets {
repositories {
jcenter()
}
}</code></pre>
<p>2.在 module 的 gradle 中添加依赖和AndroidManifest的替换变量。</p>
<pre><code>android {
......
defaultConfig {
applicationId "com.xxx.xxx" //JPush上注册的包名.
......
ndk {
//选择要添加的对应cpu类型的.so库。
abiFilters 'armeabi', 'armeabi-v7a', 'armeabi-v8a'
// 还可以添加 'x86', 'x86_64', 'mips', 'mips64'
}
manifestPlaceholders = [
JPUSH_PKGNAME : applicationId,
JPUSH_APPKEY : "你的appkey", //JPush上注册的包名对应的appkey.
JPUSH_CHANNEL : "developer-default", //暂时填写默认值即可.
]
......
}
......
}
dependencies {
......
compile 'cn.jiguang:jpush:2.2.0' // 此处以SDK 2.2.0版本为例
......
}</code></pre>
<p>这里我们只需要对其中的值根据我们的具体情况进行修改即可,其他的东西AS都会为我们自动配置好,我们不需要再在AndroidManifest.xml中手动的进行权限等等的配置了。其实我们仔细查找的话可以发现另外一个AndroidManifest.xml,它的路径为:<br>...项目名appbuildintermediatesmanifestsinstantrundebugAndroidManifest.xml,其中的内容如下,只不过在具体项目里,其中的需要替换的变量值,AS已经为我们自动替换了:</p>
<p><img src="/img/bVEIDs?w=377&h=581" alt="clipboard.png" title="clipboard.png"></p>
<pre><code><?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="您应用的包名"
android:versionCode="216"
android:versionName="2.1.6"
>
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="23" />
<!-- Required -->
<permission
android:name="您应用的包名.permission.JPUSH_MESSAGE"
android:protectionLevel="signature" />
<!-- Required -->
<uses-permission android:name="您应用的包名.permission.JPUSH_MESSAGE" />
<uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Optional. Required for location feature -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.GET_TASKS" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:name="Your Application">
<!-- Required SDK 核心功能-->
<!-- option since 2.0.5 可配置PushService,DaemonService,PushReceiver,AlarmReceiver的android:process参数 将JPush相关组件设置为一个独立进程 -->
<!-- 如:android:process=":remote" -->
<service
android:name="cn.jpush.android.service.PushService"
android:enabled="true"
android:exported="false" >
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTER" />
<action android:name="cn.jpush.android.intent.REPORT" />
<action android:name="cn.jpush.android.intent.PushService" />
<action android:name="cn.jpush.android.intent.PUSH_TIME" />
</intent-filter>
</service>
<!-- since 1.8.0 option 可选项。用于同一设备中不同应用的JPush服务相互拉起的功能。 -->
<!-- 若不启用该功能可删除该组件,将不拉起其他应用也不能被其他应用拉起 -->
<service
android:name="cn.jpush.android.service.DaemonService"
android:enabled="true"
android:exported="true">
<intent-filter >
<action android:name="cn.jpush.android.intent.DaemonService" />
<category android:name="您应用的包名"/>
</intent-filter>
</service>
<!-- Required -->
<receiver
android:name="cn.jpush.android.service.PushReceiver"
android:enabled="true" >
<intent-filter android:priority="1000">
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" />
<category android:name="您应用的包名"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
<!-- Optional -->
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<!-- Required SDK核心功能-->
<activity
android:name="cn.jpush.android.ui.PushActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false" >
<intent-filter>
<action android:name="cn.jpush.android.ui.PushActivity" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="您应用的包名" />
</intent-filter>
</activity>
<!-- Required SDK核心功能-->
<service
android:name="cn.jpush.android.service.DownloadService"
android:enabled="true"
android:exported="false" >
</service>
<!-- Required SDK核心功能-->
<receiver android:name="cn.jpush.android.service.AlarmReceiver" />
<!-- User defined. 用户自定义的广播接收器-->
<receiver
android:name="您自己定义的Receiver"
android:enabled="true">
<intent-filter>
<!--Required 用户注册SDK的intent-->
<action android:name="cn.jpush.android.intent.REGISTRATION" />
<!--Required 用户接收SDK消息的intent-->
<action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" />
<!--Required 用户接收SDK通知栏信息的intent-->
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" />
<!--Required 用户打开自定义通知栏的intent-->
<action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" />
<!-- 接收网络变化 连接/断开 since 1.6.3 -->
<action android:name="cn.jpush.android.intent.CONNECTION" />
<category android:name="您应用的包名" />
</intent-filter>
</receiver>
<!-- Required. For publish channel feature -->
<!-- JPUSH_CHANNEL 是为了方便开发者统计APK分发渠道。-->
<!-- 例如: -->
<!-- 发到 Google Play 的APK可以设置为 google-play; -->
<!-- 发到其他市场的 APK 可以设置为 xxx-market。 -->
<!-- 目前这个渠道统计功能的报表还未开放。-->
<meta-data android:name="JPUSH_CHANNEL" android:value="developer-default"/>
<!-- Required. AppKey copied from Portal -->
<meta-data android:name="JPUSH_APPKEY" android:value="Your AppKey"/>
</application>
</manifest>
配置和代码说明</code></pre>
<p>可以看到,采用jcenter 自动集成非常简单</p>
<h2>3.在代码中初始化</h2>
<p>我们只需要初始化一次就好,因此我们可以放在Activity的onCreate方法中,具体代码:</p>
<pre><code>public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//设置为调试模式,具体发布的时候可以直接设置为false
JPushInterface.setDebugMode(true);
JPushInterface.init(this);
}
}</code></pre>
<p>到这里为止,我们就可以想手机推送通知了,用法就是在极光官网的控制台里写下需要推送的通知,发送即可。<br>推送有两种:<br>1.Notification,即通知,手机会有通知弹出<br>2.Message,即消息,推送之后我们看不到效果,但是可以通过SDK获得发送过来的消息,其被封装进了Intent中,也就是发送一个广播,我们只需要定义自己的广播接收器BroadcastReceiver即可,当然,我们也可以监听Notification的事件。</p>
<h2>4.定义自己的广播接收器BroadcastReceiver对推送事件进行响应</h2>
<p>这里就比较简单了,我们创建一个类,继承自BroadcastReceiver,然后在它的onReceive方法中进行具体的逻辑操作,这里我们只是简单的将推送过来的消息内容打印到Log日志中,代码如下:</p>
<pre><code>public class MyReceiver extends BroadcastReceiver {
private static final String TAG = "test" ;
public MyReceiver() {
}
@Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
String title = bundle.getString(JPushInterface.EXTRA_TITLE);
String message = bundle.getString(JPushInterface.EXTRA_MESSAGE);
Log.i(TAG,"MyReceiver接收到的消息内容: title: "+ title + " message: " + message);
}
}</code></pre>
<p>当然,不要忘记在AndroidManifest.xml文件中对我们自定义的那个BroadcastReceiver进行配置:</p>
<pre><code> <receiver
android:name=".MyReceiver"
android:enabled="true">
<intent-filter>
<action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" />
<category android:name="我们的应用的包名" />
</intent-filter>
</receiver></code></pre>
<p>注意:<category android:name="com.qc.admin.jpushtest" />的name属性值使我们的应用的包名,而action是SDK中定义的一系列动作。具体为:</p>
<pre><code><receiver
android:name="Your Receiver"
android:enabled="true">
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTRATION" />
<action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" />
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" />
<action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" />
<category android:name="You package Name" />
</intent-filter>
</receiver></code></pre>
<p>我们在其中选择我们关心的Action即可,至于其他的设置标签或者设备号之类的,以后再补充吧。</p>
常用基本排序算法
https://segmentfault.com/a/1190000006933073
2016-09-18T19:43:57+08:00
2016-09-18T19:43:57+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<p><img src="/img/bVDfEQ" alt="clipboard.png" title="clipboard.png"></p><p>图片截取自:<a href="https://link.segmentfault.com/?enc=MbijNYda5t99WTbvrRXNfg%3D%3D.tjvWzE7HdqKCutaTDAUIFePg8LpP7C67u6HGn7f3jJF6nfpWpBanJufgDxUb08BzReULG5ezJrCIiLsueBzePg%3D%3D" rel="nofollow">http://blog.csdn.net/qy1387/a...</a></p><p>算法复杂度:</p><p><img src="/img/bVDg8o" alt="clipboard.png" title="clipboard.png"></p><p>图片截取自:<a href="https://link.segmentfault.com/?enc=cP2QOve5%2BLqoXmPxxbaJUw%3D%3D.kIwBZb9l4gNGbPc5nPOIwlrW0MS9Ojyy6mcVMSCuVv5zI4nWRzjFDBqagJKCbjykDJiBKH17XLlolrUqiKs9bV2vr1lUlzIS7SiRqBYFkmA%3D" rel="nofollow">https://github.com/JackyAndro...</a></p><p>文章参考了:<br><a href="https://link.segmentfault.com/?enc=Y%2B7bvCjj88vnjl3fb3Ie%2Fg%3D%3D.VlneBt83cGMGm6JamF6aKADFmtPGPtHnooNRSP4DtAkIUPjzPf1EDBUeuJnkgIwcZndOsOhi945QrkbiZM4Itg%3D%3D" rel="nofollow">http://blog.csdn.net/qy1387/a...</a><br><a href="https://link.segmentfault.com/?enc=qRtXC6a0L9TjKnHqY8g1Cw%3D%3D.yPEz8VuschG5l%2F7U4ozE1DXUKTOaEWmE84rFhLE7gn%2BBAhn55paEX5G9HQK4OEeYhqEB3rWzI6kVbZNgk%2B%2ByGg%3D%3D" rel="nofollow">http://blog.csdn.net/zdp072/a...</a><br><a href="https://link.segmentfault.com/?enc=6MkhIC4IslHbT80yoOgBVA%3D%3D.2mhO5cnfQt7XRCLQZUQ0u0fZ1z1V1hyXudXl%2BS5J5yZC1OJxz4F%2F23Kf5H964p776aOSKD0Nxj2ERbXpmtexkg%3D%3D" rel="nofollow">http://blog.csdn.net/morewind...</a></p><p>所需辅助空间最多:归并排序<br>所需辅助空间最少:堆排序<br>平均速度最快:快速排序<br>不稳定:快速排序,希尔排序,堆排序。</p><h2>1.冒泡排序</h2><p>冒泡排序应该属于最简单的排序算法了。冒泡排序其实就是通过比较相邻位置的元素大小,如果左边比右边大,就交换位置,继续比较,实际上就是每轮比较都得出一个最大值(或者最小值)。然后通过n-1轮比较,就能得出一个排好序的序列(通过设置一个flag,当数组基本有序的时候其实不一定需要比较到n-1轮)。</p><p>具体代码:</p><pre><code> // 1.冒泡排序
// 其实就是通过比较相邻位置的元素大小,如果左边比右边大,就交换位置,继续比较,实际上就是每轮比较都得出一个最大值(或者最小值)
public static void bubbleSort(int[] a) {
// 因此如果数组有n个元素的话,也就是说a.length值为n,那么我们只需要比较n-1次就能排好序,所以当
for (int i = 0; i < a.length-1; i++) {
// 这里j为元素的下标,而我们在每轮比较的时候需要比较的最后一个元素的下标为n-i,也就是a.length-i
for (int j = 0; j < a.length-i-1; j++) {
// 进行两个相邻具体元素的比较,如果前者比后者大,就将二者交换,类似“冒泡”效果(其实处为“下沉”)
int temp;
if (a[j] > a[j + 1]) {
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
</code></pre><h2>2.快速排序</h2><p>快速排序简单来讲就是我们选定一个数来作为比较基准值(往往是最左边或最右边的数),然后比它小的都放在它左边,大于等于它的都放在它右边,那么这个时候对这个数来讲他的位置已经排到了正确的地方了,而在这个“比较与放”的过程中,其实就涉及到这个“比较基准值”与所比较的数之前的位置调换的过程。接下来要做的就是在它的左右两边分别再进行类似操作。其实相当于先把基准值这个数拿出来,通过与其他值的比较,最终找到它应该放在的位置,然后放进去就行了。这点类似农村施肥的时候,会先铲出一个坑,然后不断的用新铲出来的坑里面的土去填平前一个放了肥料的坑。只不过这里顺序是跳跃的,而施肥的时候是线性的而已。最后再把第一铲子里的土放到该放的位置即可。</p><p>具体代码如下:</p><pre><code>// 2.快速排序
// a为待排序的数组,l,r为数组下标(l:left,r:right)
// 该函数的作用是对数组a[]从下标“l”到下标“r”这部分进行排序
// 注意,当我们调用此方法的时候1应该传0,r应该传a.length - 1
public static void quickSort(int[] a, int l, int r) {
if(a==null || a.length<=1 || l<0 || r<0 || r>=a.length || l>=r){
return;
}
int i = l, j = r, x = a[i];
// 跳出循环的条件是i == j;
// 此时已经找到了数值x应该被插入的位置(满足其坐标你的数都比它小,右边的数都比它大)
// 这个 while 里每次循环,其实只是进行了一次从左到右和从右到左的替换,只是一个回合
while (i < j) {
// 从右往左寻找第一个小于x的那个数
while (i < j && a[j] >= x) {
j--;
}
a[i] = a[j];
// 从左往右寻找第一个大于等于x的那个数
while (i < j && a[i] <= x) {
i++;
}
a[j] = a[i];
}
// 将数值x插入到其应该位于的位置(此时i==j,当然也可以用a[j] = x)
a[i] = x;
// 现在我们只是找到了一个数所属的位置,接下来我们需要在其两边分别再次进行类似寻找
// 进行递归调用
quickSort(a, i + 1, r);
quickSort(a, l, i - 1);
}
</code></pre><h2>3.直接插入排序</h2><p>直接插入排序就是我们不断的将数插入一个已经排好序的数列中,形成新的数列,当我们从第一个(准确讲应该是从第二个数)开始不断插入,直到把所有的数都插入了进去,这是我们就得到了一个有序的数列,或者说是数组。</p><p>具体代码:</p><pre><code>// 3.直接插入排序
// 其实就是将元素一个个插入一个已经排好序了的序列中,形成新的有序序列,
// 刚开始排序的时候,我们实际上认为只有一个元素的时候其本身是有序的,
// 通过比较第二个元素与第一个元素的大小来决定插入其左边或者右边,以此类推...
public static void insertSort(int[] a) {
// 用于记录下当前需要插入的元素a[i]的值
int temp;
// 要插入的元素下标为i,我们从i为1的元素开始插入,一直到将最后一个元素(下标为n-1)插入进去
for (int i = 1; i < a.length; i++) {
temp = a[i];
int j ;
// 如果遇到比我们要插入的元素的值大的元素,那么我们就将它向右移动1位
// (这时它自身的位置就会空出来,而这个位置就用来插入另一个比我们待插入元素值大的元素或者直接就是我们的待插入元素)
for (j = i - 1; j >= 0 && a[j] > temp; j--) {
a[j + 1] = a[j];
}
// 将temp赋给j+1,是因为在循环中有“j--”,当不满足的时候其实下标j已经个指向了我们需要插入位置的前一个元素了
a[j + 1] = temp;
}
}
</code></pre><h2>4.希尔排序</h2><p>希尔排序又称为减小增量排序,实际上就是“分组+直接插入排序”,我们将一组数组分成一定的组,然后每组分别进行直接插入排序,这样对每个组而言得到的都是排好了序的数列,然后我们再将分的组的数量逐渐减小,这样每个组内相邻元素的间隔也会减小(组数=组距),当然,这里的组数比较通常的叫法叫“步长”,或者说是“gap”.组数会越来越小,当最后只有一组的时候,我们在经过一轮比较之后就将整个数组排好序了(注意,当组距为1的时候还没有排好)</p><p>具体代码:</p><pre><code>// 4.希尔排序(最小增量排序),它的本质其实就是“分组+直接插入排序”
// 希尔排序实际上就是不断的减小步长(gap),其实就是组距,也就是一个组内,两个相邻元素的间距,然后对每一个组进行直接插入排序
// 这个gap=组距=组数
// 我们不断的减小步长,当步长为1的时候,这时我们就只剩下一个组了,而这时通过比较相邻元素的大小,我们就可以得到
// 最终的有序序列
public static void shellSort(int[] a) {
int temp;
// 不断的减小步长(组距),直到它为0,当组距为零的时候我们就得到了一个有序数组
// gap=组距=组数
for (int gap = a.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < a.length; i += gap) {
temp = a[i];
int j = i - gap;
for (; j >= 0 && a[j] > temp; j -= gap) {
a[j + gap] = a[j];
}
a[j + gap] = temp;
}
}
}
</code></pre><h2>5.简单选择排序</h2><p>简单选择排序其实跟冒泡排序有点相似,也是每一趟比较都能得到一个最大或者最小值,只不过冒泡排序是依次进行相邻元素的比较并且交换,来实现类似生活中“冒泡”的效果,而简单选择排序的不同点就在于,虽然它也进行依次比较,但是它并不是频繁的进行交换操作,而是不断的记下已经比较过的数中的最大值的下标,比较的时候就是通过这个下标代表的值来进行比较,最后把那个得到最大值赋值给该趟比较的数组的最后后一位,如此循环n-1次(改进的话可能需要的次数更少),就可以讲一个数组排好序。</p><p>具体代码:</p><pre><code>// 5.简单选择排序
// 在要排序的一组数中,选出最小的一个数与第一个位置的数交换;
// 然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
public static void selectSort(int[] a) {
int minId, temp;
// 从 0 号元素开始比较,其实索引为 a.length-1 的时候就不用进行比较了
for (int i = 0; i < a.length-1; i++) {
// 初始化最小值,指向开始时候的元素
minId = i ;
for (int j = i+1; j < a.length; j++) {
// 在未排序元素中,从第二个位置开始,继续寻找最小元素,并保存其下标
if (a[minId] > a[j]) {
minId = j;
}
}
if (minId != i) {
temp = a[i];
a[i] = a[minId];
a[minId] = temp;
}
}
}
</code></pre><h2>6.堆排序</h2><p>堆排序在这几个排序算法中应该属于比较复杂的了。因为它还涉及到二叉树,堆的知识。其实涉及到的只是也不难,就是一些概念性的东西而已,下面我们来简单的用自己的话来介绍一下,具体严严谨的定义可以自行百度或者google.</p><p>二叉树,顾名思义一个节点最多只能有两个子节点,而完全二叉树就是除了最后一层之外,每一层个都是满的,就是说除最后一层之外,剩余节点组成的数是个满二叉树。而我们这里要用到的堆,它就是一个完全二叉树。</p><p>我们从根节点开始,逐层(每层从左到右)开始编号,0,1,2,3,...这样,我们就可以将一个堆这种数据结构存放到一个数组中。如果某个节点的下标为i,那么他的左根节点下标就为2<em>i+1,右根节点就为2</em>i+2,而我们常用到的堆为大顶堆(和小顶堆),大顶堆的定义简单来讲就是父节点的值总比其子节点的值要大。<br>好啦,至此我们就粗略的说完了这些简单概念(没学过的同学可以看下《数据结构》相关知识)<br>可能很多正在看这篇文章的同学跟我最初接触堆排序的时候的一样,有个相同的疑惑:这堆排来排去有什么用,也没见数组有序,不久得到了一个最大值嘛...对,重点就在这个最大值。听我给你细细道来...</p><p>那么接下来我们就来具体看下堆排序的思想及具体实现步骤:<br><strong>1.由给定的数组构建一个大顶堆,这时我们其实就已经得到了这个数组中最大值了(下标为0的那个元素,也就是堆顶元素,或者说根节点)</strong><br>最大值?回想一下冒泡排序和简单选择排序,有没有发现什么相同点?对的,那两个也是最大值。其实这里跟那两个算分的思想也有点相同,也是每次都获取到一个最大值。不同点嘛,看下一条..</p><p><strong>2.将那个最大值和数组的最后一个元素交换位置</strong><br>此时最后一个元素就变成了最大值了</p><p><strong>3.将最后一个元素(现在已经为最大值了)之前的这些元素组成的数组重新构造一个大顶堆(因为顺序已经发生了变化)</strong></p><p><strong>4.如此循环,一次次取出最大值,并且重新构造大顶堆,最终也就实现了数组的排序</strong></p><p>具体代码:</p><pre><code>public class HeapSort {
/**
* heapSort(int[] arr)
* 堆排序
*
* @param arr 待排序数组
*/
public static void heapSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
// 要想将待排序的数组构建成一个大顶堆,我们只需要从最后一个非叶子节点开始调整就行了,
// 而最后一个非叶子节点的下标为:(index+1)/2 - 1。。。。。。。index为最后一个元素的index值
for (int index = arr.length -1; index > 0; index--) {
// 将数组调整为大顶堆
heapAdjust(arr, index);
// 交换下标为0的和下标为i的元素的值,这么做的意思实际是取出大顶堆的堆顶元素,放到数组的后面,
//然后将剩余的项再次构建成大顶堆,如此循环,最终将完成数组的排序
swap(arr, 0, index);
}
}
/**
* 构建堆的过程
*
* @param arr 需要进行排序的数组
* @param index 调整数组的[0,index]部分
*/
private static void heapAdjust(int[] arr, int index) {
//current为当前待调整的节点的下标,这里的for循环表示从最后一个非叶子节点开始调整,依次调整到根节点
for (int current = (index+1) / 2 - 1; current >= 0; current--) {
//child用来记录当前节点的子节点中值最大的那个节点的下标(默认为左节点)
int child = current * 2 + 1;
// child != index 是用来判断最后一个非叶子节点是否有右节点(当只有左节点的时候那么child的值为index)
// 所以child不等于 index 表示当前节点有右节点,并且当右节点的值大于左节点的值的时候,
// 就将下标加1,指向右节点(当前节点的子节点中值最大的那个节点的下标)
if (child != index && arr[child] < arr[child + 1]) {
child++; // 将序号增加1,此时child表示的是右节点的下标,并且此时右节点大于左节点
}
// 将当前节点的值和其子节点中值最大的节点进行比较,如果小于最大子节点中最大值,就把二者交换
if (arr[current] < arr[child]) {
swap(arr, current, child);
}
}
}
// 交换数组中两个元素的位置
public static void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
}
</code></pre>
SwipeMenuListView的简单使用
https://segmentfault.com/a/1190000006921745
2016-09-17T14:49:25+08:00
2016-09-17T14:49:25+08:00
warmcheng
https://segmentfault.com/u/warmcheng
1
<p>这几天在学习开源项目SwipeMenuListView的简单使用,之所以说是“几天”,是因为在导入项目到Android-studio的过程中出现了各种各样的问题,我是直接打开的它里面的Demo,但是会出现各种错误,似乎并不是一个完整的Project,发现没有app目录。在网上看各种教程,也大都是要你把它的类库直接复制一份到自己的项目中去,但是既然Android-studio支持直接使用依赖,为什么那么多人还是那么麻烦的去复制文件到自己的项目中呢?</p>
<p>答案就是:作者并没有把最新的依赖添加进去,但是网上却几乎没有几个人指出来,这才导致自己反复在这里折腾(毕竟这是个下载量很大的开源项目啊,为什么都没几个人指出来呢?)作者提供的Demo里使用了一个BaseSwipListAdapter,代码如下:</p>
<pre><code>class AppAdapter extends BaseSwipListAdapter {...}</code></pre>
<p>如果我们直接也这样写的话,会报错,告诉我们该类找不到,那些添加了依赖的同学可以在jar包中打开看下,看看里面是不是真的缺少了BaseSwipListAdapter 这个类(使用依赖的话里面有很多bug,删除一条项目只有在我们将Menu拉回来的时候才会真正删除掉)<br>下面来讲下具体怎么导入并且使用SwipeMenuListView:</p>
<h2>1.导入SwipeMenuListView</h2>
<p>我们可以从github上下载开源项目SwipeMenuListView的源码<br><a href="https://link.segmentfault.com/?enc=xrLEH7zI1NRVUBZRUd5mtw%3D%3D.TyZfOuQQfleRRTFAmEeiXYJDwdPnTdmLB1lIaHaxmt5NzvOaNtQ1Pm4foA5UAA7yKlVY%2BIMfUEx%2B3ThgVW0HEw%3D%3D" rel="nofollow">https://github.com/baoyongzha...</a></p>
<p>在此也附上它的依赖,不过在此处并不会用到:</p>
<pre><code>dependencies {
compile 'com.baoyz.swipemenulistview:library:1.3.0'
}</code></pre>
<p>这里我们在项目中新建一个包,名字为:</p>
<pre><code>com.baoyz.swipemenulistview</code></pre>
<p>(这里我们包的名字最好不要改变,否则里面一些导入的类也要改变路径),然后将目录</p>
<pre><code>SwipeMenuListView-master\library\src\main\java\com\baoyz\swipemenulistview\</code></pre>
<p>下面的文件复制到我们新建的包里面:</p>
<p><img src="/img/bVDcKO?w=717&h=285" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVDcKY?w=356&h=384" alt="clipboard.png" title="clipboard.png"></p>
<p>(这里为了方便直接运行,我们直接将Demo里的文件、资源等也逐一的复制到了我们的目录项目中,但是记得要在清单文件中为activity进行配置)</p>
<h2>2.SwipeMenuListView的具体使用</h2>
<p>这里我们先粘贴下Demo中的示例(部分地方有修改),SimpleActivity.java</p>
<pre><code>package com.qc.administrator.myswipedemo;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.baoyz.swipemenulistview.BaseSwipListAdapter;
import com.baoyz.swipemenulistview.SwipeMenu;
import com.baoyz.swipemenulistview.SwipeMenuCreator;
import com.baoyz.swipemenulistview.SwipeMenuItem;
import com.baoyz.swipemenulistview.SwipeMenuListView;
import java.util.List;
/**
* SwipeMenuListView
* Created by baoyz on 15/6/29.
*/
public class SimpleActivity extends Activity {
private List<ApplicationInfo> mAppList;
private AppAdapter mAdapter;
private SwipeMenuListView mListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
mAppList = getPackageManager().getInstalledApplications(0);
mListView = (SwipeMenuListView) findViewById(R.id.listView);
mAdapter = new AppAdapter();
mListView.setAdapter(mAdapter);
// 第1步:设置创建器,并且在其中生成我们需要的菜单项,将其添加进菜单中
SwipeMenuCreator creator = new SwipeMenuCreator() {
@Override
public void create(SwipeMenu menu) {
// 创建“打开”项
SwipeMenuItem openItem = new SwipeMenuItem(
getApplicationContext());
openItem.setBackground(new ColorDrawable(Color.rgb(0xC9, 0xC9,
0xCE)));
openItem.setWidth(dp2px(90));
openItem.setTitle("Open");
openItem.setTitleSize(18);
openItem.setTitleColor(Color.WHITE);
// 将创建的菜单项添加进菜单中
menu.addMenuItem(openItem);
// 创建“删除”项
SwipeMenuItem deleteItem = new SwipeMenuItem(
getApplicationContext());
deleteItem.setBackground(new ColorDrawable(Color.rgb(0xF9,
0x3F, 0x25)));
deleteItem.setWidth(dp2px(90));
deleteItem.setIcon(R.drawable.ic_delete);
// 将创建的菜单项添加进菜单中
menu.addMenuItem(deleteItem);
}
};
// 为ListView设置创建器
mListView.setMenuCreator(creator);
// 第2步:为ListView设置菜单项点击监听器,来监听菜单项的点击事件
mListView.setOnMenuItemClickListener(new SwipeMenuListView.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(int position, SwipeMenu menu, int index) {
//position:列表项的下标。如:0,1,2,3,4,...
//index:菜单项的下标。如:0,1,2,3,4,...
ApplicationInfo item = mAppList.get(position);
switch (index) {
case 0:
// open
open(item);
break;
case 1:
// delete
// delete(item);
mAppList.remove(position);
//通知监听者数据集发生改变,更新ListView界面
mAdapter.notifyDataSetChanged();
break;
}
// true:其他已打开的列表项的菜单状态将保持原样,不会受到其他列表项的影响而自动收回
// false:已打开的列表项的菜单将自动收回
return false;
}
});
// 设置侧滑监听器,监听侧滑开始和侧滑结束
// 注意:当我们将一个已经侧滑出来的菜单重新收回去的时候并不会调用onSwipeStart方法,
// 但是结束的时候依然会调用onSwipeEnd方法
mListView.setOnSwipeListener(new SwipeMenuListView.OnSwipeListener() {
@Override
public void onSwipeStart(int position) {
// swipe start
}
@Override
public void onSwipeEnd(int position) {
// swipe end
}
});
// 设置监听Menu状态改变的监听器(Menu的打开和关闭)
mListView.setOnMenuStateChangeListener(new SwipeMenuListView.OnMenuStateChangeListener() {
@Override
public void onMenuOpen(int position) {
}
@Override
public void onMenuClose(int position) {
}
});
// other setting
// listView.setCloseInterpolator(new BounceInterpolator());
// 设置列表项长点击的监听器
mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(getApplicationContext(), position + " long click", Toast.LENGTH_SHORT).show();
return false;
}
});
}
//删除一个应用,在这里并没有被调用,因为这会卸载相应的app
private void delete(ApplicationInfo item) {
try {
Intent intent = new Intent(Intent.ACTION_DELETE);
intent.setData(Uri.fromParts("package", item.packageName, null));
startActivity(intent);
} catch (Exception e) {
}
}
// 打开app
private void open(ApplicationInfo item) {
// Intent.ACTION_MAIN:作为主进入点启动,并不期望获得数据
Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null);
resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER);
//根据传进来的ApplicationInfo item设置要解析的应用的包名
resolveIntent.setPackage(item.packageName);
//根据指定的Intent解析出对应的应用中所有的activity的信息
List<ResolveInfo> resolveInfoList = getPackageManager()
.queryIntentActivities(resolveIntent, 0);
if (resolveInfoList != null && resolveInfoList.size() > 0) {
//ResolveInfo:返回依据IntentFilter解析出来的Intent所返回的信息。
//此部分对应于从AndroidManifest.xml中<意图>标签收集信息。
//解析出来的第一个为主activity的信息
ResolveInfo resolveInfo = resolveInfoList.get(0);
String activityPackageName = resolveInfo.activityInfo.packageName;
String className = resolveInfo.activityInfo.name;
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
ComponentName componentName = new ComponentName(
activityPackageName, className);
intent.setComponent(componentName);
startActivity(intent);
}
}
class AppAdapter extends BaseSwipListAdapter {
@Override
public int getCount() {
return mAppList.size();
}
@Override
public ApplicationInfo getItem(int position) {
return mAppList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
//夹在金列表项的布局item_list_app.xml
convertView = View.inflate(getApplicationContext(),
R.layout.item_list_app, null);
new ViewHolder(convertView);
}
ViewHolder holder = (ViewHolder) convertView.getTag();
// 获取手机全部应用的信息
ApplicationInfo item = getItem(position);
// 加载应用的图标
holder.iv_icon.setImageDrawable(item.loadIcon(getPackageManager()));
// 加载应用的标题
holder.tv_name.setText(item.loadLabel(getPackageManager()));
// 为图标设置点击事件监听器(弹出一个toast)
holder.iv_icon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(SimpleActivity.this, "iv_icon_click", Toast.LENGTH_SHORT).show();
}
});
// 为应用名字设置点击监听器(弹出一个toast)
holder.tv_name.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(SimpleActivity.this,"iv_icon_click",Toast.LENGTH_SHORT).show();
}
});
return convertView;
}
class ViewHolder {
ImageView iv_icon;
TextView tv_name;
//根据传进来的convertView创建ViewHolder,并且将其设置为convertView的Tag
public ViewHolder(View view) {
iv_icon = (ImageView) view.findViewById(R.id.iv_icon);
tv_name = (TextView) view.findViewById(R.id.tv_name);
view.setTag(this);
}
}
//这里我们可以根据列表项的位置来设置某项是否允许侧滑
//(此处我们设置的是当下标为偶数项的时候不允许侧滑)
@Override
public boolean getSwipEnableByPosition(int position) {
if(position % 2 == 0){
return false;
}
return true;
}
}
// 将dp转换为px
private int dp2px(int value) {
// 第一个参数为我们待转的数据的单位,此处为 dp(dip)
// 第二个参数为我们待转的数据的值的大小
// 第三个参数为此次转换使用的显示量度(Metrics),它提供屏幕显示密度(density)和缩放信息
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
getResources().getDisplayMetrics());
}
//另一种将dp转换为px的方法
private int dp2px(float value){
final float scale = getResources().getDisplayMetrics().density;
return (int)(value*scale + 0.5f);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_left) {
//这里我们可以通过mListView的setSwipeDirection方法来设置SwipeMenuListView的滑动方向
mListView.setSwipeDirection(SwipeMenuListView.DIRECTION_LEFT);
return true;
}
if (id == R.id.action_right) {
mListView.setSwipeDirection(SwipeMenuListView.DIRECTION_RIGHT);
return true;
}
return super.onOptionsItemSelected(item);
}
}</code></pre>
<p><strong>具体步骤:</strong></p>
<h3>1.添加布局代码</h3>
<p>在我们需要使用该效果的布局文件中添加如下代码:</p>
<pre><code><com.baoyz.swipemenulistview.SwipeMenuListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</code></pre>
<h3>2.生成一个菜单创建器</h3>
<p>生成一个菜单创建器,在其中生成菜单项,并且将其添加进菜单中,此处的菜单为我们侧滑具体的列表项之后显示出来的视图,如下图:</p>
<p><img src="/img/bVDcLY?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p>代码:</p>
<pre><code>SwipeMenuCreator creator = new SwipeMenuCreator() {
@Override
public void create(SwipeMenu menu) {
// 创建“打开”项
SwipeMenuItem openItem = new SwipeMenuItem(
getApplicationContext());
openItem.setBackground(new ColorDrawable(Color.rgb(0xC9, 0xC9,
0xCE)));
openItem.setWidth(dp2px(90));
openItem.setTitle("Open");
openItem.setTitleSize(18);
openItem.setTitleColor(Color.WHITE);
// 将创建的菜单项添加进菜单中
menu.addMenuItem(openItem);
// 创建“删除”项
SwipeMenuItem deleteItem = new SwipeMenuItem(
getApplicationContext());
deleteItem.setBackground(new ColorDrawable(Color.rgb(0xF9,
0x3F, 0x25)));
deleteItem.setWidth(dp2px(90));
deleteItem.setIcon(R.drawable.ic_delete);
// 将创建的菜单项添加进菜单中
menu.addMenuItem(deleteItem);
}
};</code></pre>
<p>然后为列表设置创建器:</p>
<pre><code>mListView.setMenuCreator(creator);
</code></pre>
<h3>3.为ListView设置菜单项点击监听器,监听菜单项的点击事件</h3>
<pre><code>mListView.setOnMenuItemClickListener(new SwipeMenuListView.OnMenuItemClickListener() {
//position:列表项的下标。如:0,1,2,3,4,...
//index:菜单项的下标。如:0,1,2,3,4,...
@Override
public boolean onMenuItemClick(int position, SwipeMenu menu, int index) {//具体实现}
</code></pre>
<h3>4.设置侧滑监听器(如果有需要的话)</h3>
<p>设置侧滑监听器,监听侧滑开始和侧滑结束。<br>注意:当我们将一个已经侧滑出来的菜单重新收回去的时候并不会调用onSwipeStart方法,但是结束的时候依然会调用onSwipeEnd方法</p>
<pre><code> mListView.setOnSwipeListener(new SwipeMenuListView.OnSwipeListener() {
@Override
public void onSwipeStart(int position) {
// swipe start
}
@Override
public void onSwipeEnd(int position) {
// swipe end
}
});
</code></pre>
<h3>5.设置监听Menu状态改变的监听器(Menu的打开和关闭)</h3>
<pre><code> mListView.setOnMenuStateChangeListener(new SwipeMenuListView.OnMenuStateChangeListener() {
@Override
public void onMenuOpen(int position) {
}
@Override
public void onMenuClose(int position) {
}
});
</code></pre>
<h3>6.为我们的ListView设置适配器</h3>
<p>我们的适配器可以继承自BasseAdapter,也可以继承自该开源项目提供的一个适配器类BaseSwipListAdapter,并重写其中的方法</p>
<h3>7.设置具体的某个列表项是否允许侧滑</h3>
<pre><code>
@Override
public boolean getSwipEnableByPosition(int position) {
if(position % 2 == 0){
return false;
}
return true;
}
</code></pre>
<p>这里作者设置的是当为偶数项的时候不允许侧滑(返回值为false)</p>
<h3>8.dp转px方法</h3>
<pre><code> // 将dp转换为px
private int dp2px(int value) {
// 第一个参数为我们待转的数据的单位,此处为 dp(dip)
// 第二个参数为我们待转的数据的值的大小
// 第三个参数为此次转换使用的显示量度(Metrics),它提供屏幕显示密度(density)和缩放信息
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
getResources().getDisplayMetrics());
}
//另一种将dp转换为px的方法
private int dp2px(float value){
final float scale = getResources().getDisplayMetrics().density;
return (int)(value*scale + 0.5f);
}</code></pre>
<p>9.设置侧滑的方向(也决定了Menu的出现位置)</p>
<p>//设置向左滑动出现Menu<br>mListView.setSwipeDirection(SwipeMenuListView.DIRECTION_LEFT);<br>//设置向右滑动出现Menu<br>mListView.setSwipeDirection(SwipeMenuListView.DIRECTION_RIGHT);</p>
<h3>10.设置列表项显示不同的Menu</h3>
<p>其实我们可以让列表项显示不同的Menu,我们可以通过重写BaseAdapter的getItemViewType(int position)和 getViewTypeCount()方法,然后在菜单创建器SwipeMenuCreator中根据传进去的SwipeMenu的getViewType()的值来决定生成怎样的Menu</p>
<p>文章最后附上原作者Demo中DifferentMenuActivity.java的代码供参考:</p>
<pre><code>/**
* SwipeMenuListView
* Created by baoyz on 15/6/29.
*/
public class DifferentMenuActivity extends Activity {
private List<ApplicationInfo> mAppList;
private AppAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
mAppList = getPackageManager().getInstalledApplications(0);
SwipeMenuListView listView = (SwipeMenuListView) findViewById(R.id.listView);
mAdapter = new AppAdapter();
listView.setAdapter(mAdapter);
// step 1. create a MenuCreator
SwipeMenuCreator creator = new SwipeMenuCreator() {
@Override
public void create(SwipeMenu menu) {
// Create different menus depending on the view type
switch (menu.getViewType()) {
case 0:
createMenu1(menu);
break;
case 1:
createMenu2(menu);
break;
case 2:
createMenu3(menu);
break;
}
}
private void createMenu1(SwipeMenu menu) {
SwipeMenuItem item1 = new SwipeMenuItem(
getApplicationContext());
item1.setBackground(new ColorDrawable(Color.rgb(0xE5, 0x18,
0x5E)));
item1.setWidth(dp2px(90));
item1.setIcon(R.drawable.ic_action_favorite);
menu.addMenuItem(item1);
SwipeMenuItem item2 = new SwipeMenuItem(
getApplicationContext());
item2.setBackground(new ColorDrawable(Color.rgb(0xC9, 0xC9,
0xCE)));
item2.setWidth(dp2px(90));
item2.setIcon(R.drawable.ic_action_good);
menu.addMenuItem(item2);
}
private void createMenu2(SwipeMenu menu) {
SwipeMenuItem item1 = new SwipeMenuItem(
getApplicationContext());
item1.setBackground(new ColorDrawable(Color.rgb(0xE5, 0xE0,
0x3F)));
item1.setWidth(dp2px(90));
item1.setIcon(R.drawable.ic_action_important);
menu.addMenuItem(item1);
SwipeMenuItem item2 = new SwipeMenuItem(
getApplicationContext());
item2.setBackground(new ColorDrawable(Color.rgb(0xF9,
0x3F, 0x25)));
item2.setWidth(dp2px(90));
item2.setIcon(R.drawable.ic_action_discard);
menu.addMenuItem(item2);
}
private void createMenu3(SwipeMenu menu) {
SwipeMenuItem item1 = new SwipeMenuItem(
getApplicationContext());
item1.setBackground(new ColorDrawable(Color.rgb(0x30, 0xB1,
0xF5)));
item1.setWidth(dp2px(90));
item1.setIcon(R.drawable.ic_action_about);
menu.addMenuItem(item1);
SwipeMenuItem item2 = new SwipeMenuItem(
getApplicationContext());
item2.setBackground(new ColorDrawable(Color.rgb(0xC9, 0xC9,
0xCE)));
item2.setWidth(dp2px(90));
item2.setIcon(R.drawable.ic_action_share);
menu.addMenuItem(item2);
}
};
// set creator
listView.setMenuCreator(creator);
// step 2. listener item click event
listView.setOnMenuItemClickListener(new SwipeMenuListView.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(int position, SwipeMenu menu, int index) {
ApplicationInfo item = mAppList.get(position);
switch (index) {
case 0:
// open
break;
case 1:
// delete
// delete(item);
mAppList.remove(position);
mAdapter.notifyDataSetChanged();
break;
}
return false;
}
});
}
class AppAdapter extends BaseAdapter {
@Override
public int getCount() {
return mAppList.size();
}
@Override
public ApplicationInfo getItem(int position) {
return mAppList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getViewTypeCount() {
// menu type count
return 3;
}
@Override
public int getItemViewType(int position) {
// current menu type
return position % 3;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = View.inflate(getApplicationContext(),
R.layout.item_list_app, null);
new ViewHolder(convertView);
}
ViewHolder holder = (ViewHolder) convertView.getTag();
ApplicationInfo item = getItem(position);
holder.iv_icon.setImageDrawable(item.loadIcon(getPackageManager()));
holder.tv_name.setText(item.loadLabel(getPackageManager()));
return convertView;
}
class ViewHolder {
ImageView iv_icon;
TextView tv_name;
public ViewHolder(View view) {
iv_icon = (ImageView) view.findViewById(R.id.iv_icon);
tv_name = (TextView) view.findViewById(R.id.tv_name);
view.setTag(this);
}
}
}
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
getResources().getDisplayMetrics());
}
}
</code></pre>
华为手机Log.d无法打印日志的解决
https://segmentfault.com/a/1190000006921246
2016-09-17T12:52:08+08:00
2016-09-17T12:52:08+08:00
warmcheng
https://segmentfault.com/u/warmcheng
0
<p>最近又遇到华为手机无法打印日志的问题,网上查资料说是华为手机默认Log调试开关是没有打开的,那么我们就可以通过以下方法来打开Log开关:<br>1.进入拨号界面输入(最好是系统自带的拨号界面,在本人的手机上其他拨号软件竟然没反应...也<br>是醉了...)<br><code>*#*#2846579#*#*</code><br>2.依次选择ProjectMenu---后台设置----LOG设置---LOG开关 点击打开<br>3.然后重启手机</p>
<p>但是...请注意,这并不是对所有的华为手机都试用,比如悲催的我的这个手机(畅玩4C)...<br>按照上面的解决方案,一步一步来是下面这个样子:<br>1.拨号界面输入<code>*#*#2846579#*#*</code>,进入了工程菜单:</p>
<p><img src="/img/bVDcFz?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p>2.点击第一项:后台设置,进入后台设置:</p>
<p><img src="/img/bVDcFH?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p>3.点击第一项:后台调试:</p>
<p><img src="/img/bVDcFU?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p>4.当我点击打开的时候,尴尬的问题出现了....</p>
<p><img src="/img/bVDcFW?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p>调试密码???...搞毛线啊...最后只好去请教华为的客服,又是关注微信公众号,又是发邮件的,最后得出来的结果是酱紫的:</p>
<p><img src="/img/bVDcGh?w=1157&h=481" alt="clipboard.png" title="clipboard.png"><br>然后又发邮件,就直接被告诉他们也不知道(邮件删了,没法截图看了).....</p>
<p>好吧,没办法那我们就换种思路:既然Log.d没办法用,那那么多打印的方法,其他的能不能用呢?经过试验,发现有两个还是可以使用的:<br><strong>1.</strong><code>Log.i</code><br><strong>2.</strong><code>Log.e</code><br>这样,我们就通过一种曲线的方式来实现了华为日志的打印,刚想批评华为两句,发现是自己的问题,其实是我自己的问题啦,我们不应该点击“后台调试”,而应该点击“LOG设置”,选中“AP日志”(其实选中AP日志的时候下次进来会默认把后两项也选中了)</p>
<p><img src="/img/bVFVYP?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<p><strong>注意:</strong>选中之后点击“关闭”的话最好再次点击进来查看一下是否设置成功了,有些情况下可能并没有设置成功,当时就是吃的这方面的亏,以为是需要后台调试密码才能把这个设置成功呢。好啦,啰嗦了那么多,就是为了写下自己的整个纠结历程吧,重要的是,问题总算解决了。</p>