开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

【奇技淫巧】AndroidStudio Nexus3x搭建

发表于 2020-02-16

之前写过 Android Studio 多个项目依赖同一个模块的用法

不过在使用中遇到了几个问题,编译速度慢,总是显示出关联项目。

所以决定将公共模块aar使用 maven 私服管理,在此记录之。

Nexus3 下载与安装

官网

下载后解压,这里以windows为例

打开 D:\nexus-3.20.1-01-win64\nexus-3.20.1-01\bin 目录

在该目录下执行

1
复制代码nexus.exe /run

见到 Started Sonatype Nexus OSS 3.20.1-01 字样即成功

打开 http://localhost:8081/ 进入配置界面

详情参考 Maven私服Nexus 3.x搭建

网上文章很多,下面说一下搭建过程中出现的问题。

问题及解决方案

1 unable to resolve dependency for:xxx

正常配置并引入私服的依赖,但是提示无法resolve该依赖

解决:

1. Nexus 允许匿名登录

勾选允许匿名登录

这种操作很暴力

2. 引用依赖配置账号密码

project 的 build.gradle allprojects->repositories中配置maven url 的同时配置用户名密码

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码allprojects {
repositories {
google()
jcenter()
maven {
credentials {
username 'username'
password 'password'
}
url 'http://localhost:8081/repository/Android/'
}
}
}

2 aar中的class.jar为空

成功引入依赖后发现找不到aar中的类

详情参考 解决aar混淆后包里是空的问题,android混淆讲解

解决:

打出的aar是release的,所以关闭release的混淆,或者想暴露出的类禁止混淆即可

3 错误: 编码GBK的不可映射字符

生成 java doc 时提示错误: 编码GBK的不可映射字符

在module的build.gradle中配置

1
2
3
4
复制代码tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
options.addStringOption('encoding', 'UTF-8')
}

4 javadoc: 错误 - 非法的程序包名称

在 Root Project 下的 build.gradle 文件中 buildscript 下的 dependencies 中添加:

1
复制代码classpath 'org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.17'

module 的 build.gradle 应用插件

1
复制代码apply plugin: 'org.jetbrains.dokka-android'

详情参考 使用Gradle打包Kotlin项目代码、生成Kotlin代码文档

5 deploy 时出现 500, ReasonPhrase: Internal Privoxy Error.

1
2
3
4
5
6
7
复制代码 > Failed to deploy artifacts: Could not transfer artifact 
cn.example.baselib:library-base:aar:0.0.1
from/to remote (http://localhost:8081/repository/Android/):

Failed to transfer file:
http://localhost:8081/repository/Android/cn/example/baselib/library-base/0.0.1/library-base-0.0.1.aar.
Return code is: 500, ReasonPhrase: Internal Privoxy Error.

解决:
关闭Android Studio代理

windows在C:\Users\Administrator\.gradle\gradle.properties文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#Mon Jan 06 13:56:29 CST 2020
移除代理
#systemProp.http.proxyHost=127.0.0.1
#systemProp.http.proxyPort=1080

感谢

Android Studio将项目发布到Maven仓库(3种方式最新最全)

Kotlin与Java混编项目的Nexus私有仓库持续交付与集成

Maven私服Nexus 3.x搭建

解决aar混淆后包里是空的问题,android混淆讲解

使用Gradle打包Kotlin项目代码、生成Kotlin代码文档

雕虫晓技(四) 搭建私有Maven仓库(带容灾备份)


关于我


我是 Fly_with24

  • 掘金
  • 简书
  • Github

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

AspectJ入门及在IDEA中的配置

发表于 2020-02-15

AspectJ安装AspectJ下载AspectJ环境变量配置IDEA下配置AspectJ激活AspectJ支持插件添加aspectjrt.jar依赖或Maven依赖使用AspectJ编译器(ajc)AspectJ简单示例示例一示例二问题记录参考文献

AspectJ

AspectJ 是一个基于 Java 语言的 AOP 框架,提供了强大的 AOP 功能,其他很多 AOP 框架都借鉴或采纳其中的一些思想。

AspectJ 是 Java 语言的一个 AOP 实现,其主要包括两个部分:第一个部分定义了如何表达、定义 AOP 编程中的语法规范,通过这套语言规范,我们可以方便地用 AOP 来解决 Java 语言中存在的交叉关注点问题;另一个部分是工具部分,包括编译器、调试工具等。

AspectJ 是最早、功能比较强大的 AOP 实现之一,对整套 AOP 机制都有较好的实现,很多其他语言的 AOP 实现,也借鉴或采纳了 AspectJ 中很多设计。在 Java 领域,AspectJ 中的很多语法结构基本上已成为 AOP 领域的标准。

安装AspectJ

下载AspectJ

安装 AspectJ 首先要到 AspectJ官网下载一个可执行的Jar包。

即可下载到一个可执行的 JAR 包,我下的是 aspectj-1.8.14.jar,使用 java -jar aspectj-1.8.14.jar 命令,

多次单击“Next”按钮, 并选择合适的安装目录,即可成功安装 AspectJ。

在安装了 AspectJ 之后,在其安装目录下,可以看到如下的文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码├─bin // 该路径下存放了 aj、aj5、ajc、ajdoc、ajbrowser 等命令。  
│  ├─aj.bat
│  ├─aj5.bat
│  ├─ajbrowser
│  ├─ajbrowser.bat
│  ├─ajc // 其中 ajc 命令最常用,它的作用类似于 javac,用于对普通 Java 类进行编译时增强。
│  ├─ajc.bat
│  ├─ajdoc
│  ├─ajdoc.bat
├─doc // 该路径下存放了AspectJ的使用说明、参考手册、API文档等文档。
├─lib // 该路径下的4个Jar文件是AspectJ的核心类库
│  ├─aspectjrt.jar
│  ├─aspectjtools.jar
│  ├─aspectjweaver.jar
│  ├─org.aspectj.matcher.jar
├─LICENSE-AspectJ.html    相关授权文件
└─README-AspectJ.html

环境变量配置

1
2
3
复制代码CLASSPATH:.;D:\Java_About\Java_component\aspectj-1.8.14\lib\aspectjrt.jar;  

Path:D:\Java_About\Java_component\aspectj-1.8.14\bin

测试是否安装成功用 ajc 命令:

IDEA下配置AspectJ

虽然 AspectJ 是 Eclipse 基金组织的开源项目,而且提供了 Eclipse 的 AJDT 插件(AspectJ Development Tools)来开发 AspectJ 应用,但 AspectJ 并不是只能在 Eclipse 中开发。由于我使用的是 IntelliJ IDEA 2019.1.2 版本,所以这里介绍IDEA中如何开发 AspectJ。

只有专业版(Ultimate)的 IntelliJ IDEA 才支持 AspectJ 的开发,而且 IDEA 也提供了 官方文档。

激活AspectJ支持插件

在专业版 IDEA 中开发 AspectJ,需要确保下述插件被激活:

  • Spring AOP/@AspectJ
  • AspectJ Support

由于本人使用的是 IDEA 2019.1.3 版本,所以同网上的说法不太一样,配置如下:

这两个插件在 2019 版本已经存在,因此不需要另外搜索进行安装。

添加aspectjrt.jar依赖或Maven依赖

添加aspectjrt.jar依赖

在项目中添加 aspectjrt.jar依赖,aspectjrt.jar即 AspectJ 安装目录中lib目录下的 jar 包。

接着进行如下操作,将该 Jar 包添加进项目依赖中:

  1. 打开 Project Structure 对话框(Ctrl+Shift+Alt+S)。
  2. 对应于创建项目级别的或者IDE级别的库,分别选择 Libraries 或者 Global Libraries。
  3. 点击+号并选择 java。
  4. 在弹出的对话框中,选择刚才我们添加进项目的lib目录下的 aspectjrt.jar 文件。
  5. 最后点击OK按钮即可。

添加Maven依赖

如果采用 Maven 来管理项目,则可以在 pom.xml 文件中添加相关依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<?xml version="1.0" encoding="UTF-8"?>  
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.msdn</groupId>
    <artifactId>spring_aop</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.14</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.8.14</version>
        </dependency>
    </dependencies>
</project>

上述两种方法都可以达到同样的效果。

使用AspectJ编译器(ajc)

IDEA 默认使用javac编译器,如果要使用 AspectJ 的编译器ajc,需要在 IDEA 中进行相应的配置。

打开settings对话框,然后做如下配置:

AspectJ简单示例

实际上,AspectJ 的用法非常简单,就像我们使用 JDK 编译、运行 Java 程序一样。下面通过一个简单的程序来示范 AspectJ 的用法,并分析 AspectJ 如何在编译时进行增强。

示例一

HelloWorld.java

1
2
3
4
5
6
7
8
9
10
java复制代码public class HelloWorld {  
    public void sayHello(){
        System.out.println("Hello AspectJ");
    }

    public static void main(String[] args) {
        HelloWorld hello = new HelloWorld();
        hello.sayHello();
    }
}

该类中有一个 sayHello()方法,该方法打印出了一句话!

假设现在我们需要在 sayHello()方法之前启动事务,当该方法结束时关闭事务,那么在传统的编程模式下,我们必须手动修改 sayHello()方法。而如果使用 AspectJ,我们则不需要修改上面的方法,只需要添加一个切面即可。

TxAspect.aj

1
2
3
4
5
6
7
java复制代码public aspect TxAspect {  
    void around():call(void HelloWorld.sayHello()){
        System.out.println("开始事务。。。");
        proceed();
        System.out.println("结束事务。。。");
    }
}

上面的 TxAspect 根本不是一个 Java 类,所以 aspect 也不是 Java 支持的关键字,它只是 AspectJ 才能识别的关键字。 其后缀为.aj,该文件的完整文件名为TxAspect.aj。切面的语法只有AspectJ可以识别,并使用其特殊的编译器ajc来编译。

这段代码拦截Hello.sayHello()方法,并在其执行之前开始事务,proceed()方法代表回调原来的sayHello()方法,执行结束后结束事务。

执行结果为:

1
2
3
java复制代码开始事务。。。  
Hello AspectJ
结束事务。。。

从上面运行结果来看,我们完全可以不对 HelloWorld.java 类进行任何修改,就给它插入了事务管理的功能,这正是面向切面编程的意义所在。从这个例子中我们也可以体会到 AspectJ 的易学易用、无侵入(不需要继承任何类和接口)的特性。

示例二

除了上述事务管理的功能,还可以在 sayHello()方法后增加记录日志的功能。我们再定义一个 LogAspect,

LogAspect.aj

1
2
3
4
5
6
7
8
9
10
11
java复制代码public aspect LogAspect {  

    // 定义一个 PointCut,其名为 logPointcut
    // 该 PointCut 对应于指定 HelloWorld 对象的 sayHello 方法
    pointcut logPointCut():execution(void HelloWorld.sayHello());

    // 在 logPointcut 之后执行下面代码块
    after():logPointCut(){
        System.out.println("记录日志。。。。");
    }
}

上述代码定义了一个 Pointcut:logPointcut - 等同于执行 HelloWorld 对象的 sayHello() 方法,并指定在 logPointcut 之后执行简单的代码块,也就是说,在 sayHello() 方法之后执行指定代码块。

执行结果为:

1
2
3
4
java复制代码开始事务。。。  
Hello AspectJ
记录日志。。。。
结束事务。。。

从上面运行结果来看,通过使用 AspectJ 提供的 AOP 支持,我们可以为 sayHello() 方法不断增加新功能。

为什么在对 HelloWorld 类没有任何修改的前提下,而 HelloWorld 类能不断地、动态增加新功能呢?这看上去并不符合 Java 基本语法规则啊。实际上我们可以使用 Java 的反编译工具来反编译前面程序生成的 HelloWorld.class 文件,发现 HelloWorld.class 文件的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class HelloWorld {  
    public HelloWorld() {
    }

    public void sayHello() {
        try {
            System.out.println("Hello AspectJ");
        } catch (Throwable var2) {
            LogAspect.aspectOf().ajc$after$com_msdn_aspectj_LogAspect$1$9e12ed77();
            throw var2;
        }

        LogAspect.aspectOf().ajc$after$com_msdn_aspectj_LogAspect$1$9e12ed77();
    }

    public static void main(String[] args) {
        HelloWorld hello = new HelloWorld();
        sayHello_aroundBody1$advice(hello, TxAspect.aspectOf(), (AroundClosure)null);
    }
}

不难发现这个 HelloWorld.class 文件不是由原来的 HelloWorld.java 文件编译得到的,该 HelloWorld.class 里新增了很多内容,sayHello() 方法中增加了日志功能,主方法中增加了事务管理功能——这表明 AspectJ 在编译时“自动”编译得到了一个新类,这个新类增强了原有的 HelloWorld.java 类的功能,因此 AspectJ 通常被称为编译时增强的 AOP 框架。

问题记录

在进行案例测试的过程中,遇到了一系列的问题,总结归纳如下:

1、安装 AspectJ 时,我首先安装的是 aspectj-1.9.2 版本,但是后续实际测试过程中,由于在 IDEA 中配置AspectJ编译器时错误导致代码执行有误,当时我配置的情况如下图所示:

图中红框标记处,我以为是填写版本号,但是代码执行之后会报这样的错误:

错误信息为:

1
java复制代码Error:ajc: Compliance level '1.8' is incompatible with target level '9'. A compliance level '9' or better is required

原因:本地使用的 JDK 版本为 1.8,此处如果配置1.9的话,会导致 Javac 编译器配置也发生变化,导致错误发生。

所以这也是为啥我改为安装 aspectj-1.8.14,当时以为需要和 JDK 版本统一。但是实际上图中红框标识处根本不需要填写内容。如下图所示:

2、执行过程中遇到这样的错误,错误信息如下:

1
java复制代码Error: java: Compliance level '1.6' is incompatible with target level '1.8'. A compliance level '1.8' or better is required

点击 File 标签里的 Project Structure,选择 Project Settings->Modules,选择1.8版本对应的 language level。

参考文献

AspectJ——简介以及在IntelliJ IDEA下的配置

AspectJ入门

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】深入研究ViewBinding 在 include,

发表于 2020-02-15

原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities

作者:Somesh Kumar

译者:Fly_with24

译者注:2020.02.27更新,探讨了 ViewBinding 的空安全问题,见文章末尾

Image Source: Google I/O 2019

谷歌在2019 I/O 大会中的 What’s New in Architecture Components 介绍了 view binding

在 What’s New in Architecture Components 中,有一个简短的关于view binding 的演讲,演讲中将 view binding 与现有解决方案进行了比较,并进一步讨论了为什么view binding 比 data binding 或 Kotlin synthetics 等现有解决方案更好。

对我而言,Kotlin synthetics 运行良好,但是没有编译时的安全性,这意味着所有 ID 都位于全局命名空间中。因此,如果您使用的 ID 具有相同的名称,并且从错误的布局导入 ID, 由于ID不是当前布局的一部分,导致崩溃,除非您将应用程序运行到该布局,否则无法提前知道这一点。

这篇文章很好地概述了 Kotlin synthetics 的问题

The Argument Over Kotlin Synthetics

View Binding 将在 Android Studio 3.6 稳定版中提供(译者注:当前Android Studio稳定版版本为3.5.3),如果您想要使用它,您可以下载 Android Studio 3.6(2020.02.25更新) 或者 Android Studio 4.0 Canary

view binding 的主要优点是所有绑定类都是由Gradle插件生成的,因此它对构建时间没有影响,并且具有编译时安全性(我们将在示例中看到)。

首先,启用 view binding, 我们需要在 module 的build.gradle文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码// Android Studio 3.6
android {
viewBinding {
enabled = true
}
}

// Android Studio 4.0
android {
buildFeatures {
viewBinding = true
}
}

注意:视图绑定是逐模块启用的,因此,如果您具有多模块项目设置,则需要在每个 build.gradle 文件中添加以上代码。

如果要在特定的布局禁用 view binding,则需要在布局文件的根视图中添加 tools:viewBindingIgnore = “true”。

启用后,我们可以立即开始使用它,并且当您完成同步 build.gradle 文件时,默认情况下会生成所有绑定类。

它通过将XML布局文件名转换为驼峰式大小写并在其末尾添加 Binding 来生成绑定类。 例如,如果您的布局文件名为 activity_splash,则它将生成绑定类为 ActivitySplashBinding。

如何使用它?

activity 中使用

1
2
3
4
5
6
复制代码    override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvVersionName.text = getString(R.string.version)
}

