注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android Studio中的 Image Asset Studio(图标生成工具)

Android 图标在线生成Android Studio 包含一个名为 Image Asset Studio 的工具,它可以帮我们把自定义图像、系统图标素材、文本字符串自动生成适配系统的应用图标。它为你的应用程序支持的每个像素密度生成一组适当分辨率的图标。Im...
继续阅读 »

Android 图标在线生成

Android Studio 包含一个名为 Image Asset Studio 的工具,它可以帮我们把自定义图像系统图标素材文本字符串自动生成适配系统的应用图标。它为你的应用程序支持的每个像素密度生成一组适当分辨率的图标。Image Asset Studio 将新生成的图标放置res/在项目目录下的特定文件夹中(例如 mipmap/ 或 drawable/)。在运行时,Android 根据运行应用的设备的屏幕密度使用适当的资源。

Image Asset Studio 可帮助您生成以下图标类型:

  • 启动图标(Launcher icons)

    • Launcher Icons(Adaptive and Legacy):AS 3.0后新增,用于自适应启动图标,兼容新旧版系统;
    • Launcher Icons(Legacy only):用于非自适应的启动图标,仅限旧版系统(Android 8.0之前);
  • 操作栏和选项卡图标(Action bar and tab icons)

  • 通知图标(Notification icons)

  • TV Banners

  • TV Channel lcons

Image Asset 是什么

Image Asset Studio 可帮助您创建不同密度的各种类型的图标,并准确显示它们在项目中的放置位置。以下部分描述了您可以创建的图标类型以及您可以使用的图像和文本输入。

Launcher icons

Image Asset Studio 将启动图标放置在目录中的适当位置res/mipmap-density/。它还创建了适合 Google Play 商店的 512 x 512 像素图像。

Action bar and tab icons

Image Asset Studio 将图标放置在res/drawable-density/目录中的适当位置 。

我们建议操作栏和选项卡图标使用 Material Design 风格。作为 Image Asset Studio 的替代方案,您可以使用 Vector Asset Studio创建操作栏和选项卡图标。矢量绘图适用于简单的图标,可以减少应用程序的大小。

Notification icons

通知是您可以在应用程序的正常 UI 之外向用户显示的消息。Image Asset Studio 将通知图标放置在目录中的适当位置 :res/drawable-density/

  • Android 2.2(API 级别 8)及更低版本的图标放置在目录中。res/drawable-density/
  • Android 2.3 到 2.3.7(API 级别 9 到 10)的图标放置在 目录中。res/drawable-density-v9/
  • Android 3(API 级别 11)及更高版本的图标放置在目录中。res/drawable-density-v11/
  • 如果你的应用程序支持 Android 2.3 到 2.3.7(API 级别 9 到 10),Image Asset Studio 会生成一个灰色版本的图标。后来的 Android 版本使用 Image Asset Studio 生成的白色图标。

Clip Art(剪贴画)

Image Asset Studio 使您可以轻松导入 VectorDrawable 和 PNG 格式的 Google Material 图标:只需从对话框中选择一个图标即可。

Images(图片)

您可以导入自己的图像并根据图标类型对其进行调整。Image Asset Studio 支持以下文件类型:PNG(首选)、JPG(可接受)和 GIF(不可用)。

Text(文本)

Image Asset Studio 允许您以各种字体键入文本字符串,并将其放置在图标上。它将基于文本的图标转换为不同密度的 PNG 文件。你可以使用计算机上安装的字体。

使用 Image Asset Studio

要启动 Image Asset Studio,请按照下列步骤操作:

  • 在Project窗口中,选择 Android view。
  • 右键单击res文件夹并选择 New > Image Asset。

  • Image Asset Studio 中的自适应和旧式图标向导。

继续执行以下步骤:

  • 如果您的应用支持 Android 8.0及以上,请创建自适应和旧版启动器图标
  • 如果您的应用支持不高于 Android 7.1 的版本,请创建旧版启动器图标
  • 创建操作栏或选项卡图标。
  • 创建通知图标。

创建Launcher Icons(Adaptive and Legacy)

打开Image Asset Studio,你可以通过以下步骤添加图标:

  • Icon Type 中, 选择Launcher Icons (Adaptive and Legacy)

  • Foreground Layer选项卡中,选择Asset Type,然后在下方的字段中指定asset

    • 选择Image以指定图像文件的路径。
    • 选择Clip Art 以从Material Design 图标集中指定一个图像 。
    • 选择Text以指定文本字符串并选择字体。  文章上面有各自选择的教程
  • Background Layer选项卡中,选择Asset Type,然后在下方的字段中指定Asset。你可以选择一种颜色或指定要用作背景层的image。

  • Options选项卡中,查看默认设置并确认您要生成 Legacy、Round 和 Google Play Store 图标。

  • (可选)更改每个Foreground Layer和Background Layer选项卡的名称和显示设置:

    • Name:如果不想使用默认名称,请键入新名称。如果该资源名称已存在于项目中,如向导底部的错误所示,它将被覆盖。名称只能包含小写字符、下划线和数字
    • Trim:要调整源资产中图标图形和边框之间的边距,请选择Yes。此操作去除透明空间,同时保留纵横比。要保持源资产不变,请选择No
    • Color:要更改Clip Art or Text图标的颜色,请单击该字段。在"选择颜色"对话框中,指定一种颜色,然后单击"选择"。新值出现在该字段中。
    • Resize:使用滑块指定比例因子以调整Image, Clip Art, or Text图标的大小。当您指定颜色资源类型时,background layer的此控件将被禁用。
  • 单击Next。

  • 或者,更改资源目录:选择要添加图像资产的资源源集:src/main/res、 src/debug/res、src/release/res或自定义源集。要定义新的源集,请选择 File > Project Structure > app > Build Types. 例如,您可以定义一个 Beta 源集并创建一个图标版本,在右下角包含文本“BETA”。有关更多信息,

  • 单击Finish。Image Asset Studio 将图像添加到不同密度的 mipmap文件夹中。

创建Launcher Icons(Legacy only)

新增:

  • Scaling:要适合图标大小,请选择Crop或 Shrink to Fit。使用Crop,图像边缘可以被剪掉,而使用Shrink to Fit,则不会。如果源资产仍然不适合,您可以根据需要调整填充。
  • Shape:要在源资产后面放置背景,请选择一个形状,圆形、正方形、垂直矩形或水平矩形之一。对于透明背景,选择None。

  • Effect:如果要在正方形或矩形形状的右上角添加狗耳朵效果,请选择DogEar。否则,选择None。

创建Action bar and tab icons

创建Notification icons

其他情况基本大同小异,这里就不多做介绍,浪费大家时间了。

收起阅读 »

Android面试题之Activity和Fragment生命周期 一次性记忆

每当我们换工作面试之前,总是会不由自主的刷起面试题,大部分题我们反反复复不知道刷了多少遍,但是今天记住了,等下一次面试的时候又刷着相同的面试题,我就想问在座的各位,Activity的生命周期,你们到底刷过多少遍 [哭笑] 作为一名程序员 把时间浪费在重复性劳动...
继续阅读 »

每当我们换工作面试之前,总是会不由自主的刷起面试题,大部分题我们反反复复不知道刷了多少遍,但是今天记住了,等下一次面试的时候又刷着相同的面试题,我就想问在座的各位,Activity的生命周期,你们到底刷过多少遍 [哭笑] 作为一名程序员 把时间浪费在重复性劳动上是极其不能忍受的 因此 为了让自己省去不必要的脑力开销 我给自己总结了一份面试相关的记忆技巧,在这里分享给大家 记忆不是目的 把知识变成自己的才最关键

前提

需要熟悉Activity的生命周期 通过Activity的周期去对比理解和记忆Fragment生命周期

Activity的生命周期

假设你已经非常熟悉Activity的生命周期了,那么接下来咱们看Fragment的生命周期图

Fragment的生命周期

找出他和Activity的相同之处

这部分完全和Activity一模一样 可以不用记忆它,咱们来看不同的地方

其实这部分才是人们最容易搞混和记不住的地方 那咱们来分析一下:

Fragment比Activity多了几个生命周期的回调方法

  • onAttach(Activity) 当Fragment与Activity发生关联的时候调用

  • onCreateView(LayoutInflater, ViewGroup, Bundle) 创建该F

  • onActivityCreated(Bundle) 当Activity的onCreated方法返回时调用

  • onDestroyView() 与onCreateView方法相对应,当该Fragment的视图被移除时调用

  • onDetach() 与onAttach方法相对应,当Fragment与Activity取消关联时调用 PS:注意:除了onCreateView,其他的所有方法如果你重写了,必须调用父类对于该方法的实现

    这些方法理解起来并不费劲 但是要完美记在脑子里 还是需要花上一番功夫的

    那咱们一个一个来 先从创建开始:

    1.首先 onAttach方法: 和Activity进行关联的时候调用 这个放在第一个 应该好理解

    2.我们知道 Activity在onCreate方法中需要调用setContentVIew()进行布局的加载,那么在Fragment中onCreateView就相当于Activity中的setContentVIew

    3.onActivityCreate是一个额外的方法 为了告诉Fragment当前Activity的创建执行情况 方便Fragment的后续操作

    先后顺序

    已知Fragment是依赖Activity而存在的 它们都有着相同的生命周期方法 那么先调用Activity的还是Fragment的呢? 这里分两种情况

    • 如果是创建 那么先创建Activity 后创建Fragment

    • 如果是销毁 那么先销毁Fragment 后销毁Activity

      网上有很多文章说Activity的onCreate方法在Fragment的onCreateView之后执行,这是不正确的 Fragment一般都是在Activity的onCreate()中创建 要么通过布局加载的方式 要么通过new创建Fragment对象的方式 如果没有Activity的onCreate 哪来的Fragment

      总结

      上面的理解好后,咱们再整理记忆一下

      一句话 Activity是老子 Fragment是小子 进门先让老子进 滚蛋先让小子滚 加载布局createView 老子完事吱一声(ActivityCreated)

      希望有帮到你

收起阅读 »

如何在大型代码仓库中删掉 6w 行废弃的文件和 exports?

起因 很多项目历史悠久,其中很多 文件或是 export 出去的变量 已经不再使用,非常影响维护迭代。 举个例子来说,后端问你:“某某接口统计一下某接口是否还有使用?”你在项目里一搜,好家伙,还有好几处使用呢,结果那些定义或文件是从未被引入的,这就会误导你们去...
继续阅读 »

起因


很多项目历史悠久,其中很多 文件或是 export 出去的变量 已经不再使用,非常影响维护迭代。
举个例子来说,后端问你:“某某接口统计一下某接口是否还有使用?”你在项目里一搜,好家伙,还有好几处使用呢,结果那些定义或文件是从未被引入的,这就会误导你们去继续维护这个文件或接口,影响迭代效率。


先从删除废弃的 exports 讲起,后文会讲删除废弃文件。


删除 exports,有几个难点:




  1. 怎么样稳定的 找出 export 出去,但是其他文件未 import 的变量




  2. 如何确定步骤 1 中变量在 本文件内部没有用到 (作用域分析)?




  3. 如何稳定的 删除这些变量




整体思路


先给出整体的思路,公司内的小伙伴推荐了 pzavolinsky/ts-unused-exports 这个开源库,并且已经在项目中稳定使用了一段时间,这个库可以搞定上述第一步的诉求,也就是找出 export 出去,但是其他文件未 import 的变量。
但下面两步依然很棘手,先给出我的结论:



  1. 如何确定步骤 1 中变量在本文件内部没有用到(作用域分析)?


对分析出的文件调用 ESLint 的 API,no-unused-vars 这个 ESLint rule 天生就可以分析出文件内部某个变量是否使用,但默认情况下它是不支持对 export 出去的变量进行分析的,因为既然你 export 了这个变量,那其实 ESLint 就认为你这个变量会被外部使用。对于这个限制,其实只需要 fork 下来稍微改写即可。



  1. 如何稳定的删除这些变量?


自己编写 rule fixer 删除掉分析出来的无用变量,之后就是格式化,由于 ESLint 删除代码后格式会乱掉,所以手动调用 prettier API 让代码恢复美观即可。


接下来我会对上述每一步详细讲解。


导出导入分析


使用测试下来, pzavolinsky/ts-unused-exports 确实可以靠谱的分析出 未使用的 export 变量 ,但是这种分析 import、export 关系的工具,只是局限于此,不会分析 export 出去的这个变量 在代码内部是否有使用到


文件内部使用分析


第二步的问题比较复杂,这里最终选用 ESLint 配合自己 fork 改写 no-unused-vars 这个 rule ,并且自己提供规则对应的修复方案 fixer 来实现。


为什么是 ESLint?




  1. 社区广泛使用,经过无数项目验证。




  2. 基于 作用域分析 ,准确的找出未使用的变量。




  3. 提供的 AST 符合 estree/estree 的通用标准,易于维护拓展。




  4. ESLint 可以解决 删除之后引入新的无用变量的问题 ,最典型的就是删除了某个函数,这个函数内部的某个函数也可能会变成无效代码。ESLint 会 重复执行 fix 函数,直到不再有新的可修复错误为止。





为什么要 fork 下来改写它?




  1. 官方的 no-unused-vars 默认是不考虑 export 出去的变量的,而经过我对源码的阅读发现,仅仅 修改少量的代码 就可以打破这个限制,让 export 出去的变量也可以被分析,在模块内部是否使用。




  2. 第一步的改写后,很多 export 出去的变量 被其他模块引用 ,但由于在 模块内部未使用 ,也会 被分析为未使用变量 。所以需要给 rule 提供一个 varsPattern 的选项,把分析范围限定在 ts-unused-exports 给出的 导出未使用变量 中,如 varsPattern: '^foo$|^bar$'




  3. 官方的 no-unused-vars 只给出提示,没有提供 自动修复 的方案,需要自己编写,下面详细讲解。




如何删除变量


当我们在 IDE 中编写代码时,有时会发现保存之后一些 ESLint 飘红的部分被自动修复了,但另一部分却没有反应。
这其实是 ESLint 的 rule fixer 的作用。
参考官方文档的 Apply Fixer 章节,每个 ESLint Rule 的编写者都可以决定自己的这条规则 是否可以自动修复,以及如何修复。
修复不是凭空产生的,需要作者自己对相应的 AST 节点做分析、删除等操作,好在 ESLint 提供了一个 fixer 工具包,里面封装了很多好用的节点操作方法,比如 fixer.remove()fixer.replaceText()
官方的 no-unused-vars 由于稳定性等原因未提供代码的自动修复方案,需要自己对这个 rule 写对应的 fixer 。官方给出的解释在 Add fix/suggestions to no-unused-vars rule · Issue #14585 · eslint/eslint


核心改动


把 ESLint Plugin 单独拆分到一个目录中,结构如下:


packages/eslint-plugin-deadvars
├── ast-utils.js
├── eslint-plugin.js
├── eslint-rule-typescript-unused-vars.js
├── eslint-rule-unused-vars.js
├── eslint-rule.js
└── package.json



  • eslint-plugin.js : 插件入口,外部引入后才可以使用 rule




  • eslint-rule-unused-vars.js : ESLint 官方的 eslint/no-unused-vars 代码,主要的核心代码都在里面。




  • eslint-rule-typescript-unused-vars : typescript-eslint/no-unused-vars 内部的代码,继承了 eslint/no-unused-vars ,增加了一些 TypeScript AST 节点的分析。




  • eslint-rule.js :规则入口,引入了 typescript rule ,并且利用 eslint-rule-composer 给这个规则增加了自动修复的逻辑。




ESLint Rule 改动


我们的分析涉及到删除,所以必须有一个严格的限定范围,就是 exports 出去 且被 ts-unused-exports 认定为 外部未使用 的变量。
所以考虑增加一个配置 varsPattern ,把 ts-unused-exports 分析出的未使用变量名传入进去,限定在这个名称范围内。
主要改动逻辑是在 collectUnusedVariables 这个函数中,这个函数的作用是 收集作用域中没有使用到的变量 ,这里把 exports 且不符合变量名范围 的全部跳过不处理。


else if (
config.varsIgnorePattern &&
config.varsIgnorePattern.test(def.name.name)
) {
// skip ignored variables
continue;
+ } else if (
+ isExported(variable) &&
+ config.varsPattern &&
+ !config.varsPattern.test(def.name.name)
+) {
+ // 符合 varsPattern
+ continue;
+ }

这样外部就可以这样使用这样的方式来限定分析范围:


rules: {
'@deadvars/no-unused-vars': [
'error',
{ varsPattern: '^foo$|^bar$' },
]
}

接着删除掉原版中 收集未使用变量时isExported 的判断,把 exports 出去但文件内部未使用 的变量也收集起来。由于上一步已经限定了变量名,所以这里只会收集到 ts-unused-exports 分析出来的变量。


if (
!isUsedVariable(variable) &&
- !isExported(variable) &&
!hasRestSpreadSibling(variable)
) {
unusedVars.push(variable);
}

ESLint Rule Fixer


接下来主要就是增加自动修复,这部分的逻辑在 eslint-rule.js 中,简单来说就是对上一步分析出来的各种未使用变量的 AST 节点进行判断和删除。
贴一下简化的函数处理代码:


module.exports = ruleComposer.mapReports(rule, (problem, context) => {
problem.fix = fixer => {
const { node } = problem;
const { parent } = node;

// 函数节点
switch (parent.type) {
case 'FunctionExpression':
case 'FunctionDeclaration':
case 'ArrowFunctionExpression':
// 调用 fixer 进行删除
return fixer.remove(parent);
...
...
default:
return null;
}
};
return problem;
});

目前会对以下几种节点类型进行删除:




  • FunctionExpression




  • FunctionDeclaration




  • ArrowFunctionExpression




  • ImportSpecifier




  • ImportDefaultSpecifier




  • ImportNamespaceSpecifier




  • VariableDeclarator




  • TSEnumDeclaration




后续新增节点的删除逻辑,只需要维护这个文件即可。


无用文件删除


之前基于 webpack-deadcode-plugin 做了一版无用代码删除,但是在实际使用的过程中,发现一些问题。


首先是 速度太慢 ,这个插件会基于 webpack 编译的结果来分析哪些文件是无用的,每次使用都需要编译一遍项目。


而且前几天加入了 fork-ts-checker-webpack-plugin 进行类型检查之后, 这个删除方案突然失效了 ,检测出来的只有 .less 类型的无用文件,经过和排查后发现是这个插件的锅,它会把 src 目录下的所有 ts 文件 都加入到 webpack 的依赖中,也就是 compilation.fileDependencies (可以尝试开启这个插件,在开发环境试着手动改一个完全未导入的 ts 文件,一样会触发重新编译)


而 deadcode-plugin 就是依赖 compilation.fileDependencies 这个变量来判断哪些文件未被使用,所有 ts 文件都在这个变量中的话,扫描出来的无用文件自然就只有其他类型了。


这个行为应该是插件的官方有意而为之,考虑如下情况:


// 直接导入一个 TS 类型
import { IProps } from "./type.ts";

// use IProps

在使用旧版的 fork-ts-checker-webpack-plugin 时,如果此时改动了 IProps 造成了类型错误,是不会触发 webpack 的编译报错的。


经过排查,目前官方的行为好像是把 tsconfig 中的 include 里的所有 ts 文件加入到依赖中,方便改动触发编译,而我们项目中的 include["src/**/*.ts"] ,所以……


具体讨论可以查看这个 Issue: Files that provide only type dependencies for main entry and unused files are not being checked for


方案


首先尝试在 deadcode 模式中手动删除 fork-ts-checker-webpack-plugin,这样可以扫描出无用依赖,但是上文中那样从文件中只导入类型的情况,还是会被认为是无用的文件而误删。


考虑到现实场景中单独建一个 type.ts 文件书写接口或类型的情况比较多,只好先放弃这个方案。


转而一想, pzavolinsky/ts-unused-exports 这个工具既然都能分析出
所有文件的 导入导出变量的依赖关系 ,那分析出未使用的文件应该也是小意思才对。


