Android内部分享[1]——概述和起步

概述

如何开始并学好一个技术栈是一个比较普遍的问题,对所有技术栈都适用,我的观点是从宏观到微观,先要从整体上对这个技术栈的方向、特点、用途等有一个总体认识,然后再进入到技术细节,这样才不至于盲人摸象。

从整体上我们先来认识一下我们要学习的Android技术是要学习哪些方面:

  1. 平台环境(Android系统、Linux系统、浏览器、数据库)。
  2. 平台所支持的计算机语言(Java、C/C++、Kotlin)和基于平台的操作API(JDK、SDK、JNI)。
  3. 通信协议(TCP/IP [Socket], HTTP, MQTT, Modbus, 串口)。
  4. 软件工程的设计和重构思维(整体结构 [MVP, MVVP, MVC]、组件化、模块化、解耦性、健壮性、可迭代性),学习使用一些优秀框架(RxJava, Retrofit, Okttp, Fresco, Glide, … 很多)。

你会发现只有第 2 项是不同技术栈的特有分支,其他三项就是计算机编程中的软实力,任何平台都不能脱离这些基础知识。当然、学会这些并不等于你可以游刃有余的开发Android项目了,因为Android平台的特殊性(开源,系统固件版本分支太多 [例如:小米、华为、三星、oppo])就涉及到系统bug和兼容性问题(和你们的浏览器兼容性类似),这些就需要一些经验性的东西。另一方面Android系统版本的迭代速度快,碎片化严重,所以给开发者带来不少兼容性困难,这个情况已经概述,因为现在基本不需要兼容到4.4以下了。

产品代号Android版本API
Pie928
Oreo8.026 or 27
Nougat7.1 and 7.024 or 25
Marshmallow6.023
Lollipop5.1 and 5.021 or 22
KitKat4.4-4.4.419 or 20

截止2018 年 10 月 26 日各个版本的使用用户数量统计如下:

Android系统版本使用统计

关于各个系统版本介绍请参考我的另一篇博文:Android历史版本变迁

另外给大家解决一个误区, Android 操作系统并不仅仅是手机操作系统,还包括 Android Wear (穿戴设备)、嵌入式设备(iot)、Android TV、Android Auto(车载系统)等。

学习

在写这篇分享文章前我认真思考了一番,知识点的细节是非常多的,不可能在两个小时内涵盖或者深究某个地方,不妨从一个小案例入手先感知一下Android开发的流程,这样就可以形成一个主线并起到一个抛转引玉的作用,后面大家也可以围绕这个主线去深究某一个细节。如果案例中遇到一些重要并常用的知识点,会给与最准确精炼的解释,但也不排除会遗漏一些重要环节,请谅解。

Android系统概述

Android系统从下至上依次为Linux内核、HAL(硬件抽象层)、系统Native库和Android运行时环境、Java框架层以及应用层这5层架构,其中每一层都包含大量的子模块或子系统。

Android系统架构

Android 应用开发一般情况下只会在应用层开发,但是有时候需要更改或自定义 Framework 层的 API,也经常会遇到修改或者编译 Native 层的 C/C++ 和 Java之间通信, JNI 虽然是 Android 开发中必不可少的,但为了简化难度和内容量,我们本次分享的内容仅限于应用层。

HelloWord

说明:本次组内分享是针对前端同学的,所以下面的内容我会认为阅读对象都是前端大牛,如果你不是前端的同学可能在你阅读的过程中会引起不适,实在抱歉。

Android的开发 IDE 经历了一个从 eclipse ADT 插件到 AndroidStudio(基于IDEA) 的转变,对于现在要学习Android的朋友来说无疑是一个好消息,因为 eclipse 的时代太痛苦了。这个 AndroidStudio 工具博大精深,介绍这个估计也得花个大把时间,好在你们都会 WebStorm.

从官网或者你能搜到的地方下载 AndroidStudio 安装包,然后安装(这个过程忽略了,毕竟我们都是大牛级别的人物嘛)。

