ソースを参照

完善音频维护

yangchong 3 年 前
コミット
d2f56cc52f
85 ファイル変更3952 行追加2840 行削除
  1. 0 117
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/FloatPlayerView.java
  2. 0 82
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/HomeKeyWatcher.java
  3. 0 179
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/OldActivity.java
  4. 0 112
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestClarityActivity.java
  5. 0 110
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestEightVideoActivity.java
  6. 0 69
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestFragment.java
  7. 0 52
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestFragmentActivity.java
  8. 0 143
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestFullActivity2.java
  9. 0 67
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestListFragment.java
  10. 0 215
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestNormalActivity.java
  11. 0 79
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestRecyclerActivity.java
  12. 0 214
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestSavePosActivity.java
  13. 0 121
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestTinyActivity.java
  14. 0 147
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestWindowActivity.java
  15. 0 28
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/Video.java
  16. 0 86
      Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/VideoAdapter.java
  17. 0 12
      Demo/src/main/res/layout/item_my_video.xml
  18. 152 0
      ReadDesign/01.设计模式导读/02.行为型模式.md
  19. 81 0
      ReadDesign/01.设计模式导读/03.创建型模式.md
  20. 80 0
      ReadDesign/01.设计模式导读/04.结构型模式.md
  21. 49 0
      ReadDesign/01.设计模式导读/05.认识设计模式.md
  22. 101 0
      ReadDesign/01.设计模式导读/06.代码质量评判.md
  23. 883 0
      ReadDesign/02.设计模式原则/01.面向对象六大原则.md
  24. 167 0
      ReadDesign/02.设计模式原则/02.单一职责原则详解.md
  25. 343 0
      ReadDesign/02.设计模式原则/03.开闭原则详细介绍.md
  26. 137 0
      ReadDesign/02.设计模式原则/04.里式替换原则介绍.md
  27. 375 0
      ReadDesign/02.设计模式原则/05.接口隔离原则介绍.md
  28. 198 0
      ReadDesign/02.设计模式原则/06.依赖倒置原则介绍.md
  29. 260 0
      ReadDesign/02.设计模式原则/07.迪米特原则介绍.md
  30. 186 0
      ReadDesign/02.设计模式原则/08.KISS和YAGNI原则.md
  31. 281 0
      ReadDesign/02.设计模式原则/09.DRY原则简单介绍.md
  32. 47 0
      ReadDesign/03.重构之路介绍/01.重构简单介绍.md
  33. 168 0
      ReadDesign/03.重构之路介绍/02.重构技术手段.md
  34. 76 0
      ReadDesign/03.重构之路介绍/07.避免过度设计.md
  35. 0 0
      ReadMeWiki/00.方案实践/01.视频播放器封装思路.md
  36. 0 0
      ReadMeWiki/00.方案实践/02.播放器内核切换封装.md
  37. 0 0
      ReadMeWiki/00.方案实践/03.播放器UI抽取封装.md
  38. 0 0
      ReadMeWiki/00.方案实践/04.视频播放器整体结构.md
  39. 0 0
      ReadMeWiki/00.方案实践/05.视频全局悬浮窗播放.md
  40. 0 0
      ReadMeWiki/00.方案实践/06.视频边播边缓存分析.md
  41. 0 0
      ReadMeWiki/00.方案实践/07.视频播放器列表播放.md
  42. 0 0
      ReadMeWiki/00.方案实践/08.视频播放器埋点监听.md
  43. 0 0
      ReadMeWiki/00.方案实践/09.视频播放器使用设计模式.md
  44. 0 0
      ReadMeWiki/00.方案实践/10.视频加密和解密处理.md
  45. 0 0
      ReadMeWiki/00.方案实践/11.视频录制和编辑实践.md
  46. 0 0
      ReadMeWiki/00.方案实践/12.TTS音频播放实践.md
  47. 0 0
      ReadMeWiki/00.方案实践/40.完整音频播放器分析.md
  48. 0 0
      ReadMeWiki/00.方案实践/42.音视频本地文件扫描.md
  49. 0 0
      ReadMeWiki/00.方案实践/45.音视频加密和解密.md
  50. 0 0
      ReadMeWiki/00.方案实践/62.视频内存优化治理.md
  51. 0 0
      ReadMeWiki/01.原理知识/01.视频播放器如何选择.md
  52. 0 0
      ReadMeWiki/01.原理知识/02.视频播放器简单案例.md
  53. 0 0
      ReadMeWiki/01.原理知识/03.视频基础概念术语.md
  54. 0 0
      ReadMeWiki/01.原理知识/04.视频VideoView学习.md
  55. 0 0
      ReadMeWiki/01.原理知识/05.视频播放器流程分析.md
  56. 0 0
      ReadMeWiki/01.原理知识/07.MediaPlayer详细介绍.md
  57. 368 20
      ReadMeWiki/01.原理知识/10.SurfaceView深入学习.md
  58. 0 0
      ReadMeWiki/01.原理知识/11.TextureView深入学习.md
  59. 0 0
      ReadMeWiki/01.原理知识/20.视频编码和解码学习.md
  60. 0 0
      ReadMeWiki/01.原理知识/32.音频焦点抢占问题.md
  61. 0 0
      ReadMeWiki/01.原理知识/43.音频基础知识点.md
  62. 0 0
      ReadMeWiki/02.如何使用/01.视频播放器介绍文档.md
  63. 0 0
      ReadMeWiki/02.如何使用/03.视频播放器Api说明.md
  64. 0 0
      ReadMeWiki/02.如何使用/49.参考项目和博客说明.md
  65. 0 0
      ReadMeWiki/02.如何使用/50.版本更新说明文档.md
  66. 0 0
      ReadMeWiki/03.优化实践/08.视频播放器优化处理.md
  67. 0 0
      ReadMeWiki/03.优化实践/09.视频深度优化处理.md
  68. 0 0
      ReadMeWiki/03.优化实践/11.视频播放器音频焦点抢占.md
  69. 0 0
      ReadMeWiki/03.优化实践/17.基础播放器问题记录.md
  70. 0 0
      ReadMeWiki/03.优化实践/34.音频播放锁屏分析.md
  71. 0 0
      ReadMeWiki/03.优化实践/35.耳机控制音视频音量.md
  72. 0 0
      ReadMeWiki/04.直播视频/51.直播基础知识点介绍.md
  73. 0 0
      ReadMeWiki/04.直播视频/52.直播推流端分析.md
  74. 0 0
      ReadMeWiki/04.直播视频/53.直播播放端分析.md
  75. 0 9
      ReadMeWiki/18.Exo播放器问题记录.md
  76. 0 3
      ReadMeWiki/19.Ijk播放器问题记录.md
  77. 0 3
      ReadMeWiki/20.视频播放器版本更新文档.md
  78. 0 7
      ReadMeWiki/21.视频播放器后期需求.md
  79. 0 376
      ReadMeWiki/24.1SurfaceView深入学习.md
  80. 0 21
      ReadMeWiki/31.音频播放器通用框架.md
  81. 0 108
      ReadMeWiki/43.音视频编解码操作.md
  82. 0 171
      ReadMeWiki/48.音视频问题考点.md
  83. 0 21
      ReadMeWiki/61.如何打造全局悬浮窗.md
  84. 0 116
      ReadMeWiki/63.获取视频对象大小.md
  85. 0 152
      VideoView/wiki/01.全局悬浮窗实践.md

+ 0 - 117
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/FloatPlayerView.java

@@ -1,117 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import com.yc.videotool.VideoLogUtils;
-import com.yc.video.config.ConstantKeys;
-import com.yc.video.old.controller.VideoPlayerController;
-import com.yc.video.old.listener.OnPlayerStatesListener;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-import com.yc.videoview.SmallWindowTouch;
-
-
-/**
- * <pre>
- *     @author yangchong
- *     blog  : https://github.com/yangchong211
- *     time  : 2018/8/29
- *     desc  : 适配了悬浮窗的view
- *     revise:
- * </pre>
- */
-public class FloatPlayerView extends FrameLayout {
-
-    private OldVideoPlayer mVideoPlayer;
-
-    public FloatPlayerView(Context context) {
-        super(context);
-        init();
-    }
-
-    public FloatPlayerView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init();
-    }
-
-    public FloatPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        init();
-    }
-
-    private void init() {
-        LayoutInflater inflater = (LayoutInflater) this.getContext()
-                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        View view ;
-        if (inflater != null) {
-            view = inflater.inflate(com.yc.video.R.layout.old_view_window_dialog, this);
-            mVideoPlayer = view.findViewById(com.yc.video.R.id.video_player);
-            mVideoPlayer.setUp(path,null);
-            mVideoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-            //创建视频控制器
-            VideoPlayerController controller = new VideoPlayerController(getContext());
-            controller.setTopVisibility(false);
-            controller.setLoadingType(ConstantKeys.Loading.LOADING_QQ);
-            controller.imageView().setBackgroundColor(Color.BLACK);
-            controller.setOnPlayerStatesListener(new OnPlayerStatesListener() {
-                @Override
-                public void onPlayerStates(int states) {
-                    if (states == ConstantKeys.PlayerStatesType.COMPLETED){
-                        VideoPlayerManager.instance().releaseVideoPlayer();
-                        if(mCompletedListener!=null){
-                            mCompletedListener.Completed();
-                        }
-                    }
-                }
-            });
-            //controller.onPlayModeChanged(ConstantKeys.PlayMode.MODE_TINY_WINDOW);
-            mVideoPlayer.setController(controller);
-            //mVideoPlayer.enterTinyWindow();
-            mVideoPlayer.postDelayed(new Runnable() {
-                @Override
-                public void run() {
-                    mVideoPlayer.start();
-                }
-            },300);
-            view.setOnClickListener(new OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    VideoLogUtils.d("点击事件"+mVideoPlayer.getCurrentState());
-                    if(mVideoPlayer.isPlaying()){
-                        mVideoPlayer.pause();
-                    }else if(mVideoPlayer.isPaused()){
-                        mVideoPlayer.restart();
-                    }
-                    VideoLogUtils.d("点击事件"+mVideoPlayer.getCurrentState());
-                }
-            });
-            view.setOnTouchListener(new SmallWindowTouch(view,0,0));
-        }
-    }
-
-    private static String path;
-    public static void setUrl(String url) {
-        path = url;
-    }
-
-    public interface CompletedListener{
-        /**
-         * 播放完成
-         */
-        void Completed();
-    }
-
-    /**
-     * 监听视频播放完成事件
-     */
-    private CompletedListener mCompletedListener;
-    public void setCompletedListener(CompletedListener listener){
-        this.mCompletedListener = listener;
-    }
-
-}

+ 0 - 82
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/HomeKeyWatcher.java

@@ -1,82 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-
-/**
- * 监听Home键按下的Wathcer
- */
-public class HomeKeyWatcher {
-
-    private Context mContext;
-    private IntentFilter mFilter;
-    private OnHomePressedListener mListener;
-    private HomeReceiver mReceiver;
-
-    public interface OnHomePressedListener {
-        //短按Home键
-        void onHomePressed();
-        //长按Home键
-        //void onHomeLongPressed();
-    }
-
-    public HomeKeyWatcher(Context context) {
-        mContext = context;
-        mFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
-    }
-
-    /**
-     * 设置监听
-     * @param listener
-     */
-    public void setOnHomePressedListener(OnHomePressedListener listener) {
-        mListener = listener;
-        mReceiver = new HomeReceiver();
-    }
-
-    /**
-     * 开始监听,注册广播
-     */
-    public void startWatch() {
-        if (mReceiver != null) {
-            mContext.registerReceiver(mReceiver, mFilter);
-        }
-    }
-
-    /**
-     * 停止监听,注销广播
-     */
-    public void stopWatch() {
-        if (mReceiver != null) {
-            mContext.unregisterReceiver(mReceiver);
-        }
-    }
-
-    class HomeReceiver extends BroadcastReceiver {
-
-        final String SYSTEM_DIALOG_REASON_KEY = "reason";
-        final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
-        final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (action!=null && action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
-                String reason = intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY);
-                if (reason != null) {
-                    if (mListener != null) {
-                        if (reason.equals(SYSTEM_DIALOG_REASON_HOME_KEY)) {
-                            // 短按home键
-                            mListener.onHomePressed();
-                        } else if (reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) {
-                            // 长按home键
-                            //mListener.onHomeLongPressed();
-                        }
-                    }
-                }
-            }
-        }
-    }
-}

+ 0 - 179
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/OldActivity.java

@@ -1,179 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.content.Intent;
-import android.graphics.Color;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
-
-import com.yc.ycvideoplayer.newPlayer.list.TestListActivity;
-import com.yc.ycvideoplayer.newPlayer.surface.TestSurfaceActivity;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.old.other.VideoPlayerManager;
-
-import cn.ycbjie.ycstatusbarlib.bar.StateAppBar;
-
-/**
- * ================================================
- * 作    者:杨充
- * 版    本:1.0
- * 创建日期:2017/11/18
- * 描    述:Main主页面
- * 修订历史:
- * ================================================
- */
-@Deprecated
-public class OldActivity extends AppCompatActivity implements View.OnClickListener {
-
-    private long exitTime;
-    private Toolbar mToolbar;
-    private TextView mTv0;
-    private TextView mTv1;
-    private TextView mTv2;
-    private TextView mTv3;
-    private TextView mTv4;
-    private TextView mTv5;
-    private TextView mTv6;
-    private TextView mTv7;
-    private TextView mTv8;
-    private TextView mTv9;
-    private TextView mTv10;
-    private TextView mTv11;
-    private TextView mTv12;
-    private TextView mTv13;
-    private TextView mTv14;
-    private TextView mTv15;
-    private TextView mTv16;
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_old);
-        StateAppBar.setStatusBarLightMode(this, Color.WHITE);
-        initView();
-    }
-
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()) {
-            return;
-        }
-        super.onBackPressed();
-    }
-
-
-    private void initView() {
-        mToolbar = (Toolbar) findViewById(R.id.toolbar);
-        mTv0 = (TextView) findViewById(R.id.tv_0);
-        mTv1 = (TextView) findViewById(R.id.tv_1);
-        mTv2 = (TextView) findViewById(R.id.tv_2);
-        mTv3 = (TextView) findViewById(R.id.tv_3);
-        mTv4 = (TextView) findViewById(R.id.tv_4);
-        mTv5 = (TextView) findViewById(R.id.tv_5);
-        mTv6 = (TextView) findViewById(R.id.tv_6);
-        mTv7 = (TextView) findViewById(R.id.tv_7);
-        mTv8 = (TextView) findViewById(R.id.tv_8);
-        mTv9 = (TextView) findViewById(R.id.tv_9);
-        mTv10 = (TextView) findViewById(R.id.tv_10);
-        mTv11 = (TextView) findViewById(R.id.tv_11);
-        mTv12 = (TextView) findViewById(R.id.tv_12);
-        mTv13 = (TextView) findViewById(R.id.tv_13);
-        mTv14 = (TextView) findViewById(R.id.tv_14);
-        mTv15 = (TextView) findViewById(R.id.tv_15);
-        mTv16 = (TextView) findViewById(R.id.tv_16);
-
-        mTv0.setOnClickListener(this);
-        mTv1.setOnClickListener(this);
-        mTv2.setOnClickListener(this);
-        mTv3.setOnClickListener(this);
-        mTv4.setOnClickListener(this);
-        mTv5.setOnClickListener(this);
-        mTv6.setOnClickListener(this);
-        mTv7.setOnClickListener(this);
-        mTv8.setOnClickListener(this);
-        mTv9.setOnClickListener(this);
-        mTv10.setOnClickListener(this);
-        mTv11.setOnClickListener(this);
-        mTv12.setOnClickListener(this);
-        mTv13.setOnClickListener(this);
-        mTv14.setOnClickListener(this);
-        mTv15.setOnClickListener(this);
-        mTv16.setOnClickListener(this);
-    }
-
-    @Override
-    public void onClick(View v) {
-        switch (v.getId()) {
-            case R.id.tv_0:
-                break;
-            case R.id.tv_1:
-                startActivity(TestNormalActivity.class);
-                break;
-            case R.id.tv_2:
-                startActivity(TestFullActivity2.class);
-                break;
-            case R.id.tv_3:
-                startActivity(TestTinyActivity.class);
-                break;
-            case R.id.tv_4:
-                startActivity(TestWindowActivity.class);
-                break;
-            case R.id.tv_5:
-                startActivity(TestClarityActivity.class);
-                break;
-            case R.id.tv_6:
-                startActivity(TestFragmentActivity.class);
-                break;
-            case R.id.tv_7:
-                startActivity(TestRecyclerActivity.class);
-                break;
-            case R.id.tv_8:
-                startActivity(TestEightVideoActivity.class);
-                break;
-            case R.id.tv_9:
-                startActivity(TestListActivity.class);
-                break;
-            case R.id.tv_10:
-
-                break;
-            case R.id.tv_11:
-                startActivity(TestWindowActivity.class);
-                break;
-            case R.id.tv_12:
-                startActivity(TestSavePosActivity.class);
-                break;
-            case R.id.tv_13:
-                startActivity(TestSurfaceActivity.class);
-                break;
-            case R.id.tv_14:
-                break;
-            case R.id.tv_15:
-                break;
-            case R.id.tv_16:
-                break;
-            default:
-                break;
-        }
-    }
-
-    private void startActivity(Class c){
-        startActivity(new Intent(this,c));
-    }
-}

+ 0 - 112
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestClarityActivity.java

@@ -1,112 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.view.View;
-import android.widget.Button;
-
-import com.bumptech.glide.Glide;
-import com.yc.ycvideoplayer.BaseActivity;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.config.VideoInfoBean;
-import com.yc.video.config.ConstantKeys;
-import com.yc.video.old.controller.VideoPlayerController;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import cn.ycbjie.ycstatusbarlib.bar.StateAppBar;
-
-
-/**
- * @author yc
- */
-public class TestClarityActivity extends BaseActivity implements View.OnClickListener {
-
-
-
-    private OldVideoPlayer videoPlayer;
-    private Button mBtnTiny1;
-    private Button mBtnTiny2;
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()) return;
-        super.onBackPressed();
-    }
-
-
-    @Override
-    public int getContentView() {
-        return R.layout.activity_test_video3;
-    }
-
-    @Override
-    public void initView() {
-        StateAppBar.translucentStatusBar(this, true);
-        videoPlayer = (OldVideoPlayer) findViewById(R.id.nice_video_player);
-        mBtnTiny1 = (Button) findViewById(R.id.btn_tiny_1);
-        mBtnTiny2 = (Button) findViewById(R.id.btn_tiny_2);
-
-
-        videoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-        VideoPlayerController controller = new VideoPlayerController(this);
-        controller.setTitle("Beautiful China...");
-        controller.setLength(117000);
-        controller.setClarity(getClarites(), 0);
-        Glide.with(this)
-                .load("http://imgsrc.baidu.com/image/c0%3Dshijue%2C0%2C0%2C245%2C40/sign=304dee3ab299a9012f38537575fc600e/91529822720e0cf3f8b77cd50046f21fbe09aa5f.jpg")
-                .placeholder(R.drawable.image_default)
-                .into(controller.imageView());
-        controller.setTopPadding(24);
-        videoPlayer.continueFromLastPosition(false);
-        videoPlayer.setController(controller);
-    }
-
-    @Override
-    public void initListener() {
-        mBtnTiny1.setText("1. 全屏播放时可以切换清晰度");
-        mBtnTiny2.setText("2. 全屏播放");
-        mBtnTiny1.setOnClickListener(this);
-        mBtnTiny2.setOnClickListener(this);
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-
-    public List<VideoInfoBean> getClarites() {
-        List<VideoInfoBean> clarities = new ArrayList<>();
-        clarities.add(new VideoInfoBean("标题哈哈哈哈","标清", "270P", "http://play.g3proxy.lecloud.com/vod/v2/MjUxLzE2LzgvbGV0di11dHMvMTQvdmVyXzAwXzIyLTExMDc2NDEzODctYXZjLTE5OTgxOS1hYWMtNDgwMDAtNTI2MTEwLTE3MDg3NjEzLWY1OGY2YzM1NjkwZTA2ZGFmYjg2MTVlYzc5MjEyZjU4LTE0OTg1NTc2ODY4MjMubXA0?b=259&mmsid=65565355&tm=1499247143&key=f0eadb4f30c404d49ff8ebad673d3742&platid=3&splatid=345&playid=0&tss=no&vtype=21&cvid=2026135183914&payff=0&pip=08cc52f8b09acd3eff8bf31688ddeced&format=0&sign=mb&dname=mobile&expect=1&tag=mobile&xformat=super"));
-        clarities.add(new VideoInfoBean("标题哈哈哈哈","高清", "480P", "http://play.g3proxy.lecloud.com/vod/v2/MjQ5LzM3LzIwL2xldHYtdXRzLzE0L3Zlcl8wMF8yMi0xMTA3NjQxMzkwLWF2Yy00MTk4MTAtYWFjLTQ4MDAwLTUyNjExMC0zMTU1NTY1Mi00ZmJjYzFkNzA1NWMyNDc4MDc5OTYxODg1N2RjNzEwMi0xNDk4NTU3OTYxNzQ4Lm1wNA==?b=479&mmsid=65565355&tm=1499247143&key=98c7e781f1145aba07cb0d6ec06f6c12&platid=3&splatid=345&playid=0&tss=no&vtype=13&cvid=2026135183914&payff=0&pip=08cc52f8b09acd3eff8bf31688ddeced&format=0&sign=mb&dname=mobile&expect=1&tag=mobile&xformat=super"));
-        clarities.add(new VideoInfoBean("标题哈哈哈哈","超清", "720P", "http://play.g3proxy.lecloud.com/vod/v2/MjQ5LzM3LzIwL2xldHYtdXRzLzE0L3Zlcl8wMF8yMi0xMTA3NjQxMzkwLWF2Yy00MTk4MTAtYWFjLTQ4MDAwLTUyNjExMC0zMTU1NTY1Mi00ZmJjYzFkNzA1NWMyNDc4MDc5OTYxODg1N2RjNzEwMi0xNDk4NTU3OTYxNzQ4Lm1wNA==?b=479&mmsid=65565355&tm=1499247143&key=98c7e781f1145aba07cb0d6ec06f6c12&platid=3&splatid=345&playid=0&tss=no&vtype=13&cvid=2026135183914&payff=0&pip=08cc52f8b09acd3eff8bf31688ddeced&format=0&sign=mb&dname=mobile&expect=1&tag=mobile&xformat=super"));
-        clarities.add(new VideoInfoBean("标题哈哈哈哈","蓝光", "1080P", "http://play.g3proxy.lecloud.com/vod/v2/MjQ5LzM3LzIwL2xldHYtdXRzLzE0L3Zlcl8wMF8yMi0xMTA3NjQxMzkwLWF2Yy00MTk4MTAtYWFjLTQ4MDAwLTUyNjExMC0zMTU1NTY1Mi00ZmJjYzFkNzA1NWMyNDc4MDc5OTYxODg1N2RjNzEwMi0xNDk4NTU3OTYxNzQ4Lm1wNA==?b=479&mmsid=65565355&tm=1499247143&key=98c7e781f1145aba07cb0d6ec06f6c12&platid=3&splatid=345&playid=0&tss=no&vtype=13&cvid=2026135183914&payff=0&pip=08cc52f8b09acd3eff8bf31688ddeced&format=0&sign=mb&dname=mobile&expect=1&tag=mobile&xformat=super"));
-        return clarities;
-    }
-
-
-    @Override
-    public void onClick(View v) {
-        switch (v.getId()) {
-            case R.id.btn_tiny_1:
-
-                break;
-            case R.id.btn_tiny_2:
-
-                break;
-            default:
-                break;
-        }
-    }
-
-
-}

+ 0 - 110
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestEightVideoActivity.java

@@ -1,110 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.os.Bundle;
-
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.yc.ycvideoplayer.BaseActivity;
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/**
- * @author yc
- */
-public class TestEightVideoActivity extends BaseActivity {
-
-
-    RecyclerView recyclerView;
-    private boolean pressedHome;
-    private HomeKeyWatcher mHomeKeyWatcher;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        mHomeKeyWatcher = new HomeKeyWatcher(this);
-        mHomeKeyWatcher.setOnHomePressedListener(new HomeKeyWatcher.OnHomePressedListener() {
-            @Override
-            public void onHomePressed() {
-                pressedHome = true;
-            }
-        });
-        pressedHome = false;
-        mHomeKeyWatcher.startWatch();
-    }
-
-    @Override
-    protected void onStop() {
-        // 在OnStop中是release还是suspend播放器,需要看是不是因为按了Home键
-        if (pressedHome) {
-            VideoPlayerManager.instance().suspendVideoPlayer();
-        } else {
-            VideoPlayerManager.instance().releaseVideoPlayer();
-        }
-        super.onStop();
-        mHomeKeyWatcher.stopWatch();
-    }
-
-    @Override
-    protected void onRestart() {
-        mHomeKeyWatcher.startWatch();
-        pressedHome = false;
-        super.onRestart();
-        VideoPlayerManager.instance().resumeVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()) {
-            return;
-        }
-        super.onBackPressed();
-    }
-
-    @Override
-    public int getContentView() {
-        return R.layout.base_recycler_view;
-    }
-
-    @Override
-    public void initView() {
-        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
-        recyclerView.setLayoutManager(new LinearLayoutManager(this));
-        recyclerView.setHasFixedSize(true);
-        VideoAdapter adapter;
-        List<Video> list = new ArrayList<>();
-        for (int a = 0; a< ConstantVideo.VideoPlayerList.length ; a++){
-            Video video = new Video(ConstantVideo.VideoPlayerTitle[a],ConstantVideo.VideoPlayerList[a]);
-            list.add(video);
-        }
-        adapter = new VideoAdapter(this, list);
-        recyclerView.setAdapter(adapter);
-        recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
-            @Override
-            public void onViewRecycled(RecyclerView.ViewHolder holder) {
-                OldVideoPlayer VideoPlayer = ((VideoAdapter.VideoViewHolder) holder).mVideoPlayer;
-                if (VideoPlayer == VideoPlayerManager.instance().getCurrentVideoPlayer()) {
-                    VideoPlayerManager.instance().releaseVideoPlayer();
-                }
-            }
-        });
-    }
-
-    @Override
-    public void initListener() {
-
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-}

+ 0 - 69
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestFragment.java

@@ -1,69 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/**
- * 如果你需要在播放的时候按下Home键能暂停,回调此Fragment又继续的话,需要继承自CompatHomeKeyFragment
- * @author yc
- */
-public class TestFragment extends Fragment {
-
-    private RecyclerView mRecyclerView;
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.base_recycler_view, container, false);
-    }
-
-    @Override
-    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
-        super.onViewCreated(view, savedInstanceState);
-        init(view);
-    }
-
-    private void init(View view) {
-        mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
-        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
-        mRecyclerView.setHasFixedSize(true);
-        List<Video> list = new ArrayList<>();
-        for (int a = 0; a< ConstantVideo.VideoPlayerList.length ; a++){
-            Video video = new Video(ConstantVideo.VideoPlayerTitle[a],ConstantVideo.VideoPlayerList[a]);
-            list.add(video);
-        }
-        VideoAdapter adapter = new VideoAdapter(getActivity(), list);
-        mRecyclerView.setAdapter(adapter);
-        mRecyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
-            @Override
-            public void onViewRecycled(RecyclerView.ViewHolder holder) {
-                OldVideoPlayer videoPlayer = ((VideoAdapter.VideoViewHolder) holder).mVideoPlayer;
-                if (videoPlayer == VideoPlayerManager.instance().getCurrentVideoPlayer()) {
-                    VideoPlayerManager.instance().releaseVideoPlayer();
-                }
-            }
-        });
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-}

+ 0 - 52
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestFragmentActivity.java

@@ -1,52 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import com.yc.ycvideoplayer.BaseActivity;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.old.other.VideoPlayerManager;
-
-
-/**
- * @author yc
- */
-public class TestFragmentActivity extends BaseActivity {
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()) {
-            return;
-        }
-        super.onBackPressed();
-    }
-
-
-    @Override
-    public int getContentView() {
-        return R.layout.activity_test_fragment;
-    }
-
-    @Override
-    public void initView() {
-        getSupportFragmentManager()
-                .beginTransaction()
-                .add(R.id.container, new TestFragment())
-                .commit();
-    }
-
-    @Override
-    public void initListener() {
-
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-}

+ 0 - 143
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestFullActivity2.java

@@ -1,143 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.view.View;
-import android.widget.Button;
-
-import com.bumptech.glide.Glide;
-import com.yc.ycvideoplayer.BaseActivity;
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.config.ConstantKeys;
-import com.yc.video.old.listener.OnVideoControlListener;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.controller.VideoPlayerController;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import cn.ycbjie.ycstatusbarlib.bar.StateAppBar;
-
-
-/**
- * @author yc
- */
-public class TestFullActivity2 extends BaseActivity implements View.OnClickListener {
-
-    private OldVideoPlayer videoPlayer;
-    private Button mBtnTiny1;
-    private Button mBtnTiny2;
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().suspendVideoPlayer();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()) {
-            return;
-        } else {
-            VideoPlayerManager.instance().releaseVideoPlayer();
-        }
-        super.onBackPressed();
-    }
-
-    @Override
-    protected void onRestart() {
-        super.onRestart();
-        VideoPlayerManager.instance().resumeVideoPlayer();
-    }
-
-    @Override
-    public int getContentView() {
-        return R.layout.activity_full_video2;
-    }
-
-    @Override
-    public void initView() {
-        StateAppBar.translucentStatusBar(this, true);
-        videoPlayer = (OldVideoPlayer) findViewById(R.id.nice_video_player);
-        mBtnTiny1 = (Button) findViewById(R.id.btn_tiny_1);
-        mBtnTiny2 = (Button) findViewById(R.id.btn_tiny_2);
-
-        videoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-        videoPlayer.setUp(ConstantVideo.VideoPlayerList[0], null);
-        VideoPlayerController controller = new VideoPlayerController(this);
-        controller.setTitle("办公室小野开番外了,居然在办公室开澡堂!老板还点赞?");
-        controller.setLength(98000);
-        Glide.with(this)
-                .load("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-30-43.jpg")
-                .placeholder(R.drawable.image_default)
-                .into(controller.imageView());
-        //设置中间播放按钮是否显示
-        controller.setTopPadding(24.0f);
-        controller.setTopVisibility(true);
-        controller.setOnVideoControlListener(new OnVideoControlListener() {
-            @Override
-            public void onVideoControlClick(int type) {
-                switch (type){
-                    case ConstantKeys.VideoControl.DOWNLOAD:
-                        //BaseToast.showRoundRectToast("下载");
-                        break;
-                    case ConstantKeys.VideoControl.SHARE:
-                        //BaseToast.showRoundRectToast("分享");
-                        break;
-                    case ConstantKeys.VideoControl.MENU:
-                        //BaseToast.showRoundRectToast("更多");
-                        break;
-                    case ConstantKeys.VideoControl.AUDIO:
-                        //BaseToast.showRoundRectToast("下载");
-                        break;
-                    default:
-                        break;
-                }
-            }
-        });
-        videoPlayer.setController(controller);
-        videoPlayer.continueFromLastPosition(false);
-        videoPlayer.postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                videoPlayer.start();
-            }
-        },500);
-    }
-
-    @Override
-    public void initListener() {
-        mBtnTiny1.setOnClickListener(this);
-        mBtnTiny2.setOnClickListener(this);
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-    @Override
-    public void onClick(View v) {
-        switch (v.getId()) {
-            case R.id.btn_tiny_1:
-                if (videoPlayer.isIdle()) {
-                    videoPlayer.start();
-                }
-                videoPlayer.enterVerticalScreenScreen();
-                break;
-            case R.id.btn_tiny_2:
-                if (videoPlayer.isIdle()) {
-                    videoPlayer.start();
-                }
-                videoPlayer.enterFullScreen();
-                break;
-            default:
-                break;
-        }
-    }
-
-}

+ 0 - 67
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestListFragment.java

@@ -1,67 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/**
- * 如果你需要在播放的时候按下Home键能暂停,回调此Fragment又继续的话,需要继承自CompatHomeKeyFragment
- */
-public class TestListFragment extends Fragment {
-
-    RecyclerView mRecyclerView;
-
-    @Nullable
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
-        View view = inflater.inflate(getContentView(),container,false);
-        initView(view);
-        return view;
-    }
-
-    public int getContentView() {
-        return R.layout.base_recycler_view;
-    }
-
-    public void initView(View view) {
-        mRecyclerView = view.findViewById(R.id.recyclerView);
-        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
-        mRecyclerView.setHasFixedSize(true);
-        List<Video> list = new ArrayList<>();
-        for (int a = 0; a< ConstantVideo.VideoPlayerList.length ; a++){
-            Video video = new Video(ConstantVideo.VideoPlayerTitle[a],ConstantVideo.VideoPlayerList[a]);
-            list.add(video);
-        }
-        VideoAdapter adapter = new VideoAdapter(getActivity(), list);
-        mRecyclerView.setAdapter(adapter);
-        mRecyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
-            @Override
-            public void onViewRecycled(RecyclerView.ViewHolder holder) {
-                OldVideoPlayer niceVideoPlayer = ((VideoAdapter.VideoViewHolder) holder).mVideoPlayer;
-                if (niceVideoPlayer == VideoPlayerManager.instance().getCurrentVideoPlayer()) {
-                    VideoPlayerManager.instance().releaseVideoPlayer();
-                }
-            }
-        });
-    }
-
-
-
-}

+ 0 - 215
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestNormalActivity.java

@@ -1,215 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.widget.ImageView;
-
-import com.yc.ycvideoplayer.BaseActivity;
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.config.ConstantKeys;
-import com.yc.video.old.controller.VideoPlayerController;
-import com.yc.video.old.listener.OnVideoControlListener;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import cn.ycbjie.ycstatusbarlib.bar.StateAppBar;
-
-
-/**
- * @author yc
- */
-public class TestNormalActivity extends BaseActivity {
-
-
-    OldVideoPlayer videoPlayer;
-    private VideoPlayerController controller;
-
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().suspendVideoPlayer();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()){
-            return;
-        }else {
-            VideoPlayerManager.instance().releaseVideoPlayer();
-        }
-        super.onBackPressed();
-    }
-
-    @Override
-    protected void onRestart() {
-        super.onRestart();
-        VideoPlayerManager.instance().resumeVideoPlayer();
-    }
-
-    @Override
-    public int getContentView() {
-        return R.layout.activity_full_video2;
-    }
-
-    @Override
-    public void initView() {
-        StateAppBar.translucentStatusBar(this, true);
-        videoPlayer = (OldVideoPlayer) findViewById(R.id.video_player);
-        //必须关键的4步,播放视频最简单的方式
-        videoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-        videoPlayer.setUp(ConstantVideo.VideoPlayerList[0], null);
-        controller = new VideoPlayerController(this);
-        controller.setTopPadding(24.0f);
-        videoPlayer.continueFromLastPosition(false);
-        videoPlayer.setController(controller);
-    }
-
-    @Override
-    public void initListener() {
-
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-    private void test(){
-        //关于视频播放相关api
-        //获取缓冲区百分比
-        int bufferPercentage = videoPlayer.getBufferPercentage();
-        //获取播放位置
-        long currentPosition = videoPlayer.getCurrentPosition();
-        //获取当前播放模式
-        int currentState = videoPlayer.getCurrentState();
-        //获取持续时长
-        long duration = videoPlayer.getDuration();
-        //获取最大音量
-        int maxVolume = videoPlayer.getMaxVolume();
-        //获取当前播放状态
-        int playType = videoPlayer.getPlayType();
-        //获取播放速度
-        long tcpSpeed = videoPlayer.getTcpSpeed();
-        //获取音量值
-        int volume = videoPlayer.getVolume();
-
-
-        //判断是否是否缓冲暂停
-        boolean bufferingPaused = videoPlayer.isBufferingPaused();
-        //判断视频是否正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
-        boolean bufferingPlaying = videoPlayer.isBufferingPlaying();
-        //判断视频是否播放完成
-        boolean completed = videoPlayer.isCompleted();
-        //判断视频是否播放错误
-        boolean error = videoPlayer.isError();
-        //判断视频是否播放全屏
-        boolean fullScreen = videoPlayer.isFullScreen();
-        //判断是否开始播放
-        boolean idle = videoPlayer.isIdle();
-        //判断视频是否正常播放
-        boolean normal = videoPlayer.isNormal();
-        //判断视频是否暂停播放
-        boolean paused = videoPlayer.isPaused();
-        //判断视频是否正在播放
-        boolean playing = videoPlayer.isPlaying();
-        //判断视频是否准备就绪
-        boolean prepared = videoPlayer.isPrepared();
-        //判断视频是否播放准备中
-        boolean preparing = videoPlayer.isPreparing();
-        //判断视频是否播放小窗口
-        boolean tinyWindow = videoPlayer.isTinyWindow();
-
-        //进入全屏模式
-        videoPlayer.enterFullScreen();
-        //进入竖屏的全屏模式
-        videoPlayer.enterVerticalScreenScreen();
-        //进入小窗口播放
-        //注意:小窗口播放视频比例是        16:9
-        videoPlayer.enterTinyWindow();
-
-        //释放,内部的播放器被释放掉,同时如果在全屏、小窗口模式下都会退出
-        videoPlayer.release();
-        //释放播放器,注意一定要判断对象是否为空,增强严谨性
-        videoPlayer.releasePlayer();
-
-        //设置播放器类型,必须设置
-        //输入值:ConstantKeys.IjkPlayerType.TYPE_IJK   或者  ConstantKeys.IjkPlayerType.TYPE_NATIVE
-        videoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-        //设置播放位置
-        videoPlayer.seekTo(100);
-        //设置播放速度,不必须
-        videoPlayer.setSpeed(100);
-        //设置视频链接
-        videoPlayer.setUp("",null);
-        //设置音量
-        videoPlayer.setVolume(50);
-
-        //是否从上一次的位置继续播放
-        videoPlayer.continueFromLastPosition(true);
-
-        //开始播放
-        videoPlayer.start();
-        //暂停播放
-        videoPlayer.pause();
-        //开始播放
-        videoPlayer.start(100);
-        //重新播放
-        videoPlayer.restart();
-
-        //设置是否显示视频头部的下载,分享,其他等控件是否显示
-        controller.setTopVisibility(true);
-        controller.setTop(20);
-        //设置top到顶部的距离
-        controller.setTopPadding(30);
-        //设置加载loading类型
-        controller.setLoadingType(ConstantKeys.Loading.LOADING_RING);
-        //设置不操作后,多久自动隐藏头部和底部布局
-        controller.setHideTime(8000);
-        //获取ImageView的对象
-        ImageView imageView = controller.imageView();
-        //重新设置
-        controller.reset();
-        //设置图片
-        //controller.setImage(R.drawable.ic_back_right);
-        //设置视频时长
-        controller.setLength(1000);
-        //设置视频标题
-        controller.setTitle("小杨逗比");
-        boolean lock = controller.getLock();
-        //设置横屏播放时,tv和audio图标是否显示
-        controller.setTvAndAudioVisibility(true,true);
-
-        //设置视频分享,下载,音视频转化点击事件
-        controller.setOnVideoControlListener(new OnVideoControlListener() {
-            @Override
-            public void onVideoControlClick(int type) {
-
-            }
-        });
-
-        //VideoPlayerManager对象
-        VideoPlayerManager instance = VideoPlayerManager.instance();
-        //当视频暂停时或者缓冲暂停时,调用该方法重新开启视频播放
-        instance.resumeVideoPlayer();
-        //当视频正在播放或者正在缓冲时,调用该方法暂停视频
-        instance.suspendVideoPlayer();
-        //释放,内部的播放器被释放掉,同时如果在全屏、小窗口模式下都会退出
-        instance.releaseVideoPlayer();
-        //处理返回键逻辑
-        //如果是全屏,则退出全屏
-        //如果是小窗口,则退出小窗口
-        instance.onBackPressed();
-        //获取对象
-        OldVideoPlayer currentVideoPlayer = instance.getCurrentVideoPlayer();
-        //设置VideoPlayer
-        instance.setCurrentVideoPlayer(videoPlayer);
-    }
-
-}

+ 0 - 79
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestRecyclerActivity.java

@@ -1,79 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.yc.ycvideoplayer.BaseActivity;
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-/**
- * @author yc
- */
-public class TestRecyclerActivity extends BaseActivity {
-
-
-    RecyclerView recyclerView;
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()){
-            return;
-        }
-        super.onBackPressed();
-    }
-
-
-    @Override
-    public int getContentView() {
-        return R.layout.base_recycler_view;
-    }
-
-    @Override
-    public void initView() {
-        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
-        recyclerView.setLayoutManager(new LinearLayoutManager(this));
-        recyclerView.setHasFixedSize(true);
-        List<Video> list = new ArrayList<>();
-        for (int a = 0; a< ConstantVideo.VideoPlayerList.length ; a++){
-            Video video = new Video(ConstantVideo.VideoPlayerTitle[a],ConstantVideo.VideoPlayerList[a]);
-            list.add(video);
-        }
-        VideoAdapter adapter = new VideoAdapter(this, list);
-        recyclerView.setAdapter(adapter);
-        recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
-            @Override
-            public void onViewRecycled(RecyclerView.ViewHolder holder) {
-                OldVideoPlayer videoPlayer = ((VideoAdapter.VideoViewHolder) holder).mVideoPlayer;
-                if (videoPlayer == VideoPlayerManager.instance().getCurrentVideoPlayer()) {
-                    VideoPlayerManager.instance().releaseVideoPlayer();
-                }
-            }
-        });
-    }
-
-    @Override
-    public void initListener() {
-
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-}

+ 0 - 214
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestSavePosActivity.java

@@ -1,214 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.widget.ImageView;
-
-import com.yc.ycvideoplayer.BaseActivity;
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.config.ConstantKeys;
-import com.yc.video.old.controller.VideoPlayerController;
-import com.yc.video.old.listener.OnVideoControlListener;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import cn.ycbjie.ycstatusbarlib.bar.StateAppBar;
-
-
-/**
- * @author yc
- */
-public class TestSavePosActivity extends BaseActivity {
-
-
-    OldVideoPlayer videoPlayer;
-    private VideoPlayerController controller;
-
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().suspendVideoPlayer();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()){
-            return;
-        }else {
-            VideoPlayerManager.instance().releaseVideoPlayer();
-        }
-        super.onBackPressed();
-    }
-
-    @Override
-    protected void onRestart() {
-        super.onRestart();
-        VideoPlayerManager.instance().resumeVideoPlayer();
-    }
-
-    @Override
-    public int getContentView() {
-        return R.layout.activity_full_video1;
-    }
-
-    @Override
-    public void initView() {
-        StateAppBar.translucentStatusBar(this, true);
-        videoPlayer = (OldVideoPlayer) findViewById(R.id.video_player);
-        //必须关键的4步,播放视频最简单的方式
-        videoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-        videoPlayer.setUp(ConstantVideo.VideoPlayerList[0], null);
-        controller = new VideoPlayerController(this);
-        controller.setTopPadding(24.0f);
-        videoPlayer.continueFromLastPosition(true);
-        videoPlayer.setController(controller);
-    }
-
-    @Override
-    public void initListener() {
-
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-    private void test(){
-        //关于视频播放相关api
-        //获取缓冲区百分比
-        int bufferPercentage = videoPlayer.getBufferPercentage();
-        //获取播放位置
-        long currentPosition = videoPlayer.getCurrentPosition();
-        //获取当前播放模式
-        int currentState = videoPlayer.getCurrentState();
-        //获取持续时长
-        long duration = videoPlayer.getDuration();
-        //获取最大音量
-        int maxVolume = videoPlayer.getMaxVolume();
-        //获取当前播放状态
-        int playType = videoPlayer.getPlayType();
-        //获取播放速度
-        long tcpSpeed = videoPlayer.getTcpSpeed();
-        //获取音量值
-        int volume = videoPlayer.getVolume();
-
-
-        //判断是否是否缓冲暂停
-        boolean bufferingPaused = videoPlayer.isBufferingPaused();
-        //判断视频是否正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
-        boolean bufferingPlaying = videoPlayer.isBufferingPlaying();
-        //判断视频是否播放完成
-        boolean completed = videoPlayer.isCompleted();
-        //判断视频是否播放错误
-        boolean error = videoPlayer.isError();
-        //判断视频是否播放全屏
-        boolean fullScreen = videoPlayer.isFullScreen();
-        //判断是否开始播放
-        boolean idle = videoPlayer.isIdle();
-        //判断视频是否正常播放
-        boolean normal = videoPlayer.isNormal();
-        //判断视频是否暂停播放
-        boolean paused = videoPlayer.isPaused();
-        //判断视频是否正在播放
-        boolean playing = videoPlayer.isPlaying();
-        //判断视频是否准备就绪
-        boolean prepared = videoPlayer.isPrepared();
-        //判断视频是否播放准备中
-        boolean preparing = videoPlayer.isPreparing();
-        //判断视频是否播放小窗口
-        boolean tinyWindow = videoPlayer.isTinyWindow();
-
-        //进入全屏模式
-        videoPlayer.enterFullScreen();
-        //进入竖屏的全屏模式
-        videoPlayer.enterVerticalScreenScreen();
-        //进入小窗口播放
-        //注意:小窗口播放视频比例是        16:9
-        videoPlayer.enterTinyWindow();
-
-        //释放,内部的播放器被释放掉,同时如果在全屏、小窗口模式下都会退出
-        videoPlayer.release();
-        //释放播放器,注意一定要判断对象是否为空,增强严谨性
-        videoPlayer.releasePlayer();
-
-        //设置播放器类型,必须设置
-        //输入值:ConstantKeys.IjkPlayerType.TYPE_IJK   或者  ConstantKeys.IjkPlayerType.TYPE_NATIVE
-        videoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-        //设置播放位置
-        videoPlayer.seekTo(100);
-        //设置播放速度,不必须
-        videoPlayer.setSpeed(100);
-        //设置视频链接
-        videoPlayer.setUp("",null);
-        //设置音量
-        videoPlayer.setVolume(50);
-
-        //是否从上一次的位置继续播放
-        videoPlayer.continueFromLastPosition(true);
-
-        //开始播放
-        videoPlayer.start();
-        //暂停播放
-        videoPlayer.pause();
-        //开始播放
-        videoPlayer.start(100);
-        //重新播放
-        videoPlayer.restart();
-
-        //设置是否显示视频头部的下载,分享,其他等控件是否显示
-        controller.setTopVisibility(true);
-        controller.setTop(20);
-        //设置top到顶部的距离
-        controller.setTopPadding(30);
-        //设置加载loading类型
-        controller.setLoadingType(ConstantKeys.Loading.LOADING_RING);
-        //设置不操作后,多久自动隐藏头部和底部布局
-        controller.setHideTime(8000);
-        //获取ImageView的对象
-        ImageView imageView = controller.imageView();
-        //重新设置
-        controller.reset();
-        //设置图片
-        //controller.setImage(R.drawable.ic_back_right);
-        //设置视频时长
-        controller.setLength(1000);
-        //设置视频标题
-        controller.setTitle("小杨逗比");
-        boolean lock = controller.getLock();
-        //设置横屏播放时,tv和audio图标是否显示
-        controller.setTvAndAudioVisibility(true,true);
-        //设置视频分享,下载,音视频转化点击事件
-        controller.setOnVideoControlListener(new OnVideoControlListener() {
-            @Override
-            public void onVideoControlClick(int type) {
-
-            }
-        });
-
-        //VideoPlayerManager对象
-        VideoPlayerManager instance = VideoPlayerManager.instance();
-        //当视频暂停时或者缓冲暂停时,调用该方法重新开启视频播放
-        instance.resumeVideoPlayer();
-        //当视频正在播放或者正在缓冲时,调用该方法暂停视频
-        instance.suspendVideoPlayer();
-        //释放,内部的播放器被释放掉,同时如果在全屏、小窗口模式下都会退出
-        instance.releaseVideoPlayer();
-        //处理返回键逻辑
-        //如果是全屏,则退出全屏
-        //如果是小窗口,则退出小窗口
-        instance.onBackPressed();
-        //获取对象
-        OldVideoPlayer currentVideoPlayer = instance.getCurrentVideoPlayer();
-        //设置VideoPlayer
-        instance.setCurrentVideoPlayer(videoPlayer);
-    }
-
-}

+ 0 - 121
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestTinyActivity.java

@@ -1,121 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.view.View;
-import android.widget.Button;
-import android.widget.Toast;
-
-import com.bumptech.glide.Glide;
-import com.yc.ycvideoplayer.BaseActivity;
-import com.yc.ycvideoplayer.ConstantVideo;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.config.ConstantKeys;
-import com.yc.video.old.controller.VideoPlayerController;
-import com.yc.video.old.listener.OnVideoControlListener;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import cn.ycbjie.ycstatusbarlib.bar.StateAppBar;
-
-
-/**
- * @author yc
- */
-public class TestTinyActivity extends BaseActivity implements View.OnClickListener {
-
-
-    private OldVideoPlayer videoPlayer;
-    private Button mBtnTiny1;
-    private Button mBtnTiny2;
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        VideoPlayerManager.instance().releaseVideoPlayer();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()) return;
-        super.onBackPressed();
-    }
-
-
-    @Override
-    public int getContentView() {
-        return R.layout.activity_test_video3;
-    }
-
-    @Override
-    public void initView() {
-        StateAppBar.translucentStatusBar(this, true);
-        videoPlayer = (OldVideoPlayer) findViewById(R.id.nice_video_player);
-        mBtnTiny1 = (Button) findViewById(R.id.btn_tiny_1);
-        mBtnTiny2 = (Button) findViewById(R.id.btn_tiny_2);
-
-
-        videoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-        videoPlayer.setUp(ConstantVideo.VideoPlayerList[0], null);
-        VideoPlayerController controller = new VideoPlayerController(this);
-        controller.setLoadingType(ConstantKeys.Loading.LOADING_RING);
-        controller.setTitle("办公室小野开番外了,居然在办公室开澡堂!老板还点赞?");
-        controller.setLength(98000);
-        Glide.with(this)
-                .load("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-30-43.jpg")
-                .placeholder(R.drawable.image_default)
-                .into(controller.imageView());
-        controller.setHideTime(2000);
-        controller.setTopPadding(24);
-        //设置横屏播放时,tv和audio图标是否显示
-        controller.setTvAndAudioVisibility(true,true);
-        controller.setOnVideoControlListener(new OnVideoControlListener() {
-            @Override
-            public void onVideoControlClick(int type) {
-                switch (type){
-                    case ConstantKeys.VideoControl.TV:
-                        //BaseToast.showRoundRectToast("投影tv电视");
-                        break;
-                    case ConstantKeys.VideoControl.HOR_AUDIO:
-                        //BaseToast.showRoundRectToast("切换音频");
-                        break;
-                    default:
-                        break;
-                }
-            }
-        });
-        videoPlayer.continueFromLastPosition(false);
-        videoPlayer.setController(controller);
-    }
-
-    @Override
-    public void initListener() {
-        mBtnTiny1.setOnClickListener(this);
-        mBtnTiny2.setOnClickListener(this);
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-    @Override
-    public void onClick(View v) {
-        switch (v.getId()){
-            case R.id.btn_tiny_1:
-                if (videoPlayer.isIdle()) {
-                    Toast.makeText(this, "要点击播放后才能进入小窗口", Toast.LENGTH_SHORT).show();
-                } else {
-                    videoPlayer.enterTinyWindow();
-                }
-                break;
-            case R.id.btn_tiny_2:
-                if (videoPlayer.isIdle()) {
-                    videoPlayer.start();
-                }
-                videoPlayer.enterVerticalScreenScreen();
-                break;
-            default:
-                break;
-        }
-    }
-}

+ 0 - 147
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/TestWindowActivity.java

@@ -1,147 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.provider.Settings;
-import android.view.View;
-import android.view.animation.BounceInterpolator;
-import android.widget.Button;
-
-import androidx.annotation.RequiresApi;
-
-import com.yc.videoview.FloatWindow;
-import com.yc.videoview.MoveType;
-import com.yc.videoview.WindowScreen;
-import com.yc.videoview.WindowUtil;
-import com.yc.ycvideoplayer.BaseActivity;
-import com.yc.ycvideoplayer.newPlayer.tiny.TestFullActivity;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.old.other.VideoPlayerManager;
-import com.yc.video.old.player.OldVideoPlayer;
-
-
-/**
- * @author yc
- */
-public class TestWindowActivity extends BaseActivity implements View.OnClickListener {
-
-    private OldVideoPlayer mVideoPlayer;
-    private Button mBtn1;
-    private Button mBtn2;
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        /*
-         * 这里在返回主页的时候销毁了,因为不想和DEMO中其他页面冲突
-         */
-        VideoPlayerManager.instance().releaseVideoPlayer();
-        FloatWindow.destroy();
-    }
-
-
-    @Override
-    public void onBackPressed() {
-        if (VideoPlayerManager.instance().onBackPressed()) {
-            return;
-        }
-        super.onBackPressed();
-    }
-
-
-    @Override
-    public int getContentView() {
-        return R.layout.activity_test_window;
-    }
-
-    @Override
-    public void initView() {
-        mVideoPlayer = (OldVideoPlayer) findViewById(R.id.video_player);
-        mBtn1 = (Button) findViewById(R.id.btn_1);
-        mBtn1.setOnClickListener(this);
-        mBtn2 = (Button) findViewById(R.id.btn_2);
-        mBtn2.setOnClickListener(this);
-        if (Build.VERSION.SDK_INT >= 23) {
-            if (!WindowUtil.hasPermission(this)) {
-                requestAlertWindowPermission();
-            }
-        }
-    }
-
-    @Override
-    public void initListener() {
-    }
-
-    @Override
-    public void initData() {
-
-    }
-
-    @Override
-    public void onClick(View v) {
-        switch (v.getId()) {
-            case R.id.btn_1:
-                startWindow();
-                break;
-            case R.id.btn_2:
-                startActivity(new Intent(this, TestFullActivity.class));
-                break;
-            default:
-                break;
-        }
-    }
-
-    private void startWindow() {
-        if (FloatWindow.get() != null) {
-            return;
-        }
-        String url = "http://play.g3proxy.lecloud.com/vod/v2/MjUxLzE2LzgvbGV0di11dHMvMTQvdmVyXzAwXzIyLTExMDc2NDEzODctYXZjLTE5OTgxOS1hYWMtNDgwMDAtNTI2MTEwLTE3MDg3NjEzLWY1OGY2YzM1NjkwZTA2ZGFmYjg2MTVlYzc5MjEyZjU4LTE0OTg1NTc2ODY4MjMubXA0?b=259&mmsid=65565355&tm=1499247143&key=f0eadb4f30c404d49ff8ebad673d3742&platid=3&splatid=345&playid=0&tss=no&vtype=21&cvid=2026135183914&payff=0&pip=08cc52f8b09acd3eff8bf31688ddeced&format=0&sign=mb&dname=mobile&expect=1&tag=mobile&xformat=super";
-        FloatPlayerView.setUrl(url);
-        FloatPlayerView floatPlayerView = new FloatPlayerView(getApplicationContext());
-        floatPlayerView.setCompletedListener(new FloatPlayerView.CompletedListener() {
-            @Override
-            public void Completed() {
-                FloatWindow.get().hide();
-            }
-        });
-        FloatWindow
-                .with(getApplicationContext())
-                .setView(floatPlayerView)
-                //.setWidth(WindowScreen.WIDTH, 0.4f)
-                //.setHeight(WindowScreen.WIDTH, 0.3f)
-                //这个是设置位置
-                .setX(WindowScreen.WIDTH, 0.8f)
-                .setY(WindowScreen.HEIGHT, 0.3f)
-                .setMoveType(MoveType.slide)
-                .setFilter(false)
-                //.setFilter(true, WindowActivity.class, EmptyActivity.class)
-                .setMoveStyle(500, new BounceInterpolator())
-                .build();
-        FloatWindow.get().show();
-    }
-
-
-    @RequiresApi(api = 23)
-    private void requestAlertWindowPermission() {
-        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
-                Uri.parse("package:" + getPackageName()));
-        startActivityForResult(intent, 1);
-    }
-
-    @RequiresApi(api = 23)
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        super.onActivityResult(requestCode, resultCode, data);
-        if (Build.VERSION.SDK_INT >= 23) {
-            if (WindowUtil.hasPermission(this)) {
-
-            } else {
-                this.finish();
-            }
-        }
-    }
-
-
-}

+ 0 - 28
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/Video.java

@@ -1,28 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-public class Video {
-
-    private String title;
-    private String url;
-
-    public Video(String title, String url) {
-        this.title = title;
-        this.url = url;
-    }
-
-    public String getTitle() {
-        return title;
-    }
-
-    public void setTitle(String title) {
-        this.title = title;
-    }
-
-    public String getUrl() {
-        return url;
-    }
-
-    public void setUrl(String url) {
-        this.url = url;
-    }
-}

+ 0 - 86
Demo/src/main/java/com/yc/ycvideoplayer/oldPlayer/VideoAdapter.java

@@ -1,86 +0,0 @@
-package com.yc.ycvideoplayer.oldPlayer;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.yc.ycvideoplayer.ImageUtil;
-
-import org.yc.ycvideoplayer.R;
-import com.yc.video.config.ConstantKeys;
-import com.yc.video.old.controller.VideoPlayerController;
-import com.yc.video.old.player.OldVideoPlayer;
-
-import java.util.List;
-
-public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHolder> {
-
-    private Context mContext;
-    private List<Video> mVideoList;
-
-    public VideoAdapter(Context context, List<Video> videoList) {
-        mContext = context;
-        mVideoList = videoList;
-    }
-
-    @Override
-    public VideoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        View itemView = LayoutInflater.from(mContext).inflate(R.layout.item_my_video, parent, false);
-        VideoViewHolder holder = new VideoViewHolder(itemView);
-        //创建视频播放控制器,主要只要创建一次就可以呢
-        VideoPlayerController controller = new VideoPlayerController(mContext);
-        holder.setController(controller);
-        return holder;
-    }
-
-    @Override
-    public void onBindViewHolder(VideoViewHolder holder, int position) {
-        Video video = mVideoList.get(position);
-        holder.bindData(video);
-    }
-
-    @Override
-    public int getItemCount() {
-        return mVideoList==null ? 0 : mVideoList.size();
-    }
-
-    public class VideoViewHolder extends RecyclerView.ViewHolder {
-
-        public VideoPlayerController mController;
-        public OldVideoPlayer mVideoPlayer;
-
-        VideoViewHolder(View itemView) {
-            super(itemView);
-            mVideoPlayer = (OldVideoPlayer) itemView.findViewById(R.id.nice_video_player);
-            // 将列表中的每个视频设置为默认16:9的比例
-            ViewGroup.LayoutParams params = mVideoPlayer.getLayoutParams();
-            // 宽度为屏幕宽度
-            params.width = itemView.getResources().getDisplayMetrics().widthPixels;
-            // 高度为宽度的9/16
-            params.height = (int) (params.width * 9f / 16f);
-            mVideoPlayer.setLayoutParams(params);
-        }
-
-        /**
-         * 设置视频控制器参数
-         * @param controller            控制器对象
-         */
-        void setController(VideoPlayerController controller) {
-            mController = controller;
-            mVideoPlayer.setPlayerType(ConstantKeys.VideoPlayerType.TYPE_IJK);
-            mVideoPlayer.setController(mController);
-        }
-
-        void bindData(Video video) {
-            mController.setTitle(video.getTitle());
-            //mController.setLength(video.getLength());
-            ImageUtil.display(itemView.getContext(),video.getUrl(),R.drawable.image_default,mController.imageView());
-            mVideoPlayer.setUp(video.getUrl(), null);
-        }
-    }
-
-
-}

+ 0 - 12
Demo/src/main/res/layout/item_my_video.xml

@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical" android:layout_width="match_parent"
-    android:layout_height="wrap_content">
-
-
-    <com.yc.video.old.player.OldVideoPlayer
-        android:id="@+id/nice_video_player"
-        android:layout_width="match_parent"
-        android:layout_height="240dp"/>
-
-</LinearLayout>

+ 152 - 0
ReadDesign/01.设计模式导读/02.行为型模式.md

@@ -0,0 +1,152 @@
+#### 目录介绍
+- 01.行为型模式
+- 02.包含模式
+- 03.解决什么问题
+- 04.简单介绍
+
+
+
+### 01.行为型模式
+- 行为型模式\(Behavioral Pattern\)是对在不同的对象之间划分责任和算法的抽象化。
+- 行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。
+- 通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象 之间的交互。在系统运行时,对象并不是孤立的,它们可以通过相互通信与协作完成某些复杂功能,一个对象在运行时也将影响到其他对象的运行。
+- 行为型模式分为类行为型模式和对象行为型模式两种:
+    * 类行为型模式:类的行为型模式使用继承关系在几个类之间分配行为,类行为型模式主要通过多态等方式来分配父类与子类的职责。
+    * 对象行为型模式:对象的行为型模式则使用对象的聚合关联关系来分配行为,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式。
+
+
+### 02.包含模式
+* **职责链模式\(Chain of Responsibility\)**
+* **命令模式\(Command\)**
+* **解释器模式\(Interpreter\)**
+* **迭代器模式\(Iterator\)**
+* **中介者模式\(Mediator\)**
+* **备忘录模式\(Memento\)**
+* **观察者模式\(Observer\)**
+* **状态模式\(State\)**
+* **策略模式\(Strategy\)**
+* **模板方法模式\(Template Method\)**
+* **访问者模式\(Visitor\)**
+
+
+
+### 03.解决什么问题
+- 创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合”问题。
+- 那行为型设计模式主要解决的就是“类或对象之间的交互”问题。
+
+
+
+### 04.简单介绍
+#### 4.1 观察者模式
+- 观察者模式将观察者和被观察者代码解耦。
+    - 观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。
+- 不同实现方式
+    - 不同的应用场景和需求下,这个模式也有截然不同的实现方式:有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。
+    - 同步阻塞是最经典的实现方式,主要是为了代码解耦;异步非阻塞除了能实现代码解耦之外,还能提高代码的执行效率;进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互。
+- 解耦业务
+    - 框架的作用有隐藏实现细节,降低开发难度,实现代码复用,解耦业务与非业务代码,让程序员聚焦业务开发。针对异步非阻塞观察者模式,我们也可以将它抽象成EventBus框架来达到这样的效果。EventBus翻译为“事件总线”,它提供了实现观察者模式的骨架代码。我们可以基于此框架非常容易地在自己的业务场景中实现观察者模式,不需要从零开始开发。
+
+
+#### 4.2 模板模式
+- 什么是模板模式
+    - 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
+- 复用和扩展
+    - 模板模式有两大作用:复用和扩展。其中复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
+- 回调
+    - 除此之外,我们还讲到回调。它跟模板模式具有相同的作用:代码复用和扩展。在一些框架、类库、组件等的设计中经常会用到,比如JdbcTemplate就是用了回调。
+    - 相对于普通的函数调用,回调是一种双向调用关系。A类事先注册某个函数F到B类,A类在调用B类的P函数的时候,B类反过来调用A类注册给它的F函数。这里的F函数就是“回调函数”。A调用B,B反过来又调用A,这种调用机制就叫作“回调”。
+- 同步和异步
+    - 回调可以细分为同步回调和异步回调。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。回调跟模板模式的区别,更多的是在代码实现上,而非应用场景上。回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活。
+
+
+
+#### 4.3 策略模式
+- 什么是策略模式
+    - 策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式就是由这三个部分组成的。
+- 策略类的定义
+    - 策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。策略的创建由工厂类来完成,封装策略创建的细节。策略模式包含一组策略可选,客户端代码选择使用哪个策略,有两种确定方法:编译时静态确定和运行时动态确定。其中,“运行时动态确定”才是策略模式最典型的应用场景。
+- 常用应用场景
+    - 在实际的项目开发中,策略模式也比较常用。最常见的应用场景是,利用它来避免冗长的if-else或switch分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。实际上,策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入bug的风险。
+
+
+#### 4.4 职责链模式
+- 什么是职责链模式
+    - 在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过A处理器处理,然后再把请求传递给B处理器,B处理器处理完后再传递给C处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
+- 定义
+    - 在GoF的定义中,一旦某个处理器能处理这个请求,就不会继续将请求传递给后续的处理器了。当然,在实际的开发中,也存在对这个模式的变体,那就是请求不会中途终止传递,而是会被所有的处理器都处理一遍。
+- 场景
+    - 职责链模式常用在框架开发中,用来实现过滤器、拦截器功能,让框架的使用者在不需要修改框架源码的情况下,添加新的过滤、拦截功能。这也体现了之前讲到的对扩展开放、对修改关闭的设计原则。
+
+
+
+#### 4.5 迭代器模式
+- 什么是迭代器模式
+    - 迭代器模式也叫游标模式,它用来遍历集合对象。这里说的“集合对象”,我们也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如,数组、链表、树、图、跳表。迭代器模式主要作用是解耦容器代码和遍历代码。大部分编程语言都提供了现成的迭代器可以使用,我们不需要从零开始开发。
+- 三种方式
+    - 遍历集合一般有三种方式:for循环、foreach循环、迭代器遍历。后两种本质上属于一种,都可以看作迭代器遍历。相对于for循环遍历,利用迭代器来遍历有3个优势:
+- 为何需要迭代器
+    - 迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
+    - 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
+    - 迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。
+    - 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。针对这个问题,有两种比较干脆利索的解决方案,来避免出现这种不可预期的运行结果。一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。第一种解决方案比较难实现,因为很难确定迭代器使用结束的时间点。第二种解决方案更加合理,Java语言就是采用的这种解决方案。增删元素之后,我们选择fail-fast解决方式,让遍历操作直接抛出运行时异常。
+
+
+
+#### 4.6 状态模式
+- 什么是状态模式
+    - 状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。状态机又叫有限状态机,它由3个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。
+- 三种实现方式
+    - 第一种实现方式叫分支逻辑法。利用if-else或者switch-case分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。
+    - 第二种实现方式叫查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
+    - 第三种实现方式就是利用状态模式。对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。
+
+
+#### 4.7 访问者模式
+- 什么是访问者模式
+    - 访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。
+- 代码实现
+    - 对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的。除此之外,我们还讲到Double Disptach。如果某种语言支持Double Dispatch,那就不需要访问者模式了。
+    - 正是因为代码实现难理解,所以,在项目中应用这种模式,会导致代码的可读性比较差。如果你的同事不了解这种设计模式,可能就会读不懂、维护不了你写的代码。所以,除非不得已,不要使用这种模式。
+
+#### 4.8 备忘录模式
+- 什么叫备忘录模式
+    - 备忘录模式也叫快照模式,具体来说,就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。这个模式的定义表达了两部分内容:一部分是,存储副本以便后期恢复;另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。
+- 场景
+    - 备忘录模式的应用场景也比较明确和有限,主要用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计。
+    - 对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长。针对这个问题,不同的业务场景有不同的处理方式。比如,只备份必要的恢复信息,结合最新的数据来恢复;再比如,全量备份和增量备份相结合,低频全量备份,高频增量备份,两者结合来做恢复。
+
+
+#### 4.9 命令模式
+- 什么是命令模式
+    - 落实到编码实现,命令模式用到最核心的实现手段,就是将函数封装成对象。我们知道,在大部分编程语言中,函数是没法作为参数传递给其他函数的,也没法赋值给变量。借助命令模式,我们将函数封装成对象,这样就可以实现把函数像对象一样使用。
+- 场景
+    - 命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等,这才是命令模式能发挥独一无二作用的地方。
+
+
+#### 4.10 解释器模式
+- 什么是揭示其模式
+    - 解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。
+    - 要想了解“语言”要表达的信息,我们就必须定义相应的语法规则。这样,书写者就可以根据语法规则来书写“句子”(专业点的叫法应该是“表达式”),阅读者根据语法规则来阅读“句子”,这样才能做到信息的正确传递。而我们要讲的解释器模式,其实就是用来实现根据语法规则解读“句子”的解释器。
+- 场景分析
+    - 解释器模式的代码实现比较灵活,没有固定的模板。我们前面说过,应用设计模式主要是应对代码的复杂性,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
+
+
+
+
+#### 4.11 中介模式
+- 什么是中介模式
+    - 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟n个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。
+- 场景
+    - 观察者模式和中介模式都是为了实现参与者之间的解耦,简化交互关系。两者的不同在于应用场景上。在观察者模式的应用场景中,参与者之间的交互比较有条理,一般都是单向的,一个参与者只有一个身份,要么是观察者,要么是被观察者。而在中介模式的应用场景中,参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。
+
+
+
+
+
+
+
+
+
+
+
+

+ 81 - 0
ReadDesign/01.设计模式导读/03.创建型模式.md

@@ -0,0 +1,81 @@
+#### 目录介绍
+- 01.创建型模式
+- 02.包含模式
+- 03.解决什么问题
+- 04.简单介绍
+
+
+### 01.创建型模式
+- 创建型模式\(Creational Pattern\)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。
+- 为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。
+- 创建型模式在创建什么\(What\),由谁创建\(Who\),何时创建\(When\)等方面都为软件设计者提供了尽可能大的灵活性。创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。
+
+
+
+### 02.包含模式
+- 分类
+    * **简单工厂模式(Simple Factory)**
+    * **工厂方法模式(Factory Method)**
+    * **抽象工厂模式(Abstract Factory)**
+    * **建造者模式(Builder)**
+    * **原型模式(Prototype)**
+    * **单例模式(Singleton)**
+- 常见的有
+    - 单例模式
+    - 工厂模式
+    - 建造者模式
+
+
+
+### 03.解决什么问题
+- 主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
+
+
+### 04.简单介绍
+#### 4.1 单例模式
+- 单例模式用来创建全局唯一的对象。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式、懒汉式、双重检测、静态内部类、枚举。
+- 尽管单例是一个很常用的设计模式,在实际的开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式(anti-pattern),并不推荐使用,主要的理由有以下几点:
+    - 单例对 OOP 特性的支持不友好
+    - 单例会隐藏类之间的依赖关系
+    - 单例对代码的扩展性不友好
+    - 单例对代码的可测试性不友好
+    - 单例不支持有参数的构造函数
+
+
+#### 4.2 工厂模式
+- 工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。实际上,如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象就可以了,不需要使用工厂模式。当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。
+- 当每个对象的创建逻辑都比较简单的时候,我推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的工厂类,我们推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
+- 详细点说,工厂模式的作用有下面4个,这也是判断要不要使用工厂模式最本质的参考标准。
+    - 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
+    - 代码复用:创建代码抽离到独立的工厂类之后可以复用。
+    - 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
+    - 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
+
+
+
+#### 4.3 建造者模式
+- 建造者模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。建造者模式的原理和实现比较简单,重点是掌握应用场景,避免过度使用。
+- 如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合set()方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
+    - 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题。如果我们把必填属性通过 set() 方法设置,那校验这些必填属性是否已经填写的逻辑就无处安放了。
+    - 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了。
+    - 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。
+
+
+
+#### 4.4 原型模式
+- 如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型模式。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 80 - 0
ReadDesign/01.设计模式导读/04.结构型模式.md

@@ -0,0 +1,80 @@
+#### 目录介绍
+- 01.结构型模式
+- 02.包含模式分类
+- 03.解决什么问题
+- 04.简单介绍
+
+
+
+### 01.结构型模式
+- 结构型模式\(Structural Pattern\)描述如何**将类或者对象结合在一起形成更大的结构**,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。
+- 结构型模式可以分为类结构型模式和对象结构型模式:
+    * 类结构型模式关心类的组合,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。 
+    * 对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。 根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。
+
+
+
+### 02.包含模式分类
+- 包含模式分类
+    * **适配器模式\(Adapter\)**
+    * **桥接模式\(Bridge\)**
+    * **组合模式\(Composite\)**
+    * **装饰模式\(Decorator\)**
+    * **外观模式\(Facade\)**
+    * **享元模式\(Flyweight\)**
+    * **代理模式\(Proxy\)**
+- 常见的有
+    - 组合模式
+    - 代理模式
+
+
+
+### 03.解决什么问题
+- 总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。
+
+
+
+### 04.简单介绍
+#### 4.1 代理模式
+- 代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。一般情况下,我们让代理类和原始类实现同样的接口。但是,如果原始类并没有定义接口,并且原始类代码并不是我们开发维护的。在这种情况下,我们可以通过让代理类继承原始类的方法来实现代理模式。
+- 静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。对于静态代理存在的问题,我们可以通过动态代理来解决。我们不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
+- 代理模式常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只需要关注业务方面的开发。除此之外,代理模式还可以用在 RPC、缓存等应用场景中。
+
+
+#### 4.2 桥接模式
+- 桥接模式的代码实现非常简单,但是理解起来稍微有点难度,并且应用场景也比较局限,所以,相对来说,桥接模式在实际的项目中并没有那么常用,你只需要简单了解,见到能认识就可以了。
+- 桥接模式有两种理解方式。第一种理解方式是“将抽象和实现解耦,让它们能独立开发”。这种理解方式比较特别,应用场景也不多。另一种理解方式更加简单,等同于“组合优于继承”设计原则,这种理解方式更加通用,应用场景比较多。不管是哪种理解方式,它们的代码结构都是相同的,都是一种类之间的组合关系。
+- 对于第一种理解方式,弄懂定义中“抽象”和“实现”两个概念,是理解它的关键。定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是的一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系组装在一起。
+
+
+
+#### 4.3 装饰器模式
+- 装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这样的需求,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
+
+
+
+#### 4.4 适配器模式
+- 代理模式、装饰器模式提供的都是跟原始类相同的接口,而适配器提供跟原始类不同的接口。适配器模式是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
+- 适配器模式是一种事后的补救策略,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。如果在设计初期,我们就能规避接口不兼容的问题,那这种模式就无用武之地了。在实际的开发中,什么情况下才会出现接口不兼容呢?我总结下了下面这 5 种场景:
+    - 封装有缺陷的接口
+    - 设计统一多个类的接口
+    - 设计替换依赖的外部系统
+    - 兼容老版本接口
+    - 适配不同格式的数据
+
+
+
+#### 4.5 组合模式
+- 组合模式跟我们之前讲的面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。这里讲的“组合模式”,主要是用来处理树形结构数据。正因为其应用场景的特殊性,数据必须能表示成树形结构,这也导致了这种模式在实际的项目开发中并不那么常用。但是,一旦数据满足树形结构,应用这种模式就能发挥很大的作用,能让代码变得非常简洁。
+- 组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。组合模式,将一组对象组织成树形结构,将单个对象和组合对象都看作树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。
+
+
+
+#### 4.6 享元模式
+- 所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
+- 具体来讲,当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引用这些享元。
+
+
+
+
+

+ 49 - 0
ReadDesign/01.设计模式导读/05.认识设计模式.md

@@ -0,0 +1,49 @@
+
+
+
+
+
+
+很多程序员都已经意识到基础知识的重要性,觉得要夯实基础,才能走得更远,但同时对于如何将基础知识转化成开发“生产力”仍然有些疑惑。所以,你可能看了很多基础的书籍,比如操作系统、组成原理、编译原理等,但还是觉得很迷茫,觉得在开发中用不上,起码在平时的 CRUD 业务开发中用不上。实际上,这些基础的知识确实很难直接转化成开发“生产力”。但是,它能潜移默化地、间接地提高你对技术的理解。
+不过,我觉得,设计模式和操作系统、组成原理、编译原理等这些基础学科是不一样的。它虽然也算是一门基础知识,但是它和数据结构、算法更像是一道儿的,相比那些更加基础的学科,设计模式能更直接地提高你的开发能力。我在开篇词里也说了,如果说数据结构和算法是教你如何写出高效代码,那设计模式讲的是如何写出可扩展、可读、可维护的高质量代码,所以,它们跟平时的编码会有直接的关系,也会直接影响到你的开发能力。
+不过,你可能还是会觉得设计模式是把屠龙刀,看起来很厉害,但平时的开发根本用不上。基于这种观点,接下来,我们就具体地聊一聊,我们为什么要学习设计模式?
+
+应对面试中的设计模式相关问题
+学习设计模式和算法一样,最功利、最直接的目的,可能就是应对面试了。
+不管你是前端工程师、后端工程师,还是全栈工程师,在求职面试中,设计模式问题是被问得频率比较高的一类问题。特别是一些像 BAT、TMD 这样的大公司,比较重视候选人的基本功,经常会拿算法、设计模式之类的问题来考察候选人。
+所以,我在求职面试的时候,都会提前准备、温习一遍设计模式。尽管并不是每次面试都会被问到,但一旦被问到,如果回答得不好,就是一个败笔,这场面试基本上也就凉凉了。所以,为了保证万无一失,摆脱一旦被问到答不出来的窘境,对于设计模式这种大概率被问到的问题,我都会未雨绸缪,提前准备一下。
+当然,我并不是临时抱佛脚。我平时就比较重视设计模式相关知识的积累,所以底子比较好,只需要在每次面试前花很短的时间,重新温习一下,便可以自信满满地去面试,而不是心里老是担心被问到,影响正常的面试发挥。
+
+告别写被人吐槽的烂代码
+我们经常说,“Talk is cheap,show me the code。”实际上,代码能力是一个程序员最基础的能力,是基本功,是展示一个程序员基础素养的最直接的衡量标准。你写的代码,实际上就是你名片。
+尽管我已经工作近十年,但我一直没有脱离编码一线,现在每天也都在坚持写代码、review 指导同事写代码、重构遗留系统的烂代码。这些年的工作经历中,我见过太多的烂代码,比如命名不规范、类设计不合理、分层不清晰、没有模块化概念、代码结构混乱、高度耦合等等。这样的代码维护起来非常费劲,添加或者修改一个功能,常常会牵一发而动全身,让你无从下手,恨不得将全部的代码删掉重写!
+当然,在这些年的工作经历中,我也看到过很多让我眼前一亮的代码。每当我看到这样的好代码,都会立刻对作者产生无比的好感和认可。且不管这个人处在公司的何种级别,从代码就能看出,他是一个基础扎实的高潜员工,值得培养,前途无量!因此,代码写得好,能让你在团队中脱颖而出。
+所以,我的专栏,不仅仅只是讲解设计模式,更加重要的是,我会通过实战例子,手把手教你如何避免刚刚提到的代码问题,告别被人诟病的烂代码,写出令人称道的好代码,成为团队中的代码标杆!而且,写出一份漂亮的代码,你自己也会很有成就感。
+
+提高复杂代码的设计和开发能力
+大部分工程师比较熟悉的都是编程语言、工具、框架这些东西,因为每天的工作就是在框架里根据业务需求,填充代码。实际上,我刚工作的时候,也是做这类事情。相对来说,这样的工作并不需要你具备很强的代码设计能力,只要单纯地能理解业务,翻译成代码就可以了。
+但是,有一天,我的 leader 让我开发一个跟业务无关的比较通用的功能模块,面对这样稍微复杂的代码设计和开发,我就发现我有点力不从心,不知从何下手了。因为我知道只是完成功能、代码能用,可能并不复杂,但是要想写出易扩展、易用、易维护的代码,并不容易。
+如何分层、分模块?应该怎么划分类?每个类应该具有哪些属性、方法?怎么设计类之间的交互?该用继承还是组合?该使用接口还是抽象类?怎样做到解耦、高内聚低耦合?该用单例模式还是静态方法?用工厂模式创建对象还是直接 new 出来?如何避免引入设计模式提高扩展性的同时带来的降低可读性问题?……各种问题,一下子挤到了我面前。
+而我当时并没有对设计模式相关的知识(包括设计模式、设计原则、面向对象设计思想等)有太多的了解和积累,所以一时间搞得我手足无措。好在因此我意识到了这方面知识的重要性,所以在之后很多年的开发中,我都一直刻意锻炼、积累这方面的能力。面对复杂代码、功能、系统的设计和开发,我也越来越得心应手,游刃有余。写出高质量代码已经成为了我的习惯,不经意间写出来的代码,都能作为同事学习、临摹的范例,这也成为了我职场中最引以为豪的亮点之一。
+
+让读源码、学框架事半功倍
+对于一个有追求的程序员来说,对技术的积累,既要有广度,也要有深度。很多技术人早早就意识到了这一点,所以在学习框架、中间件的时候,都会抽空去研究研究原理,读一读源码,希望能在深度上有所积累,而不只是略知皮毛,会用而已。
+从我的经验和同事的反馈来看,有些人看源码的时候,经常会遇到看不懂、看不下去的问题。不知道你有没有遇到过这种情况?实际上,这个问题的原因很简单,那就是你积累的基本功还不够,你的能力还不足以看懂这些代码。为什么我会这么说呢?
+优秀的开源项目、框架、中间件,代码量、类的个数都会比较多,类结构、类之间的关系极其复杂,常常调用来调用去。所以,为了保证代码的扩展性、灵活性、可维护性等,代码中会使用到很多设计模式、设计原则或者设计思想。如果你不懂这些设计模式、原则、思想,在看代码的时候,你可能就会琢磨不透作者的设计思路,对于一些很明显的设计思路,你可能要花费很多时间才能参悟。相反,如果你对设计模式、原则、思想非常了解,一眼就能参透作者的设计思路、设计初衷,很快就可以把脑容量释放出来,重点思考其他问题,代码读起来就会变得轻松了。
+实际上,除了看不懂、看不下去的问题,还有一个隐藏的问题,你可能自己都发现不了,那就是你自己觉得看懂了,实际上,里面的精髓你并没有 get 到多少!因为优秀的开源项目、框架、中间件,就像一个集各种高精尖技术在一起的战斗机。如果你想剖析它的原理、学习它的技术,而你没有积累深厚的基本功,就算把这台战斗机摆在你面前,你也不能完全参透它的精髓,只是了解个皮毛,看个热闹而已。
+因此,学好设计模式相关的知识,不仅能让你更轻松地读懂开源项目,还能更深入地参透里面的技术精髓,做到事半功倍。
+
+为你的职场发展做铺垫
+普通的、低级别的开发工程师,只需要把框架、开发工具、编程语言用熟练,再做几个项目练练手,基本上就能应付平时的开发工作了。但是,如果你不想一辈子做一个低级的码农,想成长为技术专家、大牛、技术 leader,希望在职场有更高的成就、更好的发展,那就要重视基本功的训练、基础知识的积累。
+你去看大牛写的代码,或者优秀的开源项目,代码写得都非常的优美,质量都很高。如果你只是框架用得很溜,架构聊得头头是道,但写出来的代码很烂,让人一眼就能看出很多不合理的、可以改进的地方,那你永远都成不了别人心目中的“技术大牛”。
+再者,在技术这条职场道路上,当成长到一定阶段之后,你势必要承担一些指导培养初级员工、新人,以及 code review 的工作。这个时候,如果你自己都对“什么是好的代码?如何写出好的代码?”不了解,那又该如何指导别人,如何让人家信服呢?
+还有,如果你是一个技术 leader,负责一个项目整体的开发工作,你就需要为开发进度、开发效率和项目质量负责。你也不希望团队堆砌垃圾代码,让整个项目无法维护,添加、修改一个功能都要费老大劲,最终拉低整个团队的开发效率吧?
+除此之外,代码质量低还会导致线上 bug 频发,排查困难。整个团队都陷在成天修改无意义的低级 bug、在烂代码中添补丁的事情中。而一个设计良好、易维护的系统,可以解放我们的时间,让我们做些更加有意义、更能提高自己和团队能力的事情。
+最后,当你成为 leader、或者团队中的资深工程师、技术专家之后,你势必要负责一部分团队的招聘工作。这个时候,如果你要考察候选人的设计能力、代码能力,那设计模式相关的问题便是一个很好的考察点。
+
+
+
+
+
+
+

+ 101 - 0
ReadDesign/01.设计模式导读/06.代码质量评判.md

@@ -0,0 +1,101 @@
+#### 目录介绍
+- 01.对代码认识
+- 02.切勿笼统评判
+- 03.常用评价标准
+
+ 
+
+### 01.对代码认识
+- 有一些工程师对如何评价代码质量有所认识,比如,好代码是易扩展、易读、简单、易维护的等等,但他们对于这些评价的理解往往只停留在表面概念上,对于诸多更深入的问题,比如,“怎么才算可读性好?什么样的代码才算易扩展、易维护?可读、可扩展与可维护之间有什么关系?可维护中‘维护’两字该如何理解?”等等,并没有太清晰的认识。
+- 对于程序员来说,辨别代码写得“好”还是“烂”,是一个非常重要的能力。这也是我们写出好代码的前提。毕竟,如果我们连什么是好代码、什么是烂代码,都分辨不清,又谈何写出好代码呢?
+
+
+
+### 02.切勿笼统评判
+- 常说的“好”和“烂”,是对代码质量的一种描述。“好”笼统地表示代码质量高,“烂”笼统地表示代码质量低。对于代码质量的描述,除了“好”“烂”这样比较简单粗暴的描述方式之外,我们也经常会听到很多其他的描述方式。这些描述方法语义更丰富、更专业、更细化。我搜集整理了一下,罗列在了下面。这些几乎涵盖我们所能听到的描述代码质量的所有常用词汇,你可以看一看。
+- 灵活性(flexibility)、可扩展性(extensibility)、可维护性(maintainability)、可读性(readability)、可理解性(understandability)、易修改性(changeability)、可复用(reusability)、可测试性(testability)、模块化(modularity)、高内聚低耦合(high cohesion loose coupling)、高效(high effciency)、高性能(high performance)、安全性(security)、兼容性(compatibility)、易用性(usability)、整洁(clean)、清晰(clarity)、简单(simple)、直接(straightforward)、少即是多(less code is more)、文档详尽(well-documented)、分层清晰(well-layered)、正确性(correctness、bug free)、健壮性(robustness)、鲁棒性(robustness)、可用性(reliability)、可伸缩性(scalability)、稳定性(stability)、优雅(elegant)、好(good)、坏(bad)……
+- 看到如此多的描述词,你可能要问了,我们到底该用哪些词来描述一段代码的质量呢?
+- 实际上,我们很难通过其中的某个或者某几个词汇来全面地评价代码质量。因为这些词汇都是从不同维度来说的。这就好比,对于一个人的评价,我们需要综合各个方面来给出,比如性格、相貌、能力、财富等等。代码质量高低也是一个综合各种因素得到的结论。我们并不能通过单一的维度去评价一段代码写的好坏。比如,即使一段代码的可扩展性很好,但可读性很差,那我们也不能说这段代码质量高。
+- 除此之外,不同的评价维度也并不是完全独立的,有些是具有包含关系、重叠关系或者可以互相影响的。比如,代码的可读性好、可扩展性好,就意味着代码的可维护性好。而且,各种评价维度也不是非黑即白的。比如,我们不能简单地将代码分为可读与不可读。如果用数字来量化代码的可读性的话,它应该是一个连续的区间值,而非 0、1 这样的离散值。
+- 不过,我们真的可以客观地量化一段代码质量的高低吗?答案是否定的。对一段代码的质量评价,常常有很强的主观性。比如,怎么样的代码才算可读性好,每个人的评判标准都不大一样。这就好比我们去评价一本小说写得是否精彩,本身就是一个很难量化的、非常主观的事情。
+- 正是因为代码质量评价的主观性,使得这种主观评价的准确度,跟工程师自身经验有极大的关系。越是有经验的工程师,给出的评价也就越准确。相反,资历比较浅的工程师就常常会觉得,没有一个可执行的客观的评价标准作为参考,很难准确地判断一段代码写得好与坏。有的时候,自己觉得代码写得已经够好了,但实际上并不是。所以,这也导致如果没有人指导的话,自己一个人闷头写代码,即便写再多的代码,代码能力也可能一直没有太大提高。
+
+
+
+### 03.常用评价标准
+- 仔细看前面罗列的所有代码质量评价标准,你会发现,有些词语过于笼统、抽象,比较偏向对于整体的描述,比如优雅、好、坏、整洁、清晰等;有些过于细节、偏重方法论,比如模块化、高内聚低耦合、文档详尽、分层清晰等;有些可能并不仅仅局限于编码,跟架构设计等也有关系,比如可伸缩性、可用性、稳定性等。
+- 为了做到有的放矢、有重点地学习,我挑选了其中几个最常用的、最重要的评价标准,来详细讲解,其中就包括:可维护性、可读性、可扩展性、灵活性、简洁性(简单、复杂)、可复用性、可测试性。接下来,我们逐一讲解一下。
+
+
+#### 3.1可维护性(maintainability)
+- 我们首先来看,什么是代码的“可维护性”?所谓的“维护代码”到底包含哪些具体工作?
+- 落实到编码开发,所谓的“维护”无外乎就是修改 bug、修改老的代码、添加新的代码之类的工作。所谓“代码易维护”就是指,在不破坏原有代码设计、不引入新的 bug 的情况下,能够快速地修改或者添加代码。所谓“代码不易维护”就是指,修改或者添加代码需要冒着极大的引入新 bug 的风险,并且需要花费很长的时间才能完成。
+- 我们知道,对于一个项目来说,维护代码的时间远远大于编写代码的时间。工程师大部分的时间可能都是花在修修 bug、改改老的功能逻辑、添加一些新的功能逻辑之类的工作上。所以,代码的可维护性就显得格外重要。
+- 维护、易维护、不易维护这三个概念不难理解。不过,对于实际的软件开发来说,更重要的是搞清楚,如何来判断代码可维护性的好坏。
+- 实际上,可维护性也是一个很难量化、偏向对代码整体的评价标准,它有点类似之前提到的“好”“坏”“优雅”之类的笼统评价。代码的可维护性是由很多因素协同作用的结果。代码的可读性好、简洁、可扩展性好,就会使得代码易维护;相反,就会使得代码不易维护。更细化地讲,如果代码分层清晰、模块化好、高内聚低耦合、遵从基于接口而非实现编程的设计原则等等,那就可能意味着代码易维护。除此之外,代码的易维护性还跟项目代码量的多少、业务的复杂程度、利用到的技术的复杂程度、文档是否全面、团队成员的开发水平等诸多因素有关。
+- 所以,从正面去分析一个代码是否易维护稍微有点难度。不过,我们可以从侧面上给出一个比较主观但又比较准确的感受。如果 bug 容易修复,修改、添加功能能够轻松完成,那我们就可以主观地认为代码对我们来说易维护。相反,如果修改一个 bug,修改、添加一个功能,需要花费很长的时间,那我们就可以主观地认为代码对我们来说不易维护。
+- 你可能会说,这样的评价方式也太主观了吧?没错,是否易维护本来就是针对维护的人来说的。不同水平的人对于同一份代码的维护能力并不是相同的。对于同样一个系统,熟悉它的资深工程师会觉得代码的可维护性还不错,而一些新人因为不熟悉代码,修改 bug、修改添加代码要花费很长的时间,就有可能会觉得代码的可维护性不那么好。这实际上也印证了我们之前的观点:代码质量的评价有很强的主观性。
+
+
+
+
+#### 3.2 可读性(readability)
+
+软件设计大师 Martin Fowler 曾经说过:“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”翻译成中文就是:“任何傻瓜都会编写计算机能理解的代码。好的程序员能够编写人能够理解的代码。”Google 内部甚至专门有个认证就叫作 Readability。只有拿到这个认证的工程师,才有资格在 code review 的时候,批准别人提交代码。可见代码的可读性有多重要,毕竟,代码被阅读的次数远远超过被编写和执行的次数。
+
+我个人认为,代码的可读性应该是评价代码质量最重要的指标之一。我们在编写代码的时候,时刻要考虑到代码是否易读、易理解。除此之外,代码的可读性在非常大程度上会影响代码的可维护性。毕竟,不管是修改 bug,还是修改添加功能代码,我们首先要做的事情就是读懂代码。代码读不大懂,就很有可能因为考虑不周全,而引入新的 bug。
+
+既然可读性如此重要,那我们又该如何评价一段代码的可读性呢?
+
+我们需要看代码是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等。你应该也能感觉到,从正面上,我们很难给出一个覆盖所有评价指标的列表。这也是我们无法量化可读性的原因。
+
+实际上,code review 是一个很好的测验代码可读性的手段。如果你的同事可以轻松地读懂你写的代码,那说明你的代码可读性很好;如果同事在读你的代码时,有很多疑问,那就说明你的代码可读性有待提高了。
+
+#### 3.3 可扩展性(extensibility)
+
+可扩展性也是一个评价代码质量非常重要的标准。它表示我们的代码应对未来需求变化的能力。跟可读性一样,代码是否易扩展也很大程度上决定代码是否易维护。那到底什么是代码的可扩展性呢?
+
+代码的可扩展性表示,我们在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。说直白点就是,代码预留了一些功能扩展点,你可以把新功能代码,直接插到扩展点上,而不需要因为要添加一个功能而大动干戈,改动大量的原始代码。
+
+关于代码的扩展性,在后面讲到“对修改关闭,对扩展开放”这条设计原则的时候,我会来详细讲解,今天我们只需要知道,代码的可扩展性是评价代码质量非常重要的标准就可以了。
+
+#### 3.4 灵活性(flexibility)
+
+灵活性也是描述代码质量的一个常用词汇。比如我们经常会听到这样的描述:“代码写得很灵活”。那这里的“灵活”该如何理解呢?
+
+尽管有很多人用这个词汇来描述代码的质量。但实际上,灵活性是一个挺抽象的评价标准,要给灵活性下个定义也是挺难的。不过,我们可以想一下,什么情况下我们才会说代码写得好灵活呢?我这里罗列了几个场景,希望能引发你自己对什么是灵活性的思考。
+
+当我们添加一个新的功能代码的时候,原有的代码已经预留好了扩展点,我们不需要修改原有的代码,只要在扩展点上添加新的代码即可。这个时候,我们除了可以说代码易扩展,还可以说代码写得好灵活。
+
+当我们要实现一个功能的时候,发现原有代码中,已经抽象出了很多底层可以复用的模块、类等代码,我们可以拿来直接使用。这个时候,我们除了可以说代码易复用之外,还可以说代码写得好灵活。
+
+当我们使用某组接口的时候,如果这组接口可以应对各种使用场景,满足各种不同的需求,我们除了可以说接口易用之外,还可以说这个接口设计得好灵活或者代码写得好灵活。
+
+从刚刚举的场景来看,如果一段代码易扩展、易复用或者易用,我们都可以称这段代码写得比较灵活。所以,灵活这个词的含义非常宽泛,很多场景下都可以使用。
+
+#### 3.5 简洁性(simplicity)
+
+有一条非常著名的设计原则,你一定听过,那就是 KISS 原则:“Keep It Simple,Stupid”。这个原则说的意思就是,尽量保持代码简单。代码简单、逻辑清晰,也就意味着易读、易维护。我们在编写代码的时候,往往也会把简单、清晰放到首位。
+
+不过,很多编程经验不足的程序员会觉得,简单的代码没有技术含量,喜欢在项目中引入一些复杂的设计模式,觉得这样才能体现自己的技术水平。实际上,思从深而行从简,真正的高手能云淡风轻地用最简单的方法解决最复杂的问题。这也是一个编程老手跟编程新手的本质区别之一。
+
+除此之外,虽然我们都能认识到,代码要尽量写得简洁,符合 KISS 原则,但怎么样的代码才算足够简洁?不是每个人都能很准确地判断出来这一点。所以,在后面的章节中,当我们讲到 KISS 原则的时候,我会通过具体的代码实例,详细给你解释,“为什么 KISS 原则看似非常简单、好理解,但实际上用好并不容易”。今天,我们就暂且不展开详细讲解了。
+
+#### 3.6 可复用性(reusability)
+
+代码的可复用性可以简单地理解为,尽量减少重复代码的编写,复用已有的代码。在后面的很多章节中,我们都会经常提到“可复用性”这一代码评价标准。
+
+比如,当讲到面向对象特性的时候,我们会讲到继承、多态存在的目的之一,就是为了提高代码的可复用性;当讲到设计原则的时候,我们会讲到单一职责原则也跟代码的可复用性相关;当讲到重构技巧的时候,我们会讲到解耦、高内聚、模块化等都能提高代码的可复用性。可见,可复用性也是一个非常重要的代码评价标准,是很多设计原则、思想、模式等所要达到的最终效果。
+
+实际上,代码可复用性跟 DRY(Don’t Repeat Yourself)这条设计原则的关系挺紧密的,所以,在后面的章节中,当我们讲到 DRY 设计原则的时候,我还会讲更多代码复用相关的知识,比如,“有哪些编程方法可以提高代码的复用性”等。
+
+
+#### 3.7 可测试性(testability)
+
+相对于前面六个评价标准,代码的可测试性是一个相对较少被提及,但又非常重要的代码质量评价标准。代码可测试性的好坏,能从侧面上非常准确地反应代码质量的好坏。代码的可测试性差,比较难写单元测试,那基本上就能说明代码设计得有问题。关于代码的可测试性,我们在重构那一部分,会花两节课的时间来详细讲解。现在,你暂时只需要知道,代码的可测试性非常重要就可以了。
+
+
+
+
+
+

+ 883 - 0
ReadDesign/02.设计模式原则/01.面向对象六大原则.md

@@ -0,0 +1,883 @@
+#### 目录介绍
+- 00.面向对象六大原则
+- 01.代码单一职责原则
+- 02.代码开放封闭原则
+- 03.代码里氏替换原则
+- 04.代码依赖倒置原则
+- 05.代码接口隔离原则
+- 06.代码迪米特原则
+
+
+
+### 00.面向对象六大原则
+- 单一职责原则
+    - 指一个类的功能要单一,不能包罗万象。
+- 开放封闭原则
+    - 指一个模块在扩展性方面应是开放的,在更改性方面应是封闭的
+- 替换原则
+    - 子类应当可以替换父类,并出现在父类能够出现的任何位置
+- 依赖原则
+    - 具体依赖抽象,上层依赖下层
+- 接口分离原则
+    - 模块间要通过抽象接口隔开,而不是通过具体的类强行耦合起来。
+- 迪米特法则
+    - 又称最少知道原则(Demeter Principle,DP)
+    - 最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
+- 如何运用
+    - 结合一个实际开发案例,系统理解这六大原则如何提高代码的健壮性。
+
+
+### 01.代码单一职责原则
+#### 1.1 单一职责定义
+- 单一职责原则的英文名称是Single Responsibility Principle,简称SRP。
+- 它的定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。就像秦小波老师在《设计模式之禅》中说的:“这是一个备受争议却又及其重要的原则。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的”。因为单一职责的划分界限并不是总是那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。 
+
+
+
+#### 1.2 实现图片加载
+- 小民的主管是个工作经验丰富的技术专家,对于小民的工作并不是很满意,尤其小民最薄弱的面向对象设计,而Android开发又是使用Java语言,什么抽象、接口、六大原则、23种设计模式等名词把小民弄得晕头转向。小民自己也察觉到了自己的问题所在,于是,小民的主管决定先让小民做一个小项目来锻炼锻炼这方面的能力。
+- 在经过一番思考之后,主管挑选了使用范围广、难度也适中的ImageLoader(图片加载)作为小民的训练项目。既然要训练小民的面向对象设计,那么就必须考虑到可扩展性、灵活性,而检测这一切是否符合需求的最好途径就是开源。用户不断地提出需求、反馈问题,小民的项目需要不断升级以满足用户需求,并且要保证系统的稳定性、灵活性。
+- 挑战总是要面对的,何况是从来不服输的小民。主管的要求很简单,要小民实现图片加载,并且要将图片缓存起来。在分析了需求之后,小民一下就放心下来了,“这么简单,原来我还以为很难呢……”小民胸有成足的喃喃自语。在经历了十分钟的编码之后,小民写下了如下代码:
+    ```java
+    /**
+     * 图片加载类
+     */
+    public class ImageLoader {
+        // 图片缓存
+        LruCache<String, Bitmap> mImageCache;
+        // 线程池,线程数量为CPU的数量
+        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
+    
+        public ImageLoader() {
+            initImageCache();
+        }
+    
+        private void initImageCache() {
+                // 计算可使用的最大内存
+            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+                // 取四分之一的可用内存作为缓存
+            final int cacheSize = maxMemory / 4;
+            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
+                @Override
+                protected int sizeOf(String key, Bitmap bitmap) {
+                    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
+                }
+            };
+        }                   
+    
+        public  void displayImage(final String url, final ImageView imageView) {
+            imageView.setTag(url);
+            mExecutorService.submit(new Runnable() {
+    
+               @Override
+                public  void run() {
+                  Bitmap bitmap = downloadImage(url);
+                    if (bitmap == null) {
+                        return;
+                    }
+                    if (imageView.getTag().equals(url)) {
+                        imageView.setImageBitmap(bitmap);
+                    }
+                    mImageCache.put(url, bitmap);
+              }
+           });
+        }
+    
+        public  Bitmap downloadImage(String imageUrl) {
+            Bitmap bitmap = null;
+            try {
+                URL url = newURL(imageUrl);
+                final HttpURLConnection conn =         
+                    (HttpURLConnection)url.openConnection();
+                bitmap = BitmapFactory.decodeStream(
+                      conn.getInputStream());
+                conn.disconnect();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+    
+            return bitmap;
+        }
+    }
+    ```
+- 在小民给主管报告了ImageLoader的发布消息的几分钟之后,主管就把小民叫到了会议室。这下小民纳闷了,怎么夸人还需要到会议室。“小民,你的ImageLoader耦合太严重啦!简直就没有设计可言,更不要说扩展性、灵活性了。所有的功能都写在一个类里怎么行呢,这样随着功能的增多,ImageLoader类会越来越大,代码也越来越复杂,图片加载系统就越来越脆弱……”这简直就是当头棒喝,小民的脑海里已经听不清主管下面说的内容了,只是觉得自己之前没有考虑清楚就匆匆忙忙完成任务,而且把任务想得太简单了。
+- “你还是把ImageLoader拆分一下,把各个功能独立出来,让它们满足单一职责原则。”主管最后说道。小民是个聪明人,敏锐地捕捉到了单一职责原则这个关键词。用Google搜索了一些优秀资料之后总算是对单一职责原则有了一些认识。于是打算对ImageLoader进行一次重构。ImageLoader代码修改如下所示:
+    ```java
+    /**
+     * 图片加载类
+     */
+    public  class ImageLoader {
+        // 图片缓存
+        ImageCache mImageCache = new ImageCache() ;
+        // 线程池,线程数量为CPU的数量
+        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
+    
+        // 加载图片
+        public  void displayImage(final String url, final ImageView imageView) {
+            Bitmap bitmap = mImageCache.get(url);
+            if (bitmap != null) {
+                imageView.setImageBitmap(bitmap);
+                return;
+            }
+            imageView.setTag(url);
+            mExecutorService.submit(new Runnable() {
+                @Override
+                public void run() {
+                Bitmap bitmap = downloadImage(url);
+                    if (bitmap == null) {
+                        return;
+                    }
+                    if (imageView.getTag().equals(url)) {
+                        imageView.setImageBitmap(bitmap);
+                    }
+                    mImageCache.put(url, bitmap);
+                }
+            });
+         }
+    
+        public  Bitmap downloadImage(String imageUrl) {
+            Bitmap bitmap = null;
+            try {
+                URL url = new URL(imageUrl);
+                final HttpURLConnection conn = 
+                (HttpURLConnection) 
+                            url.openConnection();
+                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
+                conn.disconnect();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            return bitmap;
+        }
+    }   
+    ```
+- 并且添加了一个ImageCache类用于处理图片缓存,具体代码如下:
+    ```java
+    public class ImageCache {
+        // 图片LRU缓存
+        LruCache<String, Bitmap> mImageCache;
+    
+        public ImageCache() {
+            initImageCache();
+        }
+    
+        private void initImageCache() {
+             // 计算可使用的最大内存
+            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+            // 取四分之一的可用内存作为缓存
+            final int cacheSize = maxMemory / 4;
+            mImageCache = new LruCache<String, Bitmap>(cacheSize) {
+    
+                @Override
+                protected int sizeOf(String key, Bitmap bitmap) {
+                    return bitmap.getRowBytes() *  
+                        bitmap.getHeight() / 1024;
+               }
+            };
+         }
+    
+        public void put(String url, Bitmap bitmap) {
+            mImageCache.put(url, bitmap) ;
+        }
+    
+        public Bitmap get(String url) {
+            return mImageCache.get(url) ;
+        }
+    }
+    ```
+- 小民将ImageLoader一拆为二,ImageLoader只负责图片加载的逻辑,而ImageCache只负责处理图片缓存的逻辑,这样ImageLoader的代码量变少了,职责也清晰了,当与缓存相关的逻辑需要改变时,不需要修改ImageLoader类,而图片加载的逻辑需要修改时也不会影响到缓存处理逻辑。主管在审核了小民的第一次重构之后,对小民的工作给予了表扬,大致意思是结构变得清晰了许多,但是可扩展性还是比较欠缺,虽然没有得到主管的完全肯定,但也是颇有进步,再考虑到自己确实有所收获,小民原本沮丧的心里也略微地好转起来。
+- 从上述的例子中我们能够体会到,单一职责所表达出的用意就是“单一”二字。正如上文所说,如何划分一个类、一个函数的职责,每个人都有自己的看法,这需要根据个人经验、具体的业务逻辑而定。但是,它也有一些基本的指导原则,例如,两个完全不一样的功能就不应该放在一个类中。一个类中应该是一组相关性很高的函数、数据的封装。工程师可以不断地审视自己的代码,根据具体的业务、功能对类进行相应的拆分,我想这会是你优化代码迈出的第一步。
+
+
+
+### 02.代码开放封闭原则
+#### 2.1 开放封闭原则定义
+- 开闭原则的英文全称是Open Close Principle,简称OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。
+- 开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。
+- 软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。而产品需要升级,修改原来的代码就可能会引发其他的问题。那么如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是尽量遵守本章要讲述的开闭原则。
+
+
+#### 2.2 添加图片缓存本地
+- 在对ImageLoader进行了一次重构之后,小民的这个开源库获得了一些用户。小民第一次感受到自己发明“轮子”的快感,对开源的热情也越发高涨起来!通过动手实现一些开源库来深入学习相关技术,不仅能够提升自我,也能更好地将这些技术运用到工作中,从而开发出更稳定、优秀的应用,这就是小民的真实想法。
+- 小民第一轮重构之后的ImageLoader职责单一、结构清晰,不仅获得了主管的一点肯定,还得到了用户的夸奖,算是个不错的开始。随着用户的增多,有些问题也暴露出来了,小民的缓存系统就是大家“吐槽”最多的地方。通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当应用重新启动之后,原来已经加载过的图片将会丢失,这样重启之后就需要重新下载!这又会导致加载缓慢、耗费用户流量的问题。
+- 小民考虑引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了!小民在和主管讨论了该问题之后就投入了编程中,下面就是小民的代码。 DiskCache.java类,将图片缓存到SD卡中:
+    ```java
+    public class DiskCache {
+        // 为了简单起见临时写个路径,在开发中请避免这种写法 !
+        static String cacheDir = "sdcard/cache/";
+         // 从缓存中获取图片
+        public Bitmap get(String url) {
+            return BitmapFactory.decodeFile(cacheDir + url);
+        }
+    
+        // 将图片缓存到内存中
+        public  void  put(String url, Bitmap bmp) {
+           FileOutputStream fileOutputStream = null;
+            try {
+                fileOutputStream = new 
+                     FileOutputStream(cacheDir + url);
+                bmp.compress(CompressFormat.PNG, 
+                     100, fileOutputStream);
+          } catch (FileNotFoundException e) {
+                e.printStackTrace();
+          } final ly {
+                if (fileOutputStream != null) {
+                    try {
+                        fileOutputStream.close();
+                  } catch (IOException e) {
+                        e.printStackTrace();
+                 }
+              }
+          }
+        }
+    }
+    ```
+- 因为需要将图片缓存到SD卡中,所以,ImageLoader代码有所更新,具体代码如下:
+    ```java
+    public class ImageLoader {
+        // 内存缓存
+        ImageCache mImageCache = new ImageCache();
+        // SD卡缓存
+        DiskCache mDiskCache = new DiskCache();
+        // 是否使用SD卡缓存
+        boolean isUseDiskCache = false;
+        // 线程池,线程数量为CPU的数量
+        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
+    
+    
+        public  void displayImage(final String url, final ImageView imageView) {
+            // 判断使用哪种缓存
+           Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) 
+                    : mImageCache.get (url);
+            if (bitmap != null) {
+                imageView.setImageBitmap(bitmap);
+                return;
+           }
+            // 没有缓存,则提交给线程池进行下载
+        }
+    
+        public void useDiskCache(boolean useDiskCache) {
+            isUseDiskCache = useDiskCache ;
+        }
+    }
+    ```
+- 从上述的代码中可以看到,仅仅新增了一个DiskCache类和往ImageLoader类中加入了少量代码就添加了SD卡缓存的功能,用户可以通过useDiskCache方法来对使用哪种缓存进行设置,例如:
+    ```java
+    ImageLoader imageLoader = new ImageLoader() ;
+     // 使用SD卡缓存
+    imageLoader.useDiskCache(true);
+    // 使用内存缓存
+    imageLoader.useDiskCache(false);
+    ```
+- 通过useDiskCache方法可以让用户设置不同的缓存,非常方便啊!小民对此很满意,于是提交给主管做代码审核。“小民,你思路是对的,但是有些明显的问题,就是使用内存缓存时用户就不能使用SD卡缓存,类似的,使用SD卡缓存时用户就不能使用内存缓存。用户需要这两种策略的综合,首先缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络上获取,这才是最好的缓存策略。”主管真是一针见血,小民这时才如梦初醒,刚才还得意洋洋的脸上突然有些泛红…… 
+- 于是小民按照主管的指点新建了一个双缓存类DoudleCache,具体代码如下:
+    ```java
+    /**
+     * 双缓存。获取图片时先从内存缓存中获取,如果内存中没有缓存该图片,再从SD卡中获取。
+     *  缓存图片也是在内存和SD卡中都缓存一份
+     */
+    public class DoubleCache {
+        ImageCache mMemoryCache = new ImageCache();
+        DiskCache mDiskCache = new DiskCache();
+    
+        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
+        public   Bitmap get(String url) {
+           Bitmap bitmap = mMemoryCache.get(url);
+            if (bitmap == null) {
+                bitmap = mDiskCache.get(url);
+            }
+            return  bitmap;
+        }
+    
+        // 将图片缓存到内存和SD卡中
+        public void put(String url, Bitmap bmp) {
+            mMemoryCache.put(url, bmp);
+            mDiskCache.put(url, bmp);
+       }
+    }
+    ```
+- 我们再看看最新的ImageLoader类吧,代码更新也不多:
+    ```java
+    public class ImageLoader {
+        // 内存缓存
+        ImageCache mImageCache = new ImageCache();
+        // SD卡缓存
+        DiskCache mDiskCache = new DiskCache();
+        // 双缓存
+        DoubleCache mDoubleCache = new DoubleCache() ;
+        // 使用SD卡缓存
+        boolean isUseDiskCache = false;
+        // 使用双缓存
+        boolean isUseDoubleCache = false;
+        // 线程池,线程数量为CPU的数量
+        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
+    
+        public void displayImage(final String url, final ImageView imageView) {
+            Bitmap bmp = null;
+             if (isUseDoubleCache) {
+                bmp = mDoubleCache.get(url);
+            } else if (isUseDiskCache) {
+                bmp = mDiskCache.get(url);
+            } else {
+                bmp = mImageCache.get(url);
+            }
+    
+             if ( bmp != null ) {
+                imageView.setImageBitmap(bmp);
+            }
+            // 没有缓存,则提交给线程池进行异步下载图片
+        }
+    
+        public void useDiskCache(boolean useDiskCache) {
+            isUseDiskCache = useDiskCache ;
+        }
+    
+        public void useDoubleCache(boolean useDoubleCache) {
+            isUseDoubleCache = useDoubleCache ;
+        }
+    }
+    ```
+- 通过增加短短几句代码和几处修改就完成了如此重要的功能。小民已越发觉得自己Android开发已经到了的得心应手的境地,不仅感觉一阵春风袭来,他那飘逸的头发一下从他的眼前拂过,小民感觉今天天空比往常敞亮许多。
+- “小民,你每次加新的缓存方法时都要修改原来的代码,这样很可能会引入Bug,而且会使原来的代码逻辑变得越来越复杂,按照你这样的方法实现,用户也不能自定义缓存实现呀!”到底是主管水平高,一语道出了小民这缓存设计上的问题。
+- 我们还是来分析一下小民的程序,小民每次在程序中加入新的缓存实现时都需要修改ImageLoader类,然后通过一个布尔变量来让用户使用哪种缓存,因此,就使得在ImageLoader中存在各种if-else判断,通过这些判断来确定使用哪种缓存。随着这些逻辑的引入,代码变得越来越复杂、脆弱,如果小民一不小心写错了某个if条件(条件太多,这是很容易出现的),那就需要更多的时间来排除。整个ImageLoader类也会变得越来越臃肿。最重要的是用户不能自己实现缓存注入到ImageLoader中,可扩展性可是框架的最重要特性之一。
+- “软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放-关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。”小民的主管补充到,小民听得云里雾里的。
+- 小民似乎明白些什么,但是又不是太明确如何修改程序。主管看到小民这般模样只好亲自上阵,带着小民把ImageLoader进行了一次重构。具体代码如下:
+    ```java
+    public class ImageLoader {
+        // 图片缓存
+        ImageCache mImageCache = new MemoryCache();
+        // 线程池,线程数量为CPU的数量
+        ExecutorService mExecutorService = Executors.newFixedThreadPool (Runtime.getRuntime().availableProcessors());
+        // 注入缓存实现
+        public void setImageCache(ImageCache cache) {
+            mImageCache = cache;
+        }
+    
+        public void displayImage(String imageUrl, ImageView imageView) {
+            Bitmap bitmap = mImageCache.get(imageUrl);
+            if (bitmap != null) {
+                imageView.setImageBitmap(bitmap);
+                return;
+            }
+            // 图片没缓存,提交到线程池中下载图片
+            submitLoadRequest(imageUrl, imageView);
+        }
+    
+        private void submitLoadRequest(final String imageUrl,
+                 final ImageView imageView) {
+            imageView.setTag(imageUrl);
+            mExecutorService.submit(new Runnable() {
+    
+                @Override
+                public  void run() {
+                  Bitmap bitmap = downloadImage(imageUrl);
+                    if (bitmap == null) {
+                        return;
+                 }
+                   if (imageView.getTag().equals(imageUrl)) {
+                        imageView.setImageBitmap(bitmap);
+                 }
+                    mImageCache.put(imageUrl, bitmap);
+             }
+          });
+        }
+    
+        public  Bitmap downloadImage(String imageUrl) {
+           Bitmap bitmap = null;
+            try {
+               URL url = new URL(imageUrl);
+                final HttpURLConnection conn = (HttpURLConnection) 
+                            url.openConnection();
+                bitmap = BitmapFactory.decodeStream(conn.getInputStream());
+                conn.disconnect();
+            } catch (Exception e) {
+                  e.printStackTrace();
+            }
+    
+            return bitmap;
+        }
+    }
+    ```
+- 经过这次重构,没有了那么多的if-else语句,没有了各种各样的缓存实现对象、布尔变量,代码确实清晰、简单了很多,小民对主管的崇敬之情又“泛滥”了起来。需要注意的是,这里的ImageCache类并不是小民原来的那个ImageCache,这次程序重构主管把它提取成一个图片缓存的接口,用来抽象图片缓存的功能。我们看看该接口的声明:
+    ```java
+    public interface ImageCache {
+        public Bitmap get(String url);
+        public void put(String url, Bitmap bmp);
+    }
+    ```
+- ImageCache接口简单定义了获取、缓存图片两个函数,缓存的key是图片的url,值是图片本身。内存缓存、SD卡缓存、双缓存都实现了该接口,我们看看这几个缓存实现:
+    ```java
+    // 内存缓存MemoryCache类
+    public class MemoryCache implements ImageCache {
+        private LruCache<String, Bitmap> mMemeryCache;
+    
+        public MemoryCache() {
+            // 初始化LRU缓存
+        }
+    
+         @Override
+        public Bitmap get(String url) {
+            return mMemeryCache.get(url);
+        }
+    
+        @Override
+        public void put(String url, Bitmap bmp) {
+            mMemeryCache.put(url, bmp);
+        }
+    }
+    
+    // SD卡缓存DiskCache类
+    public  class  DiskCache implements ImageCache {
+        @Override
+        public Bitmap get(String url) {
+            return null/* 从本地文件中获取该图片 */;
+        }
+    
+        @Override
+        public void put(String url, Bitmap bmp) {
+            // 将Bitmap写入文件中
+        }
+    }
+    
+    // 双缓存DoubleCache类
+    public class DoubleCache implements ImageCache{
+        ImageCache mMemoryCache = new MemoryCache();
+        ImageCache mDiskCache = new DiskCache();
+    
+        // 先从内存缓存中获取图片,如果没有,再从SD卡中获取
+        public Bitmap get(String url) {
+           Bitmap bitmap = mMemoryCache.get(url);
+            if (bitmap == null) {
+                bitmap = mDiskCache.get(url);
+           }
+            return bitmap;
+         }
+    
+        // 将图片缓存到内存和SD卡中
+        public void put(String url, Bitmap bmp) {
+            mMemoryCache.put(url, bmp);
+            mDiskCache.put(url, bmp);
+        }
+    }
+    ```
+- 细心的朋友可能注意到了,ImageLoader类中增加了一个setImageCache(ImageCache cache)函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。下面就看看用户是如何设置缓存实现的:
+    ```java
+    ImageLoader imageLoader = new ImageLoader() ;
+            // 使用内存缓存
+    imageLoader.setImageCache(new MemoryCache());
+            // 使用SD卡缓存
+    imageLoader.setImageCache(new DiskCache());
+            // 使用双缓存
+    imageLoader.setImageCache(new DoubleCache());
+            // 使用自定义的图片缓存实现
+    imageLoader.setImageCache(new ImageCache() {
+    
+                @Override
+            public void put(String url, Bitmap bmp) {
+                // 缓存图片
+           }
+    
+                @Override
+            public Bitmap get(String url) {
+                return null/*从缓存中获取图片*/;
+           }
+        });
+    ```
+- 在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们的一个特点是都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通过setImageCache(ImageCache cache)注入到ImageLoader中,这样ImageLoader就实现了变化万千的缓存策略,而扩展这些缓存策略并不会导致ImageLoader类的修改。经过这次重构,小民的ImageLoader已经基本算合格了。咦!这不就是主管说的开闭原则么!“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。而遵循开闭原则的重要手段应该是通过抽象……”小民细声细语的念叨中,陷入了思索中……
+- 开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的,当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以使得代码恢复到正常的“进化”轨道,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。我们的开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。
+
+
+
+
+### 03.代码里氏替换原则
+#### 3.1 里氏替换原则定义
+- 里氏替换原则英文全称是Liskov Substitution Principle,简称LSP。
+- 它的第一种定义是:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。上面这种描述确实不太好理解,理论家有时候容易把问题抽象化,本来挺容易理解的事让他们一概括就弄得拗口了。我们再看看另一个直截了当的定义。里氏替换原则第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。
+- 我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。说了那么多,其实最终总结就两个字:抽象。 
+
+
+
+#### 3.2 通过案例代码理解
+- 小民为了深入地了解Android中的Window与View的关系特意写了一个简单示例,为了便于理解,我们看看具体的代码:
+    ```java
+    // 窗口类
+    public class Window {
+        public void show(View child){
+            child.draw();
+        }
+    }
+    
+    // 建立视图抽象,测量视图的宽高为公用代码,绘制交给具体的子类
+    public abstract class  View {
+        public abstract void  draw() ;
+        public void  measure(int width, int height){
+            // 测量视图大小
+        }
+    }
+    
+    // 按钮类的具体实现
+    public class Button extends View {
+        public void draw(){
+            // 绘制按钮
+        }
+    }
+    // TextView的具体实现
+    public class TextView extends View {
+        public void draw(){
+            // 绘制文本
+        }
+    }
+    ```
+- 上述示例中,Window依赖于View,而View定义了一个视图抽象,measure是各个子类共享的方法,子类通过覆写View的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以设置给show方法,也就我们所说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的View,然后传递给Window,Window负责组织View,并且将View显示到屏幕上。 
+里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显。 
+- 优点如下:
+    - (1)代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性;
+    - (2)子类与父类基本相似,但又与父类有所区别;
+    - (3)提高代码的可扩展性。
+- 继承的缺点:
+    - (1)继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
+    - (2)可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法。
+- 事物总是具有两面性,如何权衡利与弊都是需要根据具体场景来做出选择并加以处理。里氏替换原则指导我们构建扩展性更好的软件系统,我们还是接着上面的ImageLoader来做说明。 
+- 里氏替换原则,即MemoryCache、DiskCache、DoubleCache都可以替换ImageCache的工作,并且能够保证行为的正确性。ImageCache建立了获取缓存图片、保存缓存图片的接口规范,MemoryCache等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态地替换ImageLoader中的缓存策略。这就使得ImageLoader的缓存系统具有了无线的可能性,也就是保证了可扩展性。
+- 想象一个场景,当ImageLoader中的setImageCache(ImageCache cache)中的cache对象不能够被子类所替换,那么用户如何设置不同的缓存对象以及用户如何自定义自己的缓存实现,通过1.3节中的useDiskCache方法吗?显然不是的,里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。
+- 开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。
+
+
+### 04.代码依赖倒置原则
+#### 4.1 依赖倒置原则定义
+- 依赖倒置原则英文全称是Dependence Inversion Principle,简称DIP。
+- 依赖反转原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。这个概念有点不好理解,这到底是什么意思呢? 
+- 依赖倒置原则的几个关键点:
+    - (1)高层模块不应该依赖低层模块,两者都应该依赖其抽象;
+    - (2)抽象不应该依赖细节;
+    - (3)细节应该依赖抽象。
+- 在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。
+- 高层模块就是调用端,低层模块就是具体实现类。依赖倒置原则在Java语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。这又是一个将理论抽象化的实例,其实一句话就可以概括:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一,也就是上面两节强调的抽象。
+
+
+#### 4.2 运用依赖倒置原则
+- 如果在类与类直接依赖于细节,那么它们之间就有直接的耦合,当具体实现需要变化时,意味着在这要同时修改依赖者的代码,并且限制了系统的可扩展性。
+- 我们看ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现,而不是一个抽象类或者接口。这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,例如:
+    ```java
+    public class ImageLoader {
+        // 内存缓存 ( 直接依赖于细节 )
+        MemoryCache mMemoryCache = new MemoryCache();
+         // 加载图片到ImageView中
+        public void displayImage(String url, ImageView imageView) {
+           Bitmap bmp = mMemoryCache.get(url);
+            if (bmp == null) {
+                downloadImage(url, imageView);
+            } else {
+                imageView.setImageBitmap(bmp);
+            }
+        }
+        public void setImageCache(MemoryCache cache) {
+            mCache = cache ;
+        }
+        // 代码省略
+    }
+    ```
+- 随着产品的升级,用户发现MemoryCache已经不能满足需求,用户需要小民的ImageLoader可以将图片同时缓存到内存和SD卡中,或者可以让用户自定义实现缓存。此时,我们的MemoryCache这个类名不仅不能够表达内存缓存和SD卡缓存的意义,也不能够满足功能。另外,用户需要自定义缓存实现时还必须继承自MemoryCache,而用户的缓存实现可不一定与内存缓存有关,这在命名上的限制也让用户体验不好。重构的时候到了!小民的第一种方案是将MemoryCache修改为DoubleCache,然后在DoubleCache中实现具体的缓存功能。我们需要将ImageLoader修改如下:
+    ```java
+    public class ImageLoader {
+        // 双缓存 ( 直接依赖于细节 )
+        DoubleCache mCache = new DoubleCache();
+        // 加载图片到ImageView中
+        public void displayImage(String url, ImageView imageView) {
+           Bitmap bmp = mCache.get(url);
+            if (bmp == null) {
+              // 异步下载图片
+                downloadImageAsync(url, imageView);
+           } else {
+                imageView.setImageBitmap(bmp);
+           }
+        }
+    
+        public void setImageCache(DoubleCache cache) {
+             mCache = cache ;
+        }
+        // 代码省略
+    }
+    ```
+- 我们将MemoryCache修改成DoubleCache,然后修改了ImageLoader中缓存类的具体实现,轻轻松松就满足了用户需求。等等!这不还是依赖于具体的实现类(DoubleCache)吗?当用户的需求再次变化时,我们又要通过修改缓存实现类和ImageLoader代码来实现?修改原有代码不是违反了1.3节中的开闭原则吗?小民突然醒悟了过来,低下头思索着如何才能让缓存系统更灵活、拥抱变化……
+- 当然,这些都是在主管给出以及相应的代码之前,小民体验的煎熬过程。既然是这样,那显然主管给出的解决方案就能够让缓存系统更加灵活。一句话概括起来就是:依赖抽象,而不依赖具体实现。针对于图片缓存,主管建立的ImageCache抽象,该抽象中增加了get和put方法用以实现图片的存取。每种缓存实现都必须实现这个接口,并且实现自己的存取方法。当用户需要使用不同的缓存实现时,直接通过依赖注入即可,保证了系统的灵活性。我们再来简单回顾一下相关代码:ImageCache缓存抽象:
+    ```java
+    public interface ImageCache {
+        public Bitmap get(String url);
+        public void put(String url, Bitmap bmp);
+    }
+    ```
+- ImageLoader类:
+    ```java
+    public class ImageLoader {
+        // 图片缓存类,依赖于抽象,并且有一个默认的实现
+        ImageCache mCache = new MemoryCache();
+    
+        // 加载图片
+        public void displayImage(String url, ImageView imageView) {
+           Bitmap bmp = mCache.get(url);
+            if (bmp == null) {
+            // 异步加载图片
+                downloadImageAsync(url, imageView);
+           } else {
+                imageView.setImageBitmap(bmp);
+           }
+        }
+    
+        /**
+         * 设置缓存策略,依赖于抽象
+         */
+        public void setImageCache(ImageCache cache) {
+            mCache = cache;
+        }
+        // 代码省略
+    }
+    ```
+- 在这里,我们建立了ImageCache抽象,并且让ImageLoader依赖于抽象而不是具体细节。当需求发生变更时,小民只需要实现ImageCahce类或者继承其他已有的ImageCache子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader即可实现缓存功能的替换,这就保证了缓存系统的高可扩展性,拥有了拥抱变化的能力,而这一切的基本指导原则就是我们的依赖倒置原则。从上述几节中我们发现,要想让我们的系统更为灵活,抽象似乎成了我们唯一的手段。
+
+
+
+### 05.代码接口隔离原则
+#### 5.1 接口隔离原则定义
+- 接口隔离原则英文全称是InterfaceSegregation Principles,简称ISP。
+- 它的定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。
+
+
+#### 5.2 接口隔离法则的运用
+- 接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,这样说可能还是有点抽象,我们还是以一个示例来说明一下。在此之前我们来说一个场景,在Java 6以及之前的JDK版本,有一个非常讨厌的问题,那就是在使用了OutputStream或者其他可关闭的对象之后,我们必须保证它们最终被关闭了,我们的SD卡缓存类中就有这样的代码:
+    ```
+    // 将图片缓存到内存中
+    public void put(String url, Bitmap bmp) {
+        FileOutputStream fileOutputStream = null;
+        try {
+            fileOutputStream = new FileOutputStream(cacheDir + url);
+            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } finally {
+            if (fileOutputStream != null) {
+                try {
+                    fileOutputStream.close();
+              } catch (IOException e) {
+                    e.printStackTrace();
+              }
+           } // end if
+        } // end if finally
+    }
+    ```
+- 看到的这段代码可读性非常差,各种try…catch嵌套,都是些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。大家应该对这类代码也非常反感,那我们看看如何解决这类问题。 
+- 我们可能知道Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法。我们要讲的FileOutputStream类就实现了这个接口,我们从中可以看到,还有一百多个类实现了Closeable这个接口,这意味着,在关闭这一百多个类型的对象时,都需要写出像put方法中finally代码段那样的代码。这还了得!你能忍,反正小民是忍不了的!于是小民打算要发挥他的聪明才智解决这个问题,既然都是实现了Closeable接口,那只要我建一个方法统一来关闭这些对象不就可以了么?说干就干,于是小民写下来如下的工具类:
+    ```
+    public final class CloseUtils {
+    
+        Private CloseUtils() { }
+    
+        /**
+         * 关闭Closeable对象
+         * @param closeable
+         */
+        public static void closeQuietly(Closeable closeable) {
+            if (null != closeable) {
+                try {
+                    closeable.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+           }
+        }
+    }
+    ```
+- 再看看把这段代码运用到上述的put方法中的效果如何:
+    ```java
+    public void put(String url, Bitmap bmp) {
+        FileOutputStream fileOutputStream = null;
+        try {
+            fileOutputStream = new FileOutputStream(cacheDir + url);
+            bmp.compress(CompressFormat.PNG, 100, fileOutputStream);
+       } catch (FileNotFoundException e) {
+            e.printStackTrace();
+       } final ly {
+            CloseUtils.closeQuietly(fileOutputStream);
+       }
+    }
+    ```
+- 代码简洁了很多!而且这个closeQuietly方法可以运用到各类可关闭的对象中,保证了代码的重用性。CloseUtils的closeQuietly方法的基本原理就是依赖于Closeable抽象而不是具体实现,并且建立在最小化依赖原则的基础,它只需要知道这个对象是可关闭,其他的一概不关心,也就是这里的接口隔离原则。
+- 试想一下,如果在只是需要关闭一个对象时,它却暴露出了其他的接口函数,比如OutputStream的write方法,这就使得更多的细节暴露在客户端代码面前,不仅没有很好地隐藏实现,还增加了接口的使用难度。而通过Closeable接口将可关闭的对象抽象起来,这样只需要客户端依赖于Closeable就可以对客户端隐藏其他的接口信息,客户端代码只需要知道这个对象可关闭(只可调用close方法)即可。小民ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对ImageLoader具体的隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性,更高的灵活性。
+- Bob大叔(Robert C Martin)在21世纪早期将单一职责、开闭原则、里氏替换、接口隔离以及依赖倒置(也称为依赖反转)5个原则定义为SOLID原则,指代了面向对象编程的5个基本原则。当这些原则被一起应用时,它们使得一个软件系统更清晰、简单、最大程度地拥抱变化。SOLID被典型地应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发基本原则的重要组成部分。在经过第1.1~1.5节的学习之后,我们发现这几大原则最终就可以化为这几个关键词:抽象、单一职责、最小化。那么在实际开发过程中如何权衡、实践这些原则,是大家需要在实践中多思考与领悟,正所谓”学而不思则罔,思而不学则殆”,只有不断地学习、实践、思考,才能够在积累的过程有一个质的飞越。
+
+
+
+
+### 06.代码迪米特原则
+#### 6.1 迪米特原则定义
+- 迪米特原则英文全称为Law of Demeter,简称LOD,也称为最少知识原则(Least Knowledge Principle)。
+- 虽然名字不同,但描述的是同一个原则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现、如何复杂都与调用者或者依赖者没关系,调用者或者依赖者只需要知道他需要的方法即可,其他的我一概不关心。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
+- 迪米特法则还有一个英文解释是:Only talk to your immedate friends,翻译过来就是:只与直接的朋友通信。什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。
+
+
+
+#### 6.2 迪米特原则使用
+- 光说不练很抽象呐,下面我们就以租房为例来讲讲迪米特原则。 
+- “北漂”的同学比较了解,在北京租房绝大多数都是通过中介找房。我们设定的情境为:我只要求房间的面积和租金,其他的一概不管,中介将符合我要求的房子提供给我就可以。下面我们看看这个示例:
+    ```java
+    /**
+     * 房间
+     */
+    public class Room {
+        public float area;
+        public float price;
+    
+        public Room(float  area, float  price) {
+            this.area = area;
+            this.price = price;
+        }
+    
+        @Override
+        public String toString() {
+            return "Room [area=" + area + ", price=" + price + "]";
+        }
+    
+    }
+    
+    /**
+     * 中介
+     */
+    public class Mediator {
+        List<Room> mRooms = new ArrayList<Room>();
+    
+        public Mediator() {
+            for (inti = 0; i < 5; i++) {
+                mRooms.add(new Room(14 + i, (14 + i) * 150));
+           }
+       }
+    
+        public List<Room>getAllRooms() {
+            return mRooms;
+       }
+    }
+    
+    
+    /**
+     * 租户
+     */
+    public class Tenant {
+        public float roomArea;
+        public float roomPrice;
+        public static final float diffPrice = 100.0001f;
+        public static final float diffArea = 0.00001f;
+    
+        public void rentRoom(Mediator mediator) {
+            List<Room>rooms = mediator.getAllRooms();
+            for (Room room : rooms) {
+                if (isSuitable(room)) {
+                 System.out.println("租到房间啦! " + room);
+                    break;
+              }
+           }
+        }
+    
+        private boolean isSuitable(Room room) {
+            return Math.abs(room.price - roomPrice) < diffPrice
+                    &&Math.abs(room.area - roomArea) < diffArea;
+       }
+    }
+    ```
+- 从上面的代码中可以看到,Tenant不仅依赖了Mediator类,还需要频繁地与Room类打交道。租户类的要求只是通过中介找到一间适合自己的房间罢了,如果把这些检测条件都放在Tenant类中,那么中介类的功能就被弱化,而且导致Tenant与Room的耦合较高,因为Tenant必须知道许多关于Room的细节。当Room变化时Tenant也必须跟着变化。Tenant又与Mediator耦合,就导致了纠缠不清的关系。这个时候就需要我们分清谁才是我们真正的“朋友”,在我们所设定的情况下,显然是Mediator(虽然现实生活中不是这样的)。
+- 既然是耦合太严重,那我们就只能解耦了,首先要明确地是,我们只和我们的朋友通信,这里就是指Mediator对象。必须将Room相关的操作从Tenant中移除,而这些操作案例应该属于Mediator,我们进行如下重构:
+    ```java
+    /**
+     * 中介
+     */
+    public class Mediator {
+        List<Room> mRooms = new ArrayList<Room>();
+    
+        public Mediator() {
+            for (inti = 0; i < 5; i++) {
+                mRooms.add(new Room(14 + i, (14 + i) * 150));
+           }
+        }
+    
+        public Room rentOut(float  area, float  price) {
+            for (Room room : mRooms) {
+                if (isSuitable(area, price, room)) {
+                    return  room;
+              }
+           }
+            return null;
+        }
+    
+        private boolean isSuitable(float area, float price, Room room) {
+            return Math.abs(room.price - price) < Tenant.diffPrice
+                && Math.abs(room.area - area) < Tenant.diffPrice;
+        }
+    }
+    
+    /**
+     * 租户
+     */
+    public class Tenant {
+    
+        public float roomArea;
+        public float roomPrice;
+        public static final float diffPrice = 100.0001f;
+        public static final float diffArea = 0.00001f;
+    
+        public void rentRoom(Mediator mediator) {
+            System.out.println("租到房啦 " + mediator.rentOut(roomArea, roomPrice));
+         }
+    }
+    ```
+- 只是将对于Room的判定操作移到了Mediator类中,这本应该是Mediator的职责,他们根据租户设定的条件查找符合要求的房子,并且将结果交给租户就可以了。租户并不需要知道太多关于Room的细节,比如与房东签合同、房东的房产证是不是真的、房内的设施坏了之后我要找谁维修等,当我们通过我们的“朋友”中介租了房之后,所有的事情我们都通过与中介沟通就好了,房东、维修师傅等这些角色并不是我们直接的“朋友”。“只与直接的朋友通信”这简单的几个字就能够将我们从乱七八糟的关系网中抽离出来,使我们的耦合度更低、稳定性更好。 
+- 通过上述示例以及小民的后续思考,迪米特原则这把利剑在小民的手中已经舞得风生水起。就拿sd卡缓存来说吧,ImageCache就是用户的直接朋友,而SD卡缓存内部却是使用了jake wharton的DiskLruCache实现,这个DiskLruCache就不属于用户的直接朋友了,因此,用户完全不需要知道它的存在,用户只需要与ImageCache对象打交道即可。例如将图片存到SD卡中的代码如下。
+    ```java
+    public void put(String url, Bitmap value) {
+        DiskLruCache.Editor editor = null;
+        try {
+           // 如果没有找到对应的缓存,则准备从网络上请求数据,并写入缓存
+            editor = mDiskLruCache.edit(url);
+            if (editor != null) {
+                    OutputStream outputStream = editor.newOutputStream(0);
+                if (writeBitmapToDisk(value, outputStream)) {
+                  // 写入disk缓存
+                    editor.commit();
+              } else {
+                    editor.abort();
+              }
+                CloseUtils.closeQuietly(outputStream);
+           }
+        } catch (IOException e) {
+             e.printStackTrace();
+        }
+    }
+    ```
+- 用户在使用SD卡缓存时,根本不知晓DiskLruCache的实现,这就很好地对用户隐藏了具体实现。当小民已经“牛”到可以自己完成SD卡的rul实现时,他就可以随心所欲的替换掉jake wharton的DiskLruCache。小民的代码大体如下:
+    ```java
+    @Override
+    public  void put(String url, Bitmap bmp) {
+        // 将Bitmap写入文件中
+        FileOutputStream fos = null;
+        try {
+           // 构建图片的存储路径 ( 省略了对url取md5)
+            fos = new FileOutputStream("sdcard/cache/" + imageUrl2MD5(url));
+            bmp.compress(CompressFormat.JPEG, 100, fos);
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } finally {
+            if ( fos != null ) {
+                try {
+                    fos.close();
+              } catch (IOException e) {
+                    e.printStackTrace();
+              }
+           }
+        } // end if finally
+    }
+    ```
+- SD卡缓存的具体实现虽然被替换了,但用户根本不会感知到。因为用户根本不知道DiskLruCache的存在,他们没有与DiskLruCache进行通信,他们只认识直接“朋友”ImageCache,ImageCache将一切细节隐藏在了直接“朋友”的外衣之下,使得系统具有更低的耦合性和更好的可扩展性。
+
+
+
+
+
+
+

+ 167 - 0
ReadDesign/02.设计模式原则/02.单一职责原则详解.md

@@ -0,0 +1,167 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿基础介绍
+- 02.如何理解单一指责
+- 03.如何判断是否单一
+- 04.单一判断原则
+- 05.单一就更好么
+- 06.总结回顾一下
+
+
+
+### 00.问题思考分析
+- 01.如何理解类的单一指责,单一指责中这个单一是如何评判的?
+- 02.懂了,但是会用么,或者实际开发中有哪些运用,能否举例说明单一职责优势?
+- 03.单一指责是否设计越单一,越好呢?说出你的缘由和论证的思路想法?
+- 04.单一职责原则,除了应用到类的设计上,还能延伸到哪些其他设计方面吗?
+
+
+
+### 01.前沿基础介绍
+- 学习一些经典的设计原则,其中包括,SOLID、KISS、YAGNI、DRY、LOD等。这些设计原则,从字面上理解,都不难。你一看就感觉懂了,一看就感觉掌握了,但真的用到项目中的时候,你会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。从工作经历来看,很多同事因为对这些原则理解得不够透彻,导致在使用的时候过于教条主义,拿原则当真理,生搬硬套,适得其反。
+
+
+
+### 02.如何理解单一指责
+- 单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single reponsibility。如果我们把它翻译成中文,那就是:**一个类或者模块只负责完成一个职责(或者功能)**。
+- 注意,这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,在专栏中,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。
+- 不管哪种理解方式,单一职责原则在应用到这两个描述对象的时候,道理都是相通的。为了方便你理解,接下来我只从“类”设计的角度,来讲解如何应用这个设计原则。对于“模块”来说,你可以自行引申。
+- 单一职责原则的定义描述非常简单,也不难理解。**一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类**。
+- 举一个例子来解释一下。比如,一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类。
+- 举一个例子来解释一下。比如,一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类。
+
+
+
+### 03.如何判断是否单一
+- 从刚刚这个例子来看,单一职责原则看似不难应用。那是因为我举的这个例子比较极端,一眼就能看出订单和用户毫不相干。但大部分情况下,类里的方法是归为同一类功能,还是归为不相关的两类功能,并不是那么容易判定的。在真实的软件开发中,对于一个类是否职责单一的判定,是很难拿捏的。我举一个更加贴近实际的例子来给你解释一下。
+- 在一个社交产品中,我们用下面的 UserInfo 类来记录用户的信息。你觉得,UserInfo 类的设计是否满足单一职责原则呢?
+    ```java
+    public class UserInfo {
+      private long userId;
+      private String username;
+      private String email;
+      private String telephone;
+      private long createTime;
+      private long lastLoginTime;
+      private String avatarUrl;
+      private String provinceOfAddress; // 省
+      private String cityOfAddress; // 市
+      private String regionOfAddress; // 区 
+      private String detailedAddress; // 详细地址
+      // ...省略其他属性和方法...
+    }
+    ```
+- 对于这个问题,有两种不同的观点。
+    - 一种观点是,UserInfo 类包含的都是跟用户相关的信息,所有的属性和方法都隶属于用户这样一个业务模型,满足单一职责原则;
+    - 另一种观点是,地址信息在 UserInfo 类中,所占的比重比较高,可以继续拆分成独立的 UserAddress 类,UserInfo 只保留除 Address 之外的其他信息,拆分之后的两个类的职责更加单一。
+- 哪种观点更对呢?实际上,要从中做出选择,我们不能脱离具体的应用场景。
+    - 如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。
+    - 如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。
+- 我们再进一步延伸一下。如果做这个社交产品的公司发展得越来越好,公司内部又开发出了跟多其他产品(可以理解为其他 App)。公司希望支持统一账号系统,也就是用户一个账号可以在公司内部的所有产品中登录。这个时候,我们就需要继续对 UserInfo 进行拆分,将跟身份认证相关的信息(比如,email、telephone 等)抽取成独立的类。
+- 从刚刚这个例子,我们可以总结出,不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。
+- 除此之外,从不同的业务层面去看待同一个类的设计,对类是否职责单一,也会有不同的认识。比如,例子中的 UserInfo 类。如果我们从“用户”这个业务层面来看,UserInfo 包含的信息都属于用户,满足职责单一原则。如果我们从更加细分的“用户展示信息”“地址信息”“登录认证信息”等等这些更细粒度的业务层面来看,那 UserInfo 就应该继续拆分。
+- 综上所述,评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构(后面的章节中我们会讲到)。
+
+
+### 04.单一判断原则
+- 听到这里,你可能会说,这个原则如此含糊不清、模棱两可,到底该如何拿捏才好啊?我这里还有一些小技巧,能够很好地帮你,从侧面上判定一个类的职责是否够单一。而且,我个人觉得,下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:
+    - 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
+    - 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
+    - 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
+    - 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
+    - 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。
+- 不过,你可能还会有这样的疑问:在上面的判定原则中,我提到类中的代码行数、函数或者属性过多,就有可能不满足单一职责原则。那多少行代码才算是行数过多呢?多少个函数、属性才称得上过多呢?
+    - 比较初级的工程师经常会问这类问题。实际上,这个问题并不好定量地回答,就像你问大厨“放盐少许”中的“少许”是多少,大厨也很难告诉你一个特别具体的量值。
+- 实际上, 从另一个角度来看,当一个类的代码,读起来让你头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函数、属性过多了。实际上,等你做多项目了,代码写多了,在开发中慢慢“品尝”,自然就知道什么是“放盐少许”了,这就是所谓的“专业第六感”。
+
+
+###  05.单一就更好么
+- 为了满足单一职责原则,是不是把类拆得越细就越好呢?答案是否定的。我们还是通过一个例子来解释一下。Serialization 类实现了一个简单协议的序列化和反序列功能,具体代码如下:
+    ```java
+    /**
+     * Protocol format: identifier-string;{gson string}
+     * For example: UEUEUE;{"a":"A","b":"B"}
+     */
+    public class Serialization {
+      private static final String IDENTIFIER_STRING = "UEUEUE;";
+      private Gson gson;
+      
+      public Serialization() {
+        this.gson = new Gson();
+      }
+      
+      public String serialize(Map<String, String> object) {
+        StringBuilder textBuilder = new StringBuilder();
+        textBuilder.append(IDENTIFIER_STRING);
+        textBuilder.append(gson.toJson(object));
+        return textBuilder.toString();
+      }
+      
+      public Map<String, String> deserialize(String text) {
+        if (!text.startsWith(IDENTIFIER_STRING)) {
+            return Collections.emptyMap();
+        }
+        String gsonStr = text.substring(IDENTIFIER_STRING.length());
+        return gson.fromJson(gsonStr, Map.class);
+      }
+    }
+    ```
+- 如果我们想让类的职责更加单一,我们对 Serialization 类进一步拆分,拆分成一个只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer 类。拆分后的具体代码如下所示:
+    ```java
+    public class Serializer {
+      private static final String IDENTIFIER_STRING = "UEUEUE;";
+      private Gson gson;
+      
+      public Serializer() {
+        this.gson = new Gson();
+      }
+      
+      public String serialize(Map<String, String> object) {
+        StringBuilder textBuilder = new StringBuilder();
+        textBuilder.append(IDENTIFIER_STRING);
+        textBuilder.append(gson.toJson(object));
+        return textBuilder.toString();
+      }
+    }
+    
+    public class Deserializer {
+      private static final String IDENTIFIER_STRING = "UEUEUE;";
+      private Gson gson;
+      
+      public Deserializer() {
+        this.gson = new Gson();
+      }
+      
+      public Map<String, String> deserialize(String text) {
+        if (!text.startsWith(IDENTIFIER_STRING)) {
+            return Collections.emptyMap();
+        }
+        String gsonStr = text.substring(IDENTIFIER_STRING.length());
+        return gson.fromJson(gsonStr, Map.class);
+      }
+    }
+    ```
+- 虽然经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差了。
+
+
+### 06.总结回顾一下
+- 1.如何理解单一职责原则(SRP)?
+    - 一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
+- 2. 如何判断类的职责是否足够单一?
+    - 不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:
+    - 类中的代码行数、函数或者属性过多;
+    - 类依赖的其他类过多,或者依赖类的其他类过多;
+    - 私有方法过多;
+    - 比较难给类起一个合适的名字;
+    - 类中大量的方法都是集中操作类中的某几个属性。
+- 3. 类的职责是否设计得越单一越好?
+    - 单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
+
+
+
+
+
+
+
+
+

+ 343 - 0
ReadDesign/02.设计模式原则/03.开闭原则详细介绍.md

@@ -0,0 +1,343 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿简单介绍
+- 02.如何理解开闭原则
+- 03.举一个原始的例子
+- 04.修改后的代码
+- 05.修改代码违背原则么
+- 06.如何做到开闭原则
+- 07.如何运用开闭原则
+- 08.总结一下内容
+
+
+
+### 00.问题思考分析
+- 01.什么叫作开闭原则,他的主要用途是什么?
+- 02.如何做到拓展开放,修改封闭这一准则,结合案例说一下如何实现?
+
+
+
+### 01.前沿简单介绍
+- 学习 SOLID 中的第二个原则:开闭原则。个人觉得,开闭原则是 SOLID 中最难理解、最难掌握,同时也是最有用的一条原则。
+- 之所以说这条原则难理解,那是因为,“怎样的代码改动才被定义为‘扩展’?怎样的代码改动才被定义为‘修改’?怎么才算满足或违反‘开闭原则’?修改代码就一定意味着违反‘开闭原则’吗?”等等这些问题,都比较难理解。
+- 之所以说这条原则难掌握,那是因为,“如何做到‘对扩展开放、修改关闭’?如何在项目中灵活地应用‘开闭原则’,以避免在追求扩展性的同时影响到代码的可读性?”等等这些问题,都比较难掌握。
+- 之所以说这条原则最有用,那是因为,扩展性是代码质量最重要的衡量标准之一。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。
+
+
+
+### 02.如何理解开闭原则
+- 开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“**对扩展开放、对修改关闭**”。
+    - 这个描述比较简略,如果我们详细表述一下,那就是,**添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)**。
+
+
+### 03.举一个原始的例子
+- 为了让你更好地理解这个原则,我举一个例子来进一步解释一下。这是一段 API 接口监控告警的代码。
+    - 其中,AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。关于 API 接口监控告警这部分,更加详细的业务需求分析和设计,我们会在后面的设计模式模块再拿出来进一步讲解,这里你只要简单知道这些,就够我们今天用了。
+    ```java
+    public class Alert {
+      private AlertRule rule;
+      private Notification notification;
+    
+      public Alert(AlertRule rule, Notification notification) {
+        this.rule = rule;
+        this.notification = notification;
+      }
+    
+      public void check(String api, long requestCount, long errorCount, long durationOfSeconds) {
+        long tps = requestCount / durationOfSeconds;
+        if (tps > rule.getMatchedRule(api).getMaxTps()) {
+          notification.notify(NotificationEmergencyLevel.URGENCY, "...");
+        }
+        if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
+          notification.notify(NotificationEmergencyLevel.SEVERE, "...");
+        }
+      }
+    }
+    ```
+- 上面这段代码非常简单,业务逻辑主要集中在 check() 函数中。当接口的 TPS 超过某个预先设置的最大值时,以及当接口请求出错数大于某个最大允许值时,就会触发告警,通知接口的相关负责人或者团队。
+- 现在,如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。这个时候,我们该如何改动代码呢?主要的改动有两处:第一处是修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数;第二处是在 check() 函数中添加新的告警逻辑。具体的代码改动如下所示:
+    ```java
+    public class Alert {
+      // ...省略AlertRule/Notification属性和构造函数...
+      
+      // 改动一:添加参数timeoutCount
+      public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {
+        long tps = requestCount / durationOfSeconds;
+        if (tps > rule.getMatchedRule(api).getMaxTps()) {
+          notification.notify(NotificationEmergencyLevel.URGENCY, "...");
+        }
+        if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
+          notification.notify(NotificationEmergencyLevel.SEVERE, "...");
+        }
+        // 改动二:添加接口超时处理逻辑
+        long timeoutTps = timeoutCount / durationOfSeconds;
+        if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
+          notification.notify(NotificationEmergencyLevel.URGENCY, "...");
+        }
+      }
+    }
+    ```
+- 这样的代码修改实际上存在挺多问题的。一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改。另一方面,修改了 check() 函数,相应的单元测试都需要修改(关于单元测试的内容我们在重构那部分会详细介绍)。
+
+
+
+### 04.修改后的代码
+- 上面的代码改动是基于“修改”的方式来实现新功能的。如果我们遵循开闭原则,也就是“对扩展开放、对修改关闭”。那如何通过“扩展”的方式,来实现同样的功能呢?我们先重构一下之前的 Alert 代码,让它的扩展性更好一些。重构的内容主要包含两部分:
+    - 第一部分是将 check() 函数的多个入参封装成 ApiStatInfo 类;
+    - 第二部分是引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中。
+- 具体的代码实现如下所示:
+    ```java
+    public class Alert {
+      private List<AlertHandler> alertHandlers = new ArrayList<>();
+      
+      public void addAlertHandler(AlertHandler alertHandler) {
+        this.alertHandlers.add(alertHandler);
+      }
+    
+      public void check(ApiStatInfo apiStatInfo) {
+        for (AlertHandler handler : alertHandlers) {
+          handler.check(apiStatInfo);
+        }
+      }
+    }
+    
+    public class ApiStatInfo {//省略constructor/getter/setter方法
+      private String api;
+      private long requestCount;
+      private long errorCount;
+      private long durationOfSeconds;
+    }
+    
+    public abstract class AlertHandler {
+      protected AlertRule rule;
+      protected Notification notification;
+      public AlertHandler(AlertRule rule, Notification notification) {
+        this.rule = rule;
+        this.notification = notification;
+      }
+      public abstract void check(ApiStatInfo apiStatInfo);
+    }
+    
+    public class TpsAlertHandler extends AlertHandler {
+      public TpsAlertHandler(AlertRule rule, Notification notification) {
+        super(rule, notification);
+      }
+    
+      @Override
+      public void check(ApiStatInfo apiStatInfo) {
+        long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds();
+        if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {
+          notification.notify(NotificationEmergencyLevel.URGENCY, "...");
+        }
+      }
+    }
+    
+    public class ErrorAlertHandler extends AlertHandler {
+      public ErrorAlertHandler(AlertRule rule, Notification notification){
+        super(rule, notification);
+      }
+    
+      @Override
+      public void check(ApiStatInfo apiStatInfo) {
+        if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
+          notification.notify(NotificationEmergencyLevel.SEVERE, "...");
+        }
+      }
+    }
+    ```
+- 上面的代码是对 Alert 的重构,我们再来看下,重构之后的 Alert 该如何使用呢?具体的使用代码我也写在这里了。其中,ApplicationContext 是一个单例类,负责 Alert 的创建、组装(alertRule 和 notification 的依赖注入)、初始化(添加 handlers)工作。
+    ```java
+    public class ApplicationContext {
+      private AlertRule alertRule;
+      private Notification notification;
+      private Alert alert;
+      
+      public void initializeBeans() {
+        alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
+        notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
+        alert = new Alert();
+        alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
+        alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
+      }
+      public Alert getAlert() { return alert; }
+    
+      // 饿汉式单例
+      private static final ApplicationContext instance = new ApplicationContext();
+      private ApplicationContext() {
+        instance.initializeBeans();
+      }
+      public static ApplicationContext getInstance() {
+        return instance;
+      }
+    }
+    
+    public class Demo {
+      public static void main(String[] args) {
+        ApiStatInfo apiStatInfo = new ApiStatInfo();
+        // ...省略设置apiStatInfo数据值的代码
+        ApplicationContext.getInstance().getAlert().check(apiStatInfo);
+      }
+    }
+    ```
+- 现在,我们再来看下,基于重构之后的代码,如果再添加上面讲到的那个新功能,每秒钟接口超时请求个数超过某个最大阈值就告警,我们又该如何改动代码呢?主要的改动有下面四处。
+    - 第一处改动是:在 ApiStatInfo 类中添加新的属性 timeoutCount。
+    - 第二处改动是:添加新的 TimeoutAlertHander 类。
+    - 第三处改动是:在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler。
+    - 第四处改动是:在使用 Alert 类的时候,需要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。
+- 改动之后的代码如下所示:
+    ```java
+    public class Alert { // 代码未改动... }
+    public class ApiStatInfo {//省略constructor/getter/setter方法
+      private String api;
+      private long requestCount;
+      private long errorCount;
+      private long durationOfSeconds;
+      private long timeoutCount; // 改动一:添加新字段
+    }
+    public abstract class AlertHandler { //代码未改动... }
+    public class TpsAlertHandler extends AlertHandler {//代码未改动...}
+    public class ErrorAlertHandler extends AlertHandler {//代码未改动...}
+    // 改动二:添加新的handler
+    public class TimeoutAlertHandler extends AlertHandler {//省略代码...}
+    
+    public class ApplicationContext {
+      private AlertRule alertRule;
+      private Notification notification;
+      private Alert alert;
+      
+      public void initializeBeans() {
+        alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
+        notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
+        alert = new Alert();
+        alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
+        alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
+        // 改动三:注册handler
+        alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification));
+      }
+      //...省略其他未改动代码...
+    }
+    
+    public class Demo {
+      public static void main(String[] args) {
+        ApiStatInfo apiStatInfo = new ApiStatInfo();
+        // ...省略apiStatInfo的set字段代码
+        apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值
+        ApplicationContext.getInstance().getAlert().check(apiStatInfo);
+    }
+    ```
+- 重构之后的代码更加灵活和易扩展。如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。而且,我们只需要为新的 handler 类添加单元测试,老的单元测试都不会失败,也不用修改。
+
+
+
+
+### 05.修改代码违背原则么
+- 看了上面重构之后的代码,你可能还会有疑问:在添加新的告警逻辑的时候,尽管改动二(添加新的 handler 类)是基于扩展而非修改的方式来完成的,但改动一、三、四貌似不是基于扩展而是基于修改的方式来完成的,那改动一、三、四不就违背了开闭原则吗?
+- 我们先来分析一下改动一:往 ApiStatInfo 类中添加新的属性 timeoutCount。
+    - 实际上,我们不仅往 ApiStatInfo 类中添加了属性,还添加了对应的 getter/setter 方法。那这个问题就转化为:给类中添加新的属性和方法,算作“修改”还是“扩展”?
+    - 我们再一块回忆一下开闭原则的定义:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。从定义中,我们可以看出,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,被认定为“修改”,在细代码粒度下,又可以被认定为“扩展”。比如,改动一,添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。
+    - 实际上,我们也没必要纠结某个代码改动是“修改”还是“扩展”,更没必要太纠结它是否违反“开闭原则”。我们回到这条原则的设计初衷:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。
+- 我们再来分析一下改动三和改动四:在 ApplicationContext 类的 initializeBeans() 方法中,往 alert 对象中注册新的 timeoutAlertHandler;在使用 Alert 类的时候,需要给 check() 函数的入参 apiStatInfo 对象设置 timeoutCount 的值。
+    - 这两处改动都是在方法内部进行的,不管从哪个层面(模块、类、方法)来讲,都不能算是“扩展”,而是地地道道的“修改”。不过,有些修改是在所难免的,是可以被接受的。为什么这么说呢?
+    - 在重构之后的 Alert 代码中,我们的核心逻辑集中在 Alert 类及其各个 handler 中,当我们在添加新的告警逻辑的时候,Alert 类完全不需要修改,而只需要扩展一个新 handler 类。如果我们把 Alert 类及各个 handler 类合起来看作一个“模块”,那模块本身在添加新的功能的时候,完全满足开闭原则。
+    - 而且,我们要认识到,添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。
+
+
+
+### 06.如何做到开闭原则
+- 在刚刚的例子中,我们通过引入一组 handler 的方式来实现支持开闭原则。如果你没有太多复杂代码的设计和开发经验,你可能会有这样的疑问:这样的代码设计思路怎么想不到呢?你是怎么想到的呢?
+- 先给你个结论,之所以我能想到,靠的就是理论知识和实战经验,这些需要你慢慢学习和积累。对于如何做到“对扩展开放、修改关闭”,我们也有一些指导思想和具体的方法论,我们一块来看一下。实际上,开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就说明这段代码的扩展性比较好。
+- **在讲具体的方法论之前,我们先来看一些更加偏向顶层的指导思想。为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要**。
+    - 在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。
+    - 在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。
+- **讲了实现开闭原则的一些偏向顶层的指导思想,现在我们再来看下,支持开闭原则的一些更加具体的方法论**。
+    - 代码的扩展性是代码质量评判的最重要的标准之一。实际上,我们整个专栏的大部分知识点都是围绕扩展性问题来讲解的。专栏中讲到的很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。
+    - 今天我重点讲一下,如何利用多态、依赖注入、基于接口而非实现编程,来实现“对扩展开放、对修改关闭”。实际上,多态、依赖注入、基于接口而非实现编程,以及前面提到的抽象意识,说的都是同一种设计思路,只是从不同的角度、不同的层面来阐述而已。这也体现了“很多设计原则、思想、模式都是相通的”这一思想。
+- 接下来,我就通过一个例子来解释一下,如何利用这几个设计思想或原则来实现“对扩展开放、对修改关闭”。
+    - 注意,依赖注入后面会讲到,如果你对这块不了解,可以暂时先忽略这个概念,只关注多态、基于接口而非实现编程以及抽象意识。
+    - 比如,我们代码中通过 Kafka 来发送异步消息。对于这样一个功能的开发,我们要学会将其抽象成一组跟具体消息队列(Kafka)无关的异步消息接口。所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用。当我们要替换新的消息队列的时候,比如将 Kafka 替换成 RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现。具体代码如下所示:
+    ```java
+    // 这一部分体现了抽象意识
+    public interface MessageQueue { //... }
+    public class KafkaMessageQueue implements MessageQueue { //... }
+    public class RocketMQMessageQueue implements MessageQueue {//...}
+    
+    public interface MessageFormatter { //... }
+    public class JsonMessageFormatter implements MessageFormatter {//...}
+    public class MessageFormatter implements MessageFormatter {//...}
+    
+    public class Demo {
+      private MessageQueue msgQueue; // 基于接口而非实现编程
+      public Demo(MessageQueue msgQueue) { // 依赖注入
+        this.msgQueue = msgQueue;
+      }
+      
+      // msgFormatter:多态、依赖注入
+      public void sendNotification(Notification notification, MessageFormatter msgFormatter) {
+        //...    
+      }
+    }
+    ```
+
+
+### 07.如何运用开闭原则
+- 如果你开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。如果你开发的是跟业务无关的、通用的、偏底层的系统,比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。
+- 不过,有一句话说得好,“唯一不变的只有变化本身”。即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计。
+- 最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。
+- 而且,开闭原则也并不是免费的。**有些情况下,代码的扩展性会跟可读性相冲突**。比如,我们之前举的 Alert 告警的例子。为了更好地支持扩展性,我们对代码进行了重构,重构之后的代码要比之前的代码复杂很多,理解起来也更加有难度。**很多时候,我们都需要在扩展性和可读性之间做权衡。在某些场景下,代码的扩展性很重要,我们就可以适当地牺牲一些代码的可读性;在另一些场景下,代码的可读性更加重要,那我们就适当地牺牲一些代码的可扩展性**。
+- 之前举的 Alert 告警的例子中,如果告警规则并不是很多、也不复杂,那 check() 函数中的 if 语句就不会很多,代码逻辑也不复杂,代码行数也不多,那最初的第一种代码实现思路简单易读,就是比较合理的选择。相反,如果告警规则很多、很复杂,check() 函数的 if 语句、代码逻辑就会很多、很复杂,相应的代码行数也会很多,可读性、可维护性就会变差,那重构之后的第二种代码实现思路就是更加合理的选择了。总之,这里没有一个放之四海而皆准的参考标准,全凭实际的应用场景来决定。
+
+
+
+
+### 08.总结一下内容
+- 1.如何理解“对扩展开放、对修改关闭”?
+    - 添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
+- 2.如何做到“对扩展开放、修改关闭”?
+    - 我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
+- 3.学习设计原则,要多问个为什么。
+    - 不能把设计原则当真理,而是要理解设计原则背后的思想。搞清楚这个,比单纯理解原则讲的是啥,更能让你灵活应用原则。
+
+
+
+### 09.实践案例分析
+- 将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展?
+- 看看下面这段代码,改编某伟大公司产品代码,你觉得可以利用面向对象设计原则如何改进?
+    ```
+    public class VIPCenter {
+        void serviceVIP(T extend User user>) {
+         if (user instanceof SlumDogVIP) {
+            // 穷 X VIP,活动抢的那种
+            // do somthing
+          } else if(user instanceof RealVIP) {
+            // do somthing
+          }
+          // ...
+    }
+    ```
+    - 这段代码的一个问题是,业务逻辑集中在一起,当出现新的用户类型时,比如,大数据发现了我们是肥羊,需要去收获一下, 这就需要直接去修改服务方法代码实现,这可能会意外影响不相关的某个用户类型逻辑。
+    - 利用开关原则,可以尝试改造为下面的代码。将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展。[技术博客大总结](https://github.com/yangchong211/YCBlogs)
+    ```
+    public class VIPCenter {
+        private Map<User.TYPE, ServiceProvider> providers;
+        void serviceVIP(T extend User user) {
+            providers.get(user.getType()).service(user);
+        }
+    }
+    
+    interface ServiceProvider{
+        void service(T extend User user) ;
+    }
+    
+    class SlumDogVIPServiceProvider implements ServiceProvider{
+        void service(T extend User user){
+            // do somthing
+        }
+    }
+    
+    class RealVIPServiceProvider implements ServiceProvider{
+        void service(T extend User user) {
+            // do something
+        }
+    }
+    ```
+

+ 137 - 0
ReadDesign/02.设计模式原则/04.里式替换原则介绍.md

@@ -0,0 +1,137 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿基础介绍
+- 02.如何理解里式替换原则
+- 03.通过案例来理解
+- 04.那些违背了该原则
+- 05.经典总结一下
+
+
+### 00.问题思考分析
+- 01.什么是里氏替换的原则,如何理解这一原则?
+- 02.有那些场景满足里氏替换原则?它跟多态有何区别?
+
+
+
+### 01.前沿基础介绍
+- 整体上来讲,这个设计原则是比较简单、容易理解和掌握的。今天我主要通过几个反例,带你看看,哪些代码是违反里式替换原则的?我们该如何将它们改造成满足里式替换原则?除此之外,这条原则从定义上看起来,跟我们之前讲过的“多态”有点类似。所以,我今天也会讲一下,它跟多态的区别。
+
+
+
+### 02.如何理解里式替换原则
+- 里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
+- 在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则,英文原话是这样的:Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
+- 我们综合两者的描述,将这条原则用中文描述出来,是这样的:
+    - 子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
+
+
+### 03.通过案例来理解
+- 这么说还是比较抽象,我们通过一个例子来解释一下。如下代码中,父类 Transporter 使用 org.apache.http 库中的 HttpClient 类来传输网络数据。子类 SecurityTransporter 继承父类 Transporter,增加了额外的功能,支持传输 appId 和 appToken 安全认证信息。
+    ```java
+    public class Transporter {
+      private HttpClient httpClient;
+      
+      public Transporter(HttpClient httpClient) {
+        this.httpClient = httpClient;
+      }
+    
+      public Response sendRequest(Request request) {
+        // ...use httpClient to send request
+      }
+    }
+    
+    public class SecurityTransporter extends Transporter {
+      private String appId;
+      private String appToken;
+    
+      public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
+        super(httpClient);
+        this.appId = appId;
+        this.appToken = appToken;
+      }
+    
+      @Override
+      public Response sendRequest(Request request) {
+        if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
+          request.addPayload("app-id", appId);
+          request.addPayload("app-token", appToken);
+        }
+        return super.sendRequest(request);
+      }
+    }
+    
+    public class Demo {    
+      public void demoFunction(Transporter transporter) {    
+        Reuqest request = new Request();
+        //...省略设置request中数据值的代码...
+        Response response = transporter.sendRequest(request);
+        //...省略其他逻辑...
+      }
+    }
+    
+    // 里式替换原则
+    Demo demo = new Demo();
+    demo.demofunction(new SecurityTransporter(/*省略参数*/););
+    ```
+- 在上面的代码中,子类 SecurityTransporter 的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
+- 不过,你可能会有这样的疑问,刚刚的代码设计不就是简单利用了面向对象的多态特性吗?多态和里式替换原则说的是不是一回事呢?从刚刚的例子和定义描述来看,里式替换原则跟多态看起来确实有点类似,但实际上它们完全是两回事。为什么这么说呢?
+- 我们还是通过刚才这个例子来解释一下。不过,我们需要对 SecurityTransporter 类中 sendRequest() 函数稍加改造一下。改造前,如果 appId 或者 appToken 没有设置,我们就不做校验;改造后,如果 appId 或者 appToken 没有设置,则直接抛出 NoAuthorizationRuntimeException 未授权异常。改造前后的代码对比如下所示:
+    ```java
+    // 改造前:
+    public class SecurityTransporter extends Transporter {
+      //...省略其他代码..
+      @Override
+      public Response sendRequest(Request request) {
+        if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
+          request.addPayload("app-id", appId);
+          request.addPayload("app-token", appToken);
+        }
+        return super.sendRequest(request);
+      }
+    }
+    
+    // 改造后:
+    public class SecurityTransporter extends Transporter {
+      //...省略其他代码..
+      @Override
+      public Response sendRequest(Request request) {
+        if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {
+          throw new NoAuthorizationRuntimeException(...);
+        }
+        request.addPayload("app-id", appId);
+        request.addPayload("app-token", appToken);
+        return super.sendRequest(request);
+      }
+    }
+    ```
+- 在改造之后的代码中,如果传递进 demoFunction() 函数的是父类 Transporter 对象,那 demoFunction() 函数并不会有异常抛出,但如果传递给 demoFunction() 函数的是子类 SecurityTransporter 对象,那 demoFunction() 有可能会有异常抛出。尽管代码中抛出的是运行时异常(Runtime Exception),我们可以不在代码中显式地捕获处理,但子类替换父类传递进 demoFunction 函数之后,整个程序的逻辑行为有了改变。
+- 虽然改造之后的代码仍然可以通过 Java 的多态语法,动态地用子类 SecurityTransporter 来替换父类 Transporter,也并不会导致程序编译或者运行报错。但是,从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的。
+- 稍微总结一下。
+    - 虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。
+    - 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。
+    - 里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
+
+
+
+### 04.那些违背了该原则
+- 实际上,里式替换原则还有另外一个更加能落地、更有指导意义的描述,那就是“Design By Contract”,中文翻译就是“按照协议来设计”。看起来比较抽象,我来进一步解读一下。子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。
+- 为了更好地理解这句话,我举几个违反里式替换原则的例子来解释一下。
+- 1.子类违背父类声明要实现的功能
+    - 父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。
+- 2.子类违背父类对输入、输出、异常的约定
+    - 在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。那子类的设计就违背里式替换原则。
+    - 在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
+    - 在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
+- 3.子类违背父类注释中所罗列的任何特殊说明
+    - 父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。
+    - 以上便是三种典型的违背里式替换原则的情况。除此之外,判断子类的设计实现是否违背里式替换原则,还有一个小窍门,那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。
+
+
+
+### 05.经典总结一下
+- 里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
+- 理解这个原则,我们还要弄明白里式替换原则跟多态的区别。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。
+
+
+
+

+ 375 - 0
ReadDesign/02.设计模式原则/05.接口隔离原则介绍.md

@@ -0,0 +1,375 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿简单介绍
+- 02.如何理解接口隔离原则
+- 03.接口理解为一组API接口集合
+- 04.接口理解为单个API接口或函数
+- 05.接口理解为OOP中的接口概念
+- 06.总结一下分享
+- 07.思考一道课后题
+
+
+
+### 00.问题思考分析
+- 01.什么叫作接口隔离法则,它和面向对象中的接口有何区别?
+- 02.什么叫作接口
+
+
+
+
+### 01.前沿简单介绍
+- 学习了 SOLID 原则中的单一职责原则、开闭原则和里式替换原则,今天我们学习第四个原则,接口隔离原则。它对应 SOLID 中的英文字母“I”。对于这个原则,最关键就是理解其中“接口”的含义。那针对“接口”,不同的理解方式,对应在原则上也有不同的解读方式。除此之外,接口隔离原则跟我们之前讲到的单一职责原则还有点儿类似,所以今天我也会具体讲一下它们之间的区别和联系。
+
+
+
+### 02.如何理解接口隔离原则
+- 接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文的话就是:客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。
+- 实际上,“接口”这个名词可以用在很多场合中。生活中我们可以用它来指插座接口等。在软件开发中,我们既可以把它看作一组抽象的约定,也可以具体指系统与系统之间的 API 接口,还可以特指面向对象编程语言中的接口等。
+- 前面我提到,理解接口隔离原则的关键,就是理解其中的“接口”二字。在这条原则中,我们可以把“接口”理解为下面三种东西:
+    - 一组 API 接口集合
+    - 单个 API 接口或函数
+    - OOP 中的接口概念
+
+
+### 03.接口理解为一组API接口集合
+- 还是结合一个例子来讲解。微服务用户系统提供了一组跟用户相关的 API 给其他系统使用,比如:注册、登录、获取用户信息等。具体代码如下所示:
+    ```java
+    public interface UserService {
+      boolean register(String cellphone, String password);
+      boolean login(String cellphone, String password);
+      UserInfo getUserInfoById(long id);
+      UserInfo getUserInfoByCellphone(String cellphone);
+    }
+    
+    public class UserServiceImpl implements UserService {
+      //...
+    }
+    ```
+- 现在,我们的后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口。这个时候我们该如何来做呢?你可能会说,这不是很简单吗,我只需要在 UserService 中新添加一个 deleteUserByCellphone() 或 deleteUserById() 接口就可以了。这个方法可以解决问题,但是也隐藏了一些安全隐患。
+- 删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,所以这个接口只限于给后台管理系统使用。如果我们把它放到 UserService 中,那所有使用到 UserService 的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。
+- 当然,最好的解决方案是从架构设计的层面,通过接口鉴权的方式来限制接口的调用。不过,如果暂时没有鉴权框架来支持,我们还可以从代码设计的层面,尽量避免接口被误用。我们参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台管理系统来使用。具体的代码实现如下所示:
+    ```
+    public interface UserService {
+      boolean register(String cellphone, String password);
+      boolean login(String cellphone, String password);
+      UserInfo getUserInfoById(long id);
+      UserInfo getUserInfoByCellphone(String cellphone);
+    }
+    
+    public interface RestrictedUserService {
+      boolean deleteUserByCellphone(String cellphone);
+      boolean deleteUserById(long id);
+    }
+    
+    public class UserServiceImpl implements UserService, RestrictedUserService {
+      // ...省略实现代码...
+    }
+    ```
+- 在刚刚的这个例子中,我们把接口隔离原则中的接口,理解为一组接口集合,它可以是某个微服务的接口,也可以是某个类库的接口等等。在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。
+
+
+
+### 04.接口理解为单个API接口或函数
+- 现在我们再换一种理解方式,把接口理解为单个接口或函数(以下为了方便讲解,我都简称为“函数”)。那接口隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。接下来,我们还是通过一个例子来解释一下。
+    ```java
+    public class Statistics {
+      private Long max;
+      private Long min;
+      private Long average;
+      private Long sum;
+      private Long percentile99;
+      private Long percentile999;
+      //...省略constructor/getter/setter等方法...
+    }
+    
+    public Statistics count(Collection<Long> dataSet) {
+      Statistics statistics = new Statistics();
+      //...省略计算逻辑...
+      return statistics;
+    }
+    ```
+- 在上面的代码中,count() 函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等等。按照接口隔离原则,我们应该把 count() 函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能。拆分之后的代码如下所示:
+    ```
+    public Long max(Collection<Long> dataSet) { //... }
+    public Long min(Collection<Long> dataSet) { //... } 
+    public Long average(Colletion<Long> dataSet) { //... }
+    // ...省略其他统计函数...
+    ```
+- 不过,你可能会说,在某种意义上讲,count() 函数也不能算是职责不够单一,毕竟它做的事情只跟统计相关。我们在讲单一职责原则的时候,也提到过类似的问题。实际上,判定功能是否单一,除了很强的主观性,还需要结合具体的场景。
+- 如果在项目中,对每个统计需求,Statistics 定义的那几个统计信息都有涉及,那 count() 函数的设计就是合理的。相反,如果每个统计需求只涉及 Statistics 罗列的统计信息中一部分,比如,有的只需要用到 max、min、average 这三类统计信息,有的只需要用到 average、sum。而 count() 函数每次都会把所有的统计信息计算一遍,就会做很多无用功,势必影响代码的性能,特别是在需要统计的数据量很大的时候。所以,在这个应用场景下,count() 函数的设计就有点不合理了,我们应该按照第二种设计思路,将其拆分成粒度更细的多个统计函数。
+- 接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。
+    - **单一职责原则针对的是模块、类、接口的设计**。
+    - 接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
+
+
+
+### 05.接口理解为OOP中的接口概念
+- 还可以把“接口”理解为 OOP 中的接口概念,比如 Java 中的 interface。我还是通过一个例子来给你解释。假设我们的项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都对应一系列配置信息,比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中的其他模块来使用,我们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig。具体的代码实现如下所示。注意,这里我只给出了 RedisConfig 的代码实现,另外两个都是类似的,我这里就不贴了。
+    ```java
+    public class RedisConfig {
+        private ConfigSource configSource; //配置中心(比如zookeeper)
+        private String address;
+        private int timeout;
+        private int maxTotal;
+        //省略其他配置: maxWaitMillis,maxIdle,minIdle...
+    
+        public RedisConfig(ConfigSource configSource) {
+            this.configSource = configSource;
+        }
+    
+        public String getAddress() {
+            return this.address;
+        }
+        //...省略其他get()、init()方法...
+    
+        public void update() {
+          //从configSource加载配置到address/timeout/maxTotal...
+        }
+    }
+    
+    public class KafkaConfig { //...省略... }
+    public class MysqlConfig { //...省略... }
+    ```
+- 现在,我们有一个新的功能需求,希望支持 Redis 和 Kafka 配置信息的热更新。所谓“热更新(hot update)”就是,如果在配置中心中更改了配置信息,我们希望在不用重启系统的情况下,能将最新的配置信息加载到内存中(也就是 RedisConfig、KafkaConfig 类中)。但是,因为某些原因,我们并不希望对 MySQL 的配置信息进行热更新。
+- 为了实现这样一个功能需求,我们设计实现了一个 ScheduledUpdater 类,以固定时间频率(periodInSeconds)来调用 RedisConfig、KafkaConfig 的 update() 方法更新配置信息。具体的代码实现如下所示:
+    ```java
+    public interface Updater {
+      void update();
+    }
+    
+    public class RedisConfig implemets Updater {
+      //...省略其他属性和方法...
+      @Override
+      public void update() { //... }
+    }
+    
+    public class KafkaConfig implements Updater {
+      //...省略其他属性和方法...
+      @Override
+      public void update() { //... }
+    }
+    
+    public class MysqlConfig { //...省略其他属性和方法... }
+    
+    public class ScheduledUpdater {
+        private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();;
+        private long initialDelayInSeconds;
+        private long periodInSeconds;
+        private Updater updater;
+    
+        public ScheduleUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) {
+            this.updater = updater;
+            this.initialDelayInSeconds = initialDelayInSeconds;
+            this.periodInSeconds = periodInSeconds;
+        }
+    
+        public void run() {
+            executor.scheduleAtFixedRate(new Runnable() {
+                @Override
+                public void run() {
+                    updater.update();
+                }
+            }, this.initialDelayInSeconds, this.periodInSeconds, TimeUnit.SECONDS);
+        }
+    }
+    
+    public class Application {
+      ConfigSource configSource = new ZookeeperConfigSource(/*省略参数*/);
+      public static final RedisConfig redisConfig = new RedisConfig(configSource);
+      public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
+      public static final MySqlConfig mysqlConfig = new MysqlConfig(configSource);
+    
+      public static void main(String[] args) {
+        ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig, 300, 300);
+        redisConfigUpdater.run();
+        
+        ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig, 60, 60);
+        redisConfigUpdater.run();
+      }
+    }
+    ```
+- 刚刚的热更新的需求我们已经搞定了。现在,我们又有了一个新的监控功能需求。通过命令行来查看 Zookeeper 中的配置信息是比较麻烦的。所以,我们希望能有一种更加方便的配置信息查看方式。为了实现这样一个功能,我们还需要对上面的代码做进一步改造。改造之后的代码如下所示:
+    ```java
+    public interface Updater {
+      void update();
+    }
+    
+    public interface Viewer {
+      String outputInPlainText();
+      Map<String, String> output();
+    }
+    
+    public class RedisConfig implemets Updater, Viewer {
+      //...省略其他属性和方法...
+      @Override
+      public void update() { //... }
+      @Override
+      public String outputInPlainText() { //... }
+      @Override
+      public Map<String, String> output() { //...}
+    }
+    
+    public class KafkaConfig implements Updater {
+      //...省略其他属性和方法...
+      @Override
+      public void update() { //... }
+    }
+    
+    public class MysqlConfig implements Viewer {
+      //...省略其他属性和方法...
+      @Override
+      public String outputInPlainText() { //... }
+      @Override
+      public Map<String, String> output() { //...}
+    }
+    
+    public class SimpleHttpServer {
+      private String host;
+      private int port;
+      private Map<String, List<Viewer>> viewers = new HashMap<>();
+      
+      public SimpleHttpServer(String host, int port) {//...}
+      
+      public void addViewers(String urlDirectory, Viewer viewer) {
+        if (!viewers.containsKey(urlDirectory)) {
+          viewers.put(urlDirectory, new ArrayList<Viewer>());
+        }
+        this.viewers.get(urlDirectory).add(viewer);
+      }
+      
+      public void run() { //... }
+    }
+    
+    public class Application {
+        ConfigSource configSource = new ZookeeperConfigSource();
+        public static final RedisConfig redisConfig = new RedisConfig(configSource);
+        public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
+        public static final MySqlConfig mysqlConfig = new MySqlConfig(configSource);
+        
+        public static void main(String[] args) {
+            ScheduledUpdater redisConfigUpdater =
+                new ScheduledUpdater(redisConfig, 300, 300);
+            redisConfigUpdater.run();
+            
+            ScheduledUpdater kafkaConfigUpdater =
+                new ScheduledUpdater(kafkaConfig, 60, 60);
+            redisConfigUpdater.run();
+            
+            SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389);
+            simpleHttpServer.addViewer("/config", redisConfig);
+            simpleHttpServer.addViewer("/config", mysqlConfig);
+            simpleHttpServer.run();
+        }
+    }
+    ```
+- 至此,热更新和监控的需求我们就都实现了。我们来回顾一下这个例子的设计思想。
+    - 设计了两个功能非常单一的接口:Updater 和 Viewer。ScheduledUpdater 只依赖 Updater 这个跟热更新相关的接口,不需要被强迫去依赖不需要的 Viewer 接口,满足接口隔离原则。同理,SimpleHttpServer 只依赖跟查看信息相关的 Viewer 接口,不依赖不需要的 Updater 接口,也满足接口隔离原则。
+- 你可能会说,如果我们不遵守接口隔离原则,不设计 Updater 和 Viewer 两个小接口,而是设计一个大而全的 Config 接口,让 RedisConfig、KafkaConfig、MysqlConfig 都实现这个 Config 接口,并且将原来传递给 ScheduledUpdater 的 Updater 和传递给 SimpleHttpServer 的 Viewer,都替换为 Config,那会有什么问题呢?我们先来看一下,按照这个思路来实现的代码是什么样的。
+    ```java
+    public interface Config {
+      void update();
+      String outputInPlainText();
+      Map<String, String> output();
+    }
+    
+    public class RedisConfig implements Config {
+      //...需要实现Config的三个接口update/outputIn.../output
+    }
+    
+    public class KafkaConfig implements Config {
+      //...需要实现Config的三个接口update/outputIn.../output
+    }
+    
+    public class MysqlConfig implements Config {
+      //...需要实现Config的三个接口update/outputIn.../output
+    }
+    
+    public class ScheduledUpdater {
+      //...省略其他属性和方法..
+      private Config config;
+    
+      public ScheduleUpdater(Config config, long initialDelayInSeconds, long periodInSeconds) {
+          this.config = config;
+          //...
+      }
+      //...
+    }
+    
+    public class SimpleHttpServer {
+      private String host;
+      private int port;
+      private Map<String, List<Config>> viewers = new HashMap<>();
+     
+      public SimpleHttpServer(String host, int port) {//...}
+      
+      public void addViewer(String urlDirectory, Config config) {
+        if (!viewers.containsKey(urlDirectory)) {
+          viewers.put(urlDirectory, new ArrayList<Config>());
+        }
+        viewers.get(urlDirectory).add(config);
+      }
+      
+      public void run() { //... }
+    }
+    ```
+- 这样的设计思路也是能工作的,但是对比前后两个设计思路,在同样的代码量、实现复杂度、同等可读性的情况下,第一种设计思路显然要比第二种好很多。为什么这么说呢?主要有两点原因。
+    - 首先,第一种设计思路更加灵活、易扩展、易复用。因为 Updater、Viewer 职责更加单一,单一就意味了通用、复用性好。比如,我们现在又有一个新的需求,开发一个 Metrics 性能统计模块,并且希望将 Metrics 也通过 SimpleHttpServer 显示在网页上,以方便查看。这个时候,尽管 Metrics 跟 RedisConfig 等没有任何关系,但我们仍然可以让 Metrics 类实现非常通用的 Viewer 接口,复用 SimpleHttpServer 的代码实现。具体的代码如下所示:
+    ```java
+    public class ApiMetrics implements Viewer {//...}
+    public class DbMetrics implements Viewer {//...}
+    
+    public class Application {
+        ConfigSource configSource = new ZookeeperConfigSource();
+        public static final RedisConfig redisConfig = new RedisConfig(configSource);
+        public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
+        public static final MySqlConfig mySqlConfig = new MySqlConfig(configSource);
+        public static final ApiMetrics apiMetrics = new ApiMetrics();
+        public static final DbMetrics dbMetrics = new DbMetrics();
+        
+        public static void main(String[] args) {
+            SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389);
+            simpleHttpServer.addViewer("/config", redisConfig);
+            simpleHttpServer.addViewer("/config", mySqlConfig);
+            simpleHttpServer.addViewer("/metrics", apiMetrics);
+            simpleHttpServer.addViewer("/metrics", dbMetrics);
+            simpleHttpServer.run();
+        }
+    }
+    ```
+    - 第二种设计思路在代码实现上做了一些无用功。因为 Config 接口中包含两类不相关的接口,一类是 update(),一类是 output() 和 outputInPlainText()。理论上,KafkaConfig 只需要实现 update() 接口,并不需要实现 output() 相关的接口。同理,MysqlConfig 只需要实现 output() 相关接口,并需要实现 update() 接口。但第二种设计思路要求 RedisConfig、KafkaConfig、MySqlConfig 必须同时实现 Config 的所有接口函数(update、output、outputInPlainText)。除此之外,如果我们要往 Config 中继续添加一个新的接口,那所有的实现类都要改动。相反,如果我们的接口粒度比较小,那涉及改动的类就比较少。
+
+
+
+
+### 06.总结一下分享
+- 1.如何理解“接口隔离原则”?
+    - 理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。
+    - 如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
+    - 如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
+    - 如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
+- 2.接口隔离原则与单一职责原则的区别
+    - 单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
+
+
+
+### 07.思考一道课后题
+- java.util.concurrent 并发包提供了 AtomicInteger 这样一个原子类,其中有一个函数 getAndIncrement() 是这样定义的:给整数增加一,并且返回未増之前的值。我的问题是,这个函数的设计是否符合单一职责原则和接口隔离原则?为什么?
+    ```
+    /**
+     * Atomically increments by one the current value.
+     * @return the previous value
+     */
+    public final int getAndIncrement() {//...}
+    ```
+
+
+
+
+
+
+
+
+
+
+
+

+ 198 - 0
ReadDesign/02.设计模式原则/06.依赖倒置原则介绍.md

@@ -0,0 +1,198 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿简单介绍
+- 02.控制反转(IOC)
+- 03.依赖注入(DI)
+- 05.依赖注入框架
+- 06.依赖反转原则
+- 07.重点知识回顾
+
+
+
+### 01.前沿简单介绍
+- 依赖反转原则。单一职责原则和开闭原则的原理比较简单,但是,想要在实践中用好却比较难。而今天我们要讲到的依赖反转原则正好相反。这个原则用起来比较简单,但概念理解起来比较难。比如,下面这几个问题,你看看能否清晰地回答出来:
+    - “依赖反转”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“反转”两个字该如何理解?
+    - 经常听到另外两个概念:“控制反转”和“依赖注入”。这两个概念跟“依赖反转”有什么区别和联系呢?它们说的是同一个事情吗?
+    - 如果你熟悉 Java 语言,那 Spring 框架中的 IOC 跟这些概念又有什么关系呢?
+
+
+### 02.控制反转(IOC)
+- 在讲“依赖反转原则”之前,我们先讲一讲“控制反转”。控制反转的英文翻译是 Inversion Of Control,缩写为 IOC。此处我要强调一下,如果你是 Java 工程师的话,暂时别把这个“IOC”跟 Spring 框架的 IOC 联系在一起。先通过一个例子来看一下,什么是控制反转。
+    ```java
+    public class UserServiceTest {
+      public static boolean doTest() {
+        // ... 
+      }
+      
+      public static void main(String[] args) {//这部分逻辑可以放到框架中
+        if (doTest()) {
+          System.out.println("Test succeed.");
+        } else {
+          System.out.println("Test failed.");
+        }
+      }
+    }
+    ```
+- 在上面的代码中,所有的流程都由程序员来控制。如果我们抽象出一个下面这样一个框架,我们再来看,如何利用框架来实现同样的功能。具体的代码实现如下所示:
+    ```java
+    public class UserServiceTest extends TestCase {
+      @Override
+      public boolean doTest() {
+        // ... 
+      }
+    }
+    
+    // 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
+    JunitApplication.register(new UserServiceTest();
+    ```
+- 把这个简化版本的测试框架引入到工程中之后,我们只需要在框架预留的扩展点,也就是 TestCase 类中的 doTest() 抽象函数中,填充具体的测试代码就可以实现之前的功能了,完全不需要写负责执行流程的 main() 函数了。 具体的代码如下所示:
+    ```java
+    public class UserServiceTest extends TestCase {
+      @Override
+      public boolean doTest() {
+        // ... 
+      }
+    }
+    
+    // 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
+    JunitApplication.register(new UserServiceTest();
+    ```
+- 刚刚举的这个例子,就是典型的通过框架来实现“控制反转”的例子。框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。
+- 这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。
+- 实际上,实现控制反转的方法有很多,除了刚才例子中所示的类似于模板设计模式的方法之外,还有马上要讲到的依赖注入等方法,所以,**控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计**。
+
+
+
+### 03.依赖注入(DI)
+- 再来看依赖注入。依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧。依赖注入的英文翻译是 Dependency Injection,缩写为 DI。对于这个概念,有一个非常形象的说法,那就是:依赖注入是一个标价 25 美元,实际上只值 5 美分的概念。也就是说,这个概念听起来很“高大上”,实际上,理解、应用起来非常简单。
+- 那到底什么是依赖注入呢?我们用一句话来概括就是:不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
+- 还是通过一个例子来解释一下。在这个例子中,Notification 类负责消息推送,依赖 MessageSender 类实现推送商品促销、验证码等消息给用户。我们分别用依赖注入和非依赖注入两种方式来实现一下。具体的实现代码如下所示:
+    ```
+    // 非依赖注入实现方式
+    public class Notification {
+      private MessageSender messageSender;
+      
+      public Notification() {
+        this.messageSender = new MessageSender(); //此处有点像hardcode
+      }
+      
+      public void sendMessage(String cellphone, String message) {
+        //...省略校验逻辑等...
+        this.messageSender.send(cellphone, message);
+      }
+    }
+    
+    public class MessageSender {
+      public void send(String cellphone, String message) {
+        //....
+      }
+    }
+    // 使用Notification
+    Notification notification = new Notification();
+    
+    // 依赖注入的实现方式
+    public class Notification {
+      private MessageSender messageSender;
+      
+      // 通过构造函数将messageSender传递进来
+      public Notification(MessageSender messageSender) {
+        this.messageSender = messageSender;
+      }
+      
+      public void sendMessage(String cellphone, String message) {
+        //...省略校验逻辑等...
+        this.messageSender.send(cellphone, message);
+      }
+    }
+    //使用Notification
+    MessageSender messageSender = new MessageSender();
+    Notification notification = new Notification(messageSender);
+    ```
+- 通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类。这一点在我们之前讲“开闭原则”的时候也提到过。当然,上面代码还有继续优化的空间,我们还可以把 MessageSender 定义成接口,基于接口而非实现编程。改造后的代码如下所示:
+    ```java
+    public class Notification {
+      private MessageSender messageSender;
+      
+      public Notification(MessageSender messageSender) {
+        this.messageSender = messageSender;
+      }
+      
+      public void sendMessage(String cellphone, String message) {
+        this.messageSender.send(cellphone, message);
+      }
+    }
+    
+    public interface MessageSender {
+      void send(String cellphone, String message);
+    }
+    
+    // 短信发送类
+    public class SmsSender implements MessageSender {
+      @Override
+      public void send(String cellphone, String message) {
+        //....
+      }
+    }
+    
+    // 站内信发送类
+    public class InboxSender implements MessageSender {
+      @Override
+      public void send(String cellphone, String message) {
+        //....
+      }
+    }
+    
+    //使用Notification
+    MessageSender messageSender = new SmsSender();
+    Notification notification = new Notification(messageSender);
+    ```
+- 只需要掌握刚刚举的这个例子,就等于完全掌握了依赖注入。尽管依赖注入非常简单,但却非常有用,在后面的章节中,我们会讲到,它是编写可测试性代码最有效的手段。
+
+
+
+### 05.依赖注入框架
+- 弄懂了什么是“依赖注入”,我们再来看一下,什么是“依赖注入框架”。我们还是借用刚刚的例子来解释。
+- 在采用依赖注入实现的 Notification 类中,虽然我们不需要用类似 hard code 的方式,在类内部通过 new 来创建 MessageSender 对象,但是,这个创建对象、组装(或注入)对象的工作仅仅是被移动到了更上层代码而已,还是需要我们程序员自己来实现。具体代码如下所示:
+    ```
+    public class Demo {
+      public static final void main(String args[]) {
+        MessageSender sender = new SmsSender(); //创建对象
+        Notification notification = new Notification(sender);//依赖注入
+        notification.sendMessage("13918942177", "短信验证码:2346");
+      }
+    }
+    ```
+- 在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。
+- 可能已经猜到,这个框架就是“依赖注入框架”。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
+
+
+
+### 06.依赖反转原则
+- 前面讲了控制反转、依赖注入、依赖注入框架,现在,我们来讲一讲今天的主角:依赖反转原则。依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫依赖倒置原则。
+- 为了追本溯源,我先给出这条原则最原汁原味的英文描述:High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
+- 将它翻译成中文,大概意思就是:高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。
+- 所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上,这条原则主要还是用来指导框架层面的设计,跟前面讲到的控制反转类似。我们拿 Tomcat 这个 Servlet 容器作为例子来解释一下。
+- Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。
+
+
+
+
+### 07.重点知识回顾
+- 1.控制反转
+    - 实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。
+- 2.依赖注入
+    - 依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。
+- 3.依赖注入
+    - 框架我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
+- 4.依赖反转
+    - 原则依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
+
+
+
+
+
+
+
+
+
+

+ 260 - 0
ReadDesign/02.设计模式原则/07.迪米特原则介绍.md

@@ -0,0 +1,260 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿简单介绍
+- 02.何为高内聚松耦合
+- 03.内聚和耦合关系
+- 04.迪米特法则描述
+- 05.代码实战一
+- 06.代码实战二
+- 07.辩证思考与应用
+- 08.回顾总结一下
+
+
+
+### 00.问题思考分析
+- 1.什么是迪米特原则,这个原则如何理解,如何运用到实际开发,举例说明一下?
+- 2.什么是高内聚松耦合,能否举例说明一下
+
+
+
+### 01.前沿简单介绍
+- 迪米特法则。尽管它不像 SOLID、KISS、DRY 原则那样,人尽皆知,但它却非常实用。利用这个原则,能够帮我们实现代码的“高内聚、松耦合”。今天,我们就围绕下面几个问题,并结合两个代码实战案例,来深入地学习这个法则。
+    - 什么是“高内聚、松耦合”?
+    - 如何利用迪米特法则来实现“高内聚、松耦合”?
+    - 有哪些代码设计是明显违背迪米特法则的?对此又该如何重构?
+
+
+
+### 02.何为高内聚松耦合
+- “高内聚、松耦合”是一个非常重要的设计思想,能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。实际上,在前面的章节中,我们已经多次提到过这个设计思想。很多设计原则都以实现代码的“高内聚、松耦合”为目的,比如单一职责原则、基于接口而非实现编程等。
+- 实际上,“高内聚、松耦合”是一个比较通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。为了方便我讲解,接下来我以“类”作为这个设计思想的应用对象来展开讲解,其他应用场景你可以自行类比。
+- 在这个设计思想中,“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。不过,这两者并非完全独立不相干。高内聚有助于松耦合,松耦合又需要高内聚的支持。
+- 什么是“高内聚”呢?
+    - 所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。实际上,我们前面讲过的单一职责原则是实现代码高内聚非常有效的设计原则。
+- 什么是“松耦合”?
+    - 所谓松耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。实际上,我们前面讲的依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合。
+
+
+
+### 03.内聚和耦合关系
+- “高内聚”有助于“松耦合”,同理,“低内聚”也会导致“紧耦合”。关于这一点,我画了一张对比图来解释。图中左边部分的代码结构是“高内聚、松耦合”;右边部分正好相反,是“低内聚、紧耦合”。
+    - ![image](https://static001.geekbang.org/resource/image/62/3c/62275095f1f5817cad8a9ca129a6ec3c.jpg)
+- 图中左边部分的代码设计中,类的粒度比较小,每个类的职责都比较单一。相近的功能都放到了一个类中,不相近的功能被分割到了多个类中。这样类更加独立,代码的内聚性更好。因为职责单一,所以每个类被依赖的类就会比较少,代码低耦合。一个类的修改,只会影响到一个依赖类的代码改动。我们只需要测试这一个依赖类是否还能正常工作就行了。
+- 图中右边部分的代码设计中,类粒度比较大,低内聚,功能大而全,不相近的功能放到了一个类中。这就导致很多其他类都依赖这个类。当我们修改这个类的某一个功能代码的时候,会影响依赖它的多个类。我们需要测试这三个依赖类,是否还能正常工作。这也就是所谓的“牵一发而动全身”。
+
+
+
+### 04.迪米特法则描述
+- 迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD。单从这个名字上来看,我们完全猜不出这个原则讲的是什么。不过,它还有另外一个更加达意的名字,叫作最小知识原则,英文翻译为:The Least Knowledge Principle。
+- 关于这个设计原则,我们先来看一下它最原汁原味的英文定义:
+    - Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.
+- 把它直译成中文,就是下面这个样子:
+    - 每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
+- 之前讲过,大部分设计原则和思想都非常抽象,有各种各样的解读,要想灵活地应用到实际的开发中,需要有实战经验的积累。迪米特法则也不例外。所以,我结合我自己的理解和经验,对刚刚的定义重新描述一下。注意,为了统一讲解,我把定义描述中的“模块”替换成了“类”。
+    - 不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
+
+
+
+### 05.代码实战一
+- 先来看这条原则中的前半部分,“不该有直接依赖关系的类之间,不要有依赖”。我举个例子解释一下。
+- 这个例子实现了简化版的搜索引擎爬取网页的功能。代码中包含三个主要的类。其中,NetworkTransporter 类负责底层网络通信,根据请求获取数据;HtmlDownloader 类用来通过 URL 获取网页;Document 表示网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象。具体的代码实现如下所示:
+    ```java
+    public class NetworkTransporter {
+        // 省略属性和其他方法...
+        public Byte[] send(HtmlRequest htmlRequest) {
+          //...
+        }
+    }
+    
+    public class HtmlDownloader {
+      private NetworkTransporter transporter;//通过构造函数或IOC注入
+      
+      public Html downloadHtml(String url) {
+        Byte[] rawHtml = transporter.send(new HtmlRequest(url));
+        return new Html(rawHtml);
+      }
+    }
+    
+    public class Document {
+      private Html html;
+      private String url;
+      
+      public Document(String url) {
+        this.url = url;
+        HtmlDownloader downloader = new HtmlDownloader();
+        this.html = downloader.downloadHtml(url);
+      }
+      //...
+    }
+    ```
+- 这段代码虽然“能用”,能实现我们想要的功能,但是它不够“好用”,有比较多的设计缺陷。你可以先试着思考一下,看看都有哪些缺陷,然后再来看我下面的讲解。
+- 首先,我们来看 NetworkTransporter 类。作为一个底层网络通信类,我们希望它的功能尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象 HtmlRequest。从这一点上讲,NetworkTransporter 类的设计违背迪米特法则,依赖了不该有直接依赖关系的 HtmlRequest 类。
+- 如何进行重构,让 NetworkTransporter 类满足迪米特法则呢?我这里有个形象的比喻。假如你现在要去商店买东西,你肯定不会直接把钱包给收银员,让收银员自己从里面拿钱,而是你从钱包里把钱拿出来交给收银员。这里的 HtmlRequest 对象就相当于钱包,HtmlRequest 里的 address 和 content 对象就相当于钱。我们应该把 address 和 content 交给 NetworkTransporter,而非是直接把 HtmlRequest 交给 NetworkTransporter。根据这个思路,NetworkTransporter 重构之后的代码如下所示:
+    ```java
+    public class NetworkTransporter {
+        // 省略属性和其他方法...
+        public Byte[] send(String address, Byte[] data) {
+          //...
+        }
+    }
+    ```
+- 我们再来看 HtmlDownloader 类。这个类的设计没有问题。不过,我们修改了 NetworkTransporter 的 send() 函数的定义,而这个类用到了 send() 函数,所以我们需要对它做相应的修改,修改后的代码如下所示:
+    ```java
+    public class HtmlDownloader {
+      private NetworkTransporter transporter;//通过构造函数或IOC注入
+      
+      // HtmlDownloader这里也要有相应的修改
+      public Html downloadHtml(String url) {
+        HtmlRequest htmlRequest = new HtmlRequest(url);
+        Byte[] rawHtml = transporter.send(
+          htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
+        return new Html(rawHtml);
+      }
+    }
+    ```
+- 最后,我们来看下 Document 类。这个类的问题比较多,主要有三点。第一,构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代码的可测试性。代码的可测试性我们后面会讲到,这里你先知道有这回事就可以了。第二,HtmlDownloader 对象在构造函数中通过 new 来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性。第三,从业务含义上来讲,Document 网页文档没必要依赖 HtmlDownloader 类,违背了迪米特法则。
+- 虽然 Document 类的问题很多,但修改起来比较简单,只要一处改动就可以解决所有问题。修改之后的代码如下所示:
+    ```java
+    public class Document {
+      private Html html;
+      private String url;
+      
+      public Document(String url, Html html) {
+        this.html = html;
+        this.url = url;
+      }
+      //...
+    }
+    
+    // 通过一个工厂方法来创建Document
+    public class DocumentFactory {
+      private HtmlDownloader downloader;
+      
+      public DocumentFactory(HtmlDownloader downloader) {
+        this.downloader = downloader;
+      }
+      
+      public Document createDocument(String url) {
+        Html html = downloader.downloadHtml(url);
+        return new Document(url, html);
+      }
+    }
+    ```
+
+
+### 06.代码实战二
+- 再来看一下这条原则中的后半部分:“有依赖关系的类之间,尽量只依赖必要的接口”。我们还是结合一个例子来讲解。下面这段代码非常简单,Serialization 类负责对象的序列化和反序列化。
+    ```java
+    public class Serialization {
+      public String serialize(Object object) {
+        String serializedResult = ...;
+        //...
+        return serializedResult;
+      }
+      
+      public Object deserialize(String str) {
+        Object deserializedResult = ...;
+        //...
+        return deserializedResult;
+      }
+    }
+    ```
+- 单看这个类的设计,没有一点问题。不过,如果我们把它放到一定的应用场景里,那就还有继续优化的空间。假设在我们的项目中,有些类只用到了序列化操作,而另一些类只用到反序列化操作。那基于迪米特法则后半部分“有依赖关系的类之间,尽量只依赖必要的接口”,只用到序列化操作的那部分类不应该依赖反序列化接口。同理,只用到反序列化操作的那部分类不应该依赖序列化接口。
+- 根据这个思路,我们应该将 Serialization 类拆分为两个更小粒度的类,一个只负责序列化(Serializer 类),一个只负责反序列化(Deserializer 类)。拆分之后,使用序列化操作的类只需要依赖 Serializer 类,使用反序列化操作的类只需要依赖 Deserializer 类。拆分之后的代码如下所示:
+    ```java
+    public class Serializer {
+      public String serialize(Object object) {
+        String serializedResult = ...;
+        ...
+        return serializedResult;
+      }
+    }
+    
+    public class Deserializer {
+      public Object deserialize(String str) {
+        Object deserializedResult = ...;
+        ...
+        return deserializedResult;
+      }
+    }
+    ```
+- 不知道你有没有看出来,尽管拆分之后的代码更能满足迪米特法则,但却违背了高内聚的设计思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的地方不至于过于分散。对于刚刚这个例子来说,如果我们修改了序列化的实现方式,比如从 JSON 换成了 XML,那反序列化的实现逻辑也需要一并修改。在未拆分的情况下,我们只需要修改一个类即可。在拆分之后,我们需要修改两个类。显然,这种设计思路的代码改动范围变大了。
+- 如果我们既不想违背高内聚的设计思想,也不想违背迪米特法则,那我们该如何解决这个问题呢?实际上,通过引入两个接口就能轻松解决这个问题,具体的代码如下所示。
+    ```java
+    public interface Serializable {
+      String serialize(Object object);
+    }
+    
+    public interface Deserializable {
+      Object deserialize(String text);
+    }
+    
+    public class Serialization implements Serializable, Deserializable {
+      @Override
+      public String serialize(Object object) {
+        String serializedResult = ...;
+        ...
+        return serializedResult;
+      }
+      
+      @Override
+      public Object deserialize(String str) {
+        Object deserializedResult = ...;
+        ...
+        return deserializedResult;
+      }
+    }
+    
+    public class DemoClass_1 {
+      private Serializable serializer;
+      
+      public Demo(Serializable serializer) {
+        this.serializer = serializer;
+      }
+      //...
+    }
+    
+    public class DemoClass_2 {
+      private Deserializable deserializer;
+      
+      public Demo(Deserializable deserializer) {
+        this.deserializer = deserializer;
+      }
+      //...
+    }
+    ```
+- 尽管我们还是要往 DemoClass_1 的构造函数中,传入包含序列化和反序列化的 Serialization 实现类,但是,我们依赖的 Serializable 接口只包含序列化操作,DemoClass_1 无法使用 Serialization 类中的反序列化接口,对反序列化操作无感知,这也就符合了迪米特法则后半部分所说的“依赖有限接口”的要求。
+- 实际上,上面的的代码实现思路,也体现了“基于接口而非实现编程”的设计原则,结合迪米特法则,我们可以总结出一条新的设计原则,那就是“基于最小接口而非最大实现编程”。
+
+
+
+
+### 07.辩证思考与应用
+- 对于实战二最终的设计思路,你有没有什么不同的观点呢?
+- 整个类只包含序列化和反序列化两个操作,只用到序列化操作的使用者,即便能够感知到仅有的一个反序列化函数,问题也不大。那为了满足迪米特法则,我们将一个非常简单的类,拆分出两个接口,是否有点过度设计的意思呢?
+- 设计原则本身没有对错,只有能否用对之说。不要为了应用设计原则而应用设计原则,我们在应用设计原则的时候,一定要具体问题具体分析。
+- 对于刚刚这个 Serialization 类来说,只包含两个操作,确实没有太大必要拆分成两个接口。但是,如果我们对 Serialization 类添加更多的功能,实现更多更好用的序列化、反序列化函数,我们来重新考虑一下这个问题。修改之后的具体的代码如下:
+    ```java
+    public class Serializer { // 参看JSON的接口定义
+      public String serialize(Object object) { //... }
+      public String serializeMap(Map map) { //... }
+      public String serializeList(List list) { //... }
+      
+      public Object deserialize(String objectString) { //... }
+      public Map deserializeMap(String mapString) { //... }
+      public List deserializeList(String listString) { //... }
+    }
+    ```
+- 在这种场景下,第二种设计思路要更好些。因为基于之前的应用场景来说,大部分代码只需要用到序列化的功能。对于这部分使用者,没必要了解反序列化的“知识”,而修改之后的 Serialization 类,反序列化的“知识”,从一个函数变成了三个。一旦任一反序列化操作有代码改动,我们都需要检查、测试所有依赖 Serialization 类的代码是否还能正常工作。为了减少耦合和测试工作量,我们应该按照迪米特法则,将反序列化和序列化的功能隔离开来。
+
+
+
+### 08.回顾总结一下
+- 1.如何理解“高内聚、松耦合”?
+    - “高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。
+    - 所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
+- 2.如何理解“迪米特法则”?
+    - 不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。
+
+
+

+ 186 - 0
ReadDesign/02.设计模式原则/08.KISS和YAGNI原则.md

@@ -0,0 +1,186 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿简单介绍
+- 02.理解kiss原则
+- 03.代码越少越好么
+- 04.代码复杂违背KISS么
+- 05.如何写合格代码
+- 06.YAGNI跟KISS对比
+- 07.总结分享一下
+
+
+
+### 00.前沿简单介绍
+- 1.什么是kiss原则,如何评断代码符合kiss原则?
+
+
+
+### 01.前沿简单介绍
+- KISS 原则和 YAGNI 原则。其中,KISS 原则比较经典,耳熟能详,但 YAGNI 你可能没怎么听过,不过它理解起来也不难。
+- 理解这两个原则时候,经常会有一个共同的问题,那就是,看一眼就感觉懂了,但深究的话,又有很多细节问题不是很清楚。比如,怎么理解 KISS 原则中“简单”两个字?什么样的代码才算“简单”?怎样的代码才算“复杂”?如何才能写出“简单”的代码?YAGNI 原则跟 KISS 原则说的是一回事吗?
+
+
+
+### 02.理解kiss原则
+- KISS 原则的英文描述有好几个版本,比如下面这几个。
+    - Keep It Simple and Stupid.
+    - Keep It Short and Simple.
+    - Keep It Simple and Straightforward.
+- 不过,仔细看你就会发现,它们要表达的意思其实差不多,翻译成中文就是:尽量保持简单。
+- KISS 原则算是一个万金油类型的设计原则。代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。
+- 不过,这条原则只是告诉我们,要保持代码“Simple and Stupid”,但并没有讲到,什么样的代码才是“Simple and Stupid”的,更没有给出特别明确的方法论,来指导如何开发出“Simple and Stupid”的代码。所以,看着非常简单,但不能落地,这就有点像我们常说的“心灵鸡汤”。哦,咱们这里应该叫“技术鸡汤”。
+
+
+### 03.代码越少越好么
+- 先一起看一个例子。下面这三段代码可以实现同样一个功能:检查输入的字符串 ipAddress 是否是合法的 IP 地址。
+- 一个合法的 IP 地址由四个数字组成,并且通过“.”来进行分割。每组数字的取值范围是 0~255。第一组数字比较特殊,不允许为 0。对比这三段代码,你觉得哪一段代码最符合 KISS 原则呢?如果让你来实现这个功能,你会选择用哪种实现方法呢?你可以先自己思考一下,然后再看我下面的讲解。
+    ```java
+    // 第一种实现方式: 使用正则表达式
+    public boolean isValidIpAddressV1(String ipAddress) {
+      if (StringUtils.isBlank(ipAddress)) return false;
+      String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
+              + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+              + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+              + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
+      return ipAddress.matches(regex);
+    }
+    
+    // 第二种实现方式: 使用现成的工具类
+    public boolean isValidIpAddressV2(String ipAddress) {
+      if (StringUtils.isBlank(ipAddress)) return false;
+      String[] ipUnits = StringUtils.split(ipAddress, '.');
+      if (ipUnits.length != 4) {
+        return false;
+      }
+      for (int i = 0; i < 4; ++i) {
+        int ipUnitIntValue;
+        try {
+          ipUnitIntValue = Integer.parseInt(ipUnits[i]);
+        } catch (NumberFormatException e) {
+          return false;
+        }
+        if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {
+          return false;
+        }
+        if (i == 0 && ipUnitIntValue == 0) {
+          return false;
+        }
+      }
+      return true;
+    }
+    
+    // 第三种实现方式: 不使用任何工具类
+    public boolean isValidIpAddressV3(String ipAddress) {
+      char[] ipChars = ipAddress.toCharArray();
+      int length = ipChars.length;
+      int ipUnitIntValue = -1;
+      boolean isFirstUnit = true;
+      int unitsCount = 0;
+      for (int i = 0; i < length; ++i) {
+        char c = ipChars[i];
+        if (c == '.') {
+          if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
+          if (isFirstUnit && ipUnitIntValue == 0) return false;
+          if (isFirstUnit) isFirstUnit = false;
+          ipUnitIntValue = -1;
+          unitsCount++;
+          continue;
+        }
+        if (c < '0' || c > '9') {
+          return false;
+        }
+        if (ipUnitIntValue == -1) ipUnitIntValue = 0;
+        ipUnitIntValue = ipUnitIntValue * 10 + (c - '0');
+      }
+      if (ipUnitIntValue < 0 || ipUnitIntValue > 255) return false;
+      if (unitsCount != 3) return false;
+      return true;
+    }
+    ```
+    - 第一种实现方式利用的是正则表达式,只用三行代码就把这个问题搞定了。它的代码行数最少,那是不是就最符合 KISS 原则呢?答案是否定的。虽然代码行数最少,看似最简单,实际上却很复杂。这正是因为它使用了正则表达式。一方面,正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则的设计初衷上来讲,这种实现方式并不符合 KISS 原则。
+    - 第二种实现方式使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP 地址字符串。第三种实现方式,不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法。从代码行数上来说,这两种方式差不多。但是,第三种要比第二种更加有难度,更容易写出 bug。从可读性上来说,第二种实现方式的代码逻辑更清晰、更好理解。所以,在这两种实现方式中,第二种实现方式更加“简单”,更加符合 KISS 原则。
+- 从性能的角度来说,选择第三种实现方式是不是更好些呢?
+    - 在回答这个问题之前,我先解释一下,为什么说第三种实现方式性能会更高一些。一般来说,工具类的功能都比较通用和全面,所以,在代码实现上,需要考虑和处理更多的细节,执行效率就会有所影响。而第三种实现方式,完全是自己操作底层字符,只针对 IP 地址这一种格式的数据输入来做处理,没有太多多余的函数调用和其他不必要的处理逻辑,所以,在执行效率上,这种类似定制化的处理代码方式肯定比通用的工具类要高些。
+    - 不过,尽管第三种实现方式性能更高些,但我还是更倾向于选择第二种实现方法。那是因为第三种实现方式实际上是一种过度优化。除非 isValidIpAddress() 函数是影响系统性能的瓶颈代码,否则,这样优化的投入产出比并不高,增加了代码实现的难度、牺牲了代码的可读性,性能上的提升却并不明显。
+
+
+
+### 04.代码复杂违背KISS么
+- 并不是代码行数越少就越“简单”,还要考虑逻辑复杂度、实现难度、代码的可读性等。那如果一段代码的逻辑复杂、实现难度大、可读性也不太好,是不是就一定违背 KISS 原则呢?在回答这个问题之前,我们先来看下面这段代码:
+    ```java
+    // KMP algorithm: a, b分别是主串和模式串;n, m分别是主串和模式串的长度。
+    public static int kmp(char[] a, int n, char[] b, int m) {
+      int[] next = getNexts(b, m);
+      int j = 0;
+      for (int i = 0; i < n; ++i) {
+        while (j > 0 && a[i] != b[j]) { // 一直找到a[i]和b[j]
+          j = next[j - 1] + 1;
+        }
+        if (a[i] == b[j]) {
+          ++j;
+        }
+        if (j == m) { // 找到匹配模式串的了
+          return i - m + 1;
+        }
+      }
+      return -1;
+    }
+    
+    // b表示模式串,m表示模式串的长度
+    private static int[] getNexts(char[] b, int m) {
+      int[] next = new int[m];
+      next[0] = -1;
+      int k = -1;
+      for (int i = 1; i < m; ++i) {
+        while (k != -1 && b[k + 1] != b[i]) {
+          k = next[k];
+        }
+        if (b[k + 1] == b[i]) {
+          ++k;
+        }
+        next[i] = k;
+      }
+      return next;
+    }
+    ```
+- 这段代码完全符合我们刚提到的逻辑复杂、实现难度大、可读性差的特点,但它并不违反 KISS 原则。为什么这么说呢?
+    - KMP 算法以快速高效著称。当我们需要处理长文本字符串匹配问题(几百 MB 大小文本内容的匹配),或者字符串匹配是某个产品的核心功能(比如 Vim、Word 等文本编辑器),又或者字符串匹配算法是系统性能瓶颈的时候,我们就应该选择尽可能高效的 KMP 算法。而 KMP 算法本身具有逻辑复杂、实现难度大、可读性差的特点。本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。
+    - 不过,平时的项目开发中涉及的字符串匹配问题,大部分都是针对比较小的文本。在这种情况下,直接调用编程语言提供的现成的字符串匹配函数就足够了。如果非得用 KMP 算法、BM 算法来实现字符串匹配,那就真的违背 KISS 原则了。也就是说,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。
+
+
+### 05.如何写合格代码
+- 实际上,我们前面已经讲到了一些方法。这里我稍微总结一下。
+    - 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
+    - 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
+    - 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
+- 实际上,代码是否足够简单是一个挺主观的评判。
+    - 同样的代码,有的人觉得简单,有的人觉得不够简单。而往往自己编写的代码,自己都会觉得够简单。所以,评判代码是否简单,还有一个很有效的间接方法,那就是 code review。如果在 code review 的时候,同事对你的代码有很多疑问,那就说明你的代码有可能不够“简单”,需要优化啦。
+
+
+
+### 06.YAGNI跟KISS对比
+- YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。这条原则也算是万金油了。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。
+- 比如,我们的系统暂时只用 Redis 存储配置信息,以后可能会用到 ZooKeeper。根据 YAGNI 原则,在未用到 ZooKeeper 之前,我们没必要提前编写这部分代码。当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候,再去实现 ZooKeeper 存储配置信息这部分代码。
+- 再比如,我们不要在项目中提前引入不需要依赖的开发包。对于 Java 程序员来说,我们经常使用 Maven 或者 Gradle 来管理依赖的类库(library)。我发现,有些同事为了避免开发中 library 包缺失而频繁地修改 Maven 或者 Gradle 配置文件,提前往项目里引入大量常用的 library 包。实际上,这样的做法也是违背 YAGNI 原则的。
+
+
+### 07.总结分享一下
+- KISS 原则是保持代码可读和可维护的重要手段。KISS 原则中的“简单”并不是以代码行数来考量的。
+    - 代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。除此之外,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。
+- 对于如何写出满足 KISS 原则的代码,我还总结了下面几条指导原则:
+    - 不要使用同事可能不懂的技术来实现代码;
+    - 不要重复造轮子,要善于使用已经有的工具类库;
+    - 不要过度优化。
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 281 - 0
ReadDesign/02.设计模式原则/09.DRY原则简单介绍.md

@@ -0,0 +1,281 @@
+#### 目录介绍
+- 00.问题思考分析
+- 01.前沿基础介绍
+- 02.什么是DRY原则
+- 03.实现逻辑重复
+- 04.功能语义重复
+- 05.代码执行重复
+- 06.代码复用性
+- 07.辩证思考和应用
+- 08.总结分享一下
+
+
+
+### 00.问题思考分析
+
+
+
+### 01.前沿基础介绍
+- DRY 原则。它的英文描述为:Don’t Repeat Yourself。中文直译为:不要重复自己。将它应用在编程中,可以理解为:不要写重复的代码。
+- 你可能会觉得,这条原则非常简单、非常容易应用。只要两段代码长得一样,那就是违反 DRY 原则了。真的是这样吗?答案是否定的。这是很多人对这条原则存在的误解。实际上,重复的代码不一定违反 DRY 原则,而且有些看似不重复的代码也有可能违反 DRY 原则。
+
+
+
+### 02.什么是DRY原则
+- DRY 原则的定义非常简单,我就不再过度解读。今天,我们主要讲三种典型的代码重复情况,它们分别是:实现逻辑重复、功能语义重复和代码执行重复。这三种代码重复,有的看似违反 DRY,实际上并不违反;有的看似不违反,实际上却违反了。
+
+
+
+
+### 03.实现逻辑重复
+- 先来看下面这样一段代码是否违反了 DRY 原则。如果违反了,你觉得应该如何重构,才能让它满足 DRY 原则?如果没有违反,那又是为什么呢?
+    ```java
+    public class UserAuthenticator {
+      public void authenticate(String username, String password) {
+        if (!isValidUsername(username)) {
+          // ...throw InvalidUsernameException...
+        }
+        if (!isValidPassword(password)) {
+          // ...throw InvalidPasswordException...
+        }
+        //...省略其他代码...
+      }
+    
+      private boolean isValidUsername(String username) {
+        // check not null, not empty
+        if (StringUtils.isBlank(username)) {
+          return false;
+        }
+        // check length: 4~64
+        int length = username.length();
+        if (length < 4 || length > 64) {
+          return false;
+        }
+        // contains only lowcase characters
+        if (!StringUtils.isAllLowerCase(username)) {
+          return false;
+        }
+        // contains only a~z,0~9,dot
+        for (int i = 0; i < length; ++i) {
+          char c = username.charAt(i);
+          if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
+            return false;
+          }
+        }
+        return true;
+      }
+    
+      private boolean isValidPassword(String password) {
+        // check not null, not empty
+        if (StringUtils.isBlank(password)) {
+          return false;
+        }
+        // check length: 4~64
+        int length = password.length();
+        if (length < 4 || length > 64) {
+          return false;
+        }
+        // contains only lowcase characters
+        if (!StringUtils.isAllLowerCase(password)) {
+          return false;
+        }
+        // contains only a~z,0~9,dot
+        for (int i = 0; i < length; ++i) {
+          char c = password.charAt(i);
+          if (!(c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.') {
+            return false;
+          }
+        }
+        return true;
+      }
+    }
+    ```
+- 代码很简单,我就不做过多解释了。在代码中,有两处非常明显的重复的代码片段:isValidUserName() 函数和 isValidPassword() 函数。重复的代码被敲了两遍,或者简单 copy-paste 了一下,看起来明显违反 DRY 原则。为了移除重复的代码,我们对上面的代码做下重构,将 isValidUserName() 函数和 isValidPassword() 函数,合并为一个更通用的函数 isValidUserNameOrPassword()。重构后的代码如下所示:
+    ```java
+    public class UserAuthenticatorV2 {
+    
+      public void authenticate(String userName, String password) {
+        if (!isValidUsernameOrPassword(userName)) {
+          // ...throw InvalidUsernameException...
+        }
+    
+        if (!isValidUsernameOrPassword(password)) {
+          // ...throw InvalidPasswordException...
+        }
+      }
+    
+      private boolean isValidUsernameOrPassword(String usernameOrPassword) {
+        //省略实现逻辑
+        //跟原来的isValidUsername()或isValidPassword()的实现逻辑一样...
+        return true;
+      }
+    }
+    ```
+- 经过重构之后,代码行数减少了,也没有重复的代码了,是不是更好了呢?答案是否定的,这可能跟你预期的不一样,我来解释一下为什么。
+- 单从名字上看,我们就能发现,合并之后的 isValidUserNameOrPassword() 函数,负责两件事情:验证用户名和验证密码,违反了“单一职责原则”和“接口隔离原则”。实际上,即便将两个函数合并成 isValidUserNameOrPassword(),代码仍然存在问题。
+- 因为 isValidUserName() 和 isValidPassword() 两个函数,虽然从代码实现逻辑上看起来是重复的,但是从语义上并不重复。所谓“语义不重复”指的是:从功能上来看,这两个函数干的是完全不重复的两件事情,一个是校验用户名,另一个是校验密码。尽管在目前的设计中,两个校验逻辑是完全一样的,但如果按照第二种写法,将两个函数的合并,那就会存在潜在的问题。在未来的某一天,如果我们修改了密码的校验逻辑,比如,允许密码包含大写字符,允许密码的长度为 8 到 64 个字符,那这个时候,isValidUserName() 和 isValidPassword() 的实现逻辑就会不相同。我们就要把合并后的函数,重新拆成合并前的那两个函数。
+- 尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。比如将校验只包含 a~z、0~9、dot 的逻辑封装成 boolean onlyContains(String str, String charlist); 函数。
+
+
+
+### 04.功能语义重复
+- 现在我们再来看另外一个例子。在同一个项目代码中有下面两个函数:isValidIp() 和 checkIfIpValid()。尽管两个函数的命名不同,实现逻辑不同,但功能是相同的,都是用来判定 IP 地址是否合法的。
+- 之所以在同一个项目中会有两个功能相同的函数,那是因为这两个函数是由两个不同的同事开发的,其中一个同事在不知道已经有了 isValidIp() 的情况下,自己又定义并实现了同样用来校验 IP 地址是否合法的 checkIfIpValid() 函数。
+- 那在同一项目代码中,存在如下两个函数,是否违反 DRY 原则呢?
+    ```java
+    public boolean isValidIp(String ipAddress) {
+      if (StringUtils.isBlank(ipAddress)) return false;
+      String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
+              + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+              + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+              + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
+      return ipAddress.matches(regex);
+    }
+    
+    public boolean checkIfIpValid(String ipAddress) {
+      if (StringUtils.isBlank(ipAddress)) return false;
+      String[] ipUnits = StringUtils.split(ipAddress, '.');
+      if (ipUnits.length != 4) {
+        return false;
+      }
+      for (int i = 0; i < 4; ++i) {
+        int ipUnitIntValue;
+        try {
+          ipUnitIntValue = Integer.parseInt(ipUnits[i]);
+        } catch (NumberFormatException e) {
+          return false;
+        }
+        if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {
+          return false;
+        }
+        if (i == 0 && ipUnitIntValue == 0) {
+          return false;
+        }
+      }
+      return true;
+    }
+    ```
+- 这个例子跟上个例子正好相反。上一个例子是代码实现逻辑重复,但语义不重复,我们并不认为它违反了 DRY 原则。而在这个例子中,尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。
+- 假设我们不统一实现思路,那有些地方调用了 isValidIp() 函数,有些地方又调用了 checkIfIpValid() 函数,这就会导致代码看起来很奇怪,相当于给代码“埋坑”,给不熟悉这部分代码的同事增加了阅读的难度。同事有可能研究了半天,觉得功能是一样的,但又有点疑惑,觉得是不是有更高深的考量,才定义了两个功能类似的函数,最终发现居然是代码设计的问题。
+- 除此之外,如果哪天项目中 IP 地址是否合法的判定规则改变了,比如:255.255.255.255 不再被判定为合法的了,相应地,我们对 isValidIp() 的实现逻辑做了相应的修改,但却忘记了修改 checkIfIpValid() 函数。又或者,我们压根就不知道还存在一个功能相同的 checkIfIpValid() 函数,这样就会导致有些代码仍然使用老的 IP 地址判断逻辑,导致出现一些莫名其妙的 bug。
+
+
+
+### 05.代码执行重复
+- 前两个例子一个是实现逻辑重复,一个是语义重复,我们再来看第三个例子。其中,UserService 中 login() 函数用来校验用户登录是否成功。如果失败,就返回异常;如果成功,就返回用户信息。具体代码如下所示:
+    ```java
+    public class UserService {
+      private UserRepo userRepo;//通过依赖注入或者IOC框架注入
+    
+      public User login(String email, String password) {
+        boolean existed = userRepo.checkIfUserExisted(email, password);
+        if (!existed) {
+          // ... throw AuthenticationFailureException...
+        }
+        User user = userRepo.getUserByEmail(email);
+        return user;
+      }
+    }
+    
+    public class UserRepo {
+      public boolean checkIfUserExisted(String email, String password) {
+        if (!EmailValidation.validate(email)) {
+          // ... throw InvalidEmailException...
+        }
+    
+        if (!PasswordValidation.validate(password)) {
+          // ... throw InvalidPasswordException...
+        }
+    
+        //...query db to check if email&password exists...
+      }
+    
+      public User getUserByEmail(String email) {
+        if (!EmailValidation.validate(email)) {
+          // ... throw InvalidEmailException...
+        }
+        //...query db to get user by email...
+      }
+    }
+    ```
+- 上面这段代码,既没有逻辑重复,也没有语义重复,但仍然违反了 DRY 原则。这是因为代码中存在“执行重复”。我们一块儿来看下,到底哪些代码被重复执行了?
+- 重复执行最明显的一个地方,就是在 login() 函数中,email 的校验逻辑被执行了两次。一次是在调用 checkIfUserExisted() 函数的时候,另一次是调用 getUserByEmail() 函数的时候。这个问题解决起来比较简单,我们只需要将校验逻辑从 UserRepo 中移除,统一放到 UserService 中就可以了。
+- 除此之外,代码中还有一处比较隐蔽的执行重复,不知道你发现了没有?实际上,login() 函数并不需要调用 checkIfUserExisted() 函数,只需要调用一次 getUserByEmail() 函数,从数据库中获取到用户的 email、password 等信息,然后跟用户输入的 email、password 信息做对比,依次判断是否登录成功。
+- 按照刚刚的修改思路,我们把代码重构一下,移除“重复执行”的代码,只校验一次 email 和 password,并且只查询一次数据库。重构之后的代码如下所示:
+    ```java
+    public class UserService {
+      private UserRepo userRepo;//通过依赖注入或者IOC框架注入
+    
+      public User login(String email, String password) {
+        if (!EmailValidation.validate(email)) {
+          // ... throw InvalidEmailException...
+        }
+        if (!PasswordValidation.validate(password)) {
+          // ... throw InvalidPasswordException...
+        }
+        User user = userRepo.getUserByEmail(email);
+        if (user == null || !password.equals(user.getPassword()) {
+          // ... throw AuthenticationFailureException...
+        }
+        return user;
+      }
+    }
+    
+    public class UserRepo {
+      public boolean checkIfUserExisted(String email, String password) {
+        //...query db to check if email&password exists
+      }
+    
+      public User getUserByEmail(String email) {
+        //...query db to get user by email...
+      }
+    }
+    ```
+
+
+### 06.代码复用性
+- 什么是代码的复用性?
+    - 我们首先来区分三个概念:代码复用性(Code Reusability)、代码复用(Code Resue)和 DRY 原则。
+    - 代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码。代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用。DRY 原则是一条原则:不要写重复的代码。从定义描述上,它们好像有点类似,但深究起来,三者的区别还是蛮大的。
+    - 首先,“不重复”并不代表“可复用”。在一个项目代码中,可能不存在任何重复的代码,但也并不表示里面有可复用的代码,不重复和可复用完全是两个概念。所以,从这个角度来说,DRY 原则跟代码的可复用性讲的是两回事。
+    - 其次,“复用”和“可复用性”关注角度不同。代码“可复用性”是从代码开发者的角度来讲的,“复用”是从代码使用者的角度来讲的。比如,A 同事编写了一个 UrlUtils 类,代码的“可复用性”很好。B 同事在开发新功能的时候,直接“复用”A 同事编写的 UrlUtils 类。
+    - 尽管复用、可复用性、DRY 原则这三者从理解上有所区别,但实际上要达到的目的都是类似的,都是为了减少代码量,提高代码的可读性、可维护性。除此之外,复用已经经过测试的老代码,bug 会比从零重新开发要少。
+- 怎么提高代码复用性?
+    - 减少代码耦合。对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。
+    - 满足单一职责原则。如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。
+    - 模块化。这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。
+    - 业务与非业务逻辑分离。越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。
+    - 通用代码下沉。从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。
+    - 继承、多态、抽象、封装。在讲面向对象特性的时候,我们讲到,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。
+    - 应用模板等设计模式。一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。
+
+
+### 07.辩证思考和应用
+- 实际上,编写可复用的代码并不简单。如果我们在编写代码的时候,已经有复用的需求场景,那根据复用的需求去开发可复用的代码,可能还不算难。但是,如果当下并没有复用的需求,我们只是希望现在编写的代码具有可复用的特点,能在未来某个同事开发某个新功能的时候复用得上。在这种没有具体复用需求的情况下,我们就需要去预测将来代码会如何复用,这就比较有挑战了。
+- 实际上,除非有非常明确的复用需求,否则,为了暂时用不到的复用需求,花费太多的时间、精力,投入太多的开发成本,并不是一个值得推荐的做法。这也违反我们之前讲到的 YAGNI 原则。
+- 除此之外,有一个著名的原则,叫作“Rule of Three”。
+    - 这条原则可以用在很多行业和场景中,你可以自己去研究一下。如果把这个原则用在这里,那就是说,我们在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后我们开发新的功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用。
+- 也就是说,第一次编写代码的时候,我们不考虑复用性;第二次遇到复用场景的时候,再进行重构使其复用。需要注意的是,“Rule of Three”中的“Three”并不是真的就指确切的“三”,这里就是指“二”。
+
+
+
+### 08.总结分享一下
+- 1.DRY 原则
+    - 我们今天讲了三种代码重复的情况:实现逻辑重复、功能语义重复、代码执行重复。实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则。实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。除此之外,代码执行重复也算是违反 DRY 原则。
+- 2.代码复用性
+    - 讲到提高代码可复用性的一些方法,有以下 7 点。
+        - 减少代码耦合
+        - 满足单一职责原则
+        - 模块化
+        - 业务与非业务逻辑分离
+        - 通用代码下沉
+        - 继承、多态、抽象、封装
+        - 应用模板等设计模式
+- 实际上,除了上面讲到的这些方法之外,复用意识也非常重要。在设计每个模块、类、函数的时候,要像设计一个外部 API 一样去思考它的复用性。
+- 在第一次写代码的时候,如果当下没有复用的需求,而未来的复用需求也不是特别明确,并且开发可复用代码的成本比较高,那我们就不需要考虑代码的复用性。在之后开发新的功能的时候,发现可以复用之前写的这段代码,那我们就重构这段代码,让其变得更加可复用。
+
+
+
+
+
+

+ 47 - 0
ReadDesign/03.重构之路介绍/01.重构简单介绍.md

@@ -0,0 +1,47 @@
+#### 目录介绍
+- 01.前沿简单介绍
+- 02.为什么要重构
+- 03.到底重构什么
+- 04.什么时候重构
+- 05.又该如何重构
+
+
+
+
+
+### 01.前沿简单介绍
+- “重构”这个词对于大部分工程师来说都不陌生。不过,据我了解,大部分人都只是“听得多做得少”,真正进行过代码重构的人不多,而把持续重构作为开发的一部分的人,就更是少之又少了。
+- 一方面,重构代码对一个工程师能力的要求,要比单纯写代码高得多。重构需要你能洞察出代码存在的坏味道或者设计上的不足,并且能合理、熟练地利用设计思想、原则、模式、编程规范等理论知识解决这些问题。
+- 另一方面,很多工程师对为什么要重构、到底重构什么、什么时候重构、又该如何重构等相关问题理解不深,对重构没有系统性、全局性的认识,面对一堆烂代码,没有重构技巧的指导,只能想到哪改到哪,并不能全面地改善代码质量。
+
+
+### 02.为什么要重构
+- 重构这个词可能不需要过多解释,但我们还是简单来看一下,大师是怎么描述它的。软件设计大师 Martin Fowler 是这样定义重构的:“重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。”
+- 当讲到重构的时候,很多书籍都会引用这个定义。这个定义中有一个值得强调的点:“重构不改变外部的可见行为”。我们可以把重构理解为,在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。
+- 重构是避免过度设计的有效手段。在我们维护代码的过程中,真正遇到问题的时候,再对代码进行重构,能有效避免前期投入太多时间做过度的设计,做到有的放矢。
+
+
+### 03.到底重构什么
+- 根据重构的规模,我们可以笼统地分为大规模高层次重构(以下简称为“大型重构”)和小规模低层次的重构(以下简称为“小型重构”)。
+- 大型重构指的是对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。
+- 小型重构指的是对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。小型重构更多的是利用我们能后面要讲到的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。你只需要熟练掌握各种编码规范,就可以做到得心应手。
+
+
+### 04.什么时候重构
+- 个人比较反对,平时不注重代码质量,堆砌烂代码,实在维护不了了就大刀阔斧地重构、甚至重写的行为。有时候项目代码太多了,重构很难做得彻底,最后又搞出来一个“四不像的怪物”,这就更麻烦了!所以,寄希望于在代码烂到一定程度之后,集中重构解决所有问题是不现实的,我们必须探索一条可持续、可演进的方式。
+- 平时没有事情的时候,你可以看看项目中有哪些写得不够好的、可以优化的代码,主动去重构一下。或者,在修改、添加某个功能代码的时候,你也可以顺手把不符合编码规范、不好的设计重构一下。
+- 技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就会持续在进行。时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。而那些看到别人代码有点瑕疵就一顿乱骂,或者花尽心思去构思一个完美设计的人,往往都是因为没有树立正确的代码质量观,没有持续重构意识。
+
+
+### 05.又该如何重构
+- 在进行大型重构的时候,我们要提前做好完善的重构计划,有条不紊地分阶段来进行。每个阶段完成一小部分代码的重构,然后提交、测试、运行,发现没有问题之后,再继续进行下一阶段的重构,保证代码仓库中的代码一直处于可运行、逻辑正确的状态。每个阶段,我们都要控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候还需要写一些兼容过渡代码。只有这样,我们才能让每一阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新的功能开发相冲突。
+- 小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时都可以去做。实际上,除了人工去发现低层次的质量问题,我们还可以借助很多成熟的静态代码分析工具(比如 CheckStyle、FindBugs、PMD),来自动发现代码中的问题,然后针对性地进行重构优化。
+
+
+
+
+
+
+
+
+

+ 168 - 0
ReadDesign/03.重构之路介绍/02.重构技术手段.md

@@ -0,0 +1,168 @@
+#### 目录介绍
+- 01.前沿简单介绍
+- 02.什么是单元测试
+- 03.发现代码中bug
+- 04.发现代码设计问题
+- 05.对集成测试的有力补充
+- 06.有利代码重构的过程
+- 07.如何编写单元测试
+
+
+
+
+### 01.前沿简单介绍
+- 如何保证重构不出错呢?你需要熟练掌握各种设计原则、思想、模式,还需要对所重构的业务和代码有足够的了解。除了这些个人能力因素之外,最可落地执行、最有效的保证重构不出错的手段应该就是单元测试(Unit Testing)了。当重构完成之后,如果新的代码仍然能通过单元测试,那就说明代码原有逻辑的正确性未被破坏,原有的外部可见行为未变。
+- 内容主要包含这样几个内容:什么是单元测试?为什么要写单元测试?如何编写单元测试?如何在团队中推行单元测试?
+
+
+
+### 02.什么是单元测试
+- 单元测试由研发工程师自己来编写,用来测试自己写的代码的正确性。我们常常将它跟集成测试放到一块来对比。单元测试相对于集成测试(Integration Testing)来说,测试的粒度更小一些。集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试。而单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。
+- 举个例子来解释一下
+    ```java
+    public class Text {
+      private String content;
+    
+      public Text(String content) {
+        this.content = content;
+      }
+    
+      /**
+       * 将字符串转化成数字,忽略字符串中的首尾空格;
+       * 如果字符串中包含除首尾空格之外的非数字字符,则返回null。
+       */
+      public Integer toNumber() {
+        if (content == null || content.isEmpty()) {
+          return null;
+        }
+        //...省略代码实现...
+        return null;
+      }
+    }
+    ```
+- 如果我们要测试 Text 类中的 toNumber() 函数的正确性,应该如何编写单元测试呢?
+    - 写单元测试本身不需要什么高深技术。它更多的是考验程序员思维的缜密程度,看能否设计出覆盖各种正常及异常情况的测试用例,来保证代码在任何预期或非预期的情况下都能正确运行。
+- 为了保证测试的全面性,针对 toNumber() 函数,我们需要设计下面这样几个测试用例。
+    - 如果字符串只包含数字:“123”,toNumber() 函数输出对应的整数:123。
+    - 如果字符串是空或者 null,toNumber() 函数返回:null。
+    - 如果字符串包含首尾空格:“ 123”,“123 ”,“ 123 ”,toNumber() 返回对应的整数:123。
+    - 如果字符串包含多个首尾空格:“ 123 ”,toNumber() 返回对应的整数:123;
+    - 如果字符串包含非数字字符:“123a4”,“123 4”,toNumber() 返回 null;
+- 代码贴在下面了,可以参考一下,这里没有使用任何测试框架
+    ```java
+    public class Assert {
+      public static void assertEquals(Integer expectedValue, Integer actualValue) {
+        if (actualValue != expectedValue) {
+          String message = String.format(
+                  "Test failed, expected: %d, actual: %d.", expectedValue, actualValue);
+          System.out.println(message);
+        } else {
+          System.out.println("Test succeeded.");
+        }
+      }
+    
+      public static boolean assertNull(Integer actualValue) {
+        boolean isNull = actualValue == null;
+        if (isNull) {
+          System.out.println("Test succeeded.");
+        } else {
+          System.out.println("Test failed, the value is not null:" + actualValue);
+        }
+        return isNull;
+      }
+    }
+    
+    public class TestCaseRunner {
+      public static void main(String[] args) {
+        System.out.println("Run testToNumber()");
+        new TextTest().testToNumber();
+    
+        System.out.println("Run testToNumber_nullorEmpty()");
+        new TextTest().testToNumber_nullorEmpty();
+    
+        System.out.println("Run testToNumber_containsLeadingAndTrailingSpaces()");
+        new TextTest().testToNumber_containsLeadingAndTrailingSpaces();
+    
+        System.out.println("Run testToNumber_containsMultiLeadingAndTrailingSpaces()");
+        new TextTest().testToNumber_containsMultiLeadingAndTrailingSpaces();
+    
+        System.out.println("Run testToNumber_containsInvalidCharaters()");
+        new TextTest().testToNumber_containsInvalidCharaters();
+      }
+    }
+    
+    public class TextTest {
+      public void testToNumber() {
+        Text text = new Text("123");
+        Assert.assertEquals(123, text.toNumber());
+      }
+    
+      public void testToNumber_nullorEmpty() {
+        Text text1 = new Text(null);
+        Assert.assertNull(text1.toNumber());
+    
+        Text text2 = new Text("");
+        Assert.assertNull(text2.toNumber());
+      }
+    
+      public void testToNumber_containsLeadingAndTrailingSpaces() {
+        Text text1 = new Text(" 123");
+        Assert.assertEquals(123, text1.toNumber());
+    
+        Text text2 = new Text("123 ");
+        Assert.assertEquals(123, text2.toNumber());
+    
+        Text text3 = new Text(" 123 ");
+        Assert.assertEquals(123, text3.toNumber());
+      }
+    
+      public void testToNumber_containsMultiLeadingAndTrailingSpaces() {
+        Text text1 = new Text("  123");
+        Assert.assertEquals(123, text1.toNumber());
+    
+        Text text2 = new Text("123  ");
+        Assert.assertEquals(123, text2.toNumber());
+    
+        Text text3 = new Text("  123  ");
+        Assert.assertEquals(123, text3.toNumber());
+      }
+    
+      public void testToNumber_containsInvalidCharaters() {
+        Text text1 = new Text("123a4");
+        Assert.assertNull(text1.toNumber());
+    
+        Text text2 = new Text("123 4");
+        Assert.assertNull(text2.toNumber());
+      }
+    }
+    ```
+
+
+### 03.发现代码中bug
+- 能否写出 bug free 的代码,是判断工程师编码能力的重要标准之一,也是很多大厂面试考察的重点,特别是像 FLAG 这样的外企。可以这么说,坚持写单元测试是保证我的代码质量的一个“杀手锏”。
+
+
+
+### 04.发现代码设计问题
+- 代码的可测试性是评判代码质量的一个重要标准。对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很吃力,需要依靠单元测试框架里很高级的特性才能完成,那往往就意味着代码设计得不够合理,比如,没有使用依赖注入、大量使用静态函数、全局变量、代码高度耦合等。
+
+
+
+### 05.对集成测试的有力补充
+- 程序运行的 bug 往往出现在一些边界条件、异常情况下,比如,除数未判空、网络超时。而大部分异常情况都比较难在测试环境中模拟。而单元测试可以利用下一节课中讲到的 mock 的方式,控制 mock 的对象返回我们需要模拟的异常,来测试代码在这些异常情况的表现。
+- 除此之外,对于一些复杂系统来说,集成测试也无法覆盖得很全面。复杂系统往往有很多模块。每个模块都有各种输入、输出、异常情况,组合起来,整个系统就有无数测试场景需要模拟,无数的测试用例需要设计,再强大的测试团队也无法穷举完备。
+- 尽管单元测试无法完全替代集成测试,但如果我们能保证每个类、每个函数都能按照我们的预期来执行,底层 bug 少了,那组装起来的整个系统,出问题的概率也就相应减少了。
+
+
+### 06.有利代码重构的过程
+- 要把持续重构作为开发的一部分来执行,那写单元测试实际上就是落地执行持续重构的一个有效途径。设计和实现代码的时候,我们很难把所有的问题都想清楚。而编写单元测试就相当于对代码的一次自我 Code Review,在这个过程中,我们可以发现一些设计上的问题(比如代码设计的不可测试)以及代码编写方面的问题(比如一些边界条件处理不当)等,然后针对性的进行重构。
+
+
+### 07.如何编写单元测试
+- 举了一个给 toNumber() 函数写单元测试的例子。根据那个例子,我们可以总结得出,写单元测试就是针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将这些测试用例翻译成代码的过程。
+- 
+
+
+
+
+

+ 76 - 0
ReadDesign/03.重构之路介绍/07.避免过度设计.md

@@ -0,0 +1,76 @@
+#### 目录介绍
+- 01.看一个现实场景
+- 02.提高代码质量
+- 03.先有问题后有方案
+- 04.场景是复杂代码
+- 05.代码需持续重构
+- 06.避免设计不足
+- 07.结合具体场景谈设计
+
+
+
+
+### 01.看一个现实场景
+- 一种同事会过度设计。
+    - 在开始编写代码之前,他会花很长时间做代码设计,在开发过程中应用各种设计模式,美其名曰未雨绸缪,希望代码更加灵活,为未来的扩展打好基础,实则过度设计,未来的需求并不一定会实现,实际上是增加了代码的复杂度,以后的所有开发都要在这套复杂的设计基础之上来完成。
+- 还有一种是设计不足。
+    - 怎么简单怎么来,写出来的代码能跑就可以,顶多算是demo,看似在实践KISS、YAGNI原则,实则忽略了设计环节,代码毫无扩展性、灵活性可言,添加、修改一个很小的功能就要改动很多代码。
+
+
+### 02.提高代码质量
+- 应用设计模式只是方法,最终的目的,也就是初心,是提高代码的质量。具体点说就是,提高代码的可读性、可扩展性、可维护性等。所有的设计都是围绕着这个初心来做的。
+- 所以,在做代码设计的时候,你一定要先问下自己,为什么要这样设计,为什么要应用这种设计模式,这样做是否能真正地提高代码质量,能提高代码质量的哪些方面。如果自己很难讲清楚,或者给出的理由都比较牵强,没有压倒性的优势,那基本上就可以断定这是一种过度设计,是为了设计而设计。
+- 实际上,设计原则和思想是心法,设计模式只是招式。掌握心法,以不变应万变,无招胜有招。所以,设计原则和思想比设计模式更加普适、重要。掌握了设计原则和思想,我们能更清楚地了解为什么要用某种设计模式,就能更恰到好处地应用设计模式,甚至我们还可以自己创造出来新的设计模式。
+
+
+### 03.先有问题后有方案
+- 如果我们把写出的代码看作产品,那做产品的时候,我们先要思考痛点在哪里,用户的真正需求在哪里,然后再看要开发哪些功能去满足,而不是先拍脑袋想出一个花哨的功能,再去东搬西凑硬编出一个需求来。
+- 代码设计也是类似的。我们先要去分析代码存在的痛点,比如可读性不好、可扩展性不好等等,然后再针对性地利用设计模式去改善,而不是看到某个场景之后,觉得跟之前在某本书中看到的某个设计模式的应用场景很相似,就套用上去,也不考虑到底合不合适,最后如果有人问起了,就再找几个不痛不痒、很不具体的伪需求来搪塞,比如提高了代码的扩展性、满足了开闭原则等等。
+- 实际上,很多没有太多开发经验的新手,往往在学完设计模式之后会非常“学生气”,拿原理当真理,不懂得具体问题具体分析,手里拿着锤子看哪都是钉子,不分青红皂白,上来就是套用某个设计模式。写完之后,看着自己写的很复杂的代码,还沾沾自喜,甚至到处炫耀。这完全是无知地炫技,半瓶子不满大抵就是这个样子的。等你慢慢成长之后,回过头来再看自己当年的代码,我相信你应该会感到脸红的。这里我的话说得有点重,我主要还是担心你以后在项目中,过度设计被别人鄙视,所以提前给你打个预防针!
+- 一直是从问题讲起,一步一步给你展示为什么要用某个设计模式,而不是一开始就告诉你最终的设计。实际上,这还不是最重要的,最重要的是我想培养你分析问题、解决问题的能力。这样,看到某段代码之后,你就能够自己分析得头头是道,说出它好的地方、不好的地方,为什么好、为什么不好,不好的如何改善,可以应用哪种设计模式,应用了之后有哪些副作用要控制等等。
+
+
+
+### 04.场景是复杂代码
+- 很多设计模式相关的书籍都会举一些简单的例子,这些例子仅仅具有教学意义,只是为了讲解设计模式的原理和实现,力求在有限篇幅内给你讲明白。而很多人就会误以为这些简单的例子就是这些设计模式的典型应用场景,常常照葫芦画瓢地应用到自己的项目中,用复杂的设计模式去解决简单的问题,还振振有词地说某某经典书中就是这么写的。在我看来,这是很多初学者因为缺乏经验,在学完设计模式之后,在项目中过度设计的首要原因。
+- 前面我们讲到,设计模式要干的事情就是解耦,也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。创建型模式是将创建和使用代码解耦,结构型模式是将不同的功能代码解耦,行为型模式是将不同的行为代码解耦。而解耦的主要目的是应对代码的复杂性。设计模式就是为了解决复杂代码问题而产生的。
+- 因此,对于复杂代码,比如项目代码量多、开发周期长、参与开发的人员多,我们前期要多花点时间在设计上,越是复杂代码,花在设计上的时间就要越多。
+- 不仅如此,每次提交的代码,都要保证代码质量,都要经过足够的思考和精心的设计,这样才能避免烂代码效应(每次提交的代码质量都不是太好,最终积累起来整个项目的质量就变得很差)。
+- 相反,如果你参与的只是一个简单的项目,代码量不多,开发人员也不多,那简单的问题用简单的解决方案就好,不要引入过于复杂的设计模式,将简单问题复杂化。
+
+
+
+### 05.代码需持续重构
+- 应用设计模式会提高代码的可扩展性,但同时也会带来代码可读性的降低,复杂度的升高。一旦我们引入某个复杂的设计,之后即便在很长一段时间都没有扩展的需求,我们也不可能将这个复杂的设计删除,整个团队都要一直背负着这个复杂的设计前行。
+- 为了避免错误的需求预判导致的过度设计,我非常推崇持续重构的开发方法。持续重构不仅仅是保证代码质量的重要手段,也是避免过度设计的有效方法。在真正有痛点的时候,我们再去考虑用设计模式来解决,而不是一开始就为不一定实现的未来需求而应用设计模式。
+- 当对要不要应用某种设计模式感到模棱两可的时候,你可以思考一下,如果暂时不用这种设计模式,随着代码的演进,当某一天不得不去使用它的时候,重构的代码是否很大。如果不是,那能不用就不用,怎么简单就怎么来。说句实话,对于10万行以内的代码,团队成员稳定,对代码涉及的业务比较熟悉的情况下,即便将所有的代码都推倒重写,也不会花太多时间,因此也不必为代码的扩展性太过担忧。
+
+
+
+### 06.避免设计不足
+- 首先,你要有一定理论知识的储备。
+    - 比如你要熟练掌握各种设计原则、思想、编码规范、设计模式。理论知识是解决问题的工具,是前人智慧的结晶。没有理论知识,就相当于游戏中没有厉害的装备,虽然可以靠身手徒手打怪,但肯定会影响你最高水平的发挥。
+- 其次,你还要有一定的刻意训练。
+    - 很多同学很苦恼,说理论知识都学过,但是很容易忘记,遇到问题也想不到对应的知识点。实际上,这就是缺乏理论结合实践的刻意训练。我们回想一下上学的时候,我们是如何学习的。老师讲解完某个知识点之后,往往会配合讲解几道例题,然后再让你做上个几十道题去强化这个知识点。这样当你再遇到类似的问题的时候,就能不由自主地联想到相应的知识点。而工作之后,我们自己看书学知识,别说拿几个场景来实践了,大部分都是走马观花地看看,没有经过刻意的训练,知识积累不了,能力也锻炼不了,等于白学。
+- 最后,你一定要有代码质量意识、设计意识。
+    - 在写代码之前,要多想想未来会有哪些扩展的需求,哪部分是会变的,哪部分是不变的,这样写会不会导致之后添加新的功能比较困难,代码的可读性好不好等代码质量问题。有了这样的意识,实际上,你就离写出高质量的代码不远了。
+
+
+
+### 07.结合具体场景谈设计
+- 设计是一个非常主观的事情,不夸张地讲,可以称之为一门“艺术”。那相应地,好坏就很难评判了。如果真的要评判,我们要放到具体的场景中。脱离具体的场景去谈论设计是否合理,都是空谈。这就像我们经常说的,脱离业务谈架构都是“耍流氓”。
+- 比如,一个手游项目是否能被市场接受,往往非常不确定。很多手游项目开发出来之后,市场反馈很差,立马就放弃了。除此之外,尽快上市占领市场也是一款手游致胜的关键。所以,对于手游项目的开发来说,往往前期不会花太多的时间在代码设计、代码质量上。
+- 相反,如果你开发的是MMORPG大型端游,一般都要投资上亿资金,几百号人开发好几年,推倒重来的成本很大。这个时候,代码质量就非常关键了。前期就要多花点时间在设计上,否则,代码质量太差,bug太多,后期无法维护,也会导致很多用户弃而选择同类型的其他家的游戏。
+- 再比如,如果我们开发的是偏底层的、框架类的、通用的代码,那代码质量就比较重要,因为一旦出现问题或者代码改动,影响面就比较大。相反,如果我们开发的是业务系统或者不需要长期维护的项目,那稍微放低点代码质量的要求,也是没问题的,而且,自己的代码跟其他项目没有太多耦合,即便出了问题,影响也不大。
+
+
+
+
+
+
+
+
+
+
+
+

+ 0 - 0
ReadMeWiki/04.视频播放器封装思路.md → ReadMeWiki/00.方案实践/01.视频播放器封装思路.md


+ 0 - 0
ReadMeWiki/05.播放器内核切换封装.md → ReadMeWiki/00.方案实践/02.播放器内核切换封装.md


+ 0 - 0
ReadMeWiki/06.播放器UI抽取封装.md → ReadMeWiki/00.方案实践/03.播放器UI抽取封装.md


+ 0 - 0
ReadMeWiki/02.视频播放器整体结构.md → ReadMeWiki/00.方案实践/04.视频播放器整体结构.md


+ 0 - 0
ReadMeWiki/10.视频全局悬浮窗播放.md → ReadMeWiki/00.方案实践/05.视频全局悬浮窗播放.md


+ 0 - 0
ReadMeWiki/12.视频边播边缓存分析.md → ReadMeWiki/00.方案实践/06.视频边播边缓存分析.md


+ 0 - 0
ReadMeWiki/16.视频播放器列表播放.md → ReadMeWiki/00.方案实践/07.视频播放器列表播放.md


+ 0 - 0
ReadMeWiki/29.视频播放器埋点监听.md → ReadMeWiki/00.方案实践/08.视频播放器埋点监听.md


+ 0 - 0
ReadMeWiki/30.视频播放器使用设计模式.md → ReadMeWiki/00.方案实践/09.视频播放器使用设计模式.md


+ 0 - 0
ReadMeWiki/27.视频加密和解密处理.md → ReadMeWiki/00.方案实践/10.视频加密和解密处理.md


+ 0 - 0
ReadMeWiki/28.视频录制和编辑学习.md → ReadMeWiki/00.方案实践/11.视频录制和编辑实践.md


+ 0 - 0
ReadMeWiki/54.TTS音频播放基础.md → ReadMeWiki/00.方案实践/12.TTS音频播放实践.md


+ 0 - 0
ReadMeWiki/40.完整音频播放器分析.md → ReadMeWiki/00.方案实践/40.完整音频播放器分析.md


+ 0 - 0
ReadMeWiki/42.音视频本地文件扫描.md → ReadMeWiki/00.方案实践/42.音视频本地文件扫描.md


+ 0 - 0
ReadMeWiki/45.音视频加密和解密.md → ReadMeWiki/00.方案实践/45.音视频加密和解密.md


+ 0 - 0
ReadMeWiki/62.视频内存优化治理.md → ReadMeWiki/00.方案实践/62.视频内存优化治理.md


+ 0 - 0
ReadMeWiki/13.视频播放器如何选择.md → ReadMeWiki/01.原理知识/01.视频播放器如何选择.md


+ 0 - 0
ReadMeWiki/14.视频播放器简单案例.md → ReadMeWiki/01.原理知识/02.视频播放器简单案例.md


+ 0 - 0
ReadMeWiki/22.视频基础概念术语.md → ReadMeWiki/01.原理知识/03.视频基础概念术语.md


+ 0 - 0
ReadMeWiki/23.视频VideoView学习.md → ReadMeWiki/01.原理知识/04.视频VideoView学习.md


+ 0 - 0
ReadMeWiki/15.视频播放器流程分析.md → ReadMeWiki/01.原理知识/05.视频播放器流程分析.md


+ 0 - 0
ReadMeWiki/07.MediaPlayer详细介绍.md → ReadMeWiki/01.原理知识/07.MediaPlayer详细介绍.md


+ 368 - 20
ReadMeWiki/24.2SurfaceView源码分析.md → ReadMeWiki/01.原理知识/10.SurfaceView深入学习.md

@@ -1,24 +1,347 @@
-# SurfaceView源码分析
+# 认识SurfaceView
 #### 目录介绍
-- 01.Android中绘制模型
-    - 1.1 软件绘制模型
-    - 1.2 硬件加速绘制模型
-- 02.SurfaceView是什么
-    - 2.1 继承自类View
-    - 2.2 SurfaceView优缺点
-    - 2.3 使用SurfaceView播放视频
-- 03.SurfaceView双缓冲
-    - 3.1 双缓冲机制由来
-    - 3.2 如何理解SurfaceView双缓冲
-- 04.SurfaceView源码分析
-    - 4.1 为何不会阻塞Ui线程
-    - 4.2 SurfaceHolder.Callback
-    - 4.3 SurfaceHolder类源码
-    - 4.4 SurfaceView部分源码
-    - 4.5 看看Surface源码
-- 05.SurfaceView总结
-    - 5.1 使用场景
-    - 5.2 为何使用SurfaceView
+- 01.SurfaceView有何特点
+- 02.SurfaceView的原理
+- 03.SurfaceView相关类
+    - 3.1 Surface说明
+    - 3.2 SurfaceHolder
+    - 3.3 SurfaceView
+- 04.SurfaceView的使用
+- 05.draw和onDraw都不执行
+- 06.SurfaceView双缓冲
+
+
+
+
+
+
+### 01.SurfaceView有何特点
+- Android中 View是通过刷新来重绘视图,系统通过发出VSYNC信号来进行屏幕的重绘,刷新的时间间隔是16ms,如果我们可以在16ms以内将绘制工作完成,则没有任何问题,如果我们绘制过程逻辑很复杂,并且我们的界面更新还非常频繁,这时候就会造成界面的卡顿,影响用户体验,为此Android提供了SurfaceView来解决这一问题.
+- SurfaceView 继承自View,是 Android 中一种比较特殊的视图(View)
+    - 它跟普通View最大的区别是它有自己的Surface,在WMS中有对应的WindowState,在SurfaceFlinger中有Layer
+    - 一般的Activity包含的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的。这个DecorView在WMS中有一个对应的WindowState。相应地,在SF中对应的Layer。
+    - SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SF中也会有自己的Layer。虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。
+- SurfaceView 应用场景
+    - 综合这些特点,SurfaceView 一般用在游戏、视频、摄影等一些复杂 UI 且高效的图像的显示,这类的图像处理都需要开单独的线程来处理。
+- SurfaceView 优点如下
+    - SurfaceView 通过子线程中进行画面更新,View 则在主线程中进行画面更新。
+    - SurfaceView 用于被动更新,如频繁画面更新,View 则用于主动更新,如触摸点击等事件响应等。
+    - SurfaceView 在底层实现了双缓冲机制,效率大大提升了,View 则没有。
+    - 在一个独立的线程中进行绘制,不会影响主线程。
+- SurfaceView 缺点如下
+    - Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中。
+    - SurfaceView 不能嵌套使用
+
+
+
+### 02.SurfaceView的原理
+- 如果当前画面需要不停绘制或者数据处理量较大时,为避免 UI 线程堵塞,就用 SurfaceView 代替 View。
+- SurfaceView拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面,由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行行绘制,由于不占用主线程资源,使得它可以实现大多复杂而高效的界面绘制,如视频播放 VideoView 和OpenGl es的 GLSurfaceView 直播软件的 不停地点赞动效、天气软件的全屏雨雪动效、游戏中的流水、云之类的变化等等。
+    - ![image](https://img-blog.csdnimg.cn/20191211165629427.png)
+
+
+
+### 03.SurfaceView相关类
+- 要使用 SurfaceView ,就必须了解它的另外两个组件:Surface 和 SurfaceHolder
+- SurfaceView中的MVC框架
+    - 要了解SurfaceView,还必须要了解和它息息相关的其他两个组件:Surface 和 SurfaceHolder。
+    - Surface其实就视图数据,SurfaceHolder我们都知道是个接口,用来进行绘制。而SurfaceView是显示视图并且和用户交互的界面。
+    - 而MVC(Model-View-Controller)框架,model是数据,也就是这里的Surface,View是用来显示的,也就是SurfaceView,而控制器,也就是这里SurfaceHolder。
+
+
+
+#### 3.1 Surface说明
+- 首先看一下这个类的代码
+    ```
+    public class Surface implements Parcelable {
+        
+    }
+    ```
+- 看 Surface 这个类,它实现了 Parcelable 接口进行序列化
+    - 这里主要用来在进程间传递 surface 对象,用来处理屏幕显示缓冲区的数据,源码中对它的注释为: Handle onto a raw buffer that is being managed by the screen compositor. 
+    - Surface是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。
+    - 由屏幕显示内容合成器(screen compositor)所管理的原生缓冲器的句柄(类似句柄) 
+    - 名词解释:句柄,英文:HANDLE,数据对象进入内存之后获取到内存地址,但是所在的内存地址并不是固定的,需要用句柄来存储内容所在的内存地址。从数据类型上来看它只是一个32位(或64位)的无符号整数。 
+    - Surface 充当句柄的角色,用来获取源生缓冲区以及其中的内容 
+    - 源生缓冲区(raw buffer)用来保存当前窗口的像素数据 
+    - 于是可知 Surface 就是 Android 中用来绘图的的地方,具体来说应该是 Surface 中的 Canvas Surface 中定义了画布相关的 Canvas 对象
+- 绘图通常在一个 Canvas 对象上进行的
+    - Surface 中也包含了一个 Canvas 对象,这里的 CompatibleCanvas 是Surface.java 中的一个内部类,其中包含一个矩阵对象Matrix(变量名mOrigMatrix)。矩阵Matrix就是一块内存区域,针对View的各种绘画操作都保存在此内存中。
+    - Surface 内部有一个 CompatibleCanvas 的内部类,这个内部类的作用是为了能够兼容 Android 各个分辨率的屏幕,根据不同屏幕的分辨率处理不同的图像数据。  
+    ```
+    private final class CompatibleCanvas extends Canvas {
+        // A temp matrix to remember what an application obtained via {@link getMatrix}
+        private Matrix mOrigMatrix = null;
+    
+        @Override
+        public void setMatrix(Matrix matrix) {
+            if (mCompatibleMatrix == null || mOrigMatrix == null || mOrigMatrix.equals(matrix)) {
+                // don't scale the matrix if it's not compatibility mode, or
+                // the matrix was obtained from getMatrix.
+                super.setMatrix(matrix);
+            } else {
+                Matrix m = new Matrix(mCompatibleMatrix);
+                m.preConcat(matrix);
+                super.setMatrix(m);
+            }
+        }
+    
+        @SuppressWarnings("deprecation")
+        @Override
+        public void getMatrix(Matrix m) {
+            super.getMatrix(m);
+            if (mOrigMatrix == null) {
+                mOrigMatrix = new Matrix();
+            }
+            mOrigMatrix.set(m);
+        }
+    }
+    ```
+- 两个重要的方法
+    - 需要说明的是,这里的 lockCanvas 并不是实际使用 SurfaceView 来进行绘图时 SurfaceHolder 对象调用的 lockCanvas 以及 unlockCanvasAndPost 方法。实际例子中使用的方法是在 SurfaceView 内部封装过对这两个方法封装之后的。 
+    - lockCanvas(...) + Gets a Canvas for drawing into this surface. 获取进行绘画的 Canvas 对象 + After drawing into the provided Canvas, the caller must invoke unlockCanvasAndPost to post the new contents to the surface. 绘制完一帧的数据之后需要调用 unlockCanvasAndPost 方法把画布解锁,然后把画好的图像 Post 到当前屏幕上去显示 + 当一个 Canvas 在被绘制的时候,它是出于被锁定的状态,就是说必须等待正在绘制的这一帧绘制完成之后并解锁画布之后才能进行别的操作 + 实际锁住 Canvas 的过程是在 jni 层完成的 
+    - unlockCanvasAndPost(...) + Posts the new contents of the Canvas to the surface and releases the Canvas.将新绘制的图像内容传给 surface 之后这个 Canvas 对象会被释放掉(实际释放的过程是在 jni 层完成的)
+- Surface 的 lockCanvas 和 unlockCanvasAndPost 两个方法最终都是调用 jni 层的方法来处理
+
+
+#### 3.2 SurfaceHolder
+- android.view.SurfaceHolder SurfaceHolder 实际上是一个接口,它充当的是 Controller 的角色。
+    ```
+    public interface SurfaceHolder {
+    
+    }
+    ```
+- 来看下注释是怎么说的
+    - Abstract interface to someone holding a display surface. 一个针对 Surface 的抽象接口
+    - Allows you to control the surface size and format, edit the pixels in the surface, and monitor changes to the surface. 赤裸裸的 Controller 角色,可以控制 Surface 的大小和格式,监控 Surface 的变化(在回调函数中对 Surface 的变化做相应的处理)
+    - When using this interface from a thread other than the one running its SurfaceView, you will want to carefully read the methods Callback.surfaceCreated() 如果用子线程来处理 SurfaceView 的绘制,需要用到接下来要介绍的关键接口 Callback 中的 surfaceCreated 方法。可以看到之前给的例子中就是在 surfaceCreated 方法中开启的绘制动画的线程
+- 关键接口 Callback
+    - Callback 是 SurfaceHolder 内部的一个接口,例子中就实现了这个接口来控制绘制动画的线程。
+- 接口中有以下三个方法 
+    - public void surfaceCreated(SurfaceHolder holder); + Surface 第一次被创建时被调用,例如 SurfaceView 从不可见状态到可见状态时 + 在这个方法被调用到 surfaceDestroyed 方法被调用之前的这段时间,Surface 对象是可以被操作的,拿 SurfaceView 来说就是如果 SurfaceView 只要是在界面上可见的情况下,就可以对它进行绘图和绘制动画 + 这里还有一点需要注意,Surface 在一个线程中处理需要渲染的图像数据,如果你已经在另一个线程里面处理了数据渲染,就不需要在这里开启线程对 Surface 进行绘制了 
+    - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height); + Surface 大小和格式改变时会被调用,例如横竖屏切换时如果需要对 Surface 的图像和动画进行处理,就需要在这里实现 + 这个方法在 surfaceCreated 之后至少会被调用一次 
+    - public void surfaceDestroyed(SurfaceHolder holder); + Surface 被销毁时被调用,例如 SurfaceView 从可见到不可见状态时 + 在这个方法被调用过之后,就不能够再对 Surface 对象进行任何操作,所以需要保证绘图的线程在这个方法调用之后不再对 Surface 进行操作,否则会报错
+
+
+#### 3.3 SurfaceView
+- SurfaceView,就是用来显示 Surface 数据的 View,通过 SurfaceView 来看到 Surface 的数据。
+    ```
+    public class SurfaceView extends View {  
+        // code.....
+    }
+    ```
+- 分析一下源码中对 SurfaceView 的注释
+    - Provides a dedicated drawing surface embedded inside of a view hierarchy. 在屏幕显示的视图层中嵌入了一块用做图像绘制的 Surface 视图
+    - the SurfaceView punches a hole in its window to allow its surface to be displayed. SurfaceView 在屏幕上挖了个洞来来世它所绘制的图像
+- 挖洞是什么鬼?
+    - 这里引入一个Z轴的概念,SurfaceView 视图所在层级的Z轴位置是小于用来其宿主 Activity 窗口的 Layer 的 Z 轴的,就是说其实 SurfaceView 实际是显示在 Activity 所在的视图层下方的
+- 那么问题就来了,为什么还是能看到 SurfaceView?
+    - 形象一点的说法就是你在墙上凿了一个方形的洞,然后在洞上装了块玻璃,你就能看到墙后面的东西了。SurfaceView 就做了这样的事情,它把 Activity 所在的层当作了墙
+    - The Surface will be created for you while the SurfaceView's window is visible. 这里说明了动画是什么时候开始的,当 SurfaceView 可见时,就可以开始在 Canvas 上绘制图像,并把图像数据传递给 Surface 用来显示在 SurfaceView 上
+    - you should implement SurfaceHolder.Callback#surfaceCreated and SurfaceHolder.Callback#surfaceDestroyed to discover when the Surface is created and destroyed as the window is shown and hidden. 在使用 SurfaceView 的地方需要实现 SurfaceHolder.CallBack 回调,来对 Surface 的创建和销毁进行监听以及做响应的处理,这里的处理指的是开始对 Canvas 进行绘制并把数据传递给 Surface 来做显示
+
+
+
+### 04.SurfaceView的使用
+- 如何使用 SurfaceView
+    - 需要实现 SurfaceHolder.Callback 接口
+    - 需要在 SurfaceHolder.Callback 的 surfaceCreated 方法中开启一个线程进行动画的逐帧的绘制
+    - 需要在 SurfaceHolder.Callback 的 surfaceDestroyed 方法中结束绘画的线程并调用 SurfaceHolder 的 removeCallbck 方法
+    - 绘画线程每一帧开始之前需要调用 lockCanvas 方法锁住画布进行绘图
+    - 绘制完一帧的数据之后需要调用 unlockCanvasAndPost 方法提交数据来显示图像
+- 关于自定义SurfaceView代码如下所示
+    ```
+    public class MySurfaceView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
+    
+        private final SurfaceHolder mHolder; // 用于控制SurfaceView
+        private Thread t; // 声明一条线程
+        private volatile boolean flag; // 线程运行的标识,用于控制线程
+        private Canvas mCanvas; // 声明一张画布
+        private Paint p; // 声明一支画笔
+        float m_circle_r = 10;
+    
+        public MySurfaceView(Context context) {
+            super(context);
+            mHolder = getHolder(); // 获得SurfaceHolder对象
+            mHolder.addCallback(this); // 为SurfaceView添加状态监听
+            p = new Paint(); // 创建一个画笔对象
+            p.setColor(Color.WHITE); // 设置画笔的颜色为白色
+            setFocusable(true); // 设置焦点
+        }
+    
+        /**
+         * 当SurfaceView创建的时候,调用此函数
+         */
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            // 创建一个线程对象
+            t = new Thread(this);
+            // 把线程运行的标识设置成true
+            flag = true;
+            // 启动线程
+            t.start();
+        }
+    
+        /**
+         * 当SurfaceView的视图发生改变的时候,调用此函数
+         */
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        }
+    
+        /**
+         * 当SurfaceView销毁的时候,调用此函数
+         */
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            // 把线程运行的标识设置成false
+            flag = false;
+            mHolder.removeCallback(this);
+        }
+    
+        /**
+         * 当屏幕被触摸时调用
+         */
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+    
+            return true;
+        }
+    
+        /**
+         * 当用户按键时调用
+         */
+        @Override
+        public boolean onKeyDown(int keyCode, KeyEvent event) {
+            if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+            }
+            return super.onKeyDown(keyCode, event);
+        }
+    
+        @Override
+        public boolean onKeyUp(int keyCode, KeyEvent event) {
+            surfaceDestroyed(mHolder);
+            return super.onKeyDown(keyCode, event);
+        }
+    
+        @Override
+        public void run() {
+            while (flag) {
+                try {
+                    synchronized (mHolder) {
+                        // 让线程休息100毫秒
+                        Thread.sleep(100);
+                        // 调用自定义画画方法
+                        Draw();
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                } finally {
+                    if (mCanvas != null) {
+                        // mHolder.unlockCanvasAndPost(mCanvas);//结束锁定画图,并提交改变。
+                    }
+                }
+            }
+        }
+    
+        /**
+         * 自定义一个方法,在画布上画一个圆
+         */
+        protected void Draw() {
+            // 获得画布对象,开始对画布画画
+            mCanvas = mHolder.lockCanvas();
+            if (mCanvas != null) {
+                Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+                paint.setColor(Color.BLUE);
+                paint.setStrokeWidth(10);
+                paint.setStyle(Style.FILL);
+                if (m_circle_r >= (getWidth() / 10)) {
+                    m_circle_r = 0;
+                } else {
+                    m_circle_r++;
+                }
+                Bitmap pic = ((BitmapDrawable) getResources().getDrawable(R.drawable.ic_palyer_share)).getBitmap();
+                mCanvas.drawBitmap(pic, 0, 0, paint);
+                for (int i = 0; i < 5; i++) {
+                    for (int j = 0; j < 8; j++) {
+                        mCanvas.drawCircle(
+                                (getWidth() / 5) * i + (getWidth() / 10),
+                                (getHeight() / 8) * j + (getHeight() / 16),
+                                m_circle_r, paint);
+                    }
+                }
+                // 完成画画,把画布显示在屏幕上
+                mHolder.unlockCanvasAndPost(mCanvas);
+            }
+        }
+    }
+    ```
+- 如何使用该自定义MySurfaceView
+    ```
+    mSurfaceView = new MySurfaceView(this);
+    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
+            RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
+    fl.addView(mSurfaceView, params);
+    
+    SurfaceHolder holder = mSurfaceView.getHolder();
+    //增加surfaceView的监听
+    holder.addCallback(new SurfaceHolder.Callback() {
+        @Override
+        public void surfaceCreated(SurfaceHolder surfaceHolder) {
+    
+        }
+    
+        @Override
+        public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
+    
+        }
+    
+        @Override
+        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
+        }
+    });
+    ```
+
+
+### 05.draw和onDraw都不执行
+- 在自定义MySurfaceView控件中,重写draw的两个方法,如下所示,发现这两个方法竟然没有执行,这个是为什么呢?
+    ```
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        System.out.println("MySurfaceView-----onDraw");
+    }
+    
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        System.out.println("MySurfaceView-----draw");
+    }
+    ```
+- 看一下源码中的构造方法。
+    - 查看SurfaceView的源码可知,在其构造函数中调用了setWillNotDraw(true);该方法会导致draw()、onDraw()都不执行。
+    ```
+    public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mRenderNode.requestPositionUpdates(this);
+    
+        setWillNotDraw(true);
+    }
+    
+    public void setWillNotDraw(boolean willNotDraw) {
+        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
+    }
+    ```
+
+
+
+### 06.SurfaceView双缓冲
+- 双缓冲,在运用时可以理解为:
+    - SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas。
+    - 每次实际显示的是frontCanvas,backCanvas存储的是上一次更改前的视图,当使用lockCanvas()获取画布时,得到的实际上是backCanvas而不是正在显示的frontCanvas,之后你在获取到的backCanvas上绘制新视图,再unlockCanvasAndPost(canvas)此视图,那么上传的这张canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。
+    - 例如,如果你已经先后两次绘制了视图A和B,那么你再调用lockCanvas()获取视图,获得的将是A而不是正在显示的B,之后你讲重绘的C视图上传,那么C将取代B作为新的frontCanvas显示在SurfaceView上,原来的B则转换为backCanvas。
+
+
 
 
 
@@ -513,3 +836,28 @@
 
 
 
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 0 - 0
ReadMeWiki/25.TextureView深入学习.md → ReadMeWiki/01.原理知识/11.TextureView深入学习.md


+ 0 - 0
ReadMeWiki/26.视频编码和解码学习.md → ReadMeWiki/01.原理知识/20.视频编码和解码学习.md


+ 0 - 0
ReadMeWiki/32.音频焦点抢占问题.md → ReadMeWiki/01.原理知识/32.音频焦点抢占问题.md


+ 0 - 0
ReadMeWiki/43.音频基础知识点.md → ReadMeWiki/01.原理知识/43.音频基础知识点.md


+ 0 - 0
ReadMeWiki/01.视频播放器介绍文档.md → ReadMeWiki/02.如何使用/01.视频播放器介绍文档.md


+ 0 - 0
ReadMeWiki/03.视频播放器Api说明.md → ReadMeWiki/02.如何使用/03.视频播放器Api说明.md


+ 0 - 0
ReadMeWiki/49.参考项目和博客说明.md → ReadMeWiki/02.如何使用/49.参考项目和博客说明.md


+ 0 - 0
ReadMeWiki/50.版本更新说明文档.md → ReadMeWiki/02.如何使用/50.版本更新说明文档.md


+ 0 - 0
ReadMeWiki/08.视频播放器优化处理.md → ReadMeWiki/03.优化实践/08.视频播放器优化处理.md


+ 0 - 0
ReadMeWiki/09.视频深度优化处理.md → ReadMeWiki/03.优化实践/09.视频深度优化处理.md


+ 0 - 0
ReadMeWiki/11.视频播放器音频焦点抢占.md → ReadMeWiki/03.优化实践/11.视频播放器音频焦点抢占.md


+ 0 - 0
ReadMeWiki/17.基础播放器问题记录.md → ReadMeWiki/03.优化实践/17.基础播放器问题记录.md


+ 0 - 0
ReadMeWiki/34.音频播放锁屏分析.md → ReadMeWiki/03.优化实践/34.音频播放锁屏分析.md


+ 0 - 0
ReadMeWiki/35.耳机控制音视频音量.md → ReadMeWiki/03.优化实践/35.耳机控制音视频音量.md


+ 0 - 0
ReadMeWiki/51.直播基础知识点介绍.md → ReadMeWiki/04.直播视频/51.直播基础知识点介绍.md


+ 0 - 0
ReadMeWiki/52.直播推流端分析.md → ReadMeWiki/04.直播视频/52.直播推流端分析.md


+ 0 - 0
ReadMeWiki/53.直播播放端分析.md → ReadMeWiki/04.直播视频/53.直播播放端分析.md


+ 0 - 9
ReadMeWiki/18.Exo播放器问题记录.md

@@ -1,9 +0,0 @@
-# 基础方法说明
-#### 目录介绍
-- 14.exo加载视频显示Unable to connect to url
-
-
-
-
-
-

+ 0 - 3
ReadMeWiki/19.Ijk播放器问题记录.md

@@ -1,3 +0,0 @@
-# 基础方法说明
-#### 目录介绍
-

+ 0 - 3
ReadMeWiki/20.视频播放器版本更新文档.md

@@ -1,3 +0,0 @@
-# 基础方法说明
-#### 目录介绍
-

+ 0 - 7
ReadMeWiki/21.视频播放器后期需求.md

@@ -1,7 +0,0 @@
-
-
-
-
-
-
-

+ 0 - 376
ReadMeWiki/24.1SurfaceView深入学习.md

@@ -1,376 +0,0 @@
-# 认识SurfaceView
-#### 目录介绍
-- 01.SurfaceView有何特点
-- 02.SurfaceView的原理
-- 03.SurfaceView相关类
-    - 3.1 Surface说明
-    - 3.2 SurfaceHolder
-    - 3.3 SurfaceView
-- 04.SurfaceView的使用
-- 05.draw和onDraw都不执行
-- 06.SurfaceView双缓冲
-
-
-
-
-
-
-### 01.SurfaceView有何特点
-- Android中 View是通过刷新来重绘视图,系统通过发出VSYNC信号来进行屏幕的重绘,刷新的时间间隔是16ms,如果我们可以在16ms以内将绘制工作完成,则没有任何问题,如果我们绘制过程逻辑很复杂,并且我们的界面更新还非常频繁,这时候就会造成界面的卡顿,影响用户体验,为此Android提供了SurfaceView来解决这一问题.
-- SurfaceView 继承自View,是 Android 中一种比较特殊的视图(View)
-    - 它跟普通View最大的区别是它有自己的Surface,在WMS中有对应的WindowState,在SurfaceFlinger中有Layer
-    - 一般的Activity包含的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的。这个DecorView在WMS中有一个对应的WindowState。相应地,在SF中对应的Layer。
-    - SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SF中也会有自己的Layer。虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。
-- SurfaceView 应用场景
-    - 综合这些特点,SurfaceView 一般用在游戏、视频、摄影等一些复杂 UI 且高效的图像的显示,这类的图像处理都需要开单独的线程来处理。
-- SurfaceView 优点如下
-    - SurfaceView 通过子线程中进行画面更新,View 则在主线程中进行画面更新。
-    - SurfaceView 用于被动更新,如频繁画面更新,View 则用于主动更新,如触摸点击等事件响应等。
-    - SurfaceView 在底层实现了双缓冲机制,效率大大提升了,View 则没有。
-    - 在一个独立的线程中进行绘制,不会影响主线程。
-- SurfaceView 缺点如下
-    - Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中。
-    - SurfaceView 不能嵌套使用
-
-
-
-### 02.SurfaceView的原理
-- 如果当前画面需要不停绘制或者数据处理量较大时,为避免 UI 线程堵塞,就用 SurfaceView 代替 View。
-- SurfaceView拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面,由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行行绘制,由于不占用主线程资源,使得它可以实现大多复杂而高效的界面绘制,如视频播放 VideoView 和OpenGl es的 GLSurfaceView 直播软件的 不停地点赞动效、天气软件的全屏雨雪动效、游戏中的流水、云之类的变化等等。
-    - ![image](https://img-blog.csdnimg.cn/20191211165629427.png)
-
-
-
-### 03.SurfaceView相关类
-- 要使用 SurfaceView ,就必须了解它的另外两个组件:Surface 和 SurfaceHolder
-- SurfaceView中的MVC框架
-    - 要了解SurfaceView,还必须要了解和它息息相关的其他两个组件:Surface 和 SurfaceHolder。
-    - Surface其实就视图数据,SurfaceHolder我们都知道是个接口,用来进行绘制。而SurfaceView是显示视图并且和用户交互的界面。
-    - 而MVC(Model-View-Controller)框架,model是数据,也就是这里的Surface,View是用来显示的,也就是SurfaceView,而控制器,也就是这里SurfaceHolder。
-
-
-
-#### 3.1 Surface说明
-- 首先看一下这个类的代码
-    ```
-    public class Surface implements Parcelable {
-        
-    }
-    ```
-- 看 Surface 这个类,它实现了 Parcelable 接口进行序列化
-    - 这里主要用来在进程间传递 surface 对象,用来处理屏幕显示缓冲区的数据,源码中对它的注释为: Handle onto a raw buffer that is being managed by the screen compositor. 
-    - Surface是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。
-    - 由屏幕显示内容合成器(screen compositor)所管理的原生缓冲器的句柄(类似句柄) 
-    - 名词解释:句柄,英文:HANDLE,数据对象进入内存之后获取到内存地址,但是所在的内存地址并不是固定的,需要用句柄来存储内容所在的内存地址。从数据类型上来看它只是一个32位(或64位)的无符号整数。 
-    - Surface 充当句柄的角色,用来获取源生缓冲区以及其中的内容 
-    - 源生缓冲区(raw buffer)用来保存当前窗口的像素数据 
-    - 于是可知 Surface 就是 Android 中用来绘图的的地方,具体来说应该是 Surface 中的 Canvas Surface 中定义了画布相关的 Canvas 对象
-- 绘图通常在一个 Canvas 对象上进行的
-    - Surface 中也包含了一个 Canvas 对象,这里的 CompatibleCanvas 是Surface.java 中的一个内部类,其中包含一个矩阵对象Matrix(变量名mOrigMatrix)。矩阵Matrix就是一块内存区域,针对View的各种绘画操作都保存在此内存中。
-    - Surface 内部有一个 CompatibleCanvas 的内部类,这个内部类的作用是为了能够兼容 Android 各个分辨率的屏幕,根据不同屏幕的分辨率处理不同的图像数据。  
-    ```
-    private final class CompatibleCanvas extends Canvas {
-        // A temp matrix to remember what an application obtained via {@link getMatrix}
-        private Matrix mOrigMatrix = null;
-    
-        @Override
-        public void setMatrix(Matrix matrix) {
-            if (mCompatibleMatrix == null || mOrigMatrix == null || mOrigMatrix.equals(matrix)) {
-                // don't scale the matrix if it's not compatibility mode, or
-                // the matrix was obtained from getMatrix.
-                super.setMatrix(matrix);
-            } else {
-                Matrix m = new Matrix(mCompatibleMatrix);
-                m.preConcat(matrix);
-                super.setMatrix(m);
-            }
-        }
-    
-        @SuppressWarnings("deprecation")
-        @Override
-        public void getMatrix(Matrix m) {
-            super.getMatrix(m);
-            if (mOrigMatrix == null) {
-                mOrigMatrix = new Matrix();
-            }
-            mOrigMatrix.set(m);
-        }
-    }
-    ```
-- 两个重要的方法
-    - 需要说明的是,这里的 lockCanvas 并不是实际使用 SurfaceView 来进行绘图时 SurfaceHolder 对象调用的 lockCanvas 以及 unlockCanvasAndPost 方法。实际例子中使用的方法是在 SurfaceView 内部封装过对这两个方法封装之后的。 
-    - lockCanvas(...) + Gets a Canvas for drawing into this surface. 获取进行绘画的 Canvas 对象 + After drawing into the provided Canvas, the caller must invoke unlockCanvasAndPost to post the new contents to the surface. 绘制完一帧的数据之后需要调用 unlockCanvasAndPost 方法把画布解锁,然后把画好的图像 Post 到当前屏幕上去显示 + 当一个 Canvas 在被绘制的时候,它是出于被锁定的状态,就是说必须等待正在绘制的这一帧绘制完成之后并解锁画布之后才能进行别的操作 + 实际锁住 Canvas 的过程是在 jni 层完成的 
-    - unlockCanvasAndPost(...) + Posts the new contents of the Canvas to the surface and releases the Canvas.将新绘制的图像内容传给 surface 之后这个 Canvas 对象会被释放掉(实际释放的过程是在 jni 层完成的)
-- Surface 的 lockCanvas 和 unlockCanvasAndPost 两个方法最终都是调用 jni 层的方法来处理
-
-
-#### 3.2 SurfaceHolder
-- android.view.SurfaceHolder SurfaceHolder 实际上是一个接口,它充当的是 Controller 的角色。
-    ```
-    public interface SurfaceHolder {
-    
-    }
-    ```
-- 来看下注释是怎么说的
-    - Abstract interface to someone holding a display surface. 一个针对 Surface 的抽象接口
-    - Allows you to control the surface size and format, edit the pixels in the surface, and monitor changes to the surface. 赤裸裸的 Controller 角色,可以控制 Surface 的大小和格式,监控 Surface 的变化(在回调函数中对 Surface 的变化做相应的处理)
-    - When using this interface from a thread other than the one running its SurfaceView, you will want to carefully read the methods Callback.surfaceCreated() 如果用子线程来处理 SurfaceView 的绘制,需要用到接下来要介绍的关键接口 Callback 中的 surfaceCreated 方法。可以看到之前给的例子中就是在 surfaceCreated 方法中开启的绘制动画的线程
-- 关键接口 Callback
-    - Callback 是 SurfaceHolder 内部的一个接口,例子中就实现了这个接口来控制绘制动画的线程。
-- 接口中有以下三个方法 
-    - public void surfaceCreated(SurfaceHolder holder); + Surface 第一次被创建时被调用,例如 SurfaceView 从不可见状态到可见状态时 + 在这个方法被调用到 surfaceDestroyed 方法被调用之前的这段时间,Surface 对象是可以被操作的,拿 SurfaceView 来说就是如果 SurfaceView 只要是在界面上可见的情况下,就可以对它进行绘图和绘制动画 + 这里还有一点需要注意,Surface 在一个线程中处理需要渲染的图像数据,如果你已经在另一个线程里面处理了数据渲染,就不需要在这里开启线程对 Surface 进行绘制了 
-    - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height); + Surface 大小和格式改变时会被调用,例如横竖屏切换时如果需要对 Surface 的图像和动画进行处理,就需要在这里实现 + 这个方法在 surfaceCreated 之后至少会被调用一次 
-    - public void surfaceDestroyed(SurfaceHolder holder); + Surface 被销毁时被调用,例如 SurfaceView 从可见到不可见状态时 + 在这个方法被调用过之后,就不能够再对 Surface 对象进行任何操作,所以需要保证绘图的线程在这个方法调用之后不再对 Surface 进行操作,否则会报错
-
-
-#### 3.3 SurfaceView
-- SurfaceView,就是用来显示 Surface 数据的 View,通过 SurfaceView 来看到 Surface 的数据。
-    ```
-    public class SurfaceView extends View {  
-        // code.....
-    }
-    ```
-- 分析一下源码中对 SurfaceView 的注释
-    - Provides a dedicated drawing surface embedded inside of a view hierarchy. 在屏幕显示的视图层中嵌入了一块用做图像绘制的 Surface 视图
-    - the SurfaceView punches a hole in its window to allow its surface to be displayed. SurfaceView 在屏幕上挖了个洞来来世它所绘制的图像
-- 挖洞是什么鬼?
-    - 这里引入一个Z轴的概念,SurfaceView 视图所在层级的Z轴位置是小于用来其宿主 Activity 窗口的 Layer 的 Z 轴的,就是说其实 SurfaceView 实际是显示在 Activity 所在的视图层下方的
-- 那么问题就来了,为什么还是能看到 SurfaceView?
-    - 形象一点的说法就是你在墙上凿了一个方形的洞,然后在洞上装了块玻璃,你就能看到墙后面的东西了。SurfaceView 就做了这样的事情,它把 Activity 所在的层当作了墙
-    - The Surface will be created for you while the SurfaceView's window is visible. 这里说明了动画是什么时候开始的,当 SurfaceView 可见时,就可以开始在 Canvas 上绘制图像,并把图像数据传递给 Surface 用来显示在 SurfaceView 上
-    - you should implement SurfaceHolder.Callback#surfaceCreated and SurfaceHolder.Callback#surfaceDestroyed to discover when the Surface is created and destroyed as the window is shown and hidden. 在使用 SurfaceView 的地方需要实现 SurfaceHolder.CallBack 回调,来对 Surface 的创建和销毁进行监听以及做响应的处理,这里的处理指的是开始对 Canvas 进行绘制并把数据传递给 Surface 来做显示
-
-
-
-### 04.SurfaceView的使用
-- 如何使用 SurfaceView
-    - 需要实现 SurfaceHolder.Callback 接口
-    - 需要在 SurfaceHolder.Callback 的 surfaceCreated 方法中开启一个线程进行动画的逐帧的绘制
-    - 需要在 SurfaceHolder.Callback 的 surfaceDestroyed 方法中结束绘画的线程并调用 SurfaceHolder 的 removeCallbck 方法
-    - 绘画线程每一帧开始之前需要调用 lockCanvas 方法锁住画布进行绘图
-    - 绘制完一帧的数据之后需要调用 unlockCanvasAndPost 方法提交数据来显示图像
-- 关于自定义SurfaceView代码如下所示
-    ```
-    public class MySurfaceView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
-    
-        private final SurfaceHolder mHolder; // 用于控制SurfaceView
-        private Thread t; // 声明一条线程
-        private volatile boolean flag; // 线程运行的标识,用于控制线程
-        private Canvas mCanvas; // 声明一张画布
-        private Paint p; // 声明一支画笔
-        float m_circle_r = 10;
-    
-        public MySurfaceView(Context context) {
-            super(context);
-            mHolder = getHolder(); // 获得SurfaceHolder对象
-            mHolder.addCallback(this); // 为SurfaceView添加状态监听
-            p = new Paint(); // 创建一个画笔对象
-            p.setColor(Color.WHITE); // 设置画笔的颜色为白色
-            setFocusable(true); // 设置焦点
-        }
-    
-        /**
-         * 当SurfaceView创建的时候,调用此函数
-         */
-        @Override
-        public void surfaceCreated(SurfaceHolder holder) {
-            // 创建一个线程对象
-            t = new Thread(this);
-            // 把线程运行的标识设置成true
-            flag = true;
-            // 启动线程
-            t.start();
-        }
-    
-        /**
-         * 当SurfaceView的视图发生改变的时候,调用此函数
-         */
-        @Override
-        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-        }
-    
-        /**
-         * 当SurfaceView销毁的时候,调用此函数
-         */
-        @Override
-        public void surfaceDestroyed(SurfaceHolder holder) {
-            // 把线程运行的标识设置成false
-            flag = false;
-            mHolder.removeCallback(this);
-        }
-    
-        /**
-         * 当屏幕被触摸时调用
-         */
-        @Override
-        public boolean onTouchEvent(MotionEvent event) {
-    
-            return true;
-        }
-    
-        /**
-         * 当用户按键时调用
-         */
-        @Override
-        public boolean onKeyDown(int keyCode, KeyEvent event) {
-            if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
-            }
-            return super.onKeyDown(keyCode, event);
-        }
-    
-        @Override
-        public boolean onKeyUp(int keyCode, KeyEvent event) {
-            surfaceDestroyed(mHolder);
-            return super.onKeyDown(keyCode, event);
-        }
-    
-        @Override
-        public void run() {
-            while (flag) {
-                try {
-                    synchronized (mHolder) {
-                        // 让线程休息100毫秒
-                        Thread.sleep(100);
-                        // 调用自定义画画方法
-                        Draw();
-                    }
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
-                } finally {
-                    if (mCanvas != null) {
-                        // mHolder.unlockCanvasAndPost(mCanvas);//结束锁定画图,并提交改变。
-                    }
-                }
-            }
-        }
-    
-        /**
-         * 自定义一个方法,在画布上画一个圆
-         */
-        protected void Draw() {
-            // 获得画布对象,开始对画布画画
-            mCanvas = mHolder.lockCanvas();
-            if (mCanvas != null) {
-                Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
-                paint.setColor(Color.BLUE);
-                paint.setStrokeWidth(10);
-                paint.setStyle(Style.FILL);
-                if (m_circle_r >= (getWidth() / 10)) {
-                    m_circle_r = 0;
-                } else {
-                    m_circle_r++;
-                }
-                Bitmap pic = ((BitmapDrawable) getResources().getDrawable(R.drawable.ic_palyer_share)).getBitmap();
-                mCanvas.drawBitmap(pic, 0, 0, paint);
-                for (int i = 0; i < 5; i++) {
-                    for (int j = 0; j < 8; j++) {
-                        mCanvas.drawCircle(
-                                (getWidth() / 5) * i + (getWidth() / 10),
-                                (getHeight() / 8) * j + (getHeight() / 16),
-                                m_circle_r, paint);
-                    }
-                }
-                // 完成画画,把画布显示在屏幕上
-                mHolder.unlockCanvasAndPost(mCanvas);
-            }
-        }
-    }
-    ```
-- 如何使用该自定义MySurfaceView
-    ```
-    mSurfaceView = new MySurfaceView(this);
-    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
-            RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
-    fl.addView(mSurfaceView, params);
-    
-    SurfaceHolder holder = mSurfaceView.getHolder();
-    //增加surfaceView的监听
-    holder.addCallback(new SurfaceHolder.Callback() {
-        @Override
-        public void surfaceCreated(SurfaceHolder surfaceHolder) {
-    
-        }
-    
-        @Override
-        public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
-    
-        }
-    
-        @Override
-        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
-        }
-    });
-    ```
-
-
-### 05.draw和onDraw都不执行
-- 在自定义MySurfaceView控件中,重写draw的两个方法,如下所示,发现这两个方法竟然没有执行,这个是为什么呢?
-    ```
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-        System.out.println("MySurfaceView-----onDraw");
-    }
-    
-    @Override
-    public void draw(Canvas canvas) {
-        super.draw(canvas);
-        System.out.println("MySurfaceView-----draw");
-    }
-    ```
-- 看一下源码中的构造方法。
-    - 查看SurfaceView的源码可知,在其构造函数中调用了setWillNotDraw(true);该方法会导致draw()、onDraw()都不执行。
-    ```
-    public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        mRenderNode.requestPositionUpdates(this);
-    
-        setWillNotDraw(true);
-    }
-    
-    public void setWillNotDraw(boolean willNotDraw) {
-        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
-    }
-    ```
-
-
-
-### 06.SurfaceView双缓冲
-- 双缓冲,在运用时可以理解为:
-    - SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas。
-    - 每次实际显示的是frontCanvas,backCanvas存储的是上一次更改前的视图,当使用lockCanvas()获取画布时,得到的实际上是backCanvas而不是正在显示的frontCanvas,之后你在获取到的backCanvas上绘制新视图,再unlockCanvasAndPost(canvas)此视图,那么上传的这张canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。
-    - 例如,如果你已经先后两次绘制了视图A和B,那么你再调用lockCanvas()获取视图,获得的将是A而不是正在显示的B,之后你讲重绘的C视图上传,那么C将取代B作为新的frontCanvas显示在SurfaceView上,原来的B则转换为backCanvas。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+ 0 - 21
ReadMeWiki/31.音频播放器通用框架.md

@@ -1,21 +0,0 @@
-# 音频播放器通用框架
-#### 目录介绍
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+ 0 - 108
ReadMeWiki/43.音视频编解码操作.md

@@ -1,108 +0,0 @@
-#### **目录介绍**
-- **1.做Android视频编辑的可行性开源方案**
-- 1.1 第三方流行框架
-- 1.2 Android自带框架
-- 1.3 这些方案功能分析
-- 1.4 这些方案学习门槛分析
-- **2.对音频进行编码和解码**
-- 2.1 MediaCodec基本介绍
-- 2.2 MediaCodec核心原理
-- **3.如何通俗理解编码和解码**
-- 3.1 什么是编码
-- 3.2 什么是解码
-- **4.对音频如何混音添加背景音乐**
-- 4.1 待更新实现
-
-
-
-### 1.做Android视频编辑的可行性开源方案
-#### 1.1 第三方流行框架
-- 大家熟知的ffmpeg,将ffmpeg移植到anroid平台,编译成so文件,由jni 调用,可以实现音视频的分离、裁剪、拼合、加字幕、滤镜等功能。
-
-
-#### 1.2 Android自带框架
-- android 自带的MediaCodec 框架,MediaCodec框架底层调用的是StageFright库,StageFright库是默认封装在android系统里面的
-- 官方的 MediaCodec API 虽然支持硬件编解码加速,但是问题和局限还是很多的,一方面是只能在 Android 4.1 以上机型上才能使用,另一方面,由于 Android 手机种类繁多,厂商对底层源码的修改各不相同,导致 MediaCodec API 在实际使用中,会遇到很多坑,有很多兼容性的问题,因此,可以考虑采用第三方的编解码库。
-
-
-#### 1.3 这些方案功能分析
-- **1.3.1 ffmpeg功能分析**
-- ffmpeg 无疑排第一位,他集合了视频编解码、视频滤镜、流媒体推流、音频各种特效等等,基本上你能想到的功能都在里面。
-- 在网上看到说,国内像暴风,某某影音,某某视频等等都用到了该开源框架,
-
-- **1.3.1 MediaCodec功能分析**
-- MediaCodec涵盖了音视频解复用、音频解码、视频解码、音频编码、视频编码、音视频合并的整个流程。跟ffmpeg相比,MediaCodec 更接近底层硬件。这个方案如果想要实现视频的滤镜、字幕、拼接等功能的话,需要自己配合OpenGL ES 来实现,另外,音视频拼接的话,要考虑到不同音频采样率的重采样问题,音频重采用问题,需要懂得傅立叶变换相关的离散信号变换方法,如果要实现音频特效,如变声、均衡器的话,也需要懂得上述信号变换方法。
-
-
-#### 1.4 这些方案学习门槛分析
-- 如果只是做视频转码、加文字、图片特效等,ffmpeg和MediaCodec 旗鼓相当。如果是要拼接视频、做音频的变声、均衡器特效的话,MediaCodec是难度最高的,因为这一切需要你从底层原理做起。
-- 运行效率:MediaCodec硬解硬编最快,ffmpeg硬解硬编方案稍慢。
-- 稳定性: MediaCodec和ffmpeg 的硬解硬编方案旗鼓相当。
-- 打包占用空间:国内最得最好的ffmpeg硬解硬编方案,其so文件在10.几M,MediaCodec由于是纯java 代码,占用空间很容易做到几百K甚至几十K。
-
-
-### 2.对音频进行编码
-#### 2.1 MediaCodec基本介绍
-- 提供了一套访问 Android 底层多媒体模块的接口,主要是音视频的编解码接口
-- Android 应用层统一由 MediaCodec API 来提供各种音视频编解码功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等等
-
-#### 2.2 MediaCodec核心原理
-- **2.2.1 MediaCodec 使用的基本流程**
-```
-- createEncoderByType/createDecoderByType
-- configure
-- start
-- while(1) {
-    - dequeueInputBuffer
-    - queueInputBuffer
-    - dequeueOutputBuffer
-    - releaseOutputBuffer
-}
-- stop
-- release
-```
-
-- **2.2.2 Buffer队列示意图**
-- ![image](http://s3.51cto.com/wyfs02/M00/7E/7B/wKioL1cCSojSz4HdAAD0xxQxvwg230.png)
-
-- **2.2.3 缓冲处理队列的逻辑**
-- MediaCodec 架构上采用了2个缓冲区队列,异步处理数据,下面描述的 Client 和 MediaCodec 模块是并行工作的(注:这里的 Client 就是指 “开发者,API 的使用者”):
-    * (1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]
-    * (2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer] 
-    * (3)MediaCodec 模块从 input 缓冲区队列取一帧数据进行编解码处理
-    * (4)编解码处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列
-    * (5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]
-    * (6)Client 对编解码后的 buffer 进行渲染/播放
-    * (7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]
-- MediaCodec 在架构上,其实是采用了一种基于“环形缓冲区”的“生产者-消费者”模式,它设计了 2 个基于 idx 序号的“环形缓冲区” ,注意,是 2 个,一个在 input 端, 一个在 output 端。
-
-
-
-
-### 5.其他问题说明
-#### 5.1 版本更新情况
-- v1.0.0 2017年12月8日
-- v1.0.1 2018年2月2日
-
-
-#### 5.2 参考链接
-- Android硬编码——音频编码、视频编码及音视频混合:http://blog.csdn.net/junzia/article/details/54018671
-- android MediaCodec 音频编解码的实现——转码:https://www.cnblogs.com/Sharley/p/5964490.html
-- Android音频开发(5):音频数据的编解码:http://blog.51cto.com/ticktick/1760191
-- Android 音视频编辑经验总结及开源工程分享:https://www.cnblogs.com/jerrychen33/p/8148993.html
-
-
-#### 5.2 个人博客
-- **github:** [https://github.com/yangchong211](https://github.com/yangchong211)
-- **知乎:** [https://www.zhihu.com/people/yang-chong-69-24/pins/posts](https://www.zhihu.com/people/yang-chong-69-24/pins/posts)
-- **简书:** [http://www.jianshu.com/u/b7b2c6ed9284](http://www.jianshu.com/u/b7b2c6ed9284)
-- **csdn:** [http://my.csdn.net/m0_37700275](http://my.csdn.net/m0_37700275)
-- **喜马拉雅听书:** [http://www.ximalaya.com/zhubo/71989305/](http://www.ximalaya.com/zhubo/71989305/)
-- 泡在网上的日子:[http://www.jcodecraeer.com/member/content_list.php?channelid=1](http://www.jcodecraeer.com/member/content_list.php?channelid=1)
-- 邮箱:yangchong211@163.com
-- 阿里云博客:[https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV](https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV)
-
-
-
-
-

+ 0 - 171
ReadMeWiki/48.音视频问题考点.md

@@ -1,171 +0,0 @@
-# 视频录制和编辑
-#### 目录介绍
-
-
-
-
-
-#### 14.0.0.0 SurfaceView是做什么?SurfaceView和View的本质区别?SurfaceView优缺点有哪些?
-- SurfaceView是用来做什么?
-    - SurfaceView 是 Android 中一种比较特殊的视图,它与视图容器并不是在同一个视图层上,绘制在一个独立的线程中完成,不需要及时响应用户的输入,也不会造成响应的 ANR 问题。SurfaceView 一般用在游戏、视频、摄影等一些复杂 UI 且高效的图像的显示,这类的图像处理都需要开单独的线程来处理。
-- SurfaceView和View的最本质的区别?
-    - SurfaceView是在一个新起的单独线程【子线程】中可以重新绘制画面,而view必须在UI的主线程中更新画面。
-    - 在UI的主线程中更新画面可能会引发问题,比如你更新的时间过长,那么你的主UI线程就会被你正在画的函数阻塞。那么将无法响应按键、触屏等消息;使用SurfaceView由于是在新的线程中更新画面,或者说SurfaceView却能在非UI线程中绘制,所以不会阻塞你的UI主线程导致卡顿甚至ANR。但这也带来了另外一个问题,就是事件同步。比如你触屏了一下,你需要SurfaceView中thread处理,一般就需要有一个event queue的设计来保存touchevent,这会稍稍复杂一点,因为涉及到线程安全。
-    - View 在绘图时没有实现双缓冲机制,SurfaceView 在底层机制中就实现了双缓冲机制。
-- SurfaceView优缺点有哪些?
-    - 优点:
-        - 可以在一个独立的线程中进行绘制,不会影响主线程
-        - 使用双缓冲机制,播放视频时画面更流畅
-    - 缺点:
-        - Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中。SurfaceView 不能嵌套使用。【或者说不具备view的属性】
-
-
-
-#### 14.0.0.1 SurfaceView如何保证UI界面的流畅性?如何理解双缓冲机制?
-- SurfaceView如何保证UI界面的流畅性
-    - SurfaceView中采用了双缓冲机制,保证了UI界面的流畅性
-- 双缓冲机制由来
-    - 问题的由来
-        - CPU访问内存的速度要远远快于访问屏幕的速度。如果需要绘制大量复杂的图像时,每次都一个个从内存中读取图形然后绘制到屏幕就会造成多次地访问屏幕,从而导致效率很低。这就跟CPU和内存之间还需要有三级缓存一样,需要提高效率。
-    - 第一层缓冲
-        - 在绘制图像时不用上述一个一个绘制的方案,而采用先在内存中将所有的图像都绘制到一个Bitmap对象上,然后一次性将内存中的Bitmap绘制到屏幕,从而提高绘制的效率。Android中View的onDraw()方法已经实现了这一层缓冲。onDraw()方法中不是绘制一点显示一点,而是都绘制完后一次性显示到屏幕。
-    - 第二层缓冲
-        - onDraw()方法的Canvas对象是和屏幕关联的,而onDraw()方法是运行在UI线程中的,如果要绘制的图像过于复杂,则有可能导致应用程序卡顿,甚至ANR。因此我们可以先创建一个临时的Canvas对象,将图像都绘制到这个临时的Canvas对象中,绘制完成之后再将这个临时Canvas对象中的内容(也就是一个Bitmap),通过drawBitmap()方法绘制到onDraw()方法中的canvas对象中。这样的话就相当于是一个Bitmap的拷贝过程,比直接绘制效率要高,可以减少对UI线程的阻塞。
-- 双缓冲在运用时可以理解为:
-    - SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas,每次实际显示的是frontCanvas,backCanvas存储的是上一次更改前的视图,当使用lockCanvas()获取画布时,得到的实际上是backCanvas而不是正在显示的frontCanvas,之后你在获取到的backCanvas上绘制新视图,再unlockCanvasAndPost(canvas)此视图,那么上传的这张canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。例如,如果你已经先后两次绘制了视图A和B,那么你再调用lockCanvas()获取视图,获得的将是A而不是正在显示的B,之后你将重绘的C视图上传,那么C将取代B作为新的frontCanvas显示在SurfaceView上,原来的B则转换为backCanvas。
-
-
-
-
-
-
-
-#### 14.0.0.2 SurfaceView在新的线程中更新画面为何不会阻塞UI主线程?是否跟在子线程中不能操作UI矛盾?
-- SurfaceView在新的线程中更新画面为何不会阻塞UI主线程?
-    - 首先看一下下面代码
-        - 实现 SurfaceHolder.Callback 接口,并且重写里面的三个方法
-        - 在 SurfaceHolder.Callback 的 surfaceCreated 方法中开启一个线程进行图像的绘制
-        - 在 SufaceHolder.Callback 的 surfaceDestroyed 方法中,结束绘制线程并调用 SurfaceHolder 的 removeCallbck 方法
-        - 在绘制线程每帧开始之前,调用 lockCanvas 方法锁住画布进行绘图
-        - 绘制完一帧的数据之后,调用 unlockCanvasAndPost 方法提交数据来显示图像
-        - 用于控制子线程绘制的标记参数,如上面代码中的mIsDrawing变量,需要用volatile关键字修饰,以保证多线程安全。
-        - 由于SurfaceView常被用于游戏、视频等场景,绘制操作会相对复杂很多,通常都需要开启子线程,在子线程中执行绘制操作,以免阻塞UI线程。在子线程中,我们通过SurfaceHolder的lockCanvas方法获取Canvas对象来进行具体的绘制操作,此时Canvas对象被当前线程锁定,绘制完成后通过SurfaceHolder的unlockCanvasAndPost方法提交绘制结果并释放Canvas对象。
-    - 如果上面不容易理解,那么这里提供伪代码,一看应该就更加容易明白!
-
-    ```
-    /**
-     * 必须实现SurfaceHolder.Callback接口和Runnable接口
-     */
-    public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
-    
-        // 是否绘制
-        private volatile boolean mIsDrawing;
-        // SurfaceView 控制器
-        private SurfaceHolder mSurfaceHolder;
-        // 画笔
-        private Paint mPaint;
-        // 画布
-        private Canvas mCanvas;
-        // 独立的线程
-        private Thread mThread;
-    
-        public MySurfaceView(Context context) {
-            super(context);
-            init();
-        }
-    
-        public MySurfaceView(Context context, AttributeSet attrs) {
-            super(context, attrs);
-            init();
-        }
-    
-        public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
-            super(context, attrs, defStyleAttr);
-            init();
-        }
-    
-        private void init() {
-            mSurfaceHolder = getHolder();
-            // 注册回调事件
-            mSurfaceHolder.addCallback(this);
-    
-            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-            mPaint.setStyle(Paint.Style.STROKE);
-        }
-    
-        @Override
-        public void surfaceCreated(SurfaceHolder holder) {
-            VideoLogUtil.d("onSurfaceCreated");
-            mThread = new Thread(this, "yc");
-        }
-    
-        @Override
-        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
-            VideoLogUtil.d("onSurfaceChanged"+format+ "----"+ width+ "----"+ height);
-            //并开启线程
-            mIsDrawing = true;
-            mThread.start();
-        }
-    
-        @Override
-        public void surfaceDestroyed(SurfaceHolder holder) {
-            VideoLogUtil.d("onSurfaceDestroyed");
-            // 不再绘制,移除回调,线程终止
-            mIsDrawing = false;
-            mSurfaceHolder.removeCallback(this);
-            mThread.interrupt();
-        }
-    
-        @Override
-        public void run() {
-            while (mIsDrawing) {
-                VideoLogUtil.d("draw canvas");
-                // 锁定画布,获得画布对象
-                mCanvas = mSurfaceHolder.lockCanvas();
-                try {
-                    //使用画布做具体的绘制
-                    draw();
-                    // 线程休眠 100 ms
-                    Thread.sleep(100);
-                } catch (Exception e) {
-                    VideoLogUtil.d(e.getMessage());
-                } finally {
-                    // 解锁画布,提交绘制,显示内容
-                    if (mCanvas != null) {
-                        mSurfaceHolder.unlockCanvasAndPost(mCanvas);
-                    }
-                }
-            }
-        }
-    
-        private void draw() {
-            //开始绘制
-        }
-    }
-    ```
-- 是否跟在子线程中不能操作UI矛盾?
-    - 为什么不允许在子线程中访问UI《Android艺术探索》
-        - 这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:
-        - ①首先加上锁机制会让UI访问的逻辑变得复杂
-        - ②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
-        - 所以最简单且高效的方法就是采用单线程模型来处理UI操作。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+ 0 - 21
ReadMeWiki/61.如何打造全局悬浮窗.md

@@ -1,21 +0,0 @@
-# 如何打造全局悬浮窗
-#### 目录介绍
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

+ 0 - 116
ReadMeWiki/63.获取视频对象大小.md

@@ -1,116 +0,0 @@
-# 视频内存优化治理
-#### 目录介绍
-- 01.什么时候要知道对象内存大小
-- 02.如何获取对象占用内存大小
-- 03.看案例分析对象内存大小
-- 04.使用Unsafe来获取内存大小
-
-
-### 01.什么时候要知道对象内存大小
-- 在内存足够用的情况下我们是不需要考虑java中一个对象所占内存大小的。
-- 但当一个系统的内存有限,或者某块程序代码允许使用的内存大小有限制,又或者设计一个缓存机制,当存储对象内存超过固定值之后写入磁盘做持久化等等,总之我们希望像写C一样,java也能有方法实现获取对象占用内存的大小。
-
- 
-
-### 02.如何获取对象占用内存大小
-- 在回答这个问题之前,我们需要先了解java的基础数据类型所占内存大小。
-    - 1k = 1024个字节(byte);1M = 1024kb
-    ```
-    数据类型	    所占空间(byte)
-    byte    	1
-    short	    2
-    int	        4
-    long	    8
-    float	    4
-    double	    8
-    char  	    2
-    boolean	    1
-    ```
-- 当然,java作为一种面向对象的语言,更多的情况需要考虑对象的内存布局,java对于对象所占内存大小需要分两种情况考虑:
-    - 对象类型:一般非数组对象	
-        - 内存布局构成:8个字节对象头(mark) + 4/8字节对象指针 + 数据区 + padding内存对齐(按照8的倍数对齐)
-    - 对象类型:数组对象                                 	
-        - 内存布局构成:8个字节对象头(mark) + 4/8字节对象指针 + 4字节数组长度 + 数据区 + padding内存对齐(按照8的倍数对齐)
-- 可以看到数组类型对象和普通对象的区别仅在于4字节数组长度的存储区间。而对象指针究竟是4字节还是8字节要看是否开启指针压缩。
-    - Oracle JDK从6 update 23开始在64位系统上会默认开启压缩指针:http://rednaxelafx.iteye.com/blog/1010079。
-    - 如果要强行关闭指针压缩使用-XX:-UseCompressedOops,强行启用指针压缩使用: -XX:+UseCompressedOops。 
-
-
-### 03.看案例分析对象内存大小
-- 接下来我们来举例来看实现java获取对象所占内存大小的方法,假设我们有一个类的定义如下:
-    ```
-    private static class ObjectA {
-        String str;   // 4
-        int i1;       // 4
-        byte b1;      // 1
-        byte b2;      // 1
-        int i2;       // 4
-        ObjectB obj;  // 4
-        byte b3;      // 1
-    }
-  
-    ObjectA obj = new ObjectA();
-    ```
-- 如果我们直接按照上面掌握的java对象内存布局进行计算,则有:
-    - Size(ObjectA) = Size(对象头(_mark)) + size(oop指针) + size(数据区)
-    - Size(ObjectA) = 8 + 4 + 4(String) + 4(int) + 1(byte) + 1(byte) + 2(padding) + 4(int) + 4(ObjectB指针) + 1(byte) + 7(padding)
-    - Size(ObjectA) = 40
-  
-
-### 04.使用Unsafe来获取内存大小
-- 代码如下所示
-    ```
-    private final static Unsafe UNSAFE;
-    // 只能通过反射获取Unsafe对象的实例
-    static {
-        try {
-            UNSAFE = (Unsafe) Unsafe.class.getDeclaredField("theUnsafe").get(null);
-        } catch (Exception e) {
-            throw new Error();
-        }
-    }
-    
-    Field[] fields = ObjectA.class.getDeclaredFields();
-    for (Field field : fields) {
-      System.out.println(field.getName() + "---offSet:" + UNSAFE.objectFieldOffset(field));
-    }
-    ```
-- 输出结果为:
-    ```
-    str---offSet:24
-    i1---offSet:12
-    b1---offSet:20
-    b2---offSet:21
-    i2---offSet:16
-    obj---offSet:28
-    b3---offSet:22
-    ```
-- 我们同样可以算得对象实际占用的内存大小:
-    - Size(ObjectA) = Size(对象头(_mark)) + size(oop指针) + size(排序后数据区)  =  8 + 4 + (28+4-12)  =  32.
-- 再回过头来,看我们在通过代码获取对象所占内存大小之前的预估值40。比我们实际算出来的值多了8个字节。
-    - 通过Unsafe打印的详细信息,我们不难想到这其实是由hotspot创建对象时的排序决定的:
-    - HotSpot创建的对象的字段会先按照给定顺序排列,默认的顺序为:从长到短排列,引用排最后: long/double –> int/float –> short/char –> byte/boolean –> Reference。
-- 所以我们重新计算对象所占内存大小得:
-    - Size(ObjectA) = Size(对象头(_mark)) + size(oop指针) + size(排序后数据区)
-    - Size(ObjectA) = 8 + 4 + 4(int) + 4(int) + byte(1) + byte(1) + 2(padding) + 4(String) + 4(ObjectB指针)
-    - Size(ObjectA) = 32
-    - 与上面计算结果一致。
-
-
-
-
-
-
-
-
-
-
-### 参考链接
-- https://www.cnblogs.com/tesla-turing/p/11487815.html
-- https://www.cnblogs.com/Kidezyq/p/8030098.html
-
-
-
-
-
-

+ 0 - 152
VideoView/wiki/01.全局悬浮窗实践.md

@@ -1,152 +0,0 @@
-#### 目录介绍
-- 01.全局悬浮窗介绍
-- 02.悬浮窗注意要点
-- 03.绘制悬浮窗步骤
-- 04.悬浮窗遇到的问题
-
-
-
-
-### 01.全局悬浮窗介绍
-- 什么是悬浮窗
-    - 全局悬浮窗在许多应用中都能见到,点击Home键,小窗口仍然会在屏幕上显示。
-- 悬浮窗要能够全局显示就必须要申请权限:
-    ```
-     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
-     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-     <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
-            tools:ignore="ProtectedPermissions" />
-    ```
-- 当API Level>=23的时候就要动态的申请权限了,判断是否能够绘制悬浮窗:
-    ```
-    Settings.canDrawOverlays(this)
-    ```
-    - 返回为true就表明已同意权限,否则就表示没有全局绘制的权限。此处获取权限需要跳转设置用户手动打开:
-    ```
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
-        Toast.makeText(this, "当前无权限,请授权", Toast.LENGTH_SHORT);
-        Intent intent = new Intent();
-        intent.setAction(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
-        intent.setData(Uri.parse("package:" + getPackageName()));
-        startActivityForResult(intent, 0);
-    }
-    ```
-
-
-### 02.悬浮窗注意要点
-- 应用内悬浮窗实现流程
-    - 获取WindowManager
-    - 创建悬浮View
-    - 设置悬浮View的拖拽事件
-    - 添加View到WindowManager中
-
-
-
-
-### 03.绘制悬浮窗步骤
-#### 3.1 获取WindowManager
-- 首先创建WindowManager
-    ```
-    //创建WindowManager
-    windowManager = (WindowManager)
-            applicationContext.getSystemService(Context.WINDOW_SERVICE);
-    layoutParams = new WindowManager.LayoutParams();
-    ```
-
-#### 3.2 LayoutParam设置
-- 全局的悬浮窗是通过WindowManager来绘制已达到能够全局显示的效果,而WindowManager的addView方法还需要一个WindowManager.LayoutParam对象作为参数,此处Android 8.0之后的需要适配一下:
-    - WindowManager的addView方法有两个参数,一个是需要加入的控件对象,另一个参数是WindowManager.LayoutParam对象。
-    - 这里需要着重说明的是LayoutParam里的type变量。这个变量是用来指定窗口类型的。在设置这个变量时,需要注意一个坑,那就是需要对不同版本的Android系统进行适配。
-    ```
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-      layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-    } else {
-      layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
-    }
-    ```
-- 在Android 8.0之前,悬浮窗口设置可以为TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口。
-    - 而Android 8.0对系统和API行为做了修改,包括使用SYSTEM_ALERT_WINDOW权限的应用无法再使用一下窗口类型来在其他应用和窗口上方显示提醒窗口:
-        - TYPE_PHONE
-        - TYPE_PRIORITY_PHONE
-        - TYPE_SYSTEM_ALERT
-        - TYPE_SYSTEM_OVERLAY
-        - TYPE_SYSTEM_ERROR
-    - 如果需要实现在其他应用和窗口上方显示提醒窗口,那么必须该为TYPE_APPLICATION_OVERLAY的新类型。
-    - 如果在Android 8.0以上版本仍然使用TYPE_PHONE类型的悬浮窗口,则会出现如下异常信息:
-    ```
-    android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002
-    ```
-
-
-#### 3.3 添加悬浮窗操作
-- 界面触发悬浮窗代码如下:
-    ```
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) {
-        // 获取WindowManager服务
-        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
-        // 设置LayoutParam
-        layoutParams = new WindowManager.LayoutParams();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-        } else {
-            layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
-        }
-        layoutParams.format = PixelFormat.RGBA_8888;
-        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-        //宽高自适应
-        layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
-        layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
-        //显示的位置
-        layoutParams.x = 300;
-        layoutParams.y = 300;
-    
-        // 新建悬浮窗控件
-        View view = LayoutInflater.from(this).inflate(R.layout.float_window, null);
-        view.setOnTouchListener(new FloatingOnTouchListener());
-        // 将悬浮窗控件添加到WindowManager
-        windowManager.addView(view, layoutParams);
-    }
-    ```
-
-
-#### 4.4 增加拖动功能
-- 手指滑动,需要更新悬浮窗的位置
-    ```
-    private class FloatingOnTouchListener implements View.OnTouchListener {
-        private int x;
-        private int y;
-    
-        @SuppressLint("ClickableViewAccessibility")
-        @Override
-        public boolean onTouch(View view, MotionEvent event) {
-            switch (event.getAction()) {
-                case MotionEvent.ACTION_DOWN:
-                    x = (int) event.getRawX();
-                    y = (int) event.getRawY();
-                    break;
-                case MotionEvent.ACTION_MOVE:
-                    int nowX = (int) event.getRawX();
-                    int nowY = (int) event.getRawY();
-                    int movedX = nowX - x;
-                    int movedY = nowY - y;
-                    x = nowX;
-                    y = nowY;
-                    layoutParams.x = layoutParams.x + movedX;
-                    layoutParams.y = layoutParams.y + movedY;
-                    // 更新悬浮窗控件布局
-                    windowManager.updateViewLayout(view, layoutParams);
-                    break;
-                default:
-                    break;
-            }
-            return false;
-        }
-    }
-    ```
-
-
-### 04.悬浮窗遇到的问题
-
-
-
-