Google 支付真的不难,难的是由于“你懂的”的国内网络环境导致的复杂的测试流程和 Google Console “天花烂缀”的各种配置项目还有那些“文不达意”的报错信息。
很早写过一篇《Google Play In-app Billing 踩过的那些坑》,通过网站数据统计发现这篇文章的搜索和阅读量是最大的,后来又通过这篇文章的评论还有其他渠道和不少朋友们交流关于接入 Google 支付时遇到的问题发现大家对于这块儿还是有很多“心虚”的地方,还有一些“坑”我之前也没有说明白,所以我觉得有必要再写一篇来说说 Google 支付。
先吃两颗定心丸
有两件事一定要知道:
Google 支付很简单,一点儿都不难,所以不要头疼,不好害怕,不要压力山大。
当你写好代码完成接入准备测试 Google 支付时,只要顺利弹出了 Google 支付相关的 UI 界面,哪怕是报错提示信息,千万不要怀疑你的代码,只要弹出 UI 界面,你的代码就是对的,问题出在配置上。
具体如何接入 Google 支付不是这篇文章的主旨,所以就不做详细的介绍了。因为 Google 支付接入真的非常简单。你可以仔细阅读《官方文档》或是我之前写的那篇《Google Play In-app Billing 踩过的那些坑》再或者干脆阅读 Android SDK 附带的 Google 支付的 Sample 示例工程的源代码都可以帮你快速轻松的完成 Google 支付的编码接入过程。
封闭测试时,除了要将测试人员的 Google Play 帐号加入封闭测试人员列表,还要让拥有这些测试帐号的人员通过访问生成的特殊链接来确认加入测试列表。
测试支付是不会真的扣除你的任何费用的,但是即便如此你的测试 Google Play 帐号上还是需要绑定一张有国际支付能力的信用卡或银行卡的。
这里重点说说封闭测试,相对于开放性测试封闭测试的流程稍微复杂一些。首先如图所示:
首先要将想要参与测试的 Google Play 帐号加入到测试人员列表中。这样只有加入到列表中的 Google Play 帐号才能够测试 Google 支付。
这里最需要注意也是最容易被忽略的是在将测试人员的 Google Play 帐号加入到测试人员列表后,一定要记得将下面那个生成好的链接发给参与测试的人员,让他们用浏览器打开这个链接,只有这样测试人员才真的加入了测试列表,才可以真的进行 Google 支付测试。否则在进行支付测试时你将得到无法购买您要买的商品错误提示。
最后
只要避开上面这些“坑”,你会发现其实 Google 支付真的很简单。如果你不幸掉到其他的“坑”里面了,欢迎分享给我,我们一起填坑。
上面的按钮布局代码只是一部分,具体关闭按钮的位置摆放方法我们放在后面来讲。用 XML 实现关闭按钮时并没有很好的处理上面白色的叉子的画法,当时只是“简单粗暴”的使用了按钮文字来解决,用了一个白色的大写 X 来作为关闭按钮上的叉子图形。不过接下来我们看到使用纯 Java 代码来实现时这里得到了完美的解决。
系统选择了最官方的 Raspbian 不过用的是 Lite 版本,还是因为不需要图形界面嘛,所以就不要桌面支持了。下载的是个 ZIP 包,解压之后是个 img 镜像,我是 Mac OS X ,直接用 diskutil + dd 命令将系统刷入 SD 卡,这一步树莓派官方网站的文档写的很清楚,操作也简单。
1、将 SD 卡插入电脑,运行 diskutil list 显示出目前已挂在的磁盘。这里假设你的 SD 卡的磁盘 ID 是 disk4 即 /dev/disk4
这个挺有意思。如果你对隐私什么的没有额外的洁癖,可以打开这个选项。它会将你的树莓派的地理位置和其他全世界使用树莓派的小伙伴们标记在 Google Map 上面,并可以通过 rastrack.co.uk 这个网站查看。
使用无线网卡
网线大大限制了树莓派的便捷性,给树莓派配上个 USB 的无线网卡就舒服多了。对于无线网卡的选择建议你千万不要盲目,看一下树莓派的硬件兼容列表再下单也不迟,否则买到不兼容的硬件就呵呵了(我第一次给树莓派购买的 SD 卡就因为不兼容而呵呵了)。我使用的是 EDUP EP-N8508GS黄金版 迷你USB无线网卡
插上你购买的 USB 无线网卡,通过运行命令 sudo lsusb 来查看,如果你看到
1234
Bus 001 Device 004: ID 0bda:8176 Realtek Semiconductor Corp. RTL8188CUS 802.11n WLAN Adapter
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp. LAN9500 Ethernet 10/100 Adapter / SMSC9512/9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
能找到标有 802.11n WLAN Adapter 字样,或者运行命令
1
ifconfig -a
能看到标有 wlan0 字样,那么恭喜你,说明你的 USB 无线网卡是可用的了。下面我们在做配置。首先是 /etc/network/interfaces 文件
1234567891011
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
auto wlan0
allow-hotplug wlan0
iface wlan0 inet dhcp
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
iface default inet dhcp
通过刚刚的试验我们知道只要引入资源的路径顺序不变,主工程和依赖库工程所生成的 R.java 文件中资源 ID 的值是一样的。所以在开发依赖库工程时,在依赖库工程中通过自己的 R 类来引用资源写好的逻辑代码在主工程中同样是可以正常工作的。
【集成方式2】依赖库通过复制粘贴进主工程的方式集成
如果我们开发的库要发布给第三方使用的时候,一般会将代码混淆打包成 jar 文件连同资源文件一起发送给第三方。第三方拿到这样形式的 SDK 后一般会通过复制粘贴的方式将资源文件拷贝进主工程的 res 或 assets 文件夹内,将 jar 包拷贝进主工程的 libs 文件夹内。
这时由于库使用的资源文件是直接放置在主工程中的,所以最终生成的资源 ID 是不同的。存在于第三方库 jar 包中的库逻辑如果还是按照 R 类来引用资源的话就不行了。因为库逻辑是不可能预先知道主工程的包名的,这时只能通过 Java 的“反射”机制来进行资源引用了。举个例子:
1234567891011121314151617181920212223
privateintgetResId(StringresType,StringresName){try{ClasslocalClass=Class.forName(getPackageName()+".R$"+resType);FieldlocalField=localClass.getField(resName);returnInteger.parseInt(localField.get(localField.getName()).toString());}catch(ClassNotFoundExceptione){// TODO Auto-generated catch blocke.printStackTrace();}catch(NoSuchFieldExceptione){// TODO Auto-generated catch blocke.printStackTrace();}catch(NumberFormatExceptione){// TODO Auto-generated catch blocke.printStackTrace();}catch(IllegalAccessExceptione){// TODO Auto-generated catch blocke.printStackTrace();}catch(IllegalArgumentExceptione){// TODO Auto-generated catch blocke.printStackTrace();}return0;}
.ld 文件是链接器配置文件或者叫链接脚本,它有自己的一套语法,链接器最终会根据链接器配置文件中的规则来生成最终的二进制文件。这里我们就不做具体的语法介绍了,有兴趣的同学请自行 Google 吧,我们只解释一下几个关键点
12345678910111213141516171819202122
/* Simple linker script for the JOS kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
SECTIONS
{
/* Link the kernel at this address: "." means the current address */
/* Must be equal to KERNLINK */
. = 0x80100000;
.text : AT(0x100000) {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
/* Adjust the address for the data segment to the next page */
. = ALIGN(0x1000);
// ......
}
#include "asm.h"#include "memlayout.h"#include "mmu.h"#include "param.h"# Multiboot header. Data to direct multiboot loader..p2align2.text.globlmultiboot_headermultiboot_header:#definemagic0x1badb002#defineflags0.longmagic.longflags.long(-magic-flags)# By convention, the _start symbol specifies the ELF entry point.# Since we haven't set up virtual memory yet, our entry point is# the physical address of 'entry'..globl_start_start=V2P_WO(entry)# Entering xv6 on boot processor, with paging off..globlentryentry:#Turnonpagesizeextensionfor4Mbytepagesmovl%cr4,%eaxorl$(CR4_PSE),%eaxmovl%eax,%cr4#Setpagedirectorymovl$(V2P_WO(entrypgdir)),%eaxmovl%eax,%cr3#Turnonpaging.movl%cr0,%eaxorl$(CR0_PG|CR0_WP),%eaxmovl%eax,%cr0#Setupthestackpointer.movl$(stack+KSTACKSIZE),%esp#Jumptomain(),andswitchtoexecutingat#highaddresses.Theindirectcallisneededbecause#theassemblerproducesaPC-relativeinstruction#foradirectjump.mov$main,%eaxjmp*%eax.commstack,KSTACKSIZE
// Boot page table used in entry.S and entryother.S.// Page directories (and page tables), must start on a page boundary,// hence the "__aligned__" attribute. // Use PTE_PS in page directory entry to enable 4Mbyte pages.__attribute__((__aligned__(PGSIZE)))pde_tentrypgdir[NPDENTRIES]={// Map VA's [0, 4MB) to PA's [0, 4MB)[0]=(0)|PTE_P|PTE_W|PTE_PS,// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)[KERNBASE>>PDXSHIFT]=(0)|PTE_P|PTE_W|PTE_PS,};//PAGEBREAK!// Blank page.
// Bootstrap processor starts running C code here.// Allocate a real stack and switch to it, first// doing some setup required for memory allocator to work.intmain(void){kinit1(end,P2V(4*1024*1024));// phys page allocatorkvmalloc();// kernel page tablempinit();// collect info about this machinelapicinit();seginit();// set up segmentscprintf("\ncpu%d: starting xv6\n\n",cpu->id);picinit();// interrupt controllerioapicinit();// another interrupt controllerconsoleinit();// I/O devices & their interruptsuartinit();// serial portpinit();// process tabletvinit();// trap vectorsbinit();// buffer cachefileinit();// file tableiinit();// inode cacheideinit();// diskif(!ismp)timerinit();// uniprocessor timerstartothers();// start other processorskinit2(P2V(4*1024*1024),P2V(PHYSTOP));// must come after startothers()userinit();// first user process// Finish setting up this processor in mpmain.mpmain();}
至此我们已经了解一台 PC 从加电启动开始如何从实模式到保护模式、内存寻址如何从分段式到分页式,启动方式如何从 BIOS 到引导区程序再从引导区程序加载内核到内存中运行。
#!/bin/bash## Copyright (c) 2013 Claudiu-Vlad Ursache <claudiu@cvursache.com># MIT License (see LICENSE.md file)## Based on work by Felix Schulze:## Automatic build script for libssl and libcrypto # for iPhoneOS and iPhoneSimulator## Created by Felix Schulze on 16.12.10.# Copyright 2010 Felix Schulze. All rights reserved.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.# 当执行时使用到未定义过的变量,则显示错误信息set -u
# Setup architectures, library name and other vars + cleanup from previous runs# 四个平台架构标识ARCHS=("arm64""armv7s""armv7""i386")# 四个平台架构分别对应的 SDK 名称SDKS=("iphoneos""iphoneos""iphoneos""macosx")# 使用的 OpenSSL 库版本LIB_NAME="openssl-1.0.2c"# 临时输出目录TEMP_LIB_PATH="/tmp/${LIB_NAME}"LIB_DEST_DIR="lib"HEADER_DEST_DIR="include"rm -rf "${HEADER_DEST_DIR}""${LIB_DEST_DIR}""${TEMP_LIB_PATH}*""${LIB_NAME}"# Unarchive library, then configure and make for specified architectures# 编译静态链接库的函数configure_make(){ARCH=$1; GCC=$2; SDK_PATH=$3;
LOG_FILE="${TEMP_LIB_PATH}-${ARCH}.log" tar xfz "${LIB_NAME}.tar.gz"pushd .; cd"${LIB_NAME}";
./Configure BSD-generic32 --openssldir="${TEMP_LIB_PATH}-${ARCH}" &> "${LOG_FILE}" make CC="${GCC} -arch ${ARCH}"CFLAG="-isysroot ${SDK_PATH}" &> "${LOG_FILE}";
make install &> "${LOG_FILE}";
popd; rm -rf "${LIB_NAME}";
}# 分别开始编译四个平台架构的静态链接库for((i=0; i < ${#ARCHS[@]}; i++))do# 获取 SDK 路径SDK_PATH=$(xcrun -sdk ${SDKS[i]} --show-sdk-path)# 过去 gcc 编译器路径GCC=$(xcrun -sdk ${SDKS[i]} -find gcc)# 编译 configure_make "${ARCHS[i]}""${GCC}""${SDK_PATH}"done# Combine libraries for different architectures into one# Use .a files from the temp directory by providing relative paths# 通过 lipo 命令将四个平台架构的静态库打包成一个静态库create_lib(){LIB_SRC=$1; LIB_DST=$2;
LIB_PATHS=("${ARCHS[@]/#/${TEMP_LIB_PATH}-}")LIB_PATHS=("${LIB_PATHS[@]/%//${LIB_SRC}}") lipo ${LIB_PATHS[@]} -create -output "${LIB_DST}"}mkdir "${LIB_DEST_DIR}";
create_lib "lib/libcrypto.a""${LIB_DEST_DIR}/libcrypto.a"create_lib "lib/libssl.a""${LIB_DEST_DIR}/libssl.a"# Copy header files + final cleanupsmkdir -p "${HEADER_DEST_DIR}"cp -R "${TEMP_LIB_PATH}-${ARCHS[0]}/include""${HEADER_DEST_DIR}"rm -rf "${TEMP_LIB_PATH}-*""{LIB_NAME}"
1F0 - 数据寄存器。读写数据都必须通过这个寄存器
1F1 - 错误寄存器,每一位代表一类错误。全零表示操作成功。
1F2 - 扇区计数。这里面存放你要操作的扇区数量
1F3 - 扇区LBA地址的0-7位
1F4 - 扇区LBA地址的8-15位
1F5 - 扇区LBA地址的16-23位
1F6 (低4位) - 扇区LBA地址的24-27位
1F6 (第4位) - 0表示选择主盘,1表示选择从盘
1F6 (5-7位) - 必须为1
1F7 (写) - 命令寄存器
1F7 (读) - 状态寄存器
bit 7 = 1 控制器忙
bit 6 = 1 驱动器就绪
bit 5 = 1 设备错误
bit 4 N/A
bit 3 = 1 扇区缓冲区错误
bit 2 = 1 磁盘已被读校验
bit 1 N/A
bit 0 = 1 上一次命令执行失败
稍后讲到从硬盘加载内核到内存时我们再通过 xv6 的实际代码来看看硬盘操作的具体实现。
ELF文件格式
在 Wiki 百科上有 ELF 文件格式的详细解释,简单的说 ELF 文件格式是 Linux 下可执行文件的标准格式。就好像 Windows 操作系统里的可执行文件 .exe 一样(当然,Windows 里的可执行文件的标准格式叫 PE 文件格式),Linux 操作系统里的可执行文件也有它自己的格式。只有按照文件标准格式组织好的可执行文件操作系统才知道如何加载运行它。我们并使使用 C 语言按照教科书写出的 HelloWorld 代码在 Linux 环境下最终通过编译器(gcc等)编译出的可以运行的程序就是 ELF 文件格式的。
voidreadseg(uchar*pa,uintcount,uintoffset)// 0x10000, 4096(0x1000), 0{uchar*epa;epa=pa+count;// 0x11000// 根据扇区大小 512 字节做对齐pa-=offset%SECTSIZE;// bootblock 引导区在第一扇区(下标为 0),内核在第二个扇区(下标为 1)// 这里做 +1 操作是统一略过引导区offset=(offset/SECTSIZE)+1;// If this is too slow, we could read lots of sectors at a time.// We'd write more to memory than asked, but it doesn't matter --// we load in increasing order.// 一次读取一个扇区 512 字节的数据for(;pa<epa;pa+=SECTSIZE,offset++)readsect(pa,offset);}
Gprof is a performance analysis tool for Unix applications. It uses a hybrid of instrumentation and sampling[1] and was created as extended version of the older “prof” tool. Unlike prof, gprof is capable of limited call graph collecting and printing.
因为 Android 本来就是基于 Linux 的,所以这里用 gprof 来做性能测试是没什么问题的。不过需要注意的是,这里所说的性能测试是针对 NDK 编译的 C++ 代码的。就想 Cocos2d-x 这样的 C++ 实现的游戏引擎就可以通过 gprof 来分析。下面我们来说说搞法。
环境
我是 Mac OS X 下,这里要做性能分析的 Cocos2d-x 项目是基于 Cocos2d-x 3.2 引擎,项目本身是基于 Lua 脚本编写的。其实这些都无关紧要,只不过是编译出的 so 文件有所不同罢了。只要是 NDK 的代码都可以用 gprof 来做性能分析的。
Undefined symbols for architecture i386:
"_GCControllerDidConnectNotification", referenced from:
-[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o)
"_GCControllerDidDisconnectNotification", referenced from:
-[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o)
"_OBJC_CLASS_$_GCController", referenced from:
objc-class-ref in libcocos2dx iOS.a(CCController-iOS.o)
(maybe you meant: _OBJC_CLASS_$_GCControllerConnectionEventHandler)
无论是设置成-ObjC还是-all_load编译都会失败,都会报上述找不到符号的链接错误。
正确的解决办法
这里先给出正确的解决办法再谈谈为什么要这么做。正确的做法还是设置 Other Linker Flags 这个编译选项,只不过即不用用-ObjC也不能用-all_load,而是要用-force_load path/to/your/libWeiboSDK.a,后面跟的是新浪微博 SDK 静态链接库的确切位置。
这一切是为什么?
从编译链接说起
这里不打算过多的介绍编译链接相关的只是,但是强烈推荐一本书《程序员的自我修养》,光看正标题你可能会担心这是本没什么“正经”内容的书,至少我当初第一次看到这书名的时候就是这么认为的,但是我错了,这本书的副标题是链接、装载与库。相信我,看过这本书 N 遍之后你自会对程序从源代码编译链接到生成二进制程序的原理和过程有一个非常透彻的理解,并且更重要的是看过这本书 N 遍之后你会上升几个层次。
The dynamic nature of Objective-C complicates things slightly. Because the code that implements a method is not determined until the method is actually called,
For example, if main.m includes the code [[FooClass alloc] initWithBar:nil]; then main.o will contain an undefined symbol for FooClass, but no linker symbols for the -initWithBar: method will be in main.o
WBSDKJSONKit.o:
00007ba0 t -[NSArray(WBSDKJSONKitSerializing) weibosdk_WBSDKJSONString]
00007de8 t -[NSDictionary(WBSDKJSONKitSerializing) weibosdk_WBSDKJSONString]
000079cd t -[NSString(WBSDKJSONKitSerializing) weibosdk_WBSDKJSONString]
这就可以说明新浪微博 SDK 确实使用了分类技术扩展了NSArray、NSDictionary和NSString三个 Foundation Framework 下面的类的行为。好,现在可以真相大白了:
Passing the -ObjC option to the linker causes it to load all members of static libraries that implement any Objective-C class or category. This will pickup any category method implementations. But it can make the resulting executable larger, and may pickup unnecessary objects. For this reason it is not on by default.