每天早上的小剧场

每天早上起来看书画画,攒了几天的,一块发上来。

新垣结衣的今日子形象

介个,咳咳

原型也是 Gakki 啊!!!

让人绝望的简笔画。认得出是 Gakki ?才怪!

又一个大屁股金克丝

窗台涂鸦 墨海马

发条魔灵

第一幅漫画形式的画

盲仔。不是很爱画 man 啊。

2017/3/16

金克丝动态线稿

大屁股金克丝。

2017/2/19 posted in  绘画

真人改漫画

阴影,衣褶,头的比例,头发分组。

### 赵丽颖

 
 
 

画师说我以前用色偏暗,这个特地用了粉嫩嫩的颜色。

鹿晗?


话说一只分不清鹿晗和张艺兴来着


手稿

衣服上exo字母颜色上残,后来直接自暴自弃了。

2017/2/19 posted in  绘画

第一张完整插画,线稿被彩铅毁了系列,lovelive 南小鸟 高坂穗乃果 小泉花阳

一直知道自己对颜色感觉差,画完线稿后也告诉自己,“上色会毁了这幅画!”
然后就真的被毁了。
至少是毁了一半。
以及对不起用掉一半的褐色铅笔。
我觉得有必要买本秘密花园做些针对练习。
希望明天的马赛克练习能有所帮助。
马赛克。



线稿完成!
画了三个疗程,结束时心理美滋滋的。
因为作画过程中我发现,画本子比插画容易得多,以为,不用画衣服!



上色前天晚上简单看了下蕾姆上色教学,学到些东西。不然不知道这幅会涂成什么样子。
肯定是浅棕色涂满整张脸,蓝色均匀地上满眼球,头发没有一点光泽,发根不会加重色。



颜色上到这里我已经要崩溃了。
左边衣服的颜色暗部部突出,找不到合适的颜色。
中间的头发亮瞎狗眼,衣服像烤焦了的小麦面包。不但又硬又干还散发着一股令人感到酸爽的焦味。
右边,哦,颜色简直是乱上。围巾暗部太分散了,像是天上飘的花瓣,分散视线。不过好在不是漫天飞的,夕阳。



到这个程度已经是夜里3点了。
楼房颜色,旁边路面为了方便直接画成水。
水,太浅,发白,分散视线。路面发黄,和右边人物对比度低。
左上建筑和头发明度区别小。


完工。
路面画歪了。透视太差。
左上楼房违和感很高。
我很满意。
即便颜色毁了我的线稿。
可总有一些线稿需要作为熟悉颜色的祭品。
我也,心安了。

2017/2/14 posted in  绘画

第二张默画

看的动漫很多,但观察过的太少。记忆中的就更千篇一律了。
默画时真是想不出能画什么头发。当然眼耳鼻口都是。

第二张默画

2017/2/7 posted in  绘画

哥特绅士

第一幅木偶画的成品。

画师说我的画有点哥特风格,带点神秘。
我想说,其实我非常喜欢哥特,哥特萝莉知道呢撒。

哥特绅士

2017/2/7 posted in  绘画

第一张默画的脸

一开始想画炮姐体积巨大化后,双眼冒火怒而俯视着我的那张脸。无奈细节画不出来,画出了这张gaygay的。
第一张默画的脸

2017/2/7 posted in  绘画

漂亮的小姐姐

我有一毛病。画时哪哪都觉得画的不对,画完看照片发现美呆了。可能这就是传说中的完美主义心理作祟吧。帅。

小姐姐

2017/2/7 posted in  绘画

彩铅完全不会用

上色时感觉怪怪的,后来看视频,用同样的方式人就能涂上,我就费老大力,结果还是涂不上。最后结论是,工欲善其事,必先利其器。
彩铅完全不会用

2017/2/7 posted in  绘画

植物速写

花盆用了好久来画,最后还是歪的。
最开始动手时很小气,总是把比例画小。和我性格中的某些因素相关。
植物速写

2017/2/7 posted in  绘画

静物速写2 小熊手枪和梨

静物速写2

2017/2/7 posted in  绘画

静物速写1 圆桌和鞋

其实是静物慢写,画的超级慢。用了大概4个小时。
静物速写1

2017/2/7 posted in  绘画

开发这个Chrome插件,更高效地翻译社区文档

注:本文为产品设想,没有实际的产品实现。

最近在看intellij 插件的文档,没有很好的中文版本。加之长期阅读英文文档时的想法和苦恼,准备开发一个chrome插件,用于辅助翻译社区文档。
本文从一个最简单的工具出发,设想了一下操作原型,四步走,如下:

1 . 打开待翻译页面

2 . 选取翻译段落

选取部分文字后跳出询问是否翻译的按钮,点击进入第3步。

3 . 直接修改文字 或进行标注

第2步中选中文字部分变成一个输入框,直接进行修改。

当然,在进行修改时仍然显示原文,保存翻译后隐藏原文是更好的表现方案。图片只是举例说明。

4 . 保存或导出

保存或导出这里方案也有很多,简单例举几个:
* 直接保存网页(到云端、本地)。保存后打开原文链接可选择是否直接展示翻译
* 导出到本地 pdf
* 导出markdown格式

这里最主要的一点就是,基本保留原来的排版结构。不需要在翻译的同时操心排版之事。

要点

  • 保留排版
  • 直接修改符合习惯
  • 打开原文可展示翻译页面

更多feature

  • 翻译建议(包括简单单词、词组翻译,通过翻译库匹配的建议,不同领域的建议)
  • 云端翻译库相关feature(通过插件翻译并保存于云端,用户自定义公开与否)
  • 协同翻译功能(同一页面、同一站点下)(标注、求助等)
  • 专有名词关联
  • 书库,文档库
  • 语法分析(拿不准的句子,看看类似的翻译例句)
  • 社交,这也能社交?可以。不过已经是脱离工具之外的主题了
  • 一键发布到问答社区或问答版
  • 众包形式(类协同模式)

有兴趣的同学请私信交流。

————————————
9.13 更新
谷歌翻译社区
文章片段化处理,分割成句子,用户只需完成短句翻译
分翻译、验证两类。综合评估翻译质量。

2017/2/7 posted in  产品想法

深入理解LayoutInflater.inflate()

原文链接:https://www.bignerdranch.com/blog/understanding-androids-layoutinflater-inflate/
译文链接:http://blog.chengdazhi.com/index.php/110

由于我们很容易习惯公式化的预置代码,有时我们会忽略很优雅的细节。LayoutInflater以及它在Fragment的onCreateView()中填充View的方式带给我的就是这样的感受。这个类用于将XML文件转换成相对应的ViewGroup和控件Widget。我尝试在Google官方文档与网络上其他讨论中寻找有关的说明,而后发现许多人不但不清楚LayoutInflater的inflate()方法的细节,而且甚至在误用它。

这里的困惑很大程度上是因为Google上有关attachToRoot(也就是inflate()方法第三个参数)的文档太模糊:

被填充的层是否应该附在root参数内部?如果是false,root参数只适用于为XML根元素View创建正确的LayoutParams的子类。

其实意思就是:如果attachToRoot是true的话,那第一个参数的layout文件就会被填充并附加在第二个参数所指定的ViewGroup内。方法返回结合后的View,根元素是第二个参数ViewGroup。如果是false的话,第一个参数所指定的layout文件会被填充并作为View返回。这个View的根元素就是layout文件的根元素。不管是true还是false,都需要ViewGroup的LayoutParams来正确的测量与放置layout文件所产生的View对象。

attachToRoot传入true代表layout文件填充的View会被直接添加进ViewGroup,而传入false则代表创建的View会以其他方式被添加进ViewGroup。

让我们就两种情况多举一些例子来更深入的理解。

attachToRoot是True

假设我们在XML layout文件中写了一个Button并指定了宽高为match_parent。

<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/custom_button">
</Button>

现在我们想动态地把这个按钮添加进Fragment或Activity的LinearLayout中。如果这里LinearLayout已经是一个成员变量mLinearLayout了,我们只需要通过如下代码达成目标:

inflater.inflate(R.layout.custom_button, mLinearLayout, true);

我们指定了用于填充button的layout资源文件,然后我们告诉LayoutInflater我们想把button添加到mLinearLayout中。这里Button的LayoutParams种类为LinearLayout.LayoutParams。

下面的代码也有同样的效果。LayoutInflater的两个参数的inflate()方法自动将attachToRoot设置为true。

inflater.inflate(R.layout.custom_button, mLinearLayout);

另一种在attachToRoot中传递true的情况是使用自定义View。我们看一个layout文件中根元素有标签的例子。标签标识着这个layout文件的根ViewGroup可以有多种类型。

public class MyCustomView extends LinearLayout {
...
private void init() {
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.view_with_merge_tag, this);
}
}

这就是一个很好的使用attachToRoot的例子。这个例子中layout文件没有ViewGroup作为根元素,所以我们指定我们自定义的LinearLayout作为根元素。如果layout文件有一个FrameLayout作为根元素而不是,那么FrameLayout和它的子元素都可以正常填充,而后都会被添加到LinearLayout中,LinearLayout是根ViewGroup,包含着FrameLayout和其子元素。

attachToRoot是False

我们看一下什么时候attachToRoot应该是false。在这种情况下,inflate()方法中的第一个参数所指定的View不会被添加到第二个参数所指定的ViewGroup中。

回忆一下刚才的例子中的Button,我们想通过layout文件添加自定义的Button至mLinearLayout中。当attachToRoot为false时,我们仍可以将Button添加到mLinearLayout中,但是这需要我们自己动手。

Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);
mLinearLayout.addView(button);

这两行代码与刚才attachToRoot为true时的一行代码等效。通过传入false,我们告诉LayoutInflater我们不暂时还想将View添加到根元素ViewGroup中,意思是我们一会儿再添加。在这个例子中,一会儿再添加就是在inflate()后调用addView()方法。

在将attachToRoot设置为false的例子中,由于要手动添加View进ViewGroup导致代码变多了。将Button添加到LinearLayout中还是用一行代码直接将attachToRoot设置为true简便一些。下面我们看一下什么情况下attachToRoot必须传入false。

每一个RecyclerView的子元素都要在attachToRoot设置为false的情况下填充。这里子View在onCreateViewHolder()中填充。

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
View view = inflater.inflate(android.R.layout.list_item_recyclerView, parent, false);
return new ViewHolder(view);
}

RecyclerView负责决定什么时候展示它的子View,这个不由我们决定。在任何我们不负责将View添加进ViewGroup的情况下都应该将attachToRoot设置为false。

当在Fragment的onCreateView()方法中填充并返回View时,要将attachToRoot设为false。如果传入true,会抛出IllegalStateException,因为指定的子View已经有父View了。你需要指定在哪里将Fragment的View放进Activity里,而添加、移除或替换Fragment则是FragmentManager的事情。

FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);

if (fragment == null) {
fragment = new MainFragment();
fragmentManager.beginTransaction().add(R.id.root_viewGroup, fragment).commit();
}

上面代码中root_viewGroup就是Activity中用于放置Fragment的容器,它会作为inflate()方法中的第二个参数被传入onCreateView()中。它也是你在inflate()方法中传入的ViewGroup。FragmentManager会将Fragment的View添加到ViewGroup中,你可不想添加两次。

public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
…
return view;
}

