作者归档:RAE

docker 安装 redis

1、下载镜像

$ docker pull redis

2、官网下载redis.conf配置文件

$ curl http://download.redis.io/redis-stable/redis.conf > redis.conf

2、创建容器

$ docker run --name redis -p 6379:6379 -v  /docker/redis/:/etc/redis/ -itd redis  /etc/redis/redis.conf --appendonly yes

Mysql数据库备份与恢复

备份数据库

备份整个数据库

mysqldump -u root -p source_db > /tmp/souce_db.bak

备份整个数据库 gzip压缩

mysqldump -u root -p source_db   | gzip > /tmp/souce_db.gz

指定备份某些表

mysqldump -u root -p  source_db --tables tb_user tb_config |gzip >  /tmp/souce_db.gz

恢复数据库

$ mysql -uroot -p
$ show databases;
$ use target_db;
$ source /tmp/souce_db.bak

恢复数据库 gzip压缩

gunzip < souce_db.gz | mysql -uroot -p source_db

Android-View篇之自定义验证码输入框

首先,我们来看看实现的是怎么样的效果:

验证码输入框效果图

如果我们拿到这样的UI,想到的布局应该是用4个EditText包在横向的LinearLayout里面,但今天要讲的View,所以我们决定用一个自定义的EditText 画出来。

学到什么?

  • 基本理解画布概念
  • 画布的状态、平移
  • 布局测量
  • 画图片

功能需求

  • 高亮当前输入框
  • 输入满4个数字自动调用方法

思路

完全重画一个EditText,就包含了测量布局重新绘制这两个关键步骤。好了,到这里理一下整体的思路:

  • 根据验证码个数以及边框大小来计算输入框显示的宽度
  • 覆盖原来的EditText画布,重新绘制方框
  • 根据输入的索引来确定高亮的方框
  • 重写onTextChanged 但满足验证码个数的时候调用自动完成方法

开始动手

准备开始了,果断继承一个AppCompatEditText 来初始化基本参数先:

  • 验证码个数
  • 输入方框的大小
  • 边框的大小及间距

/**
 * 验证码输入框,重写EditText的绘制方法实现。
 * @author RAE
 */
public class CodeEditText extends AppCompatEditText {

   //  验证码文本颜色
    private int mTextColor;
    // 输入的最大长度
    private int mMaxLength = 4;
    // 边框宽度
    private int mStrokeWidth;
    // 边框高度
    private int mStrokeHeight;
    // 边框之间的距离
    private int mStrokePadding = 20;
    // 用矩形来保存方框的位置、大小信息
    private final Rect mRect = new Rect();
    // 方框的背景
    private Drawable mStrokeDrawable;

   /**
     * 构造方法
     *
     */
    public CodeEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CodeEditText);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int index = typedArray.getIndex(i);
            if (index == R.styleable.CodeEditText_strokeHeight) {
                this.mStrokeHeight = (int) typedArray.getDimension(index, 60);
            } else if (index == R.styleable.CodeEditText_strokeWidth) {
                this.mStrokeWidth = (int) typedArray.getDimension(index, 60);

            } else if (index == R.styleable.CodeEditText_strokePadding) {
                this.mStrokePadding = (int) typedArray.getDimension(index, 20);

            } else if (index == R.styleable.CodeEditText_strokeBackground) {
                this.mStrokeDrawable = typedArray.getDrawable(index);

            } else if (index == R.styleable.CodeEditText_strokeLength) {
                this.mMaxLength = typedArray.getInteger(index, 4);
            }
        }
        typedArray.recycle();

        if (mStrokeDrawable == null) {
            throw new NullPointerException("stroke drawable not allowed to be null!");
        }

        setMaxLength(mMaxLength);
        setLongClickable(false);
        // 去掉背景颜色
        setBackgroundColor(Color.TRANSPARENT);
        // 不显示光标
        setCursorVisible(false);
    }

    @Override
    public boolean onTextContextMenuItem(int id) {
        return false;
    }

   /**
     * 设置最大长度
     */
    private void setMaxLength(int maxLength) {
        if (maxLength >= 0) {
            setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
        } else {
            setFilters(new InputFilter[0]);
        }
    }
}

开始测量布局

初始化完了就要开始测量布局了,计算公式为:

输入框宽度 = 边框宽度 数量 + 边框间距 (数量-1)


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 当前输入框的宽高信息
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 判断高度是否小于推荐高度
        if (height < mStrokeHeight) {
            height = mStrokeHeight;
        }

        // 输入框宽度 = 边框宽度 * 数量 + 边框间距 *(数量-1)
        int recommendWidth = mStrokeWidth * mMaxLength + mStrokePadding * (mMaxLength - 1);
        // 判断宽度是否小于推荐宽度
        if (width < recommendWidth) {
            width = recommendWidth;
        }

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode);
        // 设置测量布局
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

