• UID10
  • 登录2016-10-31
  • 粉丝38
  • 关注4
  • 发帖386
  • 主页
  • 金币2541枚
社区居民
忠实会员
原创写手
bill 发布于2015-08-08 10:53
15/12479

基于Android支付宝支付设计和开发方案

楼层直达
背景
     在移动支付领域,支付宝支付占用巨大份额,根据艾瑞咨询公布的报告数据:2014Q3,支付宝斩获了82.6%的市场份额,在移动支付的霸主地位越来越稳固。财付通支付的发力点在微信支付和手Q支付,在移动支付格局中取得了10.0%的市场份额,排名第二。

     支付宝在移动支付领域的统治地位,使得我们有必要梳理支付宝移动开发流程。本文写作的目的就是梳理支付流程,从架构层面讲述如何在移动应用中嵌入支付宝支付功能,以及指出哪些地方存在开发陷阱。

准备
     首先,支付宝SDK下载主页的地址是:https://b.alipay.com/order/productDetail.htm?productId=2013080604609654&tabId=4#ps-tabinfo-hash。这个地址隐藏很深,所以这里有必要指出。

     按照说明,首先需要申请支付宝支付账号。这方面根据网站说明进行申请即可。一般需要2周左右的时间批准下来。

申请成功后账号信息包括 合作者身份ID partner, 卖家支付宝账号 seller_id,以及私钥 privateKey等。这三项将用于开发过程。

     在官网上下载移动支付集成开发包。解压后, 发现其下包括三个文件夹(在英文Mac系统下文件名显示为乱码):
  • “商户接入支付宝收银台界面展示标准”:讲的是如何使用支付宝Logo。
  • “支付宝钱包支付接口开发包2.0标准版”:用于支付,包括客户端和服务器端开发。
  • “即时到账批量退款有密接口refund_fastpay_by_platform_pwd”:用于到账及批量退款,只需要服务器端操作处理。
后两个文件夹,都包括4方面内容:接口文档,接入与使用规则,demo代码,以及版本更新说明。
架构设计
首先,对于一个实际的App应用而言,可能会包括多种支付方式,因此可以采用设计模式中的策略Strategy模式来设计支付功能模块,支付宝支付作为其中的一个策略,pay方法是支付算法。
如果除了支付方式payment method变化,订单order也可能会有不同的形式,如格式可能不同,有些支持可退款,有的不允许退款等,在这种多维度可变的情况下,支付模块的架构可以基于桥接模式。
其次,可以把支付宝支付的各个操作步骤,比如获取订单号,生成订单数据,进行支付,获取支付结果,处理异常等操作,根据状态进行划分。这样采用状态模式,提供设计的灵活性和扩展性。另外也可以设计状态机进行统一的状态切换管理。下面为参考代码:
public class PayStateMachine {
    /* all possible state of payment */
    public enum PayState { PAY_INIT, PAY_GOT_CONTEXT, PAY_UPDATED_ORDER, PAY_APPLIED_
      ID, PAY_ORDER_CREATED, PAY_SUCCEED, ERROR_OCCURRED}
         
         
    /* errors may occurred during payment */
    public enum PayError {
        PAY_GET_CONTEXT_FAIL, PAY_UPDATE_ORDER_FAIL, PAY_APPLY_ID_FAIL, PAY_FAIL
    }
         
         
    private static PayStateMachine instance;
    private PayState state;
    private IOrder order;
    private IPayment payment;
         
         
    private PayStateMachine() {
    }
         
         
    public static PayStateMachine getInstance() {
        if (instance == null) {
            instance = new PayStateMachine();
        }
         
         
        return instance;
    }
         
         
    public void initPayment(IOrder order, IPayment payment) {
        this.order = order;
        this.payment = payment;
        this.state = PayState.PAY_INIT;
    }
         
         
    public void startPay() {
        changeState(PayState.PAY_INIT);
    }
         
         
    public void changeState(PayState state) {
        onStateChanged(this.state, state);
    }
         
         
    public void reportError(PayError error, String detail) {
        LogUtil.printPayLog("the error id is:" + error + " " + detail);
        changeState(PayState.ERROR_OCCURRED);
    }
         
         
    private void onStateChanged(PayState oldState, PayState newState) {
        LogUtil.printPayLog("oid state:" + oldState + " new state:" + newState);
        this.state = newState;
         
        handlePayStateChange();
    }
         
         
    private void handlePayStateChange() {
        if (this.order == null || this.payment == null) {
            LogUtil.printPayLog("Have not initiated payment");
            return;
        }
         
         
        switch (this.state) {
            case PAY_INIT:
                order.getPayContext();
                break;
            case PAY_GOT_CONTEXT:
                order.createOrder();
                break;
            case PAY_UPDATED_ORDER:
            case PAY_APPLIED_ID:
            case PAY_ORDER_CREATED:
                payment.pay(order);
                break;
         
         
            case PAY_SUCCEED:
            case ERROR_OCCURRED:
                finishProcess();
                break;
            default:
                LogUtil.printPayLog("state is not correct!");
                finishProcess();
         }
    }
         
         
    private void finishProcess() {
        this.order = null;
        this.payment = null;
        this.state = PayState.PAY_INIT;
    }
}
最后,订单类层次可以参考模板模式来设计,例如抽象基类负责定义订单的操作框架和流程,具体订单数据的生成延迟到子类中实现。
具体实现参考附件源码。
支付流程
本文针对Android版进行讲解主要的支付流程。IOS版流程类似。
从操作角度看支付流程:

图片:flow.png




操作2(调用支付接口)和操作7(接口返回支付结果):App与支付宝API的交互。
操作5(异步发送支付通知):支付宝服务器与App后台的交互。
从数据流角度看支付流程:

图片:data_flow.png




客户端实现
本文结合操作流程和数据流程,讲述主要的实现方案。
首先假设订单数据都已经存储在OrderPayModel中。
第一步:App客户端访问应用服务器,后者生成订单编号并返回客户端。

private void getOrderIdRequest() {
  JSONObject ob = new JSONObject();
  ob.put("amount", orderPayModel.getOrderPriceTotal());
              
  ob.put("productDescription", orderPayModel.getOrderName());
              
              
  ob.put("userId", orderPayModel.getUserId());
  ob.put("barCoupon", orderPayModel.getOrderId());
  ob.put("barId", orderPayModel.getBarId());
  ob.put("count", orderPayModel.getOrderNums());
  LogUtil.printPayLog("get order id request data:"
    + orderPayModel.toString());
              
              
  HttpRequestFactory.getInstance().doPostRequest(Urls.ALI_PAY_APPLY, ob,
    new AsyncHttpResponseHandler() {
              
              
     @Override
     public void onSuccess(String content) {
      super.onSuccess(content);
              
              
      LogUtil.printPayLog("get order id request is handled");
              
              
      PayNewOrderModel rm = new PayNewOrderModel();
      rm = JSON.parseObject(content, PayNewOrderModel.class);
              
              
      if (rm.getCode() != null
        && "200".equalsIgnoreCase(rm.getCode())) {
       tradeNo = rm.getResult().getTrade_no();
       LogUtil.printPayLog("succeed to get order id:"
         + tradeNo);
              
              
       orderStr = generateOrder();
       PayStateMachine.getInstance().changeState(
         PayState.PAY_APPLIED_ID);
              
              
      } else {
       PayStateMachine.getInstance().reportError(
         PayError.PAY_APPLY_ID_FAIL,
         "code is not right");
      }
     }
              
              
     @Override
     public void onFailure(Throwable error, String content) {
      PayStateMachine.getInstance().reportError(
        PayError.PAY_APPLY_ID_FAIL,
        "failed to get order id");
              
              
     };
              
              
     @Override
     public void onFinish() {
      LogUtil.LogDebug("Payment", "on get order id finish",
        null);
              
              
     };
    });
 }

第二步:组装订单数据,包括以下几个子步骤:
  • 创建订单数据。
    private String getOrderInfo(String partner, String seller) {
      String orderInfo;
                  
       // 合作者身份ID
       orderInfo = "partner=" + "\"" + partner + "\"";
                  
                  
       // 卖家支付宝账号
       orderInfo += "&seller_id=" + "\"" + seller + "\"";
                  
                  
       // 商户网站唯一订单号
       orderInfo += "&out_trade_no=" + "\"" + tradeNo + "\"";
                  
                  
       // 商品名称
       orderInfo += "&subject=" + "\"" + orderName + "\"";
                  
                  
       // 商品详情
       orderInfo += "&body=" + "\"" + orderDetail + "\"";
                  
                  
       // 商品金额
       orderInfo += "&total_fee=" + "\"" + totalPrice + "\"";
       // orderInfo += "&total_fee=" + "\"" + "0.01" + "\"";
                  
                  
       // 服务器异步通知页面路径
       orderInfo += "¬ify_url=" + "\"" + Urls.ALI_PAY_NOTIFY + "\"";
                  
                  
       // 接口名称, 固定值
       orderInfo += "&service=\"mobile.securitypay.pay\"";
                  
                  
       // 支付类型, 固定值
       orderInfo += "&payment_type=\"1\"";
                  
                  
       // 参数编码, 固定值
       orderInfo += "&_input_charset=\"utf-8\"";
                  
                  
       // 设置未付款交易的超时时间
       // 默认30分钟,一旦超时,该笔交易就会自动被关闭。
       // 取值范围:1m~15d。
       // m-分钟,h-小时,d-天,1c-当天(无论交易何时创建,都在0点关闭)。
       // 该参数数值不接受小数点,如1.5h,可转换为90m。
       orderInfo += "&it_b_pay=\"30m\"";
                  
                  
       // 支付宝处理完请求后,当前页面跳转到商户指定页面的路径.
       // orderInfo += "&return_url=\"m.alipay.com\"";
       // Bill: this item must not be empty! though the api demo said it
       // can be.
       orderInfo += "&return_url=\"m.alipay.com\"";
                  
                  
       // 调用银行卡支付,需配置此参数,参与签名, 固定值
       // orderInfo += "&paymethod=\"expressGateway\"";
      }
                  
                  
      return orderInfo;
     }
  • 对订单做RSA签名:  demo代码中提供SingUtils类实现该功能,即SignUtils.sign(content, RSA_PRIVATE);
  • 对签名做 URL编码:  调用java类库接口,即URLEncoder.encode来实现。
  • 将订单数据和签名信息组合,生成符合支付宝参数规范的数据:  
final String payInfo = orderInfo + "&sign=\"" + sign + "\"&" + getSignType();
第三步:在子线程里调用PayTask的pay接口,将请求数据发送出去
PayTask alipay = new PayTask(PayDemoActivity.this);
// 调用支付接口,获取支付结果
String result = alipay.pay(payInfo);

第四步:收到支付处理结果的消息。支付结果的状态码的意义如下
  •     值为“9000”,代表支付成功;
  •     值为“8000”,代表等待支付结果确认,这可能由于系统原因或者渠道支付原因。支付的最终结果需要由服务器端的异步通知为准(支付宝将向)。
  •     值为其他,代表失败。客户端需要提示用户。
注意事项:
  • 本文特别需要指出的是,也就是最容易出问题的就是订单数据的生成。在demo代码的 PayDemoActivity类中,定义了getOrderInfo方法。 其中“orderInfo += "&return_url=\"m.alipay.com\";”在该demo代码的注释中,虽然说是可以为空,但实际情况,如果为空,将导致支付失败。而且凭借失败状态码,难以识别具体原因。
  • 支付结果,除了支付宝服务器发通知到客户端外,也会异步通知应用服务器。考虑到安全性,客户端可以根据支付宝服务器的通知,进行商业逻辑的处理,比如订单更新等,但是支付的数据入库,需要由应用服务器端根据异步通知进行操作。
服务端实现
服务端基本操作包括:获取支付宝账号信息(为了安全,该信息放置在服务器,而不是客户端),创建订单,支付结果异步回调,申请退款等基本操作外。另外也可能包括:更新订单(对于支持订单可修改的应用),验证消费码,查询订单记录,删除订单等操作。
本文介绍基于Java平台的服务器方案。目前比较流行的框架组合是SpingMVC+Mybatis+Mysql。
订单的创建。当用户下订单时,如果是新订单(请求的数据没有包括订单编号信息),需要创建,并返回订单号给客户端。订单类示例:
public class PayOrder {
            
 public String tradeNo;      //随机编号
 public String amount;      //付款金额
 public String status;      //操作状态
 public String statusCode;     //操作状态代码,0-未支付,10-已支付,4000-退款中,5000-已退款,6000-付款失败,6001-取消付款,7000-已消费
 public String orderNo;      //支付宝流水号
 public String productDescription;   //商品名称
 public String payNo;      //消费码
 public String isRefund;      //是否申请退款
 public String createTime;     //创建时间
 public String modifyTime;     //修改时间
 public String userId;      //用户id
 public Integer id;       //主键
 public String pId;       //商品id
 public int buyNumber;       //存储购买数量
 public int vendorId;       //商家ID
}
tradeNo代码订单编号。payNo代码消费编号(消费码)。orderNo是支付宝服务器端生成的订单号。
@RequestMapping(value = "/payorder")
@ResponseBody
public Map<String, Object> pay(HttpServletRequest request, HttpServletResponse response) {
 Map<String, Object> map = JsonPUtil.pToMap(request);
 Map<String, Object> msgMap = new HashMap<String, Object>();
 if (!map.isEmpty()) {
  try {
   log.info("执行购买前确认操作:" + map);
   String now = String.valueOf(System.currentTimeMillis());
   String trade_no = map.get("barId").toString() + "-" + map.get("barCoupon").toString() + "-" + map.get("count").toString() + "-" + now.substring(now.length() - 6);
      
   PayOrder alipay = new PayOrder();
   alipay.setAmount(map.get("amount").toString());
   alipay.setTradeNo(trade_no);
   alipay.setProductDescription(map.get("productDescription").toString());
   alipay.setCreateTime(now);
   alipay.setStatus("待支付");
   alipay.setStatusCode("0");
   alipay.setExtInt1(Integer.parseInt(map.get("count").toString()));
   alipay.setUserId(map.get("userId").toString());
   alipay.setpId(map.get("barCoupon").toString()); 
   alipay.setExtInt2(Integer.parseInt(map.get("barId").toString()));
      
   int flag = alipayServiceImpl.pay(alipay);
   log.info("确认操作执行结果:" + flag);
      
   Map<String, Object> m = new HashMap<String, Object>();
   m.put("trade_no", trade_no);
   m.put("seller", new String(Base64.encode(AlipayConfig.SELLER.getBytes())));
   m.put("partner", new String(Base64.encode(AlipayConfig.partner.getBytes())));
   m.put("privateKey", new String(Base64.encode(AlipayConfig.ios_private_key.getBytes())));
      
   if (flag > 0)
    msgMap = ResponseMessageUtil.respMsg(Constance.BASE_SUCCESS_CODE, "success", m);
   else
    msgMap = ResponseMessageUtil.respMsg(Constance.BASE_SERVER_WRONG_CODE, "fail");
  } catch (Exception e) {
   e.printStackTrace();
   msgMap = ResponseMessageUtil.respMsg(Constance.BASE_SERVER_WRONG_CODE, "fail");
   log.error("购买前确认失败", e);
  }
 } else {
  log.info("请求参数不完整");
  msgMap = ResponseMessageUtil.respMsg(Constance.ILLEGAL_OPERATE, "fail");
 }
 return msgMap;
}

支付宝服务器回调App服务器,通知支付结果。App服务器将相应的数据入库后,通知支付宝服务器"success" or "fail"。

 
@RequestMapping(value = "/payOver")
 @ResponseBody
 public String payOver(HttpServletRequest request, HttpServletResponse response) {
  Map<String, String> map = JsonPUtil.buildMap(request);
  String result_str = "fail";
  if (!map.isEmpty()) {
   if (AlipayUtils.checkAlipay(map, false) > 0) {// 通过支付宝验证
    try {
     log.info("执行付款的回调函数传递参数:" + map);
     String now = String.valueOf(System.currentTimeMillis());
     String status = map.get("trade_status").toString();
     PayOrder alipay = new PayOrder();
     alipay.setTradeNo(String.valueOf(map.get("out_trade_no")));
     log.info("支付状态:" + status);
     if (Constance.ALIPAY_SUCCESS_CODE.equals(status) || Constance.ALIPAY_FINISHED_CODE.equals(status)) {// 支付成功
      List<Alipay> ali = alipayServiceImpl.search(alipay);
      if (ali.size() == 1 && (ali.get(0).getPayNo() == null || ali.get(0).getPayNo().equals(""))) {// 消息未处理
       Alipay pay = new Alipay();
       pay.setTradeNo(String.valueOf(map.get("out_trade_no")));
       pay.setStatus("已支付");
       pay.setStatusCode("10");
       pay.setIsRefund("0");
       pay.setModifyTime(String.valueOf(System.currentTimeMillis()));
       pay.setPayNo(new String(Base64.encode(now.substring(now.length() - 10).getBytes())));
       pay.setOrderNo(String.valueOf(map.get("trade_no")));
       int flag = alipayServiceImpl.payOver(pay);
       log.info("用户付款成功" + map);
       if (flag > 0)
        result_str = "success";
      }
     } else {
      return result_str;
     }
    } catch (Exception e) {
     e.printStackTrace();
     log.error("回调函数获取参数失败", e);
     return result_str;
    }
   }
  }
  return result_str;
 }

0人打赏
  • UID1
  • 登录2016-01-24
  • 粉丝577
  • 关注562
  • 发帖64
  • 主页
  • 金币202056枚
原创写手
社区居民
admin 发布于2015-09-16 22:41
沙发F
  • UID234
  • 登录2015-11-30
  • 粉丝3
  • 关注1
  • 发帖2
  • 主页
  • 金币6枚
missword2012 发布于2015-10-13 18:08
板凳F
过来学习一下
  • UID15
  • 登录2016-08-05
  • 粉丝48
  • 关注40
  • 发帖447
  • 主页
  • 金币1836枚
社区居民
喜欢达人
原创写手
janking 发布于2015-10-14 10:27
地板F
missword2012:过来学习一下回到原帖
有不懂得 可以在社区下面问一下
  • UID118
  • 登录2016-02-14
  • 粉丝1
  • 关注1
  • 发帖5
  • 主页
  • 金币23枚
qinjuning 发布于2015-10-16 12:53
4楼F
这个流程还是由服务端控制吧,客户端控制不好整 而且不安全
  • UID1091
  • 登录2015-12-03
  • 粉丝0
  • 关注0
  • 发帖1
  • 主页
  • 金币9枚
bajian 发布于2015-11-26 17:55
5楼F
暂时没用上、、
  • UID541
  • 登录2016-08-02
  • 粉丝2
  • 关注1
  • 发帖6
  • 主页
  • 金币20枚
社区居民
Debuff 发布于2015-12-28 18:00
6楼F
收藏了
  • UID1445
  • 登录2016-01-20
  • 粉丝0
  • 关注0
  • 发帖3
  • 主页
  • 金币17枚
社区居民
李昌骏 发布于2016-01-20 13:57
7楼F
niu
  • UID4896
  • 登录2016-12-12
  • 粉丝0
  • 关注0
  • 发帖15
  • 主页
  • 金币39枚
芒小果果 发布于2016-01-28 11:31
8楼F
讲解很详细!!学习了
  • UID2988
  • 登录2016-01-28
  • 粉丝0
  • 关注0
  • 发帖2
  • 主页
  • 金币0枚
社区居民
RVictor 发布于2016-01-28 15:01
9楼F
好东西
  • UID7849
  • 登录2016-02-20
  • 粉丝0
  • 关注0
  • 发帖4
  • 主页
  • 金币16枚
qq_dLqmsal14559 发布于2016-02-20 09:23
10楼F
学习一下
  • UID8001
  • 登录2016-02-21
  • 粉丝0
  • 关注0
  • 发帖3
  • 主页
  • 金币0枚
古桥 发布于2016-02-21 23:06
11楼F
最近正在准备看支付宝的东西呢
  • UID8829
  • 登录2017-01-07
  • 粉丝0
  • 关注0
  • 发帖112
  • 主页
  • 金币586枚
社区居民
忠实会员
大印 发布于2016-02-29 09:17
12楼F
mark
谢谢分享
  • UID6917
  • 登录2016-03-02
  • 粉丝0
  • 关注0
  • 发帖3
  • 主页
  • 金币20枚
社区居民
Trista丶 发布于2016-03-02 16:39
13楼F
棒棒棒!很实用了。
  • UID7461
  • 登录2016-06-08
  • 粉丝1
  • 关注0
  • 发帖211
  • 主页
  • 金币191枚
社区居民
忠实会员
android_chao 发布于2016-03-07 12:06
14楼F
非常不错,虽然我看不懂。
  • UID13033
  • 登录2016-07-23
  • 粉丝0
  • 关注0
  • 发帖5
  • 主页
  • 金币3枚
liuyujiesi 发布于2016-07-23 22:13
15楼F
好好好!
您需要登录后才可以回帖
发表回复
极贡献
技术问答
专题荟萃
程序人生
视觉设计
Android开发
iOS开发
编程语言
前端开发
后端开发
服务器架构
软件测试
运维方案
创业路上



最热文章墙

  • 57814/339   【精品推荐】200多种Android动画效果的强悍框架,太全了,不看这个,再有动画的问题,不理你了^@^

  • 31011/139   省时省力的Android组件群来了,非常棒的原型参考

  • 30712/187   情人节福利,程序员表白的正确姿势:改几行代码就变成自己的表白了

  • 27470/217   【精品推荐】Android版产品级的音乐播放器源码,功能太强大了,最好的产品原型有木有?

  • 25758/2   超全!整理常用的iOS第三方资源

  • 23994/70   原创表白APP,以程序员的姿势备战新年后的7夕,持续完善中!

  • 22596/0   Python爬虫:常用浏览器的useragent

  • 21731/137   2016抢红包软件及源码

  • 19625/29   麻省理工的一帮疯子,真的实现了随意操控万物!(绝对黑科技)

  • 19466/25   Android工程师面试题大全

  • 18754/27   2016程序员跳槽全攻略

  • 18671/9   GitHub上排名前50的iOS项目:总有一款你用得着

  • 18221/20   码魂:程序员的牛B漫画

  • 16666/3   吐槽那些程序员的搞笑牛逼注释

  • 15787/146   Android版类似UC浏览器:非常赞,产品级的源码

  • 15472/1   iOS 动画总结

  • 14844/41   一个绚丽的loading动效分析与实现!

  • 14418/10   女程序员的梦,众网友的神回复

  • 14253/11   年会上现场review代码是怎么样的体验!

  • 14251/73   【持续更新中】Android福利贴(二):资料源码大放送

  • 14250/83   Android小而全的博客源码:非常适合全面掌握开发技巧

  • 14153/44   惊艳的App引导页:背景图片切换加各个页面动画效果

  • 13839/5   新一代Android渠道打包工具:1000个渠道包只需要5秒

  • 13711/10   2016年最全的Android面试考题+答案 精编版

  • 13685/23   个人收集的Android 各类功能源代码

  • 12938/19   珍藏多年的素材,灵感搜寻网站

  • 12830/53   基于瀑布流的美女图片浏览App,有注释的源代码

  • 12479/15   基于Android支付宝支付设计和开发方案

  • 12443/17   用JavaScript 来开发iOS和Android 原生应用:React Native开源框架中文版来啦

  • 12170/74   仿京东商城客户端Android最新版,不错的原型和学习资料

  • 12151/20   Android福利第三波【Android电子书】

  • 12089/17   什么是真正的黑客:收获12200+Stars,人气远超微软开源VS

  • 11627/18   65条最常用正则表达式,你要的都在这里了

  • 11562/94   Android带弹幕的视频播放器源码,来自大名鼎鼎的Bilibili弹幕网站

  • 11535/7   用程序员的姿势抢过年的火车票

  • 11523/70   【精品推荐】类似360安全卫士安Android源码:非常赞的产品原型

  • 11434/7   一张图搞定iOS学习路线,非常全面

  • 11323/11   有木有这样一张酷图帮你集齐所有git命令超实用

  • 11108/0   iOS中文版资源库,非常全

  • 10949/10   成为Java顶尖程序员 ,看这11本书就够了

  • 10547/18   一张图搞定Android学习路线,非常全面

  • 10515/10   微信支付终于成功了(安卓,iOS),在此分享

  • 10154/3   基于Node.js的强大爬虫,能直接发布抓取的文章哦

  • 10072/0   GitHub iOS 库和框架Top100 

  • 10066/44   在线音乐播放器完整版(商用级的源码):非常赞,可听免费高品质专辑

  • 10059/29   【持续更新中】Android福利贴(一):资料源码

  • 9710/4   46 个非常有用的 PHP 代码片段

  • 9342/3   即时通信第三方库

  • 9332/9   烧了5亿美金,这家神秘的公司即将颠覆人类未来!

  • 9195/8   流媒体视频直播方案

  • 9150/61   【技巧一】搭配Android Studio,如何实现App远程真机debug?

  • 9057/9   B站建开源工作组:APP想支持炫酷弹幕的看过来

  • 9006/2   【精品推荐】高质量PHP代码的50个实用技巧:非常值得收藏

  • 8914/9   中国黑客的隐秘江湖:攻守对立,顶尖高手月入千万美元

  • 8882/18   八个最优秀的Android Studio插件

  • 8446/6   开箱即用!Android四款系统架构工具

  • 8354/3   一张图看清Linux 内核运行原理

  • 8247/3   10款GitHub上最火爆的国产开源项目——可以媲美西半球

  • 8233/10   十大技巧快速提升Android应用开发性能

  • 7900/1   Android性能优化视频,文档以及工具

  • 返回顶部