Android Studio

其实,Android的 Helloword 不需要你写一行代码,因为Android在创建工程的时候会有一些简单的模板(工程开始):

选择空白模板

接下来,重点来了,下面这些配置很关键:

创建工程配置

程序执行结果就是一个界面上面有一行 HelloWord 文字,工程的核心目录结构如下(我删了一些不重要的):

.
├── GroupShare.iml
├── app
│   ├── build.gradle
│   ├── libs
│   ├── proguard-rules.pro
│   └── src
│       ├── androidTest
│       │   └── java
│       │       └── com
│       │           └── dlc
│       │               └── groupshare
│       │                   └── ExampleInstrumentedTest.java
│       ├── main
│       │   ├── AndroidManifest.xml
│       │   ├── java
│       │   │   └── com
│       │   │       └── dlc
│       │   │           └── groupshare
│       │   │               └── MainActivity.java
│       │   └── res
│       │       ├── drawable
│       │       │   └── ic_launcher_background.xml
│       │       ├── drawable-v24
│       │       │   └── ic_launcher_foreground.xml
│       │       ├── layout
│       │       │   └── activity_main.xml
│       │       ├── mipmap-anydpi-v26
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── mipmap-hdpi
│       │       │   ├── ic_launcher.png
│       │       │   └── ic_launcher_round.png
│       │       ├── mipmap-mdpi
│       │       │   ├── ic_launcher.png
│       │       │   └── ic_launcher_round.png
│       │       ├── mipmap-xhdpi
│       │       │   ├── ic_launcher.png
│       │       │   └── ic_launcher_round.png
│       │       ├── mipmap-xxhdpi
│       │       │   ├── ic_launcher.png
│       │       │   └── ic_launcher_round.png
│       │       ├── mipmap-xxxhdpi
│       │       │   ├── ic_launcher.png
│       │       │   └── ic_launcher_round.png
│       │       └── values
│       │           ├── colors.xml
│       │           ├── strings.xml
│       │           └── styles.xml
│       └── test
│           └── java
│               └── com
│                   └── dlc
│                       └── groupshare
│                           └── ExampleUnitTest.java
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle

上面的目录结构可能看起来一头雾水,我们只需要看一些核心的东西就好了,其他的先不用管。例如: gradle 构建配置、测试框架、界面xml、资源文件、控制器(Activity)。

视图和界面

在 Android 中支持 xml 文档来描述界面布局结构,本质上也是转换为 Java 对象。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="100dp"
    tools:context=".MainActivity">


    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="150dp"
        android:scaleType="centerInside"
        android:src="@mipmap/logo"/>


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="80px"
        android:text="嗨,迪尔西的小伙伴们!" />

</LinearLayout>

上面有一个尺寸单位 dp , 在 Android 中尺寸单位有四个 dp 、sp、 px 、pt。

  • px 像素:这个不用解释了,对应屏幕上面的实际像素。
  • pt 榜:长度单位 1pt = 1/72英尺(1英尺 = 2.54厘米)。
  • dp/dip/sp 屏幕密度的抽象单位: 这个单位是基于 160dpi 的屏幕,所以在 160dpi屏幕上 1dp = 1px, 同理 1dp 在 320dpi 的设备上显示 2px。

dpi : 图像每英寸长度内的像素点数。DPI(Dots Per Inch,每英寸点数)是一个量度单位,或者叫像素密度单位。

Density BucketScreen DensityPhysical SizePixel Size
ldpi120 dpi0.5 x 0.5 in0.5 in * 120 dpi = 60x60 px
mdpi160 dpi0.5 x 0.5 in0.5 in * 160 dpi = 80x80 px
hdpi240 dpi0.5 x 0.5 in0.5 in * 240 dpi = 120x120 px
xhdpi320 dpi0.5 x 0.5 in0.5 in * 320 dpi = 160x160 px
xxhdpi480 dpi0.5 x 0.5 in0.5 in * 480 dpi = 240x240 px
xxxhdpi640 dpi0.5 x 0.5 in0.5 in * 640 dpi = 320x320 px