画家登场

来到最重要的步骤了,重画输入框!来一步步看代码注释:

    @Override
    protected void onDraw(Canvas canvas) {
        // 在画支持设置文本颜色,把系统化的文本透明掉,相当于覆盖
        mTextColor = getCurrentTextColor();
        setTextColor(Color.TRANSPARENT);
        //  系统画的方法
        super.onDraw(canvas);
        // 重新设置文本颜色
        setTextColor(mTextColor);
        // 重绘背景颜色
        drawStrokeBackground(canvas);
        // 重绘文本
        drawText(canvas);
    }

绘制背景方框


    /**
     * 绘制方框
     */
    private void drawStrokeBackground(Canvas canvas) {
        // 下面绘制方框背景颜色
        // 确定反馈位置
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = mStrokeWidth;
        mRect.bottom = mStrokeHeight;
        int count = canvas.getSaveCount(); //  当前画布保存的状态
        canvas.save(); // 保存画布
        for (int i = 0; i < mMaxLength; i++) {
            mStrokeDrawable.setBounds(mRect); // 设置位置
            mStrokeDrawable.setState(new int[]{android.R.attr.state_enabled}); // 设置图像状态
            mStrokeDrawable.draw(canvas); //  画到画布上
            //  确定下一个方框的位置
            float dx = mRect.right + mStrokePadding; // X坐标位置
            // 保存画布
            canvas.save();
            // [注意细节] 移动画布到下一个位置
            canvas.translate(dx, 0);
        }
        // [注意细节] 把画布还原到画反馈之前的状态,这样就还原到最初位置了
        canvas.restoreToCount(count);
        // 画布归位
        canvas.translate(0, 0);

        // 下面绘制高亮状态的边框
        // 当前高亮的索引
        int activatedIndex = Math.max(0, getEditableText().length());
        mRect.left = mStrokeWidth * activatedIndex + mStrokePadding * activatedIndex;
        mRect.right = mRect.left + mStrokeWidth;
        mStrokeDrawable.setState(new int[]{android.R.attr.state_focused});
        mStrokeDrawable.setBounds(mRect);
        mStrokeDrawable.draw(canvas);

    }

一般画布的移动canvas.translate(x,y)会结合canvas.save();来使用。
1、调用canvas.save();保存当前画布的状态,用PS来解析就是按下ctrl +s键,然后帮你新建一个新的图层。你之后画的内容不会影响到之前画的内容,要回到之前的状态就调用canvas.restoreToCount(count)来还原。
2、把画布的位置移到下一个位置canvas.translate(x,y),下图所示,你会发现方框在画布中的位置没有发生变化而是画布距离发生了变化。这就是画布平移的效果了。

画布平移

画验证码文字

    /**
     * 重绘文本
     */
    private void drawText(Canvas canvas) {
        int count = canvas.getSaveCount();
        canvas.translate(0, 0);
        int length = getEditableText().length();
        for (int i = 0; i < length; i++) {
            String text = String.valueOf(getEditableText().charAt(i));
            TextPaint textPaint = getPaint();
            textPaint.setColor(mTextColor);
            // 获取文本大小
            textPaint.getTextBounds(text, 0, 1, mRect);
            // 计算(x,y) 坐标
            int x = mStrokeWidth / 2 + (mStrokeWidth + mStrokePadding) * i - (mRect.centerX());
            int y = canvas.getHeight() / 2 + mRect.height() / 2;
            canvas.drawText(text, x, y, textPaint);
        }
        canvas.restoreToCount(count);
    }

监听文本变化回调自动完成方法

    @Override
    protected void onTextChanged(CharSequence text, int start,
                                 int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);

        // 当前文本长度
        int textLength = getEditableText().length();

        if (textLength == mMaxLength) {
            hideSoftInput();
            if (mOnInputFinishListener != null) {
                mOnInputFinishListener.onTextFinish(getEditableText().toString(), mMaxLength);
            }
        }

    }

查看完整的源码

到这里你能大概理解画布的概念了,本文完。

Android-View篇之启动页倒计时动画的实现

Hello,小伙伴们大家好,今天来实现一个很简单的倒计时动画,仿酷狗音乐的启动页倒计时效果,也是大多数APP在用的一个动画,来看看效果图:

倒计时动画

实现思路

看看是不是很简单,画个圈圈动起来,整体的思路就是用一个平滑的帧动画来画圆弧就行了。

