注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Docker 快速部署 Node express 项目

前言 本文章讲解如何简单快速部署 node API 项目。可作为docker入门学习。 Node 项目基于 express+sequelize 框架。 数据库使用 mysql。 Docker 安装 Docker 官方下载地址:docs.docker.com/g...
继续阅读 »

前言


本文章讲解如何简单快速部署 node API 项目。可作为docker入门学习。


Node 项目基于 express+sequelize 框架。


数据库使用 mysql。


Docker 安装


Docker 官方下载地址:docs.docker.com/get-docker


检查 Docker 安装版本:$ docker --version


Dockerfile



Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。

Dockerfile 学习地址:http://www.runoob.com/docker/dock…



在项目根目录下编写 Dockerfile 文件:


7231624506430_.pic.jpg


FROM node:12.1    :基于 node:12.1 的定制镜像
LABEL maintainer="kingwyh1993@163.com" :镜像作者
COPY . /home/funnyService :制文件到容器里指定路径
WORKDIR /home/funnyService :指定工作目录为,RUN/CMD 在工作目录运行
ENV NODE_ENV=production :指定环境变量 NODE_ENV 为 production
RUN npm install yarn -g :安装 yarn
RUN yarn install :初始化项目
EXPOSE 3000 :声明端口
CMD [ "node", "src/app.js" ] :运行 node 项目 `$ node src/app.js`

注:CMD 在docker run 时运行。RUN 是在 docker build。
复制代码

docker-compose



Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。

docker-compose 学习地址:http://www.runoob.com/docker/dock…



在根目录下编写 docker-compose.yml 文件:


7241624516284_.pic.jpg


container_name: 'funny-app'  :指定容器名称 funny-app
build: . :指定构建镜像上下文路径,依据 ./Dockerfile 构建镜像
image: 'funny-node:2.0' :指定容器运行的镜像,名称设置为 funny-node:2.0
ports: :映射端口的标签,格式为 '宿主机端口:容器端口'
- '3000:3000' :这里 node 项目监听3000端口,映射到宿主机3000端口

复制代码

本地调试


项目根目录下执行 $ docker-compose up -d


查看构建的镜像 $ docker images 检查有上述 node、funny-node 镜像则构建成功


查看运行的容器 $ docker ps 检查有 funny-app 容器则启动成功


调试接口 http://127.0.0.1:3000/test/demo 成功:


image.png


服务器部署运行


在服务器 git pull 该项目


执行 $ docker-compose up -d


使用 $ docker images $ docker ps 检查是否构建和启动成功


调试接口 http://服务器ip:3000/test/demo



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

收起阅读 »

[react-native]JSX和RN样式以及和web的不同之处

全屏状态栏 import { View, Text, Image, StatusBar } from 'react-native' <StatusBar backgroundColor="transparent" translucent={ true }...
继续阅读 »

全屏状态栏


import { View, Text, Image, StatusBar } from 'react-native'
<StatusBar backgroundColor="transparent" translucent={ true } />


JSX:React中写组件的代码格式, 全称是JavaScript xml


import React from 'react'
import { View, Text } from 'react-native'

const App = () => <View>
<Text>JSX Hello World</Text>
</View>

export default App


RN样式(主要讲解和web开发的不同之处)


image.png


#屏幕宽度和高度
import { Dimensions } from 'react-native'
const screenWidth = Math.round(Dimensions.set('window').width)
const screenHeight = Math.round(Dimensions.get('window').height)

#变换
<Text style={{ transform: [{translateY: 300}, {scale: 2}] }}>变换</Text>


标签



  1. View

  2. Text

  3. TouchableOpacity

  4. Image

  5. ImageBackground

  6. TextInput

  7. 其他 =>

    1. button

    2. FlatList

    3. ScrollView

    4. StatusBar

    5. TextInput




View



  1. 相当于以前web中的div

  2. 不支持设置字体大小, 字体颜色等

  3. 不能直接放文本内容

  4. 不支持直接绑定点击事件(一般使用TouchableOpactiy 来代替)


Text



  1. 文本标签,可以设置字体颜色、大小等

  2. 支持绑定点击事件


TouchableOpacity (onpress => 按下事件 onclick=> 点击事件)


可以绑定点击事件的块级标签



  1. 相当于块级的容器

  2. 支持绑定点击事件 onPress

  3. 可以设置点击时的透明度


import React from 'react'
import {TouchableOpacity, Text} from 'react-native'

const handlePress = () => {
alert('111')
}

const App = () =>
<TouchableOpacity activeOpacity={0} onPress={ handlePress }>
<Text>点击事件</Text>
</TouchableOpacity>

export default App


Image图片渲染


1.渲染本地图片时


<Image source={ require("../gril.png") } />


2.渲染网络图片时, 必须加入宽度和高度


<Image source={{ uri: 'https://timgsa.baidu.com/xxx.png }} style={{ width: 200, height: 300 }} />


3.在android上支持GIF和WebP格式图片


默认情况下Android是不支持gif和webp格式的, 只需要在 android/app/build.gradle 文件中根据需要手动添加


以下模块:


dependencies {
// 如果你需要支持android4.0(api level 14)之前的版本
implementation 'com.facebook.fresco:animated-base-support:1.3.0'

// 如果你需要支持GIF动画
implementation 'com.facebook.fresco:animated-gif:2.0.0'

// 如果你需要支持webp格式,包括webp动图
implementation 'com.facebook.fresco:animated-webp:2.1.0'
implementation 'com.facebook.fresco:webpsupport:2.0.0'

// 如果只需要支持webp格式而不需要动图
implementation 'com.facebook.fresco:websupport:2.0.0'
}


ImageBackground


一个可以使用图片当做背景的容器,相当于以前的 div + 背景图片


import React from 'react'
import { Text, ImageBackground } from 'react-native'

const App = () =>
<ImageBackground source={require('./assets/logo.png')} style={{ width: 200, height: 200 }}>
<Text>Inside</Text>
</ImageBackground>

export default App


TextInput输入框组件


可以通过 onChangeText 事件来获取输入框的值
语法:



  1. 组件

  2. 插值表达式

  3. 状态state

  4. 属性props

  5. 调试

  6. 事件

  7. 生命周期


import React from 'react'
import { TextInput } from 'react-native'

const handleChangeText = (text) => {
alert(text)
}

#onChangeText => 获取输入的值
const App = () => <TextInput onChangeText={ handleChangeText } />

export default App


花括号{}里面可以直接添加JS代码的


组件: 函数组件, 类组件


函数组件



  1. 没有state(通过hooks可以有)

  2. 没有生命周期(通过hooks可以有)

  3. 适合简单的场景


类组件



  1. 适合复杂的场景

  2. 有state

  3. 有生命周期


属性props (父子组件的传递)和插槽slot


import React from 'react'
import { View, Text } from 'react-native'

const App = () => (
<View>
<Text>==========</Text>
<Sub color="red">
<View><Text>1234</Text></View>
</Sub>
<Text>==========</Text>
</View>
)

// 子组件 props
const Sub = (props) =>
(<View><Text style={{ color: props.color }}>{ props.children }</Text></View>)

// 插槽类似于 vue中的slot
export default App



人懒,不想配图,都是自己的博客内容(干货),望能帮到大家




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

收起阅读 »

学习一下Electron,据说很简单

Electron怎么玩 真的很简单的,面向百度编程,找寻前辈的足迹,真的很容易的。😄 直接点,开整 首先安装Electron,但是有个坑 坑就是安装卡住了,没事有办法: npm config set registry=https://registry.npm....
继续阅读 »

Electron怎么玩


真的很简单的,面向百度编程,找寻前辈的足迹,真的很容易的。😄


直接点,开整


首先安装Electron,但是有个坑


坑就是安装卡住了,没事有办法:


npm config set registry=https://registry.npm.taobao.org/
npm config set ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/


第一行相信大家都做了。


第二行很关键,如果不设置的话,他会在最后卡住,一直在加载,也不知道搞什么呢。🤦‍


然后在项目的根目录下创建main.js

/* main.js */
const { app, BrowserWindow } = require('electron')
const path = require('path')
const ipc = require('electron').ipcMain
const http = require('http');
const qs = require("qs")
const os = require('os');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
let server;

const initServer = () => {
server = http.createServer(function (request, response) {
// 定义了一个post变量,用于暂存请求体的信息
let post = '';
// 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
//当有数据请求时触发
request.on('data', function (data) {
post += data;
});
// 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
request.on('end', function () {
//解析为post对象
post = JSON.parse(post);
//将对象转化为字符串
response.writeHead(200, { 'Content-Type': 'text-plain' });
response.end('{"status":200}\n');
mainWindow.webContents.send("flightdata", post)
});
}).listen(8124);
}


const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
fullscreen: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});

// and load the index.html of the app.
mainWindow.loadFile("./build/index.html");

// mainWindow.maximize()
mainWindow.removeMenu()
// mainWindow.webContents.openDevTools()
mainWindow.webContents.openDevTools({mode:'right'});
// Emitted when the window is closed.
mainWindow.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
};

const initApp = () => {
createWindow();
initServer();
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', initApp);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow();
}
});

这里面大部分逻辑你不用考虑,我以后的文章会讲到,而你只需要知道一点就行。

那就是我在这个mian中指定了一个静态网页,巧了!位置就在我们打包文件夹build下🤭。

// and load the index.html of the app.
mainWindow.loadFile("./build/index.html");

然后配置package.json

{
...
"main": "main.js",
"homepage": "./",
...
}

分析:

main:配置刚才我们创建的Electron的入口文件main.js homepage:如果不配置的话,就会。。,em~~~~就会。。算了贴代码吧

...
const publicUrlOrPath = getPublicUrlOrPath(
process.env.NODE_ENV === 'development',
require(resolveApp('package.json')).homepage,
process.env.PUBLIC_URL
);
...

这几句代码就说明webpack会通过package中配置的homepage来设置PUBLIC_URL,so,那么配置homepage就很有必要。
否则,会白屏的!!!


对了还有个大坑,一定注意


如果你用的是react-router提供的BrowserRouter,那你会蒙圈的,因为什么都不会显示,顶多有个你事先安排好的“404”页面,就好像在用浏览器直接访问地址为https://****/index.htmlhistory模式根本不起作用,我猜这是浏览器独门绝技,electron还没支持,我猜的,不一定对。


所以一定要用hash模式

<HashRouter getUserConfirmation={this.getConfirmation}>
...
</HashRouter>

最后我们再配置一下启动脚本

/* package.json */
"scripts": {
...
"electron": "electron ."
...
},

看下效果吧

结语

这么一来,“中用”的Moderate就初步集成了Electron,直接一行命令就能打包成一个pc和mac端都能用的应用,美滋滋,但请掘友们相信,这只是第一部分🤭,接下来还有很多东西要补上。


原文:https://juejin.cn/post/6977349336044666917 收起阅读 »

Vue基操会了,还有必要学React么?

React前言 很高兴你能来到这里学习React.js技术,这是本专栏的第一节,主要介绍一下小伙伴们常问的一些问题,虽然废话很多,但是还是建议你可以花几分钟看一下。 React简介 首先不能否认React.js是全球最火的前端框架(Facebook推出的前端框...
继续阅读 »

React前言


很高兴你能来到这里学习React.js技术,这是本专栏的第一节,主要介绍一下小伙伴们常问的一些问题,虽然废话很多,但是还是建议你可以花几分钟看一下。


React简介


首先不能否认React.js是全球最火的前端框架(Facebook推出的前端框架),国内的一二线互联网公司大部分都在使用React进行开发,比如阿里美团百度去哪儿网易知乎这样的一线互联网公司都把React作为前端主要技术栈。
React的社区也是非常强大的,随着React的普及也衍生出了更多有用的框架,比如ReactNative和React VR。React从13年开始推广,现在已经推出18.x.x版本,性能和易用度上,都有很大的提升。


React优点总结




  • 生态强大:现在没有哪个框架比React的生态体系好的,几乎所有开发需求都有成熟的解决方案。




  • 上手简单: 你甚至可以在几个小时内就可以上手React技术,但是他的知识很广,你可能需要更多的时间来完全驾驭它。




  • 社区强大:你可以很容易的找到志同道合的人一起学习,因为使用它的人真的是太多了。




React和Vue的对比


这是前端最火的两个框架,虽然说React是世界使用人数最多的框架,但是就在国内而言Vue的使用者很有可能超过React。两个框架都是非常优秀的,所以他们在技术和先进性上不相上下。


那个人而言在接到一个项目时,我是如何选择的那?React.js相对于Vue.js它的灵活性和协作性更好一点,所以我在处理复杂项目或公司核心项目时,React都是我的第一选择。而Vue.js有着丰富的API,实现起来更简单快速,所以当团队不大,沟通紧密时,我会选择Vue,因为它更快速更易用。(需要说明的是,其实Vue也完全胜任于大型项目,这要根据自己对框架的掌握程度来决定,以上只是站在我的知识程度基础上的个人总结)


我们将学到什么?


我们将学习所有 React 的基础概念,其中又分为三个部分:



  • 编写组件相关:包括 JSX 语法、Component、Props

  • 组件的交互:包括 State 和生命周期

  • 组件的渲染:包括列表和 Key、条件渲染

  • 和 DOM & HTML 相关:包括事件处理、表单。


前提条件


我们假设你熟系 HTML 和 JavaScript,但即使你是从其他编程语言转过来的,你也能看懂这篇教程。我们还假设你对一些编程语言的概念比较熟悉,比如函数、对象、数组,如果对类了解就更好了。


环境准备


首先准备 Node 开发环境,访问 Node 官方网站下载并安装。打开终端输入如下命令检测 Node 是否安装成功:


node -v # v10.16.0


npm -v # 6.9.0


注意


Windows 用户需要打开 cmd 工具,Mac 和 Linux 是终端。


如果上面的命令有输出且无报错,那么代表 Node 环境安装成功。接下来我们将使用 React 脚手架 -- Create React App(简称 CRA)来初始化项目,同时这也是官方推荐初始化 React 项目的最佳方式。


在终端中输入如下命令:



npx create-react-app my-todolist



等待命令运行完成,接着输入如下命令开启项目:



cd my-todolist && npm start



CRA 会自动开启项目并打开浏览器


🎉🎉🎉 恭喜你!成功创建了第一个 React 应用!


现在 CRA 初始化的项目里有很多无关的内容,为了开始接下来的学习,我们还需要做一点清理工作。首先在终端中按 ctrl + c 关闭刚刚运行的开发环境,然后在终端中依次输入如下的命令:


进入 src 目录

cd src


如果你在使用 Mac 或者 Linux:

rm -f *


或者,你在使用 Windows:

del *


然后,创建我们将学习用的 JS 文件

如果你在使用 Mac 或者 Linux:

touch index.js


或者,你在使用 Windows

type nul > index.js


最后,切回到项目目录文件夹下

cd ..
此时如果在终端项目目录下运行 npm start 会报错,因为我们的 index.js 还没有内容,我们在终端中使用 ctrl +c 关闭开发服务器,然后使用编辑器打开项目,在刚刚创建的 index.js 文件中加入如下代码:


import React from "react";
import ReactDOM from "react-dom";

class App extends React.Component {
render() {
return <div>Hello, World</div>;
}
}

ReactDOM.render(<App />, document.getElementById("root"));
复制代码

我们看到 index.js 里面的代码分为三个部分。


首先是一系列导包,我们导入了 react 包,并命名为 React,导入了 react-dom 包并命名为 ReactDOM。对于包含 React 组件(我们将在之后讲解)的文件都必须在文件开头导入 React。


然后我们定义了一个 React 组件,命名为 App,继承自 React.Component,组件的内容我们将会在后面进行讲解。


接着我们使用 ReactDOM 的 render 方法来渲染刚刚定义的 App 组件,render方法接收两个参数,第一个参数为我们的 React 根级组件,第二个参数接收一个 DOM 节点,代表我们将把和 React 应用挂载到这个 DOM 节点下,进而渲染到浏览器中。


注意


上面代码的三个部分中,第一部分和第三部分在整篇教程中是不会修改的,同时在编写任意 React 应用,这两个部分都是必须的。后面所有涉及到的代码修改都是关于第二部分代码的修改,或者是在第一部分到第三部分之间插入或删除代码。


JSX 语法


首先我们来看一下 React 引以为傲的特性之一 -- JSX。它允许我们在 JS 代码中使用 XML 语法来编写用户界面,使得我们可以充分的利用 JS 的强大特性来操作用户界面。


一个 React 组件的 render 方法中 return 的内容就为这个组件所将渲染的内容。比如我们现在的代码:


render() {
return <div>Hello, World</div>;
}


这里的 <div>Hello, World</div> 是一段 JSX 代码,它最终会被 Babel转译成下面这段 JS 代码:


React.createElement(
'div',
null,
'Hello, World'
)


React.createElement() 接收三个参数:



  • 第一个参数代表 JSX 元素标签。

  • 第二个参数代表这个 JSX 元素接收的属性,它是一个对象,这里因为我们的 div 没有接收任何属性,所以它是 null。

  • 第三个参数代表 JSX 元素包裹的内容。


React.createElement() 会对参数做一些检查确保你写的代码不会产生 BUG,它最终会创建一个类似下面的对象:


{
type: 'div',
props: {
children: 'Hello, World'
}
};


这些对象被称之为 “React Element”。你可以认为它们描述了你想要在屏幕上看到的内容。React 将会接收这些对象,使用它们来构建 DOM,并且对它们进行更新。


App 组件最终返回这段 JSX 代码,所以我们使用 ReactDOM 的 render 方法渲染 App 组件,最终显示在屏幕上的就是 Hello, World" 内容。


JSX 作为变量使用


因为 JSX 最终会被编译成一个 JS 对象,所以我们可以把它当做一个 JS 对象使用,它享有和一个 JS 对象同等的地位,比如可以将其赋值给一个变量,我们修改上面代码中的 render 方法如下:


render() {
const element = <div>Hello, World</div>;
return element;
}


保存代码,我们发现浏览器中渲染的内容和我们之前类似。


在 JSX 中使用变量


我们可以使用大括号 {} 在 JSX 中动态的插入变量值,比如我们修改 render 方法如下:


render() {
const content = "World";
const element = <div>Hello, {content}</div>;
return element;
}


JSX 中使用 JSX


我们可以在 JSX 中再包含 JSX,这样我们编写任意层次的 HTML 结构:


render() {
const element = <li>Hello, World</li>
return (
<div>
<ul>
{element}
</ul>
</div>
)
}


JSX 中添加节点属性
我们可以像在 HTML 中一样,给元素标签加上属性,只不过我们需要遵守驼峰式命名法则,比如在 HTML 上的属性 data-index 在 JSX 节点上要写成 dataIndex。


const element = <div dataIndex="0">Hello, World</div>;


注意


在 JSX 中所有的属性都要更换成驼峰式命名,比如 onclick 要改成 onClick,唯一比较特殊的就是 class,因为在 JS 中 class 是保留字,我们要把 class 改成 className 。


const element = <div className="app">Hello, World</div>;


实战


在编辑器中打开 src/index.js ,对 App 组件做如下改变:


class App extends React.Component {
render() {
const todoList = ["给npy的前端秘籍", "fyj", "天天的小迷弟", "仰望毛毛大佬"];
return (
<ul>
<li>Hello, {todoList[0]}</li>
<li>Hello, {todoList[1]}</li>
<li>Hello, {todoList[2]}</li>
<li>Hello, {todoList[3]}</li>
</ul>
);
}
}


可以看到,我们使用 const 定义了一个 todoList 数组常量,并且在 JSX 中使用 {} 进行动态插值,插入了数组的四个元素。


提示


无需关闭刚才使用 npm start 开启的开发服务器,修改代码后,浏览器中的内容将会自动刷新!


你可能注意到了我们手动获取了数组的四个值,然后逐一的用 {} 语法插入到 JSX 中并最终渲染,这样做还比较原始,我们将在后面列表和 Key小节中简化这种写法。


在这一小节中,我们了解了 JSX 的概念,并且实践了相关的知识。我们还提出了组件的概念,但是并没有深入讲解它,在下一小节中我们将详细地讲解组件的知识。


总结


专栏第一篇与大家一起学习了React基本知识、后续还会有更精彩的哇、一起加油哇~



作者:给npy的前端秘籍
链接:https://juejin.cn/post/6974651532637634568

收起阅读 »

React 毁了 Web 开发(转载)

本文并不是为了引发争论或者让大家非要争一个好坏。我仔细阅读该文章之后,发现里面提出的问题是我们常常忽视并且对于构建可持续项目发展的非常重要的问题。 不论是小兵还是leader都应该持续的去注重这些东西。 以下是正文翻译:原文来自:medium.com/buil...
继续阅读 »

本文并不是为了引发争论或者让大家非要争一个好坏。我仔细阅读该文章之后,发现里面提出的问题是我们常常忽视并且对于构建可持续项目发展的非常重要的问题。


不论是小兵还是leader都应该持续的去注重这些东西。


以下是正文翻译:原文来自:medium.com/building-pr…


以下为译文:


React 是一个很好的库,对于 Web 开发很重要,因为它引入了声明式与反应式模板,这在当时是每个人都需要的范式转变。当时(也就是 6~7 年前),我们面临着需要的范式转变的问题,而 React 很好地解决了这个问题。


另外提一句,在 React 之前,Ember 也解决了同样的问题。然而,它的性能并不那么好,而且该框架规定了太多东西,远不如 React。



然而,React 在开始流行之后,发展变得一团糟。React 社区中开启了一种新趋势,一切都围绕着炒作、新奇和创造新范式的转变。每隔几个月就会涌现一些新的库,为我们应该如何编写 React Web 应用程序设定新标准,同时还会解决大部分已经解决的问题。


下面,我们以 " 状态管理 " 为例来说明。由于 React 缺少传统的依赖注入系统(DI 是通过组件组合实现的),所以社区不得不自己解决这个问题。然而,后来就变成了一遍又一遍地解决这个问题,每年都会带来一套新的标准。



React 只是一个渲染引擎,在常见的 Web 应用程序中,你需要使用很多库来构建项目的框架,例如数据层、状态管理、路由、资产捆绑器等。


React 背后的生态系统给了你太多这样的选择,而这个技术栈也因此而变得支离破碎,并引发了著名的 "Javascript 疲劳 "。


此外,还涌现了一种趋势:" 框架比较热潮 "。各个 JS 框架之间经常会展开渲染速度以及内存占用等属性的比较。其实,这些因素在大多数情况下根本无关紧要,因为应用的速度缓慢并不是由于 JS 框架的速度过慢而引起的,而是因为糟糕的代码。


然而,就像世界上所有的趋势一样,这个趋势有点过,甚至危及了新一代的 Web 开发人员。我就在想,为什么一个库能成为 Web 开发人员简历中最耀眼的技术?更糟糕的是,它甚至算不上一个库,只不过是库中的一个模块。人们常常将 React hook 视为一项 " 技术 ",甚至可以与代码重构或代码审查等实际技术相提并论。


认真地说,我们什么时候才能停止吹捧这种技术?


比如说,你为什么不告诉我,你知道:


如何编写简单易读的代码


不要向我炫耀你掌握了某个 GitHub 上获得星星数最多的库;而是给我展示一两个优秀的代码片段。


如何管理状态


不要讨论某个流行的状态管理库,而是告诉我为什么 " 数据应该下降而动作应该上升 "。或者说,为什么应该在创建的地方修改状态,而不是组件层次结构中更深的地方。


如何测试代码


不要告诉我你知道 Jest 或 QUnit,而是解释一下为什么很难自动化端到端的测试,以及为什么最低程度的渲染测试只需付出 10% 的努力,却能带来 90% 的好处。


如何发布代码


不要告诉我你使用 CI/CD(因为如今每个项目里的成员都不止一个人),而是解释为什么部署和发布应该分离,这样新功能就不会影响到已有功能,而且还可以远程启动新功能。


如何编写可审查的代码


不要说你是一名 " 团队成员 ",而是告诉我代码审查对审查者来说同样困难,而且你知道如何优化 PR 才能提高可读性和清晰度。


如何建立稳固的项目标准


除非团队中只有你一个人,否则你就必须遵守项目中的标准和惯例。你应该告诉我命名很难,而且变量的范围越广,投入到命名中的时间就应该越多。


如何审核别人的代码


因为代码审查可确保产品质量、减少 bug 和技术债务、共同建立团队知识等等,但前提是将代码审核贯彻到底。代码审查不应该只是自上而下的活动。对于经验不足的团队成员来说,这是一个很好的学习机制。


如何在 JS 框架中找到自己的方式


这与 GitHub 上的星星数量无关,你应该学习如今大多数 JS 框架都拥有的共同原则。了解其他框架的优缺点可以让你更好地了解自己选择的框架。


如何建立最小化可行产品


技术只是制造产品的工具,而不是流程。与其将时间浪费在技术争论上,还不如花点时间优化流程。


如何优化:不要太早,也不要太晚


因为在大多数情况下根本不需要优化。


如何结对编程


因为结对编程与代码审查一样,这是最重要的共享知识和建立团队凝聚力的实践。而且也很有意思!


如何持续重构


因为每个项目都有技术债务,你应该停止抱怨,并开始重构。每次开发新功能之前都应该进行小型代码重构。大规模的重构和重写永远不会有好结果。


以上就是我认为 React 毁了 Web 开发的原因。很多人对这一说法很感兴趣,并热切地加入了辩论。


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

收起阅读 »

WebKit的使用

Web view 用于加载和显示丰富的网络内容。例如,嵌入 HTML 和网站。Mail app 使用 web view 显示邮件中的 HTML 内容。iOS 8 和 macOS 10.10 中引入了WebKit framework,用以取代UIWebView和...
继续阅读 »

Web view 用于加载和显示丰富的网络内容。例如,嵌入 HTML 和网站。Mail app 使用 web view 显示邮件中的 HTML 内容。


iOS 8 和 macOS 10.10 中引入了WebKit framework,用以取代UIWebView和WebView。同时,在两个平台上提供同一API。与UIWebView相比,WebKit 有以下优势:使用了与 Safari 一样的 JavaScript engine,在运行脚本前,将脚本编译为机器代码,速度更快;支持多进程架构,Web 内容在单独的线程中运行,WKWebView崩溃不会影响 app运行;能够以60fps滑动。另外,在 iOS 12 和 macOS 10.14中,UIWebView和WebView已经被正式弃用。

WebKit framework 提供了多个类和协议,用于在窗口中显示网络内容,并实现类似浏览器功能。例如,点击链接时显示链接内容,维护前进、后退列表,维护最近访问列表。加载网页内容时,异步从 HTTP 服务器请求内容,其响应 (response) 可能以增量、随机顺序到达,也可能因网络原因部分到达,而 WebKit 极大简化了这些过程。WebKit 框架还简化了显示各种 MIME 类型内容过程,以及管理视图中各元素滚动条。

WebKit 框架中的方法、函数只能在主线程或主队列中调用。

1. WKWebView
WKWebView是 WebKit framework 的核心。在 app 内使用WKWebView插入网络内容步骤如下:

1、创建WKWebView对象。
2、将WKWebView设置为要显示的视图。
3、向WKWebView发送加载 Web 内容的请求。

使用initWithFrame:configuration:方法创建WKWebView,使用loadHTMLString:baseURL:加载本地HTML文件,或使用loadRequest:方法加载网络内容。如下:

// Local HTMLs
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.webConfiguration];
self.view = webView;
NSURL *htmlURL = [[NSBundle mainBundle] URLForResource:@"WKWebView - NSHipster" withExtension:@"htm"];
NSURL *baseURL = [htmlURL URLByDeletingLastPathComponent];
NSString *htmlString = [NSString stringWithContentsOfURL:htmlURL
encoding:NSUTF8StringEncoding
error:NULL];
[webView loadHTMLString:htmlString baseURL:baseURL];

// Web Content
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:self.webConfiguration];
self.view = webView;
NSURL *myURL = [NSURL URLWithString:@"https://github.com/pro648/tips/wiki"];
NSURLRequest *request = [NSURLRequest requestWithURL:myURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30];
[webView loadRequest:request];

本地HTML文件可以在demo源码中获取:https://github.com/pro648/BasicDemos-iOS/tree/master/WebKit

如图所示:



设置allowsBackForwardNavigationGestures属性可以开启、关闭横向滑动触发前进、后退导航功能:

self.webView.allowsBackForwardNavigationGestures = YES;

1.1 KVO

WKWebView中的title、URL、estimatedProgress、hasOnlySecureContent和loading

属性支持键值观察,可以通过添加观察者,获得当前网页标题、加载进度等。

根据文档serverTrust属性也支持KVO,但截至目前,在iOS 12.1 (16B91)中使用观察者观察该属性,运行时会抛出this class is not key value coding-compliant for the key serverTrust的异常。

将网页标题显示出来可以帮助用户了解当前所在位置,显示当前导航进度能够能够让用户感受到加载速度,另外,还可以观察hasOnlySecureContent查看当前网页所有资源是否均通过加密连接传输。在viewDidLoad中添加以下代码:

[self.webView addObserver:self forKeyPath:@"hasOnlySecureContent" options:NSKeyValueObservingOptionNew context:webViewContext];
[self.webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:webViewContext];
[self.webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:webViewContext];

实现observerValueForKeyPath:ofObject:change:context:方法,在观察到值变化时进行对应操作:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"hasOnlySecureContent"]) {
BOOL onlySecureContent = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
NSLog(@"onlySecureContent:%@",onlySecureContent ? @"YES" : @"NO");
} else if ([keyPath isEqualToString:@"title"]) {
self.navigationItem.title = change[NSKeyValueChangeNewKey];
} else if ([keyPath isEqualToString:@"estimatedProgress"]) {
self.progressView.hidden = [change[NSKeyValueChangeNewKey] isEqualToNumber:@1];

CGFloat progress = [change[NSKeyValueChangeNewKey] floatValue];
self.progressView.progress = progress;
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

如果你对键值观察、键值编码还不熟悉,可以查看我的另一篇文章:KVC和KVO学习笔记

运行demo,如下所示:


控制台会输出如下内容:

onlySecureContent:YES

WKWebView调用reload、stopLoading、goBack、goForward可以实现刷新、返回、前进等功能:

- (void)refreshButtonTapped:(id)sender {
[self.webView reload];
}

- (void)stopLoadingButtonTapped:(id)sender {
[self.webView stopLoading];
}

- (IBAction)backButtonTapped:(id)sender {
[self.webView goBack];
}

- (IBAction)forwardButtonTapped:(id)sender {
[self.webView goForward];
}

还可以通过观察loading属性,在视图加载完成时,更新后退、前进按钮状态:

- (void)viewDidLoad {
...

[self.webView addObserver:self forKeyPath:@"loading" options:NSKeyValueObservingOptionNew context:webViewContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
...
if (context == webViewContext && [keyPath isEqualToString:@"loading"]) {
BOOL loading = [change[NSKeyValueChangeNewKey] boolValue];
// 加载完成后,右侧为刷新按钮;加载过程中,右侧为暂停按钮。
self.navigationItem.rightBarButtonItem = loading ? self.stopLoadingButton : self.refreshButton;

self.backButton.enabled = self.webView.canGoBack;
self.forwardButton.enabled = self.webView.canGoForward;
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

1.2 截取网页视图

在iOS 11和macOS High Sierra中,WebKit framework增加了takeSnapshotWithConfiguration:completionHandler: API用于截取网页视图。截取网页可见部分视图方法如下:

- (IBAction)takeSnapShot:(UIBarButtonItem *)sender {
WKSnapshotConfiguration *shotConfiguration = [[WKSnapshotConfiguration alloc] init];
shotConfiguration.rect = CGRectMake(0, 0, self.webView.bounds.size.width, self.webView.bounds.size.height);

[self.webView takeSnapshotWithConfiguration:shotConfiguration
completionHandler:^(UIImage * _Nullable snapshotImage, NSError * _Nullable error) {
// 保存截图至相册,需要在info.plist中添加NSPhotoLibraryAddUsageDescription key和描述。
UIImageWriteToSavedPhotosAlbum(snapshotImage, NULL, NULL, NULL);
}];
}

此前,截取网页视图需要结合图层和graphics context。现在,只需要调用单一API。

1.3 执行JavaScript

可以使用evaluateJavaScript:completionHandler:方法触发web view JavaScript。下面方法触发输出web view userAgent:

[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable userAgent, NSError * _Nullable error) {
NSLog(@"%@",userAgent);
}];

在iOS 12.1.2 (16C101) 中,输出如下:

Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16C101

2. WKWebViewConfiguration

WKWebViewConfiguration是用于初始化Web视图属性的集合。通过WKWebViewConfiguration类,可以设置网页渲染速度,视频是否自动播放,HTML5 视频是否一帧一帧播放,如何与本地代码通信等。

WKWebViewConfiguration属性有偏好设置preference、线程池processPool和用户内容控制器userContentController等。

Web view 初始化时才需要WKWebViewConfiguration对象,WKWebView创建后无法修改其configuration。多个WKWebView可以使用同一个configuration。


例如,设置网页中最小字体为30,自动检测电话号码:

- (WKWebViewConfiguration *)webConfiguration {
if (!_webConfiguration) {
_webConfiguration = [[WKWebViewConfiguration alloc] init];

// 偏好设置 设置最小字体
WKPreferences *preferences = [[WKPreferences alloc] init];
preferences.minimumFontSize = 30;
_webConfiguration.preferences = preferences;

// 识别网页中的电话号码
_webConfiguration.dataDetectorTypes = WKDataDetectorTypePhoneNumber;

// Web视图内容完全加载到内存之前,禁止呈现。
_webConfiguration.suppressesIncrementalRendering = YES;
}
return _webConfiguration;
}

suppressesIncrementalRendering属性是布尔值,决定Web视图内容在完全加载到内存前是否显示,默认为NO,即边加载边显示。例如,Web视图中有文字和图片,会先显示文字后显示图片。

3. Scripts

用户脚本 (User Scripts) 是文档开始加载或加载完成后注入 Web 页面的 JS。User Scripts非常强大,其能够通过客户端设置网页,允许注入事件监听器,甚至可以注入脚本,这些脚本又可以回调 native app 。

3.1 WKUserScript

WKUserScript对象表示可以注入网页的脚本。initWithSource:injectionTime:forMainFrameOnly:方法返回可以添加到userContentController控制器的脚本。其中,source 参数为 script 源码;injectionTime为WKUserScriptInjectionTimeAtDocumentStart、WKUserScriptInjectionTimeAtDocumentEnd,

其参数如下:

source: script 源码。
injectionTime: user script注入网页时间,为WKUserScriptInjectionTime枚举常量。WKUserScriptInjectionTimeAtDocumentStart在创建文档元素之后,加载任何其他内容之前注入。WKUserScriptInjectionTimeAtDocumentEnd在加载文档后,但在加载其他子资源之前注入。
forMainFrameOnly: 布尔值,YES时只注入main frame,NO时注入所有 frame。
下面代码将隐藏 Wikipedia toc、mw-panel 脚本注入网页,同时使用 JS 提取网页 toc 表格内容:

// 隐藏wikipedia左边缘和contents表格
NSURL *hideTableOfContentsScriptURL = [[NSBundle mainBundle] URLForResource:@"hide" withExtension:@"js"];
NSString *hideTableOfContentsScriptString = [NSString stringWithContentsOfURL:hideTableOfContentsScriptURL
encoding:NSUTF8StringEncoding error:NULL];
WKUserScript *hideTableOfContentsScript = [[WKUserScript alloc] initWithSource:hideTableOfContentsScriptString
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];

// 获取contents表格内容
NSString *fetchTableOfContentsScriptString = [NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"fetch" withExtension:@"js"] encoding:NSUTF8StringEncoding error:NULL];
WKUserScript *fetchTableOfContentsScript = [[WKUserScript alloc] initWithSource:fetchTableOfContentsScriptString
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
forMainFrameOnly:YES];

本文中的 js 和 HTML 均可通过文章底部源码链接获取。要使用 JavaScript 提取内容的网页为:https://en.wikipedia.org/w/index.php?title=San_Francisco&mobileaction=toggle_view_desktop

3.2 WKUserContentController
WKUserContentController对象为 JavaScript 提供了发送消息至 native app,将 user scripts 注入 Web 视图方法。

将 user script 添加到userContentController才可以注入网页中:

WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addUserScript:hideTableOfContentsScript];
[userContentController addUserScript:fetchTableOfContentsScript];

要监听 JavaScript 消息,需要先注册要监听消息名称。添加监听事件方法为addScriptMessageHandler:name:,参数如下:

scriptMessageHandler: 处理监听消息,该类需要遵守WKMessageHandler协议。
name: 要监听消息名称。
使用该方法添加监听事件后,JavaScript 的 window.webkit.messageHandlers.name.postMessage(messageBody) 函数将被定义在使用了该userContentController网页视图的所有frame。

监听fetch.js中 didFetchTableOfContents 消息:

[userContentController addScriptMessageHandler:self name:@"didFetchTableOfContents"];

// 最后,将userContentController添加到WKWebViewConfiguration
_webConfiguration.userContentController = userContentController;

3.3 WKScriptMessageHandler

监听 script message 的类必须遵守WKMessageHandler协议,实现该协议唯一且必须实现的userContentController:didReceiveScriptMessage:方法。Webpage 接收到脚本消息时会调用该方法。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"didFetchTableOfContents"]) {
id body = message.body;
if ([body isKindOfClass:NSArray.class]) {
NSLog(@"messageBody:%@",body);
}
}
}

如下所示:


JavaScript 消息是WKScriptMessage对象,该对象属性如下:

body:消息内容,可以是NSNumber、NSString、NSDate、NSArray、NSDictionary、NSNull类型。
frameInfo:发送该消息的frame。
name:接收消息对象名称。
webView:发送该消息的网页视图。
最终,我们成功的将事件从 iOS 转发到 JavaScript,并将 JavaScript 转发回 iOS。

4. WKNavigationDelegate

用户点击链接,使用前进、后退手势,JavaScript 代码(例如,window.location = ' https://github.com/pro648 '),使用代码调用loadRequest:等均会让网页加载内容,即action引起网页加载;随后,web view 会向服务器发送request,接收response,可能会是positive response,也可能请求失败;之后接收数据。我们的应用可以在action后、request前,或者response后、data前自定义网页加载,决定继续加载,或取消加载。


WKNavigationDelegate协议内方法可以自定义Web视图接收、加载和完成导航请求过程的行为。

首先,声明遵守WKNavigationDelegate协议:

@interface ViewController () <WKNavigationDelegate>

其次,指定遵守WKNavigationDelegate协议的类为 web view 代理:

self.webView.navigationDelegate = self;

最后,根据需要实现所需WKNavigationDelegate方法。

webView:decidePolicyForNavigationAction:decisionHandler:方法在action后响应,webView:decidePolicyForNavigationResponse:decisionHandler:方法在response后响应。

根据前面的配置,WKWebView会自动识别网页中电话号码。截至目前,电话号码只能被识别,无法点击。可以通过实现webView:decidePolicyForNavigationAction:decisionHandler:方法,调用系统Phone app拨打电话

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (navigationAction.navigationType == WKNavigationTypeLinkActivated && [navigationAction.request.URL.scheme isEqualToString:@"tel"]) {
[UIApplication.sharedApplication openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) {
NSLog(@"Successfully open url:%@",navigationAction.request.URL);
}];
decisionHandler(WKNavigationActionPolicyCancel);
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}

实现了该方法后,必须调用decisionHandler块。该块参数为WKNavigationAction枚举常量。WKNavigationActionPolicyCancel取消导航,WKNavigationActionPolicyAllow继续导航。

WKNavigationAction对象包含引起本次导航的信息,用于决定是否允许本次导航。其属性如下:

request:本次导航的request。
sourceFrame:WKFrameInfo类型,请求本次导航frame信息。
targetFrame:目标frame。如果导航至新窗口,则targetFrame为nil。
navigationType:WKNavigationType枚举类型,为以下常量:
WKNavigationTypeLinkActivated:用户点击href链接。
WKNavigationTypeFormSubmitted:提交表格。
WKNavigationTypeBackForward:请求前进、后退列表中item。
WKNavigationTypeReload:刷新网页。
WKNavigationTypeFormResubmitted:因后退、前进、刷新等重新提交表格。
WKNavigationTypeOther:其他原因。
WKFrameInfo对象包含了一个网页中的frame信息,其只是一个描述瞬时状态 (transient) 的纯数据 (data-only) 对象,不能在多次消息调用中唯一标志某个frame。

如果需要在response后操作导航,需要实现webView:decidePolicyForNavigationResponse:decisionHandler:方法。WKNavigationResponse对象包含navigation response信息,用于决定是否接收response。其属性如下:

canShowMIMEType:布尔类型值,指示WebKit是否显示MIME类型内容。
forMainFrame:布尔类型值,指示即将导航至的frame是否为main frame。
response:NSURLResponse类型。
实现了该方法后,必须调用decisionHandler块,否则会在运行时抛出异常。decisionHandler块参数为WKNavigationResponsePolicy枚举类型。WKNavigationResponseCancel取消导航,WKNavigationResponseAllow继续导航。

Navigation action 和 navigation response 既可以在处理完毕后立即调用decisionHandler,也可以异步调用。

5. WKUIDelegate

WKWebView与 Safari 类似,尽管前者在一个窗口显示内容。如果需要打开多个窗口、监控打开、关闭窗口,修改用户点击元素时显示哪些选项,需要使用WKUIDelegate协议。

首先,声明遵守WKUIDelegate协议:

@interface ViewController () <WKUIDelegate>

其次,指定遵守WKUIDelegate协议的类为 web view 代理:

self.webView.uiDelegate = self;

最后,根据需要实现WKUIDelegate协议内方法。

5.1 新窗口打开

如何响应 JavaScript 的打开新窗口函数、或target="_blank"标签?有以下三种方法:

创建新的WKWebView,并在新的页面打开。
在 Safari 浏览器打开。
捕捉 JS ,在同一个WKWebView加载。
当 URL 为 mail、tel、sms 和 iTunes链接时交由系统处理。此时,系统会交由对应 app 处理。其他情况在当前 web view 加载。

5.2 响应 JavaScript 弹窗
在响应 JavaScript 时,可以通过WKUIDelegate协议使用 native UI呈现,有以下三种方法:

1、webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler::为了用户安全,在该方法的实现中需要需要标志出提供当前内容的网址,最为简便的方法便是frame.request.URL.host,响应面板应只包含一个OK按钮。当 alert panel 消失后,调用completionHandler。
2、webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler::为了用户安全,在该方法的实现中需要需要标志出提供当前内容的网址,最为简便的方法便是frame.request.URL.host,响应面板包括两个按钮,一般为OK和Cancel。当 alert panel 消失后,调用completionHandler。如果用户点击的是OK按钮,为completionHandler传入YES;如果用户点击的是Cancel按钮,为completionHandler传入NO。
3、webView:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler::为了用户安全,在该方法的实现中需要需要标志出提供当前内容的网址,最为简便的方法便是frame.request.URL.host,响应面板包括两个按钮(一个OK按钮,一个Cancel按钮)和一个输入框。当面板消失时调用completionHandler,如果用户点击的是OK按钮,传入文本框文本;否则,传入nil。

例如,输入账号、密码前点击登陆按钮,大部分网页会弹出警告框。在 JavaScript 中,会弹出 alert 或 confirm box。

JavaScript alert 会堵塞当前进程,调用completionHandler后 JavaScript 才会继续执行。

6. WKURLSchemeHandler

UIWebView支持自定义NSURLProtocol协议。如果想要加载自定义 URL 内容,可以通过创建、注册NSURLProtocol子类实现。此后,任何调用自定义 scheme (例如,hello world://) 的方法,都会调用NSURLProtocol子类,在NSURLProtocol子类处理自定义 scheme ,这将非常实用。例如,在 book 中加载图片、视频等。

WKWebView不支持NSURLProtocol协议,因此不能加载自定义 URL Scheme。在 iOS 11 中,Apple 为 WebKit framework 增加了WKURLSchemeHandler协议,用于加载自定义 URL Scheme。

WebKit遇到无法识别的 URL时,会调用WKURLSchemeHandler协议。该协议包括以下两个必须实现的方法:

webView:startURLSchemeTask::加载资源时调用。
webView:stopURLSchemeTask::WebKit 调用该方法以终止 (stop) 任务。调用该方法后,不得调用WKURLSchemeTask协议的任何方法,否则会抛出异常。
使用WKURLSchemeHandler协议处理完毕任务后,调用WKURLSchemeTask协议内方法加载资源。WKURLSchemeTask协议包括request属性,该属性为NSURLRequest类型对象。还包含以下方法:

didReceiveResponse::设置当前任务的 response。每个 task 至少调用一次该方法。如果尝试在任务终止或完成后调用该方法,则会抛出异常。
didReceiveData::设置接收到的数据。当接收到任务最后的 response 后,使用该方法发送数据。每次调用该方法时,新数据会拼接到先前收到的数据中。如果尝试在发送 response 前,或任务完成、终止后调用该方法,则会引发异常。
didFinish:将任务标记为成功完成。如果尝试在发送 response 前,或将已完成、终止的任务标记为完成,则会引发异常。
didFailWithError::将任务标记为失败。如果尝试将已完成、失败,终止的任务标记为失败,则会引发异常。
在WKURLSchemeHandler协议方法内,可以获取到请求的request。因此,可以提取 URL 中任何内容,并将数据转发给WKWebView进行加载。

下面的方法分别使用 url 、custom URL Scheme加载网络图片和相册图片:

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
NSURL *url = urlSchemeTask.request.URL;
if ([url.absoluteString containsString:@"custom-scheme"]) {
NSArray<NSURLQueryItem *> *queryItems = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES].queryItems;
for (NSURLQueryItem *item in queryItems) {

// example: custom-scheme://path?type=remote&url=https://placehold.it/120x120&text=image1
if ([item.name isEqualToString:@"type"] && [item.value isEqualToString:@"remote"]) {
for (NSURLQueryItem *queryParams in queryItems) {
if ([queryParams.name isEqualToString:@"url"]) {
NSString *path = queryParams.value;
path = [path stringByReplacingOccurrencesOfString:@"\\" withString:@""];

// 获取图片
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:path] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];
}];
[task resume];
}
}
} else if ([item.name isEqualToString:@"type"] && [item.value isEqualToString:@"photos"]) { // example: custom-scheme://path?type=photos
dispatch_async(dispatch_get_main_queue(), ^{
self.imagePicker = [[ImagePicker alloc] init];
[self.imagePicker showGallery:^(BOOL flag, NSURLResponse * _Nonnull response, NSData * _Nonnull data) {
if (flag) {
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];
} else {
NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL];
[urlSchemeTask didFailWithError:error];
}
}];
});
}
}
}
});
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
NSError *error = [NSError errorWithDomain:urlSchemeTask.request.URL.absoluteString code:0 userInfo:NULL];
[urlSchemeTask didFailWithError:error];
}

实现上述方法的类必须遵守WKURLSchemeHandler协议。另外,必须在WKWebView的配置中注册所支持的 URL Scheme:

// 添加要自定义的url scheme
[_webConfiguration setURLSchemeHandler:self forURLScheme:@"custom-scheme"];

运行如下:


总结

WebKit为 iOS 、macOS 开发人员提供了一套强大的开发工具,可以直接在 app 网页视图中操作 JavaScript,使用 user script 将 JavaScript 注入网页,使用WKScriptMessageHandler协议接收 JavaScript 消息。使用WKNavigationDelegate协议自定义网页导航,使用WKUIDelegate在网页上呈现 native UI,使用WKURLSchemeHandler加载自定义 URL Scheme 内容。

如果只是简单呈现网页视图,推荐使用 iOS 9 推出的SFSafariViewController,几行代码就可实现与 Safari 一样的体验。SFSafariViewController还提供了自动填充、欺诈网站监测等功能。

Demo名称:WebKit
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/WebKit

链接:https://www.jianshu.com/p/65c66f924d56

收起阅读 »

pthread多线程(C语言) + Socket

pthread多线程(C语言) + Socketpthread是使用使用C语言编写的多线程的API, 简称Pthreads ,是线程的POSIX标准,可以在Unix / Linux / Windows 等系统跨平台使用。在类Unix操作系统(Unix、Linu...
继续阅读 »

pthread多线程(C语言) + Socket

pthread是使用使用C语言编写的多线程的API, 简称Pthreads ,是线程的POSIX标准,可以在Unix / Linux / Windows 等系统跨平台使用。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。

GitHub项目FanSocket(纯C语言socket+线程队列)+其他demo客户端

1.线程创建

//子线程1
void test1(int *a){
printf("线程test1");
//修改自己的子线程系统释放,注释打开后,线程不能用pthread_join方法
//pthread_detach(pthread_self());
}
//子线程2
void test2(int *a){
printf("线程test2");
}

/*
int pthread_create(pthread_t * thread, //新线程标识符
pthread_attr_t * attr, //新线程的运行属性
void * (*start_routine)(void *), //线程将会执行的函数
void * arg);//执行函数的传入参数,可以为结构体
*/

//创建线程方法一 (手动释放线程)
int a=10;
pthread_t pid;
pthread_create(&pid, NULL, (void *)test1, (void *)&a);


//线程退出或返回时,才执行回调,可以释放线程占用的堆栈资源(有串行的作用)
if(pthread_join(pid, NULL)==0){
//线程执行完成
printf("线程执行完成:%d\n",threadIndex);
if (message!=NULL) {
printf("线程执行完成了\n");
}
}


//创建线程方法二 (自动释放线程)
//设置线程属性
pthread_attr_t attr;
pthread_attr_init (&attr);
//线程默认是PTHREAD_CREATE_JOINABLE,需要pthread_join来释放线程的
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//线程并发
int rc=pthread_create(&pid, &attr, (void *)test2, (void *)a);
pthread_attr_destroy (&attr);
if (rc!=0) {
printf("创建线程失败\n");
return;
}

2.线程退出和其他

pthread_exit (tes1) //退出当前线程
pthread_main_np () // 获取主线程

//主线程和子线程
if(pthread_main_np()){
//main thread
}else{
//others thread
}

int pthread_cancel(pthread_t thread);//发送终止信号给thread线程,如果成功则返回0
int pthread_setcancelstate(int state, int *oldstate);//设置本线程对Cancel信号的反应
int pthread_setcanceltype(int type, int *oldtype);//设置本线程取消动作的执行时机
void pthread_testcancel(void);//检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回

3 线程互斥锁(量)与条件变量

3.1 互斥锁(量)

//静态创建
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//动态创建
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
//注销互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

//lock 和unlock要成对出现,不然会出现死锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//判断是否可以加锁,如果可以加锁并返回0,否则返回非0
int pthread_mutex_trylock(pthread_mutex_t *mutex);

3.2 条件变量

1、条件变量是利用线程间共享的全局变量进行同步的一种机制,
2、一个线程等待”条件变量的条件成立”而挂起;
3、另一个线程使”条件成立”(给出条件成立信号)。
4、为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

//静态创建
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
//动态创建
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//注销条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//条件等待,和超时等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime);
//开启条件,启动所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//开启一个等待信号量
int pthread_cond_signal(pthread_cond_t *cond);

4.线程同步:互斥锁(量)与条件变量(具体封装实现)

/*全局的队列互斥条件*/
extern pthread_cond_t fan_cond;
extern pthread_cond_t fan_cond_wait;
/*全局的队列互斥锁*/
extern pthread_mutex_t fan_mutex;
//extern pthread_mutex_t fan_mutex_wait;

extern int fan_thread_status;//0=等待 1=执行 -1=清空所有
extern int fan_thread_clean_status;//0=默认 1=清空所有

//开启线程等待 return=-2一定要处理
extern int fan_thread_start_wait(void);
//正常的超时后继续打开下一个信号量 return=-2一定要处理
int fan_thread_start_timedwait(int sec);
//启动线程,启动信号量
extern int fan_thread_start_signal(void);
//启动等待信号量
extern int fan_thread_start_signal_wait(void);
//暂停线程
extern int fan_thread_end_signal(void);
//初始化互斥锁
extern int fan_thread_queue_init(void);
//释放互斥锁信号量
extern int fan_thread_free(void);

//让队列里面全部执行完毕,而不是关闭线程;
extern int fan_thread_clean_queue(void);
//每次关闭清空后,等待1-2秒,要恢复状态,不然线程添加
extern int fan_thread_init_queue(void);
//设置线程的优先级,必须在子线程
extern int fan_thread_setpriority(int priority);

线程队列互斥,并且按入队顺序,一个一个按照外部条件,触发信号量,主要是等待队列,

/*全局的队列互斥条件*/
pthread_cond_t fan_cond=PTHREAD_COND_INITIALIZER;
pthread_cond_t fan_cond_wait=PTHREAD_COND_INITIALIZER;

/*全局的队列互斥锁*/
pthread_mutex_t fan_mutex = PTHREAD_MUTEX_INITIALIZER;
//pthread_mutex_t fan_mutex_wait = PTHREAD_MUTEX_INITIALIZER;
int fan_thread_status=1;//0=等待 1=执行
int fan_thread_clean_status;//0=默认 1=清空所有

//开启线程等待
int fan_thread_start_wait(void){
pthread_mutex_lock(&fan_mutex);
fan_thread_clean_status=0;
while (fan_thread_status==0) {
pthread_cond_wait(&fan_cond, &fan_mutex);
if (fan_thread_clean_status==1) {
break;
}
}
if (fan_thread_clean_status==1) {
pthread_mutex_unlock(&fan_mutex);
return -2;
}
if (fan_thread_status==1) {
fan_thread_status=0;
pthread_mutex_unlock(&fan_mutex);
}else{
pthread_mutex_unlock(&fan_mutex);
}
return 0;
}
//正常的超时后继续打开下一个信号量
int fan_thread_start_timedwait(int sec){
int rt=0;
pthread_mutex_lock(&fan_mutex);
struct timeval now;
struct timespec outtime;
gettimeofday(&now, NULL);
outtime.tv_sec = now.tv_sec + sec;
outtime.tv_nsec = now.tv_usec * 1000;

int result = pthread_cond_timedwait(&fan_cond_wait, &fan_mutex, &outtime);
if (result!=0) {
//线程等待超时
rt=-1;
}
if (fan_thread_clean_status==1) {
rt = -2;
}
pthread_mutex_unlock(&fan_mutex);
return rt;
}
//启动线程,启动信号量
int fan_thread_start_signal(void){
int rs=pthread_mutex_trylock(&fan_mutex);
if(rs!=0){
pthread_mutex_unlock(&fan_mutex);
}
fan_thread_status=1;
pthread_cond_signal(&fan_cond);
// pthread_cond_broadcast(&fan_cond);//全部线程
pthread_mutex_unlock(&fan_mutex);
return 0;
}
//开启等待时间的互斥信号量
int fan_thread_start_signal_wait(void){
int rs=pthread_mutex_trylock(&fan_mutex);
if(rs!=0){
pthread_mutex_unlock(&fan_mutex);
}
// fan_thread_status=1;
pthread_cond_signal(&fan_cond_wait);
// pthread_cond_broadcast(&fan_cond);//全部线程
pthread_mutex_unlock(&fan_mutex);
return 0;
}
//暂停下一个线程
int fan_thread_end_signal(void){
pthread_mutex_lock(&fan_mutex);
fan_thread_status=0;
pthread_cond_signal(&fan_cond);
pthread_mutex_unlock(&fan_mutex);
return 0;
}
//初始化互斥锁(动态创建)
int fan_thread_queue_init(void){
pthread_mutex_init(&fan_mutex, NULL);
pthread_cond_init(&fan_cond, NULL);
return 0;
}
//释放互斥锁和信号量
int fan_thread_free(void)
{
pthread_mutex_destroy(&fan_mutex);
pthread_cond_destroy(&fan_cond);
return 0;
}

//清空所有的队列
int fan_thread_clean_queue(void){
pthread_mutex_lock(&fan_mutex);
fan_thread_clean_status=1;
pthread_cond_broadcast(&fan_cond);
pthread_cond_broadcast(&fan_cond_wait);
pthread_mutex_unlock(&fan_mutex);
return 0;
}
//恢复队列
int fan_thread_init_queue(void){
pthread_mutex_lock(&fan_mutex);
fan_thread_clean_status=0;
fan_thread_status=1;
pthread_cond_signal(&fan_cond);
pthread_mutex_unlock(&fan_mutex);
return 0;
}
//设置线程的优先级,必须在子线程
int fan_thread_setpriority(int priority){
struct sched_param sched;
bzero((void*)&sched, sizeof(sched));
// const int priority1 = (sched_get_priority_max(SCHED_RR) + sched_get_priority_min(SCHED_RR)) / 2;
sched.sched_priority=priority;
//SCHED_OTHER(正常,非实时)SCHED_FIFO(实时,先进先出)SCHED_RR(实时、轮转法)
pthread_setschedparam(pthread_self(), SCHED_RR, &sched);
return 0;
}

5 其他线程方法

//return=0:线程存活。ESRCH:线程不存在。EINVAL:信号不合法。
int kill_ret=pthread_kill(pid, 0);//测试线程是否存在
printf("线程状态:%d\n",kill_ret);
if(kill_ret==0){
//关闭线程
pthread_cancel(pid);
}


pthread_equal(pid, pid1);//比较两个线程ID是否相同


//函数执行一次
pthread_once_t once = PTHREAD_ONCE_INIT;
pthread_once(&once, test1);

转自:https://www.jianshu.com/p/6fcd478635e2

收起阅读 »

FIL升级对矿工有哪些利好?现在是参与挖FIL币好时机

据财经媒体报道:FIL将在6月底进行系统升级,FIL升级对矿工有哪些利好?那对与刚了解FIL挖矿的朋友们现在正是参与的好时机+slf81818,为什么呢?一起来了解下:Filecoin’s v13 HyperDrive网络升级的重要意义在于:一:人类宝贵信息的...
继续阅读 »

据财经媒体报道:FIL将在6月底进行系统升级,FIL升级对矿工有哪些利好?那对与刚了解FIL挖矿的朋友们现在正是参与的好时机+slf81818,为什么呢?一起来了解下:

Filecoin’s v13 HyperDrive网络升级的重要意义在于:

一:人类宝贵信息的可验证存储容量将增长10—25倍。

二:GAS费大幅降低,无限期限接近于0。

三:质押费快速下降,新进场的投入成本明显降低,更有利于生态发展。

四:参与存储商激增,将实现更快的网络增长。

五:三大运营商之一(移动电信联通)智慧城市的数据将接入储存IPFS系统,进一步促进IPFS的应用落地,或将成为IPFS历史级重大标志性事件。

Filecoin V13版本的更新将是颠覆性的,也是突破共识的一次更新。其目的也不仅仅是降低Gas费,而是释放带宽,为Filecoin添加智能合约功能做准备。

目前FIL市场的具体情况具体分析,大多数人都在观望,主要还是带着想要一夜暴富的想法去炒币。这完全是两个概念,炒币没有哪个不伤筋动骨,这还是要轻的,可以考虑一下它的恐怖程度!为什么说矿工总是食物链的顶端?合理的投资方式是看其长期收益,不需要过多地去看目前的价格高低,手中的矿机每天都能产出 FIL,不用管它涨跌,相反,炒币就是你买了多少就有多少,性质不同。

例如,买一只鸡来给你每天生蛋,头七天价格比以前低,把鸡蛋存起来不卖,第八天它的价格达到了你想要的市场价就全部卖掉,与此相反,你直接买鸡蛋来倒买倒卖,风险成本是显而易见的。炒币看运气,屯币看心态,矿机相当于永动机。













现在币价低,加入挖矿成本也会很低,最重要的是其日产币并未降低,反而还在增加产量,这也大大缩短了回本周期,未来币价上涨,矿机也将随之涨价,人多挖矿效率肯定不如现在人少,回本周期更是大幅拉长,挖矿最大优势在于自身持币增多,有“粮”就能度过寒冬,躲过熊市就是迎来资产爆发的喜悦。感谢大家关注芳姐+slf81818,了解更多币圈最新资讯。

收起阅读 »

Android 布局打气筒 (一):玩转 LayoutInflater

前言很高兴遇见你~今天准备和大家分享的是 LayoutInflater,我给它取名:布局打气筒,很形象,其实就是根据英文翻译过来的😂。我们知道气球打气筒可以给气球打气从而改变它的形状。而布局打气筒的作用就是给我们的 Xml 布局打气让它变成一个个 View 对...
继续阅读 »

前言

很高兴遇见你~

今天准备和大家分享的是 LayoutInflater,我给它取名:布局打气筒,很形象,其实就是根据英文翻译过来的😂。我们知道气球打气筒可以给气球打气从而改变它的形状。而布局打气筒的作用就是给我们的 Xml 布局打气让它变成一个个 View 对象。在我们的日常工作中,经常会接触到他,因为只要你写了 Xml 布局,你就要使用 LayoutInflater,下面我们就来好好讲讲它。

注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析

一、基本使用

1、LayoutInflater 实例获取

1)、通过 LayoutInflater 的静态方法 from 获取

2)、通过系统服务 getSystemService 方法获取

3)、如果是在 Activity 或 Fragment 可直接获取到实例

//1、通过 LayoutInflater 的静态方法 from 获取
val layoutInflater: LayoutInflater = LayoutInflater.from(this)

//2、通过系统服务 getSystemService 方法获取
val layoutInflater: LayoutInflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater

//3、如果是在 Activity 或 Fragment 可直接获取到实例
layoutInflater //相当于调用 getLayoutInflater()

实际上,1 是 2 的简单写法,只是 Android 给我们做了一下封装。拿到 LayoutInflater 实例后,我们就可以调用它的 inflate 系列方法了,这几个方法是本篇文章的一个重点,如下:

image-20210622163719911

从 Xml 布局到创建 View 对象,这几个方法扮演着至关重要的作用,其中我们用的最多就是第一个和第三个重载方法,现在我们就来使用一下

二、例子

1、创建一个新项目,MainActivity 对应的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cons_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"/>

2、创建一个新的布局取名 item_main.xml,如下图:

image-20210622174620878

3、修改 MainActivity 中的代码

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val consMain = findViewById<ConstraintLayout>(R.id.cons_main)
val itemMain = layoutInflater.inflate(R.layout.item_main, null)
consMain.addView(itemMain)
}
}

上述代码我们使用了两个参数的 inflate 重载方法,第二个参数 root 传了一个 null ,然后把当前布局添加到 Activity 中,运行看下效果:

image-20210622175552693

啥情况?怎么和预想的不一样呢?我的背景颜色怎么不见了?把这个问题 1 先记着

接下来,我们修改一下 MainActivity 中的代码,如下:

val itemMain = layoutInflater.inflate(R.layout.item_main, consMain)
//等同下面这行代码
val itemMain = layoutInflater.inflate(R.layout.item_main, consMain,true)

实际上上面这句代码就相当于调用了三个参数的重载方法,且第三个参数为 true,我们看下它两个参数的源码:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

现在在运行看下结果:

image-20210622190018488

报错了,提示我们当前 child 已经有了一个父 View,你必须先调用父 View 的 removeView 方法移除当前 child 才行。是不是疑问更多了呢?把这个问题 2 也先记着

我们在修改一下 MainActivity 中的代码,如下:

val itemMain = layoutInflater.inflate(R.layout.item_main, consMain,false)

在运行看下结果:

image-20210622190835239

嗯,现在达到了我们预期的效果

现在回到上面那两个问题,分析发现是 LayoutInflater inflate 方法传了不同的参数导致的,那这些参数到底有什么玄乎的地方呢?接下来跟着我的脚步分析下源码,或许你就豁然开朗了

三、LayoutInflater inflate 系列方法源码分析

在分析源码之前,我们需要明白一些基础知识:

我们一般都会使用 layout_width 和 layout_height 来设置 View 的大小,实际上是要满足一个条件,那就是这个 View 必须存在于一个容器或布局中,否则没有意义,之后如果将 layout_width 设置成 match_parent 表示让 View 的宽度填充满布局,如果设置成 wrap_content 表示让 View 的宽度刚好可以包含其内容,如果设置成具体的数值则 View 的宽度会变成相应的数值。这也是为什么这两个属性叫作 layout_width 和layout_height,而不是 width 和 height 。

明白了上面这些知识,我们继续往下看

实际上,我们调用 LayoutInflater inflate 系列方法,最终都会走到上述截图的第 4 个重载方法,看下它的源码,仅贴出关键代码:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
//...
//获取布局 Xml 里面的属性集合
AttributeSet attrs = Xml.asAttributeSet(parser);
// 将传入的 root 赋值 给 result
View result = root;

// 创建根 View 赋值给 temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
//...
//如果传入的 root 不为空,通过 root 和布局属性生成布局参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果传入的 attachToRoot 为 false 则给当前创建的根 View 设置布局参数
temp.setLayoutParams(params);
}
}

//递归创建子 View 并添加到父布局中
rInflateChildren(parser, temp, attrs, true);

if (root != null && attachToRoot) {
//如果 root 不为空且 attachToRoot 为 true,添加当前创建的根 View 到 root
root.addView(temp, params);
}

if (root == null || !attachToRoot) {
//如果 root 为空或者 attachToRoot 为 false, 将当前创建的根 View 赋值给 result
result = temp;
}

//...
//返回当前 result
return result;
}
}

上述代码我们可以得到一些结论:

1、如果传入的 root 不为 null 且 attachToRoot 为 false,此时会给 Xml 布局生成的根 View 设置布局参数

注意:Xml 布局生成的根 View 并没有被添加到任何其他 View 中,此时根 View 的布局属性不会生效,但是我们给它设置了布局参数,那么它就会生效,只是没有被添加到任何其他 View 中

2、如果传入的 root 不为 null 且 attachToRoot 为 true,此时会将 Xml 布局生成的根 View 通过 addView 方法携带布局参数添加到 root 中

注意:此时 Xml 布局生成的根 View 已经被添加到其他 View 中,注意避免重复添加而报错

3、如果传入的 root 为 null ,此时会将 Xml 布局生成的根 View 对象直接返回

注意:此时 Xml 布局生成的根 View 既没有被添加到其他 View 中,也没有设置布局参数,那么它的布局参数将会失效

明白了上面这些知识点,我们在看下为啥为会出现之前那些问题

四、问题分析

1、问题 1

上述问题 1 实际上我们是调用了 LayoutInflater 两个参数的 inflate 重载方法:

inflate(@LayoutRes int resource, @Nullable ViewGroup root)

传入的实参: resouce 传入了一个 Xml 布局,root 传入了 null

根据我们上面源码得到的结论,当传入的 root 为 null ,此时会将 Xml 布局生成的根 View 对象直接返回

那么此时这个布局根 View 不在任何 View 中,因此它的布局属性失效了,但是 TextView 在一个布局中,它的布局属性会生效,因此就出现了上述截图中的效果

2、问题 2

上述问题 2 我们调用的还是 LayoutInflater 两个参数的构造方法

传入的实参: resouce 传入了一个 Xml 布局,root 传入了 consMain

实际又会调用 LayoutInflater 三个参数的 inflate 重载方法:

inflate(@LayoutRes int resource, @Nullable ViewGroup root,boolean attachToRoot)

此时传入实参变为:resouce 传入了一个 Xml 布局,root 传入了 consMain,attachToRoot 传入了 true

根据我们上面源码得到的结论:当传入的 root 不为 null 且 attachToRoot 为 true,此时会将 Xml 布局生成的根 View 通过 addView 方法携带布局参数添加到 root 中

此时我们在 MainActivity 中又重复调用了 addView 方法,因此就报那个错了。如果想不报错,把 MainActivity 中的那行 addView 去掉就可以了

3、预期效果

上述预期效果,我们调用的是 LayoutInflater 三个参数的 inflate 重载方法

传入的实参:resouce 传入了一个 Xml 布局,root 传入了 consMain,attachToRoot 传入了 false

根据我们上面源码得到的结论:当传入的 root 不为 null 且 attachToRoot 为 false,此时会给 Xml 布局生成的根 View 对象设置布局参数

此时根 View 的布局属性会生效,只不过没有被添加到任何 View 中,而又因为 MainActivity 中调用了 addView 方法,把当前根 View 添加了进去,所以达到了我们预期的效果

到这里,你是否明白了 LayoutInflater inflate 方法的应用了呢?

如果还有疑问,欢迎评论区给我提问,我们一起讨论

五、为啥 Activity 中布局根 View 的布局属性会生效?

看下面这张图:

注意:Android 版本号和应用主题会影响到 Activity 页面组成,这里以常见页面为例

image-20210622210219600

我们的页面中有一个顶级 View 叫 DecorView,DecorView 中包含一个竖直方向的 LinearLayout,LinearLayout 由两部分组成,第一部分是标题栏,第二部分是内容栏,内容栏是一个FrameLayout,我们在 Activity 中调用 setContentView 就是将 View 添加到这个FrameLayout 中。

看到这里你应该也明白了:Activity 中布局根 View 的布局属性之所以能生效,是因为 Android 会自动在布局文件的最外层再嵌套一个FrameLayout

六、总结

本篇文章重点内容:

1、 LayoutInflater inflate 方法参数的应用,记住下面这个规律:

  • 当传入的 root 不为 null 且 attachToRoot 为 false,此时会给 Xml 布局生成的根 View 设置布局参数
  • 当传入的 root 不为 null 且 attachToRoot 为 true,此时会将 Xml 布局生成的根 View 通过 addView 方法携带布局参数添加到 root 中
  • 当传入的 root 为 null ,此时会将 Xml 布局生成的根 View 对象直接返回

2、Activity 中布局根 View 的布局属性会生效是因为 Android 会自动在布局文件的最外层再嵌套一个 FrameLayout

收起阅读 »

通俗易懂的Android屏幕刷新机制

前言我们买手机的时候经常听说这个手机多少多少HZ刷新率。目前手机大多都是60HZ,现在有的手机都到144HZ的高刷新率了。这个刷新率指标是干什么的呢?屏幕又是如何将数据显示到Android手机屏幕上的呢?玩游戏时的卡顿是怎么形成的? 基于对这些问题的好奇,小研...
继续阅读 »

前言

我们买手机的时候经常听说这个手机多少多少HZ刷新率。目前手机大多都是60HZ,现在有的手机都到144HZ的高刷新率了。这个刷新率指标是干什么的呢?屏幕又是如何将数据显示到Android手机屏幕上的呢?玩游戏时的卡顿是怎么形成的? 基于对这些问题的好奇,小研究了一番,就有了以下这些内容:

Android屏幕刷新机制导图.png

相关基础概念

人眼视觉残留

当物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像1/24秒左右的图像,这种现象被称为视觉暂留现象,是人眼具有的一种性质。

这是因为:人眼观看物体时,成像于视网膜上,并由视神经输入人脑,感觉到物体的像。但当物体移去时,视神经对物体的印象不会立即消失,而要延续1/24秒左右的时间。

逐行扫描

显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点。

帧、帧率(数)、刷新率

在视频领域,是指每一张画面。

需要注意帧率和刷新率不是一个概念:

  • 帧率(frame rate)指的是显卡1秒钟渲染好并发送给显示器多少张画面。

  • 刷新率指的是显示器逐行扫描刷新的速度。以 60 Hz 刷新率的屏幕为例,就是1s会刷60帧,一帧需要1000 / 60 ,约等于16ms,这个速度快到普通人眼感受不到屏幕在扫描。

画面撕裂

画面撕裂的形成,简单点说就是显示器把两帧或两帧以上的数据同时显示在同一个画面的现象。就像这样:

图像撕裂.png

屏幕刷新频率是固定的,通常是60Hz。比如在60Hz的屏幕下,每16.6ms从Buffer取一帧数据并显示。理想情况下,GPU绘制完一帧,显示器显示一帧。

但现在显卡性能大幅提高,帧率太高出现画面撕裂。屏幕刷新频率是固定的,通常是60Hz,如果显卡的输出高于60fps,两者不同步,画面便会显示撕裂的效果。其实,帧率太低也是会出现画面撕裂。

所以背后的本质问题就是,当刷新率和帧率不一致就会出现,就很容易出现画面撕裂现象

拓展知识点:显卡与数据流动到显示屏过程

显卡主要负责把主机向显示器发出的显示信号转化为一般电器信号(数模转换),使得显示器能明白个人电脑在让它做什么。显卡的主要芯片叫“显示芯片”(Video chipset,也叫GPUVPU,图形处理器或视觉处理器),是显卡的主要处理单元。显卡上也有和电脑存储器相似的存储器,称为“显示存储器”,简称显存。

数据离开CPU到达显示屏,中间经历比较关键的步骤:

1.从总线进入GPU:将CPU送来的数据送到北桥(简单理解成连接显卡等高速设备的),再送到GPU里面进行处理

2.将芯片处理完的数据送到显存。

3.从显存读取出数据再送到随机读写存储,数模转换器进行数模转换的工作(但是如果是DVI接口类型的显卡,直接输出数字信号)

4.从DAC进入显示器:将转换完的模拟信号送到显示屏

所以显卡很关键的作用是起数据处理和数模转换。

那么等显示器显示完再去绘制下一帧数据不就没有这个问题了吗?

这么简单一想好像是没问题。但问题关键就出在图像绘制和屏幕读取这一帧数据使用的是一块Buffer。屏幕读取数据过程是无法确保这个Buffer不会被修改。由于屏幕是逐行扫描,它不会被打断仍然会继续上一行的位置扫描,当出现Buffer里有些数据根本没被显示器显示完就被重写了(即Buffer里的数据是来自不同帧的混合),这样就出现了画面撕裂的现象。

双缓存

针对上面的问题关键:图像绘制和屏幕读取这一帧数据使用的是一块Buffer

可以想到的一种解决方案是:不让它们使用同一块Buffer,用两块让它们各自为战不就好了,这么想的思路确实是对的。分析下这个具体过程:

当图像绘制和屏幕显示有各自的Buffer后,GPU将绘制完的一帧图像写入到后缓存(Back Buffer),显示器显示的时候只会去扫描前缓存的数据(Frame Buffer),在显示器未扫描完一帧前,前缓存区内数据不改变,屏幕就只会显示一帧的数据,避免了撕裂。

双缓存.png

但这样做的最关键一步是,什么时候去交换两块Buffer的数据?

Back Buffer准备完一帧数据就进行?这很明显是不可以的,这样就和只有一个缓存区的效果一样了,还是会出现撕裂现象。

根据逐行扫描的特性,当扫描完一个屏幕后,显示器会重新回到第一行进行下次的扫描,在这个间隙过程,屏幕没有在刷新,此时就是进行缓存区交换比较好的时机。

VBlank阶段和帧传递:

显示器扫描完一帧重新回到第一行的过程称为显示器的VBlank阶段。

缓存区交换被称为BufferSwap,帧传递。

Andrid屏幕刷新机制的演变

VSync

那是谁控制这个缓冲区交换时机,或者说专业点,什么时机进行帧传递呢?

这里就要提到VSync了,它翻译过来叫垂直同步,它会强制帧传递发生在显示器的VBlank阶段

需要注意的是:开启垂直同步后,就算显卡准备好了Back Buffer的数据,但显示器没有逐行扫描完前缓冲区的,就不允许发生帧传递。显卡就空载着,等待显示器扫描完毕后的VBlank阶段

这就解释了在玩游戏的时候,如果开启了垂直功能,游戏中显示的帧率一直处于一个帧率之下,这个显示帧率值就是屏幕刷新率。

那这个过程具体是怎么样的,真的就可以解决问题了?上面看着说的很有道理,但抽象到还是似懂非懂...

别急,下面就用几张图带你分析下具体的过程。

Jank

在下面的图中,你将会经常看到Jank一词语,它术语翻译,叫做卡顿。卡顿很容易理解了,比如我们在打游戏时,经常会遇到同一帧画面在那显示很久没有变化,这就是所谓的Jank

场景1

先看下最原始的,只有双缓冲,没有VSync影响下,它会发生什么:

vsync1.png

图中Display 为显示屏, VSync 仅仅指双缓冲的交换。

(1)Display显示第0帧,此时 CPU/GPU 渲染第1帧画面,并且在 Display 显示下一帧前完成。

(2)Display 正常渲染第一帧

(3)出于某种原因,如 CPU 资源被占用,系统没有及时处理第2帧数据,当 Display 显示下一帧时,由于数据没处理完,所以依然显示第1帧,即发生“Jank” ,

Jank术语翻译为卡顿,就是我们打游戏感受到的延迟。

上图出现的情况就是第2帧没有在显示前及时处理,导致屏幕多显示第一帧一次,导致后面的帧都延时了。根本原因是因为第2帧的数据没能在VBlank时(即本次完成到下次扫描开始前的时间间隙)完成。

上图可以看到的是由于CPU资源被抢,导致第2帧的数据处理时机太晚,假设在双缓存交换完成后,CPU资源可以立刻为处理第二帧所用,就可以处理完成该帧的数据(当前前提是该帧的处理数据不超过刷新一帧的时间),也就避免了Jank的出现。

场景2

在双缓冲下,有了VSync会怎么样呢?

vsync2.png

如图,当且仅当收到VSync通知(比如16ms触发一次),CPUGPU 立刻开始计算然后把数据写入BufferVSync同步信号的出现让绘制速度和屏幕刷新速度保持一致,使CPUGPU 充分利用了这16.6 ms的时间,减少了jank。

场景3

但是如果界面比较复杂,CPU/GPU处理时间真的超过16.6ms的话,就会发生:

vsync3.png

图中可以看出当第1个 VSync 到来时GPU还在处理数据,这时缓冲区在处理数据B,被占用了,此时的VBlank阶段就无法进行缓冲区交换,屏幕依然显示前缓冲区的数据A,发生了jank。当下一个信号到来时,此时 GPU 已经处理完了,那么就可以交换缓冲区,此时屏幕就会显示交互后缓冲区的数据B了。

由于硬件性能限制,我们无法改变 CPU/GPU 渲染的时间,所以第一次的Jank是无法避免的,但是在第二次信号来的时候,由于GPU占用了后缓冲区,没能实现缓冲区交换,导致屏幕依然显示上一帧A。由于此时,后缓冲区被占用了,就算此时CPU是空闲的也不能处理下一帧数据。增大了后期Jank的概率,比如图中第二个Jank的出现。

出现该问题本质的原因是,两个缓冲区各自被GPU/CPU、屏幕显示所占用。导致下一帧的数据不能被处理

三缓存

找到问题的本质了,那很容易想到,再加一个Buffer(这里叫它中Buffer)参与,让添加的这个中Buffer后Buffer交换,这样既不会影响到显示器读取前Buffer,又可以在后Buffer缓冲区不能处理时,让中Buffer来处理。像下图这样:

vsync4.png

当第一个信号到来时,前缓冲区在显示A、后缓冲区在处理B,它们都被占用。此时 CPU 就可以使用中缓冲区,来处理下一帧数据C。这样的话,C数据可以提前处理完成,之前第二次发生的Jank就不存在了,有效的降低了Jank出现的几率。

到这里,可以看出,不管是双缓冲和三缓冲,都会有卡顿、延时问题,只是三缓冲下,减少了卡顿的次数。

那是不是 Buffer 越多越好呢?

答案是否定的,Buffer存储的缓存数据是占有内存的,Buffer越多,缓存数据就越多,内存占用就会增大,所以Buffer只要3个就足够了。

Choreographer

那么在Android App层面,呈现在我们眼前的视觉效果(比如动画)是怎么出来的?是否和上述介绍的屏幕刷新机制呼应?或者说,它是怎么基于这个刷新机制原理实现的UI刷新?

对UI绘制流程熟悉的都知道,UI绘制会先走到ViewRootImpl#scheduleTraversals(),之后才会执行UI绘制。

#ViewRootImpl
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//重点关注这里:绘制的操作封装在mTraversalRunnable里,交给`Choreographer`类处理
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//...
}
}

重点关注mChoreographer.postCallback(..),UI绘制的操作被封装在mTraversalRunnable里,交由mChoreographerpostCallback方法处理。

mChoreographerChoreographer对象。那Choreographer类是做啥的呢,翻译为编舞者。这个类的命名很有意思,直接意思感觉和绘制毫无关联。但一只舞蹈的节奏控制是由编舞者掌控,就像绘制的过程的时机也需要类似这样一个角色控制一般。可见这个类的作者应该很喜欢舞蹈吧~

走入mChoreographer.postCallback看看做了什么

#Choreographer
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
//...
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

真正做事的是postCallbackDelayedInternal

#Choreographer
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
//把当前的runnable加入到callback队列中
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//达到期限时间
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

如果这个任务达到约定的延时时间,那么就会直接执行scheduleFrameLocked方法,如果没有达到就通过Handler发送一个延时异步消息,最终也会走到scheduleFrameLocked方法:

#Choreographer
//默认使用VSync同步机制
private static final boolean USE_VSYNC = SystemProperties.getBoolean(
"debug.choreographer.vsync", true);
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
//是否使用VSync同步机制
if (USE_VSYNC) {
//是否在主线程
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

scheduleFrameLocked()会根据是否是使用VSync同步机制,来执行不同的逻辑。下面顺着使用同步的情况分析:

判断当前线程的Looper是否是创建Choreographer时的线程Looper,由于是在ViewRootImpl中传入的,正常情况它是在主线程,所以就等价于判断当前线程是否在主线程。

如果不是就把这个消息加入到主线程,不管如何,最后都会走到scheduleVsyncLocked方法:

#Choreographer
private final FrameDisplayEventReceiver mDisplayEventReceiver;
private void scheduleVsyncLocked() {
//调用DisplayEventReceiver的scheduleVsync
mDisplayEventReceiver.scheduleVsync();
}

mDisplayEventReceiverFrameDisplayEventReceiver的对象。而FrameDisplayEventReceiver继承了DisplayEventReceiver这个抽象类。

DisplayEventReceiver如它的命名一样直观,显示事件的接收者。在DisplayEventReceiver的构造方法里面,会调用native方法nativeInit初始化一个接收者。在scheduleVsync方法里面,会调用native方法nativeScheduleVsync,把初始化的接收者对象传进去。

#DisplayEventReceiver
public abstract class DisplayEventReceiver {
public DisplayEventReceiver(Looper looper, int vsyncSource) {
//初始化一个接收者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource);
}

public void scheduleVsync() {
//初始化的接收者对象mReceiverPtr传进去
nativeScheduleVsync(mReceiverPtr);
}
}

FrameDisplayEventReceiverDisplayEventReceiver更具体一点,叫做帧显示的事件接收者。在前面介绍过,当收到同步信号过来后,就希望显示下一帧数据。那是怎么接收同步信号的呢?魔法就在上述那两个native方法里面,调用这两个方法之后。就会接收到`onVsync'方法的回调。这就是同步信号到来的时机。

#Choreographer.FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}

@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
//...
long now = System.nanoTime();
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
//timestampNanos / TimeUtils.NANOS_PER_MS 时间后走run方法
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
//接收到同步信号后执行
doFrame(mTimestampNanos, mFrame);
}
}

onVsync里,主要做的一件事就是在发送一个延时消息,时间是同步信号的时间戳,因为这个类是一个Runnable,这个消息会在run方法里面处理,之后就会执行doFrame()方法。

doFrame()从它的命名,十有八九就是我们一直提的接收到VSync同步信号后,处理帧数据的地方了:

void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
//抖动时间: 当前时间 - 同步信号通知的时间
final long jitterNanos = startNanos - frameTimeNanos;
//mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()) 类似 1s/60hz = 16.6ms,不过这里是纳秒为单位
//抖动时间超过了一帧刷新的时间,即发生了Jank
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//计算调帧数,超过一定限制(默认30),就表示应用在主线程做了大量工作,影响了绘制,打印提示
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
//...
}

try {
//按顺序执行任务(这里只留了核心代码)
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {}
}

在doFrame的最后,按顺序根据CallBack的类型执行任务,和我们在本节最开始的ViewRootImpl的这部分代码,关连起来了。我们post的这个类型是 Choreographer.CALLBACK_TRAVERSAL

 mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

终于快结束的节奏了,看看doCallbacks是做什么的

void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
try {
for (CallbackRecord c = callbacks; c != null; c = c.next) {
//注意这里:执行CallbackRecord的run方法
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
//回收处理完的CallbackRecord
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
}

CallbackRecord是记录callBack信息的类,它是个链表结构,具有next指针。它记录了callback所要执行任务或者说行为,比如Runnbable或者FrameCallback

private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;

public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
//执行我们最初post的Runnable
((Runnable)action).run();
}
}
}

对应我们最开始的postCallback方法,这个action也就是我们的mTraversalRunnable

 mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

到这里,postCallback的操作形成一个完整的闭环。关于Choreographer的介绍也就算完了。

总结

最后,我想分享一下本文的构思过程:

1.以屏幕显示的基础概念谈起,了解屏幕上的像素点是怎么显示出来的,对后面屏幕刷新的理解会变得更容易。

2.分析Android屏幕刷新机制的演变过程,更轻松的理解目前的刷新机制是怎么出来的,为什么要有双缓冲、三缓冲。

3.从ViewRootImpl的触发绘制为开始,到ChoreographerdoCallbacks结束,形成了完整的闭环。通过对这部分源码的分析,看到Choreographer这个编舞者是如何利用VSync同步机制,来掌控整个UI的刷新过程。

收起阅读 »

OpenGL ES 文字渲染

在音视频或 OpenGL 开发中,文字渲染是一个高频使用的功能,比如制作一些酷炫的字幕、为视频添加水印、设置特殊字体等等。实际上 OpenGL 并没有定义渲染文字的方式,所以我们最能想到的办法是:将带有文字的图像上传到纹理,然后进行纹理贴图。本文分别介绍下在应...
继续阅读 »

在音视频或 OpenGL 开发中,文字渲染是一个高频使用的功能,比如制作一些酷炫的字幕、为视频添加水印、设置特殊字体等等。

实际上 OpenGL 并没有定义渲染文字的方式,所以我们最能想到的办法是:将带有文字的图像上传到纹理,然后进行纹理贴图。

本文分别介绍下在应用层和 C++ 层常用的文字渲染方式。

OpenGL ES 文字渲染

基于 Canvas 绘制生成 Bitmap

在应用层实现文字渲染主要是利用 Canvas 将文本绘制成 Bitmap ,然后生成一张小图,然后在渲染的时候进行贴图。

在实际的生产环境中,一般会将这张小图转换成灰度图,减少不必要的数据拷贝和内存占用,然后在渲染的时候可以为灰度图上色,作为字体的颜色。

// 创建一个 bitmap 
Bitmap bitmap = Bitmap.createBitmap(width, hight, Bitmap.Config.ARGB_8888);
// 初始化画布绘制的图像到 bitmap 上
Canvas canvas = new Canvas(bitmap);
// 建立画笔
Paint paint = new Paint();
// 获取更清晰的图像采样,防抖动
paint.setDither(true);
paint.setFilterBitmap(true);
// 绘制文字到 bitmap
canvas.drawText text, x, y,paint);

然后生成纹理,将 bitmap 上传到纹理。

int[] textureIds = new int[1];
//创建纹理
GLES20.glGenTextures(1, textureIds, 0);
mTexId = textureIds[0];
//绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexId);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

ByteBuffer bitmapBuffer = ByteBuffer.allocate(bitmap.getHeight() * bitmap.getWidth() * 4);//RGBA
bitmap.copyPixelsToBuffer(bitmapBuffer);
bitmapBuffer.flip();

//设置内存大小绑定内存地址
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mWatermarkBitmap.getWidth(), mWatermarkBitmap.getHeight(),
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, bitmapBuffer);

//解绑纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

最后将带有文字的纹理映射到对应的位置(纹理贴图)。

FreeType

FreeType 是一个基于 C 语言实现的用于文字渲染的开源库,它小巧、高效、高度可定制,主要用于加载字体并将其渲染到位图,支持多种字体的相关操作。

FreeType 也是一个非常受欢迎的跨平台字体库,支持 Android、 iOS、 Linux 等操作系统。TrueType 字体不采用像素或其他不可缩放的方式来定义,而是一些通过数学公式(曲线的组合)。这些字形,类似于矢量图像,可以根据你需要的字体大小来生成像素图像。

FreeType 官网地址:

https://www.freetype.org/

FreeType 编译

本小节主要介绍使用 NDK 编译 Android 平台使用的 FreeType 库。首先在官网上下载最新版的 FreeType 源码,然后新建一个 jni 文件夹,将源码放到 jni 文件夹里,目录结构如下所示:

FreeType 目录结构

新建构建文件 Android.mk 和 Application.mk。

Android.mk 参考 Google 的构建脚本:

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)


LOCAL_SRC_FILES := \
./src/autofit/autofit.c \
./src/base/ftbase.c \
./src/base/ftbbox.c \
./src/base/ftbdf.c \
./src/base/ftbitmap.c \
./src/base/ftcid.c \
./src/base/ftdebug.c \
./src/base/ftfstype.c \
./src/base/ftgasp.c \
./src/base/ftglyph.c \
./src/base/ftgxval.c \
./src/base/ftinit.c \
./src/base/ftlcdfil.c \
./src/base/ftmm.c \
./src/base/ftotval.c \
./src/base/ftpatent.c \
./src/base/ftpfr.c \
./src/base/ftstroke.c \
./src/base/ftsynth.c \
./src/base/ftsystem.c \
./src/base/fttype1.c \
./src/base/ftwinfnt.c \
./src/bdf/bdf.c \
./src/bzip2/ftbzip2.c \
./src/cache/ftcache.c \
./src/cff/cff.c \
./src/cid/type1cid.c \
./src/gzip/ftgzip.c \
./src/lzw/ftlzw.c \
./src/pcf/pcf.c \
./src/pfr/pfr.c \
./src/psaux/psaux.c \
./src/pshinter/pshinter.c \
./src/psnames/psmodule.c \
./src/raster/raster.c \
./src/sfnt/sfnt.c \
./src/smooth/smooth.c \
./src/tools/apinames.c \
./src/truetype/truetype.c \
./src/type1/type1.c \
./src/type42/type42.c \
./src/winfonts/winfnt.c



LOCAL_C_INCLUDES += $(LOCAL_PATH)/include

LOCAL_CFLAGS += -W -Wall
LOCAL_CFLAGS += -fPIC -DPIC
LOCAL_CFLAGS += "-DDARWIN_NO_CARBON"
LOCAL_CFLAGS += "-DFT2_BUILD_LIBRARY"

LOCAL_CFLAGS += -O2

LOCAL_MODULE:= freetype

include $(BUILD_STATIC_LIBRARY)
#https://android.googlesource.com/platform/external/freetype/+/android-6.0.1_r28/Android.mk

Application.mk:

APP_OPTIM := release
APP_CPPFLAGS := -std=c++14 -frtti
NDK_TOOLCHAIN_VERSION := clang
APP_PLATFORM := android-28
APP_STL := c++_static
APP_ABI := arm64-v8a,armeabi-v7a

最后 jni 目录下命令行执行 ndk-build 指令即可,如果不想编译,也可以直接到下面项目取现成的静态库:

https://github.com/githubhaohao/NDK_OpenGLES_3_0

OpenGL 使用 FreeType 渲染文字

FreeType 的使用

引入头文件:

#include "ft2build.h"
#include

然后要加载一个字体,我们需要做的是初始化 FreeType 并且将这个字体加载为 FreeType 称之为面 Face 的东西。这里我在 Windows 下找了个字体文件 Antonio-Regular.ttf ,放到 sdcard 下面供 FreeType 加载。


FT_Library ft;

if (FT_Init_FreeType(&ft))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Could not init FreeType Library");


FT_Face face;
if (FT_New_Face(ft, "/sdcard/fonts/Antonio-Regular.ttf", 0, &face))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Failed to load font");


FT_Set_Pixel_Sizes(face, 0, 96);

代码片段中,FT_Set_Pixel_Sizes 用于设置文字的大小,此函数设置了字体面的宽度和高度,将宽度值设为0表示我们要从字体面通过给出的高度中动态计算出字形的宽度。

一个字体面中 Face 包含了所有字形的集合,我们可以通过调用 FT_Load_Char 函数来激活当前要表示的字形。这里我们选在加载字母字形 'A':

if (FT_Load_Char(face, 'A', FT_LOAD_RENDER))
std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;

通过将 FT_LOAD_RENDER 设为一个加载标识,我们告诉 FreeType 去创建一个 8 位的灰度位图,我们可以通过face->glyph->bitmap 来取得这个位图。

使用 FreeType 加载的字形位图并不像我们使用位图字体那样持有相同的尺寸大小。使用FreeType生产的字形位图的大小是恰好能包含这个字形的尺寸。例如生产用于表示 '.' 的位图的尺寸要比表示 'A' 的小得多。

因此,FreeType在加载字形的时候还生产了几个度量值来描述生成的字形位图的大小和位置。下图展示了 FreeType 的所有度量值的涵义。

glyph.png

那么多属性其实不用刻意取记住,这里只是作为概念性了解。最后,使用完 FreeType 记得释放相关资源:

FT_Done_Face(face);
FT_Done_FreeType(ft);

OpenGL 文字渲染

按照前面的思路,使用 FreeType 加载字形的位图然后生成纹理,然后进行纹理贴图。

然而每次渲染的时候都去重新加载位图显然不是高效的,我们应该将这些生成的数据储存在应用程序中,在渲染过程中再去取,重复利用。

方便起见,我们需要定义一个用来储存这些属性的结构体,并创建一个字符表来存储这些字形属性。

struct Character {
GLuint textureID; // ID handle of the glyph texture
glm::ivec2 size; // Size of glyph
glm::ivec2 bearing; // Offset from baseline to left/top of glyph
GLuint advance; // Horizontal offset to advance to next glyph
};

std::map m_Characters;

简单起见,我们只生成表示 128 个 ASCII 字符的字符表,并为每一个字符储存纹理和一些度量值。这样,所有需要的字符就被存下来备用了。

void TextRenderSample::LoadFacesByASCII() {
// FreeType
FT_Library ft;
// All functions return a value different than 0 whenever an error occurred
if (FT_Init_FreeType(&ft))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Could not init FreeType Library");

// Load font as face
FT_Face face;
if (FT_New_Face(ft, "/sdcard/fonts/Antonio-Regular.ttf", 0, &face))
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYPE: Failed to load font");

// Set size to load glyphs as
FT_Set_Pixel_Sizes(face, 0, 96);

// Disable byte-alignment restriction
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

// Load first 128 characters of ASCII set
for (unsigned char c = 0; c < 128; c++)
{
// Load character glyph
if (FT_Load_Char(face, c, FT_LOAD_RENDER))
{
LOGCATE("TextRenderSample::LoadFacesByASCII FREETYTPE: Failed to load Glyph");
continue;
}
// Generate texture
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_LUMINANCE,
face->glyph->bitmap.width,
face->glyph->bitmap.rows,
0,
GL_LUMINANCE,
GL_UNSIGNED_BYTE,
face->glyph->bitmap.buffer
);

// Set texture options
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Now store character for later use
Character character = {
texture,
glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
static_cast(face->glyph->advance.x)
};
m_Characters.insert(std::pair(c, character));
}
glBindTexture(GL_TEXTURE_2D, 0);
// Destroy FreeType once we're finished
FT_Done_Face(face);
FT_Done_FreeType(ft);

}

针对 OpenGL ES 灰度图要使用的纹理格式是 GL_LUMINANCE 而不是 GL_RED 。

OpenGL 纹理对应的图像默认要求 4 字节对齐,这里需要设置为 1 ,确保宽度不是 4 倍数的位图(灰度图)能够正常渲染。

渲染文字使用的 shader :

//vertex shader
#version 300 es
layout(location = 0) in vec4 a_position;//
uniform mat4 u_MVPMatrix;
out vec2 v_texCoord;
void main()
{
gl_Position = u_MVPMatrix * vec4(a_position.xy, 0.0, 1.0);;
v_texCoord = a_position.zw;
}

//fragment shader
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_textTexture;
uniform vec3 u_textColor;

void main()
{
vec4 color = vec4(1.0, 1.0, 1.0, texture(s_textTexture, v_texCoord).r);
outColor = vec4(u_textColor, 1.0) * color;
}

片段着色器有两个 uniform 变量:一个是单颜色通道的字形位图纹理,另一个是文字的颜色,我们可以同调整它来改变最终输出的字体颜色。

开启混合,去掉文字背景。

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

生成一个 VAO 和一个 VBO ,用于管理的存储顶点、纹理坐标数据,GL_DYNAMIC_DRAW 表示我们后面要使用 glBufferSubData 不断刷新 VBO 的缓存。


glGenVertexArrays(1, &m_VaoId);
glGenBuffers(1, &m_VboId);

glBindVertexArray(m_VaoId);
glBindBuffer(GL_ARRAY_BUFFER, m_VboId);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);
glBindVertexArray(GL_NONE);

每个 2D 方块需要 6 个顶点,每个顶点又是由一个 4 维向量(一个纹理坐标和一个顶点坐标)组成,因此我们将VBO 的内存分配为 6*4 个 float 的大小。

最后进行文字渲染,其中传入 viewport 主要是针对屏幕坐标进行归一化:

void TextRenderSample::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale,
glm::vec3 color, glm::vec2 viewport) {
// 激活合适的渲染状态
glUseProgram(m_ProgramObj);
glUniform3f(glGetUniformLocation(m_ProgramObj, "u_textColor"), color.x, color.y, color.z);
glBindVertexArray(m_VaoId);
GO_CHECK_GL_ERROR();
// 对文本中的所有字符迭代
std::string::const_iterator c;
x *= viewport.x;
y *= viewport.y;
for (c = text.begin(); c != text.end(); c++)
{
Character ch = m_Characters[*c];

GLfloat xpos = x + ch.bearing.x * scale;
GLfloat ypos = y - (ch.size.y - ch.bearing.y) * scale;

xpos /= viewport.x;
ypos /= viewport.y;

GLfloat w = ch.size.x * scale;
GLfloat h = ch.size.y * scale;

w /= viewport.x;
h /= viewport.y;

LOGCATE("TextRenderSample::RenderText [xpos,ypos,w,h]=[%f, %f, %f, %f]", xpos, ypos, w, h);

// 当前字符的VBO
GLfloat vertices[6][4] = {
{ xpos, ypos + h, 0.0, 0.0 },
{ xpos, ypos, 0.0, 1.0 },
{ xpos + w, ypos, 1.0, 1.0 },

{ xpos, ypos + h, 0.0, 0.0 },
{ xpos + w, ypos, 1.0, 1.0 },
{ xpos + w, ypos + h, 1.0, 0.0 }
};

// 在方块上绘制字形纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, ch.textureID);
glUniform1i(m_SamplerLoc, 0);
GO_CHECK_GL_ERROR();
// 更新当前字符的VBO
glBindBuffer(GL_ARRAY_BUFFER, m_VboId);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
GO_CHECK_GL_ERROR();
glBindBuffer(GL_ARRAY_BUFFER, 0);
// 绘制方块
glDrawArrays(GL_TRIANGLES, 0, 6);
GO_CHECK_GL_ERROR();
// 更新位置到下一个字形的原点,注意单位是1/64像素
x += (ch.advance >> 6) * scale; //(2^6 = 64)
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}

使用 RenderText 渲染 2 个文本:

	// (x,y)为屏幕坐标系的位置,即原点位于屏幕中心,x(-1.0,1.0), y(-1.0,1.0)
RenderText("My WeChat ID is Byte-Flow.", -0.9f, 0.2f, 1.0f, glm::vec3(0.8, 0.1f, 0.1f), viewport);
RenderText("Welcome to add my WeChat.", -0.9f, 0.0f, 2.0f, glm::vec3(0.2, 0.4f, 0.7f), viewport);

完整实现代码见项目: github.com/githubhaoha…

收起阅读 »

Android 第三方RoundedImageView设置各种圆形、方形头像

Android 自定义CoolImageView实现QQ首页背景图片动画效果一.第三方RoundedImageView1.在Android Studio中,可进入模块设置中添加库依赖。 进入Module结构设置添加库依赖 ,输入Rounde...
继续阅读 »

Android 自定义CoolImageView实现QQ首页背景图片动画效果




一.第三方RoundedImageView

1.在Android Studio中,可进入模块设置中添加库依赖。 进入Module结构设置添加库依赖 ,输入RoundedImageView然后搜索添加

2.在Moudle的build.gradle中添加如下代码,添加完之后在Build中进行下Make Module操作(编译下Module),使自己添加的依赖生效

repositories {

mavenCentral()

}

dependencies {

compile 'com.makeramen:roundedimageview:2.2.1'

}

3.添加相关属性:

控件属性: 

riv_border_width: 边框宽度

riv_border_color: 边框颜色

riv_oval: 是否圆形

riv_corner_radius: 圆角弧度

riv_corner_radius_top_left:左上角弧度

riv_corner_radius_top_right: 右上角弧度

riv_corner_radius_bottom_left:左下角弧度

riv_corner_radius_bottom_right:右下角弧度

4.示例布局:

 <com.makeramen.roundedimageview.RoundedImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_oval="true" />

<com.makeramen.roundedimageview.RoundedImageView

xmlns:app="http://schemas.android.com/apk/res-auto"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:scaleType="fitCenter"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius="10dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat" />

<com.makeramen.roundedimageview.RoundedImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:scaleType="fitCenter"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius_top_left="25dp"

app:riv_corner_radius_bottom_right="25dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat" />

<com.makeramen.roundedimageview.RoundedImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:scaleType="fitCenter"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius_top_right="25dp"

app:riv_corner_radius_bottom_left="25dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat" />

<com.makeramen.roundedimageview.RoundedImageView

android:layout_width="96dp"

android:layout_height="72dp"

android:scaleType="center"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius="25dp"

app:riv_mutate_background="true"

app:riv_oval="true"

app:riv_tile_mode="repeat" />

 <com.makeramen.roundedimageview.RoundedImageView

android:id="@+id/imCompanyHeadItem"

android:layout_width="50dp"

android:layout_marginTop="10dp"

android:layout_marginRight="6.5dp"

android:layout_marginLeft="6.5dp"

android:src="@drawable/head_home"

android:layout_gravity="center"

android:layout_height="50dp"

app:riv_border_color="@color/_c7ced8"

app:riv_border_width="1dp"

app:riv_corner_radius_top_left="5dp"

app:riv_corner_radius_bottom_right="5dp"

app:riv_corner_radius_bottom_left="5dp"

app:riv_corner_radius_top_right="5dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat"/>

二.自定义RoundImageView

1.布局:

 <com.iruiyou.pet.utils.RoundImageView

android:id="@+id/headIv"

android:layout_width="125dp"

android:layout_height="125dp"

android:layout_marginTop="92dp"

android:src="@drawable/head_home"

loonggg:border_incolor="#000fff"

loonggg:border_outcolor="#fff000"

loonggg:border_width="10dp"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toTopOf="parent"/>

2.自定义类:

import android.content.Context;

import android.content.res.TypedArray;

import android.graphics.Bitmap;

import android.graphics.Canvas;

import android.graphics.Paint;

import android.graphics.PorterDuff;

import android.graphics.PorterDuffXfermode;

import android.graphics.Rect;

import android.graphics.drawable.BitmapDrawable;

import android.graphics.drawable.Drawable;

import android.graphics.drawable.NinePatchDrawable;

import android.util.AttributeSet;

import android.widget.ImageView;

import com.iruiyou.pet.R;



/**

*


* @author sgf


* 自定义圆形头像


*


*/


public class RoundImageView extends ImageView {

private int mBorderThickness = 0;

private Context mContext;

private int defaultColor = 0xFFFFFFFF;

// 外圆边框颜色

private int mBorderOutsideColor = 0;

// 内圆边框颜色

private int mBorderInsideColor = 0;

// RoundImageView控件默认的长、宽

private int defaultWidth = 0;

private int defaultHeight = 0;



public RoundImageView(Context context) {

super(context);

mContext = context;

}



public RoundImageView(Context context, AttributeSet attrs) {

super(context, attrs);

mContext = context;

// 设置RoundImageView的属性值,比如颜色,宽度等

setRoundImageViewAttributes(attrs);

}



public RoundImageView(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

mContext = context;

setRoundImageViewAttributes(attrs);

}



// 从attr.xml文件中获取属性值,并给RoundImageView设置

private void setRoundImageViewAttributes(AttributeSet attrs) {

TypedArray a = mContext.obtainStyledAttributes(attrs,

R.styleable.round_image_view);

mBorderThickness = a.getDimensionPixelSize(

R.styleable.round_image_view_border_width, 0);

mBorderOutsideColor = a.getColor(

R.styleable.round_image_view_border_outcolor, defaultColor);

mBorderInsideColor = a.getColor(

R.styleable.round_image_view_border_incolor, defaultColor);

a.recycle();

}



// 具体解释:比如我自定义一个控件,怎么实现呢,以RoundImageView为例,首先是继承ImageView,然后实现其构造函数,在构造函数中,获取attr中的属性值(再次解释:这里获取的具体的这个属性的值是怎么来的呢?比如颜色和宽度,这个在attr.xml中定义了相关的名字,而在使用RoundImageView的xml布局文件中,我们会设置其值,这里需要用的值,就是从那里设置的),并设置在本控件中,然后继承onDraw方法,画出自己想要的图形或者形状即可

/**

* 这个是继承的父类的onDraw方法


*


* onDraw和下面的方法不用管,基本和学习自定义没关系,就是实现怎么画圆的,你可以改变下面代码试着画三角形头像,哈哈


*/


@Override

protected void onDraw(Canvas canvas) {

Drawable drawable = getDrawable();

if (drawable == null) {

return;

}

if (getWidth() == 0 || getHeight() == 0) {

return;

}

this.measure(0, 0);

if (drawable.getClass() == NinePatchDrawable.class)

return;

Bitmap b = ((BitmapDrawable) drawable).getBitmap();

Bitmap bitmap = b.copy(Bitmap.Config.ARGB_8888, true);

if (defaultWidth == 0) {

defaultWidth = getWidth();

}

if (defaultHeight == 0) {

defaultHeight = getHeight();

}

int radius = 0;

// 这里的判断是如果内圆和外圆设置的颜色值不为空且不是默认颜色,就定义画两个圆框,分别为内圆和外圆边框

if (mBorderInsideColor != defaultColor

&& mBorderOutsideColor != defaultColor) {

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2 - 2 * mBorderThickness;

// 画内圆

drawCircleBorder(canvas, radius + mBorderThickness / 2,

mBorderInsideColor);

// 画外圆

drawCircleBorder(canvas, radius + mBorderThickness

+ mBorderThickness / 2, mBorderOutsideColor);

} else if (mBorderInsideColor != defaultColor

&& mBorderOutsideColor == defaultColor) {// 这里的是如果内圆边框不为空且颜色值不是默认值,就画一个内圆的边框

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2 - mBorderThickness;

drawCircleBorder(canvas, radius + mBorderThickness / 2,

mBorderInsideColor);

} else if (mBorderInsideColor == defaultColor

&& mBorderOutsideColor != defaultColor) {// 这里的是如果外圆边框不为空且颜色值不是默认值,就画一个外圆的边框

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2 - mBorderThickness;

drawCircleBorder(canvas, radius + mBorderThickness / 2,

mBorderOutsideColor);

} else {// 这种情况是没有设置属性颜色的情况下,即没有边框的情况

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2;

}

Bitmap roundBitmap = getCroppedRoundBitmap(bitmap, radius);

canvas.drawBitmap(roundBitmap, defaultWidth / 2 - radius, defaultHeight

/ 2 - radius, null);

}



/**

* 获取裁剪后的圆形图片


*


* @param bmp


* @param radius


* 半径


* @return


*/


public Bitmap getCroppedRoundBitmap(Bitmap bmp, int radius) {

Bitmap scaledSrcBmp;

int diameter = radius * 2;

// 为了防止宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片

int bmpWidth = bmp.getWidth();

int bmpHeight = bmp.getHeight();

int squareWidth = 0, squareHeight = 0;

int x = 0, y = 0;

Bitmap squareBitmap;

if (bmpHeight > bmpWidth) {// 高大于宽

squareWidth = squareHeight = bmpWidth;

x = 0;

y = (bmpHeight - bmpWidth) / 2;

// 截取正方形图片

squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,

squareHeight);

} else if (bmpHeight < bmpWidth) {// 宽大于高

squareWidth = squareHeight = bmpHeight;

x = (bmpWidth - bmpHeight) / 2;

y = 0;

squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,

squareHeight);

} else {

squareBitmap = bmp;

}

if (squareBitmap.getWidth() != diameter

|| squareBitmap.getHeight() != diameter) {

scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter,

diameter, true);

} else {

scaledSrcBmp = squareBitmap;

}

Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),

scaledSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);

Canvas canvas = new Canvas(output);



Paint paint = new Paint();

Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),

scaledSrcBmp.getHeight());



paint.setAntiAlias(true);

paint.setFilterBitmap(true);

paint.setDither(true);

canvas.drawARGB(0, 0, 0, 0);

canvas.drawCircle(scaledSrcBmp.getWidth() / 2,

scaledSrcBmp.getHeight() / 2, scaledSrcBmp.getWidth() / 2,

paint);

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

canvas.drawBitmap(scaledSrcBmp, rect, rect, paint);

bmp = null;

squareBitmap = null;

scaledSrcBmp = null;

return output;

}



/**

* 画边缘的圆,即内圆或者外圆


*/


private void drawCircleBorder(Canvas canvas, int radius, int color) {

Paint paint = new Paint();

/* 去锯齿 */

paint.setAntiAlias(true);

paint.setFilterBitmap(true);

paint.setDither(true);

paint.setColor(color);

/* 设置paint的 style 为STROKE:空心 */

paint.setStyle(Paint.Style.STROKE);

/* 设置paint的外框宽度 */

paint.setStrokeWidth(mBorderThickness);

canvas.drawCircle(defaultWidth / 2, defaultHeight / 2, radius, paint);

}

}

3.res--values--attrs.xml文件


<?xml version="1.0" encoding="utf-8"?>

<resources>



<declare-styleable name="round_image_view">

<attr name="border_width" format="dimension" />

<attr name="border_incolor" format="color" />

<attr name="border_outcolor" format="color"></attr>

</declare-styleable>



</resources>

三.第三方NiceImageView

1.效果图如下:



 2.特点:

基于AppCompatImageView扩展

支持圆角、圆形显示

可绘制边框,圆形时可绘制内外两层边框

支持边框不覆盖图片

可绘制遮罩

3.基本用法:

 1. 添加JitPack仓库 在项目根目录下的 build.gradle 中添加仓库:

allprojects {

repositories {

...

maven { url "https://jitpack.io" }

}

}

2. 添加项目依赖


dependencies {

implementation 'com.github.SheHuan:NiceImageView:1.0.5'

}

3. 在布局文件中添加CornerLabelView

<com.shehuan.niv.NiceImageView

android:layout_width="200dp"

android:layout_height="200dp"

android:layout_marginTop="10dp"

android:src="@drawable/cat"

app:border_color="#FF7F24"

app:border_width="4dp"

app:is_circle="true" />

4.支持的属性、方法

属性名含义默认值对应方法
is_circle是否显示为圆形(默认为矩形)falseisCircle()
corner_top_left_radius左上角圆角半径0dpsetCornerTopLeftRadius()
corner_top_right_radius右上角圆角半径0dpsetCornerTopRightRadius()
corner_bottom_left_radius左下角圆角半径0dpsetCornerBottomLeftRadius()
corner_bottom_right_radius右下角圆角半径0dpsetCornerBottomRightRadius()
corner_radius统一设置四个角的圆角半径0dpsetCornerRadius()
border_width边框宽度0dpsetBorderWidth()
border_color边框颜色#ffffffsetBorderColor()
inner_border_width相当于内层边框(is_circle为true时支持)0dpsetInnerBorderWidth()
inner_border_color内边框颜色#ffffffsetInnerBorderColor()
is_cover_srcborder、inner_border是否覆盖图片内容falseisCoverSrc()
mask_color图片上绘制的遮罩颜色不设置颜色则不绘制setMaskColor()

可参考:https://github.com/SheHuan/NiceImageView


 5.其它:


如果你需要实现类似钉钉的圆形组合头像,例如:



可以先生成对应的Bitmap,并用圆形的 NiceImageView 显示即可。如何生成组合Bitmap可以参考这里:CombineBitmap


四.如果你的项目中只有圆形的图片而不需要设置圆角图片的话,可以试试下面的第三方:


https://github.com/hdodenhof/CircleImageView

https://github.com/open-android/RoundedImageView


收起阅读 »

Android之CircleImageView使用

文章大纲一、什么是CircleImageView二、代码实战三、项目源码下载一、什么是CircleImageView  圆角 ImageView,在我们的 App 中这个想必是太常见了,也许我们可以有无数种展示圆角图片的方法,但是 CircleImageVie...
继续阅读 »

文章大纲

一、什么是CircleImageView
二、代码实战
三、项目源码下载

一、什么是CircleImageView

  圆角 ImageView,在我们的 App 中这个想必是太常见了,也许我们可以有无数种展示圆角图片的方法,但是 CircleImageView 绝对是我们在开发时需要优先考虑的,如果你还不知道 CircleImageView,那么你需要赶快去体验它在处理圆角图片时的强大了,相信你肯定会觉得和 CircleImageView 相见恨晚。

二、代码实战

1. 添加依赖

    //添加CircleImageView依赖
implementation 'de.hdodenhof:circleimageview:2.1.0'

2. 添加图片资源

 

3. 资源文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<de.hdodenhof.circleimageview.CircleImageView
xmlns:circleimageview="http://schemas.android.com/apk/res-auto"
android:id="@+id/imageview"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:src="@drawable/test"
circleimageview:civ_border_color="@android:color/holo_red_light"
circleimageview:civ_border_overlay="false"
circleimageview:civ_border_width="2dp"
circleimageview:civ_fill_color="@android:color/holo_blue_light"/>

</android.support.constraint.ConstraintLayout>

常用属性:
(1)civ_border_width: 设置边框的宽度,默认为0,即无边框。
(2)civ_border_color: 设置边框的颜色,默认为黑色。
(3)civ_border_overlay:设置边框是否覆盖在图片上,默认为false,即边框在图片外圈。
(4)civ_fill_color:设置图片的底色,默认透明。
(5)civ_border_width:设置边框大小
(6)civ_fill_color:设置图片的底色,默认透明

4. MainActivity.java

public class MainActivity extends AppCompatActivity {

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

5. 运行结果

 
demo下载地址:CircleImageViewTest.zip
收起阅读 »

移动端强大的富文本编辑器richeditor-android

通常我们使用富文本编辑器都是在H5端实现,但是如果你遇到在移动端发表文章的功能,那么richeditor-android这套框架可以轻松为你实现,不需要再使用大量的控件进行拼凑! 功能表如下图所示: 引入richeditor-android ...
继续阅读 »


通常我们使用富文本编辑器都是在H5端实现,但是如果你遇到在移动端发表文章的功能,那么richeditor-android这套框架可以轻松为你实现,不需要再使用大量的控件进行拼凑!



  • 功能表如下图所示:





  • 引入richeditor-android



richeditor-android需要的jar:

implementation 'jp.wasabeef:richeditor-android:1.2.2'


这是一个Dialog框架,demo中不想自己去写,所以就使用了第三方
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'


  • 引入控件RichEditor


   <jp.wasabeef.richeditor.RichEditor
android:id="@+id/editor"
android:layout_width="match_parent"
android:layout_height="wrap_content" />


  • 使用到的权限


如果拍照需要相机权限,选择图片需要SD卡权限,插入网络图片需要网络权限


 <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />


  • 初始化RichEditor


       mEditor = (RichEditor) findViewById(R.id.editor);

//初始化编辑高度
mEditor.setEditorHeight(200);
//初始化字体大小
mEditor.setEditorFontSize(22);
//初始化字体颜色
mEditor.setEditorFontColor(Color.BLACK);
//mEditor.setEditorBackgroundColor(Color.BLUE);

//初始化内边距
mEditor.setPadding(10, 10, 10, 10);
//设置编辑框背景,可以是网络图片
// mEditor.setBackground("https://raw.githubusercontent.com/wasabeef/art/master/chip.jpg");
// mEditor.setBackgroundColor(Color.BLUE);
mEditor.setBackgroundResource(R.drawable.bg);
//设置默认显示语句
mEditor.setPlaceholder("Insert text here...");
//设置编辑器是否可用
mEditor.setInputEnabled(true);


  • 实时监听Editor输入内容


   mPreview = (TextView) findViewById(R.id.preview);
mEditor.setOnTextChangeListener(new RichEditor.OnTextChangeListener() {
@Override
public void onTextChange(String text) {
mPreview.setText(text);
}
});


  • 功能方法


        /**
* 撤销当前标签状态下所有内容
*/

findViewById(R.id.action_undo).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.undo();
}
});
/**
* 恢复撤销的内容
*/

findViewById(R.id.action_redo).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.redo();
}
});
/**
* 加粗
*/

findViewById(R.id.action_bold).setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setBold();
}
});
/**
* 斜体
*/

findViewById(R.id.action_italic).setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setItalic();
}
});
/**
* 下角表
*/

findViewById(R.id.action_subscript).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
if (mEditor.getHtml() == null) {
return;
}
mEditor.setSubscript();
}
});
/**
* 上角标
*/

findViewById(R.id.action_superscript).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
if (mEditor.getHtml() == null) {
return;
}
mEditor.setSuperscript();
}
});

/**
* 删除线
*/

findViewById(R.id.action_strikethrough).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setStrikeThrough();
}
});
/**
*下划线
*/

findViewById(R.id.action_underline).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setUnderline();
}
});
/**
* 设置标题(1到6)
*/

findViewById(R.id.action_heading1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(1);
}
});

findViewById(R.id.action_heading2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(2);
}
});

findViewById(R.id.action_heading3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(3);
}
});

findViewById(R.id.action_heading4).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(4);
}
});

findViewById(R.id.action_heading5).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(5);
}
});

findViewById(R.id.action_heading6).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(6);
}
});
/**
* 设置字体颜色
*/

findViewById(R.id.action_txt_color).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
new MaterialDialog.Builder(MainActivity.this)
.title("选择字体颜色")
.items(R.array.color_items)
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {

dialog.dismiss();
switch (which) {
case 0://红
mEditor.setTextColor(Color.RED);
break;
case 1://黄
mEditor.setTextColor(Color.YELLOW);
break;
case 2://蓝
mEditor.setTextColor(Color.GREEN);
break;
case 3://绿
mEditor.setTextColor(Color.BLUE);
break;
case 4://黑
mEditor.setTextColor(Color.BLACK);
break;
}
return false;
}
}).show();
}
});

findViewById(R.id.action_bg_color).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
new MaterialDialog.Builder(MainActivity.this)
.title("选择字体背景颜色")
.items(R.array.text_back_color_items)
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {

dialog.dismiss();
switch (which) {
case 0://红
mEditor.setTextBackgroundColor(Color.RED);
break;
case 1://黄
mEditor.setTextBackgroundColor(Color.YELLOW);
break;
case 2://蓝
mEditor.setTextBackgroundColor(Color.GREEN);
break;
case 3://绿
mEditor.setTextBackgroundColor(Color.BLUE);
break;
case 4://黑
mEditor.setTextBackgroundColor(Color.BLACK);
break;
case 5://透明
mEditor.setTextBackgroundColor(R.color.transparent);
break;
}
return false;
}
}).show();

}
});
/**
* 向右缩进
*/

findViewById(R.id.action_indent).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setIndent();
}
});
/**
* 向左缩进
*/

findViewById(R.id.action_outdent).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setOutdent();
}
});
/**
*文章左对齐
*/

findViewById(R.id.action_align_left).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setAlignLeft();
}
});
/**
* 文章居中对齐
*/

findViewById(R.id.action_align_center).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setAlignCenter();
}
});
/**
* 文章右对齐
*/

findViewById(R.id.action_align_right).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setAlignRight();
}
});
/**
* 无序排列
*/

findViewById(R.id.action_insert_bullets).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setBullets();
}
});
/**
* 有序排列
*/

findViewById(R.id.action_insert_numbers).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setNumbers();
}
});
/**
* 引用
*/

findViewById(R.id.action_blockquote).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setBlockquote();
}
});

/**
* 插入图片
*/

findViewById(R.id.action_insert_image).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
ActivityCompat.requestPermissions(MainActivity.this, mPermissionList, 100);
}
});
/**
* 插入连接
*/

findViewById(R.id.action_insert_link).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new MaterialDialog.Builder(MainActivity.this)
.title("将输入连接地址")
.items("http://blog.csdn.net/huangxiaoguo1")
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {
dialog.dismiss();
mEditor.focusEditor();
mEditor.insertLink("http://blog.csdn.net/huangxiaoguo1",
"http://blog.csdn.net/huangxiaoguo1");
return false;
}
}).show();
}
});
/**
* 选择框
*/

findViewById(R.id.action_insert_checkbox).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.insertTodo();
}
});

/**
* 获取并显示Html
*/

findViewById(R.id.tv_showhtml).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(v.getContext(), WebViewActivity.class);
intent.putExtra("contextURL", mEditor.getHtml());
startActivity(intent);
}
});


  • 插入图片并使用屏幕宽度




权限,我这里只是选着图片,关于拍照的自己可以去实现

String[] mPermissionList = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE};


@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 100:
boolean writeExternalStorage = grantResults[0] == PackageManager.PERMISSION_GRANTED;
boolean readExternalStorage = grantResults[1] == PackageManager.PERMISSION_GRANTED;
if (grantResults.length > 0 && writeExternalStorage && readExternalStorage) {
getImage();
} else {
Toast.makeText(this, "请设置必要权限", Toast.LENGTH_SHORT).show();
}

break;
}
}

private void getImage() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"),
REQUEST_PICK_IMAGE);
} else {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_PICK_IMAGE);
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case REQUEST_PICK_IMAGE:
if (data != null) {
String realPathFromUri = RealPathFromUriUtils.getRealPathFromUri(this, data.getData());
mEditor.insertImage("https://unsplash.it/2000/2000?random&58",
"huangxiaoguo\" style=\"max-width:100%");
mEditor.insertImage(realPathFromUri, realPathFromUri + "\" style=\"max-width:100%");
// mEditor.insertImage(realPathFromUri, realPathFromUri + "\" style=\"max-width:100%;max-height:100%");

} else {
Toast.makeText(this, "图片损坏,请重新选择", Toast.LENGTH_SHORT).show();
}

break;
}
}
}

注意这里 “\” style=\”max-width:100%”是让我们从手机选择的图片和网络加载的图片适配屏幕宽高,解决图片太大显示不全问题!


关于如何获得手机图片真正地址(realPathFromUri )请看http://blog.csdn.net/huangxiaoguo1/article/details/78983582


richeditor-android github地址:https://github.com/wasabeef/richeditor-android


demo地址:http://download.csdn.net/download/huangxiaoguo1/10205773

收起阅读 »

Android加载离线和网络git

本文介绍如何将android-gif-drawable集成到项目中,并且如何使用android-gif-drawable加载离线和网络Gif动图。 android-gif-drawable的集成 在线集成 Github上相关教程,也比较简单,将依赖添加到...
继续阅读 »


本文介绍如何将android-gif-drawable集成到项目中,并且如何使用android-gif-drawable加载离线和网络Gif动图。


android-gif-drawable的集成


在线集成


Github上相关教程,也比较简单,将依赖添加到项目的build.gradle文件即可:


dependencies {
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.11'
}

离线集成


Android Studio 3.0中有效



  1. 进入Github上的realease页面-realease点我


  2. 下载其中的android-gif-drawable-1.2.11.aar


  3. android-gif-drawable-1.2.11.aar添加到项目的libs目录中


  4. 在项目的build.gradle中添加该arr文件



compile(name:'android-gif-drawable-1.2.11', ext:'aar')


  1. 集成完毕,可以进行测试。


android-gif-drawable的使用


android-gif-drawable有四种控件:GifImageViewGifImageButtonGifTextViewGifTextureView。这里以ImageView为例进行介绍。


加载本地图片



  1. 直接在布局中选定资源文件


<pl.droidsonroids.gif.GifImageView
android:id="@+id/fragment_gif_local"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/dog"/>


  1. 通过代码进行动态添加gif动图


//1. 构建GifDrawable
GifDrawable gifFromResDrawable = new GifDrawable( getResources(), R.drawable.dog );
//2. 设置给GifImageView控件
gifImageView.setImageDrawable(gifFromResDrawable);

GifDrawable


GifDrawable是用于该开源库的Drawable类。构造方法大致有9种:


//1. asset file
GifDrawable gifFromAssets = new GifDrawable( getAssets(), "anim.gif" );

//2. resource (drawable or raw)
GifDrawable gifFromResource = new GifDrawable( getResources(), R.drawable.anim );

//3. byte array
byte[] rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

//4. FileDescriptor
FileDescriptor fd = new RandomAccessFile( "/path/anim.gif", "r" ).getFD();
GifDrawable gifFromFd = new GifDrawable( fd );

//5. file path
GifDrawable gifFromPath = new GifDrawable( "/path/anim.gif" );

//6. file
File gifFile = new File(getFilesDir(),"anim.gif");
GifDrawable gifFromFile = new GifDrawable(gifFile);

//7. AssetFileDescriptor
AssetFileDescriptor afd = getAssets().openFd( "anim.gif" );
GifDrawable gifFromAfd = new GifDrawable( afd );

//8. InputStream (it must support marking)
InputStream sourceIs = ...
BufferedInputStream bis = new BufferedInputStream( sourceIs, GIF_LENGTH );
GifDrawable gifFromStream = new GifDrawable( bis );

//9. direct ByteBuffer
ByteBuffer rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

加载网络Gif


我们解决的办法是将Gif图片下载到缓存目录中,然后从磁盘缓存中获取该Gif动图进行显示。


1、下载工具DownloadUtils.java


public class DownloadUtils {
private final int DOWN_START = 1; // Handler消息类型(开始下载)
private final int DOWN_POSITION = 2; // Handler消息类型(下载位置)
private final int DOWN_COMPLETE = 3; // Handler消息类型(下载完成)
private final int DOWN_ERROR = 4; // Handler消息类型(下载失败)
private OnDownloadListener onDownloadListener;

public void setOnDownloadListener(OnDownloadListener onDownloadListener) {
this.onDownloadListener = onDownloadListener;
}

/**
* 下载文件
*
* @param url 文件路径
* @param filepath 保存地址
*/

public void download(String url, String filepath) {
MyRunnable mr = new MyRunnable();
mr.url = url;
mr.filepath = filepath;
new Thread(mr).start();
}

@SuppressWarnings("unused")
private void sendMsg(int what) {
sendMsg(what, null);
}

private void sendMsg(int what, Object mess) {
Message m = myHandler.obtainMessage();
m.what = what;
m.obj = mess;
m.sendToTarget();
}

Handler myHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DOWN_START: // 开始下载
int filesize = (Integer) msg.obj;
onDownloadListener.onDownloadConnect(filesize);
break;
case DOWN_POSITION: // 下载位置
int pos = (Integer) msg.obj;
onDownloadListener.onDownloadUpdate(pos);
break;
case DOWN_COMPLETE: // 下载完成
String url = (String) msg.obj;
onDownloadListener.onDownloadComplete(url);
break;
case DOWN_ERROR: // 下载失败
Exception e = (Exception) msg.obj;
e.printStackTrace();
onDownloadListener.onDownloadError(e);
break;
}
super.handleMessage(msg);
}
};

class MyRunnable implements Runnable {
private String url = "";
private String filepath = "";

@Override
public void run() {
try {
doDownloadTheFile(url, filepath);
} catch (Exception e) {
sendMsg(DOWN_ERROR, e);
}
}
}

/**
* 下载文件
*
* @param url 下载路劲
* @param filepath 保存路径
* @throws Exception
*/

private void doDownloadTheFile(String url, String filepath) throws Exception {
if (!URLUtil.isNetworkUrl(url)) {
sendMsg(DOWN_ERROR, new Exception("不是有效的下载地址:" + url));
return;
}
URL myUrl = new URL(url);
URLConnection conn = myUrl.openConnection();
conn.connect();
InputStream is = null;
int filesize = 0;
try {
is = conn.getInputStream();
filesize = conn.getContentLength();// 根据响应获取文件大小
sendMsg(DOWN_START, filesize);
} catch (Exception e) {
sendMsg(DOWN_ERROR, new Exception(new Exception("无法获取文件")));
return;
}
FileOutputStream fos = new FileOutputStream(filepath); // 创建写入文件内存流,
// 通过此流向目标写文件
byte buf[] = new byte[1024];
int numread = 0;
int temp = 0;
while ((numread = is.read(buf)) != -1) {
fos.write(buf, 0, numread);
fos.flush();
temp += numread;
sendMsg(DOWN_POSITION, temp);
}
is.close();
fos.close();
sendMsg(DOWN_COMPLETE, filepath);
}

interface OnDownloadListener{
public void onDownloadUpdate(int percent);

public void onDownloadError(Exception e);

public void onDownloadConnect(int filesize);

public void onDownloadComplete(Object result);
}
}

2、调用DonwloadUtils进行下载,下载完成后加载本地图片


//1. 下载gif图片(文件名自定义可以通过Hash值作为key)
DownloadUtils downloadUtils = new DownloadUtils();
downloadUtils.download(gifUrlArray[0],
getDiskCacheDir(getContext())+"/0.gif");
//2. 下载完毕后通过“GifDrawable”进行显示
downloadUtils.setOnDownloadListener(new DownloadUtils.OnDownloadListener() {
@Override
public void onDownloadUpdate(int percent) {
}
@Override
public void onDownloadError(Exception e) {
}
@Override
public void onDownloadConnect(int filesize) {
}
//下载完毕后进行显示
@Override
public void onDownloadComplete(Object result) {
try {
GifDrawable gifDrawable = new GifDrawable(getDiskCacheDir(getContext())+"/0.gif");
mGifOnlineImageView.setImageDrawable(gifDrawable);
} catch (IOException e) {
e.printStackTrace();
}
}
});

//获取缓存的路径
public String getDiskCacheDir(Context context) {
String cachePath = null;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
// 路径:/storage/emulated/0/Android/data/<application package>/cache
cachePath = context.getExternalCacheDir().getPath();
} else {
// 路径:/data/data/<application package>/cache
cachePath = context.getCacheDir().getPath();
}
return cachePath;
}

github地址:https://github.com/koral--/android-gif-drawable

转载请注明链接:http://blog.csdn.net/feather_wch/article/details/79558240

收起阅读 »

Andorid进阶二:LeakCanary源码分析,从头到尾搞个明白

四,ObjectWatcher 保留对象检查分析我们转到 ObjectWatcher 的 expectWeaklyReachable 方法看看@Synchronized override fun expectWeaklyReachable( watched...
继续阅读 »

四,ObjectWatcher 保留对象检查分析

我们转到 ObjectWatcher 的 expectWeaklyReachable 方法看看

@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
//是否启用 , AppWatcher 持有的ObjectWatcher 默认是启用的
if (!isEnabled()) {
return
}
///移除之前已经被回收的监听对象
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
//(1) 创建弱引用
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}

watchedObjects[key] = reference
checkRetainedExecutor.execute {
//(2)
moveToRetained(key)
}
}

继续分析源码中标注的地方。

(1) 创建弱引用

标注(1.2.4)处的代码是初始化的主要代码,创建要观察对象的弱引用,传入queue 作为gc 后的对象信息存储队列,WeakReference 中,当持有对象呗gc的时候,会将其包装对象压入队列中。可以在后续对该队列进行观察。

(2) moveToRetained(key),检查对应key对象的保留

作为Executor的runner 执行,在AppWatcher中,默认延迟五秒后执行该方法 查看源码分析

@Synchronized private fun moveToRetained(key: String) {
///移除已经被回收的观察对象
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
//记录泄漏时间
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
//回调泄漏监听
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}

从上述代码可知,ObjectWatcher 监测内存泄漏总共有以下几步

  1. 清除已经被内存回收的监听对象
  2. 创建弱引用,传入 ReferenceQueue 作为gc 信息保存队列
  3. 在延迟指定的时间后,再次检查针对的对象是否被回收(通过检查ReferenceQueue队列内有无该WeakReference实例)
  4. 检测到对象没有被回收后,回调 onObjectRetainedListeners 们的 onObjectRetained

五,dumpHeap,怎么个DumpHeap流程

(1.1)objectWatcher 添加 OnObjectRetainedListeners 监听

回到最初AppWatcher的 manualInstall 方法。 可以看到其中执行了loadLeakCanary 方法。 代码如下:

///(2)
LeakCanaryDelegate.loadLeakCanary(application)
//反射获取InternalLeakCanary实例
val loadLeakCanary by lazy {
try {
val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
leakCanaryListener.getDeclaredField("INSTANCE")
.get(null) as (Application) -> Unit
} catch (ignored: Throwable) {
NoLeakCanary
}
}

该方法通过反射获取了 InternalLeakCanary 的静态实例。 并且调用了他的 invoke(application: Application)方法,所以我们接下来看InternalLeakCanary的该方法:

override fun invoke(application: Application) {
_application = application

checkRunningInDebuggableBuild()
//(1.2)添加 addOnObjectRetainedListener
AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))
//Gc触发器
val gcTrigger = GcTrigger.Default

val configProvider = { LeakCanary.config }

val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper)
///(1.3)
heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
configProvider
)
///(1.4) 添加application前后台变化监听
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
//(1.5)
registerResumedActivityListener(application)
//(1.6)
addDynamicShortcut(application)

// 6 判断是否应该DumpHeap
// We post so that the log happens after Application.onCreate()
mainHandler.post {
// https://github.com/square/leakcanary/issues/1981
// We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref
// which blocks until loaded and that creates a StrictMode violation.
backgroundHandler.post {
SharkLog.d {
when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text)
is Nope -> application.getString(
R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
)
}
}
}
}
}

我们看到初始化的时候做了这么6步

  • (1.2) 将自己加入到ObjectWatcher 的对象异常持有监听器中
  • (1.3)创建内存快照转储触发器 HeapDumpTrigger
  • (1.4)监听application 前后台变动,并且记录来到后台时间,便于LeakCanary 针对刚刚切入后台的一些destroy操作做泄漏监测
  • (1.5)注册activity生命周期回调,获取当前resumed的activity实例
  • (1.6)添加动态的桌面快捷入口
  • (1.7)在异步线程中,判断是否处于可dumpHeap的状态,如果处于触发一次内存泄漏检查 其中最重要的是 1.2,我们重点分析作为ObjectRetainedListener 他在回调中做了哪些工作。

(1.2)添加对象异常持有监听

可以看到代码(1.2),在objectWatcher将自己加入到泄漏监测回调中。 当ObjectWatcher监测到对象依然被异常持有的时候,会回调 onObjectRetained 方法。 从源码中可知,其中调用了 heapDumpTrigger的 scheduleRetainedObjectCheck方法, 代码如下。

fun scheduleRetainedObjectCheck() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.scheduleRetainedObjectCheck()
}
}

HeapDumpTrigger 顾名思义,就是内存快照转储的触发器。在回调中最终调用了HeapDumpTrigger 的 checkRetainedObjects方法来检查内存泄漏。

(1.3)检查内存泄漏checkRetainedObjects

private fun checkRetainedObjects() {
val iCanHasHeap = HeapDumpControl.iCanHasHeap()

val config = configProvider()
//省略一些代码,主要是判断 iCanHasHeap。
//如果当前处于不dump内存快照的状态,就先不处理。如果有新的异常持有对象被发现则发送通知提示
//%d retained objects, tap to dump heap
/** ...*/

var retainedReferenceCount = objectWatcher.retainedObjectCount

//主动触发gc
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
//重新获取异常持有对象
retainedReferenceCount = objectWatcher.retainedObjectCount
}
//如果泄漏数量小于阈值,且app在前台,或者刚转入后台,就展示泄漏通知,并先返回
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

//如果泄漏数量到达dumpHeap要求,继续往下
///转储内存快照在 WAIT_BETWEEN_HEAP_DUMPS_MILLIS (默认60秒)只会触发一次,如果之前刚触发过,就先不生成内存快照,直接发送通知了事。
//省略转储快照时机判断,不满足的话会提示 Last heap dump was less than a minute ago
/**...*/

dismissRetainedCountNotification()
val visibility = if (applicationVisible) "visible" else "not visible"
///转储内存快照
dumpHeap(
retainedReferenceCount = retainedReferenceCount,
retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility"
)
}

这一块也可以看出检测是否需要dumpHeap分为4步。

  1. 如果没有检测到异常持有的对象,返回
  2. 如果有异常对象,主动触发gc
  3. 如果还有异常对象,就是内存泄漏了。
  4. 判断泄漏数量是否到达需要dump的地步
  5. 判断一分钟内是否叫进行过dump了
  6. dumpHeap 前面都是判断代码,关键重点在于dumpHeap方法

(1.4)dumpHeap 转储内存快照

private fun dumpHeap(
retainedReferenceCount: Int,
retry: Boolean,
reason: String
) {
saveResourceIdNamesToMemory()
val heapDumpUptimeMillis = SystemClock.uptimeMillis()
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
when (val heapDumpResult = heapDumper.dumpHeap()) {
is NoHeapDump -> {
//省略 dump失败,等待重试代码和发送失败通知代码
}
is HeapDump -> {
lastDisplayedRetainedObjectCount = 0
lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
///清除 objectWatcher 中,在heapDumpUptimeMillis之前持有的对象,也就是已经dump的对象
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
// 发送文件到HeapAnalyzerService解析
HeapAnalyzerService.runAnalysis(
context = application,
heapDumpFile = heapDumpResult.file,
heapDumpDurationMillis = heapDumpResult.durationMillis,
heapDumpReason = reason
)
}
}
}

HeapDumpTrigger#dumpHeap中调用到了 AndroidHeapDumper#dumpHeap方法。 并且在dump后马上调用 HeapAnalyzerService.runAnalysis 进行内存分析工作,该方法在下一节分析。先看AndroidHeapDumper#dumHeap源码

override fun dumpHeap(): DumpHeapResult {
//创建新的hprof 文件
val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump

val waitingForToast = FutureResult<Toast?>()
///展示dump吐司
showToast(waitingForToast)

///如果展示吐司时间超过五秒,就不dump了
if (!waitingForToast.wait(5, SECONDS)) {
SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
return NoHeapDump
}

//省略dumpHeap通知栏提示消息代码
val toast = waitingForToast.get()

return try {
val durationMillis = measureDurationMillis {
//调用DumpHprofData
Debug.dumpHprofData(heapDumpFile.absolutePath)
}
if (heapDumpFile.length() == 0L) {
SharkLog.d { "Dumped heap file is 0 byte length" }
NoHeapDump
} else {
HeapDump(file = heapDumpFile, durationMillis = durationMillis)
}
} catch (e: Exception) {
SharkLog.d(e) { "Could not dump heap" }
// Abort heap dump
NoHeapDump
} finally {
cancelToast(toast)
notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
}
}

在该方法内,最终调用 Debug.dumpHprofData 方法 完成hprof 快照的生成。

六,分析内存 HeapAnalyzerService

上面代码分析中可以看到,在dumpHeap后紧跟着就是启动内存分析服务的方法。 现在我们跳转到HeapAnalyzerService的源码处。

override fun onHandleIntentInForeground(intent: Intent?) {
//省略参数获取代码
val config = LeakCanary.config
val heapAnalysis = if (heapDumpFile.exists()) {
analyzeHeap(heapDumpFile, config)
} else {
missingFileFailure(heapDumpFile)
}
//省略完善分析结果属性的代码
onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
}

可以看到重点在于 analyzeHeap,其中调用了 HeapAnalyzer#analyze HeapAnalyzer 类位于shark模块中。

(1)HeapAnalyzer#analyze

内存分析方法代码如下:

fun analyze(
heapDumpFile: File,
leakingObjectFinder: LeakingObjectFinder,
referenceMatchers: List<ReferenceMatcher> = emptyList(),
computeRetainedHeapSize: Boolean = false,
objectInspectors: List<ObjectInspector> = emptyList(),
metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
proguardMapping: ProguardMapping? = null
): HeapAnalysis {

//省略内存快照文件不存在的处理代码

return try {
listener.onAnalysisProgress(PARSING_HEAP_DUMP)
///io读取 内存快照
val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile))
sourceProvider.openHeapGraph(proguardMapping).use { graph ->
val helpers =
FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
//关键代码:在此处找到泄漏的结果以及其对应调用栈
val result = helpers.analyzeGraph(
metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
)
val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
///io读取状态
val randomAccessStats =
"RandomAccess[" +
"bytes=${sourceProvider.randomAccessByteReads}," +
"reads=${sourceProvider.randomAccessReadCount}," +
"travel=${sourceProvider.randomAccessByteTravel}," +
"range=${sourceProvider.byteTravelRange}," +
"size=${heapDumpFile.length()}" +
"]"
val stats = "$lruCacheStats $randomAccessStats"
result.copy(metadata = result.metadata + ("Stats" to stats))
}
} catch (exception: Throwable) {
//省略异常处理
}
}

通过分析代码可知:分析内存快照分为以下5步:

  1. 读取hprof内存快照文件
  2. 找到LeakCanary 标记的泄漏对象们的数量和弱引用包装 ids,class name 为com.squareup.leakcanary.KeyedWeakReference

代码在 KeyedWeakReferenceFinder#findLeakingObjectIds

  1. 找到泄漏对象的gcRoot开始的路径

代码在PathFinder#findPathsFromGcRoots

  1. 返回分析结果,走结果回调
  2. 回调内 展示内存分析成功或者失败的通知栏消息,并将泄漏列表存储到数据库中

详情代码看 DefaultOnHeapAnalyzedListener#onHeapAnalyzed 以及 LeaksDbHelper

  1. 点开通知栏跳转到LeaksActivity 展示内存泄漏信息。

七,总结

终于从头到尾,总算是梳理了一波LeakCanary 源码

过程中学习到了这么多—>

  • 主动调用Gc的方式 GcTrigger.Default.runGc()
Runtime.getRuntime().gc()
  • seald class 密封类来表达状态,比如以下几个(关键好处在于使用when可以直接覆盖所有情况,而不必使用else)。
sealed class ICanHazHeap {
object Yup : ICanHazHeap()
abstract class Nope(val reason: () -> String) : ICanHazHeap()
class SilentNope(reason: () -> String) : Nope(reason)
class NotifyingNope(reason: () -> String) : Nope(reason)
}
sealed class Result {
data class Done(
val analysis: HeapAnalysis,
val stripHeapDumpDurationMillis: Long? = null
) : Result()
data class Canceled(val cancelReason: String) : Result()
}
  • 了解了系统创建内存快照的api
 Debug.dumpHprofData(heapDumpFile.absolutePath)
  • 知道了通过 ReferenceQueue 检测内存对象是否被gc,之前WeakReference都很少用。
  • 学习了leakCanary的分模块思想。作为sdk,很多功能模块引入自动开启。比如 leakcanary-android-process 自动开启对应进程等。
  • 学习了通过反射hook代码,替换实例达成添加钩子的操作。比如在Service泄漏监听代码中,替换HandleractivityManager的操作。
收起阅读 »

Andorid进阶一:LeakCanary源码分析,从头到尾搞个明白

"内存优化会不会?知道怎么定位内存问题吗?"面试官和蔼地坐在小会议室的一侧,亲切地问有些拘谨地小张。"就是...那个,用LeakCanary 检测一下泄漏,然后找到对应泄漏的地方,把错误的代码改一下,没回收的引用回收掉,优化下长短生命周期线程的依赖关系吧""那...
继续阅读 »

"内存优化会不会?知道怎么定位内存问题吗?"面试官和蔼地坐在小会议室的一侧,亲切地问有些拘谨地小张。

"就是...那个,用LeakCanary 检测一下泄漏,然后找到对应泄漏的地方,把错误的代码改一下,没回收的引用回收掉,优化下长短生命周期线程的依赖关系吧"

"那你了解LeakCanary 分析内存泄漏的原理吗?"

"不好意思,平时没有注意去看过" 小张心想:面试怎么老问这个,我只是个普通的菜鸟啊。

前言

app性能优化总是开发中必不可少的一环,而其中内存优化又是重点之一。内存泄漏带来的内存溢出崩溃,内存抖动带来的卡顿不流畅。都在切切实实地影响着用户的体验。我们常常会使用LeakCanary来定位内存泄漏问题。也是时候来探索一下人家是怎么实现的了。

名词理解

hprof : hprof 文件是 Java 的 内存快照文件(Heap Profile 的缩写),格式后缀为 .hprof,在leakCanary 中用于内存保存分析 WeakReference : 弱引用,当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。在leakCanary 中用于监测该回收的无用对象是否被释放。 curtains:Square 的另一个开源框架,Curtains 提供了用于处理 Android 窗口的集中式 API。在leakCanary中用于监测window rootView 在detached 后的内存泄漏。

目录

本文主要从以下几点入手分析

  1. 如何在项目中使用 LeakCanary工具
  2. 官方原理说明
  3. 默认如何监听Activity ,view ,fragment 和 viewmodel
  4. Watcher.watch(object) 如何监听内存泄漏
  5. 如何保存内存泄漏内存文件
  6. 如何分析内存泄漏文件
  7. 展示内存泄漏堆栈到ui中 不支持在 Docs 外粘贴 block

一,怎么用?

查看官网文档 可以看出使用方法非常简单,基础用法只需要添加相关依赖就行

//(1)
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
复制代码

debugImplementation 只在debug模式的编译和最终的debug apk打包时有效 注(1):标注的代码中用了一行就实现了初始化,怎么做到的呢? 通过查看源码可以看到,leakcanary 通过 ContentProvider 进行初始化,在AppWatcherInstaller 类的oncreate方法中调用了真正的初始化代码AppWatcher.manualInstall(application)。在AndroidManifest.xml中注册该provider,注册的ContentProvider会在 application 启动的时候自动回调 oncreate方法。

internal sealed class AppWatcherInstaller : ContentProvider() {
/**[MainProcess] automatically sets up the LeakCanary code that runs in the main app process. */
// (1)
internal class MainProcess : AppWatcherInstaller()
internal class LeakCanaryProcess : AppWatcherInstaller()
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
///(2)
AppWatcher.manualInstall(application)
return true
}
//...
}
复制代码

说明一下源码中的数字标注

  1. 代码中定义了两个内部类继承自 AppWatcherInstaller。当用户额外依赖 leakcanary-android-process 模块的时候,自动在 process=":leakcanary" 也注册该provider。

代码参见 leakcanary-android-process 模块中的AndroidManifest.xml

  1. 这是真正的初始化代码注册入口

二,官方阐述

官方说明

本小节来自于官方网站的工作原理说明精简 安装 LeakCanary 后,它会通过 4 个步骤自动检测并报告内存泄漏:

  1. 检测被持有的对象

    LeakCanary 挂钩到 Android 生命周期以自动检测活动和片段何时被销毁并应进行垃圾收集。这些被销毁的对象被传递给一个ObjectWatcher,它持有对它们的弱引用。 可以主动观察一个不再需要的对象比如一个 dettached view 或者 已经销毁的 presenter

AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")
复制代码

如果ObjectWatcher等待 5 秒并运行垃圾收集后没有清除持有的弱引用,则被监视的对象被认为是保留的,并且可能会泄漏。LeakCanary 将此记录到 Logcat:

D LeakCanary: Watching instance of com.example.leakcanary.MainActivity
(Activity received Activity#onDestroy() callback)

... 5 seconds later ...

D LeakCanary: Scheduling check for retained objects because found new object
retained
复制代码
  1. Dumping the heap 转储堆信息到文件中

    当保留对象的数量达到阈值时,LeakCanary 将 Java 内存快照 dumping 转储到 Android 文件系统上的.hprof文件(堆内存快照)中。转储堆会在短时间内冻结应用程序,并展示下图的吐司: img

  2. 分析堆内存

    LeakCanary使用Shark解析.hprof文件并在该内存快照文件中定位被保留的泄漏对象。 对于每个保留对象,LeakCanary 找到该对象的引用路径,该引用阻止了垃圾收集器对它的回收。也就是泄漏跟踪。 LeakCanary为每个泄漏跟踪创建一个签名 (对持有的引用属性进行相加做sha1Hash),并将具有相同签名的泄漏(即由相同错误引起的泄漏)组合在一起。如何创建签名和通过签名分组有待后文分析。

  3. 分类内存泄漏

    LeakCanary 将它在您的应用中发现的泄漏分为两类:Application Leaks (应用程序泄漏)和Library Leaks(库泄漏)。一个Library Leaks是由已知的第三方库导致的,你没有控制权。这种泄漏正在影响您的应用程序,但不幸的是,修复它可能不在您的控制范围内,因此 LeakCanary 将其分离出来。 这两个类别分开Logcat结果中打印:

====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code

...
复制代码

LeakCanary在其泄漏列表展示中会将其用Library Leak 标签标记: img LeakCanary 附带一个已知泄漏的数据库,它通过引用名称的模式匹配来识别。例如:

Leak pattern: instance field android.app.Activity$1#this$0
Description: Android Q added a new IRequestFinishCallback$Stub class [...]
┬───
│ GC Root: Global variable in native code

├─ android.app.Activity$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of android.app.IRequestFinishCallback$Stub
│ ↓ Activity$1.this$0
│ ~~~~~~
╰→ com.example.MainActivity instance
复制代码

Library Leaks 通常我们都无力对齐进行修复 您可以在AndroidReferenceMatchers类中查看已知泄漏的完整列表。如果您发现无法识别的 Android SDK 泄漏,请报告。您还可以自定义已知库泄漏的列表

三,监测activity,fragment,rootView和viewmodel

前面提到初始化的代码如下,所以我们 查看manualInstall 的内部细节。

///初始化代码
AppWatcher.manualInstall(application)

///AppWatcher 的 manualInstall 代码
@JvmOverloads
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {
//*******检查是否为主线程********/
checkMainThread()
if (isInstalled) {
throw IllegalStateException(
"AppWatcher already installed, see exception cause for prior install call", installCause
)
}
check(retainedDelayMillis >= 0) {
"retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
}
installCause = RuntimeException("manualInstall() first called here")
this.retainedDelayMillis = retainedDelayMillis
if (application.isDebuggableBuild) {
LogcatSharkLog.install()
}
// Requires AppWatcher.objectWatcher to be set
///(2)
LeakCanaryDelegate.loadLeakCanary(application)
///(1)
watchersToInstall.forEach {
it.install()
}
}
复制代码

AppWatcher 作为Android 平台使用 ObjectWatcher 封装的api中心。自动安装配置默认的监听。 以上代码关键的地方用数字标出了

(1)Install 默认的监听观察

标注(1)处的代码执行了 InstallableWatcher 的 install 操作,在调用的时候并没有传递 watchersToInstall 参数,所以使用的是 appDefaultWatchers(application)。该处代码在下面,提供了 四个默认监听的Watcher

fun appDefaultWatchers(
application: Application,
///(1.1)
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
return listOf(
///(1.2)
ActivityWatcher(application, reachabilityWatcher),
///(1.3)
FragmentAndViewModelWatcher(application, reachabilityWatcher),
///(1.4)
RootViewWatcher(reachabilityWatcher),
///(1.5)
ServiceWatcher(reachabilityWatcher)
)
}
复制代码

用数字标出的四个我们逐个分析

(1.1) reachabilityWatcher 参数

标注(1.1)处的代码是一个 ReachabilityWatcher 参数,reachabilityWatcher 在后续的四个实例创建时候都有用到,代码中可以看到reachabilityWatcher实例是AppWatcher 的成员变量:objectWatcher,对应的实例化代码如下。

/**
* The [ObjectWatcher] used by AppWatcher to detect retained objects.
* Only set when [isInstalled] is true.
*/

val objectWatcher = ObjectWatcher(
clock = { SystemClock.uptimeMillis() },
checkRetainedExecutor = {
check(isInstalled) {
"AppWatcher not installed"
}
mainHandler.postDelayed(it, retainedDelayMillis)
},
isEnabled = { true }
)
复制代码

可以看到objectWatcher 是一个 ObjectWatcher对象,该对象负责检测持有对象的泄漏情况,会在第三小节进行分析。 回到 ActivityWatcher 实例的创建,继续往下看标注的代码

(1.2)ActivityWatcher 实例 完成Activity 实例的监听

回到之前,标注(1.2)处的代码创建了ActivityWatcher实例,并在install 的时候安装,查看ActivityWatcher 类的源码,看监听Activity泄漏是怎么实现的

class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val lifecycleCallbacks =
//(1.2.1) 通过动态代理,构造出生命周期回调的实现类
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
//(1.2.3)
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}

override fun install() {
//(1.2.3)
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}

override fun uninstall() {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
}
复制代码

(1.2.1) lifecycleCallbacks 实例

标注(1.2.1)处的代码创建了ActivityLifecycleCallbacks实例,该实例实现了Application.ActivityLifecycleCallbacks。通过 by ``*noOpDelegate*``() ,利用动态代理实现了其他回调方法,感兴趣的可以查看 noOpDelegate 的源码

(1.2.2) activity监听器的 install 方法

标注(1.2.2)处的代码是初始化的主要代码,该方法很简单,就是在application的 中注册 lifecycleCallbacks,在activity 被destroy 的时候会走到其中实现的方法

(1.2.3) 监听activity 的 onActivityDestroyed 回调

标注(1.2.3)处的代码是初始化的主要代码,在 activity被销毁的时候,回调该方法,在其中检查该实例是否有泄漏,调用AppWatcher.objectWatcher. expectWeaklyReachable 方法,在其中完成activity的泄漏监测。 这时候又回到了 1.1 提到的 ObjectWatcher源码,相关分析看第四节 。

(1.2-end)Activity监测相关总结

这样ActivityInstaller 就看完了,了解了Activity 的初始化代码以及加入监听的细节。总结一下分为如下几步:

  1. 调用ActivityInstaller.install 初始化方法
  2. 在Application 注册ActivityLifecycleCallbacks
  3. 在所有activity onDestroy的时候调用ObjectWatcher的 expectWeaklyReachable方法,检查过五秒后activity对象是否有被内存回收。标记内存泄漏。下一节分析。
  4. 检测到内存泄漏的后续操作。后文分析。

(1.3) FragmentAndViewModelWatcher 监测 Fragment 和Viewodel实例

(1.3)处是创建了 FragmentAndViewModelWatcher 实例。监测fragment和viewmodel的内存泄漏。

该类实现了 SupportFragment和 androidxFragment以及androidO 的兼容,作为sdk开发来说,这种 兼容方式可以学习一下。

private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
for (watcher in fragmentDestroyWatchers) {
watcher(activity)
}
}
}

override fun install() {
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}
复制代码

ActivityWatcher 同样的,install是注册了生命周期监听。不过是在对每个 activity create 的时候,交给 fragmentDestroyWatchers 元素们监听。所以 fragmentDestroyWatchers才是真正的fragmentviewmodel 监听者。 接下来看 fragmentDestroyWatchers 的元素们创建:

private val fragmentDestroyWatchers: List<(Activity) -> Unit> = run {
val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()

//(1.3.1) android框架自带的fragment泄漏监测支持从 AndroidO(26)开始。
if (SDK_INT >= O) {
fragmentDestroyWatchers.add(
AndroidOFragmentDestroyWatcher(reachabilityWatcher)
)
}
//(1.3.2)
getWatcherIfAvailable(
ANDROIDX_FRAGMENT_CLASS_NAME,
ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}
//(1.3.3)
getWatcherIfAvailable(
ANDROID_SUPPORT_FRAGMENT_CLASS_NAME,
ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}
fragmentDestroyWatchers
}
复制代码

可以看到内部创建了AndroidOFragmentDestroyWatcher 来针对Fragment 进行监听。原理是利用在 FragmentManager 中注册 FragmentManager.FragmentLifecycleCallbacks 来监听fragment 和 fragment.view 以及viewmodel 的实例泄漏。 从官方文档可知,android内部的 fragment 在Api 26中才添加。所以LeakCanary针对于android框架自带的fragment泄漏监测支持也是从 AndroidO(26)开始,见代码(1.3.1)。 标注的 1.3.1,1.3.2,1.3.3 实例化的三个Wathcer 分别是 AndroidOFragmentDestroyWatcher,AndroidXFragmentDestroyWatcher,AndroidSupportFragmentDestroyWatcher。内部实现代码大同小异,通过反射实例化不同的Watcher实现了androidX 和support 以及安卓版本间的兼容。

(1.3.1) AndroidOFragmentDestroyWatcher 实例

(1.3.1)处的代码添加了一个androidO的观察者实例。详情见代码,因为实现大同小异,分析参考1.3.2.

(1.3.2) AndroidXFragmentDestroyWatcher 实例

(1.3.2)处的代码 调用 getWatcherIfAvailable 通过反射创建了AndroidXFragmentDestroyWatcher实例,如果不存在Androidx库则返回null。 现在跳到 AndroidXFragmentDestroyWatcher 的源码分析

internal class AndroidXFragmentDestroyWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {

private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentCreated(
fm: FragmentManager,
fragment: Fragment,
savedInstanceState: Bundle?
) {
//(1.3.2.1)初始化 ViewModelClearedWatcher
ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
}

override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
//监测 fragment.view 的泄漏情况
val view = fragment.view
if (view != null) {
reachabilityWatcher.expectWeaklyReachable(
view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
"(references to its views should be cleared to prevent leaks)"
)
}
}

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
//监测 fragment 的泄漏情况
reachabilityWatcher.expectWeaklyReachable(
fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
)
}
}

///初始化,注册fragmentLifecycleCallbacks
override fun invoke(activity: Activity) {
if (activity is FragmentActivity) {
val supportFragmentManager = activity.supportFragmentManager
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
//注册activity的 viewModel 监听回调
ViewModelClearedWatcher.install(activity, reachabilityWatcher)
}
}
}
复制代码

通过源码可以看到,初始化该watcher是通过以下几步。

  1. FragmentManager.registerFragmentLifecycleCallbacks 注册监听回调
  2. ViewModelClearedWatcher.install 初始化了对于activity.viewModel的监听
  3. 在回调onFragmentCreated 中回调中使用ViewModelClearedWatcher.install注册了对于fragment.viewModel的监听。
  4. 在 onFragmentViewDestroyed 监听 fragment.view 的泄漏
  5. 在 onFragmentDestroyed 监听 fragment的泄漏。 监听方法和ActivityWatcher大同小异,不同是多了个 ViewModelClearedWatcher.install 。现在分析这一块的源码,也就是标注中的 (1.3.2.1)。
//该watcher 继承了ViewModel,生命周期被 ViewModelStoreOwner 管理。
internal class ViewModelClearedWatcher(
storeOwner: ViewModelStoreOwner,
private val reachabilityWatcher: ReachabilityWatcher
) : ViewModel() {

private val viewModelMap: Map<String, ViewModel>?

init {
//(1.3.2.3)通过反射获取所有的 store 存储的所有viewModelMap
viewModelMap = try {
val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
mMapField.isAccessible = true
@Suppress("UNCHECKED_CAST")
mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
} catch (ignored: Exception) {
null
}
}

override fun onCleared() {
///(1.3.2.4) viewmodle 被清理释放的时候回调,检查所有viewmodle 是否会有泄漏
viewModelMap?.values?.forEach { viewModel ->
reachabilityWatcher.expectWeaklyReachable(
viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
)
}
}

companion object {
fun install(
storeOwner: ViewModelStoreOwner,
reachabilityWatcher: ReachabilityWatcher
) {
val provider = ViewModelProvider(storeOwner, object : Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
ViewModelClearedWatcher(storeOwner, reachabilityWatcher) as T
})
///(1.3.2.2) 获取ViewModelClearedWatcher实例
provider.get(ViewModelClearedWatcher::class.java)
}
}
}
复制代码

通过代码,可以看到viewModel的泄漏监测是通过创建一个新的viewModel实例来实现。在该实例的onCleared处监听storeOwner的其余 viewModel 是否有泄漏。标注出的代码逐一分析:

(1.3.2.2 ) 处代码:

获取ViewModelClearedWatcher 实例,在自定义的 Factory中传入storeOwner 和 reachabilityWatcher。

(1.3.2.3 ) 处代码:

通过反射获取storeOwner 的viewModelMap

(1.3.2.4 ) 处代码:

在ViewModel完成使命OnClear的时候,开始监测storeOwner旗下所有ViewModel的内存泄漏情况。

(1.3-end)Fragment 和 viewmodel 监测泄漏总结:

监测方式都是通过ObjectWatcher的 expectWeaklyReachable 方法进行。fragment 利用FragmentLifecyclerCallback回调注册实现,ViewModel 则是在对应StoreOwner下创建了监测viewModel来实现生命周期的响应。 其中我们也能学习到通过反射来创建对应的平台兼容实现对象方式。以及借助创建viewModel来监听其余ViewModel生命周期的想法。

(1.4) RootViewWatcher 的源码分析

默认的四个Watcher中,来到了接下来的 RootViewWatcher。window rootview 监听依赖了squre自家的Curtains框架。

implementation "com.squareup.curtains:curtains:1.0.1"
复制代码

类的关键源码如下:

 private val listener = OnRootViewAddedListener { rootView ->
//如果是 Dialog TOOLTIP, TOAST, UNKNOWN 等类型的windows
//trackDetached 为true
if (trackDetached) {
rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {

val watchDetachedView = Runnable {
reachabilityWatcher.expectWeaklyReachable(
rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
)
}

override fun onViewAttachedToWindow(v: View) {
mainHandler.removeCallbacks(watchDetachedView)
}

override fun onViewDetachedFromWindow(v: View) {
mainHandler.post(watchDetachedView)
}
})
}
}

override fun install() {
Curtains.onRootViewsChangedListeners += listener
}

override fun uninstall() {
Curtains.onRootViewsChangedListeners -= listener
}
}
复制代码

看到关键代码,就是 在Curtains中添加onRootViewsChangedListeners 监听器。当windowsType类型为 **Dialog** ***TOOLTIP***, ***TOAST*****,**或者未知的时候 ,在 onViewDetachedFromWindow 的时候监听泄漏情况。 Curtains中的监听器会在windows rootView 变化的时候被全局调用。Curtains是squareup 的另一个开源库,Curtains 提供了用于处理 Android 窗口的集中式 API。具体移步他的官方仓库

(1.5) ServiceWatcher 监听Service内存泄漏

接下来就是AppWatcher中的最后一个Watcher。 ServiceWatcher。代码比较长,截取关键点分析。

(1.5.1)先看成员变量 activityThreadServices :

private val servicesToBeDestroyed = WeakHashMap<IBinder, WeakReference<Service>>()
private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") }
private val activityThreadInstance by lazy {
activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null)!!
}

private val activityThreadServices by lazy {
val mServicesField =
activityThreadClass.getDeclaredField("mServices").apply { isAccessible = true }

@Suppress("UNCHECKED_CAST")
mServicesField[activityThreadInstance] as Map<IBinder, Service>
}
复制代码

activityThreadServices 是个装了所有<IBinder, Service> 对的Map。代码中可以看到很粗暴地,直接通过反射从ActivityThread实例中拿到了mServices 变量 。赋值给activityThreadServices。 源码中有多个swap操作,在install的时候执行,主要目的是将原来的一些service相关生命周期回调加上一些钩子,用来监测内存泄漏,并且会在unInstall的时候给换回来。

(1.5.2)swapActivityThreadHandlerCallback :

拿到ActivityThread 的Handler,将其回调的 handleMessage,换成加了料的Handler.Callback,加料代码如下

Handler.Callback { msg ->
if (msg.what == STOP_SERVICE) {
val key = msg.obj as IBinder
activityThreadServices[key]?.let {
onServicePreDestroy(key, it)
}
}
mCallback?.handleMessage(msg) ?: false
}
复制代码

代码中可以看到,主要是对于 STOP_SERVICE 的操作做了一个钩子,在之前执行 onServicePreDestroy。主要作用是为该service 创建一个弱引用,并且加到servicesToBeDestroyed[token] 中 。

(1.5.3)然后再看 swapActivityManager 方法。

该方法完成了将ActivityManager替换成IActivityManager的一个动态代理类。代码如下:

Proxy.newProxyInstance(
activityManagerInterface.classLoader, arrayOf(activityManagerInterface)
) { _, method, args ->
//private const val METHOD_SERVICE_DONE_EXECUTING = "serviceDoneExecuting"
if (METHOD_SERVICE_DONE_EXECUTING == method.name) {
val token = args!![0] as IBinder
if (servicesToBeDestroyed.containsKey(token)) {
///(1.5.3)
onServiceDestroyed(token)
}
}
try {
if (args == null) {
method.invoke(activityManagerInstance)
} else {
method.invoke(activityManagerInstance, *args)
}
} catch (invocationException: InvocationTargetException) {
throw invocationException.targetException
}
}
复制代码

代码所示,替换后的ActivityManager 在调用serviceDoneExecuting 方法的时候添加了个钩子,如果该service在之前加入的servicesToBeDestroyed map中,则调用onServiceDestroyed 监测该service内存泄漏。

(1.5.4)代码的onServiceDestroyed具体代码如下

private fun onServiceDestroyed(token: IBinder) {
servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
serviceWeakReference.get()?.let { service ->
reachabilityWatcher.expectWeaklyReachable(
service, "${service::class.java.name} received Service#onDestroy() callback"
)
}
}
}
复制代码

这里面的代码很熟悉,和之前监测activity等是一样的。 回到swapActivityManager方法,看代理ActivityManager的具体类型。 可以看到代理的对象如下面代码所示,根据版本不同可能是ActivityManager 实例或者是ActivityManagerNative实例。 代理的接口是 Class.forName("android.app.IActivityManager")

val (className, fieldName) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
"android.app.ActivityManager" to "IActivityManagerSingleton"
} else {
"android.app.ActivityManagerNative" to "gDefault"
}
复制代码

(1.5-end)Service 泄漏监测总结

总结一下,service的泄漏分析通过加钩子的方式,对一些系统执行做了监听。主要分为以下几步:

  1. 获取ActivityThread中mService变量,得到service实例的引用
  2. 通过swapActivityThreadHandlerCallback 在ActivityThread 的 Handler.sendMessage 中添加钩子,在执行到msg.what == STOP_SERVICE 的时候


收起阅读 »

Android Compose 初探!

使用前的准备工作android studio Arctic Fox版本或更新的版本如果是一个新项目,可以在创建的时候,新建一个Empty Compose Activity在module的build.gradle文件中添加android { buildF...
继续阅读 »

使用前的准备工作

  1. android studio Arctic Fox版本或更新的版本

  2. 如果是一个新项目,可以在创建的时候,新建一个Empty Compose Activity

    image.png

  3. 在module的build.gradle文件中添加

    android {
    buildFeatures {
    compose true
    }
    composeOptions {
    kotlinCompilerExtensionVersion compose_version
    kotlinCompilerVersion '1.4.32'
    }
    }
    dependencies {
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
    }

需要添加

buildFeatures {
compose true
}

组件

组件的定义

在Compose中一个UI组件就是一个带有@Composable注解的函数

@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

布局组件

如果没有采用布局组件,直接单视图写到一个Compose中,会存在异常的情况。官方是这么说的:

A Composable function might emit several UI elements. However, if you don't provide guidance on how they should be arranged, Compose might arrange the elements in a way you don't like

  • Row 横向排列视图, Row的相关属性如下:
    inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
    )
  • Column 纵向排列视图, 其属性和上面的Row类似
  • Box 将一个元素覆盖在另一个上面, 类似于FrameLayout这种

视图组件

  • Text 类似于原生View中的TextView
  • Button 按钮
  • LazyColumn 类似于原生RecyclerView
  • Image 图片控件 关于网络图片,可以采用Coil框架
  • TextField 文件输入框
  • Surface用来控制组件的背景,边框,文本颜色等
  • AlertDialog 弹窗控件,类似于原生View中的AlertDialog

组件的状态管理

remember

通过remember来记录组件某些相关属性值,当属性发生变化,会自动触发UI的更新。

@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var nameState = remember { mutableStateOf("") }
var name = nameState.value;
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
TextField(
value = name,
onValueChange = { println("data----->$it");nameState.value = it }
)
}
}

这段代码实现的功能就是当用户在一个输入框中输入文字的时候,即时回显在页面上。当采用这种方式编码时,状态是耦合在组件中,当调用者不关心内部的状态的,这种方式是ok的,但它的弊端就是不利于组件的复用。我们可以将状态和组件分离开,此时,便就是利用状态提升(state hoisting)的手段

@Composable
fun HelloScreen() {
var nameState = remember { mutableStateOf("") }
HelloContent(name = nameState.value, onNameChange = { nameState.value = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
TextField(
value = name,
onValueChange = { onNameChange(it) }
)
}
}

这里是将状态提到HelloContent的外面, 方面HelloContent组件的复用

rememberSaveable

remember类似,区别在于rememberSaveable进行状态管理时,当activity或进程重新创建了(如屏幕旋转),其状态信息不会丢失。 将上面的var nameState = remember { mutableStateOf("") } 中的remember换成rememberSaveable就可以了

ViewModel

可以利用ViewModel进行全局的状态管理

class HelloViewModel : ViewModel() {

// LiveData holds state which is observed by the UI
// (state flows down from ViewModel)
private val _name = MutableLiveData("")
val name: LiveData = _name

// onNameChange is an event we're defining that the UI can invoke
// (events flow up from UI)
fun onNameChange(newName: String) {
_name.value = newName
}
}

@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
// by default, viewModel() follows the Lifecycle as the Activity or Fragment
// that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen.

// name is the current value of [helloViewModel.name]
// with an initial value of ""
val name: String by helloViewModel.name.observeAsState("")
HelloContent(name = name, onNameChange = { helloViewModel.onNameChange(it) })
}

Modifers

Modifers是用来装饰composable, Modifiers用来告诉一个UI元素如何布局,显示,和相关的行为。

布局相关的属性

  • fillMaxWidth
  • matchParentSize
  • height
  • width
  • padding
  • size

显示

  • background
  • clip: 如Modifier.clip(RoundedCornerShape(4.dp)),一个圆角便出来了

绑定事件

利用clickable来绑定事件

Row(
Modifier
.fillMaxWidth()
.clickable { onClick(); },
verticalAlignment = Alignment.CenterVertically
) {
...
}

实例

采用Compose方案的开发体验非常接近于用Vue或React, 代码结构非常清晰,不用xml来画UI确实省了不少事,以下是一段代码片断来画一个微信的个人中心页

image.png

@Preview(showBackground = true)
@Composable
fun PersonalCenter() {
Column() {
Header("Hello World", "Wechat_0001")
Divider(
Modifier
.fillMaxWidth()
.height(8.dp), GrayBg
)
RowList()
Divider(
Modifier
.fillMaxHeight(), GrayBg
)
}
}

@Composable
fun Header(nickName: String, wechatNo: String) {
Row(
Modifier
.fillMaxWidth()
.padding(24.dp, 24.dp, 16.dp, 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "头像",
Modifier
.size(50.dp)
.clip(
RoundedCornerShape(4.dp)
)
)
Column() {
Text(nickName, Modifier.padding(12.dp, 2.dp, 0.dp, 0.dp), TextColor, fontSize = 18.sp)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"微信号 :$wechatNo",
Modifier
.padding(12.dp, 10.dp, 0.dp, 0.dp)
.weight(1.0f), TextColorGray, fontSize = 14.sp
)
Icon(painterResource(R.drawable.ic_qrcode), "二维码", Modifier.size(16.dp))
Icon(
painterResource(R.drawable.right_arrow_3),
contentDescription = "more",
Modifier.padding(12.dp, 0.dp, 0.dp, 0.dp)
)
}
}
}
}

@Composable
fun RowItem(@DrawableRes icon: Int, title: String, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
.clickable { onClick(); },
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(icon), contentDescription = title + "icon",
Modifier
.padding(16.dp, 12.dp, 16.dp, 12.dp)
.size(24.dp)
)
Text(title, Modifier.weight(1f), TextColor, fontSize = 15.sp)
Icon(
painterResource(R.drawable.right_arrow_3),
contentDescription = "more",
Modifier.padding(0.dp, 0.dp, 16.dp, 0.dp)
)
}
}

@Composable
fun RowList() {
var context = LocalContext.current;
Column() {
RowItem(icon = R.drawable.ic_pay, title = "支付") { onItemClick(context, "payment") }
Divider(
Modifier
.fillMaxWidth()
.height(8.dp), GrayBg
)
RowItem(icon = R.drawable.ic_collections, title = "收藏") {
onItemClick(context, "收藏")
}
Divider(
Modifier
.fillMaxWidth()
.padding(56.dp, 0.dp, 0.dp, 0.dp)
.height(0.2.dp), GrayBg
)
RowItem(icon = R.drawable.ic_photos, title = "相册") {
onItemClick(context, "相册")
}
Divider(
Modifier
.fillMaxWidth()
.padding(56.dp, 0.dp, 0.dp, 0.dp)
.height(0.2.dp), GrayBg
)
RowItem(icon = R.drawable.ic_cards, title = "卡包") {
Toast.makeText(context, "payment", Toast.LENGTH_SHORT).show()
}
Divider(
Modifier
.fillMaxWidth()
.padding(56.dp, 0.dp, 0.dp, 0.dp)
.height(0.2.dp), GrayBg
)
RowItem(icon = R.drawable.ic_stickers, title = "表情") {
Toast.makeText(context, "payment", Toast.LENGTH_SHORT).show()
}
Divider(
Modifier
.fillMaxWidth()
.height(8.dp), GrayBg
)
RowItem(icon = R.drawable.ic_settings, title = "设置") {
Toast.makeText(context, "payment", Toast.LENGTH_SHORT).show()
}
}
}

fun onItemClick(context: Context, data: String) {
Toast.makeText(context, data, Toast.LENGTH_SHORT).show()
}

View中嵌Compose

var view = LinearLayout(this)
view.addView(ComposeView(this).apply {
setContent {
PersonalCenter();
}
})

Compose中嵌View

@Compose
fun RowList() {
...
AndroidView({View(context)}, Modifier.width(20.dp).height(20.dp).background(Color.Green)){}
...
}

总结

  • Compose使用了一套新的布局,渲染机制, 它里面的元素和我们以前写的各种View是有区别的,比如Compose里面的Text并不是我们以前认识的TextView或其它的原生控件, 它采用了更底层的api来实现
  • 数据的自动订阅(完成双向绑定)
  • 声明式UI: compose通过自动订阅机制来完成UI的自动更新
  • compose和现有的原生View混用
收起阅读 »

Android 安卓超级强劲的轻量级数据库ObjectBox,快的飞起

文章目录 ObjectBox 引入ObjectBox 简单的代码栗子 生成和创建数据库 ObjectBox初始化 基本操作 - 增 基本操作 - ...
继续阅读 »


在这里插入图片描述







ObjectBox


ObjectBox是一个超快的面向对象数据库,相比于Sqlite,效率高了10倍左右






引入ObjectBox


在跟项目中的build.gradle中引入:


buildscript {
...
ext.objectboxVersion = '2.9.1'

dependencies {
...
classpath "io.objectbox:objectbox-gradle-plugin:$objectboxVersion"
...
}
}

在app下的build.gradle头部引入


(有以下两种情况,看你项目中用的什么):


plugins {
...
id 'io.objectbox'
}

apply plugin: 'io.objectbox'





简单的代码栗子


接下来将会讲解ObjectBox基本使用






生成和创建数据库


1、新建一个模型类,并使用 @Entity 将类注解,@Id 为自增主键(进阶的代码栗子会详细一点讲注解),@Id 注解也是必不可少的。


package com.mt.objectboxproject

import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id

@Entity
data class Person(
@Id
var id: Long = 0,
var age: Int = 0,
var name: String? = null
)



2、AndroidStudio操作:Build -> MakeProject,或者点击运行按钮旁边的小锤子锤一下,这一步是为了生成ObjectBox所需要的文件,之后便会看到生成了 app\objectbox-models\default.json 文件






ObjectBox初始化


1、创建ObjectBox的小助手,需要在 Application 中进行调用 init 初始化


package com.mt.objectboxproject

import android.content.Context
import io.objectbox.BoxStore

/**
* ObjectBox的小助手,需要在Application中进行调用init初始化
*/

object ObjectBox {
lateinit var store: BoxStore
private set

fun init(context: Context) {
store = MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
}



2、在 Application 中初始化


package com.mt.objectboxproject

import android.app.Application

class MainApplication : Application() {
override fun onCreate() {
super.onCreate()

//初始化ObjectBox
ObjectBox.init(this)
}
}



基本操作 - 增


package com.mt.objectboxproject

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

//插入一条数据
val userBox = ObjectBox.store.boxFor(Person::class.java)
val person = Person()
person.age = 21
person.name = "第三女神程忆难"
userBox.put(person)

//==========================================================================================

//插入多条数据
val persons = mutableListOf<Person>()

//模拟多条数据
val person1 = Person()
person1.age = 24
person1.name = "1bit"

val person2 = Person()
person2.age = 25
person2.name = "梦想橡皮擦"

val person3 = Person()
person3.age = 26
person3.name = "沉默王二"

persons.add(person1)
persons.add(person2)
persons.add(person3)

//插入数据库
userBox.put(persons)


}
}



基本操作 - 查


package com.mt.objectboxproject

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val userBox = ObjectBox.store.boxFor(Person::class.java)

//==========================================================================================
//根据主键id查询
val person = userBox[1]

//==========================================================================================
//获取person有所数据
val allPersons = userBox.all

//==========================================================================================
//条件查询
val build = userBox.query()
.equal(Person_.name, "沉默王二")
.order(Person_.name)
.build()

//查找数据
val find = build.find()

//记得close
build.close()

}
}



基本操作 - 删


package com.mt.objectboxproject

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val userBox = ObjectBox.store.boxFor(Person::class.java)

//==========================================================================================
//根据主键Id去删除
val isRemoved = userBox.remove(1)

//==========================================================================================
//根据主键id集合去删除
val ids = mutableListOf<Long>(1,2,3,4)
userBox.removeByIds(ids)

//==========================================================================================
//根据模型类去删除
val person = userBox[1]
person.name = "第三女神程忆难"
userBox.remove(person)

//==========================================================================================
//删除所有数据
userBox.removeAll()

}
}



基本操作 - 改


package com.mt.objectboxproject

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val userBox = ObjectBox.store.boxFor(Person::class.java)

//==========================================================================================
//先查询获取到person,set值后重新put
val person = userBox[1]
person.name = "小傅哥"
userBox.put(person)
}
}





进阶的代码栗子


注解讲解






注解



  • @Id:主键,默认为自增主键,交由ObjectBox管理

  • @Index:注释一个属性,为相应的数据库列创建一个数据库索引。这可以提高查询该属性时的性能。

  • @Transient:标记不应保留的属性。在 Java 中,静态或瞬态属性也不会被持久化。

  • @NameInDb:对数据库中字段名进行自定义。

  • @Backlink:反向关联。

  • @ToOne:一对一关联注解。

  • @ToMany:一对多关联注解。






ObjectBox地址:https://docs.objectbox.io

收起阅读 »

简阅-一个以Kotlin实现的第三方聚合阅读App开源啦

简阅(SimpleRead)以Kotlin实现的简单纯净的阅读软件,主要使用到MVP+RxJava+Retrofit+RxLifecycle+Glide+GreenDao等技术软件开发背景简阅是我学习安卓开发的第一个项目,最初是使用传统的MVC模式,然后一步步...
继续阅读 »

简阅(SimpleRead)

以Kotlin实现的简单纯净的阅读软件,主要使用到MVP+RxJava+Retrofit+RxLifecycle+Glide+GreenDao等技术

软件开发背景

简阅是我学习安卓开发的第一个项目,最初是使用传统的MVC模式,然后一步步迭代,由MVP再到Kotlin.如今项目功能已经基本稳定,我将项目规范了下, 然后开源供大家交流学习,毕竟当时学习也看了很多前辈的项目,学到了很多,所以现在是时候回报开源社区啦。

软件地址

酷安下载地址

软件截图

  

实现的功能

知乎日报
  • 获取知乎日报最新新闻
  • 上拉加载前一天知乎新闻
  • 可选择阅读具体某天的知乎新闻
  • 可随机阅读一篇知乎新闻
  • 已读新闻灰显
  • 收藏/取消收藏某一篇新闻
  • 分享新闻
煎蛋新鲜事
  • 获取最新煎蛋新鲜事
  • 上拉加载前一天新鲜事
  • 已读新闻灰显
  • 收藏/取消收藏某一篇新闻
  • 分享新闻
每日一文
  • 查看当天的文章
  • 随机一篇文章
  • 三种阅读风格切换

其余

  • 遵循Material Design设计
  • 多种主题选择
  • Frament懒加载
  • 网络缓存
  • 离线缓存

技术慨要

  • MVP

    MVP是目前安卓开发中最流行的架构之一,Model负责数据和业务逻辑,View层负责view相关的展示以及context层的调用,Presenter层负责使M层和V层交互

  • RxJava

    RxJava是一个基于事件流的异步响应框架

    给 Android 开发者的 RxJava 详解 -- 扔物线

  • Retrofit

    RESTful的HTTP网络请求框架,优势在于可以结合RxJava实现链式网络请求以及轻松实现线程调度,同时它是以注解的方式标注请求,优雅简洁

  • RxLifecycle

    RxLifecycle是知乎团队出的一个方便取消RxJava订阅的库,使用它结合RxJava无需再到onDestory()中取消订阅

  • GreenDao

    GreenDao是一个老牌ORM数据库框架,目前3.2.2版本可以说是最值得使用的ORM框架

  • Glide

    一个API简洁但是功能极为强大的图片加载框架

  • jsoup

    jsoup是一个强大的解析html网页源码的库

  • BaseRecyclerViewAdapterHelper

    一个快速实现RecyclerviewAdapter的库,和普通写法比起来能减少70%代码量

  • 其余还有一些相关技术就不一一罗列出来了,大家可以自行查看源码.

收起阅读 »

Android资源冲突检测插件

背景 之前我们写了一篇定义关于如何定义Gradle插件,有兴趣的朋友可以看一下,今天我们就来简单讲一个自定义Gradle插件的实战Android项目Module间资源冲突检测的Gradle插件。对应的使用方法和源码已经在GitHub给出Android资源冲突检...
继续阅读 »

背景


之前我们写了一篇定义关于如何定义Gradle插件,有兴趣的朋友可以看一下,今天我们就来简单讲一个自定义Gradle插件的实战Android项目Module间资源冲突检测的Gradle插件。对应的使用方法和源码已经在GitHub给出Android资源冲突检测插件


解决问题


具体我们的插件的作用是干什么的呢?这里简单解释一下,就是当我们的项目越来越大的时候我们会将项目拆分为多个Module,这个时候,每个Module里面都有自己的资源文件,包括图片,文字,颜色,字体大小等。如果我们在多个Module里面定义了相同名字的资源,但是对应的资源内容不一样,这个时候项目并不会出错,但是当我们最终打包的时候多个Module中的资源只会留下一个,这样我们想要的效果就会出错。这个插件就是用来跑整个项目所有Module将有冲突的资源提取出来,目前只支持String,Color,Dimen,其他的会在后续补充。


实现方式


首先,我们先接着自定义Gradle插件的思路往下讲,关于自定义Gradle的一些基本知识:

大家也可以查看:Gradle官方文档

或者查看我的上一篇:如何定义Gradle插件

1、先定义一个我们自己的Task,继承DefaultTask,用来接收一些参数


public class GeekTask extends DefaultTask {
private boolean strings;
private boolean colors;
private boolean dimens;
public boolean getStringFlag() {
return strings;
}
@Input
public void checkString(boolean flag) {
this.strings = flag;
}

public boolean getColorFlag() {
return colors;
}
@Input
public void checkColors(boolean flag) {
this.colors = flag;
}

@Input
public void checkDimens(boolean flag){
this.dimens = flag;
}

public boolean getDimensFlag(){
return dimens;
}
@TaskAction
void sayGreeting() {
System.out.printf("%s, %s!\n", getStringFlag(), getColorFlag());
}

}

2、我们怎么去调用我们定义的这个Task呢?


 @Override
void apply(Project project) {
GeekTask task = project.getTasks().create("checkResources", GeekTask.class)
}

其中checkResources是我们定义的Task的名称,后面我们可以调用它。


checkResources{
checkString true
checkColors true
checkDimens true
}

这个是定义在我们需要使用我们自己写的插件的Module对应的Gradle文件里面的checkResources表示Task的名字,下面的是对应的方法和参数。当然,在这个Gradle里面需要引用我们的插件apply plugin: '插件名字'

3、使用传递进来的参数。


GeekTask task = project.getTasks().create("checkResources", GeekTask.class)
task.doLast {
System.out.println(task.getStringFlag())
if (task.getStringFlag()) {
// do check string
}
if (task.getColorFlag()) {
// do check color
}
if (task.getDimensFlag()) {
// do check dimen
}
}

上面我们通过我们定义的task就可获取到,我们传递进来的参数,task.doLast这一步表示我们里面的代码执行在Task的之后保证可以获取到参数,这里稍微讲一下插件代码的运行时机:

如果我们直接写在apply()方法中的代码是执行的编译期,也就是一开始就执行了,是执行在任何之前的。

task.doFirst {}虽然也是在Task之前执行,但是它是在要执行Task的时候先执行doFirst里面的代码。

task.doLast{}这个是执行在Task执行之后的。

4、怎么实现资源检测。

这个代码比较简单主要是获取所有module下对应资源的文件,然后进行解析和比较,具体的代码这里就不写了,有兴趣的朋友可以下载完整代码Android资源冲突检测插件


如何使用


首先我们要在项目最外层的build.gradle里面引用我上传的项目


apply plugin: 'geekplugin'

其次加载其代码


classpath 'com.geek.check:AndroidResourceCheck:1.0.0'

这里注意是calsspath具体和compile的区别大家可以Google一下

然后设置参数,用来配置我们需要检测的资源


checkResources{
checkString true
checkColors true
checkDimens true
}

最后就是运行这个插件

我们可以在项目的根目录运行这个Task


gradle checkResources

如果我们有资源冲突文件,最后会在项目的跟目录生成ResourcesError目录,对应的冲突文件在里面,大家可以查看。


总结


好了,这个插件大概就这么多东西,相信大家通过这个也会对自定义Gradle插件有更深的一些认识,当然,这还只是一些皮毛,更深层次的使用还需要大家去研究,谁有更好的资料和建议也可以评论提出,我们一起进步。



作者:Only凹凸曼
链接:https://www.jianshu.com/p/9d2a047f2c22
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一文全面了解Android单元测试

前言 众所周知,一个好的项目需要不断地打造,而一些有效的测试则是加速这一过程的利器。本篇博文将带你了解并逐步深入Android单元测试。 什么是单元测试? 单元测试就是针对类中的某一个方法进行验证是否正确的过程,单元就是指独立的粒子,在Android...
继续阅读 »

前言


众所周知,一个好的项目需要不断地打造,而一些有效的测试则是加速这一过程的利器。本篇博文将带你了解并逐步深入Android单元测试。


什么是单元测试?




单元测试就是针对类中的某一个方法进行验证是否正确的过程,单元就是指独立的粒子,在Android和Java中大都是指方法。


为什么要进行单元测试?




使用单元测试可以提高开发效率,当项目随着迭代越来越大时,每一次编译、运行、打包、调试需要耗费的时间会随之上升,因此,使用单元测试可以不需这一步骤就可以对单个方法进行功能或逻辑测试。 同时,为了能测试每一个细分功能模块,需要将其相关代码抽成相应的方法封装起来,这也在一定程度上改善了代码的设计。因为是单个方法的测试,所以能更快地定位到bug。


单元测试case需要对这段业务逻辑进行验证。在验证的过程中,开发人员可以深度了解业务流程,同时新人来了看一下项目单元测试就知道哪个逻辑跑了多少函数,需要注意哪些边界——是的,单元测试做的好和文档一样具备业务指导能力。


Android测试的分类




Android测试主要分为三个方面:



  • 1)、单元测试(Junit4、Mockito、PowerMockito、Robolectric)

  • 2)、UI测试(Espresso、UI Automator)

  • 3)、压力测试(Monkey)


一、单元测试之基础Junit4




什么是Junit4?




Junit4是事实上的Java标准测试库,并且它是JUnit框架有史以来的最大改进,其主要目标便是利用Java5的Annotation特性简化测试用例的编写。


开始使用Junit4进行单元测试




1.Android Studio已经自动集成了Junit4测试框架,如下


    dependencies {
...
testImplementation 'junit:junit:4.12'
}

2.Junit4框架使用时涉及到的重要注解如下


    @Test 指明这是一个测试方法 (@Test注解可以接受2个参数,一个是预期错误
expected,一个是超时时间timeout,
格式如 @Test(expected = IndexOutOfBoundsException.class),
@Test(timeout = 1000)
@Before 在所有测试方法之前执行
@After 在所有测试方法之后执行
@BeforeClass 在该类的所有测试方法和@Before方法之前执
行 (修饰的方法必须是静态的)@AfterClass 在该类的所有测试方法和@After
方法之后执行(修饰的方法必须是静态的)
@Ignore 忽略此单元测试

此外,很多时候,因为某些原因(比如正式代码还没有实现等),我们可能想让JUnit忽略某些方法,让它在跑所有测试方法的时候不要跑这个测试方法。要达到这个目的也很简单,只需要在要被忽略的测试方法前面加上@Ignore就可以了


3.主要的测试方法——断言


    assertEquals(expected, actual) 判断2个值是否相等,相等则测试通过。
assertEquals(expected, actual, tolerance) tolerance 偏差值

注意:上面的每一个方法,都有一个重载的方法,可以加一个String类型的参数,表示如果验证失败的话,将用这个字符串作为失败的结果报告


4.自定义Junit Rule——实现TestRule接口并重写apply方法


    public class JsonChaoRule implements TestRule {

@Override
public Statement apply(final Statement base, final Description description) {
Statement repeatStatement = new Statement() {
@Override
public void evaluate() throws Throwable {
//测试前的初始化工作
//执行测试方法
base.evaluate();
//测试后的释放资源等工作
}
};
return repeatStatement;
}
}

然后在想要的测试类中使用@Rule注解声明使用JsonChaoRule即可(注意被@Rule注解的变量必须是final的):


    @Rule
public final JsonChaoRule repeatRule = new JsonChaoRule();

5.开始上手,使用Junit4进行单元测试



  • 1.编写测试类。

  • 2.鼠标右键点击测试类,选择选择Go To->Test (或者使用快捷键Ctrl+Shift+T,此快捷键可 以在方法和测试方法之间来回切换)在Test/java/项目 测试文件夹/下自动生成测试模板。

  • 3.使用断言(assertEqual、assertEqualArrayEquals等等)进行单元测试。

  • 4.右键点击测试类,Run编写好的测试类。


6.使用Android Studio自带的Gradle脚本自动化单元测试


点击Android Studio中的Gradle projects下的app/Tasks/verification/test即可同时测试module下所有的测试类(案例),并在module下的build/reports/tests/下生成对应的index.html测试报告


7.对Junit4的总结:



  • 优点:速度快,支持代码覆盖率等代码质量的检测工具,

  • 缺点:无法单独对Android UI,一些类进行操作,与原生JAVA有一些差异。


可能涉及到的额外的概念:


打桩方法:使方法简单快速地返回一个有效的结果。


测试驱动开发:编写测试,实现功能使测试通过,然后不断地使用这种方式实现功能的快速迭代开发。


二、单元测试之基础Mockito


什么是Mockito?


Mockito 是美味的 Java 单元测试 Mock 框架,mock可以模拟各种各样的对象,从而代替真正的对象做出希望的响应。


开始使用Mockito进行单元测试


1.在build.gradle里面添加Mcokito的依赖


    testImplementation 'org.mockito:mockito-core:2.7.1'

2.使用mock()方法模拟对象


    Person mPerson = mock(Person.class); 

能量补充站(-vov-)


在JUnit框架下,case(即每一个测试点,带@Test注解的那个函数)也是个函数,直接调用这个函数就不是case,和case是无关的,两者并不会相互影响,可以直接调用以减少重复代码。单元测试不应该对某一个条件过度耦合,因此,需要用mock解除耦合,直接mock出网络请求得到的数据,单独验证页面对数据的响应。


3.验证方法的调用,指定方法的返回值,或者执行特定的动作


    when(iMathUtils.sum(1, 1)).thenReturn(2); 
doReturn(3).when(iMathUtils).sum(1,1);
//给方法设置桩可以设置多次,只会返回最后一次设置的值
doReturn(2).when(iMathUtils).sum(1,1);

//验证方法调用次数
//方法调用1次
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
//方法调用3次
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")
, Mockito.times(3).thenReturn(true);

//verify方法用于验证“模仿对象”的互动或验证发生的某些行为
verify(mPerson, atLeast(2)).getAge();

//参数匹配器,用于匹配特定的参数
any()
contains()
argThat()
when(mPerson.eat(any(String.class))).thenReturn("米饭");

//除了mock()外,spy()也可以模拟对象,spy与mock的
//唯一区别就是默认行为不一样:spy对象的方法默认调用
//真实的逻辑,mock对象的方法默认什么都不做,或直接
//返回默认值
//如果要保留原来对象的功能,而仅仅修改一个或几个
//方法的返回值,可以采用spy方法,无参构造的类初始
//化也使用spy方法
Person mPerson = spy(Person.class);

//检查入参的mocks是否有任何未经验证的交互
verifyNoMoreInteractions(iMathUtils);

4.使用Mockito后的思考


简单的测试会使整体的代码更简单,更可读、更可维护。如果你不能把测试写的很简单,那么请在测试时重构你的代码



  • 优点:丰富强大的方式验证“模仿对象”的互动或验证发生的某些行为

  • 缺点:Mockito框架不支持mock匿名类、final类、static方法、private方法。


虽然,static方法可以使用wrapper静态类的方式实现mockito的单元测试,但是,毕竟过于繁琐,因此,PowerMockito由此而来。


三、拯救Mockito于水深火热的PowerMockito


什么是PowerMockito?


PowerMockito是一个扩展了Mockito的具有更强大功能的单元测试框架,它支持mock匿名类、final类、static方法、private方法


开始PowerMockito之旅


1.在build.gradle里面添加Mcokito的依赖


    testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'

2.用PowerMockito来模拟对象


    //使用PowerMock须加注解@PrepareForTest和@RunWith(PowerMockRunner.class)(@PrepareForTest()里写的
// 是对应方法所在的类 ,mockito支持的方法使用PowerMock的形式实现时,可以不加这两个注解)
@PrepareForTest(T.class)
@RunWith(PowerMockRunner.class)

//mock含静态方法或字段的类
PowerMockito.mockStatic(Banana.class);

//Powermock提供了一个Whitebox的class,可以方便的绕开权限限制,可以get/set private属性,实现注入。
//也可以调用private方法。也可以处理static的属性/方法,根据不同需求选择不同参数的方法即可。
修改类里面静态字段的值
Whitebox.setInternalState(Banana.class, "COLOR", "蓝色");

//调用类中的真实方法
PowerMockito.when(banana.getBananaInfo()).thenCallRealMethod();

//验证私有方法是否被调用
PowerMockito.verifyPrivate(banana, times(1)).invoke("flavor");

//忽略调用私有方法
PowerMockito.suppress(PowerMockito.method(Banana.class, "flavor"));

//修改私有变量
MemberModifier.field(Banana.class, "fruit").set(banana, "西瓜");

//使用PowerMockito mock出来的对象可以直接调用final方法
Banana banana = PowerMockito.mock(Banana.class);

//whenNew 方法的意思是之后 new 这个对象时,返回某个被 Mock 的对象而不是让真的 new
//新的对象。如果构造方法有参数,可以在withNoArguments方法中传入。
PowerMockito.whenNew(Banana.class).withNoArguments().thenReturn(banana);

3.使用PowerMockRule来代替@RunWith(PowerMockRunner.class)的方式,需要多添加以下依赖:


    testImplementation "org.powermock:powermock-module-junit4-rule:1.7.4"
testImplementation "org.powermock:powermock-classloading-xstream:1.7.4"

使用示例如下:


    @Rule
public PowerMockRule mPowerMockRule = new PowerMockRule();

4.使用Parameterized来进行参数化测试:


通过注解@Parameterized.parameters提供一系列数据给构造器中的构造参数或给被注解@Parameterized.parameter注解的public全局变量


    RunWith(Parameterized.class)
public class ParameterizedTest {

private int num;
private boolean truth;

public ParameterizedTest(int num, boolean truth) {
this.num = num;
this.truth = truth;
}

//被此注解注解的方法将把返回的列表数据中的元素对应注入到测试类
//的构造函数ParameterizedTest(int num, boolean truth)中
@Parameterized.Parameters
public static Collection providerTruth()
{
return Arrays.asList(new Object[][]{
{0, true},
{1, false},
{2, true},
{3, false},
{4, true},
{5, false}
});
}

// //也可不使用构造函数注入的方式,使用注解注入public变量的方式
// @Parameterized.Parameter
// public int num;
// //value = 1指定括号里的第二个Boolean值
// @Parameterized.Parameter(value = 1)
// public boolean truth;

@Test
public void printTest() {
Assert.assertEquals(truth, print(num));
System.out.println(num);
}

private boolean print(int num) {
return num % 2 == 0;
}

}

四、能在Java单元测试里面执行Android代码的Robolectric


什么是Robolectric?


Robolectric通过一套能运行在JVM上的Android代码,解决了在Java单元测试中很难进行Android单元测试的痛点。


进入Roboletric的领地




1.在build.gradle里面添加Robolectric的依赖


        //Robolectric核心
testImplementation "org.robolectric:robolectric:3.8"
//支持support-v4
testImplementation 'org.robolectric:shadows-support-v4:3.4-rc2'
//支持Multidex功能
testImplementation "org.robolectric:shadows-multidex:3.+"

2.Robolectric常用用法


首先给指定的测试类上面进行配置


    @RunWith(RobolectricTestRunner.class)
//目前Robolectric最高支持sdk版本为23。
@Config(constants = BuildConfig.class, sdk = 23)

下面是一些常用用法:


    //当Robolectric.setupActivity()方法返回的时候,
//默认会调用Activity的onCreate()、onStart()、onResume()
mTestActivity = Robolectric.setupActivity(TestActivity.class);

//获取TestActivity对应的影子类,从而能获取其相应的动作或行为
ShadowActivity shadowActivity = Shadows.shadowOf(mTestActivity);
Intent intent = shadowActivity.getNextStartedActivity();

//使用ShadowToast类获取展示toast时相应的动作或行为
Toast latestToast = ShadowToast.getLatestToast();
Assert.assertNull(latestToast);
//直接通过ShadowToast简单工厂类获取Toast中的文本
Assert.assertEquals("hahaha", ShadowToast.getTextOfLatestToast());

//使用ShadowAlertDialog类获取展示AlertDialog时相应的
//动作或行为(暂时只支持app包下的,不支持v7。。。)
latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
Assert.assertNull(latestAlertDialog);

//使用RuntimeEnvironment.application可以获取到
//Application,方便我们使用。比如访问资源文件。
Application application = RuntimeEnvironment.application;
String appName = application.getString(R.string.app_name);
Assert.assertEquals("WanAndroid", appName);

//也可以直接通过ShadowApplication获取application
ShadowApplication application = ShadowApplication.getInstance();
Assert.assertNotNull(application.hasReceiverForIntent(intent));

自定义Shadow类:


    @Implements(Person.class)
public class ShadowPerson {

@Implementation
public String getName() {
return "AndroidUT";
}

}

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class,
sdk = 23,
shadows = {ShadowPerson.class})


Person person = new Person();
//实际上调用的是ShadowPerson的方法,输出JsonChao
Log.d("test", person.getName());

ShadowPerson shadowPerson = Shadow.extract(person);
//测试通过
Assert.assertEquals("JsonChao", shadowPerson.getName());

}

注意:异步测试出现一些问题(比如改变一些编码习惯,比如回调函数不能写成匿名内部类对象,需要定义一个全局变量,并破坏其封装性,即提供一个get方法,供UT调用),解决方案使用Mockito来结合进行测试,将异步转为同步


3.Robolectric的优缺点



  • 优点:支持大部分Android平台依赖类底层的引用与模拟。

  • 缺点:异步测试有些问题,需要结合一些框架来配合完成更多功能。


五、单元测试覆盖率报告生成之jacoco


什么是Jacoco


Jacoco的全称为Java Code Coverage(Java代码覆盖率),可以生成java的单元测试代码覆盖率报告


加入Jacoco到你的单元测试大家族


在应用Module下加入jacoco.gradle自定义脚本,app.gradle apply from它,同步,即可看到在app的Task下生成了Report目录,Report目录 下生成了JacocoTestReport任务。


    apply plugin: 'jacoco'

jacoco {
toolVersion = "0.7.7.201606060606" //指定jacoco的版本
reportsDir = file("$buildDir/JacocoReport") //指定jacoco生成报告的文件夹
}

//依赖于testDebugUnitTest任务
task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
group = "reporting" //指定task的分组
reports {
xml.enabled = true //开启xml报告
html.enabled = true //开启html报告
}

def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug",
includes: ["**/*Presenter.*"],
excludes: ["*.*"])//指定类文件夹、包含类的规则及排除类的规则,
//这里我们生成所有Presenter类的测试报告
def mainSrc = "${project.projectDir}/src/main/java" //指定源码目录

sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec")//指定报告数据的路径
}

在Gradle构建板块Gradle.projects下的app/Task/verification下,其中testDebugUnitTest构建任务会生成单元测试结果报告,包含xml及html格式,分别对应test-results和reports文件夹;jacocoTestReport任务会生成单元测试覆盖率报告,结果存放在jacoco和JacocoReport文件夹。


image


生成的JacocoReport文件夹下的index.html即对应的单元测试覆盖率报告,用浏览器打开后,可以看到覆盖情况被不同的颜色标识出来,其中绿色表示代码被单元测试覆盖到,黄色表示部分覆盖,红色则表示完全没有覆盖到


六、单元测试的流程


要验证程序正确性,必然要给出所有可能的条件(极限编程),并验证其行为或结果,才算是100%覆盖条件。实际项目中,验证一般条件边界条件就OK了。


在实际项目中,单元测试对象与页面是一对一的,并不建议跨页面,这样的单元测试耦合太大,维护困难。 需要写完后,看覆盖率,找出单元测试中没有覆盖到的函数分支条件等,然后继续补充单元测试case列表,并在单元测试工程代码中补上case。 直到规划的页面中所有逻辑的重要分支、边界条件都被覆盖,该项目的单元测试结束。


建议(-ovo-)~


可以从公司项目小规模使用,形成自己的单元测试风格后,就可更大范围地推广了。




资源git地址:==》完整项目单元测试学习案例


收起阅读 »

全新 LeakCanary 2 ! 完全基于 Kotlin 重构升级 !

大概一年以前,写过一篇 LeakCanary 源码解析 ,当时是基于 1.5.4 版本进行分析的 。Square 公司在今年四月份发布了全新的 2.0 版本,完全使用 Kotlin 进行重构,核心原理并没有太大变化,但是做了一定的性能优化。在本文中,就让我们通...
继续阅读 »

大概一年以前,写过一篇 LeakCanary 源码解析 ,当时是基于 1.5.4 版本进行分析的 。Square 公司在今年四月份发布了全新的 2.0 版本,完全使用 Kotlin 进行重构,核心原理并没有太大变化,但是做了一定的性能优化。在本文中,就让我们通过源码来看看 2.0 版本发生了哪些变化。本文不会过多的分析源码细节,详细细节可以阅读我之前基于 1.5.4 版本写的文章,两个版本在原理方面并没有太大变化。



含注释 fork 版本 LeakCanary 源码



使用


首先来对比一下两个版本的使用方式。


1.5.4 版本


在老版本中,我们需要添加如下依赖:


dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

leakcanary-android-no-op 库在 release 版本中使用,其中是没有任何逻辑代码的。


然后需要在自己的 Application 中进行初始化。


public class ExampleApplication extends Application {

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

LeakCanary.install() 执行后,就会构建 RefWatcher 对象,开始监听 Activity.onDestroy() 回调, 通过 RefWatcher.watch() 监测 Activity 引用的泄露情况。发现内存泄露之后进行 heap dump ,利用 Square 公司的另一个库 haha(已废弃)来分析 heap dump 文件,找到引用链之后通知用户。这一套原理在新版本中还是没变的。


2.0 版本


新版本的使用更加方便了,你只需要在 build.gradle 文件中添加如下依赖:


debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'

是的,你没看过,这样就可以了。你肯定会有一个疑问,那它是如何初始化的呢?我刚看到这个使用文档的时候,同样也有这个疑问。当你看看源码之后就一目了然了。我先不解释,看一下源码中的 LeakSentryInstaller 这个类:


/**
* Content providers are loaded before the application class is created. [LeakSentryInstaller] is
* used to install [leaksentry.LeakSentry] on application start.
*
* Content Provider 在 Application 创建之前被自动加载,因此无需用户手动在 onCrate() 中进行初始化
*/

internal class LeakSentryInstaller : ContentProvider() {

override fun onCreate(): Boolean {
CanaryLog.logger = DefaultCanaryLog()
val application = context!!.applicationContext as Application
InternalLeakSentry.install(application) // 进行初始化工作,核心
return true
}

override fun query(
uri: Uri,
strings: Array<String>?,
s: String?,
strings1: Array<String>?,
s1: String?
)
: Cursor? {
return null
}

override fun getType(uri: Uri): String? {
return null
}

override fun insert(
uri: Uri,
contentValues: ContentValues?
)
: Uri? {
return null
}

override fun delete(
uri: Uri,
s: String?,
strings: Array<String>?
)
: Int {
return 0
}

override fun update(
uri: Uri,
contentValues: ContentValues?,
s: String?,
strings: Array<String>?
)
: Int {
return 0
}
}

看到这个类你应该也明白了。LeakCanary 利用 ContentProvier 进行了初始化。ContentProvier 一般会在 Application 被创建之前被加载,LeakCanary 在其 onCreate() 方法中调用了 InternalLeakSentry.install(application) 进行初始化。这应该是我第一次看到第三方库这么进行初始化。这的确是方便了开发者,但是仔细想想弊端还是很大的,如果所有第三方库都这么干,开发者就没法控制应用启动时间了。很多开发者为了加快应用启动速度,都下了很大心血,包括按需延迟初始化第三方库。但在 LeakCanary 中,这个问题并不存在,因为它本身就是一个只在 debug 版本中使用的库,并不会对 release 版本有任何影响。


源码解析


前面提到了 InternalLeakSentry.install() 就是核心的初始化工作,其地位就和 1.5.4 版本中的 LeakCanary.install() 一样。下面就从 install() 方法开始,走进 LeakCanary 2.0 一探究竟。


1. LeakCanary.install()


fun install(application: Application) {
CanaryLog.d("Installing LeakSentry")
checkMainThread() // 只能在主线程调用,否则会抛出异常
if (this::application.isInitialized) {
return
}
InternalLeakSentry.application = application

val configProvider = { LeakSentry.config }
ActivityDestroyWatcher.install( // 监听 Activity.onDestroy(),见 1.1
application, refWatcher, configProvider
)
FragmentDestroyWatcher.install( // 监听 Fragment.onDestroy(),见 1.2
application, refWatcher, configProvider
)
listener.onLeakSentryInstalled(application) // 见 1.3
}

install() 方法主要做了三件事:



  • 注册 Activity.onDestroy() 监听

  • 注册 Fragment.onDestroy() 监听

  • 监听完成后进行一些初始化工作


依次看一看。


1.1 ActivityDestroyWatcher.install()


ActivityDestroyWatcher 类的源码很简单:


internal class ActivityDestroyWatcher private constructor(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) {

private val lifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
refWatcher.watch(activity) // 监听到 onDestroy() 之后,通过 refWatcher 监测 Activity
}
}
}

companion object {
fun install(
application: Application,
refWatcher: RefWatcher,
configProvider: ()
-> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(refWatcher, configProvider)
// 注册 Activity 生命周期监听
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}

install() 方法中注册了 Activity 生命周期监听,在监听到 onDestroy() 时,调用 RefWatcher.watch() 方法开始监测 Activity。


1.2 FragmentDestroyWatcher.install()


FragmentDestroyWatcher 是一个接口,它有两个实现类 AndroidOFragmentDestroyWatcherSupportFragmentDestroyWatcher


internal interface FragmentDestroyWatcher {

fun watchFragments(activity: Activity)

companion object {

private const val SUPPORT_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"

fun install(
application: Application,
refWatcher: RefWatcher,
configProvider: ()
-> LeakSentry.Config
) {
val fragmentDestroyWatchers = mutableListOf<FragmentDestroyWatcher>()

if (SDK_INT >= O) { // >= 26,使用 AndroidOFragmentDestroyWatcher
fragmentDestroyWatchers.add(
AndroidOFragmentDestroyWatcher(refWatcher, configProvider)
)
}

if (classAvailable(
SUPPORT_FRAGMENT_CLASS_NAME
)
) {
fragmentDestroyWatchers.add( // androidx 使用 SupportFragmentDestroyWatcher
SupportFragmentDestroyWatcher(refWatcher, configProvider)
)
}

if (fragmentDestroyWatchers.size == 0) {
return
}

application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
)
{
for (watcher in fragmentDestroyWatchers) {
watcher.watchFragments(activity)
}
}
})
}

private fun classAvailable(className: String): Boolean {
return try {
Class.forName(className)
true
} catch (e: ClassNotFoundException) {
false
}
}
}
}

如果我没记错的话,1.5.4 是不监测 Fragment 的泄露的。而 2.0 版本提供了对 Android O 以及 androidx 版本中的 Fragment 的内存泄露检测。 AndroidOFragmentDestroyWatcherSupportFragmentDestroyWatcher 的实现代码其实是一致的,Android O 及以后,androidx 都具备对 Fragment 生命周期的监听功能。以 AndroidOFragmentDestroyWatcher 为例,简单看一下它的实现。


@RequiresApi(Build.VERSION_CODES.O) //
internal class AndroidOFragmentDestroyWatcher(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) : FragmentDestroyWatcher {

private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
)
{
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
refWatcher.watch(view)
}
}

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
)
{
if (configProvider().watchFragments) {
refWatcher.watch(fragment)
}
}
}

override fun watchFragments(activity: Activity) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}

同样,还是使用 RefWatcher.watch() 方法来进行监测。


1.3 listener.onLeakSentryInstalled()


onLeakSentryInstalled() 回调中会初始化一些检测内存泄露过程中需要的对象,如下所示:


override fun onLeakSentryInstalled(application: Application) {
this.application = application

val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider) // 用于 heap dump

val gcTrigger = GcTrigger.Default // 用于手动调用 GC

val configProvider = { LeakCanary.config } // 配置项

val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper) // 发起内存泄漏检测的线程

heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider
)
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
addDynamicShortcut(application)
}

对老版本代码熟悉的同学,看到这些对象应该很熟悉。



  • heapDumper 用于确认内存泄漏之后进行 heap dump 工作。

  • gcTrigger 用于发现可能的内存泄漏之后手动调用 GC 确认是否真的为内存泄露。


这两个对象是 LeakCanary 检测内存泄漏的核心。后面会进行详细分析。


到这里,整个 LeakCanary 的初始化工作就完成了。与 1.5.4 版本不同的是,新版本增加了对 Fragment 以及 androidx 的支持。当发生 Activity.onDestroy()Fragment.onFragmentViewDestroyed() , Fragment.onFragmentDestroyed() 三者之一时,RefWatcher 就开始工作了,调用其 watch() 方法开始检测引用是否泄露。


2. RefWatcher.watch()


在看源码之前,我们先来看几个后面会使用到的队列。


  /**
* References passed to [watch] that haven't made it to [retainedReferences] yet.
* watch() 方法传进来的引用,尚未判定为泄露
*/

private val watchedReferences = mutableMapOf<String, KeyedWeakReference>()
/**
* References passed to [watch] that we have determined to be retained longer than they should
* have been.
* watch() 方法传进来的引用,已经被判定为泄露
*/

private val retainedReferences = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>() // 引用队列,配合弱引用使用

通过 watch() 方法传入的引用都会保存在 watchedReferences 中,被判定泄露之后保存在 retainedReferences 中。注意,这里的判定过程不止会发生一次,已经进入队列 retainedReferences 的引用仍有可能被移除。queue 是一个 ReferenceQueue 引用队列,配合弱引用使用,这里记住一句话:



弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。



也就是说,会被 GC 回收的对象引用,会保存在队列 queue 中。


回头再来看看 watch() 方法的源码。


  @Synchronized fun watch(
watchedReference: Any,
referenceName: String
)
{
if (!isEnabled()) {
return
}
removeWeaklyReachableReferences() // 移除队列中将要被 GC 的引用,见 2.1
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference = // 构建当前引用的弱引用对象,并关联引用队列 queue
KeyedWeakReference(watchedReference, key, referenceName, watchUptimeMillis, queue)
if (referenceName != "") {
CanaryLog.d(
"Watching instance of %s named %s with key %s", reference.className,
referenceName, key
)
} else {
CanaryLog.d(
"Watching instance of %s with key %s", reference.className, key
)
}

watchedReferences[key] = reference // 将引用存入 watchedReferences
checkRetainedExecutor.execute {
moveToRetained(key) // 如果当前引用未被移除,仍在 watchedReferences 队列中,
// 说明仍未被 GC,移入 retainedReferences 队列中,暂时标记为泄露
// 见 2.2
}
}

逻辑还是比较清晰的,首先会调用 removeWeaklyReachableReferences() 方法,这个方法在整个过程中会多次调用。其作用是移除 watchedReferences 中将被 GC 的引用。


2.1 removeWeaklyReachableReferences()


  private fun removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
// 弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference? // 队列 queue 中的对象都是会被 GC 的
if (ref != null) {
val removedRef = watchedReferences.remove(ref.key)
if (removedRef == null) {
retainedReferences.remove(ref.key)
}
// 移除 watchedReferences 队列中的会被 GC 的 ref 对象,剩下的就是可能泄露的对象
}
} while (ref != null)
}

整个过程中会多次调用,以确保将已经入队 queue 的将被 GC 的对象引用移除掉,避免无谓的 heap dump 操作。而仍在 watchedReferences 队列中的引用,则可能已经泄露,移到队列 retainedReferences 中,这就是 moveToRetained() 方法的逻辑。代码如下:


2.2 moveToRetained()


  @Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableReferences() // 再次调用,防止遗漏
val retainedRef = watchedReferences.remove(key)
if (retainedRef != null) {
retainedReferences[key] = retainedRef
onReferenceRetained()
}
}

这里的 onReferenceRetained() 最后会回调到 InternalLeakCanary.kt 中。


  override fun onReferenceRetained() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onReferenceRetained()
}
}

调用了 HeapDumpTriggeronReferenceRetained() 方法。


  fun onReferenceRetained() {
scheduleRetainedInstanceCheck("found new instance retained")
}

private fun scheduleRetainedInstanceCheck(reason: String) {
if (checkScheduled) {
return
}
checkScheduled = true
backgroundHandler.post {
checkScheduled = false
checkRetainedInstances(reason) // 检测泄露实例
}
}

checkRetainedInstances() 方法是确定泄露的最后一个方法了。这里会确认引用是否真的泄露,如果真的泄露,则发起 heap dump,分析 dump 文件,找到引用链,最后通知用户。整体流程和老版本是一致的,但在一些细节处理,以及 dump 文件的分析上有所区别。下面还是通过源码来看看这些区别。


  private fun checkRetainedInstances(reason: String) {
CanaryLog.d("Checking retained instances because %s", reason)
val config = configProvider()
// A tick will be rescheduled when this is turned back on.
if (!config.dumpHeap) {
return
}

var retainedKeys = refWatcher.retainedKeys

// 当前泄露实例个数小于 5 个,不进行 heap dump
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return

if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
showRetainedCountWithDebuggerAttached(retainedKeys.size)
scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
CanaryLog.d(
"Not checking for leaks while the debugger is attached, will retry in %d ms",
WAIT_FOR_DEBUG_MILLIS
)
return
}

// 可能存在被观察的引用将要变得弱可达,但是还未入队引用队列。
// 这时候应该主动调用一次 GC,可能可以避免一次 heap dump
gcTrigger.runGc()

retainedKeys = refWatcher.retainedKeys

if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return

HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)

CanaryLog.d("Found %d retained references, dumping the heap", retainedKeys.size)
HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
dismissNotification()
val heapDumpFile = heapDumper.dumpHeap() // AndroidHeapDumper
if (heapDumpFile == null) {
CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
showRetainedCountWithHeapDumpFailed(retainedKeys.size)
return
}

refWatcher.removeRetainedKeys(retainedKeys) // 移除已经 heap dump 的 retainedKeys

HeapAnalyzerService.runAnalysis(application, heapDumpFile) // 分析 heap dump 文件
}

首先调用 checkRetainedCount() 函数判断当前泄露实例个数如果小于 5 个,仅仅只是给用户一个通知,不会进行 heap dump 操作,并在 5s 后再次发起检测。这是和老版本一个不同的地方。


  private fun checkRetainedCount(
retainedKeys: Set<String>,
retainedVisibleThreshold: Int // 默认为 5 个
)
: Boolean {
if (retainedKeys.isEmpty()) {
CanaryLog.d("No retained instances")
dismissNotification()
return true
}

if (retainedKeys.size < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
CanaryLog.d(
"Found %d retained instances, which is less than the visible threshold of %d",
retainedKeys.size,
retainedVisibleThreshold
)
// 通知用户 "App visible, waiting until 5 retained instances"
showRetainedCountBelowThresholdNotification(retainedKeys.size, retainedVisibleThreshold)
scheduleRetainedInstanceCheck( // 5s 后再次发起检测
"Showing retained instance notification", WAIT_FOR_INSTANCE_THRESHOLD_MILLIS
)
return true
}
}
return false
}

当集齐 5 个泄露实例之后,也并不会立马进行 heap dump。而是先手动调用一次 GC。当然不是使用 System.gc(),如下所示:


  object Default : GcTrigger {
override fun runGc() {
// Code taken from AOSP FinalizationTest:
// https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
// java/lang/ref/FinalizationTester.java
// System.gc() does not garbage collect every time. Runtime.gc() is
// more likely to perform a gc.
Runtime.getRuntime()
.gc()
enqueueReferences()
System.runFinalization()
}

那么,为什么要进行这次 GC 呢?可能存在被观察的引用将要变得弱可达,但是还未入队引用队列的情况。这时候应该主动调用一次 GC,可能可以避免一次额外的 heap dump 。GC 之后再次调用 checkRetainedCount() 判断泄露实例个数。如果此时仍然满足条件,就要发起 heap dump 操作了。具体逻辑在 AndroidHeapDumper.dumpHeap() 方法中,核心方法就是下面这句代码:


Debug.dumpHprofData(heapDumpFile.absolutePath)

生成 heap dump 文件之后,要删除已经处理过的引用,


refWatcher.removeRetainedKeys(retainedKeys)

最后启动一个前台服务 HeapAnalyzerService 来分析 heap dump 文件。老版本中是使用 Square 自己的 haha 库来解析的,这个库已经废弃了,Square 完全重写了解析库,主要逻辑都在 moudle leakcanary-analyzer 中。这部分我还没有阅读,就不在这里分析了。对于新的解析器,官网是这样介绍的:



Uses 90% less memory and 6 times faster than the prior heap parser.



减少了 90% 的内存占用,而且比原来快了 6 倍。后面有时间单独来分析一下这个解析库。


后面的过程就不再赘述了,通过解析库找到最短 GC Roots 引用路径,然后展示给用户。


总结


通读完源码,LeakCanary 2 还是带来了很多的优化。与老版本相比,主要有以下不同:



  • 百分之百使用 Kotlin 重写

  • 自动初始化,无需用户手动再添加初始化代码

  • 支持 fragment,支持 androidx

  • 当泄露引用到达 5 个时才会发起 heap dump

  • 全新的 heap parser,减少 90% 内存占用,提升 6 倍速度



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


收起阅读 »

利用tess-two和cv4j实现简单的ocr功能

ocr
ocr 光学字符识别(英语:Optical Character Recognition, OCR)是指对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。 Tesseract Tesseract是Ray Smith于1985到1995年间在惠普...
继续阅读 »

ocr



光学字符识别(英语:Optical Character Recognition, OCR)是指对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。



Tesseract


Tesseract是Ray Smith于1985到1995年间在惠普布里斯托实验室开发的一个OCR引擎,曾经在1995 UNLV精确度测试中名列前茅。但1996年后基本停止了开发。2006年,Google邀请Smith加盟,重启该项目。目前项目的许可证是Apache 2.0。该项目目前支持Windows、Linux和Mac OS等主流平台。但作为一个引擎,它只提供命令行工具。 现阶段的Tesseract由Google负责维护,是最好的开源OCR Engine之一,并且支持中文。


tess-two是Tesseract在Android平台上的移植。


下载tess-two:


compile 'com.rmtheis:tess-two:8.0.0'

然后将训练好的eng.traineddata放入android项目的assets文件夹中,就可以识别英文了。


1. 简单地识别英文


初始化tess-two,加载训练好的tessdata


    private void prepareTesseract() {
try {
prepareDirectory(DATA_PATH + TESSDATA);
} catch (Exception e) {
e.printStackTrace();
}

copyTessDataFiles(TESSDATA);
}

/**
* Prepare directory on external storage
*
* @param path
* @throws Exception
*/
private void prepareDirectory(String path) {

File dir = new File(path);
if (!dir.exists()) {
if (!dir.mkdirs()) {
Log.e(TAG, "ERROR: Creation of directory " + path + " failed, check does Android Manifest have permission to write to external storage.");
}
} else {
Log.i(TAG, "Created directory " + path);
}
}

/**
* Copy tessdata files (located on assets/tessdata) to destination directory
*
* @param path - name of directory with .traineddata files
*/
private void copyTessDataFiles(String path) {
try {
String fileList[] = getAssets().list(path);

for (String fileName : fileList) {

// open file within the assets folder
// if it is not already there copy it to the sdcard
String pathToDataFile = DATA_PATH + path + "/" + fileName;
if (!(new File(pathToDataFile)).exists()) {

InputStream in = getAssets().open(path + "/" + fileName);

OutputStream out = new FileOutputStream(pathToDataFile);

// Transfer bytes from in to out
byte[] buf = new byte[1024];
int len;

while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.close();

Log.d(TAG, "Copied " + fileName + "to tessdata");
}
}
} catch (IOException e) {
Log.e(TAG, "Unable to copy files to tessdata " + e.toString());
}
}



拍完照后,调用startOCR方法。

    private void startOCR(Uri imgUri) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; // 1 - means max size. 4 - means maxsize/4 size. Don't use value <4, because you need more memory in the heap to store your data.
Bitmap bitmap = BitmapFactory.decodeFile(imgUri.getPath(), options);

String result = extractText(bitmap);
resultView.setText(result);

} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}

extractText()会调用tess-two的api来实现ocr文字识别。


    private String extractText(Bitmap bitmap) {
try {
tessBaseApi = new TessBaseAPI();
} catch (Exception e) {
Log.e(TAG, e.getMessage());
if (tessBaseApi == null) {
Log.e(TAG, "TessBaseAPI is null. TessFactory not returning tess object.");
}
}

tessBaseApi.init(DATA_PATH, lang);

tessBaseApi.setImage(bitmap);
String extractedText = "empty result";
try {
extractedText = tessBaseApi.getUTF8Text();
} catch (Exception e) {
Log.e(TAG, "Error in recognizing text.");
}
tessBaseApi.end();
return extractedText;
}

最后,显示识别的效果,此时的效果还算可以。






2. 识别代码

接下来,尝试用上面的程序识别一段代码。





此时,效果一塌糊涂。我们重构一下startOCR(),增加局部的二值化处理。

    private void startOCR(Uri imgUri) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; // 1 - means max size. 4 - means maxsize/4 size. Don't use value <4, because you need more memory in the heap to store your data.
Bitmap bitmap = BitmapFactory.decodeFile(imgUri.getPath(), options);

CV4JImage cv4JImage = new CV4JImage(bitmap);
Threshold threshold = new Threshold();
threshold.adaptiveThresh((ByteProcessor)(cv4JImage.convert2Gray().getProcessor()), Threshold.ADAPTIVE_C_MEANS_THRESH, 12, 30, Threshold.METHOD_THRESH_BINARY);
Bitmap newBitmap = cv4JImage.getProcessor().getImage().toBitmap(Bitmap.Config.ARGB_8888);

ivImage2.setImageBitmap(newBitmap);

String result = extractText(newBitmap);
resultView.setText(result);

} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}

在这里,使用cv4j来实现图像的二值化处理。


            CV4JImage cv4JImage = new CV4JImage(bitmap);
Threshold threshold = new Threshold();
threshold.adaptiveThresh((ByteProcessor)(cv4JImage.convert2Gray().getProcessor()), Threshold.ADAPTIVE_C_MEANS_THRESH, 12, 30, Threshold.METHOD_THRESH_BINARY);
Bitmap newBitmap = cv4JImage.getProcessor().getImage().toBitmap(Bitmap.Config.ARGB_8888);


图像二值化就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果。图像的二值化有利于图像的进一步处理,使图像变得简单,而且数据量减小,能凸显出感兴趣的目标的轮廓。



cv4j的github地址:https://github.com/imageprocessor/cv4j



cv4jgloomyfish和我一起开发的图像处理库,纯java实现。



再来试试效果,图片中间部分是二值化后的效果,此时基本能识别出代码的内容。





3. 识别中文

如果要识别中文字体,需要使用中文的数据包。可以去下面的网站上下载。


https://github.com/tesseract-ocr/tessdata


跟中文相关的数据包有chi_sim.traineddata、chi_tra.traineddata,它们分别表示是简体中文和繁体中文。


tessBaseApi.init(DATA_PATH, lang);

前面的例子都是识别英文的,所以原先的lang值为"eng",现在要识别简体中文的话需要将其值改为"chi_sim"。



最后

本项目只是demo级别的演示,离生产环境的使用还差的很远。

本项目的github地址:https://github.com/fengzhizi715/Tess-TwoDemo


为何说只是demo级别呢?



  • 数据包很大,特别是中文的大概有50多M,放在移动端的肯定不合适。一般正确的做法,都是放在云端。

  • 识别文字很慢,特别是中文,工程上还有很多优化的空间。

  • 做ocr之前需要做很多预处理的工作,在本例子中只用了二值化,其实还有很多预处理的步骤比如倾斜校正、字符切割等等。

  • 为了提高tess-two的识别率,可以自己训练数据集。




作者:fengzhizi715
链接:https://www.jianshu.com/p/5314f63c75d8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

[译] R8 优化:字节码常量操作

1. Log Tags(日志标签)关于在类中定义标记字符串的最佳方法,有一个正在进行的争论(如果您甚至可以这样称呼它的话)。历史上有两种策略:字符串文本和对类调用 getSimpleName()。private static final String TAG ...
继续阅读 »

1. Log Tags(日志标签)

关于在类中定义标记字符串的最佳方法,有一个正在进行的争论(如果您甚至可以这样称呼它的话)。历史上有两种策略:字符串文本和对类调用 getSimpleName()

private static final String TAG = "MyClass";
// or
private static final String TAG = MyClass.class.getSimpleName();

究竟孰好孰坏,让我们写个例子测试下。

class MyClass {
private static final String TAG_STRING = "MyClass";
private static final String TAG_CLASS = MyClass.class.getSimpleName();

public static void main(String... args) {
Log.d(TAG_STRING, "String tag");
Log.d(TAG_CLASS, "Class tag");
}
}

对上面的代码执行,Compilingdexing 然后查看 Dalvik 字节码。

[000194] MyClass.:()V
0000: const-class v0, LMyClass;
0002: invoke-virtual {v0}, Ljava/lang/Class;.getSimpleName:()Ljava/lang/String;
0005: move-result-object v0
0006: sput-object v0, LMyClass;.TAG_CLASS:Ljava/lang/String;
0008: return-void

[000120] MyClass.main:([Ljava/lang/String;)V
0000: const-string v1, "MyClass"
0002: const-string v0, "String tag"
0004: invoke-static {v1, v0}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I
0007: sget-object v1, LMyClass;.a:Ljava/lang/String;
0009: const-string v0, "Class tag"
000b: invoke-static {v1, v0}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I
000e: return-void

在 main 函数中,0000 位置处加载 tag 的字符串常量,在 0007 处,查找该静态字段并读取值。在  方法中,静态字段是通过加载 MyClass 类然后在运行时调用 getSimpleName 方法获取。这个方法在类第一次加载的时候调用。

可以看到使用字符串常量效率更高,但使用 Class.getSimpleName() 对于重构之类需求更灵活。我们同样使用 R8 进行编译。

[000120] MyClass.main:([Ljava/lang/String;)V
0000: const-string v1, "MyClass"
0002: const-string v0, "String tag"
0004: invoke-static {v1, v0}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I
0007: const-string v0, "Class tag"
0009: invoke-static {v1, v0}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I
000c: return-void

可以看到在 0004 位置后面的操作中将变量 v1 的 MyClass 值进行了重复。

由于 myClass 的名称在编译时已知,R8 已将 myClass.class.getSimpleName() 替换为字符串变量 "myClass"。因为字段值现在是常量,所以  方法变为空并被删除。在调用位置上,用常量字符串替换了 sget 对象字节码。最后,对引用同一字符串的两个常量字符串字节码进行了重复数据消除,并进行重用。

因此,R8 确保不会进行额外的加载。因为 getSimpleName() 计算很简单,D8 实际上也会执行这种优化!

2. Applicability(拓展)

在 MyClass.class 上能够获取 getSimpleName()(以及 getName() 和 getCanonicalName()),这种方式的用途似乎有限——甚至可能仅限于此日志标记案例。优化只适用于类文本引用– getClass() 不起作用!再次结合其他 R8 特性,这种优化开始应用得更多。

我们来看下面的一个示例:

class Logger {
static Logger get(Class cls) {
return new Logger(cls.getSimpleName());
}
private Logger(String tag) { /* … */ }

}

class MyClass {
private static final Logger logger = Logger.get(MyClass.class);
}

如果 Logger.get 内嵌在所有调用处,则对以前具有方法参数动态输入的 class.getSimpleName 的调用将更改为类引用的静态输入(在本例中为 myClass.class)。R8 现在可以用字符串文字替换调用,从而产生直接调用构造函数的字段初始值设定项(也将删除其私有修饰符)。

class MyClass {
private static final Logger logger = new Logger("MyClass");
}

这依赖于 get 方法足够小或者满足 R8 的内联调用方式。

Kotlin 语言提供了强制函数内联的能力。它还允许将内联函数上的泛型类型参数标记为 reified,从而确保编译器知道在编译时解析为哪个类。使用这些特性,我们可以确保我们的函数始终是内联的,并且总是在显式类引用上调用 getSimpleName

class Logger private constructor(val tag: String) {

}
inline fun <reified T : Any> logger() = Logger(T::class.java.simpleName)

class MyClass {

companion object {
private val logger = logger()
}
}

logger 函数的初始值将始终具有与 myClass.Class.GetSimpleName() 等效的字节码,然后 R8 可以替换为字符串常量。

对于其他 Kotlin 示例,类型推断通常允许省略显式类型参数。

inline fun <reified T> typeAndValue(value: T) = "${T::class.java.name}: $value"
fun main() {
println(typeAndValue("hey"))
}

上面示例输出结果为:“java.lang.String: hey”,同时编译后的字节码中只有两个字符串常量,并且用 StringBuilder 连接,然后调用 System.out.println 输出,如果这个问题被解决,你会发现只有一个字符串常量调用 System.out.println

3. 混淆和优化

由于这种优化是在字节码上进行的,因此它必须与R8 的其他功能交互,这些功能可能会影响类,如 Obfuscation(混淆) 和 Optimization(优化)

让我们回到原来的例子。

class MyClass {
private static final String TAG_STRING = "MyClass";
private static final String TAG_CLASS = MyClass.class.getSimpleName();

public static void main(String... args) {
Log.d(TAG_STRING, "String tag");
Log.d(TAG_CLASS, "Class tag");
}
}

如果这个类被混淆了会发生什么?如果 R8 没有替换 getSimpleName 的调用,第一条日志消息将有一个 myclass 标记,第二条日志消息将有一个与模糊类名(如“a”)匹配的标记。

为了允许 R8 替换 getSimpleName,需要使用一个与运行时行为匹配的值。值得庆幸的是,由于 R8 也是执行混淆处理的工具,所以它可以直到类被赋予其最终名称时才进行替换。

[000158] a.main:([Ljava/lang/String;)V
0000: const-string v1, "MyClass"
0002: const-string v0, "String tag"
0004: invoke-static {v1, v0}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I
0007: const-string v1, "a"
0009: const-string v0, "Class tag"
000b: invoke-static {v1, v0}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I
000e: return-void

请注意 0007 现在将如何为第二个日志调用加载标记值(与原始 R8 输出不同),以及它如何正确反映混淆名称。

即使禁用了混淆,R8 还有其它优化会影响类名。虽然我打算在以后的文章中介绍它,如果 R8 能够证明不需要超类,并且子类是唯一的, 有时 R8 会将一个超类合并成一个子类。发生这种情况时,类名字符串优化将正确反映子类型名称,即使原始代码等效于 superType.class.getSimpleName()

3. String Data Section

前一篇文章讨论了如何在编译时执行 string.substring 或字符串串联之类的操作,从而导致 dex 文件的 string 部分的大小增加。本文中讨论的优化也会生成一些不存在的字符串,也可能会变大。

所以有两种场景需要考虑:“什么时候开启混淆?什么时候关闭混淆”。

启用混淆处理时,对 getSimpleName() 的调用不应创建新字符串。类和方法都将使用同一个字典进行混淆处理,默认字典以单个字母开头。这意味着,对于名为 b 的混淆类,插入字符串 “b” 几乎总是免费的,因为将有一个方法或字段的名称也是b。在DEX文件中,所有字符串都存储在一个池中,该池包含文字、类名、方法名和字段名,使模糊时匹配的可能性大于Y高。

但是,在禁用模糊处理的情况下,替换getSimpleName()永远都不是免费的。尽管dex文件有统一的字符串部分,类名还是以类型描述符的形式存储。这包括包名称,使用/作为分隔符,前缀为L,后缀为;。对于myclass,如果在假设的com.example包中,字符串数据包含lcom/example/myclass;的条目。由于这种格式,字符串“myclass”不存在,需要添加。

getName() 和 getCanonicalName() 都会产生新的字符串,都会返回全限定符字符串,而不是考虑存在的限定符。

由于混淆潜在创建了大量的字符串对象,所以它现在除了对顶级类型才可用。在 MyClass 中起作用,但是对于匿名类和内部类无法起作用。同样有研究表明不在一个单独的方法中使用混淆,来避免增加 dex 文件大小。

4. 总结

下篇文章中,我们将讨论 R8 的另一个优化。

收起阅读 »

CocoaPods 都做了什么

稍有 iOS 开发经验的人应该都是用过 CocoaPods,而对于 CI、CD 有了解的同学也都知道 Fastlane。而这两个在 iOS 开发中非常便捷的第三方库都是使用 Ruby 来编写的,这是为什么?先抛开这个话题不谈,我们来看一下 CocoaPods ...
继续阅读 »

稍有 iOS 开发经验的人应该都是用过 CocoaPods,而对于 CI、CD 有了解的同学也都知道 Fastlane。而这两个在 iOS 开发中非常便捷的第三方库都是使用 Ruby 来编写的,这是为什么?

先抛开这个话题不谈,我们来看一下 CocoaPods 和 Fastlane 是如何使用的,首先是 CocoaPods,在每一个工程使用 CocoaPods 的工程中都有一个 Podfile:

source 'https://github.com/CocoaPods/Specs.git'

target 'Demo' do
pod 'Mantle', '~> 1.5.1'
pod 'SDWebImage', '~> 3.7.1'
pod 'BlocksKit', '~> 2.2.5'
pod 'SSKeychain', '~> 1.2.3'
pod 'UMengAnalytics', '~> 3.1.8'
pod 'UMengFeedback', '~> 1.4.2'
pod 'Masonry', '~> 0.5.3'
pod 'AFNetworking', '~> 2.4.1'
pod 'Aspects', '~> 1.4.1'
end

这是一个使用 Podfile 定义依赖的一个例子,不过 Podfile 对约束的描述其实是这样的:

source('https://github.com/CocoaPods/Specs.git')

target('Demo') do
pod('Mantle', '~> 1.5.1')
...
end

Ruby 代码在调用方法时可以省略括号。

Podfile 中对于约束的描述,其实都可以看作是对代码简写,上面的代码在解析时可以当做 Ruby 代码来执行。

Fastlane 中的代码 Fastfile 也是类似的:

lane :beta do
increment_build_number
cocoapods
match
testflight
sh "./customScript.sh"
slack
end

使用描述性的”代码“编写脚本,如果没有接触或者使用过 Ruby 的人很难相信上面的这些文本是代码的。

Ruby 概述

在介绍 CocoaPods 的实现之前,我们需要对 Ruby 的一些特性有一个简单的了解,在向身边的朋友“传教”的时候,我往往都会用优雅这个词来形容这门语言(手动微笑)。

除了优雅之外,Ruby 的语法具有强大的表现力,并且其使用非常灵活,能快速实现我们的需求,这里简单介绍一下 Ruby 中的一些特性。

一切皆对象

在许多语言,比如 Java 中,数字与其他的基本类型都不是对象,而在 Ruby 中所有的元素,包括基本类型都是对象,同时也不存在运算符的概念,所谓的 1 + 1,其实只是 1.+(1) 的语法糖而已。

得益于一切皆对象的概念,在 Ruby 中,你可以向任意的对象发送 methods 消息,在运行时自省,所以笔者在每次忘记方法时,都会直接用 methods 来“查文档”:

2.3.1 :003 > 1.methods
=> [:%, :&, :*, :+, :-, :/, :<, :>, :^, :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_s, :to_f, :div, :divmod, :fdiv, :modulo, :abs, :magnitude, :zero?, :odd?, :even?, :bit_length, :to_int, :to_i, :next, :upto, :chr, :ord, :integer?, :floor, :ceil, :round, :truncate, :downto, :times, :pred, :to_r, :numerator, :denominator, :rationalize, :gcd, :lcm, :gcdlcm, :+@, :eql?, :singleton_method_added, :coerce, :i, :remainder, :real?, :nonzero?, :step, :positive?, :negative?, :quo, :arg, :rectangular, :rect, :polar, :real, :imaginary, :imag, :abs2, :angle, :phase, :conjugate, :conj, :to_c, :between?, :instance_of?, :public_send, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :private_methods, :kind_of?, :instance_variables, :tap, :is_a?, :extend, :define_singleton_method, :to_enum, :enum_for, :=~, :!~, :respond_to?, :freeze, :display, :send, :object_id, :method, :public_method, :singleton_method, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :trust, :untrusted?, :methods, :protected_methods, :frozen?, :public_methods, :singleton_methods, :!, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]

比如在这里向对象 1 调用 methods 就会返回它能响应的所有方法。

一切皆对象不仅减少了语言中类型的不一致,消灭了基本数据类型与对象之间的边界;这一概念同时也简化了语言中的组成元素,这样 Ruby 中只有对象和方法,这两个概念,这也降低了我们理解这门语言的复杂度:

  • 使用对象存储状态
  • 对象之间通过方法通信

block

Ruby 对函数式编程范式的支持是通过 block,这里的 block 和 Objective-C 中的 block 有些不同。

首先 Ruby 中的 block 也是一种对象,所有的 Block 都是 Proc 类的实例,也就是所有的 block 都是 first-class 的,可以作为参数传递,返回。

def twice(&proc)
2.times { proc.call() } if proc
end

def twice
2.times { yield } if block_given?
end

yield 会调用外部传入的 block,block_given? 用于判断当前方法是否传入了 block。

在这个方法调用时,是这样的:

twice do 
puts "Hello"
end

eval

最后一个需要介绍的特性就是 eval 了,早在几十年前的 Lisp 语言就有了 eval 这个方法,这个方法会将字符串当做代码来执行,也就是说 eval 模糊了代码与数据之间的边界。

> eval "1 + 2 * 3"
=> 7

有了 eval 方法,我们就获得了更加强大的动态能力,在运行时,使用字符串来改变控制流程,执行代码;而不需要去手动解析输入、生成语法树。

手动解析 Podfile

在我们对 Ruby 这门语言有了一个简单的了解之后,就可以开始写一个简易的解析 Podfile 的脚本了。

在这里,我们以一个非常简单的 Podfile 为例,使用 Ruby 脚本解析 Podfile 中指定的依赖:

source 'http://source.git'
platform :ios, '8.0'

target 'Demo' do
pod 'AFNetworking'
pod 'SDWebImage'
pod 'Masonry'
pod "Typeset"
pod 'BlocksKit'
pod 'Mantle'
pod 'IQKeyboardManager'
pod 'IQDropDownTextField'
end

因为这里的 source、platform、target 以及 pod 都是方法,所以在这里我们需要构建一个包含上述方法的上下文:

# eval_pod.rb
$hash_value = {}

def source(url)
end

def target(target)
end

def platform(platform, version)
end

def pod(pod)
end

使用一个全局变量 hash_value 存储 Podfile 中指定的依赖,并且构建了一个 Podfile 解析脚本的骨架;我们先不去完善这些方法的实现细节,先尝试一下读取 Podfile 中的内容并执行会不会有什么问题。

在 eval_pod.rb 文件的最下面加入这几行代码:

content = File.read './Podfile'
eval content
p $hash_value

这里读取了 Podfile 文件中的内容,并把其中的内容当做字符串执行,最后打印 hash_value 的值。

$ ruby eval_pod.rb

运行这段 Ruby 代码虽然并没有什么输出,但是并没有报出任何的错误,接下来我们就可以完善这些方法了:

def source(url)
$hash_value['source'] = url
end

def target(target)
targets = $hash_value['targets']
targets = [] if targets == nil
targets << target
$hash_value['targets'] = targets
yield if block_given?
end

def platform(platform, version)
end

def pod(pod)
pods = $hash_value['pods']
pods = [] if pods == nil
pods << pod
$hash_value['pods'] = pods
end

在添加了这些方法的实现之后,再次运行脚本就会得到 Podfile 中的依赖信息了,不过这里的实现非常简单的,很多情况都没有处理:

$ ruby eval_pod.rb
{"source"=>"http://source.git", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]}

CocoaPods 中对于 Podfile 的解析与这里的实现其实差不多,接下来就进入了 CocoaPods 的实现部分了。

CocoaPods 的实现

在上面简单介绍了 Ruby 的一些语法以及如何解析 Podfile 之后,我们开始深入了解一下 CocoaPods 是如何管理 iOS 项目的依赖,也就是 pod install 到底做了些什么。

Pod install 的过程

pod install 这个命令到底做了什么?首先,在 CocoaPods 中,所有的命令都会由 Command 类派发到将对应的类,而真正执行 pod install 的类就是 Install:

module Pod
class Command
class Install < Command
def run
verify_podfile_exists!
installer = installer_for_config
installer.repo_update = repo_update?(:default => false)
installer.update = false
installer.install!
end
end
end
end

这里面会从配置类的实例 config 中获取一个 Installer 的实例,然后执行 install! 方法,这里的 installer 有一个 update 属性,而这也就是 pod install 和 update 之间最大的区别,其中后者会无视已有的 Podfile.lock 文件,重新对依赖进行分析

module Pod
class Command
class Update < Command
def run
...

installer = installer_for_config
installer.repo_update = repo_update?(:default => true)
installer.update = true
installer.install!
end
end
end
end

Podfile 的解析

Podfile 中依赖的解析其实是与我们在手动解析 Podfile 章节所介绍的差不多,整个过程主要都是由 CocoaPods-Core 这个模块来完成的,而这个过程早在 installer_for_config 中就已经开始了:

def installer_for_config
Installer.new(config.sandbox, config.podfile, config.lockfile)
end

这个方法会从 config.podfile 中取出一个 Podfile 类的实例:

def podfile
@podfile ||= Podfile.from_file(podfile_path) if podfile_path
end

类方法 Podfile.from_file 就定义在 CocoaPods-Core 这个库中,用于分析 Podfile 中定义的依赖,这个方法会根据 Podfile 不同的类型选择不同的调用路径:

Podfile.from_file
`-- Podfile.from_ruby
|-- File.open
`-- eval

from_ruby 类方法就会像我们在前面做的解析 Podfile 的方法一样,从文件中读取数据,然后使用 eval 直接将文件中的内容当做 Ruby 代码来执行。

def self.from_ruby(path, contents = nil)
contents ||= File.open(path, 'r:utf-8', &:read)

podfile = Podfile.new(path) do
begin
eval(contents, nil, path.to_s)
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
end
podfile
end

在 Podfile 这个类的顶部,我们使用 Ruby 的 Mixin 的语法来混入 Podfile 中代码执行所需要的上下文:

include Pod::Podfile::DSL

Podfile 中的所有你见到的方法都是定义在 DSL 这个模块下面的:

module Pod
class Podfile
module DSL
def pod(name = nil, *requirements) end
def target(name, options = nil) end
def platform(name, target = nil) end
def inhibit_all_warnings! end
def use_frameworks!(flag = true) end
def source(source) end
...
end
end
end

这里定义了很多 Podfile 中使用的方法,当使用 eval 执行文件中的代码时,就会执行这个模块里的方法,在这里简单看一下其中几个方法的实现,比如说 source 方法:

def source(source)
hash_sources = get_hash_value('sources') || []
hash_sources << source
set_hash_value('sources', hash_sources.uniq)
end

该方法会将新的 source 加入已有的源数组中,然后更新原有的 sources 对应的值。

稍微复杂一些的是 target 方法:

def target(name, options = nil)
if options
raise Informative, "Unsupported options `#{options}` for " \
"target `#{name}`."
end

parent = current_target_definition
definition = TargetDefinition.new(name, parent)
self.current_target_definition = definition
yield if block_given?
ensure
self.current_target_definition = parent
end

这个方法会创建一个 TargetDefinition 类的实例,然后将当前环境系的 target_definition 设置成这个刚刚创建的实例。这样,之后使用 pod 定义的依赖都会填充到当前的 TargetDefinition 中:

def pod(name = nil, *requirements)
unless name
raise StandardError, 'A dependency requires a name.'
end

current_target_definition.store_pod(name, *requirements)
end

当 pod 方法被调用时,会执行 store_pod 将依赖存储到当前 target 中的 dependencies 数组中:

def store_pod(name, *requirements)
return if parse_subspecs(name, requirements)
parse_inhibit_warnings(name, requirements)
parse_configuration_whitelist(name, requirements)

if requirements && !requirements.empty?
pod = { name => requirements }
else
pod = name
end

get_hash_value('dependencies', []) << pod
nil
end

总结一下,CocoaPods 对 Podfile 的解析与我们在前面做的手动解析 Podfile 的原理差不多,构建一个包含一些方法的上下文,然后直接执行 eval 方法将文件的内容当做代码来执行,这样只要 Podfile 中的数据是符合规范的,那么解析 Podfile 就是非常简单容易的。

安装依赖的过程

Podfile 被解析后的内容会被转化成一个 Podfile 类的实例,而 Installer 的实例方法 install! 就会使用这些信息安装当前工程的依赖,而整个安装依赖的过程大约有四个部分:

  • 解析 Podfile 中的依赖
  • 下载依赖
  • 创建 Pods.xcodeproj 工程
  • 集成 workspace
def install!
resolve_dependencies
download_dependencies
generate_pods_project
integrate_user_project
end

在上面的 install 方法调用的 resolve_dependencies 会创建一个 Analyzer 类的实例,在这个方法中,你会看到一些非常熟悉的字符串:

def resolve_dependencies
analyzer = create_analyzer

plugin_sources = run_source_provider_hooks
analyzer.sources.insert(0, *plugin_sources)

UI.section 'Updating local specs repositories' do
analyzer.update_repositories
end if repo_update?

UI.section 'Analyzing dependencies' do
analyze(analyzer)
validate_build_configurations
clean_sandbox
end
end

在使用 CocoaPods 中经常出现的 Updating local specs repositories 以及 Analyzing dependencies 就是从这里输出到终端的,该方法不仅负责对本地所有 PodSpec 文件的更新,还会对当前 Podfile 中的依赖进行分析:

def analyze(analyzer = create_analyzer)
analyzer.update = update
@analysis_result = analyzer.analyze
@aggregate_targets = analyzer.result.targets
end

analyzer.analyze 方法最终会调用 Resolver 的实例方法 resolve:

def resolve
dependencies = podfile.target_definition_list.flat_map do |target|
target.dependencies.each do |dep|
@platforms_by_dependency[dep].push(target.platform).uniq! if target.platform
end
end
@activated = Molinillo::Resolver.new(self, self).resolve(dependencies, locked_dependencies)
specs_by_target
rescue Molinillo::ResolverError => e
handle_resolver_error(e)
end

这里的 Molinillo::Resolver 就是用于解决依赖关系的类。

解决依赖关系(Resolve Dependencies)

CocoaPods 为了解决 Podfile 中声明的依赖关系,使用了一个叫做 Milinillo 的依赖关系解决算法;但是,笔者在 Google 上并没有找到与这个算法相关的其他信息,推测是 CocoaPods 为了解决 iOS 中的依赖关系创造的算法。

Milinillo 算法的核心是 回溯(Backtracking) 以及 向前检查(forward check)),整个过程会追踪栈中的两个状态(依赖和可能性)。

在这里并不想陷入对这个算法执行过程的分析之中,如果有兴趣可以看一下仓库中的 ARCHITECTURE.md 文件,其中比较详细的解释了 Milinillo 算法的工作原理,并对其功能执行过程有一个比较详细的介绍。

Molinillo::Resolver 方法会返回一个依赖图,其内容大概是这样的:

Molinillo::DependencyGraph:[
Molinillo::DependencyGraph::Vertex:AFNetworking(#<Pod::Specification name="AFNetworking">),
Molinillo::DependencyGraph::Vertex:SDWebImage(#<Pod::Specification name="SDWebImage">),
Molinillo::DependencyGraph::Vertex:Masonry(#<Pod::Specification name="Masonry">),
Molinillo::DependencyGraph::Vertex:Typeset(#<Pod::Specification name="Typeset">),
Molinillo::DependencyGraph::Vertex:CCTabBarController(#<Pod::Specification name="CCTabBarController">),
Molinillo::DependencyGraph::Vertex:BlocksKit(#<Pod::Specification name="BlocksKit">),
Molinillo::DependencyGraph::Vertex:Mantle(#<Pod::Specification name="Mantle">),
...
]

这个依赖图是由一个结点数组组成的,在 CocoaPods 拿到了这个依赖图之后,会在 specs_by_target 中按照 Target 将所有的 Specification 分组:

{
#<Pod::Podfile::TargetDefinition label=Pods>=>[],
#<Pod::Podfile::TargetDefinition label=Pods-Demo>=>[
#<Pod::Specification name="AFNetworking">,
#<Pod::Specification name="AFNetworking/NSURLSession">,
#<Pod::Specification name="AFNetworking/Reachability">,
#<Pod::Specification name="AFNetworking/Security">,
#<Pod::Specification name="AFNetworking/Serialization">,
#<Pod::Specification name="AFNetworking/UIKit">,
#<Pod::Specification name="BlocksKit/Core">,
#<Pod::Specification name="BlocksKit/DynamicDelegate">,
#<Pod::Specification name="BlocksKit/MessageUI">,
#<Pod::Specification name="BlocksKit/UIKit">,
#<Pod::Specification name="CCTabBarController">,
#<Pod::Specification name="CategoryCluster">,
...
]
}

而这些 Specification 就包含了当前工程依赖的所有第三方框架,其中包含了名字、版本、源等信息,用于依赖的下载。

下载依赖

在依赖关系解决返回了一系列 Specification 对象之后,就到了 Pod install 的第二部分,下载依赖:

def install_pod_sources
@installed_specs = []
pods_to_install = sandbox_state.added | sandbox_state.changed
title_options = { :verbose_prefix => '-> '.green }
root_specs.sort_by(&:name).each do |spec|
if pods_to_install.include?(spec.name)
if sandbox_state.changed.include?(spec.name) && sandbox.manifest
previous = sandbox.manifest.version(spec.name)
title = "Installing #{spec.name} #{spec.version} (was #{previous})"
else
title = "Installing #{spec}"
end
UI.titled_section(title.green, title_options) do
install_source_of_pod(spec.name)
end
else
UI.titled_section("Using #{spec}", title_options) do
create_pod_installer(spec.name)
end
end
end
end

在这个方法中你会看到更多熟悉的提示,CocoaPods 会使用沙盒(sandbox)存储已有依赖的数据,在更新现有的依赖时,会根据依赖的不同状态显示出不同的提示信息:

-> Using AFNetworking (3.1.0)

-> Using AKPickerView (0.2.7)

-> Using BlocksKit (2.2.5) was (2.2.4)

-> Installing MBProgressHUD (1.0.0)
...

虽然这里的提示会有三种,但是 CocoaPods 只会根据不同的状态分别调用两种方法:

  • install_source_of_pod
  • create_pod_installer

create_pod_installer 方法只会创建一个 PodSourceInstaller 的实例,然后加入 pod_installers 数组中,因为依赖的版本没有改变,所以不需要重新下载,而另一个方法的 install_source_of_pod 的调用栈非常庞大:

installer.install_source_of_pod
|-- create_pod_installer
| `-- PodSourceInstaller.new
`-- podSourceInstaller.install!
`-- download_source
`-- Downloader.download
`-- Downloader.download_request
`-- Downloader.download_source
|-- Downloader.for_target
| |-- Downloader.class_for_options
| `-- Git/HTTP/Mercurial/Subversion.new
|-- Git/HTTP/Mercurial/Subversion.download
`-- Git/HTTP/Mercurial/Subversion.download!
`-- Git.clone

在调用栈的末端 Downloader.download_source 中执行了另一个 CocoaPods 组件 CocoaPods-Download 中的方法:

def self.download_source(target, params)
FileUtils.rm_rf(target)
downloader = Downloader.for_target(target, params)
downloader.download
target.mkpath

if downloader.options_specific?
params
else
downloader.checkout_options
end
end

方法中调用的 for_target 根据不同的源会创建一个下载器,因为依赖可能通过不同的协议或者方式进行下载,比如说 Git/HTTP/SVN 等等,组件 CocoaPods-Downloader 就会根据 Podfile 中依赖的参数选项使用不同的方法下载依赖。

大部分的依赖都会被下载到 ~/Library/Caches/CocoaPods/Pods/Release/ 这个文件夹中,然后从这个这里复制到项目工程目录下的 ./Pods 中,这也就完成了整个 CocoaPods 的下载流程。

生成 Pods.xcodeproj

CocoaPods 通过组件 CocoaPods-Downloader 已经成功将所有的依赖下载到了当前工程中,这里会将所有的依赖打包到 Pods.xcodeproj 中:

def generate_pods_project(generator = create_generator)
UI.section 'Generating Pods project' do
generator.generate!
@pods_project = generator.project
run_podfile_post_install_hooks
generator.write
generator.share_development_pod_schemes
write_lockfiles
end
end

generate_pods_project 中会执行 PodsProjectGenerator 的实例方法 generate!:

def generate!
prepare
install_file_references
install_libraries
set_target_dependencies
end

这个方法做了几件小事:

  • 生成 Pods.xcodeproj 工程
  • 将依赖中的文件加入工程
  • 将依赖中的 Library 加入工程
  • 设置目标依赖(Target Dependencies)

这几件事情都离不开 CocoaPods 的另外一个组件 Xcodeproj,这是一个可以操作一个 Xcode 工程中的 Group 以及文件的组件,我们都知道对 Xcode 工程的修改大多数情况下都是对一个名叫 project.pbxproj 的文件进行修改,而 Xcodeproj 这个组件就是 CocoaPods 团队开发的用于操作这个文件的第三方库。

生成 workspace

最后的这一部分与生成 Pods.xcodeproj 的过程有一些相似,这里使用的类是 UserProjectIntegrator,调用方法 integrate! 时,就会开始集成工程所需要的 Target:

def integrate!
create_workspace
integrate_user_targets
warn_about_xcconfig_overrides
save_projects
end

对于这一部分的代码,也不是很想展开来细谈,简单介绍一下这里的代码都做了什么,首先会通过 Xcodeproj::Workspace 创建一个 workspace,之后会获取所有要集成的 Target 实例,调用它们的 integrate! 方法:

def integrate!
UI.section(integration_message) do
XCConfigIntegrator.integrate(target, native_targets)

add_pods_library
add_embed_frameworks_script_phase
remove_embed_frameworks_script_phase_from_embedded_targets
add_copy_resources_script_phase
add_check_manifest_lock_script_phase
end
end

方法将每一个 Target 加入到了工程,使用 Xcodeproj 修改 Copy Resource Script Phrase 等设置,保存 project.pbxproj,整个 Pod install 的过程就结束了。

总结

最后想说的是 pod install 和 pod update 区别还是比较大的,每次在执行 pod install 或者 update 时最后都会生成或者修改 Podfile.lock 文件,其中前者并不会修改 Podfile.lock 中显示指定的版本,而后者会会无视该文件的内容,尝试将所有的 pod 更新到最新版。

CocoaPods 工程的代码虽然非常多,不过代码的逻辑非常清晰,整个管理并下载依赖的过程非常符合直觉以及逻辑。

作者:Draveness

链接:https://zhuanlan.zhihu.com/p/22652365


收起阅读 »

kotlin 协变、逆变 - 猫和鱼的故事

网上找的一段协变、逆变比较正式的定义:逆变与协变用来描述类型转换后的继承关系,其定义:如果 A、B 表示类型,f(⋅) 表示类型转换,≦ 表示继承关系(比如,A≦B 表示 A 是由 B 派生出来的子类): 当 A ≦ B 时,如果有 f(A) ≦ f(B) ,...
继续阅读 »

网上找的一段协变、逆变比较正式的定义:

逆变与协变用来描述类型转换后的继承关系,其定义:如果 A、B 表示类型,f(⋅) 表示类型转换, 表示继承关系(比如,A≦B 表示 A 是由 B 派生出来的子类): 当 A ≦ B 时,如果有 f(A) ≦ f(B) ,那么 f 是协变的; 当 A ≦ B 时,如果有 f(B) ≦ f(A) ,那么 f 是逆变的; 如果上面两种关系都不成立,即 (A) 与 f(B) 相互之间没有继承关系,则叫做不变的。

java 中可以通过如下泛型通配符以支持协变和逆变:

  • ? extends 来使泛型支持协变。修饰的泛型集合只能读取不能修改,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。
  • ? super 来使泛型支持逆变。修饰的泛型集合只能修改不能读取,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

以动物举例,看代码。

abstract class Animal {
void eat() {
System.out.println("我是" + myName() + ", 我最喜欢吃" + myFavoriteFood());
}

abstract String myName();

abstract String myFavoriteFood();
}

class Fish extends Animal {

@Override
String myName() {
return "鱼";
}

@Override
String myFavoriteFood() {
return "虾米";
}
}

class Cat extends Animal {

@Override
String myName() {
return "猫";
}

@Override
String myFavoriteFood() {
return "小鱼干";
}
}

public static void extendsFun() {
List fishList = new ArrayList<>();
fishList.add(new Fish());
List catList = new ArrayList<>();
catList.add(new Cat());
List animals1 = fishList;
List animals2 = catList;

animals2.add(new Fish()); // 报错
Animal animal1 = animals1.get(0);
Animal animal2 = animals2.get(0);
animal1.eat();
animal2.eat();
}

//输出结果:
我是鱼, 我最喜欢吃虾米
我是猫, 我最喜欢吃小鱼干

协变就好比有多个集合,每个集合存储的是某中特定动物(extends Animal),但是不告诉你那个集合里存储的是鱼,哪个是猫。所以你虽然可以从任意一个集合中读取一个动物信息,没有问题,但是你没办法将一条鱼的信息存储到鱼的集合里,因为仅从变量 animals1、animals2 的类型声明上来看你不知道哪个集合里存储的是鱼,哪个集合里是猫。 假如报错的代码不报错了,那不就说明把一条鱼塞进了一堆猫里,这属于给猫加菜啊,所以肯定是不行的。? extends 类型通配符所表达的协变就是这个意思。

那逆变是什么意思呢?还是以上面的动物举例:

public static void superFun() {
List fishList = new ArrayList<>();
fishList.add(new Fish());
List animalList = new ArrayList<>();
animalList.add(new Cat());
animalList.add(new Fish());
List fish1 = fishList;
List fish2 = animalList;

fish1.add(new Fish());
Fish fish = fish2.get(0); //报错
}

从变量 fish1、fish2 的类型声明上只能知道里面存储的都是鱼的父类,如果这里也不报错的话可就从 fish2 的集合里拿出一只猫赋值给一条鱼了,这属于谋杀亲鱼。所以肯定也是不行。? super 类型通配符所表达的逆变就是这个意思。

kotlin 中对于协变和逆变也提供了两个修饰符:

  • out:声明协变;
  • in:声明逆变。

它们有两种使用方式:

  • 第一种:和 java 一样在使用处声明;
  • 第二种:在类或接口的定义处声明。

当和 java 一样在使用处声明时,将上面 java 示例转换为 kotlin

fun extendsFun() {
val fishList: MutableList = ArrayList()
fishList.add(Fish())
val catList: MutableList = ArrayList()
catList.add(Cat())
val animals1: MutableList = fishList
val animals2: MutableList = catList
animals2.add(Fish()) // 报错
val animal1 = animals1[0]
val animal2 = animals2[0]
animal1.eat()
animal2.eat()
}

fun superFun() {
val fishList: MutableList = ArrayList()
fishList.add(Fish())
val animalList: MutableList = ArrayList()
animalList.add(Cat())
animalList.add(Fish())
val fish1: MutableList = fishList
val fish2: MutableList = animalList
fish1.add(Fish())
val fish: Fish = fish2[0] //报错
}

可以看到在 kotlin 代码中除了将 ? extends 替换为了 out,将 ? super 替换为了 in,其他地方并没有发生变化,而产生的结果是一样的。那在类或接口的定义处声明 in、out 的作用是什么呢。

假设有一个泛型接口 Source,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

// Java
interface Source {
T nextT();
}

那么,在 Source  类型的变量中存储 Source  实例的引用是极为安全的——没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

// Java
void demo(Source strs) {
Source objects = strs; // !!!在 Java 中不允许
// ……
}

为了修正这一点,我们必须声明对象的类型为 Source,但这样的方式很复杂。而在 kotlin 中有一种简单的方式向编译器解释这种情况。我们可以标注 Source 的类型参数 T 来确保它仅从 Source 成员中返回(生产),并从不被消费。为此我们使用 out 修饰符修饰泛型 T

interface Source {
fun nextT(): T
}

fun demo(strs: Source) {
val objects: Source = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}

还记得开篇协变的定义吗?

当 A ≦ B 时,如果有 f(A) ≦ f(B) ,那么 f 是协变的; 当 A ≦ B 时,如果有 f(B) ≦ f(A) ,那么 f 是逆变的;

也就是说:

当一个类 C 的类型参数 T 被声明为 out 时,那么就意味着类 C 在参数 T 上是协变的;参数 T 只能出现在类 C 的输出位置,不能出现在类 C 的输入位置。

同样的,对于 in 修饰符来说

当一个类 C 的类型参数 T 被声明为 in 时,那么就意味着类 C 在参数 T 上是逆变的;参数 T 只能出现在类 C 的输如位置,不能出现在类 C 的输出位置。

interface Comparable {
operator fun compareTo(other: T): Int
}

fun demo(x: Comparable) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable 的变量
val y: Comparable = x // OK!
}

总结如下表:

image

收起阅读 »

Cocoapods原理总结

CocoaPods是IOS项目的依赖管理工具,类似于Android的gradle,不过gradle不仅有依赖管理功能,还能负责构建。CocoaPods只负责管理依赖,即对第三方库的依赖,像gradle一样支持传递依赖,即如果A依赖于B,B依赖C,我们在A工程里...
继续阅读 »

CocoaPods是IOS项目的依赖管理工具,类似于Android的gradle,不过gradle不仅有依赖管理功能,还能负责构建。CocoaPods只负责管理依赖,即对第三方库的依赖,像gradle一样支持传递依赖,即如果A依赖于B,B依赖C,我们在A工程里指出A依赖于B,CocoaPods会自动为我们下载C,并在构建时链接C库。

IOS工程有3种库项目,framework,static library,meta library,我们通常只使用前两种。我们在使用static library库工程时,一般使用它编译出来的静态库libxxx.a,以及对应的头文件,在写应用时,将这些文件拷贝到项目里,然后将静态库添加到链接的的依赖库路径里,并将头文件目录添加到头文件搜索目录中。而framework库的依赖会简单很多,framework是资源的集合,将静态库和其头文件包含在framework目录里。framework库类似于Android工程的aar库。而static library类似于Android工程的jar包。

CocoaPods同时支持static library和framework的依赖管理,下面介绍这两种情况下CocoaPods是如何实现构建上的依赖的

static library

先看一下使用CocoaPods管理依赖前项目的文件结构

1
2
3
4
5
6
7
8
CardPlayer
├── CardPlayer
│   ├── CardPlayer
│   ├── CardPlayer.xcodeproj
│   ├── CardPlayerTests
│   └── CardPlayerUITests
├── exportOptions.plist
└── wehere-dev-cloud.mobileprovision

然后我们使用Pod来管理依赖,编写的PodFile如下所示:

1
2
3
4
5
6
project 'CardPlayer/CardPlayer.xcodeproj'

target 'CardPlayer' do
pod 'AFNetworking', '~> 1.0'
end

文件结构的变化

然后使用pod install,添加好依赖之后,项目的文件结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CardPlayer
├── CardPlayer
│   ├── CardPlayer
│   ├── CardPlayer.xcodeproj
│   ├── CardPlayerTests
│   └── CardPlayerUITests
├── CardPlayer.xcworkspace
│   └── contents.xcworkspacedata
├── PodFile
├── Podfile.lock
├── Pods
│   ├── AFNetworking
│   ├── Headers
│   ├── Manifest.lock
│   ├── Pods.xcodeproj
│   └── Target\ Support\ Files
├── exportOptions.plist
└── wehere-dev-cloud.mobileprovision

可以看到我们添加了如下文件

  1. PodFile 依赖描述文件

  2. Podfile.lock 当前安装的依赖库的版本

  3. CardPlayer.xcworkspace

    xcworkspace文件,使用CocoaPod管理依赖的项目,XCode只能使用workspace编译项目,如果还只打开以前的xcodeproj文件进行开发,编译会失败

    xcworkspace文件实际是一个文件夹,实际Workspace信息保存在contents.xcworkspacedata里,该文件的内容非常简单,实际上只指示它所使用的工程的文件目录

    如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?xml version="1.0" encoding="UTF-8"?>
    <Workspace
    version = "1.0">
    <FileRef
    location = "group:CardPlayer/CardPlayer.xcodeproj">
    </FileRef>
    <FileRef
    location = "group:Pods/Pods.xcodeproj">
    </FileRef>
    </Workspace>

  4. Pods目录

    1. Pods.xcodeproj,Pods工程,所有第三方库由Pods工程构建,每个第3方库对应Pods工程的1个target,并且这个工程还有1个Pods-Xxx的target,接下来在介绍工程时再详细介绍

    2. AFNetworking 每个第3方库,都会在Pods目录下有1个对应的目录

    3. Headers

      在Headers下有两个目录,Private和Public,第3方库的私有头文件会在Private目录下有对应的头文件,不过是1个软链接,链接到第3方库的头文件 第3方库的Pubic头文件会在Public目录下有对应的头文件,也是软链接

      如下所示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      Headers/
      ├── Private
      │   └── AFNetworking
      │   ├── AFHTTPClient.h -> ../../../AFNetworking/AFNetworking/AFHTTPClient.h
      │   ├── AFHTTPRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFHTTPRequestOperation.h
      │   ├── AFImageRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFImageRequestOperation.h
      │   ├── AFJSONRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFJSONRequestOperation.h
      │   ├── AFNetworkActivityIndicatorManager.h -> ../../../AFNetworking/AFNetworking/AFNetworkActivityIndicatorManager.h
      │   ├── AFNetworking.h -> ../../../AFNetworking/AFNetworking/AFNetworking.h
      │   ├── AFPropertyListRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFPropertyListRequestOperation.h
      │   ├── AFURLConnectionOperation.h -> ../../../AFNetworking/AFNetworking/AFURLConnectionOperation.h
      │   ├── AFXMLRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFXMLRequestOperation.h
      │   └── UIImageView+AFNetworking.h -> ../../../AFNetworking/AFNetworking/UIImageView+AFNetworking.h
      └── Public
      └── AFNetworking
      ├── AFHTTPClient.h -> ../../../AFNetworking/AFNetworking/AFHTTPClient.h
      ├── AFHTTPRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFHTTPRequestOperation.h
      ├── AFImageRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFImageRequestOperation.h
      ├── AFJSONRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFJSONRequestOperation.h
      ├── AFNetworkActivityIndicatorManager.h -> ../../../AFNetworking/AFNetworking/AFNetworkActivityIndicatorManager.h
      ├── AFNetworking.h -> ../../../AFNetworking/AFNetworking/AFNetworking.h
      ├── AFPropertyListRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFPropertyListRequestOperation.h
      ├── AFURLConnectionOperation.h -> ../../../AFNetworking/AFNetworking/AFURLConnectionOperation.h
      ├── AFXMLRequestOperation.h -> ../../../AFNetworking/AFNetworking/AFXMLRequestOperation.h
      └── UIImageView+AFNetworking.h -> ../../../AFNetworking/AFNetworking/UIImageView+AFNetworking.h

    4. Manifest.lock manifest文件 描述第3方库对其它库的依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      PODS:
      - AFNetworking (1.3.4)

      DEPENDENCIES:
      - AFNetworking (~> 1.0)

      SPEC CHECKSUMS:
      AFNetworking: cf8e418e16f0c9c7e5c3150d019a3c679d015018

      PODFILE CHECKSUM: 349872ccf0789fbe3fa2b0f912b1b5388eb5e1a9

      COCOAPODS: 1.3.1

    5. Target Support Files 支撑target的文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      Target\ Support\ Files/
      ├── AFNetworking
      │   ├── AFNetworking-dummy.m
      │   ├── AFNetworking-prefix.pch
      │   └── AFNetworking.xcconfig
      └── Pods-CardPlayer
      ├── Pods-CardPlayer-acknowledgements.markdown
      ├── Pods-CardPlayer-acknowledgements.plist
      ├── Pods-CardPlayer-dummy.m
      ├── Pods-CardPlayer-frameworks.sh
      ├── Pods-CardPlayer-resources.sh
      ├── Pods-CardPlayer.debug.xcconfig
      └── Pods-CardPlayer.release.xcconfig

      在Target Support Files目录下每1个第3方库都会有1个对应的文件夹,比如AFNetworking,该目录下有一个空实现文件,也有预定义头文件用来优化头文件编译速度,还会有1个xcconfig文件,该文件会在工程配置中使用,主要存放头文件搜索目录,链接的Flag(比如链接哪些库)

      在Target Support Files目录下还会有1个Pods-XXX的文件夹,该文件夹存放了第3方库声明文档markdown文档和plist文件,还有1个dummy的空实现文件,还有debug和release各自对应的xcconfig配置文件,另外还有2个脚本文件,Pods-XXX-frameworks.sh脚本用于实现framework库的链接,当依赖的第3方库是framework形式才会用到该脚本,另外1个脚本文件: Pods-XXX-resources.sh用于编译storyboard类的资源文件或者拷贝*.xcassets之类的资源文件

工程结构的变化

上一节里提到在引入CocoaPods管理依赖后,会新增workspace文件,新增的workspace文件会引用原有的应用主工程,还会引用新增的Pods工程。后续不能再直接打开原来的应用主工程进行编译,否则会失败。实际上是因为原来的应用主工程的配置现在也有了变化。下面分别介绍一下Pods工程以及主工程的变化。

Pods工程

Pods工程配置

Pods工程会为每个依赖的第3方库定义1个Target,还会定义1个Pods-Xxx的target,每个Target会生成1个静态库,如下图所示:

cocoapods_pod_project_target

Pods工程会新建Debug和Release两个Configuration,每个Configuration会为不同的target设置不同的xcconfig,xcconfig指出了头文件查找目录,要链接的第3方库,链接目录等信息,如下图所示:

cocoapods_project_target_configuration

AFNetworking.xcconfig文件的内容如下所示:

1
2
3
4
5
6
7
8
9
10
CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/AFNetworking
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/AFNetworking" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking"
OTHER_LDFLAGS = -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/AFNetworking
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES

上述内容说明了AFNetworking编译时查找头文件的目录Header_SERACH_PATHS,OTHER_LD_FLAGS指明了要链接的framework

Pods-CardPlayer.debug.xcconfig文件的内容如下所示:

1
2
3
4
5
6
7
8
9
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking"
LIBRARY_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking"
OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking"
OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_PODFILE_DIR_PATH = ${SRCROOT}/..
PODS_ROOT = ${SRCROOT}/../Pods

Pods-CardPlayer.debug文件中OTHER_LDFLAGS说明了编译Pods时需要链接AFNetworking库,还需要链接其它framework

所以我们在xcode里能看到AFNetworking依赖的framework:

cocoapods_target_lib_dependency

Pods工程文件组织

IOS工程在XCode上看到的结构和文件系统的结构并不一致,在XCode上看到的文件夹并不是物理的文件夹,而是叫做Group,在组织IOS工程时,会将逻辑关系较近的文件放在同一个Group下。如下图所示:

cocoapods_pods_project_files

coacoapods_pods_project_afnetworking_support

可以看到Group的组织大概是以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Pods
├── Podfile # 指向根目录下的Podfile 说明依赖的第3方库
├── Frameworks # 文件系统并没有对应的目录 这只是1个虚拟的group 表示需要链接的frameowork
├── └── iOS # 文件系统并没有对应的目录 这只是1个虚拟的group 这里表示是ios需要链接的framework
├── └── Xxx.framework # 链接的frameowork列表
├── Pods # 虚拟的group 管理所有第3方库
│   └── AFNetwoking #AFNetworking库 虚拟group 对应文件系统Pods/AFNetworking/AFNetworking目录下的内容
│   ├── xxx.h #AFNetworking库的头文件 对应文件系统Pods/AFNetworking/AFNetworking目录下的所有头文件
│ ├── xxx.m #AFNetworking库的实现文件 对应文件系统Pods/AFNetworking/AFNetworking目录下的所有实现文件
│   └── Support Files # 虚拟group 支持文件 没有直接对应的文件系统目录,该group下的文件都属于目录: Pods/Target Support Files/AFNetworking/
│ ├── AFNetworking.xcconfig # AFNetworking编译的工程配置文件
│ ├── AFNetworking-prefix.pch # AFNetworking编译用的预编译头文件
│ └── AFNetworking-dummy.m # 空实现文件
├── Products # 虚拟group
│ ├── libAFNetworking.a # AFNetworking target将生成的静态库
│ └── libPods-CardPlayer.a # Pods-CardPlayer target将生成的静态库
└── Targets Support Files # 虚拟group 管理支持文件
└── Pods-CardPlayer # 虚拟group Pods-CardPlayer target
├── Pods-CardPlayer-acknowledgements.markdown # 协议说明文档
├── Pods-CardPlayer-acknowledgements.plist # 协议说明文档
├── Pods-CardPlayer-dummy.m # 空实现
├── Pods-CardPlayer-frameworks.sh # 安装framework的脚本
├── Pods-CardPlayer-resources.sh # 安装resource的脚本
├── Pods-CardPlayer.debug.xcconfig # debug configuration 的 配置文件
└── Pods-CardPlayer.release.xcconfig # release configuration 的 配置文件

主工程

引入CocoaPods之后, 主工程的设置其实也会变化, 我们先看一下引入之前,主工程的Configuration设置,如下图所示:       

cocoapods_before_project_config

可以看到Debug和Release的Configuration没有设置任何配置文件,再看引入CocoaPods之后,主工程的Configuration如下图所示:

cocoapod_main_project_configuration

可以看到采用CocoaPods之后,Debug Configuration设置了配置文件Pods-CardPlayer.debug.xcconfig文件,Release Configuration则设置了配置文件Pods-CardPlayer.release.xcconfig文件,这些配置文件指明了头文件的查找目录,要链接的第三方库

编译并链接第3方库的原理

   

  1. 头文件的查找

    上一节里已经讲到主工程的Configuration已经设置了配置文件,而这份配置文件里说明了头文件的查找目录:

    1
    2
    HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking"
    OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking"

    所以主工程可以引用第3方库的头文件,比如像这样: #import <AFNetworking/AFHTTPClient.h>

  2. 如何链接库

    配置文件同样说明了链接库的查找目录以及要链接的库:

    1
    2
    3
    LIBRARY_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking"
    OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"

    而在我们主工程的main target还会添加对libPods-CardPlayer.a的链接,如下图所示:

    cocoapod_main_project_dependency_pods

  3. 编译顺序

    我们的主工程的main target显示指出了需要链接库libPods-CardPlayer.a,而libPods-CardPlayer.a由target Pods-CardPlayer产生,所以主工程的main target将会隐式依赖于target Pods-CardPlayer,而在target Pods-CardPlayer的配置中,显示指出了依赖对第三方库对应的target的依赖,如下所示:

    cocoapods_pods_dendency

    所以main target -> target Pods-CardPlayer -> 第3方库对应的target

    因为存在上述依赖关系,所以能保证编译顺序,保证编译链接都不会有问题

framework

如果我们在PodFile设置了use_frameworks!,则第3方库使用Framework形式的库,PodFile的内容如下所示:

1
2
3
4
5
6
7
8
project 'CardPlayer/CardPlayer.xcodeproj'

use_frameworks!

target 'CardPlayer' do
pod 'AFNetworking', '~> 1.0'
end

framework这类型的库和static library比较类似,在文件结构上没什么太大变化,都是新增了Pods工程,和管理Pods工程及原主工程的workspace,但是Pods工程设置的target的类型都是framework,而不是static library,而主工程对Pods的依赖,也不再是依赖libPods-CardPlayer.a,而是Pods_CardPlayer.framework。

如下图所示:

cocoapods_framework_dependency

cocoapods_pods_framework_thrid_party

另外编译配置文件也有一些不同:

AFNetworking.xcconfig文件如下所示:

1
2
3
4
5
6
7
8
9
10
CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/AFNetworking
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public"
OTHER_LDFLAGS = -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/AFNetworking
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES

而Pods-CardPlayer.debug.xcconfig文件的内容如下所示:

1
2
3
4
5
6
7
8
9
FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/AFNetworking/AFNetworking.framework/Headers"
OTHER_LDFLAGS = $(inherited) -framework "AFNetworking"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_PODFILE_DIR_PATH = ${SRCROOT}/..
PODS_ROOT = ${SRCROOT}/../Pods

使用framework形式的库之后,Pods-CardPlayer-frameworks.sh脚本也有一些不同,

1
2
3
4
5
6
7
8
9
10
11
...
f [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework"
fi
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
wait
fi

编译framework后,它会将AFNetworking.framework安装到产品编译目录下,这样才能在运行时链接该framework

而我们的主工程的main target配置Build Phases有一项安装pod的framework,会调用Pod-CardPlayer-frameworks.sh,所以能保证正确安装framework,如下图所示:

cocoapods_target_embed_pods_framework



本文原创作者:Cloud Chou

链接:http://www.cloudchou.com/ios/post-990.html



收起阅读 »

深入理解 CocoaPods

CocoaPods 是开发 OS X 和 iOS 应用程序的一个第三方库的依赖管理工具。利用 CocoaPods,可以定义自己的依赖关系 (称作 pods),并且随着时间的变化,以及在整个开发环境中对第三方库的版本管理非常方便。CocoaPods 背...
继续阅读 »

CocoaPods 是开发 OS X 和 iOS 应用程序的一个第三方库的依赖管理工具。利用 CocoaPods,可以定义自己的依赖关系 (称作 pods),并且随着时间的变化,以及在整个开发环境中对第三方库的版本管理非常方便。

CocoaPods 背后的理念主要体现在两个方面。首先,在工程中引入第三方代码会涉及到许多内容。针对 Objective-C 初级开发者来说,工程文件的配置会让人很沮丧。在配置 build phases 和 linker flags 过程中,会引起许多人为因素的错误。CocoaPods 简化了这一切,它能够自动配置编译选项。

其次,通过 CocoaPods,可以很方便的查找到新的第三方库。当然,这并不是说你可以简单的将别人提供的库拿来拼凑成一个应用程序。它的真正作用是让你能够找到真正好用的库,以此来缩短我们的开发周期和提升软件的质量。

本文中,我们将通过分析 pod 安装 (pod install) 的过程,一步一步揭示 CocoaPods 背后的技术。

核心组件

CocoaPods是用 Ruby 写的,并由若干个 Ruby 包 (gems) 构成的。在解析整合过程中,最重要的几个 gems 分别是: CocoaPods/CocoaPodsCocoaPods/Core, 和 CocoaPods/Xcodeproj (是的,CocoaPods 是一个依赖管理工具 -- 利用依赖管理进行构建的!)。

编者注 CocoaPods 是一个 objc 的依赖管理工具,而其本身是利用 ruby 的依赖管理 gem 进行构建的

CocoaPods/CocoaPod

这是是一个面向用户的组件,每当执行一个 pod 命令时,这个组件都将被激活。该组件包括了所有使用 CocoaPods 涉及到的功能,并且还能通过调用所有其它的 gems 来执行任务。

CocoaPods/Core

Core 组件提供支持与 CocoaPods 相关文件的处理,文件主要是 Podfile 和 podspecs。

Podfile

Podfile 是一个文件,用于定义项目所需要使用的第三方库。该文件支持高度定制,你可以根据个人喜好对其做出定制。更多相关信息,请查阅 Podfile 指南

Podspec

.podspec 也是一个文件,该文件描述了一个库是怎样被添加到工程中的。它支持的功能有:列出源文件、framework、编译选项和某个库所需要的依赖等。

CocoaPods/Xcodeproj

这个 gem 组件负责所有工程文件的整合。它能够对创建并修改 .xcodeproj 和 .xcworkspace 文件。它也可以作为单独的一个 gem 包使用。如果你想要写一个脚本来方便的修改工程文件,那么可以使用这个 gem。

运行 pod install 命令

当运行 pod install 命令时会引发许多操作。要想深入了解这个命令执行的详细内容,可以在这个命令后面加上 --verbose。现在运行这个命令 pod install --verbose,可以看到类似如下的内容:

$ pod install --verbose

Analyzing dependencies

Updating spec repositories
Updating spec repo `master`
$ /usr/bin/git pull
Already up-to-date.


Finding Podfile changes
- AFNetworking
- HockeySDK

Resolving dependencies of `Podfile`
Resolving dependencies for target `Pods' (iOS 6.0)
- AFNetworking (= 1.2.1)
- SDWebImage (= 3.2)
- SDWebImage/Core

Comparing resolved specification to the sandbox manifest
- AFNetworking
- HockeySDK

Downloading dependencies

-> Using AFNetworking (1.2.1)

-> Using HockeySDK (3.0.0)
- Running pre install hooks
- HockeySDK

Generating Pods project
- Creating Pods project
- Adding source files to Pods project
- Adding frameworks to Pods project
- Adding libraries to Pods project
- Adding resources to Pods project
- Linking headers
- Installing libraries
- Installing target `
Pods-AFNetworking` iOS 6.0
- Adding Build files
- Adding resource bundles to Pods project
- Generating public xcconfig file at `
Pods/Pods-AFNetworking.xcconfig`
- Generating private xcconfig file at `
Pods/Pods-AFNetworking-Private.xcconfig`
- Generating prefix header at `
Pods/Pods-AFNetworking-prefix.pch`
- Generating dummy source file at `
Pods/Pods-AFNetworking-dummy.m`
- Installing target `
Pods-HockeySDK` iOS 6.0
- Adding Build files
- Adding resource bundles to Pods project
- Generating public xcconfig file at `
Pods/Pods-HockeySDK.xcconfig`
- Generating private xcconfig file at `
Pods/Pods-HockeySDK-Private.xcconfig`
- Generating prefix header at `
Pods/Pods-HockeySDK-prefix.pch`
- Generating dummy source file at `
Pods/Pods-HockeySDK-dummy.m`
- Installing target `
Pods` iOS 6.0
- Generating xcconfig file at `
Pods/Pods.xcconfig`
- Generating target environment header at `
Pods/Pods-environment.h`
- Generating copy resources script at `
Pods/Pods-resources.sh`
- Generating acknowledgements at `
Pods/Pods-acknowledgements.plist`
- Generating acknowledgements at `
Pods/Pods-acknowledgements.markdown`
- Generating dummy source file at `
Pods/Pods-dummy.m`
- Running post install hooks
- Writing Xcode project file to `
Pods/Pods.xcodeproj`
- Writing Lockfile in `
Podfile.lock`
- Writing Manifest in `
Pods/Manifest.lock`

Integrating client project

可以上到,整个过程执行了很多操作,不过把它们分解之后,再看看,会发现它们都很简单。让我们逐步来分析一下。

读取 Podfile 文件

你是否对 Podfile 的语法格式感到奇怪过,那是因为这是用 Ruby 语言写的。相较而言,这要比现有的其他格式更加简单好用一些。

在安装期间,第一步是要弄清楚显示或隐式的声明了哪些第三方库。在加载 podspecs 过程中,CocoaPods 就建立了包括版本信息在内的所有的第三方库的列表。Podspecs 被存储在本地路径 ~/.cocoapods 中。

版本控制和冲突

CocoaPods 使用语义版本控制 - Semantic Versioning 命名约定来解决对版本的依赖。由于冲突解决系统建立在非重大变更的补丁版本之间,这使得解决依赖关系变得容易很多。例如,两个不同的 pods 依赖于 CocoaLumberjack 的两个版本,假设一个依赖于 2.3.1,另一个依赖于 2.3.3,此时冲突解决系统可以使用最新的版本 2.3.3,因为这个可以向后与 2.3.1 兼容。

但这并不总是有效。有许多第三方库并不使用这样的约定,这让解决方案变得非常复杂。

当然,总会有一些冲突需要手动解决。如果一个库依赖于 CocoaLumberjack 的 1.2.5,另外一个库则依赖于 2.3.1,那么只有最终用户通过明确指定使用某个版本来解决冲突。

加载源文件

CocoaPods 执行的下一步是加载源码。每个 .podspec 文件都包含一个源代码的索引,这些索引一般包裹一个 git 地址和 git tag。它们以 commit SHAs 的方式存储在 ~/Library/Caches/CocoaPods 中。这个路径中文件的创建是由 Core gem 负责的。

CocoaPods 将依照 Podfile.podspec 和缓存文件的信息将源文件下载到 Pods 目录中。

生成 Pods.xcodeproj

每次 pod install 执行,如果检测到改动时,CocoaPods 会利用 Xcodeproj gem 组件对 Pods.xcodeproj进行更新。如果该文件不存在,则用默认配置生成。否则,会将已有的配置项加载至内存中。

安装第三方库

当 CocoaPods 往工程中添加一个第三方库时,不仅仅是添加代码这么简单,还会添加很多内容。由于每个第三方库有不同的 target,因此对于每个库,都会有几个文件需要添加,每个 target 都需要:

  • 一个包含编译选项的 .xcconfig 文件
  • 一个同时包含编译设置和 CocoaPods 默认配置的私有 .xcconfig 文件
  • 一个编译所必须的 prefix.pch 文件
  • 另一个编译必须的文件 dummy.m

一旦每个 pod 的 target 完成了上面的内容,整个 Pods target 就会被创建。这增加了相同文件的同时,还增加了另外几个文件。如果源码中包含有资源 bundle,将这个 bundle 添加至程序 target 的指令将被添加到 Pods-Resources.sh 文件中。还有一个名为 Pods-environment.h 的文件,文件中包含了一些宏,这些宏可以用来检查某个组件是否来自 pod。最后,将生成两个认可文件,一个是 plist,另一个是 markdown,这两个文件用于给最终用户查阅相关许可信息。

写入至磁盘

直到现在,许多工作都是在内存中进行的。为了让这些成果能被重复利用,我们需要将所有的结果保存到一个文件中。所以 Pods.xcodeproj 文件被写入磁盘,另外两个非常重要的文件:Podfile.lock 和 Manifest.lock 都将被写入磁盘。

Podfile.lock

这是 CocoaPods 创建的最重要的文件之一。它记录了需要被安装的 pod 的每个已安装的版本。如果你想知道已安装的 pod 是哪个版本,可以查看这个文件。推荐将 Podfile.lock 文件加入到版本控制中,这有助于整个团队的一致性。

Manifest.lock

这是每次运行 pod install 命令时创建的 Podfile.lock 文件的副本。如果你遇见过这样的错误 沙盒文件与 Podfile.lock 文件不同步 (The sandbox is not in sync with the Podfile.lock),这是因为 Manifest.lock 文件和 Podfile.lock 文件不一致所引起。由于 Pods 所在的目录并不总在版本控制之下,这样可以保证开发者运行 app 之前都能更新他们的 pods,否则 app 可能会 crash,或者在一些不太明显的地方编译失败。

xcproj

如果你已经依照我们的建议在系统上安装了 xcproj,它会对 Pods.xcodeproj 文件执行一下 touch 以将其转换成为旧的 ASCII plist 格式的文件。为什么要这么做呢?虽然在很久以前就不被其它软件支持了,但是 Xcode 仍然依赖于这种格式。如果没有 xcproj,你的 Pods.xcodeproj 文件将会以 XML 格式的 plist 文件存储,当你用 Xcode 打开它时,它会被改写,并造成大量的文件改动。

结果

运行 pod install 命令的最终结果是许多文件被添加到你的工程和系统中。这个过程通常只需要几秒钟。当然没有 Cocoapods 这些事也都可以完成。只不过所花的时间就不仅仅是几秒而已了。

补充:持续集成

CocoaPods 和持续集成在一起非常融洽。虽然持续集成很大程度上取决于你的项目配置,但 Cocoapods 依然能很容易地对项目进行编译。

Pods 文件夹的版本控制

如果 Pods 文件夹和里面的所有内容都在版本控制之中,那么你不需要做什么特别的工作,就能够持续集成。我们只需要给 .xcworkspace 选择一个正确的 scheme 即可。

不受版本控制的 Pods 文件夹

如果你的 Pods 文件夹不受版本控制,那么你需要做一些额外的步骤来保证持续集成的顺利进行。最起码,Podfile 文件要放入版本控制之中。另外强烈建议将生成的 .xcworkspace 和 Podfile.lock 文件纳入版本控制,这样不仅简单方便,也能保证所使用 Pod 的版本是正确的。

一旦配置完毕,在持续集成中运行 CocoaPods 的关键就是确保每次编译之前都执行了 pod install 命令。在大多数系统中,例如 Jenkins 或 Travis,只需要定义一个编译步骤即可 (实际上,Travis 会自动执行 pod install 命令)。对于 Xcode Bots,在书写这篇文章时我们还没能找到非常流畅的方式,不过我们正朝着解决方案努力,一旦成功,我们将会立即分享。

结束语

CocoaPods 简化了 Objective-C 的开发流程,我们的目标是让第三方库更容易被发现和添加。了解 CocoaPods 的原理能让你做出更好的应用程序。我们沿着 CocoaPods 的整个执行过程,从载入 specs 文件和源代码、创建 .xcodeproj 文件和所有组件,到将所有文件写入磁盘。所以接下来,我们运行 pod install --verbose,静静观察 CocoaPods 的魔力如何显现。


原文(英文):https://www.objc.io/issues/6-build-tools/cocoapods-under-the-hood/  

翻译:@BeyondVincent





收起阅读 »

iOS app的编译过程

iOS app的编译过程在 iOS 开发的过程中,Xcode 为我们提供了非常完善的编译能力,正常情况下,我们只需要 Command + R 就可以将应用运行到设备上,即使打包也是一个相对愉快的过程。但正如我们写代码无法避开 Bug 一样,项目在编译的时候也会...
继续阅读 »

iOS app的编译过程

在 iOS 开发的过程中,Xcode 为我们提供了非常完善的编译能力,正常情况下,我们只需要 Command + R 就可以将应用运行到设备上,即使打包也是一个相对愉快的过程。

但正如我们写代码无法避开 Bug 一样,项目在编译的时候也会出现各种各样的错误,最痛苦的莫过于处理这些错误。其中的各种报错都不是我们在日常编程中所能接触的,而我们无法快速精准的定位错误并解决的唯一原因就是我们根本不知道在编译的时候都做了些什么,都需要些什么。就跟使用一个新的类,如果不去查看其代码,永远也无法知道它到底能干什么一样。

这篇文章将从由简入繁的讲解 iOS App 在编译的时候到底干了什么。一个 iOS 项目的编译过程是比较繁琐的,针对源代码、xib、framework 等都将进行一定的编译和操作,再加上使用 Cocoapods,会让整个过程更加复杂。这篇文章将以 Swift 和 Objective-C 的不同角度来分析。

1.什么是编译

在开始之前,我们必须知道什么是编译?为什么要进行编译?

CPU 由上亿个晶体管组成,在运行的时候,单个晶体管只能根据电流的流通或关闭来确认两种状态,我们一般说 0 或 1,根据这种状态,人类创造了二进制,通过二进制编码我们可以表示所有的概念。但是,CPU 依然只能执行二进制代码。我们将一组二进制代码合并成一个指令或符号,创造了汇编语言,汇编语言以一种相对好理解的方式来编写,然后通过汇编过程生成 CPU 可以运行的二进制代码并运行在 CPU 上。

但是使用汇编语言开发仍然是一个相对痛苦的过程,于是通过上述方式,c、c++、Java 等语言就一层一层的被发明出来。Objective-c 和 Swift 就是这样一个过程,他们的基础都是 c 和 c++。

当我们使用 Objective-c 和 Swift 编写代码后,想要代码能运行在 CPU 上,我们必须进行编译,将我们写好的代码编译为机器可以理解的二进制代码。

1.1 LLVM

有了上面的简单介绍,可以发现,编译其实是一个用代码解释代码的过程。在 Objective-c 和 Swift 的编译过程中,用来解释代码的,就是 LLVM。点击可以看到 LLVM 的官方网站,在 Overview 的第一行就说明了 LLVM 到底是什么:

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies. Despite its name, LLVM has little to do with traditional virtual machines. The name “LLVM” itself is not an acronym; it is the full name of the project.

LLVM 项目是一个模块化、可重用的编译器、工具链技术的集合。尽管它的名字叫 LLVM,但它与传统虚拟机的关系并不大。“LLVM”这个名字本身不是一个缩略词; 它的全称是这个项目。

// LLVM 命名最早源自于底层虚拟机(Low Level Virtual Machine)的缩写。

简单的说,LLVM 是一个项目,其作用就是提供一个广泛的工具,可以将任何高级语言的代码编译为任何架构的 CPU 都可以运行的机器代码。它将整个编译过程分类了三个模块:前端、公用优化器、后端。(这里不要去思考任何关于 web 前端和 service 后端的概念。)

前端:对目标语言代码进行语法分析,语义分析,生成中间代码。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。我们在开发的过程中,其实 Xcode 也会使用前端工具对你的代码进行分析,并实时的检查出来某些错误。前端是针对特定语言的,如果需要一个新的语言被编译,只需要再写一个针对新语言的前端模块即可。

公用优化器:将生成的中间文件进行优化,去除冗余代码,进行结构优化。

后端:后段将优化后的中间代码再次转换,变成汇编语言,并再次进行优化,最后将各个文件代码转换为机器代码并链接。链接是指将不同代码文件编译后的不同机器代码文件合并成一个可执行文件。
虽然目前 LLVM 并没有达到其目标(可以编译任何代码),但是这样的思路是很优秀的,在日常开发中,这种思路也会为我们提供不少的帮助。

1.2 clang

clang 是 LLVM 的一个前端,它的作用是针对 C 语言家族的语言进行编译,像 c、c++、Objective-C。而 Swift 则自己实现了一个前端来进行 Swift 编译,优化器和后端依然是使用 LLVM 来完成,后面会专门对 Swift 语言的 前端编译流程进行分析。

上面简单的介绍了为什么需要编译,以及 Objectie-C 和 Swift 代码的编译思路。这是基础,如果没有这些基础,后面针对我们整个项目的编译就无法理解,如果你理解了上面的知识点,那么下面将要讲述的整个项目的编译过程就会显得很简单了。

2.ios项目编译过程介绍

Xcode 在编译 iOS 项目的时候,使用的正是 LLVM,其实我们在编写代码以及调试的时候也在使用 LLVM 提供的功能。例如代码高亮(clang)、实时代码检查(clang)、代码提示(clang)、debug 断点调试(LLDB)。这些都是 LLVM 前端提供的功能,而对于后端来说,我们接触到的就是关于 arm64、armv7、armv7s 这些 CPU 架构了,记得之前还有 32 位架构处理器的时候,设定指定的编译的目标 CPU 架构就是一个比较痛苦的过程。

下面来简单的讲讲整个 iOS 项目的编译过程,其中可能会有一些疑问,先保留着,后面会详细解释:
我们的项目是一个 target,一个编译目标,它拥有自己的文件和编译规则,在我们的项目中可以存在多个子项目,这在编译的时候就导致了使用了 Cocoapods 或者拥有多个 target 的项目会先编译依赖库。这些库都和我们的项目编译流程一致。Cocoapods 的原理解释将在文章后面一部分进行解释。

1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方
便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;

2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到;

3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;

4.链接文件:将项目中的多个可执行文件合并成一个文件;

5.拷贝资源文件:将项目中的资源文件拷贝到目标包;

6.编译 storyboard 文件:storyboard 文件也是会被编译的;

7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;

8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;

9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。

10.生成 .app 包

11.将 Swift 标准库拷贝到包中

12.对包进行签名

13.完成打包

在上述流程中:2 - 9 步骤的数量和顺序并不固定,这个过程可以在 Build Phases 中指定。Phases:阶段、步骤。这个 Tab 的意思就是编译步骤。其实不仅我们的整个编译步骤和顺序可以被设定,包括编译过程中的编译规则(Build Rules)和具体步骤的参数(Build Settings),在对应的 Tab 都可以看到。关于整个编译流程的日志和设定,可以查看这篇文章:Build 过程,跟着它的步骤来查看自己的项目将有助于你理解整个编译流程。后面也会详细讲解这些内容。

3.文件编译过程

Objective-C 的文件中,只有 .m 文件会被编译 .h 文件只是一个暴露外部接口的头文件,它的作用是为被编译的文件中的代码做简单的共享。下面拿一个单独的类文件进行分析。这些步骤中的每一步你都可以使用 clang 的命令来查看其进度,记住 clang 是一个命令行工具,它可以直接在终端中运行。这里我们使用 c 语言作为例子类进行分析,它的过程和 Objective-C 一样,后面 3.7 会讲到 Swift 文件是如何被编译的。

3.1预处理

在我们的代码中会有很多 #import 宏,预处理的第一步就是将 import 引入的文件代码放入对应文件。

然后将自定义宏替换,例如我们定义了如下宏并进行了使用:

#define Button_Height 44
#define Button_Width 100

button.frame = CGRectMake(0, 0, Button_Width, Button_Height);
那么代码将会被替换成

button.frame = CGRectMake(0, 0, 44, 100);

按照这样的思路可以发现,在自定义宏的时候要格外小心,尤其是一些携带参数和功能的宏,这些宏也只是简单的直接替换代码,不能真的代替方法或函数,中间会有很多问题。

在将代码完全拆开后,将会对代码进行符号化,对于分析代码的代码 (clang),我们写的代码就是一些字符串,为了后面给这些代码进行语法和语义分析,需要将我们的代码进行标记并符号化,例如一段 helloworld 的 c 代码:

#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}

使用 clang 命令 clang -Xclang -dump-tokens helloworld.c 转化后的代码如下(去掉了 stdio.h 中的内容):

int 'int'    [StartOfLine]  Loc=<helloworld.c:2:1>
identifier 'main' [LeadingSpace] Loc=<helloworld.c:2:5>
l_paren '(' Loc=<helloworld.c:2:9>
int 'int' Loc=<helloworld.c:2:10>
identifier 'argc' [LeadingSpace] Loc=<helloworld.c:2:14>
comma ',' Loc=<helloworld.c:2:18>
char 'char' [LeadingSpace] Loc=<helloworld.c:2:20>
star '*' [LeadingSpace] Loc=<helloworld.c:2:25>
identifier 'argv' Loc=<helloworld.c:2:26>
l_square '[' Loc=<helloworld.c:2:30>
r_square ']' Loc=<helloworld.c:2:31>
r_paren ')' Loc=<helloworld.c:2:32>
l_brace '{' [StartOfLine] Loc=<helloworld.c:3:1>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<helloworld.c:4:2>
l_paren '(' Loc=<helloworld.c:4:8>
string_literal '"Hello World!\n"' Loc=<helloworld.c:4:9>
r_paren ')' Loc=<helloworld.c:4:25>
semi ';' Loc=<helloworld.c:4:26>
return 'return' [StartOfLine] [LeadingSpace] Loc=<helloworld.c:5:2>
numeric_constant '0' [LeadingSpace] Loc=<helloworld.c:5:9>
semi ';' Loc=<helloworld.c:5:10>
r_brace '}' [StartOfLine] Loc=<helloworld.c:6:1>
eof '' Loc=<helloworld.c:6:2>

这里,每一个符号都会标记出来其位置,这个位置是宏展开之前的位置,这样后面如果发现报错,就可以正确的提示错误位置了。针对 Objective-C 代码,我们只需要转化对应的 .m 文件就可以查看。

3.2语意和语法分析

3.2.1AST

对代码进行标记之后,其实就可以对代码进行分析,但是这样分析起来的过程会比较复杂。于是 clang 又进行了一步转换:将之前的标记流转换为一颗抽象语法树(abstract syntax tree – AST)。

使用 clang 命令 clang -Xclang -ast-dump -fsyntax-only helloworld.c,转化后的树如下(去掉了 stdio.h 中的内容):


`-FunctionDecl 0x7f8eaf834bb0 <helloworld.c:2:1, line:6:1> line:2:5 main 'int (int, char **)'
|-ParmVarDecl 0x7f8eaf8349b8 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f8eaf834aa0 <col:20, col:31> col:26 argv 'char **':'char **'
`-CompoundStmt 0x7f8eaf834dd8 <line:3:1, line:6:1>
|-CallExpr 0x7f8eaf834d40 <line:4:2, col:25> 'int'
| |-ImplicitCastExpr 0x7f8eaf834d28 <col:2> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f8eaf834c68 <col:2> 'int (const char *, ...)' Function 0x7f8eae836d78 'printf' 'int (const char *, ...)'
| `-ImplicitCastExpr 0x7f8eaf834d88 <col:9> 'const char *' <BitCast>
| `-ImplicitCastExpr 0x7f8eaf834d70 <col:9> 'char *' <ArrayToPointerDecay>
| `-StringLiteral 0x7f8eaf834cc8 <col:9> 'char [14]' lvalue "Hello World!\n"
`-ReturnStmt 0x7f8eaf834dc0 <line:5:2, col:9>
`-IntegerLiteral 0x7f8eaf834da0 <col:9> 'int' 0

这是一个 main 方法的抽象语法树,可以看到树顶是 FunctionDecl:方法声明(Function Declaration

这里因为截取了部分代码,其实并不是整个树的树顶。真正的树顶描述应该是:TranslationUnitDecl。
然后是两个 ParmVarDecl:参数声明。

接着下一层是 CompoundStmt:说明下面有一组复合的声明语句,指的是我们的 main 方法里面所使用到的所有代码。

再到里面就是每一行代码的使用,方法的调用,传递的参数,以及返回。在实际应用中还会有变量的声明、操作符的使用等。

3.2.2静态分析

有了这样的语法树,对代码的分析就会简单许多。对这棵树进行遍历分析,包括类型检查、实现检查(某个类是否存在某个方法)、变量使用,还会有一些复杂的检查,例如在 Objective-C 中,给某一个对象发送消息(调用某个方法),检查这个对象的类是否声明这个方法(但并不会去检查这个方法是否实现,这个错误是在运行时进行检查的),如果有什么错误就会进行提示。因此可见,Xcode 对 clang 做了非常深度的集成,在编写代码的过程中它就会使用 clang 来对你的代码进行分析,并及时的对你的代码错误进行提示。

3.3生成LLVM代码

当确认代码没有问题后(静态分析可分析出来的问题),前端就将进入最后一步:生成 LLVM 代码,并将代码递交给优化器。

使用命令 clang -S -emit-llvm helloworld.c -o helloworld.ll 将生成 LLVM IR。

其设计的最重要的部分是 LLVM 中间表示(IR),它是一种在编译器中表示代码的形式。LLVM IR 旨在承载在编译器的优化器中间的分析和转换。它的设计考虑了许多特定的目标,包括支持轻量级运行时优化,跨功能/进程间优化,整个程序分析和积极的重组转换等等。但它最重要的方面是它本身被定义为具有明确定义的语义的第一类语言。

例如我们上面的代码将会被生成为:


; ModuleID = 'helloworld.c'
source_filename = "helloworld.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"

@.str = private unnamed_addr constant [14 x i8] c"Hello World!\0A\00", align 1

; Function Attrs: nounwind ssp uwtable
define i32 @main(i32, i8**) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0))
ret i32 0
}

declare i32 @printf(i8*, ...) #1

attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}


其实还是能实现我们功能的代码,在这一步,所有 LLVM 前端支持的语言都将会被转换成这样的代码,主要是为了后面的工作可以共用。下面就是 LVVM 中的优化器的工作。

在这里简单介绍一些 LLVM IR 的指令:

%:局部变量
@:全局变量
alloca:分配内存堆栈
i32:32 位的整数
i32**:一个指向 32int 值的指针的指针
align 4:向 4 个字节对齐,即便数据没有占用 4 个字节,也要为其分配四个字节
call:调用

3.4优化

上面的代码是没有进行优化过的,在语言转换的过程中,有些代码是可以被优化以提升执行效率的。使用命令 clang -O3 -S -emit-llvm helloworld.c -o helloworld.ll,其实和上面的命令的区别只有 -O3 而已,注意,这里是大写字母 O 而不是数字 0。优化后的代码如下:


; ModuleID = 'helloworld.c'
source_filename = "helloworld.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.12.0"

@str = private unnamed_addr constant [13 x i8] c"Hello World!\00"

; Function Attrs: nounwind ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i32 @puts(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @str, i64 0, i64 0))
ret i32 0
}

; Function Attrs: nounwind
declare i32 @puts(i8* nocapture readonly) #1

attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"PIC Level", i32 2}
!1 = !{!"Apple LLVM version 8.1.0 (clang-802.0.42)"}

可以看到,即使是最简单的 helloworld 代码,也会被优化。这一步骤的优化是非常重要的,很多直接转换来的代码是不合适且消耗内存的,因为是直接转换,所以必然会有这样的问题,而优化放在这一步的好处在于前端不需要考虑任何优化过程,减少了前端的开发工作。

3.5 生成目标文件

下面就是后端的工作了,将优化过的代码根据不同架构的 CPU 转化生成汇编代码,再生成对应的可执行文件,这样对应的 CPU 就可以执行了。

3.6可执行文件

在最后,LLVM 将会把这些汇编代码输出成二进制的可执行文件,使用命令 clang helloworld.c -o helloworld.out 即可查看,-o helloworld.out 如果不指定,将会被默认指定为 a.out。

可执行文件会有多个部分,对应了汇编指令中的 .section,它的名字也叫做 section,每个 section 都会被转换进某个 segment 里。这种方式用来区分不同功能的代码。将相同属性的 section 集合在一起,就是一个 segment。

使用 otool 工具可以查看生成的可执行文件的 section 和 segment:

Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x34 (addr 0x100000f50 offset 3920)
Section __stubs: 0x6 (addr 0x100000f84 offset 3972)
Section __stub_helper: 0x1a (addr 0x100000f8c offset 3980)
Section __cstring: 0xe (addr 0x100000fa6 offset 4006)
Section __unwind_info: 0x48 (addr 0x100000fb4 offset 4020)
total 0xaa
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000

上面的代码中,每个 segment 的意义也不一样:

__ PAGEZERO segment 它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读。

__ TEXT segment
包含了被执行的代码。它被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。

__ DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。

__ LINKEDIT segment 指出了 link edit 表(包含符号和字符串的动态链接器表)的地址,里面包含了加载程序的元数据,例如函数的名称和地址。

4.静态库和动态库

说起来编译,就不得不说起动态库和静态库。这两个东西可是和编译过程息息相关的,这里有几篇文章的比较透彻,可以查看,想要了解整个编译过程,库是逃不开的:

iOS 静态库,动态库与 Framework:非常完美的讲解了静态库和动态库的概念,还有一些延伸阅读也非常好。https://skyline75489.github.io/post/2015-8-14_ios_static_dynamic_framework_learning.html

5.了解了这么多编译原理,除了写一个自动化编译脚本以外,还可以看懂很多之前完全看不明白的编译错误。在 Xcode 中,也可以对编译过程进行完整的设置,很多时候编译错误的解决就是在这里进行的。Xcode编译设置

5.1Build settings

这里是编译设置,针对编译流程中的各个过程进行参数和工具的配置:

1.Architectures:编译目标 CPU 架构,这里比较常见的是 Build Active Architectures Only(只编译为当前架构,是指你在 scheme 中选定的设备的 CPU 架构),debug 设置为 YES,Release 设置为 NO。


2.Assets:Assets.xcassets 资源组的配置。

3.Build Locations:查看 Build 日志可以看到在编译过程中的目标文件夹。

4.Build Options:这里是一些编译的选项设定,包含:

a.是否总是嵌入 Swift 标准库,这个在静态库和动态库的第一篇文章中有讲,iOS 系统目前是不包含 Swift 标准库的,都是被打包在项目中。

b.c/c++/objective-c 编译器:Apple LLVM 9.0

c.是否打开 Bitcode



5.Deployment:iOS 部署设置。说白了就是安装到手机的设置。

6.Headers:头文件?具体作用不详,知道的可以说一下。

7.Kernel Module:内核模块,作用不详。

8.Linking:链接设置,链接路径、链接标记、Mach-O 文件类型。

9.Packaging:打包设置,info.plist 的路径设置、Bundle ID 、App 显示名称的设置。

10.Search Paths:库的搜索路径、头文件的搜索路径。

11.Signing:签名设置,开发、生产的签名设置,这些都和你在开发者网站配置的证书相关。

12.Testing:测试设置,作用不详。

13.Text-Based API:基于文本的 API,字面翻译,作用不详。

14.Versioning:版本管理。

15.Apple LLVM 9.0 系列:LLVM 的配置,包含路径、编译器每一步的设置、语言设置。在这里 Apple LLVM 9.0 - Warnings 可以选择在编译的时候将哪些情况认定为错误(Error)和警告(Warning),可以开启困难模式,任何一个小的警告都会被认定为错误。

16.Asset Catalog Compiler - Options:Asset 文件的编译设置。

17.Interface Builder Storyboard Compiler - Options:Storyboard 的编译设置。

18.以及一些静态分析和 Swift 编译器的设定。

5.2Build Phases

编译阶段,编译的时候将根据顺序来进行编译。这里固定的有:

1.Compile Sources:编译源文件。

2.Link Binary With Libraries:相关的链接库。

3.Copy Bundle Resources:要拷贝的资源文件,有时候如果一个资源文件在开发过程中发现找不到,可以在这里找一下,看看是不是加进来了。

如果使用了 Cocoapods,那么将会被添加:

1.[CP] Check Pods Manifest.lock:检查 Podfile.lock 和 Manifest.lock 
文件的一致性,这个会再后面的 Cocoapods 原理中详细解释。

2.[CP] Embed Pods Frameworks:将所有 cocoapods 打的 framework 拷贝到包中。

3.[CP] Copy Pods Resources:将所有 cocoapods 的资源文件拷贝到包中。

5.3Build Rules

编译规则,这里设定了不同文件的处理方式,例如:

Copy Plist File:在编译打包的时候,将 info.plist 文件拷贝。

Compress PNG File:在编译打包的时候,将 PNG 文件压缩。

Swift Compiler:Swift 文件的编译方式,使用 Swift 编译器。

6. Cocoapods 原理

使用了 Cocoapods 后,我们的编译流程会多出来一些,虽然每个 target 的编译流程都是一致的,但是 Cocoapods 是如何将这些库导入我们的项目、原项目和其他库之间的依赖又是如何实现的仍然是一个需要了解的知识点




作者:帽子和五朵玫瑰
链接:https://www.jianshu.com/p/0ad0660ac63a

收起阅读 »

iOS Crash分析中的Signal

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

下面是一些信号说明

  • SIGHUP
    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
  • SIGINT
    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
  • SIGQUIT
    SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
  • SIGILL
    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
  • SIGTRAP
    由断点指令或其它trap指令产生. 由debugger使用。
  • SIGABRT
    调用abort函数生成的信号。
  • SIGBUS
    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
  • SIGFPE
    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
  • SIGKILL
    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
  • SIGUSR1
    留给用户使用
  • SIGSEGV
    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
  • SIGUSR2
    留给用户使用
  • SIGPIPE
    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
  • SIGALRM
    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
  • SIGTERM
    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL
  • SIGCHLD
    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
  • SIGCONT
    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
  • SIGSTOP
    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
  • SIGTSTP
    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
  • SIGTTIN
    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
  • SIGTTOU
    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
  • SIGURG
    有”紧急”数据或out-of-band数据到达socket时产生.
  • SIGXCPU
    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
  • SIGXFSZ
    当进程企图扩大文件以至于超过文件大小资源限制。
  • SIGVTALRM
    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
  • SIGPROF
    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
  • SIGWINCH
    窗口大小改变时发出.
  • SIGIO
    文件描述符准备就绪, 可以开始进行输入/输出操作.
  • SIGPWR
    Power failure
  • SIGSYS
    非法的系统调用。

关键点注意

  • 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
  • 不能恢复至默认动作的信号有:SIGILL,SIGTRAP
  • 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
  • 默认会导致进程退出的信号有:
    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
  • 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
  • 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
  • 此外,SIGIOSVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。


作者:Cooci
链接:https://www.jianshu.com/p/3a9dc6bd5e58
收起阅读 »

Swift 反射,揭开面纱

与iOS Runtime不一样,Swift的反射用了另一套API,实现机制也完全不一样1. iOS Runtime其实基于Objc的Runtime是iOS开发的黑魔法,比如神奇的Method Swizzle可以交换任何iOS的系统方法, 再比如消息转发机制,又...
继续阅读 »

与iOS Runtime不一样,Swift的反射用了另一套API,实现机制也完全不一样


1. iOS Runtime

  • 其实基于Objc的Runtime是iOS开发的黑魔法,比如神奇的Method Swizzle可以交换任何iOS的系统方法, 再比如消息转发机制,又如class_copyIvarList等方法,可以动态获取一个类裡面所有的方法和属性, 以及动态给一个类新增属性和方法.
  • Objc的Runtime是如此的强大,再加上KVC和KVO这两个利器,可以实现很多你根本就想不到的功能,给iOS开发带来极大的便捷。

2. Apple推出全新的Swift语言后,单纯的Swift型别不再兼容Objc的Runtime,

Swift作为一门静态语言,所有资料的型别都是在编译时就确定好了的,但是Apple为了让Swift相容Objc,让Swift也使用了Runtime。这显然会拖累Swift的执行效率,和Apple所宣称Swift具有超越Objective-C的效能的观点完全不符。而Swift在将来是会慢慢替代 Objective-C的成为iOS或者OSX开发的主流语言,所以为了效能,我们应该尽量使用原生的Swift,避免让Runtime进行Swift型别->Objc型别的隐式转换。
Swift目前只有有限的反射功能,完全不能和Objc的Runtime相比。

首先作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

什么是反射

反射是一种计算机处理方式。是程式可以访问、检测和修改它本身状态或行为的一种能力。
上面的话来自百度百科。使用反射有什么用,看一些iOS Runtime的文章应该会很明白。下面再列举一下

  • 动态地建立物件和属性,
  • 动态地获取一个类裡面所有的属性,方法。
  • 获取它的父类,或者是实现了什么样的介面(协议)
  • 获取这些类和属性的访问限制(Public 或者 Private)
  • 动态地获取执行中物件的属性值,同时也能给它赋值(KVC)
  • 动态呼叫例项方法或者类方法
  • 动态的给类新增方法或者属性,还可以交换方法(只限于Objective-C)

上面的一系列功能的细节和计算机语言的不同而不同。对于Objective-C来说,位于中的一系列方法就是完成这些功能的,严格来说Runtime并不是反射。而Swift真正拥有了反射功能,但是功能非常弱,目前只能访问和检测它本身,还不能修改。

Swift的反射

Swift的反射机制是基于一个叫Mirror的Stuct来实现的。具体的操作方式为:首先建立一个你想要反射的类的例项,再传给Mirror的构造器来例项化一个Mirror物件,最后使用这个Mirror来获取你想要的东西。

Mirror结构体常用属性:
subjectType:对象类型
children:反射对象的属性集合
displayStyle:反射对象展示类型

下面来简单介绍下Mirror的使用:

  1. 获取对象类型
  2. 获取一个类的属性名称和属性的值
        let p = Person()
p.name = "刘伟湘"
p.age = 22

let mirror:Mirror = Mirror(reflecting: p)

/*
* 1\. 获取对象类型
*/

print("获取对象类型:\(mirror.subjectType)")
//打印结果: 获取对象类型:Person

/*
* 2\. 获取对象的所有属性名称和属性值
*/

for property in mirror.children {
let propertyNameStr = property.label! // 属性名使用!,因为label是optional类型
let propertyValue = property.value // 属性的值
print("\(propertyNameStr)的值为:\(propertyValue)")
//打印结果: name的值为:Optional("刘伟湘") age的值为:22
}


swfit反射的应用场景现在还比较狭窄,因为功能还不够完善,比较常见的反射应用场景就是自定义类模型转字典

class NewViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

self.navigationItem.title = "new"
self.view.backgroundColor = .white

// 创建一个User实例对象模型
let user1 = User()
user1.name = "刘伟湘"
user1.age = 100
user1.emails = ["506299396.qq.com","111111111111.qq.com"]
let tel1 = Telephone(title: "手机", number: "18711112222")
let tel2 = Telephone(title: "公司座机", number: "2800034")
user1.tels = [tel1, tel2]

// 模型转字典
if let model = user1.toJSONModel() {
print(model)
}
/*
*打印结果
["age": 100, "tels": ["item0": ["number": "18711112222", "title": "手机"], "item1": ["number": "2800034", "title": "公司座机"]], "emails": ["item0": "506299396.qq.com", "item1": "111111111111.qq.com"], "name": "刘伟湘"]
*
*/
}
}

class User {
var name:String = ""
var nickname:String?
var age:Int?
var emails:[String]?
var tels:[Telephone]?
}

// 电话结构体
struct Telephone {
var title:String // 电话标题
var number:String // 电话号码
}

// 自定义一个JSON协议
protocol Sam_JSON {
func toJSONModel() -> Any?
}

// 扩展协议方法,实现一个通用的toJSONModel方法(反射实现)
extension Sam_JSON {
// 将模型数据转成可用的字典数据,Any表示任何类型,除了方法类型
func toJSONModel() -> Any? {
// 根据实例创建反射结构体Mirror
let mirror = Mirror(reflecting: self)

if mirror.children.count > 0 {
// 创建一个空字典,用于后面添加键值对
var result: [String:Any] = [:]

for (idx, children) in mirror.children.enumerated() {
let propertyNameString = children.label ?? "item\(idx)"
let value = children.value
// 判断value的类型是否遵循JSON协议,进行深度递归调用
if let jsonValue = value as? Sam_JSON {
result[propertyNameString] = jsonValue.toJSONModel()
}
}
return result
}
return self
}
}

// 扩展可选类型,使其遵循JSON协议,可选类型值为nil时,不转化进字典中
extension Optional: Sam_JSON {
func toJSONModel() -> Any? {
if let x = self {
if let value = x as? Sam_JSON {
return value.toJSONModel()
}
}
return nil
}
}

// 扩展两个自定义类型,使其遵循JSON协议
extension User: Sam_JSON { }
extension Telephone: Sam_JSON { }

// 扩展Swift的基本数据类型,使其遵循JSON协议
extension String: Sam_JSON { }
extension Int: Sam_JSON { }
extension Bool: Sam_JSON { }
extension Dictionary: Sam_JSON { }
extension Array: Sam_JSON { }


作者:编程大鑫
链接:https://www.jianshu.com/p/9fdb13d62498


收起阅读 »

新时代iOS开发学习路线,预测未来不被淘汰

前言这里是大鑫,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,利用网络记录下自己的学习历程。本文阅读建议 1.一定要辩证的看待本文. 2.本文主要是本人对iOS开发经验中总结的知识点 3.本文所有观点仅代...
继续阅读 »

前言

这里是大鑫,是一名正在努力学习的iOS开发工程师,目前致力于全栈方向的学习,希望可以和大家一起交流技术,共同进步,利用网络记录下自己的学习历程。

本文阅读建议
1.一定要辩证的看待本文.
2.本文主要是本人对iOS开发经验中总结的知识点
3.本文所有观点仅代表本人.
4.本文只阐述学习路线和学习当中的重点问题.需要读者自己使用百度进行拓展学习.
5.本文所表达观点并不是最终观点,还会更新,因为本人还在学习过程中,有什么遗漏或错误还望各位指出.
6.觉得哪里不妥请在评论留下建议~
7.觉得还行的话就点个小心心鼓励下我吧~
目录
1.对本职业看法
2.学习方法
3.职业规划
4.产品公司&外包公司
5.做一个负责任的开发者
6.iOS开发学习路线
7.iOS基础知识点
8.iOS中级知识点
9.iOS高级知识点
10.官方Kit


我尝试加入各种iOS开发交流群,群里的气氛大致就是:学什么iOS,iOS完了,OC完了,群里大致三种人:谁有企业开发证书,马甲包了解一下,至今,大部分iOS开发群还都是仅供吹水用,偶尔能碰见几个好心人解决一下问题


个人观点

个人观点:iOS开发这个职业,不是别人说完就完的,那些说完了的人都是因为技术菜,没有权威性,不想想自己为什么菜,为什么没有和唐巧王巍在一个高度,因为菜.

还没有到达一个高度就轻易否定一个职业,注定被这个职业淘汰.

所以,无视掉这种人这么荒谬的观点,那些真正有技术,懂得学习的iOS高级开发工程师,现在正在各大企业(腾讯百度阿里等),一句话,不要有比上不足比下有余的态度.努力学习.

真正会学习的人,不会说iOS完了,而是想着如何提升自己,你想想,真正牛逼的人,真的只会iOS开发这一种吗?


学习方法

面对有难度的功能,不要忙着拒绝,而是挑战一下,学习更多知识.

尽量独立解决问题,而不是在遇到问题的第一想法是找人.

多学习别人开源的第三方库,能够开源的库一定有值得学习的地方,多去看别的大神的博客.

作为一个程序员,如果你停止了学习,你也必将会被这个团队淘汰.

要把学习的技能当做兴趣,而不是为了挣钱去学习,是为了学习技能而学习.

有给自己定制一个详细的职业规划和人生规划,大到5~10年,小到近3年(并且细化到月)的计划.

不要盲目的面试,要针对即将面试的工作,准备面试.

首先针对一个自己没有接触到的知识,先使用 百度\谷歌等网站搜索资料.然后进行学习

这是个好东西,我劝你多用用https://developer.apple.com/search/

尝试写一个demo,对新技术进行熟悉.

如果市面上有成熟的Demo或者第三方库,下载下来进行学习.

在熟悉的过程中,遇到了任何问题,再进行百度/谷歌,学习不同人的不同看法和解决方法.


职业规划

个人观点

首先是针对iOS这个行业,找不到工作要从自身找原因,为什么自己没有大公司的工作经历,为什么大公司会把自己毙掉,因为实力不够,因为你菜,你不够强.要从自身找原因,是,培训机构一阵子培训了一堆iOS开发工程师,但你不能从特么一堆菜鸟中杀出去,你就是菜鸟,不要怨天尤人了,好好努力学习.

不要只做到鹤立鸡群,而想着怎么离开这群鸡,重归鹤群.

针对程序员行业,这是一个需要努力奋斗的行业,也许他并不需要你有多高的文凭,好的文凭可以去大公司工作,没有好的文凭,但拥有丰富的工作经验,和开源库,也会是你本人实力的体现.所以,努力学习,路是自己走出来的,原地踏步谁也救不了你.

职业规划一般分为两种,横向和纵向,程序员行业横向走项目经理提成获得分红,纵向发展成为技术经理,必要时可以自行创业


产品公司&外包公司

外包公司与产品公司有什么区别呢,本质上的区别就是,模式不同。产品公司针对的是自己的产品,如何升级迭代做到更好,拥有更多的用户流量,如何设计功能进行盈利。而外包公司针对的是客户,项目经理往往会和销售谈妥一件件生意,隔一段时间开一个产品会议,使得开发部门,人手几个项目一起开发。这两种模式也是各有利弊。

先说外包公司的模式吧,一个好的外包公司,可能福利会好很多,阶级斗争不是很明显,大家就像打工的一样,拿着工资和项目提成,项目比较紧,成熟的外包公司拥有统一化的管理,和优秀的代码规范;

但如果是比较差的外包公司,那就不一样了,整体项目以完成为目的,不需要维护,往往只需要做出来一个雏形,不会到处崩溃,交货之后,此app将再也没有关系,如果需要维护,就再交钱。不论好与坏的外包公司,他的盈利模式就像是流水线,只需要出货量,不要求质量。这对于刚刚步入程序员行列的人会很不利,会养成不用维护,不用注重用户体验,不用做流畅度,耗电量,并发量的测试的坏习惯,得过且过。

总之不用考虑太多。这也是市面上大公司有些会看你之前的工作经历的原因,如果是外包,对不起,我们不要。

产品公司的模式,就是升职加薪,干得越久福利越好,万一你比较幸运,有幸成为未来几年要火的产品的开发者,那就是offer不断啊。产品公司往往分为有成品项目和创业两种。

成品项目人员变动一般较少,阶级斗争比较严重,为了职位更上一层楼,勾心斗角。不过在开发团队还是比较罕见的,大家大部分都是想跳槽的。

创业公司往往需要人才,全面性的人才,就单单说iOS,一个创业公司可能会要求你会 直播,支付,蓝牙,聊天,这也都是老功能了,现在都是什么 AR啊 人脸识别啊。你不学习新知识,注定被淘汰。外包公司也有一点好处就是,涉及的应用多,那功能也就自然而然比较多(如果全部接的那种简单的应用当我没说)。


做一个负责任的开发者

那么现在说正题,如何成为负责任的开发者?

首先要负责,对自己的项目负责。如果是自己新开的项目,要保证随时都能清晰的想到项目当中每个地方是怎么实现的,测试或者用户反馈了问题以后,能立马想到可能的错误原因。

如果是接手的项目,就要尽快去了解主要的界面和功能是如何实现的。你只有先做好自己分内的事,才有机会去顾暇别人的事。


1.保持一个良好的代码规范以及文件架构。
2.每天要给自己做一个TodoList 和一个BugList,时刻保持自己是在有效率的工作,严重的需要时间修复的bug汇报上去,小bug自己记下来偷偷修复。
3.有空时将排行榜上的应用下载排名靠前的应用,去欣赏并分析主流app的界面,功能实现,在拿到设计图时,去考虑界面的合理性,功能怎么实现最符合用户的操作习惯。
4.要有一定的协调能力,交流能力,稍微了解一点后台知识以及前端知识。
5.信念,一个不做初级iOS开发的信念。多去了解,不会被别人当小白,学多少都是自己的,至于在你去学习的时候,有人会说风言风语,这就是区别,他们活该初级,自己不会的东西,也看不惯别人去学习。所以,一定要有一个规划,按照自己正确的规划去学习,去成长,别原地踏步。


关于后台你需要懂什么呢,如何设计接口文档,接口怎么设计合理,后台拿到你请求的数据是怎么存储的,你需要的数据后台又是怎么查询给你的,请求方式什么时候用get什么时候适合post,JSON格式的数据以及XML数据又有什么好处。

关于前端你需要了解什么呢,这里大致提一下H5和app交互,比如H5怎么调你的方法,你怎么调H5的方法,数据如何传递,图片如何交给H5显示,这些都需要去了解。

有些人会觉得,我上面说的这都是废话,或者说你知道有什么用吗,又没你提意见的资格。iOS的群普遍是什么风气,就是你提出来一个建议或者意见,如果路人甲会,他就趾高气昂怼你一顿,如果他不会,他就会说,会这个又没用,懂这么多又没用什么的bulabulabula。这就是第五点。

如果你想变强,那就做点什么.


iOS开发学习路线

iOS定位

  • iOS定位

    • 简介:这里的定位,仅仅代表我个人意见,仅符合本笔记如何学习从哪里开始学习,怎么去学习来说.
    • 尚未入门
      • 如何判断自己是否入门
        • 是否了解Mac
        • 是否了解Xcode
        • 是否了解Objective-C
        • 是否会使用UI控件.
        • 如果上面的都不了解,那说明你还没有入门,请从iOS学习路线开始学习.
    • 初级iOS开发
      • 说明:作为一名初级的iOS开发,你需要具备以下技能
      • 必备技能(全部都会的情况下查看下一项)
        • Xcode的使用
        • 第三方库的灵活使用
          • AFN
          • MJRefresh
        • 各种网站的使用
      • 如何判断是否可以升阶
        • 是否了解AFNetworking 的实现原理
        • 是否了解SDAutolayout/Masonry 一种布局库的原理
        • 是否能够处理基本的iOS崩溃原因/无法编译原因/无法上架原因?
        • 是否拥有了一定的工作效率,稳定的工作效率.(而不是说,上面派了一个活下来,忙都忙不完,天天加班,还一堆bug)
        • 是否能够处理第三方库引起的崩溃.
        • 是否可以很好的融入工作环境,完成每一阶段的工作指标,而不会让自己疲惫不堪.
      • 结论
        • iOS中级开发说白了,就是你学会了基本的UI界面搭建,上架,沉淀一段时间,你觉得自己还适合这门行业,还适合,还能接受 这个所谓的iOS开发工程师的行业.你就可以说是一名中级iOS开发.
        • 这个沉淀时间 大约在1年的实际工作中,就可以完成.
        • 如果你觉得这门行业不适合你,请仔细结合自身情况,是否转另一门计算机语言,还是彻底转行.
    • 中级iOS开发
      • 说明:作为一名中级的iOS开发,你需要具备以下技能
      • 必备技能(全部都会的情况下查看下一项)
        • 应用的内存处理
        • 应用的推送处理
        • 应用的模块化/单元测试
        • 应用的第三方集成/集中化管理/稳定迭代
        • 阅读强大的第三方源码/拥有快速上手新的第三方库的能力.
        • 能够接受各种新功能的开发(这里是指,即使你没有做过,但是你仍然可以凭借着学习,解决任何业务需求:例如:蓝牙.AR.摄像头.硬件交互.等)
        • 清楚明白数据的传递方式,应用与后台如何交换数据,交换数据的过程,结果,格式.
        • 多线程的灵活使用.
        • 各种并发事件的处理/以及界面的合理性/流畅度
        • 设计模式的灵活使用.
      • 如何判断是否可以升阶
      • 结论
    • 高级iOS开发
      • 说明:作为一名高级的iOS开发,你需要具备以下技能(我不是高级开发,所以这里只能给你们提供建议.)
      • 必备技能
        • 应用的组件化/架构分层
        • 数据结构,操作系统,计算机网络都有自己的了解和认知
        • Shell脚本/python/Ruby/JS 至少会一种.

详细学习路线

  • 学习路线
    • 简介
      这里只简单阐述一些概念性的东西,以及学习路线规划,真正的知识请从iOS基础知识点往下开始看.
    • Objective-C
      • 介绍
      • 概念
      • 编译原理
    • 程序启动原理
      • App组成
        • Info.plist
        • .pch
      • 打开程序
      • 执行main函数
      • 执行UIApplicationMain函数
      • 初始化UIApplication(创建设置代理对象,开启事件循环)
      • 监听系统事件
      • 结束程序.
    • 语法.(此处定义可能略失严谨,口头教学为主)
      • 基础语法
      • 对象.
      • 属性
      • 数据类型
      • 方法
      • 继承
      • Frame/CGRect/CGPoint和CGSize
      • 内存(针对MRC下情况进行介绍)
      • ARC/MRC
      • 弱引用/强引用
      • Assign,retain,copy,strong
      • import 和@class的区别
    • Xcode使用
      • 首先是针对Xcode菜单栏,希望自己可以去翻译一下每个菜单里每项功能的英文都是什么意思,有助于自己熟悉并加深印象的使用Xcode.
      • 熟悉Xcode的各个功能.
    • UIKit控件.
    • 界面分析(下载App进行学习).
      • 在这里推荐有兴趣的开发人员,下载并分析,AppStore中的每项分类的top50的应用,多学习大公司以及流行应用是如何开发应用的,其中流行的,新颖的开发界面的方式可以总结下来,猜想在大应用中,别的程序员是如何开发的.
      • 界面适配
    • 代码架构.
    • 各种工具、第三方的使用.
      • 其实每个项目的建立都大致分为:项目框架搭建,原生界面搭建,嵌入第三方库.有很多功能都会用到第三方库,大多数第三方库都是本着快速开发,完整功能实现的目的存在的.需要开发人员能够根据业务逻辑不同,选择最好最优质的第三方库进行使用.
    • 代码封装
      • 当使用较多第三方库后,要求开发人员学习其开发特点,以及其封装手法,运用在自己的项目上,封装自己的代码.灵活运用.
    • 完整项目.
    • 开发技巧
    • 个人心得

iOS基础知识点

  • iOS基础知识点
    • 如何学习iOS
      • 刚刚入门(如何学习)
        • 打好基础,学习OC中各种常用语法.
        • 学习如何上架,上架会因为什么被拒,了解App上架规则.
        • 多学习官方说明文档.
      • 刚刚入职1年(如何稳定)
        • 多看开源或者注明的第三方库.
        • 收藏并阅读各种大神的博客或者论坛.
        • 开始考虑项目中的细节优化,内存处理和耗电情况
      • 入职3年(如何进阶)
        • 开始涉猎不止于iOS领域中的知识,会去了解相关职位的基础知识,例如前端和后台或者服务器运维,或者项目相关知识,具体往自己的职业规划靠拢
    • 框架的学习
      • 苹果自带框架
      • 第三方框架
        • AFNetworking
        • SDAutoLayout
        • YYKit
        • SDWebImage
        • MJRefresh
        • MJExtension
        • Bugly
        • Qiniu
        • Masonry
        • TZImagePickerController
        • Hyphenate_CN
    • 基础UI控件
      • UILabel 标题栏
      • UIButton 按钮
      • UIImageView 图片视图
      • UITextField 文本输入框
      • UITextView 文本展示视图
      • UIProgressView 进度条
      • UISlider 滑动开关
      • UIGesture 手势
      • UIActivityIndicator 菊花控件
      • UIAlertView(iOS8废除) 警告框
      • UIActionSheet(iOS8废除) 操作表单
      • UIAlertController(iOS8出现) 警告视图控制器
      • UIScrollView 滚动视图
      • UIPageControl 页面控制器
      • UISearchBar 搜索框
      • UITableView 表视图
      • UICollectionView集合视图
      • UIWebView网页浏览器
      • UISwitch开关
      • UISegmentControl选择按钮
      • UIPickerView选择器
      • UIDatePicker日期选择器
      • UIToolbar工具栏
      • UINavigationBar通知栏
      • UINavigationController通知视图控制器
      • UITabbarController选择视图控制器
      • UIImagePickerController相册
      • UIImage图片
    • Xcode的使用
      • 基础操作 状态栏
      • 偏好设置
      • Xcode Source Control 源代码管理器
      • Xcode workSpace工作组
      • Xcode Scheme 计划
      • Xcode AutoLayout 约束
      • Xcode CoreData数据库
      • LLDB 断点调试
      • StoryBoard
      • 界面预览
      • 界面适配
      • 内存监测
      • 全局断点
      • 全局搜索替换
    • 数据存储
      • plist
      • NSKeyedArchiver
      • SQLite
      • FMDB
      • CoreData
      • NSUserDefault
      • 沙盒存储
      • NSDictionary归档
    • App生命周期
      • 应用生命周期
      • 控制器生命周期
        • alloc
        • init
        • 创建View
        • ViewDidLoad
        • ViewWillAppear
        • ViewDidAppear
        • ViewWillDisappear
          • 视图将要消失 (做一些视图将要消失时的UI的处理)
        • ViewDidDisappear
          • 视图已经消失 (做一些视图消失之后数据的处理)
          • viewDidDisappear销毁定时器
        • dealloc
        • didReceiveMemoryWarning
    • 开发者账号&上架流程
      • 个人
      • 公司
      • 企业
    • 常用知识
      • 通信
      • NS系列
      • 宏定义
      • 视图层次
      • 切换视图
      • 深浅拷贝
      • 对象序列化
      • 写入文件
      • 获取沙盒路径
      • 翻转视图
      • 延伸视图
      • 九大基本数据类型
      • 九宫格
      • 坐标比较
      • UIColor 、CIColor和CGColor 之间的关系
      • 画图
      • 静态变量
      • tag值
      • 延时执行方法
      • 界面旋转+状态栏隐藏
      • plist文件
      • KVC/KVO
      • 谓词NSPredicate
      • 帧动画
      • AutoLayout
      • isKindOfClass 与 isMemberOfClass
      • Return/Break/Continue
      • Core Animation
      • CALayer
      • Quartz2D
      • 真机调试
      • 静态库
      • 内存管理
      • iPad与iPhone的区别
      • 响应链
      • 异常捕捉
      • 国际化
      • 代码模块化
      • 类别/扩展

中级知识点

  • 设计模式

  • UIScrollView/UITableView/UICollectionView 的嵌套

  • 动态行高

  • 通知/代理/block

  • 程序启动原理

  • 触摸事件/手势

  • 图文混编

  • Runtime

  • NSRunLoop

  • GCD

  • ReactiveCocoa开发

  • 3DTouch

  • 界面渲染

  • Charles花瓶抓包

  • 区分模拟器/真机项目

  • 常用知识

    • 单例模式
    • 多线程
    • 网络请求
    • 定位
    • 源代码管理Git
    • 真机调试
    • 苹果内购/广告
    • 推送/远程推送
    • 音频/视频/二维码
    • Block
    • 蓝牙/传感器
    • 物理仿真器UIDynamic
    • 通讯录获取

iOS高级知识点

  • iOS高级知识点
    • Socket
    • XMPP
    • 加密
      • MD5详解
      • Base64加密解密
      • RSA非对称加密
      • AES对称加密
    • 音频
      • 基础
      • Core Audio
      • Audio Toolbox
      • OpenAL
      • AVFoundation
      • Speex语音聊天
      • AudioQueue/AudioSession
      • Speex简介
    • 视频
      • AAC视频.H264推流
      • P2P传输
    • 直播
      • 直播的技术分析与实现
      • RTMP协议
      • RTMP直播应用与延时分析
      • 如果做一款inke版的App
      • 推流发布和播放RTMP
      • FFmpeg
      • 基于FFmpeg的推流器
      • HLS流媒体传输协议(HTTP Live Streaming)
      • FFmpeg
      • ijkPlayer
    • 算法
      • 简介
      • 冒泡排序
      • 快速排序
      • 插入排序
      • 归并排序
      • 二分查找
      • 希尔排序
      • 动态规划
      • 堆排序

官方Kit

  • ARKit.
  • SiriKit
  • HealthKit
  • HomeKit
  • SearchKit
  • IOKit
  • PDFKit
  • CloudKit
  • GameplayKit
  • SpriteKit
  • SceneKit
  • MusicKit
  • ResearchKit
  • MapKit
  • StoreKit
  • AVKit


作者:iOS_asuka
链接:https://www.jianshu.com/p/1ac0a69cd60a


收起阅读 »

iOS - Path menu 的动画效果

AwesomeMenu 是一个与Path的故事菜单外观相同的菜单。通过设置菜单项来创建菜单:UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"]; UIImage *sto...
继续阅读 »

AwesomeMenu 是一个与Path的故事菜单外观相同的菜单

通过设置菜单项来创建菜单:

UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"];
UIImage *storyMenuItemImagePressed = [UIImage imageNamed:@"bg-menuitem-highlighted.png"];
UIImage *starImage = [UIImage imageNamed:@"icon-star.png"];
AwesomeMenuItem *starMenuItem1 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
highlightedImage:storyMenuItemImagePressed
ContentImage:starImage
highlightedContentImage:nil];
AwesomeMenuItem *starMenuItem2 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
highlightedImage:storyMenuItemImagePressed
ContentImage:starImage
highlightedContentImage:nil];
// the start item, similar to "add" button of Path
AwesomeMenuItem *startItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg-addbutton.png"]
highlightedImage:[UIImage imageNamed:@"bg-addbutton-highlighted.png"]
ContentImage:[UIImage imageNamed:@"icon-plus.png"]
highlightedContentImage:[UIImage imageNamed:@"icon-plus-highlighted.png"]];




然后,设置菜单和选项:


AwesomeMenu *menu = [[AwesomeMenu alloc] initWithFrame:self.window.bounds startItem:startItem optionMenus:[NSArray arrayWithObjects:starMenuItem1, starMenuItem2]];
menu.delegate = self;
[self.window addSubview:menu];

您还可以使用菜单选项:

找到“添加”按钮的中心:

menu.startPoint = CGPointMake(160.0, 240.0);

设置旋转角度:

menu.rotateAngle = 0.0;

设置整个菜单角度:

menu.menuWholeAngle = M_PI * 2;

设置每个菜单飞出动画的延迟:

menu.timeOffset = 0.036f;

调整弹跳动画:

menu.farRadius = 140.0f;
menu.nearRadius = 110.0f;

设置“添加”按钮和菜单项之间的距离:

menu.endRadius = 120.0f;


常见问题及demo下载:https://github.com/levey/AwesomeMenu

源码下载:AwesomeMenu-master.zip

收起阅读 »

iOS 滑动效果cell - SWTableViewCell

SWTableViewCell一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)在你的 Podfile 中:- (void)tableView:(UITableView ...
继续阅读 »

SWTableViewCell

一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)

在你的 Podfile 中:

pod 'SWTableViewCell', '~> 0.3.7'


或者只是克隆这个 repo 并手动将源添加到项目

当用户向左滑动时,在表格视图单元格右侧可见的实用程序按钮。此行为类似于在 iOS 应用程序邮件和提醒中看到的行为。



实用程序按钮 当用户向右滑动时,在表格视图单元格左侧可见的实用程序按钮。


  • 动态实用程序按钮缩放。当您向单元格添加更多按钮时,该侧的其他按钮会变小以腾出空间
  • 智能选择:单元格将拾取触摸事件并将单元格滚动回中心或触发委托方法 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

因此,当实用程序按钮可见时,当用户触摸单元格时,单元格不会被视为选中,相反,单元格将滑回原位(与 iOS 7 邮件应用程序功能相同) * 创建带有标题或图标的实用程序按钮以及RGB 颜色 * 在 iOS 6.1 及更高版本上测试,包括 iOS 7

用法

标准表格视图单元格

在您的tableView:cellForRowAtIndexPath:方法中,您设置 SWTableView 单元格并使用包含的NSMutableArray+SWUtilityButtons类别向其中添加任意数量的实用程序按钮


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = @"Cell";

SWTableViewCell *cell = (SWTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];

if (cell == nil) {
cell = [[SWTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.leftUtilityButtons = [self leftButtons];
cell.rightUtilityButtons = [self rightButtons];
cell.delegate = self;
}

NSDate *dateObject = _testArray[indexPath.row];
cell.textLabel.text = [dateObject description];
cell.detailTextLabel.text = @"Some detail text";

return cell;
}

- (NSArray *)rightButtons
{
NSMutableArray *rightUtilityButtons = [NSMutableArray new];
[rightUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:0.78f green:0.78f blue:0.8f alpha:1.0]
title:@"More"];
[rightUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:1.0f green:0.231f blue:0.188 alpha:1.0f]
title:@"Delete"];

return rightUtilityButtons;
}

- (NSArray *)leftButtons
{
NSMutableArray *leftUtilityButtons = [NSMutableArray new];

[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:0.07 green:0.75f blue:0.16f alpha:1.0]
icon:[UIImage imageNamed:@"check.png"]];
[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:1.0f green:1.0f blue:0.35f alpha:1.0]
icon:[UIImage imageNamed:@"clock.png"]];
[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:1.0f green:0.231f blue:0.188f alpha:1.0]
icon:[UIImage imageNamed:@"cross.png"]];
[leftUtilityButtons sw_addUtilityButtonWithColor:
[UIColor colorWithRed:0.55f green:0.27f blue:0.07f alpha:1.0]
icon:[UIImage imageNamed:@"list.png"]];

return leftUtilityButtons;
}

###Custom Table View Cells

Thanks to Matt Bowman you can now create custom table view cells using Interface Builder that have the capabilities of an SWTableViewCell

The first step is to design your cell either in a standalone nib or inside of a table view using prototype cells. Make sure to set the custom class on the cell in interface builder to the subclass you made for it:

Then set the cell reuse identifier:

When writing your custom table view cell's code, make sure your cell is a subclass of SWTableViewCell:

#import <SWTableViewCell.h>

@interface MyCustomTableViewCell : SWTableViewCell

@property (weak, nonatomic) UILabel *customLabel;
@property (weak, nonatomic) UIImageView *customImageView;

@end

If you are using a separate nib and not a prototype cell, you'll need to be sure to register the nib in your table view:

- (void)viewDidLoad
{
[super viewDidLoad];

[self.tableView registerNib:[UINib nibWithNibName:@"MyCustomTableViewCellNibFileName" bundle:nil] forCellReuseIdentifier:@"MyCustomCell"];
}

Then, in the tableView:cellForRowAtIndexPath: method of your UITableViewDataSource (usually your view controller), initialize your custom cell:

- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
static NSString *cellIdentifier = @"MyCustomCell";

MyCustomTableViewCell *cell = (MyCustomTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];

cell.leftUtilityButtons = [self leftButtons];
cell.rightUtilityButtons = [self rightButtons];
cell.delegate = self;

cell.customLabel.text = @"Some Text";
cell.customImageView.image = [UIImage imageNamed:@"MyAwesomeTableCellImage"];
[cell setCellHeight:cell.frame.size.height];
return cell;
}

代理方法

// click event on left utility button
- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerLeftUtilityButtonWithIndex:(NSInteger)index;

// click event on right utility button
- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index;

// utility button open/close event
- (void)swipeableTableViewCell:(SWTableViewCell *)cell scrollingToState:(SWCellState)state;

// prevent multiple cells from showing utilty buttons simultaneously
- (BOOL)swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:(SWTableViewCell *)cell;

// prevent cell(s) from displaying left/right utility buttons
- (BOOL)swipeableTableViewCell:(SWTableViewCell *)cell canSwipeToState:(SWCellState)state;


常见问题及demo下载:https://github.com/CEWendel/SWTableViewCell

源码下载:SWTableViewCell-master.zip


收起阅读 »

iOS 标签浮动-JVFloatLabeledTextField

JVFloatLabeledTextFieldJVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”。由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任...
继续阅读 »

JVFloatLabeledTextField

JVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”

由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任何标签。

这个 UI 组件库包括 aUITextFieldUITextView子类,旨在通过将占位符转换为浮动标签来改善用户体验,这些标签在填充文本后悬停在字段上方。

马特 D. 史密斯的设计


通过 CocoaPods 

sudo gem install cocoapods

Podfile在您的项目目录中创建一个

pod init

将以下内容添加到您的Podfile项目目标中:

pod 'JVFloatLabeledTextField'

然后运行 CocoaPods pod install

最后,将JVFloatLabeledTextField.h包含JVFloatLabeledTextView.h在您的项目中。

Carthage

brew update
brew install carthage

Cartfile在您的项目目录中创建一个包含:

github "jverdi/JVFloatLabeledTextField"

然后运行 carthagecarthage updateJVFloatLabeledText.frameworkCarthage/Build/iOS目录中添加到您的项目中

最后,JVFloatLabeledText.h在您的项目中包含

#import <JVFloatLabeledText/JVFloatLabeledText.h>


常见问题及demo下载:https://github.com/jverdi/JVFloatLabeledTextField

源码下载:JVFloatLabeledTextField-main.zip








收起阅读 »

Android转场动画的前世今生

前一段时间做图片查看器的升级时,在打开图片查看器的时,找不到好的过渡方式。医生推荐了Android最新的Material Motion动画,虽然最终没有给我们的App安排,但给我学习Material Motion动画提供了一次契机。推荐给大家的学习资料:什么是...
继续阅读 »

前一段时间做图片查看器的升级时,在打开图片查看器的时,找不到好的过渡方式。

医生推荐了Android最新的Material Motion动画,虽然最终没有给我们的App安排,但给我学习Material Motion动画提供了一次契机。

推荐给大家的学习资料:

什么是转场动画?

在学习动画的时候,我们总是会听到转场动画,那么,什么是转场动画呢?

首先,对于一个动画而言,两个关键帧是动画的开始帧和动画的结束帧,转场则是两个关键帧之间的过渡。

一个完整的转场动画如图:

完整转场

图片来自《动态设计的转场心法》

一、最初的转场

先教大家一个干货:

adb shell settings put global window_animation_scale 10
adb shell settings put global transition_animation_scale 10
adb shell settings put global animator_duration_scale 10

这个命令可以将动画放慢10倍,方便学习动画的细节,速度恢复则把10改成1。

还记得一开始两个 Activity 怎么过渡的吗?没错就是使用 overridePendingTransition 方法。

Android 2.0 以后可以使用 overridePendingTransition(int enterAnim, int exitAnim) 来完成 Activity 的跳转动画,其中,第一个参数 exitAnim 对应着上述图片转场中的 IN,第二个参数 enterAnim 对应着上述图片中的 OUT

如果要写一个平移和透明度跳转动画,它通常是这样的:

步骤一 设置进入和退出动画

在资源文件下 anim 目录下新建一个动画的资源文件,Activity 进入动画 anim_in 文件:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500">

<translate
android:fromXDelta="100%p"
android:toXDelta="0"
/>

<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"
/>
</set>

Activity 退出动画 anim_out 文件:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500">

<translate
android:fromXDelta="0"
android:toXDelta="-100%p"
/>

<alpha
android:fromAlpha="1.0"
android:toAlpha="0.0"
/>
</set>

步骤二 引用动画文件

在界面跳转的时候,调用 overridePendingTransition 方法:

companion object {
fun start(context: Context){
val intent = Intent(context, SecondActivity::class.java)
context.startActivity(intent)
if(context is Activity){
context.overridePendingTransition(R.anim.anim_in, R.anim.anim_out)
}
}
}

效果:

资源文件动画

overridePendingTransition写法的问题

和View动画一样,使用虽爽,但只支持平移、旋转、缩放和透明度这四种动画类型,遇到稍微复杂的动画也只能撒手了。

二、Android 5.0 Material 转场动画

在 Android 5.0 之后,我们可以使用 Material Design 为我们带来的转场动画。

不说别的,先看几个案例:

官方Demo掘金App我的App

从左到右依次是官方Demo、掘金App和我的开源项目Hoo,与最初的转场的不同点如下:

  1. 如果说 overridePendingTransition 对应着 View 动画,那么 Material 转场对应着的是属性动画,所以可以自定义界面过渡动画。
  2. 除了进入、退出场景,Material 转场为我们增加一种新的场景,共享元素,上述三图的动画过渡都用到了共享元素。
  3. 不仅仅能用在Activity,还可以用在Fragment和View之间。

三张图中都使用了ImageView作为共享元素(Hoo中使用更加复杂的PhotoView),共享元素的动画看着十分有趣,看着就像图片从A界面中跳到了B界面上。

为什么我可以判断掘金也是使用的 Material 转场?因为 Material 共享元素动画开始的时候默认会将 StartView 的 Alpha 设置为0,仔细看掘金大图打开的一瞬间,后面的图已经没了~,并且一开始过渡还有一点小瑕疵。

1. 进入和退出动画

进入和退出动画不包括共享元素的动画,只支持三种动画类型:

动画解释
Explode(爆炸式)将视图移入场景中心或从中移出
Slide(滑动式)将视图从场景的其中一个边缘移入或移出
Fade(淡入淡出式)通过更改视图的不透明度,在场景中添加视图或从中移除视图

细心的同学可能发现,Material Design没有支持 Scale 和 Rotation 这两种类型的动画,可能这两种类型的过渡动画使用场景实在太少,如果实在想用,可以自定义实现。

步骤一 创建Material Bundle

    startActivity(intent,
ActivityOptions.makeSceneTransitionAnimation(this).toBundle())

步骤二 设置动画

override fun onCreate(savedInstanceState: Bundle?) {
// 开启Material动画
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
super.onCreate(savedInstanceState)
//setContentView(R.layout.detail_activity)
// 设置进入的动画
window.enterTransition = Slide()
// 设置退出动画
window.exitTransition = Slide()
}

除了这种方式,还可以通过设置主题的方式设置进入和退出动画,同样也适用于共享动画。

2. 共享元素动画

启用共享元素动画的步骤跟之前的步骤稍有不同。

步骤一 设置Activity A

先给 View 设置 transitionName

ivShoe.transitionName = transitionName

接着,它需要提供共享的 View 和 TransitionName

其实,就是想让你告诉系统,什么样的 View 需要做动画,那如果有多个 View 呢?所以,你还得给View 绑定一个 TransitionName,防止动画做混了。

代码:

val options = ActivityOptions.makeSceneTransitionAnimation(this, binding.ivShoe, transitionName)
ImageGalleryActivity.start(this, it, options.toBundle(), transitionName)

如果有多个共享元素,可以将关系存进 Pair,然后把 Pair 放进去,不懂的可以看一下 Api。

步骤二 为Activity B设置共享元素动画

默认支持的共享元素的动画也是有限的,支持的种类有:

动画说明
changeBounds为目标视图布局边界的变化添加动画效果
changeClipBounds为目标视图裁剪边界的变化添加动画效果
changeTransform为目标视图缩放和旋转方面的变化添加动画效果
changeImageTransform为目标图片尺寸和缩放方面的变化添加动画效果

通过 Window 设置 sharedElementEnterTransition 和 sharedElementExitTransition

override fun onCreate(savedInstanceState: Bundle?) {
// 开启Material动画
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
val transitionSet = TransitionSet()
transitionSet.addTransition(ChangeBounds())
transitionSet.addTransition(ChangeClipBounds())
transitionSet.addTransition(ChangeImageTransform())
window.sharedElementEnterTransition = transitionSet
window.sharedElementExitTransition = transitionSet
super.onCreate(savedInstanceState)
// 我这里的transitionName是通过Intent传进去的
val transitionName = intent.getStringExtra(CUS_TRANSITION_NAME)
// 给ImageView设置transitionName
binding.ivShoe.transitionName = transitionName
}

这样写完大部分场景都是可以用的,但是,如果你是通过 Glide 加载或者其他图片库加载的网络图片,恭喜你,大概率会遇到这样的问题:

列表页动画

为什么会出现这样的情况?因为加载网络图片是需要时间的,我们可以等 B 页面的图片加载好了,再去开启动画,Material 装厂就支持这样的操作。

在 onCreate 中调用 postponeEnterTransition() 方法表明我们的动画需要延迟执行,等我们需要的时机,再调用 Activity 中的 startPostponedEnterTransition() 方法来开始执行动画,所以,即便是在 A 界面中,跳转到 B 界面中的 Fragment,动画也是一样可以执行的。

到这儿,界面就可以正常跳转了,图片就不放了。

共享元素动画原理其实也很简单,如果是 A 跳到 B,会先把 A 和 B 的共享元素的状态分别记录下来,之后跳到 B,根据先前记录的状态执行属性动画,虽然是叫共享元素,它们可是不同的 View

不仅仅 Activity 可以支持 Material 转场动画,Fragment 和 View 也都是可以的(之前我一直以为是不可以的~),感兴趣的同学可以自行研究。

三、Android Material Motion动画

新出的 Motion 动画是什么呢?

1. Android Motion 简介

其实它就是新支持的四种动画类型,分别是:

1.1 Container transform

container_transform

Container transform 也是基于共享元素的动画,跟之前共享元素动画最大的不同点在于它的 Start View可以是一个 ViewGroup,也可以是一个 View,如图一中所看到的那样,它的 Start View 是一个 CardView

1.2 Shared axis

shared_axis

Shared axis 看上去像平移动画,官方展示的三个例子分别是,横向平移、纵向平移和Z轴平移。

1.3 Fade Through

fade_through

Fade Through 本质上是一个透明度+缩放动画,官方的建议是用在两个关联性不强的界面的跳转中。

1.4 Fade

fade

乍一看,Fade 动画和上面的 Fade Through 是一致的,就动画本质而言,它们的确是一样的透明度+缩放动画,但是官方建议,如果发生在同一个界面,比如弹出Dialog、Menu等这类的弹框可以考虑这种动画。

Google 提供了两种库供大家使用。

一种是 AndroidX 包,特点是:

  • 兼容到 API 14
  • 仅支持 Fragment 和 View 之间的过渡
  • 行为一致性

另外一种是 Platform 包,特点是:

  • 兼容到 API 21
  • 支持 Fragment、View、Activity 和 Window
  • 在不同的 API 上,可能会有点差异

现在的 App,最低版本应该都在 21 了,而且支持 Activity,所以建议还是选择 Platform。

2. Material Motion 初体验

我们以 Container transform 为例,来个 Activity 之间的 Android Motion 动画的初体验:

Container Transform

步骤一 引入依赖

implementation 'com.google.android.material:material:1.4.0-alpha01'

步骤二 设置Activity A

这里的 Activity A 对应着 MainActivity,在 MainActivity 中启用转场动画:

class MainActivity : AppCompatActivity() {

//...
override fun onCreate(savedInstanceState: Bundle?) {
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())
window.sharedElementsUseOverlay = false
super.onCreate(savedInstanceState)
//...
}
}

步骤三 设置跳转事件

跟创建共享元素的步骤一样,先设置 TransitionName:

private fun onCreateListener(id: Long, url: String): View.OnClickListener {
return View.OnClickListener {
val transitionName = "${id}-${url}"
it.transitionName = transitionName
DetailActivity.start(context, id, it as ConstraintLayout, transitionName)
}
}

这里偷了懒,将 TransitionName 的设置放在了点击事件中,接着创建 Bundle:

const val CUS_TRANSITION_NAME: String = "transition_name"
class DetailActivity : AppCompatActivity() {
companion object {
fun start(context: Context, id: Long, viewGroup: ConstraintLayout, transitionName: String){
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(BaseConstant.DETAIL_SHOE_ID, id)
intent.putExtra(CUS_TRANSITION_NAME, transitionName)
if(context is Activity){
context.startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(context, viewGroup, transitionName).toBundle())
}else {
context.startActivity(intent)
}
}
}
}

步骤四 设置Activity B

Demo 中的 Activity B 对应着 DetailActivity,这一步主要给进入和退出的共享动画设置 MaterialContainerTransform,具体的代码是:

override fun onCreate(savedInstanceState: Bundle?) {
// 1. 设置动画
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())

super.onCreate(savedInstanceState)
//...

// 2. 设置transitionName
binding.mainContent.transitionName = intent.getStringExtra(CUS_TRANSITION_NAME)
// 3. 设置具体的动画
window.sharedElementEnterTransition = MaterialContainerTransform().apply {
addTarget(binding.mainContent)
duration = 300L
}
window.sharedElementExitTransition = MaterialContainerTransform().apply {
addTarget(binding.mainContent)
duration = 300L
}
}

Demo 中使用了 DataBinding,不过你只需要了解 binding.mainContent 是一个 ViewGroup。到这儿,你就可以成功的看到 Demo 中的效果了。

Material Motion 其实 Android 5.0 中加入的转场动画一样,它们也继承自 Transition,但给我们的使用带来了很大的方便。

四、总结

在 Android 转场的过程中:

  1. 最初的 View 转场带给我们平移、缩放、旋转和透明度四种基本能力的支持;
  2. 接着,Android 5.0 Material 转场给我们带来了共享元素动画的惊喜,并具备了自定义转场动画的能力,升级了Android转场的玩法;
  3. 最后是出来不久的 Android Motion,通过封装了四种动画,降低了我们转场的使用难度。

虽然起点的图片查看器的专场没有使用 Material 转场,但是过度依然丝滑,感兴趣的话我会在后面单独开一篇。

收起阅读 »

Android判断Activity是否在AndroidManifest.xml里面注册(源码分析)

Android判断Activity是否在AndroidManifest.xml里面注册(源码分析) 这个问题相信大家在实际的开发中,都遇到过这个问题,答案就不用说了,在AndroidManifest.xml中添加Activity的注册,毕竟Activity...
继续阅读 »


Android判断Activity是否在AndroidManifest.xml里面注册(源码分析)


在这里插入图片描述
这个问题相信大家在实际的开发中,都遇到过这个问题,答案就不用说了,在AndroidManifest.xml中添加Activity的注册,毕竟Activity属于四大组件之一,使用的时候,需要要在清单文件中注册。


<activity android:name=".TargetActivity"></activity>

但是这个出现这个问题的根源在哪里?下面我们就进入源码仔细看看。


这里就不一步一步进入源码,直接分析关键代码:


public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { ...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
? //1.通过IActivityManager调用我们执行AMS的startActivity方法,并返回执行 结果
int result = ActivityManager.getService() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
//2. 检查结果
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}

通过源码execStartActivity这个方法可以看到主要是在这个检查结果这里面去分析的checkStartActivityResult(result, intent);


public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
//3. 这里我们找到了报错的地方,原来是res结果为 START_INTENT_NOT_RESOLVED,
// START_CLASS_NOT_FOUND就会报这个错误
if (intent instanceof Intent && ((Intent) intent).getComponent() != null)
throw new ActivityNotFoundException("Unable to find explicit activity class " + ((Intent) intent).getComponent().toShortString() + "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException("No Activity found to handle " + intent);
case ActivityManager.START_PERMISSION_DENIED:
throw new SecurityException("Not allowed to start activity " + intent);
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
throw new AndroidRuntimeException("FORWARD_RESULT_FLAG used while also requesting a result");
case ActivityManager.START_NOT_ACTIVITY:
throw new IllegalArgumentException("PendingIntent is not an activity");
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
throw new SecurityException("Starting under voice control not allowed for: " + intent);
case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
throw new IllegalStateException("Session calling startVoiceActivity does not match active session");
case ActivityManager.START_VOICE_HIDDEN_SESSION:
throw new IllegalStateException("Cannot start voice activity on a hidden session");
case ActivityManager.START_ASSISTANT_NOT_ACTIVE_SESSION:
throw new IllegalStateException("Session calling startAssistantActivity does not match active session");
case ActivityManager.START_ASSISTANT_HIDDEN_SESSION:
throw new IllegalStateException("Cannot start assistant activity on a hidden session");
case ActivityManager.START_CANCELED:
throw new AndroidRuntimeException("Activity could not be started for " + intent);
default:
throw new AndroidRuntimeException("Unknown error code " + res + " when starting " + intent);
}
}

可以看到当结果为


case ActivityManager.START_INTENT_NOT_RESOLVED:


case ActivityManager.START_CLASS_NOT_FOUND:


?就会报
throw new ActivityNotFoundException("Unable to find explicit activity class " + ((Intent) intent).getComponent().toShortString() + “; have you declared this activity in your AndroidManifest.xml?”);


这下我们就知道了如果没在清单文件中添加这个注册,报错的位置。


AMS是如何判断activity没有注册的,首先我们得明白startActivity执行的主流程


这个篇幅太多了,可以自己去源码跟一下,这里不作介绍,


我们这里分析主要流程代码


找到在ASR.startActivity (ActivityStarter)中返回了


START_INTENT_NOT_RESOLVED,START_CLASS_NOT_FOUND


private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent, String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid, String callingPackage, int realCallingPid, int realCallingUid, int startFlags, SafeActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity, TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup) {
int err = ActivityManager.START_SUCCESS;
...
//接下来开始做一些校验判断
if (err == ActivityManager.START_SUCCESS && intent.getComponent() == null) {
// We couldn't find a class that can handle the given Intent.
// That's the end of that! err = ActivityManager.START_INTENT_NOT_RESOLVED;
// 从Intent中无法找 到相应的Component
}
if (err == ActivityManager.START_SUCCESS && aInfo == null) {
// We couldn't find the specific class specified in the Intent.
// Also the end of the line.
err = ActivityManager.START_CLASS_NOT_FOUND;
// 从Intent中无法找到相 应的ActivityInfo
}
...
if (err != START_SUCCESS) {
//不能成功启动了,返回err
if (resultRecord != null) {
resultStack.sendActivityResultLocked(-1, resultRecord, resultWho, requestCode, RESULT_CANCELED, null);
}
SafeActivityOptions.abort(options);
return err;
}
//创建出我们的目标ActivityRecord对象,存到传入数组0索引上
ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid, callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(), resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null, mSupervisor, checkedOptions, sourceRecord);
...
return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags, true /* doResume */, checkedOptions, inTask, outActivity);
}

但是 intent.getComponent(),aInfo又是从哪儿获取的呢,我们回溯到


startActivityMayWait.


看下上面的aInfo哪来的.


ActivityInfo resolveActivity(Intent intent, ResolveInfo rInfo, int startFlags, ProfilerInfo profilerInfo) {
? final ActivityInfo aInfo = rInfo != null ? rInfo.activityInfo : null;
if (aInfo != null) {
// Store the found target back into the intent, because now that
// we have it we never want to do this again. For example, if the
// user navigates back to this point in the history, we should
// always restart the exact same activity.
intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
// Don't debug things in the system process ...
}
return aInfo;
}

发现是从rInfo来的


ResolveInfo resolveIntent(Intent intent, String resolvedType, int userId, int flags, int filterCallingUid) {
synchronized (mService) {
? try {...final long token = Binder.clearCallingIdentity();
try {
return mService.getPackageManagerInternalLocked().resolveIntent(intent, resolvedType, modifiedFlags, userId, true, filterCallingUid);
} finally {
Binder.restoreCallingIdentity(token);
} ...
}
}
}

rInfo的获取


PackageManagerInternal getPackageManagerInternalLocked() {
if (mPackageManagerInt == null) {
? mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class);
}
return mPackageManagerInt;
}

具体实现类是PackageManagerService


?@Override
public ResolveInfo resolveIntent(Intent intent, String resolvedType, int flags, int userId) {
return resolveIntentInternal(intent, resolvedType, flags, userId, false, Binder.getCallingUid());
}

看resolveIntentInternal


private ResolveInfo resolveIntentInternal(Intent intent, String resolvedType,int flags, int userId, boolean resolveForStart, int filterCallingUid) {
try {...
//获取ResolveInfo列表
final List<ResolveInfo> query = queryIntentActivitiesInternal(intent, resolvedType, flags, filterCallingUid, userId, resolveForStart, true /*allowDynamicSplits*/);
? Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
//找出最好的返回
final ResolveInfo bestChoice = chooseBestActivity(intent, resolvedType, flags, query, userId);
return bestChoice;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}

看 queryIntentActivitiesInternal


private @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent, String resolvedType, int flags, int filterCallingUid, int userId, boolean resolveForStart, boolean allowDynamicSplits) {
? ...
if (comp != null) {
final List<ResolveInfo> list = new ArrayList<ResolveInfo>(1);
final ActivityInfo ai = getActivityInfo(comp, flags, userId);
if (ai != null) {
...
}
}

原来是从getActivityInfo获取的


	@Override
public ActivityInfo getActivityInfo(ComponentName component, int flags, int userId) {
return getActivityInfoInternal(component, flags, Binder.getCallingUid(), userId);
? }

getActivityInfoInternal方法


private ActivityInfo getActivityInfoInternal(ComponentName component, int flags, int filterCallingUid, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId, component);
if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) {
mPermissionManager.enforceCrossUserPermission(Binder.getCallingUid(), userId, false /* requireFullPermission */, false /* checkShell */, "get activity info");
}
synchronized (mPackages) {
//关键点
PackageParser.Activity a = mActivities.mActivities.get(component);
if (DEBUG_PACKAGE_INFO) Log.v(TAG, "getActivityInfo " + component + ": " + a);
? if (a != null && mSettings.isEnabledAndMatchLPr(a.info, flags, userId)) {
PackageSetting ps = mSettings.mPackages.get(component.getPackageName());
if (ps == null) return null;
if (filterAppAccessLPr(ps, filterCallingUid, component, TYPE_ACTIVITY, userId)) {
return null;
}
//关键点
return PackageParser.generateActivityInfo(a, flags, ps.readUserState(userId), userId);
}
if (mResolveComponentName.equals(component)) {
return PackageParser.generateActivityInfo(mResolveActivity, flags, new PackageUserState(), userId);
}
}
return null;
}

分析到这里,大家应该知道怎么回事了吧,其实就是解析了AndroidManifest.xml里面的信息,具体怎么解析,等有空了分析。


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



收起阅读 »

美团面试题:JVM的年轻代是怎么设计的?

1、JVM中的堆,一般分为三个部分,新生代、老年代和永久代。这个是你第一天学JVM就知道的。但你可以先想想,为什么需要把堆分代?不分代不能完成他所做的事情么? 2、是这样,如果没有分代,那我们所有的对象都在一块,GC 的时候就要先找到哪些对象没用,怎么找呢...
继续阅读 »

1、JVM中的堆,一般分为三个部分,新生代、老年代和永久代。这个是你第一天学JVM就知道的。但你可以先想想,为什么需要把堆分代?不分代不能完成他所做的事情么?


2、是这样,如果没有分代,那我们所有的对象都在一块,GC 的时候就要先找到哪些对象没用,怎么找呢?没分代就得对堆的所有区域进行扫描。但你知道,很多Java对象都是朝生夕死的,如果分代的话,我们可以把新创建的对象放到某一地方,GC的时候就可以迅速回收这块存“朝生夕死”对象的区域。


3、所以,一句话总结,分代的唯一理由就是优化 GC 性能。你这么记,就容易把知识串起来了。


4、HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区和 2 个 Survivor 区(分别叫from 和 to),他们的默认比例为 8:1。一般情况下,新创建的对象都会被分配到 Eden区,这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中。这是一个对象的生存路径。


5、因为年轻代中的对象基本都是朝生夕死的( 80% 以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。


6、在 GC 开始的时候,对象只会存在于 Eden 区和名为“ From ”的 Survivor 区,Survivor 区“To”是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。


7、年龄达到一定值(可以通过-XX: MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次 GC 后,Eden 区和From 区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次 GC 前的“From”,新的“From”就是上次 GC 前的“To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。



8、Minor GC 会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。注意,我说的是 Minor GC,不是 Full GC,这俩的关系你要缕清楚。


9、好记吗?不好记,再给你做个类比。我叫小强,是一个普通的 Java 对象,我出生在Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的“From”区,自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的“From”区,有时候在 Survivor 的“To”区,居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后去世了。


10、年轻代的JVM参数也不多,给你列出来了,你也不用记,用着多了自然就熟悉了。



总而言之,JVM 内存问题排查需要掌握一定的技巧,而这些技巧并不是告诉你,你就会用的,更重要的还是需要在实战中去应用



收起阅读 »

Android:OkHttp的理解和使用

OkHttp的理解和使用 1、什么是OkHttp 1、网络请求发展 历史上Http请求库优缺点 HttpURLConnection—>Apache HTTP Client—>Volley—->okHttp 2、项目开源...
继续阅读 »




OkHttp的理解和使用


在这里插入图片描述


1、什么是OkHttp


1、网络请求发展


历史上Http请求库优缺点



HttpURLConnection—>Apache HTTP Client—>Volley—->okHttp



2、项目开源地址



https://github.com/square/okhttp



3、OkHttp是什么



  • OKhttp是一个网络请求开源项目,Android网络请求轻量级框架,支持文件上传与下载,支持https。


2、OkHttp的作用


OkHttp是一个高效的HTTP库:



  • 支持HTTP/2, HTTP/2通过使用多路复用技术在一个单独的TCP连接上支持并发, 通过在一个连接上一次性发送多个请求来发送或接收数据

  • 如果HTTP/2不可用, 连接池复用技术也可以极大减少延时

  • 支持GZIP, 可以压缩下载体积

  • 响应缓存可以直接避免重复请求

  • 会从很多常用的连接问题中自动恢复

  • 如果您的服务器配置了多个IP地址, 当第一个IP连接失败的时候, OkHttp会自动尝试下一个IP OkHttp还处理了代理服务器问题和SSL握手失败问题


优势



  • 使用 OkHttp无需重写您程序中的网络代码。OkHttp实现了几乎和java.net.HttpURLConnection一样的API。如果您用了 Apache HttpClient,则OkHttp也提供了一个对应的okhttp-apache 模块


3、Okhttp的基本使用


Okhttp的基本使用,从以下五方面讲解:



  • 1.Get请求(同步和异步)

  • 2.POST请求表单(key-value)

  • 3.POST请求提交(JSON/String/文件等)

  • 4.文件下载

  • 5.请求超时设置


加入build.gradle


compile 'com.squareup.okhttp3:okhttp:3.6.0'

3.1、Http请求和响应的组成


http请求
在这里插入图片描述
所以一个类库要完成一个http请求, 需要包含 请求方法, 请求地址, 请求协议, 请求头, 请求体这五部分. 这些都在okhttp3.Request的类中有体现, 这个类正是代表http请求的类. 看下图:


在这里插入图片描述
其中HttpUrl类代表请求地址, String method代表请求方法, Headers代表请求头, RequestBody代表请求体. Object tag这个是用来取消http请求的标志, 这个我们先不管.


http响应


响应组成图:
在这里插入图片描述
可以看到大体由应答首行, 应答头, 应答体构成. 但是应答首行表达的信息过多, HTTP/1.1表示访问协议, 200是响应码, OK是描述状态的消息.


根据单一职责, 我们不应该把这么多内容用一个应答首行来表示. 这样的话, 我们的响应就应该由访问协议, 响应码, 描述信息, 响应头, 响应体来组成.


3.2、OkHttp请求和响应的组成


OkHttp请求


构造一个http请求, 并查看请求具体内容:


final Request request = new Request.Builder().url("https://github.com/").build();

我们看下在内存中, 这个请求是什么样子的, 是否如我们上文所说和请求方法, 请求地址, 请求头, 请求体一一对应.
在这里插入图片描述
OkHttp响应


OkHttp库怎么表示一个响应:
在这里插入图片描述
可以看到Response类里面有Protocol代表请求协议, int code代表响应码, String message代表描述信息, Headers代表响应头, ResponseBody代表响应体. 当然除此之外, 还有Request代表持有的请求, Handshake代表SSL/TLS握手协议验证时的信息, 这些额外信息我们暂时不问.


有了刚才说的OkHttp响应的类组成, 我们看下OkHttp请求后响应在内存中的内容:


final Request request = new Request.Builder().url("https://github.com/").build();
Response response = client.newCall(request).execute();

在这里插入图片描述


3.3、GET请求同步方法


同步GET的意思是一直等待http请求, 直到返回了响应. 在这之间会阻塞进程, 所以通过get不能在Android的主线程中执行, 否则会报错.


对于同步请求在请求时需要开启子线程,请求成功后需要跳转到UI线程修改UI。


public void getDatasync(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象
Request request = new Request.Builder()
.url("http://www.baidu.com")//请求接口。如果需要传参拼接到接口后面。
.build();//创建Request 对象
Response response = null;
response = client.newCall(request).execute();//得到Response 对象
if (response.isSuccessful()) {
Log.d("kwwl","response.code()=="+response.code());
Log.d("kwwl","response.message()=="+response.message());
Log.d("kwwl","res=="+response.body().string());
//此时的代码执行在子线程,修改UI的操作请使用handler跳转到UI线程。
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}

此时打印结果如下:


response.code()==200
response.message()OK;
res{“code”:200,“message”:success};

OkHttpClient实现了Call.Factory接口, 是Call的工厂类, Call负责发送执行请求和读取响应.
Request代表Http请求, 通过Request.Builder辅助类来构建.


client.newCall(request)通过传入一个http request, 返回一个Call调用. 然后执行execute()方法, 同步获得Response代表Http请求的响应. response.body()是ResponseBody类, 代表响应体


注意事项:


1,Response.code是http响应行中的code,如果访问成功则返回200.这个不是服务器设置的,而是http协议中自带的。res中的code才是服务器设置的。注意二者的区别。


2,response.body().string()本质是输入流的读操作,所以它还是网络请求的一部分,所以这行代码必须放在子线程。


3,response.body().string()只能调用一次,在第一次时有返回值,第二次再调用时将会返回null。原因是:response.body().string()的本质是输入流的读操作,必须有服务器的输出流的写操作时客户端的读操作才能得到数据。而服务器的写操作只执行一次,所以客户端的读操作也只能执行一次,第二次将返回null。


4、响应体的string()方法对于小文档来说十分方便高效. 但是如果响应体太大(超过1MB), 应避免使用 string()方法, 因为它会将把整个文档加载到内存中.


5、对于超过1MB的响应body, 应使用流的方式来处理响应body. 这和我们处理xml文档的逻辑是一致的, 小文件可以载入内存树状解析, 大文件就必须流式解析.


注解:


responseBody.string()获得字符串的表达形式, 或responseBody.bytes()获得字节数组的表达形式, 这两种形式都会把文档加入到内存. 也可以通过responseBody.charStream()和responseBody.byteStream()返回流来处理.


3.4、GET请求异步方法


异步GET是指在另外的工作线程中执行http请求, 请求时不会阻塞当前的线程, 所以可以在Android主线程中使用.


这种方式不用再次开启子线程,但回调方法是执行在子线程中,所以在更新UI时还要跳转到UI线程中。


下面是在一个工作线程中下载文件, 当响应可读时回调Callback接口. 当响应头准备好后, 就会调用Callback接口, 所以读取响应体时可能会阻塞. OkHttp现阶段不提供异步api来接收响应体。


private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, Throwable throwable) {
throwable.printStackTrace();
}

@Override public void onResponse(Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}

System.out.println(response.body().string());
}
});
}

异步请求的打印结果与注意事项与同步请求时相同。最大的不同点就是异步请求不需要开启子线程,enqueue方法会自动将网络请求部分放入子线程中执行。


注意事项:



  • 1,回调接口的onFailure方法onResponse执行在子线程。

  • 2,response.body().string()方法也必须放在子线程中。当执行这行代码得到结果后,再跳转到UI线程修改UI。


3.5、post请求方法


Post请求也分同步和异步两种方式,同步与异步的区别和get方法类似,所以此时只讲解post异步请求的使用方法。


private void postDataWithParame() {
OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象。
FormBody.Builder formBody = new FormBody.Builder();//创建表单请求体
formBody.add("username","zhangsan");//传递键值对参数
Request request = new Request.Builder()//创建Request 对象。
.url("http://www.baidu.com")
.post(formBody.build())//传递请求体
.build();
client.newCall(request).enqueue(new Callback() {。。。});//回调方法的使用与get异步请求相同,此时略。
}



看完代码我们会发现:post请求中并没有设置请求方式为POST,回忆在get请求中也没有设置请求方式为GET,那么是怎么区分请求方式的呢?重点是Request.Builder类的post方法,在Request.Builder对象创建最初默认是get请求,所以在get请求中不需要设置请求方式,当调用post方法时把请求方式修改为POST。所以此时为POST请求。


3.6、POST请求传递参数的方法总结


3.6.1、Post方式提交String


下面是使用HTTP POST提交请求到服务. 这个例子提交了一个markdown文档到web服务, 以HTML方式渲染markdown. 因为整个请求体都在内存中, 因此避免使用此api提交大文档(大于1MB).


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
String postBody = ""
+ "Releases\n"
+ "--------\n"
+ "\n"
+ " * _1.0_ May 6, 2013\n"
+ " * _1.1_ June 15, 2013\n"
+ " * _1.2_ August 11, 2013\n";

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.2、Post方式提交


以流的方式POST提交请求体. 请求体的内容由流写入产生. 这个例子是流直接写入Okio的BufferedSink. 你的程序可能会使用OutputStream, 你可以使用BufferedSink.outputStream()来获取. OkHttp的底层对流和字节的操作都是基于Okio库, Okio库也是Square开发的另一个IO库, 填补I/O和NIO的空缺, 目的是提供简单便于使用的接口来操作IO.


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MEDIA_TYPE_MARKDOWN;
}

@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
for (int i = 2; i <= 997; i++) {
sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
}
}

private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.3、Post方式提交文件


public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
File file = new File("README.md");

Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.6.4、Post方式提交表单


使用FormEncodingBuilder来构建和HTML标签相同效果的请求体. 键值对将使用一种HTML兼容形式的URL编码来进行编码.


 private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println(response.body().string());
}

3.7、POST其他用法


3.7.1、提取响应头


典型的HTTP头像是一个Map<String, String> : 每个字段都有一个或没有值. 但是一些头允许多个值, 像Guava的Multimap


例如:


HTTP响应里面提供的Vary响应头, 就是多值的. OkHttp的api试图让这些情况都适用.



  • 当写请求头的时候, 使用header(name, value)可以设置唯一的name、value. 如果已经有值, 旧的将被移除,然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).

  • 当读取响应头时, 使用header(name)返回最后出现的name、value. 通常情况这也是唯一的name、value.如果没有值, 那么header(name)将返回null. 如果想读取字段对应的所有值,使用headers(name)会返回一个list.


为了获取所有的Header, Headers类支持按index访问.


private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();

Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}

3.7.2、使用Gson来解析JSON响应


Gson是一个在JSON和Java对象之间转换非常方便的api库. 这里我们用Gson来解析Github API的JSON响应.


注意: ResponseBody.charStream()使用响应头Content-Type指定的字符集来解析响应体. 默认是UTF-8.


private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();

public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().content);
}
}

static class Gist {
Map<String, GistFile> files;
}

static class GistFile {
String content;
}

3.7.3、响应缓存


为了缓存响应, 你需要一个你可以读写的缓存目录, 和缓存大小的限制. 这个缓存目录应该是私有的, 不信任的程序应不能读取缓存内容.


一个缓存目录同时拥有多个缓存访问是错误的. 大多数程序只需要调用一次new OkHttp(), 在第一次调用时配置好缓存, 然后其他地方只需要调用这个实例就可以了. 否则两个缓存示例互相干扰, 破坏响应缓存, 而且有可能会导致程序崩溃.


响应缓存使用HTTP头作为配置. 你可以在请求头中添加Cache-Control: max-stale=3600 , OkHttp缓存会支持. 你的服务通过响应头确定响应缓存多长时间, 例如使用Cache-Control: max-age=9600.


private final OkHttpClient client;

public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);

client = new OkHttpClient();
client.setCache(cache);
}

public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();

Response response1 = client.newCall(request).execute();
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

String response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());

Response response2 = client.newCall(request).execute();
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

String response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());

System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}

如果需要阻值response使用缓存, 使用CacheControl.FORCE_NETWORK. 如果需要阻值response使用网络, 使用CacheControl.FORCE_CACHE.


警告
如果你使用FORCE_CACHE, 但是response要求使用网络, OkHttp将会返回一个504 Unsatisfiable Request响应.





收起阅读 »

功能强大的升级库

CheckVersionLib V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本 效果 特点 任何地方都可以调用 简单简单简单简单(重要的话我说四遍) 扩...
继续阅读 »

CheckVersionLib


V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大

老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本


效果

V2.gif



特点



  • 任何地方都可以调用




  • 简单简单简单简单(重要的话我说四遍)




  • 扩展性强大




  • 所有具有升级功能的app均可使用,耶稣说的




  • 更强大的自定义界面支持




  • 支持强制更新(一行代码)




  • 支持静默下载 (一行代码)




  • 适配到Android Q




导入

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


appcompat

  implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_appcompat'


jitpack && android x

dependencies {
implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_androidx'
}


使用

和1.+版本一样,两种模式


只使用下载模式


先来个最简单的调用

        AllenVersionChecker
.getInstance()
.downloadOnly(
UIData.create().setDownloadUrl(downloadUrl)
)
.executeMission(context);

UIData:UIData是一个Bundle,用于存放用于UI展示的一些数据,后面自定义界面时候可以拿来用


请求服务器版本+下载


该模式最简单的使用

   AllenVersionChecker
.getInstance()
.requestVersion()
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
//如果是最新版本直接return null
return UIData.create().setDownloadUrl(downloadUrl);
}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);

请求版本一些其他的http参数可以设置,如下

 AllenVersionChecker
.getInstance()
.requestVersion()
.setHttpHeaders(httpHeader)
.setRequestMethod(HttpRequestMethod.POSTJSON)
.setRequestParams(httpParam)
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
UIData uiData = UIData
.create()
.setDownloadUrl(downloadUrl)
.setTitle(updateTitle)
.setContent(updateContent);
//放一些其他的UI参数,拿到后面自定义界面使用
uiData.getVersionBundle().putString("key", "your value");
return uiData;

}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);


合适的地方关闭任务

为了避免不必要的内存泄漏,需要在合适的地方取消任务

AllenVersionChecker.getInstance().cancelAllMission();

以上就是最基本的使用(库默认会有一套界面),如果还不满足项目需求,下面就可以用这个库来飙车了


一些其他的function设置

解释下,下面的builder叫DownloadBuilder

 DownloadBuilder builder=AllenVersionChecker
.getInstance()
.downloadOnly();


or



DownloadBuilder builder=AllenVersionChecker
.getInstance()
.requestVersion()
.request()

取消任务


 AllenVersionChecker.getInstance().cancelAllMission(this);

静默下载


 builder.setSilentDownload(true); 默认false

设置当前服务器最新的版本号,供库判断是否使用缓存



  • 缓存策略:如果本地有安装包,首先判断与当前运行的程序的versionCode是否不一致,然后判断是否有传入最新的
    versionCode,如果传入的versionCode大于本地的,重新从服务器下载,否则使用缓存

 builder.setNewestVersionCode(int); 默认null

强制更新


设置此listener即代表需要强制更新,会在用户想要取消下载的时候回调
需要你自己关闭所有界面

builder.setForceUpdateListener(() -> {
forceUpdate();
});

update in v2.2.1
动态设置是否强制更新,如果使用本库来请求服务器,可以在回调时动态设置一些参数或者回调

   public UIData onRequestVersionSuccess(DownloadBuilder downloadBuilder,String result) {
downloadBuilder.setForceUpdateListener(() -> {
forceUpdate();
});
Toast.makeText(V2Activity.this, "request successful", Toast.LENGTH_SHORT).show();
return crateUIData();
}

下载忽略本地缓存


如果本地有安装包缓存也会重新下载apk

 builder.setForceRedownload(true); 默认false

是否显示下载对话框


builder.setShowDownloadingDialog(false); 默认true

是否显示通知栏


builder.setShowNotification(false);  默认true

以前台service运行(update in 2.2.2)
推荐以前台服务运行更新,防止在后台时,服务被杀死


builder.setRunOnForegroundService(true); 默认true

自定义通知栏


      builder.setNotificationBuilder(
NotificationBuilder.create()
.setRingtone(true)
.setIcon(R.mipmap.dialog4)
.setTicker("custom_ticker")
.setContentTitle("custom title")
.setContentText(getString(R.string.custom_content_text))
);

是否显示失败对话框


  builder.setShowDownloadFailDialog(false); 默认true

自定义下载路径


  builder.setDownloadAPKPath(address); 默认:/storage/emulated/0/AllenVersionPath/

自定义下载文件名


  builder.setApkName(apkName); 默认:getPackageName()

可以设置下载监听


   builder.setApkDownloadListener(new APKDownloadListener() {
@Override
public void onDownloading(int progress) {

}

@Override
public void onDownloadSuccess(File file) {

}

@Override
public void onDownloadFail() {

}
});

设置取消监听
此回调会监听所有cancel事件


 
builder.setOnCancelListener(() -> {
Toast.makeText(V2Activity.this,"Cancel Hanlde",Toast.LENGTH_SHORT).show();
});

如果想单独监听几种状态下的cancel,可像如下这样设置


  • builder.setDownloadingCancelListener();

  • builder.setDownloadFailedCancelListener();

  • builder.setReadyDownloadCancelListener();


设置确定监听(added after 2.2.2)



  • builder.setReadyDownloadCommitClickListener();

  • builder.setDownloadFailedCommitClickListener();


静默下载+直接安装(不会弹出升级对话框)


    builder.setDirectDownload(true);
builder.setShowNotification(false);
builder.setShowDownloadingDialog(false);
builder.setShowDownloadFailDialog(false);

自定义安装回调


    setCustomDownloadInstallListener(CustomInstallListener customDownloadInstallListener)


自定义界面

自定义界面使用回调方式,开发者需要返回自己定义的Dialog(父类android.app)



  • 所有自定义的界面必须使用listener里面的context实例化




  • 界面展示的数据通过UIData拿




自定义显示更新界面


设置CustomVersionDialogListener



  • 定义此界面必须有一个确定下载的按钮,按钮id必须为@id/versionchecklib_version_dialog_commit




  • 如果有取消按钮(没有忽略本条要求),则按钮id必须为@id/versionchecklib_version_dialog_cancel



eg.

  builder.setCustomVersionDialogListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_dialog_one_layout);
//versionBundle 就是UIData,之前开发者传入的,在这里可以拿出UI数据并展示
TextView textView = baseDialog.findViewById(R.id.tv_msg);
textView.setText(versionBundle.getContent());
return baseDialog;
});

自定义下载中对话框界面


设置CustomDownloadingDialogListener


  • 如果此界面要设计取消操作(没有忽略),请务必将id设置为@id/versionchecklib_loading_dialog_cancel

    builder.setCustomDownloadingDialogListener(new CustomDownloadingDialogListener() {
@Override
public Dialog getCustomDownloadingDialog(Context context, int progress, UIData versionBundle) {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_layout);
return baseDialog;
}
//下载中会不断回调updateUI方法
@Override
public void updateUI(Dialog dialog, int progress, UIData versionBundle) {
TextView tvProgress = dialog.findViewById(R.id.tv_progress);
ProgressBar progressBar = dialog.findViewById(R.id.pb);
progressBar.setProgress(progress);
tvProgress.setText(getString(R.string.versionchecklib_progress, progress));
}
});

自定义下载失败对话框


设置CustomDownloadFailedListener



  • 如果有重试按钮请将id设置为@id/versionchecklib_failed_dialog_retry




  • 如果有 确认/取消按钮请将id设置为@id/versionchecklib_failed_dialog_cancel



   builder.setCustomDownloadFailedListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_failed_dialog);
return baseDialog;
});


update Log


  • 2.2.1

    • 修复内存泄漏问题

    • 使用binder传递参数

    • 一些已知的bug




混淆配置

 -keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe ;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
(java.lang.Throwable);
}

git地址:https://github.com/AlexLiuSheng/CheckVersionLib

下载地址:CheckVersionLib-master.zip

收起阅读 »

优秀优秀,Android图片涂鸦库

DoodleImage doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible d...
继续阅读 »

Doodle


Image doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible doodle framework & multi-function drawing board.

Android图片涂鸦,具有撤消、缩放、移动、添加文字,贴图等功能。还是一个功能强大,可自定义和可扩展的涂鸦框架、多功能画板。

01.gif

01
02
03


Feature 特性



  • Brush and shape 画笔及形状


    The brush can choose hand-painted, mosaic, imitation, eraser, text, texture, and the imitation function is similar to that in PS, copying somewhere in the picture. Shapes can be selected from hand-drawn, arrows, lines, circles, rectangles, and so on. The background color of the brush can be selected as a color, or an image.


    画笔可以选择手绘、马赛克、仿制、橡皮擦、文字、贴图,其中仿制功能跟PS中的类似,复制图片中的某处地方。形状可以选择手绘、箭头、直线、圆、矩形等。画笔的底色可以选择颜色,或者一张图片。




  • Undo/Redo 撤销/重做


    Each step of the doodle operation can be undone or redone.


    每一步的涂鸦操作都可以撤销。




  • Zoom, move, and rotate 放缩、移动及旋转


    In the process of doodle, you can freely zoom, move and rotate the picture with gestures. Also, you can move,rotate and scale the doodle item.


    在涂鸦的过程中,可以自由地通过手势缩放、移动、旋转图片。可对涂鸦移动、旋转、缩放等。




  • Zoomer 放大器


    In order to doodle more finely, an zoomer can be set up during the doodle process.


    为了更细微地涂鸦,涂鸦过程中可以设置出现放大器。




Usage 用法


Gradle

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

dependencies {
compile 'com.github.1993hzw:Doodle:5.5.3'
}

There are two ways to use the Doodle library:

这里有两种方式使用Doodle涂鸦库


A. Launch DoodleActivity directly (the layout is like demo images above). If you need to customize more interactions, please use another method (Way B).

使用写好的涂鸦界面,直接启动.启动的页面可参看上面的演示图片。如果需要自定义更多的交互方式,则请使用另一种方式(即B方式)。

DoodleParams params = new DoodleParams(); // 涂鸦参数
params.mImagePath = imagePath; // the file path of image
DoodleActivity.startActivityForResult(MainActivity.this, params, REQ_CODE_DOODLE);

See DoodleParams for more details.

查看DoodleParams获取更多涂鸦参数信息。


B. Recommend, use DoodleView and customize your layout.

推荐的方法:使用DoodleView,便于拓展,灵活性高,自定义自己的交互界面.

/*
Whether or not to optimize drawing, it is suggested to open, which can optimize the drawing speed and performance.
Note: When item is selected for editing after opening, it will be drawn at the top level, and not at the corresponding level until editing is completed.
是否优化绘制,建议开启,可优化绘制速度和性能.
注意:开启后item被选中编辑时时会绘制在最上面一层,直到结束编辑后才绘制在相应层级
*/
boolean optimizeDrawing = true;
DoodleView mDoodleView = new DoodleView(this, bitmap, optimizeDrawing, new IDoodleListener() {
/*
called when save the doodled iamge.
保存涂鸦图像时调用
*/
@Override
public void onSaved(IDoodle doodle, Bitmap bitmap, Runnable callback) {
//do something
}

/*
called when it is ready to doodle because the view has been measured. Now, you can set size, color, pen, shape, etc.
此时view已经测量完成,涂鸦前的准备工作已经完成,在这里可以设置大小、颜色、画笔、形状等。
*/
@Override
public void onReady(IDoodle doodle) {
//do something
}
});

mTouchGestureListener = new DoodleOnTouchGestureListener(mDoodleView, new DoodleOnTouchGestureListener.ISelectionListener() {
/*
called when the item(such as text, texture) is selected/unselected.
item(如文字,贴图)被选中或取消选中时回调
*/
@Override
public void onSelectedItem(IDoodle doodle, IDoodleSelectableItem selectableItem, boolean selected) {
//do something
}

/*
called when you click the view to create a item(such as text, texture).
点击View中的某个点创建可选择的item(如文字,贴图)时回调
*/
@Override
public void onCreateSelectableItem(IDoodle doodle, float x, float y) {
//do something
/*
if (mDoodleView.getPen() == DoodlePen.TEXT) {
IDoodleSelectableItem item = new DoodleText(mDoodleView, "hello", 20 * mDoodleView.getUnitSize(), new DoodleColor(Color.RED), x, y);
mDoodleView.addItem(item);
} else if (mDoodleView.getPen() == DoodlePen.BITMAP) {
IDoodleSelectableItem item = new DoodleBitmap(mDoodleView, bitmap, 80 * mDoodle.getUnitSize(), x, y);
mDoodleView.addItem(item);
}
*/
}
});

// create touch detector, which dectects the gesture of scoll, scale, single tap, etc.
// 创建手势识别器,识别滚动,缩放,点击等手势
IDoodleTouchDetector detector = new DoodleTouchDetector(getApplicationContext(), mTouchGestureListener);
mDoodleView.setDefaultTouchDetector(detector);

// Setting parameters.设置参数
mDoodleView.setPen(DoodlePen.TEXT);
mDoodleView.setShape(DoodleShape.HAND_WRITE);
mDoodleView.setColor(new DoodleColor(Color.RED));

When turning off optimized drawing, you only need to call addItem(IDoodleItem) when you create it. When you start optimizing drawing, the created or selected item needs to call markItemToOptimizeDrawing(IDoodleItem), and you should call notifyItemFinishedDrawing(IDoodleItem) when you finish drawing. So this is generally used in code:

当关闭优化绘制时,只需要在创建时调用addItem(IDoodleItem);而当开启优化绘制时,创建或选中的item需要调用markItemToOptimizeDrawing(IDoodleItem),结束绘制时应调用notifyItemFinishedDrawing(IDoodleItem)。因此在代码中一般这样使用:

// when you are creating a item or selecting a item to edit
if (mDoodle.isOptimizeDrawing()) {
mDoodle.markItemToOptimizeDrawing(item);
} else {
mDoodle.addItem(item);
}

...

// finish creating or editting
if (mDoodle.isOptimizeDrawing()) {
mDoodle.notifyItemFinishedDrawing(item);
}

Then, add the DoodleView to your layout. Now you can start doodling freely.

把DoodleView添加到布局中,然后开始涂鸦。


Demo 实例

Here are other simple examples to teach you how to use the doodle framework.



  1. Mosaic effect
    马赛克效果




  2. Change text's size by scale gesture
    手势缩放文本大小



More...

Now I think you should know that DoodleActivity has used DoodleView. You also can customize your layout like DoodleActivity. See DoodleActivity for more details.

现在你应该知道DoodleActivity就是使用了DoodleView实现涂鸦,你可以参照DoodleActivity是怎么实现涂鸦界面的交互来实现自己的自定义页面。

DoodleView has implemented IDoodle.

DoodleView实现了IDoodle接口。

public interface IDoodle {
...
public float getUnitSize();
public void setDoodleRotation(int degree);
public void setDoodleScale(float scale, float pivotX, float pivotY);
public void setPen(IDoodlePen pen);
public void setShape(IDoodleShape shape);
public void setDoodleTranslation(float transX, float transY);
public void setSize(float paintSize);
public void setColor(IDoodleColor color);
public void addItem(IDoodleItem doodleItem);
public void removeItem(IDoodleItem doodleItem);
public void save();
public void topItem(IDoodleItem item);
public void bottomItem(IDoodleItem item);
public boolean undo(int step);
...
}


Framework diagram 框架图

structure


Doodle Coordinate 涂鸦坐标

coordinate


Extend 拓展

You can create a customized item like DoodlePath, DoodleText, DoodleBitmap which extend DoodleItemBase or implement IDoodleItem.

实现IDoodleItem接口或基础DoodleItemBase,用于创建自定义涂鸦条目item,比如DoodlePath, DoodleText, DoodleBitmap

You can create a customized pen like DoodlePen which implements IDoodlePen.

实现IDoodlePen接口用于创建自定义画笔pen,比如DoodlePen

You can create a customized shape like DoodleShape which implements IDoodleShape.

实现IDoodleShape接口用于创建自定义形状shape,比如DoodleShape

You can create a customized color like DoodleColor which implements IDoodleColor.

实现IDoodleColor接口用于创建自定义颜色color,比如DoodleColor

You can create a customized touch gesture detector like DoodleTouchDetector(GestureListener) which implements IDoodleTouchDetector.

实现IDoodleTouchDetector接口用于创建自定义手势识别器,比如DoodleTouchDetector


git地址:https://github.com/1993hzw/doodle

下载地址:doodle-master.zip

收起阅读 »

Swift - 第三方日历组件CVCalendar使用详解1(配置、基本用法)

CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。一、安装配置1. 从 GitHub 上下载最新的代码:https://g...
继续阅读 »

CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。

一、安装配置

1. 从 GitHub 上下载最新的代码:https://github.com/Mozharovsky/CVCalendar
2. 将下载下来的源码包中 CVCalendar.xcodeproj 拖拽至你的工程中 


3. 工程 -> General -> Embedded Binaries 项,把 iOS 版的 framework 添加进来:CVCalendar.framework


4. 最后,在需要使用 CVCalendar 的地方 import 进来就可以了

import CVCalendar

二、基本用法

1,月视图使用样例 

① 效果图
1. 初始化的时候自动显示当月日历,且“今天”的日期文字是红色的。
2. 顶部导航栏标题显示当前日历的年、月信息,日历左右滑动切换的时候,标题内容也会随之改变。
3. 点击导航栏右侧的“今天”按钮,日历又会跳回到当前日期。
4. 点击日历上的任一日期时间后,该日期背景色会变蓝色(如果是今天则变红色)。同时我们在日期选择响应中,将选择的日期弹出显示。

      

② 样例代码
日历组件分为:CVCalendarMenuView 和 CVCalendarView 两部分。前者是显示星期的菜单栏,后者是日期表格视图。这二者的位置和大小我们可以随意调整设置。
组件提供了许多代理协议让我进行样式调整或功能响应,我们可以选择使用。但其中 CVCalendarViewDelegate, CVCalendarMenuViewDelegate 这两个协议是必须的。

import UIKit
import CVCalendar

class ViewController: UIViewController {
//星期菜单栏
private var menuView: CVCalendarMenuView!

//日历主视图
private var calendarView: CVCalendarView!

var currentCalendar: Calendar!

override func viewDidLoad() {
super.viewDidLoad()

currentCalendar = Calendar.init(identifier: .gregorian)

//初始化的时候导航栏显示当年当月
self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

//初始化星期菜单栏
self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

//初始化日历主视图
self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
height:450))

//星期菜单栏代理
self.menuView.menuViewDelegate = self

//日历代理
self.calendarView.calendarDelegate = self

//将菜单视图和日历视图添加到主视图上
self.view.addSubview(menuView)
self.view.addSubview(calendarView)
}

//今天按钮点击
@IBAction func todayButtonTapped(_ sender: AnyObject) {
let today = Date()
self.calendarView.toggleViewWithDate(today)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

//更新日历frame
self.menuView.commitMenuViewUpdate()
self.calendarView.commitCalendarViewUpdate()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
//视图模式
func presentationMode() -> CalendarMode {
//使用月视图
return .monthView
}

//每周的第一天
func firstWeekday() -> Weekday {
//从星期一开始
return .monday
}

func presentedDateUpdated(_ date: CVDate) {
//导航栏显示当前日历的年月
self.title = date.globalDescription
}

//每个日期上面是否添加横线(连在一起就形成每行的分隔线)
func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
return true
}

//切换月的时候日历是否自动选择某一天(本月为今天,其它月为第一天)
func shouldAutoSelectDayOnMonthChange() -> Bool {
return false
}

//日期选择响应
func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
//获取日期
let date = dayView.date.convertedDate()!
// 创建一个日期格式器
let dformatter = DateFormatter()
dformatter.dateFormat = "yyyy年MM月dd日"
let message = "当前选择的日期是:\(dformatter.string(from: date))"
//将选择的日期弹出显示
let alertController = UIAlertController(title: "", message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}

2,周视图使用样例

同月视图模式相比,周视图日历区域只有一行(每次显示7天日期)。其它方面和月视图相比差别不大,也都是左右滑动切换显示下一周、下一周日期。


import UIKit
import CVCalendar

class ViewController: UIViewController {
//星期菜单栏
private var menuView: CVCalendarMenuView!

//日历主视图
private var calendarView: CVCalendarView!

var currentCalendar: Calendar!

override func viewDidLoad() {
super.viewDidLoad()

currentCalendar = Calendar.init(identifier: .gregorian)

//初始化的时候导航栏显示当年当月
self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

//初始化星期菜单栏
self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

//初始化日历主视图
self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
height:50))

//星期菜单栏代理
self.menuView.menuViewDelegate = self

//日历代理
self.calendarView.calendarDelegate = self

//将菜单视图和日历视图添加到主视图上
self.view.addSubview(menuView)
self.view.addSubview(calendarView)
}

//今天按钮点击
@IBAction func todayButtonTapped(_ sender: AnyObject) {
let today = Date()
self.calendarView.toggleViewWithDate(today)
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

//更新日历frame
self.menuView.commitMenuViewUpdate()
self.calendarView.commitCalendarViewUpdate()
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
//视图模式
func presentationMode() -> CalendarMode {
//使用周视图
return .weekView
}

//每周的第一天
func firstWeekday() -> Weekday {
//从星期一开始
return .monday
}

func presentedDateUpdated(_ date: CVDate) {
//导航栏显示当前日历的年月
self.title = date.globalDescription
}

//每个日期上面是否添加横线(连在一起就形成每行的分隔线)
func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
return true
}

//切换周的时候日历是否自动选择某一天(本周为今天,其它周为第一天)
func shouldAutoSelectDayOnWeekChange() -> Bool {
return false
}

//日期选择响应
func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
//获取日期
let date = dayView.date.convertedDate(calendar: currentCalendar)!
// 创建一个日期格式器
let dformatter = DateFormatter()
dformatter.dateFormat = "yyyy年MM月dd日"
let message = "当前选择的日期是:\(dformatter.string(from: date))"
//将选择的日期弹出显示
let alertController = UIAlertController(title: "", message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}

转自:https://www.hangge.com/blog/cache/detail_1504.html#

收起阅读 »

LeakCanary原理分析

LeakCanary 是一个很好用的Android内存泄露检测工具,今天从源码角度分析下其检测内存泄露的原理,不同版本 源码 会有一定差异,这里参考的是2.7版本。1. Reference简介Java中的四种引用类型,我们先简单复习下强引用,对象有强引用时不能...
继续阅读 »

LeakCanary 是一个很好用的Android内存泄露检测工具,今天从源码角度分析下其检测内存泄露的原理,不同版本 源码 会有一定差异,这里参考的是2.7版本。

1. Reference简介

Java中的四种引用类型,我们先简单复习下

  • 强引用,对象有强引用时不能被回收
  • 软引用 SoftReference,对象只有软引用时,在内存不足时触发GC会回收该对象
  • 弱引用 WeakReference,对象只有弱引用时,下次GC就会回收该对象
  • 虚引用 PhantomReference,平常很少会用到,源码注释主要用来监听对象清理前的动作,比Java finalization更灵活,PhantomReference需要与 ReferenceQueue 一起配合使用。

Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.

ReferenceQueue

上面提到PhantomReferenceReferenceQueue配合监听对象被回收,实际上WeakReferenceSoftReference同样可以与ReferenceQueue关联使用,只要构造方法传入ReferenceQueue参数即可。在引用所指的对象被回收后,引用本身将会被加入到ReferenceQueue之中。

2. LeakCanary使用简介

  • 在app的build.gradle中加入依赖
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
  • LeakCanary会自动监控Activity、Fragment、Fragment View、RootView、Service的泄露
  • 如果需要监控其它对象的泄露,可以手动添加如下代码
AppWatcher.objectWatcher.watch(myView, "View was detached")

3. LeakCanary源码分析

初始化

LeakCanary新版本使用ContentProvider自动初始化,不需要再手动调用install方法


    android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
android:authorities="${applicationId}.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false" />

如果想禁用自动初始化,在app res中加入

false

接下来我们从源码分析下LeakCanary初始化的流程:

internal sealed class AppWatcherInstaller : ContentProvider() {

/**
* [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
*/
internal class MainProcess : AppWatcherInstaller()

/**
* When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
* [LeakCanaryProcess] automatically sets up the LeakCanary code
*/
internal class LeakCanaryProcess : AppWatcherInstaller()

override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}

...
}

AppWatcherInstaller继承ContentProvider,onCreate会调用AppWatcher的manualInstall方法,完成自动初始化。

object AppWatcher {

/**
* The [ObjectWatcher] used by AppWatcher to detect retained objects.
* Only set when [isInstalled] is true.
*/
val objectWatcher = ObjectWatcher(
clock = { SystemClock.uptimeMillis() },
checkRetainedExecutor = {
check(isInstalled) {
"AppWatcher not installed"
}
mainHandler.postDelayed(it, retainedDelayMillis)
},
isEnabled = { true }
)

@JvmOverloads
fun manualInstall(
application: Application,
retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
watchersToInstall: List = appDefaultWatchers(application)
) {
checkMainThread()
if (isInstalled) {
throw IllegalStateException(
"AppWatcher already installed, see exception cause for prior install call", installCause
)
}
check(retainedDelayMillis >= 0) {
"retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
}
installCause = RuntimeException("manualInstall() first called here")
this.retainedDelayMillis = retainedDelayMillis
if (application.isDebuggableBuild) {
LogcatSharkLog.install()
}
// Requires AppWatcher.objectWatcher to be set
LeakCanaryDelegate.loadLeakCanary(application)

watchersToInstall.forEach {
it.install()
}
}

fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}
...
}

manualInstall方法有3个参数:

  • application:application对象
  • retainedDelayMillis:默认值5s,表示5s后检测对象是否被回收
  • watchersToInstall:安装的监控器,每个监控器抽象成InstallableWatcher,默认值在appDefaultWatchers方法中定义,包括ActivityWatcher、FragmentAndViewModelWatcher、RootViewWatcher、ServiceWatcher,后面单独分析。

创建InstallableWatcher时需要传入ReachabilityWatcher,实现类是ObjectWatcher,这是监控对象可达性的核心。AppWatcher创建了ObjectWatcher对象,并在checkRetainedExecutor里面加入了延迟5s的逻辑,下面配合ObjectWatcher源码一起分析。

ObjectWatcher

上面说到手动监控其它对象是调用ObjectWatcher的watch方法,这里是真正的核心逻辑,我们看下其部分代码

class ObjectWatcher constructor(
private val clock: Clock,
private val checkRetainedExecutor: Executor,
/**
* Calls to [watch] will be ignored when [isEnabled] returns false
*/
private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {

fun watch(
watchedObject: Any,
description: String
) {
expectWeaklyReachable(watchedObject, description)
}

@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}

watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}

@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}

private fun removeWeaklyReachableObjects() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}

}

调用watch方法会走到expectWeaklyReachable,里面大致做了4件事情:

  • 从watchedObjects中移除已经正常释放的引用 removeWeaklyReachableObjects()。这里用到了前面讲的ReferenceQueue,对象被回收会加入到queue中,将queu中存在的从watchedObjects中移除。
  • 创建KeyedWeakReference引用,KeyedWeakReference继承WeakReference,创建时传入了ReferenceQueue,监听对象的回收。
  • 将引用加入到watchedObjects中 watchedObjects[key] = reference
  • 延迟5s之后执行moveToRetained(key),确认对象是否回收(延迟逻辑在AppWatcher传入的checkRetainedExecutor中实现)。moveToRetained中首先执行removeWeaklyReachableObjects,之后再判断watchedObjects中是否还存在此key,若存在说明对象未被回收,发生内存泄露。

到这里我们基本明白了LeakCanary监控对象是否回收的逻辑,接下来我们再看看他是如何自动监控Activity、Fragment等组件的。前面讲到AppWatcher初始化时,会自动创建ActivityWatcher、FragmentAndViewModelWatcher、RootViewWatcher、ServiceWatcher,我们阅读下他们的源码。

ActivityWatcher

class ActivityWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}

override fun install() {
application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}

override fun uninstall() {
application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
}
}

ActivityWatcher的代码非常简单,注册Activity生命周期回调,在onActivityDestroyed中调用ObjectWatcher的expectWeaklyReachable,监控activity对象5s之内是否被释放。

FragmentAndViewModelWatcher

FragmentAndViewModelWatcher监控Fragment和Fragment View的泄露,原理与Activity类似,在onFragmentDestroyed和onFragmentViewDestroyed中调用ObjectWatcher的expectWeaklyReachable方法。只不过监听Fragment的onDestroy相对复杂点,原理是先监听Activity生命周期,然后在Activity onCreate时通过fragmentManager.registerFragmentLifecycleCallbacks注册Fragment生命周期回调。而且同时兼容了android.app.Fragment、androidx.fragment.app.Fragment、android.support.v4.app.Fragment。

class FragmentAndViewModelWatcher(
private val application: Application,
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val fragmentDestroyWatchers: List<(Activity) -> Unit> = run {
val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()

if (SDK_INT >= O) {
fragmentDestroyWatchers.add(
AndroidOFragmentDestroyWatcher(reachabilityWatcher)
)
}

getWatcherIfAvailable(
ANDROIDX_FRAGMENT_CLASS_NAME,
ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}

getWatcherIfAvailable(
ANDROID_SUPPORT_FRAGMENT_CLASS_NAME,
ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,
reachabilityWatcher
)?.let {
fragmentDestroyWatchers.add(it)
}
fragmentDestroyWatchers
}
}

@SuppressLint("NewApi")
internal class AndroidOFragmentDestroyWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {
private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
val view = fragment.view
if (view != null) {
reachabilityWatcher.expectWeaklyReachable(
view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
"(references to its views should be cleared to prevent leaks)"
)
}
}

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
reachabilityWatcher.expectWeaklyReachable(
fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
)
}
}

override fun invoke(activity: Activity) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}

RootViewWatcher

RootViewWatcher监控RootView的泄露,在rootView onDetachedFromWindow回调时,调用ObjectWatcher的expectWeaklyReachable方法。rootView的onDetachedFromWindow回调监听是通过Square开源的Curtains库实现。

class RootViewWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

private val listener = OnRootViewAddedListener { rootView ->
val trackDetached = when(rootView.windowType) {
PHONE_WINDOW -> {
when (rootView.phoneWindow?.callback?.wrappedCallback) {
// Activities are already tracked by ActivityWatcher
is Activity -> false
is Dialog -> rootView.resources.getBoolean(R.bool.leak_canary_watcher_watch_dismissed_dialogs)
// Probably a DreamService
else -> true
}
}
// Android widgets keep detached popup window instances around.
POPUP_WINDOW -> false
TOOLTIP, TOAST, UNKNOWN -> true
}
if (trackDetached) {
rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {

val watchDetachedView = Runnable {
reachabilityWatcher.expectWeaklyReachable(
rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
)
}

override fun onViewAttachedToWindow(v: View) {
mainHandler.removeCallbacks(watchDetachedView)
}

override fun onViewDetachedFromWindow(v: View) {
mainHandler.post(watchDetachedView)
}
})
}
}

override fun install() {
Curtains.onRootViewsChangedListeners += listener
}

override fun uninstall() {
Curtains.onRootViewsChangedListeners -= listener
}
}

ServiceWatcher

ServiceWatcher监控Service的泄露,原理也是在Service的onDestroy时调用ObjectWatcher的expectWeaklyReachable方法。Service的onDestroy是通过反射和动态代理ActivityManager和ActivityThread,代码比较巧妙,可以仔细消化下。

@SuppressLint("PrivateApi")
class ServiceWatcher(private val reachabilityWatcher: ReachabilityWatcher) : InstallableWatcher {

private val servicesToBeDestroyed = WeakHashMap>()

private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") }

private val activityThreadInstance by lazy {
activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null)!!
}

private val activityThreadServices by lazy {
val mServicesField =
activityThreadClass.getDeclaredField("mServices").apply { isAccessible = true }

@Suppress("UNCHECKED_CAST")
mServicesField[activityThreadInstance] as Map
}

private var uninstallActivityThreadHandlerCallback: (() -> Unit)? = null
private var uninstallActivityManager: (() -> Unit)? = null

override fun install() {
checkMainThread()
check(uninstallActivityThreadHandlerCallback == null) {
"ServiceWatcher already installed"
}
check(uninstallActivityManager == null) {
"ServiceWatcher already installed"
}
try {
swapActivityThreadHandlerCallback { mCallback ->
uninstallActivityThreadHandlerCallback = {
swapActivityThreadHandlerCallback {
mCallback
}
}
Handler.Callback { msg ->
if (msg.what == STOP_SERVICE) {
val key = msg.obj as IBinder
activityThreadServices[key]?.let {
onServicePreDestroy(key, it)
}
}
mCallback?.handleMessage(msg) ?: false
}
}
swapActivityManager { activityManagerInterface, activityManagerInstance ->
uninstallActivityManager = {
swapActivityManager { _, _ ->
activityManagerInstance
}
}
Proxy.newProxyInstance(
activityManagerInterface.classLoader, arrayOf(activityManagerInterface)
) { _, method, args ->
if (METHOD_SERVICE_DONE_EXECUTING == method.name) {
val token = args!![0] as IBinder
if (servicesToBeDestroyed.containsKey(token)) {
onServiceDestroyed(token)
}
}
try {
if (args == null) {
method.invoke(activityManagerInstance)
} else {
method.invoke(activityManagerInstance, *args)
}
} catch (invocationException: InvocationTargetException) {
throw invocationException.targetException
}
}
}
} catch (ignored: Throwable) {
SharkLog.d(ignored) { "Could not watch destroyed services" }
}
}

override fun uninstall() {
checkMainThread()
uninstallActivityManager?.invoke()
uninstallActivityThreadHandlerCallback?.invoke()
uninstallActivityManager = null
uninstallActivityThreadHandlerCallback = null
}

private fun onServicePreDestroy(
token: IBinder,
service: Service
) {
servicesToBeDestroyed[token] = WeakReference(service)
}

private fun onServiceDestroyed(token: IBinder) {
servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
serviceWeakReference.get()?.let { service ->
reachabilityWatcher.expectWeaklyReachable(
service, "${service::class.java.name} received Service#onDestroy() callback"
)
}
}
}

private fun swapActivityThreadHandlerCallback(swap: (Handler.Callback?) -> Handler.Callback?) {
val mHField =
activityThreadClass.getDeclaredField("mH").apply { isAccessible = true }
val mH = mHField[activityThreadInstance] as Handler

val mCallbackField =
Handler::class.java.getDeclaredField("mCallback").apply { isAccessible = true }
val mCallback = mCallbackField[mH] as Handler.Callback?
mCallbackField[mH] = swap(mCallback)
}

@SuppressLint("PrivateApi")
private fun swapActivityManager(swap: (Class<*>, Any) -> Any) {
val singletonClass = Class.forName("android.util.Singleton")
val mInstanceField =
singletonClass.getDeclaredField("mInstance").apply { isAccessible = true }

val singletonGetMethod = singletonClass.getDeclaredMethod("get")

val (className, fieldName) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
"android.app.ActivityManager" to "IActivityManagerSingleton"
} else {
"android.app.ActivityManagerNative" to "gDefault"
}

val activityManagerClass = Class.forName(className)
val activityManagerSingletonField =
activityManagerClass.getDeclaredField(fieldName).apply { isAccessible = true }
val activityManagerSingletonInstance = activityManagerSingletonField[activityManagerClass]

// Calling get() instead of reading from the field directly to ensure the singleton is
// created.
val activityManagerInstance = singletonGetMethod.invoke(activityManagerSingletonInstance)

val iActivityManagerInterface = Class.forName("android.app.IActivityManager")
mInstanceField[activityManagerSingletonInstance] =
swap(iActivityManagerInterface, activityManagerInstance!!)
}

companion object {
private const val STOP_SERVICE = 116

private const val METHOD_SERVICE_DONE_EXECUTING = "serviceDoneExecuting"
}
}

总结

今天从源码角度分析了LeakCanary监控内存泄露的核心原理。除此之外,LeakCanary还可以导出、分析、分类堆栈,如果后面有时间咱们再单独讲吧。

收起阅读 »

Android即时通讯系列文章(1)多进程:为什么要把消息服务拆分到一个独立的进程?

这是即时通讯系列文章的第一篇,正式开始对IM开发技术的讲解之前,我们先来谈谈客户端在完整聊天系统中所扮演的角色,为此,我们必须先明确客户端的职责。现今主流的IM应用几乎都是采用服务器中转的方式来进行消息传输的,为的是更好地支持离线、群组等业务。在这种模式下,所...
继续阅读 »

这是即时通讯系列文章的第一篇,正式开始对IM开发技术的讲解之前,我们先来谈谈客户端在完整聊天系统中所扮演的角色,为此,我们必须先明确客户端的职责。

现今主流的IM应用几乎都是采用服务器中转的方式来进行消息传输的,为的是更好地支持离线、群组等业务。在这种模式下,所有客户端都需连接到服务端,服务端将不同客户端发给自己的消息根据消息里携带的用户标识进行转发或广播。

因此,作为消息收发的终端设备,客户端的重要职责之一就是保持与服务端的连接,该连接的稳定性直接决定消息收发的实时性和可靠性。而在上篇文章我们讲过,移动设备是资源受限的,这对连接的稳定性提出了极大的挑战,具体可体现在以下两个方面:

  • 为了维持多任务环境的正常运行,Android为每个应用的堆大小设置了硬性上限,不同设备的确切堆大小取决于设备的总体可用RAM大小,如果应用在达到堆容量上限后尝试分配更多内容,则可能引发OOM。
  • 当用户切换到其他应用时,系统会将原有应用的进程保留在缓存中,稍后如果用户返回该应用,系统就会重复使用该进程,以便加快应用切换速度。但当系统资源(如内存)不足时,系统会考虑终止占用最多内存的、优先级较低的进程以释放RAM。

虽然ART和Dalvik虚拟机会例行执行垃圾回收任务,但如果应用存在内存泄漏问题,并且只有一个主进程,势必会随着应用使用时间的延长而逐步增大内存使用量,从而增加引发OOM的概率和缓存进程被系统终止的风险。

因此,为了保证连接的稳定性,可考虑将负责连接保持工作的消息服务放入一个独立的进程中,分离之后即使主进程退出、崩溃或者出现内存消耗过高等情况,该服务仍可正常运行,甚至可以在适当的时机通过广播等方式重新唤起主进程。

但是,给应用划分进程,往往就意味着需要编写额外的进程通讯代码,特别是对于消息服务这种需要高度交互的场景。而由于各个进程都运行在相对独立的内存空间,因而是无法直接通讯的。为此,Android提供了AIDL(Android Interface Definition Language,Android接口定义语言)用于实现进程间通信,其本质就是实现对象的序列化、传输、接收和反序列化,得到可操作的对象后再进行常规的方法调用。

接下来,就让我们来一步步实现跨进程的通讯吧。

Step1 创建服务

由于连接保持的工作是需要在后台执行长时间执行的操作,通常不提供操作界面,符合这个特性的组件就是Service了,因此我们选用Service作为与远程进程进行进程间通信(IPC)的组件。创建Service的子类时,必须实现onBind回调方法,此处我们暂时返回空实现。

class MessageAccessService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
}

另外使用Service还有一个好处就是,我们可以在适当的时机将其升级为前台服务,前台服务是用户主动意识到的一种服务,进程优先级较高,因此在内存不足时,系统也不会考虑将其终止。

使用前台服务唯一的缺点就是必须在抽屉式通知栏提供一条不可移除的通知,对于用户体验极不友好,但是我们可以通过定制通知样式进行协调,后续的文章中会讲到。

step2 指定进程

默认情况下,同一应用的所有组件均在相同的进程中运行。如需控制某个组件所属的进程,可通过在清单文件中设置android:process属性实现:

<manifest ...>
<application ...>
<service
android:name=".service.MessageAccessService"
android:exported="true"
android:process=":remote" />
</application>
</manifest>

另外,为使其他进程的组件能调用服务或与之交互,还需设置android:exported属性为true。

step3 创建.aidl 文件

让我们重新把目光放回onBind回调方法,该方法要求返回IBinder对象,客户端可使用该对象定义好的接口与服务进行通信。IBinder是远程对象的基础接口,该接口描述了与远程对象交互的抽象协议,但不建议直接实现此接口,而应从Binder扩展。通常做法是是使用.aidl文件来描述所需的接口,使其生成适当的Binder子类。

那么,这个最关键的.aidl文件该如何创建,又该定义哪些接口呢?

创建.aidl文件很简单,Android Studio本身就提供了创建AIDL文件方法:项目右键 -> New -> AIDL -> AIDL File

前面讲过,客户端是消息收发的终端设备,而接入服务则是为客户端提供了消息收发的出入口。客户端发出的消息经由接入服务发送到服务端,同时客户端会委托接入服务帮忙收取消息,当服务端有消息推送过来时通知自己。

如此一来便很清晰了,我们要定义的接口总共有三个,分别为:

  • 发送消息
  • 注册消息接收器
  • 反注册消息接收器

MessageCarrier.aidl

package com.xxx.imsdk.comp.remote;
import com.xxx.imsdk.comp.remote.bean.Envelope;
import com.xxx.imsdk.comp.remote.listener.MessageReceiver;

interface MessageCarrier {
void sendMessage(in Envelope envelope);
void registerReceiveListener(MessageReceiver messageReceiver);
void unregisterReceiveListener(MessageReceiver messageReceiver);
}

这里解释一下上述接口中携带的参数的含义:

Envelope ->

解释这个参数之前,得先介绍Envelope.java这个类,该类是多进程通讯中作为数据传输的实体类。AIDL支持的数据类型除了基本数据类型、String和CharSequence,还有就是实现了Parcelable接口的对象,以及其中元素为以上几种的List和Map。

Envelope.java

**
* 用于多进程通讯的信封类
* <p>
* 在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类;
* 但实际业务中需要传递的对象所属的类往往分散在不同的模块,所以通过构建一个包装类来包含真正需要被传递的对象(必须也实现Parcelable接口)
*/
@Parcelize
data class Envelope(val messageVo: MessageVo? = null,
val noticeVo: NoticeVo? = null) : Parcelable {
}

另外,在AIDL中传递的对象,需要在上述类文件的相同包路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类,Envelope.aidl就是对应Envelope.java而创建的;

Envelope.aidl

package com.xxx.imsdk.comp.remote.bean;

parcelable Envelope;

两个文件对应的路径比较如下:

clipboard.png

那为什么是Envelope类而不直接是MessageVO类(消息视图对象)呢?这是由于考虑到实际业务中需要传递的对象所属的类往往分散在不同的模块(MessageVO从属于另外一个模块,需要被其他模块引用),所以通过构建一个包装类来包含真正需要被传递的对象(该对象必须也实现Parcelable接口),这也是该类命名为Envelope(信封)的含义。

MessageReceiver ->

跨进程的消息收取回调接口,用于将消息接入服务收取到的服务端消息传递到客户端。但这里使用的回调接口有点不一样,在AIDL中传递的接口,不能是普通的接口,只能是AIDL接口,因此我们还需要新建多一个.aidl文件:

MessageReceiver.aidl

package com.xxx.imsdk.comp.remote.listener;
import com.xxx.imsdk.comp.remote.bean.Envelope;

interface MessageReceiver {
void onMessageReceived(in Envelope envelope);
}

包目录结构如下图:

FE55B9D0FFFC48829667C01C212B2668.jpg

step4 返回IBinder接口

构建应用时,Android SDK会生成基于.aidl 文件的IBinder接口文件,并将其保存到项目的gen/目录中。生成文件的名称与.aidl 文件的名称保持一致,区别在于其使用.java 扩展名(例如,MessageCarrier.aidl 生成的文件名是 MessageCarrier .java)。此接口拥有一个名为Stub的内部抽象类,用于扩展 Binder 类并实现 AIDL 接口中的方法。

/** 根据MessageCarrier.aidl文件自动生成的Binder对象,需要返回给客户端 */
private val messageCarrier: IBinder = object : MessageCarrier.Stub() {

override fun sendMessage(envelope: Envelope?) {

}

override fun registerReceiveListener(messageReceiver: MessageReceiver?) {
remoteCallbackList.register(messageReceiver)
}

override fun unregisterReceiveListener(messageReceiver: MessageReceiver?) {
remoteCallbackList.unregister(messageReceiver)
}

}

override fun onBind(intent: Intent?): IBinder? {
return messageCarrier
}
step5 绑定服务

组件(例如 Activity)可以通过调用bindService方法绑定到服务,该方法必须提供ServiceConnection 的实现以监控与服务的连接。当组件与服务之间的连接建立成功后, ServiceConnection上的 onServiceConnected()方法将被回调,该方法包含上一步返回的IBinder对象,随后便可使用该对象与绑定的服务进行通信。

/**
* ## 绑定消息接入服务
* 同时调用bindService和startService, 可以使unbind后Service仍保持运行
* @param context 上下文
*/
@Synchronized
fun setupService(context: Context? = null) {
if (!::appContext.isInitialized) {
appContext = context!!.applicationContext
}

val intent = Intent(appContext, MessageAccessService::class.java)

// 记录绑定服务的结果,避免解绑服务时出错
if (!isBound) {
isBound = appContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}

startService(intent)
}

/** 监听与服务连接状态的接口 */
private val serviceConnection = object : ServiceConnection {

override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// 取得MessageCarrier.aidl对应的操作接口
messageCarrier = MessageCarrier.Stub.asInterface(service)
...
}

override fun onServiceDisconnected(name: ComponentName?) {
}

}

可以同时将多个组件绑定到同一个服务,但当最后一个组件取消与服务的绑定时,系统会销毁该服务。为了使服务能够无限期运行,可同时调用startService()和bindService(),创建同时具有已启动和已绑定两种状态的服务。这样,即使所有组件均解绑服务,系统也不会销毁该服务,直至调用 stopSelf() 或 stopService() 才会显式停止该服务。

/**
* 启动消息接入服务
* @param intent 意图
* @param action 操作
*/
private fun startService(
intent: Intent = Intent(appContext, MessageAccessService::class.java),
action: String? = null
) {
// Android8.0不再允许后台service直接通过startService方式去启动,将引发IllegalStateException
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& !ProcessUtil.isForeground(appContext)
) {
if (!TextUtils.isEmpty(action)) intent.action = action
intent.putExtra(KeepForegroundService.EXTRA_ABANDON_FOREGROUND, false)
appContext.startForegroundService(intent)
} else {
appContext.startService(intent)
}
}

/**
* 停止消息接入服务
*/
fun stopService() {
// 立即清除缓存的WebSocket服务器地址,防止登录时再次使用旧的WebSocket服务器地址(带的会话已失效),导致收到用户下线的通知
GlobalScope.launch {
DataStoreUtil.writeString(appContext, RemoteDataStoreKey.WEB_SOCKET_SERVER_URL, "")
}

unbindService()

appContext.stopService(Intent(appContext, MessageAccessService::class.java))
}

/**
* 解绑消息接入服务
*/
@Synchronized
fun unbindService() {
if (!isBound) return // 必须判断服务是否已解除绑定,否则会报java.lang.IllegalArgumentException: Service not registered

// 解除消息监听接口
if (messageCarrier?.asBinder()?.isBinderAlive == true) {
messageCarrier?.unregisterReceiveListener(messageReceiver)
messageCarrier = null
}

appContext.unbindService(serviceConnection)

isBound = false
}

总结

通过以上代码的实践,最终我们得以将应用拆分为主进程和远程进程。主进程主要负责用户交互、界面展示,而远程进程则主要负责消息收发、连接保持等。由于远程进程仅保持了最小限度的业务逻辑处理,内存增长相对稳定,因此会大大降低系统内存紧张时远端进程被终止的概率,即使主进程因为意外情况退出了,远程进程仍可保持运行,从而保证连接的稳定性。

收起阅读 »

Jetpack太香了,系统App也想用,怎么办?

第三方App使用Jetpack等开源框架非常流行,在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发,与第三方App脱节严重,采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架,开发效率则会大大提高。前言...
继续阅读 »

第三方App使用Jetpack等开源框架非常流行,在Gradle文件简单指定即可。然而ROM内置的系统App在源码环境下进行开发,与第三方App脱节严重,采用开源框架的情况并不常见。但如果系统App也集成了Jetpack或第三方框架,开发效率则会大大提高。

前言

系统App开发者,很少采用Jetpack 以及第三方框架的原因主要有几点:

  1. 导入麻烦:有的框架过于庞大,可能依赖的库比较多,编译文件的构建比较繁琐,没有gradle那么智能

  2. 功能单一:系统App注重功能性,业务逻辑较少,依赖庞大库文件的场景不多

  3. license风险:引用第三方框架的话,需要特别声明license ,会尽量避免采用

但对于功能复杂,架构庞大的系统App而言,集成第三方框架显得尤为必要。比如Android系统里最核心的App SystemUI,就采用了知名的DI框架Dagger2 。Dagger2的引入使得功能庞杂的SystemUI管理各个依赖模块变得游刃有余。

SystemUI将Dagger2集成的方式给了我启发,探索和总结了Android 源码中如何配置Jetpack 以及第三方库,希望能够帮到大家。

源码编译说明

与Gradle不同,源码环境里的编译构建都是配置在.mk或者.bp文件里的,配置起来较为繁琐。

.bp文件::Android.bp是用来替换Android.mk的配置文件,它使用Blueprint框架来解析。Blueprint是生成、解析Android.bp的工具,是Soong的一部分。Soong则是专为Android编译而设计的工具,Blueprint只是解析文件的形式,而Soong则解释内容的含义,最终转换成Ninja文件。下文bp 就是指.bp的文件

**注意:**以下基于Android 11上进行的演示,Android 10及之前部分Jetpack框架没有集成进源码,需留意

gradle切换到bp

gradle和bp的对比

看一个使用aar和注解库的例子。

看一个AndroidStudio(以下简称AS)下build.gradle 文件里包的导入代码:

dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
//room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}

ROM环境里的编译依赖.bp 配置如下:

android_app {
......
static_libs: [
"androidx.appcompat_appcompat",
"com.google.android.material_material",
"androidx-constraintlayout_constraintlayout",
"androidx.room_room-runtime",
],
plugins: ["androidx.room_room-compiler-plugin"],
......
}

导入关键字的差异

依赖文件里的导入关键字:

在AS和AOSP里面导入包的关键字有些差异,又分为两种情况。

build.gradle.bp
代码库implementationstatic_libs
注解使用的库annotationProcessorplugins

引入库文件(libs):比较常见。引入的方式有多种。下文会讲具体的几种方式。

引入注解库:比较流行,源码中使用比较繁琐,下文会重点讲解。

库文件的导入规则

眼尖的同学已经看出规律了

如:implementation 'androidx.appcompat:appcompat:1.2.0'

bp 文件中:androidx.appcompat_appcompat,将“:” 改为 “”即可,不需要加版本号。其实就是group 与 name 中间用“”连接,基本上符合上述规则,当然也有特殊

注解库的导入规则

如今框架流行注解编程。

gradle 配置:annotationProcessor "androidx.room:room-compiler:$room_version"

bp 中就需要使用到plugins,对应配置plugins: ["androidx.room_room-compiler-plugin"]

根据jar 包的规则,那plugin 命名应该是“:” 改为 ”_" version+"-plugin" 。

SystemUI 使用Dagger2配置 plugins: ["dagger2-compiler-2.19"],所以命名规则并不是上文猜测的那样。

那如何确定Jetpack框架的名称呢?

确定Jetpack框架的名称

源码编译,所有的内容和都在源码中,都需要在源码环境中寻找。

以Room 为例

在prebuilts/sdk/current/androidx/Android.bp 配置了引入jar包 中有如下配置

android_library {
name: "androidx.room_room-runtime",//名称
......
manifest: "manifests/androidx.room_room-runtime/AndroidManifest.xml",//配置manifast
static_libs: [//两个room库文件,三个依赖的库文件
"androidx.room_room-runtime-nodeps",
"androidx.room_room-common",
"androidx.sqlite_sqlite-framework",
"androidx.sqlite_sqlite",
"androidx.arch.core_core-runtime",
],
}

插件配置在prebuilts/sdk/current/androidx/JavaPlugins.bp

java_plugin {
name: "androidx.room_room-compiler-plugin",//名称
static_libs: [//1个room库文件,1个依赖的库文件
"androidx.room_room-compiler",
"kotlin-reflect"
],
processor_class: "androidx.room.RoomProcessor",//需要指定处理的类
}

注意:AS 开发 并不需要配置 “processor_class”,我反编译了room-compiler,找到了RoomProcessor.java.(AS 为什么不需要指定,我这里我就不研究了)

看下图,META-INF/services/javax.annotation.processing.Processor 文件中配置了RoomProcessor.java(就按照这个文件配置就可以了)

2rVokj.png

如何确定源码中哪些jetpack 库可以使用呢?

在Android.bp 中搜索,或者看androidx目录下包含了什么

prebuilts/sdk/current/androidx/m2repository/androidx$ ls

导入第三方的开源框架

以上讲的是引入Jetpack相关jar包,其他常见的是否包含呢?如Glide,它是不属于androidx 的

第三方库,Android 源码中整理就不算好了,使用比较乱。下面我梳理下

导入下载的jar包

大家最常用的,把 jar 包 放到 libs,就可以了(当然,比较简单,与其他库关联较少可以采用此种方式)

java_import {
name: "disklrucache-jar",
jars: ["disklrucache-4.12.0.jar"],
sdk_version: "current",
}
android_library_import {
name: "glide-4.12.0",
aars: ["glide-4.12.0.aar"],
sdk_version: "current",
}
android_library_import {
name: "gifdecoder-4.12.0",
aars: ["gifdecoder-4.12.0.aar",],
sdk_version: "current",
}
android_library_import {
name: "exifinterface-1.2.0",
aars: ["exifinterface-1.2.0.aar",],
sdk_version: "current",
}
android_app {
......
static_libs: [
"disklrucache-jar",
"glide-4.12.0",
"gifdecoder-4.12.0",
"exifinterface-1.2.0"
],
}

导入AOSP内置的jar包

常用第三方放在了prebuilts/tools/common/m2/repository/下面包含了很多库文件,如Glide,Okhttp,但比较尴尬的是,.bp文件并没有写好。应用需要自己编写,编写方式可以参考上文。

以后google应该会把 external 下 的整合到这个里面,可以关注下prebuilts/tools/common/m2/repository 中Android.bp文件的变化。

如:prebuilts/maven_repo/bumptech/Android.bp

java_import {
name: "glide-prebuilt",
jars: [
"com/github/bumptech/glide/glide/4.8.0/glide-4.8.0.jar",
"com/github/bumptech/glide/disklrucache/4.8.0/disklrucache-4.8.0.jar",
"com/github/bumptech/glide/gifdecoder/4.8.0/gifdecoder-4.8.0.jar",
],
jetifier: true,
notice: "LICENSE",
}

Android.bp 直接用"glide"了

static_libs: [
"glide-prebuilt"
],

导入jar包源码

external 下面 很多第三方库的源码,如Glide的源码,目录为external/glide/

android_library {
name: "glide",
srcs: [
"library/src/**/*.java",
"third_party/disklrucache/src/**/*.java",
"third_party/gif_decoder/src/**/*.java",
"third_party/gif_encoder/src/**/*.java",
],
manifest: "library/src/main/AndroidManifest.xml",
libs: [
"android-support-core-ui",
"android-support-compat",
"volley",
],
static_libs: [
"android-support-fragment",
],
sdk_version: "current",
}

App 的Android.bp 直接用"glide"了

static_libs: [
"glide"
],

以上三种方式都是引入 Android 中源码存在的。不存在怎么办,Android源码 不像 AS,连上网,配置下版本号就可以下载。

内置新的Jetpack框架

引入第三方库文件方式,方式一:aar包导入。就可以。但这里不讨论,找些复杂的,包含annotationProcessor(bp 中的plugin) 。Hilt 是 Google 相对较新的框架。

Hilt基于Dagger2开发,又针对Android进行了专属的DI优化。

所以在导入Dagger2和它的依赖文件之外还需要导入Hilt专属的一堆库和依赖文件。

1. 获取框架的库文件

一般来说AS里导入完毕的目录下即可获取到对应的库文件,路径一般在 :C:\Users\xxx\.gradle\caches\modules-2\files-2.1\com.google.dagger\hilt-android

2. 确定额外的依赖文件

为什么需要额外的依赖文件?

完全依赖AS开发可能不知道,导入的包的同时可能引入其他的包。

如Hilt的是在dagger2基础上开发,当然会引入Dagger2,

使用注解,需要javax.annotation包。

Dagger2,javax.annotation 在Gradle 自动下载好的,非项目中明确配置的,我们称之为依赖包。

使用Gradle 自动下载,都会有pom 文件。“dependency”,表示需要依赖的jar 包,还包含了版本号等

如:hilt-android-2.28-alpha.pom

``
`com.google.dagger`
`dagger` //依赖的dagger2
`2.28`//dagger2的版本
`
`
``
`com.google.dagger`
`dagger-lint-aar`
`2.28`
`
`
``
`com.google.code.findbugs`
`jsr305`
`3.0.1`
`
`
......

3. 导入需要的依赖文件

比如SystemUI,已经导入了一些文件,只要导入剩余的文件即可。

一般常用的 源码中都是存在的,决定copy 之前,可以看下先源码中是否存在,存在可以考虑使用。

当然也有例外,如Hilt 我依赖的是源码中dagger2是2.19 版本,编译中报错,没有找到dagger2 中的class,反编译jar确实不存在,使用2.28 的dagger 版本,问题就解决了。所以说可能存在库文件版本较老的情况。

以下就是新增的文件夹,其中manifests 后文中有讲。

    manifests/ 
repository/com/google/dagger/dagger-compiler/2.28/
repository/com/google/dagger/dagger-producers/2.28/
repository/com/google/dagger/dagger-spi/2.28/
repository/com/google/dagger/dagger/2.28/
repository/com/google/dagger/hilt-android-compiler/
repository/com/google/dagger/hilt-android/

4. 编写最终的bp文件

这一步就是把依赖的包,关联起来,根据上文的 pom 文件。

  • 配置dagger2 2.28 的jar
java_import {

name: "dagger2-2.28",

jars: ["repository/com/google/dagger/dagger/2.28/dagger-2.28.jar"],

host_supported: true,

}
  • 配置 dagger2-compiler 2.28 的jar (annotationProcessor 依赖的jar包)
java_import_host {

name: "dagger2-compiler-2.28-import",

jars: [

"repository/com/google/dagger/dagger-compiler/2.28/dagger-compiler-2.28.jar",

"repository/com/google/dagger/dagger-producers/2.28/dagger-producers-2.28.jar",

"repository/com/google/dagger/dagger-spi/2.28/dagger-spi-2.28.jar",

"repository/com/google/dagger/dagger/2.28/dagger-2.28.jar",

"repository/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar",

"repository/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar",

"repository/com/google/dagger/dagger-google-java-format/1.6/google-java-format-1.6-all-deps.jar",

],
}
  • 配置dagger2 的 plugin (annotationProcessor)
java_plugin {
name: "dagger2-compiler-2.28",
static_libs: [
"dagger2-compiler-2.28-import",
"jsr330",
],
processor_class: "dagger.internal.codegen.ComponentProcessor",
generates_api: true,
}
  • 配置 hilt 依赖的aar包
android_library_import {
name: "hilt-2.82-nodeps",
aars: ["repository/com/google/dagger/hilt-android/2.28-alpha/hilt-android-2.28-alpha.aar"],
sdk_version: "current",
apex_available: [
"//apex_available:platform",
"//apex_available:anyapex",
],
min_sdk_version: "14",
static_libs: [
"dagger2-2.28",
"jsr305",
"androidx.activity_activity",
"androidx.annotation_annotation",
"androidx.fragment_fragment",
],

}
  • 配置hilt 的包

    android_library 表示 aar 包,所以必须要配置manifests ,在上文中多出的manifasts文件夹中 放的就是这个文件,AndroidManifest.xml来自hilt-android-2.28-alpha.aar 中

android_library {
name: "hilt-2.82",
manifest: "manifests/dagger.hilt.android/AndroidManifest.xml",
static_libs: [
"hilt-2.82-nodeps",
"dagger2-2.28"
],
......
}
  • 配置 hilt-compiler 2.82 jar包
java_import_host {
name: "hilt-compiler-2.82-import",
jars: [
"repository/com/google/dagger/dagger-compiler/2.28/dagger-compiler-2.28.jar",
"repository/com/google/dagger/dagger-producers/2.28/dagger-producers-2.28.jar",
"repository/com/google/dagger/dagger-spi/2.28/dagger-spi-2.28.jar",
"repository/com/google/dagger/dagger/2.28/dagger-2.28.jar",
"repository/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar",
"repository/com/squareup/javapoet/1.11.1/javapoet-1.11.1.jar",
"repository/com/google/dagger/dagger-google-java-format/1.6/google-java-format-1.6-all-deps.jar",
"repository/com/google/dagger/hilt-android-compiler/2.28-alpha/hilt-android-compiler-2.28-alpha.jar",
"repository/javax/inject/javax.inject/1/javax.inject-1.jar"
],
}
  • 配置hilt的 plugin (annotationProcessor)

    反编译查看需要配置的Processer

好吧,看到上图我傻眼了,11个。下文代码我只贴了一个,需要写11个,其他省略。

java_plugin {
name: "hilt-compiler-2.82",
static_libs: [
"hilt-compiler-2.82-import",
"jsr330",
],
processor_class: "dagger.hilt.processor.internal.root.RootProcessor",
generates_api: true,
}
  • 项目中引用
    `static_libs: [`
`"androidx-constraintlayout_constraintlayout",`
`"androidx.appcompat_appcompat",`
`"com.google.android.material_material",`
`"androidx.room_room-runtime",`
`"androidx.lifecycle_lifecycle-viewmodel",`
`"androidx.lifecycle_lifecycle-livedata",`
`"hilt-2.82",`
`"jsr330"`
`],`

`plugins: ["androidx.room_room-compiler-plugin",`
`"hilt-compiler-2.82",`
`"hilt-compiler-2.82-UninstallModulesProcessor",`
`"hilt-compiler-2.82-TestRootProcessor",`
`"hilt-compiler-2.82-DefineComponentProcessor",`
`"hilt-compiler-2.82-BindValueProcessor",`
`"hilt-compiler-2.82-CustomTestApplicationProcessor",`
`"hilt-compiler-2.82-AndroidEntryPointProcessor",`
`"hilt-compiler-2.82-AggregatedDepsProcessor",`
`"hilt-compiler-2.82-OriginatingElementProcessor",`
`"hilt-compiler-2.82-AliasOfProcessor",`
`"hilt-compiler-2.82-GeneratesRootInputProcessor",`
`],`
  • 编译确认

    编译失败了!看到报错,我的心也凉了。需要配置Gradle 插件。bp 可以配置Gradle插件?

    看了下com/google/dagger/hilt-android-gradle-plugin/,但是并不清楚bp 怎么配置,在源码里,只知道一处:prebuilts/gradle-plugin/Android.bp,但并没有尝试成功。有兴趣的同学,可以研究下。

    而且hilt-android-gradle-plugin 的jar包,依赖包 至少十几个。

public class MainActivity extends AppCompatActivity { ^ Expected @AndroidEntryPoint to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

public class MainFragment extends BaseFragment { ^ Expected @AndroidEntryPoint to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

public class AppApplication extends Application { ^ Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin? [Hilt] Processing did not complete. See error above for details.

虽然Hilt引入失败,但是整个过程我觉得有必要分享一下,给大家一些导入新框架的参考。

源码环境里集成开源框架的流程

2xipH1.png

常用开源框架的对照表

 
build.gradleAndroid.bpAOSP源码位置
androidx.appcompat:appcompatandroidx.appcompat_appcompat/sdk/current/androidx/Android.bp
androidx.core:coreandroidx.core_coreprebuilts/sdk/current/androidx/Android.bp
com.google.android.material:materialcom.google.android.material_materialprebuilts/sdk/current/extras/material-design-x/Android.bp
androidx.constraintlayout:constraintlayoutandroidx-constraintlayout_constraintlayoutprebuilts/sdk/current/extras/constraint-layout-x/Android.bp
androidx.lifecycle:lifecycle-livedataandroidx.lifecycle_lifecycle-livedataprebuilts/sdk/current/androidx/Android.bp
androidx.lifecycle:lifecycle-viewmodelandroidx.lifecycle_lifecycle-viewmodelprebuilts/sdk/current/androidx/Android.bp
androidx.recyclerview:recyclerviewandroidx.recyclerview_recyclerviewprebuilts/sdk/current/androidx/Android.bp
androidx.annotation:annotationandroidx.annotation_annotationprebuilts/sdk/current/androidx/Android.bp
androidx.viewpager2:viewpager2androidx.viewpager2_viewpager2prebuilts/sdk/current/androidx/Android.bp
androidx.room:room-runtimeandroidx.room_room-runtimeprebuilts/sdk/current/androidx/Android.bp
glideglide-prebuiltprebuilts/maven_repo/bumptech/Android.bp
gsongson-prebuilt-jarprebuilts/tools/common/m2/Android.bp
Robolectric相关Robolectric相关prebuilts/tools/common/m2/robolectric.bp
 

经验总结

1、build.gradle 需要配置 额外插件的,如hilt、databinding viewbinding 不建议使用源码编译。

2、建议使用 AOSP 源码 中 bp 已经配置好的。这样就可以直接使用了。

3、jetpack 包引入或者androidx 引入,建议先prebuilts/sdk/current/androidx 下寻找配置好的bp 文件

4、非androidx ,建议先在prebuilts/tools/common/m2下寻找寻找配置好的bp 文件

5、文章中的例子都是prebuilts目录下配置,项目中使用,也可以配置在项目中,都是可以的。

收起阅读 »

探究Android View绘制流程

1.简介在开发中,我们经常会遇到各种各样的View,这些View有的是系统提供的,有的是我们自定义的View,可见View在开发中的重要性,那么了解Android View的绘制流程对于我们更好地理解View的工作原理和自定义View相当有益,本文将依据And...
继续阅读 »

1.简介

在开发中,我们经常会遇到各种各样的View,这些View有的是系统提供的,有的是我们自定义的View,可见View在开发中的重要性,那么了解Android View的绘制流程对于我们更好地理解View的工作原理和自定义View相当有益,本文将依据Android源码(API=30)探究View的绘制流程,加深大家对其的理解和认知。

2.View绘制流程概览

应用的一个页面是由各种各样的View组合而成的,它们能够按照我们的期望呈现在屏幕上,实现我们的需求,其背后是有一套复杂的绘制流程的,主要涉及到以下三个过程:

  1. measure:顾名思义,是测量的意思,在这个阶段,做的主要工作是测量出View的尺寸大小并保存。

  2. layout:这是布局阶段,在这个阶段主要是根据上个测量阶段得到的View尺寸大小以及View本身的参数设置来确定View应该摆放的位置。

  3. draw:这是阶段相当重要,主要执行绘制的任务,它根据测量和布局的结果,完成View的绘制,这样我们就能看到丰富多彩的界面了。

    这些阶段执行的操作都比较复杂,幸运的是系统帮我们处理了很多这样的工作,并且当我们需要实现自定义View的时候,系统又给我们提供了onMeasure()、onLayout()、onDraw()方法,一般来说,我们重写这些方法,在其中加入我们自己的业务逻辑,就可以实现我们自定义View的需求了。

3.View绘制的入口

讲到View绘制的流程,就要提到ViewRootImpl类中的performTraversals()方法,这个方法中涉及到performMeasure()、performLayout()、performDraw()三个方法,其中performMeasure()方法是从ViewTree的根节点开始遍历执行测量View的工作,performLayout()方法是从ViewTree的根节点开始遍历执行View的布局工作,而performDraw()方法是从ViewTree的根节点开始遍历执行绘制View的工作,ViewTree的根节点是DecorView。performTraversals()方法内容很长,以下只是部分代码。

//ViewRootImpl
private void performTraversals() {
final View host = mView;
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
}

4.measure阶段

measure是绘制流程的第一个阶段,在这个阶段主要是通过测量来确定View的尺寸大小。

4.1 MeasureSpec介绍

  1. MeasureSpec封装了从父View传递到子View的布局要求,MeasureSpec由大小和模式组成,它可能有三种模式。
  2. UNSPECIFIED模式:父View没有对子View施加任何约束,子View可以是它想要的任何大小。
  3. EXACTLY模式:父View已经为子View确定了精确的尺寸,不管子View想要多大尺寸,它都要在父View给定的界限内。
  4. AT_MOST模式:在父View指定的大小范围内,子View可以是它想要的大小。

4.2 View测量的相关方法

  1. ViewRootImpl.performMeasure()方法

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
    return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    }

    在performMeasure()中,从根布局DecorView开始遍历执行measure()操作。

  2. View.measure()方法

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int oWidth = insets.left + insets.right;
    int oHeight = insets.top + insets.bottom;
    widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
    heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    ...

    if (forceLayout || needsLayout) {
    // first clears the measured dimension flag
    mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

    resolveRtlPropertiesIfNeeded();

    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
    // measure ourselves, this should set the measured dimension flag back
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
    long value = mMeasureCache.valueAt(cacheIndex);
    // Casting a long to int drops the high 32 bits, no mask needed
    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    ...
    }

    ...
    }

    调用这个方法是为了找出视图应该有多大,父View在宽度和高度参数中提供约束信息,其中widthMeasureSpec参数是父View强加的水平空间要求,heightMeasureSpec参数是父View强加的垂直空间要求,这是一个final方法,实际的测量工作是通过调用onMeasure()方法执行的,因此只有onMeasure()方法可以被子类重写。

  3. View.onMeasure()方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    这个方法的作用是测量视图及其内容,以确定测量的宽度和高度,这个方法被measure()方法调用,并且应该被子类重写去对它们的内容进行准确和有效的测量,当重写此方法时,必须调用setMeasuredDimension()方法去存储这个View被测量出的宽度和高度。

  4. View.setMeasuredDimension()方法

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int opticalWidth = insets.left + insets.right;
    int opticalHeight = insets.top + insets.bottom;

    measuredWidth += optical ? opticalWidth : -opticalWidth;
    measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    setMeasuredDimension()方法必须被onMeasure()方法调用去存储被测量出的宽度和高度,在测量的时候如果setMeasuredDimension()方法执行失败将会抛出异常。

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

    setMeasuredDimensionRaw()方法被setMeasuredDimension()方法调用来设置出被测量出的宽度和高度给View的变量mMeasuredWidth和mMeasuredHeight。

    public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
    result = size;
    break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
    result = specSize;
    break;
    }
    return result;
    }

    参数size是这个View的默认大小,参数measureSpec是父View对子View施加的约束,通过计算的得出这个View 应该的大小,如果MeasureSpec没有施加约束则使用提供的大小,如果是MeasureSpec.AT_MOST或MeasureSpec.EXACTLY模式则会使用specSize。

    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

    getSuggestedMinimumWidth()方法返回View应该使用的最小宽度,这个返回值是View的最小宽度和背景的最小宽度二者之中较大的那一个值。当在onMeasure()方法内被使用的时候,调用者依然应该确保返回的宽度符合父View的要求。

4.3 ViewGroup测量的相关方法

ViewGroup继承View,是一个可以包含其他子View的一个特殊的View,在执行测量工作的时候,它有几个比较重要的方法,measureChildren()、measureChild()和getChildMeasureSpec()。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

measureChildren()方法要求这个View的子View们去测量它们自己,处于GONE状态的子View不会执行measureChild()方法。

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChild()方法要求子View去测量它自身,测量的同时需要考虑到父布局的MeasureSpec要求和它自身的padding。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

这个方法做了测量子View过程中复杂的工作,计算出MeasureSpec传递给特定的子节点,目标是根据来自MeasureSpec的信息以及子View的LayoutParams信息去得到一个最可能的结果。

4.4 DecorView的测量

DecorView继承了FrameLayout,FrameLayout又继承了ViewGroup,它重写了onMeasure()方法,并且调用了父类的onMeasure()方法,在遍历循环去测量它的子View,之后又调用了setMeasuredDimension()。

//DecorView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
final boolean isPortrait = getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;

final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);
...
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
//FrameLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}

5.layout阶段

当measure阶段完成后,就会进入到layout布局阶段,根据View测量的结果和其他参数来确定View应该摆放的位置。

5.1 performLayout()方法

测量完成后,在performTraverserals()方法中,会执行performLayout()方法,开始布局过程。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}

5.2 layout()方法

//ViewGroup
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}

这个是ViewGroup的layout()方法,它是一个final类型的方法,在其内部又调用了父类View的layout()方法。

//View
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
...
}
...
}

View的layout()方法作用是为它本身及其后代View分配大小和位置,派生类不应重写此方法,带有子View的派生类应该重写onLayout()方法,参数l、t、r、b指的是相对于父View的位置。

5.3 setFrame()方法

//View
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// Invalidate our old position
invalidate(sizeChanged);

mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...
}
return changed;
}

在View的layout()方法内会调用setFrame()方法,其作用是给这个视图分配一个大小和位置,如果新的大小和位置与原来的不同,那么返回值为true。

5.4 onLayout()方法

//View
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}

View的onLayout()方法是一个空方法,内部没有代码实现,带有子节点的派生类应该重写此方法,并在其每个子节点上调用layout。

//ViewGroup
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);

ViewGroup的onLayout()方法是一个抽象方法,因此直接继承ViewGroup的类需要重写此方法。

//DecorView
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
...
}

5.5 DecorView的布局

//DecorView
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mApplyFloatingVerticalInsets) {
offsetTopAndBottom(mFloatingInsets.top);
}
if (mApplyFloatingHorizontalInsets) {
offsetLeftAndRight(mFloatingInsets.left);
}

// If the application changed its SystemUI metrics, we might also have to adapt
// our shadow elevation.
updateElevation();
mAllowUpdateElevation = true;

if (changed
&& (mResizeMode == RESIZE_MODE_DOCKED_DIVIDER
|| mDrawLegacyNavigationBarBackground)) {
getViewRootImpl().requestInvalidateRootRenderNode();
}
}

DecorView重写了onLayout()方法,并且调用了其父类FrameLayout的onLayout()方法。

//FrameLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();

final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();

final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}

在FrameLayout的onLayout()方法中,调用了layoutChildren()方法,在此方法内开启循环,让子View调用layout()去完成布局。

收起阅读 »

字节跳动杨震原:没有“天才架构师”,技术团队需要市场化管理

在近日召开的火山引擎品牌发布会上,字节跳动副总裁杨震原在会上表示,火山引擎是把字节跳动内部的技术和工具开放给企业客户,由字节跳动的技术团队为外部客户提供服务。杨震原认为,把技术开放出去有助于提升科技公司的创新力。“当公司规模变得很大,有时候也会导致效率降低。因...
继续阅读 »

在近日召开的火山引擎品牌发布会上,字节跳动副总裁杨震原在会上表示,火山引擎是把字节跳动内部的技术和工具开放给企业客户,由字节跳动的技术团队为外部客户提供服务。杨震原认为,把技术开放出去有助于提升科技公司的创新力。

“当公司规模变得很大,有时候也会导致效率降低。因为没有人能够很好地规划一切,很难有一个‘天才架构师’去了解公司的方方面面,权衡各方面的优先级”,杨震原认为,公司在成长的过程中会达到一个临界值。超过这个值之后,只有引入一些市场机制,公司效率才会变得更高。

以亚马逊为例,亚马逊最强大的是它的AWS云计算能力,AWS就是把亚马逊核心技术开放给外部企业用,通过外部客户把AWS打磨得更好,既更好地服务内部客户,又给外部企业独立做大的机会。这个过程类似于种树,所以亚马逊的生态中长出了巨头企业,包括Uber、Airbnb等。

杨震原认为亚马逊是值得学习的楷模。公司本质是降低交易成本,提高效率,当公司规模变大后,技术团队需要市场化的管理。

“为什么会有公司,它的本质是什么?比如说,你需要一个程序员写代码,如果没有公司,你就要跟他结算,他写200行给他多少钱”,杨震原说,“但是有公司这个形态,你们形成一个小组,就可以对齐目标、使命、文化。他可以按月发工资,也可以考虑他的成长,交易成本变得很低。这就是为什么在市场上交易的是很多很多的公司,而非一个一个的个体”。

但是当公司规模逐步变大时,管理者很难规划所有团队的发展,必须让技术团队面对更大的市场、在更多的场景去服务外部的客户,才能打磨好团队,这也是字节跳动打造火山引擎为外部企业客户提供服务的重要动力。

据杨震原透露,火山引擎对外服务和对内服务在团队方面基本上是融合在一起的,或者说管理线的分叉很靠下,不是在比较高的管理层级就分对外或对内。以数据中台为例,细分到数据中台的一个子方向,才会有对外和对内的人员分工,相当于字节跳动是把技术中台直接市场化支持火山引擎。

收起阅读 »

手把手教你在Flutter项目优雅的使用ORM数据库

Flutter ORM数据库介绍Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是...
继续阅读 »

Flutter ORM数据库介绍

Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是最低效的了。例如IOS平台有coredata、realm等等的框架提供便捷的数据库操作,但来到flutter就又倒退回去裸写sql,这对大部分团队都是重大的成本。

本文将详细介绍一种在Flutter项目中优雅的使用ORM数据库的方法,我们使用的ORM框架是包含在一个Flutter插件flutter_luakit_plugin(如何使用可参考介绍文章)中的其中一个功能,本文只详细介绍这套ORM框架的使用和实现原理。我们给出了一个demo。

我们demo中实现了一个简单的功能,从一个天气网站上查询北京的天气信息,解析返回的json然后存数据库,下次启动优先从数据库查数据马上显示,再发请求向天气网站更新天气信息,就这么简单的一个功能。虽然功能简单,但是我们99%日常的业务逻辑也就是由这些简单的逻辑组成的了。下面是demo运行的效果图。


看完运行效果,我们开始看看ORM数据库的使用。ORM数据库的核心代码都是lua,其中WeatherManager.lua是业务逻辑代码,其他的lua文件是ORM数据库的核心代码,全部是lua实现的,所有代码文件加起来也就120k左右,非常轻量。

针对上面提到的天气信息的功能,我们来设计数据模型,从demo的展示我们看到每天天气信息包含几个信息,城市名、日出日落时间、最高温度、最低温度、风向、风力,然后为了区分是哪一天的数据,我们给每条信息加上个id的属性,作为主键。想好我们就开始定义第一个ORM数据模型,有几个必要的信息,db名,表名,后面的就是我们需要的各个字段了,我们提供IntegerField、RealField、BlobField、TextField、BooleandField。等常用的数据类型。weather 就是这个模型的名字,之后我们weather为索引使用这个数据模型。定义模型代码如下。

weather = {
__dbname__ = "test.db",
__tablename__ = "weather",
id = {"IntegerField",{unique = true, null = false, primary_key = true}},
wind = {"TextField",{}},
wind_direction = {"TextField",{}},
sun_info = {"TextField",{}},
low = {"IntegerField",{}},
high = {"IntegerField",{}},
city = {"TextField",{}},
},

定义好模型后,我们看看如何使用,我们跟着业务逻辑走,首先网络请求回来我们要生成模型对象存到数据库,分下面几步

获取模型对象

local Table = require('orm.class.table')
local _weatherTable = Table("weather”)

准备数据,建立数据对象

local t = {}
t.wind = flDict[v.fg]
t.wind_direction = fxDict[v.ff]
t.sun_info = v.fi
t.low = tonumber(v.fd)
t.high = tonumber(v.fc)
t.id = i
t.city = city
local weather = _weatherTable(t)

保存数据

weather:save()

读取数据

_weatherTable.get:all():getPureData()

是不是很简单,很优雅,什么建表、拼sql、transation、线程安全等等都不用考虑,傻瓜式地使用,一个业务就几行代码搞定。这里只演示了简单的存取,更多的select、update、联表等高级用法可参考db_test demo。

Flutter ORM数据库原理详解

好了,上面已经介绍完如何使用了,如果大家仅仅关心使用下面的可以不看了,如果大家想了解这套跨平台的ORM框架的实现原理,下面就会详细介绍,其实了解了实现原理,对大家具体业务使用还是很有好处的,虽然我感觉大家用的时候极少了解原理。

我们把orm框架分为三层接入层,cache层,db操作层,三个层分别处于对应的线程,具体可以参考下图。接入层可以在任意线程发起,接入层也是每次数据库操作的发起点,上面的demo所有操作都是在接入层,cache层,db操作层仅仅是ORM内部划分,对使用者来讲不需要关心cache层和db操作层。我们把所有的操作分成两种,db后续相关的,和db后续无关的。


db后续无关的操作是从接入层不同的线程进入到cache层的队列,所有操作在这个队列里先同步完成内存操作,然后即可马上返回接入层,异步再到db操作层进行db操作。db后续无关的操作包括 save、update、delete。

db后续相关的操作依赖db操作层操作的结果,这样的话就必须等真实的db操作完成了再返回接入层。db后续相关的操作包括select。

要做到这种数据同步,我们必须先把orm操作接口抽象化,只给几个常用的接口,所有操作都必须通过指定的接口来完成。我们总结了如下基本操作接口。

1、save

2、select where

3、select PrimaryKey

4、update where

5、update PrimaryKey

6、delete where

7、delete PrimaryKey

这七种操作只要在操作前返回前对内存中的cache做相应的处理,即可保证内存cache始终和db保持一致,这样以后我们就可以优先使用cache层的数据了。这七种操作的实现逻辑,这里先说明一下,cache里面的对象都是以主键为key,orm对象为value的形式存储在内存中的,这些控制逻辑是写在cache.lua里面的。

下面详细介绍七种基本操作的逻辑。

save操作,同步修改内存cache,然后马上返回接入层,再异步进行db replace into 的操作


where条件select,这个必须先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


select PrimaryKey,就是选一定PrimaryKey值的orm对象,这个操作首先看cache里面是否有primarykey 值的orm对,如果有,直接返回,如果没有,先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层


update where,先同步到db线程通过where 条件select出需要update的主键值,根据主键值和需要update的内容,同步更新内存cache,然后异步进行db的update操作


update PrimaryKey,根据PrimaryKey进行update操作,先同步更新内存cache,然后异步进行db的update操作


delete where,先同步到db线程通过where 条件select出需要delete的主键值,根据主键值删除内存cache,然后异步进行db的delete操作


delete PrimaryKey,根据PrimaryKey进行delete操作,先同步删除内存cache,然后异步进行db的delete操作


只要保证上面七种基本操作逻辑,即可保证cache中的内容和db最终的内容是一致的,这种尽量使用cache的特性可以提升数据库操作的效率,而且保证同一个db的所有操作都在指定的cache线程和db线程里面完成,也可以保证线程安全。

最后,由于我们所有的db操作都集中起来了,我们可以定时的transation 保存,这样可以大幅提升数据库操作的性能。

结语

目前Flutter领域最大的痛点就是数据库操作,本文提供了一种优雅使用ORM数据库的方法,大幅降低了使用数据库的门槛。希望这篇文章和flutter_luakit_plugin可以帮到大家更方便的开发Flutter
应用。

链接:https://www.jianshu.com/p/62500ae08a07

收起阅读 »

纯 CSS 创建五彩斑斓的智慧阴影!让前景图片自动转化为对应彩色的背景阴影

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下...
继续阅读 »

几天前,我在 Home Depot(aka Toys "R" Us for big kids)处发现,他们有一个巨大的显示器来展示所有这些彩色的供销售的电灯泡!其中一项是y一组在电视后面的智能灯泡。它们会在电视的后面投影近似于电视在播出的内容的彩色阴影,与以下内容 类似



注意电视后面发生的事情。屏幕前景中显示的颜色会被灯泡投影为电视机身后面的彩色阴影。随着屏幕上的颜色发生变化,投射在背景中的颜色也会发生变化。真的很酷,对吧?


自然,看到这个之后,我的第一个想法是,我们是否可以使用网络技术创建一个足够智能以模仿前景色的彩色阴影。事实证明,我们完全可以只使用 CSS 构建出这个案例。在本文中,我们将了解如何创建这种效果。


走起!


让它变成真的!


正如您将在以下部分中看到的,使用 CSS 创建这种彩色阴影似乎是一项艰巨的任务(当然,只是就刚开始而言)。当我们开始进入它并将这个任务的核心分解成更小的部分时,我们其实能够发现这真的很容易实现。在接下来的几节中,我们将创建以下示例:



你应该看到的是一张寿司的图片,后面出现了一个五颜六色的阴影。(只是为了强调我们正在做这一切,阴影被添加了脉冲的效果)抛开示例,让我们深入了解实现,看看 HTML 和 CSS 如何让这一切变为现实!


展示我们的照片


展示我们的寿司的图片对应的 HTML 起始没什么特别的:



<div class="parent">
<div class="colorfulShadow sushi"></div>
</div>


我们有一个父 div 元素,包含一个负责显示寿司的子 div 元素。我们显示寿司的方式是将其指定为背景图像,并由以下 .sushi 样式规则处理:


.sushi {
margin: 100px;
width: 150px;
height: 150px;
background-image: url("https://www.kirupa.com/icon/1f363.svg");
background-repeat: no-repeat;
background-size: contain;
}


在此样式规则中,我们将 div 的大小指定为 150 x 150 像素,并在其上设置 background-image 和相关的其他属性。就目前而言,我们所看到的 HTML 和 CSS 会给我们提供如下所示的内容:



现在是阴影时间


现在我们的图像出现了,剩下的就是我们定义阴影这一有趣的部分。我们要定义阴影的方法是指定一个子伪元素(使用 ::after),它将做三件事:



  1. 直接定位在我们的形象后面;

  2. 继承与父元素相同的背景图片;

  3. 依靠滤镜应用多彩的阴影效果;


这三件事是通过以下两条样式规则完成的:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


让我们花一点时间来看看这里发生了些什么:先注意每一个属性和对应的值,有一些值得注意的标记是 backgroundfilterbackground 属性使用了 inherit 继承父元素,意味着能够继承父元素的背景:


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们为 filter 属性定义了两个过滤的属性,分别是 drop-shadowblur


.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


我们的 drop-shadow 过滤器设置为显示不透明度为 50% 的黑色阴影,而我们的 blur 过滤器会将我们的伪元素模糊 20px。 这两个过滤器的组合最终创建了彩色的阴影,当应用这两个样式规则时,该阴影现在将出现在我们的寿司图像后面:



在这一点上,我们已经完成了。为完整起见,如果我们想要彩色阴影缩放的动画,如下 CSS 代码的添加能够助力我们实现目标:


.colorfulShadow {
position: relative;
}

.colorfulShadow::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.3, 1.3);
}
}


如果您想要一些交互性而没有不断循环的动画,您还可以使用 CSS 过渡来更改阴影在某些动作(如悬停)上的行为方式。困难的部分是像对待在 HTML 中明确定义或使用 JavaScript 动态创建的任何其他元素一样对待伪元素。唯一的区别是这个元素是完全使用 CSS 创建的!


结语


小结


伪元素允许我们使用 CSS 来完成一些历史上属于 HTML 和 JavaScript 领域的元素创建任务。对于我们多彩而智能的阴影,我们能够依靠父元素来设置背景图像。这使我们能够轻松定义一个既继承了父元素的背景图像细节,又允许我们为其设置一系列属性以实现模糊和阴影效果的子伪元素。虽然所有这些都很好,并且我们最大限度地减少了大量复制和粘贴,但这种方法不是很灵活。


如果我想将这样的阴影应用到一个不只是带有背景图像的空元素上怎么办?如果我有一个像 ButtonComboBox 这样的 HTML 元素想要应用这种阴影效果怎么办?一种解决方案是依靠 JavaScript 在 DOM 中复制适当的元素,将它们放置在前景元素下方,应用过滤器,然后就可以了。虽然这有效,但考虑到该过程的复杂程度,实在是有些不寒而栗。太糟糕了,JavaScript 没有等效的 renderTargetBitmap 这种能够把我们的视觉效果渲染成位图,然后你可以做任何你想做的事的 API…… 🥶


以上内容为译文翻译,下面为一些拓展:




拓展


说实在的,我们其实并不需要那么多复杂的内容,图片可以是任意的,比如说 PNG、SVG,最终精简后,HTML 代码仅仅为任意一个元素,附上 style 规定图片地址与大小:


<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>


CSS 代码如下:


.shadowedImage {
position: relative;
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;
}


示例代码


一段示例代码如下:


<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>

<style>
.shadowedImage {
position: relative;
}

.shadowedImage::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: inherit;
background-position: center center;
filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.50)) blur(20px);
z-index: -1;

/* animation time! */
animation: oscillate 1s cubic-bezier(.17, .67, .45, 1.32) infinite alternate;
}

@keyframes oscillate {
from {
transform: scale(1, 1);
}

to {
transform: scale(1.1, 1.1);
}
}

.shadowedImage {
margin: 100px;
width: var(--data-width);
height: var(--data-height);
max-height: 150px;
background-image: var(--data-image);
background-repeat: no-repeat;
background-size: contain;
}
</style>
</head>
<body>
<div class="parent">
<div class="shadowedImage" style="--data-width: 164px; --data-height: 48px; --data-image: url('https://sf3-scmcdn2-tos.pstatp.com/xitu_juejin_web/dcec27cc6ece0eb5bb217e62e6bec104.svg');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-dycdn-tos.pstatp.com/img/bytedance-cn/4ac74bbefc4455d0b350fff1fcd530c7~noop.image');"></div>
<div class="shadowedImage" style="--data-width: 164px; --data-height: 164px; --data-image: url('https://sf1-ttcdn-tos.pstatp.com/img/bytedance-cn/4bcac7e2843bd01c3158dcaefda77ada~noop.image');"></div>
</div>
</body>
</html>


示例效果


效果如下:


image.png



如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。





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

收起阅读 »