Android 的 UI 界面都是由 ViewViewGroup 及其派生类组合而成的。其中,View 是所有 UI 组件的基类,而 ViewGroup 是容纳 View 及其派生类的容器,ViewGroup 也是从 View 派生出来的。一般来说,开发UI界面都不会直接使用 View 和 ViewGroup (自定义控件的时候使用),而是使用其派生类。

容器类布局

  • LinearLayout 线性布局
  • RelativeLayout 相对布局
  • FrameLayout 帧布局
  • AbsoluteLayout 绝对布局(几乎没啥用)
  • TableLayout 表格布局(几乎没啥用)
  • ConstraintLayout 约束布局(2016年新出的,可替代RelativeLayout实现复杂的扁平化布局)

另外还有三个特殊的,配合适配器用的布局:

  • ListView (可以废弃了,直接用RecyclerView)
  • GridView 网格适配器布局
  • RecyclerView 复杂多样的列表布局,配合适配器使用。

组件类布局

  • ImageView 图片展示
  • Button 按钮
  • TextView 文本标签
  • EditText 文本输入框
  • RadioButton 单选框
  • CheckBox 复选框

控制器生命周期

在 Android 中有一个类负责加载界面并处理界面和数据直接的连接关系(类似于MVC中的 Controller),所以我称它为控制器(你可以理解为一个你看到的界面的所有者)。

package com.dlc.groupshare;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

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

和你们使用的 React 或者 Vue 类似,Activity 也有自己的生命周期:

Activity的生命周期

事实上 Activity 的生命周期远不止这些,但是这些是最常用的,除了 Activity 之外,还有解决碎片化的 Fragment 同样也有自己的生命周期,并且和它绑定的 Activity 的生命周期有关联(这里不讨论 Fragment)。

第二个界面

上面只有一个界面(MainActivity),它为什么是我们第一个界面,是因为在 AndroidManifest.xml 文件中注册了 android.intent.action.MAIN, 当然入口界面只能有一个,但是整个应用的启动入口并不是 MainActivity, 而是 Application,我们可以自定义 Application 做一些全局的操作和初始化等。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dlc.groupshare">

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

这个注册表文件是 Android 中举足轻重的配置文件,这里面主要做 权限声明、注册组件、配置主题、配置Application路径等。接下来我们新建一个名叫 SecondActivity 的控制器,然后创建它的布局文件 activity_second.xml, 最后记得在注册表里面注册。

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:gravity="center"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="80px"
        android:text="你到了第二个界面了" />

</LinearLayout>
<activity android:name=".SecondActivity"/>

接下来我们面临的问题是如何从一个界面跳转到另一个界面,这要使用到 Activity 里面的 Intent 了。

在注册表中需要注册的是 Android 中的四大组件 Activity , Service , BroadcaseReciver , ContentProvider

界面切换

Intent 的中文意思是“意图,意向”,在 Android 中提供了 Intent 机制来协助应用间的交互与通讯,Intent 负责对应用中一次操作的动 作、动作涉及数据、附加数据进行描述,Android 则根据此 Intent 的描述,负责找到对应的组件,将 Intent传递给调用的组件,并完成组件的调用。

在跳转前我们得先有一个触发条件,给 logo 加一个点击事件,然后点击logo就跳转到 SecondActivity.

public class MainActivity extends AppCompatActivity {

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

        findViewById(R.id.iv_logo).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
            }
        });
    }
}

很多时候跳转一个界面不会这么简单,可能需要携带一些数据,甚至是一些复杂的对象或者集合(需要序列化或实现Parcelable),Intent 中有一个 Bundle 对象可以携带数据。

//FirstActivity
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
intent.putExtra("userId", user.getUserId());
startActivity(intent);

//SecondActivity
int userId = getIntent().getIntExtra("userId", 0);

