作者归档:jimmy

由于golang.org不能正常访问导致的glide install错误Cannot detect VCS

Go语言是Google推出的一种程序语言,Glide是go语言的包管理器,可以解决项目的依赖关系。目前,Glide建议我们尽快迁移到dep上。

然而,由于许多常用的包都在golang.org上,而由于众所周知的原因,golang.org一直处于中国大陆无法正常访问的情况,所以,glide update总会出现诸如Cannot detect VCS的错误。

Mac和Linux下,可以用如下命令添加golang的镜像

rm -rf ~/.glide
mkdir -p ~/.glide
glide mirror set https://golang.org/x/mobile https://github.com/golang/mobile --vcs git
glide mirror set https://golang.org/x/crypto https://github.com/golang/crypto --vcs git
glide mirror set https://golang.org/x/net https://github.com/golang/net --vcs git
glide mirror set https://golang.org/x/tools https://github.com/golang/tools --vcs git
glide mirror set https://golang.org/x/text https://github.com/golang/text --vcs git
glide mirror set https://golang.org/x/image https://github.com/golang/image --vcs git
glide mirror set https://golang.org/x/sys https://github.com/golang/sys --vcs git
glide mirror set https://google.golang.org/grpc https://github.com/grpc/grpc-go --vcs git

比特币的UTXO和以太坊的Account Nonce

比特币和以太坊都是目前知名的数字货币,然而,两者在实现上采用了截然不同的模型。

比特币采用的是未消费输出(Unspent Transaction Output,简写UTXO),而以太坊则采用账户余额体系。

比特币的交易模型如下

一笔交易 TX 包含多个输入(input)和多个输出(output,一个输出包含两个内容,一个是比特币数量,单位为satoshi,另一个就是接受比特币的收款人的public key),当TX 1,2,3,4,5,6都还没有发生的时候,TX 0的output0和output1都没有被别的交易的输入而引用,此时,TX 0的output0和output1就被称之为未消费输出(UTXO),当TX 0的output0被TX 1的input0引用之后,TX 0的output0就被消费了。

不难看出,比特币为了防止双花(double spend),一个输出不能同时被两个输入而引用。如果A的一个UTXO有100个比特币,而我们希望转20个给B,那么,我们需要将这100个比特币的UTXO消费掉,将其中80个发送到一个新的收款人为A的UTXO中,20个发送收款人为B的UTXO中,交易完成以后,之前的有100个比特币的输出就从UTXO变成了Spent Transaction Output了。

对于比特币来说,输出的源头就是挖矿而产生的coinbase交易,一个coinbase交易的输入是一个coinbase,输出为区块奖励。挖矿奖励的coinbase交易为一个区块的第一个交易。

我们不难看到,对于比特币来说,如果我们需要计算一个地址有多少比特币余额,我们必须将该地址对应的所有的UTXO余额累加起来,才能得到账户的余额,然而,如果我们的账户拥有10个UTXO,那么,理论上,我们可以同时发送10笔交易,这笔交易同时打包到一个区块之中,只要每一笔交易引用的UTXO是不同的就好。

总结,比特币的UTXO模型,使得单个账户可同时发送多笔交易,且多笔交易之间可以不互相产生影响,这使得比特币的交易天生有着很好的并发友好特性。

而对于以太坊来说,由于采用了账户余额体系,因此,以太坊在防止双花(Double Spend)上就没有办法采用比特币的方法了。为此,以太坊规定,每一个账户有一个nonce值,这个nonce值等于该账户的累计发起的交易数量,如果该账户发起一笔交易,那么,交易的数据中必须包含一个nonce值,该值必须大于账户的nonce值,否则,该交易属于非法交易。如果交易的nonce值减去账户的nonce大于1,那么,该笔交易暂时不能被包含到区块中,必须等到nonce值为账户的nonce值加一的交易被打包以后,该笔交易才能被打包到区块中。如果有两笔交易都是同样的nonce值,那么,只有其中一笔能够成功。

以太坊采取了账户体系,并通过状态变化(State Transition)记录了交易对账户的影响。

我们不难发现以太坊的nonce值是必要的。