问题是:如果我们不需在onCreateView()中将View添加进ViewGroup,为什么还要传入ViewGroup呢?为什么inflate()方法必须要传入根ViewGroup?

原因是及时不需要马上将新填充的View添加进ViewGroup,我们还是需要这个父元素的LayoutParams来在将来添加时决定View的size和position。

你在网上一定会遇到一些不正确的建议。有些人会建议你如果将attachToRoot设置为false的话直接将根ViewGroup传入null。但是,如果有父元素的话,还是应该传入的。

null-root

Lint会警告你不要讲null作为root传入。你的App不会挂掉,但是可能会表现异常。当你的子View没有正确的LayoutParams时,它会自己通过generateDefaultLayoutParams计算。

你可能并不想要这些默认的LayoutParams。你在XML指定的LayoutParams会被忽略。我们可能已经指定了子View要填充父元素的宽度,但父View又wrap_content导致最终的View小很多。

下面是一种没有ViewGroup作为root传入inflate()方法的情况。当为AlertDialog创建自定义View时,还无法访问父元素。

AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
View customView = inflater.inflate(R.layout.custom_alert_dialog, null);
...
dialogBuilder.setView(customView);
dialogBuilder.show();

在这种情况下,可以将null作为root ViewGroup传入。后来我发现AlertDialog还是会重写LayoutParams并设置各项参数为match_parent。但是,规则还是在有ViewGroup可以传入时传入它。

避开崩溃、异常表现与误解

希望这篇文章可以帮助你在使用LayoutInflater时避开崩溃、异常表现与误解。下面整理了文章的要点:

  • 如果可以传入ViewGroup作为根元素,那就传入它。
  • 避免将null作为根ViewGroup传入。
  • 当我们不负责将layout文件的View添加进ViewGroup时设置attachToRoot参数为false。
  • 不要在View已经被添加进ViewGroup时传入true。
  • 自定义View时很适合将attachToRoot设置为true。
2017/2/7 posted in  Android开发

服务器作防盗链图片中转,nodejs 上手项目简明教程

前几天随手写的 chrome 插件遇到了防盗链问题,由于插件不能用 js iframe 的方法反防盗链,于是想用服务器做个中转。

记录一下上手项目的各个点,以后再用 nodejs 就不用到处查资料了。

之前没有一套特别熟悉的 web 开发框架,加上插件存储服务依赖的平台 LeanCloud 刚好支持部署 nodejs 网站,刚好拿这个小项目作为 nodejs 上手项目。


怎么"破解防盗链"呢?
想要破解,就得先知道目标——防盗链如何实现。
大多数站点的策略很简单: 判断request请求头的refer是否来源于本站。若不是,拒绝访问真实图片。

而我们知道: 请求头是来自于客户端,是可伪造的。

思路
那么,我们伪造一个正确的refer来访问不就行了?
整个业务逻辑大概像这样:  
1. 自己的服务器后台接受带目标图片url参数的请求
2. 伪造refer请求目标图片
3. 把请求到的数据作为response返回

这就起到了图片中转的作用。

1. 项目是什么样子

1.1 接口的样子?

  • 有一个开放接口
  • 接口有一个参数,api?url=http://abc.com/image.png,大概长这样子
  • 响应内容是反防盗链后的真实图片

1.2 应该怎么做?

  • 把服务器跑起来
  • 处理 GET 请求
  • 分析请求参数
  • 下载原图
  • response 原图

2. 学习路径(在对目标未知的前提下提出疑问)

  1. 如何开始,建立服务器
  2. 如何处理基本请求 GET POST
  3. 如何下载图片并转发
  4. 完成基本功能,上线
  5. 优化

2.1 如何开始,建立服务器

主要是 http.createServer().listen(port) 这组方法,建立服务器、监听端口一键搞定。

var http = require('http');
    
http.createServer(function (request, response) {
     // do things here
}).listen(8888);
    
console.log('Server running at: 8888');

2.2 如何处理基本请求 GET POST

createServer 回调方法的两个参数 req res 是 http requestresponse 的内容,打印一下他们的内容。

requestInComingMessage 类,打印它的 url 字段。

var http = require('http');
var url = require('url');
var util = require('util');
http.createServer(function(req, res){
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end(util.inspect(url.parse(req.url, true)));
}).listen(3000);

请求
http://localhost:3000/api?url=http://abc.com/image.png

请求结果

Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?url=http://abc.com/image.png',
  query: { url: 'http://abc.com/image.png' },
  pathname: '/api',
  path: '/api?url=http://abc.com/image.png',
  href: '/api?url=http://abc.com/image.png' }

query 字段刚好是我们想要的内容,下载这个字段对应的图片。

2.3 如何下载图片并转发

request 模块支持管道方法,可以和 shell 的管道一样理解。

这可以省很多事,不需要在本地存储图片,不需要处理杂七杂八的事情,甚至不需要再去了解 nodejs 的流。一个方法全搞定。

关键方法: request(options).pipe(res)

    var options = {
      uri: imgUrl, // 这个 uri 为空时,会认为该字段不存在,报异常
      headers: {
         'Referer': referrer // 解决部分防盗链选项
      }
    };
    request(options).pipe(res);

2.4 完成基本功能,上线

项目地址

完整代码

    'use strict';
    var router = require('express').Router();
    var http = require('http');
    var url = require('url');
    var util = require('util');
    var fs = require('fs');
    var callfile = require('child_process');
    var request = require('request');
    
    router.get('/', function(req, res, next) {
        var imgUrl = url.parse(req.url, true).query.url;
        console.log(url.parse(req.url,true).query); 
    
        console.log('get a request for ' + imgUrl);
        if (imgUrl == null || imgUrl == "" || imgUrl == undefined) {
            console.log('end');
            res.end();
            return;
        }
    
        var parsedUrl = url.parse(imgUrl);
        // 这里暂时使用图片服务器主机名做Referer
        var referrer = parsedUrl.protocol + '//' + parsedUrl.host; 
        console.log('referrer ' + referrer);
    
        var options = {
          uri: imgUrl,
          headers: {
             'Referer': referrer
          }
        };
    
        function callback(error, response, body) {
          if (!error && response.statusCode == 200) {
            console.log("type " + response.headers['content-type']);
          }
          res.end(response.body);
        }
    
        // request(options, callback);
        request(options)
            .on('error', function(err) {
                console.log(err)
            })
            .pipe(res);
    });
    
    module.exports = router;

2.5 优化

这部分主要是防盗链部分的优化。

单就 Referer 来说,使用空值和主机名都只能满足部分需求。

一个优化方式是组合,当一种方式不能突破即采用另一种方式。
这种方式的有点在于扩大了适用面积,并且方法对任何场景比较通用。

一个优化方式是接口请求参数带源引用连接。
这种方式对很多人来说不太通用,因为很多场景下并不清楚源引用连接在哪。
但是对我的插件来说非常适用,插件本身保留了源引用。因此可以很好的绕过防盗链限制。

2017/2/7 posted in  web开发

使用LeanCloud服务做一站式Chrome插件开发——Favorite Image

0. 目录

  1. 要开发的是什么项目 1.1 想法开端 1.2 应该有什么功能?
  2. 开发需要解决的核心问题
  3. 具体解决方案 3.1 帐号系统 3.2 存储服务 3.3 使用LeanEngine做反防盗链中转接口 3.4 Chrome 插件实现
  4. 对去后端化的看法

1. 要开发的是什么项目?

一个Chrome插件,用来保存浏览网页时看到的喜欢的图片。

1.1 想法开端

在 pixiv 翻图时看到一些喜欢的插画,看完就随手翻过去了,没有保存。为什么呢? 因为以我对自己的了解,图片下载下来,就相当于放进了垃圾桶。 并不是因为本地的文件管理有多乱,而是因为,几乎没有用鼠标打开文件管理器的习惯。

现在我获取信息的流量入口最常用的只有两个:1. 终端 2. 浏览器

于是乎,一个想法油然而生:

把插画存到浏览器吧!

于是就立刻构思,动手写了这款插件。

1.2 应该有什么功能?

功能很简单,
保存操作:1. 对图片点击右键 2. 选择"保存到浏览器.." 之类的选项
查看操作:1. 点击插件图标 查看保存过的图片。
其它:1. 图片同步到云端,也可保存到浏览器本地。2. 既然要保存到云端,自然需要账号系统

2. 开发需要解决的核心问题

核心问题有两个,一个是数据云存储问题,一个是图片防盗链问题。

云存储问题,帐号系统,多端同步
最开始只想做浏览器本地的存储,使用Chrome提供的localStorage存在本地就。
后来因为localStorage并不支持数据库语法查询,有很多不便。使用过程中又发现多端同步在体验上的优越性,决定要把存储放到云上。

图片防盗链问题
看了些资料,解决方式基本可以分为两种。

一类使用前端js嵌入iframe解决,优点是解决方式简单,问题是Chrome插件不支持页面嵌入式的js脚本。所以这个方案pass。

第二类使用后台服务器做反防盗链措施,作为中转给前端使用。优点是不受chrome插件的各种安全机制的限制,缺点是需要后台支持,增加工作量和资源成本。
使用第二类完成。

3. 具体解决方案

云存储及帐号系统使用LeanCloud提供的存储服务解决。
反防盗链接口使用LeanCloud提供的云引擎搭建NodeJs后台。

啰嗦一句,为什么要使用LeanCloud?
一是对我的需求可以做到完全免费,二是它们的文档实在是太xx的好用了。

3.1 帐号系统

参照:数据存储入门教程 · JavaScript

实现过程基本照抄这个教程的代码。后台账号系统包括对账号的重复检测、密码加密、session等都已经实现。

我们要做的,就是调用前端的这几个关键方法,实现简单的注册、登陆、退出:

  // LeanCloud - 注册
  // https://leancloud.cn/docs/leanstorage_guide-js.html#注册
  var user = new AV.User();
  user.setUsername(username);
  user.setPassword(password);
  user.setEmail(email);
  user.signUp().then(function (loginedUser) {
    // 注册成功
  }, (function (error) {
      alert(JSON.stringify(error));
  }));


  // LeanCloud - 登录
  // https://leancloud.cn/docs/leanstorage_guide-js.html#用户名和密码登录
  AV.User.logIn(username, password).then(function (loginedUser) {
    // 登录成功
  }, function (error) {
    alert(JSON.stringify(error));
  });


  // LeanCloud - 当前用户信息
  // https://leancloud.cn/docs/leanstorage_guide-js.html#当前用户
  var currentUser = AV.User.current();


  // 退出登陆
  AV.User.logOut();

3.2 存储服务

使用账号系统为每个用户添加身份信息后,存储部分就只需要把数据 + 用户身份信息一同上传或下载就可以了。

照样只贴关键方法

// 初始化类(在数据库中表现为数据表`ImageRepo`)和实例(数据库中表现为一条数据)
this.ImageRepo = AV.Object.extend('ImageRepo');
var repo = new this.ImageRepo();
// 填充数据
repo.put('username', 'xxx');
// 上传数据
repo.save().then(function (repo) {
    }, function (error) {
    });

// 下载数据
// 初始化对'ImageRepo'表的查询
var query = new AV.Query('ImageRepo');
// 查询条件为 username字段等于'xxx'
query.equalTo('username', 'xxx');
// 查询
query.find().then(function(results) {
    // 遍历results
    // 在页面添加解决防盗链问题后的图片
}, function(error) {
});