我们有一个名为 activity_splash 的布局文件,里面有一个ID为 tvVersionName 的 TextView ,因此在使用view binding 时,我们要做的就是获取绑定类的引用,例如:

1
复制代码val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)

在 setContentView() 方法中使用 getRoot() ,该方法将返回布局的根布局。可以从我们创建的绑定类对象访问视图,并且可以在创建对象后立即使用它,如下所示:

1
复制代码binding.tvVersionName.text = getString(R.string.version)

在这里,绑定类知道 tvVersionName 是TextView,因此我们不必担心类型转换。

fragment 中使用

1
2
3
4
5
6
7
8
9
10
11
12
复制代码class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
_binding = null
}
}

在 fragment 中,使用 view binding 有些不同。 我们需要传递 LayoutInflator,ViewGroup和一个 attachToRoot 布尔变量,这些变量是通过覆盖 onCreateView 获得的。

我们可以通过调用 binding.root 返回 view。您还注意到,我们使用了两个不同的变量 binding 和 _binding,并且 _binding 变量在 onDestroyView() 中设置为null。

这是因为该 fragment 的生命周期与 activity 的生命周期不同,并且该fragment 可以超出其视图的生命周期,因此如果不将其设置为null,则可能会发生内存泄漏。

另一个变量通过 !! 使一个变量为可空值而使另一个变量为非空值避免了空检查。 。

在 RecyclerView adapter 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码
class PaymentAdapter(private val paymentList: List<PaymentBean>) : RecyclerView.Adapter<PaymentAdapter.PaymentHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentHolder {
val itemBinding = RowPaymentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PaymentHolder(itemBinding)
}

override fun onBindViewHolder(holder: PaymentHolder, position: Int) {
val paymentBean: PaymentBean = paymentList[position]
holder.bind(paymentBean)
}

override fun getItemCount(): Int = paymentList.size

class PaymentHolder(private val itemBinding: RowPaymentBinding) : RecyclerView.ViewHolder(itemBinding.root) {
fun bind(paymentBean: PaymentBean) {
itemBinding.tvPaymentInvoiceNumber.text = paymentBean.invoiceNumber
itemBinding.tvPaymentAmount.text = paymentBean.totalAmount
}
}
}

row_payment.xml 是我们用于 RecyclerView item 的布局文件,对应生成的绑定类 RowPaymentBinding。

现在,我们所需要做的就是在onCreateViewHolder() 中调用 inflate() 方法生成 RowPaymentBinding 对象并传递到 PaymentHolder 主构造器中,并将 itemBinding.root 传递给 RecyclerView .ViewHolder() 构造函数。

处理<include>标签

view binding 可以与 <include> 标签一起使用。 布局中通常包含两种 <include> 标签,带或不带<merge> 标签。

  • <inlude> 不带 <merge>标签

我们需要为<include> 分配一个 ID,然后使用该 ID 来访问包含布局中的视图。让我们来看一个例子。

app_bar.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:background="?colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

main_layout.xml

1
2
3
4
5
6
7
8
9
10
11
12
复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<include
android:id="@+id/appbar"
layout="@layout/app_bar"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

在上面的代码中,我们在布局文件中包括了一个通用工具栏,<include> 有一个 android:id=“@+id/appbar” ID,我们将使用它从 app_bar.xml 中访问工具栏并将其设置为我们的 action bar。

1
2
3
4
5
6
复制代码    override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MainLayoutBinding = MainLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.appbar.toolbar)
}
  • <inlude> 带 <merge>标签

当在一个布局中包含另一个布局时,我们通常使用一个带有 <merge> 标记的布局,这有助于消除布局嵌套。

placeholder.xml

1
2
3
4
5
6
7
8
9
复制代码<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

<TextView
android:id="@+id/tvPlaceholder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</merge>

fragment_order.xml

1
2
3
4
5
6
7
复制代码<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<include layout="@layout/placeholder" />

</androidx.constraintlayout.widget.ConstraintLayout>