首先,如果没有nonce值的话,黑客可以通过重放攻击来偷盗数字货币,假设A向B转行了1 ETH,而且以太坊没有nonce机制,那么,B可以将之前成功的转账交易数据在以太坊网络上重复发送,这样,B就可以源源不断的获取ETH,而这显然不是A期待的。

其次,nonce值的设计确保了交易一定是顺序执行的,避免了双花(Double Spend)问题。

    然而,以太坊的设计也造成了一些天然的缺点。

  1. 如果用户发送了一笔交易,并且选择了很低的矿工费(如gasPrice设置为0),那么,该笔交易可能数日甚至几个月都无法得到确认,这会导致用户的账号在很长的时间内都无法发起转出交易。而比特币就没有这个问题。
  2. 官方的Ethereum Wallet和Mist都没有提供取消交易的功能,实际上,我们可以通过发送和上一笔交易相同的nonce的新的交易,且新的交易为自己转账给自己,并选择高矿工费(如gasPrice为50或者100 Gwei),那么上一笔交易将会被以太坊网络抛弃。然而,由于Ethereum Wallet和Mist既不支持用户自定义nonce,也不支持自己转账给自己,所以,事实上客户没有好的办法取消自己的交易。
  3. 尽管以太坊的出块速度比比特币高很多,平均15秒出一个快,且理论上以太坊没有设置block limit,实际上以太坊的TPS仍然很低,只能达到10左右。这是因为有很多复杂交易,例如创建新的合约,一笔交易就上百万的gas消耗,而以太坊的区块gas limit通常也就800万左右,一个区块能够容纳的交易个数就有限了。尽管矿工可以提升gas limit,由于单个区块交易越多,矿工计算量就越大,所以,矿工不会轻易提升区块的gas limit,毕竟小区块更容易挖出来,从而挣到block reward。基于此考虑,以太坊的TPS很难提升。

然而,不论是比特币还是以太坊,亦或是其他的公有链的区块链平台,由于他们在设计上都考虑到了全球节点的同步,这就决定了出块速度不能太快,否则必然大量节点无法及时同步最新的状态,这就导致共识无法及时形成,很容易造成区块的分叉。

以太坊 Ethereum ERC20代币批量转账接口

以太坊(Ethereum)作为一个知名的区块链平台,大量的代币发行(Initial Coin Offering)通过以太坊进行,而代币通常为以太坊上一个遵循了ERC20规范的智能合约。

如果一个以太坊智能合约实现了以下接口,那么,这个智能合约即为一个ERC20代币。

// ----------------------------------------------------------------------------
// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
// ----------------------------------------------------------------------------
contract ERC20Interface {
    function totalSupply() public constant returns (uint);
    function balanceOf(address tokenOwner) public constant returns (uint balance);
    function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
    function transfer(address to, uint tokens) public returns (bool success);
    function approve(address spender, uint tokens) public returns (bool success);
    function transferFrom(address from, address to, uint tokens) public returns (bool success);

    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

然而,我们不难看到,ERC20规范中只有一对一转账的transfer和transferFrom,如果我们要一次实现向成千上万个地址转账,那么,我们就需要产生上万个transfer交易,这未免太低效了。

所以,不少ERC20代币都实现了批量转账的接口。
如近期爆出漏洞的BEC(https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code)实现了batchTransfer函数。

SMT(https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code)也实现了allocateTokens函数。

他们都可以实现一笔以太坊交易(Transaction)完成对多个账户的代币转账或初始化。

本文提出了一种实现一对多转账的方法,该方法名称为transferMultiple

首先,本文默认已用了SafeMath库

library SafeMath {
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }
        uint256 c = a * b;
        assert(c / a == b);
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
}

接下来,transferMultiple

transferMultiple实现了从msg.sender向count个_tos地址转账,且_tos[i]获得_values[i]的代币。

首先,第一个for循环进行了前置检查,确保了每一个_tos地址都是非0地址,同时,计算了转账的总额,并将总额记录到total变量中。在计算过程中,为了防止溢出,我们采用了SafeMath库,并且每一次都要比较当前计算出来的总额total和上一笔总额total_prev,确保total大于等于total_prev,双重保证不会整数溢出导致转账故障。