3.3 使用LeanEngine做反防盗链中转接口

要实现的效果是:
1. 我有一个防盗链图片连接abc.com/xxx.png
2. 我的接口url是http://codeli.leanapp.cn/image?url=xxx
3. 访问http://codeli.leanapp.cn/image?url=abc.com/xxx.png可访问原图,不受防盗链措施限制。

主要原理很简单,后台处理图片请求时更改header中的referer字段,将结果作为response返回。

关于这部分的实现,欢迎阅读我的另一篇文章,就不再赘述:
服务器作防盗链图片中转,nodejs 上手项目简明教程

关于LeanEngin的使用,文档如下,使用方法非常简单。
云引擎快速入门

云引擎支持NodeJS Python PHP JAVA
只需要下载云引擎命令行工具lean,然后输入几行命令就可以建立一个你熟悉的web框架。
然后,使用你熟悉的语言编写反防盗链实现就行了。

3.4 Chrome 插件实现

有了 3.1~3.3 的实现,这部分就是简单的插件部署和业务逻辑了。

Chrome 插件结构如图:
图片来自 蒋国纲的技术博客

主要业务:
1. 在popup窗口中添加注册 登陆 退出 等业务。
2. 打开popup 窗口时从云端获取指定账号下保存的图片信息,并展示。若未登陆,则从浏览器localStorage获取并展示。
3. background script 中添加右键菜单项: 当目标是图片时,显示Keep image in browser
4. 点击Keep image in browser, 执行保存业务逻辑: 若登陆了,保存到云端。若未登录,保存到浏览器localStorage

具体实现见我的github项目: KeepImageInBrowser
插件Web Store地址: Favorite Image

4. 最后,对去后端化的看法

前段时间在知乎上看到了一个问题,我也顺便说下自己的看法。

web后端会不会变得越来越不需要?

像bmob和leancloud这类后台云服务的流行有一段日子了,使用这些服务使一些web、app的开发周期大大缩减。这对于小团队和初创公司尤为方便。

但这并不意味着不再需要自己开发后台。不是因为他们提供的服务不够全面(相反,我倒认为这类服务将向着全面、便捷、快速发展),而是因为很多公司和产品,为了保持服务的质量和稳定,突出自己产品的特性,需要自己定制自己的后台,有针对性的去优化某些模块。
云服务作为大众服务平台难以为每个产品做定制。

类似于游戏引擎,如今各个平台都不缺乏优秀的游戏引擎。可是仍有公司和团队耗费大量的成本自研游戏引擎,就是希望能配合自己的游戏系统,完美地展现自己的游戏。

一样的,后台云服务和自定制的后台,是相交但永远不会重合的关系。 他们彼此之间相互影响,共同进步。

2017/2/7 posted in  web开发

生而为人,总要经历送礼这回事。逢年过节倒还好,亲戚朋友送些吃的喝的也就过去了。可是平时见朋友,过生日之类,选择什么礼物就成了一大烦恼。

有人说,送礼重要的是情谊,无论礼物轻重贵贱,只要真心祝福,朋友自然会开心。
道理是这样,我也在一定程度上表示认同。所以我今天要讲的不是价格高低,也不是品类含义,而只是讲讲我在选择礼物时的思考。

消耗品 or 非消耗品

在北京租房经历过几次搬家,加之对山下英子老师断舍离思想的认同,让我形成了「如果不是必要的,就不要随意添加」的观念。以至于每每要准备礼物,我都要消瘦三斤。
因为万一选取不当,便会成为被赠与者的负担。
“丢弃过意不去,放在家里不合适,礼物本身不适合转送,干脆建个专门收藏礼物的收藏馆吧”。于是新的创业项目风火出炉。
因此若非对对方很了解或事先询问,我定会选择消耗品或生命周期较短的物品。女生花、零食、化妆品成为首选,男生吃顿饭就算过去了。

书是我比较倾向的一类。
一来身边朋友大多抱着大学文凭,书的品类众多,因此即便对方早已对读书嗤之以鼻,也总能有那么几本适合的。
二来书籍适合转送。即便很不巧地送错了书,被赠予者也可以无压力地转送出去。如上所说,送礼重要的是心意,心意传达到了,自然皆大欢喜。
缺点是,除非对方嗜书如命,否则很难从这礼物中感受到惊喜。

红包

关系很铁或很官方时没问题。

兴趣对口周边

简单粗暴但仍需仔细确认。
如果对方是宅宅,送手办。
程序员,耳机键盘科技产品。
运动达人,运动装备。
程序猿,吃。
设计师,钱。

是否要事先询问

当我做好打算赠送非消耗品时,一定会认真询问。虽然有时会略显尴尬,又让对方失去了惊喜。但是对于一个信奉简约思想的朋友来说,无疑是极为合适的。重要的是,虽然没了惊喜,却能让人感受到你满满的诚意。

说了这些,回头想想现实其实并没这么复杂。现实中多的是收到什么礼物都感到惊喜的朋友。只是当对方是一个对生活认真且谨慎的人,而他又对你很重要时,切记,要谨慎处理。

2016/11/30 posted in  生活日常

Android中 Integer对象使用==运算符还是equals()方法比较大小?

最近项目组使用findbugs辅助检测代码问题,其中一个问题提到了Integer对象的值比较问题。虽然心里很清楚,java语言类对象的双等号操作符默认比较的是对象的地址,即是否是同一个对象。可是对于Integer、Long这类基本类型的扩展类,心想存在特殊处理的可能,所以还是查了下资料,主要是解释一些心里的疑惑。

疑问?

  1. java 能不能重载+、-、 =、==这些运算符
  2. java的Integer、Long这些类型的==运算符是比较地址还是使用equals的结果
  3. 为什么两个值为10的Integer对象,用==比较的结果是true,而两个值为1000的Integer对象的比较结果为false?

解答!

1. 能不能重载运算符?

不能。
有些人疑问,可是String对象可以有形如"hello worl" + 'd'的操作,基本运算应该是不支持的。
这里是因为编译器在编译时处理成了Object s2 = (new StringBuilder("hello world")).append('d').toString();的形式。

2. java的Integer、Long这些类型的==运算符是比较地址还是使用equals的结果?

比较地址。
因为不能重载运算符,所以即便是这些特殊的类,依然只能在编译器上动动手脚。

3. 为什么两个值为10的Integer对象,用==比较的结果是true,而两个值为1000的Integer对象的比较结果为false?

Integer为-128~127范围内的对象做了缓存处理。

public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}

无论你是使用new Integer(..)还是Integer.valueOf(..)获取对象,
只要值在-128到127的范围内,拿到的就是缓存好的对象。因此无论是==运算符还是equals()方法,只要值相同,结果都是true。
而如果你的值在这个范围之外,==必然返回false,equals()方法的返回值依对象的值而定。

结论

无论如何,请使用equals()方法比较大小。

2016/9/5 posted in  Android开发

如何在Java中避免equals方法的隐藏陷阱

今天看资料时看到coolshell的这篇文章,获益匪浅,收藏到文库里。

译文原文:http://coolshell.cn/articles/1051.html
英文原文:http://www.artima.com/lejava/articles/equality.html

译者注 :你可能会觉得Java很简单,Object的equals实现也会非常简单,但是事实并不是你想象的这样,耐心的读完本文,你会发现你对Java了解的是如此的少。如果这篇文章是一份Java程序员的入职笔试,那么不知道有多少人会掉落到这样的陷阱中。原文转自http://www.artima.com/lejava/articles/equality.html 三位作者都是不同领域的大拿,有兴趣的读者可以从上面这个连接直接去阅读原文。

摘要

本文描述重载equals方法的技术,这种技术即使是具现类的子类增加了字段也能保证equal语义的正确性。

在《Effective Java》的第8项中,Josh Bloch描述了当继承类作为面向对象语言中的等价关系的基础问题,要保证派生类的equal正确性语义所会面对的困难。Bloch这样写到:

除非你忘记了面向对象抽象的好处,否则在当你继承一个新类或在类中增加了一个值组件时你无法同时保证equal的语义依然正确

在《Programming in Scala》中的第28章演示了一种方法,这种方法允许即使继承了新类,增加了新的值组件,equal的语义仍然能得到保证。虽然在这本书中这项技术是在使用Scala类环境中,但是这项技术同样可以应用于Java定义的类中。在本文中的描述来自于Programming in Scala中的文字描述,但是代码被我从scala翻译成了Java

常见的等价方法陷阱

java.lang.Object 类定义了equals这个方法,它的子类可以通过重载来覆盖它。不幸的是,在面向对象中写出正确的equals方法是非常困难的。事实上,在研究了大量的Java代码后,2007 paper的作者得出了如下的一个结论:

几乎所有的equals方法的实现都是错误的!

这个问题是因为等价是和很多其他的事物相关联。例如其中之一,一个的类型C的错误等价方法可能意味着你无法将这个类型C的对象可信赖的放入到容器中。比如说,你有两个元素elem1和elem2他们都是类型C的对象,并且他们是相等,即elem1.equals(elm2)返回ture。但是,只要这个equals方法是错误的实现,那么你就有可能会看见如下的一些行为:

Set hashSet<C> = new java.util.HashSet<C>();
hashSet.add(elem1);
hashSet.contains(elem2);    // returns false!</pre>

当equals重载时,这里有4个会引发equals行为不一致的常见陷阱:

  1. 定义了错误的equals方法签名(signature) Defining equals with the wrong signature.
  2. 重载了equals的但没有同时重载hashCode的方法。 Changing equals without also changing hashCode.
  3. 建立在会变化字域上的equals定义。 Defining equals in terms of mutable fields.
  4. 不满足等价关系的equals错误定义 Failing to define equals as an equivalence relation.

在剩下的章节中我们将依次讨论这4中陷阱。

陷阱1:定义错误equals方法签名(signature)

考虑为下面这个简单类Point增加一个等价性方法:

public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    // ...
}

看上去非常明显,但是按照这种方式来定义equals就是错误的。

// An utterly wrong definition of equals
public boolean equals(Point other) {
  return (this.getX() == other.getX() && this.getY() == other.getY());
}

这个方法有什么问题呢?初看起来,它工作的非常完美:

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

Point q = new Point(2, 3);

System.out.println(p1.equals(p2)); // prints true

System.out.println(p1.equals(q)); // prints false

然而,当我们一旦把这个Point类的实例放入到一个容器中问题就出现了:

import java.util.HashSet;

HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);

System.out.println(coll.contains(p2)); // prints false

为什么coll中没有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等价的对象吗?在下面的程序中,我们可以找到其中的一些原因,定义p2a是一个指向p2的对象,但是p2a的类型是Object而非Point类型:

Object p2a = p2;

现在我们重复第一个比较,但是不再使用p2而是p2a,我们将会得到如下的结果:

System.out.println(p1.equals(p2a)); // prints false

到底是那里出了了问题?事实上,之前所给出的equals版本并没有覆盖Object类的equals方法,因为他的类型不同。下面是Object的equals方法的定义

public boolean equals(Object other)

