从零开始搭建Android组件化框架

问题

在已经开发过几个项目的童鞋,如果这时需要重新开发一个新项目,是否需要自己重新搭建框架呢,还是从老项目中拷贝粘贴? 我们是否可以封装一个底层的lib库,这个底层的公共基础库 包括了一些第三方库(如: okhttp, retrofit2, glide 等)的初始化及简单的封装和一些公共的base类.这样我们重新开发一个新项目只要依赖这个库就马上可以进行业务逻辑的开发了.

什么是组件化

组件化简单概括就是把一个功能完整的 App 或模块拆分成多个子模块, 每个子模块可以独立编译和运行, 也可以任意组合成另一个新的 App 或模块, 每个模块即不相互依赖但又可以相互交互, 遇到某些特殊情况甚至可以升级或者降级.
大家可以点击该文章查看 [组件化框架简介] (https://www.jianshu.com/p/40e745038471)

前言

从今年开始接触组件化项目,刚开始感觉组件化非常的高大上,经过一段时间的了解,现在对组件化终于有了一定的了解,为了能更熟悉运用于实际的项目中,决定自己写一个demo框架,也想解决上述的问题,这章文档主要是对我所学习的内容做一个总结,巩固Android的一些基础知识,非常适合初学者,并且架构简单,学习成本低,对于一个急需快速组件化拆分的项目是很适合的. 望高手们多指教!
附上 Demo地址 Github : 您的 Star 是我坚持的动力 ✊

请使用 Android studio 3.0 以上版本

简洁组件化框架(重新抽取最新的组件化框架),请点击查看: https://github.com/tome34/frameMo


老规则,先上效果图

项目效果图.gif

备注:该Demo只完成十分之一,有时间会一直更新

一,demo 架构图详解

系统架构设计.png

上图是组件化工程模型,下面会列举一些组件化工程中用到的名词的含义:

集成模式: 所有的业务组件被“app壳工程”依赖,组成一个完整的APP;
组件模式: 可以独立开发业务组件,每一个业务组件就是一个APP;
app壳工程: 负责管理各个业务组件,和打包apk,没有具体的业务功能;
业务组件: 根据公司具体业务而独立形成一个的工程;
Main组件:属于业务组件,指定APP启动页面、主界面 ;
Common组件: 也就是功能组件(component_base 模块),支撑业务组件的基础,提供多数业务组件需要的功能,例如提供网络请求功能;
component_data组件: 这里我存放了与项目相关的公共数据,例如bean的基类,IntentKV存数据的键值对等.
SDK组件: 集成微信,支付宝支付,分享,推送等常用的第三方框架.


组件化的优点

Android APP组件化架构的目标是告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为arr包集成到“app壳工程”中,组成一个完整功能的APP;
从组件化工程模型中可以看到,业务组件之间是独立的,没有关联的,这些业务组件在集成模式下是一个个library,被app壳工程所依赖,组成一个具有完整业务功能的APP应用,但是在组件开发模式下,业务组件又变成了一个个application,它们可以独立开发和调试,由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可以显著减少编译时间。


Arouter路由.png

这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系,而Android中的路由实际就是对URL Scheme的封装;
对阿里巴巴的Arouter不熟悉的可以点击了解:https://github.com/alibaba/ARouter


二, 项目中base基类和Libraries的简介

项目目录.png
component_base基础库.png
第三方依赖.png

对于Android中常用的基类库,主要包括开发常用的一些框架。

  • 该项目基于目前比较流行的框架:Material Design + MVP + Rxjava2 + Retrofit2 + GreenDao + Glide

  • 部分的代码及API接口来自 Awesome WanAndroid 项目,以学习为目的,可以查看原著:https://github.com/JsonChao/Awesome-WanAndroid

  • 部分 Base基类,Libraries 简介

1、Base 基类(BaseMVPActivity, BaseMVPFragment, BasePermissionActivity,
BaseListFragment, BaseTabListFragment, BaseObserver...)
2、MVP 基类(BaseView, BasePresenter ...)
3、Retrofit2 + RxJava2 的封装 https://github.com/square/retrofit
4、Autolayout 鸿洋大神的Android全尺寸适配框架.
5、安卓调试神器-Stetho(Facebook出品 建议使用) https://github.com/facebook/stetho
6、LocaleHelper 的封装,多语言包括阿拉伯语,从右到左布局,参考:https://juejin.im/entry/599397c5f265da2480332362
7、Logger 网络日志的简单封装 https://github.com/orhanobut/logger
8、通用的工具类
9、自定义view(包括对话框,ToolBar布局,圆形图片等view的自定义)
11、其他等等

三,组件化搭建流程

好了,前面废话了很多,下面才开始我们真正的组件化搭建过程,首先我们来看看组件模式和集成模式切换的实现:

config.gradle主要来管理统一的SDK版本,避免版本冲突 (详细代码请下载项目查看 https://github.com/tome34/frameDemoMo2 )

app壳的config.gradle部分代码

ext是自定义属性,把所有关于版本的信息都利用ext放在另一个自己新建的gradle文件中集中管理

1)组件模式和集成模式的转换最终使用方式

    isModule = false
    moduleShopMall = false
    moduleShopCart = false
    moduleWelfare = false

    isModule false;  表示整个app运行, true: 表示单独运行某一个module
    moduleShopMall;  false:作为Lib组件存在,true:作为application存在(其他  
    module同理)
集成模式

1, 首先需要在 config.gradle 文件中设置 appDebug = false
2, 然后 Sync 下
3, 最后选择 app 运行即可

组件模式

1, 首先需要在 config.gradle 文件中设置 appDebug = true
2, 然后 Sync 下
3, 最后相应的模块(moduleShopMall 、moduleShopCart 、moduleWelfare )进行运行即可

2)组件化的配置流程