经过源码调试,大概梳理出了这个工具的原理:



  1. 通过 TypeScript 内置的 ts.parseJsonConfigFileContent API 扫描出项目内完整的 ts 文件路径。


 {
"path": "src/component/A",
"fullPath": "/Users/admin/works/test/src/component/A.tsx",
{
"path": "src/component/B",
"fullPath": "/Users/admin/works/test/apps/app/src/component/B.tsx",
}
...


  1. 通过 TypeScript 内置的一些 compile API 分析出文件之间的 exports 和 imports 关系。


{
"path": "src/component/A",
"fullPath": "/Users/admin/works/test/src/component/A.tsx",
"imports": {
"styled-components": ["default"],
"react": ["default"],
"src/components/B": ["TestComponentB"]
},
"exports": ["TestComponentA"]
}


  1. 根据上述信息来分析出每个文件中每个变量的使用次数,筛选出未使用的变量并且输出。


到此思路也就有了,把所有文件中的 imports 信息取一个合集,然后从第一步的文件集合中找出未出现在 imports 里的文件即可。


一些值得一提的改造


循环删除文件


在第一次检测出无用文件并删除后,很可能会暴露出一些新的无用文件。
比如以下这样的例子:


[
{
"path": "a",
"imports": "b"
},
{
"path": "b",
"imports": "c"
},
{
"path": "c"
}
]

文件 a 引入了文件 b,文件 b 引入了文件 c。


第一轮扫描的时候,没有任何文件引入 a,所以会把 a 视作无用文件。


由于 a 引入了 b,所以不会把 b 视作无用的文件,同理 c 也不会视作无用文件。


所以 第一轮删除只会删掉 a 文件


只要在每次删除后,把 files 范围缩小,比如第一次删除了 a 以后,files 只留下:


[
{
path: "b",
imports: "c",
},
{
path: "c",
},
];

此时会发现没有文件再引入 b 了,b 也会被加入无用文件的列表,再重复此步骤,即可删除 c 文件。


支持 Monorepo


原项目只考虑到了单个项目和单个 tsconfig 的处理,而如今 monorepo 已经非常流行了,monorepo 中每个项目都有自己的 tsconfig,形成一个自己的 project,而经常有项目 A 里的文件或变量被项目 B 所依赖使用的情况。


而如果单独扫描单个项目内的文件,就会把很多被子项目使用的文件误删掉。


这里的思路也很简单:




  1. 增加 --deps 参数,允许传入多个子项目的 tsconfig 路径。




  2. 过滤子项目扫描出的 imports 部分,找出从别名为 @main的主项目中引入的依赖(比如 import { Button } from '@main/components'




  3. 把这部分 imports 合并到主项目的依赖集合中,共同进行接下来的扫描步骤。




支持自定义文件扫描


TypeScript 提供的 API,默认只会扫描 .ts, .tsx 后缀的文件,在开启 allowJS 选项后也会扫描 .js, .jsx 后缀的文件。
而项目中很多的 .less, .svg 的文件也都未被使用,但它们都被忽略掉了。


这里我断点跟进 ts.parseJsonConfigFileContent 函数内部,发现有一些比较隐蔽的参数和逻辑,用比较 hack 的方式支持了自定义后缀。


当然,这里还涉及到了一些比较麻烦的改造,比如这个库原本是没有考虑 index.ts, index.less 同时存在这种情况的,通过源码的一些改造最终绕过了这个限制。


目前默认支持了 .less, .sass, .scss 这些类型文件的扫描 ,只要你确保该后缀的引入都是通过 import 语法,那么就可以通过增加的 extraFileExtensions 配置来增加自定义后缀。


import * as ts from "typescript";

const result = ts.parseJsonConfigFileContent(
parseJsonResult.config,
ts.sys,
basePath,
undefined,
undefined,
undefined,
extraFileExtensions?.map((extension) => ({
extension,
isMixedContent: false,
// hack ways to scan all files
scriptKind: ts.ScriptKind.Deferred,
}))
);

其他方案:ts-prune


ts-prune 是完全基于 TypeScript 服务实现的一个 dead exports 检测方案。


背景


TypeScript 服务提供了一个实用的 API: findAllReferences ,我们平时在 VSCode 里右键点击一个变量,选择 “Find All References” 时,就会调用这个底层 API 找出所有的引用。


ts-morph 这个库封装了包括 findAllReferences 在内的一些底层 API,提供更加简洁易用的调用方式。


ts-prune 就是基于 ts-morph 封装而成。


一段最简化的基于 ts-morph 的检测 dead exports 的代码如下:


// this could be improved... (ex. ignore interfaces/type aliases that describe a parameter type in the same file)
import { Project, TypeGuards, Node } from "ts-morph";

const project = new Project({ tsConfigFilePath: "tsconfig.json" });

for (const file of project.getSourceFiles()) {
file.forEachChild((child) => {
if (TypeGuards.isVariableStatement(child)) {
if (isExported(child)) child.getDeclarations().forEach(checkNode);
} else if (isExported(child)) checkNode(child);
});
}

function isExported(node: Node) {
return TypeGuards.isExportableNode(node) && node.isExported();
}

function checkNode(node: Node) {
if (!TypeGuards.isReferenceFindableNode(node)) return;

const file = node.getSourceFile();
if (
node.findReferencesAsNodes().filter((n) => n.getSourceFile() !== file)
.length === 0
)
console.log(
`[${file.getFilePath()}:${node.getStartLineNumber()}: ${
TypeGuards.hasName(node) ? node.getName() : node.getText()
}`
);
}

优点




  1. TS 的服务被各种 IDE 集成,经过无数大型项目检测,可靠性不用多说。




  2. 不需要像 ESLint 方案那样,额外检测变量在文件内是否使用, findAllReferences 的检测范围包括文件内部,开箱即用。




缺点




  1. 速度慢 ,TSProgram 的初始化,以及 findAllReferences 的调用,在大型项目中速度还是有点慢。




  2. 文档和规范比较差 ,ts-morph 的文档还是太简陋了,挺多核心的方法没有文档描述,不利于维护。




  3. 模块语法不一致 ,TypeScript 的 findAllReferences 并不识别 Dynamic Import 语法,需要额外处理 import() 形式导入的模块。




  4. 删除方案难做 ,ts-prune 封装了相对完善的 dead exports 检测方案,但作者似乎没有做自动删除方案的意思。这时 第二点的劣势就出来了,按照文档来探索删除方案非常艰难。看起来有个德国的小哥 好不容易说服作者 提了一个自动删除的 MR:Add a fix mode that automatically fixes unused exports (revival) ,但是最后因为内存溢出没通过 GithubCI,不了了之了。我个人把这套代码 fork 下来在公司内部的大型项目中跑了一下,也确实是内存溢出 ,看了下自动修复方案的代码,也都是很常规的基于 ts-morph 的 API 调用,猜测是底层 API 的性能问题?




所以综合评估下来,最后还是选择了 ts-unused-exports + ESLint 的方案。


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

收起阅读 »

Android JNI 原理

JNI:Java Native Interface1. 系统源码中的 JNI2. MediaRecorder 框架中的 JNIMediaRecorder,用于录音和录像。2.1 Java Framework 层的 MediaRecorder2.2 JNI 层的...
继续阅读 »
  • JNI:Java Native Interface

image.png

1. 系统源码中的 JNI

image.png

2. MediaRecorder 框架中的 JNI

MediaRecorder,用于录音和录像。

image.png

2.1 Java Framework 层的 MediaRecorder

image.png

image.png

2.2 JNI 层的 MediaRecorder

image.png

2.3 Native 方法注册

Native 方法注册分为静态注册和动态注册,其中静态注册多用于 NDK 开发,而动态注册多用于 Framework 开发。

2.3.1 静态注册

image.png

image.png

静态注册就是 Java 的 Native 方法通过方法指针来与 JNI 进行关联,如果 Java 的 Native 方法知道它在 JNI 中对应的函数指针,就可以避免上述的缺点,这就是动态注册。

2.3.2 动态注册

image.png

image.png

image.png

3. 数据类型的转换

Java 的数据类型到了 JNI 层就需要转换为 JNI 层的数据类型。

3.1 基本数据类型的转换

image.png

3.2 引用数据类型的转换

image.png

image.png

image.png

4. 方法签名

JNI 的方法签名的格式为: (参数签名格式...)返回值签名格式

image.png

image.png

5. 解析 JNIEnv

  • JNIEnv
  • Java VM
  • JNINativeInterface
  • JNIInvokeInterface
  • AttachCurrentThread
  • DetachCurrentThread

image.png

image.png

5.1 jfieldID 和 jmethodID

image.png

image.png

image.png

5.2 使用 jfieldID 和 jmethodID

image.png

image.png

6. 引用类型

  • 本地引用(Local References)
  • 全局引用(Global References)
  • 弱全局引用(Weak Global References)

6.1 本地引用

image.png

image.png

  • FindClass
  • DeleteLocalRef

6.2 全局引用

全局引用和本地引用几乎是相反的,它主要有以下特点:

image.png

image.png

  • NewGlobalRef
  • DeleteGlobalRef

6.3 弱全局引用

image.png

image.png

  • NewWeakGlobalRef
  • DeleteWeakGlobalRef
  • IsSameObject
收起阅读 »

超详细的android so库的逆向调试

好久没有写博客了,最近的精力全放在逆向上面。目前也只是略懂皮毛。android java层的逆向比较简单,主要就是脱壳 、反编译源码,通过xposed进行hook。接下来介绍一下,如何去调试hook native层的源码,也就是hook so文件。应用环境准备...
继续阅读 »

好久没有写博客了,最近的精力全放在逆向上面。目前也只是略懂皮毛。

android java层的逆向比较简单,主要就是脱壳 、反编译源码,通过xposed进行hook。
接下来介绍一下,如何去调试hook native层的源码,也就是hook so文件。

应用环境准备

首先,为了方便学习,一上来就hook第三方app难度极大,因此我们自己来创建一个native的项目,自己来hook自己的项目作为学习的练手点。

创建默认的native application

打开as,选择File -> new project -> naive c++ 创建包含c++的原生工程。

hook1.png

默认的native工程,帮我们实现了stringFromJNI方法,那我们就来探索如何hook这个stringFromJNI,并修改他的值。

修改stringFromJNI方法,便于调试

as默认实现的stringFromJNI只有在Activity onCreate的时候调用,为了便于调试,我们增加一个点击事件,每次点击重新调用,并且返回一个随机的值。

java代码增加如下方法:

	binding.sampleText.setOnClickListener {
Log.e("MainActivity", "stringFromJNI")
binding.sampleText.text = stringFromJNI()
}

修改native-lib.cpp代码:

#include <jni.h>
#include <string>

using namespace std;

int max1(int num1, int num2);
#define random(x) rand()%(x)

extern "C" JNIEXPORT jstring JNICALL
Java_com_noober_naticeapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
int result = max1(random(100), random(100));
string hello = "Hello from C++";
string hello2 = hello.append(to_string(result));
return env->NewStringUTF(hello2.c_str());
}


int max1(int num1, int num2)
{
// 局部变量声明
int result;

if (num1 > num2)
result = num1;
else
result = num2;

return result;
}

修改的代码很简单,相信不会 c++ 的同学也看得懂,就是随机输入两个数,取其中小的那一位拼接在“Hello from C++”后面,并返回。主要目的是让我们每次点击的时候,返回内容可以动态。

修改androidManifest文件

在application中增加下面两行代码:

    android:extractNativeLibs="true"
android:debuggable="true"

android:debuggable: 让我们可以对apk进行调试,如果是第三方已经打包好了app,我们需要对其manifest文件进行修改,增加这行代码,然后进行重打包,否则无法进行so的调试。

android:extractNativeLibs: 很多人在进行调试的时候发现ida pro一切正常,但是却一直没有加载我们的libnative -lib.so, 是因为缺少这行代码。如果不加,可能会使so直接自身的base.apk进行加载,导致ida pro无法识别。

修改CMakeLists.txt

在cmakelists中增加下面代码。so文件生成路径,这样编译之后就可以在main-cpp-jniLibs目录下找到生产的so文件。

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})

编译运行,获取so

上述工作做好之后,直接编译运行,同时会生成4个so文件,我们取手机运行时对应使用的那个so进行hook。
我这边使用的是arm64-v8a目录下的libnative-lib.so。

hook2.png

hook环境准备

  • 系统:windows 10 64位
  • 工具ida pro 7.5
  • java8环境
  • android sdk tools和adb工具
  • arm64-v8a目录下的libnative-lib.so
  • android 真机

使用ida pro进行hook

adb与手机的准备

  1. 首先找到ida pro的dbgsrv文件夹,里面有很多server文件

hook3.png

64代表的含义是64位,否则就是32位,我们根据我们需要调试的so的指令集进行选择。因为我这边调试的是arm64-v8a,这里我们就选择android_server64的文件。连接真机后,打开cmd,输入以下指令:

adb push "\\Mac\Home\Desktop\IDA PRO 7.5 (x86, x64, ARM, ARM64)\dbgsrv\android_server64"  /data/local/tmp
  1. 如果是真机,则需要输入su,模拟器不需要

     #真机
    su
  2. 修改权限

     chmod 777 /data/local/tmp/android_server64
  3. 运行

     /data/local/tmp/android_server64

hook9.png

  1. 新打开一个cmd,在本地执行adb 做端口转发

     adb forward tcp:23946 tcp:23946

ida pro的工作准备

  1. 打开ida pro,因为我们的so是64位的,所以打开ida64.exe。点击new,选择libnative-lib.so。

  2. 选择debugger-select debugger

hook4.png

  1. 选择Remote ARM Linux/Android debugger

hook5.png

  1. 点击debugger-Debugger options

勾选Suspend on process entry point ,也就是在断点处进行挂起暂停

hook6.png

  1. 点击debugger-Process options

填写hostname为localhost

hook10.png

  1. 找到exports标签,ctrl+f,搜索java关键字,找到我们要hook的函数。

hook7.png

  1. 双击打开,按F5,进行反汇编操作。这样就可以看到反汇编之后的c ++代码了。然后我们随便加上断点进行调试。

hook8.png

  1. 执行adb命令,进入调试状态,也就是打开我们要调试的app的启动activity,我这边如下:

     adb shell am start -D -n com.noober.naticeapplication/com.noober.naticeapplication.MainActivity
  2. 点击debugger-Attach to process

选择我们需要调试的进程。

hook11.png

  1. adb 执行如下命令,关联运行的so与本地要调试的so。

    jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
  2. 此时ida卡在libc.so的位置,点击继续执行,弹出如下界面,关联so到本地,选择same。如果没有弹出则需要通过快捷键ctrl+s, 打开所有已经加载的so,找到我们的libnative-lib.so

hook14.png

  1. 此时就会自动进入断点。

hook1.png

使用ida pro进行调试

ida pro 常用调试快捷键

  • F2下断点
  • F7单步步入
  • F8单步步过
  • F9执行到下个断点
  • G调到函数地址
  • Debugger-debugger windows-locals 查看变量

进行调试

  1. 简单分析反汇编代码,我们发现返回值是v5,通过f8,执行到return的上一行。打开locals, 获取所有变量的值。

locals.png

  1. 复制bytes的地址0x7FFE2CDEB9LL,切换到代码界面,输入快捷键g,输入地址跳转。这样我们便从内存中得到了数据结果,可以看出本次返回的值就是"Hello from c++89"

result.png

  1. 当然我们也可以在locals中直接修改值,这样就达到了我们hook so动态修改数据的目的。

收起阅读 »

Android 自动化交互实践

Android 自动化交互可以代替人工完成重复性的工作,包括通过自动操作 App 进行黑盒测试和第三方 App 的自动运行。常见的自动化交互包含启动 App、view 的点击、拖拽和文本输入等。随着 App 安防能力的提升,要想实现完整流程的自动化交互变的越来...
继续阅读 »

Android 自动化交互可以代替人工完成重复性的工作,包括通过自动操作 App 进行黑盒测试和第三方 App 的自动运行。常见的自动化交互包含启动 App、view 的点击、拖拽和文本输入等。随着 App 安防能力的提升,要想实现完整流程的自动化交互变的越来越困难,本文主要探讨目前常见的自动化交互方案以及不同方案的优劣和应用场景。

1 传统执行脚本方案

ADB 是 Google 提供的能够和 Android 设备进行交互的命令行工具,我们可以编写脚本按照事先设计好的顺序,一个一个执行各个事件。ADB 执行操作需要事先获取界面元素的坐标(获取坐标方法可以利用 uiautomator 或者 dump xml 的方法,这里不是讨论的重点),然后把坐标传入作为命令行参数。

adb shell input tap 100 500

上面命令是模拟点击屏幕坐标为(100, 500)处的控件。

adb shell input swipe 100 500 200 600

上面命令是模拟手指在屏幕上向右下方滑动的一个操作。

adb shell input keyevent "KEYCODE_BACK"

上面命令模拟返回按键的点击。

一次完整的自动化交互流程可由上面一系列命令顺序执行。

ADB 脚本方式的优点

  1. 实现简单,只需要获取目标元素的坐标等简单信息即可完成相关操作
  2. 可以实现对 webview 的自动化交互\

ADB 脚本方式的缺点

  1. 灵活度不够,依赖于写死的坐标,App 界面变更引起的 view 位置变换会让脚本中相关命令无法执行,需要重新分析页面坐标
  2. 需要建立 ADB 链接或套接字链接,交互过程中网络状况的变化会影响自动化交互效果\

ADB 脚本方式应用场景

  1. 交互简单、迭代频率低,安防级别比较低的 App
  2. webview 页面,flutter 开发的 App

2 Android 原生方法实现自动化交互

我们可以借助各种插件化框架来控制 App 页面的界面元素,其中一种思路就是在插件中借助 ActivityLifecycleCallbacks 来监听各个 activity 的生命周期。

public class MyApplication extends Application {
private static final String TAG = "MyApplication";
//声明一个监听Activity们生命周期的接口
private ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacks() {
/**
* application下的每个Activity声明周期改变时,都会触发以下的函数。
*/
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//如何区别参数中activity代表你写的哪个activity。
if (activity.getClass() == MainActivity.class)
Log.d(TAG, "MainActivityCreated.");
else if(activity.getClass()== SecondActivity.class)
Log.d(TAG, "SecondActivityCreated.");
}

@Override
public void onActivityStarted(Activity activity) {
Log.d(TAG, "onActivityStarted.");
}

@Override
public void onActivityResumed(Activity activity) {
Log.d(TAG, "onActivityResumed.");
}

@Override
public void onActivityPaused(Activity activity) {
Log.d(TAG, "onActivityPaused.");
}

@Override
public void onActivityStopped(Activity activity) {
Log.d(TAG, "onActivityStopped.");
}

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

@Override
public void onActivityDestroyed(Activity activity) {
Log.d(TAG, "onActivityDestroyed.");
}
};

@Override
public void onCreate() {
super.onCreate();
//注册自己的Activity的生命周期回调接口。![Alt text](./WechatIMG59.png)

registerActivityLifecycleCallbacks(activityLifecycleCallbacks);
}

@Override
public void onTerminate() {
//注销这个接口。
unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
super.onTerminate();
}
}

监听到 activity 的活动后,可以借助 uiautomator 分析 activity 界面元素的 viewId 以及属性,不同情况的界面 view 可以采用不同的自动化方法。

2.1 简单 view 的处理方式

如下图: image.png

像这类 view,可以直接获取到 resource id ,并且确认可点击属性为 true,操作方式比较简单, 可以在监听到的 activity 生命周期中执行如下操作:

int fl_btn = activity.getResources().getIdentifier("dashboard_title",         "id", "com.android.settings");
View v = activity.findViewById(fl_btn);

v.performClick();

2.2 隐藏属性的 view 的处理方式

在一些对 view 的属性进行隐藏,特别是利用 React Native 等混合开发的页面,上面的方法不再生效,如下图所示的 view: image.png

如图,选中的 viewgroup 及其子 view 的 clickable 属性均为 false,并且无法获取到 view 的 resource id,这时候可以利用图中 dump 出的布局信息,借助 Xpath 元素定位工具来获取到界面的 view,由于这些 view 的点击属性为 false,因此通过调用 performClick 来实现点击的方法已经无效,此时考虑在 click 更底层的与触摸事件传递相关的比较重要的类:MotionEvent, MotionEvent 可以仿真几乎所有的交互事件,包括点击,滑动,双指操作等。以单击为例:

    private void simulateClick(View view, float x, float y) {
long time = SystemClock.uptimeMillis();//必须是 SystemClock.uptimeMillis()。

MotionEvent downEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, x, y, 0);

time += 500;

MotionEvent upEvent = MotionEvent.obtain(time, time, MotionEvent.ACTION_UP, x, y, 0);

view.onTouchEvent(downEvent);
view.onTouchEvent(upEvent);
}

如果是滑动操作,可以在起始位置中间添加若干 ACTION_MOVE 类型的 MotionEvent. 综上所述,借助系统原生方法时间交互自动化的优缺点大致如下:

借助插件框架实现自动化交互的优点

  1. 可维护性强,因为可以直接获取到界面的 view 对象,因此即使页面布局发生变化,只要 view 还存在,就不需要对代码进行修改
  2. 仿真效果更好,比脚本方式更接近真人操作

借助插件框架实现自动化交互的不足

  1. 对 webview、flutter 框架 App 支持不好

应用场景

  1. 版本迭代频繁的 App
  2. 非 flutter 框架开发的 App

上面分析了两种常用的模拟真实用户进行自动化操作的方法,脚本方式和调用原生方法的方式,这两种方法基本上可以完成简单的交互流程,在此基础上,我们还可以去深究一些更深层次的交互实现,比如自动化过各种验证等,也可以基于这两种方法来完成。

收起阅读 »

Android 面试准备进行曲-Android 基础知识

基础部分Activity生命周期onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDetroy() 图片简要说明启动 onCreate...
继续阅读 »

基础部分

Activity生命周期

onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDetroy()

11.webp 图片简要说明

  • 启动 onCreate -> onStart -> onResume
  • 被覆盖/ 回到当前界面 onPause -> / -> onResume
  • 在后台 onPause -> onStop
  • 后退回到 onRestart -> onStart -> onResume
  • 退出 onPause -> onStop -> onDestory

另外说一下 其他两个比较重要的 方法

  • onSaveInstanceState : (1)Activity被覆盖或退居后台,系统资源不足将其杀死,此方法会被调用;(2)在用户改变屏幕方向时,此方法会被调用 (系统先销毁当前的Activity,然后再重建一个新的,调用此方法时,我们可以保存一些临时数据);(3)在当前Activity跳转到其他Activity或者按Home键回到主屏,自身退居后台时, 系统调用此方法是为了保存当前窗口各个View组件的状态。onSaveInstanceState该方法调用在onStop之前,但和onPause没有时序关系。 不过一般onSaveInstanceState() 保存临时数据为主,而 onPause() 适用于对数据的持久化保存。

  • onRestoreInstanceState : onRestoreInstanceState的调用顺序是在onStart之后。主要用于 恢复一些onSaveInstanceState 方法中保存的数据

onStart()和onResume()/onPause()和onStop()的区别

onStart()与onStop()是从Activity是否可见这个角度调用的 onResume()和onPause()是从Activity是否显示在前台这个角度来回调的 在实际使用没其他明显区别。

Activity A 跳转 Activity B的问题

Activity A启动另一个Activity B会回调的方法: Activity A的onPause() -->Activity B的onCreate()-->onStart()-->onResume()-->Activity A的onStop();

如果Activity B是完全透明的,则最后不会调用Activity A的onStop();如果是对话框Activity,则最后不会调用Activity A的onStop();

Activity 启动流程

22.webp 调用startActivity()后经过重重方法会转移到ActivityManagerService的startActivity(),并通过一个IPC回到ActivityThread的内部类ApplicationThread中,并调用其scheduleLaunchActivity()将启动Activity的消息发送并交由Handler H处理。 Handler H对消息的处理会调用handleLaunchActivity()->performLaunchActivity()得以完成Activity对象的创建和启动。

参考地址:Activity启动流程

Fragment 生命周期

Fragment从创建到销毁整个生命周期中涉及到的方法依次为: onAttach()->onCreate()-> onCreateView()->onActivityCreated()->onStart()->onResume()->onPause()->onStop()->onDestroyView()->onDestroy()->onDetach(), 其中和Activity有不少名称相同作用相似的方法,而不同的方法有:

onAttach():当Fragment和Activity建立关联时调用

onCreateView():当Fragment创建视图时调用

onActivityCreated():当与Fragment相关联的Activity完成onCreate()之后调用

onDestroyView():在Fragment中的布局被移除时调用

onDetach():当Fragment和Activity解除关联时调用

Activity 与 Fragment 通信

  1. 对于Activity和Fragment之间的相互调用

(1)Activity调用Fragment 直接调用就好,Activity一般持有Fragment实例,或者通过Fragment id 或者tag获取到Fragment实例 (2)Fragment调用Activity 通过activity设置监听器到Fragment进行回调,或者是直接在fragment直接getActivity获取到activity实例

  1. Activity如果更好的传递参数给Fragment

如果直接通过普通方法的调用传递参数的话,那么在fragment回收后恢复不能恢复这些数据。google给我们提供了一个方法 setArguments(bundle) 可以通过这个方法传递参数给fragment,然后在fragment中用getArguments获取到。能保证在fragment销毁重建后还能获取到数据

Service 启动及生命周期

service 启动方式

  • 不可通信Service 。 通过startService()启动,不跟随调用者关闭而关闭
  • 可通信Service 。 通过bindService()方式进行启动。跟随调用者关闭而关闭

以上两种Servcie 默认都存在于调用者一样的进程中,如果想要设置不一样的进程中则需要在 AndroidManifest.xml 中 配置 android:process = Remote 属性

生命周期 :

  • 通过startService()这种方式启动的service,生命周期 :startService() --> onCreate()--> onStartConmon()--> onDestroy()。

需要注意几个问题

1. 当我们通过startService被调用以后,多次在调用startService(),onCreate()方法也只会被调用一次,
2. 而onStartConmon()会被多次调用当我们调用stopService()的时候,onDestroy()就会被调用,从而销毁服务。
2. 当我们通过startService启动时候,通过intent传值,在onStartConmon()方法中获取值的时候,一定要先判断intent是否为null。
  • 通过bindService()方式进行绑定,这种方式绑定service,生命周期走法:bindService-->onCreate()-->onBind()-->unBind()-->onDestroy()

bindService的优点 这种方式进行启动service好处是更加便利activity中操作service,比如加入service中有几个方法,a,b ,如果要在activity中调用,在需要在activity获取ServiceConnection对象,通过ServiceConnection来获取service中内部类的类对象,然后通过这个类对象就可以调用类中的方法,当然这个类需要继承Binder对象

Service 通信方式

  1. 创建继承Binder的内部类,重写Service的onBind方法 返回 Binder 子类,重写ServiceConnection,onServiceConnected时调用逻辑方法 绑定服务。

  2. 通过接口Iservice调用Service方法

IntentService对比Service

IntentService是Service的子类,是一个异步的,会自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止并销毁Service的问题

优点:

  • 所有请求处理完成后,IntentService会自动停止,无需调用stopSelf()方法停止
  • IntentService不会阻塞UI线程,而普通Serveice会导致ANR异常
  • Intentservice若未执行完成上一次的任务,将不会新开一个线程,是等待之前的任务完成后,再执行新的任务,等任务完成后再次调用stopSelf()
  • 为Service的onBind()提供默认实现,返回null;

提高service的优先级

  1. 在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = “1000”这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时实用于广播。
  2. onStartCommand方法,手动返回START_STICKY。
  3. 监听系统广播判断Service状态。
  4. Application加上Persistent属性。
  5. 在onStartCommand里面调用 startForeground()方法把Service提升为前台进程级别,然后再onDestroy里面调用stopForeground ()方法。
  6. 在onDestroy方法里发广播重启service。

service +broadcast 方式,就是当service走ondestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service。

延伸:进程保活(毒瘤)

黑色保活:不同的app进程,用广播相互唤醒(包括利用系统提供的广播进行唤醒)
白色保活:启动前台Service
灰色保活:利用系统的漏洞启动前台Service

黑色保活 所谓黑色保活,就是利用不同的app进程使用广播来进行相互唤醒。举个3个比较常见的场景: 场景1:开机,网络切换、拍照、拍视频时候,利用系统产生的广播唤醒app 场景2:接入第三方SDK也会唤醒相应的app进程,如微信sdk会唤醒微信,支付宝sdk会唤醒支付宝。由此发散开去,就会直接触发了下面的 场景3 场景3:假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。(只是拿阿里打个比方,其实BAT系都差不多)

白色保活 白色保活手段非常简单,就是调用系统api启动一个前台的Service进程,这样会在系统的通知栏生成一个Notification,用来让用户知道有这样一个app在运行着,哪怕当前的app退到了后台。如下方的LBE和QQ音乐这样:

灰色保活 灰色保活,这种保活手段是应用范围最广泛。它是利用系统的漏洞来启动一个前台的Service进程,与普通的启动方式区别在于,它不会在系统通知栏处出现一个Notification,看起来就如同运行着一个后台Service进程一样。这样做带来的好处就是,用户无法察觉到你运行着一个前台进程(因为看不到Notification),但你的进程优先级又是高于普通后台进程的。那么如何利用系统的漏洞呢,大致的实现思路和代码如下: 思路一:API < 18,启动前台Service时直接传入new Notification(); 思路二:API >= 18,同时启动两个id相同的前台Service,然后再将后启动的Service做stop处理 熟悉Android系统的童鞋都知道,系统出于体验和性能上的考虑,app在退到后台时系统并不会真正的kill掉这个进程,而是将其缓存起来。打开的应用越多,后台缓存的进程也越多。在系统内存不足的情况下,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的app。这套杀进程回收内存的机制就叫 Low Memory Killer ,它是基于Linux内核的 OOM Killer(Out-Of-Memory killer)机制诞生。

思路二:后台播放无声音频,模拟前台服务,提高等级