这篇文章学到什么?

  • 了解属性动画ValueAnimator的用法
  • 了解动画属性插值Interpolator,让动画过度得更自然
  • 如何画圆弧

开始准备

新建一个类继承TextView,因为中间还有跳过的文本,所以选择用TextView来画个动起来的背景图。

/**
 * 倒计时文本
 * Created by ChenRui on 2017/10/31 0031 23:01.
 */
public class CountDownTextView extends RaeTextView {
    // 倒计时动画时间
    private int duration = 5000;
    // 动画扫过的角度
    private int mSweepAngle = 360;
    // 属性动画
    private ValueAnimator animator;
    // 矩形用来保存位置大小信息
    private final RectF mRect = new RectF();
    // 圆弧的画笔
    private Paint mBackgroundPaint;

    public CountDownTextView(Context context) {
        super(context);
    }

    public CountDownTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CountDownTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    void init() {
        super.init();
        // 设置画笔平滑
        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 设置画笔颜色
        mBackgroundPaint.setColor(Color.WHITE);
        // 设置画笔边框宽度
        mBackgroundPaint.setStrokeWidth(dp2px(2));
        // 设置画笔样式为边框类型
        mBackgroundPaint.setStyle(Paint.Style.STROKE);
    }

开始动画

原理: 利用圆的360度角来做属性动画,让它平滑的分配做每帧动画的角度值,然后调用invalidate() 来重绘自己本身,从而进入到本身的onDraw()方法来画图。

  /**
     * 开始倒计时
     */
    public void start() {
        // 在动画中
        if (mSweepAngle != 360) return;
        //  初始化属性动画
        animator = ValueAnimator.ofInt(mSweepAngle).setDuration(duration);
        // 设置插值
        animator.setInterpolator(new LinearInterpolator());
        // 设置动画监听
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 获取属性动画返回的动画值
                mSweepAngle = (int) animation.getAnimatedValue();
                // 重绘自己
                invalidate();
            }
        });
        // 开始动画
        animator.start();
    }

画圆弧

画圆弧比较简单, 从效果图来看,有的同学可能刚开始以为要画两个圆,一个背景的内圆和一个白色边框的大圆,其实这里可以利用画笔设置画笔样式paint.setStyle()和宽度大小paint.setStrokeWidth()的特性来实现。代码很简单,开始的角度选择-90,从头顶开始画。这样实现的是一个顺时针的倒计时效果。如果你想实现酷狗的逆时针效果,就控制mSweepAngle 的值用mSweepAngle = 360 - mSweepAngle 开始就可以了。

 @Override
    protected void onDraw(Canvas canvas) {
        int padding = dp2px(4);
        mRect.top = padding;
        mRect.left = padding;
        mRect.right = getWidth() - padding;
        mRect.bottom = getHeight() - padding;

        // 画倒计时线内圆
        canvas.drawArc(mRect, //弧线所使用的矩形区域大小
                -90,  //开始角度
                mSweepAngle, //扫过的角度
                false, //是否使用中心
                mBackgroundPaint); // 设置画笔

        super.onDraw(canvas);
    }

什么是插值动画?

为了让动画过度的更加自然或者添加一些动画效果,比如匀速运动、加速运动、减速运动、弹跳运动等等,这些的动画的效果就是靠插值来实现的。在Android中系统内置了一些插值,这里做下搬运工记录一下。推荐一个能在线运行Interpolator的效果以及数学公式定义的网站 http://inloop.github.io/interpolator/ 更加直观的展示下面介绍的动画效果。

插值 说明
LinearInterpolator 以常量速率改变
BounceInterpolator 动画结束的时候弹起
CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线
DecelerateInterpolator 在动画开始的地方快然后慢
OvershootInterpolator 向前甩一定值后再回到原来位置
AccelerateInterpolator 在动画开始的地方速率改变比较慢,然后开始加速
AnticipateInterpolator 开始的时候向后然后向前甩
AccelerateDecelerateInterpolator 在动画开始与介绍的地方速率改变比较慢,在中间的时候加速
AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值

项目使用

这里要定义文本的宽高,因为没有画底部的黑色圆背景,还要设置一下背景图。

  <com.rae.cnblogs.widget.CountDownTextView
            android:id="@+id/tv_skip"
            style="@style/Widget.AppCompat.Button.Borderless"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:background="@drawable/bg_count_down"
            android:text="跳过"
            android:textColor="#ffffff"
            android:textSize="12sp" />

背景图

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="oval">
            <solid android:color="#302d2d2d" />
        </shape>
    </item>
    <item>
        <shape android:shape="oval">
            <solid android:color="#7F2d2d2d" />
        </shape>
    </item>
</selector>

到这里结束啦,希望对你有帮助,本篇文章的源码都在开源的博客园Android客户端这里。

Visual Studio Code Vue.js 开发环境配置

插件安装

以下是必须安装的插件

  • Vetur 语法支持
  • ESLint 语法检查
  • Prettier 代码格式化

代码格式化配置

{
    "editor.fontSize": 16,
    "editor.fontFamily": "Source Code Pro",
    "editor.lineHeight": 36,
    "explorer.confirmDelete": false,
    // Default formatter for <template> region
    "git.enableSmartCommit": true,
    "editor.quickSuggestions": {
        "strings": true
    },
    "git.autofetch": true,
    "workbench.colorTheme": "One Dark Pro",
    "element-helper.version": "2.4",
    "editor.suggestSelection": "first",
    "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
    "explorer.confirmDragAndDrop": false,
    // vscode默认启用了根据文件类型自动设置tabsize的选项
    "editor.detectIndentation": false,
    "files.autoSave": "off",
    // 添加 vue 支持
    // "eslint.run": "onSave",
    //  #去掉代码结尾的分号
    "prettier.semi": false,
    //  #使用带引号替代双引号
    "prettier.singleQuote": true,
    // // #这个按用户自身习惯选择
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    // //  #让函数(名)和后面的括号之间加个空格
    "javascript.format.insertSpaceBeforeFunctionParenthesis": false,
    "typescript.format.insertSpaceBeforeFunctionParenthesis": false,
    "vetur.format.defaultFormatterOptions": {
        "js-beautify-html": {
            "wrap_attributes": "force-aligned"
            // #vue组件中html代码格式化样式
        }
    },
    "[javascript]": {
        "editor.defaultFormatter": "vscode.typescript-language-features"
    },
    "window.zoomLevel": 0,
    "terminal.integrated.fontSize": 16,
    "[vue]": {
        "editor.defaultFormatter": "octref.vetur"
    },
    "editor.codeActionsOnSave": {
        "source.fixAll.eslint": false
    },
    "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[html]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "files.exclude": {
        "**/.classpath": true,
        "**/.project": true,
        "**/.settings": true,
        "**/.factorypath": true
    },
    "workbench.sideBar.location": "left",
    "editor.formatOnSave": true,
    "vetur.format.defaultFormatter.js": "vscode-typescript",
    "eslint.codeActionsOnSave.mode": "problems",
    "eslint.enable": false,
    "eslint.format.enable": true,
    "workbench.iconTheme": "material-icon-theme"
}

把自己的Library库上传到Maven中去

一、在你要上传到maven的库中配置上传参数
代码


uploadArchives {
    apply plugin: 'maven'
    // 读取本地配置文件
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newInputStream())
    def userName = properties.getProperty('maven.user')
    def password = properties.getProperty('maven.password')
    def mavenUrl = properties.getProperty('maven.url')

    repositories.mavenDeployer {
        repository(url: mavenUrl) {
            authentication(userName: userName, password: password)
        }
        pom.project {
            // 注意:这里要修改一下你的库。比如:com.baidu:lib:1.0.0
            groupId 'com.github.raedev'
            artifactId 'session'
            version '1.0.0'
            packaging 'aar'
        }
    }

    task androidSourcesJar(type: Jar) {
        classifier = 'sources'
        from android.sourceSets.main.java.sourceFiles
    }
    artifacts {
        archives androidSourcesJar
    }
}

二、打开local.properties 配置好maven 参数

maven.url=http://maven.baidu.com/repository/maven-baidu/
maven.user=填写你的Maven账号
maven.password=填写你的Maven密码

三、运行uploadArchives任务

运行uploadArchives任务

恭喜!前往你的maven库中去看看是否上传成功了。

Android 金融类项目模块化架构

一、前言

在以往的开发中,我们通常会使用MVC的模式进行开发,这样导致了Activity处理的逻辑非常的复杂,而且耦合度非常高,代码结构混乱、层次不清,各业务技术方案不统一,冗余代码充斥项目的各个角落;甚至连基本的包结构也是胡乱不堪,项目架构更是无从谈起。大家只不过是不停地往上堆砌代码添加新功能罢了。

其中业务层是一种非标准的 MVC 架构,Activity 和 Fragment 承担了 View 和 Controller 的职责:

传统的MVC模式

为了适应项目快速开发以及项目中代码的复用,解决项目中的耦合度。我们不断引入了 Retrofit、UniversalImageLoader、OkHttp、ButterKnife 等一系列成熟的开源库,同时我们也开发了自己的 UI 组件库 UIComponent、基础工具库 CommonUtils、基于第三方地图封装的 MapSDK、即时聊天模块 ChatLibrary 等等。这样就由基础组件层、业务组件层和业务层组成的三层架构。如下图:

MVP模式

前面这种分层的架构本身是没太大问题的,即使到了现在我们的业务项目也已然是基于这种分层的架构来构建的,只不过在不断的迭代中我们做了些许调整(分层架构后面在介绍组件化和模块化的时候会详细介绍)。但是随着业务的不断迭代,我们慢慢发现业务层这种非标准的 MVC 架构带来了种种影响团队开发效率的问题:

Activity 和 Fragment 越来越多的同时承担了 Controller 和 View 的职责,导致他们变得及其臃肿且难以维护;

由于 Controller 和 View 的揉合,导致单元测试起来很困难;

回调嵌套太多,面对负责业务时的代码逻辑不清晰,难以理解且不利于后期维护;

各层次模块之间职责不清晰等等

二、项目整体架构

整体项目架构如下图:

整体项目架构

三、项目分层说明

整体项目分层:

View Layer: 只负责 UI 的绘制呈现,包含 Fragment 和一些自定义的 UI 组件,View 层需要实现 ViewInterface 接口。Activity 在项目中不再负责 View 的职责,仅仅是一个全局的控制者,负责创建 View 和 Presenter 的实例;

Model Layer: 负责检索、存储、操作数据,包括来自网络、数据库、磁盘文件和 SharedPreferences 的数据;

Presenter Layer: 作为 View Layer 和 Module Layer 的之间的纽带,它从 Model 层中获取数据,然后调用 View 的接口去控制 View;

Contract: 我们参照 Google 的 Demo 加入契约类 Contract 来统一管理 View 和 Presenter 的接口,使得某一功能模块的接口能更加直观的呈现出来,这样做是有利于后期维护的。

整体项目架构分为3层:

  • 模块层
  • 业务逻辑层
  • 基础组件层

为什么要这么分?首先从整体模式上看跟微盘的架构大同小异,很多模块都是相同的,可能UI展现跟接口数据不大一样,业务逻辑是差不多的,但是目前很难再从以前的微盘中直接重用。所以分成单独的模块。

第一利于团队多模块开发,第二利于开发速度,只针对单个模块进行编译,编译速度提升了。第三每个模块都可以单独成为APK运行,方便代码调试。第四就是易用性和重用性高。

模块层

对整个APP进行功能拆分,单独成立模块,每一个模块都独立依赖基础组件和业务组件。我们可以把 Basic Component Layer 和 Business Component Layer 放在一起看做是一层SDK,新的业务或者项目只需要依赖 SDK 就好。甚至我们可以做得更极致一些,开发一套自己的组件管理平台,业务方可以根据自己的需求选择自己需要的组件,定制业务专属的SDK。业务端和SDK 的关系如下图所示:

业务端和SDK 的关系

业务逻辑层

封装了与模块层的数据交和UI回调,实际上就相当于Presenter的职责。调用接口以及数据处理都在这一层里面做,最终把结果回调给界面。

另外的职责就是封装常用的公共模块如数据库操作,缓存操作,HTTP请求等。

这一层可以使用RxJava,可以很好的解决嵌套回调的问题。RxJava系列的文章可以参考这里

各 Layer 间严禁反向依赖:每个层要进行依赖,先画好层于层直接的调用关系,禁止相互依赖。有相互依赖的把公共部分单独拆分。

基础组件层

这一层比较好理解,封装基础组件,比如模块化需要用到的Router来连接,并且可以管理Activity的生命周期。还有一些基础UIWidget库,比如股票的图表。

依赖管理

项目间的依赖通过私有maven库进行管理,特别是sdk跟presenter这一层,强制把UI跟逻辑分离。使用maven的一个弊端就是需要频繁的上传跟重新build。前期可以在项目用complie project(‘:sdk’) 来依赖。

API 接口层

API接口作为核心的一层,每个模块都需要调用该层,我们采用分功能来设计接口,并提供统一的接口工厂来获取接口的实例。UML图如下:

接口层编码的时候要注意:

全站使用HTTPS证书

如何避免回调Listener的内存泄漏?(原因:回调都是通过onSuccess()方法去处理,很容易引用到Context,而导致Http线程没办法退出)

封装成基础类去发请求,可以控制token失效重新发请求的过程。

采用Retrofit2 +okhttp3

接口缓存策略(参考Volley的缓存):

如果缓存中存在,先从缓存读取。

每一个请求都有缓存时间,缓存过期或者失效后重新获取。

可以配置每个请求启用或者禁用缓存,比如一些增删改查操作就不需要用到缓存。列表的形式一般要缓存。

缓存默认是关闭的,根据需要来给请求缓存。

Model

一般情况下我们的实体层(entity、bean、model)这些都是跟sdk处于一层的,为了避免每个模块为了使用实体层而引用sdk,所以要把这个实体层单独一层出来,避免相互之间有反向依赖的可能性。

除了常用的实体层之外,Model层还具有负责检索、存储、操作数据,包括来自网络、数据库、磁盘文件和 SharedPreferences 的数据的功能,只是都归根到Model这一块来。实际上他们都是单独开来的。

UIWidget

View划分成若干小模块,不单单可以使用当前项目,更为了以后方便集成到其他项目当中去。

UIWidget

Router 路由管理

组件化和模块化使得程序更加灵活,为了避免在app对各个模块以及组件的依赖当组件发生改变的时候代码修改很大。所以由RouterManger去统一管理各个模块组件之间的跳转。

实现方式可以采用ARouter,支持Url方式的跳转。

APP路由管理

四、项目安全说明

接口安全

接口使用HTTPS加密证书进行传输,并进行用户鉴权,用户鉴权方面则打算采用Token方式。用户登录之后分配一个accessToken和一个refreshToken,accessToken用于发起用户请求,refreshToken用于更新accessToken。accessToken会设置有效期,可以设为24小时。而用户退出登录之后,accessToken和refreshToken都将作废。重新登录之后会分配新的accessToken和refreshToken。

然后,我还打算在App层级分配AppKey和AppSecret,Android和iOS分别分配一对。每次向服务端发送请求时,AppKey都必须带上,服务端会对相应的AppKey进行校验。而AppSecret则需要安全保存在客户端,也不能在网络上进行传输,防止泄露。AppSecret只用于加密一些安全性级别较高的数据,以及为URL生成签名。URL签名算法步骤如下:

将所有参数按参数名进行升序排序;

将排序后的参数名和值拼接成字符串stringParams,格式:key1value1key2value2…;

在上一步的字符串前面拼接上请求URI的Endpoint,字符串后面拼接上AppSecret,即:stringURI + stringParams + AppSecret;

使用AppSecret为密钥,对上一步的结果字符串使用HMAC算法计算MAC值,这个MAC值就是签名。

鉴权流程如下:

接口鉴权流程

最后总结设计为:

接口采用HTTPS和签名证书进行传输

接口参数通过排序 & URL & AppSecret 最终通过HMAC加密生成sign参数

利用底层JNI的so库提供接口进行加密,保证密钥的安全性

敏感字段so加密传输,比如设计密码、银行卡号、身份证号码等有关用户安全信息的字段

数据安全利用底层JNI实现,打包成so库提供JAVA接口调用。把公钥信息、加密算法放到so库里面防止反编译信息泄漏。

APK安全

android的apk文件实际上是压缩文件,很容易被反编译工具进行反编译,像微信这些apk都能被反编译。为了加强APK的安全性,设计如下:

发布时对代码进行混淆编译

第三方APK文件加固,加固网站:

五、模块说明

行情模块(Quotation Module)

行情使用WSS安全传输。

行情以后台服务的形式,在Application onCreate() 的时候就开始建立WebSocket连接。没有订阅行情的时候关闭行情连接,当订阅的时候再次开启行情连接。目的为了节省流量以及手机电量。

因为行情基本上每个页面都会用到,所以采用发布订阅的观察者模式进行设计,实现原理:

每个Presenter都可以注册自己需要的行情(Quotation Filter),当WebSocket 接收到对应的Quotation Filter的时候调用EventBus中间件发送消息。最终会回调到该类的定义的事件方法中去。

把数据处理好前端需要展示的数据实体返回。如行情的状态是上涨还是下跌的状态。

统一使用QuotationManager来管理行情的连接、订阅;抽象成QuotationAction 来管理注册不同的行消息,解析返回。

行情服务接口方法:

  • start() 启动服务
  • stop() 停止服务
  • getStatus() 获取当前状态
  • register(object handler, QuotationAction action) 订阅行情
  • unregister(); 反注册行情

行情模块

用户模块(User Module)

依赖项:API

用户模块

说明:

User Manager :用户管理,管理当前用户的登录状态、用户信息、用户操作(退出登录,状态维持,Token刷新)

User Module:用户模块,跟用户相关的各个功能的业务处理、数据处理

交易模块(Trade Module)

依赖项:API、行情模块

其他模块需要调用交易模块都是通过路由跳转的方式调用不会直接调用到内部方法里面,所以交易模块重点还是调用API进行数据处理:

交易模块

本文参考链接:

JS 数组组合算法

先来看看要实现什么样的效果

var data = [[1,2,3],[4,5,6]];

var result =  data.group();

// 结果:[1,4] [1,5], [1,6],[2,4],[2,5],[2,6]....以此类推;

【源码】扩展Array方法:

Array.prototype.group = function() {
    var $this = this;
    var result = new Array();
    var findNext = function next(currentIndex, arr) {
        var item = $this[currentIndex];
        var tempArr = arr;
        var index = currentIndex;
        for (var k = 0; k &lt; item.length; k++) {
            if (!arr) tempArr = new Array();
            tempArr[tempArr.length] = item[k];
            if (tempArr.length == $this.length) {
                result[result.length] = tempArr;
                tempArr = arr.slice();
                tempArr.length = tempArr.length - 1;
                continue;
            }
            index++;
            if (index &lt; $this.length &amp;&amp; next(index, tempArr)) {
                index--; // 恢复递归之前的索引
                tempArr = arr ? arr.slice() : new Array();
                tempArr.length = tempArr.length &gt; 0 ? tempArr.length - ($this.length - index) : 0;
            }
        }
        return true;
    };

    findNext(0);

    return result;
}

Android Studio APK输出文件配置

android{
  applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            if (outputFile != null && outputFile.name.endsWith('.apk')) {
                def dirName = APP_OUTPUT_DIR + "v${defaultConfig.versionName}"
                def fileName = outputFile.name.replace(".apk", "-${defaultConfig.versionName}.apk")
                output.outputFile = new File(dirName, fileName)
            }
        }
    }

APP_OUTPUT_DIRgradle.properties,如:

#小盘beta版 输出路径
APP_OUTPUT_DIR = F\:\\100bei\\xiaopan\\dev\\

Android Studio 多个编译环境配置 多渠道打包 APK输出配置

看完这篇你学到什么:

  • 熟悉gradle的构建配置
  • 熟悉代码构建环境的目录结构,你知道的不仅仅是只有src/main
  • 开发、生成环境等等环境可以任意切换打包
  • 多渠道打包
  • APK输出文件配置

需求

一般我们开发的环境分为:debug 和 release,但是你想再分内测1环境、内测2环境等等怎么办呢?

这就需要依赖强大的gradle 来配置了。

相关的配置也可以参考谷歌官方文档

配置构建类型 buildTypes

您可以在模块级 build.gradle 文件的 android {} 代码块内部创建和配置构建类型。当您创建新模块时,Android Studio 会自动为您创建调试和发布这两种构建类型。尽管调试构建类型不会出现在构建配置文件中,Android Studio 会将其配置为 debuggable true。这样,您可以在安全的 Android 设备上调试应用并使用通用调试密钥库配置 APK 签署。

如果您希望添加或更改特定设置,您可以将调试构建类型添加到您的配置中。以下示例为调试构建类型指定了 applicationIdSuffix,并配置了一个使用调试构建类型中的设置进行初始化的jnidebug构建类型。

applicationIdSuffix: 字段表示,在不改变你默认的程序ID(包名)的情况下,为其添加后缀。比如你的包名是com.rae.app,但你想区分测试包和正式包的情况,这个时候将applicationIdSuffix设置为.debug,那么你的应用程序对应的包名就变成了com.rae.app.debug

android {
    ...
    defaultConfig {...}
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            applicationIdSuffix ".debug"
        }

        /**
         * The 'initWith' property allows you to copy configurations from other build types,
         * so you don't have to configure one from the beginning. You can then configure
         * just the settings you want to change. The following line initializes
         * 'jnidebug' using the debug build type, and changes only the
         * applicationIdSuffix and versionNameSuffix settings.
         */

        jnidebug {

            // This copies the debuggable attribute and debug signing configurations.
            initWith debug

            applicationIdSuffix ".jnidebug"
            jniDebuggable true
        }
    }
}

构建源集

我们都知道,源代码是放在src/main 文件夹下的,但是由于配置了不同的构建类型像想要区分不同的源文件怎么办呢?这个时候就可以在src对应你的buildTypes来建立文件夹了 ,更多参考谷歌源集

Android Studio 按逻辑关系将每个模块的源代码和资源分组为源集。模块的 main/ 源集包括其所有构建变体共用的代码和资源。其他源集目录为可选项,在您配置新的构建变体时,Android Studio 不会自动为您创建这些目录。不过,创建类似于 main/ 的源集有助于让 Gradle 只应在构建特定应用版本时使用的文件和资源井然有序:

productFlavor 表示渠道包,可以看下面的多渠道打包

src/main/

此源集包括所有构建变体共用的代码和资源。

src/<buildType>/

创建此源集可加入特定构建类型专用的代码和资源。示例:src/jnidebug

src/<productFlavor>/

