(之前有一位仁兄说博客布局在一些浏览器会出现阻挡文字,因此为了市民的方便(也就是me^0^),建议大家也用chrome来浏览,毕竟这个博客布局简明,我有点不想换)

彩信收发宏观步骤:

a、 终端A向彩信中心(MMSC)发送一条彩信,通过WAP网关POST到MMSC

b、 MMSC通过PushProxy网关,向SMSC发送PUSH消息,SMSC转发到终端B

c、 终端B通过WAP网关利用GET方法从MMSC获取一条彩信

d、 MMSC通过PushProxy网关和SNSC向终端A发送一条传送报告(delivery report)

从上面这个步骤可以看出,彩信的接收分两个步骤:

1、接收到短信。

2、分析短信然后通过http来获取彩信附件。

(续我的另一篇博客:http://www.cnblogs.com/not-code/archive/2011/11/27/2265287.html)

因此彩信第一步跟短信的接收流程一样,在RILReceiver 接收到短信转到processUnsolicited进行处理。

GSM方式(最近才知道短信的收发有两种,一种就是通过GSM,另一种是通过CDMA):GSM其事件类型为RIL_UNSOL_RESPONSE_NEW_SMS。先调用responseString从Parcel中获取数据,再使用 newFromCMT方法获取SmsMessage对象,最后调用mSMSRegistrant的notifyRegistrant方法设置消息类型 (what属性为EVENT_NEW_SMS)并转到SMSDispatcher进行处理。这个时候就会调用子类(GsmSMSDispatcher)的dispatchMessage方法处理。

/** {@inheritDoc} */
    protected int dispatchMessage(SmsMessageBase smsb) {
        // If sms is null, means there was a parsing error.
        if (smsb == null) {
            return Intents.RESULT_SMS_GENERIC_ERROR;
        }
        SmsMessage sms = (SmsMessage) smsb;
        boolean handled = false;

if (sms.isTypeZero()) {
            // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be
            // Displayed/Stored/Notified. They should only be acknowledged.
            Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack");
            return Intents.RESULT_SMS_HANDLED;
        }

// Special case the message waiting indicator messages
        if (sms.isMWISetMessage()) {
            mGsmPhone.updateMessageWaitingIndicator(true);
            handled = sms.isMwiDontStore();
            if (Config.LOGD) {
                Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled);
            }
        } else if (sms.isMWIClearMessage()) {
            mGsmPhone.updateMessageWaitingIndicator(false);
            handled = sms.isMwiDontStore();
            if (Config.LOGD) {
                Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled);
            }
        }

if (handled) {
            return Intents.RESULT_SMS_HANDLED;
        }

if (!mStorageAvailable && (sms.getMessageClass() != MessageClass.CLASS_0)) {
            // It's a storable message and there's no storage available.  Bail.
            // (See TS 23.038 for a description of class 0 messages.)
            return Intents.RESULT_SMS_OUT_OF_MEMORY;
        }

SmsHeader smsHeader = sms.getUserDataHeader();
         // See if message is partial or port addressed.
        if ((smsHeader == null) || (smsHeader.concatRef == null)) {
            // Message is not partial (not part of concatenated sequence).
            byte[][] pdus = new byte[1][];
            pdus[0] = sms.getPdu();

if (smsHeader != null && smsHeader.portAddrs != null) {
                if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) {
                    return mWapPush.dispatchWapPdu(sms.getUserData());
                } else {
                    // The message was sent to a port, so concoct a URI for it.
                    dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort);
                }
            } else {
                // Normal short and non-port-addressed message, dispatch it.
                dispatchPdus(pdus);
            }
            return Activity.RESULT_OK;
        } else {
            // Process the message part.
            return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs);
        }
    }

在这个方法里,将会判断接收到的短信是否长短信、是否彩信、是否普通短信。首先获取SmsHeader,
如果SmsHeader或SmsHeader.concatRef均不为空,说明是长短信,则调用processMessagePart将短信分段存入raw表,待所有分段都收到后,将其组装。然后根据端口的不同,按照彩信通知(WapPushOverSms的dispatchWapPdu方法)、指定端口的彩信(dispatchPortAddressedPdus)、长短信(dispatchPdus)进行分发处理。