思路三:1像素界面

思路四:在Activity的onDestroy()通过发送广播,并在广播接收器的onReceive()中启动Service

进程的重要性,划分5级: 前台进程 (Foreground process) 可见进程 (Visible process) 服务进程 (Service process) 后台进程 (Background process) 空进程 (Empty process)

什么是oom_adj?它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收。对于oom_adj的作用,你只需要记住以下几点即可: 进程的oom_adj越大,表示此进程优先级越低,越容易被杀回收;越小,表示进程优先级越高,越不容易被杀回收 普通app进程的oom_adj>=0,系统进程的oom_adj才可能<0 有些手机厂商把这些知名的app放入了自己的白名单中,保证了进程不死来提高用户体验

Broadcast注册方式与区别

Broadcast广播,注册方式主要有两种.

  • 第一种是静态注册,也可成为常驻型广播,这种广播需要在Androidmanifest.xml中进行注册,这中方式注册的广播,不受页面生命周期的影响,即使退出了页面,也可以收到广播这种广播一般用于想开机自启动啊等等,由于这种注册的方式的广播是常驻型广播,所以会占用CPU的资源。

  • 第二种是动态注册,而动态注册的话,是在代码中注册的,这种注册方式也叫非常驻型广播,收到生命周期的影响,退出页面后,就不会收到广播,我们通常运用在更新UI方面。这种注册方式优先级较高。最后需要解绑,否则会内存泄露

广播是分为有序广播和无序广播。

Broadcast 有几种形式

普通广播:一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们接收的先后是随机的。

有序广播:一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递,所以此时的广播接收器是有先后顺序的,且优先级(priority)高的广播接收器会先收到广播消息。有序广播可以被接收器截断使得后面的接收器无法收到它。

本地广播:发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接收本应用程序发出的广播。

粘性广播:这种广播会一直滞留,当有匹配该广播的接收器被注册后,该接收器就会收到此条广播。

部分Broadcast 之间的区别

BroadcastReceiver: 是可以跨应用广播,利用Binder机制实现,支持动态和静态两种方式注册方式。

LocalBroadcastReceiver: 是应用内广播,利用Handler实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册。

OrderedBroadcast : 调用sendOrderedBroadcast()发送,接收者会按照priority优先级从大到小进行排序,如优先级相同,先注册,先处理 广播接收者还能对广播进行截断和修改

ContentProvider

作为四大组件之一,ContentProvider主要负责存储和共享数据。与文件存储、SharedPreferences存储、SQLite数据库存储这几种数据存储方法不同的是,后者保存下的数据只能被该应用程序使用,而前者可以让不同应用程序之间进行数据共享,它还可以选择只对哪一部分数据进行共享,从而保证程序中的隐私数据不会有泄漏风险。

app中有几个Context对象

先看一下源码的解释

/**
* Interface to global information about an application environment. This is
* an abstract class whose implementation is provided by
* the Android system. It
* allows access to application-specific resources and classes, as well as
* up-calls for application-level operations such as launching activities,
* broadcasting and receiving intents, etc.
*/
public abstract class Context {
/**
* File creation mode: the default mode, where the created file can only
* be accessed by the calling application (or all applications sharing the
* same user ID).
* <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> #MODE_WORLD_READABLE
* <a href="http://www.jobbole.com/members/heydee@qq.com">@see</a> #MODE_WORLD_WRITEABLE
*/
public static final int MODE_PRIVATE = 0x0000;

public static final int MODE_WORLD_WRITEABLE = 0x0002;

public static final int MODE_APPEND = 0x8000;

public static final int MODE_MULTI_PROCESS = 0x0004;

}

源码中的注释是这么来解释Context的:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。

3.webp 从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。

Application Context 启动问题

如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈,而此时Activity是以singleTask模式启动的。所有这种用Application启动Activity的方式不推荐使用,Service同Application。

如何获取 Context对象

1:View.getContext,返回当前View对象的Context对象,通常是当前正在展示的Activity对象。

2:Activity.getApplicationContext,获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。

4:Activity.this 返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。

如何避免因为Context 造成内存泄漏

一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:

1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。

2:不要让生命周期长于Activity的对象持有到Activity的引用。

3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

getApplication()和getApplicationContext() 区别

其实我们通过程序打印 两个方法获得的对象 Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?

实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了。

理解Activity,View,Window三者关系

Activity像一个工匠(控制单元),Window像窗户(承载模型),View像窗花(显示视图)LayoutInflater像剪刀,Xml配置像窗花图纸。 1:Activity构造的时候会初始化一个Window,准确的说是PhoneWindow。 2:这个PhoneWindow有一个“ViewRoot”,这个“ViewRoot”是一个View或者说ViewGroup,是最初始的根视图。 3:“ViewRoot”通过addView方法来一个个的添加View。比如TextView,Button等 4:这些View的事件监听,是由WindowManagerService来接受消息,并且回调Activity函数。比如onClickListener,onKeyDown等。

四种LaunchMode及其使用场景

此处延伸:栈(First In Last Out)与队列(First In First Out)的区别 栈与队列的区别:

队列先进先出,栈先进后出 对插入和删除操作的"限定"。 栈是限定只能在表的一端进行插入和删除操作的线性表。 队列是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。 遍历数据速度不同

standard 模式 这是默认模式,每次激活Activity时都会创建Activity实例,并放入任务栈中。使用场景:大多数Activity。 singleTop 模式 如果在任务的栈顶正好存在该Activity的实例,就重用该实例( 会调用实例的 onNewIntent() ),否则就会创建新的实例并放入栈顶,即使栈中已经存在该Activity的实例,只要不在栈顶,都会创建新的实例。使用场景如新闻类或者阅读类App的内容页面。 singleTask 模式 如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈。如果栈中不存在该实例,将会创建新的实例放入栈中。使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。 singleInstance 模式 在一个新栈中创建该Activity的实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity实例已经存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例( 会调用实例的 onNewIntent() )。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。使用场景如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。

数据存储

Android中提供哪些数据持久存储的方法?

File 文件存储:写入和读取文件的方法和 Java中实现I/O的程序一样。

SharedPreferences存储:一种轻型的数据存储方式,常用来存储一些简单的配置 信息,本质是基于XML文件存储key-value键值对数据。

SQLite数据库存储:一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,在存储大量复杂的关系型数据的时可以使用。

ContentProvider:四大组件之一,用于数据的存储和共享,不仅可以让不同应用程序之间进行数据共享,还可以选择只对哪一部分数据进行共享,可保证程序中的隐私数据不会有泄漏风险。

SharePreferences 相关问题

  1. SharePreferences是一种轻型的数据存储方式,适用于存储一些简单的配置信息,如int、string、boolean、float和long。由于系统对SharedPreferences的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据。

  2. context.getSharedPreferences()开始追踪的话,可以去到ContextImpl的getSharedPreferences(),最终发现SharedPreferencesImpl这个SharedPreferences的实现类,在代码中可以看到读写操作时都有大量的synchronized,因此它是线程安全

  3. 由于进程间是不能内存共享的,每个进程操作的SharedPreferences都是一个单独的实例,这导致了多进程间通过SharedPreferences来共享数据是不安全的,这个问题只能通过多进程间其它的通信方式或者是在确保不会同时操作SharedPreferences数据的前提下使用SharedPreferences来解决。

SharePreferences 注意事项及优化办法

  1. 第一次getSharePreference会读取磁盘文件,异步读取,写入到内存中,后续的getSharePreference都是从内存中拿了。
  2. 第一次读取完毕之前 所有的get/set请求都会被卡住 等待读取完毕后再执行,所以第一次读取会有ANR风险。
  3. 所有的get都是从内存中读取。
  4. 提交都是 写入到内存和磁盘中 。apply跟commit的区别在于

apply 是内存同步 然后磁盘异步写入任务放到一个单线程队列中 等待调用。方法无返回 即void commit 内存同步 只不过要等待磁盘写入结束才返回 直接返回写入成功状态 true or false 5. 从 Android N 开始, 不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE. 一旦指定, 会抛异常 。也不要用MODE_MULTI_PROCESS 迟早被放弃。 8.每次commit/apply都会把全部数据一次性写入到磁盘,即没有增量写入的概念 。 所以单个文件千万不要太大 否则会严重影响性能。

建议用微信的第三方MMKV来替代SharePreference

SP源码解析

SQLite 相关问题

  • 使用事务做批量操作:

使用SQLiteDatabase的beginTransaction()方法开启一个事务,将批量操作SQL语句转化成SQLiteStatement并进行批量操作,结束后endTransaction()

  • 及时关闭Cursor,避免内存泄漏

  • 耗时操作异步化:数据库的操作属于本地IO,通常比较耗时,建议将这些耗时操作放入异步线程中处理

  • ContentValues的容量调整:ContentValues内部采用HashMap来存储Key-Value数据,ContentValues初始容量为8,扩容时翻倍。因此建议对ContentValues填入的内容进行估量,设置合理的初始化容量,减少不必要的内部扩容操作

  • 使用索引加快检索速度:对于查询操作量级较大、业务对要求查询要求较高的推荐使用索引


收起阅读 »

性能优化2 - 内存、启动速度、卡顿、布局优化

性能优化是在充分了解项目+java、android基础知识牢固的基础上的。内存优化基础知识回顾(看前面文章JVM详解):jVM内存模型,除了程序计数器以外,别的都会出现 OOM。JAVA对象的生命周期,创建、运行、死亡。GC对象可回收的判定:可达性分析。GC ...
继续阅读 »

性能优化是在充分了解项目+java、android基础知识牢固的基础上的。

内存优化

基础知识回顾(看前面文章JVM详解):

jVM内存模型,除了程序计数器以外,别的都会出现 OOM。

image.png

JAVA对象的生命周期,创建、运行、死亡。
GC对象可回收的判定:可达性分析。GC root(除了堆里的对象,虚拟机栈里的引用、方法区里的引用)。
强软弱虚四种引用。
GC回收算法:复制算法、标记清楚算法、标记整理算法。

image.png

App内存组成以及限制

Android给每个App分配一个VM,让App运行在dalvik上,这样即使App崩溃也不会影响到系统。系统给VM分配了一定的内存大小,App可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM最大内存,就会出现内存溢出crash

由程序控制操作的内存空间在heap上,分java heapsizenative heapsize

  • Java申请的内存在vm heap上,所以如果java申请的内存大小超过VM的逻辑内存限制,就会出现内存溢出的异常。
  • native层内存申请不受其限制,native层受native process对内存大小的限制

总结:
app运行在虚拟机上,手机给虚拟机分配内存是固定的,超出就oom。
分配的内存大部分都是在堆上。分为java堆和native层的堆。
java层的堆超过VM给的就oom。理论上native层无限制,但是底层实现native process对内存大小是有限制的。

查看系统给App分配的内存限制

不同手机给app分配的内存大小其实是不一样大的。

  1. 通过cmd指令 adb shell cat /system/build.prop

image.png

  1. 通过代码 activityManager.getMemoryClass();
ActivityManager activityManager =(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)

activityManager.getMemoryClass();//以m为单位

这些限制其实在 AndroidRuntime.cpp的startVm里,我们改不了,手机厂商可以改

image.png

Android低内存杀进程机制

默认五个级别:空进程、后台进程、服务进程、可见进程、前台进程
所以在保活里有一个做法就是给app提升优先级,给他整成前台、可见这样的。

AMS里有一个oom_adj,会给各个进程进行评分,数字越大越容易被回收,前台进程是0,系统进程是负数,别的是正数。 image.png

内存三大问题

1、内存抖动
profiler -> 内存波动图形呈 锯齿张、GC导致卡顿。
原因是内存碎片很严重。因为android虚拟机的GC算法是标记清楚算法,所以频繁的申请内存、释放内存会让内存碎片化很严重,连续的内存越来越少,让GC非常频繁,导致了内存抖动。案例:在自定义View onDraw()里new对象

2、内存泄漏 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。内存泄露如果越来越严重的话,最终会导致OOM。案例:context被长生命周期的东西引用。没有释放listener

3、内存溢出 即OOM,OOM时会导致程序异常。Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。案例:加载大图、内存泄露、内存抖动

除了程序计数器以外 别的JVM部分都会OOM
image.png

常见分析工具

  1. Memory Analyzer Tools --- MAT

MAT是Eclipse下的内存分析工具。
用法:用cmd指令或者android studio 自带的profiler 截取一段数据,然后下载下来。得到一个.hprof

image.png

image.png
然后用AMT打开,就能看预测泄露地方,有一个图表,基本没用。还有看哪个线程调用这个对象、深浅堆等等。挺难用的。

image.png

  1. android studio自带的profiler

谷歌官网:developer.android.google.cn/studio/prof…

官网超级详细,还有视频,直接看官网的就行。 image.png

选中一段区域,就能查看这段区域内存被具体哪个对象用了多少等等。
我感觉还是用来看大势的,大的内存上涨、下落、起伏图。

  1. LeakCanary

超级推荐LeakCanary!!!永远滴神
具体内存泄露的检测,细节还得用LeakCanary。

集成:
build.gradle里添,跟集成第三方一样。
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'

然后直接跑APP,跑完以后一顿点,点完以后会给推送。写了发现几个对象有泄露。点一下推送蓝会自动下载下来。

image.png

比如这就是我的问题,期初我认为是activity的context没有释放,其实是在dialog里使用了动画animation,但是动画的listener没有释放。 image.png

Android内存泄漏常见场景以及解决方案

1、资源性对象未关闭
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap 等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。

2、注册对象未注销
例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。

3、类的静态变量持有大数据对象
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

4.单例造成的内存泄漏
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封 装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。

5、非静态内部类的静态实例
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源 不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如 果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置 空让GC可以回收,否则还是会内存泄漏。

6、Handler临时性内存泄漏
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的, 则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息, 当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message 持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回 收,引发内存泄漏。解决方案如下所示:
1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这 样在回收时,也可以回收Handler持有的对象。
2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中 有待处理的消息需要处理
(Handler那篇有讲)

7、容器中的对象没清理造成的内存泄漏
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序

8、WebView
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为 WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业 务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

9、使用ListView时造成的内存泄漏
在构造Adapter时,使用缓存的convertView。

10、Bitmap
80%的内存泄露都是Bitmap造成的,Bitmap有Recycle()方法,不用时要及时回收。但是如果遇到要用Bitmap直接用Glide就完事了,自己写10有8.9得出错。

启动速度优化

app启动流程

①点击桌面App图标,Launcher进程采用Binder IPC向AMS进程发起startActivity请求;

②AMS接收到请求后,向zygote进程发送创建进程的请求;

③Zygote进程fork出新的子进程,即App进程;

④App进程,通过Binder IPC向AMS进程发起attachApplication请求;

⑤AMS进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;

⑥App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;

⑦主线程在收到Message后,通过反射机制创建目标Activity,并回调Activity.onCreate()等方法。

⑧到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

image.png

启动状态

APP启动状态分为:冷启动、热启动、温启动。
冷启动:什么都没有,点击桌面图标,启动App。Zygote fork进程...
热启动:app挂在后台,在点击图标切换回来。
温启动:两者之间。

启动耗时统计

1.打log。displayed。有显示

image.png

2.cmd命令 adb shell am start -S -W +包名+Activity名

CPU Profile

具体怎么优化呢?得用到Android Studio自带的分析的工具CPU Profile。

  1. 打开CPU Profile,trace java Methods

image.png

image.png

  1. 跑项目,截开始的那段cpu运行图,得到启动的数据

image.png

  1. 得到具体哪个方法执行时间的情况图

Call Chart:黄、蓝、绿三色。黄色=系统、绿色=自己的、蓝色=第三方。自上而下表示调用顺序。越长就说明执行的时间越长。

如:我发现极光的初始化时间还是挺长的,如果要优化可以将极光延迟初始化。
image.png

还有onCreat占用时间也挺长,主要在setContentView里,看看能不能将布局优化一下。
image.png

Flame Chart:跟Call Chart差不多,就是反过来的图,又称火焰图。 image.png

Top Down Tree:这个就不是图标了,是方法直接的调用关系,每个方法的调用时间。一直往下点,可以找到占用时间较长的,可以优化的地方。从上往下的调用。

Bottom Up Tree:跟top反向从下往上。

image.png

启动白屏

在主题里配置一个背景。
image.png

StrictMode严苛模式

可以在application里配置严苛模式,这样不规范的操作就会有log提示,或者闪退。

image.png

启动优化的点

1). 合理的使用异步初始化、延迟初始化、懒加载机制。
2). 启动过程避免耗时操作,如数据库 I/O操作不要放在主线程执行。
3). 类加载优化:提前异步执行类加载。
4). 合理使用IdleHandler进行延迟初始化。
5). 简化布局

卡顿优化

分析工具

  1. Systrace是Android提供的一款工具,需要运行在Python环境下。

配置:http://www.jianshu.com/p/e73768e66…

Systrace主要是分析掉帧的情况的。帧:android手机一般都是60帧,所以1帧的时间=1000毫秒/60=16.6毫秒。也就是android手机16.6毫秒刷新一次,超过这个数就是掉帧了

会有三色球,绿=正常,黄=一点不正常,红=掉帧严重。
少几个没啥事,大面积的出现红、黄,就需要研究为啥掉帧了。 image.png

可以看上面有一条CPU的横轴,绿色=正在执行,蓝色=等待,可以运行。紫色=休眠。白色=休眠阻塞。如果是出现了紫色就说明IO等耗时操作导致掉帧。如果紫+蓝比较多,说明cpu拿不到时间片,cpu很忙。

CPU Profile

这个就能看那个方法运行多少时间等,所以可以直接用android studio自带的分析。

一般是在top down里一直点,耗时较多的,然后点到自己熟悉的地方,挨个分析。
这是一个漫长的耗时的过程,可能找半天只找到几个地方能优化,然后每个几毫秒,加起来也没有直观的变快,但是性能优化就是这样的一个过程,积少成多。
如:我经过查找就发现adapter的 notifyDataSetChanged因为不小心,有些地方多次调用了。
甚至还有没有在线程进行io操作。 image.png

布局优化

经过上面的一顿操作,发现占时间大块的少不了setContentView。说明布局渲染视图还是挺费时的。

减少层级

自定义Viewmeasure、layout、draw这三个过程,都需要对整个视图树自顶向下的遍历,而且很多情况都会多次触发整个遍历过程(Linearlayout 的 weight等),所以如果层级太深,就会让整个绘制过程变慢,从而造成启动速度慢、卡顿等问题。
而onDraw在频繁刷新时可能多次触发,因此 onDraw更不能做耗时操作,同时需要注意内存抖动。对于布局性能的检测,依然可以使用systrace与traceview按 照绘制流程检查绘制耗时函数。

工具Layout Inspector
DecorView开始。content往下才是自己写的布局。
image.png

重复的布局使用include。
一个布局+到另一个上,如果加上以后,有两一样的ViewGroup,可以把被加的顶层控件的ViewGroup换成merge
ViewStub:失败提示框等。需要展示的时候才创建,放在那不占位置。

过度渲染

一块区域内(一个像素),如果被绘制了好几次,就是过度渲染。
过度绘制不可避免,但是应该尽量的减少。
手机->开发者模式->GPU 过度绘制 打开以后能看到不同颜色的块,越红就说明过度绘制的越严重。对于严重的地方得减少过度绘制。

1.移除布局中不需要的背景。
2.降低透明度。
3.使视图层次结构扁平化。

布局加载优化

异步加载布局,视情况而用。

implementation"androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"

new AsyncLayoutInflater(this)
.inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
//......
}
});
收起阅读 »

iOS性能优化-卡顿

iOS
卡顿原因 成像 图像的显示可以简单理解成先经过CPU的计算/排版/编解码等操作,然后交由GPU去完成渲染放入缓冲中,当视频控制器接受到vSync时会从缓冲中读取已经渲染完成的帧并显示到屏幕上。 卡顿原理 iOS手机默认刷新率是60hz,所以GPU渲染只要达...
继续阅读 »

卡顿原因


成像


图像的显示可以简单理解成先经过CPU的计算/排版/编解码等操作,然后交由GPU去完成渲染放入缓冲中,当视频控制器接受到vSync时会从缓冲中读取已经渲染完成的帧并显示到屏幕上。


卡顿原理



iOS手机默认刷新率是60hz,所以GPU渲染只要达到60fps就不会产生卡顿。

以60fps为例,vSync会每16.67ms发出,如在16.67ms内没有准备好下一帧数据就会使画面停留在上一帧,产生卡顿,例如图中第3帧的渲染。

解决思路:尽量减小CPU和GPU的资源消耗



一些概念:

CPU:负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)

GPU:负责纹理的渲染(将数据渲染到屏幕)

垂直同步技术:让CPU和GPU在收到vSync信号后再开始准备数据,防止撕裂感和跳帧,通俗来讲就是保证每秒输出的帧数不高于屏幕显示的帧数。

双缓冲技术:iOS是双缓冲机制,前帧缓存和后帧缓存,cpu计算完GPU渲染后放入缓冲区中,当gpu下一帧已经渲染完放入缓冲区,且视频控制器已经读完前帧,GPU会等待vSync(垂直同步信号)信号发出后,瞬间切换前后帧缓存,并让cpu开始准备下一帧数据

安卓4.0后采用三重缓冲,多了一个后帧缓冲,可降低连续丢帧的可能性,但会占用更多的CPU和GPU



卡顿优化-CPU



  • 尽量用轻量级的对象,比如用不到事件处理的地方使用CALayer取代UIView

  • 尽量提前计算好布局(例如cell行高)

  • 不要频繁地调用和调整UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的调用和修改(UIView的显示属性实际都是CALayer的映射,而CALayer本身是没有这些属性的,都是初次调用属性时通过resolveInstanceMethod添加并创建Dictionry保存的,耗费资源)

  • Autolayout会比直接设置frame消耗更多的CPU资源,当视图数量增长时会呈指数级增长

  • 图片的size最好刚好跟UIImageView的size保持一致,减少图片显示时的处理计算

  • 控制一下线程的最大并发数量

  • 尽量把耗时的操作放到子线程

  • 文本处理(尺寸计算、绘制、CoreText和YYText)

    1. 计算文本宽高boundingRectWithSize:options:context: 和文本绘制drawWithRect:options:context:放在子线程操作

    2. 使用CoreText自定义文本空间,在对象创建过程中可以缓存宽高等信息,避免像UILabel/UITextView需要多次计算(调整和绘制都要计算一次),且CoreText直接使用了CoreGraphics占用内存小,效率高。(YYText)



  • 图片处理(解码、绘制)

    图片都需要先解码成bitmap才能渲染到UI上,iOS创建UIImage,不会立刻进行解码,只有等到显示前才会在主线程进行解码,固可以使用Core Graphics中的CGBitmapContextCreate相关操作提前在子线程中进行强制解压缩获得位图

    (YYImage/SDWebImage/kingfisher的对比)
SDWebImage的使用:
CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);

// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}

// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);

return imageWithoutAlpha;

卡顿优化-GPU



  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示

  • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸

  • GPU会将多个视图混合在一起再去显示,混合的过程会消耗CPU资源,尽量减少视图数量和层次

  • 减少透明的视图(alpha<1),不透明的就设置opaque为YES,GPU就不会去进行alpha的通道合成

  • 尽量避免出现离屏渲染



离屏渲染

在OpenGL中,GPU有2种渲染方式

On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作

Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作


离屏渲染消耗性能的原因

需要创建新的缓冲区

离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕


哪些操作会触发离屏渲染?



  • 光栅化,layer.shouldRasterize = YES


  • 遮罩,layer.mask


  • 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0

    考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片


  • 阴影,layer.shadowXXX

    如果设置了layer.shadowPath就不会产生离屏渲染




卡顿监控


Xcode自带Instruments


在开发阶段,可以直接使用Instrument来检测性能问题,Time Profiler查看与CPU相关的耗时操作,Core Animation查看与GPU相关的渲染操作。


FPS(CADisplayLink)


正常情况下,App的FPS只要保持在50~60之间,用户就不会感到界面卡顿。通过向主线程添加CADisplayLink我们可以接收到每次屏幕刷新的回调,从而统计出每秒屏幕刷新次数。这种方案最常见,例如YYFPSLabel,且只用了CADisplayLink,实现成本较低,但由于只能在CPU空闲时才去回调,无法精确采集到卡顿时调用栈信息,可以在开发阶段作为辅助手段使用。


//
// YYFPSLabel.m
// YYKitExample
//
// Created by ibireme on 15/9/3.
// Copyright (c) 2015 ibireme. All rights reserved.
//

#import "YYFPSLabel.h"
//#import <YYKit/YYKit.h>
#import "YYText.h"
#import "YYWeakProxy.h"

#define kSize CGSizeMake(55, 20)

@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
UIFont *_font;
UIFont *_subFont;

NSTimeInterval _llll;
}

- (instancetype)initWithFrame:(CGRect)frame {
if (frame.size.width == 0 && frame.size.height == 0) {
frame.size = kSize;
}
self = [super initWithFrame:frame];

self.layer.cornerRadius = 5;
self.clipsToBounds = YES;
self.textAlignment = NSTextAlignmentCenter;
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];