这样我们就成功的从 MainActivity 跳转到了 SecondActivity, 这里存在一个入栈和出站的问题,入栈和出站的模式有四种,可以在注册表的每个activity下配置,这里不做讨论(问题涉及到的知识点比较多),但是要明白这个过程和部分原理:

  1. 为什么能返回到上一个界面,和当前栈 (Task) 的内容有密切的关系。
  2. 一个app中可以不止一个栈 (Task)。

简单 Activity Task Push 和 Poll 过程演示

详细可以参考我的另外两篇博文:《Activity启动模式与任务栈(Task)全面深入记录(上)》《Activity启动模式与任务栈(Task)全面深入记录(下)》

线程切换

上面的案例已经完成,在打包app之前我想简单提及一下 Android 中的线程问题,因为这个是不可逃避的问题。

简单的说,在 Android 中所有 UI 渲染刷新的过程都需要在 UI 线程(主线程)中执行,而 UI 线程不能被阻塞,否则就会造成 ARN (Application Not Responding) 导致奔溃。所以通常一些耗时操作,例如 I/O、网络请求、音视频转换等需要在非 UI 线程执行,然后需要将结果数据刷新到界面的时候又要切换到 UI 线程。

下面我们来模拟一个 ANR, 假设我们点击logo跳转前要进行一次耗时操作,修改代码如下:

findViewById(R.id.iv_logo).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i("TEST", Thread.currentThread().getName());  //输出线程名
        SystemClock.sleep(10 * 1000);   //耗时10秒钟
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        startActivity(intent);
    }
});

ARN 异常模拟

接下来我们模拟用最简单的方式解决一下耗时操作造成的主线程阻塞问题:

private Handler mHandler = new Handler(){  //这里是主线程
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case HAND_SLEEP_OVER:
                    showToast("耗时操作完毕");
                    break;
            }
        }
    };
findViewById(R.id.iv_logo).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        showToast("耗时操作开始");  //这里是主线程
        new Thread(){       
            @Override
            public void run() {   //这里是非主线程
                SystemClock.sleep(20 * 1000); //耗时20秒钟
                mHandler.sendEmptyMessage(HAND_SLEEP_OVER);
            }
        }.start();
    }
}

gradle构建

Java世界中主要有三大构建工具:Ant、Maven和Gradle。经过几年的发展,Ant几乎销声匿迹、Maven也日薄西山,而Gradle的发展则如日中天。Android Studio 中默认使用 Gradle 作为构建工具,Gradle和Maven的主要功能主要分为5点,分别是依赖管理系统、多模块构建、一致的项目结构、一致的构建模型和插件机制。

关于 Maven 和 Gradle 的比较详细: https://gradle.org/maven-vs-gradle/

需要注意的是 Gradle 并不是 Android 专有的工具,它的用途非常广泛,而且要搞懂它也不容易,你需要知道 Maven 构建的过程和原理、懂得 Groovy 语法、对应插件的 DSL。

Android 的构建过程

如上图,典型的Android应用模块构建流程通常依循如下步骤:

  1. 编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中包括运行在 Android 设备上的字节码),将所有其他内容转换成已编译资源。
  2. APK 打包器将 DEX 文件和已编译资源合并成单个 APK。不过,必须先签署 APK,才能将应用安装并部署到 Android 设备上。
  3. APK 打包器使用调试或发布密钥库签署您的 APK.
  4. 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时的内存占用。

顶级构建文件

buildscript {
    
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
    }
}

上面的repositories是第三方插件的托管仓库, dependencies 是插件 jar 包地址,除了上面配置,默认生成的Android工程中还有如下配置:

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

上面的 allprojects{} 中用于配置影响到整个工程的所有 build.gradle 的公共配置。运行 gradle clean 时,执行此处定义的 task,该任务继承自 Delete,删除根目录中的 build 目录。

模块构建文件