Android Studio中的Module主要有两种属性,分别为:

application属性,可以独立运行的Android程序,也就是我们的APP;    
apply plugin: ‘com.android.application’

library属性,不可以独立运行,一般是Android程序依赖的库文件;
apply plugin: ‘com.android.library’

Module的属性是在每个组件的 build.gradle 文件中配置的,当我们在组件模式开发时,业务组件应处于application属性,这时的业务组件就是一个 Android App,可以独立开发和调试;而当我们转换到集成模式开发时,业务组件应该处于 library 属性,这样才能被我们的“app壳工程”所依赖,组成一个具有完整功能的APP;

1: 组件模式和集成模式的转换

我们在AndroidStudio创建一个Android项目后,新建了config.gradle文件,并配置ext 管理整个项目的常量,那么在Android项目中的任何一个build.gradle文件中都可以把config.gradle中的常量读取出来, 如下代码:

app壳的build.gradle

注意:每次改变isModule的值后,都要同步项目才能生效;

2: 组件之间AndroidManifest合并

在 AndroidStudio 中每一个组件都会有对应的 AndroidManifest.xml,用于声明需要的权限、Application、Activity、Service、Broadcast等,当项目处于组件模式时,业务组件的 AndroidManifest.xml 应该具有一个 Android APP 所具有的的所有属性,尤其是声明 Application 和要 launch的Activity,但是当项目处于集成模式的时候, 我们要为组件开发模式下的业务组件再创建一个AndroidManifest.xml,然后根据isModule指定AndroidManifest.xml的文件路径,让业务组件在集成模式和组件模式下使用不同的AndroidManifest.xml,这样表单冲突的问题就可以规避了.如下module目录结构及:

module目录结构

上图是组件化项目中一个标准的业务组件目录结构,首先我们在main文件夹下创建一个module文件夹用于存放组件开发模式下业务组件的 AndroidManifest.xml,而 AndroidStudio 生成的 AndroidManifest.xml 则依然保留,并用于集成开发模式下业务组件的表单;然后我们需要在业务组件的 build.gradle 中指定表单的路径,代码如下:

   /*java插件引入了一个概念叫做SourceSets,通过修改SourceSets中的属性,可以指定哪些源文件
    (或文件夹下的源文件)要被编译,哪些源文件要被排除。*/
    sourceSets {
        main {
            if (rootProject.ext.moduleShopMall) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    //排除java/debug文件夹下的所有文件
                    exclude '*module'
                }
            }
        }
    }

