前端构建工具—dawn

介绍使用

参考:Dawn

原理

dawn 是一个采用中间件技术实现的轻量的任务流协调器,类似于gulp, grunt。dawn 本身并不处理任务,转而交由中间件承担这一职能,如同 gulp, grunt 插件。在实现上,dawn 是 webpack 出台后的产物,就不需要像 gulp, grunt 那样关注任务流的始点 —— 文件位置,而更容易聚焦于任务的分解,将编译、压缩作业交给 dn-middleware-webpack 中间件。dawn 的中间件实现机制如同 koa,使用 next 引用下一个中间件串联任务流,以继承事件模型的 ctx 实例作为上下文。其核心代码如下:

class Context extends EventEmitter {
  async _execQueue(middlewares, args, onFail) {
    const middleware = middlewares.shift();
    if (!middleware) return;

    // this.load 安装中间件,并执行中间件的外层函数,获得实际的任务逻辑 handler
    const handler = await this.load(middleware);

    const next = (args) => {
      // 若返回真值,在 watch 状态下,也只执行一次
      if (next.__result) return next.__result;

      next.__result = this._execQueue(middlewares, args, onFail)
        .catch(err => onFail(err));
      return next.__result;
    };
    return handler.call(this, next, this, args);
  }
}

常用中间件介绍

1. dn-middleware-webpack

概述:基于 webpack3 实现的中间件,打包模块。本地开发模式也将打包模块,而不是读取 webpack 缓存数据。
– dn-middleware-webpack 在回调中执行后续中间件的处理逻辑。
– 通过 vmodule-webpack-plugin 插件将 config.yml 类配置文件注入为可以 import 引入的虚拟模块。
– ctx 中添加 webpack 属性,即 webpack 类库。
事件:
– ‘webpack.opts’,可用于修改 opts 配置项,参数 opts。
– ‘webpack.config’,可用于修改注入 webpack 的 config 配置,参数 config, webpack, opts。
– ‘webpack.compiler’,操纵 webpack 的编译器,参数 compiler。
– ‘webpack.stats’,可用于监控编译状态,参数 stats。

2. dn-middleware-server

概述:基于 nokit,启动本地服务。首次执行时将在项目空间创建 server.yml 配置文件。
– 在 nokit 服务器中设置拦截器,通过 httpProxy 转发请求。
– ctx 中添加 server 属性,即 nokit.Server 实例;以及 httpServer 属性,即 server.httpServer。

事件:
– ‘server.init’,服务未启动时事件,参数 server 实例。
– ‘server.start’,服务启动成功时事件,参数 server 实例。

3. dn-middleware-dll

概述:独立构建项目依赖,节省打包时间。

  • 借助 ctx.exec 方法执行 webpack 中间件,打包项目的依赖,默认存放在工程目录 .cache 文件夹内。子文件夹名基于项目所使用的依赖通过 md5 生成散列,以便在依赖更新时重新打包。再借助 ctx.exec 方法执行 copy 中间件,将打包文件拷贝到 build/js 文件夹内。
  • 通过 ‘webpack.config’ 事件,在 webpackConfig 插件中注入 webpack.DllReferencePlugin 插件。
4. dn-middleware-faked

概述:基于 faked 提供数据模拟服务。

  • 基于 faked 创建 gui server 服务器,配置的模拟数据将输出到工程目录 mock 文件夹中 index.js, gui.data.json。
  • 模拟数据文件最终将作为 webpack 入口文件,以此实现远程请求的拦截,并实现热更新。
  • 执行逻辑被封装为 ctx.faked.apply 方法,在 dn-middleware-webpack 中执行,两个中间件耦合度较高。
5. dn-middleware-i18n

概述:将工程目录中 locales 语言包加载为 $locales 模块,通过 $i18n 获取指定模块,实现国际化。

  • 使用 confman.webpackPlugin 方法将工程目录中的语言包输出为 $locales 虚拟模块。
  • 使用 vmodule-webpack-plugin 类库输出 $i18n 虚拟模块,以获取指定文案。