(从网上窃的一副图~~)(左边是GSM的处理,右边是CDMA的处理,两个处理类都是继承了SMSDispatcher)

继续分析彩信,如果是彩信就调用WapPushOverSms类dispatchWapPdu的方法。(该类的位置)

public int dispatchWapPdu(byte[] pdu) {

if (Config.DEBUG) Log.d(LOG_TAG, "Rx: " + IccUtils.bytesToHexString(pdu));

int index = 0;
        int transactionId = pdu[index++] & 0xFF;
        int pduType = pdu[index++] & 0xFF;
        int headerLength = 0;

if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) &&
                (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) {
            if (Config.DEBUG) Log.w(LOG_TAG, "Received non-PUSH WAP PDU. Type = " + pduType);
            return Intents.RESULT_SMS_HANDLED;
        }

pduDecoder = new WspTypeDecoder(pdu);

/**
         * Parse HeaderLen(unsigned integer).
         * From wap-230-wsp-20010705-a section 8.1.2
         * The maximum size of a uintvar is 32 bits.
         * So it will be encoded in no more than 5 octets.
         */
        if (pduDecoder.decodeUintvarInteger(index) == false) {
            if (Config.DEBUG) Log.w(LOG_TAG, "Received PDU. Header Length error.");
            return Intents.RESULT_SMS_GENERIC_ERROR;
        }
        headerLength = (int)pduDecoder.getValue32();
        index += pduDecoder.getDecodedDataLength();

int headerStartIndex = index;

/**
         * Parse Content-Type.
         * From wap-230-wsp-20010705-a section 8.4.2.24
         *
         * Content-type-value = Constrained-media | Content-general-form
         * Content-general-form = Value-length Media-type
         * Media-type = (Well-known-media | Extension-Media) *(Parameter)
         * Value-length = Short-length | (Length-quote Length)
         * Short-length = <Any octet 0-30>   (octet <= WAP_PDU_SHORT_LENGTH_MAX)
         * Length-quote = <Octet 31>         (WAP_PDU_LENGTH_QUOTE)
         * Length = Uintvar-integer
         */
        if (pduDecoder.decodeContentType(index) == false) {
            if (Config.DEBUG) Log.w(LOG_TAG, "Received PDU. Header Content-Type error.");
            return Intents.RESULT_SMS_GENERIC_ERROR;
        }

String mimeType = pduDecoder.getValueString();
        long binaryContentType = pduDecoder.getValue32();
        index += pduDecoder.getDecodedDataLength();

byte[] header = new byte[headerLength];
        System.arraycopy(pdu, headerStartIndex, header, 0, header.length);

byte[] intentData;

if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO)) {
            intentData = pdu;
        } else {
            int dataIndex = headerStartIndex + headerLength;
            intentData = new byte[pdu.length - dataIndex];
            System.arraycopy(pdu, dataIndex, intentData, 0, intentData.length);
        }

/**
         * Seek for application ID field in WSP header.
         * If application ID is found, WapPushManager substitute the message
         * processing. Since WapPushManager is optional module, if WapPushManager
         * is not found, legacy message processing will be continued.
         */
        if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) {
            index = (int) pduDecoder.getValue32();
            pduDecoder.decodeXWapApplicationId(index);
            String wapAppId = pduDecoder.getValueString();
            if (wapAppId == null) {
                wapAppId = Integer.toString((int) pduDecoder.getValue32());
            }
            String contentType = ((mimeType == null) ?
                                  Long.toString(binaryContentType) : mimeType);
            if (Config.DEBUG) Log.v(LOG_TAG, "appid found: " + wapAppId + ":" + contentType);
            try {
                boolean processFurther = true;
                IWapPushManager wapPushMan = mWapConn.getWapPushManager();

if (wapPushMan == null) {
                    if (Config.DEBUG) Log.w(LOG_TAG, "wap push manager not found!");
                } else {
                    Intent intent = new Intent();
                    intent.putExtra("transactionId", transactionId);
                    intent.putExtra("pduType", pduType);
                    intent.putExtra("header", header);
                    intent.putExtra("data", intentData);
                    intent.putExtra("contentTypeParameters",
                            pduDecoder.getContentParameters());
                    int procRet = wapPushMan.processMessage(wapAppId, contentType, intent);
                    if (Config.DEBUG) Log.v(LOG_TAG, "procRet:" + procRet);
                    if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0
                        && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) {
                        processFurther = false;
                    }
                }
                if (!processFurther) {
                    return Intents.RESULT_SMS_HANDLED;
                }
            } catch (RemoteException e) {
                if (Config.DEBUG) Log.w(LOG_TAG, "remote func failed...");
            }
        }
        if (Config.DEBUG) Log.v(LOG_TAG, "fall back to existing handler");