其次,第二个for循环不直接调用transfer方法,而是直接修改内部变量,这是因为前置检查已经做了,如果再次调用transfer函数的话,会再次执行额外的不必要的前置检查,会增加消耗的gas。

function transferMultiple(address[] _tos, uint256[] _values, uint count)  payable public returns (bool success) {
        uint256 total = 0;
        uint256 total_prev = 0;
        uint i = 0;

        for(i=0;i= total_prev);
        }

        require(total <= balanceOf(msg.sender);

        for(i=0;i<=count-1;i++){
            balances[msg.sender] = SafeMath.sub(balances[msg.sender], _values[i]);
            balances[_tos[i]] = SafeMath.add(balances[_tos[i]], _values[i]);
            Transfer(msg.sender, _tos[i], _values[i]);
            //以上三行也可以替换为下一行,好处是不需要假设客户的余额保存在类型为mapping的balances变量中,坏处是会额外增加很多不必要的前置检查,额外消耗gas
            //transfer(_tos[i], _values[i]);
        }

        return true;
    }

大家一定很关心,那么,我用transferMultiple一次实现一对一万转账行不行呢。
实际测试表明,一对四十转账的时候,大约消耗的gas在130万左右,而截止目前本文写作之时,以太坊一个区块的gas上限大约为800万,所以,大家不难看出,一次实现一对二百四十转账就差不多将区块的gas上限占满了。如果一次转账的收款对象数量太多,完全会因为超出区块gas上限而导致交易无法成功。

2016/2017款MacBook Pro扩展坞(或者说dongle life)选购指南

严重警告,我要给诸位一个警告,千万不要看了这篇文章
2017开门红:MacBook Pro Thunderbolt 解锁 DELL 戴尔 TB15 扩展坞 开箱使用_开箱晒物_什么值得买 就去买一个DELL TB16来配自己的MacBook Pro。因为TB16完全不兼容MacBook Pro。买来TB16的话,你就会发现你如果没有戴尔电脑,你完全就是浪费钱。

同时,不要认为我没有试过上文提到的解锁操作和tb3enabler等工具,我告诉大家,我早就试过了,根本没有用,因为TB15在mac上被识别为不兼容的设备,你用tb3enabler就能解锁了,而TB16在mac上不是被识别为不兼容的设备,而是根本找不到任何设备,诸位懂我的意思了吗。我就是买了个TB16后才发现,根本不能用在我的2017 MacBook Pro上。万幸的是,这台TB16本来是为我的Dell Precision 5520买的,所以我还没有浪费钱。

我找遍淘宝京东等地,最后发现,如果要一个dongle解决充电,USB-A,读卡器和视频输出的话,只有下面两种选择。
HyperDrive雷电3Thunderbolt3扩展坞MacBookPro转换器Type-c转usb-淘宝网
和这一款
Belkin 雷雳 3 Express HD 基座

其实,雷雳3扩展坞或者dongle,满足usbc充电,转USB-A,包含SD/Micro SD读卡器的很多。支持HDMI 4k 60hz, 那就选择少很多了,或者说应该找不到。上面两款,都是通过mini displayport和tb3输出支持4k@60hz,其中第二款Belkin根本就不提供HDMI输出。

如果搭配LG Ultrafine 5k显示器的话,那么,购买Belkin那一款就可以了,但是,如果你还有别的5k显示器,如Dell UP2715K的话,那么,你可以参考下面我的做法。

我个人建议,最好买一个tb3 -> 3 USB-A + SD/MicroSD+ tb3 +usbc的转换器,如绿联的这款
绿联Type-C转接头hdmi网线vga数据线转换器苹果华为mate10手机多功能拓扩展坞 HDMI款+千兆网卡+HUB【图片 价格 品牌 报价】-京东
或者
【绿联40873】绿联Type-C转HDMI/VGA转换器 USB-C扩展坞PD充电转接头数据线 苹果MacBook华为Mate10Pro拓展坞集线器40873【行情 报价 价格 评测】-京东

绿联的这两款的优点在于额外提供了千兆以太网,缺点在于HDMI输出最高只能到4k@30hz。个人建议买绿联的第二款,因为第二款同时有HDMI和VGA,适合你开会的时候投影用,平时外接显示器的话,就用我下面提到的方法。