我们工程中的 app 就是一个模块 (module),一般情况下我们的工程可能会有多个 module,之间会有一些依赖关系, 一般结构如下:

Android的模块结构示意

app 就是我们这个工程的主工程(主模块),其他的 module 先不需要创建,接下来我们看看 module 的构建配置文件(位于 app 模块的根路径下 build.gradle)。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.dlc.groupshare"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

上面 com.android.application 就是前面提到的 plugin id, android{ } 是 Android 插件提供的一个扩展类型,可以自定义一些特定配置。android中可用的配置模块:

模块描述
aaptOptions{ }aapt是一个可以将资源文件编译成二进制文件的工具。aaptOptions表示aapt工具设置的可选项参数。
adbOptions { }adb的可选项参数
buildTypes { }需要构建的类型,比如release、debug,更多配置请参考BuildType
compileOptions { }指定java编译器类型,更多配置请参考CompileOptions
dataBinding { }通过声明的方式将UI组件绑定到应用程序数据,更多配置请参考Data Binding Library
defaultConfig { }application的所有配置属性,更多配置请参考ProductFlavor
dexOptions { }指定dex工具选项,例如启用库预处理
externalNativeBuild { }native编译支持
jacoco { }JaCoCo可选项参数
lintOptions { }指定lint工具选项,更多配置请参考LintOptions
packagingOptions { }packaging的可选参数,更多配置请参考PackagingOptions
productFlavors { }产品风格配置
signingConfigs { }签名文件的可选项参数
sourceSets { }资源文件目录指定(Android中有自己的AndroidSourceSets,这个一般用于assets,jin等目录)
splits { }splits类型
testOptions { }测试可选项参数

除了上面配置外,默认生成的Android工程中还有如下配置:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

dependencies { } 是模块级别的配置,在其中声明本模块的依赖。另外你可能会发现最外层有一个 gradle.properties 文件,用来配置 Gradle 设置,例如 Gradle 后台进程的最大堆大小等。

安装模拟器

虽说 Android 开发都应该在实际设备上开发预览,但是难免有时候我们只能买得起苹果手机没有 Android 手机而烦恼。

Android 模拟器五花八门,但是基本上都是给游戏玩家提供的,我们作为开发人员当然不能用那么 low 的模拟器了,下面推荐两种模拟器:

Android Studio自带模拟器

X86 arm 架构选择

建议你尽可能的选择 X86 架构的模拟器,在 Android 的初期只有 arm 架构模拟器,速度是非常慢的,非常卡,但是不排除你要在 arm 架构的模拟器上做测试。还有一个要注意的是不要选择包含 Google API 的模拟器,因为国内的情况你也懂,毕竟我们的手机上也没有 Google 服务啊。

Genymotion模拟器

Genymotion模拟器不仅仅快,而且强大,但是收费的,不过我们学习的话可以用它提供的个人版已经足够了。

下载地址:https://www.genymotion.com/fun-zone/

如果你是 Mac 系统,你还得安装 Oracle Vitrual Box, 这里暂不讨论。

安装好后就可以创建我们的模拟器了,创建好后点击 Run 按钮就会发现已经识别模拟器了,直接安装即可。

总结

通过上面的小案例引出了一些 Android 开发中比较核心的问题,通常你在学习 Android 的路上都会遇到这些问题和技术点,如果你掌握了这些内容,你已经踏入了 Android 世界,这个世界是异常庞大的生态,你可以选择某一个领域深入下去,也可以只了解一下 Android 开发流程和技术概况即可,也许整个过程是痛苦的,但只要坚持结果一定是你想要的。无论怎样我希望通过这个简短的分享让大家了解并认识 Android 同时能够引起大家对 Android 开发的兴趣,毕竟任何一个培训都不可能做到完整的知识分享。

附件

PDF 版查看

小练习:尝试做如下界面布局,用如下两种方式:第一种方式:使用线性布局和相对布局。第二种方式:直接使用约束布局完成。

练习界面布局设计图