6. 其他
  • dn-middleware-clean,清理文件或目录,可用 opts.target 加以配置,默认清理 ‘./build//.’。
  • dn-middleware-copy,复制文件。选项 from 查询源文件的文件夹路径,默认 ‘./from’; to 目标文件夹路径; log 是否打印日志; dot 源文件匹配规则是否支持 ‘.’ 起始; direction 影响映射 key 键指代源文件还是目标文件; files 源文件和目标文件映射,目标文件路径支持占位符 {index} 替换(index 自右而左),或使用源文件路径(映射中,目标文件以 ‘/’ 结尾)。
  • dn-middleware-browser-sync 基于 ‘browser-sync’ 监听打包文件变更,借助 ‘connect-browser-sync’ express中间件实现热更新。选项 files 配置监听的文件,默认 [‘./build//.’];port 为 ‘browser-sync’ 服务启动端口。
  • dn-middleware-git-sync,git 操作,包含 commit, push 动作(push 又区分日常和预发环境)。
  • dn-middleware-jcs,在 babel-loader 中添加 ‘jsx-control-statements’ 插件,以使 jsx 可使用结构控制语句,同时 lint 阶段也会作代码检查,参考 通过 JSX Control Statements 编写 JSX。
  • dn-middleware-lint,使用 eslint 命令作语法检查。
  • dn-middleware-tslint,对 typescript 进行语法检查。
  • dn-middleware-typedoc,使用 typedoc 为 typescript 项目生成文档。
  • dn-middleware-typescript,在 webpackConfig 中添加 awesome-typescript-loader,支持编译 tsx 模块。选项 declaration 是否分离 ts, js 文件,默认分离,false 时不分离。
  • dn-middleware-pkginfo,更新项目 package.json 中的 name, version, description 信息。
  • dn-middleware-prepush,在 .git/hooks/pre-push 添加 shell 命令,推送前执行 dn build 命令。
  • dn-middleware-sensitive-path,在 webpackConfig 中添加 ‘case-sensitive-paths-webpack-plugin’ 插件,使 mac, windows 引入模块时严格区分模块的大小写。
  • dn-middleware-shell,调用 ctx.utils.exec 执行 shell 命令。选项 script 命令内容;wscript 为 windows 系统下命令内容;async 是否异步执行,默认同步。
  • dn-middleware-watch,基于 ‘chokidar’ 监听执行文件变更。选项 match 匹配的文件,默认为 ‘./src//.’;event 监听的事件类型;script 事件发生后执行的脚本;onChange 事件发生后执行的动作。
  • dn-middleware-unit,基于 ‘mocha’ 对 ./test/unit 文件夹中内容作单元测试。

java AQS介绍

Synchronized

Synchronized是java提供的一种同步方式。当使用Synchronized关键字修饰代码块,则该段代码成为只能互斥访问的临界区。Synchronized关键字需要和一个特定的对象绑定,进入临界区的代码会获取该对象的内置锁,且这个对象锁是可重入的。同时java的Object对象中提供了wait/notify的方法,这样,每个对象就提供了一个可重入的互斥锁以及一条条件队列。

AbstractQueuedSynchronizer

java的Concurrency库中提供了一个抽象队列同步器,这是java提供的另一种同步方式。
AQS队列是面想锁的实现者的,使用AQS对象可以自定义锁对象,用来实现不同的同步策略。比如互斥,共享,是否响应中断,支持超时,支持公平的获取锁,以及多条条件队列。
AQS的实现需要解决三个问题,一个是同步状态的管理,线程的阻塞和唤醒,线程排队。例如对于一个互斥锁的实现,线程获取锁时需要获取并设置当前锁的同步状态,如果获取到了锁,则执行临界区的代码,否则,线程将阻塞,并放入条件队列中等待,当其他线程释放了同步状态,该线程将被唤醒,并被移出等待队列。

同步状态管理

AQS的同步状态使用了一个int字段state表示。对state字段的修改必须是原子操作,且修改状态对其他线程可见。所以state字段使用volatile修饰,保证其可见性。同时,使用指令集提供的CAS原子操作来对state字段进行修改,保证访问和修改操作的原子性,避免多个线程错误的修改了state字段。在java中,原子操作由Unsafe类提供。

protected final boolean compareAndSetState(int expect, int update) {
        return U.compareAndSwapInt(this, STATE, expect, update);
}

线程的阻塞和唤醒

线程的阻塞和唤醒本质应该和synchronized的实现一样。jvm使用操作系统提供的pthread_cond_wait/phread_cond_signal pthread_mutex_lock/pthread_mutex_unlock系统调用来实现线程的阻塞和唤醒。具体使用的同样由Unsafe类的一组api提供,park和unpark。park将阻塞当前线程,而unpark(thread)会唤醒参数中的线程。
park调用提供了超时的版本,这样就可以用来实现可超时的获取锁。park同样会响应中断,但是被唤醒并不会抛出InterruptedExcption,同时会保留interrupt状态,这样,AQS通过对intrrupt状态的不同处理来实现是否可中断的锁。当检查到interrupt状态为true时,抛出InterruptedExcption异常,则获取锁的调用者就会收到该异常,并从阻塞状态中恢复。如果不抛出异常InterruptedExcption,则可实现不响应中断的锁获取。

同步队列

AQS的同步队列实现了一个CHL队列
一个标准的CLH实现如下

public class CLHLock {
    public static class CLHNode {
        private volatile boolean isLocked = true;
    }

    @SuppressWarnings("unused")
    private volatile CLHNode                                           tail;
    private static final ThreadLocal<CLHNode>                          LOCAL   = new ThreadLocal<CLHNode>();
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,
                                                                                   CLHNode.class, "tail");

    public void lock() {
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        CLHNode preNode = UPDATER.getAndSet(this, node);
        if (preNode != null) {
            while (preNode.isLocked) {
            }
            preNode = null;
            LOCAL.set(node);
        }
    }

    public void unlock() {
        CLHNode node = LOCAL.get();
        if (!UPDATER.compareAndSet(this, node, null)) {
            node.isLocked = false;
        }
        node = null;
    }
}


队列同样基于CAS和volatile,通过对tail节点原子的访问和修改,是的节点能正确的获取到其前驱节点,并通过前驱节点组成一条队列。
AQS的同步队列将没能获取到锁的线程放入到同步队列中,且每个节点还记录了后继节点,这样当线程释放锁后,可以唤醒下一个节点。
我们通过入队和出队来大致了解队列的细节

