注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

APP路由框架与组件化简析

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。路由的概...
继续阅读 »

前端开发经常遇到一个词:路由,在Android APP开发中,路由还经常和组件化开发强关联在一起,那么到底什么是路由,一个路由框架到底应该具备什么功能,实现原理是什么样的?路由是否是APP的强需求呢?与组件化到底什么关系,本文就简单分析下如上几个问题。

路由的概念

路由这个词本身应该是互联网协议中的一个词,维基百科对此的解释如下:

路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。

个人理解,在前端开发中,路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme,并生成路由表,然后,能够根据外部输入字符串在路由表中匹配到对应的页面或者服务,进行跳转或者调用,并提供会获取返回值等,示意如下

image.png

所以一个基本路由框架要具备如下能力:

    1. APP路由的扫描及注册逻辑
    1. 路由跳转target页面能力
    1. 路由调用target服务能力

APP中,在进行页面路由的时候,经常需要判断是否登录等一些额外鉴权逻辑所以,还需要提供拦截逻辑等,比如:登陆。

三方路由框架是否是APP强需求

答案:不是,系统原生提供路由能力,但功能较少,稍微大规模的APP都采用三方路由框架。

Android系统本身提供页面跳转能力:如startActivity,对于工具类APP,或单机类APP,这种方式已经完全够用,完全不需要专门的路由框架,那为什么很多APP还是采用路由框架呢?这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP,无论是从APP功能还是从其研发团队的规模上来说都很庞大,不同的业务之间也经常是不同的团队在维护,采用组件化的开发方式,最终集成到一个APK中。多团队之间经常会涉及业务间的交互,比如从电影票业务跳转到美食业务,但是两个业务是两个独立的研发团队,代码实现上是完全隔离的,那如何进行通信呢?首先想到的是代码上引入,但是这样会打破了低耦合的初衷,可能还会引入各种问题。例如,部分业务是外包团队来做,这就牵扯到代码安全问题,所以还是希望通过一种类似黑盒的方式,调用目标业务,这就需要中转路由支持,所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系,比如跳转商详的时候,不希望知道是哪个Activity来实现,只需要一个字符串映射过去即可,这对于H5、或者后端开发来处理跳转的时候,就非常标准。

原生路由的限制:功能单一,扩展灵活性差,不易协同

传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说,传统的路由有什么缺点:startActivity有两种用法,一种是显示的,一种是隐式的,显示调用如下:


import com.snail.activityforresultexample.test.SecondActivity;

public class MainActivity extends AppCompatActivity {

void jumpSecondActivityUseClassName(){

Intent intent =new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}

显示调用的缺点很明显,那就是必须要强依赖目标Activity的类实现,有些场景,尤其是大型APP组件化开发时候,有些业务逻辑出于安全考虑,并不想被源码或aar依赖,这时显式依赖的方式就无法走通。再来看看隐式调用方法。

第一步:manifest中配置activity的intent-filter,至少要配置一个action
















第二步:调用

void jumpSecondActivityUseFilter() {
Intent intent = new Intent();
intent.setAction("com.snail.activityforresultexample.SecondActivity");
startActivity(intent);
}

如果牵扯到数据传递写法上会更复杂一些,隐式调用的缺点有如下几点:

  • 首先manifest中定义复杂,相对应的会导致暴露的协议变的复杂,不易维护扩展。
  • 其次,不同Activity都要不同的action配置,每次增减修改Activity都会很麻烦,对比开发者非常不友好,增加了协作难度。
  • 最后,Activity的export属性并不建议都设置成True,这是降低风险的一种方式,一般都是收归到一个Activity,DeeplinkActivitiy统一处理跳转,这种场景下,DeeplinkActivitiy就兼具路由功能,隐式调用的场景下,新Activitiy的增减势必每次都要调整路由表,这会导致开发效率降低,风险增加。

可以看到系统原生的路由框架,并没太多考虑团队协同的开发模式,多限定在一个模块内部多个业务间直接相互引用,基本都要代码级依赖,对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制,可以认为三方路由框架完全是为了团队协同而创建的

APP三方路由框架需具备的能力

目前市面上大部分的路由框架都能搞定上述问题,简单整理下现在三方路由的能力,可归纳如下:

  • 路由表生成能力:业务组件**[UI业务及服务]**自动扫描及注册逻辑,需要扩展性好,无需入侵原有代码逻辑
  • scheme与业务映射逻辑 :无需依赖具体实现,做到代码隔离
  • 基础路由跳转能力 :页面跳转能力的支持
  • 服务类组件的支持 :如去某个服务组件获取一些配置等
  • [扩展]路由拦截逻辑:比如登陆,统一鉴权
  • 可定制的降级逻辑:找不到组件时的兜底

可以看下一个典型的Arouter用法,第一步:对新增页面添加Router Scheme 声明,

	@Route(path = "/test/activity2")
public class Test2Activity extends AppCompatActivity {
...
}

build阶段会根据注解搜集路由scheme,生成路由表。第二步使用

        ARouter.getInstance()
.build("/test/activity2")
.navigation(this);

如上,在ARouter框架下,仅需要字符串scheme,无需依赖任何Test2Activity就可实现路由跳转。

APP路由框架的实现

路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系,也就是路由表,并能根据路由表路由到对应组件的能力。其实分两部分,第一部分路由表的生成,第二部分,路由表的查询

路由表的自动生成

生成路由表的方式有很多,最简单的就是维护一个公共文件或者类,里面映射好每个实现组件跟scheme,

image.png

不过,这种做法缺点很明显:每次增删修改都要都要修改这个表,对于协同非常不友好,不符合解决协同问题的初衷。不过,最终的路由表倒是都是这条路,就是将所有的Scheme搜集到一个对象中,只是实现方式的差别,目前几乎所有的三方路由框架都是借助注解+APT[Annotation Processing Tool]工具+AOP(Aspect-Oriented Programming,面向切面编程)来实现的,基本流程如下:

image.png

其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOP(Aspect-Oriented Programming,面向切面编程)。APT常用的有JavaPoet,主要是遍历所有类,找到被注解的Java类,然后聚合生成路由表,由于组件可能有很多,路由表可能也有也有多个,之后,这些生成的辅助类会跟源码一并被编译成class文件,之后利用AOP技术【如ASM或者JavaAssist】,扫描这些生成的class,聚合路由表,并填充到之前的占位方法中,完成自动注册的逻辑。

JavaPoet如何搜集并生成路由表集合?

以ARouter框架为例,先定义Router框架需要的注解如:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

/**
* Path of route
*/

String path();

该注解用于标注需要路由的组件,用法如下:

@Route(path = "/test/activity1", name = "测试用 Activity")
public class Test1Activity extends BaseActivity {
@Autowired
int age = 10;

之后利用APT扫描所有被注解的类,生成路由表,实现参考如下:

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {

Set routeElements = roundEnv.getElementsAnnotatedWith(Route.class);

this.parseRoutes(routeElements);
...
return false;
}


private void parseRoutes(Set routeElements) throws IOException {
...
// Generate groups
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);

产物如下:包含路由表,及局部注册入口。

image.png

自动注册:ASM搜集上述路由表并聚合插入Init代码区

为了能够插入到Init代码区,首先需要预留一个位置,一般定义一个空函数,以待后续填充:

	public class RouterInitializer {

public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) {
...
loadRouterTables();
}
//自动注册代码
public static void loadRouterTables() {

}
}

首先利用AOP工具,遍历上述APT中间产物,聚合路由表,并注册到预留初始化位置,遍历的过程牵扯是gradle transform的过程,

  • 搜集目标,聚合路由表

      /**扫描jar*/
    fun scanJar(jarFile: File, dest: File?) {

    val file = JarFile(jarFile)
    var enumeration = file.entries()
    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    if (jarEntry.name.endsWith("XXRouterTable.class")) {
    val inputStream = file.getInputStream(jarEntry)
    val classReader = ClassReader(inputStream)
    if (Arrays.toString(classReader.interfaces)
    .contains("IHTRouterTBCollect")
    ) {
    tableList.add(
    Pair(
    classReader.className,
    dest?.absolutePath
    )
    )
    }
    inputStream.close()
    } else if (jarEntry.name.endsWith("HTRouterInitializer.class")) {
    registerInitClass = dest
    }
    }
    file.close()
    }

  • 对目标Class注入路由表初始化代码

      fun asmInsertMethod(originFile: File?) {

    val optJar = File(originFile?.parent, originFile?.name + ".opt")
    if (optJar.exists())
    optJar.delete()
    val jarFile = JarFile(originFile)
    val enumeration = jarFile.entries()
    val jarOutputStream = JarOutputStream(FileOutputStream(optJar))

    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    val entryName = jarEntry.getName()
    val zipEntry = ZipEntry(entryName)
    val inputStream = jarFile.getInputStream(jarEntry)
    //插桩class
    if (entryName.endsWith("RouterInitializer.class")) {
    //class文件处理
    jarOutputStream.putNextEntry(zipEntry)
    val classReader = ClassReader(IOUtils.toByteArray(inputStream))
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    val cv = RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList)
    classReader.accept(cv, EXPAND_FRAMES)
    val code = classWriter.toByteArray()
    jarOutputStream.write(code)
    } else {
    jarOutputStream.putNextEntry(zipEntry)
    jarOutputStream.write(IOUtils.toByteArray(inputStream))
    }
    jarOutputStream.closeEntry()
    }
    //结束
    jarOutputStream.close()
    jarFile.close()
    if (originFile?.exists() == true) {
    Files.delete(originFile.toPath())
    }
    optJar.renameTo(originFile)
    }

最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码:

 public static void loadRouterTables() {


register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin");
register("com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi");
register("com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava");
...
}

如此就完成了路由表的搜集与注册,大概的流程就是如此。当然对于支持服务、Fragment等略有不同,但大体类似。

Router框架对服务类组件的支持

通过路由的方式获取服务属于APP路由比较独特的能力,比如有个用户中心的组件,我们可以通过路由的方式去查询用户是否处于登陆状态,这种就不是狭义上的页面路由的概念,通过一串字符串如何查到对应的组件并调用其方法呢?这种的实现方式也有多种,每种实现方式都有自己的优劣。

  • 一种是可以将服务抽象成接口,沉到底层,上层实现通过路由方式映射对象
  • 一种是将实现方法直接通过路由方式映射

先看第一种,这种事Arouter的实现方式,它的优点是所有对外暴露的服务都暴露接口类【沉到底层】,这对于外部的调用方,也就是服务使用方非常友好,示例如下:

先定义抽象服务,并沉到底层

image.png

public interface HelloService extends IProvider {
void sayHello(String name);
}

实现服务,并通过Router注解标记

@Route(path = "/yourservicegroupname/hello")
public class HelloServiceImpl implements HelloService {
Context mContext;

@Override
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}

使用:利用Router加scheme获取服务实例,并映射成抽象类,然后直接调用方法。

  ((HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation()).sayHello("mike");

这种实现方式对于使用方其实是很方便的,尤其是一个服务有多个可操作方法的时候,但是缺点是扩展性,如果想要扩展方法,就要改动底层库。

再看第二种:将实现方法直接通过路由方式映射

服务的调用都要落到方法上,参考页面路由,也可以支持方法路由,两者并列关系,所以组要增加一个方法路由表,实现原理与Page路由类似,跟上面的Arouter对比,不用定义抽象层,直接定义实现即可:

定义Method的Router

	public class HelloService {


@MethodRouter(url = {"arouter://sayhello"})
public void sayHello(String name) {
Toast.makeText(mContext, "Hello " + name, Toast.LENGTH_SHORT).show();
}

使用即可

 RouterCall.callMethod("arouter://sayhello?name=hello");

上述的缺点就是对于外部调用有些复杂,尤其是处理参数的时候,需要严格按照协议来处理,优点是,没有抽象层,如果需要扩展服务方法,不需要改动底层。

上述两种方式各有优劣,不过,如果从左服务组件的初衷出发,第一种比较好:对于调用方比较友好。另外对于CallBack的支持,Arouter的处理方式可能也会更方便一些,可以比较方便的交给服务方定义。如果是第二种,服务直接通过路由映射的方式,处理起来就比较麻烦,尤其是Callback中的参数,可能要统一封装成JSON并维护解析的协议,这样处理起来,可能不是很好。

路由表的匹配

路由表的匹配比较简单,就是在全局Map中根据String输入,匹配到目标组件,然后依赖反射等常用操作,定位到目标。

组件化与路由的关系

组件化是一种开发集成模式,更像一种开发规范,更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件,这些组件之间可能是不同的团队,处于不同的目的在各自维护,甚至是需要代码隔离,如果牵扯到组件间的调用与通信,就不可避免的借助路由,因为实现隔离的,只能采用通用字符串scheme进行通信,这就是路由的功能范畴。

组件化需要路由支撑的根本原因:组件间代码实现的隔离

总结

  • 路由不是一个APP的必备功能,但是大型跨团队的APP基本都需要
  • 路由框架的基本能力:路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能
  • 组件化与路由的关系:组件化的代码隔离导致路由框架成为必须
收起阅读 »

AndroidRoom库基础入门

一、前言     Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充...
继续阅读 »


一、前言


    Room 是 Android Jetpack 的一部分。在 Android 中数据库是SQLite数据库,Room 就是在SQLite上面提供了一个抽象层,通过 Room 既能流畅地访问数据库,又能充分展示 SQLite 数据库的强大功能。Room 主要有以下几大优点:



  • 在编译时校验 SQL 语句;

  • 易用的注解减少重复和易错的模板代码;

  • 简化的数据库迁移路径。


    正是 Room 有以上的优点,所以建议使用 Room 访问数据库。


二、Room 主要组件


    Room 主要组件有三个:



  • 数据库类(RoomDatabase):拥有数据库,并作为应用底层持久性数据的主要访问接入点。

  • 数据实体类(Entity):表示应用数据库中的表。

  • 数据访问对象(DAO):提供方法使得应用能够在数据库中查询、更新、插入以及删除数据。


    应用从数据库类获取一个与之相关联的数据访问对象(DAO)。应用可以通过这个数据访问对象(DAO)在数据库中检索数据,并以相关联的数据实体对象呈现结果;应用也可以使用对的数据实体类对象,更新数据库对应表中的行(或者插入新行)。应用对数据库的操作完全通过 Room 这个抽象层实现,无需直接操作 SQLite数据库。下图就是 Room 各个组件之间的关系图:


Room组件关系图


三、Room 基础入门


    大致了解了 Room 的工作原理之后,下面我们就来介绍一下 Room 的使用入门。


3.1 引入 Room 库到项目


引入 Room 库到项目,在项目程序模块下的 build.gradle 文件的 dependencies


// Kotlin 开发环境,需要引入 kotlin-kapt 插件
apply plugin: 'kotlin-kapt'

// .........

dependencies {
// other dependecies

def room_version = "2.3.0"
implementation("androidx.room:room-runtime:$room_version")
// 使用 Kotlin 注解处理工具(kapt,如果项目使用Kotlin语言开发,这个必须引入,并且需要引入 kotlin-kapt 插件
kapt("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbolic Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")

// 可选 - 为 Room 添加 Kotlin 扩展和协程支持
implementation("androidx.room:room-ktx:$room_version")

// 可选 - 为 Room 添加 RxJava2 支持
implementation "androidx.room:room-rxjava2:$room_version"

// 可选 - 为 Room 添加 RxJava3 支持
implementation "androidx.room:room-rxjava3:$room_version"

// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"

// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}


注意事项:如果项目是用 kotlin 语言开发,一定要引入 kotlin 注解处理工具,并且在 build.gradle 中添加 kitlin-kapt插件(apply plugin: 'kotlin-kapt'),否则应用运行会抛出 xx.AppDatabase. AppDatabase_Impl does not exist 异常。



3.2 Room 使用示例


    使用 Room 访问数据库,需要首先定义 Room 的三个组件,然后通过数据访问对象实例访问数据。


3.2.1 定义数据实体类


    数据实体类对应数据库中的表,实体类的字段对应表中的列。定义 Room 数据实体类,使用 data class 关键字,并使用 @Entity 注解标注。更多关于数据实体类相关注解(包括属性相关注解),请参考: Android Room 数据实体类详解。如下代码所示:


@Entity
class User(@PrimaryKey val uid: Int, @ColumnInfo() val name: String, @ColumnInfo val age: Int)


注意事项:默认情况下,Room 会根据实体类的类为表名(在数据库中表名其实不区分大小写),开发者也可以在 @Entity 注解通过 tableName 参数指定表名。



3.3.2 定义数据访问对象(DAO)


    数据访问对象是访问数据库的桥梁,通过 DAO 访问数据,查询或者更新数据库中的数据(数据实体类是媒介)。数据访问对象(DAO)是一个接口,定义时添加 @Dao 注解标注,接口中的每一个成员方法表示一个操作,成员方法使用注解标示操作类型。更多关于数据访问对象(DAO)和数据操作类型注解,请参考:Android Room 数据访问对象详解。以下是简单的 DAO 示例代码:


@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>

@Query("SELECT * FROM user WHERE name LIKE :name")
fun findByName(name: String): List<User>

@Insert
fun insertAll(vararg users: User)

@Delete
fun delete(user: User)
}


注意事项:
1. 数据访问对象是接口类型,成员方法是没有方法体的,成员方法必须使用注解标示操作类型;
2. 数据库实体类成员方法中的 SQL 语句,在编译是会检查语法是否正确。



3.3.3 定义数据库类


    数据库是存储数据的地方,使用 Room 定义数据库时,声明一个抽象类(abstract class),并用 @Database 注解标示,在 @Database 注解中使用 entities 参数指定数据库关联的数据实体类列表,使用 version 参数指定数据的版本。数据库类中包含获取数据访问实体类对象的抽象方法,更多关于数据库相关内容,请参考:Android Room 数据库详解,以下是简单的数据类定义。


@Database(entities = [User::class], version = 1)
abstract class AppDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
}


注意事项:
1. 数据库类是一个抽象类,他的成员方法是抽象方法;
2. 定义数据库类时必须指定关联的数据实体类列表,这样数据库类才知道需要创建那些表;
3. 数据的版本号,如果数据库的表构造有变动时,需要升级版本号,这样数据库才会更新表结构(如修改表字段、新增表等,跟直接使用 SQLite 接口使用 SQLiteDatabase 类一样),但是数据库的升级并不是修改版本号那么简单,还需要处理数据库升级过程中需要修改的地方,更多详情请参考:Android Room 数据库升级



3.3.4 创建数据库实例


    定义好数据实体类、数据访问对象(DAO)和数据类之后,便可以创建数据库实例。使用 Room.databaseBuilder().build() 创建一个数据库实体类,Room 会根据定义的数据实体类、数据库访问对象和数据库类,以及他们定义时指定的对应关系,自动创建数据库和对应的表关系。如以下示例代码所示:


val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "app_db").build()


注意事项:
1. 每一个 RoomDatabase 实例都是非常耗费资源的,如果你的应用是单个进程中运行,那么在实例 RoomDatabase 时请遵循单例设计模式,在单个进程中几乎不需要访问多个 RoomDatabase 实例。
2. 如果你的应用在多个进程中运行(比如:远程服务(RemoteService)),在构建 RoomDatabase 的构建器中调用 Room.databaseBuilder().enableMultiInstanceInvalidation() 方法,这样一来,在每个进程中都有一个 RoomDatabase 实例,如果在某个进程中将共享的数据库文件失效,将会自动将这个失效自动同步给其他进程中的 RoomDatabase 实例。



3.3.5 从数据库实例中获取数据访问对象(DAO)实例


    在定义数据库类时,将数据访问对象(DAO)类与之相关联,定义抽象方法返回对应的数据库访问对象(DAO)实例。在数据库实例化过程中,Room 会自动生成对应的数据访问对象(DAO),只需要调用定义数据库类时定义的抽象方法,即可获取对应的数据访问对象(DAO)实例。如下示例所示:


val userDao = db.userDao()

3.3.6 通过数据访问对象(DAO)实例操作数据库


    获取到数据访问对象(DAO)实例,就可以调用数据库访问对象(DAO)类中定义的方法操作数据库了。如下示例所示:


Thread {
// 插入数据
userDao.insertAll(
User(1, "Student1", 18),
User(2, "Student2", 18),
User(3, "Student3", 17),
User(4, "Student4", 19)
)

// 查询数据
val result = userDao.getAll()

result.forEach {
println("Student: id = ${it.uid}, name = ${it.name}, age = ${it.age}")
}
}.start()


注意事项:
1. 使用数据访问对象(DAO)实例操作数据库时,不能再 UI 主线程中调用 DAO 接口,否则会抛出异常(java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.



四、编后语


    Room 是非常强大易用的,可以减少数据库操作过程中的出错,因为所有的 SQL 语句都在编译是进行检查,如果存在错误,将会在编译时就显示错误信息。不仅如此,Room 还非常优秀地处理了多进程很多线程访问数据库的问题。




————————————————
版权声明:本文为CSDN博主「精装机械师」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yingaizhu/article/details/117514630

收起阅读 »

Android数据库—SQLite

Android数据库—SQLite 不适合存储大规模数据 用来存储每一个用户各自的信息 在线查看数据库方法 Android Studio查看SQLite数据库方法大全 从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻...
继续阅读 »


Android数据库—SQLite



  • 不适合存储大规模数据

  • 用来存储每一个用户各自的信息


在线查看数据库方法


Android Studio查看SQLite数据库方法大全



从前我使用的是stetho方法来查看数据库,因为是外国网站,所以需要翻墙,比较麻烦。


如今最新版的Android Studio可以直接在里面查看数据库,无需别的了。




  • stetho使用



    • build.gradle文件中引入依赖

      implementation 'com.facebook.stetho:stetho:1.5.1'


    • 在需要操作数据库的Activity中加入以下语句

    Stetho.initializeWithDefaults(this);


    • 谷歌调试



继承SQLiteOpenHelper的类,加载驱动



继承SQLiteOpenHelper类,实现三个方法。



  • 构造函数

  • 建表方法:onCreate方法

  • 更新表方法:onUpgrade方法




  • MySQLiteOpenHelper


package com.hnucm.androiddatabase;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;

//加载数据库驱动
//建立连接
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
//构造方法
//name -> 数据库名字
public MySQLiteOpenHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}

//建表
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
//建表语句 自增长 主键
sqLiteDatabase.execSQL("create table products(id integer primary key autoincrement,name varchar(20),singleprice double,restnum integer) ");
}

//更新表
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

}
}

在Activity中进行增删改查



整个Activity都是用数据库,所以声明驱动和数据库为全局变量,方便使用。



//加载驱动
mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
"product",null,1);
//得到数据库
sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();


布局文件中设置四个按钮,进行增删改查操作。




  • 布局-------activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
tools:context=".MainActivity">


<Button
android:id="@+id/insert"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="增加一条商品信息"
android:textSize="25sp"
/>


<Button
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除一条商品信息"
android:textSize="25sp"
/>


<Button
android:id="@+id/update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改一条商品信息"
android:textSize="25sp"
/>


<Button
android:id="@+id/select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="查询一条商品信息"
android:textSize="25sp"
/>


</LinearLayout>


  • 总体逻辑代码-------MainActivity


package com.hnucm.androiddatabase;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
//声明增删改查四个按钮
Button addBtn;
Button delBtn;
Button updateBtn;
Button selectBtn;
//声明驱动
MySQLiteOpenHelper mySQLiteOpenHelper;
//声明数据库
SQLiteDatabase sqLiteDatabase;
//数据对象
ContentValues contentValues;
//增删改查条件变量
String id;
String name;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//加载驱动
mySQLiteOpenHelper = new MySQLiteOpenHelper(MainActivity.this,
"product",null,1);
//得到数据库
sqLiteDatabase = mySQLiteOpenHelper.getWritableDatabase();

//初始化四个按钮
addBtn = findViewById(R.id.insert);
delBtn = findViewById(R.id.delete);
updateBtn = findViewById(R.id.update);
selectBtn = findViewById(R.id.select);

//点击四个按钮
addBtn.setOnClickListener(this);
delBtn.setOnClickListener(this);
updateBtn.setOnClickListener(this);
selectBtn.setOnClickListener(this);
}

//四个按钮的点击事件
@Override
public void onClick(View view) {
switch (view.getId()){
//增加数据
case R.id.insert:
//创建数据,使用ContentValues -> HashMap
contentValues = new ContentValues();
//自增长 主键 增加无需加入id
//contentValues.put("id",1);
contentValues.put("name","辣条");
contentValues.put("singleprice",3.50);
contentValues.put("restnum",12);
//将创建好的数据对象加入数据库中的哪一个表
sqLiteDatabase.insert("products",null,contentValues);
break;
//删除数据
case R.id.delete:
//删除条件
id = "1";
name = "辣条";
//在哪张表里,根据条件删除
sqLiteDatabase.delete("products","id = ? and name = ?",
new String[]{id,name});
break;
//修改数据
case R.id.update:
//修改条件
id = "2";
//将满足条件的数据修改
contentValues = new ContentValues();
contentValues.put("name","薯片");
//在数据库中修改
sqLiteDatabase.update("products",contentValues,"id=?",
new String[]{id});
break;
//查询所有数据
case R.id.select:
//采用cursor游标查询
Cursor cursor = sqLiteDatabase.query("products",null,null,
null,null,null,null);
//游标下一个存在,即没有到最后
while(cursor.moveToNext()){
//每一条数据取出每一列
int id = cursor.getInt(cursor.getColumnIndex("id"));
name = cursor.getString(cursor.getColumnIndex("name"));
double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
//打印数据
Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
+ singleprice + ",restnum:" + restnum);
}
break;
}
}
}

增加数据


//创建数据,使用ContentValues -> HashMap
contentValues = new ContentValues();
//自增长 主键 增加无需加入id
//contentValues.put("id",1);
contentValues.put("name","辣条");
contentValues.put("singleprice",3.50);
contentValues.put("restnum",12);
//将创建好的数据对象加入数据库中的哪一个表
sqLiteDatabase.insert("products",null,contentValues);

删除数据


//删除条件
id = "1";
name = "辣条";
//在哪张表里,根据条件删除
sqLiteDatabase.delete("products","id = ? and name = ?",
new String[]{id,name});

修改数据


//修改条件
id = "2";
//将满足条件的数据修改
contentValues = new ContentValues();
contentValues.put("name","薯片");
//在数据库中修改
sqLiteDatabase.update("products",contentValues,"id=?",
new String[]{id});

查询数据


//采用cursor游标查询
//没有查询条件,所以查询表中所有信息
Cursor cursor = sqLiteDatabase.query("products",null,null,
null,null,null,null);
//游标下一个存在,即没有到最后
while(cursor.moveToNext()){
//每一条数据取出每一列
int id = cursor.getInt(cursor.getColumnIndex("id"));
name = cursor.getString(cursor.getColumnIndex("name"));
double singleprice = cursor.getDouble(cursor.getColumnIndex("singleprice"));
int restnum = cursor.getInt(cursor.getColumnIndex("restnum"));
//打印数据
Log.i("products","id:" + id + ",name:" + name + ",singleprice:"
+ singleprice + ",restnum:" + restnum);
}
收起阅读 »

总是听到有人说AndroidX,到底什么是AndroidX?

Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。An...
继续阅读 »

Android技术迭代更新很快,各种新出的技术和名词也是层出不穷。不知从什么时候开始,总是会时不时听到AndroidX这个名词,这难道又是什么新出技术吗?相信有很多朋友也会存在这样的疑惑,那么今天我就来写一篇科普文章,向大家介绍AndroidX的前世今生。

Android系统在刚刚面世的时候,可能连它的设计者也没有想到它会如此成功,因此也不可能在一开始的时候就将它的API考虑的非常周全。随着Android系统版本不断地迭代更新,每个版本中都会加入很多新的API进去,但是新增的API在老版系统中并不存在,因此这就出现了一个向下兼容的问题。

举个例子,当Android系统发布到3.0版本的时候,突然意识到了平板电脑的重要性,因此为了让Android可以更好地兼容平板,Android团队在3.0系统(API 11)中加入了Fragment功能。但是Fragment的作用并不只局限于平板,以前的老系统中也想使用这个功能该怎么办?于是Android团队推出了一个鼎鼎大名的Android Support Library,用于提供向下兼容的功能。比如我们每个人都熟知的support-v4库,appcompat-v7库都是属于Android Support Library的,这两个库相信任何做过Android开发的人都使用过。

但是可能很多人并没有考虑过support-v4库的名字到底是什么意思,这里跟大家解释一下。4在这里指的是Android API版本号,对应的系统版本是1.6。那么support-v4的意思就是这个库中提供的API会向下兼容到Android 1.6系统。它对应的包名如下:

类似地,appcompat-v7指的是将库中提供的API向下兼容至API 7,也就是Android 2.1系统。它对应的包名如下:

可以发现,Android Support Library中提供的库,它们的包名都是以android.support.*开头的。

但是慢慢随着时间的推移,什么1.6、2.1系统早就已经被淘汰了,现在Android官方支持的最低系统版本已经是4.0.1,对应的API版本号是15。support-v4、appcompat-v7库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。

那么很明显,Android团队也意识到这种命名已经非常不合适了,于是对这些API的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX本质上其实就是对Android Support Library进行的一次升级,升级内容主要在于以下两个方面。

第一,包名。之前Android Support Library中的API,它们的包名都是在android.support.*下面的,而AndroidX库中所有API的包名都变成了在androidx.*下面。这是一个很大的变化,意味着以后凡是android.*包下面的API都是随着Android操作系统发布的,而androidx.*包下面的API都是随着扩展库发布的,这些API基本不会依赖于操作系统的具体版本。

第二,命名规则。吸取了之前命名规则的弊端,AndroidX所有库的命名规则里都不会再包含具体操作系统API的版本号了。比如,像appcompat-v7库,在AndroidX中就变成了appcompat库。

一个AndroidX完整的依赖库格式如下所示:

implementation 'androidx.appcompat:appcompat:1.0.2'

了解了AndroidX是什么之后,现在你应该放轻松了吧?它其实并不是什么全新的东西,而是对Android Support Library的一次升级。因此,AndroidX上手起来也没有任何困难的地方,比如之前你经常使用的RecyclerView、ViewPager等等库,在AndroidX中都会有一个对应的版本,只要改一下包名就可以完全无缝使用,用法方面基本上都没有任何的变化。

但是有一点需要注意,AndroidX和Android Support Library中的库是非常不建议混合在一起使用的,因为它们可能会产生很多不兼容的问题。最好的做法是,要么全部使用AndroidX中的库,要么全部使用Android Support Library中的库。

而现在Android团队官方的态度也很明确,未来都会为AndroidX为主,Android Support Library已经不再建议使用,并会慢慢停止维护。另外,从Android Studio 3.4.2开始,我发现新建的项目已经强制勾选使用AndroidX架构了。

那么对于老项目的迁移应该怎么办呢?由于涉及到了包名的改动,如果从Android Support Library升级到AndroidX需要手动去改每一个文件的包名,那可真得要改死了。为此,Android Studio提供了一个一键迁移的功能,只需要对着你的项目名右击 → Refactor → Migrate to AndroidX,就会弹出如下图所示的窗口。

这里点击Migrate,Android Studio就会自动检查你项目中所有使用Android Support Library的地方,并将它们全部改成AndroidX中对应的库。另外Android Studio还会将你原来的项目备份成一个zip文件,这样即使迁移之后的代码出现了问题你还可以随时还原回之前的代码。


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

收起阅读 »

【面试专题】Android屏幕刷新机制

这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我...
继续阅读 »

这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我面网易云的时候也确实被问到了这个题目。


屏幕刷新这一整套,你把我这篇文章里的内容讲清楚了,肯定ok了。网易云还附加问了我CPU和GPU怎么交换绘制数据的,这个我个人认为完全是加分题了,我答不出来,感兴趣的小伙伴可以去看一看,你要是能说清楚,肯定能让面试官眼前一亮。


双缓冲


在讲双缓冲这个概念之前,先来了解一些基础知识。


显示系统基础


在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分, CPU负责计算帧数据,把计算好的数据交给GPU, GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数 据呈现到屏幕上。



  • 画面撕裂


屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一 帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即 buffer里的数据可能是来自不同的帧的。当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。


简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。


那咋解决画面撕裂呢? 答案是使用双缓冲。


双缓冲


由于图像绘制和屏幕读取 使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。


双缓冲,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。


VSync


什么时候进行两个buffer的交换呢?


假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。 看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。


当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现画面撕裂的状况。


VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。


所以说VSync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了。


Android屏幕刷新机制


先总体概括一下,Android屏幕刷新使用的是“双缓存+VSync机制”,单纯的双缓冲模式容易造成jank(丢帧)现象,为了解决这个问题,Google在 Android4.1 提出了Project Butter(?油工程),引入了 drawing with VSync 的概念。


jank(丢帧)


VSync.jpeg


以时间的顺序来看下将会发生的过程:



  1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,且在Display显示下一帧前完成

  2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧

  3. 接着第2帧开始处理,是直到第2个VSync快来前才开始处理的。

  4. 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。

  5. 当第2帧数据准备完成后,它并不会?上被显示,而是要等待下一个VSync 进行缓存交换再显示。


所以总的来说,就是屏幕平白无故地多显示了一次第1帧。 原因是第2帧的CPU/GPU计算 没能在VSync信号到来前完成。


这里注意一下一个细节,jank(丢帧、掉帧),不是说这一帧丢弃了不显示,而是这一帧延迟显示了,因为缓存交换的时机只能等下一个VSync了。


黄油计划 —— drawing with VSync


为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(?油工程): 系统在收到VSync pulse后,将?上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开 始计算然后把数据写入buffer。如下图:


VSync2.jpeg


CPU/GPU根据VSYNC信号同步处理数据,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。 一句话总结,VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。


问题又来了,如果界面比较复杂,CPU/GPU的处理时间较?,超过了16.6ms呢?如下图:


VSync3.jpeg



  1. 在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。

  2. 而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。

  3. 当下一个VSync出现时,CPU/GPU?上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。


为什么 CPU 不能在第二个 16ms 处理绘制工作呢? 因为只有两个 buffer,Back buffer正在被GPU用来处理B帧的数据, Frame buffer的内容用于Display的显示,这样两个 buffer都被占用,CPU 则无法准备下一帧的数据。 那么,如果再提供一个buffer,CPU、GPU 和显示设备都能使用各自的 buffer工作,互不影响。这就是三缓冲的来源了。


三缓冲


三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。


VSync4.jpeg



  1. 第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是 会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。

  2. 注意在第3段中,A帧的计算已完成,但是在第4个vsync来的时候才显示,如果是双缓冲,那在第三个vynsc就可以显示了。


三缓冲有效利用了等待VSync的时间,减少了jank,但是带来了延迟。是不是 Buffer 越多越好呢?这个是否定的, Buffer 正常还是两个,当出现 Jank 后三个足以。


Choreographer


上边讲的都是基础的刷新知识,那么在 Android 系统中,真正来实现绘制的类叫Choreographer


Choreographer负责对CPU/GPU绘制的指导 —— 收到VSync信号才开始绘制,保证绘制拥有完整 16.6ms,避免绘制的随机性。


通常 应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的 ValueAnimator.start()、View.invalidate()等。


(这边补充说一个面试题,属性动画更新时会回调onDraw吗?不会,因为它内部是通过AnimationHandler中的Choreographer机制来实现的更新,具体的逻辑,如果以后有时间的话可以写篇文章来说一说。)


业界一般通过Choreographer来监控应用的帧率。


(这个东西也是个面试题,会问你如何检测应用的帧率?你可以提一下Choreographer里面的FrameCallback,然后结合一些第三方库的实现具体说一下。)


View刷新的入口


Activity启动,走完onResume方法后,会进行window的添加。window添加过程会调用ViewRootImpl的setView()方法, setView()方法会调用requestLayout()方法来请求绘制布局,requestLayout()方法内部又会走到scheduleTraversals()方法。最后会走到performTraversals()方法,接着到了我们熟知的测量、布局、绘制三大流程了。


当我们使用 ValueAnimator.start()、View.invalidate()时,最后也是走到ViewRootImpl的 scheduleTraversals()方法。(View.invalidate()内部会循环获取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源码)


即所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。


这里注意一个点:scheduleTraversals()之后不是立即就执行performTraversals()的,它们中间隔了一个Choreographer机制。简单来说就是scheduleTraversals()中,Choreographer会去请求native的VSync信号,VSync信号来了之后才会去调用performTraversals()方法进行View绘制的三大流程。



//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//添加同步屏障,屏蔽同步消息,保证VSync到来立即执行绘制
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//mTraversalRunnable是TraversalRunnable实例,最终走到run(),也即doTraversal();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ...
//开始三大绘制流程
performTraversals();
...
}
}


  1. postSyncBarrier 开启同步屏障,保证VSync到来后立即执行绘制

  2. mChoreographer.postCallback()方法,发送一个会在下一帧执行的回调,即在下一个VSync到来时会执行 TraversalRunnable–>doTraversal()—>performTraversals()–>绘制流程。


Choreographer


初始化


mChoreographer,是在ViewRootImpl的构造方法内使用 Choreographer.getInstance()创建。


Choreographer和Looper一样是线程单例的,通过ThreadLocal机制来保证唯一性。因为Choreographer内部通过FrameHandler来发送消息,所以初始化的时候会先判断当前线程有无Looper,没有的话直接抛异常。


public static Choreographer getInstance() {
return sThreadInstance.get();
}

private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
//当前线程要有looper,Choreographer实例需要传入
throw new IllegalStateException("The current thread must have a looper!");
}
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};

postCallback


mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,第一个参数是CALLBACK_TRAVERSAL,表示回调任务的类型,共有以下5种类型:


//输入事件,首先执行
public static final int CALLBACK_INPUT = 0;
//动画,第二执行
public static final int CALLBACK_ANIMATION = 1;
//插入更新的动画,第三执行
public static final int CALLBACK_INSETS_ANIMATION = 2;
//绘制,第四执行
public static final int CALLBACK_TRAVERSAL = 3;
//提交,最后执行,
public static final int CALLBACK_COMMIT = 4;

五种类型任务对应存入对应的CallbackQueue中,每当收到 VSYNC 信号时,Choreographer 将首先处理 INPUT 类型的任 务,然后是 ANIMATION 类型,最后才是 TRAVERSAL 类型。


postCallback()内部调用postCallbackDelayed(),接着又调用postCallbackDelayedInternal(),正常消息执行scheduleFrameLocked,延迟运行的消息会发送一个MSG_DO_SCHEDULE_CALLBACK类型的meessage:


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis)
{
...
synchronized (mLock) {
...
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) { //立即执行
scheduleFrameLocked(now);
} else {
//延迟运行,最终也会走到scheduleFrameLocked()
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

FrameHandler这个类是内部专门用来处理消息的,可以看到延迟的MSG_DO_SCHEDULE_CALLBACK类型消息最终也是走到scheduleFrameLocked:


private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
// 执行doFrame,即绘制过程
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
//申请VSYNC信号,例如当前需要绘制任务时
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
//需要延迟的任务,最终还是执行上述两个事件
doScheduleCallback(msg.arg1);
break;
}
}
}

void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}

申请VSync信号


scheduleFrameLocked()方法里面就会去真正的申请 VSync 信号了。


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//当前执行的线程,是否是mLooper所在线程
if (isRunningOnLooperThreadLocked()) {
//申请 VSYNC 信号
scheduleVsyncLocked();
} else {
// 若不在,就用mHandler发送消息到原线程,最后还是调用scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);//异步
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
// 如果未开启VSYNC则直接doFrame方法(4.1后默认开启)
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);//异步
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

VSync信号的注册和监听是通过mDisplayEventReceiver实现的。mDisplayEventReceiver是在Choreographer的构造方法中创建的,是FrameDisplayEventReceiver的实例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子类,


private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

public DisplayEventReceiver(Looper looper, int vsyncSource) {
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mMessageQueue = looper.getQueue();
// 注册native的VSYNC信号监听者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,vsyncSource);
mCloseGuard.open("dispose");
}

VSync信号回调


native的VSync信号到来时,会走到onVsync()回调:


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable
{

@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
...
//将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

(这里补充一个面试题:页面UI没有刷新的时候onVsync()回调也会执行吗?不会,因为VSync是UI需要刷新的时候主动去申请的,而不是native层不停地往上面去推这个回调的,这边要注意。)


doFrame


doFrame()方法中会通过doCallbacks()方法去执行各种callbacks,主要内容就是取对应任务类型的队列,遍历队列执行所有任务,其中就包括了 ViewRootImpl 发起的绘制任务mTraversalRunnable了。mTraversalRunnable执行doTraversal()方法,移除同步屏障,调用performTraversals()开始三大绘制流程。


到这里整个流程就闭环了。


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

java设计模式:备忘录模式

前言 备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。 定义 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。 ...
继续阅读 »


前言


备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。


定义


在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
在这里插入图片描述


优点


提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。


缺点


资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。


结构



  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。

  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。

  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。


实现


生活中最常用的计算器自己拥有备忘录的功能,用户计算完后软件会自动为用户记录最后几次的计算结果,我们可以模拟用户使用计算器的过程,以及打开备忘录查看记录。


package com.rabbit;

/**
* 备忘录发起人,模拟计算器加法运算
* Created by HASEE on 2018/4/29.
*/
public class Originator {

private double num1;

private double num2;

//创建备忘录对象
public Memento createMemento() {
return new Memento(num1, num2);
}

public Originator(double num1, double num2) {
this.num1 = num1;
this.num2 = num2;
System.out.println(num1 + " + " + num2 + " = " + (num1 + num2));
}

}
package com.rabbit;

/**
* 备忘录,要保存的属性
* Created by HASEE on 2018/4/29.
*/
public class Memento {

private double num1;//计算器第一个数字

private double num2;//计算器第二个数字

private double result;//计算结果

public Memento(double num1, double num2) {
this.num1 = num1;
this.num2 = num2;
this.result = num1 + num2;
}

public void show() {
System.out.println(num1 + " + " + num2 + " = " + result);
}

}
package com.rabbit;

import java.util.ArrayList;
import java.util.List;

/**
* 备忘录管理者
* Created by HASEE on 2018/4/29.
*/
public class Caretaker {

private List<Memento> mementos;

public boolean addMenento(Memento memento) {
if (mementos == null) {
mementos = new ArrayList<>();
}
return mementos.add(memento);
}

public List<Memento> getMementos() {
return mementos;
}

public static Caretaker newInstance() {
return new Caretaker();
}
}
package com.rabbit;

import org.junit.Test;

import java.util.Random;

/**
* Created by HASEE on 2018/4/29.
*/
public class Demo {

@Test
public void test() {
Caretaker c = Caretaker.newInstance();
//使用循环模拟用户使用计算器做加法运算
Random ran = new Random(1000);
for (int i = 0; i < 5; i++) {
//用户计算
Originator o = new Originator(ran.nextDouble(), ran.nextDouble());
//计算器软件将用户的计算做备份,以便可以查看历史
c.addMenento(o.createMemento());
}
System.out.println("---------------------用户浏览历史记录---------------------");
for (Memento m : c.getMementos()) {
m.show();
}
System.out.println("---------------------用户选择一条记录查看----------------------");
c.getMementos().get(2).show();
}

}

收起阅读 »

java设计模式:访问者模式

前言 访问者模式是一种将数据操作和数据结构分离的设计模式。 定义 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构...
继续阅读 »


前言


访问者模式是一种将数据操作和数据结构分离的设计模式。


定义


将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。


优点



  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。

  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。

  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。


缺点



  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。

  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。


结构



  • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。

  • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。

  • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。

  • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。

  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。


示例


年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。
由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。


// 员工基类
public abstract class Staff {

public String name;
public int kpi;// 员工KPI

public Staff(String name) {
this.name = name;
kpi = new Random().nextInt(10);
}
// 核心方法,接受Visitor的访问
public abstract void accept(Visitor visitor);
}

Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码:


// 工程师
public class Engineer extends Staff {

public Engineer(String name) {
super(name);
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 工程师一年的代码数量
public int getCodeLines() {
return new Random().nextInt(10 * 10000);
}
}
// 经理
public class Manager extends Staff {

public Manager(String name) {
super(name);
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 一年做的产品数量
public int getProducts() {
return new Random().nextInt(10);
}
}

工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。
然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩,具体代码如下:


// 员工业务报表类
public class BusinessReport {

private List<Staff> mStaffs = new LinkedList<>();

public BusinessReport() {
mStaffs.add(new Manager("经理-A"));
mStaffs.add(new Engineer("工程师-A"));
mStaffs.add(new Engineer("工程师-B"));
mStaffs.add(new Engineer("工程师-C"));
mStaffs.add(new Manager("经理-B"));
mStaffs.add(new Engineer("工程师-D"));
}

/**
* 为访问者展示报表
* @param visitor 公司高层,如CEO、CTO
*/
public void showReport(Visitor visitor) {
for (Staff staff : mStaffs) {
staff.accept(visitor);
}
}
}


下面看看 Visitor 类型的定义, Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数,具体代码如下:


public interface Visitor {

// 访问工程师类型
void visit(Engineer engineer);

// 访问经理类型
void visit(Manager manager);
}

首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类,具体代码如下:


// CEO访问者
public class CEOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
}

@Override
public void visit(Manager manager) {
System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
", 新产品数量: " + manager.getProducts());
}
}

在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理,代码大致如下:


public class ReportUtil {
public void visit(Staff staff) {
if (staff instanceof Manager) {
Manager manager = (Manager) staff;
System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
", 新产品数量: " + manager.getProducts());
} else if (staff instanceof Engineer) {
Engineer engineer = (Engineer) staff;
System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
}
}
}

这就导致了 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这个 ReportUtil 就会很复杂。而使用 Visitor 模式,通过同一个函数对不同对元素类型进行相应对处理,使结构更加清晰、灵活性更高。
再添加一个CTO的 Visitor 类:



public class CTOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines());
}

@Override
public void visit(Manager manager) {
System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts());
}
}

重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。
下面是客户端代码:


public class Client {

public static void main(String[] args) {
// 构建报表
BusinessReport report = new BusinessReport();
System.out.println("=========== CEO看报表 ===========");
report.showReport(new CEOVisitor());
System.out.println("=========== CTO看报表 ===========");
report.showReport(new CTOVisitor());
}
}

具体输出如下:


=========== CEO看报表 ===========
经理: 经理-A, KPI: 9, 新产品数量: 0
工程师: 工程师-A, KPI: 6
工程师: 工程师-B, KPI: 6
工程师: 工程师-C, KPI: 8
经理: 经理-B, KPI: 2, 新产品数量: 6
工程师: 工程师-D, KPI: 6
=========== CTO看报表 ===========
经理: 经理-A, 产品数量: 3
工程师: 工程师-A, 代码行数: 62558
工程师: 工程师-B, 代码行数: 92965
工程师: 工程师-C, 代码行数: 58839
经理: 经理-B, 产品数量: 6
工程师: 工程师-D, 代码行数: 53125


在上述示例中,Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码。
访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。


应用场景


当系统中存在类型数量稳定(固定)的一类数据结构时,可以使用访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。


简而言之,就是当对集合中的不同类型数据(类型数量稳定)进行多种操作时,使用访问者模式。


通常在以下情况可以考虑使用访问者模式。



  1. 对象结构相对稳定,但其操作算法经常变化的程序。

  2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

  3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。  

收起阅读 »

面试题:介绍一下 LiveData 的 postValue ?

很多面试官喜欢会就一个问题不断深入追问。 例如一个小小的 LiveData 的 postValue,就可能会问出一连串问题: postValue 与 setValue postValue 与 setValue 一样都是用来更新 LiveData 数据...
继续阅读 »

很多面试官喜欢会就一个问题不断深入追问。


例如一个小小的 LiveData 的 postValue,就可能会问出一连串问题:


image.png


postValue 与 setValue


postValuesetValue 一样都是用来更新 LiveData 数据的方法:



  • setValue 只能在主线程调用,同步更新数据

  • postValue 可在后台线程调用,其内部会切换到主线程调用 setValue


liveData.postValue("a");
liveData.setValue("b");

上面代码,a 在 b 之后才被更新。


postValue 收不到通知


postValue 使用不当,可能发生接收到数据变更的通知:



If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.



如上,源码的注释中明确记载了,当连续调用 postValue 时,有可能只会收到最后一次数据更新通知。


梳理源码可以了解其中原由:


protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

mPendingData 被成功赋值 value 后,post 了一个 Runnable


mPostValueRunnable 的实现如下:


private final Runnable mPostValueRunnable = new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
Object newValue;
synchronized (mDataLock) {
newValue = mPendingData;
mPendingData = NOT_SET;
}
setValue((T) newValue);
}
};


  • postValue 将数据存入 mPendingDatamPostValueRunnable 在UI线程消费mPendingData


  • 在 Runnable 中 mPendingData 值还没有被消费之前,即使连续 postValue , 也不会 post 新的 Runnable


  • mPendingData 的生产 (赋值) 和消费(赋 NOT_SET) 需要加锁



这也就是当连续 postValue 时只会收到最后一次通知的原因。


源码梳理过了,但是为什么要这样设计呢?


为什么 Runnable 只 post 一次?


mPenddingData 中有数据不断更新时,为什么 Runnable 不是每次都 post,而是等待到最后只 post 一次?


一种理解是为了兼顾性能,UI只需显示最终状态即可,省略中间态造成的频发刷新。这或许是设计目的之一,但是一个更为合理的解释是:即使 post 多次也没有意义,所以只 post 一次即可


我们知道,对于 setValue 来说,连续调用多次,数据会依次更新:


如下,订阅方一次收到 a b 的通知


liveData.setValue("a");
liveData.setValue("b");

通过源码可知,dispatchingValue() 中同步调用 Observer#onChanged(),依次通知订阅方:


//setValue源码

@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}

但对于 postValue,如果当 value 变化时,我们立即post,而不进行阻塞


protected void postValue(T value) {
mPendingData = value;
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
public void run() {
setValue((T) mPendingData);
}
};

liveData.postValue("a")
liveData.postValue("b")

由于线程切换的开销,连续调用 postValue,收到通知只能是b、b,无法收到a。


因此,post 多次已无意义,一次即可。


为什么要加读写锁?


前面已经知道,是否 post 取决于对 mPendingData 的判断(是否为 NOT_SET)。因为要在多线程环境中访问 mPendingData ,不加读写锁无法保证其线程安全。


protected void postValue(T value) {
boolean postTask = mPendingData == NOT_SET; // --1
mPendingData = value; // --2
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
public void run() {
Object newValue = mPendingData;
mPendingData = NOT_SET; // --3
setValue((T) newValue);
}
};

如上,如果在 1 和 2 之间,执行了 3,则 2 中设置的值将无法得到更新


使用RxJava替换LiveData


如何避免在多线程环境下不漏掉任何一个通知? 比较好的思路是借助 RxJava 这样的流式框架,任何数据更新都以数据流的形式发射出来,这样就不会丢失了。


fun <T> Observable<T>.toLiveData(): LiveData<T> = RxLiveData(this)

class RxLiveData<T>(
private val observable: Observable<T>
) : LiveData<T>() {
private var disposable: Disposable? = null

override fun onActive() {
disposable = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
setValue(it)
}, {
setValue(null)
})
}

override fun onInactive() {
disposable?.dispose()
}
}

最后


想要保证事件在线程切换过程中的顺序性和完整性,需要使用RxJava这样的流式框架。


有时候面试官会使用追问的形式来挖掘候选人的技术深度,所以大家在准备面试时要多问自己几个问什么,知其然并知其所以然。


当然,我也不赞同这种刨根问底式的拷问方式,尤其是揪着一些没有实用价值的细枝末节不放。所以本文也是提醒广大面试官,挖掘深度的同时要注意分寸,不能以将候选人难倒为目标来问问题。



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

基于FakerAndroid的一次il2cpp游戏逆向精修实录!!!零汇编零二进制纯编码实现

~~~格式优化整理~~~1、下载FakerAndroid工具包 下载地址:https://github.com/Efaker/FakerAndroid/releases 2、cmd切换到FakerAndroid.jar平级目录  [工具包和...
继续阅读 »

~~~格式优化整理~~~
1、下载FakerAndroid工具包
下载地址:https://github.com/Efaker/FakerAndroid/releases 
2、cmd切换到FakerAndroid.jar平级目录 
[工具包和要操作的Apk]

[工具包目录]

3、执行 java -jar FakerAndroid.jar fk <apkpath>生成AndroidStudio工程
[执行命令]

[等待命令执行完成]

4、查看Apk平级目录下面生成的AndroidStudio工程
[查看原安装包目录]

5、AndroidStudio直接打开生成的Android工程
[生成的Android项目工程目录结构]

6、等待加载完成直接运行项目(确认项目加载完成,部分Res或Manifest文件有问题的话需要手动修复一下,实测大部分的未做res混淆的Apk都是没有问题的)
[直接Run运行项目]

7、Java类调用之继承(意在演示Java层原有Java类调用)
[父类继承]

8、Java类调用之Api调用(意在演示Java层原有Java Api调用)
[父类Api调用]

9、Manifest入口Activity替换
[AndroidManifest入口Activity替换]

10、Java类替换(意在演示对原有Java类的直接替换)
[类替换之原类]

[类替换之自己编写的替换类]

11、定义Jni方法进行Hoook操作和il2cpp脚手架的调用
[Jni方法定义]

[HookApi和Il2cpp脚手架的使用]

12、对原il2cpp脚手架定义过的方法进行Hook替换
[Il2cpp脚手架中的UI回调函数替换以及Il2cpp脚手架中的Api调用]

[JniHook Btn]

13、最后上一下效果图,忘记说了,文章中所有图片的宽度都使用了1024px
[效果图]

收起阅读 »

iOS离屏渲染的触发原理与躲在背后的性能优化

一.带着问题了解什么是离屏渲染?        在iOS开发中,我们经常会写到这样的代码:btn.layer.cornerRadius = 50;btn.clipsToBounds = YE...
继续阅读 »

一.带着问题了解什么是离屏渲染?

        在iOS开发中,我们经常会写到这样的代码:btn.layer.cornerRadius = 50;btn.clipsToBounds = YES;很多的面试官也会问我们平常给VIew设置圆角的时候应该注意什么?在UITableViewCell中,如果出现了btn.layer.cornerRadius = 50;btn.clipsToBounds = YES这两行代码,会给tableVIew的渲染以及滑动带来什么影响?为什么在有些地方不建议使用这样的代码设置圆角?(btn只是一个举例,实际上它可以是UIview,UIbutton,uiimageVIew等),你们是否能回答出面试官心中想要的答案?

二.离屏渲染的由来

        在上一篇文章中,我提到了图像/图形渲染的流程:GPU进⾏渲染->帧缓存区⾥ ->视频控制器->读取帧缓存区信息(位图) -> 数模转化(数字信号处->模 拟型号) ->(逐⾏扫描)显示,重点来了:当帧缓冲区的数据不能直接被视频控制器扫描显示的时候,我们要额外的开辟一个缓冲区------->离屏缓冲区来存储我们不能第一时间交给视频控制器显示的数据,在离屏缓冲区渲染好我们不能直接被视频控制器显示的数据,等到最终我们可以确认当前的VIew到底怎么显示之后,再交给帧缓冲区----->视频控制器显示。




离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了 (间接回答了如果出现了btn.layer.cornerRadius = 50;btn.clipsToBounds = YES这两行代码,会给tableVIew的渲染以及滑动带来什么影响?)   

        特别提醒:离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系

        最终当触发了离屏渲染之后,图像/图形的渲染流程变成了:app进⾏额外的渲染和合并-> offscreen Buffer(离屏缓冲区) 组合. -> FrameBuffer(帧缓冲区) -> 屏幕;特点:(离屏渲染-> 额外的存储空间/offscreen Buffer->FrameBuffer ) offscreenBuffer 空间大小-> 屏幕像素点2.5倍 


离屏渲染遵循画家算法:按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存共享物理内存,这样可以省去一些数据传输开销),然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作

三.btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES就一定会触发离屏渲染?

        首先我们先开启离屏渲染的检测,在模拟器打开color offscreen-rendered,开启后会把那些需要离屏渲染



这里就明显看出1和3变成了黄色,标记为触发了离屏渲染,个人觉得这应该是模拟器的bug吧,如果你的电脑没有出现这个问题,请忽略,有的话就试着选一选其他机型吧!!!

首先普及一下CALayer的层次结构:CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成

重点重点重点(重要的事情说三遍):cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。这也就说明了上面代码为什么1和3触发了离屏渲染,而2和4没有触发离屏渲染

解决办法:

(1)后台绘制圆角图片,前台进行设置




(2)对于 contents 无内容或者内容的背景透明(无涉及到圆角以外的区域)的layer,直接设置layer的 backgroundColor 和 cornerRadius 属性来绘制圆角。

(3)使用混合图层,在layer上方叠加相应mask形状的半透明layer

sublayer.contents=(id)[UIImage imageNamed:@"xxx"].CGImage;

[view.layer addSublayer:sublayer];

(4)- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor borderLineJoin:(CGLineJoin)borderLineJoin此方法为YY_image处理圆角的方法,你可以去下载YY_image查看源码

其他情况触发离屏渲染以及解决办法:

1. mask(遮罩)------>使用混合图层,在layer上方叠加相应mask形状的半透明layer

2.edge antialiasing(抗锯齿)----->不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

3. allowsGroupOpacity(组不透明,开启CALayer的allowsGroupOpacity属性后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity(对应UIView的alpha),并且从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。)------->关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

4.shadows(阴影)------>设置阴影后,设置CALayer的 shadowPath,view.layer.shadowPath=[UIBezierPath pathWithCGRect:view.bounds].CGPath;

CALayer离屏渲染终极解决方案:当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES(缓存离屏渲染的数据,当下次用到的时候直接拿,不需要开辟新的离屏缓冲区),此方案最为实用方便。view.layer.shouldRasterize = true;view.layer.rasterizationScale = view.layer.contentsScale;

shouldRasterize (光栅华使用建议):

1.如果layer不需要服用,则没有必要打开

2.如果layer不是静态的,需要被频繁修改,比如出于动画之中,则开启光栅华反而影响性能

3.离屏渲染缓存有时间限制,当超过100ms,内容没有被使用就会被丢弃,无法复用

4.离屏渲染缓存有空间限制,超过屏幕像素的2.5倍则失效,并无法使用

特别说明:当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

总结:

(1)离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系

(2)btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES不一定会触发离屏渲染,cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染

(3)在uitableVIewcell触发了离屏渲染,会导致在滑动的时候高频率的开辟离屏缓冲区,这样就会造成tanleView滑动卡顿,如果视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便,但是当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

(4)现在摆在我们面前得有三个选择:当前屏幕渲染、离屏渲染、CPU渲染,该用哪个呢?这需要根据具体的使用场景来决定。·   尽量使用当前屏幕渲染,鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。离屏渲染 VS CPU渲染



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









收起阅读 »

iOS------OpenGL 图形专有名词与坐标解析

一.OpenGL简介OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操...
继续阅读 »

一.OpenGL简介

OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D3D矢量图形的跨语言跨平台应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操作抽象为⼀个个的OpenGL指令,开发者可以在mac程序中使用OpenGl来实现图形渲染。图形API的目的就是实现图形的底层渲染,比如游戏场景/游戏人物的渲染,音视频解码后数据的渲染,地图上的渲染,动画绘制等。在iOS开发中,开发者唯一能够GPU的就是图形API。(GPU---图形处理器(英语:Graphics Processing Unit,缩写:GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑工作站、游戏机和一些移动设备(如平板电脑智能手机等)上做图像和图形相关运算工作的微处理器。)

二.OpenGL专业名词解析

    1.OpenGL 上下⽂( context )

        OpenGL Context,中文解释就是OpenGL的上下文,因为OpenGL没有窗口的支持,我们在使用OpenGl的时候,一般是在main函数创建窗口: 

        //GLUT窗口大小、窗口标题

        glutInitWindowSize(800, 600);

        glutCreateWindow("Triangle");

        然后我们在创建的窗口里面绘制,个人理解上下文的意思就是指的是OpenGL的作用范围,当然OpenGL的Context不只是这个窗口,这个窗口我们可以理解为OpenGL的default framebuffer,所以Context还包含关于这个framebuffer的一些参数设置信息,具体内容可以查看OpenGL的Context的结构体,Context记录了OpenGL渲染需要的所有信息,它是一个大的结构体,它里面记录了当前绘制使用的颜色、是否有光照计算以及开启的光源等非常多我们使用OpenGL函数调用设置的状态和状态属性等等,你可以把它理解为是一个巨大的状态机,它里面保存OpenGl的指令,在图形渲染的时候,可以理解为这个状态机开始工作了,对某个属性或者开关发出指令。它的特点就是:有记忆功能,接收输入,根据输入的指令,修改当前的状态,并且可以输出内容,当停机的时候不再接收指令。

2.渲染

        渲染就是把数据显示到屏幕上,在OpenGl中,渲染指的是将图形/图像数据转换为2D空间图像操作叫渲染

3.顶点数组/顶点缓冲区

        在OpenGL中,基本图元有三种:点,线,三角形,复杂的图形由这三种图元组成,我们在画点/线/三角形的时候是不是应该先知道每个顶点的坐标,而这些坐标放在数组里,就叫顶点数组。顶点数组存在内存当中,但是为了提高性能,提前分配一块显存,将顶点数组预先存入到显存当中,这部分的显存就叫顶点缓冲区。

4.着色器(shader)

        为什么要使用着色器?我们知道,OpenGL一般使用经典的固定渲染管线来渲染对象,OpenGL在实际调⽤绘制函数之前,还需指定⼀个由shader编译成的着⾊器程序。常⻅的着⾊器主要有顶点着⾊器(VertexShader),⽚段着⾊器(FragmentShader)/像素着⾊器(PixelShader),⼏何着⾊器(GeometryShader),曲⾯细分着⾊器(TessellationShader)。⽚段着⾊器和像素着⾊器只是在OpenGL和DX中的不同叫法⽽已。可惜的是,直到OpenGLES 3.0,依然只⽀持了顶点着⾊器和⽚段着⾊器这两个最基础的着⾊器。OpenGL在处理shader时,和其他编译器⼀样。通过编译、链接等步骤,⽣成了着⾊器程序(glProgram),着⾊器程序同时包含了顶点着⾊器和⽚段着⾊器的运算逻辑。在OpenGL进⾏绘制的时候,⾸先由顶点着⾊器对传⼊的顶点数据进⾏运算。再通过图元装配,将顶点转换为图元。然后进⾏光栅化,将图元这种⽮量图形,转换为栅格化数据。最后,将栅格化数据传⼊⽚段着⾊器中进⾏运算。⽚段着⾊器会对栅格化数据中的每⼀个像素进⾏运算,并决定像素的颜⾊(顶点着色器和片段/片元着色器会在下面讲解)

5.管线

        OpenGL在渲染图形/图像的时候是按照特定的顺序来执行的,不能修改打破,管线的意思个人理解是读取顶点数据—>顶点着色器—>组装图元—>光栅化图元—>片元着色器—>写入帧缓冲区—>显示到屏幕上,类似这样的流水线,当图像/图形显示到屏幕上,这一条管线完成工作。下面是分步讲解:

       (1)读取顶点数据指的是将待绘制的图形的顶点数据传递给渲染管线中。

        (2)顶点着色器最终生成每个定点的最终位置,执行顶点的各种变换,它会针对每个顶点执行一次,确定了最终位置后,OpenGL就可以把这些顶点集合按照给定的参数类型组装成点,线或者三角形。

      (3)组装图元阶段包括两部分:图元的组装和图元处理,图元组装指的是顶点数据根据设置的绘制方式参数结合成完整的图元,例如点绘制方式中每个图元就只包含一个点,线段绘制方式中每个图源包含两个点;图元处理主要是剪裁以使得图元位于视景体内部的部分传递到下一个步骤,视景体外部的部分进行剪裁。视景体的概念与投影有关。

      (4)光栅化图元主要指的是将一个图元离散化成可显示的二维单元片段,这些小单元称为片元。一个片元对应了屏幕上的一个或多个像素,片元包括了位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。

      (5)片元着色器为每个片元生成最终的颜色,针对每个片元都会执行一次。一旦每个片元的颜色确定了,OpenGL就会把它们写入到帧缓冲区中。

6.顶点着色器

         • ⼀般⽤来处理图形每个顶点变换(旋转/平移/投影等)                     

        • 顶点着⾊器是OpenGL中⽤于计算顶点属性的程序。顶点着⾊器是逐顶点运算的程序,也就是说每个顶点数据都会执⾏⼀次顶点着⾊器,当然这是并⾏的,并且顶点着⾊器运算过程中⽆法访问其他顶点的数据

        • ⼀般来说典型的需要计算的顶点属性主要包括顶点坐标变换、逐顶点光照运算等等。顶点坐标由⾃身坐标系转换到归⼀化坐标系的运算,就是在这⾥发⽣的。

7.片元着色器(片段着色器)

        ⼀般⽤来处理图形中每个像素点颜⾊计算和填充⽚段着⾊器是OpenGL中⽤于计算⽚段(像素)颜⾊的程序。⽚段着⾊器是逐像素运算的程序,也就是说每个像素都会执⾏⼀次⽚段着⾊器,当然也是并⾏的

8.光栅化Rasterization 

        • 是把顶点数据转换为⽚元的过程,具有将图转化为⼀个个栅格组成的图象的作⽤,特点是每个元素对应帧缓冲区中的⼀像素。

        • 光栅化就是把顶点数据转换为⽚元的过程。⽚元中的每⼀个元素对应于帧缓冲区中的⼀个像素。

        • 光栅化其实是⼀种将⼏何图元变为⼆维图像的过程。该过程包含了两部分的⼯作。第⼀部分⼯作:决定窗⼝坐标中的哪些整型栅格区域被基本图元占⽤;第⼆部分⼯作:分配⼀个颜⾊值和⼀个深度值到各个区域。光栅化过程产⽣的是⽚元

        • 把物体的数学描述以及与物体相关的颜⾊信息转换为屏幕上⽤于对应位置的像素及⽤于填充像素的颜⾊,这个过程称为光栅化,这是⼀个将模拟信号转化为离散信号的过程

9.纹理

        纹理可以理解为图⽚. ⼤家在渲染图形时需要在其编码填充图⽚,为了使得场景更加逼真.⽽这⾥使⽤的图⽚,就是常说的纹理.但是在OpenGL,我们更加习惯叫纹理,⽽不是图⽚

10.混合(Blending)

        在测试阶段之后,如果像素依然没有被剔除,那么像素的颜⾊将会和帧缓冲区中颜⾊附着上的颜⾊进⾏混合,混合的算法可以通过OpenGL的函数进⾏指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,⼀般可以通过像素着⾊器进⾏实现,当然性能会⽐原⽣的混合算法差⼀些,个人理解有点像iOS给RGB中红,绿,蓝设置不同的值得到不同的颜色,只是这里是操作片元着色器,来达到不同的显示。

11.变换矩阵(Transformation)/投影矩阵Projection 

        在iOS核心动画中我们也会和矩阵打交到,变换矩阵顾名思义就是对图像/图形的放大/缩小/平移/选装等座处理。

        投影矩阵就是⽤于将3D坐标转换为⼆维屏幕坐标,实际线条也将在⼆维坐标下进⾏绘制    

12.渲染上屏/交换缓冲区(SwapBuffer)     

    • 渲染缓冲区⼀般映射的是系统的资源⽐如窗⼝。如果将图像直接渲染到窗⼝对应的渲染缓冲区,则可以将图像显示到屏幕上。

    • 但是,值得注意的是,如果每个窗⼝只有⼀个缓冲区,那么在绘制过程中屏幕进⾏了刷新,窗⼝可能显示出不完整的图像

    • 为了解决这个问题,常规的OpenGL程序⾄少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在⼀个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。

    • 由于显示器的刷新⼀般是逐⾏进⾏的,因此为了防⽌交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换⼀般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进⾏交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步

    • 使⽤了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进⾏下⼀帧的渲染,使得帧率⽆法完全达到硬件允许的最⾼⽔平。为了解决这个问题,引⼊了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,⽽垂直同步发⽣时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利⽤硬件性能的⽬的

13.坐标系

      OpenGl常见的坐标系有:

        1. Object or model coordinates(物体或模型坐标系)每一个实物都有自己的坐标系,在高中数学中,以自身建立的坐标系,自身坐标系由世界坐标系平移而来

        2. World coordinates(世界坐标系)个人理解为地球相对自己建立的坐标系,地球上所有生物都处于这个坐标系当中

        3. Eye (or Camera) coordinates(眼(或相机)坐标系)

        4. Normalized device coordinates(标准化的设备坐标系)

        5. Window (or screen) coordinates(.窗口(或屏幕)坐标系)个人理解为iOS下

        6.Clip coordinates(裁剪坐标系)主要作用是当图形/图像超出时,按照这个坐标系裁剪,裁剪好之后转换到screen坐标系

14.正投影/透视投影

        正投影:类似于照镜子,1:1形成图形大小,这里不做重点讲解

        透视投影:在OpenGL中,如果想对模型进行操作,就要对这个模型的状态(当前的矩阵)乘上这个操作对应的一个矩阵.如果乘以变换矩阵(平移, 缩放, 旋转), 那相乘之后, 模型的位置被变换;如果乘以投影矩阵(将3D物体投影到2D平面), 相乘后, 模型的投影方式被设置;如果乘以纹理矩阵(), 模型的纹理方式被设置.而用来指定乘以什么类型的矩阵, 就是glMatriMode(GLenummode);glMatrixMode有3种模式: GL_PROJECTION 投影, GL_MODELVIEW 模型视图, GL_TEXTURE 纹理.所以,在操作投影矩阵以前,需要调用函数:glMatrixMode(GL_PROJECTION); //将当前矩阵指定为投影矩阵然后把矩阵设为单位矩阵






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

收起阅读 »

一文速览苹果WWDC 2021:没有硬件发布的夜晚,iOS 15才是主角

WWDC 2021在成功把M1芯片置入到了iPad Pro之后,我们最关心的另一个问题是,iPad Pro是否能有足够的软件生态来最大程度的利用好这颗高性能核心。当你带着这样的期待去收看这届的WWDC 2021之时,你会发现自己的全部期待都落了空——iPadO...
继续阅读 »

WWDC 2021

在成功把M1芯片置入到了iPad Pro之后,我们最关心的另一个问题是,iPad Pro是否能有足够的软件生态来最大程度的利用好这颗高性能核心。

当你带着这样的期待去收看这届的WWDC 2021之时,你会发现自己的全部期待都落了空——iPadOS并没有得到给力的软件生态支持,并且外界谣传的14英寸版的MacBook Pro也并没有登场。

这次的WWDC 2021总结起来,就是三个关键词:共享、统一与隐私。

iOS 15:更注重分享,也更注重你的「数字健康」

视频通话变得越来越重要,苹果也为自家iOS 15加入了语音突显模式和宽频谱模式。前者可使用机器学习降低环境噪音,增强人声;后者将捕捉周围一切的声音,可以理解为没有经过通话降噪的原声。




Share Play

当然,比起音频增强,更多人关心的是「人像模式」——在使用FaceTime之时,iPhone不仅可以帮助你虚化掉背景,更为重要的是它居然可以帮助你进行实时美颜,当然仅限于在FaceTime通话中。

好在现在的FaceTime已经支持网页接入了,换句话来说,就是除了苹果设备之外,Windows设备和Android手机也能够通过苹果用户分享的链接加入到FaceTime通话中了。

一旦接受了这种设定,你就会发现苹果有多重视「与朋友/家人共享」这件事情了。这里苹果推出了功能,也是这次全系统更新的核心功能——SharePlay。它可让用户在FaceTime通话时,共享音乐、视频以及屏幕。

有了这个功能,你就可以像使用钉钉/飞书/腾讯会议等等一系列的协同类App一样,与同事协同工作,与家人一同刷剧,与朋友一同打游戏

分享不止于此,在苹果的官方信息应用iMessage中,现在新加入了分享Apple Music中的音乐,Apple News中的文章等等功能。





专注模式

为了给你的现实生活和数字生活划上一道界线,iOS 15终于加入了专注模式。这次专注模式,笔者认为是此前「睡眠模式」的延伸——如果说睡眠模式是屏蔽掉一切通知消息,那么专注模式就是可选择性的屏蔽。

你可以设置不同的专注模式,iOS 15会帮你筛选相应的信息。比如,工作模式下,你就只能收到钉钉/微信的消息,而游戏和视频类App的推送就会被忽略掉,并且iOS 15会通过算法判断,哪一项消息更重要,并且将之置顶显示,以避免你错过重要信息。

当然,你也可以自定义不同「专注页面」,在开启相应的专注模式之后,iOS设备就会自动显示相对应的页面。



iOS 15新功能

每一年的iOS系统升级,同样会伴随大量的系统应用升级,这次也不例外


Text Live

今年的相机和图库功能的升级方面主要是体现在,对于AI算法的利用层面上。新增的Text Live功能,它可以识别拍摄/现有图片中的文本,不仅能够转换文字,还能够进行翻译,首发支持英语、汉语、法语等七种语言。

图库中的「回忆」功能再次升级,这次用户可以自定义回忆功能,包括音乐、动画、主题等等。系统也可根据照片的内容和风格,自动匹配合适的歌曲、节奏以及呈现的效果。

钱包功能也得到了升级:这次它不仅能添加信用卡和公交卡,它还支持模拟酒店门卡,迪士尼公园门票,甚至是电子sfz。目前尚不清楚,它能否替代掉你的小区门禁卡。

天气和地图应用的更新升级,则更多的体现在视觉动效的呈现上面:不同的天气会有不同的动画效果,海外部分城市的地图,支持查看海拔高度、地标景点、道路细节等。新增的公交模式,可帮助用户尽快找到附近的公交站。

另外,值得一提的是移动端的Safari现在也支持安装浏览器拓展插件了,并且新加入了「标签组」功能——这一功能与微软推出的Edge浏览器的「集锦」功能类似。

iOS 15还为健康应用带来了一些新功能,允许用户与医疗团队共享数据,评估跌倒风险的指标,以及趋势分析等等。此外还可以将健康数据与家庭成员共享,让关心你的人第一时间了解你的身体状态。



AirPods升级

顺便一提,AirPods(主要是AirPods Pro和AirPods Max)也得到了小幅度的功能升级,比如新增了对话增强模式,利用计算音频和波束成形麦克风,AirPods 可实现更清晰的对话;新增了通知播报功能,AirPods 可自动阅读具有时效性的通知内容;以及新增了和AirTag类似的防丢功能。

简单来说,如果AirPods遗失在外,其会自动发出蓝牙信号,路过的iPhone识别到上传到iCloud,直达用户的「查找app」。至于有些音乐发烧友期待的更高清的码率更新并没有到来,Apple Music也只是新增了Dolby Atmos音效。



OS 15支持的设备

令人意外的是,iOS 15支持的机型与 iOS 14基本一致。iPhone 6s、第一代iPhone SE也可升级。开发者预览版现在已经开始推送更新了,至于公测版则在7月份,也就是下个月开始推送更新,正式版会在秋季发布会之后更新。

iPadOS:你要的Mac级应用并未出现

iPadOS大部分的新功能与iOS 15一样,不过苹果还是为大屏幕新增了一些独有功能,比如说更大尺寸的小组件——现在小组件终于能够与App图标混排了。





iPadOS升级一览

借助这一功能,你可以在iPad上打造出更个性化的页面,比如游戏页面,追剧页面等等,同时iPadOS也终于加入了和iOS一样的App资源库的功能。

同时,iPadOS终于更新分屏操作的逻辑:新增了「多任务控制板」和「App组合架」的操作逻辑。通过多任务控制板,你不仅可以双开应用,甚至可以「三开」——就像之前华为的「智慧分屏」功能一样,拥有第三个悬浮的浏览页面。


多任务新特性一览

同时,你还能够将不同的分屏页面「放」在App组合架上,便于你在多个不同的分屏应用之间快速切换。你可以通过拖动应用程序来创建一个新的分屏视图,比传统的多任务还要方便。而这些操作,也都可以借助iPad妙控键盘用快捷键实现。

苹果也对iPadOS上的备忘录功能进行了升级:你可以在任意应用的角落里,通过手指/Apple Pencil滑动呼出备忘录小窗,快速记录包括手写笔记、连接、Safari 高亮内容、便签等等任何一闪而过的灵感。

快速笔记也是支持多设备同步的,例如你在 Safari 中对某段文字做了备注,当你再次浏览时,便会出现快速笔记的缩略图,将你带回之前浏览过的内容。

iPadOS上Swift Playgrounds的更新,可能是这次唯一称得上是与生产力挂钩的升级了。Swift Playgrounds是苹果推出的可视化的编程操作App。这次的更新允许用户直接在Swift Playgrounds中开发App,并且进行调试甚至是直接上架到App Store进行销售。

尽管与Mac采用同一种M1芯片的iPad Pro已经推出,但iPadOS的升级更多的是「适配更大屏幕的iOS」的逻辑,而非是想要将iPadOS打造成更强生产力,能让它取代掉Mac。这还是让笔者有些失望。

watchOS&macOS:小幅度升级,跨设备交互功能亮眼

今年的watchOS更新还是从两个层面上:一是新增了「照片表盘」功能,你可以将任意图片设置成表盘,这张图片是具备景深效果的,你可以通过表冠来调节虚化效果。




watchOS新特性一览

二是在健康应用层面上,watchOS为「呼吸」功能新增了更漂亮的动画,让「睡眠」除了能记录你的睡眠时长之外,还能记录下你的睡眠呼吸频率,从而分析出你的睡眠质量。最后订阅服务,Apple Fitness+则是增加了两种热门体能训练——太极和普拉提。

新的macOS被命名为Monterey,源自加州的蒙特雷市。新功能与iOS保持一致,但拥有足以改变多设备交互方式的Universal Control功能。


Universal Control

简单来说,通过Universal Control,你能在靠近的不同苹果设备之间共享一套键鼠,并且能够在不同设备之间快速共享文件。比如,你可以通过MacBook上的键盘和触控板,修改iPad上的图片/文稿等等,并且可以直接将文字/图片拖动到当前Mac编辑的文稿/剪辑的视频时间线之中。



macOS新特性一览

此次更新中,最让笔者兴奋的功能是,AirPlay to Mac——你终于能够把移动端的内容通过AirPlay的方式直接投屏到Mac上,通过Mac的大屏和更棒的扬声器,享受更舒适的视听体验了。

最后是iOS上的快捷指令功能被移植到了macOS之上,你终于能够通过自动化的指令,在Mac电脑上名正言顺地「偷懒」了。

隐私:从在世到离世,苹果都在为你的隐私考虑

隐私保护一直是苹果极为重视的方面,这次的多系统更新也一样:这次苹果为原生的「邮件」App新增了隐私保护功能,它不仅能够隐藏你的IP地址,还能隐藏你打开邮件的动作,以确保送信者无法得知你何时,甚至是否打开了邮件。

此前,苹果为iOS设备增加了更多的设备权限管理功能,这次则是新增了App隐私报告。你可以透过它很直观地看到哪些应用使用了相关隐私权限的次数和时间等数据。

Siri增加了语音识别功能。默认设置下,发给Siri的对话将在设备本地处理,不上传至云端。这也意味着Siri可以在离线状态下完成更多的指令,比如打开某个应用,设置提醒/闹钟等等。

为了保护用户的隐私,原有的iCloud业务也升级成了iCloud+:在浏览网页之时,用户可以通过iCloud建立一条加密的链接,实现更安全的访问。

iCloud+还可以给用户生成随机的电子邮件地址,并转发到用户的收件箱。所以在网上填写表格或新用户注册时,不必输入个人真实的电子邮箱。

此外,苹果还新增了「数字遗产计划」:用户可以自行定义遗产联系人,万一用户不幸离世之后,透过这项功能,该联系人可以申请访问离世用户的iCloud数据。

iCloud升级成了iCloud+,但其订阅价格并未改变:50GB存储空间每月付费6元,并且支持一个HomeKit安全摄像头(监控视频无限存储空间);200GB存储空间每月付费21元,支持最多五个HomeKit安全摄像头;2TB存储空间每月付费68元,支持无上限个数的HomeKit安全摄像头。

写在最后:它既是连接数字生活的纽带,也是分割现实生活的界线

每一届的WWDC都会带来苹果设备的系统级更新,而每一次的更新都会让苹果生态系统内的设备关系更加紧密,尤其是随着M1芯片的推出以及在不同平特设备上的应用(Mac和iPad Pro)。

这种密切的联系不仅仅是多设备之间的协同,更是不同设备之间同一种交互逻辑,同一种应用功能和界面。从这次的SharePlay功能和FaceTime跨平台支持的功能来看,苹果不仅想要牢牢绑定现有生态内的用户,还想要拉入其他平台的用户进来,体验苹果生态带来的统一性。

当然,这些更新中最有意思的,还是苹果对于科技与生活的理解:在你使用苹果设备之时,它不仅在意用户的数字隐私,也在意用户的身体健康。

在iOS 15公测版推送更新之后,笔者也将会第一时间给各位读者带来最新的体验。


作者/唐植潇

本文首发钛媒体APP

原地址:https://baijiahao.baidu.com/s?id=1701962186485997583&wfr=spider&for=pc



收起阅读 »

面试官问我:如何使用LeakCanary排查Android中的内存泄露,看我如何用漫画装逼!

1)在项目的build.gradle文件添加: debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' releaseCompile 'com.squareup.leakc...
继续阅读 »



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述

1)在项目的build.gradle文件添加:


    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'

可以看到,debugCompile跟releaseCompile 引入的是不同的包, 在 debug 版本上,集成 LeakCanary 库,并执行内存泄漏监测,而在 release 版本上,集成一个无操作的 wrapper ,这样对程序性能就不会有影响。


2)在Application类添加:


public class LCApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}

LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。


如果是简单的检测activity是否存在内存泄漏,上面两个步骤就可以了,是不是很简单。 那么当某个activity存在内存泄漏的时候,会有什么提示呢?LeakCanary会自动展示一个通知栏,点开提示框你会看到引起内存溢出的引用堆栈信息。




这里写图片描述



在这里插入图片描述

具体使用代码


1)Application 相关代码:


public class LCApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}

}

2)泄漏的activity类代码:


public class MainActivity extends Activity {

private Button next;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

next = (Button) findViewById(R.id.next);
next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
finish();
}
});
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("=================");
}
}
}).start();
}
}

当点击next跳到第二个界面后,LeakCanary会自动展示一个通知栏,点开提示框你会看到引起内存溢出的引用堆栈信息,如上图所示,这样你就很容易定位到原来是线程引用住当前activity,导致activity无法释放。



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述

上面提到,LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。现在很多app都使用到了fragment,那fragment如何检测呢。


1)Application 中获取到refWatcher对象。


public class LCApplication extends Application {

public static RefWatcher refWatcher;

@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
refWatcher = LeakCanary.install(this);
// Normal app init code...
}
}

2)使用 RefWatcher 监控 Fragment:


public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = LCApplication.refWatcher;
refWatcher.watch(this);
}
}

这样则像监听activity一样监听fragment。其实这种方式一样适用于任何对象,比如图片,自定义类等等,非常方便。




在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述

LeakCanary.install(this)源码如下所示:


public static RefWatcher install(Application application) {
return ((AndroidRefWatcherBuilder)refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build())).buildAndInstall();
}

listenerServiceClass(DisplayLeakService.class):用于分析内存泄漏结果信息,然后发送通知给用户。 excludedRefs(AndroidExcludedRefs.createAppDefaults().build()):设置需要忽略的对象,比如某些系统漏洞不需要统计。 buildAndInstall():真正检测内存泄漏的方法,下面将展开分析该方法。


public RefWatcher buildAndInstall() {
RefWatcher refWatcher = this.build();
if(refWatcher != RefWatcher.DISABLED) {
LeakCanary.enableDisplayLeakActivity(this.context);
ActivityRefWatcher.installOnIcsPlus((Application)this.context, refWatcher);
}

return refWatcher;
}

可以看到,上面方法主要做了三件事情: 1.实例化RefWatcher对象,该对象主要作用是检测是否有对象未被回收导致内存泄漏; 2.设置APP图标可见; 3.检测内存



在这里插入图片描述



在这里插入图片描述

RefWatcher的使用后面讲,这边主要看第二件事情的处理过程,及enableDisplayLeakActivity方法的源码


public static void enableDisplayLeakActivity(Context context) {
LeakCanaryInternals.setEnabled(context, DisplayLeakActivity.class, true);
}

public static void setEnabled(Context context, final Class<?> componentClass, final boolean enabled) {
final Context appContext = context.getApplicationContext();
executeOnFileIoThread(new Runnable() {
public void run() {
LeakCanaryInternals.setEnabledBlocking(appContext, componentClass, enabled);
}
});
}

public static void setEnabledBlocking(Context appContext, Class<?> componentClass, boolean enabled) {
ComponentName component = new ComponentName(appContext, componentClass);
PackageManager packageManager = appContext.getPackageManager();
int newState = enabled?1:2;
packageManager.setComponentEnabledSetting(component, newState, 1);
}

可见,最后调用packageManager.setComponentEnabledSetting()方法,实现应用图标的隐藏和显示。



在这里插入图片描述



在这里插入图片描述

接下来,进入真正的内存检查的方法installOnIcsPlus()


public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
if(VERSION.SDK_INT >= 14) {
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
activityRefWatcher.watchActivities();
}
}

该方法实例化出ActivityRefWatcher 对象,该对象用来监听activity的生命周期,具体实现如下所示:


public void watchActivities() {
this.stopWatchingActivities();
this.application.registerActivityLifecycleCallbacks(this.lifecycleCallbacks);
}

private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}

public void onActivityStarted(Activity activity) {
}

public void onActivityResumed(Activity activity) {
}

public void onActivityPaused(Activity activity) {
}

public void onActivityStopped(Activity activity) {
}

public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}

public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
};



在这里插入图片描述



在这里插入图片描述

调用了registerActivityLifecycleCallbacks方法后,当Activity执行onDestroy方法后,会触发ActivityLifecycleCallbacks 的onActivityDestroyed方法,在当前方法中,调用refWatcher的watch方法,前面已经讲过RefWatcher对象主要作用是检测是否有对象未被回收导致内存泄漏。下面继续看refWatcher的watch方法源码:


public void watch(Object watchedReference) {
this.watch(watchedReference, "");
}

public void watch(Object watchedReference, String referenceName) {
if(this != DISABLED) {
Preconditions.checkNotNull(watchedReference, "watchedReference");
Preconditions.checkNotNull(referenceName, "referenceName");
long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
this.retainedKeys.add(key);
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
this.ensureGoneAsync(watchStartNanoTime, reference);
}
}

可以看到,上面方法主要做了三件事情: 1.生成一个随机数key存放在retainedKeys集合中,用来判断对象是否被回收; 2.把当前Activity放到KeyedWeakReference(WeakReference的子类)中; 3.通过查找ReferenceQueue,看该Acitivity是否存在,存在则证明可以被正常回收,不存在则证明可能存在内存泄漏。 前两件事很简单,这边主要看第三件事情的处理过程,及ensureGoneAsync方法的源码:


private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}

Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
this.removeWeaklyReachableReferences();
if(this.debuggerControl.isDebuggerAttached()) {
return Result.RETRY;
} else if(this.gone(reference)) {
return Result.DONE;
} else {
this.gcTrigger.runGc();
this.removeWeaklyReachableReferences();
if(!this.gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();
if(heapDumpFile == HeapDumper.RETRY_LATER) {
return Result.RETRY;
}

long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
}

return Result.DONE;
}
}

该方法中首先执行removeWeaklyReachableReferences(),从ReferenceQueue队列中查询是否存在该弱引用对象,如果不为空,则说明已经被系统回收了,则将对应的随机数key从retainedKeys集合中删除。


 private void removeWeaklyReachableReferences() {
KeyedWeakReference ref;
while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
this.retainedKeys.remove(ref.key);
}
}

然后通过判断retainedKeys集合中是否存在对应的key判断该对象是否被回收。


private boolean gone(KeyedWeakReference reference) {
return !this.retainedKeys.contains(reference.key);
}

如果没有被系统回收,则手动调用gcTrigger.runGc();后再调用removeWeaklyReachableReferences方法判断该对象是否被回收。


GcTrigger DEFAULT = new GcTrigger() {
public void runGc() {
Runtime.getRuntime().gc();
this.enqueueReferences();
System.runFinalization();
}

private void enqueueReferences() {
try {
Thread.sleep(100L);
} catch (InterruptedException var2) {
throw new AssertionError();
}
}
};

第三行代码为手动触发GC,紧接着线程睡100毫秒,给系统回收的时间,随后通过System.runFinalization()手动调用已经失去引用对象的finalize方法。 通过手动GC该对象还不能被回收的话,则存在内存泄漏,调用heapDumper.dumpHeap()生成.hprof文件目录,并通过heapdumpListener回调到analyze()方法,后面关于dump文件的分析这边就不介绍了,感兴趣的可以自行去看。



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述








作者:天才少年_
链接:https://juejin.cn/post/6844904165265670157
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。




收起阅读 »

JAVA开发MQTT程序总结

JAVA开发MQTT总结MQTT 介绍它是一种 机器之间通讯 machine-to-machine (M2M)、物联网 Internet of Things (IoT)常用的一种轻量级消息传输协议适用于网络带宽较低的场合包含发布、订阅模式,通过一个代理服务器(...
继续阅读 »

JAVA开发MQTT总结

MQTT 介绍

  • 它是一种 机器之间通讯 machine-to-machine (M2M)、物联网 Internet of Things (IoT)常用的一种轻量级消息传输协议
  • 适用于网络带宽较低的场合
  • 包含发布、订阅模式,通过一个代理服务器(broker),任何一个客户端(client)都可以订阅或者发布某个主题的消息,然后订阅了该主题的客户端则会收到该消息

mqtt还是之前公司有需求所以写的一个demo,在这里记录下来,方便有人使用的时候查阅,不涉及mqtt的具体讲解,只是贴代码和运行过程。

MQTT的入门,以及特性,协议,结构的讲解,请看下面这篇文章

www.runoob.com/w3cnote/mqt…

什么是MQTT,它能干什么,它的应用场景在哪里?请参考下面这篇文章

www.ibm.com/developerwo…

本文中采用的MQTT服务器Apache-Apollo的下载配置搭建过程,请参考下面这篇文章

blog.csdn.net/qq_29350001…

下面就开始创建broker,

RaindeMacBook-Pro:bin rain$ ./apollo create mybroker
Creating apollo instance at: mybroker
Generating ssl keystore...

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore keystore -destkeystore keystore -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

You can now start the broker by executing:

"/Users/rain/Documents/Soft/apache-apollo-1.7.1/bin/mybroker/bin/apollo-broker" run

Or you can run the broker in the background using:

"/Users/rain/Documents/Soft/apache-apollo-1.7.1/bin/mybroker/bin/apollo-broker-service" start


进入新生成的broker中

RaindeMacBook-Pro:bin rain$ ls
apollo apollo.cmd mybroker testbroker
RaindeMacBook-Pro:bin rain$ cd mybroker/
RaindeMacBook-Pro:mybroker rain$ ls
bin data etc log tmp
RaindeMacBook-Pro:mybroker rain$ cd bin
RaindeMacBook-Pro:bin rain$ ls
apollo-broker apollo-broker-service

可以看到有两个文件,启动apollo-broker

启动成功以后,就可以在浏览器中访问了,默认用户名和密码是admin,password

刚进去是,Topics选项卡是空的,我是在运行程序后截图的,所以有一个topic列表

配置Maven

在pom.xml中添加以下配置

<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>

再创建下面的类

MqttServer

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MqttServer2 {
/**
* 代理服务器ip地址
*/
public static final String MQTT_BROKER_HOST = "tcp://127.0.0.1:61613";

/**
* 订阅标识
*/
public static final String MQTT_TOPIC = "test2";

private static String userName = "admin";
private static String password = "password";

/**
* 客户端唯一标识
*/
public static final String MQTT_CLIENT_ID = "android_server_xiasuhuei32";
private static MqttTopic topic;
private static MqttClient client;

public static void main(String... args) {
// 推送消息
MqttMessage message = new MqttMessage();
try {
client = new MqttClient(MQTT_BROKER_HOST, MQTT_CLIENT_ID, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setUserName(userName);
options.setPassword(password.toCharArray());
options.setConnectionTimeout(10);
options.setKeepAliveInterval(20);

topic = client.getTopic(MQTT_TOPIC);

message.setQos(1);
message.setRetained(false);
message.setPayload("message from server222222".getBytes());
client.connect(options);

while (true) {
MqttDeliveryToken token = topic.publish(message);
token.waitForCompletion();
System.out.println("已经发送222");
Thread.sleep(10000);
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

MqttClient

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MyMqttClient {
/**
* 代理服务器ip地址
*/
public static final String MQTT_BROKER_HOST = "tcp://127.0.0.1:61613";

/**
* 客户端唯一标识
*/
public static final String MQTT_CLIENT_ID = "android_xiasuhuei321";

/**
* 订阅标识
*/
// public static final String MQTT_TOPIC = "xiasuhuei321";

/**
*
*/
public static final String USERNAME = "admin";
/**
* 密码
*/
public static final String PASSWORD = "password";
public static final String TOPIC_FILTER = "test2";

private volatile static MqttClient mqttClient;
private static MqttConnectOptions options;

public static void main(String... args) {
try {
// host为主机名,clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,
// MemoryPersistence设置clientid的保存形式,默认为以内存保存

mqttClient = new MqttClient(MQTT_BROKER_HOST, MQTT_CLIENT_ID, new MemoryPersistence());
// 配置参数信息
options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,
// 这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(true);
// 设置用户名
options.setUserName(USERNAME);
// 设置密码
options.setPassword(PASSWORD.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 连接
mqttClient.connect(options);
// 订阅
mqttClient.subscribe(TOPIC_FILTER);
// 设置回调
mqttClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable throwable) {
System.out.println("connectionLost");
}

@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
System.out.println("Topic: " + s + " Message: " + mqttMessage.toString());
}

@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {

}
});
} catch (Exception e) {
e.printStackTrace();
}

}

}

PublishSample

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

/**
*发布端
*/
public class PublishSample {
public static void main(String[] args) {

String topic = "test2";
String content = "hello 哈哈";
int qos = 1;
String broker = "tcp://127.0.0.1:61613";
String userName = "admin";
String password = "password";
String clientId = "pubClient";
// 内存存储
MemoryPersistence persistence = new MemoryPersistence();

try {
// 创建客户端
MqttClient sampleClient = new MqttClient(broker, clientId, persistence);
// 创建链接参数
MqttConnectOptions connOpts = new MqttConnectOptions();
// 在重新启动和重新连接时记住状态
connOpts.setCleanSession(false);
// 设置连接的用户名
connOpts.setUserName(userName);
connOpts.setPassword(password.toCharArray());
// 建立连接
sampleClient.connect(connOpts);
// 创建消息
MqttMessage message = new MqttMessage(content.getBytes());
// 设置消息的服务质量
message.setQos(qos);
// 发布消息
sampleClient.publish(topic, message);
// 断开连接
sampleClient.disconnect();
// 关闭客户端
sampleClient.close();
} catch (MqttException me) {
System.out.println("reason " + me.getReasonCode());
System.out.println("msg " + me.getMessage());
System.out.println("loc " + me.getLocalizedMessage());
System.out.println("cause " + me.getCause());
System.out.println("excep " + me);
me.printStackTrace();
}
}
}

SubscribeSample

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

/**
*订阅端
*/
public class SubscribeSample {

public static void main(String[] args) throws MqttException {
String HOST = "tcp://127.0.0.1:61613";
String TOPIC = "test2";
int qos = 1;
String clientid = "subClient";
String userName = "admin";
String passWord = "password";
try {
// host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
MqttClient client = new MqttClient(HOST, clientid, new MemoryPersistence());
// MQTT的连接设置
MqttConnectOptions options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(true);
// 设置连接的用户名
options.setUserName(userName);
// 设置连接的密码
options.setPassword(passWord.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 设置回调函数
client.setCallback(new MqttCallback() {

public void connectionLost(Throwable cause) {
System.out.println("connectionLost");
}

public void messageArrived(String topic, MqttMessage message) throws Exception {
System.out.println("topic:"+topic);
System.out.println("Qos:"+message.getQos());
System.out.println("message content:"+new String(message.getPayload()));

}

public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("deliveryComplete---------"+ token.isComplete());
}

});
client.connect(options);
//订阅消息
client.subscribe(TOPIC, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
}

启动程序

1.启动MqttServer2以后,开始循环发送消息。

2.启动MyMqttClient开始接收消息。

到这里,整个程序基本可以运行。

3.启动PublishSample,发布一条消息,在启动SubscribeSample来订阅发布的消息。

4.发布的消息在MyMqttClient中也会显示出来

作者:魏小雨
链接:https://juejin.cn/post/6844904071367753735
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

MQTT在Android端的使用详解以及MQTT服务器搭建、Paho客户端使用

前言最近的项目中使用了MQTT来接收后端推送过来的一些数据,这篇文章来介绍下Android端如何集成使用,关于MQTT相关介绍将不再阐述。由于光写代码不实践的接收下数据很难验证我们写的是否正确,所以我将简单介绍下如何配置个MQTT服务端,并使用工具来发送数据到...
继续阅读 »

前言

最近的项目中使用了MQTT来接收后端推送过来的一些数据,这篇文章来介绍下Android端如何集成使用,关于MQTT相关介绍将不再阐述。

由于光写代码不实践的接收下数据很难验证我们写的是否正确,所以我将简单介绍下如何配置个MQTT服务端,并使用工具来发送数据到服务端,使得手机端能接收到数据。话不多说直接看。

1. MQTT服务器配置

1.1 下载EMQX

下载地址

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

1.2 启动EMQX

在解压后的bin目录下打开cmd命令,输入emqx.cmd start即可启动。

如果你在启动时遇到could't load module...,那就是因为你的路径中包含中文名导致启动不了,将该文件夹放到纯英文目录下即可启动。 在这里插入图片描述

完事后在浏览器内输入http://127.0.0.1:18083即可打开web管理界面,帐号为admin,密码为public

按如图方式将语言改为中文 在这里插入图片描述

1.3 界面说明

左侧的Clients标签下可以看到当前连接的客户端 在这里插入图片描述 左侧的Topics标签下可以看到当前订阅的主题 在这里插入图片描述

1.4 个人理解

到这服务端就算是配置完成了,你可能会问,服务端就是这,那我手机客户端怎么接收消息呢,服务端从哪里发送消息呢?其实EMQX服务是消息中间件服务,有点像是转发。一个客户端发送消息并指定主题,该消息发送到服务端,那么连接了服务端并且订阅了该主题的所有客户端就都能接收到该消息,所以我们手机客户端想要接收到消息,还需要有一端来给EMQX服务端来发送消息才行。

2. MQTT客户端软件 Paho

2.1 下载MQTT客户端软件

下载地址 在这里插入图片描述

下载勾选中的那个文件即可,下载完后解压得到paho.exe,即我们需要的客户端软件。

2.2 MQTT客户端使用

2.2.1 连接服务器

在这里插入图片描述

按如图所示步骤进行点击,1、新增一个连接,2、填写服务器地址和客户标识,这里的标识为自己定义的,服务器地址可在该地址那查看,可以看到是本地地址,端口号是1883或者11883 点击连接后可以看到连接状态变为已连接,就代表我们客户端已经连接到了EMQX。 在这里插入图片描述

2.2.2 发送消息

在这里插入图片描述

在1处填写主题名,2处填写消息然后3处点击发布,然后可以看到4处显示已发布,代表我们已经发送到服务端了。

2.2.3 订阅主题

订阅我们刚才发送消息的那个主题

在这里插入图片描述

点击1处来新增订阅,点击2处输入我们要订阅的主题,这里我们设置为刚才发布消息的那个主题,然后点击3处的订阅,可以看到历史记录那里显示已订阅。

接下来我们再发送一次该主题消息,观察历史记录

在这里插入图片描述

可以看到,当我们发布后,由于我们订阅了该主题,所以就接收到了该主题消息。

在MQTT服务端配置完成以及MQTT客户端软件测试可行后,现在来看我们的安卓端如何订阅并接收消息。

3. Andoird端集成使用

3.1 添加依赖、权限等配置

//MQTT
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'

AndroidManifest文件配置

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myfittinglife.mqttdemo">

<!--必要的三个权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application
...>
...
<!--添加该Service-->
<service android:name="org.eclipse.paho.android.service.MqttService"/>
</application>

3.2 使用

3.2.1 创建MqttAndroidClient对象

var mClient: MqttAndroidClient? = null

private fun createClient() {

//1、创建接口回调
//以下回调都在主线程中(如果使用MqttClient,使用此回调里面的都是非主线程)
val mqttCallback: MqttCallbackExtended = object : MqttCallbackExtended {
override fun connectComplete(reconnect: Boolean, serverURI: String) {
//连接成功
Log.i(TAG, "connectComplete: ")
showToast("连接成功")
}

override fun connectionLost(cause: Throwable) {
//断开连接
Log.i(TAG, "connectionLost: ")
showToast("断开连接")

}

@Throws(Exception::class)
override fun messageArrived(topic: String, message: MqttMessage) {
//得到的消息
var msg = message.payload
var str = String(msg)
Log.i(TAG, "messageArrived: $str")
showToast("接收到的消息为:$str")

}

override fun deliveryComplete(token: IMqttDeliveryToken) {
//发送消息成功后的回调
Log.i(TAG, "deliveryComplete: ")
showToast("发送成功")

}
}

//2、创建Client对象
try {
mClient = MqttAndroidClient(this, "tcp://192.168.14.57:1883", "客户端名称,可随意")
mClient?.setCallback(mqttCallback) //设置回调函数
} catch (e: MqttException) {
Log.e(TAG, "createClient: ", e)
}
}

3.2.2 设置MQTT连接的配置信息

val mOptions = MqttConnectOptions()
mOptions.isAutomaticReconnect = false //断开后,是否自动连接
mOptions.isCleanSession = true //是否清空客户端的连接记录。若为true,则断开后,broker将自动清除该客户端连接信息
mOptions.connectionTimeout = 60 //设置超时时间,单位为秒
//mOptions.userName = "Admin" //设置用户名。跟Client ID不同。用户名可以看做权限等级
//mOptions.setPassword("Admin") //设置登录密码
mOptions.keepAliveInterval = 60 //心跳时间,单位为秒。即多长时间确认一次Client端是否在线
mOptions.maxInflight = 10 //允许同时发送几条消息(未收到broker确认信息)
mOptions.mqttVersion = MqttConnectOptions.MQTT_VERSION_3_1_1 //选择MQTT版本

3.2.3 建立连接

try {
mClient?.connect(mOptions, this, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
Log.i(TAG, "onSuccess:连接成功 ")
}

override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
Log.i(TAG, "onFailure: " + exception?.message)
}

})
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ", e)
}

3.2.4 订阅主题

//设置监听的topic
try {
mClient?.subscribe("topicName", 0)
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ", e)
}

3.2.5 发送消息

try {
var str = "要发送的消息"
var msg = MqttMessage()
msg.payload =str.toByteArray()
mClient?.publish(Const.Subscribe.mTopic,msg)
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ",e )
}

3.3 最终效果

在我们的Paho MQTT Utility软件发送消息后,我们的手机端由于订阅了该主题,所以就可以接收到该消息。 在这里插入图片描述

4. 注意事项

  • 别忘记在manifest中添加service,否则在connect()的时候会报mClient为空。

    <service android:name="org.eclipse.paho.android.service.MqttService"/>
  • 别忘记添加localbroadcastmanager依赖,否则会报Failed resolution of: Landroidx/localbroadcastmanager/content/LocalBroadcastManager错误。

    implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
  • 启动emqx服务时,一定要将该文件目录放到纯英文的目录下,不能包含中文,否则会出现could't load module的错误。

5. 总结

按以上步骤即可完成最基本的功能,以上只是简单的使用,其实还可以设置用户登录名和密码、设置服务质量、重连的操作等。关于MQTT的相关内容可以看这篇文章MQTT

项目Github地址

如果本文对你有帮助,请别忘记三连,如果有不恰当的地方也请提出来,下篇文章见。


作者:重拾丢却的梦

链接:https://juejin.cn/post/6939153370267516941

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

学习MQTT协议,与设备沟通

概述 MQTT是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和制动器之间通信的桥梁。 MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控...
继续阅读 »

概述


MQTT是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和制动器之间通信的桥梁。


MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议。有以下特点:



  • 使用发布/订阅消息模式,提供一对多的消息发布

  • 使用TCP/IP提供网络连接

  • 小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量,传输的内容最大为256MB。

  • 使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。


1.MQTT协议实现方式




MQTT系统由与服务器通信的客户端组成,通常称服务器为“代理Broker”。客户可以是信息发布者Publish或订阅者Subscribe。每个客户端都可以连接到代理。


信息按主题层次结构组织。当发布者具有要分发的新数据时,它会将包含数据的控制消息发送到连接的代理。然后,代理将信息分发给已订阅该主题的任何客户端。发布者不需要有关于订阅者数量或位置的任何数据,而订阅者又不必配置有关发布者的任何数据。


MQTT传输的消息分为:主题(Topic)和负载(payload)两部分: (1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload); (2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。


2. MQTT协议中的术语




2.1订阅(Subscription)

订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。


2.2会话(Session)

每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。


2.3主题名(Topic Name)

连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。 系统主题:通过定义$SYS开头的主题可以查看一些系统信息,如客户端连接数量等, 详细介绍:github.com/mqtt/mqtt.g…


2.4主题筛选器(Topic Filter)

一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。 多级匹配符 # 单级匹配符 + 更多主题讨论,请移步github wiki github.com/mqtt/mqtt.g…


2.5负载(Payload)

消息订阅者所具体接收的内容。


3.保留消息和最后遗嘱




保留消息 Retained Messages

MQTT中,无论是发布还是订阅都不会有任何触发事件。 1个Topic只有唯一的retain消息,Broker会保存每个Topic的最后一条retain消息。 发布消息时把retain设置为true,即为保留信息。每个Client订阅Topic后会立即读取到retain消息。如果需要删除retain消息,可以发布一个空的retain消息,因为每个新的retain消息都会覆盖最后一个retain消息。


最后遗嘱 Last Will & Testament

MQTT本身就是为信号不稳定的网络设计的,所以难免一些客户端会无故的和Broker断开连接。 当客户端连接到Broker时,可以指定LWT,Broker会定期检测客户端是否有异常。 当客户端异常掉线时,Broker就往连接时指定的topic里推送当时指定的LWT消息。


4.消息服务质量




有三种消息发布服务质量qos(Quality of Service):


4.1“至多一次”




至多一次



消息发布完全依赖底层TCP/IP网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。


4.2“至少一次”




至少一次



PUBACK消息是对QoS级别为1的PUBLISH消息的响应.PUBACK消息由服务器发送以响应来自发布端的PUBLISH消息,订阅端也会响应来自服务器的PUBLISH消息。当发布端收到PUBACK消息时,它会丢弃原始消息,因为它也被服务器接收(并记录)。


如果一定时间内,发布端或服务器没有收到PUBACK消息,则会进行重发。这种方式虽然确保了消息到达,但消息重复可能会发生。


4.3“只有一次”




只有一次



PUBREC消息是对QoS级别为2的PUBLISH消息的响应。它是QoS级别2协议流的第二个消息。 PUBREC消息由服务器响应来自发布端的PUBLISH消息,或订阅端响应来自服务器的PUBLISH消息。发布端或服务器收到PUBREC消息时,会响应PUBREL消息。


PUBREL消息是从发布端对PUBREC的响应,或从服务器对订阅端PUBREC消息的响应。 这是QoS 2协议流中第三个消息。当服务器从发布者收到PUBREL消息时,服务器会将PUBLISH消息发送到订阅端,并发送PUBCOMP消息到发布端。 当订阅端收到来自服务器的消息PUBREL时,使得消息可用于应用程序并将PUBCOMP消息发送到服务器。


PUBCOMP消息是服务器对来自发布端的PUBREL消息的响应,或订阅者对来自服务器的PUBREL消息的响应。 它是QoS 2协议流程中的第四个也是最后一个消息。当发布端收到PUBCOMP消息时,它会丢弃原始消息,因为它已经将消息发给了服务器。


在一些要求比较严格的计费系统中,可以使用此级别。在计费系统中,消息重复或丢失会导致不正确的结果。这种最高质量的消息发布服务还可以用于即时通讯类的APP的推送,确保用户收到且只会收到一次。




附录:各编程语言对MQTT客户端/服务器的实现


NameLanguageTypeLast releaseLicense
Adafruit IORuby on RailsNode.jsClient2.0.0?
flespiCBroker?Proprietary License
M2MqttC#Client4.3.0.0Eclipse Public License 1.0
Machine HeadClojureClient1.0.0Creative Commons Attribution 3.0 Unported License
moquetteJavaBroker0.10Apache License 2.0
MosquittoCPythonBroker and client1.4.15Eclipse Public License 1.0, Eclipse Distribution License 1.0 (BSD)
Paho MQTTCC++JavaJavascriptPythonGoClient1.3.0Eclipse Public License 1.0, Eclipse Distribution License 1.0 (BSD)
SharkMQTTCClient1.5Proprietary License
VerneMQErlang/OTPBroker1.4.1Apache License 2.0
wolfMQTTCClient0.14GNU Public License, version 2
MQTTRouteCPythonBroker1.0Proprietary License
HiveMQJavaBroker3.4.0Proprietary License
SwiftMQJavaBroker11.1.0Proprietary License
JoramMQJavaBroker11.1.0Proprietary License

作者:brandonbai
链接:https://juejin.cn/post/6844903829096382471
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS Crash分析中的Signal

下面是一些信号说明SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的...
继续阅读 »

下面是一些信号说明

  • SIGHUP
    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

  • SIGINT
    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

  • SIGQUIT
    SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

  • SIGILL
    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

  • SIGTRAP
    由断点指令或其它trap指令产生. 由debugger使用。

  • SIGABRT
    调用abort函数生成的信号。

  • SIGBUS
    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

  • SIGFPE
    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

  • SIGKILL
    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

  • SIGUSR1
    留给用户使用

  • SIGSEGV
    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

  • SIGUSR2
    留给用户使用

  • SIGPIPE
    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

  • SIGALRM
    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

  • SIGTERM
    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL

  • SIGCHLD
    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

  • SIGCONT
    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

  • SIGSTOP
    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

  • SIGTSTP
    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

  • SIGTTIN
    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

  • SIGTTOU
    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

  • SIGURG
    有”紧急”数据或out-of-band数据到达socket时产生.

  • SIGXCPU
    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

  • SIGXFSZ
    当进程企图扩大文件以至于超过文件大小资源限制。

  • SIGVTALRM
    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

  • SIGPROF
    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

  • SIGWINCH
    窗口大小改变时发出.

  • SIGIO
    文件描述符准备就绪, 可以开始进行输入/输出操作.

  • SIGPWR
    Power failure

  • SIGSYS
    非法的系统调用。

关键点注意

  • 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP

  • 不能恢复至默认动作的信号有:SIGILL,SIGTRAP

  • 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

  • 默认会导致进程退出的信号有:
    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM

  • 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

  • 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

  • 此外,SIGIOSVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

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



收起阅读 »

iOS编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »

对于平常的应用程序开发,我们很少需要关注编译链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译链接的过程一步完成,通常将这种编译链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。


编译流程分析

现在我们通过一个C语言的经典例子,来具体了解一下这些机制:


#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

其实上述过程可以分解为四步:

  • 预处理(Prepressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)


预编译

首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i
还可以下面的表达
$ cpp hello.c > hello.i

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

  • 将所有的#define删除,并展开所有的宏定义
  • 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释///**/
  • 添加行号和文件名标识,比如#2 “hello.c” 2。
  • 保留所有的#pragma编译器指令

截图个大家看看效果



经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)

编译过程就是把预处理完的文件进行一系列的:词法分析语法分析语义分析优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s


通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o


链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc

下面在贴出我们的写出的源代码是如何变成目标代码的流程图:

    



主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看


iOS的编译器

iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

  • 1.LLVM核心库:
    LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。。

  • 2.LLVM IR 生成器Clang: Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。

  • 3.LLDB项目:
    LLDB项目以LLVMClang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST表达式解析器LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。

  • 4.libclibc++:
    libc 和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。

  • 5.lld项目:
    lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。

编译器前端的任务是进行:

  • 语法分析
  • 语义分析
  • 生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

iOS程序-详细编译过程

  • 1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
  • 2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
  • 3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
  • 4.链接文件:将项目中的多个可执行文件合并成一个文件;
  • 5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
  • 6.编译 storyboard 文件:storyboard 文件也是会被编译的;
  • 7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
  • 8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage
  • 9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
  • 10.生成 .app 包
  • 11.将 Swift 标准库拷贝到包中
  • 12.对包进行签名
  • 13.完成打包

编译过程的确是个比较复杂的过程,还有链接!并不是说难就不需要掌握,我个人建议每一个进阶路上iOS开发人员,都是要了解一下的。不需要你多么牛逼,但是你能在平时的交流讨论,面试中能点出一个两个相应的点,我相信绝对是逼格满满!


作者:Cooci_和谐学习_不急不躁
链接:https://www.jianshu.com/p/b60612c4d9ca





收起阅读 »

RSA概述

RSA概述首先看这个加密算法的命名.很有意思,它其实是三个人的名字.早在1977年由麻省理工学院的三位数学家Rivest、Shamir 和 Adleman一起提出了这个加密算法,并且用他们三个人姓氏开头字母命名.RSA加密算法是一种非对称加密算法,其玩法打破了...
继续阅读 »

RSA概述

首先看这个加密算法的命名.很有意思,它其实是三个人的名字.早在1977年由麻省理工学院的三位数学家Rivest、Shamir 和 Adleman一起提出了这个加密算法,并且用他们三个人姓氏开头字母命名.
RSA加密算法是一种非对称加密算法,其玩法打破了以往所有加密算法的规则.在RSA出现之前,所有的加密方法都是同一种模式:加密解密的规则使用同一种方式.这种长达几个世纪的加密方案有一个致命的缺陷.在传递加密信息时,必须让对方拿到解密的规则才能正常解密.由于加密解密的规则一致,所以保存和传递"密钥",就成了最头疼的问题。
RSA的出现解决了这个问题.我们来看看RSA是怎么玩的.


RSA加密/解密

  • 使用公钥加密的数据,利用私钥进行解密
  • 使用私钥加密的数据,利用公钥进行解密

没错,RSA加密使用了"一对"密钥.分别是公钥私钥,这个公钥和私钥其实就是一组数字!其二进制位长度可以是1024位或者2048位.长度越长其加密强度越大,目前为止公之于众的能破解的最大长度为768位密钥,只要高于768位,相对就比较安全.所以目前为止,这种加密算法一直被广泛使用.

RSA的弊端

由于RSA算法的原理都是大数计算,使得RSA最快的情况也比对称加密算法慢上好几倍。速度一直是RSA的缺陷,一般来说RSA只用于小数据的加密.RSA的速度是对应同样安全级别的对称加密算法的1/1000左右。

RSA终端命令演示

由于Mac系统内置OpenSSL(开源加密库),所以我们可以直接在终端上使用命令来玩RSA.
OpenSSL中RSA算法常用指令主要有三个,其他指令此处不介绍。



命令
含义
genrsa生成并输入一个RSA私钥
rsautl使用RSA密钥进行加密、解密、签名和验证等运算
rsa处理RSA密钥的格式转换等问题

生成RSA私钥,密钥长度为1024bit

hank$ openssl genrsa -out private.pem 1024
Generating RSA private key, 1024 bit long modulus
..++++++
..........................................++++++
e is 65537 (0x10001)

从私钥中提取公钥

hank$ openssl rsa -in private.pem -pubout -out public.pem
writing RSA key

显得非常高大上对吧!那么它里面是什么,我们可以利用终端进行查看.

//查看私钥文件
hank$ cat private.pem
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDbGfA0XdkIpK5h2O9mg5o35pitxwiHDnlpBTCTUH+pkGMdDe6d
9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy1BgSfLxUx50jmm7jnvnS4Hrb
65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNzdmrkaaAQQkQ9liN6awIDAQAB
AoGAU0gdvNn7WES4oCrEfPQDF8KIQG3KOQPwdFHrr+NGU161veKA0/xNhTvFk8IV
BqsjkdO5j2EFfTMfJ+Qg4maCfIZN+xknosXRUF3vz5CUz/rXwBupOlOiWFJbB6cV
/Jee045DjiHjciip/ZVd8A2xnUEg4pIFUujAFPH+22t5TvkCQQD73bRqCQF9sWIA
tBeNR10Mygx5wrwKvjgCvaawsgx82kuAb3CWR0G81GfU+lK0YaHdmcFHsAHlDncM
OtY6IPnNAkEA3rKP6+/jUoylsJPWuN9LyuKjtAlsNtbWaYvs8iCNhLyV9hoWjvow
AAZB1uWy5aLDtQI3v48beExwsJEFAlQtFwJASTkKU21s1or0T/oLgtJFdgtjlx6L
JqBojjtus53/zWh1XNCJLddngCtMSHnCA5kCwvcJXvsHgf0zlQWh9GJT3QJAY0+q
EwN1kpiaQzaKqQMbX6zWaDFTitkf4Q2/avLNaYZYMdnMeZJk2X3w2o6wyutc71m/
1rNRAsLD9lmVrEYxnQJAEAHb0lsRgWe/sXX2attg4NbDsEExqDZ+7GGsyvqZn1Xg
S/UPdt6rVkVQ3N7ZEPKV6SxwN9LySI4lVWmFWhCn6w==
-----END RSA PRIVATE KEY-----
//查看公钥文件
hank$ cat public.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbGfA0XdkIpK5h2O9mg5o35pit
xwiHDnlpBTCTUH+pkGMdDe6d9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy
1BgSfLxUx50jmm7jnvnS4Hrb65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNz
dmrkaaAQQkQ9liN6awIDAQAB
-----END PUBLIC KEY-----
其实就是一个文本文件,并且一看就知道是base64编码.那么公钥相比私钥要简单很多.我们可以通过命令,将私钥转换成为明文看看.

//转化为明文信息
hank$ openssl rsa -in private.pem -text -out private.txt
writing RSA key
//查看文本信息
hank$ cat private.txt
Private-Key: (1024 bit)
modulus:
00:db:19:f0:34:5d:d9:08:a4:ae:61:d8:ef:66:83:
9a:37:e6:98:ad:c7:08:87:0e:79:69:05:30:93:50:
7f:a9:90:63:1d:0d:ee:9d:f6:75:50:0e:be:b5:41:
41:0c:58:08:1b:9d:bf:e2:ad:35:e1:e5:58:a0:1a:
11:c3:6c:6f:f8:92:65:72:d4:18:12:7c:bc:54:c7:
9d:23:9a:6e:e3:9e:f9:d2:e0:7a:db:eb:98:3d:db:
4c:76:ea:06:81:5b:e2:3d:9f:d7:07:17:f4:12:86:
ba:9a:35:73:fd:0c:37:aa:86:a3:73:76:6a:e4:69:
a0:10:42:44:3d:96:23:7a:6b
publicExponent: 65537 (0x10001)
privateExponent:
53:48:1d:bc:d9:fb:58:44:b8:a0:2a:c4:7c:f4:03:
17:c2:88:40:6d:ca:39:03:f0:74:51:eb:af:e3:46:
53:5e:b5:bd:e2:80:d3:fc:4d:85:3b:c5:93:c2:15:
06:ab:23:91:d3:b9:8f:61:05:7d:33:1f:27:e4:20:
e2:66:82:7c:86:4d:fb:19:27:a2:c5:d1:50:5d:ef:
cf:90:94:cf:fa:d7:c0:1b:a9:3a:53:a2:58:52:5b:
07:a7:15:fc:97:9e:d3:8e:43:8e:21:e3:72:28:a9:
fd:95:5d:f0:0d:b1:9d:41:20:e2:92:05:52:e8:c0:
14:f1:fe:db:6b:79:4e:f9
prime1:
00:fb:dd:b4:6a:09:01:7d:b1:62:00:b4:17:8d:47:
5d:0c:ca:0c:79:c2:bc:0a:be:38:02:bd:a6:b0:b2:
0c:7c:da:4b:80:6f:70:96:47:41:bc:d4:67:d4:fa:
52:b4:61:a1:dd:99:c1:47:b0:01:e5:0e:77:0c:3a:
d6:3a:20:f9:cd
prime2:
00:de:b2:8f:eb:ef:e3:52:8c:a5:b0:93:d6:b8:df:
4b:ca:e2:a3:b4:09:6c:36:d6:d6:69:8b:ec:f2:20:
8d:84:bc:95:f6:1a:16:8e:fa:30:00:06:41:d6:e5:
b2:e5:a2:c3:b5:02:37:bf:8f:1b:78:4c:70:b0:91:
05:02:54:2d:17
exponent1:
49:39:0a:53:6d:6c:d6:8a:f4:4f:fa:0b:82:d2:45:
76:0b:63:97:1e:8b:26:a0:68:8e:3b:6e:b3:9d:ff:
cd:68:75:5c:d0:89:2d:d7:67:80:2b:4c:48:79:c2:
03:99:02:c2:f7:09:5e:fb:07:81:fd:33:95:05:a1:
f4:62:53:dd
exponent2:
63:4f:aa:13:03:75:92:98:9a:43:36:8a:a9:03:1b:
5f:ac:d6:68:31:53:8a:d9:1f:e1:0d:bf:6a:f2:cd:
69:86:58:31:d9:cc:79:92:64:d9:7d:f0:da:8e:b0:
ca:eb:5c:ef:59:bf:d6:b3:51:02:c2:c3:f6:59:95:
ac:46:31:9d
coefficient:
10:01:db:d2:5b:11:81:67:bf:b1:75:f6:6a:db:60:
e0:d6:c3:b0:41:31:a8:36:7e:ec:61:ac:ca:fa:99:
9f:55:e0:4b:f5:0f:76:de:ab:56:45:50:dc:de:d9:
10:f2:95:e9:2c:70:37:d2:f2:48:8e:25:55:69:85:
5a:10:a7:eb
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDbGfA0XdkIpK5h2O9mg5o35pitxwiHDnlpBTCTUH+pkGMdDe6d
9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy1BgSfLxUx50jmm7jnvnS4Hrb
65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNzdmrkaaAQQkQ9liN6awIDAQAB
AoGAU0gdvNn7WES4oCrEfPQDF8KIQG3KOQPwdFHrr+NGU161veKA0/xNhTvFk8IV
BqsjkdO5j2EFfTMfJ+Qg4maCfIZN+xknosXRUF3vz5CUz/rXwBupOlOiWFJbB6cV
/Jee045DjiHjciip/ZVd8A2xnUEg4pIFUujAFPH+22t5TvkCQQD73bRqCQF9sWIA
tBeNR10Mygx5wrwKvjgCvaawsgx82kuAb3CWR0G81GfU+lK0YaHdmcFHsAHlDncM
OtY6IPnNAkEA3rKP6+/jUoylsJPWuN9LyuKjtAlsNtbWaYvs8iCNhLyV9hoWjvow
AAZB1uWy5aLDtQI3v48beExwsJEFAlQtFwJASTkKU21s1or0T/oLgtJFdgtjlx6L
JqBojjtus53/zWh1XNCJLddngCtMSHnCA5kCwvcJXvsHgf0zlQWh9GJT3QJAY0+q
EwN1kpiaQzaKqQMbX6zWaDFTitkf4Q2/avLNaYZYMdnMeZJk2X3w2o6wyutc71m/
1rNRAsLD9lmVrEYxnQJAEAHb0lsRgWe/sXX2attg4NbDsEExqDZ+7GGsyvqZn1Xg
S/UPdt6rVkVQ3N7ZEPKV6SxwN9LySI4lVWmFWhCn6w==
-----END RSA PRIVATE KEY-----

通过公钥加密数据,私钥解密数据

//生成明文文件
hank$ vi message.txt
//查看文件内容
hank$ cat message.txt
密码:123456
//通过公钥进行加密
hank$ openssl rsautl -encrypt -in message.txt -inkey public.pem -pubin -out enc.txt
//通过私钥进行解密
hank$ openssl rsautl -decrypt -in enc.txt -inkey private.pem -out dec.txt

通过私钥加密数据,公钥解密数据

//通过私钥进行加密
hank$ openssl rsautl -sign -in message.txt -inkey private.pem -out enc.txt
//通过公钥进行解密
hank$ openssl rsautl -verify -in enc.txt -inkey public.pem -pubin -out dec.txt

小结

那么看到这些之后,对RSA应该有了一定的了解.由于RSA加密运行效率非常低!并不是所有数据加密都会使用它.那么它的主战场在于加密一些小的数据,比如对称加密算法的密钥.又或者数字签名.



作者:Hank
链接:https://www.jianshu.com/p/6280aa136292







收起阅读 »

java设计模式:命令模式

前言在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。 在现...
继续阅读 »

前言

在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。


在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。


再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现。


定义与特点

命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。


优点

通过引入中间件(抽象接口)降低系统的耦合度。
扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。

缺点

可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。
命令模式的结构与实现
可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。

结构

抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
在这里插入图片描述

实现

命令模式的代码如下:


package command;
public class CommandPattern {
public static void main(String[] args) {
Command cmd = new ConcreteCommand();
Invoker ir = new Invoker(cmd);
System.out.println("客户访问调用者的call()方法...");
ir.call();
}
}

//调用者


class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void setCommand(Command command) {
this.command = command;
}
public void call() {
System.out.println("调用者执行命令command...");
command.execute();
}
}

//抽象命令


interface Command {
public abstract void execute();
}

//具体命令



class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand() {
receiver = new Receiver();
}
public void execute() {
receiver.action();
}
}

//接收者


class Receiver {
public void action() {
System.out.println("接收者的action()方法被调用...");
}
}

程序的运行结果如下:



客户访问调用者的call()方法...
调用者执行命令command...
接收者的action()方法被调用...

实例


假如我们开发一个播放器,播放器播放功能、拖动进度条功能、停止播放功能、暂停功能,我们在操作播发器的时候并不知道之间调用播放器
哪个功能,而是通过一个控制传达去传递指令给播放器内核,具体传达什么指令,会被封装成一个个按钮。那么每个按钮就相当于一条命令的封装。
用控制条实现了用户发送指令与播放器内核接收指令的解耦。下面来看代码,首先创建播放器内核类:



public class GPlayer {
public void play() {
System.out.println("正常播放");
}

public void speed() {
System.out.println("拖动进度条");
}

public void stop() {
System.out.println("停止播放");
}

public void pause() {
System.out.println("暂停播放");
}
}

创建命令接口:



public interface IAction {
void execute();
}

创建播放指令类:


public class PlayAction implements IAction {
private GPlayer gplayer;

public PlayAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.play();
}
}

创建暂停指令类:


public class PauseAction implements IAction {
private GPlayer gplayer;

public PauseAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.pause();
}
}

创建拖动进度条类:


public class SpeedAction implements IAction {
private GPlayer gplayer;

public SpeedAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.speed();
}
}

创建停止播放指令:


public class StopAction implements IAction {
private GPlayer gplayer;

public StopAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.stop();
}
}

创建控制条controller类:



public class Controller {
private List<IAction> actions = new ArrayList<IAction>();

public void addAction(IAction action) {
actions.add(action);
}

public void execute(IAction action) {
action.execute();
}

public void executes() {
for (IAction action : actions) {
action.execute();
}
actions.clear();
}
}

从上面代码来看,控制条可以执行单条命令,也可以批量执行多条命令。下面看客户端的测试代码:



public class Test {
public static void main(String[] args) {

GPlayer player = new GPlayer();
Controller controller = new Controller();
controller.execute(new PlayAction(player));

controller.addAction(new PauseAction(player));
controller.addAction(new PlayAction(player));
controller.addAction(new StopAction(player));
controller.addAction(new SpeedAction(player));
controller.executes();
}
}

由于控制条已经与播放器内核解耦了,以后如果想扩展新命令,只需要增加命令即可,控制条的结构无须改动。


java源码中的命令模式


首先来看 JDK 中的 Runnable 接口,Runnable 相当于命令模式中的抽象命令角色。Runnable 中的 run() 方法就当于 execute() 方法。



public interface Runnable {
public abstract void run();
}
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + lazySingleton);
}
}

public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("Program End");
}
}

只要是实现了 Runnable 接口的类都被认为是一个线程,相当于命令模式中的具体命令角色。


实际上调用线程的 start() 方法之后,就有资格去抢 CPU 资源,而不需要编写获得 CPU 资源的逻辑。而线程抢到 CPU 资源后,就会执行 run() 方法中的内容,用 Runnable 接口把用户请求和 CPU 执行进行解耦。


收起阅读 »

Java设计模式:迭代器模式

前言在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。 既然将遍历方法封装在聚合...
继续阅读 »

前言

在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。


既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:



  1. 暴露了聚合类的内部表示,使其数据不安全;
  2. 增加了客户的负担。

“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如 Java 中的 Collection、List、Set、Map 等都包含了迭代器。


迭代器模式在生活中应用的比较广泛,比如:物流系统中的传送带,不管传送的是什么物品,都会被打包成一个个箱子,并且有一个统一的二维码。这样我们不需要关心箱子里是什么,在分发时只需要一个个检查发送的目的地即可。再比如,我们平时乘坐交通工具,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性、是残疾人还是正常人等信息。


定义与特点

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式


优点


  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 遍历任务交由迭代器完成,这简化了聚合类。
  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

缺点

增加了类的个数,这在一定程度上增加了系统的复杂性。


在日常开发中,我们几乎不会自己写迭代器。除非需要定制一个自己实现的数据结构对应的迭代器,否则,开源框架提供的 API 完全够用。


结构

迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。现在我们来分析其基本结构与实现方法。



  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

在这里插入图片描述

模式的实现

package net.biancheng.c.iterator;
import java.util.*;
public class IteratorPattern {
public static void main(String[] args) {
Aggregate ag = new ConcreteAggregate();
ag.add("中山大学");
ag.add("华南理工");
ag.add("韶关学院");
System.out.print("聚合的内容有:");
Iterator it = ag.getIterator();
while (it.hasNext()) {
Object ob = it.next();
System.out.print(ob.toString() + "\t");
}
Object ob = it.first();
System.out.println("\nFirst:" + ob.toString());
}
}
//抽象聚合
interface Aggregate {
public void add(Object obj);
public void remove(Object obj);
public Iterator getIterator();
}
//具体聚合
class ConcreteAggregate implements Aggregate {
private List<Object> list = new ArrayList<Object>();
public void add(Object obj) {
list.add(obj);
}
public void remove(Object obj) {
list.remove(obj);
}
public Iterator getIterator() {
return (new ConcreteIterator(list));
}
}
//抽象迭代器
interface Iterator {
Object first();
Object next();
boolean hasNext();
}
//具体迭代器
class ConcreteIterator implements Iterator {
private List<Object> list = null;
private int index = -1;
public ConcreteIterator(List<Object> list) {
this.list = list;
}
public boolean hasNext() {
if (index < list.size() - 1) {
return true;
} else {
return false;
}
}
public Object first() {
index = 0;
Object obj = list.get(index);
;
return obj;
}
public Object next() {
Object obj = null;
if (this.hasNext()) {
obj = list.get(++index);
}
return obj;
}
}

运行结果


聚合的内容有:中山大学    华南理工    韶关学院   
First:中山大学

java源码分析

Iterator


public interface Iterator<E> {

boolean hasNext();

E next();

default void remove() {
throw new UnsupportedOperationException("remove");
}

//剩余元素迭代
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

上面是迭代器Iterator接口的代码,定义了一些需要子类实现的方法和默认的方法。在这里说一下上面两个default方法都是JDK1.8之后才有的接口新特性,在JDK1.8之前接口中不能有方法实体。


ArrayList


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}

上面是简化的ArrayList类,因为具体实现迭代器Itr的类在ArrayList中作为内部类存在,这个内部类将接口中的方法做了具体实现,并且是只对ArrayList这个类进行实现的。


public interface List<E> extends Collection<E> {
Iterator<E> iterator();
}

上面是简化的List接口,充当的是聚合接口,可以看见内部创建了相应迭代器接口的方法。


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public Iterator<E> iterator() {
return new Itr();
}
}

上面是简化的ArrayList类,充当的是具体聚合类角色,在这里是直接返回了一个具体实现迭代器的类。


public class Test1 {

public static void main(String[] args) {
List<Integer> a=new ArrayList<>();
a.add(1);
a.add(2);
a.add(3);
Iterator itr=a.iterator();
while(itr.hasNext()){
System.out.println(itr.next());
}
}
}

收起阅读 »

java设计模式:中介者模式

前言在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵...
继续阅读 »

前言

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。


如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参加工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。


在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。
模式的定义与特点

定义

定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。


优点

类之间各司其职,符合迪米特法则。
降低了对象之间的耦合性,使得对象易于独立地被复用。
将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

缺点

中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
模式的结构与实现
中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

结构

抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
在这里插入图片描述

实现

中介者模式的实现代码如下:


package net.biancheng.c.mediator;
import java.util.*;
public class MediatorPattern {
public static void main(String[] args) {
Mediator md = new ConcreteMediator();
Colleague c1, c2;
c1 = new ConcreteColleague1();
c2 = new ConcreteColleague2();
md.register(c1);
md.register(c2);
c1.send();
System.out.println("-------------");
c2.send();
}
}
//抽象中介者
abstract class Mediator {
public abstract void register(Colleague colleague);
public abstract void relay(Colleague cl); //转发
}
//具体中介者
class ConcreteMediator extends Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMedium(this);
}
}
public void relay(Colleague cl) {
for (Colleague ob : colleagues) {
if (!ob.equals(cl)) {
((Colleague) ob).receive();
}
}
}
}
//抽象同事类
abstract class Colleague {
protected Mediator mediator;
public void setMedium(Mediator mediator) {
this.mediator = mediator;
}
public abstract void receive();
public abstract void send();
}
//具体同事类
class ConcreteColleague1 extends Colleague {
public void receive() {
System.out.println("具体同事类1收到请求。");
}
public void send() {
System.out.println("具体同事类1发出请求。");
mediator.relay(this); //请中介者转发
}
}
//具体同事类
class ConcreteColleague2 extends Colleague {
public void receive() {
System.out.println("具体同事类2收到请求。");
}
public void send() {
System.out.println("具体同事类2发出请求。");
mediator.relay(this); //请中介者转发
}
}

程序的运行结果如下:


具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

应用场景

前面分析了中介者模式的结构与特点,下面分析其以下应用场景。
当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

java源码中的体现

在看其他人写的关于Timer 的中介者设计模式,我觉得写的都不是很清楚。我大概用源码来解释一下,顺便再分析一下Timer的所有关联类的源码:


private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}

说明:所有的schedule方法都调用了sched ,那这个类的主要作用是啥呢?



将timertask加入到队列里,然后从队列里取出min任务(二叉堆的数据结构,下面会说明),判断如果min任务等于当前任务的话让队列wait的状态变为运行状态,如果不等于的话,那么线程的mainloop方法肯定是一直再运行状态的,其他任务就可以依次执行



看如下的源码


private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die

// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}

Timer相当于中介者来执行队列里的任务,用户只管将任务抛给timer就可以了。


如下详细timer源码分析

在Java中,很常见的一个定时器的实现就是 Timer 类,用来实现定时、延迟执行、周期性执行任务的功能。


Timer 是定义在 java.util 中的一个工具类,提供简单的实现定时器的功能。和它配合使用的,是 TimerTask 类,这是对一个可以被调度的任务的封装。使用起来非常简单,如下示例:


// 定义一个可调度的任务,继承自 TimerTask
class FooTimerTask extends TimerTask {

@Override
public void run() {
// do your things
}
}

// 初始化Timer 定时器对象
Timer timer = new Timer("barTimer");

// 初始化需要被调度的任务对象
TimerTask task = new FooTimerTask();

// 调度任务。延迟1000毫秒后执行,之后每2000毫秒定时执行一次
timer.schedule(task, 1000, 2000);

以上,就是一个简单的使用Timer 的示例,下文将会分析Timer的源码实现。


概述

在Timer 机制中,涉及到的关键类如下:



  • Timer: 主要的调用的,提供对外的API;
  • TimerTask: 是一个抽象类,定义一个任务,继承自Runnable
  • TimerThread: 继承自 Thread,是一个自定义的线程类;
  • TaskQueue: 一个任务队列,包含有当前Timer的所有任务,内部使用二叉堆来实现。

以上几个关键类的引用关系如下:
在这里插入图片描述
简要描述的话,是:

1个 TimerThread —-> 实现1个 线程


1个 Timer对象 —-> 持有1个 TimerThread 对象


1个 Timer对象 —-> 持有1个 TimerQueue 对象


1个 TimerQueue 对象 —-> 持有 n个 TimerTask 对象


源码分析

Timer类的源码分析
源码分析的话,我们最好是按照Timer 的使用流程来分析。 首先,是Timer 的创建:

// Timer有四个构造方法,但是本质上其实是做的相同的事情,即
// 1. 使用name 和 isDeamon 两个参数给 thread 对象做了参数设置;
// 2. 调用 thread 的 start() 方法启动线程
public Timer() {
this("" + serialNumber());
}

public Timer(boolean isDaemon) {
this("" + serialNumber(), isDaemon);
}


public Timer(String name) {
thread.setName(name);
thread.start();
}

public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}

那么,或许大家会有一个疑问,thread 成员的初始化呢?这个时候,在代码里面找,就能发现:



// 这两个成员都是直接在声明的时候进行了初始化。
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);

可以看到 thread 和 queue两个成员都是在声明的时候直接初始化的,并且有意思的是,两个成员都是 final 类型的,这也就意味着这两个成员一旦创建就不会再改了,等于说把 thread、queue 和 Timer 对象这三者的生命周期强行绑定在一起了,大家一起创建,并且一经创建将会无法改变。


然后,创建了Timer 后,与之相关的队列也已经创建成功,而且相关联的线程也启动了,就可以进行任务的调度了,我们看下它的任务调度方法:



// Timer 包含有一组重载方法,参数为以下几个:
// 1. TimerTask task:需要被调度的任务
// 2. long delay: 指定延迟的时间;
// 3. long period: 指定调度的执行周期;
schedule(TimerTask task, long delay, long period)

多个重载的调度方法在经过一些一些列的状态判断、参数设置、以及把delay时间转换成实际的执行时间等之后, 最终完成该功能的是 sched 方法,详情见注释部分:


这里涉及到一个需要留意的点,是在调用schedule 方法的时候,会根据TimerTask 的类型来进行不同的计算,进而给TimerTask设置不同的 period 参数,TimerTask 的类型有以下几种:



  • 非周期性任务;对应 TimerTask.period 值为0;
  • 周期性任务,但是没有delay值,即立即执行;对应 TimerTask.period 值为正数;
  • 周期性任务,同时包含有 delay值;对应 TimerTask.period 值为负数;

在schedule 方法中,会



// 执行任务调度的方法
// 这里的 time 已经是经过转换的,表示该task 需要被执行的时间戳
private void sched(TimerTask task, long time, long period) {
// 参数的合法性检查
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

// 核心的调度逻辑
// 由于是在多线程环境中使用的,这里为了保证线程安全,使用的是 synchronized 代码段
// 对象锁使用的是在 Timer 对象中唯一存在的 queue 对象
synchronized(queue) {

// thread.newTasksMayBeScheduled 是一个标识位,在timer cancel之后 或者 thread 被停止后该标识位会被设为false
// newTasksMayBeScheduled 为false 则表示该timer 的关联线程已经停止了。
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

// 这里是把外部的参数,如执行时间点、执行周期、设置状态等等。
// 这里为了线程安全的考虑,使用对 task 内部的 lock 对象加锁来保证。
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

// 最后,把新的 task 添加到关联队列里面
queue.add(task);

// 这里,会使用打 TimerQueue 对象的 getMin() 方法,这个方法是获取到接下来将要被执行的TimerTask 对象
// 这里的逻辑是check 新添加的 task 对象是不是接下来马上会被执行
// 如果刚添加的对象是需要马上执行的话,会使用 queue.notify 来通知在等待的线程。

// 那么,会有谁在等待这个 notify 呢?是TimerThread 内部,TimerThread 会有一个死循环,在不停从queue中取任务来执行
// 当queue为空的时候,TimerThread 会进行 queue.wait() 来进行休眠的状态,直到有新的来任务来唤醒它
// 下面的代码就是,当queue为空的时候,这个判断条件会成立,然后就通知 TimerThread 重新唤醒
// 当然,下面的条件成立也不全是 queue 为空的情况下
if (queue.getMin() == task)
queue.notify();
}
}

TimerTask 的源码分析

接下来,本文将会分析 TimerTask 的源码。相对于Timer 来说,它的源码其实很简单,TimerTask 是实现了Runnable 接口,同时也是一个抽象类,它并没有对 Runnable 的 run() 方法提供实现,而是需要子类来实现。


它对外提供了以下几个功能:


包含有一段可以执行的代码(实现的Runnable 接口的run方法)
包含状态的定义。它有一个固定的状态:VIRGIN(新创建)、SCHEDULED(被调度进某一个 timer 的队列中了,但是还没有执行到)、EXECUTED(以及执行过了)、CANCELLED(任务被取消了)。
包含有取消的方法。
包含有获取下一次执行时间的方法。

相关的源码如下:



// 取消该任务
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}

// 根据执行周期,和设置的执行时间,来确定Task的下一次执行时间。
public long scheduledExecutionTime() {
synchronized(lock) {
// 其中,period 的值分为3种情况:
// 取值为0: 表示该Task是非周期性任务;
// 取值为正数: 表示该Task 是立即执行没有delay的周期性任务,period 的数值表示该Task 的周期
// 取值为负数: 表示该Task 是有 delay 的周期性任务,period 相反数是该Task 的周期
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}

TimerThread 的源码分析

TimerThread 首先是一个 Thread 的子类,而且我们知道,在Java中,一个Thread 的对象就是代表了一个JVM虚拟机线程。那么,这个 TimerThread 其实也就是一个线程。


对于一个线程来说,那么它的关键就是它的 run() 方法,在调用线程的 start() 方法启动线程之后,接下来就会执行线程的 run() 方法,我们看下 TimerThread 的run() 方法:



public void run() {
try {
// 启动 mainLoop() 方法,这是一个阻塞方法,正常情况下会一只阻塞在这里
// 当 mainLoop() 执行完毕的时候,也即是这个线程退出的时候。
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
// 做一些收尾工作
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}

从以上可以明确得看出,TimerThread 里的实现是调用 mainLoop() 启动了一个死循环,这个死循环内部的工作就是这个线程的具体工作了,一旦线程的死循环执行完毕,线程的 run 方法就执行完了,线程紧接着就退出了。熟悉Android的朋友可能已经觉得这里的实现非常眼熟了,没错,这里的实现和Android平台的 Handler + HandlerThread + Looper 的机制非常相像,可以认为Android平台最初研发这套机制的时候,就是参考的Timer 的机制,然后在上面做了些升级和适合Android平台的一些改动。


下面是 mainLoop() 方法:



private void mainLoop() {
// 一个死循环
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 会等待到队列不为空,结合上面章节的分析,我们可以确定在新添加 TimerTask 到queue中的时候
// 会触发到 queue.notify() 然后通知到这里。
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();

// queue 为空,说明 timer 被取消了
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die


long currentTime, executionTime;
// 又一次看到这个 queue.getMin() ,这个是根据接下来的执行时间来获取下一个需要被执行的任务
task = queue.getMin();

// 需要修改 task对象的内部数值,使用synchronized 保证线程安全
synchronized(task.lock) {
// TimerTask 有多种状态,一旦一个 TimerTask 被取消之后,它就不会被执行了。
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}

// 获取到当前时间,和这个取出来的task 的下一次执行时间
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;

// 这里会check 当前这个 task 是不是已经到时间了
// 这里会把是否到时间了这个状态保存在 taskFired 里面
if (taskFired = (executionTime<=currentTime)) {
// 根据上文的分析,TimerTask 根据 task.period 值的不同,被分为3种类型
// 这里的 task.period == 0 的情况,是对应于一个非周期性任务
if (task.period == 0) {
// 非周期性任务,处理完就完事了,改状态,移除队列
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
// 周期性任务,会被重新调度,也不会被移除队列
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}

// 这里是另一个会等待的地方,这个是为了等待任务的到期,等待时间就是距离到执行之间的时长
if (!taskFired)
queue.wait(executionTime - currentTime);
}

// taskFired 变量经过上面的步骤以及判断过了,如果是 true,说明task以及到时间了
// 到时间就运行完事。
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}

TimerThread 中除了上面的主要逻辑之外,还有一些需要关注的地方,那就是它持有一个 TimerQueue 的对象,这个对象是在创建的时候外部传进来的,也是和当前的Timer 关联的TimerQueue:



// 这里的官方注释,说明了为什么是在TimerThread 中引用了 TimerQueue而不是引用了 Timer。
// 这么做是为了避免循环引用(因为Timer中引用了TimerThread),进而避免循环引用可能导致的JVM gc 失败的问题
// 我们都知道,Java 是一门通用的语言,虽然官方的HotSpot JVM中是能解决循环引用的GC问题的,但是这并不意味着
// 其他第三方的JVM也能解决循环引用导致的GC问题,所以这里干脆就避免了循环引用。

/**
* Our Timer's queue. We store this reference in preference to
* a reference to the Timer so the reference graph remains acyclic.
* Otherwise, the Timer would never be garbage-collected and this
* thread would never go away.
*/
private TaskQueue queue;

TimerQueue 的源码分析(主要是实现一个二叉堆)

TimerQueue 的逻辑上是一个队列,所有它包含有一个队列常见的那些方法,如 size()、add()、clear()等方法。下面我们找一些重要的方法进行分析:


首先,在上文的分析中,我们以及见过TimeQueue 的 getMin() 方法了,这个方法是获取当前的队列里面,接下来应该被执行的TimerTask,也就是说,是执行时间点 数值最小的那一个,那么我们就先看下它的源码:


/**
* Return the "head task" of the priority queue. (The head task is an
* task with the lowest nextExecutionTime.)
*/
TimerTask getMin() {
return queue[1];
}

What??? 就这吗?为啥这么简单?为啥就返回 queue[1] 就对了?


你是不是也有这样的疑问,那么带着疑问往下看吧。


接下来,是添加一个TimerTask 到队列中:



// 内部存放TimerTask 数据的,是一个数组,设置的数组初始大小是128
private TimerTask[] queue = new TimerTask[128];

// 存放当前的TimerTask 的数量
// 而且 TimerTask 是存放在 [1 - size] 位置的,数组的第0位置没有数据
// 至于为什么要 存放在 [1 - size] 请看下文。
private int size = 0;

/**
* Adds a new task to the priority queue.
*/
void add(TimerTask task) {
// check 下数据的容量是否还够添加,不够的话会先进行数组的扩容
// 这扩容一次就是2倍增加
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);

// 把新的TimerTask 放在数组的最后一个位置
// size 的初始化值是0,从这里可以看出来,这里会先把size自增1,然后再添加到数组中
// 其实是从数组位置的 1 开始添加 TimerTask 的,0的位置是空的
queue[++size] = task;

// 然后调用了这个数据上浮的方法
fixUp(size);
}

从上文看出,add 方法本身也没什么奇特的,就是很简单地把新的 TimerTask 放在了数据的最新的位置,只是里面调用了一下另一个方法 fixUp() ,好,那么我们接着分析这个方法:





// 从上文可以看出,参数 k 是当前的数组size 值,也是最后一个TimerTask 的下标索引
private void fixUp(int k) {
// 首先,这是一个循环,循环条件是 k > 1
while (k > 1) {
// 位运算,操作,把 k 右移一位,得到的结果是:偶数相当于除以2,奇数相当于先减1再除以2
int j = k >> 1;

// 比较 j 和 k 两个位置的下次执行时间,j 不大于 k 的话,就停止循环了
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;

// j 大于 k 位置的时间的话,就要进行下面的动作
// 这是一个典型的交换操作
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;

// k 值缩小到j,去逼近循环条件 k>1
k = j;
}
}

看了上面对 fixUp() 的分析,是不是仍然一脸懵?或许也有些熟悉算法的朋友已经觉察出些什么了,那么这个地方的逻辑是什么呢?


有了右移一位、[1, size]的区间等蛛丝马迹,我想聪明的你已经猜出来了,这个数组queue 里面,是存放了一个完全二叉树。


在发现 queue 数组是一个二叉树之后,再去理解上面的 fixUp() 方法其实就很简单了,里面的过程是这样的:


从二叉树的最后一个叶子结点开始循环;
获取这个叶子结点的父结点(完全二叉树中对应的父结点的索引是:子结点位运算右移一位得到的)
判断父结点和子结点中对应的 TimerTask 的 nextExecutionTime 的大小,如果父比子的小,则停止循环;如果父比子的大,则交互负责结点;
重复以上循环,直到遍历到根结点;

通过以上分析,能发现在每一次新增一个结点后,使用 fixUp(),方法直接对整个二叉树进行了重排序,使得 TimerTask 的nextExecutionTime 值最小的结点,永远被放置在了二叉树的根结点上,也即是queue[1]。这也就搞明白了为什么 getMin 的实现,是直接获取的 queue[1] 。


同样的道理,在每一次执行 Timer.purge() 方法,清理了TimerQueue中已经取消的Task之后,会执行另一个 fixDown() 方法,它的逻辑正好是和 fixUp() 相反的,它是从根结点开始遍历的,然后到达每一个叶子结点以整理这个二叉树,这里就不再赘述。


回过头来,我们再看下TimerQueue中的实现,会发现它其实是一个二叉堆,二叉堆是一个带有权重的二叉树,这里不再多说。


总结

通过以上的分析,总的来说,就是每一个 Timer对象中,包含有一个线程(TaskThread)和一个队列(TaskQueue)。TaskQueue 的实现是一个二叉堆(Binary Heap)的结构,二叉堆的每一个节点,就是 TimerTask 的对象。


收起阅读 »

iOS Cateogry的深入理解&&initialize方法调用理解(二)

上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调...
继续阅读 »
  • 上一篇文章我们讲到了load方法,今天我们来看看initialize

新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下

将原来的load方法换成initialize






先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize

    1. 相信大家在想什么叫第一次接收消息了,我们回到main()





说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码

从输出结果可以看到没有任何关于initialize的打印,程序直接退出

  • 2.initialize的打印

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

[TCPerson alloc];
}
return 0;
}
2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印

load是直接函数指针直接调用,类,分类,继承等等

[TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用


多次向TCPerson发送消息的输出结果

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

[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}
2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

initialize只会调用一次

我们再来看看继承关系中,initialize的调用

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

}
return 0;
}

输出结果:

2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0


从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}
020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

#import "TCStudent.h"

@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}
2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0
如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

#import "TCStudent.h"

@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"

@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"

@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end
2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0




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


收起阅读 »

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

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

首先先看几个面试问题

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

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


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


Cateogry里面有load方法么?

  • 答:分类里面肯定有load

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{

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

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

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

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

}
@end

load方法什么时候调用?

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

#import <Foundation/Foundation.h>

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

}
return 0;
}

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


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

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

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

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


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

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

NS_ASSUME_NONNULL_END

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
+ (void)test{
NSLog(@"TCPerson +test");
}
@end
分类重写test
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest1) +test1");
}
@end
#import "TCPerson+TCTest2.h"

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

在main里面我们调用test

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

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





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




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

我们打印TCPerson的类方法

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

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

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

// 释放
free(methodList);

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


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

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

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

loadMethodLock.assertLocked();

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

void *pool = objc_autoreleasePoolPush();

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

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

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

objc_autoreleasePoolPop(pool);

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

static void call_class_loads(void)
{
int I;

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

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

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

// Destroy the detached list.
if (classes) free(classes);
}
其通过的是load_method_t函数指针直接调用
函数指针直接调用
typedef void(*load_method_t)(id, SEL);

其分类load方法调用也是一样

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

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

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

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

为什么test不一样了

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

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

load的继承关系调用
首先我们先看TCStudent
#import "TCStudent.h"

@implementation TCStudent

@end

不写load方法调用

TCStudent写上load


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

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



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









收起阅读 »

java设计模式:享元模式

前言在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。 例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些...
继续阅读 »

前言

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。


例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。


定义

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。


优点

相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。


缺点

为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
读取享元模式的外部状态会使得运行时间稍微变长。

享元模式的结构与实现

享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。

比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。


享元模式的本质是缓存共享对象,降低内存消耗。


结构

抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

享元模式的实现

应用实例的话,其实上面的模板就已经是一个很好的例子了,类似于String常量池,没有的对象创建后存在池中,若池中存在该对象则直接从池中取出。


  为了更好的理解享元模式,这里再举一个实例,比如接了我一个小型的外包项目,是做一个产品展示网站,后来他的朋友们也希望做这样的网站,但要求都有些不同,我们当然不能直接复制粘贴再来一份,有任希望是新闻发布形式的,有人希望是博客形式的等等,而且因为经费原因不能每个网站租用一个空间。


  其实这里他们需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这是造成服务器的大量资源浪费。如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源;而对于代码,由于是一份实例,维护和扩展都更加容易。


  那么此时就可以用到享元模式了。UML图如下:
  在这里插入图片描述
网站抽象类
 

 public abstract class WebSite {

public abstract void use();

}

具体网站类


public class ConcreteWebSite extends WebSite {

private String name = "";

public ConcreteWebSite(String name) {
this.name = name;
}

@Override
public void use() {
System.out.println("网站分类:" + name);
}

}

网络工厂类
  这里使用HashMap来作为池,通过put和get方法实现加入池与从池中取的操作。

public class WebSiteFactory {

private HashMap<String, ConcreteWebSite> pool = new HashMap<>();

//获得网站分类
public WebSite getWebSiteCategory(String key) {
if(!pool.containsKey(key)) {
pool.put(key, new ConcreteWebSite(key));
}

return (WebSite)pool.get(key);
}

//获得网站分类总数
public int getWebSiteCount() {
return pool.size();
}

}

Client客户端
  这里测试用例给了两种网站,原先我们需要做三个产品展示和三个博客的网站,也即需要六个网站类的实例,但其实它们本质上都是一样的代码,可以利用用户ID号的不同,来区分不同的用户,具体数据和模板可以不同,但代码核心和数据库却是共享的。

public class Client {

public static void main(String[] args) {
WebSiteFactory factory = new WebSiteFactory();

WebSite fx = factory.getWebSiteCategory("产品展示");
fx.use();

WebSite fy = factory.getWebSiteCategory("产品展示");
fy.use();

WebSite fz = factory.getWebSiteCategory("产品展示");
fz.use();

WebSite fa = factory.getWebSiteCategory("博客");
fa.use();

WebSite fb = factory.getWebSiteCategory("博客");
fb.use();

WebSite fc = factory.getWebSiteCategory("博客");
fc.use();

System.out.println("网站分类总数为:" + factory.getWebSiteCount());
}

}

源码中的享元模式

享元模式很重要,因为它能帮你在一个复杂的系统中大量的节省内存空间。在JAVA语言中,String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a=”abc”,其中”abc”就是一个字符串常量。


熟悉java的应该知道下面这个例子:


Stringa="hello";
Stringb="hello";
if(a==b)
 System.out.println("OK");
else
 System.out.println("Error");

输出结果是:OK。可以看出if条件比较的是两a和b的地址,也可以说是内存空间 核心总结,可以共享的对象,也就是说返回的同一类型的对象其实是同一实例,当客户端要求生成一个对象时,工厂会检测是否存在此对象的实例,如果存在那么直接返回此对象实例,如果不存在就创建一个并保存起来,这点有些单例模式的意思。通常工厂类会有一个集合类型的成员变量来用以保存对象,如hashtable,vector等。在java中,数据库连接池,线程池等即是用享元模式的应用。


首先String不属于8种基本数据类型,String是一个对象。
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
new String()和new String(“”)都是申明一个新的空字符串,是空串不是null;
String str=”kvill”;
String str=new String (“kvill”);的区别:
在这里,我们不谈堆,也不谈栈,只先简单引入常量池这个简单的概念。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
看例1:

String s0=”kvill”; 

String s1=”kvill”;

String s2=”kv” + “ill”;

System.out.println( s0==s1 );

System.out.println( s0==s2 );

结果为:


true 

true

首先,我们要知结果为道Java会确保一个字符串常量只有一个拷贝。
因为例子中的s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”kvill”的一个引用。

所以我们得出s0==s1==s2;


用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。


看例2:


String s0=”kvill”; 

String s1=new String(”kvill”);

String s2=”kv” + new String(“ill”);

System.out.println( s0==s1 );

System.out.println( s0==s2 );

System.out.println( s1==s2 );

结果为:


false 

false

false

例2中s0还是常量池中”kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分new String(“ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。


String.intern():

再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看例3就清楚了


例3:


String s0= “kvill”; 

String s1=new String(”kvill”);

String s2=new String(“kvill”);

System.out.println( s0==s1 );

System.out.println( “**********” );

s1.intern();

s2=s2.intern(); //把常量池中“kvill”的引用赋给s2

System.out.println( s0==s1);

System.out.println( s0==s1.intern() );

System.out.println( s0==s2 );

结果为:


false 

**********

false //虽然执行了s1.intern(),但它的返回值没有赋给s1

true //说明s1.intern()返回的是常量池中”kvill”的引用

true

最后我再破除一个错误的理解:


有人说,“使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中“如果我把他说的这个全局的String表理解为常量池的话,他的最后一句话,“如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:


看例4:


String s1=new String("kvill"); 

String s2=s1.intern();

System.out.println( s1==s1.intern() );

System.out.println( s1+" "+s2 );

System.out.println( s2==s1.intern() );

结果:


false 

kvill kvill

true

在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。


s1==s1.intern()为false说明原来的“kvill”仍然存在;


s2现在为常量池中“kvill”的地址,所以有s2==s1.intern()为true。


关于equals()和==:

这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。


关于String是不可变的

这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”;


就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” “ 生成 ”kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。


okhttp3 kotlin ConnectionPool 源码分析

ConnectionPool的说明:
管理http和http/2的链接,以便减少网络请求延迟。同一个address将共享同一个connection。该类实现了复用连接的目标。

class RealConnectionPool(
/** 每个address的最大空闲连接数 */
private val maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
//这是一个用于清楚过期链接的线程池,每个线程池最多只能运行一个线程,并且这个线程池允许被垃圾回收
private val executor = ThreadPoolExecutor(
0, // corePoolSize.
Int.MAX_VALUE, // maximumPoolSize.
60L, TimeUnit.SECONDS, // keepAliveTime.
SynchronousQueue(),
threadFactory("OkHttp ConnectionPool", true)
)
//双向队列
private val connections = ArrayDeque<RealConnection>()
//路由的数据库
val routeDatabase = RouteDatabase()
//清理任务正在执行的标志
var cleanupRunning: Boolean = false


  1. 主要就是connections,可见ConnectionPool内部以队列方式存储连接;
  2. routDatabase是一个黑名单,用来记录不可用的route,但是看代码貌似ConnectionPool并没有使用它。所以此处不做分析。
  3. 剩下的就是和清理有关了,所以executor是清理任务的线程池,cleanupRunning是清理任务的标志,cleanupRunnable是清理任务。

class ConnectionPool(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
//创建一个适用于单个应用程序的新连接池。
//该连接池的参数将在未来的okhttp中发生改变
//目前最多可容乃5个空闲的连接,存活期是5分钟
constructor() : this(5, 5, TimeUnit.MINUTES)
}

init {
//保持活着的时间,否则清理将旋转循环
require(keepAliveDuration > 0L) { "keepAliveDuration <= 0: $keepAliveDuration" }
}

通过这个构造器我们知道了这个连接池最多维持5个连接,且每个链接最多活5分钟。并且包含一个线程池包含一个清理任务。
所以maxIdleConnections和keepAliveDurationNs则是清理中淘汰连接的的指标,这里需要说明的是maxIdleConnections是值每个地址上最大的空闲连接数。所以OkHttp只是限制与同一个远程服务器的空闲连接数量,对整体的空闲连接并没有限制。

这时候说下ConnectionPool的实例化的过程,一个OkHttpClient只包含一个ConnectionPool,其实例化也是在OkHttpClient的过程。这里说一下ConnectionPool各个方法的调用并没有直接对外暴露,而是通过OkHttpClient的Internal接口统一对外暴露。


然后我们来看下他的transmitterAcquirePooledConnection(获取连接)和put方法


fun transmitterAcquirePooledConnection(
address: Address,
transmitter: Transmitter,
routes: List<Route>?,
requireMultiplexed: Boolean
): Boolean {
//断言,判断线程是不是被自己锁住了
assert(Thread.holdsLock(this))
// 遍历已有连接集合
for (connection in connections) {
if (requireMultiplexed && !connection.isMultiplexed) continue
//如果connection和需求中的"地址"和"路由"匹配
if (!connection.isEligible(address, routes)) continue
//复用这个连接
transmitter.acquireConnectionNoEvents(connection)

return true
}
return false
}

put方法更为简单,就是异步触发清理任务,然后将连接添加到队列中


  fun put(connection: RealConnection) {
assert(Thread.holdsLock(this))
if (!cleanupRunning) {
cleanupRunning = true
executor.execute(cleanupRunnable)
}
connections.add(connection)
}

private val cleanupRunnable = object : Runnable {
override fun run() {
while (true) {
val waitNanos = cleanup(System.nanoTime())
if (waitNanos == -1L) return
try {
this@RealConnectionPool.lockAndWaitNanos(waitNanos)
} catch (ie: InterruptedException) {
// Will cause the thread to exit unless other connections are created!
evictAll()
}
}
}
}
这个逻辑也很简单,就是调用cleanup方法执行清理,并等待一段时间,持续清理,其中cleanup方法返回的值来来决定而等待的时间长度。那我们继续来看下cleanup函数:
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE

// Find either a connection to evict, or the time that the next eviction is due.
synchronized(this) {
for (connection in connections) {
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
continue
}
//统计空闲连接数量
idleConnectionCount++

// If the connection is ready to be evicted, we're done.
val idleDurationNs = now - connection.idleAtNanos
if (idleDurationNs > longestIdleDurationNs) {
//找出空闲时间最长的连接以及对应的空闲时间
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
}
}

when {
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
// We've found a connection to evict. Remove it from the list, then close it below
// (outside of the synchronized block).
//在符合清理条件下,清理空闲时间最长的连接
connections.remove(longestIdleConnection)
}
idleConnectionCount > 0 -> {
// A connection will be ready to evict soon.
//不符合清理条件,则返回下次需要执行清理的等待时间,也就是此连接即将到期的时间
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
// All connections are in use. It'll be at least the keep alive duration 'til we run
// again.
//没有空闲的连接,则隔keepAliveDuration(分钟)之后再次执行
return keepAliveDurationNs
}
else -> {
// No connections, idle or in use.
cleanupRunning = false
return -1
}
}
}
//关闭socket资源
longestIdleConnection!!.socket().closeQuietly()

// Cleanup again immediately.
//这里是在清理一个空闲时间最长的连接以后会执行到这里,需要立即再次执行清理
return 0
}

这里的首先统计空闲连接数量,然后通过for循环查找最长空闲时间的连接以及对应空闲时长,然后判断是否超出最大空闲连接数(maxIdleConnections)或者或者超过最大空闲时间(keepAliveDurationNs),满足其一则清除最长空闲时长的连接。如果不满足清理条件,则返回一个对应等待时间。
这个对应等待的时间又分二种情况:


  1. 有连接则等待下次需要清理的时间去清理:keepAliveDurationNs-longestIdleDurationNs;
  2. 没有空闲的连接,则等下一个周期去清理:keepAliveDurationNs
    如果清理完毕返回-1。

综上所述,我们来梳理一下清理任务,清理任务就是异步执行的,遵循两个指标,最大空闲连接数量和最大空闲时长,满足其一则清理空闲时长最大的那个连接,然后循环执行,要么等待一段时间,要么继续清理下一个连接,知道清理所有连接,清理任务才结束,下一次put的时候,如果已经停止的清理任务则会被再次触发


private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
val references = connection.transmitters
var i = 0
//遍历弱引用列表
while (i < references.size) {
val reference = references[i]
//若StreamAllocation被使用则接着循环
if (reference.get() != null) {
i++
continue
}

// We've discovered a leaked transmitter. This is an application bug.
val transmitterRef = reference as TransmitterReference
val message = "A connection to ${connection.route().address.url} was leaked. " +
"Did you forget to close a response body?"
Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace)
//若StreamAllocation未被使用则移除引用,这边注释为泄露
references.removeAt(i)
connection.noNewExchanges = true
//如果列表为空则说明此连接没有被引用了,则返回0,表示此连接是空闲连接
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs
return 0
}
}

return references.size
}

pruneAndGetAllocationCount主要是用来标记泄露连接的。内部通过遍历传入进来的RealConnection的StreamAllocation列表,如果StreamAllocation被使用则接着遍历下一个StreamAllocation。如果StreamAllocation未被使用则从列表中移除,如果列表中为空则说明此连接连接没有引用了,返回0,表示此连接是空闲连接,否则就返回非0表示此连接是活跃连接。
接下来让我看下ConnectionPool的connectionBecameIdle()方法,就是当有连接空闲时,唤起cleanup线程清洗连接池

fun connectionBecameIdle(connection: RealConnection): Boolean {
assert(Thread.holdsLock(this))
//该连接已经不可用
return if (connection.noNewExchanges || maxIdleConnections == 0) {
connections.remove(connection)
true
} else {
// Awake the cleanup thread: we may have exceeded the idle connection limit.
//欢迎clean 线程
this.notifyAll()
false
}
}

connectionBecameIdle标示一个连接处于空闲状态,即没有流任务,那么久需要调用该方法,由ConnectionPool来决定是否需要清理该连接。
再来看下evictAll()方法

fun evictAll() {
val evictedConnections = mutableListOf<RealConnection>()
synchronized(this) {
val i = connections.iterator()
while (i.hasNext()) {
val connection = i.next()
if (connection.transmitters.isEmpty()) {
connection.noNewExchanges = true
evictedConnections.add(connection)
i.remove()
}
}
}

for (connection in evictedConnections) {
connection.socket().closeQuietly()
}
}

该方法是删除所有空闲的连接,比较简单,不说了


Integer中的享元模式

那么我们来看看Integer中的享元模式具体是怎么样的吧。
通过如下代码了解一下integer的比较

public static void main(String[] args)
{
Integer integer1 = 9;
Integer integer2 = 9;
System.out.println(integer1==integer2);

Integer integer3 = 129;
Integer integer4 = 129;
System.out.println(integer3==integer4);
}

输出:


true
false

在通过等号赋值的时候,实际上是通过调用valueOf方法的返回一个对象。然后我们观察一下这个方法的源码。


public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private final int value;
public Integer(int value) {
this.value = value;
}
}

上面是我简化了的Integer类。平常在使用Integer类的时候。你是否思考过用valueOf还是用new创建Integer对象。看完源码就会发现在valueOf这个方法中它会先判断传进去的值是否在IntegerCache中,如果不在就创建新的对象,在就直接返回缓存池里的对象。这个valueOf方法就用到享元模式。它将-128到127的Integer对象先在缓存池里创建好,等我们需要的时候直接返回即可。所以在-128到127中的数值我们用valueOf创建会比new更快。因此我们在使用Integer对象的时候,也一定要记住使用equals(),而不是单纯的使用”==”,否则有可能出现不相等的情况。


收起阅读 »

java设计模式:桥接模式

桥接模式的定义与特点桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。 通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了...
继续阅读 »

桥接模式的定义与特点

桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。


通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。


优点


  • 抽象与实现分离,扩展能力强
  • 符合开闭原则
  • 符合合成复用原则
  • 其实现细节对客户透明

缺点

由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。


桥接模式的结构与实现

可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。


模式的结构

抽象化角色:定义抽象类,并包含一个对实现化对象的引用。
扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
实现化角色:定义实现化角色的接口,供扩展抽象化角色调用。
具体实现化角色:给出实现化角色接口的具体实现。

桥接模式的应用场景

当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。


桥接模式通常适用于以下场景。



  1. 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  2. 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  3. 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。

桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。


示例代码:
在这里插入图片描述


//抽象类:建筑
public abstract class Building {
protected Paint paint;
public Building(Paint paint) {
this.paint = paint;
}
public abstract void decorate();
}
//接口:油漆
public interface Paint {
void decorateImpl();
}
//教学楼
public class TeachingBuilding extends Building {
public TeachingBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的教学楼");
paint.decorateImpl();
}
}
//实验楼
public class LaboratoryBuilding extends Building {
public LaboratoryBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的实验楼");
paint.decorateImpl();
}
}
public class RedPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被红色油漆装饰过。");
}
}
public class GreenPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被绿色油漆装饰过。");
}
}
public class BulePaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被蓝色油漆装饰过。");
}
}
public class BridgePatternDemo {
public static void main(String[] args) {
//普通的教学楼被红色油漆装饰。
Building redTeachingBuilding=new TeachingBuilding(new RedPaint());
redTeachingBuilding.decorate();
//普通的教学楼被绿色油漆装饰。
Building greenTeachingBuilding1=new TeachingBuilding(new GreenPaint());
greenTeachingBuilding1.decorate();
//普通的实验楼被红色油漆装饰。
Building redLaboratoryBuilding=new LaboratoryBuilding(new RedPaint());
redLaboratoryBuilding.decorate();
//普通的实验楼被绿色油漆装饰。
Building greenLaboratoryBuilding=new LaboratoryBuilding(new GreenPaint());
greenLaboratoryBuilding.decorate();
//普通的实验楼被蓝色油漆装饰。
Building blueLaboratoryBuilding=new LaboratoryBuilding(new BulePaint());
blueLaboratoryBuilding.decorate();
}
}

运行结果:
普通的教学楼被红色油漆装饰过。
普通的教学楼被绿色油漆装饰过。
普通的实验楼被红色油漆装饰过。
普通的实验楼被绿色油漆装饰过。
普通的实验楼被蓝色油漆装饰过。

桥接模式与装饰模式对比:

两个模式都是为了解决子类过多问题, 但他们的诱因不同:



  1. 桥接模式对象自身有 沿着多个维度变化的趋势 , 本身不稳定;
  2. 装饰者模式对象自身非常稳定, 只是为了增加新功能/增强原功能。

收起阅读 »

你有原则么?懂原则么?想了解么?快看设计模式原则篇,让你做个有原则的程序员

前言无论做啥,要想好设计,就得多扩展,少修改 开闭原则此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modificatio...
继续阅读 »

前言

无论做啥,要想好设计,就得多扩展,少修改



开闭原则

此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modification”。就是说模块应对扩展开放,而对修改关闭。模块应尽量在不修改原(是”原”,指原来的代码)代码的情况下进行扩展


开闭原则的含义

当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。


开闭原则的作用


  1. 对软件测试的影响
    软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
  2. 可以提高代码的可复用性
    粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
  3. 可以提高软件的可维护性
    遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。

里氏替换原则

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。


里氏替换原则的作用

它克服了继承中重写父类造成的可复用性变差的缺点。
它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

里氏替换原则的实现方法

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。


依赖倒置原则

要面向接口编程,不要面向实现编程。


依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。


由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。


使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。


依赖、倒置原则的作用


  • 依赖倒置原则的主要作用如下。
  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性。
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

单一职责原则

单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分


单一职责原则的优点

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。



  • 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
  • 提高类的可读性。复杂性降低,自然其可读性会提高。
  • 提高系统的可维护性。可读性提高,那自然更容易维护了。
  • 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

接口隔离原则

尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
一个类对另一个类的依赖应该建立在最小的接口上

要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。


接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:



  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

    接口隔离原则的优点

    接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。


  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  3. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
  4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  5. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

迪米特法则

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。


迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。


  • 降低了类之间的耦合度,提高了模块的相对独立性。
  • 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。


合成复用原则

要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。


如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。


合成复用原则的重要性

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。



  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。



  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

收起阅读 »

华为手机升级HarmonyOS全攻略:公测&内测&线下升级

写在前面:本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)必要说明:所有消费者公测渠道最终都会跳转到花粉俱乐部;初期申请量巨大...
继续阅读 »

写在前面:

本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!

目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)

必要说明:

所有消费者公测渠道最终都会跳转到花粉俱乐部;

初期申请量巨大,花粉俱乐部很容易就挂掉,心急的小伙伴可尝试线下渠道或者多次尝试或者深夜(两点以后)申请;

申请前务必将“花粉俱乐部”、“我的华为”、“会员中心”升级到最新版本,尤其是“花粉俱乐部”。

消费者公测

包含机型:

Mate X2

Mate40、Mate40E、Mate 40 Pro、Mate 40 Pro+、Mate 40 RS 保时捷设计

P40 5G、P40 4G、P40 Pro、P40 Pro+

Mate 30 4G、Mate 30 Pro 4G、Mate 30 5G、Mate 30 Pro 5G、Mate 30 RS保时捷设计、Mate 30E Pro 5G

MatePad Pro、MatePad Pro 5G

我的华为/花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开“我的华为”,点击“升级尝鲜” / 打开“花粉俱乐部”,点击“公测尝鲜”。

△我的华为

△花粉俱乐部

2.页面加载完成后,点击“公测尝鲜”下的“立即尝鲜”按钮。(花粉俱乐部进入的请忽略此步骤,直接进入下步)

3.接下来在列表中找到当前手机型号对应的公测活动,点击“报名公测”。由于不同手机对应的系统版本不一样,请务必仔细核实你的机器型号。

4.此处会跳转到“花粉论坛”的一篇帖子,划到这篇帖子的末尾,点击“参加公测活动”。接下来系统会引导用户签订《华为公测协议》和《华为公测与隐私声明》,等待10秒点击通过。

5.通过两个协议后,系统会引导你下载协议文件。这个过程会验证你的机型是否符合要求,且下载的文件也是将来升级为正式版的必要文件,如果找到(反正我是没找到)请勿删除!!

6.下载并提示安装完描述文件后,就可以去检测系统更新,下载并更新HarmonyOS了。(P40系列当前版本116)

消费者内测

包含机型:

Mate XS、Mate 20、Mate 20 Pro、Mate 20 RS(保时捷)、Mate 20 X(4G)

nova 8、nova 8 Pro、nova 8 SE、nova 7 5G、nova 7 Pro 5G、nova 7 SE 5G、nova 7 SE 5G活力版、nova 7 SE 5G乐活版、nova 6、nova 6 5G、nova 6 SE

华为畅享20 Plus 5G、华为畅享Z 5G、华为畅享20 Pro5G

华为麦芒9 5G

MatePad 10.8、MatePad 5G 10.4、MatePad 10.4

内测时间:6月2日~6月9日上午10:00

渠道一

会员中心:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开会员中心,首页上找到“体验先锋”,点击进入。

2.点击顶部的HarmonyOS 2升级尝鲜。

3.进入页面点击报名。如果机型不符合,会弹出提示框。

4.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

渠道二

花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.进入首页点击内测报名

2.跳转后,点击立即报名

3.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

HarmonyOS体验官(线下)

包含机型:

我的华为:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

在APP首页点击“HarmonyOS体验官”海报,经过简单的互动问答即可参加。期间需要提交信息、预约门店时间和信息,最终会生成一张包含数字的海报,用户需要保存此海报才可参与活动。

活动仅在部分门店进行,具体店面和城市请在活动页面查询。到店会提供礼品,并可在线下由店面工作人员协助完成升级。

重要的补充说明

1.消费者公测仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

2.消费者内测仍然会存在审核机制,但仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

3.最终稳定的系统版本号预计为:HarmonyOS 2.0.0.116(以实际推送版本号为准!)

4.老荣耀系列机型不在本次消费者公测列表中。

收起阅读 »

FBKVOController - 面试聊到KVO如何有效的怒怼面试官!

1.系统KVO的问题2.FBKVOController优点3.FBKVOController的架构设计图4.FBKVOController源码详读5.FBKVOController总结一.系统KVO的问题当观察者被销毁之前,需要手动移除观察者,否则会出现程序异...
继续阅读 »
  • 1.系统KVO的问题
  • 2.FBKVOController优点
  • 3.FBKVOController的架构设计图
  • 4.FBKVOController源码详读
  • 5.FBKVOController总结

一.系统KVO的问题

  • 当观察者被销毁之前,需要手动移除观察者,否则会出现程序异常(向已经销毁的对象发送消息);
  • 可能会对同一个被监听的属性多次添加监听,这样我们会接收到多次监听的回调结果;
  • 当观察者对多个对象的不同属性进行监听,处理监听结果时,需要在监听回调的方法中,作出大量的if判断;
  • 当对同一个被监听的属性进行两次removeObserver时,会导致程序crash。这种情况通常出现在父类中有一个KVO,在父类的dealloc中remove一次,而在子类中再次remove。

二. FBKVOController优点

  • 可以同时对一个对象的多个属性进行监听,写法简洁;
  • 通知不会向已释放的观察者发送消息;
  • 增加了block和自定义操作对NSKeyValueObserving回调的处理支持;
  • 不需要在dealloc 方法中手动移除观察者,而且移除观察者不会抛出异常,当FBKVOController对象被释放时, 观察者被隐式移除;

三.FBKVOController架构设计图






四.FBKVOController源码详解

FBKVOController源码详解分四部分:

  • 私有类_FBKVOInfo,
  • 私有类_FBKVOSharedController
  • FBKVOController,
  • NSObject+FBKVOController的源码解读:
(一)FBKVOController

首先我们创建一个FBKVOController的实例对象时,有以下三种方法,一个类方法和两个对象方法,

//该方法是一个全能初始化的对象方法,其他初始化方法内部均调用该方法
//参数:observer是观察者,retainObserved:表示是否强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
- (instancetype)initWithObserver:(nullable id)observer;

//该初始化方法内部调用上一个初始化方法,默认强引用被观察的对象
+ (instancetype)controllerWithObserver:(nullable id)observer;
NS_DESIGNATED_INITIALIZER;

我们先来看全能初始化方法内部的实现,

  • 该方法对三个实例变量_observer(观察者)
  • _objectInfosMap(NSMapTable,被监听对象->被监听属性集合之间的映射关系)
  • pthread_mutex_init(互斥锁)

//全能初始化方法
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {

//观察者
_observer = observer;

//NSMapTable中的key可以为对象,而且可以对其中的key和value弱引用
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];

//对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER
//对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy
pthread_mutex_init(&_lock, NULL);
}
return self;
}

这里请先思考以下问题:

  • 属性observer为何使用weak,它和哪个对象之间会导致循环引用问题,是如何导致循环引用问题的?
  • 为何不使用字典来保存被监听对象和被监听属性集合之间的关系?
  • NSDictionary的局限性有哪些?NSMapTable相对字典,有哪些优点?
  • 互斥锁是为了保证哪些数据的线程安全?

带着这些问题我们来看FBKVOController内部是如何实现监听的,这里我们只看带Block回调的一个监听方法,其他几个方法和这个方法内部实现是相同的。下面的方法内部做了如下工作:

  • 1.传入的参数keyPath,block为空时,程序闪退,同时报出误提示;
  • 2.对传入参数为空的判读;
  • 3.利用传入的参数创建_FBKVOInfo对象;
  • 4.调用内部私有方法实现注册监听;
//观察者监听object中健值路径(keyPath)所对应属性的变化
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
//NSAssert是一个预处理宏, 它可以让开发者比较便捷的捕获错误, 让程序闪退, 同时报出错误提示
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);

//首先判断被监听的对象是否为空,被监听的健值路径是否为空,回调的block是否为空
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}

// 根据传进来的参数创建_FBKVOInfo对象,将这些参数封装到_FBKVOInfo对象中
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

// 监听对象object的属性信息(_FBKVOInfo对象)
[self _observe:object info:info];
}

该私有方法内部并没有实现真正的注册监听,这里使用NSMapTable保存了被监听对象object-> _FBKVOInfo对象集合的关系,具体的监听是在_FBKVOSharedController类中实现的。观察者可以监听多个对象,而每个对象中可能有多个属性被监听


内部实现思路:

  • 对当前线程访问的数据_objectInfosMap进行加锁;
  • 根据被监听对象object到_objectInfosMap取出被监听的属性信息对象集合infos;
  • 判断被监听的属性对象info是否存在集合中;
  • 如果已经存在,则不需要再次添加监听,防止多次监听;
  • 如果获取的集合infos为空,则建存放_FBKVOInfo对象的集合infos,保存映射关系:object->infos;
  • 将被监听的信息_FBKVOInfo对象存到集合infos中;
  • 解锁,其他线程可以访问该数据;
  • 调用_FBKVOSharedController 的方法实现监听;
//该方法是内部私有方法
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
//先加锁,访问_objectInfosMap
pthread_mutex_lock(&_lock);

//到_objectInfosMap中根据key(被监听的对象)获取被监听的属性信息集合
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

//判断infos集合中是否存在被监听属性信息对象info
_FBKVOInfo *existingInfo = [infos member:info];

//被监听对象的属性已经存在,不需要再次监听,防止多次添加监听
if (nil != existingInfo) {

//解锁,其他线程可以再次访问_objectInfosMap中的数据
pthread_mutex_unlock(&_lock);
return;
}

//根据被监听对象在_objectInfosMap获取的被监听属性信息的集合为空
if (nil == infos) {
//懒加载创建存放_FBKVOInfo对象的set集合infos
infos = [NSMutableSet set];

//保存被监听对象和被监听属性信息的映射关系object->infos
[_objectInfosMap setObject:infos forKey:object];
}

// 将被监听的信息_FBKVOInfo对象存到集合infos中
[infos addObject:info];

//解锁
pthread_mutex_unlock(&_lock);

//最终的监听方法是通过_FBKVOSharedController中的方法来实现
//_FBKVOSharedController内部实现系统KVO方法
[[_FBKVOSharedController sharedController] observe:object info:info];
}
(二)_FBKVOInfo

_FBKVOInfo私有类的内部很简单,没有任何业务逻辑,只是一个简单的Model,主要是将以下的实例变量封装到对象中,方便访问:

{
@public
//weak,防止循环引用
__weak FBKVOController *_controller;
//被监听属性的健值路径
NSString *_keyPath;

//NSKeyValueObservingOptionNew:观察修改前的值
// NSKeyValueObservingOptionOld:观察修改后的值
//NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
//NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(一次修改有两次触发)
NSKeyValueObservingOptions _options;

//被监听属性值变化时的回调方法
SEL _action;

//上下文信息(void * 任何类型)
void *_context;
//被监听属性值变化时的回调block
FBKVONotificationBlock _block;
//监听状态
_FBKVOInfoState _state;
}

_FBKVOInfo私有类提供了一个全能初始化方法,来初始化以上实例变量。其他几个部分初始化方法内部均调用该全能初始化方法。

//全能初始化方法
- (instancetype)initWithController:(FBKVOController *)controller
keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
block:(nullable FBKVONotificationBlock)block
action:(nullable SEL)action
context:(nullable void *)context
{
self = [super init];
if (nil != self) {
_controller = controller;
_block = [block copy];
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}


优化判断对象相等性的效率:
  • 1.首先判断hash值是否相等,若相等则进行第2步;若不等,则直接判断不等;hash值是对象判等的必要非充分条件;(即没它一定不行,有它不一定行)
  • 2.在hash值相等的情况下,再进行对象判等, 作为判等的结果
//当重写hash方法时,我们可以将关键属性的hash值进行位或运算来作为hash值
- (NSUInteger)hash
{
return [_keyPath hash];
}

/**
对于基本类型, ==运算符比较的是值;
对于对象类型, ==运算符比较的是对象的地址(即是否为同一对象)
*/

- (BOOL)isEqual:(id)object
{
//判断对象是否为空,若为空,则不相等
if (nil == object) {
return NO;
}

//判断对象的地址是否相等,若相等,则为同一个对象(即是否为同一个对象)
if (self == object) {
return YES;
}

//判断是否是同一类型,这样可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险
if (![object isKindOfClass:[self class]]) {
return NO;
}

//对各个属性分别使用默认判等方法进行判断
//返回所有属性判等的与结果
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

//输出对象的调试信息

//description: 使用NSLog从控制台输出对象的信息
//debugDescription:通过断点po打印输出对象的信息
- (NSString *)debugDescription
{
NSMutableString *s = [NSMutableString stringWithFormat:@"<%@:%p keyPath:%@", NSStringFromClass([self class]), self, _keyPath];
if (0 != _options) {
[s appendFormat:@" options:%@", describe_options(_options)];
}
if (NULL != _action) {
[s appendFormat:@" action:%@", NSStringFromSelector(_action)];
}
if (NULL != _context) {
[s appendFormat:@" context:%p", _context];
}
if (NULL != _block) {
[s appendFormat:@" block:%p", _block];
}
[s appendString:@">"];
return s;
}

  • 请分析如果将实例变量__weak FBKVOController *_controller前的 __weak去掉,它和_FBKVOInfo对象之间的循环引用环是如何形成的?
(三)_FBKVOSharedController

_FBKVOSharedController私有类内部实现了系统KVO的方法,用来接收和转发KVO的通知。接口中提供了监听和移除监听的方法。其接口如下:


@interface _FBKVOSharedController : NSObject

// 单例初始化方法
+ (instancetype)sharedController;

// 监听object的属性
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info;

//移除对object中属性的监听
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info;

// 移除对object中多个属性的监听
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;

@end
_FBKVOSharedController私有类内部有两个私有成员变量,_infos是用来存放_FBKVOInfo对象,_infos可以对其中的成员变量弱引用,这也是为何使用NSHashTable,而不使用NSSet来存放_FBKVOInfo对象的原因。_mutex是互斥锁:

{
//存放被监听属性的信息对象
NSHashTable<_FBKVOInfo *> *_infos;
//互斥锁
pthread_mutex_t _mutex;
}

_FBKVOSharedController私有类的初始化方法,支持iOS 系统和Mac系统,初始化实例变量_infos,指定了_infos对存放在其中的成员变量弱引用,及判等性方式:

//提供全局的单例初始化方法,该单例对象的生命周期与程序的生命周期相同
+ (instancetype)sharedController
{
static _FBKVOSharedController *_controller = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_controller = [[_FBKVOSharedController alloc] init];
});
return _controller;
}
//初始化成员变量_infos和_mutex
- (instancetype)init
{
self = [super init];
if (nil != self) {
//初始化实例变量
NSHashTable *infos = [NSHashTable alloc];

// iOS 系统下:hashTable中的对象是弱引用,对象的判等方式:位移指针的hash值和直接判等
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];

//MAC系统下
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}

#endif
//初始化互斥锁
pthread_mutex_init(&_mutex, NULL);
}
return self;
}

- (void)dealloc
{
//对象被销毁时,销毁互斥锁
pthread_mutex_destroy(&_mutex);
}
_FBKVOSharedController在这个方法中,调用系统KVO方法,将自己注册为观察者,思路如下:
1.首先将被监听的信息对象_FBKVOInfo保存到_infos中;
2.然后调用系统KVO方法将自己注册为被监听对象object的观察者;
3.最后修改监听的状态;当不再监听时,安全移除观察者;

//添加监听
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
//被监听的属性信息_FBKVOInfo对象为空时,直接返回
if (nil == info) {
return;
}

// 加锁,防止多线程访问时,出现数据竞争
pthread_mutex_lock(&_mutex);

// 将被监听的属性信息info对象添加到_infos中,_infos对成员变量info是弱引用
[_infos addObject:info];

//添加完成之后,解锁,其他线程可以访问
pthread_mutex_unlock(&_mutex);

// 添加监听
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

//修改监听状态
if (info->_state == _FBKVOInfoStateInitial) {

info->_state = _FBKVOInfoStateObserving;

} else if (info->_state == _FBKVOInfoStateNotObserving) {

//不再监听时安全移除观察者
// this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
// and the observer is unregistered within the callback block.
// at this time the object has been registered as an observer (in Foundation KVO),
// so we can safely unobserve it.
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
}
实现系统KVO监听回调的方法

//被监听属性更改时的回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context
{
NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

_FBKVOInfo *info;
{
pthread_mutex_lock(&_mutex);
//确定_infos是否包含给定的对象context,若存在返回该对象,否则返回nil;
//所使用的相等性比较取决于所选择的选项
//例如,使用NSPointerFunctionsObjectPersonality选项将使用isEqual:方法来判断相等。
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}

//通过上下文参数context传过来的被监听的_FBKVOInfo对象,已经存在_infos中
if (nil != info) {

//_FBKVOSharedController对象强引用FBKVOController对象,防止被提前释放
//因为在_FBKVOInfo中,对FBKVOController对象是弱引用
FBKVOController *controller = info->_controller;
if (nil != controller) {

//强引用观察者,在FBKVOController中,FBKVOController对象弱引用观察者observer,防止在使用时已经被释放
id observer = controller.observer;
if (nil != observer) {

//使用自定义block回传监听结果
if (info->_block) {

NSDictionary<NSString *, id> *changeWithKeyPath = change;

//将keyPath添加到字典中以便在观察多个keyPath时,能够清晰知道监听的是哪个keyPath
if (keyPath) {
NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath = [mChange copy];
}
info->_block(observer, object, changeWithKeyPath);

} else if (info->_action) {
//使用自定义方法回传监听结果
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
} else {
//使用系统默认方法回传监听结果
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}

_FBKVOSharedController实现了移除观察者的方法,思路如下:

  • 1.首先从_infos中移除被监听的属性信息对象info;
  • 2.然后根据监听状态,通过调用系统的方法,移除正在被监听的属性信息对象info;
  • 3.最后修改监听状态;

- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}

//先从HashTable中移除被监听的属性信息对象
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);

// 当正在监听时,则移除监听
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
//修改被监听的状态
info->_state = _FBKVOInfoStateNotObserving;
}

(四)NSObject+FBKVOController

NSObject+FBKVOController 分类比较简单,它主要通过runtime方法,以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。



@interface NSObject (FBKVOController)
@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end

五.FBKVOController总结

FBKVOController是线程安全的,相对于系统的KVO而言,使用起来更方便,安全,简洁。

  • 1.NSHashTable和NSMapTable的使用;
  • 2.互斥锁pthread_mutex_t的使用
  • 3.FBKVOController和Observer之间循环引用的形成和解决;
  • 4.FBKVOController和_FBKVOInfo之间循环引用的形成和解决;


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

收起阅读 »

没对象么?那就来了解Java创建对象详解

对象是对类的实例化。对象具有状态和行为,变量用来表明对象的状态,方法表明对象所具有的行为。Java 对象的生命周期包括创建、使用和清除,本文详细介绍对象的创建 Java虚拟机内存架构模型详解 1.使用new创建对象 使用new关键字创建对象应该是最常见的一种...
继续阅读 »

对象是对类的实例化。对象具有状态和行为,变量用来表明对象的状态,方法表明对象所具有的行为。Java 对象的生命周期包括创建、使用和清除,本文详细介绍对象的创建



Java虚拟机内存架构模型详解


1.使用new创建对象


使用new关键字创建对象应该是最常见的一种方式,但我们应该知道,使用new创建对象会增加耦合度。无论使用什么框架,都要减少new的使用以降低耦合度。


public class Hello
{
public void sayWorld()
{
System.out.println("Hello world!");
}

}
public class NewClass
{
public static void main(String[] args)
{
Hello h = new Hello();
h.sayWorld();
}
}
复制代码

2.使用反射的机制创建对象


使用Class类的newInstance方法


  Hello类的代码不变,NewClass类的代码如下:


public class NewClass
{
public static void main(String[] args)
{
try
{
Class heroClass = Class.forName("yunche.test.Hello");
Hello h =(Hello) heroClass.newInstance();
h.sayWorld();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}

}
}
复制代码

使用Constructor类的newInstance方法


public class NewClass
{
public static void main(String[] args)
{
try
{
//获取类对象
Class heroClass = Class.forName("yunche.test.Hello");

//获取构造器
Constructor constructor = heroClass.getConstructor();
Hello h =(Hello) constructor.newInstance();
h.sayWorld();
}
catch (NoSuchMethodException e)
{
e.printStackTrace();
}
catch (InvocationTargetException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}

}
}
复制代码

3.采用clone


  clone时,需要已经有一个分配了内存的源对象,创建新对象时,首先应该分配一个和源对象一样大的内存空间。


  要调用clone方法需要实现Cloneable接口,由于clone方法是protected的,所以修改Hello类。


public class Hello implements Cloneable
{
public void sayWorld()
{
System.out.println("Hello world!");

}

public static void main(String[] args)
{
Hello h1 = new Hello();
try
{
Hello h2 = (Hello)h1.clone();
h2.sayWorld();
}
catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
}
}
复制代码

4.采用序列化机制


  使用序列化时,要实现实现Serializable接口,将一个对象序列化到磁盘上,而采用反序列化可以将磁盘上的对象信息转化到内存中。


public class Serialize
{
public static void main(String[] args)
{
Hello h = new Hello();

//准备一个文件用于存储该对象的信息
File f = new File("hello.obj");

try(FileOutputStream fos = new FileOutputStream(f);
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream(f);
ObjectInputStream ois = new ObjectInputStream(fis)
)
{
//序列化对象,写入到磁盘中
oos.writeObject(h);
//反序列化对象
Hello newHello = (Hello)ois.readObject();

//测试方法
newHello.sayWorld();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
}

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

iOS必备装X技能-NSOperationQueue 控制串行执行、并发执行

 NSOperationQueue 控制串行执行、并发执行NSOperationQueue 创建的自定义队列同时具有串行、并发功能,那么他的串行功能是如何实现的?这里有个关键属性 maxConcurrentOperationCount,叫做...
继续阅读 »

 NSOperationQueue 控制串行执行、并发执行

NSOperationQueue 创建的自定义队列同时具有串行、并发功能,那么他的串行功能是如何实现的?


这里有个关键属性 maxConcurrentOperationCount,叫做最大并发操作数。用来控制一个特定队列中可以有多少个操作同时参与并发执行。

注意:这里 maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行。


最大并发操作数:maxConcurrentOperationCount
  • maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
  • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
  • maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。

/**
* 设置 MaxConcurrentOperationCount(最大并发操作数)
*/

- (void)setMaxConcurrentOperationCount {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.设置最大并发操作数
queue.maxConcurrentOperationCount = 1; // 串行队列
// queue.maxConcurrentOperationCount = 2; // 并发队列
// queue.maxConcurrentOperationCount = 8; // 并发队列

// 3.添加操作
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
}
}];
[queue addOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}

当最大并发操作数为1时,操作是按顺序串行执行的,并且一个操作完成之后,下一个操作才开始执行。当最大操作并发数为2时,操作是并发执行的,可以同时执行两个操作。而开启线程数量是由系统决定的,不需要我们来管理

这样看来,是不是比 GCD 还要简单了许多

 NSOperation 操作依赖

NSOperation、NSOperationQueue 最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序。NSOperation 提供了3个接口供我们管理和查看依赖。

  • - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
  • - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
  • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。

当然,我们经常用到的还是添加依赖操作。现在考虑这样的需求,比如说有 A、B 两个操作,其中 A 执行完操作,B 才能执行操作。

如果使用依赖来处理的话,那么就需要让操作 B 依赖于操作 A。具体代码如下:


/**
* 操作依赖
* 使用方法:addDependency:
*/

- (void)addDependency {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.创建操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];

// 3.添加依赖
[op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2

// 4.添加操作到队列中
[queue addOperation:op1];
[queue addOperation:op2];
}

通过添加操作依赖,无论运行几次,其结果都是 op1 先执行,op2 后执行

NSOperation 优先级

NSOperation 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操作队列中的操作,不适用于不同操作队列中的操作。默认情况下,所有新创建的操作对象优先级都是NSOperationQueuePriorityNormal。但是我们可以通过setQueuePriority:方法来改变当前操作在同一队列中的执行优先级。


// 优先级的取值
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};

对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。

那么,什么样的操作才是进入就绪状态的操作呢?

  • 当一个操作的所有依赖都已经完成时,操作对象通常会进入准备就绪状态,等待执行。

举个例子,现在有4个优先级都是 NSOperationQueuePriorityNormal(默认级别)的操作:op1,op2,op3,op4。其中 op3 依赖于 op2,op2 依赖于 op1,即 op3 -> op2 -> op1。现在将这4个操作添加到队列中并发执行。

  • 因为 op1 和 op4 都没有需要依赖的操作,所以在 op1,op4 执行之前,就是处于准备就绪状态的操作。
  • 而 op3 和 op2 都有依赖的操作(op3 依赖于 op2,op2 依赖于 op1),所以 op3 和 op2 都不是准备就绪状态下的操作。

理解了进入就绪状态的操作,那么我们就理解了queuePriority 属性的作用对象。

  • queuePriority 属性决定了进入准备就绪状态下的操作之间的开始执行顺序。并且,优先级不能取代依赖关系。
  • 如果一个队列中既包含高优先级操作,又包含低优先级操作,并且两个操作都已经准备就绪,那么队列先执行高优先级操作。比如上例中,如果 op1 和 op4 是不同优先级的操作,那么就会先执行优先级高的操作。
  • 如果,一个队列中既包含了准备就绪状态的操作,又包含了未准备就绪的操作,未准备就绪的操作优先级比准备就绪的操作优先级高。那么,虽然准备就绪的操作优先级低,也会优先执行。优先级不能取代依赖关系。如果要控制操作间的启动顺序,则必须使用依赖关系。


 NSOperation、NSOperationQueue 线程间的通信


在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯

/**
* 线程间通信
*/

- (void)communication {

// 1.创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];

// 2.添加操作
[queue addOperationWithBlock:^{
// 异步进行耗时操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
}

// 回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 进行一些 UI 刷新等操作
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
}
}];
}];
}
通过线程间的通信,先在其他线程中执行操作,等操作执行完了之后再回到主线程执行主线程的相应操作

NSOperation、NSOperationQueue 线程同步和线程安全

  • 线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
  • 线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

下面,我们模拟火车票售卖的方式,实现 NSOperation 线程安全和解决线程同步问题。
场景:总共有50张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。


NSOperation、NSOperationQueue 非线程安全

先来看看不考虑线程安全的代码:

/**
* 非线程安全:不使用 NSLock
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/

- (void)initTicketStatusNotSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程

self.ticketSurplusCount = 50;

// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;

// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;

// 3.创建卖票操作 op1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketNotSafe];
}];

// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketNotSafe];
}];

// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}

/**
* 售卖火车票(非线程安全)
*/

- (void)saleTicketNotSafe {
while (1) {

if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"所有火车票均已售完");
break;
}
}
}

在不考虑线程安全,不使用 NSLock 情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题

NSOperation、NSOperationQueue 线程安全

线程安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/ge等等各种方式。这里我们使用 NSLock 对象来解决线程同步问题。NSLock 对象可以通过进入锁时调用 lock 方法,解锁时调用 unlock 方法来保证线程安全。

考虑线程安全的代码:


/**
* 线程安全:使用 NSLock 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/


- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程

self.ticketSurplusCount = 50;

self.lock = [[NSLock alloc] init]; // 初始化 NSLock 对象

// 1.创建 queue1,queue1 代表北京火车票售卖窗口
NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
queue1.maxConcurrentOperationCount = 1;

// 2.创建 queue2,queue2 代表上海火车票售卖窗口
NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
queue2.maxConcurrentOperationCount = 1;

// 3.创建卖票操作 op1
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketSafe];
}];

// 4.创建卖票操作 op2
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
[self saleTicketSafe];
}];

// 5.添加操作,开始卖票
[queue1 addOperation:op1];
[queue2 addOperation:op2];
}

/**
* 售卖火车票(线程安全)
*/

- (void)saleTicketSafe {
while (1) {

// 加锁
[self.lock lock];

if (self.ticketSurplusCount > 0) {
//如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
}

// 解锁
[self.lock unlock];

if (self.ticketSurplusCount <= 0) {
NSLog(@"所有火车票均已售完");
break;
}
}
}

在考虑了线程安全,使用 NSLock 加锁、解锁机制的情况下,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题

NSOperation 常用属性和方法

  1. 取消操作方法
    • - (void)cancel; 可取消操作,实质是标记 isCancelled 状态。
  2. 判断操作状态方法
    • - (BOOL)isFinished; 判断操作是否已经结束。
    • - (BOOL)isCancelled; 判断操作是否已经标记为取消。
    • - (BOOL)isExecuting; 判断操作是否正在在运行。
    • - (BOOL)isReady; 判断操作是否处于准备就绪状态,这个值和操作的依赖关系相关。
  3. 操作同步
    • - (void)waitUntilFinished; 阻塞当前线程,直到该操作结束。可用于线程执行顺序的同步。
    • - (void)setCompletionBlock:(void (^)(void))block; completionBlock 会在当前操作执行完毕时执行 completionBlock。
    • - (void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。
    • - (void)removeDependency:(NSOperation *)op; 移除依赖,取消当前操作对操作 op 的依赖。
    • @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。


 NSOperationQueue 常用属性和方法

  • 取消/暂停/恢复操作
    • - (void)cancelAllOperations; 可以取消队列的所有操作。
    • - (BOOL)isSuspended; 判断队列是否处于暂停状态。 YES 为暂停状态,NO 为恢复状态。
    • - (void)setSuspended:(BOOL)b; 可设置操作的暂停和恢复,YES 代表暂停队列,NO 代表恢复队列。
  • 操作同步
    • - (void)waitUntilAllOperationsAreFinished; 阻塞当前线程,直到队列中的操作全部执行完毕。
  • 添加/获取操作`
    • - (void)addOperationWithBlock:(void (^)(void))block; 向队列中添加一个 NSBlockOperation 类型操作对象。
    • - (void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向队列中添加操作数组,wait 标志是否阻塞当前线程直到所有操作结束
    • - (NSArray *)operations; 当前在队列中的操作数组(某个操作执行结束后会自动从这个数组清除)。
    • - (NSUInteger)operationCount; 当前队列中的操作数。
  • 获取队列
    • + (id)currentQueue; 获取当前队列,如果当前线程不是在 NSOperationQueue 上运行则返回 nil。
    • + (id)mainQueue; 获取主队列。


  • 注意:

    1. 这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕之后不再执行新的操作。
    2. 暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。


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





    收起阅读 »

    iOS面试-与面试官盘NSOperation、NSOperationQueue

    NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。为什么...
    继续阅读 »

    NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。

    为什么要使用 NSOperation、NSOperationQueue?

    1. 可添加完成的代码块,在操作完成后执行。
    2. 添加操作之间的依赖关系,方便的控制执行顺序。
    3. 设定操作执行的优先级。
    4. 可以很方便的取消一个操作的执行。
    5. 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。

    2. NSOperation、NSOperationQueue 操作和操作队列

    既然是基于 GCD 的更高一层的封装。那么,GCD 中的一些概念同样适用于 NSOperation、NSOperationQueue。在 NSOperation、NSOperationQueue 中也有类似的任务(操作)队列(操作队列)的概念。

    • 操作(Operation):
      • 执行操作的意思,换句话说就是你在线程中执行的那段代码。
      • 在 GCD 中是放在 block 中的。在 NSOperation 中,我们使用 NSOperation 子类 NSInvocationOperationNSBlockOperation,或者自定义子类来封装操作。
    • 操作队列(Operation Queues):
      • 这里的队列指操作队列,即用来存放操作的队列。不同于 GCD 中的调度队列 FIFO(先进先出)的原则。NSOperationQueue 对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后进入就绪状态的操作的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
      • 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。
      • NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。

    3. NSOperation、NSOperationQueue 使用步骤

    NSOperation 需要配合 NSOperationQueue 来实现多线程。因为默认情况下,NSOperation 单独使用时系统同步执行操作,配合 NSOperationQueue 我们能更好的实现异步执行。

    NSOperation 实现多线程的使用步骤分为三步:

    1. 创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。
    2. 创建队列:创建 NSOperationQueue 对象。
    3. 将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。

    之后呢,系统就会自动将 NSOperationQueue 中的 NSOperation 取出来,在新线程中执行操作。

    下面我们来学习下 NSOperation 和 NSOperationQueue 的基本使用。

    4. NSOperation 和 NSOperationQueue 基本使用

    4.1 创建操作

    NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。

    1. 使用子类 NSInvocationOperation
    2. 使用子类 NSBlockOperation
    3. 自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。

    在不使用 NSOperationQueue,单独使用 NSOperation 的情况下系统同步执行操作,下面我们学习以下操作的三种创建方式。


    4.1.1 使用子类 NSInvocationOperation

    /**
    * 使用子类 NSInvocationOperation
    */

    - (void)useInvocationOperation {

    // 1.创建 NSInvocationOperation 对象
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

    // 2.调用 start 方法开始执行操作
    [op start];
    }

    /**
    * 任务1
    */

    - (void)task1 {
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }
    输出结果:

    • 可以看到:在没有使用 NSOperationQueue、在主线程中单独使用使用子类 NSInvocationOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。
    如果在其他线程中执行操作,则打印结果为其他线程。
    // 在其他线程使用子类 NSInvocationOperation
    [NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

    • 可以看到:在其他线程中单独使用子类 NSInvocationOperation,操作是在当前调用的其他线程执行的,并没有开启新线程。

    下边再来看看 NSBlockOperation。


    4.1.2 使用子类 NSBlockOperation

    /**
    * 使用子类 NSBlockOperation
    */

    - (void)useBlockOperation {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.调用 start 方法开始执行操作
    [op start];
    }

    • 可以看到:在没有使用 NSOperationQueue、在主线程中单独使用 NSBlockOperation 执行一个操作的情况下,操作是在当前线程执行的,并没有开启新线程。

    注意:和上边 NSInvocationOperation 使用一样。因为代码是在主线程中调用的,所以打印结果为主线程。如果在其他线程中执行操作,则打印结果为其他线程。

    但是,NSBlockOperation 还提供了一个方法 addExecutionBlock:,通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。这些操作(包括 blockOperationWithBlock 中的操作)可以在不同的线程中同时(并发)执行。只有当所有相关的操作已经完成执行时,才视为完成。

    如果添加的操作多的话,blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock: 中的操作一定会在当前线程中执行。(可以使用 addExecutionBlock: 多添加几个操作试试)。


    /**
    * 使用子类 NSBlockOperation
    * 调用方法 AddExecutionBlock:
    */

    - (void)useBlockOperationAddExecutionBlock {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.添加额外的操作
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.调用 start 方法开始执行操作
    [op start];
    }

    /**
    * 使用子类 NSBlockOperation
    * 调用方法 AddExecutionBlock:
    */

    - (void)useBlockOperationAddExecutionBlock {

    // 1.创建 NSBlockOperation 对象
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 2.添加额外的操作
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"6---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"7---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"8---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.调用 start 方法开始执行操作
    [op start];
    }

    • 可以看出:使用子类 NSBlockOperation,并调用方法 AddExecutionBlock: 的情况下,blockOperationWithBlock:方法中的操作 和 addExecutionBlock: 中的操作是在不同的线程中异步执行的。而且,这次执行结果中 blockOperationWithBlock:方法中的操作也不是在当前线程(主线程)中执行的。从而印证了blockOperationWithBlock: 中的操作也可能会在其他线程(非当前线程)中执行。

    一般情况下,如果一个 NSBlockOperation 对象封装了多个操作。NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。当然开启的线程数是由系统来决定的。

    4.1.3 使用自定义继承自 NSOperation 的子类

    如果使用子类 NSInvocationOperation、NSBlockOperation 不能满足日常需求,我们可以使用自定义继承自 NSOperation 的子类。可以通过重写 main 或者 start 方法 来定义自己的 NSOperation 对象。重写main方法比较简单,我们不需要管理操作的状态属性 isExecuting 和 isFinished。当 main 执行完返回的时候,这个操作就结束了。

    先定义一个继承自 NSOperation 的子类,重写main方法。

    // YSCOperation.h 文件
    #import <Foundation/Foundation.h>

    @interface YSCOperation : NSOperation

    @end

    // YSCOperation.m 文件
    #import "YSCOperation.h"

    @implementation YSCOperation

    - (void)main {
    if (!self.isCancelled) {
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2];
    NSLog(@"1---%@", [NSThread currentThread]);
    }
    }
    }

    @end
    /**
    * 使用自定义继承自 NSOperation 的子类
    */

    - (void)useCustomOperation {
    // 1.创建 YSCOperation 对象
    YSCOperation *op = [[YSCOperation alloc] init];
    // 2.调用 start 方法开始执行操作
    [op start];
    }


    • 可以看出:在没有使用 NSOperationQueue、在主线程单独使用自定义继承自 NSOperation 的子类的情况下,是在主线程执行操作,并没有开启新线程。

    下边我们来讲讲 NSOperationQueue 的创建。

    4.2 创建队列

    NSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。

    • 主队列
      • 凡是添加到主队列中的操作,都会放到主线程中执行(注:不包括操作使用addExecutionBlock:添加的额外操作,额外操作可能在其他线程执)。


    // 主队列获取方法
    NSOperationQueue *queue = [NSOperationQueue mainQueue];

    • 自定义队列(非主队列)
      • 添加到这种队列中的操作,就会自动放到子线程中执行。
      • 同时包含了:串行、并发功能。
    // 自定义队列创建方法
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    4.3 将操作加入到队列中

    上边我们说到 NSOperation 需要配合 NSOperationQueue 来实现多线程。

    那么我们需要将创建好的操作加入到队列中去。总共有两种方法:

    1. - (void)addOperation:(NSOperation *)op;
      • 需要先创建操作,再将创建好的操作加入到创建好的队列中去。
    /**
    * 使用 addOperation: 将操作加入到操作队列中
    */

    - (void)addOperationToQueue {

    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.创建操作
    // 使用 NSInvocationOperation 创建操作1
    NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

    // 使用 NSInvocationOperation 创建操作2
    NSInvocationOperation *op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil];

    // 使用 NSBlockOperation 创建操作3
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [op3 addExecutionBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];

    // 3.使用 addOperation: 添加所有操作到队列中
    [queue addOperation:op1]; // [op1 start]
    [queue addOperation:op2]; // [op2 start]
    [queue addOperation:op3]; // [op3 start]
    }
    • 使用 NSOperation 子类创建操作,并使用 addOperation: 将操作加入到操作队列后能够开启新线程,进行并发执行。
    1. - (void)addOperationWithBlock:(void (^)(void))block;
      • 无需先创建操作,在 block 中添加操作,直接将包含操作的 block 加入到队列中。


    /**
    * 使用 addOperationWithBlock: 将操作加入到操作队列中
    */


    - (void)addOperationWithBlockToQueue {
    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.使用 addOperationWithBlock: 添加操作到队列中
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    [queue addOperationWithBlock:^{
    for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"3---%@", [NSThread currentThread]); // 打印当前线程
    }
    }];
    }
    使用 addOperationWithBlock: 将操作加入到操作队列后能够开启新线程,进行并发执行


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








    收起阅读 »

    iOS开发 - 面试被问到内存概念怎么办?

    在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。这样也就是单运行的时候没有什么问题!可是,计算机会有多到程序、分时系统和多任务,当我们能够同时运行多个程序时,CPU的利用率将会比较高。那么有一个非常严重的问题:如何...
    继续阅读 »
    在早期的计算机中,程序是直接运行在物理内存上的,也就是说:程序在运行时访问的地址就是物理地址。这样也就是单运行的时候没有什么问题!可是,计算机会有多到程序、分时系统和多任务,当我们能够同时运行多个程序时,CPU的利用率将会比较高。那么有一个非常严重的问题:如何将计算机的有限的物理内存分配给多个程序使用

    假设我们计算有128MB内存,程序A需要10MB,程序B需要100MB,程序C需要20MB。如果我们需要同时运行程序A和B,那么比较直接的做法是将内存的前10MB分配给程序A,10MB~110MB分配给B。




    但这样做,会造成以下问题:

    • 当多个程序需要运行时,必须保证这些程序用到的内存总量要小于计算机实际的物理内存的大小。

    • 进程地址空间不隔离,由于程序是直接访问物理内存的,所以每一个进程都可以修改其他进程的内存数据,设置修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成一些破坏

    • 内存使用效率低 内存空间不足,就需要将其他程序展示拷贝到硬盘当中,然后将新的程序装入内存。然而由于大量的数据装入装出,内存的使用效率会非常低

    • 程序运行的地址不确定;因为内存地址是随机分配的,所以程序运行的地址也是不正确的

    解决这几个问题的思路就是使用我们非常牛逼的方法:增加中间层 - 即使用一种间接的地址访问方式。


    把程序给出的地址看做是一种虚拟地址,然后通过某种映射,将这个虚拟地址转化到实际的物理地址。这样,只需要控制好映射过程,就能保证程序所能访问的物理内存区域跟别的程序不重叠,达到空间隔离的效果。

    隔离

    普通的程序它只需要一个简单的执行环境一个单一的地址空间有自己的CPU
    地址空间比较抽象,如果把它想象成一个数组,每一个数组是一字节,数组大小就是地址空间的长度,那么32位的地址空间大小就是2^32=4294967296字节,即4G,地址空间有效位是0x00000000~0xFFFFFFFF
    地址空间分为两种:

    • 物理空间:就是物理内存。32位的机器,地址线就有32条,物理空间4G,但如果只装有512M的内存,那么实际有效的空间地址就是0x00000000~0x1FFFFFFF,其他部分都是无效的。

    • 虚拟空间:每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的空间地址,这样就有效的做到了进程隔离。

    分段

    基本思路: 把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。虚拟空间的每个字节对应物理空间的每个字节。这个映射过程由软件来完成。

    比如A需要10M,就假设有0x00000000 到0x00A00000大小的虚拟空间,然后从物理内存分配一个相同大小的空间,比如是0x001000000x00B00000。操作系统来设置这个映射函数,实际的地址转换由硬件完成。如果越界,硬件就会判断这是一个非法访问,拒绝这个地址请求,并上报操作系统或监控程序。





    这样一来利用分段的方式可以解决之前的地址空间不隔离程序运行地址不确定

    • 首先做到了地址隔离,因为A和B被映射到了两块不同的物理空间,它们之间没有任何重叠,如果A访问虚拟空间的地址超过了0x00A00000这个范围,硬件就会判断这是一个非法的访问,并将这个请求报告给操作系统或者监控程序,由它决定如何处理。

    • 再者,对于每个程序来说,无论它们被分配到地址空间的哪一个区域,对于程序来说都是透明的,它们不需要关心物理地址的变化,它们只要按照从地址0x000000000x00A00000来编写程序、放置变量,所以程序不需要重定位。

    第二问题内存使用效率问题依旧没有解决。

    但是分段的方法没有解决内存使用效率的问题。分段对于内存区域的映射还是按照程序为单位,如果内存不足,被换入换出的磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作,从而严重影响速度,这种方法还是显得粗糙,粒度比较大。事实上根据程序的局部性原理,当一个程序正在运行时,在某个时间段内,它只是频繁用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内是不会被用到的。人们很自然地想到了更小粒度的内存分割和映射方法,使得程序的局部性原理得到充分利用,大大提高了内存的使用率。这种方法就是分页。

    分页

    分页的基本方法是把地址空间人为得等分成固定大小的页,每一个页的大小由硬件决定,或硬件支持多种页的大小,由操作系统选择决定页的大小。 目前几乎所有PC的操作系统都是用4KB大小的页。我们使用的PC机是32位虚拟地址空间,也就是4GB,按4KB分页,总共有1048576个页。

    那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它们从磁盘里取出即可。图中的线表示映射关系,我们可以看到虚拟空间有些页被映射到同一个物理页,这样就可以实现内存共享。
    虚拟页,物理页,磁盘页根据内存空间不一样而区分

    我们可以看到Process 1 的VP2和VP3不在内存中,但是当进程需要用到这两个页的时候,硬件就会捕获到这个消息,就是所谓的页错误(Page Fault),然后操作系统接管进程,负责将VP2和VP3从磁盘读取出来装入内存,然都将内存中的这两个页和VP2和VP3建立映射关系。以页为单位存取和交换数据非常方便,硬件本身就支持这种以页为单位的操作方式。


  • 保护页也是页映射的目的之一,简单地说就是每个页可以设置权限属性,谁可以修改,谁可以访问,而且只有操作系统有权修改这些属性,那么操作系统就可以做到保护自己和保护进程。

  • 虚拟存储的实现需要硬件支持,几乎所有CPU都采用称为MMU的部件来进行页的映射:




  • 在页映射模式下,CPU发出的是Virtual Address,即我们程序看到的是虚拟地址。经过MMU转换以后就变成了Physical Address。一般MMU集成在CPU内部,不会以独立的部件存在。

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











    收起阅读 »

    多线程安全-iOS开发注意咯

    正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势资源利用率更好程序设计在某些情况下更简单程序响应更快但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础原子操作...
    继续阅读 »



    正式因为多线程能够在时间片里被CPU快速切换,造就了以下优势

    • 资源利用率更好
    • 程序设计在某些情况下更简单
    • 程序响应更快

    但是并不是非常完美,因为多线程常常伴有资源抢夺的问题,作为一个高级开发人员并发编程那是必须要的,同时解决线程安全也成了我们必须要要掌握的基础


    原子操作

    自旋锁其实就是封装了一个spinlock_t自旋锁

    自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。自旋锁下面还会展开来介绍

    互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态bool lock = false; // 一开始没有锁上,任何线程都可以申请锁

    do {
    while(test_and_set(&lock); // test_and_set 是一个原子操作
    Critical section // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
    Reminder section // 不需要锁保护的代码
    }


    操作在底层会被编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断,去执行别的代码,而我们的原子性的单条指令的执行是不会被打断的,所以保证了安全.

    自旋锁的BUG

    尽管原子操作非常的简单,但是它只适合于比较简单特定的场合。在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作指令就力不从心了,

    如果临界区的执行时间过长,使用自旋锁不是个好主意。之前我们介绍过时间片轮转算法,线程在多种情况下会退出自己的时间片。其中一种是用完了时间片的时间,被操作系统强制抢占。除此以外,当线程进行 I/O 操作,或进入睡眠状态时,都会主动让出时间片。显然在 while 循环中,线程处于忙等状态,白白浪费 CPU 时间,最终因为超时被操作系统抢占时间片。如果临界区执行时间较长,比如是文件读写,这种忙等是毫无必要的

    下面开始我们又爱又恨的


    iOS锁

    锁并是一种非强制机制,每一个现货出呢个在访问数据或资源之前视图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁已经被占用的时候试图获取锁,线程会等待,知道锁重新可用!

    信号量

    二元信号量(Binary Semaphore)只有两种状态:占用与非占用。它适合被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程将会等待,直到该锁被释放

    现在我们在这个基础上,我们把学习的思维由二元->多元的时候,我们的信号量由此诞生,多元信号量简称信号量

    • 将信号量的值减1

    • 如果信号量的值小于0,则进入等待状态,否则继续执行。访问玩资源之后,线程释放信号量,进行如下操作

    • 将信号量的值加1

    • 如果信号量的值小于1,唤醒一个等待中的线程


    let sem = DispatchSemaphore(value: 1)

    for index in 1...5 {
    DispatchQueue.global().async {
    sem.wait()
    print(index,Thread.current)
    sem.signal()
    }
    }

    输出结果:
    1 <NSThread: 0x600003fa8200>{number = 3, name = (null)}
    2 <NSThread: 0x600003f90140>{number = 4, name = (null)}
    3 <NSThread: 0x600003f94200>{number = 5, name = (null)}
    4 <NSThread: 0x600003fa0940>{number = 6, name = (null)}
    5 <NSThread: 0x600003f94240>{number = 7, name = (null)}

    互斥量


    互斥量(Mutex)又叫互斥锁和二元信号量很类似,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放;也就是说哪个线程锁的,要哪个线程释放锁。

    Mutex可以分为递归锁(recursive mutex)非递归锁(non-recursive mutex)。 递归锁也叫可重入锁(reentrant mutex),非递归锁也叫不可重入锁(non-reentrant mutex)
    二者唯一的区别是:
    • 同一个线程可以多次获取同一个递归锁,不会产生死锁。
    • 如果一个线程多次获取同一个非递归锁,则会产生死锁。

    NSLock 是最简单额互斥锁!但是是非递归的!直接封装了pthread_mutex 用法非常简单就不做赘述
    @synchronized 是我们互斥锁里面用的最频繁的,但是性能最差!

    int main(int argc, const char * argv[]) {
    NSString *obj = @"Iceberg";
    @synchronized(obj) {
    NSLog(@"Hello,world! => %@" , obj);
    }
    }

    底层clang

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

    NSString *obj = (NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_0;

    {
    id _rethrow = 0;
    id _sync_obj = (id)obj;
    objc_sync_enter(_sync_obj);
    try {
    struct _SYNC_EXIT {
    _SYNC_EXIT(id arg) : sync_exit(arg) {}
    ~_SYNC_EXIT() {
    objc_sync_exit(sync_exit);
    }
    id sync_exit;
    } _sync_exit(_sync_obj);

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_8l_rsj0hqpj42b9jsw81mc3xv_40000gn_T_block_main_54f70c_mi_1 , obj);

    } catch (id e) {
    _rethrow = e;
    }

    {
    struct _FIN {
    _FIN(id reth) : rethrow(reth) {}
    ~_FIN() {
    if (rethrow)
    objc_exception_throw(rethrow);
    }
    id rethrow;
    } _fin_force_rethow(_rethrow);
    }
    }

    }
    我们发现objc_sync_enter函数是在try语句之前调用,参数为需要加锁的对象。因为C++中没有try{}catch{}finally{}语句,所以不能在finally{}调用objc_sync_exit函数。因此objc_sync_exit是在_SYNC_EXIT结构体中的析构函数中调用,参数同样是当前加锁的对象。这个设计很巧妙,原因在_SYNC_EXIT结构体类型的_sync_exit是一个局部变量,生命周期为try{}语句块,其中包含了@sychronized{}代码需要执行的代码,在代码完成后,_sync_exit局部变量出栈释放,随即调用其析构函数,进而调用objc_sync_exit函数。即使try{}语句块中的代码执行过程中出现异常,跳转到catch{}语句,局部变量_sync_exit同样会被释放,完美的模拟了finally的功能。


    int objc_sync_enter(id obj)
    {
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
    SyncData* data = id2data(obj, ACQUIRE);
    require_action_string(data != NULL, done, result = OBJC_SYNC_NOT_INITIALIZED, "id2data failed");

    result = recursive_mutex_lock(&data->mutex);
    require_noerr_string(result, done, "mutex_lock failed");
    } else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
    _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    objc_sync_nil();
    }

    done:
    return result;
    }


    从上面的源码中我们可以得出你调用sychronized的每个对象,Objective-C runtime都会为其分配一个递归锁并存储在哈希表中。完美

    其实如果大家觉得@sychronized性能低的话,完全可以用NSRecursiveLock现成的封装好的递归锁

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
    [lock lock];
    if (value > 0) {
    NSLog(@"value:%d", value);
    RecursiveBlock(value - 1);
    }
    [lock unlock];
    };
    RecursiveBlock(2);
    });

    2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:2
    2016-08-19 14:43:12.327 ThreadLockControlDemo[1878:145003] value:1

    条件变量

    条件变量(Condition Variable)作为一种同步手段,作用类似一个栅栏。对于条件变量,现成可以有两种操作:

    • 首先线程可以等待条件变量,一个条件变量可以被多个线程等待
    • 其次线程可以唤醒条件变量。此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。

    换句话说:使用条件变量可以让许多线程一起等待某个时间的发生,当某个时间发生时,所有的线程可以一起恢复执行!

    相信仔细的大家肯定在锁的用法里面见过NSCondition,就是封装了条件变量pthread_cond_t和互斥锁

    - (void) signal { 
    pthread_cond_signal(&_condition);
    }
    // 其实这个函数是通过宏来定义的,展开后就是这样
    - (void) lock {
    int err = pthread_mutex_lock(&_mutex);
    }
    NSConditionLock借助 NSCondition来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个NSCondition对象,以及 _condition_value属性,在初始化时就会对这个属性进行赋值:

    // 简化版代码
    - (id) initWithCondition: (NSInteger)value {
    if (nil != (self = [super init])) {
    _condition = [NSCondition new]
    _condition_value = value;
    }
    return self;
    }

    临界区

    比互斥量更加严格的同步手段。在术语中,把临界区的获取称为进入临界区,而把锁的释放称为离开临界区。与互斥量和信号量的区别:

    • (1)互斥量和信号量字系统的任何进程都是可见的。
    • (2)临界区的作用范围仅限于本进程,其他进程无法获取该锁。

    // 临界区结构对象
    CRITICAL_SECTION g_cs;
    // 共享资源
    char g_cArray[10];
    UINT ThreadProc10(LPVOID pParam)
    {
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
    g_cArray[i] = a;
    Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
    }
    UINT ThreadProc11(LPVOID pParam)
    {
    // 进入临界区
    EnterCriticalSection(&g_cs);
    // 对共享资源进行写入操作
    for (int i = 0; i < 10; i++)
    {
    g_cArray[10 - i - 1] = b;
    Sleep(1);
    }
    // 离开临界区
    LeaveCriticalSection(&g_cs);
    return 0;
    }
    ……
    void CSample08View::OnCriticalSection()
    {
    // 初始化临界区
    InitializeCriticalSection(&g_cs);
    // 启动线程
    AfxBeginThread(ThreadProc10, NULL);
    AfxBeginThread(ThreadProc11, NULL);
    // 等待计算完毕
    Sleep(300);
    // 报告计算结果
    CString sResult = CString(g_cArray);
    AfxMessageBox(sResult);
    }

    读写锁

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);

    ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。


    #include <pthread.h>      //多线程、读写锁所需头文件
    pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //定义和初始化读写锁

    写模式:
    pthread_rwlock_wrlock(&rwlock); //加写锁
    写写写……
    pthread_rwlock_unlock(&rwlock); //解锁

    读模式:
    pthread_rwlock_rdlock(&rwlock); //加读锁
    读读读……
    pthread_rwlock_unlock(&rwlock); //解锁


    • 用条件变量实现读写锁

    这里用条件变量+互斥锁来实现。注意:条件变量必须和互斥锁一起使用,等待、释放的时候都需要加锁。

    #include <pthread.h> //多线程、互斥锁所需头文件

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //定义和初始化互斥锁
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //定义和初始化条件变量


    写模式:
    pthread_mutex_lock(&mutex); //加锁
    while(w != 0 || r > 0)
    {
    pthread_cond_wait(&cond, &mutex); //等待条件变量的成立
    }
    w = 1;

    pthread_mutex_unlock(&mutex);
    写写写……
    pthread_mutex_lock(&mutex);
    w = 0;
    pthread_cond_broadcast(&cond); //唤醒其他因条件变量而产生的阻塞
    pthread_mutex_unlock(&mutex); //解锁


    读模式:
    pthread_mutex_lock(&mutex);
    while(w != 0)
    {
    pthread_cond_wait(&cond, &mutex); //等待条件变量的成立
    }
    r++;
    pthread_mutex_unlock(&mutex);
    读读读……
    pthread_mutex_lock(&mutex);
    r- -;
    if(r == 0)
    pthread_cond_broadcast(&cond); //唤醒其他因条件变量而产生的阻塞
    pthread_mutex_unlock(&mutex); //解锁
    • 用互斥锁实现读写锁

    这里使用2个互斥锁+1个整型变量来实现

    #include <pthread.h> //多线程、互斥锁所需头文件
    pthread_mutex_t r_mutex = PTHREAD_MUTEX_INITIALIZER; //定义和初始化互斥锁
    pthread_mutex_t w_mutex = PTHREAD_MUTEX_INITIALIZER;
    int readers = 0; //记录读者的个数

    写模式:
    pthread_mutex_lock(&w_mutex);
    写写写……
    pthread_mutex_unlock(&w_mutex);


    读模式:
    pthread_mutex_lock(&r_mutex);

    if(readers == 0)
    pthread_mutex_lock(&w_mutex);
    readers++;
    pthread_mutex_unlock(&r_mutex);
    读读读……
    pthread_mutex_lock(&r_mutex);
    readers- -;
    if(reader == 0)
    pthread_mutex_unlock(&w_mutex);
    pthread_mutex_unlock(&r_mutex);


    • 用信号量来实现读写锁

    这里使用2个信号量+1个整型变量来实现。令信号量的初始数值为1,那么信号量的作用就和互斥量等价了。

    #include <semaphore.h>     //线程信号量所需头文件

    sem_t r_sem; //定义信号量
    sem_init(&r_sem, 0, 1); //初始化信号量

    sem_t w_sem; //定义信号量
    sem_init(&w_sem, 0, 1); //初始化信号量
    int readers = 0;

    写模式:
    sem_wait(&w_sem);
    写写写……
    sem_post(&w_sem);


    读模式:
    sem_wait(&r_sem);
    if(readers == 0)
    sem_wait(&w_sem);
    readers++;
    sem_post(&r_sem);
    读读读……
    sem_wait(&r_sem);
    readers- -;
    if(readers == 0)
    sem_post(&w_sem);
    sem_post(&r_sem);



    线程的安全是现在各个领域在多线程开发必须要掌握的基础!只有对底层有所掌握,才能在真正的实际开发中游刃有余!


    收起阅读 »

    为了能够摸鱼,我走上了歧路

    前言 每天都是重复的工作,这样可不行,已经严重影响我的日常摸鱼,为了减少自己日常的开发时间,我决定走一条歧路,铤而走险,将项目中的各种手动埋点统计替换成自动化埋点。以后再也不用担心没时间摸鱼了~ 作为Android届开发的一员,今天我决定将摸鱼方案分享给大家,...
    继续阅读 »

    前言


    每天都是重复的工作,这样可不行,已经严重影响我的日常摸鱼,为了减少自己日常的开发时间,我决定走一条歧路,铤而走险,将项目中的各种手动埋点统计替换成自动化埋点。以后再也不用担心没时间摸鱼了~


    作为Android届开发的一员,今天我决定将摸鱼方案分享给大家,希望更多的广大群众能够的加入到摸鱼的行列中~



    1. 声明打点的接口方法
    interface StatisticService {

    @Scan(ProxyActivity.PAGE_NAME)
    fun buttonScan(@Content(StatisticTrack.Parameter.NAME) name: String)

    @Click(ProxyActivity.PAGE_NAME)
    fun buttonClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)

    @Scan(ProxyActivity.PAGE_NAME)
    fun textScan(@Content(StatisticTrack.Parameter.NAME) name: String)

    @Click(ProxyActivity.PAGE_NAME)
    fun textClick(@Content(StatisticTrack.Parameter.NAME) name: String, @Content(StatisticTrack.Parameter.TIME) clickTime: Long)
    }




    1. 通过动态代理获取StatisticService接口引用
    private val mStatisticService = Statistic.instance.create(StatisticService::class.java)




    1. 在合适的埋点位置进行埋点统计,例如Click埋点
    2. fun onClick(view: View) {
      if (view.id == R.id.button) {
      mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
      } else if (view.id == R.id.text) {
      mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
      }
      }



    其中2、3步骤都是在对应埋点的类中使用,这里对应的是ProxyActivity

    class ProxyActivity : AppCompatActivity() {

    // 步骤2
    private val mStatisticService = Statistic.instance.create(StatisticService::class.java)

    companion object {
    private const val BUTTON = "statistic_button"
    private const val TEXT = "statistic_text"
    const val PAGE_NAME = "ProxyActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
    //...
    title = extraData.title

    // 步骤3 => 曝光点
    mStatisticService.buttonScan(BUTTON)
    mStatisticService.textScan(TEXT)
    }

    private fun getExtraData(): MainModel =
    intent?.extras?.getParcelable(ActivityUtils.EXTRA_DATA)
    ?: throw NullPointerException("intent or extras is null")

    // 步骤3 => 点击点
    fun onClick(view: View) {
    if (view.id == R.id.button) {
    mStatisticService.buttonClick(BUTTON, System.currentTimeMillis() / 1000)
    } else if (view.id == R.id.text) {
    mStatisticService.textClick(TEXT, System.currentTimeMillis() / 1000)
    }
    }
    }



    步骤1是创建新的类,不在代码注入的范围之内。自动生成类可以使用注解+process+JavaPoet来实现。类似于ButterKnifeDagger2Room等。之前我也有写过相关的demo与文章。由于不在本篇文章的范围之内,感兴趣的可以自行去了解。


    这里我们需要做的是:需要在ProxyActiviy中将2、3步骤的代码转成自动注入。


    自动注入就是在现有的类中自动加入我们预期的代码,不需要我们额外的进行编写。


    既然已经知道了需要注入的代码,那么接下的问题就是什么时候进行注入这些代码。


    这就涉及到Android构建与打包的流程,Android使用Gradle进行构建与打包,


    image.png


    在打包的过程中将源文件转化成.class文件,然后再将.class文件转成Android能识别的.dex文件,最终将所有的.dex文件组合成一个.apk文件,提供用户下载与安装。


    而在将源文件转化成.class文件之后,Google提供了一种Transform机制,允许我们在打包之前对.class文件进行修改。


    这个修改时机就是我们代码自动注入的时机。


    transform是由gradle提供,在我们日常的构建过程中也会看到系统自身的transform身影,gradle由各种task组成,transform就穿插在这些task中。


    图中高亮的部分就是本次自定义的TraceTransform, 它会在.class转化成.dex之前进行执行,目的就是修改目标.class文件内容。


    Transform的实现需要结合Gradle Plugin一起使用。所以接下来我们需要创建一个Plugin


    创建Plugin


    appbuild.gradle中,我们能够看到以下类似的插件引用方式

    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-android-extensions'
    apply plugin: 'kotlin-kapt'
    apply plugin: "androidx.navigation.safeargs.kotlin"
    apply plugin: 'trace_plugin'



    这里的插件包括系统自带、第三方的与自定义的。其中trace_plugin就是本次自定义的插件。为了能够让项目使用自定义的插件,Gradle提供了三种打包插件的方式



    1. Build Script: 将插件的源代码直接包含在构建脚本中。这样做的好处是,无需执行任何操作即可自动编译插件并将其包含在构建脚本的类路径中。但缺点是它在构建脚本之外不可见,常用在脚本自动构建中。

    2. buildSrc projectgradle会自动识别buildSrc目录,所以可以将plugin放到buildSrc目录中,这样其它的构建脚本就能自动识别这个plugin, 多用于自身项目,对外不共享。

    3. Standalone project: 创建一个独立的plugin项目,通过对外发布Jar与外部共享使用。


    这里使用第三种方式来创建Plugin。所以创建完之后的目录结构大概是这样的


    为了让别的项目能够引用这个Plugin,我们需要对外声明,可以发布到maven中,也可以本地声明,为了简便这里使用本地声明。

    apply plugin: 'java-gradle-plugin'

    dependencies {
    implementation gradleApi()
    implementation localGroovy()
    }

    gradlePlugin {
    plugins {
    version {
    // 在 app 模块需要通过 id 引用这个插件
    id = 'trace_plugin'
    // 实现这个插件的类的路径
    implementationClass = 'com.rousetime.trace_plugin.TracePlugin'
    }
    }
    }



    Pluginidtrace_plugin,实现入口为com.rousetime.trace_plugin.TracePlugin


    声明完之后,就可以直接在项目的根目录下的build.gradle中引入该id

    plugins {
    id "trace_plugin" apply false
    }


    为了能在app项目中apply这个plugin,还需要创建一个META-INF.gradle-plugins目录,对应的位置如下


    注意这里的trace_plugin.properties文件名非常重要,前面的trace_plugin就代表你在build.gradleapply的插件名称。


    文件中的内容很简单,只有一行,对应的就是TracePlugin的实现入口

    implementation-class=com.rousetime.trace_plugin.TracePlugin

    上面都准备就绪之后,就可以在build.gradle进行apply plugin

    apply plugin: 'trace_plugin'


    这个时候我们自定义的plugin就引入到项目中了。


    再回到刚刚的Plugin入口TracePlugin,来看下它的具体实现

    class TracePlugin : Plugin {

    override fun apply(target: Project) {
    if (target.plugins.hasPlugin(AppPlugin::class.java)) {
    val appExtension = target.extensions.getByType(AppExtension::class.java)
    appExtension.registerTransform(TraceTransform())
    }
    val methodVisitorConfig = target.extensions.create("methodVisitor", MethodVisitorConfig::class.java)
    LocalConfig.methodVisitorConfig = methodVisitorConfig
    target.afterEvaluate {
    }
    }

    }



    只有一个方法apply,在该方法中我们打印一行文本,然后重新构建项目,在build输出窗口就能看到这行文本

    ....
    > Configure project :app
    Trace Plugin start to apply
    mehtodVisitorConfig

    Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
    Use '--warning-mode all' to show the individual deprecation warnings.
    ...



    到这里我们自定义的plugin已经创建成功,并且已经集成到我们的项目中。


    第一步已经完成。下面进入第二步。


    实现Transform


    TracePluginapply方法中,对项目的appExtension注册了一个TraceTransform。重点来了,这个TraceTransform就是我们在gradle构建的过程中插入的Transform,也就是注入代码的入口。来看下它的具体实现

    class TraceTransform : Transform() {

    override fun getName(): String = this::class.java.simpleName

    override fun getInputTypes(): MutableSet = TransformManager.CONTENT_JARS

    override fun isIncremental(): Boolean = true

    override fun getScopes(): MutableSet = TransformManager.SCOPE_FULL_PROJECT

    override fun transform(transformInvocation: TransformInvocation?) {
    TransformProxy(transformInvocation, object : TransformProcess {
    override fun process(entryName: String, sourceClassByte: ByteArray): ByteArray? {
    // use ams to inject
    return if (ClassUtils.checkClassName(entryName)) {
    TraceInjectDelegate().inject(sourceClassByte)
    } else {
    null
    }
    }
    }).apply {
    transform()
    }
    }
    }



    代码很简单,只需要实现几个特定的方法。



    1. getName: Transform对外显示的名称

    2. getInputTypes: 扫描的文件类型,CONENT_JARS代表CLASSESRESOURCES

    3. isIncremental: 是否开启增量,开启后会提高构建速度,对应的需要手动处理增量的逻辑

    4. getScopes: 扫描作用范围,SCOPE_FULL_PROJECT代表整个项目

    5. transform: 需要转换的逻辑都在这里处理


    transform是我们接下来.class文件的入口,这个方法有个参数TransformInvocation,该参数提供了上面定义范围内扫描到的所用jar文件与directory文件。


    transform中我们主要做的就是在这些jardirectory中解析出.class文件,这是找到目标.class的第一步。只有解析出了所有的.class文件,我们才能进一步过滤出我们需要注入代码的.class文件。


    transform的工作流程是:解析.class文件,然后我们过滤出需要处理的.class文件,写入对应的逻辑,然后再将处理过的.class文件重新拷贝到之前的jar或者directory中。


    通过这种解析、处理与拷贝的方式,实现偷天换日的效果。


    既然有一套固定的流程,那么自然有对应的一套固定是实现。在这三个步骤中,真正需要实现的是处理逻辑,不同的项目有不同的处理逻辑,


    对于解析与拷贝操作,已经有相对完整的一套通用实现方案。如果你的项目中有多个这种类型的Transform,就可以将其抽离出来单个module,增加复用性。


    解析与拷贝


    下面我们来看一下它的核心实现步骤。

    fun transform() {
    if (!isIncremental) {
    // 不是增量编译,将之前的输出目录中的内容全部删除
    outputProvider?.deleteAll()
    }
    inputs?.forEach {
    // jar
    it.jarInputs.forEach { jarInput ->
    transformJar(jarInput)
    }
    // directory
    it.directoryInputs.forEach { directoryInput ->
    transformDirectory(directoryInput)
    }
    }
    executor?.invokeAll(tasks)
    }



    transform方法主要做的就是分别遍历jardirectory中的文件。在这两大种类中分别解析出.class文件。


    例如jar的解析transformJar


    如果是增量编译,就分别处理增量的不同操作,主要的是ADDEDCHANGED操作。这个处理逻辑与非增量编译的时候一样,都是去遍历jar,从中解析出对应的.class文件。


    遍历的核心代码如下

    while (enumeration.hasMoreElements()) {
    val jarEntry = enumeration.nextElement()
    val inputStream = originalFile.getInputStream(jarEntry)

    val entryName = jarEntry.name
    // 构建zipEntry
    val zipEntry = ZipEntry(entryName)
    jarOutputStream.putNextEntry(zipEntry)

    var modifyClassByte: ByteArray? = null
    val sourceClassByte = IOUtils.toByteArray(inputStream)

    if (entryName.endsWith(".class")) {
    modifyClassByte = transformProcess.process(entryName, sourceClassByte)
    }

    if (modifyClassByte == null) {
    jarOutputStream.write(sourceClassByte)
    } else {
    jarOutputStream.write(modifyClassByte)
    }
    inputStream.close()
    jarOutputStream.closeEntry()
    }



    如果entryName的后缀是.class说明当前是.class文件,我们需要单独拿出来进行后续的处理。


    后续的处理逻辑交给了transformProcess.process。具体处理先放一放。


    处理完之后,再将处理后的字节码拷贝保存到之前的jar中。


    对应的directory也是类似 同样是过滤出.class文件,然后交给process方法进行统一处理。最后将处理完的字节码拷贝保存到原路径中。


    以上就是Transform的解析与拷贝的核心处理。


    处理


    上面提到.class的处理都转交给process方法,这个方法的具体实现在TraceTransformtransform方法中

    class TraceAsmInject : Inject {

    override fun modifyClassByte(byteArray: ByteArray): ByteArray {
    val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
    val classFilterVisitor = ClassFilterVisitor(classWriter)
    val classReader = ClassReader(byteArray)
    classReader.accept(classFilterVisitor, ClassReader.EXPAND_FRAMES)
    return classWriter.toByteArray()
    }

    }



    process中使用TraceInjectDelegateinject来处理过滤出来的字节码。最终的处理会来到modifyClassByte方法。



    这里的ClassWriterClassFilterVisitorClassReader都是ASM的内容,也是我们接下来实现自动注入代码的重点。


    ASM


    ASM是操作Java字节码的一个工具。


    其实操作字节码的除了ASM还有javassist,但个人觉得ASM更方便,因为它有一系列的辅助工具,能更好的帮助我们实现代码的注入。


    在上面我们已经得到了.class的字节码文件。现在我们需要做的就是扫描整个字节码文件,判断是否是我们需要注入的文件。


    这里我将这些逻辑封装到了ClassFilterVisitor文件中。


    ASM为我们提供了ClassVisitorMethodVisitorFieldVisitorAPI。每当ASM扫描类的字节码时,都会调用它的visitvisitFieldvisitMethodvisitAnnotation等方法。


    有了这些方法,我们就可以判断并处理我们需要的字节码文件。

    class ClassFilterVisitor(cv: ClassVisitor?) : ClassVisitor(Opcodes.ASM5, cv) {

    override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
    super.visit(version, access, name, signature, superName, interfaces)
    // 扫描当前类的信息
    }

    override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor {
    // 扫描类中的方法
    }


    override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
    // 扫描类中的字段
    }

    }



    这是几个主要的方法,也是接下来我们需要重点用到的方法。


    首先我们来看个简单的,这个明白了其它的都是一样的。

    fun bindData(value: MainModel, position: Int) {
    itemView.content.apply {
    text = value.content
    setOnClickListener {
    // 自动注入这行代码
    LogUtils.d("inject success.")
    if (position == 0) {
    requestPermission(context, value)
    } else {
    navigationPage(context, value)
    }
    }
    }
    }



    假设我们需要在onClickListener中注入LogUtils.d这个行代码,本质就是在点击的时候输出一行日志。


    首先我们需要明白,setOnClickListener本质是实现了一个OnClickListener接口的匿名内部类。


    所以可以在扫描类的时候判断是否实现了OnClickListener这个接口,如果实现了,我们再去匹配它的onClick方法,并且在它的onClick方法中进行注入代码。


    而类的扫描与方法扫描分别可以使用visitvisitMetho


    visit方法中,我们保存当前类实现的接口;在visitMethod中再对当前接口进行判断,看它是否有onClick方法。



    namedesc分别为onClick方法的方法名称与方法参数描述。这是字节码匹配方法的一种规范。



    如果有的话,说明是我们需要插入的方法,这个时候返回AdviceAdapter。它是ASM提供的便捷针对方法注入的类。我们重写它的onMethodEnter方法。代表我们将在方法的开头注入代码。


    onMethodEnter方法中的代码就是LogUtils.dASM注入实现。你可能会说这个是什么,完全看不懂,更别说写字节码注入了。


    别急,下面就是ASM的方便之处,我们只需在Android Studio中下载ASM Bytecode Viewer Support Kotlin插件。


    该插件可以帮助我们查看kotlin字节码,只需右键弹窗中选择ASM Bytecode Viewer。稍后就会弹出转化后的字节码弹窗。


    在弹窗中找到需要注入的代码,具体就是下面这几行

    methodVisitor.visitFieldInsn(GETSTATIC, "com/idisfkj/androidapianalysis/utils/LogUtils", "INSTANCE", "Lcom/idisfkj/androidapianalysis/utils/LogUtils;");
    methodVisitor.visitLdcInsn("inject success.");
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/idisfkj/androidapianalysis/utils/LogUtils", "d", "(Ljava/lang/String;)V", false);



    这就是LogUtils.d的注入代码,直接copy到上面提到的onMethodEnter方法中。这样注入的代码就已经完成。


    如果你想查看是否注入成功,除了运行项目,查看效果之外,还可以直接查看注入的源码。


    在项目的build/intermediates/transforms目录下,找到自定义的TraceTransform,再找到对应的注入文件,就可以查看注入源码。


    其实到这来核心内容基本已经结束了,不管是注入什么代码都可以通过这种方法来获取注入的ASM的代码,不同的只是注入的时机判断。


    有了上面的基础,我们来实现开头的自动埋点。


    实现


    为了让自动化埋点能够灵活的传递打点数据,我们使用注解的方式来传递具体的埋点数据与类型。



    1. TrackClickData: 点击的数据

    2. TrackScanData: 曝光的数据

    3. TrackScan: 曝光点

    4. TrackClick: 点击点


    有了这些注解,剩下我们要做的就很简单了


    使用TrackClickDataTrackScanData声明打点的数据;使用TrackScanTrackClick声明打点的类型与自动化插入代码的入口方法。


    我们再回到注入代码的类ClassFilterVisitor,来实现具体的埋点代码的注入。


    在这里我们需要做的是解析声明的注解,拿到打点的数据,并且声明的TrackScanTrackClick方法中插入埋点的具体代码。

    override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
    super.visit(version, access, name, signature, superName, interfaces)
    mInterface = interfaces
    mClassName = name
    }



    通过visit方法来扫描具体的类文件,在这里保存当前扫描的类的信息,为之后注入代码做准备

    override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor {
    val filterVisitor = super.visitField(access, name, desc, signature, value)
    return object : FieldVisitor(Opcodes.ASM5, filterVisitor) {
    override fun visitAnnotation(annotationDesc: String?, visible: Boolean): AnnotationVisitor {
    if (annotationDesc == TRACK_CLICK_DATA_DESC) { // TrackClickData 注解
    mTrackDataName = name
    mTrackDataValue = value
    mTrackDataDesc = desc
    createFiled()
    } else if (annotationDesc == TRACK_SCAN_DATA_DESC) { // TrackScanData注解
    mTrackScanDataName = name
    mTrackScanDataDesc = desc
    createFiled()
    }
    return super.visitAnnotation(annotationDesc, visible)
    }
    }
    }



    visitFiled方法用来扫描类文件中声明的字段。在该方法中,我们返回并实现FieldVisitor,并重新它的visitAnnotation方法,目的是找到之前TrackClickDataTrackScanData声明的埋点字段。对应的就是mTrackModelmTrackScanData


    主要包括字段名称name与字段的描述desc,为我们之后注入埋点数据做准备。


    另外一旦匹配到埋点数据的注解,说明该类中需要进行自动化埋点,所以还需要自动创建StatisticService。这是打点的接口方法,具体打点的都是通过StatisticService来实现。


    visitField中,通过createFiled方法来创建StatisticService类型的字段

    private fun createFiled() {
    if (!mFieldPresent) {
    mFieldPresent = true
    // 注入:statisticService 字段
    val fieldVisitor = cv.visitField(ACC_PRIVATE or ACC_FINAL, statisticServiceField.name, statisticServiceField.desc, null, null)
    fieldVisitor.visitEnd()
    }
    }



    其中statisticServiceField是封装好的StatisticService字段信息。

    companion object {
    const val OWNER = "com/idisfkj/androidapianalysis/proxy/StatisticService"
    const val DESC = "Lcom/idisfkj/androidapianalysis/proxy/StatisticService;"

    val INSTANCE = StatisticService()
    }

    val statisticService = FieldConfig(
    Opcodes.PUTFIELD,
    "",
    "mStatisticService",
    DESC
    )



    创建的字段名为mStatisticService,它的类型是StatisticService


    到这里我们已经拿到了埋点的数据字段,并创建了埋点的调用字段mStatisticService;接下来要做的就是注入埋点代码。


    核心注入代码在visitMethod方法中,该方法用来扫描类中的方法。所以类中声明的方法都会在这个方法中进行扫描回调。


    visitMethod中,我们找到目标的埋点方法,即之前声明的方法注解TrackScanTrackClick


    返回并实现AdviceAdapter,重写它的visitAnnotation方法。


    该方法会自动扫描方法的注解,所以可以通过该方法来保存当前方法的注解。


    然后在onMethodExit中,即方法的开头处进行注入代码。


    在该方法中主要做三件事



    1. 向默认构造方法中,实例化statisticService

    2. 注入TrackClick 点击

    3. 注入TrackScan 曝光


    具体的ASM注入代码可以通过之前说的SM Bytecode Viewer Support Kotlin插件获取。


    有了上面的实现,再来运行运行主项目,你就会发现埋点代码已经自动注入成功。


    我们反编译一下.class文件,来看下注入后的java代码


    StatisticService初始化

    public ProxyActivity() {
    boolean var2 = false;
    List var3 = (List)(new ArrayList());
    this.mTrackScanData = var3;
    // 以下是注入代码
    this.mStatisticService = (StatisticService)Statistic.Companion.getInstance().create(StatisticService.class);
    }



    曝光埋点

    @TrackScan
    public final void onScan() {
    this.mTrackScanData.add(new TrackModel("statistic_button", 0L, 2, (DefaultConstructorMarker)null));
    this.mTrackScanData.add(new TrackModel("statistic_text", 0L, 2, (DefaultConstructorMarker)null));
    // 以下是注入代码
    LogUtils.INSTANCE.d("inject track scan success.");
    Iterator var2 = this.mTrackScanData.iterator();

    while(var2.hasNext()) {
    TrackModel var1 = (TrackModel)var2.next();
    this.mStatisticService.trackScan(var1.getName());
    }

    }



    点击埋点

    @TrackClick
    public final void onClick(@NotNull View view) {
    Intrinsics.checkParameterIsNotNull(view, "view");
    this.mTrackModel.setTime(System.currentTimeMillis() / (long)1000);
    this.mTrackModel.setName(view.getId() == 2131230792 ? "statistic_button" : "statistic_text");
    // 以下是注入代码
    LogUtils.INSTANCE.d("inject track click success.");
    this.mStatisticService.trackClick(this.mTrackModel.getName(), this.mTrackModel.getTime());
    }



    以上自动化埋点代码就已经完成了。


    简单总结一下,所用到的技术有



    1. gradle plugin插件的自定义

    2. gradle transform提供编译中字节码的修改入口

    3. asm提供代码的注入实现


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

    [Android]使用函数指针实现native层异步回调

    1. 前言 在上篇关于lambda表达式实现方式的文章中,有提到一个概念叫做MethodHandle,当时的解释是类似于C/C++的函数指针,但是文章发出后咨询友人的意见,发现很多人并不清楚函数指针是怎么用的,其实我本人也是只是知道这个概念,但是并没有实际使用...
    继续阅读 »

    1. 前言


    在上篇关于lambda表达式实现方式的文章中,有提到一个概念叫做MethodHandle,当时的解释是类似于C/C++的函数指针,但是文章发出后咨询友人的意见,发现很多人并不清楚函数指针是怎么用的,其实我本人也是只是知道这个概念,但是并没有实际使用过。仿佛冥冥中自有天意,前几天公司的项目正好用到了函数指针来做native层的事件回调,也让我理解了函数指针的妙用。但是关于C/C++我并不是特别熟练,于是将实现过程写了个DEMO,一是为了做个记录熟悉过程,二是以备后续使用。


    2. 概念


    如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。


    那么这个指针变量怎么定义呢?虽然同样是指向一个地址,但指向函数的指针变量同我们之前讲的指向变量的指针变量的定义方式是不同的。例如:


    int(*p)(int, int);


    这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即(p);其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int()(int,int)。


    所以函数指针的定义方式为:


    函数返回值类型 (* 指针变量名) (函数参数列表);


    “函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。


    我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。


    那么怎么判断一个指针变量是指向变量的指针变量还是指向函数的指针变量呢?首先看变量名前面有没有“”,如果有“”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。


    3. 定义函数指针和枚举


    假设native层有个耗时操作需要异步调用,我们在异步调用结束后通过回调通知业务层完成事件,那么这个时候就可以使用函数指针作为回调方法。


    定义方式:



    1. 首先定义事件枚举:


    enum EventEnum {
    eeSleepWake,
    };



    1. 其次,定义一个函数指针:


    typedef void (*onSleepWake)(int code, void* sender);


    这个函数指针可以指向一个返回值为void 参数分别为 int 和void型指针的函数,其中void型指针表示调用方的指针



    1. 定义一个结构体,包含函数指针和调用方的指针


    struct EventData {
    void* eventPointer;
    void* sender;
    };



    1. 注册事件持有类,使其成为单例


    这个操作的部分代码:


    class EventManager {
    public:
    static EventManager& singleton()
    {
    static EventManager sl;
    return sl;
    }
    static EventManager& getInstance()
    {
    return singleton();
    }

    //注册事件
    void addEvent(EventEnum eventEnum, void* event, void* sender);

    EventData getEventData(EventEnum eventEnum);

    private:
    std::map<EventEnum, EventData> eventMap;
    EventManager(){};
    ~EventManager(){};
    };



    1. 实现事件注册函数


    void EventManager::addEvent(EventEnum eventEnum, void* event, void* sender) {
    if(event == nullptr || sender == nullptr) {
    return;
    }
    EventData eventData;
    eventData.eventPointer = event;
    eventData.sender = sender;

    eventMap.insert(std::pair<EventEnum, EventData>(eventEnum, eventData));
    }



    1. 编写函数指针对应函数的具体实现


    void eeSleepWakeCallback(int result, void* sender) {
    JniTester *tester = (JniTester *) sender;
    tester->onResultCallback(result);
    }



    1. 在入口类中注册事件及其对应的枚举和函数


    JniTester::JniTester() {
    EventManager::getInstance().addEvent(eeSleepWake, (void*)eeSleepWakeCallback, this);
    }



    1. 编写异步函数调用


    ···
    void JniTester::getThreadResult() {
    ThreadTest *test = new ThreadTest();
    test->sleepThread();
    }
    ···
    耗时函数的具体实现:


    void ThreadTest::sleepThread() {
    std::thread cal_task(&ThreadTest::makeSleep, this);
    cal_task.detach();
    }

    void ThreadTest::makeSleep() {
    sleep(2);
    }


    这一步我们是通过新建一个线程,并让其等待2S来模拟异步耗时操作


    4. 异步回调的实现



    1. 在java层编写java的回调方法


    private OnResultCallback callback;

    public void setOnResultCallback(OnResultCallback callback) {
    this.callback = callback;
    }

    public interface OnResultCallback {
    void onResult(int result);
    }



    1. 在java曾编写java层回调的触发:


        public void onResult(int result) {
    if (this.callback != null) {
    callback.onResult(result);
    }
    }



    1. native层异步动作完成的通知


    通过向单例的事件持有类获取对应的事件枚举,获取到其对应的函数指针,并调用该函数指针实现:


    void ThreadTest::makeSleep() {
    sleep(2);
    EventData eventData = EventManager::singleton().getEventData(eeSleepWake);
    onSleepWake wake = (onSleepWake)eventData.eventPointer;
    if(wake) {
    wake(12345, eventData.sender);
    }
    }


    因为我们在第三章节第7步注册的函数指针是eeSleepWakeCallback, 因此,这里会调用到这个函数:


    void eeSleepWakeCallback(int result, void* sender) {
    JniTester *tester = (JniTester *) sender;
    tester->onResultCallback(result);
    }


    通过sender确定具体的对象,调用其onResultCallback函数



    1. onResultCallback函数的实现


    void JniTester::onResultCallback(int result) {
    JNIEnv *env = NULL;
    int status = f_jvm->GetEnv((void **) &env, JNI_VERSION_1_4);

    bool isInThread = false;
    if (status < 0) {
    isInThread = true;
    f_jvm->AttachCurrentThread(&env, NULL);
    }

    if (f_cls != NULL) {
    jmethodID id = env->GetMethodID(f_cls, "onResult", "(I)V");
    if (id != NULL) {
    env->CallVoidMethod(f_obj, id, result);
    }
    }

    if (isInThread) {
    f_jvm->DetachCurrentThread();
    }
    }


    这里因为缺少java环境,因此我们需要将该线程挂载到jvm上执行,并获取对应的JNIEnv ,通过jnienv获取java层的回调触发方法onResult并执行。


    5.效果


    编写测试代码:


            JniTester tester = new JniTester();
    Log.d("zyl", "startTime = " + System.currentTimeMillis());
    tester.setOnResultCallback(result -> {
    Log.d("zyl", "endTime = " + System.currentTimeMillis());
    Log.d("zyl", "result = " + result);
    });
    tester.requestData();


    执行结果:
    image.png


    和预期一致,完美。


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

    NestedScrollView嵌套滑动源码解读!

    1、前言滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:滑动基础ScrollView滑动源...
    继续阅读 »

    1、前言

    滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:

    在本章内,本章从两个嵌套的两个视角来分析

    1. 子滑动视图视角:涉及NestedScrollingChild3接口以及NestedScrollingChildHelper辅助类
    2. 父滑动容器视角:涉及NestedScrollingParent3接口以及NestedScrollingParentHelper辅助类

    这篇内容分三个小章节

    1. NestedScrollingChildHelper类
    2. NestedScrollingParentHelper类
    3. 实现处理以及调用时机

    在这里类的解读是必须的,不然只能死记其调用时机,这里是不建议的;下面会贴一部分源码,在源码中会对代码的一些关键进行注释说明

    2、NestedScrollingChildHelper类

    嵌套子视图角色;主要功能

    • 事件是否需要通知
    • 事件通知

    类中如下变量:

        private ViewParent mNestedScrollingParentTouch; // touch事件接力的父容器
    private ViewParent mNestedScrollingParentNonTouch; // 非touch事件接力的父容器
    private final View mView; // 当前容器,也是作为嵌套滑动时孩子角色的容器
    private boolean mIsNestedScrollingEnabled; // 当前容器是否支持嵌套滑动
    private int[] mTempNestedScrollConsumed; // 二维数组,保存x、y消耗的事件长度;减少对象生成的
    复制代码

    2.1 实例获取

        public NestedScrollingChildHelper(@NonNull View view) {
    mView = view;
    }
    复制代码

    2.2 嵌套滑动支持

    是对嵌套子视图的角色来说的

        public void setNestedScrollingEnabled(boolean enabled) {
    if (mIsNestedScrollingEnabled) {
    ViewCompat.stopNestedScroll(mView); // 兼容模式调用
    }
    mIsNestedScrollingEnabled = enabled;
    }

    public boolean isNestedScrollingEnabled() {
    return mIsNestedScrollingEnabled;
    }
    复制代码

    2.3 嵌套滑动相关方法

    要支持嵌套滑动,那么必须有多个支持嵌套滑动的容器;作为子视图,其需要有通知的一套,因此方法有:

    • 父容器的查找、判断
    • 通知开始、过程以及结束

    2.3.1 嵌套父容器的查找

    成员变量mNestedScrollingParentTouch、mNestedScrollingParentNonTouch为父容器缓存变量;其直接设置和获取方法如下

      private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
    switch (type) {
    case TYPE_TOUCH:
    return mNestedScrollingParentTouch;
    case TYPE_NON_TOUCH:
    return mNestedScrollingParentNonTouch;
    }
    return null;
    }

    private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
    switch (type) {
    case TYPE_TOUCH:
    mNestedScrollingParentTouch = p;
    break;
    case TYPE_NON_TOUCH:
    mNestedScrollingParentNonTouch = p;
    break;
    }
    }
    复制代码

    2.3.2 嵌套父容器的支持判断

        public boolean hasNestedScrollingParent() {
    return hasNestedScrollingParent(TYPE_TOUCH);
    }

    public boolean hasNestedScrollingParent(@NestedScrollType int type) {
    return getNestedScrollingParentForType(type) != null;
    }
    复制代码

    2.3.3 滑动开始通知

        public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
    if (hasNestedScrollingParent(type)) {
    return true;
    }
    if (isNestedScrollingEnabled()) { // 孩子视图支持嵌套滑动,只有支持才会继续执行
    ViewParent p = mView.getParent();
    View child = mView;
    while (p != null) { // 查找的不仅仅直接父容器
    // 兼容调用,父容器是否可以作为嵌套父容器角色
    if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
    setNestedScrollingParentForType(type, p); // 这里进行了缓存
    // 兼容调用,父容器
    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
    return true;
    }
    if (p instanceof View) {
    child = (View) p;
    }
    p = p.getParent();
    }
    }
    return false;
    }
    复制代码

    父容器的查找,采取了延时策略,在进行事件时,才进行查询,并且在查询到了,进行支持;所以可以这样理解:

    1. onStartNestedScroll:是父容器接受事件通知方法,其结果表示是否可以作为嵌套滑动的父容器角色
    2. onNestedScrollAccepted:不是必调用,调用了表明嵌套父容器角色支持view的后续嵌套处理

    2.3.4 手指滑动通知

    滑动时通知,分为滑动前和滑动后;使嵌套滑动处理更灵活 滑动前通知

        public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
    @Nullable int[] offsetInWindow) {
    return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);
    }

    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
    @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    if (isNestedScrollingEnabled()) {
    final ViewParent parent = getNestedScrollingParentForType(type);
    if (parent == null) {
    return false;
    }

    if (dx != 0 || dy != 0) {
    int startX = 0;
    int startY = 0;
    if (offsetInWindow != null) {
    mView.getLocationInWindow(offsetInWindow);
    startX = offsetInWindow[0];
    startY = offsetInWindow[1];
    }

    if (consumed == null) {
    consumed = getTempNestedScrollConsumed();
    }
    consumed[0] = 0;
    consumed[1] = 0;
    ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

    if (offsetInWindow != null) {
    mView.getLocationInWindow(offsetInWindow);
    offsetInWindow[0] -= startX;
    offsetInWindow[1] -= startY;
    }
    return consumed[0] != 0 || consumed[1] != 0;
    } else if (offsetInWindow != null) {
    offsetInWindow[0] = 0;
    offsetInWindow[1] = 0;
    }
    }
    return false;
    }
    复制代码

    其中两个二维数组作为结果回传;通过父容器的onNestedPreScroll方法进行处理并把滑动处理详情放入两个二维数组中,常用的详情为消耗长度情况;返回结果表示滑动前是否处理

    滑动后通知

        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
    int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
    return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
    offsetInWindow, TYPE_TOUCH, null);
    }

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
    int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
    return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
    offsetInWindow, type, null);
    }

    public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
    int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,
    @Nullable int[] consumed) {
    dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
    offsetInWindow, type, consumed);
    }

    private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
    int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
    @NestedScrollType int type, @Nullable int[] consumed) {
    if (isNestedScrollingEnabled()) {
    final ViewParent parent = getNestedScrollingParentForType(type);
    if (parent == null) {
    return false;
    }

    if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
    int startX = 0;
    int startY = 0;
    if (offsetInWindow != null) {
    mView.getLocationInWindow(offsetInWindow);
    startX = offsetInWindow[0];
    startY = offsetInWindow[1];
    }

    if (consumed == null) {
    consumed = getTempNestedScrollConsumed();
    consumed[0] = 0;
    consumed[1] = 0;
    }

    ViewParentCompat.onNestedScroll(parent, mView,
    dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);

    if (offsetInWindow != null) {
    mView.getLocationInWindow(offsetInWindow);
    offsetInWindow[0] -= startX;
    offsetInWindow[1] -= startY;
    }
    return true;
    } else if (offsetInWindow != null) {
    offsetInWindow[0] = 0;
    offsetInWindow[1] = 0;
    }
    }
    return false;
    }
    复制代码

    其中两个二维数组作为结果回传;通过父容器的onNestedScroll方法进行处理并把滑动处理详情放入两个二维数组中,常用的详情为消耗长度情况;返回结果表示滑动前是否处理

    2.3.5 滑翔通知

    滑翔也有两个时机

    滑翔前

       public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    if (isNestedScrollingEnabled()) {
    ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
    if (parent != null) {
    return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
    velocityY);
    }
    }
    return false;
    }
    复制代码

    返回结果表明父容器的是否处理滑翔;父容器是通过onNestedPreFling进行处理

    滑翔后

      public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    if (isNestedScrollingEnabled()) {
    ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
    if (parent != null) {
    return ViewParentCompat.onNestedFling(parent, mView, velocityX,
    velocityY, consumed);
    }
    }
    return false;
    }
    复制代码

    返回结果表明父容器的是否处理滑翔;父容器是通过onNestedFling进行处理

    滑翔是一个互斥处理的过程,而滑动是一个接力的过程

    2.3.6 滑动结束通知

        public void stopNestedScroll() {
    stopNestedScroll(TYPE_TOUCH);
    }

    public void stopNestedScroll(@NestedScrollType int type) {
    ViewParent parent = getNestedScrollingParentForType(type);
    if (parent != null) {
    // 通知嵌套父容器,滑动结束
    ViewParentCompat.onStopNestedScroll(parent, mView, type);
    setNestedScrollingParentForType(type, null); // 清理父容器引用
    }
    }
    复制代码

    3、NestedScrollingParentHelper类

    作为嵌套滑动的父容器角色,其只有接受通知时处理即可,情况没有子视图角色那么复杂;而辅助类里仅仅是对滑动方向做了声明周期处理;

    成员变量

        private int mNestedScrollAxesTouch; // Touch事件时,接受处理时,事件的滑动方法
    private int mNestedScrollAxesNonTouch; // 非Touch事件时,接受处理时,事件的滑动方法
    复制代码

    3.1 滑动方向获取

        public int getNestedScrollAxes() {
    return mNestedScrollAxesTouch | mNestedScrollAxesNonTouch;
    }
    复制代码

    3.2 滑动方向设置

        public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
    @ScrollAxis int axes) {
    onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
    }

    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
    @ScrollAxis int axes, @NestedScrollType int type) {
    if (type == ViewCompat.TYPE_NON_TOUCH) {
    mNestedScrollAxesNonTouch = axes;
    } else {
    mNestedScrollAxesTouch = axes;
    }
    }
    复制代码

    3.3 滑动方向重置

       public void onStopNestedScroll(@NonNull View target) {
    onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
    }

    public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
    if (type == ViewCompat.TYPE_NON_TOUCH) {
    mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;
    } else {
    mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;
    }
    }
    复制代码

    4、嵌套实现机制

    作为一是具有兼容性实现的嵌套滑动容器,它必须实现下面接口

    • 滑动容器接口ScrollingView
    • 嵌套滑动父容器接口NestedScrollingParent3
    • 嵌套滑动子视图接口NestedScrollingChild3

    嵌套接口,可以根据容器角色选择实现;方法实现需要利用辅助类

    从上面对两个辅助类解读;对他们已经实现的功能做了归纳

    1. 嵌套是否支持
    2. 嵌套通知
    3. 嵌套滑动方向

    也就是作为子视图角色的实现方法基本使用辅助类即可,而嵌套父容器角色需要我们增加实现逻辑;需要实现从功能上划分:

    1. 作为嵌套子视图设置,
    2. 作为嵌套父容器的实现
    3. 滑动接力处理,以及滑翔处理

    4.1 嵌套子视图支持

    构造器中进行setNestedScrollingEnabled(true)方法进行设置

    setNestedScrollingEnabled方法

        public void setNestedScrollingEnabled(boolean enabled) {
    mChildHelper.setNestedScrollingEnabled(enabled);
    }
    复制代码

    4.2 嵌套父容器的支持

        public boolean onStartNestedScroll(
    @NonNull View child, @NonNull View target, int nestedScrollAxes) {
    return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
    }

    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
    int type) {
    return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
    复制代码

    可滑动方向判断进而决定是否支持的;支持时的处理如下

        public void onNestedScrollAccepted(
    @NonNull View child, @NonNull View target, int nestedScrollAxes) {
    onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
    }

    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
    int type) {
    mParentHelper.onNestedScrollAccepted(child, target, axes, type);
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
    }
    复制代码

    其还是一个子视图角色,所以,其需要继续传递这个滑动开始的信号;可见嵌套默认处理中:其实是一个嵌套滑动容器链表,中间也可能存在滑动容器(不支持嵌套),链表组后一个容器的‘父’容器也还可能是嵌套滑动;这些情况造成的一个原因是同时是父容器还是子视图才会继续分发;这个链头容器必定是个嵌套子视图角色,中间即是子视图角色也是父容器角色,链尾容器必定是个嵌套父容器角色

    时机

    在down事件中,调用startNestedScroll方法

    4.3 利用辅助类重写

    下面方法利用了辅助类直接重写

    • 嵌套父容器存在判断:hasNestedScrollingParent
    • 子视图是否支持嵌套滑动:setNestedScrollingEnabled、isNestedScrollingEnabled
    • 开始通知:startNestedScroll
    • 滑动分发:dispatchNestedPreScroll、dispatchNestedScroll
    • 滑翔分发:dispatchNestedPreFling、dispatchNestedFling
    • 结束通知:stopNestedScroll

    参数中涉及到滑动类型时,均采用ViewCompat.TYPE_TOUCH作为默认类型

    4.4 滑动接力处理

        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
    onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
    }

    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
    int type) {
    dispatchNestedPreScroll(dx, dy, consumed, null, type);
    }
    复制代码

    其作为父容器,本身对事件并没有处理,而是作为子视图继续分发下去;时机move事件中嵌套子视图处理滑动之前

        public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
    int dxUnconsumed, int dyUnconsumed) {
    onNestedScrollInternal(dyUnconsumed, ViewCompat.TYPE_TOUCH, null);
    }

    private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
    final int oldScrollY = getScrollY();
    scrollBy(0, dyUnconsumed);
    final int myConsumed = getScrollY() - oldScrollY;

    if (consumed != null) {
    consumed[1] += myConsumed;
    }
    final int myUnconsumed = dyUnconsumed - myConsumed;

    mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
    }
    复制代码

    父容器首先处理了滑动,然后把处理后的情况继续传递;时机move事件,嵌套子视图处理之后

    4.5 滑翔互斥处理

        public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
    return dispatchNestedPreFling(velocityX, velocityY);
    }

    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
    复制代码

    不进行处理,而是做为嵌套子视图继续分发;时机up事件,拦截时,嵌套子视图处理之前

        public boolean onNestedFling(
    @NonNull View target, float velocityX, float velocityY, boolean consumed) {
    if (!consumed) {
    dispatchNestedFling(0, velocityY, true);
    fling((int) velocityY);
    return true;
    }
    return false;
    }
    复制代码

    如果接受到通知时,未处理,则进行处理;并做为嵌套子view继续通知处理;时机up事件,拦截时,嵌套子视图处理之后

    4.6 滑动结束

        public void onStopNestedScroll(@NonNull View target) {
    onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
    }
    public void onStopNestedScroll(@NonNull View target, int type) {
    mParentHelper.onStopNestedScroll(target, type);
    stopNestedScroll(type);
    }
    public void stopNestedScroll(int type) {
    mChildHelper.stopNestedScroll(type);
    }
    复制代码

    由于还是嵌套子视图角色,还需要通知其处理的嵌套父容器结束;时机up、cancel事件时

    4.7 嵌套子视图优先处理

    android中,从容器的默认拦截机制来看,父容器优先拦截;但是嵌套时做了额外判断,

    滑动事件拦截中是这样判断的

    yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0)
    复制代码

    滑动的坐标轴为0,也就是既不是x轴、也不是y轴;这说明,它作为嵌套父容器时,没有嵌套子容器传递给它;

    另外如果滑动已经被拦截处理,则不希望其它进行再次拦截;这时由于嵌套拦截体系已经提供了交互的方法,如果不这样处理,就会导致和默认的事件机制冲突;因此,如果有这种情况,那就把重写父容器,让其支持嵌套滑动吧

    5 小结

    总的来说,嵌套滑动呢,它抽象了接口和辅助类,来帮助开发者进行实现;其中实现的核心思触发点

    1. 嵌套的组织关系
    2. 嵌套的互相通知处理
    3. 自己处于角色中,是否需要处理以及如何处理
    收起阅读 »

    LinkedList源码解析(手把手带你熟悉链表)

    前言链表是常见的数据结构之一,但是很多同学只听说过链表,并不知道什么是链表,所以本文将会带领各位同学手写一个LinkedList,源码跟官方会有点不一样,不过思路是大概相同的,最后再带领大家读官方源码为了降低源码难度简化泛型代码,手写的LinkedList只能...
    继续阅读 »
    • 前言

    链表是常见的数据结构之一,但是很多同学只听说过链表,并不知道什么是链表,所以本文将会带领各位同学手写一个LinkedList,源码跟官方会有点不一样,不过思路是大概相同的,最后再带领大家读官方源码

    为了降低源码难度简化泛型代码,手写的LinkedList只能添加String类型数据

    • 什么是链表?

    可以理解为,把一些数据按照顺序排好,手拉手 每一个数据就是一个节点,所有节点连在一起,就组成了链表 在这里插入图片描述

    • LinkedList的节点定义

    LinkedList是双链表,所以有左节点和右节点,我们先定义一个实体类,这个实体类就可以理解为节点

        /**
    * 节点实体类
    */

    private static class NodeBean {
    NodeBean leftNode; //左节点
    String value; //节点的值
    NodeBean rightNode; //右节点
    }
    复制代码
    图片名称
    • 节点怎么连接?

    在这里插入图片描述

    看完上面的图片,应该大概知道两个节点的连接方法了,我们只需要:

    节点1.右节点 = 节点2
    节点2.左节点 = 节点1
    复制代码

    我们先实现链表的add方法,add方法其实就是将两个节点连接:

        /**
    * 添加值
    */

    public void add(String value) {
    //先获取尾节点(上一个节点)
    NodeBean lastNode = this.lastNode;
    //创建一个新节点
    NodeBean newNode = new NodeBean();
    //为节点赋值
    newNode.value = value;
    //左节点为最后一个节点(尾节点)
    newNode.leftNode = lastNode;
    //由于是添加节点,所以右节点为null,可以不写
    newNode.rightNode = null;
    //将成员变量的最后一个节点改为当前新节点
    this.lastNode = newNode;
    //判断头节点是否为空
    if (this.firstNode == null) {
    //如果为空说明当前是第一个节点,需要把头结点也设为当前节点
    this.firstNode = newNode;
    }else{
    //如果不为空,需要把前一个节点的右节点指向当前节点
    //两个节点相连接的条件是:
    // 1. 前一个节点的右节点指向当前节点
    // 2. 当前节点的左节点指向上一个节点
    lastNode.rightNode = newNode;
    }
    //链表长度+1
    size++;
    }
    复制代码
    • 节点怎么断开?

    两个节点断开只需要将自己的上一个节点的右节点指向自己的下一个节点左节点,同时自己的下一个节点的左节点,指向上一个节点的右节点

    注意看下图箭头方向,这样节点2就可以直接断开,节点1和节点3直接连接,这里也可以很明显的看出,链表增删很快,只需要断开前后节点就可以

    在这里插入图片描述

    先看一下断开节点的大概代码思路:

    节点2.左节点 = null
    节点2.右节点 = null
    节点1.右节点 = 节点3
    节点3.左节点 = 节点1

    这样就可以断开当前节点,并且将链表重新连接起来
    复制代码

    现在我们来实现remove(index)方法,根据索引删除指定节点:

        /**
    * 删除值
    */

    public void remove(int index){
    这里代码通过索引查找节点,为了简化代码,请忽略这里的代码
    通过索引查找节点,下面会写到
    ...
    ***indexNode就是我们通过索引拿到的节点***

    indexNode = 通过索引查找节点(index)

    //拿到该节点的左节点、右节点以及值
    NodeBean leftNode = indexNode.leftNode;
    NodeBean rightNode = indexNode.rightNode;

    //判断左节点是否为空,如果为空说明当前节点为(头结点)第一个节点
    if (leftNode == null) {
    this.firstNode = indexNode;
    }else{
    //左节点不为空,需要断开自己的左节点
    indexNode.leftNode = null;
    //将上一个节点的右节点连接到下一个节点
    leftNode.rightNode = rightNode;
    }

    //判断右节点是否为空,如果为空说明当前为(尾节点)最后一个节点
    if (rightNode == null) {
    this.lastNode = indexNode;
    }else{
    //右节点不为空,需要断开自己的右节点
    indexNode.rightNode = null;
    //将下一个节点的左节点连接到上一个节点
    rightNode.leftNode = leftNode;
    }

    //当前节点值置空
    indexNode.value = null;
    size--;
    }
    复制代码
    • 通过索引查找节点

    1. 先拿到头节点
    2. 拿到当前要查找的索引index
    3. 循环index的次数
    4. 每循环一次,就从头结点开始往后移动一个节点
    复制代码

    现在我们来实现以下get(index)方法,通过索引获取置顶节点:

        /**
    * 通过索引获取节点的值
    */

    public String get(int index){
    //由于链表没有索引,所以只能一个一个遍历查找
    //先拿到链表的第一个节点(头节点)
    NodeBean firstNode = this.firstNode;
    for (int i = 0; i < index; i++) {
    //每次循环就从头结点往后挪动一个节点
    firstNode = firstNode.rightNode;
    }
    //为了简化代码便于理解,这里不考虑tempNode为null的情况
    return firstNode.value;
    }
    复制代码
    • 总结

    1. 链表的每个节点之间都有连接,如果新增节点只需要直接插入就行,所以==链表新增快==
    2. 链表断开节点只需要将自己的前后节点重新连接就可以,所以链表==删除快==
    3. 链表没有索引,查找需要循环整个链表,所以==查询慢==
    • MyLinkedList完整代码:

    public class MyLinkedList {
    private int size; //当前链表的长度
    private NodeBean firstNode; //头节点
    private NodeBean lastNode; //尾节点

    /**
    * 添加值
    */

    public void add(String value) {
    //先获取尾节点
    NodeBean lastNode = this.lastNode;
    //创建一个新节点
    NodeBean newNode = new NodeBean();
    //为节点赋值
    newNode.value = value;
    //左节点为最后一个节点(尾节点)
    newNode.leftNode = lastNode;
    //由于是添加节点,所以右节点为null,可以不写
    newNode.rightNode = null;
    //将成员变量的最后一个节点改为当前新节点
    this.lastNode = newNode;
    //判断头节点是否为空
    if (this.firstNode == null) {
    //如果为空说明当前是第一个节点,需要把头结点也设为当前节点
    this.firstNode = newNode;
    }else{
    //如果不为空,需要把前一个节点的右节点指向当前节点
    //两个节点相连接的条件是:
    // 1. 前一个节点的右节点指向当前节点
    // 2. 当前节点的左节点指向上一个节点
    lastNode.rightNode = newNode;
    }
    //链表长度+1
    size++;
    }

    /**
    * 删除值
    */

    public void remove(int index){
    //先找到当前索引对应的节点
    //由于链表没有索引,所以只能一个一个遍历查找
    //先拿到链表的第一个节点(头节点)
    NodeBean indexNode = this.firstNode;
    for (int i = 0; i < index; i++) {
    //每次循环就从头结点往后挪动一个节点
    indexNode = indexNode.rightNode;
    }
    //拿到该节点的左节点、右节点以及值
    NodeBean leftNode = indexNode.leftNode;
    NodeBean rightNode = indexNode.rightNode;

    //判断左节点是否为空,如果为空说明当前节点为(头结点)第一个节点
    if (leftNode == null) {
    this.firstNode = indexNode;
    }else{
    //左节点不为空,需要断开自己的左节点
    indexNode.leftNode = null;
    //将上一个节点的右节点连接到下一个节点
    leftNode.rightNode = rightNode;
    }

    //判断右节点是否为空,如果为空说明当前为(尾节点)最后一个节点
    if (rightNode == null) {
    this.lastNode = indexNode;
    }else{
    //右节点不为空,需要断开自己的右节点
    indexNode.rightNode = null;
    //将下一个节点的左节点连接到上一个节点
    rightNode.leftNode = leftNode;
    }

    //当前节点值置空
    indexNode.value = null;
    size--;
    }

    /**
    * 通过索引获取节点的值
    */

    public String get(int index){
    //由于链表没有索引,所以只能一个一个遍历查找
    //先拿到链表的第一个节点(头节点)
    NodeBean firstNode = this.firstNode;
    for (int i = 0; i < index; i++) {
    //每次循环就从头结点往后挪动一个节点
    firstNode = firstNode.rightNode;
    }
    //为了简化代码便于理解,这里不考虑tempNode为null的情况
    return firstNode.value;
    }

    /**
    * 获取链表长度
    */

    public int getSize(){
    return this.size;
    }

    /**
    * 节点实体类
    */

    private static class NodeBean {
    NodeBean leftNode; //左节点
    String value; //节点的值
    NodeBean rightNode; //右节点
    }
    }

    复制代码
    • 重点

    • 如果你看完上面的增删查方法, 可以完全看懂了,就可以继续往下看了
    • 如果你没看懂,请复制上面的完整代码到编辑器,自己断点研究一下

    ==如果上面的代码理解了,恭喜你! 现在你应该已经可以看懂官方源码了==

    • 官方代码解析

    • 节点实体类

        private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
    }
    }
    复制代码

    代码对比 在这里插入图片描述

    • 插入节点

        void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
    first = newNode;
    else
    l.next = newNode;
    size++;
    modCount++;
    }
    复制代码

    代码对比 在这里插入图片描述

    • 断开节点

        E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
    first = next;
    } else {
    prev.next = next;
    x.prev = null;
    }

    if (next == null) {
    last = prev;
    } else {
    next.prev = prev;
    x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
    }
    复制代码

    代码对比 在这里插入图片描述

    • 最后总结

    • 如果现在你可以看懂官方的这三个方法了,那可以尝试自己去读剩下的部分方法,比如==unlinkFirst()和linkFirst()==
    • 读源码并不可怕,只要理解源码的思路,顿时豁然开朗
    收起阅读 »

    Android修炼系列(十二),自定义一个超顺滑的回弹RecyclerView

    前面写了一个嵌套滑动框架和分析了ViewDragHelper的事件分发,本节主要自定义一个带有回弹效果的RecyclerView,看看事件和动画的配合,这在各大App中都比较常见了,效果如下: 实现 这是定义的回弹类:OverScrollRecycl...
    继续阅读 »

    前面写了一个嵌套滑动框架和分析了ViewDragHelper的事件分发,本节主要自定义一个带有回弹效果的RecyclerView,看看事件和动画的配合,这在各大App中都比较常见了,效果如下:





    实现


    这是定义的回弹类:OverScrollRecyclerView,其是RecyclerView的子类,并实现了OnTouchListener方法:


    public class OverScrollRecyclerView extends RecyclerView implements View.OnTouchListener {

    public OverScrollRecyclerView(Context context) {
    this(context, null);
    }

    public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initParams();
    }
    }
    复制代码

    随后会定义一些必要的属性,其中DEFAULT_TOUCH_DRAG_MOVE_RATIO表示滑动的像素数与实际view偏移量的比例,减速系数和时间也都是根据实际效果不断调整的。


    ```java
    public class OverScrollRecyclerView extends RecyclerView implements View.OnTouchListener {

    // 下拉与上拉,move px / view Translation
    private static final float DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD = 2f;
    private static final float DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK = 1f;
    // 默认减速系数
    private static final float DEFAULT_DECELERATE_FACTOR = -2f;
    // 最大反弹时间
    private static final int MAX_BOUNCE_BACK_DURATION_MS = 800;
    private static final int MIN_BOUNCE_BACK_DURATION_MS = 200;

    // 初始状态,滑动状态,回弹状态
    private IDecoratorState mCurrentState;
    private IdleState mIdleState;
    private OverScrollingState mOverScrollingState;
    private BounceBackState mBounceBackState;

    private final OverScrollStartAttributes mStartAttr = new OverScrollStartAttributes();
    private float mVelocity;
    private final RecyclerView mRecyclerView = this;
    ...
    public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initParams();
    }
    ...
    }

    复制代码

    这是我们的状态接口IDecoratorState,其提供了3个方法,IdleState、OverScrollingState、BounceBackState都是它的具体实现类,符合状态模式的思想:


        protected interface IDecoratorState {
    // 处理move事件
    boolean handleMoveTouchEvent(MotionEvent event);
    // 处理up事件
    boolean handleUpTouchEvent(MotionEvent event);
    // 事件结束后的动画处理
    void handleTransitionAnim(IDecoratorState fromState);
    }
    复制代码

    初始化我们定义的变量,没有什么特殊的操作,只是一些各自属性的赋值,具体见下文:


        private void initParams() {
    mBounceBackState = new BounceBackState();
    mOverScrollingState = new OverScrollingState();
    mCurrentState = mIdleState = new IdleState();
    attach();
    }
    复制代码

    这是我们的attach,添加触摸监听,并去掉滚动到边缘的光晕效果:


        @SuppressLint("ClickableViewAccessibility")
    public void attach() {
    mRecyclerView.setOnTouchListener(this);
    mRecyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
    }
    复制代码

    核心代码就是事件的监听了,需要我们处理onTouch事件,当手指按下滑动时,此时mCurrentState还处于初始状态,其会执行相应的handleMoveTouchEvent方法:


        @Override
    public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_MOVE:
    return mCurrentState.handleMoveTouchEvent(event);
    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
    return mCurrentState.handleUpOrCancelTouchEvent(event);
    }
    return false;
    }
    复制代码

    这是初始状态IdleState处理move的逻辑,主要做些校验工作,如果移动不满足要求,就将事件透出去,具体见下:


        @Override
    public boolean handleMoveTouchEvent(MotionEvent event) {
    // 是否符合move要求,不符合不拦截事件
    if (!initMotionAttributes(mRecyclerView, mMoveAttr, event)) {
    return false;
    }
    // 在RecyclerView顶部但不能下拉 或 在RecyclerView底部但不能上拉
    if (!((isInAbsoluteStart(mRecyclerView) && mMoveAttr.mDir) ||
    (isInAbsoluteEnd(mRecyclerView) && !mMoveAttr.mDir))) {
    return false;
    }
    // 保存当前Motion信息
    mStartAttr.mPointerId = event.getPointerId(0);
    mStartAttr.mAbsOffset = mMoveAttr.mAbsOffset;
    mStartAttr.mDir = mMoveAttr.mDir;
    // 初始状态->滑动状态
    issueStateTransition(mOverScrollingState);
    return mOverScrollingState.handleMoveTouchEvent(event);
    }
    复制代码

    这是initMotionAttributes方法,会计算Y方向偏移量,如果满足要求,则为MotionAttributes赋值:


        private boolean initMotionAttributes(View view, MotionAttributes attributes, MotionEvent event) {
    if (event.getHistorySize() == 0) {
    return false;
    }
    // 像素偏移量
    final float dy = event.getY(0) - event.getHistoricalY(0, 0);
    final float dx = event.getX(0) - event.getHistoricalX(0, 0);
    if (Math.abs(dy) < Math.abs(dx)) {
    return false;
    }
    attributes.mAbsOffset = view.getTranslationY();
    attributes.mDeltaOffset = dy;
    attributes.mDir = attributes.mDeltaOffset > 0;
    return true;
    }
    复制代码

    这里的isInAbsoluteStart方法用来判断,当前RecyclerView是否不能向下滑动,另一个isInAbsoluteEnd是否不能向上滑动,代码就不展示了:


        private boolean isInAbsoluteStart(View view) {
    return !view.canScrollVertically(-1);
    }
    复制代码

    当move事件通过初始状态的校验,则改变状态为滑动态OverScrollingState,正式处理滑动逻辑,其方法见下:


        @Override
    public boolean handleMoveTouchEvent(MotionEvent event) {
    final OverScrollStartAttributes startAttr = mStartAttr;
    // 不是一个触摸点事件,则直接切到回弹状态
    if (startAttr.mPointerId != event.getPointerId(0)) {
    issueStateTransition(mBounceBackState);
    return true;
    }

    final View view = mRecyclerView;

    // 是否符合move要求
    if (!initMotionAttributes(view, mMoveAttr, event)) {
    return true;
    }

    // mDeltaOffset: 实际要移动的像素,可以为下拉和上拉设置不同移动比
    float deltaOffset = mMoveAttr.mDeltaOffset / (mMoveAttr.mDir == startAttr.mDir
    ? mTouchDragRatioFwd : mTouchDragRatioBck);
    // 计算偏移
    float newOffset = mMoveAttr.mAbsOffset + deltaOffset;

    // 上拉下拉状态与滑动方向不符,则回到初始状态,并将视图归位
    if ((startAttr.mDir && !mMoveAttr.mDir && (newOffset <= startAttr.mAbsOffset)) ||
    (!startAttr.mDir && mMoveAttr.mDir && (newOffset >= startAttr.mAbsOffset))) {
    translateViewAndEvent(view, startAttr.mAbsOffset, event);
    issueStateTransition(mIdleState);
    return true;
    }

    // 不让父类截获move事件
    if (view.getParent() != null) {
    view.getParent().requestDisallowInterceptTouchEvent(true);
    }

    // 计算速度
    long dt = event.getEventTime() - event.getHistoricalEventTime(0);
    if (dt > 0) {
    mVelocity = deltaOffset / dt;
    }

    // 改变控件位置
    translateView(view, newOffset);
    return true;
    }
    复制代码

    这是translateView方法,改变view相对父布局的偏移量:


        private void translateView(View view, float offset) {
    view.setTranslationY(offset);
    }
    复制代码

    当滑动事件结束,手指抬起时,会将状态由滑动状态切换为回弹状态:


        @Override
    public boolean handleUpTouchEvent(MotionEvent event) {
    // 事件up切换状态,有滑动态-回弹态
    issueStateTransition(mBounceBackState);
    return false;
    }
    复制代码

    上文提到的issueStateTransition方法,只是说切换了状态,但实际上它还会执行handleTransitionAnim的操作,只不过初始状态和滑动状态此接口都是空实现,只有回弹状态才会去处理动画效果罢了:


        protected void issueStateTransition(IDecoratorState state) {
    IDecoratorState oldState = mCurrentState;
    mCurrentState = state;
    // 处理回弹动画效果
    mCurrentState.handleTransitionAnim(oldState);
    }
    复制代码

    这是我们处理动画效果的方法,核心方法createAnimator具体看下,之后添加了动画监听,并开启动画:


        @Override
    public void handleTransitionAnim(IDecoratorState fromState) {
    Animator bounceBackAnim = createAnimator();
    bounceBackAnim.addListener(this);
    bounceBackAnim.start();
    }
    复制代码

    这是动画创建的核心类,使用了属性动画,先由当前速度mVelocity->0,随后回弹slowdownEndOffset->mStartAttr.mAbsOffset,具体代码见下:


        private Animator createAnimator() {
    initAnimationAttributes(view, mAnimAttributes);

    // 速度为0了或手势记录的状态与mDir不符合,直接回弹
    if (mVelocity == 0f || (mVelocity < 0 && mStartAttr.mDir) || (mVelocity > 0 && !mStartAttr.mDir)) {
    return createBounceBackAnimator(mAnimAttributes.mAbsOffset);
    }

    // 速度减到0,即到达最大距离时,需要的动画事件
    float slowdownDuration = (0 - mVelocity) / mDecelerateFactor;
    slowdownDuration = (slowdownDuration < 0 ? 0 : slowdownDuration);

    // 速度减到0,动画的距离,dx = (Vt^2 - Vo^2) / 2a
    float slowdownDistance = -mVelocity * mVelocity / mDoubleDecelerateFactor;
    float slowdownEndOffset = mAnimAttributes.mAbsOffset + slowdownDistance;

    // 开始动画,减速->回弹
    ObjectAnimator slowdownAnim = createSlowdownAnimator(view, (int) slowdownDuration, slowdownEndOffset);
    ObjectAnimator bounceBackAnim = createBounceBackAnimator(slowdownEndOffset);
    AnimatorSet wholeAnim = new AnimatorSet();
    wholeAnim.playSequentially(slowdownAnim, bounceBackAnim);
    return wholeAnim;
    }
    复制代码

    这是具体的减速动画方法,设置时间和差值器,就不细说了,不是本文的重点,直接见代码吧:


        private ObjectAnimator createSlowdownAnimator(View view, int slowdownDuration, float slowdownEndOffset) {
    ObjectAnimator slowdownAnim = ObjectAnimator.ofFloat(view, mAnimAttributes.mProperty, slowdownEndOffset);
    slowdownAnim.setDuration(slowdownDuration);
    slowdownAnim.setInterpolator(mBounceBackInterpolator);
    slowdownAnim.addUpdateListener(this);
    return slowdownAnim;
    }
    复制代码

    同样这是回弹动画的方法,设置时间和差值器,添加监听等,代码见下:


        private ObjectAnimator createBounceBackAnimator(float startOffset) {
    float bounceBackDuration = (Math.abs(startOffset) / mAnimAttributes.mMaxOffset) * MAX_BOUNCE_BACK_DURATION_MS;
    ObjectAnimator bounceBackAnim = ObjectAnimator.ofFloat(view, mAnimAttributes.mProperty, mStartAttr.mAbsOffset);
    bounceBackAnim.setDuration(Math.max((int) bounceBackDuration, MIN_BOUNCE_BACK_DURATION_MS));
    bounceBackAnim.setInterpolator(mBounceBackInterpolator);
    bounceBackAnim.addUpdateListener(this);
    return bounceBackAnim;
    }
    复制代码

    当动画结束的时候,会将状态由回弹模式切换为初始状态,代码见下:


        @Override
    public void onAnimationEnd(Animator animation) {
    // 动画结束改变状态
    issueStateTransition(mIdleState);
    }
    复制代码

    好了,到这里核心逻辑就结束啦,应该不难理解吧。如果讲的不好,博客的栗子我都上传到了gitHub上,感兴趣的可以直接下载看下。



    本文到这里,关于回弹效果的实现就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。


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

    Android修炼系列(十一),强大的可拖拽工具类ViewDragHelper

    demo实现效果图见下,可自由拖拽的view,还在自己造轮子吗?使用系统androidx包(原v4)下的ViewDragHelper 几行代码即可搞定.. 实现 ViewDragHelper是用于编写自定义ViewGroup的工具类。它提供了许多有用...
    继续阅读 »

    demo实现效果图见下,可自由拖拽的view,还在自己造轮子吗?使用系统androidx包(原v4)下的ViewDragHelper 几行代码即可搞定..





    实现


    ViewDragHelper是用于编写自定义ViewGroup的工具类。它提供了许多有用的操作和状态跟踪,以允许用户在其父级ViewGroup中拖动和重新放置视图,具体可见 官网API。好,那我们就开始自定义一个简单的ViewGroup,并创建ViewDragHelper,代码见下:


    public class DragViewGroup extends RelativeLayout {

    ViewDragHelper mDragHelper;

    public DragViewGroup(Context context) {
    this(context, null);
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
    }
    ...
    }
    复制代码

    其中ViewDragCallback是我自己创建的内部类,继承自ViewDragHelper.Callback实现类。


    private static class ViewDragCallback extends ViewDragHelper.Callback {
    @Override
    public boolean tryCaptureView(@NonNull View child, int pointerId) {
    // 决定child是否可以被拖拽,具体见下文源码分析
    return true;
    }

    @Override
    public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
    // 可决定child横向的偏移计算,见下文
    return left;
    }

    @Override
    public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
    // 可决定child竖向的偏移计算,见下文
    return top;
    }
    }
    复制代码

    重写DragViewGroup的方法onInterceptHoverEvent和onTouchEvent方法:


    public class DragViewGroup extends RelativeLayout {
    ...
    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
    return mDragHelper.shouldInterceptTouchEvent(event);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
    mDragHelper.processTouchEvent(event);
    return true;
    }
    ...
    }
    复制代码

    这是我们的layout文件,其中DragViewGroup是我们上面定义的ViewGroup,TextView就是待拖拽的child view。


    <com.blog.a.drag.DragViewGroup
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <TextView
    android:layout_width="70dp"
    android:layout_height="70dp"
    android:text="可拖拽"
    android:gravity="center"
    android:textColor="#fff"
    android:background="#6495ED"
    />
    </com.blog.a.drag.DragViewGroup>
    复制代码

    是不是非常省事,博客的栗子我都上传到了gitHub上,感兴趣的可以下载看下。


    源码


    本篇文章主要分析下,当触摸事件开始到结束,processTouchEvent的处理过程:


        public boolean onTouchEvent(MotionEvent event) {
    mDragHelper.processTouchEvent(event);
    return true;
    }
    复制代码

    MotionEvent.ACTION_DOWN


    当手指刚接触屏幕时,会触发ACTION_DOWN 事件,通过MotionEvent我们能获取到点击事件发生的 x, y 坐标,注意这里的getX/getY的坐标是相对于当前view而言的。Pointer是触摸点的概念,一个MotionEvent可能会包含多个Pointer触摸点的信息,而每个Pointer触摸点都会有一个自己的id和index。具体往下看。


        case MotionEvent.ACTION_DOWN: {
    final float x = ev.getX();
    final float y = ev.getY();
    final int pointerId = ev.getPointerId(0);
    final View toCapture = findTopChildUnder((int) x, (int) y);

    saveInitialMotion(x, y, pointerId);

    tryCaptureViewForDrag(toCapture, pointerId);
    // mTrackingEdges默认是0,可通过ViewDragHelper#setEdgeTrackingEnabled(int)
    // 来设置,用来控制触碰边缘回调onEdgeTouched
    final int edgesTouched = mInitialEdgesTouched[pointerId];
    if ((edgesTouched & mTrackingEdges) != 0) {
    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
    }
    break;
    }
    复制代码

    这里的findTopChildUnder方法是用来获取当前x, y坐标点所在的view,默认是最上层的,当然我们也可以通过callback#getOrderedChildIndex(int) 接口来自定义view遍历顺序,代码见下:


        public View findTopChildUnder(int x, int y) {
    final int childCount = mParentView.getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
    final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
    if (x >= child.getLeft() && x < child.getRight()
    && y >= child.getTop() && y < child.getBottom()) {
    return child;
    }
    }
    return null;
    }
    复制代码

    这里的saveInitialMotion方法是用来保存当前触摸位置信息,其中getEdgesTouched方法用来判断x, y是否位于此viewGroup边缘之外,并返回保存相应result结果。todo:下篇准备写一下关于位运算符的文章,很有意思。


        private void saveInitialMotion(float x, float y, int pointerId) {
    ensureMotionHistorySizeForId(pointerId);
    mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
    mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
    mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
    mPointersDown |= 1 << pointerId;
    }
    复制代码

    其中tryCaptureViewForDrag方法内,mCapturedView是当前触摸的视图view,如果相同则直接返回,否则会进行mCallback#tryCaptureView(View, int)判断,这个是不是很眼熟,我们可以重写这个回调来控制toCapture这个view能否被捕获,即能否被拖拽操作。


        boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
    if (toCapture == mCapturedView && mActivePointerId == pointerId) {
    // Already done!
    return true;
    }
    if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
    mActivePointerId = pointerId;
    captureChildView(toCapture, pointerId);
    return true;
    }
    return false;
    }
    复制代码

    这里的captureChildView方法用来保存信息,并设置拖拽状态。能注意到,这里还有个捕获view是否是child view的判断。


        public void captureChildView(@NonNull View childView, int activePointerId) {
    if (childView.getParent() != mParentView) {
    throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
    + "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
    }

    mCapturedView = childView;
    mActivePointerId = activePointerId;
    mCallback.onViewCaptured(childView, activePointerId);
    setDragState(STATE_DRAGGING);
    }
    复制代码

    MotionEvent.ACTION_POINTER_DOWN


    当用户又使用一个手指接触屏幕时,会触发ACTION_POINTER_DOWN 事件,与上面的ACTION_DOWN 相似,就不细展开了。由于ViewDragHelper一次只能操作一个视图,所以这里会先进行状态判断,如果视图还未被捕获拖动,则逻辑与上面的ACTION_POINTER_DOWN一致,反之,会判断触摸点是否在当前视图内,如果符合条件,则更新Pointer,这里很重要,体现在ui效果上就是,一个手指按住view,另一个手指仍然可以拖拽此view。


        case MotionEvent.ACTION_POINTER_DOWN: {
    final int pointerId = ev.getPointerId(actionIndex);
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);

    saveInitialMotion(x, y, pointerId);
    // A ViewDragHelper can only manipulate one view at a time.
    if (mDragState == STATE_IDLE) {
    // If we're idle we can do anything! Treat it like a normal down event.
    final View toCapture = findTopChildUnder((int) x, (int) y);
    tryCaptureViewForDrag(toCapture, pointerId);

    final int edgesTouched = mInitialEdgesTouched[pointerId];
    if ((edgesTouched & mTrackingEdges) != 0) {
    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
    }
    } else if (isCapturedViewUnder((int) x, (int) y)) {
    tryCaptureViewForDrag(mCapturedView, pointerId);
    }
    break;
    }
    复制代码

    MotionEvent.ACTION_MOVE


    当手指在屏幕移动时,如果视图正在被拖动,则会先判断当前mActivePointerId是否有效,无效则跳过当前move事件。随后获取当前x, y并计算与上次x, y移动距离。之后触发dragTo拖动逻辑,最后保存保存这次的位置。核心方法dragTo分析见下文:


        case MotionEvent.ACTION_MOVE: {
    if (mDragState == STATE_DRAGGING) {
    // If pointer is invalid then skip the ACTION_MOVE.
    if (!isValidPointerForActionMove(mActivePointerId)) break;

    final int index = ev.findPointerIndex(mActivePointerId);
    final float x = ev.getX(index);
    final float y = ev.getY(index);
    final int idx = (int) (x - mLastMotionX[mActivePointerId]);
    final int idy = (int) (y - mLastMotionY[mActivePointerId]);

    dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

    saveLastMotion(ev);
    } else {
    // Check to see if any pointer is now over a draggable view.
    ...
    }
    break;
    }
    复制代码

    在move过程中,通过dragTo方法来传入目标x, y 和横向和竖向的偏移量,并通过callback回调来通知开发者,开发者可重写clampViewPositionHorizontal与clampViewPositionVertical这两个回调方法,来自定义clampedX,clampedY目标位置。随后使用offsetLeftAndRight和offsetTopAndBottom 方法分别在相应的方向偏移(clampedX - oldLeft)和(clampedY - oldTo)的像素。最后触发onViewPositionChanged位置修改的回调。


        private void dragTo(int left, int top, int dx, int dy) {
    int clampedX = left;
    int clampedY = top;
    final int oldLeft = mCapturedView.getLeft();
    final int oldTop = mCapturedView.getTop();
    if (dx != 0) {
    clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
    ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
    }
    if (dy != 0) {
    clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
    ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
    }

    if (dx != 0 || dy != 0) {
    final int clampedDx = clampedX - oldLeft;
    final int clampedDy = clampedY - oldTop;
    mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
    clampedDx, clampedDy);
    }
    }
    复制代码

    如果当手指在屏幕移动时,发现视图未处于拖动状态呢?首先会去检查是否有其他Pointer是否有效。随后触发边缘拖动回调,随后再进行状态检查,应该是为了避免此时状态由未拖动->拖动状态了,如:smoothSlideViewTo方法就有这个能力。如果此时mDragState处于未拖动状态,则会重新获取x,y 所在视图view并重新设置拖拽状态,这个逻辑与down逻辑一样。


        case MotionEvent.ACTION_MOVE: {
    if (mDragState == STATE_DRAGGING) {
    // If pointer is invalid then skip the ACTION_MOVE.
    ...
    } else {
    // Check to see if any pointer is now over a draggable view.
    final int pointerCount = ev.getPointerCount();
    for (int i = 0; i < pointerCount; i++) {
    final int pointerId = ev.getPointerId(i);

    // If pointer is invalid then skip the ACTION_MOVE.
    if (!isValidPointerForActionMove(pointerId)) continue;

    final float x = ev.getX(i);
    final float y = ev.getY(i);
    final float dx = x - mInitialMotionX[pointerId];
    final float dy = y - mInitialMotionY[pointerId];

    reportNewEdgeDrags(dx, dy, pointerId);
    if (mDragState == STATE_DRAGGING) {
    // Callback might have started an edge drag.
    break;
    }

    final View toCapture = findTopChildUnder((int) x, (int) y);
    if (checkTouchSlop(toCapture, dx, dy)
    && tryCaptureViewForDrag(toCapture, pointerId)) {
    break;
    }
    }
    saveLastMotion(ev);
    }
    break;
    }
    复制代码

    MotionEvent.ACTION_POINTER_UP


    当处于多触摸点时,当一手指从屏幕上松开时,首先判断正在拖动视图的触摸点是否是当前触摸点,如果是,则再去检查视图上是否还有其他有效的触摸点,如果没有则释放,此时view就惯性停住了。如果还有,则清理当前up掉的触摸点数据。


        case MotionEvent.ACTION_POINTER_UP: {
    final int pointerId = ev.getPointerId(actionIndex);
    // 判断当前触摸点是否是正在拖动视图的触摸点
    if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
    // 检查是否有其他有效触摸点
    int newActivePointer = INVALID_POINTER;
    final int pointerCount = ev.getPointerCount();
    // 遍历ev内触摸点
    for (int i = 0; i < pointerCount; i++) {
    final int id = ev.getPointerId(i);
    if (id == mActivePointerId) {
    // This one's going away, skip.
    continue;
    }

    final float x = ev.getX(i);
    final float y = ev.getY(i);
    // 如果在视图上,并且可拖动,则标记找到了
    if (findTopChildUnder((int) x, (int) y) == mCapturedView
    && tryCaptureViewForDrag(mCapturedView, id)) {
    newActivePointer = mActivePointerId;
    break;
    }
    }

    if (newActivePointer == INVALID_POINTER) {
    // 如果没有发现其他触摸点在拖拽视图view,则释放掉就可以了
    releaseViewForPointerUp();
    }
    }
    // 清理当前up掉的触摸点数据
    clearMotionHistory(pointerId);
    break;
    }
    复制代码

    MotionEvent.ACTION_UP


    当手指从屏幕上离开时,会先判断当前状态,如果此时mDragState处于拖动状态,则释放,view惯性停住。通过cancel方法改变状态,清空当前触摸点数据并接触速度检测mVelocityTracker。


        case MotionEvent.ACTION_UP: {
    if (mDragState == STATE_DRAGGING) {
    releaseViewForPointerUp();
    }
    cancel();
    break;
    }
    复制代码

    好了,本文到这里,关于ViewDrafHHelper的介绍就结束了。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。


    作者:Battler
    链接:https://juejin.cn/post/6952062595767468039
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    Android修炼系列(十),事件分发从手写一个嵌套滑动框架开始

    先放了一张效果图,是一个嵌套滑动的效果。博客的栗子我都上传到了gitHub上,感兴趣的可以下载看下。 在说代码之前,可以先看下最终的NestedViewGroup XML结构,NestedViewGroup内部包含顶部地图 MapView和滑动布局L...
    继续阅读 »

    先放了一张效果图,是一个嵌套滑动的效果。博客的栗子我都上传到了gitHub上,感兴趣的可以下载看下。





    在说代码之前,可以先看下最终的NestedViewGroup XML结构,NestedViewGroup内部包含顶部地图 MapView和滑动布局LinearLayout,而LinearLayout布局的内部即我们常用的滑动控件 RecyclerView,在这里为何还要加层LinearLayout呢?这样做的好处是,我们可以更好的适配不同滑动控件,而不仅仅是将NestedViewGroup与RecyclerView 耦合住。


        <com.blog.a.nested.NestedViewGroup
    android:id="@+id/dd_view_group"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    didi:header_id="@+id/t_map_view"
    didi:target_id="@+id/target_layout"
    didi:inn_id="@+id/inner_rv"
    didi:header_init_top="0"
    didi:target_init_bottom="250">

    <com.tencent.tencentmap.mapsdk.maps.MapView
    android:id="@+id/t_map_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    <LinearLayout
    android:id="@+id/target_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#fff">

    <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/inner_rv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

    </LinearLayout>

    </com.mjzuo.views.nested.NestedViewGroup>
    复制代码

    实现


    在attrs.xml文件下为NestedViewGroup添加自定义属性,其中header_id对应顶部地图 MapView,target_id对应滑动布局LinearLayout,inn_id对应滑动控件RecyclerView。


    <resources>
    <declare-styleable name="CompNsViewGroup">
    <attr name="header_id"/>
    <attr name="target_id"/>
    <attr name="inn_id"/>
    <attr name="header_init_top" format="integer"/>
    <attr name="target_init_bottom" format="integer"/>
    </declare-styleable>
    </resources>
    复制代码

    我们根据attrs.xml中的属性,获取XML中NestedViewGroup中的View ID。


            // 获取配置参数
    final TypedArray array = context.getTheme().obtainStyledAttributes(attrs
    , R.styleable.CompNsViewGroup
    , defStyleAttr, 0);
    mHeaderResId = array.getResourceId
    (R.styleable.CompNsViewGroup_header_id, -1);
    mTargetResId = array.getResourceId
    (R.styleable.CompNsViewGroup_target_id, -1);
    mInnerScrollId = array.getResourceId
    (R.styleable.CompNsViewGroup_inn_id, -1);
    if (mHeaderResId == -1 || mTargetResId == -1
    || mInnerScrollId == -1)
    throw new RuntimeException("VIEW ID is null");
    复制代码

    我们根据attrs.xml中的属性,来初始化View的高度、距离等,计算高度时,需要考虑到状态栏因素:


            mHeaderInitTop = Utils.dip2px(getContext()
    , array.getInt(R.styleable.CompNsViewGroup_header_init_top, 0));
    mHeaderCurrTop = mHeaderInitTop;
    // 屏幕高度 - 底部距离 - 状态栏高度
    mTargetInitBottom = Utils.dip2px(getContext()
    , array.getInt(R.styleable.CompNsViewGroup_target_init_bottom, 0));
    // 注意:当前activity默认去掉了标题栏
    mTargetInitTop = Utils.getScreenHeight(getContext()) - mTargetInitBottom
    - Utils.getStatusBarHeight(getContext().getApplicationContext());
    mTargetCurrTop = mTargetInitTop;
    复制代码

    通过上面获取到的View ID,我们能够直接引用到XML中的相关View实例,而后续的滑动,本质上就是针对该View所进行的一系列判断处理。


        @Override
    protected void onFinishInflate() {
    super.onFinishInflate();
    mHeaderView = findViewById(mHeaderResId);
    mTargetView = findViewById(mTargetResId);
    mInnerScrollView = findViewById(mInnerScrollId);
    }
    复制代码

    我们重写onMeasure方法,其不仅是给childView传入测量值和测量模式,还将我们自己测量的尺寸提供给父ViewGroup让其给我们提供期望大小的区域。


        @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int widthModle = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightModle = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    ....

    setMeasuredDimension(widthSize, heightSize);
    }
    复制代码

    我们重写onLayout方法,给childView确定位置。需要注意的是,原始bottom不是height高度,而是又向下挪了mTargetInitTop,我们可以想象成,我们一直将mTargetView挪动到了屏幕下方看不到的地方。


        @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int childCount = getChildCount();
    if (childCount == 0)
    return;
    final int width = getMeasuredWidth();
    final int height = getMeasuredHeight();

    // 注意:原始bottom不是height高度,而是又向下挪了mTargetInitTop
    mTargetView.layout(getPaddingLeft()
    , getPaddingTop() + mTargetCurrTop
    , width - getPaddingRight()
    , height + mTargetCurrTop
    + getPaddingTop() + getPaddingBottom());

    int headerWidth = mHeaderView.getMeasuredWidth();
    int headerHeight = mHeaderView.getMeasuredHeight();
    mHeaderView.layout((width - headerWidth)/2
    , mHeaderCurrTop + getPaddingTop()
    , (width + headerWidth)/2
    , headerHeight + mHeaderCurrTop + getPaddingTop());
    }
    复制代码

    此功能实现的核心即事件的分发和拦截了。在接收到事件时,如果上次滚动还未结束,则先停下。随后判断TargetView内的RecyclerView能否向下滑动,如果还能滑动,则不拦截事件,将事件传递给 TargetView。如果点击在Header区域,则不拦截事件,将事件传递给地图MapView。


        @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

    // 如果上次滚动还未结束,则先停下
    if (!mScroller.isFinished())
    mScroller.forceFinished(true);

    // 不拦截事件,将事件传递给TargetView
    if (canChildScrollDown())
    return false;

    int action = event.getAction();

    switch (action) {
    case MotionEvent.ACTION_DOWN:
    mDownY = event.getY();
    mIsDragging = false;
    // 如果点击在Header区域,则不拦截事件
    isDownInTop = mDownY <= mTargetCurrTop - mTouchSlop;
    break;

    case MotionEvent.ACTION_MOVE:
    final float y = event.getY();
    if (isDownInTop) {
    return false;
    } else {
    startDragging(y);
    }

    break;

    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
    mIsDragging = false;
    break;
    }

    return mIsDragging;
    }
    复制代码

    当NestedViewGroup拦截事件后,会调用自身的onTouchEvent方法,逻辑与 onInterceptTouchEvent 类似,这里需要注意的是,当事件在ViewGroup内,我们要怎么手动分发给TargetView呢?代码见下:


        @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (canChildScrollDown())
    return false;

    // 添加速度监听
    acquireVelocityTracker(event);
    int action = event.getAction();

    switch (action) {
    case MotionEvent.ACTION_DOWN:
    mIsDragging = false;
    break;

    case MotionEvent.ACTION_MOVE:
    ...
    break;

    case MotionEvent.ACTION_UP:
    if (mIsDragging) {
    mIsDragging = false;
    mVelocityTracker.computeCurrentVelocity(500, maxFlingVelocity);
    final float vy = mVelocityTracker.getYVelocity();
    // 滚动的像素数太大了,这里只滚动像素数的0.1
    vyPxCount = (int)(vy/3);
    finishDrag(vyPxCount);
    }
    releaseVelocityTracker();
    return false;

    case MotionEvent.ACTION_CANCEL:
    // 回收滑动监听
    releaseVelocityTracker();
    return false;
    }

    return mIsDragging;
    }
    复制代码

    这是我们手指移动ACTION_MOVE 时的逻辑:


        final float y = event.getY();
    startDragging(y);

    if (mIsDragging) {
    float dy = y - mLastMotionY;
    if (dy >= 0) {
    moveTargetView(dy);
    } else if (mTargetCurrTop + dy <= 0) {
    /**
    * 此时,事件在ViewGroup内,
    * 需手动分发给TargetView
    */
    moveTargetView(dy);
    int oldAction = event.getAction();
    event.setAction(MotionEvent.ACTION_DOWN);
    dispatchTouchEvent(event);
    event.setAction(oldAction);
    } else {
    moveTargetView(dy);
    }
    mLastMotionY = y;
    }
    复制代码

    通过canChildScrollDown方法,我们能够判断RecyclerView是否能够向下滑动。这里后续会抽出一个adapter类,来处理不同的滑动控件。这里通过canScrollVertically来判断当前视图是否可以继续滚动,其中正数表示实际是判断手指能否向上滑动,负数表示实际是判断手指能否向下滑动:


        public boolean canChildScrollDown() {
    RecyclerView rv;
    // 当前只做了RecyclerView的适配
    if (mInnerScrollView instanceof RecyclerView) {
    rv = (RecyclerView) mInnerScrollView;
    return rv.canScrollVertically(-1);
    }
    return false;
    }
    复制代码

    获取向上能够滑动的距离顶部距离,如果Item数量太少,导致rv不能占满一屏时,注意向上滑动的距离。


        public int toTopMaxOffset() {
    final RecyclerView rv;
    if (mInnerScrollView instanceof RecyclerView) {
    rv = (RecyclerView) mInnerScrollView;
    if (android.os.Build.VERSION.SDK_INT >= 18) {

    return Math.max(0, mTargetInitTop -
    (rv.computeVerticalScrollRange() - mTargetInitBottom));
    }
    }
    return 0;
    }
    复制代码

    手指向下滑动或TargetView距离顶部距离 > 0,则ViewGroup拦截事件。


        private void startDragging(float y) {
    if (y > mDownY || mTargetCurrTop > toTopMaxOffset()) {
    final float yDiff = Math.abs(y - mDownY);
    if (yDiff > mTouchSlop && !mIsDragging) {
    mLastMotionY = mDownY + mTouchSlop;
    mIsDragging = true;
    }
    }
    }
    复制代码

    这是获取TargetView和HeaderView顶部距离的方法,我们通过不断刷新顶部距离来实现滑动的效果,并在这里添加距离监听。


        private void moveTargetViewTo(int target) {
    target = Math.max(target, toTopMaxOffset());
    if (target >= mTargetInitTop)
    target = mTargetInitTop;
    // TargetView的top、bottom两个方向都是加上offsetY
    ViewCompat.offsetTopAndBottom(mTargetView, target - mTargetCurrTop);
    // 更新当前TargetView距离顶部高度H
    mTargetCurrTop = target;

    int headerTarget;
    // 下拉超过定值H
    if (mTargetCurrTop >= mTargetInitTop) {
    headerTarget = mHeaderInitTop;
    } else if (mTargetCurrTop <= 0) {
    headerTarget = 0;
    } else {
    // 滑动比例
    float percent = mTargetCurrTop * 1.0f / mTargetInitTop;
    headerTarget = (int) (percent * mHeaderInitTop);
    }
    // HeaderView的top、bottom两个方向都是加上offsetY
    ViewCompat.offsetTopAndBottom(mHeaderView, headerTarget - mHeaderCurrTop);
    mHeaderCurrTop = headerTarget;

    if (mListener != null) {
    mListener.onTargetToTopDistance(mTargetCurrTop);
    mListener.onHeaderToTopDistance(mHeaderCurrTop);
    }
    }
    复制代码

    这是mScroller弹性滑动时的一些阈值判断。startScroll本身并没有做任何滑动相关的事,而是通过invalidate方法来实现View重绘,在View的draw方法中会调用computeScroll方法,但本例中并没有在computeScroll中配合scrollTo来实现滑动。注意这里的滑动,是指内容的滑动,而非View本身位置的滑动。


        private void finishDrag(int vyPxCount) {
    if ((vyPxCount >= 0 && vyPxCount <= minFlingVelocity)
    || (vyPxCount <= 0 && vyPxCount >= -minFlingVelocity))
    return;


    if (vyPxCount > 0) {
    // 速度 > 0,说明正向下滚动
    // 防止超出临界值
    if (mTargetCurrTop < mTargetInitTop) {
    mScroller.startScroll(0, mTargetCurrTop, 0,
    Math.min(vyPxCount, mTargetInitTop - mTargetCurrTop)
    , 500);
    invalidate();
    }
    } else if (vyPxCount < 0) {
    // 速度 < 0,说明正向上滚动

    if (mTargetCurrTop <= 0 && mScroller.getCurrVelocity() > 0) {
    // todo: inner scroll 接着滚动
    }

    mScroller.startScroll(0, mTargetCurrTop
    , 0, Math.max(vyPxCount, -mTargetCurrTop)
    , 500);
    invalidate();
    }
    }
    复制代码

    在View重绘后,computeScroll方法就会被调用,这里通过更新此时TargetView和HeaderView的顶部距离,来实现滑动到新的位置的目的。


        @Override
    public void computeScroll() {
    // 判断是否完成滚动,true:未结束
    if (mScroller.computeScrollOffset()) {
    moveTargetViewTo(mScroller.getCurrY());
    invalidate();
    }
    }
    复制代码


    好了,本文到这里,关于嵌套滑动的demo就结束了,当然可优化的点还很多。如果本文对你有用,来点个赞吧,大家的肯定也是阿呆i坚持写作的动力。



    作者:Battler
    链接:https://juejin.cn/post/6951069336412880933
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »

    CocoaAsyncSocket源码Write(总结篇 二)

    if (hasNewDataToWrite) { //拿到buffer偏移位置 const uint8_t *buffer = (const uint8_t *)[curr...
    继续阅读 »
    • 下面是写入的三种方式

      • CFStreamForTLS


    • SSL写的方式


    if (hasNewDataToWrite)
    {
    //拿到buffer偏移位置
    const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes]
    + currentWrite->bytesDone
    + bytesWritten;

    //得到需要读的长度
    NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten;
    //如果大于最大值,就等于最大值
    if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
    {
    bytesToWrite = SIZE_MAX;
    }

    size_t bytesRemaining = bytesToWrite;

    //循环值
    BOOL keepLooping = YES;
    while (keepLooping)
    {
    //最大写的字节数?
    const size_t sslMaxBytesToWrite = 32768;
    //得到二者小的,得到需要写的字节数
    size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite);
    //已写字节数
    size_t sslBytesWritten = 0;

    //将结果从buffer中写到socket上(经由了这个函数,数据就加密了)
    result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten);

    //如果写成功
    if (result == noErr)
    {
    //buffer指针偏移
    buffer += sslBytesWritten;
    //加上些的数量
    bytesWritten += sslBytesWritten;
    //减去仍需写的数量
    bytesRemaining -= sslBytesWritten;
    //判断是否需要继续循环
    keepLooping = (bytesRemaining > 0);
    }
    else
    {
    //IO阻塞
    if (result == errSSLWouldBlock)
    {
    waiting = YES;
    //得到缓存的大小(后续长度会被自己写到SSL缓存去)
    sslWriteCachedLength = sslBytesToWrite;
    }
    else
    {
    error = [self sslError:result];
    }

    //跳出循环
    keepLooping = NO;
    }

    } // while (keepLooping)


    这里还有对残余数据的处理:是通过指针buffer获取我们的keepLooping循环值,循环进行写入

     //将结果从buffer中写到socket上(经由了这个函数,数据就加密了)
    result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten);
    • 普通socket写入



    • 也做了完成判断
    //判断是否完成
    BOOL done = NO;
    //判断已写大小
    if (bytesWritten > 0)
    {
    // Update total amount read for the current write
    //更新当前总共写的大小
    currentWrite->bytesDone += bytesWritten;
    LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone);

    // Is packet done?
    //判断当前写包是否写完
    done = (currentWrite->bytesDone == [currentWrite->buffer length]);
    }

    同样为的也是三种数据包:一次性包,粘包,断包

      //如果完成了
    if (done)
    {
    //完成操作
    [self completeCurrentWrite];

    if (!error)
    {
    dispatch_async(socketQueue, ^{ @autoreleasepool{
    //开始下一次的读取任务
    [self maybeDequeueWrite];
    }});
    }
    }
    //未完成
    else
    {
    // We were unable to finish writing the data,
    // so we're waiting for another callback to notify us of available space in the lower-level output buffer.
    //如果不是等待 而且没有出错
    if (!waiting && !error)
    {
    // This would be the case if our write was able to accept some data, but not all of it.
    //这是我们写了一部分数据的情况。

    //去掉可接受数据的标记
    flags &= ~kSocketCanAcceptBytes;
    //再去等读source触发
    if (![self usingCFStreamForTLS])
    {
    [self resumeWriteSource];
    }
    }

    //如果已写大于0
    if (bytesWritten > 0)
    {
    // We're not done with the entire write, but we have written some bytes

    __strong id theDelegate = delegate;

    //调用写的进度代理
    if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)])
    {
    long theWriteTag = currentWrite->tag;

    dispatch_async(delegateQueue, ^{ @autoreleasepool {

    [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag];
    }});
    }
    }
    }



    那么整个 CocoaAsyncSocket Wirte的解析就到这里完成了,当你读完前面几篇,再来看这篇就跟喝水一样,故:知识在于积累


    由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度


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


    收起阅读 »

    CocoaAsyncSocket源码Write(总结篇)

    我们切入口//写数据对外方法 - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { if ([data length] == 0) re...
    继续阅读 »



    我们切入口

    //写数据对外方法
    - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag
    {
    if ([data length] == 0) return;

    //初始化写包
    GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag];

    dispatch_async(socketQueue, ^{ @autoreleasepool {

    LogTrace();

    if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
    {
    [writeQueue addObject:packet];
    //离队执行
    [self maybeDequeueWrite];
    }
    }});

    // Do not rely on the block being run in order to release the packet,
    // as the queue might get released without the block completing.
    }



    写法类似Read

    • 初始化写包 :GCDAsyncWritePacket
    • 写入包放入我们的写入队列(数组)[writeQueue addObject:packet];
    • 离队执行 [self maybeDequeueWrite];

    写入包,添加队列没什么讲的了,

    下面重点解析maybeDequeueWrite

    - (void)maybeDequeueWrite
    {
    LogTrace();
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");


    // If we're not currently processing a write AND we have an available write stream
    if ((currentWrite == nil) && (flags & kConnected))
    {
    if ([writeQueue count] > 0)
    {
    // Dequeue the next object in the write queue
    currentWrite = [writeQueue objectAtIndex:0];
    [writeQueue removeObjectAtIndex:0];

    //TLS
    if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]])
    {
    LogVerbose(@"Dequeued GCDAsyncSpecialPacket");

    // Attempt to start TLS
    flags |= kStartingWriteTLS;

    // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
    [self maybeStartTLS];
    }
    else
    {
    LogVerbose(@"Dequeued GCDAsyncWritePacket");

    // Setup write timer (if needed)
    [self setupWriteTimerWithTimeout:currentWrite->timeout];

    // Immediately write, if possible
    [self doWriteData];
    }
    }
    //写超时导致的错误
    else if (flags & kDisconnectAfterWrites)
    {
    //如果没有可读任务,直接关闭socket
    if (flags & kDisconnectAfterReads)
    {
    if (([readQueue count] == 0) && (currentRead == nil))
    {
    [self closeWithError:nil];
    }
    }
    else
    {
    [self closeWithError:nil];
    }
    }
    }
    }
    • 我们首先做了一些是否连接,写入队列任务是否大于0等等一些判断
    • 接着我们从全局的writeQueue中,拿到第一条任务,去做读取,我们来判断这个任务的类型,如果是GCDAsyncSpecialPacket类型的,我们将开启TLS认证
    • 如果是是我们之前加入队列中的GCDAsyncWritePacket类型,我们则开始读取操作,调用doWriteData
    • 如果没有可读任务,直接关闭socket

    其中 maybeStartTLS我们解析过了,我们就只要来看看核心写入方法:doWriteData

    - (void)doWriteData
    {
    LogTrace();

    // This method is called by the writeSource via the socketQueue

    //错误,不写
    if ((currentWrite == nil) || (flags & kWritesPaused))
    {
    LogVerbose(@"No currentWrite or kWritesPaused");

    // Unable to write at this time

    //
    if ([self usingCFStreamForTLS])
    {
    // CFWriteStream only fires once when there is available data.
    // It won't fire again until we've invoked CFWriteStreamWrite.
    }
    else
    {
    // If the writeSource is firing, we need to pause it
    // or else it will continue to fire over and over again.

    //如果socket中可接受写数据,防止反复触发写source,挂起
    if (flags & kSocketCanAcceptBytes)
    {
    [self suspendWriteSource];
    }
    }
    return;
    }

    //如果当前socket无法在写数据了
    if (!(flags & kSocketCanAcceptBytes))
    {
    LogVerbose(@"No space available to write...");

    // No space available to write.

    //如果不是cfstream
    if (![self usingCFStreamForTLS])
    {
    // Need to wait for writeSource to fire and notify us of
    // available space in the socket's internal write buffer.
    //则恢复写source,当有空间去写的时候,会触发回来
    [self resumeWriteSource];
    }
    return;
    }

    //如果正在进行TLS认证
    if (flags & kStartingWriteTLS)
    {
    LogVerbose(@"Waiting for SSL/TLS handshake to complete");

    // The writeQueue is waiting for SSL/TLS handshake to complete.

    if (flags & kStartingReadTLS)
    {
    //如果是安全通道,并且I/O阻塞,那么重新去握手
    if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock)
    {
    // We are in the process of a SSL Handshake.
    // We were waiting for available space in the socket's internal OS buffer to continue writing.

    [self ssl_continueSSLHandshake];
    }
    }
    //说明不走`TLS`了,因为只支持写的TLS
    else
    {
    // We are still waiting for the readQueue to drain and start the SSL/TLS process.
    // We now know we can write to the socket.

    //挂起写source
    if (![self usingCFStreamForTLS])
    {
    // Suspend the write source or else it will continue to fire nonstop.
    [self suspendWriteSource];
    }
    }

    return;
    }

    // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet)

    //开始写数据

    BOOL waiting = NO;
    NSError *error = nil;
    size_t bytesWritten = 0;

    //安全连接
    if (flags & kSocketSecure)
    {
    //CFStreamForTLS
    if ([self usingCFStreamForTLS])
    {
    #if TARGET_OS_IPHONE

    //
    // Writing data using CFStream (over internal TLS)
    //

    const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;

    //写的长度为buffer长度-已写长度
    NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;

    if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
    {
    bytesToWrite = SIZE_MAX;
    }
    //往writeStream中写入数据, bytesToWrite写入的长度
    CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite);
    LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result);

    //写错误
    if (result < 0)
    {
    error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream);
    }
    else
    {
    //拿到已写字节数
    bytesWritten = (size_t)result;

    // We always set waiting to true in this scenario.
    //我们经常设置等待来信任这个方案
    // CFStream may have altered our underlying socket to non-blocking.
    //CFStream很可能修改socket为非阻塞
    // Thus if we attempt to write without a callback, we may end up blocking our queue.
    //因此,我们尝试去写,而不用回调。 我们可能终止我们的队列。
    waiting = YES;
    }

    #endif
    }
    //SSL写的方式
    else
    {
    // We're going to use the SSLWrite function.
    //
    // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed)
    //
    // Parameters:
    // context - An SSL session context reference.
    // data - A pointer to the buffer of data to write.
    // dataLength - The amount, in bytes, of data to write.
    // processed - On return, the length, in bytes, of the data actually written.
    //
    // It sounds pretty straight-forward,
    //看起来相当直观,但是这里警告你应注意。
    // but there are a few caveats you should be aware of.
    //
    // The SSLWrite method operates in a non-obvious (and rather annoying) manner.
    // According to the documentation:
    // 这个SSLWrite方法使用着一个不明显的方法(相当讨厌)导致了下面这些事。
    // Because you may configure the underlying connection to operate in a non-blocking manner,
    //因为你要辨别出下层连接 操纵 非阻塞的方法,一个写的操作将返回errSSLWouldBlock,表明需要写的数据少了。
    // a write operation might return errSSLWouldBlock, indicating that less data than requested
    // was actually transferred. In this case, you should repeat the call to SSLWrite until some
    //在这种情况下你应该重复调用SSLWrite,直到一些其他结果被返回
    // other result is returned.
    // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock,
    //这样听起来很完美,但是当SSLWriteFunction返回errSSLWouldBlock,SSLWrite返回但是却设置了进度长度?
    // then the SSLWrite method returns (with the proper errSSLWouldBlock return value),
    // but it sets processed to dataLength !!
    //
    // In other words, if the SSLWrite function doesn't completely write all the data we tell it to,
    //另外,SSLWrite方法没有完整的写完我们给的所有数据,因此它没有告诉我们到底写了多少数据,
    // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to
    //因此。举个例子,如果我们告诉它去写256个字节,它可能只写了128个字节,但是告诉我们写了0个字节
    // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written.
    //
    // You might be wondering:
    //你可能会觉得奇怪,如果这个方法不告诉我们写了多少字节,那么该如何去更新参数来应对下一次的SSLWrite?
    // If the SSLWrite function doesn't tell us how many bytes were written,
    // then how in the world are we supposed to update our parameters (buffer & bytesToWrite)
    // for the next time we invoke SSLWrite?
    //
    // The answer is that SSLWrite cached all the data we told it to write,
    //答案就是,SSLWrite缓存了所有的数据我们要它写的。并且拉出这些数据,只要我们下次调用SSLWrite。
    // and it will push out that data next time we call SSLWrite.

    // If we call SSLWrite with new data, it will push out the cached data first, and then the new data.
    //如果我们用新的data调用SSLWrite,它会拉出这些缓存的数据,然后才轮到新数据
    // If we call SSLWrite with empty data, then it will simply push out the cached data.
    // 如果我们调用SSLWrite用一个空的数据,则它仅仅会拉出缓存数据。
    // For this purpose we're going to break large writes into a series of smaller writes.
    //为了这个目的,我们去分开一个大数据写成一连串的小数据,它允许我们去报告进度给代理。
    // This allows us to report progress back to the delegate.

    OSStatus result;

    //SSL缓存的写的数据
    BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0);
    //是否有新数据要写
    BOOL hasNewDataToWrite = YES;

    if (hasCachedDataToWrite)
    {
    size_t processed = 0;

    //去写空指针,就是拉取了所有的缓存SSL数据
    result = SSLWrite(sslContext, NULL, 0, &processed);

    //如果写成功
    if (result == noErr)
    {
    //拿到写的缓存长度
    bytesWritten = sslWriteCachedLength;
    //置空缓存长度
    sslWriteCachedLength = 0;
    //判断当前需要写的buffer长度,是否和已写的大小+缓存 大小相等
    if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten))
    {
    // We've written all data for the current write.
    //相同则不需要再写新数据了
    hasNewDataToWrite = NO;
    }
    }
    //有错
    else
    {
    //IO阻塞,等待
    if (result == errSSLWouldBlock)
    {
    waiting = YES;
    }
    //报错
    else
    {
    error = [self sslError:result];
    }

    // Can't write any new data since we were unable to write the cached data.
    //如果读写cache出错,我们暂时不能去读后面的数据
    hasNewDataToWrite = NO;
    }
    }

    //如果还有数据去读
    if (hasNewDataToWrite)
    {
    //拿到buffer偏移位置
    const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes]
    + currentWrite->bytesDone
    + bytesWritten;

    //得到需要读的长度
    NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten;
    //如果大于最大值,就等于最大值
    if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
    {
    bytesToWrite = SIZE_MAX;
    }

    size_t bytesRemaining = bytesToWrite;

    //循环值
    BOOL keepLooping = YES;
    while (keepLooping)
    {
    //最大写的字节数?
    const size_t sslMaxBytesToWrite = 32768;
    //得到二者小的,得到需要写的字节数
    size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite);
    //已写字节数
    size_t sslBytesWritten = 0;

    //将结果从buffer中写到socket上(经由了这个函数,数据就加密了)
    result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten);

    //如果写成功
    if (result == noErr)
    {
    //buffer指针偏移
    buffer += sslBytesWritten;
    //加上些的数量
    bytesWritten += sslBytesWritten;
    //减去仍需写的数量
    bytesRemaining -= sslBytesWritten;
    //判断是否需要继续循环
    keepLooping = (bytesRemaining > 0);
    }
    else
    {
    //IO阻塞
    if (result == errSSLWouldBlock)
    {
    waiting = YES;
    //得到缓存的大小(后续长度会被自己写到SSL缓存去)
    sslWriteCachedLength = sslBytesToWrite;
    }
    else
    {
    error = [self sslError:result];
    }

    //跳出循环
    keepLooping = NO;
    }

    } // while (keepLooping)

    } // if (hasNewDataToWrite)
    }
    }

    //普通socket
    else
    {
    //
    // Writing data directly over raw socket
    //

    //拿到当前socket
    int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;

    //得到指针偏移
    const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone;

    NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone;

    if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3)
    {
    bytesToWrite = SIZE_MAX;
    }
    //直接写
    ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite);
    LogVerbose(@"wrote to socket = %zd", result);

    // Check results
    if (result < 0)
    {
    //IO阻塞
    if (errno == EWOULDBLOCK)
    {
    waiting = YES;
    }
    else
    {
    error = [self errnoErrorWithReason:@"Error in write() function"];
    }
    }
    else
    {
    //得到写的大小
    bytesWritten = result;
    }
    }

    // We're done with our writing.
    // If we explictly ran into a situation where the socket told us there was no room in the buffer,
    // then we immediately resume listening for notifications.
    //
    // We must do this before we dequeue another write,
    // as that may in turn invoke this method again.
    //
    // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode.
    //注意,如果用CFStream,很可能会被恶意的放置数据 阻塞socket

    //如果等待,则恢复写source
    if (waiting)
    {
    //把socket可接受数据的标记去掉
    flags &= ~kSocketCanAcceptBytes;

    if (![self usingCFStreamForTLS])
    {
    //恢复写source
    [self resumeWriteSource];
    }
    }

    // Check our results

    //判断是否完成
    BOOL done = NO;
    //判断已写大小
    if (bytesWritten > 0)
    {
    // Update total amount read for the current write
    //更新当前总共写的大小
    currentWrite->bytesDone += bytesWritten;
    LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone);

    // Is packet done?
    //判断当前写包是否写完
    done = (currentWrite->bytesDone == [currentWrite->buffer length]);
    }

    //如果完成了
    if (done)
    {
    //完成操作
    [self completeCurrentWrite];

    if (!error)
    {
    dispatch_async(socketQueue, ^{ @autoreleasepool{
    //开始下一次的读取任务
    [self maybeDequeueWrite];
    }});
    }
    }
    //未完成
    else
    {
    // We were unable to finish writing the data,
    // so we're waiting for another callback to notify us of available space in the lower-level output buffer.
    //如果不是等待 而且没有出错
    if (!waiting && !error)
    {
    // This would be the case if our write was able to accept some data, but not all of it.
    //这是我们写了一部分数据的情况。

    //去掉可接受数据的标记
    flags &= ~kSocketCanAcceptBytes;
    //再去等读source触发
    if (![self usingCFStreamForTLS])
    {
    [self resumeWriteSource];
    }
    }

    //如果已写大于0
    if (bytesWritten > 0)
    {
    // We're not done with the entire write, but we have written some bytes

    __strong id theDelegate = delegate;

    //调用写的进度代理
    if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)])
    {
    long theWriteTag = currentWrite->tag;

    dispatch_async(delegateQueue, ^{ @autoreleasepool {

    [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag];
    }});
    }
    }
    }

    // Check for errors
    //如果有错,则报错断开连接
    if (error)
    {
    [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]];
    }

    // Do not add any code here without first adding a return statement in the error case above.
    }

  • 这里不同doRead的是没有提前通过flush写入链路层
  • 如果socket中可接受写数据,防止反复触发写source,挂起
  • 如果当前socket无法在写数据了,则恢复写source,当有空间去写的时候,会触发回来



  • 如果正在进行TLS认证 如果是安全通道,并且I/O阻塞,那么重新去握手








    收起阅读 »

    CocoaAsyncSocket源码Read(七)

    最后还是提下SSL的回调方法,数据解密的地方。两种模式的回调;Part7.两种SSL数据解密位置:1.CFStream:当我们调用:CFIndex result = CFReadStreamRead(readStream, buffer, defaultByt...
    继续阅读 »

    最后还是提下SSL的回调方法,数据解密的地方。两种模式的回调;

    Part7.两种SSL数据解密位置:

    1.CFStream:当我们调用:

    CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);


    数据就会被解密。
    2.SSL安全通道:当我们调用:

    OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);


    会触发SSL绑定的函数回调:

    //读函数
    static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
    {
    //拿到socket
    GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;

    //断言当前为socketQueue
    NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");

    //读取数据,并且返回状态码
    return [asyncSocket sslReadWithBuffer:data length:dataLength];
    }

    接着我们在下面的方法进行了数据读取:

    //SSL读取数据最终方法
    - (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength
    {
    //...
    ssize_t result = read(socketFD, buf, bytesToRead);
    //....
    }

    其实read这一步,数据是没有被解密的,然后传递回SSLReadFunction,在传递到SSLRead内部,数据被解密。


    本篇重点涉及该框架是如何利用缓冲区对数据进行读取、以及各种情况下的数据包处理,其中还包括普通的、和基于TLS的不同读取操作等等。
    注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度。


    附上一张核心代码逻辑图


    文中涉及代码比较多,建议大家结合源码一起阅读比较容易能加深理解

    之后会涉及到
    CocoaAsyncSocket
  • 初始化写包 :GCDAsyncWritePacket
  • 写入包放入我们的写入队列(数组)[writeQueue addObject:packet];
  • 离队执行 [self maybeDequeueWrite];


  • 主要介绍GCDAsyncSpecialPacketGCDAsyncWritePacket类型数据的处理,还有核心写入方法doWriteData三种不同方式的写入



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





    收起阅读 »

    CocoaAsyncSocket源码Read(六)

    讲了半天理论,想必大家看的有点不耐烦了,接下来看看代码实际是如何处理的吧:step1:从prebuffer中读取数据://先从提前缓冲区去读,如果缓冲区可读大小大于0 if ([preBuffer availableBytes] > 0) { ...
    继续阅读 »

    讲了半天理论,想必大家看的有点不耐烦了,接下来看看代码实际是如何处理的吧:

    step1:从prebuffer中读取数据:
    //先从提前缓冲区去读,如果缓冲区可读大小大于0
    if ([preBuffer availableBytes] > 0)
    {
    // There are 3 types of read packets:
    //
    // 1) Read all available data.
    // 2) Read a specific length of data.
    // 3) Read up to a particular terminator.
    //3种类型的读法,1、全读、2、读取特定长度、3、读取到一个明确的界限

    NSUInteger bytesToCopy;

    //如果当前读的数据界限不为空
    if (currentRead->term != nil)
    {
    // Read type #3 - read up to a terminator
    //直接读到界限
    bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done];
    }
    else
    {
    // Read type #1 or #2
    //读取数据,读到指定长度或者数据包的长度为止
    bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]];
    }

    // Make sure we have enough room in the buffer for our read.
    //从上两步拿到我们需要读的长度,去看看有没有空间去存储
    [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy];

    // Copy bytes from prebuffer into packet buffer

    //拿到我们需要追加数据的指针位置
    #pragma mark - 不明白
    //当前读的数据 + 开始偏移 + 已经读完的??
    uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset +
    currentRead->bytesDone;
    //从prebuffer处复制过来数据,bytesToCopy长度
    memcpy(buffer, [preBuffer readBuffer], bytesToCopy);

    // Remove the copied bytes from the preBuffer
    //从preBuffer移除掉已经复制的数据
    [preBuffer didRead:bytesToCopy];

    LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]);

    // Update totals

    //已读的数据加上
    currentRead->bytesDone += bytesToCopy;
    //当前已读的数据加上
    totalBytesReadForCurrentRead += bytesToCopy;

    // Check to see if the read operation is done
    //判断是不是读完了
    if (currentRead->readLength > 0)
    {
    // Read type #2 - read a specific length of data
    //如果已读 == 需要读的长度,说明已经读完
    done = (currentRead->bytesDone == currentRead->readLength);
    }
    //判断界限标记
    else if (currentRead->term != nil)
    {
    // Read type #3 - read up to a terminator

    // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method
    //如果没做完,且读的最大长度大于0,去判断是否溢出
    if (!done && currentRead->maxLength > 0)
    {
    // We're not done and there's a set maxLength.
    // Have we reached that maxLength yet?

    //如果已读的大小大于最大的大小,则报溢出错误
    if (currentRead->bytesDone >= currentRead->maxLength)
    {
    error = [self readMaxedOutError];
    }
    }
    }
    else
    {
    // Read type #1 - read all available data
    //
    // We're done as soon as
    // - we've read all available data (in prebuffer and socket)
    // - we've read the maxLength of read packet.
    //判断已读大小和最大大小是否相同,相同则读完
    done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength));
    }

    }


    这个方法就是利用我们之前提到的3种类型,来判断数据包需要读取的长度,然后调用:


    memcpy(buffer, [preBuffer readBuffer], bytesToCopy);


    把数据从preBuffer中,移到了currentRead数据包中。

    step2:从socket中读取数据:
    // 从socket中去读取

    //是否读到EOFException ,这个错误指的是文件结尾了还在继续读,就会导致这个错误被抛出
    BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file)

    //如果没完成,且没错,没读到结尾,且没有可读数据了
    BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more

    //如果没完成,且没错,没读到结尾,有可读数据
    if (!done && !error && !socketEOF && hasBytesAvailable)
    {
    //断言,有可读数据
    NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic");
    //是否读到preBuffer中去
    BOOL readIntoPreBuffer = NO;
    uint8_t *buffer = NULL;
    size_t bytesRead = 0;

    //如果flag标记为安全socket
    if (flags & kSocketSecure)
    {
    //...类似flushSSLBuffer的一系列操作
    }
    else
    {
    // Normal socket operation
    //普通的socket 操作

    NSUInteger bytesToRead;

    // There are 3 types of read packets:
    //
    // 1) Read all available data.
    // 2) Read a specific length of data.
    // 3) Read up to a particular terminator.

    //和上面类似,读取到边界标记??不是吧
    if (currentRead->term != nil)
    {
    // Read type #3 - read up to a terminator

    //读这个长度,如果到maxlength,就用maxlength。看如果可用空间大于需要读的空间,则不用prebuffer
    bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable
    shouldPreBuffer:&readIntoPreBuffer];
    }

    else
    {
    // Read type #1 or #2
    //直接读这个长度,如果到maxlength,就用maxlength
    bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable];
    }

    //大于最大值,则先读最大值
    if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3)
    bytesToRead = SIZE_MAX;
    }

    // Make sure we have enough room in the buffer for our read.
    //
    // We are either reading directly into the currentRead->buffer,
    // or we're reading into the temporary preBuffer.

    if (readIntoPreBuffer)
    {
    [preBuffer ensureCapacityForWrite:bytesToRead];

    buffer = [preBuffer writeBuffer];
    }
    else
    {
    [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead];

    buffer = (uint8_t *)[currentRead->buffer mutableBytes]
    + currentRead->startOffset
    + currentRead->bytesDone;
    }

    // Read data into buffer

    int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;
    #pragma mark - 开始读取数据,最普通的形式 read

    //读数据
    ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);
    LogVerbose(@"read from socket = %i", (int)result);
    //读取错误
    if (result < 0)
    {
    //EWOULDBLOCK IO阻塞
    if (errno == EWOULDBLOCK)
    //先等待
    waiting = YES;
    else
    //得到错误
    error = [self errnoErrorWithReason:@"Error in read() function"];
    //把可读取的长度设置为0
    socketFDBytesAvailable = 0;
    }
    //读到边界了
    else if (result == 0)
    {
    socketEOF = YES;
    socketFDBytesAvailable = 0;
    }
    //正常
    else
    {
    //设置读到的数据长度
    bytesRead = result;

    //如果读到的数据小于应该读的长度,说明这个包没读完
    if (bytesRead < bytesToRead)
    {
    // The read returned less data than requested.
    // This means socketFDBytesAvailable was a bit off due to timing,
    // because we read from the socket right when the readSource event was firing.
    socketFDBytesAvailable = 0;
    }
    //正常
    else
    {
    //如果 socketFDBytesAvailable比读了的数据小的话,直接置为0
    if (socketFDBytesAvailable <= bytesRead)
    socketFDBytesAvailable = 0;
    //减去已读大小
    else
    socketFDBytesAvailable -= bytesRead;
    }
    //如果 socketFDBytesAvailable 可读数量为0,把读的状态切换为等待
    if (socketFDBytesAvailable == 0)
    {
    waiting = YES;
    }
    }
    }


    本来想讲点什么。。发现确实没什么好讲的,无非就是判断应该读取的长度,然后调用:

    ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);

    socket中得到读取的实际长度。

    唯一需要讲一下的可能是数据流向的问题,这里调用:

    bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable shouldPreBuffer:&readIntoPreBuffer];

    来判断数据是否先流向prebuffer,还是直接流向currentRead,而SSL的读取中也有类似方法:

    - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr

    这个方法核心的思路就是,如果当前读取包,长度给明了,则直接流向currentRead,如果数据长度不清楚,那么则去判断这一次读取的长度,和currentRead可用空间长度去对比,如果长度比currentRead可用空间小,则流向currentRead,否则先用prebuffer来缓冲。

    至于细节方面,大家对着github中的源码注释看看吧,这么大篇幅的业务代码,一行行讲确实没什么意义。

    走完这两步读取,接着就是第三步:

    step3:判断数据包完成程度:

    这里有3种情况:
    1.数据包刚好读完;2.数据粘包;3.数据断包;
    注:这里判断粘包断包的长度,都是我们一开始调用read方法给的长度或者分界符得出的。

    很显然,第一种就什么都不用处理,完美匹配。
    第二种情况,我们把需要的长度放到currentRead,多余的长度放到prebuffer中去。
    第三种情况,数据还没读完,我们暂时为未读完。

    这里就不贴代码了。

    就这样普通读取数据的整个流程就走完了,而SSL的两种模式,和上述基本一致。

    我们接着根据之前读取的结果,来判断数据是否读完:

    //检查是否读完
    if (done)
    {
    //完成这次数据的读取
    [self completeCurrentRead];
    //如果没出错,没有到边界,prebuffer中还有可读数据
    if (!error && (!socketEOF || [preBuffer availableBytes] > 0))
    {
    //让读操作离队,继续进行下一次读取
    [self maybeDequeueRead];
    }
    }


    如果读完,则去做读完的操作,并且进行下一次读取。

    我们来看看读完的操作:
    //完成了这次的读数据
    - (void)completeCurrentRead
    {
    LogTrace();
    //断言currentRead
    NSAssert(currentRead, @"Trying to complete current read when there is no current read.");

    //结果数据
    NSData *result = nil;

    //如果是我们自己创建的Buffer
    if (currentRead->bufferOwner)
    {
    // We created the buffer on behalf of the user.
    // Trim our buffer to be the proper size.
    //修剪buffer到合适的大小
    //把大小设置到我们读取到的大小
    [currentRead->buffer setLength:currentRead->bytesDone];
    //赋值给result
    result = currentRead->buffer;
    }
    else
    {
    // We did NOT create the buffer.
    // The buffer is owned by the caller.
    // Only trim the buffer if we had to increase its size.
    //这是调用者的data,我们只会去加大尺寸
    if ([currentRead->buffer length] > currentRead->originalBufferLength)
    {
    //拿到的读的size
    NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone;
    //拿到原始尺寸
    NSUInteger origSize = currentRead->originalBufferLength;

    //取得最大的
    NSUInteger buffSize = MAX(readSize, origSize);
    //把buffer设置为较大的尺寸
    [currentRead->buffer setLength:buffSize];
    }
    //拿到数据的头指针
    uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset;

    //reslut为,从头指针开始到长度为写的长度 freeWhenDone为YES,创建完就释放buffer
    result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO];
    }

    __strong id theDelegate = delegate;

    #pragma mark -总算到调用代理方法,接受到数据了
    if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)])
    {
    //拿到当前的数据包
    GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer

    dispatch_async(delegateQueue, ^{ @autoreleasepool {
    //把result在代理queue中回调出去。
    [theDelegate socket:self didReadData:result withTag:theRead->tag];
    }});
    }
    //取消掉读取超时
    [self endCurrentRead];
    }


    这里对currentReaddata做了个长度的设置。然后调用代理把最终包给回调出去。最后关掉我们之前提到的读取超时。

    还是回到doReadData,就剩下最后一点处理了:

    //如果这次读的数量大于0
    else if (totalBytesReadForCurrentRead > 0)
    {
    // We're not done read type #2 or #3 yet, but we have read in some bytes

    __strong id theDelegate = delegate;

    //如果响应读数据进度的代理
    if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)])
    {
    long theReadTag = currentRead->tag;

    //代理queue中回调出去
    dispatch_async(delegateQueue, ^{ @autoreleasepool {

    [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag];
    }});
    }
    }


    这里未完成,如果这次读取大于0,如果响应读取进度的代理,则把当前进度回调出去。

    最后检查错误:
    //检查错误
    if (error)
    {
    //如果有错直接报错断开连接
    [self closeWithError:error];
    }
    //如果是读到边界错误
    else if (socketEOF)
    {
    [self doReadEOF];
    }

    //如果是等待
    else if (waiting)
    {
    //如果用的是CFStream,则读取数据和source无关
    //非CFStream形式
    if (![self usingCFStreamForTLS])
    {
    // Monitor the socket for readability (if we're not already doing so)
    //重新恢复source
    [self resumeReadSource];
    }
    }


    如果有错,直接断开socket,如果是边界错误,调用边界错误处理,如果是等待,说明当前包还没读完,如果非CFStreamTLS,则恢复source,等待下一次数据到达的触发。

    关于这个读取边界错误EOF,这里我简单的提下,其实它就是服务端发出一个边界错误,说明不会再有数据发送给我们了。我们讲无法再接收到数据,但是我们其实还是可以写数据,发送给服务端的。

    doReadEOF这个方法的处理,就是做了这么一件事。判断我们是否需要这种不可读,只能写的连接。

    我们来简单看看这个方法:
    Part6.读取边界错误处理:
    //读到EOFException,边界错误
    - (void)doReadEOF
    {
    LogTrace();
    //这个方法可能被调用很多次,如果读到EOF的时候,还有数据在prebuffer中,在调用doReadData之后?? 这个方法可能被持续的调用

    //标记为读EOF
    flags |= kSocketHasReadEOF;

    //如果是安全socket
    if (flags & kSocketSecure)
    {
    //去刷新sslbuffer中的数据
    [self flushSSLBuffers];
    }

    //标记是否应该断开连接
    BOOL shouldDisconnect = NO;
    NSError *error = nil;

    //如果状态为开始读写TLS
    if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS))
    {
    //我们得到EOF在开启TLS之前,这个TLS握手是不可能的,因此这是不可恢复的错误

    //标记断开连接
    shouldDisconnect = YES;
    //如果是安全的TLS,赋值错误
    if ([self usingSecureTransportForTLS])
    {
    error = [self sslError:errSSLClosedAbort];
    }
    }
    //如果是读流关闭状态
    else if (flags & kReadStreamClosed)
    {

    //不应该被关闭
    shouldDisconnect = NO;
    }
    else if ([preBuffer availableBytes] > 0)
    {
    //仍然有数据可读的时候不关闭
    shouldDisconnect = NO;
    }
    else if (config & kAllowHalfDuplexConnection)
    {

    //拿到socket
    int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN;

    //轮询用的结构体

    /*
    struct pollfd {
    int fd; //文件描述符
    short events; //要求查询的事件掩码 监听的
    short revents; //返回的事件掩码 实际发生的
    };
    */


    struct pollfd pfd[1];
    pfd[0].fd = socketFD;
    //写数据不会导致阻塞。
    pfd[0].events = POLLOUT;
    //这个为当前实际发生的事情
    pfd[0].revents = 0;

    /*
    poll函数使用pollfd类型的结构来监控一组文件句柄,ufds是要监控的文件句柄集合,nfds是监控的文件句柄数量,timeout是等待的毫秒数,这段时间内无论I/O是否准备好,poll都会返回。timeout为负数表示无线等待,timeout为0表示调用后立即返回。执行结果:为0表示超时前没有任何事件发生;-1表示失败;成功则返回结构体中revents不为0的文件描述符个数。pollfd结构监控的事件类型如下:
    int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
    */

    //阻塞的,但是timeout为0,则不阻塞,直接返回
    poll(pfd, 1, 0);

    //如果被触发的事件是写数据
    if (pfd[0].revents & POLLOUT)
    {
    // Socket appears to still be writeable

    //则标记为不关闭
    shouldDisconnect = NO;
    //标记为读流关闭
    flags |= kReadStreamClosed;

    // Notify the delegate that we're going half-duplex
    //通知代理,我们开始半双工
    __strong id theDelegate = delegate;

    //调用已经关闭读流的代理方法
    if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)])
    {
    dispatch_async(delegateQueue, ^{ @autoreleasepool {

    [theDelegate socketDidCloseReadStream:self];
    }});
    }
    }
    else
    {
    //标记为断开
    shouldDisconnect = YES;
    }
    }
    else
    {
    shouldDisconnect = YES;
    }

    //如果应该断开
    if (shouldDisconnect)
    {
    if (error == nil)
    {
    //判断是否是安全TLS传输
    if ([self usingSecureTransportForTLS])
    {
    ///标记错误信息
    if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful)
    {
    error = [self sslError:sslErrCode];
    }
    else
    {
    error = [self connectionClosedError];
    }
    }
    else
    {
    error = [self connectionClosedError];
    }
    }
    //关闭socket
    [self closeWithError:error];
    }
    //不断开
    else
    {
    //如果不是用CFStream流
    if (![self usingCFStreamForTLS])
    {
    // Suspend the read source (if needed)
    //挂起读source
    [self suspendReadSource];
    }
    }
    }

    简单说一下,这个方法主要是对socket是否需要主动关闭进行了判断:这里仅仅以下3种情况,不会关闭socket

    1. 读流已经是关闭状态(如果加了这个标记,说明为半双工连接状态)。
    • preBuffer中还有可读数据,我们需要等数据读完才能关闭连接。
    • 配置标记为kAllowHalfDuplexConnection,我们则要开始半双工处理。我们调用了:

    poll(pfd, 1, 0);

    函数,如果触发了写事件POLLOUT,说明我们半双工连接成功,则我们可以在读流关闭的状态下,仍然可以向服务器写数据。

    其他情况下,一律直接关闭socket
    而不关闭的情况下,我们会挂起source。这样我们就只能可写不可读了。



    作者:Cooci_和谐学习_不急不躁
    链接:https://www.jianshu.com/p/5a2df8a6a54e
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。









    收起阅读 »

    CocoaAsyncSocket源码Read(五)

    在我们来看flushSSLBuffers方法之前,我们先来看看这个一直提到的全局缓冲区prebuffer的定义,它其实就是下面这么一个类的实例:Part3.GCDAsyncSocketPreBuffer的定义@interface GCDAsyncSocketP...
    继续阅读 »

    在我们来看flushSSLBuffers方法之前,我们先来看看这个一直提到的全局缓冲区prebuffer的定义,它其实就是下面这么一个类的实例:

    Part3.GCDAsyncSocketPreBuffer的定义

    @interface GCDAsyncSocketPreBuffer : NSObject
    {
    //unsigned char
    //提前的指针,指向这块提前的缓冲区
    uint8_t *preBuffer;
    //size_t 它是一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。
    //它可以存储在理论上是可能的任何类型的数组的最大大小
    size_t preBufferSize;
    //读的指针
    uint8_t *readPointer;
    //写的指针
    uint8_t *writePointer;
    }

    里面存了3个指针,包括preBuffer起点指针、当前读写所处位置指针、以及一个preBufferSize,这个sizepreBuffer所指向的位置,在内存中分配的空间大小。

    我们来看看它的几个方法:

    //初始化
    - (id)initWithCapacity:(size_t)numBytes
    {
    if ((self = [super init]))
    {
    //设置size
    preBufferSize = numBytes;
    //申请size大小的内存给preBuffer
    preBuffer = malloc(preBufferSize);

    //为同一个值
    readPointer = preBuffer;
    writePointer = preBuffer;
    }
    return self;
    }


    包括一个初始化方法,去初始化preBufferSize大小的一块内存空间。然后3个指针都指向这个空间。

    - (void)dealloc
    {
    if (preBuffer)
    free(preBuffer);
    }

    销毁的方法:释放preBuffer。

    //确认读的大小
    - (void)ensureCapacityForWrite:(size_t)numBytes
    {
    //拿到当前可用的空间大小
    size_t availableSpace = [self availableSpace];

    //如果申请的大小大于可用的大小
    if (numBytes > availableSpace)
    {
    //需要多出来的大小
    size_t additionalBytes = numBytes - availableSpace;
    //新的总大小
    size_t newPreBufferSize = preBufferSize + additionalBytes;
    //重新去分配preBuffer
    uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize);

    //读的指针偏移量(已读大小)
    size_t readPointerOffset = readPointer - preBuffer;
    //写的指针偏移量(已写大小)
    size_t writePointerOffset = writePointer - preBuffer;
    //提前的Buffer重新复制
    preBuffer = newPreBuffer;
    //大小重新赋值
    preBufferSize = newPreBufferSize;

    //读写指针重新赋值 + 上偏移量
    readPointer = preBuffer + readPointerOffset;
    writePointer = preBuffer + writePointerOffset;
    }
    }


    确保prebuffer可用空间的方法:这个方法会重新分配preBuffer,直到可用大小等于传递进来的numBytes,已用大小不会变。

    //仍然可读的数据,过程是先写后读,只有写的大于读的,才能让你继续去读,不然没数据可读了
    - (size_t)availableBytes
    {
    return writePointer - readPointer;
    }

    - (uint8_t *)readBuffer
    {
    return readPointer;
    }

    - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr
    {
    if (bufferPtr) *bufferPtr = readPointer;
    if (availableBytesPtr) *availableBytesPtr = [self availableBytes];
    }

    //读数据的指针
    - (void)didRead:(size_t)bytesRead
    {
    readPointer += bytesRead;
    //如果读了这么多,指针和写的指针还相同的话,说明已经读完,重置指针到最初的位置
    if (readPointer == writePointer)
    {
    // The prebuffer has been drained. Reset pointers.
    readPointer = preBuffer;
    writePointer = preBuffer;
    }
    }
    //prebuffer的剩余空间 = preBufferSize(总大小) - (写的头指针 - preBuffer一开的指针,即已被写的大小)

    - (size_t)availableSpace
    {
    return preBufferSize - (writePointer - preBuffer);
    }

    - (uint8_t *)writeBuffer
    {
    return writePointer;
    }

    - (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr
    {
    if (bufferPtr) *bufferPtr = writePointer;
    if (availableSpacePtr) *availableSpacePtr = [self availableSpace];
    }

    - (void)didWrite:(size_t)bytesWritten
    {
    writePointer += bytesWritten;
    }

    - (void)reset
    {
    readPointer = preBuffer;
    writePointer = preBuffer;
    }

    然后就是对读写指针进行处理的方法,如果读了多少数据readPointer就后移多少,写也是一样。
    而获取当前未读数据,则是用已写指针-已读指针,得到的差值,当已读=已写的时候,说明prebuffer数据读完,则重置读写指针的位置,还是指向初始化位置。

    讲完全局缓冲区对于指针的处理,我们接着往下说
    Part4.flushSSLBuffers方法:

    //缓冲ssl数据
    - (void)flushSSLBuffers
    {
    LogTrace();
    //断言为安全Socket
    NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket");
    //如果preBuffer有数据可读,直接返回
    if ([preBuffer availableBytes] > 0)
    {
    return;
    }

    #if TARGET_OS_IPHONE
    //如果用的CFStream的TLS,把数据用CFStream的方式搬运到preBuffer中
    if ([self usingCFStreamForTLS])
    {
    //如果flag为kSecureSocketHasBytesAvailable,而且readStream有数据可读
    if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream))
    {
    LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD);

    //默认一次读的大小为4KB??
    CFIndex defaultBytesToRead = (1024 * 4);

    //用来确保有这么大的提前buffer缓冲空间
    [preBuffer ensureCapacityForWrite:defaultBytesToRead];
    //拿到写的buffer
    uint8_t *buffer = [preBuffer writeBuffer];

    //从readStream中去读, 一次就读4KB,读到数据后,把数据写到writeBuffer中去 如果读的大小小于readStream中数据流大小,则会不停的触发callback,直到把数据读完为止。
    CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
    //打印结果
    LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result);

    //大于0,说明读写成功
    if (result > 0)
    {
    //把写的buffer头指针,移动result个偏移量
    [preBuffer didWrite:result];
    }

    //把kSecureSocketHasBytesAvailable 仍然可读的标记移除
    flags &= ~kSecureSocketHasBytesAvailable;
    }

    return;
    }

    #endif

    //不用CFStream的处理方法

    //先设置一个预估可用的大小
    __block NSUInteger estimatedBytesAvailable = 0;
    //更新预估可用的Block
    dispatch_block_t updateEstimatedBytesAvailable = ^{

    //预估大小 = 未读的大小 + SSL的可读大小
    estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes];

    size_t sslInternalBufSize = 0;
    //获取到ssl上下文的大小,从sslContext中
    SSLGetBufferedReadSize(sslContext, &sslInternalBufSize);
    //再加上下文的大小
    estimatedBytesAvailable += sslInternalBufSize;
    };

    //调用这个Block
    updateEstimatedBytesAvailable();

    //如果大于0,说明有数据可读
    if (estimatedBytesAvailable > 0)
    {

    LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD);

    //标志,循环是否结束,SSL的方式是会阻塞的,直到读的数据有estimatedBytesAvailable大小为止,或者出错
    BOOL done = NO;
    do
    {
    LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable);

    // Make sure there's enough room in the prebuffer
    //确保有足够的空间给prebuffer
    [preBuffer ensureCapacityForWrite:estimatedBytesAvailable];

    // Read data into prebuffer
    //拿到写的buffer
    uint8_t *buffer = [preBuffer writeBuffer];
    size_t bytesRead = 0;
    //用SSLRead函数去读,读到后,把数据写到buffer中,estimatedBytesAvailable为需要读的大小,bytesRead这一次实际读到字节大小,为sslContext上下文
    OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
    LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead);

    //把写指针后移bytesRead大小
    if (bytesRead > 0)
    {
    [preBuffer didWrite:bytesRead];
    }

    LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]);

    //如果读数据出现错误
    if (result != noErr)
    {
    done = YES;
    }
    else
    {
    //在更新一下可读的数据大小
    updateEstimatedBytesAvailable();
    }

    }
    //只有done为NO,而且 estimatedBytesAvailable大于0才继续循环
    while (!done && estimatedBytesAvailable > 0);
    }
    }

    这个方法有点略长,包含了两种SSL的数据处理:

    1. CFStream类型:我们会调用下面这个函数去从stream并且读取数据并解密:
    CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);

    数据被读取到后,直接转移到了prebuffer中,并且调用:

    [preBuffer didWrite:result];

    让写指针后移读取到的数据大小。
    这里有两个关于CFReadStreamRead方法,需要注意的问题:
    1)就是我们调用它去读取4KB数据,并不仅仅是只读这么多,而是因为这个方法是会递归调用的,它每次只读4KB,直到把stream中的数据读完。
    2)我们之前设置的CFStream函数的回调,在数据来了之后只会被触发一次,以后数据再来都不会触发。直到我们调用这个方法,把stream中的数据读完,下次再来数据才会触发函数回调。这也是我们在使用CFStream的时候,不需要担心像source那样,有数据会不断的被触发回调,而需要挂起像source那样挂起stream(实际也没有这样的方法)。

    1. SSL安全通道类型:这里我们主要是循环去调用下面这个函数去读取数据:
    OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);

    其他的基本和CFStream一致

    这里需要注意的是SSLRead这个方法,并不是直接从我们的socket中获取到的数据,而是从我们一开始绑定的SSL回调函数中,得到数据。而回调函数本身,也需要调用read函数从socket中获取到加密的数据。然后再经由SSLRead这个方法,数据被解密,并且传递给buffer

    至于SSLRead绑定的回调函数,是怎么处理数据读取的,因为它处理数据的流程,和我们doReadData后续数据读取处理基本相似,所以现在暂时不提。

    我们绕了一圈,讲完了这个包为空或者当前暂停状态下的前置处理,总结一下:
    1. 就是如果是SSL类型的数据,那么先解密了,缓冲到prebuffer中去。
    2. 判断当前socket可读数据大于0,非CFStreamSSL类型,则挂起source,防止反复触发。
    Part5.接着我们开始doReadData正常数据处理流程:

    首先它大的方向,依然是分为3种类型的数据处理:
    1.SSL安全通道; 2.CFStream类型SSL; 3.普通数据传输。
    因为这3种类型的代码,重复部分较大,处理流程基本类似,只不过调用读取方法所有区别

    //1.
    OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead);
    //2.
    CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead);
    //3.
    ssize_t result = read(socketFD, buffer, (size_t)bytesToRead);

    SSLRead回调函数内部,也调用了第3种read读取,这个我们后面会说。
    现在这里我们将跳过前两种(方法部分调用可以见上面的flushSSLBuffers方法),只讲第3种普通数据的读取操作,而SSL的读取操作,基本一致。

    先来看看当前数据包任务是否完成,是如何定义的:

    由于框架提供的对外read接口:


    - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
    - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
    - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;

    将数据读取是否完成的操作,大致分为这3个类型:
    1.全读;2读取一定的长度;3读取到某个标记符为止。

    当且仅当上面3种类型对应的操作完成,才视作当前包任务完成,才会回调我们在类中声明的读取消息的代理:

    - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag

    否则就等待着,直到当前数据包任务完成。

    然后我们读取数据的流程大致如下:

    先从prebuffer中去读取,如果读完了,当前数据包任务仍未完成,那么再从socket中去读取。
    而判断包是否读完,都是用我们上面的3种类型,来对应处理的。



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






    收起阅读 »

    CocoaAsyncSocket源码Read(四)

    前文讲完了两次TLS建立连接的流程,接着就是本篇的重头戏了:doReadData方法。在这里我不准备直接把这个整个方法列出来,因为就光这一个方法,加上注释有1200行,整个贴过来也无法展开描述,所以在这里我打算对它分段进行讲解:注:以下代码整个包括在这个方法定...
    继续阅读 »
    前文讲完了两次TLS建立连接的流程,接着就是本篇的重头戏了:doReadData方法。在这里我不准备直接把这个整个方法列出来,因为就光这一个方法,加上注释有1200行,整个贴过来也无法展开描述,所以在这里我打算对它分段进行讲解:

    注:以下代码整个包括在doReadData大括号中:

    //读取数据
    - (void)doReadData
    {
    ....
    }
    Part1.无法正常读取数据时的前置处理:
    //如果当前读取的包为空,或者flag为读取停止,这两种情况是不能去读取数据的
    if ((currentRead == nil) || (flags & kReadsPaused))
    {
    LogVerbose(@"No currentRead or kReadsPaused");

    // Unable to read at this time
    //如果是安全的通信,通过TLS/SSL
    if (flags & kSocketSecure)
    {
    //刷新SSLBuffer,把数据从链路上移到prebuffer中 (当前不读取数据的时候做)
    [self flushSSLBuffers];
    }

    //判断是否用的是 CFStream的TLS
    if ([self usingCFStreamForTLS])
    {

    }
    else
    {
    //挂起source
    if (socketFDBytesAvailable > 0)
    {
    [self suspendReadSource];
    }
    }
    return;
    }

    当我们当前读取的包是空或者标记为读停止状态的时候,则不会去读取数据。
    前者不难理解,因为我们要读取的数据最终是要传给currentRead中去的,所以如果currentRead为空,我们去读数据也没有意义。
    后者kReadsPaused标记是从哪里加上的呢?我们全局搜索一下,发现它才read超时的时候被添加。
    讲到这我们顺便来看这个读取超时的一个逻辑,我们每次做读取任务传进来的超时,都会调用这么一个方法:

    Part2.读取超时处理:
    [self setupReadTimerWithTimeout:currentRead->timeout];

    //初始化读的超时
    - (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout
    {
    if (timeout >= 0.0)
    {
    //生成一个定时器source
    readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue);

    __weak GCDAsyncSocket *weakSelf = self;

    //句柄
    dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool {
    #pragma clang diagnostic push
    #pragma clang diagnostic warning "-Wimplicit-retain-self"

    __strong GCDAsyncSocket *strongSelf = weakSelf;
    if (strongSelf == nil) return_from_block;

    //执行超时操作
    [strongSelf doReadTimeout];

    #pragma clang diagnostic pop
    }});

    #if !OS_OBJECT_USE_OBJC
    dispatch_source_t theReadTimer = readTimer;

    //取消的句柄
    dispatch_source_set_cancel_handler(readTimer, ^{
    #pragma clang diagnostic push
    #pragma clang diagnostic warning "-Wimplicit-retain-self"

    LogVerbose(@"dispatch_release(readTimer)");
    dispatch_release(theReadTimer);

    #pragma clang diagnostic pop
    });
    #endif

    //定时器延时 timeout时间执行
    dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
    //间隔为永远,即只执行一次
    dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);
    dispatch_resume(readTimer);
    }
    }

    这个方法定义了一个GCD定时器,这个定时器只执行一次,间隔就是我们的超时,很显然这是一个延时执行,那小伙伴要问了,这里为什么我们不用NSTimer或者下面这种方式:
    [self performSelector:<#(nonnull SEL)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>

    原因很简单,performSelector是基于runloop才能使用的,它本质是转化成runloop基于非端口的源source0。很显然我们所在的socketQueue开辟出来的线程,并没有添加一个runloop。而NSTimer也是一样。

    所以这里我们用GCD Timer,因为它是基于XNU内核来实现的,并不需要借助于runloop

    这里当超时时间间隔到达时,我们会执行超时操作:

    [strongSelf doReadTimeout];


    //执行超时操作
    - (void)doReadTimeout
    {
    // This is a little bit tricky.
    // Ideally we'd like to synchronously query the delegate about a timeout extension.
    // But if we do so synchronously we risk a possible deadlock.
    // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block.

    //因为这里用同步容易死锁,所以用异步从代理中回调

    //标记读暂停
    flags |= kReadsPaused;

    __strong id theDelegate = delegate;

    //判断是否实现了延时 补时的代理
    if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)])
    {
    //拿到当前读的包
    GCDAsyncReadPacket *theRead = currentRead;

    //代理queue中回调
    dispatch_async(delegateQueue, ^{ @autoreleasepool {

    NSTimeInterval timeoutExtension = 0.0;

    //调用代理方法,拿到续的时长
    timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag
    elapsed:theRead->timeout
    bytesDone:theRead->bytesDone];

    //socketQueue中,做延时
    dispatch_async(socketQueue, ^{ @autoreleasepool {

    [self doReadTimeoutWithExtension:timeoutExtension];
    }});
    }});
    }
    else
    {
    [self doReadTimeoutWithExtension:0.0];
    }
    }
    //做读取数据延时
    - (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension
    {
    if (currentRead)
    {
    if (timeoutExtension > 0.0)
    {
    //把超时加上
    currentRead->timeout += timeoutExtension;

    // Reschedule the timer
    //重新生成时间
    dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC));
    //重置timer时间
    dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0);

    // Unpause reads, and continue
    //在把paused标记移除
    flags &= ~kReadsPaused;
    //继续去读取数据
    [self doReadData];
    }
    else
    {
    //输出读取超时,并断开连接
    LogVerbose(@"ReadTimeout");

    [self closeWithError:[self readTimeoutError]];
    }
    }
    }

    这里调用了续时代理,如果我们实现了这个代理,则可以增加这个超时时间,然后重新生成超时定时器,移除读取停止的标记kReadsPaused。继续去读取数据。
    否则我们就断开socket
    注意:这个定时器会被取消,如果当前数据包被读取完成,这样就不会走到定时器超时的时间,则不会断开socket

    我们接着回到doReadData中,我们讲到如果当前读取包为空或者状态为kReadsPaused,我们就去执行一些非读取数据的处理。
    这里我们第一步去判断当前连接是否为kSocketSecure,也就是安全通道的TLS。如果是我们则调用:

    if (flags & kSocketSecure)
    {
    //刷新,把TLS加密型的数据从链路上移到prebuffer中 (当前暂停的时候做)
    [self flushSSLBuffers];
    }

    按理说,我们有当前读取包的时候,在去从prebuffersocket中去读取,但是这里为什么要提前去读呢?
    我们来看看这个框架作者的解释:

    // Here's the situation:
    // We have an established secure connection.
    // There may not be a currentRead, but there might be encrypted data sitting around for us.
    // When the user does get around to issuing a read, that encrypted data will need to be decrypted.
    // So why make the user wait?
    // We might as well get a head start on decrypting some data now.
    // The other reason we do this has to do with detecting a socket disconnection.
    // The SSL/TLS protocol has it's own disconnection handshake.
    // So when a secure socket is closed, a "goodbye" packet comes across the wire.
    // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection.

    简单来讲,就是我们用TLS类型的Socket,读取数据的时候需要解密的过程,而这个过程是费时的,我们没必要让用户在读取数据的时候去等待这个解密的过程,我们可以提前在数据一到达,就去读取解密。
    而且这种方式,还能时刻根据TLSgoodbye包来准确的检测到TCP断开连接。



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







    收起阅读 »

    CocoaAsyncSocket源码Read(三)

    这里我们就讲讲几个重要的关于SSL的函数,其余细节可以看看注释:创建SSL上下文对象:sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); ssl...
    继续阅读 »

    这里我们就讲讲几个重要的关于SSL的函数,其余细节可以看看注释:

    1. 创建SSL上下文对象:
    sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
    sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);

    这个函数用来创建一个SSL上下文,我们接下来会把配置字典tlsSettings中所有的参数,都设置到这个sslContext中去,然后用这个sslContext进行TLS后续操作,握手等。

    1. 给SSL设置读写回调:
    status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);

    这两个回调函数如下:

    //读函数
    static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
    {
    //拿到socket
    GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;

    //断言当前为socketQueue
    NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");

    //读取数据,并且返回状态码
    return [asyncSocket sslReadWithBuffer:data length:dataLength];
    }
    //写函数
    static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)
    {
    GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection;

    NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?");

    return [asyncSocket sslWriteWithBuffer:data length:dataLength];
    }

    他们分别调用了sslReadWithBuffersslWriteWithBuffer两个函数进行SSL的读写处理,关于这两个函数,我们后面再来说。
    1. 发起SSL连接:
    status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);

    到这一步,前置的重要操作就完成了,接下来我们是对SSL进行一些额外的参数配置:
    我们根据tlsSettingsGCDAsyncSocketManuallyEvaluateTrust字段,去判断是否需要手动信任服务端证书,调用如下函数

    status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);

    这个函数是用来设置一些可选项的,当然不止kSSLSessionOptionBreakOnServerAuth这一种,还有许多种类型的可选项,感兴趣的朋友可以自行点进去看看这个枚举。

    接着我们按照字典中的设置项,一项一项去设置ssl上下文,类似:


    status = SSLSetPeerDomainName(sslContext, peer, peerLen);

    设置完这些有效的,我们还需要去检查无效的key,万一我们设置了这些废弃的api,我们需要报错处理。

    做完这些操作后,我们初始化了一个sslPreBuffer,这个ssl安全通道下的全局缓冲区:

    sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];

    然后把prebuffer全局缓冲区中的数据全部挪到sslPreBuffer中去,这里为什么要这么做呢?按照我们上面的流程图来说,正确的数据流向应该是从sslPreBuffer->prebuffer的,楼主在这里也思考了很久,最后我的想法是,就是初始化的时候,数据的流向的统一,在我们真正数据读取的时候,就不需要做额外的判断了。

    到这里我们所有的握手前初始化工作都做完了。

    接着我们调用了ssl_continueSSLHandshake方法开始SSL握手

    //SSL的握手
    - (void)ssl_continueSSLHandshake
    {
    LogTrace();

    //用我们的SSL上下文对象去握手
    OSStatus status = SSLHandshake(sslContext);
    //拿到握手的结果,赋值给上次握手的结果
    lastSSLHandshakeError = status;

    //如果没错
    if (status == noErr)
    {
    LogVerbose(@"SSLHandshake complete");

    //把开始读写TLS,从标记中移除
    flags &= ~kStartingReadTLS;
    flags &= ~kStartingWriteTLS;

    //把Socket安全通道标记加上
    flags |= kSocketSecure;

    //拿到代理
    __strong id theDelegate = delegate;

    if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)])
    {
    dispatch_async(delegateQueue, ^{ @autoreleasepool {
    //调用socket已经开启安全通道的代理方法
    [theDelegate socketDidSecure:self];
    }});
    }
    //停止读取
    [self endCurrentRead];
    //停止写
    [self endCurrentWrite];
    //开始下一次读写任务
    [self maybeDequeueRead];
    [self maybeDequeueWrite];
    }
    //如果是认证错误
    else if (status == errSSLPeerAuthCompleted)
    {
    LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval");

    __block SecTrustRef trust = NULL;
    //从sslContext拿到证书相关的细节
    status = SSLCopyPeerTrust(sslContext, &trust);
    //SSl证书赋值出错
    if (status != noErr)
    {
    [self closeWithError:[self sslError:status]];
    return;
    }

    //拿到状态值
    int aStateIndex = stateIndex;
    //socketQueue
    dispatch_queue_t theSocketQueue = socketQueue;

    __weak GCDAsyncSocket *weakSelf = self;

    //创建一个完成Block
    void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool {
    #pragma clang diagnostic push
    #pragma clang diagnostic warning "-Wimplicit-retain-self"

    dispatch_async(theSocketQueue, ^{ @autoreleasepool {

    if (trust) {
    CFRelease(trust);
    trust = NULL;
    }

    __strong GCDAsyncSocket *strongSelf = weakSelf;
    if (strongSelf)
    {
    [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex];
    }
    }});

    #pragma clang diagnostic pop
    }};

    __strong id theDelegate = delegate;

    if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)])
    {
    dispatch_async(delegateQueue, ^{ @autoreleasepool {

    #pragma mark - 调用代理我们自己去https认证
    [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];
    }});
    }
    //没实现代理直接报错关闭连接。
    else
    {
    if (trust) {
    CFRelease(trust);
    trust = NULL;
    }

    NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings,"
    @" but delegate doesn't implement socket:shouldTrustPeer:";

    [self closeWithError:[self otherError:msg]];
    return;
    }
    }

    //握手错误为 IO阻塞的
    else if (status == errSSLWouldBlock)
    {
    LogVerbose(@"SSLHandshake continues...");

    // Handshake continues...
    //
    // This method will be called again from doReadData or doWriteData.
    }
    else
    {
    //其他错误直接关闭连接
    [self closeWithError:[self sslError:status]];
    }
    }

    这个方法就做了一件事,就是SSL握手,我们调用了这个函数完成握手:


    OSStatus status = SSLHandshake(sslContext);

    然后握手的结果分为4种情况:

    1. 如果返回为noErr,这个会话已经准备好了安全的通信,握手成功。
    • 如果返回的valueerrSSLWouldBlock,握手方法必须再次调用。
    • 如果返回为errSSLServerAuthCompleted,如果我们要调用代理,我们需要相信服务器,然后再次调用握手,去恢复握手或者关闭连接。
    • 否则,返回的value表明了错误的code

    其中需要说说的是errSSLWouldBlock,这个是IO阻塞下的错误,也就是服务器的结果还没来得及返回,当握手结果返回的时候,这个方法会被再次触发。

    还有就是errSSLServerAuthCompleted下,我们回调了代理:

    [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler];

    我们可以去手动对证书进行认证并且信任,当完成回调后,会调用到这个方法里来,再次进行握手:

    //修改信息后再次进行SSL握手
    - (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex
    {
    LogTrace();

    if (aStateIndex != stateIndex)
    {
    return;
    }

    // Increment stateIndex to ensure completionHandler can only be called once.
    stateIndex++;

    if (shouldTrust)
    {
    NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError);
    [self ssl_continueSSLHandshake];
    }
    else
    {

    [self closeWithError:[self sslError:errSSLPeerBadCert]];
    }
    }



    到这里,我们就整个完成安全通道下的TLS认证。

    接着我们来看看基于CFStreamTLS

    因为CFStream是上层API,所以它的TLS流程相当简单,我们来看看cf_startTLS这个方法:


    //CF流形式的TLS
    - (void)cf_startTLS
    {
    LogTrace();

    LogVerbose(@"Starting TLS (via CFStream)...");

    //如果preBuffer的中可读数据大于0,错误关闭
    if ([preBuffer availableBytes] > 0)
    {
    NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket.";

    [self closeWithError:[self otherError:msg]];
    return;
    }

    //挂起读写source
    [self suspendReadSource];
    [self suspendWriteSource];

    //把未读的数据大小置为0
    socketFDBytesAvailable = 0;
    //去掉下面两种flag
    flags &= ~kSocketCanAcceptBytes;
    flags &= ~kSecureSocketHasBytesAvailable;

    //标记为CFStream
    flags |= kUsingCFStreamForTLS;

    //如果创建读写stream失败
    if (![self createReadAndWriteStream])
    {
    [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]];
    return;
    }
    //注册回调,这回监听可读数据了!!
    if (![self registerForStreamCallbacksIncludingReadWrite:YES])
    {
    [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]];
    return;
    }
    //添加runloop
    if (![self addStreamsToRunLoop])
    {
    [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]];
    return;
    }

    NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS");
    NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS");

    //拿到当前包
    GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
    //拿到ssl配置
    CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings;

    // Getting an error concerning kCFStreamPropertySSLSettings ?
    // You need to add the CFNetwork framework to your iOS application.

    //直接设置给读写stream
    BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
    BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);

    //设置失败
    if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug.
    {
    [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]];
    return;
    }

    //打开流
    if (![self openStreams])
    {
    [self closeWithError:[self otherError:@"Error in CFStreamOpen"]];
    return;
    }

    LogVerbose(@"Waiting for SSL Handshake to complete...");
    }
    1.这个方法很简单,首先它挂起了读写source,然后重新初始化了读写流,并且绑定了回调,和添加了runloop
    这里我们为什么要用重新这么做?看过之前connect篇的同学就知道,我们在连接成功之后,去初始化过读写流,这些操作之前都做过。而在这里重新初始化,并不会重新创建,只是修改读写流的一些参数,其中主要是下面这个方法,传递了一个YES过去:
    if (![self registerForStreamCallbacksIncludingReadWrite:YES])

    这个参数会使方法里多添加一种触发回调的方式:kCFStreamEventHasBytesAvailable
    当有数据可读时候,触发Stream回调。

    2.接着我们用下面这个函数把TLS的配置参数,设置给读写stream:

    //直接设置给读写stream
    BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings);
    BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings);

    3.最后打开读写流,整个CFStream形式的TLS就完成了。

    看到这,大家可能对数据触发的问题有些迷惑。总结一下,我们到现在一共有3种触发的回调:
    1. 读写source:这个和socket绑定在一起,一旦有数据到达,就会触发事件句柄,但是我们可以看到在cf_startTLS方法中我们调用了:

     //挂起读写source
    [self suspendReadSource];
    [self suspendWriteSource];

    所以,对于CFStream形式的TLS的读写并不是由source触发的,而其他的都是由source来触发。

    1. CFStream绑定的几种事件的读写回调函数:
    static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo)
    static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo)

    这个和CFStream形式的TLS相关,会触发这种形式的握手,流末尾等出现的错误,还有该形式下数据到达。
    因为我们在一开始的连接完成就初始化过stream,所以非CFStream形式下也回触发这个回调,只是不会在数据到达触发而已。

    1. SSL安全通道形式,绑定的SSL读写函数:
    static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength)
    static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength)

    这个函数并不是由系统触发,而是需要我们主动去调用SSLReadSSLWrite两个函数,回调才能被触发。























    收起阅读 »

    CocoaAsyncSocket源码Read(二)

    讲讲两种TLS建立连接的过程讲到这里,就不得不提一下,这里个框架开启TLS的过程。它对外提供了这么一个方法来开启TLS:- (void)startTLS:(NSDictionary *)tlsSettings 可以根据一个字典,去开启并且配置TLS,那么这个字...
    继续阅读 »
    讲讲两种TLS建立连接的过程

    讲到这里,就不得不提一下,这里个框架开启TLS的过程。它对外提供了这么一个方法来开启TLS

    - (void)startTLS:(NSDictionary *)tlsSettings

    可以根据一个字典,去开启并且配置TLS,那么这个字典里包含什么内容呢?
    一共包含以下这些key

    //配置SSL上下文的设置
    // Configure SSLContext from given settings
    //
    // Checklist:
    // 1\. kCFStreamSSLPeerName //证书名
    // 2\. kCFStreamSSLCertificates //证书数组
    // 3\. GCDAsyncSocketSSLPeerID //证书ID
    // 4\. GCDAsyncSocketSSLProtocolVersionMin //SSL最低版本
    // 5\. GCDAsyncSocketSSLProtocolVersionMax //SSL最高版本
    // 6\. GCDAsyncSocketSSLSessionOptionFalseStart
    // 7\. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
    // 8\. GCDAsyncSocketSSLCipherSuites
    // 9\. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
    //
    // Deprecated (throw error): //被废弃的参数,如果设置了就会报错关闭socket
    // 10\. kCFStreamSSLAllowsAnyRoot
    // 11\. kCFStreamSSLAllowsExpiredRoots
    // 12\. kCFStreamSSLAllowsExpiredCertificates
    // 13\. kCFStreamSSLValidatesCertificateChain
    // 14\. kCFStreamSSLLevel
    其中有些Key的值,具体是什么意思,value如何设置,可以查查苹果文档,限于篇幅,我们就不赘述了,只需要了解重要的几个参数即可。
    后面一部分是被废弃的参数,如果我们设置了,就会报错关闭socket连接。
    除此之外,还有这么3个key被我们遗漏了,这3个key,是框架内部用来判断,并且做一些处理的标识:

    kCFStreamSSLIsServer  //判断当前是否是服务端
    GCDAsyncSocketManuallyEvaluateTrust //判断是否需要手动信任SSL
    GCDAsyncSocketUseCFStreamForTLS //判断是否使用CFStream形式的TLS

    这3个key的大意如注释,后面我们还会讲到,其中最重要的是GCDAsyncSocketUseCFStreamForTLS这个key,一旦我们设置为YES,将开启CFStream的TLS,关于这种基于流的TLS与普通的TLS的区别,我们来看看官方说明:

    • GCDAsyncSocketUseCFStreamForTLS (iOS only)

    • The value must be of type NSNumber, encapsulating a BOOL value.

    • By default GCDAsyncSocket will use the SecureTransport layer to perform encryption.

    • This gives us more control over the security protocol (many more configuration options),

    • plus it allows us to optimize things like sys calls and buffer allocation.

    • However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption

    • technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket

    • will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property

    • (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method.

    • Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket,

    • and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty.

    • For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings.

    • If unspecified, the default value is NO.

    从上述说明中,我们可以得知,CFStream形式的TLS仅仅可以被用于iOS平台,并且它是一种过时的加解密技术,如果我们没有必要,最好还是不要用这种方式的TLS

    至于它的实现,我们接着往下看。

    //开启TLS
    - (void)startTLS:(NSDictionary *)tlsSettings
    {
    LogTrace();

    if (tlsSettings == nil)
    {

    tlsSettings = [NSDictionary dictionary];
    }
    //新生成一个TLS特殊的包
    GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings];

    dispatch_async(socketQueue, ^{ @autoreleasepool {

    if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites))
    {
    //添加到读写Queue中去
    [readQueue addObject:packet];
    [writeQueue addObject:packet];
    //把TLS标记加上
    flags |= kQueuedTLS;
    //开始读取TLS的任务,读到这个包会做TLS认证。在这之前的包还是不用认证就可以传送完
    [self maybeDequeueRead];
    [self maybeDequeueWrite];
    }
    }});

    }


    这个方法就是对外提供的开启TLS的方法,它把传进来的字典,包成一个TLS的特殊包,这个GCDAsyncSpecialPacket类包里面就一个字典属性:

    - (id)initWithTLSSettings:(NSDictionary *)settings;


    然后我们把这个包添加到读写queue中去,并且标记当前的状态,然后去执行maybeDequeueReadmaybeDequeueWrite
    需要注意的是,这里只有读到这个GCDAsyncSpecialPacket时,才开始TLS认证和握手。

    接着我们就来到了maybeDequeueRead这个方法,这个方法我们在前面第一条中讲到过,忘了的可以往上拉一下页面就可以看到。
    它就是让我们的ReadQueue中的读任务离队,并且开始执行这条读任务。

    • 当我们读到的是GCDAsyncSpecialPacket类型的包,则开始进行TLS认证。
    • 当我们读到的是GCDAsyncReadPacket类型的包,则开始进行一次读取数据的任务。
    • 如果ReadQueue为空,则对几种情况进行判断,是否是读取上一次数据失败,则断开连接。
      如果是基于TLSSocket,则把SSL安全通道的数据,移到全局缓冲区preBuffer中。如果数据仍然为空,则恢复读source,等待下一次读source的触发。

    接着我们来看看这其中第一条,当读到的是一个GCDAsyncSpecialPacket类型的包,我们会调用maybeStartTLS这个方法:


    //可能开启TLS
    - (void)maybeStartTLS
    {

    //只有读和写TLS都开启
    if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS))
    {
    //需要安全传输
    BOOL useSecureTransport = YES;

    #if TARGET_OS_IPHONE
    {
    //拿到当前读的数据
    GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
    //得到设置字典
    NSDictionary *tlsSettings = tlsPacket->tlsSettings;

    //拿到Key为CFStreamTLS的 value
    NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS];

    if (value && [value boolValue])
    //如果是用CFStream的,则安全传输为NO
    useSecureTransport = NO;
    }
    #endif
    //如果使用安全通道
    if (useSecureTransport)
    {
    //开启TLS
    [self ssl_startTLS];
    }
    //CFStream形式的Tls
    else
    {
    #if TARGET_OS_IPHONE
    [self cf_startTLS];
    #endif
    }
    }
    }

    这里根据我们之前添加标记,判断是否读写TLS状态,是才继续进行接下来的TLS认证。
    接着我们拿到当前GCDAsyncSpecialPacket,取得配置字典中keyGCDAsyncSocketUseCFStreamForTLS的值:
    如果为YES则说明使用CFStream形式的TLS,否则使用SecureTransport安全通道形式的TLS。关于这个配置项,还有二者的区别,我们前面就讲过了。

    接着我们分别来看看这两个方法,先来看看ssl_startTLS

    这个方法非常长,大概有400多行,所以为了篇幅和大家阅读体验,楼主简化了一部分内容用省略号+注释的形式表示。

    //开启TLS
    - (void)ssl_startTLS
    {
    LogTrace();

    LogVerbose(@"Starting TLS (via SecureTransport)...");

    //状态标记
    OSStatus status;

    //拿到当前读的数据包
    GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead;
    if (tlsPacket == nil) // Code to quiet the analyzer
    {
    NSAssert(NO, @"Logic error");

    [self closeWithError:[self otherError:@"Logic error"]];
    return;
    }
    //拿到设置
    NSDictionary *tlsSettings = tlsPacket->tlsSettings;

    // Create SSLContext, and setup IO callbacks and connection ref

    //根据key来判断,当前包是否是服务端的
    BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue];

    //创建SSL上下文
    #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080)
    {
    //如果是服务端的创建服务端上下文,否则是客户端的上下文,用stream形式
    if (isServer)
    sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType);
    else
    sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType);
    //为空则报错返回
    if (sslContext == NULL)
    {
    [self closeWithError:[self otherError:@"Error in SSLCreateContext"]];
    return;
    }
    }

    #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)
    {
    status = SSLNewContext(isServer, &sslContext);
    if (status != noErr)
    {
    [self closeWithError:[self otherError:@"Error in SSLNewContext"]];
    return;
    }
    }
    #endif

    //给SSL上下文设置 IO回调 分别为SSL 读写函数
    status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction);
    //设置出错
    if (status != noErr)
    {
    [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]];
    return;
    }

    //在握手之调用,建立SSL连接 ,第一次连接 1
    status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self);
    //连接出错
    if (status != noErr)
    {
    [self closeWithError:[self otherError:@"Error in SSLSetConnection"]];
    return;
    }

    //是否应该手动的去信任SSL
    BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue];
    //如果需要手动去信任
    if (shouldManuallyEvaluateTrust)
    {
    //是服务端的话,不需要,报错返回
    if (isServer)
    {
    [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]];
    return;
    }
    //第二次连接 再去连接用kSSLSessionOptionBreakOnServerAuth的方式,去连接一次,这种方式可以直接信任服务端证书
    status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true);
    //错误直接返回
    if (status != noErr)
    {
    [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]];
    return;
    }

    #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080)

    // Note from Apple's documentation:
    //
    // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8.
    // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the
    // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus
    // SSLSetEnableCertVerify is not available on that platform at all.

    //为了防止kSSLSessionOptionBreakOnServerAuth这种情况下,产生了不受信任的环境
    status = SSLSetEnableCertVerify(sslContext, NO);
    if (status != noErr)
    {
    [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]];
    return;
    }

    #endif
    }

    //配置SSL上下文的设置

    id value;
    //这个参数是用来获取证书名验证,如果设置为NULL,则不验证
    // 1\. kCFStreamSSLPeerName

    value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName];
    if ([value isKindOfClass:[NSString class]])
    {
    NSString *peerName = (NSString *)value;

    const char *peer = [peerName UTF8String];
    size_t peerLen = strlen(peer);

    //把证书名设置给SSL
    status = SSLSetPeerDomainName(sslContext, peer, peerLen);
    if (status != noErr)
    {
    [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]];
    return;
    }
    }
    //不是string就错误返回
    else if (value)
    {
    //这个断言啥用也没有啊。。
    NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString.");

    [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]];
    return;
    }

    // 2\. kCFStreamSSLCertificates
    ...
    // 3\. GCDAsyncSocketSSLPeerID
    ...
    // 4\. GCDAsyncSocketSSLProtocolVersionMin
    ...
    // 5\. GCDAsyncSocketSSLProtocolVersionMax
    ...
    // 6\. GCDAsyncSocketSSLSessionOptionFalseStart
    ...
    // 7\. GCDAsyncSocketSSLSessionOptionSendOneByteRecord
    ...
    // 8\. GCDAsyncSocketSSLCipherSuites
    ...
    // 9\. GCDAsyncSocketSSLDiffieHellmanParameters (Mac)
    ...

    //弃用key的检查,如果有下列key对应的value,则都报弃用的错误

    // 10\. kCFStreamSSLAllowsAnyRoot
    ...
    // 11\. kCFStreamSSLAllowsExpiredRoots
    ...
    // 12\. kCFStreamSSLAllowsExpiredCertificates
    ...
    // 13\. kCFStreamSSLValidatesCertificateChain
    ...
    // 14\. kCFStreamSSLLevel
    ...

    // Setup the sslPreBuffer
    //
    // Any data in the preBuffer needs to be moved into the sslPreBuffer,
    // as this data is now part of the secure read stream.

    //初始化SSL提前缓冲 也是4Kb
    sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)];
    //获取到preBuffer可读大小
    size_t preBufferLength = [preBuffer availableBytes];

    //如果有可读内容
    if (preBufferLength > 0)
    {
    //确保SSL提前缓冲的大小
    [sslPreBuffer ensureCapacityForWrite:preBufferLength];
    //从readBuffer开始读,读这个长度到 SSL提前缓冲的writeBuffer中去
    memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength);
    //移动提前的读buffer
    [preBuffer didRead:preBufferLength];
    //移动sslPreBuffer的写buffer
    [sslPreBuffer didWrite:preBufferLength];
    }
    //拿到上次错误的code,并且让上次错误code = 没错
    sslErrCode = lastSSLHandshakeError = noErr;

    // Start the SSL Handshake process
    //开始SSL握手过程
    [self ssl_continueSSLHandshake];
    }


    这个方法的结构也很清晰,主要就是建立TLS连接,并且配置SSL上下文对象:sslContext,为TLS握手做准备。











    收起阅读 »

    CocoaAsyncSocket源码Read(一)

    本文为CocoaAsyncSocket源码阅读 将重点涉及该框架是如何利用缓冲区对数据进行读取、以及各种情况下的数据包处理,其中还包括普通的、和基于TLS的不同读取操作等等。注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想...
    继续阅读 »
    本文为CocoaAsyncSocket源码阅读 将重点涉及该框架是如何利用缓冲区对数据进行读取、以及各种情况下的数据包处理,其中还包括普通的、和基于TLS的不同读取操作等等。
    注:由于该框架源码篇幅过大,且有大部分相对抽象的数据操作逻辑,尽管楼主竭力想要简单的去陈述相关内容,但是阅读起来仍会有一定的难度。如果不是诚心想学习IM相关知识,在这里就可以离场了...

    附上一张 SSL / TSL


    • 1.浅析Read读取,并阐述数据从socket到用户手中的流程。✅
    • 2.讲讲两种TLS建立连接的过程。✅
    • 3.深入讲解Read的核心方法---doReadData的实现。❌
    正文:
    一.浅析Read读取,并阐述数据从socket到用户手中的流程

    大家用过这个框架就知道,我们每次读取数据之前都需要主动调用这么一个Read方法:

    [gcdSocket readDataWithTimeout:-1 tag:110];


    设置一个超时和tag值,这样我们就可以在这个超时的时间里,去读取到达当前socket的数据了。

    那么本篇Read就从这个方法开始说起,我们点进框架里,来到这个方法:

    - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag
    {
    [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag];
    }

    - (void)readDataWithTimeout:(NSTimeInterval)timeout
    buffer:(NSMutableData *)buffer
    bufferOffset:(NSUInteger)offset
    tag:(long)tag
    {
    [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag];
    }

    //用偏移量 maxLength 读取数据
    - (void)readDataWithTimeout:(NSTimeInterval)timeout
    buffer:(NSMutableData *)buffer
    bufferOffset:(NSUInteger)offset
    maxLength:(NSUInteger)length
    tag:(long)tag
    {
    if (offset > [buffer length]) {
    LogWarn(@"Cannot read: offset > [buffer length]");
    return;
    }

    GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer
    startOffset:offset
    maxLength:length
    timeout:timeout
    readLength:0
    terminator:nil
    tag:tag];

    dispatch_async(socketQueue, ^{ @autoreleasepool {

    LogTrace();

    if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites))
    {
    //往读的队列添加任务,任务是包的形式
    [readQueue addObject:packet];
    [self maybeDequeueRead];
    }
    }});
    }


    这个方法很简单。最终调用,去创建了一个GCDAsyncReadPacket类型的对象packet,简单来说这个对象是用来标识读取任务的。然后把这个packet对象添加到读取队列中。然后去调用:

    [self maybeDequeueRead];


    去从队列中取出读取任务包,做读取操作。

    还记得我们之前Connect篇讲到的GCDAsyncSocket这个类的一些属性,其中有这么一个:

    //当前这次读取数据任务包
    GCDAsyncReadPacket *currentRead;

    这个属性标识了我们当前这次读取的任务,当读取到packet任务时,其实这个属性就被赋值成packet,做数据读取。

    接着来看看GCDAsyncReadPacket这个类,同样我们先看看属性:

    @interface GCDAsyncReadPacket : NSObject
    {
    @public
    //当前包的数据 ,(容器,有可能为空)
    NSMutableData *buffer;
    //开始偏移 (数据在容器中开始写的偏移)
    NSUInteger startOffset;
    //已读字节数 (已经写了个字节数)
    NSUInteger bytesDone;

    //想要读取数据的最大长度 (有可能没有)
    NSUInteger maxLength;
    //超时时长
    NSTimeInterval timeout;
    //当前需要读取总长度 (这一次read读取的长度,不一定有,如果没有则可用maxLength)
    NSUInteger readLength;

    //包的边界标识数据 (可能没有)
    NSData *term;
    //判断buffer的拥有者是不是这个类,还是用户。
    //跟初始化传不传一个buffer进来有关,如果传了,则拥有者为用户 NO, 否则为YES
    BOOL bufferOwner;
    //原始传过来的data长度
    NSUInteger originalBufferLength;
    //数据包的tag
    long tag;
    }

    这个类的内容还是比较多的,但是其实理解起来也很简单,它主要是来装当前任务的一些标识和数据,使我们能够正确的完成我们预期的读取任务。
    这些属性,大家同样过一个眼熟即可,后面大家就能理解它们了。

    这个类还有一堆方法,包括初始化的、和一些数据的操作方法,其具体作用如下注释:

    //初始化
    - (id)initWithData:(NSMutableData *)d
    startOffset:(NSUInteger)s
    maxLength:(NSUInteger)m
    timeout:(NSTimeInterval)t
    readLength:(NSUInteger)l
    terminator:(NSData *)e
    tag:(long)i;

    //确保容器大小给多余的长度
    - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead;
    ////预期中读的大小,决定是否走preBuffer
    - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr;
    //读取指定长度的数据
    - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable;

    //上两个方法的综合
    - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr;

    //根据一个终结符去读数据,直到读到终结的位置或者最大数据的位置,返回值为该包的确定长度
    - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr;
    ////查找终结符,在prebuffer之后,返回值为该包的确定长度
    - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes;

    这里暂时仍然不准备去讲这些方法,等我们用到了在去讲它。

    我们通过上述的属性和这些方法,能够把数据正确的读取到packet的属性buffer中,再用代理回传给用户。

    这个GCDAsyncReadPacket类暂时就先这样了,我们接着往下看,前面讲到调用maybeDequeueRead开始读取任务,我们接下来就看看这个方法:

    //让读任务离队,开始执行这条读任务
    - (void)maybeDequeueRead
    {
    LogTrace();
    NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue");

    // If we're not currently processing a read AND we have an available read stream

    //如果当前读的包为空,而且flag为已连接
    if ((currentRead == nil) && (flags & kConnected))
    {
    //如果读的queue大于0 (里面装的是我们封装的GCDAsyncReadPacket数据包)
    if ([readQueue count] > 0)
    {
    // Dequeue the next object in the write queue
    //使得下一个对象从写的queue中离开

    //从readQueue中拿到第一个写的数据
    currentRead = [readQueue objectAtIndex:0];
    //移除
    [readQueue removeObjectAtIndex:0];

    //我们的数据包,如果是GCDAsyncSpecialPacket这种类型,这个包里装了TLS的一些设置
    //如果是这种类型的数据,那么我们就进行TLS
    if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]])
    {
    LogVerbose(@"Dequeued GCDAsyncSpecialPacket");

    // Attempt to start TLS
    //标记flag为正在读取TLS
    flags |= kStartingReadTLS;

    // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set
    //只有读写都开启了TLS,才会做TLS认证
    [self maybeStartTLS];
    }
    else
    {
    LogVerbose(@"Dequeued GCDAsyncReadPacket");

    // Setup read timer (if needed)
    //设置读的任务超时,每次延时的时候还会调用 [self doReadData];
    [self setupReadTimerWithTimeout:currentRead->timeout];

    // Immediately read, if possible
    //读取数据
    [self doReadData];
    }
    }

    //读的队列没有数据,标记flag为,读了没有数据则断开连接状态
    else if (flags & kDisconnectAfterReads)
    {
    //如果标记有写然后断开连接
    if (flags & kDisconnectAfterWrites)
    {
    //如果写的队列为0,而且写为空
    if (([writeQueue count] == 0) && (currentWrite == nil))
    {
    //断开连接
    [self closeWithError:nil];
    }
    }
    else
    {
    //断开连接
    [self closeWithError:nil];
    }
    }
    //如果有安全socket。
    else if (flags & kSocketSecure)
    {
    [self flushSSLBuffers];

    //如果可读字节数为0
    if ([preBuffer availableBytes] == 0)
    {
    //
    if ([self usingCFStreamForTLS]) {
    // Callbacks never disabled
    }
    else {
    //重新恢复读的source。因为每次开始读数据的时候,都会挂起读的source
    [self resumeReadSource];
    }
    }
    }
    }
    }

    详细的细节看注释即可,这里我们讲讲主要的作用:

    1. 我们首先做了一些是否连接,读队列任务是否大于0等等一些判断。当然,如果判断失败,那么就不在读取,直接返回。
    • 接着我们从全局的readQueue中,拿到第一条任务,去做读取,我们来判断这个任务的类型,如果是GCDAsyncSpecialPacket类型的,我们将开启TLS认证。(后面再来详细讲)

    如果是是我们之前加入队列中的GCDAsyncReadPacket类型,我们则开始读取操作,调用doReadData,这个方法将是整个Read篇的核心方法。

    • 如果队列中没有任务,我们先去判断,是否是上一次是读取了数据,但是没有数据的标记,如果是的话我们则断开socket连接(注:还记得么,我们之前应用篇有说过,调取读取任务时给一个超时,如果超过这个时间,还没读取到任务,则会断开连接,就是在这触发的)。
    • 如果我们是安全的连接(基于TLS的Socket),我们就去调用flushSSLBuffers,把数据从SSL通道中,移到我们的全局缓冲区preBuffer中。

    讲到这,大家可能觉得有些迷糊,为了能帮助大家理解,这里我准备了一张流程图,来讲讲整个框架读取数据的流程:




    1. 这张图就是整个数据的流向了,这里我们读取数据分为两种情况,一种是基于TLS,一种是普通的数据读取。
    • 而基于TLS的数据读取,又分为两种,一种是基于CFStream,另一种则是安全通道SecureTransport形式。
    • 这两种类型的TLS都会在各自的通道内,完成数据的解密,然后解密后的数据又流向了全局缓冲区prebuffer
    • 这个全局缓冲区prebuffer就像一个蓄水池,如果我们一直不去做读取任务的话,它里面的数据会越来越多,当我们读取其中所有数据,它就会回归最初的状态。
    • 我们用currentRead的方式,从prebuffer中读取数据,当读到我们想要的位置时,就会回调代理,用户得到数据。





    收起阅读 »