这样在不同的开发模式下就会读取到不同的 AndroidManifest.xml ,然后我们需要修改这两个表单的内容以为我们不同的开发模式服务。

3: 全局Context的获取及组件数据初始化

当Android程序启动时,Android系统会为每个程序创建一个 Application 类的对象,并且只创建一个,application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。在默认情况下应用系统会自动生成 Application 对象,但是如果我们自定义了 Application,那就需要在 AndroidManifest.xml 中声明告知系统,实例化的时候,是实例化我们自定义的,而非默认的,如下图:

组件化的AndroidManifest.xml

android:name属性——是用来设置所有activity属于哪个application的,默认是android.app.Application,那么当我们是组件化项目我们就指定我们创建的module下的MyApp,该MyApp继承于基类的BaseApplication.

组件化的appliction

BaseApplication 主要用于各个业务组件和app壳工程中声明的 Application 类继承用的,只要各个业务组件和app壳工程中声明的Application类继承了 BaseApplication,当应用启动时 BaseApplication 就会被动实例化,这样从 BaseApplication 获取的 Context 就会生效,也就从根本上解决了我们不能直接从各个组件获取全局 Context 的问题;

4: library依赖问题

在组件化工程模型图中,我是把所有公共的功能组件全部放在component_base里面,我是想后期其他项目需要用到只依赖这个module就可以了,不需要依赖多个,这也有致命的缺点,就是有过多的依赖,不够单一,目前为了方便先这样做了.

上面我们有介绍了自定义 config.gradle 配置来统一管理我们的版本, 所有在每个组件化项目中都依赖这个基础库component_base,如下代码

//公用依赖包
    implementation project(':component_base')
    implementation project(':component_data')

而我们的component_base 的build.gradle的代码如下:

apply plugin: 'com.android.library'
apply plugin: 'me.tatarka.retrolambda'  //lambda配置
apply plugin: 'com.jakewharton.butterknife'

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    buildToolsVersion rootProject.ext.android.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        //MultiDex分包方法
        multiDexEnabled true

        //Arouter路由配置
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }

    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    //防止编译的时候oom、GC
    dexOptions {
        javaMaxHeapSize "4g"
    }

    //解决.9图问题
    aaptOptions {
        cruncherEnabled = false
        useNewCruncher = false
    }

}
    dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api rootProject.ext.dependencies["appcompat_v7"]
    api rootProject.ext.dependencies["constraint_layout"]
    api rootProject.ext.dependencies["cardview-v7"]
    api rootProject.ext.dependencies["design"]
    testApi rootProject.ext.dependencies["junit"]
    androidTestApi rootProject.ext.dependencies["runner"]
    androidTestApi rootProject.ext.dependencies["espresso_core"]

    //MultiDex分包方法
    api rootProject.ext.dependencies["multidex"]
    //Arouter路由
    annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
    api rootProject.ext.dependencies["arouter_api"]
    api rootProject.ext.dependencies["arouter_annotation"]
    //网络
    api rootProject.ext.dependencies["retrofit2"]
    api rootProject.ext.dependencies["converter-gson"]
    api rootProject.ext.dependencies["adapter-rxjava2"]
    api rootProject.ext.dependencies["rxjava2:rxandroid"]
    api rootProject.ext.dependencies["rxjava2"]
    api rootProject.ext.dependencies["logging-interceptor"]
    //黄油刀
    annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
    api rootProject.ext.dependencies["butterknife"]
    //日志
    api rootProject.ext.dependencies["logger"]
    //仿ios进度条
    api rootProject.ext.dependencies["kprogresshud"]
    //6.0权限
    api rootProject.ext.dependencies["permissionsdispatcher"]
    api rootProject.ext.dependencies["baseRecyclerViewAdapterHelper"]
    //图片
    api rootProject.ext.dependencies["glide"]
    //图片缩放,View Pager中浏览库
    api rootProject.ext.dependencies["photoview"]
    //仿ios 的PickerView时间选择器和条件选择器
    api rootProject.ext.dependencies["pickerView"]
    //上拉加载
    api rootProject.ext.dependencies["smartRefreshLayout"]
    api rootProject.ext.dependencies["SmartRefreshHeader"]
    //eventbus 发布/订阅事件总线
    api rootProject.ext.dependencies["eventbus"]
    //banner轮播图
    api rootProject.ext.dependencies["banner"]
    //RecyclerView万能适配器
    api rootProject.ext.dependencies["baseRecyclerViewAdapterHelper"]
    //Android屏幕适配
    api rootProject.ext.dependencies["autolayout"]
    //安卓调试神器-Stetho
    api rootProject.ext.dependencies["stetho"]
    api rootProject.ext.dependencies["stetho-okhttp3"]

    //公共数据
    implementation project(':component_data')
    }