如果我们尝试为该 <include> 提供ID,view binding 不会在绑定类中生成ID,因此我们无法像使用普通 include 那样访问视图。

在这种情况下,我们有 PlaceholderBinding,它是 placeholder.xml(<merge> 布局文件)的自动生成的类。我们可以调用其bind()方法并传递包含它的布局的根视图。

1
2
3
4
5
6
复制代码  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentOrderBinding.inflate(layoutInflater, container, false)
placeholderBinding = PlaceholderBinding.bind(binding.root)
placeholderBinding.tvPlaceholder.text = getString(R.string.please_wait)
return binding.root
}

然后,我们可以从我们的类(如 placeholderBinding.tvPlaceholder.text)访问 placeholder.xml 内部的视图。

感谢阅读。希望收到您的评论。

  • Android Developer docs — view binding

译者补充

在 fragment 中使用 view binding 比较麻烦,译者提供一个 BaseFragment 的封装供大家参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码abstract class BaseFragment<T : ViewBinding>(layoutId: Int) : Fragment(layoutId) {
private var _binding: T? = null

val binding get() = _binding!!

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = initBinding(view)
init()
}

/**
* 初始化 [_binding]
*/
abstract fun initBinding(view: View): T

abstract fun init()

override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
1
2
3
4
5
6
7
8
9
10
11
复制代码class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {

override fun initBinding(view: View): FragmentHomeBinding = FragmentHomeBinding.bind(view)

override fun init() {
binding.viewPager.adapter = SectionsPagerAdapter(this)
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
tab.text = TAB_TITLES[position]
}.attach()
}
}

译者补充2(2020.02.27更新)关于ViewBinding的空安全

关于ViewBinding旋转屏幕等状态的空安全问题,译者进行了测试,步骤如下:

  1. 创建横竖屏两套布局文件,内容只有一个TextView,id 分别为 hello1 hello2
  2. 分别使用 kotlin 和 java 创建 activity ,使用 ViewBinding 将 hello1 text 设置为 “你好”
  3. 运行项目并进行横竖屏切换

结论:kotlin 语言项目编译不通过,java 语言项目正常运行,旋转后空指针异常

分析:打开 ViewBinding 生成的 Binding 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码  /**
* This binding is not available in all configurations.
* <p>
* Present:
* <ul>
* <li>layout/</li>
* </ul>
*
* Absent:
* <ul>
* <li>layout-land/</li>
* </ul>
*/
@Nullable
public final TextView hello1;

/**
* This binding is not available in all configurations.
* <p>
* Present:
* <ul>
* <li>layout-land/</li>
* </ul>
*
* Absent:
* <ul>
* <li>layout/</li>
* </ul>
*/
@Nullable
public final TextView hello2;

@Nullable 注解 解释了 kotlin 为什么编译不通过,而对于 java 该注解仅会 lint 提示开发者该位置可能出现空指针


关于我


我是 fly_with24

  • 掘金
  • 简书
  • Github

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你连 HTTPS 原理都不懂,还讲“中间人攻击”?

发表于 2020-02-15

HTTPS

随着 HTTPS 建站的成本下降,现在大部分的网站都已经开始用上 HTTPS 协议。大家都知道 HTTPS 比 HTTP 安全,也听说过与 HTTPS 协议相关的概念有 SSL 、非对称加密、 CA证书等,但对于以下灵魂三拷问可能就答不上了:

  1. 为什么用了 HTTPS 就是安全的?
  2. HTTPS 的底层原理如何实现?
  3. 用了 HTTPS 就一定安全吗?

本文将层层深入,从原理上把 HTTPS 的安全性讲透。

HTTPS 的实现原理

大家可能都听说过 HTTPS 协议之所以是安全的是因为 HTTPS 协议会对传输的数据进行加密,而加密过程是使用了非对称加密实现。但其实,HTTPS 在内容传输的加密上使用的是对称加密,非对称加密只作用在证书验证阶段。

HTTPS的整体过程分为证书验证和数据传输阶段,具体的交互过程如下:

WX20191127-133805@2x.png

① 证书验证阶段

  1. 浏览器发起 HTTPS 请求
  2. 服务端返回 HTTPS 证书
  3. 客户端验证证书是否合法,如果不合法则提示告警

② 数据传输阶段

  1. 当证书验证合法后,在本地生成随机数
  2. 通过公钥加密随机数,并把加密后的随机数传输到服务端
  3. 服务端通过私钥对随机数进行解密
  4. 服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输

为什么数据传输是用对称加密?

首先,非对称加密的加解密效率是非常低的,而 http 的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的;

另外,在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。

为什么需要 CA 认证机构颁发证书?

HTTP 协议被认为不安全是因为传输过程容易被监听者勾线监听、伪造服务器,而 HTTPS 协议主要解决的便是网络传输的安全性问题。

首先我们假设不存在认证机构,任何人都可以制作证书,这带来的安全风险便是经典的“中间人攻击”问题。
“中间人攻击”的具体过程如下:

WX20191126-212406@2x.png

过程原理:

  1. 本地请求被劫持(如DNS劫持等),所有请求均发送到中间人的服务器
  2. 中间人服务器返回中间人自己的证书
  3. 客户端创建随机数,通过中间人证书的公钥对随机数加密后传送给中间人,然后凭随机数构造对称加密对传输内容进行加密传输
  4. 中间人因为拥有客户端的随机数,可以通过对称加密算法进行内容解密
  5. 中间人以客户端的请求内容再向正规网站发起请求
  6. 因为中间人与服务器的通信过程是合法的,正规网站通过建立的安全通道返回加密后的数据
  7. 中间人凭借与正规网站建立的对称加密算法对内容进行解密
  8. 中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输
  9. 客户端通过与中间人建立的对称加密算法对返回结果数据进行解密

由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。

浏览器是如何确保 CA 证书的合法性?

1. 证书包含什么信息?

  • 颁发机构信息
  • 公钥
  • 公司信息
  • 域名
  • 有效期
  • 指纹
  • ……

2. 证书的合法性依据是什么?

首先,权威机构是要有认证的,不是随便一个机构都有资格颁发证书,不然也不叫做权威机构。另外,证书的可信性基于信任制,权威机构需要对其颁发的证书进行信用背书,只要是权威机构生成的证书,我们就认为是合法的。所以权威机构会对申请者的信息进行审核,不同等级的权威机构对审核的要求也不一样,于是证书也分为免费的、便宜的和贵的。

3. 浏览器如何验证证书的合法性?

浏览器发起 HTTPS 请求时,服务器会返回网站的 SSL 证书,浏览器需要对证书做以下验证:

  1. 验证域名、有效期等信息是否正确。证书上都有包含这些信息,比较容易完成验证;
  2. 判断证书来源是否合法。每份签发证书都可以根据验证链查找到对应的根证书,操作系统、浏览器会在本地存储权威机构的根证书,利用本地根证书可以对对应机构签发证书完成来源验证;
    WX20191127-084216@2x.png
  3. 判断证书是否被篡改。需要与 CA 服务器进行校验;
  4. 判断证书是否已吊销。通过CRL(Certificate Revocation List 证书注销列表)和 OCSP(Online Certificate Status Protocol 在线证书状态协议)实现,其中 OCSP 可用于第3步中以减少与 CA 服务器的交互,提高验证效率

以上任意一步都满足的情况下浏览器才认为证书是合法的。

这里插一个我想了很久的但其实答案很简单的问题:
既然证书是公开的,如果要发起中间人攻击,我在官网上下载一份证书作为我的服务器证书,那客户端肯定会认同这个证书是合法的,如何避免这种证书冒用的情况?
其实这就是非加密对称中公私钥的用处,虽然中间人可以得到证书,但私钥是无法获取的,一份公钥是不可能推算出其对应的私钥,中间人即使拿到证书也无法伪装成合法服务端,因为无法对客户端传入的加密数据进行解密。

4. 只有认证机构可以生成证书吗?

如果需要浏览器不提示安全风险,那只能使用认证机构签发的证书。但浏览器通常只是提示安全风险,并不限制网站不能访问,所以从技术上谁都可以生成证书,只要有证书就可以完成网站的 HTTPS 传输。例如早期的 12306 采用的便是手动安装私有证书的形式实现 HTTPS 访问。
WX20191127-130501@2x.png

本地随机数被窃取怎么办?

证书验证是采用非对称加密实现,但是传输过程是采用对称加密,而其中对称加密算法中重要的随机数是由本地生成并且存储于本地的,HTTPS 如何保证随机数不会被窃取?

其实 HTTPS 并不包含对随机数的安全保证,HTTPS 保证的只是传输过程安全,而随机数存储于本地,本地的安全属于另一安全范畴,应对的措施有安装杀毒软件、反木马、浏览器升级修复漏洞等。

用了 HTTPS 会被抓包吗?

HTTPS 的数据是加密的,常规下抓包工具代理请求后抓到的包内容是加密状态,无法直接查看。

但是,正如前文所说,浏览器只会提示安全风险,如果用户授权仍然可以继续访问网站,完成请求。因此,只要客户端是我们自己的终端,我们授权的情况下,便可以组建中间人网络,而抓包工具便是作为中间人的代理。通常 HTTPS 抓包工具的使用方法是会生成一个证书,用户需要手动把证书安装到客户端中,然后终端发起的所有请求通过该证书完成与抓包工具的交互,然后抓包工具再转发请求到服务器,最后把服务器返回的结果在控制台输出后再返回给终端,从而完成整个请求的闭环。

既然 HTTPS 不能防抓包,那 HTTPS 有什么意义?
HTTPS 可以防止用户在不知情的情况下通信链路被监听,对于主动授信的抓包操作是不提供防护的,因为这个场景用户是已经对风险知情。要防止被抓包,需要采用应用级的安全防护,例如采用私有的对称加密,同时做好移动端的防反编译加固,防止本地算法被破解。

总结

以下用简短的Q&A形式进行全文总结:

Q: HTTPS 为什么安全?
A: 因为 HTTPS 保证了传输安全,防止传输过程被监听、防止数据被窃取,可以确认网站的真实性。

Q: HTTPS 的传输过程是怎样的?
A: 客户端发起 HTTPS 请求,服务端返回证书,客户端对证书进行验证,验证通过后本地生成用于改造对称加密算法的随机数,通过证书中的公钥对随机数进行加密传输到服务端,服务端接收后通过私钥解密得到随机数,之后的数据交互通过对称加密算法进行加解密。

