注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Swift编译器Crash—Segmentation fault解决方案

背景抖音上线 Swift 后,编译时偶现Segmentation fault: 11和Illegal instruction: 4的错误,CI/CD 和本地均有出现,且重新编译后均可恢复正常。由于属于编译器层抛出的 Crash,加之提示的错误代码不固定且非必现...
继续阅读 »

背景

抖音上线 Swift 后,编译时偶现Segmentation fault: 11Illegal instruction: 4的错误,CI/CD 和本地均有出现,且重新编译后均可恢复正常。

由于属于编译器层抛出的 Crash,加之提示的错误代码不固定且非必现,一时较为棘手。网上类似错误较多,但Segmentation fault属于访问了错误内存的通用报错,参考意义较小。和公司内外的团队交流过,也有遇到类似错误,但原因各不相同,难以借鉴。

虽然 Swift 库二进制化后,相关代码不会参与编译,本地出现的概率大大减少,但在 CI/CD/仓库二进制化任务中依旧使用源码,出现问题需要手动重试,影响效率且繁琐,故深入编译器寻求解决方案。

Crash 堆栈




结论

简而言之,是 Swift 代码中将在 OC 中声明为类属性的NSDictionary变量,当成 Swift 的Dictionary使用。即一个 immutable 变量当作 mutable 变量使用了。编译器在校验SILInstruction时出错,主动调用abort()结束进程或出现EXC_BAD_ACCESS的 Crash。

准备工作

编译 Swift

由于本地重现过错误,故拉取和本地一致的 swift-5.3.2-RELEASE 版本,同时推荐使用 VSCode 进行调试,Ninja 进行构建。

Ninja 是专注于速度的小型构建系统。


注意事项

  • 提前预留 50G 磁盘空间
  • 首次编译时长在一小时左右,CPU 基本打满

下载&编译源码

brew install cmake ninja
mkdir swift-source
cd swift-source
git clone git@github.com:apple/swift.git
cd swift/utils
./update-checkout --tag swift-5.3.2-RELEASE --clone
./build-script

主要目录




提取编译参数

笔者将相关代码抽离抖音工程, 本地复现编译报错问题后,从 Xcode 中提取编译参数:


VSCode 调试

选择合适的 LLDB 插件,以 CodeLLDB 为例配置如下的 launch.json。

其中args内容为获取前一步提取的编译参数,批量将其中每个参数用双引号包裹,再用逗号隔开所得。

{
    "version""0.2.0",
    "configurations": [
        {
            "type":  "lldb",
            "request""launch",
            "name""Debug",
            "program""${workspaceFolder}/build/Ninja-DebugAssert/swift-macosx-x86_64/bin/swift",
            "args": ["-frontend","-c","-primary-file"/*and other params*/],
            "cwd""${workspaceFolder}",
        }
    ]
}

SIL

LLVM

在深入 SIL 之前,先简单介绍 LLVM,经典的 LLVM 三段式架构如下图所示,分为前端(Frontend),优化器(Optimizer)和后端(Backend)。当需要支持新语言时只需实现前端部分,需要支持新的架构只需实现后端部分,而前后端的连接枢纽就是 IR(Intermediate Representation),IR 独立于编程语言和机器架构,故 IR 阶段的优化可以做到抽象而通用。



Frontend

前端经过词法分析(Lexical Analysis),语法分析(Syntactic Analysis)生成 AST,语义分析(Semantic Analysis),中间代码生成(Intermediate Code Generation)等步骤,生成 IR。

IR

格式

IR 是 LLVM 前后端的桥接语言,其主要有三种格式:

  • 可读的格式,以.ll 结尾
  • Bitcode 格式,以.bc 结尾
  • 运行时在内存中的格式

这三种格式完全等价。

SSA

LLVM IR 和 SIL 都是 SSA(Static Single Assignment)形式,SSA 形式中的所有变量使用前必须声明且只能被赋值一次,如此实现的好处是能够进行更高效,更深入和更具定制化的优化。

如下图所示,代码改造为 SSA 形式后,变量只能被赋值一次,就能很容易判断出 y1=1 是可被优化移除的赋值语句。


结构

基础结构由 Module 组成,每个 Module 大概相当于一个源文件。Module 包含全局变量和 Function 等。Function 对应着函数,包括方法的声实现,参数和返回值等。Function 最重要的部分就是各类 Basic Block。

Basic Block(BB) 对应着函数的控制流图,是 Instruction 的集合,且一定以 Terminator Instructions 结尾,其代表着 Basic Block 执行结束,进行分支跳转或函数返回。

Instruction 对应着指令,是程序执行的基本单元。



Optimizer

IR 经过优化器进行优化,优化器会调用执行各类 Pass。所谓 Pass,就是遍历一遍 IR,在进行针对性的处理的代码。LLVM 内置了若干 Pass,开发者也可自定义 Pass 实现特定功能,比如插桩统计函数运行耗时等。

Xcode Optimization Level

在 Xcode - Build Setting - Apple Clang - Code Generation - Optimization Level 中,可以选定优化级别,-O0 表示无优化,即不调用任何优化 Pass。其他优化级别则调用执行对应的 Pass。



Backend

后端将 IR 转成生成相应 CPU 架构的机器码。

Swiftc

不同于 OC 使用 clang 作为编译器前端,Swift 自定义了编译器前端 swiftc,如下图所示。



这里就体现出来 LLVM 三段式的好处了,支持新语言只需实现编译器前端即可。

对比 clang,Swift 新增了对 SIL(Swift Intermediate Language)的处理过程。SIL 是 Swift 引入的新的高级中间语言,用以实现更高级别的优化。

Swift 编译流程

Swift 源码经过词法分析,语法分析和语义分析生成 AST。SILGen 获取 AST 后生成 SIL,此时的 SIL 称为 Raw SIL。在经过分析和优化,生成 Canonical SIL。最后,IRGen 再将 Canonical SIL 转化为 LLVM IR 交给优化器和后端处理。


SIL 指令

SIL 假设虚拟寄存器数量无上限,以%+数字命名,如%0,%1 等一直往上递增 以下介绍几个后续会用到的指令:

  • alloc_stack `: 分配栈内存
  • apply : 传参调用函数
  • Load : 从内存中加载指定地址的值
  • function_ref : 创建对 SIL 函数的引用

SIL 详细的指令解析可参考官方文档。

Identifier

LLVM IR 标识符有 2 种基本类型:

  • 全局标识符:包含方法和全局变量等,以@开头
  • 局部标识符:包含寄存器名和类型等,以%开头,其中%+数字代表未命名变量变量

在 SIL 中,标识符以@开头

  • SIL function 名都以@+字母/数字命名,且通常都经过 mangle
  • SIL value 同样以%+字母/数字命名,表示其引用着 instruction 或 Basic block 的参数
  • @convention(swift)使用 Swift 函数的调用约定(Calling Convention),默认使用
  • @convention(c)@convention(objc_method)分别表示使用 C 和 OC 的调用约定
  • @convention(method)表示 Swift 实例方法的实现
  • @convention(witness_method)表示 Swift protocol 方法的实现

SIL 结构

SIL 实现了一整套和 IR 类似的结构,定制化实现了SILModule SILFunction SILBasicBlock SILInstruction


调试过程

复现 Crash

根据前文的准备工作设置好编译参数后,启动编译,复现 Crash,两种 Crash 都有复现,场景如下图所示。abort()EXC_BAD_ACCESS会导致上文出现的Illegal instruction: 4Segmentation fault: 11错误。由于二者的上层堆栈一致,以下以前者为例进行分析。



堆栈分析

通过堆栈溯源可看出是在生成SILFunction后,执行postEmitFunction校验SILFunction的合法性时,使用SILVerifier层层遍历并校验 BasicBlock(visitSILBasicBlock)。对 BasicBlock 内部的SILInstruction进行遍历校验(visitSILInstruction)。

在获取SILInstruction的类型时调用getKind()返回异常,触发 Crash。




异常 SIL

  • 由于此时SILInstruction异常,比较难定位是在校验哪段指令时异常,故在遍历SILInstruction时打印上一段指令的内容。
  • swift 源代码根目录执行以下命令,增量编译
cd build/Ninja-DebugAssert/swift-macosx-x86_64
ninja

复现后打印内容如下图所示:

调试小 tips:LLVM 中很多类都实现了 dump()函数用以打印内容,方便调试。





// function_ref Dictionary.subscript.setter
%32 = function_ref @$sSDyq_Sgxcis : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@in Optional<τ_0_1>, @in τ_0_0, @inout Dictionary<τ_0_0, τ_0_1>) -> () // user: %33
%33 = apply %32<AnyHashable, Any>(, , %24) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@in Optional<τ_0_1>, @in τ_0_0, @inout Dictionary<τ_0_0, τ_0_1>) -> ()
%34 = load [take] %24 : $*Dictionary<AnyHashable, Any> // users: %43, %37


正常 SIL

命令行使用swiftc -emit-silgen能生成 Raw SIL,由于该类引用到了 OC 文件,故加上桥接文件的编译参数,完整命令如下:

swiftc -emit-silgen /Users/cs/code/ThirdParty/Swift_MVP/Swift_MVP/SwiftCrash.swift -o test.sil  -import-objc-header /Users/cs/code/ThirdParty/Swift_MVP/Swift_MVP/Swift_MVP-Bridging-Header.h

截取部分 SIL 如下

%24 = alloc_stack $Dictionary<AnyHashable, Any> // users: %44, %34, %33, %31
%25 = metatype $@objc_metatype TestObject.Type  // users: %40, %39, %27, %26
%34 = load [take] %24 : $*Dictionary<AnyHashable, Any> // users: %42, %36
%35 = function_ref @$sSD10FoundationE19_bridgeToObjectiveCSo12NSDictionaryCyF : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@guaranteed Dictionary<τ_0_0, τ_0_1>) -> @owned NSDictionary // user: %37
%36 = begin_borrow %34 : $Dictionary<AnyHashable, Any> // users: %38, %37
%37 = apply %35<AnyHashable, Any>(%36) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_0 : Hashable> (@guaranteed Dictionary<τ_0_0, τ_0_1>) -> @owned NSDictionary // users: %41, %40

SIL 分析

对正常 SIL 逐条指令分析

  1. 在栈中分配类型为Dictionary<AnyHashable, Any>的内存,将其地址存到寄存器%24,该寄存器的使用者是%44, %34, %33, %31
  2. %25 表示类型TestObject.Type,即TestObject的类型 metaType
  3. 加载%24 寄存器的值到%34 中,同时销毁%24 的值
  4. 创建对函数_bridgeToObjectiveC()-> NSDictionary的引用,存到%35 中
  • 由于函数名被 mangle,先将函数名 demangle,如下图所示,得到函数



  • @convention(method)表明是 Swift 实例方法,有 2 个泛型参数,其中第一个参数τ_0_0实现了 Hashable 协议
  1. 生成一个和%34 相同类型的值,存入%36,%36 结束使用之前,%34 一直存在
  2. 执行%35 中存储的函数,传入参数%36,返回NSDictionary类型,结果存在%37。其作用就是将Dictionary转成了NSDictionary

曙光初现

对比异常 SIL,可以看出是在执行桥接方法_bridgeToObjectiveC()时失败,遂查看源码,发现是一个 OC 的NSDictionary不可变类型桥接到 Swift 的Dictionary成为一个可变类型时,对其内容进行修改。虽然这种写法存在可能导致逻辑异常,但并不致编译器 Crash,属于编译器代码 bug。更有意思的是,只有在 OC 中将该属性声明为类属性(class)时,才会导致编译器 Crash。

class SwiftCrash: NSObject {
  func execute() {
    //compiler crash
    TestObject.cachedData[""] = ""
  }
}
@interface TestObject : NSObject
@property (strong, nonatomic, class) NSDictionary *cachedData;
@end

解决方案

源码修改

找到错误根源就好处理了,将问题代码中的 NSDictionary 改成 NSMutableDictionary 即可解决。

重新运行 Swift 编译器编译源码,无报错。

修改抖音源码后,也再没出现编译器 Crash 的问题,问题修复。

静态分析

潜在问题

虽然NSDictionary正常情况下可以桥接成 Swift 的Dictionary正常使用,但当在 Swift 中对 immutable 对象进行修改后,会重新生成新的对象,对原有对象无影响,测试代码和输出结果如下:

可以看出变量temp内容无变化,Swift 代码修改无效。

TestObject *t = [TestObject new];
t.cachedData = [@{@"oc":@"oc"} mutableCopy];
NSDictionary *temp = t.cachedData;
NSLog(@"before execution : temp %p: %@",temp,temp);
NSLog(@"before execution : cachedData %p: %@",t.cachedData,t.cachedData);
[[[SwiftDataMgr alloc] init] executeWithT:t];
NSLog(@"after execution : temp %p: %@",temp,temp);
NSLog(@"after execution : cachedData %p: %@",t.cachedData,t.cachedData);
class SwiftDataMgr: NSObject {
  @objc
  func execute(t : TestObject) {
    t.cachedData["swift"] = "swift"
  }
}




新增规则

新增对抖音源码的静态检测规则,检测所有 OC immutable 类是否在 Swift 中被修改。防止编译器 crash 和导致潜在的逻辑错误。

所有需检测的类如下:

NSDictionary/NSSet/NSData/NSArray/NSString/NSOrderedSet/NSURLRequest/
NSIndexSet/NSCharacterSet/NSParagraphStyle/NSAttributedString

后记

行文至此,该编译器 Crash 问题已经解决。同时近期在升级 Xcode 至 12.5 版本时又遇到另一种编译器 Crash 且未提示具体报错文件,笔者如法炮制找出错误后并修复。待深入分析生成SILInstruction异常的根本原因后,另起文章总结。


摘自字节跳动技术团队:https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247489361&idx=1&sn=0b3b071f2103f8082d686d308379dfd1&chksm=e9d0dcb3dea755a579abbb1f5143c6228e72aad4ef3727509d459f88d8650ec71decab65ac00&scene=178&cur_album_id=1590407423234719749#rd






收起阅读 »

Flutter框架分析-BasicMessageChannel

1. 前言 在文章Flutter框架分析(八)-Platform Channel中,我们分析了BasicMessageChannel的原理和结构,并详细讲解了与其相关的一些核心类,例如MessageHandler和MessageCodec等,本文主...
继续阅读 »

1. 前言


在文章Flutter框架分析(八)-Platform Channel中,我们分析了BasicMessageChannel的原理和结构,并详细讲解了与其相关的一些核心类,例如MessageHandlerMessageCodec等,本文主要讲解使用BasicMessageChannel的示例。


2. 使用流程


BasicMessageChannel可用于Flutternative发消息,也可用于nativeFlutter发消息,所以接下来将分别分析这两种使用流程。


2.1  Flutter给native发消息


流程如下:


1)native端创建某channel name的BasicMessageChannel


2)native端使用setMessageHandler函数,设置该BasicMessageChannelMessageHandler


3)Flutter端创建该channel name的BasicMessageChannel


4)Flutter端使用该BasicMessageChannel通过send函数向native端发送消息。


5)native端刚刚注册的MessageHandler收到发送的消息,在onMessage中处理消息,通过reply函数进行回复。


6)Flutter端处理该回复。


Flutter端关键代码如下:


class _MessageChannelState extends State<MessageChannelWidget> {
  static const  _channel = BasicMessageChannel('flutter2/testmessagechannel', JSONMessageCodec());
  int i = 0;

  void _sendMessage() async {
    final String reply = await  _channel.send('Hello World i: $i');
    print('MessageChannelTest in dart $reply');
    setState(() {
      i++;
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("message channel test"),
      ),
      body: Center(
         // Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$i',
              style: Theme.*of*(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed:_sendMessage,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),  // This trailing comma makes auto-formatting nicer for build methods);
  }
}

native端关键代码如下:


class MessageChannelActivity: FlutterActivity() {

    companion object {
        fun startActivity(activity: Activity) {
            val intent = Intent(activity, MessageChannelActivity::class.java)
            activity.startActivity(intent)
        }
    }

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {

        Log.d("MessageChannelTest""MessageChannelActivity configureFlutterEngine")
val channel = BasicMessageChannel(
                flutterEngine.dartExecutor.binaryMessenger,
                "flutter2/testmessagechannel",
            JSONMessageCodec.INSTANCE)

         // Receive messages from Dart
channel.setMessageHandler  {message, reply ->
Log.d("MessageChannelTest""in android Received message = $message")
            reply.reply("Reply from Android")
        }
    }
}

2.2  native给Flutter发消息


流程如下:


1) Flutter端创建该channel name的BasicMessageChannel


2) Flutter端使用setMessageHandler函数,设置该BasicMessageChannelHandler函数。


3) native端创建某channel name的BasicMessageChannel


4) native端使用该BasicMessageChannel通过send函数向Flutter端发送消息。


5) Flutter端刚刚注册的Handler收到发送的消息,并处理消息,然后通过reply函数进行回复。


6) native端处理该回复。


Flutter端关键代码如下:


class _MessageChannelState extends State<MessageChannelWidget> {
  static const  _channel = BasicMessageChannel('flutter2/testmessagechannel', JSONMessageCodec());
  int i = 0;

  void _sendMessage() async {
    final String reply = await  _channel.send('Hello World i: $i');
    print('MessageChannelTest in dart $reply');
    setState(() {
      i++;
    });
  }

  @override
  void initState() {
     // Receive messages from platform
_channel.setMessageHandler((dynamic message) async {
      print('MessageChannelTest in dart Received message = $message');
      return 'Reply from Dart';
    });
    super.initState();
  }
}

native端关键代码如下:


class MessageChannelActivity: FlutterActivity() {

    companion object {
        fun startActivity(activity: Activity) {
            val intent = Intent(activity, MessageChannelActivity::class.java)
            activity.startActivity(intent)
        }
    }

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {

        Log.d("MessageChannelTest""MessageChannelActivity configureFlutterEngine")

val channel = BasicMessageChannel(
                flutterEngine.dartExecutor.binaryMessenger,
                "flutter2/testmessagechannel",
            JSONMessageCodec.INSTANCE)

// Send message to Dart
Handler().postDelayed( {
channel.send("Hello World from Android")  { reply ->
Log.d("MessageChannelTest""in android $reply")
             }
}, 500)
    }
}

3. 小结


本文主要介绍了BasicMessageChannel的使用流程,并列举了一个使用BasicMessageChannel的示例。


收起阅读 »

深入浅出 NavigationUI | MAD Skills

这是第二个关于导航 (Navigation) 的 MAD Skills 系列,如果您想回顾过去发布的内容,请参考下面链接查看: 导航组件概览 导航到对话框 在应用中导航时使用 SafeArgs 使用深层链接导航 打造您的首个 app b...
继续阅读 »

这是第二个关于导航 (Navigation) 的 MAD Skills 系列,如果您想回顾过去发布的内容,请参考下面链接查看:



今天为大家发布本系列文章中的第一篇。在本文中,我们将为大家讲解另外一个用例,即类似操作栏 (Action Bar)、底部标签栏或者抽屉型导航栏之类的 UI 组件如何在应用中实现导航功能。如果您更倾向于观看视频而非阅读文章,请查看 视频 内容。


概述


在之前的 导航系列文章中,Chet 开发了一个用于 跟踪甜甜圈的应用。知道什么是甜甜圈的最佳搭档吗?(难道是另一个甜甜圈?) 当然是咖啡!所以我准备增加一个追踪咖啡的功能。我需要在应用中增加一些页面,所以有必要使用抽屉式导航栏或者底部标签栏来辅助用户导航。但是我们该如何使用这些 UI 组件来集成导航功能呢?通过点击监听器手动触发导航动作吗?


不需要!无需任何监听器。NavigationUI 类通过匹配目标页面 id 与菜单 id 实现不同页面之间的导航功能。让我们深入探索一下它的内部机制吧。


添加咖啡追踪器


△ 工程结构


△ 工程结构


首先我将与甜甜圈相关的类文件拷贝了一份到新的包下,并且将它们重命名。这样的操作对于真正的应用来说也许不是最好的做法,但是在这里可以快速帮助我们添加咖啡跟踪功能到已有的应用中。如果您希望随着文章内容同步操作,可以获取 这里的代码,里面包含了全部针对 Donut Tracker 应用的修改,可以基于该代码了解 NavigationUI。


基于上面所做的修改,我更新了导航图,新增了从 coffeeFragment 到 coffeeDialogFragment 以及从 selectionFragment 到 donutFragment 相关的目的页面和操作。之后我会用到这些目的页面的 id ;)


△ 带有新的目的页面的导航图


△ 带有新的目的页面的导航图


更新导航图之后,我们可以开始将元素绑定起来,并且实现导航到 SelectionFragment。


选项菜单


应用的选项菜单现在尚未发挥作用。要启用它,需要在 onOptionsItemSelected() 函数中,为被选择的菜单项调用 onNavDestinationSelected() 函数,并传入 navController。只要目的页面的 idMenuItem 的 id 相匹配,该函数会导航到绑定在 MenuItem 上的目的页面。


override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(
findNavController(R.id.nav_host_fragment)
) || super.onOptionsItemSelected(item)
}

现在导航控制器可以 "支配" 菜单项了,我将 MenuItemid 与之前所创建的目的页面的 id 进行了匹配。这样,导航组件就可以将 MenuItem 与目的页面进行关联。


<menu 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"
tools:context="com.android.samples.donuttracker.MainActivity">

<item
android:id="@+id/selectionFragment"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />

</menu>

Toolbar


现在应用可以导航到 selectionFragment,但是标题仍然保持原样。当处于 selectionFragment 的时候,我们希望标题可以被更新并且显示返回按钮。


首先我需要添加一个 AppBarConfiguration 对象,NavigationUI 会使用该对象来管理应用左上角的导航按钮的行为。


appBarConfiguration = AppBarConfiguration(navController.graph)

该按钮会根据您的目的页面的层级改变自身的行为。比如,当您在最顶层的目的页面时,就不会显示回退按钮,因为没有更高层级的页面。



默认情况下,您应用的最初页面是唯一的最顶层目的页面,但是您也可以定义多个最顶层目的页面。比如,在我们的应用中,我可以将 donutList coffeeList 的目的页面都定义为最顶层的目的页面。



接下来,在 MainActivity 类中,获得 navControllertoolbar 的实例,并且验证 setSupportActionBar() 是否被调用。这里我还更新了传入函数的 toolbar 的引用。


val navHostFragment = supportFragmentManager.findFragmentById(
R.id.nav_host_fragment
) as NavHostFragment
navController = navHostFragment.navController
val toolbar = binding.toolbar

要在默认的操作栏 (Action Bar) 中添加导航功能,我在这里使用了 setupActionBarWithNavController() 函数。该函数需要两个参数: navControllerappBarConfiguration


setSupportActionBar(toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)

接下来,根据目前的目的页面,我覆写了 onSupportNavigationUp() 函数,然后在 nav_host_fragment 上调用 navigateUp() 并传入 appBarConfiguration 来支持回退导航或者显示菜单图标的功能。


override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.nav_host_fragment).navigateUp(
appBarConfiguration
)
}

现在我可以导航到 selectionFragment,并且您可以看到标题已经更新,并且也显示了返回按钮,用户可以返回到之前的页面。


△ 标题更新了并且也显示了返回按钮


△ 标题更新了并且也显示了返回按钮


底部标签栏


目前为止还算顺利,但是应用还不能导航到 coffeeList Fragment。接下来我们将解决这个问题。


我们从添加底部标签栏入手。首先添加 bottom_nav_menu.xml 文件并且声明两个菜单元素。NavigationUI 依赖 MenuItemid,用它与导航图中目的页面的 id 进行匹配。我还为每个目的页面设置了图标和标题。


<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/donutList"
android:icon="@drawable/donut_with_sprinkles"
android:title="@string/donut_name" />

<item
android:id="@id/coffeeList"
android:icon="@drawable/coffee_cup"
android:title="@string/coffee_name" />

</menu>

现在 MenuItem 已经就绪,我在 mainActivity 的布局中添加了 BottomNavigationView,并且将 bottom_nav_menu 设置为 BottomNavigationViewmenu 属性。


<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu" />


要使底部标签栏发挥作用,这里调用 setupWithNavController() 函数将 navController 传入 BottomNavigationView


private fun setupBottomNavMenu(navController: NavController) {
val bottomNav = findViewById<BottomNavigationView>(
R.id.bottom_nav_view
)
bottomNav?.setupWithNavController(navController)
}

请注意我并没有从导航图中调用任何导航操作。实际上导航图中甚至没有前往 coffeeList Fragment 的路径。和之前对 ActionBar 所做的操作一样,BottomNavigationView 通过匹配 MenuItemid 和导航目的页面的 id 来自动响应导航操作。


抽屉式导航栏


虽然看上去不错,但是如果您设备的屏幕尺寸较大,那么底部标签栏恐怕无法提供最佳的用户体验。要解决这个问题,我会使用另外一个布局文件,它带有 w960dp 限定符,表明它适用于屏幕更大、更宽的设备。


这个布局文件与默认的 activity_main 布局相类似,其中已经包含了 ToolbarFragmentContainerView。我需要添加 NavigationView,并且将 nav_drawer_menu 设置为 NavigationViewmenu 属性。接下来,我将在 NavigationViewFragmentContainerView 之间添加分隔符。


<RelativeLayout
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"
tools:context="com.android.samples.donuttracker.MainActivity">

<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
app:elevation="0dp"
app:menu="@menu/nav_drawer_menu" />

<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_toEndOf="@id/nav_view"
android:background="?android:attr/listDivider" />

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@color/colorPrimary"
android:layout_toEndOf="@id/nav_view"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
app:defaultNavHost="true"
android:layout_toEndOf="@id/nav_view"
app:navGraph="@navigation/nav_graph" />

</RelativeLayout>

如此一来,在宽屏幕设备上,NavigationView 会代替 BottomNavigationView 显示在屏幕上。现在布局文件已经就绪,我再创建一个 nav_drawer_menu.xml,并且将 donutListcoffeeList 作为主要的分组添加为目的页面。对于 MenuItem,我添加了 selectionFragment 作为它的目的页面。


<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/primary">
<item
android:id="@id/donutList"
android:icon="@drawable/donut_with_sprinkles"
android:title="@string/donut_name" />

<item
android:id="@id/coffeeList"
android:icon="@drawable/coffee_cup"
android:title="@string/coffee_name" />

</group>
<item
android:id="@+id/selectionFragment"
android:title="@string/action_settings" />

</menu>

现在所有布局已经就绪,我们回到 MainActivity,设置抽屉式导航栏,使其能够与 NavigationController 协作。和之前针对 BottomNavigationView 所做的相类似,这里创建一个新的方法,并且调用 setupWithNavController() 函数将 navController 传入 NavigationView。为了使代码保持整洁、各个元素之间更加清晰,我们会在新的方法中实现相关操作,并且在 onCreate() 中调用该方法。


private fun setupNavigationMenu(navController: NavController){
val sideNavView = findViewById<NavigationView>(R.id.nav_view)
sideNavView?.setupWithNavController(navController)
}

现在当我在屏幕较宽的设备上运行应用时,可以看到抽屉式导航栏已经设置了 MenuItem,并且在导航图中,MenuItem 和目的页面的 id 是相匹配的。


△ 在屏幕较宽的设备上运行 Donut Tracker


△ 在屏幕较宽的设备上运行 Donut Tracker


请注意,当我切换页面的时候返回按钮会自动显示在左上角。如果您想这么做,还可以修改 AppBarConfiguration 来将 CoffeeList 添加为最顶层的目的页面。


小结


本次分享的内容就是这些了。Donut Tracker 应用并不需要底部标签栏或者抽屉式导航栏,但是添加了新的功能和目的页面后,NavigationUI 可以很大程度上帮助我们处理应用中的导航功能。


我们无需进行多余的操作,仅需添加 UI 组件,并且匹配 MenuItem 的 id 和目的页面的 id。您可以查阅 完整代码,并且通过 main 与 starter 分支的 比较,观察代码的变化。

收起阅读 »

iOS进阶之NSNotification的实现原理

一、NSNotification使用1、向观察者中心添加观察者:方式一:观察者接收到通知后执行任务的代码在发送通知的线程中执行- (void)addObserver:(id)observer selector:(SEL)aSelector name:(null...
继续阅读 »

一、NSNotification使用

1、向观察者中心添加观察者:

  • 方式一:观察者接收到通知后执行任务的代码在发送通知的线程中执行
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

  • 方式二:观察者接受到通知后执行任务的代码在指定的操作队列中执行
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block

2、通知中心向观察者发送消息


- (void)postNotification:(NSNotification *)notification;

- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;

- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

3、移除观察者


- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

二、实现原理

1、首先了解Observation、NCTable这个结构体内部结构

当你调用addObserver:selector:name:object:会创建一个Observation,Observation的结构如下代码:

typedef struct  Obs {
id observer; //接受消息的对象
SEL selector; //执行的方法
struct Obs *next; //下一Obs节点指针
int retained; //引用计数
struct NCTbl *link; //执向chunk table指针
} Observation;

对于Observation持有observer:

  • 在iOS9以前:

    • 持有的是一个__unsafe_unretain指针对象,当对象释放时,会访问已经释放的对象,造成BAD_ACCESS。
    • 在iOS9之后:持有的是weak类型指针,当observer释放时observer会置nil,nil对象performSelector不再会崩溃。
  • name和Observation是映射关系。

    • observer和sel包含在Observation结构体中。

Observation对象存在哪?

NSNotification维护了全局对象表NCTable结构,结构体里包含GSIMapTable表的结构,用于存储Observation。代码如下:

#define CHUNKSIZE   128
#define CACHESIZE 16
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
unsigned lockCount; /* Count recursive operations. */
NSRecursiveLock *_lock; /* Lock out other threads. */
Observation *freeList;
Observation **chunks;
unsigned numChunks;
GSIMapTable cache[CACHESIZE];
unsigned short chunkIndex;
unsigned short cacheIndex;
} NCTable;

数据结构重要的参数:

  • wildcard:保存既没有通知名称又没有传入object的通知单链表;
  • nameless:存储没有传入名字的通知名称的hash表。
  • named:存储传入了名字的通知的hash表。
  • cache:用于快速缓存.

这里值得注意nameless和named的结构,虽然都是hash表,存储的东西还有点区别:

  • nameless表中的GSIMapTable的结构如下

keyvalue
objectObservation
objectObservation
objectObservation

没有传入名字的nameless表,key就是object参数,vaule为Observation结构体

  • 在named表中GSIMapTable结构如下:
keyvalue
namemaptable
namemaptable
namemaptable
  • maptable也是一个hash表,结构如下:
keyvalue
objectObservation
objectObservation
objectObservation

传入名字的通知是存放在叫named的hash表
kay为name,value还是maptable也是一个hash表
maptable表的key为object参数,value为Observation参数

2、addObserver:selector:name:object: 方法内部实现原理

- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object
{
Observation *list;
Observation *o;
GSIMapTable m;
GSIMapNode n;

//入参检查异常处理
...
//table加锁保持数据一致性,同一个线程按顺序执行,是同步的
lockNCTable(TABLE);
//创建Observation对象包装相应的调用函数
o = obsNew(TABLE, selector, observer);
//处理存在通知名称的情况
if (name)
{
//table表中获取相应name的节点
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0)
{
//未找到相应的节点,则创建内部GSIMapTable表,以name作为key添加到talbe中
m = mapNew(TABLE);
name = [name copyWithZone: NSDefaultMallocZone()];
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
GS_CONSUMED(name)
}
else
{
//找到则直接获取相应的内部table
m = (GSIMapTable)n->value.ptr;
}

//内部table表中获取相应object对象作为key的节点
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0)
{
//不存在此节点,则直接添加observer对象到table中
o->next = ENDOBS;//单链表observer末尾指向ENDOBS
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else
{
//存在此节点,则获取并将obsever添加到单链表observer中
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
}
//只有观察者对象情况
else if (object)
{
//获取对应object的table
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n == 0)
{
//未找到对应object key的节点,则直接添加observergnustep-base-1.25.0
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
else
{
//找到相应的节点则直接添加到链表中
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
}
//处理即没有通知名称也没有观察者对象的情况
else
{
//添加到单链表中
o->next = WILDCARD;
WILDCARD = o;
}
//解锁
unlockNCTable(TABLE);
}

添加通知的基本逻辑:

  1. 根据传入的selector和observer创建Observation,并存入GSIMaptable中,如果已存在,则是从cache中取。

  2. 如果name存在:

    • 则向named表中插入元素,key为name,value为GSIMaptable。
    • GSIMaptable里面key为object,value为Observation,结束
  3. 如果name不存在:

    • 则向nameless表中插入元素,key为object,value为Observation,结束
  4. 如果name和object都不存在,则把这个Observation添加WILDCARD链表中

三、addObserverForName:object:queueusingBlock:实现原理


//对于block形式,里面创建了GSNotificationObserver对象,然后在调用addObserver: selector: name: object:
- (id) addObserverForName: (NSString *)name
object: (id)object
queue: (NSOperationQueue *)queue
usingBlock: (GSNotificationBlock)block
{
GSNotificationObserver *observer =
[[GSNotificationObserver alloc] initWithQueue: queue block: block];

[self addObserver: observer
selector: @selector(didReceiveNotification:)
name: name
object: object];

return observer;
}

/*
1.初始化该队列会创建Block_copy 拷贝block
2.并确定通知操作队列
*/

- (id) initWithQueue: (NSOperationQueue *)queue
block: (GSNotificationBlock)block
{
self = [super init];
if (self == nil)
return nil;

ASSIGN(_queue, queue);
_block = Block_copy(block);
return self;
}

/*
1.通知的接受处理函数didReceiveNotification,
2.如果queue不为空,通过addOperation来实现指定操作队列处理
3.如果queue不为空,直接在当前线程执行block。
*/

- (void) didReceiveNotification: (NSNotification *)notif
{
if (_queue != nil)
{
GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc]
initWithNotification: notif block: _block];

[_queue addOperation: op];
}
else
{
CALL_BLOCK(_block, notif);
}
}

4、发送通知的实现 postNotificationName: name: object:

 - (void) _postAndRelease: (NSNotification*)notification
{
1.入参检查校验
2.创建存储所有匹配通知的数组GSIArray
3.加锁table避免数据一致性问题
4.查找既不监听name也不监听object所有的wildcard类型的Observation,加入数组GSIArray中
5.查找NAMELESS表中指定对应观察者对象object的Observation并添加到数组中
6.查找NAMED表中相应的Observation并添加到数组中
1. 首先查找name与object的一致的Observation加入数组中
2.object为nil的Observation加入数组中
3.object不为nil,并且object和发送通知的object不一致不为添加到数组中
//解锁table
//遍历整个数组并依次调用performSelector:withObject处理通知消息发送
//解锁table并释放资源
}

二、NSNotification相关问题

1、对于addObserver方法,为什么需要object参数?

  1. addObserver当你不传入name也可以,传入object,当postNotification方法同样发出这个object时,就会触发通知方法。

因为当name不存在的时候,会继续判断object,则向nameless的maptable表中插入元素,key为object,value为Observation

2、都传入null对象会怎么样

你可能也注意到了,addObserver方法name和object都可以为空,这表示将会把observer赋值为 wildcard,他将会监听所有的通知。

3、通知的发送时同步的,还是异步的。

同步异步这个问题,由于TABLE资源的问题,同一个线程会按顺序遍历数组执行,自然是同步的。

4、NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何异步发送消息

由于是使用的performSelector方法,没有进行转线程,默认是postNotification方法的线程。


[o->observer performSelector: o->selector 
withObject: notification];

对于异步发送消息,可以使用NSNotificationQueue,queue顾明意思,我们是需要将NSNotification放入queue中执行的。

NSNotificationQueue发送消息的三种模式:

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // 当runloop处于空闲状态时post
NSPostASAP = 2, // 当当前runloop完成之后立即post
NSPostNow = 3 // 立即post
};

NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification: notification postingStyle:NSPostASAP];
  • NSPostingStyle为NSPostNow 模式是同步发送,
  • NSPostWhenIdle或者NSPostASAP是异步发送

5、NSNotificationQueue和runloop的关系?

NSNotificationQueue 是依赖runloop才能成功触发通知,如果去掉runloop的方法,你会发现无法触发通知。


dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程的runloop需要自己主动开启
NSNotification *notification = [NSNotification notificationWithName:@"TEST" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
// run runloop
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSRunLoopCommonModes];
CFRunLoopRun();
NSLog(@"3");
});
NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification: notification postingStyle:NSPostASAP];
NSNotificationQueue将通知添加到队列中时,其中postringStyle参数就是定义通知调用和runloop状态之间关系。


6、如何保证通知接收的线程在主线程?

  1. 保证主线程发送消息或者接受消息方法里切换到主线程

  2. 接收到通知后跳转到主线程,苹果建议使用NSMachPort进行消息转发到主线程。

实现代码如下:




7、页面销毁时不移除通知会崩溃吗?

在iOS9之前会,iOS9之后不会

对于Observation持有observer

在iOS9之前:不是一个类似OC中的weak类型,持有的相当与一个__unsafe_unretain指针对象,当对象释放时,会访问已经释放的对象,造成BAD_ACCESS。
在iOS9之后:持有的是weak类型指针,对nil对象performSelector不再会崩溃

8、多次添加同一个通知会是什么结果?多次移除通知呢?

  1. 由于源码中并不会进行重复过滤,所以添加同一个通知,等于就是添加了2次,回调也会触发两次。

  2. 关于多次移除,并没有问题,因为会去map中查找,找到才会删除。当name和object都为nil时,会移除所有关于该observer的WILDCARD

9、下面的方式能接收到通知吗?为什么

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];

[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

根据postNotification的实现:

  • 会找到key为TestNotification的maptable,
  • 再从中选择key为nil的observation,
  • 所以是找不到以@1为key的observation的


作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/e93b81fd3aa9




收起阅读 »

iOS之响应链和事件传递,编程中的六大设计原则

一、单一职责原则简单的讲就是一个类只做一件事,例如:CALayer:动画和视图的显示。UIView:只负责事件传递、事件响应。二、开闭原则对修改关闭,对扩展开放要考虑到后续的扩展性,而不是在原有的基础上来回修改三、接口隔离原则使用多个专门的协议,而不是一个庞大...
继续阅读 »

一、单一职责原则

简单的讲就是一个类只做一件事,例如:

  • CALayer:动画和视图的显示。

  • UIView:只负责事件传递、事件响应。

二、开闭原则

  • 对修改关闭,对扩展开放

  • 要考虑到后续的扩展性,而不是在原有的基础上来回修改

三、接口隔离原则

使用多个专门的协议,而不是一个庞大臃肿的基础上来回修改,例如:

  • UITableviewDelegate : 主要提供一些可选的方法,用来控制tableView的选择、指定section的头和尾的显示以及协助完成cell的删除和排序等功能。

  • UITableViewDataSource : 主要为UITableView提供显示用的数据(UITableViewCell),指定UITableViewCell支持的编辑操作类型(insert,delete和 reordering),并根据用户的操作进行相应的数据更新操作

四、依赖倒置原则

  • 抽象不应该依赖于具体实现,具体实现可以依赖于抽象。
  • 调用接口感觉不到内部是如何操作的

五、里氏替换原则

父类可以被子类无缝替换,且原有的功能不受任何影响,例如:KVO
继承父类,也不想使用使用KVO监听对象的属性。

六、迪米特法则

一个对象应当对其他对象尽可能少的了解,实现高聚合、低耦合


前言

首先要先学习下响应者对象UIResponder,只有继承UIResponder的的类,才能处理事件。

@interface UIApplication : UIResponder

@interface UIView : UIResponder

@interface UIViewController : UIResponder

@interface UIWindow : UIView

@interface UIControl : UIView

@interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>


我们可以看出UIApplication、UIView、UIWindow->UIView、UIViewController都是继承自UIResponder类,可以响应和处理事件。CALayer不是UIResponder的子类,无法处理事件。

UIControl是UIView的子类,当然也是UIResponder的子类。UIControl是诸如UIButton,UISwitch,UItextField等控件的父类,它本身包含了一些属性和方法,但是不能直接食用UIControl类,他只是定义了子类都需要使用的方法。

我们有时候可能通过UIReponsder的nextResponder来查找控件的父视图控件


// 通过遍历button上的响应链来查找cell
UIResponder *responder = button.nextResponder;
while (responder) {
if ([responder isKindOfClass:[SWSwimCircleItemTableViewCell class]]) {
SWSwimCircleItemTableViewCell *cell = (SWSwimCircleItemTableViewCell *)responder;
break;
}
responder = responder.nextResponder;
}
}

UIControl 与 UIView的关系和区别

  • UIControl继承与UIView,在UIView基础上侧重于事件交互,最大的特点就是拥有addTaget:action:forcontrolEvents方法

  • UIVew侧重于页面布局,所以没有时间交互的方法,可以通过添加手势来实现

事件UIEvent

对于IOS设备用户来说,他们的事件类型分为三种:

  1. 触摸事件(Touch Event)

  2. 运动事件 (Motion Event)

  3. 远端控制事件 (Remote-Control Event)

今天以触屏事件(Touch Event)为例,来说明在Cocoa Touch框架中,事件的处理流程。

事件的传递和响应过程

  1. 点击屏幕后,经过系统的一系列处理,我们的应用接收到source0事件,并从事件队列中取出事件对象,开始寻找真正响应事件的视图。

  2. UIApplication将处于任务队列最前端的事件向下分发。即UIWindow。

  3. UIWindow将事件向下分发,即UIView。

  4. UIView首先看自己是否能处理事件,触摸点是否在自己身上。如果能,那么继续寻找子视图。

  5. 遍历子控件,重复3、4步骤

  6. 如果没有找到,那么自己就是事件处理者

  7. 如果自己不能处理,那么不做任何处理

其中 UIView不接受事件处理的情况主要有以下三种:

  1. alpha <0.01

  2. userInteractionEnabled = NO

  3. hidden = YES.

从父控件到子控件寻找处理事件最合适view的过程。

如果父视图不接受事件处理(上面三种情况),则子视图也不能接收事件。

事件只要触摸了就会产生,关键在于是否有最合适的view来处理和接收事件,如果遍历到最后都没有最合适的view来接收事件,则该事件被废弃。

响应者寻找过程分析

寻找相应过程主要涉及到两个方法:

//判断点击的位置是不是在视图内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

//此方法返回的View是本次点击事件需要的最佳View(第一响应者)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;

因为所有的视图类都是继承UIView,在UIView(UIViewGeometry)类别里实现这个方法,代码大概的实现流程:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判断点在不在当前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[I];
// 把当前控件上的坐标系转换成子控件上的坐标系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;

}

看着上面的代码:

  • 首先该view的hidden = YES、userInteractionEnabled=YES、alpha<0.01 三种情况成立一个就直接返回nil,代表视图无法继续寻找最合适的view

  • 其次判断触摸点在不在当前控件,不在也是返回nil

  • 最后倒序遍历子视图,把当前控件上的坐标系转成子控件上的坐标系,判断子视图能够响应,有的话说明在子视图寻找到最合适的view。

  • 如果都没有的话,循环结束,表示没有比自己更合适的view,返回自己的view

下面就通过一个例子来探寻整个寻找的过程

我们在ViewController构造一个简单的视图层级,BlueView、YellowView是两个根节点视图,RedView是他们的父视图。效果如下:





步骤1,2,3

结合上面两张图,介绍一下整个执行流程:

  1. 先从UIWindow视图开启,因此,对UIWindow对象进行hitTest: withEvent:在方法内使用pointInside:withEvent:方法判断用户点击的范围是在UIWindow的范围内,显然pointInside:withEvent:返回了YES,这时候继续检查子视图

  2. 第二步和第三步骤重复第一步的操作,pointInside:withEvent:返回的都是YES,下面对RedView里继续检查自视图是否响应该事件

  3. 遍历RedView子视图,如果先遍历的YellowView,对YellowView进行 hitTest: withEvent:里面做pointInside:withEvent判断,不在点击范围内返回NO,对应的hitTest:withEvent:返回nil;

  4. 继续遍历RedView子视图BlueView,对BlueView hitTest: withEvent:里面做pointInside:withEvent判断,发现在点击范围内返回YES.

  5. 由于BlueView没有子视图(也可以理解成对的BlueView子视图进行hitTest时返回了nil),因此,BlueView的hitTest:withEvent:会将BlueView返回,再往回回溯。

  6. ReadView的hitTest:withEvent返回的BlueView -> UIView的hitTest:withEvent返回的BlueView -> UIWindow的hitTest:withEvent返回的BlueView。

  7. UIWindow的nexResponder指向UIApplication最后指向AppDelegate。

至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。

不难看出,这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。

进一步说明

  • 如果hitTest:withEvent没有找到第一响应者,或者第一响应者没有处理改事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃。

  • hitTest:withEvent方法将会忽略以下三种情况。

    • 该view的hidden = YES

    • 该view的userInteractionEnabled=YES

    • 该view的alpha<0.01

  • 如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别。因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。

当然,也可以重写pointInside:withEvent:方法来处理这种情况。

我们可以重写hitTest:withEvent:来拦截事件传递并处理事件来达到目的,实际应用中很少用到这些。



作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/065e39cfce8b


收起阅读 »

iOS之SDWebImage内部实现原理 - 面试必问!

原理图片解释:内存层面的相当于一个缓存器,以key-value的形式存储图片。当SDImageCache缓存使用的LRU(最近最右淘汰算法)算法,来做缓存机制。当SDWebImageManager向SDImageCache要资源时,先搜索内存层面的数据,如果有...
继续阅读 »

原理

图片解释:内存层面的相当于一个缓存器,以key-value的形式存储图片。当SDImageCache缓存使用的LRU(最近最右淘汰算法)算法,来做缓存机制。当SDWebImageManager向SDImageCache要资源时,先搜索内存层面的数据,如果有就直接返回,如果没有的话访问磁盘,将图片从硬盘读取出来,然后解码(Decoder),将图片对象到内存层面做备份,在返回调用层。

SDWebImage加载图片整体流程

  1. 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

  2. 进入SDWebImageManager,调用downloadWithURL:delegate:options:userInfo:

  3. 交给SDImageCache从缓存中查找图片是否已经下载:queryDiskCacheForKey:delegate:userInfo:

  4. 如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

  5. SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

  6. 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

  7. 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

  8. 如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。然后会进行第4、5步骤来展示图片

  9. 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:

  10. 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片

  11. 图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。

    • connection:didReceiveData:中利用 ImageIO 做了按图片下载进度加载效果。

    • connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

      • 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
      • 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。
  12. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

  13. 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

  14. 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

  15. SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。



作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/70620ccdfdb7


收起阅读 »

iOS非越狱注入插件

准备工作这里我们以QQ App来举例,这里需要注入的是我自己写的一个QQPlus这个插件; 首先我们需要准备以下文件:. ├── CydiaSubstrate ├── QQ.ipa ├── QQPlus.dylib ├── QQPlusSetting.bund...
继续阅读 »

准备工作

这里我们以QQ App来举例,这里需要注入的是我自己写的一个QQPlus这个插件; 首先我们需要准备以下文件:

.
├── CydiaSubstrate
├── QQ.ipa
├── QQPlus.dylib
├── QQPlusSetting.bundle
│ ├── Root.plist
│ ├── en.lproj
│ │ └── Root.strings
│ └── interface.json
├── blank.caf
├── cy.csv
└── libsubstitute.0.dylib
  • CydiaSubstrate: 从越狱手机目录/Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate拷贝出来
  • libsubstitute.0.dylib: CydiaSubstrate依赖文件, 从越狱手机目录/usr/lib/libsubstitute.0.dylib拷贝出来
  • QQ.ipa: 一个砸壳后的ipa文件, 如果没有砸壳则无法进行以下操作, 可以使用otool验证是否加壳
  • QQPlus.dylib: 需要注入的插件(确保可用)
  • QQPlusSetting.bundle: QQPlus.dylib插件需要依赖文件
  • blank.caf: QQPlus.dylib插件需要依赖文件
  • cy.csv: QQPlus.dylib插件需要依赖文件


  • 开始注入

    1. 首先我们把QQ.ipa包解压(ipa就是个压缩包, 直接解压或者使用命令解压都可)
    unzip QQ.ipa

    解压完成后我们先确认包是否加密, 使用otool命令

    cd Payload/QQ.app/
    otool -l QQ | grep crypt

    输入以上命令后输出

    cryptoff 28672
    cryptsize 4096
    cryptid 0

    这里cryptid0则为未加密, 确认了未加密后我们就可以开始注入了;

    1. CydiaSubstrate改名为libsubstrate.dylib然后将以下文件拷贝至/Payload/QQ.app/Frameworks目录

    libsubstrate.dylib
    libsubstitute.0.dylib
    QQPlus.dylib

    修改libsubstrate.dylib依赖文件
    因为libsubstrate.dylib是从越狱手机上拷贝出来的, 他的一个依赖文件ibsubstitute.0.dylib的路径是/usr/lib/libsubstitute.0.dylib, 我们需要将他修改到Frameworks目录下, 否则会闪退, 使用otool命令查看:

    aria@shenqiHyaliyadeMacBook-Pro  ~/Desktop/remake/QQ  otool -L libSubstrate.dylib
    libSubstrate.dylib (architecture arm64):
    /usr/lib/libsubstrate.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libsubstitute.0.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.0.0)
    CydiaSubstrate (architecture arm64e):
    /usr/lib/libsubstrate.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libsubstitute.0.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.0.0)

    可以看到倒数第三个依赖, 我们需要使用install_name_tool命令修改他

    install_name_tool -change "/usr/lib/libsubstitute.0.dylib" "@executable_path/Frameworks/libsubstitute.0.dylib" libSubstrate.dylib

    然后再次使用otool命令查看是否修改成功

    aria@shenqiHyaliyadeMacBook-Pro  ~/Desktop/remake/QQ  otool -L libSubstrate.dylib
    libSubstrate.dylib (architecture arm64):
    /usr/lib/libsubstrate.dylib (compatibility version 0.0.0, current version 0.0.0)
    @executable_path/Frameworks/libsubstitute.0.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.0.0)
    libSubstrate.dylib (architecture arm64e):
    /usr/lib/libsubstrate.dylib (compatibility version 0.0.0, current version 0.0.0)
    @executable_path/Frameworks/libsubstitute.0.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.0.0)

    这里可以看到已经把/usr/lib/libsubstitute.0.dylib已经被修改为@executable_path/Frameworks/libsubstitute.0.dylib

    1. 修改QQPlus.dylib插件依赖
      因为是越狱插件, 所以他的依赖是/Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate, 但是在非越狱手机上是肯定没有这个依赖的, 所以我们一样需要对他进行修改, 用otool命令查看依赖

    aria@shenqiHyaliyadeMacBook-Pro  ~/Desktop/remake/QQ  otool -L QQPlus.dylib
    QQPlus.dylib:
    /Library/MobileSubstrate/DynamicLibraries/QQPlus.dylib (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics (compatibility version 64.0.0, current version 1355.22.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1677.104.0)
    /System/Library/Frameworks/MobileCoreServices.framework/MobileCoreServices (compatibility version 1.0.0, current version 1069.25.0)
    /System/Library/Frameworks/QuartzCore.framework/QuartzCore (compatibility version 1.2.0, current version 1.11.0)
    /System/Library/Frameworks/Security.framework/Security (compatibility version 1.0.0, current version 59306.142.1)
    /System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration (compatibility version 1.0.0, current version 1061.140.1)
    /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 61000.0.0)
    /Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
    /System/Library/Frameworks/AVFoundation.framework/AVFoundation (compatibility version 1.0.0, current version 2.0.0)
    /System/Library/Frameworks/CFNetwork.framework/CFNetwork (compatibility version 1.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1677.104.0)
    /System/Library/Frameworks/CoreTelephony.framework/CoreTelephony (compatibility version 1.0.0, current version 0.0.0)
    这里可以很清楚的看到一个依赖/Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate, 同样我们需要使用install_name_tool命令修改他把他修改到Frameworks目录下的libSubstrate.dylib

    install_name_tool -change "/Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate" "@executable_path/Frameworks/libSubstrate.dylib" QQPlus.dylib

    再使用otool命令查看是否成功修改依赖

    aria@shenqiHyaliyadeMacBook-Pro  ~/Desktop/remake/QQ  otool -L QQPlus.dylib
    QQPlus.dylib:
    /Library/MobileSubstrate/DynamicLibraries/QQPlus.dylib (compatibility version 1.0.0, current version 1.0.0)
    /System/Library/Frameworks/CoreGraphics.framework/CoreGraphics (compatibility version 64.0.0, current version 1355.22.0)
    /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 1677.104.0)
    /System/Library/Frameworks/MobileCoreServices.framework/MobileCoreServices (compatibility version 1.0.0, current version 1069.25.0)
    /System/Library/Frameworks/QuartzCore.framework/QuartzCore (compatibility version 1.2.0, current version 1.11.0)
    /System/Library/Frameworks/Security.framework/Security (compatibility version 1.0.0, current version 59306.142.1)
    /System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration (compatibility version 1.0.0, current version 1061.140.1)
    /System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 61000.0.0)
    @executable_path/Frameworks/libSubstrate.dylib (compatibility version 0.0.0, current version 0.0.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
    /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
    /System/Library/Frameworks/AVFoundation.framework/AVFoundation (compatibility version 1.0.0, current version 2.0.0)
    /System/Library/Frameworks/CFNetwork.framework/CFNetwork (compatibility version 1.0.0, current version 0.0.0)
    /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 1677.104.0)
    /System/Library/Frameworks/CoreTelephony.framework/CoreTelephony (compatibility version 1.0.0, current version 0.0.0)

    这里可以看到依赖已经被修改为@executable_path/Frameworks/libSubstrate.dylib

    1. 拷贝QQPlus.dylib依赖文件到QQ.app根目录下(如果插件没有依赖文件则不需要此步骤, 由于我自己写的QQPlus.dylib需要依赖blank.cafcy.csvQQPlusSetting.bundle这三个文件, 所以需要一起拷贝进去)

    2. 修改QQ主程序, 插入Load Commands, 使用optool或者insert_dylib都行, 这里以optool进行操作:


    aria@shenqiHyaliyadeMacBook-Pro~/Desktop/remake/QQ/Payload/QQ.app  optool install -c load -p "@executable_path/Frameworks/QQPlus.dylib" -t QQ
    Found thin header...
    Inserting a LC_LOAD_DYLIB command for architecture: arm64
    Successfully inserted a LC_LOAD_DYLIB command for arm64
    Writing executable to QQ...

    再次使用otool命令查看是否注入成功

    aria@shenqiHyaliyadeMacBook-Pro~/Desktop/remake/QQ/Payload/QQ.app  otool -L QQ
    QQ:
    @rpath/QQMainProject.framework/QQMainProject (compatibility version 1.0.0, current version 1.0.0)
    ...
    @executable_path/Frameworks/QQPlus.dylib (compatibility version 0.0.0, current version 0.0.0)

    这里可以看到我们已经插入了@executable_path/Frameworks/QQPlus.dylib

    1. 打包QQ.ipa, 使用zip命令
    zip -ry target.ipa Payload
    重新签名安装
    由于修改了包内容, 所以需要重新签名, 签名可以参考其他文章或者使用第三方软件;
    安装成功后插件成功被加载, 效果如下:







    作者:神崎H亚里亚
    链接:https://www.jianshu.com/p/741eda8d460f



    收起阅读 »

    iOS-底层原理 :类的加载

    1.1 进入 map_imagesvoid map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) ...
    继续阅读 »

    一、map_images源码初探

    1.1 进入 map_images

    void
    map_images(unsigned count, const char * const paths[],
    const struct mach_header * const mhdrs[])
    {
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
    }


    • map_images中调用了map_images_nolock
    • map_images:管理文件中和动态库中的所有符号(class protocol selector category)

    1.2 进入 map_images_nolock


    • map_images_nolock方法中,会进行首次初始化。

    • 调用关键方法_read_images读取镜像文件。

    1.3 进入 _read_images

    由于代码量过大,只显示关键代码,请自行查阅


    • _read_images中做了以下操作,即是对(selector class protocol category )的处理
    1. 条件控制进行一次加载
    2. 修复预编译阶段的 @selector 的混乱问题
    3. 检测类,修复现在不处理的,将来需要处理的类,标记bundle
    4. 修复重映射一些没有被镜像文件加载进来的类
    5. 修复旧的objc_msgSend_fixup调用站点
    6. 检测协议,修复协议,当我们类里面有协议的时候调用readProtocol
    7. 修复没有被加载协议
    8. 分类处理,启动时的类别初始化完成后才走if里面,被推迟到对_dyld_objc_notify_register的调用完成后的第一个load_images调用为止
    9. 非懒加载类处理,实现了+load方法的类叫非懒加载类
    10. 没有被处理的类,优化那些被侵犯的类

    1.4 进入 readClass

    _read_images流程中的第三步,我们找到了关键方法readClass




    • addNamedClass:把cls绑定上插入到内存中存类的数据表中
    • addClassTableEntry:把cls加入到需要开辟内存的类的表

    既然我们已经把存到了内存中,那我们中的方法属性协议又是什么时候存进入的呢?是怎么从mahodata中拿到rwrwe的呢?我们还不得而知。

    现在我们的研究对象是,而_read_images与类相关的只有第三步,第九步和第十步,而第三步我们已经探索过,即是把类绑定名字加入到内存的两张表中,一张存类的表,一张存需要开辟内存的类的表,接着研究第九步和第十步


    1.5 探索与类相关的第九步和第十步

    首先在第九步中添加自定义条件,定位到研究对象GomuPerson,并加上2个断点,如下图所示


    • 经过断点调试,发现2个断点都不会走,但是我们又根据命名推断出了关键方法realizeClassWithoutSwift,但是没有调用

    于是我们推测,第九步没有走的原因,是不是因为判断条件不满足,非懒加载类才会走进if,接着我们探索一个拓展非懒加载类 & 懒加载类

    1.6 非懒加载类 & 懒加载类

    懒加载类:数据加载推迟到第一次发送消息的时候
    非懒加载类:map_images 的时候 加载所有类数据
    区分方法:当前类是否实现了 load 方法,实现则未非懒加载类

    懒加载类加载情况:GomuPerson中未实现load方法,在方法慢速查询lookUpImpOrForward(由于是第一次发送消息,缓存中没有数据,所以会进入到慢查询流程)中,插入以下代码,运行

    • 懒加载类不会调用_read_images第九步中的realizeClassWithoutSwift,而是在main函数中实例GomuPerson调用alloc方法发送消息的时候加载类
    • lookUpImpOrForward -> realizeClassMaybeSwiftMaybeRelock -> realizeClassWithoutSwift

    非懒加载类加载情况:GomuPerson中实现load方法,会调用第九步中的realizeClassWithoutSwift

    • 非懒加载类会调用_read_images第九步中的realizeClassWithoutSwift,在map_images加载类
    • _getObjc2NonlazyClassList -> realizeClassWithoutSwift

    GomuPerson中实现load方法,继续探索realizeClassWithoutSwift流程

    1.7 进入 realizeClassWithoutSwift

    调用realizeClassWithoutSwift之前,cls只是一个地址+name,ro,rw,rwe还没有,下面开始研究是否在realizeClassWithoutSwift对它们进行了操作






    收起阅读 »

    Compose | 一文理解神奇的Modifier

    写在最前Jetpack Compose的预览版出来已经有很长时间了,相信很多读者都进行了一番尝试。注意:下文如无特殊说明,Compose均指代Jetpack Compose可以说,Compose在声明布局时,其风格和React的JSX、Flutter 等非常的...
    继续阅读 »

    写在最前

    Jetpack Compose的预览版出来已经有很长时间了,相信很多读者都进行了一番尝试。注意:下文如无特殊说明,Compose均指代Jetpack Compose

    可以说,Compose在声明布局时,其风格和React的JSX、Flutter 等非常的相似。

    而且有一个高频出现的内容: Modifier,即 修饰器,顾名思义,它是一种修饰器, 在Compose的设计中,和UI相关的内容都涉及到它,例如:尺寸形状 等

    这一篇文章,我们一起学习两部分内容:

    • Modifier的源码和设计
    • SDK中既有的Modifier实现概览

    当然,最全面的学习文档当属:官方API文档 , 后续查询API的含义和设计细节等都会用到,建议收藏

    文中的代码均源自 1.0.1 版本

    先放大招,Modifier的45行代码

    其实有效代码行大约20行。

    先举个使用示例:

    Modifier.height(320.dp).fillMaxWidth()

    这里的 Modifier 是接口 androidx.compose.ui.Modifier 的匿名实现,这也是一个很有意思的实用技巧。

    我们先简单的概览下源码,再进行解读:

    interface Modifier {
    // ...
    companion object : Modifier {
    override fun foldIn(initial: R, operation: (R, Element) -> R): R = initial
    override fun foldOut(initial: R, operation: (Element, R) -> R): R = initial
    override fun any(predicate: (Element) -> Boolean): Boolean = false
    override fun all(predicate: (Element) -> Boolean): Boolean = true
    override infix fun then(other: Modifier): Modifier = other
    override fun toString() = "Modifier"
    }
    }

    而本身的接口则为:

    package androidx.compose.ui

    import androidx.compose.runtime.Stable

    interface Modifier {

    fun foldIn(initial: R, operation: (R, Element) -> R): R

    fun foldOut(initial: R, operation: (Element, R) -> R): R

    fun any(predicate: (Element) -> Boolean): Boolean

    fun all(predicate: (Element) -> Boolean): Boolean

    infix fun then(other: Modifier): Modifier =
    if (other === Modifier) this else CombinedModifier(this, other)
    }

    Modifier接口默认实现赏析

    先看Modifier接口,和Java8类似,Kotlin的接口可以提供默认实现, 显然, foldIn 和 foldOut 在这里是看不出门道的,必须结合 operation来看,先略过。

    any 和 all 也是看不出啥的,毕竟我把注释都删了 而 then 方法则有点意思,接收一个 Modifier 接口实例, 如果该实例是Modifier的内部默认实现,则认为是无效操作,依旧返回自身,否则则返回一个 CombinedModifier实例 将自身和 other 结合在一起。

    从这里,我们可以读出一点 味道 : 设计者一定会将一系列的Modifier设计成一个类似链表的结构,并且希望我们从Modifier的 companion实现开始进行构建。

    其实,结合注释,我们可以知道Modifier确实会组成一个链表,并且 any 和 all 是对链表的元素运行判断表达式。

    Modifier companion实现赏析

    再回过头来看 companion实现thenfoldInfoldOut 都是给啥返回啥, 再结合先前的接口默认实现,我们可以推断: 正常使用的话,最终的链表中不包含 companion实现 ,这从它的 any 和 all 的实现也可见一斑。

    很显然这是一个有意思的技巧,这里不做过多解析,但既然我这样描述,一定可以让它进入链表中的

    CombinedModifier 实现

    package androidx.compose.ui

    import androidx.compose.runtime.Stable

    class CombinedModifier(
    private val outer: Modifier,
    private val inner: Modifier
    ) : Modifier {
    override fun foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
    inner.foldIn(outer.foldIn(initial, operation), operation)

    override fun foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
    outer.foldOut(inner.foldOut(initial, operation), operation)

    override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
    outer.any(predicate) || inner.any(predicate)

    override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
    outer.all(predicate) && inner.all(predicate)

    override fun equals(other: Any?): Boolean =
    other is CombinedModifier && outer == other.outer && inner == other.inner

    override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()

    override fun toString() = "[" + foldIn("") { acc, element ->
    if (acc.isEmpty()) element.toString() else "$acc, $element"
    } + "]"
    }

    目前依旧缺乏有效的信息来解读 foldIn 和 foldOut 最终会干点啥,但可以看出其执行的次序,另外可以看出 any 和 all 没啥幺蛾子。

    看完 Modifier.Element 之后我们赏析下 foldIn 和 foldOut的递归

    Modifier.Element

    不出意外,SDK内部的各种修饰效果都将实现这一接口,同样没啥幺蛾子。

    package androidx.compose.ui

    interface Modifier {
    //...

    interface Element : Modifier {
    override fun foldIn(initial: R, operation: (R, Element) -> R): R =
    operation(initial, this)

    override fun foldOut(initial: R, operation: (Element, R) -> R): R =
    operation(this, initial)

    override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)

    override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
    }
    }

    foldIn 和 foldOut 赏析

    这里举一个栗子来看 foldIn 和 foldOut 的递归:

    class A : Modifier.Element
    class B : Modifier.Element
    class C : Modifier.Element

    fun Modifier.a() = this.then(A())
    fun Modifier.b() = this.then(B())
    fun Modifier.c() = this.then(C())

    那么 Modifier.a().b().c() 的到的是什么呢?为了看起来直观点,我们 以 CM 代指 CombinedModifier

    CM (
    outer = CM (
    outer = A(),
    inner = B()
    ),
    inner = C()
    )

    结合前面阅读源码获得的知识,我们再假设一个operation:

    val initial = StringBuilder()
    val operation: (StringBuilder, Element) -> StringBuilder = { builder, e ->
    builder.append(e.toString()).append(";")
    builder
    }

    显然:

    Modifier.a().b().c().foldIn(initial, operation)

    所得到的执行过程为:

    val ra = operation.invoke(initial,A())
    val rb = operation.invoke(ra,B())
    return operation.invoke(rb,C())

    从链表的头部执行到链表的尾部。

    而foldOut 则相反,从链表的尾部执行到链表的头部。

    当然,真正使用时,我们不一定会一直返回 initial。 但这和Modifier没啥关系,只影响到你对哪个对象使用Modifier。

    SDK中既有的Modifier实现概览

    上文中,我们在 Modifier的源码和设计细节 上花费了很长的篇幅,相信各位读者也已经彻底理解,下面我们看点轻松的。

    很显然,下面这部分内容 混个脸熟 即可,就像在Android中的原生布局,一时间遗忘了布局属性的具体拼写也无伤大雅,借助SDK文档可以很快的查询到, 但是 不知道有这些属性 就会影响到开发了。

    三个重要的包

    • androidx.compose.foundation.layout: Modifier和布局相关的扩展
    • androidx.compose.ui.draw: Modifier和绘制相关的扩展
    • androidx.compose.foundation:Modifier的基础包,其中扩展部分主要为点击时间、背景、滑动等

    API文档的内容是很枯燥的,如果读者仅仅是打算先混个脸熟,可以泛读下文内容,如果打算仔细的结合API文档进行研究,可以Fork 我的WorkShop项目 ,将源码和效果对照起来

    foundation-layout库 -- androidx.compose.foundation.layout

    具体的API列表和描述见 Api文档

    这个包中,和布局相关,诸如:尺寸、边距、盒模型等,很显然,其中的内容非常的多。关于Modifier的内容,我们不罗列API。

    正如同 DSL 的设计初衷,对于Compose而言,了解Android原生开发的同学,或者对前端领域有一丁点了解的同学,70%的DSL-API可以一眼看出其含义, 而剩下来的部分,多半需要实际测试下效果。

    ui库 -- androidx.compose.ui.draw

    这部分大多和绘制相关,类比Android原生技术栈,部分内容是比较深入的,是 自定义时 使用的 工具,所幸这部分API不太多,我们花费一屏来罗列下, 混个脸熟。

    具体的API列表和描述见 Api文档

    • 透明度

    Modifier.alpha(alpha: Float)

    • 按形状裁切

    Modifier.clip(shape: Shape)

    • 按照指定的边界裁切内容, 类似Android中的子View内容不超过父View

    Modifier.clipToBounds()

    Clip the content to the bounds of a layer defined at this modifier.

    • 在此之后进行一次指定的绘制

    Modifier.drawBehind(onDraw: DrawScope.() -> Unit)

    Draw into a Canvas behind the modified content.

    • 基于缓存绘制, 用于尺寸未发生变化,状态未发生变化时

    Modifier.drawWithCache(onBuildDrawCache: CacheDrawScope.() -> DrawResult)

    • 人为控制在布局之前或者之后进行指定的绘制

    Modifier.drawWithContent(onDraw: ContentDrawScope.() -> Unit)

    • 利用Painter 进行绘制

    Modifier.paint(painter: Painter, sizeToIntrinsics: Boolean, alignment: Alignment, contentScale: ContentScale, alpha: Float, colorFilter: ColorFilter?)

    • 围绕中心进行旋转

    Modifier.rotate(degrees: Float)

    • 缩放

    Modifier.scale(scaleX: Float, scaleY: Float)

    • 等比缩放

    Modifier.scale(scale: Float)

    • 绘制阴影

    Modifier.shadow(elevation: Dp, shape: Shape, clip: Boolean)

    foundation库 -- androidx.compose.foundation

    所幸这部分也不太多,罗列下

    • 设置背景

    Modifier.background(color: Color, shape: Shape = RectangleShape)

    Modifier.background(brush: Brush, shape: Shape = RectangleShape, alpha: Float = 1.0f)

    Brush 是渐变的,Color是纯色的

    • 设置边界,即描边效果

    Modifier.border(border: BorderStroke, shape: Shape = RectangleShape)

    Modifier.border(width: Dp, color: Color, shape: Shape = RectangleShape)

    Modifier.border(width: Dp, brush: Brush, shape: Shape)

    • 点击效果

    Modifier.clickable(enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: () -> Unit = null, onDoubleClick: () -> Unit = null, onClick: () -> Unit)

    Modifier.clickable(enabled: Boolean = true, interactionState: InteractionState, indication: Indication?, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: () -> Unit = null, onDoubleClick: () -> Unit = null, onClick: () -> Unit)

    长按、单击、双击均包含在内

    • 可滑动

    Modifier.horizontalScroll(state: ScrollState, enabled: Boolean = true, reverseScrolling: Boolean = false)

    Modifier.verticalScroll(state: ScrollState, enabled: Boolean = true, reverseScrolling: Boolean = false)

    结语

    这篇博客算是正式开始学习Jetpack Compose。

    这是一个全新的内容,要真正的全面掌握还需要积累很多的知识,就如同最开始入门Android开发那样,各类控件的使用都需要学习和记忆

    但它也仅局限于:一种新的声明式、响应式UI构建框架,并不用过于畏惧,虽然有一定的上手成本,但还没有颠覆整个Android客户端的开发方式。

    另:WorkShop中的演示代码会跟随整个Compose系列的问题,我是兴致来了就更新一部分,这意味着可能会出现:有些效果博客中提到了,但WorkShop中没有写进去


    收起阅读 »

    JNI 与 NDK 入门

    JNI概念JNI是Java Native Interface的简写,它可以使Java与其他语言(如C、C++)进行交互。它是Java调用Native语言的一种特性,属于Java语言的范畴,与Android无关。为何需要JNIJava的源文件非常容易被反编译,而...
    继续阅读 »

    JNI

    概念

    JNI是Java Native Interface的简写,它可以使Java与其他语言(如C、C++)进行交互。

    它是Java调用Native语言的一种特性,属于Java语言的范畴,与Android无关。

    为何需要JNI

    • Java的源文件非常容易被反编译,而通过Native语言生成的.so库文件则不容易被反编译。
    • 有时我们使用Java时需要使用到一些库来实现功能,但这些库仅仅提供了一些Native语言的接口。
    • 使用Native语言编写的代码运行效率高,尤其体现在音频视频图片的处理等需要大量复杂运算的操作上。充分利用了硬件的性能。

    由于上述原因,此时我们就需要让Java与Native语言交互。而由于Java的特点,与Native语言的交互能力很弱。因此在此时,我们就需要用到JNI特性增强Java与Native方法的交互能力。

    实现的步骤

    1. 在Java中声明Native方法(需要调用的本地方法)
    2. 通过 javac 编译 Java源文件( 生成.class文件)
    3. 通过 javah 命令生成JNI头文件(生成.h文件)
    4. 通过Native语言实现在Java源码中声明的Native方法
    5. 编译成.so库文件
    6. 通过Java命令执行 Java程序,最终实现Java调用本地代码(借助so库文件)

    NDK

    概念

    Native是Native Development Kit的简写,是Android的开发工具包,属于Android,与Java无关系。

    它可以快速开发C/C++的动态库,自动将.so和应用一起打包为APK。因此我们可以通过NDK来在Android开发中通过JNI与Native方法交互。

    使用方式

    1. 配置 Android NDK环境(在SDK Manager中下载NDK、CMake、LLDB)
    2. 创建 Android 项目,与 NDK进行关联(创建项目时选择C++ support)
    3. 在 Android 项目中声明所需要调用的 Native方法
    4. 用Native语言实现在Android中声明的Native方法
    5. 通过 ndk-bulid 命令编译产生.so库文件

    将Android项目与NDK关联

    配置NDK路径

    local.properties中加入如下一行即可

    ndk.dir=

    添加配置

    在Gradle的 gradle.properties中加入如下一行,目的是对旧版本的NDK支持

    android.useDeprecatedNdk=true

    添加ndk节点

    在build.gradle中的defaultConfigandroid中加入如下的externalNativeBuild节点

    apply plugin: 'com.android.application'



    android {

    compileSdkVersion 27

    defaultConfig {

    applicationId "com.n0texpecterr0r.ndkdemo"

    minSdkVersion 19

    targetSdkVersion 27

    versionCode 1

    versionName "1.0"

    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    externalNativeBuild {

    cmake {

    cppFlags ""

    }

    }

    }

    buildTypes {

    release {

    minifyEnabled false

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

    }

    }

    externalNativeBuild {

    cmake {

    path "CMakeLists.txt"

    }

    }

    }



    dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:27.1.1'

    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test1.0.2'

    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    }

    开发Native代码

    在Java文件中声明native方法

    我们首先需要在Java代码的类中通过static块来加载我们的Native库。可以通过如下代码,其中loadLibrary的参数是在CMakeList.txt中定义的Native库的名称

    static {

    System.loadLibrary("native-lib");

    }

    之后,我们便可以在这个类中声明Native方法

    public native String getStringFromJNI();

    创建CMakeList.txt

    我们还需要在src中创建一个CMakeList.txt文件,这个文件约束了Native语言源文件的编译规则。比如下面

    cmake_minimum_required(VERSION 3.4.1)



    add_library(native-lib SHARED src/main/cpp/native-lib.cpp)



    find_library(log-lib log)



    target_link_libraries(native-lib ${log-lib})

    add_library方法中定义了一个so库,它的名称是native-lib,也就是我们在Java文件中用到的字符串,而后面则跟着这个库对应的Native文件的路径

    find_library则是定义了一个路径变量,经过了这个方法,log-lib这个变量中的值就是Android中log库的路径

    target_link_libraries则是将native-lib这个库和log库连接了起来,这样我们就能在native-lib中使用log库的方法。

    创建Native方法文件

    在前面的CMake文件中可以看到,我们把文件放在了src/main/cpp/,因此我们创建cpp这个目录,在里面创建C++源文件native-lib.cpp。

    然后, 我们便可以开始编写如下的代码:

    #include 

    #include



    extern "C"{

    JNIEXPORT jstring JNICALL

    Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI(

    JNIEnv* env,

    jobject) {

    std::string hello = "IG牛逼";

    return env->NewStringUTF(hello.c_str());

    }

    }

    此处我们使用的是C++语言,让我们来看看具体的代码。

    首先我们引入了jni需要的jni.h,这个头文件中声明了各个jni需要用到的函数。同时我们引入了C++中的string.h。

    然后我们看到extern "C"。为了了解这里为什么使用了extern "C",我们首先需要知道下面的知识:

    在C中,编译时的函数签名仅仅是包含了函数的名称,因此不同参数的函数都是同样的签名。这也就是为什么C不支持重载。

    而C++为了支持重载,在编译的时候函数的签名除了包含函数的名称,还携带了函数的参数及返回类型等等。

    试想此时我们有个C的函数库要给C++调用,会因为签名的不同而找不到对应的函数。因此,我们需要使用extern "C"来告诉编译器使用编译C的方式来连接。

    接下来我们看看JNIEXPORT和JNICALL关键字,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数。

    而jstring则对应了Java中的String类,JNI中有很多类似jstring的类来对应Java中的类,下面是Java中的类与JNI类型的对照表

    我们继续看到函数名Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI。其实函数名中的_相当于Java中的 . 也就是这个函数名代表了java.com.n0texpecterr0r.ndkdemo.MainActivity.java中的getStringFromJNI方法,也就是我们之前定义的native方法。

    格式大概如下:

    Java_包名_类名_需要调用的方法名

    其中,Java必须大写,包名里的.要改成__要改成_1

    接下来我们看到这个函数的两个参数:

    • JNIEnv* env:代表了JVM的环境,Native方法可以通过这个指针来调用Java代码
    • jobject obj:它就相当于定义了这个JNI方法的类 (MainActivity) 的this引用

    然后可以看到后面我们创建了一个string hello,之后通过env->NewStringUTF(hello.c_str())方法创建了一个jstring类型的变量并返回。

    在Java代码中调用native方法

    接着,我们便可以在MainActivty中像调用Java方法一样调用这个native方法

    TextView tv = findViewById(R.id.sample_text);

    tv.setText(getStringFromJNI());

    我们尝试运行,可以看到,我们成功用C++构建了一个字符串并返回给Java调用:

    image.png

    CMake

    我们在NDK开发中使用CMake的语法来编写简单的代码描述编译的过程,由于这篇文章是讲NDK的,所以关于CMake的语法就不再赘述了。。。如果想要了解CMake语法可以学习这本书《CMake Practice

    JNI与Java代码交互

    方法签名

    概念

    在我们JNI层调用一个方法时,需要传递一个参数——方法签名。

    为什么要使用方法签名呢?因为在Java中的方法是可以重载的,两个方法可能名称相同而参数不同。为了区分调用的方法,就引入了方法签名的概念。

    签名规则

    对于基本类型的参数,每个类型对应了一个不同的字母:

    • boolean Z
    • byte B
    • char C
    • short S
    • int I
    • long J
    • float F
    • double D
    • void V

    对于类,则使用 L+类名 的方式,其中(.)用(/)代替,最后加上分号

    比如 java.lang.String就是 Ljava/lang/String;

    对于数组,则在前面加 [ ,然后加类型的签名,几维数组就加几个。

    比如 int[]对应的就是[I , boolean[][]对应的则是[[Z,而java.lang.String[]就是[Ljava/lang/String;

    打印方法签名

    我们可以通过 javap -s 命令来打印方法的签名。

    例子

    比如下面的方法

    public native String getMessage();



    public native String getMessage(String id,long i);

    对应的方法签名分别为:

    ()Ljava/lang/String;

    (Ljava/long/String;J)Ljava/lang/String;

    可以看到,前面括号中表示的是方法的参数列表,后面表示的则是返回值。

    收起阅读 »

    Android自定义view之围棋动画

    Android自定义view之围棋动画好久不见,最近粉丝要求上新一篇有点难度的自定义view文章,所以它来了!!干货文,建议收藏前言废话不多说直接开始文章最后有源码完成效果图棋子加渐变色棋子不加渐变色一、测量1.获取宽高 @Override prote...
    继续阅读 »

    Android自定义view之围棋动画

    好久不见,最近粉丝要求上新一篇有点难度的自定义view文章,所以它来了!!


    干货文,建议收藏

    前言

    废话不多说直接开始


    文章最后有源码

    完成效果图

    棋子加渐变色

    在这里插入图片描述

    棋子不加渐变色

    在这里插入图片描述

    一、测量

    1.获取宽高

     @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mWidth = w;
    mHeight = h;
    useWidth = mWidth;
    if (mWidth > mHeight) {
    useWidth = mHeight;
    }
    }

    2.定义测量最小长度

    将布局分为10份。以minwidth的1,3,5,7,9的倍数为标准点。

            minwidth = useWidth / 10;

    二、绘制背景(棋盘)

    1.初始化画笔

            mPaint = new Paint();        //创建画笔对象
    mPaint.setColor(Color.BLACK); //设置画笔颜色
    mPaint.setStyle(Paint.Style.FILL); //设置画笔模式为填充
    mPaint.setStrokeWidth(4f); //设置画笔宽度为10px
    mPaint.setAntiAlias(true); //设置抗锯齿
    mPaint.setAlpha(255); //设置画笔透明度

    2.画棋盘

            //细的X轴
    canvas.drawLine(minwidth, 3 * minwidth, 9 * minwidth, 3 * minwidth, mPaint);// 斜线
    canvas.drawLine(minwidth, 5 * minwidth, 9 * minwidth, 5 * minwidth, mPaint);// 斜线
    canvas.drawLine(minwidth, 7 * minwidth, 9 * minwidth, 7 * minwidth, mPaint);// 斜线
    //细的y轴
    canvas.drawLine(3 * minwidth, minwidth, 3 * minwidth, 9 * minwidth, mPaint);// 斜线
    canvas.drawLine(5 * minwidth, minwidth, 5 * minwidth, 9 * minwidth, mPaint);// 斜线
    canvas.drawLine(7 * minwidth, minwidth, 7 * minwidth, 9 * minwidth, mPaint);// 斜线
    mPaint.setStrokeWidth(8f);
    //粗的X轴(边框)
    canvas.drawLine(minwidth, minwidth, 9 * minwidth, minwidth, mPaint);// 斜线
    canvas.drawLine(minwidth, 9 * minwidth, 9 * minwidth, 9 * minwidth, mPaint);// 斜线
    //粗的y轴(边框)
    canvas.drawLine(minwidth, minwidth, minwidth, 9 * minwidth, mPaint);// 斜线
    canvas.drawLine(9 * minwidth, minwidth, 9 * minwidth, 9 * minwidth, mPaint);// 斜线

    绘制完后,发现有点小瑕疵

    效果图:

    在这里插入图片描述

    3.补棋盘瑕疵

            canvas.drawPoint(minwidth, minwidth, mPaint);
    canvas.drawPoint(9 * minwidth, minwidth, mPaint);
    canvas.drawPoint(minwidth, 9 * minwidth, mPaint);
    canvas.drawPoint(9 * minwidth, 9 * minwidth, mPaint);

    效果图:

    在这里插入图片描述

    三.画个不可改变的棋子(以便于了解动画移动位置)

    位置比例 (3,3)(3,5)(3,7) (5,3)(5,5)(5,7) (7,3)(7,5)(7,7)

            //画围棋
    canvas.drawCircle(3*minwidth, 3*minwidth, useWidth/16, mPaint);
    canvas.drawCircle(3*minwidth, 7*minwidth, useWidth/16, mPaint);
    canvas.drawCircle(5*minwidth, 5*minwidth, useWidth/16, mPaint);
    canvas.drawCircle(7*minwidth, 3*minwidth, useWidth/16, mPaint);
    canvas.drawCircle(7*minwidth, 7*minwidth, useWidth/16, mPaint);
    mPaint.setColor(rightcolor);
    canvas.drawCircle(3*minwidth, 5*minwidth, useWidth/16, mPaint);
    canvas.drawCircle(5*minwidth, 3*minwidth, useWidth/16, mPaint);
    canvas.drawCircle(5*minwidth, 7*minwidth, useWidth/16, mPaint);
    canvas.drawCircle(7*minwidth, 5*minwidth, useWidth/16, mPaint);

    效果图:

    在这里插入图片描述

    四.为动画开始做准备以及动画

    1.三个辅助类为动画做准备(参数模仿Android官方Demo)

    主要为get set构造,代码会贴到最后

    2.自定义该接口实例来控制动画的更新计算表达式

    public class XYEvaluator implements TypeEvaluator {
    public Object evaluate(float fraction, Object startValue, Object endValue) {
    XYHolder startXY = (XYHolder) startValue;
    XYHolder endXY = (XYHolder) endValue;
    return new XYHolder(startXY.getX() + fraction * (endXY.getX() - startXY.getX()),
    startXY.getY() + fraction * (endXY.getY() - startXY.getY()));
    }
    }

    3.棋子的创建

        private ShapeHolder createBall(float x, float y, int color) {
    OvalShape circle = new OvalShape();
    circle.resize(useWidth / 8f, useWidth / 8f);
    ShapeDrawable drawable = new ShapeDrawable(circle);
    ShapeHolder shapeHolder = new ShapeHolder(drawable);
    shapeHolder.setX(x - useWidth / 16f);
    shapeHolder.setY(y - useWidth / 16f);
    Paint paint = drawable.getPaint();
    paint.setColor(color);
    return shapeHolder;
    }

    4.动画的创建

        private void createAnimation() {
    if (bounceAnim == null) {
    XYHolder lstartXY = new XYHolder(3 * minwidth - useWidth / 16f, 3 * minwidth - useWidth / 16f);
    XYHolder processXY = new XYHolder(7 * minwidth - useWidth / 16f, 3 * minwidth - useWidth / 16f);
    XYHolder lendXY = new XYHolder(7 * minwidth - useWidth / 16f, 7 * minwidth - useWidth / 16f);
    bounceAnim = ObjectAnimator.ofObject(ballHolder, "xY",
    new XYEvaluator(), lstartXY, processXY, lendXY, lstartXY);
    bounceAnim.setDuration(animaltime);
    bounceAnim.setRepeatCount(ObjectAnimator.INFINITE);
    bounceAnim.setRepeatMode(ObjectAnimator.RESTART);
    bounceAnim.addUpdateListener(this);
    }
    if (bounceAnim1 == null) {
    XYHolder lstartXY = new XYHolder(7 * minwidth - useWidth / 16f, 7 * minwidth - useWidth / 16f);
    XYHolder processXY = new XYHolder(3 * minwidth - useWidth / 16f, 7 * minwidth - useWidth / 16f);
    XYHolder lendXY = new XYHolder(3 * minwidth - useWidth / 16f, 3 * minwidth - useWidth / 16f);
    bounceAnim1 = ObjectAnimator.ofObject(ballHolder1, "xY",
    new XYEvaluator(), lstartXY, processXY, lendXY, lstartXY);
    bounceAnim1.setDuration(animaltime);
    bounceAnim1.setRepeatCount(ObjectAnimator.INFINITE);
    bounceAnim1.setRepeatMode(ObjectAnimator.RESTART);
    bounceAnim1.addUpdateListener(this);
    }
    }

    5.两个动画的同步执行

            AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.play(bounceAnim).with(bounceAnim1);
    animatorSet.start();

    6.效果图

    在这里插入图片描述

    视觉效果:感觉白子不太明显

    7.解决第6步问题

    在棋子的创建方法中添加渐变色

            RadialGradient gradient = new RadialGradient(useWidth / 16f, useWidth / 16f,
    useWidth / 8f, color, Color.GRAY, Shader.TileMode.CLAMP);
    paint.setShader(gradient);
    shapeHolder.setPaint(paint);

    效果图:

    在这里插入图片描述

    五.自定义属性

    attrs文件:

        <declare-styleable name="WeiqiView">
    <!-- 黑子颜色-->
    <attr name="leftscolor" format="reference|color"/>
    <!-- 白子颜色-->
    <attr name="rightscolor" format="reference|color"/>
    <!-- 棋盘颜色-->
    <attr name="qipancolor" format="reference|color"/>
    <!-- 动画时间-->
    <attr name="animalstime" format="integer"/>
    </declare-styleable>

    java文件中获取

        /**
    * 获取自定义属性
    */

    private void initCustomAttrs(Context context, AttributeSet attrs) {
    //获取自定义属性
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.WeiqiView);
    //获取颜色
    leftcolor = ta.getColor(R.styleable.WeiqiView_leftscolor, Color.BLACK);
    rightcolor = ta.getColor(R.styleable.WeiqiView_rightscolor, Color.WHITE);
    qipancolor = ta.getColor(R.styleable.WeiqiView_qipancolor, Color.BLACK);
    //获取动画时间
    animaltime = ta.getInt(R.styleable.WeiqiView_animalstime, 2000);
    //回收
    ta.recycle();

    }

    六.自定义属性设置后运行效果

    在这里插入图片描述

    七.小改变,视觉效果就不一样了!

    然后,把背景注释,像不像那些等待动画?

    在这里插入图片描述

    八.源码

    WeiqiView.java

    public class WeiqiView extends View implements ValueAnimator.AnimatorUpdateListener {
    private Paint mPaint;
    private int mWidth;
    private int mHeight;
    private int useWidth, minwidth;
    private int leftcolor;
    private int rightcolor;
    private int qipancolor;
    private int animaltime;
    //画一个圆(棋子)
    ValueAnimator bounceAnim, bounceAnim1 = null;
    ShapeHolder ball, ball1 = null;
    QiziXYHolder ballHolder, ballHolder1 = null;

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public WeiqiView(Context context) {
    this(context, null);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public WeiqiView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public WeiqiView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
    initCustomAttrs(context, attrs);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public WeiqiView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);


    }

    private void init() {
    initPaint();
    }


    /**
    * 获取自定义属性
    */

    private void initCustomAttrs(Context context, AttributeSet attrs) {
    //获取自定义属性。
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.WeiqiView);
    //获取颜色
    leftcolor = ta.getColor(R.styleable.WeiqiView_leftscolor, Color.BLACK);
    rightcolor = ta.getColor(R.styleable.WeiqiView_rightscolor, Color.WHITE);
    qipancolor = ta.getColor(R.styleable.WeiqiView_qipancolor, Color.BLACK);
    animaltime = ta.getInt(R.styleable.WeiqiView_animalstime, 2000);
    //回收
    ta.recycle();

    }

    /**
    * 初始化画笔
    */

    private void initPaint() {
    mPaint = new Paint(); //创建画笔对象
    mPaint.setColor(Color.BLACK); //设置画笔颜色
    mPaint.setStyle(Paint.Style.FILL); //设置画笔模式为填充
    mPaint.setStrokeWidth(4f); //设置画笔宽度为10px
    mPaint.setAntiAlias(true); //设置抗锯齿
    mPaint.setAlpha(255); //设置画笔透明度
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mWidth = w;
    mHeight = h;
    useWidth = mWidth;
    if (mWidth > mHeight) {
    useWidth = mHeight;
    }
    }


    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    init();
    minwidth = useWidth / 10;
    mPaint.setColor(qipancolor);
    if (ball == null) {
    ball = createBall(3 * minwidth, 3 * minwidth, leftcolor);
    ballHolder = new QiziXYHolder(ball);
    }
    if (ball1 == null) {
    ball1 = createBall(7 * minwidth, 7 * minwidth, rightcolor);
    ballHolder1 = new QiziXYHolder(ball1);
    }
    //细的X轴
    canvas.drawLine(minwidth, 3 * minwidth, 9 * minwidth, 3 * minwidth, mPaint);// 斜线
    canvas.drawLine(minwidth, 5 * minwidth, 9 * minwidth, 5 * minwidth, mPaint);// 斜线
    canvas.drawLine(minwidth, 7 * minwidth, 9 * minwidth, 7 * minwidth, mPaint);// 斜线
    //细的y轴
    canvas.drawLine(3 * minwidth, minwidth, 3 * minwidth, 9 * minwidth, mPaint);// 斜线
    canvas.drawLine(5 * minwidth, minwidth, 5 * minwidth, 9 * minwidth, mPaint);// 斜线
    canvas.drawLine(7 * minwidth, minwidth, 7 * minwidth, 9 * minwidth, mPaint);// 斜线
    mPaint.setStrokeWidth(8f);
    //粗的X轴(边框)
    canvas.drawLine(minwidth, minwidth, 9 * minwidth, minwidth, mPaint);// 斜线
    canvas.drawLine(minwidth, 9 * minwidth, 9 * minwidth, 9 * minwidth, mPaint);// 斜线
    //粗的y轴(边框)
    canvas.drawLine(minwidth, minwidth, minwidth, 9 * minwidth, mPaint);// 斜线
    canvas.drawLine(9 * minwidth, minwidth, 9 * minwidth, 9 * minwidth, mPaint);// 斜线
    //补瑕疵
    canvas.drawPoint(minwidth, minwidth, mPaint);
    canvas.drawPoint(9 * minwidth, minwidth, mPaint);
    canvas.drawPoint(minwidth, 9 * minwidth, mPaint);
    canvas.drawPoint(9 * minwidth, 9 * minwidth, mPaint);
    // //画围棋
    // canvas.drawCircle(3*minwidth, 3*minwidth, useWidth/16, mPaint);
    // canvas.drawCircle(3*minwidth, 7*minwidth, useWidth/16, mPaint);
    // canvas.drawCircle(5*minwidth, 5*minwidth, useWidth/16, mPaint);
    // canvas.drawCircle(7*minwidth, 3*minwidth, useWidth/16, mPaint);
    // canvas.drawCircle(7*minwidth, 7*minwidth, useWidth/16, mPaint);
    // mPaint.setColor(rightcolor);
    // canvas.drawCircle(3*minwidth, 5*minwidth, useWidth/16, mPaint);
    // canvas.drawCircle(5*minwidth, 3*minwidth, useWidth/16, mPaint);
    // canvas.drawCircle(5*minwidth, 7*minwidth, useWidth/16, mPaint);
    // canvas.drawCircle(7*minwidth, 5*minwidth, useWidth/16, mPaint);

    canvas.save();
    canvas.translate(ball.getX(), ball.getY());
    ball.getShape().draw(canvas);
    canvas.restore();

    canvas.save();
    canvas.translate(ball1.getX(), ball1.getY());
    ball1.getShape().draw(canvas);
    canvas.restore();
    }

    private ShapeHolder createBall(float x, float y, int color) {
    OvalShape circle = new OvalShape();
    circle.resize(useWidth / 8f, useWidth / 8f);
    ShapeDrawable drawable = new ShapeDrawable(circle);
    ShapeHolder shapeHolder = new ShapeHolder(drawable);
    shapeHolder.setX(x - useWidth / 16f);
    shapeHolder.setY(y - useWidth / 16f);
    Paint paint = drawable.getPaint();
    paint.setColor(color);
    RadialGradient gradient = new RadialGradient(useWidth / 16f, useWidth / 16f,
    useWidth / 8f, color, Color.GRAY, Shader.TileMode.CLAMP);
    paint.setShader(gradient);
    shapeHolder.setPaint(paint);
    return shapeHolder;
    }

    private void createAnimation() {
    if (bounceAnim == null) {
    XYHolder lstartXY = new XYHolder(3 * minwidth - useWidth / 16f, 3 * minwidth - useWidth / 16f);
    XYHolder processXY = new XYHolder(7 * minwidth - useWidth / 16f, 3 * minwidth - useWidth / 16f);
    XYHolder lendXY = new XYHolder(7 * minwidth - useWidth / 16f, 7 * minwidth - useWidth / 16f);
    bounceAnim = ObjectAnimator.ofObject(ballHolder, "xY",
    new XYEvaluator(), lstartXY, processXY, lendXY, lstartXY);
    bounceAnim.setDuration(animaltime);
    bounceAnim.setRepeatCount(ObjectAnimator.INFINITE);
    bounceAnim.setRepeatMode(ObjectAnimator.RESTART);
    bounceAnim.addUpdateListener(this);
    }
    if (bounceAnim1 == null) {
    XYHolder lstartXY = new XYHolder(7 * minwidth - useWidth / 16f, 7 * minwidth - useWidth / 16f);
    XYHolder processXY = new XYHolder(3 * minwidth - useWidth / 16f, 7 * minwidth - useWidth / 16f);
    XYHolder lendXY = new XYHolder(3 * minwidth - useWidth / 16f, 3 * minwidth - useWidth / 16f);
    bounceAnim1 = ObjectAnimator.ofObject(ballHolder1, "xY",
    new XYEvaluator(), lstartXY, processXY, lendXY, lstartXY);
    bounceAnim1.setDuration(animaltime);
    bounceAnim1.setRepeatCount(ObjectAnimator.INFINITE);
    bounceAnim1.setRepeatMode(ObjectAnimator.RESTART);
    bounceAnim1.addUpdateListener(this);
    }
    }

    public void startAnimation() {
    createAnimation();
    AnimatorSet animatorSet = new AnimatorSet();
    animatorSet.play(bounceAnim).with(bounceAnim1);
    animatorSet.start();
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    invalidate();
    }
    }

    QiziXYHolder.java

    public class QiziXYHolder {

    private ShapeHolder mBall;

    public QiziXYHolder(ShapeHolder ball) {
    mBall = ball;
    }

    public void setXY(XYHolder xyHolder) {
    mBall.setX(xyHolder.getX());
    mBall.setY(xyHolder.getY());
    }

    public XYHolder getXY() {
    return new XYHolder(mBall.getX(), mBall.getY());
    }
    }

    ShapeHolder.java

    public class ShapeHolder {
    private float x = 0, y = 0;
    private ShapeDrawable shape;
    private int color;
    private RadialGradient gradient;
    private float alpha = 1f;
    private Paint paint;

    public void setPaint(Paint value) {
    paint = value;
    }
    public Paint getPaint() {
    return paint;
    }

    public void setX(float value) {
    x = value;
    }
    public float getX() {
    return x;
    }
    public void setY(float value) {
    y = value;
    }
    public float getY() {
    return y;
    }
    public void setShape(ShapeDrawable value) {
    shape = value;
    }
    public ShapeDrawable getShape() {
    return shape;
    }
    public int getColor() {
    return color;
    }
    public void setColor(int value) {
    shape.getPaint().setColor(value);
    color = value;
    }
    public void setGradient(RadialGradient value) {
    gradient = value;
    }
    public RadialGradient getGradient() {
    return gradient;
    }

    public void setAlpha(float alpha) {
    this.alpha = alpha;
    shape.setAlpha((int)((alpha * 255f) + .5f));
    }

    public float getWidth() {
    return shape.getShape().getWidth();
    }
    public void setWidth(float width) {
    Shape s = shape.getShape();
    s.resize(width, s.getHeight());
    }

    public float getHeight() {
    return shape.getShape().getHeight();
    }
    public void setHeight(float height) {
    Shape s = shape.getShape();
    s.resize(s.getWidth(), height);
    }

    public ShapeHolder(ShapeDrawable s) {
    shape = s;
    }
    }

    XYEvaluator.java

    public class XYEvaluator implements TypeEvaluator {
    public Object evaluate(float fraction, Object startValue, Object endValue) {
    XYHolder startXY = (XYHolder) startValue;
    XYHolder endXY = (XYHolder) endValue;
    return new XYHolder(startXY.getX() + fraction * (endXY.getX() - startXY.getX()),
    startXY.getY() + fraction * (endXY.getY() - startXY.getY()));
    }
    }

    XYHolder.java

    public class XYHolder {
    private float mX;
    private float mY;

    public XYHolder(float x, float y) {
    mX = x;
    mY = y;
    }

    public float getX() {
    return mX;
    }

    public void setX(float x) {
    mX = x;
    }

    public float getY() {
    return mY;
    }

    public void setY(float y) {
    mY = y;
    }
    }

    attrs.xml

    <resources>
    <declare-styleable name="WeiqiView">
    <!-- 黑子颜色-->
    <attr name="leftscolor" format="reference|color"/>
    <!-- 白子颜色-->
    <attr name="rightscolor" format="reference|color"/>
    <!-- 棋盘颜色-->
    <attr name="qipancolor" format="reference|color"/>
    <!-- 动画时间-->
    <attr name="animalstime" format="integer"/>
    </declare-styleable>
    </resources>

    布局调用

    <com.shenzhen.jimeng.lookui.UI.WeiqiView
    android:layout_centerInParent="true"
    android:id="@+id/weiqi"
    android:layout_width="400dp"
    android:layout_height="400dp"/>

    activity文件中开启动画

         weiqi = (WeiqiView) findViewById(R.id.weiqi);
    weiqi.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    weiqi.startAnimation();
    }
    });

    收起阅读 »

    Handler 源码分析

    Handler源码的学习理解一.相关类说明1.Handler作用①实现线程切换,可以在A个线程使用B线程创建的Handler发送消息,然后在B线程的Handler handleMessage回调中接收A线程的消息。②实现发送延时消息 hanlder.postD...
    继续阅读 »

    Handler源码的学习理解

    一.相关类说明

    1.Handler作用

    ①实现线程切换,可以在A个线程使用B线程创建的Handler发送消息,然后在B线程的Handler handleMessage回调中接收A线程的消息。

    ②实现发送延时消息 hanlder.postDelay(),sendMessageDelayed()

    2.Message

    消息的载体,包含发送的handler对象等信息,Message本身是一个单链表结构,里面维护了next Message对象;内部维护了一个Message Pool,使用时调用Message.obtain()方法来从Message 池子中获取对象,可以让我们在许多情况下避免new对象,减少内存开,Message种类有普通message,异步Message,同步屏障Message。

    3.MessageQueue

    MessageQueue:一个优先级队列,内部是一个按Message.when从小到大排列的Message单链表;可以通过enqueueMessage()添加Message,通过next()取出Message,但是必须配合Handler与Looper来实现这个过程。

    4.Looper

    通过Looper.prepare()去创建一个Looper,调用Looper.loop()去不断轮询MessageQueue队列,调用MessageQueue 的next()方法直到取出Msg,然后回调msg.target.dispatchMessage(msg);实现消息的取出与分发。

    二.原理分析

    我们在MainActivity的成员变量初始化一个Handler,然后子线程通过handler post/send msg发送一个消息 ,最后我们在主线程的Handler回调handleMessage(msg: Message)拿到我们的消息。理解了这个从发送到接收的过程,Handler原理就掌握的差不多了。下面来分析这个过程:

    1.发送消息

    handler post/send msg 对象到 MessageQueue ,无论调用Handler哪个发送方法,最后都会调用到Handler.enqueueMessage()方法:

     private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {
    //handler enqueneMessage 将handler赋值给msg.target,为了区分消息队列的消息属于哪个handler发送的
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
    msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
    }
    复制代码

    然后再去调用MessageQueue.enqueueMessage(),将Message按照message 的when参数的从小到大的顺序插入到MessageQueue中.MessageQueue是一个优先级队列,内部是以单链表的形式存储Message的。这样我们完成了消息发送到MessageQueue的步骤。

    boolean enqueueMessage(Message msg, long when) {
    //、、、、、、
    msg.markInUse();
    msg.when = when;
    Message p = mMessages;
    boolean needWake;
    if (p == null || when == 0 || when < p.when) {
    //when == 0 就是直接发送的消息 将msg插到队列的头部
    // New head, wake up the event queue if blocked.
    msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
    } else {
    // Inserted within the middle of the queue. Usually we don't have to wake
    // up the event queue unless there is a barrier at the head of the queue
    // and the message is the earliest asynchronous message in the queue.
    needWake = mBlocked && p.target == null && msg.isAsynchronous();
    Message prev;
    for (;;) {
    prev = p;
    p = p.next;
    //准备插入的msg.when 时间小于队列中某个消息的when 就跳出循环
    if (p == null || when < p.when) {
    break;
    }
    if (needWake && p.isAsynchronous()) {
    needWake = false;
    }
    }
    //插入msg 到队列中这个消息的前面
    msg.next = p; // invariant: p == prev.next
    prev.next = msg;
    }

    // We can assume mPtr != 0 because mQuitting is false.
    if (needWake) {
    nativeWake(mPtr);
    }
    }
    return true;
    }
    复制代码

    2.取出消息

    我们知道消息队列MessageQueue是死的,它不会自己去取出某个消息的。所以我们需要一个让MessageQueue动起来的动力--Looper.首先创建一个Looper,代码如下:

     //Looper.java    
    // sThreadLocal.get() will return null unless you've called prepare().
    @UnsupportedAppUsage
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static void prepare(boolean quitAllowed) {
    //sThreadLocal.get() 得到Looper 不为null
    if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
    }

    //ThreadLocal.java
    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    //将thread本身最为key, looper为value存在ThreadLocalMap中
    map.set(this, value);
    else
    createMap(t, value);
    }
    复制代码

    从源码可以看出,一个线程只能存在一个Looper,接着来看looper.loop()方法,主要做了取message的工作:

    //looper.java  
    /**
    * Run the message queue in this thread. Be sure to call
    * {@link #quit()} to end the loop.
    */

    public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
    throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    //..............
    //死循环取出message
    for (;;) {
    Message msg = queue.next(); // might block 调用MessageQueue的next()方法取出消息,可能发生阻塞
    if (msg == null) {//msg == null 代表message queue 正在退出,一般不会为null
    // No message indicates that the message queue is quitting.
    return;
    }
    //...............
    try {//取出消息后,分发给对应的 msg.target 也就是handler
    msg.target.dispatchMessage(msg);
    // 、、、、、
    } catch (Exception exception) {
    // 、、、、、
    } finally {
    // 、、、、、
    }
    // 、、、、、重置属性并回收message到复用池
    msg.recycleUnchecked();
    }
    }
    //Message.java
    void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = null;
    synchronized (sPoolSync) {
    if (sPoolSize < MAX_POOL_SIZE) {
    next = sPool;
    sPool = this;
    sPoolSize++;
    }
    }
    }
    复制代码

    3.MessageQueue的next()方法

    不断的从消息队列里拿消息,如果有异步消息,先所有的异步消息放在队列的前面优先执行,然后,拿到同步消息,分发给对应的handler,在MessageQueue中没有消息要执行时即MessageQueue空闲时,如果有idelHandler则会去执行idelHandler 的任务,通过idler.queueIdle()处理任务.

      //MessageQueue.java
    Message next() {
    //........
    int nextPollTimeoutMillis = 0;
    for (;;) {
    if (nextPollTimeoutMillis != 0) {
    Binder.flushPendingCommands();
    }
    //没消息时,主线程睡眠
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
    // Try to retrieve the next message. Return if found.
    final long now = SystemClock.uptimeMillis();
    Message prevMsg = null;
    Message msg = mMessages;//mMessages保存链表的第一个元素
    if (msg != null && msg.target == null) {//当有屏障消息时,就去寻找最近的下一个异步消息

    // msg.target == null 时为屏障消息(参考MessageQueue.java 的postSyncBarrier()),找到寻找下一个异步消息
    // Stalled by a barrier. Find the next asynchronous message in the queue.
    do {
    prevMsg = msg;//记录prevMsg为异步消息的前一个同步消息

    msg = msg.next;//遍历下一个节点

    } while (msg != null && !msg.isAsynchronous());//当是同步消息时,执行循环,直到找到异步消息跳出循环


    }
    if (msg != null) {//消息处理
    if (now < msg.when) {
    // Next message is not ready. Set a timeout to wake up when it is ready.
    //下一条消息还没到执行时间,先睡眠一会儿
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
    } else {
    // Got a message.
    mBlocked = false;
    if (prevMsg != null) {//有屏障消息
    prevMsg.next = msg.next;//这个步骤保证了原来的同步消息的顺序 将异步消息前的同步消息放在异步消息后的同步消息之前执行,优先执行了异步消息

    } else {//无屏障消息直接取出这个消息,并重置MessageQueue队列的头
    mMessages = msg.next;
    }
    msg.next = null;
    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
    msg.markInUse();
    //返回队列的一个message
    return msg;
    }
    } else {
    // No more messages.
    //没消息就睡眠
    nextPollTimeoutMillis = -1;
    }

    // Process the quit message now that all pending messages have been handled.
    if (mQuitting) {
    dispose();
    return null;
    }

    // If first time idle, then get the number of idlers to run.
    // Idle handles only run if the queue is empty or if the first message
    // in the queue (possibly a barrier) is due to be handled in the future.
    if (pendingIdleHandlerCount < 0
    && (mMessages == null || now < mMessages.when)) {
    //当消息对列没有要执行的message时,去赋值pendingIdleHandlerCount
    pendingIdleHandlerCount = mIdleHandlers.size();
    }
    if (pendingIdleHandlerCount <= 0) {
    // No idle handlers to run. Loop and wait some more.
    mBlocked = true;
    continue;
    }

    if (mPendingIdleHandlers == null) {
    //创建 IdleHandler[]
    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
    }
    mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }

    // Run the idle handlers.
    // We only ever reach this code block during the first iteration.
    for (int i = 0; i < pendingIdleHandlerCount; i++) {
    final IdleHandler idler = mPendingIdleHandlers[i];
    mPendingIdleHandlers[i] = null; // release the reference to the handler

    boolean keep = false;
    try {
    //执行IdleHandle[]的每个queueIdle(),Return true to keep your idle handler active
    keep = idler.queueIdle();
    } catch (Throwable t) {
    Log.wtf(TAG, "IdleHandler threw exception", t);
    }

    if (!keep) {//mIdleHandlers 被删除了
    synchronized (this) {
    mIdleHandlers.remove(idler);
    }
    }
    }

    // Reset the idle handler count to 0 so we do not run them again.
    pendingIdleHandlerCount = 0;

    // While calling an idle handler, a new message could have been delivered
    // so go back and look again for a pending message without waiting.
    nextPollTimeoutMillis = 0;
    }
    }
    复制代码

    三.常见问题

    通过以上源码的分析,了解到Handler的工作原理,根据原理可回答出一下几个经典题目:

    1.一个线程有几个handler?

    可以有多个handler,因为可以在一个线程 new多个handler ,并且handler 发送message时,会将handler 赋值给message.target

    2.一个线程有几个looper?如何保证?

    只能有一个,否则会抛异常

    3.Handler内存泄漏原因?为什么其他的内部类没有说过这个问题?

    Handler内存泄漏的根本原因时在于延时消息,因为Handler 发送一个延时消息到MessageQueue,MessageQueue中持有的Message中持有Handler的引用,而Handler作为Activity的内部类持有Activity的引用,所以当Activity销毁时因MessageQueue的Message无法释放,会导致Activity无法释放,造成内存泄漏

    4.为何主线程可以new Handler?如果想在子线程中new Handler要做哪些准备?

    因为APP启动后ActivityThread会帮我们自动创建Looper。如果想在子线程中new Handler要做调用Looper.prepare(),Looper.loop()方法

    //ActivityThread.java    
    public static void main(String[] args) {


    Looper.prepareMainLooper();
    //............
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    复制代码

    5.子线程维护的Looper,消息队列无消息时,处理方案是什么?有什么用?

    子线程中new Handler要做调用Looper.prepare(),Looper.loop()方法,并在结束时手动调用Looper.quit(),LooperquitSafely()方法来终止Looper,否则子线程会一直卡死在这个阻塞状态不能停止

    6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可以能处于不同线程),那他内部如何保证线程安全的?

     boolean enqueueMessage(Message msg, long when) {
    //..............通过synchronized保证线程安全
    synchronized (this) {
    if (mQuitting) {
    IllegalStateException e = new IllegalStateException(
    msg.target + " sending message to a Handler on a dead thread");
    Log.w(TAG, e.getMessage(), e);
    msg.recycle();
    return false;
    }
    //..............
    }
    复制代码

    7.我们使用Message时应该如何创建它?

    Message.obtain()

    8.使用Handler的postDelay后消息队列会有什么变化?

    按照Message.when 插入message

    9.Looper死循环为啥不会导致应用卡死

    Looper死循环,没消息会进行休眠不会卡死调用linux底层的epoll机制实现;应用卡死时是ANR导致的;两者是不一样的概念

    ANR:用户输入事件或者触摸屏幕5S没响应或者广播在10S内未执行完毕,Service的各个生命周期函数在特定时间(20秒)内无法完成处理

    收起阅读 »

    JAVA面向对象之抽象类

    文章目录抽象类的概念举例1:绘制图形项目修改举例2:员工类抽象类的概念抽象类的基本概念1、很多具有相同特征和行为的对象可以抽象为一个类;很多具有相同特征和行为的类可以抽象为一个抽象类。2、使用abstract关键字声明的类为抽象类抽象类作用1、为子类提供通用代...
    继续阅读 »

    文章目录

    抽象类的概念

    抽象类的基本概念
    1、很多具有相同特征和行为的对象可以抽象为一个类;很多具有相同特征和行为的类可以抽象为一个抽象类。
    2、使用abstract关键字声明的类为抽象类

    抽象类作用
    1、为子类提供通用代码
    2、为子类提供通用方法的定义
    子类向上转型之后可以调用父类的这个通用方法(当然执行的时候是执行子类的方法),如果不定义这个抽象方法(所谓的抽象方法理解起来非常简单,就是没有完成的方法),只能向下转型成子类再调用子类中方法

    抽象类的规则
    a、抽象类可以没有抽象方法,有抽象方法的类必须是抽象类
    b、非抽象类继承抽象类必须实现所有抽象方法
    c、抽象类可以继承抽象类,可以不实现父类抽象方法。
    d、抽象类可以有方法实现和属性
    e、抽象类不能被实例化
    f、抽象类不能声明为final
    g、抽象类可以有构造方法

    举例1:绘制图形项目修改

    这个例子是根据上一章的绘制图形项目进行的修改,传送门:【达内课程】面向对象之多态

    前情回顾:

    我们有图形类 Shape(图形),它的子类有 Line(线)、Circle(圆)、Square(方)

    Shape:draw(画图)、clean(清除)
    |-Line:有自己的 draw 方法。有单独的 length 方法
    |-Circle:有自己的 draw 方法 |-Square:有自己的 draw 方法

    Shape类修改

    public abstract class Shape {
    public abstract void draw(TextView view);

    public void clean(TextView view) {
    view.setText("");
    }
    }

    其中继承 Shape 类 Circle、Line、Square 都不能使用 super.draw(view);了,所以应该在代码中去掉,例如 Circle 类:

    public class Circle extends Shape {
    @Override
    public void draw(TextView view) {
    //super.draw(view);
    view.setText("o");
    }
    }

    修改 MainActivity 中的 doClick 方法,其余点击事件都没有变,只不过由于抽象类不能创建实例,所以修改了 button1 的点击事件

    public void doClick(View view) {
    switch (view.getId()) {
    case R.id.button1:
    //f(new Shape());
    textView.setText("抽象类不能创建实例");
    break;
    case R.id.button2:
    f(new Line());
    break;
    ......
    }
    }

    运行结果:
    在这里插入图片描述
    这里重点看下 f(Shape shape) 方法,重点都写在注释里了:

    private void f(Shape shape) {
    //参数对象,保存到成员变量
    currentShape = shape;
    //调用抽象方法
    //执行的是子类实现的draw方法
    shape.draw(textView);
    //向上转型后只能访问父类定义的通用成员
    //不能访问子类特有成员
    //shape.length();
    if (shape instanceof Line) {
    Line line = (Line) shape;
    line.length(textView);
    }
    }

    举例2:员工类

    我们有一个抽象的员工类 Employee,抽象方法有 工资(gongzi())、奖金(jiangjin())。还有一个返回综合工资的方法(zonghe()

    程序员类 Programmer、经理类 Manager 继承了这个抽象方法

    Employee
    |-Programmer
    |-Manager

    Employee

    public abstract class Employee {
    public abstract double gongzi();

    public abstract double jiangjin();

    public double zonghe() {
    //抽象方法调用
    //执行具体子类中实现的方法
    return gongzi() + jiangjin();
    }
    }

    Programmer

    public class Programmer extends Employee {
    @Override
    public double gongzi() {
    return 8000;
    }

    @Override
    public double jiangjin() {
    return 1000;
    }
    }

    Manager

    public class Manager extends Employee {
    @Override
    public double gongzi() {
    return 10000;
    }

    @Override
    public double jiangjin() {
    return 3000;
    }
    }

    xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <Button
    android:id="@+id/button1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="doClick"
    android:text="Employee" />


    <Button
    android:id="@+id/button2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="doClick"
    android:text="Programmer" />


    <Button
    android:id="@+id/button3"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="doClick"
    android:text="Manager" />


    <TextView
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textColor="#222222"
    android:textSize="20sp"
    android:gravity="center" />

    </LinearLayout>

    MainActivity

    package com.example.testapplication;

    import android.os.Bundle;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;

    import androidx.appcompat.app.AppCompatActivity;

    public class MainActivity extends AppCompatActivity {
    Button button1;
    Button button2;
    Button button3;

    TextView textView;

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

    textView = (TextView) findViewById(R.id.text);
    button1 = (Button) findViewById(R.id.button1);
    button2 = (Button) findViewById(R.id.button2);
    button3 = (Button) findViewById(R.id.button3);

    }

    public void doClick(View view) {
    switch (view.getId()) {
    case R.id.button1:
    textView.setText("抽象类不能创建实例");
    break;
    case R.id.button2:
    f(new Programmer());
    break;
    case R.id.button3:
    f(new Manager());
    break;
    }
    }

    private void f(Employee employee) {
    textView.setText("");
    textView.append(employee.gongzi() + "\n");
    textView.append(employee.jiangjin() + "\n");
    textView.append(employee.zonghe() + "\n");
    }
    }
    收起阅读 »

    【Flutter 组件集录】FadeInImage

    一、认识 FadeInImage 组件 我们都知道,图片无论是从资源、文件、网络加载,都不会立刻完成,这样会出现短暂的空白,尤其是网络图片。自己处理默认占位图也比较麻烦。FadeInImage 的作用就是:在目标图片加载完成前使用默认图片占位,加载完成后,目...
    继续阅读 »
    一、认识 FadeInImage 组件

    我们都知道,图片无论是从资源、文件、网络加载,都不会立刻完成,这样会出现短暂的空白,尤其是网络图片。自己处理默认占位图也比较麻烦。FadeInImage 的作用就是:在目标图片加载完成前使用默认图片占位,加载完成后,目标图片会渐变淡入,默认图片会渐变淡出,这样可以既解决图片加载占位问题,渐变的动画在视觉上也不显突兀。本文,就来全面介绍一下 FadeInImage 组件的使用以及简单的源码实现。




    1. FadeInImage 基本信息

    首先,它是一个 StatelessWidget,就说明它本身不会维护复杂的状态类,只是在 build 方法中负责组件的构建。



    在普通构造中,必须传入两个 ImageProvider 对象,image 表示待加载的目标图片资源,placeholder 表示目标图片加载过程中显示的占位图片资源。另外还有很多用于配置图片和动画的属性,后面再一一介绍。


    final ImageProvider placeholder;
    final ImageProvider image;



    2.FadeInImage 的简单使用

    只有知道两个图片资源就能最简单地使用 FadeInImage,另外可以通过 widthheight 限制图片的大小。下面头像是使用网络图片,黑色的是占位图,效果如下:






































    属性名 类型 默认值 用途
    placeholder ImageProvider required 占位图片资源
    image ImageProvider required 目标图片资源
    width double null 图片宽
    height double null 图片高

    class FadeInImageDemo extends StatelessWidget{
    final headUrl =
    'https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/5b2b7b85d1c818fa71d9e2e8ba944a44~300x300.image';
    @override
    Widget build(BuildContext context) {
    return FadeInImage(
    width: 100,
    height: 100,
    placeholder: AssetImage(
    'assets/images/default_icon.png',
    ),
    image: NetworkImage(headUrl),
    );
    }
    }



    3.FadeInImage 动画配置

    淡出动画 fadeOut 是针对占位图 而言的,淡入动画 fadeIn 是针对目标图 而言的,我们可以配置两个动画的时长和曲线来达到期望的动画效果,如下是测试案例的效果:






































    属性名 类型 默认值 用途
    fadeOutDuration Duration 300 ms 占位图淡出时长
    fadeOutCurve Curves Curves.easeOut 占位图淡出动画曲线
    fadeInDuration Duration 700 ms 目标图淡入时长
    fadeInCurve Curves Curves.easeIn 目标图淡入动画曲线

    FadeInImage(
    width: 100,
    height: 100,
    fadeOutDuration:Duration(seconds: 1),
    fadeOutCurve: Curves.easeOutQuad,
    fadeInDuration: Duration(seconds: 2),
    fadeInCurve: Curves.easeInQuad,
    placeholder: AssetImage(
    'assets/images/default_icon.png',
    ),
    image: NetworkImage(headUrl),
    );



    4.FadeInImage 的图片错误构建器

    既然是图片加载,就可能出错,这两个 XXXErrorBuilder 就是用来处理当图片加载错误时应该如何显示。如果不处理,就会像下面这样:



    我们可以指定 XXXErrorBuilder 回调来构建错误时显示的组件,如下当占位符错误,显示蓝色 Container 示意一下,你可以指定任意的 Widget


























    属性名 类型 默认值 用途
    placeholderErrorBuilder ImageErrorWidgetBuilder null 占位图加载错误时构建器
    imageErrorBuilder ImageErrorWidgetBuilder null 目标图加载错误时构建器

    class FadeInImageDemo extends StatelessWidget{

    final headUrl =
    'https://sf1-ttcdn-tos.pstatp.com/img/user-avatar/5b2b7b85d1c818fa71d9e2e8ba944a44~300x300.image';

    @override
    Widget build(BuildContext context) {
    return
    FadeInImage(
    width: 100,
    height: 100,
    fadeOutDuration:Duration(seconds: 1),
    fadeOutCurve: Curves.easeOutQuad,
    fadeInDuration: Duration(seconds: 2),
    fadeInCurve: Curves.easeInQuad,
    placeholderErrorBuilder: _placeholderErrorBuilder,
    placeholder: AssetImage(
    'assets/images/default_icon2.png',
    ),
    image: NetworkImage(headUrl),
    );
    }

    Widget _placeholderErrorBuilder(BuildContext context, Object error, StackTrace? stackTrace) {
    return Container(
    width: 100,
    height: 100,
    color: Colors.blue,
    );
    }
    }



    5.FadeInImage 其他属性

    剩下的几个属性都是传给 Image 的,也就是说作用和 Image 中的属性一致,这里就不展开了。





    6.FadeInImage 的其他构造

    除了普通构造之外,FadeInImage 还有 assetNetworkmemoryNetwork ,这两者只是占位组件是 asset 路径 还是 Uint8List 字节数组的区别。这两个构造的目的是便于使用,可以指定缩放以及宽高。





    可以看到两个 ImageProvider 成员对象会通过 ResizeImage 进行处理,通过 ResizeImage 可以更改图片资源的大小,通常用于减少 ImageCache 的内存占用。



    到这里,FadeInImage 的使用方面就介绍完了。下面来看一下,作为一个 StatelessWidget , FadeInImage 为什么可以执行这么复杂的组件内容变化。




    二、 FadeInImage 组件的源码实现


    1. FadeInImage 组件的构建

    对于 StatelessWidget 而言,逻辑基本上只在 build 方法中如何构建组件。如下是 FadeInImage#build,会通过 _image 方法创建 result 组件,并且一个 frameBuilder 的构建回调,使用了 _AnimatedFadeOutFadeIn 组件。





    如果 excludeFromSemantics=false 会套上一个语义组件,Semantics 。这就是 FadeInImage 构造的全部内容。





    _image 方法就是根据入参和成员属性构建 Image 组件而已,也没什么特别的。现在核心就是 frameBuilder 的回调会构建 _AnimatedFadeOutFadeIn 。那 Image#frameBuilder 是什么时候会调用呢?先让子弹飞一会,现在看一下 _AnimatedFadeOutFadeIn 的实现。





    2. _AnimatedFadeOutFadeIn 组件的实现

    它继承自 ImplicitlyAnimatedWidget ,表示其是一个 隐式动画组件,在 AnimatedOpacity 一文中介绍过隐式组件的特性:外界只需要改变相关配置属性,进重构组件就能触发动画,无需操作动画控制器。



    _AnimatedFadeOutFadeInState#build 中可以看出,淡入淡出的动画实现是通过两个 FadeTransition完成的,两者通过 Stack 叠合。这样看来是不是豁然开朗。





    3. 渐变动画如何触发

    AnimatedOpacity 一文中也说过,对于隐式组件,动画的启动是通过改变属性和重建组件,来触发 State#didUpdateWidget ,开启动画。



    那问题来了,作为 StatelessWidgetFadeInImage ,如何重构 _AnimatedFadeOutFadeIn 。现在再来看 frameBuilder 就正是时候。Image 组件的 frameBuilder 是一个回调的构建,它会在 _ImageState 构建时触发。





    第一次是图片没有加载:



    第二次是图片加载完成:



    属性变化 + 组件重构,从而触发隐式组件的动画启动,完成需求。可以看出 FadeInImage 是非常巧妙的。FadeInImage 的使用方式到这里就介绍完毕,那本文到这里就结束了,谢谢观看,明天见~

    收起阅读 »

    LiveData:还没普及就让我去世?我去你的 Kotlin 协程

    在今年(2021 年)的 Google I/O 大会中的 Jetpack Q&A 环节,Android 团队被问了一个很有意思的问题:LiveData 是要被废弃了吗? 问题不是瞎问的 大家好,我是扔物线朱凯。今天来聊个轻松的,不那么硬核的...
    继续阅读 »



    在今年(2021 年)的 Google I/O 大会中的 Jetpack Q&A 环节,Android 团队被问了一个很有意思的问题:LiveData 是要被废弃了吗?




    问题不是瞎问的


    大家好,我是扔物线朱凯。今天来聊个轻松的,不那么硬核的——LiveData。


    LiveData 是 Android 官方在 2017 年推出的一系列架构组件中的一个,跟它一起的还有 ViewModel 和 Lifecycle 等等,以及这几年陆续出现的一个个新成员。这些组件后来有了统一的名字:Jetpack;而 Jetpack 的各个组件也越来越被 Android 开发者接受。LiveData 作为 Jetpack 的架构组件的元老级成员,发展势头也一直不错,可是——它从今往后要开始往下走了。就像你在视频开头看到的,有人问 Android 团队「你们是要废弃 LiveData 了吗?」这个问题可不是瞎问的。


    那是咋问的呢?这还得从当年的 RxJava 说起。


    从 RxJava 说起


    LiveData 在 2017 年刚一面世,就受到了很大的关注,其中一个原因是它让很多人想到了 RxJava。LiveData 是一个以观察者模式为核心,通过让界面对变量进行订阅来实现自动通知刷新的组件,而 RxJava 最核心的关键词就是观察者模式和事件流,所以当时很多人拿它去和 RxJava 做比较:有人说它比 RxJava 好用,有人说它没有 RxJava 强大,还有人说它俩根本就不是一个东西,放在一起比较是没有意义的。


    至于我的观点嘛……这就说。


    RxJava 是在 2014、2015 年这个时间火起来的,国内晚一些,大概在 2016 年开始爆火。当时全世界的劳动人民用 RxJava 一般是做两件事:网络请求,以及 event bus。网络请求这个就不用说了,RxJava 配合 Retrofit 来做网络请求,各种复杂操作和线程切换,谁用谁知道——现在用协程就可以了,比 RxJava 方便;而 event bus,当时比较火的是两个开源库:GreenRobot 的 EventBus——同名库——,和 Square 的 Otto,在 RxJava 流行起来之后,大家发现,哎,这 RxJava 稍微定制一下也能实现 event bus 的功能啊?那既然我都用 RxJava 了,我为啥不把 event bus 也交给它做?就这样,一种叫做 RxBus 的模式就流行了起来,后来也有人开源了这样的库。


    就在这样的时代背景下,LiveData 在 2017 年发布了。它的功能是让变量可以被订阅。跟一般的订阅比起来,LiveData 有两个特点:一是它的目标非常直接,直指界面刷新,所以它的数据更新只发生在主线程;二是它借助了另一个架构组件——Lifecycle——的功能,让它可以只在界面到了前台的时候才通知更新,在后台的时候就闷不吭声,避免浪费性能,也避免了 bug。


    为什么不用 RxJava?


    很方便,很好用。但是这里就会有一个问题:变量的订阅,用 RxJava 不能做吗?为什么要搞一个新库出来呢?RxJava 就是专门做事件订阅的呀?



    • 是因为…… LiveData 的数据更新发生在主线程?RxJava 也可以啊,一个操作符的事( observeOn(AndroidSchedulers.MainThread)) ),安排。

    • 那……是因为它结合了 Lifecycle,对生命周期的支持比较到位?RxJava 也可以啊,改吧改吧就能支持了,总比写一个新库容易吧?


    所以 LiveData 的功能,用 RxJava 可以实现吗?是完全可以的,没有任何问题。那 Android 官方为什么要做一个 LiveData 出来,而不是直接推荐大家去用 RxJava 来实现这样的功能?或者退一步,用 RxJava 来做 LiveData 的底层实现也行啊?为什么都没有?——因为 RxJava 太大了,而且它还不是 Android 自己官方的东西,而是别人的。


    这个倒不是说 Google 小心眼子,只许宣传我自己的东西,不许壮大别人,而是 Android 作为一个平台方,它肯定要考虑开发者们的普遍水平的。RxJava 说实话虽然好用,但是太复杂了,上手成本忒高,所以如果 Android 要用 RxJava 来实现 LiveData,或者推荐开发者们用 RxJava 来自己实现 LiveData 的功能,那么它就需要考虑怎么让我们开发者学会 RxJava。怎么让我们学会?就只能自己教呗!写文档、出视频,教大家用 RxJava。那这个动作就有点大了,就把事情变复杂了。再加上 RxJava 还既不是 Android 体系里的东西,也不是 Google 体系里的东西,那么如果 Android 团队就为了一个 LiveData 的功能要去全网推广和教学 RxJava,这个逻辑就有点不对了,事情不是这么玩的。所以 RxJava 太大了,并且是第三方的,这两个原因结合起来,就让 Android 的 LiveData 没有使用 RxJava。这并不是一个竞争或胸怀的问题,而是一个「不要把事情变复杂」的问题。——当然这是我自己的观点啊。


    2017 - 2021 的变化


    但!这只是当时的情况。当时是什么时候?2017 年。2017 是 Android 的大年,这一年发生了好几件大事:



    • 官方发布了几个架构组件;

    • 官方宣布对 Kotlin 的支持;

    • HenCoder 发布(假)。


    HenCoder 是我乱讲的啊。我要说的是 Kotlin,Kotlin 在 2017 得到了 Android 官方的公开支持,在接下来这几年里,Kotlin 自身越来越完善,它的协程也越来越完善。2017 年之前,事件订阅大部分人是用 EventBus 或者 Otto,并且在 RxJava 流行起来之后,EventBus 和 Otto 的使用开始持续下降;2017 之后,对于简单场景大家慢慢过渡到了 LiveData,复杂场景还在用 RxJava,因为 LiveData 不适合复杂场景;而现在,我们有了 Flow。协程的 Flow 和 RxJava 的功能范围非常相似——其实我觉得就是一样的——但是 Flow 是协程里必不可少的一部分,而协程是 Kotlin 里必不可少的一部分,而 Kotlin 是 Android 开发里必不可少的一部分——哦这个说的不对,重新说——而 Kotlin 又是 Android 现在主推的开发语言以及未来的趋势,这样的话,Flow 一出来,那就没 LiveData 什么事了。别说 LiveData 了,以后 RxJava 也没什么事了。不过这个肯定需要一个过程的,LiveData 和 RxJava——尤其是 RxJava——肯定会继续坚挺一段时间的,只是趋势会是这么一个趋势。


    「不会废弃 LiveData」……吗?


    视频(文章)开头那个问题,Yigit 的回答是:LiveData 不会被废弃,因为两个原因:



    1. 用 Java 写 Android 的人还需要它——Flow 是协程的东西,所以如果你是用 Java 的,那其实没办法用 Flow;

    2. LiveData 的使用比较简单,而且功能上对于简单场景也是足够的,而 RxJava 和 Flow 这种东西学起来就没 LiveData 那么直观。


    简单说就是,为了 Java 语言的使用者和不想学 RxJava 或者 Flow 的人,LiveData 会被保留。不过你如果用发展的眼光去看他这番话……你懂我意思吧?


    那我走?


    那……对于不会 LiveData 的人,还有必要学 LiveData 吗?以及已经在用 LiveData 的项目,需要快点移除 LiveData 吗?


    如果你不会 LiveData,对于当下(2021 年)来说,还是很有必要学一下的,因为 LiveData 现在的应用率还是很高的,所以你就算你现在不用,你未来工作的团队也可能会用,反正这东西很简单,学一下不费事。另一方面,在用 LiveData 的人,确实可以考虑摘除它了;但也不是着急忙慌地把它拿走,它不是毒药不是地雷,只是协程的 Flow 现在可以做这件事了,而未来 Flow 一定是会成为主流的,就像现在的 Kotlin 一样;在项目里用两样东西来做同一件事(事件订阅)不如只用一样,因此你可以考虑摘除 LiveData,是这么个逻辑。所以你就算是着急,也应该去着急学 Flow,而不是着急地把 LiveData 拆掉,它没有毒,等以后你觉得它给你带来不方便了,你自然会把它拆掉。


    好,今天就是这样,如果你喜欢我的内容,别忘了点赞订阅关注。我是扔物线,我不和你比高低,我只助你成长,我们下期见。



    收起阅读 »

    大厂Android岗高频面试问题:说说你对Zygote的理解!

    前言 Zygote可以说是Android开发面试很高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Zygote的理解时,面试官最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题! 了解Zygo...
    继续阅读 »

    前言


    Zygote可以说是Android开发面试很高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Zygote的理解时,面试官最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题!



    • 了解Zygote的作用

    • 熟悉Zygote的启动流程

    • 深刻理解Zygote的工作原理



    下面来我们来深入剖析



    一、 Zygote的作用


    Zygote的作用分为两点:



    • 启动SystemServer

    • 孵化应用进程


    关于这个问题答出了这两点那就是OK了。可能大部分小伙伴可能能答出第二点,第一点就不是很清楚。SystemServer也是Zygote启动的,因为SystemServer需要用到Zygote准备好的系统资源包括:


    img


    直接从Zygote继承过来就不需要重新加载过来,那么对性能将会有很大的提升。


    二、Zygote的启动流程


    2.1 启动三段式


    在说Zygote启动流程之前,**先明确一个概念:启动三段式,**这个可以理解为Android中进程启动的常用套路,分为三步骤:


    img


    这里要了解LOOP循环是什么,其实LOOP作用是不停的接受消息处理消息,消息的来源可以是SoketMessageQueueBinder驱动发过来的消息,但无论消息从哪里来,它整个流程都是去接受消息,处理消息。这个启动三段式,它不光是Zygote进程是这样的,只要是有独立进程的,比如说系统服务进程,自己的应用进程都是如此。


    2.2 Zygote进程是怎么启动的?


    Zygote进程的启动取决于init进程,init进程是它是linux启动之后用户空间的第一个进程,下面看一下启动流程



    1. linux启动init进程

    2. init进程启动之后加载init.rc配置文件


    img



    1. 启动配置文件中定义的系统服务,其中Zygote服务就是定义在配置中的


    img



    1. 同时启动的服务除了Zygote之外还有一些别的系统服务也是会启动的,比如说ServiceManager进程,它是通过fork+execve系统调用启动的


    img


    2.2.1加载Zygote的启动配置


    在init.rc 文件中会import /init.${ro.zygote}.rc,init.zygoteXX,XX指的是32或者64,对我们没差我们直接看init.zygote32.rc即可。配置文件比较长,这里做了截取保留了Zygot相关的部分。


    service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server    
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    writepid /dev/cpuset/foreground/tasks


    • service zygote:是进程名称,

    • /system/bin/app_process:可执行程序的路径,用于init进程fork,execve调用

    • -Xzygote /system/bin --zygote --start-system-server 为它的参数


    2.2.2启动进程


    说完了启动配置呢,这里来聊一下启动进程,启动进程有两种方式:


    第一种:fork+handle


    pid_t pid = fork();
    if (pid == 0){
    // child process
    } else {
    // parent process
    }

    第二种:fork+execve


    pid_t pid = fork();
    if (pid == 0) {
    // child process
    execve(path, argv, env);
    } else {
    // parent process
    }

    两者看起来差不多,首先首先都会调用fork函数创建子进程,这个函数比较奇特会返回两次,子进程返回一次,父进程返回一次。区别在于:



    • 子进程一次,返回的pid是0 但是父进程返回的pid是子进程的pid,因此可以根据判断pid来区分目前是子进程还是父进程

    • 对于handle默认的情况,子进程会继承父进程的所有资源,但当通过execve去加载二进制程序时,那父进程的资源则会被清除


    2.2.3信号处理-SIGCHLD


    当父进程fork子进程后,父进程需要关注这个信号。当子进程挂了,父进程就会收到SIGCHLD,这时候父进程就可以做一些处理。例如Zygote进程如果挂了,那父进程init进程就会收到信号将Zygote进程重启。


    img


    三、Zygote进程启动原理


    主要分为两部分Native层处理和Java层处理,Zygote进程启动之后,它执行了execve系统调用,它执行的是用C++写的二进制的可执行程序里的main函数作为入口,然后在Java层运行!


    先来看一下Native层的处理流程


    img


    在app_main.cpp文件,AndroidRuntime.cpp文件。我们可以找到几个主要函数名


    int main(int argc,char *argv[]){
    JavaVM *jvm;
    JNIEnv *env;
    JNI_CreateJavaVM(&jvm,(void**)&env,&vm_args); //创建Java虚拟机
    jclass clazz = env->FindClass("ZygoteInit"); //找到叫ZygoteInit的Java类
    jmethodID method = env->GetStaticMethodID(clazz,"Main","[Ljava/lang/String;)V"); //找到ZygoteInit类中的Main的静态函数
    env->CallStaticVoidMethod(clazz,method,args); //调用main函数
    jvm->DestroyJavaVM();
    }

    根据上述代码,你会发现在我们的应用里直接就可以 JNI 调用了,并不需要创建虚拟机。因为应用进程是Zygote进程孵化出来的,继承了父进程的拥有虚拟机,只需要重置数据即可。


    接着看一下Java层的处理,具体可参考ZygoteInit文件的main方法



    1. 预加载资源,比如常用类库、主题资源及一些共享库等


    img



    1. 启动SystemServer进程


    img



    1. 进入Socket 的Loop循环 会看到的ZygoteServer.runSelectLoop(…)调用


    boolean runOnce() {
    String[] args = readArgumentList(); //读取参数列表
    int pid = Zygote.forkAndSpecialize(); //根据读取到的参数启动子进程
    if(pid == 0) {
    //in child
    //执行ActivityThread的入口函数(main)
    handleChildProc(args,...);
    return true;
    }
    }

    img


    四、总结


    Zygote启动流程中需要主要以下2点问题



    1. Zygote fork要保证是单线程

    2. Zygote的IPC是采用socket




    收起阅读 »

    Android面试题(五)

    Android面试题系列:Android面试题(一)Android面试题(二)Android面试题(三)Android面试题(四)Android面试题(五)76.子线程发消息到主线程进行更新 UI,除了 handler 和 AsyncTask,还有什么? 用 ...
    继续阅读 »

    Android面试题系列:


    76.子线程发消息到主线程进行更新 UI,除了 handler 和 AsyncTask,还有什么?


    用 Activity 对象的 runOnUiThread 方法更新


    在子线程中通过 runOnUiThread()方法更新 UI
    如果在非上下文类中(Activity),可以通过传递上下文实现调用;

    用 View.post(Runnable r)方法更新 UI


    77.子线程中能不能 new handler?为什么?


    不能,如果在子线程中直接 new Handler()会抛出异常 java.lang.RuntimeException: Can'tcreate handler inside thread that has not called


    在没有调用 Looper.prepare()的时候不能创建 Handler,因为在创建 Handler 的源码中做了如下操作


    Handler 的构造方法中


    78.Android 中的动画有哪几类,它们的特点和区别是什么


    Frame Animation(帧动画)主要用于播放一帧帧准备好的图片,类似GIF图片,优点是使用简单方便、缺点是需要事先准备好每一帧图片;


    Tween Animation(补间动画)仅需定义开始与结束的关键帧,而变化的中间帧由系统补上,优点是不用准备每一帧,缺点是只改变了对象绘制,而没有改变View本身属性。因此如果改变了按钮的位置,还是需要点击原来按钮所在位置才有效。


    Property Animation(属性动画)是3.0后推出的动画,优点是使用简单、降低实现的复杂度、直接更改对象的属性、几乎可适用于任何对象而仅非View类,主要包括ValueAnimator和ObjectAnimator


    79.如何修改 Activity 进入和退出动画


    可 以 通 过 两 种 方 式 , 一 是 通 过 定 义 Activity 的 主 题 , 二 是 通 过 覆 写 Activity 的overridePendingTransition 方法。


    通过设置主题样式在 styles.xml 中编辑如下代码:


    添加 themes.xml 文件: 
    AndroidManifest.xml 中给指定的 Activity 指定 theme

    覆写 overridePendingTransition 方法


    overridePendingTransition(R.anim.fade, R.anim.hold); 

    80.Android与服务器交互的方式中的对称加密和非对称加密是什么?


    对称加密,就是加密和解密数据都是使用同一个key,这方面的算法有DES。
    非对称加密,加密和解密是使用不同的key。发送数据之前要先和服务端约定生成公钥和私钥,使用公钥加密的数据可以用私钥解密,反之。这方面的算法有RSA。ssh 和 ssl都是典型的非对称加密。


    82.事件分发中的 onTouch 和 onTouchEvent 有什么区别,又该如何使用?


    这两个方法都是在 View 的 dispatchTouchEvent 中调用的,onTouch 优先于 onTouchEvent执行。如果在 onTouch 方法中通过返回 true 将事件消费掉,onTouchEvent 将不会再执行。


    另外需要注意的是,onTouch 能够得到执行需要两个前提条件,第一 mOnTouchListener 的值不能为空,第二当前点击的控件必须是 enable 的。因此如果你有一个控件是非 enable 的,那么给它注册 onTouch 事件将永远得不到执行。对于这一类控件,如果我们想要监听它的 touch 事件,就必须通过在该控件中重写 onTouchEvent 方法来实现。


    83.属性动画,例如一个 button 从 A 移动到 B 点,B 点还是可以响应点击事件,这个原理是什么?


    补间动画只是显示的位置变动,View 的实际位置未改变,表现为 View 移动到其他地方,点击事件仍在原处才能响应。而属性动画控件移动后事件相应就在控件移动后本身进行处理


    都使用过哪些自定义控件


    pull2RefreshListView
    LazyViewPager
    SlidingMenu
    SmoothProgressBar
    自定义组合控件
    ToggleButton
    自定义Toast

    84.谈谈你在工作中是怎样解决一个 bug


    异常附近多打印 log 信息;
    分析 log 日志,实在不行的话进行断点调试;
    调试不出结果,上 Stack Overflow 贴上异常信息,请教大牛
    再多看看代码,或者从源代码中查找相关信息
    实在不行就 GG 了,找师傅来解决!

    85.嵌入式操作系统内存管理有哪几种, 各有何特性


    页式,段式,段页,用到了MMU,虚拟空间等技术


    86.开发中都使用过哪些框架、平台


    EventBus(事件处理)    
    xUtils(网络、图片、ORM
    JPush(推送平台)
    友盟(统计平台)
    有米(优米)(广告平台)
    百度地图
    bmob(服务器平台、短信验证、邮箱验证、第三方支付)
    阿里云 OSS(云存储)
    ShareSDK(分享平台、第三方登录)
    Gson(解析 json 数据框架)
    imageLoader (图片处理框架)
    zxing (二维码扫描)
    anroid-asyn-http(网络通讯)
    DiskLruCache(硬盘缓存框架)
    Viatimo(多媒体播放框架)
    universal-image-loader(图片缓存框架)
    讯飞语音(语音识别)

    87.谈谈你对 Bitmap 的理解, 什么时候应该手动调用 bitmap.recycle()


    Bitmap 是 android 中经常使用的一个类,它代表了一个图片资源。 Bitmap 消耗内存很严重,如果不注意优化代码,经常会出现 OOM 问题,优化方式通常有这么几种:


    使用缓存;
    压缩图片;
    及时回收;

    至于什么时候需要手动调用 recycle,这就看具体场景了,原则是当我们不再使用 Bitmap 时,需要回收之。另外,我们需要注意,2.3 之前 Bitmap 对象与像素数据是分开存放的,Bitmap 对象存在java Heap 中而像素数据存放在 Native Memory 中, 这时很有必要调用 recycle 回收内存。 但是 2.3之后,Bitmap 对象和像素数据都是存在 Heap 中,GC 可以回收其内存。


    88.请介绍下 AsyncTask 的内部实现和适用的场景


    AsyncTask 内部也是 Handler 机制来完成的,只不过 Android 提供了执行框架来提供线程池来执行相应地任务,因为线程池的大小问题,所以 AsyncTask 只应该用来执行耗时时间较短的任务,比如 HTTP 请求,大规模的下载和数据库的更改不适用于 AsyncTask,因为会导致线程池堵塞,没有线程来执行其他的任务,导致的情形是会发生 AsyncTask 根本执行不了的问题


    89.Activity间通过Intent传递数据大小有没有限制?


    Intent在传递数据时是有大小限制的,这里官方并未详细说明,不过通过实验的方法可以测出数据应该被限制在1MB之内(1024KB),笔者采用的是传递Bitmap的方法,发现当图片大小超过1024(准确地说是1020左右)的时候,程序就会出现闪退、停止运行等异常(不同的手机反应不同),因此可以判断Intent的传输容量在1MB之内。


    90.你一般在开发项目中都使用什么设计模式?如何来重构,优化你的代码?


    较为常用的就是单例设计模式,工厂设计模式以及观察者设计模式,


    一般需要保证对象在内存中的唯一性时就是用单例模式,例如对数据库操作的 SqliteOpenHelper 的对象。


    工厂模式主要是为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。


    观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新


    91.Android 应用中验证码登陆都有哪些实现方案


    从服务器端获取图片
    通过短信服务,将验证码发送给客户端

    92.定位项目中,如何选取定位方案,如何平衡耗电与实时位置的精度?


    开始定位,Application 持有一个全局的公共位置对象,然后隔一定时间自动刷新位置,每次刷新成功都把新的位置信息赋值到全局的位置对象, 然后每个需要使用位置请求的地方都使用全局的位置信息进行请求。


    该方案好处:请求的时候无需再反复定位,每次请求都使用全局的位置对象,节省时间。
    该方案弊端:耗电,每隔一定时间自动刷新位置,对电量的消耗比较大。

    按需定位,每次请求前都进行定位。这样做的好处是比较省电,而且节省资源,但是请求时间会变得相对较长。


    93.andorid 应用第二次登录实现自动登录


    前置条件是所有用户相关接口都走 https,非用户相关列表类数据走 http。


    步骤


    第一次登陆 getUserInfo 里带有一个长效 token,该长效 token 用来判断用户是否登陆和换取短 token 
    把长效 token 保存到 SharedPreferences
    接口请求用长效 token 换取短token,短 token 服务端可以根据你的接口最后一次请求作为标示,超时时间为一天。
    所有接口都用短效 token
    如果返回短效 token 失效,执行第3步,再直接当前接口
    如果长效 token 失效(用户换设备或超过一月),提示用户登录。

    94.说说 LruCache 底层原理


    LruCache 使用一个 LinkedHashMap 简单的实现内存的缓存,没有软引用,都是强引用。


    如果添加的数据大于设置的最大值,就删除最先缓存的数据来调整内存。maxSize 是通过构造方法初始化的值,他表示这个缓存能缓存的最大值是多少。


    size 在添加和移除缓存都被更新值, 他通过 safeSizeOf 这个方法更新值。 safeSizeOf 默认返回 1,但一般我们会根据 maxSize 重写这个方法,比如认为 maxSize 代表是 KB 的话,那么就以 KB 为单位返回该项所占的内存大小。


    除异常外,首先会判断 size 是否超过 maxSize,如果超过了就取出最先插入的缓存,如果不为空就删掉,并把 size 减去该项所占的大小。这个操作将一直循环下去,直到 size 比 maxSize 小或者缓存为空。


    95.jni 的调用过程?


    安装和下载 Cygwin,下载 Android NDK。
    ndk 项目中 JNI 接口的设计。
    使用 C/C++实现本地方法。
    JNI 生成动态链接库.so 文件。
    将动态链接库复制到 java 工程,在 java 工程中调用,运行 java 工程即可。

    96.一条最长的短信息约占多少byte?


    中文70(包括标点),英文160,160个字节。


    98.即时通讯是是怎么做的?


    使用asmark 开源框架实现的即时通讯功能.该框架基于开源的 XMPP 即时通信协议,采用 C/S 体系结构,通过 GPRS 无线网络用 TCP 协议连接到服务器,以架设开源的Openfn'e 服务器作为即时通讯平台。


    客户端基于 Android 平台进行开发。负责初始化通信过程,进行即时通信时,由客户端负责向服务器发起创建连接请求。系统通过 GPRS 无线网络与 Internet 网络建立连接,通过服务器实现与Android 客户端的即时通信脚。


    服务器端则采用 Openfire 作为服务器。 允许多个客户端同时登录并且并发的连接到一个服务器上。服务器对每个客户端的连接进行认证,对认证通过的客户端创建会话,客户端与服务器端之间的通信就在该会话的上下文中进行。


    99.怎样对 android 进行优化?


    对 listview 的优化。
    对图片的优化。
    对内存的优化。
    具体一些措施
    尽量不要使用过多的静态类 static
    数据库使用完成后要记得关闭 cursor
    广播使用完之后要注销

    100.如果有个100M大的文件,需要上传至服务器中,而服务器form表单最大只能上传2M,可以用什么方法。


    首先来说使用http协议上传数据,特别在android下,跟form没什么关系。


    传统的在web中,在form中写文件上传,其实浏览器所做的就是将我们的数据进行解析组拼成字符串,以流的方式发送到服务器,且上传文件用的都是POST方式,POST方式对大小没什么限制。


    回到题目,可以说假设每次真的只能上传2M,那么可能我们只能把文件截断,然后分别上传了,断点上传。


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

    Android面试题(四)

    Android面试题系列:Android面试题(一)Android面试题(二)Android面试题(三)Android面试题(四)Android面试题(五)50.ListView 可以显示多种类型的条目吗 这个当然可以的,ListView 显示的每个条目都是通...
    继续阅读 »

    Android面试题系列:

    50.ListView 可以显示多种类型的条目吗


    这个当然可以的,ListView 显示的每个条目都是通过 baseAdapter 的 getView(int position,View convertView, ViewGroup parent)来展示的,理论上我们完全可以让每个条目都是不同类型的view。


    比如:从服务器拿回一个标识为 id=1,那么当 id=1 的时候,我们就加载类型一的条目,当 id=2的时候,加载类型二的条目。常见布局在资讯类客户端中可以经常看到。


    除此之外 adapter 还提供了 getViewTypeCount()和 getItemViewType(int position)两个方法。在 getView 方法中我们可以根据不同的 viewtype 加载不同的布局文件。


    51.ListView 如何定位到指定位置


    可以通过 ListView 提供的 lv.setSelection(listView.getPosition())方法。


    52.如何在 ScrollView 中如何嵌入 ListView


    通常情况下我们不会在 ScrollView 中嵌套 ListView。


    在 ScrollView 添加一个 ListView 会导致 listview 控件显示不全,通常只会显示一条,这是因为两个控件的滚动事件冲突导致。所以需要通过 listview 中的 item 数量去计算 listview 的显示高度,从而使其完整展示。


    现阶段最好的处理的方式是: 自定义 ListView,重载 onMeasure()方法,设置全部显示。


    53.Manifest.xml文件中主要包括哪些信息?


    manifest:根节点,描述了package中所有的内容。
    uses-permission:请求你的package正常运作所需赋予的安全许可。
    permission: 声明了安全许可来限制哪些程序能你package中的组件和功能。
    instrumentation:声明了用来测试此package或其他package指令组件的代码。
    application:包含package中application级别组件声明的根节点。
    activity:Activity是用来与用户交互的主要工具。
    receiver:IntentReceiver能使的application获得数据的改变或者发生的操作,即使它当前不在运行。
    service:Service是能在后台运行任意时间的组件。
    provider:ContentProvider是用来管理持久化数据并发布给其他应用程序使用的组件。复制代码

    54.ListView 中图片错位的问题是如何产生的


    图片错位问题的本质源于我们的 listview 使用了缓存 convertView, 假设一种场景, 一个 listview一屏显示九个 item,那么在拉出第十个 item 的时候,事实上该 item 是重复使用了第一个 item,也就是说在第一个 item 从网络中下载图片并最终要显示的时候,其实该 item 已经不在当前显示区域内了,此时显示的后果将可能在第十个 item 上输出图像,这就导致了图片错位的问题。所以解决办法就是可见则显示,不可见则不显示。


    55.Fragment 的 replace 和 add 方法的区别


    Fragment 本身并没有 replace 和 add 方法,FragmentManager才有replace和add方法。我们经常使用的一个架构就是通过RadioGroup切换Fragment,每个 Fragment 就是一个功能模块。


    Fragment 的容器一个 FrameLayout,add 的时候是把所有的 Fragment 一层一层的叠加到了。FrameLayout 上了,而 replace 的话首先将该容器中的其他 Fragment 去除掉然后将当前Fragment添加到容器中。


    一个 Fragment 容器中只能添加一个 Fragment 种类,如果多次添加则会报异常,导致程序终止,而 replace 则无所谓,随便切换。因为通过 add 的方法添加的 Fragment,每个 Fragment 只能添加一次,因此如果要想达到切换效果需要通过 Fragment 的的 hide 和 show 方法结合者使用。将要显示的 show 出来,将其他 hide起来。这个过程 Fragment 的生命周期没有变化。


    通过 replace 切换 Fragment,每次都会执行上一个 Fragment 的 onDestroyView,新 Fragment的 onCreateView、onStart、onResume 方法。基于以上不同的特点我们在使用的使用一定要结合着生命周期操作我们的视图和数据。


    56.Fragment 如何实现类似 Activity 栈的压栈和出栈效果的?


    Fragment 的事物管理器内部维持了一个双向链表结构,该结构可以记录我们每次 add 的Fragment 和 replace 的 Fragment,然后当我们点击 back 按钮的时候会自动帮我们实现退栈操作。


    57.Fragment 在你们项目中的使用


    Fragment 是 android3.0 以后引入的的概念,做局部内容更新更方便,原来为了到达这一点要把多个布局放到一个 activity 里面,现在可以用多 Fragment 来代替,只有在需要的时候才加载Fragment,提高性能。


    Fragment 的好处:


    Fragment 可以使你能够将 activity 分离成多个可重用的组件,每个都有它自己的生命周期和UI。
    Fragment 可以轻松得创建动态灵活的 UI 设计,可以适应于不同的屏幕尺寸。从手机到平板电脑。
    Fragment 是一个独立的模块,紧紧地与 activity 绑定在一起。可以运行中动态地移除、加入、交换等。
    Fragment 提供一个新的方式让你在不同的安卓设备上统一你的 UI。
    Fragment 解决 Activity 间的切换不流畅,轻量切换。
    Fragment 替代 TabActivity 做导航,性能更好。
    Fragment 在 4.2.版本中新增嵌套 fragment 使用方法,能够生成更好的界面效果。复制代码

    58.如何切换 fragement,不重新实例化


    翻看了 Android 官方 Doc,和一些组件的源代码,发现 replace()这个方法只是在上一个 Fragment不再需要时采用的简便方法.


    正确的切换方式是 add(),切换时 hide(),add()另一个 Fragment;再次切换时,只需 hide()当前,show()另一个。


    这样就能做到多个 Fragment 切换不重新实例化:


    59.如何对 Android 应用进行性能分析


    如果不考虑使用其他第三方性能分析工具的话,我们可以直接使用 ddms 中的工具,其实 ddms 工具已经非常的强大了。ddms 中有 traceview、heap、allocation tracker 等工具都可以帮助我们分析应用的方法执行时间效率和内存使用情况。


    Traceview 是 Android 平台特有的数据采集和分析工具,它主要用于分析 Android 中应用程序的 hotspot(瓶颈)。Traceview 本身只是一个数据分析工具,而数据的采集则需要使用 AndroidSDK 中的 Debug 类或者利用 DDMS 工具。


    heap 工具可以帮助我们检查代码中是否存在会造成内存泄漏的地方。


    allocation tracker 是内存分配跟踪工具


    60.Android 中如何捕获未捕获的异常


    UncaughtExceptionHandler


    自 定 义 一 个 Application , 比 如 叫 MyApplication 继 承 Application 实 现UncaughtExceptionHandler。
    覆写 UncaughtExceptionHandler 的 onCreate 和 uncaughtException 方法。 注意:上面的代码只是简单的将异常打印出来。在 onCreate 方法中我们给 Thread 类设置默认异常处理 handler,如果这句代码不执行则一切都是白搭。在 uncaughtException 方法中我们必须新开辟个线程进行我们异常的收集工作,然后将系统给杀死。
    在 AndroidManifest 中配置该 Application:

    Bug 收集工具 Crashlytics


    Crashlytics 是专门为移动应用开发者提供的保存和分析应用崩溃的工具。国内主要使用的是友盟做数据统计。
    Crashlytics 的好处:
    1.Crashlytics 不会漏掉任何应用崩溃信息。
    2.Crashlytics 可以象 Bug 管理工具那样,管理这些崩溃日志。
    3.Crashlytics 可以每天和每周将崩溃信息汇总发到你的邮箱,所有信息一目了然。复制代码

    61.如何将SQLite数据库(dictionary.db文件)与apk文件一起发布


    把这个文件放在/res/raw目录下即可。res\raw目录中的文件不会被压缩,这样可以直接提取该目录中的文件,会生成资源id。


    62.什么是 IntentService?有何优点?


    IntentService 是 Service 的子类,比普通的 Service 增加了额外的功能。先看 Service 本身存在两个问题:


    Service 不会专门启动一条单独的进程,Service 与它所在应用位于同一个进程中;
    Service 也不是专门一条新线程,因此不应该在 Service 中直接处理耗时的任务;复制代码

    IntentService 特征


    会创建独立的 worker 线程来处理所有的 Intent 请求;
    会创建独立的 worker 线程来处理 onHandleIntent()方法实现的代码,无需处理多线程问题;
    所有请求处理完成后,IntentService 会自动停止,无需调用 stopSelf()方法停止 Service
    ServiceonBind()提供默认实现,返回 null
    ServiceonStartCommand 提供默认实现,将请求 Intent 添加到队列中;复制代码

    63.谈谈对Android NDK的理解


    NDK是一系列工具的集合.NDK提供了一系列的工具,帮助开发者快速开发C或C++的动态库,并能自动将so和java应用一起打包成apk.这些工具对开发者的帮助是巨大的.NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU,平台,ABI等差异,开发人员只需要简单修改 mk文件(指出"哪些文件需要编译","编译特性要求"等),就可以创建出so.


    NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作.NDK提供了一份稳定,功能有限的API头文件声明.


    Google明确声明该API是稳定的,在后续所有版本中都稳定支持当前发布的API.从该版本的NDK中看出,这些 API支持的功能非常有限,包含有:C标准库(libc),标准数学库(libm ),压缩库(libz),Log库(liblog).


    64.AsyncTask使用在哪些场景?它的缺陷是什么?如何解决?


    AsyncTask 运用的场景就是我们需要进行一些耗时的操作,耗时操作完成后更新主线程,或者在操作过程中对主线程的UI进行更新。


    缺陷:AsyncTask中维护着一个长度为128的线程池,同时可以执行5个工作线程,还有一个缓冲队列,当线程池中已有128个线程,缓冲队列已满时,如果 此时向线程提交任务,将会抛出RejectedExecutionException。


    解决:由一个控制线程来处理AsyncTask的调用判断线程池是否满了,如果满了则线程睡眠否则请求AsyncTask继续处理。


    65.Android 线程间通信有哪几种方式(重要)


    共享内存(变量);
    文件,数据库;
    Handler
    Java 里的 wait(),notify(),notifyAll()复制代码

    66.请解释下 Android 程序运行时权限与文件系统权限的区别?


    apk 程序是运行在虚拟机上的,对应的是 Android 独特的权限机制,只有体现到文件系统上时才


    使用 linux 的权限设置。


    linux 文件系统上的权限
    -rwxr-x--x system system 4156 2010-04-30 16:13 test.apk
    代表的是相应的用户/用户组及其他人对此文件的访问权限,与此文件运行起来具有的权限完全不相关。比如上面的例子只能说明 system 用户拥有对此文件的读写执行权限;system 组的用户对此文件拥有读、执行权限;其他人对此文件只具有执行权限。而 test.apk 运行起来后可以干哪些事情,跟这个就不相关了。千万不要看 apk 文件系统上属于 system/system 用户及用户组,或者root/root 用户及用户组,就认为 apk 具有 system 或 root 权限复制代码

    Android 的权限规则


    Android 中的 apk 必须签名
    基于 UserID 的进程级别的安全机制
    默认 apk 生成的数据对外是不可见的
    AndroidManifest.xml 中的显式权限声明复制代码

    67.Framework 工作方式及原理,Activity 是如何生成一个 view 的,机制是什么?


    所有的框架都是基于反射 和 配置文件(manifest)的。


    普通的情况:


    Activity 创建一个 view 是通过 ondraw 画出来的, 画这个 view 之前呢,还会调用 onmeasure方法来计算显示的大小.复制代码

    特殊情况:


    Surfaceview 是直接操作硬件的,因为 或者视频播放对帧数有要求,onDraw 效率太低,不够使,Surfaceview 直接把数据写到显存。复制代码

    68.什么是 AIDL?如何使用?


    aidl 是 Android interface definition Language 的英文缩写,意思 Android 接口定义语言。


    使用 aidl 可以帮助我们发布以及调用远程服务,实现跨进程通信。


    将服务的 aidl 放到对应的 src 目录,工程的 gen 目录会生成相应的接口类
    我们通过 bindService(Intent,ServiceConnect,int)方法绑定远程服务,在 bindService中 有 一 个 ServiceConnec 接 口 , 我 们 需 要 覆 写 该 类 的onServiceConnected(ComponentName,IBinder)方法,这个方法的第二个参数 IBinder 对象其实就是已经在 aidl 中定义的接口,因此我们可以将 IBinder 对象强制转换为 aidl 中的接口类。我们通过 IBinder 获取到的对象(也就是 aidl 文件生成的接口)其实是系统产生的代理对象,该代理对象既可以跟我们的进程通信, 又可以跟远程进程通信, 作为一个中间的角色实现了进程间通信。复制代码

    69.AIDL 的全称是什么?如何工作?能处理哪些类型的数据?


    AIDL 全称 Android Interface Definition Language(AndRoid 接口描述语言) 是一种接口描述语言; 编译器可以通过 aidl 文件生成一段代码,通过预先定义的接口达到两个进程内部通信进程跨界对象访问的目的。需要完成两件事情:


    引入 AIDL 的相关类.; 
    调用 aidl 产生的 class复制代码

    理论上, 参数可以传递基本数据类型和 String, 还有就是 Bundle 的派生类, 不过在 Eclipse 中,目前的 ADT 不支持 Bundle 做为参数。


    70.Android 判断SD卡是否存在


    首先要在AndroidManifest.xml中增加SD卡访问权限


    71.Android中任务栈的分配


    Task实际上是一个Activity栈,通常用户感受的一个Application就是一个Task。从这个定义来看,Task跟Service或者其他Components是没有任何联系的,它只是针对Activity而言的。


    Activity有不同的启动模式, 可以影响到task的分配


    72.SQLite支持事务吗? 添加删除如何提高性能?


    在sqlite插入数据的时候默认一条语句就是一个事务,有多少条数据就有多少次磁盘操作 比如5000条记录也就是要5000次读写磁盘操作。


    添加事务处理,把多条记录的插入或者删除作为一个事务


    73.Android中touch事件的传递机制是怎样的?


    1.Touch事件传递的相关API有dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent 
    2.Touch事件相关的类有View、ViewGroup、Activity
    3.Touch事件会被封装成MotionEvent对象,该对象封装了手势按下、移动、松开等动作
    4.Touch事件通常从Activity#dispatchTouchEvent发出,只要没有被消费,会一直往下传递,到最底层的View。
    5.如果Touch事件传递到的每个View都不消费事件,那么Touch事件会反向向上传递,最终交由Activity#onTouchEvent处理.
    6.onInterceptTouchEvent为ViewGroup特有,可以拦截事件.
    7.Down事件到来时,如果一个View没有消费该事件,那么后续的MOVE/UP事件都不会再给它复制代码

    74.描述下Handler 机制


    1)Looper: 一个线程可以产生一个Looper对象,由它来管理此线程里的MessageQueue(消息队列)。 
    2)Handler: 你可以构造Handler对象来与Looper沟通,以便push新消息到MessageQueue里;或者接收Looper从Message Queue取出)所送来的消息。
    3) Message Queue(消息队列):用来存放线程放入的消息。
    4)线程:UIthread 通常就是main thread,而Android启动程序时会替它建立一个MessageQueue。复制代码

    Hander持有对UI主线程消息队列MessageQueue和消息循环Looper的引用,子线程可以通过Handler将消息发送到UI线程的消息队列MessageQueue中。


    75.自定义view的基本流程


    自定义View的属性 编写attr.xml文件 
    layout布局文件中引用,同时引用命名空间
    View的构造方法中获得我们自定义的属性 ,在自定义控件中进行读取(构造方法拿到attr.xml文件值)
    重写onMesure
    重写onDraw复制代码


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

    Android面试题(三)

    Android面试题系列:Android面试题(一)Android面试题(二)Android面试题(三)Android面试题(四)Android面试题(五)21.sim卡的EF 文件有何作用 sim卡就是电话卡,sim卡内有自己的操作系统,用来与手机通讯的。E...
    继续阅读 »

    Android面试题系列:

    21.sim卡的EF 文件有何作用


    sim卡就是电话卡,sim卡内有自己的操作系统,用来与手机通讯的。Ef文件用来存储数据的。


    22.Activity的状态有几种?


    运行
    暂停
    停止

    23.让Activity变成一个窗口


    设置activity的style属性=”@android:style/Theme.Dialog”


    24.android:gravity与android:layout_gravity的区别


    gravity:表示组件内元素的对齐方式
    layout_gravity:相对于父类容器,该视图组件的对齐方式


    25.如何退出Activity


    结束当前activity


    Finish()
    killProgress()
    System.exit(0)

    关闭应用程序时,结束所有的activity


    可以创建一个List集合,每新创建一个activity,将该activity的实例放进list中,程序结束时,从集合中取出循环取出activity实例,调用finish()方法结束


    26.如果后台的Activity由于某原因被系统回收了,如何在被系统回收之前保存当前状态?


    在onPuase方法中调用onSavedInstanceState()


    27.Android中的长度单位详解


    Px:像素
    Sp与dp也是长度单位,但是与屏幕的单位密度无关。

    28.activity,service,intent之间的关系


    这三个都是android应用频率非常的组件。Activity与service是四大核心组件。Activity用来加载布局,显示窗口界面,service运行后台,没有界面显示,intent是activity与service的通信使者。


    29.activity之间传递参数,除了intent,广播接收器,contentProvider之外,还有那些方法?


    Fie:文件存储,推荐使用sharedPreferecnces
    静态变量。

    30.Adapter是什么?你所接触过的adapter有那些?


    是适配器,用来为列表提供数据适配的。经常使用的adapter有baseadapter,arrayAdapter,SimpleAdapter,cursorAdapter,SpinnerAdapter等


    31.Fragment与activity如何传值和交互?


    Fragment对象有一个getActivity的方法,通过该方法与activity交互
    使用framentmentManager.findFragmentByXX可以获取fragment对象,在activity中直接操作fragment对象

    32.如果Listview中的数据源发生改变,如何更新listview中的数据


    使用adapter的notifyDataSetChanged方法


    33.广播接受者的生命周期?


    广播接收者的生命周期非常短。当执行onRecieve方法之后,广播就会销毁
    在广播接受者不能进行耗时较长的操作
    在广播接收者不要创建子线程。广播接收者完成操作后,所在进程会变成空进程,很容易被系统回收

    34.ContentProvider与sqlite有什么不一样的?


    ContentProvider会对外隐藏内部实现,只需要关注访问contentProvider的uri即可,contentProvider应用在应用间共享。
    Sqlite操作本应用程序的数据库。
    ContentProiver可以对本地文件进行增删改查操作

    35.如何保存activity的状态?


    默认情况下activity的状态系统会自动保存,有些时候需要我们手动调用保存。


    当activity处于onPause,onStop之后,activity处于未活动状态,但是activity对象却仍然存在。当内存不足,onPause,onStop之后的activity可能会被系统摧毁。


    当通过返回退出activity时,activity状态并不会保存。


    保存activity状态需要重写onSavedInstanceState()方法,在执行onPause,onStop之前调用onSavedInstanceState方法,onSavedInstanceState需要一个Bundle类型的参数,我们可以将数据保存到bundle中,通过实参传递给onSavedInstanceState方法。


    Activity被销毁后,重新启动时,在onCreate方法中,接受保存的bundle参数,并将之前的数据取出。


    36.Android中activity,context,application有什么不同。


    Content与application都继承与contextWrapper,contextWrapper继承于Context类。


    Context:表示当前上下文对象,保存的是上下文中的参数和变量,它可以让更加方便访问到一些资源。


    Context通常与activity的生命周期是一样的,application表示整个应用程序的对象。


    对于一些生命周期较长的,不要使用context,可以使用application。


    在activity中,尽量使用静态内部类,不要使用内部类。内部里作为外部类的成员存在,不是独立于activity,如果内存中还有内存继续引用到context,activity如果被销毁,context还不会结束。


    37.Service 是否在 main thread 中执行, service 里面是否能执行耗时的操作?


    默认情况service在main thread中执行,当service在主线程中运行,那在service中不要进行一些比较耗时的操作,比如说网络连接,文件拷贝等。


    38.Service 和 Activity 在同一个线程吗


    默认情况下service与activity在同一个线程,都在main Thread,或者ui线程中。


    如果在清单文件中指定service的process属性,那么service就在另一个进程中运行。


    39.Service 里面可以弹吐司么


    可以。


    40.在 service 的生命周期方法 onstartConmand()可不可以执行网络操作?如何在 service 中执行网络操作?


    可以的,就在onstartConmand方法内执行。


    41.说说 ContentProvider、ContentResolver、ContentObserver 之间的关系


    ContentProvider:内容提供者,对外提供数据的操作,contentProvider.notifyChanged(uir):可以更新数据
    contentResolver:内容解析者,解析ContentProvider返回的数据
    ContentObServer:内容监听者,监听数据的改变,contentResolver.registerContentObServer()

    42.请介绍下 ContentProvider 是如何实现数据共享的


    ContentProvider是一个对外提供数据的接口,首先需要实现ContentProvider这个接口,然后重写query,insert,getType,delete,update方法,最后在清单文件定义contentProvider的访问uri


    43.Intent 传递数据时,可以传递哪些类型数据?


    基本数据类型以及对应的数组类型
    可以传递bundle类型,但是bundle类型的数据需要实现Serializable或者parcelable接口

    44.Serializable 和 Parcelable 的区别?


    如果存储在内存中,推荐使用parcelable,使用serialiable在序列化的时候会产生大量的临时变量,会引起频繁的GC


    如果存储在硬盘上,推荐使用Serializable,虽然serializable效率较低


    Serializable的实现:只需要实现Serializable接口,就会自动生成一个序列化id


    Parcelable的实现:需要实现Parcelable接口,还需要Parcelable.CREATER变量


    45.请描述一下 Intent 和 IntentFilter


    Intent是组件的通讯使者,可以在组件间传递消息和数据。
    IntentFilter是intent的筛选器,可以对intent的action,data,catgory,uri这些属性进行筛选,确定符合的目标组件。

    46.什么是IntentService?有何优点?


    IntentService 是 Service 的子类,比普通的 Service 增加了额外的功能。先看 Service 本身存在两个问题:


    Service 不会专门启动一条单独的进程,Service 与它所在应用位于同一个进程中;
    Service 也不是专门一条新线程,因此不应该在 Service 中直接处理耗时的任务;

    特征


    会创建独立的 worker 线程来处理所有的 Intent 请求;
    会创建独立的 worker 线程来处理 onHandleIntent()方法实现的代码,无需处理多线程问题;
    所有请求处理完成后,IntentService 会自动停止,无需调用 stopSelf()方法停止 Service
    ServiceonBind()提供默认实现,返回 null
    ServiceonStartCommand 提供默认实现,将请求 Intent 添加到队列中

    使用


    让service类继承IntentService,重写onStartCommand和onHandleIntent实现 

    47.Android 引入广播机制的用意


    从 MVC 的角度考虑(应用程序内) 其实回答这个问题的时候还可以这样问,android 为什么要有那 4 大组件,现在的移动开发模型基本上也是照搬的 web 那一套 MVC 架构,只不过稍微做了修改。android 的四大组件本质上就是为了实现移动或者说嵌入式设备上的 MVC 架构,它们之间有时候是一种相互依存的关系,有时候又是一种补充关系,引入广播机制可以方便几大组件的信息和数据交互。


    程序间互通消息(例如在自己的应用程序内监听系统来电)


    效率上(参考 UDP 的广播协议在局域网的方便性)


    设计模式上(反转控制的一种应用,类似监听者模式)


    48.ListView 如何提高其效率?


    当 convertView 为空时,用 setTag()方法为每个 View 绑定一个存放控件的 ViewHolder 对象。当convertView 不为空, 重复利用已经创建的 view 的时候, 使用 getTag()方法获取绑定的 ViewHolder对象,这样就避免了 findViewById 对控件的层层查询,而是快速定位到控件。 复用 ConvertView,使用历史的 view,提升效率 200%


    自定义静态类 ViewHolder,减少 findViewById 的次数。提升效率 50%


    异步加载数据,分页加载数据。


    使用 WeakRefrence 引用 ImageView 对象


    49.ListView 如何实现分页加载


    设置 ListView 的滚动监听器:setOnScrollListener(new OnScrollListener{….})在监听器中有两个方法: 滚动状态发生变化的方法(onScrollStateChanged)和 listView 被滚动时调用的方法(onScroll)


    在滚动状态发生改变的方法中,有三种状态:手指按下移动的状态: SCROLL_STATE_TOUCH_SCROLL:触摸滑动,惯性滚动(滑翔(flgin)状态): SCROLL_STATE_FLING: 滑翔,静止状态: SCROLL_STATE_IDLE: // 静止,对不同的状态进行处理:


    分批加载数据,只关心静止状态:关心最后一个可见的条目,如果最后一个可见条目就是数据适配器(集合)里的最后一个,此时可加载更多的数据。在每次加载的时候,计算出滚动的数量,当滚动的数量大于等于总数量的时候,可以提示用户无更多数据了。



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

    Android面试题(二)

    Android面试题系列:Android面试题(一)Android面试题(二)Android面试题(三)Android面试题(四)Android面试题(五)11.广播注册 首先写一个类要继承BroadCastReceiver 第一种:在清单文件中声明,添加 ...
    继续阅读 »

    Android面试题系列:

    11.广播注册


    首先写一个类要继承BroadCastReceiver


    第一种:在清单文件中声明,添加






    第二种:使用代码进行注册如:


    IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
    BroadCastReceiverDemo receiver = new BroadCastReceiver();
    registerReceiver(receiver, filter);

    两种注册类型的区别是:
    a.第一种是常驻型广播,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。
    b.第二种不是常驻广播,也就是说广播跟随程序的生命周期。


    12.Android中的ANR


    ANR的全称application not responding 应用程序未响应。


    在android中Activity的最长执行时间是5秒。
    BroadcastReceiver的最长执行时间则是10秒。
    Service的最长执行时间则是20秒。

    超出执行时间就会产生ANR。注意:ANR是系统抛出的异常,程序是捕捉不了这个异常的。


    解决方法:



    1. 运行在主线程里的任何方法都尽可能少做事情。特别是,Activity应该在它的关键生命周期方法 (如onCreate()和onResume())里尽可能少的去做创建操作。(可以采用重新开启子线程的方式,然后使用Handler+Message 的方式做一些操作,比如更新主线程中的ui等)

    2. 应用程序应该避免在BroadcastReceiver里做耗时的操作或计算。但不再是在子线程里做这些任务(因为 BroadcastReceiver的生命周期短),替代的是,如果响应Intent广播需要执行一个耗时的动作的话,应用程序应该启动一个 Service。


    13.ListView优化



    1. convertView重用,利用好 convertView 来重用 View,切忌每次 getView() 都新建。ListView 的核心原理就是重用 View,如果重用 view 不改变宽高,重用View可以减少重新分配缓存造成的内存频繁分配/回收;

    2. ViewHolder优化,使用ViewHolder的原因是findViewById方法耗时较大,如果控件个数过多,会严重影响性能,而使用ViewHolder主要是为了可以省去这个时间。通过setTag,getTag直接获取View。

    3. 减少Item View的布局层级,这是所有layout都必须遵循的,布局层级过深会直接导致View的测量与绘制浪费大量的时间。

    4. adapter中的getView方法尽量少使用逻辑

    5. 图片加载采用三级缓存,避免每次都要重新加载。

    6. 尝试开启硬件加速来使ListView的滑动更加流畅。

    7. 使用 RecycleView 代替。


    14.Android数字签名



    1. 所有的应用程序都必须有数字证书,Android系统不会安装一个没有数字证书的应用程序

    2. Android程序包使用的数字证书可以是自签名的,不需要一个权威的数字证书机构签名认证

    3. 如果要正式发布一个Android ,必须使用一个合适的私钥生成的数字证书来给程序签名。

    4. 数字证书都是有有效期的,Android只是在应用程序安装的时候才会检查证书的有效期。如果程序已经安装在系统中,即使证书过期也不会影响程序的正常功能。


    15.Android root机制


    root指的是你有权限可以再系统上对所有档案有 "读" "写" "执行"的权力。root机器不是真正能让你的应用程序具有root权限。它原理就跟linux下的像sudo这样的命令。在系统的bin目录下放个su程序并属主是root并有suid权限。则通过su执行的命令都具有Android root权限。当然使用临时用户权限想把su拷贝的/system/bin目录并改属性并不是一件容易的事情。这里用到2个工具跟2个命令。把busybox拷贝到你有权限访问的目录然后给他赋予4755权限,你就可以用它做很多事了。


    16.View、surfaceView、GLSurfaceView


    View


    显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等,必须在UI主线程内更新画面,速度较慢


    SurfaceView


    基于view视图进行拓展的视图类,更适合2D游戏的开发,是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快


    GLSurfaceView


    基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图,是surfaceView的子类,openGL专用


    AsyncTask


    AsyncTask的三个泛型参数说明



    1. 第一个参数:传入doInBackground()方法的参数类型

    2. 第二个参数:传入onProgressUpdate()方法的参数类型

    3. 第三个参数:传入onPostExecute()方法的参数类型,也是doInBackground()方法返回的类型


    运行在主线程的方法:


    onPostExecute()
    onPreExecute()
    onProgressUpdate(Progress...)

    运行在子线程的方法:


    doInBackground() 

    控制AsyncTask停止的方法:


    cancel(boolean mayInterruptIfRunning) 

    AsyncTask的执行分为四个步骤



    1. 继承AsyncTask。

    2. 实现AsyncTask中定义的下面一个或几个方法onPreExecute()、doInBackground(Params...)、onProgressUpdate(Progress...)、onPostExecute(Result)。

    3. 调用execute方法必须在UI thread中调用。

    4. 该task只能被执行一次,否则多次调用时将会出现异常,取消任务可调用cancel。


    17.Android i18n


    I18n 叫做国际化。android 对i18n和L10n提供了非常好的支持。软件在res/vales 以及 其他带有语言修饰符的文件夹。如: values-zh 这些文件夹中 提供语言,样式,尺寸 xml 资源。


    18.NDK



    1. NDK是一系列工具集合,NDK提供了一系列的工具,帮助开发者迅速的开发C/C++的动态库,并能自动将so和Java应用打成apk包。

    2. NDK集成了交叉编译器,并提供了相应的mk文件和隔离cpu、平台等的差异,开发人员只需要简单的修改mk文件就可以创建出so文件。


    19.启动一个程序,可以主界面点击图标进入,也可以从一个程序中跳转过去,二者有什么区别?


    通过主界面进入,就是设置默认启动的activity。在manifest.xml文件的activity标签中,写以下代码





    从另一个组件跳转到目标activity,需要通过intent进行跳转。具体


    Intent intent=new Intent(this,activity.class),startActivity(intent) 


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

    Android面试题(一)

    Android面试题系列:Android面试题(一)Android面试题(二)Android面试题(三)Android面试题(四)Android面试题(五)Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑...
    继续阅读 »

    Android面试题系列:

    Android是一种基于Linux的自由及开放源代码的操作系统,主要使用于移动设备,如智能手机和平板电脑,由Google公司和开放手机联盟领导及开发。这里会不断收集和更新Android基础相关的面试题,目前已收集100题。

    1.Android系统的架构

    1. Android系统架构之应用程序
      Android会同一系列核心应用程序包一起发布,该应用程序包包括email客户端,SMS短消息程序,日历,地图,浏览器,联系人管理程序等。所有的应用程序都是使用JAVA语言编写的。
    2. Android系统架构之应用程序框架
      开发人员可以完全访问核心应用程序所使用的API框架(android.jar)。该应用程序的架构设计简化了组件的重用;任何一个应用程序都可以发布它的功能块并且任何其它的应用程序都可以使用其所发布的功能块。
    3. Android系统架构之系统运行库
      Android 包含一些C/C++库,这些库能被Android系统中不同的组件使用。它们通过 Android 应用程序框架为开发者提供服务。
    4. Android系统架构之Linux 内核
      Android 的核心系统服务依赖于 Linux 2.6 内核,如安全性,内存管理,进程管理, 网络协议栈和驱动模型。 Linux 内核也同时作为硬件和软件栈之间的抽象层。

    2.activity的生命周期

    Activity生命周期方法主要有onCreate()、onStart()、onResume()、onPause()、onStop()、onDestroy()和onRestart()等7个方法。

    • 启动一个A Activity,分别执行onCreate()、onStart()、onResume()方法。
    • 从A Activity打开B Activity分别执行A onPause()、B onCreate()、B onStart()、B onResume()、A onStop()方法。
    • 关闭B Activity,分别执行B onPause()、A onRestart()、A onStart()、A onResume()、B onStop()、B onDestroy()方法。
    • 横竖屏切换A Activity,清单文件中不设置android:configChanges属性时,先销毁onPause()、onStop()、onDestroy()再重新创建onCreate()、onStart()、onResume()方法,设置orientation|screenSize(一定要同时出现)属性值时,不走生命周期方法,只会执行onConfigurationChanged()方法。
    • Activity之间的切换可以看出onPause()、onStop()这两个方法比较特殊,切换的时候onPause()方法不要加入太多耗时操作否则会影响体验。

    3.Fragment的生命周期

    Fragment的生命周期

    Fragment与Activity生命周期对比

    Fragment的生命周期方法主要有onAttach()、onCreate()、onCreateView()、onActivityCreated()、onstart()、onResume()、onPause()、onStop()、onDestroyView()、onDestroy()、onDetach()等11个方法。

    • 切换到该Fragment,分别执行onAttach()、onCreate()、onCreateView()、onActivityCreated()、onstart()、onResume()方法。
    • 锁屏,分别执行onPause()、onStop()方法。
    • 亮屏,分别执行onstart()、onResume()方法。
    • 覆盖,切换到其他Fragment,分别执行onPause()、onStop()、onDestroyView()方法。
    • 从其他Fragment回到之前Fragment,分别执行onCreateView()、onActivityCreated()、onstart()、onResume()方法。

    4.Service生命周期

    在Service的生命周期里,常用的有:

    4个手动调用的方法

    startService()    启动服务
    stopService() 关闭服务
    bindService() 绑定服务
    unbindService() 解绑服务

    5个内部自动调用的方法

    onCreat()            创建服务
    onStartCommand() 开始服务
    onDestroy() 销毁服务
    onBind() 绑定服务
    onUnbind() 解绑服务
    1. 手动调用startService()启动服务,自动调用内部方法:onCreate()、onStartCommand(),如果一个Service被startService()多次启动,那么onCreate()也只会调用一次。
    2. 手动调用stopService()关闭服务,自动调用内部方法:onDestory(),如果一个Service被启动且被绑定,如果在没有解绑的前提下使用stopService()关闭服务是无法停止服务的。
    3. 手动调用bindService()后,自动调用内部方法:onCreate()、onBind()。
    4. 手动调用unbindService()后,自动调用内部方法:onUnbind()、onDestory()。
    5. startService()和stopService()只能开启和关闭Service,无法操作Service,调用者退出后Service仍然存在;bindService()和unbindService()可以操作Service,调用者退出后,Service随着调用者销毁。

    5.Android中动画

    Android中动画分别帧动画、补间动画和属性动画(Android 3.0以后的)

    帧动画

    帧动画是最容易实现的一种动画,这种动画更多的依赖于完善的UI资源,他的原理就是将一张张单独的图片连贯的进行播放,从而在视觉上产生一种动画的效果;有点类似于某些软件制作gif动画的方式。在有些代码中,我们还会看到android:oneshot="false" ,这个oneshot 的含义就是动画执行一次(true)还是循环执行多次。






    补间动画

    补间动画又可以分为四种形式,分别是 alpha(淡入淡出),translate(位移),scale(缩放大小),rotate(旋转)。
    补间动画的实现,一般会采用xml 文件的形式;代码会更容易书写和阅读,同时也更容易复用。Interpolator 主要作用是可以控制动画的变化速率 ,就是动画进行的快慢节奏。pivot 决定了当前动画执行的参考位置








    ...

    属性动画

    属性动画,顾名思义它是对于对象属性的动画。因此,所有补间动画的内容,都可以通过属性动画实现。属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等。

    6.Android中4大组件

    1. Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持久化的事情,妥善管理生命周期以及一些跳转逻辑。
    2. BroadCast Receiver:接受一种或者多种Intent作触发事件,接受相关消息,做一些简单处理,转换成一条Notification,统一了Android的事件广播模型。
    3. Content Provider:是Android提供的第三方应用数据的访问方案,可以派生Content Provider类,对外提供数据,可以像数据库一样进行选择排序,屏蔽内部数据的存储细节,向外提供统一的接口模型,大大简化上层应用,对数据的整合提 供了更方便的途径。
    4. service:后台服务于Activity,封装有一个完整的功能逻辑实现,接受上层指令,完成相关的事务,定义好需要接受的Intent提供同步和异步的接口。

    7.Android中常用布局

    常用的布局:

    FrameLayout(帧布局):所有东西依次都放在左上角,会重叠
    LinearLayout(线性布局):按照水平和垂直进行数据展示
    RelativeLayout(相对布局):以某一个元素为参照物,来定位的布局方式

    不常用的布局:

    TableLayout(表格布局): 每一个TableLayout里面有表格行TableRowTableRow里面可以具体定义每一个元素(Android TV上使用)
    AbsoluteLayout(绝对布局):用X,Y坐标来指定元素的位置,元素多就不适用。(机顶盒上使用)

    新增布局:

    PercentRelativeLayout(百分比相对布局)可以通过百分比控制控件的大小。
    PercentFrameLayout(百分比帧布局)可以通过百分比控制控件的大小。

    8.消息推送的方式

    • 方案1、使用极光和友盟推送。
    • 方案2、使用XMPP协议(Openfire + Spark + Smack)

      • 简介:基于XML协议的通讯协议,前身是Jabber,目前已由IETF国际标准化组织完成了标准化工作。
      • 优点:协议成熟、强大、可扩展性强、目前主要应用于许多聊天系统中,且已有开源的Java版的开发实例androidpn。
        缺点:协议较复杂、冗余(基于XML)、费流量、费电,部署硬件成本高。
    • 方案3、使用MQTT协议(更多信息见:mqtt.org/)

      • 简介:轻量级的、基于代理的“发布/订阅”模式的消息传输协议。
      • 优点:协议简洁、小巧、可扩展性强、省流量、省电,目前已经应用到企业领域(参考:mqtt.org/software),且…
      • 缺点:不够成熟、实现较复杂、服务端组件rsmb不开源,部署硬件成本较高。
    • 方案4、使用HTTP轮循方式
      • 简介:定时向HTTP服务端接口(Web Service API)获取最新消息。
      • 优点:实现简单、可控性强,部署硬件成本低。
      • 缺点:实时性差。

    9.android的数据存储

    1. 使用SharedPreferences存储数据;它是Android提供的用来存储一些简单配置信息的一种机制,采用了XML格式将数据存储到设备中。只能在同一个包内使用,不能在不同的包之间使用。
    2. 文件存储数据;文件存储方式是一种较常用的方法,在Android中读取/写入文件的方法,与Java中实现I/O的程序是完全一样的,提供了openFileInput()和openFileOutput()方法来读取设备上的文件。
    3. SQLite数据库存储数据;SQLite是Android所带的一个标准的数据库,它支持SQL语句,它是一个轻量级的嵌入式数据库。
    4. 使用ContentProvider存储数据;主要用于应用程序之间进行数据交换,从而能够让其他的应用保存或读取此Content Provider的各种数据类型。
    5. 网络存储数据;通过网络上提供给我们的存储空间来上传(存储)和下载(获取)我们存储在网络空间中的数据信息。

    10.Activity启动模式

    介绍 Android 启动模式之前,先介绍两个概念task和taskAffinity

    • task:翻译过来就是“任务”,是一组相互有关联的 activity 集合,可以理解为 Activity 是在 task 里面活动的。 task 存在于一个称为 back stack 的数据结构中,也就是说, task 是以栈的形式去管理 activity 的,所以也叫可以称为“任务栈”。
    • taskAffinity:官方文档解释是:"The task that the activity has an affinity for.",可以翻译为 activity 相关或者亲和的任务,这个参数标识了一个 Activity 所需要的任务栈的名字。默认情况下,所有Activity所需的任务栈的名字为应用的包名。 taskAffinity 属性主要和 singleTask 启动模式或者 allowTaskReparenting 属性配对使用。

    4种启动模式

    1. standard:标准模式,也是系统默认的启动模式。假如 activity A 启动了 activity B , activity B 则会运行在 activity A 所在的任务栈中。而且每次启动一个 Activity ,都会重新创建新的实例,不管这个实例在任务中是否已经存在。非 Activity 类型的 context (如 ApplicationContext )启动 standard 模式的 Activity 时会报错。非 Activity 类型的 context 并没有所谓的任务栈,由于上面第 1 点的原因所以系统会报错。此解决办法就是为待启动 Activity 指定 FLAG_ACTIVITY_NEW_TASK 标记位,这样启动的时候系统就会为它创建一个新的任务栈。这个时候待启动 Activity 其实是以 singleTask 模式启动的。
    2. singleTop:栈顶复用模式。假如 activity A 启动了 activity B ,就会判断 A 所在的任务栈栈顶是否是 B 的实例。如果是,则不创建新的 activity B 实例而是直接引用这个栈顶实例,同时 onNewIntent 方法会被回调,通过该方法的参数可以取得当前请求的信息;如果不是,则创建新的 activity B 实例。
    3. singleTask:栈内复用模式。在第一次启动这个 Activity 时,系统便会创建一个新的任务,并且初始化 Activity 的实例,放在新任务的底部。不过需要满足一定条件的。那就是需要设置 taskAffinity 属性。前面也说过了, taskAffinity 属性是和 singleTask 模式搭配使用的。

    1. singleInstance:单实例模式。这个是 singleTask 模式的加强版,它除了具有 singleTask 模式的所有特性外,它还有一点独特的特性,那就是此模式的 Activity 只能单独地位于一个任务栈,不与其他 Activity 共存于同一个任务栈。
    收起阅读 »

    Android实现旋转动画的两种方式

    练习案例 视差动画 - 雅虎新闻摘要加载 效果展示 前期准备 第一步:准备好颜色数组 res => values => colors.xml <color name="orange">#FF9600</col...
    继续阅读 »

    练习案例


    视差动画 - 雅虎新闻摘要加载


    效果展示



    前期准备


    第一步:准备好颜色数组 res => values => colors.xml


        <color name="orange">#FF9600</color>
    <color name="aqua">#02D1AC</color>
    <color name="yellow">#FFD200</color>
    <color name="bule">#00C6FF</color>
    <color name="green">#00E099</color>
    <color name="pink">#FF3891</color>

    <array name="splash_circle_colors">
    <item>@color/orange</item>
    <item>@color/aqua</item>
    <item>@color/yellow</item>
    <item>@color/bule</item>
    <item>@color/green</item>
    <item>@color/pink</item>
    </array>


    自定义 View java代码编写


    方法一


    关键思想: 属性动画 + 计算圆心



    package com.wust.mydialog;

    import android.animation.ObjectAnimator;
    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.animation.LinearInterpolator;

    import androidx.annotation.Nullable;


    /**
    * ClassName: com.wust.mydialog.MyRotateView <br/>
    * Description: <br/>
    * date: 2021/8/7 12:13<br/>
    *
    * @author yiqi<br />
    * @QQ 1820762465
    * @微信 yiqiideallife
    * @技术交流QQ群 928023749
    */
    public class MyRotateView extends View {

    //设置旋转间隔时间
    private int SPLASH_CIRCLE_ROTATE_TIME = 3000;
    //设置中心圆半径
    private float CENTER_CIRCLE_RADIUS;
    private float SMALL_CIRCLE_RADIUS;
    private float mCurrentSingle = 0f;
    private int[] mColorArray;
    private Paint mCirclePaint;
    private ValueAnimator va;

    public MyRotateView(Context context) {
    super(context);
    }

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    //初始化参数
    initParams(width,height);

    setMeasuredDimension(width,height);
    }

    private void initParams(int w, int h) {
    //设置中心圆半径
    CENTER_CIRCLE_RADIUS = 1/4.0f * w;
    //设置小圆的半径
    SMALL_CIRCLE_RADIUS = 1/25.0f * w;
    //获取小球颜色
    mColorArray = getResources().getIntArray(R.array.splash_circle_colors);
    //初始化画笔
    mCirclePaint = new Paint();
    mCirclePaint.setDither(true);
    mCirclePaint.setAntiAlias(true);

    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //绘制圆
    drawSplashCircle(canvas);
    }

    private void drawSplashCircle(Canvas canvas) {
    //设置属性动画,让小圆转起来
    //这里得注意,是个坑,你如果不判断那球就不会动 因为会陷入死循环 值动画将值设置为0 -> invalidate()重绘 -> 执行draw 又将值设为0
    if (va == null){
    va = ObjectAnimator.ofFloat(0f, 2 * (float) Math.PI);
    va.setDuration(SPLASH_CIRCLE_ROTATE_TIME);
    va.setRepeatCount(ValueAnimator.INFINITE);
    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    mCurrentSingle = (float) animation.getAnimatedValue();
    // System.out.println("mCurrentSingle ->" + mCurrentSingle);
    invalidate();
    }
    });
    va.setInterpolator(new LinearInterpolator());
    va.start();
    }

    //计算每个小球的间隔
    double spaceAngle = Math.PI*2/mColorArray.length;

    for (int i = 0; i < mColorArray.length; i++) {
    //为 每个球 画笔 设置颜色
    mCirclePaint.setColor(mColorArray[i]);

    //利用 勾股定理 计算 小圆 中心点
    float cx = getWidth()/2 + (float) (CENTER_CIRCLE_RADIUS*Math.cos(spaceAngle*i+mCurrentSingle));
    float cy = getHeight()/2 + (float) (CENTER_CIRCLE_RADIUS*Math.sin(spaceAngle*i+mCurrentSingle));

    canvas.drawCircle(cx,cy,SMALL_CIRCLE_RADIUS,mCirclePaint);
    }
    }
    }

    方法二


    关键思想:旋转画布法


    package com.wust.mydialog;

    import android.animation.ObjectAnimator;
    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.animation.LinearInterpolator;

    import androidx.annotation.Nullable;


    /**
    * ClassName: com.wust.mydialog.MyRotateView <br/>
    * Description: <br/>
    * date: 2021/8/7 12:13<br/>
    *
    * @author yiqi<br />
    * @QQ 1820762465
    * @微信 yiqiideallife
    * @技术交流QQ群 928023749
    */
    public class MyRotateView extends View {

    //设置旋转间隔时间
    private int SPLASH_CIRCLE_ROTATE_TIME = 3000;
    //设置中心圆半径
    private float CENTER_CIRCLE_RADIUS;
    private float SMALL_CIRCLE_RADIUS;
    private float mCurrentSingle = 0f;
    private int[] mColorArray;
    private Paint mCirclePaint;
    private ValueAnimator va;

    public MyRotateView(Context context) {
    super(context);
    }

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    //初始化参数
    initParams(width,height);

    setMeasuredDimension(width,height);
    }

    private void initParams(int w, int h) {
    //设置中心圆半径
    CENTER_CIRCLE_RADIUS = 1/4.0f * w;
    //设置小圆的半径
    SMALL_CIRCLE_RADIUS = 1/25.0f * w;
    //获取小球颜色
    mColorArray = getResources().getIntArray(R.array.splash_circle_colors);
    //初始化画笔
    mCirclePaint = new Paint();
    mCirclePaint.setDither(true);
    mCirclePaint.setAntiAlias(true);

    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //绘制圆
    drawSplashCircle(canvas);
    }

    private void drawSplashCircle(Canvas canvas) {
    //设置属性动画,让小圆转起来
    //这里得注意,是个坑,你如果不判断那球就不会动 因为会陷入死循环 值动画将值设置为0 -> invalidate()重绘 -> 执行draw 又将值设为0
    if (va == null){
    // va = ObjectAnimator.ofFloat(0f, 2 * (float) Math.PI);
    va = ObjectAnimator.ofFloat(0f, 360.0f);
    va.setDuration(SPLASH_CIRCLE_ROTATE_TIME);
    va.setRepeatCount(ValueAnimator.INFINITE);
    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    mCurrentSingle = (float) animation.getAnimatedValue();
    // System.out.println("mCurrentSingle ->" + mCurrentSingle);
    invalidate();
    }
    });
    va.setInterpolator(new LinearInterpolator());
    va.start();
    }

    //计算每个小球的间隔
    // double spaceAngle = Math.PI*2/mColorArray.length;
    double spaceAngle = 360.0d/mColorArray.length;
    System.out.println("spaceAngle -> " + spaceAngle);

    //利用旋转画布法
    canvas.save();
    canvas.rotate(mCurrentSingle,getWidth()/2,getHeight()/2);
    for (int i = 0; i < mColorArray.length; i++) {
    canvas.rotate((float) spaceAngle,getWidth()/2,getHeight()/2);
    //为 每个球 画笔 设置颜色
    mCirclePaint.setColor(mColorArray[i]);

    //利用 勾股定理 计算 小圆 中心点
    //float cx = getWidth()/2 + (float) (CENTER_CIRCLE_RADIUS*Math.cos(spaceAngle*i+mCurrentSingle));
    //float cy = getHeight()/2 + (float) (CENTER_CIRCLE_RADIUS*Math.sin(spaceAngle*i+mCurrentSingle));

    //利用旋转画布法
    float cx = getWidth()/2 + CENTER_CIRCLE_RADIUS;
    float cy = getHeight()/2;

    canvas.drawCircle(cx,cy,SMALL_CIRCLE_RADIUS,mCirclePaint);
    }
    canvas.restore();
    }
    }

    易错点总结: 


    1、canvas.rotate(mCurrentSingle,getWidth()/2,getHeight()/2);中 第一个参数传的是角度(360度的那种),而 Math.cos();中 参数传的是一个弧度(2π的那种


    2、canvas.rotate() 函数执行之后对后续画布上的操作都是有影响的,所以,得配合 canvas.save();和 canvas.restore();使用。因此,里面的canvas.rotate((float) spaceAngle,getWidth()/2,getHeight()/2);中spaceAngle不能乘 i 。


    3、画布的旋转除了 canvas.rotate() 函数 可以实现外,还可以利用矩阵。代码如下:


    //创建矩阵
    private Matrix mSpaceMatrix;
    //初始化旋转矩阵
    mSpaceMatrix = new Matrix();
    //初始化旋转矩阵
    mSpaceMatrix.reset();
    mSpaceMatrix.postRotate((float) spaceAngle,getWidth()/2,getHeight()/2);
    //画布旋转角度
    canvas.concat(mSpaceMatrix);

    完整代码


    package com.wust.mydialog;

    import android.animation.ObjectAnimator;
    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Matrix;
    import android.graphics.Paint;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.animation.LinearInterpolator;

    import androidx.annotation.Nullable;


    /**
    * ClassName: com.wust.mydialog.MyRotateView <br/>
    * Description: <br/>
    * date: 2021/8/7 12:13<br/>
    *
    * @author yiqi<br />
    * @QQ 1820762465
    * @微信 yiqiideallife
    * @技术交流QQ群 928023749
    */
    public class MyRotateView extends View {

    //设置旋转间隔时间
    private int SPLASH_CIRCLE_ROTATE_TIME = 3000;
    //设置中心圆半径
    private float CENTER_CIRCLE_RADIUS;
    private float SMALL_CIRCLE_RADIUS;
    private float mCurrentSingle = 0f;
    private int[] mColorArray;
    private Paint mCirclePaint;
    private ValueAnimator va;
    private Matrix mSpaceMatrix;

    public MyRotateView(Context context) {
    super(context);
    }

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    //初始化参数
    initParams(width,height);

    setMeasuredDimension(width,height);
    }

    private void initParams(int w, int h) {
    //设置中心圆半径
    CENTER_CIRCLE_RADIUS = 1/4.0f * w;
    //设置小圆的半径
    SMALL_CIRCLE_RADIUS = 1/25.0f * w;
    //获取小球颜色
    mColorArray = getResources().getIntArray(R.array.splash_circle_colors);
    //初始化画笔
    mCirclePaint = new Paint();
    mCirclePaint.setDither(true);
    mCirclePaint.setAntiAlias(true);
    //初始化旋转矩阵
    mSpaceMatrix = new Matrix();
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //绘制圆
    drawSplashCircle(canvas);
    }

    private void drawSplashCircle(Canvas canvas) {
    //设置属性动画,让小圆转起来
    //这里得注意,是个坑,你如果不判断那球就不会动 因为会陷入死循环 值动画将值设置为0 -> invalidate()重绘 -> 执行draw 又将值设为0
    if (va == null){
    // va = ObjectAnimator.ofFloat(0f, 2 * (float) Math.PI);
    va = ObjectAnimator.ofFloat(0f, 360.0f);
    va.setDuration(SPLASH_CIRCLE_ROTATE_TIME);
    va.setRepeatCount(ValueAnimator.INFINITE);
    va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
    mCurrentSingle = (float) animation.getAnimatedValue();
    // System.out.println("mCurrentSingle ->" + mCurrentSingle);
    invalidate();
    }
    });
    va.setInterpolator(new LinearInterpolator());
    va.start();
    }

    //计算每个小球的间隔
    // double spaceAngle = Math.PI*2/mColorArray.length;
    double spaceAngle = 360.0d/mColorArray.length;
    //初始化旋转矩阵
    mSpaceMatrix.reset();
    mSpaceMatrix.postRotate((float) spaceAngle,getWidth()/2,getHeight()/2);


    //利用旋转画布法
    canvas.save();
    canvas.rotate(mCurrentSingle,getWidth()/2,getHeight()/2);
    for (int i = 0; i < mColorArray.length; i++) {
    // canvas.rotate((float) spaceAngle,getWidth()/2,getHeight()/2);
    // System.out.println("spaceAngle -> " + spaceAngle);
    canvas.concat(mSpaceMatrix);
    //为 每个球 画笔 设置颜色
    mCirclePaint.setColor(mColorArray[i]);

    //利用 勾股定理 计算 小圆 中心点
    //float cx = getWidth()/2 + (float) (CENTER_CIRCLE_RADIUS*Math.cos(spaceAngle*i+mCurrentSingle));
    //float cy = getHeight()/2 + (float) (CENTER_CIRCLE_RADIUS*Math.sin(spaceAngle*i+mCurrentSingle));

    //利用旋转画布法
    float cx = getWidth()/2 + CENTER_CIRCLE_RADIUS;
    float cy = getHeight()/2;

    canvas.drawCircle(cx,cy,SMALL_CIRCLE_RADIUS,mCirclePaint);
    }
    canvas.restore();
    }
    }

    注意事项:


    1、canvas.concat(mSpaceMatrix);对画布的操作也会对后面进行影响


    2、Android中Matrix的set、pre、post的区别



    说set、pre、post的区别之前,先说说Matrix。



    Matrix包含一个3 X 3的矩阵,专门用于图像变换匹配。


    Matrix提供了四种操作:



    • translate(平移)

    • rotate(旋转)

    • scale(缩放)

    • skew(倾斜)


    也就是说这4种操作都是对这个3 X 3的矩阵设值来达到变换的效果。


    Matrix没有结构体,它必须被初始化,通过reset或set方法。


    OK,Matrix介绍完了,我们来看看set、pre、post的区别。


    pre是在队列最前面插入,post是在队列最后面追加,而set先清空队列在添加(这也是上文提到的“Matrix没有结构体,它必须被初始化,通过reset或set方法”的原因)。


    下面通过一些例子具体说明:



    1. matrix.preScale(2f,1f);    

    2. matrix.preTranslate(5f, 0f);   

    3. matrix.postScale(0.2f, 1f);    

    4. matrix.postTranslate(0.5f, 0f);  


    执行顺序:translate(5, 0) -> scale(2f, 1f) -> scale(0.2f, 1f) -> translate(0.5f, 0f)



    1. matrix.postTranslate(2f, 0f);   

    2. matrix.preScale(0.2f, 1f);     

    3. matrix.setScale(1f, 1f);   

    4. matrix.postScale(5f, 1f);   

    5. matrix.preTranslate(0.5f, 0f);   


    执行顺序:translate(0.5f, 0f) -> scale(1f, 1f) ->  scale(5f, 1)


    收起阅读 »

    Flutter AnimatedList 使用解析

    志在巅峰的攀登者,不会陶醉在沿途的某个脚印之中,在码农的世界里,优美的应用体验,来源于程序员对细节的处理以及自我要求的境界,年轻人也是忙忙碌碌的码农中一员,每天、每周,都会留下一些脚印,就是这些创作的内容,有一种执着,就是不知为什么,如果你迷茫,不妨来瞅瞅码农...
    继续阅读 »



    志在巅峰的攀登者,不会陶醉在沿途的某个脚印之中,在码农的世界里,优美的应用体验,来源于程序员对细节的处理以及自我要求的境界,年轻人也是忙忙碌碌的码农中一员,每天、每周,都会留下一些脚印,就是这些创作的内容,有一种执着,就是不知为什么,如果你迷茫,不妨来瞅瞅码农的轨迹。



    在 Flutter 中 ,AnimatedList 、ListView 、SliverListView 、SliverAnimatedList 用来显示列表数据样式,一般情况下 使用 ListView 就可实现大部分业务需求,AnimatedList 的特点是可以在插入数据与移除数据的时候添加动画效果


    本文章实现的效果是


    在这里插入图片描述


    AnimatedList 简述


      const AnimatedList({
    Key? key,
    required this.itemBuilder,
    this.initialItemCount = 0,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.controller,
    this.primary,
    this.physics,
    this.shrinkWrap = false,
    this.padding,
    this.clipBehavior = Clip.hardEdge,
    })


    • itemBuilder 子 Item UI布局构建

    • initialItemCount 列表显示的条目 个数

    • scrollDirection 滑动方向

    • reverse scrollDirection 为 Axis.vertical 时,如果 reverse 为ture ,那么列表内容会自动滑动到底部

    • controller 滑动控制器

    • physics 滑动效果控制 ,BouncingScrollPhysics 是列表滑动 iOS 的回弹效果;AlwaysScrollableScrollPhysics 是 列表滑动 Android 的水波纹回弹效果;ClampingScrollPhysics 普通的滑动效果;

    • shrinkWrap 为true的时候 AnimatedList 会包裹所有的子Item


    本实例 Demo


    首先是我初始化了点数据‘


      List<String> _list = [];

    GlobalKey<AnimatedListState> _globalKey = new GlobalKey();

    @override
    void initState() {
    super.initState();
    for (int i = 0; i < 10; i++) {
    _list.add("早起的年轻人 $i");
    }
    }

    然后就是 AnimatedList 的使用如下:


      AnimatedList buildAnimatedList() {
    return AnimatedList(
    //关键key
    key: _globalKey,
    //列表个数
    initialItemCount: _list.length,
    //每个子Item
    itemBuilder:
    (BuildContext context, int index, Animation<double> animation) {
    return buildSizeTransition(animation, index);
    },
    );
    }

    然后列表中的每个 Item 的 UI 布局构建如下


     SizeTransition buildSizeTransition(Animation<double> animation, int index) {
    //来个动画
    return SizeTransition(
    //动画构建
    sizeFactor: animation,
    //子UI
    child: SizedBox(
    height: 80.0,
    child: Card(
    color: Colors.primaries[index % Colors.primaries.length],
    child: Center(
    child: Text(
    'Item $index ${_list[index]}',
    ),
    ),
    ),
    ),
    );
    }

    然后我们插入一条数据


    //插入源数据
    _list.insert(0, "插入数据 ${DateTime.now()}");
    //插入动画效果
    _globalKey.currentState.insertItem(
    0,//插入的位置
    duration: Duration(milliseconds: 400),
    );
    },

    然后移除一条数据


     //移除源数据
    _list.removeAt(0);
    //移除动画效果
    _globalKey.currentState.removeItem(
    0,
    (BuildContext context, Animation<double> animation) {
    return buildSizeTransition(animation, 0);
    },
    );

    收起阅读 »

    Android 65536启用 multidex

    前言         起因:项目使用的一直是multidex:1.0.3版本就想着版本低了要不要升级一下。惊喜就这么来了。 65536    &...
    继续阅读 »

    前言


            起因:项目使用的一直是multidex:1.0.3版本就想着版本低了要不要升级一下。惊喜就这么来了。


    65536


            当你的应用及其引用的库超过 65,536 个方法时,你会遇到构建错误,表明你的应用已达到 Android 构建架构的限制:


    trouble writing output:
    Too many field references: 131000; max is 65536.
    You may try using --multi-dex option.

            旧版本的构建系统报告了不同的错误,这表明存在相同的问题:


    Conversion to Dalvik format failed:
    Unable to execute dex: method ID not in [0, 0xffff]: 65536

            这两种错误情况都显示一个共同的数字:65536。这个数字表示单个 Dalvik 可执行文件 (DEX) 字节码文件中的代码可以调用的引用总数。本问介绍了如何通过启用称为multidex的应用程序配置来克服此限制,该配置允许你的应用程序构建和读取多个 DEX 文件。


    关于 64K 参考限制


            Android 应用 APK 文件包含 Dalvik 可执行文件 DEX 文件形式的可执行字节码文件,其中包含用于运行应用的编译代码。Dalvik Executable 规范将单个 DEX 文件中可以引用的方法总数限制为 65,536,包括 Android 框架方法、库方法和你自己代码中的方法。在计算机科学的上下文中,术语Kilo, K表示 1024(或 2^10)。由于 65,536 等于 64 X 1024,因此此限制称为64K 参考限制


    解决64K限制


    对 Android 5.0 及更高版本的 Multidex 支持


            Android 5.0(API 级别 21)及更高版本使用称为 ART 的运行时,该运行时本机支持从 APK 文件加载多个 DEX 文件。 ART 在应用安装时执行预编译,它会扫描 classesN.dex 文件并将它们编译成单个 .oat 文件以供 Android 设备执行。 因此,如果你的 minSdkVersion 为 21 或更高,则默认启用 multidex,并且你不需要 multidex 库



    注意: 当使用 Android Studio 运行你的应用程序时,构建会针对你部署到的目标设备进行优化。 这包括在目标设备运行 Android 5.0 及更高版本时启用 multidex。 由于此优化仅在使用 Android Studio 部署应用程序时应用,因此你可能仍需要为 multidex 配置发布版本以避免 64K 限制。




            看到没,这里最好的解决办法就是设置minSdkVersion设置为 21 或更高。这样网上的一些什么



    • multidex你遇到的坑


    • multidex与你不得不说的秘密


    • multidex原理及实现就和你没得关系了,当然你想了解也可以。



            Android SDK 为 21 或更高的问题解决了,那Android SDK 低于 21 的呢。咱接着往下看喽。


    Android 5.0 之前的 Multidex 支持


    为你的应用程序配置 multidex


            如果你的 minSdkVersion 设置为 21 或更高,则默认启用 multidex,你不需要 multidex 库。


            但是,如果你的 minSdkVersion 设置为 20 或更低,那么你必须使用 multidex 库并对你的应用项目进行以下修改:


    1.修改模块级 build.gradle 文件以启用 multidex 并添加 multidex 库作为依赖项,如下所示:



    • 使用AndroidX



    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 30
            multiDexEnabled true
        }
        ...
    }

    dependencies {
        implementation "androidx.multidex:multidex:2.0.1"
    }


    • 不使用AndroidX(已弃用)



    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 30
            multiDexEnabled true
        }
        ...
    }

    dependencies {
        implementation 'com.android.support:multidex:1.0.3'
    }

    2.根据你是否覆盖 Application 类,执行以下操作之一:



    • 如果你不覆盖 Application 类,请编辑你的清单文件以在 < application > 标记中设置 android:name,如下所示:



    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.scc.demo">
        <application
                android:name="androidx.multidex.MultiDexApplication" >
            ...
        </application>
    </manifest>


    • 如果你确实覆盖了 Application 类,请将其更改为扩展 MultiDexApplication(非必须),如下所示:



    public class MyApplication extends MultiDexApplication { ... }


    • 或者,如果你确实覆盖了 Application 类但无法更改基类,那么你可以覆盖 attachBaseContext() 方法和 callMultiDex.install(this) 以启用 multidex:



    public class MyApp extends Application {
      @Override
      protected void attachBaseContext(Context base) {
         super.attachBaseContext(base);
         MultiDex.install(this);
      }
    }


     

    注意: 在 MultiDex.install() 完成之前,不要通过反射或 JNI 执行 MultiDex.install() 或任何其他代码。 Multidex 跟踪不会跟随这些调用,导致 ClassNotFoundException 或由于 DEX 文件之间的类分区错误而导致验证错误。



            现在,当你构建应用程序时,Android 构建工具会根据需要构建一个主要的 DEX 文件 (classes.dex) 和支持的 DEX 文件(classes2.dex、classes3.dex 等)。构建系统然后将所有 DEX 文件打包到你的 APK 中。


            在运行时,multidex API 使用特殊的类加载器来搜索所有可用的 DEX 文件以查找你的方法(而不是仅在主 classes.dex 文件中搜索)。


    multidex 库的限制


            multidex 库有一些已知的限制,当你将其合并到应用程序构建配置中时,你应该注意并测试这些限制:



    • 在启动期间将 DEX 文件安装到设备的数据分区上很复杂,如果辅助 DEX 文件很大,可能会导致应用程序无响应 (ANR) 错误。为避免此问题,请启用代码收缩以最小化 DEX 文件的大小并删除未使用的代码部分。


    • 在 Android 5.0(API 级别 21)之前的版本上运行时,使用 multidex 不足以解决 linearalloc 限制(问题 78035)。此限制在 Android 4.0(API 级别 14)中有所增加,但这并没有完全解决。在低于 Android 4.0 的版本上,你可能会在达到 DEX 索引限制之前达到线性分配限制。因此,如果你的目标 API 级别低于 14,请在平台的这些版本上进行彻底测试*,因为你的应用程序可能在启动时或加载特定类组时出现问题。



            代码缩减可以减少或可能消除这些问题。


    在主 DEX 文件中声明所需的类


            如果你收到 java.lang.NoClassDefFoundError,那么你必须通过在构建类型中使用 multiDexKeepFilemultiDexKeepProguard 属性声明它们,根据主 DEX 文件中的要求手动指定这些附加类。 如果某个类在 multiDexKeepFile 或 multiDexKeepProguard 文件中匹配,则该类将添加到主 DEX 文件中。


    multiDexKeepFile 属性


            你在multiDexKeepFile其中指定的文件应每行包含一个类,格式为com/example/MyClass.class. 例如,你可以创建一个如下所示的文件multidex-config.txt:


    com/scc/MyClass.class
    com/scc/MyOtherClass.class

            然后,你可以为构建类型声明该文件,如下所示:


    android {
        buildTypes {
            release {
                multiDexKeepFile file('multidex-config.txt')
                ...
            }
        }
    }


     

    注意: Gradle 读取相对于build.gradle文件的路径,因此如果multidex-config.txt与build.gradle文件位于同一目录中,则上述示例有效。



    multiDexKeepProguard 属性


            该multiDexKeepProguard文件使用与 Proguard 相同的格式,并支持整个 Proguard 语法。


            你在multiDexKeepProguard其中指定的文件应包含 -keep 任何有效 ProGuard 语法中的选项。例如, -keep com.scc.MyClass.class。你可以创建一个名为的文件 multidex-config.pro,如下所示:


    -keep class com.scc.MyClass
    -keep class com.scc.MyClassToo

            如果要指定包中的所有类,文件如下所示:


    -keep class com.scc.** { *; } // com.scc 包中的所有类 

            然后,你可以为构建类型声明该文件,如下所示:


    android {
        buildTypes {
            release {
                multiDexKeepProguard file('multidex-config.pro')
                ...
            }
        }
    }

    在开发版本中优化 multidex


            为了减少更长的增量构建时间,使用 pre-dexing在构建之间重用 multidex 输出。Pre-dexing 依赖于仅在 Android 5.0(API 级别 21)及更高版本上可用的 ART 格式。如果你使用的是 Android Studio 2.3 及更高版本,则在将你的应用程序部署到运行 Android 5.0(API 级别 21)或更高版本的设备时,IDE 会自动使用此功能



     

    注意: 适用于 Gradle 3.0.0及更高版本的Android 插件包括进一步改进以优化构建速度,例如按类进行 dexing(以便仅对你修改的类进行重新索引)。一般来说,为了获得最佳的开发体验,你应该始终升级到 最新版本的 Android Studio和 Android 插件。



            但是,如果你从命令行运行 Gradle 构建,则需要将 minSdkVersion 设置为 21 或更高以启用 pre-dexing。保留生产版本设置的一个有用策略是使用产品风格创建两个版本的应用程序 :开发风格和发布风格,具有不同的值minSdkVersion,如下所示。


    android {
        defaultConfig {
            ...
            multiDexEnabled true
            //最低 API 级别。 
            minSdkVersion 15
        }
        productFlavors {
            dev {
                //(API 级别 21)或更高 .
                minSdkVersion 21
            }
            prod {
                // 如果你已经为生产版本配置了 defaultConfig 块
                // 你的应用程序,你可以将此块留空,Gradle 会使用其中的配置
                // 代替 defaultConfig 块。 你仍然需要包括这种味道。
                // 否则,所有变体都使用“dev”配置。 
            }
        }
        buildTypes {
            release {
                minifyEnabled true
                proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                     'proguard-rules.pro'
            }
        }
    }
    dependencies {
        implementation "androidx.multidex:multidex:2.0.1"
    }

    避免 64K 限制


            在配置应用以启用 64K 或更多方法引用之前,你应该采取措施减少应用代码调用的引用总数,包括应用代码定义的方法或包含的库。以下策略可以帮助你避免达到 DEX 参考限制:



    • 检查你的应用程序的直接和传递依赖项 - 确保你在应用程序中包含的任何大型库依赖项的使用方式都超过添加到应用程序的代码量。一个常见的反模式是包含一个非常大的库,因为一些实用方法是有用的。减少你的应用程序代码依赖性通常可以帮助你避免 DEX 引用限制。


    • 使用 R8 删除未使用的代码 -启用代码收缩以运行 R8 以用于你的发布版本。启用收缩可确保你不会随 APK 一起发送未使用的代码。



            使用这些技术可能会帮助你避免在应用中启用 multidex,同时还可以减少 APK 的整体大小。


    以上就是本文的全部内容,希望对大家学习Android multidex有所帮助和启发。

    收起阅读 »

    实现activity跳转动画的若干种方式

    第一种: (使用overridePendingTransition方法实现Activity跳转动画) 在Activity中代码如下 /** * 点击按钮实现跳转逻辑 */ button1.setOnClickListener(new View.OnClic...
    继续阅读 »

    第一种: (使用overridePendingTransition方法实现Activity跳转动画)


    在Activity中代码如下


    /**
    * 点击按钮实现跳转逻辑
    */
    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    /**
    * 在调用了startActivity方法之后立即调用overridePendingTransition方法
    */
    Intent intent = new Intent(MainActivity.this, SecondActivity.class);
    startActivity(intent);
    overridePendingTransition(R.anim.slide_in_left, R.anim.slide_in_left);
    }
    });


    在anim文件下代码如下


    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:shareInterpolator="false"
    Android:zAdjustment="top">

    <translate
    Android:duration="200"
    Android:fromXDelta="-100.0%p"
    Android:toXDelta="0.0" />
    </set>


    第二种: (使用style的方式定义Activity的切换动画)


    从清单文件入手


    <!-- 系统Application定义 -->
    <application
    Android:allowBackup="true"
    Android:icon="@mipmap/ic_launcher"
    Android:label="@string/app_name"
    Android:supportsRtl="true"
    Android:theme="@style/AppTheme">


    进入AppTheme


    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="Android:windowAnimationStyle">@style/activityAnim</item>
    </style>


    <!-- 使用style方式定义activity切换动画 -->
    <style name="activityAnim">
    <item name="Android:activityOpenEnterAnimation">@anim/slide_in_top</item>
    <item name="Android:activityOpenExitAnimation">@anim/slide_in_top</item>
    </style>



    在windowAnimationStyle中存在四种动画


    activityOpenEnterAnimation // 用于设置打开新的Activity并进入新的Activity展示的动画
    activityOpenExitAnimation // 用于设置打开新的Activity并销毁之前的Activity展示的动画
    activityCloseEnterAnimation // 用于设置关闭当前Activity进入上一个Activity展示的动画
    activityCloseExitAnimation // 用于设置关闭当前Activity时展示的动画


    Activity中的测试代码如下


    /**
    * 点击按钮,实现Activity的跳转操作
    * 通过定义style的方式实现activity的跳转动画
    */
    button2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    /**
    * 普通的Intent跳转Activity实现
    */
    Intent intent = new Intent(MainActivity.this, SecondActivity.class);
    startActivity(intent);
    }
    });


    第三种: (使用ActivityOptions切换动画实现Activity跳转动画)


    第一步


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // 设置contentFeature,可使用切换动画
    getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
    Transition explode = TransitionInflater.from(this).inflateTransition(Android.R.transition.explode);
    getWindow().setEnterTransition(explode);

    setContentView(R.layout.activity_main);
    }


    第二步


    /**
    * 点击按钮,实现Activity的跳转操作
    * 通过Android5.0及以上代码的方式实现activity的跳转动画
    */
    button3.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    Intent intent = new Intent(MainActivity.this, ThreeActivity.class);
    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
    }
    });


    第四种: (使用ActivityOptions之后内置的动画效果通过style的方式)


    先在Application项目res目录下新建一个transition目录,然后创建资源文件activity_explode,编写如下代码


    <explode xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:duration="300" />


    定义style文件



    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>

    <item name="Android:windowEnterTransition">@transition/activity_explode</item>
    <item name="Android:windowExitTransition">@transition/activity_explode</item>
    </style>


    执行跳转逻辑


    /**
    * 点击按钮,实现Activity的跳转操作
    * 通过Android5.0及以上style的方式实现activity的跳转动画
    */
    button4.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    /**
    * 调用ActivityOptions.makeSceneTransitionAnimation实现过度动画
    */
    Intent intent = new Intent(MainActivity.this, FourActivity.class);
    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this).toBundle());
    }
    });


    第五种: (使用ActivityOptions动画共享组件的方式实现跳转Activity动画)


    在Acitivity_A中布局文件中定义共享组件


    <Button
    Android:id="@+id/button5"
    Android:layout_width="match_parent"
    Android:layout_height="wrap_content"
    Android:layout_below="@+id/button4"
    Android:layout_marginTop="10dp"
    Android:layout_marginRight="10dp"
    Android:layout_marginLeft="10dp"
    Android:text="组件过度动画"
    Android:background="@color/colorPrimary"
    Android:transitionName="shareNames"
    />


    在Acitivity_B中布局文件中关联共享组件


    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:id="@+id/activity_second"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:gravity="center_horizontal"
    Android:orientation="vertical"
    Android:transitionName="shareNames"
    >

    <TextView
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:background="@color/colorAccent"
    Android:layout_marginTop="10dp"
    Android:layout_marginBottom="10dp"
    />


    执行跳转逻辑


    /**
    * 点击按钮,实现Activity的跳转操作
    * 通过Android5.0及以上共享组件的方式实现activity的跳转动画
    */
    button5.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    Intent intent = new Intent(MainActivity.this, FiveActivity.class);
    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(MainActivity.this, button5, "shareNames").toBundle());
    }
    });


    总结:




    • overridePendingTransition方法从Android2.0开始,基本上能够覆盖我们activity跳转动画的需求;




    • ActivityOptions API是在Android5.0开始的,可以实现一些炫酷的动画效果,更加符合MD风格;




    • ActivityOptions还可以实现两个Activity组件之间的过度动画;


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

    Android Activity通讯方式

    Android Activity通讯方式Activity 之间传递信息是很常见的方式,比如页面的跳转需要携带信息,比如第一个页面的参数需要到第二个页面显示,Android中对这种传值通讯提供了多种方式,这些方式又有什么异同呢。一、Bundle传递含义:把数据封...
    继续阅读 »

    Android Activity通讯方式

    Activity 之间传递信息是很常见的方式,比如页面的跳转需要携带信息,比如第一个页面的参数需要到第二个页面显示,Android中对这种传值通讯提供了多种方式,这些方式又有什么异同呢。

    一、Bundle传递

    含义:把数据封装在Bundle 对象中,通过 Intent 跳转时携带。

    伪代码

    传递基本数据类型和String类型


    // 传递
    Bundle bundle = new Bundle();
    bundle.putString("name", "Jack");
    bundle.putInt("id", 1);

    Intent intent = new Intent(this, MainActivity2.class);
    intent.putExtras(bundle);
    startActivity(intent);

    // 接收
    Bundle bundle = getIntent().getExtras();
    String name = bundle.getString("name");
    int id = bundle.getInt("id");

    Log.d("===", "name:" + name + " _ id:" + id);

    传递对象

    对象传递前需要对对象进行序列化,否则会报错。

    需要注意的是

    通过序列化传递的对象,传输和接收的对象虽然内容相同,但是引用地址是不同的。 也就是说改了接收的对象改了值,原始传递页面的对象不受影响

    • 何为序列化?

    序列化是为了将对象数据转换成字节流的形式,方便进行传输。 所以在传递的对象的时候我们需要进行序列化,在接收端再进行反序列化就可以恢复对象。

    • 如何实现序列化

    Serializable 和 Parcelable

    Serializable: 实体类直接实现Serializable接口


    import java.io.Serializable;
    public class Student implements Serializable {

    private int id;
    private String name;

    public Student() {
    }

    public Student(int id, String name) {
    this.id = id;
    this.name = name;
    }

    public int getId() {
    return id;
    }

    public void setId(int id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }
    }


    // 发送
    Student student = new Student(1, "Jack");
    Bundle bundle = new Bundle();bundle.putSerializable("student", student);
    Intent intent = new Intent(this, MainActivity2.class);
    intent.putExtras(bundle);startActivity(intent);


    // 接收
    Bundle bundle = getIntent().getExtras();
    Student student = (Student) bundle.getSerializable("student");
    Log.d("===", "person:"+student.getName());

    Parcelable: 实现接口并且实现方法

    import android.os.Parcel;
    import android.os.Parcelable;

    public class Student implements Parcelable {

    private int id;
    private String name;

    public Student() {
    }

    public Student(int id, String name) {
    this.id = id;
    this.name = name;
    }

    protected Student(Parcel in) {
    id = in.readInt();
    name = in.readString();
    }

    public static final Creator<Student> CREATOR = new Creator<Student>() {
    @Override
    public Student createFromParcel(Parcel in) {
    return new Student(in);
    }

    @Override
    public Student[] newArray(int size) {
    return new Student[size];
    }
    };

    public int getId() {
    return id;
    }

    public void setId(int id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    @Override
    public int describeContents() {
    return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(id);
    dest.writeString(name);
    }
    }


    // 发送
    Student student = new Student(1, "Jack");
    Bundle bundle = new Bundle();
    bundle.putParcelable("student", student);
    Intent intent = new Intent(this, MainActivity2.class);
    intent.putExtras(bundle);
    startActivity(intent);

    // 接收
    Bundle bundle = getIntent().getExtras();
    Student student = (Student) bundle.getParcelable("student");
    Log.d("===", "person:"+student.getName());

    从代码看来,Serializable 来自Java,而Parcelable 来自Android,Parcelable 是Android优化过后的产物,在相同条件下,Parcelable 可以减少很大的内存占用。

    二、广播传递

    广播是Android 中的四大组件之一,相当于一个全局监听器,可以监听其它App或者系统的广播消息。

    在Activity之间虽然也可以传递数据,但是有点大材小用。

    三、外部存储

    如果有大量的数据,在 A Activity 中可以将数据临时保存在存储卡中,跳转到B Activity 后再从存储卡中取出。

    四、静态变量

    将数据保存在静态变量中,在 A Activity 中对静态变量进行赋值,跳转到B Activity 后从静态变量获取数据,然后回收静态变量。

    五、Application 中转

    自定义的application类,临时保存变量,为了代码整洁,一般不用。

    六、ARouter

    Arouter 也有一些传递消息的方法。比如 withObject、withString 等

    看看它们的内部方法

    /**
    * Set object value, the value will be convert to string by 'Fastjson'
    *
    * @param key a String, or null
    * @param value a Object, or null
    * @return current
    */
    public Postcard withObject(@Nullable String key, @Nullable Object value) {
    serializationService = ARouter.getInstance().navigation(SerializationService.class);
    mBundle.putString(key, serializationService.object2Json(value));
    return this;
    }


    /**
    * Inserts a String value into the mapping of this Bundle, replacing
    * any existing value for the given key. Either key or value may be null.
    *
    * @param key a String, or null
    * @param value a String, or null
    * @return current
    */
    public Postcard withString(@Nullable String key, @Nullable String value) {
    mBundle.putString(key, value);
    return this;
    }


    同样是用Bundle传输,原理和Bundle一致。

    七、EventBus

    在使用Bundle传递数据时,当数据量过大(超过1M时),就会抛出 TransactionTooLargeException 异常。

    使用 EventBus 可以很轻松的解决这个问题。

    1. EventBus简介

    EventBus是一种用于Android的事件发布-订阅总线,由GreenRobot开发,可以很方便的进行数据传递。

    2. EventBus 三个组成部分

    • Event:事件,它可以是任意类型,EventBus会根据事件类型进行全局的通知。
    • Subscriber:事件订阅者,在EventBus 3.0之前我们必须定义以onEvent开头的那几个方法,分别是onEvent、onEventMainThread、onEventBackgroundThread和onEventAsync,而在3.0之后事件处理的方法名可以随意取,不过需要加上注解@subscribe,并且指定线程模型,默认是POSTING。
    • Publisher:事件的发布者,可以在任意线程里发布事件。一般情况下,使用EventBus.getDefault()就可以得到一个EventBus对象,然后再调用post(Object)方法即可。

    3. EventBus 四种线程模型

    • POSTING:默认,表示事件处理函数的线程跟发布事件的线程在同一个线程。
    • MAIN:表示事件处理函数的线程在主线程(UI)线程,因此在这里不能进行耗时操作。
    • BACKGROUND:表示事件处理函数的线程在后台线程,因此不能进行UI操作。如果发布事件的线程是主线程(UI线程),那么事件处理函数将会开启一个后台线程,如果果发布事件的线程是在后台线程,那么事件处理函数就使用该线程。
    • ASYNC:表示无论事件发布的线程是哪一个,事件处理函数始终会新建一个子线程运行,同样不能进行UI操作。

    4. EventBus 实战

    4.1 引入依赖

    implementation 'org.greenrobot:eventbus:3.1.1'

    4.2 新建一个实体类,作为传递的对象

    public class MessageInfo {

        private String message;

        public MessageInfo(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }

        public void setMessage(String message) {
            this.message = message;
        }
    }

    4.3 定义接收事件

    Activity:

    public class MainActivity extends AppCompatActivity {
      
      private TextView tv;

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

            tv = findViewById(R.id.tv);

            EventBus.getDefault().register(this); //初始化EventBus
        }


        @Override
        protected void onDestroy() {
            super.onDestroy();

            EventBus.getDefault().unregister(this); //释放
        }


        // 定义接收的事件
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void getMessage(MessageInfo messageInfo) {
            tv.setText(messageInfo.getMessage());
            Toast.makeText(this, "接收到的消息为:" + messageInfo.getMessage(), Toast.LENGTH_SHORT).show();
        }


        public void GoMain2Activity(View view) {
            Intent intent = new Intent(this, Main2Activity.class);
            startActivity(intent);
        }
    }

    layout:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 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"
        tools:context=".MainActivity">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="GoMain2Activity"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


        <TextView
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text=""/>

    </android.support.constraint.ConstraintLayout>

    4.4 定义发送事件

    Activity:

    public class Main2Activity extends AppCompatActivity {

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


        /**
         * 发送消息
         *
         * @param view
         */
        public void publishMessage(View view) {
            EventBus.getDefault().post(new MessageInfo("小李子"));
        }
    }

    layout:

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 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"
        tools:context=".Main2Activity">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:onClick="publishMessage"/>

    </android.support.constraint.ConstraintLayout>

    4.5 粘性事件

    所谓的黏性事件,就是指发送了该事件之后再订阅者依然能够接收到的事件。使用黏性事件的时候有两个地方需要做些修改。一个是订阅事件的地方,这里我们在先打开的Activity中注册监听黏性事件: 添加 sticky = true 属性


    // 定义接收的事件
        @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
        public void getMessage(MessageInfo messageInfo) {
            tv.setText(messageInfo.getMessage());
            Toast.makeText(this, "接收到的消息为:" + messageInfo.getMessage(), Toast.LENGTH_SHORT).show();
        }

    在发送事件时使用postSticky来发送:

      /**
         * 发送消息
         *
         * @param view
         */
        public void publishMessage(View view) {
            EventBus.getDefault().postSticky(new MessageInfo("小李子"));
            Intent intent = new Intent(this, MainActivity.class);
            startActivity(intent);
        }


    4.6 优先级

    在Subscribe注解中总共有3个参数,上面我们用到了其中的两个,这里我们使用以下第三个参数,即priority。它用来指定订阅方法的优先级,是一个整数类型的值,默认是0,值越大表示优先级越大。在某个事件被发布出来的时候,优先级较高的订阅方法会首先接受到事件。 这里有几个地方需要注意:

    • 只有当两个订阅方法使用相同的ThreadMode参数的时候,它们的优先级才会与priority指定的值一致;
    • 只有当某个订阅方法的ThreadMode参数为POSTING的时候,它才能停止该事件的继续分发。

    八、EventBus 问题汇总(持续更新...)

    问题一、EventBus 粘性事件接收不到的问题

    1、起因

    由于EventBus发送的是对象,我们经常构建一个共用的对象,在共用对象中添加tag,用于方便在接收中区分作用。

    例如用EventBusHelper工具类发送EventBusMessage对象。

    2、遇到的问题

    在发送粘性事件时,在A场景发送了一次,然后还没有接收,然后在B场景又发送了一次,这时B发送的粘性事件可以收到,而A场景的粘性事件被替换掉了。

    这时因为在EventBus的源码中,粘性事件使用Map集合存储,key为 object.getClass(), 当我们自定义EventBusMessage的时候,导致object.getClass()一直是相同的,以至于会替换前一次的key。

        public void postSticky(Object event) {
    synchronized (stickyEvents) {
    stickyEvents.put(event.getClass(), event);
    }
    // Should be posted after it is putted, in case the subscriber wants to remove immediately
    post(event);
    }

    3、解决方法

    创造多个单独的对象,用于发送粘性服务。

    上面的方法比较Low,还没有想到比较优雅的方法,有的话分享出来吧。

    问题二、Post/postSticky 不能传递基本类型。

    eg: 使用post/postSticky直接传递,在Subscribe处接收int,此时收不到消息。 将int tag改为 Integer tag 就能收到。

    这时因为EventBus中要求传递的为Object,Object属于类类型,也就是复合数据类型,int属于简单数据类型。

     EventBus.getDefault().post(111);

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void getMessage(int tag) {
    if (tag == 111)
    Log.d(TAG, "received");
    }



    收起阅读 »

    关于 PendingIntent 您需要知道的那些事

    PendingIntent 是 Android 框架中非常重要的组成部分,但是目前大多数与该主题相关的开发者资源更关注它的实现细节,即 "PendingIntent 是由系统维护的 token 引用",而忽略了它的用途。由于 Android 12 对 Pend...
    继续阅读 »

    PendingIntent 是 Android 框架中非常重要的组成部分,但是目前大多数与该主题相关的开发者资源更关注它的实现细节,即 "PendingIntent 是由系统维护的 token 引用",而忽略了它的用途。

    由于 Android 12 对 PendingIntent 进行了 重要更新,包括需要显式确定 PendingIntent 是否是可变的,所以我认为有必要和大家深入聊聊 PendingIntent 有什么作用,系统如何使用它,以及为什么您会需要可变类型的 PendingIntent。

    PendingIntent 是什么?

    PendingIntent 对象封装了 Intent 对象的功能,同时以您应用的名义指定其他应用允许哪些操作的执行,来响应用户未来会进行的操作。比如,所封装的 Intent 可能会在闹铃关闭后或者用户点击通知时被触发。

    PendingIntent 的关键点是其他应用在触发 intent 时是 以您应用的名义。换而言之,其他应用会使用您应用的身份来触发 intent。

    为了让 PendingIntent 具备和普通 Intent 一样的功能,系统会使用创建 PendingIntent 时的身份来触发它。在大多数情况下,比如闹铃和通知,其中所用到的身份就是应用本身。

    我们来看应用中使用 PendingIntent 的不同方式,以及我们使用这些方式的原因。

    常规用法

    使用 PendingIntent 最常规最基础的用法是作为关联某个通知所进行的操作。

    val intent = Intent(applicationContext, MainActivity::class.java).apply {
    action = NOTIFICATION_ACTION
    data = deepLink
    }
    val pendingIntent = PendingIntent.getActivity(
    applicationContext,
    NOTIFICATION_REQUEST_CODE,
    intent,
    PendingIntent.FLAG_IMMUTABLE
    )
    val notification = NotificationCompat.Builder(
    applicationContext,
    NOTIFICATION_CHANNEL
    ).apply {
    // ...
    setContentIntent(pendingIntent)
    // ...
    }.build()
    notificationManager.notify(
    NOTIFICATION_TAG,
    NOTIFICATION_ID,
    notification
    )

    可以看到我们构建了一个标准类型的 Intent 来打开我们的应用,然后,在添加到通知之前简单用 PendingIntent 封装了一下。

    在本例中,由于我们明确知道未来需要进行的操作,所以我们使用 FLAG_IMMUTABLE 标记构建了无法被修改的 PendingIntent

    调用 NotificationManagerCompat.notify() 之后工作就完成了。当系统显示通知,且用户点击通知时,会在我们的 PendingIntent 上调用 PendingIntent.send(),来启动我们的应用。

    更新不可变的 PendingIntent

    您也许会认为如果应用需要更新 PendingIntent,那么它需要是可变类型,但其实并不是。应用所创建的 PendingIntent 可通过 FLAG_UPDATE_CURRENT 标记来更新。

    val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {
    action = NOTIFICATION_ACTION
    data = differentDeepLink
    }

    // 由于我们使用了 FLAG_UPDATE_CURRENT 标记,所以这里可以更新我们在上面创建的
    // PendingIntent
    val updatedPendingIntent = PendingIntent.getActivity(
    applicationContext,
    NOTIFICATION_REQUEST_CODE,
    updatedIntent,
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
    )
    // 该 PendingIntent 已被更新

    在接下来的内容中我们会解释为什么将 PendingIntent 设置为可变类型。

    跨应用 API

    通常的用法并不局限于与系统交互。虽然在某些操作后使用 startActivityForResult() 和 onActivityResult() 来 接收回调 是非常常见的用法,但它并不是唯一用法。

    想象一下一个线上订购应用提供了 API 使其他应用可以集成。当 Intent 启动了订购食物的流程后,应用可以 Intent 的 extra 的方式访问 PendingIntent。一旦订单完成传递,订购应用仅需启动一次 PendingIntent

    在本例中,订购应用使用了 PendingIntent 而没有直接发送 activity 结果,因为订单可能需要更长时间进行提交,而让用户在这个过程中等待是不合理的。

    我们希望创建一个不可变的 PendingIntent,因为我们不希望线上订购应用修改我们的 Intent。当订单生效时,我们仅希望其他应用发送它,并保持它本身不变。

    可变 PendingIntent

    但是如果我们作为订购应用的开发者,希望添加一个特性可以允许用户回送消息至调用订购功能的应用呢?比如可以让调用的应用提示,"现在是披萨时间!"

    要实现这样的效果就需要使用可变的 PendingIntent 了。

    既然 PendingIntent 本质上是 Intent 的封装,有人可能会想可以通过一个 PendingIntent.getIntent() 方法来获得其中所封装的 Intent。但是答案是不可以的。那么该如何实现呢?

    PendingIntent 中除了不含任何参数的 send() 方法之外,还有其他 send 方法的版本,包括这个可以接受 Intent 作为参数的 版本:

    fun PendingIntent.send(
    context: Context!,
    code: Int,
    intent: Intent?
    )

    这里的 Intent 参数并不会替换 PendingIntent 所封装的 Intent,而是通过 PendingIntent 在创建时所封装的 Intent 来填充参数。

    我们来看下面的例子。

    val orderDeliveredIntent = Intent(applicationContext, OrderDeliveredActivity::class.java).apply {
    action = ACTION_ORDER_DELIVERED
    }
    val mutablePendingIntent = PendingIntent.getActivity(
    applicationContext,
    NOTIFICATION_REQUEST_CODE,
    orderDeliveredIntent,
    PendingIntent.FLAG_MUTABLE
    )

    这里的 PendingIntent 会被传递到我们的线上订购应用。当传递完成后,应用可以得到一个 customerMessage,并将其作为 intent 的 extra 回传,如下示例所示:

    val intentWithExtrasToFill = Intent().apply {
    putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)
    }
    mutablePendingIntent.send(
    applicationContext,
    PENDING_INTENT_CODE,
    intentWithExtrasToFill
    )

    调用端的应用会在它的 Intent 中得到 EXTRA_CUSTOMER_MESSAGE extra,并显示消息。

    声明可变的 PendingIntent 时需要特别注意的事

    ⚠️当创建可变的 PendingIntent 时,始终 显式设置要启动的 Intent 的 component。可以通过我们上面的实现方式操作,即显式设置要接收的准确类名,不过也可以通过 Intent.setComponent() 实现。

    您的应用可能会在某些场景下调用 Intent.setPackage() 来实现更方便。但是请特别注意这样的做法有可能会 匹配到多个 component。如果可以的话,最好指定特定的 component。

    ⚠️如果您尝试覆写使用 FLAG_IMMUTABLE 创建的 PendingIntent 中的值,那么该操作会失败且没有任何提示,并传递原始封装未修改的 Intent

    请记住应用总是可以更新自身的 PendingIntent,即使是不可变类型。使 PendingIntent 成为可变类型的唯一原因是其他应用需要通过某种方式更新其中封装的 Intent

    关于标记的详情

    我们上面介绍了少数几个可用于创建 PendingIntent 的标记,还有一些标记也为大家介绍一下。

    FLAG_IMMUTABLE: 表示其他应用通过 PendingIntent.send() 发送到 PendingIntent 中的 Intent 无法被修改。应用总是可以使用 FLAG_UPDATE_CURRENT 标记来修改它自己的 PendingIntent。

    在 Android 12 之前的系统中,不带有该标记创建的 PendingIntent 默认是可变类型。

    ⚠️ Android 6 (API 23) 之前的系统中,PendingIntent 都是可变类型。

    🆕FLAG_MUTABLE: 表示由 PendingIntent.send() 传入的 intent 内容可以被应用合并到 PendingIntent 中的 Intent。

    ⚠️ 对于任何可变类型的 PendingIntent,始终 设置其中所封装的 Intent 的 ComponentName。如果未采取该操作的话可能会造成安全隐患。

    该标记是在 Android 12 版本中加入。Android 12 之前的版本中,任何未指定 FLAG_IMMUTABLE标记所创建的 PendingIntent 都是隐式可变类型。

    FLAG_UPDATE_CURRENT: 向系统发起请求,使用新的 extra 数据更新已有的 PendingIntent,而不是保存新的 PendingIntent。如果 PendingIntent 未注册,则进行注册。

    FLAG_ONE_SHOT: 仅允许 PendingIntent (通过 PendingIntent.send()) 被发送一次。对于传递 PendingIntent 时,其内部的 Intent 仅能被发送一次的场景就非常重要了。该机制可能便于操作,或者可以避免应用多次执行某项操作。

    🔐 使用 FLAG_ONE_SHOT 来避免类似 "重放攻击" 的问题。

    FLAG_CANCEL_CURRENT: 在注册新的 PendingIntent 之前,取消已存在的某个 PendingIntent。该标记用于当某个 PendingIntent 被发送到某应用,然后您希望将它转发到另一个应用,并更新其中的数据。使用 FLAG_CANCEL_CURRENT 之后,之前的应用将无法再调用 send 方法,而之后的应用可以调用。

    接收 PendingIntent

    有些情况下系统或者其他框架会将 PendingIntent 作为 API 调用的返回值。举一个典型例子是方法 MediaStore.createWriteRequest(),它是在 Android 11 中新增的。

    static fun MediaStore.createWriteRequest(
    resolver: ContentResolver,
    uris: MutableCollection<Uri>
    ): PendingIntent

    正如我们应用创建的 PendingIntent 一样,它是以我们应用的身份运行,而系统创建的 PendingIntent,它是以系统的身份运行。具体到这里 API 的使用场景,它允许应用打开 Activity 并赋予我们的应用 Uri 集合的写权限。

    总结

    我们在本文中介绍了 PendingIntent 如何作为 Intent 的封装使系统或者其他应用能够在未来某一时间以某个应用的身份启动该应用所创建的 Intent。

    我们还介绍了 PendingIntent 为何需要设置为不可变,以及这么做并不会影响应用修改自身所创建的 PendingIntent 对象。可以通过 FLAG_UPDATE_CURRENT 标记加上 FLAG_IMMUTABLE 来实现该操作。

    我们还介绍了如果 PendingIntent 是可变的,需要做的预防措施 — 保证对封装的 Intent 设置 ComponentName

    最后,我们介绍了有时系统或者框架如何向应用提供 PendingIntent,以便我们能够决定如何并且何时运行它们。

    收起阅读 »

    Android工程Gradle构建-笔记

    1、统一版本库管理1.1、统一版本号管理创建一个gradle文件统一管理 不同module下的第三方库和其他属性的配置参数 如下,在项目根目录创建config.gradleext { COMPILE_SDK = 30 APPLICATION_ID ...
    继续阅读 »

    1、统一版本库管理

    1.1、统一版本号管理

    创建一个gradle文件统一管理 不同module下的第三方库和其他属性的配置参数 如下,在项目根目录创建config.gradle

    ext {
    COMPILE_SDK = 30
    APPLICATION_ID = "com.chenyangqi.gradle"
    MIN_SDK = 19
    TARGET_SDK = 30
    VERSION_CODE = 1
    VERSION_NAME = "1.0.0"

    JVM_TARGET = '1.8'

    MULTIDEX = 'androidx.multidex:multidex:2.0.1'
    CORE_KTX = 'androidx.core:core-ktx:1.3.2'
    APPCOMPAT = 'androidx.appcompat:appcompat:1.2.0'
    ANDROID_MATERIAL = 'com.google.android.material:material:1.3.0'
    CONSTRAINTLAYOUT = 'androidx.constraintlayout:constraintlayout:2.1.0'
    TEST_JUNIT = 'junit:junit:4.+'
    ANDROID_EXT_JUNIT = 'androidx.test.ext:junit:1.1.2'
    ANDROID_TEST_ESPRESSO = 'androidx.test.espresso:espresso-core:3.3.0'
    }

    然后在项目的gradle中引用config.gradle

    apply from: project.file('config.gradle')

    1.2、local.perporties使用场景

    一些不便对外展示的敏感参数可以定义在local.properties中,如一些付费库的key,maven仓库的用户名和密码等

    sdk.dir=/Users/mutou/Library/Android/sdk
    username=chenyangqi
    password=123456

    编译时通过如下方式获取local.properties中定义的属性

    Properties properties = new Properties()
    properties.load(project.rootProject.file("local.properties").newDataInputStream())
    def username = properties.getProperty('username')
    def password = properties.getProperty('password')

    在运行时获得Local.properties属性要借助BuildConfig

    def getUsername() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())
    return properties.getProperty("username");
    }

    android {
    defaultConfig {
    buildConfigField "String", "USERNAME", "\""+getUsername()+"\""
    }
    }

    1.3、版本冲突处理

    出现依赖冲突时可通过gradle的dependencies查看依赖树,定位冲突位置 ,比如我要查看的Build Variants为oppoNormalProdRelease,命令如下

    ./gradlew :app:dependencies --configuration oppoNormalProdReleaseCompileClasspath

    依赖树如下

    mutou@chenyangqi GradleDemo % ./gradlew :app:dependencies --configuration oppoNormalProdReleaseCompileClasspath
    ...
    oppoNormalProdReleaseCompileClasspath - Compile classpath for compilation 'oppoNormalProdRelease' (target (androidJvm)).
    +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21
    | +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.21
    | | +--- org.jetbrains:annotations:13.0
    | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.21
    | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.21
    | \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.21 (*)
    +--- androidx.multidex:multidex:2.0.1
    +--- androidx.core:core-ktx:1.3.2
    | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.71 -> 1.5.21 (*)
    | +--- androidx.annotation:annotation:1.1.0
    | \--- androidx.core:core:1.3.2
    | +--- androidx.annotation:annotation:1.1.0
    | +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.1.0
    | | +--- androidx.lifecycle:lifecycle-common:2.1.0
    | | | \--- androidx.annotation:annotation:1.1.0
    | | +--- androidx.arch.core:core-common:2.1.0
    | | | \--- androidx.annotation:annotation:1.1.0
    | | \--- androidx.annotation:annotation:1.1.0
    | \--- androidx.versionedparcelable:versionedparcelable:1.1.0
    | +--- androidx.annotation:annotation:1.1.0
    | \--- androidx.collection:collection:1.0.0 -> 1.1.0
    | \--- androidx.annotation:annotation:1.1.0
    +--- androidx.appcompat:appcompat:1.2.0
    | +--- androidx.annotation:annotation:1.1.0
    | +--- androidx.core:core:1.3.0 -> 1.3.2 (*)
    | +--- androidx.cursoradapter:cursoradapter:1.0.0
    | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | +--- androidx.fragment:fragment:1.1.0
    | | +--- androidx.annotation:annotation:1.1.0
    | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
    | | +--- androidx.collection:collection:1.1.0 (*)
    | | +--- androidx.viewpager:viewpager:1.0.0
    | | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
    | | | \--- androidx.customview:customview:1.0.0
    | | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | | \--- androidx.core:core:1.0.0 -> 1.3.2 (*)
    | | +--- androidx.loader:loader:1.0.0
    | | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
    | | | +--- androidx.lifecycle:lifecycle-livedata:2.0.0
    | | | | +--- androidx.arch.core:core-runtime:2.0.0
    | | | | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | | | | \--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
    | | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0
    | | | | | +--- androidx.lifecycle:lifecycle-common:2.0.0 -> 2.1.0 (*)
    | | | | | +--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
    | | | | | \--- androidx.arch.core:core-runtime:2.0.0 (*)
    | | | | \--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
    | | | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -> 2.1.0
    | | | \--- androidx.annotation:annotation:1.1.0
    | | +--- androidx.activity:activity:1.0.0
    | | | +--- androidx.annotation:annotation:1.1.0
    | | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
    | | | +--- androidx.lifecycle:lifecycle-runtime:2.1.0 (*)
    | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.1.0 (*)
    | | | \--- androidx.savedstate:savedstate:1.0.0
    | | | +--- androidx.annotation:annotation:1.1.0
    | | | +--- androidx.arch.core:core-common:2.0.1 -> 2.1.0 (*)
    | | | \--- androidx.lifecycle:lifecycle-common:2.0.0 -> 2.1.0 (*)
    | | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -> 2.1.0 (*)
    | +--- androidx.appcompat:appcompat-resources:1.2.0
    | | +--- androidx.annotation:annotation:1.1.0
    | | +--- androidx.core:core:1.0.1 -> 1.3.2 (*)
    | | +--- androidx.vectordrawable:vectordrawable:1.1.0
    | | | +--- androidx.annotation:annotation:1.1.0
    | | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
    | | | \--- androidx.collection:collection:1.1.0 (*)
    | | \--- androidx.vectordrawable:vectordrawable-animated:1.1.0
    | | +--- androidx.vectordrawable:vectordrawable:1.1.0 (*)
    | | +--- androidx.interpolator:interpolator:1.0.0
    | | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | \--- androidx.collection:collection:1.1.0 (*)
    | \--- androidx.drawerlayout:drawerlayout:1.0.0
    | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
    | \--- androidx.customview:customview:1.0.0 (*)
    +--- com.google.android.material:material:1.3.0
    | +--- androidx.annotation:annotation:1.0.1 -> 1.1.0
    | +--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*)
    | +--- androidx.cardview:cardview:1.0.0
    | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | +--- androidx.coordinatorlayout:coordinatorlayout:1.1.0
    | | +--- androidx.annotation:annotation:1.1.0
    | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
    | | +--- androidx.customview:customview:1.0.0 (*)
    | | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
    | +--- androidx.constraintlayout:constraintlayout:2.0.1 -> 2.1.0
    | +--- androidx.core:core:1.2.0 -> 1.3.2 (*)
    | +--- androidx.dynamicanimation:dynamicanimation:1.0.0
    | | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
    | | +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
    | | \--- androidx.legacy:legacy-support-core-utils:1.0.0
    | | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
    | | +--- androidx.documentfile:documentfile:1.0.0
    | | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | +--- androidx.loader:loader:1.0.0 (*)
    | | +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
    | | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | | \--- androidx.print:print:1.0.0
    | | \--- androidx.annotation:annotation:1.0.0 -> 1.1.0
    | +--- androidx.annotation:annotation-experimental:1.0.0
    | +--- androidx.fragment:fragment:1.0.0 -> 1.1.0 (*)
    | +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.1.0 (*)
    | +--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0
    | | +--- androidx.annotation:annotation:1.1.0
    | | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
    | | +--- androidx.customview:customview:1.0.0 (*)
    | | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
    | +--- androidx.transition:transition:1.2.0
    | | +--- androidx.annotation:annotation:1.1.0
    | | +--- androidx.core:core:1.0.1 -> 1.3.2 (*)
    | | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
    | +--- androidx.vectordrawable:vectordrawable:1.1.0 (*)
    | \--- androidx.viewpager2:viewpager2:1.0.0
    | +--- androidx.annotation:annotation:1.1.0
    | +--- androidx.fragment:fragment:1.1.0 (*)
    | +--- androidx.recyclerview:recyclerview:1.1.0 (*)
    | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)
    | \--- androidx.collection:collection:1.1.0 (*)
    ...
    BUILD SUCCESSFUL in 1s

    1.3.1、去除冲突依赖

    当确定最终只保留的版本时,过滤掉其他版本,如下只保留ore:0.9.5.0

    api("com.afollestad.material-dialogs:core:0.9.5.0") {
    exclude group: 'com.android.support'
    }

    1.3.2、CompileOnly只编译不打包

    当我们开发SDK时如果需要依赖第三方库,使用CompileOnly引用第三方库,而让使用SDK的开发者去决定选择哪个版本的第三方库,避免他人调用你的SDK时出现版本冲突

    1.3.3、通过gradle脚本检测依赖库版本

    待实现...

    2、MultiDex分包

    2.1、65535产生原因

    默认情况下,Android工程代码编译后的.class文件会打包进一个dex中,dex中每一个方法会使用C++中的unsigned short类型的字段标记,unsigned short取值范围为0~65535,所以一旦方法数超过上限就会造成65536

    2.2、分包

    通过分包解决65535问题,根据minSdk版本不同分两种情况

    2.2.1、minSdk>=21时分包

    在app module下的build.gradle中设置multiDexEnabled=true即可

    android {
    compileSdk 30
    defaultConfig {
    applicationId "com.chenyangqi.gradle"
    minSdk 21
    targetSdk 30
    versionCode 1
    versionName "1.0"
    multiDexEnabled true
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    ...
    }

    2.2.2、minSdk<21时分包

    minSdk小于21时除了设置multidexEnabled=true还要引用androidx.multidex:multidex:2.0.1这个Google提供的分包库

    android {
    compileSdk 30

    defaultConfig {
    applicationId "com.chenyangqi.gradle"
    minSdk 19
    targetSdk 30
    versionCode 1
    versionName "1.0"
    multiDexEnabled true
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    ...
    }

    dependencies {
    implementation 'androidx.multidex:multidex:2.0.1'
    ...
    }

    然后再在application中继承分包库中的MultiDexApplication,又分三种情况

    没有自定义Application时,直接在清单文件中注册name=MultiDexApplication

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

    <application
    android:name="androidx.multidex.MultiDexApplication"
    ...
    </application>

    </manifest>

    有自定义Application时直接继承

    class MyApplication:MultiDexApplication() {
    }

    当自定义Application已继承其他父类时重写attachBaseContext方法进行初始化

    class MyApplication:OtherApplication() {

    override fun onCreate() {
    super.onCreate()
    }

    override fun attachBaseContext(base: Context?) {
    super.attachBaseContext(base)
    MultiDex.install(this)
    }
    }

    3、代码混淆

    3.1、开启混淆

    buildTypes {
    debug {
    minifyEnabled false
    shrinkResources false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    release {
    minifyEnabled true
    shrinkResources true
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    }

    设置minifyEnabled=true和shrinkResources=true启用压缩、混淆和优化功能, proguard-android-optimize.txt为存放在SDK中Android默认的混淆规则,存放路径~/SDK/tools/proguard/proguard-android-optimize.txt,proguard-rules.pro为项目中自己定义的混淆规则

    3.2、常用混淆规则

    关键字描述
    keep保留类和类中的成员,防止被混淆或者移除
    keepnames保留类和类中的成员,防止被混淆,但是当成员没有被引用时会被移除
    keepclassmembers只保留类中的成员,防止他们被混淆或者移除
    keepclassmembersnames只保留类中的成员,防止他们被混淆或者移除,但是当类中的成员没有被引用时还是会被移除
    keepclasseswithmembers保留类和类中的成员,前提是指明的类中必须含有该成员,没有的话还是会被混淆
    keepclasseswithmembersnames保留类和类中的成员,前提是指明的类中必须含有该成员,没有的话还是会被混淆。需要注意的是没有被引用的成员会被移除
     
    关键字描述
    <filed>匹配类中的所有字段
    <method>匹配类中的所有方法
    <init>匹配类中的所有构造函数
    *匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是无法匹配的,因为*无法匹配包名中的分隔符,正确的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。
    **匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。
    ***匹配任意参数类型。比如void set*(***)就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。
    匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。

    3.3、Android常用混淆模板

    #-------------------------------------------基本不用动区域--------------------------------------------
    #---------------------------------基本指令区----------------------------------
    -optimizationpasses 5
    -dontskipnonpubliclibraryclassmembers
    -printmapping proguardMapping.txt
    -optimizations !code/simplification/cast,!field/*,!class/merging/*
    -keepattributes *Annotation*,InnerClasses
    -keepattributes Signature
    -keepattributes SourceFile,LineNumberTable
    #----------------------------------------------------------------------------

    #---------------------------------默认保留区---------------------------------
    #继承activity,application,service,broadcastReceiver,contentprovider....不进行混淆
    -keep public class * extends android.app.Activity
    -keep public class * extends android.app.Application
    -keep public class * extends androidx.multidex.MultiDexApplication
    -keep public class * extends android.app.Service
    -keep public class * extends android.content.BroadcastReceiver
    -keep public class * extends android.content.ContentProvider
    -keep public class * extends android.app.backup.BackupAgentHelper
    -keep public class * extends android.preference.Preference
    -keep public class * extends android.view.View
    -keep class android.support.** {*;}

    -keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    }
    -keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    }
    #这个主要是在layout 中写的onclick方法android:onclick="onClick",不进行混淆
    -keepclassmembers class * extends android.app.Activity {
    public void *(android.view.View);
    }

    -keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
    }

    -keep class **.R$* {
    *;
    }

    -keepclassmembers class * {
    void *(*Event);
    }

    #枚举
    -keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
    }

    -keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
    }

    #// natvie 方法不混淆
    -keepclasseswithmembernames class * {
    native <methods>;
    }

    #保持 Parcelable 不被混淆
    -keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
    }

    #---------------------------------webview------------------------------------
    -keepclassmembers class android.webkit.WebView {
    public *;
    }
    -keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
    }
    -keepclassmembers class * extends android.webkit.WebViewClient {
    public void *(android.webkit.WebView, java.lang.String);
    }
    #----------------------------------------------------------------------------
    #---------------------------------------------------------------------------------------------------
    #---------------------------------实体类---------------------------------
    #修改成你对应的包名
    -keep class com.chenyangqi.gradle.proguard.bean.** { *; }

    #---------------------------------第三方包-------------------------------

    #---------------------------------反射相关的类和方法-----------------------

    #---------------------------------与js互相调用的类------------------------

    #---------------------------------自定义View的类------------------------

    收起阅读 »

    OC与Swift API的交互

    互用性是让 Swift 和 Objective-C 相接合的一种特性,使你能够在一种语言编写的文件中使用另一种语言。当你准备开始把 Swift 融入到你的开发流程中时,你应该懂得如何利用互用性来重新定义并提高你写 Cocoa 应用的方案。互用性很重要的一点就是...
    继续阅读 »
    互用性是让 Swift 和 Objective-C 相接合的一种特性,使你能够在一种语言编写的文件中使用另一种语言。当你准备开始把 Swift 融入到你的开发流程中时,你应该懂得如何利用互用性来重新定义并提高你写 Cocoa 应用的方案。

    互用性很重要的一点就是允许你在写 Swift 代码时使用 Objective-C 的 API 接口。当你导入一个 Objective-C 框架后,你可以使用原生的 Swift 语法实例化它的 Class 并且与之交互。

    初始化

    为了使用 Swift 实例化 Objective-C 的 Class,你应该使用 Swift 语法调用它的一个初始化器。当 Objective-C 的init方法变化到 Swift,他们用 Swift 初始化语法呈现。“init”前缀被截断当作一个关键字,用来表明该方法是初始化方法。那些以“initWith”开头的init方法,“With”也会被去除。从“init”或者“initWith”中分离出来的这部分方法名首字母变成小写,并且被当做是第一个参数的参数名。其余的每一部分方法名依次变味参数名。这些方法名都在圆括号中被调用。

    举个例子,你在使用 Objective-C 时会这样做:

    1.  //Objective-C
    2. UITableView *myTableView = [[UITableView alloc]
    3. initWithFrame:CGRectZero style:UITableViewStyleGrouped];

    在 Swift 中,你应该这样做:

    1.  //Swift
    2. let myTableView: UITableView = UITableView(frame: CGRectZero, style: .Grouped)

    你不需要调用 alloc,Swift 能替你处理。注意,当使用 Swift 风格的初始化函数的时候,“init”不会出现。
    你可以在初始化时显式的声明对象的类型,也可以忽略它,Swift 能够正确判断对象的类型。


    1.  //Swift
    2. let myTextField = UITextField(frame: CGRect(0.0, 0.0, 200.0, 40.0))
    这里的UITableView和UITextField对象和你在 Objective-C 中使用的具有相同的功能。你可以用一样的方式使用他们,包括访问属性或者调用各自的类中的方法。
    为了统一和简易,Objective-C 的工厂方法也在 Swift 中映射为方便的初始化方法。这种映射能够让他们使用同样简洁明了的初始化方法。例如,在 Objective-C 中你可能会像下面这样调用一个工厂方法:

    1.  //Objective-C
    2. UIColor *color = [UIColor colorWithRed:0.5 green:0.0 blue:0.5 alpha:1.0];

    在 Swift 中,你应该这样做:

    1.  //Swift
    2. let color = UIColor(red: 0.5, green: 0.0, blue: 0.5, alpha: 1.0)

    访问属性

    在 Swift 中访问和设置 Objective-C 对象的属性时,使用点语法:

    1.  // Swift
    2. myTextField.textColor = UIColor.darkGrayColor()
    3. myTextField.text = "Hello world"
    4. if myTextField.editing {
    5. myTextField.editing = false
    6. }

    当 get 或 set 属性时,直接使用属性名称,不需要附加圆括号。注意,darkGrayColor后面附加了一对圆括号,这是因为darkGrayColor是UIColor的一个类方法,不是一个属性。

    在 Objective-C 中,一个有返回值的无参数方法可以被作为一个隐式的访问函数,并且可以与访问器使用同样的方法调用。但在 Swift 中不再能够这样做了,只有使用@property关键字声明的属性才会被作为属性引入。

    方法

    在 Swift 中调用 Objective-C 方法时,使用点语法。

    当 Objective-C 方法转换到 Swift 时,Objective-C 的selector的第一部分将会成为方法名并出现在圆括号的前面,而第一个参数将直接在括号中出现,并且没有参数名,而剩下的参数名与参数则一一对应的填入圆括号中。

    举个例子,你在使用 Objective-C 时会这样做:

    1.  //Objective-C
    2. [myTableView insertSubview:mySubview atIndex:2];

    在 Swift 中,你应该这样做:

    1.  //Swift
    2. myTableView.insertSubview(mySubview, atIndex: 2)

    如果你调用一个无参方法,仍必须在方法名后面加上一对圆括号

    1.  //Swift
    2. myTableView.layoutIfNeeded()

    id 兼容性(id Compatibility)

    Swift 包含一个叫做AnyObject的协议类型,表示任意类型的对象,就像 Objective-C 中的id一样。AnyObject协议允许你编写类型安全的 Swift 代码同时维持无类型对象的灵活性。因为AnyObject协议保证了这种安全,Swift 将 id 对象导入为 AnyObject。

    举个例子,跟 id 一样,你可以为AnyObject类型的对象分配任何其他类型的对象,你也同样可以为它重新分配其他类型的对象。

    1.  //Swift
    2. var myObject: AnyObject = UITableViewCell()
    3. myObject = NSDate()

    你也可以在调用 Objective-C 方法或者访问属性时不将它转换为具体类的类型。这包括了 Objcive-C 中标记为 @objc 的方法。

    1.  //Swift
    2. let futureDate = myObject.dateByAddingTimeInterval(10)
    3. let timeSinceNow = myObject.timeIntervalSinceNow
    然而,由于直到运行时才知道AnyObject的对象类型,所以有可能在不经意间写出不安全代码。另外,与 Objective-C 不同的是,如果你调用方法或者访问的属性 AnyObject 对象没有声明,将会报运行时错误。比如下面的代码在运行时将会报出一个 unrecognized selector error 错误:

    1.  //Swift
    2. myObject.characterAtIndex(5)
    3. // crash, myObject does't respond to that method

    但是,你可以通过 Swift 的 optinals 特性来排除这个 Objective-C 中常见的错误,当你用AnyObject对象调用一个 Objective-C 方法时,这次调用将会变成一次隐式展开 optional(implicitly unwrapped optional)的行为。你可以通过 optional 特性来决定 AnyObject 类型的对象是否调用该方法,同样的,你可以把这种特性应用在属性上。

    举个例子,在下面的代码中,第一和第二行代码将不会被执行因为length属性和characterAtIndex:方法不存在于 NSDate 对象中。myLength常量会被推测成可选的Int类型并且被赋值为nil。同样你可以使用if-let声明来有条件的展开这个方法的返回值,从而判断对象是否能执行这个方法。就像第三行做的一样。


    1.  //Swift
    2. let myLength = myObject.length?
    3. let myChar = myObject.characterAtIndex?(5)
    4. if let fifthCharacter = myObject.characterAtIndex(5) {
    5. println("Found \(fifthCharacter) at index 5")
    6. }


    对于 Swift 中的强制类型转换,从 AnyObject 类型的对象转换成明确的类型并不会保证成功,所以它会返回一个可选的值。而你需通过检查该值的类型来确认转换是否成功。

    1.  //Swift
    2. let userDefaults = NSUserDefaults.standardUserDefaults()
    3. let lastRefreshDate: AnyObject? = userDefaults.objectForKey("LastRefreshDate")
    4. if let date = lastRefreshDate as? NSDate {
    5. println("\(date.timeIntervalSinceReferenceDate)")
    6. }

    当然,如果你能确定这个对象的类型(并且确定不是nil),你可以添加as操作符强制调用。

    1.  //Swift
    2. let myDate = lastRefreshDate as NSDate
    3. let timeInterval = myDate.timeIntervalSinceReferenceDate

    使用nil

    在Objective-C中,对象的引用可以是值为NULL的原始指针(同样也是Objective-C中的nil)。而在Swift中,所有的值–包括结构体与对象的引用–都被保证为非空。作为替代,你将这个可以为空的值包装为optional type。当你需要宣告值为空时,你需要使用nil。你可以在Optionals中了解更多。

    因为Objective-C不会保证一个对象的值是否非空,Swift在引入Objective-C的API的时候,确保了所有函数的返回类型与参数类型都是optional,在你使用Objective-C的API之前,你应该检查并保证该值非空。 在某些情况下,你可能绝对确认某些Objective-C方法或者属性永远不应该返回一个nil的对象引用。为了让对象在这种情况下更加易用,Swift使用 implicitly unwrapped optionals 方法引入对象, implicitly unwrapped optionals 包含optional 类型的所有安全特性。此外,你可以直接访问对象的值而无需检查nil。当你访问这种类型的变量时, implicitly unwrapped optional 首先检查这个对象的值是否不存在,如果不存在,将会抛出运行时错误。

    扩展(Extensions)

    Swift 的扩展和 Objective-C 的类别(Category)相似。扩展为原有的类,结构和枚举丰富了功能,包括在 Objective-C 中定义过的。你可以为系统的框架或者你自己的类型增加扩展,只需要导入合适的模块并且保证你在 Objective-C 中使用的类、结构或枚举拥有相同的名字。

    举个例子,你可以扩展UIBezierPath类来为它增加一个等边三角形,这个方法只需提供三角形的边长与起点。

    1.  //Swift
    2. extension UIBezierPath {
    3. convenience init(triangleSideLength: Float, origin: CGPoint) {
    4. self.init()
    5. let squareRoot = Float(sqrt(3))
    6. let altitude = (squareRoot * triangleSideLength) / 2
    7. moveToPoint(origin)
    8. addLineToPoint(CGPoint(triangleSideLength, origin.x))
    9. addLineToPoint(CGPoint(triangleSideLength / 2, altitude))
    10. closePath()
    11. }
    12. }
    你也可以使用扩展来增加属性(包括类的属性与静态属性)。然而,这些属性必须是通过计算才能获取的,扩展不会为类,结构体,枚举存储属性。下面这个例子为CGRect类增加了一个叫area的属性。

    1.  //Swift
    2. extension CGRect {
    3. var area: CGFloat {
    4. return width * height
    5. }
    6. }
    7. let rect = CGRect(x: 0.0, y: 0.0, width: 10.0, height: 50.0)
    8. let area = rect.area
    9. // area: CGFloat = 500.0

    你同样可以使用扩展来为类添加协议而无需增加它的子类。如果这个协议是在 Swift 中被定义的,你可以添加 comformance 到它的结构或枚举中无论它们在 Objective-C 或在 Swift 中被定义。

    你不能使用扩展来覆盖 Objective-C 类型中存在的方法与属性。

    闭包(Closures)

    Objective-C 中的blocks会被自动导入为 Swift 中的闭包。例如,下面是一个 Objective-C 中的 block 变量:


    1.  //Objective-C
    2. void (^completionBlock)(NSData *, NSError *) = ^(NSData *data, NSError *error) {/* ... */}

    而它在 Swift 中的形式为

    1.  //Swift
    2. let completionBlock: (NSData, NSError) -> Void = {data, error in /* ... */}

    Swift 的闭包与 Objective-C 中的 blocks 能够和睦相处,所以你可以把一个 Swift 闭包传递给一个把 block 作为参数的 Objective-C 函数。Swift 闭包与函数具有互通的类型,所以你甚至可以传递 Swift 函数的名字。
    闭包与 blocks 语义上想通但是在一个地方不同:变量是可以直接改变的,而不是像 block 那样会拷贝变量。换句话说,Swift 中变量的默认行为与 Objective-C 中 __block 变量一致。

    比较对象

    当比较两个 Swift 中的对象时,可以使用两种方式。第一种,使用(==),判断两个对象内容是否相同。第二种,使用(===),判断常量或者变量是否为同一个对象的实例。

    Swift 与 Objective-C 一般使用 == 与 === 操作符来做比较。Swift 的 == 操作符为源自 NSObject 的对象提供了默认的实现。在实现 == 操作符时,Swift 调用 NSObject 定义的 isEqual: 方法。

    NSObject 类仅仅做了身份的比较,所以你需要在你自己的类中重新实现 isEqual: 方法。因为你可以直接传递 Swift 对象给 Objective-C 的 API,你也应该为这些对象实现自定义的 isEqual: 方法,如果你希望比较两个对象的内容是否相同而不是仅仅比较他们是不是由相同的对象派生。

    作为实现比较函数的一部分,确保根据Object comparison实现对象的hash属性。更进一步的说,如果你希望你的类能够作为字典中的键,也需要遵从Hashable协议以及实现hashValues属性。

    Swift 类型兼容性

    当你定义了一个继承自NSObject或者其他 Objective-C 类的 Swift 类,这些类都能与 Objective-C 无缝连接。所有的步骤都有 Swift 编译器自动完成,如果你从未在 Objective-C 代码中导入 Swift 类,你也不需要担心类型适配问题。另外一种情况,如果你的 Swift 类并不来源自 Objectve-C 类而且你希望能在 Objecive-C 的代码中使用它,你可以使用下面描述的 @objc 属性。

    @objc可以让你的 Swift API 在 Objective-C 中使用。换句话说,你可以通过在任何 Swift 方法、类、属性前添加@objc,来使得他们可以在 Objective-C 代码中使用。如果你的类继承自 Objective-C,编译器会自动帮助你完成这一步。编译器还会在所有的变量、方法、属性前加 @objc,如果这个类自己前面加上了@objc关键字。当你使用@IBOutlet,@IBAction,或者是@NSManaged属性时,@objc也会自动加在前面。这个关键字也可以用在 Objetive-C 中的 target-action 设计模式中,例如,NSTimer或者UIButton。

    当你在 Objective-C 中使用 Swift API,编译器基本对语句做直接的翻译。例如,Swift API func playSong(name: String)会被解释为- (void)playSong:(NSString *)name。然而,有一个例外:当在 Objective-C 中使用 Swift 的初始化函数,编译器会在方法前添加“initWith”并且将原初始化函数的第一个参数首字母大写。例如,这个 Swift 初始化函数init (songName: String, artist: String将被翻译为- (instancetype)initWithSongName:(NSString *)songName artist:(NSString *)artist 。

    Swift 同时也提供了一个@objc关键字的变体,通过它你可以自定义在 Objectiv-C 中转换的函数名。例如,如果你的 Swift 类的名字包含 Objecytive-C 中不支持的字符,你就可以为 Objective-C 提供一个可供替代的名字。如果你给 Swift 函数提供一个 Objecytive-C 名字,要记得为带参数的函数添加(:)

    1.  //Swift
    2. @objc(Squirrel)
    3. class Белка {
    4. @objc(initWithName:)
    5. init (имя: String) { /*...*/ }
    6. @objc(hideNuts:inTree:)
    7. func прячьОрехи(Int, вДереве: Дерево) { /*...*/ }
    8. }

    当你在 Swift 类中使用@objc(<#name#>)关键字,这个类可以不需要命名空间即可在 Objective-C 中使用。这个关键字在你迁徙 Objecive-C 代码到 Swift 时同样也非常有用。由于归档过的对象存贮了类的名字,你应该使用@objc(<#name#>)来声明与旧的归档过的类相同的名字,这样旧的类才能被新的 Swift 类解档。

    Objective-C 选择器(Selectors)

    一个 Objective-C 选择器类型指向一个 Objective-C 的方法名。在 Swift 时代,Objective-C 的选择器被Selector结构体替代。你可以通过字符串创建一个选择器,比如let mySelector: Selector = "tappedButton:"。因为字符串能够自动转换为选择器,所以你可以把字符串直接传递给接受选择器的方法。


    1.  //Swift
    2. import UIKit
    3. class MyViewController: UIViewController {
    4. let myButton = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))

    6. init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
    7. super.init(nibName: nibName, bundle: nibBundle)
    8. myButton.targetForAction("tappedButton:", withSender: self)
    9. }

    11. func tappedButton(sender: UIButton!) {
    12. println("tapped button")
    13. }
    14. }

    注意: performSelector:方法和相关的调用选择器的方法没有导入到 Swift 中因为它们是不安全的。

    如果你的 Swift 类继承自 Objective-C 的类,你的所有方法都可以用作 Objective-C 的选择器。另外,如果你的 Swift 类不是继承自 Objective-C,如果你想要当选择器来使用你就需要在前面添加@objc关键字,详情请看Swift 类型兼容性。


    作者:iOS鑫
    链接:https://www.jianshu.com/p/c17977fd96aa




    收起阅读 »

    iOS 优雅的处理网络数据,你真的会吗?不如看看这篇.

    相信大家平时在用 App 的时候, 往往有过这样的体验,那就是加载网络数据等待的时间过于漫长,滚动浏览时伴随着卡顿,甚至在没有网络的情况下,整个应用处于不可用状态。那么我们该怎么去提高用户体验,保证用户没有漫长的等待感,还可以轻松自在的享受等待,对加载后的内容...
    继续阅读 »
    相信大家平时在用 App 的时候, 往往有过这样的体验,那就是加载网络数据等待的时间过于漫长,滚动浏览时伴随着卡顿,甚至在没有网络的情况下,整个应用处于不可用状态。那么我们该怎么去提高用户体验,保证用户没有漫长的等待感,还可以轻松自在的享受等待,对加载后的内容有明确的预期呢?

    案例分享

    在现代的工作生活中,手机早已不是单纯的通信工具了,它更像是一个集办公,娱乐,消费的终端,潜移默化的成为了我们生活的一部分。所以作为 iOS 开发者的我们,在日常的开发中,也早已不是处理显示零星的数据这么简单,为了流量往往我们需要在 App 里显示大量有价值的信息来吸引用户,如何优雅的显示这些海量的数据,考量的就是你的个人经验了。

    正如大多数 iOS 开发人员所知,显示滚动数据是构建移动应用中常见的任务,Apple 的 SDK 提供了 UITableView 和 UICollectionVIew 这俩大组件来帮助执行这样的任务。但是,当需要显示大量数据时,确保平滑如丝的滚动可能会非常的棘手。所以今天正好趁这个机会,和大家分享一下处理大量可滚动数据方面的个人经验。

    在这篇文章中,你将会学到以下内容:

    1.让你的 App 可以无限滚动(infinite scrolling),并且滚动数据无缝加载

    2.让你的 App 数据滚动时避免卡顿,实现平滑如丝的滚动

    3.异步存储(Cache)和获取图像,来使你的 App 具有更高的响应速度


    无限滚动,无缝加载

    提到列表分页,相信大家第一个想到的就是 MJRefresh,用于上拉下拉来刷新数据,当滚动数据到达底部的时候向服务器发送请求,然后在控件底部显示一个 Loading 动画,待请求数据返回后,Loading 动画消失,由 UITableView 或者 UICollectionView 控件继续加载这些数据并显示给用户,效果如下图所示





    在这种情况下就造成了一种现象,那就是 App 向服务器请求数据到数据返回这段时间留下了一个空白,如果在网络差的情况下,这段空白的时间将会持续,这给人的体验会很不好。那该如何去避免这种现象呢!或者说我们能否去提前获取到其余的数据,在用户毫无感知的情况下把数据请求过来,看上去就像无缝加载一样呢!

    答案当然是肯定的!

    为了改善应用程序体验,在 iOS 10 上,Apple 对 UICollectionView 和 UITableView 引入了 Prefetching API,它提供了一种在需要显示数据之前预先准备数据的机制,旨在提高数据的滚动性能。

    首先,我先和大家介绍一个概念:无限滚动,无限滚动是可以让用户连续的加载内容,而无需分页。在 UI 初始化的时候 App 会加载一些初始数据,然后当用户滚动快要到达显示内容的底部时加载更多的数据。

    多年来,像 Instagram, Twitter 和 Facebook 这样的社交媒体公司都使这种技术。如果查看他们的 App ,你就可以看到无限滚动的实际效果,这里我就给大伙展示下 Instagram 的效果吧!





    如何实现

    由于 Instagram 的 UI 过于复杂,在这我就不去模仿实现了,但是我模仿了它的加载机制,同样的实现了一个简单的数据无限滚动和无缝加载的效果。

    简单的说下我的思路:

    先自定义一个 Cell 视图,这个视图由一个 UILabel 和 一个 UIImageView 构成,用于显示文本和网络图片;然后模拟网络请求来获取数据,注意该步骤一定是异步执行的;最后用 UITableView 来显示返回的数据,在 viewDidLoad 中先请求网络数据来获取一些初始化数据,然后再利用 UITableView 的 Prefetching API 来对数据进行预加载,从而来实现数据的无缝加载。

    那关于无限滚动该如何实现呢!其实这个无限滚动并不是真正意义上的永无止尽,严格意义上来讲它是有尽头的,只不过这个功能背后的数据是不可估量的,只有大量的数据做支持才能让应用一直不断的从服务端获取数据。

    正常情况下,我们在构建 UITableView 这个控件的时候,需要对它的行数(numsOfRow)做一个初始化,这个行数对我们实现无限加载和无缝加载是一个很关键的因素,假设我们每次根据服务端返回的数据量去更新 UITableView 的行数并 Reload,那我之前说的 Prefetching API 在这种情况下就失去作用了,因为它起作用的前提是要保证预加载数据时 UITableView 当前的行数要小于它的总行数。当然前者也可以实现数据加载,但它的效果就不是无缝加载,它在每次加载数据的时候都会有一个 Loading 等待的时间。

    回到我上面所说的无限滚动, 其实实现起来并不难,正常情况下,我们向服务端请求大量相同类型的数据的时候,都会提供一个接口,我称之为分页请求接口,该接口在每次数据返回的时候,都会告诉客户端总共有多少页数据,每页的数据量是多少,当前是第几页,这样我们就能计算出来总共的数据有多少,而这就是 UITableView 的总行数。

    响应数据的示范如下(为清楚起见,它仅显示与分页有关的字段):


    {

    "has_more": true,
    "page": 1,
    "total": 84,
    "items": [

    ...
    ...
    ]
    }




    下面,我就用代码来一步步的实现它吧!

    模拟分页请求

    由于没有找到合适的分页测试接口,于是我自己模拟一了一个分页请求接口,每次调用该接口的时候都延时 2s 来模拟网络请求的状态,代码如下:

     func fetchImages() {
    guard !isFetchInProcess else {
    return
    }

    isFetchInProcess = true
    // 延时 2s 模拟网络环境
    print("+++++++++++ 模拟网络数据请求 +++++++++++")
    DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 2) {
    print("+++++++++++ 模拟网络数据请求返回成功 +++++++++++")
    DispatchQueue.main.async {
    self.total = 1000
    self.currentPage += 1
    self.isFetchInProcess = false
    // 初始化 30个 图片
    let imagesData = (1...30).map {
    ImageModel(url: baseURL+"\($0).png", order: $0)
    }
    self.images.append(contentsOf: imagesData)

    if self.currentPage > 1 {
    let newIndexPaths = self.calculateIndexPathsToReload(from: imagesData)
    self.delegate?.onFetchCompleted(with: newIndexPaths)
    } else {
    self.delegate?.onFetchCompleted(with: .none)
    }
    }
    }
    }

    数据回调处理:

    extension ViewController: PreloadCellViewModelDelegate {

    func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
    guard let newIndexPathsToReload = newIndexPathsToReload else {
    tableView.tableFooterView = nil
    tableView.reloadData()
    return
    }

    let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
    indicatorView.stopAnimating()
    tableView.reloadRows(at: indexPathsToReload, with: .automatic)
    }

    func onFetchFailed(with reason: String) {
    indicatorView.stopAnimating()
    tableView.reloadData()
    }
    }

    预加载数据

    首先如果你想要 UITableView 预加载数据,你需要在 viewDidLoad() 函数中插入如下代码,并且请求第一页的数据:

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    ...
    tableView.prefetchDataSource = self
    ...
    // 模拟请求图片
    viewModel.fetchImages()
    }

    然后,你需要实现 UITableViewDataSourcePrefetching 的协议,它的协议里包含俩个函数:

    // this protocol can provide information about cells before they are displayed on screen.

    @protocol UITableViewDataSourcePrefetching <NSObject>

    @required

    // indexPaths are ordered ascending by geometric distance from the table view
    - (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

    @optional

    // indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths:
    - (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

    @end

    第一个函数会基于当前滚动的方向和速度对接下来的 IndexPaths 进行 Prefetch,通常我们会在这里实现预加载数据的逻辑。

    第二个函数是一个可选的方法,当用户快速滚动导致一些 Cell 不可见的时候,你可以通过这个方法来取消任何挂起的数据加载操作,有利于提高滚动性能, 在下面我会讲到。

    实现这俩个函数的逻辑代码为:

    extension ViewController: UITableViewDataSourcePrefetching {
    // 翻页请求
    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
    let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
    if needFetch {
    // 1.满足条件进行翻页请求
    indicatorView.startAnimating()
    viewModel.fetchImages()
    }

    for indexPath in indexPaths {
    if let _ = viewModel.loadingOperations[indexPath] {
    return
    }

    if let dataloader = viewModel.loadImage(at: indexPath.row) {
    print("在 \(indexPath.row) 行 对图片进行 prefetch ")
    // 2 对需要下载的图片进行预热
    viewModel.loadingQueue.addOperation(dataloader)
    // 3 将该下载线程加入到记录数组中以便根据索引查找
    viewModel.loadingOperations[indexPath] = dataloader
    }
    }
    }

    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
    // 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
    indexPaths.forEach {
    if let dataLoader = viewModel.loadingOperations[$0] {
    print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
    dataLoader.cancel()
    viewModel.loadingOperations.removeValue(forKey: $0)
    }
    }
    }
    }

    最后,再加上俩个有用的方法该功能就大功告成了:

        // 用于计算 tableview 加载新数据时需要 reload 的 cell
    func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {
    let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []
    let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)
    return Array(indexPathsIntersection)
    }

    // 用于确定该索引的行是否超出了目前收到数据的最大数量
    func isLoadingCell(for indexPath: IndexPath) -> Bool {
    return indexPath.row >= (viewModel.currentCount)
    }

    见证时刻的奇迹到了,请看效果:




    通过日志,我们也可以清楚的看到,在滚动的过程中是有 Prefetch 和 CancelPrefetch 操作的:


    好了,到这里我就简单的实现了 UITableView 无尽滚动和对数据无缝加载的效果,你学会了吗?

    如何避免滚动时的卡顿

    当你遇到滚动卡顿的应用程序时,通常是由于任务长时间运行阻碍了 UI 在主线程上的更新,想让主线程有空来响应这类更新事件,第一步就是要将消耗时间的任务交给子线程去执行,避免在获取数据时阻塞主线程。

    苹果提供了很多为应用程序实现并发的方式,例如 GCD,我在这里对 Cell 上的图片进行异步加载使用的就是它。

    代码如下:


    class DataLoadOperation: Operation {
    var image: UIImage?
    var loadingCompleteHandle: ((UIImage?) -> ())?
    private var _image: ImageModel
    private let cachedImages = NSCache<NSURL, UIImage>()

    init(_ image: ImageModel) {
    _image = image
    }

    public final func image(url: NSURL) -> UIImage? {
    return cachedImages.object(forKey: url)
    }

    override func main() {
    if isCancelled {
    return
    }

    guard let url = _image.url else {
    return
    }
    downloadImageFrom(url) { (image) in
    DispatchQueue.main.async { [weak self] in
    guard let ss = self else { return }
    if ss.isCancelled { return }
    ss.image = image
    ss.loadingCompleteHandle?(ss.image)
    }
    }

    }

    // Returns the cached image if available, otherwise asynchronously loads and caches it.
    func downloadImageFrom(_ url: NSURL, completeHandler: @escaping (UIImage?) -> ()) {
    // Check for a cached image.
    if let cachedImage = image(url: url) {
    DispatchQueue.main.async {
    print("命中缓存")
    completeHandler(cachedImage)
    }
    return
    }

    URLSession.shared.dataTask(with: url as URL) { data, response, error in
    guard
    let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
    let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
    let data = data, error == nil,
    let _image = UIImage(data: data)
    else { return }
    // Cache the image.
    self.cachedImages.setObject(_image, forKey: url, cost: data.count)
    completeHandler(_image)
    }.resume()
    }
    }

    那具体如何使用呢!别急,听我娓娓道来,这里我再给大家一个小建议,大家都知道 UITableView 实例化 Cell 的方法是:tableView:cellForRowAtIndexPath: ,相信很多人都会在这个方法里面去进行数据绑定然后更新 UI,其实这样做是一种比较低效的行为,因为这个方法需要为每个 Cell 调用一次,它应该快速的执行并返回重用 Cell 的实例,不要在这里去执行数据绑定,因为目前在屏幕上还没有 Cell。我们可以在 tableView:willDisplayCell:forRowAtIndexPath: 这个方法中进行数据绑定,这个方法在显示cell之前会被调用。

    为每个 Cell 执行下载任务的实现代码如下:

     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "PreloadCellID") as? ProloadTableViewCell else {
    fatalError("Sorry, could not load cell")
    }

    if isLoadingCell(for: indexPath) {
    cell.updateUI(.none, orderNo: "\(indexPath.row)")
    }

    return cell
    }

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    // preheat image ,处理将要显示的图像
    guard let cell = cell as? ProloadTableViewCell else {
    return
    }

    // 图片下载完毕后更新 cell
    let updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in
    cell.updateUI(image, orderNo: "\(indexPath.row)")
    viewModel.loadingOperations.removeValue(forKey: indexPath)
    }

    // 1\. 首先判断是否已经存在创建好的下载线程
    if let dataLoader = viewModel.loadingOperations[indexPath] {
    if let image = dataLoader.image {
    // 1.1 若图片已经下载好,直接更新
    cell.updateUI(image, orderNo: "\(indexPath.row)")
    } else {
    // 1.2 若图片还未下载好,则等待图片下载完后更新 cell
    dataLoader.loadingCompleteHandle = updateCellClosure
    }
    } else {
    // 2\. 没找到,则为指定的 url 创建一个新的下载线程
    print("在 \(indexPath.row) 行创建一个新的图片下载线程")
    if let dataloader = viewModel.loadImage(at: indexPath.row) {
    // 2.1 添加图片下载完毕后的回调
    dataloader.loadingCompleteHandle = updateCellClosure
    // 2.2 启动下载
    viewModel.loadingQueue.addOperation(dataloader)
    // 2.3 将该下载线程加入到记录数组中以便根据索引查找
    viewModel.loadingOperations[indexPath] = dataloader
    }
    }
    }

    对预加载的图片进行异步下载(预热):


    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
    let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount}
    if needFetch {
    // 1.满足条件进行翻页请求
    indicatorView.startAnimating()
    viewModel.fetchImages()
    }

    for indexPath in indexPaths {
    if let _ = viewModel.loadingOperations[indexPath] {
    return
    }

    if let dataloader = viewModel.loadImage(at: indexPath.row) {
    print("在 \(indexPath.row) 行 对图片进行 prefetch ")
    // 2 对需要下载的图片进行预热
    viewModel.loadingQueue.addOperation(dataloader)
    // 3 将该下载线程加入到记录数组中以便根据索引查找
    viewModel.loadingOperations[indexPath] = dataloader
    }
    }
    }

    取消 Prefetch 时,cancel 任务,避免造成资源浪费

    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){
    // 该行在不需要显示的时候,取消 prefetch ,避免造成资源浪费
    indexPaths.forEach {
    if let dataLoader = viewModel.loadingOperations[$0] {
    print("在 \($0.row) 行 cancelPrefetchingForRowsAt ")
    dataLoader.cancel()
    viewModel.loadingOperations.removeValue(forKey: $0)
    }
    }
    }

    经过这般处理,我们的 UITableView 滚动起来肯定是如丝般顺滑,小伙伴们还等什么,还不赶紧试一试。


    图片缓存

    虽然我在上面对我的应用增加了并发操作,但是我一看 Xcode 的性能分析,我不禁陷入了沉思,我的应用程序太吃内存了,假如我不停的刷,那我的手机应该迟早会把我的应用给终止掉,下图是我刷到 200 行的时候的性能分析图:

    内存



    可以看到我的应用的性能分析很不理想,究其原因在于我的应用里显示了大量的图片资源,每次来回滚动的时候,都会重新去下载新的图片,而没有对图片做缓存处理。

    所以,针对这个问题,我为我的应用加入了缓存 NSCache 对象,来对图片做一个缓存,具体代码实现如下:


    class ImageCache: NSObject {

    private var cache = NSCache<AnyObject, UIImage>()
    public static let shared = ImageCache()
    private override init() {}

    func getCache() -> NSCache<AnyObject, UIImage> {
    return cache
    }
    }

    在下载开始的时候,检查有没有命中缓存,如果命中则直接返回图片,否则重新下载图片,并添加到缓存中:

    func downloadImageFrom(_ url: URL, completeHandler: @escaping (UIImage?) -> ()) {
    // Check for a cached image.
    if let cachedImage = getCacheImage(url: url as NSURL) {
    print("命中缓存")
    DispatchQueue.main.async {
    completeHandler(cachedImage)
    }
    return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
    guard
    let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
    let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
    let data = data, error == nil,
    let _image = UIImage(data: data)
    else { return }

    // Cache the image.
    ImageCache.shared.getCache().setObject(_image, forKey: url as NSURL)

    completeHandler(_image)
    }.resume()
    }
    有了缓存的加持后,再用 Xcode 来查看我的应用的性能,就会发现内存和磁盘的占用已经下降了很多:




    关于图片缓存的技术,这里只是用了最简单的一种,外面很多开源的图片库都有不同的缓存策略,感兴趣的可以去 GitHub 上学习一下它们的源码,我在这里就不做赘述了。




    作者:iOS鑫
    链接:https://www.jianshu.com/p/f00f43e401da

    收起阅读 »

    CameraX 入门食用方法

    CameraX 已经发布了 1.0正式版 对于涉及到使用摄像头的 App , 能否充分利用摄像头有着很大的区别,为此 对 CameraX 进行了解与认知有一定的必要性.📑即将学会用 Jetpack 组件支持库 CameraX 创建相机、拍摄、预览⭕要求Goog...
    继续阅读 »

    CameraX 已经发布了 1.0正式版 对于涉及到使用摄像头的 App , 能否充分利用摄像头有着很大的区别,为此 对 CameraX 进行了解与认知有一定的必要性.

    📑即将学会

    用 Jetpack 组件支持库 CameraX 创建相机、拍摄、预览

    ⭕要求

    Google官方声明 CameraX 是 Jetpack组件支持库,帮助我们简化相机相关应用的开发工作。并且提供了一致且易用的 API 接口,适用于大多数 Android 设备,并向后兼容至 Android 5.0(API 级别 21)

    因此 创建Android应用的时候mininumSDK 需要选择5.0

    前置内容

    使用 Intent 进行拍照

    在应用中拍照的最简便的方法便是使用MediaStore.ACTION_IMAGE_CAPTURE,传入Intent并启动

    image.png 这会启动系统照相机,并为用户提供一套完整的拍照功能 这时,如果用户授予了用户拍照权限,并对拍照图片满意,我们将在onActivityResult中获取图片.默认情况下,拍摄的照片会以缩略图的形式返回,使用键data可以获得缩略图

    Bundle extras = data.getExtras();
    Bitmap imageBitmap = (Bitmap) extras.get("data");

    而要获取完整图片,需要在启动相机的Intent中,在intent的意图中,添加MediaStore.EXTRA_OUTPUT 参数中设置文件的输出URI,该URI会存储完整的照片

    image.png 存储后进行完整图片路径后的图片 并进行设置 该存储设置有一定的局限性,请参考官方对拍照的操作 拍照  |  Android 开发者  |  Android Developers (google.cn)image.png 这是我们日常中会使用到的一些摄像头获取图片的操作. 而在开发过程中,我们对camera2的API使用时还是会有很多脏代码,而CameraX作为Google推出的jetpack组件.简化了对Camera的开发,让我们看看如何使用CameraX

    实战

    创建项目

    image.png

    在 App 模块中添加 CameraX 依赖 并同步

    image.png

    使用 PreviewView 实现 CameraX 预览

    1.修改布局文件 布局中添加 PreviewView

    在布局文件中进行修改 image.png

    Manifest声明相机权限
    <uses-permission android:name="android.permission.CAMERA" />
    动态申请相机权限
    //覆写onRequestPermissionsResult()方法 
    override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    if (requestCode == REQUEST_CODE_PERMISSIONS) {
    if (allPermissionsGranted()) {
    startCamera()
    } else {
    Toast.makeText(this,
    "权限未得到用户授予",
    Toast.LENGTH_SHORT).show()
    finish()
    }
    }
    2.请求 ProcessCameraProvider

    ProcessCameraProvider是一个单例 用于将相机的生命周期绑定到应用程序进程中的任何 生命周期持有者。因为cameraX具有生命周期感应 这可以使我们的应用程序省去打开和关闭相机的任务

    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
    3.检查 ProcessCameraProvider 可用性
    cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
    4.ProcessCameraProvider可用后 选择相机并绑定生命周期和用例
    • 创建 Preview
    • 指定所需的相机
    • 将所选相机和任意用例绑定到生命周期
    • 将 Preview 连接到 PreviewView
     cameraProviderFuture.addListener(Runnable {
    // 1将camera 绑定到 生命周期持有者
    val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

    // 2创建 Preview。
    val preview = Preview.Builder()
    .build()
    .also {
    //preview 连接 previewView
    it.setSurfaceProvider(findViewById<PreviewView>(R.id.viewFinder).getSurfaceProvider())
    }

    //3指定所需的相机
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
    // 4绑定前解绑
    cameraProvider.unbindAll()

    // 5绑定用户用例和相机到生命周期
    cameraProvider.bindToLifecycle(
    this, cameraSelector, preview)

    } catch(exc: Exception) {
    Log.e(TAG, "Use case binding failed", exc)
    }

    }, ContextCompat.getMainExecutor(this))

    效果图

    image.png

    这是camera的预览功能

    cameraX相片拍摄

    配置应用拍摄图片
    • 利用 ImageCapture.Builder 构建 imageCapture
    • 绑定用户用例和相机到生命周期 多了一个 imageCapture
    cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
    拍照
    • 获取对 ImageCapture 用例的引用
    • 创建图片存储容纳文件
    • 创建OutputFileOptions指定输出方式 输出路径等内容
    val outputFileOptions = ImageCapture.OutputFileOptions.Builder(File(...)).build()
    • 对 imageCapture 对象调用 takePicture()方法。传入刚才构建的 outputOptions 以及保存图片的回调

    最终修改代码

    cameraProviderFuture.addListener(Runnable {
    // 1将camera 绑定到 生命周期持有者
    val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

    // 2创建 Preview。
    val preview = Preview.Builder()
    .build()
    .also {
    //preview 连接 previewView
    it.setSurfaceProvider(findViewById<PreviewView>(R.id.viewFinder).getSurfaceProvider())
    }
    //todo 新增行
    //图像捕获 构建
    imageCapture = ImageCapture.Builder()
    .build()

    //3指定所需的相机
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
    // 4绑定前解绑
    cameraProvider.unbindAll()

    //todo 新增修改行
    // 5绑定用户用例和相机到生命周期
    cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

    } catch(exc: Exception) {
    Log.e(TAG, "Use case binding failed", exc)
    }

    }, ContextCompat.getMainExecutor(this))


    private fun takePhoto() {
    // 获取图像捕获用例
    val imageCapture = imageCapture ?: return


    // 创建存储文件对象
    val photoFile = File(
    outputDirectory,
    SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA
    ).format(System.currentTimeMillis()) + ".jpg")

    // 输出条件构建
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

    // 拍照 传入输出条件 以及拍照回调
    imageCapture.takePicture(
    outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
    override fun onError(exc: ImageCaptureException) {
    //异常打印
    Log.e(TAG, "拍照失败: ${exc.message}", exc)
    }

    override fun onImageSaved(output: ImageCapture.OutputFileResults) {
    val savedUri = Uri.fromFile(photoFile)
    val msg = "拍照成功: $savedUri"
    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
    Log.d(TAG, msg)
    }
    })
    }

    运行展示

    image.png


    收起阅读 »

    RecyclerView列表动画

    一 ItemAnimator的使用触发删除动画mDatas.remove(position); notifyItemRemoved(position) 触发添加动画mDatas.add(position,data); notifyItemInserted(p...
    继续阅读 »

    一 ItemAnimator的使用

    触发删除动画

    mDatas.remove(position);
    notifyItemRemoved(position)

    触发添加动画

    mDatas.add(position,data);
    notifyItemInserted(position)

    触发改变动画

    mDatas.set(position,newData);
    notifyItemChanged(position)

    使用简单动画

    //RecyclerView.LayoutManager.java
    public boolean supportsPredictiveItemAnimations() {
    return false;
    }

    添加/删除/改变都是渐入渐出动画:

    使用预测性动画

    //RecyclerView.LayoutManager.java
    public boolean supportsPredictiveItemAnimations() {
    return true;
    }

    二 配合LayoutAnimation使用

    RecyclerView和普通的ViewGroup一样支持LayoutAnimation.使用起来比较简单,设置RecyclerView的 animateLayoutChangeslayoutAnimation属性即可.

    案例

    layout_animation_recyclerview.gif
    图片出处:Layout animations on RecyclerView

    layoutanimation1.gif
    图片出处:RecyclerView 与 LayoutAnimation 实现的进入动画(一 ): List

    三 自定义RecyclerView列表动画

    案例学习:自定义change动画

    效果:

    代码实现:

    1.新建MyChangeAnimator类继承DefaultItemAnimator

    private class MyChangeAnimator extends DefaultItemAnimator {
    }

    2.重写DefaultItemAnimatorcanReuseUpdatedViewHolder()方法.

    @Override
    public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
    //change动画在同一个ItemHolder上执行
    return true;
    }

    3.新建ColorTextInfo类继承ItemHolderInfo,新增两个字段记录item的颜色和文字.

    private class ColorTextInfo extends ItemHolderInfo {
    int color;
    String text;
    }

    4.重写DefaultItemAnimatorobtainHolderInfo()方法,新建一个ColorTextInfo对象返回.

    @Override
    public ItemHolderInfo obtainHolderInfo() {
    return new ColorTextInfo();
    }
    1. 重写DefaultItemAnimatorrecordPreLayoutInformation()recordPostLayoutInformation()方法.在item变化前和变化后记录颜色和文字信息.
    public ItemHolderInfo recordPreLayoutInformation(RecyclerView.State state,
    RecyclerView.ViewHolder viewHolder, int changeFlags, List<Object> payloads) {
    ColorTextInfo info = (ColorTextInfo) super.recordPreLayoutInformation(state, viewHolder,changeFlags, payloads);
    //记录item变化前的颜色和文字
    return getItemHolderInfo((MyViewHolder) viewHolder, info);
    }

    public ItemHolderInfo recordPostLayoutInformation(@NonNull RecyclerView.State state,
    @NonNull RecyclerView.ViewHolder viewHolder) {
    ColorTextInfo info = (ColorTextInfo) super.recordPostLayoutInformation(state, viewHolder);
    //记录item变化后的颜色和文字
    return getItemHolderInfo((MyViewHolder) viewHolder, info);
    }

    //从viewHolder上获取ColorTextInfo信息
    private ItemHolderInfo getItemHolderInfo(MyViewHolder viewHolder, ColorTextInfo info) {
    final MyViewHolder myHolder = viewHolder;
    final int bgColor = ((ColorDrawable) myHolder.container.getBackground()).getColor();
    info.color = bgColor;
    info.text = (String) myHolder.textView.getText();
    return info;
    }

    6.重写DefaultItemAnimatoranimateChange()方法,执行自定义的change动画

    顺序执行
    顺序执行
    开始change动画
    旧的颜色到黑色的渐变
    旧的文字绕x轴旋转0->90度
    黑色到新的颜色的渐变
    新的文字绕x轴旋转-90->0度
    change动画结束
    public boolean animateChange(@NonNull final RecyclerView.ViewHolder oldHolder,
    @NonNull final RecyclerView.ViewHolder newHolder,
    @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {

    ...

    final MyViewHolder viewHolder = (MyViewHolder) newHolder;

    //读取change前后的颜色和文字信息
    ColorTextInfo oldInfo = (ColorTextInfo) preInfo;
    ColorTextInfo newInfo = (ColorTextInfo) postInfo;
    int oldColor = oldInfo.color;
    int newColor = newInfo.color;
    final String oldText = oldInfo.text;
    final String newText = newInfo.text;

    LinearLayout newContainer = viewHolder.container;
    final TextView newTextView = viewHolder.textView;


    // 构造旧的颜色到黑色的渐变动画
    ObjectAnimator fadeToBlack = null, fadeFromBlack;
    fadeToBlack = ObjectAnimator.ofInt(newContainer, "backgroundColor",
    startColor, Color.BLACK);
    fadeToBlack.setEvaluator(mColorEvaluator);

    // 构造黑色到新的颜色的渐变动画
    fadeFromBlack = ObjectAnimator.ofInt(newContainer, "backgroundColor",
    Color.BLACK, newColor);

    // 这两个渐变动画顺序执行
    AnimatorSet bgAnim = new AnimatorSet();
    bgAnim.playSequentially(fadeToBlack, fadeFromBlack);

    //构造旧的文字绕x轴旋转0->90度的动画
    ObjectAnimator oldTextRotate = null, newTextRotate;
        oldTextRotate = ObjectAnimator.ofFloat(newTextView, View.ROTATION_X, 0, 90);
        oldTextRotate.setInterpolator(mAccelerateInterpolator);
        oldTextRotate.addListener(new AnimatorListenerAdapter() {
         boolean mCanceled = false;
         @Override
         public void onAnimationStart(Animator animation) {
         //动画开始时显示旧的文字
         newTextView.setText(oldText);
         }

         @Override
         public void onAnimationCancel(Animator animation) {
         mCanceled = true;
         }

         @Override
         public void onAnimationEnd(Animator animation) {
         if (!mCanceled) {
         //动画结束时显示新的文字
         newTextView.setText(newText);
         }
         }
        });

    // 构造新的文字绕x轴旋转-90->0度的动画
    newTextRotate = ObjectAnimator.ofFloat(newTextView, View.ROTATION_X, -90, 0);
    newTextRotate.setInterpolator(mDecelerateInterpolator);

    //两个旋转动画顺序执行
    AnimatorSet textAnim = new AnimatorSet();
    textAnim.playSequentially(oldTextRotate, newTextRotate);

    // 渐变的动画和旋转的动画同时执行
    AnimatorSet changeAnim = new AnimatorSet();
    changeAnim.playTogether(bgAnim, textAnim);
    changeAnim.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    dispatchAnimationFinished(newHolder);
    ...
    }
    });
    changeAnim.start();

    return true;
    }

    7.设置RecyclerViewItemAnimator为自定义的MyChangeAnimator

    mRecyclerView.setItemAnimator(mChangeAnimator);

    这是简化后的代码.官方完整的demo里还包括了边界情况的处理,处理了上一次change动画没执行完时触发新的change动画的情况.
    代码地址:github.com/android/vie…

    自定义add和del动画

    目标效果:

    代码实现:

    1.拷贝DefaultItemAnimator源码,命名为DefaultItemAnimatorOpen.java

    2.重写添加和删除的动画实现.(这一步需要修改DefaultItemAnimatorOpen中一些方法和属性的可见性为protected).

    参考DefaultItemAnimatorOpen中对应方法的实现,修改动画相关的代码即可:

    class CustomAddDelAnimation : DefaultItemAnimatorOpen() {


    override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
    //重写item添加的动画
    resetAnimation(holder)
    //将新增的item绕x轴旋转90度
    holder.itemView.rotationX = 90f
    mPendingAdditions.add(holder)
    return true
    }

    override fun animateAddImpl(holder: RecyclerView.ViewHolder) {
    //添加item时执行绕x轴90->0度的旋转
    mAddAnimations.add(holder)
    holder.itemView.animate().apply {
    rotationX(0f)
    duration = addDuration
    interpolator = DecelerateInterpolator(3f)
    setListener(object : AnimatorListenerAdapter() {

    override fun onAnimationStart(animation: Animator?) {
    dispatchAddStarting(holder)
    }

    override fun onAnimationCancel(animation: Animator?) {
    ViewHelper.clear(holder.itemView)
    }

    override fun onAnimationEnd(animation: Animator) {
    ViewHelper.clear(holder.itemView)
    dispatchAddFinished(holder)
    mAddAnimations.remove(holder)
    dispatchFinishedWhenDone()
    }
    })
    }.start()
    }

    override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
    //重写item删除的动画
    resetAnimation(holder)
    mPendingRemovals.add(holder)
    return true
    }

    override fun animateRemoveImpl(holder: RecyclerView.ViewHolder) {
    //删除item时执行绕x轴0->90度的旋转
    mRemoveAnimations.add(holder)
    holder.itemView.animate().apply {
    rotationX(90f)
    duration = addDuration
    interpolator = DecelerateInterpolator(3f)
    setListener(object : AnimatorListenerAdapter() {

    override fun onAnimationStart(animation: Animator?) {
    dispatchRemoveStarting(holder)
    }

    override fun onAnimationCancel(animation: Animator?) {
    ViewHelper.clear(holder.itemView)
    }

    override fun onAnimationEnd(animation: Animator) {
    ViewHelper.clear(holder.itemView)
    dispatchRemoveFinished(holder)
    mRemoveAnimations.remove(holder)
    dispatchFinishedWhenDone()
    }
    })
    }.start()
    }

    }

    demo地址:HoopAndroidDemos

    四 RecyclerView列表动画的原理简析

    «abstract»ItemAnimator«interface»ItemAnimatorListener«interface»ItemAnimatorFinishedListenerItemHolderInfo«abstract»SimpleItemAnimatorDefaultItemAnimator依赖依赖依赖继承继承

    默认可预测动画执行的顺序

    执行删除动画
    移动动画
    改变动画
    item动画执行结束
    执行延迟的item动画(下一帧)
    默认效果为渐出
    默认效果为平移
    默认效果为渐出+渐入
    添加动画(默认效果为渐入)
    重置状态

    列表动画是立即执行吗?
    animateXXX()方法返回true时,延迟到在下一帧率执行.返回false时立即执行.
    DefaultItemAnimator的动画都是在下一帧执行.

    列表动画的实现流程

    自定义立即执行的change动画:

    RecyclerViewItenAnimatordispatchLayoutStep1()recordPreLayoutInformation()dispatchLayoutStep3()recordPostLayoutInformation()animateChange()RecyclerViewItenAnimator

    默认延迟执行的add动画:

    RecyclerViewItenAnimatordispatchLayoutStep3()animateMove()animateAdd()postAnimationRunner()animateMoveImplanimateAddImplRecyclerViewItenAnimator

    收起阅读 »

    深入理解内存泄漏

    一、JVM内存模型常见jvm内存模型,主要分为堆区,本地方法栈,虚拟机栈,程序计数器,和方法区。如下图所示: (1)程序计数器每个线程都会有自己私有的程序计数器(PC)。可以看作是当前线程所执行的字节码的行号指示器。 也可以理解为下一条将要执行的指令...
    继续阅读 »

    一、JVM内存模型

    常见jvm内存模型,主要分为堆区,本地方法栈,虚拟机栈,程序计数器,和方法区。如下图所示: image.png

    (1)程序计数器

    每个线程都会有自己私有的程序计数器(PC)。可以看作是当前线程所执行的字节码的行号指示器。 也可以理解为下一条将要执行的指令的地址或者行号。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程上下文切换,线程恢复时,都要依赖PC.

    • 如果线程正在执行的是一个Java方法,PC值为正在执行的虚拟机字节码指令的地址
    • 如果线程正在执行的是Native方法,PC值为空(未定义)

    (2)虚拟机栈(VM Stack)

    简介

    VM Stack也是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。 **在《java虚拟机规范》一书中对这部分的描述如下:**栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在 Java 虚拟机栈( §2.5.5)之中,每一个栈帧都有自己的局部变量表( Local Variables, §2.6.1)、操作数栈( OperandStack, §2.6.2)和指向当前方法所属的类的运行时常量池( §2.5.5)的引用。 VM-Stack 说白了,VM Stack是一个栈,也是一块内存区域。 所以,他是有大小的。虽然有大小,但是一般而言,各种虚拟机的实现都支持动态扩展这部分内存。

    • 如果线程请求的栈深度太大,则抛出StackOverflowError
    • 如果动态扩展时没有足够的大小,则抛出OutOfMemoryError以下代码肯定会导致StackOverflowError:

    StackOverflowError

    public static void method() {
    method();
    }

    public static void main(String[] args) {
    method();
    }

    Exception in thread "main" java.lang.StackOverflowError
    at xxx.xxx.xxx.method(JavaVMStackSOF.java:10)
    复制代码

    (3)本地方法栈

    Java 虚拟机实现可能会使用到传统的栈(通常称之为“ C Stacks”)来支持 native 方法( 指使用 Java 以外的其他语言编写的方法)的执行,这个栈就是本地方法栈( Native MethodStack)。 VM Stack是为执行java方法服务的,此处的Native Method Stack是为执行本地方法服务的。 此处的本地方法指定是和具体的底层操作系统层面相关的接口调用了(这部分太高高级了,不想深究……)。 《java虚拟机规范》中没有对这部分做具体的规定。所以就由VM的实现者自由发挥了。 有的虚拟机(比如HotSpot)将VM Stack和Native Method Stack合二为一,所以VM的另一种内存区域图就如下面所示了: image.png

    (4)方法区

    方法区是由所有线程共享的内存区域。 方法区存储的大致内容如下:

    • 每一个类的结构信息
      • 运行时常量池( Runtime Constant Pool)
      • 字段和方法数据
      • 构造函数和普通方法的字节码内容
    • 类、实例、接口初始化时用到的特殊方法

    以下是本人对《java虚拟机规范》一书中对方法区的介绍的总结:

    • 在虚拟机启动的时候被创建
    • 虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集
    • 不限定实现方法区的内存位置和编译代码的管理策略
    • 容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
    • 方法区在实际内存空间中可以是不连续的
    • Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段
      • 对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段
    • 如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常

    (5)堆

    简介

    在 Java 虚拟机中,堆( Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。 以下是本人对《java虚拟机规范》一书中对Java堆的介绍的总结:

    • 在虚拟机启动的时候就被创建
    • 是所有线程共享的内存区域
    • 存储了被自动内存管理系统所管理的各种对象
    • Java 堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩
    • Java 堆所使用的内存不需要保证是连续的
    • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常
    • 实现者应当提供给程序员或者最终用户调节 Java 堆初始容量的手段
    • 所有的对象实例以及数组都要在堆上分配
    • 至于堆内存的详细情况,将在后续的GC相关文章中介绍。

    堆内存中的OutOfMemoryError以下示例代码肯定导致堆内存溢出:

    public static void main(String[] args) {
       ArrayList<Integer> list = Lists.newArrayList();
       while (true) {
           list.add(1);
      }
    }
    复制代码

    无限制的往list中添加元素,无论你的堆内存分配的多大,都会有溢出的时候。

    java.lang.OutOfMemoryError: Java heap space
    复制代码

    二、内存优化工具

    (1)LeakCanary

    介绍

    leakCanary这是一个集成方便, 使用便捷,配置超级简单的框架,实现的功能却是极为强大的线下内存检测工具。 screenshot-2.0.png

    如何使用

    dependencies {
    // debugImplementation because LeakCanary should only run in debug builds.
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
    }
    复制代码

    在项目集成之后,在Android Studio Logcat日志通过筛选LeakCanary可以看到如下日志,标志LeakCanary已经安装成功,并且已经启动。 image.png

    内存泄漏

    我们经常会用到很多单例的工具类,往往这些单例也是经常容易发生内存泄漏的地方。下面我们模拟一下单例工具类造成内存泄漏的情况,封装一个ToastUtils的单例类,内部持有context的引用,这样在页面销毁之后依然持有context的引用,造成无法销毁,从而造成内存泄漏。

    object ToastUtils {
    private var context: Context? = null

    fun toast(context: Context, text: String) {
    this.context =context
    Toast.makeText(context, text, Toast.LENGTH_LONG)
    }
    }
    复制代码

    在控制台和手机上都可以看到内存泄漏的信息。如下所示: image.pngimage.png 手机上可以明显看到内存泄漏的列表,通过点击item即可看到详细的堆栈信息。

    ====================================
    1 APPLICATION LEAKS

    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.

    76243 bytes retained by leaking objects
    Signature: 409f986871fac1acf6527c76b4d658d03ffa8e11
    ┬───
    │ GC Root: Local variable in native code

    ├─ android.os.HandlerThread instance
    │ Leaking: NO (PathClassLoader↓ is not leaking)
    │ Thread name: 'LeakCanary-Heap-Dump'
    │ ↓ Thread.contextClassLoader
    ├─ dalvik.system.PathClassLoader instance
    │ Leaking: NO (ToastUtils↓ is not leaking and A ClassLoader is never leaking)
    │ ↓ ClassLoader.runtimeInternalObjects
    ├─ java.lang.Object[] array
    │ Leaking: NO (ToastUtils↓ is not leaking)
    │ ↓ Object[].[620]
    ├─ com.caichen.article_caichen.ToastUtils class
    Leaking: NO (a class is never leaking)
    │ ↓ static ToastUtils.context
    │ ~~~~~~~
    ╰→ com.caichen.article_caichen.LeakActivity instance
    Leaking: YES (ObjectWatcher was watching this because com.caichen.article_caichen.LeakActivity received
    Activity#onDestroy() callback and Activity#mDestroyed is true)
    Retaining 76.2 kB in 1135 objects
    key = 42b604fb-b746-48f5-8ff5-f56cd01a570e
    watchDurationMillis = 5204
    retainedDurationMillis = 190
    mApplication instance of android.app.Application
    mBase instance of androidx.appcompat.view.ContextThemeWrapper
    ====================================
    复制代码

    同时通过控制台也可以看到详细的信息。

    如何分析

        ├─ com.caichen.article_caichen.ToastUtils class
    Leaking: NO (a class is never leaking)
    │ ↓ static ToastUtils.context
    │ ~~~~~~~
    ╰→ com.caichen.article_caichen.LeakActivity instance
    复制代码

    通过堆栈信息,大致会得到内存泄漏的大致引用调用路径,最终定位到ToastUtils类中,存在内存泄漏的地方就是内部的context变量,因为被单例对象所持有那么他引用的context和单例的生命周期相同。所以当页面消失时,无法被垃圾回收器销毁从而造成内存泄漏。

    (2)Profile Memory

    介绍

    Profile Memory是 Android Profiler 中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,让您可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。

    如何使用

    如需打开内存性能分析器,请按以下步骤操作:

    1. 依次点击 View > Tool Windows > Profiler(您也可以点击工具栏中的 Profile 图标 )。
    2. 从 Android Profiler 工具栏中选择要分析的设备和应用进程。如果您已通过 USB 连接设备但系统未列出该设备,请确保您已启用 USB 调试
    3. 点击 MEMORY 时间轴上的任意位置以打开内存性能分析器。

    如图所示: image.png 点击Memory选项可以进入内存性能分析器界面, image.png 右上角分别展示出jvm对应的内存的情况。 内存计数中的类别如下:

    • Total:内存占用的总和值
    • Java:从 Java 或 Kotlin 代码分配的对象的内存。
    • Native:从 C 或 C++ 代码分配的对象的内存。即使您的应用中不使用 C++,您也可能会看到此处使用了一些原生内存,因为即使您编写的代码采用 Java 或 Kotlin 语言,Android 框架仍使用原生内存代表您处理各种任务,如处理图像资源和其他图形。
    • Graphics:图形缓冲区队列为向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
    • Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。
    • Code:您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。
    • Others:您的应用使用的系统不确定如何分类的内存。
    • Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。如果连接到搭载 Android 7.1 及更低版本的设备,只有在内存性能分析器连接到您运行的应用时,才开始此分配计数。因此,您开始分析之前分配的任何对象都不会被计入。但是,Android 8.0 及更高版本附带一个设备内置性能剖析工具,该工具可跟踪所有分配,因此,在 Android 8.0 及更高版本上,此数字始终表示您的应用中待处理的 Java 对象总数。

    内存抖动

    什么是内存抖动,内存抖动就是内存在短暂时间频繁的GC和分配内存,导致内存不稳定。体现在profile中就是内存曲线呈现锯齿状的形状。 image.png

    影响

    1. 频繁的创建对象造成内存碎片和不足
    2. 碎片的内存无法被分配从而容易导致内存溢出
    3. 频繁GC导致应用性能下降

    如何分析

    1. 通过点击Record然后可以记录出内存片的内存信息

    image.png

    1. 通过分析可以看出,string,char,int和StringBuilder都是占用内存比较多的对象
    2. 分析源码可以看出,在handleMessage中不断的进行创建intArray的对象,然后handlerMessage执行完毕之后创建的对象进行销毁,所以内存曲线呈现出来的形状呈锯齿状。
    3. 其中可以看出int占用的内存并没有string占有的内存大,那是因为通过log日志打印的字符串,通过StringBuilder进行了拼接,所以string和stringBuilder占用的内存比Int占用的内存还要大。
    private fun initHandler() {
    handler = Handler(Looper.getMainLooper(), Handler.Callback {
    for (index in 0..1000) {
    val asc = IntArray(100000) { 0 }
    Log.d("YI", "$asc")
    }
    handler?.sendEmptyMessageDelayed(0, 300)
    true
    })

    handler?.sendEmptyMessageDelayed(0, 300)
    }
    复制代码

    (3)Memory Analyzer

    介绍

    Eclipse Memory Analysis Tools (MAT) 是一个分析 Java堆数据的专业工具,用它可以定位内存泄漏的原因。

    如何使用

    1. 通过As导出的Heap文件不能直接使用需要通过SDK/platform-tools中的Hprof可执行文件将androidhea文件进行转化。
    2. 打开MAT程序,通过Open heap打开文件。

    image.pngimage.png

    如何分析

    1. 通过筛选类进行查看实例

    image.png

    1. 通过包名进行查看实例

    image.png 图中1处可可以指定分类的方式,图中2处可以找到自己包名下面的类,图中3处可以看到所有的包名下的实例和个数。可以看出图中LeakActivity的实例的个数11个,我们可以大致猜测出这个Activity是泄漏了。 接着我们查看Dominator Tree然后可以看出,罗列出leakActivity的所有的实例。 image.png 右键选中,然后点击Merge Shortest Paths to GC Roots可以看到引用路径。LeakActivity的引用呗Toastutils所持有。然后通过检查源码,可以看出内部context持有外部的引用,ToastUtils又是单例造成了activity的泄漏。 image.png


    收起阅读 »

    轻量级APP启动信息构建方案

    背景在头条的启动框架下,启动任务已经划分的较为明确,而启动时序是启动任务中的关键信息。目前我们获取这些信息的主要手段是看systrace,但直接读systrace存在一些问题:systrace在release下一些信息不全,例如IO线程信息,而启动优化的主要评...
    继续阅读 »

    背景

    在头条的启动框架下,启动任务已经划分的较为明确,而启动时序是启动任务中的关键信息。目前我们获取这些信息的主要手段是看systrace,但直接读systrace存在一些问题:

    • systrace在release下一些信息不全,例如IO线程信息,而启动优化的主要评估场景是release
    • systrace信息相对较重,可阅读性差,同时对启动任务的阅读的干扰性大

    在上述问题的影响下,会增加开发人员排查、验证启动任务问题,以及优化启动任务的难度。

    因此本文考虑设计一个轻量级的信息描述、收集与信息重建方案,灵活适应release模式与debug模式,同时增加可阅读性,降低开发人员排查问题的成本。

    1 方案设计

    轻量级启动信息构建方案主要由三部分组成:

    • 启动信息构建:负责提炼关键信息做成新数据结构
    • 启动信息收集:负责收集、输出各个任务的信息到重建模块
    • 启动信息重建:负责信息构建、输出可视化图形

    2 具体模块实现

    2.1 启动信息构建

    data class InitDataStruct(
    var startTime: Long = 0,
    var duration: Long = 0,
    var currentProcess: String = "",
    var taskName: String = ""
    )
    复制代码

    关键的启动信息主要有这么几个维度:

    • 启动时间(归一化)
    • 启动耗时
    • 启动线程
    • 启动名称

    而并不关心,即需要剔除掉的任务:

    • 非启动任务信息(这并不是说它不重要,只是在启动框架这一环它并不是高优)
    • 启动任务stack

    Format形如

    {"task_name":"class com.xxx.xxxTask","start_time":5,"duration":9,"current_process":"AA xxxThread#4"}
    复制代码

    2.2 启动信息收集

    由于没接入公司平台(太小),因此考虑就以log的方式输出结果。

    大概是希望实现下面的功能,但一个一个加就有点复制粘贴有点太low了

    调研了一下有一种AspectJ的做法,可以利用

    @PointCut("execution(* com.xxx.xxx.xxxTask.run(*))")

    在task周围埋下切入点

    利用@Before@After注入切入代码即可。

    2.3 启动信息收集与绘制

    由于目前是依赖人工进行启动分析,因此我们收集启动信息的手段依赖于Console打印的日志,形如

    {"task_name":"class com.xxx.Task","start_time":0,"duration":2,"current_process":"main"}
    复制代码

    这里我们直接写个读取工具给他转义一下,让他变成具有可读性的数据结构

    # 在Client中以json保存下来的
    def toInitInfo(json):
    return InitInfo(json["start_time"], json["duration"], json["current_process"], str(json["task_name"]).split('.')[-1])

    class InitInfo:
    #startTime和duration均做了归一化
    def __init__(self, startTime, duration, currentProcessName, taskName):
    self.startTime = startTime
    self.taskName = taskName
    self.duration = duration
    self.currentProcessName = currentProcessName

    def printitself(self):
    print("task_name : " + self.taskName)
    print("\tstartTime : " + str(self.startTime))
    print("\tduration : " + str(self.duration))
    print("\tcurrentProcessName : " + self.currentProcessName)

    # 获取task时长
    def getNameCombineDuration(self):
    return self.taskName + " " + str(self.duration)

    # 获取当前打印的最大长度
    def getConstructLen(self):
    return len(self.getNameCombineDuration()) + 2

    def generateFormatStr(self, perTime, perBlank):
    totalLen = max(3, int(1.0 * perBlank * max(1, self.duration) / perTime))
    cntLen = max(0, totalLen - self.getConstructLen())
    strr = "|" + (cntLen / 2 + cntLen % 2) * "-" + self.getNameCombineDuration()[0:min(totalLen - 2, len(self.getNameCombineDuration()))]+ cntLen / 2 * "-" + "|"
    return strr

    def generateBlank(self, timeNow, perTime, perBlank):
    strr = max(0, int((self.startTime - timeNow) / perTime) * perBlank) * " "
    return strr
    复制代码

    并将所有task插入到list中,以完成时间作为sort Function

    def sortByEnd(initInfo1, initInfo2):
    return (initInfo1.startTime + initInfo1.duration) <= (initInfo2.startTime + initInfo2.duration)

    def dealWithList():
    for item in line_jsons:
    if(taskMap.has_key(item.currentProcessName)):
    taskMap[item.currentProcessName].append(item)
    else:
    taskMap[item.currentProcessName] = []
    taskMap[item.currentProcessName].append(item)
    复制代码

    现在到了问题的核心,我们该采用什么规则把绘图绘制出来,这取决于我们需要得到的信息有哪些:

    • 第一种:分析启动任务耗时,可采用类似systrace,横轴为固定的单位时间长度,纵轴是currentProcess
    def drawMp():
    duraLen = 0
    maxLen = 0

    # 10ms间隔
    currentPerTime = 10
    endFile = open("timeline.txt","w")

    # 先保证起始坐标轴一致
    for key in taskMap.keys():
    maxLen = max(maxLen, len(key))

    # 计算最长字符串

    for item in line_jsons:
    duraLen = max(duraLen, item.getConstructLen())

    # 画个坐标轴
    xplot = maxLen * " " + " :"
    for index in range(0, (line_jsons[-1].startTime + line_jsons[-1].duration) / currentPerTime):
    cntLen = duraLen - 2 - len(str(index * currentPerTime))
    xplot += "|" + (cntLen / 2 + cntLen % 2) * "-" + str(index * currentPerTime) + cntLen / 2 * "-" + "|"

    endFile.write(xplot + "\n")

    # 画图
    for key in taskMap.keys():
    strr = key + (maxLen - len(key)) * " " + " :"
    timeNow = 0
    for item in taskMap[key]:
    item.printitself()
    strr += item.generateBlank(timeNow, perTime = currentPerTime, perBlank = duraLen)
    strr += item.generateFormatStr(10, duraLen)
    timeNow = item.startTime + item.duration

    strr += "\n"
    endFile.write(strr)

    endFile.close()
    复制代码
    • 第二种:分析启动任务排布的合理性,即是否存在长尾型的启动路径,这里考虑横轴为离散化后的启动任务时间,纵轴为currentProcess
    ## 第二种画图法:离散

    # 离散点阵图
    duraCordi = []

    def drawMp2():
    # 离散单位区间长度
    duraLen = 0

    def addBlank(st, ed):
    return (ed - st) * duraLen * " "

    def formatString(st, ed, taskName, duraLen):
    strr = "|"
    leftBlank = (ed - st) * duraLen - 2 - len(taskName)
    strr += (leftBlank / 2 + leftBlank % 2) * "-"
    strr += taskName
    strr += leftBlank / 2 * "-" + "|"
    return strr

    # 先离散
    # 最短是 -> |maxLen(xxxTask)|
    dura = []
    filee = open("timeline2.txt","w")
    for item in line_jsons:
    duraLen = max(duraLen, len(item.getNameCombineDuration()) + 2)
    dura.append(item.startTime)
    dura.append(item.startTime + item.duration)

    duraCordi = list(set(dura))
    duraCordi.sort()
    print(duraCordi)

    #再遍历塞值进去
    maxLen = 0
    for key in taskMap.keys():
    maxLen = max(maxLen, len(key))

    for key in taskMap.keys():
    currentIndex = 0
    strr = key + (maxLen - len(key)) * " " + " :"
    for item in taskMap[key]:
    stIndex = bisect.bisect_left(duraCordi, item.startTime)
    edIndex = bisect.bisect_left(duraCordi, item.startTime + max(item.duration, 1))
    strr += addBlank(currentIndex, stIndex)
    strr += formatString(stIndex, edIndex, item.getNameCombineDuration(), duraLen = duraLen)
    currentIndex = edIndex

    strr += "\n"
    filee.write(strr)

    filee.close()
    复制代码

    3 效果对比

    • 第一种启动耗时为单位的

    • 第二种启动时间离散化后的

    比如我们需要分析启动任务的排布是否合理,就可以看第二种图像,可以看到主线程启动任务较多,可能存在一定的长尾效应。

    相比systrace,更为轻量

    收起阅读 »

    JetpackMVVM七宗罪之二:在launchWhenX中启动协程

    首先承认这个系列有点标题党,Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,帮助大家打造更健康的应用架构 Flow vs LiveData 自 StateFlow/ SharedFlow ...
    继续阅读 »




    首先承认这个系列有点标题党,Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,帮助大家打造更健康的应用架构



    Flow vs LiveData


    自 StateFlow/ SharedFlow 出现后, 官方开始推荐在 MVVM 中使用 Flow 替换 LiveData。 ( 见文章:从 LiveData 迁移到 Kotlin 数据流 )


    Flow 基于协程实现,具有丰富的操作符,通过这些操作符可以实现线程切换、处理流式数据,相比 LiveData 功能更加强大。 但唯有一点不足,无法像 LiveData 那样感知生命周期。


    感知生命周期为 LiveData 至少带来以下两个好处:




    1. 避免泄漏:当 lifecycleOwner 进入 DESTROYED 时,会自动删除 Observer

    2. 节省资源:当 lifecycleOwner 进入 STARTED 时才开始接受数据,避免 UI 处于后台时的无效计算。



    Flow 也需要做到上面两点,才能真正地替代 LiveData。


    lifecycleScope


    lifecycle-runtime-ktx 库提供了 lifecycleOwner.lifecycleScope 扩展,可以在当前 Activity 或 Fragment 销毁时结束此协程,防止泄露。


    Flow 也是运行在协程中的,lifecycleScope 可以帮助 Flow 解决内存泄露的问题:


    lifecycleScope.launch {
    viewMode.stateFlow.collect {
    updateUI(it)
    }
    }

    虽然解决了内存泄漏问题, 但是 lifecycleScope.launch 会立即启动协程,之后一直运行直到协程销毁,无法像 LiveData 仅当 UI 处于前台才执行,对资源的浪费比较大。


    因此,lifecycle-runtime-ktx 又为我们提供了 LaunchWhenStartedLaunchWhenResumed ( 下文统称为 LaunchWhenX


    launchWhenX 的利与弊


    LaunchWhenX 会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。 lifecycleScope + launchWhenX 的组合终于使 Flow 有了与 LiveData 相媲美的生命周期可感知能力:




    1. 避免泄露:当 lifecycleOwner 进入 DESTROYED 时, lifecycleScope 结束协程

    2. 节省资源:当 lifecycleOwner 进入 STARTED/RESUMED 时 launchWhenX 恢复执行,否则挂起。



    但对于 launchWhenX 来说, 当 lifecycleOwner 离开 X 状态时,协程只是挂起协程而非销毁,如果用这个协程来订阅 Flow,就意味着虽然 Flow 的收集暂停了,但是上游的处理仍在继续,资源浪费的问题解决地不够彻底。



    资源浪费


    举一个资源浪费的例子,加深理解


    fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult?) {
    result ?: return
    try { offer(result.lastLocation) } catch(e: Exception) {}
    }
    }
    // 持续获取最新地理位置
    requestLocationUpdates(
    createLocationRequest(), callback, Looper.getMainLooper())

    }

    如上,使用 callbackFlow 封装了一个 GoogleMap 中获取位置的服务,requestLocationUpdates 实时获取最新位置,并通过 Flow 返回


    class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // 进入 STATED 时,collect 开始接收数据
    // 进入 STOPED 时,collect 挂起
    lifecycleScope.launchWhenStarted {
    locationProvider.locationFlow().collect {
    // Update the UI
    }
    }
    }
    }

    LocationActivity 进入 STOPED 时, lifecycleScope.launchWhenStarted 挂起,停止接受 Flow 的数据,UI 也随之停止更新。但是 callbackFlow 中的 requestLocationUpdates 仍然还在持续,造成资源的浪费。


    因此,即使在 launchWhenX 中订阅 Flow 仍然是不够的,无法完全避免资源的浪费


    解决办法:repeatOnLifecycle


    lifecycle-runtime-ktx 自 2.4.0-alpha01 起,提供了一个新的协程构造器 lifecyle.repeatOnLifecycle, 它在离开 X 状态时销毁协程,再进入 X 状态时再启动协程。从其命名上也可以直观地认识这一点,即围绕某生命周期的进出反复启动新协程



    使用 repeatOnLifecycle 可以弥补上述 launchWhenX 对协程仅挂起而不销毁的弊端。因此,正确订阅 Flow 的写法应该如下(以在 Fragment 中为例):


    onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
    viewMode.stateFlow.collect { ... }
    }
    }
    }

    当 Fragment 处于 STARTED 状态时会开始收集数据,并且在 RESUMED 状态时保持收集,最终在 Fragment 进入 STOPPED 状态时结束收集过程。


    需要注意 repeatOnLifecycle 本身是个挂起函数,一旦被调用,将走不到后续代码,除非 lifecycle 进入 DESTROYED。


    冷流 or 热流


    顺道提一点,前面举得地图SDK的例子是个冷流的例子,对于热流(StateFlow/SharedFlow)是否有必要使用 repeatOnLifecycle 呢? 个人认为热流的使用场景中,像前面例子那样的情况会少一些,但是在 StateFlow/SharedFlow 的实现中,需要为每个 FlowCollector 分配一些资源,如果 FlowCollector 能即使销毁也是有利的,同时为了保持写法的统一,无论冷流热流都建议使用 repeatOnLifecycle


    最后:Flow.flowWithLifecycle


    当我们只有一个 Flow 需要收集时,可以使用 flowWithLifecycle 这样一个 Flow 操作符的形式来简化代码


    lifecycleScope.launch {
    viewMode.stateFlow
    .flowWithLifecycle(this, Lifecycle.State.STARTED)
    .collect { ... }
    }

    当然,其本质还是对 repeatOnLifecycle 的封装:


    public fun <T> Flow<T>.flowWithLifecycle(
    lifecycle: Lifecycle,
    minActiveState: Lifecycle.State = Lifecycle.State.STARTED
    )
    : Flow<T> = callbackFlow {
    lifecycle.repeatOnLifecycle(minActiveState) {
    this@flowWithLifecycle.collect {
    send(it)
    }
    }
    close()
    }


    收起阅读 »

    客户端网络优化(一)-原理篇

    0x01 前言 网络优化是客户端技术方向中公认的一个深度领域,对于 App 性能和用户体验至关重要。本文除了 DNS 、连接和带宽方面的优化技术外,会结合着优化的一些实践,以及在成本和收益的衡量,会有区别于市面上其他的分享,希望对大家有所帮助。 为什么优化...
    继续阅读 »

    0x01 前言


    网络优化是客户端技术方向中公认的一个深度领域,对于 App 性能和用户体验至关重要。本文除了 DNS 、连接和带宽方面的优化技术外,会结合着优化的一些实践,以及在成本和收益的衡量,会有区别于市面上其他的分享,希望对大家有所帮助。


    为什么优化


    肯定有同学会有疑问,网络请求不就是通过网络框架 SDK 去服务端请求数据吗,尤其在这个性能过剩的年代,时间都不会差多少,我们还有没有必要再去抠细节做优化了,废话不多说咱们直接看数据,来证明优化的价值



    • BBC 发现网站加载时间每延长 1 秒 用户便会流失 10%


    • Google 发现页面加载时间超过 353% 的用户将停止访问


    • Amazon 发现加载时间每延长 1 秒一年就会减少 16 亿美元的营收



    如何优化


    想知道如何优化,首先我们需要先确定优化方向:



    • 提高成功率

    • 减少请求耗时

    • 减少网络带宽


    接下来我们了解下 https 网络连接流程,如下图:


    connection.png


    从上图我们能清晰的看出 https 连接经历了以下几个流程:



    • DNS Query: 1 个 RTT

    • TCP 需要经历 SYNSYN/ACKACK 三次握手1.5个RTT,不过 ACKClientHello 合并了: 1 个 RTT

    • TLS 需要经过握手和密钥交换: 2 个 RTT

    • Application Data 数据传输


    综上所述,一个 https 连接需要 4 个 RTT 才到数据传输阶段,如果我们能减少建连的 RTT 次数,或者降低数据传输量,会对网络的稳定和速度带来很大的提升。


    0x02 DNS 优化



    • DNS & HttpDNS



    DNS(Domain Name System,域名系统),DNS 用于用户在网络请求时,根据域名查出 IP 地址,使用户更方便的访问互联网。传统DNS面临DNS缓存、解析转发、DNS攻击等问题,具体的DNS流程如下图所示:


    local_dns.png


    HttpDNS 优先通过 HTTP 协议与自建 DNS 服务器交互,如果有问题再降级到运营商的 LocalDNS 方案,既有效防止域名劫持,提高域名解析效率又保证了稳定可靠,HttpDNS 的原理如下图所示:


    httpdns.png


    HttpDNS优势



    • 稳定性:绕过运营商 LocalDNS,避免域名劫持,解决了由于 Local DNS 缓存导致的变更域名后无法即时生效的问题

    • 精准调度:避免 LocalDNS 调度不准确问题,自己做解析,返回最优服务端 IP 地址

    • 缩短解析延迟:通过域名预解析、缓存 DNS 解析结果等方式实现缩短域名解析延迟的效果


    综述


    先来看看主要的两点收益



    • 防止劫持,不过由于客户端大都已经全栈 HTTPS 了,HTTP 时代的中间人攻击已经基本没有了,但是还是有收益的。

    • 解析延迟带来的速度提升,目前全栈 HTTP/2 后,大都已经是长连接,数据统计单域名下通过 DNS Query 占比 5%+,DNS 解析平均耗时 80ms 左右,如果平摊到全量的网络请求 HttpDNS 会带来 1% 左右的提升


    上面的收益没有提到精准调度,是因为我们的 APP 流量主要在国内,国内节点相对丰富,服务整体质量也较高,即使出现调度不准确的情况,差值也不会太大,但如果在国外情况可能会差很多。


    0x03 连接优化


    长连接


    长连接指在一个连接上可以连续发送多个数据包,做到连接复用 我们知道从 HTTP/1.1 开始就支持了长连接,但是 HTTP/2 中引入了多路复用的技术。 大家可以通过 该链接 直观感受下 HTTP/2HTTP/1.1 到底快了多少。


    http1_2.png


    这是 Akamai 公司建立的一个官方的演示,用以说明 HTTP/2 相比于之前的 HTTP/1.1 在性能上的大幅度提升。同时请求 379 张图片,从加载时间 的对比可以看出HTTP/2在速度上的优势。


    HTTP/2的多路复用技术代替了原来的序列和阻塞机制。 HTTP/1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有连接数量限制,有了二进制分帧之后,HTTP/2不再依赖 TCP 链接去实现多流并行了,所有请求都是通过一个 TCP 连接并发送完成,利用请求优先级解决关键请求阻塞问题,使得重要的请求优先得到响应。这样更容易实现全速传输,减少 TCP 慢启动时间,提高传输的速度。传输流程如下图:


    multiplexing.png


    说了这么多优点,那多路复用有缺点么,主要是受限TCP的限制,一般来说同一域名下只需要使用一个 TCP 连接。但当连接中出现频繁丢包情况,就会有队头阻塞问题,所有的包都会等待丢包重传成功后传输,这样HTTP/2的表现情况反倒不如 HTTP/1.1了,HTTP/1.1还可以通过多个 TCP 连接并行传输数据。


    域名合并


    随着开发规模逐渐扩大,各业务团队出于独立性和稳定性的考虑,纷纷申请了自己的接口域名,域名会越来越多。我们知道 HTTP 属于应用层协议,在传输层使用 TCP 协议,在网络层使用IP协议,所以 HTTP 的长连接本质上是 TCP 长连接,是对一个域名( IP )来说的,如果域名过多面临下面几个问题:



    • 长连接的复用效率降低

    • 每个域名都需要经过 DNS 服务来解析服务器 IP。

    • HTTP 请求需要跟不同服务器同时保持长连接,增加了客户端和服务端的资源消耗。


    具体方案如下图所示


    domain_merge.png


    TLS-v1.2 会话恢复


    会话恢复主要是通过减少 TLS 密钥协商交换的过程,在第二次建连时提高连接效率 2-RTT -> 1-RTT 。具体是如何实现的呢?包含两种方式,一种是 Sesssion ID,一种是 Session Ticket。下面讲解了 Session Ticket 的原理:


    img



    • Session ID

      Session ID 类似我们熟知的 Session 的概念。 由服务器端支持,协议中的标准字段,因此基本所有服务器都支持,客户端只缓存会话 ID,服务器端保存会话 ID 以及协商的通信信息,占用服务器资源较多。


    • Session Ticket

      Session ID 存在一些弊端,比如分布式系统中的 Session Cache 同步问题,如果不能同步则大大限制了会话复用效率,但 Session Ticket 没问题。Session Ticket 更像我们熟知的 Cookie 的概念,Session Ticket 用只有服务端知道的安全密钥加密过的会话信息,保存在客户端上。客户端在 ClientHello 时带上了 Session Ticket,服务器如果能成功解密就可以完成快速握手。



    不管是 Session ID 还是 Session Ticket 都存在时效性问题,不是永久有效的。


    TLS-v1.3


    首先需要明确的是,同等情况下,TLS/1.3TLS/1.2 少一个 RTT 时间。并且废除了一些不安全的算法,提升了安全性。首次握手,TLS/1.2完成 TLS 密钥协商需要 2 个 RTT 时间,TLS/1.3只需要 1 个 RTT 时间。会话恢复 TLS/1.2 需要 1 个 RTT 时间,TLS/1.3 则因为在第一个包中携带数据(early data),只需要 0 个 RTT,有点类似 TLS 层的 TCP Fast Open



    • 首次握手流程

      img


    • 会话恢复-0RTT

      TLS/1.3 中更改了会话恢复机制,废除了原有的 Session IDSession Ticket 的方式,使用 PSK 的机制,并支持了 0RTT模式下的恢复模式(实现 0-RTT 的条件比较苛刻,目前不少浏览器虽然支持 TLS/1.3 协议,但是还不支持发送 early data,所以它们也没法启用 0-RTT 模式的会话恢复)。当 client 和 server 共享一个 PSK(从外部获得或通过一个以前的握手获得)时,TLS/1.3 允许 client 在第一个发送出去的消息中携带数据("early data")。Client 使用这个 PSK 生成 client_early_traffic_secret 并用它加密 early data。Server 收到这个 ClientHello 之后,用 ClientHello 扩展中的 PSK 导出 client_early_traffic_secret 并用它解密 early data0-RTT 会话恢复模式如下:

      img



    HTTP/3


    QUIC 首次在2013年被提出,互联网工程任务组在 2018 年的时候将基于 QUIC 协议的 HTTP (HTTP over QUIC) 重命名为 HTTP/3。不过目前 HTTP/3 还没有最终定稿,最新我看已经到了第 34 版,应该很快就会发布正式版本。某些意义上说 HTTP/3 协议实际上就是IETF QUICQUICQuick UDP Internet Connections,快速 UDP 网络连接) 基于 UDP,利用 UDP 的速度与效率。同时 QUIC 也整合了 TCPTLSHTTP/2 的优点,并加以优化。用一张图可以清晰地表示他们之间的关系。


    HTTP/3主要带来了零 RTT 建立连接、连接迁移、队头阻塞/多路复用等诸多优势,具体细节暂不介绍了。推出HTTP/3(QUIC)的原理与实践,敬请期待。


    0x04 带宽优化


    HTTP/2 头部压缩


    HTTP1.xheader 中的字段很多时候都是重复的,例如 method:getscheme:https 等。随着请求增长,这些请求中的冗余标头字段不必要地消耗带宽,从而显著增加了延迟,因此,HTTP/2 头部压缩技术应时而生,使用的压缩算法为 HPACK。借用 Google 的性能专家 Ilya Grigorik 在 Velocity 2015 ? SC 会议中分享的一张图,让我们了解下头部压缩的原理:


    head_compress1.png


    上述图主要涉及两个点



    • 静态表中定义了61个 header 字段与 Index,可以通过传输 Index 进而获取 header,极大减少了报文大小。静态表中的字段和值固定,而且是只读的。详见静态表

    • 动态表接在静态表之后,结构与静态表相同,以先进先出的顺序维护的 header 字段列表,可随时更新。同一个连接上产生的请求和响应越多,动态字典积累得越全,头部压缩效果也就越好。所以尽量上面提到的域名合并,不仅提升连接性能也能提高带宽优化效果


    简单描述一下 HPACK 算法的过程:



    • 发送方和接受方共同维护一份静态表和动态表

    • 发送方根据字典表的内容,编码压缩消息头部

    • 接收方根据字典表进行解码,并且根据指令来判断是否需要更新动态表


    看完了 HTTP/2 头部压缩的介绍,那在没有头部压缩技术的 HTTP/1 时代,当处理一些通用参数时,我们当时只能把参数压缩后放入 body 传输,因为 header 只支持 ascii 码,压缩导致乱码,如果放入 header 还得 encode 或者 base64 编码,不仅增大了体积还要浪费解码的性能。


    数据表明通用参数通过 HTTP/2header 传输,由于长连通道大概在 90%+ 复用比例,压缩率可以达到惊人的 90%,同样比压缩后放在 body 中减少约 50% 的 size,如果遇到一些无规则的文本数据,zip 压缩率也会随着变低,这时候提升将会更明显。说了这么多,最后让我们通过抓包看下,HTTP/2 头部压缩的效果吧:


    head_compress2.png


    Json vs Protobuffer


    Protobuffer 是 Google 出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json 好,Size 比 Json 要小



    • Protobuffer 数据格式

      因为咱们这里说的是带宽,所以咱们详细说下 size 这块的优化,相比 json 来说,Protobuffer 节省了字段名字和分隔符,具体格式如下:

      // tag: 字段标识号,不可重复
      // type: 字段类型
      // length: 字段长度,当data可以用Varint表示的时候不需要 (可选)
      // data: 字段数据

      <tag> <type> [<length>] <data>

    • Protobuffer 数据编码方式

      Protobuffer 的数据格式不可避免的存在定长数据和变长数据的表示问题,编码方式用到了 Varint & Zigzag。 这里主要介绍下 Varint,因为 Zigzag 主要是对于负数时的一个补充( VarInt 不太适合表示负数,有兴趣的同学可以自行查下资料 ) ,Varint 其实是一种变长的编码方式,用字节表示数字,征用了每个字节的最高位 (MSB), MSB 是 1 话,则表示还有后序字节,一直到 MSB 为 0 的字节为止,具体表示如下表:

         0 ~ 2^07 - 1 0xxxxxxx
      2^07 ~ 2^14 - 2 1xxxxxxx 0xxxxxxx
      2^14 ~ 2^21 - 3 1xxxxxxx 1xxxxxxx 0xxxxxxx
      2^21 ~ 2^28 - 4 1xxxxxxx 1xxxxxxx 1xxxxxxx 0xxxxxxx

      不难看出值越小的数字,使用越少的字节数表示。如对于 int32 类型的数字,一般需要 4 个字节 表示,但是用了 Varint 小于 128 一个字节就够了,但是对于大的 int32 数字(大于2^28)会需要 5 个 字节 来表示,但大多数情况下,数据中不会有很大的数字,所以采用 Varint 方法总是可以用更少的字节数来表示数字


    • 具体示例

      首先定义一个实体类

      // bean
      class Person {
      int age = 1;
      }

      Json 的序列化结果

      // json
      {
      "age":1
      }

      Protobuffer 的序列化结果

      // Protobuffer
      00001000 00000001

      简单说下 Protobuffer 的序列化逻辑吧,Personage 字段取值为 1 的话,类型为 int32 则对应的编码是:0x08 0x01age 的类型是 int32,对应的 type 取 0。而它的 tag 又是 1,所以第一个字节是 (1 << 3) | 0 = 0x08,第二个字节是数字 1 的 VarInt 编码,即 0x01

      // bean
      class Person {
      int age = 1;
      }

      // Protobuffer 描述文件
      message Person {
      int32 age = 1;
      }

      // protobuf值
      +-----+---+--------+
      |00001|000|00000001|
      +-----+---+--------+
      tag type data

    • 优化数据

      原始数据 Size ProtobufferJson 小 30%左右,压缩后 Size 差距 10%+



    0x05 总结


    上面说了这么多技术的原理,接下来分享下我们的技术选型以及思考,希望能给大家带来帮助



    • HttpDNS

      我们的应用主要在国内使用,这个功能只对首次建连有提升,目前首次建连的比例较小,也就在5%左右,所以对性能提升较小;安全问题再全栈切了 https 后也没有那么突出;并且 HttpDNS 也会遇到一些技术难点,比如自建 DNS 需要维护一套聪明的服务端IP调度策略;客户端需要注意 IP 缓存 以及 IPV4 IPV6 的双栈探测和选取策略等,综合考虑下目前我们没有使用HttpDNS


    • 长连接&域名合并

      长连接和域名合并,这两个放在一起说下吧,因为他们是相辅相成的,域名合并后,不同业务线使用一个长连接通道,减少了TCP 慢启动时间,更容易实现全速传输,优势还是很明显的。

      长连接方案还是有很多的,涉及到 HTTP/1.1HTTP/2HTTP/3、自建长链等方式,最终我们选择了HTTP/2,并不是因为这个方案性能最优,而是综合来看这个方案最有优势,HTTP/1.1 就不用说了,这个方案早就淘汰了,而 HTTP/3 虽然性能好,但是目前阶段并没有完整稳定的前后端框架,所以只适合尝鲜,是我们接下来的一个目标,自建长链虽然能结合业务定制逻辑,达到最优的性能,但是比较复杂,也正是由于特殊的定制导致没办法方便的切换官方的协议(如HTTP/3


    • TLS 协议

      TLS 协议我们积极跟进了官方最新稳定版 TLS/1.3 版本的升级,客户端在 Android 10iOS 12.2 后已经开始默认支持 TLS/1.3,想在低系统版本上支持需要接入三方扩展库,由于支持的系统版本占比已经很高,后面也会越来越高,并且 TLS 协议是兼容式的升级,允许客户端同时存在TLS/1.2TLS/1.3两个版本,综合考虑下我们的升级策略就是只升级服务端

      TLS 协议升级主要带来两个方面的提升,性能和安全。先来说下性能的提升,TLS/1.3 对比TLS/1.2版本来说,性能的提升体现在减少握手时的一次 RTT 时间,由于连接复用率很高,所以性能的提升和 HttpDNS 一样效果很小。至于安全前面也有提到,废弃了一些相对不安全的算法,提升了安全性


    • 带宽优化

      带宽优化对于网络传输的速度和用户的流量消耗都是有帮助的,所以我们要尽量减少数据的传输,那么在框架层来说主要的策略有两种:

      一是减少相同数据的传输次数,也就是对应着 HTTP/2 的头部压缩,原理上面也有介绍,这里就不在赘述了,对于目前长连接的通道,header 中不变化的数据只传输一个标识即可,这个的效果还是很明显的,所以框架可以考虑一些通用并且不长变化的参数放在 Header 中传输,但是此处需要注意并不是所有的参数都适合在 Header 中,因为 Header 中的数据只支持 ASCII 编码,所以有一些敏感数据放在此处会容易被破解,如果加密后在放进来,还需要在进行 Base64 编码处理,这样可能会加大很多传输的 size,所以框架侧要进行取舍 (PS:在补充个 Header 字段的小坑:HTTP/2 Header都会转成小写,而 HTTP/1.x 大小写均可,在升级 HTTP/2 协议的时候需要注意下)

      二是减少传输 size,常规的做法如切换更省空间的数据格式 Protobuffer,因为关联到数据格式的变动,需要前后端一起调整,所以改造历史业务成本会比较高有阻力,比较适合在新的业务上尝鲜,总结出一套最佳实践后,再去尝试慢慢改造旧的业务


    收起阅读 »

    SpringBoot实战基于异常日志的邮件报警

    SpringBoot实战基于异常日志的邮件报警 相信所有奋斗在一线的小伙伴,会很关心自己的系统的运行情况,一般来说,基础设施齐全一点的公司都会有完善的报警方案,那么如果我们是一个小公司呢,不能因为基础设施没有,就失去对象的感知能力吧;如果我们的系统大量异...
    继续阅读 »





    SpringBoot实战基于异常日志的邮件报警



    相信所有奋斗在一线的小伙伴,会很关心自己的系统的运行情况,一般来说,基础设施齐全一点的公司都会有完善的报警方案,那么如果我们是一个小公司呢,不能因为基础设施没有,就失去对象的感知能力吧;如果我们的系统大量异常却不能实时的触达给我们,那么也就只会有一个结果--杀个程序猿祭天


    本文简单的介绍一种实现思路,基于error日志来实现邮件的报警方案


    I. 项目环境


    1. 项目依赖


    本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发


    开一个web服务用于测试


    <dependencies>
    <!-- 邮件发送的核心依赖 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    </dependencies>

    2. 配置


    邮件相关配置如下,注意使用自己的用户名 + 授权码填充下面缺失的配置


    spring:
    #邮箱配置
    mail:
    host: smtp.163.com
    from: xhhuiblog@163.com
    # 使用自己的发送方用户名 + 授权码填充
    username:
    password:
    default-encoding: UTF-8
    properties:
    mail:
    smtp:
    auth: true
    starttls:
    enable: true
    required: true

    II. 异常日志的邮件预警


    1. 设计思路


    接下来这个方案的主要出发点在于,当程序出现大量的异常,表明应用多半出现了问题,需要立马发送给项目owner


    要实现这个方案,关键点就在于异常出现的感知与上报



    • 异常的捕获,并输出日志(这个感觉属于标配了吧,别告诉我现在还有应用不输出日志文件的...)

      • 对于这个感知,借助logback的扩展机制,可以实现,后面介绍


    • 异常上报:邮件发送


    关于email的使用姿势,推荐参考博文 SpringBoot 系列之邮件发送姿势介绍


    2. 自定义appender


    定义一个用于错误发送的Appender,如下


    public class MailUtil extends AppenderBase<ILoggingEvent> {

    public static void sendMail(String title, String context) {
    SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
    //邮件发送人
    simpleMailMessage.setFrom(ContextUtil.getEnvironment().getProperty("spring.mail.from", "bangzewu@126.com"));
    //邮件接收人,可以是多个
    simpleMailMessage.setTo("bangzewu@126.com");
    //邮件主题
    simpleMailMessage.setSubject(title);
    //邮件内容
    simpleMailMessage.setText(context);

    JavaMailSender javaMailSender = ContextUtil.getApplicationContext().getBean(JavaMailSender.class);
    javaMailSender.send(simpleMailMessage);
    }

    private static final long INTERVAL = 10 * 1000 * 60;
    private long lastAlarmTime = 0;

    @Override
    protected void append(ILoggingEvent iLoggingEvent) {
    if (canAlarm()) {
    sendMail(iLoggingEvent.getLoggerName(), iLoggingEvent.getFormattedMessage());
    }
    }

    private boolean canAlarm() {
    // 做一个简单的频率过滤
    long now = System.currentTimeMillis();
    if (now - lastAlarmTime >= INTERVAL) {
    lastAlarmTime = now;
    return true;
    } else {
    return false;
    }
    }
    }

    3. Spring容器


    上面的邮件发送中,需要使用JavaMailSender,写一个简单的SpringContext工具类,用于获取Bean/Propertiy


    @Component
    public class ContextUtil implements ApplicationContextAware, EnvironmentAware {

    private static ApplicationContext applicationContext;

    private static Environment environment;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    ContextUtil.applicationContext = applicationContext;
    }

    @Override
    public void setEnvironment(Environment environment) {
    ContextUtil.environment = environment;
    }

    public static ApplicationContext getApplicationContext() {
    return applicationContext;
    }

    public static Environment getEnvironment() {
    return environment;
    }
    }

    4. logback配置


    接下来就是在日志配置中,使用我们上面定义的Appender


    logback-spring.xml文件内容如下:


    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <pattern>%d [%t] %-5level %logger{36}.%M\(%file:%line\) - %msg%n</pattern>
    <!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
    <charset>UTF-8</charset>
    </encoder>
    </appender>

    <appender name="errorAlarm" class="com.git.hui.demo.mail.util.MailUtil">
    <!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>ERROR</level>
    </filter>
    </appender>


    <!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
    <!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
    <!-- additivity=false 表示匹配之后,不再继续传递给其他的logger-->
    <logger name="com.git.hui" level="DEBUG" additivity="false">
    <appender-ref ref="STDOUT"/>
    <appender-ref ref="errorAlarm"/>
    </logger>

    <!-- 控制台输出日志级别 -->
    <root level="INFO">
    <appender-ref ref="STDOUT"/>
    </root>
    </configuration>

    5. 测试demo


    接下来演示一下,是否可以达到我们的预期


    @Slf4j
    @RestController
    @SpringBootApplication
    public class Application {

    public static void main(String[] args) {
    SpringApplication.run(Application.class);
    }

    @GetMapping("div")
    public String div(int a, int b) {
    try {
    return String.valueOf(a / b);
    } catch (Exception e) {
    log.error("div error! {}/{}", a, b, e);
    return "some error!";
    }
    }
    }


    5.小结


    本篇博文主要提供了一个思路,借助logback的扩展机制,来实现错误日志与预警邮件绑定,实现一个简单的应用异常监控


    上面这个实现只算是一个雏形,算是抛砖引玉,有更多可以丰富的细节,比如



    • 飞书/钉钉通知(借助飞书钉钉的机器来报警,相比较于邮件感知性更高)

    • 根据异常类型,做预警的区分

    • 更高级的频率限制等


    在这里推荐一个我之前开源的预警系统,可以实现灵活预警方案配置,频率限制,重要性升级等



    III. 不能错过的源码和相关知识点


    0. 项目


    收起阅读 »

    Kotlin infix 关键字与高阶函数的应用[第一行代码 Kotlin 学习笔记]

    使用 infix 函数构建更可读的语法 在前面的 Kotlin 学习笔记中,我们已经多次使用过 A to B 这样的语法结构构建键值对,包括 Kotlin 自带的 mapOf() 函数。 这种语法结构的优点是可读性高,相比于调用一个函数,它更接近于使用英语...
    继续阅读 »

    使用 infix 函数构建更可读的语法


    在前面的 Kotlin 学习笔记中,我们已经多次使用过 A to B 这样的语法结构构建键值对,包括 Kotlin 自带的 mapOf() 函数。


    这种语法结构的优点是可读性高,相比于调用一个函数,它更接近于使用英语的语法来编写程序。可能你会好奇,这种功能是怎么实现的呢?to 是不是 Kotlin 语言中的一个关键字?本节我们就对这个功能进行深度解密。


    首先,to 并不是 Kotlin 语言中的一个关键字,之所以我们能够使用 A to B 这样的语法结构,是因为 Kotlin 提供了一种高级语法糖特性:infix 函数。当然,infix 函数也并不是什么难理解的事物,它只是把编程语言函数调用的语法规则调整了一下而已,比如 A to B 这样的写法,实际上等价于 A.to(B) 的写法。


    下面我们就通过两个具体的例子来学习一下 infix 函数的用法,先从简单的例子看起。


    String 类中有一个 startsWith() 函数,你一定使用过,它可以用于判断一个字符串是否是以某个指定参数开头的。比如说下面这段代码的判断结果一定会是 true:


    if ("Hello Kotlin".startsWith("Hello")) {
    // 处理具体的逻辑
    }

    startsWith() 函数的用法虽然非常简单,但是借助 infix 函数,我们可以使用一种更具可读性的语法来表达这段代码。新建一个 infix.kt 文件,然后编写如下代码:


    infix fun String.beginsWith(prefix: String) = startsWith(prefix)

    首先,除去最前面的 infix 关键字不谈,这是一个 String 类的扩展函数。我们给 String 类添加了一个 beginsWith() 函数,它也是用于判断一个字符串是否是以某个指定参数开头的,并且它的内部实现就是调用的 String 类的 startsWith() 函数。


    但是加上了 infix 关键字之后,beginsWith() 函数就变成了一个 infix 函数,这样除了传统的函数调用方式之外,我们还可以用一种特殊的语法糖格式调用 beginsWith() 函数,如下所示:


    if ("Hello World" beginsWith "Hello") {
    // 处理具体的逻辑
    }

    从这个例子就能看出,infix 函数的语法规则并不复杂,上述代码其实就是调用的 " HelloKotlin " 这个字符串的 beginsWith() 函数,并传入了一个 "Hello" 字符串作为参数。但是 infix 函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。


    另外,infix 函数由于其语法糖格式的特殊性,有两个比较严格的限制:首先,infix 函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;其次,infix 函数必须接收且只能接收一个参数,至于参数类型是没有限制的。只有同时满足这两点, infix 函数的语法糖才具备使用的条件,你可以思考一下是不是这个道理。


    看完了简单的例子,接下来我们再看一个复杂一些的例子。比如这里有一个集合,如果想要判断集合中是否包括某个指定元素,一般可以这样写:


    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
    if (list.contains("Banana")) {
    // 处理具体的逻辑
    }

    很简单对吗?但我们仍然可以借助 infix 函数让这段代码变得更加具有可读性。在 infix.kt 文件中添加如下代码:


    infix fun <T> Collections<T>.has(element: T) = contains(element)

    可以看到,我们给 Collection 接口添加了一个扩展函数,这是因为 Collection 是 Java 以及 Kotlin 所有集合的总接口,因此给 Collection 添加一个 has() 函数,那么所有集合的子类就都可以使用这个函数了。


    另外,这里还使用了泛型函数的定义方法,从而使得 has() 函数可以接收任意具体类型的参数。而这个函数内部的实现逻辑就相当简单了,只是调用了 Collection 接口中的 contains() 函数而已。也就是说,has() 函数和 contains() 函数的功能实际上是一模一样的,只是它多了一个 infix 关键字,从而拥有了 infix 函数的语法糖功能。


    现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:


    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
    if (list has "Banana") {
    // 处理具体的逻辑
    }

    好了,两个例子都已经看完了,你对于 infix 函数应该也了解得差不多了。但是或许现在你的心中还有一个疑惑没有解开,就是 mapOf() 函数中允许我们使用 A to B 这样的语法来构建键值对,它的具体实现是怎样的呢?为了解开谜团,我们直接来看一看 to() 函数的源码吧,如下所示:


    public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

    可以看到,这里使用定义泛型函数的方式将 to() 函数定义到了 A 类型下,并且接收一个 B 类型的参数。因此 A 和 B 可以是两种不同类型的泛型,也就使得我们可以构建出字符串 to 整型这样的键值对。


    再来看 to() 函数的具体实现,非常简单,就是创建并返回了一个 Pair 对象。也就是说,A to B 这样的语法结构实际上得到的是一个包含 A、B 数据的 Pair 对象,而 mapOf() 函数实际上接收的正是一个 Pair 类型的可变参数列表,这样我们就将这种神奇的语法结构完全解密了。


    本着动手实践的精神,其实我们也可以模仿 to() 函数的源码来编写一个自己的键值对构建函数。在 infix.kt 文件中添加如下代码:


    infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)

    这里只是将 to() 函数改名成了 with() 函数,其他实现逻辑是相同的,因此相信没有什么解释的必要。现在我们的项目中就可以使用 with() 函数来构建键值对了,还可以将构建的键值对传入 mapOf() 方法中:


    val map = mapOf("Apple" with 1, "Banana" with 2, "Orange" with 3, "Pear" with 4, "Grape" with 5)

    是不是很神奇?这就是 infix 函数给我们带来的诸多有意思的功能,灵活运用它确实可以让语法变得更具可读性。


    高阶函数的应用


    高阶函数非常适用于简化各种 API 的调用,一些 API 的原有用法在使用高阶函数简化之后,不管是在易用性还是可读性方面,都可能会有很大的提升。


    为了进行举例说明,我们在本节会使用高阶函数简化 SharedPreferences 和 ContentValues 这两种 API 的用法,让它们的使用变得更加简单。


    简化 SharedPreferences 的用法


    首先来看 SharedPreferences,在开始对它进行简化之前,我们先回顾一下 SharedPreferences 原来的用法。向 SharedPreferences 中存储数据的过程大致可以分为以下 3 步:



    1. 调用 SharedPreferences 的 edit() 方法获取 SharedPreferences.Editor 对象;

    2. 向 SharedPreferences.Editor 对象中添加数据;

    3. 调用 apply() 方法将添加的数据提交,完成数据存储操作。


    对应的代码示例如下:


    val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
    editor.putString("name", "Tom")
    editor.putInt("age", 28)
    editor.putBoolean("married", false)
    editor.apply()

    当然,这段代码其实本身已经足够简单了,但是这种写法更多还是在用 Java 的编程思维来编写代码,而在 Kotlin 当中我们明显可以做到更好。


    接下来我们就尝试使用高阶函数简化 SharedPreferences 的用法,新建一个 SharedPreferences.kt 文件,然后在里面加入如下代码:


    fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
    val editor = edit()
    editor.block()
    editor.apply()
    }

    这段代码虽然不长,但是涵盖了高阶函数的各种精华,下面我来解释一下。


    首先,我们通过扩展函数的方式向 SharedPreferences 类中添加了一个 open 函数,并且它还接收一个函数类型的参数,因此 open 函数自然就是一个高阶函数了。


    由于 open 函数内拥有 SharedPreferences 的上下文,因此这里可以直接调用 edit() 方法来获取 SharedPreferences.Editor 对象。另外 open 函数接收的是一个 SharedPreferences.Editor 的函数类型参数,因此这里需要调用 editor.block() 对函数类型参数进行调用,我们就可以在函数类型参数的具体实现中添加数据了。最后还要调用 editor.apply() 方法来提交数据,从而完成数据存储操作。


    定义好了 open 函数之后,我们以后在项目中使用 SharedPreferences 存储数据就会更加方便了,写法如下所示:


    getSharedPreferences("data", Context.MODE_PRIVATE).open {
    putString("name", "Tom")
    putInt("age", 28)
    putBoolean("married", false)
    }

    可以看到,我们可以直接在 SharedPreferences 对象上调用 open 函数,然后在 Lambda 表达式中完成数据的添加操作。注意,现在 Lambda 表达式拥有的是 SharedPreferences.Editor 的上下文环境,因此这里可以直接调用相应的 put 方法来添加数据。最后我们也不再需要调用 apply() 方法来提交数据了,因为 open 函数会自动完成提交操作。


    怎么样,使用高阶函数简化之后,不管是在易用性还是在可读性上,SharedPreferences 的用法是不是都简化了很多?这就是高阶函数的魅力所在。好好掌握这个知识点,以后在诸多其他 API 的使用方面,我们都可以使用这个技巧,让API变得更加简单。


    当然,最后不得不提的是,其实 Google 提供的 KTX 扩展库中已经包含了上述 SharedPreferences 的简化用法,这个扩展库会在 Android Studio 创建项目的时候自动引入 build.gradle 的 dependencies 中。


    因此,我们实际上可以直接在项目中使用如下写法来向 SharedPreferences 存储数据:


    getSharedPreferences("data", Context.MODE_PRIVATE).edit {
    putString("name", "Tom")
    putInt("age", 28)
    putBoolean("married", false)
    }

    可以看到,其实就是将 open 函数换成了 edit 函数,但是 edit 函数的语义性明显要更好一些。当然,我前面命名成 open 函数,主要是为了防止和 KTX 的 edit 函数同名,以免你在理解的时候产生混淆。


    那么你可能会问了,既然 Google 的 KTX 库中已经自带了一个 edit 函数,我们为什么还编写这个 open 函数呢?这是因为我希望你对于高阶函数的理解不要仅仅停留在使用的层面,而是要知其然也知其所以然。KTX 中提供的功能必然是有限的,但是掌握了它们背后的实现原理,你将可以对无限的 API 进行更多的扩展。


    简化 ContentValues 的用法


    接下来我们开始学习如何简化 ContentValues 的用法。


    ContentValues 的基本用法在 7.4 节中已经学过了,它主要用于结合 SQLiteDatabase 的 API 存储和修改数据库中的数据,具体的用法示例如下:


    val values = ContentValues()
    values.put("name", "Game of Thrones")
    values.put("author", "George Martin")
    values.put("pages", 720)
    values.put("price", 20.85)
    db.insert("Book", null, values)

    你可能会说,这段代码可以使用 apply 函数进行简化。这当然没有错,只是我们其实还可以做到更好。


    不过在正式开始我们的简化之旅之前,我还得向你介绍一个额外的知识点。还记得在 2.6.1 小节中学过的 mapOf() 函数的用法吗?它允许我们使用 "Apple" to 1 这样的语法结构快速创建一个键值对。这里我先为你进行部分解密,在 Kotlin 中使用 A to B 这样的语法结构会创建一个 Pair 对象,暂时你只需要知道这些就可以了,至于为什么,我们将在第 9 章的 Kotlin 课堂中学习。


    有了这个知识前提之后,就可以进行下一步了。新建一个 ContentValues.kt 文件,然后在里面定义一个 cvOf() 方法,如下所示:


    fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {

    }

    这个方法的作用是构建一个 ContentValues 对象,有几点我需要解释一下。首先,cvOf() 方法接收了一个 Pair 参数,也就是使用 A to B 语法结构创建出来的参数类型,但是我们在参数前面加上了一个 vararg 关键字,这是什么意思呢?其实 vararg 对应的就是 Java 中的可变参数列表,我们允许向这个方法传入 0 个、1 个、2 个甚至任意多个 Pair 类型的参数,这些参数都会被赋值到使用 vararg 声明的这一个变量上面,然后使用 for-in 循环可以将传入的所有参数遍历出来。


    再来看声明的 Pair 类型。由于 Pair 是一种键值对的数据结构,因此需要通过泛型来指定它的键和值分别对应什么类型的数据。值得庆幸的是,ContentValues 的所有键都是字符串类型的,这里可以直接将 Pair 键的泛型指定成 String。但 ContentValues 的值却可以有多种类型(字符串型、整型、浮点型,甚至是 null),所以我们需要将 Pair 值的泛型指定成 Any?。这是因为 Any 是 Kotlin 中所有类的共同基类,相当于 Java 中的 Object,而 Any? 则表示允许传入空值。


    接下来我们开始为 cvOf() 方法实现功能逻辑,核心思路就是先创建一个 ContentValues 对象,然后遍历 pairs 参数列表,取出其中的数据并填入 ContentValues 中,最终将 ContentValues 对象返回即可。思路并不复杂,但是存在一个问题:Pair 参数的值是 Any? 类型的,我们怎样让它和 ContentValues 所支持的数据类型对应起来呢?这个确实没有什么好的办法,只能使用 when 语句一一进行条件判断,并覆盖 ContentValues 所支持的所有数据类型。结合下面的代码来理解应该更加清楚一些:


    fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
    val cv = ContentValues()
    for (pair in pairs) {
    val key = pair.first
    val value = pair.second
    when (value) {
    is Int -> cv.put(key, value)
    is Long -> cv.put(key, value)
    is Short -> cv.put(key, value)
    is Float -> cv.put(key, value)
    is Double -> cv.put(key, value)
    is Boolean -> cv.put(key, value)
    is String -> cv.put(key, value)
    is Byte -> cv.put(key, value)
    is ByteArray -> cv.put(key, value)
    null -> cv.putNull(key)
    }
    }
    return cv
    }

    可以看到,上述代码基本就是按照刚才所说的思路进行实现的。我们使用 for-in 循环遍历了 pairs 参数列表,在循环中取出了 key 和 value,并使用 when 语句来判断 value 的类型。注意,这里将 ContentValues 所支持的所有数据类型全部覆盖了进去,然后将参数中传入的键值对逐个添加到 ContentValues 中,最终将 ContentValues 返回。


    另外,这里还使用了 Kotlin 中的 Smart Cast 功能。比如 when 语句进入 Int 条件分支后,这个条件下面的 value 会被自动转换成 Int 类型,而不再是 Any? 类型,这样我们就不需要像 Java 中那样再额外进行一次向下转型了,这个功能在 if 语句中也同样适用。


    有了这个 cvOf() 方法之后,我们使用 ContentValues 时就会变得更加简单了,比如向数据库中插入一条数据就可以这样写:


    val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
    db.insert("Book", null, values)

    怎么样?现在我们可以使用类似于 mapOf() 函数的语法结构来构建 ContentValues 对象,有没有觉得很神奇?


    当然,虽然 cvOf() 方法已经非常好用了,但是它和高阶函数却一点关系也没有。因为 cvOf() 方法接收的参数是 Pair 类型的可变参数列表,返回值是 ContentValues 对象,完全没有用到函数类型,这和高阶函数的定义不符。


    从功能性方面,cvOf() 方法好像确实用不到高阶函数的知识,但是从代码实现方面,却可以结合高阶函数来进行进一步的优化。比如借助 apply 函数,cvOf() 方法的实现将会变得更加优雅:


    fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
    for (pair in pairs) {
    val key = pair.first
    when (val value = pair.second) {
    is Int -> put(key, value)
    is Long -> put(key, value)
    is Short -> put(key, value)
    is Float -> put(key, value)
    is Double -> put(key, value)
    is Boolean -> put(key, value)
    is String -> put(key, value)
    is Byte -> put(key, value)
    is ByteArray -> put(key, value)
    null -> putNull(key)
    }
    }
    }

    由于 apply 函数的返回值就是它的调用对象本身,因此这里我们可以使用单行代码函数的语法糖,用等号替代返回值的声明。另外,apply 函数的 Lambda 表达式中会自动拥有 ContentValues 的上下文,所以这里可以直接调用 ContentValues 的各种 put 方法。借助高阶函数之后,你有没有觉得代码变得更加优雅一些了呢?


    当然,虽然我们编写了一个非常好用的 cvOf() 方法,但是或许你已经猜到了,KTX 库中也提供了一个具有同样功能的 contentValuesOf() 方法,用法如下所示:


    val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
    db.insert("Book", null, values)

    平时我们在编写代码的时候,直接使用 KTX 提供的 contentValuesOf() 方法就可以了,但是通过本小节的学习,你不仅掌握了它的用法,还明白了它的源码实现,有没有觉得收获了更多呢?


    收起阅读 »

    AFNetWorking为何在发起请求时要通过runloop!OC 中常用关键字的区别!

    最近几天经历了多场面试,由于简历上写了runloop,跟AFNetworing的字眼。面试官好像特别喜欢问这个问题。一连几场都遇到。可惜平时开发过程中,知识的累计跟沉淀不足。都不能回答的很好..趁着现在有时间。查阅一下资料 在这里进行一个总结。。Questio...
    继续阅读 »

    最近几天经历了多场面试,由于简历上写了runloop,跟AFNetworing的字眼。面试官好像特别喜欢问这个问题。一连几场都遇到。

    可惜平时开发过程中,知识的累计跟沉淀不足。都不能回答的很好..


    趁着现在有时间。查阅一下资料 在这里进行一个总结。。

    Question:AFNetworking 2.x怎么开启常驻子线程?为何需要常驻子线程?对应以下代码:


    + (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
    [[NSThread currentThread] setName:@"AFNetworking"];

    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
    }
    }

    + (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
    _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
    [_networkRequestThread start];
    });

    return _networkRequestThread;
    }

    首先,我们要了解为何要开启常驻子线程?

    NSURLConnection的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到NSURLConnection的回调,就必须让子线程至少存活到回调的时机。而AF让线程常驻的原因是,当发起多个http请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来。

    上面说的一般情况,子线程执行完任务就会退出,那么什么情况下,子线程能够继续存活呢?这就涉及到第二个问题了,AF是如何开启常驻线程的,这里实际上考察的是runloop的基础知识。

    这里简单来说,当runloop发现还有source/timer/observer的时候,runloop就不会退出。所以AF这里就通过给当前runloop添加一个NSMachPort,这个port实际上相对于添加了一个source事件源,这样子线程的runloop就会一直处于循环状态,等待别的线程向这个port发送消息,而实际上AF这里是没有消息发送到这个port的。


    OC 中 strong, weak, assign, copy 的区别

    strong

    强引用,只可以修饰对象,属性的默认修饰符,其修饰的对象引用计数增加1

    weak

    弱引用,只可以修饰对象,指向但不拥有对象,其修饰的对象引用计数不增加,可以避免循环引用,weak修饰的对象释放后,指针会被系统置为nil,此时向对象发送消息不会奔溃

    assign

    可以修饰对象和基本数据类型,如果修饰对象,其修饰的对象引用计数不增加,可以避免循环引用,但assign修饰的对象释放后,指针不会被系统置为nil,这会产生野指针的问题,此时向对象发送消息会奔溃。所以assign通常用于基本数据类型,如int ,float, NSInteger, CGFloat ,这是因为基本数据类型放在栈区,先进先出,基本数据类型出栈后,assign修饰的变量就不存在了,不用担心指针的问题。

    copy

    引用,修饰不可变的对象,比如NSString, NSArray, NSDictionary。copy和strong类似,不同之处在于,copy修饰的对象会先在内存中拷贝一个新的对象,copy会指向那个新的对象的内存地址,这样避免了多个指针指向同一对象,而导致的其中一个指针改变了对象,其他指针指向的对象跟着改变,举个例子:

    @property(strong) NSString *name1;
    @property(copy) NSString *name2;

    NSMutableString *name3 = [NSMutableString stringWithString:@"Danny"];
    self.name1 = name3;
    self.name2 = name3;
    [name3 setString:@"Liming"];
    NSLog(@"%@", self.name1); // Liming
    NSLog(@"%@", self.name2); // Danny

    我们可以看到使用strong的属性name1会跟着name3变,因为他们都指向同一个NSMutableString的对象,而name2预先拷贝了name1,从而避免了和name1一起变化。

    copy的原则就是,把一个对象赋值给一个属性变量,当这个对象变化了,如果希望属性变量变化就使用strong属性,如果希望属性变量不跟着变化,就是用copy属性。






    收起阅读 »

    iOS一些容易被忽略的基础面试题

    什么是对象 ,OC中的对象有哪些?对象是类的实例;是通过一个类创建出来的实例,一般称之为实例对象;OC中的常见对象有实例对象、类对象、元类对象;什么是类?什么是元类?类对象和类,元类对象和元类有什么区别?类: 是面向对象程序设计(OOP,Object-Orie...
    继续阅读 »




    什么是对象 ,OC中的对象有哪些?

    对象是类的实例;是通过一个类创建出来的实例,一般称之为实例对象;OC中的常见对象有实例对象、类对象、元类对象;

    什么是类?什么是元类?类对象和类,元类对象和元类有什么区别?

    类: 是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象
    元类:以类作为其实例的类;
    类对象:类本身也是一个对象,所以就有类对象;类对象可以通过实例对象的ISA指针获得
    元类对象:元类本身也是一个对象,所以就有元类对象;元类对象可以通过类对象的ISA指针获得
    区分二者:

    • 类、元类是面向对象编程中的一种类型
    • 类对象、元类对象是一种对象

    什么是分类?

    分类也是一个类,其底层结构和类稍有不同;给分类添加的方法会在运行时合并到原有类的方法列表(二维数组)中
    分类多用来给类做扩展使用;在OC开发中应用广泛





    什么是类扩展?
    类扩展:用来给类扩充私有属性、方法

    什么是数组?
    数组可表示为占用一块连续的内存空间用来存储元素的数据结构;OC中的数组有可变和不可变两种;可变数组做了优化利用环形缓冲区技术提高增删改查时的性能

    什么是字典?
    字典以键值的形式存储数据,底层实现是哈希表;OC对象作为字典的Key需要遵守NSCopying协议并且实现hash和isEqual两个方法。比如:NSNumber、NSArray 、NSDictionary、自定义OC对象 都可以作为key

    什么是集合?
    集合是一种用来存储数据的数据结构,内部存储的数据时无序的,其他和数组相同

    OC语法有哪些?
    OC中的语法有点语法.,这里的点一般转化为setter、getter方法调用

    什么是Method?
    Method是method_t的结构体,是对一个方法的描述:

    struct method_t{
    SEL name; //函数名/方法名
    const char *types;//编码(返回值类型、参数类型)
    IMP imp; //指向函数的指针(函数地址)
    }



    什么是内敛函数?

    • 内联函数基本概念
      在c++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。

    在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。

    inline void func(int a);

    以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:

    inline int func(int a){return ++;}

    注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。

    这些事预处理器无法完成的。
    内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。

    • 类内部的内联函数

      为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。

    什么是构造函数?
    在一个类中定义一个和类名相同的函数,这个函数就是构造函数

    面向对象的设计原则是什么 ?
    单一责任原则开闭原则接口隔离原则依赖倒置原则里式替换原则迪米特原则

    • 单一责任原则
      一个类只负责一件事情,CALayer只负责动画和视图的显示,UIView只负责事件的传递、事件的响应
    • 开闭原则
      对修改关闭,对扩展开放; 要考虑API的后续扩展,而不是在原有基础上来回修改
    • 接口隔离原则
      使用多协议的方式来定义接口,而不是一个臃肿的协议;比如delagate, datesource
    • 依赖倒置原则
      抽象不应该依赖具体实现,具体实现依赖于抽象
    • 里式替换原则
      父类和子类无缝衔接,且原有功能不受影响;比如:KVO, 用完就走不留痕迹
    • 迪米特原则
      高内聚,低耦合

    面向对象语言的三大特性是什么 ?
    封装、继承、多态

    OC的继承体系





    关键字的使用标准?
    ARC环境修饰OC对象用strong、copy、weak,修饰基本数据类型用assign;
    静态变量用static, 修饰为常量用const

    常用设计模式?

    设计模式分为四类:结构型模式、创建型模式、行为型模式、软件设计原则

    常用的结构型模式有:代理、装饰
    常用的创建型模式有:单利、工厂
    常用的行为型模式有:观察者、发布订阅模式

    代理:是一种消息传递方式,一个完整的代理模式包括:委托对象、代理对象和协议。

    • 请代理三部曲:
      1 定义代理协议
      2 声明delegate对象
      3 调用代理方法
    • 当别人代理的三部曲
      1 遵循协议
      2 设置代理
      3 实现方法

    装饰:动态地给一个类添加一些额外的职责;Category 就是实现了装饰的设计模式;Category是一个特殊的类,通过它可以给类添加方法的接口与实现;
    观察者:包含通知和KVO
    单利:单:唯一,例:实例;即唯一的一个实例,该实例自创建开始到程序退出由系统自动释放;单利常被当做共有类使用;

    系统常见单利类
    UIApplication(应用程序实例类)
    NSNotificationCenter(消息中心类)
    NSFileManager(文件管理类)
    NSUserDefaults(应用程序设置)
    NSURLCache(请求缓存类)
    NSHTTPCookieStorage(应用程序cookies池)


    工厂模式:分为简单工厂模式、工厂模式、抽象工厂模式
    简单工厂模式:简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。
    工厂模式:抽象了工厂接口的具体产品,应用程序的调用不同工厂创建不同产品对象。(抽象产品)
    抽象工厂模式:在工厂模式的基础上抽象了工厂,应用程序调用抽象的工厂发发创建不同产品对象。(抽象产品+抽象工厂)

    懒加载:把初始化逻辑通过重写的方式封装起来,到需要时直接调用的方式
    懒加载的优点

    • 相对来说,如果代码量不是很多,可读性略强
    • 相对来说,防止为nil,减少了后续使用时安全检查的后顾之忧
    • 使用适当,可节省内存资源
    • 一定程度上,节省了某一个期间内的时间
    • 使用得当,优化性能,提高用户体验
      懒加载的缺点
    • 使用太泛滥,导致可读性变差
    • 使用不得当,可能会造成死循环,导致crash
    • 代码量增多(每增加一个懒加载,代码会平均多出3-4行)

    什么时候使用懒加载?

    一般情况下,不需要使用懒加载,懒加载未必能增强可读性、独立性,滥用反而让可读性适得其反。简言之,就是在逻辑上,觉得现在不需要加载,而在后面某一时间段内可能会加载,就可以考虑懒加载

    生产者消费者:
    在编码中,有时会遇到一个模块产生数据,另外一个模块处理数据的情况,不论是为了模块间的结偶或是并发处理还是忙闲不均,我们都会在产生和处理数据的模块之间放置缓存区,作为生产和处理数据的仓库。以上的模型就是生产者消费者模型
    生产者-消费者

    中介者:

    • 中介者模式又叫做调停者模式,其实就是中间人或者调停者的意思
    • 概念:中介者模式(Mediator),用一个中介者对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可 以独立地改变他们之间的交互
    • UINavigationViewController就是属于一个中介者
    • 中介者模式的优缺点
      中介者模式很容易在系统中应用,也很容易在系统中误用。当系统出现了多对多交互复杂的对象群时,不要急于使用中介者模式,而要先反思你在系统上设计是否合理。
      优点就是集中控制,减少了对象之间的耦合度。缺点就是太过于集中
    • 应用场景
      对象间的交互虽定义明确然而非常复杂,导致一组对象彼此相互依赖而且难以理解。
      因为对象引用了许多其他对象并与其通信,导致对象难以复用。
      想要定制一个分布在多个类中的逻辑或者行为,又不想生成太多子类

    发布订阅模式

    其实基本的设计模式中并没有发布订阅模式,上面也说了,他只是观察者模式的一个别称。但是经过时间的沉淀,似乎他已经强大了起来,已经独立于观察者模式,成为另外一种不同的设计模式。
    在现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。

    举一个例子,你在微博上关注了A,同时其他很多人也关注了A,那么当A发布动态的时候,微博就会为你们推送这条动态。A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接的消息往来的,全是通过微博来协调的(你的关注,A的发布动态

    观察者模式和发布订阅模式有什么区别?

    观察者模式: 观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

    发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。常见的是用协议的方式来做

    • 观察者模式是不是发布订阅模式

    网上关于这个问题的回答,出现了两极分化,有认为发布订阅模式就是观察者模式的,也有认为观察者模式和发布订阅模式是真不一样的。

    其实我不知道发布订阅模式是不是观察者模式,就像我不知道辨别模式的关键是设计意图还是设计结构(理念),虽然《JavaScript设计模式与开发实践》一书中说了分辨模式的关键是意图而不是结构。

    如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。

    不过,不管他们是不是同一个设计模式,他们的实现方式确实有差别,我们在使用的时候应该根据场景来判断选择哪个

    block

    • block是封装了函数调用和函数调用环境的OC对象,block分为3种类型:NSGlobalBlock、NSStackBlock、NSMallocBlock; 其都继承自NSBlock,NSBlock 继承自NSObject;
    • block使用需注意循环引用问题;一般需要使用强弱引用、__block来解决问题
    • block声明为属性时需要使用copy或strong来修饰;因为block最初是被分配在栈空间,内存由系统管理;但一般使用block是需要在运行时的某一个时机使用,所以需要开发者自己管理block的内存,使用copy和strong修饰会把block的内存复制到堆空间,这样就达到了自己管理内存的目的

    为什么block一开始的内存会被分配在栈空间?
    block使用会有两种情况:局部变量typedef声明
    局部变量申请的内存肯定在栈空间

    对象的本质?
    对象的本质是结构体;
    内存分配原理:以16个字节为单位且遵循了内存对齐原则向堆内存申请内存空间

    isa指针?
    isa指针是OC对象的第一个成员变量;isa是一个联合体结构,通过位域来存储数据;
    isa最重要的作用是用于消息发送;

    位域宏定义(真机环境arm64)
    # if __arm64__
    # define ISA_MASK 0x0000000ffffffff8ULL
    # define ISA_MAGIC_MASK 0x000003f000000001ULL
    # define ISA_MAGIC_VALUE 0x000001a000000001ULL
    # define ISA_BITFIELD \
    uintptr_t nonpointer : 1; 拿二进制的1位来存储 \
    uintptr_t has_assoc : 1; \
    uintptr_t has_cxx_dtor : 1; \
    uintptr_t shiftcls : 33;
    /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
    uintptr_t magic : 6; \
    uintptr_t weakly_referenced : 1; \
    uintptr_t deallocating : 1; \
    uintptr_t has_sidetable_rc : 1; \
    uintptr_t extra_rc : 19
    # define RC_ONE (1ULL<<45)
    # define RC_HALF (1ULL<<18)



    OC的内存管理原则 ?
    OC中内存管理是通过引用计数管理实现的,当一个对象的引用计数为0时就会进入释放流程;ARC利用LLVM编译器动态的在合适的位置添加内存管理代码的方式帮助开发者管理内存,同时又通过runtime管理weak修饰的弱引用表;基本实现了不用开发者关心内存问题就可以进行开发;

    • block、定时器时需要注意循环引用问题
    • 声明属性时需要注意强弱引用的使用

    多线程?

    即 multithreading , 是指从软件或者硬件上实现多个线程并发执行的技术。
    具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。
    多线程的调度原理可以认为是:时间片轮转调度算法,每个线程都会分配一个时间片然后大家轮着做任务,多线程执行时会快速切换时间片来完成多线程任务的执行;其实操作系统对进程、线程都是按照这种调度逻辑实现的

    程序、进程、线程、例程、协程是什么?

    • 程序:全称 计算机程序(Computer Program),是一组计算机能识别和执行的指令,又称计算机软件
      是指为了得到某种结果而可以由计算机等具有信息处理能力的装置执行的代码化指令序列,用某些程序设计语言编写,如:C、C++、OC等;它运行于电子计算机

    • 进程:是计算机中的程序关于某数据集合上的一次运行活动;是独立运行、独立分配资源和独立接受调度的基本单位;是操作系统结构的基础

    在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

    • 线程:线程是计算机调度的最小单位,用来处理不同的任务;

    • 例程:即函数,一个函数就可以看做是一个例程

    • 协程:利用单线程执行多任务的技术解决方案,性能上避免线程间切换要优于线程调度;是线程的更小拆分,又称之为“微线程”,是一种用户太的轻量级线程;
      和线程的区别:
      线程是系统级别的,它们由操作系统调度;同时是可被调度的最小单位;
      协程则是程序级别的,由程序员根据需要自己调度
      子程序:函数
      在一个线程中会有很多子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程及称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似于yield操作

    实例的生命周期?

    • alloc、new、copy、mutableCopy
    • 引用计数变化
    • 引用计数为0
    • dealloc


    作者:9523_it
    链接:https://www.jianshu.com/p/7646a2e8165f





    收起阅读 »

    Android 面试题及答案

    15、 说说mvc模式的原理,它在android中的运用,android的官方建议应用程序的开发采用mvc模式。何谓mvc? mvc是model,view,controller的缩写,mvc包含三个部分:   模型(model)对象:是应用程序的主体部分,所有...
    继续阅读 »

    15、 说说mvc模式的原理,它在android中的运用,android的官方建议应用程序的开发采用mvc模式。何谓mvc?


    mvc是model,view,controller的缩写,mvc包含三个部分:


      模型(model)对象:是应用程序的主体部分,所有的业务逻辑都应该写在该层。


      视图(view)对象:是应用程序中负责生成用户界面的部分。也是在整个mvc架构中用户唯一可以看到的一层,接收用户的输入,显示处理结果。


      控制器(control)对象:是根据用户的输入,控制用户界面数据显示及更新model对象状态的部分,控制器更重要的一种导航功能,响应用户出发的相关事件,交给m层处理。


      android鼓励弱耦合和组件的重用,在android中mvc的具体体现如下:


      1)视图层(view):一般采用xml文件进行界面的描述,使用的时候可以非常方便的引入,当然,如果你对android了解的比较的多了话,就一定可以想到在android中也可以使用JavaScript+html等的方式作为view层,当然这里需要进行java和javascript之间的通信,幸运的是,android提供了它们之间非常方便的通信实现。


      2)控制层(controller):android的控制层的重任通常落在了众多的acitvity的肩上,这句话也就暗含了不要在acitivity中写代码,要通过activity交割model业务逻辑层处理,这样做的另外一个原因是android中的acitivity的响应时间是5s,如果耗时的操作放在这里,程序就很容易被回收掉。


      3)模型层(model):对数据库的操作、对网络等的操作都应该在model里面处理,当然对业务计算等操作也是必须放在的该层的。


    16、 什么是ANR 如何避免它?


    答:ANR:Application Not Responding。在Android中,活动管理器和窗口管理器这两个系统服务负责监视应用程序的响应,当用户操作的在5s内应用程序没能做出反应,BroadcastReceiver在10秒内没有执行完毕,就会出现应用程序无响应对话框,这既是ANR。


    避免方法:Activity应该在它的关键生命周期方法(如onCreate()和onResume())里尽可能少的去做创建操作。潜在的耗时操作,例如网络或数据库操作,或者高耗时的计算如改变位图尺寸,应该在子线程里(或者异步方式)来完成。主线程应该为子线程提供一个Handler,以便完成时能够提交给主线程。


    17、 什么情况会导致Force Close ?如何避免?能否捕获导致其的异常?


    答:程序出现异常,比如nullpointer。


    避免:编写程序时逻辑连贯,思维缜密。能捕获异常,在logcat中能看到异常信息


    18、 描述一下android的系统架构


    android系统架构分从下往上为linux 内核层、运行库、应用程序框架层、和应用程序层。


    linuxkernel:负责硬件的驱动程序、网络、电源、系统安全以及内存管理等功能。


    libraries和 android runtime:libraries:即c/c++函数库部分,大多数都是开放源代码的函数库,例如webkit(引擎),该函数库负责 android网页浏览器的运行,例如标准的c函数库libc、openssl、sqlite等,当然也包括支持游戏开发2dsgl和 3dopengles,在多媒体方面有mediaframework框架来支持各种影音和图形文件的播放与显示,例如mpeg4、h.264、mp3、 aac、amr、jpg和png等众多的多媒体文件格式。android的runtime负责解释和执行生成的dalvik格式的字节码。


     applicationframework(应用软件架构),java应用程序开发人员主要是使用该层封装好的api进行快速开发。


      applications:该层是java的应用程序层,android内置的googlemaps、e-mail、即时通信工具、浏览器、mp3播放器等处于该层,java开发人员开发的程序也处于该层,而且和内置的应用程序具有平等的位置,可以调用内置的应用程序,也可以替换内置的应用程序。


      上面的四个层次,下层为上层服务,上层需要下层的支持,调用下层的服务,这种严格分层的方式带来的极大的稳定性、灵活性和可扩展性,使得不同层的开发人员可以按照规范专心特定层的开发。


    android应用程序使用框架的api并在框架下运行,这就带来了程序开发的高度一致性,另一方面也告诉我们,要想写出优质高效的程序就必须对整个 applicationframework进行非常深入的理解。精通applicationframework,你就可以真正的理解android的设计和运行机制,也就更能够驾驭整个应用层的开发。


    19、 请介绍下ContentProvider是如何实现数据共享的。


    一个程序可以通过实现一个Content provider的抽象接口将自己的数据完全暴露出去,而且Content providers是以类似数据库中表的方式将数据暴露。Content providers存储和检索数据,通过它可以让所有的应用程序访问到,这也是应用程序之间唯一共享数据的方法。


    要想使应用程序的数据公开化,可通过2种方法:创建一个属于你自己的Content provider或者将你的数据添加到一个已经存在的Content provider中,前提是有相同数据类型并且有写入Content provider的权限。


    如何通过一套标准及统一的接口获取其他应用程序暴露的数据?


    Android提供了ContentResolver,外界的程序可以通过ContentResolver接口访问ContentProvider提供的数据。


    20、 Service和Thread的区别?


    答:servie是系统的组件,它由系统进程托管(servicemanager);它们之间的通信类似于client和server,是一种轻量级的ipc通信,这种通信的载体是binder,它是在linux层交换信息的一种ipc。而thread是由本应用程序托管。1). Thread:Thread 是程序执行的最小单元,它是分配CPU的基本单位。可以用 Thread 来执行一些异步的操作。


    2). Service:Service 是android的一种机制,当它运行的时候如果是Local Service,那么对应的 Service 是运行在主进程的 main 线程上的。如:onCreate,onStart 这些函数在被系统调用的时候都是在主进程的 main 线程上运行的。如果是Remote Service,那么对应的 Service 则是运行在独立进程的 main 线程上。


    既然这样,那么我们为什么要用 Service 呢?其实这跟 android 的系统机制有关,我们先拿 Thread 来说。Thread 的运行是独立于 Activity 的,也就是说当一个 Activity 被 finish 之后,如果你没有主动停止 Thread 或者 Thread 里的 run 方法没有执行完毕的话,Thread 也会一直执行。因此这里会出现一个问题:当 Activity 被 finish 之后,你不再持有该 Thread 的引用。另一方面,你没有办法在不同的 Activity 中对同一 Thread 进行控制。  


    举个例子:如果你的 Thread 需要不停地隔一段时间就要连接服务器做某种同步的话,该 Thread 需要在 Activity 没有start的时候也在运行。这个时候当你 start 一个 Activity 就没有办法在该 Activity 里面控制之前创建的 Thread。因此你便需要创建并启动一个 Service ,在 Service 里面创建、运行并控制该 Thread,这样便解决了该问题(因为任何 Activity 都可以控制同一 Service,而系统也只会创建一个对应 Service 的实例)。  


    因此你可以把 Service 想象成一种消息服务,而你可以在任何有 Context 的地方调用 Context.startService、Context.stopService、Context.bindService,Context.unbindService,来控制它,你也可以在 Service 里注册 BroadcastReceiver,在其他地方通过发送 broadcast 来控制它,当然这些都是 Thread 做不到的。


    21、 Android本身的api并未声明会抛出异常,则其在运行时有无可能抛出runtime异常,你遇到过吗?诺有的话会导致什么问题?如何解决?


    答:会,比如nullpointerException。我遇到过,比如textview.setText()时,textview没有初始化。会导致程序无法正常运行出现forceclose。打开控制台查看logcat信息找出异常信息并修改程序。


    22、 IntentService有何优点?


    答:Acitivity的进程,当处理Intent的时候,会产生一个对应的Service; Android的进程处理器现在会尽可能的不kill掉你;非常容易使用


    23、 如果后台的Activity由于某原因被系统回收了,如何在被系统回收之前保存当前状态?


    答:重写onSaveInstanceState()方法,在此方法中保存需要保存的数据,该方法将会在activity被回收之前调用。通过重写onRestoreInstanceState()方法可以从中提取保存好的数据


    24、 如何将一个Activity设置成窗口的样式。


    答:中配置:android :theme="@android:style/Theme.Dialog" 


    另外android:theme="@android:style/Theme.Translucent" 是设置透明


    25、 如何退出Activity?如何安全退出已调用多个Activity的Application?


    答:对于单一Activity的应用来说,退出很简单,直接finish()即可。当然,也可以用killProcess()和System.exit()这样的方法。


    对于多个activity,1、记录打开的Activity:每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。2、发送特定广播:在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。3、递归退出:在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。为了编程方便,最好定义一个Activity基类,处理这些共通问题。


    在2.1之前,可以使用ActivityManager的restartPackage方法。


    它可以直接结束整个应用。在使用时需要权限android.permission.RESTART_PACKAGES。


    注意不要被它的名字迷惑。


    可是,在2.2,这个方法失效了。在2.2添加了一个新的方法,killBackground Processes(),需要权限 android.permission.KILL_BACKGROUND_PROCESSES。可惜的是,它和2.2的restartPackage一样,根本起不到应有的效果。


    另外还有一个方法,就是系统自带的应用程序管理里,强制结束程序的方法,forceStopPackage()。它需要权限android.permission.FORCE_STOP_PACKAGES。并且需要添加android:sharedUserId="android.uid.system"属性。同样可惜的是,该方法是非公开的,他只能运行在系统进程,第三方程序无法调用。


    因为需要在Android.mk中添加LOCAL_CERTIFICATE := platform。


    而Android.mk是用于在Android源码下编译程序用的。


    从以上可以看出,在2.2,没有办法直接结束一个应用,而只能用自己的办法间接办到。


    现提供几个方法,供参考:


    1、抛异常强制退出:


    该方法通过抛异常,使程序Force Close。


    验证可以,但是,需要解决的问题是,如何使程序结束掉,而不弹出Force Close的窗口。


    2、记录打开的Activity:


    每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。


    3、发送特定广播:


    在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。


    4、递归退出


    在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。


    除了第一个,都是想办法把每一个Activity都结束掉,间接达到目的。但是这样做同样不完美。你会发现,如果自己的应用程序对每一个Activity都设置了nosensor,在两个Activity结束的间隙,sensor可能有效了。但至少,我们的目的达到了,而且没有影响用户使用。为了编程方便,最好定义一个Activity基类,处理这些共通问题。


    26、 AIDL的全称是什么?如何工作?能处理哪些类型的数据?


    答:全称是:Android Interface Define Language


    在Android中, 每个应用程序都可以有自己的进程. 在写UI应用的时候, 经常要用到Service. 在不同的进程中, 怎样传递对象呢?显然, Java中不允许跨进程内存共享. 因此传递对象, 只能把对象拆分成操作系统能理解的简单形式, 以达到跨界对象访问的目的. 在J2EE中,采用RMI的方式, 可以通过序列化传递对象. 在Android中, 则采用AIDL的方式. 理论上AIDL可以传递Bundle,实际上做起来却比较麻烦。


    AIDL(AndRoid接口描述语言)是一种借口描述语言; 编译器可以通过aidl文件生成一段代码,通过预先定义的接口达到两个进程内部通信进程的目的. 如果需要在一个Activity中, 访问另一个Service中的某个对象, 需要先将对象转化成AIDL可识别的参数(可能是多个参数), 然后使用AIDL来传递这些参数, 在消息的接收端, 使用这些参数组装成自己需要的对象.


    AIDL的IPC的机制和COM或CORBA类似, 是基于接口的,但它是轻量级的。它使用代理类在客户端和实现层间传递值. 如果要使用AIDL, 需要完成2件事情: 1. 引入AIDL的相关类.; 2. 调用aidl产生的class.


    AIDL的创建方法:


    AIDL语法很简单,可以用来声明一个带一个或多个方法的接口,也可以传递参数和返回值。 由于远程调用的需要, 这些参数和返回值并不是任何类型.下面是些AIDL支持的数据类型:


    1. 不需要import声明的简单Java编程语言类型(int,boolean等)


    2. String, CharSequence不需要特殊声明


    3. List, Map和Parcelables类型, 这些类型内所包含的数据成员也只能是简单数据类型, String等其他比支持的类型.


    (另外: 我没尝试Parcelables, 在Eclipse+ADT下编译不过, 或许以后会有所支持)


    27、 请解释下Android程序运行时权限与文件系统权限的区别。


    答:运行时权限Dalvik( android授权) 


    文件系统 linux 内核授权


    28、 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。


    通过直接发送Uri把参数带过去,或者通过manifest里的intentfilter里的data属性
    29、 android系统的优势和不足



    答:Android平台手机 5大优势: 


    一、开放性 


    在优势方面,Android平台首先就是其开发性,开发的平台允许任何移动终端厂商加入到Android联盟中来。显著的开放性可以使其拥有更多的开发者,随着用户和应用的日益丰富,一个崭新的平台也将很快走向成熟。开放性对于Android的发展而言,有利于积累人气,这里的人气包括消费者和厂商,而对于消费者来讲,随大的受益正是丰富的软件资源。开放的平台也会带来更大竞争,如此一来,消费者将可以用更低的价位购得心仪的手机。


    二、挣脱运营商的束缚 


    在过去很长的一段时间,特别是在欧美地区,手机应用往往受到运营商制约,使用什么功能接入什么网络,几乎都受到运营商的控制。从去年iPhone 上市 ,用户可以更加方便地连接网络,运营商的制约减少。随着EDGE、HSDPA这些2G至3G移动网络的逐步过渡和提升,手机随意接入网络已不是运营商口中的笑谈,当你可以通过手机IM软件方便地进行即时聊天时,再回想不久前天价的彩信和图铃下载业务,是不是像噩梦一样?互联网巨头Google推动的Android终端天生就有网络特色,将让用户离互联网更近。


    三、丰富的硬件选择 


    这一点还是与Android平台的开放性相关,由于Android的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色,却不会影响到数据同步、甚至软件的兼容,好比你从诺基亚 Symbian风格手机 一下改用苹果 iPhone ,同时还可将Symbian中优秀的软件带到iPhone上使用、联系人等资料更是可以方便地转移,是不是非常方便呢?


    四、不受任何限制的开发商 


    Android平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰,可想而知,会有多少新颖别致的软件会诞生。但也有其两面性,血腥、暴力、情色方面的程序和游戏如可控制正是留给Android难题之一。


    五、无缝结合的Google应用 


    如今叱诧互联网的Google已经走过10年度历史,从搜索巨人到全面的互联网渗透,Google服务如地图、邮件、搜索等已经成为连接用户和互联网的重要纽带,而Android平台手机将无缝结合这些优秀的Google服务。


    再说Android的5大不足:


    一、安全和隐私 


    由于手机 与互联网的紧密联系,个人隐私很难得到保守。除了上网过程中经意或不经意留下的个人足迹,Google这个巨人也时时站在你的身后,洞穿一切,因此,互联网的深入将会带来新一轮的隐私危机。


    二、首先开卖Android手机的不是最大运营商 


    众所周知,T-Mobile在23日,于美国纽约发布 了Android首款手机G1。但是在北美市场,最大的两家运营商乃AT&T和Verizon,而目前所知取得Android手机销售权的仅有 T-Mobile和Sprint,其中T-Mobile的3G网络相对于其他三家也要逊色不少,因此,用户可以买账购买G1,能否体验到最佳的3G网络服务则要另当别论了!


    三、运营商仍然能够影响到Android手机 


    在国内市场,不少用户对购得移动定制机不满,感觉所购的手机被人涂画了广告一般。这样的情况在国外市场同样出现。Android手机的另一发售运营商Sprint就将在其机型中内置其手机商店程序。


    四、同类机型用户减少 


    在不少手机论坛都会有针对某一型号的子论坛,对一款手机的使用心得交流,并分享软件资源。而对于Android平台手机,由于厂商丰富,产品类型多样,这样使用同一款机型的用户越来越少,缺少统一机型的程序强化。举个稍显不当的例子,现在山寨机泛滥,品种各异,就很少有专门针对某个型号山寨机的讨论和群组,除了哪些功能异常抢眼、颇受追捧的机型以外。


    五、过分依赖开发商缺少标准配置 


    在使用PC端的Windows Xp系统的时候,都会内置微软Windows Media Player这样一个浏览器程序,用户可以选择更多样的播放器,如Realplay或暴风影音等。但入手开始使用默认的程序同样可以应付多样的需要。在 Android平台中,由于其开放性,软件更多依赖第三方厂商,比如Android系统的SDK中就没有内置音乐 播放器,全部依赖第三方开发,缺少了产品的统一性。


    30、 Android dvm的进程和Linux的进程, 应用程序的进程是否为同一个概念 


    答:DVM指dalivk的虚拟机。每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例。而每一个DVM都是在Linux 中的一个进程,所以说可以认为是同一个概念。


    31、 sim卡的EF文件是什么?有何作用


    答:sim卡的文件系统有自己规范,主要是为了和手机通讯,sim本 身可以有自己的操作系统,EF就是作存储并和手机通讯用的


    32、 嵌入式操作系统内存管理有哪几种, 各有何特性


      页式,段式,段页,用到了MMU,虚拟空间等技术


    33、 什么是嵌入式实时操作系统, Android 操作系统属于实时操作系统吗?


    嵌入式实时操作系统是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的嵌入式操作系统。主要用于工业控制、 军事设备、 航空航天等领域对系统的响应时间有苛刻的要求,这就需要使用实时系统。又可分为软实时和硬实时两种,而android是基于linux内核的,因此属于软实时。


    34、 一条最长的短信息约占多少byte?


      中文70(包括标点),英文160,160个字节。  


    35、 如何将SQLite数据库(dictionary.db文件)与apk文件一起发布


    解答:可以将dictionary.db文件复制到Eclipse Android工程中的res aw目录中。所有在res aw目录中的文件不会被压缩,这样可以直接提取该目录中的文件。可以将dictionary.db文件复制到res aw目录中


    36、 如何将打开res aw目录中的数据库文件?


    解答:在Android中不能直接打开res aw目录中的数据库文件,而需要在程序第一次启动时将该文件复制到手机内存或SD卡的某个目录中,然后再打开该数据库文件。


    复制的基本方法是使用getResources().openRawResource方法获得res aw目录中资源的 InputStream对象,然后将该InputStream对象中的数据写入其他的目录中相应文件中。在Android SDK中可以使用SQLiteDatabase.openOrCreateDatabase方法来打开任意目录中的SQLite数据库文件。


    37、 DDMS和TraceView的区别? 


    DDMS是一个程序执行查看器,在里面可以看见线程和堆栈等信息,TraceView是程序性能分析器 。


    38、 java中如何引用本地语言 


    可以用JNI(java native interface  java 本地接口)接口 。


    39、 谈谈Android的IPC(进程间通信)机制 


    IPC是内部进程通信的简称, 是共享"命名管道"的资源。Android中的IPC机制是为了让Activity和Service之间可以随时的进行交互,故在Android中该机制,只适用于Activity和Service之间的通信,类似于远程方法调用,类似于C/S模式的访问。通过定义AIDL接口文件来定义IPC接口。Servier端实现IPC接口,Client端调用IPC接口本地代理。


    40、 NDK是什么


    NDK是一些列工具的集合,NDK提供了一系列的工具,帮助开发者迅速的开发C/C++的动态库,并能自动将so和java 应用打成apk包。


    NDK集成了交叉编译器,并提供了相应的mk文件和隔离cpu、平台等的差异,开发人员只需简单的修改mk文件就可以创建出so


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

    Android 面试题及答案

    ‍‍8、跟activity和Task 有关的 Intent启动方式有哪些?其含义?‍‍ ‍‍核心的Intent Flag有:‍‍  ‍‍FLAG_ACTIVITY_NEW_TASK‍‍  ‍‍FLAG_ACTI...
    继续阅读 »

    ‍‍8、跟activity和Task 有关的 Intent启动方式有哪些?其含义?‍‍


    ‍‍核心的Intent Flag有:‍‍ 


    ‍‍FLAG_ACTIVITY_NEW_TASK‍‍ 


    ‍‍FLAG_ACTIVITY_CLEAR_TOP ‍‍


    ‍‍FLAG_ACTIVITY_RESET_TASK_IF_NEEDED ‍‍


    ‍‍FLAG_ACTIVITY_SINGLE_TOP‍‍


    ‍‍FLAG_ACTIVITY_NEW_TASK‍‍ 


      ‍‍如果设置,这个Activity会成为历史stack中一个新Task的开始。一个Task(从启动它的Activity到下一个Task中的 Activity)定义了用户可以迁移的Activity原子组。Task可以移动到前台和后台;在某个特定Task中的所有Activity总是保持相同的次序。‍‍ 


     ‍‍ 这个标志一般用于呈现“启动”类型的行为:它们提供用户一系列可以单独完成的事情,与启动它们的Activity完全无关。 


     使用这个标志,如果正在启动的Activity的Task已经在运行的话,那么,新的Activity将不会启动;代替的,当前Task会简单的移入前台。参考FLAG_ACTIVITY_MULTIPLE_TASK标志,可以禁用这一行为。 


      这个标志不能用于调用方对已经启动的Activity请求结果。


    FLAG_ACTIVITY_CLEAR_TOP 


      如果设置,并且这个Activity已经在当前的Task中运行,因此,不再是重新启动一个这个Activity的实例,而是在这个Activity上方的所有Activity都将关闭,然后这个Intent会作为一个新的Intent投递到老的Activity(现在位于顶端)中。 


      例如,假设一个Task中包含这些Activity:A,B,C,D。如果D调用了startActivity(),并且包含一个指向Activity B的Intent,那么,C和D都将结束,然后B接收到这个Intent,因此,目前stack的状况是:A,B。 


      上例中正在运行的Activity B既可以在onNewIntent()中接收到这个新的Intent,也可以把自己关闭然后重新启动来接收这个Intent。如果它的启动模式声明为 “multiple”(默认值),并且你没有在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,那么它将关闭然后重新创建;对于其它的启动模式,或者在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,都将把这个Intent投递到当前这个实例的onNewIntent()中。 


      这个启动模式还可以与FLAG_ACTIVITY_NEW_TASK结合起来使用:用于启动一个Task中的根Activity,它会把那个Task中任何运行的实例带入前台,然后清除它直到根Activity。这非常有用,例如,当从Notification Manager处启动一个Activity。 


    FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 


        如果设置这个标志,这个activity不管是从一个新的栈启动还是从已有栈推到栈顶,它都将以the front door of the task的方式启动。这就讲导致任何与应用相关的栈都讲重置到正常状态(不管是正在讲activity移入还是移除),如果需要,或者直接重置该栈为初始状态。


    FLAG_ACTIVITY_SINGLE_TOP 


      如果设置,当这个Activity位于历史stack的顶端运行时,不再启动一个新的


    FLAG_ACTIVITY_BROUGHT_TO_FRONT 


      这个标志一般不是由程序代码设置的,如在launchMode中设置singleTask模式时系统帮你设定。 


    FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET 


      如果设置,这将在Task的Activity stack中设置一个还原点,当Task恢复时,需要清理Activity。也就是说,下一次Task带着 FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记进入前台时(典型的操作是用户在主画面重启它),这个Activity和它之上的都将关闭,以至于用户不能再返回到它们,但是可以回到之前的Activity。 


      这在你的程序有分割点的时候很有用。例如,一个e-mail应用程序可能有一个操作是查看一个附件,需要启动图片浏览Activity来显示。这个 Activity应该作为e-mail应用程序Task的一部分,因为这是用户在这个Task中触发的操作。然而,当用户离开这个Task,然后从主画面选择e-mail app,我们可能希望回到查看的会话中,但不是查看图片附件,因为这让人困惑。通过在启动图片浏览时设定这个标志,浏览及其它启动的Activity在下次用户返回到mail程序时都将全部清除。 


    FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 


      如果设置,新的Activity不会在最近启动的Activity的列表中保存。 


    FLAG_ACTIVITY_FORWARD_RESULT 


      如果设置,并且这个Intent用于从一个存在的Activity启动一个新的Activity,那么,这个作为答复目标的Activity将会传到这个新的Activity中。这种方式下,新的Activity可以调用setResult(int),并且这个结果值将发送给那个作为答复目标的 Activity。 


    FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 


      这个标志一般不由应用程序代码设置,如果这个Activity是从历史记录里启动的(常按HOME键),那么,系统会帮你设定。 


    FLAG_ACTIVITY_MULTIPLE_TASK 


      不要使用这个标志,除非你自己实现了应用程序启动器。与FLAG_ACTIVITY_NEW_TASK结合起来使用,可以禁用把已存的Task送入前台的行为。当设置时,新的Task总是会启动来处理Intent,而不管这是是否已经有一个Task可以处理相同的事情。 


      由于默认的系统不包含图形Task管理功能,因此,你不应该使用这个标志,除非你提供给用户一种方式可以返回到已经启动的Task。 


      如果FLAG_ACTIVITY_NEW_TASK标志没有设置,这个标志被忽略。 


    FLAG_ACTIVITY_NO_ANIMATION 


      如果在Intent中设置,并传递给Context.startActivity()的话,这个标志将阻止系统进入下一个Activity时应用 Acitivity迁移动画。这并不意味着动画将永不运行——如果另一个Activity在启动显示之前,没有指定这个标志,那么,动画将被应用。这个标志可以很好的用于执行一连串的操作,而动画被看作是更高一级的事件的驱动。 


    FLAG_ACTIVITY_NO_HISTORY 


      如果设置,新的Activity将不再历史stack中保留。用户一离开它,这个Activity就关闭了。这也可以通过设置noHistory特性。 


    FLAG_ACTIVITY_NO_USER_ACTION 


      如果设置,作为新启动的Activity进入前台时,这个标志将在Activity暂停之前阻止从最前方的Activity回调的onUserLeaveHint()。 


      典型的,一个Activity可以依赖这个回调指明显式的用户动作引起的Activity移出后台。这个回调在Activity的生命周期中标记一个合适的点,并关闭一些Notification。 


      如果一个Activity通过非用户驱动的事件,如来电或闹钟,启动的,这个标志也应该传递给Context.startActivity,保证暂停的Activity不认为用户已经知晓其Notification。 


    FLAG_ACTIVITY_PREVIOUS_IS_TOP 


      If set and this intent is being used to launch a new activity from an existing one, the current activity will not be counted as the top activity for deciding whether the new intent should be delivered to the top instead of starting a new one. The previous activity will be used as the top, with the assumption being that the current activity will finish itself immediately. 


    FLAG_ACTIVITY_REORDER_TO_FRONT 


      如果在Intent中设置,并传递给Context.startActivity(),这个标志将引发已经运行的Activity移动到历史stack的顶端。 


      例如,假设一个Task由四个Activity组成:A,B,C,D。如果D调用startActivity()来启动Activity B,那么,B会移动到历史stack的顶端,现在的次序变成A,C,D,B。如果FLAG_ACTIVITY_CLEAR_TOP标志也设置的话,那么这个标志将被忽略。 


    9、 请描述下Activity的生命周期。


    答:activity的生命周期方法有:onCreate()、onStart()、onReStart()、onResume()、onPause()、onStop()、onDestory();


    可见生命周期:从onStart()直到系统调用onStop()


    前台生命周期:从onResume()直到系统调用onPause()


    10、 activity在屏幕旋转时的生命周期


    答:不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次;设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次;设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法


    11、 如何启用Service,如何停用Service。


    服务的开发比较简单,如下:


    第一步:继承Service类


    public class SMSService extends Service {}

    第二步:在AndroidManifest.xml文件中的节点里对服务进行配置:


    服务不能自己运行,需要通过调用Context.startService()或Context.bindService()方法启动服务。这两个方法都可以启动Service,但是它们的使用场合有所不同。使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。


    如果打算采用Context.startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onStart()方法。如果调用startService()方法前服务已经被创建,多次调用startService()方法并不会导致多次创建服务,但会导致多次调用onStart()方法。采用startService()方法启动的服务,只能调用Context.stopService()方法结束服务,服务结束时会调用onDestroy()方法。


    如果打算采用Context.bindService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onBind()方法。这个时候调用者和服务绑定在一起,调用者退出了,系统就会先调用服务的onUnbind()方法,接着调用onDestroy()方法。如果调用bindService()方法前服务已经被绑定,多次调用bindService()方法并不会导致多次创建服务及绑定(也就是说onCreate()和onBind()方法并不会被多次调用)。如果调用者希望与正在绑定的服务解除绑定,可以调用unbindService()方法,调用该方法也会导致系统调用服务的onUnbind()-->onDestroy()方法。


    服务常用生命周期回调方法如下: 


    onCreate() 该方法在服务被创建时调用,该方法只会被调用一次,无论调用多少次startService()或bindService()方法,服务也只被创建一次。


    onDestroy()该方法在服务被终止时调用。


    与采用Context.startService()方法启动服务有关的生命周期方法


    onStart() 只有采用Context.startService()方法启动服务时才会回调该方法。该方法在服务开始运行时被调用。多次调用startService()方法尽管不会多次创建服务,但onStart() 方法会被多次调用。


    与采用Context.bindService()方法启动服务有关的生命周期方法


    onBind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用,当调用者与服务已经绑定,多次调用Context.bindService()方法并不会导致该方法被多次调用。


    onUnbind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务解除绑定时被调用


    12、 注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。


    答:首先写一个类要继承BroadcastReceiver


    第一种:在清单文件中声明,添加




       
    复制代码

    ‍‍第二种使用代码进行注册如:‍‍


    IntentFilter filter =  new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
    IncomingSMSReceiver receiver = new IncomgSMSReceiver();
    registerReceiver(receiver.filter);复制代码

    两种注册类型的区别是:


    1)第一种不是常驻型广播,也就是说广播跟随程序的生命周期。


    2)第二种是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。


    13、 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系。


    答:简单的说,Handler获取当前线程中的looper对象,looper用来从存放Message的MessageQueue中取出Message,再有Handler进行Message的分发和处理.


    Message Queue(消息队列):用来存放通过Handler发布的消息,通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列


    Handler:可以发布或者处理一个消息或者操作一个Runnable,通过Handler发布消息,消息将只会发送到与它关联的消息队列,然也只能处理该消息队列中的消息


    Looper:是Handler和消息队列之间通讯桥梁,程序组件首先通过Handler把消息传递给Looper,Looper把消息放入队列。Looper也把消息队列里的消息广播给所有的


    Handler:Handler接受到消息后调用handleMessage进行处理


    Message:消息的类型,在Handler类中的handleMessage方法中得到单个的消息进行处理


    在单线程模型下,为了线程通信问题,Android设计了一个Message Queue(消息队列), 线程间可以通过该Message Queue并结合Handler和Looper组件进行信息交换。下面将对它们进行分别介绍:


    1. Message 


        Message消息,理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。


    2. Handler 


        Handler处理者,是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message)方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。


    3. Message Queue 


        Message Queue消息队列,用来存放通过Handler发布的消息,按照先进先出执行。


        每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。


    4. Looper 


        Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。对于子线程使用Looper,API Doc提供了正确的使用方法:这个Message机制的大概流程:


        1. 在Looper.loop()方法运行开始后,循环地按照接收顺序取出Message Queue里面的非NULL的Message。


        2. 一开始Message Queue里面的Message都是NULL的。当Handler.sendMessage(Message)到Message Queue,该函数里面设置了那个Message对象的target属性是当前的Handler对象。随后Looper取出了那个Message,则调用 该Message的target指向的Hander的dispatchMessage函数对Message进行处理。在dispatchMessage方法里,如何处理Message则由用户指定,三个判断,优先级从高到低:


        1) Message里面的Callback,一个实现了Runnable接口的对象,其中run函数做处理工作;


        2) Handler里面的mCallback指向的一个实现了Callback接口的对象,由其handleMessage进行处理;


        3) 处理消息Handler对象对应的类继承并实现了其中handleMessage函数,通过这个实现的handleMessage函数处理消息。


        由此可见,我们实现的handleMessage方法是优先级最低的!


        3. Handler处理完该Message (update UI) 后,Looper则设置该Message为NULL,以便回收!


        在网上有很多文章讲述主线程和其他子线程如何交互,传送信息,最终谁来执行处理信息之类的,个人理解是最简单的方法——判断Handler对象里面的Looper对象是属于哪条线程的,则由该线程来执行! 


        1. 当Handler对象的构造函数的参数为空,则为当前所在线程的Looper; 


    2. Looper.getMainLooper()得到的是主线程的Looper对象,Looper.myLooper()得到的是当前线程的Looper对象。


    14、 简要解释一下activity、 intent 、intent filter、service、Broadcase、BroadcaseReceiver


    答:一个activity呈现了一个用户可以操作的可视化用户界面;一个service不包含可见的用户界面,而是在后台运行,可以与一个activity绑定,通过绑定暴露出来接口并与其进行通信;一个broadcast receiver是一个接收广播消息并做出回应的component,broadcast receiver没有界面;一个intent是一个Intent对象,它保存了消息的内容。对于activity和service来说,它指定了请求的操作名称和待操作数据的URI,Intent对象可以显式的指定一个目标component。如果这样的话,android会找到这个component(基于manifest文件中的声明)并激活它。但如果一个目标不是显式指定的,android必须找到响应intent的最佳component。它是通过将Intent对象和目标的intent filter相比较来完成这一工作的;一个component的intent filter告诉android该component能处理的intent。intent filter也是在manifest文件中声明的。


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

    Android 面试题及答案

    1、 Android的四大组件是哪些,它们的作用? 答:Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持久化的事情,妥善管理生命周期以及一些跳转逻辑 service...
    继续阅读 »

    1、 Android的四大组件是哪些,它们的作用?


    答:Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持久化的事情,妥善管理生命周期以及一些跳转逻辑


    service:后台服务于Activity,封装有一个完整的功能逻辑实现,接受上层指令,完成相关的事物,定义好需要接受的Intent提供同步和异步的接口


    Content Provider:是Android提供的第三方应用数据的访问方案,可以派生Content Provider类,对外提供数据,可以像数据库一样进行选择排序,屏蔽内部数据的存储细节,向外提供统一的借口模型,大大简化上层应用,对数据的整合提供了更方便的途径


    BroadCast Receiver:接受一种或者多种Intent作触发事件,接受相关消息,做一些简单处理,转换成一条Notification,统一了Android的事件广播模型


    2、 请介绍下Android中常用的五种布局。


    常用五种布局方式,分别是:FrameLayout(框架布局),LinearLayout (线性布局),AbsoluteLayout(绝对布局),RelativeLayout(相对布局),TableLayout(表格布局)。


    一、FrameLayout:所有东西依次都放在左上角,会重叠,这个布局比较简单,也只能放一点比较简单的东西。二、LinearLayout:线性布局,每一个LinearLayout里面又可分为垂直布局(android:orientation="vertical")和水平布局(android:orientation="horizontal" )。当垂直布局时,每一行就只有一个元素,多个元素依次垂直往下;水平布局时,只有一行,每一个元素依次向右排列。三、AbsoluteLayout:绝对布局用X,Y坐标来指定元素的位置,这种布局方式也比较简单,但是在屏幕旋转时,往往会出问题,而且多个元素的时候,计算比较麻烦。四、RelativeLayout:相对布局可以理解为某一个元素为参照物,来定位的布局方式。主要属性有:相对于某一个元素android:layout_below、      android:layout_toLeftOf相对于父元素的地方android:layout_alignParentLeft、android:layout_alignParentRigh;五、TableLayout:表格布局,每一个TableLayout里面有表格行TableRow,TableRow里面可以具体定义每一个元素。每一个布局都有自己适合的方式,这五个布局元素可以相互嵌套应用,做出美观的界面。


    3、 android中的动画有哪几类,它们的特点和区别是什么  


    答:两种,一种是Tween动画、还有一种是Frame动画。Tween动画,这种实现方式可以使视图组件移动、放大、缩小以及产生透明度的变化;另一种Frame动画,传统的动画方法,通过顺序的播放排列好的图片来实现,类似电影。


    4、 android 中有哪几种解析xml的类?官方推荐哪种?以及它们的原理和区别。


    答:XML解析主要有三种方式,SAX、DOM、PULL。常规在PC上开发我们使用Dom相对轻松些,但一些性能敏感的数据库或手机上还是主要采用SAX方式,SAX读取是单向的,优点:不占内存空间、解析属性方便,但缺点就是对于套嵌多个分支来说处理不是很方便。而DOM方式会把整个XML文件加载到内存中去,这里Android开发网提醒大家该方法在查找方面可以和XPath很好的结合如果数据量不是很大推荐使用,而PULL常常用在J2ME对于节点处理比较好,类似SAX方式,同样很节省内存,在J2ME中我们经常使用的KXML库来解析。


    5、 ListView的优化方案


    答:1、如果自定义适配器,那么在getView方法中要考虑方法传进来的参数contentView是否为null,如果为null就创建contentView并返回,如果不为null则直接使用。在这个方法中尽可能少创建view。


      2、给contentView设置tag(setTag()),传入一个viewHolder对象,用于缓存要显示的数据,可以达到图像数据异步加载的效果。


      3、如果listview需要显示的item很多,就要考虑分页加载。比如一共要显示100条或者更多的时候,我们可以考虑先加载20条,等用户拉到列表底部的时候再去加载接下来的20条。


    6、 请介绍下Android的数据存储方式。


    答:使用SharedPreferences存储数据;文件存储数据;SQLite数据库存储数据;使用ContentProvider存储数据;网络存储数据;


    Preference,File, DataBase这三种方式分别对应的目录是/data/data/Package Name/Shared_Pref, /data/data/Package Name/files, /data/data/Package Name/database 。


    一:使用SharedPreferences存储数据


    首先说明SharedPreferences存储方式,它是 Android提供的用来存储一些简单配置信息的一种机制,例如:登录用户的用户名与密码。其采用了Map数据结构来存储数据,以键值的方式存储,可以简单的读取与写入,具体实例如下:


    void ReadSharedPreferences(){
    String strName,strPassword;
    SharedPreferences   user = getSharedPreferences(“user_info”,0);
    strName = user.getString(“NAME”,””);
    strPassword = user getString(“PASSWORD”,””);
    }
    void WriteSharedPreferences(String strName,String strPassword){
    SharedPreferences   user = getSharedPreferences(“user_info”,0);
    uer.edit();
    user.putString(“NAME”, strName);
    user.putString(“PASSWORD” ,strPassword);
    user.commit();
    }复制代码

    ‍‍数据读取与写入的方法都非常简单,只是在写入的时候有些区别:先调用edit()使其处于编辑状态,然后才能修改数据,最后使用commit()提交修改的数据。实际上SharedPreferences是采用了XML格式将数据存储到设备中,在DDMS中的File Explorer中的/data/data//shares_prefs下。使用SharedPreferences是有些限制的:只能在同一个包内使用,不能在不同的包之间使用。‍‍


    ‍‍二:文件存储数据‍‍


    ‍‍文件存储方式是一种较常用的方法,在Android中读取/写入文件的方法,与 Java中实现I/O的程序是完全一样的,提供了openFileInput()和openFileOutput()方法来读取设备上的文件。具体实例如下:‍‍


    String fn = “moandroid.log”;
    FileInputStream fis = openFileInput
    (fn);
    FileOutputStream fos = openFileOutput
    (fn,Context.MODE_PRIVATE);复制代码

    ‍‍三:网络存储数据‍‍


    ‍‍网络存储方式,需要与Android 网络数据包打交道,关于Android 网络数据包的详细说明,请阅读Android SDK引用了Java SDK的哪些package?。‍‍


    ‍‍四:ContentProvider‍‍


    ‍‍1、ContentProvider简介‍‍


    ‍‍当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。‍‍


    ‍‍2、Uri类简介‍‍


    ‍‍Uri代表了要操作的数据,Uri主要包含了两部分信息:1.需要操作的ContentProvider ,2.对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成:‍‍


    ‍‍1.scheme:ContentProvider(内容提供者)的scheme已经由Android所规定为:content://…‍‍


    ‍‍2.主机名(或Authority):用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。‍‍


    ‍‍3.路径(path):可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:‍‍


    要操作contact表中id为10的记录,可以构建这样的路径:/contact/10


    要操作contact表中id为10的记录的name字段, contact/10/name


    要操作contact表中的所有记录,可以构建这样的路径:/contact?


    要操作的数据不一定来自数据库,也可以是文件等他存储方式,如下:


    要操作xml文件中contact节点下的name节点,可以构建这样的路径:/contact/name


    如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:


    Uri uri = Uri.parse("content://com.changcheng.provider.contactprovider/contact")


    3、UriMatcher、ContentUrist和ContentResolver简介


    因为Uri代表了要操作的数据,所以我们很经常需要解析Uri,并从 Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。掌握它们的使用,会便于我们的开发工作。


    UriMatcher:用于匹配Uri,它的用法如下:


    ‍‍1.首先把你需要匹配Uri路径全部给注册上,如下:‍‍


    //常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码(-1)。
    UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    //如果match()方法匹配content://com.changcheng.sqlite.provider.contactprovider /contact路径,返回匹配码为1
    uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact”, 1);//添加需要匹配uri,如果匹配就会返回匹配码
    //如果match()方法匹配 content://com.changcheng.sqlite.provider.contactprovider/contact/230路径,返回匹配码为2
    uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact/#”, 2);//#号为通配符复制代码

    ‍‍2.注册完需要匹配的Uri后,就可以使用uriMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用 addURI()方法传入的第三个参数,假设匹配 content://com.changcheng.sqlite.provider.contactprovider/contact路径,返回的匹配码为1。‍‍


    ‍‍ContentUris:用于获取Uri路径后面的ID部分,它有两个比较实用的方法:‍‍


    ‍‍withAppendedId(uri, id)用于为路径加上ID部分‍‍


    ‍‍parseId(uri)方法用于从路径中获取ID部分‍‍


    ‍‍ContentResolver:当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用 ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver使用insert、delete、update、query方法,来操作数据。‍‍


    ‍‍7、activity的启动模式有哪些?是什么含义?‍‍


    ‍‍答:在android里,有4种activity的启动模式,分别为: ‍‍


    ‍‍“standard” (默认) ‍‍


    ‍‍“singleTop” ‍‍


    ‍‍“singleTask” ‍‍


    ‍‍“singleInstance”


    ‍‍它们主要有如下不同:‍‍


    ‍‍1. 如何决定所属task ‍‍


    ‍‍“standard”和”singleTop”的activity的目标task,和收到的Intent的发送者在同一个task内,除非intent包括参数FLAG_ACTIVITY_NEW_TASK。‍‍ 


    ‍‍如果提供了FLAG_ACTIVITY_NEW_TASK参数,会启动到别的task里。 ‍‍


    ‍‍“singleTask”和”singleInstance”总是把activity作为一个task的根元素,他们不会被启动到一个其他task里。‍‍


    ‍‍2. 是否允许多个实例 ‍‍


    ‍‍“standard”和”singleTop”可以被实例化多次,并且存在于不同的task中,且一个task可以包括一个activity的多个实例; ‍‍


    ‍‍“singleTask”和”singleInstance”则限制只生成一个实例,并且是task的根元素。 singleTop要求如果创建intent的时候栈顶已经有要创建的Activity的实例,则将intent发送给该实例,而不发送给新的实例。‍‍


    ‍‍3. 是否允许其它activity存在于本task内 ‍‍


    ‍‍“singleInstance”独占一个task,其它activity不能存在那个task里;如果它启动了一个新的activity,不管新的activity的launch mode 如何,新的activity都将会到别的task里运行(如同加了FLAG_ACTIVITY_NEW_TASK参数)。 ‍‍


    ‍‍而另外三种模式,则可以和其它activity共存。‍‍


    ‍‍4. 是否每次都生成新实例‍‍ 


    ‍‍“standard”对于没一个启动Intent都会生成一个activity的新实例; ‍‍


    ‍‍“singleTop”的activity如果在task的栈顶的话,则不生成新的该activity的实例,直接使用栈顶的实例,否则,生成该activity的实例。 ‍‍


    ‍‍比如现在task栈元素为A-B-C-D(D在栈顶),这时候给D发一个启动intent,如果D是 “standard”的,则生成D的一个新实例,栈变为A-B-C-D-D。‍‍ 


    ‍‍如果D是singleTop的话,则不会生产D的新实例,栈状态仍为A-B-C-D ‍‍


    ‍‍如果这时候给B发Intent的话,不管B的launchmode是”standard” 还是 “singleTop” ,都会生成B的新实例,栈状态变为A-B-C-D-B。‍‍


    ‍‍“singleInstance”是其所在栈的唯一activity,它会每次都被重用。‍‍


    ‍‍“singleTask”如果在栈顶,则接受intent,否则,该intent会被丢弃,但是该task仍会回到前台。‍‍


    ‍‍当已经存在的activity实例处理新的intent时候,会调用onNewIntent()方法 如果收到intent生成一个activity实例,那么用户可以通过back键回到上一个状态;如果是已经存在的一个activity来处理这个intent的话,用户不能通过按back键返回到这之前的状态。


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

    为数不多的人知道的 Kotlin 技巧

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。众所周知 xml 十分耗时,因此在 Android 10.0 ...
    继续阅读 »

    Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。

    众所周知 xml 十分耗时,因此在 Android 10.0 上新增加 tryInflatePrecompiled 方法,这是一个在编译期运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled 方法根据 XML 预编译生成 compiled_view.dex, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,但是目前一直处于禁用状态。源码解析请查看 Android 资源加载源码分析一

    因此一些体量比较大的应用,为了极致的优化,缩短一点时间,对于简单的布局,会使用 Kotlin 去重写这部分 UI,但是门槛还是很高,随着 Jetpack Compose 的出现,其目的是让您更快、更轻松地构建原生 Android 应用,前不久 Google 正式发布了 Jetpack Compose 1.0。

    Kotlin 优势已经体现在了方方面面,结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是如果使用不当会对性能造成一些损耗,更多内容可前往查看。

    以上两篇文章,主要分享了 Kotlin 在实际项目中使用的技巧,以及如果使用不当会对 性能 和 内存 造成的那些影响以及如何规避这些问题等等。

    通过这篇文章你将学习到以下内容:

    • 什么是 Contract,以及如何使用?
    • Kotlin 注解在项目中的使用?
    • 一行代码接受 Activity 或者 Fragment 传递的参数?
    • 一行代码实现 Activity 之间传递参数?
    • 一行代码实现 Fragment 之间传递参数?
    • 一行代码实现点击事件,避免内存泄露?

    KtKit 仓库

    这篇文章主要围绕一个新库 KtKit 来介绍一些 Kotlin 技巧,正如其名 KtKit 是用 Kotlin 语言编写的工具库,包含了项目中常用的一系列工具,是 Jetpack ktx 系列的补充,涉及到了很多从 Kotlin 源码、Jetpack ktx、anko 等等知名的开源项目中学习到的技巧,包含了 Kotlin 委托属性、高阶函数、扩展函数、内联、注解的使用等等。

    如果想要使用文中的 API 需要将下列代码添加在模块级 build.gradle 文件内, 最新版本号请查看 版本记录

    implementation "com.hi-dhl:ktkit:${ktkitVersion}"

    因为篇幅原因,文章中不会过多的涉及源码分析,源码部分将会在后续的文章中分享。

    什么是 Contract,以及如何使用

    众所周知 Kotlin 是比较智能的,比如 smart cast 特性,但是在有些情况下显得很笨拙,并不是那么智能,如下所示。

    public inline fun String?.isNotNullOrEmpty(): Boolean {
    return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
    }

    fun testString(name: String?) {
    if (name.isNotNullOrEmpty()) {
    println(name.length) // 1
    }
    }

    正如你所见,只有字符串 name 不为空时,才会进入注释 1 的地方,但是以上代码却无法正常编译,如下图所示。

    编译器会告诉你一个编译错误,经过代码分析只有当字符串 name 不为空时,才会进入注释 1 的地方,但是编译器却无法正常推断出来,真的是编译器做不到吗?看看官方文档是如何解释的。

    However, as soon as these checks are extracted in a separate function, all the smartcasts immediately disappear:

    将检查提取到一个函数中, smart cast 所带来的效果都会消失

    编译器无法深入分析每一个函数,原因在于实际开发中我们可能写出更加复杂的代码,而 Kotlin 编译器进行了大量的静态分析,如果编译器去分析每一个函数,需要花费时间分析上下文,增加它的编译耗时的时间。

    如果要解决上诉问题,这就需要用到 Contract 特性,Contract 是 Kotlin 提供的非常有用的特性,Contract 的作用就是当 Kotlin 编译器没有足够的信息去分析函数的情况的时候,Contracts 可以为函数提供附加信息,帮助 Kotlin 编译器去分析函数的情况,修改代码如下所示。

    inline fun String?.isNotNullOrEmpty(): Boolean {
    contract {
    returns(true) implies (this@isNotNullOrEmpty != null)
    }

    return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
    }

    fun testString(name: String?) {
    if (name != null && name.isNotNullOrEmpty()) {
    println(name.length) // 1
    }
    }

    相比于之前的代码,在 isNotNullOrEmpty() 函数中添加了 contract 代码块即可正常编译通过,这行代码的意思就是,如果返回值是 true ,this 所指向对象就不为 null。 而在 Kotlin 标准库中大量的用到 contract 特性。 上述示例的使用可前往查看 KtKit/ProfileActivity.kt

    Kotlin 注解在项目中的使用

    contract 是 Kotlin 1.3 添加的实验性的 API,如果我们调用实验性的 API 需要添加 @ExperimentalContracts 注解才可以正常使用,但是如果添加 @ExperimentalContracts 注解,所有调用这个方法的地方都需要添加注解,如果想要解决这个问题。只需要在声明 contract 文件中的第一行添加以下代码即可。

    @file:OptIn(ExperimentalContracts::class)

    在上述示例中使用了 inline 修饰符,但是编译器会有一个黄色警告,如下图所示。

    编译器建议我们将函数作为参数时使用 Inline,Inline (内联函数) 的作用:提升运行效率,调用被 inline 修饰符的函数,会将方法内的代码段放到调用处。

    既然 Inline 修饰符可以提升运行效率,为什么还给出警告,因为 Inline 修饰符的滥用会带来性能损失,更多内容前往查看 Inline 修饰符带来的性能损失

    Inline 修饰符常用于下面的情况,编译器才不会有警告:

    • 将函数作为参数(例如:lambda 表达式)
    • 结合 reified 实化类型参数一起使用

    但是在普通的方法中,使用 Inline 修饰符,编译会给出警告,如果方法体的代码段很短,想要通过 Inline 修饰符提升性能(虽然微乎其微),可以在文件的第一行添加下列代码,可消除警告。

    @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")

    然后在使用 Inline 修饰符的地方添加以下注解,即可愉快的使用。

    @kotlin.internal.InlineOnly

    注解 @kotlin.internal.InlineOnly 的作用:

    • 消除编译器的警告
    • 修改内联函数的可见性,在编译时修改成 private
    // 未添加 InlineOnly 编译后的代码
    public static final void showShortToast(@NotNull Context $this$showShortToast, @NotNull String message) {
    ......
    Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
    }


    // 添加 InlineOnly 编译后的代码
    @InlineOnly
    private static final void showShortToast(Context $this$showShortToast, String message) {
    ......
    Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
    }

    关于注解完整的使用案例,可前往仓库 KtKit 查看。

    一行代码接受 Activity 或者 Fragment 传递的参数

    如果想要实现一行代码接受 Activity 或者 Fragment 传递的参数,可以通过 Kotlin 委托属性来实现,在仓库 KtKit 中提供了两个 API,根据实际情况使用即可。案例可前往查看 KtKit/ProfileActivity.kt

    class ProfileActivity : Activity() {
    // 方式一: 不带默认值
    private val userPassword by intent<String>(KEY_USER_PASSWORD)

    // 方式二:带默认值:如果获取失败,返回一个默认值
    private val userName by intent<String>(KEY_USER_NAME) { "公众号:ByteCode" }
    }

    一行代码实现 Activity 之间传递参数

    这个思路是参考了 anko 的实现,同样是提供了两个 API , 根据实际情况使用即可,可以传递 Android 支持的任意参数,案例可前往查看 KtKit/ProfileActivity.kt

    // API:
    activity.startActivity<ProfileActivity> { arrayOf( KEY_USER_NAME to "ByteCode" ) }
    activity.startActivity<ProfileActivity>( KEY_USER_NAME to "ByteCode" )

    // Example:
    class ProfileActivity : Activity() {
    ......
    companion object {
    ......

    // 方式一
    activity.startActivity<ProfileActivity> {
    arrayOf(
    KEY_USER_NAME to "ByteCode",
    KEY_USER_PASSWORD to "1024"
    )
    }

    // 方式二
    activity.startActivity<ProfileActivity>(
    KEY_USER_NAME to "ByteCode",
    KEY_USER_PASSWORD to "1024"
    )
    }
    }

    Activity 之间传递参数 和 并回传结果

    // 方式一
    context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE,
    KEY_USER_NAME to "ByteCode",
    KEY_USER_PASSWORD to "1024"
    )

    // 方式二
    context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE) {
    arrayOf(
    KEY_USER_NAME to "ByteCode",
    KEY_USER_PASSWORD to "1024"
    )
    }

    回传结果

    // 方式一
    setActivityResult(Activity.RESULT_OK) {
    arrayOf(
    KEY_RESULT to "success",
    KEY_USER_NAME to "ByteCode"
    )
    }

    // 方式二
    setActivityResult(
    Activity.RESULT_OK,
    KEY_RESULT to "success",
    KEY_USER_NAME to "ByteCode"
    )

    一行代码实现 Fragment 之间传递参数

    和 Activity 一样提供了两个 API 根据实际情况使用即可,可以传递 Android 支持的任意参数,源码前往查看 KtKit/LoginFragment.kt

    // API: 
    LoginFragment().makeBundle( KEY_USER_NAME to "ByteCode" )
    LoginFragment().makeBundle { arrayOf( KEY_USER_NAME to "ByteCode" ) }

    // Example:
    class LoginFragment : Fragment(R.layout.fragment_login) {
    ......
    companion object {
    ......
    // 方式一
    fun newInstance1(): Fragment {
    return LoginFragment().makeBundle(
    KEY_USER_NAME to "ByteCode",
    KEY_USER_PASSWORD to "1024"
    )
    }

    // 方式二
    fun newInstance2(): Fragment {
    return LoginFragment().makeBundle {
    arrayOf(
    KEY_USER_NAME to "ByteCode",
    KEY_USER_PASSWORD to "1024"
    )
    }
    }
    }
    }

    一行代码实现点击事件,避免内存泄露

    KtKit 提供了常用的三个 API:单击事件、延迟第一次点击事件、防止多次点击

    单击事件

    view.click(lifecycleScope) { showShortToast("公众号:ByteCode" }

    延迟第一次点击事件

    // 默认延迟时间是 500ms
    view.clickDelayed(lifecycleScope){ showShortToast("公众号:ByteCode" }

    // or
    view.clickDelayed(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

    防止多次点击

    // 默认间隔时间是 500ms
    view.clickTrigger(lifecycleScope){ showShortToast("公众号:ByteCode") }

    // or
    view.clickTrigger(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

    但是 View#setOnClickListener 造成的内存泄露,如果做过性能优化的同学应该会见到很多这种 case。

    根本原因在于不规范的使用,在做业务开发的时候,根本不会关注这些,那么如何避免这个问题呢,Kotlin Flow 提供了一个非常有用的 API callbackFlow,源码如下所示。

    fun View.clickFlow(): Flow<View> {
    return callbackFlow {
    setOnClickListener {
    safeOffer(it)
    }
    awaitClose { setOnClickListener(null) }
    }
    }

    callbackFlow 正如其名将一个 callback 转换成 flow,awaitClose 会在 flow 结束时执行。

    那么 flow 什么时候结束执行

    源码中我将 Flow 通过 lifecycleScope 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会将 Listener 置为 null,有效的避免内存泄漏,源码如下所示。

    inline fun View.click(lifecycle: LifecycleCoroutineScope, noinline onClick: (view: View) -> Unit) {
    clickFlow().onEach {
    onClick(this)
    }.launchIn(lifecycle)
    }

    收起阅读 »

    从XML到View显示在屏幕上,都发生了什么

    前言 View绘制可以说是Android开发的必备技能,但是关于View绘制的的知识点也有些繁杂。 如果我们从头开始阅读源码,往往千头万绪,抓不住要领。 目前当我们写页面时,布局都是写在XML里的,我们可以思考下:布局从XML到显示到屏幕上,都发生了什么,可...
    继续阅读 »

    前言


    View绘制可以说是Android开发的必备技能,但是关于View绘制的的知识点也有些繁杂。
    如果我们从头开始阅读源码,往往千头万绪,抓不住要领。
    目前当我们写页面时,布局都是写在XML里的,我们可以思考下:布局从XML到显示到屏幕上,都发生了什么,可以分为哪几个部分?
    我们将整个显示流程分解为以下几个部分



    1. 代码是怎么从XML转换成View的?

    2. View是怎么添加到页面上的?

    3. 在内存中View到底是怎么绘制的?

    4. View绘制完成后是怎么显示到屏幕上的?


    本文目录如下所示:


    1. XML是怎么转换成View的?


    我们都知道,在android中写布局一般是通过XML,然后通过setContentView方法配置到页面中
    看来XML转换成View就是在这个setContentView中了


    1.1 setContentView中做了什么


        public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

    可以看到resId传给了我们熟悉的LayoutInflater,看来xml转化成View就是在LayoutInflater方法中实现的了


    1.2 LayoutInflater中做了什么?


        public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    //预编译直接返回view,目前还未启用
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
    return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
    //真正将`XML`转化为`View`
    return inflate(parser, root, attachToRoot);
    } finally {
    parser.close();
    }
    }

    代码也比较简单,我们一起来分析下



    1. 首先我们需要明确,将XML转化为View牵涉到一些耗时操作,比如XML解析是一个io操作,将XML转化为View涉及到反射,这也是耗时的

    2. 我们可以看到在解析前有个tryInflatePrecompiled方法,这个方法就是希望可以在编译阶段直接预编译XML,在运行时直接返回构建好的View,看起来Google希望通过这种方式解决XML的性能问题。不过这个功能目前还没有启用,因此此方法直接返回null,目前生效的还是下面的方法

    3. 真正将XML解析为View的还是在inflate方法中,将标签名转化为View的名称,XML中的各种属性转化为AttributeSet对象,然后通过反射生成View对象


    由于篇幅原因,这里就不再粘贴inflate方法的源码了,里面主要需要注意下setFactorysetFactory2方法
    在真正进行反射前,会先调用这两个方法尝试创建一下View,而且系统开放了API,我们可以自定义解析XML方式
    这就给了我们一些面向切面编程的空间,可以利用这两个API实现换肤,替换字体,替换 View,提升View构建速度等操作
    希望进一步了解的同学可参考:探究 LayoutInflater setFactory


    1.3 小结


    XML转化为View转化为主要是通过LayoutInflator来完成的,将标签名转化为View的名称,XML中的各种属性转化为AttributeSet对象,然后通过反射生成View对象
    这个过程中存在一些耗时操作,比如解析XMLIO操作,通过反射生成View等,我们可以通过多种方式优化这个过程,比如将反向的耗时转移到编译期,有兴趣的同学可以参阅:Android "退一步"的布局加载优化


    2. View是怎么添加到页面上的?


    经过上面这步,View已经被创建出来了,但是View又是怎么添加到页面(Activity)上的呢?
    我们再来看下setContentView方法


        public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

    LayoutInflater有两个参数,第二个参数就是root,即创建出的view要被添加的父view
    所以答案也就呼之欲出了,创建出来的view被添加到了contentParent上,即R.id.content
    那么问题来了,这个R.id.content是哪来的呢?


    2.1 R.id.content从何而来?


    我们看到,setContentView开头调用了ensureSubDecor方法,一起来看下它的源码


        private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
    mSubDecor = createSubDecor();
    }
    }
    private ViewGroup createSubDecor() {
    // Now let's make sure that the Window has installed its decor by retrieving it
    ensureWindow();
    mWindow.getDecorView();

    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;

    //省略其他样式subDecor布局的实例化
    //包含 actionBar floatTitle ActionMode等样式
    subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);

    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    // 把`contentView`的id设置为android.R.id.content,把windowContentView的id设置为View.NO_ID
    windowContentView.setId(View.NO_ID);
    contentView.setId(android.R.id.content);

    //将subDecor添加到window
    mWindow.setContentView(subDecor);
    return subDecor;
    }

    可以看出,主要工作是创建subDecor并添加到window



    • 步骤一:确认windowattach(设置背景等操作)

    • 步骤二:获取DecorView,因为是第一次调用所以会installDecor(创建DecorViewwindowContentView)

    • 步骤三:从xml中实例化出subDecor布局

    • 步骤四:将subDecorcontentViewid设置为R.id.content

    • 步骤四:将subDecor添加到window


    现在我们已经知道R.id.content从何而来了,并且知道了subDecor最终会添加到window
    那么问题来了,window又是什么呢?


    2.2 window到底是什么?


    我们上文提到,我们创建的view会被添加到subDecor上,最后会被添加到window中,那么window是什么?为什么要有window?


    我们在应用中有多个页面,手机上也有多个应用,这么多页面同时只能有一个页面显示在手机上,这个时候就需要有一个机制来管理当前显示哪个页面
    于是Android在系统进程中创建了一个系统服务WindowManagerService(WMS)专门用来管理屏幕上的窗口,而View只能显示在对应的窗口上,如果不符合规定就不开辟窗口进而对应的View也无法显示



    window机制就是为了管理屏幕上的view的显示以及触摸事件的传递问题



    值得注意的事,上面的window窗口很容易混淆,Android SDK中的Window是一个抽象类,它有一个唯一实现类PhoneWindowPhoneWindow内部会持有一个DecorView(根View),它的职责就是对DecorView做一些标准化的处理,比如标题、背景、导航栏、事件中转等,很显然与我们前面所说的窗口概念不符合


    总得来说PhoneWindow只是提供些标准的UI方案,与窗口不等价
    窗口是一个抽象概念,即当前应该显示哪个页面,系统通过WindowManagerService(WMS)来管理
    关于窗口机制,想了解更加详细的同学,可参考:通俗易懂 Android视图系统的设计与实现,写得非常通俗易懂,有兴趣的可以了解下


    2.3 View什么时候真正可见?


    上面提到PhoneWindow只是提供些标准的UI方案,并不是真正的窗口
    那么我们的View到底什么时候添加到窗口上,什么时候真正对用户可见?


    #ActivityThread
    public void handleResumeActivity(...) {
    //...
    //注释1
    r.window = r.activity.getWindow();
    View decor = r.window.getDecorView();
    decor.setVisibility(View.INVISIBLE);
    ViewManager wm = a.getWindowManager();
    WindowManager.LayoutParams l = r.window.getAttributes();
    ...
    //注释2
    wm.addView(decor, l);
    ...
    }

    #ViewRootImpl.java
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
    if (mView == null) {
    //记录DecorView
    mView = view;
    //省略
    //开启View的三大流程(measure、layout、draw)
    requestLayout();
    try {
    //添加到WindowManagerService里,这里是真正添加window到底层
    //这里的返回值判断window是否成功添加,权限判断等。
    //比如用Application的context开启dialog,这里会添加不成功
    // 注释3
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
    getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
    mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
    mTempInsets);
    setFrame(mTmpFrame);
    } catch (RemoteException e) {
    }
    //省略
    //输入事件接收
    }
    }
    }



    1. 注释1处会从Activity中取出PhoneWindow,DecorView,WindowManager

    2. 注释2处调用了WindowManageraddView方法,顾名思义就是将DecorView添加至窗口当中

    3. 最后会调到ViewRootImpl中注释3处,这里才是真正的通过WMS在屏幕上开辟一个窗口,到这一步我们的View也就可以显示到屏幕上了


    可以看出,当我们打开一个Activity时,界面真正可见是在onResume之后


    2.4 Activity,PhoneWindow,View的关系



    1. Phonewindowactivity 的一个成员变量,会在Activity.attatch时初始化

    2. PhoneWindowView的容器,对DecorView做一些标准化的处理,比如标题、背景、导航栏、事件中转等

    3. Activity则提供了窗口的生命周期,屏蔽了窗口机制的复杂细节,开发者只需要基于模板方法开发即可


    如下图所示


    2.5 小结


    View添加到页面上,主要经过了这么几个过程



    • 1.启动activity

    • 2.创建PhoneWindow

    • 3.设置布局setContentView,将layoutId转化为View

    • 4.确认subDecorView的初始化,将subDecorView添加到PhoneWindow

    • 5.添加layoutId转化后的Viewandroid.R.id.content

    • 6.在onResume中将DecorViewView添加到WindowManager

    • 7.View真正显示到屏幕上了


    3. View到底是怎么绘制的?


    经过上一步,View已经添加到window上了,接下来就是View本身的绘制了
    View的绘制主要经过以下几步
    1、首先需要确定View占的空间尺寸(measure)
    2、确定了空间尺寸,就需要确定摆放在哪个位置(layout)
    3、确认了摆放位置,就需要确定在上面展示些什么东西(draw)


    这几个阶段,View已经封装了模板方法给我们,我们直接重写onMeasure,onLayout,onDraw这几个方法就好了
    而绘制的入口,就是上面ViewRootImpl.setView中的requestLayout


    3.1 requestLayout如何触发绘制


    上文说到requestLayout会触发绘制,我们一起来看下源码


    ViewRootImpl.java
    public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
    //检查是否是主线程,如果不是则直接抛出异常,ViewRootImpl创建的时候生成一个主线程引用
    //用当前线程和引用比较,如果是同一个则是主线程
    //这也是为什么在子线程对View进行更新、绘制会报错的原因
    checkThread();
    //用来标记需要进行layout
    mLayoutRequested = true;
    //绘制请求
    scheduleTraversals();
    }
    }

    void scheduleTraversals() {
    if (!mTraversalScheduled) {
    //标记一次绘制请求,用来屏蔽短时间内的重复请求
    mTraversalScheduled = true;
    //往主线程Looper队列里放同步屏障消息,用来控制异步消息的执行
    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    //放入mChoreographer队列里
    //主要是将mTraversalRunnable放入队列
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    //省略
    }
    }

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

    void doTraversal() {
    //没有取消绘制的话则开始绘制
    if (mTraversalScheduled) {
    mTraversalScheduled = false;
    //移除同步屏障
    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

    //真正开始执行measure、layout、draw等方法
    performTraversals();
    }
    }

    requestLayout中其实主要也是做了以下几件事



    1. 检查绘制的线程与View创建的线程是否是同一个线程

    2. 通过Handler同步屏障机制,保证UI绘制消息优先级是最高的

    3. mTraversalRunnable传入Choreographer,监听vsync信号。

    4. 收到vsync信号后会回调TraversalRunnable,移除同步屏障并开始真正的measure,layout,draw


    View绘制流程图如下:


    3.2 MeasureSpec分析


    在测量过程中,会传入一个MeasureSpec参数,MeasureSpec封装了View的规格尺寸参数,包括View的宽高以及测量模式。
    它的高2位代表测量模式,低30位代表尺寸。其中测量模式总共有3中。



    • UNSPECIFIED:未指定模式不对子View的尺寸进行限制。

    • AT_MOST:最大模式对应于wrap_content属性,父容器已经确定子View的大小,并且子View不能大于这个值。

    • EXACTLY:精确模式对应于match_parent属性和具体的数值,子View可以达到父容器指定大小的值。


    普通viewMeasureSpec创建规则如下


    结合这个表,我们可以一起来看一个问题


    <FrameLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/red"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <View
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/blue"/>
    </FrameLayout>

    请问这样一个布局,最后是什么颜色呢?
    答案是蓝色,并且占满屏幕


    简单来说,当我们自定义View 时,如果没有对MODE做处理,设置wrap_contentmatch_content结果其实是一样的,View 的宽高都是取父 View 的宽高
    本问题的详细解析可见:一道滴滴面试题


    3.3 小结



    1. View的绘制需要定位,测量,绘制三个步骤,为了简化自定义View的过程,官方已经提供了模板方法,我们重写相关方法即可

    2. ViewRootImpl中的requestLayout是绘制的入口,当然我们在View中调用invalidate或者requestLayout也会触发重绘

    3. 绘制过程本质上也是通过Handler发送消息,为了提高绘制消息的优先级,会开启同步屏蔽机制

    4. mTraversalRunnable传入Choreographer,监听vsync信号。注意,vsync信号注册了才会监听。

    5. 收到vsync信号后会回调TraversalRunnable,移除同步屏障并开始真正的measure,layout,draw过程

    6. 接下来就是回调各个ViewonMeasure,onLayout,onDraw过程


    4 View绘制完成后是怎么显示到屏幕上的?


    目前我们已经知道了,从XML到调用View.onDraw的过程,但是从onDraw到显示到屏幕上似乎还有些距离
    我们知道,View最后要显示在屏幕上,CPU负责计算帧数据,把计算好的数据交给GPUGPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上
    那么问题来了,canvas.draw是怎么转化成Graphic Buffer的呢?


    其大概流程如图所示:

    可以看出,这个过程还是相当复杂的,由于篇幅原因,这里就不展开了,感兴趣的同学可以参阅苍耳叔叔的系列文章:Android图形系统综述(干货篇)


    总结


    XMLView显示到屏幕上主要涉及到以下知识点



    1. Activity的启动

    2. LayoutInflater填充View的原理

    3. PhoneWindow,Activity,View的关系

    4. Android窗口机制与WindowManagerService管理窗口

    5. View的绘制流程,measure,layout,draw等与Handler同步屏障机制

    6. Android屏幕刷新机制,VSync信号监听,三级缓冲等

    7. Android图形绘制,包括SurfaceFinger工作流程,软件绘制,硬件加速等


    这篇文章其实已经比较长了,但是要完全了解从XML到显示到屏幕上的过程,还是不够详细,有很多地方只做了简述,如果有什么错误或者需要补充的地方,欢迎在评论区提出
    由于篇幅原因,有一些知识点没有写得很详细,下面列出一些更好的文章供参考:
    Android窗口机制:通俗易懂 Android视图系统的设计与实现
    Android屏幕刷新机制: “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!
    Android图形系统: Android图形系统综述(干货篇)
    Handler同步屏障机制:关于Handler同步屏障你可能不知道的问题


    参考资料


    【Android进阶】这一次把View绘制流程刻在脑子里!!
    Android Activity创建到View的显示过程



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

    Android AGP 7.0 适配,开始强制 JDK 11

    本次跟随 Arctic Fox 更新的其中一个重点就是 AGP 7.0 的调整,估计很多直接升级到 AGP 7.0 的开发者都会发现项目出现一些异常,本篇主要结合官方简单介绍 AGP 7.0 下的主要调整内容。 跳过版本 5 和 6 直接进入 AGP 7...
    继续阅读 »

    本次跟随 Arctic Fox 更新的其中一个重点就是 AGP 7.0 的调整,估计很多直接升级到 AGP 7.0 的开发者都会发现项目出现一些异常,本篇主要结合官方简单介绍 AGP 7.0 下的主要调整内容。



    跳过版本 5 和 6 直接进入 AGP 7.0.0 的原因,是为了和 Gradle 的版本 匹配,这意味着 AGP 7.x 就是和 Gradle 7.x API 一一对应。



    通过此次版本号的更改,AGP 版本号将与 Android Studio 版本号分开,不过目前的情况看 Android Studio 和 Android Gradle 插件会同时发布。一般来说使用稳定版 AGP 的项目是应该可以在较新版本的 Android Studio 中打开。



    运行 AGP 7 需要 JDK 11


    是的,不要惊讶,使用 Android Gradle plugin 7.0 构建时需要 JDK 11 才能运行 Gradle


    但是也请不必过多当心,这里的 JDK 11 是免费的 OpenJDK 11 ,并且只要你更新到 Android Studio Arctic Fox ,它是直接捆绑了 JDK 11 并将 Gradle 配置为默认使用它,所以大多数情况下,如果你本地配置正常,是可以直接使用 AGP 7.0 的升级。


    当然,你也可以手动选择配置,在 Project StructureSDK Location 栏目,可以看到 JDK 的配置位置已经被移动到 Gradle Settings



    在打开的 Gradle projects 可以看到 Gradle 对应的配置选项,并且有 Gradle JDK 等可选的参数,你可以选择自己的 Java SDK ,也可以选择 AS 自带的 JDK




    比如 Mac 下可以看到捆绑的 JDK 位置在 /Applications/Android\ Studio.app/Contents/jre/Contents/Home/bin/java





    如果是需要抛开 Android Studio 来配置运行的 AGP 时,通过会使用 JAVA_HOME 环境变量 或 -Dorg.gradle.java.home 命令行选项 设置为 JDK 11 的安装目录来升级 JDK 版本。



    Variant API stable


    新的 Variant API 现在已经是稳定的版本, 可以查看 com.android.build.api.variant 包中的新接口,以及 gradle-recipes GitHub 项目中的示例。


    作为新 Variant API 的一部分,通过 Artifacts 接口提供了许多称为 artifacts 的中间文件,如合并的清单功能,可以通过使用第三方插件和代码安全地获取和定制。



    后续将通过添加新功能和增加可用于定制的中间件的数量来继续扩展 Variant API。



    Lint 的行为变化


    改进了库依赖项的 lint


    运行 lint with checkDependencies = true 现在比以前更快了,对于库依赖的 Android App 的项目,建议使用 checkDependencies = true 的方式,和运行 ./gradlew :app:lint,这将并行分析的所有依赖模块,并且提供一份单独包含有 App 和依赖的 issues 文件。


    // build.gradle

    android {
      ...
      lintOptions {
        checkDependencies true
      }
    }

    // build.gradle.kts

    android {
      ...
      lint {
        isCheckDependencies = true
      }
    }

    Lint 任务现在可以 UP-TO-DATE


    如果模块的源和资源没有改变,则不需要再次运行该模块的 lint 分析任务,当出现这种情况时,任务的执行在 Gradle 输出中显示为“UP-TO-DATE”。


    通过此次的变换,当在带有 checkDependencies = true 的应用程序模块上运行 lint 时,只有发生更改的模块需要运行分析,因此 Lint 可以运行得更快。



    如果输入没有发生更改,则 Lint 报告任务也不需要运行,这里有一个相关的已知问题是,当 lint 任务为 UP-TO-DATE 时,没有打印到 stdout 的 lint 文本输出(问题 #191897708)。



    在动态功能模块上运行 lint


    AGP 不再支持从动态功能模块运行 lint,从相应的模块运行 lint 将在其动态功能模块上运行 lint,并将所有问题包含在应用程序的 lint 报告中。


    一个相关的已知问题 是,当 checkDependencies = true 时,从模块运行 lint 不再会检查动态功能库依赖项,除非它们也是应用程序的依赖项(问题 #191977888)。


    仅在默认 variant 上运行 lint


    运行 ./gradlew :app:lint 现在只运行默认的 variant , 在以前版本的 AGP 中,它将为所有 variants 运行 lint。


    Missing class warnings in R8 shrinker


    R8 可以更精确和更一致地处理丢失类和 -dontwarn 的选项,因此开发者应该开始针对 R8 发出的缺失类警告进行处理。


    当 R8 遇到未在 App 或其依赖项之一中定义的类引用时,它将发出警告,并显示在您的构建输出中。例如:


    R8: Missing class: java.lang.instrument.ClassFileTransformer

    此警告意味着 java.lang.instrument.ClassFileTransformer 在分析代码时找不到类定义,虽然这些经过可能存在错误,所以开发者可能希望可以忽略此警告,忽略警告的两个常见原因是:





      1. 以 JVM 为目标的库和缺少的类是 JVM 库类型(如上例所示)。




      1. 依赖项之一使用仅限编译时的 API。



    所以可以通过向文件添加 -dontwarn 规则来向 proguard-rules.pro 忽略缺少的类警告,例如:


    -dontwarn java.lang.instrument.ClassFileTransformer

    为方便起见,AGP 将生成一个包含所有可能丢失的规则的文件,将它们写入如下文件路径  app/build/outputs/mapping/release/missing_rules.txt, 这样可以方便地将规则添加到 proguard-rules.pro 文件以忽略警告。



    在 AGP 7.0 中缺少类消息将显示为警告,当然你可以通过 android.r8.failOnMissingClasses = truegradle.properties 讲他们变成 errors 。



    在 AGP 8.0 中,这些警告将变为破坏构建的 errors,你可以通过将选项添加 -ignorewarnings配置到 proguard-rules.pro 文件来保持 AGP 7.0 行为,但不建议这样做


    移除了 Android Gradle 插件构建缓存


    AGP 构建缓存已在 AGP 4.1 中删除,之前在 AGP 2.3 中引入是为了补充 Gradle 构建缓存,AGP 构建缓存被 AGP 4.1 中的 Gradle 构建缓存完全取代,这个变换其实不会影响构建时间。


    在 AGP 7.0 中 android.enableBuildCache 属性、android.buildCacheDir属性和cleanBuildCache 任务已经被删除。


    在项目中使用 Java 11


    现在可以项目中使用 Java 11 去编译你的项目代码了,开发者能够使用更新后的语言功能,例如私有接口方法、匿名类的 diamond 运输符和 lambda 参数的局部变量语法。


    要启用此功能,请设置 compileOptions 为所需的 Java 版本并设置 compileSdkVersion 为 30 或更高版本:


    // build.gradle

    android {
        compileSdkVersion 30

        compileOptions {
          sourceCompatibility JavaVersion.VERSION_11
          targetCompatibility JavaVersion.VERSION_11
        }

        // For Kotlin projects
        kotlinOptions {
          jvmTarget = "11"
        }
    }

    // build.gradle.kts

    android {
        compileSdkVersion(30)

        compileOptions {
          sourceCompatibility(JavaVersion.VERSION_11)
          targetCompatibility(JavaVersion.VERSION_11)
        }

        kotlinOptions {
          jvmTarget = "11"
        }
    }

    已知的问题


    与 1.?4.?x 的 Kotlin 多平台插件兼容


    Android Gradle 插件 7.0.0 与 Kotlin 多平台插件 1.5.0 及更高版本兼容。


    使用 Kotlin 多平台支持的项目需要更新到 Kotlin 1.5.0 才能使用 Android Gradle 插件 7.0.0。


    缺少 lint 输出


    当 lint 任务是最新的(问题 #191897708)时,文本输出没有打印到 stdout 的 lint ,此问题将在 Android Gradle 插件 7.1 中修复。


    并非所有动态功能库依赖项都经过 lint 检查


    checkDependencies = true 从应用程序模块运行 lint 时,不会检查动态功能库依赖项,除非它们也是应用程序依赖项(问题 #191977888)。


    收起阅读 »

    【Flutter 组件集录】Switch 是怎样炼成的

    一、 Switch 组件使用详解 可能有人会觉得 Switch 组件非常简单,有什么好说的呢?其实 Switch 组件源码洋洋洒洒 近千行 ,其中关于主题处理、平台适配、事件处理、动画处理、绘制处理 都有值得我们学习的地方。那么废话不多说,来一起看看 Swi...
    继续阅读 »
    一、 Switch 组件使用详解

    可能有人会觉得 Switch 组件非常简单,有什么好说的呢?其实 Switch 组件源码洋洋洒洒 近千行 ,其中关于主题处理平台适配事件处理动画处理绘制处理 都有值得我们学习的地方。那么废话不多说,来一起看看 Switch 是怎么炼成的吧。





    1. Switch 最简使用:valueonChanged

    Switch 组件的使用中注意:该组件是 StatelessWidget ,表示本身并不维护 开关状态。这也就意味着,我把只能通过 重新构建 Switch组件 来切换 开关状态 。在构建 Switch 时必须传入 valueonChanged 两个参数,其中 value 表示 Switch 开关的状态,onChanged 是状态变化回调函数。


    如下,在 _SwitchDemoState 中定义状态 _value 用于表示 Switch 开关的状态,在 _onChanged 回调中改变状态值,并 重新构建 Switch 组件,这样就能达到点击进行开关的效果。


    class SwitchDemo extends StatefulWidget {
    const SwitchDemo({Key? key}) : super(key: key);

    @override
    _SwitchDemoState createState() => _SwitchDemoState();
    }

    class _SwitchDemoState extends State<SwitchDemo> {
    bool _value = false;

    @override
    Widget build(BuildContext context) {
    return Switch(
    value: _value,
    onChanged: _onChanged,
    );
    }

    void _onChanged(bool value) {
    setState(() {
    _value = value;
    });
    }
    }

    其实这里可能很让人疑惑 Switch 为什么不自己维护 开关状态,要将改状态交由外界指定呢?既然 SwitchStatelessWidget ,为什么可以执行滑动的动画?还有 onChanged 方法又是何时触发的?带着这些问题我们来逐渐去认识这个属性而陌生的 Switch 组件。




    2. Switch 的四个主要颜色

    Switch 的构造方法中可以看出,其中定义了非常多的颜色相关属性。



    先看前四个颜色属性:



    • inactiveThumbColor 代表关闭时圆圈的颜色。

    • inactiveTrackColor 代表关闭时滑槽的颜色。




    • activeColor 代表打开时圆圈的颜色。

    • inactiveTrackColor 代表打开时滑槽的颜色。



    Switch(
    activeColor: Colors.blue,
    activeTrackColor: Colors.green,
    inactiveThumbColor: Colors.orange,
    inactiveTrackColor: Colors.pinkAccent,
    value: _value,
    onChanged: _onChanged,
    );



    3. hoverColor 、 mouseCursor 和 splashRadius

    前两个属性一般只能在桌面或web 端起作用,hoverColor 顾名思义是鼠标悬浮时,外层的大圈颜色,splashRadius 表示大圈的半径,如果不想要外圈的悬浮效果,可以将半径设为 0 。另外, mouseCursor 代表鼠标的样式,比如下面的小拳头是 SystemMouseCursors.grabbing



    Switch(
    activeColor: Colors.blue,
    activeTrackColor: Colors.green,
    inactiveThumbColor: Colors.orange,
    inactiveTrackColor: Colors.pinkAccent,
    hoverColor: Colors.blue.withOpacity(0.2),
    mouseCursor: SystemMouseCursors.grabbing,
    value: _value,
    onChanged: _onChanged,
    );

    mouseCursor 属性的类型为 MouseCursor ,其中 SystemMouseCursors 中定义了非常多的鼠标指针类型以供使用。下面给出几个效果:




















    contextMenu copy forbidden text



    5. 指定图片

    通过 activeThumbImageinactiveThumbImage 可以指定小圆中开启/关闭 时的图片。另外 onActiveThumbImageErroronInactiveThumbImageError 两个回调用于图片加载错误的监听。




    当小圆同时指定 图片颜色 属性时,会显示 图片


    Switch(
    activeColor: Colors.blue,
    activeThumbImage: AssetImage('assets/images/icon_head.png'),
    inactiveThumbImage: AssetImage('assets/images/icon_8.jpg'),
    activeTrackColor: Colors.green,
    inactiveThumbColor: Colors.orange,
    inactiveTrackColor: Colors.pinkAccent,
    hoverColor: Colors.blue.withOpacity(0.2),
    mouseCursor: SystemMouseCursors.move,
    splashRadius: 15,
    value: _value,
    onChanged: _onChanged,
    );



    6.主题相关属性: thumbColor 和 trackColor

    一些具有交互性的 Material 组件会通过有 MaterialState 枚举定义交互行为,有如下 7 个元素。


    enum MaterialState {
    hovered,
    focused,
    pressed,
    dragged,
    selected,
    disabled,
    error,
    }

    可以看出这两个成员都是 MaterialStateProperty 类型,那这种类型的对象如何创建,又有什么特点呢?


    ---->[Switch 成员声明]----
    final MaterialStateProperty<Color?>? thumbColor;
    final MaterialStateProperty<Color?>? trackColor;



    简单来说通过 MaterialStateProperty.resolveWith 方法,传入一个函数返回对应泛型数据。如下回调函数为 getThumbColor ,回调参数为 Set<MaterialState> 。也仅仅说,会根据 MaterialState 集合,来返回泛型数据。从 thumbColor 属性源码注释中可以看出,Switch 有如下四种 MaterialState



    getThumbColor 中根据 states 的情况,分别对几种状态返回不同颜色,这样 Switch 在不同的状态下,就会自动使用对应颜色。比如下面的 onChanged: null 代表 Switch 不可用,在 getThumbColor 中当为 disabled ,会返回红色。



    thumbColor 代表小圆颜色,trackColor 代表滑槽颜色,使用方式是一样的。这里可能有人会问:有三个属性可以设置小圆,那它们同时存在,优先级怎么样?结果测试发现,inactiveThumbImage 会优先显示,优先级如下:


    inactiveThumbImage > thumbColor > inactiveThumbColor > 默认 Switch 主题



    上面提到了 默认 Switch 主题 ,这里就来说一下 SwitchTheme ,它是一个 InheritedWidget,维护 SwitchThemeData 类型数据,具体内容如下:



    我们可以通过在上层嵌套 SwitchTheme 来为子树中的 Switch 指定默认样式,由于 MaterialApp 内部继承了 SwitchTheme 组件,我们可以在 theme 中指定 Switch 的主题样式。这样在指定 Switch 的相关颜色属性,就会使用默认的主题样式:






    7. Switch 的焦点: focusColor 与 autofocus

    Switch 组件是拥有焦点的,焦点相关的处理被封装在组件内部。focusColor 表示聚焦时的颜色,可被聚焦的组件有个特点:在桌面或 web 平台中可以通过 Tab 键,切换焦点。如下是六个 Switch 通过 Tab 键切换焦点的效果:



    @override
    Widget build(BuildContext context) {
    return
    Wrap(
    children: List.generate(6, (index) => Switch(
    value: _value,
    focusColor: Colors.blue.withOpacity(0.1),
    onChanged: _onChanged,
    ))
    );
    }



    8. Switch 的尺寸相关: materialTapTargetSize

    MaterialTapTargetSize 是一个枚举类型,有两个元素。该属性可以影响 Switch 的大小,如下分布是 paddedshrinkWrap 的效果。通过调试可知,默认是 padded 。下面在源码分析中会详细介绍该属性的作用。


    enum MaterialTapTargetSize {
    padded,
    shrinkWrap,
    }




    二、 挖掘 Switch 源码中的一些细节


    1. 类型 _SwitchType

    Switch 类中有一个 _SwitchType 类型成员,该成员完全被封装在 Switch 内部,我们是无法直接操作的。 _SwitchType 是只有两个元素的枚举类。


    enum _SwitchType { material, adaptive }

    ---->[Switch 成员声明]----
    final _SwitchType _switchType;

    既然是成员变量,必然会在类内部被初始化,一般来说对 成员变量 初始化的地方在 构造方法 中。如下, Switch 的普通构造 中,会将 _switchType 设为 _SwitchType.material





    一般来说,枚举对象就是为了分类处理,在 Switch#build 方法中,会根据 _switchType 的值进行不同的构建逻辑,如果是 material ,则所有的平台都使用Material风格的 Switch 。 如果是 adaptive 会根据平台的不同,使用不同的风格的 Switch 。在 androidfuchsialinuxwindows 中会使用 Material 风格;在 iOSmacOS 中会使用 Cupertino 风格。



    到这里,可能有人会问, _SwitchType 成员完全被封装在 Switch 内部,那如何设置 adaptive 类型呢?仔细查看源码可以看出 Switch 还有一个 adaptive 构造,此处会将 _switchType 设为 _SwitchType.adaptive





    2. 两种风格的 Switch 构建

    _buildCupertinoSwitch 是当模式为 adaptive 时,用于构建 iOSmacOS 平台 Switch 组件构建,可以看出其内部是通过 CupertinoSwitch 进行构建,效果如下:






    _buildMaterialSwitch 用于构建 Material 风格的 Switch 组件构建,可见其内部通过 _MaterialSwitch 组件进行构建。到这里我们就可以回答:既然 SwitchStatelessWidget ,为什么可以执行滑动的动画?因为 _MaterialSwitch 组件是 StatefulWidget ,它可以在内部改变组件状态。





    3.Switch 尺寸的确定

    从上面可以看出,两种风格的 Switch 都是通过 _getSwitchSize 获取 Size 尺寸的。如下代码中,可以看出,尺寸是通过 MaterialTapTargetSize 对象控制的。如果未指定 materialTapTargetSize 则会通过主题获取,调试可以看出,主题中 materialTapTargetSize 默认是 padded


    Size _getSwitchSize(ThemeData theme) {
    final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
    ?? theme.switchTheme.materialTapTargetSize
    ?? theme.materialTapTargetSize;
    switch (effectiveMaterialTapTargetSize) {
    case MaterialTapTargetSize.padded:
    return const Size(_kSwitchWidth, _kSwitchHeight);
    case MaterialTapTargetSize.shrinkWrap:
    return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
    }
    }

    下面分别是 paddedshrinkWrap 的调试信息,可以很清楚地看出尺寸情况。






    到这里 Switch 组件的源码就已经面面俱到了,我们可以发现,它作为一个 StatelessWidget 并不能做太多的事,只是定义了很多属性,并通过别的组件进行构建。也就是说,它本身起到平台差异的统筹、封装的作用,目的就是方便用户使用。




    4. onChanged 方法触发的时机

    通过调试可以发现,onChanged 方法 的触发是 ToggleableStateMixin#_handleTap 中触发的。如下是 buildToggleable 的源码,可以看出其中通过 GestureDetector 监听点击事件。



    _MaterialSwitchState.build 方法中,可以看到其中通过 GestureDetector 监听了水平拖拽事件,这也是为什么 Switch 可以支持拖动的原因,同时 child 属性是 buildToggleable ,也就是上面的组件,支持点击事件。这是一个很好的多事件监听的案例。





    5.动画的创建与触发

    仔细看一下滑动的过程,可以看出其中有 位移动画透明度渐变动画。 首先来说一下动画的来源:






    这些动画器都定义在 ToggleableStateMixin 中。而 _MaterialSwitchState 混入了 ToggleableStateMixin





    和隐式动画一样, _MaterialSwitchState 中的动画触发也是通过重构组件,执行 didUpdateWidget 。如果你了解隐式动画,就不难理解 Switch 的动画触发机制。



    最后,绘制是通过 _SwitchPainter 画出来的,这个画板是比较复杂的,这里就不展开了,有兴趣的可以自己研究一下。



    Switch 组件的使用方式到这里就完全介绍完毕,那本文到这里就结束了,谢谢观看,明天见~


    作者:张风捷特烈

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

    【SpringBoot + Mybatis系列】插件机制 Interceptor

    【SpringBoot + Mybatis系列】插件机制 Interceptor 在 Mybatis 中,插件机制提供了非常强大的扩展能力,在 sql 最终执行之前,提供了四个拦截点,支持不同场景的功能扩展 Executor (update, q...
    继续阅读 »



    【SpringBoot + Mybatis系列】插件机制 Interceptor



    在 Mybatis 中,插件机制提供了非常强大的扩展能力,在 sql 最终执行之前,提供了四个拦截点,支持不同场景的功能扩展



    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

    • ParameterHandler (getParameterObject, setParameters)

    • ResultSetHandler (handleResultSets, handleOutputParameters)

    • StatementHandler (prepare, parameterize, batch, update, query)


    本文将主要介绍一下自定义 Interceptor 的使用姿势,并给出一个通过自定义插件来输出执行 sql,与耗时的 case


    I. 环境准备


    1. 数据库准备


    使用 mysql 作为本文的实例数据库,新增一张表


    CREATE TABLE `money` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
    `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
    `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
    `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    KEY `name` (`name`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

    2. 项目环境


    本文借助 SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发


    pom 依赖如下


    <dependencies>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    </dependencies>

    db 配置信息 application.yml


    spring:
    datasource:
    url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password:

    II. 实例演示


    关于 myabtis 的配套 Entity/Mapper 相关内容,推荐查看之前的系列博文,这里就不贴出来了,将主要集中在 Interceptor 的实现上


    1. 自定义 interceptor


    实现一个自定义的插件还是比较简单的,试下org.apache.ibatis.plugin.Interceptor接口即可


    比如定义一个拦截器,实现 sql 输出,执行耗时输出


    @Slf4j
    @Component
    @Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    })

    public class ExecuteStatInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    // MetaObject 是 Mybatis 提供的一个用于访问对象属性的对象
    MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
    BoundSql sql = statement.getBoundSql(invocation.getArgs()[1]);

    long start = System.currentTimeMillis();
    List<ParameterMapping> list = sql.getParameterMappings();
    OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
    List<Object> params = new ArrayList<>(list.size());
    for (ParameterMapping mapping : list) {
    params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
    }
    try {
    return invocation.proceed();
    } finally {
    System.out.println("------------> sql: " + sql.getSql() + "\n------------> args: " + params + "------------> cost: " + (System.currentTimeMillis() - start));
    }
    }

    @Override
    public Object plugin(Object o) {
    return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
    }

    注意上面的实现,核心逻辑在intercept方法,内部实现 sql 获取,参数解析,耗时统计


    1.1 sql 参数解析说明


    上面 case 中,对于参数解析,mybatis 是借助 Ognl 来实现参数替换的,因此上面直接使用 ognl 表达式来获取 sql 参数,当然这种实现方式比较粗暴


    // 下面这一段逻辑,主要是OGNL的使用姿势
    OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
    List<Object> params = new ArrayList<>(list.size());
    for (ParameterMapping mapping : list) {
    params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
    }

    除了上面这种姿势之外,我们知道最终 mybatis 也是会实现 sql 参数解析的,如果有分析过源码的小伙伴,对下面这种姿势应该比较熟悉了


    源码参考自: org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters


    BoundSql sql = statementHandler.getBoundSql();
    DefaultParameterHandler handler = (DefaultParameterHandler) statementHandler.getParameterHandler();
    Field field = handler.getClass().getDeclaredField("configuration");
    field.setAccessible(true);
    Configuration configuration = (Configuration) ReflectionUtils.getField(field, handler);
    // 这种姿势,与mybatis源码中参数解析姿势一直
    //
    MetaObject mo = configuration.newMetaObject(sql.getParameterObject());
    List<Object> args = new ArrayList<>();
    for (ParameterMapping key : sql.getParameterMappings()) {
    args.add(mo.getValue(key.getProperty()));
    }

    但是使用上面这种姿势,需要注意并不是所有的切点都可以生效;这个涉及到 mybatis 提供的四个切点的特性,这里也就不详细进行展开,在后面的源码篇,这些都是绕不过去的点


    1.2 Intercepts 注解


    接下来重点关注一下类上的@Intercepts注解,它表明这个类是一个 mybatis 的插件类,通过@Signature来指定切点


    其中的 type, method, args 用来精确命中切点的具体方法


    如根据上面的实例 case 进行说明


    @Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    })


    首先从切点为Executor,然后两个方法的执行会被拦截;这两个方法的方法名分别是query, update,参数类型也一并定义了,通过这些信息,可以精确匹配Executor接口上定义的类,如下


    // org.apache.ibatis.executor.Executor

    // 对应第一个@Signature
    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;

    // 对应第二个@Signature
    int update(MappedStatement var1, Object var2) throws SQLException;

    1.3 切点说明


    mybatis 提供了四个切点,那么他们之间有什么区别,什么样的场景选择什么样的切点呢?


    一般来讲,拦截ParameterHandler是最常见的,虽然上面的实例是拦截Executor,切点的选择,主要与它的功能强相关,想要更好的理解它,需要从 mybatis 的工作原理出发,这里将只做最基本的介绍,待后续源码进行详细分析



    • Executor:代表执行器,由它调度 StatementHandler、ParameterHandler、ResultSetHandler 等来执行对应的 SQL,其中 StatementHandler 是最重要的。

    • StatementHandler:作用是使用数据库的 Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。

    • ParameterHandler:是用来处理 SQL 参数的。

    • ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。


    借用网上的一张 mybatis 执行过程来辅助说明




    原文 blog.csdn.net/weixin_3949…



    2. 插件注册


    上面只是自定义插件,接下来就是需要让这个插件生效,也有下面几种不同的姿势


    2.1 Spring Bean


    将插件定义为一个普通的 Spring Bean 对象,则可以生效


    2.2 SqlSessionFactory


    直接通过SqlSessionFactory来注册插件也是一个非常通用的做法,正如之前注册 TypeHandler 一样,如下


    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setMapperLocations(
    // 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
    new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
    // 注册typehandler,供全局使用
    bean.setTypeHandlers(new Timestamp2LongHandler());
    bean.setPlugins(new SqlStatInterceptor());
    return bean.getObject();
    }

    2.3 xml 配置


    习惯用 mybatis 的 xml 配置的小伙伴,可能更喜欢使用下面这种方式,在mybatis-config.xml全局 xml 配置文件中进行定义


    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE configuration
    PUBLIC "-//ibatis.apache.org//DTD Config 3.1//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">

    <configuration>
    <settings>
    <!-- 驼峰下划线格式支持 -->
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
    <package name="com.git.hui.boot.mybatis.entity"/>
    </typeAliases>

    <!-- type handler 定义 -->
    <typeHandlers>
    <typeHandler handler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>
    </typeHandlers>

    <!-- 插件定义 -->
    <plugins>
    <plugin interceptor="com.git.hui.boot.mybatis.interceptor.SqlStatInterceptor"/>
    <plugin interceptor="com.git.hui.boot.mybatis.interceptor.ExecuteStatInterceptor"/>
    </plugins>
    </configuration>

    3. 小结


    本文主要介绍 mybatis 的插件使用姿势,一个简单的实例演示了如果通过插件,来输出执行 sql,以及耗时


    自定义插件实现,重点两步



    • 实现接口org.apache.ibatis.plugin.Interceptor

    • @Intercepts 注解修饰插件类,@Signature定义切点


    插件注册三种姿势:



    • 注册为 Spring Bean

    • SqlSessionFactory 设置插件

    • myabtis.xml 文件配置



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

    搞懂Objective-C中的ARC

    写这篇文章的背景前段时间招人,面试了一个多月,有关内存的基础问题,能完全答出来的竟无一人,回答出百分之80的人也寥寥无几,于是决定写这篇文章,简单业务流水线道友们一般都能写出符合需求,可以正常工作的代码,稍微复杂点的也许也不再话下,一旦涉及到性能、鲁棒性等要求...
    继续阅读 »

    写这篇文章的背景

    前段时间招人,面试了一个多月,有关内存的基础问题,能完全答出来的竟无一人,回答出百分之80的人也寥寥无几,于是决定写这篇文章,简单业务流水线道友们一般都能写出符合需求,可以正常工作的代码,稍微复杂点的也许也不再话下,一旦涉及到性能、鲁棒性等要求很高的项目,不能真正理解内存的程序员将给整个项目带来灾难和隐藏的坑,所以本文旨在让道友们真正理解内存,这是再基础不过的东西,然而又是必须知道的东西,让我们一起,重温下基础吧,本文不打算大量罗列源码,而是从显而易见的东西开始

    先从一个小问题开始

    面试官:alloc的对象都存储在堆上是吗?
    候选人:是的
    面试官:好的,静态变量存储在数据段是吗?
    候选人:是的,未初始化的存储在bss段,初始化的存储在data段
    面试官:很好,不错,看一段代码,这两行代码可以写成一行吗

    static NSObject *obj = nil;
    obj = [[NSObject alloc] init];

    像这样:

    static NSObject *obj = [[NSObject alloc] init];
    候选人:应该可以吧(get不到问题的点)
    面试官:那内存是如何分布的呢?
    候选人:不可能同时存储在数据段和堆区吧(小声嘀咕)
    面试官:顺着这个思路再思考下
    候选人:。。。(过去三分钟)
    面试官:好的,换种问法,单例很常见吧,那么它可以手动释放吗?
    候选人:既然用单例来实现,说明整个程序生命周期需要共享一个实例,不会存在需要释放的场景
    面试官:比如一个app里面有很多业务线,业务线退出的时候,需要清理业务线所占内存,如若有单例存在,这个时候可以手动释放吗?
    候选人:把指针置为nil?,应该可以吧(试图得到面试官的提示)
    面试官:那这以后呢,单例就不占用内存了吗?
    候选人:。。。(彻底卡住)

    Objective-C中的指针

    可曾听过一句话,一切OC对象皆指针,嗯~这句话很对,我想开讲之前有必要说下究竟OC中的指针是什么,对象又是什么,它们不是一个东西嘛?iOS操作系统是基于unix的一个分支开发的,自然继承了unix内核的部分功能,内存分区为:栈、堆、数据段、常量区、代码段,本文不打算枯燥的讲理论,我们设计几个demo直接从表象出发,去探寻理论,会不会记忆更深刻呢?!

    还是从上面的代码出发

    static NSObject *obj = [[NSObject alloc] init];

    这样是无法通过编译的,编译器提示Initializer element is not a compile-time constant,意思是初始化的元素不是编译期分配的常量,obj指针即是分配在数据段的变量,在编译时就需要分配内存,alloc的对象内存开辟在堆上并且是运行时分配的,用运行时的对象去初始化编译期的指针是没有办法做到的,所以编译器提示我们这样做是不对的
    以上得出个结论:

    • 栈、堆内存是运行时分配的
    • 数据段内存是编译时分配的(这么说并不完全准确,往下看)

    (注:app的可执行文件二进制里面包括静态库和源码,动态库和资源文件会单独存储,系统的动态库整个操作系统共享一份)
    注意我们讲的是内存,我们的可执行程序是以二进制的形式存在于手机上的,这里说的代码段并非用于存储二进制文件,而是存储程序启动时候被载入内存中的可执行代码,紧随其后,操作系统会为程序中的全局变量和静态变量在数据段开辟内存(起初会存储在bss段,初始化后会清空bss段,存储在data段),常量的内存空间开辟和初始化是一起执行的,初始化后不再有机会改变,所以准确的说:

    • 代码段内存是装载时分配的,数据段和常量区紧随其后,这些都发生在动态链接之前

    看一个demo:环境是x86模拟器,嗯~64位架构

    @interface Person : NSObject

    @property (nonatomic, assign) int a;
    @property (nonatomic, assign) int b;
    @property (nonatomic, assign) int c;

    @end
    Person *obj = nil;
    NSLog(@"%lu", sizeof(obj));
    NSLog(@"%lu", malloc_size((__bridge const void *)obj));
    obj = [[Person alloc] init];
    NSLog(@"%lu", malloc_size((__bridge const void *)obj));
    NSLog(@"%lu", class_getInstanceSize(Person.class));

    2021-06-05 16:41:36.982538+0800 test[69294:38540223] 8
    2021-06-05 16:41:36.982640+0800 test[69294:38540223] 0
    2021-06-05 16:41:36.982712+0800 test[69294:38540223] 32
    2021-06-05 16:41:36.982772+0800 test[69294:38540223] 24

    首先我们看Person的实例对象有哪些成员需要在堆上开辟内存空间,一个isa指针8个字节,三个int类型变量12个字节,总共20个字节
    控制台输出sizeof是8,证明指针本身在64位系统占用8个字节,紧接着malloc_size输出0,证明只是一个指向nil的指针,还没有在堆区分配内存,malloc_size然后输出32证明在堆区开辟了32字节的内存,16字节为一个开辟单元是iOS系统的规范,所以要想存储20个字节就需要开辟两个单元的大小,就是32字节,最后class_getInstanceSize输出24证明对象实际占用24字节,是因为iOS系统内存存储是按照8字节对齐的,所以20个字节之后需要补齐4个字节的0用于内存对齐,无论是开辟空间对齐,还是存储对齐都是操作系统设计之初的效率考虑

    好的~我们回到上面说的单例释放问题,是否可手动释放呢?答案是部分可以,部分不能,原因是,堆栈的内存动态分配,动态释放,而数据段、常量区、代码段内存直到app进程退出才会释放,所以单例指针置为nil的时候,堆区对象的引用计数为0会自动释放,而还有一个指针存储在数据段,占用8个字节

    什么是ARC,引用计数存储在哪里,哪些对象是通过引用计数来管理内存的

    面试官:如下代码在MRC环境会有内存泄漏,为什么?

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *obj = [[NSObject alloc] init];
    }
    候选人:因为obj没有调用release或者autorelease
    面试官:嗯,那还是MRC环境,下面的代码会泄露吗?

    - (void)viewDidLoad {
    [super viewDidLoad];
    static NSObject *obj = nil;
    obj = [[NSObject alloc] init];
    }

    候选人:嗯~~~会吧
    面试官:你怎样理解内存泄漏,什么叫内存泄漏
    候选人:就是一个对象,没有释放掉,就泄露了
    面试官:啊~~~,那么能在ARC环境举个内存泄漏的例子吗
    候选人:比如block是self的属性,然后里面引用了self,没有加__weak
    面试官:这个是循环引用吧,所以循环引用会内存泄漏是吗
    候选人:是的(over)

    只有堆区对象才有引用计数,引用计数存储在对象本身的结构里,嗯~可以通过isa指针辗转访问到
    ARC是自动引用计数,即编译器在编译期在合适的位置自动插入release或是autorelease

    回到上面问题

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *obj = [[NSObject alloc] init];
    }

    MRC下如上代码泄漏的根本原因是,obj是声明在栈上的指针,作用域之外自动释放,即大括号之外指针已经不存在了,但是alloc的对象引用计数是1,但是已经没有指针引用它了,所以这块堆内存将没有机会释放了,这就是内存泄漏

    那么下面的代码在MRC下为什么就没有泄漏呢

    - (void)viewDidLoad {
    [super viewDidLoad];
    static NSObject *obj = nil;
    obj = [[NSObject alloc] init];
    }

    原因是这个obj指针声明在数据段,生命周期和app进程生命周期一致,虽然alloc的对象引用计数也始终为1,但是有个static指针一直引用它,所以这块堆内存没有泄漏

    以上明确几个常见内存问题概念:

    • 野指针:堆内存已经释放,但是还有指针指向这块内存,就是野指针,访问野指针crash
    • 内存泄漏:堆区内存引用计数不为0,但是没有指针指向这块内存,内存碎片
    • 循环引用:堆内存之间存在相互强引用,并且没有第三种力量打破这个环,内存碎片
    • OOM:堆内存开辟大小不固定,超过系统的限制,crash
    • 栈溢出:栈内存大小是固定的,超过系统限制,crash

    ARC下哪些对象是autorelease对象

    面试官:ARC下除了__autoreleasing显式创建autorelease对象的方式,还有哪些情况会生成autorelease对象
    候选人:alloc和new出来的对象都会加入默认的autoreleasePool中,所以都是autorelease对象
    面试官:哦?那ARC下release关键字是被弃用了吗?
    候选人:是的(斩钉截铁)

    这么回答的人占3成,有些恐怖~,我们还是通过一个demo来探索下,__autoreleasing显式的创建autorelease对象比较明显,我们来聊下隐式的情况


    __weak NSString *weak_String;
    __weak NSString *weak_StringRelease;
    __weak NSString *weak_StringAutorelease;

    - (void)testArc {
    [self createString];
    NSLog(@"------%s------", __func__);
    NSLog(@"%@", weak_String);
    NSLog(@"%@\n\n", weak_StringRelease);
    NSLog(@"%@\n\n", weak_StringAutorelease);
    }

    - (void)createString {
    NSString *constAreaString = @"字面量string";
    NSString *heapAreastring = [[NSString alloc] initWithFormat:@"堆区string-release"];
    NSString *stringAutorelease = [NSString stringWithFormat:@"堆区string-autorelease"];
    NSLog(@"%lu", malloc_size((__bridge const void *)constAreaString));
    NSLog(@"%lu", malloc_size((__bridge const void *)heapAreastring));
    NSLog(@"%lu", malloc_size((__bridge const void *)stringAutorelease));

    weak_String = constAreaString;
    weak_StringRelease = heapAreastring;
    weak_StringAutorelease = stringAutorelease;

    NSLog(@"------%s------", __func__);
    NSLog(@"%@", weak_String);
    NSLog(@"%@\n\n", weak_StringRelease);
    NSLog(@"%@\n\n", weak_StringAutorelease);
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    [self testArc];
    }

    - (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"------%s------", __func__);
    NSLog(@"%@", weak_String);
    NSLog(@"%@\n\n", weak_StringRelease);
    NSLog(@"%@\n\n", weak_StringAutorelease);
    }

    结果如下:

    2021-06-06 00:25:44.981838+0800 test[81234:39339960] 0
    2021-06-06 00:25:44.981936+0800 test[81234:39339960] 64
    2021-06-06 00:25:44.982022+0800 test[81234:39339960] 64
    2021-06-06 00:25:44.982116+0800 test[81234:39339960] -------[ViewController createString]------
    2021-06-06 00:25:44.982213+0800 test[81234:39339960] 字面量string
    2021-06-06 00:25:44.982278+0800 test[81234:39339960] 堆区string-release
    2021-06-06 00:25:44.982357+0800 test[81234:39339960] 堆区string-autorelease
    2021-06-06 00:25:44.982428+0800 test[81234:39339960] -------[ViewController testArc]------
    2021-06-06 00:25:44.982508+0800 test[81234:39339960] 字面量string
    2021-06-06 00:25:44.982578+0800 test[81234:39339960] (null)
    2021-06-06 00:25:44.982656+0800 test[81234:39339960] 堆区string-autorelease
    2021-06-06 00:25:44.992637+0800 test[81234:39339960] -------[ViewController viewDidAppear:]------
    2021-06-06 00:25:44.992753+0800 test[81234:39339960] 字面量string
    2021-06-06 00:25:44.992830+0800 test[81234:39339960] (null)
    2021-06-06 00:25:44.992899+0800 test[81234:39339960] (null)
    首先看字面量的方式创建的字符串malloc_size为0,说明它不在堆上,嗯~在常量区,另外两个malloc_size都是正数,证明是堆区对象,createString函数三个对象都有值,而当testArc的时候weak_StringRelease的值已经为空,即离开了createString函数的作用域就释放了,此时weak_StringAutorelease还有值,直到viewDidAppear的时候只有字面量创建的对象才能够打印出来,这个结果说明了什么呢,说明编译器做了如下优化:

    - (void)createString {
    //这行类型变成了__NSCFConstantString
    __NSCFConstantString *constAreaString = @"字面量string";
    NSString *heapAreastring = [[NSString alloc] initWithFormat:@"堆区string-release"];
    //这行在末尾插入了autorelease
    NSString *stringAutorelease = [[NSString stringWithFormat:@"堆区string-autorelease"] autorelease];
    NSLog(@"%lu", malloc_size((__bridge const void *)constAreaString));
    NSLog(@"%lu", malloc_size((__bridge const void *)heapAreastring));
    NSLog(@"%lu", malloc_size((__bridge const void *)stringAutorelease));

    weak_String = constAreaString;
    weak_StringRelease = heapAreastring;
    weak_StringAutorelease = stringAutorelease;

    NSLog(@"------%s------", __func__);
    NSLog(@"%@", weak_String);
    NSLog(@"%@\n\n", weak_StringRelease);
    NSLog(@"%@\n\n", weak_StringAutorelease);

    //在作用域末尾插入了release
    [heapAreastring release];
    }

    注意关键点,有三处变化__NSCFConstantString *constAreaString、[[NSString stringWithFormat:@"堆区string-autorelease"] autorelease]、[heapAreastring release];

    • 字面量创建的直接存储在常量区
    • alloc出来的存储在堆区并且作用域结束前直接插入release
    • 通过stringWithFormat工厂方法创建的对象则在其后插入autorelease,这是因为工厂方法里面通过alloc分配堆内存,到返回出来以后其作用域已经结束,所以只能延迟释放了,否则没有办法返回非空对象

    同样是这个demo把字符串的长度缩短,结果会很不一样

    NSString *constAreaString = @"字面量";
    NSString *heapAreastring = [[NSString alloc] initWithFormat:@"release"];
    NSString *stringAutorelease = [NSString stringWithFormat:@"autorelease"];

    2021-06-06 00:51:27.827647+0800 test[82761:39451059] 0
    2021-06-06 00:51:27.827750+0800 test[82761:39451059] 0
    2021-06-06 00:51:27.827843+0800 test[82761:39451059] 0
    2021-06-06 00:51:27.827941+0800 test[82761:39451059] -------[ViewController createString]------
    2021-06-06 00:51:27.828040+0800 test[82761:39451059] 字面量
    2021-06-06 00:51:27.828139+0800 test[82761:39451059] release
    2021-06-06 00:51:27.828224+0800 test[82761:39451059] autorelease
    2021-06-06 00:51:27.828292+0800 test[82761:39451059] -------[ViewController testArc]------
    2021-06-06 00:51:27.828369+0800 test[82761:39451059] 字面量
    2021-06-06 00:51:27.828444+0800 test[82761:39451059] release
    2021-06-06 00:51:27.828535+0800 test[82761:39451059] autorelease
    2021-06-06 00:51:27.838554+0800 test[82761:39451059] -------[ViewController viewDidAppear:]------
    2021-06-06 00:51:27.838684+0800 test[82761:39451059] 字面量
    2021-06-06 00:51:27.838765+0800 test[82761:39451059] release
    2021-06-06 00:51:27.838853+0800 test[82761:39451059] autorelease
    我们发现他们已经都不在堆区了,而是存储在常量区,这是一项优化叫做NSTagged Pointer,即指针和对象存储在一起,这项技术是苹果公司对小对象做的优化NSString、NSNumber、NSDate。所以上述代码经过编译器的优化,就变成了下面这样

    __NSCFConstantString *constAreaString = @"字面量";
    NSTaggedPointerString *heapAreastring = [[NSString alloc] initWithFormat:@"release"];
    NSTaggedPointerString *stringAutorelease = [[NSString stringWithFormat:@"autorelease"];

    以上结论:

    • 字面量创建的直接存储在常量区
    • alloc出来的存储在堆区并且作用域结束前直接插入release(符合NSTagged Pointer的会直接分配在常量区,类型是NSTaggedPointer_接类型名,标识指针和对象存储在一起)
    • 通过stringWithFormat工厂方法创建的对象则在其后插入autorelease,这是因为工厂方法里面通过alloc分配堆内存,到返回出来以后其作用域已经结束,所以只能延迟释放了,否则没有办法返回非空对象(符合NSTagged Pointer的会直接分配在常量区,类型是NSTaggedPointer_接类型名,标识指针和对象存储在一起)

    objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue的纠正

    值得一提的是,即便编译器插入autorelease关键字,也不一定会将这个对象放入autoreleasePool,为了减轻autoreleasePool的负担,苹果做了一项优化,objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue,这里不分析源码,直接给出上层的解释,如下,对象在加入autoreleasePool之前会调用objc_autoreleaseReturnValue,这个方法会检测后面串行的代码是否调用了objc_retainAutoreleasedReturnValue(就是一次引用计数+1的操作),如果有则不加入autoreleasePool,直接返回对象,❌引用计数不会+1并且在当前线程存储区域做个标记,待到执行到objc_retainAutoreleasedReturnValue的时候检测标志位,如在优化流程中则直接返回对象并且重置标志位,否则加入autoreleasePool,❌引用计数+1

    注意:上面一段话❌部分都是错误的理解,实际上引用计数在对象初始化后就已经存在,是对象相关联的东西,+1与否和自动释放池没有半点关系,-1与否才有关系

    错误的理解如下:
    优化前

    id obj = objc_msgSend(objc_msgSend(NSMutableString, @selector(string)));
    objc_autorelease(obj);
    objc_retain(obj);
    // 这里引用计数为2
    objc_release(obj);

    优化后

    id obj = objc_msgSend(objc_msgSend(NSMutableString, @selector(string)));
    // 这里引用计数为1
    objc_release(obj);

    而实际上呢:

    NSMutableString *str = [NSMutableString string];
    NSMutableString *strRetain = str;
    NSLog(@"%li", CFGetRetainCount((__bridge CFTypeRef)str));

    优化后retainCount的结果是2

    021-06-07 00:53:55.610395+0800 test[92513:40083306] 2

    所以结论是:

    编译器优化后,会在执行到objc_retainAutoreleasedReturnValue的时候,不会将对象加入autoreleasePool,而是在这次引用计数+1操作之后作用域结束之前再加入一个release操作


    对象是何时被加入autoreleasepool的

    运行时,对象在调用autorelease的时候就开始检测后续串行代码是否有引用计数加1操作,没有的话就会直接调用AutoreleasePoolPage的add函数添加到双向链表
    源码级别的回答:

    id *add(id obj)
    {
    ASSERT(!full());
    unprotect();
    id *ret = next; // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
    }

    在NSObject.mm可以查到源码,AutoreleasePoolPage定义了一系列工具函数,其中添加到autoreleasePool中的操作是add函数,这个函数的调用栈如下:

    • add
    • autoreleaseFast
    • autorelease
    • objc_autorelease
    • objc_autoreleaseReturnValue

    程序的最小执行流是线程,iOS系统为每个线程定义了一系列的数据结构,在线程初始化的时候就初始化相关结构,一个栈、一个autoreleasepool、一个runloop还有一个线程局部存储区域(很小),运行时执行到autorelease语句的时候,会优先检测对象是否符合NSTaggedPointer,如果符合就抛出异常,证明程序不应该进入autorelease环节,如果不符合就往下走流程,调用objc_autoreleaseReturnValue函数,优先进行检测后续串行代码是否调用了objc_retainAutoreleasedReturnValue函数,如果没有调用,就会直接调用add函数添加到双向链表,如果有引用计数+1操作,则会把一个标记存储在TLS(线程局部存储),待到执行到objc_retainAutoreleasedReturnValue的时候检测标志位,如在优化流程中则直接返回对象并且重置标志位



    作者:野码道人
    链接:https://www.jianshu.com/p/ed84101e0efe
    收起阅读 »

    iOS 控制器生命周期

    1,单个viewController的生命周期①,initWithCoder:(NSCoder *)aDecoder:(如果使用storyboard或者xib)②,loadView:加载view③,viewDidLoad:view加载完毕④,viewWillA...
    继续阅读 »

    1,单个viewController的生命周期

    ①,initWithCoder:(NSCoder *)aDecoder:(如果使用storyboard或者xib)

    ②,loadView:加载view

    ③,viewDidLoad:view加载完毕

    ④,viewWillAppear:控制器的view将要显示

    ⑤,viewWillLayoutSubviews:控制器的view将要布局子控件

    ⑥,viewDidLayoutSubviews:控制器的view布局子控件完成

    这期间系统可能会多次调用viewWillLayoutSubviews 、 viewDidLayoutSubviews 俩个方法

    ⑦,viewDidAppear:控制器的view完全显示

    ⑧,viewWillDisappear:控制器的view即将消失的时候

    这期间系统也会调用viewWillLayoutSubviews 、viewDidLayoutSubviews 两个方法

    ⑨,viewDidDisappear:控制器的view完全消失的时候

    ⑩,didReceiveMemoryWarning(内存满时)

    当程序发出一个内存警告--->

    系统询问控制器有View吗--->如果有View

    系统询问这个View能够销毁吗---->通过判断View是否在Windown上面,如果不在,就表示可以销毁

    如果可以销毁,就执行viewWillUnLoad()----->对你的View进行一次release,此时View就为nil

    然后调用viewDidUnLoad()----->一般还会在这个方法里将一些不需要属性清空

    2,多个viewControllers跳转

    当我们点击push的时候首先会加载下一个界面然后才会调用界面的消失方法

    initWithCoder:(NSCoder *)aDecoder:ViewController2(如果用xib创建的情况下)

    loadView:ViewController2

    viewDidLoad:ViewController2

    viewWillDisappear:ViewController1将要消失

    viewWillAppear:ViewController2将要出现

    viewWillLayoutSubviewsViewController2

    viewDidLayoutSubviewsViewController2

    viewWillLayoutSubviews:ViewController1

    viewDidLayoutSubviews:ViewController1

    viewDidDisappear:ViewController1完全消失

    viewDidAppear:ViewController2完全出现

    3,相关解释

    ①,loadView()

    若控制器有关联的 Nib 文件,该方法会从 Nib 文件中加载 view;如果没有,则创建空白 UIView 对象。

    自定义实现不应该再调用父类的该方法。

    ②,viewDidLoad()

    view 被加载到内存后调用viewDidLoad()。

    重写该方法需要首先调用父类该方法。

    该方法中可以额外初始化控件,例如添加子控件,添加约束。

    该方法被调用意味着控制器有可能(并非一定)在未来会显示。

    在控制器生命周期中,该方法只会被调用一次。

    ③,viewWillAppear()

    该方法在控制器 view 即将添加到视图层次时以及展示 view 时所有动画配置前被调用。

    重写该方法需要首先调用父类该方法。

    该方法中可以进行操作即将显示的 view,例如改变状态栏的取向,类型。

    该方法被调用意味着控制器将一定会显示。

    在控制器生命周期中,该方法可能会被多次调用。

    注意:

    如果A控制器present到B控制器,B控制器dismiss的时候,A的viewWillAppear不会被调用

    ④,viewWillLayoutSubviews()

    该方法在通知控制器将要布局 view 的子控件时调用。

    每当视图的 bounds 改变,view 将调整其子控件位置。

    该方法可重写以在 view 布局子控件前做出改变。

    该方法的默认实现为空。

    该方法调用时,AutoLayout 未起作用。

    在控制器生命周期中,该方法可能会被多次调用。

    ⑤,viewDidLayoutSubviews()

    该方法在通知控制器已经布局 view 的子控件时调用。

    该方法可重写以在 view 布局子控件后做出改变。

    该方法的默认实现为空。

    该方法调用时,AutoLayout 已经完成。

    在控制器生命周期中,该方法可能会被多次调用。

    ⑥,viewDidAppear()

    该方法在控制器 view 已经添加到视图层次时被调用。

    重写该方法需要首先调用父类该方法。

    该方法可重写以进行有关正在展示的视图操作。

    在控制器生命周期中,该方法可能会被多次调用。

    ⑦,viewWillDisappear()

    该方法在控制器 view 将要从视图层次移除时被调用。

    类似 viewWillAppear()。

    该方法可重写以提交变更,取消视图第一响应者状态。

    ⑧,viewDidDisappear()

    该方法在控制器 view 已经从视图层次移除时被调用。

    类似 viewDidAppear()

    该方法可重写以清除或隐藏控件。

    ⑨,didReceiveMemoryWarning()

    当内存预警时,该方法被调用。

    不能直接手动调用该方法。

    该方法可重写以释放资源、内存。

    ⑩,deinit

    控制器销毁时(离开堆),调用该方法。

    可以移除通知,调试循环测试

    总结:

    当屏幕旋转,view 的 bounds 改变,其内部的子控件也需要按照约束调整为新的位置,因此也调用了 viewWillLayoutSubviews() 和 viewDidLayoutSubviews()。

    当在一个控制器内 Present 新的控制器,原先的控制器并不会销毁,但会消失,因此调用了viewWillDisappear和viewDidDisappear方法。

    如果新的控制器 Dismiss,即清除自己,原先的控制器会再一次出现,因此调用了其中的viewWillAppear和viewDidAppear方法。

    若 loadView() 没有加载 view,viewDidLoad() 会一直调用 loadView() 加载 view,因此构成了死循环,程序即卡死。原文

    附加面试题:load和initialize的区别

    ******:load是只要类所在的文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会调用load方法,即使类文件被引用进来,如果没有使用,那么initialize不会被调用。

    调用方式
    1、load是根据函数地址直接调用
    2、initialize是通过objc_msgSend调用
    调用时刻
    1、load是runtime加载类、分类的时候调用(只会调用一次)
    2、initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次)

    load和initializee的调用顺序

    1、load:
    先调用类的load, 在调用分类的load
    先编译的类, 优先调用load, 调用子类的load之前, 会先调用父类的load
    先编译的分类, 优先调用load

    2、initialize
    先初始化分类, 后初始化子类
    通过消息机制调用, 当子类没有initialize方法时, 会调用父类的initialize方法, 所以父类的initialize方法会调用多次


    作者:大宝的爱情
    链接:https://www.jianshu.com/p/bd2197d5e547



    收起阅读 »