final boolean acquireQueued(final Node node, int arg) {
        try {
            /**
             * 返回中断状态,如果是不可中断的获取锁,
             * 通过返回值可以知道线程在获取锁时是否被中断过
             */
            boolean interrupted = false;
            /**
             * 死循环的获取锁,当获取到锁后,会将当前节点设置
             * 为head(类似空节点),通过这种方式将节点出队
            for (;;) {
                final Node p = node.predecessor();
                //到了CLH队列头部,且可以获取到锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                /**
                 * 1。先将该线程入队
                 * 2。调用park阻塞该线程
                 * 3。如果是被中断唤醒的,则标记interrupt状态
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

CLH队列是公平,所以当线程要获取锁的同步状态时,首先需要入队,这样就可以保证公平。同样的,如果一个线程不需要入队就可以直接尝试获取同步状态,则这个线程就插队了。这可以由锁的实现者决定。
AQS为CHL节点增加了一个nextWaiter字段,该字段标记该节点是互斥还是共享的。如果节点是共享的,则当该节点被唤醒后,将唤醒该节点的后继。如果是同步的,则只有释放锁的时候,才会唤醒后继。

AQS的使用

实现一个简单的互斥锁

public static class Mutex implements Lock {

        private Sync mSync = new Sync();

        @Override
        public void lock() {
            mSync.acquire(1);
        }

        @Override
        public void unlock() {
            mSync.release(1);
        }

        private static class Sync extends AbstractQueuedSynchronizer {
            @Override
            public boolean tryAcquire(int acquires) {
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                } 
                return false;
            }

            @Override
            public boolean tryRelease(int acquires) {
                if (Thread.currentThread() == getExclusiveOwnerThread()
                        && compareAndSetState(1, 0)) {
                    setExclusiveOwnerThread(null);
                    return true;
                }
                return false;
            }
        }

该类实现了Lock接口,具体的功能代理给Sync对象,Sync继承自AQS,通过实现AQS的一些简单方法,就能够使用AQS的功能帮助我们实现各种类型的锁。

音频编码实战PCM转AAC(上)

AAC(Advanced Audio Coding)中文名称为高级音频编码,它是由FraunhoferIIS、杜比实验室、AT&T、Sony等公司在1997年基于MPEG-2开发的音频编码技术,它出现的目的是取代MP3格式。相对于MP3格式来说AAC格式的音质更佳,文件更小,不足之处是AAC属于有损压缩,无法媲美APE、FLAC等无损压缩的音频格式,总的来说AAC有一下特点:

  • 提升压缩率
    可以以更小的文件大小获得更高的音质

  • 支持多声道
    可提供最多48个全音域声道

  • 更高解析度
    最高支持96KHz的采样频率

  • 提升解码效率
    解码播放所占的资源更少

由于项目需求,需要把第三方服务提供的PCM格式的音频数据编码成AAC格式的音频数据,查阅资源发现Android在5.0版本以后提供了MediaCodec API,它可以实现把PCM转码成AAC,于是参照网上demo做了测试,测试期间遇见过声音小有杂音等问题最后是通过阅读官方文件解决了该问题,本篇文章做个记录,希望能给小伙伴们一点帮助。

MediaCodec是一个编解码器,它采用硬编码的方法把输入数据编码成我们设置的目标格式,它的工作原理如下所示:

MediaCodec

MediaCodec开始工作时会提供一个null的输入缓冲去ByteBuffer给Client端,Client端把要编码的数据填充到这个空的ByteBuffer中返回给MediaCodec,MediaCodec内部将数据编码完成后再把数据填充一个空的ByteBuffer中供Client端进行处理。这就是MediaCodec的工作流程。

现在我们要想实现一个PCM转AAC的库,我们要明确几个问题:编码要单独启动一个线程,防止对现有线程造成影响;原始音频数据要使用缓存同时还要注意多线程的问题;定义通用的代表原始音频数据的数据格式。

明确了以上几点,我们首先可以简单的对原始音频数据做封装,代码如下:

public static final class AudioFrameData {
    public byte[] data;
    public int sampleRate;
    public int channelCount;
    public int bitRate;
}

其中data表示的原始PCM音频数据,sampleRate表示当前音频的采用率,channelCount表示当前音频的声道数,bitRate表示转码成AAC数据需要的比特率,定义完帧数据后就可以初始化我们的MediaCodeC了,源码如下:

private boolean initMediaCodec(AudioFrameData data) {
    try {
        MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, data.sampleRate, data.channelCount);
        mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, getProfile());
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, data.bitRate);

        mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mediaCodec.start();

        bufferInfo = new BufferInfo();
        return true;
    } catch (Throwable ignored) {
        Log.e(TAG, "init failure", ignored);
    }
    return false;
}

initMediaCodec()方法负责初始化MediaCodeC,初始化成功返回true否则返回false。MediaCodeC需要由MediaForamt来确定转码后的音频参数,所以initMediaCodec()方法中先根据AudioFrameData参数初始化一个mediaFormat实例出来,该实例确定了转码出来的音频数据使用AAC的级别、采用率、比特率、声道数。确定了音频格式后调用MediaCodeC的静态方法createEncoderByType()方法创建一个编码器实例mediaCodeC,其中为MediaFormat.MIMETYPE_AUDIO_AAC表示需要转码成AAC数据格式,然后调用mediaCodeC的configure()方法告诉编码器具体规则,最后调用start()方法表示即将开始编码工作,注意start()方法是必须要调用的。

初始化完编码器后表示可以进行编码工作了,这个时候需要待编码的数据,因此我们提供一个put方法,代码如下:

public void putData(AudioFrameData data) {
    if (null == data) return;
    if (null == mQueueData) {
        mQueueData = new ArrayBlockingQueue<>(50);
    }
    try {
        mQueueData.put(data);
        Log.e(TAG, "after put, capacity is " + mQueueData.size());
    } catch (Throwable e) {
        Log.e(TAG, "error occurred", e);
    }
}

putData()方法中mQueueData是一个线程安全的队列,我们把外界传递进来的data数据缓存到该队列中,然后编码器在新启动的线程中循环取出缓存数据进行编码,编码完成之后还要对外输出,因此需要外界提供一个输出路径,代码如下:

public void setFilePath(String path) {
    this.mFilePath = path;
    deleteFileIfExist(path);
}

private void deleteFileIfExist(String path) {
    File file = new File(path);
    if (file.exists() && !file.delete()) {
        try {
            FileWriter writer = new FileWriter(file);
            writer.write("");
            writer.flush();
            writer.close();
        } catch (Throwable ignored) {
        }
    }
}

setFilePath()方法中对传入的文件路径参数做了校验,如果文件存在就删除,若删除文件失败就清空文件内容。现在有了输出路径,接着就是开始启动编码线程对数据进行编码了,代码如下:

public void start() {
    if (isStarted) return;
    isStarted = true;
    Thread thread = new Thread(this);
    thread.start();
}

start()方法中isStarted是boolean变量,作用是防止编码线程多次被启动,线程启动后就会执行run()方法,代码如下:

@Override
public void run() {
    try {
        isRecording = true;
        FileOutputStream writer = new FileOutputStream(mFilePath, true);
        Log.e(TAG, "start encode");
        while (isStarted || !mQueueData.isEmpty()) {
            AudioFrameData data = takeData();
            if (null != data) {
                encode(data, writer);
            }
        }
        writer.flush();
        writer.close();
        if (null != mCallback) {
            mCallback.onFinished(mFilePath);
        }
        Log.e(TAG, "finish encode");
    } catch (Throwable ignored) {
        Log.e(TAG, "error occurred", ignored);
    } finally {
        isRecording = false;
    }
}

run()方法中我们使用了一个while循环依次从mQueueData队列中取出数据并调用encode()方法进行编码,最终的结果通过writer写入到文件中,encode()方法如下:

private void encode(AudioFrameData data, FileOutputStream writer) {
    try {
        if (null == mediaCodec) {
            if (!initMediaCodec(data)) {
                Log.e(TAG, "init mediaCodec failure");
                return;
            }
        }

        final byte[] input = data.data;

        int inputBufferIndex = mediaCodec.dequeueInputBuffer(100);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
            if (null != inputBuffer) {
                inputBuffer.clear();
                inputBuffer.put(input);
                inputBuffer.limit(input.length);
            }

            mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
        }

        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100);

        while (outputBufferIndex >= 0) {

            ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
            if (null != outputBuffer) {
                int outPacketSize = bufferInfo.size + 7;
                byte[] outData = new byte[outPacketSize];
                //添加ADTS头
                addADTStoPacket(outData, outPacketSize, data);


                outputBuffer.position(bufferInfo.offset);
                outputBuffer.get(outData, 7, bufferInfo.size);
                outputBuffer.clear();

                //写到输出流里
                writer.write(outData);

                Log.e(TAG, "origin data length : " + input.length + "   encoded data length : " + outData.length + "    encode percent : " + (outData.length * 1.0f / input.length));
            }

            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100);
        }
    } catch (Throwable ignore) {
        Log.e(TAG, "error occurred", ignore);
        TKLog.e(TAG, "error occurred : " + ignore.getMessage());
    }
}

encode()方法先对编码器mediaCodeC进行校验防止编码器没有初始化,然后调用mediaCodeC的dequeueInputBuffer()方法获取输入索引,根据输入索引获取输入缓冲区ByteBuffer然后往缓冲区ByteBuffer中填入待编码数据;填充数据后调用mediaCodec的dequeueOutputBuffer()方法获取输出索引并根据输出索引获取输出缓冲区的ByteBuffer,该ByteBuffer中存储的就是编码后的AAC数据,但是这种数据流播放器并不能识别需要为其添加头文件,因此调用addADTStoPacket()方法为数据库添加头文件,之后写入文件即可,代码如下:

/**
 * <a href='https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Sampling_Frequencies'>https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Sampling_Frequencies</a>
 */
private void addADTStoPacket(byte[] packet, int packetLen, AudioFrameData data) {
    int profile = getProfile();     // AAC LC
    int freqIdx = getFreqIdx(data); // 44.1KHz
    int chanCfg = getChanCfg(data); // CPE
    packet[0] = (byte) 0xFF;
    packet[1] = (byte) 0xF9;
    packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
    packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
    packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
    packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
    packet[6] = (byte) 0xFC;
}

addADTStoPacket()方法中profile表示AAC编码级别,该值要和前文初mediaFormat里设置的MediaFormat.KEY_AAC_PROFILE的值相同,freqIdx表示当前音频编码的采样率下标,所谓采样率下标就是头文件不写入具体的采用率数值而是通过一个整数来映射采用率,这样可以有效降低AAC文件的大小;chanCfg表示声道数量,它也是使用映射关系来表示声道数量,具体映射关系可参照这里或如下所示:

Saimpling Frequencies and Channel Configurations

需要注意的是addADTStoPacket()方法内profile、freqIdx和chanCfg的参数不要取错了,否则会造成没有声音、有杂音或者语速不正确等情况发生,我当时就是在该出碰了钉子,最后是查阅官方文档才解决了这个问题。现在我们自定义的编码器完整代码如下所示:

public class AACEncoder implements Runnable {

    private static final String TAG = AACEncoder.class.getSimpleName();

    private static final int DEFAULT_FREQIDX = 4;
    private static final int DEFAULT_CHANCFG = 1;

    private MediaCodec mediaCodec;
    private BufferInfo bufferInfo;


    private String mFilePath;
    private volatile boolean isRecording;
    private volatile boolean isStarted;
    private volatile OnEncodeFinishedCallback mCallback;
    private ArrayBlockingQueue<AudioFrameData> mQueueData;

    private Map<Integer, Integer> mFreqIdx = new HashMap<>();
    private Map<Integer, Integer> mChanCfg = new HashMap<>();

    public AACEncoder() {
        mFreqIdx.put(96000, 0);
        mFreqIdx.put(88200, 1);
        mFreqIdx.put(64000, 2);
        mFreqIdx.put(48000, 3);
        mFreqIdx.put(44100, 4);
        mFreqIdx.put(32000, 5);
        mFreqIdx.put(24000, 6);
        mFreqIdx.put(22050, 7);
        mFreqIdx.put(16000, 8);
        mFreqIdx.put(12000, 9);
        mFreqIdx.put(11025, 10);
        mFreqIdx.put(8000, 11);
        mFreqIdx.put(7350, 12);

        mChanCfg.put(1, 1);
        mChanCfg.put(2, 2);
        mChanCfg.put(3, 3);
        mChanCfg.put(4, 4);
        mChanCfg.put(5, 5);
        mChanCfg.put(6, 6);
        mChanCfg.put(8, 7);
    }

    private boolean initMediaCodec(AudioFrameData data) {
        try {
            MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, data.sampleRate, data.channelCount);
            mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, getProfile());
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, data.bitRate);
            mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 64);

            mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mediaCodec.start();

            bufferInfo = new BufferInfo();
            Log.e(TAG, "init success");
            return true;
        } catch (Throwable ignored) {
            Log.e(TAG, "init failure", ignored);
        }
        return false;
    }

    public void setFilePath(String path) {
        this.mFilePath = path;
        deleteFileIfExist(path);
    }

    private void deleteFileIfExist(String path) {
        File file = new File(path);
        if (file.exists() && !file.delete()) {
            try {
                FileWriter writer = new FileWriter(file);
                writer.write("");
                writer.flush();
                writer.close();
            } catch (Throwable ignored) {
            }
            Log.e(TAG, "delete file failure : " + path);
        }
    }


    public void putData(AudioFrameData data) {
        if (null == data) return;
        if (null == mQueueData) {
            mQueueData = new ArrayBlockingQueue<>(50);
        }
        try {
            mQueueData.put(data);
            Log.e(TAG, "after put, capacity is " + mQueueData.size());
        } catch (Throwable e) {
            Log.e(TAG, "error occurred", e);
        }
    }

    private AudioFrameData takeData() {
        try {
            if (null != mQueueData && !mQueueData.isEmpty()) {
                AudioFrameData data = mQueueData.take();
                Log.e(TAG, "after take, capacity is " + mQueueData.size());
                return data;
            }
        } catch (Throwable e) {
            Log.e(TAG, "error occurred", e);
        }
        return null;
    }

    @Override
    public void run() {
        try {
            isRecording = true;
            FileOutputStream writer = new FileOutputStream(mFilePath, true);
            Log.e(TAG, "start encode");
            while (isStarted || !mQueueData.isEmpty()) {
                AudioFrameData data = takeData();
                if (null != data) {
                    encode(data, writer);
                }
            }
            writer.flush();
            writer.close();
            if (null != mCallback) {
                mCallback.onFinished(mFilePath);
            }
            Log.e(TAG, "finish encode");
        } catch (Throwable ignored) {
            Log.e(TAG, "error occurred", ignored);
        } finally {
            isRecording = false;
        }
    }

    public boolean isEncoding() {
        return isRecording;
    }

    public void start() {
        if (TextUtils.isEmpty(mFilePath)) {
            throw new RuntimeException("setFilePath() must be called");
        }
        if (isStarted) return;
        isStarted = true;
        Thread thread = new Thread(this);
        thread.start();
    }

    public void stop() {
        isStarted = false;
    }

    public void release() {
        try {
            isStarted = false;
            mediaCodec.stop();
            mediaCodec.release();
            mediaCodec = null;
        } catch (Throwable e) {
            Log.e(TAG, "error occurred", e);
        }
    }

    private void encode(AudioFrameData data, FileOutputStream fos) {
        try {
            if (null == mediaCodec) {
                if (!initMediaCodec(data)) {
                    Log.e(TAG, "init mediaCodec failure");
                    return;
                }
            }

            final byte[] input = data.data;

            int inputBufferIndex = mediaCodec.dequeueInputBuffer(100);
            if (inputBufferIndex >= 0) {
                ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
                if (null != inputBuffer) {
                    inputBuffer.clear();
                    inputBuffer.put(input);
                    inputBuffer.limit(input.length);
                }

                mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
            }

            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100);

            while (outputBufferIndex >= 0) {

                ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
                if (null != outputBuffer) {
                    int outPacketSize = bufferInfo.size + 7;
                    byte[] outData = new byte[outPacketSize];
                    //添加ADTS头
                    addADTStoPacket(outData, outPacketSize, data);


                    outputBuffer.position(bufferInfo.offset);
                    outputBuffer.get(outData, 7, bufferInfo.size);
                    outputBuffer.clear();

                    //写到输出流里
                    fos.write(outData);

                    Log.e(TAG, "origin data length : " + input.length + "   encoded data length : " + outData.length + "    encode percent : " + (outData.length * 1.0f / input.length));
                }

                mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100);
            }
        } catch (Throwable ignore) {
            Log.e(TAG, "error occurred", ignore);
        }
    }

    /**
     * <a href='https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Sampling_Frequencies'>https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Sampling_Frequencies</a>
     */
    private void addADTStoPacket(byte[] packet, int packetLen, AudioFrameData data) {
        int profile = getProfile();     // AAC LC
        int freqIdx = getFreqIdx(data); // 44.1KHz
        int chanCfg = getChanCfg(data); // CPE
        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
    }

    private int getProfile() {
        return MediaCodecInfo.CodecProfileLevel.AACObjectLC;
    }

    private int getFreqIdx(AudioFrameData data) {
        Integer freqIdx = mFreqIdx.get(data.sampleRate);
        return null == freqIdx ? DEFAULT_FREQIDX : freqIdx;
    }

    private int getChanCfg(AudioFrameData data) {
        Integer chanCfg = mChanCfg.get(data.channelCount);
        return null == chanCfg ? DEFAULT_CHANCFG : chanCfg;
    }

    public void setEncodeFinishedCallback(OnEncodeFinishedCallback callback) {
        this.mCallback = callback;
    }

    public interface OnEncodeFinishedCallback {
        void onFinished(String path);
    }

    public static final class AudioFrameData {
        public byte[] data;
        public int sampleRate;
        public int channelCount;
        public int bitRate;
    }
}