Q: 为什么需要证书?
A: 防止”中间人“攻击,同时可以为网站提供身份证明。

Q: 使用 HTTPS 会被抓包吗?
A: 会被抓包,HTTPS 只防止用户在不知情的情况下通信被监听,如果用户主动授信,是可以构建“中间人”网络,代理软件可以对传输内容进行解密。

BLOG地址:www.liangsonghua.com

关注微信公众号:松花皮蛋的黑板报,获取更多精彩!

公众号介绍:分享在京东工作的技术感悟,还有JAVA技术和业内最佳实践,大部分都是务实的、能看懂的、可复现的

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Django源码分析(一):自动重启

发表于 2020-02-14

本文基于 django-2.1.x 版本系列编写。

初试 - 文件变化后 server 自动重启

在此之前,不妨先了解下 django 是如何做到自动重启的

开始

django 使用 runserver 命令的时候,会启动俩个进程。

runserver 主要调用了 django/utils/autoreload.py 下 main 方法。

至于为何到这里的,我们这里不作详细的赘述,后面篇章会进行说明。

主线程通过 os.stat 方法获取文件最后的修改时间进行比较,继而重新启动 django 服务(也就是子进程)。

大概每秒监控一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码# django/utils/autoreload.py 的 reloader_thread 方法

def reloader_thread():
...
# 监听文件变化
# -- Start
# 这里主要使用了 `pyinotify` 模块,因为目前可能暂时导入不成功,使用 else 块代码
# USE_INOTIFY 该值为 False
if USE_INOTIFY:
fn = inotify_code_changed
else:
fn = code_changed
# -- End
while RUN_RELOADER:
change = fn()
if change == FILE_MODIFIED:
sys.exit(3) # force reload
elif change == I18N_MODIFIED:
reset_translations()
time.sleep(1)

code_changed 根据每个文件的最好修改时间是否发生变更,则返回 True 达到重启的目的。

父子进程&多线程

关于重启的代码在 python_reloader 函数内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
python复制代码
# django/utils/autoreload.py

def restart_with_reloader():
import django.__main__
while True:
args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions]
if sys.argv[0] == django.__main__.__file__:
# The server was started with `python -m django runserver`.
args += ['-m', 'django']
args += sys.argv[1:]
else:
args += sys.argv
new_environ = {**os.environ, 'RUN_MAIN': 'true'}
exit_code = subprocess.call(args, env=new_environ)
if exit_code != 3:
return exit_code


def python_reloader(main_func, args, kwargs):
# 一开始环境配置是没有该变量的,所有走的是 else 语句块
if os.environ.get("RUN_MAIN") == "true":
# 开启一个新的线程启动服务
_thread.start_new_thread(main_func, args, kwargs)
try:
# 程序接着向下走,监控文件变化
# 文件变化,退出该进程,退出码反馈到了 subprocess.call 接收处...
reloader_thread()
except KeyboardInterrupt:
pass
else:
try:
# 而在 restart_with_reloader 这个函数设置了 RUN_MAIN 变量
exit_code = restart_with_reloader()
if exit_code < 0:
os.kill(os.getpid(), -exit_code)
else:
sys.exit(exit_code)
except KeyboardInterrupt:
pass

程序启动,因为没有 RUN_MAIN 变量,所以走的 else 语句块。

颇为有趣的是,restart_with_reloader 函数中使用 subprocess.call 方法执行了启动程序的命令( e.g:python3 manage.py runserver ),此刻 RUN_MAIN 的值为 True ,接着执行 _thread.start_new_thread(main_func, args, kwargs) 开启新线程,意味着启动了 django 服务。

如果子进程不退出,则停留在 call 方法这里(进行请求处理),如果子进程退出,退出码不是3,while 则被终结。反之就继续循环,重新创建子进程。

检测文件修改

具体检测文件发生改变的函数实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
python复制代码
# django/utils/autoreload.py

def code_changed():
global _mtimes, _win
# 获取所有文件
for filename in gen_filenames():
# 通过 os 模块查看每个文件的状态
stat = os.stat(filename)
# 获取最后修改时间
mtime = stat.st_mtime
if _win:
mtime -= stat.st_ctime
if filename not in _mtimes:
_mtimes[filename] = mtime
continue
# 比较是否修改
if mtime != _mtimes[filename]:
_mtimes = {}
try:
del _error_files[_error_files.index(filename)]
except ValueError:
pass
return I18N_MODIFIED if filename.endswith('.mo') else FILE_MODIFIED
return False

总结

以上就是 django 检测文件修改而达到重启服务的实现流程。

结合 subprocess.call 和 环境变量 创建俩个进程。主进程负责监控子进程和重启子进程。
子进程下通过开启一个新线程(也就是 django 服务)。主线程监控文件变化,如果变化则通过 sys.exit(3) 来退出子进程,父进程获取到退出码不是3则继续循环创建子进程,反之则退出整个程序。

好,到这里。我们勇敢的迈出了第一步,我们继续下一个环节!!! ヾ(◍°∇°◍)ノ゙

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

StackOverflow上87万访问量的问题:什么是“找不

发表于 2020-02-13

你好呀,我是沉默王二,一个和黄家驹一样身高,和刘德华一样颜值的程序员。为了输出更好的内容,我就必须先输入更多的内容,于是我选择 Stack Overflow 作为学习的第一战线,毕竟很多大牛都在强烈推荐。本篇文章,我们来探讨一下访问量足足有 87+ 万次的问题——什么是“找不到符号”,它是什么意思,它是如何发生的,以及如何修复它。

额外多 BB 几句。坚持写作这些年来,真的成长特别快,我建议你也行动起来——我坚信,学习不在入,而在出!

上图是之前的一个领导给我发的微信,看来他也看到了我的成长。(一不小心,暴露了自己的真名)

如果你还有啥想看的、想了解的,欢迎在评论区留言!我会的、我能写的,我都非常乐意分享出来,和你共同成长!接下来,我们来看正文。

01、“找不到符号”错误是什么意思

先来看一段代码:

1
java复制代码String s = String();

有点经验的 Java 程序员应该能够发现上面这段代码中的错误,它缺少了一个 new 关键字。因此,这段代码在编译阶段是不会通过的。

当我们对编译错误置之不理,尝试运行它的时候,程序会抛出以下错误。

“找不到符号”,意味着要么源代码有着明显的错误,要么编译方式有问题。总之呢,是我们程序员搞的鬼,把编译器搞懵逼了,它有点力不从心,很无辜。

02、“找不到符号”是如何发生的

1)拼写错误

程序员毕竟也是人,是人就会犯错。

  • 单词拼错了,比如说把 StringBuilder 拼写成了 StringBiulder。
1
java复制代码StringBuilder sb = new StringBiulder(); // 找不到符号,类 StringBiulder
  • 大小写错了,比如说把 StringBuilder 拼写成了 Stringbuilder。
1
java复制代码StringBuilder sb = new Stringbuilder(); // 找不到符号,类 Stringbuilder

2)未声明变量

有时候,我们会在没有声明变量的情况下使用一个变量。

1
java复制代码System.out.println(sss); // 找不到符号,变量 sss

或者变量超出了作用域。

1
2
3
4
java复制代码for (int i = 0; i < 100; i++);  
{
    System.out.println("i is " + i);
}

上面这段代码很不容易发现错误,因为仅仅是在“{”前面多了一个“;”。“;”使得 for 循环的主体被切割成了两个部分,“{}”中的 i 超出了“()”中定义的 i 范围。

3)方法用错了,或者不存在

比如说,Java 如何获取数组和字符串的长度?length 还是 length()?

1
2
3
4
5
java复制代码String[] strs = {"沉默王二"};  
System.out.println(strs.length()); // 找不到符号,方法 length()

String str = "沉默王二";
System.out.println(str.length); // 找不到符号,变量 length

4)忘记导入类了

在使用第三方类库的时候,切记要先导入类。

1
java复制代码StringUtils.upperCase("abcd");// 找不到符号,类 StringUtils

不过,IDEA 中可以设置类自动导入,来避免这个错误。

。。。。。。

导致出现“找不到符号”的错误原因千奇百怪,上面也只是列举出了其中的一小部分。问题的根源在于程序员本身,随着编程经验的积累,以及集成开发工具的帮助,这些错误很容易在代码编写阶段被发现。

03、如何修复“找不到符号”错误

一般来说,修复“找不到符号”的错误很简单,要么根据 IDE 的提示在编写代码的时候直接修复;要么根据运行后输出的堆栈日志顺藤摸瓜。

日志会给出具体的行号,以及错误的类型。根据提示,想一下自己的代码要表达什么意思,然后做出修复的具体动作。比如上图中提醒我们 35 行代码出错了,找不到变量 j,那么就意味着我们需要给变量 j 一个类型声明即可。

04、更复杂的原因

在实际的项目当中,出现“找不到符号”的错误原因往往很复杂,但大多数情况下,可以归结为以下几点:

  • 编码格式不对。比如说应该是 UTF-8,但有些遗留的项目会设置为 GBK、GB2312 等等。
  • JDK 的版本不匹配。比如说某些团队成员的电脑上安装的是 JDK 1.6,有的是 JDK 8,版本升级后的一些新语法自然就会和老版本发生冲突。
  • 第三方类库的升级。一些开源的共同类库往往会不兼容旧的版本,比如说最新版的 StringUtils 类的包为 org.apache.commons.lang3,但之前是 org.apache.commons.lang。
  • 类名和方法名都相同,但包名不同,方法的参数不同,在使用的时候就容易造成“找不到符号”。

在我初学 Java 的时候,老师要求我们用记事本来编写代码,然后在命令行中编译和运行代码,那时候真的叫一个痛苦啊。

经常出现“找不到符号”的错误,差点入门到放弃。因为初学阶段,哪能记住那么多编程语言的规则啊,经常忘东忘西,再者记事本是没有行号的,找起问题来,简直要了老命。

吃过这样的苦后,我就强烈建议初学者不要再使用记事本编程了(莫装逼),直接上 IDE,有啥问题,工具帮你悠着点。

05、鸣谢

