<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Abbott</title>
    <description>分享技术，分享生活</description>
    <link>http://vno.onevcat.com/</link>
    <atom:link href="http://vno.onevcat.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 07 May 2018 10:55:10 +0000</pubDate>
    <lastBuildDate>Mon, 07 May 2018 10:55:10 +0000</lastBuildDate>
    <generator>Jekyll v3.7.3</generator>
    
      <item>
        <title>small踩坑记录</title>
        <description>&lt;p&gt;前段时间，调研了插件化的一些相关方案。感觉small的文档比较清晰，而且做到极致裁剪。源码也能够直接看到，被称为是最精简的插件化方案。
故决定将公司以前的一个组件化的应用进行插件化改造。&lt;/p&gt;

&lt;p&gt;虽然demo运行起来很简单，但是在实践的过程中，还是非常曲折。最后在不懈的努力下，终于完成了插件化的集成。下面把集成过程中遇到的一些坑记录
下来，仅供大家参考。&lt;/p&gt;

&lt;p&gt;small踩坑记。。&lt;/p&gt;

&lt;p&gt;1、app插件模块的 package 必须和applicationId一致，否则会出错。 报r文件找不到。但是app模块可以单独运行成功。&lt;/p&gt;

&lt;p&gt;2、application的理解上存在问题。其实插件在加载的过程中，每个插件如果有application都会加载一次。&lt;/p&gt;

&lt;p&gt;某个插件都有相应的application的生命周期和调用。所以插件中应用中，没个插件都有一个application的实例的。&lt;/p&gt;

&lt;p&gt;3、app+sub&lt;/p&gt;

&lt;p&gt;在RootPlugin中可以看到，所有app和lib模块都有依赖app+stub。但是在打包时候，app+stub会打包到宿主中。这就是所谓的分身。
这里面一般会放一些权限，受限 Activity,必须在宿主占坑的资源.(例如转场动画，通知栏图标，自定义视图，桌面快捷方式图标)&lt;/p&gt;

&lt;p&gt;问题列表
a.虽然app+stub在编译的时候，给host lib 以及app提供了依赖，但是打包的时候，stub中menifest的权限却没有merge进来。原因还没有找到？？？？&lt;/p&gt;

&lt;p&gt;4、DexException: Cannot merge new index 65536 into a non-jumbo instruction&lt;/p&gt;

&lt;p&gt;https://stackoverflow.com/questions/26093664/dexexception-cannot-merge-new-index-65536-into-a-non-jumbo-instruction
http://www.flysnow.org/2016/09/18/android-dex-jumbo-mode.html&lt;/p&gt;

&lt;p&gt;5、在app依赖lib时。如果两个工程的manifest都有设置application@name 则在编译的时候，merge会报错。可以通过设置 tools:replace=”name”来消除错误。small
是在hookProcessManifest的里面做了这件事情。&lt;/p&gt;

&lt;p&gt;另外经过测试theme也是如此 而label,supportsRtl,allowBackup则不需要&lt;/p&gt;

&lt;p&gt;6、用到了字体，例如DinTextView在初始化的时候，会加载某个字体RobotoThin。如果这个字段存在覆盖的话，会导致找不到的问题产生。&lt;/p&gt;

&lt;p&gt;7、如果使用small中配置buildToAssets = true 表示将插件打包成apk（主要是方便解析和查看），这个时候需要在application的oncreate中指定Small.setLoadFromAssets(BuildConfig.LOAD_FROM_ASSETS); 来显示的赋值。否则会找不到apk的路径&lt;/p&gt;

&lt;p&gt;8、在bundle.json中配置的时候，如果pkg不符合相应的规范 例如 app. lib. 则需要显示地指定type.否则这个apk不能加载起来&lt;/p&gt;

&lt;p&gt;9、After a long analysis, i got solution,platformBuildVersionCode is targetSDKVersion and platformBuildVersionName is version name of targetSDK(like Android 6.0) which are mentioned in build.gradle.&lt;/p&gt;

&lt;p&gt;ps:small用到platformBuildVersion这个字段，通过在gradle编译的时候写入，是否该插件包存在资源（这么做，是因为M平台在加入无资源的apk时会crash）&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                // The flag bits are:
                //  F    F    F    F    F    F    F    F
                // 1111 1111 1111 1111 1111 1111 1111 1111
                // ^^^^ ^^^^ ^^^^ ^^^^ ^^^^
                //       ABI Flags (20)
                //                          ^
                //                 nonResources Flag (1)
                //                           ^^^ ^^^^ ^^^^
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;table&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;int newFlag = (flags « 11)&lt;/td&gt;
      &lt;td&gt;versionCode&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                //                     platformBuildVersionCode (11) =&amp;gt; MAX=0x7FF=4095 最后发现，统一so，并不做filter后成功。。 排查方法，直接解压so，看看lib下是否有对应的so。
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;10、插件单独运行起来ok，但是跳转的时候，会发现ou need to use a Theme.AppCompat theme (or descendant) with this activity.
解决方法，直接将theme独立成一个lib.style工程。其它工程都的theme定义都去掉。直接依赖此方程即可。。&lt;/p&gt;

&lt;p&gt;11、由于以前组件化的时候，是使用的Arouter的方案来加载路由。但是插件化后，只能加载一个模块的路由。这是由路由框架确定的。在application的中调用Arouter.init()后，加载完成后，会打上标志，导致其它插件的路由无法加载成功。&lt;/p&gt;

&lt;p&gt;这是需要重写路由加载的部分逻辑…&lt;/p&gt;

&lt;p&gt;大概思路说明：
a、在attachBaseContext方法中通过反射拿到loadApk里面的dexFile。然后遍历并且注册到warehouse中，&lt;/p&gt;

&lt;p&gt;这里遇到的问题是：插件 java.lang.IllegalAccessError  原因说明：因为不同的dex中都有加载warehouse这个类。如果dexA引用dexB中的类，就会出现这个问题。所以在编译的时候，考虑用provided的方式。只提供一个依赖即可。&lt;/p&gt;

&lt;p&gt;参考资料：http://www.jianshu.com/p/57fc356b9093&lt;/p&gt;

&lt;p&gt;ps:发现路由加载好后，可以直接使用arouter的路由即可，不需要做任何调整。&lt;/p&gt;

&lt;p&gt;12、app host工程不能依赖lib工程，否则会报错。 错误忘记贴出来了。&lt;/p&gt;

&lt;p&gt;13、BundleParser: Package com.topstech.saas has mismatched certificates at entry AndroidManifest.xml; ignoring!&lt;/p&gt;

&lt;p&gt;修改签名文件后，bundleParser解压会失败。所以所有的插件需要重新打包。。&lt;/p&gt;

&lt;p&gt;ps:所有的插件都是用的一套签名认证。&lt;/p&gt;

&lt;p&gt;14、百度地图在客户插件模块初始化失败。&lt;/p&gt;

&lt;p&gt;在插件初始化的时候，拿到的pkg和sh1貌似都不对。如何拿到主应用的pkg和sh1（签名文件的）。 原因是不同application初始化的时候，对应的上下文实际是不一样的。这个要切记了。&lt;/p&gt;

&lt;p&gt;15、签名问题：各组件编译时使用宿主的release签名，以使得release版本的宿主可以通过比对签名来校验插件。为此在调试运行宿主的时候也使用release签名，避免调试时签名校验失败而中断。
所以针对14中提到的问题，理论上不应该签名不对的。&lt;/p&gt;

&lt;p&gt;解决方法。直接缓存host的context然后在插件初始化的时候，传入即可。  即通过 getApplicationContext()能到宿主的context&lt;/p&gt;

&lt;p&gt;关于application关系  参考issuse:https://github.com/wequick/Small/issues/288&lt;/p&gt;

&lt;p&gt;16、aar的引用不支持 mavenLocal 也不支持compile(name: ‘rnfetchblob-0.10.6’, ext: ‘aar’)  导致rn模块的aar加载异常。。&lt;/p&gt;

&lt;p&gt;ref:https://github.com/wequick/Small/issues/211
解决方法：File-&amp;gt;New-&amp;gt;New Module-&amp;gt;Import .JAR/.AAR Package&lt;/p&gt;

&lt;p&gt;17、由于rn模块使用了一个本地仓库依赖，本地依赖解析了自己的依赖。直接通过16的方法，会导致依赖的依赖无法拿到而报错。—-因为打包出来的aar不会包含本aar的相关依赖信息。所以只能进行远程依赖。或者下载好所有的依赖aar&lt;/p&gt;

&lt;p&gt;http://blog.csdn.net/u011840744/article/details/50608677&lt;/p&gt;

&lt;p&gt;在jcenter上面找到相应的依赖  com.facebook.react:react-native:0.20.1  版本太低。而我们用的是0.45&lt;/p&gt;

&lt;p&gt;解决思路：把aar和pom文件上传到公司的服务器。&lt;/p&gt;

&lt;p&gt;ps:一定要区分好 buildscript  和 allprojects 的 repositories 需要单独指定&lt;/p&gt;

&lt;p&gt;18、rn模块报   package R does not exist.   同样原因，因为pkg 和 applicationId不一致导致的。&lt;/p&gt;

&lt;p&gt;19、 java.lang.UnsatisfiedLinkError: couldn’t find DSO to load: libreactnativejni.so 经排查，在对应文件夹下 /data/data/com.topstech.saas/files/storage/com.kakao.topsales.rnmodule/lib/x86确实存在这个so文件。。但是却加载不成功&lt;/p&gt;

&lt;p&gt;原因分析：SoSource指定了so的加载目录，导致so加载不成功。&lt;/p&gt;

&lt;p&gt;解决方法：通过反射SoSource的加载路径，即可正常加载so了&lt;/p&gt;

&lt;p&gt;20、java.lang.NoClassDefFoundError: Failed resolution of: Lokhttp3/JavaNetCookieJar&lt;/p&gt;

&lt;p&gt;在rxlib添加 com.squareup.okhttp3:okhttp-urlconnection:3.4.1 依赖即可。原因未知。。有可能是跟RNFetchBlob里面的版本冲突导致。由于RNFetchBlob的源码没有。暂时做此猜测&lt;/p&gt;

&lt;p&gt;21、在真机上测试，发现找不到so。并且so路径也没有加入到目录。&lt;/p&gt;

&lt;p&gt;原因是small会根据我手机的架构找对应的so。而我的手机是arm64-8v.并不会做所谓的降级处理。加上这个对应的构架即可了&lt;/p&gt;

&lt;p&gt;22、经常遇到编译不通过的原因。。
问题终于找到，还是下载依赖时候的网络问题。因为默认使用了代理，会导致依赖内网的aar下载失败。。因为现在我们的应用依赖了不少内网的aar。切记了，很惨的教训。&lt;/p&gt;

&lt;p&gt;23、如果有两个工程都在执行gradle指令。如果一个阻塞，会导致别个一个也处于阻塞状态 。。切记了。&lt;/p&gt;

&lt;p&gt;24、java.lang.UnsatisfiedLinkError: dlopen failed: “/data/data/com.topstech.saas/files/storage/com.rxlib.rxlib/lib/armeabi-v7a/libjnimain.so” is 32-bit instead of 64-bit&lt;/p&gt;

&lt;p&gt;原因是因为：64位的android机上，会有32位的虚拟机和64位的虚拟机，在启动apk的时候，虚拟机会根据apk中的so的位数启动对应的虚拟机。解决方法是在宿主中加入一个32位的so即可。&lt;/p&gt;

&lt;p&gt;参考、；https://github.com/singwhatiwanna/dynamic-load-apk/issues/113&lt;/p&gt;

&lt;p&gt;25、个人模块下rn的本地图片无法显示。。
原因：是因为我们工程是通过require来引用图片，还这种引用是最后通过assets目录下去查找的。
解决方法：把rn下res目录的资源文件放到宿主中即可。&lt;/p&gt;

&lt;p&gt;参考文章：http://www.jianshu.com/p/0866a1e0dff4&lt;/p&gt;

&lt;p&gt;26、集成极光后，会出现退出主页后再进入，报错
explicit activity class {com.kakao.topsales.customer/com.kakao.topsales.activity.HomeConsultantActivity}; have you declared this activity in your AndroidManifest.xml?&lt;/p&gt;

&lt;p&gt;发现pkg com.kakao.topsales.customer的指向明显不正确。初步怀疑路由加载不正确。&lt;/p&gt;

&lt;p&gt;重新整理路由启动的逻辑，解决问题。&lt;/p&gt;

&lt;p&gt;27、Execution failed for task ‘:app.rn:transformClassesWithMultidexlistForRelease’.&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;java.io.IOException: The output jar is empty. Did you specify the proper ‘-keep’ options?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;莫名其妙的问题:可能是编译不稳定导致的。。。执行clean等操作后正常&lt;/p&gt;

&lt;p&gt;总结：遇到编译不稳定的报错，建议clean后再重新执行即可。&lt;/p&gt;

&lt;p&gt;28、插件全局更新后，第一次启动的时候，crash.&lt;/p&gt;

&lt;p&gt;java.lang.RuntimeException: Unable to instantiate receiver cn.jpush.android.service.PushReceiver: java.lang.ClassNotFoundException: Didn’t find class “cn.jpush.android.service.PushReceiver” on path: DexPathList[[zip file “/data/app/com.topstech.saas-UvfzIh10TH2Yr-fNTnGb6g==/base.apk”],nativeLibraryDirectories=[/data/app/com.topstech.saas-UvfzIh10TH2Yr-fNTnGb6g==/lib/x86, /system/fake-libs, /data/app/com.topstech.saas-UvfzIh10TH2Yr-fNTnGb6g==/base.apk!/lib/x86, /system/lib, /system/vendor/lib]]&lt;/p&gt;