我们还是要考虑另一个情况,我们在build.gradle中compile的第三方库,例如AndroidSupport库经常会被一些开源的控件所依赖,而我们自己一定也会compile AndroidSupport库 ,这就会造成第三方包和我们自己的包存在重复加载,解决办法就是找出那个多出来的库,并将多出来的库给排除掉,而且Gradle也是支持这样做的,分别有两种方式:根据组件名排除或者根据包名排除,下面以排除support-v4库为例:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
        exclude module: 'support-v4'//根据组件名排除
        exclude group: 'android.support.v4'//根据包名排除
    }
}
5 Android Studio 3.0开始Gradle的配置
  • 这里介绍一下:android gradle tools 3.X 中依赖,implement、api 和compile区别

2017 年google 后,Android studio 版本更新至3.0,更新中,连带着com.android.tools.build:gradle 工具也升级到了3.0.0,在3.0.0中使用了最新的Gralde 4.0 里程碑版本作为gradle 的编译版本,该版本gradle编译速度有所加速,更加欣喜的是,完全支持Java8。当然,对于Kotlin的支持,在这个版本也有所体现,Kotlin插件默认是安装的。

在com.android.tools.build:gradle 3.0 以下版本依赖在gradle 中的声明写法

compile fileTree(dir: 'libs', include: ['*.jar'])

但在3.0后的写法为

implementation fileTree(dir: 'libs', include: ['*.jar'])
或
api fileTree(dir: 'libs', include: ['*.jar'])

在3.0版本中,compile 指令被标注为过时方法,而新增了两个依赖指令,一个是implement 和api,这两个都可以进行依赖添加,他们的区别是:

  • api 指令: 完全等同于compile指令,没区别,你将所有的compile改成api,完全没有错,它是对外部公开的。
  • implement指令: 这个指令的特点就是,对于使用了该命令编译的依赖,对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序,也就是将该依赖隐藏在内部,而不对外部公开。
  • 从Android Studio 3.0开始,使用annotationProcessor代替apt。不可再使用apt,否则会编译报错。
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //把implementation 用api代替,它是对外部公开的, 所有其他的module就不需要    
    //添加该依赖
    api rootProject.ext.dependencies["appcompat_v7"]
    api rootProject.ext.dependencies["constraint_layout"]
    api rootProject.ext.dependencies["cardview-v7"]
    api rootProject.ext.dependencies["design"]
    testApi rootProject.ext.dependencies["junit"]
    androidTestApi rootProject.ext.dependencies["runner"]
    androidTestApi rootProject.ext.dependencies["espresso_core"]
}

如果你想了解更多的Gradle知识,请点击查看:https://www.jianshu.com/p/8b8a550246bd

6 组件之间调用和通信