好了,我亲爱的读者朋友,以上就是本文的全部内容了。毫无疑问,能看到这里你在我心目中就是最棒的求知者,我必须要伸出大拇指为你点个赞👍。如果还想看到更多,我再推荐你 2 篇,希望你能够喜欢。

如何快速打好Java基础?
如何优雅地打印一个Java对象?

最后,我有一个小小的请求,原创不易,如果觉得有点用的话,请不要吝啬你手中点赞的权力——因为这将是我写作的最强动力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记一次阿里面试,我居然挂在了最熟悉的 LRU 缓存算法设计上

发表于 2020-02-12

最近春招提前批也要打响了,我会在推送算法+计算机基础的文章下,尽快推一些面试相关的文章

大概是去年的三月份,在找春招实习的时候,面了一次阿里,然后第一面就是写算法题,然而万万没有想到的是,我居然挂在了 LRU 缓存算法上了,这可是我再熟悉不过的算法。。。。。。。今天就来分享一波,说不定,你也做不出!

一、勤于动脑,懒于行动的祸

当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存算法,不会这么简单啊,于是理解错了题意(我也是服了,还能理解成这样,,,,),自己一波操作写了好多代码,后来卡住了,再去仔细看题,发现自己应该是理解错了,就是这么简单,设计一个 LRU 缓存算法。

不过这时时间就很紧了,按道理如果你真的对这个算法很熟,十分钟就能写出来了,但是,自己虽然理解 LRU 缓存算法的思想,也知道具体步骤,但之前却从来没有去动手写过,导致在写的时候,非常不熟练,也就是说,你感觉自己会 和你能够用代码完美着写出来是完全不是一回事。

所以在此提醒各位,如果可以,一定要自己用代码实现一遍自己自以为会的东西。千万不要觉得自己理解了思想,就不用去写代码了,独自撸一遍代码,才是真的理解了,而且面试的时候,需要你再记事本里打代码,不给你编译器的。。。。。。。

不过今天我带大家用代码来实现一遍 LRU 缓存算法,并且提供最优解,以后你在遇到这类型的题,保证你完美秒杀它。

题目描述

设计并实现最近最少经用(LRU)缓存的数据结构。它应该支持以下操作:get 和 put。

get(key) - 如果键存在于缓存中,则获取键的值(总是正数),否则返回 -1。

put(key, value) - 如果键不存在,请设置或插入值。当缓存达到其容量时,它应该在插入新项目之前,
使最近最少使用的项目无效。

进阶:

你是否可以在 O(1) 时间复杂度内执行两项操作?

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码LFUCache cache = new LFUCache( 2 /* capacity (缓存容量) */ );



cache.put(1, 1);

cache.put(2, 2);

cache.get(1); // 返回 1

cache.put(3, 3); // 去除 key 2

cache.get(2); // 返回 -1 (未找到key 2)

cache.get(3); // 返回 3

cache.put(4, 4); // 去除 key 1

cache.get(1); // 返回 -1 (未找到 key 1)

cache.get(3); // 返回 3

cache.get(4); // 返回 4

基础版:单链表来解决

我们要删的是最近最少使用的节点,一种比较容易想到的方法就是使用单链表这种数据结构来存储了。当我们进行 put 操作的时候,会出现以下几种情况:

1、如果要 put(key,value) 已经存在于链表之中了(根据key来判断),那么我们需要把链表中久的数据删除,然后把新的数据插入到链表的头部。、

2、如果要 put(key,value) 的数据没有存在于链表之后,我们我们需要判断下缓存区是否已满,如果满的话,则把链表尾部的节点删除,之后把新的数据插入到链表头部。如果没有满的话,直接把数据插入链表头部即可。

对于 get 操作,则会出现以下情况

1、如果要 get(key) 的数据存在于链表中,则把 value 返回,并且把该节点删除,删除之后把它插入到链表的头部。

2、如果要 get(key) 的数据不存在于链表之后,则直接返回 -1 即可。

大概的思路就是这样,不要觉得很简单,让你手写的话,十分钟你不一定手写的出来。具体的代码,为了不影响阅读,我在文章的最后面在放出来。

时间、空间复杂度分析

对于这种方法,put 和 get 都需要遍历链表查找数据是否存在,所以时间复杂度为 O(n)。空间复杂度为 O(1)。

空间换时间

在实际的应用中,当我们要去读取一个数据的时候,会先判断该数据是否存在于缓存器中,如果存在,则返回,如果不存在,则去别的地方查找该数据(例如磁盘),找到后在把该数据存放于缓存器中,在返回。

所以在实际的应用中,put 操作一般伴随着 get 操作,也就是说,get 操作的次数是比较多的,而且命中率也是相对比较高的,进而 put 操作的次数是比较少的,我们我们是可以考虑采用空间换时间的方式来加快我们的 get 的操作的。

例如我们可以用一个额外哈希表(例如HashMap)来存放 key-value,这样的话,我们的 get 操作就可以在 O(1) 的时间内寻找到目标节点,并且把 value 返回了。

然而,大家想一下,用了哈希表之后,get 操作真的能够在 O(1) 时间内完成吗?

用了哈希表之后,虽然我们能够在 O(1) 时间内找到目标元素,可以,我们还需要删除该元素,并且把该元素插入到链表头部啊,删除一个元素,我们是需要定位到这个元素的前驱的,然后定位到这个元素的前驱,是需要 O(n) 时间复杂度的。

最后的结果是,用了哈希表时候,最坏时间复杂度还是 O(1),而空间复杂度也变为了 O(n)。

双向链表+哈希表

我们都已经能够在 O(1) 时间复杂度找到要删除的节点了,之所以还得花 O(n) 时间复杂度才能删除,主要是时间是花在了节点前驱的查找上,为了解决这个问题,其实,我们可以把单链表换成双链表,这样的话,我们就可以很好着解决这个问题了,而且,换成双链表之后,你会发现,它要比单链表的操作简单多了。