好了,AACEncoder类已经完成了,通过测试编码后的数据量比原始数据小了20%以上。另外MediaCodec只能在Android 5.0以上版本中使用,如果想在5.0以下版本中实现PCM转AAC的功能,可以使用ffmpeg库来实现,Github上已有大神编译出了库http://writingminds.github.io/ffmpeg-android-java/ 可以直接使用。由于篇幅原因,我将在下篇文章中具体讲解一下AAC数据格式,敬请期待(*^__^*) ……

iOS无埋点方案

1.背景

埋点分为客户端埋点(前端埋点)和服务端埋点(后端埋点),关于后端埋点这里不做展开描述。本文介绍的 iOS 埋点属于客户端埋 点,客户端埋点一般是针对用户操作进⾏上报,如用户的点击操作,⻚⾯路径等。传统的代码埋点对代码侵入⾮常⼤,和代码耦合⾮常高,随着需求的改变,代码和埋点都要进⾏调整。基于此,无埋点数据统计被很多公司应用。

2. iOS 埋点方案介绍

iOS 埋点方案主要可以分为三种:
2.1 代码埋点
概念 代码埋点就是在代码里需要上报的地⽅添加上报的代码。 优点
开发简单。 可以精确的控制上报的事件和时机。 方便地把自定义的事件和详细的参数上报。
缺点
对代码侵入⽐较大。
埋点⼯作量大,必须是技术开发人员完成。 更新或新增埋点成本较高,如果遇到漏埋或者需要新埋的点,需要重新发版。

