zoukankan      html  css  js  c++  java
  • Caused by java.lang.IllegalStateException Not allowed to start service Intent { cmp=com.x.x.x/.x.x.xService }: app is in background uid UidRecord问题原因分析(二)

    应用在适配Android 8.0以上系统时,会发现后台启动不了服务,会报出如下异常,并强退:

    Fatal Exception: java.lang.IllegalStateException
    Not allowed to start service Intent { act=com.xxx.xxx.xxx pkg=com.xxx.xxx (has extras) }: app is in background uid UidRecord{1cbd9ed u0a1967 CEM idle procs:1 seq(0,0,0)}

    问题原因分析

    Android 8.0 行为变更

    https://developer.android.com/about/versions/oreo/android-8.0-changes.html#back-all

    Android 8.0 除了提供诸多新特性和功能外,还对系统和 API 行为做出了各种变更。本文重点介绍您应该了解并在开发应用时加以考虑的一些主要变更。

    其中大部分变更会影响所有应用,而不论应用针对的是何种版本的 Android。不过,有几项变更仅影响针对 Android 8.0 的应用。为清楚起见,本页面分为两个部分:针对所有 API 级别的应用针对 Android 8.0 的应用

    针对所有 API 级别的应用

    这些行为变更适用于 在 Android 8.0 平台上运行的 所有应用,无论这些应用是针对哪个 API 级别构建。所有开发者都应查看这些变更,并修改其应用以正确支持这些变更(如果适用)。

    后台执行限制

    Android 8.0 为提高电池续航时间而引入的变更之一是,当您的应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。

    此外,为提高设备性能,系统会限制未在前台运行的应用的某些行为。具体而言:

    • 现在,在后台运行的应用对后台服务的访问受到限制。
    • 应用无法使用其清单注册大部分隐式广播(即,并非专门针对此应用的广播)。

    默认情况下,这些限制仅适用于针对 O 的应用。不过,用户可以从 Settings 屏幕为任意应用启用这些限制,即使应用并不是以 O 为目标平台。

    Android 8.0 还对特定函数做出了以下变更:

    • 如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException
    • 新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。

    如需了解详细信息,请参阅后台执行限制

    出错代码定位

    在ContextImpl的startServiceCommon函数中爆出异常, 
    http://androidxref.com/8.1.0_r33/xref/frameworks/base/core/java/android/app/ContextImpl.java

    private ComponentName startServiceCommon(Intent service, boolean requireForeground,
                UserHandle user) {
            try {
                validateServiceIntent(service);
                service.prepareToLeaveProcess(this);
                ComponentName cn = ActivityManager.getService().startService(
                    mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
                                getContentResolver()), requireForeground,
                                getOpPackageName(), user.getIdentifier());
                if (cn != null) {
                    if (cn.getPackageName().equals("!")) {
                        throw new SecurityException(
                                "Not allowed to start service " + service
                                + " without permission " + cn.getClassName());
                    } else if (cn.getPackageName().equals("!!")) {
                        throw new SecurityException(
                                "Unable to start service " + service
                                + ": " + cn.getClassName());
                     //此处就是曝出异常的地方,非法状态,不允许启动服务
                    } else if (cn.getPackageName().equals("?")) {
                        throw new IllegalStateException(
                                "Not allowed to start service " + service + ": " + cn.getClassName());
                    }
                }
                return cn;
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }

    startServiceCommon这个函数做的操作是AMS的startService,用于启动服务

    AMS的startService

    接下去看AMS的startService,稍微注意一下传递的参数,里面有一个前台后台相关的requireForeground,可能跟问题有关系。 
    AMS代码位置 

    http://androidxref.com/8.1.0_r33/xref/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    @Override
    public ComponentName startService(IApplicationThread caller, Intent service,
                String resolvedType, boolean requireForeground, String callingPackage, int userId)
               throws TransactionTooLargeException {
            enforceNotIsolatedCaller("startService");
            // Refuse possible leaked file descriptors
           if (service != null && service.hasFileDescriptors() == true) {
                throw new IllegalArgumentException("File descriptors passed in Intent");
           }
    
            if (callingPackage == null) {
                throw new IllegalArgumentException("callingPackage cannot be null");
           }
    
            if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,
                    "*** startService: " + service + " type=" + resolvedType + " fg=" + requireForeground);
           synchronized(this) {
                final int callingPid = Binder.getCallingPid();
                final int callingUid = Binder.getCallingUid();
                final long origId = Binder.clearCallingIdentity();
                ComponentName res;
                try {
                //调用ActiveServices的startServiceLocked
                    res = mServices.startServiceLocked(caller, service,
                            resolvedType, callingPid, callingUid,
                            requireForeground, callingPackage, userId);
                } finally {
                    Binder.restoreCallingIdentity(origId);
                }
                return res;
            }
        }

    会调用ActiveServices的startServiceLocked

    ActiveServices的startServiceLocked

    http://androidxref.com/8.1.0_r33/xref/frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

        ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
                int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
                throws TransactionTooLargeException {
            //...
            // 启动服务之前有2个判断一个是startRequested,一个是fgRequired。
            // startRequested代表的是:是否已经启动过服务,一般出现问题都是启动一个没有运行的服务,
            // 那么这个就是false。
            // fgRequired这个就是启动服务传递的requireForeground,
            if (!r.startRequested && !fgRequired) {
                // 这里面有个关键函数getAppStartModeLocked,判断是否运行启动服务
                // 注意此处传递的最后2个参数:alwaysRestrict和disabledOnly都是false
                final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
                        r.appInfo.targetSdkVersion, callingPid, false, false);
                // 如果不允许启动服务则会运行到里面
                if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
                    //...
                    UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
                    // 此处就是不允许运行服务返回的原因"app is in background"
                    // 找到了原因就在这里
                    return new ComponentName("?", "app is in background uid " + uidRec);
                }
            }
            //...
        }

    这里面由于出现错误,那么startRequested==false而且fgRequired==false,说明这个服务是第一次启动,而且是后台请求启动服务。
    至于为什么不允许启动服务,我们还需要查看AMS的getAppStartModeLocked函数。

    AMS判断并返回服务启动模式

    http://androidxref.com/8.1.0_r33/xref/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
                int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
            UidRecord uidRec = mActiveUids.get(uid);
            if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg="
                    + packageName + " rec=" + uidRec + " always=" + alwaysRestrict + " idle="
                    + (uidRec != null ? uidRec.idle : false));
            if (uidRec == null || alwaysRestrict || uidRec.idle) {
                boolean ephemeral;
                if (uidRec == null) {
                    ephemeral = getPackageManagerInternalLocked().isPackageEphemeral(
                            UserHandle.getUserId(uid), packageName);
                } else {
                    ephemeral = uidRec.ephemeral;
                }
    
                if (ephemeral) {
                    // We are hard-core about ephemeral apps not running in the background.
                    return ActivityManager.APP_START_MODE_DISABLED;
                } else {
                    if (disabledOnly) {
                        // The caller is only interested in whether app starts are completely
                        // disabled for the given package (that is, it is an instant app).  So
                        // we don't need to go further, which is all just seeing if we should
                        // apply a "delayed" mode for a regular app.
                        return ActivityManager.APP_START_MODE_NORMAL;
                    }
              // 此处alwaysRestrict==false,于是调用的是appServicesRestrictedInBackgroundLocked
    final int startMode = (alwaysRestrict) ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk) : appServicesRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk); if (DEBUG_BACKGROUND_CHECK) Slog.d(TAG, "checkAllowBackground: uid=" + uid + " pkg=" + packageName + " startMode=" + startMode + " onwhitelist=" + isOnDeviceIdleWhitelistLocked(uid)); if (startMode == ActivityManager.APP_START_MODE_DELAYED) { // This is an old app that has been forced into a "compatible as possible" // mode of background check. To increase compatibility, we will allow other // foreground apps to cause its services to start. if (callingPid >= 0) { ProcessRecord proc; synchronized (mPidsSelfLocked) { proc = mPidsSelfLocked.get(callingPid); } if (proc != null && !ActivityManager.isProcStateBackground(proc.curProcState)) { // Whoever is instigating this is in the foreground, so we will allow it // to go through. return ActivityManager.APP_START_MODE_NORMAL; } } } return startMode; } } return ActivityManager.APP_START_MODE_NORMAL; }

    根据alwaysRestrict的值会调用appRestrictedInBackgroundLocked或者appServicesRestrictedInBackgroundLocked;
    其中appRestrictedInBackgroundLocked是直接根据应用sdk进行判断,
    appServicesRestrictedInBackgroundLocked会进行条件过滤,直接运行部分应用启动服务,其它的进行应用sdk的判断。

    接下来看appServicesRestrictedInBackgroundLocked进行条件过滤,允许部分启动服务

    int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
            // Persistent app?
            // 如果是常驻内存的,可以直接启动服务
    
            if (mPackageManagerInt.isPackagePersistent(packageName)) {
                if (DEBUG_BACKGROUND_CHECK) {
                    Slog.i(TAG, "App " + uid + "/" + packageName
                            + " is persistent; not restricted in background");
                }
                return ActivityManager.APP_START_MODE_NORMAL;
            }
    
            // Non-persistent but background whitelisted?
         // 如果是非常驻内存的话,但是在白名单列表里面的uid也是允许的 
         // 目前这个白名单里面就只有一个:蓝牙BLUETOOTH_UID = 1002
    
            if (uidOnBackgroundWhitelist(uid)) {
                if (DEBUG_BACKGROUND_CHECK) {
                    Slog.i(TAG, "App " + uid + "/" + packageName
                            + " on background whitelist; not restricted in background");
                }
                return ActivityManager.APP_START_MODE_NORMAL;
            }
    
            // Is this app on the battery whitelist?
            // 如果是在电源相关的白名单里面,也是允许启动服务的
    
            if (isOnDeviceIdleWhitelistLocked(uid)) {
                if (DEBUG_BACKGROUND_CHECK) {
                    Slog.i(TAG, "App " + uid + "/" + packageName
                            + " on idle whitelist; not restricted in background");
                }
                return ActivityManager.APP_START_MODE_NORMAL;
            }
    
            // None of the service-policy criteria apply, so we apply the common criteria
            return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
        }

    分别对于: 
    1. 是否常驻内存应用,常驻内存允许启动服务 
    2.如果是蓝牙也是允许启动服务 
    3.是在电源相关的DeviceIdle白名单里面,允许启动服务的 
    4.如果都不是则执行默认策略appRestrictedInBackgroundLocked

    再看appServicesRestrictedInBackgroundLocked进行条件过滤,允许部分启动服务

     int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
            // Apps that target O+ are always subject to background check
            // 如果apk的sdk版本大于AndroidO的话,那么默认是不允许启动服务的
    
            if (packageTargetSdk >= Build.VERSION_CODES.O) {
                if (DEBUG_BACKGROUND_CHECK) {
                    Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted");
                }
                return ActivityManager.APP_START_MODE_DELAYED_RIGID;
            }
            // ...and legacy apps get an AppOp check
           // 如果是之前版本的apk,会查看AppOps是否允许后台运行权限, 
           // 由于我们sdk版本肯定会升级的,这个就暂时不考虑了
    
            int appop = mAppOpsService.noteOperation(AppOpsManager.OP_RUN_IN_BACKGROUND,
                    uid, packageName);
            if (DEBUG_BACKGROUND_CHECK) {
                Slog.i(TAG, "Legacy app " + uid + "/" + packageName + " bg appop " + appop);
            }
            switch (appop) {
                case AppOpsManager.MODE_ALLOWED:
                    return ActivityManager.APP_START_MODE_NORMAL;
                case AppOpsManager.MODE_IGNORED:
                    return ActivityManager.APP_START_MODE_DELAYED;
                default:
                    return ActivityManager.APP_START_MODE_DELAYED_RIGID;
            }
        }

    如果apk的sdk版本大于AndroidO的话,那么默认是不允许启动服务的,那么要适配Android O/GO以后的版本,此处是绕不过去的坎,建议尽早处理。

    网上所传的notification隐藏是否可以?

    有的需求是启动服务,但是又不想有通知,这和Google定的规则有冲突呀,有没有什么办法呢?网上2年前的方案是启动两个Service,一个Service干活,另外一个Service把通知隐藏掉。

    Android O Google应该考虑到这个漏洞了:

    private void cancelForegroundNotificationLocked(ServiceRecord r) {
            if (r.foregroundId != 0) {
                // First check to see if this app has any other active foreground services
                // with the same notification ID.  If so, we shouldn't actually cancel it,
                // because that would wipe away the notification that still needs to be shown
                // due the other service.
                ServiceMap sm = getServiceMapLocked(r.userId);
                if (sm != null) {
                    for (int i = sm.mServicesByName.size()-1; i >= 0; i--) {
                        ServiceRecord other = sm.mServicesByName.valueAt(i);
                        if (other != r && other.foregroundId == r.foregroundId
                                && other.packageName.equals(r.packageName)) {
                            // Found one!  Abort the cancel.
                            return;
                        }
                    }
                }
                r.cancelNotification();
            }
        }

    如果前台服务的通知还有被占用,那就别想用其他服务把它干掉了

     框架规避方案

    修改方案(仅供参考):ActiveServices.java如下加一个packageName的crash的规避,anr同理,发出消息的地方可以修改为不发出timeout消息,也可以在startForeground的时候就移除。(如果Service耗时小于5s,Service在stop流程的时候会将anr消息移除,可不修改)

            // Check to see if the service had been started as foreground, but being
            // brought down before actually showing a notification.  That is not allowed.
            if (r.fgRequired) {
                Slog.w(TAG_SERVICE, "Bringing down service while still waiting for start foreground: "
                        + r);
                r.fgRequired = false;
                r.fgWaiting = false;
                mAm.mHandler.removeMessages(
                        ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r);
                if (r.app != null && !"packageName".equals(r.packageName)) {
                    Message msg = mAm.mHandler.obtainMessage(
                            ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG);
                    msg.obj = r.app;
                    mAm.mHandler.sendMessage(msg);
                }
            }

    修改原理:

    编译一个service.jar,打印报错堆栈

    01-01 07:02:17.669   918  1334 W ActivityManager: Bringing down service while still waiting for start foreground: ServiceRecord{2d44a2d u0 packageName/.servicename}
    01-01 07:02:17.669   918  1334 W ActivityManager: java.lang.Throwable
    01-01 07:02:17.669   918  1334 W ActivityManager:     at com.android.server.am.ActiveServices.bringDownServiceLocked(ActiveServices.java:2612)
    01-01 07:02:17.669   918  1334 W ActivityManager:     at com.android.server.am.ActiveServices.bringDownServiceIfNeededLocked(ActiveServices.java:2559)
    01-01 07:02:17.669   918  1334 W ActivityManager:     at com.android.server.am.ActiveServices.stopServiceTokenLocked(ActiveServices.java:792)
    01-01 07:02:17.669   918  1334 W ActivityManager:     at com.android.server.am.ActivityManagerService.stopServiceToken(ActivityManagerService.java:18789)
    01-01 07:02:17.669   918  1334 W ActivityManager:     at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:759)
    01-01 07:02:17.669   918  1334 W ActivityManager:     at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3080)
    01-01 07:02:17.669   918  1334 W ActivityManager:     at android.os.Binder.execTransact(Binder.java:697)

    找到对应抛出Context.startForegroundService() did not then call Service.startForeground()的逻辑代码:

    ActiveServices.java bringDownServiceLocked

          // Check to see if the service had been started as foreground, but being
            // brought down before actually showing a notification.  That is not allowed.
            if (r.fgRequired) {
                Slog.w(TAG_SERVICE, "Bringing down service while still waiting for start foreground: "
                        + r);
                r.fgRequired = false;
                r.fgWaiting = false;
                mAm.mHandler.removeMessages(
                        ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r);
                if (r.app != null) {
                    Message msg = mAm.mHandler.obtainMessage(
                            ActivityManagerService.SERVICE_FOREGROUND_CRASH_MSG);
                    msg.obj = r.app;
                    mAm.mHandler.sendMessage(msg);
                }
            }

    走到这里面继而会由ams发出一个service_foreground_crash_msg的消息,导致crash。

    至于为嘛会走到这里呢,都是id = 0 的过,既没有走前台服务的流程也没有将r.fgRequired设为false,anr的msg也没有移除掉。

    private void setServiceForegroundInnerLocked(ServiceRecord r, int id,
                Notification notification, int flags) {
            if (id != 0) {
                if (notification == null) {
                    throw new IllegalArgumentException("null notification");
                }
                // Instant apps need permission to create foreground services.
                ...
                if (r.fgRequired) {
                    if (DEBUG_SERVICE || DEBUG_BACKGROUND_CHECK) {
                        Slog.i(TAG, "Service called startForeground() as required: " + r);
                    }
                    r.fgRequired = false;
                    r.fgWaiting = false;
                    mAm.mHandler.removeMessages(
                            ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG, r);
                }
                if (r.foregroundId != id) {
                    cancelForegroundNotificationLocked(r);
                    r.foregroundId = id;
                }
                notification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
                r.foregroundNoti = notification;
                if (!r.isForeground) {
                    final ServiceMap smap = getServiceMapLocked(r.userId);
                    if (smap != null) {
                        ActiveForegroundApp active = smap.mActiveForegroundApps.get(r.packageName);
                        if (active == null) {
                            active = new ActiveForegroundApp();
                            active.mPackageName = r.packageName;
                            active.mUid = r.appInfo.uid;
                            active.mShownWhileScreenOn = mScreenOn;
                            if (r.app != null) {
                                active.mAppOnTop = active.mShownWhileTop =
                                        r.app.uidRecord.curProcState
                                                <= ActivityManager.PROCESS_STATE_TOP;
                            }
                            active.mStartTime = active.mStartVisibleTime
                                    = SystemClock.elapsedRealtime();
                            smap.mActiveForegroundApps.put(r.packageName, active);
                            requestUpdateActiveForegroundAppsLocked(smap, 0);
                        }
                        active.mNumActive++;
                    }
                    r.isForeground = true;
                }
                r.postNotification();
                if (r.app != null) {
                    updateServiceForegroundLocked(r.app, true);
                }
                getServiceMapLocked(r.userId).ensureNotStartingBackgroundLocked(r);
                mAm.notifyPackageUse(r.serviceInfo.packageName,
                                     PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
            } else {
                if (r.isForeground) {
                    final ServiceMap smap = getServiceMapLocked(r.userId);
                    if (smap != null) {
                        decActiveForegroundAppLocked(smap, r);
                    }
                    r.isForeground = false;
                    if (r.app != null) {
                        mAm.updateLruProcessLocked(r.app, false, null);
                        updateServiceForegroundLocked(r.app, true);
                    }
                }
                if ((flags & Service.STOP_FOREGROUND_REMOVE) != 0) {
                    cancelForegroundNotificationLocked(r);
                    r.foregroundId = 0;
                    r.foregroundNoti = null;
                } else if (r.appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
                    r.stripForegroundServiceFlagFromNotification();
                    if ((flags & Service.STOP_FOREGROUND_DETACH) != 0) {
                        r.foregroundId = 0;
                        r.foregroundNoti = null;
                    }
                }
            }
        }

    anr的时限为嘛是5s呢?

        void scheduleServiceForegroundTransitionTimeoutLocked(ServiceRecord r) {
            if (r.app.executingServices.size() == 0 || r.app.thread == null) {
                return;
            }
            Message msg = mAm.mHandler.obtainMessage(
                    ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG);
            msg.obj = r;
            r.fgWaiting = true;
            mAm.mHandler.sendMessageDelayed(msg, SERVICE_START_FOREGROUND_TIMEOUT);
        }
        // How long the startForegroundService() grace period is to get around to
        // calling startForeground() before we ANR + stop it.
        static final int SERVICE_START_FOREGROUND_TIMEOUT = 5*1000;

    这种timeout流程就很熟悉了。

    修改方案

    有上面可知,问题原因主要是后台启动了服务,在这部分Android O/GO做了限制,
    根据章节2.4的过滤条件可以提供如下修改方案:
    1) 提升应用优先级到常驻内存级别 (不建议应用采纳这种方式,会导致手机出现很多性能问题)
    => 在AndroidManifest.xml添加android:persistent=”true”
    并且签上系统签名
    2) 类似与蓝牙BLUETOOTH_UID一样放在白名单里面(需要拥有源码修改权限,而且修改了源码,不利于apk的版本兼容,不建议采纳)
    3) 添加在电源相关的DeviceIdle白名单(不建议添加,可能导致功耗增加)

    按照上面的都说是不建议采取,是否没有办法了呢?

    我们继续往源头找找看看是否有办法:
    在ContextImpl的startServiceCommon、ActiveServices的startServiceLocked有一个参数requireForeground/fgRequired,是否前台请求,如果requireForeground/fgRequired为false才会进行后台请求判断,如果是true的话,是可以直接绕过去的

    回到ActiveServices的startServiceLocked=>

      ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
                int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
                throws TransactionTooLargeException {
            //...
            // fgRequired是true可以直接绕过
            if (!r.startRequested && !fgRequired) {
                //..
            }
            //...
        }

    那么方案4,我们可以采取如下方式
    4) 通过Context(activity、service的this都是包含context的,故不用担心调用方式),将之前的startService,修改成ContextImpl的startForegroundService或者startForegroundServiceAsUser方法,启动一个前台服务。
    //frameworks/base/core/java/android/app/ContextImpl.java

        @Override
        public ComponentName startForegroundService(Intent service) {
            warnIfCallingFromSystemProcess();
            return startServiceCommon(service, true, mUser);
        }
    
        @Override
        public ComponentName startForegroundServiceAsUser(Intent service, UserHandle user) {
            return startServiceCommon(service, true, user);
        }

    ps:注意上面的方法是启动前台服务,你的服务需要是前台的,这个怎么做呢,下面提供2种方法:
    1) 在service中调用startForeground (最常见方法)
    2) 设置service为前台,可以使用AMS的setProcessImportant设置优先级别 (优点是:不会在通知栏中出现通知图标。缺点是:需要相应的权限)

    解决办法

    https://www.cnblogs.com/mingfeng002/p/9647720.html

  • 相关阅读:
    Ext JS学习第三天 我们所熟悉的javascript(二)
    Ext JS学习第二天 我们所熟悉的javascript(一)
    Ext JS学习第十七天 事件机制event(二)
    Ext JS学习第十六天 事件机制event(一)
    Ext JS学习第十五天 Ext基础之 Ext.DomQuery
    Ext JS学习第十四天 Ext基础之 Ext.DomHelper
    Ext JS学习第十三天 Ext基础之 Ext.Element
    Ext JS学习第十天 Ext基础之 扩展原生的javascript对象(二)
    针对错误 “服务器提交了协议冲突. Section=ResponseHeader Detail=CR 后面必须是 LF” 的原因分析
    C# 使用HttpWebRequest通过PHP接口 上传文件
  • 原文地址:https://www.cnblogs.com/mingfeng002/p/10725364.html
Copyright © 2011-2022 走看看