因为Point类中的equals方法使用的是以Point类而非Object类做为参数,因此它并没有覆盖Object中的equals方法。而是一种变化了的重载。在Java中重载被解析为静态的参数类型而非运行期的类型,因此当静态参数类型是Point,Point的equals方法就被调用。然而当静态参数类型是Object时,Object类的equals就被调用。因为这个方法并没有被覆盖,因此它仍然是实现成比较对象标示。这就是为什么虽然p1和p2a具有同样的x,y值,”p1.equals(p2a)”仍然返回了false。这也是会什么HasSet的contains方法返回false的原因,因为这个方法操作的是泛型,他调用的是一般化的Object上equals方法而非Point类上变化了的重载方法equals

一个更好但不完美的equals方法定义如下:

// A better definition, but still not perfect
@Override public boolean equals(Object other) {
    boolean result = false;
    if (other instanceof Point) {
        Point that = (Point) other;
        result = (this.getX() == that.getX() && this.getY() == that.getY());
    }
    return result;
}

现在equals有了正确的类型,它使用了一个Object类型的参数和一个返回布尔型的结果。这个方法的实现使用instanceof操作和做了一个造型。它首先检查这个对象是否是一个Point类,如果是,他就比较两个点的坐标并返回结果,否则返回false。

陷阱2:重载了equals的但没有同时重载hashCode的方法

如果你使用上一个定义的Point类进行p1和p2a的反复比较,你都会得到你预期的true的结果。但是如果你将这个类对象放入到HashSet.contains()方法中测试,你就有可能仍然得到false的结果:

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);

HashSet<Point> coll = new HashSet<Point>();
coll.add(p1);

System.out.println(coll.contains(p2)); // 打印 false (有可能)

事实上,这个个结果不是100%的false,你也可能有返回ture的经历。如果你得到的结果是true的话,那么你试试其他的坐标值,最终你一定会得到一个在集合中不包含的结果。导致这个结果的原因是Point重载了equals却没有重载hashCode。

注意上面例子的的容器是一个HashSet,这就意味着容器中的元素根据他们的哈希码被被放入到”哈希桶 hash buckets”中。contains方法首先根据哈希码在哈希桶中查找,然后让桶中的所有元素和所给的参数进行比较。现在,虽然最后一个Point类的版本重定义了equals方法,但是它并没有同时重定义hashCode。因此,hashCode仍然是Object类的那个版本,即:所分配对象的一个地址的变换。所以p1和p2的哈希码理所当然的不同了,甚至是即时这两个点的坐标完全相同。不同的哈希码导致他们具有极高的可能性被放入到集合中不同的哈希桶中。contains方法将会去找p2的哈希码对应哈希桶中的匹配元素。但是大多数情况下,p1一定是在另外一个桶中,因此,p2永远找不到p1进行匹配。当然p2和p2也可能偶尔会被放入到一个桶中,在这种情况下,contains的结果就为true了。

最新一个Point类实现的问题是,它的实现违背了作为Object类的定义的hashCode的语义。

如果两个对象根据equals(Object)方法是相等的,那么在这两个对象上调用hashCode方法应该产生同样的值

事实上,在Java中,hashCode和equals需要一起被重定义是众所周知的。此外,hashCode只可以依赖于equals依赖的域来产生值。对于Point这个类来说,下面的的hashCode定义是一个非常合适的定义。

public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY());
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * (41 + getX()) + getY());
    }

}

这只是hashCode一个可能的实现。x域加上常量41后的结果再乘与41并将结果在加上y域的值。这样做就可以以低成本的运行时间和低成本代码大小得到一个哈希码的合理的分布(译者注:性价比相对较高的做法)。

增加hashCode方法重载修正了定义类似Point类等价性的问题。然而,关于类的等价性仍然有其他的问题点待发现。

陷阱3:建立在会变化字段上的equals定义

让我们在Point类做一个非常微小的变化

public class Point {

    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public void setX(int x) { // Problematic
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY());
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * (41 + getX()) + getY());
    }
}

唯一的不同是x和y域不再是final,并且两个set方法被增加到类中来,并允许客户改变x和y的值。equals和hashCode这个方法的定义现在是基于在这两个会发生变化的域上,因此当他们的域的值改变时,结果也就跟着改变。因此一旦你将这个point对象放入到集合中你将会看到非常神奇的效果。

Point p = new Point(1, 2);

HashSet<Point> coll = new HashSet<Point>();
coll.add(p);

System.out.println(coll.contains(p)); // 打印 true

现在如果你改变p中的一个域,这个集合中还会包含point吗,我们将拭目以待。

p.setX(p.getX() + 1);

System.out.println(coll.contains(p)); // (有可能)打印 false

看起来非常的奇怪。p去那里去了?如果你通过集合的迭代器来检查p是否包含,你将会得到更奇怪的结果。

Iterator<Point> it = coll.iterator();
boolean containedP = false;
while (it.hasNext()) {
    Point nextP = it.next();
    if (nextP.equals(p)) {
        containedP = true;
        break;
    }
}

System.out.println(containedP); // 打印 true

结果是,集合中不包含p,但是p在集合的元素中!到底发生了什么!当然,所有的这一切都是在x域的修改后才发生的,p最终的的hashCode是在集合coll错误的哈希桶中。即,原始哈希桶不再有其新值对应的哈希码。换句话说,p已经在集合coll的是视野范围之外,虽然他仍然属于coll的元素。

从这个例子所得到的教训是,当equals和hashCode依赖于会变化的状态时,那么就会给用户带来问题。如果这样的对象被放入到集合中,用户必须小心,不要修改这些这些对象所依赖的状态,这是一个小陷阱。如果你需要根据对象当前的状态进行比较的话,你应该不要再重定义equals,应该起其他的方法名字而不是equals。对于我们的Point类的最后的定义,我们最好省略掉hashCode的重载,并将比较的方法名命名为equalsContents,或其他不同于equals的名字。那么Point将会继承原来默认的equals和hashCode的实现,因此当我们修改了x域后p依然会呆在其原来在容器中应该在位置。

陷阱4:不满足等价关系的equals错误定义

Object中的equals的规范阐述了equals方法必须实现在非null对象上的等价关系:

  • 自反原则:对于任何非null值X,表达式x.equals(x)总返回true。
  • 等价性:对于任何非空值x和y,那么当且仅当y.equals(x)返回真时,x.equals(y)返回真。
  • 传递性:对于任何非空值x,y,和z,如果x.equals(y)返回真,且y.equals(z)也返回真,那么x.equals(z)也应该返回真。
  • 一致性:对于非空x,y,多次调用x.equals(y)应该一致的返回真或假。提供给equals方法比较使用的信息不应该包含改过的信息。
  • 对于任何非空值x,x.equals(null)应该总返回false.

Point类的equals定义已经被开发成了足够满足equals规范的定义。然而,当考虑到继承的时候,事情就开始变得非常复杂起来。比如说有一个Point的子类ColoredPoint,它比Point多增加了一个类型是Color的color域。假设Color被定义为一个枚举类型:

public enum Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
}

ColoredPoint重载了equals方法,并考虑到新加入color域,代码如下:

public class ColoredPoint extends Point { // Problem: equals not symmetric

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}

这是很多程序员都有可能写成的代码。注意在本例中,类ColoredPointed不需要重载hashCode,因为新的ColoredPoint类上的equals定义,严格的重载了Point上equals的定义。hashCode的规范仍然是有效,如果两个着色点(colored point)相等,其坐标必定相等,因此它的hashCode也保证了具有同样的值。

对于ColoredPoint类自身对象的比较是没有问题的,但是如果使用ColoredPoint和Point混合进行比较就要出现问题。

Point p = new Point(1, 2);

ColoredPoint cp = new ColoredPoint(1, 2, Color.RED);

System.out.println(p.equals(cp)); // 打印真 true

System.out.println(cp.equals(p)); // 打印假 false

“p等价于cp”的比较这个调用的是定义在Point类上的equals方法。这个方法只考虑两个点的坐标。因此比较返回真。在另外一方面,“cp等价于p”的比较这个调用的是定义在ColoredPoint类上的equals方法,返回的结果却是false,这是因为p不是ColoredPoint,所以equals这个定义违背了对称性。

违背对称性对于集合来说将导致不可以预期的后果,例如:

Set<Point> hashSet1 = new java.util.HashSet<Point>();
hashSet1.add(p);
System.out.println(hashSet1.contains(cp));    // 打印 false

Set<Point> hashSet2 = new java.util.HashSet<Point>();
hashSet2.add(cp);
System.out.println(hashSet2.contains(p));    // 打印 true

因此虽然p和cp是等价的,但是contains测试中一个返回成功,另外一个却返回失败。

你如何修改equals的定义,才能使得这个方法满足对称性?本质上说有两种方法,你可以使得这种关系变得更一般化或更严格。更一般化的意思是这一对对象,a和b,被用于进行对比,无论是a比b还是b比a 都返回true,下面是代码:

public class ColoredPoint extends Point { // Problem: equals not transitive

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        else if (other instanceof Point) {
            Point that = (Point) other;
            result = that.equals(this);
        }
        return result;
    }
}

在ColoredPoint中的equals的新定义比老定义中检查了更多的情况:如果对象是一个Point对象而不是ColoredPoint,方法就转变为Point类的equals方法调用。这个所希望达到的效果就是equals的对称性,不管”cp.equals(p)”还是”p.equals(cp)”的结果都是true。然而这种方法,equals的规范还是被破坏了,现在的问题是这个新等价性不满足传递性。考虑下面的一段代码实例,定义了一个点和这个点上上两种不同颜色点:

ColoredPoint redP = new ColoredPoint(1, 2, Color.RED);
ColoredPoint blueP = new ColoredPoint(1, 2, Color.BLUE);

redP等价于p,p等价于blueP

System.out.println(redP.equals(p)); // prints true

System.out.println(p.equals(blueP)); // prints true

然而,对比redP和blueP的结果是false:

System.out.println(redP.equals(blueP)); // 打印 false

因此,equals的传递性就被违背了。

使equals的关系更一般化似乎会将我们带入到死胡同。我们应该采用更严格化的方法。一种更严格化的equals方法是认为不同类的对象是不同的。这个可以通过修改Point类和ColoredPoint类的equals方法来达到。你能增加额外的比较来检查是否运行态的这个Point类和那个Point类是同一个类,就像如下所示的代码一样:

// A technically valid, but unsatisfying, equals method
public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result = (this.getX() == that.getX() && this.getY() == that.getY()
                    && this.getClass().equals(that.getClass()));
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * (41 + getX()) + getY());
    }
}

你现在可以将ColoredPoint类的equals实现用回刚才那个不满足对称性要的equals实现了。

public class ColoredPoint extends Point { // 不再违反对称性需求

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }
}

这里,Point类的实例只有当和另外一个对象是同样类,并且有同样的坐标时候,他们才被认为是相等的,即意味着 .getClass()返回的是同样的值。这个新定义的等价关系满足了对称性和传递性因为对于比较对象是不同的类时结果总是false。所以着色点(colored point)永远不会等于点(point)。通常这看起来非常合理,但是这里也存在着另外一种争论——这样的比较过于严格了。

考虑我们如下这种稍微的迂回的方式来定义我们的坐标点(1,2)

Point pAnon = new Point(1, 1) {
    @Override public int getY() {
        return 2;
    }
};