购买戴尔的tb3 -> displayport线,这条线肯定兼容MacBook Pro,因为我自己就买了两条,用来将15寸的MacBook Pro 2017与我的戴尔UP2715K显示器相连接,并且成功的输出了5k分辨率。
戴尔DELL Thunderbolt3 Type-c USB-C转DisplayPort DP接口适配器

这样最实用,因为既可以接一台UP2715K 5k分辨率,也可以接两台4k显示器,而且由于用了Displayport,一定支持60hz刷新。同时,也不影响额外再用别的tb3继续外接一台LG Ultrafine 5k显示器。

解决问题:iOS模拟器无法联网了

今天在Xcode上用模拟器进行调试的时候,突然发现一个奇怪的问题,如果我用了iOS 8版本的模拟器,无论是我的应用还是Simulator中的Safari,都无法连接互联网。Safari报告的错误是:“Safari cannot open the page because the network connection was lost”。我如果用iOS 11的模拟器,就没有这个问题。

我通过百度,Bing等搜索引擎进行了查找,找到了如下信息:
Can’t use HTTP in iOS 8 simulator https://stackoverflow.com/questions/25654679/cant-use-http-in-ios-8-simulator

ios模拟器连不上网 https://www.2cto.com/os/201612/576530.html

xcode 模拟器无法连接网络 http://www.cocoachina.com/bbs/read.php?tid-226644-page-e-fpage-96.html

结果,以上的方案没有一个适合我的情况。

最后,我想起来,我安装了Avast Security,并且启用了Web Shield,我关闭了Web Shield的话,iOS 8模拟器就可以联网了。
如果你也遇到了类似的问题,如果常规的方法不能解决的话,不妨检查一下你是不是也安装了类似的安全软件。

NSDateFormatter性能优化

近期在某个项目上,遇到了一个NSDateFormatter的性能问题。

不知何故,app中需要把将近一千个字符串转换为NSDate类型,运行后发现,转换居然要600ms以上,实在是不可忍受。最开始,我发现代码是每次进行转换都新创建一个NSDateFormatter对象,既然如此,我就进行了第一步优化,创建并缓存了全局的NSDateFormatter对象。可是,再次运行后发现,这个优化居然没有效果!!!怎么回事,不是苹果自己的文档说的“Cache Formatters for Efficiency”吗?

我用Instrument对应用进行了Profile,发现,创建一个NSDateFormatter并不消耗太长时间,反而是stringFromDate和dateFromString这两个方法耗时间,相比之下,创建NSDateFormatter的时间完全可以忽略不计。

最后,发现NSDateFormatter性能实在是太差,只能用sqlite了。

        sqlite3_stmt *statement = NULL;
        sqlite3_prepare_v2(db, "SELECT strftime('%s', ?);", -1, &statement, NULL);
        
        sqlite3_bind_text(statement, 1, [str UTF8String], -1, SQLITE_STATIC);
        sqlite3_step(statement);
        sqlite3_int64 interval = sqlite3_column_int64(statement, 0);
        NSDate *date = [NSDate dateWithTimeIntervalSince1970:interval];

strftime可以将yyyy-MM-dd HH:mm:ss这样的字符串转换为timestamp,然后再根据timestamp创建NSDate对象,这样比直接dateFromString要快很多。

同样,stringFromDate性能也很差,我最后直接用

calendar = [NSCalendar calendarWithIdentifier:NSGregorianCalendar];

获取到NSDateComponents,然后用

[NSString stringWithFormat:@"%4ld-%02ld-%02ld", components.year, components.month, components.day];

这样的代码获取字符串的日期和时间。

经过Instrument的Profiling,我发现calendarWithIdentifier:这个方法才是真的消耗时间,所以我缓存了NSCalendar对象。

结论,优化NSDateFormatter的结果就是别用NSDateFormatter。

一个小问题Phabricator的部分界面无法本地化

最近我进行了一些Phabricator本地化的工作,在这个过程中,我发现Phabricator的一小部分界面始终无法翻译,即使我在PhabricatorCNChineseTranslation.php中添加了相应的翻译项。

