注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS 音视频编解码----H264-I(关键)帧,B/P(参考)帧

内容元素1.图像(image)2.音频(Audio)3.元素信息(Meta-data)编码格式1.Video:H2642.Audio:AAC3.容器封装:MP4/MOV/FLV/RM/RMVB/AVIH264当我们需要对发送的视频文件进行编码时,只要是H264...
继续阅读 »




  • 内容元素

    1.图像(image)


    2.音频(Audio)


    3.元素信息(Meta-data)


  • 编码格式

    1.Video:H264


    2.Audio:AAC


    3.容器封装:MP4/MOV/FLV/RM/RMVB/AVI


  • H264

    当我们需要对发送的视频文件进行编码时,只要是H264文件,AVFoundation都提供视频编解码器支持,这个标准被广泛应用于消费者视频摄像头捕捉到的资源并成为网页流媒体视频最主要的格式。H264规范是MPEG4定义的一部分,H264遵循早期的MPEG-1/MPEG-2标准,但是在以更低比特率得到 更高图片质量方面有了进步。


  • 编码的本质






    • 为什么要编码?

    举个例子: (具体大小我没数过,只做讲解参考)
    你的老公,Helen,将于明天晚上6点零5份在重庆的江北机场接你
    ----------------------23 * 2 + 10 = 56个字符--------------------------


    你的老公将于明天晚上6点零5分在江北机场接你
    ----------------------20 * 2 + 2 = 42个字符----------------------------


    Helen将于明天晚上6点在机场接你
    ----------------------10 * 2 + 2 = 26个字符----------------------------


    相信大家看了这个例子之后,心里应该大概明白编码的本质:只要不接收方不会产生误解,就可以产生数据的承载量,编码视频的本质也是如此,编码的本质就是减少数据的冗余


    • 在引入I(关键)帧,B/P(参考)帧之前,我们先来了解一下人眼和视频摄像头的对帧的识别

      我们人眼看实物的时候,一般的一秒钟只要是连续的16帧以上,我们就会认为事物是动的,对于摄像头来说,一秒钟所采集的图片远远高于了16帧,可以达到几十帧,对于一些高质量的摄像头,一秒钟可以达到60帧,对于一般的事物来说,你一秒钟能改变多少个做动作?所以,当摄像头在一秒钟内采集视频的时候,前后两帧的图片数据里有大量的相同数据,对于这些数据我们该怎么处理了?当然前后两帧也有不同





  • I帧(I-frames,也叫关键帧)


    • 我们知道在视频的传输过程中,它是分片段传输的,这个片段的第一帧,也就是第一张图片,就是I帧,也就是传说中的关键帧
    • I帧:也就是关键帧,帧内压缩(也就是压缩独立视频帧,被称为帧内压缩),帧内压缩通过消除包含在每个独立视频帧内的色彩以及结构中的冗余信息来进行压缩,因此可在不降低图片适量的情况下尽可能的缩小尺寸,这类似于JEPG压缩的原理,帧内压缩也可以称为有损压缩算法,但通常用于对原因图片的一部分进行处理,以生成极高质量的照片,通过这一过程的创建的帧称为I-frames;将第一帧完整的保存下来.如果没有这个关键帧后面解码数据,是完成不了的.所以I帧特别关键.

  • P帧(P-frames,又称为预测帧)

    • P帧:向前参考帧(在I帧(关键帧)后,P帧参考关键帧,保存下一帧和前一帧的不同数据).压缩时只参考前一个帧.属于帧间压缩技术.
    • 帧间压缩技术:很多帧被组合在一起作为一组图片(简称GOP),对于GOP所存在的时间维度的冗余可以被消除,如果想象视频文件中的典型场景,就会有一些特定的运动元素的概念,比如行驶中的汽车或者街道上行走的路人,场景的背景环信通道是固定的,或者在一定的时间内,有些元素的改变很小或者不变,这些数据就称为时间上的冗余,这些数据就可以通过帧间压缩的方式进行消除,也就是帧间压缩,视频的第一帧会被作为关键帧完整保存下来.而后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面所有的帧只存储于前一帧的差异.这样就能将数据大大的减少.从而达到一个高压缩率的效果.这就是P帧,保存前后两帧不通的数据

  • B帧(B-frames,又称为双向帧)

    • B帧,又叫双向帧,它是基于使用前后两帧进行编码后得到的帧,几乎不需要存储空间,但是解压过程会消耗较长的时间,因为它依赖周围其他的帧
    • B帧的特点:B帧使得视频的压缩率更高.存储的数据量更小.如果B帧的数量越多,你的压缩率就越高.这是B帧的优点,但是B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.如果网络状态很好的话,解码会比较快,如果网络不好时解码会稍微慢一些.丢包时还需要重传.对实时互动的直播,一般不会使用B帧.如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧.如果我们在实时互动的直播,我们需要提高时效性,这时就不能使用B帧了.

  • GOP一组帧的理解

    • 如果在一秒钟内,有30帧.这30帧可以画成一组.如果摄像机或者镜头它一分钟之内它都没有发生大的变化.那也可以把这一分钟内所有的帧画做一组.

    • 什么叫一组帧?
      就是一个I帧到下一个I帧.这一组的数据.包括B帧/P帧.我们称为GOP






    • 视频花屏/卡顿原因

      • 我们平常在观看视频的时候,出现视频的花屏或者卡顿,第一反应就是我们的网络出现了问题,其实我们的网络没有问题,是我们在解码的时候I帧,B/P帧出现了丢失
      • 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误.
      • 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像.
      • 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因.

    所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.



    作者:枫紫
    链接:https://www.jianshu.com/p/94d2a8bbc3ac





    收起阅读 »

    iOS 音视频编解码基本概念

    内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC容器封装: • MP4/MOV/FLV/RM/RMVB/AVI.视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面...
    继续阅读 »



    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 容器封装: • MP4/MOV/FLV/RM/RMVB/AVI

    • .视频相关基础概念

      • 1.视频文件格式

        相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.jpg等,我们常见的视频文件后缀有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。这些后缀名通常在操作系统上用相应的应用程序打开,比如.doc用word打开。对于视频来说,为什么会有这么多的文件格式了,那是因为通过了不同的方式来实现了视频这件事---------视频的封装格式。
    • 2.视频的封装格式

      视频封装格式,通常我们把它称作为视频格式,它相当于一种容器,比如可乐的瓶子,矿泉水瓶等等。它里面包含了视频的相关信息(视频信息,音频信息,解码方式等等),一种封装格式直接反应了视频的文件格式,封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.




    • 相关视频封装格式的优缺点:

      • 1.AVI 格式:这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。
      • 2.WMV 格式:可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。
      • 3.MPEG 格式:为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。
      • 4.Matroska 格式:是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。
      • 5.Real Video 格式:用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。
      • 6.QuickTime File Format 格式:是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。
      • 7.Flash Video 格式: Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。
    • 视频的编码格式

    • 视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:

      • 视频的质量、
      • 用来表示视频所需要的数据量(通常称之为码率)、
      • 编码算法和解码算法的复杂度
      • 针对数据丢失和错误的鲁棒性(Robustness)
      • 编辑的方便性
      • 随机访问
      • 编码算法设计的完美性
      • 端到端的延时以及其它一些因素
    • 常见的编码方式:

    • H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

      • H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
      • H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
      • H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
      • H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
    • 当前不建议用H.265是因为太过于消耗CPU,而且目前H.264已经满足了大多的视频需求,虽然H.265是H.264的升级版,期待后续硬件跟上

    • MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

      • MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
      • MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
      • MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
        其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。
    • 可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

    • 音频编码方式

      • 视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

      • AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。

      • MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。

      • WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

    直播/小视频中的编码格式

    • 视频编码格式

      • H264编码的优势:
        低码率
        高质量的图像
        容错能力强
        网络适应性强
    • 总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
      举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
      音频编码格式:

    • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

    • LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)

    • HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)

    • 优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

    关于H264

    • H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

    • 图像

      • H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

    当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

    当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

    「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好




    • 片(Slice),每一帧图像可以分为多个片

    网络提取层单元(NALU, Network Abstraction Layer Unit),
    NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

    宏块(Macroblock),分片是由宏块组成。




    作者:枫紫
    链接:https://www.jianshu.com/p/9602f3c9b82b


    收起阅读 »

    iOS 特效 - iCarousel

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其...
    继续阅读 »

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其他“CoverFlow”库不同,iCarousel 可以处理任何类型的视图,而不仅仅是图像,因此它非常适合在您的应用程序中以流畅且令人印象深刻的方式呈现分页数据。它还使得以最少的代码更改在不同的轮播效果之间切换变得非常容易。

    支持的操作系统和 SDK 版本

    • 支持的构建目标 - iOS 10.0 / Mac OS 10.12(Xcode 8.0,Apple LLVM 编译器 8.0)
    • 最早支持的部署目标 - iOS 5.0 / Mac OS 10.7
    • 最早的兼容部署目标 - iOS 4.3 / Mac OS 10.6

    注意:“支持”表示该库已经过此版本的测试。“兼容”意味着库应该在这个操作系统版本上工作(即它不依赖于任何不可用的 SDK 功能)但不再进行兼容性测试,可能需要调整或错误修复才能正确运行。

    ARC兼容性

    从 1.8 版开始,iCarousel 需要 ARC。如果您希望在非 ARC 项目中使用 iCarousel,只需将 -fobjc-arc 编译器标志添加到 iCarousel.m 类。为此,请转到目标设置中的 Build Phases 选项卡,打开 Compile Sources 组,双击列表中的 iCarousel.m 并在弹出窗口中键入 -fobjc-arc。

    如果您希望将整个项目转换为 ARC,请在 iCarousel.m 中注释掉 #error 行,然后在 Xcode 中运行 Edit > Refactor > Convert to Objective-C ARC... 工具并确保您希望转换的所有文件使用 ARC 进行(包括 iCarousel.m)检查。

    线程安全

    iCarousel 派生自 UIView 并且 - 与所有 UIKit 组件一样 - 它只能从主线程访问。您可能希望使用线程来加载或更新轮播内容或项目,但始终确保一旦您的内容加载完毕,您就可以在更新轮播前切换回主线程。

    安装

    要在应用程序中使用 iCarousel 类,只需将 iCarousel 类文件(不需要演示文件和资产)拖到您的项目中并添加 QuartzCore 框架。您也可以使用 Cocoapods 以正常方式安装它。


    轮播类型

    iCarousel 支持以下内置显示类型:

    • iCarouselTypeLinear
    • iCarouselTypeRotary
    • iCarouselTypeInvertedRotary
    • iCarouselTypeCylinder
    • iCarouselTypeInvertedCylinder
    • iCarouselTypeWheel
    • iCarouselTypeInvertedWheel
    • iCarouselTypeCoverFlow
    • iCarouselTypeCoverFlow2
    • iCarouselTypeTimeMachine
    • iCarouselTypeInvertedTimeMachine

    您还可以使用iCarouselTypeCustomcarousel:itemTransformForOffset:baseTransform:委托方法实现自己的定制轮播样式

    注意:iCarouselTypeCoverFlowiCarouselTypeCoverFlow2类型之间的区别非常微妙,但是 for 的逻辑要iCarouselTypeCoverFlow2复杂得多。如果您轻弹转盘,它们基本上是相同的,但是如果您用手指缓慢拖动转盘,则差异应该很明显。iCarouselTypeCoverFlow2旨在尽可能接近地模拟标准 Apple CoverFlow 效果,并且将来可能会为了该目标而进行微妙的更改。

    显示类型可视化示例

    线性

    线性

    旋转式

    旋转式


    倒转

    倒转


    圆筒

    圆筒

    倒置气缸

    倒置气缸

    Cover Flow功能

    Cover Flow功能



    特性

    iCarousel 具有以下属性(注意:对于 Mac OS,在使用属性时将 NSView 替换为 UIView):

    @property (nonatomic, weak) IBOutlet id dataSource;

    一个支持 iCarouselDataSource 协议并可以提供视图来填充轮播的对象。

    @property (nonatomic, weak) IBOutlet id delegate;

    一个支持 iCarouselDelegate 协议并且可以响应轮播事件和布局请求的对象。

    @property (nonatomic, assign) iCarouselType type;

    用于切换轮播显示类型(详见上文)。

    @property (nonatomic, assign) CGFloat perspective;

    用于调整各种 3D 轮播视图的透视缩短效果。应为负值,小于 0 且大于 -0.01。超出此范围的值将产生非常奇怪的结果。默认值为 -1/500 或 -0.005;

    @property (nonatomic, assign) CGSize contentOffset;

    此属性用于调整轮播项目视图相对于轮播中心的偏移。它默认为 CGSizeZero,这意味着轮播项目居中。更改此值会移动轮播项目而不改变其视角,即消失点随轮播项目移动,因此如果您将轮播项目向下移动,则不会看起来好像您在俯视轮播。

    @property (nonatomic, assign) CGSize viewpointOffset;

    此属性用于调整相对于轮播项目的用户视角。它与调整 contentOffset 有相反的效果,即如果您向上移动视点,则轮播似乎向下移动。与 contentOffset 不同,移动视点也会改变相对于旋转木马项目的透视消失点,因此如果您向上移动视点,它会看起来好像您在俯视旋转木马。

    @property (nonatomic, assign) CGFloat decelerationRate;

    旋转木马在轻弹时减速的速率。较高的值意味着较慢的减速。默认值为 0.95。值应在 0.0(释放时旋转木马立即停止)到 1.0(旋转木马无限期地继续而不减速,除非它到达终点)的范围内。

    @property (nonatomic, assign) BOOL bounces;

    设置旋转木马是应该弹过终点并返回,还是停止不动。请注意,这对设计为包装的轮播类型或 carouselShouldWrap 委托方法返回 YES 的类型没有影响。

    @property (nonatomic, assign) CGFloat bounceDistance;

    未包裹的传送带越过末端时反弹的最大距离。这是以 itemWidth 的倍数来衡量的,因此值 1.0 表示轮播将反弹整个项目宽度,值 0.5 表示项目宽度的一半,依此类推。默认值为 1.0;

    @property (nonatomic, assign, getter = isScrollEnabled) BOOL scrollEnabled;

    启用和禁用用户滚动轮播。如果此属性设置为 NO,则仍然可以通过编程方式滚动轮播。

    @property (nonatomic, readonly, getter = isWrapEnabled) BOOL wrapEnabled;

    如果启用包装,则返回 YES,否则返回 NO。此属性是只读的。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 返回一个值iCarouselOptionWrap

    @property (nonatomic, assign, getter = isPagingEnabled) BOOL pagingEnabled;

    启用和禁用分页。启用分页后,轮播将在用户滚动时在每个项目视图处停止,这与 UIScrollView 的 pagingEnabled 属性非常相似。

    @property (nonatomic, readonly) NSInteger numberOfItems;

    轮播中的项目数(只读)。要设置它,请实现numberOfItemsInCarousel:dataSource 方法。请注意,并非所有这些项目视图都会在给定的时间点加载或可见 - 轮播在滚动时按需加载项目视图。

    @property (nonatomic, readonly) NSInteger numberOfPlaceholders;

    要在轮播中显示的占位符视图的数量(只读)。要设置它,请实现numberOfPlaceholdersInCarousel:dataSource 方法。

    @property (nonatomic, readonly) NSInteger numberOfVisibleItems;

    屏幕上同时显示的轮播项目视图的最大数量(只读)。此属性对于性能优化很重要,并且会根据轮播类型和视图框架自动计算。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 iCarouselOptionVisibleItems 返回一个值。

    @property (nonatomic, strong, readonly) NSArray *indexesForVisibleItems;

    一个数组,包含当前加载和在轮播中可见的所有项目视图的索引,包括占位符视图。该数组包含 NSNumber 对象,其整数值与视图的索引匹配。项目视图的索引从零开始并匹配传递给 dataSource 以加载视图的索引,但是任何可见占位符视图的索引要么是负数(小于零)要么大于或等于numberOfItems此数组中占位符视图的索引等同于与 dataSource 一起使用的占位符视图索引。

    @property (nonatomic, strong, readonly) NSArray *visibleItemViews;

    当前显示在轮播中的所有项目视图的数组(只读)。这包括任何可见的占位符视图。此数组中的视图索引与项目索引不匹配,但是这些视图的顺序与 visibleItemIndexes 数组属性的顺序匹配,即您可以通过从visibleItemIndexes 数组(或者,您可以只使用该indexOfItemView:方法,这要容易得多)。

    @property (nonatomic, strong, readonly) UIView *contentView;

    包含轮播项目视图的视图。如果您想将它们与轮播项目散布,您可以向此视图添加子视图。如果您希望视图出现在所有轮播项目的前面或后面,您应该将其直接添加到 iCarousel 视图本身。请注意,当应用程序运行时, contentView 中的视图顺序会经常发生且未记录的更改。添加到 contentView 的任何视图都应将其 userInteractionEnabled 属性设置为 NO 以防止与 iCarousel 的触摸事件处理发生冲突。

    @property (nonatomic, assign) CGFloat scrollOffset;

    这是轮播的当前滚动偏移量,是 itemWidth 的倍数。这个值,四舍五入到最接近的整数,是 currentItemIndex 值。您可以使用此值在轮播移动时定位其他屏幕元素。如果您希望以编程方式将轮播滚动到特定偏移量,也可以设置该值。如果您希望禁用内置手势处理并提供您自己的实现,这可能很有用。

    @property (nonatomic, readonly) CGFloat offsetMultiplier;

    这是用户用手指拖动轮播时使用的偏移乘数。它不影响编程滚动或减速速度。对于大多数轮播类型,这默认为 1.0,但对于 CoverFlow 风格的轮播默认为 2.0,以补偿它们的项目间隔更近的事实,因此必须进一步拖动以移动相同的距离。您不能直接设置此属性,但可以通过实现carouselOffsetMultiplier:委托方法来覆盖默认值

    @property (nonatomic, assign) NSInteger currentItemIndex;

    轮播中当前居中项目的索引。设置此属性等效于scrollToItemAtIndex:animated:将动画参数设置为 NO进行调用

    @property (nonatomic, strong, readonly) UIView *currentItemView;

    轮播中当前居中的项目视图。此视图的索引匹配currentItemIndex

    @property (nonatomic, readonly) CGFloat itemWidth;

    轮播中项目的显示宽度(只读)。这是从使用carousel:viewForItemAtIndex:reusingView:dataSource 方法传递给轮播的第一个视图自动派生的您还可以使用carouselItemWidth:委托方法覆盖此值,这将更改为轮播项目分配的空间(但不会调整项目视图的大小或缩放)。

    @property (nonatomic, assign) BOOL centerItemWhenSelected;

    当设置为 YES 时,点击轮播中除与 currentItemIndex 匹配的项目之外的任何项目都会使其平滑地动画到中心。点击当前选定的项目将不起作用。默认为是。

    @property (nonatomic, assign) CGFloat scrollSpeed;

    这是用户用手指轻弹轮播时的滚动速度倍增器。默认为 1.0。

    @property (nonatomic, readonly) CGFloat toggle;

    此属性用于iCarouselTypeCoverFlow2轮播变换。它是公开的,以便您可以使用carousel:itemTransformForOffset:baseTransform:委托方法实现自己的 CoverFlow2 样式变体

    @property (nonatomic, assign) BOOL stopAtItemBoundary;

    默认情况下,轮播将在轻弹时停在确切的项目边界处。如果将此属性设置为 NO,它将自然停止,然后 - 如果 scrollToItemBoundary 设置为 YES - 向后或向前滚动到最近的边界。

    @property (nonatomic, assign) BOOL scrollToItemBoundary;

    默认情况下,当轮播停止移动时,它会自动滚动到最近的项目边界。如果将此属性设置为 NO,则轮播在停止后将不会滚动并停留在它所在的位置,即使它在当前索引上没有完全对齐。例外情况是,如果 wrapping 被禁用并bounces设置为 YES,那么无论此设置如何,如果轮播结束后停止,轮播将自动滚动回第一个或最后一个项目索引。

    @property (nonatomic, assign, getter = isVertical) BOOL vertical;

    此属性切换轮播是在屏幕上水平显示还是垂直显示。所有内置的轮播类型都适用于两个方向。切换到垂直会更改轮播的布局以及屏幕上滑动检测的方向。请注意,自定义轮播变换不受此属性影响,但滑动手势方向仍会受到影响。

    @property (nonatomic, readonly, getter = isDragging) BOOL dragging;

    如果用户已开始滚动轮播但尚未释放它,则返回 YES。

    @property (nonatomic, readonly, getter = isDecelerating) BOOL decelerating;

    如果用户不再拖动轮播,但它仍在移动,则返回 YES。

    @property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;

    如果当前正在以编程方式滚动轮播,则返回 YES。

    @property (nonatomic, assign) BOOL ignorePerpendicularSwipes;

    如果是,则轮播将忽略与轮播方向垂直的滑动手势。所以对于水平轮播,垂直滑动不会被拦截。这意味着您可以在旋转木马项目视图中拥有一个垂直滚动的 scrollView,它仍然可以正常工作。默认为是。

    @property (nonatomic, assign) BOOL clipsToBounds;

    这实际上不是 iCarousel 的属性,而是继承自 UIView。它包含在此处是因为它是一个经常被遗漏的功能。将此设置为 YES 以防止轮播项目视图溢出其边界。您可以通过勾选“剪辑子视图”选项在界面生成器中设置此属性。默认为否。

    @property (nonatomic, assign) CGFloat autoscroll;

    此属性可用于设置轮播以恒定速度滚动。值为 1.0 将以每秒一项的速度向前滚动轮播。自动滚动值可以为正也可以为负,默认为 0.0(固定)。如果用户与轮播交互,自动滚动将停止,并在他们停止时恢复。

    方法

    iCarousel 类具有以下方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (void)scrollToItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将使轮播在指定的项目上居中,无论是立即还是平滑的动画。对于包裹式轮播,轮播将自动确定要滚动的最短(直接或环绕)距离。如果您需要控制滚动方向,或者想要滚动一圈以上,请改用 scrollByNumberOfItems 方法。

    - (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)scrollDuration;

    此方法允许您控制轮播滚动到指定索引所需的时间。

    - (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration;

    此方法允许您将轮播滚动固定距离,以轮播项目宽度为单位。可以为 itemCount 指定正值或负值,具体取决于您希望滚动的方向。iCarousel 优雅地处理边界问题,因此如果您指定的距离大于轮播中项目的数量,滚动将在到达轮播结束时被限制(如果环绕被禁用)或无缝环绕。

    - (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollToItemAtIndex:,但允许您滚动到小数偏移量。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则调用此方法后,轮播将自动滚动到最近的项目索引。反正。

    - (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollByNumberOfItems:,但允许您滚动项目的小数部分。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则无论如何调用此方法后,轮播都会自动滚动到最近的项目索引。

    - (void)reloadData;

    这将从数据源重新加载所有轮播视图并刷新轮播显示。

    - (UIView *)itemViewAtIndex:(NSInteger)index;

    返回具有指定索引的可见项视图。请注意,索引与轮播中的位置有关,而不是在visibleItemViews数组中的位置,这可能会有所不同。传递负索引或大于或等于的索引numberOfItems以检索占位符视图。该方法仅适用于可见的项目视图,如果指定索引处的视图尚未加载,或者索引超出范围,则返回 nil。

    - (NSInteger)indexOfItemView:(UIView *)view;

    轮播中给定项目视图的索引。适用于项目视图和占位符视图,但是占位符视图索引与数据源使用的索引不匹配,并且可能为负数(indexesForVisibleItems有关更多详细信息,请参阅上面的属性)。此方法仅适用于可见的项目视图,并且将为当前未加载的视图返回 NSNotFound。要获取所有当前加载的视图的列表,请使用该visibleItemViews属性。

    - (NSInteger)indexOfItemViewOrSubview:(UIView *)view

    此方法为您提供传递的视图或包含作为参数传递的视图的视图的项目索引。它的工作方式是从传递的视图开始沿着视图层次结构向上移动,直到找到一个项目视图并在轮播中返回其索引。如果未找到当前加载的项目视图,则返回 NSNotFound。此方法对于处理嵌入在项目视图中的控件上的事件非常有用。这允许您将所有项目控件绑定到视图控制器上的单个操作方法,然后确定触发操作的控件与哪个项目相关。您可以在Controls Demo示例项目中看到此技术的示例。

    - (CGFloat)offsetForItemAtIndex:(NSInteger)index;

    itemWidth以中心位置的倍数返回指定项索引的偏移量这与用于计算视图变换和 alpha 的值相同,可用于根据它们在轮播中的位置自定义项目视图。每当carouselDidScroll:调用委托方法时,每个视图的这个值都会发生变化

    - (UIView *)itemViewAtPoint:(CGPoint)point;

    返回轮播边界内指定点的最前面的项目视图。用于实现您自己的点击检测。

    - (void)removeItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将从轮播中删除一个项目。其余项目将滑过以填补空白。请注意,调用此方法时数据源不会自动更新,因此后续调用 reloadData 将恢复已删除的项目。

    - (void)insertItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这会将一个项目插入到轮播中。新的item会从dataSource中请求,所以在调用这个方法之前要确保新的item已经添加到数据源data中,否则会在carousel中得到重复的item,或者其他怪事。

    - (void)reloadItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    此方法将重新加载指定的项目视图。将从数据源请求新项目。如果动画参数为 YES,它将从旧项目视图交叉淡入淡出到新项目视图,否则将立即交换。

    协议

    iCarousel 通过提供两个协议接口 iCarouselDataSource 和 iCarouselDelegate 来遵循 Apple 的数据驱动视图约定。iCarouselDataSource 协议具有以下必需的方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel;

    返回轮播中的项目(视图)数。

    - (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示在轮播中指定索引处的视图。reusingView参数的工作方式类似于 UIPickerView,其中先前显示在轮播中的视图被传递回要回收的方法。如果这个参数不是 nil,你可以设置它的属性并返回它,而不是创建一个新的视图实例,这会稍微提高性能。与 UITableView 不同,没有用于区分不同轮播视图类型的重用标识符,因此如果您的轮播包含多个不同的视图类型,那么您应该忽略此参数并在每次调用该方法时返回一个新视图。您应该确保每次carousel:viewForItemAtIndex:reusingView: 方法被调用时,它要么返回 reusingView 要么返回一个全新的视图实例,而不是维护自己的可回收视图池,因为为不同的轮播项目索引返回同一视图的多个副本可能会导致轮播显示问题。

    iCarouselDataSource 协议有以下可选方法:

    - (NSUInteger)numberOfPlaceholdersInCarousel:(iCarousel *)carousel;

    返回要在轮播中显示的占位符视图的数量。当轮播中的项目数量太少而无法填充轮播宽度,并且您希望在空白空间中显示某些内容时,将使用占位符视图。它们与轮播一起移动并且行为与任何其他轮播项目一样,但它们不计入 numberOfItems 值,并且不能设置为当前选定的项目。启用换行时,占位符会隐藏。占位符出现在轮播项目的两侧。对于 n 个占位符视图,前 n/2 个项目将出现在项目视图的左侧,接下来的 n/2 个项目将出现在右侧。您可以有奇数个占位符,在这种情况下,轮播将是不对称的。

    - (UIView *)carousel:(iCarousel *)carousel placeholderViewAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示为占位符视图的视图。工作方式与carousel:viewForItemAtIndex:reusingView:占位符 reusingViews 与用于常规轮播的 reusingViews 存储在单独的池中,因此如果您的占位符视图与项目视图不同,这不是问题。

    iCarouselDelegate 协议具有以下可选方法:

    - (void)carouselWillBeginScrollingAnimation:(iCarousel *)carousel;

    每当轮播开始动画滚动时,都会调用此方法。这可以在用户完成滚动轮播后以编程方式或自动触发,因为轮播会重新对齐自身。

    - (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel;

    当轮播结束动画滚动时调用此方法。

    - (void)carouselDidScroll:(iCarousel *)carousel;

    每当滚动轮播时都会调用此方法。无论轮播是通过编程还是通过用户交互滚动,它都会被调用。

    - (void)carouselCurrentItemIndexDidChange:(iCarousel *)carousel;

    每当轮播滚动到足以改变 currentItemIndex 属性时,就会调用此方法。无论项目索引是以编程方式更新还是通过用户交互更新,都会调用它。

    - (void)carouselWillBeginDragging:(iCarousel *)carousel;

    当用户开始拖动轮播时调用此方法。如果用户点击/点击轮播,或者轮播以编程方式滚动,它不会触发。

    - (void)carouselDidEndDragging:(iCarousel *)carousel willDecelerate:(BOOL)decelerate;

    当用户停止拖动轮播时调用此方法。willDecelerate 参数指示转盘是否行得足够快以至于它在停止之前需要减速(即当前索引不一定是它将停止的索引),或者它是否会在它所在的位置停止。请注意,即使 willDecelerate 为 NO,轮播仍会自动滚动,直到它与当前索引完全对齐。如果您需要知道它何时完全停止移动,请使用 carouselDidEndScrollingAnimation 委托方法。

    - (void)carouselWillBeginDecelerating:(iCarousel *)carousel;

    当轮播开始减速时调用此方法。它通常会在 carouselDidEndDragging:willDecelerate: 方法之后立即调用,假设 willDecelerate 为 YES。

    - (void)carouselDidEndDecelerating:(iCarousel *)carousel;

    当轮播完成减速时调用此方法,您可以假设此时的 currentItemIndex 是最终停止值。与以前的版本不同,在大多数情况下,轮播现在将准确地停在最终索引位置。唯一的例外是启用了弹跳的非包裹式转盘,如果最终停止位置超出转盘的末端,则转盘将自动滚动,直到它与结束索引完全对齐。为了向后兼容,轮播将始终scrollToItemAtIndex:animated:在完成减速后调用如果您需要确定轮播何时完全停止移动,请使用carouselDidEndScrollingAnimation委托方法。

    - (CGFloat)carouselItemWidth:(iCarousel *)carousel;

    返回轮播中每个项目的宽度 - 即每个项目视图的间距。如果未实现该方法,则默认为carousel:viewForItemAtIndex:reusingView:dataSource 方法返回的第一个项目视图的宽度如果从返回的视图carousel:viewForItemAtIndex:reusingView:不正确(例如,如果视图大小不同,或者在其背景图像中包含影响其大小的投影或外部发光),则此方法应仅用于裁剪或填充项目视图- 如果您只是想要将视图隔开一点,那么最好使用该iCarouselOptionSpacing值。

    - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform;

    此方法可用于为每个轮播视图提供自定义转换。offset 参数是视图与旋转木马中间的距离。当前居中的项目视图的偏移量为 0.0,右侧的偏移值为 1.0,左侧的偏移值为 -1.0,依此类推。要实现线性轮播样式,您只需将偏移值乘以项目宽度并将其用作变换的 x 值。仅当轮播类型为 iCarouselTypeCustom 时才会调用此方法。

    - (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value;

    该方法用于自定义标准轮播类型的参数。通过实施此方法,您可以调整选项,例如圆形转盘中显示的项目数量,或coverflow 转盘中的倾斜量,以及转盘是否应环绕以及是否应在末端淡出等. 对于任何您不想调整的选项,只需返回默认值即可。这些选项的含义在下面的iCarouselOption 值下列出检查选项演示以获取使用此方法的高级示例。

    - (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。如果用户点击当前选定视图中的控件(即作为 UIControl 子类的任何视图),则不会触发此方法。

    - (BOOL)carousel:(iCarousel *)carousel shouldSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。方法的目的是让您有机会忽略轮播上的点击。如果你从方法中返回 YES,或者没有实现它,tap 将正常处理并carousel:didSelectItemAtIndex:调用方法。如果您返回 NO,轮播将忽略点击并继续向上传播视图层次结构。这是防止轮播拦截打算由另一个视图处理的点击事件的好方法。


    检测项目视图上的点击

    在 iOS 上的 iCarousel 中检测点击视图有两种基本方法。第一种方法是简单地使用carousel:didSelectItemAtIndex:委托方法,每次点击项目时都会触发方法。如果您只对点击当前居中的项目感兴趣,您可以将该currentItemIndex属性与此方法的 index 参数进行比较

    或者,如果您想要更多控制,您可以提供 UIButton 或 UIControl 作为项目视图并自己处理触摸交互。有关如何完成此操作的示例,请参阅按钮演示示例项目(不适用于 Mac OS;见下文)。

    您还可以在您的项目视图中嵌套 UIControls,这些将按预期接收触摸(请参阅Controls Demo示例项目以获取示例)。

    如果您希望检测其他类型的交互,例如滑动、双击或长按,最简单的方法是将 UIGestureRecognizer 附加到您的项目视图或其子视图,然后再将其传递给轮播。

    请注意,除了当前选定的项目视图之外,任何项目视图上的点击和手势都将被忽略,除非您将该centerItemWhenSelected属性设置为 NO。

    在 Mac OS 上,目前没有简单的方法可以在 iCarousel 项目视图中嵌入控件。您不能只在项目视图中或在项目视图中提供 NSButton,因为应用于项目视图的转换意味着命中检测无法正常工作。我正在研究可能的解决方案(如果您知道解决此问题的好方法,请与我们联系,或在 github 上 fork 项目)。

    demo及常见问题:https://github.com/nicklockwood/iCarousel

    源码下载:iCarousel-master.zip


    收起阅读 »

    iOS 应用分享平台fir使用遇到的一些坑

    前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分...
    继续阅读 »

    前几天项目要通过fir(http://fir.im 一个免费的应用发布平台)用作给测试团队装机。于是点开它,直接找到帮助中心开始一步步照做,中间碰到不少坑,(还有万恶的苹果官网登陆不上!!!)网上的资料也不是太多,白白浪费了许多时间(害我加班😠),所以记下来分享出来给大家,希望能对你有所帮助。

    首先要确定你们使用平台的需求,我这里有蒲公英(fir同类型网站)对于应用分享需求的介绍


    如果只是小范围的几个人来安装,使用Ad-hoc方式,去一个个添加UDID就好了,好处是使用你自己的免费证书也可以申请。
    如果是想做线下推广,没办法及时获取添加目标UDID的话,最好还是要使用In-house方式,不过装机数量苹果好像还是有一定限制,这个具体政策不太清楚。

    我的目的是给测试团队装机,所以选择Ad-hoc方式做。

    简化下来一共需要三大步
    1 . 在你的Apple Developer 页面的Devices中添加目标的(于我就是“测试团队”)苹果手机UDID。(关于UDID的获取看这里 http://fir.im/udid 这个网址使用苹果手机的Safari浏览器访问)


    在这里点击“+”输入用户的UDID(name是你自己定的,建议起个和此UDID手机拥有者相关的名字,后面会用到),点击下方的注册,会跳转确认注册页面


    确认账号无误后可以点击下方的确定,目标UDID就乖乖加入到你的Devices列表中了😊。

    注意:这里就会有一个坑,我导入的第一个UDID出现这种情况


    你会发现这个缺少了Model:这一项,目前我没有发现是因为什么(隐约赶脚是因为录入这个UDID时,网络或者苹果官网之类的问题😊)。这种账号是无法添加进描述文件的,添加进去也无法识别和使用。

    还有一种情况是你添加了目标UDID,在Devices列表中找不到,再次注册该UDID又会提示它不是有效的,多次尝试无果也只好作罢。

    2 . 在Distribution中添加一个用于测试的描述文件,并在此步骤中添加目标手机到描述文件中。


    在此点击“+”,添加一个新的描述文件。


    选择你需要的方式,我的是Ad-hoc


    然后是选择自己项目


    选择开发者(或团队)


    选择你要添加的目标UDID(此时使用的是你创建Device时的名字)


    给你的描述文件命名(项目中添加Provisioning Profile时使用这个名字)

    creat之后点击下载,描述文件就会下载到电脑。

    这里倒是没有什么坑,就是苹果官网如果访问起来困难,部分页面会不显示你已有的一些资料,会提示要你新建一个项目。如果你确定自己有项目的话,刷新一下就好了。

    3 . 将描述文件添加到Xcode,然后在项目中选择相应的打包选项,生成.ipa文件。然后大功告成,将其上传到fir平台后点击“预览”会自动生成一个带有二维码的网址。(需要使用iphone自带的safari浏览器访问该链接)

    现在可以关掉万恶的苹果官网,来到桌面上,建议先彻底关闭Xcode,然后双击一下你下载下来的描述文件,Xcode会自动打开,此时描述文件就已经添加好了。


    在 Xcode 中点击project图标,在info这个tab下找到configuration设置,里面默认的是debug和release。点击+,选择Duplicate the “Release configuration”,给生成的新东西起个名字,推荐使用ad hoc distribution


    点击targets图标,在build settings这个tab下,找到code signing部分。将Code Signing Identity中的ad hoc distribution证书设置为刚刚导入到 Xcode 中对应测试应用的证书。注意不要改动Debug和Release中的证书。
    在下方的Provisioning Profile中选择你下载下来的描述文件。
    保证target中info这个tab下的bundle indentifier里面有预设值,其必须和provision portal输入匹配。这个很重要,否则将来会出错。


    在Xcode左上角run按钮右侧有一个下拉菜单,选择device或者simulator,点击菜单下方的edit schema。保证Archive中Build Configuration中的值是ad hoc distribution


    配置工作到此结束。点击Product中的Archive,程序开始编译,编译完成后弹出设置框,点选"Export" 然后选"Save for Ad Hoc Develoyment"

    按操作提示就会生成一个.ipa文件。此.ipa可以被安装到之前设置的测试应用设备中。

    然后创建一个fir账号,在其上发布就好了。

    本文借鉴于http://blog.csdn.net/yuanbohx/article/details/9213879
    该博客6楼指出其在文章中的错误,实测6楼所说是正确的。

    链接:https://www.jianshu.com/p/cbda1e434add

    收起阅读 »

    超强的游戏模拟器, 做游戏开发必备 - OpenEmu

    OpenEmuOpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 S...
    继续阅读 »

    OpenEmu

    alt text


    OpenEmu 是一个开源项目,其目的是将 macOS 游戏模拟带入一流公民的领域。该项目利用现代 macOS 技术,例如 Cocoa、Core Animation with Quartz Composer 和其他第三方库。一个第三方库示例是 Sparkle,它用于自动更新。OpenEmu 使用模块化架构,允许使用游戏引擎插件,允许 OpenEmu 支持大量不同的仿真引擎和后端,同时保留熟悉的 macOS 原生前端。

    目前 OpenEmu 可以加载以下游戏引擎作为插件:



    最低要求

    macOS 10.14


    demo及常见问题:https://github.com/OpenEmu/OpenEmu

    源码下载:OpenEmu-master.zip





    收起阅读 »

    Apple 的xcodebuild的扩展!

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。特征xctool是替代品,xcodebuild test它增加了一些额外的功能:更快的并行测试运行。xctool可以选择并行运行所有测...
    继续阅读 »

    xctool是 Apple 的xcodebuild的扩展,可以更轻松地测试 iOS 和 Mac 产品。它对持续集成特别有帮助。

    特征

    xctool是替代品,xcodebuild test它增加了一些额外的功能:

    • 更快的并行测试运行。

      xctool可以选择并行运行所有测试包,从而显着加快测试运行速度。在 Facebook,通过并行运行,我们看到了 2 倍和 3 倍的加速。

      使用-parallelize带有run-teststest选项来启用。有关详细信息,请参阅并行化测试运行

    • 测试结果的结构化输出。

      xctool将所有测试结果捕获为结构化 JSON 对象。如果您正在构建一个持续集成系统,这意味着您不再需要正则表达式解析xcodebuild输出。

      尝试使用Reporters之一自定义输出或使用该-reporter json-stream选项获取完整的事件流

    • 人性化的 ANSI 颜色输出。

      xcodebuild非常冗长,为每个源文件打印完整的编译命令和输出。默认情况下,xctool仅在出现问题时才详细说明,从而更容易确定问题所在。

    • 用Objective-C编写。

      xctool是用 Objective-C 编写的。Mac OS X 和 iOS 开发人员可以轻松提交新功能并修复他们可能遇到的任何错误,而无需学习新语言。我们非常欢迎拉取请求!

    注意:不推荐使用 xctool 构建项目,并且不会更新以支持 Xcode 的未来版本。我们建议移动到 xcodebuild(使用xcpretty)来满足简单的需求,或者使用xcbuild来满足更多的需求。xctool 将继续支持测试(见上文)。

    要求

    • Xcode 7 或更高版本
    • 您需要安装 Xcode 的命令行工具。从 Xcode,通过Xcode → Preferences → Downloads安装

    安装

    brew install xctool

    xctool 的命令和选项主要是 xcodebuild 的超集。在大多数情况下,您只需将xcodebuildxctool交换,事情就会按预期运行,但输出更具吸引力。

    您始终可以通过以下方式获得帮助和完整的选项列表:

    path/to/xctool.sh -help


    在运行测试之前,您需要构建它们。您可以使用xcodebuild、 xcbuildBuck来做到这一点。

    例如:

    xcodebuild \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-for-testing


    如果您使用 Xcode 7 进行构建,您可以继续使用 xctool 使用 build-tests 构建测试,或者仅使用测试操作来运行测试。

    例如:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    build-tests

    并行化测试运行

    xctool可以选择并行运行单元测试,从而更好地利用其他空闲的 CPU 内核。在 Facebook,通过并行化我们的测试运行,我们已经看到了 2 倍和 3 倍的收益。

    要允许测试包同时运行,请使用以下-parallelize 选项:

    path/to/xctool.sh \
    -workspace YourWorkspace.xcworkspace \
    -scheme YourScheme \
    run-tests -parallelize


    常见问题及demo下载:https://github.com/facebookarchive/xctool

    源码下载:xctool-master.zip



    收起阅读 »

    DKNightVersion 的实现 --- 如何为 iOS 应用添加夜间模式

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。在正式介绍目前版本的实现之前,我会先简单介绍...
    继续阅读 »

    从开始写 DKNightVersion 这个框架到现在已经将近一年了,目前整个框架的设计也趋于稳定。

    其实夜间模式的实现就是相当于多主题加颜色管理。而最新版本的 DKNightVersion 已经很好的解决了这个问题。

    在正式介绍目前版本的实现之前,我会先简单介绍一下 1.0 时代的 DKNightVersion 的实现,为各位读者带来一些新的思路,也确实想梳理一下这个框架是如何演变的。

    我们会以对 backgroundColor 为例说明整个框架的工作原理。

    方法调剂的版本

    如何在不改变原有的架构,甚至不改变原有的代码的基础上,为应用优雅地添加夜间模式成为很多开发者不得不面对的问题。这也是 1.0 时代的 DKNightVersion 想要实现的目标。

    其核心思路就是使用方法调剂修改 backgroundColor 的存取方法。

    使用 nightBackgroundColor

    在思考之后,我想到,想要在不改动原有代码的基础上实现夜间模式只能通过在分类中添加 nightBackgroundColor 属性,并且使用方法调剂改变 backgroundColor 的 setter 方法。

    在当前主题为 DKThemeVersionNormal 时,将颜色保存至 normalBackgroundColor 中,然后再调用原 backgroundColor 的 setter 方法,更新视图的颜色。

    DKNightVersionManager

    这里只解决了颜色设置的问题,下面会说明,如果在主题改变时,实时更新颜色,而不用重新进入当前页面。

    整个 DKNightVersion 都是由一个 DKNightVersionManager 的单例来管理的,而它的主要工作就是负责改变应用的主题、并在主题改变时通知其它视图更新颜色:

    - (void)changeColor:(id <DKNightVersionChangeColorProtocol>)object {
    if ([object respondsToSelector:@selector(changeColor)]) {
    [object changeColor];
    }
    if ([object respondsToSelector:@selector(subviews)]) {
    if (![object subviews]) {
    // Basic case, do nothing.
    return;
    } else {
    for (id subview in [object subviews]) {
    // recursive darken all the subviews of current view.
    [self changeColor:subview];
    if ([subview respondsToSelector:@selector(changeColor)]) {
    [subview changeColor];
    }
    }
    }
    }
    }

    如果主题更新,那么就会递归地调用 changeColor 方法,刷新全部的视图颜色,而这个方法的实现比较简单:

    - (void)changeColor {
    if ([DKNightVersionManager currentThemeVersion] == DKThemeVersionNormal) {
    self.backgroundColor = self.normalBackgroundColor;
    } else {
    self.backgroundColor = self.nightBackgroundColor;
    }
    }

    上面就是整个框架在 1.0 版本时的实现思路。不过这个版本的 DKNightVersion 在实际应用中会有比较多的问题:

    1、在高速滚动的 scrollView 上面来回切换夜间模式,会出现颜色错乱的问题
    2、由于对 backgroundColor 属性进行不合适的方法调剂,其行为无法预测,比如:在设置颜色后,再取出,不一定与设置时传入的颜色相同
    3、无法适配第三方 UI 控件

    使用色表的版本

    为了解决 1.0 中的各种问题,我决定在 2.0 版本中放弃对 nightBackgroundColor 的使用,并且重新设计底层的实现,转而使用更为稳定、安全的方法实现夜间模式,先看一下效果图:

    <em>新的实现不仅能够支持夜间模式,而且能够支持多主题。</em>

    DKColorPicker

    与上一个版本实现上的不同,在 2.0 中删除了全部的 nightBackgroundColor,使用一个名为 dk_backgroundColorPicker 的属性取代它。

    @property (nonatomic, copy) DKColorPicker dk_backgroundColorPicker;

    这个属性其实就是一个 block,它接收参数 DKThemeVersion *themeVersion,但是会返回一个 UIColor *:

    在第一次传入 picker 或者每次主题改变时,都会将当前主题 DKThemeVersion 传入 picker 并执行,然后,将得到的 UIColor 赋值给对应的属性 backgroundColor 更新视图颜色。

    typedef UIColor *(^DKColorPicker)(DKThemeVersion *themeVersion);

    比如下面使用 DKColorPickerWithRGB 创建一个临时的 DKColorPicker:

    1、在 DKThemeVersionNormal 时返回 0xffffff
    2、在 DKThemeVersionNight 时返回 0x343434
    3、在自定义的主题下返回 0xfafafa (这里的顺序与色表中主题的顺序有关)

    cell.dk_backgroundColorPicker = DKColorPickerWithRGB(0xffffff, 0x343434, 0xfafafa);

    同时,每一个对象还持有一个 pickers 数组,来存储自己的全部 DKColorPicker:

    @interface NSObject ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    在第一次使用这个属性时,当前对象注册为 DKNightVersionThemeChangingNotificaiton 通知的观察者。

    在每次收到通知时,都会调用 night_update 方法,将当前主题传入 DKColorPicker,并再次执行,并将结果传入对应的属性 [self performSelector:sel withObject:result]。

    - (void)night_updateColor {
    [self.pickers enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull selector, DKColorPicker _Nonnull picker, BOOL * _Nonnull stop) {
    SEL sel = NSSelectorFromString(selector);
    id result = picker(self.dk_manager.themeVersion);
    [UIView animateWithDuration:DKNightVersionAnimationDuration
    animations:^{
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:sel withObject:result];
    #pragma clang diagnostic pop
    }];
    }];
    }

    也就是说,在每次改变主题的时候,都会发出通知。

    DKColorTable

    虽然我们在上面临时创建了一些 DKColorPicker。不过在 DKNightVersion 中,我更推荐使用色表,来减少相同的 DKColorPicker 的创建,并且能够更好地管理整个应用中的颜色:

    NORMAL   NIGHT    RED
    #ffffff #343434 #fafafa BG
    #aaaaaa #313131 #aaaaaa SEP
    #0000ff #ffffff #fa0000 TINT
    #000000 #ffffff #000000 TEXT
    #ffffff #444444 #ffffff BAR

    上面就是默认色表文件 DKColorTable.txt 中的内容,其中,第一行表示主题,NORMAL 主题必须存在,而且必须为第一列,而最右面的 BG、SEP 就是对应 DKColorPicker 的 key。

    self.tableView.dk_backgroundColorPicker =  DKColorPickerWithKey(BG);

    在使用时,上面的代码就相当于返回了一个在 NORMAL 时返回 #ffffff、NIGHT 时返回 #343434 以及 RED 时返回 #fafafa 的 DKColorPicker。

    pickerify

    虽然说,我们使用色表以及 DKColorPicker 解决了,但是,到目前为止我们还没有解决第三方框架的问题。

    比如我们使用了某个第三方框架,或者自己添加了某个 color 属性,比如说:

    @interface DKView ()

    @property (nonatomic, strong) UIColor *weirdColor;

    @end

    weirdColor 并没有对应的 DKColorPicker,但是,我们可以通过 pickerify 在想要使用 dk_weirdColorPicker 的地方生成这个对应的 picker:

    @pickerify(DKView, weirdColor);

    然后,我们就可以使用 dk_weirdColorPicker 属性了:

    view.dk_weirdColorPicker = DKColorPickerWithKey(BG);

    pickerify 其实是一个宏:

    #define pickerify(KLASS, PROPERTY) interface \
    KLASS (Night) \
    @property (nonatomic, copy, setter = dk_set ## PROPERTY ## Picker:) DKColorPicker dk_ ## PROPERTY ## Picker; \
    @end \
    @interface \
    KLASS () \
    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers; \
    @end \
    @implementation \
    KLASS (Night) \
    - (DKColorPicker)dk_ ## PROPERTY ## Picker { \
    return objc_getAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker)); \
    } \
    - (void)dk_set ## PROPERTY ## Picker:(DKColorPicker)picker { \
    objc_setAssociatedObject(self, @selector(dk_ ## PROPERTY ## Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC); \
    [self setValue:picker(self.dk_manager.themeVersion) forKeyPath:@keypath(self, PROPERTY)];\
    [self.pickers setValue:[picker copy] forKey:_DKSetterWithPROPERTYerty(@#PROPERTY)]; \
    } \
    @end

    这个宏根据传入的类和属性名,为我们生成了对应 picker 的存取方法,它也可以说是一种元编程的手段。

    这里生成的 setter 方法不是标准意义上的驼峰命名法 dk_setweirdColorPicker:,因为我不知道怎么才能让大写首字母之后的属性添加到这里(如果各位读者有解决方案,欢迎提 PR 或者 issue)。

    嵌入式 Ruby
    由于框架中很多的代码,都是重复的,所以在这里使用了嵌入式 Ruby 模板来生成对应的文件 color.m.irb:

    //
    // <%= klass.name %>+Night.m
    // <%= klass.name %>+Night
    //
    // Copyright (c) 2015 Draveness. All rights reserved.
    //
    // These files are generated by ruby script, if you want to modify code
    // in this file, you are supposed to update the ruby code, run it and
    // test it. And finally open a pull request.

    #import "<%= klass.name %>+Night.h"
    #import "DKNightVersionManager.h"
    #import <objc/runtime.h>

    @interface <%= klass.name %> ()

    @property (nonatomic, strong) NSMutableDictionary<NSString *, DKColorPicker> *pickers;

    @end

    @implementation <%= klass.name %> (Night)

    <% klass.properties.each do |property| %><%= """
    - (DKColorPicker)dk_#{property.name}Picker {
    return objc_getAssociatedObject(self, @selector(dk_#{property.name}Picker));
    }

    - (void)dk_set#{property.cap_name}Picker:(DKColorPicker)picker {
    objc_setAssociatedObject(self, @selector(dk_#{property.name}Picker), picker, OBJC_ASSOCIATION_COPY_NONATOMIC);
    self.#{property.name} = picker(self.dk_manager.themeVersion);
    [self.pickers setValue:[picker copy] forKey:@\"#{property.setter}\"];
    }
    """ %><% end %>

    @end

    这部分的实现并不在这篇文章的讨论范围之内,如果,对这部分看兴趣,可以看一下仓库中的 generator 文件夹,其中包含了代码生成器的全部代码。

    小结

    如果你对 DKNightVersion 的使用有兴趣,可以查看仓库的 README 文件,有人会说不要在项目中 ObjC runtime,我个人觉得是没有问题,AFNetworking、 BlocksKit 也使用方法调剂来改变原有方法的实现,不能因为它强大就不使用它;正相反,有时候,使用 runtime 才能优雅地解决问题。

    Git仓库地址

    转自:https://draveness.me/night/

    收起阅读 »

    iOS-单元测试汇总

    前言:对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。我看...
    继续阅读 »

    前言:
    对于单元测试来说,我想大部分同行,在项目中,很少会用到,也有一大部分,知道单元测试这个东西,但是确切的说没有尝试过,也不知道怎么回事,我想写篇文章总结一下,了解一下单元测试。我也志在学习一下单元测试。如果触碰到什么误区,希望大家多多提醒,帮助,谢谢。

    我看了几篇单元测试的文章,其中写到单元测试多数用于:

    1.调试接口是否正常使用。比如要测试一个网络接口,通常每次都要重新启动,经过繁复的操作之后,才能测试到网络接口。要是用单元测试,就可以直接测试那个方法,相对方便很多。

    2.比如由于修改较多,想测试分享功能是否正常,(而不是重新启动程序,进入到分享界面,点击分享,填写分享内容。),在单元测试通过了,直接用到相应的地方。

    3.自动发布、自动测试(特别在一些大的项目,以防止程序被误改或引起新的问题)。
    4.用户注册/登陆等

    了解一下单元测试:
    单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

    通常来说,程序员每修改一次代码就会修改某个单元,那我们就可以对这个单元做修改的验证(单元测试),在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书(产品需求)要求的工作目标,而且没有程序错误。

    每个理想的测试案例独立于其它case,测试时需隔离模块。单元测试通常由软件开发人员编写,用于确保所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是手动的,或是构建自动化的一部分。

    单元测试允许程序员在未来重构代码,且确保模块依然工作正确。这个过程是为所有方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。

    可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。在连续的单元测试环境,通过其固有的持续维护工作,单元测试可以延续用于准确反映当任何变更发生时可执行程序和代码的表现。借助于上述开发实践和单元测试的覆盖,可以总是维持准确性。

    了解一下单元测试目的:
    保证代码的质量 (帮助你编写高质量代码、减少bu)
    代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,尤其包含了许多单元分支的情况下,单元测试可以保证代码的行为和结果与我们的预期和需求一致。在测试某段代码的行为是否和你的期望一致时,你需要确认,在任何情况下,这段代码是否都和你的期望一致,譬如参数可能为空,可能的异步操作等。

    有一部分bug的原因是开发人员在编写工作代码的时候没有考虑到某些case或者边际条件。造成这种问题的原因很多,其中很重要的一个原因是我们对工作代码所要完成的功能思考不足,而编写单元测试,特别是先写单元测试再写工作代码就可以帮助开发人员思考编写的代码到底要实现哪些功能。例如实现一个简单的用户注册功能的业务类方法,用单元测试再写工作代码的方式来工作的话开发人员就会先考虑各种场景相关,例如正常注册、用户名重复、没有满足必要的填写内容......等等,之后就会编写相关的测试用例。编写单元测试代码的过程就是促使开发人员思考工作代码实现内容和逻辑的过程,之后实现工作代码的时候,开发人员思路会更清晰,实现代码的质量也会有相应的提升。

    保证代码的可维护性 (提升代码的反馈速度,减少重复工作,保证你最后的代码修改不会破坏之前代码的功能)
    保证原有单元测试正确的情况下,无论如何修改单元内部代码,测试的结果应该是正确的,且修改后不会影响到其他的模块。

    开发人员实现某个功能或者修补了某个bug,如果有相应的单元测试支持的话,开发人员可以马上通过运行单元测试来验证之前完成的代码是否正确,而不需要反复通过编译运行simulator、等待应用启动、通过输入数据等繁琐的步骤来验证所完成的功能。用单元测试代码来验证代码和通过发布应用以人工的方式来验证代码这两者的效率差很多,所以单元测试其实还能节约人力成本。

    项目越做越大,代码越来越多,特别涉及到一些公用接口之类的代码或是底层的基础库,谁也不敢保证这次修改的代码不会破坏之前的功能,所以与此相关的需求会被搁置或推迟,由于不敢改进代码,代码也变得越来越难以维护,质量也越来越差。而单元测试就是解决这种问题的很好方法(不敢说最好的)。由于代码的历史功能都有相应的单元测试保证,修改了某些代码以后,通过运行相关的单元测试就可以验证出新调整的功能是否有影响到之前的功能。当然要实现到这种程度需要很大的付出,不但要能够达到比较高的测试覆盖率,而且单元测试代码的编写质量也要有保证。

    保证代码的可扩展性
    为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行,说明代码的依赖性很高。

    了解一下单元测试的本质:
    是一种验证行为
    单元测试在开发前期检验了代码逻辑的正确性,开发后期,无论是修改代码内部抑或重构,测试的结果为这一切提供了可量化的保障。

    是一种设计行为
    为了可进行单元测试,尤其是先写单元测试(TDD),我们将从调用者思考,从接口上思考,我们必须把程序单元设计成接口功能划分清晰的,易于测试的,且与外部模块耦合性尽可能小。

    是一种快速回归的方式
    在原代码基础上开发及修改功能时,单元测试是一种快捷,可靠的回归。

    除了那些大拿们编写的代码,我相信很多易于维护、设计良好的代码都是通过不断的重构才得到的。虽然说单元测试本身不能直接改进生产代码的质量,但它为生产代码提供了“安全网”,让开发人员可以勇敢地改进代码,从而让代码的clean和beautiful不再是梦想。

    是程序优良的文档
    从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。

    由于给代码写很多单元测试,相当于给代码加上了规格说明书,开发人员通过读单元测试代码也能够帮助开发人员理解现有代码。很有Open Source的项目(如,AFNetworking, FMDB,喵神的VVDoucment等)都有相当量的单元测试代码,通过读这些测试代码会有助于理解生产源代码。

    两种测试思想
      测试驱动开发(Test-driven development,TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。

    行为驱动开发(Behavior-driven development,BDD)是一种敏捷软件开发的技术,BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了 测试驱动开发方法(TDD)。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。

    在iOS单元测试框架中,kiwi是BDD的代表。

    介绍
    OCUnit(即用XCTest进行测试)其实就是苹果自带的测试框架。

    GHUnit是一个可视化的测试框架。
    有了它,你可以点击APP来决定测试哪个方法,并且可以点击查看测试结果等。

    OCMock就是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法A,方法A里面调用到了方法B,而且方法B是有参数传入,但又不是方法A所提供。这时候,你可以使用OCMock来模拟方法B返回的值。(在不影响测试的情况下,就可以这样去模拟。)除了这些,在没有网络的情况下,也可以通过OCMock模拟返回的数据。

    UITests就是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UITests就可以帮助解决这个问题了。

    案例 1

    简单的单元测试
    1-1 创建一个新的项目


    1-2点开测试文件,进入到这个类

    setUp       :每个测试方法调用前执行
    tearDown :每个测试方法调用后执行
    testExample :是测试方法,和我们新建的没有差别。
    测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法
    测试方法的执行顺序: 字典序排序。
    快捷键:Command + U进行单元测试,这个快捷键是全部测试。


    1-3在testExample方法中输入如下:

    NSLog(@"自定义测试testExample");
    int a= 3;
    XCTAssertTrue(a == 0,"a 不能等于 0");


    备注:红色的叉子:代表测试未通过。绿色叉子:代表测试通过。

    案例 2

    iOS-Main - 单元测试 &基本体验

    案例 3
    进行网络请求的测试
    使用CocoaPods安装AFNetworking和STAlertView(CocoaPods安装和使用教程 )
    Pofile:

    platform :ios, '7.0'
    target 'UnitTestDemoTests' do
    pod 'AFNetworking', '~> 2.5.0'
    pod 'STAlertView', '~> 1.0.0'
    end
    target 'UnitTestDemoTestsTests' do
    pod 'AFNetworking', '~> 2.5.0'
    pod 'STAlertView', '~> 1.0.0'
    end

    iOS9的http安全问题:现在进行异步请求的网络测试,由于测试方法主线程执行完就会结束,所以需要设置一下,否则没法查看异步返回结果。
    也可以在方法结束前设置等待,调回回来的时候再让它继续执行。(另一种异步函数的单元测试)定义宏如下:

    //waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。
    #define WAIT do {\
    [self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\
    [self waitForExpectationsWithTimeout:30 handler:nil];\
    } while (0);

    #define NOTIFY \
    [[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil];

    增加测试方法:

    -(void)testRequest{
    // 1.获得请求管理者
    AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
    mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil];

    // 2.发送GET请求
    [mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"responseObject:%@",responseObject);
    XCTAssertNotNil(responseObject, @"返回出错");
    NOTIFY //继续执行
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"error:%@",error);
    XCTAssertNil(error, @"请求出错");
    NOTIFY //继续执行
    }];
    WAIT //暂停
    }

    有时候我们想测试一下整个流程是否可以跑通,比如获取验证码、登录、上传头像,查询个人资料。其实只要输入验证码就可以完成整个测试。这时候就需要用到输入框了,以便程序继续执行。使用了一个第三方的弹出输入框STAlertView,前面已经设置。
    STAlertView的使用方法:

    - (void)testAlertView
    {

    self.stAlertView = [[STAlertView alloc]initWithTitle:@"验证码" message:nil textFieldHint:@"请输入手机验证码" textFieldValue:nil cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancelButtonBlock:^{
    //点击取消返回后执行
    [self testAlertViewCancel];
    NOTIFY //继续执行
    } otherButtonBlock:^(NSString *b) {
    //点击确定后执行
    [self alertViewComfirm:b];
    NOTIFY //继续执行
    }];

    [self.stAlertView show];
    WAIT //设置等待时间
    }

    案例 4
    测试的执行顺序


    通过上述测试得出结论:
    可以看到无论我们怎样调换test方法的书写顺序,其测试顺序都是不变的。
    目前初步的结论:测试方法执行的顺序与方法名中test后面的字符大小有关,小者优先,例如testA,testB1,testB2三个方法相继执行。
    案例 5
    Xcode集成了对单元测试的支持,XCode4.x集成的是OCUnit,到了XCode5.x时代就升级为了XCTest,XCode7.x时代XCtest还可以进行UI测试。下面我们简单介绍下XCTest的使用。

    在xcode新建项目中,默认会建一个单元测试的target,并建立一个继承于XCTestCase的测试用例类


     本例实现了一个个税计算方法,在测试用例中测试输入后输出是否符合结果。
    创建一个名为ASRevenueBL的 .h .m文件,如下面所示:


    ASRevenueBL.h

    #import <Foundation/Foundation.h>
    @interface ASRevenueBL : NSObject
    - (double)calculate:(double)revenue;
    @end

    ASRevenueBL.m

    import "ASRevenueBL.h"

    #define baseNum 3500.0 // 起征点

    @implementation ASRevenueBL

    /*
    * method:传入收入计算税值
    * revenue:收入
    */
    - (double)calculate:(double)revenue
    {
    double tax = 0.0; // 税
    // 应纳税所得额 = 工资收入金额 - 各项社会保险费 - 起征点(3500元)
    // 应纳税额 = 应纳税所得额 x 税率 - 速算扣除数
    double dbTaxRevenue = revenue - baseNum;
    if(dbTaxRevenue <= 1500){
    tax = dbTaxRevenue * 0.03;
    } else if (dbTaxRevenue > 1500 && dbTaxRevenue <= 4500){
    tax = dbTaxRevenue *0.1 -105;
    } else if (dbTaxRevenue > 4500 && dbTaxRevenue <= 9000){
    tax = dbTaxRevenue * 0.2 - 555;
    }else if (dbTaxRevenue > 9000 && dbTaxRevenue <= 35000) {
    tax = dbTaxRevenue * 0.25 - 1005;
    } else if (dbTaxRevenue > 35000 && dbTaxRevenue <= 55000) {
    tax = dbTaxRevenue * 0.3 - 2755;
    } else if (dbTaxRevenue > 55000 && dbTaxRevenue <= 80000) {
    tax = dbTaxRevenue * 0.35 - 5505;
    } else if (dbTaxRevenue > 80000) {
    tax = dbTaxRevenue * 0.45 - 13505;
    }
    return tax;
    }

    导入测试方法所在的类的头文件,并创建一个类,在测试方法调用前,初始化类对象,测试完毕后,将对象置nil,其方法测试如下方测试代码:

    #import <XCTest/XCTest.h>
    #import "ASRevenueBL.h"

    @interface UnitTestsTwoTests : XCTestCase
    @property (nonatomic, strong) ASRevenueBL *revenueBL;
    @end

    @implementation UnitTestsTwoTests

    - (void)setUp {
    [super setUp];

    self.revenueBL = [[ASRevenueBL alloc] init];
    }

    - (void)tearDown {
    self.revenueBL = nil;
    [super tearDown];
    }

    - (void)testLevel1
    {
    double revenue = 5000;
    double tax = [self.revenueBL calculate:revenue];
    XCTAssertEqual(tax, 45.0,@"测试案例1失败");
    XCTAssertTrue(tax == 45.0);
    }

    - (void)testLevel2 {
    XCTestExpectation *exp = [self expectationWithDescription:@"超时"];
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{
    double revenue = 1500;
    double tax = [self.revenueBL calculate:revenue];
    sleep(1);
    NSLog(@"%f",tax);
    XCTAssertEqual(tax, 45, @"用例2测试失败");
    [exp fulfill]; // exp结束
    }];

    [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
    if (error) {
    NSLog(@"Timeout Error: %@", error);
    }
    }];
    }


    - (void)testExample {

    }

    - (void)testPerformanceExample {

    [self measureBlock:^{
    for (int a = 0; a<10; a+=a) {
    NSLog(@"%zd", a);
    }
    }];

    }
    @end


    testLevel1通过revenueBL计算出来的tax与预期相同,测试通过;testLevel2通过revenueBL计算出来的tax与预期不同,测试不通过,反映出了程序一些逻辑漏洞;testPerformanceExample中的平均执行时间比基准值低,测试通过。

    案例 6 命令行测试
    在命令行中也可以启动测试,便于持续集成。

    Assuner$ cd Desktop/
    Desktop Assuner$ cd ASUnitTestFirstDemo/
    ASUnitTestFirstDemo Assuner$ xcodebuild test -project ASUnitTestFirstDemo.xcodeproj -scheme ASUnitTestFirstDemo -destination 'platform=iOS Simulator,OS=11.4,name=iPhone 7'
    // 可以有多个destination

    结果

    Test Suite 'All tests' started at 2017-09-11 11:12:16.348
    Test Suite 'ASUnitTestFirstDemoTests.xctest' started at 2017-09-11 11:12:16.349
    Test Suite 'ASUnitTestFirstDemoTests' started at 2017-09-11 11:12:16.349
    Test Case '-[ASUnitTestFirstDemoTests testLevel1]' started.
    Test Case '-[ASUnitTestFirstDemoTests testLevel1]' passed (0.001 seconds).
    Test Case '-[ASUnitTestFirstDemoTests testLevel2]' started.
    /Users/liyongguang-eleme-iOS-Development/Desktop/ASUnitTestFirstDemo/ASUnitTestFirstDemoTests/ASUnitTestFirstDemoTests.m:46: error: -[ASUnitTestFirstDemoTests testLevel2] : ((tax) equal to (45.0)) failed: ("-60") is not equal to ("45") - 用例2测试失败
    Test Case '-[ASUnitTestFirstDemoTests testLevel2]' failed (1.007 seconds).
    Test Suite 'ASUnitTestFirstDemoTests' failed at 2017-09-11 11:12:17.358.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.009) seconds
    Test Suite 'ASUnitTestFirstDemoTests.xctest' failed at 2017-09-11 11:12:17.359.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.010) seconds
    Test Suite 'All tests' failed at 2017-09-11 11:12:17.360.
    Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.012) seconds
    Failing tests:
    -[ASUnitTestFirstDemoTests testLevel2]
    ** TEST FAILED **

    如果是workspace

    xcodebuild -workspace ASKiwiTest.xcworkspace -scheme ASKiwiTest-Example -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' test

    每个test方法都会跑一遍,并给出结果描述。

    案例 7 代码的执行时间测试-(性能测试)
    性能测试主要使用 measureBlock 方法 ,用于测试一组方法的执行时间,通过设置baseline(基准)和stddev(标准偏差)来判断方法是否能通过性能测试。


    假如直接执行方法,因为block中没有内容,所以方法的执行时间为0.0s,如果我们把baseline设成0.05,偏差10%,是可以通过的测试的。但是如果设置如果我们把baseline为1,偏差10%,那测试会失败,因为不满足条件。
    如上图所示,这个方法是用来测试block内代码的执行时间的,我们可以通过打印很清楚的看到它其实执行了10次,用处也很宽广,比如想测试shenfenzheng的识别时间,请求的时间,转模型的速度等等都可以通过它来测试,这里只是举个简单的例子.

    我们可以看下打印发现他确实是执行了十次.


    再来看看左边的执行代码相关信息,这里由于打印"1"执行的太快无法看出效果,所以我将测试内容换成了使用for循环打印1-9999,看看他们的执行时间.


    可以很清楚的看到,10次的平均时间是1.382秒,第一次时间是1.85秒,并且可以看到第一次执行时间超过了平均时间33%,这里的测试结果都是和机器性能有关系的.

    案例 8 登陆模块测试


    案例 9 加法测试

    - (void)testExample {
    //设置变量和设置预期值
    NSUInteger a = 10;NSUInteger b = 15;
    NSUInteger expected = 24;
    //执行方法得到实际值
    NSUInteger actual = [self add:a b:b];
    //断言判定实际值和预期是否符合
    XCTAssertEqual(expected, actual,@"add方法错误!");
    }

    -(NSUInteger)add:(NSUInteger)a b:(NSUInteger)b{
    return a+b;
    }

    从这也能看出一个测试用例比较规范的写法,1:定义变量和预期,2:执行方法得到实际值,3:断言

    案例 10 代码来自于AFNetworking,用于测试backgroundImageForState方法

    - (void)testThatBackgroundImageChanges {
    XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);
    NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIButton * _Nonnull button, NSDictionary<NSString *,id> * _Nullable bindings) {
    return [button backgroundImageForState:UIControlStateNormal] != nil;
    }];

    [self expectationForPredicate:predicate
    evaluatedWithObject:self.button
    handler:nil];
    [self waitForExpectationsWithTimeout:20 handler:nil];
    }

    利用谓词计算,button是否正确的获得了backgroundImage,如果正确20秒内正确获得则通过测试,否则失败。

    expectationForNotification 方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。

    - (void)testAsynExample1 {
    [self expectationForNotification:(@"监听通知的名称xxx") object:nil handler:nil];
    [[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx" object:nil];

    //设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:3 handler:nil];
    }

    这个例子也可以用expectationWithDescription实现,只是多些很多代码而已,但是这个可以帮助你更好的理解 expectationForNotification 方法和 expectationWithDescription的区别。同理,expectationForPredicate方法也可以使用expectationWithDescription实现。

    func testAsynExample1() {
    let expectation = expectationWithDescription("监听通知的名称xxx")
    let sub = NSNotificationCenter.defaultCenter().addObserverForName("监听通知的名称xxx", object: nil, queue: nil) { (not) -> Void in
    expectation.fulfill()
    }

    NSNotificationCenter.defaultCenter().postNotificationName("监听通知的名称xxx", object: nil)
    waitForExpectationsWithTimeout(1, handler: nil)
    NSNotificationCenter.defaultCenter().removeObserver(sub)
    }

    XCTest常见的断言

    XCTFail(format...)  生成一个失败的测试
    XCTAssertNil(a1,format...)为空判断, a1为空时通过,反之不通过;
    XCTAssertNotNil(a1,format...) 不为空判断,a1不为空时通过,反之不通过;
    XCTAssert(expression,format...) 当expression求值为true时通过;
    XCTAssertTrue(expression,format...) 当expression求值为true时通过;
    XCTAssertFalse(expression,format...) 当expression求值为False时通过;
    XCTAssertEqualObjects(a1, a2,format...) 判断相等 [a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
    XCTAssertNotEqualObjects(a1, a2,format...) 判断不等,[a1 isEqual:a2]值为False时通过;
    XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
    XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
    XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
    XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
    XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态)
    XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
    XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
    XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
    XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过

    特别注意下XCTAssertEqualObjects和XCTAssertEqual。 XCTAssertEqualObjects(a1, a2, format...)的判断条件是[a1 isEqual:a2]是否返回一个YES。XCTAssertEqual(a1, a2, format...)的判断条件是a1 == a2是否返回一个YES。对于后者,如果a1和a2都是基本数据类型变量,那么只有a1 == a2才会返回YES

    备注:

    1.关于私有方法的测试,只能通过扩展来实现

    2.关于case的方法名字,一定要以test开头并注意驼峰命名法,且不能加入参数。

    3.单元测试类继承自XCTestCase,他有一些重要的方法,其中最重要的有3个,setUp ,tearDown,measureBlock.

    4.md + 5切换到测试选项卡后会看到很多小箭头,点击可以单独或整体测试.

    5.cmd + U运行整个单元测试

    6.使用pod的项目中,在XC测试框架中测试内容包括第三方包时,需要手动去设置Header Search Paths才能找到头文件 ,还需要设置test target的PODS_ROOT。

    7.xcode7要使用真机做跑测试时,证书必须配对,否则会报错exc_breakpoint错误

    链接:https://www.jianshu.com/p/4001e06b150e

    收起阅读 »

    OpenGLES/(GLKit/CoreAnimation正方体的渲染+旋转)

    一.Hello--OpenGLES                 OpenGL可用于渲染...
    继续阅读 »

    一.Hello--OpenGLES 

                    OpenGL可用于渲染2D和3D图像,是一个多用途的开源图形库。OpenGL设计用来将函数命令转换成图形命令,发送到GPU中。GPU正是被设计用来处理图形命令的,所以OpenGL的绘制非常高效。

                    OpenGLES是OpenGL的简化版本,抛弃了冗余的文件及命令,使之专用于嵌入式设备。OpenGLES使得移动APP能充分利用GPU的强大运算能力。iOS设备上的GPU能执行更精确的2D和3D绘制,以及更加复杂的针对每个像素的图形脚本(shader)计算。⽀持的平台: iOS, Andriod , BlackBerry ,bada ,Linux ,Windows。

    1.1准备工程

                    iOS新建工程,@interface ViewController : UIViewController改成-->@interface ViewController : GLKViewController,.h文件导入#import,.m导入#import#import,最后在Main.storyboard中将view改成GLVIew






    GLView

    1.2EAGLContext(OpenGL 上下文)

                    EAGLContext对象管理着OpenGLES的渲染context,即所有绘制的状态,命令及资源信息,并控制GPU去执行渲染运算。 绘制如textures及renderbuffers的过程,是由一个与context绑定的EAGLSharegroup对象来管理的。当初始化一个EAGLContext对象的时候,可选择新建一个sharegroup,或者使用已有的,这一点我们往往采用系统默认即可。在绘制到context之前,我们要先绑定一个完整的framebuffer对象到context中。






    Hello-OpenGLES

                    1)初始化上写文:context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];(参数知识选择版本)

                    2)设置当前上下文:[EAGLContext setCurrentContext:context];

                    3)GLView绑定上下文:GLKView *view =(GLKView *) self.view;  view.context=context;

                    注意:在使用GLview中,我们必须实现它的协议:GLKViewDelegate--->- (void)glkView:(GLKView*)viewdrawInRect:(CGRect)rect,GLKView对象使其OpenGL ES上下文成为当前上下文,并将其framebuffer绑定为OpenGL ES呈现命令的目标。然后,委托方法应该绘制视图的内容。我们给GLview设置颜色,看一下效果:glClearColor(1, 0, 0, 1.0);






    Hello--OpenGlES

    二.显示图片







    加载图片

    2.1设置顶点坐标/纹理坐标

                     在OpenGl中我们显示一张图片,首先我们设置顶点数组,绑定纹理,在OpenGLES中,我们一样这么设置:

                                    GLfloatvertexData[] = {

                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            0.5,0.5,  0.0f,    1.0f,1.0f,//右上

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上


                                                                            0.5, -0.5,0.0f,    1.0f,0.0f,//右下

                                                                            -0.5,0.5,0.0f,    0.0f,1.0f,//左上

                                                                            -0.5, -0.5,0.0f,  0.0f,0.0f,//左下

                                                                        };

                      在OpenGL中我们提到了图形绘制是点,线,三角形,正方形由两个三角形组成,就是六个顶点,而我们知道,纹理的坐标范围是(0,1),其原点是在左下角,所以坐标(0,0)是原点,右上角(1,1);

    2.2开辟顶点缓存区并把数据存到缓中区

                    (1).创建顶点缓存区标识符ID

                            GLuint  bufferID;

                            glGenBuffers(1, &bufferID);(分配纹理)

                    (2).绑定顶点缓存区.(明确作用)

                            glBindBuffer(GL_ARRAY_BUFFER, bufferID);

                    (3).将顶点数组的数据copy到顶点缓存区中(GPU显存中)

                            glBufferData(GL_ARRAY_BUFFER,sizeof(vertexData), vertexData,GL_STATIC_DRAW);

                    (4).打开读取通道.

                            1)顶点坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribPosition);

                                glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 0);

                            2)纹理坐标数据

                                glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

                                glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 3);

    特别说明:

                        (1)在iOS中, 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的.意味着,顶点数据在着色器端(服务端)是不可用的. 即使你已经使用glBufferData方法,将顶点数据从内存拷贝到顶点缓存区中(GPU显存中).所以, 必须由glEnableVertexAttribArray 方法打开通道.指定访问属性.才能让顶点着色器能够访问到从CPU复制到GPU的数据.

                         注意: 数据在GPU端是否可见,即,着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。

                        (2)方法简介

                            glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)    功能: 上传顶点数据到显

    存的方法(设置合适的方式从buffer里面读取数据)

             参数列表:

                            index,指定要修改的顶点属性的索引值,例如 size, 每次读取数量。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a),纹理则是2个,type,指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。 normalized,指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)stride,指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0 ,ptr指定一个指针,指向数组中第一个顶点属性的第一个组件。初始值为0







    参数流程说明

    2.2获取纹理







    纹理

                    1)路径:NSString *filePath = [[NSBundle mainBundle]pathForResource:@"kunkun" ofType:@"jpg"];

                    2)参数:NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];

                                    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];

                    说明:纹理坐标原点是左下角,但是图片显示原点应该是左上角.我们要设置图片绘制从左上角开始绘制GLKTextureLoaderOriginBottomLeft;

                    3)cEffect:你可以把它理解成UIimageVIew,用于显示图片的控件,iOS提供GLKBaseEffect 完成着色器工作(顶点/片元)

                                cEffect = [[GLKBaseEffect alloc]init];

                                cEffect.texture2d0.enabled = GL_TRUE;

                                cEffect.texture2d0.name= textureInfo.name;

                    最后在GLVIew的delegate中:

                                                    1.清除颜色缓冲区

                                                    glClear(GL_COLOR_BUFFER_BIT);

                                                    2.准备绘制

                                                    [cEffect prepareToDraw];

                                                    3.开始绘制

                                                    glDrawArrays(GL_TRIANGLES, 0, 6);







    效果

    三.OpenGLES绘制立方体

            在OpenGLES绘制立方体,相当于绘制六个面,十二个三角形,60个数据(当然你在图元连接方式那里可以选择平面,GL_TRIANGLE_FAN,这样就会少设置一点数据,这里我选择GL_TRIANGLES)

    GLfloatvertexData[] = {

            //第一个面

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  -0.5f,    1.0f,1.0f,//右上

            -0.5,0.5, -0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5, -0.5f,  0.0f,0.0f,//左下

            0.5, -0.5,0.5f,    1.0f,0.0f,//右下

            0.5,0.5,  0.5f,    1.0f,1.0f,//右上

            -0.5,0.5,0.5f,    0.0f,1.0f,//左上

            -0.5, -0.5,0.5f,  0.0f,0.0f,//左下

    //

            //2

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5, -0.5,0.5f,    0.0f,1.0f,//右下

            -0.5, -0.5, -0.5f,    0.0f,0.0f,//右下

            0.5,0.5, -0.5f,    1.0f,0.0f,//右下

            0.5,0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            //3

            0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            0.5,0.5,0.5f,    0.0f,1.0f,//右下

            0.5,0.5, -0.5f,    0.0f,0.0f,//右下

            -0.5, -0.5, -0.5f,    1.0f,0.0f,//右下

            -0.5, -0.5,0.5f,    1.0f,1.0f,//右下

            -0.5,0.5,0.5f,    0.0f,1.0f,//右下

            -0.5,0.5, -0.5f,    0.0f,0.0f,//右下

        };







    效果


    四。CoreAnimation正方体的大体原理就是一个VIew上放六个imageVIew,并设置imageVIew旋转组成一个立方体,一共6个,最后添加定时器控制view的layer转动,达到效果,因为比较简单,这里不做展示



    作者:枫紫_6174
    链接:https://www.jianshu.com/p/035061d80d5c
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    OpenGl纹理相关常用API

    一.原始图像数据1.像素包装:                    图像存储空间=图像的宽度*图像的高度*每个像素的字节数二.相关函数(加粗部分表示常用)2....
    继续阅读 »

    一.原始图像数据

    1.像素包装:

                        图像存储空间=图像的宽度*图像的高度*每个像素的字节数

    二.相关函数(加粗部分表示常用)

    2.1  改变像素存储方式----->void glPixelStorei(GLenum pname,GLint param);

            恢复像素存储⽅式----->void glPixelStoref(GLenum pname,GLfloat param);

    参数说明:

                    //参数1:GL_UNPACK_ALIGNMENT 指定OpenGL 如何从数据缓存区中解包图像数据

                    //参数2:表示参数GL_UNPACK_ALIGNMENT 设置的值

                   //GL_UNPACK_ALIGNMENT 指内存中每个像素⾏起点的排列请求

                    允许设置为1 (byte排列)

                                        2(排列为偶数byte的⾏)

                                        4(字word排列)

                                        8(⾏从双字节边界开始)

             举例: glPixelStorei(GL_UNPACK_ALIGNMENT,1);

    2.2  从颜⾊缓存区内容作为像素图直接读取

    void glReadPixels(GLint x,GLint y,GLSizei width,GLSizei height, GLenum format, GLenum type,const void * pixels);

    参数说明:

                    //参数1:x,矩形左下⻆的窗⼝坐标

                    //参数2:y,矩形左下⻆的窗⼝坐标

                    //参数3:width,矩形的宽,以像素为单位

                    //参数4:height,矩形的⾼,以像素为单位

                    //参数5:format,OpenGL 的像素格式,参考 表6-1

                    //参数6:type,解释参数pixels指向的数据,告诉OpenGL 使⽤缓存区中的什么数据类型来存储颜⾊分量,像素数据的数据类型,参考 表6-2

                    //参数7:pixels,指向图形数据的指针

                    glReadBuffer(mode);—> 指定读取的缓存

                    glWriteBuffer(mode);—> 指定写⼊的缓存

    2.3载⼊纹理

                    void glTexImage1D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLint border,GLenum format,GLenum type,void *data);

                    void glTexImage2D(GLenum target,GLint level,GLint internalformat,GLsizei width,GLsizei height,GLint border,GLenum format,GLenum type,void * data);(这个是比较常用的)

                    void glTexImage3D(GLenum target,GLint level,GLint internalformat,GLSizei width,GLsizei height,GLsizei depth,GLint border,GLenum format,GLenum type,void *data);

    参数说明:

                        * target:`GL_TEXTURE_1D`、`GL_TEXTURE_2D`、`GL_TEXTURE_3D`。 

                        * Level:指定所加载的mip贴图层次。⼀般我们都把这个参数设置为0。

                        * internalformat:每个纹理单元中存储多少颜⾊成分。

                        * width、height、depth参数:指加载纹理的宽度、⾼度、深度。==注意!==这些值必须是2的整数次⽅。(这是因为OpenGL 旧版本上的遗留下的⼀个要求。当然现在已经可以⽀持不是2的整数次⽅。但是开发者们还是习惯使⽤以2的整数次⽅去设置这些参数。)

                        * border参数:允许为纹理贴图指定⼀个边界宽度。

                        * format、type、data参数:与我们在讲glDrawPixels 函数对于的参数相同

    2.4更新纹理

                    void glTexSubImage1D(GLenum target,GLint level,GLint xOffset,GLsizei width,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage2D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLsizei width,GLsizei height,GLenum format,GLenum type,const GLvoid *data);

                    void glTexSubImage3D(GLenum target,GLint level,GLint xOffset,GLint yOffset,GLint zOffset,GLsizei width,GLsizei height,GLsizei depth,Glenum type,const GLvoid * data);

    参数说明:同载入纹理一样

    2.5插入替换纹理

                    void glCopyTexSubImage1D(GLenum target,GLint level,GLint xoffset,GLint x,GLint y,GLsizei width);

                    void glCopyTexSubImage2D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint x,GLint y,GLsizei width,GLsizei height);

                    void glCopyTexSubImage3D(GLenum target,GLint level,GLint xoffset,GLint yOffset,GLint zOffset,GLint x,GLint y,GLsizei width,GLsizei height);

    参数说明:同载入纹理一样

    2.6使⽤颜⾊缓存区加载数据,形成新的纹理使⽤

                    void glCopyTexImage1D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLint border);

                    void glCopyTexImage2D(GLenum target,GLint level,GLenum internalformt,GLint x,GLint y,GLsizei width,GLsizei height,GLint border);

    特别说明:x,y 在颜⾊缓存区中指定了开始读取纹理数据的位置;缓存区⾥的数据,是源缓存区通过glReadBuffer设置的。注意:不存在glCopyTextImage3D ,因为我们⽆法从2D 颜⾊缓存区中获取体积 数据

    三.纹理对象

    3.1使⽤函数分配纹理对象&&指定纹理对象的数量 和 指针(指针指向⼀个⽆符号整形数组,由纹理对象标识符填充)。

                    void glGenTextures(GLsizei n,GLuint * textTures);

    3.2绑定纹理状态

                    void glBindTexture(GLenum target,GLunit texture);

    参数说明:

                    参数target:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数texture:需要绑定的纹理对象

    3.2删除纹理对象

                    void glDeleteTextures(GLsizei n,GLuint *textures);

                    参数说明:同分配纹理对象一样

    3.3测试纹理对象是否有效

                    GLboolean glIsTexture(GLuint texture);

                    说明:如果texture是⼀个已经分配空间的纹理对象,那么这个函数会返回GL_TRUE,否则会返回GL_FALSE。

    3.4设置纹理参数

                    glTexParameterf(GLenum target,GLenum pname,GLFloat param);

                    glTexParameteri(GLenum target,GLenum pname,GLint param);

                    glTexParameterfv(GLenum target,GLenum pname,GLFloat *param);

                    glTexParameteriv(GLenum target,GLenum pname,GLint *param);

    参数说明:

                    参数1:target,指定这些参数将要应⽤在那个纹理模式上,⽐如GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D。

                    参数2:pname,指定需要设置那个纹理参数

                    参数3:param,设定特定的纹理参数的值

    3.5过滤方式

            1)邻近过滤(GL_NEAREST)


      说明:当一像素点靠近A时,返回离这个点最近的像素值

            2)线性过滤(GL_LINEAR)













    说明:两种过滤效果本质上没有多大区别,肉眼很难区分的出来,只有当图片放大后,可惜清晰的看清楚两种过滤方式的差别,一般情况下,glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR) 纹理放⼤时,使⽤线性过滤

    3.6设置环绕⽅式

    当纹理坐标超出默认范围时,每个选项都有不同的输出效果



    设置环绕方式;

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_S,GL_CLAMP_TO_EDGE);

    glTextParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_T,GL_CLAMP_TO_EDGE);

    参数说明:

                    参数1:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

                    参数2:GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,针对s,t,r坐标(s->x,t->y,r->z)

                    参数3:GL_REPEAT、GL_CLAMP、GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER

                    GL_REPEAT:OpenGL 在纹理坐标超过1.0的⽅向上对纹理进⾏重复;

                    GL_CLAMP:所需的纹理单元取⾃纹理边界或TEXTURE_BORDER_COLOR.

                    GL_CLAMP_TO_EDGE环绕模式强制对范围之外的纹理坐标沿着合法的纹理单元的最后⼀⾏或者最后⼀列来进⾏采样。

                    GL_CLAMP_TO_BORDER:在纹理坐标在0.0到1.0范围之外的只使⽤边界纹理单元。边界纹理单元是作为围绕基本图像的额外的⾏和列,并与基本纹理图像⼀起加载的。

    3.7OpenGL 像素格式

    常量说明

    GL_RGB                                                         描述红、绿、蓝顺序排列的颜⾊

    GL_RGBA                                                      按照红、绿、蓝、Alpha顺序排列的颜⾊

    GL_BGR                                                         按照蓝、绿、红顺序排列颜⾊

    GL_BGRA                                                       按照蓝、绿、红、Alpha顺序排列颜⾊

    GL_RED                                                         每个像素只包含了⼀个红⾊分量

    GL_GREEN                                                    每个像素只包含了⼀个绿⾊分量

    GL_BLUE                                                       每个像素只包含了⼀个蓝⾊分量

    GL_RG                                                           每个像素依次包含了一个红色和绿色的分量

    GL_RED_INTEGER                                        每个像素包含了一个整数形式的红⾊分量

    GL_GREEN_INTEGER                                   每个像素包含了一个整数形式的绿色分量

    GL_BLUE_INTEGER                                     每个像素包含了一个整数形式的蓝色分量

    GL_RG_INTEGER                                          每个像素依次包含了一个整数形式的红⾊、绿⾊分量

    GL_RGB_INTEGER                                       每个像素包含了一个整数形式的红⾊、蓝⾊、绿色分量

    GL_RGBA_INTEGER                                     每个像素包含了一个整数形式的红⾊、蓝⾊、绿⾊、Alpah分量

    GL_BGR_INTEGER                                        每个像素包含了一个整数形式的蓝⾊、绿⾊、红色分量

    GL_BGRA_INTEGER                                     每个像素包含了一个整数形式的蓝⾊、绿⾊、红色、Alpah分量

    GL_STENCIL_INDEX                                    每个像素只包含了一个模板值

    GL_DEPTH_COMPONENT                          每个像素只包含一个深度值

    GL_DEPTH_STENCIL                                 每个像素包含一个深度值和一个模板值

    3.8像素数据的数据类型

    GL_UNSIGNED_BYTE                        每种颜色分量都是一个8位无符号整数

    GL_BYTE                                            8位有符号整数

    GL_UNSIGNED_SHORT                    16位无符号整数

    GL_SHORT                                         16位有符号整数

    CL_UNSIGNED_INT                            32位无符号整数

    GL_INT                                               32位有符号整数

    GL_FLOAT                                        单精度浮点数

    GL_HALF_FLOAT                                半精度浮点数

    GL_UNSIGNED_BYTE_3_2_3            包装的RGB值

    GL_UNSIGNED_BYTE_2_3_3_REV    包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5         包装的RGB值

    GL_UNSIGNED_SHORT_5_6_5_REV  包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4      包装的RGB值

    GL_UNSIGNED_SHORT_4_4_4_4_REV   包装的RGB值

    GL_UNSIGNED_SHORT_5_5_5_1        包装的RGB值

    GL_UNSIGNED_SHORT_1_5_5_5_REV   包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8               包装的RGB值

    GL_UNSIGNED_INT_8_8_8_8_REV      包装的RGB值

    GL_UNSIGNED_INT_10_10_10_2       包装的RGB值

    GL_UNSIGNED_INT_2_10_10_10_REV   包装的RGB值

    GL_UNSIGNED_INT_24_8                   包装的RGB值

    GL_UNSIGNED_INT_10F_11F_REV       包装的RGB值

    GL_FLOAT_24_UNSIGNED_INT_24_8_REV     包装的RGB值




    作者:枫紫
    链接:https://www.jianshu.com/p/bea1fd229b18


    收起阅读 »

    iOS---webView相关及原生和web的交互

    webView的基本应用,监听加载进度,返回上一页,异常处理web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView原生调用web:获取webView的标题等web原生互相调用:web获取a...
    继续阅读 »


  • webView的基本应用,监听加载进度,返回上一页,异常处理
  • web调用原生:处理跳转到指定的原生页面,拦截跳转其他app,添加app白名单,拦截通用链接跳转,js注入,关闭webView
  • 原生调用web:获取webView的标题等
  • web原生互相调用:web获取app当前的id、token等用户信息
  • 微信web里打开原生app
  • 一、webView的基本应用

    现在基本每个app都会或多或少用到web来实现快速迭代。正常都会将其封装在一个控制器里,以使其样式、功能统一
    (iOS8引入了WKWebView,使用独立的进程渲染web,解决了之前UIWebView内存泄漏和crash率高等被诟病已久的问题,所以现在基本都是用WKWebView了)


        //如果不考虑和原生的交互
    _webView = [[WKWebView alloc] initWithFrame:CGRectZero];
    [self.view addSubview:_webView];
    [_webView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(self.view);
    }];
    _webView.UIDelegate = self;
    _webView.navigationDelegate = self;
    [_webView loadRequest:[NSURLRequest requestWithURL:URL]];//这里的url是经过校检的

    如果要监听webview的加载进度

        //kvo监听
    [_webView addObserver:self forKeyPath:@"estimatedProgress" options:0 context:nil];

    //创建加载进度条UIProgressView
    {
    init progressView
    }

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqualToString:NSStringFromSelector(@selector(estimatedProgress))]) {
    self.progressView.alpha = 1.0f;
    BOOL animated = _webView.estimatedProgress > self.progressView.progress;
    [self.progressView setProgress:_webView.estimatedProgress animated:animated];

    if (_webView.estimatedProgress >= 1.0f) {
    [UIView animateWithDuration:0.3f delay:0.3f options:UIViewAnimationOptionCurveEaseOut animations:^{
    self.progressView.alpha = 0.0f;
    } completion:^(BOOL finished) {}];
    }
    } else {
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    }
    返回上一页

            //kvo监听
    [_webView addObserver:self forKeyPath:@"canGoBack" options:0 context:nil];//监听是否有上一页

    //configBackButton里判断canGoBack,如果不可以返回就将按钮置灰或者隐藏
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == _webView && [keyPath isEqual: @"canGoBack"]) {
    [self configBackButton];
    }
    }
    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    [self configBackButton];
    }

    //按钮事件
    if ([weakSelf.webView canGoBack]) {
    [weakSelf.webView goBack];
    }
    当 WKWebView 总体内存占用过大,页面即将白屏的时候,系统会调用下面的回调函数,我们在这里执行[webView reload]解决白屏问题

    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {

    [webView reload];
    }

    二、web调用原生

    1.这三个代理方法是可以接收到web的调用比如 window.prompt("xxx")

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler;
    1. 在发送请求之前,决定是否跳转
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    NSURL *url = navigationAction.request.URL;
    //可以在这里处理一些跳转,比如通过scheme来处理跳转到指定的原生页面(xxx://xxx),拦截跳转其他app,添加app白名单,拦截通用链接跳转等等

    //比如
    if ([@"mailto" isEqualToString:url.scheme] || [@"tel" isEqualToString:url.scheme]) {//系统scheme
    if ([[UIApplication sharedApplication] canOpenURL:url]) {
    [[UIApplication sharedApplication] openURL:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
    } if ([@"xxxx" isEqualToString:url.scheme]) {
    // 如果该scheme是你定义好的scheme,可以根据后面的参数去处理跳转到app内的指定页面,或者其他操作
    decisionHandler(WKNavigationActionPolicyCancel);
    }else if ([scheme白名单 containsObject:url.scheme]) {//白名单
    // 打开scheme
    [[UIApplication sharedApplication] openURL:url];
    decisionHandler(WKNavigationActionPolicyCancel);
    } else {
    BOOL canOpenUniversalUrl = NO;
    for (NSString *str in universalLink白名单) {
    if ([url.absoluteString rangeOfString:str].location != NSNotFound) {
    canOpenUniversalUrl = YES;
    break;
    }
    }
    if (canOpenUniversalUrl) {
    // 打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow);
    } else {
    // Default 可以正常访问网页,但禁止打开通用链接
    decisionHandler(WKNavigationActionPolicyAllow+2);
    }
    }
    }
    web只需
    window.location.href = "xxx"//这里的地址就是上方代理方法的url
    WKWebView可以使用WKScriptMessageHandler来实现JS调用原生方法
    首先初始化的时候,这里拿最常用的web调用关闭webView:xxx_close举例(也可以用上边的href的scheme方式实现,但不太合理)
        _webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:[self configWKConfiguration]];

    // config,js注入
    - (WKWebViewConfiguration *)configWKConfiguration {
    WKWebViewConfiguration* webViewConfig = [WKWebViewConfiguration new];
    WKUserContentController *userContentController = [WKUserContentController new];
    //这里如果用的不多,可以不用单独写一个js文件,直接用字符串就行了
    NSString *jsStr = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"js文件地址"] encoding:NSUTF8StringEncoding error:nil];
    WKUserScript *userScript = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
    [userContentController addUserScript:userScript];
    [userContentController addScriptMessageHandler:self name:"closeWebView"];

    webViewConfig.userContentController = userContentController;
    webViewConfig.preferences = [[WKPreferences alloc] init];
    webViewConfig.preferences.javaScriptEnabled = YES;
    return webViewConfig;
    }

    //app里的js文件里实现
    var xxx = {
    close: function() {
    window.webkit.messageHandlers.closeWebView.postMessage(null);
    },
    }

    //在这里能收到回调
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    if ([message.name isEqualToString:@"closeWebView"]) {
    // 关闭页面
    [self.navigationController popViewControllerAnimated:YES];
    }
    }

    web中只需
          try {
    window.xxx.close();
    } catch (err) {}

    三、原生调用web,就是app调用web里的js方法

    1.比较常用的一种,获取webView的标题
    //也可以用正则去获取标题、图片之类的

        [webView evaluateJavaScript:@"document.title" completionHandler:^(id result, NSError * _Nullable error) {
    }];

    四. web原生互相调用

    比如一个场景,在web里获取app当前登录的账号id

    1. 首先像上边一样,通过js注入的方式web向app发送getUserId请求,app也要同步处理

    //web代码

          try {
    window.xxx.getUserId();//这里可以直接加返回值,但是app内的js没办法直接去获取原生用户信息这些变量,所以还是要通过原生的代理去实现
    } catch (err) {}
    1. 这时候app接收到这个请求,但还要将userId告诉web

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:"getUserId"]){
    NSDictionary *dict = xxx;//因为这个过程是异步的,所以这里最好多加一点信息,以便于web确认该结果是上边请求的返回
    NSString * jsMethod = [NSString stringWithFormat:@"window.getUserId(%@)",[dict yy_modelToJSONString]];
    [webView evaluateJavaScript:@"xxx" completionHandler:^(id result, NSError * _Nullable error) {
    }];
    }
    }

    1. web需要将getUserId方法挂载到window上,算是自定义回调,将上一步生成的用户信息dic当成参数传进来,然后去处理接收到的信息

    //web代码

        window["getUserId"] = function(value) {
    //在这里解析处理接收到的用户信息
    };

    五. web如何在微信里打开原生?

    普通的scheme跳转被微信给禁了,所以现在基本都是通过universalLink通用链接的方式,设置universalLink的方式网上有好多,另外通用链接可以设置多个,最好设置两个以上(因为这里有个隐藏的坑:web的域名不能和universalLink一样,否则无法跳转)
    web代码:


    window.location.href = '通用链接://具体落地页'//可以通过参数跳转到具体的页面
    作者:Theendisthebegi
    链接:https://www.jianshu.com/p/d66d694b762f










    收起阅读 »

    iOS开发宏定义整理

    宏定义今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能下面分开介绍各种宏:Macros.h这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK#...
    继续阅读 »

    宏定义

    今天整理一些自己的项目里零零碎碎的东西,发现有些东西太杂太乱,就是定义的全局.这里一个宏,那边一个#define,发现这东西会左右引用,很影响性能

    下面分开介绍各种宏:

    • Macros.h

    这里面就放各各宏的头文件,然后在PCH文件中引用着这个一个头文件就OK
    #import "DimensMacros.h"
    #import "UtilsMacros.h"
    #import "PathMacros.h"
    #import "NotificationMacros.h"
    #import "APIStringMacros.h"

    • APIStringMacros_h(服务端API接口的宏)

    这里面主要放一些API相关的东西:比如你请求网络的接口hostname,port还有一些第三方的关键字段:极光推送的appkey....

    • DimensMacros.h (定义尺寸类的宏)

    这里面定义一些尺寸相关的宏:

    #pragma mark - 系统UI
    #define kNavigationBarHeight 44
    #define kStatusBarHeight 20
    #define kTopBarHeight 64
    #define kToolBarHeight 44
    #define kTabBarHeight 49
    #define kiPhone4_W 320
    #define kiPhone4_H 480
    #define kiPhone5_W 320
    #define kiPhone5_H 568
    #define kiPhone6_W 375
    #define kiPhone6_H 667
    #define kiPhone6P_W 414
    #define kiPhone6P_H 736
    /*** 当前屏幕宽度 */
    #define kScreenWidth [[UIScreen mainScreen] bounds].size.width
    /*** 当前屏幕高度 */
    #define kScreenHeight [[UIScreen mainScreen] bounds].size.height
    /*** 普通字体 */
    #define kFont(size) [UIFont systemFontOfSize:size]
    /*** 粗体 */
    #define kBoldFont(size) [UIFont boldSystemFontOfSize:size]
    #define kLineHeight (1 / [UIScreen mainScreen].scale)
    • NotificationMacros.h(通知Notification相关宏)

    这里面放一些关于通知定义的宏

    #define TNCancelFavoriteProductNotification     @"TNCancelFavoriteProductNotification"      //取消收藏时
    #define TNMarkFavoriteProductNotification @"TNMarkFavoriteProductNotification" //标记收藏时

    #define kNotficationDownloadProgressChanged @"kNotficationDownloadProgressChanged" //下载进度变化
    #define kNotificationPauseDownload @"kNotificationPauseDownload" //暂停下载
    #define kNotificationStartDownload @"kNotificationStartDownload" //开始下载

    #define kNotificationDownloadSuccess @"kNotificationDownloadSuccess" //下载成功
    #define kNotificationDownloadFailed @"kNotificationDownloadFailed" //下载失败
    #define kNotificationDownloadNewMagazine @"kNotificationDownloadNewMagazine"
    • UtilsMacros_h(工具类的宏)

    这里面存放一些方便开发的工具:颜色,打印,单利,版本...
    // 日志输出
    #ifdef DEBUG
    #define LMLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTIONLINE, ##VA_ARGS);
    #else
    #define LMLog(...)
    #endif

    #define WeakSelf(weakSelf)  __weak __typeof(&*self)weakSelf = self;

    #pragma mark - 颜色
    #define kWhiteColor [UIColor whiteColor]
    #define kBlackColor [UIColor blackColor]
    #define kDarkGrayColor [UIColor darkGrayColor]
    #define kLightGrayColor [UIColor lightGrayColor]
    #define kGrayColor [UIColor grayColor]
    #define kRedColor [UIColor redColor]
    #define kGreenColor [UIColor greenColor]
    #define kBlueColor [UIColor blueColor]
    #define kCyanColor [UIColor cyanColor]
    #define kYellowColor [UIColor yellowColor]
    #define kMagentaColor [UIColor magentaColor]
    #define kOrangeColor [UIColor orangeColor]
    #define kPurpleColor [UIColor purpleColor]
    #define kBrownColor [UIColor brownColor]
    #define kClearColor [UIColor clearColor]

    //16进制
    #define LMColorFromHex(s) [UIColor colorWithRed:(((s & 0xFF0000) >> 16))/255.0green:(((s &0xFF00) >>8))/255.0blue:((s &0xFF))/255.0alpha:1.0]
    //RGB
    #define kRGBAColor(r,g,b,a) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a]
    #define kRGBColor(r,g,b) kRGBAColor(r,g,b,1.0f)
    #define kSeperatorColor kRGBColor(234,237,240)
    #define kBgColor kRGBColor(243,245,247)

    #define krgbaColor(r,g,b,a) [UIColor colorWithRed:r green:g blue:b alpha:a]
    #define krgbColor(r,g,b) krgbColor(r,g,b,1.0f)

    #define kCommonHighLightRedColor krgbColor(1.00f,0.49f,0.65f)
    #define kCommonGrayTextColor krgbColor(0.63f,0.63f,0.63f)
    #define kCommonRedColor krgbColor(0.91f,0.33f,0.33f)
    #define kCommonBlackColor krgbColor(0.17f,0.23f,0.28f)
    #define kCommonTintColor krgbColor(0.42f,0.33f,0.27f)
    #define kCommonBgColor krgbColor(0.86f,0.85f,0.80f)
    #define kDetailTextColor krgbColor(0.56f,0.60f,0.62f)
    #define kLineBgColor krgbColor(0.86f,0.88f,0.89f)
    #define kTextColor krgbColor(0.32f,0.36f,0.40f)


    #define kVersion [NSString stringWithFormat:@"%@",[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]

    //System version utils

    #define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
    #define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
    #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
    #define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

    //大于等于7.0的ios版本
    #define iOS7_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")

    //大于等于8.0的ios版本
    #define iOS8_OR_LATER SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")

    //iOS6时,导航VC中view的起始高度
    #define YH_HEIGHT (iOS7_OR_LATER ? 64:0)

    //获取系统时间戳
    #define getCurentTime [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]]

    #define kWindow [UIApplication sharedApplication].keyWindow //主窗口
    #define kUserDefault [NSUserDefaults standardUserDefaults]

    #pragma mark - 字符串转化
    #define kEmptyStr @""
    #define kIntToStr(i) [NSString stringWithFormat: @"%d", i]
    #define kIntegerToStr(i) [NSString stringWithFormat: @"%ld", i]
    #define kValidStr(str) [NHUtils validString:str]

    #pragma mark - 单利
    #define SingletonH(methodName) + (instancetype)shared##methodName;
    // .m文件的实现
    #if __has_feature(objc_arc) // 是ARC
    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    #else // 不是ARC

    #define SingletonM(methodName) \
    static id _instace = nil; \
    + (id)allocWithZone:(struct _NSZone *)zone \
    { \
    if (_instace == nil) { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super allocWithZone:zone]; \
    }); \
    } \
    return _instace; \
    } \
    \
    - (id)init \
    { \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
    _instace = [super init]; \
    }); \
    return _instace; \
    } \
    \
    + (instancetype)shared##methodName \
    { \
    return [[self alloc] init]; \
    } \
    \
    - (oneway void)release \
    { \
    \
    } \
    \
    - (id)retain \
    { \
    return self; \
    } \
    \
    - (NSUInteger)retainCount \
    { \
    return 1; \
    } \
    + (id)copyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    } \
    \
    + (id)mutableCopyWithZone:(struct _NSZone *)zone \
    { \
    return _instace; \
    }

    *PathMacros.h(沙河路径宏)

    这里面是一些沙河路径,还有一些plist路径
    //文件目录
    #define kPathTemp NSTemporaryDirectory()
    #define kPathDocument [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathCache [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
    #define kPathSearch [kPathDocument stringByAppendingPathComponent:@"Search.plist"]

    #define kPathMagazine               [kPathDocument stringByAppendingPathComponent:@"Magazine"]
    #define kPathDownloadedMgzs [kPathMagazine stringByAppendingPathComponent:@"DownloadedMgz.plist"]
    #define kPathDownloadURLs [kPathMagazine stringByAppendingPathComponent:@"DownloadURLs.plist"]
    #define kPathOperation [kPathMagazine stringByAppendingPathComponent:@"Operation.plist"]

    #define kPathSplashScreen [kPathCache stringByAppendingPathComponent:@"splashScreen"]

    这样导入宏,简单明了



    作者:Cooci
    链接:https://www.jianshu.com/p/db4f67e56214

    收起阅读 »

    iOS开发必备 - iOS 的锁

    这次主要想解决这些疑问:锁是什么?为什么要有锁?锁的分类问题为什么 OSSpinLock 不安全?解决自旋锁不安全问题有几种方式为什么换用其它的锁,可以解决 OSSpinLock 的问题?自旋锁和互斥锁的关系是平行对立的吗?信号量和互斥量的关系信号量和条件变量...
    继续阅读 »

    这次主要想解决这些疑问:

      1. 锁是什么?
      1. 为什么要有锁?
      1. 锁的分类问题
      1. 为什么 OSSpinLock 不安全?
      1. 解决自旋锁不安全问题有几种方式
      1. 为什么换用其它的锁,可以解决 OSSpinLock 的问题?
      1. 自旋锁和互斥锁的关系是平行对立的吗?
      1. 信号量和互斥量的关系
      1. 信号量和条件变量的区别


    锁是什么

    锁 -- 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

    为什么要有锁?

    前面说到了,锁是用来保护线程安全的工具。

    可以试想一下,多线程编程时,没有锁的情况 -- 也就是线程不安全。

    当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:

    程序执行的顺序会被打乱,可能造成提前释放一个变量,计算结果错误等情况。

    所以我们需要将线程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization)

    属性设置 atomic

    上面提到了原子性,我马上想到了属性关键字里, atomic 的作用。

    设置 atomic 之后,默认生成的 getter 和 setter 方法执行是原子的。

    但是它只保证了自身的读/写操作,却不能说是线程安全。

    如下情况:

    //thread A
    for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
    self.arr = @[@"1", @"2", @"3"];
    }else {
    self.arr = @[@"1"];
    }
    NSLog(@"Thread A: %@\n", self.arr);
    }

    //thread B
    if (self.arr.count >= 2) {
    NSString* str = [self.arr objectAtIndex:1];
    }

    就算在 thread B 中针对 arr 数组进行了大小判断,但是仍然可能在 objectAtIndex: 操作时被改变数组长度,导致出错。这种情况声明为 atomic 也没有用。

    而解决方式,就是进行加锁。

    需要注意的是,读/写的操作都需要加锁,不仅仅是对一段代码加锁。

    锁的分类

    锁的分类方式,可以根据锁的状态,锁的特性等进行不同的分类,很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。关于锁的分类,可以参考 Java中的锁分类 看一下。

    自旋锁和互斥锁的关系

    很多谈论锁的文章,都会提到互斥锁,自旋锁。很少有提到它们的关系,其实自旋锁,也是互斥锁的一种实现,而 spin lock和 mutex 两者都是为了解决某项资源的互斥使用,在任何时刻只能有一个保持者。

    区别在于 spin lock和 mutex 调度机制上有所不同。

    OSSpinLock

    OSSpinLock 是一种自旋锁。它的特点是在线程等待时会一直轮询,处于忙等状态。自旋锁由此得名。

    自旋锁看起来是比较耗费 cpu 的,然而在互斥临界区计算量较小的场景下,它的效率远高于其它的锁。

    因为它是一直处于 running 状态,减少了线程切换上下文的消耗。

    为什么 OSSpinLock 不再安全?

    关于 OSSpinLock 不再安全,原因就在于优先级反转问题。

    优先级反转(Priority Inversion)

    什么情况叫做优先级反转?

    wikipedia 上是这么定义的:

    优先级倒置,又称优先级反转、优先级逆转、优先级翻转,是一种不希望发生的任务调度状态。在该种状态下,一个高优先级任务间接被一个低优先级任务所抢先(preemtped),使得两个任务的相对优先级被倒置。 这往往出现在一个高优先级任务等待访问一个被低优先级任务正在使用的临界资源,从而阻塞了高优先级任务;同时,该低优先级任务被一个次高优先级的任务所抢先,从而无法及时地释放该临界资源。这种情况下,该次高优先级任务获得执行权。

    再消化一下

    有:高优先级任务A / 次高优先级任务B / 低优先级任务C / 资源Z 。
    A 等待 C 执行后的 Z
    而 B 并不需要 Z,抢先获得时间片执行
    C 由于没有时间片,无法执行(优先级相对没有B高)。
    这种情况造成 A 在C 之后执行,C在B之后,间接的高优先级A在次高优先级任务B 之后执行, 使得优先级被倒置了。(假设: A 等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源。此时 C 无法与 A 争夺 CPU 时间,从而 C 无法执行,进而无法释放资源。造成的后果,就是 A 无法获得 Z 而继续推进。)

    而 OSSpinLock 忙等的机制,就可能造成高优先级一直 running ,占用 cpu 时间片。而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

    优先级反转的解决方案

    关于优先级反转一般有以下三种解决方案

    优先级继承

    优先级继承,故名思义,是将占有锁的线程优先级,继承等待该锁的线程高优先级,如果存在多个线程等待,就取其中之一最高的优先级继承。

    优先级天花板

    优先级天花板,则是直接设置优先级上限,给临界区一个最高优先级,进入临界区的进程都将获得这个高优先级。

    如果其他试图进入临界区的进程的优先级,都低于这个最高优先级,那么优先级反转就不会发生。

    禁止中断

    禁止中断的特点,在于任务只存在两种优先级:可被抢占的 / 禁止中断的 。

    前者为一般任务运行时的优先级,后者为进入临界区的优先级。

    通过禁止中断来保护临界区,没有其它第三种的优先级,也就不可能发生反转了。

    为什么使用其它的锁,可以解决优先级反转?

    我们看到很多本来使用 OSSpinLock 的知名项目,都改用了其它方式替代,比如 pthread_mutex 和 dispatch_semaphore 。

    那为什么其它的锁,就不会有优先级反转的问题呢?如果按照上面的想法,其它锁也可能出现优先级反转。

    原因在于,其它锁出现优先级反转后,高优先级的任务不会忙等。因为处于等待状态的高优先级任务,没有占用时间片,所以低优先级任务一般都能进行下去,从而释放掉锁。

    线程调度

    为了帮助理解,要提一下有关线程调度的概念。

    无论多核心还是单核,我们的线程运行总是 "并发" 的。

    当 cpu 数量大于等于线程数量,这个时候是真正并发,可以多个线程同时执行计算。

    当 cpu 数量小于线程数量,总有一个 cpu 会运行多个线程,这时候"并发"就是一种模拟出来的状态。操作系统通过不断的切换线程,每个线程执行一小段时间,让多个线程看起来就像在同时运行。这种行为就称为 "线程调度(Thread Schedule)"

    线程状态

    在线程调度中,线程至少拥有三种状态 : 运行(Running),就绪(Ready),等待(Waiting)

    处于 Running的线程拥有的执行时间,称为 时间片(Time Slice),时间片 用完时,进入Ready状态。如果在Running状态,时间片没有用完,就开始等待某一个事件(通常是 IO 或 同步 ),则进入Waiting状态。

    如果有线程从Running状态离开,调度系统就会选择一个Ready的线程进入 Running 状态。而Waiting的线程等待的事件完成后,就会进入Ready状态。

    dispatch_semaphore

    dispatch_semaphore 是 GCD 中同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。

    信号量机制

    信号量中,二元信号量,是一种最简单的锁。只有两种状态,占用和非占用。二元信号量适合唯一一个线程独占访问的资源。而多元信号量简称 信号量(Semaphore)。

    信号量和互斥量的区别

    信号量是允许并发访问的,也就是说,允许多个线程同时执行多个任务。信号量可以由一个线程获取,然后由不同的线程释放。

    互斥量只允许一个线程同时执行一个任务。也就是同一个程获取,同一个线程释放。

    之前我对,互斥量只由一个线程获取和释放,理解的比较狭义,以为这里的获取和释放,是系统强制要求的,用 NSLock 实验发现它可以在不同线程获取和释放,感觉很疑惑。

    实际上,的确能在不同线程获取/释放同一个互斥锁,但互斥锁本来就用于同一个线程中上锁和解锁。这里的意义更多在于代码使用的层面。

    关键在于,理解信号量可以允许 N 个信号量允许 N 个线程并发地执行任务。

    @synchonized

    @synchonized 是一个递归锁。

    递归锁

    递归锁也称为可重入锁。互斥锁可以分为非递归锁/递归锁两种,主要区别在于:同一个线程可以重复获取递归锁,不会死锁; 同一个线程重复获取非递归锁,则会产生死锁。

    因为是递归锁,我们可以写类似这样的代码:


    - (void)testLock{
    if(_count>0){
    @synchronized (obj) {
    _count = _count - 1;
    [self testLock];
    }
    }
    }

    而如果换成NSLock,它就会因为递归发生死锁了。

    实际使用问题

    如果obj 为 nil,或者 obj地址不同,锁会失效。

    所以我们要防止如下的情况:

    @synchronized (obj) {
    obj = newObj;
    }

    这里的 obj 被更改后,等到其它线程访问时,就和没加锁一样直接进去了。

    另外一种情况,就是 @synchonized(self). 不少代码都是直接将self传入@synchronized当中,而 self 很容易作为一个外部对象,被调用和修改。所以它和上面是一样的情况,需要避免使用。

    正确的做法是什么?obj 应当传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的,不被随便修改的。

    pthread_mutex

    pthread定义了一组跨平台的线程相关的 API,其中可以使用 pthread_mutex作为互斥锁。

    pthread_mutex 不是使用忙等,而是同信号量一样,会阻塞线程并进行等待,调用时进行线程上下文切换。

    pthread_mutex` 本身拥有设置协议的功能,通过设置它的协议,来解决优先级反转:

    pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol)

    其中协议类型包括以下几种:

    • PTHREAD_PRIO_NONE:线程的优先级和调度不会受到互斥锁拥有权的影响。
    • PTHREAD_PRIO_INHERIT:当高优先级的等待低优先级的线程锁定互斥量时,低优先级的线程以高优先级线程的优先级运行。这种方式将以继承的形式传递。当线程解锁互斥量时,线程的优先级自动被降到它原来的优先级。该协议就是支持优先级继承类型的互斥锁,它不是默认选项,需要在程序中进行设置。
    • PTHREAD_PRIO_PROTECT:当线程拥有一个或多个使用 PTHREAD_PRIO_PROTECT初始化的互斥锁时,此协议值会影响其他线程(如 thrd2)的优先级和调度。thrd2 以其较高的优先级或者以thrd2 拥有的所有互斥锁的最高优先级上限运行。基于被thrd2拥有的任一互斥锁阻塞的较高优先级线程对于 thrd2的调度没有任何影响。

    设置协议类型为 PTHREAD_PRIO_INHERIT ,运用优先级继承的方式,可以解决优先级反转的问题。

    而我们在 iOS 中使用的 NSLock,NSRecursiveLock等都是基于pthread_mutex 做实现的。

    NSLock

    NSLock属于 pthread_mutex的一层封装, 设置了属性为 PTHREAD_MUTEX_ERRORCHECK 。

    它会损失一定性能换来错误提示。并简化直接使用 pthread_mutex 的定义。

    NSCondition

    NSCondition是通过pthread中的条件变量(condition variable) pthread_cond_t来实现的。

    条件变量

    在线程间的同步中,有这样一种情况: 线程 A 需要等条件 C 成立,才能继续往下执行.现在这个条件不成立,线程 A 就阻塞等待. 而线程 B 在执行过程中,使条件 C 成立了,就唤醒线程 A 继续执行。

    对于上述情况,可以使用条件变量来操作。

    条件变量,类似信号量,提供线程阻塞与信号机制,可以用来阻塞某个线程,等待某个数据就绪后,随后唤醒线程。

    一个条件变量总是和一个互斥量搭配使用。

    NSCondition其实就是封装了一个互斥锁和条件变量,互斥锁的lock/unlock方法和后者的wait/signal统一封装在 NSCondition对象中,暴露给使用者。

    用条件变量控制线程同步,最为经典的例子就是 生产者-消费者问题。

    生产者-消费者问题

    生产者消费者问题,是一个著名的线程同步问题,该问题描述如下:

    有一个生产者在生产产品,这些产品将提供给若干个消费者去消费。要求让生产者和消费者能并发执行,在两者之间设置一个具有多个缓冲区的缓冲池,生产者将它生产的产品放入一个缓冲区中,消费者可以从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个已经放入产品的缓冲区中再次投放产品。

    我们可以刚好可以使用 NSCondition解决生产者-消费者问题。具体的代码放置在文末的 Demo 里了。

    if(count==0){
    [condition wait];
    }

    上面这样是不能保证消费者是线程安全的。

    因为NSCondition可以给每个线程分别加锁,但加锁后不影响其他线程进入临界区。所以 NSCondition使用 wait并加锁后,并不能真正保证线程的安全。

    当一个signal操作发出时,如果有两个线程都在做 消费者 操作,那同时都会消耗掉资源,于是绕过了检查。

    例如我们的条件是,count == 0 执行等待。

    假设当前 count = 0,线程A 要判断到 count == 0,执行等待;

    线程B 执行了count = 1,并唤醒线程A 执行 count - 1,同时线程C 也判断到 count > 0 。因为处在不同的线程锁,同样判断执行了 count - 1。2 个线程都会执行count - 1,但是 count = 1,实际就出现count = -1的情况。

    所以为了保证消费者操作的正确,使用 while 循环中的判断,进行二次确认:


     while (count == 0) {
    [condition wait];
    }

    条件变量和信号量的区别

    每个信号量有一个与之关联的值,发出时+1,等待时-1,任何线程都可以发出一个信号,即使没有线程在等待该信号量的值。

    可是对于条件变量,例如 pthread_cond_signal发出信号后,没有任何线程阻塞在 pthread_cond_wait 上,那这个条件变量上的信号会直接丢失掉。

    NSConditionLock

    NSConditionLock称为条件锁,只有 condition 参数与初始化时候的 condition相等,lock才能正确进行加锁操作。

    这里分清两个概念:

    • unlockWithCondition:,它是先解锁,再修改 condition 参数的值。 并不是当 condition 符合某个件值去解锁。
    • lockWhenCondition:,它与 unlockWithCondition: 不一样,不会修改 condition 参数的值,而是符合 condition 的值再上锁。

    在这里可以利用 NSConditionLock实现任务之间的依赖.

    NSRecursiveLock

    NSRecursiveLock 和前面提到的 @synchonized一样,是一个递归锁。

    NSRecursiveLock 与 NSLock 的区别在于内部封装的pthread_mutex_t 对象的类型不同,NSRecursiveLock 的类型被设置为 PTHREAD_MUTEX_RECURSIVE

    NSDistributedLock

    这里顺带提一下 NSDistributedLock, 是 macOS 下的一种锁.

    苹果文档 对于NSDistributedLock 的描述是:

    A lock that multiple applications on multiple hosts can use to restrict access to some shared resource, such as a file

    意思是说,它是一个用在多个主机间的多应用的锁,可以限制访问一些共享资源,例如文件。

    按字面意思翻译,NSDistributedLock 应该就叫做 分布式锁。但是看概念和资料,在 解决NSDistributedLock进程互斥锁的死锁问题(一) 里面看到,NSDistributedLock 更类似于文件锁的概念。 有兴趣的可以看一看 Linux 2.6 中的文件锁

    其它保证线程安全的方式

    除了用锁之外,有其它方法保证线程安全吗?

    使用单线程访问

    首先,尽量避免多线程的设计。因为多线程访问会出现很多不可控制的情况。有些情况即使上锁,也无法保证百分之百的安全,例如自旋锁的问题。

    不对资源做修改

    而如果还是得用多线程,那么避免对资源做修改。

    如果都是访问共享资源,而不去修改共享资源,也可以保证线程安全。

    比如NSArry作为不可变类是线程安全的。然而它们的可变版本,比如 NSMutableArray 是线程不安全的。事实上,如果是在一个队列中串行地进行访问的话,在不同线程中使用它们也是没有问题的。

    总结

    如果实在要使用多线程,也没有必要过分追求效率,而更多的考虑线程安全问题,使用对应的锁。

    对于平时编写应用里的多线程代码,还是建议用 @synchronized,NSLock 等,可读性和安全性都好,多线程安全比多线程性能更重要。



    作者:Cooci
    链接:https://www.jianshu.com/p/c557308c0ec5




    收起阅读 »

    iOS开发堆栈你理解多少?

    浅谈堆栈理解Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。1、栈区(stack):...
    继续阅读 »

    浅谈堆栈理解
    Objective-C的对象在内存中是以堆的方式分配空间的,并且堆内存是由你释放的,即release;

    栈由编译器管理自动释放的,在方法中(函数体)定义的变量通常是在栈内,因此如果你的变量要跨函数的话就需要将其定义为成员变量。

    1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等值。其操作方式类似于数据结构中的栈。
    2、堆区(heap):一般由程序员分配释放,若程序员不释放,则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样,其类是与链表。

    操作系统iOS 中应用程序使用的计算机内存不是统一分配空间,运行代码使用的空间在几个个不同的内存区域 。


    栈区(stack):
    1、存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;
    2、程序猿不需要管理栈区变量的内存; 栈区地址从高到低分配;

    堆区(heap):
    1、堆区的内存分配使用的是alloc;
    2、需要程序猿管理内存;
    3、ARC的内存的管理,是编译器再编译的时候自动添加 retain、release、autorelease;
    4、堆区的地址是从低到高分配)

    全局区/静态区(static):
    包括两个部分:未初始化过 、初始化过; 也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域; eg:int a;未初始化的。int a = 10;已初始化的。

    常量区:常量字符串cString等就是放在这里;

    代码区:存放App代码;

    例子:

    int a = 10;  全局初始化区
    char *p; 全局未初始化区

    main{
    int b; 栈区
    char s[] = "abcdef" 栈
    char *p1; 栈
    char *p2 = "qwerty"; \\\\qwerty在常量区,p2在栈上。
    static int c =0; 全局(静态)初始化区
    leap1 = (char *)malloc(100);
    leap2 = (char *)malloc(200);
    分配得来得100和200字节的区域就在堆区。
    }

    “stack”
    局部变量、参数、返回值都存在这里,函数调用开始会参数入栈、局部变量入栈;调用结束依次出栈。

    正如名称所示,stack 是后进先出(LIFO )结构。当函数调用其他的函数时,stack frame会被创建;当其他函数退出后,这个frame会自动被破坏。

    “heap”

    动态内存区域,使用alloc或new申请的内存;为了访问你创建在heap 中的数据,你最少要求有一个保存在stack中的指针,因为你要通过stack中的指针访问heap 中的数据。

    你可以认为stack 中的一个指针仅仅是一个整型变量,保存了heap 中特定内存地址的数据。实际上,它有一点点复杂,但这是它的基本结构。

    简而言之,操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。

    在iOS 操作系统的stack 段和heap 段中,一般来说你都可以创建数据对象。

    stack 对象的优点主要有两点,一是创建速度快,二是管理简单,它有严格的生命周期。stack 对象的缺点是它不灵活。创建时长度是多大就一直是多 大,创建时是哪个函数创建的,它的owner 就一直是它。不像heap 对象那样有多个owner ,其实多个owner 等同于引用计数。只有 heap 对象才是采用“引用计数”方法管理它。

    堆空间和栈空间的大小是可变的,堆空间从下往上生长,栈空间从上往下生长。

    stack 对象的创建

    只要栈的剩余空间大于 stack 对象申请创建的空间,操作系统就会为程序提供这段内存空间,否则将报异常提示栈溢出。

    heap 对象的创建

    操作系统对于内存heap 段是采用链表进行管理的。操作系统有一个记录空闲内存地址的链表,当收到程序的申请时,会遍历链表,寻找第一个空间大于所申请的heap 节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。

    例如:

    NSString 的对象就是 stack 中的对象,NSMutableString 的对象就是heap 中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,可有多个owner, 适用于计数管理内存管理模式。

    两类对象的创建方法也不同,前者直接创建 NSString * str1=@"welcome"; ,而后者需要先分配再初始化 NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];。

    引用计数是放在堆内存中的一个整型,对象alloc开辟堆内存空间后,引用计数自动置1;

    NSString直接赋值是创建在_TEXT段中,_TEXT段是在编译时保存程序代码段的机器码,也就是说NSString会以字符串的形式保存起来,只要字符串名称相同,其地址就相同,就算在新建一个名字一样的NSString,还是原来那个;顺便讲一下_DATA段,他是保存全局变量和静态变量的值的)

    _TEXT段:整个程序的代码,以及所有的常量。这部分内存是是固定大小的,只读的。
    _DATA段:初始化为非零值的全局变量。
    BSS段:初始化为0或未初始化的全局变量和静态变量。
    更多细节我后面会讲一篇Mach-O内核方面的文章;

    静态和全局的区别

    static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;

    static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;

    static函数与普通函数有什么区别:static函数与普通函数作用域不同,只在定义该变量的源文件内有效;

    全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。

    补充:内存引用计数的实现

    GNUstep的实现是将引用计数保存在对象占用内存块头部的变量中

    好处是:

    少量的代码即可完成。

    能够统一管理引用计数内存块和对象引用计数内存块

    苹果的实现是保存在引用计数hash表中

    好处是:

    对象用内存块的分配无需考虑内存块的头部

    引用计数表各记录中存有内存块地址,可以从各个记录追溯到各对象的内存块,这点对调试非常重要

    weak对象释放是自动致nil实现:

    也是通过一个weakhash表实现的,将weak的对象地址注册到weakhash表中,如果该对象被destroy销毁,则在weak表中将该对象地址致nil,并清除记录

    链接:https://www.jianshu.com/p/1f075bdc2e29

    收起阅读 »

    浅谈Android插件化

    一、认识插件化1.1 插件化起源插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。想必大家都知道,在 Android ...
    继续阅读 »

    一、认识插件化

    1.1 插件化起源

    插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主。

    想必大家都知道,在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已。

    常见的应用安装目录有:

    • /system/app:系统应用
    • /system/priv-app:系统应用
    • /data/app:用户应用

    那可能大家会想问,既然安装这个过程如此简单,Android 是怎么运行应用中的代码的呢,我们先看 Apk 的构成,一个常见的 Apk 会包含如下几个部分:

    • classes.dexJava 代码字节码
    • res:资源文件
    • libso 文件
    • assets:静态资产文件
    • AndroidManifest.xml:清单文件

    其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已。

    那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?

    1.2 插件化优点

    插件化让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益:

    • 减少安装Apk的体积、按需下载模块
    • 动态更新插件
    • 宿主和插件分开编译,提升开发效率
    • 解决方法数超过65535的问题

    想象一下,你的应用拥有 Native 应用一般极高的性能,又能获取诸如 Web 应用一样的收益。

    嗯,理想很美好不是嘛?

    1.3 与组件化的区别

    • 组件化:是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。
    • 插件化:是将整个App拆分成很多模块,每个模块都是一个Apk(组件化的每个模块是一个lib),最终打包的时候将宿主Apk和插件Apk分开打包,只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk。

    二、插件化的技术难点

    想让插件的Apk真正运行起来,首先要先能找到插件Apk的存放位置,然后我们要能解析加载Apk里面的代码。

    但是光能执行Java代码是没有意义的,在Android系统中有四大组件是需要在系统中注册的,具体来说是在 Android 系统的 ActivityManagerService (AMS) 和 PackageManagerService (PMS) 中注册的,而四大组件的解析和启动都需要依赖 AMS 和 PMS,如何欺骗系统,让他承认一个未安装的 Apk 中的组件,如何让宿主动态加载执行插件Apk中 Android 组件(即 ActivityServiceBroadcastReceiverContentProviderFragment)等是插件化最大的难点。

    另外,应用资源引用(特指 R 中引用的资源,如 layoutvalues 等)也是一大问题,想象一下你在宿主进程中使用反射加载了一个插件 Apk,代码中的 R 对应的 id 却无法引用到正确的资源,会产生什么后果。

    总结一下,其实做到插件化的要点就这几个:

    • 如何加载并执行插件 Apk 中的代码(ClassLoader Injection
    • 让系统能调用插件 Apk 中的组件(Runtime Container
    • 正确识别插件 Apk 中的资源(Resource Injection

    当然还有其他一些小问题,但可能不是所有场景下都会遇到,我们后面再单独说。

    三、ClassLoader Injection

    ClassLoader 是插件化中必须要掌握的,因为我们知道Android 应用本身是基于魔改的 Java 虚拟机的,而由于插件是未安装的 apk,系统不会处理其中的类,所以需要使用 ClassLoader 加载 Apk,然后反射里面的代码。

    3.1 java 中的 ClassLoader

    • BootstrapClassLoader 负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等

    • ExtensionClassLoader 负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包

    • AppClassLoader 负责加载 classpath 里的 jar 包和目录

    3.2 android 中的 ClassLoader

    在Android系统中ClassLoader是用来加载dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件

    • PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

      public class PathClassLoader extends BaseDexClassLoader {
      public PathClassLoader(String dexPath, ClassLoader parent) {
      super(dexPath, null, null, parent);
      }

      public PathClassLoader(String dexPath, String libraryPath,
      ClassLoader parent) {
      super(dexPath, null, libraryPath, parent);
      }
      }

    • DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

      public class DexClassLoader extends BaseDexClassLoader {
      public DexClassLoader(String dexPath, String optimizedDirectory,
      String libraryPath, ClassLoader parent) {
      super(dexPath, new File(optimizedDirectory), libraryPath, parent);
      }
      }

    我们在插件化中一般使用的是 DexClassLoader。

    3.3 双亲委派机制

    每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。

        protected Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
    try {
    if (parent != null) {
    // 先从父类加载器中进行加载
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }

    if (c == null) {
    // 没有找到,再自己加载
    c = findClass(name);
    }
    }
    return c;
    }

    3.4 如何加载插件中的类

    要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要哪些参数。

    public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    // ...
    }
    }

    构造函数需要四个参数: dexPath 是需要加载的 dex / apk / jar 文件路径 optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置 librarySearchPath 是 native 依赖的位置 parent 就是父类加载器,默认会先从 parent 加载对应的类

    创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:

        // 从 assets 中拿出插件 apk 放到内部存储空间
    private fun extractPlugin() {
    var inputStream = assets.open("plugin.apk")
    File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init() {
    extractPlugin()
    pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
    nativeLibDir = File(filesDir, "pluginlib").absolutePath
    dexOutPath = File(filesDir, "dexout").absolutePath
    // 生成 DexClassLoader 用来加载插件类
    pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
    }

    3.5 执行插件类的方法

    通过反射来执行类的方法

    val loadClass = pluginClassLoader.loadClass(activityName)
    loadClass.getMethod("test",null).invoke(loadClass)

    我们称这个过程叫做 ClassLoader 注入。完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载,而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了。

    四、Runtime Container

    我们之前说到 Activity 插件化最大的难点是如何欺骗系统,让他承认一个未安装的 Apk 中的组件。 因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的 Manifest 文件中,而没有在 Manifest 中注册的四大组件是不能和系统直接进行交互的。 如果直接把插件的 Activity 注册到宿主 Manifest 里就失去了插件化的动态特性,因为每次插件中新增 Activity 都要修改宿主 Manifest 并且重新打包,那就和直接写在宿主中没什么区别了。

    4.1 为什么没有注册的 Activity 不能和系统交互

    这里的不能直接交互的含义有两个

    1. 系统会检测 Activity 是否注册 如果我们启动一个没有在 Manifest 中注册的 Activity,会发现报如下 error:

      android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?


    这个 log 在 Instrumentation 的 checkStartActivityResult 方法中可以看到:

    public class Instrumentation {
    public static void checkStartActivityResult(int res, Object intent) {
    if (!ActivityManager.isStartResultFatalError(res)) {
    return;
    }

    switch (res) {
    case ActivityManager.START_INTENT_NOT_RESOLVED:
    case ActivityManager.START_CLASS_NOT_FOUND:
    if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
    throw new ActivityNotFoundException(
    "Unable to find explicit activity class "
    + ((Intent)intent).getComponent().toShortString()
    + "; have you declared this activity in your AndroidManifest.xml?");
    throw new ActivityNotFoundException(
    "No Activity found to handle " + intent);
    ...
    }
    }
    }


    1. Activity 的生命周期无法被调用,其实一个 Activity 主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了 Manifest 注册文件,启动 Activity 被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件 Activity 也就不能正常运行了。

    4.2 运行时容器技术

    由于Android中的组件(Activity,Service,BroadcastReceiver和ContentProvider)是由系统创建的,并且由系统管理生命周期。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。插件化如何支持组件生命周期的管理。 大致分为两种方式:

    • 运行时容器技术(ProxyActivity代理)
    • 预埋StubActivity,hook系统启动Activity的过程

    我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它。

    它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:

    • pluginName
    • pluginApkPath
    • pluginActivityName

    等,其实最重要的就是 pluginApkPath 和 pluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoaderResource,并反射 pluginActivityName 对应的 Activity 类。当完成加载后,ContainerActivity 要做两件事:

    • 转发所有来自系统的生命周期回调至插件 Activity
    • 接受 Activity 方法的系统调用,并转发回系统

    我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity

    public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    String pluginActivityName = getIntent().getString("pluginActivityName", "");
    pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
    if (pluginActivity == null) {
    super.onCreate(savedInstanceState);
    return;
    }

    pluginActivity.onCreate();
    }

    @Override
    protected void onResume() {
    if (pluginActivity == null) {
    super.onResume();
    return;
    }
    pluginActivity.onResume();
    }

    @Override
    protected void onPause() {
    if (pluginActivity == null) {
    super.onPause();
    return;
    }
    pluginActivity.onPause();
    }

    // ...
    }

    public class PluginActivity {

    private ContainerActivity containerActivity;

    public PluginActivity(ContainerActivity containerActivity) {
    this.containerActivity = containerActivity;
    }

    @Override
    public T findViewById(int id) {
    return containerActivity.findViewById(id);
    }
    // ...
    }

    // 插件 `Apk` 中真正写的组件
    public class TestActivity extends PluginActivity {
    // ......
    }

    是不是感觉有点看懂了,虽然真正搞的时候还有很多小坑,但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。

    4.3 字节码替换

    该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。

    class TestActivity extends Activity {}
    ->
    class TestActivity extends PluginActivity {}

    有没有什么办法能让插件组件的编写与原来没有任何差别呢?

    Shadow 的做法是字节码替换插件,这是一个非常棒的想法,简单来说,Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换,具体怎么做的不说了,可以自己看文档。

    实现的功能嘛,就是用户配置 Gradle 插件后,正常开发,依然编写:

    class TestActivity extends Activity {}
    然后完成编译后,最后的字节码中,显示的却是:
    class TestActivity extends PluginActivity {}

    到这里基本的框架就差不多结束了。

    五、Resource Injection

    最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layoutvalues 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id

    资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:

    • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的 PackageInfo
    • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例

    我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:

    PackageManager packageManager = getPackageManager();
    PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,
    PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
    );
    packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
    packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

    Resources injectResources = null;
    try {
    injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
    } catch (PackageManager.NameNotFoundException e) {
    // ...
    }

    拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:

    public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources, Resources injectResources) {
    super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
    this.hostResources = hostResources;
    this.injectResources = injectResources;
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
    try {
    return injectResources.getString(id, formatArgs);
    } catch (NotFoundException e) {
    return hostResources.getString(id, formatArgs);
    }
    }

    // ...
    }

    然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:

    public class ContainerActivity extends Activity {
    private Resources pluginResources;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    // ...
    pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
    // ...
    }

    @Override
    public Resources getResources() {
    if (pluginActivity == null) {
    return super.getResources();
    }
    return pluginResources;
    }
    }

    这样就完成了资源的注入。

    收起阅读 »

    APP路由框架与组件化简析

    前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。路由的概...
    继续阅读 »

    前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。

    路由的概念

    路由这个词本身应该是互联网协议中的一个词,维基百科对此的解释如下:

    路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

    个人理解,在前端开发中,路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme,并生成路由表,然后,能够根据外部输入字符串在路由表中匹配到对应的页面或者服务,进行跳转或者调用,并提供会获取返回值等,示意如下

    image.png

    所以一个基本路由框架要具备如下能力:

      1. APP路由的扫描及注册逻辑
      1. 路由跳转target页面能力
      1. 路由调用target服务能力

    APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。

    三方路由框架是否是APP强需求

    答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。

    Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。

    原生路由的限制:功能单一,扩展灵活性差,不易协同

    传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:


    import com.snail.activityforresultexample.test.SecondActivity;

    public class MainActivity extends AppCompatActivity {

    void jumpSecondActivityUseClassName(){

    Intent intent =new Intent(MainActivity.this, SecondActivity.class);
    startActivity(intent);
    }

    显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。

    第一步:manifest中配置activity的intent-filter,至少要配置一个action
















    第二步:调用

    void jumpSecondActivityUseFilter() {
    Intent intent = new Intent();
    intent.setAction("com.snail.activityforresultexample.SecondActivity");
    startActivity(intent);
    }

    如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:

    • 首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。
    • 其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。
    • 最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。

    可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的

    APP三方路由框架需具备的能力

    目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:

    • 路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑
    • scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离
    • 基础路由跳转能力 :页面跳转能力的支持
    • 服务类组件的支持 :如去某个服务组件获取一些配置等
    • [扩展]路由拦截逻辑:比如登陆,统一鉴权
    • 可定制的降级逻辑:找不到组件时的兜底

    可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,

    	@Route(path = "/test/activity2")
    public class Test2Activity extends AppCompatActivity {
    ...
    }

    build阶段会根据注解搜集路由scheme,生成路由表。第二步使用

            ARouter.getInstance()
    .build("/test/activity2")
    .navigation(this);

    如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。

    APP路由框架的实现

    路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询

    路由表的自动生成

    生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,

    image.png

    不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:

    image.png

    其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。

    JavaPoet如何搜集并生成路由表集合?

    以ARouter框架为例,先定义Router框架需要的注解如:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.CLASS)
    public @interface Route {

    /**
    * Path of route
    */

    String path();

    该注解用于标注需要路由的组件,用法如下:

    @Route(path = "/test/activity1", name = "测试用 Activity")
    public class Test1Activity extends BaseActivity {
    @Autowired
    int age = 10;

    之后利用APT扫描所有被注解的类,生成路由表,实现参考如下:

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
    if (CollectionUtils.isNotEmpty(annotations)) {

    Set routeElements = roundEnv.getElementsAnnotatedWith(Route.class);

    this.parseRoutes(routeElements);
    ...
    return false;
    }


    private void parseRoutes(Set routeElements) throws IOException {
    ...
    // Generate groups
    String groupFileName = NAME_OF_GROUP + groupName;
    JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
    TypeSpec.classBuilder(groupFileName)
    .addJavadoc(WARNING_TIPS)
    .addSuperinterface(ClassName.get(type_IRouteGroup))
    .addModifiers(PUBLIC)
    .addMethod(loadIntoMethodOfGroupBuilder.build())
    .build()
    ).build().writeTo(mFiler);

    产物如下:包含路由表,及局部注册入口。

    image.png

    自动注册:ASM搜集上述路由表并聚合插入Init代码区

    为了能够插入到Init代码区,首先需要预留一个位置,一般定义一个空函数,以待后续填充:

    	public class RouterInitializer {

    public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
    ...
    loadRouterTables();
    }
    //自动注册代码
    public static void loadRouterTables() {

    }
    }

    首先利用AOP工具,遍历上述APT中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,

    • 搜集目标,聚合路由表

        /**扫描jar*/
      fun scanJar(jarFile: File, dest: File?) {

      val file = JarFile(jarFile)
      var enumeration = file.entries()
      while (enumeration.hasMoreElements()) {
      val jarEntry = enumeration.nextElement()
      if (jarEntry.name.endsWith("XXRouterTable.class")) {
      val inputStream = file.getInputStream(jarEntry)
      val classReader = ClassReader(inputStream)
      if (Arrays.toString(classReader.interfaces)
      .contains("IHTRouterTBCollect")
      ) {
      tableList.add(
      Pair(
      classReader.className,
      dest?.absolutePath
      )
      )
      }
      inputStream.close()
      } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
      registerInitClass = dest
      }
      }
      file.close()
      }

    • 对目标Class注入路由表初始化代码

        fun asmInsertMethod(originFile: File?) {

      val optJar = File(originFile?.parent, originFile?.name + ".opt")
      if (optJar.exists())
      optJar.delete()
      val jarFile = JarFile(originFile)
      val enumeration = jarFile.entries()
      val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

      while (enumeration.hasMoreElements()) {
      val jarEntry = enumeration.nextElement()
      val entryName = jarEntry.getName()
      val zipEntry = ZipEntry(entryName)
      val inputStream = jarFile.getInputStream(jarEntry)
      //插桩class
      if (entryName.endsWith("RouterInitializer.class")) {
      //class文件处理
      jarOutputStream.putNextEntry(zipEntry)
      val classReader = ClassReader(IOUtils.toByteArray(inputStream))
      val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
      val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
      classReader.accept(cv, EXPAND_FRAMES)
      val code = classWriter.toByteArray()
      jarOutputStream.write(code)
      } else {
      jarOutputStream.putNextEntry(zipEntry)
      jarOutputStream.write(IOUtils.toByteArray(inputStream))
      }
      jarOutputStream.closeEntry()
      }
      //结束
      jarOutputStream.close()
      jarFile.close()
      if (originFile?.exists() == true) {
      Files.delete(originFile.toPath())
      }
      optJar.renameTo(originFile)
      }

    最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码:

     public static void loadRouterTables() {


    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
    register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
    register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
    ...
    }

    如此就完成了路由表的搜集与注册,大概的流程就是如此。当然对于支持服务、Fragment等略有不同,但大体类似。

    Router框架对服务类组件的支持

    通过路由的方式获取服务属于APP路由比较独特的能力,比如有个用户中心的组件,我们可以通过路由的方式去查询用户是否处于登陆状态,这种就不是狭义上的页面路由的概念,通过一串字符串如何查到对应的组件并调用其方法呢?这种的实现方式也有多种,每种实现方式都有自己的优劣。

    • 一种是可以将服务抽象成接口,沉到底层,上层实现通过路由方式映射对象
    • 一种是将实现方法直接通过路由方式映射

    先看第一种,这种事Arouter的实现方式,它的优点是所有对外暴露的服务都暴露接口类【沉到底层】,这对于外部的调用方,也就是服务使用方非常友好,示例如下:

    先定义抽象服务,并沉到底层

    image.png

    public interface HelloService extends IProvider {
    void sayHello(String name);
    }

    实现服务,并通过Router注解标记

    @Route(path = "/yourservicegroupname/hello")
    public class HelloServiceImpl implements HelloService {
    Context mContext;

    @Override
    public void sayHello(String name) {
    Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }

    使用:利用Router加scheme获取服务实例,并映射成抽象类,然后直接调用方法。

      ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

    这种实现方式对于使用方其实是很方便的,尤其是一个服务有多个可操作方法的时候,但是缺点是扩展性,如果想要扩展方法,就要改动底层库。

    再看第二种:将实现方法直接通过路由方式映射

    服务的调用都要落到方法上,参考页面路由,也可以支持方法路由,两者并列关系,所以组要增加一个方法路由表,实现原理与Page路由类似,跟上面的Arouter对比,不用定义抽象层,直接定义实现即可:

    定义Method的Router

    	public class HelloService {


    @MethodRouter(url = {"arouter://sayhello"})
    public void sayHello(String name) {
    Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
    }

    使用即可

     RouterCall.callMethod("arouter://sayhello?name=hello");

    上述的缺点就是对于外部调用有些复杂,尤其是处理参数的时候,需要严格按照协议来处理,优点是,没有抽象层,如果需要扩展服务方法,不需要改动底层。

    上述两种方式各有优劣,不过,如果从左服务组件的初衷出发,第一种比较好:对于调用方比较友好。另外对于CallBack的支持,Arouter的处理方式可能也会更方便一些,可以比较方便的交给服务方定义。如果是第二种,服务直接通过路由映射的方式,处理起来就比较麻烦,尤其是Callback中的参数,可能要统一封装成JSON并维护解析的协议,这样处理起来,可能不是很好。

    路由表的匹配

    路由表的匹配比较简单,就是在全局Map中根据String输入,匹配到目标组件,然后依赖反射等常用操作,定位到目标。

    组件化与路由的关系

    组件化是一种开发集成模式,更像一种开发规范,更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件,这些组件之间可能是不同的团队,处于不同的目的在各自维护,甚至是需要代码隔离,如果牵扯到组件间的调用与通信,就不可避免的借助路由,因为实现隔离的,只能采用通用字符串scheme进行通信,这就是路由的功能范畴。

    组件化需要路由支撑的根本原因:组件间代码实现的隔离

    总结

    • 路由不是一个APP的必备功能,但是大型跨团队的APP基本都需要
    • 路由框架的基本能力:路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能
    • 组件化与路由的关系:组件化的代码隔离导致路由框架成为必须
    收起阅读 »

    MVVMFrame for Android 是一个基于Google官方推出的JetPack(Lifecycle,LiveData,ViewModel,Room)构建的快速开发框架,从此构建一个MVVM模式的项目变得快捷简单。

    MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开...
    继续阅读 »

    MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开发框架。有了 MVVMFrame 的加持,从此构建一个 MVVM 模式的项目变得快捷简单。

    架构

    Image

    Android version

    引入

    由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

    1. 在Project的 build.gradle 里面添加远程仓库
    allprojects {
    repositories {
    //...
    mavenCentral()
    }
    }
    1. 在Module的 build.gradle 里面添加引入依赖项

    v2.x(使用 Hilt 简化 Dagger2 依赖注入用法)

    //AndroidX 版本
    implementation 'com.github.jenly1314:mvvmframe:2.1.0'

    以前发布至JCenter的版本

    v2.0.0(使用 Hilt 简化 Dagger2 依赖注入用法)

    //AndroidX 版本
    implementation 'com.king.frame:mvvmframe:2.0.0'

    v1.x 以前版本(使用 Dagger2)

    //AndroidX 版本
    implementation 'com.king.frame:mvvmframe:1.1.4'

    //Android Support版本
    implementation 'com.king.frame:mvvmframe:1.0.2'

    Dagger和 Room 的相关注解处理器

    你需要引入下面的列出的编译时的注解处理器,用于自动生成相关代码。其它对应版本具体详情可查看 Versions

    v2.x 版本($versions 相关可查看Versions

    你需要在项目根目录的 build.gradle 文件中配置 Hilt 的插件路径:

    buildscript {
    ...
    dependencies {
    ...
    classpath "com.google.dagger:hilt-android-gradle-plugin:$versions.daggerHint"
    }
    }

    接下来,在 app/build.gradle 文件中,引入 Hilt 的插件和相关依赖:

    ...
    apply plugin: 'dagger.hilt.android.plugin'

    dependencies{
    ...

    //AndroidX ------------------ MVVMFrame v2.x.x
    //lifecycle
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle"
    //room
    annotationProcessor "androidx.room:room-compiler:$versions.room"
    //hilt
    implementation "com.google.dagger:hilt-android:$versions.daggerHint"
    annotationProcessor "com.google.dagger:hilt-android-compiler:$versions.daggerHint"

    //从2.1.0以后已移除
    // implementation "androidx.hilt:hilt-lifecycle-viewmodel:$versions.hilt"
    // annotationProcessor "androidx.hilt:hilt-compiler:$versions.hilt"
    }

    v1.x 以前版本,建议 查看分支版本

    在 app/build.gradle 文件中引入 Dagger 和 Room 相关依赖:


    dependencies{
    ...

    //AndroidX ------------------ MVVMFrame v1.1.4
    //dagger
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.30.1'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.30.1'
    //room
    annotationProcessor 'androidx.room:room-compiler:2.2.5'
    }

    dependencies{
    ...

    // Android Support ------------------ MVVMFrame v1.0.2
    //dagger
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.19'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
    //room
    annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
    }

    如果你的项目使用的是 Kotlin,记得加上 kotlin-kapt 插件,并需使用 kapt 替代 annotationProcessor

    MVVMFrame引入的库(具体对应版本请查看 Versions

        //appcompat
    compileOnly deps.appcompat

    //retrofit
    api deps.retrofit.retrofit
    api deps.retrofit.gson
    api deps.retrofit.converter_gson

    //retrofit-helper
    api deps.jenly.retrofit_helper

    //lifecycle
    api deps.lifecycle.runtime
    api deps.lifecycle.extensions
    annotationProcessor deps.lifecycle.compiler

    //room
    api deps.room.runtime
    annotationProcessor deps.room.compiler

    //hilt
    compileOnly deps.dagger.hilt_android
    annotationProcessor deps.dagger.hilt_android_compiler

    compileOnly deps.hilt.hilt_viewmodel
    annotationProcessor deps.hilt.hilt_compiler

    //log
    api deps.timber

    示例

    集成步骤代码示例 (示例出自于app中)

    Step.1 启用DataBinding,在你项目中的build.gradle的android{}中添加配置:

    Android Studio 4.x 以后版本

    buildFeatures{
    dataBinding = true
    }

    Android Studio 4.x 以前版本

    dataBinding {
    enabled true
    }

    Step.2 使用JDK8编译(v1.1.2新增),在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    Step.3 自定义全局配置(继承MVVMFrame中的FrameConfigModule)(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

    /**
    * 自定义全局配置
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    public class AppConfigModule extends FrameConfigModule {
    @Override
    public void applyOptions(Context context, ConfigModule.Builder builder) {
    builder.baseUrl(Constants.BASE_URL)//TODO 配置Retrofit中的baseUrl
    .retrofitOptions(new RetrofitOptions() {
    @Override
    public void applyOptions(Retrofit.Builder builder) {
    //TODO 配置Retrofit
    //如想使用RxJava
    //builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    }
    })
    .okHttpClientOptions(new OkHttpClientOptions() {
    @Override
    public void applyOptions(OkHttpClient.Builder builder) {
    //TODO 配置OkHttpClient
    }
    })
    .gsonOptions(new GsonOptions() {
    @Override
    public void applyOptions(GsonBuilder builder) {
    //TODO 配置Gson
    }
    })
    .roomDatabaseOptions(new RoomDatabaseOptions<RoomDatabase>() {
    @Override
    public void applyOptions(RoomDatabase.Builder<RoomDatabase> builder) {
    //TODO 配置RoomDatabase
    }
    });
    }
    }

    Step.4 在你项目中的AndroidManifest.xml中通过配置meta-data来自定义全局配置(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

    <!-- MVVMFrame 全局配置 -->
    <meta-data android:name="com.king.mvvmframe.config.AppConfigModule"
    android:value="FrameConfigModule"/>

    Step.5 关于Application

    2.x版本 因为从2.x开始使用到了Hilt,所以你自定义的Application需加上 @HiltAndroidApp 注解,这是使用Hilt的一个必备前提。示例如下:

       @HiltAndroidApp
    public class YourApplication extends Application {

    }

    1.x版本 将你项目的 Application 继承MVVMFrame中的 BaseApplication

    /**
    * MVVMFrame 框架基于Google官方的Architecture Components dependencies 构建,在使用MVVMFrame时,需遵循一些规范:
    * 1.你的项目中的Application中需初始化MVVMFrame框架相关信息,有两种方式处理:
    * a.直接继承本类{@link BaseApplication}即可;
    * b.如你的项目中的Application本身继承了其它第三方的Application,因为Java是单继承原因,导致没法继承本类,可参照{@link BaseApplication}类,
    * 将{@link BaseApplication}中相关代码复制到你项目的Application中,在相应的生命周期中调用即可。
    *
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    public class App extends BaseApplication {

    @Override
    public void onCreate() {
    //TODO 如果默认配置已经能满足你的需求,你不需要自定义配置,可以通过下面注释掉的方式设置 BaseUrl,从而可以省略掉 step3 , setp4 两个步骤。
    // RetrofitHelper.getInstance().setBaseUrl(baseUrl);
    super.onCreate();
    //开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
    ApplicationComponent appComponent = DaggerApplicationComponent.builder()
    .appComponent(getAppComponent())
    .build();
    //注入
    appComponent.inject(this);

    }


    }

    其他

    关于v2.x

    因为v2.x版本 使用了 Hilt 的缘故,简化了之前 Dagger2 的用法,建议在新项目中使用。如果是从 v1.x 升级到 v2.x,集成步骤稍有变更,详情请查看 Step.5,并且可能还需要删除以前 @Component@Module等注解桥接层相关的逻辑代码,因为从v2.x开始,这些桥接逻辑无需自己编写,全部交由 Hilt 处理。

    关于使用 Hilt

    Hilt 是JetPack中新增的一个依赖注入库,其基于Dagger2研发(后面统称为Dagger),但它不同于Dagger。对于Android开发者来说,Hilt可以说专门为Android 打造。

    之前使用的Dagger for Android虽然也是针对于Android打造,也能通过 @ContributesAndroidInjector 来通过生成简化一部分样板代码,但是感觉还不够彻底。因为 Component 层相关的桥接还是要自己写。Hilt的诞生改善了这些问题。

    Hilt 大幅简化了Dagger 的用法,使得我们不用通过 @Component 注解去编写桥接层的逻辑,但是也因此限定了注入功能只能从几个 Android 固定的入口点开始,

    Hilt 一共支持 6 个入口点,分别是:

    Application

    Activity

    Fragment

    View

    Service

    BroadcastReceiver

    其中,只有 Application 这个入口点是使用 @HiltAndroidApp 注解来声明,示例如下

    Application 示例

       @HiltAndroidApp
    public class YourApplication extends Application {

    }

    其他的所有入口点,都是用 @AndroidEntryPoint 注解来声明,示例如下

    Activity 示例

       @AndroidEntryPoint
    public class YourActivity extends BaseActivity {

    }

    Fragment 示例

       @AndroidEntryPoint
    public class YourFragment extends BaseFragment {

    }

    Service 示例

       @AndroidEntryPoint
    public class YourService extends BaseService {

    }

    BroadcastReceiver 示例

       @AndroidEntryPoint
    public class YourBroadcastReceiver extends BaseBroadcastReceiver {

    }

    其它示例

    BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在类上添加 @HiltViewModel 并在构造函数上添加 @Inject 注解)

       @HiltViewModel
    public class YourViewModel extends BaseViewModel<YourModel> {
    @Inject
    public DataViewModel(@NonNull Application application, YourModel model) {
    super(application, model);
    }
    }

    BaseModel 示例 (如果您继承使用了BaseModel或其子类,你需要参照如下方式在构造函数上添加 @Inject 注解)

       public class YourModel extends BaseModel {
    @Inject
    public BaseModel(IDataRepository dataRepository){
    super(dataRepository);
    }
    }

    如果使用的是 v2.0.0 版本 (使用 androidx.hilt:hilt-lifecycle-viewmodel 的方式)

    BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在构造函数上添加 @ViewModelInject 注解)

       public class YourViewModel extends BaseViewModel<YourModel> {
    @ViewModelInject
    public DataViewModel(@NonNull Application application, YourModel model) {
    super(application, model);
    }
    }

    关于使用 Dagger

    之所以特意说 Dagger 是因为Dagger的学习曲线相对陡峭一点,没那么容易理解。

    1. 如果你对 Dagger 很了解,那么你将会更加轻松的去使用一些注入相关的骚操作。

    因为 MVVMFrame 中使用到了很多 Dagger 注入相关的一些操作。所以会涉及Dagger相关技术知识。

    但是并不意味着你一定要会使用 Dagger,才能使用MVVMFrameComponent

    如果你对 Dagger 并不熟悉,其实也是可以用的,因为使用 Dagger 全局注入主要都已经封装好了。你只需参照Demo 中的示例,照葫芦画瓢。 主要关注一些继承了BaseActivityBaseFragmentBaseViewModel等相关类即可。

    这里列一些主要的通用注入参照示例:(下面Dagger相关的示例仅适用于v1.x版本,因为v2.x已基于Hilt编写,简化了Dagger依赖注入桥接层相关逻辑)

    直接或间接继承了 BaseActivity 的配置示例:

    /**
    * Activity模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(subcomponents = BaseActivitySubcomponent.class)
    public abstract class ActivityModule {

    @ContributesAndroidInjector
    abstract MainActivity contributeMainActivity();

    }

    直接或间接继承了 BaseFragment 的配置示例:

    /**
    * Fragment模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(subcomponents = BaseFragmentSubcomponent.class)
    public abstract class FragmentModule {

    @ContributesAndroidInjector
    abstract MainFragment contributeMainFragment();

    }

    直接或间接继承了 BaseViewModel 的配置示例:

    /**
    * ViewModel模块统一管理:通过{@link Binds}和{@link ViewModelKey}绑定关联对应的ViewModel
    * ViewModelModule 例子
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module
    public abstract class ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(MainViewModel.class)
    abstract ViewModel bindMainViewModel(MainViewModel viewModel);
    }

    ApplicationModule 的配置示例

    /**
    * Application模块:为{@link ApplicationComponent}提供注入的各个模块
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @Module(includes = {ViewModelFactoryModule.class,ViewModelModule.class,ActivityModule.class,FragmentModule.class})
    public class ApplicationModule {

    }

    ApplicationComponent 的配置示例

    /**
    * @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
    */
    @ApplicationScope
    @Component(dependencies = AppComponent.class,modules = {ApplicationModule.class})
    public interface ApplicationComponent {
    //指定你的 Application 继承类
    void inject(App app);
    }

    通过上面的通用配置注入你所需要的相关类之后,如果配置没什么问题,你只需 执行Make Project 一下,或通过 Make Project 快捷键 Ctrl + F9 ,就可以自动生产相关代码。 比如通过 ApplicationComponent 生成的 DaggerApplicationComponent 类。

    然后在你的 Application 集成类 App 中通过 DaggerApplicationComponent 构建 ApplicationComponent,然后注入即可。

        //开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
    ApplicationComponent appComponent = DaggerApplicationComponent.builder()
    .appComponent(getAppComponent())
    .build();
    //注入
    appComponent.inject(this);

    你也可以直接查看app中的源码示例

    关于设置 BaseUrl

    目前通过设置 BaseUrl 的入口主要有两种:

    1.一种是通过在 Manifest 中配置 meta-data 的来自定义 FrameConfigModule,在里面 通过 {@link ConfigModule.Builder#baseUrl(String)}来配置 BaseUrl。(一次设置,全局配置)

    2.一种就是通过RetrofitHelper {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来配置 BaseUrl。(可多次设置,动态全局配置,有前提条件)

    以上两种配置 BaseUrl 的方式都可以达到目的。但是你可以根据不同的场景选择不同的配置方式。

    主要场景与选择如下:

    一般场景:对于只使用单个不变的 BaseUrl的

    场景1:如果本库的默认已满足你的需求,无需额外自定义配置的。
         选择:建议你直接使用 {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来初始化 BaseUrl,切记在框架配置初始化 BaseUrl之前,建议在你自定义的 {@link Application#onCreate()}中初始化。
    场景2:如果本库的默认配置不满足你的需求,你需要自定义一些配置的。(比如需要使用 RxJava相关)
         选择:建议你在自定义配置中通过 {@link ConfigModule.Builder#baseUrl(String)} 来初始化 BaseUrl。

    二般场景:对于只使用单个 BaseUrl 但是,BaseUrl中途会变动的。

    场景3:和一般场景一样,也能分两种,所以选择也和一般场景也可以是一样的。
         选择:两种选择都行,但当 BaseUrl需要中途变动时,还需将 {@link RetrofitHelper#setDynamicDomain(boolean)} 设置为 {@code true} 才能支持动态改变 BaseUrl。

    特殊场景:对于支持多个 BaseUrl 且支持动态可变的。

       选择:这个场景的选择,主要涉及到另外的方法,请查看 {@link RetrofitHelper#putDomain(String, String)} 和 {@link RetrofitHelper#putDomain(String, HttpUrl)}相关详情

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档


    代码下载:MVVMFrame.zip

    收起阅读 »

    RetrofitHelper是一个支持配置多个BaseUrl,支持动态改变BaseUrl,动态配置超时时长的Retrofit帮助类

    RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。 支持配置多个BaseUrl 支持动态改变BaseUrl 支持动态配置超时时长 支持添加公...
    继续阅读 »


    RetrofitHelper

    RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。

    主要功能介绍

    •  支持配置多个BaseUrl
    •  支持动态改变BaseUrl
    •  支持动态配置超时时长
    •  支持添加公共请求头

    Gif 展示

    Image

    引入

    由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

    1. 在Project的 build.gradle 里面添加远程仓库
    allprojects {
    repositories {
    //...
    mavenCentral()
    }
    }
    1. 在Module的 build.gradle 里面添加引入依赖项
    //AndroidX 版本
    implementation 'com.github.jenly1314:retrofit-helper:1.0.1'

    RetrofitHelper引入的库(具体对应版本请查看 Versions

        compileOnly "androidx.appcompat:appcompat:$versions.appcompat"
    compileOnly "com.squareup.retrofit2:retrofit:$versions.retrofit"

    因为 RetrofitHelper 的依赖只在编译时有效,并未打入包中,所以您的项目中必须依赖上面列出相关库

    示例

    主要集成步骤代码示例

    Step.1 需使用JDK8编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    Step.2 通过RetrofitUrlManager初始化OkHttpClient,进行初始化配置

    //通过RetrofitHelper创建一个支持多个BaseUrl的 OkHttpClient
    //方式一
    val okHttpClient = RetrofitHelper.getInstance()
    .createClientBuilder()
    //...你自己的其他配置
    .build()
    //方式二
    val okHttpClient = RetrofitHelper.getInstance()
    .with(builder)
    //...你自己的其他配置
    .build()
    //完整示例
    val okHttpClient = RetrofitHelper.getInstance()
    .createClientBuilder()
    .addInterceptor(LogInterceptor())
    .build()
    val retrofit = Retrofit.Builder()
    .baseUrl(Constants.BASE_URL)
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create(Gson()))
    .build()

    Step.3 定义接口时,通过注解标记对应接口,支持动态改变 BaseUrl相关功能

     interface ApiService {

    /**
    * 接口示例,没添加任何标识,和常规使用一致
    * @return
    */
    @GET("api/user")
    fun getUser(): Call<User>


    /**
    * Retrofit默认返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
    * @return
    */
    @DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
    @Timeout(connectTimeout = 15,readTimeout = 15,writeTimeout = 15,timeUnit = TimeUnit.SECONDS) //超时标识,用于自定义超时时长
    @GET("api/user")
    fun getUser(): Call<User>

    /**
    * 动态改变 BaseUrl
    * @return
    */
    @BaseUrl(baseUrl) //baseUrl 标识,用于支持指定 BaseUrl
    @GET("api/user")
    fun getUser(): Call<User>


    //--------------------------------------

    /**
    * 使用RxJava返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
    * @return
    */
    @DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
    @Timeout(connectTimeout = 20,readTimeout = 10) //超时标识,用于自定义超时时长
    @GET("api/user")
    fun getUser(): Observable<User>

    }

    Step.4 添加多个 BaseUrl 支持

            //添加多个 BaseUrl 支持 ,domainName为域名别名标识,domainUrl为域名对应的 BaseUrl,与上面的接口定义表示一致即可生效
    RetrofitHelper.getInstance().putDomain(domainName,domainUrl)
            //添加多个 BaseUrl 支持 示例
    RetrofitHelper.getInstance().apply {
    //GitHub baseUrl
    putDomain(Constants.DOMAIN_GITHUB,Constants.GITHUB_BASE_URL)
    //Google baseUrl
    putDomain(Constants.DOMAIN_GOOGLE,Constants.GOOGLE_BASE_URL)
    }

    RetrofitHelper

    /**
    * Retrofit帮助类
    *


    * 主要功能介绍:
    * 1.支持管理多个 BaseUrl,且支持运行时动态改变
    * 2.支持接口自定义超时时长,满足每个接口动态定义超时时长
    * 3.支持添加公共请求头
    *


    *
    * RetrofitHelper中的核心方法
    *
    * {@link #createClientBuilder()} 创建 {@link OkHttpClient.Builder}初始化一些配置参数,用于支持多个 BaseUrl
    *
    * {@link #with(OkHttpClient.Builder)} 传入 {@link OkHttpClient.Builder} 配置一些参数,用于支持多个 BaseUrl
    *
    * {@link #setBaseUrl(String)} 和 {@link #setBaseUrl(HttpUrl)} 主要用于设置默认的 BaseUrl。
    *
    * {@link #putDomain(String, String)} 和 {@link #putDomain(String, HttpUrl)} 主要用于支持多个 BaseUrl,且支持 BaseUrl 动态改变。
    *
    * {@link #setDynamicDomain(boolean)} 设置是否支持 配置多个BaseUrl,且支持动态改变,一般会通过其他途径自动开启,此方法一般不会主动用到,只有在特殊场景下可能会有此需求,所以提供此方法主要用于提供更多种可能。
    *
    * {@link #setHttpUrlParser(HttpUrlParser)} 设置 HttpUrl解析器 , 当前默认采用的 {@link DomainParser} 实现类,你也可以自定义实现 {@link HttpUrlParser}
    *
    * {@link #setAddHeader(boolean)} 设置是否添加头,一般会通过{@link #addHeader(String, String)}相关方法自动开启,此方法一般不会主动用到,只有特殊场景下会有此需求,主要用于提供统一控制。
    *
    * {@link #addHeader(String, String)} 设置头,主要用于添加公共头消息。
    *
    * {@link #addHeaders(Map)} 设置头,主要用于设置公共头消息。
    *
    * 这里只是列出一些对外使用的核心方法,和相关的简单说明。如果想了解更多,可以查看对应的方法和详情。
    *
    *


    *
    * @author Jenly
    */
    public final class RetrofitHelper{
    //...
    }

    特别说明

            //通过setBaseUrl可以动态改变全局的 BaseUrl,优先级比putDomain(domainName,domainUrl)低,谨慎使用
    RetrofitHelper.getInstance().setBaseUrl(dynamicUrl)

    更多使用详情,请查看Demo中的源码使用示例或直接查看API帮助文档


    代码下载:RetrofitHelper.zip

    收起阅读 »

    LLDB调试利器及高级用法

    LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我...
    继续阅读 »

    LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器,默认内置于Xcode中。能够很好的运用它会使我们的开发效率事半功倍,接下来将讲解lldb常用命令及一些高级用法。下面将不会讲解命令的基本格式及命令的缩写来源,我会把重点放在常用命令的使用方式和技巧上。

    一、 LLDB常用调试命令
    ❶ p、po及 image命令
    1、是打印对象,是打印对象的description,演示如下:


    2、p命令修改变量,演示如下:


    3、imagelookup -a用于寻找栈地址对应的代码位置,演示如下:


    3.1 从上图中我们可以看到当程序崩溃时并不能定位到指定的代码位置,使用image寻址命令可以定位到具体的崩溃位置在viewDidLoad方法中的第51行。


    3.2 这里说明为什么是程序的名称,因为LLDBDebug在编译后就是一个Macho的可执行文件,也可以理解为镜像文件,image并不是图像的意思,而是代表镜像。这里跟上我们自己的工程名,即用image定位寻址才是寻找我们自己的代码。
    ❷ bt及frame命令
    1、使用命令可以查看函数调用堆栈,然后用 命令即可查看对应函数详细,演示如下:


    1.1 上面函数执行的顺序如下:点击登录按钮--验证手机号--验证密码--开始登录。

    - (IBAction)login:(UIButton *)sender {

    [self validationPhone];
    }
    #pragma mark --验证手机号
    -(void)validationPhone{

    [self validationPwd];
    }
    #pragma mark --验证密码
    -(void)validationPwd{

    [self startLogin];
    }
    #pragma mark --开始登陆
    -(void)startLogin{

    NSLog(@"------开始登录...------");
    }

    1.2 从bt命令的打印信息中,我们可以很清楚看到函数调用顺序,如下图:


    1.3 接下来我们执行 frame select命令即可以查看函数相关信息,同时配合up和down命令追踪函数的调用和被调用关系,演示如下:


    1.4 同时可以使用frame variable很方便的查方法的调用者及方法名称,如下图:


    ❸ breakpoint命令
    1、b命令给函数下断点,演示如下图


    1.1 当我们的断点下成功后,控制台会打印如下信息:
    Breakpoint 1: where = LLDBDebug`-[ViewController login:] at ViewController.m:53, address = 0x00000001034fb0a0

    1.2 我们可以看到断点的位置在.m文件的53行,Breakpoint 1这里的1代表的是编号为1的组断点。
    使用 我们可以看到断点的数量,同时使用 后面跟上组号,即可删除,演示如下:


    3、\color{red}{breakpoint}的\color{red}{c},\color{red}{n},\color{red}{s}以及\color{red}{finish}命令,对应关系如下图:


    3.1 我们执行\color{red}{c},\color{red}{n},\color{red}{s}及\color{red}{finish}命令演示如下:


    ❹ breakpoint命令
    1.target stop-hook add -o "frame variable"每次进入断点都会自动打印详细的参数信息,演示如下:


    二、 LLDB高级用法
    ❶ 我们先来简单看下\color{red}{menthods}和\color{red}{pviews}命令的执行效果,演示如下图:


    1.1 \color{red}{menthods}命令可以打印当前对象的属性和方法,如下所示:

    (lldb) methods p1
    <Person: 0x60000003eac0>:
    in Person:
    Properties:
    @property (copy, nonatomic) NSString* name; (@synthesize name = _name;)
    @property (nonatomic) long age; (@synthesize age = _age;)
    Instance Methods:
    - (void) eat; (0x1098bf3e0)
    - (void) .cxx_destruct; (0x1098bf4f0)
    - (id) description; (0x1098bf410)
    - (id) name; (0x1098bf430)
    - (void) setName:(id)arg1; (0x1098bf460)
    - (void) setAge:(long)arg1; (0x1098bf4c0)
    - (long) age; (0x1098bf4a0)
    (NSObject ...)

    1.2 \color{red}{pviews}命令可以打印当前视图的层级结构,如下所示:

    (lldb) pviews
    <UIWindow: 0x7fd1719060a0; frame = (0 0; 414 736); gestureRecognizers = <NSArray: 0x60c000058660>; layer = <UIWindowLayer: 0x60c0000364c0>>
    | <UIView: 0x7fd16fc06d10; frame = (0 0; 414 736); alpha = 0.8; autoresize = W+H; layer = <CALayer: 0x60000003e7e0>>
    | | <UIButton: 0x7fd16fe0b520; frame = (54 316; 266 53); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x60400003b040>>
    | | | <UIButtonLabel: 0x7fd16fe023f0; frame = (117.667 17.6667; 30.6667 18); text = '登录'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60400008ac80>>
    | | | | <_UILabelContentLayer: 0x600000220260> (layer)
    | | <UILabel: 0x7fd16fc04a60; frame = (164 225; 80 47); text = 'Qinz'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600000088fc0>>
    (lldb)

    1.3 如果你在原生的XCode中,是敲不出这些命令的,上面只是演示了两个常见的LLDB插件命令的用法,更加高级的用法下面会详细说明。不过在这之前,我们要安装两个插件,接下来先讲解环境的配置。
    ❷ LLDB插件配置:chisel及LLDB
    2.1 chisel是facebook开源的一款LLDB插件,里面封装了很多好用的命令,当然这些命令都是基于苹果提供的api。chisel下载
    2.2 这里建议使用包管理工具Homebrew来安装,然后配置脚本路径,演示如下:


    2.3 然后在lldb窗口执行命令,演示如下:


    2.4 看到输出"command script import /usr/local/opt/chisel/libexec/fblldb.py"即代表安装成功,这里还会看到一个"command script import /opt/LLDB/lldb_commands/dslldb.py
    "路径,这是我们接下来要安装的第二个插件

    Executing commands in '/Users/Qinz/.lldbinit'.
    command script import /usr/local/opt/chisel/libexec/fblldb.py
    command script import /opt/LLDB/lldb_commands/dslldb.py
    (lldb)

    2.5 这个插件的名称也叫LLDB,LLDB下载。我们先clone文件,我这里放置在opt文件夹下,你可以选择自己的文件目录放置,然后依次找到dslldb文件,在~/.initlldb文件中配置路径,演示如下:


    2.6 接下来依然在lldb窗口执行 command source ~/.lldbinit命令。到此LLDB插件的配置环境完成,接下来我们讲解这些插件的实用命令。
    ❸ lldb高级用法
    1. 搭配,让你快速找准控件,演示如下:


    1.1 taplog是点击控件,会打印控件的地址,大小及透明度等信息,我们拿到地址后执行flicker 0x7fd321e09710命令,此时控件会进行闪烁,这里动态图显示的闪烁效果明显。
    2. 和显示和隐藏控件,演示如下:


    \color{red}{vs}命令方便动态查看控件的层级关系,演示如下:


    3.1 当我们执行\color{red}{vs}命令后会进入动态调试阶段,会出现以下五个命令,每个命令我做了详细注释如下:

    (lldb) vs 0x7fe73550a090
    Use the following and (q) to quit.
    (w) move to superview //移动到父视图
    (s) move to first subview //移动到第一个子视图
    (a) move to previous sibling //移动上一个兄弟视图
    (d) move to next sibling //移动下一个兄弟视图
    (p) print the hierarchy //打印视图层级结构

    \color{red}{pactions}直接打印对象调用者及方法,演示如下:


    \color{red}{border}&\color{red}{unborder}给控件增加和去除边框,演示如下:


    5.1 这里的-c即是color,-w即设置边框的宽度。通过这个命令我们可以很方便的查看边框的边缘的问题,而不需要每次重启运行。
    6.\color{red}{pclass}打印对象的继承关系,演示如下图:


    \color{red}{presponder}命令打印响应链,演示如下图:


    \color{red}{caflush}这个命令会重新渲染,即可以重新绘制界面, 相当于执行了 [CATransaction flush] 方法,演示如下:


    \color{red}{search}搜索已经存在于栈中的控件及其子控件,演示如下:


    \color{red}{lookup}搜索,可执行正则表达式。演示如下:


    10.1 上面的搜索会搜索所用镜像模块,我们重点看与我们工程名字相同的模块,即可查看哪些地方调用了这些方法。
    11. \color{red}{pbundlepath}打印app路径及\color{red}{pdocspath}打印文档路径,演示如下:


    总结:上面详细讲解了LLDB常用命令及高级命令的用法,熟练掌握可大幅度提高Debug能力和开发效率。

    我是Qinz,希望我的文章对你有帮助。

    转自:https://www.jianshu.com/p/c91f843a64fc

    收起阅读 »

    UIViewController解耦---浅析Three20架构

    前言Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在...
    继续阅读 »

    前言
    Three20是一款由Facebook开源的框架,由大神Joe Hewitt创建,曾经风靡一时,被无数开发者观阅。Three20主要提供了UI模块、Network模块以及相关的一些工具。Three20自开源之初就褒贬不一,有人称赞它强大的UI工具,也有人在诟病Three20各个模块之间的耦合度太高,而且更多人在抱怨Three20极少的开发文档,我想这些大概也是Three20在苹果发布iOS6之后就停止了更新维护的原因吧。大神Joe Hewitt创建的在Github上的源码早已删除,目前只有少数人在GitHub上为自己的项目维护。而我也是有幸在某个项目中见识到了曾经耳闻,却未目睹的Three20框架,因此才有了这篇文章。

    架构
    最近大家都在讨论MVC、MVVM以及MVP三种在移动端开发中常用到的架构模式,究竟是哪种架构最强大,最适合移动开发者使用。这里笔者也阐述一下个人意见,有句方言叫“树挪死,人挪活”,个人认为,架构是死的,开发者是活的,我们不需要局限于哪一种架构的模式之下,看到大家都在用MVVM,于是花大成本将MVC架构模式的老项目重构成了MVVM架构,这种重构个人看来其实并没有意义。更多的架构话题就不想在这里讨论了,笔者推荐几篇大神们关于架构的见解。

    1、被误解的 MVC 和被神化的 MVVM
    这是一篇被早已被翻烂了的文章,起码我个人反复阅读了数次,由家喻户晓的唐巧大神编写。
    2、iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构
    最近在Cocoa China上发表的一篇译文,笔者之前看过俩次原文,讲的比较形象。
    3、MVC,MVP 和 MVVM 的图示
    大神阮一峰的博文,以图形展示的方式使得各层结构更加清晰明了。
    4、猿题库 iOS 客户端架构设计
    猿题库 iOS客户端开发者蓝晨钰的博文,以实际项目猿题库详解了架构设计

    UIViewController瘦身

    架构模式并不是限制思维,相反应该是发散思维,我们并不应该为了架构而架构,架构应该是服务于我们的代码逻辑,打造更具有扩展性和健壮的代码结构。就比如,大多数开发者都会遇到一个同样的问题,随着项目一天天的壮大,功能越来越多,需求越来越多,而我们的UIViewController也变得越来越臃肿。在上面推荐的博文中,笔者们都或多或少的阐述了如何打造更轻量级的UIViewController,大都列举了一些共性策略:

    1、将一个界面中的数据获取抽象成一个类,这里面细分一下,包括了网络请求和数据库缓存,我们可以针对这俩点再次封装成俩个类。
    2、将一个界面中的数据处理逻辑抽象成一个类,这里面包含了各种数据转换和算法逻辑,比如数据检索,数据遍历等。
    3、将一个界面中数据传递到UIView视图的过程抽象成一个模型类,这里面就包含了对应到UIView视图的每一个数据的传递,比如icon图标,title标题,comment评论内容等。
    4、将一个界面中所有展示的UIView视图的添加和渲染抽象成一个类,这里包含了添加控件,自定义动画等。这个对视图的封装仍然可以细分,每一个自定义控件都可以单独封装,因为这样可以完美的在其他的UIViewController达到复用的目的。

    而完成了上述抽象之后,就会发现我们需要在UIViewController中完成的工作仅仅是处理视图交互逻辑和数据传递逻辑,这样我们的UIViewController就比较容易维护了。

    Three20架构
    每一种框架的兴起和衰落都有其相应的时势和必然性。虽然Three20饱受诟病,早已跌落神坛,但是它的存在是有一定道理的。虽然它在模块之间的耦合度较高,但是个人认为它对UIViewController的抽象和封装也是一个非常好的借鉴。在这里以Three20中对TTTableViewController的解耦为例,先上图看一下TTTableViewController包含的模块:


    这里根据上面的结构图具体地解释一下解耦的设计方式。TTTableViewController的设计遵从了经典的MVC模式,TTModel负责数据的获取和处理逻辑,TTTableView负责视图展示,TTTableViewController负责TTModel与TTTableView之间的通信逻辑和界面的控件添加渲染。而TTTableViewController在顺应了MVC模式的前提下,也做了一些扩展,它将TTTableViewDatasource接收数据传递的逻辑抽象出来封装成了TTTableItem。而TTTableItem就是关联TTModel传递数据的过程,因而我们也可以把这一层称作是MVVM架构模式中的ViewModel

    根据上面的图示,我们可以看到获取数据的逻辑都在TTModel中,而且界面控件添加和动画渲染这些逻辑仍然都在TTTableViewController中,因此我根据大神们的一些建议,对项目中的Three20进行了一下强化,先上图看一下增加的结构:


    可以清晰地看到,我将TTModel中处理缓存数据的逻辑抽象出来,单独放在了TTCacheModel中,此外还将TTTableViewController中添加控件和渲染动画的逻辑抽象出来,放到了TTViewRender中,这样TTTableViewController就只关心界面交互以及TTModel和TTTableItem之间的数据传递逻辑。

    链接:https://www.jianshu.com/p/a748f6fd99dd

    收起阅读 »

    iOS RESideMenu 侧滑 第三方类库

    下载地址:https://github.com/romaonthego/RESideMenu效果如下:官方案例自己的实现效果具体代码下:AppDelegate.m文件中- (BOOL)application:(UIApplication *)applicati...
    继续阅读 »

    下载地址:https://github.com/romaonthego/RESideMenu
    效果如下:官方案例


    自己的实现效果


    具体代码下:

    AppDelegate.m文件中

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法

    DEMOLeftMenuViewController *leftMenuViewController = [[DEMOLeftMenuViewController alloc] init];

    RESideMenu *sideMenuViewController = [[RESideMenu alloc] initWithContentViewController:[[MainTabBarController alloc]init] leftMenuViewController:leftMenuViewController rightMenuViewController:[UINavigationController new]];
    sideMenuViewController.backgroundImage = [UIImage imageNamed:@"005.jpg"];
    sideMenuViewController.menuPreferredStatusBarStyle = 1; // UIStatusBarStyleLightContent
    sideMenuViewController.delegate = self;
    // sideMenuViewController.parallaxContentMaximumRelativeValue=100;
    // sideMenuViewController.bouncesHorizontally=YES;
    sideMenuViewController.contentViewShadowColor = [UIColor blackColor];
    sideMenuViewController.contentViewShadowOffset = CGSizeMake(0, 0);
    sideMenuViewController.contentViewShadowOpacity = 0.6;
    sideMenuViewController.contentViewShadowRadius = 12;
    // sideMenuViewController.contentViewShadowEnabled = YES;
    // sideMenuViewController.panFromEdge=NO;
    self.window.rootViewController = sideMenuViewController;

    左侧的控制器DEMOLeftMenuViewController.h和DEMOLeftMenuViewController.m

    #import <UIKit/UIKit.h>
    #import "RESideMenu.h"

    @interface DEMOLeftMenuViewController : UIViewController<UITableViewDataSource, UITableViewDelegate, RESideMenuDelegate>


    @end
    #import "DEMOLeftMenuViewController.h"
    #import "HomeViewController.h"
    #import "UIViewController+RESideMenu.h"
    #import "LoginViewController.h"
    #import "resigeViewController.h"

    @interface DEMOLeftMenuViewController ()
    @property (strong, readwrite, nonatomic) UITableView *tableView;

    @end

    @implementation DEMOLeftMenuViewController

    - (void)viewDidLoad
    {
    [super viewDidLoad];
    self.navigationController.title=@"登陆";
    self.tableView = ({
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, (self.view.frame.size.height - 54 * 5) / 2.0f, self.view.frame.size.width, 54 * 5) style:UITableViewStylePlain];
    tableView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth;
    tableView.delegate = self;
    tableView.dataSource = self;
    tableView.opaque = NO;
    tableView.backgroundColor = [UIColor clearColor];
    tableView.backgroundView = nil;
    tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    tableView.bounces = NO;
    tableView.scrollsToTop = NO;
    tableView;
    });
    [self.view addSubview:self.tableView];
    }

    #pragma mark -
    #pragma mark UITableView Delegate

    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
    {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    switch (indexPath.row) {
    case 0:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[LoginViewController alloc] init]] animated:YES completion:nil];
    break;
    case 1:
    [self presentViewController:[[UINavigationController alloc] initWithRootViewController:[[resigeViewController alloc] init]] animated:YES completion:nil];
    break;
    default:
    break;
    }
    }

    #pragma mark -
    #pragma mark UITableView Datasource

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    return 54;
    }

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    return 1;
    }

    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)sectionIndex
    {
    return 5;
    }

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *cellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    cell.backgroundColor = [UIColor clearColor];
    cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue" size:21];
    cell.textLabel.textColor = [UIColor whiteColor];
    cell.textLabel.highlightedTextColor = [UIColor lightGrayColor];
    cell.selectedBackgroundView = [[UIView alloc] init];
    }

    NSArray *titles = @[@"Home", @"Calendar", @"Profile", @"Settings", @"Log Out"];
    NSArray *images = @[@"IconHome", @"IconCalendar", @"IconProfile", @"IconSettings", @"IconEmpty"];
    cell.textLabel.text = titles[indexPath.row];
    cell.imageView.image = [UIImage imageNamed:images[indexPath.row]];

    return cell;
    }


    @end

    主页HomeViewController.h和HomeViewController.m实现侧滑的关键代码

    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"个人中心"
    style:UIBarButtonItemStylePlain
    target:self
    action:@selector(presentLeftMenuViewControl

    这个第三番可以实现很多效果

    总结

    优点:

    1.里面的文件较少,不需要使用cocoapods即可运行。

    2.里面自定义API也比较多,可以设置变小的抽屉效果或者不变小。

    3.里面有两个事例程序,一个是纯手码,一个是Storyboard得。可见作者也非常喜欢IB开发,此框架用IB开发应该可以完美兼容。

    4.可以使用手势拖来拖去。

    5.项目里各个文件不需要继承,导入头文件就行。

    缺点:

    1.左边显示的菜单可选项是固定的几个button,暂时想把左边换成tableView还不知道可不可行。

    2.不能实现状态栏右移。

    3.暂时没找到两边控制器的占比怎么自定义。

    转自:https://www.cnblogs.com/qianLL/p/5425738.html

    收起阅读 »

    PNChart:一个简单漂亮的iOS图表库

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。要求PNChart 适用...
    继续阅读 »

    PNChart是一个简单漂亮的动画图表库,Piner和CoinsMan的 iOS 客户端中使用了这个框架。你也可以查看 Swift 版本(开源链接:https://github.com/kevinzhow/PNChart-Swift)。

    要求

    PNChart 适用于 iOS 7.0 或更高版本,与 ARC 项目兼容。如果需要支持 iOS 6 ,请使用 0.8.1 版本之前的 PNChart 。注意 0.8.2 版本仅支持 iOS 8.0+ ,0.8.3 及更新版本支持 iOS 7.0+ 。

    PNChart 依赖于下列框架,这些框架已经嵌入了 Xcode 开发工具:

    Foundation.framework

    UIKit.framework

    CoreGraphics.framework

    QuartzCore.framework

    你需要 LLVM 3.0 或更高版本来建立 PNChart 。

    安装

    通过CocoaPods安装(推荐):

    1、在你的 Podfile 文件中添加pod 'PNChart'。

    2、运行pod install进行安装。

    3、按需导入头文件#import "PNChart.h"。

    手动安装:

    拷贝PNChart文件夹到你的工程中。

    使用

    #import "PNChart.h"

    //For Line Chart

    PNLineChart*lineChart=[[PNLineChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [lineChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    // Line Chart No.1

    NSArray*data01Array=@[@60.1,@160.1,@126.4,@262.2,@186.2];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=lineChart.xLabels.count;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart No.2

    NSArray*data02Array=@[@20.1,@180.1,@26.4,@202.2,@126.2];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=lineChart.xLabels.count;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    lineChart.chartData=@[data01,data02];

    [lineChartstrokeChart];
    #import "PNChart.h"

    //For BarC hart

    PNBarChart*barChart=[[PNBarChartalloc]initWithFrame:CGRectMake(0,135.0,SCREEN_WIDTH,200.0)];

    [barChartsetXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5"]];

    [barChartsetYValues:@[@1,@10,@2,@6,@3]];

    [barChartstrokeChart];



    ``` Objective-C

    #import "PNChart.h"

    //For Circle Chart

    PNCircleChart*circleChart=[[PNCircleChartalloc]initWithFrame:CGRectMake(0,80.0,SCREEN_WIDTH,100.0)total:[NSNumbernumberWithInt:100]current:[NSNumbernumberWithInt:60]clockwise:NOshadow:NO];

    circleChart.backgroundColor=[UIColorclearColor];

    [circleChartsetStrokeColor:PNGreen];

    [circleChart strokeChart];



    ```Objective-C

    # import "PNChart.h"

    //For Pie Chart

    NSArray*items=@[[PNPieChartDataItemdataItemWithValue:10color:PNRed],

    [PNPieChartDataItemdataItemWithValue:20color:PNBluedescription:@"WWDC"],

    [PNPieChartDataItemdataItemWithValue:40color:PNGreendescription:@"GOOL I/O"],

    ];

    PNPieChart*pieChart=[[PNPieChartalloc]initWithFrame:CGRectMake(40.0,155.0,240.0,240.0)items:items];

    pieChart.descriptionTextColor=[UIColorwhiteColor];

    pieChart.descriptionTextFont=[UIFontfontWithName:@"Avenir-Medium"size:14.0];

    [pieChartstrokeChart];
    # import "PNChart.h"

    //For Scatter Chart

    PNScatterChart*scatterChart=[[PNScatterChartalloc]initWithFrame:CGRectMake(SCREEN_WIDTH/6.0-30,135,280,200)];

    [scatterChartsetAxisXWithMinimumValue:20andMaxValue:100toTicks:6];

    [scatterChartsetAxisYWithMinimumValue:30andMaxValue:50toTicks:5];

    NSArray*data01Array=[selfrandomSetOfObjects];

    PNScatterChartData*data01=[PNScatterChartDatanew];

    data01.strokeColor=PNGreen;

    data01.fillColor=PNFreshGreen;

    data01.size=2;

    data01.itemCount=[[data01ArrayobjectAtIndex:0]count];

    data01.inflexionPointStyle=PNScatterChartPointStyleCircle;

    __blockNSMutableArray*XAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:0]];

    __blockNSMutableArray*YAr1=[NSMutableArrayarrayWithArray:[data01ArrayobjectAtIndex:1]];

    data01.getData=^(NSUIntegerindex){

    CGFloatxValue=[[XAr1objectAtIndex:index]floatValue];

    CGFloatyValue=[[YAr1objectAtIndex:index]floatValue];

    return[PNScatterChartDataItemdataItemWithX:xValueAndWithY:yValue];

    };

    [scatterChartsetup];

    self.scatterChart.chartData=@[data01];

    /***

    this is for drawing line to compare

    CGPoint start = CGPointMake(20, 35);

    CGPoint end = CGPointMake(80, 45);

    [scatterChart drawLineFromPoint:start ToPoint:end WithLineWith:2 AndWithColor:PNBlack];

    ***/

    scatterChart.delegate=self;

    图例

    PNChart 允许在折线图和饼状图中添加图例,图例可以竖向堆叠布置或者横向并列布置。

    #import "PNChart.h"

    //For Line Chart

    //Add Line Titles for the Legend

    data01.dataTitle=@"Alpha";

    data02.dataTitle=@"Beta Beta Beta Beta";

    //Build the legend

    self.lineChart.legendStyle=PNLegendItemStyleSerial;

    self.lineChart.legendFontSize=12.0;

    UIView*legend=[self.lineChartgetLegendWithMaxWidth:320];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(100,400,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    //For Pie Chart

    //Build the legend

    self.pieChart.legendStyle=PNLegendItemStyleStacked;

    self.pieChart.legendFontSize=12.0;

    UIView*legend=[self.pieChartgetLegendWithMaxWidth:200];

    //Move legend to the desired position and add to view

    [legendsetFrame:CGRectMake(130,350,legend.frame.size.width,legend.frame.size.height)];

    [self.viewaddSubview:legend];

    更新数据

    实时更新数据也非常简单。

    Objective-C

    if([self.titleisEqualToString:@"Line Chart"]){

    // Line Chart #1

    NSArray*data01Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data01=[PNLineChartDatanew];

    data01.color=PNFreshGreen;

    data01.itemCount=data01Array.count;

    data01.inflexionPointStyle=PNLineChartPointStyleTriangle;

    data01.getData=^(NSUIntegerindex){

    CGFloatyValue=[data01Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    // Line Chart #2

    NSArray*data02Array=@[@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300),@(arc4random()%300)];

    PNLineChartData*data02=[PNLineChartDatanew];

    data02.color=PNTwitterColor;

    data02.itemCount=data02Array.count;

    data02.inflexionPointStyle=PNLineChartPointStyleSquare;

    data02.getData=^(NSUIntegerindex){

    CGFloatyValue=[data02Array[index]floatValue];

    return[PNLineChartDataItemdataItemWithY:yValue];

    };

    [self.lineChartsetXLabels:@[@"DEC 1",@"DEC 2",@"DEC 3",@"DEC 4",@"DEC 5",@"DEC 6",@"DEC 7"]];

    [self.lineChartupdateChartData:@[data01,data02]];

    }

    elseif([self.titleisEqualToString:@"Bar Chart"])

    {

    [self.barChartsetXLabels:@[@"Jan 1",@"Jan 2",@"Jan 3",@"Jan 4",@"Jan 5",@"Jan 6",@"Jan 7"]];

    [self.barChartupdateChartData:@[@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30),@(arc4random()%30)]];

    }

    elseif([self.titleisEqualToString:@"Circle Chart"])

    {

    [self.circleChartupdateChartByCurrent:@(arc4random()0)];

    }

    代理回调

    Objective-C

    #import "PNChart.h"

    //For LineChart

    lineChart.delegate=self;

    动画

    默认绘制图表时使用动画,可以通过设置displayAnimation = NO来禁止动画。

    Objective-C


    #import "PNChart.h"

    //For LineChart

    lineChart.displayAnimation=NO;

    ```Objective-C



    //For DelegateMethod

    -(void)userClickedOnLineKeyPoint:(CGPoint)pointlineIndex:(NSInteger)lineIndexpointIndex:(NSInteger)pointIndex{

    NSLog(@"Click Key on line %f, %f line index is %d and point index is %d",point.x,point.y,(int)lineIndex,(int)pointIndex);

    }

    -(void)userClickedOnLinePoint:(CGPoint)pointlineIndex:(NSInteger)lineIndex{

    NSLog(@"Click on line %f, %f, line index is %d",point.x,point.y,(int)lineIndex);

    }

    开源协议

    PNChart 在MIT开源协议下可以使用,也就是说,只要在项目副本中包含了版权声明和许可声明,用户就可以使用 PNChart 做任何想做的事情,而 PNChart 也无需承担任何责任。可以通过查看 LICENSE 文件来获取更多相关信息。

    开源地址:https://github.com/kevinzhow/PNChart

    链接:https://www.jianshu.com/p/9c162d6f8f14

    收起阅读 »

    Android原生绘图进度条+简单自定义属性代码生成器

    先一下效果:一、简单自定义属性生成器1.玩安卓的应该都写过自定义控件的自定义属性:如下我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?2.通过attrs.xml自动生成相应代码秉承着能用代码解决的问题,绝对不动手。能够靠...
    继续阅读 »

    先一下效果:

    圆形进度条.gif

    横向进度条.gif

    一、简单自定义属性生成器

    1.玩安卓的应该都写过自定义控件的自定义属性:如下

    自定义控件.png

    我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?

    2.通过attrs.xml自动生成相应代码

    秉承着能用代码解决的问题,绝对不动手。能够靠智商解决的问题,绝对不靠体力的大无畏精神:

    写了一个小工具,将代码里的内容自动生成一下:基本上就是字符串的切割和拼装,工具附在文尾

    使用方法与注意点:

    1.拷贝到AndroidStudio的test里,将attrs.xml的文件路径设置一下,运行
    2.自定义必须符合命名规则,如z_pb_on_height,专属前缀如z_,单词间下划线连接即可
    3.它并不是什么高大上的东西,只是简单的字符串切割拼组,只适用简单的自定义属性[dimension|color|boolean|string](不过一般的自定义属性也够用了)

    自动生成.png

    在开篇之前:先看一下Android系统内自定义控件的书写风格,毕竟跟原生看齐没有什么坏处

    看一下LinearLayout的源码:

    1.构造方法使用最多参数的那个,其他用this(XXX)调用

     public LinearLayout(Context context) {
    this(context, null);
    }

    public LinearLayout(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
    }

    public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    ...
    }

    2.自定义属性的书写

    1).先将自定义属性的成员变量定义好

    2).如果自定义属性不是很多,一个一个a.getXXX,默认值直接写在后面就行了
    3).看了一下TextView的源码,自定义属性很多,它是先定义默认值的变量,再使用,而且用switch来对a.getXXX进行赋值

    final TypedArray a = context.obtainStyledAttributes(
    attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);

    int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
    if (index >= 0) {
    setOrientation(index);
    }

    index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
    if (index >= 0) {
    setGravity(index);
    }

    boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
    if (!baselineAligned) {
    setBaselineAligned(baselineAligned);
    }
    ......
    a.recycle();

    一、水平的进度条

    条形进度条分析.png

    1.自定义控件属性:values/attrs.xml

        






















    2.初始代码:将进行一些常规处理

    public class TolyProgressBar extends ProgressBar {

    private Paint mPaint;
    private int mPBWidth;
    private RectF mRectF;
    private Path mPath;
    private float[] mFloat8Left;//左边圆角数组
    private float[] mFloat8Right;//右边圆角数组

    private float mProgressX;//进度理论值
    private float mEndX;//进度条尾部
    private int mTextWidth;//文字宽度
    private boolean mLostRight;//是否不画右边
    private String mText;//文字

    private int mPbBgColor = 0xffC9C9C9;
    private int mPbOnColor = 0xff54F340;
    private int mPbOnHeight = dp(6);
    private int mPbBgHeight = dp(6);
    private int mPbTxtColor = 0xff525252;
    private int mPbTxtSize = sp(10);
    private int mPbTxtOffset = sp(10);
    private boolean mPbTxtGone= false;

    public TolyProgressBar(Context context) {
    this(context, null);
    }

    public TolyProgressBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TolyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyProgressBar);
    mPbOnHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_on_height, mPbOnHeight);
    mPbTxtOffset = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_offset, mPbTxtOffset);
    mPbOnColor = a.getColor(R.styleable.TolyProgressBar_z_pb_on_color, mPbOnColor);
    mPbTxtSize = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_size, mPbTxtSize);
    mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
    mPbBgColor = a.getColor(R.styleable.TolyProgressBar_z_pb_bg_color, mPbBgColor);
    mPbTxtGone = a.getBoolean(R.styleable.TolyProgressBar_z_pb_txt_gone, mPbTxtGone);
    a.recycle();

    init();
    }

    private void init() {
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setTextSize(mPbTxtSize);
    mPaint.setColor(mPbOnColor);
    mPaint.setStrokeWidth(mPbOnHeight);

    mRectF = new RectF();
    mPath = new Path();


    mFloat8Left = new float[]{//仅左边两个圆角--为背景
    mPbOnHeight / 2, mPbOnHeight / 2,//左上圆角x,y
    0, 0,//右上圆角x,y
    0, 0,//右下圆角x,y
    mPbOnHeight / 2, mPbOnHeight / 2//左下圆角x,y
    };

    mFloat8Right = new float[]{
    0, 0,//左上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
    0, 0//左下圆角x,y
    };
    }

    }

    private int sp(int sp) {
    return (int) TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    private int dp(int dp) {
    return (int) TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }


    2.测量:

        @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = measureHeight(heightMeasureSpec);
    setMeasuredDimension(width, height);
    mPBWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//进度条实际宽度
    }

        /**
    * 测量高度
    *
    * @param heightMeasureSpec
    * @return
    */

    private int measureHeight(int heightMeasureSpec) {
    int result = 0;
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    int size = MeasureSpec.getSize(heightMeasureSpec);

    if (mode == MeasureSpec.EXACTLY) {
    //控件尺寸已经确定:如:
    // android:layout_height="40dp"或"match_parent"
    result = size;
    } else {
    int textHeight = (int) (mPaint.descent() - mPaint.ascent());
    result = getPaddingTop() + getPaddingBottom() + Math.max(
    Math.max(mPbBgHeight, mPbOnHeight), Math.abs(textHeight));

    if (mode == MeasureSpec.AT_MOST) {//最多不超过
    result = Math.min(result, size);
    }
    }
    return result;
    }
    复制代码

    3.绘制:

        @Override
    protected synchronized void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.translate(getPaddingLeft(), getHeight() / 2);

    parseBeforeDraw();//1.绘制前对数值进行计算以及控制的flag设置

    if (getProgress() == 100) {//进度达到100后文字消失
    whenOver();//2.
    }
    if (mEndX > 0) {//当进度条尾部>0绘制
    drawProgress(canvas);//3.
    }
    if (!mPbTxtGone) {//绘制文字
    mPaint.setColor(mPbTxtColor);
    int y = (int) (-(mPaint.descent() + mPaint.ascent()) / 2);
    canvas.drawText(mText, mProgressX, y, mPaint);
    } else {
    mTextWidth = 0 - mPbTxtOffset;
    }
    if (!mLostRight) {//绘制右侧
    drawRight(canvas);/4.
    }

    canvas.restore();
    }

    1).praseBeforeDraw()

    /**
    * 对数值进行计算以及控制的flag设置
    */

    private void parseBeforeDraw() {
    mLostRight = false;//lostRight控制是否绘制右侧
    float radio = getProgress() * 1.f / getMax();//当前百分比率
    mProgressX = radio * mPBWidth;//进度条当前长度
    mEndX = mProgressX - mPbTxtOffset / 2; //进度条当前长度-文字间隔的左半
    mText = getProgress() + "%";
    if (mProgressX + mTextWidth > mPBWidth) {
    mProgressX = mPBWidth - mTextWidth;
    mLostRight = true;
    }
    //文字宽度
    mTextWidth = (int) mPaint.measureText(mText);
    }

    2).whenOver()

    /**
    * 当结束是执行:
    */

    private void whenOver() {
    mPbTxtGone = true;
    mFloat8Left = new float[]{//只有进度达到100时让进度圆角是四个
    mPbBgHeight / 2, mPbBgHeight / 2,//左上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
    mPbBgHeight / 2, mPbBgHeight / 2//左下圆角x,y
    };
    }

    3).drawProgress()

    /**
    * 绘制左侧:(进度条)
    *
    * @param canvas
    */

    private void drawProgress(Canvas canvas) {
    mPath.reset();
    mRectF.set(0, mPbOnHeight / 2, mEndX, -mPbOnHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Left, Path.Direction.CW);//顺时针画
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPbOnColor);
    canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
    }

    4).drawRight()

    /**
    * 绘制左侧:(背景)
    *
    * @param canvas
    */

    private void drawRight(Canvas canvas) {
    float start = mProgressX + mPbTxtOffset / 2 + mTextWidth;
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeWidth(mPbBgHeight);
    mPath.reset();
    mRectF.set(start, mPbBgHeight / 2, mPBWidth, -mPbBgHeight / 2);
    mPath.addRoundRect(mRectF, mFloat8Right, Path.Direction.CW);//顺时针画
    canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
    }

    xml里使用:


    三、圆形进度条
    1.自定义属性





    2.代码实现:

    /**
    * 作者:张风捷特烈


    * 时间:2018/11/9 0009:11:49


    * 邮箱:1981462002@qq.com


    * 说明:圆形进度条
    */

    public class TolyRoundProgressBar extends TolyProgressBar {

    private int mPbRadius = dp(30);//进度条半径
    private int mMaxPaintWidth;

    public TolyRoundProgressBar(Context context) {
    this(context, null);
    }

    public TolyRoundProgressBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public TolyRoundProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyRoundProgressBar);
    mPbRadius = (int) a.getDimension(R.styleable.TolyRoundProgressBar_z_pb_radius, mPbRadius);
    mPbOnHeight = (int) (mPbBgHeight * 1.8f);//让进度大一点
    a.recycle();

    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setDither(true);
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    mMaxPaintWidth = Math.max(mPbBgHeight, mPbOnHeight);
    int expect = mPbRadius * 2 + mMaxPaintWidth + getPaddingLeft() + getPaddingRight();
    int width = resolveSize(expect, widthMeasureSpec);
    int height = resolveSize(expect, heightMeasureSpec);
    int realWidth = Math.min(width, height);
    mPaint.setStrokeCap(Paint.Cap.ROUND);

    mPbRadius = (realWidth - getPaddingLeft() - getPaddingRight() - mMaxPaintWidth) / 2;
    setMeasuredDimension(realWidth, realWidth);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {

    String txt = getProgress() + "%";
    float txtWidth = mPaint.measureText(txt);
    float txtHeight = (mPaint.descent() + mPaint.ascent()) / 2;
    canvas.save();
    canvas.translate(getPaddingLeft() + mMaxPaintWidth / 2, getPaddingTop() + mMaxPaintWidth / 2);
    drawDot(canvas);
    mPaint.setStyle(Paint.Style.STROKE);
    //背景
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeWidth(mPbBgHeight);
    canvas.drawCircle(mPbRadius, mPbRadius, mPbRadius, mPaint);
    //进度条
    mPaint.setColor(mPbOnColor);
    mPaint.setStrokeWidth(mPbOnHeight);
    float sweepAngle = getProgress() * 1.0f / getMax() * 360;//完成角度
    canvas.drawArc(
    0, 0, mPbRadius * 2, mPbRadius * 2,
    -90, sweepAngle, false, mPaint);
    //文字
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setColor(mPbTxtColor);
    canvas.drawText(txt, mPbRadius - txtWidth / 2, mPbRadius - txtHeight / 2, mPaint);
    canvas.restore();
    }

    /**
    * 绘制一圈点
    *
    * @param canvas
    */

    private void drawDot(Canvas canvas) {
    canvas.save();
    int num = 40;
    canvas.translate(mPbRadius, mPbRadius);
    for (int i = 0; i < num; i++) {
    canvas.save();
    int deg = 360 / num * i;
    canvas.rotate(deg);
    mPaint.setStrokeWidth(dp(3));
    mPaint.setColor(mPbBgColor);
    mPaint.setStrokeCap(Paint.Cap.ROUND);
    if (i * (360 / num) < getProgress() * 1.f / getMax() * 360) {
    mPaint.setColor(mPbOnColor);
    }
    canvas.drawLine(0, mPbRadius * 3 / 4, 0, mPbRadius * 4 / 5, mPaint);
    canvas.restore();
    }
    canvas.restore();
    }
    }




    附录:简单自定义属性生成器

    public class Attrs2Code {
    @Test
    public void main() {
    File file = new File("C:\\Users\\Administrator\\Desktop\\attrs.xml");
    initAttr("z_", file);
    }

    public static void initAttr(String preFix, File file) {
    HashMap format = format(preFix, file);
    String className = format.get("className");
    String result = format.get("result");
    StringBuilder sb = new StringBuilder();
    sb.append("TypedArray a = context.obtainStyledAttributes(attrs, R.styleable." + className + ");\r\n");
    format.forEach((s, s2) -> {
    String styleableName = className + "_" + preFix + s;
    if (s.contains("_")) {
    String[] partStrArray = s.split("_");
    s = "";
    for (String part : partStrArray) {
    String partStr = upAChar(part);
    s += partStr;
    }
    }
    if (s2.equals("dimension")) {
    // mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
    sb.append("m" + s + " = (int) a.getDimension(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("color")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getColor(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("boolean")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getBoolean(R.styleable." + styleableName + ", m" + s + ");\r\n");
    }
    if (s2.equals("string")) {
    // mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
    sb.append("m" + s + " = a.getString(R.styleable." + styleableName + ");\r\n");
    }
    });
    sb.append("a.recycle();\r\n");
    System.out.println(result);
    System.out.println(sb.toString());
    }

    /**
    * 读取文件+解析
    *
    * @param preFix 前缀
    * @param file 文件路径
    */

    public static HashMap format(String preFix, File file) {
    HashMap container = new HashMap<>();
    if (!file.exists() && file.isDirectory()) {
    return null;
    }
    FileReader fr = null;
    try {
    fr = new FileReader(file);
    //字符数组循环读取
    char[] buf = new char[1024];
    int len = 0;
    StringBuilder sb = new StringBuilder();
    while ((len = fr.read(buf)) != -1) {
    sb.append(new String(buf, 0, len));
    }
    String className = sb.toString().split(""));
    container.put("className", className);
    String[] split = sb.toString().split("<");
    String part1 = "private";
    String type = "";//类型
    String name = "";
    String result = "";
    String def = "";//默认值

    StringBuilder sb2 = new StringBuilder();
    for (String s : split) {
    if (s.contains(preFix)) {
    result = s.split(preFix)[1];
    name = result.substring(0, result.indexOf("\""));
    type = result.split("format=\"")[1];
    type = type.substring(0, type.indexOf("\""));
    container.put(name, type);
    if (type.contains("color") || type.contains("dimension") || type.contains("integer")) {
    type = "int";
    def = "0";
    }
    if (result.contains("fraction")) {
    type = "float";
    def = "0.f";
    }
    if (result.contains("string")) {
    type = "String";
    def = "\"toly\"";
    }
    if (result.contains("boolean")) {
    type = "boolean";
    def = "false";

    }
    if (name.contains("_")) {
    String[] partStrArray = name.split("_");
    name = "";
    for (String part : partStrArray) {
    String partStr = upAChar(part);
    name += partStr;
    }
    sb2.append(part1 + " " + type + " m" + name + "= " + def + ";\r\n");
    }
    container.put("result", sb2.toString());
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    try {
    if (fr != null) {
    fr.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    return container;
    }

    /**
    * 将字符串仅首字母大写
    *
    * @param str 待处理字符串
    * @return 将字符串仅首字母大写
    */

    public static String upAChar(String str) {
    String a = str.substring(0, 1);
    String tail = str.substring(1);
    return a.toUpperCase() + tail;
    }
    }

    代码下载:bobing107-IPhoneWatch_progressbar-master.zip

    收起阅读 »

    一个Android强大的饼状图

    一、思路 1、空心图(一个大圆中心绘制一个小圆) 2、根据数据算出所占的角度 3、根据动画获取当前绘制的角度 4、根据当前角度获取Paint使用的颜色 5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要) 二、实现 1、空心图(一个大...
    继续阅读 »

    一、思路


      1、空心图(一个大圆中心绘制一个小圆)
    2、根据数据算出所占的角度
    3、根据动画获取当前绘制的角度
    4、根据当前角度获取Paint使用的颜色
    5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要)


    二、实现


    1、空心图(一个大圆中心绘制一个小圆)初始化数据


          paint = new Paint();
    paint.setAntiAlias(true);
    paint.setStyle(Paint.Style.FILL_AND_STROKE);

    screenW = DensityUtils.getScreenWidth(context);

    int width = DensityUtils.dip2px(context, 15);//圆环宽度
    int widthXY = DensityUtils.dip2px(context, 10);//微调距离

    int pieCenterX = screenW / 2;//饼状图中心X
    int pieCenterY = screenW / 3;//饼状图中心Y
    int pieRadius = screenW / 4;// 大圆半径

    //整个饼状图rect
    pieOval = new RectF();
    pieOval.left = pieCenterX - pieRadius;
    pieOval.top = pieCenterY - pieRadius + widthXY;
    pieOval.right = pieCenterX + pieRadius;
    pieOval.bottom = pieCenterY + pieRadius + widthXY;

    //里面的空白rect
    pieOvalIn = new RectF();
    pieOvalIn.left = pieOval.left + width;
    pieOvalIn.top = pieOval.top + width;
    pieOvalIn.right = pieOval.right - width;
    pieOvalIn.bottom = pieOval.bottom - width;

    //里面的空白画笔
    piePaintIn = new Paint();
    piePaintIn.setAntiAlias(true);
    piePaintIn.setStyle(Paint.Style.FILL);
    piePaintIn.setColor(Color.parseColor("#f4f4f4"));

    2、根据数据算出所占的角度


    使用递归保证cakeValues的值的总和必为100,然后根据值求出角度


       private void settleCakeValues(int i) {
    float sum = getSum(cakeValues, i);
    CakeValue value = cakeValues.get(i);
    if (sum <= 100f) {
    value.setItemValue(100f - sum);
    cakeValues.set(i, value);
    } else {
    value.setItemValue(0);
    settleCakeValues(i - 1);
    }
    }
    复制代码

    3、根据动画获取当前绘制的角度


    curAngle就是当前绘制的角度,drawArc()就是绘制的方法


    cakeValueAnimator.addUpdateListener(new AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    float mAngle = obj2Float(animation.getAnimatedValue("angle"));
    curAngle = mAngle;
    drawArc();
    }
    });

    4、根据当前角度获取Paint使用的颜色


    根据当前的角度,计算当前是第几个item,通过
    paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
    来设置paint的颜色
    复制代码

    private int getCurItem(float curAngle) {
    int res = 0;
    for (int i = 0; i < itemFrame.length; i++) {
    if (curAngle <= itemFrame[i] * ANGLE_NUM) {
    res = i;
    break;
    }
    }
    return res;
    }

    5、动态绘制即将绘制的 和 绘制已经绘制的部分


    最重要的一步,我的需求是4类,用不同的颜色




    绘制当前颜色的扇形,curStartAngle扇形的起始位置,curSweepAngle扇形的终止位置


      paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
    float curStartAngle = 0;
    float curSweepAngle = curAngle;
    if (curItem > 0) {
    curStartAngle = itemFrame[curItem - 1] * ANGLE_NUM;
    curSweepAngle = curAngle - (itemFrame[curItem - 1] * ANGLE_NUM);
    }
    canvas.drawArc(pieOval, curStartAngle, curSweepAngle, true, paint);

    绘制已经绘制的扇形。根据curItem判断绘制过得扇形


    for (int i = 0; i < curItem; i++) {
    paint.setColor(Color.parseColor(cakeValues.get(i).getColors()));
    if (i == 0) {
    canvas.drawArc(pieOval, startAngle,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
    continue;
    }
    canvas.drawArc(pieOval,itemFrame[i - 1] * ANGLE_NUM,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
    }


    绘制中心的圆


     canvas.drawArc(pieOvalIn, 0, 360, true, piePaintIn);

    6、特别注意


    isFirst判断是够是第一次绘制(绘制完成后,home键进入后台,再次进入,不需要动态绘制)


     @Override
    protected void onDraw(Canvas canvas) {
    if (isFirst && isDrawByAnim) {
    drawCakeByAnim();
    }
    isFirst = false;
    }
    复制代码
    isDrawByAnim判断是否需要动画绘制
    drawCake()为静态绘制饼状图


    public void surfaceCreated(SurfaceHolder holder) {
    if (!isFirst||!isDrawByAnim)
    drawCake();
    }

    更新


    增加立体效果,提取配置参数


    <declare-styleable name="CakeSurfaceView">
    <attr name="isDrawByAnim" format="boolean"/>//是否动画
    <attr name="isSolid" format="boolean"/>//是否立体
    <attr name="duration" format="integer|reference"/>//动画时间
    <attr name="defaultColor" format="string"/>//默认颜色

    <attr name="ringWidth" format="integer|reference"/>//圆环宽度
    <attr name="solidWidth" format="integer|reference"/>//立体宽度
    <attr name="fineTuningWidth" format="integer|reference"/>//微调宽度
    </declare-styleable>
    复制代码
    xml中使用
    复制代码

    <com.xp.xppiechart.view.CakeSurfaceView
    android:id="@+id/assets_pie_chart"
    android:background="#ffffff"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:defaultColor="#ff8712"
    app:ringWidth="20"
    app:solidWidth="5"
    app:duration="3000"
    app:isSolid="true"
    app:isDrawByAnim="true"/>
    复制代码


    以上就是简单的实现动态绘制饼状图,待完善,以后会更新。如有建议和意见,请及时沟通。


    代码下载:bobing107-CircularSectorProgressBar-master.zip

    收起阅读 »

    Android商品属性筛选与商品筛选!

    前言这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。筛选属性最终完成关于商品筛选是有两种方式(至少...
    继续阅读 »

    前言

    这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。

    筛选属性最终完成
    筛选属性最终完成

    关于商品筛选是有两种方式(至少我只见到两种):

    第一种: 将所有的商品的所有属性及详情返回给客户端,由客户端进行筛选。
    淘宝用的就是这种。
    第二种: 将所有的属性返回给客户端,客户选择完成属性后将属性发送给后台
    ,再由后台根据属性筛选出具体商品返回给客户端。
    京东就是这样搞的。。

    两种方式各有各的好处:

    第一种:体验性特别好,用户感觉不到延迟,立即选中立即就筛选出了详情。就是客户端比较费劲。。。

    第二种:客户端比较省时间,但是体验性太差了,你想想,在网络不是很通畅的时候,你选择一个商品还得等老半天。

    因为当时我没有参加到这个接口的设计,导致一直在变化。。我才不会告诉不是后台不给力,筛选不出来才一股脑的将所有锅甩给客户端。

    技术点

    1. 流式布局

       商品的属性并不是一样长的,所以需要自动适应内容的一个控件。
      推荐hongyang的博客。我就是照着那个搞的。
    2. RxJava

       不要问我,我不知道,我也是新手,我就是用它做出了效果,至于有没有
      用对,那我就不知道了。反正目的是达到了。
    3. Json解析???

    准备

    1. FlowLayout
    2. RxJava

    xml布局

    这个部分的布局不是很难,只是代码量较多,咱们就省略吧,直接看效果吧

    布局完成
    布局完成

    可以看到机身颜色、内存、版本下面都是空的,因为我们还没有将属性筛选出来。

    数据分析

    先看看整体的数据结构是怎么样的

    数据结构
    数据结构

    每一个商品都有一个父类,仅作标识,不参与计算,比如数据中的华为P9就是一个商品的类目,在这下面有着各种属性组成的商品子类,这才是真正的商品。

    而一个详细的商品是有三个基础属性所组成:

    1. 版本
    2. 内存
    3. 制式

    如上图中一个具体的商品的名称:"华为 P9全网通 3GB+32GB版 流光金 移动联通电信4G手机 双卡双待"

    商品属性据结构
    商品属性据结构

    所以,要获得一个具体的商品是非常的简单,只需要客户选中的三个属性与上图中所对应的属性完全相同,就能得到这个商品。其中最关键的还是将所有的商品属性筛选出来。

    筛选出所有属性及图片

    本文中使用的数据是直接从Assets目录中直接读取的。

    筛选出该商品的所有属性,怎么做呢?其实也是很简单的,直接for所有商品的所有属性,然后存储起来,去除重复的属性,那么最后剩下的就是该商品的属性了

     /**
    * 初始化商品信息
    *
  • 1. 提取所有的属性

  • *
  • 2. 提取所有颜色的照片

  • */

    private void initGoodsInfo() {
    //所有的颜色
    mColors = new ArrayList<>();
    //筛选过程中临时存放颜色
    mTempColors = new ArrayList<>();
    //所有的内存
    mMonerys = new ArrayList<>();
    //筛选过程中临时的内存
    mTempMonerys = new ArrayList<>();
    //所有的版本
    mVersions = new ArrayList<>();
    //筛选过程中的临时版本
    mTempVersions = new ArrayList<>();
    //获取到所有的商品
    shopLists = responseDto.getMsg().getChilds();
    callBack.refreshSuccess("¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax(), responseDto.getMsg().getParent().getName());
    callBack.parentName(responseDto.getMsg().getParent().getName());
    //遍历商品
    Observable.from(shopLists)
    //转换对象 获取所有商品的属性集合
    .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
    .subscribe(attrsEntity -> {
    //判断颜色
    if (mActivity.getString(R.string.shop_color).equals(attrsEntity.getAttrname()) && !mTempColors.contains(attrsEntity.getAttrvalue())) {
    mColors.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempColors.add(attrsEntity.getAttrvalue());
    }
    //判断制式
    if (mActivity.getString(R.string.shop_standard).equals(attrsEntity.getAttrname()) && !mTempVersions.contains(attrsEntity.getAttrvalue())) {
    mVersions.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempVersions.add(attrsEntity.getAttrvalue());
    }
    //判断内存
    if (mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()) && !mTempMonerys.contains(attrsEntity.getAttrvalue())) {
    mMonerys.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempMonerys.add(attrsEntity.getAttrvalue());
    }
    });

    // 提取出 每种颜色的照片
    tempImageColor = new ArrayList<>();
    mImages = new ArrayList<>();
    //遍历所有的商品列表
    Observable.from(shopLists)
    .subscribe(childsEntity -> {
    String color = childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue();
    if (!tempImageColor.contains(color)) {
    mImages.add(childsEntity.getShowimg());
    tempImageColor.add(color);
    }
    });
    // 提取出 每种颜色的照片

    //通知图片
    callBack.changeData(mImages, "¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax());
    callBack.complete(null);
    }

    初始化属性列表

    属性之间是有一些关系的,比如我这里是以颜色为初始第一项,那么我就得根据颜色筛选出这个颜色下的所有内存,然后根据内存筛选出所有的版本。同时,只要颜色、内存、版本三个都选择了,就得筛选出这个商品。

    {颜色>内存>版本}>具体商品

    颜色

    初始化颜色,设置选择监听,一旦用户选择了某个颜色,那么需要获取这个颜色下的所有内存,并且要开始尝试获取商品详情。

    1. 初始化颜色

       /**
      * 初始化颜色
      *
      * @hint
      */

      private void initShopColor() {
      for (TagInfo mColor : mColors) {
      //初始化所有的选项为未选择状态
      mColor.setSelect(false);
      }
      tvColor.setText("\"未选择颜色\"");
      mColors.get(colorPositon).setSelect(true);
      colorAdapter = new ProperyTagAdapter(mActivity, mColors);
      rlShopColor.setAdapter(colorAdapter);
      colorAdapter.notifyDataSetChanged();
      rlShopColor.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopColor.setOnTagSelectListener((parent, selectedList) -> {
      colorPositon = selectedList.get(0);
      strColor = mColors.get(colorPositon).getText();
      // L.e("选中颜色:" + strColor);
      tvColor.setText("\"" + strColor + "\"");
      //获取颜色照片
      initColorShop();
      //查询商品详情
      iterationShop();
      });
      }
    2. 获取颜色下所有的内存和该颜色的照片

       /**
      * 初始化相应的颜色的商品 获得 图片
      */

      private void initColorShop() {
      //初始化 选项数据
      Observable.from(mMonerys).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      L.e("开始筛选颜色下的内存----------------------------------------------------------------------------------");
      final List tempColorMemery = new ArrayList<>();
      //筛选内存
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()))
      .subscribe(attrsEntity -> {
      tempColorMemery.add(attrsEntity.getAttrvalue());
      // L.e("内存:"+attrsEntity.getAttrvalue());
      });

      Observable.from(mTempMonerys)
      .filter(s -> !tempColorMemery.contains(s))
      .subscribe(s -> {
      L.e("没有的内存:" + s);
      mMonerys.get(mTempMonerys.indexOf(s)).setChecked(false);
      });
      momeryAdapter.notifyDataSetChanged();
      L.e("筛选颜色下的内存完成----------------------------------------------------------------------------------");

      //获取颜色的照片
      ImageHelper.loadImageFromGlide(mActivity, mImages.get(tempImageColor.indexOf(strColor)), ivShopPhoto);
      }
    1. 根据选中的属性查询是否存在该商品

       /**
      * 迭代 选择商品属性
      */

      private void iterationShop() {
      // 选择的内存 选择的版本 选择的颜色
      if (strMemory == null || strVersion == null || strColor == null)
      return;
      //隐藏购买按钮 显示为缺货
      resetBuyButton(false);
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(1).getAttrvalue().equals(strVersion))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .subscribe(childsEntity -> {
      L.e(childsEntity.getShopprice());
      tvPrice.setText("¥" + childsEntity.getShopprice());
      // ImageHelper.loadImageFromGlide(mActivity, Constant.IMAGE_URL + childsEntity.getShowimg(), ivShopPhoto);
      L.e("已找到商品:" + childsEntity.getName() + " id:" + childsEntity.getPid());
      selectGoods = childsEntity;
      tvShopName.setText(childsEntity.getName());
      //显示购买按钮
      resetBuyButton(true);
      initShopStagesCount++;
      });
      }

    内存

    通过前面一步,已经获取了所有的内存。这一步只需要展示该所有内存,设置选择监听,选择了某个内存后就根据 选择颜色>选择内存 获取所有的版本。并在在其中也是要iterationShop()查询商品的,万一你是往回点的时候呢?

    1. 初始化版本

       /**
      * 初始化内存
      */

      private void initShopMomery() {
      for (TagInfo mMonery : mMonerys) {
      mMonery.setSelect(false);
      Log.e(" ", "initShopMomery: " + mMonery.getText());
      }
      tvMomey.setText("\"未选择内存\"");
      mMonerys.get(momeryPositon).setSelect(true);
      //-----------------------------创建适配器
      momeryAdapter = new ProperyTagAdapter(mActivity, mMonerys);
      rlShopMomery.setAdapter(momeryAdapter);
      rlShopMomery.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopMomery.setOnTagSelectListener((parent, selectedList) -> {
      momeryPositon = selectedList.get(0);
      strMemory = mMonerys.get(momeryPositon).getText();
      // L.e("选中内存:" + strMemory);
      iterationShop();
      tvMomey.setText("\"" + strMemory + "\"");
      iterationVersion();
      });
      }
    2. 根据已选择的颜色和内存获取到版本

       /**
      * 迭代 获取版本信息
      */

      private void iterationVersion() {
      if (strColor == null || strMemory == null) {
      return;
      }
      // L.e("开始迭代版本");
      Observable.from(mVersions).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      final List iterationTempVersion = new ArrayList<>();
      //1. 遍历出 这个颜色下的所有手机
      //2. 遍历出 这些手机的所有版本
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> attrsEntity.getAttrname().equals(mActivity.getString(R.string.shop_standard)))
      .subscribe(attrsEntity -> {
      iterationTempVersion.add(attrsEntity.getAttrvalue());
      });

      Observable.from(mTempVersions).filter(s -> !iterationTempVersion.contains(s)).subscribe(s -> {
      mVersions.get(mTempVersions.indexOf(s)).setChecked(false);
      });
      versionAdapter.notifyDataSetChanged();
      // L.e("迭代版本完成");
      }

    版本

    其实到了这一步,已经算是完成了,只需要设置监听,获取选中的版本,然后开始查询商品。

        /**
    * 初始化版本
    */

    private void initShopVersion() {
    for (TagInfo mVersion : mVersions) {
    mVersion.setSelect(false);
    }
    tvVersion.setText("\"未选择版本\"");
    mVersions.get(versionPositon).setSelect(true);
    //-----------------------------创建适配器
    versionAdapter = new ProperyTagAdapter(mActivity, mVersions);
    rlShopVersion.setAdapter(versionAdapter);
    rlShopVersion.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
    rlShopVersion.setOnTagSelectListener((parent, selectedList) -> {
    versionPositon = selectedList.get(0);
    strVersion = mVersions.get(versionPositon).getText();
    // L.e("选中版本:" + strVersion);
    iterationShop();
    tvVersion.setText("\"" + strVersion + "\"");
    });
    }

    完成

    最终效果图如下:

    筛选属性最终完成
    筛选属性最终完成

    不要在意后面的轮播图,那其实很简单的。

    代码下载:JiuYouYiShuSheng-Selector-master.zip

    收起阅读 »

    【开奖咯!】回帖晒晒端午节你们公司都发了什么?顺便抽个奖!~

    开奖咯!本次使用excel开奖,真实随机(参考链接https://www.excelhome.net/316.html)。部分用户回帖不符合活动要求,不参与本次开奖。参与回帖的10个随机幸运伙伴是:获得点赞最多的柳天明 5AuCf 4Lambert 3获得3...
    继续阅读 »
    开奖咯!本次使用excel开奖,真实随机(参考链接https://www.excelhome.net/316.html)。
    部分用户回帖不符合活动要求,不参与本次开奖。

    参与回帖的10个随机幸运伙伴是:


    获得点赞最多的

    柳天明 5
    AuCf 4
    Lambert 3

    获得3个最惨伙伴:

    yangjian、春春、孤狼☞小九

    请以上同学在6月17日 23:59前,将你的收件人,地址,电话,衣服图案(星空/字母)+尺码(L-3XL)信息发站内私信给@admin,超过领取截止时间未提交信息,视为放弃领取~

    感谢大家参与!下次见~

    =================================

    首先祝各位端午安康

    然而端午来临之际,各种群兴起了一些攀比之风

    有这样的



    还有这样的



    还有这样的



    然而我是这样的:




    不过节日没福利的同学们也没关系.环信精心为大家准备了端午福利 有福利的也可双喜临门!!!


    活动规则


    • 活动时间:即日起至 6 月 15 日 中午 12:00 截止
    • 参与方式 :在本篇帖子下留言关于端午福利或端午计划的回复(图文皆可,发图请单独开帖然后链接回到本帖下方)
    • 活动结束后,将从所有参与回帖的用户里随机抽取10人,赠送imgeek定制T恤。😉
    • 并且选出3个端午福利寒酸的盆友赠送夏日清凉挂脖风扇😆
    • 最多的前3名直接获得一件T恤!
    • 获奖名单将会在 6 月 15 日公布于本篇帖子下。
    T恤:



    收起阅读 »

    你还在用宏定义“iphoneX”判断安全区域(safe area)吗,教你正确使用Safe Area

    你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望...
    继续阅读 »

    你还在用宏定义“iphone X”判断安全区域(safe area)吗,教你正确使用Safe Area。
    iOS 7 之后苹果给 UIViewController 引入了 topLayoutGuide 和 bottomLayoutGuide 两个属性来描述不希望被透明的状态栏或者导航栏遮挡的最高位置(status bar, navigation bar, toolbar, tab bar 等)。这个属性的值是一个 length 属性( topLayoutGuide.length)。 这个值可能由当前的 ViewController 或者 NavigationController 或者 TabbarController 决定。

    1、一个独立的ViewController,不包含于任何其他的ViewController。如果状态栏可见,topLayoutGuide表示状态栏的底部,否则表示这个ViewController的上边缘。

    2、包含于其他ViewController的ViewController不对这个属性起决定作用,而是由容器ViewController决定这个属性的含义:

    3、如果导航栏(Navigation Bar)可见,topLayoutGuide表示导航栏的底部。

    4、如果状态栏可见,topLayoutGuide表示状态栏的底部。

    5、如果都不可见,表示ViewController的上边缘。

    6、这部分还比较好理解,总之是屏幕上方任何遮挡内容的栏的最底部。

    iOS 11 开始弃用了这两个属性, 并且引入了 Safe Area 这个概念。苹果建议: 不要把 Control 放在 Safe Area 之外的地方

    // These objects may be used as layout items in the NSLayoutConstraint API
    @available(iOS, introduced: 7.0, deprecated: 11.0)
    open var topLayoutGuide: UILayoutSupport {get}
    @available(iOS, introduced: 7.0, deprecated: 11.0)
    open var bottomLayoutGuide: UILayoutSupport { get}

    今天, 来研究一下 iOS 11 中新引入的这个 API。

    UIView 中的 safe area
    iOS 11 中 UIViewController 的 topLayoutGuide 和 bottonLayoutGuide 两个属性被 UIView 中的 safe area 替代了。

    open var safeAreaInsets: UIEdgeInsets {get}
    @available(iOS 11.0, *)
    open func safeAreaInsetsDidChange()

    safeAreaInsets

    这个属性表示相对于屏幕四个边的间距, 而不仅仅是顶部还有底部。这么说好像没有什么感觉, 我们来看一看这个东西分别在 iPhone X 和 iPhone 8 中是什么样的吧!

    什么都没有做, 只是新建了一个工程然后在 Main.storyboard 中的 UIViewController 中拖了一个橙色的 View 并且设置约束为:


    在 ViewController.swift 的 viewDidLoad 中打印

    override func viewDidLoad() {
    super.viewDidLoad()
    print(view.safeAreaInsets)
    }
    // 无论是iPhone 8 还是 iPhone X 输出结果均为
    // UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)


    iPhone 8 VS iPhone X Safe Area (竖屏)


    iPhone 8 VS iPhone X Safe Area (横屏)

    这样对比可以看出, iPhone X 同时具有上下, 还有左右的 Safe Area。

    **再来看这个例子: ** 拖两个自定义的 View, 这个 View 上有一个 显示很多字的Label。然后设置这两个 View 的约束分别是:

    let view1 = MyView()
    let view2 = MyView()
    view.addSubview(view1)
    view.addSubview(view2)
    let screenW = UIScreen.main.bounds.size.width
    let screenH = UIScreen.main.bounds.size.height
    view1.frame = CGRect(x: 0, y: 0, width:screenW, height: 200)
    view2.frame = CGRect( x: 0, y: screenH - 200, width:screenW, height: 200)


    可以看出来, 子视图被顶部的刘海以及底部的 home 指示区挡住了。我们可以使用 frame 布局或者 auto layout 来优化这个地方:

    let insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? UIEdgeInsets.zero  
    view1.frame = CGRect(x: insets.left,y: insets.top,width:view.bounds.width - insets.left - insets.right,height: 200)
    view2.frame = CGRect(x: insets.left,y: screenH - insets.bottom - 200,width:view.bounds.width - insets.left - insets.right,height: 200)


    这样起来好多了, 还有另外一个更好的办法是直接在自定义的 View 中修改 Label 的布局:

    override func layoutSubviews() {
    super.layoutSubviews()
    if #available(iOS 11.0, *) {
    label.frame = safeAreaLayoutGuide.layoutFrame
    }
    }


    这样, 不仅仅是在 ViewController 中能够使用 safe area 了。

    UIViewController 中的 safe area

    在 iOS 11 中 UIViewController 有一个新的属性

    @available(iOS 11.0, *)
    open var additionalSafeAreaInsets: UIEdgeInsets

    当 view controller 的子视图覆盖了嵌入的子 view controller 的视图的时候。比如说, 当 UINavigationController 和 UITabbarController 中的 bar 是半透明(translucent) 状态的时候, 就有 additionalSafeAreaInsets


    自定义的 View 上面的 label 布局兼容了 safe area。

    // UIView
    @available(iOS 11.0, *)
    open func safeAreaInsetsDidChange()
    //UIViewController
    @available(iOS 11.0, *)
    open func viewSafeAreaInsetsDidChange()

    这两个方法分别是 UIView 和 UIViewController 的 safe area insets 发生改变时调用的方法,如果需要做一些处理,可以重写这个方法。有点类似于 KVO 的意思。

    模拟 iPhone X 的 safe area


    额外的 safe area insets 也能用来测试你的 app 是否支持 iPhone X。在没有 iPhone X 也不方便使用模拟器的时候, 这个还是很有用的。

    //竖屏
    additionalSafeAreaInsets.top = 24.0
    additionalSafeAreaInsets.bottom = 34.0
    //竖屏, status bar 隐藏
    additionalSafeAreaInsets.top = 44.0
    additionalSafeAreaInsets.bottom = 34.0
    //横屏
    additionalSafeAreaInsets.left = 44.0
    additionalSafeAreaInsets.bottom = 21.0
    additionalSafeAreaInsets.right = 44.0

    UIScrollView 中的 safe area
    在 scroll view 上加一个 label。设置scroll 的约束为:

    scrollView.snp.makeConstraints { (make)  in
    make.edges.equalToSuperview()
    }


    iOS 7 中引入 UIViewController 的 automaticallyAdjustsScrollViewInsets 属性在 iOS11 中被废弃掉了。取而代之的是 UIScrollView 的 contentInsetAdjustmentBehavior

    @available(iOS 11.0 , *)
    public enum UIScrollViewContentInsetAdjustmentBehavior : Int {
    case automatic //default value
    case scrollableAxes
    case never
    case always
    }
    @available(iOS 11.0 , *)
    open var contentInsetAdjustmentBehavior: UIScrollViewContentInsetAdjustmentBehavior

    Content Insets Adjustment Behavior

    never 不做调整。

    scrollableAxes content insets 只会针对 scrollview 滚动方向做调整。

    always content insets 会针对两个方向都做调整。

    automatic 这是默认值。当下面的条件满足时, 它跟 always 是一个意思

    1、能够水平滚动,不能垂直滚动

    2、scroll view 是 当前 view controller 的第一个视图

    3、这个controller 是被navigation controller 或者 tab bar controller 管理的

    4、automaticallyAdjustsScrollViewInsets 为 true

    在其他情况下 automoatc 跟 scrollableAxes 一样

    Adjusted Content Insets

    iOS 11 中 UIScrollView 新加了一个属性: adjustedContentInset

    @available(iOS 11.0, *)
    open var adjustedContentInset: UIEdgeInsets {get}

    adjustedContentInset 和 contentInset 之间有什么区别呢?

    在同时有 navigation 和 tab bar 的 view controller 中添加一个 scrollview 然后分别打印两个值:

    //iOS 10
    //contentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)
    //iOS 11
    //contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    //adjustedContentInset = UIEdgeInsets(top: 64.0, left: 0.0, bottom: 49.0, right: 0.0)

    然后再设置:

    `// 给 scroll view 的四个方向都加 10 的间距`
    `scrollView.contentInset = UIEdgeInsets(top: ``10``, left: ``10``, bottom: ``10``, right: ``10``)`

    打印:

    //iOS 10
    //contentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)
    //iOS 11
    //contentInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
    //adjustedContentInset = UIEdgeInsets(top: 74.0, left: 10.0, bottom: 59.0, right: 10.0)

    由此可见,在 iOS 11 中 scroll view 实际的 content inset 可以通过 adjustedContentInset 获取。这就是说如果你要适配 iOS 10 的话。这一部分的逻辑是不一样的。

    系统还提供了两个方法来监听这个属性的改变

    //UIScrollView
    @available(iOS 11.0, *)
    open func adjustedContentInsetDidChange()
    //UIScrollViewDelegate
    @available(iOS 11.0, *)
    optional public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)

    UITableView 中的 safe area

    我们现在再来看一下 UITableView 中 safe area 的情况。我们先添加一个有自定义 header 以及自定义 cell 的 tableview。设置边框为 self.view 的边框。也就是

    tableView.snp.makeConstraints { (make) in
    make.edges.equalToSuperview()
    }
    或者
    tableView.frame = view.bounds


    自定义的 header 上面有一个 lable,自定义的 cell 上面也有一个 label。将屏幕横屏之后会发现,cell 以及 header 的布局均自动留出了 safe area 以外的距离。cell 还是那么大,只是 cell 的 contnt view 留出了相应的距离。这其实是 UITableView 中新引入的属性管理的:

    @available(iOS 11.0, *)
    open var insetsContentViewsToSafeArea: Bool

    insetsContentViewsToSafeArea 的默认值是 true, 将其设置成 no 之后:


    可以看出来 footer 和 cell 的 content view 的大小跟 cell 的大小相同了。这就是说:在 iOS 11 下, 并不需要改变 header/footer/cell 的布局, 系统会自动区适配 safe area

    需要注意的是, Xcode 9 中使用 IB 拖出来的 TableView 默认的边框是 safe area 的。所以实际运行起来 tableview 都是在 safe area 之内的。


    UICollectionView 中的 safe area

    我们在做一个相同的 collection view 来看一下 collection view 中是什么情况:


    这是一个使用了 UICollectionViewFlowLayout 的 collection view。 滑动方向是竖向的。cell 透明, cell 的 content view 是白色的。这些都跟上面 table view 一样。header(UICollectionReusableView) 没有 content view 的概念, 所以给其自身设置了红色的背景。

    从截图上可以看出来, collection view 并没有默认给 header cell footer 添加safe area 的间距。能够将布局调整到合适的情况的方法只有将 header/ footer / cell 的子视图跟其 safe area 关联起来。跟 IB 中拖 table view 一个道理。


    现在我们再试试把布局调整成更像 collection view 那样:


    截图上可以看出来横屏下, 左右两边的 cell 都被刘海挡住了。这种情况下, 我们可以通过修改 section insets 来适配 safe area 来解决这个问题。但是再 iOS 11 中, UICollectionViewFlowLayout 提供了一个新的属性 sectionInsetReference 来帮你做这件事情。

    @available(iOS 11.0, *)
    public enum UICollectionViewFlowLayoutSectionInsetReference : Int {
    case fromContentInset
    case fromSafeArea
    case fromLayoutMargins
    }
    /// The reference boundary that the section insets will be defined as relative to. Defaults to .fromContentInset.

    /// NOTE: Content inset will always be respected at a minimum. For example, if the sectionInsetReference equals `.fromSafeArea`, but the adjusted content inset is greater that the combination of the safe area and section insets, then section content will be aligned with the content inset instead.
    @available(iOS 11.0, *)
    open var sectionInsetReference: UICollectionViewFlowLayoutSectionInsetReference

    可以看出来,系统默认是使用 .fromContentInset 我们再分别修改, 看具体会是什么样子的。

    fromSafeArea

    这种情况下 section content insets 等于原来的大小加上 safe area insets 的大小。

    跟使用 .fromLayoutMargins 相似使用这个属性 colection view 的 layout margins 会被添加到 section content insets 上面。


    IB 中的 Safe Area

    前面的例子都说的是用代码布局要实现的部分。但是很多人都还是习惯用 Interface Builder 来写 UI 界面。苹果在 WWDC 2107 Session 412 中提到:Storyboards 中的 safe area 是向下兼容的 也就是说, 即使在 iOS10 及以下的 target 中,也可以使用 safe area 来做布局。唯一需要做的就是给每个 stroyboard 勾选 Use Safe Area Layout Guide。实际测试看,应该是 iOS9 以后都只需要这么做。

    知识点: 在使用 IB 设置约束之后, 注意看相对的是 superview 还是 topLayoutGuide/bottomLayoutGuide, 包括在 Xcode 9 中勾选了 Use Safe Area Layout Guide 之后,默认应该是相对于 safe area 了。

    总结

    1、在适配 iPhone X 的时候首先是要理解 safe area 是怎么回事。盲目的 if iPhoneX{} 只会给之后的工作代码更多的麻烦。

    2、如果只需要适配到 iOS9 之前的 storyboard 都只需要做一件事情。

    3、Xcode9 用 IB 可以看得出来, safe area 到处都是了。理解起来很简单。就是系统对每个 View 都添加了 safe area, 这个区域的大小,是否跟 view 的大小相同是系统来决定的。在这个 View 上的布局只需要相对于 safe area 就可以了。每个 View 的 safe area 都可以通过 iOS 11 新增的 API safeAreaInsets 或者 safeAreaLayoutGuide 获取。

    4、对与 UIViewController 来说新增了 **additionalSafeAreaInsets **这个属性, 用来管理有 tabbar 或者 navigation bar 的情况下额外的情况。

    5、对于 UIScrollView, UITableView, UICollectionView 这三个控件来说,系统以及做了大多数的事情。

    6、scrollView 只需要设置 contentInsetAdjustmentBehavior 就可以很容易的适配带 iPhoneX

    7、tableView 只需要在 cell header footer 等设置约束的时候相对于 safe area 来做

    8、对 collection view 来说修改 sectionInsetReference 为 .safeArea 就可以做大多数的事情了。

    总的来说, safe area 可以看作是系统在所有的 view 上加了一个虚拟的 view, 这个虚拟的 view 的大小等都是跟 view 的位置等有关的(当然是在 iPhoneX上才有值) 以后在写代码的时候,自定义的控件都尽量针对 safe area 这个虚拟的 view 进行布局。
    文中有些图片都是从这里来的, 很多内容也跟这篇文章差不多 可能需要梯子

    参考文章 可能需要梯子

    作者:CepheusSun
    链接:http://www.jianshu.com/p/63c0b6cc66fd

    转自:https://www.jianshu.com/p/5bebc28e0ede

    收起阅读 »

    深度优先搜索和广度优先搜索

    不撞南墙不回头-深度优先搜索基础部分对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321把问题形象化,假如有1,2,3三张扑克牌和编...
    继续阅读 »

    不撞南墙不回头-深度优先搜索

    基础部分

    对于深度优先搜索和广度优先搜索,我很难形象的去表达它的定义。我们从一个例子来切入。

    输入一个数字n,输出1~n的全排列。即n=3时,输出123,132,213,231,312,321

    把问题形象化,假如有1,2,3三张扑克牌和编号为1,2,3的三个箱子,把三张扑克牌分别放到三个箱子里有几种方法?

    我们用深度优先遍历搜索的思想来考虑这个问题。

    到1号箱子面前时,我们手里有1,2,3三种牌,我们把1放进去,然后走到2号箱子面签,手里有2,3两张牌, 然后我们把2放进去,再走到3号箱子前,手里之后3这张牌,所以把3放进去,然后再往前走到我们想象出来的一个4号箱子前,我们手里没牌了,所以,前面三个箱子中放牌的组合就是要输出的一种组合方式。(123)

    然后我们后退到3号箱子,把3这张拍取出来,因为这时我们手里只有一张牌,所以再往里放的话还是原来那种情况,所以我们还要再往后推,推到2号箱子前,把2从箱子中取出来,这时候我们手里有2,3两张牌,这时我们可以把3放进2号箱子,然后走到3号箱子中把2放进去,这又是一种要输出的组合方式.(132)

    就找这个思路继续下去再次回退的时候,我们就要退到1号箱,取出1,然后分别放2和3进去,然后产生其余的组合方式。

    有点啰嗦,但是基本是这么一个思路。

    我们来看一下实现的代码

    def sortNumber(self, n):
    flag = [False for i in range(n)]
    a = [0 for i in range(n)]
    l = []

    def dfs(step):
    if step == n:
    l.append(a[:])
    return
    for i in range(n):
    if flag[i] is False:
    flag[i] = True
    a[step] = i
    dfs(step + 1)
    flag[i] = False
    dfs(0)
    return l

    输出是

    [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]

    我们创建的a这个list相当于上面说到的箱子,flag这个list呢,来标识某一个数字是否已经被用过了。

    其实主要的思想就这dfs方法里面的这个for循环中,在依次的排序中,我们默认优先使用最小的那个数字,这个for循环其实就代表了一个位置上有机会放所有的这些数字,这个flag标识就避免了在一个位置重复使用数字的问题。

    如果if 成立,说明当前位置可以使用这个数字,所以把这个数字放到a这个数组中,然后flag相同为的标识改为True,也就是说明这个数已经被占用了,然后在调用方法本身,进行下一步。

    flag[i] = False这句代码是很重要的,在上面的dfs(也就是下一步)结束之后,返回到当前这个阶段,我们必须模拟收回这个数字,也就是把flag置位False,表示这个数字又可以用了。

    思路大概就是这样子的,这就是深度优先搜索的一个简单的场景。用debug跟一下,一步一步的来看代码就更清晰的了。

    迷宫问题

    上面我们已经简单的了解了深度优先搜索,下面我们通过一个迷宫的问题来进一步数字这个算法,然后同时引出我们的广度优先搜索。

    迷宫是由m行n列的单元格组成,每个单元格要不是空地,要不就是障碍物,我们的任务是找到一条从起点到终点的最短路径。

    我们抽象成模型来看一下


    start代表起点,end代表终点,x代表障碍物也就是不能通过的点。

    首先我们来分析一下,从start(0,0)这个点,甚至说是每一个点出发,都有四个方向可以走,上下左右,仅对于(0,0)这个点来说,只能往右和下走,因为往左和上就到了单元格外面了,我们可以称之为越界了。

    我们用深度优先的思想来考虑的话,我们可以从出发点开始,全部都先往一个方向走,然后走到遇到障碍物或者到了边界的情况下,在改变另一个方向,然后再走到底,这样一直走下去。

    拿到我们这个题目中,我们可以这样来思考,在走的时候,我们规定一个右下左上这样的顺序,也就是先往右走,走到不能往右走的时候在变换方向。比如我们从(0,0)走到(0,1)这个点,在(0,1)这个点也是先往右走,但是我们发现(0,2)是障碍物,所以我们就改变为往下走,走到(1,1),然后在(1,1)开始也是先向右走,这样一直走下去,直到找到我们的目标点。

    其中我们要注意一点,在右下左上这四个方向中有一个方向是我们来时候的方向,在当前这个点,四个方向没有走完之前我们不要后退到上一个点,所以我们也需要一个像前面排数字代码里面的flag数组来记录当前位置时候被占用。我们必须是四个方向都走完了才能往后退到上一个换方向。

    下面我贴一下代码

    def depthFirstSearch(self):
    m = 5
    n = 4

    # 5行 4 列
    flag = [[False for i in range(n)] for j in range(m)]
    # 存储不能同行的位置
    a = [[False for i in range(n)] for j in range(m)]
    a[0][2] = True
    a[2][2] = True
    a[3][1] = True
    a[4][3] = True

    global min_step
    min_step = 99999

    director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

    def dfs(x, y, step):

    # 什么情况下停止 (找到目标坐标)
    if x == 3 and y == 2:
    global min_step
    if step < min_step:
    min_step = step
    return

    # 右下左上
    for i in range(4):
    # 下一个点
    nextX = x + director_l[i][0]
    nextY = y + director_l[i][1]

    # 是否越界
    if nextX < 0 or nextX >= m or nextY < 0 or nextY >= n:
    continue

    # 不是障碍 and 改点还没有走过
    if a[x][y] is False and flag[x][y] is False:
    flag[x][y] = True
    dfs(nextX, nextY, step+1)
    flag[x][y] = False #回收

    dfs(0, 0, 0)
    return min_step

    首先flag这个算是二位数组吧,来记录我们位置是否占用了,然后a这个数组,是来记录整个单元格的,也就是标识那些障碍物的位置坐标。同样的,重点是这个dfs方法,他的参数x,y是指当前的坐标,step是步数。

    这个大家可以看到一个director_l的数组,他是来辅助我们根据当前左边和不同方向计算下一个位置的坐标的。

    dfs中我们已经注明了搜索停止的判断方式,也就是找到(3,2)这个点,然后下面的for循环,则代表四个不同的方向,每一个方向我们都会先求出他的位置,然后判断是否越界,如果没有越界在判断是否是障碍或者是否已经走过了,满足了所有的判断条件,我们在继续往下一个点,直到找到目标,比较路径的步数。

    这就是深度优先搜索了,当然,这个题目我们还有别的解法,这就到了我们说的广度优先搜索。

    层层递进-广度优先搜索

    我们先大体说一下广度优先搜索的思路,深度优先是先穷尽一个方向,而广度优先呢,则是基于一个位置,先拿到他所有能到达的位置,然后分别基于这些新位置,拿到他们能到达的所有位置,一次这样层层的递进,直到找到我们的终点。


    从(0,0)出发,可以到达(0,1)和(1,0),然后再从(0,1)出发到达(1,1),从(1,0)出发,到达(2,0)和(1,1),以此类推。

    所以我们我们维护一个队列来储存每一层遍历到达的点,当然了,不要重复储存同一个点。我们用一个指针head来标识当前的基准位置,也就是说最开始指向(0,0),当储存完毕所有(0,0)能抵达的位置时,我们就应该改变我们的基准位置了,这时候head++,就到了(0,1)这个位置,然后储存完他能到的所有位置,head++,就到了(1,0),然后继续。

    def breadthFirstSearch(self):

    class Node:
    def __init__(self):
    x = 0
    y = 0
    step = 0

    m, n = 5, 4
    # 记录
    flag = [[False for i in range(n)] for j in range(m)]

    # 储存地图信息
    a = [[False for i in range(n)] for j in range(m)]
    a[0][2] = True
    a[2][2] = True
    a[3][1] = True
    a[4][3] = True
    # 队列
    l = []
    startX, startY, step = 0, 0, 0
    head = 0
    index = 0

    node = Node()
    node.x = startX
    node.y = startY
    node.step = step
    index += 1
    l.append(node)
    flag[0][0] = True

    director_l = [[0, 1], [1, 0], [0, -1], [-1, 0]]

    while head < index:

    last_node = l[head]
    # 处理四个方向
    for i in range(4):

    # 当前位置
    currentX = last_node.x + director_l[i][0]
    currentY = last_node.y + director_l[i][1]

    # 找到目标
    if currentX == 4 and currentY == 2:
    print('step = ' + str(last_node.step + 1))
    return

    #是否越界
    if currentX < 0 or currentY < 0 or currentX >= m or currentY >= n:
    continue

    if a[currentX][currentY] is False and flag[currentX][currentY] is False:


    #不是目标
    flag[currentX][currentY] = True

    node_new = Node()
    node_new.x = currentX
    node_new.y = currentY
    node_new.step = last_node.step+1
    l.append(node_new)
    index += 1



    head += 1

    首先我们定义了一个节点Node的类,来封装节点位置和当前的步数,flag,a,director_l这两个数组作用跟深度优先搜索相同,l是我们维护的队列,head指针指向当前基准的那个位置的,index指针指向队列尾。首先我们先把第一个Node(也就是起点)存进队列,广度优先搜索不需要递归,只要加一个循环就行。

    每次走到符合要求的位置,我们便把他封装成Node来存进对列中,每存一个index都要+1.

    head指针必须在一个节点四个方向都处理完了之后才可以+1,变换下一个基准节点。

    小结

    简单的介绍了深度优先搜索和广度优先搜索,深度优先有一种先穷尽一个方向然后结合使用回溯来找到解,广度呢,可能就是每做一次操作就涵盖了所有的可能结果,然后一步步往后推出去,找到最后的解。这算我个人的理解吧,不准确也不官方,思想也只能算是稍有体会,还得继续努力。

    题外话

    碍于自己的算法基础太差,最近一直在做算法题,我是先刷了一段时间的题目,发现吃力了,才开始看的书。感觉有点本末倒置。其实应该是先看看书,把算法的一些常用大类搞清楚了,形成一个知识框架,这样在遇到问题的时候可以知道往那些方向上面思考,可能会好一些吧。

    链接:https://www.jianshu.com/p/9a6a65078fc2

    收起阅读 »

    AndroidRoom库基础入门

    一、前言     Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充...
    继续阅读 »


    一、前言


        Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充分展示 SQLite 数据库的强大功能。Room 主要有以下几大优点:



    • 在编译时校验 SQL 语句;

    • 易用的注解减少重复和易错的模板代码;

    • 简化的数据库迁移路径。


        正是 Room 有以上的优点,所以建议使用 Room 访问数据库。


    二、Room 主要组件


        Room 主要组件有三个:



    • 数据库类(RoomDatabase):拥有数据库,并作为应用底层持久性数据的主要访问接入点。

    • 数据实体类(Entity):表示应用数据库中的表。

    • 数据访问对象(DAO):提供方法使得应用能够在数据库中查询、更新、插入以及删除数据。


        应用从数据库类获取一个与之相关联的数据访问对象(DAO)。应用可以通过这个数据访问对象(DAO)在数据库中检索数据,并以相关联的数据实体对象呈现结果;应用也可以使用对的数据实体类对象,更新数据库对应表中的行(或者插入新行)。应用对数据库的操作完全通过 Room 这个抽象层实现,无需直接操作 SQLite数据库。下图就是 Room 各个组件之间的关系图:


    Room组件关系图


    三、Room 基础入门


        大致了解了 Room 的工作原理之后,下面我们就来介绍一下 Room 的使用入门。


    3.1 引入 Room 库到项目


    引入 Room 库到项目,在项目程序模块下的 build.gradle 文件的 dependencies


    // Kotlin 开发环境,需要引入 kotlin-kapt 插件
    apply plugin: 'kotlin-kapt'

    // .........

    dependencies {
    // other dependecies

    def room_version = "2.3.0"
    implementation("androidx.room:room-runtime:$room_version")
    // 使用 Kotlin 注解处理工具(kapt,如果项目使用Kotlin语言开发,这个必须引入,并且需要引入 kotlin-kapt 插件
    kapt("androidx.room:room-compiler:$room_version")
    // To use Kotlin Symbolic Processing (KSP)
    // ksp("androidx.room:room-compiler:$room_version")

    // 可选 - 为 Room 添加 Kotlin 扩展和协程支持
    implementation("androidx.room:room-ktx:$room_version")

    // 可选 - 为 Room 添加 RxJava2 支持
    implementation "androidx.room:room-rxjava2:$room_version"

    // 可选 - 为 Room 添加 RxJava3 支持
    implementation "androidx.room:room-rxjava3:$room_version"

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation "androidx.room:room-guava:$room_version"

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")
    }


    注意事项:如果项目是用 kotlin 语言开发,一定要引入 kotlin 注解处理工具,并且在 build.gradle 中添加 kitlin-kapt插件(apply plugin: 'kotlin-kapt'),否则应用运行会抛出 xx.AppDatabase. AppDatabase_Impl does not exist 异常。



    3.2 Room 使用示例


        使用 Room 访问数据库,需要首先定义 Room 的三个组件,然后通过数据访问对象实例访问数据。


    3.2.1 定义数据实体类


        数据实体类对应数据库中的表,实体类的字段对应表中的列。定义 Room 数据实体类,使用 data class 关键字,并使用 @Entity 注解标注。更多关于数据实体类相关注解(包括属性相关注解),请参考: Android Room 数据实体类详解。如下代码所示:


    @Entity
    class User(@PrimaryKey val uid: Int, @ColumnInfo() val name: String, @ColumnInfo val age: Int)


    注意事项:默认情况下,Room 会根据实体类的类为表名(在数据库中表名其实不区分大小写),开发者也可以在 @Entity 注解通过 tableName 参数指定表名。



    3.3.2 定义数据访问对象(DAO)


        数据访问对象是访问数据库的桥梁,通过 DAO 访问数据,查询或者更新数据库中的数据(数据实体类是媒介)。数据访问对象(DAO)是一个接口,定义时添加 @Dao 注解标注,接口中的每一个成员方法表示一个操作,成员方法使用注解标示操作类型。更多关于数据访问对象(DAO)和数据操作类型注解,请参考:Android Room 数据访问对象详解。以下是简单的 DAO 示例代码:


    @Dao
    interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Query("SELECT * FROM user WHERE name LIKE :name")
    fun findByName(name: String): List<User>

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
    }


    注意事项:
    1. 数据访问对象是接口类型,成员方法是没有方法体的,成员方法必须使用注解标示操作类型;
    2. 数据库实体类成员方法中的 SQL 语句,在编译是会检查语法是否正确。



    3.3.3 定义数据库类


        数据库是存储数据的地方,使用 Room 定义数据库时,声明一个抽象类(abstract class),并用 @Database 注解标示,在 @Database 注解中使用 entities 参数指定数据库关联的数据实体类列表,使用 version 参数指定数据的版本。数据库类中包含获取数据访问实体类对象的抽象方法,更多关于数据库相关内容,请参考:Android Room 数据库详解,以下是简单的数据类定义。


    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase: RoomDatabase() {
    abstract fun userDao(): UserDao
    }


    注意事项:
    1. 数据库类是一个抽象类,他的成员方法是抽象方法;
    2. 定义数据库类时必须指定关联的数据实体类列表,这样数据库类才知道需要创建那些表;
    3. 数据的版本号,如果数据库的表构造有变动时,需要升级版本号,这样数据库才会更新表结构(如修改表字段、新增表等,跟直接使用 SQLite 接口使用 SQLiteDatabase 类一样),但是数据库的升级并不是修改版本号那么简单,还需要处理数据库升级过程中需要修改的地方,更多详情请参考:Android Room 数据库升级



    3.3.4 创建数据库实例


        定义好数据实体类、数据访问对象(DAO)和数据类之后,便可以创建数据库实例。使用 Room.databaseBuilder().build() 创建一个数据库实体类,Room 会根据定义的数据实体类、数据库访问对象和数据库类,以及他们定义时指定的对应关系,自动创建数据库和对应的表关系。如以下示例代码所示:


    val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db").build()


    注意事项:
    1. 每一个 RoomDatabase 实例都是非常耗费资源的,如果你的应用是单个进程中运行,那么在实例 RoomDatabase 时请遵循单例设计模式,在单个进程中几乎不需要访问多个 RoomDatabase 实例。
    2. 如果你的应用在多个进程中运行(比如:远程服务(RemoteService)),在构建 RoomDatabase 的构建器中调用 Room.databaseBuilder().enableMultiInstanceInvalidation() 方法,这样一来,在每个进程中都有一个 RoomDatabase 实例,如果在某个进程中将共享的数据库文件失效,将会自动将这个失效自动同步给其他进程中的 RoomDatabase 实例。



    3.3.5 从数据库实例中获取数据访问对象(DAO)实例


        在定义数据库类时,将数据访问对象(DAO)类与之相关联,定义抽象方法返回对应的数据库访问对象(DAO)实例。在数据库实例化过程中,Room 会自动生成对应的数据访问对象(DAO),只需要调用定义数据库类时定义的抽象方法,即可获取对应的数据访问对象(DAO)实例。如下示例所示:


    val userDao = db.userDao()

    3.3.6 通过数据访问对象(DAO)实例操作数据库


        获取到数据访问对象(DAO)实例,就可以调用数据库访问对象(DAO)类中定义的方法操作数据库了。如下示例所示:


    Thread {
    // 插入数据
    userDao.insertAll(
    User(1, "Student1", 18),
    User(2, "Student2", 18),
    User(3, "Student3", 17),
    User(4, "Student4", 19)
    )

    // 查询数据
    val result = userDao.getAll()

    result.forEach {
    println("Student: id = ${it.uid}, name = ${it.name}, age = ${it.age}")
    }
    }.start()


    注意事项:
    1. 使用数据访问对象(DAO)实例操作数据库时,不能再 UI 主线程中调用 DAO 接口,否则会抛出异常(java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.



    四、编后语


        Room 是非常强大易用的,可以减少数据库操作过程中的出错,因为所有的 SQL 语句都在编译是进行检查,如果存在错误,将会在编译时就显示错误信息。不仅如此,Room 还非常优秀地处理了多进程很多线程访问数据库的问题。




    ————————————————
    版权声明:本文为CSDN博主「精装机械师」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/yingaizhu/article/details/117514630

    收起阅读 »

    Android数据库—SQLite

    Android数据库—SQLite 不适合存储大规模数据 用来存储每一个用户各自的信息 在线查看数据库方法 Android Studio查看SQLite数据库方法大全 从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻...
    继续阅读 »


    Android数据库—SQLite



    • 不适合存储大规模数据

    • 用来存储每一个用户各自的信息


    在线查看数据库方法


    Android Studio查看SQLite数据库方法大全



    从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻墙,比较麻烦。


    如今最新版的Android Studio可以直接在里面查看数据库,无需别的了。




    • stetho使用



      • build.gradle文件中引入依赖

        implementation 'com.facebook.stetho:stetho:1.5.1'


      • 在需要操作数据库的Activity中加入以下语句

      Stetho.initializeWithDefaults(this);


      • 谷歌调试



    继承SQLiteOpenHelper的类,加载驱动



    继承SQLiteOpenHelper类,实现三个方法。



    • 构造函数

    • 建表方法:onCreate方法

    • 更新表方法:onUpgrade方法




    • MySQLiteOpenHelper


    package com.hnucm.androiddatabase;

    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    import androidx.annotation.Nullable;

    //加载数据库驱动
    //建立连接
    public class MySQLiteOpenHelper extends SQLiteOpenHelper {
    //构造方法
    //name -> 数据库名字
    public MySQLiteOpenHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
    super(context, name, factory, version);
    }

    //建表
    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
    //建表语句 自增长 主键
    sqLiteDatabase.execSQL("create table products(id integer primary key autoincrement,name varchar(20),singleprice double,restnum integer) ");
    }

    //更新表
    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
    }

    在Activity中进行增删改查



    整个Activity都是用数据库,所以声明驱动和数据库为全局变量,方便使用。



    //加载驱动
    mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
    "product",null,1);
    //得到数据库
    sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();


    布局文件中设置四个按钮,进行增删改查操作。




    • 布局-------activity_main.xml


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">


    <Button
    android:id="@+id/insert"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="增加一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/delete"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="删除一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/update"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="修改一条商品信息"
    android:textSize="25sp"
    />


    <Button
    android:id="@+id/select"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="查询一条商品信息"
    android:textSize="25sp"
    />


    </LinearLayout>


    • 总体逻辑代码-------MainActivity


    package com.hnucm.androiddatabase;

    import androidx.appcompat.app.AppCompatActivity;

    import android.content.ContentValues;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;

    public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    //声明增删改查四个按钮
    Button addBtn;
    Button delBtn;
    Button updateBtn;
    Button selectBtn;
    //声明驱动
    MySQLiteOpenHelper mySQLiteOpenHelper;
    //声明数据库
    SQLiteDatabase sqLiteDatabase;
    //数据对象
    ContentValues contentValues;
    //增删改查条件变量
    String id;
    String name;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    //加载驱动
    mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
    "product",null,1);
    //得到数据库
    sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();

    //初始化四个按钮
    addBtn = findViewById(R.id.insert);
    delBtn = findViewById(R.id.delete);
    updateBtn = findViewById(R.id.update);
    selectBtn = findViewById(R.id.select);

    //点击四个按钮
    addBtn.setOnClickListener(this);
    delBtn.setOnClickListener(this);
    updateBtn.setOnClickListener(this);
    selectBtn.setOnClickListener(this);
    }

    //四个按钮的点击事件
    @Override
    public void onClick(View view) {
    switch (view.getId()){
    //增加数据
    case R.id.insert:
    //创建数据,使用ContentValues -> HashMap
    contentValues = new ContentValues();
    //自增长 主键 增加无需加入id
    //contentValues.put("id",1);
    contentValues.put("name","辣条");
    contentValues.put("singleprice",3.50);
    contentValues.put("restnum",12);
    //将创建好的数据对象加入数据库中的哪一个表
    sqLiteDatabase.insert("products",null,contentValues);
    break;
    //删除数据
    case R.id.delete:
    //删除条件
    id = "1";
    name = "辣条";
    //在哪张表里,根据条件删除
    sqLiteDatabase.delete("products","id = ? and name = ?",
    new String[]{id,name});
    break;
    //修改数据
    case R.id.update:
    //修改条件
    id = "2";
    //将满足条件的数据修改
    contentValues = new ContentValues();
    contentValues.put("name","薯片");
    //在数据库中修改
    sqLiteDatabase.update("products",contentValues,"id=?",
    new String[]{id});
    break;
    //查询所有数据
    case R.id.select:
    //采用cursor游标查询
    Cursor cursor = sqLiteDatabase.query("products",null,null,
    null,null,null,null);
    //游标下一个存在,即没有到最后
    while(cursor.moveToNext()){
    //每一条数据取出每一列
    int id = cursor.getInt(cursor.getColumnIndex("id"));
    name = cursor.getString(cursor.getColumnIndex("name"));
    double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
    int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
    //打印数据
    Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
    + singleprice + ",restnum:" + restnum);
    }
    break;
    }
    }
    }

    增加数据


    //创建数据,使用ContentValues -> HashMap
    contentValues = new ContentValues();
    //自增长 主键 增加无需加入id
    //contentValues.put("id",1);
    contentValues.put("name","辣条");
    contentValues.put("singleprice",3.50);
    contentValues.put("restnum",12);
    //将创建好的数据对象加入数据库中的哪一个表
    sqLiteDatabase.insert("products",null,contentValues);

    删除数据


    //删除条件
    id = "1";
    name = "辣条";
    //在哪张表里,根据条件删除
    sqLiteDatabase.delete("products","id = ? and name = ?",
    new String[]{id,name});

    修改数据


    //修改条件
    id = "2";
    //将满足条件的数据修改
    contentValues = new ContentValues();
    contentValues.put("name","薯片");
    //在数据库中修改
    sqLiteDatabase.update("products",contentValues,"id=?",
    new String[]{id});

    查询数据


    //采用cursor游标查询
    //没有查询条件,所以查询表中所有信息
    Cursor cursor = sqLiteDatabase.query("products",null,null,
    null,null,null,null);
    //游标下一个存在,即没有到最后
    while(cursor.moveToNext()){
    //每一条数据取出每一列
    int id = cursor.getInt(cursor.getColumnIndex("id"));
    name = cursor.getString(cursor.getColumnIndex("name"));
    double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
    int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
    //打印数据
    Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
    + singleprice + ",restnum:" + restnum);
    }
    收起阅读 »

    总是听到有人说AndroidX,到底什么是AndroidX?

    Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。An...
    继续阅读 »

    Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。

    Android系统在刚刚面世的时候,可能连它的设计者也没有想到它会如此成功,因此也不可能在一开始的时候就将它的API考虑的非常周全。随着Android系统版本不断地迭代更新,每个版本中都会加入很多新的API进去,但是新增的API在老版系统中并不存在,因此这就出现了一个向下兼容的问题。

    举个例子,当Android系统发布到3.0版本的时候,突然意识到了平板电脑的重要性,因此为了让Android可以更好地兼容平板,Android团队在3.0系统(API 11)中加入了Fragment功能。但是Fragment的作用并不只局限于平板,以前的老系统中也想使用这个功能该怎么办?于是Android团队推出了一个鼎鼎大名的Android Support Library,用于提供向下兼容的功能。比如我们每个人都熟知的support-v4库,appcompat-v7库都是属于Android Support Library的,这两个库相信任何做过Android开发的人都使用过。

    但是可能很多人并没有考虑过support-v4库的名字到底是什么意思,这里跟大家解释一下。4在这里指的是Android API版本号,对应的系统版本是1.6。那么support-v4的意思就是这个库中提供的API会向下兼容到Android 1.6系统。它对应的包名如下:

    类似地,appcompat-v7指的是将库中提供的API向下兼容至API 7,也就是Android 2.1系统。它对应的包名如下:

    可以发现,Android Support Library中提供的库,它们的包名都是以android.support.*开头的。

    但是慢慢随着时间的推移,什么1.6、2.1系统早就已经被淘汰了,现在Android官方支持的最低系统版本已经是4.0.1,对应的API版本号是15。support-v4、appcompat-v7库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。

    那么很明显,Android团队也意识到这种命名已经非常不合适了,于是对这些API的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX本质上其实就是对Android Support Library进行的一次升级,升级内容主要在于以下两个方面。

    第一,包名。之前Android Support Library中的API,它们的包名都是在android.support.*下面的,而AndroidX库中所有API的包名都变成了在androidx.*下面。这是一个很大的变化,意味着以后凡是android.*包下面的API都是随着Android操作系统发布的,而androidx.*包下面的API都是随着扩展库发布的,这些API基本不会依赖于操作系统的具体版本。

    第二,命名规则。吸取了之前命名规则的弊端,AndroidX所有库的命名规则里都不会再包含具体操作系统API的版本号了。比如,像appcompat-v7库,在AndroidX中就变成了appcompat库。

    一个AndroidX完整的依赖库格式如下所示:

    implementation 'androidx.appcompat:appcompat:1.0.2'

    了解了AndroidX是什么之后,现在你应该放轻松了吧?它其实并不是什么全新的东西,而是对Android Support Library的一次升级。因此,AndroidX上手起来也没有任何困难的地方,比如之前你经常使用的RecyclerView、ViewPager等等库,在AndroidX中都会有一个对应的版本,只要改一下包名就可以完全无缝使用,用法方面基本上都没有任何的变化。

    但是有一点需要注意,AndroidX和Android Support Library中的库是非常不建议混合在一起使用的,因为它们可能会产生很多不兼容的问题。最好的做法是,要么全部使用AndroidX中的库,要么全部使用Android Support Library中的库。

    而现在Android团队官方的态度也很明确,未来都会为AndroidX为主,Android Support Library已经不再建议使用,并会慢慢停止维护。另外,从Android Studio 3.4.2开始,我发现新建的项目已经强制勾选使用AndroidX架构了。

    那么对于老项目的迁移应该怎么办呢?由于涉及到了包名的改动,如果从Android Support Library升级到AndroidX需要手动去改每一个文件的包名,那可真得要改死了。为此,Android Studio提供了一个一键迁移的功能,只需要对着你的项目名右击 → Refactor → Migrate to AndroidX,就会弹出如下图所示的窗口。

    这里点击Migrate,Android Studio就会自动检查你项目中所有使用Android Support Library的地方,并将它们全部改成AndroidX中对应的库。另外Android Studio还会将你原来的项目备份成一个zip文件,这样即使迁移之后的代码出现了问题你还可以随时还原回之前的代码。


    版权声明:本文为CSDN博主「guolin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/guolin_blog/article/details/97142065

    收起阅读 »

    ReactiveObjC看这里就够了

    1、什么是ReactiveObjCReactiveObjC是ReactiveCocoa系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传...
    继续阅读 »

    1、什么是ReactiveObjC

    ReactiveObjC是ReactiveCocoa
    系列的一个OC方面用得很多的响应式编程三方框架,其Swift方面的框架是(ReactiveSwift)。RAC用信号(类名为RACSignal)来代替和处理各种变量的变化和传递。核心思路:创建信号->订阅信号(subscribeNext)->发送信号
    通过信号signals的传输,重新组合和响应,软件代码的编写逻辑思路将变得更清晰紧凑,有条理,而不再需要对变量的变化不断的观察更新。

    2、什么是函数响应式编程

    响应式编程是一种和事件流有关的编程模式,关注导致状态值改变的改变的行为事件,一系列事件组成了事件流,一系列事件是导致属性值发生变化的原因,非常类似于设计模式中的观察者模式。在网上流传一个非常经典的解释响应式编程的概念,在一般的程序开发中:a = b + c,赋值之后 b 或者 c 的值变化后,a 的值不会跟着变化,而响应式编程的目标就是,如果 b 或者 c 的数值发生变化,a 的数值会同时发生变化;

    3、ReactiveObjC的流程分析

    ReactiveObjC主要有三个关键类:
    1、RACSignal信号
    RACSignal 是各种信号的基类,其中RACDynamicSignal是用的最多的动态信号
    2、RACSubscriber订阅者
    RACSubscriber是实现了RACSubscriber协议的订阅者类,这个协议定义了4个必须实现的方法

    @protocol RACSubscriber <NSObject>
    @required
    - (void)sendNext:(nullable id)value; //常见
    - (void)sendError:(nullable NSError *)error; //常见
    - (void)sendCompleted; //常见
    - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;
    @end

    RACSubscriber主要保存了三个block,跟三个常见的协议方法一一对应\

    @property (nonatomic, copy) void (^next)(id value);
    @property (nonatomic, copy) void (^error)(NSError *error);
    @property (nonatomic, copy) void (^completed)(void);

    3、RACDisposable清洁工
    RACDisposable主要是对资源的释放处理,其中使用RACDynamicSignal时,会创建一个RACCompoundDisposable管理清洁工对象。其内部定义了两个数组,一个是_inlineDisposables[2]固定长度2的A fast array,超出2个对象的长度由_disposables数组管理,_inlineDisposables数组速度快,两个数组都是线程安全的。

    4、ReactiveObjC导入工程的方式

    pod 'ReactiveObjC'

    5、ReactiveObjC的几种使用情况

    1、NSArray 数组遍历

    NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
    [array.rac_sequence.signal subscribeNext:^(id _Nullable x) {
    NSLog(@"数组内容:%@", x);
    }];

    2、NSArray快速替换数组中内容为99和单个替换数组内容,两个方法都不会改变原数组内容,操作完后都会生成一个新的数组,省去了创建可变数组然后遍历出来单个添加的步骤。

    NSArray * array = @[@"1",@"2",@"3",@"4",@"5",@"6"];
    /*
    NSArray * newArray = [[array.rac_sequence mapReplace:@"99"] array];
    NSLog(@"%@",newArray);
    */
    NSArray * newArray = [[array.rac_sequence map:^id _Nullable(id _Nullable value) {
    NSLog(@"原数组内容%@",value);
    return @"99";
    }] array];
    NSLog(@"%@",newArray);

    3、NSDictionary 字典遍历

    NSDictionary * dic = @{@"name":@"Tom",@"age":@"20"};
    [dic.rac_sequence.signal subscribeNext:^(id _Nullable x) {

    RACTupleUnpack(NSString *key, NSString * value) = x;//X为为一个元祖,RACTupleUnpack能够将key和value区分开
    NSLog(@"数组内容:%@--%@",key,value);
    }];

    4、UIButton 监听按钮的点击事件

    UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(100, 200, 100, 60);
    btn.backgroundColor = [UIColor blueColor];
    [self.view addSubview:btn];
    //监听点击事件
    [[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
    }];

    5、UITextField 监听输入框的一些事件

    UITextField * textF = [[UITextField alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
    textF.placeholder = @"请输入内容";
    textF.textColor = [UIColor blackColor];
    [self.view addSubview:textF];
    //实时监听输入框中文字的变化
    [[textF rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
    NSLog(@"输入框的内容--%@",x);
    }];
    //UITextField的UIControlEventEditingChanged事件,免去了KVO
    [[textF rac_signalForControlEvents:UIControlEventEditingChanged] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);
    }];
    //添加监听条件
    [[textF.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
    return [value isEqualToString:@"100"];//此处为判断条件,当输入的字符为100的时候执行下面方法
    }]subscribeNext:^(NSString * _Nullable x) {
    NSLog(@"输入框的内容为%@",x);
    }];

    6、KVO 代替KVO来监听按钮frame的改变

    UIButton * loginBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    loginBtn.frame = CGRectMake(100, 210, 100, 60);
    loginBtn.backgroundColor = [UIColor blueColor];
    [loginBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [loginBtn setTitle:@"666" forState:UIControlStateNormal];
    //[loginBtn setTitle:@"111" forState:UIControlStateDisabled];
    [self.view addSubview:loginBtn];
    //监听点击事件
    [[loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"%@",x);//x为一个button对象,别看类型为UIControl,继承关系UIButton-->UIControl-->UIView-->UIResponder-->NSObject
    x.frame = CGRectMake(100, 210, 200, 300);
    }];
    //KVO监听按钮frame的改变
    [[loginBtn rac_valuesAndChangesForKeyPath:@"frame" options:(NSKeyValueObservingOptionNew) observer:self] subscribeNext:^(RACTwoTuple<id,NSDictionary *> * _Nullable x) {
    NSLog(@"frame改变了%@",x);
    }];

    //下面方法也能监听,但是在按钮创建的时候此方法也执行了,简单说就是在界面展示之前此方法就走了一遍,总感觉怪怪的。
    /*
    [RACObserve(loginBtn, frame) subscribeNext:^(id _Nullable x) {
    NSLog(@"frame改变了%@",x);
    }];

    7、NSNotification 监听通知事件

    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardDidShowNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
    NSLog(@"监听键盘弹出"); //不知道为啥此方法不止走一次,但是原本的通知监听方法只走一次,有知道的可以私信我,谢谢
    }];

    8、timer 代替timer定时循环执行方法

    [[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    //这里面的方法2秒一循环
    }];
    //如果关闭定时器,停止需要创建一个全局的disposable
    //@property (nonatomic, strong) RACDisposable * disposable;//创建
    /*
    self.disposable = [[RACSignal interval:2.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    NSLog(@"当前时间:%@", x); // x 是当前的系统时间
    //关闭计时器
    [self.disposable dispose];
    }];
    */

    6、开发中用到的小栗子

    1、发送短信验证码的按钮倒计时

    /*
    @property (nonatomic, strong) RACDisposable * disposable;
    @property (nonatomic, assign) NSInteger time;
    */
    //上面两句要提前定义
    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(100, 200, 200, 50)];
    btn.titleLabel.textAlignment = NSTextAlignmentCenter;
    btn.backgroundColor = [UIColor greenColor];
    [btn setTitle:@"发送验证码" forState:(UIControlStateNormal)];
    [self.view addSubview:btn];
    [[btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    self.time = 10;
    btn.enabled = NO;
    [btn setTitle:[NSString stringWithFormat:@"请稍等%zd秒",self.time] forState:UIControlStateDisabled];
    self.disposable = [[RACSignal interval:1.0 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
    //减去时间
    self.time --;
    //设置文本
    NSString *text = (self.time > 0) ? [NSString stringWithFormat:@"请稍等%zd秒",_time] : @"重新发送";
    if (self.time > 0) {
    btn.enabled = NO;
    [btn setTitle:text forState:UIControlStateDisabled];
    }else{
    btn.enabled = YES;
    [btn setTitle:text forState:UIControlStateNormal];
    //关掉信号
    [self.disposable dispose];
    }
    }];
    }];

    2、登录按钮的状态根据账号和密码输入框内容的长度来改变

    UITextField *userNameTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 70, 200, 50)];
    UITextField *passwordTF = [[UITextField alloc]initWithFrame:CGRectMake(100, 130, 200, 50)];
    userNameTF.placeholder = @"请输入用户名";
    passwordTF.placeholder = @"请输入密码";
    [self.view addSubview:userNameTF];
    [self.view addSubview:passwordTF];
    UIButton *loginBtn = [[UIButton alloc]initWithFrame:CGRectMake(40, 180, 200, 50)];
    [loginBtn setTitle:@"马上登录" forState:UIControlStateNormal];
    [self.view addSubview:loginBtn];
    //根据textfield的内容来改变登录按钮的点击可否
    RAC(loginBtn, enabled) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
    return @(username.length >= 11 && password.length >= 6);
    }];
    //根据textfield的内容来改变登录按钮的背景色
    RAC(loginBtn, backgroundColor) = [RACSignal combineLatest:@[userNameTF.rac_textSignal, passwordTF.rac_textSignal] reduce:^id _Nullable(NSString * username, NSString * password){
    return (username.length >= 11 && password.length >= 6) ? [UIColor redColor] : [UIColor grayColor];
    }];

    结尾:
    本文参考:

    关于ReactiveObjC原理及流程简介https://www.jianshu.com/p/fecbe23d45c1

    响应式编程之ReactiveObjC常见用法https://www.jianshu.com/p/6af75a449d90

    【iOS 开发】ReactiveObjC(RAC)的使用汇总

    https://www.jianshu.com/p/0845b1a07bfa

    链接:https://www.jianshu.com/p/222c21007251

    收起阅读 »

    提升用户愉悦感的润滑剂-看SDWebImage本地缓存结构设计

    手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会...
    继续阅读 »

    手机应用发展到今天,用户的体验至关重要,有时决定着应用产品的生死,比如滑动一个商品列表时,用户自然地希望列表的滑动跟随手指,如丝般顺滑,如果卡顿,不耐烦的用户就会点退出按钮,商品也就失去了展示机会;
    而当一个用户发现自己装了某个APP后流量用的特别快,Ta可能会永远将这个APP打入冷宫。想要优化界面的响应、节省流量,本地缓存对用户而言是透明的,却是必不可少的一环。
    设计本地缓存并不是开一个数组或本地数据库,把数据丢进去就能达到预期效果的,这是因为:

    1、内存读写快,但容量有限,图片容易丢失;
    2、磁盘容量大,图片“永久”保存,但读写较慢。

    这对计算机与生俱来的矛盾,导致缓存设计必须将两种存储方式组合使用,加上iOS系统平台特性,无形中增加了本地缓存系统的复杂度,本篇来看看 SDWebImage 是如何实现一个流畅的缓存系统的。

    SDWebImage 本地缓存的整体流程如下:


    缓存数据的格式

    在深入具体的读写流程之前,先了解一下存储数据的格式,这有助于我们理解后续的操作步骤:

    1、为了加快界面显示的需要,内存缓存的图片用 UIImage;
    2、磁盘缓存的是 NSData,是从网络下载到的原始数据。

    写入流程

    存入图片时,调用入口方法:

    - (void)storeImage:(nullable UIImage *)image
    imageData:(nullable NSData *)imageData
    forKey:(nullable NSString *)key
    toDisk:(BOOL)toDisk
    completion:(nullable SDWebImageNoParamsBlock)completionBlock

    先写入 SDMemoryCache :

    [self.memCache setObject:image forKey:key cost:cost];

    再写入磁盘,由 ioQueue 异步执行:

    - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

    读取流程

    读取图片时,调用入口方法为:

    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock

    首先从内存缓存中获取:

    UIImage *image = [self imageFromMemoryCacheForKey:key];

    如果内存中有,直接返回给外部调用者;当内存缓存获取失败时,从磁盘获取图片文件数据:

    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];

    解码为 UIImage:

    diskImage = [self diskImageForKey:key data:diskData options:options];

    并写回内存缓存,再返回给调用者。

    磁盘缓存

    磁盘缓存位于沙盒的 Caches 目录
    下:/Library/Caches/default/com.hackemist.SDWebImageCache.default/,
    保证了缓存图片在下次启动还存在,又不会被iTunes备份。
    文件名由cachedFileNameForKey生成,使用Key(即图片URL)的MD5值,顺便说明一下,图片的Key还有其他作用:

    1、作为获取缓存的索引
    2、防止重复写入

    写入过程很简单:

    - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key

    利用 NSData 的文件写入方法:

    [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];

    内存缓存

    SDMemoryCache 是继承 NSCache 实现的,占用空间是用像素值来统计的(SDCacheCostForImage),因为 NSCache 的totalCostLimit 并不严格(关于 NSCache 的一些特性,请参考被忽视和误解的NSCache),用像素计算可以方
    便预估和加快运算。

    辅助内存缓存 weakCache

    你可能从看前面流程图时,就好奇这个辅助内存缓存的作用是什么,这是由于收到内存警告时,NSCache 里的图片可能已经被系统清除,但实际图片还是被界面上的 ImageView 保留着,因此在 weakCache 再保存一份,遇到这种情况时,只要简单地将 weakCache 中的值写回 NSCache 即可,这样提高了缓存命中率,也避免在界面保有图片时,缓存系统的误判,导致重复下载或从磁盘加载图片。
    weakCache 由 NSMapTable 实现,因为普通的NSDictionary无法分别对Key强引用,对值弱引用,即 weakCache 利用对 UIImage 的弱引用,可以判断是否被缓存以外的对象使用,是本地缓存加倍顺滑的关键喔。

    总结

    SDMemoryCache 的本地缓存很好地平衡了内存和磁盘的优缺点,最大限度利用了系统本身提供的 NSCache 和 NSData 的原生方法,巧妙地利用 weak 属性判断 UIImage 是否被引用问题,为我们开发提供了值得借鉴的思路。

    链接:https://www.jianshu.com/p/49ceb5f58590

    收起阅读 »

    几句代码轻松拥有扫码功能!

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。简单如斯,你不试试? Come on~ViewfinderVi...
    继续阅读 »

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。

    简单如斯,你不试试? Come on~

    ViewfinderView属性说明

    属性值类型默认值说明
    maskColorcolor#60000000扫描区外遮罩的颜色
    frameColorcolor#7F1FB3E2扫描区边框的颜色
    cornerColorcolor#FF1FB3E2扫描区边角的颜色
    laserColorcolor#FF1FB3E2扫描区激光线的颜色
    labelTextstring扫描提示文本信息
    labelTextColorcolor#FFC0C0C0提示文本字体颜色
    labelTextSizedimension14sp提示文本字体大小
    labelTextPaddingdimension24dp提示文本距离扫描区的间距
    labelTextWidthdimension提示文本的宽度,默认为View的宽度
    labelTextLocationenumbottom提示文本显示位置
    frameWidthdimension扫码框宽度
    frameHeightdimension扫码框高度
    laserStyleenumline扫描激光的样式
    gridColumninteger20网格扫描激光列数
    gridHeightinteger40dp网格扫描激光高度,为0dp时,表示动态铺满
    cornerRectWidthdimension4dp扫描区边角的宽
    cornerRectHeightdimension16dp扫描区边角的高
    scannerLineMoveDistancedimension2dp扫描线每次移动距离
    scannerLineHeightdimension5dp扫描线高度
    frameLineWidthdimension1dp边框线宽度
    scannerAnimationDelayinteger20扫描动画延迟间隔时间,单位:毫秒
    frameRatiofloat0.625f扫码框与屏幕占比
    framePaddingLeftdimension0扫码框左边的内间距
    framePaddingTopdimension0扫码框上边的内间距
    framePaddingRightdimension0扫码框右边的内间距
    framePaddingBottomdimension0扫码框下边的内间距
    frameGravityenumcenter扫码框对齐方式

    引入

    Gradle:

    最新版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:2.0.3'

    v1.x 旧版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9-androidx'

    //Android Support 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9'
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的JitPack来compile)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    版本说明

    v2.x 基于CameraX重构震撼发布

    v2.x 相对于 v1.x 的优势

    • v2.x基于CameraX,抽象整体流程,可扩展性更高。
    • v2.x基于CameraX通过预览裁剪的方式确保预览界面不变形,无需铺满屏幕,就能适配(v1.x通过遍历Camera支持预览的尺寸,找到与屏幕最接近的比例,减少变形的可能性(需铺满屏幕,才能适配))

    v2.x 特别说明

    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,那么动态权限申请相关都已经在CaptureActivity或CaptureFragment处理好了。
    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,如果有想要修改默认配置,可重写initCameraScan方法,修改CameraScan的配置即可,如果无需修改配置,直接在跳转原界面的onActivityResult 接收扫码结果即可(更多具体详情可参见app中的使用示例)。
    关于CameraX
    • CameraX暂时还是Beta版,可能会存在一定的稳定性,如果您有这个考量,可以继续使用 ZXingLite 以前的 v1.x 版本。相信不久之后CameraX就会发布稳定版。

    v1.x 说明

    【v1.1.9】 如果您正在使用 1.x 版本请点击下面的链接查看分支版本,当前 2.x 版本已经基于 Camerx 进行重构,不支持升级,请在新项目中使用。

    查看AndroidX版 1.x 分支 请戳此处

    查看Android Support版 1.x 分支 请戳此处

    查看 1.x API帮助文档

    使用 v1.x 版本的无需往下看了,下面的示例和相关说明都是针对于当前最新版本。

    示例

    布局示例

    可自定义布局(覆写getLayoutId方法),布局内至少要保证有PreviewView。

    PreviewView 用来预览,布局内至少要保证有PreviewView,如果是继承CaptureActivity或CaptureFragment,控件id可覆写getPreviewViewId方法自定义

    ViewfinderView 用来渲染扫码视图,给用户起到一个视觉效果,本身扫码识别本身没有关系,如果是继承CaptureActivity或CaptureFragment,控件id可复写getViewfinderViewId方法自定义,默认为previewView,返回0表示无需ViewfinderView

    ivFlashlight 用来内置手电筒,如果是继承CaptureActivity或CaptureFragment,控件id可复写getFlashlightId方法自定义,默认为ivFlashlight。返回0表示无需内置手电筒。您也可以自己去定义

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <com.king.zxing.ViewfinderView
    android:id="@+id/viewfinderView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <ImageView
    android:id="@+id/ivFlashlight"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:src="@drawable/zxl_flashlight_selector"
    android:layout_marginTop="@dimen/zxl_flashlight_margin_top" />
    </FrameLayout>

    或在你的布局中添加

        <include layout="@layout/zxl_capture"/>

    代码示例 (二维码/条形码)

        //跳转的默认扫码界面
    startActivityForResult(new Intent(context,CaptureActivity.class),requestCode);

    //生成二维码
    CodeUtils.createQRCode(content,600,logo);
    //生成条形码
    CodeUtils.createBarCode(content, BarcodeFormat.CODE_128,800,200);
    //解析条形码/二维码
    CodeUtils.parseCode(bitmapPath);
    //解析二维码
    CodeUtils.parseQRCode(bitmapPath);

    CameraScan配置示例

        //获取CameraScan,扫码相关的配置设置。CameraScan里面包含部分支持链式调用的方法,即调用返回是CameraScan本身的一些配置建议在startCamera之前调用。
    getCameraScan().setPlayBeep(true)//设置是否播放音效,默认为false
    .setVibrate(true)//设置是否震动,默认为false
    .setCameraConfig(new CameraConfig())//设置相机配置信息,CameraConfig可覆写options方法自定义配置
    .setNeedAutoZoom(false)//二维码太小时可自动缩放,默认为false
    .setNeedTouchZoom(true)//支持多指触摸捏合缩放,默认为true
    .setDarkLightLux(45f)//设置光线足够暗的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .setBrightLightLux(100f)//设置光线足够明亮的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .bindFlashlightView(ivFlashlight)//绑定手电筒,绑定后可根据光线传感器,动态显示或隐藏手电筒按钮
    .setOnScanResultCallback(this)//设置扫码结果回调,需要自己处理或者需要连扫时,可设置回调,自己去处理相关逻辑
    .setAnalyzer(new MultiFormatAnalyzer(new DecodeConfig()))//设置分析器,DecodeConfig可以配置一些解码时的配置信息,如果内置的不满足您的需求,你也可以自定义实现,
    .setAnalyzeImage(true)//设置是否分析图片,默认为true。如果设置为false,相当于关闭了扫码识别功能
    .startCamera();//启动预览


    //设置闪光灯(手电筒)是否开启,需在startCamera之后调用才有效
    getCameraScan().enableTorch(torch);

    CameraScan配置示例(只需识别二维码的配置示例)

            //初始化解码配置
    DecodeConfig decodeConfig = new DecodeConfig();
    decodeConfig.setHints(DecodeFormatManager.QR_CODE_HINTS)//如果只有识别二维码的需求,这样设置效率会更高,不设置默认为DecodeFormatManager.DEFAULT_HINTS
    .setFullAreaScan(false)//设置是否全区域识别,默认false
    .setAreaRectRatio(0.8f)//设置识别区域比例,默认0.8,设置的比例最终会在预览区域裁剪基于此比例的一个矩形进行扫码识别
    .setAreaRectVerticalOffset(0)//设置识别区域垂直方向偏移量,默认为0,为0表示居中,可以为负数
    .setAreaRectHorizontalOffset(0);//设置识别区域水平方向偏移量,默认为0,为0表示居中,可以为负数

    //在启动预览之前,设置分析器,只识别二维码
    getCameraScan()
    .setVibrate(true)//设置是否震动,默认为false
    .setAnalyzer(new MultiFormatAnalyzer(decodeConfig));//设置分析器,如果内置实现的一些分析器不满足您的需求,你也可以自定义去实现

    如果直接使用CaptureActivity需在您项目的AndroidManifest中添加如下配置

        <activity
    android:name="com.king.zxing.CaptureActivity"
    android:screenOrientation="portrait"
    android:theme="@style/CaptureTheme"/>

    快速实现扫码有以下几种方式:

    1、直接使用CaptureActivity或者CaptureFragment。(纯洁的扫码,无任何添加剂)

    2、通过继承CaptureActivity或者CaptureFragment并自定义布局。(适用于大多场景,并无需关心扫码相关逻辑,自定义布局时需覆写getLayoutId方法)

    3、在你项目的Activity或者Fragment中实例化一个CameraScan即可。(适用于想在扫码界面写交互逻辑,又因为项目架构或其它原因,无法直接或间接继承CaptureActivity或CaptureFragment时使用)

    4、继承CameraScan自己实现一个,可参照默认实现类DefaultCameraScan,其它步骤同方式3。(扩展高级用法,谨慎使用)

    其他

    需使用JDK8+编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    代码下载:ZXingLite.zip

    收起阅读 »

    Android一个专注于App更新,一键傻瓜式集成App版本升级的开源库!

    AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-update...
    继续阅读 »




    AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-updater和app-dialog。

    下载更新和弹框提示分开,是因为这本来就是两个逻辑。完全独立开来能有效的解耦。

    • app-updater 主要负责后台下载更新App,无需担心下载时各种配置相关的细节,一键傻瓜式升级。
    • app-dialog 主要是提供常用的Dialog和DialogFragment,简化弹框提示,布局样式支持自定义。

    app-updater + app-dialog 配合使用,谁用谁知道。

    功能介绍

    •  专注于App更新一键傻瓜式升级
    •  够轻量,体积小
    •  支持监听下载过程
    •  支持下载失败,重新下载
    •  支持下载优先取本地缓存
    •  支持通知栏提示内容和过程全部可配置
    •  支持Android Q(10)
    •  支持取消下载
    •  支持使用OkHttpClient下载

    Gif 展示

    Image

    引入

    Maven:

        //app-updater
    <dependency>
    <groupId>com.king.app</groupId>
    <artifactId>app-updater</artifactId>
    <version>1.0.10</version>
    <type>pom</type>
    </dependency>

    //app-dialog
    <dependency>
    <groupId>com.king.app</groupId>
    <artifactId>app-dialog</artifactId>
    <version>1.0.10</version>
    <type>pom</type>
    </dependency>

    Gradle:


    //----------AndroidX 版本
    //app-updater
    implementation 'com.king.app:app-updater:1.0.10-androidx'
    //app-dialog
    implementation 'com.king.app:app-dialog:1.0.10-androidx'

    //----------Android Support 版本
    //app-updater
    implementation 'com.king.app:app-updater:1.0.10'
    //app-dialog
    implementation 'com.king.app:app-dialog:1.0.10'

    Lvy:

        //app-updater
    <dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>

    //app-dialog
    <dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
        allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

        //一句代码,傻瓜式更新
    new AppUpdater(getContext(),url).start();
        //简单弹框升级
    AppDialogConfig config = new AppDialogConfig(context);
    config.setTitle("简单弹框升级")
    .setOk("升级")
    .setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
    .setOnClickOk(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new AppUpdater.Builder()
    .setUrl(mUrl)
    .build(getContext())
    .start();
    AppDialog.INSTANCE.dismissDialog();
    }
    });
    AppDialog.INSTANCE.showDialog(getContext(),config);
        //简单DialogFragment升级
    AppDialogConfig config = new AppDialogConfig(context);
    config.setTitle("简单DialogFragment升级")
    .setOk("升级")
    .setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
    .setOnClickOk(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new AppUpdater.Builder()
    .setUrl(mUrl)
    .setFilename("AppUpdater.apk")
    .build(getContext())
    .setHttpManager(OkHttpManager.getInstance())//不设置HttpManager时,默认使用HttpsURLConnection下载,如果使用OkHttpClient实现下载,需依赖okhttp库
    .start();
    AppDialog.INSTANCE.dismissDialogFragment(getSupportFragmentManager());
    }
    });
    AppDialog.INSTANCE.showDialogFragment(getSupportFragmentManager(),config);

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    混淆

    app-updater Proguard rules

    app-dialog Proguard rules

    代码下载:AppUpdater.zip

    收起阅读 »

    一个支持可拖动多边形,可拖动多边形的角改变其形状的任意多边形控件

    DragPolygonViewDragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。特性说明 支持添加多个任意多边形 支持通过触摸多边形拖动改变其位置 支...
    继续阅读 »


    DragPolygonView

    DragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。

    特性说明

    •  支持添加多个任意多边形
    •  支持通过触摸多边形拖动改变其位置
    •  支持通过触摸多边形的角改变其形状
    •  支持点击、长按、改变等事件监听
    •  支持多边形单选或多选模式

    Gif 展示

    Image

    DragPolygonView 自定义属性说明

    属性值类型默认值说明
    dpvStrokeWidthfloat4画笔描边的宽度
    dpvPointStrokeWidthMultiplierfloat1.0绘制多边形点坐标时基于画笔描边的宽度倍数
    dpvPointNormalColorcolor#FFE5574C多边形点的颜色
    dpvPointPressedColorcolor多边形点按下状态时的颜色
    dpvPointSelectedColorcolor多边形点选中状态时的颜色
    dpvLineNormalColorcolor#FFE5574C多边形边线的颜色
    dpvLinePressedColorcolor多边形边线按下状态的颜色
    dpvLineSelectedColorcolor多边形边线选中状态的颜色
    dpvFillNormalColorcolor#3FE5574C多边形填充的颜色
    dpvFillPressedColorcolor#7FE5574C多边形填充按下状态时的颜色
    dpvFillSelectedColorcolor#AFE5574C多边形填充选中状态时的颜色
    dpvAllowableOffsetsdimension16dp触点允许的误差偏移量
    dpvDragEnabledbooleantrue是否启用拖动多边形
    dpvChangeAngleEnabledbooleantrue是否启用多边形的各个角的角度支持可变
    dpvMultipleSelectionbooleanfalse是否是多选模式,默认:单选模式
    dpvClickToggleSelectedbooleanfalse是否点击就切换多边形的选中状态
    dpvAllowDragOutViewbooleanfalse是否允许多边形拖出视图范围
    dpvTextSizedimension16sp是否允许多边形拖出视图范围
    dpvTextNormalColorcolor#FFE5574C多边形文本的颜色
    dpvTextPressedColorcolor多边形文本按下状态的颜色
    dpvTextSelectedColorcolor多边形文本选中状态的颜色
    dpvShowTextbooleantrue是否显示多边形的文本
    dpvFakeBoldTextbooleanfalse多边形Text的字体是否为粗体

    引入

    Maven:

    <dependency>
    <groupId>com.king.view</groupId>
    <artifactId>dragpolygonview</artifactId>
    <version>1.0.2</version>
    <type>pom</type>
    </dependency>

    Gradle:

    implementation 'com.king.view:dragpolygonview:1.0.2'

    Lvy:

    <dependency org='com.king.view' name='dragpolygonview' rev='1.0.2'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    布局示例

        <com.king.view.dragpolygonview.DragPolygonView
    android:id="@+id/dragPolygonView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

    代码示例

        //添加多边形
    dragPolygonView.addPolygon(Polygon polygon);
    //添加多边形(多边形的各个点)
    dragPolygonView.addPolygon(PointF... points);
    //根据位置将多边形改为选中状态
    dragPolygonView.setPolygonSelected(int position);
    //改变监听
    dragPolygonView.setOnChangeListener(OnChangeListener listener);
    //点击监听
    dragPolygonView.setOnPolygonClickListener(OnPolygonClickListener listener);
    //长按监听
    dragPolygonView.setOnPolygonLongClickListener(OnPolygonLongClickListener listener)

    更多使用详情,请查看app中的源码使用示例

    代码下载:jenly1314-DragPolygonView-master.zip

    收起阅读 »

    iOS崩溃统计原理 & 日志分析整理

    简介当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。崩溃原因崩...
    继续阅读 »

    简介

    当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。
    包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。

    崩溃原因

    崩溃是指应用产生了系统不允许的行为时,系统终止其运行导致的现象。崩溃发生的原因有:

    1、存在CPU无法运行的代码
    不存在或者无法执行
    2、操作系统执行某项策略,终止程序
    启动时间过长或者消耗过多内存时,操作系统会终止程序运行
    3、编程语言为了避免错误终止程序:抛出异常
    4、开发者为了避免失败终止程序:Assert

    产生崩溃日志

    在程序出现以上问题时,系统会抛出异常,结束程序:
    出现异常情况,终止程序:


    分析崩溃日志

    在发生崩溃时,会产生崩溃日志并且保存在设备上,用于后期对问题定位,崩溃日志的内容包括以下部分:程序信息、异常信息、崩溃堆栈、二进制镜像。下面对每部分进行说明。

    崩溃日志程序信息:

    Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
    CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
    Hardware Model: iPad6,8
    Process: TheElements [303]
    Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
    Identifier: com.example.apple-samplecode.TheElements
    Version: 1.12
    Code Type: ARM-64 (Native)
    Role: Foreground
    Parent Process: launchd [1]
    Coalition: com.example.apple-samplecode.TheElements [402]

    Date/Time: 2016-08-22 10:43:07.5806 -0700
    Launch Time: 2016-08-22 10:43:01.0293 -0700
    OS Version: iPhone OS 10.0 (14A5345a)
    Report Version: 104

    汇总部分包含崩溃发生环境的基本信息:

    1、Incident Identifier:日志ID。

    2、CrashReport Key:设备匿名ID,同一设备的崩溃日志该值相同。

    3、Beta Identifier:设备和崩溃应用组合ID。

    4、Process:执行程序名,等同CFBundleExecutable。

    5、Version:程序版本号,等同CFBundleVersion/CFBundleVersionString。

    6、Code type:程序构造:ARM-64、ARM、x86

    异常信息:

    Exception Type: EXC_BAD_ACCESS (SIGSEGV)
    Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
    Termination Signal: Segmentation fault: 11
    Termination Reason: Namespace SIGNAL, Code 0xb
    Terminating Process: exc handler [0]
    Triggered by Thread: 0

    异常信息:

    1、Exception Codes:使用十六进制表示的程序特定信息,一般不展示。

    2、Exception Subtype:易读(相比十六进制地址)的异常信息。

    3、Exception Message:异常的额外信息。

    4、Exception Note:不特指某种异常类型的额外信息。

    5、Termination Reason:程序终止的异常信息。

    6、Triggered Thread:异常发生时的线程。

    崩溃堆栈:

    Thread 0 name: Dispatch queue: com.apple.main-thread
    Thread 0 Crashed:
    0 TheElements 0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)
    1 UIKit 0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
    2 UIKit 0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160
    3 QuartzCore 0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260
    4 libdispatch.dylib 0x000000018dd6d1c0 _dispatch_client_callout + 16
    5 libdispatch.dylib 0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000
    6 CoreFoundation 0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
    7 CoreFoundation 0x000000018ee8fb18 __CFRunLoopRun + 1660
    8 CoreFoundation 0x000000018edbe048 CFRunLoopRunSpecific + 444
    9 GraphicsServices 0x000000019083f198 GSEventRunModal + 180
    10 UIKit 0x0000000194d21bd0 -[UIApplication _run] + 684
    11 UIKit 0x0000000194d1c908 UIApplicationMain + 208
    12 TheElements 0x00000001000653c0 main (main.m:55)
    13 libdyld.dylib 0x000000018dda05b8 start + 4

    Thread 1:
    0 libsystem_kernel.dylib 0x000000018deb2a88 __workq_kernreturn + 8
    1 libsystem_pthread.dylib 0x000000018df75188 _pthread_wqthread + 968
    2 libsystem_pthread.dylib 0x000000018df74db4 start_wqthread + 4

    ...

    第一行列出了线程信息以及所在队列,之后是追溯链中独立栈帧的详细信息:

    1、栈帧号。栈帧号为0的代表当前执行停顿的函数,1则是调用当前停顿函数的主调函数,即0为1的被调函数,1为0的主调函数,以此类推。
    2、执行函数所在的二进制包
    3、地址信息:对于0栈帧来说,代表当前执行停顿的地址。其他栈帧则是获取控制权后接下来执行的地址。
    4、函数名

    二进制镜像:

    Binary Images:
    0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
    ...

    日之内包含多个二进制镜像,每个二进制镜像内包含以下信息:

    1、二进制镜像在程序内的地址空间
    2、二进制的名称或者bundleID
    3、二进制镜像的架构信息 arm64等
    4、二进制镜像的UUID,每次构建都会改变,该值用于在符号化日志时定位对应的dSYM文件。
    5、磁盘上的二进制路径

    符号化

    app.xcarchive文件,包内容包含dSYM和应用的二进制文件。
    更精确的符号化,可以结合崩溃日志、项目二进制文件、dSYM文件,对其进行反汇编,从而获得更详细的信息。

    符号化就是将追溯的地址信息转换成函数名及行数等信息,便于研发人员定位问题。
    当程序结束运行时,会产生崩溃日志,日志内包含每个线程的堆栈信息。当我们使用Xcode进行调试时,崩溃或者断点信息都会展示出实例和方法名等信息(符号信息)。相反,当应用被发布后,符号信息并不会包含在应用的二进制文件中,所以服务端收到的是未符号化的包含十六进制地址信息的日志文件。
    查看本机崩溃日志步骤如下:

    1、将手机连接到Mac
    2、启动Xcode->Window->Devices and simulators
    3、选择View Device Logs

    选择左侧应用,之后就可以在右侧看到崩溃日志信息:


    日志内包含符号化内容-[__NSArrayI objectAtIndex:]和十六进制地址0x000db142 0xb1000 + 172354。这种日志类型成为部分符号化崩溃日志。
    部分符号化的原因在于,Xcode只能符号化系统组件,例如UIKit、CoreFoundation等。但是对于非系统库产生的崩溃,在没有符号表的情况下就无法符号化。
    分析第三行未符号化的代码:

    0x000db142 0xb1000 + 172354

    以上内容说明了崩溃发生在内存地址0x000db142,此地址和0xb1000 + 172354是相等的。0xb1000代表这部分许的起始地址,172354代表偏移位。

    崩溃日志类型:

    崩溃日志可能包含几种状态:未符号化、完全符号化、部分符号化。
    未符号化的崩溃日志追溯链中没有函数的名字等信息,而是二进制镜像执行代码的十六进制地址。
    完全符号化的崩溃日志中,所有的十六进制地址都被替换为对应的函数符号。

    符号化流程

    符号化需要两部分内容:崩溃的二进制代码和编译产生的对应dSYM。

    符号表

    当编译器将源码转换为机器码时,会生成一个调试符号表,表内是二进制结构到原始源码的映射关系。调试符号表保存在dSYM(debug symbol调试符号表)文件内。调试模式下符号表会保存在编译的二进制内,发布模式则将符号表保存在dSYM文件内用于减少包的体积。

    当崩溃发生时,会在设备存储一份未符号化的崩溃日志
    获取崩溃日志后,通过dSYM对追溯链中的每个地址进行符号化,转换为函数信息,产生的结果就是符号化后的崩溃日志。

    函数调用堆栈

    我们知道,崩溃日志内包含函数调用的追溯信息,明白堆栈是怎么产生的有利于我们理解和分析崩溃日志。


    函数调用是在栈进行的,函数从调用和被调用方分为:主调函数和被调函数,这次我们只讨论每个函数在栈中的几个核心部分:

    1、上一个函数(主调函数)的堆栈信息。
    2、入参。
    3、局部变量。

    入参和局部变量容易理解,下面讨论为什么要保存主调函数的堆栈信息。
    说到这点就需要聊到寄存器。

    寄存器


    寄存器的类型和基本功能:

    eax:累加寄存器,用于运算。
    ebx:基址寄存器,用于地址索引。
    ecx:计数寄存器,用于计数。
    edx:数据寄存器,用于数据传递。
    esi:源变址寄存器,存放相对于DS段之源变址指针。
    edi:目的变址寄存器,存放相对于ES段之目的的变址指针。
    esp:堆栈指针,指向当前堆栈位置。
    ebp:基址指针寄存器,相对基址位置。

    寄存器约定

    背景:

    1、所有函数都可以访问和操作寄存器,寄存器对于单个CPU来说数量是固定的
    2、单个CPU来说,某一时刻只有一个函数在执行
    3、需要保证函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后使用的寄存器值

    被调函数在执行时,需要使用寄存器来保存数据和执行计算,但是在被调函数完成时,需要把寄存器还原,用于主调函数的执行,所以出现了寄存器约定。

    约定内容:

    1、主调函数的保存寄存器,在唤起被调函数前,需要显示的将其保存在栈中。
    主调寄存器:%eax、%edx、%ecx
    2、被调函数的保存寄存器,使用前压栈,并在函数返回前从栈中恢复原值。
    被调寄存器:%ebx、%esi、%edi
    3、被调函数必须保存%ebp和%esp,并在函数返回后恢复调用前的值。

    遵守寄存器约定的函数堆栈调用

    了解了寄存器功能和寄存器约定后,我们再看函数调用堆栈:


    1、栈帧逻辑:栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
    2、保存栈帧:被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。
    3、回溯:所以获取到崩溃时线程的ebp和esp 就能回溯到上一个调用,依次类推,回溯出所有的调用堆栈

    总结

    通过以上内容,我们了解了崩溃日志产生原理、崩溃日志内容和崩溃日志分析,下面分享几个分析崩溃日志的小提示作为结束:

    1、不止关注崩溃本行,结合上下文进行分析。
    2、不止关注崩溃线程,要结合其他线程的堆栈信息。
    3、通过多个崩溃日志,组合分析。
    4、使用地址定位和野指针工具重现内存问题。

    参考资料

    Apple Understanding and Analyzing Application Crash Reports
    Overview Of iOS Crash Reporting Tools
    Understanding Crashes and Crash Logs Video

    链接:https://www.jianshu.com/p/e05498960209

    收起阅读 »

    如何构建优雅的ViewController

    前言关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是...
    继续阅读 »

    前言

    关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是否足够清晰。ViewController也从很大程度上反应一个程序员的真实水平,一个平庸的程序员他的ViewController永远是臃肿的、肥胖的,什么功能都可以往里面塞,不同功能间缺乏清晰的界限。而一个优秀的程序员它的ViewController显得如此优雅,让你产生一种竟不能修改一笔一画的感觉。

    ViewController职责

    1、UI 属性 和 布局
    2、用户交互事件
    3、用户交互事件处理和回调

    用户交互事件处理: 通常会交给其他对象去处理

    回调: 可以根据具体的设计模式和应用场景交给 ViewController 或者其他对象处理

    而通常我们在阅读别人ViewController代码的时候,我们关注的是什么?

    控件属性配置在哪里?
    用户交互的入口位置在哪里?
    用户交互会产生什么样的结果?(回调在哪里?)
    所以从这个角度来说,这三个功能一开始就应该是被分离的,需要有清新明确的界限。因为谁都不希望自己在查找交互入口的时候 ,去阅读一堆控件冗长的控件配置代码, 更不愿意在一堆代码去慢慢理清整个用户交互的流程。 我们通常只关心我当前最关注的东西,当看到一堆无关的代码时,第一反应就是我想注释掉它。

    基于协议分离UI属性的配置

    protocol MFViewConfigurer {
    var rootView: UIView { get }
    var contentViews: [UIView] { get }
    var contentViewsSettings: [() -> Void] { get }

    func addSubViews()
    func configureSubViewsProperty()
    func configureSubViewsLayouts()

    func initUI()
    }

    依赖这个协议就可以完成所有控件属性配置,然后通过extension protocol 大大减少重复代码,同时提高可读性

    extension MFViewConfigurer {
    func addSubViews() {
    for element in contentViews {
    if let rootView = rootView as? UIStackView {
    rootView.addArrangedSubview(element)
    } else {
    rootView.addSubview(element)
    }
    }
    }

    func configureSubViewsProperty() {
    for element in contentViewsSettings {
    element()
    }
    }

    func configureSubViewsLayouts() {
    }

    func initUI() {
    addSubViews()
    configureSubViewsProperty()
    configureSubViewsLayouts()
    }
    }

    这里 我将控件的添加和控件的配置分成两个函数addSubViews和configureSubViewsProperty, 因为在我的眼里函数就应该遵循单一职责这个概念:
    addSubViews: 明确告诉阅读者,我这个控制器只有这些控件
    configureSubViewsProperty: 明确告诉阅读者,控件的所有属性配置都在这里,想要修改属性请阅读这个函数

    来看一个实例:

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    // 初始化 UI
    initUI()

    // 绑定用户交互事件
    bindEvent()

    // 将ViewModel.value 绑定至控件
    bindValueToUI()

    }

    // MARK: - UI configure

    // MARK: - UI

    extension MFWeatherViewController: MFViewConfigurer {
    var contentViews: [UIView] { return [scrollView, cancelButton] }

    var contentViewsSettings: [() -> Void] {
    return [{
    self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7)
    self.scrollView.hiddenSubViews(isHidden: false)
    }]
    }

    func configureSubViewsLayouts() {
    cancelButton.snp.makeConstraints { make in
    if #available(iOS 11, *) {
    make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
    } else {
    make.top.equalTo(self.view.snp.top).offset(20)
    }

    make.left.equalTo(self.view).offset(20)
    make.height.width.equalTo(30)
    }

    scrollView.snp.makeConstraints { make in
    make.top.bottom.left.right.equalTo(self.view)
    }
    }

    }


    而对于UIView 这套协议同样适用

    ```Swift
    // MFWeatherSummaryView
    private override init(frame: CGRect) {
    super.init(frame: frame)

    initUI()
    }


    // MARK: - UI

    extension MFWeatherSummaryView: MFViewConfigurer {
    var rootView: UIView { return self }

    var contentViews: [UIView] {
    return [
    cityLabel,
    weatherSummaryLabel,
    temperatureLabel,
    weatherSummaryImageView,
    ]
    }

    var contentViewsSettings: [() -> Void] {
    return [UIConfigure]
    }

    private func UIConfigure() {
    backgroundColor = UIColor.clear
    }

    public func configureSubViewsLayouts() {
    cityLabel.snp.makeConstraints { make in
    make.top.centerX.equalTo(self)
    make.bottom.equalTo(temperatureLabel.snp.top).offset(-10)
    }

    temperatureLabel.snp.makeConstraints { make in
    make.top.equalTo(cityLabel.snp.bottom).offset(10)
    make.right.equalTo(self.snp.centerX).offset(0)
    make.bottom.equalTo(self)
    }

    weatherSummaryImageView.snp.makeConstraints { make in
    make.left.equalTo(self.snp.centerX).offset(20)
    make.bottom.equalTo(temperatureLabel.snp.lastBaseline)
    make.top.equalTo(weatherSummaryLabel.snp.bottom).offset(5)
    make.height.equalTo(weatherSummaryImageView.snp.width).multipliedBy(61.0 / 69.0)
    }

    weatherSummaryLabel.snp.makeConstraints { make in
    make.top.equalTo(temperatureLabel).offset(20)
    make.centerX.equalTo(weatherSummaryImageView)
    make.bottom.equalTo(weatherSummaryImageView.snp.top).offset(-5)
    }
    }
    }

    由于我使用的是MVVM模式,所以viewDidLoad 和MVC模式还是有些区别,如果是MVC可能就是这样

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    // 初始化 UI
    initUI()

    // 用户交互事件入口
    addEvents()


    }

    // MARK: callBack
    ......

    由于MVC的回调模式很难统一,有Delegate, closure, notification 等等,所以回调通常会散落在控制器各个角落。最好加个MARK flag, 尽量收集在同一个区域中, 同时对于每个回调加上必要的注释:

    1、由哪种操作触发
    2、会导致什么后果
    3、最终会留下哪里

    所以从这个角度来说UITableViewDataSource 和 UITableViewDelegate 完全是两种不一样的行为, 一个是 configure UI , 一个是 control behavior , 所以不要在把这两个东西写一块了, 真的很难看。

    总结

    基于职责对代码进行分割,这样会让你的代码变得更加优雅简洁,会大大减少一些万金油代码的出现,减少阅读代码的成本也是我们优化的一个方向,比较谁都不想因为混乱的代码影响自己的心情

    链接:https://www.jianshu.com/p/266cbca1439c

    收起阅读 »

    【面试专题】Android屏幕刷新机制

    这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我...
    继续阅读 »

    这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我面网易云的时候也确实被问到了这个题目。


    屏幕刷新这一整套,你把我这篇文章里的内容讲清楚了,肯定ok了。网易云还附加问了我CPU和GPU怎么交换绘制数据的,这个我个人认为完全是加分题了,我答不出来,感兴趣的小伙伴可以去看一看,你要是能说清楚,肯定能让面试官眼前一亮。


    双缓冲


    在讲双缓冲这个概念之前,先来了解一些基础知识。


    显示系统基础


    在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分, CPU负责计算帧数据,把计算好的数据交给GPU, GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数 据呈现到屏幕上。



    • 画面撕裂


    屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一 帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即 buffer里的数据可能是来自不同的帧的。当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。


    简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。


    那咋解决画面撕裂呢? 答案是使用双缓冲。


    双缓冲


    由于图像绘制和屏幕读取 使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。


    双缓冲,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。


    VSync


    什么时候进行两个buffer的交换呢?


    假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。 看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。


    当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现画面撕裂的状况。


    VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。


    所以说VSync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了。


    Android屏幕刷新机制


    先总体概括一下,Android屏幕刷新使用的是“双缓存+VSync机制”,单纯的双缓冲模式容易造成jank(丢帧)现象,为了解决这个问题,Google在 Android4.1 提出了Project Butter(?油工程),引入了 drawing with VSync 的概念。


    jank(丢帧)


    VSync.jpeg


    以时间的顺序来看下将会发生的过程:



    1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,且在Display显示下一帧前完成

    2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧

    3. 接着第2帧开始处理,是直到第2个VSync快来前才开始处理的。

    4. 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。

    5. 当第2帧数据准备完成后,它并不会?上被显示,而是要等待下一个VSync 进行缓存交换再显示。


    所以总的来说,就是屏幕平白无故地多显示了一次第1帧。 原因是第2帧的CPU/GPU计算 没能在VSync信号到来前完成。


    这里注意一下一个细节,jank(丢帧、掉帧),不是说这一帧丢弃了不显示,而是这一帧延迟显示了,因为缓存交换的时机只能等下一个VSync了。


    黄油计划 —— drawing with VSync


    为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(?油工程): 系统在收到VSync pulse后,将?上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开 始计算然后把数据写入buffer。如下图:


    VSync2.jpeg


    CPU/GPU根据VSYNC信号同步处理数据,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。 一句话总结,VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。


    问题又来了,如果界面比较复杂,CPU/GPU的处理时间较?,超过了16.6ms呢?如下图:


    VSync3.jpeg



    1. 在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。

    2. 而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。

    3. 当下一个VSync出现时,CPU/GPU?上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。


    为什么 CPU 不能在第二个 16ms 处理绘制工作呢? 因为只有两个 buffer,Back buffer正在被GPU用来处理B帧的数据, Frame buffer的内容用于Display的显示,这样两个 buffer都被占用,CPU 则无法准备下一帧的数据。 那么,如果再提供一个buffer,CPU、GPU 和显示设备都能使用各自的 buffer工作,互不影响。这就是三缓冲的来源了。


    三缓冲


    三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。


    VSync4.jpeg



    1. 第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是 会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。

    2. 注意在第3段中,A帧的计算已完成,但是在第4个vsync来的时候才显示,如果是双缓冲,那在第三个vynsc就可以显示了。


    三缓冲有效利用了等待VSync的时间,减少了jank,但是带来了延迟。是不是 Buffer 越多越好呢?这个是否定的, Buffer 正常还是两个,当出现 Jank 后三个足以。


    Choreographer


    上边讲的都是基础的刷新知识,那么在 Android 系统中,真正来实现绘制的类叫Choreographer


    Choreographer负责对CPU/GPU绘制的指导 —— 收到VSync信号才开始绘制,保证绘制拥有完整 16.6ms,避免绘制的随机性。


    通常 应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的 ValueAnimator.start()、View.invalidate()等。


    (这边补充说一个面试题,属性动画更新时会回调onDraw吗?不会,因为它内部是通过AnimationHandler中的Choreographer机制来实现的更新,具体的逻辑,如果以后有时间的话可以写篇文章来说一说。)


    业界一般通过Choreographer来监控应用的帧率。


    (这个东西也是个面试题,会问你如何检测应用的帧率?你可以提一下Choreographer里面的FrameCallback,然后结合一些第三方库的实现具体说一下。)


    View刷新的入口


    Activity启动,走完onResume方法后,会进行window的添加。window添加过程会调用ViewRootImpl的setView()方法, setView()方法会调用requestLayout()方法来请求绘制布局,requestLayout()方法内部又会走到scheduleTraversals()方法。最后会走到performTraversals()方法,接着到了我们熟知的测量、布局、绘制三大流程了。


    当我们使用 ValueAnimator.start()、View.invalidate()时,最后也是走到ViewRootImpl的 scheduleTraversals()方法。(View.invalidate()内部会循环获取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源码)


    即所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。


    这里注意一个点:scheduleTraversals()之后不是立即就执行performTraversals()的,它们中间隔了一个Choreographer机制。简单来说就是scheduleTraversals()中,Choreographer会去请求native的VSync信号,VSync信号来了之后才会去调用performTraversals()方法进行View绘制的三大流程。



    //ViewRootImpl.java
    void scheduleTraversals() {
    if (!mTraversalScheduled) {
    mTraversalScheduled = true;
    //添加同步屏障,屏蔽同步消息,保证VSync到来立即执行绘制
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    //mTraversalRunnable是TraversalRunnable实例,最终走到run(),也即doTraversal();
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    if (!mUnbufferedInputDispatch) {
    scheduleConsumeBatchedInput();
    }
    notifyRendererOfFramePending();
    pokeDrawLockIfNeeded();
    }
    }
    final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
    doTraversal();
    }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    void doTraversal() {
    if (mTraversalScheduled) {
    mTraversalScheduled = false;
    //移除同步屏障
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ...
    //开始三大绘制流程
    performTraversals();
    ...
    }
    }


    1. postSyncBarrier 开启同步屏障,保证VSync到来后立即执行绘制

    2. mChoreographer.postCallback()方法,发送一个会在下一帧执行的回调,即在下一个VSync到来时会执行 TraversalRunnable–>doTraversal()—>performTraversals()–>绘制流程。


    Choreographer


    初始化


    mChoreographer,是在ViewRootImpl的构造方法内使用 Choreographer.getInstance()创建。


    Choreographer和Looper一样是线程单例的,通过ThreadLocal机制来保证唯一性。因为Choreographer内部通过FrameHandler来发送消息,所以初始化的时候会先判断当前线程有无Looper,没有的话直接抛异常。


    public static Choreographer getInstance() {
    return sThreadInstance.get();
    }

    private static final ThreadLocal<Choreographer> sThreadInstance =
    new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
    Looper looper = Looper.myLooper();
    if (looper == null) {
    //当前线程要有looper,Choreographer实例需要传入
    throw new IllegalStateException("The current thread must have a looper!");
    }
    Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
    if (looper == Looper.getMainLooper()) {
    mMainInstance = choreographer;
    }
    return choreographer;
    }
    };

    postCallback


    mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,第一个参数是CALLBACK_TRAVERSAL,表示回调任务的类型,共有以下5种类型:


    //输入事件,首先执行
    public static final int CALLBACK_INPUT = 0;
    //动画,第二执行
    public static final int CALLBACK_ANIMATION = 1;
    //插入更新的动画,第三执行
    public static final int CALLBACK_INSETS_ANIMATION = 2;
    //绘制,第四执行
    public static final int CALLBACK_TRAVERSAL = 3;
    //提交,最后执行,
    public static final int CALLBACK_COMMIT = 4;

    五种类型任务对应存入对应的CallbackQueue中,每当收到 VSYNC 信号时,Choreographer 将首先处理 INPUT 类型的任 务,然后是 ANIMATION 类型,最后才是 TRAVERSAL 类型。


    postCallback()内部调用postCallbackDelayed(),接着又调用postCallbackDelayedInternal(),正常消息执行scheduleFrameLocked,延迟运行的消息会发送一个MSG_DO_SCHEDULE_CALLBACK类型的meessage:


    private void postCallbackDelayedInternal(int callbackType,
    Object action, Object token, long delayMillis)
    {
    ...
    synchronized (mLock) {
    ...
    mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
    if (dueTime <= now) { //立即执行
    scheduleFrameLocked(now);
    } else {
    //延迟运行,最终也会走到scheduleFrameLocked()
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    msg.arg1 = callbackType;
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, dueTime);
    }
    }
    }

    FrameHandler这个类是内部专门用来处理消息的,可以看到延迟的MSG_DO_SCHEDULE_CALLBACK类型消息最终也是走到scheduleFrameLocked:


    private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
    super(looper);
    }
    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case MSG_DO_FRAME:
    // 执行doFrame,即绘制过程
    doFrame(System.nanoTime(), 0);
    break;
    case MSG_DO_SCHEDULE_VSYNC:
    //申请VSYNC信号,例如当前需要绘制任务时
    doScheduleVsync();
    break;
    case MSG_DO_SCHEDULE_CALLBACK:
    //需要延迟的任务,最终还是执行上述两个事件
    doScheduleCallback(msg.arg1);
    break;
    }
    }
    }

    void doScheduleCallback(int callbackType) {
    synchronized (mLock) {
    if (!mFrameScheduled) {
    final long now = SystemClock.uptimeMillis();
    if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
    scheduleFrameLocked(now);
    }
    }
    }
    }

    申请VSync信号


    scheduleFrameLocked()方法里面就会去真正的申请 VSync 信号了。


    private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
    mFrameScheduled = true;
    if (USE_VSYNC) {
    //当前执行的线程,是否是mLooper所在线程
    if (isRunningOnLooperThreadLocked()) {
    //申请 VSYNC 信号
    scheduleVsyncLocked();
    } else {
    // 若不在,就用mHandler发送消息到原线程,最后还是调用scheduleVsyncLocked方法
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
    msg.setAsynchronous(true);//异步
    mHandler.sendMessageAtFrontOfQueue(msg);
    }
    } else {
    // 如果未开启VSYNC则直接doFrame方法(4.1后默认开启)
    final long nextFrameTime = Math.max(
    mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
    Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
    msg.setAsynchronous(true);//异步
    mHandler.sendMessageAtTime(msg, nextFrameTime);
    }
    }
    }

    VSync信号的注册和监听是通过mDisplayEventReceiver实现的。mDisplayEventReceiver是在Choreographer的构造方法中创建的,是FrameDisplayEventReceiver的实例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子类,


    private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
    }

    public DisplayEventReceiver(Looper looper, int vsyncSource) {
    if (looper == null) {
    throw new IllegalArgumentException("looper must not be null");
    }
    mMessageQueue = looper.getQueue();
    // 注册native的VSYNC信号监听者
    mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,vsyncSource);
    mCloseGuard.open("dispose");
    }

    VSync信号回调


    native的VSync信号到来时,会走到onVsync()回调:


    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
    implements Runnable
    {

    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
    ...
    //将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
    mHavePendingVsync = false;
    doFrame(mTimestampNanos, mFrame);
    }
    }

    (这里补充一个面试题:页面UI没有刷新的时候onVsync()回调也会执行吗?不会,因为VSync是UI需要刷新的时候主动去申请的,而不是native层不停地往上面去推这个回调的,这边要注意。)


    doFrame


    doFrame()方法中会通过doCallbacks()方法去执行各种callbacks,主要内容就是取对应任务类型的队列,遍历队列执行所有任务,其中就包括了 ViewRootImpl 发起的绘制任务mTraversalRunnable了。mTraversalRunnable执行doTraversal()方法,移除同步屏障,调用performTraversals()开始三大绘制流程。


    到这里整个流程就闭环了。


    作者:吉原拉面
    链接:https://juejin.cn/post/6971330532785274917
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    java设计模式:备忘录模式

    前言 备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。 定义 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。 ...
    继续阅读 »


    前言


    备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。


    定义


    在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
    在这里插入图片描述


    优点


    提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
    实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
    简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。


    缺点


    资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。


    结构



    • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。

    • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。

    • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。


    实现


    生活中最常用的计算器自己拥有备忘录的功能,用户计算完后软件会自动为用户记录最后几次的计算结果,我们可以模拟用户使用计算器的过程,以及打开备忘录查看记录。


    package com.rabbit;

    /**
    * 备忘录发起人,模拟计算器加法运算
    * Created by HASEE on 2018/4/29.
    */
    public class Originator {

    private double num1;

    private double num2;

    //创建备忘录对象
    public Memento createMemento() {
    return new Memento(num1, num2);
    }

    public Originator(double num1, double num2) {
    this.num1 = num1;
    this.num2 = num2;
    System.out.println(num1 + " + " + num2 + " = " + (num1 + num2));
    }

    }
    package com.rabbit;

    /**
    * 备忘录,要保存的属性
    * Created by HASEE on 2018/4/29.
    */
    public class Memento {

    private double num1;//计算器第一个数字

    private double num2;//计算器第二个数字

    private double result;//计算结果

    public Memento(double num1, double num2) {
    this.num1 = num1;
    this.num2 = num2;
    this.result = num1 + num2;
    }

    public void show() {
    System.out.println(num1 + " + " + num2 + " = " + result);
    }

    }
    package com.rabbit;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * 备忘录管理者
    * Created by HASEE on 2018/4/29.
    */
    public class Caretaker {

    private List<Memento> mementos;

    public boolean addMenento(Memento memento) {
    if (mementos == null) {
    mementos = new ArrayList<>();
    }
    return mementos.add(memento);
    }

    public List<Memento> getMementos() {
    return mementos;
    }

    public static Caretaker newInstance() {
    return new Caretaker();
    }
    }
    package com.rabbit;

    import org.junit.Test;

    import java.util.Random;

    /**
    * Created by HASEE on 2018/4/29.
    */
    public class Demo {

    @Test
    public void test() {
    Caretaker c = Caretaker.newInstance();
    //使用循环模拟用户使用计算器做加法运算
    Random ran = new Random(1000);
    for (int i = 0; i < 5; i++) {
    //用户计算
    Originator o = new Originator(ran.nextDouble(), ran.nextDouble());
    //计算器软件将用户的计算做备份,以便可以查看历史
    c.addMenento(o.createMemento());
    }
    System.out.println("---------------------用户浏览历史记录---------------------");
    for (Memento m : c.getMementos()) {
    m.show();
    }
    System.out.println("---------------------用户选择一条记录查看----------------------");
    c.getMementos().get(2).show();
    }

    }

    收起阅读 »

    java设计模式:访问者模式

    前言 访问者模式是一种将数据操作和数据结构分离的设计模式。 定义 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构...
    继续阅读 »


    前言


    访问者模式是一种将数据操作和数据结构分离的设计模式。


    定义


    将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。


    优点



    1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

    2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。

    3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。

    4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。


    缺点



    1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

    2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。

    3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。


    结构



    • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。

    • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。

    • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。

    • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。

    • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。


    示例


    年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。
    由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。


    // 员工基类
    public abstract class Staff {

    public String name;
    public int kpi;// 员工KPI

    public Staff(String name) {
    this.name = name;
    kpi = new Random().nextInt(10);
    }
    // 核心方法,接受Visitor的访问
    public abstract void accept(Visitor visitor);
    }

    Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码:


    // 工程师
    public class Engineer extends Staff {

    public Engineer(String name) {
    super(name);
    }

    @Override
    public void accept(Visitor visitor) {
    visitor.visit(this);
    }
    // 工程师一年的代码数量
    public int getCodeLines() {
    return new Random().nextInt(10 * 10000);
    }
    }
    // 经理
    public class Manager extends Staff {

    public Manager(String name) {
    super(name);
    }

    @Override
    public void accept(Visitor visitor) {
    visitor.visit(this);
    }
    // 一年做的产品数量
    public int getProducts() {
    return new Random().nextInt(10);
    }
    }

    工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。
    然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩,具体代码如下:


    // 员工业务报表类
    public class BusinessReport {

    private List<Staff> mStaffs = new LinkedList<>();

    public BusinessReport() {
    mStaffs.add(new Manager("经理-A"));
    mStaffs.add(new Engineer("工程师-A"));
    mStaffs.add(new Engineer("工程师-B"));
    mStaffs.add(new Engineer("工程师-C"));
    mStaffs.add(new Manager("经理-B"));
    mStaffs.add(new Engineer("工程师-D"));
    }

    /**
    * 为访问者展示报表
    * @param visitor 公司高层,如CEO、CTO
    */
    public void showReport(Visitor visitor) {
    for (Staff staff : mStaffs) {
    staff.accept(visitor);
    }
    }
    }


    下面看看 Visitor 类型的定义, Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数,具体代码如下:


    public interface Visitor {

    // 访问工程师类型
    void visit(Engineer engineer);

    // 访问经理类型
    void visit(Manager manager);
    }

    首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类,具体代码如下:


    // CEO访问者
    public class CEOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
    System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
    }

    @Override
    public void visit(Manager manager) {
    System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
    ", 新产品数量: " + manager.getProducts());
    }
    }

    在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理,代码大致如下:


    public class ReportUtil {
    public void visit(Staff staff) {
    if (staff instanceof Manager) {
    Manager manager = (Manager) staff;
    System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
    ", 新产品数量: " + manager.getProducts());
    } else if (staff instanceof Engineer) {
    Engineer engineer = (Engineer) staff;
    System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
    }
    }
    }

    这就导致了 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这个 ReportUtil 就会很复杂。而使用 Visitor 模式,通过同一个函数对不同对元素类型进行相应对处理,使结构更加清晰、灵活性更高。
    再添加一个CTO的 Visitor 类:



    public class CTOVisitor implements Visitor {
    @Override
    public void visit(Engineer engineer) {
    System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines());
    }

    @Override
    public void visit(Manager manager) {
    System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts());
    }
    }

    重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。
    下面是客户端代码:


    public class Client {

    public static void main(String[] args) {
    // 构建报表
    BusinessReport report = new BusinessReport();
    System.out.println("=========== CEO看报表 ===========");
    report.showReport(new CEOVisitor());
    System.out.println("=========== CTO看报表 ===========");
    report.showReport(new CTOVisitor());
    }
    }

    具体输出如下:


    =========== CEO看报表 ===========
    经理: 经理-A, KPI: 9, 新产品数量: 0
    工程师: 工程师-A, KPI: 6
    工程师: 工程师-B, KPI: 6
    工程师: 工程师-C, KPI: 8
    经理: 经理-B, KPI: 2, 新产品数量: 6
    工程师: 工程师-D, KPI: 6
    =========== CTO看报表 ===========
    经理: 经理-A, 产品数量: 3
    工程师: 工程师-A, 代码行数: 62558
    工程师: 工程师-B, 代码行数: 92965
    工程师: 工程师-C, 代码行数: 58839
    经理: 经理-B, 产品数量: 6
    工程师: 工程师-D, 代码行数: 53125


    在上述示例中,Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码。
    访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。


    应用场景


    当系统中存在类型数量稳定(固定)的一类数据结构时,可以使用访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。


    简而言之,就是当对集合中的不同类型数据(类型数量稳定)进行多种操作时,使用访问者模式。


    通常在以下情况可以考虑使用访问者模式。



    1. 对象结构相对稳定,但其操作算法经常变化的程序。

    2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

    3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。  

    收起阅读 »

    5分钟快速使用MQTT客户端连接环信MQTT消息云

    本文介绍如何使用MQTT客户端快速连接环信MQTT消息云一.操作流程 1、开通MQTT业务 开通环信MQTT消息云服务见快速开通MQTT服务;2、下载MQTT客户端 常见的MQTT客户端整理如下,下载客户端后可快速连接环信MQTT...
    继续阅读 »

    本文介绍如何使用MQTT客户端快速连接环信MQTT消息云

    一.操作流程 

    1、开通MQTT业务 
    开通环信MQTT消息云服务见快速开通MQTT服务

    2、下载MQTT客户端 

    常见的MQTT客户端整理如下,下载客户端后可快速连接环信MQTT消息云:

    MQTT客户端 操作系统 下载地址 
     MQTT Explorer Windows,macOS,Linux点击下载
     MQTT.fx Windows,macOS,Linux点击下载
     MQTT Box Windows,macOS,Linux点击下载
     mqtt-spy Windows,macOS,Linux点击下载
     Mosquitto CLI Windows,macOS,Linux点击下载


    二.接入指引
     

     1、连接五要素 
    MQTT客户端在连接环节需要5个基本参数,包括连接地址(Host)、端口(Port)、clientID(MQTT client ID)、用户ID(Username)、token(Password)。 
    获取方式如下:
    step1.进入console控制台,选择【MQTT】->【服务概览】;
    step2.获取clientID,clientID由两部分组成,组织形式为“deviceID@AppID”,deviceID由用户自定义,AppID见【服务配置】下AppID; 
    step3.获取连接地址(Host); 
    step4.获取端口(Port); 
    step5.选择左侧菜单栏【应用概览】->【用户认证】; 
    step6.获取用户ID(Username); 
    step7.获取token(Password); 




    2、连接环信MQTT消息云
    本文以MQTT Explorer for MAC版本为例(可通过APP Store下载)。打开MQTT客户端软件,选择“+”新建图标。



    step1.用户自定义连接名称; 
    step2.是否选择开启tls加密,取值:“开启”、“关闭”;
    step3.选择连接协议,取值:“ws:(websocket)”、“mqtt:”,若step2选择开启tls,协议为“wss:”、“mqtts”; 
    step4.填写环信MQTT消息云连接地址(Host); 
    step5.填写端口(Port); 
    step6.填写用户ID(username); 
    step7.填写token(Password); 
    step8.选择【ADVANCE】,填写clientID,clientID由两部分组成,“deviceID@AppID”; 
    step9.填写订阅主题名称,此例为“t/t1” ;
    step10.填写后点击【ADD】按钮添加至订阅列表中; 
    step11. 填写clientID名称; 
    step12. 选择【BACK】按钮,返回至主页面;
    step13. 选择主页面中【CONNECT】即可连接成功;



    3、订阅/发布消息 
    在创建一个MQTT 客户端,执行【连接环信MQTT消息云】流程;

    【发布消息】
    step1.填写发布的主题,本例中为“/t/t1”; 
    step2.选择消息体格式,取值:“raw”、“xml”、“json”;
    step3.填写消息体内容,本例中为“hello world”; 
    step4.选择QoS等级,取值:“0:至多发送一次,不保留”、“1:至少一次,保留”、“2:仅发一次,保留”; 
    step5.选择是否为保留消息,取值:“0:不保留”、“1:保留,订阅客户端重新接入环信MQTT消息云时,可以接收保留消息”; 
    step6.发送消息; 


    【订阅消息】
    订阅/t/t1的MQTT客户端即可接收消息

    收起阅读 »

    Swift是否可以集成环信IM SDK?

    可以。Swift集成SDK时,在自定义cell和EaseIMKit混用时,会导致程序崩溃问题.原因是,无法返回nil,应该怎么处理?解决方法:让用户集成easeIMKit源码,将创建自定义cell回调的返回值添加一个可为空的关键字nullable

    可以。

    Swift集成SDK时,在自定义cell和EaseIMKit混用时,会导致程序崩溃问题.原因是,无法返回nil,应该怎么处理?
    解决方法:让用户集成easeIMKit源码,将创建自定义cell回调的返回值添加一个可为空的关键字nullable

    OC对象的本质(上) —— OC对象的底层实现原理

    一个NSObject对象占用多少内存?Objective-C的本质平时我们编写的OC代码,底层实现都是C/C++代码Objective-C --> C/C++ --> 汇编语言 --> 机器码所以Objective-C的面向对象都是基于C/C...
    继续阅读 »

    一个NSObject对象占用多少内存?

    Objective-C的本质
    平时我们编写的OC代码,底层实现都是C/C++代码

    Objective-C --> C/C++ --> 汇编语言 --> 机器码

    所以Objective-C的面向对象都是基于C/C++的数据结构实现的,所以我们可以将Objective-C代码转换成C/C++代码,来研究OC对象的本质。

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    }
    return 0;
    }

    我们在main函数里面定义一个简单对象,然后通过 clang -rewrite-objc main.m -o main.cpp命令,将main.m文件进行重写,即可转换出对应的C/C++代码。但是可以看到一个问题,就是转换出来的文件过长,将近10w行。


    因为不同平台支持的代码不同(Windows/Mac/iOS),那么同样一句OC代码,经过编译,转成C/C++代码,以及最终的汇编码,是不一样的,汇编指令严重依赖平台环境。
    我们当前关注iOS开发,所以,我们只需要生成iOS支持的C/C++代码。因此,可以使用如下命令
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc <OC源文件> -o <输出的cpp文件>
    -sdk:指定sdk
    -arch:指定机器cpu架构(模拟器-i386、32bit、64bit-arm64 )
    如果需要链接其他框架,使用-framework参数,比如-framework UIKit
    一般我们手机都已经普及arm64,所以这里的架构参数用arm64,生成的cpp代码如下



    接下来,我们查看一下main_arm64.cpp源文件,如果熟悉这个文件,你将会发现这么一个结构体

    struct NSObject_IMPL {
    Class isa;
    };

    我们再来对比看一下NSObject头文件的定义

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    @end

    简化一下,就是

    @interface NSObject  {
    Class isa ;
    }
    @end

    是不是猜到点什么了?没错,struct NSObject_IMPL其实就是NSObject的底层结构,或者说底层实现。换个角度理解,可以说C/C++的结构体类型支撑了OC的面相对象。

    点进Class的定义,我们可以看到 是typedef struct objc_class *Class;

    Class isa; 等价于 struct objc_class *isa;

    所以NSObject对象内部就是放了一个名叫isa的指针,指向了一个结构体 struct objc_class。

    总结一:一个OC对象在内存中是如何布局的?


    猜想:NSObject对象的底层就是一个包含了一个指针的结构体,那么它的大小是不是就是8字节(64位下指针类型占8个字节)?
    为了验证猜想,我们需要借助runtime提供的一些工具,导入runtime头文件,class_getInstanceSize ()方法可以计算一个类的实例对象所实际需要的的空间大小

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    size_t size = class_getInstanceSize([NSObject class]);
    NSLog(@"NSObject对象的大小:%zd",size);
    }
    return 0;
    }

    结果是


    完美验证,it's over,let's go home!


    等等,就这么简单?确定吗?答案是否定的~~~
    介绍另一个库#import <malloc/malloc.h>,其下有个方法 malloc_size(),该函数的参数是一个指针,可以计算所传入指针 所指向内存空间的大小。我们来用一下

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    #import <malloc/malloc.h>

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    size_t size = class_getInstanceSize([NSObject class]);
    NSLog(@"NSObject实例对象的大小:%zd",size);
    size_t size2 = malloc_size((__bridge const void *)(obj));
    NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);
    }
    return 0;
    }

    结果是16,如何解释呢?


    想要真正弄清楚其中的缘由,就需要去苹果官方的开源代码里面去一探究竟了。苹果的开源代请看这里。
    先看一下class_getInstanceSize的实现。我们需要进到objc4/文件里面下载一份最新的源码,我当前最新的版本是objc4-750.1.tar.gz。下载解压之后,打开工程,就可以查看runtime的实现源码。
    搜索class_getInstanceSize找到实现代码

    size_t class_getInstanceSize(Class cls)
    {
    if (!cls) return 0;
    return cls->alignedInstanceSize();
    }

    再点进alignedInstanceSize方法的实现

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
    }

    可以看到该方法的注释说明Class's ivar size rounded up to a pointer-size boundary.,意思就是获得类的成员变量的大小,其实也就是计算类所对应的底层结构体的大小,注意后面的这个rounded up to a pointer-size boundary指的是系统在为类的结构体分配内存时所进行的内存对齐,要以一个指针的长度作为对齐系数,64位系统指针长度(字长)是8个字节,那么返回的结果肯定是8的最小整数倍。为什么需要用指针长度作为对齐系数呢?因为类所对应的结构体,在头部的肯定是一个isa指针,所以指针肯定是该结构体中最大的基本数据类型,所以根据结构体的内存对齐规则,才做此设定。如果对这里有疑惑的话,请先复习一下有关内存对齐的知识,便一目了然了。
    所以class_getInstanceSize方法,可以帮我们获取一个类的的实例对象所对应的结构体的实际大小。

    我们再从alloc方法探究一下,alloc方法里面实际上是AllocWithZone方法,我们在objc源码工程里面搜索一下,可以在Object.mm文件里面找到一个_objc_rootAllocWithZone方法。

    id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
    {
    id obj;

    #if __OBJC2__
    // allocWithZone under __OBJC2__ ignores the zone parameter
    (void)zone;
    obj = class_createInstance(cls, 0);
    #else
    if (!zone) {
    obj = class_createInstance(cls, 0);
    }
    else {
    obj = class_createInstanceFromZone(cls, 0, zone);
    }
    #endif

    if (slowpath(!obj)) obj = callBadAllocHandler(cls);
    return obj;
    }

    再点进里面的关键方法class_createInstance的实现看一下

    id  class_createInstance(Class cls, size_t extraBytes)
    {
    return _class_createInstanceFromZone(cls, extraBytes, nil);
    }

    继续点进_class_createInstanceFromZone方法

    static __attribute__((always_inline)) 
    id
    _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
    bool cxxConstruct = true,
    size_t *outAllocatedSize = nil)
    {
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();

    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone && fast) {
    obj = (id)calloc(1, size);
    if (!obj) return nil;
    obj->initInstanceIsa(cls, hasCxxDtor);
    }
    else {
    if (zone) {
    obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
    } else {
    obj = (id)calloc(1, size);
    }
    if (!obj) return nil;

    // Use raw pointer isa on the assumption that they might be
    // doing something weird with the zone or RR.
    obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
    obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
    }

    这个方法有点长,有时分析一个方法,不要过分拘泥细节,先针对我们寻找的问题,找到关键点,像这个比较长的方法,我们知道,它的主要功能就是创建一个实例,为其开辟内存空间,我们可以发现中间的这句代码obj = (id)calloc(1, size);,是在分配内存,这里的size是需要分配的内存的大小,那这句应该就是为对象开辟内存的核心代码,再看它里面的参数size,我们能在上两行代码中找到size_t size = cls->instanceSize(extraBytes);,于是我们继续点进instanceSize看看

    size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
    }

    翻译一下这句注//CF requires all objects be at least 16 bytes.我们就明白了,CF作出了硬性的规定:当创建一个实例对象的时候,为其分配的空间不能小于16个字节,为什么这么规定呢,我个人目前的理解是这可能就相当于一种开发规范,或者对于CF框架内部的一些实现提供的规范。
    这个size_t instanceSize(size_t extraBytes)返回的字节数,其实就是为 为一个类创建实例对象所需要分配的内存空间。这里我们的NSObject类创建一个实例对象,就分配了16个字节。
    我们在点进上面代码中的alignedInstanceSize方法

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
    return word_align(unalignedInstanceSize());
    }

    这不就是我们上面分析class_getInstanceSize方法里面看到的那个alignedInstanceSize嘛。


    总结二:class_getInstanceSize&malloc_size的区别

    class_getInstanceSize:获取一个objc类的实例的实际大小,这个大小可以理解为创建这个实例对象至少需要的空间(系统实际为这个对象分配的空间可能会比这个大,这是出于系统内存对齐的原因)。
    malloc_size:得到一个指针所指向的内存空间的大小。我们的OC对象就是一个指针,利用这个函数,我们可以得到该对象所占用的内存大小,也就是系统为这个对象(指针)所指向对象所实际分配的内存大小。
    sizeof():获取一个类型或者变量所占用的存储空间,这是一个运算符。
    [NSObject alloc]之后,系统为其分配了16个字节的内存,最终obj对象(也就是struct NSObject_IMPL结构体),实际使用了其中的8个字节内存,(也就是其内部的那个isa指针所用的8个字节,这里我们是在64位系统为前提下来说的)
    关于运算符和函数的一些对比理解

    函数在编译完之后,是可以在程序运行阶段被调用的,有调用行为的发生
    运算符则是在编译按一刻,直接被替换成运算后的结果常量,跟宏定义有些类似,不存在调用的行为,所以效率非常高

    更为复杂的自定义类

    我们开发中会自定义各种各样的类,基本上都是NSObject的子类。更为复杂的子类对象的内存布局又是如何的呢?我们新建一个NSObject的子类Student,并为其增加一些成员变量

    @interface Student : NSObject
    {
    @public
    int _age;
    int _no;
    }

    @end

    @implementation Student

    @end

    使用我们之前介绍过的方法,查看一下这个类的底层实现代码

    struct NSObject_IMPL {
    Class isa;
    };

    struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _no;

    };

    我们发现其实Student的底层结构里,包含了它的成员变量,还有一个NSObject_IMPL结构体变量,也就是它的父类的结构体。根据我们上面的总结,NSObject_IMPL结构体需要的空间是8字节,但是系统给NSObject对象实际分配的内存是16字节,那么这里Student的底层结构体里面的成员变量NSObject_IMPL应该会得到多少的内存分配呢?我们验证一下。

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSObject *obj = [[NSObject alloc] init];
    //获取`NSObject`类的实例对象的成员变量所占用的大小
    size_t size = class_getInstanceSize([NSObject class]);
    NSLog(@"NSObject实例对象的大小:%zd",size);
    //获取obj所指向的内存空间的大小
    size_t size2 = malloc_size((__bridge const void *)(obj));
    NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);

    Student * std = [[Student alloc]init];
    size_t size3 = class_getInstanceSize([Student class]);
    NSLog(@"Student实例对象的大小:%zd",size3);
    size_t size4 = malloc_size((__bridge const void *)(std));
    NSLog(@"对象std所指向的的内存空间大小:%zd",size4);
    }
    return 0;
    }


    貌似是对的了,但是为什么用malloc_size得到std所被分配的内存是32?再来一发试试

    @interface Student : NSObject
    {

    @public
    //父类的isa还会占用8个字节
    int _age;//4字节
    int _no;//4字节
    int _grade;//4字节
    int *p1;//8字节
    int *p2;//8字节
    }

    Student结构体所有成员变量所需要的总空间为 36字节,根据内存对齐原则,最后结构体所需要的空间应该是8的倍数,那应该就是40,我们看一下结果


    从结果看没错,但是同时也发现了一个规律,随着std对象成员变量的增加,系统为Student对象std分配的内存空间总是以16的倍数增加(16~32~48......),我们之前分析源码好像没看到有做这个设定


    其实上面这个方法只是可以用来计算一个结构体对象所实际需要的内存大小。 [update]其实instanceSize()-->alignedInstanceSize()只是可以用来计算一个结构体对象理论上(按照内存对其规则)所需要分配的内存大小。

    真正给实例对象完成分配内存操作的是下面这个方法calloc()


    这个方法位于苹果源码的libmalloc文件夹中。但是里面的代码再往下深究,介于我目前的知识储备以及专业出身(数学专业),还是困难比较大。好在从一些大神那里得到了指点。
    刚才文章开始,我们讨论到了结构体的内存对齐,这是针对数据结构而言的。从系统层面来说,就以苹果系统而言,出于对内存管理和访问效率最优化的需要,会实现在内存中规划出很多块,这些块有大有小,但都是16的倍数,比如有的是32,有的是48,在libmalloc源码的nano_zone.h里面有这么一段代码

    #define NANO_MAX_SIZE    256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

    NANO是源码库里面的其中一种内存分配方法,类似的还有frozen、legacy、magazine、purgeable。


    这些是苹果基于各种场景优化需求而设定的对应的内存管理相关的库,暂时不用对其过分解读。
    上面的NANO_MAX_SIZE解释中有个词Buckets sized,就是苹果事先规划好的内存块的大小要求,针对nano,内存块都被设定成16的倍数,并且最大值是256。举个例子,如果一个对象结构体需要46个字节,那么系统会找一块48字节的内存块分配给它用,如果另一个结构体需要58个字节,那么系统会找一块64字节的内存块分配给它用。
    到这里,应该就可以基本上解释清楚,为什么刚才student结构需要40个字节的时候,被分配到的内存大小确实48个字节。至此,针对一个NSObject对象占用内存的问题,以及延伸出来的内存布局,以及其子类的占内存问题,应该就都可以得到解答了。

    OC对象的本质(上):OC对象的底层实现
    OC对象的本质(中):OC对象的分类
    OC对象的本质(下):详解isa&superclass指针

    面试题解答

    一个NSObject对象占用多少内存?
    1)系统分配了16字节给NSObject对象(通过malloc_size函数可以获得)
    2)NSObject对象内部只使用了8个字节的空间,用来存放isa指针变量(64位系统下,可以通过class_getInstanceSize函数获得)

    链接:https://www.jianshu.com/p/1bf78e1b3594

    收起阅读 »

    iOS内存(Heap堆内存 && Anonymous VM 虚拟内存) 分析和理解

    在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的...
    继续阅读 »

    在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的时候 我们是否需要
    去了解它

    前言所需要的图片(如下图)


    1) 什么是堆
    堆是一种完全结构的二叉树 堆与二叉树的理解

    堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。堆里面一般 放的是静态数据,比如static的数据和字符串常量等,资源加载后一般也放在堆里面。一个进程的所有线程共有这些堆 ,所以对堆的操作要考虑同步和互斥的问题。程序里面编译后的数据段都是堆的一部分。

    — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表

    {
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
    char *p3 = "123456"; //123456{row.content}在常量区,p3在栈上。
    static int c =0; //全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);//分配得来的10和20字节的区域就在堆区。
    strcpy(p1, "123456"); //123456{row.content}放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。}

    堆:首 先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结 点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才 能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出分配方式:堆都是动态分配的,没有静态分配的堆。

    1.1) 堆上消耗的内存


    1、View 函数的调用
    2、注册通知
    3、抛出通知
    4、view 的布局
    5、函数代码的执行
    6、sqlite 数据库的创建
    7、向字典中增加对象


    8、等等

    都需要消耗内存, 上面的代码都是程序员创建的, 程序员去控制堆的内存

    1.2) 堆上的内存是否释放

    1.2.1) 已经释放的例子:

    点击步骤1)箭头


    查看步骤2)箭头


    步骤2) 箭头中有 free 函数, 可以看出, 这个对象 已经被释放

    1.2.2) 堆上内存不释放的例子:


    上图中箭头执行的地方 没有free 函数 说明 这个对象已经释放

    2) Anonymous VM

    2.1) 苹果官方文档对虚拟内存的解释

    更小的内存消耗不仅可以减少内存, 还可以减少cpu 的时间
    我们可能会看到这样的情况, All Heap Allocations 是程序真实的内存分配情况,All Anonymous VM则是系统为程序分配的虚拟内存,为的就是当程序有需要的时候,能够及时为程序提供足够的内存空间,而不会现用现创建

    Anonymous VM内存是虚拟内存
    、All Anonymous VM。我们无法控制Anonymous VM部分 ,(更新,其实还是可以优化 比如图片绘制相关 详情参见iOS内存探究,需要对虚拟内存熟悉 才能优化)

    2.2) 问题: 我们需要关注Anonymous VM 内存吗 ?
    问答连接
    Should you focus on the Live Bytes column for heap allocations or anonymous VM? Focus on the heap allocations because your app has more control over heap allocations. Most of the memory allocations your app makes are heap allocations.

    The VM in anonymous VM stands for virtual memory. When your app launches, the operating system reserves a block of virtual memory for your application. This block is usually much larger than the amount of memory your app needs. When your app allocates memory, the operating system allocates the memory from the block it reserved.

    Remember the second sentence in the previous paragraph. The operating system determines the size of the virtual memory block, not your app. That’s why you should focus on the heap allocations instead of anonymous VM. Your app has no control over the size of the anonymous VM.

    2.3) 不需要关注 Anonymous VM
    我们应该关注堆内存, 因为我们对堆内存有更大的掌控, 大部分我们在app的内存分配是堆内存

    VM 在匿名空间中代表的是虚拟内存, 当你的app启动的时候, 操作系统为你的应用程序分配内存, 这个分配的虚拟内存一般比你的app需要的内存大很多,

    操作系统决定虚拟内存的分配, 而不是你的app, 这就是你为什么要集中精力处理堆内存, 你的app 对虚拟内存没有掌控力

    2.4) 虚拟内存过大 (未解之谜) 如果知道结果请评论留言, 多谢


    CGBitmapContextCreateImage 函数会导致虚拟内存过大 ,并且还不释放, 用法未发现问题

    CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
    UIImage *result = [UIImage imageWithCGImage:alphaMaskImage scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(alphaMaskImage);
    CGContextRelease(alphaOnlyContext);
    return result;

    参考文献

    iOS中的堆(heap)和栈(stack)的理解

    苹果虚拟内存的官方文档

    转自:https://www.jianshu.com/p/dffd5c24dc9a

    收起阅读 »

    【环信IM集成指南】iOS端常见问题整理

    建议用浏览器搜索定位问题~本文持续更新,欢迎大家留言点菜~1、集成IM如何自定义添加表情组https://www.imgeek.org/article/8253575062、旧版音视频与EaseCallKit兼容升级方案https://www.imgeek.o...
    继续阅读 »
    建议用浏览器搜索定位问题~
    本文持续更新,欢迎大家留言点菜~


    1、集成IM如何自定义添加表情组

    https://www.imgeek.org/article/825357506


    2、旧版音视频与EaseCallKit兼容升级方案

    https://www.imgeek.org/article/825357507

     
    3、如何集成环信EaseIMKit和EaseCallKit源码

    https://www.imgeek.org/article/825357493

     
    4、解决集成EaseIMKit源码后没有图片的问题

    https://www.imgeek.org/article/825357495

     
    5、EaseIMKit如何设置昵称、头像

    https://www.imgeek.org/article/825354241

     
    6、Swift是否可以集成环信IM SDK?

    https://www.imgeek.org/article/825357511

     
    7、环信IM会话列表和聊天界面修改头像和昵称

    https://www.imgeek.org/article/825357608

     
    8、手把手教集成EaseIMKit源码 

    https://www.imgeek.org/article/825357673


    9、环信聊天室如何每次进来可以看到之前的已读消息

    https://imgeek.org/article/825357723


    10、这几个iOS拓展字段,是只对iOS生效吗?对安卓没有影响吧?
    em_push_content 自定义推送显示

    em_push_category 向 APNs Payload 中添加 category 字段
    em_push_sound 自定义推送提示音
    em_push_mutable_content 开启 APNs 通知扩展
    em_ignore_notification 发送静默消息
    em_force_notification 设置强制推送型 APNs

    答:下面这三个对安卓不生效,其他的是两端都会起作用。
    em_push_category、em_push_sound、em_push_mutable_content

    11、无网时发送消息,然后迅速切到有网状态。这时显示发送成功,然后回退到上一个页面再进入到IM页,刚刚那条消息被重复发送了


    可以开通服务端消息去重功能。


    12、如果设置了离线不踢出聊天室,那聊天室的消息会有离线推送吗?

    聊天室没有离线处理,所以没有离线推送。


    13、cmd不进行漫游功能配置成功之前的历史消息,在配置好之后还是能拉下来的。


    14、iOS和安卓端发视频消息,对视频格式有要求吗?
    答:没有


    15.图片发送设置了缩略图,收到的消息里面没有缩略图,只有源文件数据



    接收方会直接将缩略图下载到本地,SDK会自动把缩略图缓存到本地,您直接通过body.thumbnailLocalPath就可以获取到了, 我们的UI SDK已经对这些做了封装,不需要您再单独进行处理,如果您这边就是想拿到这张缩略图来使用的话,就需要在messagesDidReceive方法里面自己再判断一下,如果是图片消息的话,就去打印缩略图的路径,然后通过这个路径可以获取到缩略图的原图

    case EMMessageBodyTypeImage:
    {
    // 得到一个图片消息body
    EMImageMessageBody *body = ((EMImageMessageBody *)msgBody);
    NSLog(@"大图remote路径 -- %@" ,body.remotePath);
    NSLog(@"大图local路径 -- %@" ,body.localPath); // // 需要使用sdk提供的下载方法后才会存在
    NSLog(@"大图的secret -- %@" ,body.secretKey);
    NSLog(@"大图的W -- %f ,大图的H -- %f",body.size.width,body.size.height);
    NSLog(@"大图的下载状态 -- %lu",body.downloadStatus);


    // 缩略图sdk会自动下载
    NSLog(@"小图remote路径 -- %@" ,body.thumbnailRemotePath);
    NSLog(@"小图local路径 -- %@" ,body.thumbnailLocalPath);
    NSLog(@"小图的secret -- %@" ,body.thumbnailSecretKey);
    NSLog(@"小图的W -- %f ,大图的H -- %f",body.thumbnailSize.width,body.thumbnailSize.height);
    NSLog(@"小图的下载状态 -- %lu”,body.thumbnailDownloadStatus);


    16.后端该如何操作用户上麦

    后端无法直接控制让谁上麦,所以只能通过发送CMD消息的方式来和移动端进行交互,移动端根据逻辑指令去操作

    17.使用[[EMClient sharedClient].chatManager ackConversationRead:_conversation.conversationId completion:nil];将消息置为已读,但是还是有未读数

    [[EMClient sharedClient].chatManager ackConversationRead:_conversation.conversationId completion:nil]; —- 这个方法是发送会话已读消息,将通知服务器将此会话未读数置为0,而将消息置为已读是本地操作,可以使用方法:
    1).[[EaseIMKitManager shared] markAllMessagesAsReadWithConversation:_conversation];
    2). [conversation markMessageAsReadWithId:message.messageId error:nil];
    3).[conversation markMessageAsReadWithId:message.messageId error:nil];
    注意:方法1是EaseIMKitManager调用的,方法2、3是EMConversation调用的

    18.聊天页面头像设置圆角失败
    如果要设置聊天页面头像的圆角值,需要先设置avatarType为圆形才会生效,如果想要设置为圆形,则直接给图片宽度的一半即可


    19.调用getGroupSpecificationFromServerWithId获取群组详情失败,失败的原因 --- you have no permission to do this, group member permission is required

    出现此问题的原因是当前用户不在群组内,获取群组详情必须是群组成员才有权限,如果因为场景特殊的话,可以使用rest接口获取。

    20、如何将附件保存在自己的服务器上

    1.项目中搜索:isAutoTransferMessageAttachments,将属性值改为no
    2.用户上传文件完成后,不建议用户直接使用remotePath,而是使用ext扩展来存放文件链接.


    21、请问后台和sdk对群组名称和群组描述,有字数或其他限制吗?分别是多少?
    后台:名称 16字符 超出部分截去,描述64字符 超出部分截去
    Sdk:无限制


    22、全局广播相关:
    (1)支持发送自定义类型消息和扩展消息吗?

    支持。
    (2)会有离线推送延迟的问题吗?
    会,慢速堆积,就会延迟。延迟15分钟很正常的。
    (3)全局广播的延迟是根据用户量来的,按每秒下发1000个来推,如果有用户1万个,预计需要10秒。

    23、同一个环信id在多设备登录,可以同时加入同一个聊天室。但设备数量有限制,根据多端多设备功能配置的数量来。


    24、p8证书在开发和生产环境下都可以工作(不需要在证书之间切换),最重要的是,它不会过期!


    25、console后台添加推送证书有数量限制吗?

    无限制。但不要短时间内快速上传大量证书。

    26、获取token的接口,是根据ip做限制的。例如一个ip,每秒最多10次。

    27、iOS端对于离线推送扩展字段:em_push_title、em_push_content的显示逻辑。
    如果title和content都有,就显示title的,没有title就取content的值,两个是有优先级的。
    如果想要标题和内容都有的样式,可以只用em_push_content,然后将内容进行换行


    28、群消息可以单独指定给某人吗?
    我们没有这个功能,您可以自己实现。
    消息带上扩展,可以是指定人的环信id,群成员们收到消息(messagesDidReceive)后判断扩展内容是不是自己的环信id,是的话就展示,不是就不展示。


    29、如果同时设置了发送前和发送后回调,会先执行发送前,再执行发送后。

    30、回调会保证顺序发送吗?
    回调不保证发送顺序 消息里面都是带时间的。


    31、自定义的聊天cell,在哪里设置cell 的高度?
    自定义cell的高度是自动计算的,自适应的,正常不用单独设置。
    如果有问题,看下自定义的cell的布局是不是不对。


    32、从服务器端获取会话列表功能相关规则:

    (1)、时效是7天(社区、企业等版本都是统一的)。
    如果购买了消息漫游,会话列表保存时长延长至购买的漫游时长。
    也可以单独延长保存时长,收费相关需要和商务沟通。
    (2)、只获取到会话的最新一条消息,要获取这个会话的其他历史消息可以再调用漫游
    (3)、调用后会自动同步到本地数据库(app端)
    (4)、默认可以获取10个会话,最大可以上调到100个。需要联系商务调整
    (5)、cmd消息不计入会话列表
    (6)、开通后需要发送新的消息测试,开通前的数据获取不到


    33、群组全员禁言、将某成员解禁,此成员还是无法发消息
    这个现象是正常的。


    34、图片消息的大图、缩略图的服务器端路径为什么是一样的?
    这是正常的,对于服务器端来说,下载缩略图就是多个参数,sdk下载时会有区分。



    35、发消息超时重试机制
    (1)、断网的情况下发消息,30秒后直接返回error消息
    (2)、弱网的情况下,发送附件类型消息需要先进行上传,调用 rest接口,60秒 + 60秒重试,2min后返回error消息
    (3)、弱网的情况下,发送非附件类型消息直接走mysnc,1min后返回error消息


    36. 大小写敏感的问题
    Q:获取不到会话~
    A: 大小写敏感
    message里面 conversation:WePlay_eTl36Lbp***LQRbX6CzL-
    conversation数据库里面conversation: weplay_etl36lbp***lqrbx6czl-

    37. Q: 请问一下,SDK发送文件、图片、视频的时候,默认是存储在你们的服务器上的吗?存储地址是否可以自定义?
    A: 是可以的
    1. 关闭环信的自动下载或者上传附件isAutoTransferMessageAttachments
    2. 发送正常消息的时候,在ext里面加上传到自己服务器的资源地址、和文件类型

    38. sdk 报300 

    是客户端连不上msync服务,这个分两种情况 1.客户端自身网络问题,比如设置了代理服务、网络异常 2.环信服务异常 客户端连接的msync服务异常 提示300


    39. 发送方网络不好时发了一条消息,接收方收到两条。
    原因:就是网络不好,消息发出去后,sdk没有收到服务器端的ack,sdk认为消息没有发送成功,然后又发了一条。
    Q:如果客户的场景就是短时间内重复发相同内容的消息,那配置了这个去重,是不是就会把重复的消息也过滤掉?
    A:不会。正常情况下,发相同的消息,每条消息的metaid不同,异常情况下metaid是相同的。meta id就是sdk本地临时生成的消息id,就是网络不好,消息发出去后,sdk没有收到服务器端的ack,sdk认为消息没有发送成功,然后又发了一条
    就是这种情况,涉及到sdk重发消息,所以两条消息的metaid是相同的
    此情况可以配置“服务端消息去重 unique_client_msgid”,或者联系运维配置。

    40. EaseCallKit 问题
    Q: 声网的音视频发送消息类型全部是EMMessageTypeCmd 吗?

    A: 音视频通话过程中的第一条呼叫邀请是文本消息,其他都是cmd消息

    41. EaseCallKit 问题
    EMMessageTypePictMixText是用来标记图片类型的,在现在的项目里面是没有用到的

    42. 场景:
    Q: 现在有个这样的场景,客户端申请创建群服务审核后由服务端创建了一个群,还有就是客户端也可以申请加入群,加入成功和创建成功后都会收到这个回调,我有办法区分出来是服务端创建的群还是我自己主动加入的群么?

    A: 服务端在创建群组的时候,有一个custom字段,您可以做业务相关的标记~ custom字段对应客户端的字段是:EMGroup -> ext字段

    43. iOS应用的强杀,在聊天室里的人需要立马能感受到他的离线。或者离开。推荐怎么做好
    A: 正常情况是离线2min,服务器会将此成员踢出聊天室,2min的时间在我们这儿是可以设置的

    44、 加个 em_push_name 有没有用?
    A: iOS 目前不支持em_push_name,解决方案是通过\n,来模拟类似标题的效果



    45.、用户如果杀进程,你们日志里面会添加记录吗
    A: 杀死就是直接log突然没记录了



    46、我们的SDK,自动登录,是有token校验的机制的?
    我在A设备登录完,又在B设备登录了
    这个时候,我又从A设备自登录了,A这边能感知到token变化?(或者说已经在别的设备登录过了)
    A: 这个需求研发已经在排期做,完成后的预期时A离线再上线时也可以感知到有其他设备登陆过

    47、群组的代理在EaseIMKitManager里面可以走,在help类里面是不走的,需要进一步核实~

    48、为啥不用UUID用作环信ID?
    A:环信最初设计是来源注册规则,其中username是现在客户注册的环信ID,环信系统收到这个username后会自动生成一个内部的UUID,所以不允许用户使用UUID作为环信ID,避免冲突。username是64位的,UUID是36位的,客户可以在UUID的前边加个前缀作为username。


    49、 the resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
    A: 在app的info.plist 文件中,设置Allow Arbitrary Loads = yes
    或者是在EMOptions 调用usingHttpsOnly = YES 仅支持https

    flutter问题
    50、语音播放

    使用RecordAmr,播放remotePath,安卓可以、iOS不可以
    目前给的解决方案:把remotePath修改成localPath


    51、添加回调规则添加失败。
    A:检查下回调规则名称是不是用的汉字,回调规则只能是数字、字母,不能用汉字。


    52、对方离线了之后,发送的消息,上线后如何获取?
    A:对方离线,消息会进入离线队列,如果没有集成第三方厂商离线推送,用户上线后,服务器下发给客户端。


    53、调用SDK 方法报错: Cannot read property 'lookup' of undefined?
    A:因为未登陆成功就调用了SDK 的api,需要在onOpened 链接成功回调执行后再去调用SDK 的api。


    54、聊天室如何获取历史消息?
    A:两种方式:1、环信服务器端主动推,需要联系商务开通服务,默认10条,数量可以调整。2、通过消息漫游接口自己去拉取历史消息,各端都有提供拉取漫游消息接口。


    55、拉取消息漫游,conversationId是怎么获取的?
    A:单聊的话,conversationId 就是对方用户的环信id。
    群聊或聊天室的话,conversationId 就是groupid 或者chatroomid。


    56、如何实现只有好友才可以发消息?
    A:可以使用环信的发送前回调服务,消息先回调给配置的回调服务器,然后去判断收发双方是否是好友关系,如果是好友关系,那么下发消息,如果是非好友关系,则不下发消息,客户端ui可以根据不下发返回的code做提示。


    57、调rest接口报401是什么原因?
    A:调环信rest接口,需要管理员权限的token,确认下请求是否有token,且是在有效期,token的有效期以请求时服务器返回的时间为准。


    58、调修改群信息报错如下
    System.Net.WebException:“远程服务器返回错误: (400) 错误的请求。
    A:检查下请求体,看下参数格式是否正确,比如"membersonly",,"allowinvites" 这两个参数的值为布尔值。


    59、注册用户username是纯数字可以吗。

    调restapi是可以的,serversdk的话,为了让用户使用更规范的名字,命名规则更严格一些,要求首位是字母。


    60、 SDK相关
    第1个 SDK 3.8.4 会有长链接特殊情况下无故断开情况,升级至3.8.5即可
    第2个 SDK3.8.5.1 会有重复收到推送的情况,升级到3.8.5.2即可
    第3个 SDK 3.8.2 以下启动闪退,报network问题,升级至 3.8.3.1即可


    61、 3.8.0以上版本与3.8.0以下版本有什么区别?

    目前官方demo最新版SDK版本号是3.8.6.2,SDK名称叫HyphenateChat
    3.8.0以下(不包含3.8.0)名称为Hyphenate
    如果您需要easeIMkit源码,建议您直接使用最新版.

    Hyphenate和HyphenateChat的关系:
    1.Hyphenate和HyphenateChat都是环信SDK,只需要引用一份即可.
    2.Hyphenate和HyphenateChat名字不同,版本也不同:
    Hyphenate是3.7.4及以下的SDK命名
    HyphenateChat是3.8.0及以上的SDK命名

    3.升级前后最大的区别:
    Hyphenate包含环信音视频功能
    配套的EaseIMKit(3.7.4/3.7.3两个版本)内部也有音视频的UI界面

    HyphenateChat已去除环信音视频,单有IM功能
    需要集成声网SDK
    配套EaseIMKit本身也没有音视频界面.
    EaseIMKit介绍文档:http://docs-im.easemob.com/im/ios/other/easeimkit#easeimkit_使用指南
    对应声网的音视频SDK,我们专门做了对应的UI界面:EaseCallKit.对EaseIMKit没有音视频界面的一个补充.


    注意:
    1 EaseCallKit就像EaseIMKit一样,也需要集成,而不被EaseIMKit包含.
    2 集成EaseCallKit前,还需要集成声网SDK,您可以阅读开发文档来理解其中关系:http://docs-im.easemob.com/im/ios/other/easecallkit#简介


    62、 如何集成easeIMkit源码?
    建议客户升级到最新版本即可集成easeIMkit源码.

    但需要注意:如果集成easeIMkit源码,虽然可以看到源码实现,但不能修改,如果需要修改调整,则需要使用本地集成的方式.
    本地集成方式可借鉴官方demo集成方式(这里由于步骤比较繁琐,如果客户还是不会,会考虑远程操作)

    63、本地集成easeIMkit,并修改调整,那么后续是不是无法再升级了?
    是这样的,虽然后续无法升级,但未来您是需要做出自己的ui界面的,甚至未来可能会移除easeIMkit,所以请大胆修改调整吧.

    我们更希望能够看到三个阶段:
    第一阶段:您可以简单快捷地集成环信聊天,并实现聊天功能.同时可以在EaseIMKit.framework开放的接口或属性范围内,可做出适合项目需求的调整.
    第二阶段:您在使用EaseIMKit.framework时遇到了限制,有诸多需要调整的部分在动态库内部,您无法去做调整.这时,您可以集成EaseIMKit源码来达到您的目的,集成EaseIMKit源码后,您可以修改源码内部的代码,以满足您项目需求.
    第三阶段:在您项目发展到一定程度后,往往EaseIMKit无法再陪伴您继续向更高层次发展.在这时,您对于EaseIMKit源码的熟悉度也非常高了.其中有很多实现方式已经为您之前遇到的困惑做了更好的解答,再加上您早已按耐不住的灵感,重建出属于您项目专属的界面吧!如此更加契合您的产品需求,也更加容易维护.

    64、 rest相关
     环信服务器的聊天记录能清除吗 删除了用户。重新注册的用户id对上了 又把聊天记录拉下来了

    你删了环信id,聊天记录是不会删除的,这么设计的逻辑是因为每个客户的业务场景不同,如果客户误删了环信id,需要重新注册回来,并且需要看到历史聊天记录。如果你这边的业务,是不希望这种场景,你可以去定义注册环信IM的id规则,你用户注册你自己应用的username时,按你定义的规则去注册IM的id,也就是说你这边的username和环信的id不是同一个,环信这边是根据环信id保存历史记录的

     
    65、关于rest接口注册用户,批量注册

    单次批量注册上限为100,不受接口限流频率影响.


    66、解决方案
    消息部分

    1.当前场景:SDK本身已经创建数据库,并有简单的增删改查,但由于SDK本身对于数据库的操作较为简单,所以舍弃当前SDK内数据库,创建新的数据库.

    举例:当前会搜索到携带关键字的所有类型的消息.期望只搜索我想搜索的消息类型

    思路:期望从根源上解决此类问题,让客户可以根据自己的业务场景来实现各类增删改查,突破SDK接口功能比较通用,却又无法主动实现复杂业务逻辑的痛点.


    67、当前场景:SDK文件存储限制. 
    思路:对接第三方文件存储。当前已可以提供上传下载思路与demo,需要的话官方支持群里@杨剑



    68、在聊天过程中,返回界面,未读消息数不归0问题
    一般未读消息数无法归零,最有可能的一个原因是easeIMkitmanager没有初始化,从而导致代理为nil,也就没有进行下一步的逻辑.可先检查下easeIMkitmanager是否进行初始化了.
    如果您这边没用到easeIMkit,可使用下面截图部分api进行操作:


    69、使用xcframework动态库时,如何去除动态库内的x86架构?
    xcframework不需要去除x86架构,直接打包即可.

    70、 3.8.4版本的gif大表情无法正常显示
    请升级版本至3.8.6以上

    71、 环信im针对appkey迁移用户数据
    我们无法操作客户数据,否则会破坏完整性,建议您自己操作

    72、在进入聊天界面时,会有一个滚动的动态效果,我不想要这个效果.
    您好,您这边将如下图所示红色箭头指向的bool值改成false即可



    73、服务端获取新的token之后旧的token是否失效?
    不会失效,token失效以服务器返回时间为准。


    74、客户APP集成环信后打包上架过程中审核报错:调用了苹果私有Api.

    1.尝试再次提交审核;2.发邮件申诉并贴出EMMessage代码。


    75、iOS端SDK没有找回密码的API,这个功能如何实现?

    需要后台调用Rest Api重置用户密码的接口即可。


    76、每次重启App,deviceToken会不会改变?

    不会改变,一般是卸载app重装,升级手机系统,deviceToken才会改变。


    77、如何获取昵称?
    环信是不涉及用户个人信息的,所以通过环信ID是获取不到用户昵称头像的,用户的个人信息可以在自己服务器与环信ID绑定存储维护,知道环信ID就可以到自己服务器下载这个环信ID对应的用户信息。

    78、线上的离线推送不成功,开发环境没问题,如何排查?
    换一个新的账号

    79、用户未读消息数,在服务端怎么获取?
    目前服务端不支持获取未读消息数,可在客户端获取。

    80、群组功能,邀请用户进入群组,被邀人能直接进入群组吗?还是需要被邀请人同意才能进入群组?
    客户端用户接收群邀请可以自动进入群组,也可以被邀请人同意后进入群组。

    81、如何同时集成IM和客服云SDK?
    开通IM和客服之后,只集成客服即可(客服内部有IM模块,可直接使用此模块)


    82、环信IM账号客户端退出登录为什么还能够收到推送消息?
    在登出的方法里将BOOL值设置为YES

    83、更换AppKey后服务端发送推送消息移动端收到的消息通知不显示消息内容?

    更换AppKey,下属账号默认不显示消息内容,客户端的默认设置不显示消息内容,需要将displayStyle打开即可。

    84、将用户拉入黑名单还能聊天?
    A把B拉黑,A能B发消息,但是B不能给A发消息。


    收起阅读 »

    旧版音视频与EaseCallKit兼容升级方案

    适用场景当前旧App(1.0)使用旧版音视频SDK,想升级到App 2.0,使用EaseCallKit,但不能强制客户的App升级,在一定时间内,App2.0要与App1.0同时存在,且可以进行音视频通信。方案一1、在App2.0中同时集成旧版音视频SDK(H...
    继续阅读 »

    适用场景
    当前旧App(1.0)使用旧版音视频SDK,想升级到App 2.0,使用EaseCallKit,但不能强制客户的App升级,在一定时间内,App2.0要与App1.0同时存在,且可以进行音视频通信。

    方案一

    1、在App2.0中同时集成旧版音视频SDK(Hyphenate)、声网SDK和EaseCallKit(EaseCallKit需要修改源码,改成使用Hyphenate),新增App Server(或已有AppServer),AppServer包含以下接口

    设置版本信息
    获取版本信息


    2、新App初始化时,调用AppServer,设置本身账户的版本信息 

    3、旧App呼叫,依然走旧版呼叫过程,新App集成了旧版音视频SDK,可以接通

    4、新App呼叫前,调用AppServer,获取被叫用户的版本信息,能获取到,则使用EaseCallKit呼叫,不能获取到版本信息,则依然走旧版SDK 呼叫过程




    问题:
    1、被叫方在多端同时登录,且各端的新旧版本不同,那么只有新版本能收到呼叫(多端不考虑。多端是指web端和移动端,不含桌面端)。
    2、客户先登录新版本,然后退出,再登录旧版本,无法接听(升级后再降级,不考虑)。

    方案二

    App 2.0 同时集成旧版本和新版本,初始化时从AppServer获取开关,若为关,使用旧版音视频,若为开,使用新版音视频,在3个月-6个月内开关关闭 ,大部分客户都更新新版本后,打开开关

    方案一和方案二可以结合使用

    收起阅读 »

    集成环信IM自定义添加表情组

    除了默认的兔斯基示例,想要自定义添加表情组,如何下手呢。今天手把手教你去哪里研究。1、iOS端添加自定义表情组先集成源码,然后找如下截图部分代码,这部分代码即为表情功能的逻辑,大家可以从此处着手,去实现自己需要的逻辑。2、Android端添加自定义表情组参考下...
    继续阅读 »

    除了默认的兔斯基示例,想要自定义添加表情组,如何下手呢。今天手把手教你去哪里研究。


    1、iOS端添加自定义表情组

    先集成源码,然后找如下截图部分代码,这部分代码即为表情功能的逻辑,大家可以从此处着手,去实现自己需要的逻辑。





    2、Android端添加自定义表情组

    参考下面这块代码添加表情组⬇️⬇️⬇️



    初始化之后设置这个provider,根据表情id返回具体表情数据



    其他的大家自己研究啦,如果还是没研究明白,欢迎留言~~

    收起阅读 »

    Android运行时权限终极方案,用PermissionX

    痛点在哪里?没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不...
    继续阅读 »

    痛点在哪里?

    没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。

    这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不友好。

    以一个拨打电话的功能为例,因为 CALL_PHONE 权限是危险权限,所以在我们除了要在 AndroidManifest.xml 中声明权限之外,还要在执行拨打电话操作之前进行运行时权限处理才行。

    权限声明如下:

    然后,编写如下代码来进行运行时权限处理

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    makeCallBtn.setOnClickListener {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
    call()
    } else {
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
    }
    }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
    1 -> {
    if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    call()
    } else {
    Toast.makeText(this, "You denied CALL_PHONE permission", Toast.LENGTH_SHORT).show()
    }
    }
    }
    }

    private fun call() {
    try {
    val intent = Intent(Intent.ACTION_CALL)
    intent.data = Uri.parse("tel:10086")
    startActivity(intent)
    } catch (e: SecurityException) {
    e.printStackTrace()
    }
    }

    }


    这段代码中真有正意义的功能逻辑就是 call() 方法中的内容,可是如果直接调用 call() 方法是无法实现拨打电话功能的,因为我们还没有申请 CALL_PHONE 权限。

    那么整段代码其他的部分就都是在处理 CALL_PHONE 权限申请。可以看到,这里需要先判断用户是否已授权我们拨打电话的权限,如果没有的话则要进行权限申请,然后还要在 onRequestPermissionsResult() 回调中处理权限申请的结果,最后才能去执行拨打电话的操作。

    你可能觉得,这也不算是很繁琐呀,代码量并不是很多。那是因为,目前我们还只是处理了运行时权限最简单的场景,而实际的项目环境中有着更加复杂的场景在等着我们。

    比如说,你的 App 可能并不只是单单申请一个权限,而是需要同时申请多个权限。虽然 ActivityCompat.requestPermissions() 方法允许一次性传入多个权限名,但是你在 onRequestPermissionsResult() 回调中就需要判断哪些权限被允许了,哪些权限被拒绝了,被拒绝的权限是否影响到应用程序的核心功能,以及是否要再次申请权限。

    而一旦牵扯到再次申请权限,就引出了一个更加复杂的问题。你申请的权限被用户拒绝过了一次,那么再次申请将很有可能再次被拒绝。为此,Android 提供了一个 shouldShowRequestPermissionRationale() 方法,用于判断是否需要向用户解释申请这个权限的原因,一旦 shouldShowRequestPermissionRationale() 方法返回 true,那么我们最好弹出一个对话框来向用户阐明为什么我们是需要这个权限的,这样可以增加用户同意授权的几率。

    是不是已经觉得很复杂了?不过还没完,Android 系统还提供了一个 “拒绝,不要再询问” 的选项,如下图所示:

    只要用户选择了这个选项,那么我们以后每次执行权限申请的代码都将会直接被拒绝。

    可是如果我的某项功能就是必须要依赖这个权限才行呢?没有办法,你只能提示用户去应用程序设置当中手动打开权限,程序方面已无法进行操作。

    可以看出,如果想要在项目中对运行时权限做出非常全面的处理,是一件相当复杂的事情。事实上,大部分的项目都没有将权限申请这块处理得十分恰当,这也是我编写 PermissionX 的理由。

    PermissionX 的实现原理

    在开始介绍 PermissionX 的具体用法之前,我们先来讨论一下它的实现原理。

    其实之前并不是没有人尝试过对运行时权限处理进行封装,我之前在做直播公开课的时候也向大家演示过一种运行时权限 API 的封装过程。

    但是,想要对运行时权限的 API 进行封装并不是一件容易的事,因为这个操作是有特定的上下文依赖的,一般需要在 Activity 中接收 onRequestPermissionsResult() 方法的回调才行,所以不能简单地将整个操作封装到一个独立的类中。

    为此,也衍生出了一系列特殊的封装方案,比如将运行时权限的操作封装到 BaseActivity 中,或者提供一个透明的 Activity 来处理运行时权限等。

    不过上述两种方案都不够轻量,因为改变 Activity 的继承结构这可是大事情,而提供一个透明的 Activty 则需要在 AndroidManifest.xml 中进行额外的声明。

    现在,业内普遍比较认可使用另外一种小技巧来进行实现。是什么小技巧呢?回想一下,之前所有申请运行时权限的操作都是在 Activity 中进行的,事实上,Android 在 Fragment 中也提供了一份相同的 API,使得我们在 Fragment 中也能申请运行时权限。

    但不同的是,Fragment 并不像 Activity 那样必须有界面,我们完全可以向 Activity 中添加一个隐藏的 Fragment,然后在这个隐藏的 Fragment 中对运行时权限的 API 进行封装。这是一种非常轻量级的做法,不用担心隐藏 Fragment 会对 Activity 的性能造成什么影响。

    这就是 PermissionX 的实现原理了,书中其实也已经介绍过了这部分内容。但是,在其实现原理的基础之上,后期我又增加了很多新功能,让 PermissionX 变得更加强大和好用,下面我们就来学习一下 PermissionX 的具体用法。

    基本用法

    要使用 PermissionX 之前,首先需要将其引入到项目当中,如下所示

    dependencies {
    ...
    implementation 'com.permissionx.guolindev:permissionx:1.1.1'
    }


    我在写本篇文章时 PermissionX 的最新版本是 1.1.1,想要查看它的当前最新版本,请访问 PermissionX 的主页:github.com/guolindev/P…

    PermissionX 的目的是为了让运行时权限处理尽可能的容易,因此怎么让 API 变得简单好用就是我优先要考虑的问题。

    比如同样实现拨打电话的功能,使用 PermissionX 只需要这样写:

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    makeCallBtn.setOnClickListener {
    PermissionX.init(this)
    .permissions(Manifest.permission.CALL_PHONE)
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    call()
    } else {
    Toast.makeText(this, "您拒绝了拨打电话权限", Toast.LENGTH_SHORT).show()
    }
    }
    }
    }

    ...

    }


    是的,PermissionX 的基本用法就这么简单。首先调用 init() 方法来进行初始化,并在初始化的时候传入一个 FragmentActivity 参数。由于 AppCompatActivity 是 FragmentActivity 的子类,所以只要你的 Activity 是继承自 AppCompatActivity 的,那么直接传入 this 就可以了。

    接下来调用 permissions() 方法传入你要申请的权限名,这里传入 CALL_PHONE 权限。你也可以在 permissions() 方法中传入任意多个权限名,中间用逗号隔开即可。

    最后调用 request() 方法来执行权限申请,并在 Lambda 表达式中处理申请结果。可以看到,Lambda 表达式中有 3 个参数:allGranted 表示是否所有申请的权限都已被授权,grantedList 用于记录所有已被授权的权限,deniedList 用于记录所有被拒绝的权限。

    因为我们只申请了一个 CALL_PHONE 权限,因此这里直接判断:如果 allGranted 为 true,那么就调用 call() 方法,否则弹出一个 Toast 提示。

    运行结果如下:

    怎么样?对比之前的写法,是不是觉得运行时权限处理没那么繁琐了?

    核心用法

    然而我们目前还只是处理了最普通的场景,刚才提到的,假如用户拒绝了某个权限,在下次申请之前,我们最好弹出一个对话框来向用户解释申请这个权限的原因,这个又该怎么实现呢?

    别担心,PermissionX 对这些情况进行了充分的考虑。

    onExplainRequestReason() 方法可以用于监听那些被用户拒绝,而又可以再次去申请的权限。从方法名上也可以看出来了,应该在这个方法中解释申请这些权限的原因。

    而我们只需要将 onExplainRequestReason() 方法串接到 request() 方法之前即可,如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这种情况下,所有被用户拒绝的权限会优先进入 onExplainRequestReason() 方法进行处理,拒绝的权限都记录在 deniedList 参数当中。接下来,我们只需要在这个方法中调用 showRequestReasonDialog() 方法,即可弹出解释权限申请原因的对话框,如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    showRequestReasonDialog() 方法接受 4 个参数:第一个参数是要重新申请的权限列表,这里直接将 deniedList 参数传入。第二个参数则是要向用户解释的原因,我只是随便写了一句话,这个参数描述的越详细越好。第三个参数是对话框上确定按钮的文字,点击该按钮后将会重新执行权限申请操作。第四个参数是一个可选参数,如果不传的话相当于用户必须同意申请的这些权限,否则对话框无法关闭,而如果传入的话,对话框上会有一个取消按钮,点击取消后不会重新进行权限申请,而是会把当前的申请结果回调到 request() 方法当中。

    另外始终要记得将所有申请的权限都在 AndroidManifest.xml 中进行声明:

    重新运行一下程序,效果如下图所示:

    当前版本解释权限申请原因对话框的样式还无法自定义,1.3.0 版本当中已支持了自定义权限提醒对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

    当然,我们也可以指定要对哪些权限重新申请,比如上述申请的 3 个权限中,我认为 CAMERA 权限是必不可少的,而其他两个权限则可有可无,那么在重新申请的时候也可以只申请 CAMERA 权限:


    PermissionX.init(this)   
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION)
    .onExplainRequestReason { deniedList ->
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这样当再次申请权限的时候就只会申请 CAMERA 权限,剩下的两个权限最终会被传入到 request() 方法的 deniedList 参数当中。

    解决了向用户解释权限申请原因的问题,接下来还有一个头疼的问题要解决:如果用户不理会我们的解释,仍然执意拒绝权限申请,并且还选择了拒绝且不再询问的选项,这该怎么办?通常这种情况下,程序层面已经无法再次做出权限申请,唯一能做的就是提示用户到应用程序设置当中手动打开权限。

    更多用法

    那么 PermissionX 是如何处理这种情况的呢?我相信绝对会给你带来惊喜。PermissionX 中还提供了一个 onForwardToSettings() 方法,专门用于监听那些被用户永久拒绝的权限。另外从方法名上就可以看出,我们可以在这里提醒用户手动去应用程序设置当中打开权限。代码如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .onForwardToSettings { deniedList ->
    showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    可以看到,这里又串接了一个 onForwardToSettings() 方法,所有被用户选择了拒绝且不再询问的权限都会进行到这个方法中处理,拒绝的权限都记录在 deniedList 参数当中。

    接下来,你并不需要自己弹出一个 Toast 或是对话框来提醒用户手动去应用程序设置当中打开权限,而是直接调用 showForwardToSettingsDialog() 方法即可。类似地,showForwardToSettingsDialog() 方法也接收 4 个参数,每个参数的作用和刚才的 showRequestReasonDialog() 方法完全一致,我这里就不再重复解释了。

    showForwardToSettingsDialog() 方法将会弹出一个对话框,当用户点击对话框上的我已明白按钮时,将会自动跳转到当前应用程序的设置界面,从而不需要用户自己慢慢进入设置当中寻找当前应用了。另外,当用户从设置中返回时,PermissionX 将会自动重新请求相应的权限,并将最终的授权结果回调到 request() 方法当中。效果如下图所示:

    同样,1.3.0 版本也支持了自定义这个对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

    PermissionX 最主要的功能大概就是这些,不过我在使用一些 App 的时候发现,有些 App 喜欢在第一次请求权限之前就先弹出一个对话框向用户解释自己需要哪些权限,然后才会进行权限申请。这种做法是比较提倡的,因为用户同意授权的概率会更高。

    那么 PermissionX 中要如何实现这样的功能呢?

    其实非常简单,PermissionX 还提供了一个 explainReasonBeforeRequest() 方法,只需要将它也串接到 request() 方法之前就可以了,代码如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")
    }
    .onForwardToSettings { deniedList ->
    showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这样,当每次请求权限时,会优先进入 onExplainRequestReason() 方法,弹出解释权限申请原因的对话框,用户点击我已明白按钮之后才会执行权限申请。效果如下图所示:

    不过,你在使用 explainReasonBeforeRequest() 方法时,其实还有一些关键的点需要注意。

    第一,单独使用 explainReasonBeforeRequest() 方法是无效的,必须配合 onExplainRequestReason() 方法一起使用才能起作用。这个很好理解,因为没有配置 onExplainRequestReason() 方法,我们怎么向用户解释权限申请原因呢?

    第二,在使用 explainReasonBeforeRequest() 方法时,如果 onExplainRequestReason() 方法中编写了权限过滤的逻辑,最终的运行结果可能和你期望的会不一致。这一点可能会稍微有点难理解,我用一个具体的示例来解释一下。

    观察如下代码:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
    }
    ...


    这里在 onExplainRequestReason() 方法中编写了刚才用到的权限过滤逻辑,当有多个权限被拒绝时,我们只重新申请 CAMERA 权限。

    在没有加入 explainReasonBeforeRequest() 方法时,一切都可以按照我们所预期的那样正常运行。但如果加上了 explainReasonBeforeRequest() 方法,在执行权限请求之前会先进入 onExplainRequestReason() 方法,而这里将除了 CAMERA 之外的其他权限都过滤掉了,因此实际上 PermissionX 只会请求 CAMERA 这一个权限,剩下的权限将完全不会尝试去请求,而是直接作为被拒绝的权限回调到最终的 request() 方法当中。

    效果如下图所示:

    针对于这种情况,PermissionX 在 onExplainRequestReason() 方法中提供了一个额外的 beforeRequest 参数,用于标识当前上下文是在权限请求之前还是之后,借助这个参数在 onExplainRequestReason() 方法中执行不同的逻辑,即可很好地解决这个问题,示例代码如下:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList, beforeRequest ->
    if (beforeRequest) {
    showRequestReasonDialog(deniedList, "为了保证程序正常工作,请您同意以下权限申请", "我已明白")
    } else {
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
    }
    }
    ...


    可以看到,当 beforeRequest 为 true 时,说明此时还未执行权限申请,那么我们将完整的 deniedList 传入 showRequestReasonDialog() 方法当中。

    而当 beforeRequest 为 false 时,说明某些权限被用户拒绝了,此时我们只重新申请 CAMERA 权限,因为它是必不可少的,其他权限则可有可无。

    最终运行效果如下:

    代码下载:XPermission-master.zip

    收起阅读 »