_font = [UIFont fontWithName:@"Menlo" size:14];
if (_font) {
_subFont = [UIFont fontWithName:@"Menlo" size:4];
} else {
_font = [UIFont fontWithName:@"Courier" size:14];
_subFont = [UIFont fontWithName:@"Courier" size:4];
}
// 创建CADisplayLink并添加到主线程的RunLoop中
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
return self;
}

- (void)dealloc {
[_link invalidate];
}

- (CGSize)sizeThatFits:(CGSize)size {
return kSize;
}

//刷新回调时去计算fps
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}

_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
float fps = _count / delta;
_count = 0;

CGFloat progress = fps / 60.0;
UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];

NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
[text yy_setColor:color range:NSMakeRange(0, text.length - 3)];
[text yy_setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
text.yy_font = _font;
[text yy_setFont:_subFont range:NSMakeRange(text.length - 4, 1)];

self.attributedText = text;
}

@end

RunLoop


关于RunLoop,推荐参考深入理解RunLoop,这里只列出其简化版的状态。



经典图片
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)

// 2.RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3.RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 4.RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle)
// 5.执行被加入的block等Source1事件
__CFRunLoopDoBlocks(runloop, currentMode);

// 6.RunLoop 的线程即将进入休眠(sleep)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);

// 7.调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)


// 进入休眠


// 8.RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting

// 9.1.如果一个 Timer 到时间了,触发这个Timer的回调
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())

// 9.2.如果有dispatch到main_queue的block,执行bloc
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

// 9.3.如果一个 Source1 (基于port) 发出事件了,处理这个事件
__CFRunLoopDoSource1(runloop, currentMode, source1, msg);

// 10.RunLoop 即将退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);


由于source0处理的是app内部事件,包括UI事件,所以可知处理事件主要是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间。我们可以创建一个子线程去监听主线程状态变化,通过dispatch_semaphore在主线程进入状态时发送信号量,子线程设置超时时间循环等待信号量,若超过时间后还未接收到主线程发出的信号量则可判断为卡顿,保存响应的调用栈信息去进行分析。线上卡顿的收集多采用这种方式,可将卡顿信息上传至服务器且用户无感知。


#pragma mark - 注册RunLoop观察者

//在主线程注册RunLoop观察者
- (void)registerMainRunLoopObserver
{
//监听每个步凑的回调
CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL};
self.runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopObserver, kCFRunLoopCommonModes);
}

//观察者方法
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
self.runLoopActivity = activity;
//触发信号,说明开始执行下一个步骤。
if (self.semaphore != nil)
{
dispatch_semaphore_signal(self.semaphore);
}
}

#pragma mark - RunLoop状态监测

//创建一个子线程去监听主线程RunLoop状态
- (void)createRunLoopStatusMonitor
{
//创建信号
self.semaphore = dispatch_semaphore_create(0);
if (self.semaphore == nil)
{
return;
}

//创建一个子线程,监测Runloop状态时长
dispatch_async(dispatch_get_global_queue(0, 0), ^
{
while (YES)
{
//如果观察者已经移除,则停止进行状态监测
if (self.runLoopObserver == nil)
{
self.runLoopActivity = 0;
self.semaphore = nil;
return;
}

//信号量等待。状态不等于0,说明状态等待超时
//方案一->设置单次超时时间为500毫秒
long status = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC));
if (status != 0)
{
if (self.runLoopActivity == kCFRunLoopBeforeSources || self.runLoopActivity == kCFRunLoopAfterWaiting)
{
...
//发生超过500毫秒的卡顿,此时去记录调用栈信息
}
}
/*
//方案二->连续5次卡顿50ms上报
long status = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (status != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}

if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
//保存调用栈信息
}
}
timeoutCount = 0;
*/
}
});
}


子线程Ping


根据卡顿发生时,主线程无响应的原理,创建一个子线程循环去Ping主线程,Ping之前先设卡顿置标志为True,再派发到主线程执行设置标志为False,最后子线程在设定的阀值时间内休眠结束后判断标志来判断主线程有无响应。该方法的监控准确性和性能损耗与ping频率成正比。

代码部分来源于ANREye

private class AppPingThread: Thread {


private let semaphore = DispatchSemaphore(value: 0)
//判断主线程是否卡顿的标识
private var isMainThreadBlock = false

private var threshold: Double = 0.4

fileprivate var handler: (() -> Void)?

func start(threshold:Double, handler: @escaping AppPingThreadCallBack) {
self.handler = handler
self.threshold = threshold
self.start()
}

override func main() {

while self.isCancelled == false {
self.isMainThreadBlock = true
//主线程去重置标识
DispatchQueue.main.async {
self.isMainThreadBlock = false
self.semaphore.signal()
}

Thread.sleep(forTimeInterval: self.threshold)
//若标识未重置成功则说明再设置的阀值时间内主线程未响应,此时去做响应处理
if self.isMainThreadBlock {
//采集卡顿调用栈信息
self.handler?()
}

_ = self.semaphore.wait(timeout: DispatchTime.distantFuture)
}
}


}

参考文章:

iOS 保持界面流畅的技巧

屏幕成像原理

iOS 性能优化总结

质量监控-卡顿检测



链接:https://www.jianshu.com/p/4151e4def785

收起阅读 »

iOS 简单监测iOS卡顿的demo

iOS
本文的demo代码也会更新到github上。 做这个demo思路来源于微信team的:微信iOS卡顿监控系统。 主要思路:通过监测Runloop的kCFRunLoopAfterWaiting,用一个子线程去检查,一次循环是否时间太长。 其中主要涉及到了runl...
继续阅读 »

本文的demo代码也会更新到github上。


做这个demo思路来源于微信team的:微信iOS卡顿监控系统

主要思路:通过监测Runloop的kCFRunLoopAfterWaiting,用一个子线程去检查,一次循环是否时间太长。

其中主要涉及到了runloop的原理。关于整个原理:深入理解RunLoop讲解的比较仔细。

以下就是runloop大概的运行方式:

  /// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {

/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

/// 5. GCD处理main block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();


/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


} while (...);

/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

其中UI主要集中在__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);之前。

获取kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态就可以知道是否有卡顿的情况。


NSTimer的实现


具体代码如下:


//
// MonitorController.h
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/13.
// Copyright © 2016年 game3108. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface MonitorController : NSObject
+ (instancetype) sharedInstance;
- (void) startMonitor;
- (void) endMonitor;
- (void) printLogTrace;
@end

//
// MonitorController.m
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/13.
// Copyright © 2016年 game3108. All rights reserved.
//

#import "MonitorController.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>

@interface MonitorController(){
CFRunLoopObserverRef _observer;
double _lastRecordTime;
NSMutableArray *_backtrace;
}

@end

@implementation MonitorController

static double _waitStartTime;

+ (instancetype) sharedInstance{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

- (void) startMonitor{
[self addMainThreadObserver];
[self addSecondaryThreadAndObserver];
}

- (void) endMonitor{
if (!_observer) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = NULL;
}

#pragma mark printLogTrace
- (void)printLogTrace{
NSLog(@"====================堆栈\n %@ \n",_backtrace);
}

#pragma mark addMainThreadObserver
- (void) addMainThreadObserver {
dispatch_async(dispatch_get_main_queue(), ^{
//建立自动释放池
@autoreleasepool {
//获得当前thread的Run loop
NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];

//设置Run loop observer的运行环境
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};

//创建Run loop observer对象
//第一个参数用于分配observer对象的内存
//第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
//第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
//第四个参数用于设置该observer的优先级
//第五个参数用于设置该observer的回调函数
//第六个参数用于设置该observer的运行环境
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);

if (_observer) {
//将Cocoa的NSRunLoop类型转换成Core Foundation的CFRunLoopRef类型
CFRunLoopRef cfRunLoop = [myRunLoop getCFRunLoop];
//将新建的observer加入到当前thread的run loop
CFRunLoopAddObserver(cfRunLoop, _observer, kCFRunLoopDefaultMode);
}
}
});
}

void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
//The entrance of the run loop, before entering the event processing loop.
//This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode
case kCFRunLoopEntry:
NSLog(@"run loop entry");
break;
//Inside the event processing loop before any timers are processed
case kCFRunLoopBeforeTimers:
NSLog(@"run loop before timers");
break;
//Inside the event processing loop before any sources are processed
case kCFRunLoopBeforeSources:
NSLog(@"run loop before sources");
break;
//Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire.
//This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds.
//It also does not occur in a particular iteration of the event processing loop if a version 0 source fires
case kCFRunLoopBeforeWaiting:{
_waitStartTime = 0;
NSLog(@"run loop before waiting");
break;
}
//Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up.
//This activity occurs only if the run loop did in fact go to sleep during the current loop
case kCFRunLoopAfterWaiting:{
_waitStartTime = [[NSDate date] timeIntervalSince1970];
NSLog(@"run loop after waiting");
break;
}
//The exit of the run loop, after exiting the event processing loop.
//This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode
case kCFRunLoopExit:
NSLog(@"run loop exit");
break;
/*
A combination of all the preceding stages
case kCFRunLoopAllActivities:
break;
*/
default:
break;
}
}

#pragma mark addSecondaryThreadAndObserver
- (void) addSecondaryThreadAndObserver{
NSThread *thread = [self secondaryThread];
[self performSelector:@selector(addSecondaryTimer) onThread:thread withObject:nil waitUntilDone:YES];
}

- (NSThread *)secondaryThread {
static NSThread *_secondaryThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_secondaryThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_secondaryThread start];
});
return _secondaryThread;
}

- (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"monitorControllerThread"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
}
}

- (void) addSecondaryTimer{
NSTimer *myTimer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
}

- (void)timerFired:(NSTimer *)timer{
if ( _waitStartTime < 1 ){
return;
}
double currentTime = [[NSDate date] timeIntervalSince1970];
double timeDiff = currentTime - _waitStartTime;
if (timeDiff > 2.0){
if (_lastRecordTime - _waitStartTime < 0.001 && _lastRecordTime != 0){
NSLog(@"last time no :%f %f",timeDiff, _waitStartTime);
return;
}
[self logStack];
_lastRecordTime = _waitStartTime;
}
}

- (void)logStack{
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
int i;
_backtrace = [NSMutableArray arrayWithCapacity:frames];
for ( i = 0 ; i < frames ; i++ ){
[_backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
}

@end

主要内容是首先在主线程注册了runloop observer的回调myRunLoopObserver

每次小循环都会记录一下kCFRunLoopAfterWaiting的时间_waitStartTime,并且在kCFRunLoopBeforeWaiting制空。


另外开了一个子线程并开启他的runloop(模仿了AFNetworking的方式),并加上一个timer每隔1秒去进行监测。


如果当前时长与_waitStartTime差距大于2秒,则认为有卡顿情况,并记录了当前堆栈信息。


PS:整个demo写的比较简单,最后获取堆栈也仅获取了当前线程的堆栈信息([NSThread callStackSymbols]有同样效果),也在寻找获取所有线程堆栈的方法,欢迎指点一下。




更新:


了解到 plcrashreporter (github地址) 可以做到获取所有线程堆栈。




更新2:


这篇文章也介绍了监测卡顿的方法:检测iOS的APP性能的一些方法

通过Dispatch Semaphore保证同步这里记录一下。


写一个Semaphore版本的代码,也放在github上:


//
// SeMonitorController.h
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/14.
// Copyright © 2016年 game3108. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface SeMonitorController : NSObject
+ (instancetype) sharedInstance;
- (void) startMonitor;
- (void) endMonitor;
- (void) printLogTrace;
@end
//
// SeMonitorController.m
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/14.
// Copyright © 2016年 game3108. All rights reserved.
//

#import "SeMonitorController.h"
#import <libkern/OSAtomic.h>
#import <execinfo.h>

@interface SeMonitorController(){
CFRunLoopObserverRef _observer;
dispatch_semaphore_t _semaphore;
CFRunLoopActivity _activity;
NSInteger _countTime;
NSMutableArray *_backtrace;
}

@end

@implementation SeMonitorController

+ (instancetype) sharedInstance{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}

- (void) startMonitor{
[self registerObserver];
}

- (void) endMonitor{
if (!_observer) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = NULL;
}

- (void) printLogTrace{
NSLog(@"====================堆栈\n %@ \n",_backtrace);
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
SeMonitorController *instrance = [SeMonitorController sharedInstance];
instrance->_activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = instrance->_semaphore;
dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

// 创建信号
_semaphore = dispatch_semaphore_create(0);

// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (_activity==kCFRunLoopBeforeSources || _activity==kCFRunLoopAfterWaiting)
{
if (++_countTime < 5)
continue;
[self logStack];
NSLog(@"something lag");
}
}
_countTime = 0;
}
});
}