&lt;p&gt;原因是，首次安装完后，启动时,apk对应的资源没有加入到classloader.&lt;/p&gt;

&lt;p&gt;或者杀掉进程后（模拟器会自动重启），再启动，也会报和上面一样的错误。&lt;/p&gt;

&lt;p&gt;原因同样是，应用在没有加载插件时，就提前启动了service。导致找不到class.&lt;/p&gt;

&lt;p&gt;最后解决方案是=》把极光相关的数据放入到app+的分身中，极将其打包到base.apk中。这样系统启动的时候，极光相关的资源已经加载到classloader。问题得到解决。&lt;/p&gt;

&lt;p&gt;29、打包出来的apk无法通过as自带的解析工具解析 resource.arsc   (暂时不是问题，不需要解决。)
PackageChunk contains an unexpected chunk: class com.google.devrel.gmscore.tools.apk.arsc.LibraryChunk&lt;/p&gt;

&lt;p&gt;30、有个奇怪的问题，在8.0的模拟器上下载完成跳转到安装界面（应用进入后台会会处于dead状态，这里会突然重启），此时再点击取消返回后，会crash.&lt;/p&gt;

&lt;p&gt;原因中在升级新包的设置，默认设置了进行更新，打上标志后。在应用进入后台的时候，会kill自己导致。去掉此标志即可。&lt;/p&gt;

&lt;p&gt;31、在lib包中引用的aar存在activtiy的显示调用的时候，会出现因为manifest.xml中没有存在此声明，导致无法调用。即stub的占桩也没有起作用。&lt;/p&gt;

&lt;p&gt;原因：在对应的lib的manifest.xml文件中添加对应的声明即可。&lt;/p&gt;

&lt;p&gt;32、发现调用photoselect后界面卡死。&lt;/p&gt;

&lt;p&gt;原因：在lib包中不能使用anim的转场动画设置。。
解决方法：将此依赖引用添加到stub中即可解决问题。&lt;/p&gt;

&lt;p&gt;33、从客户详情到客户跟进crash.
原因：是升级了arouter后，序列化方法变化。导致序列化一直为空。
解决：实现序列化方法即可。。—-本质是因为升级arouter后导致的问题。&lt;/p&gt;

&lt;p&gt;34、android N对就的系统，限制了系统么有库的调用。导致出现如下问题。&lt;/p&gt;

&lt;p&gt;java.lang.UnsatisfiedLinkError: dlopen failed: library “/system/lib/libimagepipeline.so” needed or dlopened by “/system/lib/libnativeloader.so” is not accessible for the namespace “classloader-namespace”&lt;/p&gt;

&lt;p&gt;从Android N开始，本地库只能引用公共的NDK API，系统将阻止应用动态连接非NDK平台的so库。Android 7.0 行为变更 中指出如果您的应用使用动态链接到私有平台 API 的第三方库，您可能也会看到上述错误 logcat 输出。&lt;/p&gt;

&lt;p&gt;考虑到rn的实用性和兼容性，以及导致包过大的问题。因此考虑将rn换回原生的内容。&lt;/p&gt;

&lt;p&gt;35、lib模块在删除资源后，需要清理public.txt文件进行重新生成。&lt;/p&gt;

&lt;p&gt;public.txt保留当前文件的资源引用。便于在宿主打包的时候，剥离这些资源文件。减少体积。&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
</description>
        <pubDate>Tue, 02 Jan 2018 05:32:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2018/01/small%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2018/01/small%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/</guid>
        
        <category>small-插件化-踩坑记录</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>Android SDK目录及版本号区别</title>
        <description>&lt;h1 id=&quot;sdk目录&quot;&gt;SDK目录&lt;/h1&gt;

&lt;h2 id=&quot;add-ons&quot;&gt;add-ons&lt;/h2&gt;
&lt;p&gt;这里面保存着附加库，第三方公司为android 平台开发的附加功能系统。比如GoogleMaps，当然你如果安装了OphoneSDK，这里也会有一些类库在里面。&lt;/p&gt;

&lt;h2 id=&quot;docs&quot;&gt;docs&lt;/h2&gt;
&lt;p&gt;这里面是Android SDKAPI参考文档，所有的API都可以在这里查到。&lt;/p&gt;

&lt;h2 id=&quot;extras&quot;&gt;extras&lt;/h2&gt;

&lt;p&gt;该文件夹下存放了==Android support v4，v7，v13，v17包==；
还有google提供额USB驱动、Intel提供的硬件加速等附加工具包，
和market_licensing作为AndroidMarket版权保护组件，一般发布付费应用到电子市场可以用它来反盗版。&lt;/p&gt;

&lt;h2 id=&quot;platforms&quot;&gt;platforms&lt;/h2&gt;

&lt;p&gt;是每个平台的SDK真正的文件，存放了不同版本的android系统。里面会根据APILevel划分的SDK版本，这里就以Android2.2来说，进入后有 一个android-8的文件夹，android-8进入后是Android2.2SDK的主要文件，其中ant为ant编译脚本，data保存着一些系 统资源，images是模拟器映像文件，skins则是Android模拟器的皮肤，templates是工程创建的默认模板，&lt;strong&gt;android.jar则 是该版本的主要framework文件&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;samples&quot;&gt;samples&lt;/h2&gt;
&lt;p&gt;是Android SDK自带的默认示例工程，里面的apidemos强烈推荐初学者运行学 习，对于SQLite数据库操作可以查看NotePad这个例子，对于游戏开发Snake、LunarLander都是不错的例子，对于Android主 题开发Home则是androidm5时代的主题设计原理。&lt;/p&gt;

&lt;h1 id=&quot;下面重点介绍这3个&quot;&gt;下面重点介绍这3个！！！！&lt;/h1&gt;
&lt;h2 id=&quot;platform-tools&quot;&gt;platform-tools&lt;/h2&gt;
&lt;p&gt;保存着一些Android平台相关通用工具，比如adb、和aapt、aidl、dx等文件，这里和platforms目录中tools文件夹有些重复，主要是从android2.3开始这些工具被划分为通用了。Fastboot 刷机工具。&lt;/p&gt;

&lt;h2 id=&quot;tools&quot;&gt;tools&lt;/h2&gt;
&lt;p&gt;作为SDK根目录下的tools文件夹，这里包含了android &lt;strong&gt;开发和调试的工具&lt;/strong&gt;，比如ddms用于启动Android调试工具，比如logcat、屏幕截图和文件管理器，而draw9patch则是绘制android平台的可缩放png图片的工具，sqlite3可以在PC上操作SQLite数据库， 而monkeyrunner则是一个不错的压力测试应用，模拟用户随机按键，mksdcard则是模拟器SD映像的创建工具，emulator是 Android SDK模拟器主程序，不过从android 1.5开始，需要输入合适的参数才能启动模拟器，traceview作为android平台上重要的调试工具。&lt;/p&gt;

&lt;h2 id=&quot;build-tools&quot;&gt;build-tools&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;保存着一些Android平台相关通用工具，比如adb、和aapt、aidl、dx等文件。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;aapt&lt;/strong&gt;即Android Asset Packaging Tool , 在SDK的build-tools目录下. 该工具可以查看, 创建, 更新ZIP格式的文档附件(zip, jar, apk). 也可将资源文件编译成二进制文件.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adb&lt;/strong&gt; 即android debug bridge 管理模拟器和真机的万能工具，ddms 调试环境&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AIDL&lt;/strong&gt; 即 Android Interface definition language
它是一种android内部进程通信接口的描述语言，通过它我们可以定义进程间的通信接口
Emulator即android 的模拟器&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dx&lt;/strong&gt;：转化.class中间代码为dvlik中间代码,所有经过java编译的生成.class文件都需要此工具进行转换,最后打包进apk文件中.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dexdump&lt;/strong&gt; 即Android Emulator中可以找到一个名为dexdump的程序，通过dexdump可以查看出apk文件中的dex执行情况，粗略分析出原始java代码是什 么样的和Dot Net中的Reflector很像。&lt;/p&gt;

&lt;p&gt;注意：这里会涉及到一个问题，就是build-tools后边会有不同的api版本号！
①buildeToolVersion是你构建工具的版本，这个版本号一般是API-LEVEL.0.0。 例如I/O2014大会上发布了API20对应的build-tool的版本就是20.0.0，在这之间可能有小版本，例如20.0.1等等。&lt;/p&gt;

&lt;p&gt;②在ecplise的project.properties中可以设置sdk.buildtools=20.0.0。也可以不设置，不设置的话就是指定最新版本。而在android studio中是必须在build.gradle中设置。&lt;/p&gt;

&lt;p&gt;③Android都是向下兼容的，你可以用高版本的&lt;strong&gt;build-tool去构建一个低版本的sdk工程&lt;/strong&gt;，例如build-tool的版本为20，去构建一个sdk版本为18的工程！&lt;/p&gt;

&lt;h3 id=&quot;mincompiletarget版本的区别&quot;&gt;min、compile、target版本的区别&lt;/h3&gt;
&lt;p&gt;compileSdkVersion, minSdkVersion 和 targetSdkVersion 的作用：他们分别控制可以使用哪些 API ，要求的 API 级别是什么，以及应用的兼容模式。&lt;/p&gt;

&lt;h3 id=&quot;compilesdkversion&quot;&gt;compileSdkVersion&lt;/h3&gt;

&lt;p&gt;compileSdkVersion 告诉 Gradle 用哪个 Android SDK 版本编译你的应用。使用任何新添加的 API 就需要使用对应等级的 Android SDK。&lt;/p&gt;

&lt;p&gt;==需要强调的是修改 compileSdkVersion 不会改变运行时的行为==。当你修改了 compileSdkVersion 的时候，可能会出现新的编译警告、编译错误，但新的 compileSdkVersion 不会被包含到 APK 中：&lt;strong&gt;它纯粹只是在编译的时候使用&lt;/strong&gt;。（你真的应该修复这些警告，他们的出现一定是有原因的！）&lt;/p&gt;

&lt;p&gt;因此我们强烈推荐你总是使用最新的 SDK 进行编译。在现有代码上使用新的编译检查可以获得很多好处，避免新弃用的 API ，并且为使用新的 API 做好准备。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;，如果使用 Support Library ，那么使用最新发布的 Support Library 就需要使用最新的 SDK 编译。例如，要使用 23.1.1 版本的 Support Library，compileSdkVersion 就必需至少是 23 （大版本号要一致！）。通常，新版的 Support Library 随着新的系统版本而发布，它为系统新增加的 API 和新特性提供兼容性支持。&lt;/p&gt;

&lt;h3 id=&quot;minsdkversion&quot;&gt;minSdkVersion&lt;/h3&gt;
&lt;p&gt;如果 compileSdkVersion 设置为可用的最新 API，那么 minSdkVersion 则是应用可以运行的最低要求。minSdkVersion 是 &lt;strong&gt;Google Play 商店用来判断用户设备是否可以安装某个应用的标志之一（还有应用和版本号）&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;在开发时 minSdkVersion 也起到一个重要角色：lint 默认会在项目中运行，它在你使用了高于 minSdkVersion 的 API 时会警告你，帮你避免调用不存在的 API 的运行时问题。如果只在较高版本的系统上才使用某些 API，通常使用“运行时检查系统版本”的方式解决。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;请记住&lt;/strong&gt;，你所使用的库，如 Support Library 或 Google Play services，可能有他们自己的 minSdkVersion 。你的应用设置的 minSdkVersion 必须大于等于这些库的 minSdkVersion 。例如有三个库，它们的 minSdkVersion 分别是 4, 7 和 9 ，那么你的 minSdkVersion 必需至少是 9 才能使用它们。在少数情况下，你仍然想用一个比你应用的 minSdkVersion 还高的库（处理所有的边缘情况，确保它只在较新的平台上使用），你可以使用 tools:overrideLibrary 标记，但请做彻底的测试！&lt;/p&gt;

&lt;h3 id=&quot;targetsdkversion&quot;&gt;targetSdkVersion&lt;/h3&gt;

&lt;p&gt;三个版本号中最有趣的就是 targetSdkVersion 了。 targetSdkVersion 是 Android 提供向前兼容的主要依据，&lt;strong&gt;在应用的 targetSdkVersion 没有更新之前系统不会应用最新的行为变化&lt;/strong&gt;。这允许你在适应新的行为变化之前就可以使用新的 API （因为你已经更新了 compileSdkVersion 不是吗？）。
targetSdkVersion 所暗示的许多行为变化都记录在 VERSION_CODES 文档中了，但是所有恐怖的细节也都列在每次发布的平台亮点中了，在这个 API Level 表中可以方便地找到相应的链接。&lt;/p&gt;

&lt;p&gt;如果描述不太清楚，&lt;a href=&quot;http://www.race604.com/android-targetsdkversion/&quot;&gt;查考文章&lt;/a&gt;即可明白了。&lt;/p&gt;

&lt;h3 id=&quot;综合来看&quot;&gt;综合来看&lt;/h3&gt;