这部分无法翻译的界面是下图中的”Tag”这样的文字。
"Tag"无法被翻译

Phabricator是通过pht函数实现本地化的,pht(‘User’)将会返回User的本地化翻译,如果没有可用的翻译,那么就会返回’User’自身。

如果我们深入Phabricator的代码,我们不难发现,上图中的”Tag”之所以无法翻译,是因为这个”Tag”来源于PhabricatorProjectIconSet类的如下代码。

private static function getIconSpecifications() {
  return PhabricatorEnv::getEnvConfig('projects.icons');
}

上述代码其实是从Phabricator的配置项projects.icons中载入”Tag”, “Project”等配置。
这个配置项其实就是一串JSON
projects.icons

不难看出,无论如何,载入projects.icons的时候,Phabricator不会对projects.icons这个配置项的内容进行任何pht操作,这也就导致”Tag”等文本不会被翻译了。

我在自己的本地代码中,对PhabricatorProjectIconSet进行了如下改动,即在getIconSpec和getIconName函数上加上pht的调用。这样就可以实现本地化翻译了。但是,这样的改动是不符合Phabricator的pht规范的,即传入给pht的参数必须是scala string value,而由于下面的代码的缘故,我们并不能保证pht($value)的$value是一个scala value,理论上也可能是一个array(projects.icons是一个可以任意修改的配置项,所以我们并不能保证$value是scala),所以,我的代码是不能通过arc lint的。

 public static function getIconName($key) {
    $spec = self::getIconSpec($key);
    return pht(idx($spec, 'name', null));
  }

 private static function getIconSpec($key) {
    $icons = self::getIconSpecifications();
    foreach ($icons as $icon) {
      if (idx($icon, 'key') === $key) {
        $spec_local = array();
        foreach ($icon as $key => $value) {
          if ($key == 'name') {
            $spec_local['name'] = pht($value);
          } else {
            $spec_local[$key] = $value;
          }
        }
        return $spec_local;
      }
    }

    return array();
  }

简而言之,上述的代码只是一个补丁,仍然不是最好的方案,但是至少能够保证翻译了。

resource fork, Finder information, or similar detritus not allowed

今天,我准备更新我的一个app,这个app很久以前上架的,当时还是iPhone 32位 CPU的时候,所以有必要更新一次。

然而,在编译过程中,我遇到了以下错误:

resource fork, Finder information, or similar detritus not allowed

经过一番搜索,找到了解决方案。

http://stackoverflow.com/questions/39652867/code-sign-error-in-macos-sierra-xcode-8-resource-fork-finder-information-or

https://developer.apple.com/library/content/qa/qa1940/_index.html

原来,MacOS的文件有三个fork,data fork, resource fork和Finder info。Data fork存储了文件的内容;resource fork保存了一些扩展信息,如什么应用创建了这个文件,又例如上次你打开这个txt文件的时候,正在显示的是第几行,等等; Finder Info则保存了文件所有者,创建者等信息。

从iOS 10和macOS Sierra开始,从安全考虑,app bundle中的文件将不能包含resource for和Finder info了。所以,我们必须去掉这两个信息,才能成功进行代码签名。具体如何去掉这两块信息,大家可以参考下面的说明。

40
Code signing fails with error 'resource fork, Finder information, or similar detritus not allowed'

Q:  When I build my app, code signing fails with the error "resource fork, Finder information, or similar detritus not allowed." What does this mean and what should I do about it?

A: This is a security hardening change that was introduced with iOS 10, macOS Sierra, watchOS 3, and tvOS 10.

Code signing no longer allows any file in an app bundle to have an extended attribute containing a resource fork or Finder info.

To see which files are causing this error, run this command in Terminal:

$ xattr -lr 

replacing  with the path to your actual app bundle.

Here's an example of this command in action:

$ xattr -lr Foo.app
/Applications/Foo.app: com.apple.FinderInfo:
00000000  00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  |................|
You can also remove all extended attributes from your app bundle with the xattr command:

$ xattr -cr 

Note that browsing files within a bundle with Finder's Show Package Contents command can cause Finder info to be added to those files. Otherwise, audit your build process to see where the extended attributes are being added

