注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS砸壳

一、砸壳软件脱壳,顾名思义,就是对软件加壳的逆操作,把软件上存在的壳去掉(解密)。1.1 砸壳原理1.1.1 应用加壳(加密)提交给Appstore发布的App,都经过官方保护而加密,这样可以保证机器上跑的应用是苹果审核过的,也可以管理软件授权(企业包默认情况...
继续阅读 »

一、砸壳

软件脱壳,顾名思义,就是对软件加壳的逆操作,把软件上存在的壳去掉(解密)。

1.1 砸壳原理

1.1.1 应用加壳(加密)

提交给Appstore发布的App,都经过官方保护而加密,这样可以保证机器上跑的应用是苹果审核过的,也可以管理软件授权(企业包默认情况下也是没有加密的,TF是加壳的。)。经过App Store加密的应用,我们无法通过Hopper等反编译静态分析,也无法Class-Dump,在逆向分析过程中需要对加密的二进制文件进行解密才可以进行静态分析,这一过程就是大家熟知的砸壳(脱壳)。

App Store是通过对称加密(AES)加壳的,为了速度和效率。

1.1.2 应用砸壳(解密)

静态砸壳
静态砸壳就是在已经掌握和了解到了壳应用的加密算法和逻辑后在不运行壳应用程序的前提下将壳应用程序进行解密处理。静态脱壳的方法难度大,而且加密方发现应用被破解后就可能会改用更加高级和复杂的加密技术。

动态砸壳  
动态砸壳就是从运行在进程内存空间中的可执行程序映像(image)入手,来将内存中的内容进行转储(dump)处理来实现脱壳处理。这种方法实现起来相对简单,且不必关心使用的是何种加密技术。在iOS中都是用的动态砸壳。

1.2 iOS应用运行原理


  • 加了壳的程序CPU是读不懂的,只有解密后才能载入内存。
  • iOS系统内核会对MachO进行脱壳。
  • 所以我们只需要将解密后的MachO拷贝出来。
  • 非越狱手机做不到跨进程访问,越狱后拿到root权限就可以访问了。这就是砸壳的原理。(按页加解密-代码段)


  • 二、Clutch

    Clutch是由KJCracks开发的一款开源砸壳工具。工具支持iPhoneiPod TouchiPad。该工具需要使用iOS8.0以上的越狱手机应用。

    2.1安装

    Clutch官网找到发布的版本下载:




    查看这个文件可以看到支持arm_v7arm_v7sarm64设备:

    file Clutch-2.0.4
    Clutch-2.0.4: Mach-O universal binary with 3 architectures: [arm_v7:Mach-O executable arm_v7] [arm_v7s:Mach-O executable arm_v7s] [arm64:Mach-O 64-bit executable arm64]
    Clutch-2.0.4 (for architecture armv7): Mach-O executable arm_v7
    Clutch-2.0.4 (for architecture armv7s): Mach-O executable arm_v7s
    Clutch-2.0.4 (for architecture arm64): Mach-O 64-bit executable arm64

    2.2 使用


    映射端口,python或者iproxy都可以

    usbConnect.sh
    1. 拷贝Clutch到手机(注意加可执行权限)
      scp -P 端口 文件 用户@地址:目录/别名
    ➜ scp -P 12345 ./Clutch-2.0.4  root@localhost:/var/root/Clutch
    Clutch-2.0.4 100% 1204KB 32.1MB/s 00:00

    手机端查看:

    zaizai:~ root# ls
    Application\ Support/ Clutch Library/ Media/

    zaizai:~ root# ls -l
    total 1204
    drwxr-xr-x 3 root wheel 96 Mar 17 2018 Application\ Support/
    -rw-r--r-- 1 root wheel 1232832 May 25 16:59 Clutch
    drwxr-xr-x 11 root wheel 352 Oct 23 2019 Library/
    drwxr-xr-x 2 root wheel 64 Feb 27 2008 Media/
    加可执行权限:

    zaizai:~ root# chmod +x Clutch
    zaizai:~ root# ls -l
    total 1204
    drwxr-xr-x 3 root wheel 96 Mar 17 2018 Application\ Support/
    -rwxr-xr-x 1 root wheel 1232832 May 25 16:59 Clutch*
    drwxr-xr-x 11 root wheel 352 Oct 23 2019 Library/
    drwxr-xr-x 2 root wheel 64 Feb 27 2008 Media/
    1. 列出可以砸壳的应用列表 Clutch -i
    root# ./Clutch -i
    1. 砸壳 Clutch –d 应用ID
    root# Clutch –d  4
    砸壳成功后的应用在Device->private->var->mobileDocuments->Dumped目录下。

    自己拷贝的应用是加壳的。

    1. 在手机端通过ps -A找到进程:
    14837 ??         0:03.93 /var/containers/Bundle/Application/8F382114-BBA7-4D81-AA3E-3CD02E03E23E/WeChat.app/WeChat
    16560 ttys000 0:00.02 grep WeChat
    1. 然后拷贝:
    scp -P 12345 root@localhost://var/containers/Bundle/Application/8F382114-BBA7-4D81-AA3E-3CD02E03E23E/WeChat.app/WeChat
     otool -l WeChat  | grep crypt
    cryptoff 28672
    cryptsize 4096
    cryptid 1

    三、插入动态库

    是通过DYLD_INSERT_LIBRARIES来实现的。

    1. 创建一个HPHook动态库,创建一个类实现一个+load方法:
    + (void)load {
    NSLog(@"\n\n\nInject SUCCESS 🍉🍉🍉\n\n\n");
    }

    编译拷贝出HPHook.framework

    1. 拷贝HPHook.framework到越狱手机
     scp -r -P 12345 HPHook.framework root@localhost:/var/root
    CodeResources 100% 2258 301.0KB/s 00:00
    HPHook 100% 85KB 9.0MB/s 00:00
    HPHook.h 100% 422 91.2KB/s 00:00
    module.modulemap 100% 93 25.4KB/s 00:00
    Info.plist 100% 744 187.8KB/s 00:00

    • -r:代表循环拷贝文件夹。
    1. 查看手机App进程(任意一个)
    zaizai:/var/root mobile$ ps -A | grep InsertDemo
    16708 ?? 0:00.13 /var/containers/Bundle/Application/5AC46FE0-EB40-4FE2-BEA5-1AED9C95E7E9/InsertDemo.app/InsertDemo
    16710 ttys000 0:00.01 grep InsertDemo

    1. HPHook.framework插入步骤3中的App

    zaizai:/var/root mobile$ DYLD_INSERT_LIBRARIES=HPHook.framework/HPHook  /var/containers/Bundle/Application/5AC46FE0-EB40-4FE2-BEA5-1AED9C95E7E9/InsertDemo.app/InsertDemo
    2021-05-25 18:32:07.606 InsertDemo[16797:7420505]


    Inject SUCCESS 🍉🍉🍉

    这个时候就插入成功了。


    iOS9.1以后root用户不能用DYLD_INSERT_LIBRARIES(会报错kill 9),需要切换到mobile用户(su mobile)。
    主流App会有防护,可以自己创建一个App插入。
    高版本的iOS系统可能会遇到错误。

    四、dumpdecrypted

    dumpdecryptedGithub开源工具。这个工具就是通过建立一个名为dumpdecrypted.dylib的动态库,插入目标应用实现脱壳。


    4.1 安装

    1. dumpdecrypted官网直接git clone

    2. 通过make编译生成动态库:

    ➜  dumpdecrypted-master make
    `xcrun --sdk iphoneos --find gcc` -Os -Wimplicit -isysroot `xcrun --sdk iphoneos --show-sdk-path` -F`xcrun --sdk iphoneos --show-sdk-path`/System/Library/Frameworks -F`xcrun --sdk iphoneos --show-sdk-path`/System/Library/PrivateFrameworks -arch armv7 -arch armv7s -arch arm64 -dynamiclib -o dumpdecrypted.dylib dumpdecrypted.o
    ld: warning: directory not found for option '-F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.4.sdk/System/Library/PrivateFrameworks'
    ld: warning: directory not found for option '-F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.4.sdk/System/Library/PrivateFrameworks'
    ld: warning: directory not found for option '-F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.4.sdk/System/Library/PrivateFrameworks'
    直接在clone的目录make,最后会生成dumpdecrypted.dylib

    1. 拷贝到手机


    ➜  dumpdecrypted-master scp  -P 12345  dumpdecrypted.dylib  mobile@localhost:/var/mobile/
    mobile@localhost's password:
    dumpdecrypted.dylib
    100% 209KB 24.6MB/s 00:00
    1. 通过DYLD_INSERT_LIBRARIES 环境变量插入动态库执行
    DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/containers/Bundle/Application/36B02DC8-B625-4633-A2C7-45079855BFAC/Aweme.app/Aweme
    需要将dumpdecrypted.dylib拷贝到mobile路径中,为了导出有写的权限。dumpdecrypted.dylib会导出和自己同一目录。

    五、frida-iOS-dump

    该工具基于frida提供的强大功能通过注入js实现内存dump然后通过python自动拷贝到电脑生成ipa文件。

    5.1 安装

    5.1.1 Mac安装

    1. 查看python版本(Mac自带)


      ~ python --version
    Python 2.7.16

    如果是python3这里需要改成python3。根据自己的版本进行配置。

    2.查看pip版本

      ~ pip --version
    pip 19.0.1 from /Library/Python/2.7/site-packages/pip-19.0.1-py2.7.egg/pip (python 2.7)
    如果没有安装,执行:

    sudo easy_install pip
    卸载pip:python -m pip uninstall pip,如果是python3就安装pip3

    3.安装frida

    sudo pip install frida-tools



    出现这个提示表明目录不归当前用户所有。请检查该目录的权限和所有者。需要sudo-H标志。

    sudo -H pip install frida-tools

    • sudo -H:  set-home 将 HOME 变量设为目标用户的主目录

    5.1.2 iOS安装

    1. 添加源(需要科学上网)
    https://build.frida.re
    安装Frida




    5.2 Mac配置ios-dump

    1. 下载脚本
    sudo git clone https://github.com/AloneMonkey/frida-ios-dump
    或者直接去github下载。然后拷贝到/opt目录。

    当然如果电脑上安装了Monkey那直接在monkey目录中安装frida就好了。不然的话导出环境变量可能会有冲突。(monkey中已经导出dump.py了)。直接在Monkey/bin目录进行安装,将安装需要的内容直接从下载的frida-ios-dump中拷贝到Monkey/bin目录(其实只需要requirements.txtdump.jsdump.py。然后在该目录下安装依赖。

    1. 安装依赖
    //sudo pip install -r /opt/frida-ios-dump/requirements.txt –upgrade
    sudo pip install -r requirements.txt --ignore-installed six
    在这个过程中有可能报错:

    *frida-tools 1.2.2 has requirement prompt-toolkit<2.0.0,>=0.57, but you'll have >prompt-toolkit 2.0.7 which is incompatible.
    需要降低 prompt-toolkit 版本:

    //卸载
    $sudo pip uninstall prompt-toolkit
    //安装指定版本
    $sudo pip install prompt-toolkit==1.0.6
    1. 修改dump.py
    User = 'root'
    Password = 'alpine'
    Host = 'localhost'
    Port = 12345
    一般只需要修改Port就好了,和自己映射的本地端口一致。

    5.3 frida 命令

    • frida-ps:列出电脑上的进程
      ~ frida-ps
    PID Name
    ----- --------------------------------------------------------------------------------
    514 AirPlayUIAgent
    573 AppSSOAgent
    65527 Backup and Sync from Google
    533 Backup and Sync from Google
    636 CoreLocationAgent
    73560 CoreServicesUIAgent

    • frida-ps -U:列出手机进程
      ~ frida-ps -U
    PID Name
    ----- -----------------------------------------------
    15758 AlipayWallet
    16643 BTFuwa
    18079 CAReportingService
    11127 CMFSyncAgent
    1644 CommCenter
    17367 ContainerMetadataExtractor
    6691 EscrowSecurityAlert
    16196 HeuristicInterpreter
    11204 IDSRemoteURLConnectionAgent
    16218 MQQSecure
    11119 MobileGestaltHelper
    17611 PhotosReliveWidget
    10051 PinCleaner

    • frida -U 微信:进入微信进程,调试微信
    ➜  ~ frida -U AlipayWallet
    ____
    / _ | Frida 14.2.18 - A world-class dynamic instrumentation toolkit
    | (_| |
    > _ | Commands:
    /_/ |_| help -> Displays the help system
    . . . . object? -> Display information about 'object'
    . . . . exit/quit -> Exit
    . . . .
    . . . . More info at https://frida.re/docs/home/

    5.4 砸壳

    5.4.1 查看安装的应用

    dump.py -l可以查看已经安装的应用

    ~ dump.py -l
    /Library/Python/2.7/site-packages/paramiko/transport.py:33: CryptographyDeprecationWarning: Python 2 is no longer supported by the Python core team. Support for it is now deprecated in cryptography, and will be removed in the next release.
    from cryptography.hazmat.backends import default_backend
    PID Name Identifier
    ----- ------------ -------------------------------
    13582 微信 com.tencent.xin
    10769 支付宝 com.alipay.iphoneclient
    9912 相机 com.apple.camera
    11265 腾讯手机管家 com.tencent.mqqsecure
    - Acrobat com.adobe.Adobe-Reader
    - App Store com.apple.AppStore
    - Cydia com.saurik.Cydia
    - Enframe me.sspai.Enframe
    - Excel com.microsoft.Office.Excel

    这个时候是不需要映射的。

    5.4.2 导出ipa

    dump.py bundleId/displayName:

    //dump.py 微信
    dump.py com.tencent.xin
    • 可以通过bundleId或者displayName导出应用,推荐使用bundleIddisplayName可能会有同名。如果有同名哪个排在前面导出哪个。
    • 导出ipa包时需要app在运行状态(正常情况下会自动打开App),最好在前台不锁屏。
    • 导出的ipa包一般在你执行导出命令的目录。(如果配置了环境变量,任何目录都可以执行)目录没有权限会报错。
    • 导出包的时候是需要打开端口映射的。

    验证(需要解压拿到.app):


    ➜ otool -l WeChat.app/WeChat | grep crypt
    cryptoff 16384
    cryptsize 101646336
    cryptid 0
    • cryptid0表示没有加密,否则是加密的包。

    错误信息

    1. ImportError: No module named typing
      pip安装后报错:
    ~ sudo pip install frida-tools
    Traceback (most recent call last):
    File "/usr/local/bin/pip", line 11, in <module>
    load_entry_point('pip==21.1.1', 'console_scripts', 'pip')()
    File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/pkg_resources/__init__.py", line 489, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
    File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/pkg_resources/__init__.py", line 2843, in load_entry_point
    return ep.load()
    File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/pkg_resources/__init__.py", line 2434, in load
    return self.resolve()
    File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/pkg_resources/__init__.py", line 2440, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
    File "/Library/Python/2.7/site-packages/pip-21.1.1-py2.7.egg/pip/__init__.py", line 1, in <module>
    from typing import List, Optional
    ImportError: No module named typing
    解决方案:

    sudo easy_install pip==19.0.1

    直接指定版本安装。

    2.Invalid requirement: '–upgrade'
    直接替换掉–upgrade命令:

    //sudo pip install -r /opt/frida-ios-dump/requirements.txt –upgrade
    sudo pip install -r requirements.txt --ignore-installed six

    3.如果遇见命令输入没有反映,电脑端frida没有问题。那么需要重新安装手机端frida

    4.Start the target app QQMusic unable to launch iOS app: The operation couldn’t be completed. Application info provider (FBSApplicationLibrary) returned nil for ""
    如果App启动后还是无法dump,直接通过BundleId dump就好了。



    作者:HotPotCat

    总结

    • 应用砸壳:一般应用为了防止反编译分析会对应用进行加密(加壳)。砸壳就是解密的过程。
      • 静态砸壳:已经知道了解密方式,不需要运行应用的情况下直接解密。
      • 动态砸壳:在应用启动之后从内存中找到应用的位置,dump(内存中)数据。
    • Clutch( 命令行工具)
      • Clutch -i:列出可以砸壳的应用列表。
      • Clutch –d 应用ID:砸壳
    • dumpdecrypted(动态库)
      • 通过DYLD_INSERT_LIBRARIES环境变量插入动态库载入某个进程。
      • 配置DYLD_INSERT_LIBRARIES=dumpdecrypted路径 Macho路径
    • frida-iOS-dump(利用frida加载脚本砸壳)
      • 安装frida(MaciPhone都需要)
      • 下载frida-iOS-dump脚本工具
      • 执行dump.py displayName /BundleId


    作者:HotPotCat
    链接:https://www.jianshu.com/p/0d89bbff8140




    收起阅读 »

    Flutter实战详解--高仿好奇心日报

    前言最近Flutter一直比较火,我也它也是非常感兴趣,看了下官网的基础教程后我决定直接上手做一个App,一是这样学的比较快印象更加深刻,二是可以记录其中遇到的一些坑,帮助大家少走一些弯路.本篇文章我会尽可能详细的讲到每一个点上.项目地址Github,如果觉得...
    继续阅读 »

    前言

    最近Flutter一直比较火,我也它也是非常感兴趣,看了下官网的基础教程后我决定直接上手做一个App,一是这样学的比较快印象更加深刻,二是可以记录其中遇到的一些坑,帮助大家少走一些弯路.本篇文章我会尽可能详细的讲到每一个点上.

    项目地址

    Github,如果觉得不错,欢迎Star

    注意事项

    1.下载项目后报错是因为没有添加依赖,在pubspec.yaml文件中点击Packages get下载依赖,有时候会在这里出现卡死的情况,可以配置一下环境变量.在终端执行vi ~/.bash_profile,再添加export PUB_HOSTED_URL=https://pub.flutter-io.cn和
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn.详情请看修改Flutter环境变量.
    2.需要将File Encodings里的Project Encoding设置为UTF-8,否则有时候安卓会报错
    3.如果cocoapods不是最新可能会出现Error Running Pod Install,请更新cocoapods.
    4.由于flutter_webview_plugin这个插件只支持加载url,于是就需要做一些修改.

    iOS 在FlutterWebviewPlugin.m文件中的- (void)navigate:(FlutterMethodCall*)call方法中的最后一排,将[self.webview loadRequest:request]方法改为[self.webview loadHTMLString:url baseURL:nil]
    Android 在WebViewManager.java文件中webView.loadUrl(url)方法改为webView.loadData(url, "text/html", "UTF-8"),以及下面那排的void reloadUrl(String url) { webView.loadUrl(url); }改为void reloadUrl(String url) { webView.loadData(url, "text/html", "UTF-8"); }
    先看看效果图吧.

    iOS效果图


    Android效果图


    正题

    怎么搭建Flutter环境我就不多说了,官网上讲的很详细,还没有搭建开发环境的可以看看这个Flutter中文网.

    1导航栏Tabbar


    这里我用到了DefaultTabController这个控件,使用DefaultTabController包裹需要用到Tab的页面即可,它的child为Scaffold,Scaffold有个appBar属性,在AppBar中设置具体的样式,大家看代码会更加清楚.相关注释也都写上了.

    home: new DefaultTabController(
    length: titleList.length,
    child: new Scaffold(
    appBar: new AppBar(
    elevation: 0.0,//导航栏下面那根线
    title: new TabBar(
    isScrollable: false,//是否可滑动
    unselectedLabelColor: Colors.black26,//未选中按钮颜色
    labelColor: Colors.black,//选中按钮颜色
    labelStyle: TextStyle(fontSize: 18),//文字样式
    indicatorSize: TabBarIndicatorSize.label,//滑动的宽度是根据内容来适应,还是与整块那么大(label表示根据内容来适应)
    indicatorWeight: 4.0,//滑块高度
    indicatorColor: Colors.yellow,//滑动颜色
    indicatorPadding: EdgeInsets.only(bottom: 1),//与底部距离为1
    tabs: titleList.map((String text) {//tabs表示具体的内容,是一个数组
    return new Tab(
    text: text,
    );
    }).toList(),
    ),
    ),
    //body表示具体展示的内容
    body:TabBarView(children: [News(url: 'http://app3.qdaily.com/app3/homes/index_v2/'),News(url: 'http://app3.qdaily.com/app3/papers/index/')]) ,
    ),
    ),

    大家也可以看看官网的示例Flutter官网示例

    2. 不同样式的item

    样式一


    这种布局的大概结构如下


    注意这里图片是紧贴着右边屏幕的,所以这里需要用到Expanded控件,用于自动填充子控件.

    样式二


    这个样式的控件布局就很简单了,结构如下


    样式三


    这个和样式二差不多,只不过最上面多了一块.

    这里需要注意的是,那个你猜这个图片是堆叠在整个大图上面的,所以需要用到Stack这个控件,其中Stack中有个属性const FractionalOffset(double dx, double dy)用于表示子控件相对于父控件的位置

    样式四


    这种样式稍微复杂一点,结构如下


    3、数据抓取

    用青花瓷抓取了好奇心数据.青花瓷使用教程


    简单分析一下,has_more表示是否可以加载更多,last_key用于上拉加载的时候请求用的,feeds就是每一条数据,banners就是轮播图的信息,columns就是横向滚动的ListView的相关数据,这个后面讲.接下来就做json序列化相关的了.

    4.Json序列化

    首先在pubspec.yaml中导入

    dependencies:
    json_annotation: ^2.0.0
    dev_dependencies:
    build_runner: ^1.0.0
    json_serializable: ^2.0.0

    创建一个model.dart文件
    引入文件

    import 'package:json_annotation/json_annotation.dart';
    part 'model.g.dart';

    其中这个model.g.dart等会儿会自动生成.这里需要掌握两个知识点

    1.@JsonSerializable() 这是表示告诉编译器这个类是需要生成Model类的
    2,@JsonKey 由于服务器返回的部分数据名称在Dart语言中是不被允许的,比如has_more,Dart中命名不能出现下划线,所以就需要用到@JsonKey来告诉编译器这个参数对于json中的哪个字段

    @JsonSerializable()
    class Feed {
    String image;
    int type;
    @JsonKey(name: 'index_type')
    int indexType;
    Post post;
    @JsonKey(name: 'news_list')
    List<News> newsList;
    Feed(this.image,this.type,this.post,this.indexType,this.newsList);
    factory Feed.fromJson(Map<String,dynamic> json) => _$FeedFromJson(json);
    Map<String, dynamic> toJson() => _$FeedToJson(this);
    }

    好了,写完后会报错,因为FeedFromJson和FeedToJson没有找到,这个时候在控制到输入flutter packages pub run build_runner build指令后会自动生成一个moded.g.dart文件,于是在网络请求下来数据后就可以用Feed feed = Feed.fromJson(data)这个方法来将Json中数据转换保存在Feed这个实例中了.在model类中还有些复杂的Json嵌套,但是也都很简单,大家看一眼应该就会了,哈哈.JSON和序列化具体教程

    5.轮播图

    Flutter中的轮播图我用到了Fluuter_Swiper这个组件,这里设置小圆点属性的时候稍微麻烦了点,网上好像也没有讲到,我这里讲一下.
    首先要创建DotSwiperPaginationBuilder

    DotSwiperPaginationBuilder builder = DotSwiperPaginationBuilder(
    color: Colors.white,//未选中圆点颜色
    activeColor: Colors.yellow,//选中圆点颜色
    size:7,//未选中大小
    activeSize: 7,//选中圆点大小
    space: 5//圆点间距
    );

    然后在Swiper中的pagination属性中设置它

    pagination: new SwiperPagination(
    builder: builder,
    ),

    6.网络请求

    首先,展示页面要继承自StatefulWidget,因为需要动态更新数据和列表.
    网络请求插件我用的Dio,非常好用.
    在initState方法中请求数据表示刚加载页面的时候进行网络请求,请求数据方法如下

    void getData()async{
    if (lastKey == '0'){
    dataList = [];//下拉刷新的时候将DataList制空
    }
    Dio dio = new Dio();
    Response response = await dio.get("$url$lastKey.json");
    Reslut reslut = Reslut.fromJson(response.data);
    if(!reslut.response.hasMore){
    return;//如果没有数据就不继续了
    }
    if(reslut.response.columns != null) {
    columnList = reslut.response.columns;
    }
    lastKey = reslut.response.lastKey;//更新lastkey
    setState(() {
    if (reslut.response.banners != null){
    banners = reslut.response.banners;//给轮播图赋值
    }
    dataList.addAll(reslut.response.feeds);//给数据源赋值
    });
    }

    因为用到了setState()方法,所以在该方法中改变了的数据会对其相应的地方进行刷新,比如设置了ListView的itemCount个数为dataList.length,如果在SetState方法中dataList.length改变了,那么ListView的itemCount树也会自动改变并刷新ListView.

    7. 上拉刷新与加载

    Flutter中有RefreshIndicator用于下拉刷新,它有个onRefresh闭包方法,表示下拉的时候执行的方法,一般用于网络请求.onRefresh方法如下

    Future<void> _handleRefresh() {
    final Completer<void> completer = Completer<void>();
    Timer(const Duration(seconds: 1), () {
    completer.complete();
    });
    return completer.future.then<void>((_) {
    lastKey = '0';
    getData();
    });
    }

    下拉加载的话需要初始化一个ScrollController,将它设为ListView的controller,并对其进行监听,当滑动到最底部的时候进行网络请求.

    @override
    void initState() {
    url = widget.url;
    getData();
    _scrollController.addListener(() {
    ///判断当前滑动位置是不是到达底部,触发加载更多回调
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
    getData();
    }
    });
    }
    final ScrollController _scrollController = new ScrollController();

    上拉加载loading框用到了flutter_spinkit插件,提供了大量的加载样式.


    代码如下

    ///上拉加载更多
    Widget _buildProgressIndicator() {
    ///是否需要显示上拉加载更多的loading
    Widget bottomWidget = new Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
    ///loading框
    new SpinKitThreeBounce(color: Color(0xFF24292E)),
    new Container(
    width: 5.0,
    ),
    ]);
    return new Padding(
    padding: const EdgeInsets.all(20.0),
    child: new Center(
    child: bottomWidget,
    ),
    );
    }

    8. ListView赋值

    由于最上面有一个轮播图,最下面有加载框,所以ListView的itemCount个数为dataList.length+2,又因为每个item之间都有一个浅灰色的风格线,所以需要用到ListView.separated,具体代码如下:

    Widget build(BuildContext context) {
    return RefreshIndicator(
    onRefresh:(()=> _handleRefresh()),
    color: Colors.yellow,//刷新控件的颜色
    child: ListView.separated(
    physics: const AlwaysScrollableScrollPhysics(),
    itemCount: _getListCount(),//item个数
    controller: _scrollController,//用于监听是否滑到最底部
    itemBuilder: (context,index){
    if(index == 0){
    return SwiperWidget(context, banners);//如果是第一个,则展示banner
    }else if(index < dataList.length + 1){
    return WidgetUtils.GetListWidget(context, dataList[index - 1]);//展示数据
    }else {
    return _buildProgressIndicator();//展示加载loading框
    }
    },
    separatorBuilder: (context,idx){//分割线
    return Container(
    height: 5,
    color: Color.fromARGB(50,183, 187, 197),
    );
    },
    ),
    );
    }

    9. ListView嵌套横向滑动ListView

    这种的话也稍微复杂一点,有两种样式.并且到滑到最右边的时候可以继续请求并加载数据.


    首先来分析一下数据


    这个colunmns就是横向滑动列表的重要数据.


    里面的id是请求参数,show_type表示列表的样式,location表示插入的位置.而且通过抓取接口发现,当横向列表快要展示出来的时候,才会去请求横向列表的具体接口.
    那么思路就很清晰了,在请求获得数据后遍历colunmns,根据每个colunmn的location插入一个Map,如下

    data.insert(colunm.location,  {'id':colunm.id,'showType':colunm.showType});

    再创建一个ColumnsListWidget类,继承自StatefulWidget,是一个新item,在滑动到该列表的位置的时候,会将该Map数据传给ColumnsListWidget,这个时候ColumnsListWidget就会加载数据并展示出来了,滑到最右边的时候加载和滑到最底部加载的方法一样,就不多说了.具体可以查看源码,关键代码如下:

    static Widget GetListWidget(BuildContext context, dynamic data) {
    Widget widget;
    if(data.runtimeType == Feed) {
    if (data.indexType != null) {
    widget = NewsListWidget(context, data);
    } else if (data.type == 2) {
    widget = ListImageTop(context, data);
    } else if (data.type == 0) {
    widget = ActivityWidget(context, data);
    } else if (data.type == 1) {
    widget = ListImageRight(context, data);
    }
    }else{
    widget = ColumnsListWidget(id: data['id'],showType: data['showType'],);
    }

    1.横向ListView外需要用Flexible包裹,Flexible组件可以使Row、Column、Flex等子组件在主轴方向有填充可用空间的能力(例如,Row在水平方向,Column在垂直方向),但是它与Expanded组件不同,它不强制子组件填充可用空间。
    2.ListView初始位置用到padding: new EdgeInsets.symmetric(horizontal: 12.0),用padding: EdgeInsets.only(left: 12)的话会让ListView和最左边一直有条线

    10.webview加载复杂的Html字段


    获取到网页详情的数据发现是Html字段,并且其中的css是url地址,试了很多Flutter加载Html的插件发现样式都不正确,最后决定使用原生和Flutter混编,这时候发现flutter_webview_plugin这个插件是使用原生网页的,不过它只支持加载url,于是就需要做一些修改.
    iOS
    在FlutterWebviewPlugin.m文件中的- (void)navigate:(FlutterMethodCall*)call方法中的最后一排,将[self.webview loadRequest:request]方法改为[self.webview loadHTMLString:url baseURL:nil]
    Android
    在WebViewManager.java文件中webView.loadUrl(url)方法改为webView.loadData(url, "text/html", "UTF-8"),以及下面那排的void reloadUrl(String url) { webView.loadUrl(url); }改为void reloadUrl(String url) { webView.loadData(url, "text/html", "UTF-8"); }
    由于服务器端返回的Html中的css和js文件地址是/assets/app3开头的,所以需要替换成绝对路径,所以要用到这个方法htmlBody.replaceAll( '/assets/app3','http://app3.qdaily.com/assets/app3')
    好了,这下就可以呈现出漂亮的网页了.

    11.ListView嵌套GridView

    在点击横向滑动列表的总标题的时候,会进入到相关栏目的详情页,如图


    这个ListView包含上下两部分.上面这部分为:


    结构如下


    下面就是一个GridView,不过有时候下面会是ListView,根据shouwType字段来判断,GridView的代码如下:

    Widget ColumnsDetailTypeTwo(BuildContext context,List<Feed> feesList){
    return GridView.count(
    physics: NeverScrollableScrollPhysics(),
    crossAxisCount: 2,
    shrinkWrap: true,
    mainAxisSpacing: 10.0,
    crossAxisSpacing: 15.0,
    childAspectRatio: 0.612,
    padding: new EdgeInsets.symmetric(horizontal: 20.0),
    children: feesList.map((Feed feed) {
    return ColumnsTypeTwoTile(context, feed);
    }).toList()
    );
    }

    其中 childAspectRatio表示宽高比.

    圆角头像需要用到
    CircleAvatar(backgroundImage:NetworkImage(url),),这个控件

    12、在切换Tab的时候防止执行initState

    在切换顶部tab的时候会发现下面的界面会自动滑动到顶(位置重置)并执行initState,同时每次滑到横向ListView的时候,它也会执行initState并且位置也会重置,要让它只执行一次initState方法的话需要这么做.

    class _XXXState extends State<XXX> with AutomaticKeepAliveClientMixin{
    @override
    bool get wantKeepAlive => true;

    这样它就会只执行一次initState方法了.

    总结
    做了这个项目最大的感受就是界面布局是真的很方便很简单,因为做了一遍对很多知识点也理解的更深了.如果觉得有帮助到你的话,希望可以给个 Star

    项目地址
    Github

    链接:https://www.jianshu.com/p/4a0185b5a8f5

    收起阅读 »

    OLLVM代码混淆移植与使用

    简介OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfusc...
    继续阅读 »

    简介

    OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfuscator-llvm/obfuscator,只不过仅更新到llvm的4.0,2017年开始就没在更新。

    移植

    OLLVM如果自己想拿最新版的LLVM和Clang进行移植功能其实也并不是很难,整理一下其实改动很小,接下来将会讲一下移植的方法。

    个人整理

    先放一下个人移植好的版本地址https://github.com/heroims/obfuscator.git,个人fork原版后又加入了llvm5.0,6.0,7.0以及swift-llvm5.0的版本,应该能满足大部分需求了,如果有新版本下面的讲解,各位也可以自己动手去下载自己需要的llvm和clang进行移植。git上的提交每次都很独立如下图,方便各位cherry-pick。


    下载LLVM

    llvm地址:https://github.com/llvm-mirror
    swift-llvm地址:https://github.com/apple
    大家可以从上面的地址下载最新的自己需要的llvm和clang

    #下载llvm源码
    wget https://codeload.github.com/llvm-mirror/llvm/zip/release_70
    unzip llvm-release_70.zip
    mv llvm-release_70 llvm


    #下载clang源码
    wget https://codeload.github.com/llvm-mirror/clang/zip/release_70
    unzip clang-release_70.zip
    mv clang-release_70 llvm/tools/clang

    添加混淆代码

    如果用git的话只需要执行git cherry-pick xxxx把xxxx换成对应的我的版本上的提交哈希填上即可。极度推荐用git搞定。

    如果手动一点点加的话,第一步就是把我改过的OLLVM文件夹里/include/llvm/Transforms/Obfuscation和/lib/llvm/Transforms/Obfuscation移动到刚才下载好的llvm源码文件夹相同的位置。

    git clone https://github.com/heroims/obfuscator.git
    cd obfuscator
    git checkout llvm-7.0
    cp include/llvm/Transforms/Obfuscation llvm/include/llvm/Transforms/Obfuscation
    cp lib/llvm/Transforms/Obfuscation llvm/lib/llvm/Transforms/Obfuscation

    然后手动修改8个文件如下:









    编译

    mkdir build
    cd build
    #如果不想跑测试用例加上-DLLVM_INCLUDE_TESTS=OFF
    cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_CREATE_XCODE_TOOLCHAIN=ON ../obfuscator/
    make -j7

    使用

    这里原版提供了3种混淆方式分别是控制流扁平化,指令替换,虚假控制流程,用起来都是加cflags的方式。下面简单说下这几种模式。

    控制流扁平化

    这个模式主要是把一些if-else语句,嵌套成do-while语句

    -mllvm -fla:激活控制流扁平化
    -mllvm -split:激活基本块分割。在一起使用时改善展平。
    -mllvm -split_num=3:如果激活了传递,则在每个基本块上应用3次。默认值:1

    指令替换

    这个模式主要用功能上等效但更复杂的指令序列替换标准二元运算符(+ , – , & , | 和 ^)

    -mllvm -sub:激活指令替换
    -mllvm -sub_loop=3:如果激活了传递,则在函数上应用3次。默认值:1

    虚假控制流程

    这个模式主要嵌套几层判断逻辑,一个简单的运算都会在外面包几层if-else,所以这个模式加上编译速度会慢很多因为要做几层假的逻辑包裹真正有用的代码。

    另外说一下这个模式编译的时候要浪费相当长时间包哪几层不是闹得!

    -mllvm -bcf:激活虚假控制流程
    -mllvm -bcf_loop=3:如果激活了传递,则在函数上应用3次。默认值:1
    -mllvm -bcf_prob=40:如果激活了传递,基本块将以40%的概率进行模糊处理。默认值:30

    上面说完模式下面讲一下几种使用方式

    直接用二进制文件

    直接使用编译的二进制文件build/bin/clang test.c -o test -mllvm -sub -mllvm -fla -mllvm -bcf

    NDK集成

    这里分为工具链的制作和项目里的配置。

    制作Toolchains

    这里以修改最新的ndk r18为例,老的ndk版本比这更容易都在ndk-bundle/toolchains里放着需要修改的文件。

    #复制ndk的toolschain里的llvm
    cp -r ndk-bundle/toolchains/llvm ndk-bundle/toolchains/ollvm
    #删除prebuilt文件夹下的文件夹的bin和lib64,prebuilt文件夹下根据系统不同命名也不同
    rm -rf ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/bin
    rm -rf ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/lib64
    #把我们之前编译好的ollvm下的bin和lib移到我们刚才删除bin和lib64的目录下
    mv build/bin ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/
    mv build/lib ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/
    #复制ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains的文件夹,这里根据自己对CPU架构的需求自己复制然后修改
    cp -r ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains/arm-linux-androideabi-clang⁩ ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains/arm-linux-androideabi-clang-ollvm

    最后把arm-linux-androideabi-clang-ollvm里的setup.mk文件进行修改

    TOOLCHAIN_NAME := ollvm
    TOOLCHAIN_ROOT := $(call get-toolchain-root,$(TOOLCHAIN_NAME))
    TOOLCHAIN_PREFIX := $(TOOLCHAIN_ROOT)/bin

    config.mk里是CPU架构,刚才是复制出来的所以不用修改,但如果要添加其他的自定义架构需要严格按照格式规范命名最初的文件夹,如mips的需要添加文件夹mipsel-linux-android-clang-ollvm,setup.mk和刚才的修改一样即可。

    项目中配置

    到了项目里还需要修改两个文件:
    在Android.mk 中添加混淆编译参数

    LOCAL_CFLAGS += -mllvm -sub -mllvm -bcf -mllvm -fla

    Application.mk中配置NDK_TOOLCHAIN_VERSION

    #根据需要添加
    APP_ABI := x86 armeabi-v7a x86_64 arm64-v8a mips armeabi mips64
    #使用刚才我们做好的编译链
    NDK_TOOLCHAIN_VERSION := ollvm

    Visual Studio集成

    编译ollvm的时候,使用cmake-gui选择Visual Studio2015或者命令行选择cmake -G "Visual Studio 14 2015" -DCMAKE_BUILD_TYPE=Release ../obfuscator/
    然后cmake会产生一个visual studio工程,用vs编译即可!
    至于将Visual Studio的默认编译器换成clang编译,参考https://www.ishani.org/projects/ClangVSX/

    Visual Studio2015起官方开始支持Clang,具体做法:
    文件->新建->项目->已安装->Visual C++->跨平台->安装Clang with Microsoft CodeGen
    Clang是一个完全不同的命令行工具链,这时候可以在工程配置中,平台工具集选项里找到Clang,然后使用ollvm的clang替换该clang即可。

    XCode集成

    XCode里集成需要看版本,XCode10之前和之后是一个分水岭,XCode9之前和之后有一个小配置不同。

    XCode10以前

    $ cd /Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/
    $ sudo cp -r Clang\ LLVM\ 1.0.xcplugin/ Obfuscator.xcplugin
    $ cd Obfuscator.xcplugin/Contents/
    $ sudo plutil -convert xml1 Info.plist
    $ sudo vim Info.plist

    修改:

    <string>com.apple.compilers.clang</string> -> <string>com.apple.compilers.obfuscator</string>
    <string>Clang LLVM 1.0 Compiler Xcode Plug-in</string> -> <string>Obfuscator Xcode Plug-in</string>

    执行:

    $ sudo plutil -convert binary1 Info.plist
    $ cd Resources/
    $ sudo mv Clang\ LLVM\ 1.0.xcspec Obfuscator.xcspec
    $ sudo vim Obfuscator.xcspec

    修改:

    <key>Description</key>
    <string>Apple LLVM 8.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
    <key>ExecPath</key>
    <string>clang</string> -> <string>/path/to/obfuscator_bin/clang</string>
    <key>Identifier</key>
    <string>com.apple.compilers.llvm.clang.1_0</string> -> <string>com.apple.compilers.llvm.obfuscator.4_0</string>
    <key>Name</key>
    <string>Apple LLVM 8.0</string> -> <string>Obfuscator 4.0</string>
    <key>Vendor</key>
    <string>Apple</string> -> <string>HEIG-VD</string>
    <key>Version</key>
    <string>7.0</string> -> <string>4.0</string>

    执行:

    $ cd English.lproj/
    $ sudo mv Apple\ LLVM\ 5.1.strings "Obfuscator 3.4.strings"
    $ sudo plutil -convert xml1 Obfuscator\ 3.4.strings
    $ sudo vim Obfuscator\ 3.4.strings

    修改:

    <key>Description</key>
    <string>Apple LLVM 8.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
    <key>Name</key>
    <string>Apple LLVM 8.0</string> -> <string>Obfuscator 4.0</string>
    <key>Vendor</key>
    <string>Apple</string> -> <string>HEIG-VD</string>
    <key>Version</key>
    <string>7.0</string> -> <string>4.0</string>

    执行:

    $ sudo plutil -convert binary1 Obfuscator\ 3.4.strings

    XCode9之后要设置Enable Index-While-Building成NO



    XCode10之后

    xcode10之后无法使用添加ideplugin的方法,但添加编译链跑的依然可行,另外网上一些人说不能开bitcode,不能提交AppStore,用原版llvm改的ollvm的确有可能出现上述情况,所以我用苹果的swift-llvm改了一版暂时没去试着提交,或许可以,有兴趣的也可以自己下载使用试试obfuscator这版,特别备注由于修改没有针对swift部分所以用swift写的代码没混淆,回头有空的话再弄。

    创建XCode的toolchain然后把生成的文件夹放到/Library/Developer/下

    cd build
    sudo make install-xcode-toolchain
    mv /usr/local/Toolchains /Library/Developer/

    Toolchains下的.xctoolchain文件就是一个文件夹,进去修改info.plist

    <key>CFBundleIdentifier</key>
    <string>org.llvm.7.0.0svn</string> -> <string>org.ollvm-swift.5.0</string>

    修改完在XCode的Toolchains下就会显示相应的名称

    然后如图打开XCode选择Toolchaiins




    按这些配置好后就算是可以用了。

    最后

    简单展示一下混淆后的成果

    源码


    反编译未混淆代码


    反编译混淆后代码


    扩展:字符串混淆

    原版是没有这功能的本来,Armariris 提供了这个功能,我这也移植过来了,毕竟不难。
    首先把StringObfuscation的.h,.cpp文件放到对应的Obfuscation文件夹下,然后分别修改下面的文件。


    用法

    -mllvm -sobf:编译时候添加选项开启字符串加密
    -mllvm -seed=0xdeadbeaf:指定随机数生成器种子

    效果

    看个添加了-mllvm -sub -mllvm -sobf -mllvm -fla -mllvm -bcf这么一串的效果。

    源码


    反编译未混淆代码


    反编译混淆后代码


    转自:https://www.jianshu.com/p/e0637f3169a3

    收起阅读 »

    iOS多设备适配简史以及相应的API支撑实现

    远古的iPhone3和iPhone4时代,设备尺寸都是固定3.5inch,没有所谓的适配的问题,只需要用视图的frame属性进行硬编码即可。随着时间的推移,苹果的设备种类越来越多,尺寸也越来越大,单纯的frame已经不能简单解决问题了,于是推出了AutoLay...
    继续阅读 »

    远古的iPhone3和iPhone4时代,设备尺寸都是固定3.5inch,没有所谓的适配的问题,只需要用视图的frame属性进行硬编码即可。随着时间的推移,苹果的设备种类越来越多,尺寸也越来越大,单纯的frame已经不能简单解决问题了,于是推出了AutoLayout技术和SizeClasses技术来解决多种设备的适配问题。一直在做iOS开发的程序员相信在下面的两个版本交界处需要处理适配的坎一定让你焦头烂额过:

    1、iOS7出来后视图控制器的根视图默认的尺寸是占据整个屏幕的,如果有半透明导航条的话也默认是延伸到导航栏和状态栏的下面。这段时间相信你对要同时满足iOS7和以下的版本进行大面积的改版和特殊适配处理,尤其是状态栏的高度问题尤为棘手。

    2、iOS11出来后尤其是iPhoneX设备推出,iPhoneX设备的特殊性表现为顶部的状态栏高度由20变为了44,底部还出现了一个34的安全区,当横屏时还需要考虑左右两边的44的缩进处理。你需要对所有的布局代码进行重新适配和梳理以便兼容iPhoneX和其他设备,这里面还是状态栏的高度以及底部安全区的的高度尤为棘手。

    个人认为这两个版本的发布是iOS开发人员遇到的需要大量布局改版的版本。为了达到完美适配我们可能需要写大量的if,else以及写很多宏以及版本兼容来进行特殊处理。当然苹果也为上面两次大改版提供了诸多的解决方案:

    1、iOS7中对视图控制器提供了如下属性来解决版本兼容性的问题:

    @property(nonatomic,assign) UIRectEdge edgesForExtendedLayout NS_AVAILABLE_IOS(7_0); // Defaults to UIRectEdgeAll
    @property(nonatomic,assign) BOOL extendedLayoutIncludesOpaqueBars NS_AVAILABLE_IOS(7_0); // Defaults to NO, but bars are translucent by default on 7_0.
    @property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets API_DEPRECATED_WITH_REPLACEMENT("Use UIScrollView's contentInsetAdjustmentBehavior instead", ios(7.0,11.0),tvos(7.0,11.0)); // Defaults to YES

    @property(nonatomic,readonly,strong) id<UILayoutSupport> topLayoutGuide API_DEPRECATED_WITH_REPLACEMENT("-[UIView safeAreaLayoutGuide]", ios(7.0,11.0), tvos(7.0,11.0));
    @property(nonatomic,readonly,strong) id<UILayoutSupport> bottomLayoutGuide API_DEPRECATED_WITH_REPLACEMENT("-[UIView safeAreaLayoutGuide]", ios(7.0,11.0), tvos(7.0,11.0));

    2、iOS11中提出了一个安全区的概念,要求我们的可操作视图都放置在安全区内,并对视图和滚动视图提供了如下扩展属性:

    @property (nonatomic,readonly) UIEdgeInsets safeAreaInsets API_AVAILABLE(ios(11.0),tvos(11.0));
    - (void)safeAreaInsetsDidChange API_AVAILABLE(ios(11.0),tvos(11.0));

    /* The top of the safeAreaLayoutGuide indicates the unobscured top edge of the view (e.g, not behind
    the status bar or navigation bar, if present). Similarly for the other edges.
    */
    @property(nonatomic,readonly,strong) UILayoutGuide *safeAreaLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));
    /* When contentInsetAdjustmentBehavior allows, UIScrollView may incorporate
    its safeAreaInsets into the adjustedContentInset.
    */
    @property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0),tvos(11.0));

    /* Also see -scrollViewDidChangeAdjustedContentInset: in the UIScrollViewDelegate protocol.
    */
    - (void)adjustedContentInsetDidChange API_AVAILABLE(ios(11.0),tvos(11.0)) NS_REQUIRES_SUPER;

    /* Configure the behavior of adjustedContentInset.
    Default is UIScrollViewContentInsetAdjustmentAutomatic.
    */
    @property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));

    /* contentLayoutGuide anchors (e.g., contentLayoutGuide.centerXAnchor, etc.) refer to
    the untranslated content area of the scroll view.
    */
    @property(nonatomic,readonly,strong) UILayoutGuide *contentLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));

    /* frameLayoutGuide anchors (e.g., frameLayoutGuide.centerXAnchor) refer to
    the untransformed frame of the scroll view.
    */
    @property(nonatomic,readonly,strong) UILayoutGuide *frameLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0));

    这些属性的具体意义这里就不多说了,网络上以及苹果的官方都有很多资料在介绍这些属性的意思。从上面的这些属性中可以看出苹果提出的这些解决方案其主要是围绕解决视图和导航条、滚动视图、状态栏、屏幕边缘之间的关系而进行的。因为iOS7和iOS11两个版本中控制器中的视图和上面所列出的一些内容之间的关系变化最大。

    NSLayoutConstraint约束以及iOS9上的封装改进
    在iOS6时代苹果推出了AutoLayout的技术解决方案,这是一套采用以相对约束来替代硬编码的解决方法,然而糟糕的方法名和使用方式导致使用成本和代码量的急剧增加。比如下面的一段代码:

    UIButton *button = [self createDemoButton:NSLocalizedString(@"Pop layoutview at center", "") action:@selector(handleDemo1:)];
    button.translatesAutoresizingMaskIntoConstraints = NO; //button使用AutoLayout
    [scrollView addSubview:button];

    //下面的代码是iOS6以来自带的约束布局写法,可以看出代码量较大。
    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];

    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTop multiplier:1 constant:10]];

    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:40]];

    [scrollView addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeWidth multiplier:1 constant:-20]];


    一个简单的将按钮放到一个UIScrollView中去的代码,当用AutoLayout来实现时出现了代码量风暴问题。对于约束的设置到了iOS9以后有了很大的改进,苹果对约束的设置进行了封装,提供了三个类:NSLayoutXAxisAnchor, NSLayoutYAxisAnchor, NSLayoutDimension来简化约束的设置,还是同样的功能用新的类来写约束就简洁清晰很多了:

    UIButton *button = [self createDemoButton:NSLocalizedString(@"Pop layoutview at center", "") action:@selector(handleDemo1:)];
    button.translatesAutoresizingMaskIntoConstraints = NO; //button使用AutoLayout
    [scrollView addSubview:button];
    [button.centerXAnchor constraintEqualToAnchor:scrollView.centerXAnchor].active = YES;
    [button.topAnchor constraintEqualToAnchor:scrollView.topAnchor constant:10].active = YES;
    [button.heightAnchor constraintEqualToConstant:40].active = YES;
    [button.widthAnchor constraintEqualToAnchor:scrollView.widthAnchor multiplier:1 constant:-20].active = YES;

    UIStackView
    在iOS9中还提供了一个UIStackView的类来简化那些视图需要从上往下或者从左往右依次添加排列的场景,通过UIStackView容器视图的使用就不再需要为每个子视图添加冗余的依赖约束关系了。在大量的实践中很多应用的各板块其实都是按顺序从上到下排列或者从左到右排列的。所以如果您的应用最低支持到iOS9的话就可以大量的应用这个类来构建你的程序了。

    占位视图类UILayoutGuide
    在iOS9以前两个视图之间的间距和间隔是无法支持浮动和可伸缩设置的,以及我们可以需要在两个视图之间保留一个浮动尺寸的空白区域,解决的方法是在它们中间加入一个透明颜色的UIView来进行处理,不管如何只要是View都需要进行渲染和绘制从而有可能一定程度上影响程序的性能,而在iOS9以后提供了一个占位视图类UILayoutGuide,这个类就像是一个普通的视图一样可以为它设置约束,也可以将它添加进入视图中去,也可以将这个占位视图作为其他视图的约束依赖项,唯一的不同就是占位视图不会进行任何的渲染和绘制,它只会参与布局处理。因此这个类的引入可以很大程度上解决那些浮动间距的问题。

    SizeClasses多屏幕适配
    当我们的程序可能需要同时在横屏和竖屏下运行并且横屏和竖屏下的布局还不一致时,而且希望我们的应用在小屏幕上和大屏幕上(比如iPhone8 Plus 以及iPhoneX S Max)的布局有差异时,我们可能需要用到苹果的SizeClasses技术。这是苹果在iOS8中推出来的一个概念。 但是在实际的实践中我们很少有看到使用SizeClasses的例子和场景以及在我们开发中很少有使用到这方面的技术,所以我认为这应该是苹果的一个多屏幕适配的失败解决的方案。从字面理解SizeClasses就是尺寸的种类,苹果将设备的宽和高分为了压缩和常规两种尺寸类型,因此我们可以得到如下几种类型的设备:


    很欣慰的是如果您的应用是一个带有系统导航条的应用时很多适配的问题都能够得到很好的解决,因为系统已经为你做了很多事情,你不需要做任何特殊的处理。而如果你的应用的某个界面是present出来的,或者是你自己实现的自定义导航条的话,那么你可能就需要自己来处理各种版本的适配问题了。并且如果你的应用可能还有横竖屏的话那这个问题就更加复杂了。

    最后除了可以用系统提供的API来解决所有的适配问题外,还向大家推荐我的开源布局库:MyLayout。它同时支持Objective-C以及Swift版本。而且用这个库后上面的所有适配问题都不是问题。

    转自:https://www.jianshu.com/p/b43b22fa40e3

    收起阅读 »

    一套包含了社区匹配聊天以及语音互动直播相关的社交系统模板项目

    一套包含了社区匹配聊天语音以及直播相关的社交系统模板项目,包括服务器端以及 Android 客户端背景及选型在实现社交相关项目时,少不了 IM 及时聊天功能,这里选择了自己比较熟悉的环信三方 SDK,环信 IM...
    继续阅读 »

    社交模板项目

    一套包含了社区匹配聊天语音以及直播相关的社交系统模板项目,包括服务器端以及 Android 客户端

    项目资源均来自于互联网,如果有侵权请联系我

    背景及选型

    一直以来都是标榜自己是一个喜欢开源的程序猿,一直想做一款能够被大家认同的开源项目,也是想提供给广大的新手程序猿一个比较完整系统的社交系统以供参考,因此有了这一套社交系统模板项目, 当前模板项目主要功能可以看下边的 功能与TODO

    在实现社交相关项目时,少不了 IM 及时聊天功能,这里选择了自己比较熟悉的环信三方 SDK,环信 IMSDK 能够比较方便的实现自定义扩展功能,比如会话扩展,消息扩展等,消息效果可以看下方 项目截图

    通话方面这里选择了声网提供的服务,看了下他们提供的功能还是比较多的,这里主要用到了语音通话,以及变声效果处理,感觉集成还是比较方便的,之前没用过的情况下,其实两天就搞定了 1V1 通话和多人互动通话的功能,他们还提供了更多场景使用,比如教育,直播等,更多功能大家可以搜索他们官网查看,通话效果可以看下方 项目截图

    开发环境

    项目基本属于在Android开发环境下开发,全局使用Kotlin语言,项目已经适配Android6.x以上的动态权限适配,以及7.x的文件选择,和8.x的通知提醒,10.x的文件选择等;

    • 开发系统:Mac OS 11.1
    • 开发工具:Android Studio 4.2
    • 打包工具:Gradle 4.2.0
    • 开发语言:Kotlin 1.4.32

    项目模块儿

    • app是项目主模块儿,这部分主要包含了项目的业务逻辑,比如匹配、内容发布,信息展示等
    • vmcommon是项目基础模块儿,这部分主要包含了项目的基类封装,依赖管理,包括网络请求,图片加载等工具类
    • vmim聊天与通话模块儿,这是为了方便大家引用到自己的项目中做的一步封装,不用再去复杂的复制代码和资源等,只需要将vmimmodule导入到自己的项目中就行了,具体使用方式参见项目app模块儿;

    功能与TODO

    IM部分功能

    •  链接监听
    •  登录注册
    •  会话功能
      •  置顶
      •  标为未读
      •  删除与清空
      •  草稿功能
    •  聊天功能
      • [x]消息类型
        •  文本消息
        •  图片消息
          •  查看大图
          •  保存图片
      •  消息处理
        •  删除
        •  撤回
        •  复制(仅文本可复制)
      •  昵称头像处理(通过回调实现)
      •  头像点击(回调到 App 层)
      •  语音实时通话功能
        •  1V1音频通话
        •  静音、扬声器播放
        •  音效变声
    •  解忧茶室
      •  创建房间
      •  发送消息
        •  文本消息
        •  鼓励消息
      •  上下麦处理
      •  音效变声(彩蛋功能)

    App部分功能

    •  登录注册(包括业务逻辑和 IM 逻辑)
    •  首页
      •  自己的状态
      •  拉取其他人的状态信息
      •  心情匹配
      •  解忧聊天室
    •  聊天(这里直接加载 IM 模块儿)
    •  发现
      •  发布内容
      •  喜欢操作
      •  详情展示
        •  喜欢操作
        •  评论获取
        •  添加评论
    •  我的
      •  个人信息展示
      •  上传头像、封面
      •  设置昵称、签名、职业、地址、生日、性别等
      •  邮箱绑定
      •  个人发布与喜欢内容展示
    •  设置
      •  个人信息设置
      •  深色模式适配
      •  通知设置
      •  资源加载设置
      •  关于
        •  检查更新
        •  问题反馈
      •  环境切换
      •  退出

    发布功能

    •  多渠道打包
    •  签名配置
    •  开发与线上环境配置
    •  敏感信息保护

    配置运行

    1. 首先复制config.default.gradleconfig.gradle
    2. 配置下config.gradle内相关字段
    3. 正式打包需要自己生成签名文件,然后修改下config.gradlesignings签名信息
    4. 需配合服务器端一起使用,修改上边config.gradle配置文件的baseDebugUrlbaseReleaseUrl

    参与贡献

    如果你有什么好的想法,或者好的实现,可以通过下边的步骤参与进来,让我们一起把这个项目做得更好,欢迎参与 😁

    1. Fork本仓库
    2. 新建feature_xxx分支 (单独创建一个实现你自己想法的分支)
    3. 提交代码
    4. 新建Pull Request
    5. 等待Review & Merge

    其他

    下载体验 

    这就是一个使用当前模板运营的一个项目

    项目截图

    这里简单截取了几个界面,更多功能自己去发现吧

    matchHome matchExplore matchMsg matchMine matchChat matchChatFast matchCall matchAbout matchInfo matchAbout

    交流

    QQ群: 901211985 个人QQ: 1565176197

    QQ 交流群 个人 QQ
    收起阅读 »

    android(6大布局)

    LinearLayout(线性布局) RelativeLayout(相对布局) TableLayout(表格布局) FrameLayout(帧布局) FrameLayout的属性很少就两个,但是在说之前我们先介绍一个东西: 前景图像:永远处于帧布局最上面...
    继续阅读 »


    LinearLayout(线性布局)
    在这里插入图片描述
    RelativeLayout(相对布局)
    在这里插入图片描述
    TableLayout(表格布局)
    在这里插入图片描述
    FrameLayout(帧布局)
    FrameLayout的属性很少就两个,但是在说之前我们先介绍一个东西:
    前景图像:永远处于帧布局最上面,直接面对用户的图像,就是不会被覆盖的图片。
    两个属性:
    android:foreground:*设置改帧布局容器的前景图像
    android:foregroundGravity:设置前景图像显示的位置


    GridLayout(网格布局)
    在这里插入图片描述


    AbsoluteLayout(绝对布局)
    1.四大控制属性(单位都是dp):
    ①控制大小: android:layout_width:组件宽度 android:layout_height:组件高度 ②控制位置: android:layout_x:设置组件的X坐标 android:layout_y:设置组件的Y坐标


    收起阅读 »

    Android四大组件的启动分析与整理(二):Service的启动过程

    前言 换工作后,一直忙,没时间整理,逼自己一把吧,目标一周整理出来,理顺思路,这里先起个头。 service的启动过程分两种,一种是直接start,另一种是bind;我们先来分析第一种,直接start过程要简单的多。一样,先分析源码,然后一幅图总结: st...
    继续阅读 »


    前言


    换工作后,一直忙,没时间整理,逼自己一把吧,目标一周整理出来,理顺思路,这里先起个头。
    service的启动过程分两种,一种是直接start,另一种是bind;我们先来分析第一种,直接start过程要简单的多。一样,先分析源码,然后一幅图总结:


    startService()


    	startService(new Intent());
    public ComponentName startService(Intent service) {
    return mBase.startService(service);
    }

    startService();其实是调用了ContextWrapper中的startService方法,ContextWrapper我的理解是一个外观模式,他基本没有什么自己的东西,而是都去间接调用mBase中的方法,mBase,其实就是Context的实现类ContextImpl类;在 Activity的启动过程 的最后已经介绍了,这个ContextImpl是怎么来的了,这里不多将,继续。


        public ComponentName startService(Intent service) {
    warnIfCallingFromSystemProcess();
    return startServiceCommon(service, false, mUser);
    }
    private ComponentName startServiceCommon(Intent service, boolean requireForeground,
    UserHandle user) {
    try {
    validateServiceIntent(service);
    service.prepareToLeaveProcess(this);
    ComponentName cn = ActivityManager.getService().startService(
    mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
    getContentResolver()), requireForeground,
    getOpPackageName(), user.getIdentifier());
    ..................
    return cn;
    } catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
    }
    }

    这个地方非常熟悉了,调用了AMS的startService方法;


    public ComponentName startService(IApplicationThread caller, Intent service,
    String resolvedType, boolean requireForeground, String callingPackage, int userId)
    throws TransactionTooLargeException {
    enforceNotIsolatedCaller("startService");
    ...............
    synchronized(this) {
    final int callingPid = Binder.getCallingPid();
    final int callingUid = Binder.getCallingUid();
    final long origId = Binder.clearCallingIdentity();
    ComponentName res;
    try {
    res = mServices.startServiceLocked(caller, service,
    resolvedType, callingPid, callingUid,
    requireForeground, callingPackage, userId);
    } finally {
    Binder.restoreCallingIdentity(origId);
    }
    return res;
    }
    }

    这里将启动工作委托给了ActiveService,就像Activity启动的时候将委托工作交给ActivityStarter一样;


    ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
    int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
    throws TransactionTooLargeException {
    final boolean callerFg;
    if (caller != null) {
    final ProcessRecord callerApp = mAm.getRecordForAppLocked(caller);(1)
    ..................
    } else {
    callerFg = true;
    }
    ServiceLookupResult res =
    retrieveServiceLocked(service, resolvedType, callingPackage,
    callingPid, callingUid, userId, true, callerFg, false);(2)
    ..................
    ServiceRecord r = res.record;(3)
    if (!mAm.mUserController.exists(r.userId)) {
    return null;
    }
    ..................
    ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting);(4)
    return cmp;
    }

    这个方法很长,主要是为了获取ProcessRecorder和ServiceRecorder,就跟Activity启动需要ProcessRecorder和ActivityRecorder一样。
    (2)处先从缓存中查找,没有的话直接new一个对象
    (4)处继续调用startServiceInnerLocked方法,这个方法调用了bringUpServiceLocked()方法。


        private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
    boolean whileRestarting, boolean permissionsReviewRequired)
    throws TransactionTooLargeException {
    if (r.app != null && r.app.thread != null) {
    sendServiceArgsLocked(r, execInFg, false);1
    return null;
    }
    final boolean isolated = (r.serviceInfo.flags&ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;
    final String procName = r.processName;
    String hostingType = "service";
    ProcessRecord app;
    if (!isolated) {
    app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
    if (app != null && app.thread != null) {
    try {
    app.addPackage(r.appInfo.packageName, r.appInfo.versionCode, mAm.mProcessStats);
    realStartServiceLocked(r, app, execInFg);(2)
    return null;
    }
    }
    } else {
    .......................
    }
    if (app == null && !permissionsReviewRequired) {(1)
    if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
    hostingType, r.name, false, isolated, false)) == null) {
    bringDownServiceLocked(r);
    return msg;
    }
    if (isolated) {
    r.isolatedProc = app;
    }
    }
    .......................
    return null;
    }

    (1)处是发送service的入参,就是走的onStartCommand()方法,这里第一次进来,app为null,因为ServiceRecorder是新new出来的
    (2)从AMS中获取ProcessRecorder,获取到成功之后,调用realStartServiceLocked()方法去启动service
    (3)如果上一步没有获取到ProcessRecorder,那么就创建一个,这个过程跟Activity创建进程是一样,都是通过Zygote去执行Process.start方法创建新的进程


        private final void realStartServiceLocked(ServiceRecord r,
    ProcessRecord app, boolean execInFg) throws RemoteException {
    .................
    try {
    .................
    app.thread.scheduleCreateService(r, r.serviceInfo,mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
    app.repProcState);1
    r.postNotification();
    created = true;
    }
    .................
    sendServiceArgsLocked(r, execInFg, true);2
    .................
    }

    (1)通知ApplicationThread去执行scheduleCreateService方法,
    (2)创建完了之后,发送入参,也就是调用哦那onStartCommand()方法。


            public final void scheduleCreateService(IBinder token,
    ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
    updateProcessState(processState, false);
    CreateServiceData s = new CreateServiceData();
    s.token = token;
    s.info = info;
    s.compatInfo = compatInfo;
    sendMessage(H.CREATE_SERVICE, s);
    }

    然后就是非常熟悉的地方了,发送handler:CREATE_SERVICE消息


                    case CREATE_SERVICE:
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, ("serviceCreate: " + String.valueOf(msg.obj)));
    handleCreateService((CreateServiceData)msg.obj);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    break;

    private void handleCreateService(CreateServiceData data) {
    unscheduleGcIdler();
    LoadedApk packageInfo = getPackageInfoNoCheck(
    data.info.applicationInfo, data.compatInfo);
    Service service = null;
    try {
    java.lang.ClassLoader cl = packageInfo.getClassLoader();
    service = (Service) cl.loadClass(data.info.name).newInstance();1
    }
    .................
    try {
    ContextImpl context = ContextImpl.createAppContext(this, packageInfo);2
    context.setOuterContext(service);
    Application app = packageInfo.makeApplication(false, mInstrumentation);3
    service.attach(context, this, data.info.name, data.token, app,
    ActivityManager.getService());4
    service.onCreate();5
    mServices.put(data.token, service);
    .................
    }
    .................
    }

    跟启动Activity一样,需要两个必备因素,Context和Application
    (1)处跟Activity一样通过反射创建Service
    (2)处new一个上下文,跟Activity的区别就是不需要传入AMS和classloader
    (3)处跟Activity一样通过反射创建Application
    (4)处attach上去,将context、app、AMS、binder等都封装进去
    (5)处执行onCreate()方法,区别是Activity通过Instrumentation去创建,这里直接调用


    bindService()


    这里追加一下bindService的过程:
    从调用bindService(new Intent(), mConnection, Context.BIND_AUTO_CREATE);开始,跟startService一样,走的context中的方法,然后调用了bindServiceCommon()


    private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags, Handler
    handler, UserHandle user) {
    IServiceConnection sd;
    if (conn == null) {
    throw new IllegalArgumentException("connection is null");
    }
    if (mPackageInfo != null) {
    sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);(1)
    } else {
    throw new RuntimeException("Not supported in system context");
    }
    validateServiceIntent(service);
    try {
    .............
    int res = ActivityManager.getService().bindService(
    mMainThread.getApplicationThread(), getActivityToken(), service,
    service.resolveTypeIfNeeded(getContentResolver()),
    sd, flags, getOpPackageName(), user.getIdentifier());(2)
    .............
    } catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
    }
    }

    (1)跟startService不一样的是,需要先获取IServiceConnection,从名字可以看出实现了binder,那么service就可以跨进程绑定了,IServiceConnection内部new了一个ServiceDispatcher对象,ServiceDispatcher的内部类InnerConnection就是继承了IServiceConnection.stub,实现binder的。
    (2)走AMS的bindService方法,AMS委托给了ActiveService去执行bindServiceLocked()


        int bindServiceLocked(IApplicationThread caller, IBinder token, Intent service,
    String resolvedType, final IServiceConnection connection, int flags,
    String callingPackage, final int userId) throws TransactionTooLargeException {
    final ProcessRecord callerApp = mAm.getRecordForAppLocked(caller);1
    ................
    ServiceLookupResult res =2
    retrieveServiceLocked(service, resolvedType, callingPackage, Binder.getCallingPid(),
    Binder.getCallingUid(), userId, true, callerFg, isBindExternal);
    ................
    ServiceRecord s = res.record;
    try {
    ................
    AppBindRecord b = s.retrieveAppBindingLocked(service, callerApp);3
    ConnectionRecord c = new ConnectionRecord(b, activity,
    connection, flags, clientLabel, clientIntent);4
    ................
    if ((flags&Context.BIND_AUTO_CREATE) != 0) {
    s.lastActivity = SystemClock.uptimeMillis();
    if (bringUpServiceLocked(s, service.getFlags(), callerFg, false,
    permissionsReviewRequired) != null) {5
    return 0;
    }
    }
    if (s.app != null && b.intent.received) {
    try {
    c.conn.connected(s.name, b.intent.binder, false);6
    } catch (Exception e) {
    }
    if (b.intent.apps.size() == 1 && b.intent.doRebind) {
    requestServiceBindingLocked(s, b.intent, callerFg, true);7
    }
    } else if (!b.intent.requested) {
    requestServiceBindingLocked(s, b.intent, callerFg, false);8
    }
    getServiceMapLocked(s.userId).ensureNotStartingBackgroundLocked(s);
    } finally {
    Binder.restoreCallingIdentity(origId);
    }
    return 1;
    }

    (1)处拿到请求者的进程
    (2)处创建必备的条件:ServiceRecord
    (3、4)处bind要比start多两个对象,AppBindRecord和ConnectionRecord,AppBindRecord对象是
    (5)处因为flag是BIND_AUTO_CREATE,因此走bringUpServiceLocked方法去创建Service
    (6)创建成功后,如果b.intent.received表示已经接受到了绑定的bind就会执行c.conn.connected,这个c.conn就是IServiceConnection,前面bindServiceCommon就讲了,ServiceConnection被封到了LoaderApk中的内部类ServiceDispatcher中,ServiceDispatcher的内部类innerConnection继承了IServiceConnection.stub类,并调用ServiceDispatcher的connect方法,并向mActivityThread 的handler发送一个runnable方法执行mConnection.onServiceConnected回调,到此绑定成功。
    (7)如果第一次bind且还没有rebind过,requestServiceBindingLocked第三个参数为true表重新绑定
    (8)如果创建成功还没有绑定,就执行requestServiceBindingLocked第三个参数为false
    这里第一次bind应该是创建了但还没有发送请求,走的8;


    private final boolean requestServiceBindingLocked(ServiceRecord r, IntentBindRecord i,
    boolean execInFg, boolean rebind) throws TransactionTooLargeException {
    if ((!i.requested || rebind) && i.apps.size() > 0) {(1)
    try {
    bumpServiceExecutingLocked(r, execInFg, "bind");
    r.app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_SERVICE);
    r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind,
    r.app.repProcState);2
    if (!rebind) {
    i.requested = true;3
    }
    i.hasBound = true;
    i.doRebind = false;4
    } catch (TransactionTooLargeException e) {
    .............
    } catch (RemoteException e) {
    .............
    }
    }
    return true;
    }

    (1)第一次进来,i.requested没有发送过请求,因此为false,不是重新rebind,在创建AppBinderRecord的时候,i.apps.size() > 0;
    (2)熟悉的一幕,发送scheduleBindService方法,然后发送BIND_SERVICE,然后执行handleBindService方法
    (3、4)设置标志位,请求过了,非重绑


    private void handleBindService(BindServiceData data) {
    Service s = mServices.get(data.token);
    if (s != null) {
    try {
    data.intent.setExtrasClassLoader(s.getClassLoader());
    data.intent.prepareToEnterProcess();
    try {
    if (!data.rebind) {
    IBinder binder = s.onBind(data.intent);
    ActivityManager.getService().publishService(
    data.token, data.intent, binder);
    } else {
    s.onRebind(data.intent);
    ActivityManager.getService().serviceDoneExecuting(
    data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
    }
    ensureJitEnabled();
    } catch (RemoteException ex) {
    }
    } catch (Exception e) {
    }
    }
    }


    没有rebind过的话,通知AMS去执行publishService方法,如果是rebind操作,那么就直接s.onRebind方法,然后通知AMS绑定结束。这里第一次进来,通知AMS去publishService,然后委托ActiveService去执行publishServiceLocked方法;


    void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {
    final long origId = Binder.clearCallingIdentity();
    try {
    if (r != null) {
    Intent.FilterComparison filter
    = new Intent.FilterComparison(intent);
    IntentBindRecord b = r.bindings.get(filter);
    if (b != null && !b.received) {
    b.binder = service;
    b.requested = true;(1)
    b.received = true;(2)
    for (int conni=r.connections.size()-1; conni>=0; conni--) {
    ArrayList<ConnectionRecord> clist = r.connections.valueAt(conni);
    for (int i=0; i<clist.size(); i++) {
    ConnectionRecord c = clist.get(i);(3)
    .....................
    try {
    c.conn.connected(r.name, service, false);3
    } catch (Exception e) {
    .....................
    }
    }
    }
    }
    serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false);
    }
    } finally {
    Binder.restoreCallingIdentity(origId);
    }
    }

    (1)处设置已请求,(2)处设置已绑定;(3)处就是调用IServiceConnection的connect方法。


    		private static class InnerConnection extends IServiceConnection.Stub {
    final WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;
    InnerConnection(LoadedApk.ServiceDispatcher sd) {
    mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);
    }
    public void connected(ComponentName name, IBinder service, boolean dead)
    throws RemoteException {
    LoadedApk.ServiceDispatcher sd = mDispatcher.get();
    if (sd != null) {
    sd.connected(name, service, dead);1
    }
    }
    }
    public void connected(ComponentName name, IBinder service, boolean dead) {
    if (mActivityThread != null) {2
    mActivityThread.post(new RunConnection(name, service, 0, dead));
    } else {
    doConnected(name, service, dead);
    }
    }
    private final class RunConnection implements Runnable {
    .........
    public void run() {
    if (mCommand == 0) {
    doConnected(mName, mService, mDead);
    } else if (mCommand == 1) {
    doDeath(mName, mService);
    }
    }
    .........
    }
    ----------------doConnected-----------------
    mConnection.onServiceConnected(name, service);

    在bind的第一步,其实就将ServiceConnection封装到了ServiceDispatcher中了,其内部类InnerConnection 继承了IServiceConnection.Stub,那么就可以通过binder进行跨进程的通信了,很方便。
    上一步骤的(3)其实就是调用了innerConnection的connect方法(1)处
    (2)处mActivityThread其实就是ActivityThread的handler方法执行run方法,简介调用了doConnected,然后调用mConnection的onServiceConnected()方法,这个mConnection其实就是我们自定义的ServiceConnection类,就此结束;


    startService图解:


    在这里插入图片描述


    bindService图解:


    在这里插入图片描述


    ————————————————
    版权声明:本文为CSDN博主「蒋八九」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/massonJ/article/details/117914349

    收起阅读 »

    Android四大组件的启动分析与整理(一):Activity的启动过程

    前言 换工作后,一直忙,没时间整理,逼自己一把吧,目标一周整理出来,理顺思路,这里先起个头。 首先Activity的启动分两种,一种是根Activity的启动,另一种是普通Activity的启动,根Activity的启动,从LauncherActivity...
    继续阅读 »


    前言


    换工作后,一直忙,没时间整理,逼自己一把吧,目标一周整理出来,理顺思路,这里先起个头。


    首先Activity的启动分两种,一种是根Activity的启动,另一种是普通Activity的启动,根Activity的启动,从LauncherActivity开始,启动方式跟我们平时的startActivity是基本一样的。


    public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
    。。。。。。。。。。。
    //设置flag为new task
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    。。。。。。。。。。。
    if (Utilities.ATLEAST_MARSHMALLOW
    && (item instanceof ShortcutInfo)
    && (item.itemType == Favorites.ITEM_TYPE_SHORTCUT
    || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
    && !((ShortcutInfo) item).isPromise()) {
    // Shortcuts need some special checks due to legacy reasons.
    startShortcutIntentSafely(intent, optsBundle, item);
    } else if (user == null || user.equals(Process.myUserHandle())) {
    // Could be launching some bookkeeping activity
    //通过startActivity开启
    startActivity(intent, optsBundle);
    } else {
    LauncherAppsCompat.getInstance(this).startActivityForProfile(
    intent.getComponent(), user, intent.getSourceBounds(), optsBundle);
    }
    return true;
    } catch (ActivityNotFoundException|SecurityException e) {
    Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
    Log.e(TAG, "Unable to launch. tag=" + item + " intent=" + intent, e);
    }
    return false;
    }

    当点击桌面的图标的时候,调用startActivitySafely(),然后先设置它的flag是new task,然后调用Activity的startActivity()方法,然后调用继续调用startActivityForResult(intent, -1, options);


    	public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
    @Nullable Bundle options) {
    if (mParent == null) {
    options = transferSpringboardActivityOptions(options);
    Instrumentation.ActivityResult ar =
    mInstrumentation.execStartActivity(
    this, mMainThread.getApplicationThread(), mToken, this,
    intent, requestCode, options);
    。。。。。。。。。。。。
    } else {
    。。。。。。。。。。。。
    }
    }

    因为根Activity,mParent肯定为null,Activity最终都会通过Instrumentation工具类去执行,这里就调用了execStartActivity()方法。


    	public ActivityResult execStartActivity(
    Context who, IBinder contextThread, IBinder token, String target,
    Intent intent, int requestCode, Bundle options) {
    IApplicationThread whoThread = (IApplicationThread) contextThread;
    。。。。。。。。。。
    try {
    intent.migrateExtraStreamToClipData();
    intent.prepareToLeaveProcess(who);
    int result = ActivityManager.getService()
    .startActivity(whoThread, who.getBasePackageName(), intent,
    intent.resolveTypeIfNeeded(who.getContentResolver()),
    token, target, requestCode, 0, null, options);
    checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
    throw new RuntimeException("Failure from system", e);
    }
    return null;
    }

    instrumentation中通过调用ActivityManager.getService()方法,得到AMS,然后调用AMS中的startActivity方法继续执行。


        public static IActivityManager getService() {
    return IActivityManagerSingleton.get();
    }

    private static final Singleton<IActivityManager> IActivityManagerSingleton =
    new Singleton<IActivityManager>() {
    @Override
    protected IActivityManager create() {
    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
    final IActivityManager am = IActivityManager.Stub.asInterface(b);
    return am;
    }
    };

    这里需要注意一下,在Android8.0以前,都是通过ActivityManagerNative.getDefalt()方法,然后通过IActiv tyManager am= asinterface (b) ; 去获取AMS的代理类ActivityManagerProxy对象的。在asinterface 中直接new ActivityManagerProxy(b)并返回,8.0之后通过通过IActivityManager.Stub.asInterface(b)去获得,典型的AIDL写法AMS中也继承了IActivityManager.Stub。


    @Override
    public final int startActivity(.....) {
    return startActivityAsUser(.....,UserHandle.getCallingUserId());
    }
    public final int startActivityAsUser(.....) {
    enforceNotIsolatedCaller("startActivity");1
    userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),2
    userId, false, ALLOW_FULL_ONLY, "startActivity", null);
    return mActivityStarter.startActivityMayWait(caller, -1, callingPackage, intent,
    resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,
    profilerInfo, null, null, bOptions, false, userId, null, null,
    "startActivityAsUser");
    }

    (1)处判断调用者的进程是否被隔离,如果是就抛出SecurityException异常。
    (2)处检查调用者是否有权限,如果没有也会抛出SecurityException异常。
    然后继续调用ActivityStarter中的startActivityMayWait()方法,ActivityStarter是Activity的一个控制类,主要将flag和intent转为Activity,然后将Activity和task以及stack关联起来。


    int startActivityLocked(...., String reason) {
    if (TextUtils.isEmpty(reason)) {1
    throw new IllegalArgumentException("Need to specify a reason.");
    }
    mLastStartReason = reason;
    mLastStartActivityTimeMs = System.currentTimeMillis();
    mLastStartActivityRecord[0] = null;
    mLastStartActivityResult = startActivity(....);
    if (outActivity != null) {
    outActivity[0] = mLastStartActivityRecord[0];
    }
    return mLastStartActivityResult;
    }

    startActivityMayWait()方法很长,其中调用了startActivityLocked方法,(1)处就是之前传入的“startActivityAsUser”参数,用来说明调用原因,如果没有原因,抛出IllegalArgument异常,然后继续调用startActivity方法。


    private int startActivity(IApplicationThread caller,.....ActivityRecord[] outActivity,.....) {
    int err = ActivityManager.START_SUCCESS;
    final Bundle verificationBundle
    = options != null ? options.popAppVerificationBundle() : null;
    ProcessRecord callerApp = null;
    if (caller != null) {1
    //获取Launcher进程
    callerApp = mService.getRecordForAppLocked(caller);//2
    if (callerApp != null) {
    //获取Launcher进程的pid和uid并赋值
    callingPid = callerApp.pid;
    callingUid = callerApp.info.uid;
    } else {
    Slog.w(TAG,.....);
    err = ActivityManager.START_PERMISSION_DENIED;
    }
    }
    ...
    //创建即将要启动的Activity的描述类ActivityRecord
    ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
    callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
    resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
    mSupervisor, container, options, sourceRecord);2
    if (outActivity != null) {
    outActivity[0] = r;3
    }
    ...
    doPendingActivityLaunchesLocked(false);
    return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true,
    options, inTask, outActivity);//4
    }

    这里首先在(1)处通过AMS的getRecordForAppLocked方法获取请求进程对象callerApp ,因为是从launcher启动,所以这里是launcher所在的进程,他是一个ProcessRecord对象,然后拿到pid和uid。
    (2)处创建Activity信息,这样ProcessRecord和ActivityRecord就齐全了。继续startActivity;


        private int startActivityUnchecked(....) {
    ........
    int result = START_SUCCESS;
    if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask
    && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {
    newTask = true;
    result = setTaskFromReuseOrCreateNewTask(
    taskToAffiliate, preferredLaunchStackId, topStack);—————(1
    } else if (mSourceRecord != null) {
    result = setTaskFromSourceRecord();
    } else if (mInTask != null) {
    result = setTaskFromInTask();
    } else {
    .........
    setTaskToCurrentTopOrCreateNewTask();
    }
    ........
    if (mDoResume) {
    final ActivityRecord topTaskActivity =
    mStartActivity.getTask().topRunningActivityLocked();
    if (!mTargetStack.isFocusable()
    || (topTaskActivity != null && topTaskActivity.mTaskOverlay
    && mStartActivity != topTaskActivity)) {
    mTargetStack.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
    mWindowManager.executeAppTransition();
    } else {
    if (mTargetStack.isFocusable() && !mSupervisor.isFocusedStack(mTargetStack)) {
    mTargetStack.moveToFront("startActivityUnchecked");
    }
    mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity,
    mOptions);————(2
    }
    } else {
    mTargetStack.addRecentActivityLocked(mStartActivity);
    }
    .............
    return START_SUCCESS;
    }

    (1)处通过setTaskFromReuseOrCreateNewTask()方法,创建TaskRecorder,这样一来,ProcessRecorder、ActivityRecorder以及TaskRecorder都齐全了,
    (2)处调用resumeFocusedStackTopActivityLocked方法


        boolean resumeFocusedStackTopActivityLocked(
    ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) {
    if (targetStack != null && isFocusedStack(targetStack)) {
    return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
    }
    final ActivityRecord r = mFocusedStack.topRunningActivityLocked();
    if (r == null || r.state != RESUMED) {
    mFocusedStack.resumeTopActivityUncheckedLocked(null, null);
    } else if (r.state == RESUMED) {
    mFocusedStack.executeAppTransition(targetOptions);
    }
    return false;
    }
    boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) {
    if (mStackSupervisor.inResumeTopActivity) {
    return false;
    }
    boolean result = false;
    try {
    mStackSupervisor.inResumeTopActivity = true;
    result = resumeTopActivityInnerLocked(prev, options);
    } finally {
    mStackSupervisor.inResumeTopActivity = false;
    }
    mStackSupervisor.checkReadyForSleepLocked();
    return result;
    }

    这里因为我们启动的是根Activity,那么topActivity肯定是为没有在running状态的,走的resumeTopActivityUncheckedLocked方法,然后执行resumeTopActivityInnerLocked方法。


        private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
    ........
    ActivityStack lastStack = mStackSupervisor.getLastStack();
    if (next.app != null && next.app.thread != null) {
    final boolean lastActivityTranslucent = lastStack != null
    && (!lastStack.mFullscreen
    || (lastStack.mLastPausedActivity != null
    && !lastStack.mLastPausedActivity.fullscreen));
    ..........
    } else {
    ..........
    mStackSupervisor.startSpecificActivityLocked(next, true, true);
    }
    if (DEBUG_STACK) mStackSupervisor.validateTopActivitiesLocked();
    return true;
    }

    这里代码很多,最终执行的是ActivityStackSupervisor类中的startSpecificActivityLocked方法。


        void startSpecificActivityLocked(ActivityRecord r,
    boolean andResume, boolean checkConfig) {
    ProcessRecord app = mService.getProcessRecordLocked(r.processName,
    r.info.applicationInfo.uid, true);
    r.getStack().setLaunchTime(r);
    if (app != null && app.thread != null) {
    try {
    if ((r.info.flags&ActivityInfo.FLAG_MULTIPROCESS) == 0
    || !"android".equals(r.info.packageName)) {
    app.addPackage(r.info.packageName, r.info.applicationInfo.versionCode,
    mService.mProcessStats);
    }
    realStartActivityLocked(r, app, andResume, checkConfig);(1)
    return;
    } catch (RemoteException e) {
    }
    }
    mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
    "activity", r.intent.getComponent(), false, false, true);(2)
    }

    这里先获取启动进程ProcessRecord 然后调用realStartActivityLocked()方法;
    如果ProcessRecorder进程为null那么就通过AMS的startProcessLocked去执行Process.start创建。


        final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
    boolean andResume, boolean checkConfig) throws RemoteException {
    ..........
    app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
    System.identityHashCode(r), r.info,
    mergedConfiguration.getGlobalConfiguration(),
    mergedConfiguration.getOverrideConfiguration(), r.compat,
    r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
    r.persistentState, results, newIntents, !andResume,
    mService.isNextTransitionForward(), profilerInfo);
    ..........
    return true;
    }

    其中app.thread其实就是ProcessRecorder的IApplicationManager,也就是ActivityThread的内部类ApplicationThread。
    Activity启动过程其实就目标应用程序进程启动Activity的过程,这里的app就代表目标应用程序进程,那ApplicationThread继承了IApplicationThread.Stub就是目标应用程序与AMS进行binder通信的桥梁。
    最后通知ApplicationThread调用scheduleLaunchActivity去启动Activity;


    public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
    ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
    CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
    int procState, Bundle state, PersistableBundle persistentState,
    List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
    boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
    updateProcessState(procState, false);
    ActivityClientRecord r = new ActivityClientRecord();
    r.token = token;
    r.ident = ident;
    r.intent = intent;
    r.referrer = referrer;
    r.voiceInteractor = voiceInteractor;
    r.activityInfo = info;
    r.compatInfo = compatInfo;
    r.state = state;
    r.persistentState = persistentState;
    r.pendingResults = pendingResults;
    r.pendingIntents = pendingNewIntents;
    r.startsNotResumed = notResumed;
    r.isForward = isForward;
    r.profilerInfo = profilerInfo;
    r.overrideConfig = overrideConfig;
    updatePendingConfiguration(curConfig);
    sendMessage(H.LAUNCH_ACTIVITY, r);
    }

    这里就是设置一堆属性,然后通过Activity的内部类H,其实就是handler类,发送LAUNCH_ACTIVITY消息,去执行


    case LAUNCH_ACTIVITY: {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
    r.packageInfo = getPackageInfoNoCheck(
    r.activityInfo.applicationInfo, r.compatInfo);
    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    } break;
    ----------------handleLaunchActivity------------------
    Activity a = performLaunchActivity(r, customIntent);

    然后调用handleLaunchActivity,然后继续调用performLaunchActivity,要执行一个Activity首先需要两个必备因素,一个是Context上下文,一个是Application。


    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    .......................
    ContextImpl appContext = createBaseContextForActivity(r);1
    Activity activity = null;
    try {
    java.lang.ClassLoader cl = appContext.getClassLoader();
    activity = mInstrumentation.newActivity(
    cl, component.getClassName(), r.intent);2
    .......................
    } catch (Exception e) {
    .......................
    }
    try {
    Application app = r.packageInfo.makeApplication(false, mInstrumentation);3
    if (activity != null) {
    .......................
    appContext.setOuterContext(activity);
    activity.attach(appContext, this, getInstrumentation(), r.token,
    r.ident, app, r.intent, r.activityInfo, title, r.parent,
    r.embeddedID, r.lastNonConfigurationInstances, config,
    r.referrer, r.voiceInteractor, window, r.configCallback);4
    .......................
    if (r.isPersistable()) {
    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);5
    } else {
    mInstrumentation.callActivityOnCreate(activity, r.state);
    }
    .......................
    return activity;
    }

    (1)处就是通过new ContextImpl的方式,创建Actviity的上下文。
    (2)处就是通过反射的方式,创建Actviity类。
    (3)处通过反射的方式,创建Application类,并把ContextImpl(这个上下文跟Activity的不一样,上面那个多了token和classloader)上下文attach到父类ContextWrapper中去,也就是mBase。
    (4)有了ContextImpl和Application,然后将Actviity做attach操作,就是将ContextImpl给父类ContextWrapper中的mBase,同时创建PhoneWindow。
    (5)处就是通过Instrumentation去调用oncreate方法
    罗里吧嗦讲那么多,先去上个厕所,回来用一张图总结一下:


    在这里插入图片描述



    ————————————————
    版权声明:本文为CSDN博主「蒋八九」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/massonJ/article/details/117914286


    收起阅读 »

    如何实现跨设备的双向连接? Labo涂鸦鸿蒙亲子版分布式开发技术分享

    近期,首届HarmonyOS开发者创新大赛正式落下帷幕。大赛共历时5个月,超过3000支队伍的10000多名选手参赛,25000多位开发者参与了大赛学习,最终23支参赛队伍斩获奖项,产出了多款有创新、有创意、有价值的优秀作品。其中由“Labo Lado儿童艺术...
    继续阅读 »

    近期,首届HarmonyOS开发者创新大赛正式落下帷幕。大赛共历时5个月,超过3000支队伍的10000多名选手参赛,25000多位开发者参与了大赛学习,最终23支参赛队伍斩获奖项,产出了多款有创新、有创意、有价值的优秀作品。其中由“Labo Lado儿童艺术创想”团队打造的《Labo涂鸦鸿蒙亲子版》就是其中之一,其创造性地通过HarmonyOS分布式技术,实现了多设备下的亲子互动涂鸦功能,最终摘得大赛一等奖。

    在很早以前,“Labo Lado儿童艺术创想”团队就做过一款涂鸦游戏的应用,该应用可以让孩子和父母在一个平板或者手机上进行绘画比赛,比赛的方式就是屏幕一分为二,两人各在设备的一边进行涂鸦。这种方式虽然有趣,但是对于绘画而言,屏幕尺寸限制了用户的发挥和操作。因此团队希望这类玩法能通过多个设备完成,于是他们研究了ZeroConf、iOS的Multipeer Connectivity、Google Nearby等近距离互联的技术, 结果发现这些技术在设备发现和应用拉起方面实现的都不理想,尤其是当目标用户是儿童的情况下,操作起来不够简便也不易上手。

    HarmonyOS的出现给团队带来了希望。他们发现HarmonyOS的分布式技术有着很大的应用潜力,这项技术让设备的发现和应用拉起变的非常的简单自然,互联的过程也很流畅,很好地解决了单机操作的限制,让跨设备联机功能能够非常容易地实现。此外,HarmonyOS的开发也给团队留下了很深刻的印象,以往繁琐的开发步骤,在 HarmonyOS 中仅需几个配置、几行代码即可完成,无需花费太多精力。在《Labo涂鸦鸿蒙亲子版》里面的5个分布式玩法的开发只用了团队一名开发者不到两个月的时间,其中还包括了学习上手、解决文档不全和各种疑难问题的过程。

    以下是“Labo Lado儿童艺术创想”团队基于HarmonyOS的分布式开发关键技术的简单分享:

    一、分布式技术实践

    HarmonyOS的分布式能力是在系统层面实现的,在设备双方同属一个局域网的情况下,设备都可以快速的发现和进行流畅的通讯。下面将从HarmonyOS设备的发现、应用的拉起、应用通讯和双向通讯几个部分来进行分享。

    1、设备的发现

    假设设备A想要邀请另外一个设备B加入,AB任何一方都无需启动特别的广播服务,只要发起方设备A在应用内调用设备发现代码,就可以列出附近符合条件可用的的设备。

    以下是获取设备列表的示例代码:

    public static List<DeviceInfo> getRemoteDevice() {

    List<DeviceInfo> deviceInfoList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);

    return deviceInfoList;

    }

    列出设备之后,用户就可以通过设备名选择想要邀请的设备了。

    (左侧设备A发现右侧名为“ye”的设备B的界面展示)

    2、应用的拉起

    设备A邀请了设备B之后,如果设备B上应用没启动,设备A可直接通过调用startAbility方法来拉起设备B上的应用。双方应用都启动了之后,就可以进行RPC通讯了。如果需要事先检查设备B上的应用是否已经启动或者是否在后台,可通过在应用中增加一个PA来实现。在拉起之前,设备A先连接设备B的应用中的PA可以实现更复杂精准的远程应用启动控制。

    3、应用通讯

    在应用中启动一个PA,专门用作通讯的服务器端。当设备B的应用被拉起之后,设备A就会通过connectAbility与设备B的PA进行连接,通讯采用RPC方式实现,并使用IDL定义通讯接口。

    4、双向通讯

    RPC的通讯方式使用简单,但是只能支持单向通讯。为了实现双向通讯,可在设备A与设备B发起建立连接成功之后,再让设备B与设备A发起建立一个连接,用两个连接实现了双向通讯。下面是这两个连接建立过程的示意时序图:

    在设备A与设备B建立连接的时候,设备A必须将自己的DeviceId发送给设备B,然后设备B才可以主动发起一个与设备A的连接,获取当前设备的DeviceId方法如下:

    KvManagerFactory.getInstance().createKvManager(new KvManagerConfig(this)).getLocalDeviceInfo().getId()

    应用中,FA主要实现了界面层逻辑,PA部分用做数据通讯的服务端。为了防止拉起应用导致用户当前面的操作被中断,可通过PA来查询当前FA的状态,如果FA已经启动了,就跳过拉起,直接进行下一步操作即可。

    二、数据接口与数据结构定义

    使用了IDL定义了两个通用的接口,分别用来进行异步和同步调用:

    int sendSyncCommand([in] int command, [in] String params);

    void sendAsyncCommand([in] int command, [in] String params, [in] byte[] content);

    大部分情况下,远程调用大部分都通过同步的方式进行,用户之间的绘画数据通过异步接口传输,数据在用户绘制的时候采集,每50ms左右发送一次,这个频率可以大概保证用户视觉上没有卡顿,而又不至于因为接口过度调用导致卡顿或者耗电量过大。采集的绘画数据的数据结构大致如下:

    enum action //动作,表示落笔、移动、提笔等动作

    int tagId //多点触摸识别标记

    int x //x坐标

    int y //y坐标

    enum brushType //笔刷类型

    int brushSize //笔刷大小

    enum brushColor //笔刷颜色

    int layer //图层

    这款应用是支持多点触摸的,所以每个触摸点在落笔的的时候,都使用了tagId进行标记。这些数据除了通讯外,还会完整地保存在文件中,这样用户就可以通过应用内的播放功能播放该数据,回看绘画的整个过程。

    三、教程录制与曲线平滑

    1、教程制作

    这款产品的特色之一是教程是动态的,用户可以自己拼装或者通过游戏生成教程角色。目前应用内置六种教程。这些教程预先由设计师在photoshop中画好并标记各个部位,然后再通过专门的photoshop脚本导出到教程录制应用中,再由设计师按部位逐个进行临摹绘制,绘制完成,应用会将设计师的绘制过程数据保存为json文件,通过将这些json的文件里的部位互换,我们就实现了用户自己拼装教程的功能了。

    2、曲线平滑

    绘制过程,为了让用户绘制的曲线更加平滑,采用二次贝塞尔曲线算法进行差值(Quadratic Bezier Curve),该算法简单效率也非常不错:

    public Point quadraticBezier(Point p0, Point p1, Point p2, float t) {

    Point pFinal = new Point();

    pFinal.x = (float) (Math.pow(1 - t, 2) * p0.x + (1 - t) * 2 * t * p1.x + t * t * p2.x);

    pFinal.y = (float) (Math.pow(1 - t, 2) * p0.y + (1 - t) * 2 * t * p1.y + t * t * p2.y);

    return pFinal;

    }

    基于HarmonyOS的分布式特性,《Labo涂鸦鸿蒙亲子版》完成了一次已有应用的自我尝试和突破,大大的增加了用户在使用过程中的乐趣,为用户带来了全新的跨设备亲子交互体验,“Labo Lado儿童艺术创想”团队在未来将与更多的HarmonyOS开发者一起,为用户创作出更多更有趣的儿童创造类应用。

    近一段时间以来,HarmonyOS 2的发布吸引了广大开发者的关注。作为一款面向万物互联时代的智能终端操作系统,HarmonyOS 2带来了诸多新特性、新功能和新玩法,等待开发者去探索、去学习、去实践。也欢迎广大开发者继续发挥创造力和想象力,基于HarmonyOS开发出更多有创新、有创意、有价值的作品,打造出专属于万物互联时代的创新产品。

    收起阅读 »

    【面试官爸爸】来给我讲讲View绘制?

    前言 迎面走来的一位中年男子,他一手拿着保温杯,一手抱着笔记本电脑,顶着惺忪的睡眼,不紧不慢地走着,不多的几根头发在他头顶自由飞翔。过了一会,他面对着我坐下,放下电脑和保温杯,边揉眉头边对我说 “来面试的?” “对对对” 我赶紧答应 ...
    继续阅读 »

    前言


    迎面走来的一位中年男子,他一手拿着保温杯,一手抱着笔记本电脑,顶着惺忪的睡眼,不紧不慢地走着,不多的几根头发在他头顶自由飞翔。过了一会,他面对着我坐下,放下电脑和保温杯,边揉眉头边对我说



    “来面试的?”




    “对对对” 我赶紧答应




    “行吧,那你讲讲 View 的绘制流程吧”




    起一个好头


    View 的绘制流程应该是每个初高级 Android 攻城狮必知必会的东西,也是面试必考的内容,每个人都有不同的回答方式。


    简单点譬如 measure,layout,draw 分别对应测量,布局,绘制三个过程,高明一点的会引申出 Handler,同步屏障,View 的事件传递,甚至 activity 的启动过程。掌握哪些东西,如何回答,能够给面试官一种清晰,了然于胸的感觉,同时又不会被追问三连一问三不知。各位老爷听我慢慢道来。



    “噢噢,View 的绘制啊。这个可以分为顶级 View 的绘制,Viewgroup 的绘制和 View 的绘制三个方面。顶级 View 就是 ViewrootImpl”



    将回答的内容分类是体现自己思考能力和知识结构的重要表现。


    什么是 ViewRootImpl


    相比 Viewgroup 和 View,ViewRootImpl 可能更为陌生,实际开发中我们基本用不到它。那么



    什么是 ViewRootImpl 呢?



    从结构上来看,ViewRootImpl 和 ViewGroup 其实是一种东西


    图 9


    它们都继承了 ViewParent。ViewParent 是一个接口,定义了一些父 View 的基本行为,比如 requestlayout,getparent 等。不同的是,ViewRootImpl 并不会像 ViewGroup 一样被真正绘制在屏幕上。在 activity 中,它是专门用来绘制 DecorView 的,核心方法是 setView


    回答的好好的偏要问我其他问题


    提到 DecorView,就不得不说一下 window 了。面试中常常我们提到一个点,或者一个词,面试官会马上引申出这个知识点相关的问题。如果我们只是死记硬背,自顾自背一堆绘制相关的东西而回答不上来,会大大减分。所以储备与必问内容相关的东西对面试和自己的知识体系很有帮助。不少老爷被面试的时候都会被问到一个问题



    “activity,window,View 三者之间的关系是什么?”



    我们可以通过一张图来说明。


    图 1


    如图所示,window 是 activity 里的一个实例变量,本质是一个接口,唯一的实现类是 PhoneWindow。


    activity 的 setContentView 方法实际上是就是交给 phonewindow 去做的。window 和 View 的关系可以类比为显示器显示的内容


    每个 activity 都有一个“显示器” window,“显示的内容”就是 DecorView。这个“显示器”定义了一些方法来决定如何显示内容。比如 setTitleColor setTitle 是设置导航栏的颜色和 title , setAllowReturnTransitionOverlap 设置进/出场动画等等。


    所以 window 是 activity 的一个成员变量,window 和 View 是“显示器”和“显示内容”的关系。


    这就是他们的关系


    View 是怎么绘制的



    “呦呵,不错嘛,这个比喻不错,看来平时还挺爱思考的。行,你继续说说 View 是怎么绘制的”



    在整个 activity 的生命周期中,setContentView 是在 onCreate 中调用的,它实现了对资源文件的解析,完成了 xml 文件到 View 的转化。那么 View 真正开始绘制是在哪个生命周期呢?



    答案是 onResume 结束后



    他们的关系在源码中一目了然。


    图 4


    从源码中可以看到,onResume 之后,ActivityThread 通过调用 activity 中 windowmanager 的 addView 方法,将 decorView 传入到 ViewRootImpl 的 setView 方法中,通过 setView 来完成 View 的绘制。


    问题又来了,setView 到底有什么魔法,为什么他就能完成 View 的绘制工作呢?


    ViewRootImpl 是如何绘制 View 的


    我们再来看一下 setView 方法


    图 5


    简单来说 setView 做了三件事


    ① 检查绘制的线程是不是创建 View 的线程。这里可以引申出一个问题,View 的绘制必须在主线程吗?


    ② 通过内存屏障保证绘制 View 的任务是最优先的


    ③ 调用 performTraversals 完成 measure,layout,draw 的绘制


    看到这里,ViewRootImpl 的绘制基本就完成了。其实这也是面试官希望听到的内容。考察的是面试者对 View 绘制体系的理解。


    后续 ViewGroup 和 View 的绘制其实是 performTraversals 对整个 ViewTree 的绘制。他们的关系可以用下面这张图表示


    图 2


    考考你对知识的运用



    “不错不错,看来你对 Viewrootimpl 的绘制过程掌握的不错嘛,你刚才提到 View 的绘制是在 onResume 之后才开始的,那为什么我在 onCreate 中调用 View.post 方法可以得到 View 的宽高呢”



    这个问题乍看挺唬人的。其实看一眼源码大概就明白了


    图 6


    View.post 会判断当前 View 是否已经被添加到 window 上。如果添加了则立即执行 runnable,如果没有被添加则先放到一个队列中存储起来,等添加到 window 上时再执行。


    而 View 被测量完成后才会 attachToWindow。所以当 post 的 runnable 执行时,View 已经绘制完成了。


    MeasureSpec 的理解



    “可以可以。看来这个小细节你注意到了。再问你个简单的问题,你刚才说到 measure 方法吧,那你说说什么是 MeasureSpec?为什么测量宽高要用它作为参数呢?”



    这个问题看似很简单死板,其实是想考察对 View 测量的理解。


    View 的大小不仅仅取决于自身的宽高,还取决于父 View 的大小和测量模式。一个 200200 的父 View 是不可能容纳一个 300300 的子 View 的,父 View 的 wrap_content 和 match_content 也会影响子 View 的大小。


    所以 View 的 measure 函数其实应该有 4 个参数:父 View 的宽父 View 的高宽的测量模式高的测量模式


    Android 这里用了一个巧妙的设计,用一个 Int 值来表示宽/高的测量模式和大小。一个 int 有 32 位,前 2 位表示测量 MODE,后 30 位表示 SIZE。


    为什么要用 2 位表示 MODE 呢?因为 MODE 只有 3 种呀,UNSPECIFIED,EXACTLY,AT_MOST,小傻瓜。




    “不错啊小伙子,那我自定义一个 View 的时候,如果不对 MeasureSpec 做处理。使用这个 View 时宽高传入 wrap_content,结果会怎么样?”



    这个考察的就是 View 绘制的实际运用了。当我们自定义一个 View 时,如果继承的是 View,measure 方法走的就是 View 默认的逻辑


    图 7


    所以当我们自定义 View 时,如果没有对 MODE 做处理,设置 wrap_content 和 match_content 结果其实是一样的,View 的宽高都是取父 View 的宽高。


    再来点细节



    “呦呵,那你说说 invaliate 和 requestlayout 方法的区别”



    前面我们说到,ViewRootImpl 作为顶级 View 负责 View 的绘制。所以简单来说,requestlayout 和 invaliate 最终都会向上回溯调用到 ViewRootImpl 的 postTranversals 方法来绘制 View。


    不同的是 requestlayout 会绘制 View 的 measure,layout 和 draw 过程。invaliate 因为只添加了绘制 draw 的标志位,只会绘制 draw 过程。


    这也能考算法



    “可以可以,看来 View 绘制这块你理解的不错嘛。来考你个小算法,实现一下 findViewbyid 的过程”



    一般对开发而言,算法的考察都不会太深,主要是常见算法的简单使用。目的是对业务中遇到的一些问题有更好的解决思路。像这个问题其实是想考察一下递归算法的简单使用。


    图 8



    “小伙子准备的不错嘛,好了,View 绘制这块我没有什么问题了,我们来聊聊 View 事件处理吧....”



    View 绘制相关的问题到这里就结束啦。如果大家觉得还不错的话,欢迎各位点赞,收藏,关注三连~


    后续我还会继续更新【面试官爸爸】这个系列,包括事件处理HandlerActivity 启动流程编译打包优化Context 等面试最常问的问题。如果不想错过,欢迎点赞,收藏,关注我!


    也可以关注我的公众号 @方木Rudy 里面不仅有技术,还有故事和感悟。你的支持,是我不断创作的动力!


    哦对了,是不是看完一遍觉得不够爽?杂七杂八说一大堆复习的时候一点也不轻松! 嘿嘿,我把上面提到的所有问题整理成了思维导图,方便各位观众老爷复习 ~


    图 1


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

    Android平台debug完全解析

    一:Java程序调试原理:java这种上层语言编译结果是字节码,字节码需要jvm解释执行,那么调试java具体就是和jvm通信的问题,一般IDE中对Java程序的调试功能都是对jdb的包装,关于jvm调试体系网上有很多文章,比如:juejin.cn/post/...
    继续阅读 »

    一:Java程序调试原理:

    java这种上层语言编译结果是字节码,字节码需要jvm解释执行,那么调试java具体就是和jvm通信的问题,一般IDE中对Java程序的调试功能都是对jdb的包装,关于jvm调试体系网上有很多文章,比如:juejin.cn/post/688739… ,不赘述了

    二:Native 程序调试原理:

    native代码包含的是对应平台的cpu指令,是直接cpu跑的,对native代码调试需要cpu的支持(比如int3软中断指令),以及操作系统的协助(比如Linux的ptrace系统调用),lldb,gdb,IDA的android_server等调试器都是基于上面的功能实现的,具体网上有资料,比如:zhuanlan.zhihu.com/p/336922639 ,不赘述了

    三:Class,Dex,Elf三种文件指令和源码对应关系描述结构:

    1:class字节码和源码行号对应关系描述结构:

    image.png

    2:dex字节码和源码行号对应关系描述结构:

    image.png

    3:elf指令和源码行号对应关系描述结构:

    image.png

    4:小结

    当class没有了行号,那只能反编译调试class指令

    当dex没有了行号,那只能反编译调试smali指令

    当elf没有了行号,那只能反汇编调试汇编指令

    四:调试Android Studio

    AS本质上是个Java程序,调试AS就是调试个Java程序

    1:配置AS以Debug模式启动

    dmg安装包安装后,可执行程序路径: /Applications/Android Studio.app/Contents/MacOS/studio 由于这是个mac下的可执行文件,此程序内部又启动java程序,并传入andorid studio相关的jar路径和参数,直接通过这个没法传递java参数,不过AS提供了个VM配置文件,启动时候会读取此文件的内容加入到java参数中 VM配置文件路径: Applications/Android Studio.app/Contents/bin/studio.vmoptions 加上:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=6006 这里为方便观察,直接双击/Applications/Android Studio.app/Contents/MacOS/studio 程序启动AS,可观察到终端中输出: image.png

    说明jvm已经准备好被调试器附加了

    2:调试配置:

    实现了JDWP协议的程序都可以作为调试器来用,当然没道理自己搞一个,用jdb则命令行操作太繁琐,手动管理源码也很费劲,不如使用包装完善的IDE,这里使用Idea,新建一个Remote JVM Debug 类型的configuration。配置如下图: image.png

    3:导入代码

    源码从哪来,你可以去下载下来,甚至可以自己编译个AS,参考:tools.android.com/build/studi… 但我们大多数时候只是想调试下,不想这么麻烦,可以直接导入AS的jar包到Idea中,利用Idea的反编译和Debug功能完成我们的目标,(AS的程序包的各个目录中有很多jar,需要哪个导入哪个,如下图):

    image.png

    导入Idea方式:Idea中新建个java项目,随便建个文件夹,把需要的jar复制过去,在jar上右键->Add as Library

    image.png image.png

    4:开始调试

    首先要找到要调试的功能所涉及的类或者方法,寻找的方式可以通过在jar中搜索字符串,或者尝试在感觉相关名称的Class中断点,在 IDEA Plugin 框架体系中,大多数插件的功能入口都依赖 Action,那就可以在Action的一些方法中断点 image.png

    五:调试Gradle

    Gradle本质上是个Java程序,调试Gradle就是调试个Java程序

    1:配置Gradle以Debug模式启动

    gradle.properties中添加 org.gradle.jvmargs=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005

    启动gradle,比如执行下assemble task,执行后下图所示,由于上面设置的suspend=y,启动后会等待调试器链接后再继续运行

    image.png

    2:调试配置

    这里都使用Idea调试,配置和AS调试一样:

    image.png

    3:导入代码

    源码从哪来,你可以去下载下来,甚至可以自己编译个Gradle,参考:github.com/gradle/grad… 但我们大多数时候只是想调试下,不想这么麻烦,可以直接导入Gradle的jar包到Idea中,利用Idea的反编译和Debug功能完成我们的目标,Gradle以及Gradle plugin的jar位置,如下图):

    gradle程序位置

    image.png

    图中lib目录是编译后的jar,如果现在的是gradle-{version}-all类型的,则src目录中会对应的源码,可以导入源码调试

    gradle plugin位置

    在下图所示的文件夹中搜索目标插件:

    image.png

    比如我要搜索android gradle plugin:

    image.png

    导入Idea方式:Idea中新建个java项目,随便建个文件夹,把需要的jar复制过去,在jar上右键->Add as Library,和AS一样,不赘述了

    4:开始调试

    首先要找到要调试的功能所涉及的类或者方法,寻找的方式不多说了,自己摸索着来吧

    image.png

    六:调试任意App Java层

    自己编个aosp刷机,类型选择userDebug或者Eng,这样的系统有root权限且全局可以调试,如何编译网上很多资料,中间若遇到问题也可以参考我写的aosp编译的坑点,不赘述

    如果是第三方App,需要反编译dex为java源码导入AS调试,如果行号对不上老是调飞,说明行号信息被混淆了或去掉了,这时候可以考虑反编译成smali,使用AS+smalidea插件调试smali代码,网上有很多资料。比如:blog.csdn.net/YJJYXM/arti… 如果遇到AS无法对smali类型的文件下断点,就参考 blog.csdn.net/qq_43278826…

    七:调试任意App Native层

    自己编个aosp刷机,类型选择userDebug或者Eng,这样的系统有root权限且全局可以调试,如何编译网上很多资料,中间若遇到问题也可以参考我写的aosp编译的坑点,不赘述

    由于第三方app中的so都是去除debug信息的,以及我们并没有对应源码,所以只能反汇编调试,我一般都是习惯使用IDA,网上有很多资料,比如: blog.csdn.net/Breeze_CAT/… IDA调试时候注意下这个坑: bbs.pediy.com/thread-2654…

    八:调试Android系统Java层

    自己编个aosp刷机,类型选择userDebug或者Eng,这样的系统有root权限且全局可以调试,如何编译网上很多资料,中间若遇到问题也可以参考我写的aosp编译的坑点,不赘述

    导入编译的代码到AS中(可以参考http://www.jianshu.com/p/2ba5d6bd4… ),或者也可以按需要把android sdk中的源码替换为编译系统用的源码(我就是这样),注意targetSdk版本要和编译的系统版本一致

    九:调试Android系统Native层

    自己编个aosp刷机,类型选择userDebug或者Eng,这样的系统有root权限且全局可以调试,如何编译网上很多资料,中间若遇到问题也可以参考我写的aosp编译的坑点,不赘述

    收起阅读 »

    Android常见图形绘制方式

    图形绘制概述Android平台提供丰富的官方控件给开发者实现界面UI开发,但在实际业务中经常会遇到各种各样的定制化需求,这必须由开发者通过自绘控件的方式来实现。通常Android提供了Canvas和OpenGL ES两种方式来实现,其中Canvas借助于And...
    继续阅读 »

    图形绘制概述

    Android平台提供丰富的官方控件给开发者实现界面UI开发,但在实际业务中经常会遇到各种各样的定制化需求,这必须由开发者通过自绘控件的方式来实现。通常Android提供了Canvas和OpenGL ES两种方式来实现,其中Canvas借助于Android底层的Skia 2D向量图形处理函数库来实现的。具体如何通过Canvas和OpenGL来绘制图形呢?这必须依赖于Android提供的View类来具体实现,下面组合几种常见的应用方式,如下所示:

    Canvas

    • View + Canvas
    • SurfaceView + Canvas
    • TextureView + Canvas

    OpenGL ES

    • SurfaceView + OpenGL ES
    • GLSurfaceView + OpenGL ES
    • TextureView + OpenGL ES

    View + Canvas

    这是一种通常使用的自绘控件方式,通过重写View类的onDraw(Canvas canvas)方法实现。当需要刷新绘制图形时,调用invalidate()方法让View对象自身进行刷新。该方案比较简单,涉及自定义逻辑较少,缺点是绘制逻辑在UI线程中进行,刷新效率不高,且不支持3D渲染。

    public class CustomView extends View {
    public CustomView(Context context) {
    super(context);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    // draw whatever.
    }
    }
    复制代码

    SurfaceView + Canvas

    这种方式相对于View + Canvas方式在于使用SurfaceView,因此会在Android的WMS系统上创建一块自己的Surface进行渲染绘制,其绘制逻辑可以在独立的线程中进行,因此性能相对于View + Canvas方式更高效。但通常情况下需要创建一个绘制线程,以及实现SurfaceHolder.Callback接口来管理SurfaceView的生命周期,其实现逻辑相比View + Canvas略复杂。另外它依然不支持3D渲染,且Surface因不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,SurfaceView 不能嵌套使用。

    public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    private boolean mRunning = false;
    private SurfaceHolder mSurfaceHolder;

    public CustomSurfaceView(Context context) {
    super(context);
    initView();
    }

    public CustomSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
    }

    public CustomSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView();
    }

    private void initView() {
    getHolder().addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    mSurfaceHolder = holder;
    new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    mSurfaceHolder = holder;
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    mRunning = false;
    }

    @Override
    public void run() {
    mRunning = true;
    while (mRunning) {
    SystemClock.sleep(333);
    Canvas canvas = mSurfaceHolder.lockCanvas();
    if (canvas != null) {
    try {
    synchronized (mSurfaceHolder) {
    onRender(canvas);
    }
    } finally {
    mSurfaceHolder.unlockCanvasAndPost(canvas);
    }
    }
    }
    }

    private void onRender(Canvas canvas) {
    // draw whatever.
    }
    }
    复制代码

    TextureView + Canvas

    该方式同SurfaceView + Canvas方式有些类似,但由于它是通过TextureView来实现的,所以可以摒弃Surface不在View hierachy中缺陷,TextureView不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。这种方式也有自身缺点,它必须在硬件加速的窗口中才能使用,占用内存比SurfaceView要高,在5.0以前在主UI线程渲染,5.0以后有单独的渲染线程。

    public class CustomTextureView extends TextureView implements TextureView.SurfaceTextureListener, Runnable {

    private boolean mRunning = false;
    private SurfaceTexture mSurfaceTexture;
    private Surface mSurface;
    private Rect mRect;

    public CustomTextureView(Context context) {
    super(context);
    initView();
    }

    public CustomTextureView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
    }

    public CustomTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView();
    }

    private void initView() {
    setSurfaceTextureListener(this);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    mSurfaceTexture = surface;
    mRect = new Rect(0, 0, width, height);
    mSurface = new Surface(mSurfaceTexture);
    new Thread(this).start();
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    mSurfaceTexture = surface;
    mRect = new Rect(0, 0, width, height);
    mSurface = new Surface(mSurfaceTexture);
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    mRunning = false;
    return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }

    @Override
    public void run() {
    mRunning = true;
    while (mRunning) {
    SystemClock.sleep(333);
    Canvas canvas = mSurface.lockCanvas(mRect);
    if (canvas != null) {
    try {
    synchronized (mSurface) {
    onRender(canvas);
    }
    } finally {
    mSurface.unlockCanvasAndPost(canvas);
    }
    }
    }
    }

    private void onRender(Canvas canvas) {
    canvas.drawColor(Color.RED);
    // draw whatever.
    }
    }
    复制代码

    以上都是2D图形渲染常见的方式,如果想要进行3D图形渲染或者是高级图像处理(比如滤镜、AR等效果),就必须得引入OpenGL ES来实现了。OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计,是一种图形渲染API的设计标准,不同的软硬件开发商在OpenGL API内部可能会有不同的实现方式。下面介绍一下在Android平台上,如何进行OpenGL ES渲染绘制,通常有以下三种方式:

    SurfaceView + OpenGL ES

    EGL是OpenGL API和原生窗口系统之间的接口,OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平台的差异。如果使用OpenGL API来绘制图形就必须先构建EGL环境。

    通常使用 EGL 渲染的一般步骤:

    - 获取 EGLDisplay对象,建立与本地窗口系统的连接调用eglGetDisplay方法得到EGLDisplay。

    - 初始化EGL方法,打开连接之后,调用eglInitialize方法初始化。

    - 获取EGLConfig对象,确定渲染表面的配置信息调用eglChooseConfig方法得到 EGLConfig。

    - 创建渲染表面EGLSurface通过EGLDisplay和EGLConfig,调用eglCreateWindowSurface或eglCreatePbufferSurface方法创建渲染表面得到EGLSurface。

    - 创建渲染上下文EGLContext通过EGLDisplay和EGLConfig,调用eglCreateContext方法创建渲染上下文,得到EGLContext。

    - 绑定上下文通过eglMakeCurrent 方法将 EGLSurface、EGLContext、EGLDisplay 三者绑定,绑定成功之后OpenGLES环境就创建好了,接下来便可以进行渲染。

    - 交换缓冲OpenGLES 绘制结束后,使用eglSwapBuffers方法交换前后缓冲,将绘制内容显示到屏幕上,而屏幕外的渲染不需要调用此方法。

    - 释放EGL环境绘制结束后,不再需要使用EGL时,需要取消eglMakeCurrent的绑定,销毁 EGLDisplay、EGLSurface、EGLContext三个对象。

    以上EGL环境构建比较复杂,这里先不做过多解释,下面可以通过代码参考其具体实现:

    public class OpenGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    private boolean mRunning = false;
    private SurfaceHolder mSurfaceHolder;

    public OpenGLSurfaceView(Context context) {
    super(context);
    initView();
    }

    public OpenGLSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
    }

    public OpenGLSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView();
    }

    private void initView() {
    getHolder().addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    mSurfaceHolder = holder;
    new Thread(this).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    mSurfaceHolder = holder;
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    mRunning = false;
    }

    @Override
    public void run() {
    //创建一个EGL实例
    EGL10 egl = (EGL10) EGLContext.getEGL();
    //
    EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
    //初始化EGLDisplay
    int[] version = new int[2];
    egl.eglInitialize(dpy, version);

    int[] configSpec = {
    EGL10.EGL_RED_SIZE, 5,
    EGL10.EGL_GREEN_SIZE, 6,
    EGL10.EGL_BLUE_SIZE, 5,
    EGL10.EGL_DEPTH_SIZE, 16,
    EGL10.EGL_NONE
    };

    EGLConfig[] configs = new EGLConfig[1];
    int[] num_config = new int[1];
    //选择config创建opengl运行环境
    egl.eglChooseConfig(dpy, configSpec, configs, 1, num_config);
    EGLConfig config = configs[0];

    EGLContext context = egl.eglCreateContext(dpy, config,
    EGL10.EGL_NO_CONTEXT, null);
    //创建新的surface
    EGLSurface surface = egl.eglCreateWindowSurface(dpy, config, mSurfaceHolder, null);
    //将opengles环境设置为当前
    egl.eglMakeCurrent(dpy, surface, surface, context);
    //获取当前opengles画布
    GL10 gl = (GL10)context.getGL();

    mRunning = true;
    while (mRunning) {
    SystemClock.sleep(333);
    synchronized (mSurfaceHolder) {
    onRender(gl);

    //显示绘制结果到屏幕上
    egl.eglSwapBuffers(dpy, surface);
    }
    }

    egl.eglMakeCurrent(dpy, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
    egl.eglDestroySurface(dpy, surface);
    egl.eglDestroyContext(dpy, context);
    egl.eglTerminate(dpy);
    }

    private void onRender(GL10 gl) {
    gl.glClearColor(1.0F, 0.0F, 0.0F, 1.0F);
    // Clears the screen and depth buffer.
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT
    | GL10.GL_DEPTH_BUFFER_BIT);
    }
    }
    复制代码

    从上面的代码可以看到,相对于SurfaceView + Canvas的绘制方式,主要有以下两点变化:

    • 在while(true)循环前后增加了EGL环境构造的代码
    • onRender()方法内参数用的是GL10而不是Canvas

    GLSurfaceView + OpenGL ES

    由于构建EGL环境比较繁琐,以及还需要健壮地维护一个线程,直接使用SurfaceView进行OpenGL绘制并不方便。幸好Android平台提供GLSurfaceView类,很好地封装了这些逻辑,使开发者能够快速地进行OpenGL的渲染开发。要使用GLSurfaceView类进行图形渲染,需要实现GLSurfaceView.Renderer接口,该接口提供一个onDrawFrame(GL10 gl)方法,在该方法内实现具体的渲染逻辑。

    public class OpenGLGLSurfaceView extends GLSurfaceView implements GLSurfaceView.Renderer {
    public OpenGLGLSurfaceView(Context context) {
    super(context);
    setRenderer(this);
    }

    public OpenGLGLSurfaceView(Context context, AttributeSet attrs) {
    super(context, attrs);
    setRenderer(this);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // pass through
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
    gl.glClearColor(1.0F, 0.0F, 0.0F, 1.0F);
    // Clears the screen and depth buffer.
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT
    | GL10.GL_DEPTH_BUFFER_BIT);
    }
    }
    复制代码

    TextureView + OpenGL ES

    该方式跟SurfaceView + OpenGL ES使用方法比较类似,使用该方法有个好处是它是通过TextureView来实现的,所以可以摒弃Surface不在View hierachy中缺陷,TextureView不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。这里使用TextureView类在构建EGL环境时需要注意,传入eglCreateWindowSurface()的参数是SurfaceTexture实例。

    public class OpenGLTextureView extends TextureView implements TextureView.SurfaceTextureListener, Runnable {
    private boolean mRunning = false;
    private SurfaceTexture mSurfaceTexture;

    public OpenGLTextureView(Context context) {
    super(context);
    initView();
    }

    public OpenGLTextureView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initView();
    }

    public OpenGLTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView();
    }

    private void initView() {
    setSurfaceTextureListener(this);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    mSurfaceTexture = surface;
    new Thread(this).start();
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    mSurfaceTexture = surface;
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    mRunning = false;
    return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }

    @Override
    public void run() {
    //创建一个EGL实例
    EGL10 egl = (EGL10) EGLContext.getEGL();
    //
    EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
    //初始化EGLDisplay
    int[] version = new int[2];
    egl.eglInitialize(dpy, version);

    int[] configSpec = {
    EGL10.EGL_RED_SIZE, 5,
    EGL10.EGL_GREEN_SIZE, 6,
    EGL10.EGL_BLUE_SIZE, 5,
    EGL10.EGL_DEPTH_SIZE, 16,
    EGL10.EGL_NONE
    };

    EGLConfig[] configs = new EGLConfig[1];
    int[] num_config = new int[1];
    //选择config创建opengl运行环境
    egl.eglChooseConfig(dpy, configSpec, configs, 1, num_config);
    EGLConfig config = configs[0];

    EGLContext context = egl.eglCreateContext(dpy, config,
    EGL10.EGL_NO_CONTEXT, null);
    //创建新的surface
    EGLSurface surface = egl.eglCreateWindowSurface(dpy, config, mSurfaceTexture, null);
    //将opengles环境设置为当前
    egl.eglMakeCurrent(dpy, surface, surface, context);
    //获取当前opengles画布
    GL10 gl = (GL10)context.getGL();

    mRunning = true;
    while (mRunning) {
    SystemClock.sleep(333);
    synchronized (mSurfaceTexture) {
    onRender(gl);

    //显示绘制结果到屏幕上
    egl.eglSwapBuffers(dpy, surface);
    }
    }

    egl.eglMakeCurrent(dpy, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
    egl.eglDestroySurface(dpy, surface);
    egl.eglDestroyContext(dpy, context);
    egl.eglTerminate(dpy);
    }

    private void onRender(GL10 gl) {
    gl.glClearColor(1.0F, 0.0F, 1.0F, 1.0F);
    // Clears the screen and depth buffer.
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT
    | GL10.GL_DEPTH_BUFFER_BIT);
    }
    }
    收起阅读 »

    使用Jetpack Compose完成自定义手势处理

    概述Jetpack Compose 为我们提供了许多手势处理 Modifier,对于常见业务需求来说已足够我们使用了,然而如果说我们对手势有定制需求,就需要具备自定义手势处理的能力了。通过使用官方所提供的基础 API 来完成各类手势交互需求,触摸反馈基础 AP...
    继续阅读 »

    概述

    Jetpack Compose 为我们提供了许多手势处理 Modifier,对于常见业务需求来说已足够我们使用了,然而如果说我们对手势有定制需求,就需要具备自定义手势处理的能力了。通过使用官方所提供的基础 API 来完成各类手势交互需求,触摸反馈基础 API 类似传统 View 系统的 onTouchEvent()。 当然 Compose 中也支持类似传统 ViewGroup 通过 onInterceptTouchEvent()定制手势事件分发流程。通过对自定义手势处理的学习将帮助大家掌握处理绝大多数场景下手势需求的能力。

    使用 PointerInput Modifier

    对于所有手势操作的处理都需要封装在这个 Modifier 中,我们知道 Modifier 是用来修饰 UI 组件的,所以将手势操作的处理封装在 Modifier 符合开发者设计直觉,这同时也做到了手势处理逻辑与 UI 视图的解耦,从而提高复用性。

    通过翻阅 Swipeable Modifier 、Draggable Modifier 以及 Transformer Modifier,我们都能看到 PointerInput Modifier 的身影。因为这类上层的手势处理 Modifier 其实都是基于这个基础 Modifier 实现的。所以既然要自定义手势处理流程,自定义逻辑也必然要在这个 Modifier 中进行实现。

    通过 PointerInput Modifier 实现我们可以看出,我们所定义的自定义手势处理流程均发生在 PointerInputScope 中,suspend 关键字也告知我们自定义手势处理流程是发生在协程中。这其实是无可厚非的,在探索重组工作原理的过程中我们也经常能够看到协程的身影。伴随着越来越多的主流开发技术拥抱协程,这也就意味着协程成了 Android 开发者未来必须掌握的技能。推广协程同时其实也是在推广 Kotlin,即使官方一直强调不会放弃 Java,然而谁又会在 Java 中使用 Kotlin 协程呢?

    fun Modifier.pointerInput(
    vararg keys: Any?,
    block: suspend PointerInputScope.() -> Unit
    ): Modifier = composed(
    ...
    ) {
    ...
    remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply {
    LaunchedEffect(this, *keys) {
    block()
    }
    }
    }

    接下来我们就看看 PointerInputScope 作用域中,为我们可以使用哪些 API 来处理手势交互。本文将会根据手势能力分类进行解释说明。

    拖动类型基础 API

    API 介绍

    API名称作用
    detectDragGestures监听拖动手势
    detectDragGesturesAfterLongPress监听长按后的拖动手势
    detectHorizontalDragGestures监听水平拖动手势
    detectVerticalDragGestures监听垂直拖动手势

    谈及拖动,许多人第一个反应就是 Draggable Modifier,因为 Draggable Modifier 为我们提供了监听 UI 组件拖动能力。然而 Draggable Modifier 在提供了监听 UI 组件拖动能力的同时也拓展增加其他功能,我们通过 Draggable Modifier 参数列表即可看出。例如通过使用 DraggableState 允许开发者根据需求使 UI 组件自动被拖动。

    fun Modifier.draggable(
    state: DraggableState,
    orientation: Orientation,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    startDragImmediately: Boolean = false,
    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
    reverseDirection: Boolean = false
    )

    我们上面所罗列的这些拖动 API 只提供了监听 UI 组件拖动的能力,我们可以根据需求为其拓展功能,这也是这些API所存在的意义。我们从字面上就可以看出每个 API 所对应的含义,由于这些API的功能与参数相近,这里我们仅以 detectDragGestures 作为举例说明。

    举例说明

    接下来我们将完成一个绿色方块的手势拖动。在 Draggabel Modifier 中我们还只能监听垂直或水平中某一个方向的手势拖动,而使用 detectDragGestures 所有手势信息都是可以拿到的。如果我们还是只希望拿到某一个方向的手势拖动,使用 detectHorizontalDragGestures 或 detectVerticalDragGestures 即可,当然我们也可以使用 detectDragGestures 并且忽略掉某个方向的手势信息。如果我们希望在长按后才能拿到手势信息可以使用 detectDragGesturesAfterLongPress

    detectDragGestures 提供了四个参数。

    onDragStart (可选):拖动开始时回调

    onDragEnd (可选):拖动结束时回调

    onDragCancel (可选):拖动取消时回调

    onDrag (必须):拖动时回调

    decectDragGestures 的源码分析在 awaitTouchSlopOrCancellation 小节会有讲解。

    suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
    )

    💡 Tips

    有些同学可能困惑 onDragCancel 触发时机。在一些场景中,当组件拖动时会根据事件分发顺序进行事件分发,当前面先处理事件的组件满足了设置的消费条件,导致手势事件被消费,导致本组件拿到的是被消费的手势事件,从而会执行 onDragCancel 回调。如何定制事件分发顺序并消费事件后续会进行详细的描述。

    示例如下所示

    @Preview
    @Composable
    fun DragGestureDemo() {
    var boxSize = 100.dp
    var offset by remember { mutableStateOf(Offset.Zero) }
    Box(contentAlignment = Alignment.Center,
    modifier = Modifier.fillMaxSize()
    ) {
    Box(Modifier
    .size(boxSize)
    .offset {
    IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
    }
    .background(Color.Green)
    .pointerInput(Unit) {
    detectDragGestures(
    onDragStart = { offset ->
    // 拖动开始
    },
    onDragEnd = {
    // 拖动结束
    },
    onDragCancel = {
    // 拖动取消
    },
    onDrag = { change: PointerInputChange, dragAmount: Offset ->
    // 拖动中
    offset += dragAmount
    }
    )
    }
    )
    }
    }

    drag.gif

    点击类型基础 API

    API 介绍

    API名称作用
    detectTapGestures监听点击手势

    与 Clickable Modifier 不同的是,detectTapGestures 可以监听更多的点击事件。作为手机监听的基础 API,必然不会存在 Clickable Modifier 所拓展的涟漪效果。

    举例说明

    接下来我们将为一个绿色方块添加点击手势处理逻辑。detectTapGestures 提供了四个可选参数,用来监听不同点击事件。

    onDoubleTap (可选):双击时回调

    onLongPress (可选):长按时回调

    onPress (可选):按下时回调

    onTap (可选):轻触时回调

    suspend fun PointerInputScope.detectTapGestures(
    onDoubleTap: ((Offset) -> Unit)? = null,
    onLongPress: ((Offset) -> Unit)? = null,
    onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
    onTap: ((Offset) -> Unit)? = null
    )

    💡 Tips

    onPress 普通按下事件

    onDoubleTap 前必定会先回调 2 次 Press

    onLongPress 前必定会先回调 1 次 Press(时间长)

    onTap 前必定会先回调 1 次 Press(时间短)

    示例如下所示

    @Preview
    @Composable
    fun TapGestureDemo() {
    var boxSize = 100.dp
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    Box(Modifier
    .size(boxSize)
    .background(Color.Green)
    .pointerInput(Unit) {
    detectTapGestures(
    onDoubleTap = { offset: Offset ->
    // 双击
    },
    onLongPress = { offset: Offset ->
    // 长按
    },
    onPress = { offset: Offset ->
    // 按下
    },
    onTap = { offset: Offset ->
    // 轻触
    }
    )
    }
    )
    }
    }

    变换类型基础 API

    API 介绍

    API名称作用
    detectTransformGestures监听拖动、缩放与旋转手势

    与 Transfomer Modifier 不同的是,通过这个 API 可以监听单指的拖动手势,和拖动类型基础 API所提供的功能一样,除此之外还支持监听双指缩放与旋转手势。反观Transfomer Modifier 只能监听到双指拖动手势,不知设计成这样的行为不一致是否是 Google 有意而为之。

    举例说明

    接下来我们为这个绿色方块添加变化手势处理逻辑。detectTransformGestures 方法提供了两个参数。

    panZoomLock(可选): 当拖动或缩放手势发生时是否支持旋转

    onGesture(必须):当拖动、缩放或旋转手势发生时回调

    suspend fun PointerInputScope.detectTransformGestures(
    panZoomLock: Boolean = false,
    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
    )

    💡 Tips

    关于偏移、缩放与旋转,我们建议的调用顺序是 rotate -> scale -> offset

    1. 若offset发生在rotate之前时,rotate会对offset造成影响。具体表现为当出现拖动手势时,组件会以当前角度为坐标轴进行偏移。

    2. 若offset发生在scale之前是,scale也会对offset造成影响。具体表现为UI组件在拖动时不跟手

    @Preview
    @Composable
    fun TransformGestureDemo() {
    var boxSize = 100.dp
    var offset by remember { mutableStateOf(Offset.Zero) }
    var ratationAngle by remember { mutableStateOf(0f) }
    var scale by remember { mutableStateOf(1f) }
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    Box(Modifier
    .size(boxSize)
    .rotate(ratationAngle) // 需要注意offset与rotate的调用先后顺序
    .scale(scale)
    .offset {
    IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
    }
    .background(Color.Green)
    .pointerInput(Unit) {
    detectTransformGestures(
    panZoomLock = true, // 平移或放大时是否可以旋转
    onGesture = { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
    offset += pan
    scale *= zoom
    ratationAngle += rotation
    }
    )
    }
    )
    }
    }

    forEachGesture

    在传统 View 系统中,一次手指按下、移动到抬起过程中的所有手势事件可以共同构成一个手势事件序列。我们可以通过自定义手势处理来对于每一个手势事件序列进行定制处理。Compose 提供了 forEachGesture 以允许用户可以对每一个手势事件序列进行相同的定制处理。如果我们忘记使用 forEachGesture ,那么只会处理第一次手势事件序列。有些同学可能会问,为什么我不能在手势处理逻辑最外层套一层 while(true) 呢,通过 forEachGesture 的实现我们可以看到 forEachGesture 其实内部也是由while 实现的,除此之外他保证了协程只有存活时才能监听手势事件,同时也保证了每次交互结束时所有手指都是离开屏幕的。有些同学看到 while 可能新生疑问,难道这样不会阻塞主线程嘛?其实我们在介绍 PointerInput Modifier 时就提到过,我们的手势操作处理均发生在协程中。其实前面我们所提到的绝大多数 API 其内部实现均使用了 forEachGesture 。有些特殊场景下我们仅使用前面所提出的 API 可能仍然无法满足我们的需求,当然如果可以满足的话我们直接使用其分别对应的 Modifier 即可,前面所提出的 API 存在的意义是为了方便开发者为其进行功能拓展。既然要掌握自定义手势处理,我们就要从更底层角度来看这些上层 API 是如何实现的,了解原理我们就可以轻松自定义了。

    suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {
    val currentContext = currentCoroutineContext()
    while (currentContext.isActive) {
    try {
    block()
    // 挂起等待所有手指抬起
    awaitAllPointersUp()
    } catch (e: CancellationException) {
    ...
    }
    }
    }

    手势事件作用域 awaitPointerEventScope

    在 PointerInputScope 中我们可以找到一个名为 awaitPointerEventScope 的 API 方法。

    通过翻阅方法声明可以发现这是个挂起方法,其尾部 lambda 在 AwaitPointerEventScope 作用域中。 通过这个 AwaitPointerEventScope 作用域我们可以获取到更加底层的 API 手势事件,这也为自定义手势处理提供了可能。

    suspend fun <R> awaitPointerEventScope(
    block: suspend AwaitPointerEventScope.() -> R
    ): R

    我们在 AwaitPointerEventScope 中发现了以下这些基础手势方法,可以发现这些 API 均是挂起函数,接下来我们会对每个 API 进行描述说明。

    API名称作用
    awaitPointerEvent手势事件
    awaitFirstDown第一根手指的按下事件
    drag拖动事件
    horizontalDrag水平拖动事件
    verticalDrag垂直拖动事件
    awaitDragOrCancellation单次拖动事件
    awaitHorizontalDragOrCancellation单次水平拖动事件
    awaitVerticalDragOrCancellation单次垂直拖动事件
    awaitTouchSlopOrCancellation有效拖动事件
    awaitHorizontalTouchSlopOrCancellation有效水平拖动事件
    awaitVerticalTouchSlopOrCancellation有效垂直拖动事件

    万物之源 awaitPointerEvent

    awaitPointerEvent 类似于传统 View 系统的 onTouchEvent() 。无论用户是按下、移动或抬起都将视作一次手势事件,当手势事件发生时 awaitPointerEvent 会恢复执行并将手势事件返回。

    suspend fun awaitPointerEvent(
    pass: PointerEventPass = PointerEventPass.Main
    ): PointerEvent

    通过 API 声明可以看到 awaitPointerEvent 有个可选参数 PointerEventPass

    我们知道手势事件的分发是由父组件到子组件的单链结构。这个参数目的是用以设置父组件与子组件的事件分发顺序,PointerEventPass 有 3 个枚举值可供选择,每个枚举值的具体含义如下

    枚举值含义
    PointerEventPass.Initial本组件优先处理手势,处理后交给子组件
    PointerEventPass.Main若子组件为Final,本组件优先处理手势。否则将手势交给子组件处理,结束后本组件再处理。
    PointerEventPass.Final若子组件也为Final,本组件优先处理手势。否则将手势交给子组件处理,结束后本组件再处理。

    大家可能觉得 Main 与 Final 是等价的。但其实两者在作为子组件时分发顺序会完全不同,举个例子。

    当父组件为Final,子组件为Main时,事件分发顺序: 子组件 -> 父组件

    当父组件为Final,子组件为Final时,事件分发顺序: 父组件 -> 子组件

    文字描述可能并不直观,接下来进行举例说明。

    事件分发流程

    接下来,我将通过一个嵌套了三层 Box 的示例来直观表现事件分发过程。我们为这嵌套的三层Box 中的每一层都进行手势获取。

    如果我们点击中间的绿色方块时,便会触发手势事件。

    当三层 Box 均使用默认 Main 模式时,事件分发顺序为:第三层 -> 第二层 -> 第一层

    当第一层Box使用 Inital 模式,第二层使用 Final 模式,第三层使用 Main 模式时,事件分发顺序为:第一层 -> 第三层 -> 第二层

    @Preview
    @Composable
    fun NestedBoxDemo() {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .fillMaxSize()
    .pointerInput(Unit) {
    awaitPointerEventScope {
    awaitPointerEvent(PointerEventPass.Initial)
    Log.d("compose_study", "first layer")
    }
    }
    ) {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .size(400.dp)
    .background(Color.Blue)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    awaitPointerEvent(PointerEventPass.Final)
    Log.d("compose_study", "second layer")
    }
    }
    ) {
    Box(
    Modifier
    .size(200.dp)
    .background(Color.Green)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    awaitPointerEvent()
    Log.d("compose_study", "third layer")
    }
    }
    )
    }
    }
    }

    // Output:
    // first layer
    // third layer
    // second layer

    能够自定义事件分发顺序之后,我们就可以决定手势事件由事件分发流程中哪个组件进行消费。那么如何进行消费呢,这就需要我们看看 awaitPointerEvent 返回的手势事件了。通过 awaintPointerEvent 声明,我们可以看到返回的手势事件是个 PointerEvent 实例。

    通过 PointerEvent 类声明,我们可以看到两个成员属性 changes 与 motionEvent。

    motionEvent 我们再熟悉不过了,就是传统 View 系统中的手势事件,然而却被声明了 internal 关键字,看来是不希望我们使用。

    changes 是一个 List,其中包含了每次发生手势事件时,屏幕上所有手指的状态信息。

    当只有一根手指时,这个 List 的大小为 1。在多指操作时,我们通过这个 List 获取其他手指的状态信息就可以轻松定制多指自定义手势处理了。

    actual data class PointerEvent internal constructor(
    actual val changes: List<PointerInputChange>,
    internal val motionEvent: MotionEvent?
    )

    PointerInputChange

    class PointerInputChange(
    val id: PointerId, // 手指Id
    val uptimeMillis: Long, // 当前手势事件的时间戳
    val position: Offset, // 当前手势事件相对组件左上角的位置
    val pressed: Boolean, // 当前手势是否按下
    val previousUptimeMillis: Long, // 上一次手势事件的时间戳
    val previousPosition: Offset, // 上一次手势事件相对组件左上角的位置
    val previousPressed: Boolean, // 上一次手势是否按下
    val consumed: ConsumedData, // 当前手势是否已被消费
    val type: PointerType = PointerType.Touch // 手势类型(鼠标、手指、手写笔、橡皮)
    )
    API名称作用
    changedToDown是否已经按下(按下手势已消费则返回false)
    changedToDownIgnoreConsumed是否已经按下(忽略按下手势已消费标记)
    changedToUp是否已经抬起(按下手势已消费则返回false)
    changedToUpIgnoreConsumed是否已经抬起(忽略按下手势已消费标记)
    positionChanged是否位置发生了改变(移动手势已消费则返回false)
    positionChangedIgnoreConsumed是否位置发生了改变(忽略已消费标记)
    positionChange位置改变量(移动手势已消费则返回Offset.Zero)
    positionChangeIgnoreConsumed位置改变量(忽略移动手势已消费标记)
    positionChangeConsumed当前移动手势是否已被消费
    anyChangeConsumed当前按下手势或移动手势是否有被消费
    consumeDownChange消费按下手势
    consumePositionChange消费移动手势
    consumeAllChanges消费按下与移动手势
    isOutOfBounds当前手势是否在固定范围内

    这些 API 会在我们自定义手势处理时会被用到。可以发现的是,Compose 通过 PointerEventPass 来定制事件分发流程,在事件分发流程中即使前一个组件先获取了手势信息并进行了消费,后面的组件仍然可以通过带有 IgnoreConsumed 系列 API 来获取到手势信息。这也极大增加了手势操作的可定制性。就好像父组件先把事件消费,希望子组件不要处理这个手势了,但子组件完全可以不用听从父组件的话。

    我们通过一个实例来看看该如何进行手势消费,处于方便我们的示例不涉及移动,只消费按下手势事件来进行举例。和之前的样式一样,我们将手势消费放在了第三层 Box,根据事件分发规则我们知道第三层Box是第2个处理手势事件的,所以输出结果如下。

    @Preview
    @Composable
    fun Demo() {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .fillMaxSize()
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent(PointerEventPass.Initial)
    Log.d("compose_study", "first layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    }
    ) {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .size(400.dp)
    .background(Color.Blue)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent(PointerEventPass.Final)
    Log.d("compose_study", "second layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    }
    ) {
    Box(
    Modifier
    .size(200.dp)
    .background(Color.Green)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent()
    event.changes[0].consumeDownChange()
    Log.d("compose_study", "third layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    }
    )
    }
    }
    }

    // Output:
    // first layer, downChange: false
    // third layer, downChange: true
    // second layer, downChange: true

    ⚠️ 注意事项

    如果我们是在定制事件分发流程,那么需要注意以下两种写法

    // 正确写法
    awaitPointerEventScope {
    var event = awaitPointerEvent()
    event.changes[0].consumeDownChange()
    }

    // 错误写法
    var event = awaitPointerEventScope {
    awaitPointerEvent()
    }
    event.changes[0].consumeDownChange()

    他们的区别在于 awaitPointerEventScope 会在其内部所有手势在事件分发流程结束后返回,当所有组件都已经完成手势处理再进行消费已经没有什么意义了。我们仍然用刚才的例子来直观说明这个问题。我们在每一层Box awaitPointerEventScope 后面添加了日志信息。

    通过输出结果可以发现,这三层执行的相对顺序没有发生变化,然而却是在事件分发流程结束后才进行输出的。

    @Preview
    @Composable
    fun Demo() {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .fillMaxSize()
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent(PointerEventPass.Initial)
    Log.d("compose_study", "first layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    Log.d("compose_study", "first layer Outside")
    }
    ) {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .size(400.dp)
    .background(Color.Blue)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent(PointerEventPass.Final)
    Log.d("compose_study", "second layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    Log.d("compose_study", "second layer Outside")
    }
    ) {
    Box(
    Modifier
    .size(200.dp)
    .background(Color.Green)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent()
    event.changes[0].consumeDownChange()
    Log.d("compose_study", "third layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    Log.d("compose_study", "third layer Outside")
    }
    )
    }
    }
    }

    // Output:
    // first layer, downChange: false
    // third layer, downChange: true
    // second layer, downChange: true
    // first layer Outside
    // third layer Outside
    // second layer Outside

    awaitFirstDown

    awaitFirstDown 将等待第一根手指按下事件时恢复执行,并将手指按下事件返回。分析源码我们可以发现 awaitFirstDown 也使用的是 awaitPointerEvent 实现的,默认使用 Main 模式。

    suspend fun AwaitPointerEventScope.awaitFirstDown(
    requireUnconsumed: Boolean = true
    ): PointerInputChange {
    var event: PointerEvent
    do {
    event = awaitPointerEvent()
    } while (
    !event.changes.fastAll {
    if (requireUnconsumed) it.changedToDown() else it.changedToDownIgnoreConsumed()
    }
    )
    return event.changes[0]
    }

    drag

    看到 drag 可能很多同学疑惑为什么又是拖动。其实前面所提到的拖动类型基础API detectDragGestures 其内部就是使用 drag 而实现的。与 detectDragGestures 不同的是,drag 需要主动传入一个 PointerId 用以表示要具体获取到哪根手指的拖动事件。

    suspend fun AwaitPointerEventScope.drag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
    )

    翻阅源码可以发现,其实 drag 内部实现最终使用的仍然还是 awaitPointerEvent 。这里就不具体展开看了,感兴趣的可以自己去跟源码。

    收起阅读 »

    Gradle实战2:微信Tinker项目中的tinker-patch-gradle-plugin模块解析

    引言上一篇,我们学习了《微信Tinker项目中的maven-publish封装》,了解到了在一个成熟项目中,maven相关gradle的通用封装,进而巩固前面学习的gradle相关理论知识接下来,我们将对Tinker项目中的tinker-patch-gradl...
    继续阅读 »

    引言

    上一篇,我们学习了《微信Tinker项目中的maven-publish封装》,了解到了在一个成熟项目中,maven相关gradle的通用封装,进而巩固前面学习的gradle相关理论知识

    接下来,我们将对Tinker项目中的tinker-patch-gradle-plugin模块进行解析,进一步感受Gradle在亿级应用中散发的魅力

    PS1:本章主要是跟踪《tinker-patch-gradle-plugin模块》实现,来巩固gradle相关知识,具体热修相关安卓知识的话不会展开

    PS2:由于tinker的官方工程比较大,对于巩固gradle知识干扰比较大,所以本章的代码工程是阉割了官方的代码展开,更加聚焦,同时实现博客和源码配套的模式

    简介

    tinker-patch-gradle-plugin模块,是开发者使用Thinker入口;

    如果我们app集成thinker,其实就是对这个模块的使用,因为thinker的实现

    都以插件的方式被封装到了这个模块,具体官方代码位置戳这里>>>

    上面说到,我们会对官方工程进行裁剪,裁剪后对应的模块位置戳这里>>>

    解析过程

    应用模块

    1.png 此图为我们的app模块引入thinker的步骤

    1)模块所在位置,其实就是我们app模块的gradle文件

    2)插件引入,通过classpath关键字引入封装好的thinker插件,其中插件的maven发布我们发布到了本地,所以用的时候我们maven指向了本地的 ‘../repo’

    3)插件的使用,通过apply引入,然后tinkerPatch,buildConfig都是插件的自定义拓展,具体实现在下面步骤讲解

    模块之工程定义

    2.png

    1)插件实现工程目录,可以看出这是一个gradle插件的标准目录,具体诠释见往期教程

    2)插件实现工程gradle文件,这里除了有自定义插件的依赖外,还用到了上一章讲解的maven-publish封装

    模块之自定义拓展

    上面有提到tinkerPatch,buildConfig关键字为自定义拓展,这两个关键字只是thinker

    中的拓展之一,我们来看下thinker的自定义拓展全貌:

     tinkerPatch {
    /**
    * necessary,default 'null'
    * the old apk path, use to diff with the new apk to build
    * add apk from the build/bakApk
    */

    oldApk = getOldApkPath()
    /**
    * optional,default 'false'
    * there are some cases we may get some warnings
    * if ignoreWarning is true, we would just assert the patch process
    * case 1: minSdkVersion is below 14, but you are using dexMode with raw.
    * it must be crash when load.
    * case 2: newly added Android Component in AndroidManifest.xml,
    * it must be crash when load.
    * case 3: loader classes in dex.loader{} are not keep in the main dex,
    * it must be let tinker not work.
    * case 4: loader classes in dex.loader{} changes,
    * loader classes is ues to load patch dex. it is useless to change them.
    * it won't crash, but these changes can't effect. you may ignore it
    * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
    */

    ignoreWarning = false

    /**
    * optional,default 'true'
    * whether sign the patch file
    * if not, you must do yourself. otherwise it can't check success during the patch loading
    * we will use the sign config with your build type
    */

    useSign = true

    /**
    * optional,default 'true'
    * whether use tinker to build
    */

    tinkerEnable = buildWithTinker()

    /**
    * Warning, applyMapping will affect the normal android build!
    */

    buildConfig {
    /**
    * optional,default 'null'
    * if we use tinkerPatch to build the patch apk, you'd better to apply the old
    * apk mapping file if minifyEnabled is enable!
    * Warning:
    * you must be careful that it will affect the normal assemble build!
    */

    applyMapping = getApplyMappingPath()
    /**
    * optional,default 'null'
    * It is nice to keep the resource id from R.txt file to reduce java changes
    */

    applyResourceMapping = getApplyResourceMappingPath()

    /**
    * necessary,default 'null'
    * because we don't want to check the base apk with md5 in the runtime(it is slow)
    * tinkerId is use to identify the unique base apk when the patch is tried to apply.
    * we can use git rev, svn rev or simply versionCode.
    * we will gen the tinkerId in your manifest automatic
    */

    tinkerId = getTinkerIdValue()

    /**
    * if keepDexApply is true, class in which dex refer to the old apk.
    * open this can reduce the dex diff file size.
    */

    keepDexApply = false

    /**
    * optional, default 'false'
    * Whether tinker should treat the base apk as the one being protected by app
    * protection tools.
    * If this attribute is true, the generated patch package will contain a
    * dex including all changed classes instead of any dexdiff patch-info files.
    */

    isProtectedApp = false

    /**
    * optional, default 'false'
    * Whether tinker should support component hotplug (add new component dynamically).
    * If this attribute is true, the component added in new apk will be available after
    * patch is successfully loaded. Otherwise an error would be announced when generating patch
    * on compile-time.
    *
    * <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
    */

    supportHotplugComponent = false
    }

    dex {
    /**
    * optional,default 'jar'
    * only can be 'raw' or 'jar'. for raw, we would keep its original format
    * for jar, we would repack dexes with zip format.
    * if you want to support below 14, you must use jar
    * or you want to save rom or check quicker, you can use raw mode also
    */

    dexMode = "jar"

    /**
    * necessary,default '[]'
    * what dexes in apk are expected to deal with tinkerPatch
    * it support * or ? pattern.
    */

    pattern = ["classes*.dex",
    "assets/secondary-dex-?.jar"]
    /**
    * necessary,default '[]'
    * Warning, it is very very important, loader classes can't change with patch.
    * thus, they will be removed from patch dexes.
    * you must put the following class into main dex.
    * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
    * own tinkerLoader, and the classes you use in them
    *
    */

    loader = [
    //use sample, let BaseBuildInfo unchangeable with tinker
    "tinker.sample.android.app.BaseBuildInfo"
    ]
    }

    lib {
    /**
    * optional,default '[]'
    * what library in apk are expected to deal with tinkerPatch
    * it support * or ? pattern.
    * for library in assets, we would just recover them in the patch directory
    * you can get them in TinkerLoadResult with Tinker
    */

    pattern = ["lib/*/*.so"]
    }

    res {
    /**
    * optional,default '[]'
    * what resource in apk are expected to deal with tinkerPatch
    * it support * or ? pattern.
    * you must include all your resources in apk here,
    * otherwise, they won't repack in the new apk resources.
    */

    pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

    /**
    * optional,default '[]'
    * the resource file exclude patterns, ignore add, delete or modify resource change
    * it support * or ? pattern.
    * Warning, we can only use for files no relative with resources.arsc
    */

    ignoreChange = ["assets/sample_meta.txt"]

    /**
    * default 100kb
    * for modify resource, if it is larger than 'largeModSize'
    * we would like to use bsdiff algorithm to reduce patch file size
    */

    largeModSize = 100
    }

    packageConfig {
    /**
    * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
    * package meta file gen. path is assets/package_meta.txt in patch file
    * you can use securityCheck.getPackageProperties() in your ownPackageCheck method
    * or TinkerLoadResult.getPackageConfigByName
    * we will get the TINKER_ID from the old apk manifest for you automatic,
    * other config files (such as patchMessage below)is not necessary
    */

    configField("patchMessage", "tinker is sample to use")
    /**
    * just a sample case, you can use such as sdkVersion, brand, channel...
    * you can parse it in the SamplePatchListener.
    * Then you can use patch conditional!
    */

    configField("platform", "all")
    /**
    * patch version via packageConfig
    */

    configField("patchVersion", "1.0")
    }
    //or you can add config filed outside, or get meta value from old apk
    //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
    //project.tinkerPatch.packageConfig.configField("test2", "sample")

    /**
    * if you don't use zipArtifact or path, we just use 7za to try
    */

    sevenZip {
    /**
    * optional,default '7za'
    * the 7zip artifact path, it will use the right 7za with your platform
    */

    zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
    /**
    * optional,default '7za'
    * you can specify the 7za path yourself, it will overwrite the zipArtifact value
    */

    // path = "/usr/local/bin/7za"
    }
    }
    复制代码

    精简后大致结构如下:

     tinkerPatch {
    oldApk = getOldApkPath()
    buildConfig {
    supportHotplugComponent = false
    }
    dex {
    pattern = ["classes*.dex",
    "assets/secondary-dex-?.jar"]
    }
    lib {
    pattern = ["lib/*/*.so"]
    }
    res {
    largeModSize = 100
    }
    packageConfig {
    configField("patchVersion", "1.0")
    }
    sevenZip {
    zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
    }
    }
    复制代码

    这里具体的配置是什么意思,先不用关注,因为配置的意思涉及到thinker本身业务和一些安卓热修相关知识点,我们主要关注下如果我们要实现这样的结构拓展,应该怎么做?下面我们来看看thinker是怎么做的:

    3.png

    1)插件入口,gradle插件的代码类入口

    2)和 3),拓展调用和定义,在定义中可以看到通过‘project.extensions.create’ 来创建自定义拓展,层级嵌套拓展通过追加方式,如:先自定义tinkerPatch,然后在tinkerPatch中嵌套buildConfig

    其中,细心的朋友会看到,为什么有些create是2个参数,有些是3个参数

    4.png

    根据官方定义可以看出,前面两个是定义拓展的key和value,然后后面的可变参数是 要传给value拓展的参数,举个例子:

    project.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, project)
    复制代码

    key为‘buildConfig’,value为‘TinkerBuildConfigExtension’

    然后project则是传递参数给到了‘TinkerBuildConfigExtension’,具体见TinkerBuildConfigExtension的定义如下:

    6.png

    其他拓展以此类推,这里不一一展开

    模块之配置android属性

    7.png

    1)preDexLibraries 设置为false

    默认情况下,preDexLibraries是为true的,作用主要是用来确认是否对Lib做preDexing操作,操作了的话带来的好处提高增量构建的速度;

    这里设置为false,猜测是thinker涉及了多dex,为避免和库工程冲突

    另外preDexLibraries是dexOptions属性之一,dexoptions是一个gradle对象,这个对象用来设置从java代码向.dex文件转化的过程中的一些配置选项;

    更多dexOptions属性可以戳这里>>>

    2)jumboMode 设置为true

    jumboMode设置为true,意识是忽略方法数限制的检查

    这样做的缺点是apk无法再低版本的设备上面安装,会出现错误:INSTALL_FAILED_DEXOPT

    具体细节戳这里>>>

    3)关闭 ENABLE_DEX_ARCHIVE

    void disableArchiveDex(Project project) {
    println 'disableArchiveDex -->'
    try {
    def booleanOptClazz = Class.forName('com.android.build.gradle.options.BooleanOption')
    def enableDexArchiveField = booleanOptClazz.getDeclaredField('ENABLE_DEX_ARCHIVE')
    enableDexArchiveField.setAccessible(true)
    def enableDexArchiveEnumObj = enableDexArchiveField.get(null)
    def defValField = enableDexArchiveEnumObj.getClass().getDeclaredField('defaultValue')
    defValField.setAccessible(true)
    defValField.set(enableDexArchiveEnumObj, false)
    } catch (Throwable thr) {
    // To some extends, class not found means we are in lower version of android gradle
    // plugin, so just ignore that exception.
    if (!(thr instanceof ClassNotFoundException)) {
    project.logger.error("reflectDexArchiveFlag error: ${thr.getMessage()}.")
    }
    }
    }
    复制代码

    ENABLE_DEX_ARCHIVE 这个功能主要是减少dex的大小

    这里关闭主要避免破坏multidex的maindex规则,进而实现多dex的场景

    4)keepRuntimeAnnotatedClasses 设置为 false

    keepRuntimeAnnotatedClasses 主要作用是带有运行时注解的类,保留在主dex中

    thinker关闭,主要降低主dex大小,兼容5.0以下的情况

    模块之aapt2和资源固定相关

    1.png 2.png

    该部分功能实现:使用导出的符号表进行资源id的固定

    为什么要进行资源ID的固定?具体戳这里>>,背景细节不在这展开

    其中,这里实现逻辑为:

    1)判断当前《Android Gradle Plugin》是否启动aapt2,如果没有启动跳过,如果启动了则进行aapt2的资源固定适配

    2)aapt2的资源固定适配操作,通过指定稳定的资源id映射文件,同时结合“--stable-ids”命令进行固定

    代码下载:DaviGradlePlu-main.zip

    收起阅读 »

    关于Socket,看我这几篇就够了(二)之HTTP

    在上一篇中,我们初步的讲述了socket的定义,以及socket中的TCP的简单用法。这篇我们主要讲的是HTTP相关的东西。什么是HTTPHTTP -> Hyper Text Transfer Protocol(超文本传输协议),它是基于TCP/IP协议...
    继续阅读 »

    在上一篇中,我们初步的讲述了socket的定义,以及socket中的TCP的简单用法。

    这篇我们主要讲的是HTTP相关的东西。

    什么是HTTP

    HTTP -> Hyper Text Transfer Protocol(超文本传输协议),它是基于TCP/IP协议的一种无状态连接

    特性

    无状态

    无状态是指,在标准情况下,客户端的发出每一次请求,都是独立的,服务器并不能直接通过标准http协议本身获得用户对话的上下文。

    这里,可能很多人会有疑问,我们平时使用的http不是这样的啊,服务器能识别我们请求的身份啊,要不免登录怎么做啊?

    所以额外解释下,我们说的这些状态,如cookie/session是由服务器与客户端双方约定好,每次请求的时候,客户端填写,服务器获取到后查询自身记录(数据库、内存),为客户端确定身份,并返回对应的值。

    从另一方面也可说,这个特性和http协议本身无关,因为服务器不是从这个协议本身获取对应的状态。

    无状态也可这样理解: 从同一客户端连续发出两次http请求到服务器,服务器无法从http协议本身上获取两次请求之间的关系

    无连接

    无连接指的是,服务器在响应客户端的请求后,就主动断开连接,不继续维持连接

    结构

    http 是超文本传输协议,顾名思义,传输的是一定格式的文本,所以,我们接下来讲述一下这个协议的格式

    在http中,一个很重要的分割符就是 CRLF(Carriage-Return Line-Feed) 也就是 \r 回车符 + \n 换行符,它是用来作为识别的字符

    请求 Request


    上图为请求格式

    请求行

    GET / HTTP/1.1\r\n

    首行也叫请求行,是用来告诉服务器,客户端调用的请求类型,请求资源路径,请求协议类型

    请求类型也就是我们常说的(面试官总问的)GET,POST等等发送的位置,它位于请求的最开始

    请求资源路径是提供给服务器内部的寻址路径,用来告诉服务器客户端希望访问什么资源,在浏览器中访问 https://www.jianshu.com/p/6cfbc63f3a2b (用简书做一波示范了),则我们请求的就是 /p/6cfbc63f3a2b

    请求协议类型目前使用最多的是HTTP/1.1说不定在不远的未来,将会被HTTP/2.0所取代

    注:

    所使用链接为https链接,但是其内容与http一样,因此使用该链接做为例子,ssl 将会在接下来的几篇文章中讲述

    请求行的不同内容需要用 " "空格符 来做分割

    请求行的结尾需要添加CRLF分割符

    请求头Request Headers

    请求行之后,一直到请求体(body),之间的部分,被我们成为请求头。

    请求头的长度并不固定,我们可以放置无限多的内容到请求头中。

    但是请求头的格式是固定的,我们可以把它看做是键值对。

    格式:

    key: value\r\n

    我们通常所说的cookie便是请求头中的一项

    一些常用的http头的定义与作用: https://blog.csdn.net/philos3/article/details/76946029

    注:

    当所有请求头都已经结束(即我们要发送body)的时候,我们需要额外增加一个空行(CRLF) 告诉服务器请求头已经结束

    请求体Request Body

    如果说header我们没有那么多的使用机会的话,那么body则是几乎每个开发人员都必须接触的了。

    通常,当我们进行 POST 请求的时候,我们上传的参数就在这里了。

    服务器是如何获得我们上传的完整Body呢?换句话说,就是服务器怎么知道我们的body已经传输完毕了呢?

    我们想一下,如果我们在需要实现这个协议的时候,我们会怎么做?

    可以约定特殊字节作为终止字符,当读取到指定字符时,即认为读取完毕

    发送方肯定知道要发送的数据的大小,直接告诉接收方,接收方只需要在收到指定大小的数据的时候就可以停止接收了

    发送方也不知道数据的大小(或者他需要花很大成本才能知道数据的大小),就先告诉接收方,我现在也不知道有多少,等发送的时候看,真正发送的时候告诉接收方,"我这次要发送多少",最后告诉接收方,"我发完了",接收方以此停止接收。‘

    也许你会有别的想法,那恭喜你,你可以自己实现类似的接收方法了。

    目前,服务器是依靠上述三种方法接收的:

    约定特殊字节:
    客户端在发送完数据后,就调用关闭socket连接,服务器在收到关闭请求后开始解析数据,并返回结果,最后关闭连接

    确定数据大小:
    客户端在请求头中给定字段 Content-Length,服务器解析到对应数据后接受body,当body数据达到指定长度后,服务器开始解析数据,并返回结果

    不确定数据大小(Http/1.1 可用)
    客户端在请求头中给定头 Transfer-Encoding: chunked,随后开始准备发送数据

    发送的每段数据都有特定的格式,

    格式为:

    长度行:
    每段数据的开头的文本为该段真实发送的数据的16进制长度加CRLF分割符

    数据行:
    真实发送的数据加CRLF分割符

    例:

    12\r\n // 长度行 16进制下的12就是10进制下的 18
    It is a chunk data\r\n // 数据行 CRLF 为分割符

    结尾段:

    用以告诉服务器数据发送完成,开始解析或存储数据。

    结尾段格式固定

    0\r\n
    \r\n

    目前,客户端使用这种方法的不多。

    到这里,如何告诉服务器应该接收多少数据的部分已经完成了

    接下来就到了,告诉服务器,数据究竟是什么了

    同样也是头部定义:Content-Type

    Content-Type介绍:
    https://blog.csdn.net/qq_23994787/article/details/79044908

    到这里,Request的基本格式已经讲完

    响应 Response



    相应结构

    其实Response 和 Request 从协议上分析,他们是一样的,但是他们是对Http协议中文本协议的不同的实现。

    响应行

    HTTP/1.1 200 OK\r\n

    首行也叫响应行,是用来告诉客户端当前请求的处理状况的,由请求协议类型,服务器状态码,对应状态描述构成

    请求协议类型 是用来告诉客户端,服务器采用的协议是什么,以便于客户端接下来的处理。

    服务器状态码 是一个很重要的返回值,它是用来通知服务器对本次客户端请求的处理结果。

    状态码非常多,但是对于我们开发一般用到的是如下几个状态码


    完整错误码请参照网址:
    https://baike.baidu.com/item/HTTP%E7%8A%B6%E6%80%81%E7%A0%81/5053660?fr=aladdin

    响应头Response Headers 及 响应体Response Body
    这些内容与Request中对应部分并无区别,顾不赘述了

    我们已经从特性与结构两部分讲述了Http相关的属性,到这里这篇文章的主要内容基本上算是结束了,接下来我要讲讲一些其他的http相关的知识

    跨域
    作为移动端开发人员,我们对这个的了解不是很多,也几乎用不到,但是我这里还是需要说明。因为现在已经到了前端的时代,万一我们以后需要踏足前端,了解跨域,至少能为我们解决不少事情。

    这篇文章不会详细讲解如何解决跨域,只会讲解跨域形成的原因

    什么是 跨域
    在讲跨域的时候,需要先讲什么是域

    什么是域
    在上一课讲解socket的过程中,我们已经发现了,想建立一个TCP/IP的连接需要知道至少两个事情

    对方的地址(host)
    对方的门牌号(port)

    我们只有依靠这两个才能建立TCP/IP 的连接,其中host标明我们该怎么找到对方,port表示,我们应该连接具体的那个端口。

    服务器应用是一直在监听着这个端口的,这样才能保证在有连接进入的时候,服务器直接响应对应的信息

    向上聊聊吧,我们通常讲的服务器指的是服务器应用,比如常说Tomcat,Apache 等等,他们启动的时候一般会绑定好一个指定的端口(通常不会同时绑定两个端口)。所以呢,作为客户端,就可以用host+port来确定一个指定的服务器应用

    由此,域的概念就此生成,就是host + port

    举个例子: http://127.0.0.1:8056/

    这个网址所属的域就是127.0.0.1+8056 也可以写成127.0.0.1:8056

    这时候有人就会问了,那localhost:8056和127.0.0.1:8056是同一域么,他们实际是等价的啊。

    他们不属于同一域,规定的很死,因为他们的host的表示不同,所以不是。

    跨域
    我们已经知道域了,跨域也就出现了,就是一个域访问另一个域。

    我们从http协议中可以发现,服务器并不任何强制规定域,也就是说,服务器并不在乎这个访问是从哪个域访问过来的,同时,作为客户端,我们也并没有域这么一说。

    那么跨域究竟是什么呢?


    这就要说跨域的来源了,我们日常访问的网站,它实际上就是html代码,服务器将代码下发到了浏览器,由浏览器渲染并展示给我们。

    开发浏览器的程序员在开发的时候,也不知道这个网页究竟要做什么,但是他们为了安全着想,不能给网页和客户端(socket)同样的权限,因此他们限制了某些操作,在本域的网页的某些请求操作在对方的服务器没有添加允许该域的访问权限的时候,访问操作将不会被执行,这些操作会对浏览器的安全性有很大到的影响。

    所以跨域就此产生。

    跨域从头到尾都只是一个客户端的操作行为,从某种角度上说,它与服务器毫无关系,因为服务器无法得知某次请求是否来自于某一网页(在客户端不配合的情况下),也就无从禁止了

    对于我们移动端,了解跨域后我们至少可以说,跨域与我们无关-_-

    socket实现简单的http请求
    事实上,一篇文章如果没有代码上的支撑,只是纯理念上的阐述,终究还是感觉缺点什么,本文将在上篇文章代码的基础上做些小的改进。

    这里就以菜鸟教程网的http教程作为本篇文章的测试(http://www.runoob.com/http/http-tutorial.html)(ip:47.246.3.228:80)

    // MARK: - Create 建立
    let socketFD = Darwin.socket(AF_INET, SOCK_STREAM, 0)

    func converIPToUInt32(a: Int, b: Int, c: Int, d: Int) -> in_addr {
    return Darwin.in_addr(s_addr: __uint32_t((a << 0) | (b << 8) | (c << 16) | (d << 24)))
    }
    // MARK: - Connect 连接
    var sock4: sockaddr_in = sockaddr_in()

    sock4.sin_len = __uint8_t(MemoryLayout.size(ofValue: sock4))
    // 将ip转换成UInt32
    sock4.sin_addr = converIPToUInt32(a: 47, b: 246, c: 3, d: 228)
    // 因内存字节和网络通讯字节相反,顾我们需要交换大小端 我们连接的端口是80
    sock4.sin_port = CFSwapInt16HostToBig(80)
    // 设置sin_family 为 AF_INET表示着这个为IPv4 连接
    sock4.sin_family = sa_family_t(AF_INET)
    // Swift 中指针强转比OC要复杂
    let pointer: UnsafePointer<sockaddr> = withUnsafePointer(to: &sock4, {$0.withMemoryRebound(to: sockaddr.self, capacity: 1, {$0})})

    var result = Darwin.connect(socketFD, pointer, socklen_t(MemoryLayout.size(ofValue: sock4)))
    guard result != -1 else {
    fatalError("Error in connect() function code is \(errno)")
    }
    // 组装文本协议 访问 菜鸟教程Http教程
    let sendMessage = "GET /http/http-tutorial.html HTTP/1.1\r\n"
    + "Host: http://www.runoob.com\r\n"
    + "Connection: keep-alive\r\n"
    + "USer-Agent: Socket-Client\r\n\r\n"
    //转换成二进制
    guard let data = sendMessage.data(using: .utf8) else {
    fatalError("Error occur when transfer to data")
    }
    // 转换指针
    let dataPointer = data.withUnsafeBytes({UnsafeRawPointer($0)})

    let status = Darwin.write(socketFD, dataPointer, data.count)

    guard status != -1 else {
    fatalError("Error in write() function code is \(errno)")
    }
    // 设置32Kb字节存储防止溢出
    let readData = Data(count: 64 * 1024)

    let readPointer = readData.withUnsafeBytes({UnsafeMutableRawPointer(mutating: $0)})
    // 记录当前读取多少字节
    var currentRead = 0

    while true {
    // 读取socket数据
    let result = Darwin.read(socketFD, readPointer + currentRead, readData.count - currentRead)

    guard result >= 0 else {
    fatalError("Error in read() function code is \(errno)")
    }
    // 这里睡眠是减少调用频率
    sleep(2)
    if result == 0 {
    print("无新数据")
    continue
    }
    // 记录最新读取数据
    currentRead += result
    // 打印
    print(String(data: readData, encoding: .utf8) ?? "")

    }

    对应代码例子已经放在github上,地址:https://github.com/chouheiwa/SocketTestExample

    总结
    越学习越觉得自己懂得越少,我们现在走的每一步,都是在学习。

    题外话:画图好费劲啊,都是用PPT画的-_-

    注: 本文原创,若希望转载请联系作者

    链接:https://www.jianshu.com/p/2b56a9cdf49d

    收起阅读 »

    集成环信聊天室后,如何每次进来可以看到之前的已读消息

    环信IM服务端目前不支持聊天室拉取已读消息。但是!!!!有个实现方法:移动端是有本地DB的,可以将聊天室的聊天记录放在本地db里,这样他每次进去的时候可以拉取。现在之所以没有,是因为退出聊天室会有一个删除本地缓存的操作,把他去掉即可;web端的话,需要您在应用...
    继续阅读 »

    环信IM服务端目前不支持聊天室拉取已读消息。

    但是!!!!

    有个实现方法:移动端是有本地DB的,可以将聊天室的聊天记录放在本地db里,这样他每次进去的时候可以拉取。现在之所以没有,是因为退出聊天室会有一个删除本地缓存的操作,把他去掉即可;web端的话,需要您在应用层来做

    收起阅读 »

    将构建配置从 Groovy 迁移到 KTS

    将构建配置从 Groovy 迁移到 KTS前言作为Android开发习惯了面向对象编程,习惯了IDEA提供的各种辅助开发快捷功能。那么带有陌生的常规语法的Groovy脚本对于我来说一向敬而远之。Kotlin DSL的出现感觉是为了我们量身定做的,因为采用 Ko...
    继续阅读 »

    将构建配置从 Groovy 迁移到 KTS

    前言

    作为Android开发习惯了面向对象编程,习惯了IDEA提供的各种辅助开发快捷功能。

    那么带有陌生的常规语法的Groovy脚本对于我来说一向敬而远之。

    Kotlin DSL的出现感觉是为了我们量身定做的,因为采用 Kotlin 编写的代码可读性更高,并且 Kotlin 提供了更好的编译时检查和 IDE 支持。


    名词概念解释

    • Gradle: 自动化构建工具. 平行产品: Maven.

    • Groovy: 语言, 编译后变为JVM byte code, 兼容Java平台.

    • DSLDomain Specific Language, 领域特定语言.

    • Groovy DSLGradle的API是Java的, Groovy DSL是在其之上的脚本语言. Groovy DS脚本文件后缀: .gradle.

    • KTS:是指 Kotlin 脚本,这是 Gradle 在构建配置文件中使用的一种 Kotlin 语言形式。Kotlin 脚本是可从命令行运行的 Kotlin 代码。

    • Kotlin DSL:主要是指 Android Gradle 插件 Kotlin DSL,有时也指底层 Gradle Kotlin DSL

    在讨论从 Groovy 迁移时,术语“KTS”和“Kotlin DSL”可以互换使用。换句话说,“将 Android 项目从 Groovy 转换为 KTS”与“将 Android 项目从 Groovy 转换为 Kotlin DSL”实际上是一个意思。

    Groovy和KTS对比

    类型KotlinGroovy
    自动代码补全支持不支持
    是否类型安全不是
    源码导航支持不支持
    重构自动关联手动修改

    优点:

    • 可以使用Kotlin, 开发者可能对这个语言更熟悉更喜欢.
    • IDE支持更好, 自动补全提示, 重构, imports等.
    • 类型安全: Kotlin是静态类型.
    • 不用一次性迁移完: 两种语言的脚本可以共存, 也可以互相调用.

    缺点和已知问题:

    • 目前,采用 KTS 的构建速度可能比采用 Groovy 慢(自测小demo耗时增加约40%(约8s))。

    • Project Structure 编辑器不会展开在 buildSrc 文件夹中定义的用于库名称或版本的常量。

    • KTS 文件目前在项目视图中不提供文本提示

    Android构建配置从Groovy迁移KTS

    准备工作

    1. Groovy 字符串可以用单引号 'string' 或双引号 "string" 引用,而 Kotlin 需要双引号 "string"

    2. Groovy 允许在调用函数时省略括号,而 Kotlin 总是需要括号。

    3. Gradle Groovy DSL 允许在分配属性时省略 = 赋值运算符,而 Kotlin 始终需要赋值运算符。

    所以在KTS中需要统一做到:

    • 使用双引号统一引号.

    groovy-kts-diff1.png

    • 消除函数调用和属性赋值的歧义(分别使用括号和赋值运算符)。

    groovy-kts-diff2.png

    脚本文件名

    Groovy DSL 脚本文件使用 .gradle 文件扩展名。

    Kotlin DSL 脚本文件使用 .gradle.kts 文件扩展名。

    一次迁移一个文件

    由于您可以在项目中结合使用 Groovy build 文件和 KTS build 文件,因此将项目转换为 KTS 的一个简单方法是先选择一个简单的 build 文件(例如 settings.gradle),将其重命名为 settings.gradle.kts,然后将其内容转换为 KTS。之后,确保您的项目在迁移每个 build 文件之后仍然可以编译。

    自定义Task

    由于Koltin 是静态类型语言,Groovy是动态语言,前者是类型安全的,他们的性质区别很明显的体现在了 task 的创建和配置上。详情可以参考Gradle官方迁移教程

    // groovy
    task clean(type: Delete) {
    delete rootProject.buildDir
    }
    // kotiln-dsl
    tasks.register("clean", Delete::class) {
    delete(rootProject.buildDir)
    }
    val clean by tasks.creating(Delete::class) {
    delete(rootProject.buildDir)
    }
    复制代码
    open class GreetingTask : DefaultTask() {
    var msg: String? = null
    @TaskAction
    fun greet() {
    println("GreetingTask:$msg")
    }
    }
    val msg by tasks.creating(GreetingTask::class) {}
    val testTask: Task by tasks.creating {
    doLast {
    println("testTask:Run")
    }
    }
    val testTask2: Task = task("test2") {
    doLast {
    println("Hello, World!")
    }
    }
    val testTask3: Task = tasks.create("test3") {
    doLast {
    println("testTask:Run")
    }
    }
    复制代码

    使用 plugins 代码块

    如果您在 build 文件中使用 plugins 代码块,IDE 将能够获知相关上下文信息,即使在构建失败时也是如此。IDE 可使用这些信息执行代码补全并提供其他实用建议,从而帮助您解决 KTS 文件中存在的问题。

    在您的代码中,将命令式 apply plugin 替换为声明式 plugins 代码块。Groovy 中的以下代码…

    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-kapt'
    apply plugin: 'androidx.navigation.safeargs.kotlin'
    复制代码

    在 KTS 中变为以下代码:

    plugins {
    id("com.android.application")
    id("kotlin-android")
    id("kotlin-kapt")
    id("androidx.navigation.safeargs.kotlin")
    }
    复制代码

    如需详细了解 plugins 代码块,请参阅 Gradle 的迁移指南

    注意plugins 代码块仅解析 Gradle 插件门户中提供的插件或使用 pluginManagement 代码块指定的自定义存储库中提供的插件。如果插件来自插件门户中不存在的 buildScript 依赖项,那么这些插件在 Kotlin 中就必须使用 apply 才能应用。例如:

    apply(plugin = "kotlin-android")
    apply {
    from("${rootDir.path}/config.gradle")
    from("${rootDir.path}/version.gradle.kts")
    }
    复制代码

    如需了解详情,请参阅 Gradle 文档

    强烈建议您plugins {}优先使用块而不是apply()函数。

    有两个关键的最佳实践可以更轻松地在 Kotlin DSL 的静态上下文中工作:

    • 使用plugins {}
    • 将本地构建逻辑放在构建的buildSrc目录中

    plugins {}块是关于保持您的构建脚本声明性,以便充分利用 Kotlin DSL

    使用buildSrc项目是关于将您的构建逻辑组织成共享的本地插件和约定,这些插件和约定易于测试并提供良好的 IDE 支持。

    依赖管理

    常见依赖

    // groovy
    implementation project(':library')
    implementation 'com.xxxx:xxxx:8.8.1'

    // kotlin
    implementation(project(":library"))
    implementation("com.xxxx:xxx:8.8.1")
    复制代码

    freeTree

    // groovy
    implementation fileTree(include: '*.jar', dir: 'libs')

    //kotlin
    implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
    复制代码

    特别类型库依赖

    //groovy
    implementation(name: 'splibrary', ext: 'aar')

    //kotlin
    implementation (group="",name="splibrary",ext = "aar")
    复制代码

    构建变体

    显式和隐式 buildTypes

    在 Kotlin DSL 中,某些 buildTypes(如 debug 和 release,)是隐式提供的。但是,其他 buildTypes 则必须手动创建。

    例如,在 Groovy 中,您可能有 debugrelease 和 staging buildTypes

    buildTypes
    debug {
    ...
    }
    release {
    ...
    }
    staging {
    ...
    }
    复制代码

    在 KTS 中,仅 debug 和 release buildTypes 是隐式提供的,而 staging 则必须由您手动创建:

    buildTypes
    getByName("debug") {
    ...
    }
    getByName("release") {
    ...
    }
    create("staging") {
    ...
    }
    复制代码

    举例说明

    Grovvy编写:

    productFlavors {
    demo {
    dimension "app"
    }
    full {
    dimension "app"
    multiDexEnabled true
    }
    }

    buildTypes {
    release {
    signingConfig signingConfigs.signConfig
    minifyEnabled true
    debuggable false
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }

    debug {
    minifyEnabled false
    debuggable true
    }
    }
    signingConfigs {
    release {
    storeFile file("myreleasekey.keystore")
    storePassword "password"
    keyAlias "MyReleaseKey"
    keyPassword "password"
    }
    debug {
    ...
    }
    }
    复制代码

    kotlin-KTL编写:

    productFlavors {
    create("demo") {
    dimension = "app"
    }
    create("full") {
    dimension = "app"
    multiDexEnabled = true
    }
    }

    buildTypes {
    getByName("release") {
    signingConfig = signingConfigs.getByName("release")
    isMinifyEnabled = true
    isDebuggable = false
    proguardFiles(getDefaultProguardFile("proguard-android.txtt"), "proguard-rules.pro")
    }

    getByName("debug") {
    isMinifyEnabled = false
    isDebuggable = true
    }
    }

    signingConfigs {
    create("release") {
    storeFile = file("myreleasekey.keystore")
    storePassword = "password"
    keyAlias = "MyReleaseKey"
    keyPassword = "password"
    }
    getByName("debug") {
    ...
    }
    }
    复制代码

    访问配置

    gradle.properties

    我们通常会把签名信息、版本信息等配置写在gradle.properties中,在kotlin-dsl中我们可以通过一下方式访问:

    1. rootProject.extra.properties
    2. project.extra.properties
    3. rootProject.properties
    4. properties
    5. System.getProperties()

    System.getProperties()使用的限制比较多

    • 参数名必须按照systemProp.xxx格式(例如:systemProp.kotlinVersion=1.3.72);
    • 与当前执行的task有关(> Configure project :buildSrc> Configure project :的结果不同,后者无法获取的gradle.properties中的数据);

    local.properties

    获取工程的local.properties文件

    gradleLocalProperties(rootDir)

    gradleLocalProperties(projectDir)

    获取系统环境变量的值

    val JAVA_HOME:String = System.getenv("JAVA_HOME") ?: "default_value"

    关于Ext

    Google 官方推荐的一个 Gradle 配置最佳实践是在项目最外层 build.gradle 文件的ext代码块中定义项目范围的属性,然后在所有模块间共享这些属性,比如我们通常会这样存放依赖的版本号。

    // build.gradle

    ext {
    compileSdkVersion = 28
    buildToolsVersion = "28.0.3"
    supportLibVersion = "28.0.0"
    ...
    }
    复制代码

    但是由于缺乏IDE的辅助(跳转查看、全局重构等都不支持),实际使用体验欠佳。

    KTL中用extra来代替Groovy中的ext

    // The extra object can be used for custom properties and makes them available to all
    // modules in the project.
    // The following are only a few examples of the types of properties you can define.
    extra["compileSdkVersion"] = 28
    // You can also create properties to specify versions for dependencies.
    // Having consistent versions between modules can avoid conflicts with behavior.
    extra["supportLibVersion"] = "28.0.0"
    复制代码
    android {
    // Use the following syntax to access properties you defined at the project level:
    // rootProject.extra["property_name"]
    compileSdkVersion(rootProject.extra["sdkVersion"])

    // Alternatively, you can access properties using a type safe delegate:
    val sdkVersion: Int by rootProject.extra
    ...
    compileSdkVersion(sdkVersion)
    }
    ...
    dependencies {
    implementation("com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}")
    ...
    }
    复制代码

    build.gralde中的ext数据是可以在build.gradle.kts中使用extra进行访问的。

    修改生成apk名称和BuildConfig中添加apk支持的cpu架构

    val abiCodes = mapOf("armeabi-v7a" to 1, "x86" to 2, "x86_64" to 3)
    android.applicationVariants.all {
    val buildType = this.buildType.name
    val variant = this
    outputs.all {
    val name =
    this.filters.find { it.filterType == com.android.build.api.variant.FilterConfiguration.FilterType.ABI.name }?.identifier
    val baseAbiCode = abiCodes[name]
    if (baseAbiCode != null) {
    //写入cpu架构信息
    variant.buildConfigField("String", "CUP_ABI", "\"${name}\"")
    }
    if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
    //修改apk名称
    if (buildType == "release") {
    this.outputFileName = "KotlinDSL_${name}_${buildType}.apk"
    } else if (buildType == "debug") {
    this.outputFileName = "KotlinDSL_V${variant.versionName}_${name}_${buildType}.apk"
    }
    }
    }
    }
    复制代码

    buildSrc

    我们在使用Groovy语言构建的时候,往往会抽取一个version_config.gradle来作为全局的变量控制,而ext扩展函数则是必须要使用到的,而在我们的Gradle Kotlin DSL中,如果想要使用全局控制,则需要建议使用buildSrc

    复杂的构建逻辑通常很适合作为自定义任务或二进制插件进行封装。自定义任务和插件实现不应存在于构建脚本中。buildSrc则不需要在多个独立项目之间共享代码,就可以非常方便地使用该代码了。

    buildSrc被视为构建目录。编译器发现目录后,Gradle会自动编译并测试此代码,并将其放入构建脚本的类路径中。

    1. 先创建buildSrc目录;
    2. 在该目录下创建build.gradle.kts文件;
    3. 创建一个buildSrc/src/main/koltin目录;
    4. 在该目录下创建Dependencies.kt文件作为版本管理类;

    需要注意的是buildSrcbuild.gradle.kts

    plugins {
    `kotlin-dsl`
    }
    repositories {
    jcenter()
    }
    复制代码

    或者

    apply {
    plugin("kotlin")
    }
    buildscript {
    repositories {
    gradlePluginPortal()
    }
    dependencies {
    classpath(kotlin("gradle-plugin", "1.3.72"))
    }
    }
    //dependencies {
    // implementation(gradleKotlinDsl())
    // implementation(kotlin("stdlib", "1.3.72"))
    //}
    repositories {
    gradlePluginPortal()
    }
    复制代码

    不同版本之间buildSrc下的build.gradle文件执行顺序:

    gradle-wrapper.properties:5.6.4

    com.android.tools.build:gradle:3.2.0

    1. BuildSrc:build.gradle
    2. setting.gradle
    3. Project:build.gradle
    4. Moudle:build.gradle

    gradle-wrapper.properties:6.5

    com.android.tools.build:gradle:4.1.1

    1. setting.gradle
    2. BuildSrc:build.gradle
    3. Project:build.gradle
    4. Moudle:build.gradle

    所以在非buildSrc目录下的build.gradle.kts文件中我们使用Dependencies.kt需要注意其加载顺序。

    收起阅读 »

    老生新谈,从OkHttp原理看网络请求

    OkHttp作为一个网络请求框架,地位是不言而喻的,研究它的好处就在于能够将TCP、HTTP、HTTPS等这些基础的网络知识实例化,抽象变为形象。 读完这篇文章您将了解到: OkHttp的整体请求结构; 责任链模式下各个拦截器的实现细节与职责; ...
    继续阅读 »

    OkHttp作为一个网络请求框架,地位是不言而喻的,研究它的好处就在于能够将TCP、HTTP、HTTPS等这些基础的网络知识实例化,抽象变为形象。


    读完这篇文章您将了解到:



    • OkHttp的整体请求结构;

    • 责任链模式下各个拦截器的实现细节与职责;

    • 如何找到可用且健康的连接?即连接池的复用;

    • 如何找到Http1和Http2的编/解码器?

    • NetworkInterceptor与ApplicationInterceptor拦截器的区别?

    • 如何建立TCP/TLS连接?



    本文源码为okhttp:4.9.1版本,文中没有贴大量源码,结合源码一起阅读最佳。



    OkHttp整体结构


    OkHttp的使用不是本文的主要内容,它只是作为源码解读的一个入口。


            val okHttpClient = OkHttpClient()
    val request: Request = Request.Builder()
    .url("https://cn.bing.com/")
    .build()

    okHttpClient.newCall(request).enqueue(object :Callback{
    override fun onFailure(call: Call, e: IOException) {
    }

    override fun onResponse(call: Call, response: Response) {
    }
    })

    OkHttp使用起来很简单,先创建OkHttpClient和Request对象,以Request来创建一个RealCall对象,利用它执行异步enqueue或者同步execute操作将请求发送出去,并监听请求失败或者成功的反馈Callback。


    这里有三个主要的类需要说明一下:OkHttpClient、Request以及RealCall



    • OkHttpClient: 相当于配置中?,可用于发送 HTTP 请求并读取其响应。它的配置有很多,例如connectTimeout:建?连接(TCP 或 TLS)的超时时间,readTimeout :发起请求到读到响应数据的超时时间,Dispatcher:调度器,?于调度后台发起的?络请求,等等。还有其他配置可查看源码。

    • Request: 一个主要设置网络请求Url请求方法(GET、POST......)请求头请求body的请求类。

    • RealCall: RealCall是由newCall(Request)方法返回,是OkHttp执行请求最核心的一个类之一,用作连接OkHttp的应用程序层和网络层,也就是将OkHttpClient和Request结合起来,发起异步和同步请求。


    从上面的使用步骤可以看到,OkHttp最后执行的是okHttpClient.newCall(request).enqueue,也就是RealCall的enqueue方法,这是一个异步请求,同样的,也可以执行同步请求RealCall.execute()


    RealCall的同步请求最后其实会调用RealCall.getResponseWithInterceptorChain(),而RealCall的异步请求是使用线程池先将请求放置到后台处理,但是最后还是会调用RealCall.getResponseWithInterceptorChain()来获取网络请求的返回值Response。从这里就基本能嗅到网络请求的核心其实与getResponseWithInterceptorChain()方法有关,那到底如何与服务器连接进行网络请求的?这个问题就先抛在这,后面再详细说。


    我们先从异步请求enqueue开始,来看异步请求的主要结构。


      类:Dispatcher

    private fun promoteAndExecute(): Boolean {
    ...
    val executableCalls = mutableListOf<AsyncCall>()
    synchronized(this) {
    val i = readyAsyncCalls.iterator()
    while (i.hasNext()) {
    val asyncCall = i.next()

    if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
    if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

    i.remove()
    asyncCall.callsPerHost.incrementAndGet()
    executableCalls.add(asyncCall)
    runningAsyncCalls.add(asyncCall)
    }
    isRunning = runningCallsCount() > 0
    }

    for (i in 0 until executableCalls.size) {
    val asyncCall = executableCalls[i]
    asyncCall.executeOn(executorService)
    }

    return isRunning
    }

    异步请求首先会将AsyncCall添加到双向队列readyAsyncCalls中(即准备执行但还没有执行的队列),做请求的准备动作。接着遍历准备执行队列readyAsyncCalls,寻找符合条件的请求,并将其加入到一个保存有效请求的列表executableCalls和正在执行队列runningAsyncCalls中,而这个筛选条件主要有两条:



    • if (runningAsyncCalls.size >= this.maxRequests) break :并发执行的请求数要小于最大的请求数64。


    • if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue :某个主机的并发请求数不能超过最大请求数5



    也就是说,当我们的并发请求量超过64个或者某个主机的的请求数超过5,则超过的请求暂时不能执行,需要等一等才能再加入执行队列中。


    将有效的请求筛选出后并保存,立即开始遍历请求,一一利用调度器Dispatcher里的ExecutorService进行Runnable任务,也就是遍历后加入到线程池中执行这些有效的网络请求。


     类:RealCall.AsyncCall

    override fun run() {
    threadName("OkHttp ${redactedUrl()}") {
    ...
    try {
    val response = getResponseWithInterceptorChain()
    signalledCallback = true
    responseCallback.onResponse(this@RealCall, response)
    } catch (e: IOException) {
    ...
    responseCallback.onFailure(this@RealCall, e)
    }
    }
    }

    上面的代码就是在线程池中执行的请求任务,可以看到try-catch块中有一句 val response = getResponseWithInterceptorChain() 得到网络请求结果resonse ,将返回的response或者错误,通过callback告知给用户。这个callback也就是一开始OkHttp使用时所注册监听的callback。


    另外,这个方法是不是很熟悉?因为在上面说明三个主要核心类时提到过,RealCall的同步请求或者异步请求,最后都会走到getResponseWithInterceptorChain()这一步。


    网络请求结果response就是通过这个getResponseWithInterceptorChain()方法返回的,那网络请求结果到底是如何拿到的? 与服务器又是如何交互的呢? 我们就来剖析这个方法的内部结构。


    拦截器内部实现


    从上面OkHttp的结构分析知道,所有网络请求的细节都封装在getResponseWithInterceptorChain() 这个核心方法中。那我们就来研究一下它的具体实现。


     类:RealCall

    internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
    interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
    call = this,
    interceptors = interceptors,
    index = 0,
    exchange = null,
    request = originalRequest,
    connectTimeoutMillis = client.connectTimeoutMillis,
    readTimeoutMillis = client.readTimeoutMillis,
    writeTimeoutMillis = client.writeTimeoutMillis
    )
    ...
    try {
    val response = chain.proceed(originalRequest)
    ...
    return response
    }
    ...
    }

    getResponseWithInterceptorChain()的内部实现是通过一个责任链模式来完成,将网络请求的各个阶段封装到各个链条中(即各个拦截器Interceptor),配置好各个Interceptor后将其放在?个List?,然后作为参数,创建?个RealInterceptorChain对象,并调? chain.proceed(request)来发起请求和获取响应。


    在每一条拦截器中,会先做一些准备动作,例如对该请求进行是否可用的判断,或者将请求转换为服务器解析的格式,等等,接着就对请求执行chain.proceed(request)。上面提到getResponseWithInterceptorChain()的内部实现是一个责任链模式,而chain.proceed(request)的作用就是责任链模式的核心所在,将请求移交给下一个拦截器。


    OkHttp中连自定义拦截器包括在内,一共有7种拦截器,在这里,网络请求的细节就封装在各个拦截器中,每个拦截器也都有自己的职责,只要把每个拦截器研究清楚,整个网络请求也就明了了。下面就来一一分析这些拦截器的职责。


    7种拦截器的职责


    1、用户自定义拦截器interceptors


    用户自定义拦截器是在所有其他拦截器之前,开发者可根据业务需求进行网络拦截器的自定义,例如我们常常自定义Token处理拦截器,日志打印拦截器等。


    2、RetryAndFollowUpInterceptor


    RetryAndFollowUpInterceptor是一个请求失败和重定向时重试的拦截器。它的内部开启了一个请求循环,每次循环都会先做一个准备动作(call.enterNetworkInterceptorExchange(request, newExchangeFinder)),这个准备动作最主要的目的在于创建一个ExchangeFinder,为请求寻找可用的Tcl或者Tsl连接以及设置跟连接相关的一些参数,如连接编码解码器等。 ExchangeFinder在后面网络连接时,会详细说明。


    准备工作做好后便开始了一个网络请求(response = realChain.proceed(request)),这句代码的目的是为了将请求传递给下一个拦截器。同时,会判断当前请求是否会出错以及是否需要重定向。如果出错或者需要重定向,那么就又开始新一轮的循环,直到没有出错和需要重定向为止。


    这里出错和重定向的判断标准也简单说一下:



    • 判断出错的标准: 利用try-catch块对请求进行异常捕获,这里会捕获RouteException和IOException,并且在出错后都会先判断当前请求是否能够进行重试的操作。

    • 重定向标准: 这里判断是否需要重定向,是对Response的状态码Code进行审查,当状态码为3xx时,则表示需要重定向,而后创建一个新的request,进行重试操作。


    3、BridgeInterceptor


    BridgeInterceptor是用来连接应用程序代码和网络代码的一个拦截器。也就是说该拦截器会帮用户准备好服务器请求所需要的一些配置。可能定义太抽象,我们就先来看一下一个请求Url所对应的服务器请求头是怎么样的?



    URL: wanandroid.com/wxarticle/c…
    方法: GET



    那它所对应的请求头如下:



    GET /wxarticle/chapters/json HTTP/1.1
    Host: wanandroid.com
    Accept: application/json, text/plain, /
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9
    Connection: keep-alive
    User-Agent: Mozilla/5.0 xxx
    ......



    你可能会问,BridgeInterceptor拦截器和这个有什么关系?其实BridgeInterceptor的作用就是帮用户处理网络请求,它会帮助用户填写服务器请求所需要的配置信息,如上面所展示的User-Agent、Connection、Host、Accept-Encoding等。同时也会对请求的结果进行相应处理。


    BridgeInterceptor的内部实现主要分为以下三步:



    1. 为用户网络请求设置Content-Type、Content-Length、Host、Connection、Cookie等参数,也就是将一般请求转换为适合服务器解析的格式,以适应服务器端;


    2. 通过 chain.proceed(requestBuilder.build())方法,将转换后的请求移交给下一个拦截器CacheInterceptor,并接收返回的结果Response;


    3. 对结果Response也进行gzip、Content-Type转换,以适应应用程序端。



    所以说BridgeInterceptor是应用程序和服务器端的一个桥梁。


    4、CacheInterceptor


    CacheInterceptor是一个处理网络请求缓存的拦截器。它的内部处理和一些图片缓存的逻辑相似,首先会判断是否存在可用的缓存,如果存在,则直接返回缓存,反之,调用chain.proceed(networkRequest)方法将请求移交给下一个拦截器,有了结果后,将结果put到cache中。


    5、ConnectInterceptor


    ConnectInterceptor是建立连接去请求的拦截器。


      internal fun initExchange(chain: RealInterceptorChain): Exchange {
    ...
    val exchangeFinder = this.exchangeFinder!!
    val codec = exchangeFinder.find(client, chain)
    val result = Exchange(this, eventListener, exchangeFinder, codec)
    this.interceptorScopedExchange = result
    this.exchange = result
    ...

    if (canceled) throw IOException("Canceled")
    return result
    }

    从它的源码可以看到,它首先会通过ExchangeFinder查询到codec,这个ExchangeFinder是不是很熟悉?在上面RetryAndFollowUpInterceptor分析中,每次循环都会先做创建ExchangeFinder的准备工作。


    而这个codec是什么?它是一个编码解码器,来确定是用Http1的方式还是以Http2的方式进行请求。


    在找到合适的codec后,作为参数创建Exchange。Exchange内部涉及了很多网络连接的实现,这个后面再详细说,我们先看看是如何找到合适的codec?


    如何找到可用连接?


    找到合适的codec,就必须先找到一个可用的网络连接,再利用这个可用的连接创建一个新的codec。 为了找到可用的连接,内部使用了大概5种方式进行筛选。


    第一种:从连接池中查找


    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
    val result = call.connection!!
    return result
    }

    尝试在连接池中查找可用的连接,在遍历连接池中的连接时,就会判断每个连接是否可用,而判断连接是否可用的条件如下:



    1. 请求数要小于该连接最大能承受的请求数,Http2以下,最大请求数为1个,并且此连接上可创建新的交换;

    2. 该连接的主机和请求的主机一致;


    如果从连接池中拿到了合格的连接connection,则直接返回。


    如果没有拿到,那就进行第二种拿可用连接的方式。


    第二种:传入Route,从连接池中查找


     if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
    val result = call.connection!!
    return result
    }

    第二种依然是从连接池中拿,但是这次不同的是,参数里传入了routes,这个routes是包含路由Route的一个List集合,而Route其实指的是连接的IP地址、TCP端口以及代理模式。


    而这次从连接池中拿,主要是针对Http2,路由必须共用一个IP地址,此连接的服务器证书必须包含新主机且证书必须与主机匹配。


    第三种:自己创建连接


    如果前两次从连接池里都没有拿到可用连接,那么就自己创建连接。


     val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
    newConnection.connect(
    connectTimeout,
    readTimeout,
    writeTimeout,
    pingIntervalMillis,
    connectionRetryEnabled,
    call,
    eventListener
    )
    }

    创建连接其实是内部自己在进行socket,tls的连接,这里抛出一个问题在后面解答:TCP/TLS连接是如何实现的?


    自己创建好连接后,又做了一次从连接池中查找的操作。


    第四种:多路复用置为true,依然从连接池中查找


     if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
    val result = call.connection!!
    newConnection.socket().closeQuietly()
    return result
    }

    这次从连接池中查找,requireMultiplexed置为了true,只查找支持多路复用的连接。并且在建立连接后,将新的连接保存到连接池中。


    如何找到Http1和Http2的编/解码器?


    上面已经分析出寻找可用且健康的连接的几种方式,那对于codec的创建则需要根据这些连接进行Http1和Http2的区分。如果http2Connection不为null,则创建Http2ExchangeCodec,反之创建Http1ExchangeCodec。


    找到编解码器后,我们就回到ConnectInterceptor的一开始,利用编解码器codec创建了一个Exchange,而这个Exchange的内部其实是利用Http1解码器或者Http2解码器,分别进行请求头的编写writeRequestHeaders,或者创建Request Body,发送给服务器。


    Exchange初始化成功后,就又将请求移交给了下一个拦截器CallServerInterceptor。


    6、CallServerInterceptor


    CallServerInterceptor是链中最后一个拦截器,主要用于向服务器发送内容,主要传输http的头部和body信息。


    其内部利用上面创建的Exchange进行请求头编写,创建Request body,发送请求,得到结果后,对结果进行解析并回传。


    7、NetworkInterceptor


    networkInterceptor也是属于用户自定义的一种拦截器,它的位置在ConnectInterceptor之后,CallServerInterceptor之前。我们知道第一个拦截器便是用户自定义,那和这个有什么区别呢?


    networkInterceptor前面已经存在有多个拦截器的使用,在请求到达该拦截器时,请求信息已经相当复杂了,其中就包括RetryAndFollowUpInterceptor重试拦截器,经过分析知道,每当重试一次,其后面的拦截器也都会被调用一次,这样就导致networkInterceptor也会被调用多次,而第一个自定义拦截器只会调用一次。当我们需要自定义拦截器时,如token、log,为了资源消耗这一点,一般都是使用第一个。


    到这里为止,7种拦截器都分析完成。在分析ConnectInterceptor时抛出了一个问题:TCP/TLS连接是如何实现的?


    如何建立TCP/TLS连接?


    TCP连接


    fun connect(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    call: Call,
    eventListener: EventListener
    )
    {
    ...

    while (true) {
    try {
    if (route.requiresTunnel()) {
    connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
    if (rawSocket == null) {
    // We were unable to connect the tunnel but properly closed down our resources.
    break
    }
    } else {
    connectSocket(connectTimeout, readTimeout, call, eventListener)
    }
    establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
    eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
    break
    } catch (e: IOException) {
    ...
    }


    1. 在connect的内部开启了一个while循环,可以看到第一步就是route.requiresTunnel()判断,这个requiresTunnel()方法表示该请求是否使用了Proxy.Type.HTTP代理且目标是Https连接;

    2. 如果是,则创建一个代理隧道连接Tunnel(connectTunnel)。创建这个隧道的目的在于利用Http来代理请求Https;

    3. 如果不是,则直接建立一个TCP连接(connectSocket);

    4. 建立请求协议。


    代理隧道是如何创建的?它的内部会先通过Http代理创建一个TLS的请求,也就是在地址url上增加Host、Proxy-Connection、User-Agent首部。接着最多21次的尝试,利用connectSocket开启TCP连接且利用TLS请求创建一个代理隧道。


    从这里可以看见,不管是否需要代理隧道,都会开始建立一个TCP连接(connectSocket),那又是如何建立TCP连接的?


     private fun connectSocket(
    connectTimeout: Int,
    readTimeout: Int,
    call: Call,
    eventListener: EventListener
    )
    {
    val proxy = route.proxy
    val address = route.address

    val rawSocket = when (proxy.type()) {
    Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
    else -> Socket(proxy)
    }
    this.rawSocket = rawSocket

    eventListener.connectStart(call, route.socketAddress, proxy)
    rawSocket.soTimeout = readTimeout
    try {
    Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
    } catch (e: ConnectException) {
    throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
    initCause(e)
    }
    }

    ...
    }

    从源码上看,如果代理类型为直连或者HTTP/FTP代理,则直接创建一个socket,反之,则指定代理类型进行创建。我们看到创建后返回了一个rawSocket,这个就代表着TCP连接。在最后 调用Platform.get().connectSocket,而这实际就是调用socket的connect方法来打开一个TCP连接。


    TLS连接


    在建立TCP连接或者创建Http代理隧道后,就会开始建立连接协议(establishProtocol)。


      private fun establishProtocol(
    connectionSpecSelector: ConnectionSpecSelector,
    pingIntervalMillis: Int,
    call: Call,
    eventListener: EventListener
    )
    {
    if (route.address.sslSocketFactory == null) {
    if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
    socket = rawSocket
    protocol = Protocol.H2_PRIOR_KNOWLEDGE
    startHttp2(pingIntervalMillis)
    return
    }

    socket = rawSocket
    protocol = Protocol.HTTP_1_1
    return
    }

    eventListener.secureConnectStart(call)
    connectTls(connectionSpecSelector)
    eventListener.secureConnectEnd(call, handshake)

    if (protocol === Protocol.HTTP_2) {
    startHttp2(pingIntervalMillis)
    }
    }


    1. 判断当前地址是否是HTTPS;

    2. 如果不是HTTPS,则判断当前协议是否是明文HTTP2,如果是的则调用startHttp2,开始Http2的握手动作,如果是Http/1.1则直接return返回;

    3. 如果是HTTPS,就开始建立TLS安全协议连接了(connectTls);

    4. 如果是HTTPS且为HTTP2,除了建立TLS连接外,还会调用startHttp2,开始Http2的握手动作。


    在上述第3步时就提到了TLS的连接(connectTls),那我们就来看一下它的内部实现:


    private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
    val address = route.address
    val sslSocketFactory = address.sslSocketFactory
    var success = false
    var sslSocket: SSLSocket? = null
    try {
    // Create the wrapper over the connected socket.
    sslSocket = sslSocketFactory!!.createSocket(
    rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket

    // Configure the socket's ciphers, TLS versions, and extensions.
    val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket)
    if (connectionSpec.supportsTlsExtensions) {
    Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols)
    }

    // Force handshake. This can throw!
    sslSocket.startHandshake()
    // block for session establishment
    val sslSocketSession = sslSocket.session
    val unverifiedHandshake = sslSocketSession.handshake()

    // Verify that the socket's certificates are acceptable for the target host.
    if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) {
    val peerCertificates = unverifiedHandshake.peerCertificates
    if (peerCertificates.isNotEmpty()) {
    val cert = peerCertificates[0] as X509Certificate
    throw SSLPeerUnverifiedException("""
    |Hostname ${address.url.host} not verified:
    | certificate: ${CertificatePinner.pin(cert)}
    | DN: ${cert.subjectDN.name}
    | subjectAltNames: ${OkHostnameVerifier.allSubjectAltNames(cert)}
    "
    "".trimMargin())
    } else {
    throw SSLPeerUnverifiedException(
    "Hostname ${address.url.host} not verified (no certificates)")
    }
    }

    val certificatePinner = address.certificatePinner!!

    handshake = Handshake(unverifiedHandshake.tlsVersion, unverifiedHandshake.cipherSuite,
    unverifiedHandshake.localCertificates
    )
    {
    certificatePinner.certificateChainCleaner!!.clean(unverifiedHandshake.peerCertificates,
    address.url.host)
    }

    // Check that the certificate pinner is satisfied by the certificates presented.
    certificatePinner.check(address.url.host) {
    handshake!!.peerCertificates.map { it as X509Certificate }
    }

    // Success! Save the handshake and the ALPN protocol.
    val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {
    Platform.get().getSelectedProtocol(sslSocket)
    } else {
    null
    }
    socket = sslSocket
    source = sslSocket.source().buffer()
    sink = sslSocket.sink().buffer()
    protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1
    success = true
    } finally {
    ...
    }
    }

    这段代码很长,具体逻辑我就源码总结了以下几点:



    1. 利用请求地址host,端口以及TCP socket共同创建sslSocket;

    2. 为Socket 配置加密算法,TLS版本等;

    3. 调用startHandshake()进行强制握手;

    4. 验证服务器证书的合法性;

    5. 利用握手记录进行证书锁定校验(Pinner);

    6. 连接成功则保存握手记录和ALPN协议。


    Tsl加密连接的源码内容其实与HTTPS所定义的客户端与服务器通信的规则一致。创建好sslSocket后就会开始进行client和server的通信操作。


    总结


    OkHttp大致的请求实现如上面解析,跟着源码走完了一个请求到处理再到返回结果的整个流程,期间OkHttp做了很多细节封装,也使用了很多设计模式,如做核心的责任链模式、建造者模式、工厂模式以及策略模式等,都值得我们学习。


    以上便是OkHttp的解析,希望这篇文章能帮到您,感谢阅读。



    参考资料


    OkHttp源码深度解析-OPPO互联网技术


    推荐阅读


    【网络篇】开发必备知识点:UDP/TCP协议



    作者:付十一
    链接:https://juejin.cn/post/6979729429228421134
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


    收起阅读 »

    性能优化一分钟定位Android启动耗时问题

    前言 Tencent Matrix默认无法监测Application冷启动的耗时方法,本文介绍了如何改造Matrix支持冷启动耗时方法监测。让你一分钟就能给App启动卡顿号脉。 1. 接入Tencent Matrix 1.1 在你项目根目录下的 grad...
    继续阅读 »

    前言


    Tencent Matrix默认无法监测Application冷启动的耗时方法,本文介绍了如何改造Matrix支持冷启动耗时方法监测。让你一分钟就能给App启动卡顿号脉。


    1. 接入Tencent Matrix


    1.1 在你项目根目录下的 gradle.properties 中配置要依赖的 Matrix 版本号,如:


    MATRIX_VERSION=1.0.0

    1.2 在你项目根目录下的 build.gradle 文件添加 Matrix 依赖,如:


    dependencies {
    classpath ("com.tencent.matrix:matrix-gradle-plugin:${MATRIX_VERSION}") { changing = true }
    }

    1.3 在 app/build.gradle 文件中添加 Matrix 各模块的依赖,如:


      dependencies {
    implementation group: "com.tencent.matrix", name: "matrix-android-lib", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-android-commons", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-trace-canary", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-resource-canary-android", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-resource-canary-common", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-io-canary", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-sqlite-lint-android-sdk", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-battery-canary", version: MATRIX_VERSION, changing: true
    implementation group: "com.tencent.matrix", name: "matrix-hooks", version: MATRIX_VERSION, changing: true
    }

    apply plugin: 'com.tencent.matrix-plugin'
    matrix {
    trace {
    enable = true //if you don't want to use trace canary, set false
    baseMethodMapFile = "${project.buildDir}/matrix_output/Debug.methodmap"
    blackListFile = "${project.projectDir}/matrixTrace/blackMethodList.txt"
    }
    }


    1.4 实现 PluginListener,接收 Matrix 处理后的数据, 如:


    class MatrixListener(context: Context?) : DefaultPluginListener(context) {
    companion object {
    const val TAG: String = "Matrix.TestPluginListener"
    }

    override fun onReportIssue(issue: Issue) {
    super.onReportIssue(issue)
    MatrixLog.e(TAG, issue.toString())

    }
    }

    1.5 实现动态配置接口, 可修改 Matrix 内部参数. 在 sample-android 中 我们有个简单的动态接口实例DynamicConfigImplDemo.java, 其中参数对应的 key 位于文件 MatrixEnum中, 摘抄部分示例如下:


      class MatrixConfig : IDynamicConfig {
    val isFPSEnable: Boolean
    get() = true
    val isTraceEnable: Boolean
    get() = true
    val isMatrixEnable: Boolean
    get() = true

    override fun get(key: String, defStr: String): String {

    // for Activity leak detect
    if (ExptEnum.clicfg_matrix_resource_detect_interval_millis.name == key || ExptEnum.clicfg_matrix_resource_detect_interval_millis_bg.name == key) {
    Log.d(
    "DynamicConfig",
    "Matrix.ActivityRefWatcher: clicfg_matrix_resource_detect_interval_millis 10s"
    )
    return TimeUnit.SECONDS.toMillis(5).toString()
    }
    if (ExptEnum.clicfg_matrix_resource_max_detect_times.name == key) {
    Log.d(
    "DynamicConfig",
    "Matrix.ActivityRefWatcher: clicfg_matrix_resource_max_detect_times 5"
    )
    return 3.toString()
    }
    return defStr
    }

    override fun get(key: String, defInt: Int): Int {
    //TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
    if (MatrixEnum.clicfg_matrix_resource_max_detect_times.name == key) {
    MatrixLog.i(TAG, "key:$key, before change:$defInt, after change, value:2")
    return 2 //new value
    }
    if (MatrixEnum.clicfg_matrix_trace_fps_report_threshold.name == key) {
    return 10000
    }
    if (MatrixEnum.clicfg_matrix_trace_fps_time_slice.name == key) {
    return 12000
    }
    if (ExptEnum.clicfg_matrix_trace_app_start_up_threshold.name == key) {
    return 3000
    }
    return if (ExptEnum.clicfg_matrix_trace_evil_method_threshold.name == key) {
    200
    } else defInt
    }

    override fun get(key: String, defLong: Long): Long {
    //TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
    if (MatrixEnum.clicfg_matrix_trace_fps_report_threshold.name == key) {
    return 10000L
    }
    if (MatrixEnum.clicfg_matrix_resource_detect_interval_millis.name == key) {
    MatrixLog.i(TAG, "$key, before change:$defLong, after change, value:2000")
    return 2000
    }
    return defLong
    }

    override fun get(key: String, defBool: Boolean): Boolean {
    //TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
    return defBool
    }

    override fun get(key: String, defFloat: Float): Float {
    //TODO here return default value which is inside sdk, you can change it as you wish. matrix-sdk-key in class MatrixEnum.
    return defFloat
    }

    companion object {
    private const val TAG = "Matrix.DynamicConfigImplDemo"
    }
    }

    1.6 选择程序启动的位置对 Matrix 进行初始化,如在 Application 的继承类中, Init 核心逻辑如下:


      Matrix.Builder builder = new Matrix.Builder(application); // build matrix
    builder.patchListener(new TestPluginListener(this)); // add general pluginListener
    DynamicConfigImplDemo dynamicConfig = new DynamicConfigImplDemo(); // dynamic config

    // init plugin
    IOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder()
    .dynamicConfig(dynamicConfig)
    .build());
    //add to matrix
    builder.plugin(ioCanaryPlugin);

    //init matrix
    Matrix.init(builder.build());

    // start plugin
    ioCanaryPlugin.start();


    2. 改造Application子类


    2.1 模拟Application卡顿


    private fun A() {
    B()
    H()
    L()
    SystemClock.sleep(800)
    }

    private fun B() {
    C()
    G()
    SystemClock.sleep(200)
    }

    private fun C() {
    D()
    E()
    F()
    SystemClock.sleep(100)
    }

    private fun D() {
    SystemClock.sleep(20)
    }

    private fun E() {
    SystemClock.sleep(20)
    }

    private fun F() {
    SystemClock.sleep(20)
    }

    private fun G() {
    SystemClock.sleep(20)
    }

    private fun H() {
    SystemClock.sleep(20)
    I()
    J()
    K()
    }

    private fun I() {
    SystemClock.sleep(20)
    }

    private fun J() {
    SystemClock.sleep(6)
    }

    private fun K() {
    SystemClock.sleep(10)
    }


    private fun L() {
    SystemClock.sleep(10000)
    }

    2.2 Application.onCreate()调用卡顿方法


    override fun onCreate() {
    A()
    }

    2.3 反射获取ActivityThread的mHandler


    override fun attachBaseContext(base: Context?) {
    super.attachBaseContext(base)
    println("zijiexiaozhan MyApp attachBaseContext")
    time1 = SystemClock.uptimeMillis()
    time3 = System.currentTimeMillis()

    try {
    val forName = Class.forName("android.app.ActivityThread")
    val field = forName.getDeclaredField("sCurrentActivityThread")
    field.isAccessible = true
    val activityThreadValue = field[forName]
    val mH = forName.getDeclaredField("mH")
    mH.isAccessible = true
    val handler = mH[activityThreadValue]
    mHandler = handler as Handler
    } catch (e: Exception) {
    }
    }

    2.4 将原来的onCreate的方法调用转入匿名内部类调用


    inner class ApplicationTask : Runnable {
    override fun run() {
    A()
    }
    }

    2.5 重写Application onCreate方法


    override fun onCreate() {
    super.onCreate()
    //重点
    mHandler.postAtFrontOfQueue(ApplicationTask())
    }

    3.运行,快速定位


    3.1 关键字"Trace_EvilMethod"查找日志



    tag[Trace_EvilMethod]type[0];key[null];content[{"machine":"MIDDLE","cpu_app":0,"mem":3822452736,"mem_free":1164132,"detail":"NORMAL","cost":1344,"usage":"0.37%","scene":"default","stack":"0,1048574,1,1344\n1,5471,1,1338\n2,17582,1,1338\n3,17558,1,1338\n4,17560,1,379\n5,17562,1,160\n6,17563,1,17\n6,17566,1,20\n6,17568,1,20\n5,17569,1,20\n4,17573,1,56\n5,17575,1,21\n5,17576,1,5\n5,17578,1,10\n4,17580,1,102\n","stackKey":"17558|","tag":"Trace_EvilMethod","process":"com.peter.viewgrouptutorial","time":1624837969986}]



    3.2 解析日志 打印卡顿堆栈


    android.os.Handler dispatchMessage 1344
    .com.peter.viewgrouptutorial.MyApp$ApplicationTask run 1338
    ..com.peter.viewgrouptutorial.MyApp access$A 1338
    ...com.peter.viewgrouptutorial.MyApp A 1338
    ....com.peter.viewgrouptutorial.MyApp B 379
    .....com.peter.viewgrouptutorial.MyApp C 160
    ......com.peter.viewgrouptutorial.MyApp D 17
    ......com.peter.viewgrouptutorial.MyApp E 20
    ......com.peter.viewgrouptutorial.MyApp F 20
    .....com.peter.viewgrouptutorial.MyApp G 20
    ....com.peter.viewgrouptutorial.MyApp H 56
    .....com.peter.viewgrouptutorial.MyApp I 21
    .....com.peter.viewgrouptutorial.MyApp J 5
    .....com.peter.viewgrouptutorial.MyApp K 10
    ....com.peter.viewgrouptutorial.MyApp L 102


    收起阅读 »

    【开源项目】利用环信IM开发的一款兴趣社交APP——相约国粹

    项目背景 相约国粹是一个集结国内名著有声书,国粹欣赏以及外国著名音乐为主题 供用户欣赏 蕴含了古典文化的传播,教育,欣赏等 我们将每一种资源(比如论语)都单独开设群聊用户们可以根据喜欢的资源将志同道合的朋友欢聚一堂进行线上畅聊讨论运行说明&...
    继续阅读 »

    项目背景

     相约国粹是一个集结国内名著有声书,国粹欣赏以及外国著名音乐为主题 

    供用户欣赏 蕴含了古典文化的传播,教育,欣赏等 我们将每一种资源(比如论语)都单独开设群聊

    用户们可以根据喜欢的资源将志同道合的朋友欢聚一堂进行线上畅聊讨论

    运行说明 

    1.终端进入对应目录进行 pod install 

    2.资源只做展示,项目中已去除资源获取地址

    功能介绍 

    1.音视频资源的展示,播放与下载 

    2.通过资源匹配响应的群聊 

    3.可以进行1对1聊天,语音,视频


    github地址:https://github.com/xiyupingxuan/RTE-2021-Innovation-Challenge/tree/master/Application-Challenge/


    收起阅读 »

    Android APT 系列 (一):APT 筑基之反射

    前言很高兴遇见你~这又是一个新的系列,灵感来源于最近做的一次布局优化,我们知道:Android 中少量的系统控件是通过 new 的方式创建出来的,而大部分控件如 androidx.appcompat.widget 下的控...
    继续阅读 »

    前言

    很高兴遇见你~

    这又是一个新的系列,灵感来源于最近做的一次布局优化,我们知道:Android 中少量的系统控件是通过 new 的方式创建出来的,而大部分控件如 androidx.appcompat.widget 下的控件,自定义控件,第三方控件等等,都是通过反射创建的。大量的反射创建多多少少会带来一些性能问题,因此我们需要去解决反射创建的问题,我的解决思路是:

    1、通过编写 Android 插件获取 Xml 布局中的所有控件

    2、拿到控件后,通过 APT 生成用 new 的方式创建 View 的类

    3、最后通过反射获取当前类并在基类里面完成替换

    一个小小的布局优化,涉及的东西还挺多的,Android 插件我们后续在讲,话说 Gradle 系列目前只更了一篇😂,别急,后面都会有的。我们这个系列主要是讲 APT,而讲 APT ,我们必须先了解两个重点知识:注解和反射

    今天就重点来介绍下反射

    Github Demo 地址 , 大家可以看 Demo 跟随我的思路一起分析

    一、什么是反射?

    简单来讲,反射就是:已知一个类,可以获取这个类的所有信息

    一般情况下,根据面向对象封装原则,Java 实体类的属性都是私有的,我们不能获取类中的属性。但我们可以根据反射,获取私有变量、方法、构造方法,注解,泛型等等,非常的强大

    注意:Google 在 Android 9.0 及之后对反射做了限制,被使用 @hide 标记的属性和方法通过反射拿不到

    二、反射使用

    下面给出一段已知的代码,我们通过实践来对反射进行讲解:

    //包路径
    package com.dream.aptdemo;

    //自定义注解1
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface CustomAnnotation1{

    }

    //自定义注解2
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @interface CustomAnnotation2{

    }

    //自定义注解3
    @Target(ElementType.TYPE)
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @interface CustomAnnotation3{

    }

    //接口
    interface ICar {
    void combine();
    }

    //车
    @CustomAnnotation3
    class Car<K,V> {
    private String carDesign = "设计稿";
    public String engine = "发动机";

    public void run(long kilometer) {
    System.out.println("Car run " + kilometer + " km");
    }
    }
    //==============================上面这些都是为下面这台奔驰服务的😂===========================
    //奔驰
    @CustomAnnotation1
    @CustomAnnotation2
    class Benz extends Car<String,Integer> implements ICar {

    private String carName = "奔驰";
    public String carColor = "白色";

    public Benz() {
    }

    private Benz(String carName) {
    this.carName = carName;
    }

    public Benz(String carName, String carColor) {
    this.carName = carName;
    this.carColor = carColor;
    }

    @Override
    public void combine() {
    System.out.println("组装一台奔驰");
    }

    private void privateMethod(String params){
    System.out.println("我是私有方法: " + params);
    }
    }

    下面所讲到的都是关于反射一些常用的 Api

    三、类

    我们可以通过 3 种方式去获取类对象:

    1)、Benz.class :类获取

    2)、benz.getClass :对象获取

    3)、Class.forName :静态获取

     Benz benz = new Benz();
    Class benzClass = Benz.class;
    Class benzClass1 = benz.getClass();
    Class benzClass2 = Class.forName("com.dream.aptdemo.Benz");

    注意

    1、在一个 JVM 中,一种类,只会有一个类对象存在。所以以上三种方式取出来的类对象,都是一样的。

    2、无论哪种途径获取类对象,都会导致静态属性被初始化,而且只会执行一次。(除了直接使用 Benz.class 类获取这种方式,这种方式不会导致静态属性被初始化)

    下面的流程会经常使用到 benz 实例和 benzClass 类对象

    4)、获取类名

    String className = benzClass.getSimpleName();
    System.out.println(className);

    //打印结果
    Benz

    5)、获取类路径

    String classPath1 = benzClass.getName();
    String classPath2 = benzClass.getCanonicalName();
    System.out.println(classPath1);
    System.out.println(classPath2);
    //打印结果
    com.dream.aptdemo.Benz
    com.dream.aptdemo.Benz

    这里可能大家会有个疑问:benzClass.getName() 和 benzClass.getCanonicalName() 有啥区别吗?

    从上面打印结果来看,没啥区别,但是如果我们在 Benz 这个里面加个内部类,然后获取内部类的路径,你就会看到区别了:

    //...
    class Benz extends Car implements ICar {
    //...
    class InnerClass{

    }
    }

    Class<Benz.InnerClass> innerClass = Benz.InnerClass.class;
    System.out.println(innerClass.getName());
    System.out.println(innerClass.getCanonicalName());
    //打印结果
    com.dream.aptdemo.Benz$InnerClass
    com.dream.aptdemo.Benz.InnerClass

    看到区别了吧,因此我们可以得到结论:在正常情况下,getCanonicalName和 getName 获取到的都是包含路径的类名。但内部类有点特殊,getName 获取的是路径.类名$内部类

    6)、获取父类名

    String fatherClassName = benzClass.getSuperclass().getSimpleName();
    System.out.println(fatherClassName);
    //打印结果
    Car

    7)、获取接口

    Class[] interfaces = benzClass.getInterfaces();
    for (Class anInterface : interfaces) {
    System.out.println(anInterface.getName());
    }
    //打印结果
    com.dream.aptdemo.ICar

    8)、创建实例对象

    //获取构造方法
    Constructor constructor = benzClass.getDeclaredConstructor();
    //创建实例
    Benz myBenz = (Benz) constructor.newInstance();
    //修改属性
    myBenz.carColor = "黑色";
    myBenz.combine();
    System.out.println(myBenz.carColor);
    //打印结果
    组装一台奔驰
    黑色

    注意:下面要讲的关于带 Declare 的属性和方法和不带Declare 区别:

    1、带 Declare 的属性和方法获取的是本类所有的属性和方法,不包含继承得来的

    2、不带 Declare 的属性和方法获取的是所有 public 修饰的属性和方法,包含继承得来的

    3、访问 private 修饰的属性和方法,需调用 setAccessible 设置为 true ,表示允许我们访问私有变量

    四、属性

    1)、获取单个属性

    Field carName = benzClass.getDeclaredField("carName");

    2)、获取多个属性

    //获取本类全部属性
    Field[] declaredFields = benzClass.getDeclaredFields();
    for (Field declaredField : declaredFields) {
    System.out.println("属性: " + declaredField.getName());
    }
    //打印结果
    属性: carName
    属性: carColor

    //获取本类及父类全部 public 修饰的属性
    Field[] fields = benzClass.getFields();
    for (Field field : fields) {
    System.out.println("属性: " + field.getName());
    }
    //打印结果
    属性: carColor
    属性: engine

    3)、设置允许访问私有变量

    carName.setAccessible(true);

    4)、获取属性名

    System.out.println(carName.getName());
    //打印结果
    carName

    5)、获取变量类型

    System.out.println(carName.getType().getName());
    //打印结果
    java.lang.String

    6)、获取对象中该属性的值

    System.out.println(carName.get(benz));
    //打印结果
    奔驰

    7)、给属性设置值

    carName.set(benz,"sweetying");
    System.out.println(carName.get(benz));
    //打印结果
    sweetying

    五、方法

    1)、获取单个方法

    //获取 public 方法
    Method publicMethod = benzClass.getMethod("combine");

    //获取 private 方法
    Method privateMethod = benzClass.getDeclaredMethod("privateMethod",String.class);

    2)、获取多个方法

    //获取本类全部方法
    Method[] declaredMethods = benzClass.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
    System.out.println("方法名: " + declaredMethod.getName());
    }
    //打印结果
    方法名: privateMethod
    方法名: combine


    //获取本类及父类全部 public 修饰的方法
    Method[] methods = benzClass.getMethods();
    for (Method method : methods) {
    System.out.println("方法名: " + method.getName());
    }
    //打印结果 因为所有类默认继承 Object , 所以打印了 Object 的一些方法
    方法名: combine
    方法名: run
    方法名: wait
    方法名: wait
    方法名: wait
    方法名: equals
    方法名: toString
    方法名: hashCode
    方法名: getClass
    方法名: notify
    方法名: notifyAll

    3)、方法调用

    Method privateMethod = benzClass.getDeclaredMethod("privateMethod",String.class);
    privateMethod.setAccessible(true);
    privateMethod.invoke(benz,"接收传入的参数");
    //打印结果
    我是私有方法: 接收传入的参数

    六、构造方法

    1)、获取单个构造方法

    //获取本类单个构造方法
    Constructor declaredConstructor = benzClass.getDeclaredConstructor(String.class);

    //获取本类单个 public 修饰的构造方法
    Constructor singleConstructor = benzClass.getConstructor(String.class,String.class);

    2)、获取多个构造方法

    //获取本类全部构造方法
    Constructor[] declaredConstructors = benzClass.getDeclaredConstructors();
    for (Constructor declaredConstructor1 : declaredConstructors) {
    System.out.println("构造方法: " + declaredConstructor1);
    }
    //打印结果
    构造方法: public com.dream.aptdemo.Benz()
    构造方法: public com.dream.aptdemo.Benz(java.lang.String,java.lang.String)
    构造方法: private com.dream.aptdemo.Benz(java.lang.String)


    //获取全部 public 构造方法, 不包含父类的构造方法
    Constructor[] constructors = benzClass.getConstructors();
    for (Constructor constructor1 : constructors) {
    System.out.println("构造方法: " + constructor1);
    }
    //打印结果
    构造方法: public com.dream.aptdemo.Benz()
    构造方法: public com.dream.aptdemo.Benz(java.lang.String,java.lang.String)

    3)、构造方法实例化对象

    //以上面 declaredConstructor 为例
    declaredConstructor.setAccessible(true);
    Benz declareBenz = (Benz) declaredConstructor.newInstance("");
    System.out.println(declareBenz.carColor);
    //打印结果
    白色

    //以上面 singleConstructor 为例
    Benz singleBenz = (Benz) singleConstructor.newInstance("奔驰 S ","香槟金");
    System.out.println(singleBenz.carColor);
    //打印结果
    香槟金

    七、泛型

    1)、获取父类的泛型

    Type genericType = benzClass.getGenericSuperclass();
    if (genericType instanceof ParameterizedType) {
    Type[] actualType = ((ParameterizedType) genericType).getActualTypeArguments();
    for (Type type : actualType) {
    System.out.println(type.getTypeName());
    }
    }
    //打印结果
    java.lang.String
    java.lang.Integer

    八、注解

    1)、获取单个注解

    //获取单个本类或父类注解
    Annotation annotation1 = benzClass.getAnnotation(CustomAnnotation1.class);
    System.out.println(annotation1.annotationType().getSimpleName());
    Annotation annotation3 = benzClass.getAnnotation(CustomAnnotation3.class);
    System.out.println(annotation3.annotationType().getSimpleName());
    //打印结果
    CustomAnnotation1
    CustomAnnotation3

    //获取单个本类注解
    Annotation declaredAnnotation1 = benzClass.getDeclaredAnnotation(CustomAnnotation2.class);
    System.out.println(declaredAnnotation1.annotationType().getSimpleName());
    //打印结果
    CustomAnnotation2

    2)、获取全部注解

    //获取本类和父类的注解(父类的注解需用 @Inherited 表示可被继承)
    Annotation[] annotations = benzClass.getAnnotations();
    for (Annotation annotation : annotations) {
    System.out.println("注解名称: " + annotation.annotationType().getSimpleName());
    }
    //打印结果
    注解名称: CustomAnnotation3
    注解名称: CustomAnnotation1
    注解名称: CustomAnnotation2

    //获取本类的注解
    Annotation[] declaredAnnotations = benzClass.getDeclaredAnnotations();
    for (Annotation declaredAnnotation : declaredAnnotations) {
    System.out.println("注解名称: " + declaredAnnotation.annotationType().getSimpleName());
    }
    //打印结果
    注解名称: CustomAnnotation1
    注解名称: CustomAnnotation2

    通过上面的讲解,我们把反射大部分知识点都讲完了,可以说反射是非常的强大,但是学习了之后,你可能会不知道该如何使用,反而觉得还不如直接调用方法来的直接和方便,下面我们通过实践来感受一下。

    九、反射实践

    需求大概就是:通过后台配置下发,完成 App 业务功能的切换。因为只是模拟,我们这里就以通过读取本地配置文件完成 App 业务功能的切换:

    1)、首先准备两个业务类,假设他们的功能都很复杂

    //包名
    package com.dream.aptdemo;

    //业务1
    class Business1 {

    public void doBusiness1Function(){
    System.out.println("复杂业务功能1");
    }
    }

    //业务2
    class Business2 {

    public void doBusiness2Function(){
    System.out.println("复杂业务功能2");
    }
    }

    2)、非反射方式

    public class Client {

    @Test
    public void test() {
    //业务功能1
    new Business1().doBusiness1Function();
    }
    }

    假设这个时候需要从第一个业务功能切换到第二个业务功能,使用非反射方式,必须修改代码,并且重新编译运行,才可以达到效果。那么我们可以通过反射去通过读取配置从而完成功能的切换,这样我们就不需要修改代码且代码变得更加通用

    3)、反射方式

    1、首先准备一个配置文件,如下图:

    image-20210625180301557

    2、读取配置文件,反射创建实例并调用方法

    public class Client {

    @Test
    public void test() throws Exception {
    try {
    //获取文件
    File springConfigFile = new File("/Users/zhouying/AndroidStudioProjects/AptDemo/config.txt");
    //读取配置
    Properties config= new Properties();
    config.load(new FileInputStream(springConfigFile));
    //获取类路径
    String classPath = (String) config.get("class");
    //获取方法名
    String methodName = (String) config.get("method");

    //反射创建实例并调用方法
    Class aClass = Class.forName(classPath);
    Constructor declaredConstructor = aClass.getDeclaredConstructor();
    Object o = declaredConstructor.newInstance();
    Method declaredMethod = aClass.getDeclaredMethod(methodName);
    declaredMethod.invoke(o);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    3、完成上面两步后,后续我们就只需要修改配置文件就能完成 App 业务功能的切换了

    十、总结

    本篇文章讲的一些重点内容:

    1、反射常用 Api 的使用,注意在访问私有属性和方法时,调用 setAccessible 设置为 true ,表示允许我们访问私有变量

    2、实践通过反射完成 App 业务功能的切换

    收起阅读 »

    Android APT 系列 (二):APT 筑基之注解

    前言很高兴遇见你~在本系列的上一篇文章中,我们对反射一些常用的知识进行了讲解,还没有看过上一篇文章的朋友,建议先去阅读 Android APT 系列 (一):APT 筑基之反射。接下来我们看下 Java 注解Github Demo 地址 ,...
    继续阅读 »

    前言

    很高兴遇见你~

    在本系列的上一篇文章中,我们对反射一些常用的知识进行了讲解,还没有看过上一篇文章的朋友,建议先去阅读 Android APT 系列 (一):APT 筑基之反射。接下来我们看下 Java 注解

    Github Demo 地址 , 大家可以看 Demo 跟随我的思路一起分析

    一、注解介绍

    1)、什么是注解?

    要解释注解我们首先要明白什么是元数据:元数据就是为其他数据提供信息的数据

    那么还是引入官方一段对注解的解释:注解用于为代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 JDK 1.5 开始添加到 Java 的。

    简单的理解:注解就是附加到代码上的一种额外补充信息

    2)、注解有哪些作用?

    源码阶段注解: 编译器可利用该阶段注解检测错误,提示警告信息,打印日志等

    编译阶段注解:利用注解信息自动生成代码、文档或者做其它相应的自动处理

    运行阶段注解: 可通过反射获取注解信息,做相应操作

    3)、如何自定义定义一个注解?

    使用 @interface + 注解名称这种语法结构就能定义一个注解,如下:

    @interface TestAnnotation{

    }

    通常我们会使用一些元注解来修饰自定义注解

    二、元注解

    了解了之前的元数据,元注解就是为注解提供注解的注解 😂,这句话可能有点绕,反正你清楚元注解是给注解用的就行了

    JDK 给我们提供的元注解有如下几个:

    1、@Target

    2、@Retention

    3、@Inherited

    4、@Documented

    5、@Repeatable

    1)、@Target

    @Target 表示这个注解能放在什么位置上,具体选择的位置列表如下:

    ElementType.ANNOTATION_TYPE //能修饰注解
    ElementType.CONSTRUCTOR //能修饰构造器
    ElementType.FIELD //能修饰成员变量
    ElementType.LOCAL_VARIABLE //能修饰局部变量
    ElementType.METHOD //能修饰方法
    ElementType.PACKAGE //能修饰包名
    ElementType.PARAMETER //能修饰参数
    ElementType.TYPE //能修饰类、接口或枚举类型
    ElementType.TYPE_PARAMETER //能修饰泛型,如泛型方法、泛型类、泛型接口 (jdk1.8加入)
    ElementType.TYPE_USE //能修饰类型 可用于任意类型除了 class (jdk1.8加入)

    @Target(ElementType.TYPE)
    @interface TestAnnotation{

    }

    注意:默认情况下无限制

    2)、@Retention

    @Retention 表示注解的的生命周期,可选的值有 3 个:

    RetentionPolicy.SOURCE //表示注解只在源码中存在,编译成 class 之后,就没了

    RetentionPolicy.CLASS //表示注解在 java 源文件编程成 .class 文件后,依然存在,但是运行起来后就没了

    RetentionPolicy.RUNTIME //表示注解在运行起来后依然存在,程序可以通过反射获取这些信息

    @Retention(RetentionPolicy.RUNTIME)
    @interface TestAnnotation{

    }

    注意:默认情况下为 RetentionPolicy.CLASS

    3)、@Inherited

    @Inherited 表示该注解可被继承,即当一个子类继承一个父类,该父类添加的注解有被 @Inherited 修饰,那么子类就可以获取到该注解,否则获取不到

    @Inherited
    @interface TestAnnotation{

    }

    注意:默认情况下为不可继承

    4)、@Documented

    @Documented 表示该注解在通过 javadoc 命令生成 Api 文档后,会出现该注解的注释说明

    @Documented
    @interface TestAnnotation{

    }

    注意:默认情况下为不出现

    5)、@Repeatable

    @Repeatable 是 JDK 1.8 新增的元注解,它表示注解在同一个位置能出现多次,这个注解有点抽象,我们通过一个实际例子理解一下

    //游戏玩家注解
    @Inherited
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface GamePlayer{
    Game[] value();
    }

    //游戏注解
    @Inherited
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Repeatable(GamePlayer.class)
    @interface Game{
    String gameName();
    }

    @Game(gameName = "CF")
    @Game(gameName = "LOL")
    @Game(gameName = "DNF")
    class GameTest{

    }

    注意:默认情况下不可重复

    经验:通常情况下,我们会使用多个元注解组合来修饰自定义注解

    三、注解属性

    1)、注解属性类型

    注解属性类型可以为以下的一些类型:

    1、基本数据类型

    2、String

    3、枚举类型

    4、注解类型

    5、Class 类型

    6、以上类型的一维数组类型

    2)、定义注解属性

    首先我们定义一些注解属性,如下:

    @Inherited
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface TestAnnotation{
    //这就是注解属性的语法结构
    //定义一个属性并给了默认值
    String name() default "erdai";

    //定义一个属性未给默认值
    int age();
    }

    可能你会有些疑问:这难道不是在定义方法吗?还可以给默认值?

    这些疑问先留着,我们继续分析

    自定义注解默认都会继承 Annotation ,Annotation 是一个接口,源码如下:

    public interface Annotation {

    boolean equals(Object obj);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
    }

    我们知道,在接口中可以定义属性和方法,那么作为自定义注解,是否也可以定义呢?

    可以,接口中的属性默认都是用public static final 修饰的,默认是一个常量,对于自定义注解来说,这点没有任何区别。而接口中的方法其实就相当于自定义注解的属性,只不过自定义注解还可以给默认值。因此我们在学习自定义注解属性时,我们应该把它当作一个新知识,加上我刚才对接口的分析对比,你上面的那些疑问便可以迎刃而解了

    3)、注解属性使用

    1、在使用注解的后面接上一对括号,括号里面使用 属性名 = value 的格式,多个属性之间中间用 ,隔开

    2、未给默认值的属性必须进行赋值,否则编译器会报红

    //单个属性
    @TestAnnotation(age = 18)
    class Test{

    }

    //多个属性
    @TestAnnotation(age = 18,name = "erdai666")
    class Test{

    }

    4)、注解属性获取

    注解属性的获取可以参考我的上一篇文章 传送门 ,上篇文章我们讲的是通过类对象获取注解,咱们补充点上篇文章没讲到的

    1、我们在获取属性的时候,可以先判断一下是否存在该注解,增强代码的健壮性,如下:

    @TestAnnotation(age = 18,name = "erdai666")
    class Test{

    }

    Class<Test> testClass = Test.class;
    //获取当前注解是否存在
    boolean annotationPresent = testClass.isAnnotationPresent(TestAnnotation.class);
    //如果存在则进入条件体
    if(annotationPresent){
    TestAnnotation declaredAnnotation = testClass.getDeclaredAnnotation(TestAnnotation.class);
    System.out.println(declaredAnnotation.name());
    System.out.println(declaredAnnotation.age());
    }

    2、获取类属性的注解属性

    @Inherited
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface TestField{
    String filed();
    }

    class Test{
    @TestField(filed = "我是属性")
    public String test;
    }

    //通过反射获取属性注解
    Class<Test> testClass1 = Test.class;
    try {
    Field field = testClass1.getDeclaredField("test");
    if(field.isAnnotationPresent(TestField.class)){
    TestField fieldAnnotation = field.getDeclaredAnnotation(TestField.class);
    System.out.println(fieldAnnotation.filed());
    }
    } catch (NoSuchFieldException e) {
    e.printStackTrace();
    }
    //打印结果
    我是属性

    3、获取类方法的注解属性

    @Inherited
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @interface TestMethod{
    String method();
    }

    class Test{
    @TestMethod(method = "我是方法")
    public void test(){

    }
    }
    //通过反射获取方法注解
    Class<Test> testClass2 = Test.class;
    try {
    Method method = testClass2.getDeclaredMethod("test");
    if(method.isAnnotationPresent(TestMethod.class)){
    TestMethod methodAnnotation = method.getDeclaredAnnotation(TestMethod.class);
    System.out.println(methodAnnotation.method());
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    //打印结果
    我是方法

    四、JDK 提供的内置注解

    JDK 给我们提供了很多内置的注解,其中常用的有:

    )1、@Override

    2、@Deprecated

    3、@SuppressWarnings

    4、@FunctionalInterface

    1)、@Override

    @Override 用在方法上,表示这个方法重写了父类的方法,例如 toString 方法

    @Override
    public String toString() {
    return super.toString();
    }

    2)、@Deprecated

    @Deprecated 表示这个方法被弃用,不建议开发者使用

    image-20210626113644915

    可以看到用 @Deprecated 注解的方法调用的时候会被划掉

    3)、@SuppressWarnings

    @SuppressWarnings 用于忽略警告信息,常见的取值如下:

    • deprecation:使用了不赞成使用的类或方法时的警告(使用 @Deprecated 使得编译器产生的警告)
    • unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关闭编译器警告
    • fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告
    • path:在类路径、源文件路径等中有不存在的路径时的警告
    • serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告
    • finally:任何 finally 子句不能正常完成时的警告
    • rawtypes 泛型类型未指明
    • unused 引用定义了,但是没有被使用
    • all:关于以上所有情况的警告

    以泛型举个例子:

    image-20210626114048630

    当我们创建 List 未指定泛型时,编译器就会报黄提示我们未指明泛型,这个时候就可以使用这个注解了:

    image-20210626114241155

    4)、@FunctionalInterface

    @FunctionalInterface 是 JDK 1.8 新增的注解,用于约定函数式接口,函数式接口就是接口中只有一个抽象方法

    @FunctionalInterface
    interface testInterface{
    void testMethod();
    }

    而当你有两个抽象方法时,注解会报红提示你:

    image-20210626114855416

    五、注解实际应用场景

    1)、使用自定义注解代替枚举类型

    主要针对源码阶段注解

    这个在我们实际工作中也挺常用的,使用枚举类型开销大,我们一般都会使用自定义注解进行替代,如下:

    //1、使用枚举
    enum EnumFontType{
    ROBOTO_REGULAR,ROBOTO_MEDIUM,ROBOTO_BOLD
    }
    //实际调用
    EnumFontType type1 = EnumFontType.ROBOTO_BOLD;

    //================================ 完美的分割线 ==================================
    //2、使用自定义注解
    @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({AnnotationFontType.ROBOTO_REGULAR,AnnotationFontType.ROBOTO_MEDIUM,AnnotationFontType.ROBOTO_BOLD})
    @interface AnnotationFontType{
    int ROBOTO_REGULAR = 1;
    int ROBOTO_MEDIUM = 2;
    int ROBOTO_BOLD = 3;
    }
    //实际调用
    @AnnotationFontType int type2 = AnnotationFontType.ROBOTO_MEDIUM;

    2)、注解处理器 (APT)

    主要针对编译阶段注解

    实际我们日常开发中,经常会遇到它,因为我们常用的一些开源库如 ButterKnife,Retrofit,Arouter,EventBus 等等都使用到了 APT 技术。也正是因为这些著名的开源库,才使得 APT 技术越来越火,在本系列的下一篇中,我也会讲到。

    3)、运行时注解处理

    主要针对运行阶段注解

    举个实际的例子:例如我们开车去自助加油机加油,设定的 Money 是 200,如果少于 200 则提示 加油中...,否则提示 油已加满,如果出现异常情况,提示 加油失败

    现在我们通过注解来实现一下它,如下:

    @Inherited
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @interface OilAnnotation{
    double maxOilMoney() default 0;
    }

    class GasStation{

    @OilAnnotation(maxOilMoney = 200)
    public void addOil(double money){
    String tips = processOilAnnotation(money);
    System.out.println(tips);
    }

    @SuppressWarnings("all")
    private String processOilAnnotation(double money){
    try {
    Class<GasStation> aClass = GasStation.class;
    //获取当前方法的注解
    Method addOilMethod = aClass.getDeclaredMethod("addOil", double.class);
    //获取方法注解是否存在
    boolean annotationPresent = addOilMethod.isAnnotationPresent(OilAnnotation.class);
    if(annotationPresent){
    OilAnnotation oilAnnotation = addOilMethod.getDeclaredAnnotation(OilAnnotation.class);
    if(money >= oilAnnotation.maxOilMoney()){
    return "油已加满";
    }else {
    return "加油中...";
    }
    }
    } catch (NoSuchMethodException e) {
    e.printStackTrace();
    }
    return "加油失败";
    }
    }

    new GasStation().addOil(100);
    //打印结果
    加油中...

    new GasStation().addOil(200);
    //打印结果
    油已加满

    六、总结

    本篇文章讲的一些重点内容:

    1、自定义注解时,元注解的组合使用

    2、注解属性的定义,使用和获取

    3、一些常用的 JDK 内置注解

    4、注解的实际应用及运行阶段注解的一个实践

    收起阅读 »

    Android APT 系列 (三):APT 技术探究

    前言很高兴遇见你~在本系列的上一篇文章中,我们对注解进行了讲解,还没有看过上一篇文章的朋友,建议先去阅读 Android APT 系列 (二):APT 筑基之注解。至此,关于 Apt 基础部分我们都讲完了,接下来就正式进入 APT 技术的学习Github De...
    继续阅读 »

    前言

    很高兴遇见你~

    在本系列的上一篇文章中,我们对注解进行了讲解,还没有看过上一篇文章的朋友,建议先去阅读 Android APT 系列 (二):APT 筑基之注解。至此,关于 Apt 基础部分我们都讲完了,接下来就正式进入 APT 技术的学习

    Github Demo 地址 , 大家可以看 Demo 跟随我的思路一起分析

    一、APT 介绍

    1)、什么是 APT ?

    APT 全称 Annotation Processing Tool,翻译过来即注解处理器。引用官方一段对 APT 的介绍:APT 是一种处理注释的工具, 它对源代码文件进行检测找出其中的注解,并使用注解进行额外的处理。

    2)、APT 有什么用?

    APT 能在编译期根据编译阶段注解,给我们自动生成代码,简化使用。很多流行框架都使用到了 APT 技术,如 ButterKnife,Retrofit,Arouter,EventBus 等等

    二、APT 工程

    1)、APT 工程创建

    一般情况下,APT 大致的的一个实现过程:

    1、创建一个 Java Module ,用来编写注解

    2、创建一个 Java Module ,用来读取注解信息,并根据指定规则,生成相应的类文件

    3、创建一个 Android Module ,通过反射获取生成的类,进行合理的封装,提供给上层调用

    如下图:

    image-20210627182425586

    这是我的 APT 工程,关于 Module 名称可以任意取,按照我上面说的规则去进行就好了

    2)、Module 依赖

    工程创建好后,我们就需要理清楚各个 Module 之间的一个依赖关系:

    1、因为 apt-processor 要读取 apt-annotation 的注解,所以 apt-processor 需要依赖 apt-annotation

    //apt-processor 的 build.gradle 文件
    dependencies {
    implementation project(path: ':apt-annotation')
    }

    2、app 作为调用层,以上 3 个 Module 都需要进行依赖

    //app 的 build.gradle 文件
    dependencies {
    //...
    implementation project(path: ':apt-api')
    implementation project(path: ':apt-annotation')
    annotationProcessor project(path: ':apt-processor')
    }

    APT 工程配置好之后,我们就可以对各个 Module 进行一个具体代码的编写了

    三、apt-annotation 注解编写

    这个 Module 的处理相对来说很简单,就是编写相应的自定义注解就好了,我编写的如下:

    @Inherited
    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target({ElementType.TYPE,ElementType.METHOD})
    public @interface AptAnnotation {
    String desc() default "";
    }

    四、apt-processor 自动生成代码

    这个 Module 相对来说比较复杂,我们把它分为以下 3 个步骤:

    1、注解处理器声明

    2、注解处理器注册

    3、注解处理器生成类文件

    1)、注解处理器声明

    1、新建一个类,类名按照自己的喜好取,继承 javax.annotation.processing 这个包下的 AbstractProcessor 类并实现其抽象方法

    public class AptAnnotationProcessor extends AbstractProcessor {

    /**
    * 编写生成 Java 类的相关逻辑
    *
    * @param set 支持处理的注解集合
    * @param roundEnvironment 通过该对象查找指定注解下的节点信息
    * @return true: 表示注解已处理,后续注解处理器无需再处理它们;false: 表示注解未处理,可能要求后续注解处理器处理
    */

    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
    return false;
    }
    }

    重点看下第一个参数中的 TypeElement ,这个就涉及到 Element 的知识,我们简单的介绍一下:

    Element 介绍

    实际上,Java 源文件是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element ,例如包,类,字段,方法等等:

    package com.dream;         // PackageElement:包元素

    public class Main<T> { // TypeElement:类元素; 其中 属于 TypeParameterElement 泛型元素

    private int x; // VariableElement:变量、枚举、方法参数元素

    public Main() { // ExecuteableElement:构造函数、方法元素
    }
    }

    Java 的 Element 是一个接口,源码如下:

    public interface Element extends javax.lang.model.AnnotatedConstruct {
    // 获取元素的类型,实际的对象类型
    TypeMirror asType();
    // 获取Element的类型,判断是哪种Element
    ElementKind getKind();
    // 获取修饰符,如public static final等关键字
    Set getModifiers();
    // 获取类名
    Name getSimpleName();
    // 返回包含该节点的父节点,与getEnclosedElements()方法相反
    Element getEnclosingElement();
    // 返回该节点下直接包含的子节点,例如包节点下包含的类节点
    List getEnclosedElements();

    @Override
    boolean equals(Object obj);

    @Override
    int hashCode();

    @Override
    List getAnnotationMirrors();

    //获取注解
    @Override
    A getAnnotation(Class annotationType);

    R accept(ElementVisitor v, P p);
    }

    我们可以通过 Element 获取如上一些信息(写了注释的都是一些常用的)

    由 Element 衍生出来的扩展类共有 5 种:

    1、PackageElement 表示一个包程序元素

    2、TypeElement 表示一个类或者接口程序元素

    3、TypeParameterElement 表示一个泛型元素

    4、VariableElement 表示一个字段、enum 常量、方法或者构造方法的参数、局部变量或异常参数

    5、ExecuteableElement 表示某个类或者接口的方法、构造方法或初始化程序(静态或者实例)

    可以发现,Element 有时会代表多种元素,例如 TypeElement 代表类或接口,此时我们可以通过 element.getKind() 来区分:

    Set elements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);
    for (Element element : elements) {
    if (element.getKind() == ElementKind.CLASS) {
    // 如果元素是类

    } else if (element.getKind() == ElementKind.INTERFACE) {
    // 如果元素是接口

    }
    }

    ElementKind 是一个枚举类,它的取值有很多,如下:

    PACKAGE	//表示包
    ENUM //表示枚举
    CLASS //表示类
    ANNOTATION_TYPE //表示注解
    INTERFACE //表示接口
    ENUM_CONSTANT //表示枚举常量
    FIELD //表示字段
    PARAMETER //表示参数
    LOCAL_VARIABLE //表示本地变量
    EXCEPTION_PARAMETER //表示异常参数
    METHOD //表示方法
    CONSTRUCTOR //表示构造函数
    OTHER //表示其他

    关于 Element 就介绍到这,我们接着往下看

    2、重写方法解读

    除了必须实现的这个抽象方法,我们还可以重写其他 4 个常用的方法,如下:

    public class AptAnnotationProcessor extends AbstractProcessor {
    //...

    /**
    * 节点工具类(类、函数、属性都是节点)
    */

    private Elements mElementUtils;

    /**
    * 类信息工具类
    */

    private Types mTypeUtils;

    /**
    * 文件生成器
    */

    private Filer mFiler;

    /**
    * 日志信息打印器
    */

    private Messager mMessager;

    /**
    * 做一些初始化的工作
    *
    * @param processingEnvironment 这个参数提供了若干工具类,供编写生成 Java 类时所使用
    */

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    mElementUtils = processingEnv.getElementUtils();
    mTypeUtils = processingEnv.getTypeUtils();
    mFiler = processingEnv.getFiler();
    mMessager = processingEnv.getMessager();
    }

    /**
    * 接收外来传入的参数,最常用的形式就是在 build.gradle 脚本文件里的 javaCompileOptions 的配置
    *
    * @return 属性的 Key 集合
    */

    @Override
    public Set getSupportedOptions() {
    return super.getSupportedOptions();
    }

    /**
    * 当前注解处理器支持的注解集合,如果支持,就会调用 process 方法
    *
    * @return 支持的注解集合
    */

    @Override
    public Set getSupportedAnnotationTypes() {
    return super.getSupportedAnnotationTypes();
    }

    /**
    * 编译当前注解处理器的 JDK 版本
    *
    * @return JDK 版本
    */

    @Override
    public SourceVersion getSupportedSourceVersion() {
    return super.getSupportedSourceVersion();
    }
    }

    注意getSupportedAnnotationTypes()getSupportedSourceVersion()getSupportedOptions() 这三个方法,我们还可以采用注解的方式进行提供:

    @SupportedOptions("MODULE_NAME")
    @SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class AptAnnotationProcessor extends AbstractProcessor {
    //...
    }

    2)、注解处理器注册

    注解处理器声明好了,下一步我们就要注册它,其中注册有两种方式:

    1、手动注册

    2、自动注册

    手动注册比较繁琐固定且容易出错,不推荐使用,这里就不讲了。我们主要看下自动注册

    自动注册

    1、首先我们要在 apt-processor这个 Module 下的 build.gradle 文件导入如下依赖:

    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

    注意:这两句必须都要加,否则注册不成功,我之前踩坑了

    2、在注解处理器上加上 @AutoService(Processor.class) 即可完成注册

    @AutoService(Processor.class)
    public class AptAnnotationProcessor extends AbstractProcessor {
    //...
    }

    3)、注解处理器生成类文件

    注册完成之后,我们就可以正式编写生成 Java 类文件的代码了,其中生成也有两种方式:

    1、常规的写文件方式

    2、通过 javapoet 框架来编写

    1 的方式比较死板,需要把每一个字母都写上,不推荐使用,这里就不讲了。我们主要看下通过 javapoet 这个框架生成 Java 类文件

    javapoet 方式

    这种方式更加符合面向对象编码的一个风格,对 javapoet 还不熟的朋友,可以去 github 上学习一波 传送门,这里我们介绍一下它常用的一些类:

    TypeSpec:用于生成类、接口、枚举对象的类

    MethodSpec:用于生成方法对象的类

    ParameterSpec:用于生成参数对象的类

    AnnotationSpec:用于生成注解对象的类

    FieldSpec:用于配置生成成员变量的类

    ClassName:通过包名和类名生成的对象,在JavaPoet中相当于为其指定 Class

    ParameterizedTypeName:通过 MainClass 和 IncludeClass 生成包含泛型的 Class

    JavaFile:控制生成的 Java 文件的输出的类

    1、导入 javapoet 框架依赖
    implementation 'com.squareup:javapoet:1.13.0'
    2、按照指定代码模版生成 Java 类文件

    例如,我在 app 的 build.gradle 下进行了如下配置:

    android {
    //...
    defaultConfig {
    //...
    javaCompileOptions {
    annotationProcessorOptions {
    arguments = [MODULE_NAME: project.getName()]
    }
    }
    }
    }

    在 MainActivity 下面进行了如下注解:

    image-20210627212604288

    我希望生成的代码如下:

    image-20210627220320906

    现在我们来实操一下:

    @AutoService(Processor.class)
    @SupportedOptions("MODULE_NAME")
    @SupportedAnnotationTypes("com.dream.apt_annotation.AptAnnotation")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class AptAnnotationProcessor extends AbstractProcessor {

    //文件生成器
    Filer filer;
    //模块名
    private String mModuleName;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    //初始化文件生成器
    filer = processingEnvironment.getFiler();
    //通过 key 获取 build.gradle 中对应的 value
    mModuleName = processingEnv.getOptions().get("MODULE_NAME");
    }

    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
    if (set == null || set.isEmpty()) {
    return false;
    }

    //获取当前注解下的节点信息
    Set rootElements = roundEnvironment.getElementsAnnotatedWith(AptAnnotation.class);

    // 构建 test 函数
    MethodSpec.Builder builder = MethodSpec.methodBuilder("test")
    .addModifiers(Modifier.PUBLIC) // 指定方法修饰符
    .returns(void.class) // 指定返回类型
    .addParameter(String.class, "param"); // 添加参数
    builder.addStatement("$T.out.println($S)", System.class, "模块: " + mModuleName);

    if (rootElements != null && !rootElements.isEmpty()) {
    for (Element element : rootElements) {
    //当前节点名称
    String elementName = element.getSimpleName().toString();
    //当前节点下注解的属性
    String desc = element.getAnnotation(AptAnnotation.class).desc();
    // 构建方法体
    builder.addStatement("$T.out.println($S)", System.class,
    "节点: " + elementName + " " + "描述: " + desc);
    }
    }
    MethodSpec main =builder.build();

    // 构建 HelloWorld 类
    TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC) // 指定类修饰符
    .addMethod(main) // 添加方法
    .build();

    // 指定包路径,构建文件体
    JavaFile javaFile = JavaFile.builder("com.dream.aptdemo", helloWorld).build();
    try {
    // 创建文件
    javaFile.writeTo(filer);
    } catch (IOException e) {
    e.printStackTrace();
    }

    return true;
    }
    }

    经过上面这些步骤,我们运行 App 就能生成上面截图的代码了,现在还差最后一步,对生成的代码进行使用

    注意:不同版本的 Gradle 生成的类文件位置可能不一样,我的 Gradle 版本是 6.7.1,生成的类文件在如下位置:

    image-20210627221836736

    一些低版本的 Gradle 生成的类文件在 /build/source 这个目录下

    五、apt-api 调用生成代码完成业务功能

    这个 Module 的操作相对来说也比较简单,就是通过反射获取到生成的类,进行相应的封装使用即可,我的编写如下:

    public class MyAptApi {

    @SuppressWarnings("all")
    public static void init() {
    try {
    Class c = Class.forName("com.dream.aptdemo.HelloWorld");
    Constructor declaredConstructor = c.getDeclaredConstructor();
    Object o = declaredConstructor.newInstance();
    Method test = c.getDeclaredMethod("test", String.class);
    test.invoke(o, "");
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    接着我们在 MainActivity 的 oncreate 方法里面进行调用:

    @AptAnnotation(desc = "我是 MainActivity 上面的注解")
    public class MainActivity extends AppCompatActivity {

    @AptAnnotation(desc = "我是 onCreate 上面的注解")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    MyAptApi.init();
    }
    }
    //打印结果
    模块: app
    节点: MainActivity 描述: 我是 MainActivity 上面的注解
    节点: onCreate 描述: 我是 onCreate 上面的注解

    六、总结

    本篇文章讲的一些重点内容:

    1、APT 工程所需创建的不同种类的 Module 及 Module 之间的依赖关系

    2、Java 源文件实际上是一种结构体语言,源代码的每一个部分都对应了一个特定类型的 Element

    3、采用 auto-service 对注解处理器进行自动注册

    4、采用 javapoet 框架编写所需生成的 Java 类文件

    5、通过反射及适当的封装,将生成的类的功能提供给上层调用

    收起阅读 »

    Android APT 系列 (四):APT 实战应用

    前言很高兴遇见你~在本系列的上一篇文章中,我们对 APT 技术进行了讲解,还没有看过上一篇文章的朋友,建议先去阅读 Android APT 系列 (三):APT 技术探究。接下来,我们就使用 APT 技术来进行实战应用。Github Demo 地址 , 大家可...
    继续阅读 »

    前言

    很高兴遇见你~

    在本系列的上一篇文章中,我们对 APT 技术进行了讲解,还没有看过上一篇文章的朋友,建议先去阅读 Android APT 系列 (三):APT 技术探究。接下来,我们就使用 APT 技术来进行实战应用。

    Github Demo 地址 , 大家可以看 Demo 跟随我的思路一起分析

    回顾

    在本系列的开篇,我讲了在项目实践过程中做的一个布局优化,Android 中少量的系统控件是通过 new 的方式创建出来的,而大部分控件如 androidx.appcompat.widget 下的控件,自定义控件,第三方控件等等,都是通过反射创建的。大量的反射创建多多少少会带来一些性能问题,因此我们需要去解决反射创建的问题,我的解决思路是:

    1、通过编写 Android 插件获取 Xml 布局中的所有控件

    2、拿到控件后,通过 APT 生成用 new 的方式创建 View 的类

    3、最后通过反射获取当前类并在基类里面完成替换

    一、准备 Android 插件生成的文件

    其中 1 的具体流程是:通过 Android 插件获取所有 Xml 布局中的控件名称,并写入到一个.txt文件中,因 Gradle 系列还没讲,这里我们假设所有的控件名称已经写入到.txt文件,如下:

    image-20210629191446005

    上述文件我们可以看到:

    1、一些不带 . 的系统控件,如 TextView,ImageView 。系统会默认给我们通过 new 的方式去创建,且替换为了androidx.appcompat.widget包下的控件,例如:TextView -> AppCompatTextView ,ImageView -> AppCompatImageView

    2、带 . 的控件。可能为 androidx.appcompat.widget 下的控件,自定义控件,第三方控件等等,这些控件如果我们不做处理,系统会通过反射去创建。因此我们主要是针对这些控件去做处理

    注意:我这里在根目录下创建了一个 all_view_name.txt 的文件,然后放入了一些 View 的名称,这里只是方便我们演示。实际上用 Android 插件去生成的文件我们一般会指定放在 app 的 /build目录下,这样我们在 clean 的时候就能顺带把它给干掉

    现在 1 完成了,接下来 2 和 3 就回到了我们熟悉的 APT 流程,我们需要读取该文件,通过 APT 生成相应的类,最后使用这个类的功能就 OK 了,还不熟悉 APT 的,先去学习一波 传送门

    还是基于上篇文章的工程进行实操,为了方便后续流程的讲解,我还是贴出上篇文章的工程图:

    image-20210627182425586

    二、apt-annotation 注解编写

    编写注解,如下:

    @Inherited
    @Documented
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.TYPE)
    public @interface ViewCreator {

    }

    三、规定生成的类模版,为后续自动生成代码做准备

    在实际工作中,我们一般会这么做:

    1、将需要生成的类文件实现某个定义好的接口,通过接口代理来使用

    2、规定生成的 Java 类模版,根据模版去进行生成代码逻辑的编写

    1、将需要生成的类文件实现某个定义好的接口,通过接口代理来使用

    关于接口,我们一般会放到 apt-api 这个 Module 中

    2、规定生成的 Java 类模版,根据模版去进行生成代码逻辑的编写

    假设我们需要生成的 Java 类模版如下:

    package com.dream.aptdemo;

    public class MyViewCreatorImpl implements IMyViewCreator {
    @Override
    public View createView(String name, Context context, AttributeSet attr) {
    View view = null;
    switch(name) {
    case "androidx.core.widget.NestedScrollView":
    view = new NestedScrollView(context,attr);
    break;
    case "androidx.constraintlayout.widget.ConstraintLayout":
    view = new ConstraintLayout(context,attr);
    break;
    case "androidx.appcompat.widget.ButtonBarLayout":
    view = new ButtonBarLayout(context,attr);
    break;
    //...
    default:
    break;
    }
    return view;
    }

    根据上面这些信息,我们就可以进行自动生成代码逻辑的编写了

    四、apt-processor 自动生成代码

    这里你就对着上面给出的代码模版,通过 javapoet 框架编写相应的代码生成逻辑即可,对 javapoet 不熟的赶紧去学习一波 传送门

    @AutoService(Processor.class)
    @SupportedAnnotationTypes("com.dream.apt_annotation.ViewCreator")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class MyViewCreatorProcessor extends AbstractProcessor {

    /**文件生成器*/
    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    mFiler = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
    //从文件中读取控件名称,并转换成对应的集合
    Set mViewNameSet = readViewNameFromFile();
    //如果获取的控件名称集合为空,则终止流程
    if(mViewNameSet == null || mViewNameSet.isEmpty()){
    return false;
    }

    //获取使用了注解的元素
    Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(ViewCreator.class);
    for (Element element : elementsAnnotatedWith) {
    System.out.println("Hello " + element.getSimpleName() + ", 欢迎使用 APT");
    startGenerateCode(mViewNameSet);
    //如果有多个地方标注了注解,我们只读取第一次的就行了
    break;
    }
    return true;
    }

    /**
    * 开始执行生成代码的逻辑
    *
    * @param mViewNameSet 控件名称集合
    */

    private void startGenerateCode(Set mViewNameSet) {
    System.out.println("开始生成 Java 类...");
    System.out.println("a few moment later...");
    //=================================== 构建方法 start ======================================
    //1、构建方法:方法名,注解,修饰符,返回值,参数

    ClassName viewType = ClassName.get("android.view","View");
    MethodSpec.Builder methodBuilder = MethodSpec
    //方法名
    .methodBuilder("createView")
    //注解
    .addAnnotation(Override.class)
    //修饰符
    .addModifiers(Modifier.PUBLIC)
    //返回值
    .returns(viewType)
    //第一个参数
    .addParameter(String.class,"name")
    //第二个参数
    .addParameter(ClassName.get("android.content","Context"),"context")
    //第三个参数
    .addParameter(ClassName.get("android.util","AttributeSet"),"attr");

    //2、构建方法体
    methodBuilder.addStatement("$T view = null",viewType);
    methodBuilder.beginControlFlow("switch(name)");
    //循环遍历控件名称集合
    for (String viewName : mViewNameSet) {
    //针对包含 . 的控件名称进行处理
    if(viewName.contains(".")){
    //分离包名和控件名,如:androidx.constraintlayout.widget.ConstraintLayout
    //packageName:androidx.constraintlayout.widget
    //simpleViewName:ConstraintLayout
    String packageName = viewName.substring(0,viewName.lastIndexOf("."));
    String simpleViewName = viewName.substring(viewName.lastIndexOf(".") + 1);
    ClassName returnType = ClassName.get(packageName, simpleViewName);

    methodBuilder.addCode("case $S:\n",viewName);
    methodBuilder.addStatement("\tview = new $T(context,attr)", returnType);
    methodBuilder.addStatement("\tbreak");
    }
    }
    methodBuilder.addCode("default:\n");
    methodBuilder.addStatement("\tbreak");
    methodBuilder.endControlFlow();
    methodBuilder.addStatement("return view");

    MethodSpec createView = methodBuilder.build();
    //=================================== 构建方法 end ======================================

    //=================================== 构建类 start ======================================
    TypeSpec myViewCreatorImpl = TypeSpec.classBuilder("MyViewCreatorImpl")
    //类修饰符
    .addModifiers(Modifier.PUBLIC)
    //实现接口
    .addSuperinterface(ClassName.get("com.dream.apt_api", "IMyViewCreator"))
    //添加方法
    .addMethod(createView)
    .build();
    //=================================== 构建类 end ========================================

    //=================================== 指定包路径,构建文件体 start =========================
    //指定类包路径
    JavaFile javaFile = JavaFile.builder("com.dream.aptdemo",myViewCreatorImpl).build();
    //生成文件
    try {
    javaFile.writeTo(mFiler);
    System.out.println("生成成功...");
    } catch (IOException e) {
    e.printStackTrace();
    System.out.println("生成失败...");
    }
    //=================================== 指定包路径,构建文件体 end ============================
    }

    /**
    * 从文件中读取控件名称,并转换成对应的集合
    */

    private Set readViewNameFromFile() {
    try {
    //获取存储控件名称的文件
    File file = new File("/Users/zhouying/AndroidStudioProjects/AptDemo/all_view_name.txt");
    Properties config = new Properties();
    config.load(new FileInputStream(file));
    //获取控件名称集合
    return config.stringPropertyNames();
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }
    }

    上述生成代码的逻辑写了详细的注释,主要就是对 javapoet 框架的一个应用

    代码生成好了,接下来就需要提供给上层使用

    五、apt-api 业务封装供上层使用

    1、定义一个接口, apt-api 和 apt-processor 都会使用到

    //定义一个接口
    public interface IMyViewCreator {
    /**
    * 通过 new 的方式创建 View
    *
    * @param name 控件名称
    * @param context 上下文
    * @param attributeSet 属性
    */

    View createView(String name, Context context, AttributeSet attributeSet);
    }

    2、反射获取生成的类,提供相应的代理类供上层调用

    public class MyViewCreatorDelegate implements IMyViewCreator{

    private IMyViewCreator mIMyViewCreator;

    //================================== 单例 start =====================================
    @SuppressWarnings("all")
    private MyViewCreatorDelegate(){
    try {
    // 通过反射拿到 Apt 生成的类
    Class aClass = Class.forName("com.dream.aptdemo.MyViewCreatorImpl");
    mIMyViewCreator = (IMyViewCreator) aClass.newInstance();
    } catch (Throwable t) {
    t.printStackTrace();
    }
    }

    public static MyViewCreatorDelegate getInstance(){
    return Holder.MY_VIEW_CREATOR_DELEGATE;
    }

    private static final class Holder{
    private static final MyViewCreatorDelegate MY_VIEW_CREATOR_DELEGATE = new MyViewCreatorDelegate();
    }
    //================================== 单例 end =======================================


    /**
    * 通过生成的类创建 View
    *
    * @param name 控件名称
    * @param context 上下文
    * @param attributeSet 属性
    * @return View
    */

    @Override
    public View createView(String name, Context context, AttributeSet attributeSet) {
    if(mIMyViewCreator != null){
    return mIMyViewCreator.createView(name, context, attributeSet);
    }
    return null;
    }
    }

    到这里我们布局优化流程差不多就要结束了,接下来就是上层调用

    六、app 上层调用

    1、在创建的 MyApplication 上添加注解

    关于注解你可以添加在其他地方,因为我注解处理器里面做了逻辑判断,只会读取第一次的注解。为了对应,我选择把注解加到 MyApplication 中,如下图:

    image-20210629192519893

    2、最后在 MainActviity 中加入替换 View 的逻辑

    如下:

    //...
    public class MainActivity extends AppCompatActivity {

    //...
    @Nullable
    @Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    //1、优先使用我们生成的类去进行 View 的创建
    View view = MyViewCreatorDelegate.getInstance().createView(name, context, attrs);
    if (view != null) {
    return view;
    }
    //2、一些系统的 View ,则走系统的一个创建流程
    return super.onCreateView(name, context, attrs);
    }
    }

    注意:一般我们会把替换 View 的逻辑放到基类里面

    七、效果验证

    运行项目

    1、先看下我们打印的日志,如下图:

    image-20210629195411055

    2、在看一眼我们生成的 Java 类文件,如下图:

    image-20210629194711378

    3、最后 debug 项目跟下流程,发现和我们预期的一致,如下图:

    image-20210629194101025

    至此,需求完结

    八、总结

    本篇文章讲的一些重点内容:

    1、通过 APT 读取文件获取所有的控件名称并生成 Java 类

    2、通过接口代理,合理的业务封装提供给上层调用

    3、在上层 Application 里面进行注解,在 Activity 中进行 View 控件的替换

    4、实际完成后的一个效果验证

    收起阅读 »

    使用Jetpack Compose完成自定义手势处理

    概述Jetpack Compose 为我们提供了许多手势处理 Modifier,对于常见业务需求来说已足够我们使用了,然而如果说我们对手势有定制需求,就需要具备自定义手势处理的能力了。通过使用官方所提供的基础 API 来完成各类手势交互需求,触摸反馈基础 AP...
    继续阅读 »

    概述

    Jetpack Compose 为我们提供了许多手势处理 Modifier,对于常见业务需求来说已足够我们使用了,然而如果说我们对手势有定制需求,就需要具备自定义手势处理的能力了。通过使用官方所提供的基础 API 来完成各类手势交互需求,触摸反馈基础 API 类似传统 View 系统的 onTouchEvent()。 当然 Compose 中也支持类似传统 ViewGroup 通过 onInterceptTouchEvent()定制手势事件分发流程。通过对自定义手势处理的学习将帮助大家掌握处理绝大多数场景下手势需求的能力。

    使用 PointerInput Modifier

    对于所有手势操作的处理都需要封装在这个 Modifier 中,我们知道 Modifier 是用来修饰 UI 组件的,所以将手势操作的处理封装在 Modifier 符合开发者设计直觉,这同时也做到了手势处理逻辑与 UI 视图的解耦,从而提高复用性。

    通过翻阅 Swipeable Modifier 、Draggable Modifier 以及 Transformer Modifier,我们都能看到 PointerInput Modifier 的身影。因为这类上层的手势处理 Modifier 其实都是基于这个基础 Modifier 实现的。所以既然要自定义手势处理流程,自定义逻辑也必然要在这个 Modifier 中进行实现。

    通过 PointerInput Modifier 实现我们可以看出,我们所定义的自定义手势处理流程均发生在 PointerInputScope 中,suspend 关键字也告知我们自定义手势处理流程是发生在协程中。这其实是无可厚非的,在探索重组工作原理的过程中我们也经常能够看到协程的身影。伴随着越来越多的主流开发技术拥抱协程,这也就意味着协程成了 Android 开发者未来必须掌握的技能。推广协程同时其实也是在推广 Kotlin,即使官方一直强调不会放弃 Java,然而谁又会在 Java 中使用 Kotlin 协程呢?

    fun Modifier.pointerInput(
    vararg keys: Any?,
    block:
    suspend PointerInputScope.() -> Unit
    )
    : Modifier = composed(
    ...
    ) {
    ...
    remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply {
    LaunchedEffect(this, *keys) {
    block()
    }
    }
    }

    接下来我们就看看 PointerInputScope 作用域中,为我们可以使用哪些 API 来处理手势交互。本文将会根据手势能力分类进行解释说明。

    拖动类型基础 API

    API 介绍

    API名称作用
    detectDragGestures监听拖动手势
    detectDragGesturesAfterLongPress监听长按后的拖动手势
    detectHorizontalDragGestures监听水平拖动手势
    detectVerticalDragGestures监听垂直拖动手势

    谈及拖动,许多人第一个反应就是 Draggable Modifier,因为 Draggable Modifier 为我们提供了监听 UI 组件拖动能力。然而 Draggable Modifier 在提供了监听 UI 组件拖动能力的同时也拓展增加其他功能,我们通过 Draggable Modifier 参数列表即可看出。例如通过使用 DraggableState 允许开发者根据需求使 UI 组件自动被拖动。

    fun Modifier.draggable(
    state:
    DraggableState,
    orientation:
    Orientation,
    enabled:
    Boolean = true,
    interactionSource:
    MutableInteractionSource? = null,
    startDragImmediately:
    Boolean = false,
    onDragStarted:
    suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
    onDragStopped:
    suspend CoroutineScope.(velocity: Float) -> Unit = {},
    reverseDirection:
    Boolean = false
    )

    我们上面所罗列的这些拖动 API 只提供了监听 UI 组件拖动的能力,我们可以根据需求为其拓展功能,这也是这些API所存在的意义。我们从字面上就可以看出每个 API 所对应的含义,由于这些API的功能与参数相近,这里我们仅以 detectDragGestures 作为举例说明。

    举例说明

    接下来我们将完成一个绿色方块的手势拖动。在 Draggabel Modifier 中我们还只能监听垂直或水平中某一个方向的手势拖动,而使用 detectDragGestures 所有手势信息都是可以拿到的。如果我们还是只希望拿到某一个方向的手势拖动,使用 detectHorizontalDragGestures 或 detectVerticalDragGestures 即可,当然我们也可以使用 detectDragGestures 并且忽略掉某个方向的手势信息。如果我们希望在长按后才能拿到手势信息可以使用 detectDragGesturesAfterLongPress

    detectDragGestures 提供了四个参数。

    onDragStart (可选):拖动开始时回调

    onDragEnd (可选):拖动结束时回调

    onDragCancel (可选):拖动取消时回调

    onDrag (必须):拖动时回调

    decectDragGestures 的源码分析在 awaitTouchSlopOrCancellation 小节会有讲解。

    suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (
    Offset) -> Unit = { },
    onDragEnd: () ->
    Unit = { },
    onDragCancel: () ->
    Unit = { },
    onDrag: (
    change: PointerInputChange, dragAmount: Offset) -> Unit
    )

    💡 Tips

    有些同学可能困惑 onDragCancel 触发时机。在一些场景中,当组件拖动时会根据事件分发顺序进行事件分发,当前面先处理事件的组件满足了设置的消费条件,导致手势事件被消费,导致本组件拿到的是被消费的手势事件,从而会执行 onDragCancel 回调。如何定制事件分发顺序并消费事件后续会进行详细的描述。

    示例如下所示

    @Preview
    @Composable
    fun DragGestureDemo() {
    var boxSize = 100.dp
    var offset by remember { mutableStateOf(Offset.Zero) }
    Box(contentAlignment = Alignment.Center,
    modifier = Modifier.fillMaxSize()
    ) {
    Box(Modifier
    .size(boxSize)
    .offset {
    IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
    }
    .background(Color.Green)
    .pointerInput(Unit) {
    detectDragGestures(
    onDragStart = { offset ->
    // 拖动开始
    },
    onDragEnd = {
    // 拖动结束
    },
    onDragCancel = {
    // 拖动取消
    },
    onDrag = { change: PointerInputChange, dragAmount: Offset ->
    // 拖动中
    offset += dragAmount
    }
    )
    }
    )
    }
    }

    drag.gif

    点击类型基础 API

    API 介绍

    API名称作用
    detectTapGestures监听点击手势

    与 Clickable Modifier 不同的是,detectTapGestures 可以监听更多的点击事件。作为手机监听的基础 API,必然不会存在 Clickable Modifier 所拓展的涟漪效果。

    举例说明

    接下来我们将为一个绿色方块添加点击手势处理逻辑。detectTapGestures 提供了四个可选参数,用来监听不同点击事件。

    onDoubleTap (可选):双击时回调

    onLongPress (可选):长按时回调

    onPress (可选):按下时回调

    onTap (可选):轻触时回调

    suspend fun PointerInputScope.detectTapGestures(
    onDoubleTap: ((
    Offset) -> Unit)? = null,
    onLongPress: ((
    Offset) -> Unit)? = null,
    onPress:
    suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
    onTap: ((
    Offset) -> Unit)? = null
    )

    💡 Tips

    onPress 普通按下事件

    onDoubleTap 前必定会先回调 2 次 Press

    onLongPress 前必定会先回调 1 次 Press(时间长)

    onTap 前必定会先回调 1 次 Press(时间短)

    示例如下所示

    @Preview
    @Composable
    fun TapGestureDemo() {
    var boxSize = 100.dp
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    Box(Modifier
    .size(boxSize)
    .background(Color.Green)
    .pointerInput(Unit) {
    detectTapGestures(
    onDoubleTap = { offset: Offset ->
    // 双击
    },
    onLongPress = { offset: Offset ->
    // 长按
    },
    onPress = { offset: Offset ->
    // 按下
    },
    onTap = { offset: Offset ->
    // 轻触
    }
    )
    }
    )
    }
    }

    变换类型基础 API

    API 介绍

    API名称作用
    detectTransformGestures监听拖动、缩放与旋转手势

    与 Transfomer Modifier 不同的是,通过这个 API 可以监听单指的拖动手势,和拖动类型基础 API所提供的功能一样,除此之外还支持监听双指缩放与旋转手势。反观Transfomer Modifier 只能监听到双指拖动手势,不知设计成这样的行为不一致是否是 Google 有意而为之。

    举例说明

    接下来我们为这个绿色方块添加变化手势处理逻辑。detectTransformGestures 方法提供了两个参数。

    panZoomLock(可选): 当拖动或缩放手势发生时是否支持旋转

    onGesture(必须):当拖动、缩放或旋转手势发生时回调

    suspend fun PointerInputScope.detectTransformGestures(
    panZoomLock:
    Boolean = false,
    onGesture: (
    centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
    )

    💡 Tips

    关于偏移、缩放与旋转,我们建议的调用顺序是 rotate -> scale -> offset

    1. 若offset发生在rotate之前时,rotate会对offset造成影响。具体表现为当出现拖动手势时,组件会以当前角度为坐标轴进行偏移。

    2. 若offset发生在scale之前是,scale也会对offset造成影响。具体表现为UI组件在拖动时不跟手

    @Preview
    @Composable
    fun TransformGestureDemo() {
    var boxSize = 100.dp
    var offset by remember { mutableStateOf(Offset.Zero) }
    var ratationAngle by remember { mutableStateOf(0f) }
    var scale by remember { mutableStateOf(1f) }
    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    Box(Modifier
    .size(boxSize)
    .rotate(ratationAngle) // 需要注意offset与rotate的调用先后顺序
    .scale(scale)
    .offset {
    IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
    }
    .background(Color.Green)
    .pointerInput(Unit) {
    detectTransformGestures(
    panZoomLock = true, // 平移或放大时是否可以旋转
    onGesture = { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
    offset += pan
    scale *= zoom
    ratationAngle += rotation
    }
    )
    }
    )
    }
    }

    transform.gif

    forEachGesture

    在传统 View 系统中,一次手指按下、移动到抬起过程中的所有手势事件可以共同构成一个手势事件序列。我们可以通过自定义手势处理来对于每一个手势事件序列进行定制处理。Compose 提供了 forEachGesture 以允许用户可以对每一个手势事件序列进行相同的定制处理。如果我们忘记使用 forEachGesture ,那么只会处理第一次手势事件序列。有些同学可能会问,为什么我不能在手势处理逻辑最外层套一层 while(true) 呢,通过 forEachGesture 的实现我们可以看到 forEachGesture 其实内部也是由while 实现的,除此之外他保证了协程只有存活时才能监听手势事件,同时也保证了每次交互结束时所有手指都是离开屏幕的。有些同学看到 while 可能新生疑问,难道这样不会阻塞主线程嘛?其实我们在介绍 PointerInput Modifier 时就提到过,我们的手势操作处理均发生在协程中。其实前面我们所提到的绝大多数 API 其内部实现均使用了 forEachGesture 。有些特殊场景下我们仅使用前面所提出的 API 可能仍然无法满足我们的需求,当然如果可以满足的话我们直接使用其分别对应的 Modifier 即可,前面所提出的 API 存在的意义是为了方便开发者为其进行功能拓展。既然要掌握自定义手势处理,我们就要从更底层角度来看这些上层 API 是如何实现的,了解原理我们就可以轻松自定义了。

    suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {
    val currentContext = currentCoroutineContext()
    while (currentContext.isActive) {
    try {
    block()
    // 挂起等待所有手指抬起
    awaitAllPointersUp()
    } catch (e: CancellationException) {
    ...
    }
    }
    }

    手势事件作用域 awaitPointerEventScope

    在 PointerInputScope 中我们可以找到一个名为 awaitPointerEventScope 的 API 方法。

    通过翻阅方法声明可以发现这是个挂起方法,其尾部 lambda 在 AwaitPointerEventScope 作用域中。 通过这个 AwaitPointerEventScope 作用域我们可以获取到更加底层的 API 手势事件,这也为自定义手势处理提供了可能。

    suspend fun  awaitPointerEventScope(
    block:
    suspend AwaitPointerEventScope.() -> R
    )
    : R

    我们在 AwaitPointerEventScope 中发现了以下这些基础手势方法,可以发现这些 API 均是挂起函数,接下来我们会对每个 API 进行描述说明。

    API名称作用
    awaitPointerEvent手势事件
    awaitFirstDown第一根手指的按下事件
    drag拖动事件
    horizontalDrag水平拖动事件
    verticalDrag垂直拖动事件
    awaitDragOrCancellation单次拖动事件
    awaitHorizontalDragOrCancellation单次水平拖动事件
    awaitVerticalDragOrCancellation单次垂直拖动事件
    awaitTouchSlopOrCancellation有效拖动事件
    awaitHorizontalTouchSlopOrCancellation有效水平拖动事件
    awaitVerticalTouchSlopOrCancellation有效垂直拖动事件

    万物之源 awaitPointerEvent

    awaitPointerEvent 类似于传统 View 系统的 onTouchEvent() 。无论用户是按下、移动或抬起都将视作一次手势事件,当手势事件发生时 awaitPointerEvent 会恢复执行并将手势事件返回。

    suspend fun awaitPointerEvent(
    pass:
    PointerEventPass = PointerEventPass.Main
    )
    : PointerEvent

    通过 API 声明可以看到 awaitPointerEvent 有个可选参数 PointerEventPass

    我们知道手势事件的分发是由父组件到子组件的单链结构。这个参数目的是用以设置父组件与子组件的事件分发顺序,PointerEventPass 有 3 个枚举值可供选择,每个枚举值的具体含义如下

    枚举值含义
    PointerEventPass.Initial本组件优先处理手势,处理后交给子组件
    PointerEventPass.Main若子组件为Final,本组件优先处理手势。否则将手势交给子组件处理,结束后本组件再处理。
    PointerEventPass.Final若子组件也为Final,本组件优先处理手势。否则将手势交给子组件处理,结束后本组件再处理。

    大家可能觉得 Main 与 Final 是等价的。但其实两者在作为子组件时分发顺序会完全不同,举个例子。

    当父组件为Final,子组件为Main时,事件分发顺序: 子组件 -> 父组件

    当父组件为Final,子组件为Final时,事件分发顺序: 父组件 -> 子组件

    文字描述可能并不直观,接下来进行举例说明。

    事件分发流程

    接下来,我将通过一个嵌套了三层 Box 的示例来直观表现事件分发过程。我们为这嵌套的三层Box 中的每一层都进行手势获取。

    box_nest.jpg

    如果我们点击中间的绿色方块时,便会触发手势事件。

    当三层 Box 均使用默认 Main 模式时,事件分发顺序为:第三层 -> 第二层 -> 第一层

    当第一层Box使用 Inital 模式,第二层使用 Final 模式,第三层使用 Main 模式时,事件分发顺序为:第一层 -> 第三层 -> 第二层

    @Preview
    @Composable
    fun NestedBoxDemo() {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .fillMaxSize()
    .pointerInput(Unit) {
    awaitPointerEventScope {
    awaitPointerEvent(PointerEventPass.Initial)
    Log.d("compose_study", "first layer")
    }
    }
    ) {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .size(400.dp)
    .background(Color.Blue)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    awaitPointerEvent(PointerEventPass.Final)
    Log.d("compose_study", "second layer")
    }
    }
    ) {
    Box(
    Modifier
    .size(200.dp)
    .background(Color.Green)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    awaitPointerEvent()
    Log.d("compose_study", "third layer")
    }
    }
    )
    }
    }
    }

    // Output:
    // first layer
    // third layer
    // second layer

    能够自定义事件分发顺序之后,我们就可以决定手势事件由事件分发流程中哪个组件进行消费。那么如何进行消费呢,这就需要我们看看 awaitPointerEvent 返回的手势事件了。通过 awaintPointerEvent 声明,我们可以看到返回的手势事件是个 PointerEvent 实例。

    通过 PointerEvent 类声明,我们可以看到两个成员属性 changes 与 motionEvent。

    motionEvent 我们再熟悉不过了,就是传统 View 系统中的手势事件,然而却被声明了 internal 关键字,看来是不希望我们使用。

    changes 是一个 List,其中包含了每次发生手势事件时,屏幕上所有手指的状态信息。

    当只有一根手指时,这个 List 的大小为 1。在多指操作时,我们通过这个 List 获取其他手指的状态信息就可以轻松定制多指自定义手势处理了。

    actual data class PointerEvent internal constructor(
    actual val changes: List,
    internal val motionEvent: MotionEvent?
    )

    PointerInputChange

    class PointerInputChange(
    val id: PointerId, // 手指Id
    val uptimeMillis: Long, // 当前手势事件的时间戳
    val position: Offset, // 当前手势事件相对组件左上角的位置
    val pressed: Boolean, // 当前手势是否按下
    val previousUptimeMillis: Long, // 上一次手势事件的时间戳
    val previousPosition: Offset, // 上一次手势事件相对组件左上角的位置
    val previousPressed: Boolean, // 上一次手势是否按下
    val consumed: ConsumedData, // 当前手势是否已被消费
    val type: PointerType = PointerType.Touch // 手势类型(鼠标、手指、手写笔、橡皮)
    )
    API名称作用
    changedToDown是否已经按下(按下手势已消费则返回false)
    changedToDownIgnoreConsumed是否已经按下(忽略按下手势已消费标记)
    changedToUp是否已经抬起(按下手势已消费则返回false)
    changedToUpIgnoreConsumed是否已经抬起(忽略按下手势已消费标记)
    positionChanged是否位置发生了改变(移动手势已消费则返回false)
    positionChangedIgnoreConsumed是否位置发生了改变(忽略已消费标记)
    positionChange位置改变量(移动手势已消费则返回Offset.Zero)
    positionChangeIgnoreConsumed位置改变量(忽略移动手势已消费标记)
    positionChangeConsumed当前移动手势是否已被消费
    anyChangeConsumed当前按下手势或移动手势是否有被消费
    consumeDownChange消费按下手势
    consumePositionChange消费移动手势
    consumeAllChanges消费按下与移动手势
    isOutOfBounds当前手势是否在固定范围内

    这些 API 会在我们自定义手势处理时会被用到。可以发现的是,Compose 通过 PointerEventPass 来定制事件分发流程,在事件分发流程中即使前一个组件先获取了手势信息并进行了消费,后面的组件仍然可以通过带有 IgnoreConsumed 系列 API 来获取到手势信息。这也极大增加了手势操作的可定制性。就好像父组件先把事件消费,希望子组件不要处理这个手势了,但子组件完全可以不用听从父组件的话。

    我们通过一个实例来看看该如何进行手势消费,处于方便我们的示例不涉及移动,只消费按下手势事件来进行举例。和之前的样式一样,我们将手势消费放在了第三层 Box,根据事件分发规则我们知道第三层Box是第2个处理手势事件的,所以输出结果如下。

    // Output:
    // first layer, downChange: false
    // third layer, downChange: true
    // second layer, downChange: true

    ⚠️ 注意事项

    如果我们是在定制事件分发流程,那么需要注意以下两种写法

    // 正确写法
    awaitPointerEventScope {
    var event = awaitPointerEvent()
    event.changes[0].consumeDownChange()
    }

    // 错误写法
    var event = awaitPointerEventScope {
    awaitPointerEvent()
    }
    event.changes[0].consumeDownChange()

    他们的区别在于 awaitPointerEventScope 会在其内部所有手势在事件分发流程结束后返回,当所有组件都已经完成手势处理再进行消费已经没有什么意义了。我们仍然用刚才的例子来直观说明这个问题。我们在每一层Box awaitPointerEventScope 后面添加了日志信息。

    通过输出结果可以发现,这三层执行的相对顺序没有发生变化,然而却是在事件分发流程结束后才进行输出的。

    @Preview
    @Composable
    fun Demo() {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .fillMaxSize()
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent(PointerEventPass.Initial)
    Log.d("compose_study", "first layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    Log.d("compose_study", "first layer Outside")
    }
    ) {
    Box(
    contentAlignment = Alignment.Center,
    modifier = Modifier
    .size(400.dp)
    .background(Color.Blue)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent(PointerEventPass.Final)
    Log.d("compose_study", "second layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    Log.d("compose_study", "second layer Outside")
    }
    ) {
    Box(
    Modifier
    .size(200.dp)
    .background(Color.Green)
    .pointerInput(Unit) {
    awaitPointerEventScope {
    var event = awaitPointerEvent()
    event.changes[0].consumeDownChange()
    Log.d("compose_study", "third layer, downChange: ${event.changes[0].consumed.downChange}")
    }
    Log.d("compose_study", "third layer Outside")
    }
    )
    }
    }
    }

    // Output:
    // first layer, downChange: false
    // third layer, downChange: true
    // second layer, downChange: true
    // first layer Outside
    // third layer Outside
    // second layer Outside

    awaitFirstDown

    awaitFirstDown 将等待第一根手指按下事件时恢复执行,并将手指按下事件返回。分析源码我们可以发现 awaitFirstDown 也使用的是 awaitPointerEvent 实现的,默认使用 Main 模式。

    suspend fun AwaitPointerEventScope.awaitFirstDown(
    requireUnconsumed:
    Boolean = true
    )
    : PointerInputChange {
    var event: PointerEvent
    do {
    event = awaitPointerEvent()
    } while (
    !event.changes.fastAll {
    if (requireUnconsumed) it.changedToDown() else it.changedToDownIgnoreConsumed()
    }
    )
    return event.changes[0]
    }

    drag

    看到 drag 可能很多同学疑惑为什么又是拖动。其实前面所提到的拖动类型基础API detectDragGestures 其内部就是使用 drag 而实现的。与 detectDragGestures 不同的是,drag 需要主动传入一个 PointerId 用以表示要具体获取到哪根手指的拖动事件。

    suspend fun AwaitPointerEventScope.drag(
    pointerId:
    PointerId,
    onDrag: (
    PointerInputChange) -> Unit
    )

    翻阅源码可以发现,其实 drag 内部实现最终使用的仍然还是 awaitPointerEvent 。这里就不具体展开看了,感兴趣的可以自己去跟源码。

    举例说明

    通过结合 awaitFirstDown 与 drag 这些基础 API 我们已经可以自己实现 UI 拖动手势流程了。我们仍然以我们的绿色方块作为实例,为其添加拖动手势。

    @Preview
    @Composable
    fun BaseDragGestureDemo() {
    var boxSize = 100.dp
    var offset by remember { mutableStateOf(Offset.Zero) }
    Box(contentAlignment = Alignment.Center,
    modifier = Modifier.fillMaxSize()
    ) {
    Box(Modifier
    .size(boxSize)
    .offset {
    IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
    }
    .background(Color.Green)
    .pointerInput(Unit) {
    forEachGesture { // 循环监听每一组事件序列
    awaitPointerEventScope {
    var downEvent = awaitFirstDown()
    drag(downEvent.id) {
    offset += it.positionChange()
    }
    }
    }
    }
    )
    }
    }

    awaitDragOrCancellation

    与 drag 不同的是,awaitDragOrCancellation 负责监听单次拖动事件。当手指已经抬起或拖动事件已经被消费时会返回 null。当然我们也可以使用 awaitDragOrCancellation 来完成 UI 拖动手势处理流程。通过翻阅源码可以发现 drag 其实内部也是使用 awaitDragOrCancellation 进行实现的。而 awaitDragOrCancellation 内部仍然是 awaitPointerEvent

    @Preview
    @Composable
    fun BaseDragGestureDemo() {
    var boxSize = 100.dp
    var offset by remember { mutableStateOf(Offset.Zero) }
    Box(contentAlignment = Alignment.Center,
    modifier = Modifier.fillMaxSize()
    ) {
    Box(Modifier
    .size(boxSize)
    .offset {
    IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
    }
    .background(Color.Green)
    .pointerInput(Unit) {
    forEachGesture {
    awaitPointerEventScope {
    var downPointer = awaitFirstDown()
    while (true) {
    var event = awaitDragOrCancellation(downPointer.id)
    if (event == null) {
    break
    }
    offset += event.positionChange()
    }
    }
    }
    }
    )
    }
    }

    awaitTouchSlopOrCancellation

    awaitTouchSlopOrCancellation 用于监测当前拖动手势是否是一次有效的拖动。有效指的是当前手势滑动的欧式距离(位移)是否超过设定的阈值。若拖动手势还没有达到阈值便抬起或拖动手势事件已经被消费时将返回null,翻阅源码我们又找到了awaitPointerEvent ,所以说 awaitPointerEvent 是万物之源嘛~

    我们前面所提到的 detectDragGestures 其内部不仅使用了 drag 还使用了 awaitTouchSlopOrCancellation 来判断手势拖动操作。仅当监测为一次有效的拖动时,才会执行 onDragStart 回调。接下来就是使用 drag 来监听拖动手势,仅当 drag 返回 false (即在拖动过程中事件分发流程前面的组件达成定制条件消费了这次的拖动手势事件) 会执行 onDragCancel 回调,否则如果所有手指抬起正常结束则会执行 onDragEnd 回调。

    收起阅读 »

    Xcode 设置启动页

    前言:IOS 中设置启动页有两种方式 Launch Image 和 LaunchScreen一、Launch Image1.在工程 targets--Build Settings 搜索 Asset Catalog Launch Image Set Name 然...
    继续阅读 »

    前言:IOS 中设置启动页有两种方式 Launch Image 和 LaunchScreen

    一、Launch Image

    1.在工程 targets--Build Settings 搜索 Asset Catalog Launch Image Set Name 然后设置创建的启动页名字即可如下图所示。


    2.再在Info.plist中删除 Launch screen interface file base name并添加 Launch image并设置LaunchImage 

    3.资源文件中添加LaunchImage放入不同尺寸的图片,如何所示:


    4.删除已安装的App 重新打包


    二、Launch Screen

    1.再在Info.plist中添加 Launch screen interface file base name并设置LaunchScreen


    2.在工程 targets -- General 中 设置 Launch Screen File


    3.在LaunchScreen.storyboard文件中设计启动页样式


    end

    收起阅读 »

    iOS逆向-逆向比较实用的工具

    ChiselChisel is a collection of LLDB commands to assist in the debugging of iOS apps通过github上面说明安装一下pviews 找所有的视图pviews -u 查看上一层视图...
    继续阅读 »

    Chisel

    Chisel is a collection of LLDB commands to assist in the debugging of iOS apps
    通过github上面说明安装一下
    pviews 找所有的视图
    pviews -u 查看上一层视图
    pvc 打印所有的控制器
    pmethods 0x107da5370 打印所有方法
    pinternals 0x107da5370 打印所有成员
    fvc -v 0x107da5370,根据视图找到控制器
    fv



    flicker 会让视图闪烁两次

    LLDB

    search class 搜索对象
    methods 0x 方法
    b -a 0x02 下断点
    sbt 恢复方法符号

    cycript

    ./cycript 开始
    ctrl + d 退出
    首先要配置cycript,我这里面配置的是moneyDev,因为moneyDev里面包含cycript
    ./cycript -r 192.168.1.101:6666找到ip地址+:调试端口号默认6666

    cy# keyWd .recursiveDescription().toString()层级视图



    choose (UIButton)
    这个工具不会阻塞进程
    只要进程不被kill,ctrl+d在重新进入变量是都在的、
    使用自己的cy

    封装成脚本,在任意位置sh cyConnect.sh


    配置.zshrc



    使用


    这里面也可以使用pviews() pvcs()等


    转自链接:https://www.jianshu.com/p/a1c619e2da97
    收起阅读 »

    iOS逆向-18:LLDB调试

    在逆向环境中,拿不到源码,只能通过指令设置断点LLDB(Low Lever Debug)默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足...
    继续阅读 »

    在逆向环境中,拿不到源码,只能通过指令设置断点

    LLDB(Low Lever Debug)

    默认内置于Xcode中的动态调试工具。标准的 LLDB 提供了一组广泛的命令,旨在与老版本的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足实际需要。

    这里列举了一些常用的命令:

    断点设置

    • 设置断点
      breakpoint set -n XXX
      set 是子命令
      -n 是选项 是--name 的缩写!

    • 查看断点列表
      breakpoint list

    • 删除
      breakpoint delete 组号

    • 禁用/启用
      breakpoint disable 禁用
      breakpoint enable 启用

    • 遍历整个项目中满足Game:这个字符的所有方法
      breakpoint set -r Game:

    流程控制

    • 继续执行
      continue c

    • 单步运行,将子函数当做整体一步执行
      n next
      单步运行,遇到子函数会进去
      s

    • stop-hook
      让你在每次stop的时候去执行一些命令,只对breadpoint,watchpoint

    • 其他命令
      image list
      p expression 除了打印还可以执行一些代码
      b -[xxx xxx]
      x16进制打印
      register read 读寄存器
      po
      b -r xx断住所有包含的方法
      b -selector xx断住所有xx方法
      help xx查看指令

    函数调用栈

    bt //所有调用栈
    up //跳上层堆栈
    down
    frame select 12 跳指定下标堆栈
    frame variable 当前函数参数,只能修改当前函数参数
    thread return 代码回滚,直接返回,不执行后面的代码。提前返回,可以通过这种方式绕过hook

    内存断点

    Person *person = [Person new];
    person.name = "FY";
    下内存断点:
    watchpoint set variable person->_name
    watchpoint set expression 0x456 &person->_name
    当进行修改的时候就会触发内存断点


    然后我们bt一下

    可以看到方法的调用栈
    break command add 1
    在断点中添加一些指令

    让你在每次stop的时候去执行一些命令,只对breadpoint,watchpoint
    target stop-hook add -o "frame variable"
    target stop-hook add -o "po self.view"
    target stop-hook list

    这些指令也可以放到家目录下的.lldbinit中,只要lldb一启动就会执行里面的命令,一般的lldb插件就是在这个目录配置的

    cd ~ 进入家目录
    .lldbinit




    转自链接:https://www.jianshu.com/p/59123ee28503
    收起阅读 »

    Swift中的闭包

    一、简介闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 OC 中的代码块(blocks)以及其他一些编程语言中的 匿名函数 ...
    继续阅读 »

    一、简介

    闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。
    Swift 中的闭包与 C 和 OC 中的代码块(blocks)以及其他一些编程语言中的 匿名函数 比较相似。全局函数和嵌套函数其实就是特殊的闭包。

    由于之前对 Swift 中的闭包不太熟悉,所以在此归纳总结一下闭包的语法。

    二、语法

    Swift 中的闭包有很多优化的地方:

    根据上下文推断参数和返回值类型
    从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略 return)
    可以使用简化参数名,如$0, $1(从 0 开始,表示第 i 个参数...)
    提供了尾随闭包语法(Trailing closure syntax)
    闭包表达式 提供了一些语法优化,使得撰写闭包变得简单明了。下面 闭包表达式 的例子通过使用几次迭代展示了 sorted(by:) 方法定义和语法优化的方式。下面的每一次迭代都用更简洁的方式描述了相同的功能。🐳->🐘->🦛->🐷->🐔->🐹->🦟。

    sorted(by:) 函数介绍:
    Swift 标准库提供了名为 sorted(by:) 的方法,它会根据你所提供的用于排序的闭包函数将已知类型数组中的值进行排序。一旦排序完成,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

    下面的闭包表达式示例使用 sorted(by:)方法对一个 String 类型的数组进行字母逆序排序。以下是初始数组:

    let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

    sorted(by:)方法接受一个 闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

    该例子对一个 String 类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool

    原始实现方式:

    提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 sorted(by:)方法的参数传入:

    func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
    }
    var reversedNames = names.sorted(by: backward)
    // 打印可得 reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

    然而,以这种方式来编写一个实际上很简单的表达式(a > b),确实太过繁琐了。对于这个例子来说,利用 闭包表达式语法 可以更好地构造一个 内联排序闭包

    闭包表达式语法:

    闭包表达式语法有如下的一般形式:

    { (parameters) -> return type in
    statements
    }

    闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。

    第一次精简——闭包语法:

    下面的例子展示了之前 backward(_:_:) 函数对应的闭包表达式版本的代码:

    reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
    })
    需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:) 函数类型声明相同。在这两种方式中,都写成了 (s1: String, s2: String) -> Bool。然而在内联闭包表达式中,函数和返回值类型都写在大括号 ,而不是大括号 
    关键字 in:闭包的函数体部分由关键字 in 引入。该关键字表示 “闭包的参数和返回值类型定义已经完成,闭包函数体即将开始”。

    该例中 sorted(by:) 方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了 内联闭包

    第二次精简——根据上下文推断类型:

    因为排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。sorted(by:)方法被一个字符串数组调用,因此其参数必须是 (String, String) -> Bool 类型的函数。这意味着 (String, String)和 Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
    reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

    实际上,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。

    第三次精简——单表达式闭包隐式返回:

    单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:

    reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

    在这个例子中,sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。因为闭包函数体只包含了一个单一表达式(s1 > s2),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略。

    第四次精简——参数名称缩写:

    Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0,$1,$2 来顺序调用闭包的参数,以此类推。

    如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
    reversedNames = names.sorted(by: { $0 > $1 } )

    在这个例子中,$0 和 $1 表示闭包中第一个和第二个 String 类型的参数。

    第五次精简——运算符方法:

    实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断出你想使用大于号的字符串函数实现:
    reversedNames = names.sorted(by: >)

    第六次精简——尾随闭包:

    什么是尾随闭包?
    如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用 尾随闭包 来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
    func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
    }

    // 以下是不使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure(closure: {
    // 闭包主体部分
    })

    // 以下是使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure() {
    // 闭包主体部分
    }

    在上面 sorted(by:) 方法参数的字符串排序闭包可以改写为:

    reversedNames = names.sorted() { $0 > $1 }

    如果闭包表达式是函数或方法的唯一参数,则当使用尾随闭包时,甚至可以把 () 省略掉:

    reversedNames = names.sorted { $0 > $1 }

    以上只是列举了闭包的一些基本语法与用法,还有一些其他概念需要继续学习,如 自动闭包、逃逸闭包,后续我会慢慢补齐总结的,谢谢!


    转自链接:https://www.jianshu.com/p/7043ffaac2f2
    收起阅读 »

    YTKNetwork的基本使用

    YTKNetwork是一个对AFNetworking封装的一个框架,虽然二者底层原理相同,但使用方法和使用效果是大不相同的。YTKNetwork 提供了以下更高级的功能:1.支持按时间缓存网络请求内容2.支持按版本号缓存网络请求内容3.支持统一设置服务器和 C...
    继续阅读 »

    YTKNetwork是一个对AFNetworking封装的一个框架,虽然二者底层原理相同,但使用方法和使用效果是大不相同的。YTKNetwork 提供了以下更高级的功能:

    1.支持按时间缓存网络请求内容
    2.支持按版本号缓存网络请求内容
    3.支持统一设置服务器和 CDN 的地址
    4.支持检查返回 JSON 内容的合法性
    5.支持文件的断点续传
    6.支持 block 和 delegate 两种模式的回调方式
    7.支持批量的网络请求发送,并统一设置它们的回调(实现在 YTKBatchRequest 类中)
    支持方便地设置有相互依赖的网络请求的发送,例如:发送请求 A,根据请求 A 的结果,选择性的发送请求 B
    和 C,再根据 B 和 C 的结果,选择性的发送请求 D。(实现在 YTKChainRequest 类中)
    支持网络请求 URL 的 filter,可以统一为网络请求加上一些参数,或者修改一些路径。


    YTKNetwork包含了这几个类:1、YTKNetworkConfig (设置域名) 2、YTKRequest (网络请求)3、YTKBatchRequest (请求多个类 )4、YTKChainRequest (依赖请求)5、YTKBaseRequest(YTKRequest的父类)

    YTKNetwork 的基本思想

    YTKNetwork 的基本的思想是把每一个网络请求封装成对象。所以使用 YTKNetwork,你的每一个请求都需要继承 YTKRequest 类,通过覆盖父类的一些方法来构造指定的网络请求。


    集约式和离散式API

    集约式API

    介绍:所有API的调用只有一个类,然后这个类接收API名字,API参数,以及回调着陆点,即项目中的每个请求都会走统一的入口,对外暴露了请求的 URL 和 Param 以及请求方式,入口一般都是通过单例 来实现,AFNetworking 的官方 demo 就是采用的集约式的方式对网络请求进行的封装,也是目前比较流行的网络请求方式。

    优点:使用便捷,能实现快速开发
    缺点:
    1.对每个请求的定制型不够强
    2.不方便后期业务拓展

    我们常用的AFNetworking框架就是集约式,在简单程序中AFNetworking 将请求逻辑写在 Controller 中比YTK更加方便,也不用一个个请求新建不同的request类。而YTKNetworking则是离散式的

    离散式API

    介绍:离散型API调用是这样的,一个API对应于一个APIManager,然后这个APIManager只需要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。即每个网络请求类都是一个对象,它的 URL 以及请求方式和响应方式 均不暴露给外部调用。只能内部通过 重载或实现协议 的方式来指定,外部调用只需要传 Param 即可,YTKNetwork就是采用的这种网络请求方式。


    优点:URL 以及请求和响应方式不暴露给外部,避免外部调用的时候写错
    业务方使用起来较简单,业务使用者不需要去关心它的内部实现
    可定制性强,可以为每个请求指定请求的超时时间以及缓存的周期
    缺点:
    网络层需要业务实现方去写,变相的增加了部分工作量
    文件增多,程序包会变大一些

    YTKNetworkConfig 类

    YTKNetworkConfig 类有两个作用:
    统一设置网络请求的服务器和 CDN 的地址。
    管理网络请求的 YTKUrlFilterProtocol 实例
    我们为什么需要统一设置服务器地址呢?因为:

    按照设计模式里的 Do Not Repeat Yourself 原则,我们应该把服务器地址统一写在一个地方。
    在实际业务中,我们的测试人员需要切换不同的服务器地址来测试。统一设置服务器地址到 YTKNetworkConfig 类中,也便于我们统一切换服务器地址。
    具体的用法是,在程序刚启动的回调中,设置好 YTKNetworkConfig 的信息,如下所示:

    - (BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig];
    config.baseUrl = @"http://yuantiku.com";
    config.cdnUrl = @"http://fen.bi";
    }

    设置好之后,所有的网络请求都会默认使用 YTKNetworkConfig 中 baseUrl 参数指定的地址。

    大部分企业应用都需要对一些静态资源(例如图片、js、css)使用 CDN。YTKNetworkConfig 的 cdnUrl 参数用于统一设置这一部分网络请求的地址。

    当我们需要切换服务器地址时,只需要修改 YTKNetworkConfig 中的 baseUrl 和 cdnUrl 参数即可。

    YTKRequest

    YTK把每个请求实例化,管理它的生命周期,也可以管理多个请求,在github的基础教程里面我们可以看到YTK是把每个网络请求都封装成对象,每一种网络请求继承 YTKRequest 类后,需要用方法覆盖(overwrite)的方式,来指定网络请求的具体信息。如下是一个示例:

    假如我们要向网址 http://www.yuantiku.com/iphone/register 发送一个 POST 请求,请求参数是 username 和 password。那么,这个类应该如下所示:

    // RegisterApi.h
    #import "YTKRequest.h"

    @interface RegisterApi : YTKRequest

    - (id)initWithUsername:(NSString *)username password:(NSString *)password;

    @end


    // RegisterApi.m

    #import "RegisterApi.h"

    @implementation RegisterApi {
    NSString *_username;
    NSString *_password;
    }

    - (id)initWithUsername:(NSString *)username password:(NSString *)password {
    self = [super init];
    if (self) {
    _username = username;
    _password = password;
    }
    return self;
    }

    - (NSString *)requestUrl {
    // “ http://www.yuantiku.com ” 在 YTKNetworkConfig 中设置,这里只填除去域名剩余的网址信息
    return @"/iphone/register";
    }

    - (YTKRequestMethod)requestMethod {
    return YTKRequestMethodPOST;
    }

    - (id)requestArgument {
    return @{
    @"username": _username,
    @"password": _password
    };
    }

    @end

    在上面这个示例中,我们可以看到:

    • 我们通过覆盖 YTKRequest 类的 requestUrl 方法,实现了指定网址信息。并且我们只需要指定除去域名剩余的网址信息,因为域名信息在 YTKNetworkConfig 中已经设置过了。
    • 我们通过覆盖 YTKRequest 类的 requestMethod 方法,实现了指定 POST 方法来传递参数。
    • 我们通过覆盖 YTKRequest 类的 requestArgument 方法,提供了 POST 的信息。这里面的参数 username 和 password 如果有一些特殊字符(如中文或空格),也会被自动编码。

    调用 RegisterApi

    在构造完成 RegisterApi 之后,具体如何使用呢?我们可以在登录的 ViewController 中,调用 RegisterApi,并用 block 的方式来取得网络请求结果:
    - (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
    RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
    [api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) {
    // 你可以直接在这里使用 self
    NSLog(@"succeed");
    } failure:^(YTKBaseRequest *request) {
    // 你可以直接在这里使用 self
    NSLog(@"failed");
    }];
    }
    }

    注意:你可以直接在 block 回调中使用 self,不用担心循环引用。因为 YTKRequest 会在执行完 block 回调之后,将相应的 block 设置成 nil。从而打破循环引用。

    除了 block 的回调方式外,YTKRequest 也支持 delegate 方式的回调:

    - (void)loginButtonPressed:(id)sender {
    NSString *username = self.UserNameTextField.text;
    NSString *password = self.PasswordTextField.text;
    if (username.length > 0 && password.length > 0) {
    RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password];
    api.delegate = self;
    [api start];
    }
    }

    - (void)requestFinished:(YTKBaseRequest *)request {
    NSLog(@"succeed");
    }

    - (void)requestFailed:(YTKBaseRequest *)request {
    NSLog(@"failed");
    }

    验证服务器返回内容

    有些时候,由于服务器的 Bug,会造成服务器返回一些不合法的数据,如果盲目地信任这些数据,可能会造成客户端 Crash。如果加入大量的验证代码,又使得编程体力活增加,费时费力。

    使用 YTKRequest 的验证服务器返回值功能,可以很大程度上节省验证代码的编写时间。

    例如,我们要向网址 http://www.yuantiku.com/iphone/users 发送一个 GET 请求,请求参数是 userId 。我们想获得某一个用户的信息,包括他的昵称和等级,我们需要服务器必须返回昵称(字符串类型)和等级信息(数值类型),则可以覆盖 jsonValidator 方法,实现简单的验证。

    - (id)jsonValidator {
    return @{
    @"nick": [NSString class],
    @"level": [NSNumber class]
    };
    }

    断点续传

    要启动断点续传功能,只需要覆盖 resumableDownloadPath 方法,指定断点续传时文件的存储路径即可,文件会被自动保存到此路径。如下代码将刚刚的取图片的接口改造成了支持断点续传:

    @implementation GetImageApi {
    NSString *_imageId;
    }

    - (id)initWithImageId:(NSString *)imageId {
    self = [super init];
    if (self) {
    _imageId = imageId;
    }
    return self;
    }

    - (NSString *)requestUrl {
    return [NSString stringWithFormat:@"/iphone/images/%@", _imageId];
    }

    - (BOOL)useCDN {
    return YES;
    }

    - (NSString *)resumableDownloadPath {
    NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *cachePath = [libPath stringByAppendingPathComponent:@"Caches"];
    NSString *filePath = [cachePath stringByAppendingPathComponent:_imageId];
    return filePath;
    }

    @end

    按时间缓存内容

    刚刚我们写了一个 GetUserInfoApi ,这个网络请求是获得用户的一些资料。

    我们想像这样一个场景,假设你在完成一个类似微博的客户端,GetUserInfoApi 用于获得你的某一个好友的资料,因为好友并不会那么频繁地更改昵称,那么短时间内频繁地调用这个接口很可能每次都返回同样的内容,所以我们可以给这个接口加一个缓存。

    在如下示例中,我们通过覆盖 cacheTimeInSeconds 方法,给 GetUserInfoApi 增加了一个 3 分钟的缓存,3 分钟内调用调 Api 的 start 方法,实际上并不会发送真正的请求。

    @implementation GetUserInfoApi {
    NSString *_userId;
    }

    - (id)initWithUserId:(NSString *)userId {
    self = [super init];
    if (self) {
    _userId = userId;
    }
    return self;
    }

    - (NSString *)requestUrl {
    return @"/iphone/users";
    }

    - (id)requestArgument {
    return @{ @"id": _userId };
    }

    - (id)jsonValidator {
    return @{
    @"nick": [NSString class],
    @"level": [NSNumber class]
    };
    }

    - (NSInteger)cacheTimeInSeconds {
    // 3 分钟 = 180 秒
    return 60 * 3;
    }

    @end

    该缓存逻辑对上层是透明的,所以上层可以不用考虑缓存逻辑,每次调用 GetUserInfoApi 的 start 方法即可。GetUserInfoApi 只有在缓存过期时,才会真正地发送网络请求。



    转自链接:https://www.jianshu.com/p/8213f3e3b0ea
    收起阅读 »

    Linux - 远程操作

    shotdown命令,默认表示1分钟后关机.命令格式:$shutdown [选项] <参数>参数示例一分钟以后关机$shutdown 立刻关机$shutdown now 在今天的21:30关机$shutdown 21:30 10分钟以后关机$s...
    继续阅读 »


    关机重/启命令

    shutdown命令可以安全关闭 或者 重新启动系统,直接使用 shotdown命令,默认表示1分钟后关机.
    命令格式:

    $shutdown [选项] <参数>

    选项
    功能
    [-r]重新启动
    [-c]取消之前的关机计划

    参数

    • [时间]:设置多久时间后执行shutdown指令;
    • [警告信息]:要传送给所有登入用户的信息。


    示例

    • 一分钟以后关机
    $shutdown  
    • 立刻关机
    $shutdown now
    • 在今天的21:30关机
    $shutdown 21:30
    • 10分钟以后关机
    $shutdown +10
    • 10分钟以后关机,同时发出警告信息
    $shutdown +10 "System will shutdown after 10 minutes"
    • 取消关机计划
    $shutdown -c

    reboot命令也可以用来重新启动正在运行的Linux操作系统。
    和 shutdown -r now一样

    网络配置命令

    命令功能
    ifconfigconfigure a network interface,查看/配置计算机当前的网卡信息
    ping测试目标ip地址的连接是否正常

    ifconfig命令

    ifconfig命令被用于配置和显示Linux中网卡信息。
    查看网卡信息

    $ifconfig

    快速定位IP地址

    $ifconfig | grep inet

    一台计算机中可能会有一个 物理网卡 和 多个虚拟网卡,在Linux中物理网卡名字一般是 ensXX

    • 127.0.0.1这个地址是一个比较特殊的地址,称之为本地回环地址,可以用来测试本机网卡是否正常工作。

    ping命令

    ping命令用来测试主机之间网络的连通性。执行ping指令会使用ICMP传输协议,发出要求回应的信息。一般用于检测计算机之间的网络通讯是否正常。

    由于ping命令的工作原理,服务器人员给往往将ping用作动词。经常说:“ping一下某某计算机”

    示例:

    “ping”目标主机

    $ping IP地址

    检测本地网卡是否正常

    $ping 127.0.0.1

    结束ping的执行使用Ctrl+C。在Linux中终止一个终端程序绝大多数都可以使用Ctrl+C

    SSH(Secure Shell)

    简单说,SSH是一种网络协议,用于计算机之间的加密登录。
    最早的时候,互联网通信都是明文通信,一旦被截获,内容就暴露无疑。1995年,芬兰学者Tatu Ylonen设计了SSH协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广,目前已经成为Linux系统的标准配置。

    OpenSSH

    SSH只是一种协议,存在多种实现OpenSSH就是其中一种,它是一款软件,应用非常广泛在Mac以及Ubuntu中都自带OpenSSH

    SSH的登录过程

    • (1)远程主机收到用户的登录请求,把自己的公钥发给用户。
    • (2)用户使用这个公钥,将登录密码加密后,发送回来。
    • (3)远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录。

    SSH客户端命令

    ssh [-p port] user@remote

    • user 是远程端上的用户名,默认是当前用户
    • remote是远程端的地址,可以是IP/域名
    • port是远程端的端口,默认是22

    Ubuntu下开启SSH

    Ubuntu下SSH分

    • openssh-client(客户端)
    • openssh-server (服务端)
    检测是否有开启ssh服务
    hank@ubuntu:~$ ps -e | grep ssh
    4910 ? 00:00:00 sshd
    其中sshd 为server端的守护进程,如果没有出现sshd,那么很有可能你的系统中没有安装server端。或者ssh服务没有启动。

    开启ssh服务
    hank@ubuntu:~$ sudo /etc/init.d/ssh start
    [ ok ] Starting ssh (via systemctl): ssh.service.
    安装openssh-server

    如果显示上述命令找不到。那么是因为我们的Ubuntu系统默认没有服务端,所以可以通过下面命令安装。
    $ sudo apt-get install openssh-server

    可能出现错误
    $ sudo apt-get install openssh-server
    正在读取软件包列表... 完成
    正在分析软件包的依赖关系树
    正在读取状态信息... 完成
    有一些软件包无法被安装。如果您用的是 unstable 发行版,这也许是
    因为系统无法达到您要求的状态造成的。该版本中可能会有一些您需要的软件
    包尚未被创建或是它们已被从新到(Incoming)目录移出。
    下列信息可能会对解决问题有所帮助:

    下列软件包有未满足的依赖关系:
    openssh-server : 依赖: openssh-client (= 1:7.1p1-4)
    依赖: openssh-sftp-server 但是它将不会被安装
    推荐: ssh-import-id 但是它将不会被安装
    E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。

    因为openssh-server 需要依赖openssh-client,但是很明显,我们系统自带的版本和目前要安装的server版本不同。所以我们重新安装一下client版本。


    hank@ubuntu:~$ sudo apt-get install openssh-client=1:7.1p1-4
    正在读取软件包列表... 完成
    正在分析软件包的依赖关系树
    正在读取状态信息... 完成
    建议安装:
    ssh-askpass libpam-ssh keychain monkeysphere
    下列软件包将被【降级】:
    openssh-client
    升级了 0 个软件包,新安装了 0 个软件包,降级了 1 个软件包,要卸载 0 个软件包,有 0 个软件包未被升级。
    需要下载 581 kB 的归档。
    解压缩后将会空出 36.9 kB 的空间。
    您希望继续执行吗? [Y/n] y
    获取:1 http://mirror.neu.edu.cn/ubuntu xenial/main amd64 openssh-client amd64 1:7.1p1-4 [581 kB]
    已下载 581 kB,耗时 33 (17.6 kB/s)
    dpkg:警告:即将把 openssh-client 1:7.2p2-4 降级到 1:7.1p1-4
    正在将 openssh-client (1:7.1p1-4) 解包到 (1:7.2p2-4) 上 ...
    正在处理用于 man-db (2.7.5-1) 的触发器 ...
    正在设置 openssh-client (1:7.1p1-4) ...
    正在安装新版本配置文件 /etc/ssh/ssh_config ...
    这样可以看到降级成功。然后我们再次安装openssh-server就OK了!

    hank@ubuntu:~$ sudo apt-get install openssh-server

    SCP(Secure copy)

    • scp scp是linux系统下基于ssh登陆进行安全的远程文件拷贝命令。
    • 命令格式
    scp -P port 源文件路径 目标文件路径
    # 将本地目录下的123.txt拷贝到远程桌面目录下
    $scp -P port 123.txt user@remote:Desktop/123.txt

    # 把远程桌面目录下的123.txt文件 复制到 本地当前目录下
    scp -P port user@remote:Desktop/123.txt 123.txt

    # 加上 -r 选项可以传送文件夹
    # 把当前目录下的 demo 文件夹 复制到 远程 家目录下的 Desktop
    scp -r demo user@remote:Desktop

    # 把远程 家目录下的 Desktop 复制到 当前目录下的 demo 文件夹
    scp -r user@remote:Desktop demo

    选项功能
    -r若给出的源文件是目录文件,则 scp 将递归复制该目录下的所有子目录和文件,目标文件必须为一个目录名
    -P若远程 SSH 服务器的端口不是 22,需要使用大写字母 -P 选项指定端口

    SSH常用配置

    免密登陆

    • 配置公钥
      执行 ssh-keygen 即可生成 SSH 钥匙,一路回车即可
    • 上传公钥到服务器
      执行 ssh-copy-id -p port user@remote,可以让远程服务器记住我们的公钥

    配置别名

    每次都输入ssh -p port user@remote,非常不方便,而且还不好记忆

    而 配置别名 可以让我们进一步偷懒,譬如用:ssh mac 来替代上面这么一长串,那么就在 ~/.ssh/config 里面追加以下内容:


    Host mac
    HostName ip地址
    User H
    Port 22

    保存之后,即可用 ssh mac 实现远程登录了,scp 同样可以使用。


    作者:请叫我Hank
    链接:https://www.jianshu.com/p/9b31892a572f



    收起阅读 »

    Linux简介

    Linux 内核以及发行版Linux内核(kernel)操作系统内核是指大多数操作系统的核心部分。它由操作系统中用于管理存储器、文件、外设和系统资源的那些部分组成。操作系统内核通常运行进程,并提供进程间的通信。Linux 内核版本又分为 稳定版&nb...
    继续阅读 »

    Linux 内核以及发行版

    • Linux内核(kernel)

    操作系统内核是指大多数操作系统的核心部分。它由操作系统中用于管理存储器、文件、外设和系统资源的那些部分组成。操作系统内核通常运行进程,并提供进程间的通信。
    Linux 内核版本又分为 稳定版 和 开发版,两种版本是相互关联,相互循环

    • 稳定版:具有工业级强度,可以广泛地应用和部署。

    • 开发版:由于要试验各种解决方案,所以变化很快

    • 内核源码网址:http://www.kernel.org

    • Linux发行版

    Linux 发行版:我们常说的Linux操作系统,也是由Linux内核与各种常用软件的集合产品. 类似Windows包含了桌面环境.全球大约有数百款的Linux系统版本,每个系统版本都有自己的特性和目标人群.

    Ubuntu(乌班图)

    Ubuntu是一个以桌面应用为主的开源GNU/Linux操作系统,主要依赖Canonical有限公司的支持,同时也有很多来自Linux社区的热心人士提供协助。
    作为Linux发行版之一.Canonical 的Ubuntu 胜过其他所有的 Linux 服务器发行版 ,它简单易用同时又相当稳定,而且具有庞大的社区力量,用户可以方便地从社区获得帮助.Ubuntu在服务器领域是妥妥的赢家.

    Ubuntu的目录结构



    Ubuntu的主要目录
    • /:根目录,一般根目录下只存放目录,在 linux 下有且只有一个根目录,所有的东西都是从这里开始
    • /bin、/usr/bin:可执行二进制文件的目录,如常用的命令 ls、tar、mv、cat 等
    • /boot:放置 linux 系统启动时用到的一些文件,如 linux 的内核文件:/boot/vmlinuz,系统引导管理器:/boot/grub
    • /dev:存放linux系统下的设备文件,访问该目录下某个文件,相当于访问某个设备,常用的是挂载光驱mount /dev/cdrom /mnt
    • /etc:系统配置文件存放的目录,不建议在此目录下存放可执行文件,重要的配置文件有
      • /etc/inittab
      • /etc/fstab
      • /etc/init.d
      • /etc/X11
      • /etc/sysconfig
      • /etc/xinetd.d
    • /home:系统默认的用户家目录,新增用户账号时,用户的家目录都存放在此目录下
      • ~ 表示当前用户的家目录
      • ~edu 表示用户 edu 的家目录
    • /lib、/usr/lib、/usr/local/lib:系统使用的函数库的目录,程序在执行过程中,需要调用一些额外的参数时需要函数库的协助
    • /lost+fount:系统异常产生错误时,会将一些遗失的片段放置于此目录下
    • /mnt: /media:光盘默认挂载点,通常光盘挂载于 /mnt/cdrom 下,也不一定,可以选择任意位置进行挂载
    • /opt:给主机额外安装软件所摆放的目录
    • /proc:此目录的数据都在内存中,如系统核心,外部设备,网络状态,由于数据都存放于内存中,所以不占用磁盘空间,比较重要的文件有:/proc/cpuinfo、/proc/interrupts、/proc/dma、/proc/ioports、/proc/net/* 等
    • /root:系统管理员root的家目录
    • /sbin、/usr/sbin、/usr/local/sbin:放置系统管理员使用的可执行命令,如 fdisk、shutdown、mount 等。与 /bin 不同的是,这几个目录是给系统管理员 root 使用的命令,一般用户只能"查看"而不能设置和使用
    • /tmp:一般用户或正在执行的程序临时存放文件的目录,任何人都可以访问,重要数据不可放置在此目录下
    • /srv:服务启动之后需要访问的数据目录,如 www 服务需要访问的网页数据存放在 /srv/www 内
    • /usr:应用程序存放目录
      • /usr/bin:存放应用程序
      • /usr/share:存放共享数据
      • /usr/lib:存放不能直接运行的,却是许多程序运行所必需的一些函数库文件
      • /usr/local:存放软件升级包
      • /usr/share/doc:系统说明文件存放目录
      • /usr/share/man:程序说明文件存放目录
    • /var:放置系统执行过程中经常变化的文件
      • /var/log:随时更改的日志文件
      • /var/spool/mail:邮件存放的目录
      • /var/run:程序或服务启动后,其 PID 存放在该目录下
    Ubuntu的常见快捷键

    可以在System Setting -> Keyboard -> Shortcuts中查看各种快捷键.

    • 终端: Ctrl+Alt+T
    • 终端新建标签页: Ctrl+Shift+T
    • 终端复制粘贴: Ctrl+Shift+C, Ctrl+Shift+V
    • 显示常用快捷键: 按住Super(Win)不动
    • 截活动窗口图: Alt+Print
    • 区域截图: Shift+Print
    • 源切换: Super(Win)+Space
    • 安装: sudo apt-get install
    • 卸载: sudo apt-get remove
    • 移除没用的包: sudo apt-get autoremove
    Ubuntu的常见设置
    首先语言设置
    • 通过右上角的 设置按钮 找到System Settings...
    • 然后选中Language Support 项
    • 注意Ubuntu的语言选项有多种语言.将第一语言设置为中文(因为如果中文显示不了的,会使用英文显示)




    • 设置完成后.选择Apply System-wide(应用到整个系统)这时,输入管理员密码以确认.最后点击 Close 按钮关闭对话框,重启电脑。


    注意:重启成功后,会让你选择文件夹名称显示.如果是为了学习.我建议大家保持原来的文件夹名称,这样便于后期在学习中熟悉Linux目录结构. 选择Keep Old Names





    Launcher(菜单栏)设置

    在系统设置中,找不到菜单栏的位置设置.所以只能通过终端命令进行设置

    • 菜单栏靠左(注意参数首字母大写)
    $ gsettings set com.canonical.Unity.Launcher launcher-position Left
    • 菜单栏靠下
    $ gsettings set com.canonical.Unity.Launcher launcher-position Bottom
    Ubuntu常用软件
    • 设置软件源: 默认的软件源是官方的, 速度慢的令人发指, 所以需要先设置一个速度较快的软件源, System Settings -> Software & Updates -> Ubuntu Software -> Download from选择Others, 然后自动选择一个网速比较快的服务器(多半是某个大学的)即可:
    • apt(Advanced Packaging Tool) 安装/卸载软件 (Ctrl+Alt+T 调出终端)

    安装软件

    $ sudo apt install 软件包

    卸载软件

    $ sudo apt remove 软件名

    更新已安装的包

    $ sudo apt upgrade  或者 sudo apt-get upgrade
    升级

     sudo apt-get update.

    那么由于有些Ubuntu中没有自带vim 而是 vi 这个古老的编辑器.所以我们需要安装vim

    sudo apt-get install vim
    在安装过程中有可能出现下列错误
    vim : 依赖: vim-common (= 2:7.4.826-1ubuntu1) 但是 2:7.4.1689-3ubuntu1.1 正要被安装
    E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。

    解决方案:

    sudo apt-get remove vim-common
    sudo apt-get install vim


    作者:Hank
    链接:https://www.jianshu.com/p/2ca7f448ffa7




    收起阅读 »

    汇编-函数本质(下)

    函数的返回值一般是一个指针,不会超过8字节。寄存器就完全够用了。如果要返回一个结构体类型超过字节。下面的例子(结构体占用字节):汇编代码:str这里没有使用作为返回值,而是使用了栈空间。8字节,也会保存在栈中返回(上一个函数栈空间)struct str { ...
    继续阅读 »

    篇幅限制,分为2篇


    返回值



    函数的返回值一般是一个指针,不会超过8字节。X0寄存器就完全够用了。如果要返回一个结构体类型超过8字节。
    下面的例子(str结构体占用24字节):

    struct str {
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    };

    struct str getStr(int a, int b, int c, int d, int e, int f) {
    struct str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    return str1;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    struct str str2 = getStr(1,2,3,4,5,6);
    }

    汇编代码:

    TestDemo`-[ViewController viewDidLoad]:
    0x1042b5e58 <+0>: sub sp, sp, #0x50 ; =0x50
    0x1042b5e5c <+4>: stp x29, x30, [sp, #0x40]
    0x1042b5e60 <+8>: add x29, sp, #0x40 ; =0x40
    0x1042b5e64 <+12>: stur x0, [x29, #-0x8]
    0x1042b5e68 <+16>: stur x1, [x29, #-0x10]
    0x1042b5e6c <+20>: ldur x8, [x29, #-0x8]
    0x1042b5e70 <+24>: add x9, sp, #0x20 ; =0x20
    0x1042b5e74 <+28>: str x8, [sp, #0x20]
    0x1042b5e78 <+32>: adrp x8, 4
    0x1042b5e7c <+36>: add x8, x8, #0x418 ; =0x418
    0x1042b5e80 <+40>: ldr x8, [x8]
    0x1042b5e84 <+44>: str x8, [x9, #0x8]
    0x1042b5e88 <+48>: adrp x8, 4
    0x1042b5e8c <+52>: add x8, x8, #0x3e8 ; =0x3e8
    0x1042b5e90 <+56>: ldr x1, [x8]
    0x1042b5e94 <+60>: mov x0, x9
    0x1042b5e98 <+64>: bl 0x1042b6564 ; symbol stub for: objc_msgSendSuper2
    //x8指向栈空间的区域,预留足够的空间
    0x1042b5e9c <+68>: add x8, sp, #0x8 ; =0x8
    0x1042b5ea0 <+72>: mov w0, #0x1
    0x1042b5ea4 <+76>: mov w1, #0x2
    0x1042b5ea8 <+80>: mov w2, #0x3
    0x1042b5eac <+84>: mov w3, #0x4
    0x1042b5eb0 <+88>: mov w4, #0x5
    0x1042b5eb4 <+92>: mov w5, #0x6
    0x1042b5eb8 <+96>: bl 0x1042b5e04 ; getStr at ViewController.m:59
    -> 0x1042b5ebc <+100>: ldp x29, x30, [sp, #0x40]
    0x1042b5ec0 <+104>: add sp, sp, #0x50 ; =0x50
    0x1042b5ec4 <+108>: ret
    str函数:

        TestDemo`getStr:
    -> 0x1001d1e04 <+0>: sub sp, sp, #0x20 ; =0x20
    //参数分别放入栈中
    0x1001d1e08 <+4>: str w0, [sp, #0x1c]
    0x1001d1e0c <+8>: str w1, [sp, #0x18]
    0x1001d1e10 <+12>: str w2, [sp, #0x14]
    0x1001d1e14 <+16>: str w3, [sp, #0x10]
    0x1001d1e18 <+20>: str w4, [sp, #0xc]
    0x1001d1e1c <+24>: str w5, [sp, #0x8]

    //取出来放入w9,
    0x1001d1e20 <+28>: ldr w9, [sp, #0x1c]
    //存入x8,也就是上一个栈中直到写完
    0x1001d1e24 <+32>: str w9, [x8]
    0x1001d1e28 <+36>: ldr w9, [sp, #0x18]
    0x1001d1e2c <+40>: str w9, [x8, #0x4]
    0x1001d1e30 <+44>: ldr w9, [sp, #0x14]
    0x1001d1e34 <+48>: str w9, [x8, #0x8]
    0x1001d1e38 <+52>: ldr w9, [sp, #0x10]
    0x1001d1e3c <+56>: str w9, [x8, #0xc]
    0x1001d1e40 <+60>: ldr w9, [sp, #0xc]
    0x1001d1e44 <+64>: str w9, [x8, #0x10]
    0x1001d1e48 <+68>: ldr w9, [sp, #0x8]
    0x1001d1e4c <+72>: str w9, [x8, #0x14]
    //栈平衡,这里没有以 x0 作为返回值,已经全部写入上一个函数栈x8中。
    0x1001d1e50 <+76>: add sp, sp, #0x20 ; =0x20
    0x1001d1e54 <+80>: ret
    这里没有使用X0作为返回值,而是使用了栈空间。



    如果返回值大于8字节,也会保存在栈中返回(上一个函数栈空间)

    那么结构体参数超过8个呢?
    猜测参数和返回值都存在上一个函数的栈中,参数应该在低地址。返回值在高地址。


    struct str {
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    int g;
    int h;
    int i;
    int j;
    };

    struct str getStr(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) {
    struct str str1;
    str1.a = a;
    str1.b = b;
    str1.c = c;
    str1.d = d;
    str1.e = e;
    str1.f = f;
    str1.g = g;
    str1.h = h;
    str1.i = i;
    str1.j = j;
    return str1;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    struct str str2 = getStr(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    printf("%d",func(10,20));
    }

    ⚠️:有两个函数 A BA -> B,在B执行完后A传递给B的参数释放了么?
    在上面的例子中910没有释放,相当于A的局部变量。

    对应的汇编代码:

    TestDemo`-[ViewController viewDidLoad]:
    //函数开始
    0x100c31ee4 <+0>: sub sp, sp, #0x60 ; =0x60
    0x100c31ee8 <+4>: stp x29, x30, [sp, #0x50]
    0x100c31eec <+8>: add x29, sp, #0x50 ; =0x50

    //参数入栈
    0x100c31ef0 <+12>: stur x0, [x29, #-0x8]
    0x100c31ef4 <+16>: stur x1, [x29, #-0x10]
    //x8获取参数x0
    0x100c31ef8 <+20>: ldur x8, [x29, #-0x8]
    //x9指向 x29 - 0x20
    0x100c31efc <+24>: sub x9, x29, #0x20 ; =0x20
    //x8 存入 x29 - 0x20
    0x100c31f00 <+28>: stur x8, [x29, #-0x20]

    //address page 内存中取数据
    0x100c31f04 <+32>: adrp x8, 4
    0x100c31f08 <+36>: add x8, x8, #0x418 ; =0x418
    //x8 所指的内存取出来
    0x100c31f0c <+40>: ldr x8, [x8]
    0x100c31f10 <+44>: str x8, [x9, #0x8]
    0x100c31f14 <+48>: adrp x8, 4
    0x100c31f18 <+52>: add x8, x8, #0x3e8 ; =0x3e8
    0x100c31f1c <+56>: ldr x1, [x8]
    0x100c31f20 <+60>: mov x0, x9
    0x100c31f24 <+64>: bl 0x100c32584 ; symbol stub for: objc_msgSendSuper2
    //x8指向 sp + 0x8
    0x100c31f28 <+68>: add x8, sp, #0x8 ; =0x8
    0x100c31f2c <+72>: mov w0, #0x1
    0x100c31f30 <+76>: mov w1, #0x2
    0x100c31f34 <+80>: mov w2, #0x3
    0x100c31f38 <+84>: mov w3, #0x4
    0x100c31f3c <+88>: mov w4, #0x5
    0x100c31f40 <+92>: mov w5, #0x6
    0x100c31f44 <+96>: mov w6, #0x7
    0x100c31f48 <+100>: mov w7, #0x8
    //sp的值给x9
    0x100c31f4c <+104>: mov x9, sp
    //9 w10
    0x100c31f50 <+108>: mov w10, #0x9
    //w10写入 x9 所指向的地址
    0x100c31f54 <+112>: str w10, [x9]
    //10 w10
    0x100c31f58 <+116>: mov w10, #0xa
    //w10写入 x9 所指向的地址 偏移4个字节
    0x100c31f5c <+120>: str w10, [x9, #0x4]
    //跳转getStr
    0x100c31f60 <+124>: bl 0x100c31e58 ; getStr at ViewController.m:31

    //函数结束
    -> 0x100c31f64 <+128>: ldp x29, x30, [sp, #0x50]
    0x100c31f68 <+132>: add sp, sp, #0x60 ; =0x60
    0x100c31f6c <+136>: ret
    str:

    TestDemo`getStr:
    //开辟空间
    0x100c31e58 <+0>: sub sp, sp, #0x30 ; =0x30
    //从上一个栈空间 获取9 10
    0x100c31e5c <+4>: ldr w9, [sp, #0x30]
    0x100c31e60 <+8>: ldr w10, [sp, #0x34]
    //参数入栈
    0x100c31e64 <+12>: str w0, [sp, #0x2c]
    0x100c31e68 <+16>: str w1, [sp, #0x28]
    0x100c31e6c <+20>: str w2, [sp, #0x24]
    0x100c31e70 <+24>: str w3, [sp, #0x20]
    0x100c31e74 <+28>: str w4, [sp, #0x1c]
    0x100c31e78 <+32>: str w5, [sp, #0x18]
    0x100c31e7c <+36>: str w6, [sp, #0x14]
    0x100c31e80 <+40>: str w7, [sp, #0x10]
    0x100c31e84 <+44>: str w9, [sp, #0xc]
    0x100c31e88 <+48>: str w10,[sp, #0x8]

    //获取参数分别存入上一个栈x8所指向的地址中
    -> 0x100c31e8c <+52>: ldr w9, [sp, #0x2c]
    0x100c31e90 <+56>: str w9, [x8]
    0x100c31e94 <+60>: ldr w9, [sp, #0x28]
    0x100c31e98 <+64>: str w9, [x8, #0x4]
    0x100c31e9c <+68>: ldr w9, [sp, #0x24]
    0x100c31ea0 <+72>: str w9, [x8, #0x8]
    0x100c31ea4 <+76>: ldr w9, [sp, #0x20]
    0x100c31ea8 <+80>: str w9, [x8, #0xc]
    0x100c31eac <+84>: ldr w9, [sp, #0x1c]
    0x100c31eb0 <+88>: str w9, [x8, #0x10]
    0x100c31eb4 <+92>: ldr w9, [sp, #0x18]
    0x100c31eb8 <+96>: str w9, [x8, #0x14]
    0x100c31ebc <+100>: ldr w9, [sp, #0x14]
    0x100c31ec0 <+104>: str w9, [x8, #0x18]
    0x100c31ec4 <+108>: ldr w9, [sp, #0x10]
    0x100c31ec8 <+112>: str w9, [x8, #0x1c]
    0x100c31ecc <+116>: ldr w9, [sp, #0xc]
    0x100c31ed0 <+120>: str w9, [x8, #0x20]
    0x100c31ed4 <+124>: ldr w9, [sp, #0x8]
    0x100c31ed8 <+128>: str w9, [x8, #0x24]
    //恢复栈
    0x100c31edc <+132>: add sp, sp, #0x30 ; =0x30
    0x100c31ee0 <+136>: ret



    和之前的猜测相符。

    函数的局部变量


    int func1(int a, int b) {
    int c = 6;
    return a + b + c;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    func1(10, 20);
    }

    对应的汇编指令:

    TestDemo`func1:
    -> 0x104bc5e40 <+0>: sub sp, sp, #0x10 ; =0x10
    0x104bc5e44 <+4>: str w0, [sp, #0xc]
    0x104bc5e48 <+8>: str w1, [sp, #0x8]
    //局部变量c存入自己的栈区
    0x104bc5e4c <+12>: mov w8, #0x6
    0x104bc5e50 <+16>: str w8, [sp, #0x4]
    0x104bc5e54 <+20>: ldr w8, [sp, #0xc]
    0x104bc5e58 <+24>: ldr w9, [sp, #0x8]
    0x104bc5e5c <+28>: add w8, w8, w9
    0x104bc5e60 <+32>: ldr w9, [sp, #0x4]
    0x104bc5e64 <+36>: add w0, w8, w9
    0x104bc5e68 <+40>: add sp, sp, #0x10 ; =0x10
    0x104bc5e6c <+44>: ret
    函数的局部变量放在栈里面!(自己的栈)
    那么有嵌套调用呢?

    int func1(int a, int b) {
    int c = 6;
    int d = func2(a, b, c);
    int e = func2(a, b, c);
    return d + e;
    }

    int func2(int a, int b, int c) {
    int d = a + b + c;
    printf("%d",d);
    return d;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    func1(10, 20);
    }
    对应的汇编:

    TestDemo`func1:
    //函数的开始
    -> 0x100781d9c <+0>: sub sp, sp, #0x30 ; =0x30
    0x100781da0 <+4>: stp x29, x30, [sp, #0x20]
    0x100781da4 <+8>: add x29, sp, #0x20 ; =0x20

    //参数入栈
    0x100781da8 <+12>: stur w0, [x29, #-0x4]
    0x100781dac <+16>: stur w1, [x29, #-0x8]

    //局部变量入栈
    0x100781db0 <+20>: mov w8, #0x6
    0x100781db4 <+24>: stur w8, [x29, #-0xc]

    //读取参数和局部变量
    0x100781db8 <+28>: ldur w0, [x29, #-0x4]
    0x100781dbc <+32>: ldur w1, [x29, #-0x8]
    0x100781dc0 <+36>: ldur w2, [x29, #-0xc]

    //执行func2
    0x100781dc4 <+40>: bl 0x100781df8 ; func2 at ViewController.m:86
    //func2 返回值入栈
    0x100781dc8 <+44>: str w0, [sp, #0x10]

    //读取参数和局部变量
    0x100781dcc <+48>: ldur w0, [x29, #-0x4]
    0x100781dd0 <+52>: ldur w1, [x29, #-0x8]
    0x100781dd4 <+56>: ldur w2, [x29, #-0xc]

    //第二次执行func2
    0x100781dd8 <+60>: bl 0x100781df8 ; func2 at ViewController.m:86

    //func2 返回值入栈
    0x100781ddc <+64>: str w0, [sp, #0xc]

    //读取两次 func2 返回值
    0x100781de0 <+68>: ldr w8, [sp, #0x10]
    0x100781de4 <+72>: ldr w9, [sp, #0xc]
    //相加存入w0返回上层函数
    0x100781de8 <+76>: add w0, w8, w9

    //函数的结束
    0x100781dec <+80>: ldp x29, x30, [sp, #0x20]
    0x100781df0 <+84>: add sp, sp, #0x30 ; =0x30
    0x100781df4 <+88>: ret
    可以看到参数被保存到栈中。
    ⚠️:现场保护包含:FPLR参数返回值

    总结

      • 是一种具有特殊的访问方式的存储空间(后进先出,LIFO)
      • SP和FP寄存器
        • sp寄存器在任意时刻保存栈顶的地址
        • fp(x29)寄存器属于通用寄存器,在某些时刻利用它保存栈底的地址(嵌套调用)
      • ARM64里面栈的操作16字节对齐
      • 栈读写指令
        • 读:ldr(load register)指令LDR、LDP
        • 写:str(store register)指令STR、STP
      • 汇编练习
        • 指令:
          • sub sp, sp,#0x10 ;拉伸栈空间16个字节
          • stp x0,x1,[sp];往sp所在位置存放x0和x1
          • ldp x0,x1,[sp];读取sp存入x0和x1
          • add sp,#0x10;恢复栈空间
        • 简写:
          • stp x0, x1,[sp,#-0x10]!;前提条件是正好开辟的空间放满栈。先开辟空间,存入值,再改变sp的值。
          • ldp x0,x1,[sp],#0x10
    • bl指令
      • 跳转指令:bl标号,转到标号处执行指令并将下一条指令的地址保存到lr寄存器
      • B代表跳转
      • L代表lr(x30)寄存器
    • ret指令
      • 类似函数中的return
      • 让CPU执行lr寄存器所指向的指令
      • 有跳转需要“保护现场”
    • 函数
      • 函数调用栈
        • ARM64中栈是递减栈,向低地址延伸的栈
        • SP寄存器指向栈顶的位置
        • X29(FP)寄存器指向栈底的位置
      • 函数的参数
        • ARM64中,默认情况下参数是放在X0~X7的8个寄存器中
        • 如果是浮点数,会用浮点寄存器
        • 如果超过8个参数会用栈传递(多过8个的参数在函数调用结束后参数不会释放,相当于局部变量,属于调用方,只有调用方函数执行结束栈平衡后才释放。)
      • 函数的返回值
        • 一般情况下函数的返回值使用X0寄存器保存
        • 如果返回值大于了8个字节(放不下),就会利用内存。写入上一个调用栈内部,用X8寄存器作为参照。
      • 函数的局部变量
        • 使用栈保存局部变量
      • 函数的嵌套调用
        • 会将X29,X30寄存器入栈保护。
        • 同时现场保护的还有:FP,LR,参数,返回值。


    收起阅读 »

    汇编-函数本质(上)

    栈函数调用栈恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。内存读写指令⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x02和0x03。str(store register)指...
    继续阅读 »


    栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)


    SP和FP寄存器

    • sp寄存器在任意时刻会保存我们栈顶的地址。
    • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!(没有出现函数嵌套调用的时候不需要fp,相当于分界点)
      ⚠️:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stpARM64里面 对栈的操作是16字节对齐的!!

    ARM64是先开辟一段栈空间,fp移动到栈顶再往栈中存放内容(编译期就已经确定大小)。不存在push操作。在iOS中栈是往低地址开辟空间




    函数调用栈

    常见的函数调用开辟和恢复的栈空间:

    //开辟栈空间
    sub sp, sp, #0x40 ; 拉伸0x4064字节)空间
    stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
    add x29, sp, #0x30 ; x29指向栈帧的底部
    ...
    //恢复栈空间
    ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
    add sp, sp, #0x40 ;栈平衡
    ret

    恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。

    内存读写指令

    ⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x020x03

    str(store register)指令
    将数据从寄存器中读出来,存到内存中。

    ldr(load register)指令
    将数据从内存中读出来,存到寄存器中。

    ldr 和 str 的变种 ldp 和 stp 还可以操作2个寄存器。


    堆栈操作案例

    使用32个字节空间作为这段程序的栈空间,然后利用栈将x0x1的值进行交换。

    .text
    .global _C

    _C:
    sub sp, sp, #0x20 ;拉伸栈空间32个字节
    stp x0, x1, [sp, #0x10] ;sp 偏移 16字节存放 x0和x1 []的意思是寻址。这sp并没有改变
    ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0。这里内存相当于temp 交换了 x0 和 x1。寄存器中的值交换了,内存中的值不变。
    add sp, sp, #0x20 ;恢复栈空间
    ret
    这段代码相当于 x0,x1遍历,sp和内存没有变。
    栈空间分配:





    断点调试

    0x102e6e518断点处对x0x1分别赋值0xa0xb。然后单步执行:




    拉伸后sp也变了。

    (lldb) register write x0 0xa
    (lldb) register write x1 0xb
    (lldb) register read sp
    sp = 0x000000016cf95b30
    (lldb) register read sp
    sp = 0x000000016cf95b10
    (lldb)

    看下0x000000016cf95b10的空间:




    目前还没有写入内存,是脏数据。接着单步执行:



    这个时候x0x1的数据完成了交换。内存的数据并没有变化。
    继续单步执行:

    (lldb) register write x0 0xa
    (lldb) register write x1 0xb
    (lldb) register read sp
    sp = 0x000000016cf95b30
    (lldb) register read sp
    sp = 0x000000016cf95b10
    (lldb) register read sp
    sp = 0x000000016cf95b30
    (lldb)

    sp还原了,栈空间释放,这时候0xa0xb还依然存在内存中,等待下次拉伸栈空间写数据覆盖:




    bl和ret指令

    bl标号

    • 将下一条指令的地址放入lr(x30)寄存器
    • 转到标号处执行指令

    b就是跳转,l将下一条指令的地址放入lr(x30)寄存器。


    lr相当于保存的”回家的路“。


    ret

    • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

    ret只会看lr


    ARM64平台的特色指令,它面向硬件做了优化处理。



    x30寄存器

    x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!
    一个嵌套调用的案例,汇编代码如下:

    .text
    .global _C, _D

    _C:
    mov x0,#0xaaaa
    bl _D
    mov x0,#0xaaaa
    ret

    _D:
    mov x0,#0xbbbb
    ret
    ViewController.m中调用:

    int C();
    int D();
    - (void)viewDidLoad {
    [super viewDidLoad];
    printf("C");
    C();
    printf("D");
    }
    C();打断点执行,进入C中:






    继续执行发现一直在0x104c8e4f80x104c8e4fc中跳转返不回去viewDidLoad中了,发生了死循环。

    ->  0x104c8e4f8 <+8>:  mov    x0, #0xaaaa
    0x104c8e4fc <+12>: ret
    那么如果要返回,就必须将viewDidLoad中下一条指令告诉lr,这个时候就必须在bl之前保护lr寄存器(遇到bllr就会改变。需要保护“回家的路”)。那么这个时候能不能把lr保存到其它寄存器?这里我们没法保证其它寄存器不会被使用。这个时候唯一属于当前函数的也就是自己的栈区了。保存到栈区应该就能解决了。
    可以看下系统是怎么实现的,写一个c函数断点调试看下:

    void c() {
    d();
    return;;
    }

    void d() {

    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    c();
    }

    系统的实现如下:



    TestDemo`c:
    //边开辟空间边写入 x29(fp) x30(lr) 的值。[sp, #-0x10]! !代表赋值给sp,相当于 sp -= 0x10
    -> 0x102a21e84 <+0>: stp x29, x30, [sp, #-0x10]!
    0x102a21e88 <+4>: mov x29, sp
    0x102a21e8c <+8>: bl 0x102a21e98 ; d at ViewController.m:34:1
    //将sp所指向的地址读取给x29,x30。[sp], #0x10 等价于 sp += 0x10
    0x102a21e90 <+12>: ldp x29, x30, [sp], #0x10
    0x102a21e94 <+16>: ret

    可以看到系统先开辟栈空间,然后将x29x30寄存器的值存入栈区。在ret之前恢复x29x30的值。

    • stp x29, x30, [sp, #-0x10]!:开辟空间并将x29x30存入栈区。!代表赋值给sp,相当于 sp -= 0x10
    • ldp x29, x30, [sp], #0x10:将栈区的值给x29x30并回收空间。[sp], #0x10 等价于 sp += 0x10

    那么对于CD的案例自己实现下保存和恢复lr寄存器。


    .text
    .global _C, _D

    _C:
    //sub sp,sp,#0x10
    //str x30,[sp] ;等价
    str x30, [sp,#-0x10]! ;16字节对齐,必须最小0x10
    mov x0,#0xaaaa
    bl _D
    mov x0,#0xaaaa
    //ldr x30,[sp]
    //add sp,#0x10 ;等价
    ldr x30,[sp],#0x10
    ret

    _D:
    mov x0,#0xbbbb
    ret



    这个时候进入Dlr值已经发生变化。



    继续执行正常返回viewDidload了,这个时候死循环就已经解决了。

    ⚠️:在函数嵌套调用的时候,需要将x30入栈!开辟空间需要16字节对齐。如果开辟8字节再读的时候会坏地址访问。写的时候没问题。




    函数的参数和返回值

    先看下系统的实现:

    int sum(int a, int b) {
    return a + b;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    sum(10,20);
    }




    可以看到变量1020分别存入了w0w1
    sum调用如下(release模式下编译器会优化):

    TestDemo`sum:
    //开辟空间
    -> 0x100121e68 <+0>: sub sp, sp, #0x10 ; =0x10
    //w0 w1 存入栈中
    0x100121e6c <+4>: str w0, [sp, #0xc]
    0x100121e70 <+8>: str w1, [sp, #0x8]
    //从栈中读取参数
    0x100121e74 <+12>: ldr w8, [sp, #0xc]
    0x100121e78 <+16>: ldr w9, [sp, #0x8]
    //参数相加存入w0
    0x100121e7c <+20>: add w0, w8, w9
    //恢复栈空间
    0x100121e80 <+24>: add sp, sp, #0x10 ; =0x10
    //返回
    0x100121e84 <+28>: ret
    从上面可以看出返回值在w0中。那么自己实现sum函数的汇编代码:

    .text
    .global _suma

    _suma:
    add x0,x0,x1
    ret
    调用:

    int suma(int a, int b);
    - (void)viewDidLoad {
    [super viewDidLoad];
    printf("%d",suma(10,20));
    }

    ⚠️ARM64下,函数的参数是存放在X0X7(W0W7)这8个寄存器里面的。如果超过8个参数就会入栈。那么oc的方法最好不要超过6个(selfcmd)。
    函数的返回值是放在X0寄存器里面的。


    参数超过8个


    int test(int a, int b, int c ,int d, int e, int f, int g, int h, int i) {
    return a + b + c + d + e + f + g + h + i;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    test(1, 2, 3, 4, 5, 6, 7, 8, 9);
    }




    可以看到前8个参数分别保存在w0~w7寄存器中,第9个参数先保存在w10中,然后写入x8中(这个时候x8指向sp,相当于第9个参数写入了当前函数栈中)。


    TestDemo`-[ViewController viewDidLoad]:
    //拉伸栈空间,保存fp lr
    0x100f09e5c <+0>: sub sp, sp, #0x40 ; =0x40
    0x100f09e60 <+4>: stp x29, x30, [sp, #0x30]

    //fp指向 sp+0x30
    0x100f09e64 <+8>: add x29, sp, #0x30 ; =0x30
    //fp-0x8 存放x0
    0x100f09e68 <+12>: stur x0, [x29, #-0x8]
    //fp-0x10 存放x1
    0x100f09e6c <+16>: stur x1, [x29, #-0x10]
    //fp-0x8 给到 x8
    0x100f09e70 <+20>: ldur x8, [x29, #-0x8]
    //sp+0x10 指针给到 x9
    0x100f09e74 <+24>: add x9, sp, #0x10 ; =0x10
    //x8写入 sp+0x10
    0x100f09e78 <+28>: str x8, [sp, #0x10]

    //adrp = address page 内存中取数据
    0x100f09e7c <+32>: adrp x8, 4
    0x100f09e80 <+36>: add x8, x8, #0x418 ; =0x418
    //x8所指向的内容去出来
    0x100f09e84 <+40>: ldr x8, [x8]
    //x8写入栈中,这个时候x9指向地址,这个时候是一个新的x8
    0x100f09e88 <+44>: str x8, [x9, #0x8]
    0x100f09e8c <+48>: adrp x8, 4
    0x100f09e90 <+52>: add x8, x8, #0x3e8 ; =0x3e8
    0x100f09e94 <+56>: ldr x1, [x8]
    0x100f09e98 <+60>: mov x0, x9
    0x100f09e9c <+64>: bl 0x100f0a568 ; symbol stub for: objc_msgSendSuper2

    //sp 一直没有改变过,w0~w7 分别存放前8个参数
    0x100f09ea0 <+68>: mov w0, #0x1
    0x100f09ea4 <+72>: mov w1, #0x2
    0x100f09ea8 <+76>: mov w2, #0x3
    0x100f09eac <+80>: mov w3, #0x4
    0x100f09eb0 <+84>: mov w4, #0x5
    0x100f09eb4 <+88>: mov w5, #0x6
    0x100f09eb8 <+92>: mov w6, #0x7
    0x100f09ebc <+96>: mov w7, #0x8
    //x8 指向 sp
    -> 0x100f09ec0 <+100>: mov x8, sp
    //参数 9 存入 w10
    0x100f09ec4 <+104>: mov w10, #0x9
    //w10 存入 x8地址中,也就是sp栈底中
    0x100f09ec8 <+108>: str w10, [x8]

    0x100f09ecc <+112>: bl 0x100f09de4 ; test at ViewController.m:41
    0x100f09ed0 <+116>: ldp x29, x30, [sp, #0x30]
    0x100f09ed4 <+120>: add sp, sp, #0x40 ; =0x40
    0x100f09ed8 <+124>: ret



    接着往下直接跳转到test函数中:

    TestDemo`test:
    //开辟空间48字节
    0x100f09de4 <+0>: sub sp, sp, #0x30 ; =0x30

    //从viewDidLoad栈中取数据 第9个参数(读写往高地址)
    0x100f09de8 <+4>: ldr w8, [sp, #0x30]

    //参数入栈,分别占4个字节
    0x100f09dec <+8>: str w0, [sp, #0x2c]
    0x100f09df0 <+12>: str w1, [sp, #0x28]
    0x100f09df4 <+16>: str w2, [sp, #0x24]
    0x100f09df8 <+20>: str w3, [sp, #0x20]
    0x100f09dfc <+24>: str w4, [sp, #0x1c]
    0x100f09e00 <+28>: str w5, [sp, #0x18]
    0x100f09e04 <+32>: str w6, [sp, #0x14]
    0x100f09e08 <+36>: str w7, [sp, #0x10]
    0x100f09e0c <+40>: str w8, [sp, #0xc]

    -> 0x100f09e10 <+44>: ldr w8, [sp, #0x2c]
    0x100f09e14 <+48>: ldr w9, [sp, #0x28]
    0x100f09e18 <+52>: add w8, w8, w9
    0x100f09e1c <+56>: ldr w9, [sp, #0x24]
    0x100f09e20 <+60>: add w8, w8, w9
    0x100f09e24 <+64>: ldr w9, [sp, #0x20]
    0x100f09e28 <+68>: add w8, w8, w9
    0x100f09e2c <+72>: ldr w9, [sp, #0x1c]
    0x100f09e30 <+76>: add w8, w8, w9
    0x100f09e34 <+80>: ldr w9, [sp, #0x18]
    0x100f09e38 <+84>: add w8, w8, w9
    0x100f09e3c <+88>: ldr w9, [sp, #0x14]
    0x100f09e40 <+92>: add w8, w8, w9
    0x100f09e44 <+96>: ldr w9, [sp, #0x10]
    0x100f09e48 <+100>: add w8, w8, w9
    0x100f09e4c <+104>: ldr w9, [sp, #0xc]
    //最终相加结果给 w0
    0x100f09e50 <+108>: add w0, w8, w9
    //栈平衡
    0x100f09e54 <+112>: add sp, sp, #0x30 ; =0x30
    0x100f09e58 <+116>: ret



    最终函数返回值放入w0中,如果在release模式下test不会被调用(被优化掉,因为没有意义,有没有对app没有影响。)

    自己实现一个简单有参数并且嵌套调用的汇编:


    .text
    .global _func,_sum

    _func:
    //sub sp,sp,#0x10
    //stp x29,x30,[sp]
    stp x29,x30,[sp, #-0x10]!
    bl _sum
    //ldp x29,x30,[sp]
    //add sp,sp,#0x10
    ldp x29,x30,[sp],#0x10
    ret
    _sum:
    add x0,x0,x1
    ret


    篇幅限制 分为2篇

    作者:HotPotCat
    链接:https://www.jianshu.com/p/69b9c49b0e71




    收起阅读 »

    汇编-基本概念

    在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。汇编语言的发展机器语言由0...
    继续阅读 »


    在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。


    汇编语言的发展

    机器语言

    01组成的机器指令。0代表有电,1代表没电。

    • 加:0100 0000
    • 减:0100 1000
    • 乘:1111 0111 1110 0000
    • 除:1111 0111 1111 0000

    汇编语言(assembly language)

    为了高效的写代码出现了助记符,使用助记符代替机器语言,如:

    • 加:INC EAX 通过编译器 0100 0000
    • 减:DEC EAX 通过编译器 0100 1000
    • 乘:MUL EAX 通过编译器 1111 0111 1110 0000
    • 除:DIV EAX 通过编译器 1111 0111 1111 0000

    助记符就是汇编语言的前身,当有专门的编译器出现的时候就有了汇编语言。

    高级语言(High-level programming language)

    C\C++\Java\OC\Swift,更加接近人类的自然语言。
    比如C语言:

    • 加:A + B 通过编译器 0100 0000
    • 减:A - B 通过编译器 0100 1000
    • 乘:A * B 通过编译器 1111 0111 1110 0000
    • 除:A / B 通过编译器 1111 0111 1111 0000

    代码在终端设备上的过程:





    • 汇编语言机器语言一一对应,每一条机器指令都有与之对应的汇编指令
    • 汇编语言可以通过编译得到机器语言机器语言可以通过反汇编得到汇编语言
    • 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言(不是一一对应关系,反推出是不准确的,只能大致。)

    汇编语言的特点


    • 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
    • 能够不受编译器的限制,对生成的二进制代码进行完全的控制
    • 目标代码简短,占用内存少,执行速度快
    • 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
    • 开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
    • 不区分大小写,比如movMOV是一样的

    汇编的用途

    • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
    • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编
    • 软件安全
      1.病毒分析与防治
      2.逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
    • 理解整个计算机系统的最佳起点和最有效途径
    • 为编写高效代码打下基础
    • 弄清代码的本质

    汇编语言的种类

    目前讨论比较多的汇编语言有:

    • 8086汇编(8086处理器是16bitCPU
    • Win32汇编
    • Win64汇编
    • ARM汇编(嵌入式、MaciOS
    • ......

    iPhone里面用到的是ARM汇编,但是不同的设备也有差异(因CPU的架构不同)。

    位数架构设备
    32armv6iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch
    32armv7iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4
    32armv7siPhone5, iPhone5C, iPad4(iPad with Retina Display)
    64arm64iPhone5s,iPhone6、7、8,iPhone6、7、8 Plus,iPhone X,iPad Air,iPad mini2(iPad mini with Retina Display)
    64arm64eXS/XS Max/XR/ iPhone 11, iPhone 11 pro 及以后
    64x86_64模拟器64位处理器 (intel)
    32i386模拟器32位处理器(intel)

    ⚠️:苹果A7处理器支持两个不同的指令集:32ARM指令集(armv6|armv7|armv7s)和64ARM指令集(arm64

    汇编相关的学习需要了解CPU等硬件结构,最为重要的是CPU/内存。在汇编中,大部分指令都是和CPU与内存相关的。
    APP/程序的执行过程:





    执行过程:
    1.地址总线先去内存地址。
    2.控制读取发送读/写命令。
    3.数据总线写数据->内存/ 内存发送数据->数据总线

    地址总线

    • 它的宽度决定了CPU的寻址能力(也就是寻址范围)
    • 8086的地址总线宽度是20,所以寻址能力是1M( 220)(这里的M是大小,数量单位)




    内存中的MB是容量单位。如果内存很大, 地址总线宽度不够怎么处理?以前的cpu是通过2次寻址相加得到一个最终的值来访问内存,现在的cpu没有寻址能力的问题。
    数量单位:M,K。1M = 1024K,1K= 1024。比如:10,100
    容量单位:字节Byte。 1024B = 1KB,1024KB = 1MB。比如:10个,100只。(大部分计算机都是以1个字节为单位。银行系统的IBM电脑例外是2个字节为单位。)
    对于100M 宽带,这里的100M是100Mbps(每秒钟传递多少二进制位,bit位。所以100M带宽理论下载速度12.5MB/s)。

    数据总线

    • 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度(吞吐量)
    • 8086的数据总线宽度是16,所以单次最大传递2个字节的数据

    我们现在常说的32位,64位cpu说的就是它的数据吞吐量。1次放电分别4字节,8字节数据。

    控制总线

    • 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制

    案例:
    1.一个CPU 的寻址能力为8KB,那么它的地址总线的宽度为____
    答案:8KB对应 8192, 213 = 8192 所以为13。

    1. 8080,8088,80286,80386 的地址总线宽度分别为16根,20根,24根,32根。那么他们的寻址能力分别为多少____KB, ____MB,____MB,____GB?
      答案:1kb = 210 = 1024
      1kb * 26 = 64kb
      1kb * 1kb = 1mb
      1mb * 24 = 16mb
      1kb * 1kb * 1kb * 22 = 4gb

    2. 8080,8088,8086,80286,80386 的数据总线宽度分别为8根,8根,16根,16根,32根.那么它们一次可以传输的数据为:____B,____B,____B,____B,____B
      答案:1 、1、2、2、4

    4.从内存中读取1024字节的数据,8086至少要读____次,80386至少要读取____次.
    答案:8086 数据总线宽度为16。8086一次读2个字节,那么需要512次,80286数据总线宽度为32,一次4个字节,需要256次。

    内存




  • 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位220个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
  • 0x00000~0x9FFFF:主存储器。可读可写
  • 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
  • 0xC0000~0xFFFFF:存储各种硬件\系统信息。只读

  • 进制

    想学好进制首先要忘掉十进制,也要忘掉进制间的转换。

    进制的定义

    • 八进制由8个符号组成:0 1 2 3 4 5 6 7 逢八进一
    • 十进制由10个符号组成:0 1 2 3 4 5 6 7 8 9逢十进一
    • N进制就是由N个符号组成:逢N进一

    ⚠️:进制的本质是符号。

    案例

    1. 1 + 1 在____情况下等于 3 ?
      除了算错的情况下。在十进制由10个符号组成,假如由: 0 1 3 2 8 A B E S 7组成逢十进一,那么在这种情况下1+1=3

    传统定义的十进制和自定义的十进制不一样。那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的,可以用于加密!
    ⚠️:十进制由十个符号组成,逢十进一,符号是可以自定义的!!!

    1. 八进制运算:
    • 2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
      答案:5,6,11,24
    • 277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .
      答案:632, 20250, 163,47


    八进制加法表
    0 1 2 3 4 5 6 7
    10 11 12 13 14 15 16 17
    20 21 22 23 24 25 26 27
    ...

    1+1 = 2                     
    1+2 = 3 2+2 = 4
    1+3 = 4 2+3 = 5 3+3 = 6
    1+4 = 5 2+4 = 6 3+4 = 7 4+4 = 10
    1+5 = 6 2+5 = 7 3+5 = 10 4+5 = 11 5+5 = 12
    1+6 = 7 2+6 = 10 3+6 = 11 4+6 = 12 5+6 = 13 6+6 = 14
    1+7 = 10 2+7 = 11 3+7 = 12 4+7 = 13 5+7 = 14 6+7 = 15 7+7 = 16

    八进制乘法表
    0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27...

    1*1 = 1                     
    1*2 = 2 2*2 = 4
    1*3 = 3 2*3 = 6 3*3 = 11
    1*4 = 4 2*4 = 10 3*4 = 14 4*4 = 20
    1*5 = 5 2*5 = 12 3*5 = 17 4*5 = 24 5*5 = 31
    1*6 = 6 2*6 = 14 3*6 = 22 4*6 = 30 5*6 = 36 6*6 = 44
    1*7 = 7 2*7 = 16 3*7 = 25 4*7 = 34 5*7 = 43 6*7 = 52 7*7 = 61

    二进制的简写形式

                   二进制: 1 0 1 1 1 0 1 1 1 1 0 0
    三个二进制一组: 101 110 111 100
                    八进制:    5     6     7      4
    四个二进制一组: 1011 1011 1100
                十六进制:     b        b       c

    二进制:从 0 写到 1111
    0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
    这种二进制使用起来太麻烦,改成更简单一点的符号:
    0 1 2 3 4 5 6 7 8 9 A B C D E F 这就是十六进制了

    数据的宽度

    数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。


    int test() {
    int cTemp = 0x1FFFFFFFF;
    return cTemp;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%x",test());
    }
    输出:

    ffffffff
    数据溢出了。刚开始cTemp默认值1,溢出后变为-1第一位符号位,1代表负数,0代表正数。往后逐位取反,末尾加1。)。

    (lldb) p cTemp
    (int) $0 = 1
    (lldb) p cTemp
    (int) $1 = -1
    (lldb) p &cTemp
    (int *) $2 = 0x000000016b3a9b1c
    (lldb) x 0x000000016b3a9b1c
    0x16b3a9b1c: ff ff ff ff 10 00 00 00 00 00 00 00 ef 98 3a 6b ..............:k
    0x16b3a9b2c: 01 00 00 00 70 a9 f0 59 01 00 00 00 50 d4 a5 04 ....p..Y....P...
    (lldb) p (uint)cTemp
    (uint) $3 = 4294967295

    Debug -> Debug Workflow -> View Memory中也可以查看(这里查看内容更新后需要翻页刷新然后切换回来才能显示新值):




    再看下汇编代码(Debug -> Debug Workflow -> Always Show Disassembly):


    TestDemo`test:
    0x104a59ec8 <+0>: sub sp, sp, #0x10 ; =0x10
    0x104a59ecc <+4>: mov w8, #-0x1
    0x104a59ed0 <+8>: str w8, [sp, #0xc]
    -> 0x104a59ed4 <+12>: ldr w0, [sp, #0xc]
    0x104a59ed8 <+16>: add sp, sp, #0x10 ; =0x10
    0x104a59edc <+20>: ret

    可以看到直接将-1给力w8。指令在内存中占用4字节。

    计算机中常见的数据宽度

    • 位(Bit): 1个位就是1个二进制位。0或者1
    • 字节(Byte): 1个字节由8个Bit组成(8位)。内存中的最小单元Byte
    • 字(Word): 1个字由2个字节组成(16位),这2个字节分别称为高字节低字节
    • 双字(Doubleword): 1个双字由两个字组成(32位)。

    计算机存储数据会分为有符号数和无符号数(对于数据本身内容没有变化,取决于你怎么看):


    无符号数,直接换算!
    有符号数:
    正数: 0 1 2 3 4 5 6 7
    负数: F E D B C A 9 8
    -1 -2 -3 -4 -5 -6 -7 -8

    自定义进制符号

    案例:

    • 现在有10进制数10个符号分别是:2,9,1,7,6,5,4, 8,3 , A 逢10进1 那么: 123 + 234 = ____

    十进制:
    0 1 2 3 4 5 6 7 8 9
    自定义:
    2 9 1 7 6 5 4 8 3 A
    92 99 91 97 96 95 94 98 93 9A
    12 19 11 17 16 15 14 18 13 1A
    72 79 71 77 76 75 74 78 73 7A
    62 69 61 67 66 65 64 68 63 6A
    52 59 51 57 56 55 54 58 53 5A
    42 49 41 47 46 45 44 48 43 4A
    82 89 81 87 86 85 84 88 83 8A
    32 39 31 37 36 35 34 38 33 3A
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = A 5+5 = 92
    9+4 = 8 1+4 = 3 7+4 = a 6+4 = 92 5+4 = 99 4+4 = 91
    9+8 = 3 1+8 = A 7+8 = 92 6+8 = 99 5+8 = 91 4+8 = 97 8+8 = 96
    9+3 = A 1+3 = 92 7+3 = 99 6+3 = 91 5+3 = 97 4+3 = 96 8+3 = 95 3+3 = 94
    9+A = 92 1+A = 99 7+A = 91 6+A = 97 5+A = 96 4+A = 95 8+A = 94 3+A = 98 A+A = 93

    123 + 234 = 1A6

    • 现在有9进制数 9个符号分别是:2,9,1,7,6,5,4, 8,3 逢9进1 那么: 123 + 234 = __

    十进制:
    0 1 2 3 4 5 6 7 8
    自定义:
    2 9 1 7 6 5 4 8 3
    92 99 91 97 96 95 94 98 93
    12 19 11 17 16 15 14 18 13
    72 79 71 77 76 75 74 78 73
    62 69 61 67 66 65 64 68 63
    52 59 51 57 56 55 54 58 53
    42 49 41 47 46 45 44 48 43
    82 89 81 87 86 85 84 88 83
    32 39 31 37 36 35 34 38 33
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = 92 5+5 = 99
    9+4 = 8 1+4 = 3 7+4 = 92 6+4 = 99 5+4 = 91 4+4 = 97
    9+8 = 3 1+8 = 92 7+8 = 99 6+8 = 91 5+8 = 97 4+8 = 96 8+8 = 95
    9+3 = 92 1+3 = 99 7+3 = 91 6+3 = 97 5+3 = 96 4+3 = 95 8+3 = 94 3+3 = 98

    123 + 234 = 725

    CPU&寄存器

    内部部件之间由总线连接


    CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。

    CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器

    对于arm64系的CPU来说, 如果寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的

    • 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
    • 不同的CPU,寄存器的个数、结构是不相同的

    浮点寄存器

    因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数。

    • 浮点寄存器 64位D0 - D31 32位: S0 - S31

    向量寄存器

    现在的CPU支持向量运算。(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

    • 向量寄存器 128位:V0-V31

    通用寄存器

    • 通用寄存器也称数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
    • ARM64拥有32个64位的通用寄存器x0x30,以及XZR(零寄存器),这些通用寄存器有时也有特定用途。
      1.64位X0-X30, XZR(零寄存器)w0 到 w28 这些是32位的。因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
      2.32位W0-W30, WZR(零寄存器)。 w0 就是 x0 的低32位!

    ⚠️:了解过8086汇编的都知道,有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中。在ARM中并没有。

    在"Xcode"中我们可以查看具体寄存器的内容:




    分别看一下x0w0的值:
    x0  unsigned long   0x0000000159f0a970
    w0 unsigned int 0x59f0a970

    验证了w0x0的低32位。

    通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
    假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间:




    pc寄存器

    单步执行汇编代码(pc始终指向下一条指令):




  • 为指令指针寄存器,它指示了CPU当前要读取指令的地址(指向下一条即将执行的指令
  • 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
  • CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义

  • 比如 1110 0000 0000 0011 0000 1000 1010 1010,
    可以当做数据 0xE003008AA。
    也可以当做指令 mov x0, x8

    • CPU根据什么将内存中的信息看做指令?

    CPU将pc指向的内存单元的内容看做指令
    如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过。

    高速缓存

    iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M。

    CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成)。CPU直接从高速缓存依次读取指令来执行。

    bl指令

    bl分位bl:
    b:跳转。
    l:lr寄存器。

    • CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令
    • ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如:
      mov x0,#10、mov x1,#20
    • 但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能
    • ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的是bl指令

    案例
    现在有两段代码!假设程序先执行A,请写出指令执行顺序。最终寄存器x0的值是多少?

    _A:
    mov x0,#0xa0
    mov x1,#0x00
    add x1, x0, #0x14
    mov x0,x1
    bl _B
    mov x0,#0x0
    ret

    _B:
    add x0, x0, #0x10
    ret

    分析:


    Xcode中创建Empty文件命名为asm.s.s汇编代码会被Xcode自动识别编译)。


    //asm.s
    .text // 告诉是代码
    .global _A, _B //.global 是标号

    _A:
    mov x0,#0xa0 //a0 给 x0 x0 = 0xa0
    mov x1,#0x00 //00 给x1 x1 = 0x00
    add x1, x0, #0x14 //x0 + 0x14 给 x1 x1 = 0xb4
    mov x0,x1 //x1 的值给 x0 x0 = 0xb4
    bl _B //跳转B
    mov x0,#0x0 //0x0 给 x0 x0 = 0x0
    ret //return 上层调用的地方

    _B:
    add x0, x0, #0x10 //x0 + 0x10 给 x0 x0 = 0xc4
    ret //return A

    oc调用汇编:

    //ViewController.m
    int A();

    - (void)viewDidLoad {
    [super viewDidLoad];
    A();
    }
    swift调用汇编:

    //声明方法A。Swift中C和汇编都可以这么暴露。
    @_silgen_name("A")
    func A()

    class ViewController: UIViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    A();
    }

    }
    答案:0x00



    断点验证了x0最终值为0x00。这里有个问题是发生死循环了。(bl跳转指令导致的,lr寄存器在跳转后需要保护现场还原。)


    总结

    • 汇编概述:
      • 使用助记符代替集齐指令的一种编程语言。
      • 汇编和及其指令是一一对应的关系,拿到二进制就可以反汇编。
      • 由于汇编和CPU指令集是对应的,所以汇编不具备移植性。
    • 总线:是一堆导线的集合
      • 地址总线:地址总线的宽度决定了寻址能力
      • 数据总线:数据总线的宽度决定了CPU的吞吐量
    • 进制
      • 任意进制都是由对应个数的符号组成的。符号可以自定义。
      • 2/8/16是相对完美的集智,他们之间的关系
        • 3个2进制使用一个8进制标识
        • 4个2进制使用一个16进制标识
        • 两个16进制位可以标识一个字节
      • 数量单位
        • 1024 = 1K;1024K = 1M;1024M = 1G
      • 容量单位
        • 1024B = 1KB;1024KB = 1MB; 1024MB = 1GB
        • B:byte(字节)1B = 8bit
        • bit(比特):一个二进制位
      • 数据的宽度
        • 计算机中的数据是有宽度的,超过了就会溢出
    • 寄存器:CPU为了性能,在内部开辟了一小块临时存储区域
      • 浮点向量寄存器
      • 异常状态寄存器
      • 通用寄存器:除了存放数据有时候也有特殊的用途
        • ARM64拥有32个64位的通用寄存器X0—X30以及XZR(令寄存器)
        • 为了兼容32位,所以ARM64拥有W0—W28\WZR 30个32位寄存器
        • 32位寄存器并不是独立存在的,比如W0是X0的低32位
      • PC寄存器:指令指针寄存器
        • PC寄存器里面的值保存的就是CPU接下来需要执行的指令地址!
        • 改变PC的值可以改变程序的执行流程!


    作者:HotPotCat
    链接:https://www.jianshu.com/p/e8ea78cb10f0



    收起阅读 »

    iOS越狱

    一、概述越狱(jailBreak),通过iOS系统安全启动链漏洞,从而禁止掉信任链中负责验证的组件。拿到iOS系统最大权限ROOT权限。iOS系统安全启动链当启动一台iOS设备时,系统首先会从只读的ROM中读取初始化指令,也就是系统的引导程序(事实上所有的操作...
    继续阅读 »

    一、概述

    越狱(jailBreak),通过iOS系统安全启动链漏洞,从而禁止掉信任链中负责验证的组件。拿到iOS系统最大权限ROOT权限。

    iOS系统安全启动链
    当启动一台iOS设备时,系统首先会从只读的ROM中读取初始化指令,也就是系统的引导程序(事实上所有的操作系统启动时都要经过这一步,只是过程略有不同)。这个引导ROM包含苹果官方权威认证的公钥,他会验证底层启动加载器(LLB)的签名,一旦通过验证后就启动系统。LLB会做一些基础工作,然后验证第二级引导程序iBootiBoot启动后,设备就可以进入恢复模式或启动内核。在iBoot验证完内核签名的合法性之后,整个启动程序开始步入正轨:加载驱动程序、检测设备、启动系统守护进程。这个信任链会确保所有的系统组件都有苹果官方写入、签名、分发,不能来自第三方机构。


    越狱 的工作原理正是攻击这一信任链。所有的越狱工具的作者都需要找到这一信任链上的漏洞,从而禁止掉信任链中负责验证的组件。拿到iOS系统最大权限ROOT权限。

    熟悉越狱的都听说过 完美越狱 和 非完美越狱

    • 完美越狱:所谓完美越狱就是破解iOS系统漏洞之后,每次系统重启都能自动调用注入的恶意代码,达到破坏安全验证,再次获得ROOT权限。

    • 非完美越狱:所谓非完美越狱是指越狱系统后,并没有完全破解安全链,有部分信息或功能应用不佳;比如关机以后必须去连接越狱软件来引导开机;或者重启会导致越狱的失效;这样的越狱称为 不完美越狱

    目前iOS10以上没有完美越狱工具开放出来,iOS10以下有。目前比较靠谱的两个越狱工具:uncOver 和 Odyssey


    二、unc0ver越狱


    macOSunc0ver有3种越狱方式,这里使用Xcode重签名的方式越狱。其它方式参考官网方式就可以了。

    2.1 环境配置

    • Xcode
    • unc0ver
    • iOS App Signer(️:脚本/Monkey方式不需要这个)

    #1.网站下载
    https://dantheman827.github.io/ios-app-signer/
    #2.命令安装
    sudo gem install sigh
    • 设备 iPhone7 14.0(需要确保设备在自己的账号下)

    2.2 工程配置

    1.安装好Xcode并且新建一个iOS App
    确保自己的设备加入到自己的账号中(我这里使用免费账号)

    2.连接手机build新建的iOS App到设备
    在这个过程中需要手机信任证书(设置->通用->描述文件与设备管理


    2.3 方式一:iOS App Signer 重签名

    1.导出embedded.mobileprovision
    个人开发者账号有效期为7天,由于个人开发者账号苹果官网没有提供导出入口,需要build成功后在products app 中拷贝。如果有付费账号直接官网导出就可以了。



    2.Signer重签名
    Input File为要重签名的ipa包,这里是下载好的unc0ver,证书选择自己的证书(免费开发者账号也可以,有效期7天,前提是自己的设备已经加入免费账号并且导出.mobileprovision)。当然有企业证书是最好的。




    3.Xcode安装重签名后的unc0ver
    Xcode中打开Window → Devices and Simulatorscommand + shift +2),然后在Installed Apps中拖入重签名的unc0ver进行安装。


    4.打开unc0ver进行越狱
    越狱成功后桌面会出现CydiaSubstitute。没有出现的话uncover重新操作一遍。

    2.4 方式二:脚本重签名

    1.项目根目录下创建IPA文件夹并将unc0ver ipa包拷贝放到目录中




    2.根目录下创建appResign.sh重签名脚本
    脚本内容如下:

    # SRCROOT 为工程所在目录,Temp 为创建的临时存放 ipa 解压文件的文件夹。
    TEMP_PATH="${SRCROOT}/Temp"
    # APP 文件夹,存放要重签名的ipa包。
    IPA_PATH="${SRCROOT}/IPA"
    #重签名 ipa 包路径
    TARGRT_IPA_PATH="${IPA_PATH}/*.ipa"

    #清空 Temp 文件夹,重新创建目录
    rm -rf "$TEMP_PATH"
    mkdir -p "$TEMP_PATH"



    #1.解压 ipa 包到 Temp 目录下
    unzip -oqq "$TARGRT_IPA_PATH" -d "$TEMP_PATH"
    #获取解压后临时 App 路径
    TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
    echo "临时App路径:$TEMP_APP_PATH"

    #2.将解压出来的 .app 拷贝到工程目录,
    # BUILT_PRODUCTS_DIR 工程生成的App包路径
    # TARGET_NAME target 名称
    TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
    echo "app路径:$TARGET_APP_PATH"

    #删除工程自己创建的 app
    rm -rf "$TARGET_APP_PATH"
    mkdir -p "$TARGET_APP_PATH"
    #拷贝解压的临时 Temp 文件到工程目录
    cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"

    #3.删除 extension 和 WatchAPP。个人证书无法签名 Extention
    rm -rf "$TARGET_APP_PATH/PlugIns"
    rm -rf "$TARGET_APP_PATH/Watch"


    #4.更新 info.plist 文件 CFBundleIdentifier
    # 设置:"Set :KEY Value" "目标文件路径"
    /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"


    #5. macho 文件加上可执行权限。
    #获取 macho 文件路径
    APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
    #加上可执行权限
    chmod +x "$TARGET_APP_PATH/$APP_BINARY"


    #6.重签名第三方 FrameWorks
    TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
    if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
    then
    for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
    do
    #签名
    /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
    done
    fi

    3.在Xcode工程中配置重签名脚本



    4.重新build工程到手机上。

    5.打开unc0ver进行越狱
    越狱成功后桌面会出现CydiaSubstitute。没有出现的话uncover重新操作一遍。

    2.5 方式三:MonkeyDev


    通过Monkey可以帮助我们自动重签名,只需要准备好要签名的包和配置好证书运行工程就可以了

    Settings -> Restore RootFS 可以恢复到未越狱状态(越狱相关的内容会被删干净)





    越狱前最好在设置中勾选OpenSSH选项,一个连接手机的工具。



    三、Odyssey越狱

    Odysseyunc0ver越狱流程差不多,推荐使用Monkey。区别是Odyssey安装好后的应用商店是Sileounc0verCydia。更推荐使用unc0ver

    ️越狱注意事项:

    • odyssey 越狱中断开网络开始执行越狱,等需要开启网络的时候再联网。
    • 两种越狱方式都在安装好包后断开Xcode连接再进行越狱操作。(Xcode启动应用是附加的状态)
    • 在越狱的过程中遇到任何错误重新恢复手机再尝试。
    • iOS10以下设备直接用爱思助手越狱。
    • 恢复和越狱出错的情况下请删除unc0ver重新安装尝试。

    总结

    越狱:通过破解iOS的安全启启动链的漏洞,拿到iOSRoot权限。

    • 完美越狱:每次系统重新启动都会再次进入越狱状态。
    • 非完美越狱:没有完全破解,一般重启以后会失去越狱环境。

    附系统查询:






















































    作者:HotPotCat
    链接:https://www.jianshu.com/p/2ded2dc425cc










    收起阅读 »

    什么是库(Library)?

    常见库文件格式:.a,.dylib,.framework,.xcframework,.tdb什么是库(Library)?库(Library)本质上就是一段编译好的二进制代码,加上头文件就可以供别人使用。应用场景?某些代码需要给别人使用,但是不希望别人看到源码,...
    继续阅读 »

    常见库文件格式:.a.dylib.framework.xcframework.tdb

    什么是库(Library)?

    库(Library)本质上就是一段编译好的二进制代码,加上头文件就可以供别人使用。

    应用场景?

    1. 某些代码需要给别人使用,但是不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。
    2. 对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。

    什么是链接(Link)?

    库在使用的时候需要链接(Link),链接 的方式有两种:

    1. 静态
    2. 动态

    静态库

    静态库即静态链接库:可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的文件。Windows 下的 .libLinux 和 Mac 下的 .aMac独有的.framework

    缺点: 浪费内存和磁盘空间,模块更新困难。

    静态库链接

    将一份AFNetworking静态库文件(.h头文件和.a组成)和test.m放到统一目录。test.m如下:

    #import <Foundation/Foundation.h>
    #import <AFNetworking.h>

    int main(){
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    NSLog(@"test----%@", manager);
    return 0;
    }


    直接终端查看下.a静态库究竟是什么。

    ➜  AFNetworking file libAFNetworking.a
    libAFNetworking.a: current ar archive

    可以看到.a实际上是一个文档格式。也就是.o文件的合集。可以通过ar命令验证下。

    ar -- create and maintain library archives

    ➜  AFNetworking ar -t libAFNetworking.a
    __.SYMDEF
    AFAutoPurgingImageCache.o
    AFHTTPSessionManager.o
    AFImageDownloader.o
    AFNetworkActivityIndicatorManager.o
    AFNetworking-dummy.o
    AFNetworkReachabilityManager.o
    AFSecurityPolicy.o
    AFURLRequestSerialization.o
    AFURLResponseSerialization.o
    AFURLSessionManager.o
    UIActivityIndicatorView+AFNetworking.o
    UIButton+AFNetworking.o
    UIImageView+AFNetworking.o
    UIProgressView+AFNetworking.o
    UIRefreshControl+AFNetworking.o
    WKWebView+AFNetworking.o
    确认.a确实是.o文件的合集。清楚了.a后将AFNetworking链接到test.m文件。
    1.通过clangtest.m编译成目标文件.o

    clang - the Clang C, C++, and Objective-C compiler
    DESCRIPTION
    clang is a C, C++, and Objective-C compiler which encompasses prepro-
    cessing, parsing, optimization, code generation, assembly, and linking.
    Depending on which high-level mode setting is passed, Clang will stop
    before doing a full link. While Clang is highly integrated, it is
    important to understand the stages of compilation, to understand how to
    invoke it. These stages are:
    Driver The clang executable is actually a small driver which controls
    the overall execution of other tools such as the compiler,
    assembler and linker. Typically you do not need to interact
    with the driver, but you transparently use it to run the other
    tools.
    通过man命令我们看到clangCC++OC编译器,是一个集合包含了预处理解析优化代码生成汇编化链接

    clang -x objective-c \
    -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -I ./AFNetworking \
    -c test.m -o test.o

    回车后就生成了test.o目标文件。

    \为了转译回车,让命令换行更易读。-x制定编译语言,-target指定编译平台,-fobjc-arc编译成ARC-isysroot指定用到的Foundation的路径,-I<directory>在指定目录寻找头文件 header search path

    为什么生成目标文件只需要告诉头文件的路径就可以了?
    因为在生成目标文件的时候,重定位符号表只需要记录哪个地方的符号需要重定位。在连接的时候链接器会自动重定位。(上面的例子中只需要保留AFHTTPSessionManager的符号。)
    2..o生成可执行文件

    clang -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -L./AFNetworking \
    -lAFNetworking \
    test.o -o test

    这个时候test可执行程序就生成了。

    -L要链接的库文件(libAFNetworking.a)目录,-l要链接的库文件(libAFNetworking.a)这里只写AFNetworking是有查找规则的:先找lib+<library_name>的动态库,找不到,再去找lib+<library_name>的静态库,还找不到,就报错。会自动去找libAFNetworking

    经过上面的编译和链接清楚了其它参数都是固定的,那么链接成功一个库文件有3个要素:
    1.  -I<directory> 在指定目录寻找头文件 header search path头文件
    2.  -L<dir> 指定库文件路径(.a\.dylib库文件) library search path库文件路径
    3.  -l<library_name> 指定链接的库文件名称(.a\.dylib库文件)other link flags -lAFNetworking (库文件名称

    生成静态库

    将自己的一个工程编译成.a静态库。工程只有一个文件HPExample``.h和 .m

    #import <Foundation/Foundation.h>

    @interface HPExample : NSObject

    - (void)hp_test:(_Nullable id)e;

    @end

    #import "HPExample.h"

    @implementation HPExample

    - (void)hp_test:(_Nullable id)e {
    NSLog(@"hp_test----");
    }

    @end

    HPExample.m编译成.o文件:

    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I./StaticLibrary \
    -c HPExample.m -o HPExample.o

    这个时候生成了HPExample.o文件,由于工程只有一个.o文件,直接将文件修改为libExample.dylib或者libHPExample.a
    然后创建一个test.m文件调用HPExample:


    #import <Foundation/Foundation.h>
    #import "HPExample.h"

    int main(){
    NSLog(@"testApp----");
    HPExample *manager = [HPExample new];
    [manager hp_test: nil];
    return 0;
    }

    test.m编译成test.o

    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I./StaticLibrary \
    > -c test.m -o test.o

    test.o链接HPExample

    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -L./StaticLibrary \
    -lHPExample \
    test.o -o test
    现在就已经生成了可执行文件test
    终端lldb执行test:

    ➜  staticLibraryCreat lldb
    (lldb) file test
    Current executable set to '/Users/binxiao/projects/library/staticLibraryCreat/test' (x86_64).
    (lldb) r
    Process 2148 launched: '/Users/binxiao/projects/library/staticLibraryCreat/test' (x86_64)
    2021-02-13 13:22:49.150091+0800 test[2148:13026772] testApp----
    2021-02-13 13:22:49.150352+0800 test[2148:13026772] hp_test----
    Process 2148 exited with status = 0 (0x00000000)
    这也从侧面印证了.a就是.o的合集。file test是创建一个targetr是运行的意思。
    接着再看下libHPExample.a文件。

    objdump --macho --private-header libHPExample.a
    Mach header
    magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
    MH_MAGIC_64 X86_64 ALL 0x00 OBJECT 4 1160 SUBSECTIONS_VIA_SYMBOLS
    确认还是一个目标文件。

    静态库的合并

    根据上面的分析,那么静态库的合并也就是将所有的.o放到一个文件中。
    有两个.a库:
    静态库的合并有两种方式:libAFNetworking.a,libSDWebImage.a
    1.ar -rc libAFNetworking.a libSDWebImage.a

    ar -rc libAFNetworking.a  libSDWebImage.a

    就相当于将后面的libSDWebImage.a合并到libAFNetworking.a

    2.libtool -static -o <OUTPUT NAME> <LIBRARY_1> <LIBRARY_2>
    libtool合并静态库。

    libtool -static \
    -o \
    libMerge.a \
    libAFNetworking.a \
    libSDWebImage.a
    //libAFNetworking.a要为目标文件路径,libMerge.a为输出文件
    这样就合并了libAFNetworking.alibSDWebImage.alibMerge.a了。在这个过程中libtool会先解压两个目标文件,然后合并。在合并的过程中有两个问题:
    1.冲突问题。
    2..h文件。

    clang提供了mudule可以预先把头文件(.h)预先编译成二进制缓存到系统目录中, 再去编译.m的时候就不需要再去编译.h了。

    LC_LINKER_OPTION链接器的特性,Auto-Link。启用这个特性后,当我们import <模块>,不需要我们再去往链接器去配置链接参数。比如import <framework>我们在代码里使用这个framework格式的库文件,那么在生成目标文件时,会自动在目标文件的Mach-O中,插入一个 load command格式是LC_LINKER_OPTION,存储这样一个链接器参数-framework <framework>

    动态库

    与静态库相反,动态库在编译时并不会被拷⻉到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。格式有:.framework.dylib.tdb

    缺点:会导致一些性能损失。但是可以优化,比如延迟绑定(Lazy Binding)技术。

    .tdb

    tbd全称是text-based stub libraries本质上就是一个YAML描述的文本文件。他的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息。用于避免在真机开发过程中直接使用传统的dylib。对于真机来说,由于动态库都是在设备上,在Xcode上使用基于tbd格式的伪framework可以大大减少Xcode的大小。

    framework

    Mac OS/iOS 平台还可以使用 FrameworkFramework 实际上是一种打包方式,将库的二进制文件、头文件和有关的资源文件打包到一起方便管理和分发。

    Framework 和系统的 UIKit.Framework 还是有很大区别。系统的 Framework 不需要拷⻉到目标程序中,我们自己做出来的 Framework 哪怕是动态的,最后也还是要拷⻉到 App 中(App 和 Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为 Embedded Framework

    Embedded Framework

    开发中使用的动态库会被放入到ipa下的framework目录下,基于沙盒运行。
    不同的App使用相同的动态库,并不会只在系统中存在一份。而是会在多个App中各自打包、签名、加载一份。


    framework即可以代表动态库也可以代表静态库。

    生成framework




    编译test.m

    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I ./Frameworks/HPExample.framework/Headers \
    -c test.m -o test.o
    链接.o生成test可执行文件
    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -F./Frameworks \
    -framework HPExample \
    test.o -o test
    那么链接一个framework也就需要三个条件:
    1.  -I<directory>:在指定目录寻找头文件 header search path(头文件)
    2. -F<directory>:在指定目录寻找framework framework search path
    3. -framework <framework_name>:指定链接的framework名称 other link flags -framework AFNetworking


    脚本执行命令

    上面都是通过命令行来进行编译连接的,每次输入都很麻烦(即使粘贴复制),我们可以将命令保存在脚本中,通过执行脚本来执行命令。
    还是以HPExample为例,整理后脚本如下(可以加一些日志观察执行问题):

    echo "test.m -> test.o"
    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I ./StaticLibrary \
    -c test.m -o test.o

    echo "pushd -> StaticLibrary"
    #cd可以进入到一个目录不推荐使用,cd会修改目录栈上层,推荐使用 pushd,pushd是往目录栈中push一个目录。
    pushd ./StaticLibrary

    echo "HPExample.m -> HPExample.o"
    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -I./StaticLibrary \
    -c HPExample.m -o HPExample.o

    echo "HPExample.o -> libHPExample.a"
    #打包.o成静态库
    ar -rc libHPExample.a HPExample.o
    echo "popd -> StaticLibrary"
    popd

    echo "test.o -> test"
    #链接
    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk \
    -L./StaticLibrary \
    -lHPExample \
    test.o -o test
    这个时候就已经自动编译链接完成了,其中路径是pushdpopd自动生成的。
    可以简单优化下脚本:

    SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk
    #${SYSROOT}和$SYSROOT都行,如果要匹配比如${SYSROOT}.mm则用{}
    FILE_NAME=test
    HEADER_SEARCH_PATH=./StaticLibrary

    function MToOOrExec {
    if [[ $2 == ".m" ]]; then
    clang -x objective-c \
    -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot ${SYSROOT} \
    -I${HEADER_SEARCH_PATH} \
    -c $1.m -o $1.o
    else
    clang -target x86_64-apple-macos11.0 \
    -fobjc-arc \
    -isysroot ${SYSROOT} \
    -L${HEADER_SEARCH_PATH} \
    -l$1 \
    ${FILE_NAME}.o -o ${FILE_NAME}
    fi
    return 0
    }

    echo "test.m -> test.o"
    MToOOrExec ${FILE_NAME} ".m"
    echo "pushd -> StaticLibrary"
    #cd可以进入到一个目录不推荐使用,cd会修改目录栈上层,推荐使用 pushd,pushd是往目录栈中push一个目录。
    pushd ${HEADER_SEARCH_PATH}
    echo "HPExample.m -> HPExample.o"
    MToOOrExec HPExample ".m"
    echo "HPExample.o -> libHPExample.a"
    #打包.o成静态库
    ar -rc libHPExample.a HPExample.o
    echo "popd -> StaticLibrary"
    popd
    echo "test.o -> test"
    #链接
    MToOOrExec HPExample ".o"

    dead code strip

    对于上面的例子,如果我们在test.m中不使用HPExample只是导入。

    #import <Foundation/Foundation.h>
    #import "HPExample.h"

    int main(){
    NSLog(@"test----");
    // HPExample *manager = [HPExample new];
    // [manager hp_test: nil];
    return 0;
    }
    默认clangdead code strip是生效的。
    在有分类的情况下
    看另外一个例子,我们直接用Xcode创建一个framework,设置为静态库。(Targets -> Build Settings -> Linking -> Macho-type)

    这个库有一个HPTestObject以及HPTestObject+HPAdditions。实现如下:
    HPTestObject

    //.h
    #import <Foundation/Foundation.h>

    @interface HPTestObject : NSObject

    - (void)hp_test;

    @end

    //.m
    #import "HPTestObject.h"
    #import "HPTestObject+HPAdditions.h"

    @implementation HPTestObject

    - (void)hp_test {
    [self hp_test_additions];
    }

    @end

    HPTestObject+HPAdditions

    //.h
    #import "HPTestObject.h"

    @interface HPTestObject (HPAdditions)

    - (void)hp_test_additions;

    @end

    //.m
    #import "HPTestObject+HPAdditions.h"

    @implementation HPTestObject (HPAdditions)

    - (void)hp_test_additions {
    NSLog(@"log: hp_test_additions");
    }

    @end
    HPTestObject设置为public

    我们知道分类是在运行时动态创建的,dead code strip是在链接的过程中生效的。那么应该在链接的时候会strip掉分类。
    我们创建一个workspace验证下

    workspace
    A. 可重用性。多个模块可以在多个项目中使用。节约开发和维护时间。
    B. 节省测试时间。单独模块意味着每个模块中都可以添加测试功能。
    C. 更好的理解模块化思想。

    1.File -> save as workspace




    2.创建一个project(TestApp)。



    3.打开workspace,添加一个project(创建的TestApp)(⚠️需要关闭打开的文件才会出现Add Files to TestDeadCodeStrip):





    4.ViewController.m中使用HPTestObject

    #import <HPStaticFramework/HPTestObject.h>

    - (void)viewDidLoad {
    [super viewDidLoad];
    HPTestObject *hpObject = [HPTestObject new];
    [hpObject hp_test];
    }

    5.运行

    libc++abi.dylib: terminating with uncaught exception of type NSException
    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[HPTestObject hp_test_additions]: unrecognized selector sent to instance 0x600001048020'
    terminating with uncaught exception of type NSException

    和预想的一样直接报错了,原因是dead code strip脱掉了分类。要解决问题还是要告诉编译器不要脱。
    6.配置XCConfig告诉编译器不要脱。

    //-Xlinker 告诉 clang -all_load 参数是传给 ld 的。
    OTHER_LDFLAGS=-Xlinker -all_load

    再次运行App:

     TestApp[8958:13347736] log: hp_test_additions

    ⚠️
    -Xlinker 告诉 clang -all_load 参数是传给ld的。
    -all_load:全部链接

    OTHER_LDFLAGS=-Xlinker -all_load

    -ObjCOC相关的代码不要剥离

    //OTHER_LDFLAGS=-Xlinker -ObjC

    -force_load:指定哪些静态库不要 dead strip

    HPSTATIC_FRAMEWORK_PATH=${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/HPStaticFramework.framework/HPStaticFramework
    OTHER_LDFLAGS=-Xlinker -force_load $HPSTATIC_FRAMEWORK_PATH

    -noall_load: 默认,没有使用静态库代码则不往可执行文件添加。This is the default. This option is obsolete.

    以上4种参数仅针对静态库。dead code strip是在链接过程中连接器提供的优化方式。

    -dead_strip
    Remove functions and data that are unreachable by the entry point or exported symbols.
    移除没有被入口点(也就是main)和导出符号用到的代码。

    接着libraryDeadCodeStrip工程验证下:
    修改test.m如下:


    #import <Foundation/Foundation.h>
    //#import "HPExample.h"

    //全局函数
    void global_function() {

    }

    //entry point
    int main(){
    // global_function();
    NSLog(@"test----");
    // HPExample *manager = [HPExample new];
    // [manager hp_test: nil];
    return 0;
    }

    //本地
    static void static_function(){

    }

    运行build.sh
    可以看到没有静态库libHPExample.a相关的代码,加上all_load再查看下:
    build.sh修改增加

    -Xlinker -all_load \
    hp_test方法已经有了。
    修改-Xlinker -all_load-Xlinker -dead_strip 再查看下:

    global_functionhp_test都没有了。
    打开mainglobal_function()的注释再看下:

    所以dead code strip-all_load-ObjC-force_load-noall_load不是一个东西,他有一定规则:

    1. 入口点没有使用->干掉
    2. 没有被导出符号使用->干掉

    接着-Xlinker -dead_strip-Xlinker -all_load一起添加:


    链接器有一个参数-why_live可以查看某一个符号为什么没有被干掉,比如我们要知道global_function为什么没有被干掉:
        -Xlinker -why_live -Xlinker _global_function

    .o -> .o.o -> .a

    .o -> .o是合并成一个大的.o再去链接生成可执行文件。先组合再链接。所以这里dead code strip干不掉,可以通过LTO(Link-Time Optimization)去优化。
    .o链接静态库是.o是去使用静态库。先dead code strip再使用。





  • Do Not Embed
    用于静态库
  • Embed & Sign
    嵌入,用于动态库,动态库在运行时链接,所以它们编译的时候需要被打进bundle里面。静态库链接的时候代码就已经在一起了,所以不需要拷贝,直接Do Not Embed就可以了。可以通过file命令验证:

  • file HPStaticFramework.framework/HPStaticFramework
    HPStaticFramework.framework/HPStaticFramework: current ar archive random library

    current ar archive:说明是静态库,选择Do not embed
    Mach-0 dynamically:说明是动态库,选择Embed

    1. Embed Without Signing
      Signing:只用于动态库,如果已经有签名了就不需要再签名。终端执行codesign -dv判断:

    codesign -dv HPStaticFramework.framework
    Executable=/Users/***/Library/Developer/Xcode/DerivedData/TestDeadCodeStrip-fhbiunbplvqefkftfystdixdxmkq/Build/Products/Debug-iphonesimulator/HPStaticFramework.framework/HPStaticFramework
    Identifier=HotpotCat.HPStaticFramework
    Format=bundle with generic
    CodeDirectory v=20100 size=204 flags=0x2(adhoc) hashes=1+3 location=embedded
    Signature=adhoc
    Info.plist entries=20
    TeamIdentifier=not set
    Sealed Resources version=2 rules=10 files=2
    Internal requirements count=0 size=12

    命令总结

    clang命令参数

    -x: 指定编译文件语言类型
    -g: 生成调试信息
    -c: 生成目标文件,只运行preprocesscompileassemble不链接
    -o: 输出文件
    -isysroot: 使用的SDK路径
    -I<directory>: 在指定目录寻找头文件 header search path
    -L<directory> :指定库文件路径(.a.dylib库文件)library search path
    -l<library_name>: 指定链接的库文件名称(.a.dylib库文件)other link flags -lAFNetworking。链接的名称为libAFNetworking/AFNetworking的动态库或者静态库,查找规则:先找lib+<library_name>的动态库,找不到,再去找lib+<library_name>的静态库,还找不到,就报错。
    -F<directory>: 在指定目录寻找framework,framework search path
    -framework <framework_name>: 指定链接的framework名称,other link flags -framework AFNetworking

    test.m编译成test.o过程

    1. 使用OC
    2. 生成指定架构的代码,Big Sur是:x86_64-apple-macos11.1,之前是:x86_64-apple-macos10.15。iOS模拟器是:x86_64-apple-ios14-simulator。更多内容可以参考target部分。
    3. 使用ARC
    4. 使用的SDK的路径在:
      Big Sur是:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk
      之前是:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk
      模拟器是:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
      更多内容可以参考sdk部分。
    5. 用到的其他库的头文件地址在./Frameworks
      命令示例:
    clang -x objective-c \
    -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -I ./AFNetworking \
    -c test.m -o test.o

    test.o链接生成test可执行文件

    clang链接.a静态库

    顺序和生成.o差不多,不需要指定语言。
    命令示例:

    clang -target x86_64-apple-ios14-simulator \
    -fobjc-arc \
    -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk \
    -L./AFNetworking \
    -lAFNetworking \
    test.o -o test

    ld链接.framework静态库

    ld -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
    -lsystem -framework Foundation \
    -lAFNetworking \
    -L.AFNetworking \
    test.o -o test





    作者:HotPotCat
    链接:https://www.jianshu.com/p/298efeb8732c










    收起阅读 »

    Mac终端快捷键

    编辑命令行

    快捷键说明
    control + k删除从光标到行尾
    control + u删除从光标到行首
    control + w从光标向前一个单词剪切到剪贴板
    option + d从光标向后删除一个单词。⚠️option键需要自己配置。详见后面[终端option键配置]
    control + d删除光标下一个字母
    control + h删除光标前一个字母
    option + tswap(当前单词,上一个单词),尾部会交换前两个单词
    control + tswap(当前字母,上一个字母)
    control + y粘贴上一次删除的文本
    option + c大写当前字母,并移动光标到单词尾
    option + u大写从当前光标到单词尾
    option + c小写从当前光标到单词尾,光标后的第一个字母会大写
    control + r向后搜索历史命令,control + r后输入关键字比如i然后再control + r一直往上查找,当然也可以通过control + pcontrol + n配合查找
    control + g退出搜索
    control + p历史中上一个命令
    control + n历史中下一个命令
    option + .上一个命令的最后一个单词
    control + l / command + k清屏,当前命令前面的所有内容
    control + s停止输出(zsh中为向前搜索历史命令)
    control + q继续输出
    control + c终止当前命令
    control + z挂起当前命令
    control + d结束输出(产生一个EOF
    control + a移动光标到行首
    control + e移动光标到行尾
    option + b移动光标后退一个单词(词首)
    option + f移动光标前进一个单词(词首)
    control + b光标前进一个字母(这两个没什么实际意义,通过左右箭头就可以操作了-><-
    control + f光标后退一个字母
    control + xx当前位置与行首之间选中
    control + -撤销,类似macOS系统的control +z
    option + r取消更改,并恢复历史记录中的行(还原)
    esc + t1.光标在行尾交换光标前的最后两个单词。
    2.在中间交换光标前后单词。
    3.在行首无效。
    !!重复上一条命令,类似上箭头
    !n交换光标前的最后两个单词
    !:n-m重复最后一条命令取参数n-m,比如:!:3-4
    !:n-$重复最后一条命令取参数n-最后,比如:!:3-$
    !:q引用最后一条命令,相当于分割单词
    !:q命令
    !$上一条命令的最后一个参数
    !*上一条命令的所有参数
    !*命令
    option + 方向键光标以单词为单位移动(仅在Terminal有效,iTerm无效
    command + fn + 左/右箭头滚动到顶部/底部
    command + fn + 上/下箭头上/下一页
    optional + command + fn + 上/下箭头上/下一行
    delete/fn + delete向前/后删除一个字符

    分屏

    快捷键说明
    command + d分屏
    1.在mac默认终端Terminal下是上下分屏,显示内容一致。
    2.在iTerm下是横向分屏相当于多个终端
    command + shift + d
    1.在mac默认终端Terminal下是取消分屏。
    2.在iTerm下是纵向分屏

    标签&窗口

    快捷键说明
    command + t新建标签
    command + w关闭标签
    command + shift + 左右箭头/control + tab/control + shift + tab选择标签
    command + shift + |mac默认终端Terminal下有效。相当于Mac触摸板的四指上滑 (调度中心) 
    image.png
    command + n新建窗口
    shift + command + t显示或隐藏标签页栏
    隐藏
    显示
    shift + command + n新建命令(Terminal下有效)
    shift + command + k新建远程连接(Terminal下有效)
    command + i显示或隐藏检查器(Terminal下有效) 
    image.png
    command + +/-放大/缩小字体
    command + 重音符/command + shift + 重音符下/上一个窗口,重音符(`)


    1.使用“终端”窗口和标签页

    操作
    快捷键
    新建窗口
    Command-N
    使用相同命令新建窗口
    Control-Command-N
    新建标签页
    Command-T
    使用相同命令新建标签页
    Control-Command-T
    显示或隐藏标签页栏
    Shift-Command-T
    显示所有标签页或退出标签页概览
    Shift-Command-反斜杠 (\)
    新建命令
    Shift-Command-N
    新建远程连接
    Shift-Command-K
    显示或隐藏检查器
    Command-I
    编辑标题
    Shift-Command-I
    编辑背景颜色
    Option-Command-I
    放大字体
    Command-加号键 (+)
    缩小字体
    Command-减号键 (–)
    下一个窗口
    Command-重音符键 (`)
    上一个窗口
    Command-Shift-波浪符号 (~)
    下一个标签页
    Control-Tab
    上一个标签页
    Control-Shift-Tab
    将窗口拆分为两个面板
    Command-D
    关闭拆分面板
    Shift-Command-D
    关闭标签页
    Command-W
    关闭窗口
    Shift-Command-W
    关闭其他标签页
    Option-Command-W
    全部关闭
    Option-Shift-Command-W
    滚动到顶部
    Command-Home
    滚动到底部
    Command-End
    上一页
    Command-Page Up
    下一页
    Command-Page Down
    上一行
    Option-Command-Page Up
    下一行
    Option-Command-Page Down


    2.编辑命令行

    操作
    快捷键
    重新定位插入点
    在按住 Option 键的同时将指针移到新的插入点。
    将插入点移到行的开头
    Control-A
    将插入点移到行的结尾
    Control-E
    将插入点前移一个字符
    右箭头键
    将插入点后移一个字符
    左箭头键
    将插入点前移一个字词
    Option-右箭头键
    将插入点后移一个字词
    Option-左箭头键
    删除到行的开头
    Control-U
    删除到行的结尾
    Control-K
    向前删除到字词的结尾
    Option-D(选中将 Option 键用作 Meta 键后可用)
    向后删除到字词的开头
    Control-W
    删除一个字符
    Delete
    向前删除一个字符
    向前删除(或使用 Fn-Delete)
    转置两个字符
    Control-T


    3.在“终端”窗口中选择和查找文本

    操作
    快捷键
    选择完整文件路径
    按住 Shift-Command 键并连按路径
    选择整行文本
    点按该行三下
    选择一个词
    连按该词
    选择 URL
    按住 Shift-Command 键并连按 URL
    选择矩形块
    按住 Option 键并拖移来选择文本
    剪切
    Command-X
    拷贝
    Command-C
    不带背景颜色拷贝
    Control-Shift-Command-C
    拷贝纯文本
    Option-Shift-Command-C
    粘贴
    Command-V
    粘贴所选内容
    Shift-Command-V
    粘贴转义文本
    Control-Command-V
    粘贴转义的所选内容
    Control-Shift-Command-V
    查找
    Command-F
    查找下一个
    Command-G
    查找上一个
    Command-Shift-G
    使用选定的文本查找
    Command-E
    跳到选定的文本
    Command-J
    全选
    Command-A
    打开字符检视器
    Control-Command-Space


    4.使用标记和书签

    操作
    快捷键
    标记
    Command-U
    标记为书签
    Option-Command-U
    取消标记
    Shift-Command-U
    标记命令行并发送返回结果
    Command-Return
    发送返回结果但不标记
    Shift-Command-Return
    插入书签
    Shift-Command-M
    插入包含名称的书签
    Option-Shift-Command-M
    跳到上一个标记
    Command-上箭头键
    跳到下一个标记
    Command-下箭头键
    跳到上一个书签
    Option-Command-上箭头键
    跳到下一个书签
    Option-Command-下箭头键
    清除到上一个标记
    Command-L
    清除到上一个书签
    Option-Command-L
    清除到开头
    Command-K
    在标记之间选择
    Shift-Command-A


    5.其他快捷键

    操作
    快捷键
    进入或退出全屏幕
    Control-Command-F
    显示或隐藏颜色
    Shift-Command-C
    打开“终端”偏好设置
    Command-逗号键 (,)
    中断
    键入 Command-句点键 (.) 等于在命令行上输入 Control-C
    打印
    Command-P
    软重置终端仿真器状态
    Option-Command-R
    硬重置终端仿真器状态
    Control-Option-Command-R
    打开 URL
    按住 Command 键并连按 URL
    添加至文件的完整路径
    从“访达”将文件拖移到“终端”窗口中
    将文本导出为
    Command-S
    将选定的文本导出为
    Shift-Command-S
    反向搜索命令历史
    Control-R
    开关“允许鼠标报告”选项
    Command-R
    开关“将 Option 键用作 Meta 键”选项
    Command-Option-O
    显示备用屏幕
    Option-Command-Page Down
    隐藏备用屏幕
    Option-Command-Page Up
    打开所选内容的 man 页面
    Control-Shift-Command-问号键 (?)
    搜索所选内容的 man 页面索引
    Control-Option-Command-斜杠 (/)
    完整的目录或文件名称
    在命令行上,键入一个或多个字符,然后按下 Tab 键
    显示可能的目录或文件名称补全列表
    在命令行上,键入一个或多个字符,然后按下 Tab 键两次


    MacHomeEndPageUPPageDOWN

    • Home = Fn + 左方向
    • End = Fn + 右方向、
    • PageUP = Fn + 上方向
    • PageDOWN = Fn + 下方向
    • 向前Delete = Fn + delete

    终端option键配置

    将 Option 键用作 Meta 键

    Terminal配置

    Preferences -> Profiles -> 将optional键用作Meta键

    Terminal配置>

    iTerm配置

    iTerm需要在Preferences -> Profiles -> "your Profile" -> Keys -> left/right option key ->Esc+配置。
    ⚠️这里是配置成Esc+不是Meta




     

    作者:HotPotCat
    开码牛

    链接:https://www.jianshu.com/p/524d02ee49cf

    https://blog.csdn.net/helunqu2017/article/details/113749611

     

    Xcode多环境配置

    Xcode多环境配置一共有3种形式:TargetSchemexcconfigProject:包含了项目所有的代码、资源文件、所有信息。(一个项目是多个project的集合)Target:对指定代码和资源文件的具体构建方式。(指定某些代码如何生成ipa包,类似打...
    继续阅读 »

    Xcode多环境配置一共有3种形式:

    • Target
    • Scheme
    • xcconfig

    Project:包含了项目所有的代码、资源文件、所有信息。(一个项目是多个project的集合)
    Target:对指定代码和资源文件的具体构建方式。(指定某些代码如何生成ipa包,类似打工人的角色)
    Scheme:对指定Target的环境配置。(配置编译环境变量)
    这也就是我们修改一些配置的时候需要选中Target再去修改的原因。

    多Target配置

    在项目中选中Target复制就生成新的Target了。




    相当于可以直接分别配置Info.plist文件,在Target中修改bundleId后就相当于两个Target是两个App了。
    同时可以在Preprocessor Macros中配置一些宏定义用于代码中区分Target



    #if DEV
    #import "TestMutableConfig_dev-Swift.h"
    #else
    #import "TestMutableConfig-Swift.h"
    #endif


    ~  swiftc --help | grep -- '-D'
    -D <value> Marks a conditional compilation flag as true


    Target方式配置多环境
    1.会生成多个Info.plist文件;
    2.配置比较繁琐,需要同步配置容易混乱
    那么对于多Target的场景是可以在Build Phases中控制要编译的文件和资源。



    多scheme配置

    scheme默认有DebugReleaseconfig我们可以按需添加。在Target中添加变量的时候已经用到过了。
    配置在Project -> Info -> Configurations




    运行/打包的时候选择对应的Scheme就可以了。



    这个时候只需要切换Scheme运行就可以了。

    比如我们上传打包ipa的时候,有时候会错将debug模式下的包上传上去,尤其是在发灰度包的时候。这里有两个方案:
    1.通过config配置。
    2.打包的时候通过脚本修改info.plist文件增加一个变量。

    release包赋值为0debug包赋值为1。这样在上传ipa包的时候后端读取Info.plist做判断,debug包直接报错不让传。
    这里实现以下方式1:
    Targets -> Build Settings -> + -> Add User-Defined Setting





    到这里就完成了

    • User-Defined添加配置;
    • Info.plist暴露配置的目的。

    在代码中测试下:

        NSString *infoPath = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
    NSDictionary *infoDic = [[NSDictionary alloc] initWithContentsOfFile:infoPath];
    NSLog(@"IPAFLAG = %@",infoDic[@"IPAFLAG"]);
    Debug下:

     IPAFLAG = 1
    当然也可以配置app图标:
    Assets.xcassets中添加不同的资源文件


    Scheme情况只需要在一个build setting中就能完成配置了,比多Target方便好维护。
    缺点是还需要在build setting中设置。

    xcconfig


    就是使用xcconfig配置的:

    FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking"
    GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
    HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking/AFNetworking.framework/Headers"
    LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
    OTHER_LDFLAGS = $(inherited) -framework "AFNetworking"
    PODS_BUILD_DIR = ${BUILD_DIR}
    PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
    PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
    PODS_ROOT = ${SRCROOT}/Pods
    USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES

    xcconfigkey-value的形式配置的。那么怎么对应到xcconfig文件的呢?


    Configurations中对应配置的。

    配置自己的xcconfig文件

    新建 -> Configuration Settings File





    • 1中设置是对整个Project生效。
    • 2中设置是对Target生效。

    还是以IPAFLAG为例,以xcconfig的方式配置。
    plist中的配置不变,User-Defined配置删除

        <key>IPAFLAG</key>
    <string>${IPAFLAG}</string>
    Config-TestMutableConfig.Debug.xcconfig

    IPAFLAG = 1

    Config-TestMutableConfig.Release.xcconfig

    IPAFLAG = 0
    代码中读取下:

        NSString *infoPath = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
    NSDictionary *infoDic = [[NSDictionary alloc] initWithContentsOfFile:infoPath];
    NSLog(@"IPAFLAG = %@",infoDic[@"IPAFLAG"]);
    IPAFLAG = 1
    这样配置更清晰,便于管理。


    xcconfig配置总结

    key-value 组成

    配置文件由一系列键值分配组成:

    BUILD_SETTING_NAME = value

    注释

    xcconfig文件只有一种注释方式//

    //

    那么这里就有一个问题了,如果我们要配置一个域名该怎么办呢?比如:

    HOST_URL = https://127.0.0.1

    可以通过中间值解决:

    TEMP=/
    HOST_URL = https:${TEMP}/127.0.0.1

    include导入其他设置

    可以通过include关键字导入其他的xcconfig内的配置。通过include关键字后接上双引号:

    #include "Other.xcconfig"

    在引入的文件时,如果是以/开头,代表绝对路径:

    #include "/Users/zaizai/Desktop/TestMutableConfig/Pods/Target Support Files/Pods-TestMutableConfig/Pods-TestMutableConfig.debug.xcconfig"

    相对路径,以${SRCROOT}路径为开始:

    #include "Pods/Target Support Files/Pods-TestMutableConfig/Pods-TestMutableConfig.debug.xcconfig"

    变量

    变量定义,按照OC命名规则,仅由大写字母,数字和下划线_组成,原则上大写,也可以不。字符串可以是"也可以是'号。

    1. xcconfig中定义的变量与Build Settings的一致,会发生覆盖。可以通过$(inherited)让当前变量继承变量原有值。(当然对于系统的key最好都加上$(inherited)`)
    //A config
    OTHER_LDFLAGS = -framework SDWebImage
    //B config
    OTHER_LDFLAGS = $(inherited) -framework AFNetworking
    //build setting中
    // OTHER_LDFLAGS = -framework SDWebImage -framework AFNetworking

    ⚠️:有部分变量不能通过xcconfig配置到Build Settings中。如:配置PRODUCT_BUNDLE_IDENTIFIER不起作用。

    1. 引用变量,$()${}两种写法都可以
    VALUE=HotpotCat

    KEY1=$(VALUE)
    KEY2=${VALUE}
    1. 条件变量,根据SDKArchConfigration对设置进行条件化:
    // 指定`Configration`是`Debug`
    // 指定`SDK`是模拟器,还有iphoneos*、macosx*等
    // 指定生效架构为`x86_64`
    OTHER_LDFLAGS[config=Debug][sdk=iphonesimulator*][arch=x86_64]= $(inherited) -framework "HotpotCat"

    ⚠️:在Xcode 11.4及以后版本,可以使用default来指定变量为空时的默认值。

    $(BUILD_SETTING_NAME:default=value)

    优先级(高->低)

    • 手动配置Target Build Settings;
    • Target中配置的xcconfig文件;
    • 手动配置Project Build Settings;
    • Project中配置的xcconfig文件。



    作者:HotPotCat
    链接:https://www.jianshu.com/p/ca0ac4ff4fc1
    收起阅读 »

    llvm优化alloc

    为什么调用alloc最终调用了objc_alloc?objc源码中探索分析在源码中我们点击alloc会进入到+ (id)alloc方法,但是在实际调试中却是先调用的objc_alloc,系统是怎么做到的呢?可以看到在这个方法中进行了imp的重新绑定将alloc...
    继续阅读 »

    为什么调用alloc最终调用了objc_alloc


    objc源码中探索分析

    在源码中我们点击alloc会进入到+ (id)alloc方法,但是在实际调试中却是先调用的objc_alloc,系统是怎么做到的呢?




    可以看到在这个方法中进行了imp的重新绑定将alloc绑定到了objc_alloc上面。当然retainrelease等都进行了同样的操作。
    既然在_read_images中出现问题的时候尝试进行fixup,那么意味着正常情况下在_read_images之前llvm的编译阶段就完成了绑定。

    llvm源码探索分析

    那么直接在llvm中搜索objc_alloc,在ObjCRuntime.h中发现了如下注释:

      /// When this method returns true, Clang will turn non-super message sends of
    /// certain selectors into calls to the corresponding entrypoint:
    /// alloc => objc_alloc
    /// allocWithZone:nil => objc_allocWithZone

    这说明方向没有错,最中在CGObjC.cpp中找到了如下代码:


      case OMF_alloc:
    if (isClassMessage &&
    Runtime.shouldUseRuntimeFunctionsForAlloc() &&
    ResultType->isObjCObjectPointerType()) {
    // [Foo alloc] -> objc_alloc(Foo) or
    // [self alloc] -> objc_alloc(self)
    if (Sel.isUnarySelector() && Sel.getNameForSlot(0) == "alloc")
    return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType));
    // [Foo allocWithZone:nil] -> objc_allocWithZone(Foo) or
    // [self allocWithZone:nil] -> objc_allocWithZone(self)
    if (Sel.isKeywordSelector() && Sel.getNumArgs() == 1 &&
    Args.size() == 1 && Args.front().getType()->isPointerType() &&
    Sel.getNameForSlot(0) == "allocWithZone") {
    const llvm::Value* arg = Args.front().getKnownRValue().getScalarVal();
    if (isa<llvm::ConstantPointerNull>(arg))
    return CGF.EmitObjCAllocWithZone(Receiver,
    CGF.ConvertType(ResultType));
    return None;
    }
    }
    break;

    可以看出来alloc最后执行到了objc_alloc。那么具体的实现就要看CGF.EmitObjCAlloc方法:

    llvm::Value *CodeGenFunction::EmitObjCAlloc(llvm::Value *value,
    llvm::Type *resultType) {
    return emitObjCValueOperation(*this, value, resultType,
    CGM.getObjCEntrypoints().objc_alloc,
    "objc_alloc");
    }

    llvm::Value *CodeGenFunction::EmitObjCAllocWithZone(llvm::Value *value,
    llvm::Type *resultType) {
    return emitObjCValueOperation(*this, value, resultType,
    CGM.getObjCEntrypoints().objc_allocWithZone,
    "objc_allocWithZone");
    }

    llvm::Value *CodeGenFunction::EmitObjCAllocInit(llvm::Value *value,
    llvm::Type *resultType) {
    return emitObjCValueOperation(*this, value, resultType,
    CGM.getObjCEntrypoints().objc_alloc_init,
    "objc_alloc_init");
    }

    这里可以看到alloc以及objc_alloc_init相关的逻辑。这样就实现了绑定。那么系统是怎么走到OMF_alloc的逻辑的呢?
    通过发送消息走到这块流程:

    CodeGen::RValue CGObjCRuntime::GeneratePossiblySpecializedMessageSend(
    CodeGenFunction &CGF, ReturnValueSlot Return, QualType ResultType,
    Selector Sel, llvm::Value *Receiver, const CallArgList &Args,
    const ObjCInterfaceDecl *OID, const ObjCMethodDecl *Method,
    bool isClassMessage) {
    //尝试发送消息
    if (Optional<llvm::Value *> SpecializedResult =
    tryGenerateSpecializedMessageSend(CGF, ResultType, Receiver, Args,
    Sel, Method, isClassMessage)) {
    return RValue::get(SpecializedResult.getValue());
    }
    return GenerateMessageSend(CGF, Return, ResultType, Sel, Receiver, Args, OID,
    Method);
    }
  • 苹果对alloc等特殊函数做了hook,会先走底层的标记emitObjCValueOperation。最终再走到alloc等函数。
  • 第一次会走tryGenerateSpecializedMessageSend分支,第二次就走GenerateMessageSend分支了。
    • 也就是第一次alloc调用了objc_alloc,第二次alloc后就没有调用objc_alloc走了正常的objc_msgSendalloc-> objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc。这也就是callAlloc走两次的原因。
    • 再创建个对象调用流程就变成了:alloc -> objc_alloc -> callAlloc


  • 内存分配优化

    HPObject *hpObject = [HPObject alloc];
    NSLog(@"%@:",hpObject);

    对于hpObject我们查看它的内存数据如下:

    (lldb) x hpObject
    0x6000030cc2e0: c8 74 e6 0e 01 00 00 00 00 00 00 00 00 00 00 00 .t..............
    0x6000030cc2f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
    (lldb) p 0x000000010ee674c8
    (long) $4 = 4544951496

    可以打印的isa4544951496并不是HPObject。因为这里要&mask,在源码中有一个&mask结构。arm64`定义如下:

    #   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
    # define ISA_MASK 0x007ffffffffffff8ULL
    # else
    # define ISA_MASK 0x0000000ffffffff8ULL

    这样计算后就得到isa了:

    (lldb) po 0x000000010ee674c8 & 0x007ffffffffffff8
    HPObject

    HPObjetc添加属性并赋值,修改逻辑如下:

    @interface HPObject : NSObject

    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) double height;
    @property (nonatomic, assign) BOOL marry;

    @end
    调用:
        HPObject *hpObject = [HPObject alloc];
    hpObject.name = @"HotpotCat";
    hpObject.age = 18;
    hpObject.height = 180.0;
    hpObject.marry = YES;
    这个时候发现agemarry存在了isa后面存在了一起。
    那么多增加几个BOOL属性呢?

    @interface HPObject : NSObject

    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) int age;
    @property (nonatomic, assign) double height;
    @property (nonatomic, assign) BOOL marry;
    @property (nonatomic, assign) BOOL flag1;
    @property (nonatomic, assign) BOOL flag2;
    @property (nonatomic, assign) BOOL flag3;

    @end

    int类型的age单独存放了,5bool值放在了一起。这也就是内存分配做的优化。

    init源码探索

    既然alloc已经完成了内存分配和isa与类的关联那么init中做了什么呢?

    init
    init源码定义如下:

    - (id)init {
    return _objc_rootInit(self);
    }

    _objc_rootInit

    id
    _objc_rootInit(id obj)
    {
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
    }




    可以看到init中调用了_objc_rootInit,而_objc_rootInit直接返回obj没有做任何事情。就是给子类用来重写的,提供接口便于扩展。所以如果没有重写init方法,那么在创建对象的时候可以不调用init方法。

    有了alloc底层骚操作的经验后,打个断点调试下:

    NSObject *obj = [NSObject alloc];
    [obj init];

    这里allocinit分开写是为了避免被优化。这时候调用流程和源码看到的相同。

    那么修改下调用逻辑:

    NSObject *obj = [[NSObject alloc] init];

    alloc init一起调用后会先进入objc_alloc_init方法。

    objc_alloc_init

    id
    objc_alloc_init(Class cls)
    {
    return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
    }

    objc_alloc_init调用了callAllocinit

    new源码探索

    既然alloc init 和new都能创建对象,那么它们之间有什么区别呢?
    new

    + (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
    }
    alloc init一起调用的不同点是checkNil传递的是fasle
    源码调试发现new调用的是objc_opt_new

    // Calls [cls new]
    id
    objc_opt_new(Class cls)
    {
    #if __OBJC2__
    if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
    return [callAlloc(cls, false/*checkNil*/) init];
    }
    #endif
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
    }

    objc2下也是callAllocinit

    • init方法内部默认没有进行任何操作,只是返回了对象本身。
    • allot initnew底层实现一致,都是调用callAllocinit。所以如果自定义了init方法调用两者效果相同。

    objc_alloc_initobjc_opt_new的绑定与objc_alloc的实现相同。同样的实现绑定的还有:

    const char *AppleObjCTrampolineHandler::g_opt_dispatch_names[] = {
    "objc_alloc",//alloc
    "objc_autorelease",//autorelease
    "objc_release",//release
    "objc_retain",//retain
    "objc_alloc_init",// alloc init
    "objc_allocWithZone",//allocWithZone
    "objc_opt_class",//class
    "objc_opt_isKindOfClass",//isKindOfClass
    "objc_opt_new",//new
    "objc_opt_respondsToSelector",//respondsToSelector
    "objc_opt_self",//self
    };

    总结

    alloc调用过程:

    • objc_alloc
      • alloc底层首先调用的是objc_alloc
      • objc_allocalloc是在llvm编译阶段进行关联的。苹果会对系统特殊函数做hook进行标记。
    • callAlloc判断应该初始化的分支。
    • _class_createInstanceFromZone进行真正的开辟和关联操作:
      • instacneSize计算应该开辟的内存空间。
        • alignedInstanceSize内部进行字节对齐。
        • fastInstanceSize内部会进行内存对齐。
      • calloc开辟内存空间。
      • initInstanceIsa关联isa与创建的对象。
    • init & new
      • init方法内部默认没有进行任何操作,只是为了方便扩展。
      • allot initnew底层实现一致,都是调用callAllocinit



    作者:HotPotCat
    链接:https://www.jianshu.com/p/884275c811d5


    收起阅读 »

    OC alloc 底层探索

    一、alloc对象的指针地址和内存有如下代码://alloc后分配了内存,有了指针。 //init所指内存地址一样,init没有对指针进行操作。 HPObject *hp1 = [HPObject alloc]; HPObject *hp2 = [hp1 in...
    继续阅读 »

    一、alloc对象的指针地址和内存

    有如下代码:

    //alloc后分配了内存,有了指针。
    //init所指内存地址一样,init没有对指针进行操作。
    HPObject *hp1 = [HPObject alloc];
    HPObject *hp2 = [hp1 init];
    HPObject *hp3 = [hp1 init];
    NSLog(@"%@-%p",hp1,hp1);
    NSLog(@"%@-%p",hp2,hp2);
    NSLog(@"%@-%p",hp3,hp3);

    输出:

    <HPObject: 0x600000f84330>-0x600000f84330
    <HPObject: 0x600000f84330>-0x600000f84330
    <HPObject: 0x600000f84330>-0x600000f84330


    说明alloc后进行了内存分配有了指针,而init后所指内存地址一致,所以init没有对指针进行操作。
    修改NSLog内容如下:

    NSLog(@"%@-%p &p:%p",hp1,hp1,&hp1);
    NSLog(@"%@-%p &p:%p",hp2,hp2,&hp2);
    NSLog(@"%@-%p &p:%p",hp3,hp3,&hp3);

    输出:

    <HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d8
    <HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40d0
    <HPObject: 0x600000e7c2c0>-0x600000e7c2c0 &p:0x7ffeefbf40c8


    这就说明hp1hp2hp3都指向堆空间的一块区域。而3个指针本身是在栈中连续开辟的空间,从高地址->低地址。
    那么alloc是怎么开辟的内存空间呢?


    二、底层探索思路


    1. 断点结合Step into instruction进入调用堆栈找到关键函数:


    找到了最中调用的是libobjc.A.dylibobjc_alloc:`。

    下断点后通过汇编查看调用流程Debug->Debug workflow->Always Show Disassembly通过已知符号断点确定未知符号。

    直接alloc下符号断点跟踪:


    三、alloc源码分析

    通过上面的分析已经能确定allocobjc框架中,正好苹果开源了这块代码,源码:objc源码地址:Source Browser
    最好是自己能编译一份能跑通的源码(也可以直接github上找别人编译好的)。当然也可以根据源码下符号断点跟踪调试。由于objc4-824目前下载不了,这里以objc4-824.2为例进行调试。

    HPObject定义如下:


    @interface HPObject : NSObject

    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) int age;

    @end

    3.1 alloc

    直接搜索alloc函数的定义发现在NSObject.mm 2543,通过断点调试类。
    调用alloc会首先调用objc_alloc:

    id
    objc_alloc(Class cls)
    {
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
    }


    callAlloc会走到调用alloc分支。

    + (id)alloc {
    return _objc_rootAlloc(self);
    }

    alloc直接调用了_objc_rootAlloc

    id
    _objc_rootAlloc(Class cls)
    {
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
    }

    • _objc_rootAlloc传递参数checkNilfalseallocWithZonetrue直接调用了callAlloc
    • 在调用objc_alloc的时候传递的checkNiltrueallocWithZonefalse

    这里没什么好说的只是方法的一些封装,具体实现要看callAlloc



    3.2 callAlloc

    static ALWAYS_INLINE id
    callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
    {
    #if __OBJC2__
    //表示值为假的可能性更大。即执行else里面语句的机会更大
    if (slowpath(checkNil && !cls)) return nil;
    //hasCustomAWZ方法判断是否实现自定义的allocWithZone方法,如果没有实现就调用系统默认的allocWithZone方法。
    //表示值为真的可能性更大;即执行if里面语句的机会更大
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
    return _objc_rootAllocWithZone(cls, nil);
    }
    #endif

    // No shortcuts available.
    if (allocWithZone) {
    return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
    }

    slowpath:表示值为假的可能性更大。即执行else里面语句的机会更大。
    fastpath:表示值为真的可能性更大;即执行if里面语句的机会更大。
    OBJC2:是因为有两个版本。Legacy版本(早期版本,对应Objective-C 1.0) 和 Modern版本(现行版本Objective-C 2.0)。

    • 在首次调用的时候会走alloc分支进入到alloc逻辑。
    • hasCustomAWZ意思是hasCustomAllocWithZone有没有自定义实现AllocWithZone。没有实现就走(这里进行了取反)_objc_rootAllocWithZone,实现了走allocWithZone:
    • 第二次调用直接走callAlloc的其它分支不会调用到alloc

    ⚠️:自己实现一个类的allocWithZone alloc分支就每次都被调用了


    3.3 _objc_rootAllocWithZone

    NEVER_INLINE
    id
    _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
    {
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
    OBJECT_CONSTRUCT_CALL_BADALLOC);
    }

    _objc_rootAllocWithZone直接调用了_class_createInstanceFromZone

    3.4 allocWithZone

    // Replaced by ObjectAlloc
    + (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
    }


    _objc_rootAllocWithZone直接调用了_objc_rootAllocWithZone,与上面的3.3中的逻辑汇合了。

    3.5 _class_createInstanceFromZone

    最终会调用_class_createInstanceFromZone进程内存的计算和分配。


    static ALWAYS_INLINE id
    _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
    int construct_flags = OBJECT_CONSTRUCT_NONE,
    bool cxxConstruct = true,
    size_t *outAllocatedSize = nil)
    {
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    //判断当前class或者superclass是否有.cxx_construct构造方法的实现
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    //判断当前class或者superclass是否有.cxx_destruct析构方法的实现
    bool hasCxxDtor = cls->hasCxxDtor();
    //标记类是否支持优化的isa
    bool fast = cls->canAllocNonpointer();
    size_t size;
    //通过内存对齐得到实例大小,extraBytes是由对象所拥有的实例变量决定的。
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    //对象分配空间
    if (zone) {
    obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
    obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
    if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
    return _objc_callBadAllocHandler(cls);
    }
    return nil;
    }
    //初始化实例isa指针
    if (!zone && fast) {
    obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
    // Use raw pointer isa on the assumption that they might be
    // doing something weird with the zone or RR.
    obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
    return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
    }
  • 调用instanceSize计算空间大小。
  • 根据zone是否有值调用malloc_zone_calloccalloc进行内存分配。
    calloc之前分配的obj是一块脏内存,执行calloc后才会真正分配内存。执行前后内存地址发生了变化。

  • 根据!zone && fast分别调用initInstanceIsainitIsa进行isa实例化。
    • 执行完initInstanceIsa后再次打印就有类型了。
    • 根据是否有hasCxxCtor分别返回obj和调用object_cxxConstructFromClass

    3.6 instanceSize 申请内存

    在这个函数中调用了instanceSize计算实例大小:


    inline size_t instanceSize(size_t extraBytes) const {
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
    return cache.fastInstanceSize(extraBytes);
    }

    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
    }
    • 没有缓存的话会调用alignedInstanceSize,如果最终的size < 16会返回16
    • 有缓存则调用fastInstanceSize
    • 正常情况下缓存是在_read_images的时候生成的。所以这里一般会走fastInstanceSize分支。

    3.6.1 alignedInstanceSize



    #ifdef __LP64__
    # define WORD_MASK 7UL
    #else
    # define WORD_MASK 3UL

    uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
    }

    static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
    }

    uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
    }

    xunalignedInstanceSize获取。读取的是data()->ro()->instanceSize实例变量的大小。由ivars决定。这里为8,因为默认有个isaisaClass ,Classobjc_class struct *类型。

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    • 字节对齐算法为:(x + WORD_MASK) & ~WORD_MASKWORD_MASK 64位下为732位下为3
      那么对于HPObject对象计算方法如下:
      根据公式可得1:(8 + 7) & ~7 等价于 (8 + 7) >>3 << 3
      根据1可得2:15 & ~7
      转换为二进制:0000 1111 & ~0000 0111 = 0000 1111 & 1111 1000
      计算可得:00001000 = 8
      所以alignedInstanceSize计算就是以8字节对齐取8的倍数(算法中是往下取,对于内存分配来讲是往上取)。

    那么为什么以8字节对齐,最后最小分配16呢?
    分配16是为了做容错处理。以8字节对齐(选择8字节是因为8字节类型是最常用最多的)是以空间换取时间,提高CPU读取速度。当然这过程中会做一定的优化。

    3.6.2 fastInstanceSize

    bool hasFastInstanceSize(size_t extra) const
    {
    if (__builtin_constant_p(extra) && extra == 0) {
    return _flags & FAST_CACHE_ALLOC_MASK16;
    }
    return _flags & FAST_CACHE_ALLOC_MASK;
    }

    size_t fastInstanceSize(size_t extra) const
    {
    ASSERT(hasFastInstanceSize(extra));

    if (__builtin_constant_p(extra) && extra == 0) {
    return _flags & FAST_CACHE_ALLOC_MASK16;
    } else {
    size_t size = _flags & FAST_CACHE_ALLOC_MASK;
    // remove the FAST_CACHE_ALLOC_DELTA16 that was added
    // by setFastInstanceSize
    return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    }
    }

    • fastInstanceSize中会调用align16,实现如下(16字节对齐):
    static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
    }

    void setInstanceSize(uint32_t newSize) {
    ASSERT(isRealized());
    ASSERT(data()->flags & RW_REALIZING);
    auto ro = data()->ro();
    if (newSize != ro->instanceSize) {
    ASSERT(data()->flags & RW_COPIED_RO);
    *const_cast<uint32_t *>(&ro->instanceSize) = newSize;
    }
    cache.setFastInstanceSize(newSize);
    }

    size变化只会走会更新在缓存中。那么调用setInstanceSize的地方如下:

    • realizeClassWithoutSwift:类加载的时候计算。这里包括懒加载和非懒加载。这里会调用方法,根据类的实例变量进行size计算。这里是在_read_images的时候调用。
    • class_addIvar:动态添加属性的时候会重新计算实例大小。
    • objc_initializeClassPair_internal:动态添加类相关的初始化。

    instanceSize对于HPObject而言分配内存大小应该为8(isa) + 8(name)+4(age)= 20根据内存对齐应该分配24字节。


    3.7 initInstanceIsa

    inline void 
    objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
    {
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
    }

    initInstanceIsa最终会调用initIsainitIsa最后会对isa进行绑定:

    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
    {
    ASSERT(!isTaggedPointer());

    isa_t newisa(0);

    if (!nonpointer) {
    newisa.setClass(cls, this);
    } else {
    ASSERT(!DisableNonpointerIsa);
    ASSERT(!cls->instancesRequireRawIsa());


    #if SUPPORT_INDEXED_ISA
    ASSERT(cls->classArrayIndex() > 0);
    newisa.bits = ISA_INDEX_MAGIC_VALUE;
    // isa.magic is part of ISA_MAGIC_VALUE
    // isa.nonpointer is part of ISA_MAGIC_VALUE
    newisa.has_cxx_dtor = hasCxxDtor;
    newisa.indexcls = (uintptr_t)cls->classArrayIndex();
    #else
    newisa.bits = ISA_MAGIC_VALUE;
    // isa.magic is part of ISA_MAGIC_VALUE
    // isa.nonpointer is part of ISA_MAGIC_VALUE
    # if ISA_HAS_CXX_DTOR_BIT
    newisa.has_cxx_dtor = hasCxxDtor;
    # endif
    newisa.setClass(cls, this);
    #endif
    newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
    }
    • isa_t是一个union
    • nonpointer表示是否进行指针优化。不优化直接走setClass逻辑,优化走else逻辑。


    作者:HotPotCat
    链接:https://www.jianshu.com/p/884275c811d5



    收起阅读 »

    React的路由,怎么开发得劲儿

    首先确定业务场景如果我们把场景设定在开发一个pc端管理后台的话,那么很常见的需求就是根据不同用户,配置不同的权限,显示不同的菜单项目,渲染不同的路由。那权限到底归谁管一般来说都是后台配置权限,然后驱动前端显示菜单,但我觉得这样不太好,加一个menu就要向后台申...
    继续阅读 »

    首先确定业务场景

    如果我们把场景设定在开发一个pc端管理后台的话,那么很常见的需求就是根据不同用户,配置不同的权限,显示不同的菜单项目,渲染不同的路由。

    那权限到底归谁管

    一般来说都是后台配置权限,然后驱动前端显示菜单,但我觉得这样不太好,加一个menu就要向后台申请,太不灵活,费劲儿。

    我觉得应该也给前台一定程度的权利,让其可以“绕过”后台主导一部分菜单项和路由项的渲染.

    __一言以蔽之__:

    前后台协同把事情办了,后台为主,前端为辅。

    基于以上分析,制定了一个解决方案

    首先列出一下“出场角色”:

    动态结构数据 :通过前后台协同创建数据,其描述的是一种树状关系。

    静态内容数据 :渲染路由和菜单项的基本数据信息。

    菜单项和其关联的路由 :根据以上数据驱动显示。

    静态内容配置

    主要为两个成员:
    • 路由配置:routesMap
    • 菜单项配置:menusMap

      二者相关性太高,故在一起进行管理。

    路由配置:routesMap

    作用:

    每一个路由都是一个单体对象,通过注册routesMap内部来进行统一管理。

    结构:
    {
    ...
    {
    name: "commonTitle_nest", //国际化单位ID
    icon: "thunderbolt", //antd的icon
    path: "/pageCenter/nestRoute", //路径规则
    exact: true, //是否严格匹配
    component: lazyImport(() =>
    import('ROUTES/login')
    ), //组件
    key: uuid() //唯一标识
    }
    ...
    }


    个体参数一览:
    参数类型说明默认值
    namestring国际化的标识ID_
    iconstringantd的icon标识-
    pathstring路径规则-
    exactboolan是否严格匹配false
    componentstring渲染组件-
    keystring唯一标识-
    redirectstring重定向路由地址-
    searchobject"?="-
    paramstringnumber"/*"-
    isNoFormatboolean标识拒绝国际化false

    基本是在react-router基础上进行扩展的,保留了其配置项。

    菜单项配置:menusMap

    作用:

    每个显示在左侧的菜单项目都是一个单体对象,菜单单体内容与路由对象进行关联,并通过注册routesToMenuMap内部来进行统一管理。

    结构:
    {
    ...
    [LIGHT_ID]: {
    ...routesMap.lightHome,
    routes: [
    routesMap.lightAdd,
    routesMap.lightEdit,
    routesMap.lightDetail,
    ],
    }
    ...
    }


    个体参数一览:
    参数类型说明默认值
    routesarray转载路由个体_

    该个体主要关联路由个体,故其参数基本与之一致

    动态结构配置

    主要为两个类别:
    • __menuLocalConfig.json__:前端期望的驱动数据。
    • __menuRemoteConfig.json__:后端期望的驱动数据。
    作用:

    __动静结合,驱动显示__:两文件融合作为动态数据,去激活静态数据(菜单项menusMap)来驱动显示菜单项目和渲染路由组件。

    强调:
    • __menuLocalConfig.json__:是动态数据的组成部份,是“动”中的“静”,完全由前端主导配置。
    • __menuRemoteConfig.json__:应该由后台配置权限并提供,前端配置该数据文件,目的是在后台未返回数据作默认配置,还有模拟mock开发使用。
    结构:
    [   
    ...
    {
    "menuId": 2001,
    "parentId": 1001
    }
    ...
    ]

    简单,直接地去表示结构的数据集合

    动态配置的解释:

    简单讲,对于驱动菜单项和路由的渲染,无论后台配置权限控制前端也好,前端想绕过后端主导显示也好,都是一种期望(种因)。二者协商,结合,用尽可能少的信息描述一个结构(枝繁),从而让静态数据对其进行补充(叶茂),然后再用形成的整体去驱动(结果)。

    快速上手

    注册路由个体

    位置在/src/routes/config.js,栗:

    /* 路由的注册数据,新建路由在这配置 */
    export const routesMap = {
    ...
    templates: {
    name: "commonTitle_nest",
    icon: "thunderbolt",
    path: "/pageCenter/nestRoute",
    exact: true,
    redirect: "/pageCenter/light",
    key: uuid()
    }
    ...
    }


    详:/路由相关/配置/静态内容配置

    决定该路由个体的“出场”

    位置同上,栗:

    /* 路由匹配menu的注册数据,新建后台驱动的menu在这配置 */
    export const menusMap = {
    ...
    [LIGHT_ID]: {
    ...routesMap.lightHome, //“主角”
    routes: [
    routesMap.lightAdd, //“配角”
    routesMap.lightEdit,
    routesMap.lightDetail,
    ],
    },
    ...
    }


    解:首先路由个体出现在该配置中,就说明出场(驱动渲染route)了,但是出场又分为两种:

    类别驱动显示了左侧 MenuItem可以跳转么
    主角可以
    配角没有可以

    以上就已经完成了静态数据的准备,接下来就等动态结构数据类激活它了。

    配置动态结构数据

    后台配置的权限:
    [
    { "menuId": 1002, "parentId": 0 },
    { "menuId": 1001, "parentId": 0 }
    ]

    主导

    前端自定义的权限:
    [
    { "menuId": 2002, "parentId": 1001 },
    { "menuId": 2001, "parentId": 1001 },
    { "menuId": 2003, "parentId": 0 },
    { "menuId": 2004, "parentId": 1002 },
    { "menuId": 2005, "parentId": 1002 }
    ]


    补充

    解:1***2***分别是后台和前台的命名约定(能区分就行,怎么定随意),通过以上数据不难看出二者结合描述了一个树状关系,进而去激活静态数据以驱动渲染页面的菜单和路由。

    简单讲:就是动态数据描述结构,静态数据描述内容,结构去和内容进行匹配,有就显示,没有也不会出问题,二者配合驱动显示。

    至此配置基本完成,可以通过直接修改该文件的方式进行开发和调整,也可以可视化操作。

    配置调整费劲?拖拽吧

    操作后自动刷新。

    自动生成文件
    menuLocalConfig.json

    menuRemoteConfig.json

    总结:

    这样我觉得react的路由开发起来得劲儿了不少,整体的解决方案已经确定,供参考。

    收起阅读 »

    宝, 来学习一下CSS中的宽高比,让 h5 开发更想你的夜!

    在图像和其他响应式元素的宽度和高度之间有一个一致的比例是很重要的。在CSS中,我们使用padding hack已经很多年了,但现在我们在CSS中有了原生的长宽比支持。在这篇文章中,我们将讨论什么是宽高比,我们过去是怎么做的,新的做法是什么。当然,也会有一些用例...
    继续阅读 »

    在图像和其他响应式元素的宽度和高度之间有一个一致的比例是很重要的。在CSS中,我们使用padding hack已经很多年了,但现在我们在CSS中有了原生的长宽比支持。

    在这篇文章中,我们将讨论什么是宽高比,我们过去是怎么做的,新的做法是什么。当然,也会有一些用例,对它们进行适当的回退。

    什么是高宽比

    根据维基百科的说法:

    在数学上,比率表示一个数字包含另一个数字的多少倍。例如,如果一碗水果中有八个橙子和六个柠檬,那么橙子和柠檬的比例是八比六(即8∶6,相当于比值4∶3)。

    在网页设计中,高宽比的概念是用来描述图像的宽度和高度应按比例调整。

    考虑下图

    比率是4:3,这表明苹果和葡萄的比例是4:3

    换句话说,我们可以为宽高比为4:3的最小框是4px * 3px框。 当此盒式高度按比例调整为其宽度时,我们将有一个致宽尺寸的框。

    考虑下图。

    盒子被按比例调整大小,其宽度和高度之间的比例是一致的。现在,让我们想象一下,这个盒子里有一张重要的图片,我们关心它的所有细节。

    请注意,无论大小如何,图像细节都被保留。通过拥有一致的高宽比,我们可以获得以下好处

    • 整个网站的图像将在不同的视口大小上保持一致。
    • 我们也可以有响应式的视频元素。
    • 它有助于设计师创建一个图像大小的清晰指南,这样开发者就可以在开发过程中处理它们。

    计算宽高比

    为了测量宽高比,我们需要将宽度除以如下图所示的高度。

    宽度和高度之间的比例是1.33。这意味着这个比例应该得到遵守。请考虑

    注意右边的图片,宽度÷高度的值是 1.02,这不是原来的长宽比(1.33或4:3)。

    你可能在想,如何得出4:3这个数值?嗯,这被称为最接近的正常长宽比,有一些工具可以帮助我们找到它。在进行UI设计时,强烈建议你确切地知道你所使用的图像的宽高比是多少。使用这个网址可以帮我们快速计算。

    网址地址:http://lawlesscreation.github...

    在 CSS 中实现宽高比

    我们过去是通过在CSS中使用百分比padding 来实现宽高比的。好消息是,最近,我们在所有主要的浏览器中都得到了aspect-ratio的原生支持。在深入了解原生方式之前,我们先首先解释一下好的老方法。

    当一个元素有一个垂直百分比的padding时,它将基于它的父级宽度。请看下图。

    当标题有padding-top: 50%时,该值是根据其父元素的宽度来计算的。因为父元素的宽度是200px,所以padding-top会变成100px

    为了找出要使用的百分比值,我们需要将图像的高度除以宽度。得到的数字就是我们要使用的百分比。

    假设图像宽度为260px,高度为195px

    Percentage padding = height / width

    195/260的结果为 0.75(或75%)。

    我们假设有一个卡片的网格,每张卡片都有一个缩略图。这些缩略图的宽度和高度应该是相等的。

    由于某些原因,运营上传了一张与其他图片大小不一致的图片。注意到中间那张卡的高度与其他卡的高度不一样。

    你可能会想,这还不容易解决?我们可以给图片加个object-fit: cover。问题解决了,对吗?不是这么简单滴。这个解决方案在多种视口尺寸下都不会好看。

    注意到在中等尺寸下,固定高度的图片从左边和右边被裁剪得太厉害,而在手机上,它们又太宽。所有这些都是由于使用了固定高度的原因。我们可以通过不同的媒体查询手动调整高度,但这不是一个实用的解决方案。

    我们需要的是,无论视口大小如何,缩略图的尺寸都要一致。为了实现这一点,我们需要使用百分比padding来实现一个宽高比。

    HTML

    <article class="card">
    <div class="card__thumb">
    <img src="thumb.jpg" alt="" />
    </div>
    <div class="card__content">
    <h3>Muffins Recipe</h3>
    <p>Servings: 3</p>
    </div>
    </article>

    CSS

    .card__thumb {
    position: relative;
    padding-top: 75%;
    }

    .card__thumb img {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    }


    通过上述,我们定义了卡片缩略图包装器(.card__thumb)的高度取决于其宽度。另外,图片是绝对定位的,它有它的父元素的全部宽度和高度,有object-fit: cover,用于上传不同大小的图片的情况。请看下面的动图。

    请注意,卡片大小的变化和缩略图的长宽比没有受到影响。

    aspect-ratio 属性

    今年早些时候,Chrome、Safari TP和Firefox Nightly都支持aspect-ratio CSS 属性。最近,它在Safari 15的官方版本中得到支持。

    我们回到前面的例子,我们可以这样改写它。

    /* 上面的方式 */
    .card__thumb {
    position: relative;
    padding-top: 75%;
    }

    /* 使用 aspect-ratio 属性 */
    .card__thumb {
    position: relative;
    aspect-ratio: 4/3;
    }


    请看下面的动图,了解宽高比是如何变化的。

    Demo 地址:https://codepen.io/shadeed/pe...

    有了这个,让我们探索原始纵横比可以有用的一些用例,以及如何以逐步增强的方法使用它。

    渐进增强

    我们可以通过使用CSS @supports和CSS变量来使用CSS aspect-ratio

    .card {
    --aspect-ratio: 16/9;
    padding-top: calc((1 / (var(--aspect-ratio))) * 100%);
    }

    @supports (aspect-ratio: 1) {
    .card {
    aspect-ratio: var(--aspect-ratio);
    padding-top: initial;
    }
    }


    Logo Images

    来看看下面的 logo

    你是否注意到它们的尺寸是一致的,而且它们是对齐的?来看看幕后的情况。

    // html
    <li class="brands__item">
    <a href="#">
    <img src="assets/batch-2/aanaab.png" alt="" />
    </a>
    </li>
    .brands__item a {
    padding: 1rem;
    }

    .brands__item img {
    width: 130px;
    object-fit: contain;
    aspect-ratio: 2/1;
    }


    我添加了一个130px的基本宽度,以便有一个最小的尺寸,而aspect-ratio会照顾到高度。

    蓝色区域是图像的大小,object-fit: contain是重要的,避免扭曲图像。

    Responsive Circles

    你是否曾经需要创建一个应该是响应式的圆形元素?CSS aspect-ratio是这种使用情况的最佳选择。

    .person {
    width: 180px;
    aspect-ratio: 1;
    }

    如果宽高比的两个值相同,我们可以写成aspect-ratio: 1而不是aspect-ratio: 1/1。如果你使用flexboxgrid ,宽度将是可选的,它可以被添加作为一个最小值。

    ~完,我是小智,宝,你学会了吗~


    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    原文:https://ishadeed.com/article/...


    收起阅读 »

    前端白屏监控探索

    背景不知从什么时候开始,前端白屏问题成为一个非常普遍的话题,'白屏' 甚至成为了前端 bug 的代名词:_喂,你的页面白了。_而且,'白' 这一现象似乎对于用户体感上来说更加强,回忆起 windows 系统的崩溃 '蓝屏'。可以说是非常相似了,甚至能明白了白屏...
    继续阅读 »

    背景

    不知从什么时候开始,前端白屏问题成为一个非常普遍的话题,'白屏' 甚至成为了前端 bug 的代名词:_喂,你的页面白了。_而且,'白' 这一现象似乎对于用户体感上来说更加强,回忆起 windows 系统的崩溃 '蓝屏'。
    可以说是非常相似了,甚至能明白了白屏这个词汇是如何统一出来的。那么,体感如此强烈的现象势必会给用户带来一些不好的影响,如何能尽早监听,快速消除影响就显得很重要了。

    为什么单独监控白屏

    不光光是白屏,白屏只是一种现象,我们要做的是精细化的异常监控。异常监控各个公司肯定都有自己的一套体系,集团也不例外,而且也足够成熟。但是通用的方案总归是有缺点的,如果对所有的异常都加以报警和监控,就无法区分异常的严重等级,并做出相应的响应,所以在通用的监控体系下定制精细化的异常监控是非常有必要的。这就是本文讨论白屏这一场景的原因,我把这一场景的边界圈定在了 “白屏” 这一现象。

    方案调研

    白屏大概可能的原因有两种:

    1. js 执行过程中的错误
    2. 资源错误

    这两者方向不同,资源错误影响面较多,且视情况而定,故不在下面方案考虑范围内。为此,参考了网上的一些实践加上自己的一些调研,大概总结出了一些方案:

    一、onerror + DOM 检测

    原理很简单,在当前主流的 SPA 框架下,DOM 一般挂载在一个根节点之下(比如 <div id="root"></div> )发生白屏后通常现象是根节点下所有 DOM 被卸载,该方案就是通过监听全局的 onerror 事件,在异常发生时去检测根节点下是否挂载 DOM,若无则证明白屏。
    我认为是非常简单暴力且有效的方案。但是也有缺点:其一切建立在 **白屏 === 根节点下 DOM 被卸载** 成立的前提下,实际并非如此比如一些微前端的框架,当然也有我后面要提到的方案,这个方案和我最终方案天然冲突。

    二、Mutation Observer Api

    不了解的可以看下文档
    其本质是监听 DOM 变化,并告诉你每次变化的 DOM 是被增加还是删除。为其考虑了多种方案:

    1. 搭配 onerror 使用,类似第一个方案,但很快被我否决了,虽然其可以很好的知道 DOM 改变的动向,但无法和具体某个报错联系起来,两个都是事件监听,两者是没有必然联系的。
    2. 单独使用判断是否有大量 DOM 被卸载,缺点:白屏不一定是 DOM 被卸载,也有可能是压根没渲染,且正常情况也有可能大量 DOM 被卸载。完全走不通。
    3. 单独使用其监听时机配合 DOM 检测,其缺点和方案一一样,而且我觉得不如方案一。因为它没法和具体错误联系起来,也就是没法定位。当然我和其他团队同学交流的时候他们给出了其他方向:通过追踪用户行为数据来定位问题,我觉得也是一种方法。

    一开始我认为这就是最终答案,经过了漫长的心里斗争,最终还是否定掉了。不过它给了一个比较好的监听时机的选择。

    三、饿了么-Emonitor 白屏监控方案

    饿了么的白屏监控方案,其原理是记录页面打开 4s 前后 html 长度变化,并将数据上传到饿了么自研的时序数据库。如果一个页面是稳定的,那么页面长度变化的分布应该呈现「幂次分布」曲线的形态,p10、p20 (排在文档前 10%、20%)等数据线应该是平稳的,在一定的区间内波动,如果页面出现异常,那么曲线一定会出现掉底的情况。

    其他

    其他都大同小样,其实调研了一圈下来发现无非就是两点

    1. 监控时机:调研下来常见的就三种:

      • onerror
      • mutation observer api
      • 轮训
    2. DOM 检测:这个方案就很多了,除了上述的还可以:

      • elementsFromPoint api 采样
      • 图像识别
      • 基于 DOM 的各种数据的各种算法识别
      • ...

    改变方向

    几番尝试下来几乎没有我想要的,其主要原因是准确率 -- 这些方案都不能保证我监听到的是白屏,单从理论的推导就说不通。他们都有一个共同点:监听的是'白屏'这个现象,从现象去推导本质虽然能成功,但是不够准确。所以我真正想要监听的是造成白屏的本质。

    那么回到最开始,什么是白屏?他是如何造成的?是因为错误导致的浏览器无法渲染?不,在这个 spa 框架盛行的现在实际上的白屏是框架造成的,本质是由于错误导致框架不知道怎么渲染所以干脆就不渲染。由于我们团队 React 技术栈居多,我们来看看 React 官网的一段话

    React 认为把一个错误的 UI 保留比完全移除它更糟糕。我们不讨论这个看法的正确与否,至少我们知道了白屏的原因:渲染过程的异常且我们没有捕获异常并处理。

    反观目前的主流框架:我们把 DOM 的操作托管给了框架,所以渲染的异常处理不同框架方法肯定不一样,这大概就是白屏监控难统一化产品化的原因。但大致方向肯定是一样的。

    那么关于白屏我认为可以这么定义:异常导致的渲染失败

    那么白屏的监控方案即:监控渲染异常。那么对于 React 而言,答案就是: Error Boundaries

    Error Boundaries

    我们可以称之为错误边界,错误边界是什么?它其实就是一个生命周期,用来监听当前组件的 children 渲染过程中的错误,并可以返回一个 降级的 UI 来渲染:

    class ErrorBoundary extends React.Component {
    constructor(props) {
    super(props);
    this.state = { hasError: false };
    }

    static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
    // 我们可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
    }

    render() {
    if (this.state.hasError) {
    // 我们可以自定义降级后的 UI 并渲染
    return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
    }
    }


    一个有责任心的开发一定不会放任错误的发生。错误边界可以包在任何位置并提供降级 UI,也就是说,一旦开发者'有责任心' 页面就不会全白,这也是我之前说的方案一与之天然冲突且其他方案不稳定的情况。
    那么,在这同时我们上报异常信息,这里上报的异常一定会导致我们定义的白屏,这一推导是 100% 正确的。

    100% 这个词或许不够负责,接下来我们来看看为什么我说这一推导是 100% 准确的:

    React 渲染流程

    我们来简单回顾下从代码到展现页面上 React 做了什么。
    我大致将其分为几个阶段:render => 任务调度 => 任务循环 => 提交 => 展示
    我们举一个简单的例子来展示其整个过程(任务调度不再本次讨论范围故不展示):

    const App = ({ children }) => (
    <>
    <p>hello</p>
    { children }
    </>
    );
    const Child = () => <p>I'm child</p>

    const a = ReactDOM.render(
    <App><Child/></App>,
    document.getElementById('root')
    );


    首先浏览器是不认识我们的 jsx 语法的,所以我们通过 babel 编译大概能得到下面的代码:

    var App = function App(_ref2) {
    var children = _ref2.children;
    return React.createElement("p", null, "hello"), children);
    };

    var Child = function Child() {
    return React.createElement("p", null, "I'm child");
    };

    ReactDOM.render(React.createElement(App, null, React.createElement(Child, null)), document.getElementById('root'));

    babel 插件将所有的 jsx 都转成了 createElement 方法,执行它会得到一个描述对象 ReactElement 大概长这样子:

    {
    $$typeof: Symbol(react.element),
    key: null,
    props: {}, // createElement 第二个参数 注意 children 也在这里,children 也会是一个 ReactElement 或 数组
    type: 'h1' // createElement 的第一个参数,可能是原生的节点字符串,也可能是一个组件对象(Function、Class...)
    }


    所有的节点包括原生的 <a></a> 、 <p></p> 都会创建一个 FiberNode ,他的结构大概长这样:

    FiberNode = {
    elementType: null, // 传入 createElement 的第一个参数
    key: null,
    type: HostRoot, // 节点类型(根节点、函数组件、类组件等等)
    return: null, // 父 FiberNode
    child: null, // 第一个子 FiberNode
    sibling: null, // 下一个兄弟 FiberNode
    flag: null, // 状态标记
    }


    你可以把它理解为 Virtual Dom 只不过多了许多调度的东西。最开始我们会为根节点创建一个 FiberNodeRoot 如果有且仅有一个 ReactDOM.render 那么他就是唯一的根,当前有且仅有一个 FiberNode 树。

    我只保留了一些渲染过程中重要的字段,其他还有很多用于调度、判断的字段我这边就不放出来了,有兴趣自行了解

    render

    现在我们要开始渲染页面,是我们刚才的例子,执行 ReactDOM.render 。这里我们有个全局 workInProgress 对象标志当前处理的 FiberNode

    1. 首先我们为根节点初始化一个 FiberNodeRoot ,他的结构就如上面所示,并将 workInProgress= FiberNodeRoot
    2. 接下来我们执行 ReactDOM.render 方法的第一个参数,我们得到一个 ReactElement :
    ReactElement = {
    $$typeof: Symbol(react.element),
    key: null,
    props: {
    children: {
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ƒ Child(),
    }
    }
    ref: null,
    type: f App()
    }


    该结构描述了 <App><Child /></App>

    1. 我们为 ReactElement 生成一个 FiberNode 并把 return 指向父 FiberNode ,最开始是我们的根节点,并将 workInProgress = FiberNode
    {
    elementType: f App(), // type 就是 App 函数
    key: null,
    type: FunctionComponent, // 函数组件类型
    return: FiberNodeRoot, // 我们的根节点
    child: null,
    sibling: null,
    flags: null
    }


    1. 只要workInProgress 存在我们就要处理其指向的 FiberNode 。节点类型有很多,处理方法也不太一样,不过整体流程是相同的,我们以当前函数式组件为例子,直接执行 App(props) 方法,这里有两种情况

      • 该组件 return 一个单一节点,也就是返回一个 ReactElement 对象,重复 3 - 4 的步骤。并将当前 节点的 child 指向子节点 CurrentFiberNode.child = ChildFiberNode 并将子节点的 return 指向当前节点 ChildFiberNode.return = CurrentFiberNode
      • 该组件 return 多个节点(数组或者 Fragment ),此时我们会得到一个 ChildiFberNode 的数组。我们循环他,每一个节点执行 3 - 4 步骤。将当前节点的 child 指向第一个子节点 CurrentFiberNode.child = ChildFiberNodeList[0] ,同时每个子节点的 sibling 指向其下一个子节点(如果有) ChildFiberNode[i].sibling = ChildFiberNode[i + 1] ,每个子节点的 return 都指向当前节点 ChildFiberNode[i].return = CurrentFiberNode

    如果无异常每个节点都会被标记为待布局 FiberNode.flags = Placement

    1. 重复步骤直到处理完全部节点 workInProgress 为空。

    最终我们能大概得到这样一个 FiberNode 树:

    FiberNodeRoot = {
    elementType: null,
    type: HostRoot,
    return: null,
    child: FiberNode<App>,
    sibling: null,
    flags: Placement, // 待布局状态
    }

    FiberNode<App> {
    elementType: f App(),
    type: FunctionComponent,
    return: FiberNodeRoot,
    child: FiberNode<p>,
    sibling: null,
    flags: Placement // 待布局状态
    }

    FiberNode<p> {
    elementType: 'p',
    type: HostComponent,
    return: FiberNode<App>,
    sibling: FiberNode<Child>,
    child: null,
    flags: Placement // 待布局状态
    }

    FiberNode<Child> {
    elementType: f Child(),
    type: FunctionComponent,
    return: FiberNode<App>,
    child: null,
    flags: Placement // 待布局状态
    }


    提交阶段

    提交阶段简单来讲就是拿着这棵树进行深度优先遍历 child => sibling,放置 DOM 节点并调用生命周期。

    那么整个正常的渲染流程简单来讲就是这样。接下来看看异常处理

    错误边界流程

    刚刚我们了解了正常的流程现在我们制造一些错误并捕获他:

    const App = ({ children }) => (
    <>
    <p>hello</p>
    { children }
    </>
    );
    const Child = () => <p>I'm child {a.a}</p>

    const a = ReactDOM.render(
    <App>
    <ErrorBoundary><Child/></ErrorBoundary>
    </App>,
    document.getElementById('root')
    );


    执行步骤 4 的函数体是包裹在 try...catch 内的如果捕获到了异常则会走异常的流程:

    do {
    try {
    workLoopSync(); // 上述 步骤 4
    break;
    } catch (thrownValue) {
    handleError(root, thrownValue);
    }
    } while (true);

    执行步骤 4 时我们调用 Child 方法由于我们加了个不存在的表达式 {a.a} 此时会抛出异常进入我们的 handleError 流程此时我们处理的目标是 FiberNode<Child> ,我们来看看 handleError :

    function handleError(root, thrownValue): void {
    let erroredWork = workInProgress; // 当前处理的 FiberNode 也就是异常的 节点
    throwException(
    root, // 我们的根 FiberNode
    erroredWork.return, // 父节点
    erroredWork,
    thrownValue, // 异常内容
    );
    completeUnitOfWork(erroredWork);
    }

    function throwException(
    root: FiberRoot,
    returnFiber: Fiber,
    sourceFiber: Fiber,
    value: mixed,
    ) {
    // The source fiber did not complete.
    sourceFiber.flags |= Incomplete;

    let workInProgress = returnFiber;
    do {
    switch (workInProgress.tag) {
    case HostRoot: {
    workInProgress.flags |= ShouldCapture;
    return;
    }
    case ClassComponent:
    // Capture and retry
    const ctor = workInProgress.type;
    const instance = workInProgress.stateNode;
    if (
    (workInProgress.flags & DidCapture) === NoFlags &&
    (typeof ctor.getDerivedStateFromError === 'function' ||
    (instance !== null &&
    typeof instance.componentDidCatch === 'function' &&
    !isAlreadyFailedLegacyErrorBoundary(instance)))
    ) {
    workInProgress.flags |= ShouldCapture;
    return;
    }
    break;
    default:
    break;
    }
    workInProgress = workInProgress.return;
    } while (workInProgress !== null);
    }


    代码过长截取一部分
    先看 throwException 方法,核心两件事:

    1. 将当前也就是出问题的节点状态标志为未完成 FiberNode.flags = Incomplete
    2. 从父节点开始冒泡,向上寻找有能力处理异常( ClassComponent )且的确处理了异常的(声明了 getDerivedStateFromError 或 componentDidCatch 生命周期)节点,如果有,则将那个节点标志为待捕获 workInProgress.flags |= ShouldCapture ,如果没有则是根节点。

    completeUnitOfWork 方法也类似,从父节点开始冒泡,找到 ShouldCapture 标记的节点,如果有就标记为已捕获 DidCapture  ,如果没找到,则一路把所有的节点都标记为 Incomplete 直到根节点,并把 workInProgress 指向当前捕获的节点。

    之后从当前捕获的节点(也有可能没捕获是根节点)开始重新走流程,由于其状态 react 只会渲染其降级 UI,如果有 sibling 节点则会继续走下面的流程。我们看看上述例子最终得到的 FiberNode 树:

    FiberNodeRoot = {
    elementType: null,
    type: HostRoot,
    return: null,
    child: FiberNode<App>,
    sibling: null,
    flags: Placement, // 待布局状态
    }

    FiberNode<App> {
    elementType: f App(),
    type: FunctionComponent,
    return: FiberNodeRoot,
    child: FiberNode<p>,
    sibling: null,
    flags: Placement // 待布局状态
    }

    FiberNode<p> {
    elementType: 'p',
    type: HostComponent,
    return: FiberNode<App>,
    sibling: FiberNode<ErrorBoundary>,
    child: null,
    flags: Placement // 待布局状态
    }

    FiberNode<ErrorBoundary> {
    elementType: f ErrorBoundary(),
    type: ClassComponent,
    return: FiberNode<App>,
    child: null,
    flags: DidCapture // 已捕获状态
    }

    FiberNode<h1> {
    elementType: f ErrorBoundary(),
    type: ClassComponent,
    return: FiberNode<ErrorBoundary>,
    child: null,
    flags: Placement // 待布局状态
    }


    如果没有配置错误边界那么根节点下就没有任何节点,自然无法渲染出任何内容。

    ok,相信到这里大家应该清楚错误边界的处理流程了,也应该能理解为什么我之前说由 ErrorBoundry 推导白屏是 100% 正确的。当然这个 100% 指的是由 ErrorBoundry 捕捉的异常基本上会导致白屏,并不是指它能捕获全部的白屏异常。以下场景也是他无法捕获的:

    • 事件处理
    • 异步代码
    • SSR
    • 自身抛出来的错误

    React SSR 设计使用流式传输,这意味着服务端在发送已经处理好的元素的同时,剩下的仍然在生成 HTML,也就是其父元素无法捕获子组件的错误并隐藏错误的组件。这种情况似乎只能将所有的 render 函数包裹 try...catch ,当然我们可以借助 babel 或 TypeScript 来帮我们简单实现这一过程,其最终得到的效果是和 ErrorBoundry 类似的。

    而事件和异步则很巧,虽说 ErrorBoundry 无法捕获他们之中的异常,不过其产生的异常也恰好不会造成白屏(如果是错误的设置状态,间接导致了白屏,刚好还是会被捕获到)。这就在白屏监控的职责边界之外了,需要别的精细化监控能力来处理它。

    总结

    那么最后总结下本文的出的几个结论:
    我对白屏的定义:异常导致的渲染失败
    对应方案是:资源监听 + 渲染流程监听

    在目前 SPA 框架下白屏的监控需要针对场景做精细化的处理,这里以 React 为例子,通过监听渲染过程异常能够很好的获得白屏的信息,同时能增强开发者对异常处理的重视。而其他框架也会有相应的方法来处理这一现象。

    当然这个方案也有弱点,由于是从本质推导现象其实无法 cover 所有的白屏的场景,比如我要搭配资源的监听来处理资源异常导致的白屏。当然没有一个方案是完美的,我这里也是提供一个思路,欢迎大家一起讨论。


    收起阅读 »

    面试官问我会canvas? 我可以绘制一个烟花?动画

    在我们日常开发中贝塞尔曲线无处不在:svg 中的曲线(支持 2阶、 3阶)canvas 中绘制贝塞尔曲线几乎所有前端2D或3D图形图表库(echarts,d3,three.js)都会使用到贝塞尔曲线所以掌握贝塞尔曲线势在必得。 这篇文章主要是实战篇,不会介绍和...
    继续阅读 »

    在我们日常开发中贝塞尔曲线无处不在:

    1. svg 中的曲线(支持 2阶、 3阶)
    2. canvas 中绘制贝塞尔曲线
    3. 几乎所有前端2D或3D图形图表库(echarts,d3,three.js)都会使用到贝塞尔曲线

    所以掌握贝塞尔曲线势在必得。 这篇文章主要是实战篇,不会介绍和贝塞尔相关的知识, 如果有同学对贝塞尔曲线不是很清楚的话:可以查看我这篇文章——深入理解SVG

    绘制贝塞尔曲线

    第一步我们先创建ctx, 用ctx 画一个二阶贝塞尔曲线看下。二阶贝塞尔曲线有1个控制点,一个起点,一个终点。

    const canvas = document.getElementById( 'canvas' );
    const ctx = canvas.getContext( '2d' );
    ctx.beginPath();
    ctx.lineWidth = 2;
    ctx.strokeStyle = '#000';
    ctx.moveTo(100,100)
    ctx.quadraticCurveTo(180,50, 200,200)
    ctx.stroke();


    这样我们就画好了一个贝塞尔曲线了。

    绘制贝塞尔曲线动画

    画一条线谁不会哇?接下来文章的主体内容。 首先试想一下动画我们肯定一步步画出曲线? 但是这个ctx给我们全部画出来了是不是有点问题。我们重新看下二阶贝塞尔曲线的实现过程动画,看看是否有思路。

    从图中可以分析得出贝塞尔上的曲线是和t有关系的, t的区间是在0-1之间,我们是不是可以通过二阶贝塞尔的曲线方程去算出每一个点呢,这个专业术语叫离散化,但是这样的得出来的点的信息是不太准的,我们先这样实现。

    先看下方程:

    我们模拟写出代码如如下:

    //这个就是二阶贝塞尔曲线方程
    function twoBezizer(p0, p1, p2, t) {
    const k = 1 - t
    return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2
    }

    //离散
    function drawWithDiscrete(ctx, start, control, end,percent) {
    for ( let t = 0; t <= percent / 100; t += 0.01 ) {
    const x = twoBezizer(start[0], control[0], end[0], t)
    const y = twoBezizer(start[1], control[1], end[1], t)
    ctx.lineTo(x, y)
    }
    }


    我们看下效果:

    和我们画的几乎是一模一样,接下啦就用requestAnimationFrame 开始我们的动画给出以下代码:

    let percent = 0
    function animate() {
    ctx.clearRect( 0, 0, 800, 800 );
    ctx.beginPath();
    ctx.moveTo(100,100)
    drawWithDiscrete(ctx,[100,100],[180,50],[200,200],percent)
    ctx.stroke();
    percent = ( percent + 1 ) % 100;
    id = requestAnimationFrame(animate)
    }
    animate()


    这里有两个要注意的是, 我是是percent 不断加1 和100 求余,所以呢 percent 会不断地从1-100 这样往复,OK所以我们必须要动画之前做一次区域清理, ctx.clearRect( 0, 0, 800, 800 ); 这样就可以不断的从开始到结束循环往复,我们看下效果:

    看着样子是不是还不错哈哈哈😸。

    绘制贝塞尔曲线动画方法2

    你以为这样就结束了? 当然不是难道我们真的没有办法画出某一个t的贝塞尔曲线了? 当前不是,这里放一下二阶贝塞尔方程的推导过程:

    二阶贝塞尔曲线上的任意一点,都是可以通过同样比例获得。 在两点之间的任意一点,其实满足的一阶贝塞尔曲线, 一阶贝塞尔曲线满足的其实是线性变化。我给出以下方程

     function oneBezizer(p0,p1,t) {
    return p0 + (p1-p0) * t
    }

    从我画的图可以看出,我们只要 不断求A点 和C点就可以画出在某一时间段的贝塞尔了。

    我给出以下代码和效果图:

    function drawWithDiscrete2(ctx, start, control, end,percent) {
    const t = percent/ 100;
    // 求出A点
    const A = [];
    const C = [];
    A[0] = oneBezizer(start[0],control[0],t);
    A[1] = oneBezizer(start[1],control[1],t);
    C[0] = twoBezizer(start[0], control[0], end[0], t)
    C[1] = twoBezizer(start[1], control[1], end[1], t)
    ctx.quadraticCurveTo(
    A[ 0 ], A [ 1 ],
    C[ 0 ], C[ 1 ]
    );
    }


    礼花🎉动画

    上文我们实现了一条贝塞尔线,我们将这条贝塞尔的曲线的开始点作为一个圆的圆心,然后按照某个次数求出不同的结束点。 再写一个随机颜色,礼花效果就成了, 直接上代码,

    for(let i=0; i<count; i++) {
    const angle = Math.PI * 2 / count * i;
    const x = center[ 0 ] + radius * Math.sin( angle );
    const y = center[ 1 ] + radius * Math.cos( angle );
    ctx.strokeStyle = colors[ i ];
    ctx.beginPath();
    drawWithDiscrete(ctx, center,[180,50],[x,y],percent)
    ctx.stroke();
    }

    function getRandomColor(colors, count) {
    // 生成随机颜色
    for ( let i = 0; i < count; i++ ) {
    colors.push(
    'rgb( ' +
    ( Math.random() * 255 >> 0 ) + ',' +
    ( Math.random() * 255 >> 0 ) + ',' +
    ( Math.random() * 255 >> 0 ) +
    ' )'
    );
    }
    }


    我们看下动画吧:



    收起阅读 »

    在 React 应用中展示报表数据

    创建 React 应用创建 React 应用 参考链接, 如使用npx 包运行工具:npx create-react-app arjs-react-viewer-app如果您使用的是yarn,执行命令:yarn create react-app arjs-re...
    继续阅读 »

    创建 React 应用

    创建 React 应用 参考链接, 如使用npx 包运行工具:

    npx create-react-app arjs-react-viewer-app
    如果您使用的是yarn,执行命令:

    yarn create react-app arjs-react-viewer-app
    更多创建 React方法可参考 官方文档

    安装 ActivereportsJS NPM 包

    React 报表 Viewer 组件已经放在了npm @grapecity/activereports-react npm 中。 @grapecity/activereports 包提供了全部的核心功能。

    运行以下命令安装包:

    npm install @grapecity/activereports-react @grapecity/activereports
    或使用yarn命令

    yarn add @grapecity/activereports-react @grapecity/activereports

    导入 ActiveReportsJS 样式

    打开 src\App.css 文件并添加以下代码,导入Viewer 的默认样式,定义了元素宿主的样式React Report Viewer 控件:

    @import "@grapecity/activereports/styles/ar-js-ui.css";
    @import "@grapecity/activereports/styles/ar-js-viewer.css";

    viewer-host {

    width: 100%;
    height: 100vh;
    }

    添加 ActiveReportsJS 报表

    ActiveReportsJS 使用 JSON格式和 rdlx-json扩展报表模板文件。在应用程序的public文件夹中,创建名为 report.rdlx-json 的新文件,并在该文件中插入以下JSON内容:

    {
    "Name": "Report",
    "Body": {

    "ReportItems": [
    {
    "Type": "textbox",
    "Name": "TextBox1",
    "Value": "Hello, ActiveReportsJS Viewer",
    "Style": {
    "FontSize": "18pt"
    },
    "Width": "8.5in",
    "Height": "0.5in"
    }
    ]
    }
    }


    添加 React 报表 Viewer 控件

    修改 src\App.js代码:

    import React from "react";
    import "./App.css";
    import { Viewer } from "@grapecity/activereports-react";

    function App() {
    return (

    <div id="viewer-host">
    <Viewer report={{ Uri: 'report.rdlx-json' }} />
    </div>
    );
    }


    export default App;

    运行和调试

    使用 npm start 或 yarn start 命令运行项目,如果编译失败了,报以下错误,请删除node_modules 文件夹并重新运行 npm install 或 yarn命令来重新安装需要的包文件。

    react-scripts start

    internal/modules/cjs/loader.js:883
    throw err;
    ^

    Error: Cannot find module 'react'
    当应用程序启动时,ActiveReportsJS Viewer组件将出现在页面上。Viewer将显示显示“ Hello,ActiveReportsJS Viewer”文本的报表。您可以通过使用工具栏上的按钮或将报表导出为可用格式之一来测试。

    原文:https://segmentfault.com/a/1190000040257641

    收起阅读 »

    【开源项目】声网Agora+环信IM实现的社交APP---CircleLive

    CircleLive分享,遇见,Live分享自己絮语,心情,碎碎念,在Live中遇见共鸣。技术支持1、Agora互动直播SDK多人音视频互动2、Agora云信令实时消息通信,Live过程的消息分发3、环信IM 支持,建立好有关系,会话主要功能1、发布和浏览心情...
    继续阅读 »

    CircleLive
    分享,遇见,Live

    分享自己絮语,心情,碎碎念,在Live中遇见共鸣。

    技术支持
    1、Agora互动直播SDK
    多人音视频互动

    2、Agora云信令
    实时消息通信,Live过程的消息分发

    3、环信
    IM 支持,建立好有关系,会话

    主要功能
    1、发布和浏览心情动态
    2、Live
      -发布Live
      -订阅共鸣Live
      -多人音视频Live
      -Live心情滤镜
    3、IM
      -加好友(自动创建会话)
      -基本IM(文字,语言,图片,视频,表情)


    视频Demo




    安装包
    Apk下载:链接: https://pan.baidu.com/s/111xZY2ANYCNZORt0ydLg9Q 密码: voil


    视频和APK链接如果失效,请email我 stonelavender@hotmail.com

    测试账号
    也可以新注册

    欢天喜地
    你好
    灰色头像
    密码均为:123456

    收起阅读 »

    【开源项目】用环信IM实现的公益APP--宝贝回家Baby back home

    项目背景有时,只一瞬间没回头,生命中的最重要就消失不见。 这是电影《亲爱的》中一句最经典的台词,看完整个电影,就会明白失去孩子的父母有多无助,拐卖孩子的人贩子有多可恨。 当今社会通过网络平台,短视频,404页面等寻亲信息曝光,很多热心群众都自发参与帮助走失儿童...
    继续阅读 »

    项目背景
    有时,只一瞬间没回头,生命中的最重要就消失不见。 这是电影《亲爱的》中一句最经典的台词,看完整个电影,就会明白失去孩子的父母有多无助,拐卖孩子的人贩子有多可恨。 当今社会通过网络平台,短视频,404页面等寻亲信息曝光,很多热心群众都自发参与帮助走失儿童寻找家人。但还没有发现一个功能实用,寻亲信息集中的移动端公益平台。 本项目是一个为丢失家人的家庭提供发布寻人信息和搜集线索的移动端平台,借助网络传播的力量,通过环信IM一对一实时消息,声网音视频的高效连接,帮助走失的宝贝尽快回归家庭。

    项目说明
    本项目使用OC开发
    接入声网SDK AgoraRtcEngine_iOS
    接入环信SDK EaseCallKit

    运行说明
    本项目使用iOS设备运行
    在注册页面注册账号后就能正常登录

    开发环境
    Xcode 12.1

    运行环境
    iOS 11.0

    功能介绍
    0, 注册登录
    本项目登录注册功能全部采用环信sdk提供的登录注册功能

    1, 首页
    首页主要是展示寻人信息,点击列表可以进入查看详情并私信发布人,利用环信一对一实时通讯让有线索的人更高效顺畅的与寻人者建立通讯。

    2, 寻找
    寻找页可以按条件筛选丢失人员的信息,除内容关键词搜索外,还可按地区,按发布时间,按性别分类筛选,快速查找到符合的信息。

    3,消息
    可以接收到有线索人的私信,点击头像添加好友,进行聊天。 界面右上角 “+” 号可以搜索添加好友,左上角点击进入联系人界面。

    4,我的
    个人信息管理。可更换头像,修改昵称,查看我发布的信息,以及退出登录

    5,发布
    发布寻人信息,输入被寻人相关信息,丢失原因,时间,地址,姓名,性别,联系方式等。发布后还可以将本条信息转发至qq群,微信群,微信好友,借助网友的力量一起寻找。 因没有服务器,使用的本地数据库。发布后可以在首页查看


    项目截图:





    iOS端源码下载地址:https://hub.fastgit.org/AgoraIO-Community/RTE-2021-Innovation-Challenge/tree/master/Application-Challenge/%E3%80%90%E5%BC%A0%E5%AE%87%E3%80%91%E5%AE%9D%E8%B4%9D%E5%9B%9E%E5%AE%B6/Baby%20back%20home


    欢迎添加环信冬冬微信,联系该项目作者

    收起阅读 »

    OC 对象、位域、isa

    一、对象的本质1.1 clang1.1.1clang 概述Clang是一个C语言、C++、Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexp...
    继续阅读 »

    一、对象的本质

    1.1 clang

    1.1.1clang 概述

    Clang是一个C语言C++Objective-C语言的轻量级编译器。源代码发布于BSD协议下。 Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

    Clang是一个由Apple主导编写,基于LLVMC/C++/Objective-C编译器。
    它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC

    1.1.2 clang与xcrun命令

    1.1.2.1 clang

    把目标文件编译成c++文件,最简单的方式:

    clang -rewrite-objc main.m -o main.cpp

    如果包含其它SDK,比如UIKit则需要指定isysroot

    clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m -o main.cpp

    • 如果找不到Foundation则需要排查clang版本设置是否正确。使用which clang可以直接查看路径。有些公司会使用clang-format来进行代码格式化,需要排查环境变量中是否导出了相关路径(如果导出先屏蔽掉)。正常路径为/usr/bin/clang
    • isysroot也可以导出环境变量进行配置方便使用。

    1.1.2.2 xcrun(推荐)

    xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了 一些封装,比clang更好用。


    模拟器命令:

    xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64simulator.cpp

    真机命令:

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

    1.2 对象c++代码分析

    main.m文件如下,直接生成对应的.cpp文件对HotpotCat进行分析。

    #import <Foundation/Foundation.h>

    @interface HotpotCat : NSObject

    @end

    @implementation HotpotCat

    @end

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
    }

    1.2.1 对象在底层是结构体

    直接搜索HotpotCat

    可以看到生成了HotpotCat_IMPL是一个结构体,那么HotpotCat_IMPL就是HotpotCat的底层实现么?对HotpotCat增加属性hp_name:

    @property(nonatomic, copy) NSString *hp_name;

    重新生成.cpp文件:

    这也就验证了HotpotCat_IMPL就是HotpotCat的底层实现,那么说明: 对象在底层的本质就是结构体

    HotpotCat_IMPL结构体中又嵌套了NSObject_IMPL结构体,这可以理解为继承。
    NSObject_IMPL定义如下:


    struct NSObject_IMPL {
    Class isa;
    };

    所以NSObject_IVARS就是成员变量isa

    1.2.2 objc_object & objc_class

    HotpotCat_IMPL上面有如下代码:

    typedef struct objc_object HotpotCat;

    为什么HotpotCatobjc_object类型?这是因为NSObject的底层实现就是objc_object

    同样的Class定义如下:

    typedef struct objc_class *Class;

    objc_class类型的结构体指针。

    同样可以看到idobjc_object结构体类型指针。

    typedef struct objc_object *id;

    这也就是id声明的时候不需要*的原因。


    1.2.3 setter & getter

    .cpp文件中有以下代码:


    // @property(nonatomic, copy) NSString *hp_name;


    /* @end */


    // @implementation HotpotCat

    //这里是setter和getter 参数self _cmd 隐藏参数
    static NSString * _I_HotpotCat_hp_name(HotpotCat * self, SEL _cmd) {
    //return self + 成员变量偏移
    return (*(NSString **)((char *)self + OBJC_IVAR_$_HotpotCat$_hp_name));
    }
    extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

    static void _I_HotpotCat_setHp_name_(HotpotCat * self, SEL _cmd, NSString *hp_name) {
    objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct HotpotCat, _hp_name), (id)hp_name, 0, 1);
    }
    // @end
    • 根据系统默认注释和函数名称确认这里是hp_namesettergetter方法。
    • getter方法返回hp_name是通过self + 成员变量偏移获取的。getter同理。

    二、位域

    struct Direction {
    BOOL left;
    BOOL right;
    BOOL front;
    BOOL back;
    };

    上面是一个记录方向的结构体。这个结构体占用4字节32位:00000000 00000000 00000000 00000000。但是对于BOOL值只有两种情况YES/NO。那么如果能用40000来代替前后左右,就只需要0.5个字节就能表示这个数据结构了(虽然只需要0.5字节,但是数据单元最小为1字节)。那么Direction的实现显然浪费了3倍的空间。有什么优化方式呢?位域

    2.1 结构体位域

    修改DirectionHPDirection:


    struct HPDirection {
    BOOL left : 1;
    BOOL right : 1;
    BOOL front : 1;
    BOOL back : 1;
    };

    格式为:数据类型 位域名称:位域长度

    验证:

    struct Direction dir;
    dir.left = YES;
    dir.right = YES;
    dir.front = YES;
    dir.back = YES;
    struct HPDirection hpDir;
    hpDir.left = YES;
    hpDir.right = YES;
    hpDir.front = YES;
    hpDir.back = YES;
    printf("\nDirection size:%zu\nHPDirection size:%zu\n",sizeof(dir),sizeof(hpDir));



    2.2 联合体

    2.2.1结构体&联合体对比


    //结构体联合体对比
    //共存
    struct HPStruct {
    char *name;
    int age;
    double height;
    };

    //互斥
    union HPUnion {
    char *name;
    int age;
    double height;
    };


    void testStructAndUnion() {
    struct HPStruct s;
    union HPUnion u;
    s.name = "HotpotCat";
    u.name = "HotpotCat";
    s.age = 18;
    u.age = 18;
    s.height = 180.0;
    u.height = 180.0;
    }
    分别定义了HPStruct结构体和HPUnion共用体,在整个赋值过程中变化如下:

    总结:

    • 结构体(struct)中所有变量是“共存”的。
      优点:“有容乃大”, 全面;
      缺点:struct内存空间的分配是粗放的,不管用不用全分配。
    • 联合体/共用体(union)中是各变量是“互斥”的。
      缺点:不够“包容”;
      优点:内存使用更为精细灵活,节省了内存空间。
    • 联合体在未进行赋值前数据成员会存在脏数据。

    2.2.2 联合体位域

    HPDirectionItem.h

    @interface HPDirectionItem : NSObject

    @property (nonatomic, assign) BOOL left;
    @property (nonatomic, assign) BOOL right;
    @property (nonatomic, assign) BOOL front;
    @property (nonatomic, assign) BOOL back;

    @end
    HPDirectionItem.m:

    #define HPDirectionLeftMask   (1 << 0)
    #define HPDirectionRightMask (1 << 1)
    #define HPDirectionFrontMask (1 << 2)
    #define HPDirectionBackMask (1 << 3)

    #import "HPDirectionItem.h"

    @interface HPDirectionItem () {
    //这里bits和struct用任一一个就可以,结构体相当于是对bits的解释。因为是共用体用同一块内存。
    union {
    char bits;
    //位域,这里是匿名结构体(anonymous struct)
    struct {
    char left : 1;
    char right : 1;
    char front : 1;
    char back : 1;
    };
    }_direction;
    }

    @end

    @implementation HPDirectionItem

    - (instancetype)init {
    self = [super init];
    if (self) {
    _direction.bits = 0b00000000;
    }
    return self;
    }

    - (void)setLeft:(BOOL)left {
    if (left) {
    _direction.bits |= HPDirectionLeftMask;
    } else {
    _direction.bits &= ~HPDirectionLeftMask;
    }
    }

    - (BOOL)left {
    return _direction.bits & HPDirectionLeftMask;
    }
    //……
    //其它方向设置同理
    //……
    @end

    • HPDirectionItem是一个方向类,类中有一个_direction的共用体。
    • _direction中有bitsanonymous struct,这里anonymous struct相当于是对bits的一个解释(因为是共用体,同一个字节内存。下面的调试截图很好的证明力这一点)。
    • 通过对bits位移操作来进行数据的存储,其实就相当于对结构体位域的操作。连这个可以互相操作。

    所以可以将settergetter通过结构体去操作,效果和操作bits相同:

    - (void)setLeft:(BOOL)left {

        _direction.left = left;
    }

    - (BOOL)left {
    return _direction.left;
    }

    当然也可以两者混用:

    - (void)setLeft:(BOOL)left {
    _direction.left = left;
    }

    - (BOOL)left {
    return _direction.bits & HPDirectionLeftMask;
    }

    根本上还是对同一块内存空间进行操作。

    调用:

    void testUnionBits() {
    HPDirectionItem *item = [HPDirectionItem alloc];
    item.left = 1;
    item.right = 1;
    item.front = 1;
    item.back = 1;
    item.right = 0;
    item.back = 0;
    NSLog(@"testUnionBits");
    }



    这样整个赋值流程就符合预期满足需求了。

    • 联合体位域作用:优化内存空间和访问速度。

    三、 isa

    alloc分析的文章中已经了解到执行完initIsa后将alloc开辟的内存与类进行了关联。在initIsa中首先创建了isa_t也就是isa,去掉方法后它的主要结构如下:

    union isa_t {
    //……
    uintptr_t bits;
    private:
    Class cls;
    public:
    #if defined(ISA_BITFIELD)
    struct {
    ISA_BITFIELD; // defined in isa.h
    };
    //……
    #endif
    //……
    };

    它是一个union,包含了bitscls(私有)和一个匿名结构体,所以这3个其实是一个内容,不同表现形式罢了。这个结构似曾相识,与2.2.2中联合体位域一样。不同的是isa_t占用8字节64位。

    没有关联类时isa分布(默认都是0,没有指向):





    bitscls分析起来比较困难,既然三者一样,那么isa_t的核心就是ISA_BITFIELD


    作者:HotPotCat
    链接:https://www.jianshu.com/p/84749f140139

    收起阅读 »

    objc_msgSend cache查找

    分析objc_msgSend中缓存的查找逻辑以及汇编代码是如何进入c/c++代码的。一、CacheLookup 查找缓存1.1 CacheLookup源码分析传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncache...
    继续阅读 »

    分析objc_msgSend中缓存的查找逻辑以及汇编代码是如何进入c/c++代码的。

    一、CacheLookup 查找缓存

    1.1 CacheLookup源码分析

    传递的参数是NORMAL, _objc_msgSend, __objc_msgSend_uncached


    //NORMAL, _objc_msgSend, __objc_msgSend_uncached

    .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    // requirements:
    // //缓存不存在返回NULL,x0设置为0
    // GETIMP:
    // The cache-miss is just returning NULL (setting x0 to 0)
    // 参数说明
    // NORMAL and LOOKUP:
    // - x0 contains the receiver
    // - x1 contains the selector
    // - x16 contains the isa
    // - other registers are set as per calling conventions
    //
    //调用过来的p16存储的是cls,将cls存储在x15.
    mov x15, x16 // stash the original isa
    //_objc_msgSend
    LLookupStart\Function:
    // p1 = SEL, p16 = isa
    //arm64 64 OSX/SIMULATOR
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //isa->cache,首地址也就是_bucketsAndMaybeMask
    ldr p10, [x16, #CACHE] // p10 = mask|buckets
    //lsr逻辑右移 p11 = _bucketsAndMaybeMask >> 48 也就是 mask
    lsr p11, p10, #48 // p11 = mask
    //p10 = _bucketsAndMaybeMask & 0xffffffffffff = buckets(保留后48位)
    and p10, p10, #0xffffffffffff // p10 = buckets
    //x12 = cmd & mask w1为第二个参数cmd(self,cmd...),w11也就是p11 也就是执行cache_hash。这里没有>>7位的操作
    and w12, w1, w11 // x12 = _cmd & mask
    //arm64 64 真机这里p11计算后是_bucketsAndMaybeMask
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
    //arm64 + iOS + !模拟器 + 非mac应用
    #if CONFIG_USE_PREOPT_CACHES
    //iphone 12以后指针验证
    #if __has_feature(ptrauth_calls)
    //tbnz 测试位不为0则跳转。与tbz对应。 p11 第0位不为0则跳转 LLookupPreopt\Function。
    tbnz p11, #0, LLookupPreopt\Function
    //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff // p10 = buckets
    #else
    //p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe = buckets
    and p10, p11, #0x0000fffffffffffe // p10 = buckets
    //p11 第0位不为0则跳转 LLookupPreopt\Function。
    tbnz p11, #0, LLookupPreopt\Function
    #endif
    //eor 逻辑异或(^) 格式为:EOR{S}{cond} Rd, Rn, Operand2
    //p12 = selector ^ (selector >> 7) select 右移7位&自己给到p12
    eor p12, p1, p1, LSR #7
    //p12 = p12 & (_bucketsAndMaybeMask >> 48) = index & mask值 = buckets中的下标
    and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else
    //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
    and p10, p11, #0x0000ffffffffffff // p10 = buckets
    //p12 = selector & (_bucketsAndMaybeMask >>48) = sel & mask = buckets中的下标
    and p12, p1, p11, LSR #48 // x12 = _cmd & mask
    #endif // CONFIG_USE_PREOPT_CACHES
    //arm64 32
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //后4位为mask前置0的个数的case
    ldr p11, [x16, #CACHE] // p11 = mask|buckets
    and p10, p11, #~0xf // p10 = buckets 相当于后4位置为0,取前32位
    and p11, p11, #0xf // p11 = maskShift 取的是后4位,为mask前置位的0的个数
    mov p12, #0xffff
    lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
    and p12, p1, p11 // x12 = _cmd & mask
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    //通过上面的计算 p10 = buckets,p11 = mask(arm64真机是_bucketsAndMaybeMask), p12 = index
    // p13(bucket_t) = buckets + 下标 << 4 PTRSHIFT arm64 为3. <<4 位为16字节 buckets + 下标 *16 = buckets + index *16 也就是直接平移到了第几个元素的地址。
    add p13, p10, p12, LSL #(1+PTRSHIFT)
    // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    //这里就直接遍历查找了,因为arm64下cache_next相当于遍历(这里只扫描了前面)
    // do {
    //p17 = imp, p9 = sel
    1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
    //sel - _cmd != 0 则跳转 3:,也就意味着没有找到就跳转到__objc_msgSend_uncached
    cmp p9, p1 // if (sel != _cmd) {
    b.ne 3f // scan more
    // } else {
    //找到则调用或者返回imp,Mode为 NORMAL
    2: CacheHit \Mode // hit: call or return imp 命中
    // }
    //__objc_msgSend_uncached
    //缓存中找不到方法就走__objc_msgSend_uncached逻辑了。
    //cbz 为0跳转 sel == nil 跳转 \MissLabelDynamic
    3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; 有空位没有找到说明没有缓存
    //bucket_t - buckets 由于是递减操作
    cmp p13, p10 // } while (bucket >= buckets) //⚠️ 这里一直是往前找,后面的元素在后面还有一次循环。
    //无符号大于等于 则跳转1:f b 分别代表front与back
    b.hs 1b

    //没有命中cache 查找 p13 = mask对应的元素,也就是倒数第二个
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    //p13 = buckets + (mask << 4) 平移找到对应mask的bucket_t。UXTW 将w11扩展为64位后左移4
    add p13, p10, w11, UXTW #(1+PTRSHIFT)
    // p13 = buckets + (mask << 1+PTRSHIFT)
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //p13 = buckets + (mask >> 44) 这里右移44位,少移动4位就不用再左移了。因为maskZeroBits的存在 就找到了mask对应元素的地址
    add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
    // p13 = buckets + (mask << 1+PTRSHIFT)
    // see comment about maskZeroBits
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    //p13 = buckets + (mask << 4) 找到对应mask的bucket_t。
    add p13, p10, p11, LSL #(1+PTRSHIFT)
    // p13 = buckets + (mask << 1+PTRSHIFT)
    #else
    #error Unsupported cache mask storage for ARM64.
    #endif
    //p12 = buckets + (p12<<4) index对应的bucket_t
    add p12, p10, p12, LSL #(1+PTRSHIFT)
    // p12 = first probed bucket

    //之前已经往前查找过了,这里从后往index查找
    // do {
    //p17 = imp p9 = sel
    4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
    //sel - _cmd
    cmp p9, p1 // if (sel == _cmd)
    //sel == _cmd跳转CacheHit
    b.eq 2b // goto hit
    //sel != nil
    cmp p9, #0 // } while (sel != 0 &&
    //
    ccmp p13, p12, #0, ne // bucket > first_probed)
    //有值跳转4:
    b.hi 4b

    LLookupEnd\Function:
    LLookupRecover\Function:
    //仍然没有找到缓存,缓存彻底不存在 __objc_msgSend_uncached()
    b \MissLabelDynamic

    核心逻辑:

    • 根据不同架构找到bucketssel对应的indexp10 = buckets,p11 = mask / _bucketsAndMaybeMask(arm64_64 是 _bucketsAndMaybeMask),p12 = index
      • arm64_64的情况下如果_bucketsAndMaybeMask0位为1则执行LLookupPreopt\Function
    • p13 = buckets + index << 4找到cls对应的buckets地址,地址平移找到对应bucket_t
    • do-while循环扫描buckets[index]的前半部分(后半部分逻辑不在这里)。
      • 如果存在sel为空,则说明是没有缓存的,就直接__objc_msgSend_uncached()
      • 命中直接CacheHit \Mode,这里ModeNORMAL
    • 平移获得p13 = buckets[mask]对应的元素,也就是倒数第二个(倒数第一个为buckets地址)。
    • p13 = buckets + mask << 4找到mask对应的buckets地址,地址平移找到对应bucket_t
    • do-while循环扫描buckets[mask]的前面元素,直到index(不包含index)。
      • 命中CacheHit \Mode
      • 如果存在sel为空,则说明是没有缓存的,就直接结束循环。
    • 最终仍然没有找到则执行__objc_msgSend_uncached()
    1. CACHEcache_t相对isa的偏移。 #define CACHE (2 * __SIZEOF_POINTER__)
    2. maskZeroBits始终是40p13 = buckets + (_bucketsAndMaybeMask >> 44)右移44位后就不用再<<4找到对应bucket_t的地址了。这也是maskZeroBitsarm64_64下存在的意义。
    3. f b 分别代表frontback,往下往上的意思。

    1.2 CacheLookup 伪代码实现


    //NORMAL, _objc_msgSend, __objc_msgSend_uncached

    void CacheLookup(Mode,Function,MissLabelDynamic,MissLabelConstant) {
    //1. 根据架构不同集算sel在buckets中的index
    if (arm64_64 && OSX/SIMULATOR) {
    p10 = isa->cache //_bucketsAndMaybeMask
    p11 = _bucketsAndMaybeMask >> 48//mask
    p10 = _bucketsAndMaybeMask & 0xffffffffffff//buckets
    x12 = sel & mask //index 也就是执行cache_hash
    } else if (arm64_64) {//真机 //这个分支下没有计算mask
    p11 = isa->cache //_bucketsAndMaybeMask
    if (arm64 + iOS + !模拟器 + 非mac应用) {
    if (开启指针验证 ) {
    if (_bucketsAndMaybeMask 第0位 != 0) {
    goto LLookupPreopt\Function
    } else
    {
    p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff//buckets
    }
    } else {
    p10 = _bucketsAndMaybeMask & 0x0000fffffffffffe //buckets
    if (_bucketsAndMaybeMask 第0位 != 0) {
    goto LLookupPreopt\Function
    }
    }
    //计算index
    p12 = selector ^ (selector >> 7)
    p12 = p12 & (_bucketsAndMaybeMask & 48) = p12 & mask//index
    } else
    {
    p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff //buckets
    p12 = selector & (_bucketsAndMaybeMask >>48) //index
    }
    } else if (arm64_32) {
    p11 = _bucketsAndMaybeMask
    p10 = _bucketsAndMaybeMask &(~0xf//buckets 相当于后4位置为0,取前32位
    p11 = _bucketsAndMaybeMask & 0xf //mask前置位0的个数
    p11 = 0xffff >> p11 //获取到mask的值
    x12 = selector & mask //index
    } else {
    #error Unsupported cache mask storage for ARM64.
    }

    //通过上面的计算 p10 = buckets,p11 = mask/_bucketsAndMaybeMask, p12 = index
    p13 = buckets + index << 4 //找到cls对应的buckets地址。地址平移找到对应bucket_t。

    //2.找缓存(这里只扫描了前面)
    do {
    p13 = *bucket-- //赋值后指向前一个bucket
    p17 = bucket.imp
    p9 = bucket.sel
    if (p9 != selector) {
    if (p9 == 0) {//说明没有缓存
    __objc_msgSend_uncached()
    }
    } else {//缓存命中,走命中逻辑 call or return imp
    CacheHit \Mode
    }
    } while(bucket >= buckets) //buckets是首地址,bucket是index对应的buckct往前移动

    //查找完后还没有缓存?
    //查找 p13 = mask对应的元素,也就是倒数第二个
    if (arm64_64 && OSX/SIMULATOR) {
    p13 = buckets + (mask << 4)
    } else if (arm64_64) {//真机
    p13 = buckets + (_bucketsAndMaybeMask >> 44)//这里右移44位,少移动4位就不用再左移了。这里就找到了对应index的bucket_t。
    } else if (arm64_32) {
    p13 = buckets + (mask << 4)
    } else {
    #error Unsupported cache mask storage for ARM64.
    }

    //index的bucket_t 从mask对应的buckets开始再往前找
    p12 = buckets + (index<<4)
    do {
    p17 = imp;
    p9 = sel;
    *p13--;
    if (p9 == selector) {//命中
    CacheHit \Mode
    }
    } while (p9 != nil && bucket > p12)//从后往前 p9位nil则证明没有存,也就不存在缓存了。

    //仍然没有找到缓存,缓存彻底不存在。
    __objc_msgSend_uncached()
    }

    二、LLookupPreopt\Function

    arm64_64真机的情况下,如果_bucketsAndMaybeMask的第0位为1则会执行LLookupPreopt\Function的逻辑。简单看了下汇编发现与cache_t 中的_originalPreoptCache有关。

    2.1 LLookupPreopt\Function 源码分析

    LLookupPreopt\Function:

    #if __has_feature(ptrauth_calls)
    //p10 = _bucketsAndMaybeMask & 0x007ffffffffffffe = buckets
    and p10, p11, #0x007ffffffffffffe // p10 = x
    //buckets x16为cls 验证
    autdb x10, x16 // auth as early as possible
    #endif

    // x12 = (_cmd - first_shared_cache_sel)
    //(_cmd >> 12 + PAGE) << 12 + PAGEOFF 第一个sel
    adrp x9, _MagicSelRef@PAGE
    ldr p9, [x9, _MagicSelRef@PAGEOFF]
    //差值index
    sub p12, p1, p9

    // w9 = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
    #if __has_feature(ptrauth_calls)
    // bits 63..60 of x11 are the number of bits in hash_mask
    // bits 59..55 of x11 is hash_shift

    // 取到 hash_shift...
    lsr x17, x11, #55 // w17 = (hash_shift, ...)
    //w9 = index >> hash_shift
    lsr w9, w12, w17 // >>= shift
    //x17 = _bucketsAndMaybeMask >>60 //mask_bits
    lsr x17, x11, #60 // w17 = mask_bits
    mov x11, #0x7fff
    //x11 = 0x7fff >> mask_bits //mask
    lsr x11, x11, x17 // p11 = mask (0x7fff >> mask_bits)
    //x9 = x9 & mask
    and x9, x9, x11 // &= mask
    #else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    lsr x17, x11, #48 // w17 = (hash_shift, hash_mask)
    lsr w9, w12, w17 // >>= shift
    and x9, x9, x11, LSR #53 // &= mask
    #endif
    //x17 = el_offs | (imp_offs << 32)
    ldr x17, [x10, x9, LSL #3] // x17 == sel_offs | (imp_offs << 32)
    // cmp x12 x17 是否找到sel
    cmp x12, w17, uxtw

    .if \Mode == GETIMP
    b.ne \MissLabelConstant // cache miss
    //imp = isa - (sel_offs >> 32)
    sub x0, x16, x17, LSR #32 // imp = isa - imp_offs
    //注册imp
    SignAsImp x0
    ret
    .else
    b.ne 5f // cache miss
    //imp(x17) = (isa - sel_offs>> 32)
    sub x17, x16, x17, LSR #32 // imp = isa - imp_offs
    .if \Mode == NORMAL
    //跳转imp
    br x17
    .elseif \Mode == LOOKUP
    //x16 = isa | 3 //这里为或的意思
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    //注册imp
    SignAsImp x17
    ret
    .else
    .abort unhandled mode \Mode
    .endif
    //x9 = buckets-1
    5: ldursw x9, [x10, #-8] // offset -8 is the fallback offset
    //计算回调isa x16 = x16 + x9
    add x16, x16, x9 // compute the fallback isa
    //使用新isa重新查找缓存
    b LLookupStart\Function // lookup again with a new isa
    .endif
    • 找到imp就跳转/返回。
    • 没有找到返回下一个isa重新CacheLookup

    ⚠️@TODO 真机调试的时候进不到这块流程,这块分析的还不是很透彻,后面再补充。

    三、CacheHit

    在查找缓存命中后会执行CacheHit

    3.1 CacheHit源码分析

    #define NORMAL 0

    #define GETIMP 1
    #define LOOKUP 2

    // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
    .macro CacheHit
    //这里传入的为NORMAL
    .if $0 == NORMAL
    //调用imp TailCallCachedImp(imp,buckets,sel,isa)
    TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
    .elseif $0 == GETIMP
    //返回imp
    mov p0, p17
    //imp == nil跳转9:
    cbz p0, 9f // don't ptrauth a nil imp
    //有imp执行AuthAndResignAsIMP(imp,buckets,sel,isa)最后给到x0返回。
    AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
    9: ret // return IMP
    .elseif $0 == LOOKUP
    // No nil check for ptrauth: the caller would crash anyway when they
    // jump to a nil IMP. We don't care if that jump also fails ptrauth.
    //找imp(imp,buckets,sel,isa)
    AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
    //isa与x15比较
    cmp x16, x15
    //cinc如果相等 就将x16+1,否则就设成0.
    cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
    ret // return imp via x17
    .else
    .abort oops
    .endif
    .endmacro
    • 这里其实走的是NORMAL逻辑,NORMALcase直接验证并且跳转imp
    • TailCallCachedImp内部执行的是imp^cls,对imp进行了解码。
    • GETIMP返回imp
    • LOOKUP查找注册imp并返回。

    3.1 CacheHit伪代码实现

    //x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa

    void CacheHit(Mode) {
    if (Mode == NORMAL) {
    //imp = imp^cls 解码
    TailCallCachedImp x17, x10, x1, x16 // 解码跳转imp
    } else if (Mode == GETIMP) {
    p0 = IMP
    if (p0 == nil) {
    return
    } else {
    AuthAndResignAsIMP(imp,buckets,sel,isa)//resign cached imp as IMP
    }
    } else if (Mode == LOOKUP) {
    AuthAndResignAsIMP(x17, buckets, sel, isa)//resign cached imp as IMP
    if (isa == x15) {
    x16 += 1
    } else {
    x16 = 0
    }
    } else {
    .abort oops//报错
    }
    }

    四、__objc_msgSend_uncached

    在缓存没有命中的情况下会走到__objc_msgSend_uncached()的逻辑:

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p15 is the class to search
    //查找imp
    MethodTableLookup
    //跳转imp
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached
    • MethodTableLookup查找imp
    • TailCallFunctionPointer跳转imp

    MethodTableLookup

    .macro MethodTableLookup

        
    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    //x2 = cls
    mov x2, x16
    //x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver
    //_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    mov x3, #3
    bl _lookUpImpOrForward

    // IMP in x0
    mov x17, x0

    RESTORE_REGS MSGSEND

    .endmacro
    • 调用_lookUpImpOrForward查找imp。这里就调用到了c/c++的代码了:
    • IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

    最终会调用_lookUpImpOrForward进入c/c++环境逻辑。

    对于架构的一些理解
    LP64 //64位
    x86_64 // interl 64位
    i386 // intel 32位
    arm // arm指令 32 位
    arm64 //arm64指令
    arm64 && LP64 //arm64 64位
    arm64 && !LP64 //arm64 32 位

    五、 objc_msgSend流程图

    objc_msgSend流程图

    总结

    • 判断receiver是否存在。
    • 通过isa获取cls
    • cls内存平移0x10获取cache也就是_bucketsAndMaybeMask
    • 通过buckets & bucketsMask获取buckets`地址。
    • 通过bucketsMask >> maskShift获取mask
    • 通过sel & mask获取第一次查找的index
    • buckets + index << 4找到index对应的地址。
    • do-while循环判断找缓存,这次从[index~0]查找imp
    • 取到buckets[mask]继续do-while循环,从[mask~index)查找imp。两次查找过程中如果有sel为空则会结束查找。走__objc_msgSend_uncached的逻辑。
    • 找到imp就解码跳转imp


    作者:HotPotCat
    链接:https://www.jianshu.com/p/c29c07a1e93d

    收起阅读 »