- (void)logStack{
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
int i;
_backtrace = [NSMutableArray arrayWithCapacity:frames];
for ( i = 0 ; i < frames ; i++ ){
[_backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
}

@end

用Dispatch Semaphore简化了代码复杂度,更加简洁。


参考资料


本文csdn地址

1.微信iOS卡顿监控系统

2. iphone——使用run loop对象

3.深入理解RunLoop

4.检测iOS的APP性能的一些方法

5.iOS实时卡顿监控



链接:https://www.jianshu.com/p/71cfbcb15842

收起阅读 »

iOS 调试:通过 Safari/Chrome 调试 WebView

iOS 调试:通过 Safari/Chrome 调试 WebView主要汇总通过 Safari 和 Chrome 调试网页的步骤Safari 调试 WebView1、真机/模拟器开启 Safari 调试开关如果需要远程调试iOS Safari,必须启用Web检...
继续阅读 »

iOS 调试:通过 Safari/Chrome 调试 WebView

主要汇总通过 Safari 和 Chrome 调试网页的步骤

Safari 调试 WebView

1、真机/模拟器开启 Safari 调试开关

如果需要远程调试iOS Safari,必须启用Web检查功能

  • 设置 -> Safari -> 高级 -> 启动”Web检查“



2、Safari 开启调试模式

  • Safari浏览器 -> 偏好设置 -> 高级 -> 勾选“在菜单栏中显示开发菜单”



3、开始调试 WebView

  • 将手机通过数据线连接到mac上

  • 打开 Safari 浏览器,运行手机 app 中的 Web 界面

  • 在 Safari -> 开发 中选择连接的手机,并找到调试的网页




Chrome 调试 WebView

1、准备工作

  • 安装部署ios-webkit-debug-proxy,在终端中输入
brew install ios-webkit-debug-proxy
  • 启动 proxy,在终端输入以下命令
ios_webkit_debug_proxy -f chrome-devtools://devtools/bundled/inspector.html

运行结果如下所示



2、调试

  • 在 Chrome 中打开 localhost:9221 ,可以看到当前已连接的设备列表




在app中打开Web页面,并在Chrome中点击local进入新页面,并右键转到该连接的页面





最后在Web页面中,右键,选择检查即可






作者:Style_月月
链接:https://www.jianshu.com/p/99b52270c59d

收起阅读 »

性能优化面试官想听的是什么?别再说那些老掉牙的性能优化了

网上性能优化的文章太多了,都说如何如何请求优化代码优化之类的,所有人都知道的事,而且实际工作中根本不可能每个项目都用到那些全部,而是应该对我们的项目有针对性的优化,你说是吗? 比如 说一下前端性能优化? 你平时是怎么做性能优化的? 等等类似这样的问题,不过就是...
继续阅读 »

网上性能优化的文章太多了,都说如何如何请求优化代码优化之类的,所有人都知道的事,而且实际工作中根本不可能每个项目都用到那些全部,而是应该对我们的项目有针对性的优化,你说是吗?


比如


说一下前端性能优化?


你平时是怎么做性能优化的?


等等类似这样的问题,不过就是换汤不换药罢了


好吧,先上药


这性能优化呢,它是一个特别大的方向,因为每个项目可能优化的点都不一样,每一种框架或者每一种客户端可以优化的点也都不一样


总的来说,现在B/S架构下都是前端向后端请求,后端整理好数据给客户端返回,然后客户端再进行数据处理、到渲染将界面展示出来,这么一个大致流程


那我们优化就是要基于这一过程,说白了我们能够优化的点,就只有两个大的方向


一是更快的网络通信,就是客户端和服务端之间,请求或响应时 数据在路上的时间让它更快


二是更快的数据处理,指的是



  • 服务器接到请求之后,更快的整理出客户端需要的数据

  • 客户端收到响应的数据后,更快的给用户展示 以及 交互时更快的处理


然后!开始blablabla.....




更快网络通信方面比如:CDN做全局负载均衡、CDN缓存、域名分片、资源合并、雪碧图、字体图标、http缓存,以减少请求;还有gzip/br压缩、代码压缩、减少头信息、减少Cookie、使用http2、用jpg/webp、去除元数据等等,blablabla.....


更快数据处理方面比如:SSR、SSG、预解析、懒加载、按需引入、按需加载、CSS放上面、JS放下面、语义化标签、动画能用CSS就不用JS、事件委托、减少重排、等等代码优化,blablabla.....




.....


请直接把上面的总结成一句话 给面试官


请求优化、代码优化、打包优化都是常规操作,像雅虎34条军规,都知道的事就不用说了


因为每个项目优化的点可能都不一样,所以优化主要 还是根据自己的项目来


要么跟人家聊一下框架优化,深入原理也很不错


具体要优化什么主要还是看浏览器(Chrome为例)开发者工具里的 LighthousePerformance


image.png


Lighthouse 是生成性能分析报告。可以看到每一项的数据和评分和建议优化的点,比如哪里图片过大


Performance 是运行时数据,可以看到更多细节数据。比如阻塞啊,重排重绘啊,都会体现出来,够不够细节


然后再去根据这些报告和性能指标体现出来的情况,有针对性的去不断优化我们的项目


Lighthouse


直接在Chrome开发者工具中打开


或者 Node 12.x或以上的版本可以直接安装在本地


npm install -g lighthouse

安装好后,比如生成掘金的性能分析报告,一句代码就够了,然后就会生成一个html文件


lighthouse https://juejin.cn/

不管是浏览器还是安装本地的,生成好的报告都是长的一模一样的,一个英文的html文件,翻译了一张给大家看看,如图


image.png


如图,分析报告内容一共五个大项,每一项满分100分,然后下面是再把每项分别展开说明


还是看一下英文版吧,一个程序员必须要养成这个习惯


image.png


如图,五项分别是



  • Performance:这个又分为三块性能指标可优化的项和手动诊断,看这一块就可以我们就可以优化很多东西了

  • Accessibility:无障碍功能分析。比如前景色和背景色没有足够对比度、图片有没有alt属性、a链接有没有可识别名称等等

  • Best Practices:最佳实践策略。比如图片纵横比不正确,控制台有没有报错信息等

  • SEO:有没有SEO搜索引擎优化的一些东西

  • PWA:官方说法是衡量站点是否快速、可靠和可安装。在国内浏览器内核不统一,小程序又这么火,所以好像没什么落地的场景


然后我们知道了这么多信息,是不是就可以对我们的项目诊断和针对性的优化了呢


是不是很棒


image.png


Performance


如果说 Lighthouse 是开胃菜,那 Performance 就是正餐了


它记录了网站运行过程中性能数据。我们回放整个页面执行过程的话,就可以定位和诊断每个时间段内页面运行情况,不过它没有性能评分,也没有优化建议,只是将采集到的数据按时间线的方式展现


打开 Performance,勾选 Memory,点击左边的 Record 开始录制,然后执行操作或者刷新页面,然后再点一下(Stop)就结束录制,生成数据


image.png


如图


image.png


概况面板


里面有页面帧速(FPS)、白屏时间、CPU资源消耗、网络加载情况、V8内存使用量(堆)等等,按时间顺序展示。


那么怎么看这个图表找到可能存在问题的地方呢




  • 如果FPS(看图右上角)图表上出现红色块,就表示红色块附近渲染出一帧的时间太长了,就有可能导致卡顿




  • 如果CPU图形占用面积太大,表示CPU使用率高,就可能是因为某个JS占用太多主线程时间,阻塞其他任务执行




  • 如果V8的内存使用量一直在增加,就可能因为某种原因导致内存泄露



    • 一次查看内存占用情况后,看当前内存占用趋势图,走势呈上升趋势,可以认为存在内存泄露

    • 多次查看内存占用情况后截图对比,比较每次内存占用情况,如果呈上升趋势,也可以认为存在内存泄露




通过概览面板定位到可能存在问题的时间节点后,怎么拿到更进一步的数据来分析导致该问题的直接原因呢


就是点击时间线上有问题的地方,然后这一块详细内容就会显示在性能面板中


性能面板


比如我们点击时间线上的某个位置(如红色块),性能面板就会显示该时间节点内的性能数据,如图


image.png


性能面板上会列出很多性能指标的项,图中左边,比如



  • Main 指标:是渲染主线程的任务执行记录

  • Timings 指标:记录如FP、FCP、LCP等产生一些关键时间点的数据信息(下面有介绍)

  • Interactions 指标:记录用户交互操作

  • Network 指标:是页面每个请求所耗时间

  • Compositor 指标:是合成线程的任务执行记录

  • GPU 指标:是GPU进程的主线程的任务执行记录

  • Chrome_ChildIOThread 指标:是IO线程的任务执行记录,里面有用户输入事件,网络事件,设备相关等事件

  • Frames 指标:记录每一帧的时间、图层构造等信息

  • .......


Main 指标


性能指标项有很多,而我使用的时候多数时间都是分析Main指标,如图


image.png


上面第一行灰色的,写着 Task 的,一个 Task 就是一个任务


下面黄色紫色的都是啥呢,那是一个任务里面的子任务


我们放大,举个例子


image.png


Task 是一个任务,下面的就是 Task 里面的子任务,这个图用代码表示就是


function A(){
A1()
A2()
}
function Task(){
A()
B()
}
Task()

是不是就好理解得多了


所以我们就可以选中有问题的,比如标红的 Task ,然后放大(滑动鼠标就可放大),看里面具体的耗时点


比如都做了哪些操作,哪些函数耗时了多少,代码有压缩的话看到的就是压缩后的函数名。然后我们点击一下某个函数,在面板最下面,就会出现代码的信息,是哪个函数,耗时多少,在哪个文件上的第几行等。这样我们就很方便地定位到耗时函数了


还可以横向切换 tab ,看它的调用栈等情况,更方便地找到对应代码


具体大家可以试试~


Timings 指标


Timings 指标也需要注意,如图


image.png


它上面的FP、FCP、DCL、L、LCP这些都是个啥呢


别着急


上面说了 Timings 表示一些关键时间点的数据信息,那么表示哪些时间呢,怎么表示的呢?




  • FP表示首次绘制。记录页面第一次绘制像素的时间




  • FCP表示首次内容绘制(只有文本、图片(包括背景图)、非白色的canvas或SVG时才被算作FCP)




  • LCP最大内容绘制,是代表页面的速度指标。记录视口内最大元素绘制时间,这个会随着页面渲染变化而变化




  • FID首次输入延迟,代表页面交互体验的指标。记录FCP和TTI之间用户首次交互的时间到浏览器实际能够回应这种互动的时间




  • CLS累计位移偏移,代表页面稳定的指标。记录页面非预期的位移,比如渲染过程中插入一张图片或者点击按钮动态插入一段内容等,这时候就会触发位移




  • TTI首次可交互时间。指在视觉上已经渲染了,完全可以响应用户的输入了。是衡量应用加载所需时间并能够快速响应用户交互的指标。与FMP一样,很难规范化适用于所有网页的TTI指标定义




  • DCL: 表示HTML加载完成时间


    注意:DCL和L表示的时间在 Performance 和 NetWork 中是不同的,因为 Performance 的起点是点击录制的时间,Network中起点时间是 fetchStart 时间(检查缓存之前,浏览器准备好使用http请求页面文档的时间)




  • L表示页面所有资源加载完成时间




  • TBT阻塞总时间。记录FCP到TTI之间所有长任务的阻塞时间总和




  • FPS每秒帧率。表示每秒钟画面更新次数,现在大多数设备是60帧/秒




  • FMP首次有意义的绘制。是页面主要内容出现在屏幕上的时间,这是用户感知加载体验的主要指标。有点抽象,因为目前没有标准化的定义。因为很难用通用的方式来确定各种类型的页面的关键内容




  • FCI首次CPU空闲时间。表示网页已经满足了最小程度的与用户发生交互行为的时间




好了,然后根据指标体现出来的问题,有针对性的优化就好


结语


点赞支持、手留余香、与有荣焉


感谢你能看到这里,加油哦!


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

收起阅读 »

微信小程序中wxs文件的妙用

wxs文件是小程序中的逻辑文件,它和wxml结合使用。 不同于js, wxs可以直接作用到视图层,而不需要进行视图层和逻辑层的setData数据交互; 因为这个特性,wxs非常适合应用于优化小程序的频繁交互操作中; 应用 过滤器 在IOS环境中wxs的运行...
继续阅读 »

wxs文件是小程序中的逻辑文件,它和wxml结合使用。

不同于js, wxs可以直接作用到视图层,而不需要进行视图层和逻辑层的setData数据交互;

因为这个特性,wxs非常适合应用于优化小程序的频繁交互操作中;



应用


过滤器



在IOS环境中wxs的运行速度要远高于js,在android中两者表现相当。

使用wxs作为过滤器也可以一定幅度提升性能;让我们来看一个过滤器来了解其语法。



wxs文件:


var toDecimal2 = function (x) {
var f = parseFloat(x);
if (isNaN(f)) {
return '0.00'
}
var f = Math.round(x * 100) / 100;
var s = f.toString();
var rs = s.indexOf('.');
if (rs < 0) {
rs = s.length;
s += '.';
}
while (s.length <= rs + 2) {
s += '0';
}
return s;
}
module.exports = toDecimal2

上面的代码实现了数字保留两位小数的功能。


wxml文件:


<wxs src="./filter.wxs" module="filter"></wxs>
<text>{{filter(1)}}</text>

基本语法:在视图文件中通过wxs标签引入,module值是自定义命名,之后在wxml中可以通过filter调用方法



上面的代码展示了 wxs的运行逻辑,让我们可以像函数一样调用wxs中的方法;

下面再看一下wxs针对wxml页面事件中的表现。



拖拽



使用交互时(拖拽、上下滑动、左右侧滑等)如果依靠js逻辑层,会需要大量、频繁的数据通信。卡顿是不可避免的;

使用wxs文件替代交互,不需要频繁使用setData导致实时大量的数据通信,从而节省性能。



下面展示一个拖拽例子


wxs文件:


function touchstart(event) {
var touch = event.touches[0] || event.changedTouches[0]
startX = touch.pageX
startY = touch.pageY
}

事件参数event和js中的事件event内容中touches和changedTouches属性一致


function touchmove(event, ins) {
var touch = event.touches[0] || event.changedTouches[0]
ins.selectComponent('.div').setStyle({
left: startX - touch.pageX + 'px',
top: startY - touch.pageY + 'px'
})
}

ins(第二个参数)为触发事件的视图层wxml上下文。可以查找页面所有元素并设置style,class(足够完成交互效果)



注意:在参数event中同样有一个上下文实例instance;

event中的实例instance作用范围是触发事件的元素内,而事件的ins参数作用范围是触发事件的组件内。



module.exports = {
touchstart: touchstart,
touchmove: touchmove,
}

最后将方法抛出去,给wxml文件引用。


wxml文件


<wxs module="action" src="./movable.wxs"></wxs> 
<view class="div" bindtouchstart="{{action.touchstart}}" bindtouchmove="{{action.touchmove}}"></view>


上面的例子,解释了事件的基本交互用法。



文件之中相互传参



在事件交互中,少不了需要各个文件之中传递参数。 下面是比较常用的几种



wxs传参到js逻辑层


wxs文件中:


var dragStart = function (e, ins) {
ins.callMethod('callback','sldkfj')
}

js文件中:


callback(e){
console.log(e)
}
// sldkfj


使用callMethod方法,可以执行js中的callback方法。也可以实现传参;



js逻辑层传参到wxs文件


js文件中:


handler(e){
this.setData({a:1})
}

wxml文件:


<wxs module="action" src="./movable.wxs"></wxs> 
<view change:prop="{{action.change}}" prop="{{a}}"></view>

wxs文件中:


change(newValue,oldValue){}

js文件中的参数传递到wxs需要通过wxml文件中转。

js文件触发handler事件,改变a的值之后,最新的a传递到wxml中。

wxml中prop改变会触发wxs中的change事件。change中则会接收到最新prop值


wxs中获取dataset(wxs中获取wxml数据)


wxs中代码


var dragStart = function (e) {
var index = e.currentTarget.dataset.index;
var index = e.instance.getDataset().index;
}

上面有提到e.instance是当前触发事件的元素实例。

所以e.instance.getDataset()获取的是当前触发事件的dataset数据集


注意点



wxs和js为不同的两个脚本语言。但是语法和es5基本相同,确又不支持es6语法;
getState 在多元素交互中非常实用,欢迎探索。



不知道是否是支持的语法可以跳转官网文档;
wxs运算符、语句、基础类库、数据类型



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

收起阅读 »

使用 Electron 开发桌面应用

介绍 Electron,官方简介:使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。 出于个人爱好,接触到了Electron,并开始尝试开发一些本地小工具。 以下是对开发过程做的一个经验总结,便于回顾和交流。 使用 下面来构建一...
继续阅读 »

介绍



Electron,官方简介:使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。

出于个人爱好,接触到了Electron,并开始尝试开发一些本地小工具。

以下是对开发过程做的一个经验总结,便于回顾和交流。



使用



下面来构建一个简单的electron应用。

应用源码地址:github.com/zhuxingmin/…



1. 项目初始化



项目基于 create-react-app@3.3.0 搭建,执行命令生成项目



// 全局安装 create-react-app
npm install -g create-react-app

// 执行命令生成项目
create-react-app electronApp

// 安装依赖并启动项目
yarn && yarn start


此时启动的只是一个react应用,下一步安装 electron electron-updater electron-builder electron-is-dev等库



yarn add electron electron-updater electron-builder electron-is-dev

2. 配置package.json



安装完项目依赖后,在package.json中添加electron应用相关配置。



"version": "0.0.1"              // 设置应用版本号 
"productName": "appName" // 设置应用名称
"main": "main.js" // 设置应用入口文件
"homepage": "." // 设置应用根路径


scripts中添加应用命令,启动以及打包。



"estart": "electron ."              // 启动
"package-win": "electron-builder" // 打包 (此处以windows平台为例,故命名为package-win)


新增build配置项,添加打包相关配置。

主要有以下几个配置:


"build": {
// 自定义appId 一般以安装路径作为id windows下可以在 PowerShell中输入Get-StartApps查看应用id
"appId": "org.develar.zhuxingmin",
// 打包压缩 "store" | "normal"| "maximum"
"compression": "store",
// nsis安装配置
"nsis": {
"oneClick": false, // 一键安装
"allowToChangeInstallationDirectory": true, // 允许修改安装目录
// 下面这些配置不常用
"guid": "haha", // 注册表名字
"perMachine": true, // 是否开启安装时权限限制(此电脑或当前用户)
"allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
"installerIcon": "xxx.ico", // 安装图标
"uninstallerIcon": "xxx.ico", //卸载图标
"installerHeaderIcon": "xxx.ico", // 安装时头部图标
"createDesktopShortcut": true, // 创建桌面图标
"createStartMenuShortcut": true, // 创建开始菜单图标
"shortcutName": "lalala" // 图标名称
},
// 应用打包所包含文件
"files": [
"build/**/*",
"main.js",
"source/*",
"service/*",
"static/*",
"commands/*"
],
// 应用打包地址和输出地址
"directories": {
"app": "./",
"output": "dist"
},
// 发布配置 用于配合自动更新
"publish": [
{
// "generic" | "github"
"provider": "generic", // 静态资源服务器
"url": "http://你的服务器目录/latest.yml"
}
],
// 自定义协议 用于唤醒应用
"protocols": [
{
"name": "myProtocol",
"schemes": [
"myProtocol"
]
}
],
// windows打包配置
"win": {
"icon": "build/fav.ico",
// 运行权限
// "requireAdministrator" | "获取管理员权"
// "highestAvailable" | "最高可用权限"
"requestedExecutionLevel": "highestAvailable",
"target": [
{
"target": "nsis"
}
]
},
},

3. 编写入口文件 main.js



众所周知,基于react脚手架搭建的项目,入口文件为index.js,因此在上面配置完成后,我们想要启动electron应用,需要修改项目入口为main.js




  1. 首先在目录下新建main.js文件,并在package.json文件中,修改应用入口字段main的值为main.js

  2. 通过electron提供的BrowserWindow,创建一个窗口实例mainWindow

  3. 通过mainWindow实例方法loadURL, 加载静态资源

  4. 静态资源分两种加载方式:开发和生产;需要通过electron-is-dev判断当前环境;若是开发环境,可以开启调试入口,通过http://localhost:3000/加载本地资源(react项目启动默认地址);若是生产环境,则要关闭调试入口,并通过本地路径找到项目入口文件index.html



大体代码如下



const { BrowserWindow } = require("electron");
const url = require("url");
const isDev = require('electron-is-dev');
mainWindow = new BrowserWindow({
width: 1200, // 初始宽度
height: 800, // 初始高度
minWidth: 1200,
minHeight: 675,
autoHideMenuBar: true, // 隐藏应用自带菜单栏
titleBarStyle: false, // 隐藏应用自带标题栏
resizable: true, // 允许窗口拉伸
frame: false, // 隐藏边框
transparent: true, // 背景透明
backgroundColor: "none", // 无背景色
show: false, // 默认不显示
hasShadow: false, // 应用无阴影
modal: true, // 该窗口是否为禁用父窗口的子窗口
webPreferences: {
devTools: isDev, // 是否开启调试功能
nodeIntegration: true, // 默认集成node环境
},
});

const config = dev
? "http://localhost:3000/"
: url.format({
pathname: path.join(__dirname, "./build/index.html"),
protocol: "file:",
slashes: true,
});

mainWindow.loadURL(config);

4. 项目启动



项目前置操作完成,运行上面配置的命令来启动electron应用



   // 启动react应用,此时应用运行在"http://localhost:3000/"
yarn start
// 再启动electron应用,electron应用会在入口文件`main.js`中通过 mainWindow.loadURL(config) 来加载react应用
yarn estart


文件目录





至此,一个简单的electron应用已经启动,效果图如下(这是示例项目的截图)。



效果图



作为一个客户端应用,它的更新与我们的网页开发相比要显得稍微复杂一些,具体将会通过下面一个应用更新的例子来说明。



5. 应用更新



electron客户端的更新与网页不同,它需要先下载更新包到本地,然后通过覆盖源文件来达到更新效果。




首先第一步,安装依赖



yarn add electron-updater electron-builder
复制代码


应用通过electron-updater提供的api,去上文配置的服务器地址寻找并对比latest.yml文件,如果版本号有更新,则开始下载资源,并返回下载进度相关信息。下载完成后可以自动也可以手动提示用户,应用有更新,请重启以完成更新 (更新是可以做到无感的,下载完更新包之后,可以不提示,下次启动客户端时会自动更新)



// 主进程
const { autoUpdater } = require("electron-updater");
const updateUrl = "应用所在的远程服务器目录"
const message = {
error: "检查更新出错",
checking: "正在检查更新……",
updateAva: "检测到新版本,正在下载……",
updateNotAva: "现在使用的就是最新版本,不用更新",
};
autoUpdater.setFeedURL(updateUrl);
autoUpdater.on("error", (error) => {
sendUpdateMessage("error", message.error);
});
autoUpdater.on("checking-for-update", () => {
sendUpdateMessage("checking-for-update", message.checking);
});
autoUpdater.on("update-available", (info) => {
sendUpdateMessage("update-available", message.updateAva);
});
autoUpdater.on("update-not-available", (info) => {
sendUpdateMessage("update-not-available", message.updateNotAva);
});
// 更新下载进度事件
autoUpdater.on("download-progress", (progressObj) => {
mainWindow.webContents.send("downloadProgress", progressObj);
});
autoUpdater.on("update-downloaded", function (
event,
releaseNotes,
releaseName,
releaseDate,
updateUrl,
quitAndUpdate
) {
ipcMain.on("isUpdateNow", (e, arg) => {
// 接收渲染进程的确认消息 退出应用并更新
autoUpdater.quitAndInstall();
});
//询问是否立即更新
mainWindow.webContents.send("isUpdateNow");
});
ipcMain.on("checkForUpdate", () => {
//检查是否有更新
autoUpdater.checkForUpdates();
});

function sendUpdateMessage(type, text) {
// 将更新的消息事件通知到渲染进程
mainWindow.webContents.send("message", { text, type });
}

// 渲染进程
const { ipcRenderer } = window.require("electron");

// 发送检查更新的请求
ipcRenderer.send("checkForUpdate");

// 设置检查更新的监听频道

// 监听检查更新事件
ipcRenderer.on("message", (event, data) => {
console.log(data)
});

// 监听下载进度
ipcRenderer.on("downloadProgress", (event, data) => {
console.log("downloadProgress: ", data);
});

// 监听是否可以开始更新
ipcRenderer.on("isUpdateNow", (event, data) => {
// 用户点击确定更新后,回传给主进程
ipcRenderer.send("isUpdateNow");
});


应用更新的主要步骤




  1. 在主进程中,通过api获取远程服务器上是否有更新包

  2. 对比更新包的版本号来确定是否更新

  3. 对比结果如需更新,则开始下载更新包并返回当前下载进度

  4. 下载完成后,开发者可选择自动提示还是手动提示或者不提醒(应用在下次启动时会自动更新)



上文演示了在页面上(渲染进程),是如何与主进程进行通信,让主进程去检查更新。

在实际使用中,如果我们需要用到后台的能力或者原生功能时,主进程与渲染进程的交互必不可少。

那么他们有哪些交互方式呢?



在看下面的代码片段之前,可以先了解一下electron主进程与渲染进程
简单来说就是,通过main.js来执行的都属于主进程,其余皆为渲染进程。


6. 主进程与渲染进程间的常用交互方式


// 主进程中使用
const { ipcMain } = require("electron");

// 渲染进程中使用
const { ipcRenderer } = window.require("electron");

方式一



渲染进程 发送请求并监听回调频道



ipcRenderer.send(channel, someRequestParams);
ipcRenderer.on(`${channel}-reply`, (event, result)=>{
// 接收到主进程返回的result
})


主进程 监听请求并返回结果



ipcMain.on(channel, (event, someRequestParams) => {
// 根据someRequestParams,经过操作后得到result
event.reply(`${channel}-reply`, result)
})

方式二



渲染进程



const result = await ipcRenderer.invoke(channel, someRequestParams);


主进程:



ipcMain.handle(channel, (event, someRequestParams) => {
// 根据someRequestParams,经过操作后得到result
return result
});

方式三
以上两种方式均为渲染进程通知主进程, 第三种是主进程通知渲染进程



主进程



/*
* 使用`BrowserWindow`初始化的实例`mainWindow`
*/
mainWindow.webContents.send(channel, something)


渲染进程



ipcRenderer.on(channel, (event, something) => {
// do something
})

上文的应用更新用的就是方式一


还有其它通讯方式postMessage, sendTo等,可以根据具体场景决定使用何种方式。


7. 应用唤醒(与其他应用联动)


electron应用除了双击图标运行之外,还可以通过协议链接启动(浏览器地址栏或者命令行)。这使得我们可以在网页或者其他应用中,以链接的形式唤醒该应用。链接可以携带参数 例:zhuxingmin://?a=1&b=2&c=3 ‘自定义协议名:zhuxingmin’ ‘参数:a=1&b=2&c=3’。


我们可以通过参数,来使应用跳转到某一页或者让应用做一些功能性动作等等。


const path = require('path');
const { app } = require('electron');

// 获取单实例锁
const gotTheLock = app.requestSingleInstanceLock();

// 如果获取失败,证明已有实例在运行,直接退出
if (!gotTheLock) {
app.quit();
}

const args = [];
// 如果是开发环境,需要脚本的绝对路径加入参数中
if (!app.isPackaged) {
args.push(path.resolve(process.argv[1]));
}
// 加一个 `--` 以确保后面的参数不被 Electron 处理
args.push('--');
const PROTOCOL = 'zhuxingmin';
// 设置自定义协议
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, args);

// 如果打开协议时,没有其他实例,则当前实例当做主实例,处理参数
handleArgv(process.argv);

// 其他实例启动时,主实例会通过 second-instance 事件接收其他实例的启动参数 `argv`
app.on('second-instance', (event, argv) => {
if (process.platform === 'win32') {
// Windows 下通过协议URL启动时,URL会作为参数,所以需要在这个事件里处理
handleArgv(argv);
}
});

// macOS 下通过协议URL启动时,主实例会通过 open-url 事件接收这个 URL
app.on('open-url', (event, urlStr) => {
handleUrl(urlStr);
});

function handleArgv(argv) {
const prefix = `${PROTOCOL}:`;
const offset = app.isPackaged ? 1 : 2;
const url = argv.find((arg, i) => i >= offset && arg.startsWith(prefix));
if (url) handleUrl(url);
}

function handleUrl(urlStr) {
// myapp://?a=1&b=2
let paramArr = urlStr.split("?")[1].split("&");
const params = {};
paramArr.forEach((item) => {
if (item) {
const [key, value] = item.split("=");
params[key] = value;
}
});
/**
{
a: 1,
b: 2
}
*/

}

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

收起阅读 »

H5 性能极致优化

项目背景 H5 项目是企鹅辅导的核心项目,已迭代四年多,包括了课程详情页/老师详情页/报名页/支付页面等页面,构建产物用于企鹅辅导 APP/H5(微信/QQ/浏览器),迭代过程中了也累积了一些性能问题导致页面加载、渲染速度变慢, 为了提升用户体验,近期启动了“...
继续阅读 »

项目背景


H5 项目是企鹅辅导的核心项目,已迭代四年多,包括了课程详情页/老师详情页/报名页/支付页面等页面,构建产物用于企鹅辅导 APP/H5(微信/QQ/浏览器),迭代过程中了也累积了一些性能问题导致页面加载、渲染速度变慢, 为了提升用户体验,近期启动了“H5 性能优化”项目,针对页面加载速度,渲染速度做了专项优化,下面是对本次优化的总结,包括以下几部分内容。



  1. 性能优化效果展示

  2. 性能指标及数据采集

  3. 性能分析方法及环境准备

  4. 性能优化具体实践


一、性能指标及数据采集


企鹅辅导 H5 采用的性能指标包括:


1.页面加载时间:页面以多快的速度加载和渲染元素到页面上。



  • First contentful paint (FCP): 测量页面开始加载到某一块内容显示在页面上的时间。

  • Largest contentful paint (LCP): 测量页面开始加载到最大文本块内容或图片显示在页面中的时间。

  • DomContentLoaded Event:DOM解析完成时间

  • OnLoad Event:页面资源加载完成时间


2.加载后响应时间:页面加载和执行js代码后多久能响应用户交互。



  • First input delay (FID): 测量用户首次与网站进行交互(例如点击一个链接、按钮、js自定义控件)到浏览器真正进行响应的时间。


3.视觉稳定性:页面元素是否会以用户不期望的方式移动,并干扰用户的交互。



  • Cumulative layout shift (CLS): 测量从页面开始加载到状态变为隐藏过程中,发生不可预期的layout shifts的累积分数。


项目使用了 IMLOG 进行数据上报,ELK 体系进行现网数据监控,Grafana 配置视图,观察现网情况。


根据指标的数据分布,能及时发现页面数据异常采取措施。


二、性能分析及环境准备


现网页面情况:



可以看到进度条在页面已经展示后还在持续 loading,加载时间长达十几秒,比较影响了用户体验。


根据 Google 开发文档 对浏览器架构的解释:



当导航提交完成后,渲染进程开始着手加载资源以及渲染页面。一旦渲染进程“完成”(finished)渲染,它会通过IPC告知浏览器进程(注意这发生在页面上所有帧(frames)的 onload 事件都已经被触发了而且对应的处理函数已经执行完成了的时候),然后UI线程就会停止导航栏上旋转的圈圈



我们可以知道,进度条的加载时长和 onload 时间密切相关,要想进度条尽快结束就要 减少 onload时长。


根据现状,使用ChromeDevTool作为基础的性能分析工具,观察页面性能情况


Network:观察网络资源加载耗时及顺序


Performace:观察页面渲染表现及JS执行情况


Lighthouse:对网站进行整体评分,找出可优化项


下面以企鹅辅导课程详情页为案例进行分析,找出潜在的优化项


(注意使用Chrome 隐身窗口并禁用插件,移除其他加载项对页面的影响)


1. Network 分析


通常进行网络分析需要禁用缓存、启用网络限速(4g/3g) 模拟移动端弱网情况下的加载情况,因为wifi网络可能会抹平性能差距。



可以看到DOMContentLoaded的时间在 6.03s ,但onload的时间却在 20.92s


先观察 DOMContentLoaded 阶段,发现最长请求路径在 vendor.js ,JS大小为170kB,花费时间为 4.32s


继续观察 DOMContentLoaded 到 onload 的这段时间



可以发现onload事件被大量媒体资源阻塞了,关于 onload 事件的影响因素,可以参考这篇文章


结论是 浏览器认为资源完全加载完成(HTML解析的资源 和 动态加载的资源)才会触发 onload


结合上图 可以发现加载了图片、视频、iFrame等资源,阻塞了 onload 事件的触发


Network 总结



  1. DOM的解析受JS加载和执行的影响,尽量对JS进行压缩、拆分处理(HTTP2下),能减少 DOMContentLoaded 时间

  2. 图片、视频、iFrame等资源,会阻塞 onload 事件的触发,需要优化资源的加载时机,尽快触发onload


2. Performance 分析


使用Performance模拟移动端注意手机处理器能力比PC差,所以一般将 CPU 设置为 4x slowdown 或 6x slowdown 进行模拟



观察几个核心的数据



  1. Web Vitals ( FP / FCP / LCP / Layout Shift ) 核心页面指标 和 Timings 时长


可以看到 LCP、DCL和 Onload Event 时间较长,且出现了多次 Layout Shift。


要 LCP 尽量早触发,需要减少页面大块元素的渲染时间,观察 Frames 或ScreenShots 的截图,关注页面的元素渲染情况。


可以通过在 Experience 行点击Layout Shift ,在 Summary 面板找到具体的偏移内容。




  1. Main Long Tasks 长任务数量和时长


可以看到页面有大量的Long Tasks需要进行优化,其中couse.js(页面代码)的解析执行时间长达800ms。


处理Long Tasks,可以在开发环境进行录制,这样在 Main Timeline 能看到具体的代码执行文件和消耗时长。


Performance 总结



  1. 页面LCP触发时间较晚,且出现多次布局偏移,影响用户体验,需要尽早渲染内容和减少布局偏移

  2. 页面 Long Tasks 较多,需要对 JS进行合理拆分和加载,减少 Long Tasks 数量,特别是 影响 DCL 和 Onload Event 的 Task


3. Lighthouse 分析


使用ChromeDevTool 内置 lighthouse 对页面进行跑分



分数较低,可以看到 Metrics 给出了核心的数据指标,这边显示的是 TTI SI TBT 不合格,LCP 需要提升,FCP 和 CLS 达到了良好的标准,可以查看分数计算标准


同时 lighthouse 会提供一些 优化建议,在 Oppotunities 和 Diagnostics 项,能看到具体的操作指南,如 图片大小、移除无用JS等,可以根据指南进行项目的优化。


lighthouse 的评分内容是根据项目整体加载项目进行打分的,审查出的问题同样包含Network、Performance的内容,所以也可以看作是对 Network、Performance问题的优化建议。


Lighthouse 总结



  1. 根据评分,可以看出 TTI、SI、TBT、LCP这四项指标需要提高,可以参考lighthouse 文档进行优化。

  2. Oppotunities 和 Diagnostics 提供了具体的优化建议,可以参考进行改善。


4. 环境准备


刚才是对线上网页就行初步的问题分析,要实际进行优化和观察,需要进行环境的模拟,让优化效果能更真实在测试环境中体现。


代理使用:whistle、charles、fiddler等


本地环境、测试环境模拟:nginx、nohost、stke等


数据上报:IMLOG、TAM、RUM等


前端代码打包分析:webpack-bundle-analyzer 、rollup-plugin-visualizer等


分析问题时使用本地代码,本地模拟线上环境验证优化效果,最后再部署到测试环境验证,提高开发效率。


三、性能优化具体实践


PART1: 加载时间优化


Network 中对页面中加载的资源进行分类


第一部分是影响 DOM解析的JS资源,可以看到这里分类为 关键JS和非关键JS,是根据是否参与首面渲染划分的


这里的非关键JS我们可以考虑延迟异步加载,关键JS进行拆分优化处理


1. 关键JS打包优化



JS 文件数量8个,总体积 460.8kB,最大文件 170KB


1.1 Splitchunks 的正确配置

vendor.js 170kB(gzipd) 是所有页面都会加载的公共文件,打包规则是 miniChunks: 3,引用超过3次的模块将被打进这个js




分析vendor.js的具体构成(上图)


以string-strip-html.umd.js 为例 大小为34.7KB,占了 vendor.js的 20%体积,但只有一个页面多次使用到了这个包,触发了miniChunks的规则,被打进了vendor.js。


同理对vendor.js的其他模块进行分析,iosSelect.js、howler.js、weixin-js-sdk等模块都只有3、4个页面/组件依赖,但也同样打进了 vendor.js。


由上面的分析,我们可以得出结论:不能简单的依靠miniChunks规则对页面依赖模块进行抽离打包,要根据具体情况拆分公共依赖。


修改后的vendor根据业务具体的需求,提取不同页面和组件都有的共同依赖(imutils/imlog/qqapi)


vendor: {
test({ resource }) {
return /[\\/]node_modules[\\/](@tencent\/imutils|imlog\/)|qqapi/.test(resource);
},
name: 'vendor',
priority: 50,
minChunks: 1,
reuseExistingChunk: true,
},

而其他未指定的公共依赖,新增一个common.js,将阈值调高到20或更高(当前页面数76),让公共依赖成为大多数页面的依赖,提高依赖缓存利用率,调整完后,vendor.js 的大小减少到 30KB,common.js 大小为42KB


两个文件加起来大小为 72KB,相对于优化前体积减少了 60%(100KB)


1.2 公共组件的按需加载


course.js 101kB (gzipd) 这个文件是页面业务代码的文件



观察上图,基本都是业务代码,除了一个巨大的** component Icon,占了 25k**,页面文件1/4的体积,但在代码中使用到的 Icon 总共才8个


分析代码,可以看到这里使用require加载svg,Webpack将require文件夹内的内容一并打包,导致页面 Icon 组件冗余



如何解决这类问题实现按需加载?


按需加载的内容应该为独立的组件,我们将之前的单一入口的 ICON 组件(动态dangerouslySetInnerHTML)改成单文件组件模式直接引入使用图标。



但实际开发中这样会有些麻烦,一般需要统一的 import 路径,指定需要的图标再加载,参考 babel-plugin-import,我们可以配置 babel 的依赖加载路径调整 Icon 的引入方式,这样就实现了图标的按需加载。



按需加载后,重新编译,查看打包带来的收益,页面的 Icons 组件 stat size 由 74KB 降到了 20KB,体积减少了 70%


1.3 业务组件的代码拆分 (Code Splitting)


观察页面,可以看到”课程大纲“、”课程详情“、”购课须知“这三个模块并不在页面的首屏渲染内容里,



我们可以考虑对页面这几部分组件进行拆分再延迟加载,减少业务代码JS大小和执行时长


拆分的方式很多,可以使用react-loadable、@loadable/component 等库实现,也可以使用React 官方提供的React.lazy


拆分后的代码



代码拆分会导致组件会有渲染的延迟,所以在项目中使用应该综合用户体验和性能再做决定,通过拆分也能使部分资源延后加载优化加载时间。


1.4 Tree Shaking 优化


项目中使用了 TreeShaking的优化,用时候要注意 sideEffects 的使用场景,以免打包产物和开发不一致。


经过上述优化步骤,整体打包内容:



JS 文件数量6个,总体积 308KB,最大文件体积 109KB


关键 JS 优化数据对比:



























文件总体积最大文件体积
优化前460.8 kb170 kb
优化后308 kb109 kb
优化效果总体积减少 50%最大文件体积减少 56%

2.非关键 JS 延迟加载


页面中包含了一些上报相关的 JS 如 sentry,beacon(灯塔 SDK)等,对于这类资源,如果在弱网情况,可能会成为影响 DOM 解析的因素


为了减少这类非关键JS的影响,可以在页面完成加载后再加载非关键JS,如sentry官方也提供了延迟加载的方案


在项目中还发现了一部分非关键JS,如验证码组件,为了在下一个页面中能利用缓存尽快加载,所以在上一个页面提前加载一次生成缓存



如果不访问下一个页面,可以认为这是一次无效加载,这类的提前缓存方案反而会影响到页面性能。


针对这里资源,我们可以使用 Resource Hints,针对资源做 Prefetch 处理


检测浏览器是否支持 prefech,支持的情况下我们可以创建 Prefetch 链接,不支持就使用旧逻辑直接加载,这样能更大程度保证页面性能,为下一个页面提供提前加载的支持。


const isPrefetchSupported = () => {
const link = document.createElement('link');
const { relList } = link;

if (!relList || !relList.supports) {
return false;
}
return relList.supports('prefetch');
};
const prefetch = () => {
const isPrefetchSupport = isPrefetchSupported();
if (isPrefetchSupport) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = type;
link.href = url;
document.head.appendChild(link);
} else if (type === 'script') {
// load script
}
};