if (mimeType == null) {
            if (Config.DEBUG) Log.w(LOG_TAG, "Header Content-Type error.");
            return Intents.RESULT_SMS_GENERIC_ERROR;
        }
        String permission;
        if (mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_MMS)) {
            permission = "android.permission.RECEIVE_MMS";
        } else {
            permission = "android.permission.RECEIVE_WAP_PUSH";
        }
        Intent intent = new Intent(Intents.WAP_PUSH_RECEIVED_ACTION);
        intent.setType(mimeType);
        intent.putExtra("transactionId", transactionId);
        intent.putExtra("pduType", pduType);
        intent.putExtra("header", header);
        intent.putExtra("data", intentData);
        intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters());
        mSmsDispatcher.dispatch(intent, permission);
        return Activity.RESULT_OK;
    }

该方法比较长,我们直接看最后的一步:调用父类的dispatch将信息广播出去。

Intent intent = new Intent(Intents.WAP_PUSH_RECEIVED_ACTION);

intent.setType(mimeType);

intent.putExtra("transactionId", transactionId);

intent.putExtra("pduType", pduType);

intent.putExtra("header", header);

intent.putExtra("data", intentData);

intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters());

mSmsDispatcher.dispatch(intent, permission);

应用层PushReceiver类的onReceive将被调用,让屏幕亮5秒,然后创建一个ReceivePushTask并使用它的execute方法。ReceivePushTask是一个AsyncTask,实现了doInBackground方法。当传入intent后,会在doInBackground中将其中的数据转成GenericPdu,并根据其消息类型做出不同的操作。
如果是发送报告或已读报告,将其存入数据库。
如果是彩信通知,若已存在,则不处理。否则将其存入数据库。启动TransactionService进行处理。

case MESSAGE_TYPE_NOTIFICATION_IND: {
    NotificationInd nInd = (NotificationInd) pdu;

if (MmsConfig.getTransIdEnabled()) {
        byte [] contentLocation = nInd.getContentLocation();
        if ('=' == contentLocation[contentLocation.length - 1]) {
            byte [] transactionId = nInd.getTransactionId();
            byte [] contentLocationWithId = new byte [contentLocation.length
                                                      + transactionId.length];
            System.arraycopy(contentLocation, 0, contentLocationWithId,
                    0, contentLocation.length);
            System.arraycopy(transactionId, 0, contentLocationWithId,
                    contentLocation.length, transactionId.length);
            nInd.setContentLocation(contentLocationWithId);
        }
    }

if (!isDuplicateNotification(mContext, nInd)) {
        Uri uri = p.persist(pdu, Inbox.CONTENT_URI);
        // Start service to finish the notification transaction.
        Intent svc = new Intent(mContext, TransactionService.class);
        svc.putExtra(TransactionBundle.URI, uri.toString());
        svc.putExtra(TransactionBundle.TRANSACTION_TYPE,
                Transaction.NOTIFICATION_TRANSACTION);
        mContext.startService(svc);
    } else if (LOCAL_LOGV) {
        Log.v(TAG, "Skip downloading duplicate message: "
                + new String(nInd.getContentLocation()));
    }
    break;
}

启动TransactionService服务,在onStartCommand中调用launchTransaction方法。

private void launchTransaction(int serviceId, TransactionBundle txnBundle,
            boolean noNetwork) {
        if (noNetwork) {
            Log.w(TAG, "launchTransaction: no network error!");
            onNetworkUnavailable(serviceId, txnBundle.getTransactionType());
            return;
        }
        Message msg = mServiceHandler.obtainMessage(EVENT_TRANSACTION_REQUEST);
        msg.arg1 = serviceId;
        msg.obj = txnBundle;

if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
            Log.v(TAG, "launchTransaction: sending message " + msg);
        }
        mServiceHandler.sendMessage(msg);
    }