在组件化开发的时候,组件之间是没有依赖关系,我们不能在使用显示调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,假如现在要从A业务组件跳转到业务B组件,并且要携带参数跳转,这时候就需要引入“路由”的概念了.目前项目使用了阿里巴巴的Arouter路由,有兴趣的童鞋也可以去了解其他的"路由"框架,比如开源库的ActivityRouter, LiteRouter 路由框架 , AndRouter 路由框架 等.

阿里巴巴的Arouter我就不在这里介绍了,请点击了解使用方式: https://blog.csdn.net/zhaoyanjun6/article/details/76165252

7 组件化中的butterKnife的坑
  • 特别注意不要使用最新的8.8.1版本,而应该使用8.4.0 ,因为最新的版本好像不兼容组件化模式.按照8.4.0版本的方式依赖.
    第一步:
    在项目的buid.gradle中添加这两个依赖.
buildscript {
  dependencies {
        classpath 'com.jakewharton:butterknife-gradle-plugin:8.4.0'
   classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

第二步:
在module的build.gredle 文件中的dependencies标签中添加

 annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0',
 implementation 'com.jakewharton:butterknife:8.4.0',

第三步:
在module的build.gredle 文件中添加

apply plugin: 'com.jakewharton.butterknife'

这三步就完成了butterKnife的接入了,如果你的Android studio 3.x 还没安装butterKnife的插件的,就先安装一下插件.

  • 组件化中的butterKnife的使用

1、用R2代替R findviewid

  @BindView(R2.id.view_pager)
    ViewPager mViewPager;
    @BindView(R2.id.bottom_navigation_view)
    BottomNavigationView mBottomNavigationView;
    @BindView(R2.id.nav_view)
    NavigationView mNavView;

每次都要syn一下,才会生效的.

2、在click方法中同样使用R2,但是找id的时候使用R。

@OnClick({R2.id.textView, R2.id.button1})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.textView:
                break;
            case R.id.button1:
                break;
        }
    }

3、特别注意library中switch-case的使用,在library中是不能使用switch- case 找id的,解决方法就是用if-else代替。

@OnClick({R2.id.textView, R2.id.button1, R2.id.button2})
    public void onViewClicked(View view) {
        int i = view.getId();
        if (i == R.id.textView) {
        
        } else if (i == R.id.button1) {

        } else if (i == R.id.button2) {

        } 

就这样通过几个简单的步骤基本就完成了组件化的配置了. 具体可以运行项目查看.

组件化项目的混淆方案

组件化项目的Java代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。集成开发模式下在app壳工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在app壳工程中,各个业务组件的混淆配置规则都应该在app壳工程中的混淆配置文件中添加和修改。

之所以不采用在每个业务组件中开启混淆的方案,是因为 组件在集成模式下都被 Gradle 构建成了 release 类型的arr包,一旦业务组件的代码被混淆,而这时候代码中又出现了bug,将很难根据日志找出导致bug的原因;另外每个业务组件中都保留一份混淆配置文件非常不便于修改和管理,这也是不推荐在业务组件的 build.gradle 文件中配置 buildTypes (构建类型)的原因。

四,版本更新

2018.7.5 Android studio 版本升级到最新(3.1.3),并修复一些异常.

2018.7.23 更新内容:
1)完善该demo的页面
2)新增部分常用自定义控件
3)新增mvc,mvp两种模式
4)优化组件化框架
5)新增MVP模版代码生成插件 https://github.com/longforus/MvpAutoCodePlus
6)修改已知bug

git2.gif

git3.gif

2018.8.14 更新了购物车,商品详情页,仿淘宝属性选择

商品详情页及购物车.gif

...

五,组件化接口实现

使用组件化框架,假如不使用第三方框架路由,我们该如何去实现呢?以下通过接口的方式来实现跨模块调用.
先来个组件化的思维导图:


组件化框架.png

模块A(mall),模块B(cart)和Base Library(projectCore) 都依赖于Main APP, 假如有两个业务模块A和模块B,需要模块A调用模块B的界面.该如何实现呢?

步骤:
1,在BaseLibrary新建一个接口ItestService

public interface ItestService {
    void launch(Context context);
}