pAnon等于p吗?答案是假,因为p和pAnon的java.lang.Class对象不同。p是Point,而pAnon是Point的一个匿名派生类。但是,非常清晰的是pAnon的确是在坐标1,2上的另外一个点。所以将他们认为是不同的点是没有理由的。

canEqual 方法

到此,我们看其来似乎是遇到阻碍了,存在着一种正常的方式不仅可以在不同类继承层次上定义等价性,并且保证其等价的规范性吗?事实上,的确存在这样的一种方法,但是这就要求除了重定义equals和hashCode外还要另外的定义一个方法。基本思路就是在重载equals(和hashCode)的同时,它应该也要要明确的声明这个类的对象永远不等价于其他的实现了不同等价方法的超类的对象。为了达到这个目标,我们对每一个重载了equals的类新增一个方法canEqual方法。这个方法的方法签名是:

public boolean canEqual(Object other)

如果other 对象是canEquals(重)定义那个类的实例时,那么这个方法应该返回真,否则返回false。这个方法由equals方法调用,并保证了两个对象是可以相互比较的。下面Point类的新的也是最终的实现:

public class Point {

    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof Point) {
            Point that = (Point) other;
            result =(that.canEqual(this) && this.getX() == that.getX() && this.getY() == that.getY());
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * (41 + getX()) + getY());
    }
    public boolean canEqual(Object other) {
        return (other instanceof Point);
    }

}

这个版本的Point类的equals方法中包含了一个额外的需求,通过canEquals方法来决定另外一个对象是否是是满足可以比较的对象。在Point中的canEqual宣称了所有的Point类实例都能被比较。

下面是ColoredPoint相应的实现

public class ColoredPoint extends Point { // 不再违背对称性

    private final Color color;

    public ColoredPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    @Override public boolean equals(Object other) {
        boolean result = false;
        if (other instanceof ColoredPoint) {
            ColoredPoint that = (ColoredPoint) other;
            result = (that.canEqual(this) && this.color.equals(that.color) && super.equals(that));
        }
        return result;
    }

    @Override public int hashCode() {
        return (41 * super.hashCode() + color.hashCode());
    }

    @Override public boolean canEqual(Object other) {
        return (other instanceof ColoredPoint);
    }
}

在上显示的新版本的Point类和ColoredPoint类定义保证了等价的规范。等价是对称和可传递的。比较一个Point和ColoredPoint类总是返回false。因为点p和着色点cp,“p.equals(cp)返回的是假。并且,因为cp.canEqual(p)总返回false。相反的比较,cp.equals(p)同样也返回false,由于p不是一个ColoredPoint,所以在ColoredPoint的equals方法体内的第一个instanceof检查就失败了。

另外一个方面,不同的Point子类的实例却是可以比较的,同样没有重定义等价性方法的类也是可以比较的。对于这个新类的定义,p和pAnon的比较将总返回true。下面是一些例子:

Point p = new Point(1, 2);

ColoredPoint cp = new ColoredPoint(1, 2, Color.INDIGO);

Point pAnon = new Point(1, 1) {
    @Override public int getY() {
        return 2;
    }
};

Set<Point> coll = new java.util.HashSet<Point>();
coll.add(p);

System.out.println(coll.contains(p)); // 打印 true

System.out.println(coll.contains(cp)); // 打印 false

System.out.println(coll.contains(pAnon)); // 打印 true

这些例子显示了如果父类在equals的实现定义并调用了canEquals,那么开发人员实现的子类就能决定这个子类是否可以和它父类的实例进行比较。例如ColoredPoint,因为它以”一个着色点永远不可以等于普通不带颜色的点重载了” canEqual,所以他们就不能比较。但是因为pAnon引用的匿名子类没有重载canEqual,因此它的实例就可以和Point的实例进行对比。

canEqual方法的一个潜在的争论是它是否违背了Liskov替换准则(LSP)。例如,通过比较运行态的类来实现的比较技术(译者注: canEqual的前一版本,使用.getClass()的那个版本),将导致不能定义出一个子类,这个子类的实例可以和其父类进行比较,因此就违背了LSP。这是因为,LSP原则是这样的,在任何你能使用父类的地方你都可以使用子类去替换它。在之前例子中,虽然cp的x,y坐标匹配那些在集合中的点,然而”coll.contains(cp)”仍然返回false,这看起来似乎违背得了LSP准则,因为你不能这里能使用Point的地方使用一个ColoredPointed。但是我们认为这种解释是错误的,因为LSP原则并没有要求子类和父类的行为一致,而仅要求其行为能一种方式满足父类的规范。

通过比较运行态的类来编写equals方法(译者注: canEqual的前一版本,使用.getClass()的那个版本)的问题并不是违背LSP准则的问题,但是它也没有为你指明一种创建派生类的实例能和父类实例进行对比的的方法。例如,我们使用这种运行态比较的技术在之前的”coll.contains(pAnon)”将会返回false,并且这并不是我们希望的。相反我们希望“coll.contains(cp)”返回false,因为通过在ColoredPoint中重载的equals,我基本上可以说,一个在坐标1,2上着色点和一个坐标1,2上的普通点并不是一回事。然而,在最后的例子中,我们能传递Point两种不同的子类实例到集合中contains方法,并且我们能得到两个不同的答案,并且这两个答案都正确。

–全文完–

2016/9/5 posted in  Android开发

Android GPS定位欺骗(模拟定位)的两种方式

前段时间发布的手游PokemonGo相信大家都有耳闻,而因为这个游戏在国内的坐标遭到了封锁,很多科学游戏方法也陆续涌现。好不热闹。
那其实,PokemonGo最初的版本,在大陆是可以通过简单的vpn+gps欺骗进行游戏的。
不过很快地,在新的版本更新中就封锁了这一方式。

而对Android系统使用GPS欺骗,应用场景也绝不只是这一个游戏而已。所以我今天来简单介绍一下可使用的几种方式。

控制噪声的方式有三种:防止噪声产生,阻断噪声传播和防止噪声进入耳朵

相对应的,

修改GPS定位结果的三种途径: 编译时修改NLP结果,运行时修改LocationManager结果,从应用获取到的结果修改。

1. 编译时修改NLP结果

难度系数:五颗星
建议:想都别想
大概思路:修改nlp部分源码,重编系统

2. 运行时修改LocationManager结果

这个分两类:

一类: 使用android自带的调试api,模拟gps provider的结果。

LocationManager.setTestProviderLocation(Provider, Location);

优点:简单,无需root
缺点:不稳定,特征明显,容易按特征嗅探到(有反作弊机制的游戏基本都能查出来),需要打开开发者的允许模拟位置选项

第二类: 使用xposed,传说中的android神器,用它对app_process进行注入。

有什么用呢,就是你可以放个钩子,英文名叫hook。这个钩子能知道你系统里的每个应用什么时候调用了哪个函数,还能修改对应的这个函数。
说到这就懂了吧。比如你猜测对应app会使用LocationManager.getLastKnownLocation的结果。然后你用xposed把内存里的这个函数返回值改成 纬度 N 39.832670° 东经 E116.460370°,然后调用这个函数的程序看到的记过就是你修改之后的结果。
具体代码看这里吧(非本人repo,只是找了个简单易懂的demo)
FakeGPS demo

优点:稳定,难以被反查
缺点:需要root

3. 对想欺骗的app反编译,修改结果

该怎么做看标题就明白了。
步骤就是
1. 反编译
2. 找到所有使用了定位结果的位置
3. 修改结果
4. 重新打包

这个方式的优缺点也很明显。
优点: 无需root,稳定性强(前提是找准入口)
缺点: 技术水平要求高。根据应用复杂程度、混淆、安全策略等不同,难度差异较大。难易程度包括很多内容,包括混淆部分、入口寻找、签名验证等。 我也不熟啊,感兴趣的同学请自行深入学习吧。

————————

3类4种欺骗方式,各位看官收好。

以上。

2016/8/24 posted in  Android开发

android - LinearLayout RelativeLayout在布局时优先选择谁?有何区别?

很多初接触android的同学在布局时都会有这样一个疑问:

这个布局的父亲用LinearLayout和RelativeLayout都可以,我用哪个更合适呢?

有些同学可能就开始权量了,RelativeLayout更灵活,用这个吧。但是感觉LinearLayout更方便啊,自动帮我把这几个View分布开了,不会一开始就挤在一块。

其实呢,随着项目的进展,布局会发生调整,这是再自然不过的事情。

不要让自己这些『主观』的思考拖慢进度。

今天我从数据上来说下在面临这种情况(两者都可选择)时,应如何选择。

注: 当然,很多情况会有其它更好的选择,我这里仅就这两者进行对比。

从效率的角度考虑,选取渲染速度更快的那个。

我们知道一个View的绘制包括三个步骤:
1. 测量 measure
2. 布局 layout
3. 绘制 draw

这三个哪个拖后腿都会影响渲染效率,我们就来看看使用不同的布局时,这三者之间的差距。

子View 父View
父View使用LinearLayout linear_child.png linear_parent.png
父View使用RelativeLayout relative_child.png relative_parent.png

这里我们主要关注 Measure 时间。
通过对比我们可以看到,父View使用LinearLayout时,父亲和孩子的Measure时间是相差无几的。
而父View使用RelativeLayout时,父亲几乎是孩子的两倍。

RelativeLayout 的子View经常会被measure两次。

结论

  1. 两者都可使用,并且层数不受影响的前提下,尽量使用LinearLayout.
  2. 若是层数受影响,优先考虑层数少的。(这点会在以后的文章中谈到)
2016/8/24 posted in  Android开发

带阴影的TextView 淡入淡出动画异常,显示多余的阴影

1. 问题

带阴影的TextView 淡入淡出动画异常,有多余的阴影

2. 复现步骤

  1. TextView 设置阴影
  2. 对TextView设置的alpha值设置 ObjectAnimator
  3. 观察

3. 上图

TextView底部有多余阴影 动画正常

4. 原因

  1. 硬件加速并非对所有2D图形的支持都很好,处理alpha相关的事件时需尤为注意。
  2. 并非所有方法都有做应对处理。

5. 解决方案

  1. 对相应的View或Activity或Application关闭硬件加速。
  2. 使用其他替代方法实现动画。这就要看具体方法的实现了。
2016/8/24 posted in  Android开发

微博自动发贴,简单却不容易被注意到的反爬方式

移动端登录后移步 http://m.weibo.cn/mblog 页面发贴,正常思路是:填写消息->其它选项->点击发送。

tv_msg.send_keys("msg")
btn_send.click()

仔细看下细节,会发现,发送按钮一开始是disable的,输入消息后才会变成enable。
所以按理说,代码确实没有问题。可是执行结果是最终停留在这个页面,而且send按钮并没有变成可用的橙色。

测试最后发现,msg输入后,send并不会立即改变状态,而是有一个很小的延迟时间。而因为机器的执行速度极快,导致在send状态改变前,已经执行了click动作。所以发送动作没有如期进行。

在其中加入一点delay解决问题。

tv_msg.send_keys("msg")
time.sleep(0.1)
btn_send.click()
2016/8/7 posted in  生活日常

利用 Python + Selenium 实现对页面的指定元素截图(可截长图元素)

对WebElement截图

WebDriver.Chrome自带的方法只能对当前窗口截屏,且不能指定特定元素。若是需要截取特定元素或是窗口超过了一屏,就只能另辟蹊径了。
WebDriver.PhantomJS自带的方法支持对整个网页截屏。
下面提供几种思路。