更新到cocoapods 1.2.1了

今天准备更新到最新的cocoapods。我在mac上执行了遇到了一系列错误

$ sudo gem install cocoapods
ERROR: While executing gem ... (Gem::DependencyError)
Unable to resolve dependencies: cocoapods requires cocoapods-core (= 1.2.1), claide (< 2.0, >= 1.0.1), cocoapods-downloader (< 2.0, >= 1.1.3), cocoapods-trunk (< 2.0, >= 1.2.0), molinillo (~> 0.5.7), xcodeproj (< 2.0, >= 1.4.4), colored2 (~> 3.1), fourflusher (~> 2.0.1), ruby-macho (~> 1.1)

随后,我只能逐个执行gem install安装上述错误信息提供的依赖,结果,这样就安装成功了。似乎rubygems没有成功解决依赖,让我手动解决了????

$ sudo gem install cocoapods-core
Fetching: cocoapods-core-1.2.1.gem (100%)
Successfully installed cocoapods-core-1.2.1
Parsing documentation for cocoapods-core-1.2.1
Installing ri documentation for cocoapods-core-1.2.1
Done installing documentation for cocoapods-core after 1 seconds
1 gem installed


$ sudo gem install claide
Fetching: claide-1.0.1.gem (100%)
Successfully installed claide-1.0.1
Parsing documentation for claide-1.0.1
Installing ri documentation for claide-1.0.1
Done installing documentation for claide after 0 seconds
1 gem installed


$ sudo gem install cocoapods-downloader
Fetching: cocoapods-downloader-1.1.3.gem (100%)
Successfully installed cocoapods-downloader-1.1.3
Parsing documentation for cocoapods-downloader-1.1.3
Installing ri documentation for cocoapods-downloader-1.1.3
Done installing documentation for cocoapods-downloader after 0 seconds
1 gem installed

$ sudo gem install cocoapods-trunk
Fetching: cocoapods-trunk-1.2.0.gem (100%)
Successfully installed cocoapods-trunk-1.2.0
Parsing documentation for cocoapods-trunk-1.2.0
Installing ri documentation for cocoapods-trunk-1.2.0
Done installing documentation for cocoapods-trunk after 0 seconds
1 gem installed

$ sudo gem install molinillo
Fetching: molinillo-0.5.7.gem (100%)
Successfully installed molinillo-0.5.7
Parsing documentation for molinillo-0.5.7
Installing ri documentation for molinillo-0.5.7
Done installing documentation for molinillo after 0 seconds
1 gem installed

$ sudo gem install xcodeproj
ERROR: While executing gem ... (Gem::DependencyError)
Unable to resolve dependencies: xcodeproj requires colored2 (~> 3.1), nanaimo (~> 0.2.3)

$ sudo gem install colored2
Fetching: colored2-3.1.2.gem (100%)
Successfully installed colored2-3.1.2
Parsing documentation for colored2-3.1.2
Installing ri documentation for colored2-3.1.2
Done installing documentation for colored2 after 0 seconds
1 gem installed
mbp2:Caches jimmy$ sudo gem install nanaimo
Fetching: nanaimo-0.2.3.gem (100%)
Successfully installed nanaimo-0.2.3
Parsing documentation for nanaimo-0.2.3
Installing ri documentation for nanaimo-0.2.3
Done installing documentation for nanaimo after 0 seconds
1 gem installed

$ sudo gem install molinillo
Successfully installed molinillo-0.5.7
Parsing documentation for molinillo-0.5.7
Done installing documentation for molinillo after 0 seconds
1 gem installed

$ sudo gem install fourflusher
Fetching: fourflusher-2.0.1.gem (100%)
Successfully installed fourflusher-2.0.1
Parsing documentation for fourflusher-2.0.1
Installing ri documentation for fourflusher-2.0.1
Done installing documentation for fourflusher after 0 seconds
1 gem installed
mbp2:Caches jimmy$ sudo gem install ruby-macho
Fetching: ruby-macho-1.1.0.gem (100%)
Successfully installed ruby-macho-1.1.0
Parsing documentation for ruby-macho-1.1.0
Installing ri documentation for ruby-macho-1.1.0
Done installing documentation for ruby-macho after 0 seconds
1 gem installed

