-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 295 KB
/
content.json
1
{"meta":{"title":"Mycroft","subtitle":"Mycroft的个人微博","description":"天道酬勤","author":"Mycroft Wong","url":"https://blog.mycroft.wang","root":"/"},"pages":[{"title":"friends","date":"2019-08-09T09:09:16.000Z","updated":"2023-06-13T07:01:29.495Z","comments":true,"path":"friends/index.html","permalink":"https://blog.mycroft.wang/friends/index.html","excerpt":"","text":""},{"title":"categories","date":"2019-08-09T06:57:31.000Z","updated":"2023-06-13T07:01:29.495Z","comments":true,"path":"categories/index.html","permalink":"https://blog.mycroft.wang/categories/index.html","excerpt":"","text":""},{"title":"tags","date":"2019-08-09T08:28:20.000Z","updated":"2023-06-13T07:01:29.496Z","comments":true,"path":"tags/index.html","permalink":"https://blog.mycroft.wang/tags/index.html","excerpt":"","text":""},{"title":"about","date":"2019-08-09T08:29:06.000Z","updated":"2023-06-13T07:01:29.495Z","comments":true,"path":"about/index.html","permalink":"https://blog.mycroft.wang/about/index.html","excerpt":"","text":""}],"posts":[{"title":"CLion Toolchains","slug":"CLion-Toolchains","date":"2023-07-26T09:46:56.000Z","updated":"2023-07-31T02:10:30.903Z","comments":true,"path":"2023/07/26/clion-toolchains/","link":"","permalink":"https://blog.mycroft.wang/2023/07/26/clion-toolchains/","excerpt":"","text":"CLion ToolchainsToolchains 工具链,一些列工具。 ChatGPT 解答 Toolchains 是什么: 在 CLion 中,Toolchain 是指编译和构建项目所需的一组工具和配置。它包括编译器、链接器、构建系统等,用于将源代码转换为可执行文件或库文件。 CLion 支持多种不同的 Toolchain,您可以根据项目的需求选择合适的 Toolchain。 简单来说,Toolchain 包含编译、链接、构建、调试等的一系列工具。在 CLion 的配置中我们可以看到包括 CMake、Build Tool、C Compiler、C++ Compiler、Debugger。 在不同的操作系统上,这些工具会有一些差异,如在 Linux 中,C Compiler 通常为 cc,C++ Compiler 通常为 c++,Debugger 为 gdb。而在 Mac 上,默认的 Debugger 则为 LLDB C Compiler询问 ChatGPT 有哪些 C Compiler: 在C语言编程中,有多种C编译器可供选择,这些编译器可以将C源代码转换为可执行的机器代码。以下是一些常见的C编译器: GCC (GNU Compiler Collection):GCC 是一个广泛使用的开源C编译器,它支持多种平台和架构,并且拥有强大的优化功能。GCC是许多Linux发行版和其他类Unix系统中默认的C编译器。 Clang:Clang 是LLVM项目的一部分,是一个快速、高度兼容C编译器。它在性能上与GCC相媲美,并且支持较新的C标准。 Microsoft Visual C++:Microsoft Visual C++ 是Windows平台上的官方C编译器,它集成在Visual Studio开发环境中。它通常用于Windows平台的C语言开发。 Intel C++ Compiler:Intel C++ Compiler 是由英特尔公司开发的C编译器,专注于优化性能和并行计算。 TCC (Tiny C Compiler):TCC 是一个小巧、快速的C编译器,适用于嵌入式系统和资源受限的环境。 Pelles C:Pelles C 是一个Windows平台上的免费C编译器,适用于学习和轻量级C开发。 Open Watcom:Open Watcom 是一个开源的C/C++编译器套件,支持多种平台。 这些是一些常见的C编译器,每个编译器都有其特定的优点和用途。您可以根据需要和平台选择合适的C编译器来进行C语言开发。 简而言之,有多种 C Compiler,每种操作系统有自己特有的 C Compiler,如 Linux 上的 GCC,MacOS 上的 Clang,Windows 上的 Microsoft Visual C++。 而 Microsoft Visual C++ 实际上是 C++ Compiler,它支持 C90 标准,也在逐步支持 C99,但未完全支持 C99 标准。 GCC GCC (GNU Compiler Collection) 是一个广泛使用的开源编译器集合,由GNU计划开发。它支持多种编程语言,包括C、C++、Objective-C、Fortran、Ada等,并能在多种操作系统和架构上运行。 GCC 是一个强大的编译器,它不仅能将源代码编译成可执行的机器代码,还提供了许多优化选项和功能,以提高程序的性能和效率。GCC 还支持各种平台上的交叉编译,可以为不同的目标平台生成代码。 GCC 是自由软件,使用GNU通用公共许可证 (GPL) 授权,这意味着它可以免费使用,并且用户可以查看和修改其源代码。由于其功能强大和广泛的支持,GCC 成为了许多Linux发行版和其他类Unix系统中的默认编译器,也被广泛应用于各种开发项目中。 GCC 实际上是 GNU 计划开发的 C Compiler,基本很快支持最新的 C 标准。 Clang Clang 是一个开源的C、C++、Objective-C 和Objective-C++编程语言的编译器,它是 LLVM(Low Level Virtual Machine)项目的一部分。Clang 最初由苹果公司开发,现在由一个全球性的开发社区维护和发展。 Clang 的目标是成为一个高性能、高质量的编译器,它主要关注于编译速度、代码质量和用户友好性。Clang 的设计目标之一是提供更好的错误和警告信息,以帮助开发者更容易地发现和解决代码中的问题。 Clang 支持多种平台和架构,可以在 Linux、macOS、Windows 和其他操作系统上运行。它在性能上与传统的编译器(如GCC)相媲美,并且有一些独特的优势,如更好的错误诊断、更快的编译速度和更好的模板支持等。 除了作为独立的编译器使用,Clang 还被广泛应用于其他项目中,如编码工具、静态代码分析工具、代码编辑器等。在 macOS 和 iOS 平台上,Xcode 集成了 Clang 作为默认的编译器。 总的来说,Clang 是一个强大的、高质量的开源编译器,它为 C、C++ 和 Objective-C 等编程语言提供了一种快速、高质量的编译解决方案,并且得到了全球开发者的广泛支持和贡献。 Clang 实际上就是 LLVM 项目中开发的 C、C++、Objective-C、Objective-C++ 编译器。虽然可以在多种系统上运行,但是实际上一般只会在 MacOS 上使用。 LLVM - Low Level Virtual Machine LLVM 是一个开源的编译器基础设施项目,它最初是由苹果公司开发的,现在由一个全球性的开发社区维护和发展。LLVM 的名称最初是 “Low Level Virtual Machine” 的缩写,但随着项目的发展,它已经成为一个代表更广泛目标的名字。 LLVM 提供了一套模块化、灵活和可扩展的编译器工具和技术,用于编译各种编程语言的源代码到可执行的机器代码。它的设计目标是提供高度优化的编译能力,同时提供灵活性和可扩展性,使得开发者能够针对不同的目标平台进行代码优化。 LLVM 的核心组件包括: 前端 (Frontend):负责将不同编程语言(如C、C++、Objective-C、Swift等)的源代码转换成中间表示(Intermediate Representation,IR)。 中间表示 (IR):LLVM使用一种通用的中间表示,它是一种低级别的、静态单赋值(Static Single Assignment,SSA)形式的代码表示形式。这种IR使得LLVM能够在不同的优化阶段进行代码优化。 优化器 (Optimizer):负责在中间表示层次上对代码进行各种优化,以提高代码的性能和效率。 后端 (Backend):负责将优化后的中间表示代码转换成目标平台的机器代码。 LLVM 的设计和实现使得它能够支持多种编程语言,能够在不同的硬件和操作系统上进行交叉编译,也能够应用于各种领域,如编译器、调试器、虚拟机等。 LLVM 的开源许可证是 Apache 2.0 License,这意味着任何人都可以免费使用、修改和分发LLVM的代码。LLVM 的开发社区非常活跃,它得到了全球开发者的广泛支持和贡献。 LLVM 实际就是一系列的编译工具。将源代码经过多重处理后,生成目标平台的机器代码。 前两天刚好看到 Bennyhuo 老师对 kotlin 的科普,kotlin 的编译器实际上也是使用 kotlin 编写的,目前 kotlin 稳定版本是 1.9.0,但 kotlin 后端编译部分已经在使用 k2(kotlin 2.0) 了。 Microsoft Visual C++ Microsoft Visual C++(简称VC++)是微软公司开发的一款集成开发环境 (IDE) 和 C++ 编译器,主要用于在 Windows 平台上进行 C++ 开发。它是 Microsoft Visual Studio 的一部分,提供了丰富的功能和工具,用于编写、调试和部署 C++ 应用程序。 Microsoft Visual C++ 提供了一个直观的图形用户界面,集成了代码编辑器、调试器、资源编辑器、可视化设计器等工具,使得开发者可以在一个集成的开发环境中完成整个 C++ 开发流程。 主要特点和功能包括: C++ 编译器:Microsoft Visual C++ 提供了一个强大的 C++ 编译器,用于将 C++ 源代码编译成可执行的机器代码。 代码编辑器:VC++ 的代码编辑器提供了代码自动补全、语法高亮、代码折叠等功能,帮助开发者提高编码效率。 调试器:VC++ 的调试器支持逐步调试、断点调试、变量监视等功能,帮助开发者定位和修复程序中的错误。 可视化设计器:VC++ 提供了可视化设计器,使得开发者可以通过拖拽和放置的方式设计用户界面。 支持多种工程类型:VC++ 支持多种工程类型,包括控制台应用程序、Windows 窗体应用程序、DLL 库等。 广泛的库和工具:VC++ 提供了丰富的库和工具,包括标准模板库(STL)、MFC(Microsoft Foundation Classes)、ATL(Active Template Library)等,帮助开发者快速构建应用程序。 Microsoft Visual C++ 是一个强大且广泛使用的 C++ 开发工具,适用于 Windows 平台上的各种 C++ 应用程序开发,从简单的控制台程序到复杂的图形界面应用程序和游戏开发都可以使用 VC++ 来完成。 Microsoft Visual C++ 更多的是对 C++ 的支持,对 C 的支持并不是很好。 所以我们在 Windows 上进行 C 开发时,如果想要使用更高的 C 标准,可以安装 Cygwin 或 MinGW。他们可以在 Windows 提供 GNU 工具集,如 GCC、GDB。 Cygwin 和 MinGW 的目的是在 Windows 上提供 GNU 工作环境。而不用必须在 Unix Like 上工作。实际上,Windows 能够提供更全面的开发工具,并且开发的程序也可能需要运行在 Windows 上,所以使用 Cygwin、MinGW 能够很好的解决 Microsoft Visual C++ 对 C 标准的支持。 Cygwin Cygwin 是一个在 Windows 操作系统上提供类 Unix 环境的兼容层和工具集。它允许在 Windows 平台上运行类 Unix 的应用程序和工具,使得开发者可以在 Windows 上编译和运行 Unix/Linux 软件。 Cygwin 包含了一套开发工具和库,如 GCC(GNU Compiler Collection)、GDB(GNU Debugger)、GNU Make 等,以及一些常用的 Unix/Linux 命令行工具和库,如 ls、grep、awk、sed 等。这些工具和库在 Cygwin 环境下运行,提供了类 Unix 的命令行接口和功能,使得开发者可以在 Windows 上使用类 Unix 的开发环境。 Cygwin 是自由软件,使用 GNU 通用公共许可证(GPL)许可证,这意味着用户可以免费使用和修改它。Cygwin 的目标是提供一个完整的 Unix/Linux 兼容环境,使得开发者可以在 Windows 上轻松地进行 Unix/Linux 软件的开发和移植。 需要注意的是,Cygwin 是一个完整的 Unix/Linux 兼容环境,它在 Windows 上运行的应用程序和工具将会在 Cygwin 环境中运行,而不是原生的 Windows 环境。这意味着 Cygwin 程序依赖于 Cygwin DLL,而不能直接使用 Windows DLL。如果需要在 Windows 上开发本地的 Windows 应用程序,可以考虑使用其他 Windows 开发环境,如 Microsoft Visual Studio 和 MinGW。 MinGW MinGW(Minimalist GNU for Windows)是一个在 Windows 平台上使用 GNU 工具集的开发环境。它提供了一套开发工具和库,使得开发者可以在 Windows 上编译和运行类似于 Unix 系统上的软件。 MinGW 包含了 GNU 工具链,如 GCC(GNU Compiler Collection)、GDB(GNU Debugger)、GNU Make 等,以及一些常用的开发库,如头文件和静态链接库,使得开发者能够编写和构建在 Windows 上运行的 C、C++ 等应用程序。 MinGW 是自由软件,使用 GPL 许可证,这意味着用户可以免费使用和修改它。MinGW 的目标是提供一个轻量级的、简化的 GNU 工具链,用于在 Windows 上进行开发,而不需要像 Cygwin 那样提供完整的 Unix 兼容性。 由于 MinGW 提供了与 Unix 环境类似的开发工具和库,使得开发者可以更容易地将他们的 Unix/Linux 软件移植到 Windows 平台上,并且可以在 Windows 上开发跨平台的软件。同时,MinGW 也能够开发本地的 Windows 应用程序,这使得它成为一个受欢迎的 Windows 开发环境选择。 需要注意的是,MinGW 是一个 32 位的工具集,它不能直接用于开发 64 位的应用程序。对于 64 位开发,可以使用 MinGW-w64,它是 MinGW 的一个分支,提供了对 64 位开发的支持。 Cygwin vs MinGW Cygwin 和 MinGW 都是用于在 Windows 平台上提供类 Unix 环境的开发工具和库,但它们在实现和用途上有一些区别。下面是 Cygwin 和 MinGW 的主要区别: 实现方式: Cygwin:Cygwin 是一个完整的 Unix/Linux 兼容环境,它通过提供一个兼容层来在 Windows 上模拟 Unix/Linux 环境。Cygwin 应用程序和工具在运行时依赖于 Cygwin DLL,它负责处理与 Windows 系统的交互,并提供类 Unix 的环境支持。 MinGW:MinGW 是一个最小化的 GNU 工具链,它提供了一套纯粹的编译器和工具,用于在 Windows 上编译和运行类 Unix 的应用程序。MinGW 通过直接将源代码编译为 Windows 可执行文件来实现,不需要类 Unix 的兼容层。 应用场景: Cygwin:Cygwin 主要用于在 Windows 上运行和开发 Unix/Linux 软件,它提供了完整的 Unix/Linux 兼容环境,可以运行大部分 Unix/Linux 命令行工具和应用程序。Cygwin 可以用于开发跨平台的软件,并且提供了类 Unix 的命令行界面和功能。 MinGW:MinGW 主要用于在 Windows 上开发本地的 Windows 应用程序,它提供了 Windows 版本的 GNU 工具链,可以将 C/C++ 代码编译为原生的 Windows 可执行文件。MinGW 不提供类 Unix 的环境支持,它适用于开发原生的 Windows 应用程序。 依赖性: Cygwin:Cygwin 应用程序和工具依赖于 Cygwin DLL,这意味着在运行 Cygwin 程序时需要有 Cygwin 环境。 MinGW:MinGW 应用程序和工具不依赖于特定的 DLL,它们是原生的 Windows 可执行文件,可以直接在 Windows 上运行。 综上所述,Cygwin 和 MinGW 在实现和用途上有所区别。如果需要在 Windows 上运行和开发类 Unix 的应用程序,可以选择 Cygwin;如果需要在 Windows 上开发本地的 Windows 应用程序,可以选择 MinGW。 并没有理解 ChatGPT 的解释。下面试试 Claude 2: Mingw和Cygwin都是在Windows上运行Linux/Unix软件的解决方案,主要区别如下: 原理不同: Mingw是最小化的Unix环境和工具集,直接调用Windows的API。 Cygwin提供了类Unix的系统调用接口,需要Cygwin DLL进行系统调用转化。 目标不同: Mingw主要面向开发者,用于在Windows上编译Linux/Unix程序。 Cygwin更像是一个完整的Linux环境,面向所有Linux/Unix用户和程序。 性能不同: Mingw编译的程序不依赖DLL,性能更好。 Cygwin依赖Cygwin DLL,性能较差。 兼容性不同: Mingw编译的程序和Linux二进制更兼容。 Cygwin提供的是模拟层,和Linux有差异。 使用不同: Mingw更适合交叉编译开发。 Cygwin更像是Linux subsystem,可以直接运行Linux程序。 总体来说,Mingw适合在Windows上编译Linux/Unix程序,Cygwin适合在Windows上模拟Linux环境。 虽然也不完全明白,但是能够简单理解了。 Debugger C 语言的调试器有很多选择,以下是一些常见的 C 调试器: GDB(GNU Debugger):GDB 是一个功能强大的开源调试器,属于 GNU 工具集的一部分。它支持多种编程语言,包括 C 语言,可以在命令行中使用,也可以与许多集成开发环境(IDE)集成。 LLDB:LLDB 是一个开源调试器,属于 LLVM 项目的一部分。它最初是为 macOS 和 iOS 开发的,后来也被移植到其他平台上。LLDB 支持多种编程语言,包括 C 语言,具有现代化的调试功能和用户友好的界面。 Visual Studio Debugger:Visual Studio 是微软公司的集成开发环境(IDE),它包含了一个强大的调试器用于 C 语言开发。Visual Studio Debugger 提供了丰富的调试功能,包括逐步调试、断点调试、变量监视等。 Eclipse CDT Debugger:Eclipse 是一个开源的跨平台 IDE,CDT(C/C++ Development Tooling)是 Eclipse 的 C/C++ 开发工具包。Eclipse CDT Debugger 提供了调试功能,支持多种编程语言,包括 C 语言。 Xcode Debugger:Xcode 是苹果公司的集成开发环境(IDE),用于开发 macOS 和 iOS 应用程序。Xcode Debugger(也称为 LLDB)提供了强大的调试功能,支持 C 语言等多种编程语言。 这些调试器都提供了丰富的调试功能,开发者可以根据自己的需求和偏好选择合适的调试器来进行 C 语言的调试工作。 可以看到 GNU、LLVM、Visual Studio 都提供了相应的调试器。 CLion Cmake 配置就像 Intellij Idea 实际上仍然使用 javac 编译 Java 代码一样,CLion 也是使用这些工具对 C 代码进行编译、调试的。 在 CLion 中配置 Toolchains(工具链)和 CMake 可以让你为你的 C++ 项目选择适当的编译器和构建工具。这样,你可以确保你的项目能够正确编译和构建。 以下是在 CLion 中配置 Toolchains 和 CMake 的步骤: 配置 Toolchains: 打开 CLion,进入 “File”(文件)菜单,然后选择 “Settings”(设置)。 在左侧的面板中,选择 “Build, Execution, Deployment”(构建、执行、部署)选项。 在右侧的面板中,选择 “Toolchains” 选项卡。 点击 “+ Add” 按钮,选择你想要配置的工具链类型,比如 “MinGW”、”MSVC”(Microsoft Visual C++)或者 “WSL”(Windows Subsystem for Linux)。 根据你选择的工具链类型,配置相应的工具链路径和参数。例如,对于 MinGW,你需要指定 MinGW 安装路径,对于 MSVC,你需要选择 Visual Studio 安装路径等。 点击 “Apply”(应用)按钮保存配置。 配置 CMake: 在同一个 “Settings” 窗口中,仍然在 “Build, Execution, Deployment” 选项下,选择 “CMake” 选项卡。 确保 “CMake executable”(CMake 可执行文件)字段指向正确的 CMake 可执行文件路径。你可以手动输入路径或者使用 “…” 按钮浏览选择。 如果你的项目有自己的 CMake 配置文件(CMakeLists.txt),你可以在 “CMake options”(CMake 选项)字段中添加任何你想要传递给 CMake 的参数。这些参数会影响项目的构建过程。 点击 “Apply” 按钮保存配置。 完成上述配置后,CLion 将根据你选择的工具链和 CMake 配置来构建和编译你的项目。确保你的工程设置和 CMakeLists.txt 文件正确配置,这样你就能顺利构建和运行你的 C++ 项目了。 在配置 CMake 时有一个 Toolchain 选项,就可以选择我们之前设置的 Toolchain,甚至直接可以选择 Remote,使用 ssh 使用服务器进行编译。","categories":[],"tags":[]},{"title":"Install CMake","slug":"Install-CMake","date":"2023-06-13T07:08:01.000Z","updated":"2023-06-13T07:16:01.814Z","comments":true,"path":"2023/06/13/install-cmake/","link":"","permalink":"https://blog.mycroft.wang/2023/06/13/install-cmake/","excerpt":"","text":"Install CMakeUbuntu 22.04 仓库目前的 CMake 版本是 3.22.1-1ubuntu1.22.04.1,有时需要安装更新的版本,则可以下载官方包进行安装。 下载地址: https://github.com/Kitware/CMake/releases https://cmake.org/files/ 如我们下载 3.25.0 的文件 https://cmake.org/files/v3.25/cmake-3.25.0-linux-x86_64.sh。 下载后执行以下命令,就可以将执行文件和相关的文档说明安装到 /usr/local 中: bash cmake-3.25.0-linux-x86_64.sh --prefix=/usr/local --exclude-subdir","categories":[],"tags":[]},{"title":"Install zsh","slug":"Install-zsh","date":"2022-11-22T13:02:32.000Z","updated":"2023-06-20T09:07:42.972Z","comments":true,"path":"2022/11/22/install-zsh/","link":"","permalink":"https://blog.mycroft.wang/2022/11/22/install-zsh/","excerpt":"","text":"Install zsh# install zsh ## CentOS yum install zsh wget git -y # install oh-my-zsh sh -c \"$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\" # install zsh-syntax-highlighting ## Linux git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_CUSTOM/plugins/zsh-syntax-highlighting && sed -i 's/^plugins=(/plugins=(zsh-syntax-highlighting /' ~/.zshrc ## Mac git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_CUSTOM/plugins/zsh-syntax-highlighting && sed -i \"\" 's/^plugins=(/plugins=(zsh-syntax-highlighting /' ~/.zshrc # install zsh-autosuggestions ## Linux git clone https://github.com/zsh-users/zsh-autosuggestions.git $ZSH_CUSTOM/plugins/zsh-autosuggestions && sed -i 's/^plugins=(/plugins=(zsh-autosuggestions /' ~/.zshrc ## Mac git clone https://github.com/zsh-users/zsh-autosuggestions.git $ZSH_CUSTOM/plugins/zsh-autosuggestions && sed -i \"\" 's/^plugins=(/plugins=(zsh-autosuggestions /' ~/.zshrc","categories":[],"tags":[]},{"title":"DNF on CentOS 7","slug":"DNF-on-CentOS-7","date":"2022-11-16T09:24:52.000Z","updated":"2023-06-13T07:01:29.478Z","comments":true,"path":"2022/11/16/dnf-on-centos-7/","link":"","permalink":"https://blog.mycroft.wang/2022/11/16/dnf-on-centos-7/","excerpt":"","text":"DNF on CentOS 7在 CentOS 7 上大部分时候需要通过 YUM 安装软件,但有些软件只能使用 DNF,所以必须先安装 DNF 后,再安装其他软件。 安装 DNF安装 DNF 之前需要先安装并启用 epel-release # install epel-release yum install epel-release # install dnf yum install dnf 使用 DNF 安装软件以安装 zoxide 为例,在 zoxide github 仓库中描述的安装方式如下: dnf copr enable atim/zoxidednf install zoxide 但在使用 dnf copr enable atim/zoxide 时提示 copr command doesn't exist,实际上 copr 是一个插件,通过它来启用本地或在线软件源,而安装的 DNF 并不自带该插件。 # install copr dnf install 'dnf-command(copr)' # enable repository dnf copr enable atim/zoxide # install zoxide dnf install zoxide 参考文章CentOS 安装 DNF fedora dnf 命令 在CentOS 8上使用DNF管理软件包","categories":[],"tags":[{"name":"CentOS","slug":"CentOS","permalink":"https://blog.mycroft.wang/tags/CentOS/"}]},{"title":"Socks 5 in Java","slug":"Socks-5-in-Java","date":"2022-09-10T04:05:20.000Z","updated":"2023-06-13T07:01:29.482Z","comments":true,"path":"2022/09/10/socks-5-in-java/","link":"","permalink":"https://blog.mycroft.wang/2022/09/10/socks-5-in-java/","excerpt":"","text":"Socks 5 in Java通过梳理 Socks 5 在 Java 中的实现,去理解 Socks 5 代理的流程。 HTTP以 OkhttpClient 为例,了解基于 Socket 的 HTTP 通信方式。 当使用 Proxy.NO_PROXY,即 type = Proxy.Type.DIRECT 时 DNS 查询:Dns.lookup 创建 Socket:new Socket() 连接 endpoint:Socket.connect(InetSocketAddress) 交换数据:Socket.getInputStream()、Socket.getOutputStream() 关闭 Socket:Socket.close() 1. DNS 查询这一步其实可以和创建 Socket 合并,如果 new Socket() 时传入服务器地址,则会调用 InetAddress.getAllByName(String host) 进行操作系统 DNS 查询。 将这一步拆分出来可以扩展更多的功能。 2. 创建 Socket对于 DIRECT 模式,通过 SocketFactory.createSocket() 进行创建。 工厂类同样可以扩展出更多的功能。 3. 连接 endpointendpoint 一般指的是目标服务器地址,SocketAddress 类型,但其只有一个实现类 InetSocketAddress。 SocketAddress 表示传输层的地址 IP Socket Address (IP address + port number)。 4. 交换数据连接成功之后,就可以通过 OutputStream 和 InputStream 写入和读取数据了。 5. 关闭 SocketSocket 在使用之后,会进行复用,如果一定时间没有再使用,则会进行关闭。 所以不是每次使用 Socket 之后就会立马关闭。 Socket 5同样以 OkhttpClient 为例,了解基于其使用 Socks 5 代理时的通信过程。 需要使用 Socket 5,则需要创建一个 Proxy 代理类,指定代理服务器的地址。 DNS 查询:Dns.lookup 创建 Socket:new Socket(Proxy proxy) 连接 endpoint:Socket.connect(InetSocketAddress) 交换数据:Socket.getInputStream()、Socket.getOutputStream() 关闭 Socket:Socket.close() 和 Proxy.Type.DIRECT 几乎完全相同。但需要注意的两点: DNS 查询的目的是查询出 Socks 5 服务器的 IP 地址,而真正需要访问的目标服务器地址,实际上是通过 Socks 5 服务器去处理的。如果 Socks 5服务器用的是 IP 地址,这一步其实可以忽略。 在创建 Socket 时,将 Proxy 作为参数传入进去。 Socket 内部有一个 SocketImpl 对象,实际的连接和数据交换都是通它来完成的,Socket 类则提供调用的接口。 当使用 Proxy.Type.DIRECT 时,Socket 内部通过 SocketImpl.createPlatformSocketImpl(false) 创建 SocketImpl。这个 SocketImpl 内部就是和 native 层进行交互了,后续建立连接,交换数据,都是使用它来完成。 而当使用 Proxy.Type.SOCKS 时,Socket 通过 SocketImpl.createPlatformSocketImpl(false) 之后,再封装了一层 SocksSocketImpl,并主要重写了 connect(SocketAddress endpoint, int timeout) 方法,用于负责 Socks 5 协议握手。 这相当于如果是一个 HTTP 的数据交换,实际上是运行在 Socks 5 的 Socket 上,而这个 Socket 使用系统底层的 Socket,先完成了 Socks 5 的连接、认证过程,再进行后续的数据交换。 而如果没有使用 Socks 5,则使用 SocketImpl.createPlatformSocketImpl(false) 创建的 SocketImpl 进行连接,后进行数据交换。 以本机 Socks 5 代理服务器为例当客户端 A 通过本机 Socks 5 代理服务器访问公共网络时(假设使用默认端口 1080): 与 Socks 5 代理服务器建立连接,如客户端使用随机端口 40001 与 Socks 5 端口 1080 建立 TCP 连接(进行三次握手) 进行 Socks 5 协议握手,进行认证 Socks 5 协议握手,认证完成之后,后续的数据流量,就直接写入即可 DNS此时,客户端不再解析目标服务器的 IP,即不会再进行 DNS 查询,这个工作是交由 Socks 5 服务器来完成:在 Socks 5 协议握手阶段,客户端会将目标服务器的 IP v4、IP v6 或 Domain,发送给服务器。如果服务器收到的是 IP,则不再需要查询了,但由于 DNS 污染、CDN 等的存在,可能会导致客户端 DNS 查询失败,或查询到的服务器离 Socks 5 服务器比较远。所以这一工作一般会交由 Socks 5 服务器来完成。 DNS 查询完成后,就可以创建与目标服务器连接的 Socket了,然后将客户端的内容,发送给目标服务器,并将从目标服务器收到的响应,发送给客户端。 连接同时,如果客户端需要访问多个服务器,或同时需要与一个服务器建立多个连接,那实际上会与 Socks 5 建立多个连接,同样的,Socks 5 服务器会与目标服务器一一建立连接。 UDPSocks 5 有 UDP relay 的功能,UDP relay 的实现,实际上是,客户端仍然通过 TCP 与 Socks 5 建立连接,但会告诉服务器,我需要你帮我发送 UDP 的数据。Socks 5 服务器收到此请求后,会创建 UDP DatagramSocket,并将监听的 UDP 端口,发送给客户端。客户端收到此端口后,就可以与服务器进行 UDP 的数据交换了。服务器收到 UDP 数据包后,会对其进行拆封,获取目标地址、端口和需要发送的数据,构建自己的 UDP 请求,并使用同一个 DatagramSocket 发送给目标服务器,收到目标服务器的响应后,同样需要进行封装,使用同一个 DatagramSocket 发送给客户端。客户端收到 UDP 数据后就应该主动关闭一开始的 TCP 连接了。 Socks 5Socks 5 的协议握手过程可以查看 socks5协议详解,这里完全不再赘述。 需要添加的一点是,发送的认证信息、域名长度都不是固定的,所以在这些信息之前需要添加一个表示长度的字节,这一点并没有在 rfc1928 中体现。 Socks 5 提供3种可用的命令,用于三种不同的需求: CONNECT 客户端通过 Socks 5 服务器,向目标服务器发送 TCP 请求消息,并接收响应 BIND 用于客户端接收从服务器的请求。FTP 被动模式是一个著名的例子 UDP ASSOCIATE 客户端通过 Socks 5 服务器,向目标服务器发送 UDP 请求消息,并转发响应 CONNECTCONNECT 是 Socks 5 使用最广泛的命令。由于 HTTP、HTTPS 协议是基于 TCP 的,所以大部分 HttpClient 都会提供 Socks 5 的功能。 比较重要的是,使用 CONNECT 命令,客户端就不再通过 DNS 解析目标服务器的 IP,当然可能 Socks 5 服务器是在同一台电脑上。 BINDBIND 很难理解它的用途,目前 HTTP 协议都能完成基本上所有的工作,而且目前有丰富的 HttpClient 实现,那么 BIND 基本上就不会使用到了。 UDP ASSOCIATE又称 UDP relay,这也是 Socks 5 能够代理 UDP 流量的原因。 UDP relay 的主要流程: 客户端与 Socks 5 服务器建立连接,完成认证 客户端发送 UDP 命令,其中包括目标服务器地址 服务端收到后,建立一个 UDP 接收器,准备接收来自客户端的 UDP 请求,并通过 Socks 5 的 Socket 将 IP + port number 发送给客户端 客户端收到后,使用 DatagramSocket 像服务器发送 UDP 请求,其中包括: RSV 保留字段,2个字节,X’0000’ FRAG 当前片段编号,1个字节,通常不支持,则为 X’00’ ATYP 以下地址的地址类型,1个字节 IP V4 地址: X’01’ DOMAINNAME 域名: X’03’ IP V6 地址: X’04’ DST.ADDR 目标服务器地址,变长,IP V4 地址 4个字节;IP V6 地址 16个字节;如果是域名,第一个字节为域名长度,后面字节是域名 DST.PORT 目标服务器端口,2个字节 DATA 用户数据,变长,第一个字节为数据长度,后面字节是数据 Socks 5 服务器 UDP 服务端收到数据后,解析目标服务器地址,并使用 UDP 将用户数据发送给目标服务器 Socks 5 收到从目标服务器的数据后,同样使用第4步,客户端发送给客户端的数据格式,将其转发给客户端 客户端收到数据后,对数据进行解析,并主动关闭 Socket 5 连接 服务器关闭连接 需要注意的是,UDP 数据包的目标地址,只可能是 IP 地址,如果使用 DatagramSocket 的程序员传入的是一个域名,那么实际上先进行系统 DNS 查询。此时,DNS 查询并没有走代理,所以一般情况下,DNS 查询这一步都会拆分出来,同样使用 UDP ASSOCIATE 功能,先进行 DNS 查询,然后再完成之前的请求。 另外,UDP ASSOCIATE 是“一次性”的,在完成一次 UDP 转发之后,都会关闭 Socks 5 连接,如果有新的请求,那么需要重新建立连接。 参考文章rfc1928 Socks5工作原理与搭建 浅谈在代理环境中的 DNS 解析行为 sockslib - A Java library of SOCKS5 protocol including client and server java-socks-proxy-server - Java SOCKS 4/5 server implementation for Java socks5协议详解","categories":[{"name":"Socks 5","slug":"Socks-5","permalink":"https://blog.mycroft.wang/categories/Socks-5/"}],"tags":[{"name":"Socks 5","slug":"Socks-5","permalink":"https://blog.mycroft.wang/tags/Socks-5/"},{"name":"Proxy","slug":"Proxy","permalink":"https://blog.mycroft.wang/tags/Proxy/"}]},{"title":"gitlab配置SMTP总结","slug":"gitlab配置SMTP总结","date":"2021-09-17T06:28:33.000Z","updated":"2023-06-13T07:01:29.482Z","comments":true,"path":"2021/09/17/gitlab-pei-zhi-smtp-zong-jie/","link":"","permalink":"https://blog.mycroft.wang/2021/09/17/gitlab-pei-zhi-smtp-zong-jie/","excerpt":"","text":"gitlab配置SMTP总结自建gitlab有两种配置邮箱管理的方式: MTA(Mail Transport Agent),即搭建邮箱服务器,如postfix、sendmail 使用SMTP服务器,即使用其他平台的注册邮箱。 gitlab的Docker镜像并不包含MTA,如果需要,建议在单独的容器中运行MTA,这样也易于升级。相比于搭建邮箱服务器,更推荐使用SMTP服务器,目前也有很多平台推出企业邮箱,所以直接使用企业邮箱即可。 由于我目前没有企业邮箱,下面我以QQ邮箱为例,进行配置。相比于传统的账号+密码的方式使用邮箱,现在很多平台退出了授权码,更加安全。所以这里使用QQ邮箱授权码,也更加符合真实场景。 docker-compose配置QQ邮箱直接看配置 web: image: 'gitlab/gitlab-ce:latest' container_name: 'gitlab' restart: always hostname: 'gitlab.mycroft.com' environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.mycroft.com:9080' # 指定http host:port gitlab_rails['gitlab_shell_ssh_port'] = 9022 # 指定ssh port gitlab_rails['time_zone'] = 'Asia/Shanghai' # 指定时区 # SMTP设置 gitlab_rails['smtp_enable'] = true # 开启SMTP gitlab_rails['smtp_address'] = \"smtp.qq.com\" # SMTP服务器地址 gitlab_rails['smtp_port'] = 465 # SMTP服务器端口 gitlab_rails['smtp_user_name'] = \"[email protected]\" # 邮箱 gitlab_rails['smtp_password'] = \"xxxxxxxxxxxxxxxx\" # 授权码 gitlab_rails['smtp_domain'] = \"smtp.qq.com\" # 服务器域名 gitlab_rails['smtp_authentication'] = \"login\" # 认证方式 gitlab_rails['smtp_enable_starttls_auto'] = true # 自动启用starttls gitlab_rails['smtp_tls'] = true # 使用TLS gitlab_rails['smtp_pool'] = false # SMTP连接池 # 邮箱设置 gitlab_rails['gitlab_email_from'] = '[email protected]' # 发件人 gitlab_rails['gitlab_email_display_name'] = 'gitlab admin' # 显示的发件人 ports: - '9080:9080' - '9443:443' - '9022:22' volumes: - '$GITLAB_HOME/config:/etc/gitlab' - '$GITLAB_HOME/logs:/var/log/gitlab' - '$GITLAB_HOME/data:/var/opt/gitlab' 邮箱配置使用邮箱有三个目的: 登录 提交 接收事件触发邮件 登录gitlab可配置是否允许用户注册,可在管理员设置中启用/关闭: 如果不允许注册,那么将由管理员创建账号,创建账号时填写邮箱,自动认为验证成功: 但一般内部使用,不会启用用户注册。 管理员创建账号后为其设置密码,就可以使用邮箱登录了,gitlab会提示新用户修改密码。 提交用户通过git配置user.name和user.email就可以配置提交的邮箱。 接收事件触发邮件我们一般会希望团队合作时发送必要的邮件,如合并请求、CI/CD等。 管理员邮箱目前没有找到在初始化安装时指定root用户的邮箱(默认为[email protected]),如果需要修改root用户的邮箱,可以登录root账户,添加邮箱,并将新的邮箱设置为主邮箱。 参考SMTP settings Does ActionMailer’s “enable_starttls_auto” setting protect my email credentials when communicating with Gmail? GitLab 配置邮箱 How to setup a GitLab server using Docker","categories":[{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.mycroft.wang/categories/CI-CD/"}],"tags":[{"name":"Docker","slug":"Docker","permalink":"https://blog.mycroft.wang/tags/Docker/"},{"name":"gitlab","slug":"gitlab","permalink":"https://blog.mycroft.wang/tags/gitlab/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.mycroft.wang/tags/CI-CD/"}]},{"title":"Ubuntu安装OpenVPN服务器","slug":"Ubuntu安装OpenVPN服务器","date":"2021-09-15T16:09:36.000Z","updated":"2023-06-13T07:01:29.482Z","comments":true,"path":"2021/09/16/ubuntu-an-zhuang-openvpn-fu-wu-qi/","link":"","permalink":"https://blog.mycroft.wang/2021/09/16/ubuntu-an-zhuang-openvpn-fu-wu-qi/","excerpt":"","text":"Ubuntu安装OpenVPN服务器1. 安装OpenVPN(2.4.7-1ubuntu2.20.04.3)、easy-rsa(3.0.6-1)sudo apt install openvpn sudo apt install easy-rsa 2. 生成证书参考文档/usr/share/doc/easy-rsa/README.Debian 1. 生成证书文件夹make-cadir ca cd ca vim vars 一般不需要修改vars中生成证书的变量。 1. 初始化./easyrsa init-pki 2. 创建根证书./easyrsa build-ca # 要求输入密码 ./easyrsa build-ca nopass # 不使用密码 # 要求输入 Common Name,或者直接回车,不输入 $ Common Name (eg: your user, host, or server name) [Easy-RSA CA]:mycroft 生成的根证书地址:/path/to/ca/pki/ca.crt 3. 生成服务器证书和私钥./easyrsa build-server-full vpnserver nopass 生成的服务器证书和私钥地址:/path/to/ca/pki/issued/vpnserver.crt、/path/to/ca/pki/private/vpnserver.key 4. 生成客户端证书和私钥./easyrsa build-client-full vpnclient nopass 生成的客户端证书和私钥地址:/path/to/ca/pki/issued/vpnclient.crt、/path/to/ca/pki/private/vpnclient.key 5. 创建Diffie-Hellman 密钥交换算法./easyrsa gen-dh 时间会有点长,耐心等待。生成的dh地址:/path/to/ca/pki/dh.pem。 6. 创建Hmac 消息摘要算法/usr/sbin/openvpn --genkey --secret ta.key 生成的HMAC地址:/path/to/ca/ta.key。 配置VPN1. 将生成的服务端文件拷贝到OpenVPN配置文件夹cp pki/ca.crt pki/issued/vpnserver.crt pki/private/vpnserver.key pki/dh.pem ta.key /etc/openvpn/ 2. 服务端配置# 复制配置样例,并解压出来 server.conf cp /usr/share/doc/openvpn/examples/sample-config-files/server.conf.gz /etc/openvpn/ # 解压 gzip -d server.conf.gz # 编辑配置文件 vim server.conf 3. 修改服务端配置参数参考配置参数: 传输协议:proto udp或proto tcp;udp速度更快 端口:port 1194 根证书:ca ca.crt 服务端证书:cert vpnserver.crt 服务端私钥:key vpnserver.key dh密钥交换算法:dh dh2048.pem(实际这里dh.pem) HMAC消息摘要算法:tls-auth ta.key 0(如果使用内联行嵌入ta.key的内容,则需要通过key-direction 0添加第二个参数) 开启流量通过VPN网络网关:push "redirect-gateway def1 bypass-dhcp" log日志:log-append /var/log/openvpn/openvpn.log 客户端可能使用相同的证书-私钥对:duplicate-cn 当服务端关闭时,客户端不再自动重连:explicit-exit-notify 0 4. 启动OpenVPN服务端进程sudo systemctl start openvpn@server 配置客户端参数复制文件/usr/share/doc/openvpn/examples/sample-config-files/client.conf到一个文件夹,对其进行编辑 修改其中的配置参数: 服务器地址:remote 192.168.0.30 1194 从服务器地址列表(可能有多个)中随机选择:remote-random 配置根证书(内联行):把ca证书内容放在<ca></ca>中 配置客户端证书(内联行):把vpnclient.crt内容放在<cert></cert>中 配置客户端私钥内容(内联行):把vpnclient.key内容放在<key></key>中 配置HMAC(内联行):把ta.key内容放在<tls-auth></tls-auth>中,key-direction 1","categories":[],"tags":[{"name":"Ubuntu","slug":"Ubuntu","permalink":"https://blog.mycroft.wang/tags/Ubuntu/"},{"name":"OpenVPN","slug":"OpenVPN","permalink":"https://blog.mycroft.wang/tags/OpenVPN/"}]},{"title":"Ubuntu使用Docker安装gitlab和gitlab-runner","slug":"Ubuntu使用Docker安装gitlab和gitlab-runner","date":"2021-09-15T12:24:37.000Z","updated":"2023-06-13T07:01:29.482Z","comments":true,"path":"2021/09/15/ubuntu-shi-yong-docker-an-zhuang-gitlab-he-gitlab-runner/","link":"","permalink":"https://blog.mycroft.wang/2021/09/15/ubuntu-shi-yong-docker-an-zhuang-gitlab-he-gitlab-runner/","excerpt":"","text":"Ubuntu 20.04使用Docker安装gitlab和gitlab runner实现CI/CD 安装配置gitlab1. Docker 拉取 gitlab 镜像docker pull gitlab/gitlab-ce:latest 2. 配置挂载文件目录在环境变量中配置GITLAB_HOME,指向挂载文件目录的根目录: export GITLAB_HOME=/usr/local/src/docker/gitlab 3. 创建gitlab容器使用命令: sudo docker run --detach \\ --hostname gitlab.mycroft.com \\ --env GITLAB_OMNIBUS_CONFIG=\"external_url 'http://gitlab.mycroft.com:9080'; gitlab_rails['gitlab_shell_ssh_port'] = 9022; gitlab_rails['smtp_enable'] = false; gitlab_rails['time_zone'] = 'Asia/Shanghai';\" \\ --publish 9443:443 --publish 9080:9080 --publish 9022:22 \\ --name gitlab \\ --restart always \\ --volume $GITLAB_HOME/config:/etc/gitlab \\ --volume $GITLAB_HOME/logs:/var/log/gitlab \\ --volume $GITLAB_HOME/data:/var/opt/gitlab \\ gitlab/gitlab-ce:latest 使用docker-compose.yml: web: image: 'gitlab/gitlab-ce:latest' container_name: 'gitlab' restart: always hostname: 'gitlab.mycroft.com' environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://gitlab.mycroft.com:9080' # 指定http host:port gitlab_rails['gitlab_shell_ssh_port'] = 9022 # 指定ssh port gitlab_rails['smtp_enable'] = false # 指定不使用smtp gitlab_rails['time_zone'] = 'Asia/Shanghai' # 指定时区 ports: - '9080:9080' - '9443:443' - '9022:22' volumes: - '$GITLAB_HOME/config:/etc/gitlab' - '$GITLAB_HOME/logs:/var/log/gitlab' - '$GITLAB_HOME/data:/var/opt/gitlab' 随后执行命令:docker-compose up -d 启动安装。 其中需要注意的点: 启动前的环境变量配置GITLAB_OMNIBUS_CONFIG查看gitlab.rb.template,实际上就是处理配置文件gitlab.rb里面的变量 启动时的GITLAB_OMNIBUS_CONFIG变量内容,并不会写入gitlab.rb配置文件,如果是命令启动,建议在启动后,手动修改gitlab.rb并重启gitlab。如果是docker-compose启动,则不用修改。 由于主机ssh一般占用了22端口,所以gitlab的ssh映射主机其他端口。因为配置中gitlab_rails['gitlab_shell_ssh_port'] = 9022修改的是gitlab的网页显示端口,实际的ssh仍然是22 挂载的目录:配置文件目录、日志文件目录、应用数据文件目录。 hostname用于访问,一般我们会将其添加到hosts文件中方便访问,同时需要修改gitlab的配置文件,已便于后续操作 创建需要大约3分钟,可以使用命令查看日志 docker logs -f gitlab 4. 修改配置如果使用命令安装,则需要修改配置 修改配置文件: # $GITLAB_HOME/config/gitlab.rb external_url 'http://gitlab.mycroft.com:9080/' # 修改暴露的http url,但是不能添加9080,不然无法运行 gitlab_rails['gitlab_shell_ssh_port'] = 9022 # 修改暴露的ssh端口 gitlab_rails['smtp_enable'] = false # 关闭smtp gitlab_rails['time_zone'] = 'Asia/Shanghai' # 设置时区 让配置生效: # 让配置生效 docker exec -it gitlab bash gitlab-ctl reconfigure gitlab-ctl restart exit # 重启gitlab,配置并没有生效 docker restart gitlab 安装配置gitlab-runner1. 安装gitlab-runner使用命令: docker run --detach \\ --add-host=gitlab.mycroft.com:192.168.0.30 \\ --name gitlab-runner \\ --restart always \\ --volume $GITLAB_HOME/runner:/etc/gitlab-runner \\ --volume /var/run/docker.sock:/var/run/docker.sock \\ gitlab/gitlab-runner:latest 使用docker-compose: version: '3' services: gitlab-runner: image: 'gitlab/gitlab-runner:latest' container_name: 'gitlab-runner' volumes: - $GITLAB_HOME/runner:/etc/gitlab-runner - /var/run/docker.sock:/var/run/docker.sock restart: always extra_hosts: - \"gitlab.mycroft.com:192.168.0.30\" 2. 注册使用gitlab-runner命令可以进行注册,如下所示,其中url是gitlab服务器url,registration-token是注册token,可能是特定的项目,也可能是共享的项目,description是名字,可以用于删除,tag-list是注册的表切列表,用于匹配合适的runner。 docker exec -it gitlab-runner \\ gitlab-runner register \\ --non-interactive \\ --executor \"docker\" \\ --docker-image alpine:latest \\ --url \"http://gitlab.mycroft.com:9080/\" \\ --registration-token \"yZAtiZf_H24DCsmZYjGy\" \\ --description \"first-runner\" \\ --tag-list \"docker,test\" \\ --run-untagged=\"true\" \\ --locked=\"false\" \\ --access-level=\"not_protected\" 由于我们上面命令指定的executor是docker,镜像是alpine:latest,所以实际会通过镜像alpine:latest创建临时的容器执行项目中的脚本。 然而我们是通过hosts配置的域名-ip解析,临时容器没有配置hosts,所以在拉取项目时,会出现错误:Could not resolve host 'gitlab.mycroft.com'。 解决方法是在gitlab-runner配置文件$GITLAB_HOME/runner/config.toml中为runner添加hosts,如下: concurrent = 1 check_interval = 0 [session_server] session_timeout = 1800 [[runners]] name = \"first-runner\" url = \"http://gitlab.mycroft.com:9080/\" token = \"XWtxoA5SVNzBEhS_NsbJ\" executor = \"docker\" [runners.custom_build_dir] [runners.cache] [runners.cache.s3] [runners.cache.gcs] [runners.cache.azure] [runners.docker] tls_verify = false image = \"alpine:latest\" privileged = false disable_entrypoint_overwrite = false oom_kill_disable = false disable_cache = false volumes = [\"/cache\"] shm_size = 0 # 新添加的hosts extra_hosts = [\"gitlab.mycroft.com:192.168.0.30\"] 参考GitLab Docker images Configuration options gitlab.rb.template Docker-compose部署gitlab中文版 ‘unable to access ‘XXX’: Could not resolve host’ Gitlab CI/CD pipeline Gitlab runner docker Could not resolve host gitlab-runner Could not resolve host add hosts redirection in docker Local Gitlab cicd failed ‘fatal: unable to access…Could not resolve host:…’ with linux runner","categories":[{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.mycroft.wang/categories/CI-CD/"}],"tags":[{"name":"Ubuntu","slug":"Ubuntu","permalink":"https://blog.mycroft.wang/tags/Ubuntu/"},{"name":"Docker","slug":"Docker","permalink":"https://blog.mycroft.wang/tags/Docker/"},{"name":"gitlab","slug":"gitlab","permalink":"https://blog.mycroft.wang/tags/gitlab/"},{"name":"gitlab-runner","slug":"gitlab-runner","permalink":"https://blog.mycroft.wang/tags/gitlab-runner/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.mycroft.wang/tags/CI-CD/"}]},{"title":"Intent的一些知识点","slug":"Intent的一些知识点","date":"2019-12-03T07:47:06.000Z","updated":"2023-06-13T07:01:29.478Z","comments":true,"path":"2019/12/03/intent-de-yi-xie-zhi-shi-dian/","link":"","permalink":"https://blog.mycroft.wang/2019/12/03/intent-de-yi-xie-zhi-shi-dian/","excerpt":"","text":"Intent的一些知识点Intent的用途Intent是一个消息传递对象。使用Intent向系统请求操作,主要包括: 启动Activity 启动Service 发送Broadcast 下图展示了启动Activity时,Intent在两个Activity之间如何传递的 Intent类型官方的说法: 显式Intent:通过提供目标应用的软件包名称或完全限定的组件类名来指定可处理Intent的应用 隐式Intent:不会指定特定的组件,而是声明要执行的常规操作,从而允许其他应用中的组件来处理 是否指定组件是区别显式和隐式Intent的关键。显式Intent启动指定的组件,而隐式Intent通过声明执行的操作向系统请求操作。 构造显式Intent可通过Intent构造器、setComponent(ComponentName)、setClassName(Context, String)、setClassName(String, String)、setClass(Context, Class<?>)指定组件。 Activity如何声明接收隐式IntentActivity想要接收隐式Intent,必须在manifest中的声明中添加<intent-filter>元素,在<intent-filter>内部,可使用action、data、category三个元素的一个或多个指定要接收的Intent类型。 action前提条件:一个Activity可以声明一个或多个action,未声明action表示不接受隐式Intent;一个Intent不设置或只能设置一个action。 匹配条件:Activity声明的action必须包含想要接收的隐式Intent的action。 category前提条件:一个Activity可以声明一个或多个category,一个Intent也可以包含一个或多个category。 重点:**Intent不存在没有category的情况,因为使用startActivity或startActivityForResult启动隐式Activity时会自动将CATEGORY_DEFAULT应用到Intent中,所以Activity中至少有一个android.intent.category.DEFAULT** 匹配条件:Activity声明的所有category必须包含Intent的所有category。 datadata分为两部分:MIME和URI。MIME和URI各自有匹配的规则,非常简单,这里就不说了。 前提条件:一个Activity可以声明0个、一个或多个data,一个Intent也可以包含0个或一个data。 当Activity未声明data时,只能接受无data的Intent 只声明了URI的Activity,只能接受只有且能够匹配URI部分的data的Intent 只声明了MIME的Activity,接受能够匹配MIME的data的Intent,或是能够匹配MIME,URI是content或file类型数据的Intent 声明了URI和MIME的Activity,只能接受URI和MIME同时匹配的Intent 注意其中有点差异的是,之声明了MIME部分的Activity,可以接受content或file类型URI数据的Intent。 一个可接受隐式Intent的Activity的过滤器的友好设计 声明一个或多个想要接收的action 声明至少一个category————android.intent.category.DEFAULT,若想要被浏览器启动,还需要声明android.intent.category.BROWSABLE 声明一个或多个想要接收的data,若想要接收文件类型的data,可以只声明MIME 可声明多个<intent-filter>接收多种过滤器 (很少使用)若是不想其他app启动,则可以添加"exported"=false。 总结这个规则其实很简单,我们通常会在打开相机、访问文件时使用隐式Intent调动系统组件。反过来,我们想在其他应用如浏览器打开我们的app时,就需要自己声明过滤器,这时就要站在编写<intent-filter>的角度来看待Intent的过滤规则,这是之前一直困扰我,没能够友好理解Intent的关键。","categories":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"Intent","slug":"Intent","permalink":"https://blog.mycroft.wang/tags/Intent/"}]},{"title":"Permission 权限","slug":"Permission-权限","date":"2019-10-14T01:30:30.000Z","updated":"2023-06-13T07:01:29.480Z","comments":true,"path":"2019/10/14/permission-quan-xian/","link":"","permalink":"https://blog.mycroft.wang/2019/10/14/permission-quan-xian/","excerpt":"","text":"Permission 权限Android引入权限的目的是保护用户隐私。APP一定要在访问用户敏感数据(如短信服务)和系统功能(如相机、网络)时申请权限。对于不同的功能,系统可能会自动通过权限申请,也可能提示用户批准权限。 安卓系统安全架构的中心点是禁止任何APP默认拥有对其他APP、系统或是用户拥有不利影响操作的权限。 申请权限APP功能需要的权限一定要在manifest中声明。如下APP需要发送短信功能的权限时: <manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" package=\"com.example.snazzyapp\"> <uses-permission android:name=\"android.permission.SEND_SMS\"/> <application ...> ... </application> </manifest> 如果在manifest中申请的权限是普通权限,那么系统将自动同意 如果在manifest中申请的权限是危险权限,系统将提示用户同意APP的权限申请 关于普通权限和危险权限的区别,查看后面的保护等级。 危险权限的申请提示根据Android版本的不同和APP的目标版本的不同,Android提示用户批准危险权限的方式也不同。 运行时申请 Android 6.0及以上Android版本在6.0及以上,并且APP目标版本在23及以上时,在安装时,系统不会提醒用户批准权限(荣耀手机会在安装完成时,显示一个用户可以操作权限的界面),用户需要在运行时申请权限。如下图左,系统将弹出Dialog供用户操作。若用户已经拒绝过一次,APP再次申请该权限是,Dialog则会显示“不再提醒”的勾选框,以后APP申请权限时,将不在提示用户申请,自动拒绝,如下图右(现在很多系统会在第一次申请时就显示“不再提醒”的勾选框): 另外,及时用户通过了权限,但是也可以在之后在系统设置中关闭权限,所以一定要在运行时每次需要就申请。 安装时申请Android版本在5.1.1及以下或是APP目标版本在22及以下时,系统将在安装时询问用户批准所有的危险权限。若用户同意,那么系统会批准APP声明的所有权限,若用户不同意,APP将无法安装。(某些定制系统会允许安装,但是需要特殊的方法在运行时申请权限)。 获取用户敏感信息的申请提示一些APP可能需要获取依赖于短信的用户敏感信息。如果想申请这些权限,一定要在运行时申请权限之前,提示用户在系统设置中,修改“默认应用”,如默认的短信服务、电话服务。 可选的硬件功能的权限访问硬件功能时需要申请应用权限。然而,不是所有的安卓设备有特定的硬件功能呢。所以,加入你申请相机权限,那么在manifest中一定要使用<uses-feature>标记声明需要的硬件功能。如下: <uses-feature android:name=\"android.hardware.camera\" android:required=\"false\" /> 如果硬件功能的需求设置为true,那么如果硬件上没有该功能,那么系统将不允许安装该APP,如果为false,那么最好在运行时需要改功能前,使用PackageManager.hasSystemFeature()判断设备是否有改功能。 权限的强制执行权限不仅仅用于申请系统的功能,APP的Service也可以强制执行想使用它的自定义权限。关于如果自定义权限,可查看自定义的APP权限。 Activity权限的强制执行可以在<activity>标记中添加android:permission属性,限制谁可以启动该Activity。在Context.startActivity()和Activity.startActivityForResult()会检查权限,如果调用者没有该权限,调用时将抛出SecurityException。 Service权限的强制执行和Activity一样,可以在<service>设置需要的权限。将在Context.startService()、Context.stopService()和Context.bindService()中检查权限。 Broadcast权限的强制执行和Activity一样,可以在<receiver>设置需要的权限。系统将在Context.sendBroadcast()返回后检查权限,若通过,系统将发送,若未通过,系统不会抛出异常,但也不会发送该Broadcast。 动态注册的BroadcastReceiver也可以在注册时的代码中设置权限。 重点:发送者和接收者都需要申请权限。 发送时的权限当发送广播Broadcast时,可以指定权限参数。只有拥有该权限的接收者能够收到该广播。如下 sendBroadcast(Intent(\"com.example.NOTIFY\"), Manifest.permission.SEND_SMS) 接收者一定要获取该权限: <uses-permission android:name=\"android.permission.SEND_SMS\"/> 不仅仅可以使用Android自带的权限,也可以使用自定义的权限。 接收时的权限当在<receiver>(接收者)声明时指定权限(危险的权限),那么想要接收者收到该通知,那么一定要在manifest中使用<uses-permission>声明接收者所需要的权限。如下: <receiver android:name=\".MyBroadcastReceiver\" android:permission=\"android.permission.SEND_SMS\"> <intent-filter> <action android:name=\"android.intent.action.AIRPLANE_MODE\"/> </intent-filter> </receiver> 动态注册类似 BroadcastReceiver权限总结 在发送时指定权限,指的是接收者必须拥有该权限,并且需要在发送者的manifest中定义该权限。 接收者指定权限,指的是发送者必须拥有并携带权限,并且需要在接收者的manifest中定义该权限。 Android 8.0 权限限制为了避免像IM SDK这种进行相互启动的机制。从Android 8.0开始,对广播进行了极大限制,自定义的ACTION不再能够使用。所以,当我在荣耀手机(Android 9.0)上测试时,使用了自定义的ACTION发送广播,接收者始终没有收到。解决方案是使用动态注册。不过这样就实现不了很多功能了。 ContentProvider权限的强制执行在<provider>上添加android:permission属性声明想要操作ContentProvider数据的权限。ContentProvder拥有额外的一个重要的安全特性,被称为URI权限,后面将说明。不同于其他的组件,ContentProvider有两个单独的权限属性可以设置android:readPermission和android:writePermission。注意,拥有写权限,并不意味着拥有读权限,这两者是独立的。 当首次检索一个ContentProvider,进行对应操作时将进行权限检查,若未通过,将抛出SecurityException异常。使用ContentResolver.query()时,需要读权限,使用ContentResolver.insert()、ContentResolver.update()、ContentResolver.delete()时,需要写权限。 URI 权限标准的权限系统对ContentProvider的功能有限。ContentProvider获取想保护它自己的读写权限,并且使其提供的URI只被单独需要的APP使用。 典型的例子是邮箱应用。应该通过权限机制保护用户的隐私数据。如想要展示邮箱应用中的图片时,提供一个URI给图片预览APP,那么当不再预览时,应该收回该权限,因为没有任何理由再让图片预览APP获取其操作了。 解决方法是为每个URI设置单独的权限,如当启动一个Activity时,设置Intent.FLAG_GRANT_READ_URI_PERMISSION和/或Intent.FLAG_GRANT_WRITE_URI_PERMISSION。这样保证接收的Activity能够获取Intent中指定的URI数据,而不管是否操作ContentProvider的权限。 关于URI权限的使用,可以参考FileProvider 其他的权限强制执行参考:权限概览 权限级别权限被分为3个级别: 普通权限:对于用户隐私或是设备操作没有危险的权限,系统将在安装时自动通过该权限 签名权限:系统将在安装时通过的权限,但是只有相同签名的APP才能够获取。注意,一些签名权限并不用于第三方APP 危险权限:可能会影响到用户隐私或是设备的普通操作 特殊权限某些权限并不是与普通权限和危险权限表现相同。SYSTEM_ALERT_WINDOW和WRITE_SETTINGS两个权限相当的敏感,大部分APP不应该使用它们。如果APP需要使用,那么需要在manifest中声明,并且发送Intent请求用户验证。如下: if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_SETTINGS ) ) &#123; startActivityForResult(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply &#123; data = Uri.parse(\"package:$packageName\") &#125;, 2) &#125; override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) &#123; super.onActivityResult(requestCode, resultCode, data) when (requestCode) &#123; 2 -> if (Settings.System.canWrite(this)) &#123; Toast.makeText(this, \"获取到了WRITE_SETTINGS权限\", Toast.LENGTH_LONG).show() &#125; else &#123; Toast.makeText(this, \"没有获取到了WRITE_SETTINGS权限\", Toast.LENGTH_LONG).show() &#125; &#125; &#125; 权限组权限根据设备特性或功能被分为不同的组。在这种系统下,权限请求被组织为组级别处理。并且单个清单组对应多个权限声明。例如,SMS权限组包含READ_SMS和RECEIVE_SMS权限。这样对权限进行分组,让用户做出更有意义的选择,而不是被复杂的权限请求弄得不知所措。 所有危险的权限都属于权限组。无论保护级别如何,任何权限都可以属于权限组。 但是,如果权限很危险,则权限的组只会影响用户体验。 当设备运行的是Android 6.0及以上,并且APP目标版本是23及以上时,当请求一个危险权限时,系统的表现:当我们请求权限时,系统检查APP是否已经获得同组下的其他权限,若没有,将使用系统Dialog提示用户。若APP已经又有了同组下的危险权限,那么系统将直接通过新申请的权限,不必再提示用户。 请求应用权限非常简单,直接可以参考请求应用权限 定义自定义权限自定义权限非常简单,重要的是如何使用权限。如下所示: <permission android:name=\"com.example.myapp.permission.DEADLY_ACTIVITY\" android:label=\"@string/permlab_deadlyActivity\" android:description=\"@string/permdesc_deadlyActivity\" android:permissionGroup=\"android.permission-group.COST_MONEY\" android:protectionLevel=\"dangerous\" /> 总结理解设计权限的目的,就非常容易理解权限的使用。自定义权限较少,大多数时候我们是在使用权限。当然自定义权限也可以使用到,不过这通常是在Service中。另外注意Android 8.0开始,ContentProvider的静态声明受到了极大限制,对于推送功能的实现,目前都需要根据厂商的渠道。","categories":[{"name":"Android - 基础","slug":"Android-基础","permalink":"https://blog.mycroft.wang/categories/Android-%E5%9F%BA%E7%A1%80/"}],"tags":[{"name":"Permission","slug":"Permission","permalink":"https://blog.mycroft.wang/tags/Permission/"},{"name":"权限","slug":"权限","permalink":"https://blog.mycroft.wang/tags/%E6%9D%83%E9%99%90/"},{"name":"ContentProvider","slug":"ContentProvider","permalink":"https://blog.mycroft.wang/tags/ContentProvider/"}]},{"title":"okhttp实现token验证","slug":"okhttp实现token验证","date":"2019-09-21T08:51:47.000Z","updated":"2023-06-13T07:01:29.490Z","comments":true,"path":"2019/09/21/okhttp-shi-xian-token-yan-zheng/","link":"","permalink":"https://blog.mycroft.wang/2019/09/21/okhttp-shi-xian-token-yan-zheng/","excerpt":"","text":"okhttp实现token验证前言公司目前的项目使用了token来验证用户。登陆之后会返回最新的access token,后续在每次请求API时,服务端会返回最新的access token,客户端进行保存。若一段时间内(假定是7天)没有进行操作,则需要重新登陆。 验证流程 在登陆成功之后,后续的token验证流流程如下: 服务端返回access token,客户端保存 客户端发起HTTP请求,携带token,添加header Authorization: Bearer {access token} 服务端验证access token,若过期,则返回状态码401,若未过期,会添加header NewToken: {new access token}。 若返回401,客户端需显示登陆页面,提示用户登陆,若验证通过,正常处理请求,并保存最新的access token,在后续请求中使用。 客户端的处理思考需要考虑的点: 因为我们的APP是必须登陆之后才能使用,后续的接口必须依赖access token,故可以认为(除登录页和闪屏页部分页面的)所有界面都需要验证access token是否有效。 若是在后续所有页面都进行access token验证的判断,侵入性较强,故access token验证肯定是需要进行同意处理的。 在okhttp中,处理access token验证有两种方式:最常用的Interceptor和较少使用的Authenticator。另外,我们也可以统一HTTP请求和响应,不过这和Interceptor的功能重合,所以只需要考虑Interceptor和Authenticator。 实际使用中,会发现Authenticator的调用是在HTTP响应之后(RetryAndFollowUpInterceptor的功能),若是使用Authenticator,则是在一次HTTP相应之后,添加access token再进行一次请求,不可取,所以最后的结论是在Interceptor中添加access token。 在okhttp中,有两种添加Interceptor的方式,一种是在添加在RetryAndFollowUpInterceptor之前,一种是在CallServerInterceptor之前(即networkInterceptors),后一种是一定需要网络请求时才会调用,前者则是可能在缓存中就得到了值,对于一些静态资源,是不需要access token验证的,所以自然添加到networkInterceptors中。 Interceptor实现的代码: object TokenInterceptor : Interceptor &#123; override fun intercept(chain: Interceptor.Chain): Response &#123; val oldRequest = chain.request() val newRequestBuilder = oldRequest.newBuilder() .header(\"User-Agent\", \"android/\" + AppUtils.getAppVersionName()) // 判断是否请求的我们公司的服务端 if (isNeedToken(oldRequest)) &#123; // 若存在token,添加token DataService.accessToken?.let &#123; newRequestBuilder.header(\"Authorization\", \"Bearer $it\") &#125; val response = chain.proceed(newRequestBuilder.build()) // 若是401,表示验证失败 if (HttpURLConnection.HTTP_UNAUTHORIZED == response.code) &#123; // 启动登陆界面 ActivityUtils.startActivity(LoginActivity.getIntent(ActivityUtils.getTopActivity())) &#125; // 若是返回新的token,则保存 response.header(\"NewToken\")?.let &#123; DataService.accessToken = it &#125; return response &#125; return chain.proceed(newRequestBuilder.build()) &#125; private fun isNeedToken(request: Request): Boolean &#123; // 匹配... &#125; &#125; 若同时发起多个HTTP请求,上面的实现则会出现问题,并且这样的情况不可避免。通常在主页会有比较复杂的逻辑,会同时发起多个HTTP请求。下面是可能存在的问题: 若同时发起多个HTTP请求,则会出现多个请求同时返回401的问题,同时启动多个登陆页面,当然这样的错误也可以避免,如设置登陆页面Activity的launchMode为singleTask。 多个HTTP响应也会造成页面上可能会同时弹出多个Toast的情况。 多个HTTP请求携带同样的access token,返回新的access token则不同,可能会因为网络延时,造成服务端最新的access token反而会被前一个access token覆盖。 综上,我给出的解决方案是进行同步: object TokenInterceptor : Interceptor &#123; override fun intercept(chain: Interceptor.Chain): Response &#123; val oldRequest = chain.request() val newRequestBuilder = oldRequest.newBuilder() .header(\"User-Agent\", \"android/\" + AppUtils.getAppVersionName()) // 判断是否请求的我们公司的服务端 if (isNeedToken(oldRequest)) &#123; synchronized(this) &#123; // 若存在token,添加token DataService.accessToken?.let &#123; newRequestBuilder.header(\"Authorization\", \"Bearer $it\") &#125; val response = chain.proceed(newRequestBuilder.build()) // 若是401,表示验证失败 if (HttpURLConnection.HTTP_UNAUTHORIZED == response.code) &#123; // 启动登陆界面 ActivityUtils.startActivity(LoginActivity.getIntent(ActivityUtils.getTopActivity())) &#125; // 若是返回新的token,则保存 response.header(\"NewToken\")?.let &#123; DataService.accessToken = it &#125; return response &#125; &#125; return chain.proceed(newRequestBuilder.build()) &#125; private fun isNeedToken(request: Request): Boolean &#123; // 匹配... &#125; &#125; 除了上面的实现,还需要设置登陆页面Activity的launchMode为singleTask,减少首页Activity请求失败后弹出Toast,另外可以在闪屏页面进行验证,不过设置超时时间尽量短。 不过使用同步就会使每个需要token验证的请求排队,对于APP并发量较少的情况来说没有问题,若是一些不需要token验证,并发量高的HTTP请求,如加载瀑布流图片,则可以进一步判断,不进入同步代码块,这样也能提高效率。 总结实现看起来非常简单,不过这是要在理解access token验证的基础上,并且需要经历很多测试(我让服务端的大神设置access token过期时间为5分钟,这样可以频繁触发access token)。建议阅读后面的参考文章。 参考阅读理解OAuth 2.0 OAuth 2.0 筆記 (6) Bearer Token 的使用方法 OAuth 2.0: Bearer Token Usage RxJava2 + Retrofit2 完全指南 之 Authenticator处理与Token静默刷新","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"开源","slug":"开源","permalink":"https://blog.mycroft.wang/tags/%E5%BC%80%E6%BA%90/"},{"name":"token","slug":"token","permalink":"https://blog.mycroft.wang/tags/token/"}]},{"title":"Okio官方文档翻译","slug":"Okio官方文档翻译","date":"2019-09-12T16:27:40.000Z","updated":"2023-06-13T07:01:29.480Z","comments":true,"path":"2019/09/13/okio-guan-fang-wen-dang-fan-yi/","link":"","permalink":"https://blog.mycroft.wang/2019/09/13/okio-guan-fang-wen-dang-fan-yi/","excerpt":"","text":"Okio官方文档翻译Okio是一个辅助java.io和java.nio变得更易于访问、存储、操作数据的库。它一开始是作为Okhttp的组件存在。Okhttp是在Android中非常好用的HTTP客户端。Okio经过充分测试,并且准备好处理新的问题了。 ByteStrings and BuffersOkio围绕两种类型构建。将大量的功能集成到一个简单的API中: ByteString是不可更改的字节序列。对于字符数据,String是基础。ByteString则是String失散多年的兄弟,更易于将二进制数据作为值来处理。这个类非常好用:它知道如何编码和解码为16进制、base64和UTF-8。 Buffer是可更改的字节序列。和ArrayList一样,我们不需要提前设置它的大小。应该像一个队列一样读取和写入Buffer:在末尾写入数据,从头部读取数据。不必考虑管理位置、长度限制和容量。 在内部,ByteString和Buffer可以节省CPU和内存。如果将UTF-8字符串转换为ByteString,它将保存这个字符串的引用,需要时再进行解码,无需做执行任何操作。 Buffer使用一个Segment链表实现。当我们将数据从一个Buffer移到另一个Buffer时,它重新分配Segment的所有权,而不是复制数据。这在多线程编程中特别有用:一个线程用于网络交换数据,与另一个工作线程交换数据时不必对数据进行复制。 Sources and Sinksjava.io包设计非常好的一点是能够将如加密、压缩等转换进行分层。Okio包含了它自己的流类型,被称为Source和Sink。它用起来和InputStream、OutputStream差不多,但是有几点关键的不同: Timeout,超时。流提供底层IO机制的超时访问。和java.io套接字流不同,read()和write()调用都会有超时信息。 易于实现。Source定义了三个方法:read(),close()和timeout()。没有如available()或是单字节读取造成正确性和性能的危险。 易于使用。即是Source和Sink都只有三个方法用于读写,调用者可以通过使用BufferedSource和BufferedSink接口获得更多的API。 字节流和字符流本质上并没有任何区别,都是数据。UTF-8字符串、big-endian32位整数,little-endian短整数或是任何想要的数据,都可以被认为是字节读取和写入。不必再使用InputStreamReader了。 易于测试。Buffer类实现了BufferedSource和BufferedSink接口,所以测试代码可以非常简单和清晰。 Source、Sink可以和InputStream、OutputStream相互操作,你可以将任何Source视为InputStream,也可以将InputStream视为Source。Sink和OutputStream同理。 例子官方写了一些例子来演示如何使用Okio来处理一些常见的问题。阅读学习如何使用他们。需要的话可以进行复制粘贴。 按行读取一个文本文件使用Okio.source(File)打开一个文件。返回的Source接口非常轻量级,并且只有有限的方法。通常会使用一个Buffer来包装这个Source。有两个好处: API更有用。不像Source只提供了基础的方法,BufferedSource有非常多的方法易于使用。 编程更快。Buffer可以使用更少的IO操作来完成工作。 Source每次打开都需要关闭。打开流的代码负责保证它的关闭。这里我们使用Java的try块来自动关闭Source。 public void readLines(File file) throws IOException &#123; try (Source fileSource = Okio.source(file); BufferedSource bufferedSource = Okio.buffer(fileSource)) &#123; while (true) &#123; String line = bufferedSource.readUtf8Line(); if (line == null) break; if (line.contains(\"square\")) &#123; System.out.println(line); &#125; &#125; &#125; &#125; readUtf8Line()方法读取一行数据(以换行符\\n或\\r\\n分隔),或者直到文件末尾(无换行符)。返回一个字符串,省略末尾的换行符。如果遇到空行,将返回空字符串。如果到文件末尾,则返回null。 上面的代码可以内联fileSource变量使得代码更加紧凑,使用漂亮的for循环代替while循环: public void readLines(File file) throws IOException &#123; try (BufferedSource source = Okio.buffer(Okio.source(file))) &#123; for (String line; (line = source.readUtf8Line()) != null; ) &#123; if (line.contains(\"square\")) &#123; System.out.println(line); &#125; &#125; &#125; &#125; readUtf8Line()方法适用于解析大多数文件。对于某些特定例子,可以考虑使用readUtf8LineStrict()。两者非常相似,但是readUtf8LineStrice()方法要求每行以换行符(\\n或\\r\\n)结尾。在此之前遇到文件末尾,将抛出EOFException异常。readUtf8LineStrict()方法还允许字节数限制,用于防止输入格式错误。 public void readLines(File file) throws IOException &#123; try (BufferedSource source = Okio.buffer(Okio.source(file))) &#123; while (!source.exhausted()) &#123; String line = source.readUtf8LineStrict(1024L); if (line.contains(\"square\")) &#123; System.out.println(line); &#125; &#125; &#125; &#125; 写入文件上面我们使用了Source和BufferedSource读取一个文件。对于写入,我们则使用Sink和BufferedSink。使用Buffer的好处一样:更好的API和性能。 public void writeEnv(File file) throws IOException &#123; try (Sink fileSink = Okio.sink(file); BufferedSink bufferedSink = Okio.buffer(fileSink)) &#123; for (Map.Entry<String, String> entry : System.getenv().entrySet()) &#123; bufferedSink.writeUtf8(entry.getKey()); bufferedSink.writeUtf8(\"=\"); bufferedSink.writeUtf8(entry.getValue()); bufferedSink.writeUtf8(\"\\n\"); &#125; &#125; &#125; 因为没有提供写入换行的API,所以我们手动插入换行字符。大多数时候使用\\n作为换行字符。在很少情况下,可以使用System.lineSeparator()代替\\n:System.lineSeparator()在Windows系统返回\\r\\n,而在其他系统则返回\\n。 我们内联fileSink变量,并且利用链式调用来重写上面的方法: public void writeEnv(File file) throws IOException &#123; try (BufferedSink sink = Okio.buffer(Okio.sink(file))) &#123; for (Map.Entry<String, String> entry : System.getenv().entrySet()) &#123; sink.writeUtf8(entry.getKey()) .writeUtf8(\"=\") .writeUtf8(entry.getValue()) .writeUtf8(\"\\n\"); &#125; &#125; &#125; 上面的代码,我们使用了writeUtf8()方法。调用四次writeUtf8()比下面的代码更高效,因为VM不用去对产生的临时字符串进行垃圾回收。 sink.writeUtf8(entry.getKey() + \"=\" + entry.getValue() + \"\\n\"); // Slower! UTF-8从上面用到的API可以看出,Okio非常喜欢UTF-8。早期的电脑系统经历了各种字符编码:ISO-8859-1,ShiftJIS,ASCII,EBCDIC等等。为了支持多种字符集的编程非常糟糕,甚至,我们还要使用emoji。目前非常幸运的是,世界各地统一都支持UTF-8。只有很少一部分老系统还在使用其他字符集。 如果你需要另外的字符集,可以使用readString()和writeString()。这两个方法需要你指定字符集。除非数据只需要本机读取,否则大多数情况下,都应该使用UTF-8方法来编程。 当在解析字符串时,你需要记住字符串时如何表示和编码的。当一个字形有声调或是其他变形时,它代表一个单独的混合字符,如é,或是一个字符e接一个修饰符´。当整个字形是一个单独的字符时,它被称为NFC,当它是多个字符组成时,被称为NFD。 即使我们使用IO操作字符串时都使用UTF-8,他们在内存的存在形式是Java String,使用的是另一个变种UTF-16进行编码。这是非常糟糕的编码,因为它大部分字符都使用16字节字符,但是却用不到16个字节。特别是,emoji字符占两个java字符。这存在的问题是,String.length()返回不一样的结果:UTF-16的字符长度,并不是原生字形的长度。 Café 🍩 Café 🍩 Form NFC NFD Code Points c a f é ␣ 🍩 c a f e ´ ␣ 🍩 UTF-8 bytes 43 61 66 c3a9 20 f09f8da9 43 61 66 65 cc81 20 f09f8da9 String.codePointCount 6 7 String.length 7 8 Utf8.size 10 11 大部分情况,Okio可以让你忽略这些问题更关注数据。但是当你需要这些时,有一些方便的API处理低级的UTF-8字符串。 使用Utf8.size()来统计使用UTF-8编码时的字节长度。这在如协议缓冲等长度前缀编码时非常方便。 使用BufferedSource.readUtf8CodePoint()读取单个可变长度代码点,使用BufferedSinkel.writeUtf8CodePoint()写入。 Golden ValuesOkio喜欢测试。库已经经过了严格的测试,并且它有易于测试的特性。我们找到一种十分好用的测试模式,称为golden value测试。这种测试的目的是确认被早期版本的程序进行编码的数据能够安全的使用当前版本的程序进行解码。 我们将通过使用Java Serialization(Java序列化)编码来进行演示。题外话,我们一定要放弃糟糕的Java序列化,大多数编程我们应该使用其他编码,如JSON或protobuf。下面是对一个对象进行序列化的方法,返回一个ByteString: private ByteString serialize(Object o) throws IOException &#123; Buffer buffer = new Buffer(); try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) &#123; objectOut.writeObject(o); &#125; return buffer.readByteString(); &#125; 这里对上面的代码进行解释: 我们构造一个Buffer存储序列化后的数据。Buffer是ByteArrayOutputStream更好的替代者。 我们获取Buffer的OutputStream,通过它,我们将数据写入Buffer中,并且总在Buffer结尾添加数据。 构造一个ObjectOutputStream(Java序列化API)并且写入对象。try块为我们自动关闭流。注意关闭Buffer没有用。 最后,调用Buffer.readByteString()方法,获取ByteString。这个方法允许我们指定读取的字节数,在这里我们不指定数量,获取整个字符串。从Buffer中读取数据,总是能保证数据从Buffer头部读取的。 通过上面的serialize()方法,我们准备好计算和打印golden value了。 Point point = new Point(8.0, 15.0); ByteString pointBytes = serialize(point); System.out.println(pointBytes.base64()); 打印出ByteString的base64值,是因为base64紧凑和便于嵌入测试用例的特性。程序打印如下: rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA 这就是我们的golden value了。我们再次使用base64键入到我们的测试用例中,将其转换回一个ByteString: ByteString goldenBytes = ByteString.decodeBase64(\"rO0ABXNyAB5va2lvLnNhbXBsZ\" + \"XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA\" + \"AAAAAAA\"); 下一步,将ByteString反序列成我们需要的对象。这个方法和上面的serialize()方法相反:添加一个ByteString到Buffer中,然后使用一个ObjectInputStream进行读取。 private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException &#123; Buffer buffer = new Buffer(); buffer.write(byteString); try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) &#123; return objectIn.readObject(); &#125; &#125; 下面我们对golden value进行解码测试: ByteString goldenBytes = ByteString.decodeBase64(\"rO0ABXNyAB5va2lvLnNhbXBsZ\" + \"XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA\" + \"AAAAAAA\"); Point decoded = (Point) deserialize(goldenBytes); assertEquals(new Point(8.0, 15.0), decoded); 通过这里的测试,我们可以在不破坏兼容性的情况下改变Point类的序列化。 写入二进制文件对二进制文件编码和对文本文件编码没有什么不同。Okio同样使用BufferedSink和BufferedSource进行操作。这对那些既包含字节,有包含字符的二进制编码非常友好。 写入二进制数据比写入文字更加的危险,这是因为非常难诊断出出错的地方。围绕这些问题,可以通过注意下面几点来尽量避免: 每个域的宽度。这是使用的字节数量。Okio不包含发射部分字节的机制。如果需要这个功能,那么就需要在写入前自己进行位移和屏蔽。 每个域的字节序。所有超过一个字节的域都有字节序:字节是否经过了排序(从大到小的big endian和从小到达的little endian)。Okio使用在方法名加后缀Le的方式表示使用little-endian的方法;没有后缀则是使用big-endian。 是否带符号。Java没有不带符号的基础类型(除了char),所以对于这种,通常是在应用层进行处理。为了保证更简单,Okio在writeByte()和writeShort()接收int类型。这样你就可以传入一个无符号的byte如255(byte值最大是127),Okio可以保证写入正确。 解释:对于上面的第二点,如果写入一个int值3,如果是big endian,那么写入的则是00 00 00 03,如果是little endian则是03 00 00 00。对于上面的第三点,如果写入的一个值是255,如果是Java中的byte类型,它最大值是127,并不能写入255,所以Okio可以使用writeShort()方法写入int值,保证一个字节可以表示255。 Method Width Endianness Value Encoded Value writeByte 1 3 03 writeShort 2 big 3 00 03 writeInt 4 big 3 00 00 00 03 writeLong 8 big 3 00 00 00 00 00 00 00 03 writeShortLe 2 little 3 03 00 writeIntLe 4 little 3 03 00 00 00 writeLongLe 8 little 3 03 00 00 00 00 00 00 00 writeByte 1 Byte.MAX_VALUE 7f writeShort 2 big Short.MAX_VALUE 7f ff writeInt 4 big Int.MAX_VALUE 7f ff ff ff writeLong 8 big Long.MAX_VALUE 7f ff ff ff ff ff ff ff writeShortLe 2 little Short.MAX_VALUE ff 7f writeIntLe 4 little Int.MAX_VALUE ff ff ff 7f writeLongLe 8 little Long.MAX_VALUE ff ff ff ff ff ff ff 7f 下面的代码将一个bitmap写入成一个BMP的文件格式。 void encode(Bitmap bitmap, BufferedSink sink) throws IOException &#123; int height = bitmap.height(); int width = bitmap.width(); int bytesPerPixel = 3; int rowByteCountWithoutPadding = (bytesPerPixel * width); int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4; int pixelDataSize = rowByteCount * height; int bmpHeaderSize = 14; int dibHeaderSize = 40; // BMP Header sink.writeUtf8(\"BM\"); // ID. sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize); // File size. sink.writeShortLe(0); // Unused. sink.writeShortLe(0); // Unused. sink.writeIntLe(bmpHeaderSize + dibHeaderSize); // Offset of pixel data. // DIB Header sink.writeIntLe(dibHeaderSize); sink.writeIntLe(width); sink.writeIntLe(height); sink.writeShortLe(1); // Color plane count. sink.writeShortLe(bytesPerPixel * Byte.SIZE); sink.writeIntLe(0); // No compression. sink.writeIntLe(16); // Size of bitmap data including padding. sink.writeIntLe(2835); // Horizontal print resolution in pixels/meter. (72 dpi). sink.writeIntLe(2835); // Vertical print resolution in pixels/meter. (72 dpi). sink.writeIntLe(0); // Palette color count. sink.writeIntLe(0); // 0 important colors. // Pixel data. for (int y = height - 1; y >= 0; y--) &#123; for (int x = 0; x < width; x++) &#123; sink.writeByte(bitmap.blue(x, y)); sink.writeByte(bitmap.green(x, y)); sink.writeByte(bitmap.red(x, y)); &#125; // Padding for 4-byte alignment. for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) &#123; sink.writeByte(0); &#125; &#125; &#125; 这段代码最麻烦的部分是格式需要填充。BMP格式期望一行以4个字节的边界开始,所以添加0值进行对齐非常重要。 对其他二进制格式进行编码通常非常相似。下面是一些建议: 使用golden values编写测试代码。确保程序发送期望的结果,可以让测试更加的简单。 使用Utf8.size()计算一个编码的字符串的字节长度。这对长度前缀的格式非常有效。 使用Float.floatToIntBits()和Double.doubleToLongBits()对浮点值进行编码。 通过Socket进行通信通过网络发送和接受数据,和写入和读取文件非常相似,使用BufferedSink对输出值编码,使用BufferedSource对输入值编码。和文件一样,网络协议可以使用文本、二进制,或是两者的组合。但是网络和文件系统中也有很多的不同。 你只能同时读、或者写入一个文件,但是通过网络,你两者可以同时进行。一些协议通过循环的方式完成:发送一个请求,读取结果,重复。你可以通过单线程来实现这种协议。另外有的协议可能就允许你同时进行读写。典型的,你想使用一个专门的线程进行读。对于写,你可以使用一个专门的线程进行写,或是在多线程中使用同一个Sink使用。Okio的流对于多线程使用是不安全的。 Sink对输出的数据进行缓冲以最大化地减少IO操作。这是非常有效的,但是这意味着你必须手动调用flush()方法发送数据。典型地面向消息协议在每个消息之后都会进行flush。当缓冲数据超过一定阈值时,Okio将自动进行flush。这是为了节省内存,但你也不应该依赖这个机制。 Okio基于java.io.Socket进行连接。一个服务器或是客户端构造一个Socket,然后使用Okio.source(Socket)读取,Okio.sink(Socket)进行写。这两个API也可以和SSLSocket使用。你也应该尽量使用SSL。 在任何线程中调用Socket.close()取消Socket,这样会导致Source和Sink立即失败并抛出IOException。你也可以为所有的Socket操作配置超时。不需要为了配合超时去持有一个Socket的引用:Source和Sink直接暴露超时。即使流被装饰过,这个API也可以工作。 作为使用Okio进行网络操作的一个完整的例子,我们写了一个基本的SOCKS代理服务器。下面是一些重点: Socket fromSocket = ... BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket)); BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket)); 为Socket构造Source和Sink和为文件构造它们是一样的。一旦构造得到Source和Sink,那么你将禁止在使用InputStream和OutputStream。 Buffer buffer = new Buffer(); for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) &#123; sink.write(buffer, byteCount); sink.flush(); &#125; 上面的代码循环复制Source中的数据到Sink中,在每次读取之后进行flush。如果我们不需要flush,我们可以使用一句代码替换这个循环,BufferedSink.writeAll(Source)。 参数8192指的是在每次返回之前读取的最大的字节数。我们可以在其中传入任何的值。但是我们喜欢8kb,因为这是Okio在一次系统调用中的最大值。大多数时候,应用代码不需要处理这个限制。 int addressType = fromSource.readByte() & 0xff; int port = fromSource.readShort() & 0xffff; Okio使用有符号的类型如byte和short,但是部分协议协议使用的是无符号的值。使用位操作符&将一个有无符号的值转换成一个无符号的值是Java的惯用方法。下面是byte、short、和int的一个转换备忘单(注意将byte和short转换成了int,int转换成了long): Type Signed Range Unsigned Range Signed to Unsigned byte -128..127 0..255 int u = s & 0xff; short -32,768..32,767 0..65,535 int u = s & 0xffff; int -2,147,483,648..2,147,483,647 0..4,294,967,295 long u = s & 0xffffffffL; Java没有可替代无符号long类型的基础类型。 哈希作为Java程序员,我们都受到过哈希的轰炸。早期,我们被介绍hashCode()方法,我们知道需要重写这个方法,否则可能会发生意想不到的事情。后来我们我们接触LinkedHashMap和与其相关的类。它们组织数据以快速取出的机制都建立在hashCode()方法上。 在其他地方我们有用到加密哈希函数。这些都被广泛使用。如HTTPS证书、git提交,BitTorrent完整性检查和区块链分块,都使用了加密哈希。哈希使用得当可以提升性能、隐私、安全和应用的简洁性。 每个加密哈希函数接受一个变长的字节输入流,产生一个固定长度字节字符串(称为hash)。哈希函数有以下几个重要特征: 确定性:同一个输入总是产生同样的输出。 统一性:每个输出字节字符串都容易进行对比。很难去找到不同的输入有同样输出的哈希。这被称为“意外”。 不可逆:知道输出并不能帮助你找到输入。注意,如果你知道一些可能的输入,你可以对他们进行hash,看他们的结果是否匹配。 知名:哈希在各种地方都有实现,并且严格。 好的哈希函数计算消耗很低(大约10微秒),并且逆向消耗很高(以千年为单位)。计算稳定和数学性质使得一个好的哈希函数很难被逆向。当选择一个哈希函数时,一定注意不是所有的都是相等的。Okio支持以下几种知名的哈希加密算法: MD5:一个128位(16字节)的哈希值。不安全并且已经过时了,因为逆向消耗并不是非常巨大。提供这种哈希算法是因为它非常流行,并且对于早期对安全不敏感的系统来说非常便捷。 SHA-1:一个160位(20字节)的哈希值。最近被证明是可能发生“意外”的。所以考虑从SHA-1升级到SHA-256。 SHA-256:256位(32字节)的哈希值。SHA-256被广泛的理解,并且逆向消耗巨大。大多数系统应该使用这个哈希函数。 SHA-512:512位(64字节)的哈希值。非常难以逆向。 每一个哈希构造一个固定长度的ByteString。使用hex()方法获取方便的16进制字符串。或者保留为ByteString,因为ByteString是一个方便的数据类型。 Okio使用ByteString生成加密哈希值: ByteString byteString = readByteString(new File(\"README.md\")); System.out.println(\" md5: \" + byteString.md5().hex()); System.out.println(\" sha1: \" + byteString.sha1().hex()); System.out.println(\"sha256: \" + byteString.sha256().hex()); System.out.println(\"sha512: \" + byteString.sha512().hex()); 从Buffer中读取: Buffer buffer = readBuffer(new File(\"README.md\")); System.out.println(\" md5: \" + buffer.md5().hex()); System.out.println(\" sha1: \" + buffer.sha1().hex()); System.out.println(\"sha256: \" + buffer.sha256().hex()); System.out.println(\"sha512: \" + buffer.sha512().hex()); 从Source对应的输入流: try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); BufferedSource source = Okio.buffer(Okio.source(file))) &#123; source.readAll(hashingSink); System.out.println(\"sha256: \" + hashingSink.hash().hex()); &#125; 从Sink对应的输出流: try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole()); BufferedSink sink = Okio.buffer(hashingSink); Source source = Okio.source(file)) &#123; sink.writeAll(source); sink.close(); // Emit anything buffered. System.out.println(\"sha256: \" + hashingSink.hash().hex()); &#125; Okio也支持HMAC(),它组合密钥和一个哈希值。应用程序通常使用HMAC保证数据正确性和验证。 ByteString secret = ByteString.decodeHex(\"7065616e7574627574746572\"); System.out.println(\"hmacSha256: \" + byteString.hmacSha256(secret).hex()); 通过哈希,你可以使用ByteString,Buffer,HashingSource和HashingSink生成HMAC。注意Okio没有实现MD5的HMAC。Okio利用Java的java.security.MessageDigest生成哈希,使用javax.crypto.Mac生成HMAC。","categories":[{"name":"开源库 - okio","slug":"开源库-okio","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okio/"}],"tags":[{"name":"okio","slug":"okio","permalink":"https://blog.mycroft.wang/tags/okio/"},{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"源码","slug":"源码","permalink":"https://blog.mycroft.wang/tags/%E6%BA%90%E7%A0%81/"}]},{"title":"FileProvider","slug":"FileProvider","date":"2019-09-05T06:42:41.000Z","updated":"2023-06-13T07:01:29.478Z","comments":true,"path":"2019/09/05/fileprovider/","link":"","permalink":"https://blog.mycroft.wang/2019/09/05/fileprovider/","excerpt":"","text":"FileProvider前言Android开发始终脱离不了图片处理,特别是Android 7.0开始,无法通过file:///的URI来进行在应用之间共享文件,取而代之的是content uri。这样必然增加了开发难度,如必须生成content uri,赋予访问权限,同时暂时没有找到能够通过content uri获取文件大小的方法。同时一些特殊需要,如安装apk、调用相机拍照都需要改成content uri,而无法再直接通过Uri.fromFile(File)获取的URI共享文件。所以认真研究FileProvider是必要的。 下面着重通过官方文档介绍FileProvider的使用:FileProvider 注:本文所用代码在FileProvider中,可进行查阅。 翻译这里就不在引用原文了,下面是翻译的内容 FileProvider是ContentProvider的一个特殊子类,为了加强在应用之间安全的分享文件。通过创建content://的uri替换file:///的uri。 content://的uri允许授予临时的读写权限。当我们创建一个Intent,其中包含一个content uri,为了能够让对方的应用能够访问到这个content uri,可以使用Intent.setFlags()添加权限。如果收到content uri的是Activity,只要该Activity所在栈处于活动状态,那么这些权限一直会存在。如果收到content uri的是Service,那么Service一直运行,权限就会一直在。 相对于file:///的uri,你想控制文件的控制权限,那么你必须修改系统的底层文件权限。你提供的这些权限将对所有的应用有效,一直保留到你更改他们。这种权限控制根本上是不安全的。 提供content uri增加了文件访问权限等级,使得FileProvider成为Android安全基础框架非常重要的部分。 关于FileProvider,通过下面5点介绍: 定义FileProvider 指定可用的文件 为一个文件生成content uri 为一个uri赋予临时权限 提供content uri给其他应用 1. 定义FileProvider因为FileProvider的默认功能就是为文件提供content uri,你不需要定义FileProvider的子类。而是,你在manifest中包含一个FileProvider。为了指定FileProvider组件,在manifest添加一个provider元素。这是android:name属性为android.support.v4.content.FileProvider(androidx为androidx.core.content.FileProvider)。设置android:authorities为content uri的域名。例如你的域名是wang.mycroft,你应该设置authority为wang.mycroft.fileprovider。设置android:exported属性为false,FileProvider不需要设置为公开的。设置android:grantUriPermissions属性为true,为了允许赋予文件的临时访问权限。如下: <manifest> ... <application> ... <provider android:name=\"androidx.core.content.FileProvider\" android:authorities=\"wang.mycroft.fileprovider\" android:exported=\"false\" android:grantUriPermissions=\"true\"> ... </provider> ... </application> </manifest> 如果你想重写FileProvider方法的默认行为,那么继承FileProvider类,在provider中指定android:name为其的全路径类名。 2. 指定可用的文件一个FileProvider只能为预先指定的文件夹下的文件提供content uri。为了指定一个文件夹,在xml中指定文件的存储路径,在paths下添加子属性。例如,下列的path元素告诉FileProvider你想要把私有文件区域下的image/子目录提供content uri。 <paths xmlns:android=\"http://schemas.android.com/apk/res/android\"> <files-path name=\"my_images\" path=\"images/\"/> ... </paths> <paths>元素必须包含一个或多个子元素: <files-path name=\"name\" path=\"path\" /> 代表app内部存储区域的files/子目录下的文件。此子目录的值和Context.getFilesDir()的返回值相同。 <cache-path name=\"name\" path=\"path\" /> 代表app内部存储区域的缓存子目录。此子目录的值和Context.getCacheDir()的返回值相同。 <external-path name=\"name\" path=\"path\" /> 代表外部存储区域的根目录。此子目录的值和Environment.getExternalStorageDirectory()的返回值相同。 <external-files-path name=\"name\" path=\"path\" /> 代表app外部存储区域的文件。此子目录的值和Context.getExternalFilesDir(null)的返回值相同。 <external-cache-path name=\"name\" path=\"path\" /> 代表app外部存储区域的缓存子目录文件。此子目录的值和Context.getExternalCacheDir()的返回值相同。 <external-media-path name=\"name\" path=\"path\" /> 代表app外部存储区域的多媒体子目录文件。此子目录的值和Context.getExternalMediaDirs()的返回值相同。(Context.getExternalMediaDirs()需要API > 21) 这些子元素使用相同的属性: name:uri相对路径。为了强制保证安全,这个值用于隐藏实际分享的子目录。子目录名包含在path属性上。 path:被分享的目录。name被认为是uri相对路径,path则是实际分享的子目录。注意,path的值是一个子目录,不是具体的文件。不能单独指定一个分享的文件名,也不能使用通配符指定一系列的文件。 一定要将被分享文件的所在目录添加到paths中,作为一个子元素,如下xml中制定了两个子目录: <paths xmlns:android=\"http://schemas.android.com/apk/res/android\"> <files-path name=\"my_images\" path=\"images/\"/> <files-path name=\"my_docs\" path=\"docs/\"/> </paths> 将paths元素和其子元素添加到项目中的xml文件中。例如将其放在res/xml/file_paths.xml中。为了在FileProvider中引用这个文件,添加一个<meta-data>元素作为我们定义的<provider>的子元素。设置<meta-data>元素的子元素android:name值为android.support.FILE_PROVIDER_PATHS,设置子元素android:resource的属性值为@xml/file_paths(注意不需要添加后缀.xml)。如下: <provider android:name=\"androidx.core.content.FileProvider\" android:authorities=\"wang.mycroft.fileprovider\" android:exported=\"false\" android:grantUriPermissions=\"true\"> <meta-data android:name=\"android.support.FILE_PROVIDER_PATHS\" android:resource=\"@xml/file_paths\" /> </provider> 3. 为一个文件生成content uri为了使用content uri分享一个文件到另外的应用程序,你的app必须生成content uri。为了生成content uri,为这个文件构造一个File对象,传入FileProvider.getUriForFile()方法中,得到一个URI对象。你可以将得到的URI添加到一个Intent中,然后发送到另外的应用程序。收到URI的应用程序,可以通过调用ContentResolver.openFileDescriptor()得到一个ParcelFileDescriptor对象,用于打开文件和获取其中的内容。 例如,假定你的app使用一个FileProvider分享文件到其他app中,authority值为wang.mycroft.fileprovier。为了获取在内部存储区域的images/子目录中的文件default_image.jpg的content uri,使用如下代码: File imagePath = new File(Context.getFilesDir(), \"images\"); File newFile = new File(imagePath, \"default_image.jpg\"); Uri contentUri = FileProvider.getUriForFile(getContext(), \"wang.mycroft.fileprovider\", newFile); 最后得到的content uri的值是:content://wang.mycroft.fileprovider/images/default_image.jpg。 4. 为一个uri赋予临时权限为了为FileProvider.getUriForFile()得到的content uri赋予访问权限,需要如下步骤: 为一个content uri调用Context.grantUriPermission(package, Uri, mode_flags),使用期望的标记(flags)。这样就为指定的包赋予了content uri临时的访问权限。标记(flags)可以设置的值为:Intent.FLAG_GRANT_READ_URI_PERMISSION和(或) Intent.FLAG_GRANT_WRITE_URI_PERMISSION。权限保留到你调用revokeUriPermission()或者直到设备重启。 调用Intent.setData()将content uri添加到Intent中。 调用Intent.setFlags()设置Intent.FLAG_GRANT_READ_URI_PERMISSION和(或) Intent.FLAG_GRANT_WRITE_URI_PERMISSION。最后将Intent发送到另外的app中。大多数时候,你会通过Activity.setResult()使用。 content uri的Activity所在的栈保持活跃状态,那么权限就会一直会被保留。当任务栈结束,权限将自动移除。权限会被赋予给Activity所在app的所有组件。 5. 提供content uri给其他应用会有多种方法将一个content uri提供给其他app。一个通用的方法是通过调用startActivityForResult(),其他应用通过发送一个Intent启动我们app的Activity。作为相应,我们的app将直接返回一个content uri给对方的app,或者提供一个界面,让用户选择文件。在后一种情况下,一旦用户选择我们app的文件,我们将提供文件的content uri。在两种情况下,我们的app都会通过setResult()返回带有content uri的Intent。 你也可以将content uri放在ClipData中。然后将ClipData添加到Intent发送到指定app。通过调用Intent.setClipData()即可。可以在Intent添加多个ClipData。当你调用Intent.setFlags()设置临时权限时,同样的权限将被设置到所有的content uri中。 源码FileProvider的源码比较简单,反而我觉得应该更多的了解ContentResolver,FileDescriptor,ParcelFileDescriptor的使用,这是FileProvider的基础知识。所以这里不分析源码,后面有机会再深入了解。 使用举几个我们在开发过程中,实际会遇到的问题。 下面的代码中都是用了Intent.addFlags(int)添加Intent.FLAG_GRANT_READ_URI_PERMISSION或Intent.FLAG_GRANT_WRITE_URI_PERMISSION权限,这样就为Intent中所有的Uri和ClipData赋予了临时权限。另外还有一种方法是使用Context.grantUriPermission(String, Uri)来单独为某一个package(包/app)赋予Uri的访问权限。两者必有其一,不用重复添加。 前提在manifest中添加FileProvider: <provider android:name=\"androidx.core.content.FileProvider\" android:authorities=\"wang.mycroft.fileprovider.fileprovider\" android:exported=\"false\" android:grantUriPermissions=\"true\"> <meta-data android:name=\"android.support.FILE_PROVIDER_PATHS\" android:resource=\"@xml/file_paths\" /> </provider> 下面是res/xml/file_paths.xml的内容: <?xml version=\"1.0\" encoding=\"utf-8\"?> <paths> <files-path name=\"image\" path=\"image_file\" /> <external-files-path name=\"apk\" path=\"apk_file\" /> </paths> 1. 下载apk,调用系统安装因为无法再使用Uri.fromFile(File),所以就必须使用FileProvider。如下所示,得到apk文件的content uri,然后启动Android安装器。另外需要注意,添加安装文件的权限。 private fun installApk(file: File) &#123; val uri = FileProvider.getUriForFile(this, \"wang.mycroft.fileprovider.fileprovider\", file) val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE) installIntent.data = uri installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val packageList = packageManager.queryIntentActivities(installIntent, 0) if (packageList.size > 0) &#123; startActivity(installIntent) &#125; &#125; <!-- 请求安装APK的权限,API29舍弃,应该使用PackageInstaller --> <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" /> 2. 调用相机拍照下面将app私有文件提供给相机,拍照后进行保存。 private var tempPhotoUri: Uri? = null private var tempFile: File? = null private fun takePhoto() &#123; tempFile = File(File(filesDir, \"image_file\"), \"$&#123;UUID.randomUUID()&#125;.jpg\") if (!tempFile?.parentFile?.exists()!! && tempFile?.parentFile?.mkdirs()!!) &#123; return &#125; tempPhotoUri = FileProvider.getUriForFile(this, \"wang.mycroft.fileprovider.fileprovider\", tempFile!!) val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) intent.putExtra(MediaStore.EXTRA_OUTPUT, tempPhotoUri) intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) val packageList = packageManager.queryIntentActivities(intent, 0) if (packageList.size > 0) &#123; startActivityForResult(intent, 1) &#125; &#125; override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) &#123; super.onActivityResult(requestCode, resultCode, data) if (requestCode == 1) &#123; // 回收权限 revokeUriPermission(tempPhotoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) // 有可能返回结果为Activity.RESULT_CANCEL if (resultCode == Activity.RESULT_OK) &#123; val option = BitmapFactory.Options() option.inSampleSize = 4 image.setImageBitmap(BitmapFactory.decodeFile(tempFile?.absolutePath, option)) &#125; &#125; &#125; 3. 图片读取并压缩在如下代码中,读取一个content uri的图片文件,进行压缩,并显示在屏幕上。 private fun compressImageUri(imageUri: Uri) &#123; // 打开流 contentResolver.openInputStream(imageUri)?.let &#123; // 仅仅读取尺寸 val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeStream(it, null, options) // 读取到了尺寸,设置inSampleSize options.inJustDecodeBounds = false val screenWidth = getScreenWidth() if (screenWidth != -1) &#123; options.inSampleSize = options.outWidth / screenWidth &#125; // 真正的读取内容 val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri), null, options) bitmap.let &#123; // 显示在ImageView上 image.setImageBitmap(bitmap) &#125; &#125; &#125; private fun getScreenWidth(): Int &#123; val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager val point = Point() wm.defaultDisplay.getRealSize(point) return point.x &#125; 总结我们在app之间共享内容最多的应该就是图片了,而Android 7.0开始,不允许直接使用file:///的URI进行共享,这样会触发FileUriExposedException。取而代之的是使用content uri。避免了文件安全问题,但是也增加了开发成本,当然也是我们必须学习的一环。 FileProvider的使用其实非常简单,难以理解的是Uri的操作。但是作为安卓开发工作者要接受一个比较重要的概念:避免直接使用文件,一切使用Uri来共享内容,并赋予访问权限。","categories":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"FileProvider","slug":"FileProvider","permalink":"https://blog.mycroft.wang/tags/FileProvider/"}]},{"title":"三、View布局","slug":"三、View布局","date":"2019-08-24T13:51:24.000Z","updated":"2023-06-13T07:01:29.492Z","comments":true,"path":"2019/08/24/san-view-bu-ju/","link":"","permalink":"https://blog.mycroft.wang/2019/08/24/san-view-bu-ju/","excerpt":"","text":"View布局前言什么是layout布局?前面,我们通过measure测量得到了View的尺寸,那么View到底是放在哪个位置上的呢?这就是layout的功能,确定View在屏幕上的位置(通常是相对于其parent的位置)。 谁来布局不同于measure,View的布局并不是在View内部设置,而是在其parent内确定。这也是合理的,因为ViewGroup的作用就是管理View在其内部的布局。 明白了这个概念,下面我们来看看layout的一些相关方法。 layout相关的方法说明1. layout(int l, int t, int r, int b) Assign a size and position to a view and all of its descendants. 翻译:为View及其children指定尺寸和位置。 This is the second phase of the layout mechanism. (The first is measuring). In this phase, each parent calls layout on all of its children to position them. This is typically done using the child measurements that were stored in the measure pass(). 翻译:这是布局机制的第二阶段(第一阶段是测量)。在这个阶段中,每个parent调用它所有children的layout方法,设置这些children的位置。 Derived classes should not override this method. Derived classes with children should override onLayout. In that method, they should call layout on each of their children. 翻译:子类不应该重写layout方法,而是重写onLayout方法。在onLayout方法中,它应该调用它所有的children的layout(int l, int t, int r, int b)方法 谁使用View的parent用来设置这个View在parent中的位置。 用来做什么parent设置View在parent的位置。 2. onLayout(boolean changed, int left, int top, int right, int bottom) Called from layout when this view should assign a size and position to each of its children. 翻译:在View的layout(int, int, int, int)中被调用,用于指定每个children的尺寸和位置。 谁使用View自身的layout调用时,自动会被调用 用来做什么通常是parent用来确定它的children在它里面的位置(相对位置)。 特别注意第一个参数changed指的是:这个View的尺寸或(和)位置改变了,这通常表明可能进行了多次无意义的layout 其他参数都是child相对于parent的位置(相对位置)。 总结 layout(int, int, int, int)是parent调用,child在parent中的位置 parent则应该在onLayout(boolean, int, int, int, int)中调用其child.layout(int, int, int, int) FrameLayout源码分析自定义一个ViewGroup我也暂时没有好的想法,这里来分析一下Android自带的布局中最简单的FrameLayout。 FrameLayout is designed to block out an area on the screen to display a single item. Generally, FrameLayout should be used to hold a single child view, because it can be difficult to organize child views in a way that’s scalable to different screen sizes without the children overlapping each other. You can, however, add multiple children to a FrameLayout and control their position within the FrameLayout by assigning gravity to each child, using the android:layout_gravity attribute. 翻译:FrameLayout被设计用来为单独的一个View划分出一个区域。通常FrameLayout应该用于包含单独的一个View,因为对于扩展到多个不同的屏幕尺寸来说,FrameLayout则很难组织它的child views以保证相互不重叠。当然一页可以添加多个children到FrameLayout中,并且为每个child指定gravity属性来控制他们的位置。 Child views are drawn in a stack, with the most recently added child on top. The size of the FrameLayout is the size of its largest child (plus padding), visible or not (if the FrameLayout’s parent permits). Views that are android.view.View#GONE are used for sizing only if setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring() is set to true. 翻译:child views以栈形式绘制,最后添加的在最上层。FrameLayout和它尺寸最大的child尺寸相同(加上padding属性)。只有当setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()被设置为true时,设置为GONE的child才会进行测量。 @RemoteView public class FrameLayout extends ViewGroup &#123; // 默认的gravity private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START; // 是否强制测量所有的children,可以在xml中指定measureAllChildren boolean mMeasureAllChildren = false; // 下面四个属性,指的是foreground的padding,若foreground有padding,那么将影响最后的尺寸 private int mForegroundPaddingLeft = 0; private int mForegroundPaddingTop = 0; private int mForegroundPaddingRight = 0; private int mForegroundPaddingBottom = 0; // 所有layout_width或layout_height指定了match_parent属性的children,用于2次计算尺寸 private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1); // 下面四个构造器 public FrameLayout(@NonNull Context context) &#123; super(context); &#125; public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) &#123; this(context, attrs, 0); &#125; public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) &#123; this(context, attrs, defStyleAttr, 0); &#125; public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) &#123; super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes); // 获取并设置measureAllChildren属性 if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) &#123; setMeasureAllChildren(true); &#125; a.recycle(); &#125; /** * 描述前景色的gravity,默认是START|TOP,可以通过foregroundGravity属性设置 */ @android.view.RemotableViewMethod public void setForegroundGravity(int foregroundGravity) &#123; if (getForegroundGravity() != foregroundGravity) &#123; super.setForegroundGravity(foregroundGravity); // 获取foreground的padding属性,用于布局 final Drawable foreground = getForeground(); if (getForegroundGravity() == Gravity.FILL && foreground != null) &#123; Rect padding = new Rect(); if (foreground.getPadding(padding)) &#123; mForegroundPaddingLeft = padding.left; mForegroundPaddingTop = padding.top; mForegroundPaddingRight = padding.right; mForegroundPaddingBottom = padding.bottom; &#125; &#125; else &#123; mForegroundPaddingLeft = 0; mForegroundPaddingTop = 0; mForegroundPaddingRight = 0; mForegroundPaddingBottom = 0; &#125; // 重新布局 requestLayout(); &#125; &#125; /** * 返回一个默认MATCH_PARENT的FrameLayout.LayoutParams * child没有LayoutParams时使用这个 */ @Override protected LayoutParams generateDefaultLayoutParams() &#123; return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); &#125; /** * 下面四个是获取FrameLayout的padding,将foreground计算在内 * Android内部使用,只用于FrameLayout和内部的屏幕layout */ int getPaddingLeftWithForeground() &#123; return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) : mPaddingLeft + mForegroundPaddingLeft; &#125; int getPaddingRightWithForeground() &#123; return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) : mPaddingRight + mForegroundPaddingRight; &#125; private int getPaddingTopWithForeground() &#123; return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) : mPaddingTop + mForegroundPaddingTop; &#125; private int getPaddingBottomWithForeground() &#123; return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) : mPaddingBottom + mForegroundPaddingBottom; &#125; /** * 真正的measure过程 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) &#123; int count = getChildCount(); // 这句话的意思是:FrameLayout的尺寸需要根据children的尺寸确定(长或宽不是确定的) // 那么就需要测量两次: // 1. 找出最大的child,FrameLayout尺寸根据最大的child得出 // 2. 重新测量所有match_parent的children final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; // 用于存储需要重新测量的children(match_parent) mMatchParentChildren.clear(); int maxHeight = 0; int maxWidth = 0; int childState = 0; for (int i = 0; i < count; i++) &#123; final View child = getChildAt(i); // View不是GONE,或者强制测量所有的children时才进行测量 if (mMeasureAllChildren || child.getVisibility() != GONE) &#123; // 测量一个child measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); // 获取LayoutParams final LayoutParams lp = (LayoutParams) child.getLayoutParams(); // 计算得到maxWidth maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); // 计算得到maxHeight maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); // 计算childState childState = combineMeasuredStates(childState, child.getMeasuredState()); // 找出需要测量两次的children if (measureMatchParentChildren) &#123; if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) &#123; mMatchParentChildren.add(child); &#125; &#125; &#125; &#125; // 在宽高上加上FrameLayout本身的padding maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); // 宽高不能小于最小值(background和minHeight检测) maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // 宽高不能小于最小值(foreground检测) final Drawable drawable = getForeground(); if (drawable != null) &#123; maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); &#125; // 确定FrameLayout的尺寸 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); // 二次测量match_parent的children,下面的一些就不说了,说一下这个count count = mMatchParentChildren.size(); // 这里判断count>1并不是count>0,是因为 // 1.如果没有MATCH_PARENT,那么count==0 // 2.如果有MATCH_PARENT,那么count=1的话,那么child就是尺寸最大的,不需要再次测量 if (count > 1) &#123; for (int i = 0; i < count; i++) &#123; final View child = mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) &#123; final int width = Math.max(0, getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); &#125; else &#123; childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); &#125; final int childHeightMeasureSpec; if (lp.height == LayoutParams.MATCH_PARENT) &#123; final int height = Math.max(0, getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height, MeasureSpec.EXACTLY); &#125; else &#123; childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); &#125; child.measure(childWidthMeasureSpec, childHeightMeasureSpec); &#125; &#125; &#125; /** * 我们真正关心的地方 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) &#123; layoutChildren(left, top, right, bottom, false /* no force left gravity */); &#125; // 真正的逻辑执行地方 void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) &#123; final int count = getChildCount(); // 获取padding用于layout 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++) &#123; final View child = getChildAt(i); // 只layout非GONE的View if (child.getVisibility() != GONE) &#123; final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); int childLeft; int childTop; int gravity = lp.gravity; // 若无gravity,则设置默认的gravity if (gravity == -1) &#123; gravity = DEFAULT_CHILD_GRAVITY; &#125; // 布局方向,一般是从左至右 final int layoutDirection = getLayoutDirection(); // absoluteGravity,忽略布局方向,可以认为是horizontalGravity final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; // 计算horizontal方向,即childLeft switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) &#123; // centerHorizontal,计算出在中间位置是的childLeft case Gravity.CENTER_HORIZONTAL: childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin; break; // right/end,注意,实际上一定会执行下面的if case Gravity.RIGHT: if (!forceLeftGravity) &#123; childLeft = parentRight - width - lp.rightMargin; break; &#125; // 默认是start|top case Gravity.LEFT: default: childLeft = parentLeft + lp.leftMargin; &#125; // 计算vertical方向,即childTop switch (verticalGravity) &#123; case Gravity.TOP: childTop = parentTop + lp.topMargin; break; case Gravity.CENTER_VERTICAL: childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin; break; case Gravity.BOTTOM: childTop = parentBottom - height - lp.bottomMargin; break; default: childTop = parentTop + lp.topMargin; &#125; // 进行layout child.layout(childLeft, childTop, childLeft + width, childTop + height); &#125; &#125; &#125; /** * 设置是否measure所有的children,默认false,不measure设置GONE的View */ @android.view.RemotableViewMethod public void setMeasureAllChildren(boolean measureAll) &#123; mMeasureAllChildren = measureAll; &#125; /** * 返回是否measure所有的children,deprecated */ @Deprecated public boolean getConsiderGoneChildrenWhenMeasuring() &#123; return getMeasureAllChildren(); &#125; /** * 返回是否measure所有的children */ public boolean getMeasureAllChildren() &#123; return mMeasureAllChildren; &#125; /** * 根据xml配置,获取LayoutParams */ @Override public LayoutParams generateLayoutParams(AttributeSet attrs) &#123; return new FrameLayout.LayoutParams(getContext(), attrs); &#125; /** * 不应该拦截press显示状态 */ @Override public boolean shouldDelayChildPressedState() &#123; return false; &#125; // 检查LayoutParams是否是FrameLayout.LayoutParams @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) &#123; return p instanceof LayoutParams; &#125; // 转换LayoutParams @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) &#123; if (sPreserveMarginParamsInLayoutParamConversion) &#123; if (lp instanceof LayoutParams) &#123; return new LayoutParams((LayoutParams) lp); &#125; else if (lp instanceof MarginLayoutParams) &#123; return new LayoutParams((MarginLayoutParams) lp); &#125; &#125; return new LayoutParams(lp); &#125; // 用于accessibility @Override public CharSequence getAccessibilityClassName() &#123; return FrameLayout.class.getName(); &#125; // 内部的方法 @Override protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) &#123; super.encodeProperties(encoder); encoder.addProperty(\"measurement:measureAllChildren\", mMeasureAllChildren); encoder.addProperty(\"padding:foregroundPaddingLeft\", mForegroundPaddingLeft); encoder.addProperty(\"padding:foregroundPaddingTop\", mForegroundPaddingTop); encoder.addProperty(\"padding:foregroundPaddingRight\", mForegroundPaddingRight); encoder.addProperty(\"padding:foregroundPaddingBottom\", mForegroundPaddingBottom); &#125; /** * children的LayoutParams,包含了布局属性margin和layout_gravity */ public static class LayoutParams extends MarginLayoutParams &#123; /** * Value for &#123;@link #gravity&#125; indicating that a gravity has not been * explicitly specified. */ public static final int UNSPECIFIED_GRAVITY = -1; public int gravity = UNSPECIFIED_GRAVITY; public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) &#123; super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout); gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY); a.recycle(); &#125; public LayoutParams(int width, int height) &#123; super(width, height); &#125; public LayoutParams(int width, int height, int gravity) &#123; super(width, height); this.gravity = gravity; &#125; public LayoutParams(@NonNull ViewGroup.LayoutParams source) &#123; super(source); &#125; public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) &#123; super(source); &#125; public LayoutParams(@NonNull LayoutParams source) &#123; super(source); this.gravity = source.gravity; &#125; &#125; &#125; FrameLayout代码分析完了,其实非常简单,需要注意的是,FrameLayout可能会对match_parent的children进行两次测量。 我们这里着重需要关心的还是layout: 考虑到了padding,所以我们在定义View时,注意padding是我们在定义时决定的,所以不要忘记 考虑margin,margin是child相对于在parent的,需要在ViewGroup控制 考虑child的visibility属性,如果是GONE,那么ViewGroup则不应该显示 计算出child的左上角的位置,自然就可以得到右下角的位置,调用child.layout(int, int, int, int),如此循环 自定义View需要注意的点FrameLayout除了实现onMeasure(int, int)和onLayout(boolean, int, int, int, int)之外,还重写了一些方法。FrameLayout本身是一个非常简单的ViewGroup,所以它的实现可以作为定义初级ViewGroup的参考。 LayoutParams generateDefaultLayoutParams() Returns a set of default layout parameters. These parameters are requested when the View passed to {@link #addView(View)} has no layout parameters already set. If null is returned, an exception is thrown from addView. 翻译:返回默认的LayoutParams。其中的属性要求在addView(View)时传递,因为这时没有指定LayoutParams。如果为null,那么在调用addView(View)时会抛出异常。 LayoutParams generateLayoutParams(AttributeSet attrs) Returns a new set of layout parameters based on the supplied attributes set. 翻译:返回读取AttributeSet属性的LayoutParams。 这样可以在xml配置一些布局相关的属性,这些属性封装在自定义的LayoutParams中。 boolean shouldDelayChildPressedState() Return true if the pressed state should be delayed for children or descendants of this ViewGroup. Generally, this should be done for containers that can scroll, such as a List. This prevents the pressed state from appearing when the user is actually trying to scroll the content. 翻译:如果ViewGroup的children或子代(可能child是一个ViewGroup,其内又有View),如果返回true,表示自身将优先处理press状态。通常,这应该是在可以滑动的容器中返回true,例如一个ListView。这避免了press状态在用户实际上想滑动内容时提前出现。 boolean checkLayoutParams(ViewGroup.LayoutParams p)检查LayoutParams是否是我们需要的类型。如果我们自定义了一个LayoutParams,那么我们应该应该实现这个方法,保证会配合generateLayoutParams(LayoutParams)生成我们自定义的LayoutParams。 ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) Returns a safe set of layout parameters based on the supplied layout params. When a ViewGroup is passed a View whose layout params do not pass the test of {@link #checkLayoutParams(android.view.ViewGroup.LayoutParams)}, this method is invoked. This method should return a new set of layout params suitable for this ViewGroup, possibly by copying the appropriate attributes from the specified set of layout params. 翻译:基于提供的ViewGroup.LayoutParams返回一个类型安全的LayoutParams。当一个View添加到ViewGroup中,但是View的LayoutParams并不能通过checkLayoutParams(ViewGroup.LayoutParams)检查,那么这个方法将会被调用。这个方法返回了一个新的LayoutParams(通常是自定义的),并且可能会从提供的ViewGroup.LayoutParams中复制一些合适的属性。 CharSequence getAccessibilityClassName() Return the class name of this object to be used for accessibility purposes. Subclasses should only override this if they are implementing something that should be seen as a completely new class of view when used by accessibility, unrelated to the class it is deriving from. This is used to fill in AccessibilityNodeInfo#setClassName AccessibilityNodeInfo.setClassName. 大致翻译:返回用于accessibility目的当前的类名。如果需要提供这样的功能,那么子类应该实现这个方法。 LayoutParams LayoutParams are used by views to tell their parents how they want to be laid out. 翻译:LayoutParams被view用于告诉他们的parent他们想要的布局信息。 我们知道LayoutParams包含了很多布局信息,在xml中通常是以android:layout_xxx的形式存在,如android:layout_gravity:start|top。在Android读取xml生成对应的View对象时,将一些属性赋予LayoutParams,那么View的parent就可以根据LayoutParams来对它进行布局。 总结其实layout是一个相对而言比较繁琐的工作。因为要考虑到各个方面,padding,margin,gravity等等。更重要的是,通常Layout如LinearLayout,ConstraintLayout会有自己的一套布局逻辑,这个逻辑可能非常的繁琐,如ConstraintLayout。逻辑相对简单的LinearLayout也有2000多行的代码。 layout的相关方法使用很简单,难的是需要实现自己的逻辑。这篇文章分析了简单的FrameLayout,我们在定义时,可以参考FrameLayout的实现,同时如果想要更好的定义ViewGroup,建议多阅读其他优秀实现的源码。 参考文章小Demo小知识-android:foreground与android:background","categories":[{"name":"Android - View","slug":"Android-View","permalink":"https://blog.mycroft.wang/categories/Android-View/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"View","slug":"View","permalink":"https://blog.mycroft.wang/tags/View/"},{"name":"measure","slug":"measure","permalink":"https://blog.mycroft.wang/tags/measure/"},{"name":"layout","slug":"layout","permalink":"https://blog.mycroft.wang/tags/layout/"},{"name":"draw","slug":"draw","permalink":"https://blog.mycroft.wang/tags/draw/"},{"name":"touch event","slug":"touch-event","permalink":"https://blog.mycroft.wang/tags/touch-event/"}]},{"title":"二、View测量","slug":"二、View测量","date":"2019-08-24T06:34:31.000Z","updated":"2023-06-13T07:01:29.493Z","comments":true,"path":"2019/08/24/er-view-ce-liang/","link":"","permalink":"https://blog.mycroft.wang/2019/08/24/er-view-ce-liang/","excerpt":"","text":"View测量前言自定义View实际上是Android给我们定下了一些规则,我们需要遵循这些规则去定义一个View,符合这个规则的View才会更好的显示。实际上,它并没有如Java的强类型般的限制我们怎么做,我们在使用中可能时长在破坏这些规则。不了解规则就导致了我们定义的View却不是我们想要的。所以定义View之前,一定要清楚明白这些规则是什么。 基础知识先来说明一下一个View显示在屏幕上需要哪些属性: View的大小(尺寸) View在屏幕上的位置(通常是相对其parent的位置) View显示内容,当然也可以没有内容 Android对于一个View如何显示在屏幕上确定了一个流程:measure -> layout -> draw,即:测量 -> 布局 -> 绘制。 关于绘制的问题,我们后面再讨论,首先来看一下测量与布局。 MeasureSpec(重点)MeasureSpec是什么?这是一个非常重要,且难以理解的概念。 MeasureSpec是View的一个静态公开内部类,先看看的说明。 A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode. There are three possible modes:UNSPECIFIED: The parent has not imposed any constraint on the child. It can be whatever size it wants.EXACTLY: The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be.AT_MOST: The child can be as large as it wants up to the specified size.MeasureSpecs are implemented as ints to reduce object allocation. This class is provided to pack and unpack the <size, mode> tuple into the int. 翻译:MeasureSpec封装了parent对child的布局(实际上是尺寸)要求。每个MeasureSpec代表宽或高的要求。一个MeasureSpec是一个mode和一个size的组合。有如下三种mode: UNSPECIFED:parent对child的尺寸不做限制。child想显示多大都可以 EXACTLY:parent确定了child的尺寸。不管child想要多大,必须按照parent给的尺寸 AT_MOST:parent对child限制了一个最大尺寸,child应该不大于这个尺寸 为了减少内存消耗,MeasureSpec使用的是int来实现。MeasureSpec类提供了封装和解封<size, mode>组合为int的方法。 从这几句话当中,我们可以提取出几点: MeasureSpec是parent对child的布局(实际上是尺寸)要求 MeasureSpec是由两部分组成:mode、size MeasureSpec实际上是int值 MeasureSpec的组成MeasureSpec是由一个32位的int值表示。其中,前2位表示mode,后30位表示size(屏幕尺寸远远小于30位组成的int值1,073,741,823,所以不用担心屏幕大小超出值的问题)。 我们知道,两位可以组成4个数,在这里00代表UNSEPECIFIED,01代表EXACTLY,10代表AT_MOST,11并没有用到。 而关于这些值的获取,MeasureSpec提供了很多方法获取,所以不用自己去计算。如下: public static int getMode(int measureSpec) &#123; return (measureSpec & MODE_MASK); &#125; public static int getSize(int measureSpec) &#123; return (measureSpec & ~MODE_MASK); &#125; public static int makeMeasureSpec(int size,@MeasureSpecMode int mode) &#123; if (sUseBrokenMakeMeasureSpec) &#123; return size + mode; &#125; else &#123; return (size & ~MODE_MASK) | (mode & MODE_MASK); &#125; &#125; public static int makeSafeMeasureSpec(int size, int mode) &#123; if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) &#123; return 0; &#125; return makeMeasureSpec(size, mode); &#125; MeasureSpec的产生直接说结果:child的MeasureSpec是parent计算得到,传给child的。child的MeasureSpec是根据parent的MeasureSpec+child的LayoutParams得到的。如下图: ViewGroup在measureChild(View, int, int)中调用了getChildMeasureSpec(int, int, int)。源码如下: /** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding. * The heavy lifting is done in getChildMeasureSpec. * * 让其中一个child开始计算自己的尺寸,同时考虑到自身的padding属性和MeasureSpec要求。 * 重要的工作在getChildMeasureSpec方法中。 */ protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) &#123; 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); &#125; /** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * 处理measureChildren的难点部分:计算出传递个child的MeasureSpec。这个方法计算出正确child的MeasureSpec。 * 这个方法的目标是结合ViewGroup的MeasureSpec信息和child的LayoutParams,获取有可能最好的结果。 * 例如,如果我们知道View的尺寸(因为MeasureSpec属性是EXACTLY), * child并且在LayoutParams指定为MATCH_PARENT,和parent一样的尺寸,那么parent应该给child一个确定的大小。 */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) &#123; 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) &#123; // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) &#123; resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; &#125; else if (childDimension == LayoutParams.MATCH_PARENT) &#123; // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; &#125; else if (childDimension == LayoutParams.WRAP_CONTENT) &#123; // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; &#125; break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) &#123; // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; &#125; else if (childDimension == LayoutParams.MATCH_PARENT) &#123; // 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; &#125; else if (childDimension == LayoutParams.WRAP_CONTENT) &#123; // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; &#125; break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) &#123; // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; &#125; else if (childDimension == LayoutParams.MATCH_PARENT) &#123; // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; &#125; else if (childDimension == LayoutParams.WRAP_CONTENT) &#123; // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; &#125; break; &#125; //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); &#125; 代码非常的长,但是还是比较容易理解。起决定因素的是ViewGroup自己的MeasureSpec,和View的LayoutParams属性。简化之后如下图所示: MeasureSpec如何影响尺寸MeasureSpec决定了View的最终尺寸(宽高)。为什么呢,怎么做到的呢,对于这点,我们需要对measure的一些方法进行解释。 View的measure相关的方法说明这里着重View而不是ViewGroup。 1. measure(int widthMeasureSpec, int heightMeasureSpec) This is called to find out how big a view should be. The parent supplies constraint information in the width and height parameters. 翻译:这个方法被调用来确定一个View多大。View的parent提供在宽高上的约束信息。 谁使用View的parent。View的parent通过自身的MeasureSpec和View的LayoutParam得到View的MeasureSpec. 做什么的使用MeasureSpec计算View的尺寸 2. onMeasure(int widthMeasureSpec, int heightMeasureSpec) Measure the view and its content to determine the measured width and the measured height. This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents. 翻译:测量View和它的内容,决定测量的宽高。这个方法被measure(int, int)调用,并且应该被子类重写,保证对View的内容有效、准确的测量。 谁使用View自身的measure(int, int)调用的。重写其中的内容 做什么的View在这个方法里面根据自身的逻辑确定自身的尺寸(宽高) 3. setMeasuredDimension(int measuredWidth, int measuredHeight) This method must be called by {@link #onMeasure(int, int)} to store the measured width and measured height. Failing to do so will trigger an exception at measurement time. 翻译:这个方法一定要在onMeasure(int, int)调用,来存储测量好的宽高。如果没有调用,将在测量时间触发异常。 谁使用View在onMeasure(int, int)方法中调用,设置宽高值。 做什么View设置确切的宽高值。 4. getDefaultSize(int size, int measureSpec) Utility to return a default size. Uses the supplied size if the MeasureSpec imposed no constraints. Will get larger if allowed by the MeasureSpec. 翻译:帮助类,用于返回一个默认的尺寸值。如果MeasureSpec是UNSPECIFIED类型(即父类不约束),那么尺寸就是默认的尺寸(第一个参数size)。如果MeasureSpec是EXACTLY和AT_MOST(父类限制了尺寸),那么就将是限制的尺寸。 谁使用View自身,通常是在根据parent给的MeasureSpec计算实际宽高值时调用 做什么根据parent给的MeasureSpec,获得默认的一个尺寸,可以用来确认最终的尺寸值。 5. getSuggestedMinimumWidth()和getSuggestedMinimumHeight() Returns the suggested minimum height that the view should use. This returns the maximum of the view’s minimum height and the background’s minimum height 翻译:返回View最小的宽/高。这个方法返回View自身设置的minHeight和背景的minHeight的较大值。 总结这些都是一些View在根据其parent传过来的MeasureSpec用于计算真正尺寸时会用的一些方法。不仅仅是MeasureSpec,多数情况下是根据我们自定义View的逻辑,来决定View应该多大的。 如我定义了一个时钟,如果时钟太小,那么它显示出来并没有任何意义,所以我希望它最小是200dp*200dp,所以这个时候,即是parent限制时钟的尺寸小于这个数,即是可能显示不全,那么我一定也要让它不小于我期望的尺寸。 可以这样理解,MeasureSpec是parent对child期望的尺寸要求,但是View也需要根据自身情况,决定如何去满足这个尺寸要求。 很多时候,使用者使用了我们不期望的属性,导致我们自定义的View显示不合理。这不是一个谁一定要遵守谁的规定,而是两者相互配合的结果。 View的尺寸前面,已经看到了真正设置View尺寸的是setMeasuredDimension(int, int)方法。而尺寸的来源,则是根据parent给的MeasureSpec加上一些限制条件的结果。这些限制条件有如背景的尺寸、设置的minHeight,业务逻辑等。 简单的自定义的View我自定义了一个简单的时钟,在onMeasure中根据parent给的MeasureSpec和自身的逻辑确定了尺寸,而大部分工作是在onDraw。所以需要明白的一点是,View的工作实际上是显示内容的,很多时候决定尺寸的恰恰是内容,如TextView通常设置了wrap_content,尺寸也随着内容的变化而变化。 class Clock : View &#123; constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) constructor( context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) : super(context, attrs, defStyleAttr, defStyleRes) companion object &#123; // 最小边长 private const val MIN_SLIDE = 200f // 时钟圆半径 private const val RADIUS = 80f // 时钟圆线宽 private const val CIRCLE_WIDTH = 1.5f // 时钟圆心圆圈半径 private const val CENTER_RADIUS = 3f // 小时刻度长度 private const val QUARTER_LENGTH = 10f // 分钟刻度长度 private const val TIME_LENGTH = 5f // 小时刻度颜色 private const val COLOR_HOUR = Color.RED // 分钟刻度颜色 private const val COLOR_MINUTE = Color.BLACK // 小时文字大小 private const val TEXT_SIZE_HOUR = 14f // 小时文字与小时刻度的间距 private const val TEXT_DIVIDER = 5f // 时针宽度 private const val WIDTH_HOUR = 6f // 分针宽度 private const val WIDTH_MINUTE = 4f // 秒针宽度 private const val WIDTH_SECOND = 2f &#125; private val paint = Paint(Paint.ANTI_ALIAS_FLAG) override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) &#123; super.onMeasure(widthMeasureSpec, heightMeasureSpec) val minSlide = dp2px(MIN_SLIDE) // 计算尺寸,保证边长不能小于MIN_SLIDE var width = getSize(suggestedMinimumWidth, widthMeasureSpec) var height = getSize(suggestedMinimumHeight, heightMeasureSpec) if (width < minSlide) &#123; width = minSlide.toInt() &#125; if (height < minSlide) &#123; height = minSlide.toInt() &#125; // 设置最终的尺寸 setMeasuredDimension(width, height) &#125; /** * 根据MeasureSpec得到理想的尺寸 * * @param size 尺寸默认值 * @param measureSpec parent给的MeasureSpec * @return */ private fun getSize(size: Int, measureSpec: Int): Int &#123; var result = size val specMode = MeasureSpec.getMode(measureSpec) val specSize = MeasureSpec.getSize(measureSpec) when (specMode) &#123; MeasureSpec.AT_MOST, MeasureSpec.UNSPECIFIED -> result = size MeasureSpec.EXACTLY -> result = specSize &#125; return result &#125; /** * 存放文字的尺寸 */ private val textBounds = Rect() override fun onDraw(canvas: Canvas?) &#123; super.onDraw(canvas) // 获取尺寸 val width = measuredWidth val height = measuredHeight val radius = dp2px(RADIUS) val circleWidth = dp2px(CIRCLE_WIDTH) val centerRadius = dp2px(CENTER_RADIUS) val c = canvas!! c.save() // 将坐标轴移动到View中心 c.translate(width.shr(1).toFloat(), height.shr(1).toFloat()) // 画刻度 drawScale(c, radius, circleWidth) // 画时钟外圆 drawCircle(c, radius, circleWidth) // 画时针、分针、秒针 drawTimeHand(c, radius) // 画中心圆 drawCenterCircle(c, centerRadius) c.restore() // 保证实时更新 invalidate() &#125; /** * 画刻度 * * @param c canvas * @param radius 半径 * @param circleWidth 外圆的宽度 */ private fun drawScale(c: Canvas, radius: Float, circleWidth: Float) &#123; val quarterLength = dp2px(QUARTER_LENGTH) val timeLength = dp2px(TIME_LENGTH) val hourTextSize = dp2px(TEXT_SIZE_HOUR) val textDivider = dp2px(TEXT_DIVIDER) for (i in 0..59) &#123; if (i % 5 == 0) &#123; paint.color = COLOR_HOUR c.drawLine(radius - quarterLength, 0f, radius - circleWidth, 0f, paint) val hour = ((i / 5 + 2) % 12 + 1).toString() paint.textSize = hourTextSize paint.getTextBounds(hour, 0, hour.length, textBounds) c.save() c.translate(radius - quarterLength - textBounds.width() / 2f - textDivider, 0f) c.rotate(-i * 6f) c.drawText( hour, 0, hour.length, -textBounds.exactCenterX(), -textBounds.exactCenterY(), paint ) c.restore() &#125; else &#123; paint.color = COLOR_MINUTE c.drawLine(radius - timeLength, 0f, radius - circleWidth, 0f, paint) &#125; c.rotate(6f) &#125; &#125; /** * 画时钟外圆 * * @param c canvas * @param radius 半径 * @param circleWidth 外圆的宽度 */ private fun drawCircle(c: Canvas, radius: Float, circleWidth: Float) &#123; c.save() paint.style = Paint.Style.STROKE paint.strokeWidth = circleWidth c.drawCircle(0f, 0f, radius - circleWidth, paint) c.restore() &#125; /** * 画时针、分针、秒针 * * @param c canvas * @param radius 外圆半径 */ private fun drawTimeHand(c: Canvas, radius: Float) &#123; val now = Calendar.getInstance() now.time = Date() val hour = now.get(Calendar.HOUR) val minute = now.get(Calendar.MINUTE) val second = now.get(Calendar.SECOND) val millis = now.get(Calendar.MILLISECOND) val hourWidth = dp2px(WIDTH_HOUR) val minuteWidth = dp2px(WIDTH_MINUTE) val secondWidth = dp2px(WIDTH_SECOND) val hourLength = radius / 2 paint.style = Paint.Style.FILL // draw hour hand c.save() c.rotate((hour - 3) * 30f) c.drawRoundRect( -hourLength / 4f, -hourWidth / 2f, hourLength, hourWidth / 2f, hourWidth / 2f, hourWidth / 2f, paint ) c.restore() // draw minute hand val minuteLength = radius / 3 * 2 c.save() c.rotate((minute - 15) * 6f) c.drawRoundRect( -minuteLength / 4f, -minuteWidth / 2f, minuteLength, minuteWidth / 2f, minuteWidth / 2, minuteWidth / 2f, paint ) c.restore() // draw second hand val secondLength = radius / 3 * 2 c.save() paint.color = COLOR_HOUR c.rotate((second - 15) * 6f + millis * 0.006f) c.drawRoundRect( -secondLength / 4f, -secondWidth / 2f, secondLength, secondWidth / 2f, secondWidth / 2, secondWidth / 2f, paint ) c.restore() &#125; /** * 画中心圆 * * @param c canvas * @param centerRadius 中心圆半径 */ private fun drawCenterCircle(c: Canvas, centerRadius: Float) &#123; c.save() paint.color = COLOR_HOUR paint.style = Paint.Style.FILL c.drawCircle(0f, 0f, centerRadius, paint) c.restore() &#125; /** * dp to px * * @param dp dp * @return px */ private fun dp2px(dp: Float): Float &#123; return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) &#125; &#125; 在onMeasure中的逻辑: 根据parent给的MeasureSpec,并结合background、minWidth/minHeight计算出理想的尺寸 但是为了符合业务逻辑,如果得到的尺寸太小,则强制使用我期望的最小值。 确定计算得到的宽高 总结View的尺寸计算非常简单,因为它只是对它本身的计算,并没有child,大部分工作在onDraw里。所以,我们在定义View时,知道在onMeasure中如何计算得到想要的尺寸,再更多的学习Canvas相关的API就可以了。","categories":[{"name":"Android - View","slug":"Android-View","permalink":"https://blog.mycroft.wang/categories/Android-View/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"View","slug":"View","permalink":"https://blog.mycroft.wang/tags/View/"},{"name":"measure","slug":"measure","permalink":"https://blog.mycroft.wang/tags/measure/"},{"name":"layout","slug":"layout","permalink":"https://blog.mycroft.wang/tags/layout/"},{"name":"draw","slug":"draw","permalink":"https://blog.mycroft.wang/tags/draw/"},{"name":"touch event","slug":"touch-event","permalink":"https://blog.mycroft.wang/tags/touch-event/"}]},{"title":"一、View基础知识","slug":"一、View基础知识","date":"2019-08-24T03:41:12.000Z","updated":"2023-06-13T07:01:29.491Z","comments":true,"path":"2019/08/24/yi-view-ji-chu-zhi-shi/","link":"","permalink":"https://blog.mycroft.wang/2019/08/24/yi-view-ji-chu-zhi-shi/","excerpt":"","text":"View基础知识前言View基础知识点,完全可以参考文章自定义View基础 - 最易懂的自定义View原理系列(1),这篇文章已经非常的详细。 我这里将我理解的东西总结一下。 View与ViewGroupViewView的代码有近3万行,文件大小有1M,这是一个非常庞大的代码,我想谁也不愿意读完全部代码来分析它的机制,而是更愿意通过官方的文档来理解。 This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling. View is the base class for widgets, which are used to create interactive UI components (buttons, text fields, etc.). The {@link android.view.ViewGroup} subclass is the base class for layouts, which are invisible containers that hold other Views (or other ViewGroups) and define their layout properties. 翻译:View代表了用户界面组件的基础构建模块。View占据屏幕上的一块矩形区域,负责绘制和处理事件(屏幕触摸事件)。View是widget的基类,用于创造交互式的UI控件,如Button等。子类ViewGroup是layout的基类,layout是不可见的容器,用于包含其他View或ViewGroup,并且定义布局属性。 大致意思是(重点),View作为交互式组件,而ViewGroup则是用于布局。 ViewGroup A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the {@link android.view.ViewGroup.LayoutParams} class which serves as the base class for layouts parameters. 翻译:ViewGroup是特殊的View,用于包含其他的View(被称为children)。ViewGroup是layout的基类,也可以说是View的容器。在其中定义了ViewGroup.LayoutParams类,是layout parameters的基类。 这里再次说明了ViewGroup的任务是布局。而ViewGroup.LayoutParams则是封装了布局的参数。 强调我再换一种说法解释上面的内容: View用于显示内容,着重在于measure,draw和touch event ViewGroup用于控制布局,着重在于layout,measure和touch event View负责自身的显示 ViewGroup着重children如何在ViewGroup中显示 下面是View与ViewGroup的树形结构 Android坐标系Android的坐标系和我们数学上的坐标系有出入。 Android坐标系以屏幕左上角为起点,向右为X轴增大方向,向下为Y轴增大方向。如下图: View在ViewGroup上的坐标,则是由top,bottom,left,right决定。如下图,ABCD四个点的坐标分别是(left, top),(right, top),(right, bottom),(left, bottom)。注意,是相对于其parent的坐标,并不是相对于屏幕左上角的坐标。在View显示在屏幕上之后,可以通过对应的get方法获得值。 Android角度回顾一下角度与弧度的概念: 在Android坐标系中,顺时针为角度增大方向。如下图 总结这是View必须了解的一些基础知识,在后面的使用中一定会用到。 参考文章自定义View基础 - 最易懂的自定义View原理系列(1) Android自定义View之时钟","categories":[{"name":"Android - View","slug":"Android-View","permalink":"https://blog.mycroft.wang/categories/Android-View/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"View","slug":"View","permalink":"https://blog.mycroft.wang/tags/View/"},{"name":"measure","slug":"measure","permalink":"https://blog.mycroft.wang/tags/measure/"},{"name":"layout","slug":"layout","permalink":"https://blog.mycroft.wang/tags/layout/"},{"name":"draw","slug":"draw","permalink":"https://blog.mycroft.wang/tags/draw/"},{"name":"touch event","slug":"touch-event","permalink":"https://blog.mycroft.wang/tags/touch-event/"}]},{"title":"自定义View系列","slug":"自定义View系列","date":"2019-08-24T03:22:37.000Z","updated":"2023-06-13T07:01:29.494Z","comments":true,"path":"2019/08/24/zi-ding-yi-view-xi-lie/","link":"","permalink":"https://blog.mycroft.wang/2019/08/24/zi-ding-yi-view-xi-lie/","excerpt":"","text":"自定义View系列前言自定义View,这是一个我曾经尝试逃避的问题,但是作为Android开发,想要走得更远,那么永远也逃避不了。前两天在面试中遇到了这个问题,并没有回答得很好。所以这两天静下心来,明白了不少。 在这一系列文章中,着重理解自定义View的原理,不会进行太深入的分析。 系列文章 一、View基础知识 二、View测量 三、View布局 参考文章自定义View基础 - 最易懂的自定义View原理系列(1) 自定义View Measure过程 - 最易懂的自定义View原理系列(2) (3)自定义View Layout过程 - 最易懂的自定义View原理系列 (4)自定义View Draw过程- 最易懂的自定义View原理系列","categories":[{"name":"Android - View","slug":"Android-View","permalink":"https://blog.mycroft.wang/categories/Android-View/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"View","slug":"View","permalink":"https://blog.mycroft.wang/tags/View/"},{"name":"measure","slug":"measure","permalink":"https://blog.mycroft.wang/tags/measure/"},{"name":"layout","slug":"layout","permalink":"https://blog.mycroft.wang/tags/layout/"},{"name":"draw","slug":"draw","permalink":"https://blog.mycroft.wang/tags/draw/"},{"name":"touch event","slug":"touch-event","permalink":"https://blog.mycroft.wang/tags/touch-event/"}]},{"title":"学习计划","slug":"学习计划","date":"2019-08-22T03:03:27.000Z","updated":"2023-06-13T07:01:29.494Z","comments":true,"path":"2019/08/22/xue-xi-ji-hua/","link":"","permalink":"https://blog.mycroft.wang/2019/08/22/xue-xi-ji-hua/","excerpt":"","text":"2019年8月22日 [√]View的测量、布局、绘制 [√]View.post(Runnable)深入探讨 [√]图片压缩原理 断点续传、断点下载 2019年8月25日 阅读自定义View系列教程 [√]阅读Android网络编程-HTTP/HTTPS 阅读invalidate、postInvalidate与requestLayout浅析 阅读重学安卓:学习 View 事件分发,就像外地人上了黑车! 阅读探索 Android 自定义控件:基础图形 2019年8月29日 [√]阅读Okio好在哪 阅读Choreographer 解析 [√]阅读Java远程网络通讯协议之TCP/IP [√]阅读HTTP协议详解 [√]阅读从设计HTTPS开始分析HTTPS协议 [√]阅读漫话:如何给女朋友解释什么是BIO、NIO和AIO? 2019年9月8日 阅读这是一篇成为 git 高手的文章 2019年9月20日 参考Android自定义Dialog网络加载等待弹窗自定义dialog [√]深耕LiveData+Retrofit网络请求实战 [√]深耕RxJava2 + Retrofit2 完全指南 之 Authenticator处理与Token静默刷新,并梳理客户端认证的流程 深耕打开页面自动登录并回来-拦截器(升级版),并结合Lifecycle,RxPermissions实现原理,自己可以实现丰富的功能。同时测试,使用无View的Fragment,启动Activity从onActivityResult接受到消息之后,Fragment依赖的Activity被销毁,是否仍生效的问题。 [√]阅读接口请求身份认证的Token和RefreshToken的解决方案 2019年9月21日 阅读UML 交互图 (顺序图、通信图、鲁棒图、定时图) 2019年12月12日学习使用Toolbar完全代替TitleBar。学习点: Activity返回栈 Fragment配合Activity使用 结合DrawerLayout,NavigationView使用 结合Navigation使用 参考文章图片压缩知识梳理(1) - PNG 原理 View.Post() 的身世大揭秘 自定义控件(三) 源码分析measure流程 自定义View Measure过程 - 最易懂的自定义View原理系列(2) 自定义View系列教程 Android网络编程-HTTP/HTTPS invalidate、postInvalidate与requestLayout浅析 Android中图片压缩分析(上) Android中图片压缩分析(下) HTTP断点续传(分块传输)(HTTP头格式非常清楚) 重学安卓:学习 View 事件分发,就像外地人上了黑车! toolbar和标题和menu汇总","categories":[{"name":"学习计划","slug":"学习计划","permalink":"https://blog.mycroft.wang/categories/%E5%AD%A6%E4%B9%A0%E8%AE%A1%E5%88%92/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"View","slug":"View","permalink":"https://blog.mycroft.wang/tags/View/"}]},{"title":"Parcelable为什么效率高于Serializable","slug":"Parcelable为什么效率高于Serializable","date":"2019-08-22T02:15:23.000Z","updated":"2023-06-13T07:01:29.480Z","comments":true,"path":"2019/08/22/parcelable-wei-shi-me-xiao-lu-gao-yu-serializable/","link":"","permalink":"https://blog.mycroft.wang/2019/08/22/parcelable-wei-shi-me-xiao-lu-gao-yu-serializable/","excerpt":"","text":"Parcelable为什么效率高于Serializable前言在WanAndroid上看到每日一问 Parcelable 为什么效率高于 Serializable ?这篇文章,虽然知道Parcelable比Serializable效率高,但是一直不知道原因。这里总结一下。 相同点Parcelable和Serializable都是用于数据传输(多用于应用内传输),特别是在Android组件之间传输时,非常常用。 不同点1. API不同Serializable是Java API,而Parcelable是Android API,所以通常Serializable更通用些 2. 目的不同Serializable其实是进行Java对象序列化的,可以持久化,甚至在不同应用中传输,而Parcelable是Android为了解决对象传输效率的问题开发的,用于组件之间传输数据。 3. 效率不同Serializable使用的是反射机制,在序列化过程中会产生很多冗余对象,触发GC。 Parcelable则是将对象中所有的内容分解成可支持、可传递的基础属性,而且这些属性完全保存在内存中,效率很快。 4. Parcelable的缺点 不能持久化 实现较为复杂 一句话总结Serializable是利用反射进行对象序列化,开发简单但开销大效率低 Parcelable是将对象分解成基础属性,在内存中处理,高效但开发较为复杂 JSON虽然在应用程序中传递数据,一般都是使用对象,但是我个人认为JSON也可以用来传递数据,在JSON工具好用的今天,完全可以解决日常问题。","categories":[{"name":"Android - 序列化","slug":"Android-序列化","permalink":"https://blog.mycroft.wang/categories/Android-%E5%BA%8F%E5%88%97%E5%8C%96/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"Parcelable","slug":"Parcelable","permalink":"https://blog.mycroft.wang/tags/Parcelable/"},{"name":"Serializable","slug":"Serializable","permalink":"https://blog.mycroft.wang/tags/Serializable/"}]},{"title":"okhttp RealConnectionPool","slug":"okhttp-RealConnectionPool","date":"2019-08-19T02:32:18.000Z","updated":"2023-06-13T07:01:29.487Z","comments":true,"path":"2019/08/19/okhttp-realconnectionpool/","link":"","permalink":"https://blog.mycroft.wang/2019/08/19/okhttp-realconnectionpool/","excerpt":"","text":"okhttp RealConnectionPool前一篇知道了RealConnection是真正建立连接的地方。现在我们看看RealConnectionPool是如何管理RealConnection的呢。 属性先看看RealConnectionPool中的属性: private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */, Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory(\"OkHttp ConnectionPool\", true)); /** * The maximum number of idle connections for each address. */ private final int maxIdleConnections; private final long keepAliveDurationNs; private final Runnable cleanupRunnable = () -> &#123; while (true) &#123; long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) &#123; long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (RealConnectionPool.this) &#123; try &#123; RealConnectionPool.this.wait(waitMillis, (int) waitNanos); &#125; catch (InterruptedException ignored) &#123; &#125; &#125; &#125; &#125; &#125;; private final Deque<RealConnection> connections = new ArrayDeque<>(); final RouteDatabase routeDatabase = new RouteDatabase(); boolean cleanupRunning; Executor这个线程池的功能只有一个,运行下面的cleanupRunnable,在其中调用了cleanup(long)。cleanupRunning则标记是否在执行清理任务。 使用了Deque来存储RealConnection,只是用来存储,不要在意数据结构,可以使用Collection来代替。 RouteDatabase用于记录与目标地址无法建立连接的错误路由,用于优化。 maxIdelConnection:最大空闲连接数。 keepAliveDurationNs:空闲连接的最长空置时间。 方法再来看看方法 cleanupRunnable.run()while (true) &#123; long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) &#123; long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (RealConnectionPool.this) &#123; try &#123; RealConnectionPool.this.wait(waitMillis, (int) waitNanos); &#125; catch (InterruptedException ignored) &#123; &#125; &#125; &#125; &#125; 在这个方法中,循环调用了cleanup(long)方法,返回需要暂停的时间,如果是-1,表示不再进行清理任务。 cleanup(long) Performs maintenance on this pool, evicting the connection that has been idle the longest if either it has exceeded the keep alive limit or the idle connections limit. 翻译:维护这个连接池。如果连接已经超过Keep-Alive的显示或空置时间了,则清理空置最长的连接。 Returns the duration in nanos to sleep until the next scheduled call to this method. Returns -1 if no further cleanups are required. 翻译:返回等待下一次清理任务执行的间隔时间,暂停清理任务的线程。如果是-1,表示不再需要清理任务(如池中没有连接)。 long cleanup(long now) &#123; // 使用中的Connection数量 int inUseConnectionCount = 0; // 空闲的Connection数量 int idleConnectionCount = 0; // 空置时间最长的Connection RealConnection longestIdleConnection = null; // 最长的空置时间 long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, or the time that the next eviction is due. // 找出需要被清理的Connection,或者下一次清理任务的时间 synchronized (this) &#123; // 循环对每个RealConnection进行检查,找出最长的控制时间 for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) &#123; RealConnection connection = i.next(); // 如果这个RealConnection在使用中,则进行去检查下一个 if (pruneAndGetAllocationCount(connection, now) > 0) &#123; inUseConnectionCount++; continue; &#125; idleConnectionCount++; // 循环计算出空置的最长时间 long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) &#123; longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; &#125; &#125; // 确定最长空置时间是否需要被清理的条件 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) &#123; // 表示已经超时了,从列表中移除这个Connection,并且在下面关闭它的Socket connections.remove(longestIdleConnection); &#125; else if (idleConnectionCount > 0) &#123; // 返回下一次清理任务应该被执行的时间 return keepAliveDurationNs - longestIdleDurationNs; &#125; else if (inUseConnectionCount > 0) &#123; // 表示并没有Connection空置,所以下一次检查时间是连接空置的最长时间 return keepAliveDurationNs; &#125; else &#123; // 根本就没有连接,关闭清理任务 cleanupRunning = false; return -1; &#125; &#125; closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; &#125; 逻辑很清晰,最重要的是pruneAndGetAllocationCount(RealConnection, long)用于判断连接是否是空置的。 pruneAndGetAllocationCount(RealConnection, long) Prunes any leaked transmitters and then returns the number of remaining live transmitters on {@code connection}. Transmitters are leaked if the connection is tracking them but the application code has abandoned them. Leak detection is imprecise and relies on garbage collection. 翻译:清除任何被释放掉的Transmitter,然后返回RealConnection上剩余存活的Transmitter的数量。如果应用程序舍弃了Transimitter但是Connection仍然在引用它,那么就会出现被释放。释放是不精准的,依赖于垃圾回收机制。 大致意思是,一个RealConnection上有多个Transmitter,如果没有在使用,那么Transmitter会被垃圾回收机制处理掉。这个方法就是计算还在使用的Transmitter的数量,确定Connection是否是空置的。 下面在看看源码: private int pruneAndGetAllocationCount(RealConnection connection, long now) &#123; // 获取Transmitter列表 List<Reference<Transmitter>> references = connection.transmitters; for (int i = 0; i < references.size(); ) &#123; Reference<Transmitter> reference = references.get(i); // 判断是否被回收掉了 if (reference.get() != null) &#123; i++; continue; &#125; // 出现一个泄漏的Transmitter,是个应用层错误 TransmitterReference transmitterRef = (TransmitterReference) reference; String message = \"A connection to \" + connection.route().address().url() + \" was leaked. Did you forget to close a response body?\"; Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace); // 如果Transmitter被释放了,那么就没必要保存引用了 references.remove(i); connection.noNewExchanges = true; // 如果Connection上没有Transmitter了,表示Connection处于空置状态,长时间空置则应该被回收 if (references.isEmpty()) &#123; connection.idleAtNanos = now - keepAliveDurationNs; return 0; &#125; &#125; // 返回Connection上剩余Transmitter的数量 return references.size(); &#125; 我们再重新审视一下Transmitter的概念 Transmitter Bridge between OkHttp’s application and network layers. This class exposes high-level application layer primitives: connections, requests, responses, and streams. 翻译:OkHttp应用层与网络层的桥梁。这个类暴露了上层应用层的基本属性:连接、请求、响应和流。 This class supports {@linkplain #cancel asynchronous canceling}. This is intended to have the smallest blast radius possible. If an HTTP/2 stream is active, canceling will cancel that stream but not the other streams sharing its connection. But if the TLS handshake is still in progress then canceling may break the entire connection. 翻译:这个类支持异步取消。为了让取消动作影响最小,如果是HTTP/2的流是活跃的,那么取消动作只取消它自己的流,不会取消共享的连接。但是如果是正在进行TLS握手,那么就会取消整个连接。 在应用层,与网络层沟通使用的是Transmitter和Exchange。使用Transmitter可以建立连接。而在网络层,一个RealConnection上又有多个Transmitter,这多个Transmitter依赖于这个RealConnection,获得Exchange给应用层,表示进行一次请求/响应。 再看看其他方法最重要的几个方法看完了,再来说说其他方法 public synchronized int idleConnectionCount() &#123; int total = 0; for (RealConnection connection : connections) &#123; if (connection.transmitters.isEmpty()) total++; &#125; return total; &#125; public synchronized int connectionCount() &#123; return connections.size(); &#125; 这两个方法就不说了,只是查看Connection的数量 transmitterAcquirePooledConnection(Address, Transmitter, List, boolean) Attempts to acquire a recycled connection to {@code address} for {@code transmitter}. Returns true if a connection was acquired. 翻译:尝试获取Address,Transmitter的一个循环使用的RealConnection。返回true表示获取到了。 boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter, @Nullable List<Route> routes, boolean requireMultiplexed) &#123; assert (Thread.holdsLock(this)); for (RealConnection connection : connections) &#123; // 要求是HTTP2多路复用 if (requireMultiplexed && !connection.isMultiplexed()) continue; // 判断能否匹配 if (!connection.isEligible(address, routes)) continue; // Transmitter绑定Connection上 transmitter.acquireConnectionNoEvents(connection); return true; &#125; return false; &#125; 这个方法提供给ExchangeFinder用于匹配Transmitter和Connection put(RealConnection)void put(RealConnection connection) &#123; assert (Thread.holdsLock(this)); if (!cleanupRunning) &#123; cleanupRunning = true; executor.execute(cleanupRunnable); &#125; connections.add(connection); &#125; 添加RealConnection到连接池中,并开始清理任务。这个方法提供给ExchangeFinder,在构造RealConnection进行连接,并添加到池中。 connectionBecameIdle(RealConnection) Notify this pool that {@code connection} has become idle. Returns true if the connection has been removed from the pool and should be closed. 翻译:提醒连接池Connection处于空闲状态了。返回true表示Connection已经被从池中移除了,并且应该被关闭。 boolean connectionBecameIdle(RealConnection connection) &#123; assert (Thread.holdsLock(this)); // Connection不再被使用,或者不允许Connection空置,那么就移除它 if (connection.noNewExchanges || maxIdleConnections == 0) &#123; connections.remove(connection); return true; &#125; else &#123; // 唤醒清理任务 notifyAll(); return false; &#125; &#125; Transmitter使用这个方法判断绑定的RealConnection是否应该被关闭。 evictAll()public void evictAll() &#123; List<RealConnection> evictedConnections = new ArrayList<>(); synchronized (this) &#123; for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) &#123; RealConnection connection = i.next(); if (connection.transmitters.isEmpty()) &#123; connection.noNewExchanges = true; evictedConnections.add(connection); i.remove(); &#125; &#125; &#125; for (RealConnection connection : evictedConnections) &#123; closeQuietly(connection.socket()); &#125; &#125; 删除并关闭所有的空置的Connection connectFailed(Route, IOException) Track a bad route in the route database. Other routes will be attempted first. 翻译:在路由数据库中记录错误的路由。其他的路由会优先尝试。 public void connectFailed(Route failedRoute, IOException failure) &#123; // Tell the proxy selector when we fail to connect on a fresh connection. if (failedRoute.proxy().type() != Proxy.Type.DIRECT) &#123; Address address = failedRoute.address(); address.proxySelector().connectFailed( address.url().uri(), failedRoute.proxy().address(), failure); &#125; routeDatabase.failed(failedRoute); &#125; 总结RealConnectionPool实际上很简单,维护RealConnection的集合,提供清理任务,提供外部处理这个集合的接口。 多个Transmitter对应一个RealConnection 一个Transmitter对应一个ExchangeFinder 一个Transmitter对应一个Exchange 再来看看流程图:","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"开源","slug":"开源","permalink":"https://blog.mycroft.wang/tags/%E5%BC%80%E6%BA%90/"},{"name":"责任链模式","slug":"责任链模式","permalink":"https://blog.mycroft.wang/tags/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/"},{"name":"ConnectInterceptor","slug":"ConnectInterceptor","permalink":"https://blog.mycroft.wang/tags/ConnectInterceptor/"},{"name":"RealConnectionPool","slug":"RealConnectionPool","permalink":"https://blog.mycroft.wang/tags/RealConnectionPool/"}]},{"title":"okhttp 连接","slug":"okhttp-连接","date":"2019-08-17T12:10:10.000Z","updated":"2023-06-13T07:01:29.489Z","comments":true,"path":"2019/08/17/okhttp-lian-jie/","link":"","permalink":"https://blog.mycroft.wang/2019/08/17/okhttp-lian-jie/","excerpt":"","text":"okhttp 连接前言前面一篇,主要分析了OkHttp的整体设计,但是需要重提一句OkHttp是一个网络库。所以这篇开始,来说一说OkHttp如何建立连接和如何交换数据的。 ConnectInterceptor从前面我们知道了,ConnectInterceptor承载着OkHttp与服务器建立网络连接的任务。下面看看ConnectInterceptor的源码: public final class ConnectInterceptor implements Interceptor &#123; public final OkHttpClient client; public ConnectInterceptor(OkHttpClient client) &#123; this.client = client; &#125; @Override public Response intercept(Chain chain) throws IOException &#123; RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); // 网络层与应用层的桥梁 Transmitter transmitter = realChain.transmitter(); // 我们需要保证网络能够满足网络请求。除GET方法外,我们都需要进行严格的检查 boolean doExtensiveHealthChecks = !request.method().equals(\"GET\"); // 连接管理与事件的交换桥梁 Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks); // 建立好连接之后,交给CallServerInterceptor读取数据 return realChain.proceed(request, transmitter, exchange); &#125; &#125; 从这段代码中,实际上并不能看出来什么,但是我们可以知道,这个跟Transmitter和Exchange关系莫大。 Transmitter Bridge between OkHttp’s application and network layers. This class exposes high-level application layer primitives: connections, requests, responses, and streams. 翻译:OkHttp应用层与网络层的桥梁。这个类暴露了上层应用层的基本属性:连接、请求、响应和流。 This class supports {@linkplain #cancel asynchronous canceling}. This is intended to have the smallest blast radius possible. If an HTTP/2 stream is active, canceling will cancel that stream but not the other streams sharing its connection. But if the TLS handshake is still in progress then canceling may break the entire connection. 翻译:这个类支持异步取消。为了让取消动作影响最小,如果是HTTP/2的流是活跃的,那么取消动作只取消它自己的流,不会取消共享的连接。但是如果是正在进行TLS握手,那么就会取消整个连接。 从上面的说明,可以简单的了解,但是仍然不清楚它的作用,它既然是应用层和网络层的桥梁,那么它提供给网络层的是什么,又返回给应用层了什么呢。 上面已经说明了,它提供给网络层的是应用层的Connection,Request,Response和流。而在ConnectInterceptor方法中,我们可以看出,它给应用层返回的是一个Exchange。 我们可以发现,Transmitter的构造只有一个地方: static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) &#123; RealCall call = new RealCall(client, originalRequest, forWebSocket); call.transmitter = new Transmitter(client, call); return call; &#125; 在获得一个RealCall的时候,就生成了一个Transmitter。 Exchange Transmits a single HTTP request and a response pair. This layers connection management and events on {@link ExchangeCodec}, which handles the actual I/O. 翻译:传输单个HTTP请求和响应。将连接管理部分和在ExchangeCodec的事件分层,ExchangeCodec处理IO。 大致意思应该清楚,ExchangeCodec负责IO操作,而Exchange则是处理HTTP请求和响应。 那么谁又负责连接管理部分呢? Exchange的构造反向思考,Exchange是在哪里构造的呢?查看代码,只有一个地方,是在Transmitter.newExchange(Interceptor.Chain, boolean) /** * Returns a new exchange to carry a new request and response. * 返回一个Exchange负责请求、响应 */ Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) &#123; synchronized (connectionPool) &#123; if (noMoreExchanges) &#123; throw new IllegalStateException(\"released\"); &#125; if (exchange != null) &#123; throw new IllegalStateException(\"cannot make a new request because the previous response \" + \"is still open: please call response.close()\"); &#125; &#125; ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks); Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec); synchronized (connectionPool) &#123; this.exchange = result; this.exchangeRequestDone = false; this.exchangeResponseDone = false; return result; &#125; &#125; 除了前后的一些检查、复制代码,我们可以看到,这是通过一个ExchangeFinder,构造了一个ExchangeCodec负责IO操作,并使用这个ExchangeCodec构造了Exchange。 那么Transmitter.newExchange(Interceptor.Chain, boolean)什么时候被调用的呢?是在ConnectInterceptor.intercept(Chain)中调用的,并且交由CallServerInterceptor使用。而在CallServerInterceptor进行的是在建立好连接的基础上进行请求。那么我们就应该知道,Exchange内部肯定是已经建立好了连接。 特别注意的是,在构造Exchange时,有一个ExchangeFinder,它是做什么的呢? ExchangeFinder Attempts to find the connections for a sequence of exchanges. This uses the following strategies:If the current call already has a connection that can satisfy the request it is used. Using the same connection for an initial exchange and its follow-ups may improve locality.If there is a connection in the pool that can satisfy the request it is used. Note that it is possible for shared exchanges to make requests to different host names! See {@link RealConnection#isEligible} for details.If there’s no existing connection, make a list of routes (which may require blocking DNS lookups) and attempt a new connection them. When failures occur, retries iterate the list of available routes. 翻译:尝试为一系列的Exchange找到Connection连接。使用下列的策略: 如果当前有Connection能够满足请求Request,那么使用初始时的Exchange构造的Connection。 如果连接池中有Connection能够满足请求Request。注意,共享同一Connection的Exchange可能向不同的主机名发送请求。 如果没有存在的Connection,创建一个路由列表(获取需要使用阻塞DNS查询),并且尝试建立一个新的连接。如果发生错误,那么将根据路由列表重试。 这下明白了,建立连接的过程是在ExchangeFinder里面呀。看一下ExchangeFinder的代码: public ExchangeCodec find( OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) &#123; int connectTimeout = chain.connectTimeoutMillis(); int readTimeout = chain.readTimeoutMillis(); int writeTimeout = chain.writeTimeoutMillis(); int pingIntervalMillis = client.pingIntervalMillis(); boolean connectionRetryEnabled = client.retryOnConnectionFailure(); try &#123; RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks); return resultConnection.newCodec(client, chain); &#125; catch (RouteException e) &#123; trackFailure(); throw e; &#125; catch (IOException e) &#123; trackFailure(); throw new RouteException(e); &#125; &#125; /** * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated * until a healthy connection is found. * 找到可用的Connection。如果不可用,将重复查找直到找到为止。 */ private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException &#123; while (true) &#123; RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled); // If this is a brand new connection, we can skip the extensive health checks. synchronized (connectionPool) &#123; if (candidate.successCount == 0) &#123; return candidate; &#125; &#125; // Do a (potentially slow) check to confirm that the pooled connection is still good. If it // isn't, take it out of the pool and start again. if (!candidate.isHealthy(doExtensiveHealthChecks)) &#123; candidate.noNewExchanges(); continue; &#125; return candidate; &#125; &#125; /** * Returns a connection to host a new stream. This prefers the existing connection if it exists, * then the pool, finally building a new connection. * 返回一个Connection用于处理流。最好的先从已有的查找,再从连接池中查找,最后没找到则新建一个Connection */ private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException &#123; RealConnection result = null; // ... result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener); // ... return result; &#125; ExchangeFinder.find(Interceptor.Chain, boolean)得到IO操作的ExchangeCodec 在其中调用了RealConnection ExchangeFinder.findHealthyConnection(int, int, int, int, boolean, boolean) 继续调用RealConnection ExchangeFinder.findConnection(int, int, int, int, boolean)得到了一个RealConnection 然后调用RealConnection.connect(int, int, int, int, boolean, Call, EventListener)建立了真正的连接 最后通过这个RealConnection返回了的ExchangeCodec。 RealConnection现在知道了RealConnection才是真正的服务器连接。通过它进行连接服务器,建立好连接之后,通过ExchangeCodec newCodec(OkHttpClient, Interceptor.Chain)返回ExchangeCodec用于处理IO,并将其封装在Exchange中,供应用层调用(大部分是在CallServerInterceptor使用)。 在Java中,连接服务器使用的是Socket,我们查看Socket的使用。下面,我们返回来看,哪里真正调用Socket.connect(SocketAddress, int)了呢。 两处进行了连接,Platform.connectSocket(Socket, InetSocketAddress, int)和AndroidPlatform.connectSocket(Socket, InetSocketAddress, int),AndroidPlatform继承了Platform 查看Platform.connectSocket(Socket, InetSocketAddress, int)的使用,就发现是RealConnection.connectSocket(int, int, Call, EventListener) 最后直接或间接都被RealConnection.connect(int, int, int, int, boolean, Call, EventListener)调用 终于,我们来看一下,一个完整的连接过程 在这张图上,我们可以看到,这个跟Connection和ConnectionPool并没有任何关系。而RealConnection实际上实现了Connection接口 Connection The sockets and streams of an HTTP, HTTPS, or HTTPS+HTTP/2 connection. May be used for multiple HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy. 翻译:HTTP,HTTPS,HTTPS+HTTP/2连接的Socket和流。可能会被用于多个HTTP请求/相应交换。可以直接连接服务器,也可以通过代理连接服务器。 Typically instances of this class are created, connected and exercised automatically by the HTTP client. Applications may use this class to monitor HTTP connections as members of a {@linkplain ConnectionPool connection pool}. 翻译:通常,这个类会自动被OkHttp创建、连接、使用。应用层可以使用这个类作为ConnectionPool的成员来监听HTTP连接。 Do not confuse this class with the misnamed {@code HttpURLConnection}, which isn’t so much a connection as a single request/response exchange. 翻译:不要和HttpURLConnection弄混了,后者不是一个单独的请求响应交换的连接。 下面是Connection的代码: public interface Connection &#123; /** * Returns the route used by this connection. */ Route route(); /** * Returns the socket that this connection is using. Returns an &#123;@linkplain * javax.net.ssl.SSLSocket SSL socket&#125; if this connection is HTTPS. If this is an HTTP/2 * connection the socket may be shared by multiple concurrent calls. */ Socket socket(); /** * Returns the TLS handshake used to establish this connection, or null if the connection is not * HTTPS. */ @Nullable Handshake handshake(); /** * Returns the protocol negotiated by this connection, or &#123;@link Protocol#HTTP_1_1&#125; if no protocol * has been negotiated. This method returns &#123;@link Protocol#HTTP_1_1&#125; even if the remote peer is * using &#123;@link Protocol#HTTP_1_0&#125;. */ Protocol protocol(); &#125; 可以看到,这个类提供了连接的Socket和一些其他属性。使用这个Socket可以直接与服务器进行数据交换。这个类抽象了连接。同时,OkHttp也告诉我们,通常只是通过Connection来用于监听,所以实际上,它并没有做什么实质性的工作,存在的目的是为应用层提供连接的一些信息。 ConnectionPool Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that share the same {@link Address} may share a {@link Connection}. This class implements the policy of which connections to keep open for future use. 翻译:管理HTTP和HTTP/2的连接的重复使用,以减少网络延迟。使用同一地址的HTTP请求可能会分享同一个Connection。这个类实现了哪些连接应该保持打开以供后来使用的策略。 现在,我们知道了ConnectionPool是用来管理连接的,然而,它并没有做实质性的工作,代理了RealConnectionPool,所以实际的连接池是RealConnectionPool。 提一下,ConnectionPool默认可以保持5个空的连接,最长5分钟的空置时间。根据需要可以更改这两个属性。 继承与代理可以思考一下,在应用层,我们是使用的Connection和ConnectionPool,而在OkHttp内部,使用的却是RealConnection和RealConnectionPool。我们就可以明白了,OkHttp在尽量少的暴露内部API。 无论是RealConnection继承Connection,还是ConnectionPool代理RealConnectionPool,都是为了让使用者尽量少的知道内部的实现。 RealConnectionPool明白了OkHttp的设计之后,就比较容易理解每个类的功能了。 RealConnectionPool对外,提供和ConnectionPool一样的功能。那么对内是怎么实现连接池的功能呢?下一篇详说。 总结现在我们来简单总结一下,每个类的抽象概念。 下面是应用层的概念: ConnectionInterceptor:实现链的功能,承载着OkHttp建立连接的任务 Transmitter:应用层与网络层的桥梁,通知网络层进行连接,对应用层提供Exchange Exchange:单个网络请求,使用它可以进行IO操作。在内部,使用ExchangeCodec进行IO操作 下面是网络层的概念: ExchangeFinder:为Exchange找到合适的RealConnection,并使用RealConnection构造ExchangeCodec RealConnection:进行网络连接,并提供网络连接的属性 而Connection和ConnectionPool则是对外提供使用。上面这些类并不提供外部使用。","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"开源","slug":"开源","permalink":"https://blog.mycroft.wang/tags/%E5%BC%80%E6%BA%90/"},{"name":"责任链模式","slug":"责任链模式","permalink":"https://blog.mycroft.wang/tags/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/"},{"name":"ConnectInterceptor","slug":"ConnectInterceptor","permalink":"https://blog.mycroft.wang/tags/ConnectInterceptor/"},{"name":"Transmitter","slug":"Transmitter","permalink":"https://blog.mycroft.wang/tags/Transmitter/"},{"name":"Exchange","slug":"Exchange","permalink":"https://blog.mycroft.wang/tags/Exchange/"},{"name":"RealConnection","slug":"RealConnection","permalink":"https://blog.mycroft.wang/tags/RealConnection/"}]},{"title":"okhttp 缓存","slug":"okhttp-缓存","date":"2019-08-17T02:16:18.000Z","updated":"2023-06-13T07:01:29.488Z","comments":true,"path":"2019/08/17/okhttp-huan-cun/","link":"","permalink":"https://blog.mycroft.wang/2019/08/17/okhttp-huan-cun/","excerpt":"","text":"okhttp 缓存前言缓存的使用可以减少我们程序请求服务器、读取文件等耗时IO的次数,能够极大的提高程序的运行速度、性能,除了在OkHttp中使用了缓存,在很多优秀的库中都使用了缓存,图片库最为明显,如glide,fresco在这方面都是很极致的使用者,又如Android控件的ListView,RecyclerView使用缓存很大的提高了界面的流畅度。 所以,理解缓存、使用缓存是必备的知识。 在OkHttp中,CacheInterceptor担负着缓存的作用。这一章,深入讨论CacheInterceptor的使用。 缓存的操作首先应该明白缓存应该具备的最基本的操作:添加、删除、更新、获取。这很像CRUD。当然在实际情况中,我们会额外添加操作,如设置缓存的大小。 CacheInterceptor Serves requests from the cache and writes responses to the cache. 翻译:使用缓存处理请求,将结果写入缓存。 下面是CacheInterceptor的源码(删除了非逻辑代码)。 public final class CacheInterceptor implements Interceptor &#123; // OkHttp的内部缓存接口 @Nullable final InternalCache cache; public CacheInterceptor(@Nullable InternalCache cache) &#123; this.cache = cache; &#125; @Override public Response intercept(Chain chain) throws IOException &#123; // 获取是否有对应Request的Response缓存对象 Response cacheCandidate = cache != null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); // 缓存策略,弄清楚是否到底使用网络、缓存,还是两者都使用 CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); // 得到的网络请求,若不使用网络(客户端强制使用缓存)则为null Request networkRequest = strategy.networkRequest; // 得到对应的缓存响应,用于返回或者需要被更新,如果这次请求不使用缓存,则为null Response cacheResponse = strategy.cacheResponse; if (cache != null) &#123; cache.trackResponse(strategy); &#125; // 如果应用不使用缓存,清理资源 if (cacheCandidate != null && cacheResponse == null) &#123; closeQuietly(cacheCandidate.body()); &#125; // 如果不使用网络请求,并且没有缓存,则返回504错误 if (networkRequest == null && cacheResponse == null) &#123; return new Response.Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(504) .message(\"Unsatisfiable Request (only-if-cached)\") .body(Util.EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); &#125; // 如果不使用网络,并且有缓存,则返回缓存结果 if (networkRequest == null) &#123; return cacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); &#125; // 前面处理了缓存,现在开始网络请求 Response networkResponse = null; try &#123; // 交给下一个Interceptor处理,最后会交给CallServerInterceptor返回一个从网络请求得到的Response networkResponse = chain.proceed(networkRequest); &#125; finally &#123; // 如果出现网络错误,清理资源 if (networkResponse == null && cacheCandidate != null) &#123; closeQuietly(cacheCandidate.body()); &#125; &#125; // 获得网络结果,如果是304,则更新、并返回缓存结果 if (cacheResponse != null) &#123; if (networkResponse.code() == HTTP_NOT_MODIFIED) &#123; Response response = cacheResponse.newBuilder() .headers(combine(cacheResponse.headers(), networkResponse.headers())) .sentRequestAtMillis(networkResponse.sentRequestAtMillis()) .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); // 在组合和网络结果的header之后更新缓存 cache.trackConditionalCacheHit(); cache.update(cacheResponse, response); return response; &#125; else &#123; // 清理资源 closeQuietly(cacheResponse.body()); &#125; &#125; // 构造返回的结果Response Response response = networkResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (cache != null) &#123; // 若根据HTTP协议可以缓存,那么进行缓存 if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) &#123; // Offer this request to the cache. CacheRequest cacheRequest = cache.put(response); return cacheWritingResponse(cacheRequest, response); &#125; // 如果是不可缓存的method,则移除缓存 if (HttpMethod.invalidatesCache(networkRequest.method())) &#123; try &#123; cache.remove(networkRequest); &#125; catch (IOException ignored) &#123; // The cache cannot be written. &#125; &#125; &#125; // 返回结果 return response; &#125; &#125; 主要的逻辑是: 首先判断是否需要使用缓存结果: 需要,是否有缓存结果 有,返回 无,返回504错误 不需要,进行网络请求,分析。判断是否之前有缓存,并且网络结果code是304(资源没有修改过) 是,更新缓存,并且返回更新后的缓存结果 否,返回网络结果,判断是否可以将网络结果缓存 是,缓存网络返回结果 否,不缓存 这些条件的由来,是对HTTP协议的理解。根据HTTP协议,判断是否使用缓存、是否更新缓存、是否删除缓存、缓存是否过期等。 InternalCache了解了OkHttp的缓存逻辑,我们来看看实际它是如何缓存的。在CacheInterceptor使用了InternalCache这个接口来进行缓存。 OkHttp’s internal cache interface. Applications shouldn’t implement this: instead use {@link okhttp3.Cache}. 翻译:OkHttp的内部缓存接口。应用层应该使用Cache类。 下面是InternalCache的代码 public interface InternalCache &#123; @Nullable Response get(Request request) throws IOException; @Nullable CacheRequest put(Response response) throws IOException; void remove(Request request) throws IOException; void update(Response cached, Response network); void trackConditionalCacheHit(); void trackResponse(CacheStrategy cacheStrategy); &#125; 除了两个用于log的方法,其他四个接口满足最基本的缓存要求。 Cache Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and bandwidth. 翻译:使用文件缓存HTTP和HTTPS响应,节约时间和带宽。 代码很长,说明主要的几个点: 1. Cache内部有一个OkHttp需要的InternalCache匿名对象在这个匿名对象中,所有方法都是代理,都调用Cache本身的方法。下面是源码: final InternalCache internalCache = new InternalCache() &#123; @Override public @Nullable Response get(Request request) throws IOException &#123; return Cache.this.get(request); &#125; @Override public @Nullable CacheRequest put(Response response) throws IOException &#123; return Cache.this.put(response); &#125; @Override public void remove(Request request) throws IOException &#123; Cache.this.remove(request); &#125; @Override public void update(Response cached, Response network) &#123; Cache.this.update(cached, network); &#125; @Override public void trackConditionalCacheHit() &#123; Cache.this.trackConditionalCacheHit(); &#125; @Override public void trackResponse(CacheStrategy cacheStrategy) &#123; Cache.this.trackResponse(cacheStrategy); &#125; &#125;; 2. Cache使用DiskLruCache来管理缓存写入到文件的内容final DiskLruCache cache; public Cache(File directory, long maxSize) &#123; this(directory, maxSize, FileSystem.SYSTEM); &#125; Cache(File directory, long maxSize, FileSystem fileSystem) &#123; this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize); &#125; DiskLruCache A cache that uses a bounded amount of space on a filesystem. Each cache entry has a string key and a fixed number of values. Each key must match the regex [a-z0-9_-]{1,64}. Values are byte sequences, accessible as streams or files. Each value must be between {@code 0} and {@code Integer.MAX_VALUE} bytes in length. 翻译:这是一个在文件系统中使用了有限空间的缓存。每一个缓存都有一个字符串的键和一定数量的值。每个键必须满足正则表达式:[a-z0-9_-]{1,64}。值是byte序列,通过文件或者流获取。每个值的长度是0-Integer.MAX_VALUE之间的byte。 The cache stores its data in a directory on the filesystem. This directory must be exclusive to the cache; the cache may delete or overwrite files from its directory. It is an error for multiple processes to use the same cache directory at the same time. 翻译:缓存将数据存入一个文件夹内的文件中。这个文件夹必须是这个缓存独享的。缓存需要删除或者重写其中的文件。同时在同一个缓存目录使用多个操作会造成错误。 很清楚,DiskLruCache是磁盘缓存的实现。 读取缓存时,我们得到的是一个DiskLruCache.Snapshot,我们通过其中的okio.Source读取缓存的内容。写入缓存时,我们得到一个DiskLruCache.Editor,通过其中okio.Sink写入缓存的内容。 这样,我们就可以将Request、Response的内容写入进行缓存,同时,将读取的缓存内容转换为Request、Response对象。 总结下图是OkHttp缓存的调用关系: CacheInterceptor通过InternalCache操作缓存,交换对象是Request和Response InternalCache则只是代理了Cache Cache将Request和Response的内容转换成明文,通过DiskLruCache保存到文件中 OkHttp在这里非常合理的使用了接口: InternalCache定义了CacheInterceptor操作缓存的接口,不提供额外的功能 Cache则是向应用层提供了操作接口,隐藏了代理InternalCache的方法 特别感谢OKHttp源码解析(六)–中阶之缓存基础","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"开源","slug":"开源","permalink":"https://blog.mycroft.wang/tags/%E5%BC%80%E6%BA%90/"},{"name":"责任链模式","slug":"责任链模式","permalink":"https://blog.mycroft.wang/tags/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/"},{"name":"Cache","slug":"Cache","permalink":"https://blog.mycroft.wang/tags/Cache/"},{"name":"缓存","slug":"缓存","permalink":"https://blog.mycroft.wang/tags/%E7%BC%93%E5%AD%98/"}]},{"title":"okhttp Interceptor接口","slug":"okhttp-Interceptor接口","date":"2019-08-16T11:53:36.000Z","updated":"2023-06-13T07:01:29.486Z","comments":true,"path":"2019/08/16/okhttp-interceptor-jie-kou/","link":"","permalink":"https://blog.mycroft.wang/2019/08/16/okhttp-interceptor-jie-kou/","excerpt":"","text":"okhttp Interceptor接口前言前面一直在说Interceptor责任链,那Interceptor到底是什么呢 Interceptor Observes, modifies, and potentially short-circuits requests going out and the corresponding responses coming back in. Typically interceptors add, remove, or transform headers on the request or response. 翻译:观察、修改、并有可能拦截Request,而且可以返回自定义的Response。通常Interceptor作用是在Request和Response上添加、删除、转换header。 下面是Interceptor的源码 public interface Interceptor &#123; // 得到Chain对象,获取请求必要的参数如Request,Call,处理得到Response Response intercept(Chain chain) throws IOException; interface Chain &#123; // 得到责任链上的Request Request request(); // 在Interceptor中调用,将其交由责任链中的下一个Interceptor处理 Response proceed(Request request) throws IOException; /** * Returns the connection the request will be executed on. This is only available in the chains * of network interceptors; for application interceptors this is always null. */ // 返回将会被用于执行的Connection。只有在责任链是networkInterceptor是会得到值,在应用层interceptor总是返回null @Nullable Connection connection(); // 得到Call对象 Call call(); // 连接超时时间 int connectTimeoutMillis(); // 得到一个新的责任链,设置了连接超时 Chain withConnectTimeout(int timeout, TimeUnit unit); // 读取超时时间 int readTimeoutMillis(); // 得到一个新的责任链,设置了读取超时 Chain withReadTimeout(int timeout, TimeUnit unit); // 写入超时时间 int writeTimeoutMillis(); // 得到一个新的责任链,设置了写入超时 Chain withWriteTimeout(int timeout, TimeUnit unit); &#125; &#125; Interceptor的代码很简单,通常,我们需要实现Interceptor实现我们自己的逻辑时,我们会调用Chain.proceed(Request)方法,表示交由下一个Interceptor进行处理。我们可以处理Request,也可以处理由下一个Interceptor返回的结果Response。如果我们自己能够处理(这样的情况极少),那么我们可以不用调用Chain.proceed(Request)方法,交由下一Interceptor处理。 Interceptor.ChainChain是什么呢,也没有注释,我们来看一下Chain的唯一实现:RealInterceptorChain。 RealInterceptorChain A concrete interceptor chain that carries the entire interceptor chain: all application interceptors, the OkHttp core, all network interceptors, and finally the network caller.If the chain is for an application interceptor then {@link #connection} must be null. Otherwise it is for a network interceptor and {@link #connection} must be non-null. 翻译:这是一个确切的Interceptor.Chain实现类,负责整个Interceptor责任链的传递:通过应用层的interceptor,OkHttp的核心,所有的networkInterceptor,最后进行网络请求。如果是应用层的interceptor,那么connection()方法返回的是null,否则是一个networkInterceptor,返回的则不是null。 public final class RealInterceptorChain implements Interceptor.Chain &#123; // 责任链列表 private final List<Interceptor> interceptors; // 应用层与网络层的桥梁 private final Transmitter transmitter; // 连接管理与事件的交换桥梁 private final @Nullable Exchange exchange; // 责任链的位置 private final int index; // 请求 private final Request request; // 执行器 private final Call call; // 连接超时 private final int connectTimeout; // 读取超时 private final int readTimeout; // 写入超时 private final int writeTimeout; // 标记proceed被执行的次数,保证只能被执行一次 private int calls; public RealInterceptorChain(List<Interceptor> interceptors, Transmitter transmitter, @Nullable Exchange exchange, int index, Request request, Call call, int connectTimeout, int readTimeout, int writeTimeout) &#123; this.interceptors = interceptors; this.transmitter = transmitter; this.exchange = exchange; this.index = index; this.request = request; this.call = call; this.connectTimeout = connectTimeout; this.readTimeout = readTimeout; this.writeTimeout = writeTimeout; &#125; // 删除了与逻辑无关的代码 @Override public Response proceed(Request request) throws IOException &#123; return proceed(request, transmitter, exchange); &#125; public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange) throws IOException &#123; // 不能超过责任链数量 if (index >= interceptors.size()) throw new AssertionError(); // 标记当前Interceptor.Chain已经被执行过了 calls++; // 保证需要执行网络请求时,Exchange已经准备好了 if (this.exchange != null && !this.exchange.connection().supportsUrl(request.url())) &#123; throw new IllegalStateException(\"network interceptor \" + interceptors.get(index - 1) + \" must retain the same host and port\"); &#125; // 保证Chain.proceed()只被执行过一次 if (this.exchange != null && calls > 1) &#123; throw new IllegalStateException(\"network interceptor \" + interceptors.get(index - 1) + \" must call proceed() exactly once\"); &#125; // 调用责任链的下一个Interceptor RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange, index + 1, request, call, connectTimeout, readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); // 确保下一个Interceptor被执行过了`Chain.proceed()` if (exchange != null && index + 1 < interceptors.size() && next.calls != 1) &#123; throw new IllegalStateException(\"network interceptor \" + interceptor + \" must call proceed() exactly once\"); &#125; // 确保Interceptor返回的Response不是null if (response == null) &#123; throw new NullPointerException(\"interceptor \" + interceptor + \" returned null\"); &#125; // 保证Interceptor返回的ResponseBody不是null if (response.body() == null) &#123; throw new IllegalStateException( \"interceptor \" + interceptor + \" returned a response with no body\"); &#125; // 返回Response return response; &#125; &#125; 从代码可以看出,核心部分是执行下一个责任链,前后做了大量的判断,以保证责任链正确的执行。 Chain是什么这下我们知道:Chain保证在责任链上Interceptor正确的执行。如果Interceptor调用了Chain.proceed(Request)方法,表示继续交由下一Interceptor处理,当然Interceptor也可以自己处理得到Response直接返回,不再传递下去。","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"开源","slug":"开源","permalink":"https://blog.mycroft.wang/tags/%E5%BC%80%E6%BA%90/"},{"name":"责任链模式","slug":"责任链模式","permalink":"https://blog.mycroft.wang/tags/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/"},{"name":"Interceptor","slug":"Interceptor","permalink":"https://blog.mycroft.wang/tags/Interceptor/"},{"name":"Chain","slug":"Chain","permalink":"https://blog.mycroft.wang/tags/Chain/"}]},{"title":"okhttp Interceptor责任链","slug":"okhttp-Interceptor责任链","date":"2019-08-16T05:47:15.000Z","updated":"2023-06-13T07:01:29.486Z","comments":true,"path":"2019/08/16/okhttp-interceptor-ze-ren-lian/","link":"","permalink":"https://blog.mycroft.wang/2019/08/16/okhttp-interceptor-ze-ren-lian/","excerpt":"","text":"okhttp Interceptor责任链责任链 责任链模式将处理用户请求的对象形成一个链,责任链上的每个处理者要么处理用户的请求,要么把请求传递给责任链上的下一个处理者 实例: 请求 处理者:处理请求的对象 链:处理者形成的链表 说明: 一个请求交给链表上第一个处理者处理 第一个处理者处理不了,交由下一个处理者 如果链上某一处理者能够处理,则直接返回处理结果,不再交由后面的处理者处理 如果最后一个处理者也无法处理,则返回无效的结果 后面处理者处理的结果,前面的处理者也可以操作。 RealCall.getResponseWithInterceptorChain()源码: Response getResponseWithInterceptorChain() throws IOException &#123; // Build a full stack of interceptors. // 构造Interceptor栈 List<Interceptor> interceptors = new ArrayList<>(); // 添加配置的Interceptor interceptors.addAll(client.interceptors()); // 下面添加逻辑处理的的Interceptor interceptors.add(new RetryAndFollowUpInterceptor(client)); interceptors.add(new BridgeInterceptor(client.cookieJar())); interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client)); // 添加配置的networkInterceptor,不是长连接才添加 if (!forWebSocket) &#123; interceptors.addAll(client.networkInterceptors()); &#125; // 添加真正执行的网络连接的Interceptor interceptors.add(new CallServerInterceptor(forWebSocket)); // 构造Interceptor链 Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); // 标记请求是否抛出异常 boolean calledNoMoreExchanges = false; try &#123; // 执行Interceptor责任链 Response response = chain.proceed(originalRequest); // 若在执行过程前已经关闭了,则关闭Response并抛出异常 if (transmitter.isCanceled()) &#123; closeQuietly(response); throw new IOException(\"Canceled\"); &#125; // 返回结果 return response; &#125; catch (IOException e) &#123; // 标记发生了异常,抛出异常,并申请关闭连接 calledNoMoreExchanges = true; throw transmitter.noMoreExchanges(e); &#125; finally &#123; // 若未发生异常,申请关闭连接 if (!calledNoMoreExchanges) &#123; transmitter.noMoreExchanges(null); &#125; &#125; &#125; 下面是Interceptor责任链流程图 在这个链表中,可以直接自行处理Request得到Response,不交由下一级处理者的Interceptor有:CacheInterceptor,CallServerInterceptor。其他的Interceptor用于其他处理,如ConnectInterceptor用于连接服务器。 下面分别分析这些Interceptor的作用 RetryAndFollowUpInterceptor This interceptor recovers from failures and follows redirects as necessary. It may throw an {@link IOException} if the call was canceled. 翻译:这个Interceptor用于失败重试和必要时重定向。如果Call被取消,可能会抛出IOException。 具体我就不深入代码了,这更多的涉及HTTP协议的相关内容,其实知道这个类的作用就行了。可以简单的了解几点: 重定向最大次数为20次,Chrome是21次,Firefox,curl,wget是20次,Safari是16次,HTTP/1.0推荐是5次。 不能重试的情况 应用层禁止重试 无法发送RequestBody:只有当RequestBody是被缓存的(Buffered)才能重试(可能RequestBody是一次性的) 无法恢复的异常,如ProtocolException协议异常 没有更多的线路去重试 BridgeInterceptor Bridges from application code to network code. First it builds a network request from a user request. Then it proceeds to call the network. Finally it builds a user response from the network response. 翻译:应用层代码和网络层代码的桥梁。首先它从用户请求构建一个网络层的Request,然后它交由网络层处理,最后从网络层的Response处理得到一个应用层的Response。 这段话很直接,实际上,它处理传送过来的应用Request,添加一些参数,然后对交由网络层处理得到的Response再进一步处理,返回给应用层。 BridgeInterceptor对Request的处理: 如果有请求体RequestBody,则根据请求体内容,添加header:Content-Type、Content-Length、Transfer-Encoding header中添加Host 若header中没有Connection,则设置为Keep-Alive 若header中没有Accept-Encoding和Range,设置Accept-Encoding为gzip 添加cookie 添加User-Agent BridgeInterceptor对Response的处理: 复制网络层的Response内容,得到新的Response 处理cookie 处理gzip响应结果 CacheInterceptor Serves requests from the cache and writes responses to the cache. 翻译:使用缓存处理请求,将结果写入缓存。 字面意思很简单。下面是需要注意的点: 如果只能从配置了只能从Cache中获取结果,但是没有缓存结果,将返回504错误。 如果可以从缓存中获取数据,并且有缓存,则直接返回缓存数据 进行网络请求,如果返回304表示结果与上次请求相同,返回缓存结果,同时更新缓存。 如有必要,将得到的网络请求结果,写入缓存 可以看出,CacheInterceptor是可以处理Request得到Response的,如果可以,不必交由下一个Interceptor进行处理。 ConnectInterceptor Opens a connection to the target server and proceeds to the next interceptor. 翻译:为目标服务器打开一个连接Connection,并且交由下一个Interceptor进行处理。 从这段话就可以看出ConnectIntercetpor则不产生直接结果。并且只是用于建立连接。 CallServerInterceptor This is the last interceptor in the chain. It makes a network call to the server. 翻译:这是链中最后一个Interceptor。它向服务器发送一个网络请求。 它实际完成的工作: 写入请求头 写入请求体 读取请求头 读取请求体 处理请求结果 除了控制逻辑之外,实际处理请求的是Exchange Exchange Transmits a single HTTP request and a response pair. This layers connection management and events on {@link ExchangeCodec}, which handles the actual I/O. 翻译:发送单独HTTP Request-Response对。它将连接层管理与事件分层,ExchangeCodec处理真实的IO操作。 ExchangeCodec Encodes HTTP requests and decodes HTTP responses. 翻译:编码HTTP请求,解码HTTP响应。 实际上就是转换对象/实例与HTTP协议内容。 总结这样子分别理清所有Interceptor的功能,一下就豁然开朗了。不需要在意代码细节,整个流程真的是非常的漂亮。我在编码,而OkHttp的开发者是在做艺术。 同时,我们通过OkHttpClient添加的interceptor和networkInterceptor分别是在RetryAndFollowUpInterceptor、CallServerInterceptor之前。所以可以明白: 如果使用的是缓存,则interceptor可以被调用到,而networkdInterceptor是不会调用到的 我们通常可以在networkInterceptor中进行一些特殊的处理,如access token过期时,使用refresh token刷新access token,配合Request的tag可以区分普通请求和刷新access token请求 我们可以在interceptor中加密数据 甚至我们可以自己返回Response,拦截责任链。","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"源码","slug":"源码","permalink":"https://blog.mycroft.wang/tags/%E6%BA%90%E7%A0%81/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"}]},{"title":"okhttp请求与响应执行过程","slug":"okhttp请求与响应执行过程","date":"2019-08-16T01:37:19.000Z","updated":"2023-06-13T07:01:29.491Z","comments":true,"path":"2019/08/16/okhttp-qing-qiu-yu-xiang-ying-zhi-xing-guo-cheng/","link":"","permalink":"https://blog.mycroft.wang/2019/08/16/okhttp-qing-qiu-yu-xiang-ying-zhi-xing-guo-cheng/","excerpt":"","text":"okhttp请求与响应执行过程前言不考虑真正的网络请求部分,看一下从一个Request到Response的过程。 请求再来看一下上一篇最简单的执行代码 OkHttpClient httpClient = new OkHttpClient.Builder() .build(); Request request = new Request.Builder() .url(\"https://wanandroid.com/wxarticle/chapters/json\") .get() .build(); Call call = httpClient.newCall(request); Response response = call.execute(); System.out.println(response.body().string()); OkHttpClient Factory for {@linkplain Call calls}, which can be used to send HTTP requests and read their responses. 翻译:Call的工厂类,用于发送HTTP请求,读取响应。 OkHttpClient实现类Call.Factory接口,下面是源码,实际上只构造一个Call实例。 // Call.Factory interface Factory &#123; Call newCall(Request request); &#125; // OkHttpClient 实现 @Override public Call newCall(Request request) &#123; return RealCall.newRealCall(this, request, false /* for web socket */); &#125; OkHttpClient多用于配置,最重要的方法就是Call newCall(Request),在这个方法中,实际上构造了一个RealCall,最后一个参数用于长连接,目前没有用到。并且构造了一个Transmitter。 private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) &#123; this.client = client; this.originalRequest = originalRequest; this.forWebSocket = forWebSocket; &#125; static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) &#123; // Safely publish the Call instance to the EventListener. RealCall call = new RealCall(client, originalRequest, forWebSocket); call.transmitter = new Transmitter(client, call); return call; &#125; Transmitter Bridge between OkHttp’s application and network layers. This class exposes high-level application layer primitives: connections, requests, responses, and streams. 翻译:是OkHttp应用层与网络层的桥梁。这个类向上面应用层暴露基本信息:连接、请求、响应、流。 Transmitter,可以直接翻译成发射器,将网络层的信息如资源、执行过程告诉应用层(即OkHttpClient配置的一些参数如EventListener。 RealCall我们得到了Call实例之后可以使用execute()同步执行,enqueue(Callback)异步执行。下面看看这两个方法的执行。 同步执行@Override public Response execute() throws IOException &#123; // 判断是否已经执行,并且只能执行一次 synchronized (this) &#123; if (executed) throw new IllegalStateException(\"Already Executed\"); executed = true; &#125; // 超时计时开始 transmitter.timeoutEnter(); // 发送开始执行的通知 transmitter.callStart(); try &#123; // 加入执行队列中 client.dispatcher().executed(this); // 开始执行链 return getResponseWithInterceptorChain(); &#125; finally &#123; // 完成执行,从队列中移除 client.dispatcher().finished(this); &#125; &#125; 前面是一些额外的处理,真正执行的代码在try-finally中。 RealCall.getResponseWithInterceptorChain()为了不影响结构,先说一下getResponseWithInterceptorChain()方法,这也是最重要的方法。 Response getResponseWithInterceptorChain() throws IOException &#123; // Build a full stack of interceptors. // 构造Interceptor栈 List<Interceptor> interceptors = new ArrayList<>(); // 添加配置的Interceptor interceptors.addAll(client.interceptors()); // 下面添加逻辑处理的的Interceptor interceptors.add(new RetryAndFollowUpInterceptor(client)); interceptors.add(new BridgeInterceptor(client.cookieJar())); interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client)); // 添加配置的networkInterceptor,不是长连接才添加 if (!forWebSocket) &#123; interceptors.addAll(client.networkInterceptors()); &#125; // 添加真正执行的网络连接的Interceptor interceptors.add(new CallServerInterceptor(forWebSocket)); // 构造Interceptor链 Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); // 标记请求是否抛出异常 boolean calledNoMoreExchanges = false; try &#123; // 执行Interceptor责任链 Response response = chain.proceed(originalRequest); // 若在执行过程前已经关闭了,则关闭Response并抛出异常 if (transmitter.isCanceled()) &#123; closeQuietly(response); throw new IOException(\"Canceled\"); &#125; // 返回结果 return response; &#125; catch (IOException e) &#123; // 标记发生了异常,抛出异常,并申请关闭连接 calledNoMoreExchanges = true; throw transmitter.noMoreExchanges(e); &#125; finally &#123; // 若未发生异常,申请关闭连接 if (!calledNoMoreExchanges) &#123; transmitter.noMoreExchanges(null); &#125; &#125; &#125; 这一段代码,将一个Request处理得到Response。 1. 构造Interceptor责任链在知道Interceptor责任链的功能前,先不详说,只需要知道,这个责任链处理了Request得到Response。 2. 执行责任链这个是最重要的代码,这部分后面隐藏着整个OkHttp的核心。 3. 结果处理若执行过程中已经取消,那么清理资源。 异步执行@Override public void enqueue(Callback responseCallback) &#123; // 判断是否已经执行,并且只能执行一次 synchronized (this) &#123; if (executed) throw new IllegalStateException(\"Already Executed\"); executed = true; &#125; // 发送开始执行的通知 transmitter.callStart(); // 加入执行队列中 client.dispatcher().enqueue(new AsyncCall(responseCallback)); &#125; 没有实际的代码,只是在将一个AsnycCall添加到Dispatcher中的队列。 AsyncCallAsyncCall是在RealCall的一个内部类,它继承自NamedRunnable,而NamedRunnable实现了Runnable接口,下面是源码,只是将当前线程设置了一个名字,并且将逻辑抽象成了execute()方法。 public abstract class NamedRunnable implements Runnable &#123; protected final String name; public NamedRunnable(String format, Object... args) &#123; this.name = Util.format(format, args); &#125; @Override public final void run() &#123; // 设置当前线程的名字 String oldName = Thread.currentThread().getName(); Thread.currentThread().setName(name); try &#123; execute(); &#125; finally &#123; Thread.currentThread().setName(oldName); &#125; &#125; protected abstract void execute(); &#125; 下面是AsyncCall.execute()实现 @Override protected void execute() &#123; boolean signalledCallback = false; transmitter.timeoutEnter(); try &#123; Response response = getResponseWithInterceptorChain(); signalledCallback = true; responseCallback.onResponse(RealCall.this, response); &#125; catch (IOException e) &#123; if (signalledCallback) &#123; // Do not signal the callback twice! Platform.get().log(INFO, \"Callback failure for \" + toLoggableString(), e); &#125; else &#123; responseCallback.onFailure(RealCall.this, e); &#125; &#125; finally &#123; client.dispatcher().finished(this); &#125; &#125; 这里也是调用了RealCall.getResponseWithInterceptorChain()方法,并且处理了回调。所以只需要将AsyncCall作为Runnable使用ExecutorService执行,就自然而然执行了。 Dispatcher Policy on when async requests are executed.Each dispatcher uses an {@link ExecutorService} to run calls internally. If you supply your own executor, it should be able to run {@linkplain #getMaxRequests the configured maximum} number of calls concurrently. 翻译:异步执行时的策略。每个Dispatcher在内部使用一个ExecutorService去执行Call。如果你提供自己的执行器,应该能够保证getMaxRequests得到的配置最大数量的请求。 查看源码,实际上内部有三个ArrayDeque双向队列用于管理Call,其中一个用于管理同步执行的RealCall,另外两个用于管理异步执行的AsyncCall。 先看一下在RealCall.execute()方法Dispatcher做了什么: /** * Used by &#123;@code Call#execute&#125; to signal it is in-flight. * 用于标记Call正在执行 */ synchronized void executed(RealCall call) &#123; runningSyncCalls.add(call); &#125; /** * Used by &#123;@code Call#execute&#125; to signal completion. * 用于标记同步执行完毕 */ void finished(RealCall call) &#123; finished(runningSyncCalls, call); &#125; private <T> void finished(Deque<T> calls, T call) &#123; Runnable idleCallback; synchronized (this) &#123; // 判断错误 if (!calls.remove(call)) throw new AssertionError(\"Call wasn't in-flight!\"); // Dispatcher空闲时的回调接口 idleCallback = this.idleCallback; &#125; // 判断Dispatcher是否有在执行的任务,并推动队列执行 boolean isRunning = promoteAndExecute(); // 当Dispatcher没有在执行代码时,执行空闲借口回调 if (!isRunning && idleCallback != null) &#123; idleCallback.run(); &#125; &#125; 实际上只是先将RealCall添加到了这个双向队列中,标记它正在被执行,执行结束后,做移除它。 当然它还做了一些额外的工作,如回调空闲接口、通知其他在排队执行的任务开始执行。 下面看看enqueue()做了什么 void enqueue(AsyncCall call) &#123; synchronized (this) &#123; // 添加到准备执行的双向队列中 readyAsyncCalls.add(call); // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to // the same host. // 转换AsyncCall,便于在多个运行同一host的Call中分享AtomicInteger,这个AtomicInteger标记在同一host上同时运行的数量 if (!call.get().forWebSocket) &#123; AsyncCall existingCall = findExistingCallWithHost(call.host()); if (existingCall != null) call.reuseCallsPerHostFrom(existingCall); &#125; &#125; // 推动队列执行 promoteAndExecute(); &#125; 关于promoteAndExecute()方法的执行,可以在详解Dispatcher时深入,先看下这个方法的注释: Promotes eligible calls from {@link #readyAsyncCalls} to {@link #runningAsyncCalls} and runs them on the executor service. Must not be called with synchronization because executing calls can call into user code. 翻译:将符合条件的Call从准备队列readyAsyncCalls移动到执行中队列runningAsyncCalls,然后使用ExecutorService执行。禁止同步执行,因为正在执行的Call能够会调用到使用者的代码。 意思是如果满足条件(如未达到最大执行数、同一host上的请求没有达到最大数)时,启动那些等待执行的Call。这个方法间接使用ExecutorService启动了AsyncCall. 总结无论是同步执行,还是异步执行,核心代码都是RealCall.getResponseWithInterceptorChain(),除开一些额外的配置,最重要的是Interceptor责任链,这个责任链将一个Request转换成了一个Response。所以要明白OkHttp,就是理解Interceptor责任链。","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"源码","slug":"源码","permalink":"https://blog.mycroft.wang/tags/%E6%BA%90%E7%A0%81/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"}]},{"title":"okhttp基本概念","slug":"okhttp基本概念","date":"2019-08-15T08:45:27.000Z","updated":"2023-06-13T07:01:29.489Z","comments":true,"path":"2019/08/15/okhttp-ji-ben-gai-nian/","link":"","permalink":"https://blog.mycroft.wang/2019/08/15/okhttp-ji-ben-gai-nian/","excerpt":"","text":"okhttp基本概念okhttp官网 An HTTP client for Android, Kotlin, and Java. 官方介绍:Android, Kotlin, Java的HTTP客户端 基本类在使用okhttp时,我们一定会碰到的一些类的介绍 基本的网络请求下面是使用okhttp一串最基本的网络请求 OkHttpClient httpClient = new OkHttpClient.Builder() .build(); Request request = new Request.Builder() .url(\"https://wanandroid.com/wxarticle/chapters/json\") .get() .build(); Call call = httpClient.newCall(request); Response response = call.execute(); System.out.println(response.body().string()); 1. Request An HTTP request. Instances of this class are immutable if their {@link #body} is null or itself immutable. 翻译:代表有一个HTTP请求,这个类的实例是不可变的。 Request有多个属性:url,method,header,body,tag 字面意思很简单,需要解释一下的是tag,标记这个request,在interceptor,event listener,callback中都可以特殊处理标记的request。 body实际上是RequestBody对象,通常当请求method为POST,PUT之类的时可以使用。 2. RequestBodyHTTP的请求体,如POST提交表单。 常用到的有两个子类:FormBody,MultipartBody。前者多用于POST表单提交,后者多用于文件上传。 3. Call A call is a request that has been prepared for execution. A call can be canceled. As this object represents a single request/response pair (stream), it cannot be executed twice. 翻译:Call表示将Request请求为执行做准备。Call可以被取消。它只代表单独的一个请求-相应对,所以不能被执行两次。 Call是一个接口,包含的主要方法:Response execute(),void enqueue(Callback),cancel(),分别表示同步执行、加入队列等待执行、取消执行。 4. Response An HTTP response. Instances of this class are not immutable: the response body is a one-shot value that may be consumed only once and then closed. All other properties are immutable. 翻译:HTTP响应。实例不可变:其中的ResponseBody是一次性的值,当被消费一次之后就会被关闭。其他所有属性都是不可变的。 Response封装了响应结果,其中主要的属性有:code,message,request,header,body。 body指的是ResponseBody。 5. ResponseBody A one-shot stream from the origin server to the client application with the raw bytes of the response body. Each response body is supported by an active connection to the webserver. This imposes both obligations and limits on the client application. 翻译:远程服务器响应客户端请求得到的一次性的二进制数据流。每个ResponseBody依赖于活跃的与服务器的连接。这种机制强行限制了客户端应用的使用。 ResponseBody实际上将返回的数据作为流来处理,最重要方法是BufferedSource source(),得到BufferedSource流接口,可以转换成真实数据。 责任链 Chain of Responsibility The chain of responsibility pattern let these object which can handle the request of customer become to a chain,and a handler of the responsibility chain handles the request of a customer or deliver the request of a customer to next handler of the chain of responsibility. 责任链模式将处理用户请求的对象形成一个链,责任链上的每个处理者要么处理用户的请求,要么把请求传递给责任链上的下一个处理者","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"源码","slug":"源码","permalink":"https://blog.mycroft.wang/tags/%E6%BA%90%E7%A0%81/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"责任链模式","slug":"责任链模式","permalink":"https://blog.mycroft.wang/tags/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/"}]},{"title":"okhttp系列","slug":"okhttp系列","date":"2019-08-15T08:43:07.000Z","updated":"2023-06-13T07:01:29.490Z","comments":true,"path":"2019/08/15/okhttp-xi-lie/","link":"","permalink":"https://blog.mycroft.wang/2019/08/15/okhttp-xi-lie/","excerpt":"","text":"okhttp系列前言今天看到一个面试题:okhttp的原理 刚好有时间,搜索了一下,其中一篇文章瞬间点醒我,解决了我对okhttp部分一直不解的问题。 最近有时间更新完自己的okhttp源码解析系列 系列文章 okhttp基本概念 okhttp请求与响应执行过程 okhttp Interceptor责任链 okhttp Interceptor接口 okhttp 缓存 okhttp 连接 okhttp RealConnectionPool 流程下面是okhttp的流程图,其中最核心的是interceptor的责任链机制,这也是上面面试题的答案:okhttp的原理。 参考文章OKhttp源码解析详解系列 Android开源框架源码鉴赏:Okhttp Okhttp3源码分析 OkHttp3架构分析 Android OkHttp3源码分析 OKHttp源码解析(一)–初阶","categories":[{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"}],"tags":[{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"开源","slug":"开源","permalink":"https://blog.mycroft.wang/tags/%E5%BC%80%E6%BA%90/"},{"name":"责任链模式","slug":"责任链模式","permalink":"https://blog.mycroft.wang/tags/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/"}]},{"title":"StringBuilder、StringBuffer的线程安全到底是什么","slug":"StringBuilder、StringBuffer的线程安全到底是什么","date":"2019-08-15T03:16:44.000Z","updated":"2023-06-13T07:01:29.482Z","comments":true,"path":"2019/08/15/stringbuilder-stringbuffer-de-xian-cheng-an-quan-dao-di-shi-shi-me/","link":"","permalink":"https://blog.mycroft.wang/2019/08/15/stringbuilder-stringbuffer-de-xian-cheng-an-quan-dao-di-shi-shi-me/","excerpt":"","text":"StringBuilder、StringBuffer的线程安全到底是什么前言昨天看到这个面试题:String、StringBuilder、StringBuffer之间的区别是什么? 这个问题本身很简单: String是不可变的,对于字符串的拼接,都需要重新构造String对象 StringBuilder和StringBuffer主要解决String拼接字符串的问题,他们不需要重新构造对象 StringBuilder和StringBuffer的区别在于StringBuffer是线程安全的 但是根据这个问题可以引申出一些其他问题 String为什么不可变查看源码,String、StringBuilder、StringBuffer内部都是使用char数组实现的。 在String中,这个char数组是用final进行修饰的,所以一旦创建并不能改变其值(非常规方法是可以的)。 为什么要设计String不可变Java将String保留在一个特殊的区域:字符串常量池。当创建一个String时,若在池中已经存在此字符串,则不会再创建,而是直接引用,若不存在,则创建。这是一种缓存策略。 为什么StringBuilder、StringBuffer可变StringBuilder和StringBuffer都继承自AbstractStringBuilder,其中的char数组并没有被final修饰。在append之类的方法中,进行了整个数组的复制,它们引用了新的char数组,原来的char数组相当于被抛弃了。 为什么StringBuffer是线程安全的什么是线程安全定义:多线程环境中,能永远保证程序的正确性 StringBuffer的线程安全到底是什么意思在StringBuilder和StringBuffer的append之类的方法中,执行了一系列的代码。 这个是AbstractStringBuilder源码 public AbstractStringBuilder append(String str) &#123; if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; &#125; 这一段代码大致意思是:计算新的数组的长度、将原来的数组内容复制到新的数组中,在新的数组中添加内容,增加长度,返回。 在多线程环境中,可能进行了多个append操作,导致在其中一个append的执行过程中,已经在执行其他的append操作。如:前一个append生成新的数组后,还未将内容添加到新的数组时,后面一个append又生成了新的数组,导致在新的数组中添加内容这一步,在同一个位置添加了两次,若添加的内容超过新的数组长度,会抛出StringIndexOutOfBoundsException。 在StringBuilder中,并没有对append进行处理,所以它仍然是按照AbstractStringBuilder的逻辑执行。 但是在StringBuffer中,重写了所有的append之类的操作,添加了synchronized关键字,保证了这一段代码的原子性,保证了线程安全。 测试代码import java.util.Locale; import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class Main &#123; private static final Random seedRandom = new Random(4L); public static void main(String[] args) throws Exception &#123; StringBuilder builder = new StringBuilder(); StringBuffer buffer = new StringBuffer(); ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 50; i++) &#123; executor.execute(() -> &#123; Random random = new Random(seedRandom.nextLong()); builder.append(random.nextInt(10)); buffer.append(random.nextInt(10)); &#125;); &#125; TimeUnit.SECONDS.sleep(1); System.out.println(builder); System.out.println(builder.length()); System.out.println(buffer); System.out.println(buffer.length()); executor.shutdown(); &#125; &#125; 多执行这段代码几次,StringBuffer得到的结果长度会总是50,而StringBuilder偶尔会少于50次,这就是线程不安全的情况下出现的错误。 我的疑问这样的线程安全有什么用,说实话,没有用过StringBuffer,一般的需求都是在单线程进行字符串的拼接。 我的疑问是:多线程使用StringBuffer怎么能够保证拼接的顺序呢? 我在使用过程中,对字符串拼接对顺序是有要求的,并不希望因为线程原因导致错误。而且字符串拼接的线程安全性,通常可以使用其他方法来解决。 总结:线程安全 != 保证拼接顺序StringBuffer的线程安全只是保证在一次append操作中,不会有其他append介入,避免了拼接的过程中出现异常。 若在多线程环境中,StringBuffer并不像队列一般,能够将拼接按特定的顺序执行。","categories":[{"name":"Java - 基础知识","slug":"Java-基础知识","permalink":"https://blog.mycroft.wang/categories/Java-%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://blog.mycroft.wang/tags/Java/"},{"name":"String","slug":"String","permalink":"https://blog.mycroft.wang/tags/String/"},{"name":"StringBuilder","slug":"StringBuilder","permalink":"https://blog.mycroft.wang/tags/StringBuilder/"},{"name":"StringBuffer","slug":"StringBuffer","permalink":"https://blog.mycroft.wang/tags/StringBuffer/"}]},{"title":"Android包优化个人总结","slug":"Android包优化个人总结","date":"2019-08-13T06:05:17.000Z","updated":"2023-06-13T07:01:29.476Z","comments":true,"path":"2019/08/13/android-bao-you-hua-ge-ren-zong-jie/","link":"","permalink":"https://blog.mycroft.wang/2019/08/13/android-bao-you-hua-ge-ren-zong-jie/","excerpt":"","text":"Android包优化个人总结前言打包apk对app运行本身会有很多用处,减少代码量,减少资源量,减少资源名等,都可以减少apk的大小。如果apk足够大,会发现,release包比debug包启动都会快很多。 所以优化非常有必要,同时为了安全,还需要数据加密和apk加固 优化项基本配置release { // 删除无用资源 shrinkResources true // 开启混淆 minifyEnabled true // apk对齐 zipAlignEnabled true // 指定混淆规则文件 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // 只使用中文资源,xhdpi图片资源 resConfigs "zh", "xhdpi" // 配置主dex的规则,将首页和第二页需要的代码放在主dex中有利于提升app启动效率 multiDexKeepProguard file("maindexlist.pro") // 配置签名 signingConfig signingConfigs.release // 关闭所有debug jniDebuggable false renderscriptDebuggable false debuggable false } 一、混淆一些通用的配置 -optimizationpasses 5 -dontusemixedcaseclassnames -dontskipnonpubliclibraryclasses -dontskipnonpubliclibraryclassmembers -dontpreverify -verbose -printmapping proguardMapping.txt -optimizations !code/simplification/cast,!field/*,!class/merging/* -keepattributes *Annotation*,InnerClasses -keepattributes Signature -keepattributes SourceFile,LineNumberTable 1. 通用混淆配置 不混淆Android组件类,View类等需要在配置中使用的类 -keep public class * extends android.app.Activity -keep public class * extends android.app.Application -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends android.app.backup.BackupAgentHelper -keep public class * extends android.preference.Preference -keep public class com.android.vending.licensing.ILicensingService 不混淆通用的如Serializable, Parcelable子类 -keepclassmembers class * implements android.os.Parcelable { static ** CREATOR; } -keepclassmembers class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator CREATOR; } -keepnames class * implements java.io.Serializable -keepclassmembers class * implements java.io.Serializable { static final long serialVersionUID; private static final java.io.ObjectStreamField[] serialPersistentFields; !static !transient <fields>; !private <fields>; !private <methods>; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } 枚举类 -keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); } 含有native方法的类 -keepclasseswithmembernames,includedescriptorclasses class * { native <methods>; } -keepclasseswithmembernames class * { native <methods>; } androidx.annotation.Keep标注的类、方法、域 # AndroidX -keep class androidx.annotation.Keep -keep @androidx.annotation.Keep class * {*;} -keepclasseswithmembers class * { @androidx.annotation.Keep <methods>; } -keepclasseswithmembers class * { @androidx.annotation.Keep <fields>; } -keepclasseswithmembers class * { @androidx.annotation.Keep <init>(...); } # 非AndroidX -keep class android.support.annotation.Keep -keep @android.support.annotation.Keep class * {*;} -keepclasseswithmembers class * { @android.support.annotation.Keep <methods>; } -keepclasseswithmembers class * { @android.support.annotation.Keep <fields>; } -keepclasseswithmembers class * { @android.support.annotation.Keep <init>(...); } R类中的所有域(后面可以使用AndResGuard进行处理) -keep class **.R$* { public static <fields>; } -keep class **.R 2. 库相关的类引用的库中需要保留一些代码,通常只需要去相对应的库文档中查找即可,下面举一些常用的库 Glide -keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * extends com.bumptech.glide.module.AppGlideModule -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { **[] $VALUES; public *; } # for DexGuard only -keepresourcexmlelements manifest/application/meta-data@value=GlideModule EventBus -keepclassmembers class * { @org.greenrobot.eventbus.Subscribe <methods>; } -keep enum org.greenrobot.eventbus.ThreadMode { *; } # Only required if you use AsyncExecutor -keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent { <init>(java.lang.Throwable); } Okio # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. -dontwarn org.codehaus.mojo.animal_sniffer.* Okhttp # JSR 305 annotations are for embedding nullability information. -dontwarn javax.annotation.** # A resource is loaded with a relative path so the package of this class must be preserved. -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. -dontwarn org.codehaus.mojo.animal_sniffer.* # OkHttp platform used only on JVM and when Conscrypt dependency is available. -dontwarn okhttp3.internal.platform.ConscryptPlatform Retrofit 除了下面官方的配置之外,因为在json转model时使用了反射,所以不能混淆model类、域 # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and # EnclosingMethod is required to use InnerClasses. -keepattributes Signature, InnerClasses, EnclosingMethod # Retrofit does reflection on method and parameter annotations. -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations # Retain service method parameters when optimizing. -keepclassmembers,allowshrinking,allowobfuscation interface * { @retrofit2.http.* <methods>; } # Ignore annotation used for build tooling. -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement # Ignore JSR 305 annotations for embedding nullability information. -dontwarn javax.annotation.** # Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. -dontwarn kotlin.Unit # Top-level functions that can only be used by Kotlin. -dontwarn retrofit2.KotlinExtensions -dontwarn retrofit2.KotlinExtensions$* # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy # and replaces all potential values with null. Explicitly keeping the interfaces prevents this. -if interface * { @retrofit2.http.* <methods>; } -keep,allowobfuscation interface <1> 二、减少资源名 AndResGuard是一个帮助你缩小APK大小的工具,他的原理类似Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a。 当项目逐渐增大,包里面的资源越来越多,名字也越来越长(名字清晰当然易于阅读),AndResGuard的作用就越大,当使用AndResGuard后,会发现apk明显的缩短,并且某种程度上,进行了混淆。 官方地址:AndResGuard 1. 配置在项目build.gradle中添加插件 buildscript { dependencies { classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17' } } 在主模块build.gradle中添加,其中注意一定要将需要保留的资源文件名加入白名单,另外如果使用了如友盟、融云等库时,因为在这些库中使用资源文件名获取资源,所以一定要添加白名单,不然会出现资源找不到的问题。(我更喜欢用LeanCloud) andResGuard { // mappingFile = file(\"./resource_mapping.txt\") mappingFile = null use7zip = true useSign = true // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字 keepRoot = false // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小 fixedResName = \"arg\" // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源 mergeDuplicatedRes = true whiteList = [ // for your icon \"R.drawable.icon\", // for fabric \"R.string.com.crashlytics.*\", // for google-services \"R.string.google_app_id\", \"R.string.gcm_defaultSenderId\", \"R.string.default_web_client_id\", \"R.string.ga_trackingId\", \"R.string.firebase_database_url\", \"R.string.google_api_key\", \"R.string.google_crash_reporting_api_key\" ] compressFilePattern = [ \"*.png\", \"*.jpg\", \"*.jpeg\", \"*.gif\", ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.17' //path = \"/usr/local/bin/7za\" } /** * 可选: 如果不设置则会默认覆盖assemble输出的apk **/ // finalApkBackupPath = \"${project.rootDir}/final.apk\" /** * 可选: 指定v1签名时生成jar文件的摘要算法 * 默认值为“SHA-1” **/ // digestalg = \"SHA-256\" } 2. 启动 使用Android Studio的同学可以再 andresguard 下找到相关的构建任务; 命令行可直接运行./gradlew resguard[BuildType | Flavor], 这里的任务命令规则和assemble一致。 详细使用查看官方地址AndResGuard 三、删除无用资源虽然可以配置在打包时删除无用资源,但是Android Studio本身就提供找出未使用资源的方法 流程:Analyze -> Run Inspection by Name -> 输入Unused resources -> 选择需要查找的范围 -> OK 等待几分钟,就可以发现目前没有使用到的资源,根据自身情况删除,如果存在相互引用,可以多运行几次 如下图: 四、代码审查这篇文章不讨论代码相关的事情,使用代码审查可以找出代码中的问题,可能引用了一些没有使用到的资源,同时建议开启阿里Java代码指导插件Alibaba Java Coding Guidelines,代码的质量就是我们程序员的脸面,当别人看你代码时,即使功能不强大,但是代码一定要漂亮。 使用方法:Analyze -> Inspect Code,运行结束可以在下方看到Android Studio帮忙找出的问题,虽然它没有那么智能,有些东西也不需要修改,但是我们可以做修改参考意见。 另外,还可以多看看Analyze下的一些功能,可以帮助提升代码质量。一经检查,发现修改根本停不下来… 一些可以参考的文章Android APP终极瘦身指南 APK瘦身三步曲 Android 可能你想要的APK瘦身笔记 西瓜视频apk瘦身之 Java access 方法删除","categories":[{"name":"Android - 优化","slug":"Android-优化","permalink":"https://blog.mycroft.wang/categories/Android-%E4%BC%98%E5%8C%96/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"优化","slug":"优化","permalink":"https://blog.mycroft.wang/tags/%E4%BC%98%E5%8C%96/"},{"name":"apk","slug":"apk","permalink":"https://blog.mycroft.wang/tags/apk/"}]},{"title":"Luban压缩实现分析","slug":"Luban压缩实现分析","date":"2019-08-12T07:06:52.000Z","updated":"2023-06-13T07:01:29.479Z","comments":true,"path":"2019/08/12/luban-ya-suo-shi-xian-fen-xi/","link":"","permalink":"https://blog.mycroft.wang/2019/08/12/luban-ya-suo-shi-xian-fen-xi/","excerpt":"","text":"Luban压缩实现分析前言Luban是一个非常好用的Android图片压缩库,据作者所言,作者逆向推算了微信的压缩算法,压缩结果很接近微信朋友圈压缩后的效果。在实际使用中,压缩的结果大大缩小了从相册选择出来的图片,质量也几乎没有太大差别。 压缩流程 鲁班压缩的核心非常简单,最重要的就两个类Luban.java和Engine.java,核心算法主要是在Engine.java Luban.javaLuban.java的作用主要是进行一些压缩的配置,如下 设置图片源 设置压缩后图片的存储文件夹 压缩后图片的名字 可压缩图片的最小文件大小 设置压缩选择器,用户可根据图片,决定是否压缩 Engine.javaEngine.java处理真正的压缩 计算压缩比例 压缩图片 问题因为Android 7.0限制了获取图片的方式,我们获取到的图片地址由file://...变成了content://...,而且是不能通过content://...得到对应文件的。 在Luban决定图片是否应该被压缩时,仍然是将其转换为File进行处理,因为时无效File,所以这时就出现了bug,图片并没有被压缩,得到的也是无效的File对象。这时可以设置可压缩图片的最小文件大小为-1,ignoreBy(-1)来解决这个问题,这样设置时,会直接去压缩,并不会去判断File文件的大小,所以就避免了这个问题 知道这个bug也自然可以解决。在Android平台上,操作图片、文件最好都是用Uri来进行处理,会避免很多的问题,如果在使用一个图片选择库、文件选择库、图片剪切库时,返回的是一个Uri,不要害怕。 压缩算法作者公开了压缩算法,其中主要根据图片尺寸来计算压缩后的图片尺寸,同时设置压缩质量为60% 作者的压缩算法: 判断图片比例值,是否处于以下区间内; [1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内 [0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内 [0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内 判断图片最长边是否过边界值; [1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3) [0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1) [0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1) 计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1) 计算压缩图片的实际文件大小,以第2、3步结果为准,图片比例越大则文件越大。 size = (newW * newH) / (width * height) * m; [1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300; [0.5625, 0.5) 则 width = 1440,height = 2560, m = 200; [0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值 判断第4步的size是否过小 [1, 0.5625) 则最小 size 对应 60,60,100 [0.5625, 0.5) 则最小 size 都为 100 [0.5, 0) 则最小 size 都为 100 将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值 java实现既然有了算法,那么就自己实现一遍,因为只是计算尺寸,所以实现就非常简单了 下面是我的java实现,用到了google的开源库thumbnailtor,在服务端,若发现客户端上传的图片过大,可以使用这样的算法进行压缩 import net.coobird.thumbnailator.Thumbnails; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; /** * Luban压缩算法Java实现 * * @author mycroft */ public class FileCompressor &#123; /** * 压缩图片文件 * * @param sourceFile 需要进行压缩的图片 * @param destFile 压缩后的图片位置 */ public static void compressFile(File sourceFile, File destFile) throws IOException &#123; if (null == sourceFile || !sourceFile.exists()) &#123; return; &#125; BufferedImage image = ImageIO.read(sourceFile); int width = image.getWidth(); int height = image.getHeight(); if (width == 0 || height == 0) &#123; return; &#125; int scale = calculateSize(width, height); int destWidth = width / scale; int destHeight = height / scale; Thumbnails.of(image).size(destWidth, destHeight).outputQuality(0.6f).toFile(destFile); &#125; /** * 根据图片宽高计算压缩尺寸 * * @param srcWidth 图片宽度 * @param srcHeight 图片高度 * @return 压缩比例 */ private static int calculateSize(int srcWidth, int srcHeight) &#123; srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth; srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight; int longSide = Math.max(srcWidth, srcHeight); int shortSide = Math.min(srcWidth, srcHeight); float scale = ((float) shortSide / longSide); if (scale <= 1 && scale > 0.5625) &#123; if (longSide < 1664) &#123; return 1; &#125; else if (longSide >= 1664 && longSide < 4990) &#123; return 2; &#125; else if (longSide > 4990 && longSide < 10240) &#123; return 4; &#125; else &#123; return longSide / 1280 == 0 ? 1 : longSide / 1280; &#125; &#125; else if (scale <= 0.5625 && scale > 0.5) &#123; return longSide / 1280 == 0 ? 1 : longSide / 1280; &#125; else &#123; return (int) Math.ceil(longSide / (1280.0 / scale)); &#125; &#125; &#125; 后话压缩实现非常简单,但是据作者所言,其在朋友圈发送了100张图片,进行对比逆向推算出来的算法,并且公开了算法,这是非常敬佩的。 同时这个算法实现非常简单,如有需要,可以在其他平台和语言实现,如windows, iOS, flutter。","categories":[{"name":"Android - 图片","slug":"Android-图片","permalink":"https://blog.mycroft.wang/categories/Android-%E5%9B%BE%E7%89%87/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"Luban","slug":"Luban","permalink":"https://blog.mycroft.wang/tags/Luban/"},{"name":"图片","slug":"图片","permalink":"https://blog.mycroft.wang/tags/%E5%9B%BE%E7%89%87/"},{"name":"压缩","slug":"压缩","permalink":"https://blog.mycroft.wang/tags/%E5%8E%8B%E7%BC%A9/"}]},{"title":"hexo-bug解决方案","slug":"hexo-bug解决方案","date":"2019-08-12T01:12:08.000Z","updated":"2023-06-13T07:01:29.486Z","comments":true,"path":"2019/08/12/hexo-bug-jie-jue-fang-an/","link":"","permalink":"https://blog.mycroft.wang/2019/08/12/hexo-bug-jie-jue-fang-an/","excerpt":"","text":"1. git clone下来的主题,没有添加到项目中前提:一开始知道需要删除.git文件夹 问题:提交之后发现,远程仓库只有一个主题目录,其中的内容并没有提交,尝试进入主题目录,添加所有的文件 # 进入文件夹 $ cd themes/hexo-theme-matery # 尝试添加所有文件 $ git add * fatal: in unpopulated submodule 'themes/hexo-theme-matery' # 出现了unpopulated错误,是因为没有删除其中的git缓存 # 退到themes文件夹 cd .. # 删除缓存 $ git rm -rf --cached hexo-theme-matery rm 'themes/hexo-theme-matery' # 重新添加 $ git add ./hexo-theme-matery/* # 提交 $ git commit -m \"添加漏掉hexo-theme-matery主题\"","categories":[{"name":"网站搭建","slug":"网站搭建","permalink":"https://blog.mycroft.wang/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"}],"tags":[{"name":"bug","slug":"bug","permalink":"https://blog.mycroft.wang/tags/bug/"},{"name":"hexo","slug":"hexo","permalink":"https://blog.mycroft.wang/tags/hexo/"},{"name":"git","slug":"git","permalink":"https://blog.mycroft.wang/tags/git/"}]},{"title":"将阿里云作为git服务器","slug":"将阿里云作为git服务器","date":"2019-08-11T07:42:57.000Z","updated":"2023-06-13T07:01:29.494Z","comments":true,"path":"2019/08/11/jiang-a-li-yun-zuo-wei-git-fu-wu-qi/","link":"","permalink":"https://blog.mycroft.wang/2019/08/11/jiang-a-li-yun-zuo-wei-git-fu-wu-qi/","excerpt":"","text":"将阿里云作为git服务器前言本来一开始在研究,如何部署在github上的博客自动发布到aliyun上,这样就不用每次更新需要在aliyun服务器上进行一次pull操作 看了些使用git hook操作的文章,没有看到,很多都是一些步骤,也不说明原因,出了问题也不知道怎么解决。想到另外的方法是直接弄一个定时任务,这样也可以进行更新,不过感觉git hook的实现机制比较好。 不过忽然看到可以仓库直接放在aliyun上,aliyun提供了这样的功能,这也算是第一步吧。 流程 在aliyun code上添加本地电脑的公钥,建立安全的加密连接 在aliyun code上添加项目 和常规仓库一样的操作 一、在aliyun code上添加本地电脑的公钥,建立安全的加密连接 查看本机电脑是否有公钥公钥文件一般是~/.ssh/id_rsa.pub文件,同时对应一个~/.ssh/id_rsa的私钥文件 生成公钥文件使用命令ssh-keygen -t rsa -C "[email protected]",其实使用ssh-keygen命令即可,-t参数指定算法,-C只是一个描述而已,官方建议是使用阿里云邮箱 添加本地电脑的公钥,建立安全的加密连接 登陆网站aliyun code,选择设置->SSH公钥->+增加SSH密钥->粘贴公钥的内容->增加密钥 三、在aliyun code上添加项目登陆aliyun code,选择项目->新建项目->项目设置,因为我的项目已经放在了gitee,所以选择了其他仓库的链接,这样直接就将项目导入了aliyun code,也可以新建一个项目(仓库),pull下来之后,将项目代码复制到该文件夹中。最后点击创建项目 四、修改git同时提交到多个仓库原理就是增加远程仓库链接 打开项目中的./git/config文件,其中的内容如下(我的项目): [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true [remote \"origin\"] url = https://gitee.com/mycroftwong/LoveServer.git fetch = +refs/heads/*:refs/remotes/origin/* [branch \"master\"] remote = origin merge = refs/heads/master 可以看到它实际上就是绑定了一个远程仓库地址 增加远程仓库地址增加后的内容如下: [core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true [remote \"origin\"] url = https://gitee.com/mycroftwong/LoveServer.git fetch = +refs/heads/*:refs/remotes/origin/* url = https://code.aliyun.com/mycroftwong/LoveServer.git [branch \"master\"] remote = origin merge = refs/heads/master [remote \"aliyun\"] url = https://code.aliyun.com/mycroftwong/LoveServer.git fetch = +refs/heads/*:refs/remotes/gitee/* tagopt = --no-tags 修改文件,进行push 如我修改了README.md文件 # 添加文件 git add README.md # 提交文件 git commit -m \"修改README.md\" # 切换到本地master git checkout master # 将本地dev merge到master git merge dev # 将本地master push 到remote dev git push origin master:dev # 结果如下 Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 4 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 322 bytes | 322.00 KiB/s, done. Total 3 (delta 2), reused 0 (delta 0) remote: Checking connectivity: 3, done. remote: Powered By Gitee.com To https://gitee.com/mycroftwong/LoveServer.git a8fe678..03d1b99 master -> dev Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 4 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 322 bytes | 322.00 KiB/s, done. Total 3 (delta 2), reused 0 (delta 0) To https://code.aliyun.com/mycroftwong/LoveServer.git a8fe678..03d1b99 master -> dev 这样就自动提交到了多个仓库 参考文章使用阿里云code和git管理项目 git同时提交到2个仓库gitee github git 实现关联 aliyun code 仓库 使用阿里云作为git远程仓库的实践 4.3 服务器上的 Git - 生成 SSH 公钥","categories":[{"name":"工具 - git","slug":"工具-git","permalink":"https://blog.mycroft.wang/categories/%E5%B7%A5%E5%85%B7-git/"}],"tags":[{"name":"git","slug":"git","permalink":"https://blog.mycroft.wang/tags/git/"},{"name":"aliyun","slug":"aliyun","permalink":"https://blog.mycroft.wang/tags/aliyun/"}]},{"title":"Spring bug解决方案","slug":"Spring-bug解决方案","date":"2019-08-10T09:14:35.000Z","updated":"2023-06-13T07:01:29.482Z","comments":true,"path":"2019/08/10/spring-bug-jie-jue-fang-an/","link":"","permalink":"https://blog.mycroft.wang/2019/08/10/spring-bug-jie-jue-fang-an/","excerpt":"","text":"这是在实际过程中遇到的bug及解决方案1. QQ邮件发送错误在本地测试发送邮件没有问题,打包发送到服务器之后,无法发送邮件 问题:端口修改成465,出现错误spring boot Could not connect to SMTP host: smtp.xxx.com, port: 465, response: -1 参考spring boot Could not connect to SMTP host: smtp.xxx.com, port: 465, response: -1 解决:在properties配置中加上 spring.mail.properties.mail.smtp.ssl.enable=true # 完整如下 spring.mail.host=smtp.qq.com [email protected] spring.mail.password=密码 spring.mail.protocol=smtp spring.mail.properties.mail.smtp.port=465 spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.ssl.enable=true spring.mail.default-encoding=utf-8 2. 访问http://localhost:8080/druid需要用户名和密码问题:使用druid监控,访问监控网页时,需要输入用户名和密码 解决:在application.properties中配置账号密码 spring.datasource.druid.stat-view-servlet.login-username=用户名 spring.datasource.druid.stat-view-servlet.login-password=密码","categories":[{"name":"spring boot - bug","slug":"spring-boot-bug","permalink":"https://blog.mycroft.wang/categories/spring-boot-bug/"}],"tags":[{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mycroft.wang/tags/spring-boot/"},{"name":"bug","slug":"bug","permalink":"https://blog.mycroft.wang/tags/bug/"},{"name":"mail","slug":"mail","permalink":"https://blog.mycroft.wang/tags/mail/"}]},{"title":"hexo添加category目录","slug":"hexo添加category目录","date":"2019-08-09T07:07:53.000Z","updated":"2023-06-13T07:01:29.486Z","comments":true,"path":"2019/08/09/hexo-tian-jia-category-mu-lu/","link":"","permalink":"https://blog.mycroft.wang/2019/08/09/hexo-tian-jia-category-mu-lu/","excerpt":"","text":"hexo添加category目录一开始我以为直接在配置文件_config.yml中去掉注释categories即可,这样是不对的,还需要手动建立categories目录 新建categories目录使用命令hexo new page categories 新建完成之后虽然可以看到category目录,但是里面没有任何东西,需要修改文件categories/index.md,在其中添加type: "categories"表示指定的是categories 参考文章hexo的Next创建categories hexo博客出现“Cannot GET/xxxx”的错误","categories":[{"name":"网站搭建","slug":"网站搭建","permalink":"https://blog.mycroft.wang/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mycroft.wang/tags/hexo/"},{"name":"categories","slug":"categories","permalink":"https://blog.mycroft.wang/tags/categories/"}]},{"title":"Android点击空白处关闭软键盘","slug":"Android点击空白处关闭软键盘","date":"2019-08-09T07:02:34.000Z","updated":"2023-06-13T07:01:29.477Z","comments":true,"path":"2019/08/09/android-dian-ji-kong-bai-chu-guan-bi-ruan-jian-pan/","link":"","permalink":"https://blog.mycroft.wang/2019/08/09/android-dian-ji-kong-bai-chu-guan-bi-ruan-jian-pan/","excerpt":"","text":"Android点击空白处关闭软键盘答案直接说解决方法,在Activity中重写dispatchTouchEvent(MotionEvent ev)方法 /** * 点击空白处,关闭软键盘 * * @param event event * @return boolean Return true if this event was consumed. */ @Override public boolean dispatchTouchEvent(MotionEvent event) &#123; if (event.getAction() == MotionEvent.ACTION_DOWN) &#123; View v = getCurrentFocus(); if (v instanceof EditText) &#123; Rect outRect = new Rect(); v.getGlobalVisibleRect(outRect); if (!outRect.contains((int) event.getRawX(), (int) event.getRawY())) &#123; v.setFocusable(false); v.setFocusableInTouchMode(true); InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); &#125; &#125; &#125; return super.dispatchTouchEvent(event); &#125; 原理:在当前焦点在EditText的情况下,点击的不是该EditText所在区域,则关闭软键盘 另外的方法监听整个根布局MotionEvent,当然效果没有上面这个方法好","categories":[{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/categories/Android/"}],"tags":[{"name":"android","slug":"android","permalink":"https://blog.mycroft.wang/tags/android/"},{"name":"EditText","slug":"EditText","permalink":"https://blog.mycroft.wang/tags/EditText/"},{"name":"软键盘","slug":"软键盘","permalink":"https://blog.mycroft.wang/tags/%E8%BD%AF%E9%94%AE%E7%9B%98/"}]},{"title":"nginx配置多个域名","slug":"nginx配置多个域名","date":"2019-08-09T06:06:50.000Z","updated":"2023-06-13T07:01:29.486Z","comments":true,"path":"2019/08/09/nginx-pei-zhi-duo-ge-yu-ming/","link":"","permalink":"https://blog.mycroft.wang/2019/08/09/nginx-pei-zhi-duo-ge-yu-ming/","excerpt":"","text":"nginx配置多个域名新建了这个blog,同时上传了github和gitee,另外想发布到个人阿里云服务器,在阿里云服务器上直接拉取github仓库。 目标在不更改原来请求的情况下,添加二级域名blog,转发请求到博客文件夹。 nginx配置解析原本的配置如下 server { listen 80; server_name mycroft.wang www.mycroft.wang; #这是新版本的Nginx转发 return 301 https://$server_name$request_uri; # 固定写法------------- tcp_nodelay on; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } server { listen 443; server_name mycroft.wang www.mycroft.wang; ssl on; root /home/www; index index.html index.htm; ssl_certificate ...; // 这里省略证书地址 ssl_certificate_key ...; // 这里省略证书地址 ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location ~ \\.(html|htm|gif|jpeg|jpg|css|js|png|swf|ico)$ { root /home/www; index index.html index.htm; } location ~ ^/$ { root /home/www; } location / { proxy_pass http://localhost:8080; include /home/nginx/proxy.conf; } error_page 404 /404.html; location = /404.html { root /home/www/error; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /home/www/error; } } 转发www二级域名到443端口,然后在443端口上转发,静态文件转发到/home/www文件夹,动态请求转发到8080端口。 需要的工作 新建一个server监听80端口,指定监听的域名blog.mycroft.wang,然后转发到443端口 新建一个server监听443端口,指定监听的域名blog.mycroft.wang,转发到文件夹/home/blog 过滤掉git相关文件夹 下面是添加的server,注意没有包括之前的配置 server { listen 80; server_name blog.mycroft.wang; #这是新版本的Nginx转发 return 301 https://$server_name$request_uri; # 固定写法------------- tcp_nodelay on; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } server { listen 443; server_name blog.mycroft.wang; ssl on; root /home/blog; index index.html; ssl_certificate ...; // 这里省略证书地址 ssl_certificate_key ...; // 这里省略证书地址 ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location ~ ^(.*)/\\.(svn|git)/ { deny all; } } 说明并没有说明nginx使用的原理,因为我也不懂,这是之前学了一点,依葫芦画瓢弄的,关于nginx的使用,后面再认真学学。 参考文章如何配置nginx 同一ip,多域名,不同端口? 在Nginx上配置多个站点 nginx忽略.svn和.git","categories":[{"name":"网站搭建","slug":"网站搭建","permalink":"https://blog.mycroft.wang/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mycroft.wang/tags/hexo/"},{"name":"nginx","slug":"nginx","permalink":"https://blog.mycroft.wang/tags/nginx/"},{"name":"blog","slug":"blog","permalink":"https://blog.mycroft.wang/tags/blog/"},{"name":"github","slug":"github","permalink":"https://blog.mycroft.wang/tags/github/"},{"name":"gitee","slug":"gitee","permalink":"https://blog.mycroft.wang/tags/gitee/"}]},{"title":"使用hexo搭建博客","slug":"使用hexo搭建博客","date":"2019-08-09T03:21:43.000Z","updated":"2023-06-13T07:01:29.493Z","comments":true,"path":"2019/08/09/shi-yong-hexo-da-jian-bo-ke/","link":"","permalink":"https://blog.mycroft.wang/2019/08/09/shi-yong-hexo-da-jian-bo-ke/","excerpt":"","text":"使用hexo搭建博客前言之前也使用了hexo搭建博客,发布到github, 一开始发布了2篇文章之后也没怎么关心,一个是自己懒,二个是当时并不知道如果使用多用户管理,无法在公司的电脑上也同步更新,如果每次在公司写好,再回家整理发布,就有些太花时间了。所以专门抽空,花了几个小时学习了git分支管理 目标 使用hexo搭建博客网站 将博客发布到gitee上 在多台电脑上管理 将博客同步到个人服务器 一、使用hexo搭建博客网站查看hexo官方网站 1. 安装git从git官网下载并安装git 安装完成后,使用命令git version查看版本,能查看到表示已经安装 2. 安装node.js从node.js官网下载安装 使用命令node -v, npm -v查看版本 3. 安装hexo使用命令npm install -g hexo-cli下载hexo-cli(hexo客户端) 4. 建立博客具体如何使用hexo建立博客,查看hexo官网,这里说一下简单使用 使用命令hexo init blog在一个blog文件中建立博客,blog一定需要是空文件夹,进入文件夹之后,使用命令hexo s就启动了,然后查看网站http://localhost:4000即可查看到网站 5. 网站配置查看hexo官网教程 6. 配置主题next很简单,查看next官网 二、将博客发布到gitee上发布gitee博客非常简单,难点在于如何将hexo博客站点整体配置到仓库中。 下面是发布博客,只需要三步,如下。 在gitee上新建博客站点 创建和用户名同名的仓库,如我的gitee用户名是mycroftwong, 我新建了一个mycroftwong仓库,里面没有任何文件,或者只有一个README.md文件 然后选择 服务 -> Gitee Pages -> 选择部署分支master -> 部署 即可 修改博客配置文件_config.yml下面是我的配置 # URL ## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/' url: https://mycroftwong.gitee.io/ root: / permalink: :year/:month/:day/:title/ permalink_defaults: deploy: type: git repo: https://gitee.com/mycroftwong/mycroftwong.git branch: master 发布 使用命令hexo d即可将博客发布到gitee上,然后再在gitee pages中更新即可通过https://mycroftwong.gitee.io访问到 三、在多台电脑上管理在多台电脑上管理博客的原理,就是将这个博客发布到git仓库 两种实现方式: 将hexo和博客网站放在不同的git仓库 将hexo和博客网站放在同一个git仓库的不同分支 各有利弊,放在不同的仓库方便管理,放在同一个仓库也不用在两个仓库中麻烦,个人倾向于放在不同的仓库中,但是这里使用的是第二种方式 1. 建立hexo博客分支将上面提到的仓库pull到本地,使用命令git branch hexo新建分支hexo, 切换到分支hexo 这里说一下,hexo只能init空文件夹,官方建议如有需要,将文件复制到指定文件夹 所以我们将之前的blog文件夹中的内容复制到这个分支中,然后添加、提交。同时也建立同名的远程分支,命令git push origin hexo:hexo,这样就建立了远程hexo分支,并将本地hexo的内容push到了远程分支hexo中。 2. 发布博客知道原理就很简单了,本地hexo分支对应远程hexo分支,master分支对应远程master分支,我们在hexo分支中管理hexo博客网站,master则是实际的博客文件。具体想怎么操作,都可以的。 四、将博客同步到个人服务器这一步并没有做,不过实际上,可以直接将仓库pull到个人服务器,使用nginx反向代理 后话在写这篇文章的过程中发现,gitee提供了另一种方式,可以在配置gitee pages时,选择部署目录,那么可以不用建立分支,直接将整个hexo项目发布到一个分支中,只将public目录作为博客部署 参考文章使用 Hexo + Github 或 Gitee 搭建个人博客 Hexo+github个人博客搭建+异地管理 在VSCode中使用码云(Gitee)进行代码管理 基于Gitee+Hexo搭建个人博客 hexo官方网站 hexo d后 ERROR Deployer not found: git git无法pull仓库refusing to merge unrelated histories Hexo托管到Coding;Hexo同时部署到多个平台 Hexo server报错Cannot read property ‘offset’ of null解决方法","categories":[{"name":"网站搭建","slug":"网站搭建","permalink":"https://blog.mycroft.wang/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"https://blog.mycroft.wang/tags/hexo/"}]},{"title":"学习git branch","slug":"学习git-branch","date":"2019-08-09T03:10:28.000Z","updated":"2023-06-13T07:01:29.494Z","comments":true,"path":"2019/08/09/xue-xi-git-branch/","link":"","permalink":"https://blog.mycroft.wang/2019/08/09/xue-xi-git-branch/","excerpt":"","text":"学习git branch分支没有实际的代码,只有用于区分分支的一些文件 一、创建工程、分享到github,创建分支时间:2019年8月8日15:30:49 用到的命令-- 创建分支 git branch [branch-name] -- 切换分支 git checkout [branch-name] -- 查看本地分支 git branch -- 查看远程分支 git branch -r -- 查看所有分支 git branch -a -- 删除分支 git branch -d [branch-name] 实际操作 创建dev分支git branch dev 切换到dev分支git checkout dev 查看当前分支 $ git branch -a * dev master remotes/origin/master 可以看到所有的分支:本地dev, master, 远程remotes/origin/master,其中*表示当前分支 二、将本地分支项目提交到本地主分支时间:2019年8月8日15:34:53 用到的命令-- 查看当前状态 git status -- 添加文件 git add [file-name] -- 提交 git commit -m "comment info" 实际操作 查看当前状态 $ git status On branch dev Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: .idea/vcs.xml Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: README.md 可以看到Android Studio自动创建了一个.idea/vcs.xml文件,等待提交,README.md文件修改过,等待添加、提交 添加文件git add README.md添加README.md 查看状态 $ git status On branch dev Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: .idea/vcs.xml modified: README.md 可以看到上述两个文件等待提交 提交 $ git commit -m "测试提交dev分支" [dev 7c5254a] 测试提交dev分支 2 files changed, 59 insertions(+) create mode 100644 .idea/vcs.xml 提交成功 三、合并分支用到的命令-- 合并分支 git merge [branch-name] 实际操作 切换到主分支 $ git checkout master Switched to branch 'master' Your branch is up to date with 'origin/master'. 合并分支 $ git merge dev Updating 57c8d40..1e0b9f7 Fast-forward .idea/misc.xml | 2 +- .idea/vcs.xml | 6 ++++ README.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 .idea/vcs.xml 这就将dev上修改的结果,合并到了master 查看状态 $ git status On branch master Your branch is ahead of 'origin/master' by 2 commits. (use "git push" to publish your local commits) nothing to commit, working tree clean 可以看到我在dev上的两次提交,也显示在了master中,并且查看文件,说明合并成功 四、提交本地分支到远程分支用到的命令-- 创建并提交远程分支(若没有远程分支,则会自动创建) git push origin [local-branch-name]:[remote-branch-name] 实际操作 查看当前分支 $ git branch -a dev * master remotes/origin/master 可以看到所有的分支,当前所在分支是master 创建远程分支dev $ git push origin master:dev fatal: HttpRequestException encountered. 发送请求时出错。 Username for 'https://github.com': MycroftWong Password for 'https://[email protected]': Enumerating objects: 14, done. Counting objects: 100% (14/14), done. Delta compression using up to 4 threads Compressing objects: 100% (10/10), done. Writing objects: 100% (10/10), 1.83 KiB | 468.00 KiB/s, done. Total 10 (delta 6), reused 0 (delta 0) remote: Resolving deltas: 100% (6/6), completed with 3 local objects. remote: remote: Create a pull request for 'dev' on GitHub by visiting: remote: https://github.com/MycroftWong/GitBranchLearn/pull/new/dev remote: To https://github.com/MycroftWong/GitBranchLearn.git * [new branch] master -> dev 创建时,输入账号密码,创建并提交成功,可以看到是本地master分支提交到了远程dev分支,查看github, 也是如此 五、创建分支跟踪理想状态:local dev -> local master -> remote dev -> remote master但是实际上只能本地分支跟踪远程分支 用到的命令 实际操作 local dev -> local master 合并在前面已经讲述了,切换到master分支后,合并dev分支git merge dev local master -> remote dev 远程跟踪在以后master的直接git pull, git push时会自动提交到跟踪的远程分支 $ git branch -u origin/dev master Branch 'master' set up to track remote branch 'dev' from 'origin'. 这样,本地master就跟踪(track)了远程分支dev remote dev -> remote master并没有直接的命令,因为local master和remote dev是相同的,所以可以直接提交local master到remote master git push origin master:master 参考Android Studio Git 分支实践如何进行 git 分支管理? 本项目GitBranchLearn","categories":[{"name":"工具 - git","slug":"工具-git","permalink":"https://blog.mycroft.wang/categories/%E5%B7%A5%E5%85%B7-git/"}],"tags":[{"name":"git","slug":"git","permalink":"https://blog.mycroft.wang/tags/git/"},{"name":"branch","slug":"branch","permalink":"https://blog.mycroft.wang/tags/branch/"}]},{"title":"购买阿里云服务器第一天","slug":"购买阿里云服务器第一天","date":"2019-06-09T03:18:10.000Z","updated":"2023-06-13T07:01:29.494Z","comments":true,"path":"2019/06/09/gou-mai-a-li-yun-fu-wu-qi-di-yi-tian/","link":"","permalink":"https://blog.mycroft.wang/2019/06/09/gou-mai-a-li-yun-fu-wu-qi-di-yi-tian/","excerpt":"","text":"购买阿里云服务器的第一天 ———— 配置前言使用优惠,购买了阿里云轻量级服务器,今天一开始的目的就很明确,能够在外网访问到服务器的内容。 安装因为之前使用过ubuntu进行开发,所以基本操作完全有,安装了jdk1.8.0-211和apache-tomcat-9.0.20。不过麻烦的是,购买的是centos系统,没有UI,只能用命令,安装还好,编辑文件就遇到了问题,然后就学习了vim简单的命令,配置了环境变量。 运行运行tomcat很简单,但是最后发现,阿里云没有开放8080端口,研究了很久,也不知道轻量级服务器如何开启8080端口,最后尝试将tomcat的端口设置为80,在我尝试连接之后差一点觉得没有希望了的时候,发现连接上了。 将本地项目发布到服务器上 开发打包 本地文件上传到服务器 启动 1. 开发打包参考文章IDEA 将项目打包war包 2. 本地文件上传到服务器参考文章1_如何将本地文件通过终端上传到linux服务器 /服务器/阿里云 3. 启动重启tomcat 结果失败,最后发现IDEA打包生成的war包里面的class字节码使用WebServlet注解,猜测是tomcat默认没有使用相应的jar包来解析,或者是IDEA并没有生成相应的类,查看web.xml中也没有相应的注册。今天没有什么时间了,后面再研究。","categories":[{"name":"网站搭建","slug":"网站搭建","permalink":"https://blog.mycroft.wang/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"}],"tags":[{"name":"aliyun","slug":"aliyun","permalink":"https://blog.mycroft.wang/tags/aliyun/"},{"name":"tomcat","slug":"tomcat","permalink":"https://blog.mycroft.wang/tags/tomcat/"}]},{"title":"制作酱香饼","slug":"制作酱香饼","date":"2019-06-08T03:15:58.000Z","updated":"2023-06-13T07:01:29.494Z","comments":true,"path":"2019/06/08/zhi-zuo-jiang-xiang-bing/","link":"","permalink":"https://blog.mycroft.wang/2019/06/08/zhi-zuo-jiang-xiang-bing/","excerpt":"","text":"今天和媳妇儿学习制作酱香饼时间线1. 搜索制作酱香饼的材料时间:2019年06月08日16:20:33 家庭制作酱香饼就不要太过分追求很多原料了,找到一份资料比较靠谱。在家做酱香饼的做法 原料 量 面粉 300g 食用油 适量 八角 适量 豆瓣酱 适量 海鲜酱 适量 甜面酱 适量 花椒粉 适量 白芝麻 适量 葱花 适量 2. 购买材料 原本就有的材料:面粉、食用油、豆瓣酱、花椒粉 去超市购买的材料:八角、甜面酱、白芝麻、葱花 没有买到的材料:海鲜酱 3. 制作1) 揉面团结果:失败 信了上面推荐文章的鬼,300g左右的面粉,加了200g左右的水,太多了。后面知道了,明明只需要一点点水就行了,加水也是慢慢的加。 2) 调酱结果:失败 把所有的调酱的材料放进去热,发现温度并不需要这么高,而且比例也不对,味道还凑合,最后用来拌饭了。 3) 煎饼结果:失败 没有擀面杖,面团也很软很软,所以不言而喻,失败了 4) 涂酱结果:没做 在煎饼的过程中发现没做好,就扔掉了,最后没有完成 结果整体的体验很差,两个人都没有经验,下次从大厨那里取经。","categories":[{"name":"生活","slug":"生活","permalink":"https://blog.mycroft.wang/categories/%E7%94%9F%E6%B4%BB/"}],"tags":[{"name":"生活","slug":"生活","permalink":"https://blog.mycroft.wang/tags/%E7%94%9F%E6%B4%BB/"}]}],"categories":[{"name":"Socks 5","slug":"Socks-5","permalink":"https://blog.mycroft.wang/categories/Socks-5/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.mycroft.wang/categories/CI-CD/"},{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/categories/Android/"},{"name":"Android - 基础","slug":"Android-基础","permalink":"https://blog.mycroft.wang/categories/Android-%E5%9F%BA%E7%A1%80/"},{"name":"开源库 - okhttp","slug":"开源库-okhttp","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okhttp/"},{"name":"开源库 - okio","slug":"开源库-okio","permalink":"https://blog.mycroft.wang/categories/%E5%BC%80%E6%BA%90%E5%BA%93-okio/"},{"name":"Android - View","slug":"Android-View","permalink":"https://blog.mycroft.wang/categories/Android-View/"},{"name":"学习计划","slug":"学习计划","permalink":"https://blog.mycroft.wang/categories/%E5%AD%A6%E4%B9%A0%E8%AE%A1%E5%88%92/"},{"name":"Android - 序列化","slug":"Android-序列化","permalink":"https://blog.mycroft.wang/categories/Android-%E5%BA%8F%E5%88%97%E5%8C%96/"},{"name":"Java - 基础知识","slug":"Java-基础知识","permalink":"https://blog.mycroft.wang/categories/Java-%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/"},{"name":"Android - 优化","slug":"Android-优化","permalink":"https://blog.mycroft.wang/categories/Android-%E4%BC%98%E5%8C%96/"},{"name":"Android - 图片","slug":"Android-图片","permalink":"https://blog.mycroft.wang/categories/Android-%E5%9B%BE%E7%89%87/"},{"name":"网站搭建","slug":"网站搭建","permalink":"https://blog.mycroft.wang/categories/%E7%BD%91%E7%AB%99%E6%90%AD%E5%BB%BA/"},{"name":"工具 - git","slug":"工具-git","permalink":"https://blog.mycroft.wang/categories/%E5%B7%A5%E5%85%B7-git/"},{"name":"spring boot - bug","slug":"spring-boot-bug","permalink":"https://blog.mycroft.wang/categories/spring-boot-bug/"},{"name":"生活","slug":"生活","permalink":"https://blog.mycroft.wang/categories/%E7%94%9F%E6%B4%BB/"}],"tags":[{"name":"CentOS","slug":"CentOS","permalink":"https://blog.mycroft.wang/tags/CentOS/"},{"name":"Socks 5","slug":"Socks-5","permalink":"https://blog.mycroft.wang/tags/Socks-5/"},{"name":"Proxy","slug":"Proxy","permalink":"https://blog.mycroft.wang/tags/Proxy/"},{"name":"Docker","slug":"Docker","permalink":"https://blog.mycroft.wang/tags/Docker/"},{"name":"gitlab","slug":"gitlab","permalink":"https://blog.mycroft.wang/tags/gitlab/"},{"name":"CI/CD","slug":"CI-CD","permalink":"https://blog.mycroft.wang/tags/CI-CD/"},{"name":"Ubuntu","slug":"Ubuntu","permalink":"https://blog.mycroft.wang/tags/Ubuntu/"},{"name":"OpenVPN","slug":"OpenVPN","permalink":"https://blog.mycroft.wang/tags/OpenVPN/"},{"name":"gitlab-runner","slug":"gitlab-runner","permalink":"https://blog.mycroft.wang/tags/gitlab-runner/"},{"name":"Android","slug":"Android","permalink":"https://blog.mycroft.wang/tags/Android/"},{"name":"Intent","slug":"Intent","permalink":"https://blog.mycroft.wang/tags/Intent/"},{"name":"Permission","slug":"Permission","permalink":"https://blog.mycroft.wang/tags/Permission/"},{"name":"权限","slug":"权限","permalink":"https://blog.mycroft.wang/tags/%E6%9D%83%E9%99%90/"},{"name":"ContentProvider","slug":"ContentProvider","permalink":"https://blog.mycroft.wang/tags/ContentProvider/"},{"name":"okhttp","slug":"okhttp","permalink":"https://blog.mycroft.wang/tags/okhttp/"},{"name":"http","slug":"http","permalink":"https://blog.mycroft.wang/tags/http/"},{"name":"开源","slug":"开源","permalink":"https://blog.mycroft.wang/tags/%E5%BC%80%E6%BA%90/"},{"name":"token","slug":"token","permalink":"https://blog.mycroft.wang/tags/token/"},{"name":"okio","slug":"okio","permalink":"https://blog.mycroft.wang/tags/okio/"},{"name":"源码","slug":"源码","permalink":"https://blog.mycroft.wang/tags/%E6%BA%90%E7%A0%81/"},{"name":"FileProvider","slug":"FileProvider","permalink":"https://blog.mycroft.wang/tags/FileProvider/"},{"name":"面试","slug":"面试","permalink":"https://blog.mycroft.wang/tags/%E9%9D%A2%E8%AF%95/"},{"name":"View","slug":"View","permalink":"https://blog.mycroft.wang/tags/View/"},{"name":"measure","slug":"measure","permalink":"https://blog.mycroft.wang/tags/measure/"},{"name":"layout","slug":"layout","permalink":"https://blog.mycroft.wang/tags/layout/"},{"name":"draw","slug":"draw","permalink":"https://blog.mycroft.wang/tags/draw/"},{"name":"touch event","slug":"touch-event","permalink":"https://blog.mycroft.wang/tags/touch-event/"},{"name":"Parcelable","slug":"Parcelable","permalink":"https://blog.mycroft.wang/tags/Parcelable/"},{"name":"Serializable","slug":"Serializable","permalink":"https://blog.mycroft.wang/tags/Serializable/"},{"name":"https","slug":"https","permalink":"https://blog.mycroft.wang/tags/https/"},{"name":"责任链模式","slug":"责任链模式","permalink":"https://blog.mycroft.wang/tags/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F/"},{"name":"ConnectInterceptor","slug":"ConnectInterceptor","permalink":"https://blog.mycroft.wang/tags/ConnectInterceptor/"},{"name":"RealConnectionPool","slug":"RealConnectionPool","permalink":"https://blog.mycroft.wang/tags/RealConnectionPool/"},{"name":"Transmitter","slug":"Transmitter","permalink":"https://blog.mycroft.wang/tags/Transmitter/"},{"name":"Exchange","slug":"Exchange","permalink":"https://blog.mycroft.wang/tags/Exchange/"},{"name":"RealConnection","slug":"RealConnection","permalink":"https://blog.mycroft.wang/tags/RealConnection/"},{"name":"Cache","slug":"Cache","permalink":"https://blog.mycroft.wang/tags/Cache/"},{"name":"缓存","slug":"缓存","permalink":"https://blog.mycroft.wang/tags/%E7%BC%93%E5%AD%98/"},{"name":"Interceptor","slug":"Interceptor","permalink":"https://blog.mycroft.wang/tags/Interceptor/"},{"name":"Chain","slug":"Chain","permalink":"https://blog.mycroft.wang/tags/Chain/"},{"name":"Java","slug":"Java","permalink":"https://blog.mycroft.wang/tags/Java/"},{"name":"String","slug":"String","permalink":"https://blog.mycroft.wang/tags/String/"},{"name":"StringBuilder","slug":"StringBuilder","permalink":"https://blog.mycroft.wang/tags/StringBuilder/"},{"name":"StringBuffer","slug":"StringBuffer","permalink":"https://blog.mycroft.wang/tags/StringBuffer/"},{"name":"优化","slug":"优化","permalink":"https://blog.mycroft.wang/tags/%E4%BC%98%E5%8C%96/"},{"name":"apk","slug":"apk","permalink":"https://blog.mycroft.wang/tags/apk/"},{"name":"Luban","slug":"Luban","permalink":"https://blog.mycroft.wang/tags/Luban/"},{"name":"图片","slug":"图片","permalink":"https://blog.mycroft.wang/tags/%E5%9B%BE%E7%89%87/"},{"name":"压缩","slug":"压缩","permalink":"https://blog.mycroft.wang/tags/%E5%8E%8B%E7%BC%A9/"},{"name":"bug","slug":"bug","permalink":"https://blog.mycroft.wang/tags/bug/"},{"name":"hexo","slug":"hexo","permalink":"https://blog.mycroft.wang/tags/hexo/"},{"name":"git","slug":"git","permalink":"https://blog.mycroft.wang/tags/git/"},{"name":"aliyun","slug":"aliyun","permalink":"https://blog.mycroft.wang/tags/aliyun/"},{"name":"spring boot","slug":"spring-boot","permalink":"https://blog.mycroft.wang/tags/spring-boot/"},{"name":"mail","slug":"mail","permalink":"https://blog.mycroft.wang/tags/mail/"},{"name":"categories","slug":"categories","permalink":"https://blog.mycroft.wang/tags/categories/"},{"name":"android","slug":"android","permalink":"https://blog.mycroft.wang/tags/android/"},{"name":"EditText","slug":"EditText","permalink":"https://blog.mycroft.wang/tags/EditText/"},{"name":"软键盘","slug":"软键盘","permalink":"https://blog.mycroft.wang/tags/%E8%BD%AF%E9%94%AE%E7%9B%98/"},{"name":"nginx","slug":"nginx","permalink":"https://blog.mycroft.wang/tags/nginx/"},{"name":"blog","slug":"blog","permalink":"https://blog.mycroft.wang/tags/blog/"},{"name":"github","slug":"github","permalink":"https://blog.mycroft.wang/tags/github/"},{"name":"gitee","slug":"gitee","permalink":"https://blog.mycroft.wang/tags/gitee/"},{"name":"branch","slug":"branch","permalink":"https://blog.mycroft.wang/tags/branch/"},{"name":"tomcat","slug":"tomcat","permalink":"https://blog.mycroft.wang/tags/tomcat/"},{"name":"生活","slug":"生活","permalink":"https://blog.mycroft.wang/tags/%E7%94%9F%E6%B4%BB/"}]}