方式一

针对WebDriver.Chrome

通过WebDriver的js脚本注入功能,曲线救国。
1. 注入第三方html转canvas的js库(见下方推荐)
2. 获取元素html源码
3. 将html转换为canvas
4. 下载canvas

优点: 截取长图容易实现
缺点: 加载第三方库耗费时间,转换原理请参考这篇文章:
将 DOM 对象绘制到 canvas 中

方式二

针对WebDriver.Chrome

截取全图,自行裁剪、拼接
1. 获取元素位置、大小
2. 获取窗口大小
3. 截取包含元素的窗口
4. 进行相应的裁剪和拼接。

具体算法思路很清晰,但需要注意的细节较多。这里就不在赘述。示例代码请移步:
[Github]PythonSpiderLibs

优点: 不需太多js工作,python+少量js代码即可完成
缺点: 拼接等工作会被WebDriver的实现差异、图片加载速度等因素影响,需多加注意。 在保证截图质量的情况下,速度较慢

方式三

针对WebDriver.PhantomJS
由于接口实现的差异,PhantomJS相比于Chrome,可以截取到整个网页。所以获取指定元素的截图也就简单很多

  1. 截取网页全图
  2. 裁剪指定元素
driver = webdriver.Chrome()
driver.get('http://stackoverflow.com/')
driver.save_screenshot('screenshot.png')

left = element.location['x']
top = element.location['y']
right = element.location['x'] + element.size['width']
bottom = element.location['y'] + element.size['height']

im = Image.open('screenshot.png') 
im = im.crop((left, top, right, bottom))
im.save('screenshot.png')

优点: 实现简单
缺点: 对于高度太高的页面会导致文件过大,处理会有问题,我测试的最大图片尺寸是12.8M。

解决图片加载不完整的问题

参考: 利用 Python + Selenium 自动化快速截图

我们先在首页上执行一段 JavaScript 脚本,将页面的滚动条拖到最下方,然后再拖回顶部,最后才截图。这样可以解决像上面那种按需加载图片的情况。

from selenium import webdriver
import time


def take_screenshot(url, save_fn="capture.png"):
    browser = webdriver.Firefox() # Get local session of firefox
    browser.set_window_size(1200, 900)
    browser.get(url) # Load page
    browser.execute_script("""
        (function () {
            var y = 0;
            var step = 100;
            window.scroll(0, 0);

            function f() {
                if (y < document.body.scrollHeight) {
                    y += step;
                    window.scroll(0, y);
                    setTimeout(f, 100);
                } else {
                    window.scroll(0, 0);
                    document.title += "scroll-done";
                }
            }

            setTimeout(f, 1000);
        })();
    """)

    for i in xrange(30):
        if "scroll-done" in browser.title:
            break
        time.sleep(10)

    browser.save_screenshot(save_fn)
    browser.close()


if __name__ == "__main__":

    take_screenshot("http://codingpy.com")

不同wewbdriver对某些方法的实现不同

Chrome和PhantomJS 的接口差异
抓知乎时的坑,
1. Chrome用WebElement.text可以正常得到值,用PhantomJS只能用 WebElement.get_attribute('innerHTML')
2. WebDriver.Chrome截图只能截当前屏幕区域。WebDriver.PhantomJS截图可以获取整个页面的长图。

其它还有一些坑等待发现

推荐

  1. html2canvas库
  2. 将 DOM 对象绘制到 canvas 中
  3. 利用 Python + Selenium 自动化快速截图
2016/8/7 posted in  生活日常

技术资料

2016/8/7

Android应用横竖屏切换的两种方式,从表现上看最大的区别

我们知道android应用更改屏幕方向有两种方式,对应两种过程,一是销毁重建,二是设置onConfigurationChanged。在其中做改变方向的处理。

很久前我们的测试同学给计算器应用报了一个bug,说应用旋转到横屏后锁屏,再解锁。此时应用会先回到竖屏,再转换到横屏(图一)。正确的表现应该和浏览器的表现一样(图二),解锁后直接进入横屏。
图一,表现错误,使用销毁重建的方式
图二,表现正确,使用onConfigurationChanged

百思不得其解。
后来解一个性能相关的bug,很巧地把原来销毁重建的方式,改成了onConfigurationChanged处理。无意间解了这个搁置很久的问题。

根据systrace的结果,推测可能的原因如下。
1. 销毁重建,其间会进行目标activity的销毁、重建以及其它系统调用。这一系列动作不能在一帧内完成,导致解锁时能看到竖屏切换到横屏的动作。
2. onConfigurationChanged,横竖屏切换其间只执行这一个函数,其中只有layout的MLD操作,这一系列动作在短时间内完成,因此我们看到的也就直接进入和横屏。
3. 可能有其它activity管理之间的原因,暂不清楚。知道的请不吝赐教。

2016/8/2 posted in  Android开发

谈谈爬虫-模拟登录思路

最近在做的sideproject,需要网络上的文章数据。于是顺便学习了下爬虫技术,也算是有些心得体会。写下来分享给刚入坑的新人。

怎么理解模拟登录?

怎么理解模拟登录?
把这句话补全就是: 怎么(让机器)模拟(人在浏览器上的行为)登录(指定的网站)。
那么这个问题实际上问的是: 人通过浏览器登录网站时,浏览器为我们做了哪些事情。
那么我们需要做的只有:写一个脚本,让这个脚本模拟浏览器的行为,做我们希望它做的事情。

有兴趣参考:
当在浏览器地址栏输入一个URL后回车,将会发生的事情?

那么
人类在登录时做了哪些事情呢,很简单:
1. 打开登录页面
2. 输入用户名密码,有时可能还有验证码,各种各样的验证码
3. 点击登录
4. 等待浏览器自动跳转

只要你稍微懂一点html语言,就应该能分析个八九不离十。
机器人怎么做呢:
两种方式:

方式一
需要使用虚拟的浏览器引擎。
优点: 适合几乎所有的网站登录,可以人为输入验证码
缺点: 速度较慢
1. 请求登录页面的url,比如微博的(https://passport.weibo.cn/signin/login)
2. 分析html中的表单数据
2.1 找到输入用户名、密码的输入框
2.2 把输入框的text域替换成自己的用户名密码
3. 模拟点击提交按钮

方式二
分析登录信息提交方式,一般就是表单
优点: 轻量,速度快
缺点: 局限性大,对技术要求高,对验证码机制需要做针对的破解
1. 使用浏览器的调试模式查看网页
2. 检查是否使用表单提交
3. 点击登录按钮,查看发送的请求数据。主要查看参数有无加密验证或其它隐藏信息。
4. 使用分析结果进行请求操作

对于一般用户,所有的非特殊性需求都可以使用方式一进行完成。
若非是为了学习,推荐方式一。

技术资料请参考:
Python爬虫学习系列教程(推荐)
[Python爬虫] Selenium爬取新浪微博移动端热点话题及评论 (下)

如何让脚本的行为看起来像人?

为什么要像人
因为很多服务器会使用一些反爬技术拒绝爬虫软件访问。

哪些东西让你看起来像人,哪些不像人
像人,其实可以分为两点。
一类是看请求数据,是否符合是浏览器发出的正常数据,比如header内容。
一类是看行为模式,发送请求对象的行为更像人类还是机器人,比如请求的频率。

不像人,和上面对应。
从请求数据上看,你没说明user-agent,我就可以认为你是非法侵入。你没有带着我之前给你的饼干(cookie)来,我也可以拒绝你。
从行为模式上,同一个ip访问的频率过高,短时间内流量异常,都可以作为非人类处理。

结合反爬技术

  1. 需要登录用cookie
  2. ip限制加代理
  3. 用user-agent告诉对方你是浏览器
  4. 服务器限制访问频率,加延迟
  5. ajax异步加载,使用js引擎或者人工分析
  6. redirect,最简单的方式虚拟内核+延迟
  7. 验证码,虚拟内核

如何找切入点?

什么是好的登录页面?
没有验证码,非ajax异步加载。
不一定局限于pc端网页,app端、移动端一般做的反爬策略比较少,可以从这里入手,寻找适合的站点。

理解自己要做什么,如何伪装成人类。仔细思考访问流程,针对性的有哪些反爬手段。把这些想通了,爬虫之路会好走很多。

转载请注明:未命名的博客

相关文章和资料

技术语言资料请自行google。

  1. 如何应对网站反爬虫策略?如何高效地爬大量数据?
  2. 能利用爬虫技术做到哪些很酷很有趣很有用的事情?(很有意思)
  3. Python爬虫学习系列教程(推荐)
  4. [Python爬虫] Selenium爬取新浪微博移动端热点话题及评论 (下)
2016/7/30 posted in  生活日常

使用Github的Webhooks进行网站的自动化部署

使用mWeb做自己的博客,服务器没有直接使用github的gh-pages功能,而是部署到了自己的服务器上。
从此更新博客变成了三步走:1. 使用mWeb生成静态网页 2. push 到github 3. 登录服务器拉取最新内容。

昨天想到,能不能再简化一些步骤,让我的文章push到github后,让服务器自动拉取文章,部署新内容。说干就干,实施想法。

1. 目标

服务器自动拉取push到github上的新文章。

2. 想法

想法一: 定时检查置顶repo的提交,有更新,则启动部署流程。(主动查询方式)
想法二: github是否支持事件提醒或者第三方有无支持。(被动唤醒方式)(相当于消息推送)

3. 思考

主动查询,耗费cpu时间及流量,并且必然会和github产生同步间隔。
被动唤醒,不会消耗不必要的资源,若是支持必然是第一选项。

4. 查阅资料(可行性分析)

github支持Webhooks及大量的第三方服务,可以很好得对repo的push等操作做出反应。

Webhooks做了什么?
当github收到repo的操作行为时,会向指定的url发送一个带有描述操作内容的post请求。

5. 实现思路(总结)

对指定repo注册webhooks,指向我的服务器上的接口,服务器解析数据,若操作是push,则进行部署行为。

6. 实现

6.1 部署脚本:

deploy.sh

#!/bin/bash

LOG_FILE="/var/log/blog_deploy.log"

date >> "$LOG_FILE"
echo "Start deployment" >>"$LOG_FILE"
cd /Path/need/be/deployed/
echo "pulling source code..." >> "$LOG_FILE"
git checkout origin gh-pages
git pull origin gh-pages
echo "Finished." >>"$LOG_FILE"
echo >> $LOG_FILE

每当接收到带push的post请求时,执行上面的脚本。

6.2 处理post请求

注:以下nodejs内容摘自曾曦前辈博客-尘埃落定

然后我们就要写一个脚本在 http://dev.lovelucy.info/incoming 这里接受 POST 请求了。因为本人机器上跑的是 node,俺就找了个 nodejs 的中间件 github-webhook-handler 。如果你要部署的是 PHP 网站,那你应该找一个世界上最好的语言 PHP 的版本,或者自己写一个,只需要接收 $_POST 嘛,好简单的,不多废话啦。么么哒 ( • ̀ω•́ )

$ npm install -g github-webhook-handler

鉴于在天朝的服务器上 npm 拉 repo 比拉屎还难的状况,我们可以 选用 阿里的镜像,据说 10 分钟和官方同步一次。_(:3 」∠ )_

$ npm install -g cnpm --registry=http://r.cnpmjs.org
$ cnpm install -g github-webhook-handler

好了,万事俱备,下面是 NodeJS 的监听程序 deploy.js

var http = require('http')
var createHandler = require('github-webhook-handler')
var handler = createHandler({ path: '/incoming', secret: 'myHashSecret' }) 
// 上面的 secret 保持和 GitHub 后台设置的一致
 
function run_cmd(cmd, args, callback) {
  var spawn = require('child_process').spawn;
  var child = spawn(cmd, args);
  var resp = "";
 
  child.stdout.on('data', function(buffer) { resp += buffer.toString(); });
  child.stdout.on('end', function() { callback (resp) });
}
 
http.createServer(function (req, res) {
  handler(req, res, function (err) {
    res.statusCode = 404
    res.end('no such location')
  })
}).listen(7777)
 
handler.on('error', function (err) {
  console.error('Error:', err.message)
})
 
handler.on('push', function (event) {
  console.log('Received a push event for %s to %s',
    event.payload.repository.name,
    event.payload.ref);
  run_cmd('sh', ['./deploy.sh'], function(text){ console.log(text) });
})
 
/*
handler.on('issues', function (event) {
  console.log('Received an issue event for % action=%s: #%d %s',
    event.payload.repository.name,
    event.payload.action,
    event.payload.issue.number,
    event.payload.issue.title)
})
*/

之后把服务器跑起来就可以了。

$ nodejs deploy.js

为了防止服务挂掉,我们有很多方式可以处理。我选择了用系统自带的nohup。

$ nohup nodejs deply.js &

曾曦前辈使用的是 NodeJs的forever,也可以使用python的supervisor。
曾曦前辈博客-尘埃落定有相关介绍。

6.3 配置Webhooks监听

将Payload URL指向自己服务器的接口

var handler = createHandler({ path: '/incoming', secret: 'myHashSecret' })

http.createServer(function (req, res) {
  handler(req, res, function (err) {
    res.statusCode = 404
    res.end('no such location')
  })
}).listen(7777)

这是deploy.js 的关键代码。
listen(7777),表明服务器监听的是7777端口
path:'/incoming',表示在 ip:7777/incoming 接收POST请求
secret: 'myHashSecret', 要求和上图的Secret字段一样,不然服务器会因为不匹配,拒绝接收到的请求。主要为了防止第三方向这个端口发送请求。

7. 最后梳理一下

6.3 那里知道什么时候有人提交文章了,然后告诉6.2 有人push
6.2 从6.3 得到消息,看下你的密码(secret)和我的一样不,如果一样,我就把这个消息告诉6.1
6.1 开始跑到github数据库拉取最新的数据,部署完成

澄清

有朋友告诉我,复制粘贴的部分比较多。即便加了转载说明,也不是很好。
在这里澄清一下:
网络上技术文章特点:
可用的经典实例:
自生产实例的成本: 费时
一篇全原创的优质文章需要:思考原创实例码字重复前三项
而对于学习者而言,思想+实例+思路已经满足80%。
所以我认为,一篇能学到东西的技术文章,并不需要全原创
清晰的思路+前人提供的经典实例+个人思考,传达到位即可。

前人都总结好了,你再发一遍,不是制造网络垃圾吗?
打造一个以思路清晰著称的博客,专注于技术文章整理、重成文是本博客存在的意义。我不是垃圾的生产者,我是大自然的清道夫。

欢迎关注个人微博斯科特安的时间 ,进行技术、非技术交流。

2016/7/21 posted in  生活日常

10分钟快速搭建无限制流量的"VPN"(shadowsocks协议)

Too Long No Read: 阅读标题和标重点部分就能了解全部内容。

1. 简介

良心声明: 有朋友说10分钟根本连文章都看不完,哪能建起个自己完全不熟悉的V P N 呢?
所以在这里必须解释一波:10分钟指的是开始动手到可以使用的时间。并不包括读这篇文章的时间,也不包括你在搭建服务过程中自我纠结的时间(比如,租多大的服务器?用什么密码?剁完手后又要饿多久的肚子才能给女票买下件内衣?之类。别问我为什么知道你一定是男的)最后,不包括运行出错,调试测试的时间。因为,按我说的做,你根本不可能失败!

接下来是简介:
说是无限制流量,其实骗你的啦(可爱)。说是VPN,其实不限于VPN。(本文搭建的也不是VPN,而是被称为Shadowsocks的协议。)
接下来解释:
原理: 租一个国外的服务器 -->这个服务器上搭建自己的VPN --> 通过这个VPN科学上网。
无限制流量:500G,1T,甚至更多。一月这么多,用的完吗?用不完不就相当于无限制。
VPN:既然有了自己墙外的服务器,就可以用它搭建任何自己想要的科学上网利器。本文章主要介绍当下最安全、最流行的ShadowSocks.
价格:很便宜!!很便宜!!很便宜!!

2. 工具篇

2.1 VPS 国外的服务器

VPS:Virtual Private Server 虚拟专用服务器
其实你就知道是个自己能用来搭建科学上网服务的主机就行了。
一般不了解的人,第一反应都是:卧槽,我指用个10G流量只能翻墙的VPN就几十块一个月。那租一个可以干很多事情、不限流量的服务器,岂不要几百几千?其实不用998、不用98,绝对用你想不到的价格,买到最不可思议的产品!

2019新年限时特惠!Vultr 虚拟服务器注册即得100美元奖励金!
点击注册得100美金
仅此一个月!

推荐:

1. 搬瓦工 (稳定推荐)

优点:便宜!!最低500G流量,年购19.99美刀,使用优惠码还可以再减1刀左右。相当于每月10元。这价格已经比大多数VPN便宜了。
支持支付宝交易!
支持30天内退款 一般一个工作日内就能回复,支付宝收到2~3天。
一键配置shadowsocks!如果使用搬瓦工,那后面的内容都不用看了,点下面的链接注册即可。
官方网址:https://bandwagonhost.com/
如果被墙也可使用: https://bwh8.net
两个都是官方地址

数据中心:美国西雅图、佛罗里达、洛杉矶、荷兰
套餐价格:64MB内存年3.99美元 / 96MB内存年4.99美元/128MB内存年5.99美元/512MB内存年9.99美元
简单介绍:IT7官方旗下的低价VPS主机产品,拥有速度较好的西岸亚利桑那州机房,最低年付仅需3.99美元,我们可以用来学习、工作项目演示,以及需要支持PPP/TUN搭建工具使用需求。拥有4个数据中心,而且可以自由切换IP,更换不同的IP,解决IP被封问题。

最新资料 可登录搬瓦工中文资料站进行查看
搬瓦工中文

2. Vultr (低价推荐,我自己目前在用)

2020年限时特惠!注册即得100美元奖励金!
点击注册得100美金
仅此一个月!


注册网址
优点
1. 注册赠送100美金。使用最低标准服务5刀/月,相当于可以免费使用20个月。
2. 服务稳定。至少我还没碰到过当机情况。
3. 第三点是缺点,想获得100美金,必须使用信用卡支付,并且扣除2.5美元的验证费。(以后会返还)
4. 现在已全面支持支付宝/微信付款,非常方便,也不需要绑定信用卡了。
点击注册并获取100美金

数据中心:日本、洛杉矶、英国、法国、德国、荷兰、澳大利亚等14个机房
套餐价格:KVM 768MB 15GB SSD 1TB月流量 $5/月
简单介绍:Vultr作为全球最大的游戏主机提供商背景之一,上线之后以高质的性价比、12个数据中心,以及新注册账户赠送5美金的账户使用金优惠促销,吸引广大的用户。作为我们用户,日本、洛杉矶等数据中心速度较好,如果有需要海外其他机房也可以在其12个数据中心中选择到适合自己的。

官方网站:https://www.vultr.com

3. 其它

因为我指用过上面两个,所以其他的也不多介绍了。列个列表,大家可以自行google。
Linode: 很多人推荐。速度快。价格中等。
DigitalOcean: 很多人推荐。速度快,价格差不多。

为什么说价格便宜

除了明码标价的价格。其实本身已经和普通VPN价格差不多了。但是仍然,有一点。虽然流量并不是无限,但是带宽并没有限制。就是说,在流量还够用的前提下,和朋友一起使用,是几乎不影响访问速度的,价格又能再除以...,最后折算下来非常可观。当然,不能超越物理极限,3、5个人一起用,是保险又便利的方式。

价格上,贵就是好

对于同类物品,贵就是好。所以无论是我提到的,还是没提到的,虽然价格有差异,但是毕竟体现在服务好坏上。所以,如果你发现不同价格,买到了同样的配置,但是实际效果却有差距,这很正常。

2.2 Python Shadowsocks 搭建服务的工具和协议

Shadowsocks 属于socks5 代理,稳定性好,抗干扰能力强。

搭建服务 三步走

1 . 安装
在CentOS中运行下面两条命令就完成了shadowsocks的安装了:

yum install python-setuptools && easy_install pip
pip install shadowsocks

2 . 配置
完成之后创建一个配置文件 /etc/shadowsocks.json,写入以下内容:

{ 
        "server":"0.0.0.0",            #服务器IP地址
        "server_port":8388,                 #服务监听端口
        "local_port":1080,                  #本地连接端口
        "password":"barfoo",               #加密传输使用到的密码
        "timeout":600,                      #连接超时时间
        "method":"aes-256-cfb"             #加密算法
}

3 . 启动、停止
运行下面的命令来启动和停止后台服务:

ssserver -c /etc/shadowsocks.json -d start
ssserver -c /etc/shadowsocks.json -d stop

然后你就可以使用上面的配置连接shadowsocks了。

  1. 客户端如何用?

各个平台使用的客户端都有差异,但是用到的信息就这些:
- 服务器IP: 不是上面的0.0.0.0,是你申请的VPS,会提供一个ip。打开网站,登录,找到它
- 端口(port): 8388
- 协议类型: aes-256-cfb 一般默认就这个,不用换。但还是要看一眼。
- 密码(password): barfoo
连接,欢呼。

2016/7/16 posted in  生活日常

PokemonGo破解

  1. 简单的更改unity位置 报错 07-14 22:04:02.801 7512-9260/? E/NianticAccountManager: User cannot be authenticated. com.google.android.gms.auth.GoogleAuthException: INVALID_AUDIENCE at com.google.android.gms.auth.GoogleAuthUtil.zza(Unknown Source) at com.google.android.gms.auth.GoogleAuthUtil.getToken(Unknown Source) at com.google.android.gms.auth.GoogleAuthUtil.getToken(Unknown Source) at com.google.android.gms.auth.GoogleAuthUtil.getToken(Unknown Source) at com.nianticlabs.nia.account.NianticAccountManager.getAccount(NianticAccountManager.java:75)

测试一下是否有签名登录保护之类的策略。

使用apk反编译后直接重新打包,打开app。
google账号鉴权成功。没问题!

测试做些简单的修改

invoke-virtual {p0, v0}, Lcom/kodelabs/boilerplate/presentation/ui/activities/MainActivity;->updateLocation(Landroid/location/Location;)V

.method public updateLocation(Landroid/location/Location;)V
.locals 2
.param p1, "location" # Landroid/location/Location;

.prologue
.line 31
const-wide v0, -0x3fa7170e2c12ad82L    # -99.63976

invoke-virtual {p1, v0, v1}, Landroid/location/Location;->setLongitude(D)V

.line 32
const-wide v0, 0x4043d0087ca643ccL    # 39.625259

invoke-virtual {p1, v0, v1}, Landroid/location/Location;->setLatitude(D)V

.line 33
return-void

.end method

打包安装

2016/7/14 posted in  Android开发

Creating a Navigation Drawer

表现

Navigation Drawer

手指从屏幕左边右滑,打开的这个像 Menu 一样的的页面就被称为 NavigationDrawer。
谷歌在 MD 规范中详细说明了它的设计规范,在开始设计你的应用时请务必阅读作为参考。
Navigation Drawer

实现思路

分析一下,很容易看出来,基本是分为两层。上层的这个 Drawer 层(包含你看到的这个 View 和可能被忽略的覆盖下层的阴影),下层的包含了页面本身的内容层(ActionBar TabLayout 其它内容)。

使用方式

所以,使用也很符合思考模式:以 DrawerLayout 为根 View 包含两层子 View。

1. XML 文件

    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- The main content view -->
    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <!-- The navigation drawer -->
    <ListView android:id="@+id/left_drawer"
        android:layout_width="240dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:divider="@android:color/transparent"
        android:dividerHeight="0dp"
        android:background="#111"/>
</android.support.v4.widget.DrawerLayout>

注意的点
* 内容层必须是DrawerLayout 的第一个子 View。因为 XML 以 View 的顺序决定 Z 的次序。
* drawer 层必须设定 android:layout_gravity 属性。用来支持 right-to-left 语言。注意用 "start" 代替 "left"。
* drawer 层的宽度小于320dp,这样展开时也能看到下方的内容。高度 match_parent。

2. 代码对内容进行配置

public class MainActivity extends Activity {
    private String[] mPlanetTitles;
    private DrawerLayout mDrawerLayout;
    private ListView mDrawerList;
    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mPlanetTitles = getResources().getStringArray(R.array.planets_array);
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerList = (ListView) findViewById(R.id.left_drawer);

        // Set the adapter for the list view
        mDrawerList.setAdapter(new ArrayAdapter<String>(this,
                R.layout.drawer_list_item, mPlanetTitles));
        // Set the list's click listener
        mDrawerList.setOnItemClickListener(new DrawerItemClickListener());

        ...
    }
}