接着以what= EVENT_TRANSACTION_REQUEST的参数运行mServiceHandler,在mServiceHandler的处理中将创建NotificationTransaction类的对象,经一系列的判断最后将调用processTransaction方法处理NotificationTransaction对象。

private boolean processTransaction(Transaction transaction)
            throws IOException {
        // Check if transaction already processing
        synchronized (mProcessing) {
            for (Transaction t : mPending) {
                if (t.isEquivalent(transaction)) {
                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
                        Log.v(TAG, "Transaction already pending: "
                                + transaction.getServiceId());
                    }
                    return true;
                }
            }
            for (Transaction t : mProcessing) {
                if (t.isEquivalent(transaction)) {
                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
                        Log.v(TAG,
                                "Duplicated transaction: "
                                        + transaction.getServiceId());
                    }
                    return true;
                }
            }

/*
             * Make sure that the network connectivity necessary for MMS traffic
             * is enabled. If it is not, we need to defer processing the
             * transaction until connectivity is established.
             */
            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
                Log.v(TAG, "processTransaction: call beginMmsConnectivity...");
            }
            int connectivityResult = beginMmsConnectivity();
            if (connectivityResult == Phone.APN_REQUEST_STARTED) {
                mPending.add(transaction);
                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
                    Log.v(TAG,
                            "processTransaction: connResult=APN_REQUEST_STARTED, "
                                    + "defer transaction pending MMS connectivity");
                }
                return true;
            }

if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
                Log.v(TAG, "Adding transaction to 'mProcessing' list: "
                        + transaction);
            }
            mProcessing.add(transaction);
        }

// Set a timer to keep renewing our "lease" on the MMS connection
        sendMessageDelayed(obtainMessage(EVENT_CONTINUE_MMS_CONNECTIVITY),
                APN_EXTENSION_WAIT);

if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
            Log.v(TAG, "processTransaction: starting transaction "
                    + transaction);
        }

// Attach to transaction and process it
        transaction.attach(TransactionService.this);
        transaction.process();
        return true;
    }

该方法首先会先判断该Transaction对象是否存在mPending或mProcessing队列中,如果没有则将对象加入到mProcessing中,并将TransactionService本身加入到NotificationTransaction对象的观察者列表(这样做的目的是为了后面下载完成后通知该服务TransactionService的mProcessing移除掉NotificationTransaction对象并发送完成下载的广播)。最后将调用NotificationTransaction的process方法。

public void process() {
        new Thread(this).start();
    }