iOS/Android的WebView中用file input支持拍照或选择相册的照片

如果我们的一个移动端的网页需要让用户上传一张照片,那么,通常而言,我们可以写了以下一段HTML代码

<input type='file' />

那么Mobile Web界面将会显示下面的一个控件,通过该控件,用户可以拍照或者选择手机中的文件而上传。

在iOS下,Mobile Safari会在你点击上面的控件之后弹出如下界面
Screen Shot 2017-03-12 at 下午6.44.31

在Android系统下,弹出的界面根据具体的Android版本(不同的厂商定制版本、不同的Android系统版本)不同而略有不同,不过,通常也会提供相机,相册等选项。下图为小米手机的例子。
IMG_20170312_184934

然而,如果你在原生的应用中内嵌了一个UIWebView/WKWebView(iOS)或者WebView(Android),你就会发现,原来在Mobile Safari和Chrome中可以正常运行的代码很有可能不能正常工作了。

在iOS系统下,你会发现,如果UIWebView所在的View Controller是通过presentViewController展示的,那么,你会发现,用户点击 后,你的iOS应用会出现一些奇怪的问题,如应用崩溃,如你在相册选择了照片之后,UIWebView所在的View Controller不见了,等等。

最后,我的解决方法是,不通过presentViewController展示UIWebView所在的View Controller,而是通过UINavigationController的pushViewController去展示View Controller。

而对于Android系统,应用内嵌的WebView本来就不支持使用文件上传功能,所以,<input type=’file’ />在Mobile Chrome上有效,到了应用内嵌的WebView中就无效了。

在Android上,我的解决方法是这样的,通过WebView的addJavascriptInterface注入一个camera对象,让WebView中的js代码通过camera对象从而唤起相机或者相册。

webView.addJavascriptInterface(new DemoJavaScriptInterface(), “demo”);

Javascript代码

window.demo.camera();

假设DemoJavaScriptInterface的是位于WebViewActivity里面的一个内部类,且WebViewActivity有一个私有变量String mCameraPhotoPath,那么,DemoJavaScriptInterface代码如下

 final class DemoJavaScriptInterface {
        DemoJavaScriptInterface() {
        }

        @JavascriptInterface
        public void camera() {

                Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
                    // Create the File where the photo should go
                    File photoFile = null;
                    try {
                        photoFile = createImageFile();
                        takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath);
                    } catch (Exception ex) {
                        // Error occurred while creating the File
                        Log.e("WebViewSetting", "Unable to create Image File", ex);
                    }

                    // Continue only if the File was successfully created
                    if (photoFile != null) {
                        mCameraPhotoPath = photoFile.getAbsolutePath();
                        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                                Uri.fromFile(photoFile));
                    } else {
                        takePictureIntent = null;
                    }
                }

                Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
                contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
                contentSelectionIntent.setType("image/*");

                Intent[] intentArray;
                if (takePictureIntent != null) {
                    intentArray = new Intent[]{takePictureIntent};
                } else {
                    intentArray = new Intent[0];
                }

                chooserIntent = new Intent(Intent.ACTION_CHOOSER);
                chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
                chooserIntent.putExtra(Intent.EXTRA_TITLE, "拍照或者选择图片");
                chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);

                Activity mContext = WebViewActivity.this;
                if (Build.VERSION.SDK_INT >= 23) {
                    int checkCallPhonePermission = ContextCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA);
                    if (checkCallPhonePermission != PackageManager.PERMISSION_GRANTED) {
                        ActivityCompat.requestPermissions(mContext, new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_ASK_CAMERA);
                        return;
                    }
                }

                WebViewActivity.this.startActivityForResult(chooserIntent, 101);

            }

    }

WebViewActivity中的onActivityResult方法代码如下

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {
            switch (requestCode) {
                case 101:
                    if(data == null){
                        String imageurl = mCameraPhotoPath;
                        //相机拍好的照片就保存在路径imageurl中
                    }
                    else{
                        Uri uri = data.getData();
                        //通过uri获取照片数据
                    }

                    break;

                default:
                    return;
            }
        }

    }