2.2 可视化埋点
概念 可视化埋点是用可视化的⽅式,提前把需要采集的控件事件进行圈选,并下发给客户端,⽤户进行操作时,埋点SDK会自动根据下发的配置进行上报,不需要进行代码埋点。
Mixpanel 提供了可视化埋点的⽅方案,并且把代码进⾏了开源。SensorsData 参考了了 Mixpanel,也进⾏了开源。本文介绍的技术⽅案参考了这两个开源框架。
优点
解决了代码埋点的侵入性、⼯作量和发版成本问题。
缺点
不是所有的事件都可以圈选埋点,⽐如一些和业务逻辑耦合比较紧的事件埋点。 ⽆法在埋点时增加⾃定义的字段。

2.3 无埋点
概念 无埋点最早是在2013年被Heap Analytics等公司提出。无埋点就是把所有的控件操作全部由埋点SDK⾃动收集,所以也被称为“全 埋点”。无埋点和可视化埋点⾮常相似,区别是可视化埋点是先进行圈选再上报,也就是只上报圈选配置的事件;⽆埋点是先上报用户操作,再在服务端进行圈选分析。
优点
在可视化埋点的基础上,可以分析已经上报的数据。也就是如果哪天突然想对某个⽤户操作进⾏分析,历史数据也可以分析。
缺点
不是所有的事件都可以圈选埋点,⽐如一些和业务逻辑耦合比较紧的事件埋点。 ⽆法在埋点时增加⾃定义的字段。 此外因为上报了所有的⽤户的操作,相⽐可视化埋点⽹络负担稍微增⼤。

3. ⽆无埋点技术

由于可视化埋点和无埋点核心技术点相同,可视化埋点也可以看做⽆埋点的一种⽅方式。下⾯主要介绍几个这两种埋点方案的核心技术解决⽅方案。
1. 如何不侵⼊业务代码⾃动进行用户操作的收集。
2. 事件的唯一标识(即 ID) 如何确定。
3. 圈选如何实现。

3.1 如何不侵入代码⾃自动进行收集

3.1.1 AOP
这⾥提到⼀个重要的概念 AOP,AOP 是 Aspect Oriented Programming 的缩写,意为面向切面编程。AOP 是一种编程思想,和 OOP(⾯向对象编程) ⾮常相似。AOP 是对业务处理过程中的切⾯进⾏提取,在统⼀的地⽅进行处理,可以降低各个逻辑部分之间的耦合。
我们根据下⾯面的两张图来直观地体会下 AOP 。

从图中我们可以看出,如果我们要在不同的地⽅执行一些不同的操作,但都会在操作前的某一个时机做相同的动作(如用户验证,⽇志 打印,数据上报等等)。这种情况传统的做法是每个地方都调⽤一遍相关接⼝API。

再看上图,把验证这块整体框出来,可以形象地理解为就像一个板子,这块板子插入上面的逻辑流程。 再举个例子,iOS里想在所有 Controller 的 ViewDidAppear ⾥加⾏日志,可以用 AOP 很好地实现。

3.1.2 Runtime Method Swizzling

苹果有提供对应的 API 接口,如 method_exchangeImplementations()

3.1.2.2 消息转发拦截

我们知道,OC 发送消息时如果无法识别一个 Selector 时,会进行消息转发服务。消息转发分为三步, 1. ⽅法解析处理,2. 备援接受者,3. 完整的消息转发。

Aspects 是一套经典的应⽤ AOP 思想的 iOS 框架,它具有良好的⽤户体验,比较安全,⽀持撤销等优点。它的总体方案是参考 KVO, 通过动态生成子类,子类把原⽅法的 IMP 动态指向 _objc_msgForward ,当方法触发的时候,会直接进⾏消息转发。 通过把
forwardInvocation 指向自定义的⽅法,在⾃定义的⽅法里进⾏拦截操作处理。注意:Aspects 和 method_exchangeImplementations 同时使⽤会有冲突。

3.2 事件的唯⼀标识(即 ID) 如何确定。

要实现埋点事件的唯⼀性,需要对每个用户操作确定一个唯⼀标识,以此区分不同的事件。
下面介绍的方法和 XPath 的思路是相同的,XPath 是一门在 XML ⽂档中查找信息的语⾔,使⽤路径表达式来选取 XML ⽂档中的节点或节点集。
我们知道 APP 所有的⻚面可以看做一颗树的结构,对于屏幕中任何一个 view 对象,都可以得到⼀条唯⼀的从 window 到它的 path路径。唯一标识可以由 path路径、类名和一些属性的值来确定,称为 viewPath。示例例如下:

这⾥生成 viewPath 会把 UIWindow 和系统的⼀些 Controller 忽略掉,减少 viewPath 的⻓度。还会做⼀些优化,如果路径中多个 view 的层级相同,会加上 view 在 superview 的 index 作为区分。

参考框架:
1. Mixpanel(开源)
2. SensorsData(神策)(开源) >. Heap
3. GrowingIO
4. MTA(腾讯移动分析)
5. 友盟
6. TalkingData
7. MTJ(百度移动统计)
8. GoogleAnalytics(⾕谷歌统计)
9. HubbleData(⽹网易易内部使⽤用)
10. 此外,iOS⽆无埋点数据SDK实践之路路和iOS⽆无埋点数据SDK的整体设计与技术实现这两篇⽂文章实现讲述了了业务数据⽆无埋点的⽅方案。主 要思路路是提前配置eventID和业务参数的KVC。

Web Workers 初探

介绍

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

API

首先我们来总览下Web Workers的Api

主线程:

  1. 构造函数 Worker(), 用来初始化一个worker对象

worker对象的属性和方法如下

  1. self.name: Worker 的名字。该属性只读,由构造函数指定。
  2. self.onmessage:指定message事件的监听函数。
  3. self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  4. self.close():关闭 Worker 线程。
  5. self.postMessage():向产生这个 Worker 线程发送消息。
  6. self.importScripts():加载 JS 脚本。

子线程:

注意,子线程是有自己的全局对象的,也就是说子线程是没办法调取获取主线程的windows对象。

  1. self.name: Worker 的名字。该属性只读,由构造函数指定。
  2. self.onmessage:指定message事件的监听函数。
  3. self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
  4. self.close():关闭 Worker 线程。
  5. self.postMessage():向产生这个 Worker 线程发送消息。
  6. self.importScripts():加载 JS 脚本。

使用方法

创建一个worker

Worker构造函数可以接收两个参数,第一个是jsUrl,也就是work要执行的js脚本,第二个参数是option,可以指定worker的名称

    const myWorker = new Worker('mywork.js', {name: 'mywork'})

主线程像子线程发消息

myWorker.postMessage('Hello 子线程!')

子线程接收消息

self.onmessage = function (e) {
  console.log('来自主线程的消息:' + e.data);
}

子线程发消息

self.postMessage('Hello 主线程!')

主线程接收消息

myWorker.onmessage = function (e) {
  console.log('来自主线程的消息:' + e.data);
}

关闭子线程

// 主线程主动关闭
myWorker.terminate();

// 子线程主动关闭
self.close();

对Web Workers的一些思考

目前来看,web worker对传统的网站开发作用不是很大,但是在一些特殊领域会发挥非常大的作用

  1. 图片处理、文件加密、视频转码等运算密集型的功能上
  2. 非浏览器宿主环境的应用,JS的领域目前不止web领域,在桌面啊客户端、游戏引擎、云服务中都可以拿JS来写业务,这些业务中是Web Worker的。
  3. 需要用到多核性能的场景,例如大数据处理等。

聊聊全链路压测(二)

全链路压测流量和数据的隔离

因为全链路压测是在实际的生产环境中执行的,所以测试产生的数据与真实的用户数据必须进行有效隔离,以防止压测的流量和数据污染、干扰生产环境的情况。比如,不能将压测数据记录到统计分析报表里;再比如,压测完成后可以方便地清洗掉压测产生的数据。

为了达到这个目的,我们就需要对压测流量进行特殊的数据标记,以区别于真实的流量和数据。这就要求各个链路上的系统,都能传递和处理这种特殊的数据标记,同时写入数据库中的数据也必须带有这种类型的标记以便区分数据,或者直接采用专门的影子数据库来存储压测的数据。

可以看出,为了实现压测产生的真实的流量和数据隔离,我们就需要对各个业务模块和中间件进行特殊的改造和扩展。而这个工作量相当大,而且牵涉的范围也非常广,也就进一步增加了实施全链路压测的难度。

而且通常来讲,首次全链路压测的准备周期会需要半年以上的时间,这其中最大的工作量在于对现有业务系统和中间件的改造,来实现压测流量和数据的隔离。所以,在实际得工程项目中,如果全链路压测不是由高层领导直接牵头推动的话,很难推进。

另外,在对各个业务模块和中间件添加特殊标记的改造过程中,我们会尽可能少地改动业务模块,而是更倾向于通过中间件来尽可能多地完成特殊数据标记的处理和传递。

实际业务负载的模拟

一直以来,如何尽可能准确地模拟业务系统的负载,都是设计全链路压测时的难题。这里的难点主要体现在两个方面:首先,要估算负载的总体量级;其次,需要详细了解总负载中各个操作的占比情况以及执行频次。

业界通常采用的策略是,采用已有的历史负载作为基准数据,然后在此基础上进行适当调整。具体到执行层面,通常的做法是,录制已有的实际用户负载,然后在此基础上做以下两部分修改:
1、录制数据的清洗,将录制得到的真实数据统一替换成为压测准备的数据,比如,需要将录制得到的真实用户替换成专门为压测准备的测试用户等等。
2、基于用户模型的估算,在全链路压测过程中,按比例放大录制脚本的负载。

真实交易和支付的撤销以及数据清理

由于全链路压测是在真实的生产环境中进行的,那么完成的所有交易以及相关的支付都是真实有效的,所以我们就需要在测试结束后,将这些交易撤销。

因为,我们已经对这些交易的流量和数据进行了特定标记,所以我们可以比较方便地筛选出需要撤销的交易,然后通过自动化脚本的方式来完成批量的数据清理工作。

扒公众号视频及插件Puppeteer介绍

介绍

PuppeteerJs是一款无头浏览器库,类似于命令行版的Chrome。写过爬虫和做过网站SEO的同学一定知道。他可以用来做非常多的事,提供的能力超过真正的浏览器。比如

1.将网页导出为PDF图片之类。
2.预渲染单页面应用然后爬下来。
3.自动提交表单键盘输入等。
4.修改UA及窗口尺寸进行UI测试。
5.创建一个拥有最新js特性的自动更新的测试环境。
6.对网站进行性能检测。
7.Chrome插件开发

项目由谷歌团队维护,目的有5点1.提供一个精简的规范库,突出显示DevTools协议的功能。2.为类似的测试库提供参考。其他框架可以采用Puppeteer作为它们的基础层。3.促进无头/自动化浏览器测试的采用。4.帮助谷歌自家的产品找bug,开发DevTools新功能。5.找到浏览器自动化测试的痛点,并解决它。

实战爬取微信公众号视频

首先找一个带视频的公众号,比如这个小米的https://mp.weixin.qq.com/s/MpxPN_Rhv20Jr5V_3lC_6g
里面是雷军和王源在打广告。视频全部使用的是腾讯视频的iframe。估计是为了方便打广告,在我印象中去年用的还是一个简单的video标签。
用浏览器打开后审查元素分析结构(其实就是搜一下有没有video标签)。

找到了两个标签,但是没有找到地址,于是向上看,去到这个iframe地址。打开后是腾讯视频的网站了。接着搜video标签,这下查到地址了。所以整体的流程就是1.打开公众号地址找到iframe。2.打开iframe地址找到video标签的地址。

部分代码
// 创建浏览器,有头模式运行
const browser = await puppeteer.launch({
    headless: false
  });
//设置ua,我用了华为p10的
 await page.setUserAgent(UA);
// 跳转到公众号地址
 await page.goto(URL);
// 等待1秒钟,因为公众号是SPA模式,我们需要等页面全部load完成
await page.waitFor(1000);
// 找到腾讯视频iframe的地址 跳转过去然后再等1秒种
const bodyHandle1 = await page.$('body');
const iframeURL = await page.evaluate(body =&gt; {
    return body.querySelector('iframe').src;
}, bodyHandle1);
await page.goto(iframeURL);
await page.waitFor(1000);
// 点击播放按钮,唯一一个<a>标签,地址会变为视频地址
await page.click('a')
// 拿到src
const videoSrc = page.url()
// 使用superagent插件来下载资源,生成本地mp4文件
superagent
    .get(videoSrc)
    .end((err, res) =&gt; {
    fs.writeFile(`${__dirname}/video.mp4`, res.body, function () {
        spinner.succeed('下完啦~~~')
        setTimeout(() =&gt; {
            process.exit()
        }, 1000)
    });
})
需要注意的点

不知道腾讯视频是怎么标注用户的,正常浏览器端页面内用了自己写的webComponent。包含广告,推荐视频等业务功能,但是用无头浏览器就会变成一个简简单单的video标签,通过一个a标签触发。所以我们再分析页面结构的时候需要开着Puppeteer的有头模式。能够帮我们绕过一些坎儿。
无头浏览器渲染的腾讯视频结构

普通浏览器渲染的腾讯视频结构

展示


OkHttp源码解析(二) —线程管理

跟上篇文章一样,我们还是按照请求和响应的处理流程梳理OkHttp的源码,不过这次重点放到线程管理这一部分。

入口仍然是两种调用方式,同步调用:

Response execute() throws IOException;

异步调用:

void enqueue(Callback responseCallback);

具体的实现类RealCall, 同步调用全部在当前线程处理,得到响应,并不会使用OkHttp内部创建的线程,因此我们重点查看异步调用的实现:

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));//生成一个AsyncCall对象,并调用dispatcher的enqueue方法
  }

最关键的只有一行代码,生成一个AsyncCall对象,并调用dispatcher的enqueue方法,因此我们查看enqueue方法:

  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

我们看到OkHttp在这里做了并发控制,如果总并发数小于允许的最大并发数,且对于单一域名的并发数小于允许的最大并发数,则调用excutorService的execute去执行,否则加入准备队列。多线程控制肯定在excutorService无疑了。

executorService实现如下:

  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

我们看到如果没有设置自定义的executorService, 就会创建一个线程池, 查看OkHttpClient类的代码, 默认的HttpClient.Bulder 创建了一个dispatcher:dispatcher = new Dispatcher(); 默认的构造函数并没有对executorService赋值,因此可以确定OkHttpClient默认即是采用的这个ThreadPoolExecutor。

于是关键的代码就在于这个ThreadPoolExecutor的构造函数了,相关文档说明如下:

    /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters and default rejected execution handler.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

根据传入的参数和文档说明, 这个线程池的配置如下: 核心线程为0, 最大线程数为2^32-1, 非核心线程的活跃时间为60秒。单纯考虑这个线程池并未限制线程数,综合考虑dispatcher自己的限制:

private int maxRequests = 64;
private int maxRequestsPerHost = 5;

因此就只有最多64个并发请求,如果App内只有1个域名的话,最多只能有5个并发请求。

再来看一下ThreadPoolExecutor的workQueue 参数: new SynchronousQueue(), 字面意义是同步队列,扩展了AbstractQueue 并实现了BlockingQueue , BlockingQueue定义了阻塞队列的接口,阻塞队列在获取元素时如果队列为空则等待,同样插入队列元素时,如果队列满了也等待。

同步队列的说明如下:

/**
 * A {@linkplain BlockingQueue blocking queue} in which each insert
 * operation must wait for a corresponding remove operation by another
 * thread, and vice versa.  A synchronous queue does not have any
 * internal capacity, not even a capacity of one.  You cannot
 * {@code peek} at a synchronous queue because an element is only
 * present when you try to remove it; you cannot insert an element
 * (using any method) unless another thread is trying to remove it;
 * you cannot iterate as there is nothing to iterate.  The
 * <em>head</em> of the queue is the element that the first queued
 * inserting thread is trying to add to the queue; if there is no such
 * queued thread then no element is available for removal and
 * {@code poll()} will return {@code null}.  For purposes of other
 * {@code Collection} methods (for example {@code contains}), a
 * {@code SynchronousQueue} acts as an empty collection.  This queue
 * does not permit {@code null} elements.
 *
 * <p>Synchronous queues are similar to rendezvous channels used in
 * CSP and Ada. They are well suited for handoff designs, in which an
 * object running in one thread must sync up with an object running
 * in another thread in order to hand it some information, event, or
 * task.
 *
 * <p>This class supports an optional fairness policy for ordering
 * waiting producer and consumer threads.  By default, this ordering
 * is not guaranteed. However, a queue constructed with fairness set
 * to {@code true} grants threads access in FIFO order.
 *
 * <p>This class and its iterator implement all of the
 * <em>optional</em> methods of the {@link Collection} and {@link
 * Iterator} interfaces.
 ```
简要总结下是说在插入同步队列元素时,需要等待其他线程移除元素,在取元素时需要等待其他线程插入元素, 同步队列内部并不存储元素,因此不能peek。  同步队列也实现了一个可选的公平策略,允许等待线程排序,默认不采用公平策略。

最后看一下threadFactory参数: `Util.threadFactory("OkHttp Dispatcher", false)`, 实现如下:

```java
public static ThreadFactory threadFactory(final String name, final boolean daemon) {
    return new ThreadFactory() {
      @Override public Thread newThread(Runnable runnable) {
        Thread result = new Thread(runnable, name);
        result.setDaemon(daemon);
        return result;
      }
    };
  }

生成了一个Thread,同时设置成非守护线程(用户线程),守护线程和非守护线程的唯一区别在于: 如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

至此,我们搞明白了OkHttp对于异步和线程这块的管理。

安卓注解使用介绍

在Java中,注解(Annotation)引入始于Java5,用来描述Java代码的元信息,通常情况下注解不会直接影响代码的执行,尽管有些注解可以用来做到影响代码执行。

在代码文件中使用‘@’字符告诉编译器接下来的是一个注解。注解可以用在类,构造方法,成员变量,方法,参数等的声明中。作用主要是对编译器警告等辅助工具产生影响,如果传递了错误的类型那么编译器就会发出警告,这样就可以在编码和维护的过程中辅助发现问题,提高开发效率,提升代码质量,也促进形成编码规范。

安卓开发中用到的注解主要有四个方面:JDK内置注解、JDK自定义注解包、android sdk内置注解、android.support.annotation注解。

JDK内置注解

如下图,左侧是JDK提供的三个标准注解,在java.lang包内。右侧JDK提供的四个是对自定义注解的支持,在java.lang.annotation包内。

@Deprecated是一个标记注解,表示被标记的成员变量或者成员方法已经不建议使用,原因可能是这个方法/变量有缺陷或者在新的SDK中已经不被支持。

@Override注解在继承过程中,标识对父类方法的覆盖关系。这个在Java中不是必须的,但是建议在需要的地方强制使用。防止在子类或者父类中误操作修改方法签名或者遗漏相关的代码(kotlin中对于子类覆盖父类方法强制使用override关键字,不再需要注解标记)。

@SuppressWarnings用来抑制编译器生成警告信息,对指定类型的警告保持静默。可以修饰类、方法、方法参数、属性和局部变量,采用就近原则,尽量放在被需要静默的警告语句附近。接收一个字符串或者一个字符集作为参数(参数详细介绍参考文章),它指示将取消的警告。

JDK提供了四种元注解,支持用户对注解进行自定义扩展。自定义注解后面会详细介绍,这里先略过。

Android SDK内置注解

Android SDK注解有两个@SuppressLint和@TargetApi。


这两个注解是使用Lint静态检测对应的标记。如果禁用了Lint,用或者不用这些注解都没有太大关系。

@TargetApi:Android工程需要设置所支持的最小的系统版本,Android Studio是在Gradle中设置minSdkVersion的值。如果某个方法或者类被声明需要在某个版本和更高版本的系统上运行,可以使用@RequiresApi(requires)声明支持的最小系统版本。当在声明minSdkVersion的工程中使用了requires大于minSdkVersion的类或者方法时,Lint就会报错误提醒,这时候可以使用@TargetApi使Lint保持静默,但是要添加代码为低版本的系统提供对应的备选方案,否则在低版本系统上运行会产生崩溃。

@SuppressLint:上面的@TargetApi注解只针对API版本进行注解,使Lint对版本错误保持静默。@SuppressLint针对的范围更广,通过设置一个参数(identified by the lint issue id)通知Lint对相应的警告⚠和错误❎️保持静默。

@SuppressLint(“NewApi”),这个注解可以实现@TargetApi(version)相同的作用,只是没有指定特定的API版本,导致工程师和Lint都不知道响应的范围,容易导致错误,不建议使用。

android support注解

Android Support Library提供com.android.support:support-annotations对Android的注解进行了拓展。下图以v25为例列出了所有定义的46个注解(到v27增加了5个,分别为ColorLong、FontRes、GuardedBy、HalfFloat、NavigationRes)。

  • 其中22个资源类注解,如下:
  • 取值范围类3个注解,IntRange/FloatRange对响应类型的变量或参数规定取值范围,参数有from和to两个;Size对数组的长度约束,参数min/max(含)组合声明数组的长度范围,参数value指定数据的具体长度,参数multiple指定数组长度必须是某个数字的倍数。
  • Android中新引入的替代枚举的注解有IntDef和StringDef,他们唯一的区别一个是int类型,一个是string类型,下面我们列一下官方API文档中给出的使用方法。

枚举类型的注解的使用,先定义一系列可用的取值,然后定义一个注解使用@IntDef或者@StringDef指定新的注解可用的取值列表。

使用 Enum 的缺点:每一个枚举值都是一个对象,在使用它时会增加额外的内存消耗,所以枚举相比于Integer和String会占用更多的内存,较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的开销,使我们的应用需要更多的空间。特别是分dex的大APP,枚举的初始化很容易导致ANR。

@Retention(SOURCE)
@StringDef({
    POWER_SERVICE,
    WINDOW_SERVICE,
    LAYOUT_INFLATER_SERVICE
})
public @interface ServiceName {}
public static final String POWER_SERVICE = "power";
public static final String WINDOW_SERVICE = "window";
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
@Retention(SOURCE)
@IntDef({
    NAVIGATION_MODE_STANDARD, 
    NAVIGATION_MODE_LIST, 
    NAVIGATION_MODE_TABS
})
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
  ...
public abstract void setNavigationMode(@NavigationMode int mode);
@NavigationMode
public abstract int getNavigationMode();

自定义注解

上面的枚举注解,就是自定义注解的例子。自定义注解使用关键字@interface声明,然后用相应的修饰。

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Entity {
    String value();
    String name();
}
  • @Documented表示拥有该注解的元素可通过javadoc此类的工具进行文档化。该类型应用于注解那些影响客户使用带注释(comment)的元素声明的类型。如果类型声明是用Documented来注解的,这种类型的注解被作为被标注的程序成员的公共API。
  • @Inherited:表示该注解类型被自动继承
  • @Retention:表示该注解类型的注解保留的时长。可用的参数都在枚举类型RetentionPolicy中给出了定义。当注解类型声明中没有@Retention元注解,则默认保留策略为RetentionPolicy.CLASS。
  • @Target:表示该注解类型的所使用的程序元素类型。可用的参数都在枚举类型ElementType中给出了定义。当注解类型声明中没有@Target元注解,则默认为可适用所有的程序元素。

附录:ButterKnife

使用ButterKnife只需要在module的gradle文件中加入下面代码:

implementation 'com.jakewharton:butterknife:8.4.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'

在library工程中,直接使用R.xxx.xxx会报“元素值必须为常量表达式”的错误提示,处理步骤如下:

  • 在根gradle文件中添加(版本根据需要变化):
classpath 'com.jakewharton:butterknife-gradle-plugin:8.4.0'
  • 在module的gradle中添加:
apply plugin: 'com.jakewharton.butterknife'
  • R.xxx.xxx替换成R2.xxx.xxx

参考文章