public void run() {
        DownloadManager downloadManager = DownloadManager.getInstance();
        boolean autoDownload = downloadManager.isAuto();
        boolean dataSuspended = (TelephonyManager.getDefault().getDataState() == TelephonyManager.DATA_SUSPENDED);
        try {
            if (LOCAL_LOGV) {
                Log.v(TAG, "Notification transaction launched: " + this);
            }

// By default, we set status to STATUS_DEFERRED because we
            // should response MMSC with STATUS_DEFERRED when we cannot
            // download a MM immediately.
            int status = STATUS_DEFERRED;
            // Don't try to download when data is suspended, as it will fail, so
            // defer download
            if (!autoDownload || dataSuspended) {
                downloadManager
                        .markState(mUri, DownloadManager.STATE_UNSTARTED);
                sendNotifyRespInd(status);
                return;
            }

downloadManager.markState(mUri, DownloadManager.STATE_DOWNLOADING);

if (LOCAL_LOGV) {
                Log.v(TAG, "Content-Location: " + mContentLocation);
            }

byte[] retrieveConfData = null;
            // We should catch exceptions here to response MMSC
            // with STATUS_DEFERRED.
            try {
                retrieveConfData = getPdu(mContentLocation);
            } catch (IOException e) {
                mTransactionState.setState(FAILED);
            }

if (retrieveConfData != null) {
                GenericPdu pdu = new PduParser(retrieveConfData).parse();
                if ((pdu == null)
                        || (pdu.getMessageType() != MESSAGE_TYPE_RETRIEVE_CONF)) {
                    Log.e(TAG, "Invalid M-RETRIEVE.CONF PDU.");
                    mTransactionState.setState(FAILED);
                    status = STATUS_UNRECOGNIZED;
                } else {
                    // Save the received PDU (must be a M-RETRIEVE.CONF).
                    PduPersister p = PduPersister.getPduPersister(mContext);
                    Uri uri = p.persist(pdu, Inbox.CONTENT_URI);
                    // We have successfully downloaded the new MM. Delete the
                    // M-NotifyResp.ind from Inbox.
                    SqliteWrapper.delete(mContext,
                            mContext.getContentResolver(), mUri, null, null);
                    // Notify observers with newly received MM.
                    mUri = uri;
                    status = STATUS_RETRIEVED;
                }
            }

if (LOCAL_LOGV) {
                Log.v(TAG, "status=0x" + Integer.toHexString(status));
            }

// Check the status and update the result state of this Transaction.
            switch (status) {
            case STATUS_RETRIEVED:
                mTransactionState.setState(SUCCESS);
                break;
            case STATUS_DEFERRED:
                // STATUS_DEFERRED, may be a failed immediate retrieval.
                if (mTransactionState.getState() == INITIALIZED) {
                    mTransactionState.setState(SUCCESS);
                }
                break;
            }

sendNotifyRespInd(status);

// Make sure this thread isn't over the limits in message count.
            Recycler.getMmsRecycler().deleteOldMessagesInSameThreadAsMessage(
                    mContext, mUri);
        } catch (Throwable t) {
            Log.e(TAG, Log.getStackTraceString(t));
        } finally {
            mTransactionState.setContentUri(mUri);
            if (!autoDownload || dataSuspended) {
                // Always mark the transaction successful for deferred
                // download since any error here doesn't make sense.
                mTransactionState.setState(SUCCESS);
            }
            if (mTransactionState.getState() != SUCCESS) {
                mTransactionState.setState(FAILED);
                Log.e(TAG, "NotificationTransaction failed.");
            }
            notifyObservers();
        }
    }

NotificationTransaction的process方法将下载相应彩信,首先删除彩信通知,通知mmsc,删除超过容量限制的彩信,彩信附件的获取最终是通过getPdu(mContentLocation)来请求附件的流,返回byte[]类型。最后将notifyObservers()通知TransactionService处理其余待发送的彩信和发送下载完成的广播。

看图比较容易理解:(网上窃的一副妙图~~,不过里面最后那里好像有一个小错误,不过没关系~~)

(彩信接收主要大概涉及到的类的类图,这是原创滴,原创就是简陋)

RIL到SMSDispatch中间其实涉及很多步骤,这里就简明一点,抽丝剥茧,让大家对彩信接收涉及到的类的分布和以及它们的作用有个大概的蓝图。短信来后发现自己属于GsmSMSDispatch类,WapPushOverSms实例调用dispatchWapPdu发送广播,PushReceive接收到广播后启动TransactionService服务,TransactionService将自己attach到mObservers的观察列表中,然后调用NotificationTransaction对象的process方法,该方法将通过getPdu来获取附件内容,最后将调用notifyObservers通知所有添加到观察列表的对象调用update方法实现更新。 (这个就是传说中的观察者模式,改网上的一个例子:邮局现在有少男少女和花花公子的杂志(杂志是被观察者,里面维护一条订阅者的队列),现在有一个女孩想订阅少男少女(女孩是观察者,订阅就是将自己add注册到被观察者的队列),一个男孩订阅少男少女和花花公子,当邮局某本杂志到来就会通知到订阅者(通知就是循环队列通知每个订阅该杂志的人,然后将这本杂志作为变量传过去,这样订阅者就知道是哪本杂志到来)。 同理:TransactionService是观察者(男孩或者女孩),NotificationTransaction是被观察者(杂志))

参考和引用部分文字和图片的文章:

http://www.diybl.com/course/3_program/java/android/20111124/562797.html

http://www.2cto.com/kf/201111/109802.html

转载于:https://www.cnblogs.com/not-code/archive/2011/12/01/2270903.html

android 彩信接收到附件的下载原理分析相关推荐

  1. android 彩信,android 彩信接收到附件的下载原理分析

    转:http://xiaoyuang.com/print.php?id=12586 首先,了解下彩信收发的宏观步骤: a. 终端A向彩信中心(MMSC)发送一条彩信,通过WAP网关POST到MMSC ...

  2. xposed hook java_[原创]Android Hook 系列教程(一) Xposed Hook 原理分析

    章节内容 一. Android Hook 系列教程(一) Xposed Hook 原理分析 二. Android Hook 系列教程(二) 自己写APK实现Hook Java层函数 三. Androi ...

  3. Android Jetpack组件ViewModel基本使用和原理分析

    本文整体流程:首先要知道什么是 ViewModel,然后演示一个例子,来看看 ViewModel 是怎么使用的,接着提出问题为什么是这样的,最后读源码来解释原因! 1.什么是ViewModel 1.1 ...

  4. 【Android APT】编译时技术 ( ButterKnife 原理分析 )

    文章目录 一.编译时技术简介 二.ButterKnife 原理分析 二.ButterKnife 生成 Activity_ViewBinding 代码分析 一.编译时技术简介 APT ( Annotat ...

  5. Android 兼容 Java 8 语法特性的原理分析

    本文主要阐述了Lambda表达式及其底层实现(invokedynamic指令)的原理.Android第三方插件RetroLambda对其的支持过程.Android官方最新的dex编译器D8对其的编译支 ...

  6. 断点续传和下载原理分析

    最近做一个文件上传和下载的应用对文件上传和下载进行了一个完整的流程分析 以来是方便自己对文件上传和下载的理解,而来便于团队内部的分享 故而做了几张图,将整体的流程都画下来,便于大家的理解和分析,如果有 ...

  7. java解析bt协议详解_BT下载原理分析

    BT全名为BitTorrent,是一个p2p软件,你在下载download的同时,也在为其他用户提供上传upload,因为大家是"互相帮助",所以不会随着用户数的增加而降低下载速度 ...

  8. Android 几种换肤方式和原理分析

    1.通过Theme切换主题 通过在setContentView之前设置Theme实现主题切换. 在styles.xml定义一个夜间主题和白天主题: <style name="Light ...

  9. 【AOP 面向切面编程】Android Studio 使用 AspectJ 监控方法运行原理分析

    文章目录 一.查看使用 AspectJ 后生成的 Class 字节码类 二.AspectJ 的本质 一.查看使用 AspectJ 后生成的 Class 字节码类 在 Android Studio 中查 ...

最新文章

  1. Eclipse的ExtJs智能提示
  2. android 手动 打包,android 手动打包apk
  3. 计算机网络(谢希仁第八版)第三章:数据链路层
  4. 非常有趣的古越及吴语-台州话
  5. JAVA设计模式总结之23种设计模式
  6. MySQL5.7多源复制的实验
  7. 多普勒效应及多普勒频移的简单推导
  8. tcppwebbrower 关闭安全警报_【知识】锅炉与压力容器安全
  9. Keras-1 学习Keras,从Hello World开始
  10. B站推出“锤人类”作品试运行管理办法 将减少推荐争议视频
  11. net.sf.json.JSONObject处理 null 字符串的一些坑
  12. openstack运维实战系列(一)之keystone用户建立
  13. 【入门经典】准备工作
  14. Matlab图像处理系列1———线性变换和直方图均衡
  15. Android 上千实例源码分析以及开源分析
  16. html动画人物走路,CSS3动画中的steps(),制作人物行走动画
  17. 快捷餐饮之店家后台订单管理实现
  18. 解决windows 10桌面文件图标上出现两个蓝色箭头
  19. 游戏建模行业就国内发展的前景,以及行业变化是如何的?
  20. 火影段位赛服务器响应超时,火影手游,谜一样的跨服积分赛已上线,奖励不到位惹人嫌弃...

热门文章

  1. API:BUMO Keypair 指导
  2. 怎样恢复计算机桌面,电脑桌面空白怎么恢复_怎样恢复电脑桌面图标
  3. 如何优雅的消除代码里的NullPointerException!
  4. 巨变的中国与数字化转型,创造了中国企业技术出海的历史机遇
  5. C++函数返回值和返回引用
  6. 页面加载时,vue生命周期的触发顺序
  7. 鹅厂招人啦!限量内推码和面试直通卡!助你直拿Offer(内附岗位介绍)
  8. JetBrains IDE全新UI预览版来了,要做简洁与强大兼顾的IDE
  9. 【原创】uniapp开发的微商个人相册多端小程序源码以及安装
  10. wordPress 安装