&lt;p&gt;如果你按照上面示例那样配置，你会发现这三个值的关系是：&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;minSdkVersion &amp;lt;= targetSdkVersion &amp;lt;= compileSdkVersion
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这种直觉是合理的，如果 compileSdkVersion 是你的最大值，minSdkVersion 是最小值，那么最大值必需至少和最小值一样大且 target 必需在二者之间。
理想上，在稳定状态下三者的关系应该更像这样：&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;minSdkVersion (lowest possible) &amp;lt;= targetSdkVersion == compileSdkVersion (latest SDK)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;用较低的 minSdkVersion 来覆盖最大的人群，用最新的 SDK 设置 target 和 compile 来获得最好的外观和行为。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;buildtoolsversion-vs-compilesdkversion&quot;&gt;buildtoolsVersion vs compileSdkVersion&lt;/h3&gt;

&lt;p&gt;compileSdkVersion is the API version of Android that you compile against.&lt;/p&gt;

&lt;p&gt;buildToolsVersion is the version of the compilers (&lt;strong&gt;aapt, dx, renderscript&lt;/strong&gt; compiler, etc…) that you want to use. For each API level (starting with 18), &lt;strong&gt;there is a matching .0.0 version&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;简而言之，buildToolsVersion是负责把指定用哪一组编译程序来将你所编写的 Source Code 转成操作系统可以载入的形式，也就是产出 Apk。这并不会影响到应用程序的运行。不同的buildToolsVersion版本，原则上不会有太大的差异，大概就是&lt;strong&gt;编译的效能、产出的文件大小等等的细节&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;但有时候还是可能会牵涉到 Apk 内容或结构的问题影响运行，仍然要多注意改版的资讯。最保险的做法还是让 &lt;strong&gt;buildToolsVersion 的==主版本编号==与 compileSdkVersion 同步&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;当然可用使用高版本的buildToolsVersion了与compileSdkVersion使用，但是没啥必要。&lt;/p&gt;
</description>
        <pubDate>Tue, 02 Jan 2018 05:32:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2018/01/Android-SDK%E7%9B%AE%E5%BD%95%E5%8F%8A%E7%89%88%E6%9C%AC%E5%8F%B7%E5%8C%BA%E5%88%AB/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2018/01/Android-SDK%E7%9B%AE%E5%BD%95%E5%8F%8A%E7%89%88%E6%9C%AC%E5%8F%B7%E5%8C%BA%E5%88%AB/</guid>
        
        <category>sdk-android-工具介绍</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>ShadowSocks全局上网</title>
        <description>&lt;p&gt;如果用过ShadowSocks的朋友，可能会慢慢意识到，ShadowSocks开启后，在浏览器上好使，但是我们在使用terminal终端的时候，发现一些wget、curl、git、brew等命令行工具都会变得很慢。。 这是为什么呢，可能大家像我一样困惑，那么下面我们来简单介绍下，出现这种问题的原因，以及该如何去做呢？&lt;/p&gt;

&lt;p&gt;首先我们先了解一些ShadowSocks的基本工作原理。用一张图简单来表示下。
&lt;img src=&quot;http://upload-images.jianshu.io/upload_images/2159256-bd93ae4bb4bdac61.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;ShadowSocks的基本工作原理&quot; /&gt;&lt;/p&gt;

&lt;p&gt;简单来说，ShadowSocks分为客户端和服务端，用户发出的请求基于Socks5协议与ShadowSocks客户端进行通信，一般情况下SS客户端都在本机，通过ShadowSocksX、GoAgentX等应用启动，所以这一步是不会经过GFW的，然后ShadowSocks提供了多种加密方式供客户端和服务端之间进行通信，并且在经过GFW时是普通的TCP协议数据包，没有明显的特征，而且GFW也无法解密分析，从而实现绕墙访问资源。&lt;/p&gt;

&lt;p&gt;由于是基于socket5协议，我也就导致了使用ShadowSocks代理的局限性。即只有支持socket协议的软件才能正常翻墙使用。而我们在使用terminal终端进行网络连接的时候，都是基于http协议，我就导致了我们不能正常的代理上网。&lt;/p&gt;

&lt;p&gt;可能大家会疑问，浏览器本身不也是使用http协议上网的，为什么就能通过ShadowSocks进行翻墙上网呢，这是因为chrome浏览器本身是支持从http转换为socket协议的。（并非所有的浏览器都支持socket协议的）&lt;/p&gt;

&lt;p&gt;ps：为什么vpn就可以直接全局上网，这是因为vpn本身是经过一道加密的通道进行上网，所以开启vpn后就是全局的走vpn通道了。&lt;/p&gt;

&lt;p&gt;所以为了使不支持socket协议的应用也能够使用ShadowSocks代理上网，所以我们需要使用工具进行一层转换。其中最经典的就是使用proxifier了。关于proxifier的工作原理，我们也用一个图做一个简单介绍&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://upload-images.jianshu.io/upload_images/2159256-124cd6c75eaea4df.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;proxifier代理转换&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Proxifier的客户端&lt;a href=&quot;https://www.proxifier.com/download.htm&quot;&gt;下载地址&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;操作步骤如下&lt;/p&gt;
&lt;h3 id=&quot;一添加socket代理&quot;&gt;一、添加socket代理&lt;/h3&gt;

&lt;p&gt;选择proxies–&amp;gt;add&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://upload-images.jianshu.io/upload_images/2159256-ed81b4b10e481d66.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;二设置rulues&quot;&gt;二、设置rulues&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;http://upload-images.jianshu.io/upload_images/2159256-2732c6e6e1e66d05.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在defalut对应的栏目中，设置action为我们设置的代理即可。这样，表示默认所有请求都走这个代理。&lt;/p&gt;

&lt;p&gt;其它配置，请大家根据需要自己配置即可。&lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
</description>
        <pubDate>Sat, 02 Dec 2017 05:32:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/12/ShadowSocks%E5%85%A8%E5%B1%80%E4%B8%8A%E7%BD%91/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/12/ShadowSocks%E5%85%A8%E5%B1%80%E4%B8%8A%E7%BD%91/</guid>
        
        <category>翻墙-科学上网-终端上网</category>
        
        
        <category>network</category>
        
      </item>
    
      <item>
        <title>多图合并框架实现</title>
        <description>&lt;p&gt;现在多数app里面加入聊天已经是一个非常普遍的现象了，而微信和qq则是通讯领域的鼻祖了。如果产品经理在考虑做聊天设计的时候，多数会参考。&lt;/p&gt;

&lt;p&gt;常常你会听到，你看微信和qq都是这么做的，你就这么来吧，虽然心理有一万个不痛快，但谁叫我们是有一个有追求的程序员呢。&lt;/p&gt;

&lt;p&gt;所以产品的要求是实现类似微信的群头像。类似如下&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://upload-images.jianshu.io/upload_images/2159256-139f238c47917a08.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;多图合并&quot; /&gt;&lt;/p&gt;

&lt;p&gt;作为程序员，首先会评估下工作量吧。在产品眼里，就是把图片合成一起嘛，有啥难度吗？所以工作时间决定了你能做成什么样吧&lt;/p&gt;

&lt;h3 id=&quot;方案分析&quot;&gt;方案分析：&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;方案1&lt;/strong&gt;、直接写成布局，然后按照不同的布局加载不同张数的图片。而大家通用的图片加载方案都是异步加载的，这样的话，加载的时候，会一闪一闪的合并成一张图。由于现在的图片框架都有缓存，第二次会好很多。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;优点：实现起来快&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;缺点：很low，不是一个有逼格程序员的做法，而且效果也不好。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;方案2&lt;/strong&gt;、自定义一个控件，还是通过异步的方式下载所有图片。在控件里面加一个计数器，确保所有图片下载完成后，一起同步显示出来。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;优点：难度适中&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;缺点：扩展性差，哪天产品想换一个合成方案呢&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;方案3&lt;/strong&gt;、还是使用原生的控件，对群图像进行合并后生成一个新的图像，原后进行缓存。将合并算法抽象成接口。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;优点：易扩展，体验更好&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;缺点：多花一些时间&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当然啦，作为一个有梦想有逼格的程序员，我们应该考虑实现方案3，并且造福一些被产品折磨的程序猿同胞。&lt;/p&gt;

&lt;p&gt;接下来，我来说一下主要思路和关键性代码吧。其实整体上的思路说起来也比较简单，可以用一幅流程图来概括。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://upload-images.jianshu.io/upload_images/2159256-4dda788d264dfb68.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;合并图加载逻辑&quot; /&gt;&lt;/p&gt;

&lt;p&gt;首先，我们知道，程序的输入参数应该是一个ImageView控件，一个urls列表。当然还有一个合并回调函数，用于自定义合并方法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public void displayImages(
    final List&amp;lt;String&amp;gt; urls,
    final ImageView imageView, 
    final MergeCallBack mergeCallBack
)

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