优化效果:非关键JS不影响页面加载




3.媒体资源加载优化


3.1 加载时序优化


可以观察到onload被大量的图片资源和视频资源阻塞了,但是页面上并没有展示对应的图片或视频,这部分内容应该进行懒加载处理。



处理方式主要是要控制好图片懒加载的逻辑(如 onload 后再加载),可以借助各类 lazyload 的库去实现。 H5项目用的是位置检测(getBoundingClientRect )图片到达页面可视区域再展示。


但要注意懒加载不能阻塞业务的正常展示,应该做好超时处理、重试等兜底措施


3.2 大小尺寸优化


课程详情页 每张详情图的宽为 1715px,以6s为基准(375px)已经是 4x图了,大图片在弱网情况下会影响页面加载和渲染速度



使用CDN 图床尺寸大小压缩功能,根据不同的设备渲染不同大小的图片调整图片格式,根据网络情况,渲染不同清晰度的图



可以看到在弱网(移动3G网络)的情况下,同一张图片不同尺寸加载速度最高和最低相差接近6倍,给用户的体验截然不同


CDN配合业务具体实现:使用 img 标签 srcset/sizes 属性和 picutre 标签实现响应式图片,具体可参考文档


使用URL动态拼接方式构造url请求,根据机型宽度和网络情况,判断当前图片宽度倍数进行调整(如iphone 1x,ipad 2x,弱网0.5x)


优化效果:移动端 正常网络情况下图片体积减小 220%、弱网情况下图片体积减小 13倍


注意实际业务中需要视觉同学参与,评估图片的清晰度是否符合视觉标准,避免反向优化!


3.3 其他类型资源优化


iframe


加载 iframe 有可能会对页面的加载产生严重的影响,在 onload 之前加载会阻塞 onload 事件触发,从而阻塞 loading,但是还存在另一个问题


如下图所示,页面在已经 onload 的情况下触发 iframe 的加载,进度条仍然在不停的转动,直到 iframe 的内容加载完成。



可以将iframe的时机放在 onload 之后,并使用setTimeout触发异步加载iframe,可避免iframe带来的loading影响


数据上报


项目中使用 image 的数据上报请求,在正常网络情况下可能感受不到对页面性能的影响


但在一些特殊情况,如其中一个图片请求的耗时特别长就会阻塞页面 onload 事件的触发,延长 loading 时间



解决上报对性能的影响问题有以下方案



  1. 延迟合并上报

  2. 使用 Beacon API

  3. 使用 post 上报


H5项目采用了延迟合并上报的方案,业务可根据实际需要进行选择


优化效果:全部数据上报在onload后处理,避免对性能产生影响。



字体优化


项目中可能会包含很多视觉指定渲染的字体,当字体文件比较大的时候,也会影响到页面的加载和渲染,可以使用 fontmin 将字体资源进行压缩,生成精简版的字体文件、


优化前:20kB => 优化后:14kB



PART2: 页面渲染优化


1.直出页面 TTFB 时间优化


目前我们在STKE部署了直出服务,通过监控发现直出平均耗时在 300+ms


TTFB时间在 100 ~ 200 之间波动,影响了直出页面的渲染



通过日志打点、查看 Nginx Accesslog 日志、网关监控耗时,得出以下数据(如图)



  • STKE直出程序耗时是 20ms左右

  • 直出网关NGW -> STKE 耗时 60ms 左右

  • 反向代理网关NGINX -> NGW 耗时 60ms 左右


登陆 NGW 所在机器,ping STKE机器,有以下数据


平均时延在 32ms,tcp 三次握手+返回数据(最后一次 ack 时发送数据)= 2个 rtt,约 64ms,和日志记录的数据一致


查看 NGW 机器所在区域为天津,STKE 机器所在区域为南京,可以初步判断是由机房物理距离导致的网络时延,如下图所示



切换NGW到南京机器 ping STKE南京的机器,有以下数据:


同区域机器 ping 的网络时延只有 0.x毫秒,如下图所示:


综合上述分析,直出页面TTFB时间过长的根本原因是:NGW 网关部署和 Nginx、STKE 不在同一区域,导致网络时延的产生


解决方案是让网关和直出服务机房部署在同一区域,执行了以下操作:



  • NGW扩容

  • 北极星开启就近访问


优化前


优化后


优化效果如上图:



















七天网关平均耗时
优化前153 ms
优化后31 ms 优化 80%(120 ms)

2.页面渲染时间优化


模拟弱网情况(slow 3g)Performance 录制页面渲染情况,从下图Screenshot中可以发现



  1. DOM 开始解析,但页面还未渲染

  2. CSS 文件下载完成后页面才正常渲染


CSS不会阻塞页面解析,但会阻塞页面渲染,如果CSS文件较大或弱网情况,会影响到页面渲染时间,影响用户体验。


借助 ChromeDevTool 的 Coverage 工具(More Tools里面),录制页面渲染时CSS的使用率



发现首屏的CSS使用率才15%,可以考虑对页面首屏的关键CSS进行内联让页面渲染不被CSS阻塞,再把完整CSS加载进来


实现Critial CSS 的优化可以考虑使用 critters


优化后效果:


CSS 资源正在下载时,页面已经能正常渲染显示了,对比优化前,渲染时间上 提升了 1~2 个 css 文件加载的时间。



3. 页面布局抖动优化


观察页面的元素变化



优化前(左图):图标缺失、背景图缺失、字体大小改变导致页面抖动、出现非预期页面元素导致页面抖动


优化后:内容相对固定, 页面元素出现无突兀感



主要优化内容:



  1. 确定直出页面元素出现位置,根据直出数据做好布局

  2. 页面小图可以通过base64处理,页面解析的时候就会立即展示

  3. 减少动态内容对页面布局的影响,使用脱离文档流的方式或定好宽高


四、性能优化效果展示


优化效果由以下指标量化


首次内容绘制时间FCP(First Contentful Paint):标记浏览器渲染来自 DOM 第一位内容的时间点


视窗最大内容渲染时间LCP(Largest Contentful Paint):代表页面可视区域接近完整渲染


加载进度条时间:浏览器 onload 事件触发时间,触发后导航栏进度条显示完成


Chrome 模拟器 4G 无缓存对比(左优化前、右优化后)























首屏最大内容绘制时间进度条加载(onload)时间
优化前1067 ms6.18s
优化后31 ms 优化 80%(120 ms)1.19s 优化 81%

Lighthouse 跑分对比


优化前


优化后



srobot 性能检测一周数据



srobot 是团队内的性能检测工具,使用TRobot指令一键创建页面健康检测,定时自动化检测页面性能及异常



优化前


优化后


五、优化总结和未来规划



  1. 以上优化手段主要是围绕首次加载页面的耗时和渲染优化,但二次加载还有很大的优化空间 如 PWA 的使用、非直出页面骨架屏处理、CSR 转 SSR等

  2. 对比竞品发现我们 CDN 的下载耗时较长,近期准备启动 CDN 上云,期待上云后 CDN 的效果提升。

  3. 项目迭代一直在进行,需要思考在工程上如何持续保障页面性能

  4. 上文是围绕课程详情页进行的分析和优化处理,虽然对项目整体做了优化处理,但性能优化没有银弹,不同页面的优化要根据页面具体需求进行,需要开发同学主动关注。

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

收起阅读 »

code review 流程探索

前言 没有无缘无故的爱,也没有无缘无故的恨,当然也没有无缘无故的 code review 为什么要 CR 给大家讲个故事,“大神 A”上班时突然恼羞成怒的骂道,这是谁写的代码,没有注释啥也没有,这么明显的 bug。当时整个小组都不敢说话,慌的要死,生怕说的就是...
继续阅读 »

前言


没有无缘无故的爱,也没有无缘无故的恨,当然也没有无缘无故的 code review


为什么要 CR


给大家讲个故事,“大神 A”上班时突然恼羞成怒的骂道,这是谁写的代码,没有注释啥也没有,这么明显的 bug。当时整个小组都不敢说话,慌的要死,生怕说的就是自己。领导发话:“大神 A”查下提交记录,谁提交的谁请吃饭。过了两分钟,“大神 A”:这,这是我自己一年前提交的。所以不想自己尴尬,赶紧 code review 吧


一、角色职能



author 即需求开发者。要求:



  1. 注重注释。对复杂业务写明相应注释,commit 写名具体提交背景,便于 reviewer 理解。

  2. 端正心态接受他人 review。对 reviewer 给出的 comment,不要有抵触的情绪,对你觉得不合理的建议,可以委婉地进行拒绝,或者详细说明自己的看法以及原因。reviewer 持有的观点并不一定是合理的,所以 review 也是一个相互学习的过程。

  3. 完成 comment 修改后及时反馈。commit 提交信息备注如"reivew: xxxx",保证复检效率。


reviewer 作为 cr 参与者,建议由项目责任人和项目参与者组成。要求:



  1. 说明 comment 等级。reviewer 对相应代码段提出评价时,需要指明对应等级,如

    • fix: xxxxxxx 此处需强制修改,提供修改建议

    • advise: xxxxxxx 此处主观上建议修改,不强制,可提供修改建议

    • question: xxxxxx 此处存在疑虑,需要 author 作出解释



  2. 友好 comment。评价注意措辞,可以说“我们可以如何去调整修改,可能会更合适。。。”,对于比较好的代码,也应该给与足够的赞美。

  3. 享受 review。避免以挑毛病的心态 review,好的 reviewer 并不是以提的问题多来衡量的。跳出自己的编码风格,主动理解 author 的思路,也是一个很好的学习过程。


二、CR 流程


1、self-review



  • commit 之前要求 diff 一下,查看文件变更情况,可接着 gitk 完成。当然如果项目使用 pre-commit 关联 lint 校验,也能发现例如 debugger、console.log 之类语句。但是仍然提倡大家每次提交之前检查一下提交文件。

  • 多人协作下的 commit。多人合作下的分支在合并请求时,需要关注是否带入没必要的 commit。

  • commit message。建议接入 husky、commitlint/cli 以及 commitlint/config-conventional 校验 commit message。commitlint/config-conventional 所提供的类型如

    • feat: 新特性

    • fix: 修改 bug

    • chore: 优化,如项目结构,依赖安装更新等

    • docs: 文档变更

    • style: 样式相关修改

    • refactor:项目重构




此目的为了进一步增加 commit message 信息量,帮助 reviewer 以及自己更有效的了解 commit 内容。


2、CR



  1. 提测时发起 cr,需求任务关联 reviewer。提供合并请求,借助 gitlab/sourcetree/vscode gitlens 等工具。reviewer 结束后给与反馈

  2. 针对 reviewer 提出的建议修改之后,commit message 注明类似'review fix'相关信息,便于 reviewer 复检。

  3. 紧急需求,特事特办,跳过 cr 环节,事后 review。


三、CR 标准



  1. 不纠结编码风格。编码风格交给 eslint/tslint/stylelint

  2. 代码性能。大数据处理、重复渲染等

  3. 代码注释。字段注释、文档注释等

  4. 代码可读性。过多嵌套、低效冗余代码、功能独立、可读性变量方法命名等

  5. 代码可扩展性。功能方法设计是否合理、模块拆分等

  6. 控制 review 时间成本。reviewer 尽量由项目责任人组成,关注代码逻辑,无需逐字逐句理解。


四、最后


总的来说,cr 并不是一个找 bug 挑毛病的过程,更不会降低整体开发效率。其目的是为了保证项目的规范性,使得其他开发人员在项目扩展和维护时节省更多的时间和精力。当然 cr 环节需要团队每一个成员去推动,只有每一个人都认可且参与进来,才能发挥 cr 的最大价值。


f5e284a8e87e4340b5f20e9c88fb2777_tplv-k3u1fbpfcp-zoom-1.gif


最后安利一波本人开发vscode小插件搭配gitlab进行review。因为涉及内部代码,暂时不能对外开放,这里暂时提供思路,后续开放具体代码。



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

收起阅读 »

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






收起阅读 »

【开源项目】音视频LowCode平台——AgoraFlow

此开源项目由热心网友@Lu-Derek 开发AgoraFlow是一款音视频 Low Code Web 共享编辑器。将音视频相关功能进行模块化集成,提供一个图形化界面,让开发者可以用做 PPT 的形式来完成想要实现的功能。Q:AgoraFlow是一个怎么样的项目...
继续阅读 »

此开源项目由热心网友@Lu-Derek 开发

AgoraFlow是一款音视频 Low Code Web 共享编辑器。将音视频相关功能进行模块化集成,提供一个图形化界面,让开发者可以用做 PPT 的形式来完成想要实现的功能。

Q:AgoraFlow是一个怎么样的项目?

A:AgoraFlow是一个基于Agora+环信MQTT服务,在Low Code方向上的一次尝试。除了Agora Vue Web SDK和环信之外,还用到了Node-RED,一个IBM的物联网框架。

这个项目允许用户通过拖拽的方式安排音视频流在不同设备上的采集-发布-订阅-播放行为。在积木式的搭建工作流后,程序会根据逻辑在指定设备部署应用。


Q:为什么要做这样一个项目?

A:作为程序员,总会耳濡目染一些行业热点。最近Low Code比较火,所以想看看Low Code和音视频结合会产生什么样的火花。其实大家对于Low Code的产品形态还没有一个准确的定位,所以我想利用这次编程赛阐述我的理解。


Q:对于这个项目的技术选型有什么想说的?

我觉得大家都应该利用自己的【工作技能】,接触一些【工作内容】以外的东西。这是我在这个项目中使用了平时并不常用的技术栈、选型比较奇怪的原因。我使用了声网尚在Beta中的产品【Agora
Vue
SDK】、使用了偏物联网行业的技术栈【Node-RED】和【MQTT】,按照我的理解做了这样一个不完整但是有趣的产品。对我来说,这是一次尝试,一次娱乐,而非工作。



【项目介绍】

AgoraFlow

基于声网+环信MQTT的音视频LowCode平台

https://agoraflow.wrtc.dev/


安装/设置:
npm install

访问 http://localhost:1880  即可编辑音视频上下行

运行:

目前支持四台设备:
device1:https://agoraflow.wrtc.dev/devices/index.html?deviceName=device1
device2:https://agoraflow.wrtc.dev/devices/index.html?deviceName=device2
device3:https://agoraflow.wrtc.dev/devices/index.html?deviceName=device3
device4:https://agoraflow.wrtc.dev/devices/index.html?deviceName=device4

使用四代设备分别打开网页,网页的上下行受low code平台控制


Github地址:

https://hub.fastgit.org/AgoraIO-Community/RTE-2021-Innovation-Challenge/tree/master/Application-Challenge/%E3%80%90%E7%BA%A2%E9%B2%A4%E9%B1%BC%E4%B8%8E%E7%BB%BF%E9%B2%A4%E9%B1%BC%E4%B8%8E%E9%A9%B4%E3%80%91AgoraFlow

作者联系邮箱:scaret.in@gmail.com

收起阅读 »

iOS之网络优化

一、正常一个网络请求过程正常一条网络请求需要经过:DNS解析,请求DNS服务器,获取对应的IP地址与服务端建立连接,TCP三次握手,安全协议的同步流程连接建立完成,发送和接受数据,解码数据。优化点:直接使用IP地址,除去DNS解析的流程不要每个请求都重复建立连...
继续阅读 »

一、正常一个网络请求过程

正常一条网络请求需要经过:

  • DNS解析,请求DNS服务器,获取对应的IP地址
  • 与服务端建立连接,TCP三次握手,安全协议的同步流程
  • 连接建立完成,发送和接受数据,解码数据。

优化点:

  • 直接使用IP地址,除去DNS解析的流程
  • 不要每个请求都重复建立连接,复用连接使用同一条连接(长连接)
  • 压缩数据,减少传输数据的大小

二、正常的DNS流程

DNS完整的解析流程:

  1. 先动本地系统缓存获取,偌没有就到最近的DNS服务器获取。

  2. 偌依旧没有,就到主域名服务器获取,每一层都有有缓存。

为了域名解析的实时性,每一层的缓存都有一个过期时间。

缺点:

  1. 缓存时间过长,域名更新不及时,设置的端,单量的DNS解析请求影响速度

  2. 域名劫持,容易被中间人攻击或运营商劫持,把域名解析到第三⽅IP地址,劫持率⽐较⾼。

  3. DNS解析过程不受控制,⽆法保证解析到最快的IP

  4. ⼀次请求只能解析到⼀个域名

处理DNS耗时和防劫持的方式

HTTPDNS原理就是:

⾃⼰做域名解析⼯作,通过HTTP请求后台去拿到域名对应的IP地址,可以解决上述问题。

  • 域名解析与请求分离,所有的请求都直接⽤IP,⽆需DNS解析,APP定时请求HTTPDNS服务器更 新IP地址即可

  • 通过签名等⽅式,保证HTTPDNS请求的安全性,避免被劫持

  • DNS解析有⾃⼰控制,可以确保更具⽤⼾所在地返回就近的IP地址,或者根据客⼾端测速结果使⽤ 速度最快的IP

  • ⼀次请求可以解析多个域名

三、TCP连接耗时优化

解决思路就是:复用接连

  • 不⽤每次重新建⽴连接,⽅案是⾼效的复⽤连接

HTTP1.1版本产生的问题:

  • HTTP1.1 的问题是默认开启的keep-alive,⼀次的连接只能发送接收⼀个请求,同时发起多个请求,会产⽣问题。

  • 若串⾏发送请求,可以⼀直复⽤⼀个连接,但是速度很慢,每个请求都需要等待上⼀个请求完成再发 送,也产⽣了连接的浪费,没⽤充分的利⽤带宽

  • 若并⾏发送请求,那么⾸次请求都要TCP三次握⼿建⽴新的连接,即使第⼆次请求可以复⽤连接池的 连接,但是会导致连接池的连接过多,对服务端资源产⽣浪费,若限制保持的连接数,会有超出的连 接仍要每次建⽴连接。

HTTP2.0 提出了多路复⽤的⽅式解决HTTP1.1的问题:

  • HTTP2 的多路复⽤机制也是复⽤连接,但是它的复⽤的这条连接⽀持同时处理多条请求,所有的请求 都可以并发的在这条连接上进⾏,解决了并发请求需要建⽴多次连接的问题

  • HTTP2 把连接⾥传输的数据都封装成⼀个个stream,每个stream都有⼀个标识,stream的发送和接 收可以是乱序,不依赖顺序,不会有阻塞的问题,接收端可以根据stream的标识区分属于哪个请求, 在进⾏数据拼接,最终得到数据

HTTP2.0 TCP队头阻塞

  • HTTP2还是有问题存在,就是队头阻塞,这是受限于TCP协议,TCP协议为保证数据的可靠性,若传 输过程中有⼀个TCP的包丢失,会等待这个包重传之后,才会处理后续的包.

  • HTTP2的多路复⽤让所 有的请求都在同⼀条连接上,中间有⼀个包丢失,就会阻塞等待重传,所有的请求也会被阻塞

HTTP2.0 TCP队头阻塞解决方案

  • 这个问题需要改变TCP协议,但是TCP协议依赖操作系统实现以及部分硬件的定制,所以改进缓慢。
  • 于是Google提出了QUIC协议,相当于在UDP的基础上在定义⼀套可靠的传输协议,解决TCP的缺陷, 包括队头阻塞,但是客⼾端少有介⼊

四、传输数据优化

传输数据有⼤⼩,数据也会对请求速度有影响。主要优化两个方面:

  • 压缩率,⽽是解压序列化反
    序列化的速度。使⽤Protobuf 可以⽐json的数据量⼩⾄少⼀个数量级

  • 压缩算法的选择,⽬前⽐较好的是Z-Standard HTTP的请求头数据的在HTTP2中也进⾏了压缩。

五、弱⽹优化

根据不同的⽹络设置不同的超时时间

六、数据安全优化

使⽤Https,是基于http协议上的TLS安全协议,安全协议解决了保证安全降低加密成本

1、安全上

  • 使⽤加密算法组合对传输的数据加密,避免被窃听和篡改

  • 认证对⽅⾝份,避免被第三⽅冒充

  • 加密算法保持灵活可更新,防⽌定死算法被破解后⽆法更换,禁⽌已被破解的算法

2、降低加密成本

  • ⽤对称加密算法加密传输的数据,解决⾮对称加密算法的性能低和⻓度限制的问题

  • 缓存安全协议握⼿后的秘钥等数据,加快第⼆次建⽴连接的速度

  • 3、加快握⼿过程2RTT -> 0RTT。加快握⼿的思路,原本客⼾端和服务端需要协商使⽤什么算法后才 可以加密发送数据,变成通过内置的公钥和默认算法,在握⼿的同时,就把数据发送出去,不需要等 待握⼿就开始发送数据,达到0RTT

3.充分利用缓存

  • Get请求可以被缓存,Get请求也是幂等的,
  • 简单的处理缓存的80%需求

使⽤Get请求的代码设置如下:


///objective-c代码 
NSURLCache *urlCache =[[NSURLCache alloc]initWithMemoryCapacity:4 *1024 * 1024
diskCapacity:20 * 1024 *1024 diskPath:nil];
[NSURLCachesetSharedURLCache:urlCache];

