
<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.8.7">Jekyll</generator><link href="https://violinday.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://violinday.github.io/" rel="alternate" type="text/html" /><updated>2020-06-01T17:41:05+08:00</updated><id>https://violinday.github.io/feed.xml</id><title type="html">Violinday</title><subtitle>Violinday个人博客</subtitle><author><name>Violinday</name></author><entry><title type="html">源码解读系列（一）：Retrofit 源码重点解读</title><link href="https://violinday.github.io/2019/09/16/Retrofit-%E6%BA%90%E7%A0%81%E9%87%8D%E7%82%B9%E8%A7%A3%E8%AF%BB/" rel="alternate" type="text/html" title="源码解读系列（一）：Retrofit 源码重点解读" /><published>2019-09-16T00:00:00+08:00</published><updated>2019-09-16T00:00:00+08:00</updated><id>https://violinday.github.io/2019/09/16/Retrofit%20%E6%BA%90%E7%A0%81%E9%87%8D%E7%82%B9%E8%A7%A3%E8%AF%BB</id><content type="html" xml:base="https://violinday.github.io/2019/09/16/Retrofit-%E6%BA%90%E7%A0%81%E9%87%8D%E7%82%B9%E8%A7%A3%E8%AF%BB/">&lt;h2 id=&quot;源码解读系列一retrofit-源码重点解读&quot;&gt;源码解读系列（一）：Retrofit 源码重点解读&lt;/h2&gt;

&lt;h3 id=&quot;retrofit-源码要点&quot;&gt;Retrofit 源码要点：&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;设置必要参数，供 ServiceMethod 被调用时使用。&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;BaseUrl&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;CallAdapterFactory（RxJavaCallAdapter等）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;ConverterFactory（GsonConverterFactory、FastjsonConverterFactory等）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Call.Factory (OkHttpClient)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;接口注解、参数、返回值类型的信息&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
  &lt;li&gt;动态代理&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;调用顺序：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;调用请求网络的”接口方法” ==&amp;gt;&lt;/p&gt;

    &lt;p&gt;动态代理类 ==&amp;gt;&lt;/p&gt;

    &lt;p&gt;生成 ServiceMethod，调用 invoke ==&amp;gt;&lt;/p&gt;

    &lt;p&gt;adapt适配合适的Call并返回给用户使用。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
  &lt;li&gt;ServiceMethod
    &lt;ul&gt;
      &lt;li&gt;
        &lt;p&gt;动态代理类的方法调用返回了由 HttpCallAdapter 包装后的 retrofit2.OkHttpCall，即 HttpCallAdapter。&lt;/p&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;ServiceMethod (寻找合适的CallAdapterFactory) &lt;strong&gt;==&amp;gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;p&gt;调用 CallAdapterFactory#adapt 方法，包装下retrofit2.OkHttpCall &lt;strong&gt;==&amp;gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;p&gt;返回 HttpCallAdapter(实现自定义HttpCall接口，和retrofit2.Call接口的方法相似，保证基础请求功能、行为可用)给 Client 调用端 &lt;strong&gt;==&amp;gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;p&gt;发送 HttpCallAdapter 到请求队列（HttpCallAdapter.enqueue(自定义 CallBack) &lt;strong&gt;==&amp;gt;&lt;/strong&gt;
retrofit2.OkHttpCall.enqueue(retrofit2.Callback) &lt;strong&gt;==&amp;gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;p&gt;okhttp3.Call call = serviceMethod.&lt;strong&gt;callFactory&lt;/strong&gt;.newCall(&lt;strong&gt;request&lt;/strong&gt;) ），最终使用 CallFactory，一般是OkHttpClient，请求网络。&lt;/p&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;retrofit 源码结构、调用流程图
 &lt;img src=&quot;../images/2019-09-21-retrofit 源码结构流程图.png&quot; alt=&quot;retrofit 源码结构流程图&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如有缺漏不足之处，欢迎创建issue，PR 😄&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">源码解读系列（一）：Retrofit 源码重点解读</summary></entry><entry><title type="html">如何从手机媒体库中获取全部视频文件</title><link href="https://violinday.github.io/2018/07/01/%E5%A6%82%E4%BD%95%E4%BB%8E%E6%89%8B%E6%9C%BA%E5%AA%92%E4%BD%93%E5%BA%93%E4%B8%AD%E8%8E%B7%E5%8F%96%E5%85%A8%E9%83%A8%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6/" rel="alternate" type="text/html" title="如何从手机媒体库中获取全部视频文件" /><published>2018-07-01T00:00:00+08:00</published><updated>2018-07-01T00:00:00+08:00</updated><id>https://violinday.github.io/2018/07/01/%E5%A6%82%E4%BD%95%E4%BB%8E%E6%89%8B%E6%9C%BA%E5%AA%92%E4%BD%93%E5%BA%93%E4%B8%AD%E8%8E%B7%E5%8F%96%E5%85%A8%E9%83%A8%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6</id><content type="html" xml:base="https://violinday.github.io/2018/07/01/%E5%A6%82%E4%BD%95%E4%BB%8E%E6%89%8B%E6%9C%BA%E5%AA%92%E4%BD%93%E5%BA%93%E4%B8%AD%E8%8E%B7%E5%8F%96%E5%85%A8%E9%83%A8%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6/">&lt;h2 id=&quot;如何从手机媒体库中获取全部视频文件&quot;&gt;如何从手机媒体库中获取全部视频文件&lt;/h2&gt;