3. 处理其它问题

3.1 处理点击事件导航

给 Drawer 的 ListView 设置 OnItemClickListener 就行。
示例代码
```
private class DrawerItemClickListener implements ListView.OnItemClickListener {
@Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
selectItem(position);
}
}

/** Swaps fragments in the main content view */
private void selectItem(int position) {
// Create a new fragment and specify the planet to show based on position
Fragment fragment = new PlanetFragment();
Bundle args = new Bundle();
args.putInt(PlanetFragment.ARG_PLANET_NUMBER, position);
fragment.setArguments(args);

// Insert the fragment by replacing any existing fragment
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction()
               .replace(R.id.content_frame, fragment)
               .commit();

// Highlight the selected item, update the title, and close the drawer
mDrawerList.setItemChecked(position, true);
setTitle(mPlanetTitles[position]);
mDrawerLayout.closeDrawer(mDrawerList);

}

@Override
public void setTitle(CharSequence title) {
mTitle = title;
getActionBar().setTitle(mTitle);
}
```

监听 Drawer 展开、关闭事件

调用 DrawerLayout.setDrawerListener() 方法,传递一个 DrawerLayout.DrawerListener 接口的实现。
实现这个接口有两种方式。
1. 直接实现 DrawerListener.
2. 写一个继承 ActionBarDrawerToggle 的子类,这个类实现了这个接口,你可以在子类中重写 DrawerListener 接口函数。

示例代码
```
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
private CharSequence mDrawerTitle;
private CharSequence mTitle;
...

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ...

    mTitle = mDrawerTitle = getTitle();
    mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
    mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
            R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {

        /** Called when a drawer has settled in a completely closed state. */
        public void onDrawerClosed(View view) {
            super.onDrawerClosed(view);
            getActionBar().setTitle(mTitle);
            invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
        }

        /** Called when a drawer has settled in a completely open state. */
        public void onDrawerOpened(View drawerView) {
            super.onDrawerOpened(drawerView);
            getActionBar().setTitle(mDrawerTitle);
            invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()
        }
    };

    // Set the drawer toggle as the DrawerListener
    mDrawerLayout.setDrawerListener(mDrawerToggle);
}

/* Called whenever we call invalidateOptionsMenu() */
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    // If the nav drawer is open, hide action items related to the content view
    boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
    menu.findItem(R.id.action_websearch).setVisible(!drawerOpen);
    return super.onPrepareOptionsMenu(menu);
}

}
```

通过 App Icon 打开、关闭 Drawer

通过使用 ActionBarDrawerToggle 类,可以控制这些行为。
* 点击控制 Drawer 行为
* 指定展开时的 icon

标准的导航 Icon 图标在 (Download the Action Bar Icon Pack)[http://developer.android.com/downloads/design/Android_Design_Icons_20130926.zip]

最后你需要在 Activity 的生命周期中使用 ActionBarDrawerToggle 的这些方法。
示例代码
```
public class MainActivity extends Activity {
private DrawerLayout mDrawerLayout;
private ActionBarDrawerToggle mDrawerToggle;
...

public void onCreate(Bundle savedInstanceState) {
    ...

    mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
    mDrawerToggle = new ActionBarDrawerToggle(
            this,                  /* host Activity */
            mDrawerLayout,         /* DrawerLayout object */
            R.drawable.ic_drawer,  /* nav drawer icon to replace 'Up' caret */
            R.string.drawer_open,  /* "open drawer" description */
            R.string.drawer_close  /* "close drawer" description */
            ) {

        /** Called when a drawer has settled in a completely closed state. */
        public void onDrawerClosed(View view) {
            super.onDrawerClosed(view);
            getActionBar().setTitle(mTitle);
        }

        /** Called when a drawer has settled in a completely open state. */
        public void onDrawerOpened(View drawerView) {
            super.onDrawerOpened(drawerView);
            getActionBar().setTitle(mDrawerTitle);
        }
    };

    // Set the drawer toggle as the DrawerListener
    mDrawerLayout.setDrawerListener(mDrawerToggle);

    getActionBar().setDisplayHomeAsUpEnabled(true);
    getActionBar().setHomeButtonEnabled(true);
}

@Override
protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    // Sync the toggle state after onRestoreInstanceState has occurred.
    mDrawerToggle.syncState();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    mDrawerToggle.onConfigurationChanged(newConfig);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Pass the event to ActionBarDrawerToggle, if it returns
    // true, then it has handled the app icon touch event
    if (mDrawerToggle.onOptionsItemSelected(item)) {
      return true;
    }
    // Handle your other action bar items...

    return super.onOptionsItemSelected(item);
}
...

}
```

2016/3/19 posted in  Android开发

Smartoo 开发中遇到的问题和解决

Weibo客户端sso认证不成功
uri mismatch 的问题是因为weibo开放平台配置有延迟,在平台上修改redirect uri后,隔天就可以通过了
闪退的问题,可能是由于不是打的release的包,做reader的分享时就有这样的问题。明天用release的包测试一下。
使用web验证可以顺利通过。

未通过审核帐号的接口调用限制
大概在200-300次之间

2016/1/27 posted in  Android开发

风の诗(风之诗 Wind Song)

F4

音程

n度 大n度 小n度
和弦 和谐,不和谐
和弦 根音+1 3 5度,小三度+大三度 minor和弦, 大三度+小三度 大和弦, 小三度+小三度 减和弦

2016/1/17 posted in  吉它学习🎸

下町火箭

http://i12.tietuku.com/82e26086176beb7c.png
《下町火箭》讲述的是小小的佃制造所通过努力,成功帮助帝国重工将火箭升空的一系列感人的故事。
通常这类故事都会被打上励志的标签,本剧却不止于此。

如何能够在一次火箭发射失败后从头再来,潜心研究。如何平衡工作和女儿的关系。如何能像信任自己的孩子一般信任自己的作品。
《下町火箭》更多讲的是人看待梦想的态度,人与人之间的羁绊。

日剧总是把人们的这些感情解读并重点展现出来,这也是为什么我会对日剧如此钟爱。

2015/11/29 posted in  书籍电影

app 我有一个问题

这个app不是问答式的社区

疑问

人每天都会产生疑问,但每天也有疑问被搁置下来,没有解答
想想,如果这些问题全部或者大部分都能被解决并记录。那么一年以后的自己将会变得多么博学

想想,这些问题被搁置的原因

  • 时间: “我在做其它事情,分不开时间”
  • 优先级: “其实问题也不是那么重要”
  • 复杂性: “这可能不是一会儿能了解的”
  • 领域: “无从下手”
  • 人真是复杂慵懒的动物: “想之后解决,然后就忘记了,或者想起来也没兴趣了”
  • 比干货更简洁: “我只想知道个大概,可网上的答案总不能让我满意。要么太全面,要么没讲到点”

没能在产生疑问的时间,找到合适的资料,和那个能一句话讲到点的

因此,我们需要什么

正式点讲 最简洁,最get到point的回答
不正式点 最右神回复

然而提供内容并不是我们要做的

人从心底总是自私的
即便在知乎这类为他人服务的平台上,人们也总是倾向于回答自己感兴趣的提问

如何做

因此,
我们只是提供给用户一个私人的记录平台, 个人的问题管理平台

定位:

  • 个人手账类的记事本
  • 将公开内容聚合的知识库
  • 社区

功能

  • 随时定义新概念:有时为了讲解方便,需要用一个词代替一个概念。在当下没有相应的概念时,一个新的概念词诞生是被允许和值得推荐的。我们也有机制推荐其中优秀和广为传播的新概念。
  • 随时记录新疑问
  • 对于未填补答案的问题
    • 自动归档到todo
    • 手动填补答案
    • 查看知识库中推荐的已有答案
    • 公开征求答案
    • 选择合适的答案mark到自己的手账中
  • 对于已填补的问题
    • 根据艾宾浩斯记忆法,进行复习
    • 知识库中有相关问题的优解,推荐
    • 在知识库中作为备选答案,接受其它用户浏览、评分
  • 知识库的答案全部由运营商或其它用户提供
  • 打赏功能:对于很好地解决了自己问题的优质答案,用户可选择进行打赏
  • 专题功能:专家级的用户可以创建自己的专题,专题内容通过一定的标准(质与量),可以将其电子版内容打包出售
  • 发烧圈子:可由专题、小组组成。是人的圈子,也是内容的圈子。一个内容可以在某个圈子发表,圈子内可见。一个用户可以在圈子中被推荐。
  • 热门的分类社区:howto类,购物类,等

用户群体

  • 文青
  • 动手达人
  • 领域专家
  • 爱思考的人
  • 拖延症患者
  • 除上面以外的其他人

盈利模式

这是我的一个问题。期待社区帮我解答。

最后

我们希望并渴求精炼的一句话答案
帮助大多数人瞬间开窍

我们也渴望专业级别的教科书
提升你自己的同时,带给世界更优质的内容

2015/11/2 posted in  产品想法