4、控制缓存的有效性

1、⽂件缓存:借助ETag或者Last-Modified判断⽂件缓存是不是有效

Last-Modified

  • ⼤多采⽤资源变动后就重新⽣成⼀个链接的做法,但是不排除没有换链接的,这种情况下就需要借助 ETagorLast-Modified判断⽂件的有效性

  • Last-Modified 资源的最后修改时间戳,与缓存时间进⾏对⽐来判断是否过期,请求时返回If- Modified-Since,返回的数据若果没变化http返回的状态码就是403(Not changed),内容为空,节省 传输数据

ETagIf-None-Match

  • HTTP 协议规格说明定义ETag为“被请求变量的实体值” 。另⼀种说法是,ETag是⼀个可以与Web 资源关联的记号(token)。
  • 它是⼀个 hash 值,⽤作 Request 缓存请求头,每⼀个资源⽂件都对应⼀ 个唯⼀的 ETag 值。如果Etag没有改变,则返回状态码304,返回的内容为空

下载的图⽚的格式最好是WebP格式,因为他是同等图⽚质量下最⼩数量的图⽚,可以降低流量损失



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

收起阅读 »

iOS 渲染过程

iOS
背景 app如何快速显示首屏? 滑动列表时候如何做到流畅? 当我们说界面卡了我们在说什么? ...... 应用运行的卡顿率是一个十分重要的指标,相比慢、发热、占用内存高来讲,卡顿是用户第一时间能感知的东西,三步两卡的应用基本逃不出被卸载的命运,要想优化卡顿...
继续阅读 »

背景



app如何快速显示首屏?

滑动列表时候如何做到流畅?

当我们说界面卡了我们在说什么?

......



应用运行的卡顿率是一个十分重要的指标,相比慢、发热、占用内存高来讲,卡顿是用户第一时间能感知的东西,三步两卡的应用基本逃不出被卸载的命运,要想优化卡顿就要搞清楚画面卡住不动的原因,这就需要对整个渲染过程有一定了解,本文会从图层说起,来聊聊整个渲染过程以及优化点,在写这篇文章之前笔者努力在想,对于完全没有做过图形处理相关工作的工程师来说,理解这个过程是有一定难度的,那么要怎么写才可以脉络清晰又浅显易懂呢,想来想去还是从日常开发中的界面UI开始分析吧,毕竟可直接感知


从一个简单的界面开始

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.blueColor;
// 蒙板视图
UIView *maskView = [[UIView alloc] initWithFrame:self.view.bounds];
maskView.backgroundColor = [UIColor redColor];
maskView.alpha = 0.3;
[self.view addSubview:maskView];
// 初始化屏幕大小的矩形路径
UIBezierPath *bpath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height) cornerRadius:8.0];
// 中间添加一个圆形的路径
[bpath appendPath:[UIBezierPath bezierPathWithArcCenter:maskView.center radius:100 startAngle:0 endAngle:2*M_PI clockwise:NO]];
//创建一个layer设置其CGPath为我们创建的路径并且赋值给蒙板视图的layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = bpath.CGPath;
maskView.layer.mask = shapeLayer;
}

效果如下:我们设置self.view的背景色为蓝色,然后添加了一层中间镂空的红色透明度0.3的蒙板,经过叠加、裁剪、混合(后面会详述)

数一数得到图中的效果,我们一共用了几个元素



  • 一个蓝色背景UIView

  • 一个蒙板视图UIView

  • 一个用贝塞尔曲线修改过的CAShapeLayer


我们自己创建的元素有三个,背景视图上面添加蒙板视图,蒙板视图的图层的蒙板层mask设置为自定义的镂空CAShapeLayer层,可见开发中既有视图又有图层,会不会觉得有些冗余呢


视图和图层为何拆分开来又聚合在一起呢


这是自图形界面发明出来就广泛应用的设计,拆开之后,layer负责界面显示,view负责事件分发,聚合一起是因为操作起来更方便,不能说开发者设置完事件层次之后,再设置一遍图层层次吧



iOS中用UIView和CALayer来描述两者,所有视图相关的类都继承自UIView,所有图层相关的类都继承自CALayer,view是layer的代理并且苹果建议我们不要修改这种关系,为了对开发者更友好,view中暴露了一些界面设置相关的属性,所有view显示相关的设置最终都会映射到与之绑定的layer上,这样的api设计有意隐藏了CALayer的部分功能,对开发者来讲不用关心那么多显示相关的细节



因此当我们研究渲染过程的时候,只需要关注CALayer即可


渲染数据流


纵观我们的app界面,图层上面加图层,图层又有子图层,整个结构是以CALayer或其子类对象为节点连接而成的树型结构,我们通常称之为图层树,苹果称其为模型树,layer默认是个矩形,我们可以通过path属性修改其形状,可以修改为圆形、三角形等任何规则不规则的形状,那么从图层树到屏幕上显示的界面都需要哪些步骤呢,如果你去查资料你可能会翻到图层树->呈现树->渲染树,那么他们都是什么东西呢,数据是如何流转的,CPU、GPU和渲染引擎都扮演了怎样的角色呢,我们来拆解下渲染数据流的处理过程



用CALayer构建图层树

我们编写的所有UI代码,最终都会以一个个的CALayer对象的形式被添加到渲染数据流中,在此过程中会做如下处理




  • 视图懒加载

    这是常规设计,通常所有界面元素都会用懒加载的方式去处理,即只有视图需要显示的时候才去加载它,以最大化优化内存,此过程同时会进行图片解压缩(当设置资源路径到UIImage或者UIImageVIew的时候)


  • 布局计算

    加载完视图之后,会进行addSubview、addSublayer操作,这是在设置图层之间的关系,所有图层会以superlayer、sublayer指针连接起来成为一个树型结构,一个节点只能有一个superlayer,可以有无限个sublayer,每个节点承载着与父子节点的位置关系以及渲染属性,当发生布局改变的时候,若是单纯的修改某个节点的渲染属性,则开销较小,若是修改层级,则整个图层树都需要重新计算修正


  • Core Graphics绘制

    如果实现了-drawRect:或者-drawLayer:inContext:方法,系统会以当前layer为画布创建一个寄宿图来单独绘制字符串或者图片,若是图片,同样会进行图片解压缩操作



以上过程图层树已生成,就是以CALayer对象为节点的树型结构



Core Animation构建呈现树

图层树仅仅是一个数据结构,而GPU只负责计算处理图形图像数据,因此在发送数据到渲染服务之前还需要把图层树图形图像化,在此过程中会做如下处理




  • CALayer生成图形

    遍历图层树,取出每个layer节点,根据渲染属性生成图形,通常都是矩形(可以是任意形状就像文章开篇那个界面一样)


  • 图片生成位图

    无论是直接通过UIImage或者UIImageView加载的图片还是通过drawRect或者drawLayer:inContext绘制的图片都会在此过程中生成位图(第一步仅仅是解码生成非压缩二进制流)



以上过程呈现树已生成,图层树、呈现树构建过程都是由CPU负责计算



渲染服务

呈现树已经是图形图像组织而成的树型结构,Core Animation通过IPC进程通信将其发送到渲染服务进程




  • 生成纹理

    如上的图形图像都是非格式化的数据,在计算机图形学里面通常称为Buffer,而GPU能处理的是格式化纹理数据Texture,在此过程中Core Animation会将呈现树Buffer数据通过渲染引擎转化为纹理数据Texture,自此渲染树已生成,这一步骤仍然由CPU计算


  • 顶点数据:包括顶点坐标、纹理坐标、顶点法线和顶点颜色等属性,顶点数据构成的图元信息(点、线、三角形等)需要参数代入绘制指令


  • 顶点着色器:将输入的局部坐标变换到世界坐标、观察坐标和裁剪坐标


  • 图元装配:将输入的顶点组装成指定的图元,这个阶段会进行裁剪和背面剔除相关优化


  • 几何着色器:将输入的图元扩展成多边形,将物体坐标变换为窗口坐标


  • 光栅化:将多边形转化为离散屏幕像素点并得到片元信息


  • 片元着色器:通过片元信息为像素点着色,这个阶段会进行光照计算、阴影处理等特效处理


  • 测试混合阶段:依次进行裁切测试、Alpha测试、模板测试和深度测试


  • 帧缓存:最终生成的图像存储在帧缓存,然后放入渲染缓冲区

  • 显示到屏幕



以上过程都由渲染引擎处理,除了生成纹理是CPU负责,其他都全权由GPU负责,以上就是界面渲染的全部过程,那我们自定义的画布呢,比如常见的播放器业务、地图业务都是怎么最终显示到屏幕的呢


自定义画布


我这篇文章有详述其组织渲染过程:https://www.jianshu.com/p/4613e0bcd31f,但是渲染到帧缓存之后,显示到屏幕过程是怎样的呢

iOS中是不支持直接渲染到屏幕的,我们的自定义画布,需要配合Core Animation来完成最终的显示,诸如播放器、地图等业务得到的渲染缓冲renderBuffer,需要通过layer关联到Core Animation层,待到生成渲染树的时候替换原来的内容,此过程只是一个指针替换,即将渲染树对应层的指针指向renderBuffer,然后由渲染引擎通过GPU最终将其呈现到屏幕


图层截图黑屏问题


播放器、地图类业务图层截图的时候会黑屏,原因是图层截图截取的是呈现树,此时自定义画布得到的renderBuffer还没有替换layer原来的内容,解决这个问题需要在截图的时候,将renderBuffer的内容在layer的上下文上单独绘制


当我们说界面卡了我们在说什么



屏幕会以60帧每秒的频率刷新,就是16.7毫秒一帧,系统渲染进程是不会卡的,除非发生了系统错误或者硬件错误,通常渲染服务进程会一直以16.7毫秒一帧不停的刷新,这个过程由VSync信号驱动,VSync信号由硬件时钟生成,每秒钟发出60次,渲染服务进程接收到VSync信号后,会通过IPC通知到活跃的App进程,进程内的所有线程的Runloop在启动后会注册对应的CFRunLoopSource,通过mach_port接收传过来的VSync信号,Runloop随之执行一次以驱动整个app的运行



页面卡顿,现象是当我们滑动列表或者点击一个按钮之后页面没有响应,本质原因是主线程卡了,因为整个UI界面的构建、计算、合成纹理过程都是在主线程进行的,主线程16.7毫秒之内没有执行完当前任务,该任务可能是:



  • 非UI业务逻辑耗时过多

  • 图层树构建组织过程耗时过多

  • 呈现树生成过程耗时过多


总之是渲染树没有更新导致看上去还是上一帧的画面,这就是卡顿


卡顿优化


优化卡顿无非就是减少上面几个步骤的耗时,使Runloop能在16.7毫秒之内执行完一次





  • 非UI业务逻辑耗时

    尽可能的将其放在非主线程执行,完成之后异步刷新UI






  • 图层树构建组织过程耗时

    1、布局计算的耗时与图层的个数、图层之间的层级关系和位置关系呈线性关系,即图层越多越耗时,图层关系越复杂越耗时,因此尽量用更少的图层个数和简单的图层关系来布局就是优化方向,而且尽量不要动态修改图层的层级关系,否则整个图层树都需要重新计算修正

    2、尽量不要重写-drawRect:或者-drawLayer:inContext:方法,因为Core Animation不得不生成一张layer等大小的寄宿图用于绘制,不仅占用额外的内存而且绘制过程是CPU计算的

    3、图片解码尽量在需要展示之前进行,SDWebImage做的就很好,不仅优化了图片文件IO而且图片解码也是在非主线程执行,完成之后异步刷新UI






  • 呈现树生成过程耗时

    这里要纠正个问题,离屏渲染是常规操作,经过优化的播放器、地图等业务都是用的离屏渲染,发生在主线程的离屏渲染才有性能问题,可以用CPU渲染也可以用GPU渲染,离屏渲染也是同理

    1、CALayer生成图形,离屏渲染发生在这个阶段,对于特定图层的圆角、图层遮罩、阴影或者是图层光栅化都会使Core Animation不得不进行当前图层的离屏绘制,不过在界面设计的时候,大多数设计师都钟爱于以上效果,使用起来也没有太大影响,只是会造成多余的计算、耗时和内存占用,如果不是列表型的界面,可以尽情的使用,对于列表型界面,我们可以禁用shouldRasterize(就是光栅化),这将会让图层离屏渲染一次后把结果保存起来,后面刷新会直接用缓存的结果

    2、图片生成位图,显然图片越多、越大计算量越大,就越耗时,因此如果能用CALayer实现的效果,尽量不要让设计师出图,不仅占用存储空间、占用内存而且加载耗时



总结


本文从一个简单的界面开始详细阐述了iOS渲染过程以及卡顿优化点,有些内容官方文档写的很清楚,甚至还有demo,大家在学习的时候首先应该关注的就是官方文档,下面给出两个官方参考链接:


https://developer.apple.com/documentation/quartzcore/calayer?language=objc


https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/WorkingwithEAGLContexts/WorkingwithEAGLContexts.html#//apple_ref/doc/uid/TP40008793-CH103-SW7



链接:https://www.jianshu.com/p/0ced61fd1f21

收起阅读 »

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的示例。


收起阅读 »

iOS 离屏渲染的研究

iOS
GPU渲染机制: CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。 GPU屏幕渲染有以下两种方式: On-Screen Ren...
继续阅读 »

GPU渲染机制:


CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。




GPU屏幕渲染有以下两种方式:



  • On-Screen Rendering

    意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。


  • Off-Screen Rendering

    意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。




特殊的离屏渲染:

如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式: CPU渲染。

如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内 同步地

完成,渲染得到的bitmap最后再交由GPU用于显示。

备注:CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程,一个简单的异步绘制过程大致如下

 - (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}



离屏渲染的触发方式


设置了以下属性时,都会触发离屏绘制:



  • shouldRasterize(光栅化)

  • masks(遮罩)

  • shadows(阴影)

  • edge antialiasing(抗锯齿)

  • group opacity(不透明)

  • 复杂形状设置圆角等

  • 渐变



其中shouldRasterize(光栅化)是比较特别的一种:

光栅化概念:将图转化为一个个栅格组成的图象。

光栅化特点:每个元素对应帧缓冲区中的一像素。




shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。




相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用。




当你使用光栅化时,你可以开启“Color Hits Green and Misses Red”来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。




如果光栅化的层变红得太频繁那么光栅化对优化可能没有多少用处。位图缓存从内存中删除又重新创建得太过频繁,红色表明缓存重建得太迟。可以针对性的选择某个较小而较深的层结构进行光栅化,来尝试减少渲染时间。




注意:

对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费




例如我们日程经常打交道的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化。则会造成大量的离屏渲染,降低图形性能。




光栅化有什么好处?



shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。



举个栗子



如果在滚动tableView时,每次都执行圆角设置,肯定会阻塞UI,设置这个将会使滑动更加流畅。

当shouldRasterize设成true时,layer被渲染成一个bitmap,并缓存起来,等下次使用时不会再重新去渲染了。实现圆角本身就是在做颜色混合(blending),如果每次页面出来时都blending,消耗太大,这时shouldRasterize = yes,下次就只是简单的从渲染引擎的cache里读取那张bitmap,节约系统资源。



而光栅化会导致离屏渲染,影响图像性能,那么光栅化是否有助于优化性能,就取决于光栅化创建的位图缓存是否被有效复用,而减少渲染的频度。可以使用Instruments进行检测:



当你使用光栅化时,你可以开启“Color Hits Green and Misses Red”来检查该场景下光栅化操作是否是一个好的选择。

如果光栅化的图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。



注意:

对于经常变动的内容,这个时候不要开启,否则会造成性能的浪费



为什么会使用离屏渲染


当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。


屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。


所以当使用离屏渲染的时候会很容易造成性能消耗,因为在OPENGL里离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。




Instruments监测离屏渲染


Instruments的Core Animation工具中有几个和离屏渲染相关的检查选项:



  • Color Offscreen-Rendered Yellow

    开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。


  • Color Hits Green and Misses Red

    如果shouldRasterize被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。





iOS版本上的优化


iOS 9.0 之前UIimageView跟UIButton设置圆角都会触发离屏渲染


iOS 9.0 之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染的。


这可能是苹果也意识到离屏渲染会产生性能问题,所以能不产生离屏渲染的地方苹果也就不用离屏渲染了。






链接:https://www.jianshu.com/p/6d24a4c29e18

收起阅读 »

Android 图片转场和轮播特效,你想要的都在这了

使用 OpenGL 做图像的转场效果或者图片轮播器,可以实现很多令人惊艳的效果。 GLTransitions 熟悉的 OpenGL 开发的朋友已经非常了解 GLTransitions 项目,该项目主要用来收集各种 GL 转场特效及其 GLSL 实现...
继续阅读 »

使用 OpenGL 做图像的转场效果或者图片轮播器,可以实现很多令人惊艳的效果。


ogl.gif


GLTransitions


gallery.gif


熟悉的 OpenGL 开发的朋友已经非常了解 GLTransitions 项目,该项目主要用来收集各种 GL 转场特效及其 GLSL 实现代码,开发者可以很方便地移植到自己的项目中。


GLTransitions 项目网站地址: gl-transitions.com/gallery


config.gif


GLTransitions 项目已经有接近 100 种转场特效,能够非常方便地运用在视频处理中,**很多转场特效包含了混合、边缘检测、腐蚀膨胀等常见的图像处理方法,由易到难。 **


对于想学习 GLSL 的同学,既能快速上手,又能学习到一些高阶图像处理方法 GLSL 实现,强烈推荐。


edit.png


另外 GLTransitions 也支持 GLSL 脚本在线编辑、实时运行,非常方便学习和实践。


Android OpenGL 怎样移植转场特效


github.gif github2.gif github3.gif


由于 GLSL 脚本基本上是通用的,所以 GLTransitions 特效可以很方便地移植到各个平台,本文以 GLTransitions 的 HelloWorld 项目来介绍下特效移植需要注意的几个点。


GLTransitions 的 HelloWorld 项目是一个混合渐变的特效:


// transition of a simple fade.
vec4 transition (vec2 uv) {
return mix(
getFromColor(uv),
getToColor(uv),
progress
);
}


transition 是转场函数,功能类似于纹理采样函数,根据纹理坐标 uv 输出 rgba ,getFromColor(uv) 表示对源纹理进行采样,getToColor(uv) 表示对目标纹理进行采样,输出 rgba ,progress 是一个 0.0~1.0 数值之间的渐变量,mix 是 glsl 内置混合函数,根据第三个参数混合 2 个颜色。


根据以上信息,我们在 shader 中只需要准备 2 个纹理,一个取值在 0.0~1.0 的(uniform)渐变量,对应的 shader 脚本可以写成:


#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D u_texture0;
uniform sampler2D u_texture1;
uniform float u_offset;//一个取值在 0.0~1.0 的(uniform)渐变量

vec4 transition(vec2 uv) {
return mix(
texture(u_texture0, uv);,
texture(u_texture1, uv);,
u_offset
);
}

void main()
{
outColor = transition(v_texCoord);
}


代码中设置纹理和变量:


glUseProgram (m_ProgramObj);

glBindVertexArray(m_VaoId);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[0]);
GLUtils::setInt(m_ProgramObj, "u_texture0", 0);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[1]);
GLUtils::setInt(m_ProgramObj, "u_texture1", 1);

GLUtils::setFloat(m_ProgramObj, "u_offset", offset);

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);

本文的 demo 实现的是一个图像轮播翻页效果,Android 实现代码见项目:


github.com/githubhaoha…


转场特效移植是不是很简单,动手试试吧。

收起阅读 »