&lt;p&gt;最近项目在做一个视频选择的功能，一开始的思路是通过 MediaStore获取媒体信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;  public static List&amp;lt;LocalVideoBean&amp;gt; getLocalVideoList(Context context) {
    List&amp;lt;LocalVideoBean&amp;gt; localVideoBeans = new ArrayList&amp;lt;&amp;gt;();
    // MediaStore.Video.Thumbnails.DATA:视频缩略图的文件路径
    String[] thumbColumns = {MediaStore.Video.Thumbnails.DATA,
            MediaStore.Video.Thumbnails.VIDEO_ID};
    // 视频其他信息的查询条件
    String[] mediaColumns = {MediaStore.Video.Media._ID,
            MediaStore.Video.Media.DATA, MediaStore.Video.Media.DURATION, MediaStore.Video.Media.SIZE};

    Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media
                    .EXTERNAL_CONTENT_URI,
            mediaColumns, null, null, null);

    if (cursor == null) {
      return localVideoBeans;
    }

    try {
      if (cursor.moveToFirst()) {
        do {
          LocalVideoBean info = new LocalVideoBean();
          int id = cursor.getInt(cursor
                  .getColumnIndex(MediaStore.Video.Media._ID));
          Cursor thumbCursor = context.getContentResolver().query(
                  MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,
                  thumbColumns, MediaStore.Video.Thumbnails.VIDEO_ID
                          + &quot;=&quot; + id, null, null);
          if (thumbCursor == null) {
            continue;
          }
          try {
            if (thumbCursor.moveToFirst()) {
              info.setId(id);
              info.setVideoPath(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media
                      .DATA)));
              ......
              localVideoBeans.add(info);
            }
          } catch (Exception e) {
            LogUtil.e(TAG, e.toString());
          } finally {
            if (!thumbCursor.isClosed()) {
              thumbCursor.close();
            }
          }
        } while (cursor.moveToNext());
      }
    } catch (Exception e) {
      LogUtil.e(TAG, e.toString());
    } finally {
      if (!cursor.isClosed()) {
        cursor.close();
      }
    }
    return localVideoBeans;
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可是发现在一加手机5和三星的部分机型上并不能将&lt;strong&gt;最近&lt;/strong&gt;拍的新视频获取到。网络上也大多是相互抄差不多的代码。&lt;/p&gt;

&lt;p&gt;后来灵机一动，既然手机上的图库软件都能将最近拍摄的视频加载出来，那么肯定存在实现方式，能将最新的视频加载出来。&lt;/p&gt;

&lt;p&gt;后来找到一个图库实现库，看了下源码，其实和上面的代码并没有多少差别：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;  /**
   * 获取本地视频
   *
   * @param context
   * @return 本地视频列表
   */
  public static List&amp;lt;LocalVideoBean&amp;gt; getLocalVideoList(Context context) {
    List&amp;lt;LocalVideoBean&amp;gt; localVideoBeans = new ArrayList&amp;lt;&amp;gt;();
    ContentResolver contentResolver = context.getContentResolver();
    String[] projection = new String[]{
            MediaStore.Video.Media._ID,
            MediaStore.Video.Media.DATA,
            MediaStore.Video.Media.DURATION,
            MediaStore.Video.Media.SIZE
    };

    Cursor cursor = contentResolver.query(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            projection,
            null,
            null,
            MediaStore.Video.Media.DATE_ADDED + &quot; DESC LIMIT &quot; + Integer.MAX_VALUE + &quot; OFFSET &quot; + 0);

    if (cursor != null) {
      try {
        int count = cursor.getCount();
        if (count &amp;gt; 0) {
          cursor.moveToFirst();
          do {
            LocalVideoBean localVideoBean = parseVideoCursor(context, cursor);
            if (localVideoBean != null) {
              localVideoBeans.add(localVideoBean);
            }
          } while (cursor.moveToNext());
        }
      } catch (Exception e) {
        LogUtil.e(TAG, e);
      } finally {
        if (!cursor.isClosed()) {
          cursor.close();
        }
      }
    }
    return localVideoBeans;
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;和第一段代码并没有什么不同之处，除了一处…&lt;/p&gt;

&lt;p&gt;Cursor cursor = contentResolver.query(
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
        projection,
        null,
        null,
        &lt;strong&gt;MediaStore.Video.Media.DATE_ADDED + “ DESC LIMIT “ + Integer.MAX_VALUE + “ OFFSET “ + 0&lt;/strong&gt;);&lt;/p&gt;

&lt;p&gt;为什么变为这个排序条件就可以将最近拍摄的视频取出呢？这需要看看源码是怎么实现的，阅读源码中…&lt;/p&gt;

&lt;p&gt;如有缺漏不足之处，欢迎创建issue，PR 😄&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">如何从手机媒体库中获取全部视频文件</summary></entry><entry><title type="html">具有选择效果的flexablelayout</title><link href="https://violinday.github.io/2018/06/17/%E5%85%B7%E6%9C%89%E9%80%89%E6%8B%A9%E6%95%88%E6%9E%9C%E7%9A%84flexboxlayout/" rel="alternate" type="text/html" title="具有选择效果的flexablelayout" /><published>2018-06-17T00:00:00+08:00</published><updated>2018-06-17T00:00:00+08:00</updated><id>https://violinday.github.io/2018/06/17/%E5%85%B7%E6%9C%89%E9%80%89%E6%8B%A9%E6%95%88%E6%9E%9C%E7%9A%84flexboxlayout</id><content type="html" xml:base="https://violinday.github.io/2018/06/17/%E5%85%B7%E6%9C%89%E9%80%89%E6%8B%A9%E6%95%88%E6%9E%9C%E7%9A%84flexboxlayout/">&lt;h2 id=&quot;具有选择效果的flexablelayout&quot;&gt;具有选择效果的flexablelayout&lt;/h2&gt;

&lt;p&gt;最近项目用到标签选择效果，类似
&lt;img src=&quot;/images/6-17-flexboxlayout.png&quot; alt=&quot;效果图&quot; /&gt;&lt;/p&gt;

&lt;p&gt;不同的是，要带有选中效果，即，点击选中，并回调选中项。
有人可能会问，为什么不说那个FlexBoxLayoutManager + RecyclerView实现点击选中等效果呢？&lt;/p&gt;

&lt;p&gt;是因为目前FlexBoxLayoutManager存在问题，在布局的时候回出现位置计算不准确的问题，详情见&lt;img src=&quot;&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;回调代码如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public interface OnSelectChangedListener {
    void onItemSelected(int position, View view);
}


final String[] labels = new String[]{&quot;程序员&quot;, &quot;影视天堂&quot;, &quot;美食&quot;, &quot;漫画.手绘&quot;, &quot;广告图&quot;, &quot;旅行.在路上&quot;,&quot;娱乐八卦&quot;, &quot;青春&quot;, &quot;谈写作&quot;, &quot;短篇小说&quot;, &quot;散文&quot;, &quot;摄影&quot;};

houseFlexboxlayoutVideolabels.setData(labels);

houseFlexboxlayoutVideolabels.setOnSelectChangedListener(new SelectableFlexboxLayout.OnSelectChangedListener() {
    @Override
    public void onItemSelected(int position, View view) {
        Toast.makeText(getApplication(),labels[position], Toast.LENGTH_LONG).show;
    }
});
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面直接将封装代码奉上，有问题可以留言联系哈。&lt;/p&gt;

&lt;p&gt;selectable_flexboxlayout_container.xml&lt;/p&gt;
&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;com.google.android.flexbox.FlexboxLayout&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns:android=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res/android&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;xmlns:link=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/tools&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;xmlns:app=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res-auto&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/house_video_label_flexbox_container&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;match_parent&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;wrap_content&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;/com.google.android.flexbox.FlexboxLayout&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;label_item.xml&lt;/p&gt;
&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;TextView&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns:android=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res/android&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;xmlns:app=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res-auto&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@+id/tv_video_label&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_width=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;wrap_content&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:layout_height=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@dimen/dimen_32dp&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:paddingLeft=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@dimen/margin_12&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:paddingRight=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@dimen/margin_12&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:gravity=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;center&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:textSize=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@dimen/textsize_13&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:textColor=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@color/color_4a4e59&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;android:background=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@drawable/selector_bg_label&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;点击效果 selector_bg_label.xml&lt;/p&gt;
&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;selector&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;xmlns:android=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://schemas.android.com/apk/res/android&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;item&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;android:drawable=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@drawable/label_selected&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;android:state_selected=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;true&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;item&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;android:drawable=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@drawable/label_selected&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;android:state_checked=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;true&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;item&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;android:drawable=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;@drawable/label_normal&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/selector&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;label_selected,label_normal即为自己实现的选中效果&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public class SelectableFlexboxLayout extends FlexboxLayout implements View.OnClickListener {

  private FlexboxLayout container;
  private LayoutInflater inflater;
  private ViewHolder mSelectedItem;
  private OnSelectChangedListener mOnSelectChangedListener;
  private List&amp;lt;ViewHolder&amp;gt; holders = new ArrayList&amp;lt;&amp;gt;();

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

  public SelectableFlexboxLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    inflater = LayoutInflater.from(getContext());
    inflate(context, R.layout.selectable_flexboxlayout_container, this);
    container = (FlexboxLayout)findViewById(R.id.house_video_label_flexbox_container);
    container.setFlexWrap(FLEX_WRAP_WRAP);
    container.setAlignItems(ALIGN_ITEMS_FLEX_START);
    container.setDividerDrawable(getResources().getDrawable(R.drawable.house_video_label_divider_placeholder));
    container.setShowDivider(SHOW_DIVIDER_MIDDLE);
  }

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

  private void addItems(CharSequence[] titles, int initSelected) {
    if (titles == null || titles.length == 0) return;

    for (int i = 0; i &amp;lt; titles.length; i++) {
      View item = inflater.inflate(R.layout.label_item, container, false);
      ViewHolder holder = new ViewHolder(item, i, titles[i]);
      item.setTag(holder);
      item.setOnClickListener(this);
      holders.add(holder);
      if (i == initSelected) {
        mSelectedItem = holder;
        holder.setItemStatus(true);
      } else {
        holder.setItemStatus(false);
      }
      container.addView(item);
    }
  }

  @Override public void onClick(View v) {
    ViewHolder holder = (ViewHolder) v.getTag();
    if (holder == mSelectedItem) return;

    mSelectedItem.setItemStatus(false);
    mSelectedItem = holder;
    holder.setItemStatus(true);
    if (mOnSelectChangedListener != null) {
      mOnSelectChangedListener.onItemSelected(holder.mPosition, v);
    }
  }

  public static class ViewHolder {
    private Context context;

    private View mRoot;
    private TextView mTitle;
    private int mPosition;

    ViewHolder(final View root, int position, CharSequence title) {
      this.context = root.getContext();
      mRoot = root;
      mTitle = (TextView) root.findViewById(R.id.tv_video_label);
      mTitle.setText(title);
      mPosition = position;
    }

    public void setItemStatus(boolean isSelected) {
      mTitle.setTextColor(isSelected ? Color.WHITE : ContextCompat.getColor(context, R.color.color_4A4E59));
      mRoot.setSelected(isSelected);
    }
  }

  public void setOnSelectChangedListener(OnSelectChangedListener listener) {
    mOnSelectChangedListener = listener;
  }

  public void setSelectPosition(int position) {
    if (mSelectedItem != null &amp;amp;&amp;amp; mSelectedItem.mPosition == position) return;
    if (position &amp;gt;= this.getChildCount()) return;

    View view = this.getChildAt(position);
    if (view != null) {
      mSelectedItem.setItemStatus(false);
      mSelectedItem = (ViewHolder) view.getTag();
      mSelectedItem.setItemStatus(true);
    }
  }

  public void setData(String[] titles) {
    setData(titles, 0);
  }

  public void setData(String[] titles, int initIndex) {
    addItems(titles, initIndex);
  }

  public List&amp;lt;ViewHolder&amp;gt; getViewHolders() {
    return holders != null ? Collections.unmodifiableList(holders)
            : Collections.&amp;lt;ViewHolder&amp;gt;emptyList();
  }

  public interface OnSelectChangedListener {
    void onItemSelected(int position, View view);
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如有缺漏不足之处，欢迎创建issue，PR 😄&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">具有选择效果的flexablelayout</summary></entry><entry><title type="html">《算法》 第四版 读书笔记（3）</title><link href="https://violinday.github.io/2018/05/30/algorithm-4th-chapter-4/" rel="alternate" type="text/html" title="《算法》 第四版 读书笔记（3）" /><published>2018-05-30T00:00:00+08:00</published><updated>2018-05-30T00:00:00+08:00</updated><id>https://violinday.github.io/2018/05/30/algorithm-4th-chapter-4</id><content type="html" xml:base="https://violinday.github.io/2018/05/30/algorithm-4th-chapter-4/">&lt;h2 id=&quot;算法-第四版-第四章-读书笔记-思维导图&quot;&gt;《算法》 第四版 第四章 读书笔记 思维导图&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;最近在整理之前读书做的笔记，很多时候都是自己做完笔记，留一份xmind思维导图就放下了，这样很不好，不如拿出来分享给大家，让大家都能有所收获，在此基础上更进一步。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;还是先上 xmind思维导图，没有安装xmind的可以直接保存下载图片哈：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/5-30-algorithm-4th-chapter-4.png&quot; alt=&quot;第三章 图论&quot; /&gt;&lt;/p&gt;

&lt;p&gt;思维导图见github链接： &lt;a href=&quot;https://github.com/violinday/TechWithMindMap/blob/master/%E7%AE%97%E6%B3%95%20%E7%AC%AC%E5%9B%9B%E7%89%88/%E7%AE%97%E6%B3%95%20%E7%AC%AC%E5%9B%9B%E7%89%88%20%E7%AC%AC%E5%9B%9B%E7%AB%A0%20%E5%9B%BE%E8%AE%BA.xmind&quot;&gt;https://github.com/violinday/TechWithMindMap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如有缺漏不足之处，欢迎创建issue，PR 😄&lt;/p&gt;

&lt;p&gt;前篇&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://violinday.github.io/2018/05/19/algorithm-4th-chapter-2/&quot;&gt;《算法》 第四版 读书笔记（1） 排序&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://violinday.github.io/2018/05/19/algorithm-4th-chapter-3/&quot;&gt;《算法》 第四版 读书笔记（2） 查找&lt;/a&gt;&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">《算法》 第四版 第四章 读书笔记 思维导图</summary></entry><entry><title type="html">《算法》 第四版 读书笔记（2）</title><link href="https://violinday.github.io/2018/05/27/algorithm-4th-chapter-3/" rel="alternate" type="text/html" title="《算法》 第四版 读书笔记（2）" /><published>2018-05-27T00:00:00+08:00</published><updated>2018-05-27T00:00:00+08:00</updated><id>https://violinday.github.io/2018/05/27/algorithm-4th-chapter-3</id><content type="html" xml:base="https://violinday.github.io/2018/05/27/algorithm-4th-chapter-3/">&lt;h2 id=&quot;算法-第四版-第三章-读书笔记-思维导图&quot;&gt;《算法》 第四版 第三章 读书笔记 思维导图&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;最近在整理之前读书做的笔记，很多时候都是自己做完笔记，留一份xmind思维导图就放下了，这样很不好，不如拿出来分享给大家，让大家都能有所收获，在此基础上更进一步。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;还是先上 xmind思维导图，没有安装xmind的可以直接保存下载图片哈：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/5-27-algorithm-4th-chapter-3.png&quot; alt=&quot;第三章 查找&quot; /&gt;&lt;/p&gt;

&lt;p&gt;思维导图见github链接： &lt;a href=&quot;https://github.com/violinday/TechWithMindMap/blob/master/%E7%AE%97%E6%B3%95%20%E7%AC%AC%E5%9B%9B%E7%89%88/%E7%AE%97%E6%B3%95%20%E7%AC%AC%E5%9B%9B%E7%89%88%20%E7%AC%AC%E4%B8%89%E7%AB%A0%20%E6%9F%A5%E6%89%BE.xmind&quot;&gt;https://github.com/violinday/TechWithMindMap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如有缺漏不足之处，欢迎创建issue，PR 😄&lt;/p&gt;

&lt;p&gt;前篇 &lt;a href=&quot;https://violinday.github.io/2018/05/19/algorithm-4th-chapter-2/&quot;&gt;《算法》 第四版 读书笔记（1） 排序&lt;/a&gt;&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">《算法》 第四版 第三章 读书笔记 思维导图</summary></entry><entry><title type="html">《算法》 第四版 读书笔记（1）</title><link href="https://violinday.github.io/2018/05/19/algorithm-4th-chapter-2/" rel="alternate" type="text/html" title="《算法》 第四版 读书笔记（1）" /><published>2018-05-19T00:00:00+08:00</published><updated>2018-05-19T00:00:00+08:00</updated><id>https://violinday.github.io/2018/05/19/algorithm-4th-chapter-2</id><content type="html" xml:base="https://violinday.github.io/2018/05/19/algorithm-4th-chapter-2/">&lt;h2 id=&quot;算法-第四版-第二章-读书笔记-思维导图&quot;&gt;《算法》 第四版 第二章 读书笔记 思维导图&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;最近在整理之前读书做的笔记，很多时候都是自己做完笔记，留一份xmind思维导图就放下了，这样很不好，不如拿出来分享给大家，让大家都能有所收获，在此基础上更进一步。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;首先是第二章 排序 xmind思维导图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/5-19-algorithm-4th-chapter-2.png&quot; alt=&quot;第二章 排序&quot; /&gt;&lt;/p&gt;

&lt;p&gt;思维导图见github链接： &lt;a href=&quot;https://github.com/violinday/TechWithMindMap/blob/master/%E7%AE%97%E6%B3%95%20%E7%AC%AC%E5%9B%9B%E7%89%88/%E7%AE%97%E6%B3%95%20%E7%AC%AC%E5%9B%9B%E7%89%88%20%E7%AC%AC%E4%BA%8C%E7%AB%A0%20%E6%8E%92%E5%BA%8F.xmind&quot;&gt;https://github.com/violinday/TechWithMindMap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如有缺漏不足之处，欢迎创建issue，PR 😄&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">《算法》 第四版 第二章 读书笔记 思维导图</summary></entry><entry><title type="html">Android 自定义进度条效果</title><link href="https://violinday.github.io/2018/05/13/andrid-custom-view/" rel="alternate" type="text/html" title="Android 自定义进度条效果" /><published>2018-05-13T00:00:00+08:00</published><updated>2018-05-13T00:00:00+08:00</updated><id>https://violinday.github.io/2018/05/13/andrid-custom-view</id><content type="html" xml:base="https://violinday.github.io/2018/05/13/andrid-custom-view/">&lt;h2 id=&quot;android-自定义进度条效果&quot;&gt;Android 自定义进度条效果&lt;/h2&gt;

&lt;p&gt;最近再看自定义View的相关代码，
比照着网上的效果做了一个，录屏有卡顿&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/5-13-customview-progress.gif&quot; alt=&quot;效果图&quot; /&gt;&lt;/p&gt;

&lt;p&gt;下面直接放出源码：&lt;/p&gt;

&lt;p&gt;主要分为两部分 重写的onDraw方法、自定义的setProgress(int progress)方法&lt;/p&gt;

&lt;p&gt;下面看onDraw方法，分为三步：画背景，画进度，画文字内容。：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // TODO: consider storing these as member variables to reduce
    // allocations per draw cycle.
    int paddingLeft = getPaddingLeft();
    int paddingTop = getPaddingTop();
    int paddingRight = getPaddingRight();
    int paddingBottom = getPaddingBottom();

    int contentWidth = getWidth() - paddingLeft - paddingRight;
    int contentHeight = getHeight() - paddingTop - paddingBottom;

    // 2.画背景大圆弧
    int centerX = contentWidth / 2;
    int centerY = contentHeight / 2;
    // 设置圆弧画笔的宽度
    mTextPaint.setStrokeWidth(mRoundWidth);
    // 设置为 ROUND
    mTextPaint.setStrokeCap(Paint.Cap.ROUND);
    mTextPaint.setStrokeJoin(Paint.Join.ROUND);
    // 设置画笔颜色
    mTextPaint.setColor(mRoundColor);
    mTextPaint.setStyle(Paint.Style.STROKE);
    // 半径
    int radius = (int) (centerX - mRoundWidth);
    //避免频繁创建对象
    if (mOval == null) {
        mOval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
    }
    // 画背景圆弧
    canvas.drawArc(mOval, 150, 240, false, mTextPaint);

    //画进度
    mTextPaint.setColor(Color.BLUE);
    // 根据当前百分比计算圆弧扫描的角度
    canvas.drawArc(mOval, mStartAngle, progress/100f  * mSweepAngle, false, mTextPaint);

    // 重置画笔
    mTextPaint.reset();
    mTextPaint.setAntiAlias(true);
    mTextPaint.setTextSize(getResources().getDimensionPixelSize(R.dimen.lb_action_text_size));
    mTextPaint.setColor(Color.RED);
    String mStep = progress + &quot;%&quot;;
    // 测量文字的宽高
    mTextPaint.getTextBounds(mStep, 0, mStep.length(), textBounds);
    int dx = (getWidth() - textBounds.width()) / 2;
    // 获取画笔的FontMetrics
    Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
    // 计算文字的基线
    int baseLine = (int) (getHeight() / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom);
    // 绘制步数文字
    canvas.drawText(mStep, dx, baseLine, mTextPaint);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后按照一定的效果（Interceptor插值器实现进度效果）更新progress，invalidate页面，从而重新调用onDraw方法回执界面。实现如下方法：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public void setProgress(int setProgress) {
    objectAnimator = ValueAnimator.ofInt(0, setProgress);
    objectAnimator.setDuration(3000);
    // 不同的拦截器实现不同的展示效果 new BounceInterpolator(); 
    // 可以实现类似速度表盘效果
    objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    objectAnimator.setEvaluator(new IntEvaluator());

    objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            progress = (int)animation.getAnimatedValue();
            invalidate();
        }
    });
    objectAnimator.start();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后，有什么可以优化的地方请多指教 (ﾟ▽ﾟ)/&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">Android 自定义进度条效果</summary></entry><entry><title type="html">Android App（更新时）签名(证书)校验过程源码分析</title><link href="https://violinday.github.io/2018/05/11/andrid-app-update-signature/" rel="alternate" type="text/html" title="Android App（更新时）签名(证书)校验过程源码分析" /><published>2018-05-11T00:00:00+08:00</published><updated>2018-05-11T00:00:00+08:00</updated><id>https://violinday.github.io/2018/05/11/andrid-app-update-signature</id><content type="html" xml:base="https://violinday.github.io/2018/05/11/andrid-app-update-signature/">&lt;h2 id=&quot;android-app更新时签名证书校验过程源码分析&quot;&gt;Android App（更新时）签名(证书)校验过程源码分析&lt;/h2&gt;

&lt;p&gt;还是看到知乎上的一个【&lt;a href=&quot;https://www.zhihu.com/question/266891554/answer/388809643&quot;&gt;问题&lt;/a&gt;】，然后翻了翻Android源代码，给出了一个答案，在这里好好整理一下代码流程。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;对于非系统APP来说，更新包的时候，需要检查更新包和被更新目标是否是相同的签发者所签发，即更新包和被更新包是由相同的签名密钥签发，此时认为更新包和原应用是由相同实体（发布者）所生成，并建立更新包和已经存在应用的信任关系。这个规则叫做【同源策略TOFU Trust On First Use】——摘自《Android安全架构探究》&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;APP在升级的时候，会验证新的APK包的完整性，确保没被修改，验证通过，证书信息最终会保存到PackageParser.Package.mSignatures字段中。然后判断新旧证书是否是同一个，接下来进行升级或者拒绝。&lt;/p&gt;

&lt;p&gt;接下来感兴趣的话看代码吧~
PackageManagerService&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;@Override
public int checkSignatures(String pkg1, String pkg2) {
    synchronized (mPackages) {
        final PackageParser.Package p1 = mPackages.get(pkg1);
        final PackageParser.Package p2 = mPackages.get(pkg2);
        if (p1 == null || p1.mExtras == null
                || p2 == null || p2.mExtras == null) {
            return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
        }
        final int callingUid = Binder.getCallingUid();
        final int callingUserId = UserHandle.getUserId(callingUid);
        final PackageSetting ps1 = (PackageSetting) p1.mExtras;
        final PackageSetting ps2 = (PackageSetting) p2.mExtras;
        if (filterAppAccessLPr(ps1, callingUid, callingUserId)
                || filterAppAccessLPr(ps2, callingUid, callingUserId)) {
            return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
        }
        return compareSignatures(p1.mSignatures, p2.mSignatures);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;
/**
 * Compares two sets of signatures. Returns:
 * &amp;lt;br /&amp;gt;
 * {@link PackageManager#SIGNATURE_NEITHER_SIGNED}: if both signature sets are null,
 * &amp;lt;br /&amp;gt;
 * {@link PackageManager#SIGNATURE_FIRST_NOT_SIGNED}: if the first signature set is null,
 * &amp;lt;br /&amp;gt;
 * {@link PackageManager#SIGNATURE_SECOND_NOT_SIGNED}: if the second signature set is null,
 * &amp;lt;br /&amp;gt;
 * {@link PackageManager#SIGNATURE_MATCH}: if the two signature sets are identical,
 * &amp;lt;br /&amp;gt;
 * {@link PackageManager#SIGNATURE_NO_MATCH}: if the two signature sets differ.
 */
static int compareSignatures(Signature[] s1, Signature[] s2) {
    if (s1 == null) {
        return s2 == null
                ? PackageManager.SIGNATURE_NEITHER_SIGNED
                : PackageManager.SIGNATURE_FIRST_NOT_SIGNED;
    }

    if (s2 == null) {
        return PackageManager.SIGNATURE_SECOND_NOT_SIGNED;
    }

    if (s1.length != s2.length) {
        return PackageManager.SIGNATURE_NO_MATCH;
    }

    // Since both signature sets are of size 1, we can compare without HashSets.
    if (s1.length == 1) {
        return s1[0].equals(s2[0]) ?
                PackageManager.SIGNATURE_MATCH :
                PackageManager.SIGNATURE_NO_MATCH;
    }

    ArraySet&amp;lt;Signature&amp;gt; set1 = new ArraySet&amp;lt;Signature&amp;gt;();
    for (Signature sig : s1) {
        set1.add(sig);
    }
    ArraySet&amp;lt;Signature&amp;gt; set2 = new ArraySet&amp;lt;Signature&amp;gt;();
    for (Signature sig : s2) {
        set2.add(sig);
    }
    // Make sure s2 contains all signatures in s1.
    if (set1.equals(set2)) {
        return PackageManager.SIGNATURE_MATCH;
    }
    return PackageManager.SIGNATURE_NO_MATCH;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;咱们来看看Signature类，看看什么时候认为两个包是相同实体发布的，重点关注equals、hashCode方法：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.content.pm;

import android.os.Parcel;
import android.os.Parcelable;

import com.android.internal.util.ArrayUtils;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;

/**
 * Opaque, immutable representation of a signing certificate associated with an
 * application package.
 * &amp;lt;p&amp;gt;
 * This class name is slightly misleading, since it's not actually a signature.
 */
public class Signature implements Parcelable {
    private final byte[] mSignature;
    private int mHashCode;
    private boolean mHaveHashCode;
    private SoftReference&amp;lt;String&amp;gt; mStringRef;
    private Certificate[] mCertificateChain;

    /**
     * Create Signature from an existing raw byte array.
     */
    public Signature(byte[] signature) {
        mSignature = signature.clone();
        mCertificateChain = null;
    }

    /**
     * Create signature from a certificate chain. Used for backward
     * compatibility.
     *
     * @throws CertificateEncodingException
     * @hide
     */
    public Signature(Certificate[] certificateChain) throws CertificateEncodingException {
        mSignature = certificateChain[0].getEncoded();
        if (certificateChain.length &amp;gt; 1) {
            mCertificateChain = Arrays.copyOfRange(certificateChain, 1, certificateChain.length);
        }
    }

    private static final int parseHexDigit(int nibble) {
        if ('0' &amp;lt;= nibble &amp;amp;&amp;amp; nibble &amp;lt;= '9') {
            return nibble - '0';
        } else if ('a' &amp;lt;= nibble &amp;amp;&amp;amp; nibble &amp;lt;= 'f') {
            return nibble - 'a' + 10;
        } else if ('A' &amp;lt;= nibble &amp;amp;&amp;amp; nibble &amp;lt;= 'F') {
            return nibble - 'A' + 10;
        } else {
            throw new IllegalArgumentException(&quot;Invalid character &quot; + nibble + &quot; in hex string&quot;);
        }
    }

    /**
     * Create Signature from a text representation previously returned by
     * {@link #toChars} or {@link #toCharsString()}. Signatures are expected to
     * be a hex-encoded ASCII string.
     *
     * @param text hex-encoded string representing the signature
     * @throws IllegalArgumentException when signature is odd-length
     */
    public Signature(String text) {
        final byte[] input = text.getBytes();
        final int N = input.length;

        if (N % 2 != 0) {
            throw new IllegalArgumentException(&quot;text size &quot; + N + &quot; is not even&quot;);
        }

        final byte[] sig = new byte[N / 2];
        int sigIndex = 0;

        for (int i = 0; i &amp;lt; N;) {
            final int hi = parseHexDigit(input[i++]);
            final int lo = parseHexDigit(input[i++]);
            sig[sigIndex++] = (byte) ((hi &amp;lt;&amp;lt; 4) | lo);
        }

        mSignature = sig;
    }

	......
    
    /**
     * Returns the public key for this signature.
     *
     * @throws CertificateException when Signature isn't a valid X.509
     *             certificate; shouldn't happen.
     * @hide
     */
    public PublicKey getPublicKey() throws CertificateException {
        final CertificateFactory certFactory = CertificateFactory.getInstance(&quot;X.509&quot;); // DER 编码的X.509格式密钥
        final ByteArrayInputStream bais = new ByteArrayInputStream(mSignature);
        final Certificate cert = certFactory.generateCertificate(bais);
        return cert.getPublicKey();
    }

    ......

    @Override
    public boolean equals(Object obj) {
        try {
            if (obj != null) {
                Signature other = (Signature)obj;
                return this == other || Arrays.equals(mSignature, other.mSignature);
            }
        } catch (ClassCastException e) {
        }
        return false;
    }

    @Override
    public int hashCode() {
        if (mHaveHashCode) {
            return mHashCode;
        }
        mHashCode = Arrays.hashCode(mSignature);
        mHaveHashCode = true;
        return mHashCode;
    }
    ......
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看源代码可知，&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Opaque, immutable representation of a signing certificate associated with an application package.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;它是一个“不透明的、不可变的且与应用程序包关联的签名证书表示类（签名整数对象）”。
即，它是一个与某个APK文件关联的DER编码格式签名证书的封装。&lt;/p&gt;

&lt;p&gt;查看APK安装过程源码，找到证书数据加载的位置，在PackageManagerService.java类中:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;private PackageParser.Package scanPackageInternalLI(PackageParser.Package pkg, File scanFile, int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user) {
	......

	//注意变量是否APP更新
	final boolean isUpdatedPkg = updatedPkg != null; 
    final boolean isUpdatedSystemPkg = isUpdatedPkg &amp;amp;&amp;amp; (policyFlags &amp;amp; PackageParser.PARSE_IS_SYSTEM) != 0;
    
    ......

    //如果APP更新，设置policyFlags
    if (isUpdatedPkg) {
        // An updated system app will not have the PARSE_IS_SYSTEM flag set
        // initially
        policyFlags |= PackageParser.PARSE_IS_SYSTEM;

        // An updated privileged app will not have the PARSE_IS_PRIVILEGED
        // flag set initially
        if ((updatedPkg.pkgPrivateFlags &amp;amp; ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
            policyFlags |= PackageParser.PARSE_IS_PRIVILEGED;
        }
    }

    // Verify certificates against what was last scanned
    collectCertificatesLI(ps, pkg, scanFile, policyFlags);

}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;经过
collectCertificatesLI，collectCertificatesInternal，
层层跳转，进入 PackageParse.java&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;private static void collectCertificates(Package pkg, File apkFile, int parseFlags)
            throws PackageParserException {

    ......

    //省略了V2签名，直接看V1
    StrictJarFile jarFile = null;
    try {
        // Ignore signature stripping protections when verifying APKs from system partition.
        // For those APKs we only care about extracting signer certificates, and don't care
        // about verifying integrity.
        boolean signatureSchemeRollbackProtectionsEnforced =
                (parseFlags &amp;amp; PARSE_IS_SYSTEM_DIR) == 0;
        //初始化 jarFile
        jarFile = new StrictJarFile(
                apkPath,
                !verified, // whether to verify JAR signature
                signatureSchemeRollbackProtectionsEnforced);
        
        ......

        // Verify that entries are signed consistently with the first entry
        // we encountered. Note that for splits, certificates may have
        // already been populated during an earlier parse of a base APK.
        for (ZipEntry entry : toVerify) {
        	//紧接着，调用loadCertificates方法：
            final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
            if (ArrayUtils.isEmpty(entryCerts)) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                        &quot;Package &quot; + apkPath + &quot; has no certificates at entry &quot;
                        + entry.getName());
            }
            final Signature[] entrySignatures = convertToSignatures(entryCerts);

            //仅赋值一次，不为空则判断每次获取到的Certificate是否相同，不同则抛出PackageParserException异常
            if (pkg.mCertificates == null) {
                pkg.mCertificates = entryCerts;
                pkg.mSignatures = entrySignatures;
                pkg.mSigningKeys = new ArraySet&amp;lt;PublicKey&amp;gt;();
                for (int i=0; i &amp;lt; entryCerts.length; i++) {
                    pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
                }
            } else {
                if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
                    throw new PackageParserException(
                            INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, &quot;Package &quot; + apkPath
                                    + &quot; has mismatched certificates at entry &quot;
                                    + entry.getName());
                }
            }
        }
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    } catch (GeneralSecurityException e) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                &quot;Failed to collect certificates from &quot; + apkPath, e);
    } catch (IOException | RuntimeException e) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                &quot;Failed to collect certificates from &quot; + apkPath, e);
    } finally {
        closeQuietly(jarFile);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;快到地方了，不要着急，咱们接着看loadCertificates方法：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
        throws PackageParserException {
    InputStream is = null;
    try {
        // We must read the stream for the JarEntry to retrieve
        // its certificates.
        
        //在getInputStream方法中初始化certificates数据
        is = jarFile.getInputStream(entry);
        
        //在其中调用is[JarFileInputStream类型]的read方法
        //read方法调用verify方法，读取密钥到verifiedEntries中
        readFullyIgnoringContents(is);
        return jarFile.getCertificateChains(entry);
    } catch (IOException | RuntimeException e) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
                &quot;Failed reading &quot; + entry.getName() + &quot; in &quot; + jarFile, e);
    } finally {
        IoUtils.closeQuietly(is);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK,终于看到出处了，StrictJarFile类中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public InputStream getInputStream(ZipEntry ze) {
    final InputStream is = getZipInputStream(ze);
    if (isSigned) {
    	//init 方法，创建VerifierEntry
        StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
        if (entry == null) {
            return is;
        }

        return new JarFileInputStream(is, ze.getSize(), entry);
    }

    return is;
} 

public Certificate[][] getCertificateChains(ZipEntry ze) {
    if (isSigned) {
    	//verifier 从哪里来呢，接下来往下看。
        return verifier.getCertificateChains(ze.getName());
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;verifier类型为StrictJarVerifier.java类查看源代码可知，verifiedEntries的赋值是在其内部类VerifierEntry的verify()方法中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;//存放密钥的变量
private final Hashtable&amp;lt;String, Certificate[][]&amp;gt; verifiedEntries =
            new Hashtable&amp;lt;String, Certificate[][]&amp;gt;();

......

Certificate[][] getCertificateChains(String name) {
    return verifiedEntries.get(name);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;又因为verifiedEntries数据从外部传进来，所以还需要继续找。。。源头，在
 jarFile = new StrictJarFile(apkPath, !verified, signatureSchemeRollbackProtectionsEnforced);
这句代码中，&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;private StrictJarFile(String name,
        FileDescriptor fd,
        boolean verify,
        boolean signatureSchemeRollbackProtectionsEnforced)
                throws IOException, SecurityException {
    this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
    this.fd = fd;

    try {
        // Read the MANIFEST and signature files up front and try to
        // parse them. We never want to accept a JAR File with broken signatures
        // or manifests, so it's best to throw as early as possible.
        if (verify) {
            HashMap&amp;lt;String, byte[]&amp;gt; metaEntries = getMetaEntries();
            this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
            this.verifier =
                    new StrictJarVerifier(
                            name,
                            manifest,
                            metaEntries,
                            signatureSchemeRollbackProtectionsEnforced);
            Set&amp;lt;String&amp;gt; files = manifest.getEntries().keySet();
            for (String file : files) {
                if (findEntry(file) == null) {
                    throw new SecurityException(&quot;File &quot; + file + &quot; in manifest does not exist&quot;);
                }
            }
            //此处readCertificates为最终数据初始化的地方
            isSigned = verifier.readCertificates() &amp;amp;&amp;amp; verifier.isSignedJar();
        } else {
            isSigned = false;
            this.manifest = null;
            this.verifier = null;
        }
    } catch (IOException | SecurityException e) {
        nativeClose(this.nativeHandle);
        IoUtils.closeQuietly(fd);
        closed = true;
        throw e;
    }

    guard.open(&quot;close&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是要到StrictJarVerifier.java类中&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;/**
 * If the associated JAR file is signed, check on the validity of all of the
 * known signatures.
 *
 * @return {@code true} if the associated JAR is signed and an internal
 *         check verifies the validity of the signature(s). {@code false} if
 *         the associated JAR file has no entries at all in its {@code
 *         META-INF} directory. This situation is indicative of an invalid
 *         JAR file.
 *         &amp;lt;p&amp;gt;
 *         Will also return {@code true} if the JAR file is &amp;lt;i&amp;gt;not&amp;lt;/i&amp;gt;
 *         signed.
 * @throws SecurityException
 *             if the JAR file is signed and it is determined that a
 *             signature block file contains an invalid signature for the
 *             corresponding signature file.
 */
synchronized boolean readCertificates() {
    if (metaEntries.isEmpty()) {
        return false;
    }

    Iterator&amp;lt;String&amp;gt; it = metaEntries.keySet().iterator();
    while (it.hasNext()) {
        String key = it.next();
        //只解析这三种类型的密钥
        if (key.endsWith(&quot;.DSA&quot;) || key.endsWith(&quot;.RSA&quot;) || key.endsWith(&quot;.EC&quot;)) {
            verifyCertificate(key);
            it.remove();
        }
    }
    return true;
}


/**
 * @param certFile
 */
private void verifyCertificate(String certFile) {
    ...... //省略了使用证书（如RSA这行书）中的 .SF属性块对整个MF文件验证的过程

    //验证通过，signature在StrictJarVerifier类中的initEntry方法中变成二维数组，
    //放进StrictJarVerifier 的verifiedEntries变量中
    signatures.put(signatureFile, entries);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;至此分析结束&lt;/p&gt;

&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.2cto.com/kf/201609/551023.html&quot;&gt;Android App签名(证书)校验过程源码分析&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.520monkey.com/archives/571&quot;&gt;Android签名机制之—签名过程详解&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.520monkey.com/archives/573&quot;&gt;Android签名机制之—签名验证过程详解&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bbs.kafan.cn/thread-1792048-1-1.html&quot;&gt;Android证书验证存漏洞 开发者身份信息可被篡改&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Violinday</name></author><summary type="html">Android App（更新时）签名(证书)校验过程源码分析</summary></entry><entry><title type="html">Android中，在子线程使用Toast会报错？</title><link href="https://violinday.github.io/2018/05/10/andrid-Toast-dont-show-error/" rel="alternate" type="text/html" title="Android中，在子线程使用Toast会报错？" /><published>2018-05-10T00:00:00+08:00</published><updated>2018-05-10T00:00:00+08:00</updated><id>https://violinday.github.io/2018/05/10/andrid-Toast-dont-show-error</id><content type="html" xml:base="https://violinday.github.io/2018/05/10/andrid-Toast-dont-show-error/">&lt;h2 id=&quot;android中在子线程使用toast会报错&quot;&gt;Android中，在子线程使用Toast会报错？&lt;/h2&gt;

&lt;hr /&gt;
&lt;p&gt;在知乎看到一个问题：
https://www.zhihu.com/question/51099935/answer/387828477&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;在子线程中使用Toast抛出异常，提示错误显示:Can’t create handler inside thread that has not called Looper.prepare()&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;@森羴 的回答：https://www.zhihu.com/question/51099935/answer/125487934&lt;/p&gt;

&lt;p&gt;Toast,Handler,分析为什么抛异常。ActivityThread和ViewRootImpl分析到底什么叫子线程不能更新UI。&lt;/p&gt;

&lt;p&gt;Toast本质上是一个window，跟activity是平级的，checkThread只是Activity维护的View树的行为。Toast使用的无所谓是不是主线程Handler，吐司操作的是window，不属于checkThread抛主线程不能更新UI异常的管理范畴。它用Handler只是为了用队列和时间控制排队显示吐司。即使是子线程，先Looper.prepare,再show吐司，再Looper.loop一样可以吐出来，只不过loop操作会阻塞这个线程，没人这么玩罢了，都是让Toast用主线程的Handler，这个是在ActivityThread里初始化的，本来就是阻塞处理所有的UI交互逻辑。贴个代码吧还是～～&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;new Thread(){
        public void run(){
          Looper.prepare();//给当前线程初始化Looper
          Toast.makeText(getApplicationContext(),&quot;你猜我能不能弹出来～～&quot;,0).show();
          //Toast初始化的时候会new Handler();无参构造默认获取当前线程的Looper，如果没有prepare过，则抛出题主描述的异常。上一句代码初始化过了，就不会出错。
          Looper.loop();
          //这句执行，Toast排队show所依赖的Handler发出的消息就有人处理了，Toast就可以吐出来了。但是，这个Thread也阻塞这里了，因为loop()是个for (;;) ...
        }
  }.start();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我的补充,和 @森羴回答结合起来看效果更好😄：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);//①注意，这里looper为空
}
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);//② looper为null

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        mTN = new TN(context.getPackageName(), looper);//③创建TN
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt; mTN = new TN(context.getPackageName(), looper);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-Java&quot;&gt;TN(String packageName, @Nullable Looper looper) {
    ......
    if (looper == null) {
        // Use Looper.myLooper() if looper is not specified.
        //④从当前线程获取looper，一般为主线程，故在主线程中时，让Toast用主线程的Looper创建一个Handler
        looper = Looper.myLooper(); 
        if (looper == null) {
            throw new RuntimeException(
                    &quot;Can't toast on a thread that has not called Looper.prepare()&quot;);
        }
    }
    mHandler = new Handler(looper, null) {
    }
    ......
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看源码可以很清楚地分析出问题原因，多看源码对上层APP开发有很大益处 ; )&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">Android中，在子线程使用Toast会报错？</summary></entry><entry><title type="html">android.intent.category.DEFAULT的用途和使用</title><link href="https://violinday.github.io/2018/04/30/android-category/" rel="alternate" type="text/html" title="android.intent.category.DEFAULT的用途和使用" /><published>2018-04-30T00:00:00+08:00</published><updated>2018-04-30T00:00:00+08:00</updated><id>https://violinday.github.io/2018/04/30/android-category</id><content type="html" xml:base="https://violinday.github.io/2018/04/30/android-category/">&lt;h2 id=&quot;androidintentcategorydefault&quot;&gt;android.intent.category.DEFAULT&lt;/h2&gt;

&lt;hr /&gt;
&lt;h3 id=&quot;两个概念&quot;&gt;两个概念&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;explicit(明确，显式) intent&lt;/p&gt;

    &lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;nc&quot;&gt;Intent&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;intent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Intent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MainActivity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Secondary&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;；&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;implicit(隐藏,隐式) intent&lt;/p&gt;

    &lt;p&gt;Implicit Intent没有明确的指定要启动哪个Activity ，而是通过设置一些Intent Filter来让系统去筛选合适的Acitivity去启动。&lt;/p&gt;

    &lt;p&gt;在官方文档中有这么一段话：&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;Android treats all implicit intents passed to startActivity() as if they contained at least one category: “android.intent.category.DEFAULT” (the CATEGORY_DEFAULT constant). Therefore, activities that are willing to receive implicit intents must include “android.intent.category.DEFAULT” in their intent filters&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;意思是说，每一个通过 startActivity() 方法发出的隐式 Intent 都至少有一个 category，就是 “android.intent.category.DEFAULT”，所以只要是想接收一个隐式 Intent 的 Activity 都应该包括”android.intent.category.DEFAULT” category，不然将导致 Intent 匹配失败。&lt;/p&gt;

&lt;p&gt;即：如果要启动的Activity的&lt;intent-filter&gt;&lt;/intent-filter&gt;在AndroidManifest.xml文件中没有android.intent.category.DEFAULT这个category的话，就会匹配失败。&lt;/p&gt;

&lt;p&gt;从上面的论述还可以获得以下信息：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;一个 Intent 可以有多个 category，但至少会有一个，也是默认的一个 category。&lt;/li&gt;
  &lt;li&gt;只有 Intent 的所有 category 都匹配上，Activity 才会接收这个 Intent。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;Android 源代码 【正在寻找自动添加的地方…】：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;寻找ing...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;引用： &lt;a href=&quot;https://blog.csdn.net/jason0539/article/details/10049899&quot;&gt;https://blog.csdn.net/jason0539/article/details/10049899&lt;/a&gt;&lt;/p&gt;</content><author><name>Violinday</name></author><summary type="html">android.intent.category.DEFAULT</summary></entry></feed>