&lt;p&gt;按照思路，我们需要根据urls生成一个新key,用于缓存合并后的图像，下次就可以直接从缓存中加载。毕竟合并头像是耗时操作&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public String getNewUrlByList(List&amp;lt;String&amp;gt; urls, String mark) {
        StringBuilder sb = new StringBuilder();
        for (String url : urls) {
            sb.append(url + mark);
        }

        return sb.toString();
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里只是一个简单对所有的url进行了一个拼接，然后再md5.&lt;/p&gt;

&lt;p&gt;缓存处理才是最关键的步骤，这里涉及到单个链接图片的缓存和合并图的缓存。对于缓存系统来说，单张图和多张图是同样对待的，都是一个key对应一个缓存对象。只是key的规则稍有不同。&lt;/p&gt;

&lt;p&gt;而缓存方案也是通用的DiskLruCache和MemoryLruCache实现的二级缓存，这样可以保持缓存的高效。（关于Lru算法，就是简单的Least Recently Used,即最近使用原则，具体不清楚请百度 ）&lt;/p&gt;

&lt;p&gt;我们来看下displayImages的核心代码，就是先找内存缓存，然后再找磁盘缓存，如果都没有，则再同步的找到所有的单张图片&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public void displayImages(final List&amp;lt;String&amp;gt; urls, final ImageView imageView, final MergeCallBack mergeCallBack, final int dstWidth, final int dstHeight) {
        if (urls == null || urls.size() &amp;lt;= 0) {
            throw new IllegalArgumentException(&quot;url不能为空&quot;);
        }

        if (mergeCallBack == null) {
            throw new IllegalArgumentException(&quot;mergeCallBack 不能为空&quot;);
        }
        final String url = getNewUrlByList(urls, mergeCallBack.getMark());

        imageView.setTag(IMG_URL, url);
        //内存中加载
        Bitmap bitmap = loadFromMemory(url);
        if (bitmap != null) {
            LogUtil.e(Tag, &quot;displayImages this is from Memory&quot;);
            imageView.setImageBitmap(bitmap);
            return;
        }

        try {
            //磁盘中加载
            bitmap = loadFromDiskCache(url, dstWidth, dstHeight);
            if (bitmap != null) {
                LogUtil.e(Tag, &quot;displayImages this is from Disk&quot;);
                imageView.setImageBitmap(bitmap);
                return;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        //设置一张默认图
        bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher_round);
        imageView.setImageBitmap(bitmap);
        LogUtil.e(Tag, &quot;displayImages this is from default&quot;);
        //开启一个新的线程，同步加载所有的图片。如果加载成功，则返回。
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                ArrayList&amp;lt;Bitmap&amp;gt; bitmaps = loadBitMaps(urls, dstWidth, dstHeight);
                if (bitmaps != null &amp;amp;&amp;amp; bitmaps.size() &amp;gt; 0) {
                    Result result;
                    if (mergeCallBack != null) {
                        Bitmap mergeBitmap = mergeCallBack.merge(bitmaps, mContext, imageView);
                        if (urls.size() == bitmaps.size()) {
                            //加入缓存
                            try {
                                saveDru(url, mergeBitmap);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        } else {
                            LogUtil.e(Tag, &quot;size change. so can not save&quot;);
                        }
                        LogUtil.e(Tag, &quot;displayImages this is from Merge&quot;);
                        result = new Result(mergeBitmap, url, imageView);
                    } else {
                        result = new Result(bitmaps.get(0), url, imageView);
                    }
                    Message msg = mMainHandler.obtainMessage(MESSAGE_SEND_RESULT, result);
                    msg.sendToTarget();
                }
            }
        };

        threadPoolExecutor.execute(loadBitmapTask);
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如果从缓存中加载失败，我们会开启一个线程，去执行头像合并的操作。那头像合并是同步操作，需要得到需要合并头像的对象，那如何得到呢，我们继续看代码&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    private ArrayList&amp;lt;Bitmap&amp;gt; loadBitMaps(List&amp;lt;String&amp;gt; urls, int dstWidth, int dstHeight) {
        ArrayList&amp;lt;Bitmap&amp;gt; bitmaps = new ArrayList&amp;lt;&amp;gt;();
        for (String url : urls) {
            //同步获得所有图像
            Bitmap bitmap = loadBitMap(url, dstWidth, dstHeight);
            if (bitmap != null) {
                bitmaps.add(bitmap);
            }
        }
        return bitmaps;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;显示，图像是通过loadBitMap()函数返回，而这个函数的核心方法是&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    private Bitmap loadBitMap(String url, int dstWidth, int dstHeight) {
        //内存
        Bitmap bitmap = loadFromMemory(url);
        if (bitmap != null) {
            LogUtil.e(Tag, &quot;this is from Memory&quot;);
            return bitmap;
        }

        try {
            //磁盘
            bitmap = loadFromDiskCache(url, dstWidth, dstHeight);
            if (bitmap != null) {
                LogUtil.e(Tag, &quot;this is from Disk&quot;);
                return bitmap;
            }
            //网络
            bitmap = loadFromNet(url, dstWidth, dstHeight);
            LogUtil.e(Tag, &quot;this is from Net&quot;);
            if (bitmap == null) {
                LogUtil.e(Tag, &quot;bitmap null network error&quot;);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return bitmap;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;可以清楚的看到，又返回了displayImages()方法的逻辑中，套用了同样的缓存思路。我们再回到loadBitmapTask这个线程的执行方法中，其中有一段重要的逻辑是&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Bitmap mergeBitmap = mergeCallBack.merge(bitmaps, mContext, imageView);
if (urls.size() == bitmaps.size()) {
     //加入缓存
     try {
          saveDru(url, mergeBitmap);
     } catch (IOException e) {
          e.printStackTrace();
    }
} 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这个mergeCallBack方法是用户需要自己实现的图像合并方法，传入一个列表的bitmap，然后返回一个合并图对象，最后我们把这个合并再加入缓存。下次就能直接从缓存中找到了。&lt;/p&gt;

&lt;p&gt;接下来的重点就是图像合并的技术了。我在代码里面加入实现了微信和qq的群头像，接下来就简单讲下微信合并的方案，QQ的合并方案，大家可以自己去看代码。&lt;/p&gt;

&lt;p&gt;首先我们看下MergeCallBack的实现方法&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
    @Override
    public Bitmap merge(List&amp;lt;Bitmap&amp;gt; bitmapArray, Context context, ImageView imageView) {
        this.context = context;

        // 画布的宽
        ViewGroup.LayoutParams lp = imageView.getLayoutParams();
        int tempWidth;
        int tempHeight;
        if (lp != null) {
            tempWidth = dip2px(context, lp.width);
            tempHeight = dip2px(context, lp.height);
        } else {
            //否则给一个默认的高度
            tempWidth = dip2px(context, 70);
            tempHeight = dip2px(context, 70);
        }


        return CombineBitmapTools.combimeBitmap(context, tempWidth, tempHeight,
                bitmapArray);
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;再看看combimeBitmap的实现&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static Bitmap combimeBitmap(Context context, int combineWidth,
                                       int combineHeight, List&amp;lt;Bitmap&amp;gt; bitmaps) {
        if (bitmaps == null || bitmaps.size() == 0)
            return null;

        if (bitmaps.size() &amp;gt;= 9) {
            bitmaps = bitmaps.subList(0, 9);
        }


        Bitmap resultBitmap = null;
        int len = bitmaps.size();
        // 绘制数据，这里记录所有的绘制坐标。
        List&amp;lt;CombineBitmapEntity&amp;gt; combineBitmapEntities = CombineNineRect
                .generateCombineBitmapEntity(combineWidth, combineHeight, len);
        // 缩略图
        List&amp;lt;Bitmap&amp;gt; thumbnailBitmaps = new ArrayList&amp;lt;Bitmap&amp;gt;();
        for (int i = 0; i &amp;lt; len; i++) {
            thumbnailBitmaps.add(ThumbnailUtils.extractThumbnail(bitmaps.get(i),
                    (int) combineBitmapEntities.get(i).width,
                    (int) combineBitmapEntities.get(i).height));
        }
        // 合成
        resultBitmap = getCombineBitmaps(combineBitmapEntities,
                thumbnailBitmaps, combineWidth, combineHeight);

        return resultBitmap;
    }


    private static Bitmap getCombineBitmaps(
            List&amp;lt;CombineBitmapEntity&amp;gt; mEntityList, List&amp;lt;Bitmap&amp;gt; bitmaps,
            int width, int height) {
        Bitmap newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        for (int i = 0; i &amp;lt; mEntityList.size(); i++) {
            //合并图像
            newBitmap = mixtureBitmap(newBitmap, bitmaps.get(i), new PointF(
                    mEntityList.get(i).x, mEntityList.get(i).y));
        }
        return newBitmap;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;最后调用getCombineBitmaps合成图像，合成图像的关键就是通过Bitmap.createBitmap实现。&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    private static Bitmap mixtureBitmap(Bitmap first, Bitmap second,
                                        PointF fromPoint) {
        if (first == null || second == null || fromPoint == null) {
            return null;
        }
        Bitmap newBitmap = Bitmap.createBitmap(first.getWidth(),
                first.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas cv = new Canvas(newBitmap);
        cv.drawBitmap(first, 0, 0, null);
        cv.drawBitmap(second, fromPoint.x, fromPoint.y, null);
        cv.save(Canvas.ALL_SAVE_FLAG);
        cv.restore();

        if (first != null) {
            first.recycle();
            first = null;
        }
        if (second != null) {
            second.recycle();
            second = null;
        }

        return newBitmap;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;所有关键逻辑已经备注到代码里面了。&lt;/p&gt;

&lt;p&gt;如果大家想看完整效果和完整代码，可以点击我的git地址&lt;a href=&quot;https://github.com/jinyb09017/MutiImgLoader&quot;&gt;MutiImgLoader&lt;/a&gt;。如果大家觉得有帮助，记得star哦&lt;/p&gt;
</description>
        <pubDate>Tue, 19 Sep 2017 03:32:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/09/%E5%A4%9A%E5%9B%BE%E5%90%88%E5%B9%B6%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0(android)/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/09/%E5%A4%9A%E5%9B%BE%E5%90%88%E5%B9%B6%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0(android)/</guid>
        
        <category>框架|图片加载</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>android线程消息机制之Handler详情</title>
        <description>&lt;p&gt;android线程消息机制主要由Handler,Looper,Message和MessageQuene四个部分组成。平常在开发中，我们常用来在子线程中通知主线程来更新，其实整个安卓生命周期的驱动都是通过Handler(ActivityThread.H)来实现的。&lt;/p&gt;

&lt;p&gt;首先我们先介绍这四个类的作用：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handler&lt;/strong&gt;：消息的发送者。负责将Message消息发送到MessageQueue中。以及通过Runnable,Callback或者handleMessage()来实现消息的回调处理&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Looper&lt;/strong&gt;：是消息的循环处理器，它负责从MessageQueue中取出Message对象进行处理。(Looper含有MessageQueue的引用)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message&lt;/strong&gt;：是消息载体，通过target来指向handler的引用。通过object来包含业务逻辑数据。其中MessagePool为消息池，用于回收空闲的Message对象的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MessageQueue&lt;/strong&gt;：消息队列，负责维护待处理的消息对象。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://img.blog.csdn.net/20150801014511416&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;p&gt;通过上面的图，我们可以比较清楚地知道他们的作用以及关系。接下来，我们从源码角度来分析这种关系是如何建立的。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;hander的其它构造方法可以自己去查看，通过这个构造方法，我们知道，handler持有MessageQueue的引用。所以可以方便地将Message加入到队列中去。&lt;/p&gt;

&lt;p&gt;通过源码我们发现，sendMessage-&amp;gt;sendMessageDelayed-&amp;gt;sendMessageAtTime-&amp;gt;enqueueMessage&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + &quot; sendMessageAtTime() called with no mQueue&quot;);
        Log.w(&quot;Looper&quot;, e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;都是通过enqueueMessage将message将加入到MessageQueue中。&lt;/p&gt;

&lt;p&gt;接下来，我们看Message是如何构造的。通过Message的构造方法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看到，Message是通过obtain的静态方法从消息池sPool中拿到的。这样可以做到消息的复用。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public static Message obtain(Handler h) {
    Message m = obtain();
    m.target = h;

    return m;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;其中有一个重载方法中m.target = h;这段代码非常重要，便于后面找到消息的目标handler进行处理。&lt;/p&gt;

&lt;p&gt;接下来，我们来看Looper。我们知道Looper通过过Looper.loop来进入循环的，而循环是通过线程的run方法的驱动的。&lt;/p&gt;

&lt;p&gt;首先我们知道，我们在创建Handler的时候，都没有去创建Looper，那么Looper哪里来的呢？&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public Handler(Callback callback, boolean async) {
        ...
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                &quot;Can't create handler inside thread that has not called Looper.prepare()&quot;);
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;再看看Looper.myLooper()&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;ThreadLocal是线程创建线程局部变量的类。表示此变量只属于当前线程。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings(&quot;unchecked&quot;)
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看到了sThreadLocal.get()的方法实际是取当前线程中的Looper对象。&lt;/p&gt;

&lt;p&gt;那么我们主线程的Looper到底在哪里创建的呢？
而我们清楚地知道，如果在子线程中创建handler调用，则需要使用Looper.prepare方法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException(&quot;Only one Looper may be created per thread&quot;);
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看到此方法中，如果此线程中没有Looper对象，则创建一个Looper对象。接下来我们在源码中看到一个熟悉的方法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException(&quot;The main Looper has already been prepared.&quot;);
            }
            sMainLooper = myLooper();
        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;此方法单独的创建了一个sMainLooper用于主线程的Looper。这个prepareMainLooper到底在哪里调用呢？&lt;/p&gt;

&lt;p&gt;高过引用指向发现，我们在ActivityThread.main()方法中发现&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static void main(String[] args) {
        ...
        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, &quot;ActivityThread&quot;));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException(&quot;Main thread loop unexpectedly exited&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;而ActivityThread.main()是程序的入口方法。这样我们就非常清楚了，主线程的Looper在程序的启动过程中就已经创建并循环。&lt;/p&gt;

&lt;p&gt;那么如果在子线程中创建Looper该如何正确调用呢？&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;class LooperThread extends Thread {
      public Handler mHandler;

      public void run() {
          Looper.prepare();

          mHandler = new Handler() {
              public void handleMessage(Message msg) {
                  // process incoming messages here
              }
          };

          Looper.loop();
      }
  }

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

&lt;p&gt;接下来，我们需要看下Looper.loop()的执行方法&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public static void loop() {
        final Looper me = myLooper();//拿到当前线程的looper
        if (me == null) {
            throw new RuntimeException(&quot;No Looper; Looper.prepare() wasn't called on this thread.&quot;);
        }
        final MessageQueue queue = me.mQueue;//拿到当前looper的消息队列

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {//死循环遍历消息体。如果为null，则休眠。
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(&quot;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Dispatching to &quot; + msg.target + &quot; &quot; +
                        msg.callback + &quot;: &quot; + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 &amp;amp;&amp;amp; Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);//此处是真正的分发消息。此处的target即是handler对象
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs &amp;gt; 0) {
                final long time = end - start;
                if (time &amp;gt; slowDispatchThresholdMs) {
                    Slog.w(TAG, &quot;Dispatch took &quot; + time + &quot;ms on &quot;
                            + Thread.currentThread().getName() + &quot;, h=&quot; +
                            msg.target + &quot; cb=&quot; + msg.callback + &quot; msg=&quot; + msg.what);
                }
            }

            if (logging != null) {
                logging.println(&quot;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; Finished to &quot; + msg.target + &quot; &quot; + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, &quot;Thread identity changed from 0x&quot;
                        + Long.toHexString(ident) + &quot; to 0x&quot;
                        + Long.toHexString(newIdent) + &quot; while dispatching to &quot;
                        + msg.target.getClass().getName() + &quot; &quot;
                        + msg.callback + &quot; what=&quot; + msg.what);
            }

            msg.recycleUnchecked();
        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;最后我们看下dispatchMessage的处理方法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看到，dispatchMessage是优化处理msg.callback,然后就是实现的Callback接口，最后才是handleMessage方法。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;重点说明：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;1、handler在实例化的时候，持有Looper的引用。是通过ThreadLocal&lt;Looper&gt;与Handler进行关联的。&lt;/Looper&gt;&lt;/p&gt;

&lt;p&gt;2、Message在实例化的过程中，通过target 持有Handler的引用。&lt;/p&gt;

&lt;p&gt;3、通常一个线程对应一个Looper.一个Looper可以属于多个Handler.&lt;/p&gt;

</description>
        <pubDate>Thu, 10 Aug 2017 02:42:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/08/android%E7%BA%BF%E7%A8%8B%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6%E4%B9%8BHandler%E8%AF%A6%E6%83%85/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/08/android%E7%BA%BF%E7%A8%8B%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6%E4%B9%8BHandler%E8%AF%A6%E6%83%85/</guid>
        
        <category>源码分析|源码|handler|Looper|消息机制</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>AsyncTask源码解析</title>
        <description>&lt;p&gt;AsyncTask,是android提供的轻量级的异步类。本质上还是基于Thread和消息机制（handler）的封装。&lt;/p&gt;

&lt;p&gt;首先我们先看一下，通常AsyncTask的用法。首先，AsyncTask是一个抽象类，需要实现doInBackground方法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;private class MyTask extends AsyncTask&amp;lt;String, Integer, String&amp;gt; {
    //onPreExecute方法用于在执行后台任务前做一些UI操作
    @Override
    protected void onPreExecute() {
        Log.i(TAG, &quot;onPreExecute&quot;);
        textView.setText(&quot;loading...&quot;);
    }

    //doInBackground方法内部执行后台任务,不可在此方法内修改UI（在单独的线程里面处理任务）
    @Override
    protected String doInBackground(String... params) {
        Log.i(TAG, &quot;doInBackground&quot;);
        ...
        return &quot;FAIL&quot;;
    }

    //onProgressUpdate方法用于更新进度信息(此方法通过在doInBackground内调用publishProgress触发。)
    @Override
    protected void onProgressUpdate(Integer... progresses) {
        Log.i(TAG, &quot;onProgressUpdate&quot;);
        progressBar.setProgress(progresses[0]);
        textView.setText(&quot;loading...&quot; + progresses[0] + &quot;%&quot;);
    }

    //onPostExecute方法用于在执行完后台任务后更新UI,显示结果
    @Override
    protected void onPostExecute(String result) {
        Log.i(TAG, &quot;onPostExecute&quot;);
        textView.setText(result);

        execute.setEnabled(true);
        cancel.setEnabled(false);
    }

    //onCancelled方法用于在取消执行中的任务时更改UI
    @Override
    protected void onCancelled() {
        Log.i(TAG, &quot;onCancelled&quot;);
        textView.setText(&quot;cancelled&quot;);
        progressBar.setProgress(0);

        execute.setEnabled(true);
        cancel.setEnabled(false);
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;创建并执行AsyncTask的接口如下：&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mTask = new MyTask();
mTask.execute(&quot;http://www.baidu.com&quot;);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;而取消任务&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mTask.cancel();
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;从源码角度解读，首先我们从new MyTask()构架方法看起&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public AsyncTask() {
        //合建一个实现Callable接口的任务。
        mWorker = new WorkerRunnable&amp;lt;Params, Result&amp;gt;() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //noinspection unchecked
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    postResult(result);
                }
                return result;
            }
        };
        //传入mWorker参数创建一个Future对象，主要用于了解线程运行的执行情况。
        mFuture = new FutureTask&amp;lt;Result&amp;gt;(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException(&quot;An error occurred while executing doInBackground()&quot;,
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;从构造方法中，我们可以看出，首先我们创建一个继承于Callable的任务。此任务通常在线程池中执行。然后通过传入线程任务mWorker创建一个future,主要用于查询线程执行以及获得线程的返回值。&lt;/p&gt;

&lt;p&gt;再来看AsyncTask的执行方法excute();&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public final AsyncTask&amp;lt;Params, Progress, Result&amp;gt; execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;实质上调用executeOnExecutor（）方法&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public final AsyncTask&amp;lt;Params, Progress, Result&amp;gt; executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException(&quot;Cannot execute task:&quot;
                            + &quot; the task is already running.&quot;);
                case FINISHED:
                    throw new IllegalStateException(&quot;Cannot execute task:&quot;
                            + &quot; the task has already been executed &quot;
                            + &quot;(a task can be executed only once)&quot;);
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看到，参数Executor表示一个执行器。Params…表示传入动态参数，那默认的sDefaultExecutor这个执行器又是什么什么呢？&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

private static class SerialExecutor implements Executor {
        final ArrayDeque&amp;lt;Runnable&amp;gt; mTasks = new ArrayDeque&amp;lt;Runnable&amp;gt;();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们发现这个执行器只是对runnable的一个包装，只是将任务包装，然后将任务交给THREAD_POOL_EXECUTOR执行。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static final Executor THREAD_POOL_EXECUTOR;

    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;而这个THREAD_POOL_EXECUTOR是一个线程池。负责任务真正的执行。&lt;/p&gt;

&lt;p&gt;我们再回到executeOnExecutor方法中，因为这个方法是主线程中执行的，所以onPreExecute()方法也在主线程中执行，然后再调用exec.execute(mFuture);在线程中执行mFuture任务。&lt;/p&gt;

&lt;p&gt;而前面我们知道，mFuture任务的执行体是mWorker(WorkerRunnable)的call方法。我们再回到call()方法中，发现result = doInBackground(mParams)方法，所以线程中耗时任务是在doInBackground中处理的。耗时任务执行完后，会调用postResult(result)方法。&lt;/p&gt;

&lt;p&gt;我们再来看postResult(result)方法&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    private Result postResult(Result result) {
        @SuppressWarnings(&quot;unchecked&quot;)
        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult&amp;lt;Result&amp;gt;(this, result));
        message.sendToTarget();
        return result;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;很显示，是通过handler机制把消息发送到主线程中，我们看看handler的处理对象。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({&quot;unchecked&quot;, &quot;RawUseOfParameterizedType&quot;})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult&amp;lt;?&amp;gt; result = (AsyncTaskResult&amp;lt;?&amp;gt;) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接收到消息后，执行了result.mTask.finish(result.mData[0]);方法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;最后如果消息取消，则回调onCancelled，否则回调onPostExecute。此两个方法都在ui线程。&lt;/p&gt;

&lt;p&gt;大家可以会疑惑，onProgressUpdate是在哪里执行的呢？众所周知，这个方法是负责更新任务进度的。而任务进度只有在doInBackground方法中可以得知，而doInBackground是在后台线程中，故肯定需要通过Handler通知，所以AsyncTask封装了publishProgress(progress…)来通过标志
MESSAGE_POST_PROGRESS回调onProgressUpdate方法。&lt;/p&gt;

&lt;p&gt;重点说明：&lt;/p&gt;

&lt;p&gt;1、executeOnExecutor()首先会对任务的状态进行处理。任务共三种姿态&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;PENDING: 挂起状态。当AsyncTask被创建时，就进入了PENDING状态。&lt;/li&gt;
  &lt;li&gt;RUNNING: 运行状态。当AsyncTask被执行时，就进入了RUNNING状态。&lt;/li&gt;
  &lt;li&gt;FINISHED: 完成状态。当AsyncTask完成(被客户cancel()或正常运行完毕)时，就进入了FINISHED状态。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当任务是RUNNING或PENDING状态时，会抛出异常。这就决定了，一个AsyncTask只能被执行一次，即只能对一个AsyncTask调用一次execute()；如果要重新执行任务，则需要新建AsyncTask后再调用execute()。&lt;/p&gt;

&lt;p&gt;SerialExecutor是一个顺序执行器，那么这个执行器到底是如何做到的，我们再贴一下代码。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;private static class SerialExecutor implements Executor {
    final ArrayDeque&amp;lt;Runnable&amp;gt; mTasks = new ArrayDeque&amp;lt;Runnable&amp;gt;();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;可以看到，SerialExecutor是使用ArrayDeque这个队列来管理Runnable对象的，如果我们一次性启动了很多个任务，首先在第一次运行execute()方法的时候，会调用ArrayDeque的offer()方法将传入的Runnable对象添加到队列的尾部，然后判断mActive对象是不是等于null，第一次运行当然是等于null了，于是会调用scheduleNext()方法。在这个方法中会从队列的头部取值，并赋值给mActive对象，然后调用THREAD_POOL_EXECUTOR去执行取出的取出的Runnable对象。之后如何又有新的任务被执行，同样还会调用offer()方法将传入的Runnable添加到队列的尾部，但是再去给mActive对象做非空检查的时候就会发现mActive对象已经不再是null了，于是就不会再调用scheduleNext()方法。&lt;/p&gt;

&lt;p&gt;那么后面添加的任务岂不是永远得不到处理了？当然不是，看一看offer()方法里传入的Runnable匿名类，这里使用了一个try finally代码块，并在finally中调用了scheduleNext()方法，保证无论发生什么情况，这个方法都会被调用。也就是说，每次当一个任务执行完毕后，下一个任务才会得到执行，SerialExecutor模仿的是单一线程池的效果，如果我们快速地启动了很多任务，同一时刻只会有一个线程正在执行，其余的均处于等待状态。&lt;/p&gt;

&lt;p&gt;也许你不知道，在android3.0之前并没有这个SerialExecutor类。个时候是直接在AsyncTask中构建了一个sExecutor常量，并对线程池总大小，同一时刻能够运行的线程数做了规定，代码如下所示：&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 10;
……
private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
        MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;表示同一个时刻，可以运行的线程数据为5个，最后可以同时存在128个线程。所以在3.0之前的版本，如果5个任务执行的时候，可以同时执行，而在3.0之后的版本中，5个任务是串行执行的。&lt;/p&gt;

&lt;p&gt;当然在新版本中，我们也可以直接调用executeOnExecutor方法，传入指定的执行器（或者线程也）即可。&lt;/p&gt;

&lt;p&gt;例如起用调用task.executeOnExecutor(THREAD_POOL_EXECUTOR,params)即可。&lt;/p&gt;

</description>
        <pubDate>Mon, 07 Aug 2017 03:42:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/08/AsyncTask%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/08/AsyncTask%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/</guid>
        
        <category>源码分析|源码</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>Thread、Handler和HandlerThread关系何在</title>
        <description>&lt;p&gt;HandlerThread看名字，确实比较奇怪。到底是handler还是thread.其实看过源码后，就会非常清楚。&lt;/p&gt;

&lt;p&gt;HandlerThread 继承自thread。所以本质上是一个线程，内部有Looper和Handler引用。它和AsyncTask非常像，都是google为了方便开发者，封装的工具类。HandlerThread可以让你不用维护Looper来实现线程的消息通知机制。&lt;/p&gt;

&lt;p&gt;这个类非常简单，我们用下源码并可以得知。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    ...

}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;可以清楚的看到，它继承自Thread.所以是一个线程。同时持有Looper和handler的引用。mPriority用于设置线程的优先级。&lt;/p&gt;

&lt;p&gt;我们再看下线程的run方法&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看到，通过Looper.prepare();的静态方法调用 基于ThreadLocal最终后创建一个线程独有Looper。&lt;/p&gt;

&lt;p&gt;同步代码块保证looper创建成功后，唤醒阻塞的队列。最后通过Looper.loop();来开启looper的循环机制。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }

        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() &amp;amp;&amp;amp; mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;getLooper返回Looper对象。其中同步代码块，如果looper为空，则等待唤醒。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public Handler getThreadHandler() {
        if (mHandler == null) {
            mHandler = new Handler(getLooper());
        }
        return mHandler;
    }

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;getThreadHandler返回hander对象。&lt;/p&gt;

&lt;p&gt;所以如果我们要自定义一套非主线程的消息通知机制，只需要HandlerThread的帮助就可以轻松实现。而不需要自己去创建looper对象了。&lt;/p&gt;

&lt;p&gt;所以调用的时候，只需要开启一个HandlerThread线程。&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;private HandlerThread mHandlerThread;
......
mHandlerThread = new HandlerThread(&quot;HandlerThread&quot;);
handlerThread.start();
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;由于HandlerThread内部的handler即没有实现Handler.Callback方法，也没有重写handleMessage方法，所以只能通过msg.callback回调来实现后续处理。所以通常我们可以自定义一个自己的Handler来处理消息即可。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;final Handler handler = new Handler(mHandlerThread.getLooper()){
            @Override
            public void handleMessage(Message msg) {
                System.out.println(&quot;收到消息&quot;);
            }
        };

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;其中handler一定要传入本线程的looper对象。这样我们就可以在handleMessage处理后续的业务问题。&lt;/p&gt;

&lt;p&gt;最后我们可以在任何一个线程中通过handler来发送消息了。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;handler.sendEmptyMessage(0);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最后一定不要忘了在onDestroy释放,避免内存泄漏&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandlerThread.quit();
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

</description>
        <pubDate>Mon, 07 Aug 2017 03:42:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/08/Thread-Handler%E5%92%8CHandlerThread%E5%85%B3%E7%B3%BB%E4%BD%95%E5%9C%A8/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/08/Thread-Handler%E5%92%8CHandlerThread%E5%85%B3%E7%B3%BB%E4%BD%95%E5%9C%A8/</guid>
        
        <category>源码分析|源码</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>handler引用的内存泄露</title>
        <description>&lt;p&gt;通常我们在合适handler进行线程通信的时候，会简单的如下调用&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    Handler handler1 = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            tv.setText(&quot;haha&quot;);
        }
    };
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;为什么这样调用会存在内存泄露呢？这是因为非静态的内部类（匿名类）会持有外部类对象的引用。为此什么这么说呢，你会发现，我们可以直接在handleMessage中调用外部Activity的引用，因此可以直接调用Activity的tv引用。&lt;/p&gt;

&lt;p&gt;所以说handler持有activity。而我们知道Message对象的target持有handler的引用。android应用的整个生命周期靠mainLoop来循环MessageQueue,MessageQueue持有Message的引用。所以如果相应的Message没有处理完的后，例如通过发送一个延时消息&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;则在10分钟内，Message都不会被处理，相应的Actiivty也不会释放。&lt;/p&gt;

&lt;p&gt;同理，如果这里存在另外一个线程的耗时操作（网络访问），此线程持有handler的引用，如果此线程不结束，也不会释放引用。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        new Thread(new Runnable() {
            @Override
            public void run() {
                doMuchWork();
                handler.sendEmptyMessage(100);

            }
        });
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;那么我们应该如何使用handler呢？使用静态内部类，或者单独抽取出一个文件来存放Handler类。当然这个时候，是没有外部类的引用。但是我们还是需要用到外部类对象的引用，应该如何处理呢，对，那就是通过一个弱引用。&lt;/p&gt;

&lt;p&gt;弱引用的作用是，当垃圾回收的时候，可以直接回收掉。如&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static class MyHandler extends Handler{
        WeakReference&amp;lt;Activity&amp;gt; weakReference ;
        public MyHandler(Activity aty){
            weakReference = new WeakReference&amp;lt;Activity&amp;gt;(aty);
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            MainActivity aty = (MainActivity) weakReference.get();

            aty.tv.setText(&quot;haha&quot;);
        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;还有一点需要注意的是，我们平时使用匿名内部类的时候，如runnable也要需要，他也持有外部类对象的引用，例如我们在onCreate方法中调用&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        handler.post(new Runnable() {
            @Override
            public void run() {
                tv.setText(&quot;haha&quot;);
            }
        });
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;那么如果这个runnable对应的Message没有处理完，也会有内存泄露。正确的做法还是声明为一个匿名的内部类。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static class MyRun implements Runnable{
        WeakReference&amp;lt;Activity&amp;gt; weakReference ;
        public MyRun(Activity aty){
            weakReference = new WeakReference&amp;lt;Activity&amp;gt;(aty);
        }
        @Override
        public void run() {
            MainActivity aty = (MainActivity) weakReference.get();

            aty.tv.setText(&quot;haha&quot;);
        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

</description>
        <pubDate>Sun, 06 Aug 2017 03:32:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/08/handler%E5%BC%95%E7%94%A8%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/08/handler%E5%BC%95%E7%94%A8%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2/</guid>
        
        <category>源码分析|源码</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>android 登录成功后再跳转到目标界面的思考</title>
        <description>&lt;p&gt;项目中经常有遇到一个典型的需求，就是在用户在需要进入A界面的时候，需要先判断用户是否登录，如果没有登录，则需要先进入登录界面，如果登录成功了，再直接跳转到A界面。&lt;/p&gt;

&lt;h3 id=&quot;需求定义&quot;&gt;需求定义&lt;/h3&gt;
&lt;p&gt;所以这里有两个需求： 1、自动跳转到登录界面 2、登录成功后再自动跳转到目标A界面&lt;/p&gt;

&lt;p&gt;如果我们直接判断用户有没有登录，提醒用户登录。也没有让用户登录成功后再直接跳转到目标界面，这样的用户体验恐怕是不能满足一个高逼格程序员的要求。那么，我们来思考下，如何才能更加优雅的完成这个工作呢？&lt;/p&gt;

&lt;p&gt;当然，在开始之前，我们可以先了解下其他人都是怎么做的，毕竟我们可以站在巨人的肩膀上才能看得更远。&lt;/p&gt;

&lt;h3 id=&quot;思考可行的方案&quot;&gt;思考可行的方案&lt;/h3&gt;
&lt;p&gt;首先我们第一个想到的解决方式，就是拦截器。如果我们在进入A界面的时候，可以在操作之前加入一个拦截器的话，岂不是可以做到在进入A界面前的判断呢？&lt;/p&gt;

&lt;h4 id=&quot;在google之后找到两个方案&quot;&gt;在google之后，找到两个方案。&lt;/h4&gt;
&lt;p&gt;A、 &lt;a href=&quot;http://www.jianshu.com/p/1487d72bb745&quot;&gt;Android拦截器&lt;/a&gt; (可以点击查看)&lt;/p&gt;

&lt;p&gt;此方案通过注解。在进入目标界面A时，判断是否有指定的拦截器，如果有，则检验是否满足拦截器要求，不满足，则执行拦截器的处理，处理完成后，通过onActivityResult最后触发invoke的回调方法。&lt;/p&gt;

&lt;p&gt;此方案和我们需求略有不同，那么说下此方案存在的缺点：
1、用了继承的方式,来插入invoke的回调方法。由于java的单继承的特性，如果工程中已经有基类的情况，调整起来比较麻烦。侵入性太高。&lt;/p&gt;

&lt;p&gt;2、此方案中，在没有登录的情况下，其实已经进入了目标A页面。相应的初始化都已经执行了。如果没有登录成功，这样工作其实是白做了。如果目标A界面要登录才能进入的话，此方案不符合要求的。&lt;/p&gt;

&lt;p&gt;B、我们直接使用路由框架，参考下阿里的ARouter方案，可以看到，我们可以在固定路由上面插入拦截器。这里有一篇文章介绍 &lt;a href=&quot;http://www.jianshu.com/p/c8d7b1379c1b&quot;&gt;阿里ARouter拦截器使用及源码解析&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;看了文章后，发现拦截器实现的非常优雅，但是依然不是我们想要的。因为这个拦截器执行完后，马上会执行目标方法。中间并不会等待。所以我们根本没有办法去执行我们的登录操作。 所以pass了。&lt;/p&gt;

&lt;p&gt;我们再回过头来思考，拦截器似乎并不能直接完成我们的需求，因为我们需要插入一个验证行为后（例如进入登录界面），还要执行相应的操作后，保证这个验证行为通过后，才能真正进入到我们的目标界面。&lt;/p&gt;

&lt;p&gt;其实如果我们只是单纯的完成这个功能的话，可能大家最容易想到的就是，在进入登录界面的时候，在intent中装载一个目标target的intent.如果登录成功了，就判断是否有目标target，如果有，就跳转到目标target.&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        Intent intent = new Intent(this,LoginActivity.class);
        Intent target = new Intent(this,OrderDetailActivity.class);
        intent.putExtra(&quot;target&quot;,target);
        startActivity(intent);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这种方式做起来非常直接，也可理解，但是最明显的问题就是，会导致登录界面多了很多与自己无关的业务判断。那我们继续google看看，有没有类似的做法，并且实现优雅一点的呢？&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://blog.csdn.net/xiaochouwangzi188/article/details/49768969&quot;&gt;Android 登录判断器，登录成功后帮你准确跳转到目标activity&lt;/a&gt; 这篇的访问量比较大，似乎是个比较靠谱的方法。我们来大概分析下它的做法。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public static void interceptor(Context ctx, String target, Bundle bundle, Intent intent) {
        if (target != null &amp;amp;&amp;amp; target.length() &amp;gt; 0) {
            LoginCarrier invoker = new LoginCarrier(target, bundle);
            if (getLogin()) {
                invoker.invoke(ctx);
            } else {
                if (intent == null) {
                    intent = new Intent(ctx, LoginActivity.class);
                }
                login(ctx, invoker, intent);
            }
        } else {
            Toast.makeText(ctx, &quot;没有activity可以跳转&quot;, 300).show();
        }
    }

private static void login(Context context, LoginCarrier invoker, Intent intent) {
        intent.putExtra(mINVOKER, invoker);
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        context.startActivity(intent);
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看上面的核心代码就是，封装一个LoginCarrier。如果没有登录，则把这个LoginCarrier传入到登录界面。登录成功后，触发invoke()方法。本质上和我们上面的想法差不多。&lt;/p&gt;

&lt;p&gt;看完之后，还是觉得实现上不够完美，总觉得有些缺点。例如&lt;/p&gt;

&lt;p&gt;1、在登录界面还是侵入了过多的逻辑（这似乎不可避免，但是否可以简洁些呢）&lt;/p&gt;

&lt;p&gt;2、扩展性比较差。比方说我要购买某个礼品，需要登录，然后再跳转到充值界面充值完成后再回来。&lt;/p&gt;

&lt;p&gt;那到底有没有更好的实现方案呢，谷歌后，发现暂时没有找到可靠的方案了，所以说靠天靠地，不如靠自己，既然找不到合适的方案，那就好好思考下，自己动手来干了。&lt;/p&gt;

&lt;p&gt;首先，我们再回过头考虑我们的需求，我们需要执行一个目标方法。但是目标方法需要一个前置的条件满足才能执行，并且这个前置条件可能不只一个，还有就是这个前置条件并不是马上就能完成的。&lt;/p&gt;

&lt;p&gt;那我们根据需求抽象出来的数据模型应该是。&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class CallUnit {
    //目标行为
    private Action action;
    //先进先出验证模型
    private Queue&amp;lt;Valid&amp;gt; validQueue = new ArrayDeque&amp;lt;&amp;gt;();
    //上一个执行的valid
    private Valid lastValid;

}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;那么目标行为action就是一个执行体。负责执行目标方法。&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public interface Action {
    void call();
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;验证操作validQueue保存一个验证队列，Valid的验证模型是&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public interface Valid {

    /**
     * 是否满足检验器的要求，如果不满足的话，则执行doValid()方法。如果满足，则执行目标action.call
     * @return
     */
    boolean check();
   //去执行验证前置行为，例如跳转到登录界面。（但并未完成验证。）
    void doValid();
}

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;那么整个逻辑用一幅图表达出来，会比较清楚。
&lt;img src=&quot;https://upload-images.jianshu.io/upload_images/2159256-91dedfb30a1c140c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;执行逻辑&quot; /&gt;&lt;/p&gt;

&lt;p&gt;接下来根据图，来讲解代码实现。&lt;/p&gt;

&lt;p&gt;第一步，我们需要构造一个CallUnit单元。例如，我们需要跳转到折扣界面，前置是我们必须要登录，并且要有折扣码。&lt;/p&gt;

&lt;p&gt;所以这里，我们有两个验证模型，一个是登录，一个是拿到折扣。&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class DiscountValid implements Valid {
    private Context context;

    public DiscountValid(Context context) {
        this.context = context;
    }

    /**
     *
     * @return
     */
    @Override
    public boolean check() {
        return UserConfigCache.isDiscount(context);
    }


    /**
     * if check() return false. then doValid was called
     */
    @Override
    public void doValid() {
         DiscountActivity.start((Activity) context);
    }
}


public class LoginValid implements Valid {
    private Context context;

    public LoginValid(Context context) {
        this.context = context;
    }

    /**
     * check whether it login in or not
     * @return
     */
    @Override
    public boolean check() {
        return UserConfigCache.isLogin(context);
    }


    /**
     * if check() return false. then doValid was called
     */
    @Override
    public void doValid() {
         LoginActivity.start((Activity) context);
    }
}

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

&lt;p&gt;然后我们需要构造一个执行体。直接在当前的Activity里面实现Action接口即可。例如我们在MainActivity中实现。&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    @Override
    public void call() {
        //这是我们的目标行为
        OrderDetailActivity.startActivity(MainActivity.this, &quot;1234&quot;);
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;接下来，我们就可以构造一个CallUnit对象并进行执行了。&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                CallUnit.newInstance(MainActivity.this)
                        .addValid(new LoginValid(MainActivity.this))
                        .addValid(new DiscountValid(MainActivity.this))
                        .doCall();
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们来看看doCall到底做了什么？&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public void doCall(){
        ActionManager.instance().postCallUnit(this);
    }

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;发现，我们是通过ActionManager的单例调用了postCallUnit().我们看下这个单例有啥作用&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;public class ActionManager {

    static ActionManager instance = new ActionManager();

    public static ActionManager instance() {

        return instance;
    }

    Stack&amp;lt;CallUnit&amp;gt; delaysActions = new Stack&amp;lt;&amp;gt;();
    ....
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;这个单例维护了一个CallUnit的堆栈，表示我们支持一个目标行为里面再嵌入一个目标行为。但是这个需求恐怕很少会遇到。但是设计上是支持的。&lt;/p&gt;

&lt;p&gt;我们再回过头看看，postCallUnit()到底做了啥？&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    /**
     * 根据条件判断，是否要执行一个action
     *
     * @param callUnit
     */
    public void postCallUnit(CallUnit callUnit) {

        //清除所有的actions
        delaysActions.clear();
        //执行check
        callUnit.check();
        //如果全部满足，则直接跳转目标方法
        if (callUnit.getValidQueue().size() == 0) {
            callUnit.getAction().call();
        } else {
            //加入到延迟执行体中来
            delaysActions.push(callUnit);

            Valid valid = callUnit.getValidQueue().peek();
            callUnit.setLastValid(valid);
            //是否会有后置任务
            valid.doValid();

        }
    }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;备注非常清楚，就是判断是否验证条件都满足，如果满足，则直接执行目标方法，如果不满足，则执行doValid方法。并且保存当前valid的引用，以便后面验证valid是否满足条件。如果不满足，是不允许再执行下一轮的验证。&lt;/p&gt;

&lt;p&gt;到这里，我们知道，我们已经触发了执行体，并顺利进入了登录验证的执行体。因为登录这个操作需要用户手动触发完成，我们只是引导用户到了登录界面(当然登录操作也可以代码自动完成，那就没有必要跳页面了)，由于我们因为等待用户的输入，我们的验证模型就在这里停下来了，如果登录成功了，我们才需要让整个验证模型再运转起来了，所以验证后，永远少不了手动开启验证模型。&lt;/p&gt;

&lt;p&gt;例如我们在登录成功后，需要调用方法CallUnit.reCall()：&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(LoginActivity.this,&quot;登录成功&quot;,Toast.LENGTH_SHORT).show();
                UserConfigCache.setLogin(LoginActivity.this, true);
                //这里执行延迟的action方法。
                CallUnit.reCall();
                finish();
            }
        });
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;我们看看CallUnit.reCall()的执行方法&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    public static void reCall(){
        ActionManager.instance().checkValid();
    }

    public void checkValid() {

        if (delaysActions.size() &amp;gt; 0) {
            CallUnit callUnit = delaysActions.peek();

            if (callUnit.getLastValid().check() == false) {
                throw new ValidException(String.format(&quot;you must pass through the %s,and then reCall()&quot;, callUnit.getLastValid().getClass().toString()));

            }

            if (callUnit != null) {
                Queue&amp;lt;Valid&amp;gt; validQueue = callUnit.getValidQueue();

                validQueue.remove(callUnit.getLastValid());
                //valid已经执行完了，则表示此delay已经检验完了--执行目标方法
                if (validQueue.size() == 0) {
                    callUnit.getAction().call();
                    //把这个任务移出
                    delaysActions.remove(callUnit);
                } else {

                    Valid valid = callUnit.getValidQueue().peek();
                    callUnit.setLastValid(valid);
                    //是否会有后置任务
                    valid.doValid();
                }
            }
        }
    }


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;最终是调用ActionManager.instance().checkValid()的方法，就是判断上一个valid是否执行成功，如果没有成功，则会报出异常。提示必须满足check()为true后，才能执行下一个valid.如果你永远都不想目标行为执行过去，就不要调用CallUnit.reCall()方法即可。如果上一个valid执行成功，则会再调用下一个valid，直到所有的valid都执行完成后，则进入callUnit.getAction().call()的执行。最后进入订单折扣界面了。&lt;/p&gt;

&lt;p&gt;ps:其实工程也实现了注解调用的实现。但是前提是所有的检验模型不需要传入额外的参数才行。 具体看代码&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    /**
     * 通过反射注解来组装(但是这个前提是无参的构造方法才行)
     *
     * @param action
     */
    public void postCallUnit(Action action) {
        Class clz = action.getClass();
        try {
            Method method = clz.getMethod(&quot;call&quot;);
            Interceptor interceptor = method.getAnnotation(Interceptor.class);
            Class&amp;lt;? extends Valid&amp;gt;[] clzArray = interceptor.value();
            CallUnit callUnit = new CallUnit(action);
            for (Class cla : clzArray) {
                callUnit.addValid((Valid) cla.newInstance());
            }

            postCallUnit(callUnit);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

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

&lt;h3 id=&quot;演示流程图如下&quot;&gt;演示流程图如下&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://upload-images.jianshu.io/upload_images/2159256-f5fef56d94e06eda.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;只需要进行登录的验证&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://upload-images.jianshu.io/upload_images/2159256-eb61449c1b85a498.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;需同时进行登录和优惠券的验证
&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;代码地址&quot;&gt;代码地址&lt;/h3&gt;
&lt;p&gt;最后放下完整的&lt;a href=&quot;https://github.com/jinyb09017/delayActionDemo&quot;&gt;代码链接库&lt;/a&gt;，如果对你有帮助，记得star哦&lt;/p&gt;
</description>
        <pubDate>Thu, 06 Jul 2017 03:32:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/07/android-%E7%99%BB%E5%BD%95%E6%88%90%E5%8A%9F%E5%90%8E%E5%86%8D%E8%B7%B3%E8%BD%AC%E5%88%B0%E7%9B%AE%E6%A0%87%E7%95%8C%E9%9D%A2%E7%9A%84%E6%80%9D%E8%80%83/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/07/android-%E7%99%BB%E5%BD%95%E6%88%90%E5%8A%9F%E5%90%8E%E5%86%8D%E8%B7%B3%E8%BD%AC%E5%88%B0%E7%9B%AE%E6%A0%87%E7%95%8C%E9%9D%A2%E7%9A%84%E6%80%9D%E8%80%83/</guid>
        
        <category>android实用工具|高效</category>
        
        
        <category>android</category>
        
      </item>
    
      <item>
        <title>jekyll Github Pages 博客搭建 并实现评论 阅读量 以及分类功能</title>
        <description>&lt;p&gt;搭建一个属于自己的博客系统，不仅可以将平时记录的知识进行归档和存储，同时也可以进行分享，让自己积累的成果得到别人的认同。相比使用一些博客平台，没有规则的限制，烦恼于编辑器不够好用，以及担心哪天博客gg了，数据会丢失，自建的博客系统可以随心所欲地满足自己的需求，哪时不爽调整哪里，如果你是个文艺小青年，创建一个独一无二的博客系统，不是逼格满满的，想想都让人兴奋吧&lt;/p&gt;

&lt;p&gt;当然，你会说，自建一个博客平台，需要太多技术的支持，例如数据库，前端页面，后台逻辑等，买域名，建服务器，岂不是得不偿失。当博客技术发展到今天，已经有不少简单和靠谱的方法，就能实现上面我们所说的需求。接下来，我会给大家讲解，如何实现一个逼格满满，并且功能齐全的博客站点。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://jinyb09017.github.io/&quot;&gt;Abbott的博客&lt;/a&gt;就是我个人花了一天时间实现的博客系统，接下来，我把整个博客搭建的过程记录下来，希望大家能够搭建一个属于你自己的博客网站。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://note.youdao.com/yws/public/resource/9c0aa2797b78886ad1cdf66d45169a4a/xmlnote/98E26220F077461F9199458815E2D855/7077&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;一在本地搭建一个jekyll环境&quot;&gt;一、在本地搭建一个jekyll环境&lt;/h3&gt;

&lt;p&gt;简单地说，Jekyll是一个用ruby写的解析引擎，用于从动态的组件中生成静态的网站。&lt;/p&gt;

&lt;h4 id=&quot;1安装ruby环境&quot;&gt;1、安装ruby环境&lt;/h4&gt;

&lt;p&gt;以下为windows的环境，其它环境大同小异，操作稍有不同。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.1.tar.gz&quot;&gt;ruby下载地址&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;安装完成后，在cmd下面执行&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ruby -v

output:
H:\Users\***&amp;gt;ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [x64-mingw32]

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如果看到了ruby版本,说明ruby已经安装成功。如果不成功，请查看ruby是否添加到环境变量。&lt;/p&gt;

&lt;h4 id=&quot;2安装jekyll&quot;&gt;2、安装jekyll&lt;/h4&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gem install jekyll

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如果是中国大陆用户那么默认的gem源进行安装会有一定困难。这里推荐使用taobao的ruby源。简单的使用以下命令就可以将淘宝repo作为默认repo。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gem sources --remove https://rubygems.org/
gem sources -a https://ruby.taobao.org/
gem sources -l
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;3主题下载到本地vno-jekyll&quot;&gt;3、主题下载到本地Vno-Jekyll&lt;/h4&gt;

&lt;p&gt;下载主题 &lt;a href=&quot;https://github.com/onevcat/vno-jekyll&quot;&gt;onevcat&lt;/a&gt;,更新到本地，进入主题根目录。&lt;/p&gt;

&lt;p&gt;再次执行下面的命令（下载工程依赖，类似于npm install）&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gem install jekyll 
bundle install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;4启动server&quot;&gt;4、启动server&lt;/h4&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;bundle exec jekyll serve

output:
Configuration file: E:/blog/abbott.github.io/_config.yml
Configuration file: E:/blog/abbott.github.io/_config.yml
            Source: E:/blog/abbott.github.io
       Destination: E:/blog/abbott.github.io/_site
 Incremental build: disabled. Enable with --incremental
      Generating...
                    done in 1.04 seconds.
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '&amp;gt;= 0.1.0' if Gem.win_platform?
 Auto-regeneration: enabled for 'E:/blog/abbott.github.io'
Configuration file: E:/blog/abbott.github.io/_config.yml
    Server address: http://127.0.0.1:4000/
  Server running... press ctrl-c to stop.

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;如果看到以上输出，表示jekyll启动成功。在浏览器访问http://127.0.0.1:4000/，即可看到博客的初步界面了。&lt;/p&gt;

&lt;p&gt;ps:Ruby 环境 SCSS 编译中文出现 Syntax error: Invalid GBK character 错误解决方法&lt;/p&gt;

&lt;p&gt;找到 Ruby 的安装目录，里面也有sass模块，如这个路径：&lt;/p&gt;

&lt;p&gt;C:\Ruby21-x64\lib\ruby\gems\2.1.0\gems\sass-3.4.8\lib\sass&lt;/p&gt;

&lt;p&gt;在该路径文件里面 engine.rb，添加一行代码（放在所有的require XXXX 之后即可）：&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;require ...

require'sass/supports'

Encoding.default_external = Encoding.find('utf-8')
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;二发布到github-pages&quot;&gt;二、发布到Github Pages.&lt;/h3&gt;
&lt;p&gt;这个步骤比较简单，&lt;a href=&quot;https://pages.github.com/&quot;&gt;Github Pages&lt;/a&gt;官网首页就有图文说明。&lt;/p&gt;

&lt;h4 id=&quot;1创建仓库&quot;&gt;1、创建仓库&lt;/h4&gt;
&lt;p&gt;打开创建代码仓库界面，创建一个与&lt;用户名&gt;.github.io的创建，如下 
![image](http://note.youdao.com/yws/public/resource/9c0aa2797b78886ad1cdf66d45169a4a/xmlnote/6DB40DFC32044E7C8DC5696A84C3D828/7079)&lt;/用户名&gt;&lt;/p&gt;

&lt;p&gt;ps：记住用户名一定要是自己的github的用户名。&lt;/p&gt;

&lt;h4 id=&quot;2将仓库克隆到本地&quot;&gt;2、将仓库克隆到本地&lt;/h4&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone 仓库地址
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;3添加文件indexhtml上传到仓库&quot;&gt;3、添加文件index.html,上传到仓库。&lt;/h4&gt;

&lt;div class=&quot;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;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello World&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;I'm hosted with GitHub Pages.&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h4 id=&quot;4浏览器输入你的用户名githubio就初步看到效果啦&quot;&gt;4、浏览器输入(你的用户名).github.io，就初步看到效果啦&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;http://upload-images.jianshu.io/upload_images/1812927-42b11234a5c3c09c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;5将步骤一中下载的代码上传到步骤二中建立的仓库即可以看到通过你的用户名githubio域名访问的博客了&quot;&gt;5、将步骤一中下载的代码上传到步骤二中建立的仓库，即可以看到通过(你的用户名).github.io域名访问的博客了。&lt;/h4&gt;

&lt;hr /&gt;

&lt;p&gt;到此为止，我们已经创建了一个简单的在线博客系统了。那么问题来了，我们如何管理自己的博客呢，以及如何为博客添加阅读数目统计，评论系统，以及分类呢，请看下面的介绍。&lt;/p&gt;

&lt;h3 id=&quot;三添加博客&quot;&gt;三、添加博客&lt;/h3&gt;

&lt;p&gt;在学习如何添加博客内容的时候，我们先大概了解下jekyll的基本结果&lt;/p&gt;

&lt;p&gt;Jekyll的核心其实就是一个文本的转换引擎，用你最喜欢的标记语言写文档，可以是Markdown、Textile或者HTML等等，再通过layout将文档拼装起来，根据你设置的URL规则来展现，这些都是通过严格的配置文件来定义，最终的产出就是web页面。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;|-- _config.yml
|-- _includes
|-- _layouts
|   |-- default.html
|    -- post.html
|-- _posts
|   |-- 2007-10-29-why-every-programmer-should-play-nethack.textile
|    -- 2009-04-26-barcamp-boston-4-roundup.textile
|-- _site
 -- index.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;config文件放全局配置信息&lt;/p&gt;

&lt;p&gt;include文件放布局文件&lt;/p&gt;

&lt;p&gt;layouts下面放通用模板布局&lt;/p&gt;

&lt;p&gt;posts下面则是我们的博客内容&lt;/p&gt;

&lt;p&gt;_site则是jekyll生成的静态网站目录&lt;/p&gt;

&lt;p&gt;index则是访问首页&lt;/p&gt;

&lt;p&gt;1、添加&lt;a href=&quot;https://jinyb09017.github.io/2017/06/%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91/&quot;&gt;《科学上网》&lt;/a&gt;的博客&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://note.youdao.com/yws/public/resource/9c0aa2797b78886ad1cdf66d45169a4a/xmlnote/AD82B45A9905499BA3257AF5D8C34A30/7134&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如上图所示，我们添加的文章是通过markdown的格式添加的，熟悉markdown格式的小伙伴会发现，基本没啥难点。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;---
layout: post
title: 科学上网
date: 2017-06-05 09:32:24.000000000 +09:00
categories: network
tags: 翻墙-科学上网
---
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;头部的内容为&lt;a href=&quot;http://jekyllrb.com/docs/frontmatter/&quot;&gt;YAML Front Matter&lt;/a&gt;,列举了博客的相关属性。&lt;/p&gt;

&lt;p&gt;ps：博客的文件名尽量不要做修改，博客的id是通过文件名字映射索引的。&lt;/p&gt;

&lt;h3 id=&quot;四添加disqus评论系统&quot;&gt;四、添加Disqus评论系统&lt;/h3&gt;

&lt;p&gt;Disqus款国外第三方社会化评论系统,将当前不同网站的相对孤立、隔绝的评论系统，连接成具有社会化特性的大网.&lt;/p&gt;

&lt;h4 id=&quot;1注册disqus&quot;&gt;1、注册&lt;a href=&quot;https://disqus.com/&quot;&gt;Disqus&lt;/a&gt;&lt;/h4&gt;

&lt;h4 id=&quot;2选择setting图标添加add-disqus-to-site&quot;&gt;2、选择setting图标，添加Add disqus to site&lt;/h4&gt;

&lt;p&gt;如果找不到，直接&lt;a href=&quot;https://disqus.com/admin/create/&quot;&gt;点击&lt;/a&gt;跳转，输入Website Name&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://note.youdao.com/yws/public/resource/9c0aa2797b78886ad1cdf66d45169a4a/xmlnote/D5A7006B6E8242E7AF853AA20BEA224D/7174&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;3设置根目录下_configyml文件的disqus的url&quot;&gt;3、设置根目录下_config.yml文件的disqus的URL&lt;/h4&gt;

&lt;p&gt;修改comment下面的disqus的值为自己填的Website Name.&lt;/p&gt;
&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Comment 这是disqus的评论系统
comment:
    disqus: abbott-king
    # duoshuo: 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样，我们的的评论系统就建立好了，可以登陆Disqus帐号后台，查看评论相关的数据。&lt;/p&gt;

&lt;h3 id=&quot;五添加阅读量&quot;&gt;五、添加阅读量&lt;/h3&gt;

&lt;p&gt;一般的博客系统，都会有帖子的阅读数量，可以查看自己文章有多少人阅读了。jekyll好像暂时没有相应的插件支持，但是我们可以通过cloudlean进行支持。&lt;/p&gt;

&lt;p&gt;一般的博客系统，都会有帖子的阅读数量，可以查看自己文章有多少人阅读了。jekyll好像暂时没有相应的插件支持，但是我们可以通过LeanCloud进行支持。&lt;/p&gt;

&lt;p&gt;LeanCloud 介绍：LeanCloud 是行业领先的一站式后端云服务提供商，专注于为移动开发者提供一流的工具、平台和服务。LeanCloud提供了很强大的功能，本文中用到的只是LeanCloud中的数据存储功能，而且没有涉及复杂的数据处理。&lt;/p&gt;

&lt;p&gt;思路：在进入文章的时候，调用LeanCloud的api，实现具体文章的点击数目添加。并存储到LeanCloud云平台。&lt;/p&gt;

&lt;h4 id=&quot;1注册leancloud&quot;&gt;1、注册LeanCloud&lt;/h4&gt;

&lt;h4 id=&quot;2创建应用申请appid和appkey&quot;&gt;2、创建应用，申请appid和appkey&lt;/h4&gt;

&lt;p&gt;需要注意：App ID和App Key要用在你的博客中，所以基本上等同于是公开的。所以需要做一些安全设置，以防止App ID和 App Key被滥用&lt;/p&gt;

&lt;p&gt;千万不要泄露Master Key，拥有Master Key相当于拥有了root权限。&lt;/p&gt;

&lt;h4 id=&quot;3-创建class&quot;&gt;3 创建Class&lt;/h4&gt;

&lt;p&gt;名字自定义，名字限制：只能包含字母、数字、下划线，必须以字母开头。&lt;/p&gt;

&lt;p&gt;我们可以创建了名为Counter的Class，采用默认的“限制写入”这种ACL权限设置&lt;/p&gt;

&lt;h4 id=&quot;4-修改代码&quot;&gt;4 修改代码&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;4.1 在_config.yml文件中，添加如下代码：&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;leancloud:
  enable: true
  app_id: your_id
  app_key: your_key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;app_id和app_key为我们自己注册申请的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.2 添加leancloud-analytics.html文件&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;

&amp;lt;script src=&quot;https://code.jquery.com/jquery-3.2.0.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;https://cdn1.lncld.net/static/js/av-core-mini-0.6.1.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;AV.initialize(&quot;n9H2t3VFwwMPqC0rSYfvlJuu-gzGzoHsz&quot;, &quot;DkeWUHsd9YIYIsnKRg6vw1Lk&quot;);&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    function showHitCount(Counter) {
        var query = new AV.Query(Counter);
        var entries = [];
        var $visitors = $(&quot;.leancloud_visitors&quot;);
        $visitors.each(function () {
            entries.push( $(this).attr(&quot;id&quot;).trim() );
        });
        query.containedIn('url', entries);
        query.find()
                .done(function (results) {
                    console.log(&quot;results&quot;,results);
                    var COUNT_CONTAINER_REF = '.leancloud-visitors-count';
                    if (results.length === 0) {
                        $visitors.find(COUNT_CONTAINER_REF).text(0);
                        return;
                    }
                    for (var i = 0; i &amp;lt; results.length; i++) {
                        var item = results[i];
                        var url = item.get('url');
                        var hits = item.get('hits');
                        var element = document.getElementById(url);
                        $(element).find(COUNT_CONTAINER_REF).text(hits);
                    }
                    for(var i = 0; i &amp;lt; entries.length; i++) {
                        var url = entries[i];
                        var element = document.getElementById(url);
                        var countSpan = $(element).find(COUNT_CONTAINER_REF);
                        if( countSpan.text() == '') {
                            countSpan.text(0);
                        }
                    }
                })
                .fail(function (object, error) {
                    console.log(&quot;Error: &quot; + error.code + &quot; &quot; + error.message);
                });
    }
    function addCount(Counter) {
        var $visitors = $(&quot;.leancloud_visitors&quot;);
        var url = $visitors.attr('id').trim();
        var title = $visitors.attr('data-flag-title').trim();
        var query = new AV.Query(Counter);
        query.equalTo(&quot;url&quot;, url);
        query.find({
            success: function(results) {
                if (results.length &amp;gt; 0) {
                    var counter = results[0];
                    counter.fetchWhenSave(true);
                    counter.increment(&quot;hits&quot;);
                    counter.save(null, {
                        success: function(counter) {
                            var $element = $(document.getElementById(url));
                            $element.find('.leancloud-visitors-count').text(counter.get('hits'));
                        },
                        error: function(counter, error) {
                            console.log('Failed to save Visitor num, with error message: ' + error.message);
                        }
                    });
                } else {
                    var newcounter = new Counter();
                    /* Set ACL */
                    var acl = new AV.ACL();
                    acl.setPublicReadAccess(true);
                    acl.setPublicWriteAccess(true);
                    newcounter.setACL(acl);
                    /* End Set ACL */
                    newcounter.set(&quot;title&quot;, title);
                    newcounter.set(&quot;url&quot;, url);
                    newcounter.set(&quot;hits&quot;, 1);
                    newcounter.save(null, {
                        success: function(newcounter) {
                            var $element = $(document.getElementById(url));
                            $element.find('.leancloud-visitors-count').text(newcounter.get('hits'));
                        },
                        error: function(newcounter, error) {
                            console.log('Failed to create');
                        }
                    });
                }
            },
            error: function(error) {
                console.log('Error:' + error.code + &quot; &quot; + error.message);
            }
        });
    }
    $(function() {
        var Counter = AV.Object.extend(&quot;Counter&quot;);
        if ($('.leancloud_visitors').length == 1) {
            // in post.html, so add 1 to hit counts
            addCount(Counter);
        } else if ($('.post-link').length &amp;gt; 1){
            // in index.html, there are many 'leancloud_visitors' and 'post-link', so just show hit counts.
            showHitCount(Counter);
        }
    });
&amp;lt;/script&amp;gt;

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

&lt;p&gt;&lt;strong&gt;4.3 _layouts/default.html&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;default.html是所有页面（包括index.html和每一篇博文）的模板文件。 
在 _layouts/default.html 中添加 leancloud-analytics.html 。 
在default.html中添加，是为了让LeanCloud的代码出现在每一篇blog中。&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;

&amp;lt;script src=&quot;https://code.jquery.com/jquery-3.2.0.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;https://cdn1.lncld.net/static/js/av-core-mini-0.6.1.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;AV.initialize(&quot;n9H2t3VFwwMPqC0rSYfvlJuu-gzGzoHsz&quot;, &quot;DkeWUHsd9YIYIsnKRg6vw1Lk&quot;);&amp;lt;/script&amp;gt;
&amp;lt;!--&amp;lt;script&amp;gt;console.log(&quot;Error: &quot; + error.code + &quot; &quot; + error.message);&amp;lt;/script&amp;gt;--&amp;gt;
&amp;lt;script&amp;gt;
    function showHitCount(Counter) {
        console.log(&quot;i was called&quot;);
        var query = new AV.Query(Counter);
        var entries = [];
        var $visitors = $(&quot;.leancloud_visitors&quot;);
        $visitors.each(function () {
            entries.push( $(this).attr(&quot;id&quot;).trim() );
        });
        query.containedIn('url', entries);
        query.find()
                .done(function (results) {
                    console.log(&quot;results&quot;,results);
                    var COUNT_CONTAINER_REF = '.leancloud-visitors-count';
                    if (results.length === 0) {
                        $visitors.find(COUNT_CONTAINER_REF).text(0);
                        return;
                    }
                    for (var i = 0; i &amp;lt; results.length; i++) {
                        var item = results[i];
                        var url = item.get('url');
                        var hits = item.get('hits');
                        var element = document.getElementById(url);
                        $(element).find(COUNT_CONTAINER_REF).text(hits);
                    }
                    for(var i = 0; i &amp;lt; entries.length; i++) {
                        var url = entries[i];
                        var element = document.getElementById(url);
                        var countSpan = $(element).find(COUNT_CONTAINER_REF);
                        if( countSpan.text() == '') {
                            countSpan.text(0);
                        }
                    }
                })
                .fail(function (object, error) {
                    console.log(&quot;Error: &quot; + error.code + &quot; &quot; + error.message);
                });
    }
    function addCount(Counter) {
        var $visitors = $(&quot;.leancloud_visitors&quot;);
        var url = $visitors.attr('id').trim();
        var title = $visitors.attr('data-flag-title').trim();
        var query = new AV.Query(Counter);
        query.equalTo(&quot;url&quot;, url);
        query.find({
            success: function(results) {
                if (results.length &amp;gt; 0) {
                    var counter = results[0];
                    counter.fetchWhenSave(true);
                    counter.increment(&quot;hits&quot;);
                    counter.save(null, {
                        success: function(counter) {
                            var $element = $(document.getElementById(url));
                            $element.find('.leancloud-visitors-count').text(counter.get('hits'));
                        },
                        error: function(counter, error) {
                            console.log('Failed to save Visitor num, with error message: ' + error.message);
                        }
                    });
                } else {
                    var newcounter = new Counter();
                    /* Set ACL */
                    var acl = new AV.ACL();
                    acl.setPublicReadAccess(true);
                    acl.setPublicWriteAccess(true);
                    newcounter.setACL(acl);
                    /* End Set ACL */
                    newcounter.set(&quot;title&quot;, title);
                    newcounter.set(&quot;url&quot;, url);
                    newcounter.set(&quot;hits&quot;, 1);
                    newcounter.save(null, {
                        success: function(newcounter) {
                            var $element = $(document.getElementById(url));
                            $element.find('.leancloud-visitors-count').text(newcounter.get('hits'));
                        },
                        error: function(newcounter, error) {
                            console.log('Failed to create');
                        }
                    });
                }
            },
            error: function(error) {
                console.log('Error:' + error.code + &quot; &quot; + error.message);
            }
        });
    }
    $(function() {
        var Counter = AV.Object.extend(&quot;Counter&quot;);
        console.log('this is a test');
        console.log('this is a test-add',$('.leancloud_visitors'));
        console.log('this is a test-show',$('.post-link'));
        if ($('.leancloud_visitors').length == 1) {
            // in post.html, so add 1 to hit counts
            addCount(Counter);
        } else if ($('.post-link').length &amp;gt; 1){
            // in index.html, there are many 'leancloud_visitors' and 'post-link', so just show hit counts.
            showHitCount(Counter);
        }
    });
&amp;lt;/script&amp;gt;

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

&lt;p&gt;&lt;strong&gt;4.4 _layouts/post.html&lt;/strong&gt;
post.html是我所发的blog的布局模板。在post.html中有发布时间、作者、标签、分类和文章内容。文章点击数量需要在这里显示，所以需要加上如下代码：&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
       &amp;lt;span id=&quot;/2017/06/jekyll-Github-Pages-%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA-%E5%B9%B6%E5%AE%9E%E7%8E%B0%E8%AF%84%E8%AE%BA-%E9%98%85%E8%AF%BB%E9%87%8F-%E4%BB%A5%E5%8F%8A%E5%88%86%E7%B1%BB%E5%8A%9F%E8%83%BD/&quot; class=&quot;leancloud_visitors&quot; data-flag-title=&quot;jekyll Github Pages 博客搭建 并实现评论 阅读量 以及分类功能&quot;&amp;gt;
        &amp;lt;span class=&quot;post-meta-divider&quot;&amp;gt;|&amp;lt;/span&amp;gt;
        &amp;lt;span class=&quot;post-meta-item-text&quot;&amp;gt; Hits:  &amp;lt;/span&amp;gt;
        &amp;lt;span class=&quot;leancloud-visitors-count&quot;&amp;gt;&amp;lt;/span&amp;gt;
       &amp;lt;/span&amp;gt;
   
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;需要注意的是，当前页面的信息记录在变量page中。page.url表示当前文章的地址，是以斜线开头的相对路径，例如blog/2016/12/18/test2。page.title是文章的标题，是为了在LeanCloud后台中看着方便的。当然，也可以用于判断是否是同一篇文章。&lt;/p&gt;

&lt;h3 id=&quot;六添加分类&quot;&gt;六、添加分类&lt;/h3&gt;
&lt;p&gt;随着文章的数目变多，如果没有一个合适的分类进行索引，不仅不利于我们自己检索内容，也不方便读者进行筛选。所以添加一个分类也是一个博客系统必备的功能。&lt;/p&gt;

&lt;p&gt;操作方法：&lt;a href=&quot;https://christianspecht.de/2014/10/25/separate-pages-per-tag-category-with-jekyll-without-plugins/&quot;&gt;参考文章&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;好累。。。写个操作手册，比搭建网站还费时间，希望能给大家带来一些帮助。&lt;/p&gt;
</description>
        <pubDate>Tue, 06 Jun 2017 03:32:24 +0000</pubDate>
        <link>http://vno.onevcat.com/2017/06/jekyll-Github-Pages-%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA-%E5%B9%B6%E5%AE%9E%E7%8E%B0%E8%AF%84%E8%AE%BA-%E9%98%85%E8%AF%BB%E9%87%8F-%E4%BB%A5%E5%8F%8A%E5%88%86%E7%B1%BB%E5%8A%9F%E8%83%BD/</link>
        <guid isPermaLink="true">http://vno.onevcat.com/2017/06/jekyll-Github-Pages-%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA-%E5%B9%B6%E5%AE%9E%E7%8E%B0%E8%AF%84%E8%AE%BA-%E9%98%85%E8%AF%BB%E9%87%8F-%E4%BB%A5%E5%8F%8A%E5%88%86%E7%B1%BB%E5%8A%9F%E8%83%BD/</guid>
        
        <category>网站搭建|个人博客</category>
        
        
        <category>talk</category>
        
      </item>
    
  </channel>
</rss>