为了能统一管理整个接口,我们新建一个单例,添加set和get方法

public class ServiceFactory {

    private static  ServiceFactory instance;

    private static  Object tag = new Object();

    public ServiceFactory() {
    }

    public static ServiceFactory getInstance(){
        if (instance == null){
            synchronized (tag){
                if (instance == null){
                    instance = new ServiceFactory();
                }
            }
        }
        return instance;
    }
    private ItestService mItestService;

    public ItestService getItestService() {
        return mItestService;
    }

    public void setItestService(ItestService itestService) {
        mItestService = itestService;
    }
    
}

2,我们在B模块新建一个CartService类,去实现接口ItestService.通过传过来的context,去调用CartTestActivity.

public class CartService implements ItestService {
    @Override
    public void launch(Context context) {
        context.startActivity(new Intent(context,CartTestActivity.class));
    }
}

3,A模块的TestActivity 调用B模块的CartTestActivity界面,那我们在A模块的activity传一个上下文context,让实现类CartService去启动B模块的CartTestService界面(见步骤2).

 TextView button =(TextView) findViewById(R.id.mall_textview);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(ServiceFactory.getInstance().getItestService() != null){
                    ServiceFactory.getInstance().getItestService().launch(TestActivity.this);
                }else {
                    ToastUtils.showShort(TestActivity.this, "为空了");
                }
            }
        });

4,由步骤3我们知道,ServiceFactory.getInstance().getItestService()为空,此时我们需要给setItestService传递对象

//为了规范统一,我们先在BaseLibrary新建一个接口
public interface IAppComponent {

   public void initializa(Application app);
}
//我们在B模块的新建一个CartApp,实现IAppComponent
//注意:B模块作为Library运行,是不执行CartApp的.单独运行才执行CartAPP.
public class CartApp extends Application implements IAppComponent {

    private Application mApplication;
    @Override
    public void onCreate() {
        super.onCreate();
        initializa(this);
    }

    @Override
    public void initializa(Application app) {
        mApplication = app;
        L.d("执行了initializa");
        ServiceFactory.getInstance().setItestService(new CartService());
    }
}

5,在步骤4,我们已经实现了接口,最后一步就是我们需要调用这个接口,我们可以在Main APP 新建一个MyApplication 继承Application,通过反射调用cartApp的initializa.

 @Override
    public void onCreate() {
        super.onCreate();
        myApplication = this ;
        initiali(this);
    }


    public void initiali(Application app) {
        L.d("执行了+++"+"initializa");
        //通过反射
        try {
            Class<?> aClass = Class.forName("com.fec.yunmall.cart.app.CartApp");
            Object object = aClass.newInstance();
            if (object instanceof IAppComponent){
                ((IAppComponent)object).initializa(this);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

完毕!!!



该demo会持续优化更新,把知识点及工具类都汇总于该demo中,便于学习及日后查找.

最后贴出Android组件化Demo地址:https://github.com/tome34/frameDemoMo2

** 如果你觉得这篇文章对你有帮助或启发,请点下关注,谢谢 _ **


感谢以下文章提供的帮助:

1, https://www.jianshu.com/p/8b8a550246bd
2, https://www.jianshu.com/p/f671dd76868f
3, https://blog.csdn.net/guiying712/article/details/55213884
4, https://mp.weixin.qq.com/s/8PRbtmr2TNBH1MkqdFNiyg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • Android组件化项目地址:Android组件化项目AndroidModulePattern Android组件...
    半灬边灬天阅读 2,862评论 4 37
  • 林颖涟,是我的名字。我的生活或许没什么特别的,或许有些特别的就是喜欢一个学长叫胥唅。 天色暗下来,室友木子洗澡出来...
    玻璃之瞳阅读 221评论 1 1
  • b448d4edabfa阅读 433评论 0 1
  • 你夺走了我的爱情,夺走了我的记忆,最后连我的念想你也要夺走,连一个称呼都不给我留下。你怎么可以做到这样残忍?我努力...
    追风骑士J阅读 150评论 0 2