深入浅出 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




    收起阅读 »

    ConstraintLayout 中的 Barrier 和 Chains

    1. Barrier 是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier。 具体看图 “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ,但是有可能“第二...
    继续阅读 »



    1. Barrier


    是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier



    1. 具体看图


    1883633-62653bd01cb70813.webp “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ,但是有可能“第二行value”的值为空或者是空,也需要“第二行label”距离上面的距离是 100dp ,由于我们知道“第二行value”的高度高于第一个,所以采用的是“第二行label”跟“第二行value”对其,“第二行value”距离上边 100dp 的距离,但是由于“第二行value”有可能为空,所以当“第二行value”为空的时候就会出现下面的效果:


    1883633-043d00e43ff22557.webp 我们发现达不到预期,现在能想到的办法有,首先在代码控制的时候随便把“第二行label”的 marginTop 也添加进去;还有就是换布局,将“第二行label”和“第二行value”放到一个布局中,比如 LinearLayout ,这样上边的 marginTopLinearLayout 控制;这样的话即便“第二行value”消失了也会保持上边的效果。


    除了上边的方法还能使用其他的嘛,比如我们不使用代码控制,我们不使用其他的布局,因为我们知道布局嵌套太多性能也会相应的下降,所以在编写的时候能减少嵌套的情况下尽可能的减少,当然也不能为了减少嵌套让代码变得格外的复杂。


    为了满足上面的需求, Barrier 出现了,它能做到隐藏的也能依靠它,并且与它的距离保持不变对于隐藏的“第二行value”来说,虽然消失了,但保留了 marginTop 的数值。下面看看布局:


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.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">


    <TextView
    android:id="@+id/textView4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="头部"
    android:textSize="36sp"
    app:layout_constraintBottom_toTopOf="@id/barrier3"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />



    <androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="top"
    android:layout_marginTop="100dp"
    app:constraint_referenced_ids="textView2,textView3" />


    <TextView
    android:id="@+id/textView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="4dp"
    android:text="第二行label"
    android:textSize="36sp"
    app:layout_constraintBottom_toBottomOf="@+id/textView3"
    app:layout_constraintStart_toStartOf="@+id/textView4"
    app:layout_constraintTop_toTopOf="@+id/textView3"
    app:layout_constraintVertical_bias="0.538" />


    <TextView
    android:id="@+id/textView3"
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:layout_marginStart="12dp"
    android:layout_marginTop="100dp"
    android:text="第二行value"
    android:textSize="36sp"
    app:layout_constraintStart_toEndOf="@+id/textView2"
    app:layout_constraintTop_toBottomOf="@+id/barrier3" />


    </androidx.constraintlayout.widget.ConstraintLayout>

    这样即便将“第二行value”消失,那么总体的布局仍然达到预期,并且也没有添加很多布局内容。在代码中:


    <androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="top"
    android:layout_marginTop="100dp"
    app:constraint_referenced_ids="textView2,textView3" />


    这里主要有两个属性 app:barrierDirectionapp:constraint_referenced_ids



    • app:barrierDirection 是代表位置,也就是在包含内容的哪一个位置,我这里写的是 top ,是在顶部,还有其他的属性 top,bottom,left,right,startend 这几个属性,看意思就很明白了。

    • app:constraint_referenced_ids 上面说的内容就是包含在这里面的,这里面填写的是 id 的名称,如果有多个,那么使用逗号隔开;这里面的到 Barrier 的距离不会改变,即便隐藏了也不会变。


    这里可能会有疑惑,为啥我写的 idtextView4 的也依赖于 Barrier ,这是因为本身 Barrier 只是规则不是实体,它的存在只能依附于实体,不能单独存在于具体的位置,如果我们只有“第二行value”依赖于它,但是本身“第二行value”没有上依赖,也相当于没有依赖,这样只会导致“第二行label”和“第二行value”都消失,如果 textView4 依赖于 Barrier ,由于 textView4 的位置是确定的,所以 Barrier 的位置也就确定了。



    1. 类似表格的效果。看布局效果:


    1883633-c4b862a2df57fb96.webp 我要做成上面的样子。也就是右边永远与左边最长的保持距离。下面是我的代码:


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.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">



    <TextView
    android:id="@+id/textView4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:text="头部"
    android:textSize="36sp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />


    <TextView
    android:id="@+id/textView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="4dp"
    android:text="第二行"
    android:textSize="36sp"
    app:layout_constraintBottom_toBottomOf="@+id/textView3"
    app:layout_constraintStart_toStartOf="@+id/textView4"
    app:layout_constraintTop_toTopOf="@+id/textView3" />


    <TextView
    android:id="@+id/textView3"
    android:layout_width="wrap_content"
    android:layout_height="50dp"
    android:layout_marginStart="20dp"
    android:layout_marginTop="10dp"
    android:text="第二行value"
    android:textSize="36sp"
    app:layout_constraintStart_toEndOf="@+id/barrier4"
    app:layout_constraintTop_toBottomOf="@+id/textView4" />



    <TextView
    android:id="@+id/textView5"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="4dp"
    android:text="第三次测试"
    android:textSize="36sp"
    app:layout_constraintBottom_toBottomOf="@+id/textView6"
    app:layout_constraintStart_toStartOf="@+id/textView4"
    app:layout_constraintTop_toTopOf="@+id/textView6" />


    <TextView
    android:id="@+id/textView6"
    android:layout_width="wrap_content"
    android:layout_height="50dp"
    android:layout_marginStart="20dp"
    android:layout_marginTop="10dp"
    android:text="第三行value"
    android:textSize="36sp"
    app:layout_constraintStart_toEndOf="@+id/barrier4"
    app:layout_constraintTop_toBottomOf="@+id/textView3" />


    <androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="end"
    app:constraint_referenced_ids="textView2,textView5"
    tools:layout_editor_absoluteX="411dp" />



    </androidx.constraintlayout.widget.ConstraintLayout>

    添加好了,记得让右边的约束指向 Barrier 。这里的 Barrier ,我们看到包含 textView2textView5 ,这个时候就能做到谁长听谁的,如果此时 textView2 变长了,那么就会将就 textView2


    2. Chains


    我们特别喜欢使用线性布局,因为我们发现 UI 图上的效果使用线性布局都可以使用,当然也有可能跟大部分人的思维方式有关系。比如我们非常喜欢的,水平居中,每部分的空间分布等等都非常的顺手。既然线性布局这么好用,那为啥还有约束布局呢,因为线性布局很容易写出嵌套很深的布局,但约束布局不会,甚至大部分情况都可以不需要嵌套就能实现,那是不是代表线性布局有的约束布局也有,答案是肯定的。


    使用普通的约束关系就很容易实现水平居中等常用效果,其他的如水平方向平均分布空间,使用一般的约束是实现不了的,于是就要使用 Chains ,这个就很容易实现下面的效果:


    1883633-78aa31c23dcb4c4f.webp 其实上一篇中我已经把官网的教程贴上去了,这里主要写双向约束怎么做,一旦双向约束形成,那么就自然进入到 Chains 模式。


    1)在视图模式中操作


    1883633-618f9b2eb563a637.webp


    如果直接操作,那么只能单向约束,如果要形成这样的约束,需要选择相关的的节点,比如我这里就是同时选择 AB ,然后点击鼠标右键,就可以看到 ChainsCreate Horizontal Chain


    对应的操作


    选择图中的选项即可完成从 A 指向 B ,修改的示意图为:


    1883633-cf3984e22df83c7c.webp


    我们发现已经实现了水平方向的排列效果了。至于怎么实现上面的效果,主要是改变 layout_constraintVertical_chainStylelayout_constraintHorizontal_chainStyle 属性。至于权重则是属性 layout_constraintHorizontal_weight


    layout_constraintHorizontal_chainStyle 属性说明:



    • spread 默认选项,效果就是上面的那种,也就是平均分配剩余空间;

    • spread_inside 两边的紧挨着非 Chains 的视图,中间的平均分配;


    1883633-49c52026c6797e51.webp



    • packed 所有的都在中间


    1883633-714e58d28eaab99c.webp 注意了, layout_constraintHorizontal_weight 这个属性只有在 A 身上设置才可以,也就是首节点上设置才可行,同时 layout_constraintHorizontal_weight 是代表水平方向,只能在水平方向才发生作用,如果水平的设置了垂直则不生效。


    layout_constraintHorizontal_weight 这个属性只有在当前视图的宽或者高是 0dp 。至于这个的取值跟线性布局相同。


    1883633-072f1f968528ef1a.webp


    2)代码的方式 跟上面的差别就是在做双向绑定,用代码就很容易实现双向绑定,可平时添加约束相同。

    收起阅读 »

    iOS - 多线程应用场景

    iOS
    实际项目开发中为了能够给用户更好的体验,有些延时操作我们都会放在子线程中进行。今天我们就来聊聊多线程在实际项目中的运用。我们先来看看多线程的基础知识:1.多线程的原理:        同一时间,CPU只能处理一条线程,也...
    继续阅读 »

    实际项目开发中为了能够给用户更好的体验,有些延时操作我们都会放在子线程中进行。

    今天我们就来聊聊多线程在实际项目中的运用。

    我们先来看看多线程的基础知识:

    1.多线程的原理:

            同一时间,CPU只能处理一条线程,也就是只有一条线程在工作。所谓多线程并发(同时)执行,

    其实是CPU快速的在多线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并

    发执行的假象。

    2.在实际项目开发中并不是线程越多越好,如果开了大量的线程,会消耗大量的CPU资源,CPU会

    被累死,所以一般手机只开1~3个线程为宜,不超过5个。

    3.多线程的优缺点:

    优点:1.能适当提高程序的执行效率

           2.能适当提高资源的利用率,这个利用率表现在(CPU,内存的利用率)

    缺点:1.开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,

    子线程占用512KB,如果开启大量的线程,会占用大量的内存空间,降低程序

    的性能)

         2.线程越多,CPU在调度线程上的开销就越大

         3.程序设计就越复杂:比如线程之间的通信,多线程的数据共享,这些

    都需要程序的处理,增加了程序的复杂度。

    4.在iOS开发中使用线程的注意事项:

        1.别将比较耗时的操作放在主线程中

        2.耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验

    好了,多线程在iOS中的开发概念性的东西就讲这么多,下面我们来模拟一种开发中的场景:

    我们在开发中经常会遇到,当你要缓存一组图片,但是这些图片必须要等到你缓冲好了后再来展现在UI上,

    可是我们缓存图片的时候用的是SDWebImage框架,缓存的操作是异步进行的,我们如何来做到等缓存好了

    再来执行以后的操作呢?下面讲个实现起来非常简单,方便的方法:

    我先来放上代码,后面进行讲解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //1.添加一个组
            let group = dispatch_group_create()
             
            //缓存图片
            for url in picURLs! {
                 
                //2.将当前的下载操作添加到组中
                dispatch_group_enter(group)
                SDWebImageManager.sharedManager().downloadImageWithURL(url, options: SDWebImageOptions.init(rawValue: 0), progress: nil, completed: { (_, _, _, _, _) in
                     
                    //3.离开当前组
                    dispatch_group_leave(group)
                    print("正在缓存中...")
                })
            }
             
            //通过闭包将数据传递给调用者(通知图片缓存完毕)
            dispatch_group_notify(group, dispatch_get_main_queue()) {
                print("缓存完毕!")
                finished()
            }

     

    从输出结果我们就可以看出来:我们做到了缓存完毕后再来执行以后的操作。

    是如何做到的呢?

    我在代码中已经用数字标出来了:

    1.我们首先用

    let group = dispatch_group_create()

    函数来创建一个组,用来存放缓冲的操作

    2.用这个函数做到把每一次的缓冲操作都添加到组中

    1
    dispatch_group_enter(group)

     3.缓存图片我用的是SDWebImage框架,我们可以看到,我在缓冲完毕后离开当前组,用到如下函数

    dispatch_group_leave(group)

    用了这三步就能做到我们想要的功能吗?显然不是,做了这三部系统内部就会为我们做些事了,

     

    当我们离开当前组的时候,系统就会发出一个通知,我们来接收这个通知,当我们接收到这个通知的时候

    我们就可以执行finished的操作了,接收通知的函数是:

    dispatch_group_notify(group, dispatch_get_main_queue()) {
    print(
    "缓存完毕!")
    finished()
    }

    以上就是一个非常方便的实现我们需要的功能的方法

     

    https://blog.csdn.net/qq_24904667/article/details/52679473

    收起阅读 »

    swift 多线程下

    iOS
    Swift多线程编程方案: Thread Cocoa Operation (Operation 和 OperationQueue) Grand Central Dispath (GCD) 1. Thread在三种多线程技术中是最轻量级的, 但需要自己...
    继续阅读 »

    Swift多线程编程方案:



    • Thread


    • Cocoa Operation (OperationOperationQueue)


    • Grand Central Dispath (GCD)




    1. Thread在三种多线程技术中是最轻量级的, 但需要自己管理线程的生命周期和线程同步. 线程同步对数据的加锁会有一定的系统开销.



    • detachNewThread(_ block: @escaping () -> Void)

    • detachNewThreadSelector(_ selector: Selector, to Target target: Any, with argument: Any?)



    e.g.

    for i in 0...10 {
    Thread.detachNewThread {
    print("\(i) \(Thread.current)")
    }
    }

    输出结果:

    8 <NSThread: 0x6000000f8e40>{number = 12, name = (null)}
    10 <NSThread: 0x6000000f0240>{number = 17, name = (null)}
    7 <NSThread: 0x6000000cc0c0>{number = 10, name = (null)}
    1 <NSThread: 0x6000000c0180>{number = 14, name = (null)}
    6 <NSThread: 0x6000000efe80>{number = 9, name = (null)}
    4 <NSThread: 0x6000000efdc0>{number = 11, name = (null)}
    5 <NSThread: 0x6000000c8580>{number = 15, name = (null)}
    9 <NSThread: 0x6000000cc080>{number = 8, name = (null)}
    0 <NSThread: 0x6000000fd300>{number = 7, name = (null)}
    2 <NSThread: 0x6000000cc5c0>{number = 13, name = (null)}
    3 <NSThread: 0x6000000f0780>{number = 16, name = (null)}


    e.g.

    class ObjectForThread {
    func threadTest() -> Void {
    let thread = Thread(target: self, selector: #selector(threadWorker), object: nil)
    thread.start()
    print("threadTest")
    }
    @objc func threadWorker() -> Void {
    print("threadWorker Run")
    }
    }

    let obj = ObjectForThread()
    obj.threadTest()

    输出结果:

    threadTest
    threadWorker Run




    2. OperationOperationQueue


    Operation



    • 面向对象 (OperationBlockOperation)


    • Operation+ OperationQueue

    • 取消、依赖、任务优先级、复杂逻辑、保存业务状态、子类化


    Operation的四种状态 :



    • isReady

    • isExecuting

    • isFinished

    • isCancelled


    OperationQueue




    • OperationQueue队列里可以加入很多个Operation, 可以把OperationQueue看做一个线程池, 可以往线程池中添加操作(Operation)到队列中

    • 底层使用GCD


    • maxConcurrentOperationCount可以设置最大并发数


    • defaultMaxConcurrentOperationCount根据当前系统条件动态确定的最大并发数

    • 可以取消所有Operation, 但是当前正在执行的不会取消

    • 所有Operation执行完毕后退出销毁



    e.g. BlockOperation

    class ObjectForThread {
    func threadTest() -> Void {
    let operation = BlockOperation { [weak self] in
    self?.threadWorker()
    }
    let queue = OperationQueue()
    queue.addOperation(operation)
    print("threadTest")
    }
    }

    let obj = ObjectForThread()
    obj.threadTest()


    e.g. 自定义的Operation

    class ObjectForThread {
    func threadTest() -> Void {
    let operation = MyOperation()
    operation.completionBlock = {() -> Void in
    print("完成回调")
    }
    let queue = OperationQueue()
    queue.addOperation(operation)
    print("threadTest")
    }
    }
    class MyOperation: Operation {
    override func main() {
    sleep(1)
    print("MyOperation")
    }
    }

    let obj = ObjectForThread()
    obj.threadTest()



    3. GCD


    GCD特点



    • 任务+队列

    • 易用

    • 效率

    • 性能


    GCD - 队列



    • 主队列: 任务在主线程执行

    • 并行队列: 任务会以先进先出的顺序入列和出列, 但是因为多个任务可以并行执行, 所以完成顺序是不一定的.

    • 串行队列: 任务会以先进先出的顺序入列和出列, 但是同一时刻只会执行一个任务


    GCD - 队列API



    • Dispatch.main

    • Dispatch.global

    • DispatchQueue(label:,qos:,attributes:,autoreleaseFrequency:,target:)

    • queue.label

    • setTarget(queue:DispatchQueue)


    • 最终的目标队列都是主队列和全局队列

    • 如果把一个并行队列的目标队列都设置为同一个串行队列, 那么这多个队列连同目标队列里的任务都将串行执行

    • 如果设置目标队列成环了, 结果是不可预期的

    • 如果在一个队列正在执行任务的时候更换目标队列, 结果也是不可预期的



    e.g.

    let queue = DispatchQueue(label: "myQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
    queue.async {
    sleep(3)
    print("queue")
    }
    print("end")


    e.g.

    let queue = DispatchQueue(label: "myQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
    queue.asyncAfter(deadline: DispatchTime.now() + 10) {
    print("in asyncAfter")
    }
    print("end")

    DispatchTime 系统时钟

    DispatchWallTime生活时间


    GCD 高级特性- DispatchGroup



    e.g. 阻塞当前线程

    let group = DispatchGroup()
    let queue = DispatchQueue(label: "test.queue")

    group.enter()
    queue.async {
    sleep(3)
    print("操作 1")
    group.leave()
    }

    group.enter()
    queue.async {
    sleep(3)
    print("操作 2")
    group.leave()
    }

    print("操作1 操作2 安排完成")

    group.wait()
    print("操作1 操作2 全部执行完毕")


    e.g. 非阻塞方式:

    let group = DispatchGroup()
    let queue = DispatchQueue(label: "test.queue")

    group.enter()
    queue.async {
    sleep(3)
    print("操作 1")
    group.leave()
    }

    group.enter()
    queue.async {
    sleep(3)
    print("操作 2")
    group.leave()
    }

    print("操作1 操作2 安排完成")

    group.notify(queue: queue) {
    print("操作1 操作2 全部执行完毕")
    }
    print("非阻塞")

    GCD 高级特性- DispatchSource



    • 简单来说, dispatch source是一个监听某些类型事件的对象. 当这些事件方法时, 它自动将一个task放入一个dispatch queue的执行历程中.



    e.g. DispatchSource- Timer

    var seconds = 10
    let timer: DispatchSourceTimer = DispatchSource.makeTimerSource(flags: .strict, queue: .global())
    timer.schedule(deadline: .now(), repeating: 1.0)
    timer.setEventHandler {
    seconds -= 1
    if seconds < 0 {
    timer.cancel()
    }
    else {
    print(seconds)
    }
    }
    timer.resume()


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

    收起阅读 »

    Swift 多线程上

    iOS
    提到多线程,无非就是关注二点,一是线程安全问题,二是在合适的地方合适的使用多线程(这个就有点广泛了,但是很重要不能为了去使用而使用)。 先看下OC中定义属性的关键字atomic/nonatomic,原子属性和非原子属性(此处先不谈内存相关的知识),有啥区别呢...
    继续阅读 »

    提到多线程,无非就是关注二点,一是线程安全问题,二是在合适的地方合适的使用多线程(这个就有点广泛了,但是很重要不能为了去使用而使用)。




    先看下OC中定义属性的关键字atomic/nonatomic,原子属性和非原子属性(此处先不谈内存相关的知识),有啥区别呢?property申明的属性会默认实现setter和get方法,而atomic默认会给setter方法加锁。也就是atomic线程安全,防止数据在被不同线程读写时发生了错误,设置属性时默认就是nonatomic。万能的苹果开发大神为啥不都设置为线程安全呢,主要是因为atomic很耗内存资源,相比这点安全远不能弥补在内存资源和运行速度上的缺陷,所以需要我们在开发时,适当的时候加锁。


    说到加锁(我自己了解的也不是很多),只能说说最基本的。先看我们在OC中创建单利时,用到的关键字synchronized('加锁对象'){'加锁代码'},互斥锁,简单粗暴,不需要释放啥的,在不考虑效率下还是很方便(创建个单利能耗啥效率了,如果在swift中使用那么就是objc_sync_enter('加锁对象')和objc_sync_exit('加锁对象'))。还有就是NSLock,这个或许偶尔会用到,对象锁,lock和unlock需要成对出现,不能频繁调用lock不然可能会出现线程锁死

    let lock = NSLock
    lock.lock()
    defer {
    lock.unlock()
    }

    只是简单说下锁(至于NSRecursiveLock,NSConditionLock等各种锁的我也不是很熟,应该说是没有用过),如果多线程出现问(愿你写的多线程永远不出现线程安全问题,但是需要谨记一句:线程复杂必死),至少知道有解决的方法,在使用多线程稍微注意下。关于线程锁和线程发生锁死,我也只是略懂,以后再总结。除了加锁还可以用其他方法避免线程问题,比如:串行队列(FMDB线程安全就是这种方式),合适的使用信号量DispatchSemaphore。


    我自己也是在开发中遇到二次线程安全问题,一次在使用CoreData,还有一次就是在多线程上传图片的时候,都是泪。CoreData,最后我是使用了串行队列解决线程安全问题,CoreData是有安全队列方法的,但是呢那个是接手的项目,别人写好了,我就在原来的基础上修改的。多线程上传图片出现问题,完全是自己不够细心导致的,过了好久才意识到的问题,先说下,OC中的系统对象基本都是线程不安全的,都是操作指针,swift中的系统对象基本都是线程安全的,都是直接读取内存。即使对象是线程安全的,但是也不能在不同的线程操作同一个对象,应该先copy在再操作,就是直接用=(OC是不行的)。




    说了一大堆线程相关的废话,那再看看怎样在Swift 中使用多线程,其实最常用的也就是GCD和Operation,如果说对线程安全,GCD,Operation,队列和线程关系不了解,那就去网上找资料补一补。


    最常用的API,主线程延迟执行: DispatchQueue.main.asyncAfter(deadline: DispatchTime) {}

    获取主线程(一个应用就一个主程):

    DispatchQueue.main.async {}


    再就是GCD创建队列了,分为串型队列和并发队列(串型队列只会开一个子线程,并发队列会创建多个子线程)


    /// 并发队列
    let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
    /// 串行队列
    let queue = DispatchQueue(label: "test.queue")

    接着就是异步/同步(是否阻塞当前线程的区别,注意下线程相互锁死问题)执行任务的API了,任务放到队列里面去,由队列决定是否开辟新的线程执行任务,强调一点,任何UI相关的任务不能放到子线程中执行,子线程是不能刷新UI的,如果你看到你的子线程刷新了UI那是你的眼晴欺骗了你,原因是UIKit框架不是线程安全,可能会出现资源争夺,所以只能在主线程中绘制。只是简单的介绍下比较常用的API,相对于OC的写法已经简单很多了。具体的区别和参数可以参考官方的文档,貌似还很复杂。

    接下来就可以愉快的异步线程执行耗时任务了,执行完再回到主线程中,该干嘛就干嘛了,简单粗暴。但是还是有问题需要我们去注意:



    • 任务全部执行完成的回调

    • 怎样控制执行的先后顺序(不考虑串行)

    • 怎样控制并发的数量

    • 怎样暂停/挂起任务


    很明显,有些问题GCD可能不太容易解决。


    Operation 和OperationQueue,基于GCD封装的对象,支持KVO,处理多线程的API更方便,如果你看过其他第三方框架,可以经常看到这些对象,GCD反而用的比较少。最开始出来找工作时,其实我很不喜欢别人在没有给应用场景时问我什么时候使用GCD,什么时候使用OperationQueue有种被套路的感觉(在合适的地方合理的去使用合理的多线程),就现在而言,如果简单不复杂首选GCD,当然想用OperationQueue去封装,也完全赞同,主要是看业务需求,而不是看技术需求。关于GCD和OperationQueue的区别以及性能,具体还是推荐看网上的博客,最好是先了解里面处理多线程的API,知道各自的优缺点,再去看会有不一样的收获。Operation是一个抽象类不能直接使用,可以自定义其子类,或者直接使用系统提供的子类BlockOperation(swift中系统提供的子类貌似就剩这么一个了)。Operation直接使用是没有开辟新的线程的,只有放到OperationQueue 中才是多线程(OperationQueue可以直接使用,复杂场景时会使用到Operation的子类)。OperationQueue使用的优点会在下面解决问题中提到部分。


    再看GCD中的队列组DispatchGroup,也可以对队列做简单的管理。主要将某一个任务入组和出组,实现入组和出组主要用到二种方式,那么就可以监听入组的任务是否全部完成。

    let groupQueue = DispatchGroup()
    let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.background)
    defer {
    groupQueue.notify(queue: DispatchQueue.main) {
    debugPrint("执行完成\(Thread.current)")
    }
    }
    /// 第一种直接创建的时候就加到组中,执行完成自动出组
    queue.async(group: groupQueue) {
    for i in 0 ... 100 {
    debugPrint(Thread.current , i , "------1")
    }
    }
    /// 第二种自己执行前加入,完成后手动出组
    groupQueue.enter()
    queue.async() {
    for i in 0 ... 100 {
    debugPrint(Thread.current , i , "------2")
    }
    groupQueue.leave()
    }

    二种方式只有选择的差距,第一种比较简单,但是适用的场景就比较简单,但是缺点也很明显,当执行的任务也存在异步,那么就不能判断是否真的完成任务了,很简单的例子,同时发送5个请求,要数据全部返回才能执行回调,那么很明显就只能使用第二种方式了,只有数据真正的返回了才出组。




    先看第一个问题,比较简单,怎样知道队列中的任务全部执行完成?

    使用GCD就很简单了,将任务放到队列组中去执行,完成后自动回调,当然也可以使用OperationQueue,那就是要使用KVO了监听OperationQueue 的maxConcurrentOperationCount属性,当为0的时候任务全部执行完成。


    第二个问题,怎样控制执行的先后顺序?如果使用OperationQueue比较容易,方法比较多,举二个例子。看参数就知道,直到ops中的任务执行完成再接着执行其他的任务,如果每个任务都有严格的顺序,那么直接设置OperationQueue的maxConcurrentOperationCount为1就行了,为1时候其实也就是串行队列了。

    func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool)

    第二种方式就是添加依耐关系

    let operation1 = BlockOperation {
    debugPrint("开始1", Thread.current)
    sleep(2)
    debugPrint("完成 ")
    }
    let operation2 = BlockOperation {
    debugPrint("开始2")
    sleep(2)
    debugPrint("完成")
    }
    operation2.addDependency(operation1)
    operationQueue.addOperation(operation1)
    operationQueue.addOperation(operation2)

    如果使用GCD的话,那么就需要使用栅栏函数了,这个可以具体看看官方的文档或者网上优秀博客,这就举一个简单的列子

    /// 栅栏函数要求队列参数为. concurrent 
    let queue = DispatchQueue(label: "text.queue", attributes: .concurrent)
    queue.async {
    for i in 0 ... 10 {
    debugPrint(i , Thread.current , "------1")
    }
    }
    queue.async {
    for i in 0 ... 10 {
    debugPrint(i , Thread.current , "-----2")
    }
    }
    /// 栅栏函数,前面的执行完,再执行栅栏函数里面的代码,执行完,再执行后面的任务
    queue.async(flags: .barrier) {
    for i in 0 ... 10 {
    debugPrint(i , Thread.current , "------3")
    }
    }
    queue.async {
    for i in 0 ... 10 {
    debugPrint(i , Thread.current , "------4")
    }
    }
    queue.async {
    for i in 0 ... 10 {
    debugPrint(i , Thread.current , "------5")
    }
    }

    举的例子比较简单,遇到实际问题其实是很复杂的(强烈建议多看第三方框架,看看别人怎么封装的,多学习),考虑的问题比较多。


    第三个问题,怎样控制并发数量?问题二中已经回答了设置OperationQueue的maxConcurrentOperationCount就可以控制并发量了。GCD中的话就可以使用信号量(DispatchSemaphore)去控制并发量了,用法也很简单。信号量还有很多其他的用法,比如将异步变成同步(这种骚操作不建议去使用信号量),资源的保护等等

    let queue = DispatchQueue.global(qos:    DispatchQoS.QoSClass.background)
    /// 初始化信号量为2,最大并发为2,为0时会等待
    let semap = DispatchSemaphore.init(value: 2)
    semap.wait() // 信号量减1
    queue.async {
    debugPrint("开始执行", Thread.current)
    sleep(2)
    debugPrint("执行完成" , Thread.current)
    semap.signal() // 信号量加1
    }
    semap.wait()
    queue.async {
    debugPrint("开始执行", Thread.current)
    sleep(2)
    debugPrint("执行完成" , Thread.current)
    semap.signal()
    }
    semap.wait()
    queue.async {
    debugPrint("开始执行", Thread.current)
    sleep(2)
    debugPrint("执行完成" , Thread.current)
    semap.signal()
    }

    第四个问题,怎样暂停/挂起任务?GCD可以但是相当麻烦,OperationQueue就很简单了,可以挂起,暂停所有的任务,也可以取消所有任务,在控制任务执行上,还是很方便的。这个可以参考下Alamofire中的设计,可以避免双重回调嵌套,在此不仔细详解,大概提下Alamofire设计的思路,有个方法是json序列化的,在我们最后获取数据的回调block中,但是,调用这个方法时,是将任务放到了OperationQueue中并且设置了maxConcurrentOperationCount为1,并且设置isSuspended为true,什么时候执行呢,当后台数据返回成功时isSuspended置为false,开始执行序列化的代码,执行完后我们在回调中获取后台返回的数据,没有嵌套block。


    整个总结比较简单,只是大概说了下可能会遇到的问题,提供下简单的解决思路,解决复杂的问题,还需要再去多尝试,多去了解相关的文档,了解每一个API甚至参数使用的场景,或许用不到,但是万一在解决bug时不小心找到灵感了呢,书到用时方恨少。但是还是想强调下:线程复杂必死。



    链接:https://www.jianshu.com/p/5fb6e565ee23

    收起阅读 »

    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
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »