精华内容
下载资源
问答
  • Pick 语音App是一款专为年轻群体打造的“开黑约玩”+“语音交友”的社交应用,该App提供语音服务,针对热门手游进行专业的开黑语音技术指导;并通过多种匹配机制,帮玩家秒配达人组队吃鸡开黑;玩家可以在线录音交友...

    pick语音

    概述

    Pick 语音App是一款专为年轻群体打造的“开黑约玩”+“语音交友”的社交应用,该App提供语音服务,针对热门手游进行专业的开黑语音技术指导;并通过多种匹配机制,帮玩家秒配达人组队吃鸡开黑;玩家可以在线录音交友,多种品类录音任其发挥:劲歌金曲对唱、经典影视配音,模仿秀;玩家还可以在【聊天室】在线实时语音,用声音结识更多朋友。 匿名发布烦恼,在私密的世界窥探别人的心事。

    基本信息

    软件名称 Pick 语音
    开 发 商 厦门迅熙网络科技有限公司
    软件平台 Android4.2、IOS 8.0以上
    软件版本 v3.3.1
    更新时间 2019/05/13
    软件语言 简体中文
    软件大小 37.5MB
    类 型 社交

    基本简介

    Pick语音是一款以“开黑约玩”+“语音交友”为核心的社交类APP。
    用户可以在Pick语音APP中,根据自身的约玩需求,一键组队, 和达人一起开黑,一起上分。 一键下单和普通下单,通过线上预约下单的模式,达成约玩和技能服务 。
    用户通过Pick语音APP,寻找心仪的“达人”。APP中约玩类型覆盖广泛,主要以“竞技游戏”为切入点 ,包括有和平精英、王者荣耀、绝地求生、英雄联盟、第五人格等各种游戏的美女帅哥陪玩 。还有亲密无间的娱乐社交,一键结交好友,包括语音约玩;灵魂声线,情感交友;超凡技能,极速赚钱等。

    软件特点

    【声潮】用声音发现小伙伴,用声音结交朋友!在这里,左右滑动就可寻找喜欢的声音;戳下红心,把每个好声音都收藏进宝库;美丽的声音和声优都在这;
    【聊天室】告别看脸时代,用声音展现魅力,多人连麦,亲密无间,处CP、找老铁,一键约玩,边玩边聊边交友;在线语音聊天,用声音认识更多的人;
    【动态社区】汇集时下最热门话题,零距离互动,寻找志趣相投的人;
    【开黑约玩】线上组局,一秒匹配游戏达人,让电竞达人带你飞,陪你嗨不停;
    【树洞】不想做“不动声色”的成年人?!想找到懂你的人?!树洞可以听你的私语,做你情绪垃圾桶;
    【星球】你常常因为朋友不够沙雕而和你格格不入?建专属星球,找到和你一样沙雕的TA

    主要功能

    首页

    板块

    【声潮】:爱唱歌、声控们的福利。用户在线录音交友,多种品类录音任其发挥:唱歌、配台词,沙雕配,自由发挥;通过智能匹配,左右滑动寻找声音,聆听各种苏到你的声音,收藏或者撩一下,与心仪的TA畅快嗨聊;

    【聊天室】:开放多人聊天室,选择自己喜欢的声音,在线与小哥哥小姐姐聊天互动 ,红包礼物刷不停,达人带你一起飞。聊天室分情感厅和派单厅,每个房间都有一个主持人负责接待,主持人会根据玩家需求,安排符合要求的达人上麦,在聊天室打赏便可享受更多服务;达人也可以在这里,用声音展现自己的技能。如果觉得自己气质佳声音美也可以申请当主持人,充分发挥自己的特长。
    【树洞】:匿名发布烦恼,放置情绪的频道。用户无固定身份:无头像、个人主页,昵称可每日修改;无社交关系链:没有关注、转发,只基于内容,吸引兴趣相近的人,进行匿名互动,在私密的世界窥探别人的心事。
    【服务品类】Pick语音APP中,有10多种线上约玩品类,致力为用户打造多样化的娱乐交互平台。游戏品类主要有和平精英、王者荣耀、绝地求生、英雄联盟、第五人格等;休闲娱乐品类主要有歌手、叫醒、哄睡、情感咨询、虚拟恋人、声音鉴定等。

    动态

    用户可以浏览“关注”、“推荐”、“最新”的动态信息 ,动态汇集最新热门话题、热门段子、各种表情包神回复等,热门、爆料、励志等应有尽有,让你停不下来刷动态;别人话题不吸引?可以免费创建话题,发布日常动态,随时随地,分享热门话题,把志趣相投的人聚在一起互动聊天,让社区更加活跃。

    消息

    用户将在这里接收官方消息;互动消息,完成在线沟通,除了常规的交流外,还可以赠送丰富的钻石小礼物,增加用户之间的亲密度;第一时间接收订单消息,根据约玩需求(时间、地点等)进行约玩;还可以查看个人关注和互粉的数据。

    我的

    用户可以通过“我的”模块,在个人主页里编辑或分享个人基本资料,展示个人形象照,查看陪玩技能品类和接单次数,以及动态详情;
    【达人认证】如果你从菜鸟成长起来了,可以认证为“达人”,技能认证,开启陪玩之旅;
    【我的钱包】账户明细,可以查看充值和可提现的Doki值;
    【装扮大厅】用户可以在装扮大厅,选择自己喜欢的挂件,做独一无二的自己,为自己代言;
    【星球】:可以定制兴趣,添加标签,寻找志同道合的朋友;所有的标签,构成专属的交友星球;陌生人可通过定制的专属星球了解你,与之深入交流。

    版本更新

    更新时间:2019/05/13
    版本号:v3.3.1
    官网地址
    应用宝下载链接

    展开全文
  • Speex说一种有损的语音压缩算法,可以极大的压缩语音文件大小。 官网地址 个人认为翻译文档最全的地址 简单说下个人理解: 我们需要知道speex的数据帧概念,还要知道音频的采样率、比特率等基本概念。 speex默认的...

    speex简介

    Speex说一种有损的语音压缩算法,可以极大的压缩语音文件大小。
    官网地址
    个人认为翻译文档最全的地址
    简单说下个人理解:

    • 我们需要知道speex的数据帧概念,还要知道音频的采样率、比特率等基本概念。
    • speex默认的三种模式,nb,wb,uwb对应的采样率分别是8000,16000,32000;其对应的帧大小分别为:160,320,640.这点很重要。
    • 我们需要知道不同采样率,不同压缩质量下,其对应的压缩前后数据大小,对于我们解压来说很重要。

    网上目前大多数都是基于nb,也就是8000采样率的例子。本文是16000,也就是wb模式,压缩质量为8.其对应的帧、压缩前数据大小、压缩后数据大小为:320,640,70.我们这里采用的是定长压缩。没有用动态压缩,动态情况下,压缩后数据大小不可知。

    服务端语音压缩

    环境介绍

    1. speex框架:开源的speex4j
    2. springboot 2.4.4

    pom依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.orctom</groupId>
            <artifactId>speex4j</artifactId>
            <version>1.0.3</version>
        </dependency>
    </dependencies>

    压缩代码很简单如下:

    @GetMapping("/pcm2spx")
    public void pcm2spx(HttpServletResponse response){
        try(InputStream inputStream = new FileInputStream(ResourceUtils.getFile("test.wav"));
            ByteArrayOutputStream bos = new ByteArrayOutputStream()){
            int len = 0;
            byte[] buff = new byte[1024];
            // 读取文件
            while ((len = inputStream.read(buff)) > 0) {
                bos.write(buff, 0, len);
            }
            // 去掉wav文件头
            byte[] source = new byte[bos.toByteArray().length - 44];
            System.arraycopy(bos.toByteArray(), 44, source, 0, source.length);
            response.getOutputStream().write(SpeexUtils.pcm2spx(source));
            response.getOutputStream().flush();
            response.getOutputStream().close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    说明:

    1. 如果流式处理压缩,请使用SpeexEncoder和SpeexDecoder类,不要直接使用speexUtils类。
    2. 如果流式压缩,请在每次压缩/解压时传入帧数的整数倍,对于我们这里来说就是压缩时,每次传入640的整数倍数据,解压时传入71的整数倍。
    3. 来说下,为什么是71.上文说了,speex,wb模式,质量8,定长压缩的话,其压缩后数据大小是70.但是speex4j,自己在压缩数据头增加了一个字节用于记录当前压缩后数据大小。因此,使用speex4j会多出一个头字节。我们自己解压时也需要注意,将该字节剔除。

    终端解压

    终端so库封装

    可以参考上文里面提到的文档最全的地址。也可以参考本文,其实,本文就是参照上面内容来做的。

    1. 登录speex官网,下载源码,本文下载的是1.20版本

    2. 新建android项目

    3. 在项目根目录下,新建jni目录,并将speex的include和libspeex文件夹拷贝到该目录下:
      创建目录

    4. 进入jni/include/speex/目录,新建speex_config.type.h头文件。内容如下:

    #ifndef __SPEEX_TYPES_H__
    #define __SPEEX_TYPES_H__
    typedef short spx_int16_t;
    typedef unsigned short spx_uint16_t;
    typedef int spx_int32_t;
    typedef unsigned int spx_uint32_t;
    #endif
    1. 进入jni目录,新建Android.mk内容如下:
    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    
    LOCAL_LDLIBS += -llog
    
    LOCAL_MODULE := libspeex
    
    LOCAL_CFLAGS = -DFIXED_POINT -DUSE_KISS_FFT -DEXPORT="" -UHAVE_CONFIG_H
    
    LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
    
    LOCAL_SRC_FILES := speex_jni.cpp \
        ./libspeex/bits.c \
        ./libspeex/cb_search.c \
        ./libspeex/exc_10_16_table.c \
        ./libspeex/exc_10_32_table.c \
        ./libspeex/exc_20_32_table.c \
        ./libspeex/exc_5_256_table.c \
        ./libspeex/exc_5_64_table.c \
        ./libspeex/exc_8_128_table.c \
       ./libspeex/filters.c \
       ./libspeex/gain_table.c \
       ./libspeex/gain_table_lbr.c \
       ./libspeex/hexc_10_32_table.c \
       ./libspeex/hexc_table.c \
       ./libspeex/high_lsp_tables.c \
       ./libspeex/kiss_fft.c \
       ./libspeex/kiss_fftr.c \
       ./libspeex/lpc.c \
       ./libspeex/lsp.c \
       ./libspeex/lsp_tables_nb.c \
       ./libspeex/ltp.c \
       ./libspeex/modes.c \
       ./libspeex/modes_wb.c \
       ./libspeex/nb_celp.c \
       ./libspeex/quant_lsp.c \
       ./libspeex/sb_celp.c \
       ./libspeex/smallft.c \
       ./libspeex/speex.c \
       ./libspeex/speex_callbacks.c \
       ./libspeex/speex_header.c \
       ./libspeex/stereo.c \
       ./libspeex/vbr.c \
       ./libspeex/vorbis_psy.c\
       ./libspeex/vq.c \
       ./libspeex/window.c
    
    include $(BUILD_SHARED_LIBRARY)

    请注意,这里引入的c文件,一定适合include目录下对应的。请根据自己下载speex源码做增减。

    1. 新建Application.mk
      这里我们编译全环境
    APP_ABI := all
    1. 创建我们自己的逻辑文件,speex_jni.cpp
    #include <jni.h>
    
    #include <string.h>
    #include <unistd.h>
    //日志输出
    #include <android/log.h>
    #include <speex/speex.h>
    //标志位,标识是否开启编码
    static int codec_open = 0;
    //解码的帧长度
    static int dec_frame_size;
    //编码的帧长度
    static int enc_frame_size;
    
    static SpeexBits ebits, dbits;
    void *enc_state;
    void *dec_state;
    
    static JavaVM *gJavaVM;
    
    extern "C"
    JNIEXPORT jint JNICALL Java_com_nmm_speex_util_SpeexUtil_open
      (JNIEnv *env, jobject obj, jint compression, jint mode) {
        int tmp;
    
        if (codec_open++ != 0)
            return (jint)0;
        //初始化SpeexBits
        speex_bits_init(&ebits);
        speex_bits_init(&dbits);
        // 按照模型初始化状态值
        if(mode == 0) {
            enc_state = speex_encoder_init(&speex_nb_mode);
            dec_state = speex_decoder_init(&speex_nb_mode);
        } else if(mode == 2) {
            enc_state = speex_encoder_init(&speex_uwb_mode);
            dec_state = speex_decoder_init(&speex_uwb_mode);
        } else {
            enc_state = speex_encoder_init(&speex_wb_mode);
            dec_state = speex_decoder_init(&speex_wb_mode);
        }
        tmp = compression;
        speex_encoder_ctl(enc_state, SPEEX_SET_QUALITY, &tmp);
        //get和set方法,这时从上下文获取帧长度并赋值。
        speex_encoder_ctl(enc_state, SPEEX_GET_FRAME_SIZE, &enc_frame_size);
        speex_decoder_ctl(dec_state, SPEEX_GET_FRAME_SIZE, &dec_frame_size);
    
        return (jint)0;
    }
    
    extern "C"
    JNIEXPORT jint JNICALL Java_com_nmm_speex_util_SpeexUtil_encode
        (JNIEnv *env, jobject obj, jshortArray lin, jint offset, jbyteArray encoded, jint size) {
    
            jshort buffer[enc_frame_size];
            jbyte output_buffer[enc_frame_size];
        int nsamples = (size-1)/enc_frame_size + 1;
        int i, tot_bytes = 0;
    
        if (!codec_open)
            return 0;
        //设置完参数后,更新bits
        speex_bits_reset(&ebits);
    
        for (i = 0; i < nsamples; i++) {
            env->GetShortArrayRegion(lin, offset + i*enc_frame_size, enc_frame_size, buffer);
            speex_encode_int(enc_state, buffer, &ebits);
        }
        tot_bytes = speex_bits_write(&ebits, (char *)output_buffer,
                         enc_frame_size);
        env->SetByteArrayRegion(encoded, 0, tot_bytes,
                    output_buffer);
    
    
        return (jint)tot_bytes;
    }
    
    extern "C"
    JNIEXPORT jint JNICALL Java_com_nmm_speex_util_SpeexUtil_decode
        (JNIEnv *env, jobject obj, jbyteArray encoded, jshortArray lin, jint size) {
    
            jbyte buffer[dec_frame_size];
            jshort output_buffer[dec_frame_size];
            jsize encoded_length = size;
    
        if (!codec_open)
            return 0;
    
        // jni语法,数据拷贝,从java数据拷贝到C里面。
        env->GetByteArrayRegion(encoded, 0, encoded_length , buffer);
        speex_bits_read_from(&dbits, (char *)buffer, encoded_length );
        // 将编译结果返回
        int res = speex_decode_int(dec_state, &dbits, output_buffer);
        // jni工具类,将数据导出到java对象中
        env->SetShortArrayRegion(lin, 0, dec_frame_size,
                     output_buffer);
    
        return (jint)res;
    }
    
    extern "C"
    JNIEXPORT jint JNICALL Java_com_nmm_speex_util_SpeexUtil_getFrameSize
        (JNIEnv *env, jobject obj) {
    
        if (!codec_open)
            return 0;
        return (jint)enc_frame_size;
    
    }
    
    extern "C"
    JNIEXPORT void JNICALL Java_com_nmm_speex_util_SpeexUtil_close
        (JNIEnv *env, jobject obj) {
    
        if (--codec_open != 0)
            return;
    
        speex_bits_destroy(&ebits);
        speex_bits_destroy(&dbits);
        speex_decoder_destroy(dec_state);
        speex_encoder_destroy(enc_state);
    }
    1. 在Application.mk上右键执行。
      External Tools/BuildSo,会在libs目录下生成相关so文件。

    说明:

    • 没有右键ExternalTools的,在idea,File/Settings/tools/External Tool中添加
      BuildSo
      program内容:D:\environment\android\ndk\22.1.7171670\ndk-build.cmd就是我们安装的ndk目录

    • 编译时可能会报警告,这时因我们用的是int16,speex源码里面是浮点数,可以忽略,也可以转成int16

    • 注意,speex_jni中,包路径,类名和函数名的声明,如果想要自己封装,请自行修改。

    构建speex的aar依赖。

    1. 新建library module。
      新建module
    2. 在module下新建libs目录,并将刚刚生成so文件,连带文件夹都考本进来。
    3. 修改library的gradle文件。增加以下内容:
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
    1. SpeexUtil类声明
    package com.nmm.speex.util;
    
    import java.io.Closeable;
    
    /**
     * <p>Title: SpeexUtil</p>
     * <p>description: speex压缩、解压工具类</p>
     *
     * @author niemingming 2021/6/4
     */
    
    public class SpeexUtil implements Closeable {
        // 压缩质量。默认为8
        private static final int DEFAULT_COMPRESSION = 8;
    
        private SpeexMode speexMode;
    
        static {
            try {
                // 加载实现so包。
                System.loadLibrary("speex");
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public SpeexUtil(){
            // 默认宽带,采样率16000,压缩质量8.
            this(DEFAULT_COMPRESSION, SpeexMode.SPEEX_WB_MODE);
        }
    
        public SpeexUtil(int compression, SpeexMode speexMode) {
            int res = open(compression, speexMode.mode());
            this.speexMode = speexMode;
            if (res != 0) {
                throw new RuntimeException("初始化失败!");
            }
        }
    
        /**
         * 将音频文件压缩成video,这里应该有个速查表,用于计算质量、采样率与帧、分子大小的。
         * 这里我们按照宽带,8来计算。其值为640:70
         * @param video 带压缩音频
         * @return 压缩后音频
         */
        public byte[] cpm2spx(byte[] video) {
            int length = video.length;
            int size = ((length - 1)/640+1)*70;
            byte[] encoded = new byte[size];
            encode(Bytes.toShortArray(video), 0, encoded, video.length);
            return encoded;
        }
    
        /**
         * 这里按照质量8,宽带处理。
         * @param encoded
         * @return
         */
        public byte[] spx2pcm(byte[] encoded){
            int length = encoded.length;
            // 没帧大小320
            int size = ((length - 1)/70 + 1)*320;
            short[] source = new short[size];
            int res = decode(encoded, source, length);
            System.out.println("解压结果:" + res);
            return Bytes.toByteArray(source);
        }
    
        /**
         * 初始化speex
         * @param compression 压缩质量
         * @return 初始化结果
         */
        public native int open(int compression, int mode);
    
        /**
         * 压缩方法,最好根据自己设置来整帧传入
         * @param source 待压缩文件
         * @param offset 偏移量,从什么位置开始压缩
         * @param encoded 压缩后数据
         * @param size 原数据大小
         * @return 返回压缩后的长度
         */
        public native int encode(short[] source, int offset, byte[] encoded, int size);
    
        /**
         * 音频文件解压
         * @param encoded 压缩数据
         * @param source 解压后的数据
         * @param size 压缩数据长度
         * @return 是否成功,0,成功,-1结束,-2失败
         */
        public native int decode(byte[] encoded, short[] source, int size);
    
        /**
         * 获取帧长度
         * @return 帧长度
         */
        public native int getFrameSize();
        /**
         * 关闭方法,实际就是释放资源
         */
        @Override
        public native void close();
    }
    
    1. SpeexMode
    package com.nmm.speex.util;
    
    public enum SpeexMode {
        // 窄带音频,对应采样率为8000,framesize为160
        SPEEX_NB_MODE(0),
        // 宽带音频, 对应采样率为16000,framesize 320
        SPEEX_WB_MODE(1),
        // 超宽带音频,对应采样率为32000 ,framesize 640
        SPEEX_UWB_MODE(2);
    
        private int mode;
        private SpeexMode(int mode) {
            this.mode = mode;
        }
    
        public int mode() {
            return this.mode;
        }
    }
    
    1. Bytes
    package com.nmm.speex.util;
    
    /**
     * <p>Title: Bytes</p>
     * <p>description: Speex专业换工具类</p>
     *
     * @author niemingming 2021/5/25
     */
    
    public class Bytes {
        public Bytes() {
        }
    
        public static byte[] shortToByte(short var0) {
            int var1 = var0;
            byte[] var2 = new byte[2];
    
            for(int var3 = 0; var3 < var2.length; ++var3) {
                var2[var3] = (new Integer(var1 & 255)).byteValue();
                var1 >>= 8;
            }
    
            return var2;
        }
    
        public static short byteToShort(byte[] var0) {
            boolean var1 = false;
            short var2 = (short)(var0[0] & 255);
            short var3 = (short)(var0[1] & 255);
            var3 = (short)(var3 << 8);
            short var4 = (short)(var2 | var3);
            return var4;
        }
    
        public static byte[] toByteArray(short[] var0) {
            int var1 = var0.length * 2;
            byte[] var2 = new byte[var1];
    
            for(int var3 = 0; var3 < var1; var3 += 2) {
                var2[var3] = (byte)(var0[var3 / 2] & 255);
                var2[var3 + 1] = (byte)(var0[var3 / 2] >> 8 & 255);
            }
    
            return var2;
        }
    
        protected static short[] toShortArray(byte[] var0) {
            int var1 = var0.length / 2;
            short[] var2 = new short[var1];
    
            for(int var3 = 0; var3 < var1; ++var3) {
                var2[var3] = (short)(var0[var3 * 2 + 1] << 8 & '\uffff' | var0[var3 * 2] & 255);
            }
    
            return var2;
        }
    
        public static byte[] shortArray2ByteArray(short[] var0) {
            byte[] var1 = new byte[var0.length * 2];
            int var2 = 0;
    
            for(int var3 = 0; var3 < var0.length; ++var3) {
                short var4 = var0[var3];
                byte[] var5 = shortToByte(var4);
                var1[var2] = var5[0];
                var1[var2 + 1] = var5[1];
                var2 += 2;
            }
    
            return var1;
        }
    
        public static byte[] concat(byte[]... var0) {
            int var1 = 0;
            byte[][] var2 = var0;
            int var3 = var0.length;
    
            for(int var4 = 0; var4 < var3; ++var4) {
                byte[] var5 = var2[var4];
                var1 += var5.length;
            }
    
            byte[] var8 = new byte[var1];
            var3 = 0;
            byte[][] var9 = var0;
            int var10 = var0.length;
    
            for(int var6 = 0; var6 < var10; ++var6) {
                byte[] var7 = var9[var6];
                System.arraycopy(var7, 0, var8, var3, var7.length);
                var3 += var7.length;
            }
    
            return var8;
        }
    }
    1. 编译打包,会在build/outputs/aar目录下生成相关aar文件。打包命令### app中引入aar
    2. 在app中新建libs目录,并将生成的aar文件拷贝进去。
    3. 修改gradle配置文件
    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 30
        buildToolsVersion "30.0.3"
        defaultConfig {
            applicationId "com.hisense.tts.speexutil"
            minSdkVersion 24
            targetSdkVersion 30
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        repositories {
            flatDir{
                dirs 'libs'
            }
        }
    }
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'androidx.appcompat:appcompat:1.0.2'
        implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test:runner:1.1.1'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
        api(name: "speexsdk-release", ext: "aar")
    }
    
    1. 仅放置一个播放工具类:
    package com.nmm.tts.speexutil.sdk.audio;
    
    import android.media.AudioFormat;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.util.Log;
    import com.nmm.speex.util.SpeexUtil;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.LinkedList;
    import java.util.Queue;
    
    /**
     * <p>Title: AudioPlayer</p>
     * <p>description: 基于AudioTrack实现播放功能。</p>
     *
     * @author niemingming 2021/5/17
     */
    public class AudioPlayer {
    
        private AudioTrack mAudioTrack;
        private InputStream dataInputStream;
        private boolean isStoped = false;
        private static AudioPlayer audioPlayer;
    
        /**
         * 音频流类型为音乐流
         */
        private static final int mStreamType = AudioManager.STREAM_MUSIC;
        /**
         * 采样率,我们合成语音采样率为32000
         */
        private static final int mSampleRateInHz = 16000;
        /**
         * 声道配置
         * CHANNEL_CONFIGURATION_MONO过时了,
         * 采用CHANNEL_OUT_MONO,标识输出单声道。
         */
        private static final int mChannelConfig = AudioFormat.CHANNEL_OUT_MONO;
        /**
         * 编码格式16位编码,音频量化位
         */
        private static final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
        /**
         * 缓冲区大小,就是数据块大小,一般采用声道数*位数*频率/8
         * 不同播放器可能有出入,这里我们使用AudioTrack提供方法获取
         */
        private int mMinBufferSize;
        /**
         * speex4j在8,16k下,每帧压缩后的大小为71.会额外增加一个头字节用于表示数据长度
         */
        private final int spxFrameSize = 71;
        /**
         * 模式采用流式处理。
         */
        private static int mode = AudioTrack.MODE_STREAM;
    
        private AudioPlayer() {
            init();
        }
    
        /**
         * 初始化
         */
        private void init() {
            //根据采样率、声道及量化位计算缓存数据区大小,起始就是完整的一秒钟单数据块大小
            mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz, mChannelConfig, mAudioFormat);
            //创建AudioTrack
            mAudioTrack = new AudioTrack(mStreamType, mSampleRateInHz, mChannelConfig, mAudioFormat, mMinBufferSize, mode);
        }
    
        /**
         * 单例模式获取对象
         * @return
         *  返回博阿芳对象
         */
        public static AudioPlayer getInstance() {
            if (audioPlayer == null) {
                synchronized (AudioPlayer.class) {
                    if (audioPlayer == null) {
                        audioPlayer = new AudioPlayer();
                    }
                }
            }
            return audioPlayer;
        }
    
        /**
         * 播放语音
         */
        public void playSpeech(InputStream inputStream) throws IOException {
            stopPlay();
            //启动播放
            isStoped = false;
            dataInputStream = inputStream;
            byte[] tmpbuffer = new byte[mMinBufferSize];
            Queue<Byte> spxQueue = new LinkedList<>();
            Queue<Byte> videoQueue = new LinkedList<>();
            int total = 0;
            int readCount = 0;
            try (SpeexUtil speexUtil = new SpeexUtil()){
                while ((readCount = dataInputStream.read(tmpbuffer)) != -1) {
                    if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                        continue;
                    }
                    //解决强电流问题
                    //获取到读取内容,写出播放
                    if (readCount > 0) {
                        //speex解码,并对齐因素播放
                        for (int i = 0; i < readCount; i ++ ) {
                            spxQueue.add(tmpbuffer[i]);
                        }
                        //解压前读取数据,我们采用8,16k,压缩后长度为71,我们可以从这里入手考虑如何因素对齐
                        while (spxQueue.size() >= spxFrameSize) {
                            // 处理speex4j自加的一个字节数据
                            byte[] encoded = new byte[spxFrameSize - 1];
                            for (int i = 0; i < spxFrameSize; i++) {
                                if ( i == 0) {
                                    byte head = spxQueue.poll();
                                    // 去掉speex4j增加的头字节。
                                    continue;
                                }
                                encoded[i - 1] = spxQueue.poll();
                            }
                            byte[] decoded = speexUtil.spx2pcm(encoded);
                            int len = decoded.length;
    //                        Log.i("tts", "readCount:" + readCount + "; decoded:" + len + "; encoded:" + size);
                            total += len;
                            for (int i = 0; i < len; i++) {
                                videoQueue.add(decoded[i]);
                            }
                        }
                        //停止播放会释放资源
                        if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) {
                            init();
                        }
                        while (videoQueue.size() > mMinBufferSize) {
                            byte[] vidio = new byte[mMinBufferSize];
                            for (int i = 0; i < mMinBufferSize; i++) {
                                vidio[i] = videoQueue.poll();
                            }
                            mAudioTrack.play();
                            mAudioTrack.write(vidio, 0 , mMinBufferSize);
                        }
                    }
                    if (isStoped) {
                        break;
                    }
                }
                if (spxQueue.size() > 0 && !isStoped) {
                    int len = spxQueue.size();
                    byte[] encoded = new byte[len];
                    for (int i = 0; i < len; i++) {
                        encoded[i] = spxQueue.poll();
                    }
                    byte[] decoded = speexUtil.spx2pcm(encoded);
                    len = decoded.length;
                    total += encoded.length;
                    for (int j = 0; j < len; j++) {
                        videoQueue.add(decoded[j]);
                    }
                }
                Log.i("tts", "total decoded size: " + total);
                while (videoQueue.size() > 0 && !isStoped) {
                    byte[] vidio = new byte[mMinBufferSize];
                    int len = Math.min(videoQueue.size(), vidio.length);
                    for (int i = 0; i < len; i++) {
                        vidio[i] = videoQueue.poll();
                    }
                    mAudioTrack.play();
                    mAudioTrack.write(vidio, 0 , len);
                }
                stopPlay();
            } catch (IOException e) {
                Log.e("tts", "播放异常!", e);
                throw new IOException(e);
            }
        }
    
    
        private String encodeHex(byte[] bytes) {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < bytes.length; i++ ) {
                int j = bytes[i];
                if (j < 0) {
                    j += 256;
                }
                if (j < 16) {
                    stringBuilder.append("0");
                }
                stringBuilder.append(Integer.toString(j, 16)).append(" ");
    
            }
            return stringBuilder.toString();
        }
        /**
         * 停止当前播放
         */
        private void stopPlay() {
            isStoped = true;
            if (mAudioTrack != null) {
                if (mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
                    //初始化完成了,如果停止播放,会释放。
                    mAudioTrack.stop();//停止播放
                }
                if (mAudioTrack != null) {
                    //释放资源
                    mAudioTrack.release();
                }
            }
            if (dataInputStream != null) {
                try {
                    dataInputStream.close();
                } catch (IOException e) {
                    Log.e("tts" +
                            "", "关闭语音流出现异常!", e);
                }
            }
        }
    
        public void stop(){
            isStoped = true;
        }
    }
    

    几点说明:

    • AiduoTrack电流声音,个人猜测是我们每次传入的数据不满足buffsize导致的,我这里按照buffsize传入后,会有明显改善
    • aar文件更新问题,请在每次替换aar文件时,做好app的clean/build操作,否则不能生效。
    展开全文
  • 9款好看又实用的手机APP UI模板

    万次阅读 2017-09-12 10:25:02
    这也就解释了为什么手机应用市场有很多APP功能差不多,但我们只使用一款就够了,而这一款必定有更优的界面设计及用户体验。 App界面设计出色与否将直接影响App用户的体验。要提高UI设计水平,除了经历一定的实践

    一款优秀的界面设计,最重要的两点应该是产品本身的UI和用户体验(UX),唯有将二者完美结合才能称作优秀的设计作品。但随着智能手机及各类设计工具的发展,用户对手机界面设计的要求也与日俱增。这也就解释了为什么手机应用市场有很多APP功能差不多,但我们只使用一款就够了,而这一款必定有更优的界面设计及用户体验。

    App界面设计出色与否将直接影响App用户的体验。要提高UI设计水平,除了经历一定的实践及积累,还可以动手临摹或欣赏一些手机UI设计作品,提高审美能力、增加设计灵感。今天小编给大家挑选了9款由国内外设计大咖制作的高质量手机APP UI模板,涵盖摄影、新闻、游戏、美食、房地产等板块,附有具体的手机支持版本及下载地址哦。看看有没有你中意的APP模板吧,Enjoy!

    (一)Android手机模板

    1. 游戏类-Fortin Quiz Pro

    推荐:★★★


    这是一款安卓手机上在线测试的游戏APP模板,其中的问题是以“图片+文字”的组合形式出现,并且按照不同的难易度进行了分类。此外,这款模板包含非传统的语音测试方式,且支持国外的Google Leaderboard排行榜,可以与好友实时分享测试分数,看看谁更机智呢!

    价格:$23

    软件版本:Android 4.1+

    下载地址

    2. 新闻类-Android News App

    推荐:★★★★


    这款模板主要由安卓客户端及带有MYSQL服务器的PHP组成,可以帮助你快速搭建起自己的新闻平台。一大亮点功能就是它能根据用户的具体要求来启用、禁用功能。如果你希望找到一款支持多国语言的模板,那么这款模板就会派上用场了。

    价格:$18

    软件版本:Android 4.0+

    下载地址

    3. 生活类-City Guide

    推荐:★★★★


    City Guide是一款提供实时定位及导航的APP模板,有8种不同的配色主题,同时支持动画效果、响应式设计等。它最大的优点在于使用本地的SQL数据库来存储数据,从而最大限度地让我们摆脱对互联网的依赖。

    价格:$20

    软件版本:Android 4.0+

    下载地址

    4. 美食类-Recipes

    推荐:★★★★


    一看便知这是一款专为美食达人打造的美食菜谱类App。小伙伴可以通过在它的Android Studio中创建自己喜欢的食谱、分类、推送通知、In-App Purchase、购物清单等。此外,国民原型工具Mockplus也推出过这款Recipes模板的原型例子,其中添加了图片及滚动的微交互,达到了极高还原性,非常适合刚上手的设计菜鸟参考借鉴。

    价格:$25

    软件版本:Android 4.0+

    下载地址

    (二)iOS手机模板

    5. 摄影类-PhotoGram

    推荐:★★★★


    PhotoGram是一款可高度自定义的摄影模板,提供多种有趣、有用的功能:滤镜,尺寸裁剪,光效调整,文字/视频添加等。照片美化后,可以通过自带的分享按钮,分享给身边的小伙伴,一秒见证黑科技!

    价格:$19

    软件版本:iOS 3.1+

    下载地址

    6. 项目管理类-Task Reminder

    推荐:★★★★


    如果你也认同只有合理的计划才能高效办事,那么这款模板会是你的菜!这是当下iOS客户端中最流行的项目管理类App之一,可无限制创建任务及提醒清单,友好的开发环境及设计也受众多程序员的青睐。

    价格:$49

    软件版本:iOS 7.0+

    下载地址

    7. 购物类-Store Finder

    推荐:★★★★


    不可否认,网购已经成为很多人日常生活中密不可分的一种消费方式。但不可避免有时候需要在实体商店中购买的情况,例如需要试穿的衣服或鞋子等。这种情况下,像Store Finder这类App就派上用场了。支持手势及缩放,也可离线操作。

    价格:$21

    软件版本:iOS 8.0+

    下载地址

    8. 房地产类-Real Estate Finder

    推荐:★★★★


    顾名思义,这是一款能帮助用户获取房地产类资讯的App。可以通过卫星定位及Apple地图,进行实时搜索及导航。同时集成多种通讯方式:电话、短信和电子邮件,一步就到位。

    价格:$23

    软件版本:iOS 7.0+

    下载地址

    (三)跨平台通用模板

    9. ionWordPress

    推荐:★★★★


    这是一款适用于多个平台(包括Android/iOS/WordPress)的通用App模板,拥有精致的界面及可利用后期开发的语言环境支持,支持HTML5/CSS/JavaScript等。常用的主要页面:登陆/注册页面、菜单页面、详情页面、工作台页面等都能找到。

    价格:$19

    软件版本:Android 4.+, iOS 6.1+, WordPress 4.0+

    下载地址

    以上便是小编推荐大家可以下载使用的精美App UI模板。如果你也想亲自制作一套“低调奢华有内涵”的App UI模板,那可以试试Mockplus这款工具。除了海量的封装组件(200个)及图标(3000个)资源,摩客现推出了直接导入设计模板的功能,用户可以根据不同的设计需求来选择、修改示例项目,提高效率、激发灵感。如果感兴趣的小伙伴,可以看看这里更多的App及网页原型项目例子


    更多模板APP模板及UI资源网站:

    1. Themeforest - 超全的App模板及网站主题平台

    网址:https://themeforest.net/tags/mobile%20app

    2. W3Layouts – 付费、免费的APP及图标资源平台

    网址:https://w3layouts.com/mobile-application-templates/

    3. Graphicriver – 搜罗App及网页相关所有字体、图标等素材资源

    网址:https://graphicriver.net/tags/mobile%20app

    4. Pinterest – 在线UI图片合辑社区

    网址:https://www.pinterest.com/explore/mobile-website-template/?lp=true

    5. Template Monster – 多平台兼容的应用、网站UI素材库

    网址:https://www.templatemonster.com/

    6. PNGTree -提供高质量及多种格式图片资源平台,可免费下载

    网址:https://pngtree.com/

    如果有任何建议或意见,欢迎随时交流。

    展开全文
  • 如何做好App

    千次阅读 2016-02-25 12:51:53
    yujie_pei做更好的APP [原]IOS UIApplication OpenURL 详解 由于我们家app需要大量跳转到外部,所以想写一个工具类来解决这个问题: 首先,我们需要的是 1.打电话,弹出一个提示框,是否要拨打电话。 2....

    yujie_pei做更好的APP
    [原]IOS UIApplication OpenURL 详解


    由于我们家app需要大量跳转到外部,所以想写一个工具类来解决这个问题:
    首先,我们需要的是
    1.打电话,弹出一个提示框,是否要拨打电话。
    2.打开外部浏览器。
    3.打开到系统设置的更新页面
    4.打开到系统设置的wifi选择页面
    5.打开到系统设置的声音页面 


    代码写好了 原地址:https://github.com/sunqichao/sqcjump ,接口部分
    typedef enum : NSInteger{
        //打开设置中的图片设置
        SQCJumpTophotos,
        //打开设置中的语音助手
        SQCJumpToSiri,
        //打开设置中的软件更新
        SQCJumpToSoftWareUpdate,
        //打开设置中的wifi设置页
        SQCJumpToWiFi,
        //打开设置中的声音设置页
        SQCJumpToSounds
    }SQCJumpType;
    @interface SQCJumpOut : NSObject

    + (BOOL)jumpToOSDetailSet:(SQCJumpType)type;
    /**
     *  打电话 两种方式,一种是弹出一个alert提示是否要打电话,一种是直接打电话
     *
     *  @param phone    电话号码
     *  @param isAppear 是否弹出alert
     *
     *  @return
     */
    + (BOOL)callWithNumber:(NSString *)phone appearAlert:(BOOL)isAppear;

    /**
     *  打开外部浏览器
     *
     *  @param URL 地址
     *
     *  @return  
     */
    + (BOOL)openWebSite:(NSString *)URL;


    作者:sqc3375177 发表于2015/12/17 15:40:35  原文链接
    阅读:112 评论:0  查看评论
     
    [原]解决phpmyadmin中缺少mysqli扩展问题的方法

    phpMyAdmin错误 缺少 mysqli 扩展。请检查 PHP 配置 的解决方案

    phpMyAdmin 缺少 mysqli 扩展。请检查 PHP 配置 的解决方案:缺少 mysqli 扩展。请检查 PHP 配置。

    打开你的php.ini->一般在C:WINDOWS目录下。找到

    代码如下:

    ;extension=php_msql.dll;extension=php_mssql.dllextension=php_mysql.dllextension=php_mysqli.dll

    需要开启哪个扩展,就把这一行前面的分号去掉就行,注意要重启Apache 
    或者IIS
    解决步骤:1.看看php的ext目录中是否有php_mysqli.dll文件如果有,继续下面的步骤。没有的话重新装个php2.打开php.ini,找到

    代码如下:

    ;extension=php_mysqli.dll

    把前面的分号";"去掉,改为

    代码如下:

    extension=php_mysqli.dll

    3.找到

    代码如下:

    ;extension_dir="./ext"

    把前面的分号";"去掉,改为(相对于php的安装路径的ext目录)

    代码如下:

    extension_dir = "e:\php\ext"

    e:php为php的存放目录

    4.把php.ini拷贝到c:windows下5.把libmysql.dll复制到Windows下面的System32目录(这个很关键)6.重新启动IIS(不是网站)

    cmd下运行 iisreset /RESTART

    如果上面方法还不行,可参考下面方法操作

    第一步,输出 phpinfo() 看看 MySQL 模块是否成功装载,如果成功装载说明你见鬼了,否则看第二步,

    IIS下phpinfo()无mysql模块是何原因?原先用的php是.msi安装的,后来全部重来了一次,换成压缩包形式安装的。又照着配置了php.ini,复制到windows,复制libmysql.dll到system32。之后测试成功。。。第二步,查看 
    extension_dir 的设置是否正确第三步,查看是否把 libmysql.dll 文件复制到 c:windowssystem32 下

    1、没有正确安装Mysql数据库,在系统服务中Mysql相关的服务没有启动   (请查看正确安装Mysql的方法)

    2、在系统的 system32(C:windowssystem32) 目录下缺少 
    libmysql.dll文件,解决方法是找到php目录下的libmysql.dll,并将libmysql.dll复制到C:windowssystem32目录中,然后重新启动Web服务。

    3、在C:windows目录下的php.ini文件中,没有将“;extension=php_mysql.dll”中的前面一个“;”去掉,所以不能使用相应功能,解决方法是打开php.ini文件

    4、Mysql目录没有读取权限,正确的目录权限如下:administrator   完全控制system    
    完全控制user    读取加运行其他的用户权限全部删除(也可保留,但安全性不高,建议删除),然后重启MYsql服务和Web服务。

    当你打开phpmyadmin时可能会报这样的一个错误,

    缺少 mysqli 扩展。请检查 PHP 配置。 <a href="Documentation.html#faqmysql" 
    target="documentation"><img class="icon" 
    src="./themes/original/img/b_help.png" width="11" height="11" alt="文档" 
    title="文档" /></a>

    你可以按照以下这几种方法进行检查:

    1.检查php.ini ;extension=php_mysqli.dll是不是已经启用 也就是去掉前面的;

    2.检查php.ini extension_dir 地址是不是指向了php目录的ext目录下这步是关键 
    我就是处在这一步的问题上 我把 extension_dir = "e:\www\php54\ext" 设置成了自己的目录 就可以了

    3.检查lib_mysql.dll有没有拷贝到windows目录下  这个dll文件有多种形式的 
    有没加下划线的libmysql_d.dll 因此要注意自己看

    4.检查php安装目录ext目录下,php_mysqli.dll文件是不是存在 网上也说明 最好使用zip包解压 
    不会出现文件丢失

    5.查看php详细页面<?php phpinfo(); 
    ?>,看看mysql和mysqli是不是已经启动这一步就是检查你有没有开启mysqli的没有开启的话 
    你查找mysqli关键字是无法再php详细页面看到的

    最后要注意!!!!重启Apache

    作者:sqc3375177 发表于2015/11/24 16:31:25  原文链接
    阅读:75 评论:0  查看评论
     
    [原]Linux新手入门:Unable to locate package错误解决办法

    原文地址:http://www.cppblog.com/colorful/archive/2012/04/29/173122.html


    最近刚开始接触Linux,在虚拟机中装了个Ubuntu,当前的版本是Ubuntu 11.10,装好后自然少不了安装一些软件,在设置了软件的源后,就开始了 sudo apt-get install,结果出现了下面的Unable to locate package错误:

    1. E: Unable to locate package mysql-server
    2. E: Unable to locate package mysql-client

      这叫一个郁闷啊,出师不利,不带这么吓唬刚玩Ubuntu的小朋友吧~于是赶紧找资料,又回顾下前面的操作,最后发现问题出在执行sudo apt-get install之前更换了软件源,但是却忘了update下了,于是执行下面的命令:

    1. sudo apt-get update

    等上面命令执行完后,再执行sudo apt-get install就可以了!其实错误信息已经很明确了,Unable to locate packet就是无法找到包嘛,那还不赶紧sudo apt-get update下!


    附另一篇文章:

    原文地址:http://blog.chinaunix.net/uid-22002627-id-3475650.html

    碰到这个问题后找到这个帖子就转了过来 当用apt-get更新软件包时常出现错误提示Unable to locate package update, 尤其是在ubuntu server上,解决方法是: 先更新apt-get #sudo apt-get update 执行完后,问题就解决了。 继续更新: #sudo apt-get upgrade 然后就可以安装apache: #sudo apt-get install apache2 等就可以了 安装mysql命令:sudo apt-get install mysql-server mysql-client

    作者:sqc3375177 发表于2015/11/24 15:36:57  原文链接
    阅读:130 评论:0  查看评论
     
    [原]Password and U option must have a non-empty value

    今天上传新版本遇到这个问题,

    • The u option must have a non-empty value"
    • "The password option must have a non-empty value" 


    正确的解决方案是升级XCode 到7.1,然后再上传就可以了


    作者:sqc3375177 发表于2015/10/23 14:39:45  原文链接
    阅读:2237 评论:2  查看评论
     
    [原]iOS 9 适配中出现 问题

    本文主要是说一些iOS9适配中出现的坑,如果只是要单纯的了解iOS9新特性可以看瞄神的开发者所需要知道的 iOS 9 SDK 新特性。9月17日凌晨,苹果给用户推送了iOS9正式版,随着有用户陆续升级iOS9,也就逐渐的衍生出了一系列的问题,笔者也在赶忙为自己维护的App做适配,本文写的一些坑基本都是亲身体验了。

    一、NSAppTransportSecurity

    iOS9让所有的HTTP默认使用了HTTPS,原来的HTTP协议传输都改成TLS1.2协议进行传输。直接造成的情况就是App发请求的时候弹出网络无法连接。解决办法就是在项目的info.plist 文件里加上如下节点:

    blob.png

    NSAppTransportSecurity - NSAllowsArbitraryLoads

    这个子节点的意思是:是否允许任性的加载?! 设为YES的话就将禁用了AppTransportSecurity转而使用用户自定义的设置,这个问题就解决了。

    上面说是苹果限制了HTTP协议,但是也并不是说所有的HTTPS都能完美适配iOS9了。

    举个栗子,从app内起webView加载https的网页。新建个项目写几行起网页的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    - (void)loadView{
        UIWebView *web = [[UIWebView alloc]initWithFrame:[UIScreen mainScreen].bounds];
        self.view = web;
    }
    - (void)viewDidLoad {
        [super viewDidLoad];
          
        UIWebView *web = (UIWebView *)self.view; //董铂然
        NSURL *url = [NSURL URLWithString:@"https://github.com/"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        [web loadRequest:request];
    }

    中间的url就是我们想要加载的https地址,用https://baidu.com/ 和 https://github.com/ 分别试一下,结果不同

    blob.png

    blob.png

    github的网页能打开,百度的网页打不开,下面打印了一行log

    1
    NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

    原因是苹果的官方资料说首先必须要基于TLS 1.2版本协议。然后证书的加密的算法还需要达到SHA256或者更高位的RSA密钥或ECC密钥,如果不符合,请求将被中断并返回nil.

    在浏览器中是可以直接查看这个网站的加密算法的,先点绿锁再点证书信息。

    blob.pngblob.pngblob.png
    从右边两张图可以看出,github带RSA加密的SHA-256符合苹果的要求,所以才可以展示。

    针对百度的情况可以在info.plist中配置如下,如果网站引用的比较多应该是需要针对每个网站进行配置。

    blob.png

    NSAppTransportSecurity,NSExceptionDomains,NSIncludesSubdomains,NSExceptionRequiresForwardSecrecy,NSExceptionAllowInsecureHTTPLoads 写在下面便于复制。

    其中的ForwardSecrecy理解为超前的密码保护算法,在官方资料里有写,一共是11种。配置完毕百度可以访问。

    blob.png

    blob.png

    二、Bitcode

    bitcode的理解应该是把程序编译成的一种过渡代码,然后苹果再把这个过渡代码编译成可执行的程序。bitcode也允许苹果在后期重新优化我们程序的二进制文件,有类似于App瘦身的思想。

    用了xcode7的编译器编译之前没问题的项目可能会出现下列报错。

    1
    XXXX’ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

    问题的原因是:某些第三方库还不支持bitcode。要不然是等待库的开发者升级了此项功能我们更新库,要不就是把这个bitcode禁用。

    禁用的方法就是找到如下配置,选为NO.(iOS中bitcode是默认YES,watchOS中bitcodes是不让改的必须YES。)

    blob.png

    三、设置信任

    这一条只和企业级应用或inhose 有关,和AppStore渠道的应用无关。

    在iOS8只是弹出一个窗问你是否需要让手机信任这个应用,但是在iOS9却直接禁止,如果真的想信任需要自己去手动开启。类似于Mac系统从未知开发者处下载的dmg直接打不开,然后要到系统偏好设置的安全性与隐私手动打开。 下图展示左边iOS8,右边iOS9

    blob.png

    blob.png

    用户需要去 设置---》通用---》描述文件 里面自行添加信任。

    这种问题的处理方法也就两种:1.提前周知暂时不要升级iOS9  2.大多是公司员工使用的企业级应用,群发一个指导邮件。 

    四、字体

    iOS8中,字体是Helvetica,中文的字体有点类似于“华文细黑”。只是苹果手机自带渲染,所以看上去可能比普通的华文细黑要美观。iOS9中,中文系统字体变为了专为中国设计的“苹方” 有点类似于一种word字体“幼圆”。字体有轻微的加粗效果,并且最关键的是字体间隙变大了!

    所以很多原本写死了width的label可能会出现“...”的情况。

    iOS8

    blob.png


    iOS9 蛋疼

    blob.png


    上面这两张图也可以直观的看出同一个界面,同一个label的变化。

    所以为了在界面显示上不出错,就算是固定长度的文字也还是建议使用sizetofit 或者ios向上取整 ceilf() 或者提前计算

    1
    2
    CGSize size = [title sizeWithAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:14.0f]}];
    CGSize adjustedSize = CGSizeMake(ceilf(size.width), ceilf(size.height));

    五、URL scheme

    URL scheme一般使用的场景是应用程序有分享或跳其他平台授权的功能,分享或授权后再跳回来。

    在iOS8并没有做过多限制,但是iOS9需要将你要在外部调用的URL scheme列为白名单,才可以完成跳转

    如果iOS9没做适配 会报如下错误

    1
    canOpenURL: failed for URL : "mqzone://qqapp" - error: "This app is not allowed to query for scheme mqzone"

    具体的解决方案也是要在info.plist中设置 LSApplicationQueriesSchemes 类型为数组,下面添加所有你用到的scheme

    blob.png

    六、statusbar

    这个还好只是报一个警告,如果就是不管他,也不会出现问题。

    1
    : CGContextSaveGState: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.

    以前我们为了能够实时的控制顶部statusbar的样式,可能会在喜欢使用

    1
    2
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]
    [[UIApplication sharedApplication]setStatusBarHidden:YES];

    但是这么做之前需要将 info.plist 里面加上View controller-based status bar appearance  BOOL值设为NO,就是把控制器控制状态栏的权限给禁了,用UIApplication来控制。但是这种做法在iOS9不建议使用了,建议我们使用吧那个BOOL值设为YES,然后用控制器的方法来管理状态栏比如。

    1
    2
    3
    4
    - (UIStatusBarStyle)preferredStatusBarStyle
    {
        return UIStatusBarStyleLightContent;
    }

    点进头文件可以验证刚才说法:

    1
    @property(readwrite, nonatomic,getter=isStatusBarHidden) BOOL statusBarHidden NS_DEPRECATED_IOS(2_0, 9_0, "Use -[UIViewController prefersStatusBarHidden]");

    七、didFinishLaunchingWithOptions

    如果运行的时候报下列错误,那就是你的didFinishLaunchingWithOptions写的不对了

    1
    ***** Assertion failure in -[UIApplication _runWithMainScene:transitionContext:completion:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3505.16/UIApplication.m:3294**

    iOS9不允许在didFinishLaunchingWithOptions结束了之后还没有设置window的rootViewController。 也许是xcode7的编译器本身就不支持。

    解决的方法当然就是先初始化个值,之后再赋值替换掉

    1
    2
    UIWindow *window = [[UIWindowalloc] initWithFrame:[UIScreenmainScreen].bounds];
    window.rootViewController = [[UIViewController alloc]init];

    八、tableView

    虽然现在的iOS9已经推送正式版了,但是iOS9使用时还是会感觉到App比以前更加卡顿了,tableView拖动时卡顿显示的最为明显。 并且之前遇到一个bug,原本好的项目用xcode7一编译,tableView刷新出了问题 ,[tableView reloadData]无效 有一行cell明明改变了但是刷新不出来。 感觉可能是这个方法和某种新加的特性冲突了,猜测可能是reloadData的操作被推迟到下一个RunLoop执行最终失效。

    解决的方法是,注释[tableView reloadData],改用局部刷新,问题居然就解决了。

    1
    [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];

    暂时遇到这些问题,感觉iOS9的出现让所有iOS开发都是菊花一紧,希望苹果这种大刀阔斧做改变,特立独行的风格发展下去以后别和government 产生矛盾,然后公司倒闭 导致开发人员失业,也许是我想多了。预祝所有的iOS都能及时的做好适配改完bug,下个版本一上线,所有问题都解决。

    作者:sqc3375177 发表于2015/10/16 11:27:14  原文链接
    阅读:966 评论:1  查看评论
     
    [原]XCode7 application loader 上传报错提示(ERROR ITMS-90168)

    最近提交版本的时候出现以下提示:

    Could not make parent directory for: /Users/xxx/.itmstransporter/softwaresupport/bin/Frameworks/ITunesSoftwareService.framework/Resources/ITunesSoftwareServiceConfiguration.xml

        Could not configure software support.
        An exception has occurred: /Users/xxx/.itmstransporter/softwaresupport/bin/Frameworks/ITunesSoftwareService.framework/Resources/ITunesSoftwareServiceConfiguration.xml (No such file or directory)

        ERROR ITMS-90168: "The binary you uploaded was invalid." 


    解决方法如下:


    打开终端,输入以下指令:

    [python] view plaincopy
    1. $ cd ~/.itmstransporter  
    2. $ rm update_check*  
    3. $ mv softwaresupport softwaresupport.bak  
    4. $ cd UploadTokens  
    5. $ rm *.token  

    作者:sqc3375177 发表于2015/10/16 10:40:04  原文链接
    阅读:1354 评论:0  查看评论
     
    [原]Xcode 升级到7后,打包ipa提交出现问题 Unexpected CFBundleExecutable key

    I encountered the same problem today with the same exact error message when trying to submit our app (using Xcode 7 beta 5) but instead of the instabug.bundle bit, it was for me TencentOpenApi_IOS_Bundle.bundle.

    I solved the problem by finding the named bundle in the project then - just as the error message suggests - edited the Info.plist that is in the bundle by removing the CFBundleExecutable key. The CFBundlePackageType key was already set to BNDL so I didn't touch it.

    After these changes I did Product > Clean and then had no problem submitting the app to the App store.

    I hope this helps.

    作者:sqc3375177 发表于2015/9/24 17:22:15  原文链接
    阅读:3648 评论:3  查看评论
     
    [原]Xcode7 beta 网络请求报错:The resource could not be loaded because the App Transport Security policy requir

    今天升级Xcode 7.0 bata发现网络访问失败。
    输出错误信息

    <code style="padding: 0px; color: inherit; white-space: inherit; background-color: transparent;"><span style="font-family:Comic Sans MS;font-size:18px;">The resource could not be loaded because the App Transport Security policy requires the <span class="hljs-operator"><span class="hljs-keyword" style="color: rgb(133, 153, 0);">use</span> <span class="hljs-keyword" style="color: rgb(133, 153, 0);">of</span> a secure <span class="hljs-keyword" style="color: rgb(133, 153, 0);">connection</span>.
    </span></span></code>

    Google后查证,iOS9引入了新特性App Transport Security (ATS)。详情:App Transport Security (ATS)

    新特性要求App内访问的网络必须使用HTTPS协议。
    但是现在公司的项目使用的是HTTP协议,使用私有加密方式保证数据安全。现在也不能马上改成HTTPS协议传输。

    最终找到以下解决办法:

    1. 在Info.plist中添加NSAppTransportSecurity类型Dictionary
    2. NSAppTransportSecurity下添加NSAllowsArbitraryLoads类型Boolean,值设为YES

    参考:

    作者:sqc3375177 发表于2015/9/21 14:57:34  原文链接
    阅读:5657 评论:1  查看评论
     
    [原]IOS9 xcode7 You must rebuild it with bitcode enabled

    以前的项目 放到Xcode7中运行时可能会遇到以下错误:You must rebuild it with bitcode enabled (Xcodesetting ENABLE_BITCODE)

    未来Watch应用须包含Bitcode,iOS不强制,但Xcode7默认会开启Bitcode。

    如何适配?

    方法一:更新library使包含Bitcode,否则会出现以下中的警告;

    1
    ( null ): URGENT: all bitcode will be dropped because  ‘/Users/myname/Library/Mobile Documents/com~apple~CloudDocs/foldername/appname/GoogleMobileAds.framework/GoogleMobileAds(GADSlot+AdEvents.o)‘  was built without bitcode. You must rebuild it  with  bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode  for  this  target. Note: This will be an error  in  the future.

    方法二:关闭Bitcode

    工程设置中 buildingsetting 中搜索bitcode 选择no

     

    解决问题!!!!

    作者:sqc3375177 发表于2015/9/20 18:54:29  原文链接
    阅读:1852 评论:0  查看评论
     
    [原]nil/Nil/NULL/NSNull的区别

    原文地址:http://blog.csdn.net/wzzvictory/article/details/18413519    感谢原作者


    作者:wangzz
    转载请注明出处
    如果觉得文章对你有所帮助,请通过留言或关注微信公众帐号wangzzstrive来支持我,谢谢!

    平时开发过程中经常遇到这几个表示空的关键字:nil、Nil、NULL,一向搞不清楚,作为一个有两年开发经验的程序员,连那么基础的东西都不知道未免太丢人了。
    首先要说明的是,nil、Nil、NULL三个关键字和NSNull类都是表示空,只是用处不一样,具体的区别如下:

    一、NULL

    1、声明位置

    stddef.h文件

    2、定义

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. #undef NULL  
    2. #ifdef __cplusplus  
    3. #  if !defined(__MINGW32__) && !defined(_MSC_VER)  
    4. #    define NULL __null  
    5. #  else  
    6. #    define NULL 0  
    7. #  endif  
    8. #else  
    9. #  define NULL ((void*)0)  
    10. #endif  
    其中__cplusplus表示是不是C++代码,所以对于普通的iOS开发者来说,通常NULL的定义就是:
    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. #  define NULL ((void*)0)  

    因此,NULL本质上是:(void*)0

    3、用处及含义

    NULL表示C指针为空

    4、示例

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. charchar *string = NULL;  

    二、nil

    1、声明位置

    objc.h文件

    2、定义

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. #ifndef nil  
    2. # if __has_feature(cxx_nullptr)  
    3. #   define nil nullptr  
    4. # else  
    5. #   define nil __DARWIN_NULL  
    6. # endif  
    7. #endif  
    其中__has_feature(cxx_nullptr)用于判断C++中是否有nullptr特性,对于普通iOS开发者来说,nil的定义形式为:
    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. #   define nil __DARWIN_NULL  
    就是说nil最终是__DARWIN_NULL的宏定义,__DARWIN_NULL是定义在_types.h中的宏,其定义形式如下:
    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. #ifdef __cplusplus  
    2. #ifdef __GNUG__  
    3. #define __DARWIN_NULL __null  
    4. #else /* ! __GNUG__ */  
    5. #ifdef __LP64__  
    6. #define __DARWIN_NULL (0L)  
    7. #else /* !__LP64__ */  
    8. #define __DARWIN_NULL 0  
    9. #endif /* __LP64__ */  
    10. #endif /* __GNUG__ */  
    11. #else /* ! __cplusplus */  
    12. #define __DARWIN_NULL ((void *)0)  
    13. #endif /* __cplusplus */  
    非C++代码的__DARWIN_NULL最终定义形式如下:
    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. #define __DARWIN_NULL ((void *)0)  
    也就是说,nil本质上是:(void *)0

    3、用处及含义

    用于表示指向Objective-C中对象的指针为空

    4、示例

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. NSString *string = nil;  
    2. id anyObject = nil;  

    三、Nil

    1、声明位置

    objc.h文件

    2、定义

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. #ifndef Nil  
    2. # if __has_feature(cxx_nullptr)  
    3. #   define Nil nullptr  
    4. # else  
    5. #   define Nil __DARWIN_NULL  
    6. # endif  
    7. #endif  
    和上面讲到的nil一样,Nil本质上也是:(void *)0

    3、用处及含义

    用于表示Objective-C类(Class)类型的变量值为空

    4、示例

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. Class anyClass = Nil;  

    四、NSNull

    1、声明位置

    NSNull.h文件

    2、定义

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. @interface NSNull : NSObject <NSCopying, NSSecureCoding>  
    2.   
    3. + (NSNull *)null;  
    4.   
    5. @end  

    3、用处及含义

    从定义中可以看出,NSNull是一个Objective-C类,只不过这个类相当特殊,因为它表示的是空值,即什么都不存。它也只有一个单例方法+[NSUll null]。该类通常用于在集合对象中保存一个空的占位对象。

    4、示例

    我们通常初始化NSArray对象的形式如下:
    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. NSArray *arr = [NSArray arrayWithObjects:@"wang",@"zz",nil];  
    当NSArray里遇到nil时,就说明这个数组对象的元素截止了,即NSArray只关注nil之前的对象,nil之后的对象会被抛弃。比如下面的写法:
    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. NSArray *arr = [NSArray arrayWithObjects:@"wang",@"zz",nil,@"foogry"];  
    这是NSArray中只会保存wang和zz两个字符串,foogry字符串会被抛弃。
    这种情况,就可以使用NSNull实现:
    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
    1. NSArray *arr = [NSArray arrayWithObjects:@"wang",@"zz",[NSNull null],@"foogry"];  


    五、总结

    从前面的介绍可以看出,不管是NULL、nil还是Nil,它们本质上都是一样的,都是(void *)0,只是写法不同。这样做的意义是为了区分不同的数据类型,比如你一看到用到了NULL就知道这是个C指针,看到nil就知道这是个Objective-C对象,看到Nil就知道这是个Class类型的数据。




    作者:sqc3375177 发表于2015/9/17 13:01:24  原文链接
    阅读:193 评论:0  查看评论
     
    [原]在Mac上配置adb命令

    在Mac上配置adb命令

    在Mac OS中使用adb命令时,应进行变量配置,步骤如下:

    一、终端中输入 cd ~

    二、输入touch .bash_profile 回车

    touch:如果没有,则创建文件,如果有,更新一下文件时间

    三、输入open -e .bash_profile

    open:打开文件

    回车后会在TextEdit中打开这个文件(如果未配置过环境变量,应该是个空白文件)。在文件中加如以下内容

    export PATH=${PATH}:/Users/sunqichao/worksoft/android-sdk-mac_x86/platform-tools

    这是我机器上的路径,具体个人机器路径,可右键点击platform-tools--》显示简介  查看

    作者:sqc3375177 发表于2015/9/14 16:27:07  原文链接
    阅读:109 评论:0  查看评论
     
    [原]Swift项目兼容Objective-C问题汇总

    原文地址:http://www.cocoachina.com/swift/20150608/12025.html


    一、解决问题

    Swift项目需要使用封装好的Objective-c组件、第三方类库,苹果提供的解决方案能够处理日常大部分需求,但还不能称之为完美,混编过程中会遇到很多问题。本文将Swift兼容Objective-c的问题汇总,以帮助大家更好的使用Swift,内容列表如下:

    1. Swift调用Objective-c代码

    2. Objective-c调用Swift代码

    3. Swift兼容Xib/Storyboard

    4. Objective-c巧妙调用不兼容的Swift方法

    5. 多Target编译错误解决

    6. 第三方类库支持

    二、基础混合编程

    Swift与Objective-c的代码相互调用,并不像Objective-c与C/C++那样方便,需要做一些额外的配置工作。无论是Swift调用Objective-c还是Objective-c调用Swift,Xcode在处理上都需要两个步骤:

    image_step.png

    2.1 Swift调用Objective-c代码

    Xcode对于Swift调用Objective-c代码,除宏定义外,其它支持相对完善。

    2.1.1 使用Objetvie-c的第一步

    告诉Xcode、哪些Objective-c类要使用,新建.h头文件,文件名可以任意取,建议采用“项目名-Bridging-Header.h”命令格式。

    image_0.png

    Tips

    Swift之IOS项目,在Xcode6创建类文件,默认会自动选择OS X标签下的文件,这时一定要选择iOS标签下的文件,否则会出现语法智能提示不起作用,严重时会导致打包出错。

    2.1.2 第二步,Target配置,使创建的头文件生效

    QQ截图20150605172845.png

    设置Objective-C Bridging Header时,路径要配置正确,例如:创建的名为“ILSwift-Bridging-Header.h”文件,存于ILSwift项目文件夹的根目录下,写法如下:

    1
    ILSwift/ILSwift-Bridging-Header.h

    当然,在新项目中,直接创建一个Objective-c类,Xcode会提示:

    QQ截图20150605172937.png

    直接选择Yes即可,如果不小心点了其它按钮,可以按照上面的步骤一步一步添加。

    2.2 Objective-c调用Swift代码

    2.2.1 Objective-c调用Swift代码两个步骤

    第一步告诉Xcode哪些类需要使用(继承自NSObject的类自动处理,不需要此步骤),通过关键字@objc(className)来标记

    1
    2
    3
    4
    5
    6
    7
    8
    import UIKit
    @objc(ILWriteBySwift)
    class ILWriteBySwift {
        var name: String!
        class func newInstance() -> ILWriteBySwift {
            return ILWriteBySwift()
        }
    }

    第二步引入头文件,Xcode头文件的命名规则为

    1
    $(SWIFT_MODULE_NAME)-Swift.h

    示例如下:

    1
    #import "ILSwift-Swift.h"

    Tips

    不清楚SWIFT_MODULE_NAME可通过以下步骤查看

    54.png

    2.2.2找不到$(SWIFT_MODULE_NAME)-Swift.h

    image_4.png

    1.遇到此问题可按以下步骤做常规性检查

    • 确定导入SWIFT_MODULE_NAME)-Swift.h头文件的文件名正确

    • SWIFT_MODULE_NAME)-Swift.h在clean后没有重新构建,执行Xcode->Product->Build

    2.头文件循环

    在混合编程的项目中,由于两种语言的同时使用,经常会出现以下需求:在Swift项目中需要使用Objectvie-c写的A类,而A类又会用到Swift的一些功能,头文件的循环,导致编译器不能正确构建$(SWIFT_MODULE_NAME)-Swift.h,遇到此问题时,在.h文件做如下处理

    1
    2
    3
    4
    //删除以下头文件
    //#import "ILSwift-Swift.h"
    //通过代码导入类
    @class ILSwiftBean;

    在Objevtive-c的.m文件最上面,添加

    1
    #import "ILSwift-Swift.h"

    出现Use of undecalared identifier错误或者找不到方法,如下:

    87.png

    引起的原因有以下几种可能:

    • 使用的Swift类不是继承自NSObject,加入关键字即可

    • SWIFT_MODULE_NAME)-Swift.h没有实时更新,Xcode->Product->Build

    • 此Swift文件中使用了Objective-c不支持的类型或者语法,如private

    出现部分方法找不到的问题,Xcode无智能提示:

    • 此方法使用了Objective-c不支持的类型或者语法

    苹果官方给出的不支持转换的类型

    • Generics

    • Tuples

    • Enumerations defined in Swift

    • Structures defined in Swift

    • Top-level functions defined in Swift

    • Global variables defined in Swift

    • Typealiases defined in Swift

    • Swift-style variadics

    • Nested types

    • Curried functions

    三、Xib/StoryBoard支持

    Swift项目在使用Xib/StoryBoard时,会遇到两种不同的问题

    • Xib:不加载视图内容

    • Storyboard:找不到类文件

    3.1 Xib不加载视图内容

    在创建UIViewController时,默认选中Xib文件,在Xib与类文件名一致时,可通过以下代码实例化:

    1
    let controller = ILViewController()

    运行,界面上空无一物,Xib没有被加载。解决办法,在类的前面加上@objc(类名),例如:  

    1
    2
    3
    4
    import UIKit
    @objc(ILViewController)
    class ILViewController: UIViewController {
    }

    Tips:

    StoryBoard中创建的UIViewController,不需要@objc(类名)也能够保持兼容 

    3.2 Storyboard找不到类文件

    Swift语言引入了Module概念,在通过关键字@objc(类名)做转换的时候,由于Storboard没有及时更新Module属性,会导致如下两种类型错误:

    3.2.1 用@objc(类名)标记的Swift类或者Objective-c类可能出现错误:

    2015-06-02 11:27:42.626 ILSwift[2431:379047] Unknown class _TtC7ILSwift33ILNotFindSwiftTagByObjcController in Interface Builder file.

    解决办法,按下图,选中Module中的空白,直接回车

    22.png

    3.2.2 无@objc(类名)标记的Swift类

    1
    2015-06-02 11:36:29.788 ILSwift[2719:417490] Unknown class ILNotFindSwiftController in Interface Builder file.

    解决办法,按下图,选择正确的Module

    23.png

    3.产生上面错误的原因: 在设置好Storyboard后,直接在类文件中,添加或者删除@objc(类名)关键字,导致Storyboard中 Module属性没有自动更新,所以一个更通用的解决办法是,让Storyboard自动更新Module,如下:

    69.png

    3.3 错误模拟Demo下载

    为了能够让大家更清楚的了解解决流程,将上面的错误进行了模拟,想动手尝试解决以上问题的同学可以直接下载demo

    四、Objective-c巧妙调用不兼容的Swift方法

    在Objective-c中调用Swift类中的方法时,由于部分Swift语法不支持转换,会遇到无法找到对应方法的情况,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import UIKit
    enum HTTPState {
        case Succed, Failed, NetworkError, ServerError, Others
    }
    class ILHTTPRequest: NSObject {
        class func requestLogin(userName: String, password: String, callback: (state: HTTPState) -> (Void)) {
            dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
                NSThread.sleepForTimeInterval(3)
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    callback(state: HTTPState.Succed)
                })
            })
        }
    }

    对应的$(SWIFT_MODULE_NAME)-Swift.h文件为:

    1
    2
    3
    4
    SWIFT_CLASS("_TtC12ILSwiftTests13ILHTTPRequest")
    @interface ILHTTPRequest : NSObject
    - (SWIFT_NULLABILITY(nonnull) instancetype)init OBJC_DESIGNATED_INITIALIZER;
    @end

    从上面的头文件中可以看出,方法requestLogin使用了不支持的Swift枚举,转换时方法被自动忽略掉,有以下两种办法,可以巧妙解决类似问题:

    4.1 用支持的Swift语法包装

    在Swift文件中,添加一个可兼容包装方法wrapRequestLogin,注意此方法中不能使用不兼容的类型或者语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import UIKit
    enum HTTPState: Int {
        case Succed = 0, Failed = 1, NetworkError = 2, ServerError = 3, Others = 4
    }
    class ILHTTPRequest: NSObject {
        class func requestLogin(userName: String, password: String, callback: (state: HTTPState) -> (Void)) {
            dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
                NSThread.sleepForTimeInterval(3)
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    callback(state: HTTPState.Succed)
                })
            })
        }
        class func wrapRequestLogin(userName: String, password: String, callback: (state: Int) -> (Void)) {
            self.requestLogin(userName, password: password) { (state) -> (Void) in
                callback(state: state.rawValue)
            }
        }
    }

    对应的$(SWIFT_MODULE_NAME)-Swift.h文件为:

    1
    2
    3
    4
    5
    SWIFT_CLASS("_TtC12ILSwiftTests13ILHTTPRequest")
    @interface ILHTTPRequest : NSObject
    + (void)wrapRequestLogin:(NSString * __nonnull)userName password:(NSString * __nonnull)password callback:(void (^ __nonnull)(NSInteger))callback;
    - (SWIFT_NULLABILITY(nonnull) instancetype)init OBJC_DESIGNATED_INITIALIZER;
    @end

    此时,我们可以在Objective-c中直接使用包装后的方法wrapRequestLogin

    4.2 巧妙使用继承

    使用继承可以支持所有的Swift类型,主要的功能在Objective-c中实现,不支持的语法在Swift文件中调用,例如,ILLoginSuperController做为父类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @interface ILLoginSuperController : UIViewController
    @property (weak, nonatomic) IBOutlet UITextField *userNameField;
    @property (weak, nonatomic) IBOutlet UITextField *passwordField;
    - (IBAction)loginButtonPressed:(id)sender;
    @end
    @implementation ILLoginSuperController
    - (IBAction)loginButtonPressed:(id)sender
    {
    }
    @end

    创建Swift文件,继承自ILLoginSuperController,在此Swift文件中调用那些不支持的语法

    1
    2
    3
    4
    5
    6
    7
    8
    import UIKit
    class ILLoginController: ILLoginSuperController {
        override func loginButtonPressed(sender: AnyObject!) {
            ILHTTPRequest.requestLogin(self.userNameField.text, password: self.passwordField.text) { (state) -> (Void) in
                //具体业务逻辑
            }
        }
    }

    五、多Target编译错误解决

    在使用多Target时,会出现一些编译错误

    5.1 Use of undeclared type

    image_9.png

    此类错误,是因为当前运行的Target找不到必须编译文件。将文件添加到Target即可,如下支持ILSwiftTests Target,选中ILSwiftTests前的复选框即可

    5.2.png

    5.2 does not have a member named

    此类错误可能由于如下两种原因引起,解决办法同上:

    5.22.png

    1.此方法来自父类,父类文件没有加入到当前Target

    2.此方法来自扩展,扩展没有加入到当前Target

    Tips

    如果检查发现,所有的类文件都已经准确添加到Target中,但编译还是不通过,此时着重检查桥接文件是否正确设置,是否将相应的头文件加入到了桥接文件中。如无特别要求,建议将所有Target的桥接文件全都指向同一文件。关于桥接文件的设置,请参考2.1

    六、第三方类库支持

    Swift项目取消了预编译文件,一些第三方Objective-c库没有导入必要框架(如UIKit)引起编译错误

    6.1 Cocoapods找不到.o文件

    在使用了Cocoapods项目中,会出现部分类库的.o文件找不到,导致此种错误主要是以下两种问题:

    • 类库本身存在编译错误

    • Swift没有预编译,UIKit等没有导入

    将此库文件中的代码文件直接加到项目中,编译,解决错误。

    6.2 JSONModel支持

    在Swift中可以使用JSONModel部分简单功能,一些复杂的数据模型建议使用Objevtive-c

    1
    2
    3
    4
    5
    6
    7
    import UIKit
    @objc(ILLoginBean)
    public class ILLoginBean: JSONModel {
        var userAvatarURL: NSString?
        var userPhone: NSString!
        var uid: NSString!
    }

    Tips

    在Swift使用JSONModel框架时,字段只能是NSFoundation中的支持类型,Swift下新添加的String、Int、Array等都不能使用

    6.3 友盟统计

    Swift项目中引入友盟统计SDK会出现referenced from错误:

    006.png

    解决办法,找到Other Linker Flags,添加-lz

    74.png

    七、综述

    现在大部分成熟的第三方框架都是使用Objective-c写的,开发时不可避免的涉及到两种语言的混合编程,期间会遇到很多奇怪的问题。因为未知才有探索的价值,Swift的简洁快速,能够极大的推进开发进度。所以从今天开始,大胆的开始尝试。


    作者:sqc3375177 发表于2015/6/9 18:17:06  原文链接
    阅读:677 评论:0  查看评论
     
    [原]转 Grand Central Dispatch 基础教程:Part 1/2 -swift

    本文转载,原文地址:http://www.cocoachina.com/ios/20150609/12072.html 


    原文 Grand Central Dispatch Tutorail for Swift: Part 1/2

    原文作者:Bj1433384542976729.pngrn Olav Ruud

    译者:Ethan Joe

    尽管Grand Central Dispatch(以下简称为GCD)已推出一段时间了,但并不是所有人都明白其原理;当然这是可以理解的,毕竟程序的并发机制很繁琐,而且基于C的GCD的API对于Swift的新世界并不是特别友好。

    在接下来的两节教程中,你将学习GCD的输入 (in)与输出 (out)。第一节将解释什么是GCD并了解几个GCD的基础函数。在第二节,你将学习几个更加进阶的GCD函数。

    Getting Started

    GCD是libdispatch的代名词,libdispatch代表着运行iOS与OS X的多核设备上执行并行代码的官方代码库。它经常有以下几个特点:

    • GCD通过将高代价任务推迟执行并调至后台运行的方式来提升App的交互速度。

    • GCD提供比锁与多线程更简单的并发模型,以此来避免一些由并发引起的Bug。

    为了理解GCD,你需要明白一些与线程、并发的相关的概念。这些概念间有着细微且模糊的差别,所以在学习GCD前请简略地熟悉一下这些概念。

    连续性 VS 并发性

    这些术语用来描述一些被执行的任务彼此间的关系。连续性执行任务代表着同一时间内只执行一个任务,而并发性执行任务则代表着同一时间内可能会执行多个任务。

    任务

    在这篇教程中你可以把每个任务看成是一个闭包。 事实上,你也可以通过函数指针来使用GCD,但在大多数情况下这明显有些麻烦。所以,闭包用起来更简单。

    不知道什么是Swift中的闭包?闭包是可被储存并传值的可调用代码块,当它被调用时可以像函数那样包含参数并返回值。

    Swift中的闭包和Objective-C的块很相近,它们彼此间是可以相互交替的。这个过程中有一点你不能做的是:用Objective-C的块代码去交互具有Swift独有属性属性的闭包,比如说具有元组属性的闭包。但是从Swift端交互Objective-C端的代码则是毫无障碍的,所以无论何时你在文档中看到到的Objective-C的块代码都是可用Swift的闭包代替的。

    同步 VS 异步

    这些术语用来描述当一个函数的控制权返回给调用者时已完成的工作的数量。

    同步函数只有在其命令的任务完成时才会返回值。

    异步函数则不会等待其命令的任务完成,即会立即返回值。所以,异步函数不会锁住当前线程使其不能向队列中的下一位函数执行。

    值得注意的是---当你看到一个同步函数锁住(block)了当前进程,或者一个函数是锁函数(blocking function)或是锁运算(block operation)时别认混了。这里的锁(blocks)是用来形容其对于自己线程的影响,它跟Objective-C中的块(block)是不一样的。再有一点要记住的就是在任何GCD文档中涉及到Objective-C的块代码都是可以用Swift的闭包来替换的。

    临界区

    这是一段不能被在两个线程中同时执行的代码。这是因为这段代码负责管理像变量这种若被并发进程使用便会更改的可共享资源。

    资源竞争

    这是一种软件系统在一种不被控制的模式下依靠于特定队列或者基于事件执行时间进行运行的情况,比如说程序当前多个任务执行的具体顺序。资源竞争可以产生一些不会在代码排错中立即找到的错误。

    死锁

    两个或两个以上的进程因等待彼此完成任务或因执行其他任务而停止当前进程运行的情况被称作为死锁。举个例子,进程A因等待进程B完成任务而停止运行,但进程B也在等待进程A完成任务而停止运行的僵持状态就是死锁。

    线程安全性

    具有线程安全性的代码可以在不产生任何问题(比如数据篡改、崩溃等)的情况下在多线程间或是并发任务间被安全的调用。不具有线程安全性的代码的正常运行只有在单一的环境下才可被保证。举个具有线性安全性的代码示例let a = ["thread-safe"]。你可以在多线程间,不产生任何bug的情况下调用这个具有只读性的数组。相反,通过var a = ["thread-unsafe"]声明的数组是可变可修改的。这就意味着这个数组在多线层间可被修改从而产生一些不可预测的问题,对于那些可变的变量与数据结构最好不要同时在多个线程间使用。

    上下文切换

    上下文切换是当你在一个进程中的多个不同线程间进行切换时的一种进程进行储存与恢复的状态。这种进程在写多任务App时相当常见,但这通常会产生额外的系统开销。

    并发 VS 并行

    并发和并行总是被同时提及,所以有必要解释一下两者间的区别。

    并发代码中各个单独部分可以被"同时"执行。不管怎样,这都由系统决定以何种方式执行。具有多核处理器的设备通过并行的方式在同一时间内实现多线程间的工作;但是单核处理器设备只能在同一时间内运行在单一线程上,并利用上下文切换的方式切换至其他线程以达到跟并行相同的工作效果。如下图所示,单核处理器设备运行速度快到形成了一种并行的假象。

    1.jpg

    并发 VS 并行

    尽管你会在GCD下写出使用多线程的代码,但这仍由GCD来决定是否会使用并发机制。并行机制包含着并发机制,但并发机制却不一定能保证并行机制的运行。

    队列

    GCD通过队列分配的方式来处理待执行的任务。这些队列管理着你提供给GCD待处理的任务并以FIFO的顺序进行处理。这就得以保证第一个加进队列的任务会被首个处理,第二个加进队列的任务则被其次处理,其后则以此类推。

    连续队列

    连续队列中的任务每次执行只一个,一个任务只有在其前面的任务执行完毕后才可开始运行。如下图所示,你不会知道前一个任务结束到下一个任务开始时的时间间隔。

    2.jpg

    连续队列

    每一个任务的执行时间都是由GCD控制的;唯一一件你可以确保的事便是GCD会在同一时间内按照任务加进队列的顺序执行一个任务。

    因为在连续队列中不允许多个任务同时运行,这就减少了同时访问临界区的风险;这种机制在多任务的资源竞争的过程中保护了临界区。假如分配任务至分发队列是访问临界区的唯一方式,那这就保证了的临界区的安全。

    并发队列

    并发队列中的任务依旧以FIFO顺序开始执行。。。但你能知道的也就这么多了!任务间可以以任何顺序结束,你不会知道下一个任务开始的时间也不会知道一段时间内正在运行任务的数量。因为,这一切都是由GCD控制的。

    如下图所示,在GCD控制下的四个并发任务:

    3.jpg

    并发队列

    需要注意的是,在任务0开始执行后花了一段时间后任务1才开始执行,但任务1、2、3便一个接一个地快速运行起来。再有,即便任务3在任务2开始执行后才开始执行,但任务3却更早地结束执行。

    任务的开始执行的时间完全由GCD决定。假如一个任务与另一个任务的执行时间相互重叠,便由GCD决定(在多核非繁忙可用的情况下)是否利用不同的处理器运行或是利用上下文切换的方式运行不同的任务。

    为了用起来有趣一些,GCD提供了至少五种特别的队列来对应不同情况。

    队列种类

    首先,系统提供了一个名为主队列(main queue)的特殊连续队列。像其他连续队列一样,这个队列在同一间内只能执行一个任务。不管怎样,这保证了所有任务都将被这个唯一被允许刷新UI的线程所执行。它也是唯一一个用作向UIView对象发送信息或推送监听(Notification)。

    GCD也提供了其他几个并发队列。这几个队列都与自己的QoS (Quality of Service)类所关联。Qos代表着待处理任务的执行意图,GCD会根据待处理任务的执行意图来决定最优化的执行优先权。

    • QOS_CLASS_USER_INTERACTIVE: user interactive类代表着为了提供良好的用户体验而需要被立即执行的任务。它经常用来刷新UI、处理一些要求低延迟的加载工作。在App运行的期间,这个类中的工作完成总量应该很小。

    • QOS_CLASS_USER_INITIATED:user initiated类代表着从UI端初始化并可异步运行的任务。它在用户等待及时反馈时和涉及继续运行用户交互的任务时被使用。

    • QOS_CLASS_UTILITY:utility类代表着长时间运行的任务,尤其是那种用户可见的进度条。它经常用来处理计算、I/O、网络通信、持续数据反馈及相似的任务。这个类被设计得具有高效率处理能力。

    • QOS_CLASS_BACKBROUND:background类代表着那些用户并不需要立即知晓的任务。它经常用来完成预处理、维护及一些不需要用户交互的、对完成时间并无太高要求的任务。

    要知道苹果的API也会使用这些全局分配队列,所以你分派的任务不会是队列中的唯一一个。

    最后,你也可以自己写一个连续队列或是并发队列。算起来你起码最少会有五个队列:主队列、四个全局队列再加上你自己的队列。

    以上便是分配队列的全体成员。

    GCD的关键在于选择正确的分发函数以此把你的任务分发至队列。理解这些东西的最好办法就是完善下面的Sample Project。

    Sample Project

    既然这篇教程的目的在于通过使用GCD在不同的线程间安全地调用代码,那么接下来的任务便是完成这个名为GooglyPuff的半成品。

    GooglyPuff是一款通过CoreImage脸部识别API在照片中人脸的双眼的位置上贴上咕噜式的大眼睛且线程不安全的App。你既可以从Photo Library中选择照片,也可以通过网络从事先设置好的地址下载照片。

    GooglyPuff Swift Start 1

    将工程下载至本地后用Xcode打开并编译运行。它看起来是这样的:

    4.jpg

    GooglyPuff

    在工程中共有四个类文件:

    • PhotoCollectionViewController:这是App运行后显示的首个界面。它将显示所有被选照片的缩略图。

    • PhotoDetailViewController:它将处理将咕噜眼添加至照片的工作并将处理完毕的照片显示在UIScrollView中。

    • Photo:一个包含着照片基本属性的协议,其中有image(未处理照片)、thumbnail(裁减后的照片)及status(照片可否使用状态);两个用来实现协议的类,DownloadPhoto将从一个NSURL实例中实例化照片,而AssetPhoto则从一个ALAsset实例中实例化照片。

    • PhotoManager:这个类将管理所有Photo类型对象。

    使用dispatch_async处理后台任务

    回到刚才运行的App后,通过自己的Photo Library添加照片或是使用Le internet下载一些照片。

    需要注意的是当你点击PhotoCollectionViewController中的一个UICollectionViewCell后,界面切换至一个新的PhotoDetailViewController所用的时间;对于那些处理速度较慢的设备来说,处理一张较大的照片会产生一个非常明显的延迟。

    这种情况下很容易使UIViewController的viewDidLoad因处理过于混杂的工作而负载;这么做的结果便在view controller出现前产生较长的延迟。假如可能的话,我们最好将某些工作放置后台处理。

    这听起来dispatch_async该上场了。

    打开PhotoDetailViewController后将viewDidLoad函数替换成下述代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    override func viewDidLoad() {
    super.viewDidLoad()
    assert(image != nil, "Image not set; required to use view  controller")
    photoImageView.image = image
    // Resize if neccessary to ensure it's not pixelated
    if image.size.height <= photoImageView.bounds.size.height &&
     image.size.width <= photoImageView.bounds.size.width {
    photoImageView.contentMode = .Center
    }
    dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { // 1
    let overlayImage = self.faceOverlayImageFromImage(self.image)
    dispatch_async(dispatch_get_main_queue()) { // 2
    self.fadeInNewImage(overlayImage) // 3
      }
     }
    }

    在这里解释一下上面修改的代码:

    1. 你首先将照片处理工作从主线程(main thread)移至一个全局队列(global queue)。因为这是一个异步派发(dispatch_async的调用,闭包以异步的形式进行传输意味着调用的线程将会被继续执行。这样一来便会使viewDidLoad更早的在主线程上结束执行并使得整个加载过程更加流畅。与此同时,脸部识别的过程已经开始并在一段时间后结束。

    2. 这时脸部识别的过程已经结束并生成了一张新照片。当你想用这张新照片来刷新你的UIImageView时,你可以向主线程添加一个新的闭包。需要注意的是--主线程只能用来访问UIKit。

    3. 最后,你便用这张有着咕噜眼的fadeInNewImage照片来刷新UI。

    有没有注意到你已经用了Swift的尾随闭包语法(trailing closure syntax),就是以在包含着特定分配队列参数的括号后书写表达式的形式了向dispatch_async传递闭包。假如把闭包写出函数括号的话,语法会看起来更加简洁。

    运行并编译App;选一张照片后你会发现view controller加载得很快,咕噜眼会在很短的延迟后出现。现在的运行效果看起来比之前的好多了。当你尝试加载一张大得离谱的照片时,App并不会在view controller加载时而延迟,这种机制便会使App表现得更加良好。

    综上所述,dispatch_async将任务以闭包的形式添加至队列后立即返回。这个任务在之后的某个时间段由GCD所执行。当你要在不影响当前线程工作的前提下将基于网络或高密度CPU处理的任务移至后台处理时,dispatch_asnyc便派上用场了。

    接下来是一个关于在使用dispatch_asnyc的前提下,如何使用以及何时使用不同类型队列的简洁指南:

    • 自定义连续队列(Custom Serial Queue): 在当你想将任务移至后台继续工作并且时刻监测它的情况下,这是一个不错的选择。需要注意的是当你想从一个方法中调用数据时,你必须再添加一个闭包来回调数据或者考虑使用dispatch_sync。

    • 主队列(Main Queue[Serial]):这是一个当并发队列中的任务完成工作时来刷新UI的普遍选择。为此你得在一个闭包中写入另一个闭包。当然,假如你已经在主线程并调用一个面向主线程的dispatch_async的话,你需要保证这个新任务在当前函数运行结束后的某个时间点开始执行。

    • 并发队列(Concurrent Queue):对于要运行后台的非UI工作是个普遍的选择。

    获取全局队列的简洁化变量

    你也许注意到了dispatch_get_global_queue函数里的QoS类的参数写起来有些麻烦。这是因为qos_class_t被定义成一个值类型为UInt32且最后还要被转型为Int的结构体。我们可以在Utils.swift中的URL变量下面添加一些全局的简洁化变量,以此使得调用全局队列更加简便。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var GlobalMainQueue: dispatch_queue_t {
    return dispatch_get_main_queue()
    }
    var GlobalUserInteractiveQueue: dispatch_queue_t {
    return dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.value), 0)
    }
    var GlobalUserInitiatedQueue: dispatch_queue_t {
    return dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)
    }
    var GlobalUtilityQueue: dispatch_queue_t {
    return dispatch_get_global_queue(Int(QOS_CLASS_UTILITY.value), 0)
    }
    var GlobalBackgroundQueue: dispatch_queue_t {
    return dispatch_get_global_queue(Int(QOS_CLASS_BACKGROUND.value), 0)
    }

    回到PhotoDetailViewController中viewDidLoad函数中,用简洁变量代替dispatch_get_global_queue和dispatch_get_main_queue。

    1
    2
    3
    4
    5
    6
    dispatch_async(GlobalUserInitiatedQueue) {
       let overlayImage = self.faceOverlayImageFromImage(self.image)
       dispatch_async(GlobalMainQueue) {
         self.fadeInNewImage(overlayImage)
       }
     }

    这样就使得派发队列的调用的代码更加具有可读性并很轻松地得知哪个队列正在被使用。

    利用dispatch_after实现延迟

    考虑一下你App的UX。你的App有没有使得用户在第一次打开App的时候不知道该干些什么而感到不知所措呢?: ]

    假如在PhotoManager中没有任何一张照片的时候便向用户发出提醒应该是一个不错的主意。不管怎样,你还是要考虑一下用户在App主页面上的注意力:假如你的提醒显示得过快的话,用户没准在因为看着其他地方而错过它。

    当用户第一次使用App的时候,在提醒显示前执行一秒钟的延迟应该足以吸引住用户的注意力。

    在PhotoCollectionViewController.swift底部的showOrHideBarPrompt函数中添加如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func showOrHideNavPrompt() {
       let delayInSeconds = 1.0
       let popTime = dispatch_time(DISPATCH_TIME_NOW,
                              Int64(delayInSeconds * Double(NSEC_PER_SEC))) // 1
       dispatch_after(popTime, GlobalMainQueue) { // 2
       let count = PhotoManager.sharedManager.photos.count
       if count > 0 {
        self.navigationItem.prompt = nil
       else {
        self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
       }
      }
     }

    当你的UICollectionView重载的时候,viewDidLoad函数中的showOrHideNavPrompt将被执行。解释如下:

    1. 你声明了一个代表具体延迟时间的变量。

    2. 你将等待delayInSeconds变量中设定的时间然后向主队列异步添加闭包。

    编译并运行App。你会看到一个在很大程度上吸引用户注意力并告知他们该做些什么的细微延迟。

    dispatch_after就像一个延迟的dispatch_async。你仍旧在实时运行的时候毫无操控权并且一旦dispatch_after返回后你也无法取消整个延迟任务。

    还在思考如何适当的使用dispatch_after?

    • 自定义连续队列(Custom Serial Queue):当你在自定义连续队列上使用dispatch_after时一定要当心,此时最好不要放到主队列上执行。

    • 主队列(Main Queue[Serial]):这对于dispatch_after是个很好的选择;Xcode对此有一个不错的自动执行至完成的样板。

    • 并发队列(Concurrent Queue):在自定义并发队列上使用dispatch_after时同样要当心,即便你很少这么做。此时最好放到主队列上执行。

    单例和线程安全

    单例,不管你love it还是hate it,他们对于iOS都是非常重要的。: ]

    一提到单例(Singleton)人们便觉得他们是线程不安全的。这么想的话也不是没有道理:单例的实例经常在同一时间内被多线程所访问。PhotoManager类便是一个单例,所以你要思考一下上面提到的问题。

    两个需要考虑的情况,单例实例初始化时和实例读写时的线程安全性。

    先考虑第一种情况。因为在swift是在全局范围内初始化变量,所以这种情况较为简单。在Swift中,当全局变量被首次访问调用时便被初始化,并且整个初始化过程具有原子操作性。由此,代码的初始化过程便成为一个临界区并且在其他线程访问调用全局变量前完成初始化。Swift到底是怎么做到的?其实在整个过程中,Swift通过dispatch_once函数使用了GCD。若想了解得更多的话请看这篇Swift官方Blog

    在线程安全的模式下dispatch_once只会执行闭包一次。当一个在临界区执行的线程--向dispatch_once传入一个任务--在它结束运行前其它的线程都会被限制住。一旦执行完成,它和其他线程便不会再次在此区域执行。通过let把单例定义为全局定量的话,我们就可以保证这个变量的值在初始化后不会被修改。总之,Swift声明的所有全局定量都是通过线程安全的初始化得到的单例。

    但我们还是要考虑读写问题。尽管Swift通过使用dispatch_once确保我们在线程安全的模式下初始化单例,但这并不能代表单例的数据类型同样具有线程安全性。举个例子,假如一个全局变量是一个类的实例,你仍可以在类内的临界区操控内部数据,这将需要利用其他的方式来保证线程安全性。

    处理读取与写入问题

    保证线程安全性的实例化不是我们处理单例时的唯一问题。假如一个单例属性代表着一个可变的对象,比如像PhotoManager 中的photos数组,那么你就需要考虑那个对象是否就有线程安全性。

    在Swift中任何用let声明的变量都是一个只可读并线程安全的常量。但是用var声明的变量都是值可变且并线程不安全的。比如Swift中像Array和Dictionary这样的集合类型若被声明为值可变的话,它们就是线程不安全的。那Foundation中的NSArray线程是否安全呢?不一定!苹果还专门为那些线程非安全的Foundation类列了一个清单。

    尽管多线程可以在不出现问题的情况下同时读取一个Array的可变实例,但当一个线程试图修改实例的时候另一个线程又试图读取实例,这样的话安全性可就不能被保证了。

    在下面PhotoManager.swift中的addPhoto函数中找一找错误:

    1
    2
    3
    4
    5
    6
    func addPhoto(photo: Photo) {
      _photos.append(photo)
      dispatch_async(dispatch_get_main_queue()) {
        self.postContentAddedNotification()
      }
    }

    这个写取方法修改了可变数组的对象。

    再来看一看photos的property:

    1
    2
    3
    4
    private var _photos: [Photo] = []
    var photos: [Photo] {
      return _photos
    }

    当property的getter读取可变数组的时候它就是一个读取函数。调用者得到一份数组的copy并阻止原数组被不当修改,但这不能在一个线程调用addPhoto方法的同时阻止另一个线程回调photo的property的getter。

    提醒:在上述代码中,调用者为什么不直接得到一份photos的copy呢?这是因为在Swift中,所有的参数和函数的返回值都是通过推测(Reference)或值传输的。通过推测进行传输和Objective-C中传输指针是一样的,这就代表着你可以访问调用原始对象,并且对于同一对象的推测后其任何改变都可以被显示出来。在对象的copy中通过值结果传值且对于copy的更改都不对原是对象造成影响。Swift默认以推测机制或结构体的值来传输类的实例。

    Swift中的Array和Dictionary都是通过结构体来实现的,当你向前或向后传输这些实例的时候,你的代码将会执行很多次的copy。这时不要当心内存使用问题,因为这些Swift的集合类型(如Array、Dictionary)的执行过程都已被优化,只有在必要的时候才会进行copy。对于来一个通过值传输的Array实例来说,只有在被传输后才会进行其第一次修改。

    这是一个常见的软件开发环境下的读写问题。GCD通过使用dispatch barriers提供了一个具有读/写锁的完美解决方案。

    在使用并发队列时,dispatch barriers便是一组像连续性路障的函数。使用GCD的barrier API保证了被传输的闭包是在特定时间内、在特定队列上执行的唯一任务。这就意味着在派发的barrier前传输的任务必须在特定闭包开始执行前完成运行。

    当闭包到达后,barrier便开始执行闭包并保证此段时间内队列不会再执行任何其他的闭包。特定闭包一旦完成执行,队列便会返回其默认的执行状态。GCD同样提供了具有同步与异步功能的barrier函数。

    下面的图式描述了在多个异步任务中的barrier函数的运行效果:

    5.jpg

    dispatch barrier

    需要注意的是在barrier执行前程序是以并发队列的形式运行,但当barrier一旦开始运行后,程序便以连续队列的形式运行。没错,barrier是这段特定时间内唯一被执行的任务。当barrier执行结束后,程序再次回到了普通的并发队列运行状态。

    对于barrier函数我们做一些必要的说明:

    • 自定义连续队列(Custom Serial Queue):在这种情况下不是特别建议使用barrier,因为barrier在连续队列执行期间不会起到任何帮助。

    • 全局并发队列(Global Concurrent Queue):谨慎使用;当其他系统也在使用队列的时候,你应该不想把所有的队列都垄为自己所用。

    • 自定义并发队列(Custom Concurrent Queue):适用于涉及临界区及原子性的代码。在任何你想要保正设定(setting)或初始化具有线程安全性的情况下,barrier都是一个不错的选择。

    从上面对于自定义并发序列解释可以得出结论,你得写一个自己的barrier函数并将读取函数和写入函数彼此分开。并发序列将允许多个读取过程同步运行。

    打开PhotoManager.swift,在photos属性下给类文件添加如下的私有属性:

    1
    2
    private let concurrentPhotoQueue = dispatch_queue_create(
        "com.raywenderlich.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT)

    通过dispatch_queue_create函数初始化了一个名为concurrentPhotoQueue的并发队列。第一个参数是一个逆DNS风格的命名方式;其描述在debugging时会非常有用。第二个参数设定了你的队列是连续性的还是并发性的。

    很多网上的实例代码中都喜欢给dispatch_queue_create的第二个参数设定为0或NULL。其实这是一种过时的声明连续分派队列的方法。你最好用你自己的参数设定它。

    找到addPhoto函数并代替为以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    func addPhoto(photo: Photo) {
      dispatch_barrier_async(concurrentPhotoQueue) { // 1
        self._photos.append(photo) // 2
        dispatch_async(GlobalMainQueue) { // 3
          self.postContentAddedNotification()
        }
      }
    }

    你的新函数是这样工作的:

    1. 通过使用你自己的自定义队列添加写入过程,在不久后临界区执行的时候这将是你的队列中唯一执行的任务。

    2. 向数组中添加对象。只要这是一个barrier属性的闭包,那么它在concurrentPhotoQueue队列中绝不会和其他闭包同时运行。

    3. 最后你推送了一个照片添加完毕的消息。这个消息应该从主线程推送因为它将处理一些涉及UI的工作,所以你为这个消息以异步的形式向主线程派发了任务。

    以上便处理好了写入方法的问题,但是你还要处理一下photos的读取方法。

    为了保证写入方面的线程安全行,你需要在concurrentPhotoQueue队列中运行读取方法。因为你需要从函数获取返回值并且在读取任务返回前不会运行任何其他的任务,所以你不能向队列异步派发任务。

    在这种情况下,dispatch_sync是一个不错的选择。

    dispatch_sync可以同步传输任务并在其返回前等待其完成。使用dispatch_sync跟踪含有派发barrier的任务,或者在当你需要使用闭包中的数据时而要等待运行结束的时候使用dispatch_sync。

    谨慎也是必要的。想象一下,当你对一个马上要运行的队列调用dispatch_sync时,这将造成死锁。因为调用要等到闭包B执行后才能开始运行,但是这个闭包B只有等到当前运行的且不可能结束的闭包A执行结束后才有可能结束。

    这将迫使你时刻注意自己调用的的或是传入的队列。

    来看一下dispatch_sync的使用说明:

    • 自定义连续队列(Custome Serial Queue):这种情况下一定要非常小心;假如一个队列中正在执行任务并且你将这个队列传入dispatch_sync中使用,这毫无疑问会造成死锁。

    • 主队列(Main Queue[Serial]):同样需要小心发生死锁。

    • 并发队列(Concurrent Queue):在对派发barrier执行同步工作或等待一个任务的执行结束后需要进行下一步处理的情况下,dispatch_sync是一个不错的选择。

    依旧在PhotoManager.swift文件中,用以下代码替换原有的photos属性:

    1
    2
    3
    4
    5
    6
    7
    var photos: [Photo] {
      var photosCopy: [Photo]!
      dispatch_sync(concurrentPhotoQueue) { // 1
        photosCopy = self._photos // 2
      }
      return photosCopy
    }

    分布解释一下:

    1. 同步派发concurrentPhotoQueue使其执行读取功能。

    2. 储存照片数组至photosCopy并返回。

    恭喜--你的PhotoManager单例现在线程安全了。不管现在是执行读取还是写入功能,你都可以保证整个单例在安全模式下运行。

    队列可视化

    还不能完全理解GCD的基础知识?接下来我们将在一个简单的示例中使用断点和NSLog功能确保你进一步理解GCD函数运行原理。

    我将使用两个动态的GIF帮助你理解dispatch_async和dispatch_sync。在GIF的每步切换下,注意代码断点与图式的关系。

    dispatch_sync重览

    1
    2