创建此源集可加入特定产品风味专用的代码和资源。比如百度渠道包:src/baidu

src/<productFlavorBuildType>/

创建此源集可加入特定构建变体专用的代码和资源。
例如,要生成应用的“完全调试”版本,构建系统需要合并来自以下源集的代码、设置和资源。比如:百度的开发环境包:src/baiduDebug

构建类型依赖配置

很多时候我们会把sdk或者api接口单独做成一个库,一般会有生产环境和测试环境之分,但在依赖的时候往往我们会像这样去引用: compile project(':sdk'),这样依赖的环境就是release,在开发调试的时候测试环境的时候就不行了。我们得换另外一种方式:

<buildType>Compile project()

这样会根据不同的构建类型去依赖不同的包,比如我们测试环境的依赖包:debugCompile project(':sdk'),再比如上面的jnidebugjnidebugCompile project(':sdk')

那么问题来了,我当前的构建类型怎么对应到其他的module去呢?比如你的app要依赖sdk module 的debug 环境,那么你可以这么做:

configuration:目标module<buildType>,比如你sdk 中<buildType>debug构建类型

debugCompile project(path: ':sdk', configuration: 'debug')

综合示例:

1、先看app这边的build.gradle配置:

apply plugin: 'com.android.application'

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            applicationIdSuffix '.debug'
            minifyEnabled false
        }

        // 自定义的构建类型,名字随便取,一定要有意义
        raedebug {
            initWith debug
            applicationIdSuffix '.raedebug'
        }
    }
}

dependencies {
    // 生成环境依赖
    releaseCompile project(path: ':sdk', configuration: 'release')
    // 测试环境依赖
    debugCompile project(path: ':sdk', configuration: 'debug')
    // 自定义构建类型依赖
    raedebugCompile project(path: ':sdk', configuration: 'uutest')
}

2、sdk module的build.gradle配置:

apply plugin: 'com.android.library'

android {
       buildTypes {
        debug {
            debuggable true
            minifyEnabled false
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        // 注意这里,跟第一点的 raedebugCompile project的configuration要匹配。
        uutest {
            initWith debug
        }
    }
}

多渠道打包 productFlavors

先看看build.gradle配置你就懂了

android{

    // 渠道包定义,默认定义的名称就是渠道名称
    productFlavors {

        dev {} // 测试
        baidu {}        // 百度手机助手
        yinyongbao {}   // 应用宝
        m360 {}         // 360手机助手
        pp {}           // PP助手
        anzhi{}         // 安智市场
        xiaomi {}       // 小米商店
        letv {}         // 乐视商店
        huawei {}       // 华为商店
        lenovomm {}     // 联想乐商店
        other {}        // 其他市场
        official{}      // 官方版本

    }

    // 批量渠道包值替换
    productFlavors.all { flavor ->
        // 友盟、极光推送渠道包, UMENG_CHANNEL 是根据你AndroidManifest.xml来配置的,请看下面。
        flavor.manifestPlaceholders = [UMENG_CHANNEL: name, JPUSH_CHANNEL: name]
    }
}

AndroidManifest.xml 配置:


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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

       <!--变量采用${变量名}这样来替换,不仅限与<meta-data /> 标签,任何你想替换的都行。-->
         <meta-data
            android:name="UMENG_APPKEY"
            android:value="${UMENG_APPKEY}"/>

        <meta-data
            android:name="UMENG_CHANNEL"
            android:value="${UMENG_CHANNEL}"/>

        <!--${变量随变换}-->   
        <activity
            android:name=".DemoActivity"
            android:label="${变量随变换}"/>

    </application>

</manifest>

sync gradle之后看看gradle projects 面板列表就多出了好到渠道的任务了,Build Variants 面板也相对应多了这些构建类型。

APK输出配置

在结合到多渠道打包后,运营的那边希望我们给的渠道包是这种格式的app-{版本号}-{渠道名称}.apk,那我们来看看怎么来满足这个多渠道打包输出apk文件名修改的。

android{

    // 输出文件配置
   applicationVariants.all { variant ->
        variant.outputs.each { output ->
            def outputFile = output.outputFile
            if (outputFile != null && outputFile.name.endsWith('.apk')) {
                def dirName = outputFile.parent // 输出文件夹所在的位置

                // 文件名修改
                def fileName = "app-${output.processResources.variantName}-${defaultConfig.versionName}-${variant.flavorName}.apk"

                // 比如不想这么麻烦,直接在后面加上版本号也行:
                // def fileName = outputFile.name.replace(".apk", "-${defaultConfig.versionName}.apk")

                output.outputFile = new File(dirName, fileName)
            }
        }
    }
}