所以我们最后的方案是:双链表 + 哈希表,采用这两种数据结构的组合,我们的 get 操作就可以在 O(1) 时间复杂度内完成了。由于 put 操作我们要删除的节点一般是尾部节点,所以我们可以用一个变量 tai 时刻记录尾部节点的位置,这样的话,我们的 put 操作也可以在 O(1) 时间内完成了。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// 链表节点的定义
class LRUNode{
String key;
Object value;
LRUNode next;
LRUNode pre;

public LRUNode(String key, Object value) {
this.key = key;
this.value = value;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
复制代码// LRU
public class LRUCache {
Map<String, LRUNode> map = new HashMap<>();
LRUNode head;
LRUNode tail;
// 缓存最大容量,我们假设最大容量大于 1,
// 当然,小于等于1的话需要多加一些判断另行处理
int capacity;

public LRUCache(int capacity) {
this.capacity = capacity;
}

public void put(String key, Object value) {
if (head == null) {
head = new LRUNode(key, value);
tail = head;
map.put(key, head);
}
LRUNode node = map.get(key);
if (node != null) {
// 更新值
node.value = value;
// 把他从链表删除并且插入到头结点
removeAndInsert(node);
} else {
LRUNode tmp = new LRUNode(key, value);
// 如果会溢出
if (map.size() >= capacity) {
// 先把它从哈希表中删除
map.remove(tail.key);
// 删除尾部节点
tail = tail.pre;
tail.next = null;
}
map.put(key, tmp);
// 插入
tmp.next = head;
head.pre = tmp;
head = tmp;
}
}

public Object get(String key) {
LRUNode node = map.get(key);
if (node != null) {
// 把这个节点删除并插入到头结点
removeAndInsert(node);
return node.value;
}
return null;
}
private void removeAndInsert(LRUNode node) {
// 特殊情况先判断,例如该节点是头结点或是尾部节点
if (node == head) {
return;
} else if (node == tail) {
tail = node.pre;
tail.next = null;
} else {
node.pre.next = node.next;
node.next.pre = node.pre;
}
// 插入到头结点
node.next = head;
node.pre = null;
head.pre = node;
head = node;
}
}

这里需要提醒的是,对于链表这种数据结构,头结点和尾节点是两个比较特殊的点,如果要删除的节点是头结点或者尾节点,我们一般要先对他们进行处理。

这里放一下单链表版本的吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
复制代码// 定义链表节点
class LRUNode{
String key;
Object value;
LRUNode next;

public LRUNode(String key, Object value) {
this.key = key;
this.value = value;
}
}
// 刚开始把名字写错了,把 LRU写成了RLU
public class LRUCache {
LRUNode head;
int size = 0;// 当前大小
int capacity = 0; // 最大容量

public LRUCache(int capacity) {
this.capacity = capacity;
}

public Object get(String key) {
LRUNode cur = head;
LRUNode pre = head;// 指向要删除节点的前驱
// 找到对应的节点,并把对应的节点放在链表头部
// 先考虑特殊情况
if(head == null)
return null;
if(cur.key.equals(key))
return cur.value;
// 进行查找
cur = cur.next;
while (cur != null) {
if (cur.key.equals(key)) {
break;
}
pre = cur;
cur = cur.next;
}
// 代表没找到了节点
if (cur == null)
return null;

// 进行删除
pre.next = cur.next;
// 删除之后插入头结点
cur.next = head;
head = cur;
return cur.value;
}

public void put(String key, Object value) {
// 如果最大容量是 1,那就没办法了,,,,,
if (capacity == 1) {
head = new RLUNode(key, value);
}
LRUNode cur = head;
LRUNode pre = head;
// 先查看链表是否为空
if (head == null) {
head = new RLUNode(key, value);
return;
}
// 先查看该节点是否存在
// 第一个节点比较特殊,先进行判断
if (head.key.equals(key)) {
head.value = value;
return;
}
cur = cur.next;
while (cur != null) {
if (cur.key.equals(key)) {
break;
}
pre = cur;
cur = cur.next;
}
// 代表要插入的节点的 key 已存在,则进行 value 的更新
// 以及把它放到第一个节点去
if (cur != null) {
cur.value = value;
pre.next = cur.next;
cur.next = head;
head = cur;
} else {
// 先创建一个节点
LRUNode tmp = new LRUNode(key, value);
// 该节点不存在,需要判断插入后会不会溢出
if (size >= capacity) {
// 直接把最后一个节点移除
cur = head;
while (cur.next != null && cur.next.next != null) {
cur = cur.next;
}
cur.next = null;
tmp.next = head;
head = tmp;
}
}
}
}

如果要时间,强烈建议自己手动实现一波。

兄dei,如果觉得我写的不错,不妨帮个忙

1、关注我的原创微信公众号「帅地玩编程」,每天准时推送干货技术文章,专注于写算法 + 计算机基础知识(计算机网络+ 操作系统+数据库+Linux),听说关注了的不优秀也会变得优秀哦。

2、给俺点个赞呗,可以让更多的人看到这篇文章,顺便激励下我,嘻嘻。

作者简洁

作者:大家好,我是帅地,从大学、自学一路走来,深知算法,计算机基础知识的重要性,所以申请了一个微星公众号『帅地玩编程』,专业于写这些底层知识,提升我们的内功,帅地期待你的关注,和我一起学习。 转载说明:未获得授权,禁止转载

最后,给大家推荐一个 Github,里面收集了挺多优质编程书籍:[几百本CS类的优质书籍整理](

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0xA01 ASOP应用框架:Apk是如何生成的

发表于 2020-02-12

前言

  • 这是 Android 10 源码分析系列的第 1 篇
  • 分支:android-10.0.0_r14
  • 全文阅读大概 10 分钟

在 Android Studio 中直接点击 Run ‘app’ 就可以在 build/outputs/apk 生成可以在 android 设备中安装的 APK 文件,那么 APK 生成的过程是怎么样的呢?

APK 文件大概可以分为两个部分:代码和资源,所以打包的也分为代码和资源两个部分,我们可以根据 Google提供的流程图 来具体了解一个 APK 的构建过程

新版构建流程图

15813472877044-w350

APK 打包的内容主要有:应用模块也就是自己开发的用到的源代码、资源文件、aidl 接口文件,还有就是依赖模块即源代码用到的第三方依赖库如:aar、jar、so 文件

为了能够清楚的了解 APK 是如何生成的, 来看一下老版构建流程图

老版构建流程图
2019-03-22-15532697195669-w350

在了解 APK 生成的过程之前,我们需要了解一下图中各个工具的作用

工具

名字 功能
AAPT/APT2 Android 资源打包工具
AIDL 将所有的 AIDL 接口转化为 Java 接口
Javac(Java Compiler) 将所有的 Java 代码编译成 Class文件
Dex 将 Class 文件编译成 Dex 文件
Apkbuilder 将处理后的资源和代码打包生成 APK 文件
Jarsigner/Apksigner 对未签名的 APK 文件进行签名
Zipalign 优化签名后的 APK,减少运行时所占用的内存

构建过程

1. 使用 AAPT 工具生成 R.java 文件

AAPT(Android Asset Packaging Tool)android 资源打包工具,将资源文件(包括AndroidManifest.xml、布局文件、各种 xml 资源等)打包生成 R.java 文件,将 AndroidManifest.xml 生成二进制的 AndroidManifest.java 文件

1
2
3
4
5
6
7
8
9
css复制代码aapt p -M AndroidManifest.xml -S output/res/ -I android.jar -J ./ -F input/out.apk

p:打包
-M:AndroidManifest.xml 文件路径
-S:res 目录路径
-A:assets 目录路径
-I:android.jar 路径,会用到的一些系统库
-J 指定生成的 R.java 的输出目录
-F 具体指定 APK 文件的输出

但是从 Android Studio 3.0 开始,google 默认开启了 AAPT2 作为资源编译的编译器,AAPT2 的出现为资源的增量编译提供了支持,aapt2 主要分两步,compile 和 link

compile

1
2
3
4
bash复制代码aapt2  compile -o res.apk --dir output/res/

-o:指定已编译资源的输出路径
--dir:指定包含多个资源文件的资源目录

link

1
2
3
4
5
6
css复制代码aapt2 link -o input/out.apk -I tools/android.jar --manifest output/AndroidManifest.xml -A  res.apk --java ./

-o:指定链接的资源 APK 的输出路径
-I:指定 android.jar 路径
--manifest:指定 AndroidManifest.xml 路径
--java :指定要在其中生成 R.java 的目录

2. 所有的 AIDL 接口转化为 Java 接口

使用 AIDL(Android Interface Denifition Language),位于 sdk\build-tools 目录下的 aidl 工具,将源码文件、aidl 文件、framework.aidl 等所有的 AIDL 文件,生成相应的 Java 文件,命令如下:

1
2
3
4
5
bash复制代码aidl -Iaidl -pAndroid/Sdk/platforms/android-29/framework.aidl -obuild aidl/com/android/vending/billing/IInAppBillingService.aidl

-I 指定 import 语句的搜索路径,注意 -I 与目录之间一定不要有空格
-p 指定系统类的 import 语句路径,如果是要用到 android.os.Bundle 系统的类,一定要设置 sdk 的 framework.aidl 路径
-o 生成 java 文件的目录,注意 -o 与目录之间一定不要有空格,而且这设置项一定要在 aidl 文件路径之前设置

3. 将 Java 代码编译成 Class 文件

使用 Javac(Java Compiler)把项目中所有的 Java 代码编译成 class 文件, 包括 Java 源文件、AAPT 生成的 R.java 文件 以及 aidl 生成的 Java 接口文件,命令如下:

1
bash复制代码javac -target 1.8 -bootclasspath platforms/android-28/android.jar -d ./java/com/testjni/*.java

4. 将 Class 文件编译成 Dex 文件

使用 DX 工具将所有的 Class 文件(包括第三方库中的 class 文件)转换成 Dex 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),该过程主要完成 Java 字节码转换成 Dalvik 字节码, 命令如下:

1
2
3
4
lua复制代码java -jar dx.jar --dex --ouput=classes.dex ./java/com/testjni/*.class

--dex:将 class 文件转成dex文件
--output:指定生成 dex 文件到具体位置

5. 打包生成 APK 文件

使用 Apkbuilder(主要用到的是 sdk/tools/lib/sdklib.jar 文件中的 ApkBuilderMain 类)将所有的 Dex 文件、Resource.arsc、Res 文件夹、Assets 文件夹、AndroidManifest.xml 打包生成 APK 文件(未签名)

6. 对 APK 文件签名

使用 Apksigner(Android官方针对 APK 签名及验证工具)或 Jarsigner(JDK提供针对 jar 包签名工具)对未签名的 APK 文件进行签名

ps:如果使用 Apksigner 签名需要(7. 优化 APK 文件)放到(6. 对 APK 文件签名)签名前面,为什么?请查看关于 Apksigner 和 Jarsigner 的区别,请移步到文末

7. 优化 APK 文件

使用 zipalign 对签名后的 APK 文件进行对齐处理,对齐的主要过程是将 APK 包中所有的资源文件距离文件起始偏移为 4 字节整数倍,这样通过内存映射访问 APK 文件时的速度会更快,减少其在设备上运行时所占用的内存

总结

上述打包过程都是 AndroidStudio 编译时,调用各种编译命令自动完成的, 总结一下上述打包过程:

  1. 除了 assets 和 res/raw 资源被原装不动地打包进 APK 之外,其它的资源都会被编译或者处理
  2. 除了 assets 资源之外,其它的资源都会被赋予一个资源 ID
  3. 打包工具负责编译和打包资源,编译完成之后,会生成一个 resources.arsc 文件和一个 R.java,前者保存的是一个资源索引表,后者定义了各个资源 ID 常量
  4. 应用程序配置文件 AndroidManifest.xml 同样会被编译成二进制的 xml 文件,然后再打包到 APK 里面去
  5. 应用程序在运行时通过 AssetManager 来访问资源,或通过资源 ID 来访问,或通过文件名来访问

APK 文件大概可以分为两个部分:代码和资源, 代码部分通过 Javac 将 Java 代码编译成 Class 文件, 然后通过 DX 工具将 Class 文件编译成 Dex 文件,接下来我们主要来分析一下资源的编译和打包

资源的编译和打包

在分析资源的编译和打包之前,我们需要了解一下 Android 都有哪些资源,其实 Android 资源大概分为两个部分:assets 和 res

1. assets 资源

assets 资源放在 assets 目录下,它里面保存一些原始的文件,可以以任何方式来进行组织,这些文件最终会原封不动的被打包进 APK 文件中,通过 AssetManager 来获取 asset 资源,代码如下

1
2
ini复制代码AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("fileName");

2. res 资源

res 资源放在主工程的 res 目录下,这类资源一般都会在编译阶段生成一个资源ID供我们使用,res 目录包括 animator、anim、 color、drawable、layout、menu、raw、values、xml 等

上述资源文件除了 raw 类型资源,以及 drawable 文件夹下的 Bitmap 资源之外,其它的资源文件均会被编译成二进制格式的 XML 文件,生成的二进制格式的 XML 文件分别有一个字符串资源池,用来保存文件中引用到的每一个字符串

这样原来在文本格式的 XML 文件中的每一个放置字符串的地方在二进制格式的XML文件中都被替换成一个索引到字符串资源池的整数值,将整数值保存在 R.java 类中,R.java 会和其他源文件一起编译到 APK 中去

将资源编译成二进制文件,都是由 AAPT 工具来完成的,资源打包主要有以下几个流程:

  1. 解析 AndroidManifest.xml,获得应用程序的包名称,创建资源表
  2. 添加被引用资源包,被添加的资源会以一种资源 ID 的方式定义在 R.java 中
  3. 资源打包工具创建一个 AaptAssets 对象,收集当前需要编译的资源文件,收集到的资源保存在 AaptAssets 对象对象中
  4. 将上一步 AaptAssets 对象保存的资源,添加到资源表 ResourceTable 中去,用于最终生成资源描述文件 resources.arsc
  5. 编译 values 类资源,这类资源包括数组、颜色、尺寸、字符串等值
  6. 给 style、array 这类资源分配资源 ID
  7. 编译 XML 资源文件,编译的流程分为:① 解析 XML 文件 ② 赋予属性名称资源 ID ③ 解析属性值 ④ 将 XML 文件从文本格式转换为二进制格式
  8. 生成资源索引表 resources.arsc
2.1 资源 ID

AAP 工具会所有的资源都会生成一个 R.java 文件,并且每个资源都对应 R.java 中的十六进制整数变量,其实这些十六进制的整数是由三部分组成:PackageId + TypeId + ItemValue,代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码public final class R {
public static final class anim {
public static final int abc_fade_in=0x7f010000;
public static final int abc_fade_in=0x7f010001;
//***
}
public static final class string {
public static final int a11y_no_data=0x7f100000;
public static final int a11y_no_permission=0x7f100001;
//***
}
}

最高字节是 Package ID 表示命名空间,标明资源的来源,Android 系统自己定义了两个 Package ID,系统资源命名空间:0x01 和 应用资源命名空间:0x7f

正因为应用资源命名空间:0x7f,我们在做插件化的时候就会出现一个问题,宿主和插件包,合并资源后资源 ID 冲突。通过上面分析要解决这个问题,就要为不同的插件设置不同的 PackageId,而宿主可以保留原来 0x7f 不变,这样就永远不会有冲突发生了

如何解决资源冲突

  1. 制定一个不用冲突的命名规范
  2. library Module 的 build.gradle 中设置资源前缀(推荐)
1
2
3
arduino复制代码android {
resourcePrefix "<前缀>"
}
2.2 资源索引(resources.arsc)

最终生成的是资源索引表 resources.arsc ,resources.arsc 是一个编译后的二进制文件, 在 AndroidStudio 打开 resources.arsc 文件,如下所示

Android 正是利用这个索引表根据资源 ID 进行资源的查找,为不同语言、不同地区、不同设备提供相对应的最佳资源。查找和通过 Resources 和 AssetManger 来完成的

在文中提到了两个工具 Apksigner 和 Jarsigner,下面一起来了解一下 Apksigner 和 Jarsigner 的区别

Apksigner 和 Jarsigner 的区别

在 Android Studio 中点击菜单 Build->Generate signed apk… 打包签名过程中,可以看到两种签名选项 V1(Jar Signature) 和 V2(Full APK Signature)

  • Jarsigner 是 JDK 提供的针对 JAR 包签名的通用工具
  • Apksigner 是 Google 官方提供的针对 Android APK 签名及验证的专用工具

从Android 7.0 开始, 谷歌增加新签名方案 V2 Scheme (APK Signature),但Android 7.0 以下版本, 只能用旧签名方案 V1 scheme (JAR signing)

V1(Jar Signature)签名:

来自 JDK(Jarsigner),对 ZIP 压缩包的每个文件进行验证, 签名后还能对压缩包修改(移动/重新压缩文件),对 V1 签名的 APK/JAR 解压,在 META-INF 存放签名文件(MANIFEST.MF, CERT.SF, CERT.RSA), 其中 MANIFEST.MF 文件保存所有文件的 SHA1 指纹(除了 META-INF 文件), 由此可知: V1 签名是对压缩包中单个文件签名验证

V2(Full APK Signature)签名:

来自 Google(apksigner), 对 ZIP 压缩包的整个文件验证, 签名后不能修改压缩包(包括 zipalign), 对 V2 签名的 APK 解压, 没有发现签名文件, 重新压缩后 V2 签名就失效, 由此可知: V2 签名是对整个 APK 签名验证

创建发布密钥库,请参阅在 Android Studio 中为应用签名

总结

  • V1 签名是对压缩包中单个文件签名验证
  • V2 签名是对整个 APK 签名验证
  • zipalign 可以在 V1 签名后执行
  • zipalign 不能在 V2 签名后执行,只能在 V2 签名之前执行
  • V2 签名更安全(不能修改压缩包)
  • V2 签名验证时间更短(不需要解压验证), 因而安装速度加快

注意: apksigner 工具默认同时使用 V1 和 V2 签名,以兼容 Android 7.0 以下版本

参考文献

  • Google的 Apk 构建流程
  • Android Studio 中为应用签名
  • AAPT2

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,文章中有什么没有写明白的地方,或者有什么更好的建议欢迎留言,欢迎一起来学习,在技术的道路上一起前进。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 如何高效获取视频截图
  • 10分钟入门 Shell 脚本编程
  • 如何在项目中封装 Kotlin + Android Databinding

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

系统权限设计 - 基本概念和思路

发表于 2020-02-12

权限系统的设计几乎是每个系统都必需的模块,最近对系统的权限设计有一些心得体会。遇到过一些坑,也有一些思考,所以想写下来分享给大家。

本文的目的是帮助大家理清楚权限设计中的一些基本概念,提供常用的权限系统设计思路。

首先,我们抛出一个案例,读者可以想一想如果自己来设计权限系统,如何满足这个需求?

需求描述:

在一个大学里,有许多人。学生即将进行期末考试。对于某个期末考试的试卷,有以下权限管理需求:

  • 在整个过程,该课程的老师全程可以查看试卷
  • 在考试中,该课程的监考老师可以查看试卷
  • 在考试中,该课程的学生可以查看和写入自己的试卷
  • 在考试后,该课程的老师可以写入试卷
  • 在考试后,该年级的教务主任可以查看这个年级所有课程的试卷
  • 在整个过程,打扫清洁的阿姨不能查看/写入试卷
  • 在整个过程,校长可以查看任何试卷

备注:考试过程分为考试前、考试中、考试后。一个人可能有多个角色。

UML图:

课程考试权限问题UML.jpg

认证与授权

系统的权限控制可以分为两部分:认证和授权。两者是相对独立的概念,把它们拆开有助于我们更好地理解权限系统的设计。

认证 Authentication

所谓认证,就是判断你是不是你本人。通俗地说,就是我们系统中常见的“登录”这一步。这个验证的手段和技术有很多种,比如:表单验证、HTTP Basic验证、基于cookie的remember-me、OAuth2、甚至是指纹验证等等都是可以的。

关于各种认证方式的技术细节本文就不细讲了,这部分不在本文重点。读者只需要知道的是,权限控制的第一阶段就是认证,如果认证通过了,就说明用户登录进了系统。至于后续的各种业务的权限控制,就不归认证管了。

授权 Authorization

认证不管业务的权限控制,那谁来管呢?那就是授权了。也就是本文要讨论的重点。

首先我们需要思考一个问题:我们系统的权限设计应该有一个什么大的目标?我认为是兼顾“灵活”和“易管理”。也就是说,我们的权限设计应该能够灵活应对变化,包括用户的变化、业务权限要求的变化。但同时,它又应该比较容易管理,这样才能够让我们的权限足够清晰,不至于混乱。

所以基于上述目标,我认为权限系统的设计应该有以下三个原则:

够用就行

这里强调的是权限系统应该在满足需求的前提下尽量简单,不要过度设计。比如对于我的个人博客网站来说,只需要有我自己一个用户,那就没必要做很复杂的权限控制。

粒度适中

这里的“粒度”指的是权限的粒度。一个权限如果粒度过粗,就会导致多个业务场景使用一个权限,灵活性很差;而权限如果粒度过细,就会让权限变多,难以管理。

推荐以业务操作为权限的粒度。一个业务操作对应一个权限。业界也有DDD(领域驱动设计)等工具来帮助更好地划分业务。

比如我们上述案例中,虽然老师和学生都有“写入试卷”的操作,但其实通过理解业务,我们可以把它分为“答题”和“打分”。

那API与业务操作是一一对应的吗?多数业务场景应该是的,但也有例外。比如发微信朋友圈,就需要at人,上传图片、已经真正保存自己要发送的内容这三个操作组成“发朋友圈”这个业务,也就对应了三个业务。

这里根据笔者的实践来看,更推荐使用一个权限,可以访问这三个API。或者三个权限,分别对应相应的API,但有“权限组”的概念,把它们三个加进一个权限组,实际分配权限的时候就分配这个权限组就行了。不推荐仅仅只是三个权限分别控制三个API。

尽量使用白名单

这个是众所周知的安全原则之一。我们在实现权限系统的时候,应该尽量使用白名单,而不是黑名单。比如:有xx权限的人才能进行xx业务。而不是:没有xx权限的人就能进行xx业务。

主流的权限设计模型

权限设计是一个不容易的事,但基本上所有的系统都需要权限设计。实际上,业界已经有一些主流的权限设计模型了。这里只简单介绍一些常用的。

ACL

访问控制列表,多用于简单的权限控制。尤其在网络流量控制那块用得很多,很少用在业务系统上。

RBAC

基于角色的访问控制(Role-Based Access Control),权限与角色相关联,用户通过分配适当角色而得到这些角色的权限。也是业界最主流的权限设计模型。RBAC不能很好地检查与“数据状态”相关的权限,比如“试卷在考试前,只有老师能查看”这种需求。需要自己把角色写死,然后在代码里写逻辑检查。

ABAC

基于属性的访问控制(Attribute-Based Access Control),为了解决上述问题,业界有一种叫ABAC的解决方案。通过写动态的DSL来判断数据的状态,通过一些自定义的语法来做权限控制。

ABAC有时也被称为PBAC(Policy-Based Access Control)或CBAC(Claims-Based Access Control)。ABAC通常用于平台级的系统。比如AWS、阿里云等“云提供商”,他们有海量的资源、角色,需要很灵活的权限管理系统。

ABAC的实现成本很高。管理上也比较复杂。

示例(阿里云的RAM):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码{
"Version": "1",
"Statement":
[{
"Effect": "Allow",
"Action": ["oss:List*", "oss:Get*"],
"Resource": ["acs:oss:*:*:samplebucket", "acs:oss:*:*:samplebucket/*"],
"Condition":
{
"IpAddress":
{
"acs:SourceIp": "42.160.1.0"
}
}
}]
}

权限设计演进

本着上面提到的够用就行的原则,权限系统不应该过度设计。我们可以根据权限需求从简单到复杂,大概分为三类:

  • 只需要一个管理员,管理所有内容,比如个人博客网站。
  • 只有一些用户和角色,且角色所拥有的权限相对稳定。适用于绝大多数系统。
  • 有很多的用户和角色,且角色的权限很容易变化。平台级系统,如AWS。

因为第二种适用于绝大多数日常开发的项目,所以后面主要讨论的是第二种。如果是第三种的话,推荐基于ABAC开发出一套定制化的权限系统。

区分Access与Validation

先来澄清一些这两个名词的概念:

  • Access:我能不能call这个API,与数据无关,可以在网关这一层就拦截。
  • Validation:我能Call这个API,但到底有没有相应的权限,与业务数据有关,可以写Validator来验证,推荐放在最下层service。

很多第一次做权限设计的朋友,容易把他两搞混,设计出来的权限就可能看起来很混乱。

我们再来分析一下案例,就拿“读取试卷”这个业务操作来说,我们可以有一个Access叫做READ_PAPER,把它赋予给学生、老师、监考老师、教务主任、校长这几种角色。代表他们可以call这个业务API。而进到具体的验证逻辑的时候,就需要去查数据库取出试卷的状态、对应的班级、监考老师ID等等信息,通过Validation去验证这些数据,当前角色是否可以访问。

下篇文章会更具体地从前后端的视角、权限的粒度和代码层面来给出一套推荐的权限设计方案。

下篇预告:《系统权限设计-推荐方案》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

推荐一个项目管理工具,落地基于Scrum的敏捷开发!

发表于 2020-02-11

SpringBoot实战电商项目mall(25k+star)地址:github.com/macrozheng/…

摘要

作为一个开发人员,我们也该懂一些项目管理的知识,今天我们来讲一个基于Scrum的项目管理工具禅道。本文将从禅道的安装部署开始讲起,然后讲讲Scrum的核心概念,最后通过禅道的一套操作来实践下Scrum的开发流程。

禅道简介

禅道由青岛易软天创网络科技有限公司开发,国产开源项目管理软件。它集产品管理、项目管理、质量管理、文档管理、组织管理和事务管理于一体,是一款专业的研发项目管理软件,完整覆盖了研发项目管理的核心流程。禅道项目管理软件的主要管理思想基于国际流行的敏捷项目管理方法—Scrum。Scrum方法注重实效,操作性强,非常适合软件研发项目的快速迭代开发。禅道在遵循其管理方式基础上,结合国内研发现状,整合了Bug管理,测试用例管理,发布管理,文档管理等功能,完整的覆盖了软件研发项目的整个生命周期。

安装及部署

禅道的安装方式有很多,这里我们使用它在Docker环境下的安装方式。

  • 下载禅道的Docker镜像:
1
bash复制代码docker pull idoop/zentao:latest
  • 在Docker容器中运行禅道:
1
2
3
4
5
6
bash复制代码docker run -d -p 80:80 -p 4306:3306 --name zentao-server \
-e ADMINER_USER="admin" -e ADMINER_PASSWD="123456" \
-e BIND_ADDRESS="false" \
-v /mydata/zbox/:/opt/zbox/ \
--add-host smtp.exmail.qq.com:163.177.90.125 \
-d idoop/zentao:latest
  • 启动参数说明:
+ ADMINER\_USER:管理员账号;
+ ADMINER\_PASSWD:管理员密码;
+ BIND\_ADDRESS:若设置参数为"false",禅道数据库启动后允许远程访问,选填;
+ SMTP\_HOST:设置smtp服务IP和主机名,用于解决无法发送邮件的问题。
  • 安装成功后,访问该地址即可登录禅道系统,登录用户名和密码为admin:123456:http://192.168.6.132/

Scrum的核心概念

敏捷开发的产生

我们比较熟知的软件项目管理方法是瀑布,其基本流程是需求->设计->开发->测试。基本假设只要每个环节都做正确,那么终得到的结果也是正确的。但从总体来讲,瀑布项目失败率比较高。国外的软件先行者们针对瀑布开发中暴露出来的问题进行了一系列的探索、思考和总结,最终提出了敏捷开发的概念。敏捷开发有很多种方式,其中Scrum是比较流行的一种。

Scrum中的角色

Scrum是由产品经理(product owner)、项目经理(scrum master)和研发团队(dev team)组成的。

  • 其中产品经理负责整理用户故事(user story),定义其商业价值,对其进行排序,制定发布计划,对产品负责;
  • 项目经理负责召开各种会议,协调项目,为研发团队服务;
  • 研发团队则由不同技能的成员组成,通过紧密协同,完成每一次迭代的目标,交付产品。

这里我们讲下什么是用户故事:所谓用户故事,就是来描述一件事情,作为什么用户,希望如何,这样做的目的或者价值何在,这样有用户角色,有行为,也有目的和价值所在,非常方便与团队成员进行沟通。

Scrum中的迭代开发

与瀑布不同,Scrum将产品的开发分解为若干个小迭代(sprint),其周期从1周到4周不等,但不会超过4周。
参与的团队成员一般是5到9人,每期迭代要完成的用户故事是固定的,每次迭代会产生一定的交付。

Scrum的基本流程

Scrum的基本流程如上图所示:

  • 产品经理负责整理用户故事,形成左侧的产品订单(product backlog);
  • 发布计划会议:项目经理负责讲解用户故事,对其进行估算和排序,发布计划会议的产出就是制定出这一期迭代要完成的用户故事列表,即迭代订单(sprint backlog);
  • 迭代计划会议:项目团队对每一个用户故事进行任务分解,分解的标准是完成该用户故事的所有任务,最终每个任务都有明确的负责人,并完成工时的初估计;
  • 每日例会:每天项目经理召集站立会议,团队成员回答昨天做了什么,今天计划做什么,遇到了什么问题;
  • 演示会议:迭代结束之后,召开演示会议,相关人员都受邀参加,团队负责向大家展示本次迭代取得的成果。期间大家的反馈记录下来,由产品经理整理,形成新的用户故事;
  • 回顾会议:项目团队对本期迭代进行总结,发现不足,制定改进计划,下一次迭代继续改进,已达到持续改进的效果。

禅道使用

接下来我们将按角色来讲讲如何使用禅道来实现基于Scrum的项目管理。

管理员

禅道安装成功之后,管理员的第一件要做的事情就是设置部门结构,并添加用户账号。

  • 通过组织->用户->维护部门可以为企业添加部门结构:

  • 通过组织->用户->添加用户可以为企业添加用户:

  • 注意添加用户是需要添加职位和权限分组的:

  • 这里我们添加了产品经理、项目经理、研发主管、测试主管四个账号以便下面使用。

产品经理

产品经理对于公司来讲,至关重要。只有做出好的产品或者服务出来,才能赢得市场,谋求发展和生存。
下面我们用产品经理的账号登录,来演示下产品经理在敏捷开发中所要做的事情。

  • 通过产品左上角的下拉菜单可以添加产品:

  • 添加产品时需要完善相关信息:

  • 添加完产品后产品经理可以通过产品->需求->维护模块来创建产品的模块:

  • 在相应模块中通过产品->需求->提需求可以创建需求:

  • 之后可以完善需求的信息并进行创建:

  • 创建完需求后还需要对需求进行评审操作,只有评审通过的需求才会由项目经理进行任务分解,从而转为为开发任务指派给开发团队:

  • 评审时选择评审结果为确认通过后该需求就会被激活了:

  • 当然产品经理也可以对当前的需求进行变更操作,但是变更完的需求需要开发团队确认后才能进行后续开发;

  • 产品经理还可以创建计划,规定需求的完成时间:

  • 完善计划信息时,主要是要完善计划的开始和截止时间:

  • 可以通过关联需求,指定此次产品计划需要完成的需求:

项目经理

项目经理主要负责管理开发团队,将产品经理的需求讲解给开发团队听,确定项目要完成的需求列表,对需求进行任务分解并指派给开发团队,以及各种会议的组织。下面我们用项目经理的账号登录,来演示下项目经理在敏捷开发中所要做的事情。

  • 在禅道中项目其实对应的是敏捷开发里面的迭代的概念,项目经理首先需要创建一个项目:

  • 创建时需要完善项目信息,设定项目开发时间以及关联相关产品与计划:

  • 接下来项目经理要做的就是创建项目团队,可以通过项目->团队->团队管理来为项目团队添加成员:

  • 项目团队组建完毕之后,项目经理通过关联产品即可将项目和产品进行关联:

  • 然后通过关联需求即可确定当前项目要做的需求,可以选择关联需求或按计划关联需求:

  • 需求确定之后,项目中几个关键的因素都有了:周期确定、资源确定、需求确定。下面项目经理要做的事情就是为每一个需求做任务分解:

  • 任务分解时需要完善任务详情,明确任务的执行时间:

  • 这里把商品管理功能这个需求分解为了商品列表、添加商品和编辑商品三个任务并指派给了开发人员。

开发团队

项目的任务分解完毕之后,开发团队成员需要领取自己的任务,开始每天的开发。除了日常的编码工作之外,还应当每天花点时间在禅道里面更新下任务的状态以及消耗情况。下面我们用开发人员的账号登录,来演示下开发人员在敏捷开发中所要做的事情。

  • 首先开发人员需要找到自己需要完成的任务,从项目->任务中可以查看到指派给自己的任务:

  • 开发人员开始做任务时点击开始按钮,完成任务时点击完成按钮:

  • 任务开始时需要填写自己的预计剩余时间,最初预计工时可以在编辑任务里设置:

  • 任务完成时需要填写自己的本次消耗时间:

  • 物理介质的看板比较直观,是Scrum标准的管理工具,禅道里面也有一个电子看板:

  • 当项目的任务都完成以后,开发人员可以创建版本:

  • 完善完版本信息后即可创建版本:

  • 有了版本以后,才可以根据当前版本创建测试单:

  • 创建测试单需要完善版本、负责人、名称等信息:

测试团队

测试团队是项目质量的保证,测试团队主要负责对项目的版本进行测试,提出Bug指派给开发人员,开发人员解决Bug后对Bug进行验证并关闭。下面我们用测试人员的账号登录,来演示下测试人员在敏捷开发中所要做的事情。

  • 测试人员开始测试时,需要把测试单状态设置为进行中:

  • 测试人员可以在测试->Bug中提出测试过程中发现的Bug:

  • 需要完善BUG信息并指派给相应开发人员:

  • 当开发人员解决完Bug后可以把Bug标记为已经解决:

  • 此时该Bug会自动指派给测试人员,测试人员确认已经解决后可以关闭该问题,如未解决,可以激活该问题。

总结

我们通过在禅道里面的一系列操作完整地演示了一套基于Scrum敏捷开发流程,其实所有角色的职责可以用下图来概况。

参考资料

更多资料可以参考官方文档:www.zentao.net/book/zentao…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…833834835…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%