热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

android开发分享详解Android4.4RIL短信接收流程分析

 最近有客户反馈android接收不到短信,于是一头扎进ril里面找原因。最后发现不是ril的问题,而是bc72上报短信的格式不对,at+cnma1

 最近有客户反馈android接收不到短信,于是一头扎进ril里面找原因。最后发现不是ril的问题,而是bc72上报
短信的格式不对,at+cnma=1无作用等几个小问题导致的。尽管问题不在ril,但总算把ril短信接收流程搞清楚了。

接收到新信息的log:

d/atc ( 1269): at<+cmt:,27
d/atc ( 1268): at<0891683108705505f0040d91683117358313f500009101329154922307ea31da2c36a301
d/rilj ( 1792): [unsl] d/smsmessage( 1792): sms sc address: +8613800755500
v/smsmessage( 1792): sms originating address: +8613715338315
v/smsmessage( 1792): sms tp-pid:0 data coding scheme: 0
d/smsmessage( 1792): sms sc timestamp: 1571831129000
v/smsmessage( 1792): sms message body (raw): ‘jchfbfh’
d/gsminboundsmshandler( 1776): idle state processing message type 1
d/gsminboundsmshandler( 1776): acquired wakelock, leaving idle state
d/gsminboundsmshandler( 1776): entering delivering state
d/gsminboundsmshandler( 1776): uri of new row -> content://raw/3
d/rilj ( 1775): [3706]> sms_acknowledge true 0
d/rilc ( 1254): onrequest: sms_acknowledge
d/atc ( 1254): at> at+cnma=1
d/atc ( 1254): at d/rilj ( 1775): [3706] d/gsminboundsmshandler( 1775): delivering sms to: com.android.mms com.android.mms.transaction.privilegedsmsreceiver
e/gsminboundsmshandler( 1775): unexpected broadcastreceiver action: android.provider.telephony.sms_received
d/gsminboundsmshandler( 1775): successful broadcast, deleting from raw table.
d/smsmessage( 2124): sms sc address: +8613800755500
d/gsminboundsmshandler( 1775): deleted 1 rows from raw table.
d/gsminboundsmshandler( 1775): ordered broadcast completed in: 276 ms
d/gsminboundsmshandler( 1775): leaving delivering state
d/gsminboundsmshandler( 1775): entering delivering state
d/gsminboundsmshandler( 1775): leaving delivering state
d/gsminboundsmshandler( 1775): entering idle state
v/smsmessage( 2124): sms originating address: +8613715338315
v/smsmessage( 2124): sms tp-pid:0 data coding scheme: 0
d/smsmessage( 2124): sms sc timestamp: 1572253549000
v/smsmessage( 2124): sms message body (raw): ‘jchfbfh’
d/gsminboundsmshandler( 1775): idle state processing message type 5
d/gsminboundsmshandler( 1775): mwakelock released

一、短信接收

1. vendor ril接收到modem上报的短信息

  hardware/ril/reference-ril/reference-ril.c  static void onunsolicited (const char *s, const char *sms_pdu)  {   ... ...   if (strstartswith(s, "+cmt:")) {    ril_onunsolicitedresponse (     ril_unsol_response_new_sms,        /* 上报unsol_response_new_sms消息 */     sms_pdu, strlen(sms_pdu));    }   ... ...  }

2. rild把短信息发送到rilj

  hardware/ril/libril/ril.cpp  extern "c"  void ril_onunsolicitedresponse(int unsolresponse, void *data,          size_t datalen)  {   ... ...   unsolrespOnseindex= unsolresponse - ril_unsol_response_base; /* 找出消息在s_unsolresponses[]的索引 */   ... ...   switch (s_unsolresponses[unsolresponseindex].waketype) {   /* 禁止进入休眠 */    case wake_partial:     grabpartialwakelock();     shouldscheduletimeout = true;    break;    ... ...   }   ... ...   ret = s_unsolresponses[unsolresponseindex]      /* 调用消息处理函数responsestring() */      .responsefunction(p, data, datalen);   ... ...   ret = sendresponse(p);           /* 发送parcel中的信息内容到服务端rilj */  }  static unsolresponseinfo s_unsolresponses[] = {   ... ...  /* 消息对应的消息处理函数,新信息到来会唤醒系统 */  {ril_unsol_response_new_sms, responsestring, wake_partial},  ... ...  };  static int responsestring(parcel &p, void *response, size_t responselen) {   /* one string only */   startresponse;   appendprintbuf("%s%s", printbuf, (char*)response);       closeresponse;   writestringtoparcel(p, (const char *)response);     /* 把字符串格式的信息存到parcel容器中 */   return 0;  }

二、解析短信息

1. rilj获取短信息

 

  frameworks/opt/telephony/src/java/com/android/internal/telephony/ril.java  private void   processunsolicited (parcel p) {    ... ...   case ril_unsol_response_new_sms: ret = responsestring(p); break;   ... ...   switch(response) {    ... ...    case ril_unsol_response_new_sms: {     if (rilj_logd) unsljlog(response);      /* 参考log:[unsl]

2. 解析短信息

        smsmessage.newfromcmt(a);根据import android.telephony.smsmessage,得知代码路径:

 

  frameworks/opt/telephony/src/java/android/telephony/smsmessage.java  public static smsmessage newfromcmt(string[] lines) {   // received sms in 3gpp format   smsmessagebase wrappedmessage =     com.android.internal.telephony.gsm.smsmessage.newfromcmt(lines);  /* 是对另一个newfromcmt的封装,因为有gsm和cdma两种短信,                       * 即cdma中也有newfromcmt,根据情况按需选择                        */   return new smsmessage(wrappedmessage);  }    com.android.internal.telephony.gsm.smsmessage.newfromcmt(lines)的实现在  frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/smsmessage.java  public class smsmessage extends smsmessagebase {   ... ...   public static smsmessage newfromcmt(string[] lines) {    try {     smsmessage msg = new smsmessage();     msg.parsepdu(iccutils.hexstringtobytes(lines[1]));    /* 解析pdu短信 */     return msg;    } catch (runtimeexception ex) {     rlog.e(log_tag, "sms pdu parsing failed: ", ex);     return null;    }   }   ... ...  }    iccutils.hexstringtobytes(lines[1])把十六进制的字符串转换成字节数组msg.parsepdu()解析这个数组的内容,最后获得短信内容  frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/smsmessage.java  private void parsepdu(byte[] pdu) {    ... ...   mscaddress = p.getscaddress();    if (mscaddress != null) {      if (vdbg) rlog.d(log_tag, "sms sc address: " + mscaddress);   /* 参考log:sms sc address: +8613800755500 */   }   ... ...   mmti = firstbyte & 0x3;   switch (mmti) {    ... ...     case 3: //gsm 03.40 9.2.3.1: mti == 3 is reserved.       //this should be processed in the same way as mti == 0 (deliver)      parsesmsdeliver(p, firstbyte);         /* 对短信类型为deliver的短信进行解析 */      break;     ... ...    }   ... ...  }  private void parsesmsdeliver(pduparser p, int firstbyte) {   ... ...   moriginatingaddress = p.getaddress();                               if (moriginatingaddress != null) {    if (vdbg) rlog.v(log_tag, "sms originating address: "    /* 参考log: sms originating address: +861371533xxxx */                           + moriginatingaddress.address);                              }   ... ...   mprotocolidentifier = p.getbyte();   // tp-data-coding-scheme   // see ts 23.038   mdatacodingscheme = p.getbyte();   if (vdbg) {    rlog.v(log_tag, "sms tp-pid:" + mprotocolidentifier      + " data coding scheme: " + mdatacodingscheme);    /* 参考log: sms tp-pid:0 data coding scheme: 0 */   }   msctimemillis = p.getsctimestampmillis();   if (vdbg) rlog.d(log_tag, "sms sc timestamp: " + msctimemillis);   /* 参考log:sms sc timestamp: 1571831129000 */   boolean hasuserdataheader = (firstbyte & 0x40) == 0x40;   parseuserdata(p, hasuserdataheader);          /* 解析信息有效内容 */   ... ...   }    private void parseuserdata(pduparser p, boolean hasuserdataheader) {   ... ...   if (vdbg) rlog.v(log_tag, "sms message body (raw): '" + mmessagebody + "'"); /* 短信内容,参考log: sms message body (raw): 'jchfbfh' */   ... ...  } 

三、处理短信息    

        对用户有效的短信内容,最终保存在类型为string的mmessagebody变量中,该变量属于smsmessagebase抽象类,而
smsmessage继承于smsmessagebase。
        回到前面frameworks/opt/telephony/src/java/com/android/internal/telephony/ril.java中processunsolicited(),
sms = smsmessage.newfromcmt(a);解析完短信息后,返回一个smsmessage并通知上层应用。

  frameworks/opt/telephony/src/java/com/android/internal/telephony/ril.java  mgsmsmsregistrant   .notifyregistrant(new asyncresult(null, sms, null));        /* 把sms转成object类型 */  frameworks/base/core/java/android/os/asyncresult.java  public class asyncresult  {   ... ...   /** please note, this sets m.obj to be this */   public   asyncresult (object uo, object r, throwable ex)   {    userobj = uo;    result = r;    exception = ex;   }   ... ...  }

        根据mgsmsmsregistrant.notifyregistrant(new asyncresult(null, sms, null));找到mgsmsmsregistrant注册的代码:

  frameworks/opt/telephony/src/java/com/android/internal/telephony/basecommands.java   public abstract class basecommands implements commandsinterface {   ... ...   @override                                       public void setonnewgsmsms(handler h, int what, object obj) {  /* mgsmsmsregistrant.notifyregistrant(new asyncresult(null, sms, null))中的mgsmsmsregistrant是在这里创建的 */                          mgsmsmsregistrant = new registrant (h, what, obj);                            }   ... ...  } 

        封装消息event_new_sms消息

  frameworks/base/core/java/android/os/registrant.java  public class registrant                                    {    public   registrant(handler h, int what, object obj)      /* 传入需要处理消息为what的事件处理handler h,obj为事件内容,参考phone.mci.setonnewgsmsms(gethandler(), event_new_sms, null); */                             {     refh = new weakreference(h);                                   this.what = what;                                     userobj = obj;                                     }    ... ...   /**    * this makes a copy of @param ar    */   public void   notifyregistrant(asyncresult ar)         /* 参考mgsmsmsregistrant.notifyregistrant(new asyncresult(null, sms, null)) */   {    internalnotifyregistrant (ar.result, ar.exception);   /* ar.result为sms */   }   /*package*/ void   internalnotifyregistrant (object result, throwable exception)  /* internalnotifyregistrant (sms, throwable exception) */   {    handler h = gethandler();    if (h == null) {     clear();    } else {     message msg = message.obtain();       /* 创建一个消息 */     msg.what = what;           /* 消息类型event_new_sms */     msg.obj = new asyncresult(userobj, result, exception); /* 消息内容sms */     h.sendmessage(msg);          /* 发送消息到注册了这个消息的handler,参考phone.mci.setonnewgsmsms(gethandler(), event_new_sms, null);的gethandler() */    }   }   ... ...  } 

        然而basecommands是一个抽象类,实现了commandsinterface中的setonnewgsmsms接口,这个接口由gsminboundsmshandler调用
(phone.mci.setonnewgsmsms(gethandler(), event_new_sms, null)),也就是说gsminboundsmshandler的gethandler()是event_new_sms
的监听者,也就是说frameworks/opt/telephony/src/java/com/android/internal/telephony/ril.java中mgsmsmsregistrant.notifyregistrant(new asyncresult(null, sms, null))
调用之后,会触发gsminboundsmshandler中gethandler()的handler对event_new_sms消息进行解析。这个handler肯定是gsminboundsmshandler
实例化的对象中的,这个对象在什么时候,在哪里创建的,暂且不管。我们只管event_new_sms这个消息从哪里来,然后到哪里去
就行了。

  ./frameworks/opt/telephony/src/java/com/android/internal/telephony/imssmsdispatcher.java  public final class imssmsdispatcher extends smsdispatcher {   ... ...   mgsminboundsmshandler = gsminboundsmshandler.makeinboundsmshandler(phone.getcontext(),   /* 获取mgsminboundsmshandler,并启动状态机 */     storagemonitor, phone);    ... ...  }  ./frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/gsminboundsmshandler.java  public class gsminboundsmshandler extends inboundsmshandler {   ... ...   /**    * create a new gsm inbound sms handler.    */   private gsminboundsmshandler(context context, smsstoragemonitor storagemonitor,     phonebase phone) {    super("gsminboundsmshandler", context, storagemonitor, phone,        /* 构造gsminboundsmshandler时,通过super()调用inboundsmshandler的构造函数 */      gsmcellbroadcasthandler.makegsmcellbroadcasthandler(context, phone));    phone.mci.setonnewgsmsms(gethandler(), event_new_sms, null);        /* 注册event_new_sms消息 */    mdatadownloadhandler = new usimdatadownloadhandler(phone.mci);   }   ... ...   /**     * wait for state machine to enter startup state. we can't send any messages until then.    */   public static gsminboundsmshandler makeinboundsmshandler(context context,     smsstoragemonitor storagemonitor, phonebase phone) {    gsminboundsmshandler handler = new gsminboundsmshandler(context, storagemonitor, phone); /* 实例化gsminboundsmshandler */    handler.start();                   /* 抽象类inboundsmshandler继承与statemachine,而gsminboundsmshandler继承于inboundsmshandler,                           * gsminboundsmshandler调用启动状态机方法start()                           */    return handler;   }   ... ...  }  ./frameworks/opt/telephony/src/java/com/android/internal/telephony/inboundsmshandler.java  public abstract class inboundsmshandler extends statemachine {   ... ...   protected inboundsmshandler(string name, context context, smsstoragemonitor storagemonitor,     phonebase phone, cellbroadcasthandler cellbroadcasthandler) {    ... ...    addstate(mdefaultstate);                 /* 构造inboundsmshandler时,添加状态机的状态 */    addstate(mstartupstate, mdefaultstate);    addstate(midlestate, mdefaultstate);    addstate(mdeliveringstate, mdefaultstate);    addstate(mwaitingstate, mdeliveringstate);    setinitialstate(mstartupstate);               /* 初始化状态机 */    if (dbg) log("created inboundsmshandler");   }   ... ...    class idlestate extends state {    @override    public void enter() {     if (dbg) log("entering idle state");     sendmessagedelayed(event_release_wakelock, wakelock_timeout);    }    @override    public void exit() {     mwakelock.acquire();     if (dbg) log("acquired wakelock, leaving idle state");    }    @override    public boolean processmessage(message msg) {     if (dbg) log("idle state processing message type " + msg.what);     switch (msg.what) {      case event_new_sms:                /* 空闲时,接收到短信 */      case event_broadcast_sms:       defermessage(msg);       transitionto(mdeliveringstate);            /* 转到mdeliveringstate */       return handled;      ... ...     }    }   }     ... ...   class deliveringstate extends state {               /* 转到mdeliveringstate状态 */    @override    public void enter() {     if (dbg) log("entering delivering state");    }    @override    public void exit() {     if (dbg) log("leaving delivering state");    }    @override    public boolean processmessage(message msg) {     switch (msg.what) {      case event_new_sms:       // handle new sms from ril       handlenewsms((asyncresult) msg.obj);           /* 处理新sms */       sendmessage(event_return_to_idle);            /* 处理完回到空闲状态 */       return handled;      ... ...     }    }      ... ...   }  }  void handlenewsms(asyncresult ar) {   ... ...   smsmessage sms = (smsmessage) ar.result;   result = dispatchmessage(sms.mwrappedsmsmessage);   ... ...  }  public int dispatchmessage(smsmessagebase smsb) {   ... ...   return dispatchmessageradiospecific(smsb);   ... ...  }

 

        通过以上流程可以了解到,当状态机接收到sms后,对消息进行分发,针对type zero, sms-pp data download,
和3gpp/cphs mwi type sms判断,如果是normal sms messages,则调用dispatchnormalmessage(smsb),然后创建
一个inboundsmstracker对象,把信息保存到raw table,然后在通过sendmessage(event_broadcast_sms, tracker)把消息广播出去。

  ./frameworks/opt/telephony/src/java/com/android/internal/telephony/inboundsmshandler.java       class deliveringstate extends state {   ... ...   public boolean processmessage(message msg) {    switch (msg.what) {     ... ...     case event_broadcast_sms:               /* 接收到event_broadcast_sms消息并处理 */      // if any broadcasts were sent, transition to waiting state      if (processmessagepart((inboundsmstracker) msg.obj)) {       transitionto(mwaitingstate);      }      return handled;     ... ...    }   }   ... ...    }     boolean processmessagepart(inboundsmstracker tracker) {   ... ...   broadcastreceiver resultreceiver = new smsbroadcastreceiver(tracker);     /* 创建一个广播接收者,用来处理短信广播的结果 */   ... ...   intent = new intent(intents.sms_deliver_action);           /* 设置当前intent的action为sms_deliver_action */     // direct the intent to only the default sms app. if we can't find a default sms app   // then sent it to all broadcast receivers.   componentname compOnentname= smsapplication.getdefaultsmsapplication(mcontext, true); /* 这个action只会发送给carrier app,而且carrier app可以通过set result为result_canceled来终止这个广播 */   if (componentname != null) {    // deliver sms message only to this receiver    intent.setcomponent(componentname);    log("delivering sms to: " + componentname.getpackagename() +      " " + componentname.getclassname());   }   ... ...   dispatchintent(intent, android.manifest.permission.receive_sms,       /* 广播intent */      appopsmanager.op_receive_sms, resultreceiver);   ... ...  }       private final class smsbroadcastreceiver extends broadcastreceiver {   ... ...    public void onreceive(context context, intent intent) {    ... ...    // now that the intents have been deleted we can clean up the pdu data.    if (!intents.data_sms_received_action.equals(action)      && !intents.data_sms_received_action.equals(action)      && !intents.wap_push_received_action.equals(action)) {     loge("unexpected broadcastreceiver action: " + action);    }      int rc = getresultcode();    if ((rc != activity.result_ok) && (rc != intents.result_sms_handled)) {     loge("a broadcast receiver set the result code to " + rc       + ", deleting from raw table anyway!");    } else if (dbg) {     log("successful broadcast, deleting from raw table.");    }      deletefromrawtable(mdeletewhere, mdeletewhereargs);    sendmessage(event_broadcast_complete);            /* 成功广播 */      ... ...   }   ... ...  }

       到这里,在应用层注册具有intents.sms_received_action这样action的广播,就可以获取到短信了。

总结

以上所述是小编给大家介绍的android4.4 ril短信接收流程分析,希望对大家有所帮助


推荐阅读
  • JUC(三):深入解析AQS
    本文详细介绍了Java并发工具包中的核心类AQS(AbstractQueuedSynchronizer),包括其基本概念、数据结构、源码分析及核心方法的实现。 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • POJ 2482 星空中的星星:利用线段树与扫描线算法解决
    在《POJ 2482 星空中的星星》问题中,通过运用线段树和扫描线算法,可以高效地解决星星在窗口内的计数问题。该方法不仅能够快速处理大规模数据,还能确保时间复杂度的最优性,适用于各种复杂的星空模拟场景。 ... [详细]
  • 本文介绍了Spring 2.0引入的TaskExecutor接口及其多种实现,包括同步和异步执行任务的方式。文章详细解释了如何在Spring应用中配置和使用这些线程池实现,以提高应用的性能和可管理性。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • MySQL Decimal 类型的最大值解析及其在数据处理中的应用艺术
    在关系型数据库中,表的设计与SQL语句的编写对性能的影响至关重要,甚至可占到90%以上。本文将重点探讨MySQL中Decimal类型的最大值及其在数据处理中的应用技巧,通过实例分析和优化建议,帮助读者深入理解并掌握这一重要知识点。 ... [详细]
  • 在HTML布局中,即使将 `top: 0%` 和 `left: 0%` 设置为元素的定位属性,浏览器中仍然会出现空白填充。这个问题通常与默认的浏览器样式、盒模型或父元素的定位方式有关。为了消除这些空白,可以考虑重置浏览器的默认样式,确保父元素的定位方式正确,并检查是否有其他CSS规则影响了元素的位置。 ... [详细]
  • 在分析和解决 Keepalived VIP 漂移故障的过程中,我们发现主备节点配置如下:主节点 IP 为 172.16.30.31,备份节点 IP 为 172.16.30.32,虚拟 IP 为 172.16.30.10。故障表现为监控系统显示 Keepalived 主节点状态异常,导致 VIP 漂移到备份节点。通过详细检查配置文件和日志,我们发现主节点上的 Keepalived 进程未能正常运行,最终通过优化配置和重启服务解决了该问题。此外,我们还增加了健康检查机制,以提高系统的稳定性和可靠性。 ... [详细]
  • 在对WordPress Duplicator插件0.4.4版本的安全评估中,发现其存在跨站脚本(XSS)攻击漏洞。此漏洞可能被利用进行恶意操作,建议用户及时更新至最新版本以确保系统安全。测试方法仅限于安全研究和教学目的,使用时需自行承担风险。漏洞编号:HTB23162。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 单链表的高效遍历及性能优化策略
    本文探讨了单链表的高效遍历方法及其性能优化策略。在单链表的数据结构中,插入操作的时间复杂度为O(n),而遍历操作的时间复杂度为O(n^2)。通过在 `LinkList.h` 和 `main.cpp` 文件中对单链表进行封装,我们实现了创建和销毁功能的优化,提高了单链表的使用效率。此外,文章还介绍了几种常见的优化技术,如缓存节点指针和批量处理,以进一步提升遍历性能。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
author-avatar
Pissa_lo
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有