IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 10 低内存应用白名单和应用保活 -> 正文阅读

[移动开发]Android 10 低内存应用白名单和应用保活

功能:1、实现低内存时候不杀应用 2、应用保活
代码路径:
frameworks/base/service/score/java/com/android/server/am/ActivityManagerServiceEx.java
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
frameworks/base/core/java/android/app/ProcessProtection.java
frameworks/base/services/core/java/com/android/server/am/ProcessList.java
frameworks/base//core/res/res/values/config_ex.xml
frameworks/base/services/core/java/com/android/server/am/LmKillerTracker.java
一、应用保活
应用保活:1、保持进程不被系统杀死 2、进程被系统杀死之后,可以重新复活。
应用保活原理:提高应用的oom_adj值,这样应用不管切换到后台的时候,都让该进程保持前台进程,这样就不容易被系统杀死,如果adj的值越大就越容易被系统杀死。
如何提高adj值就可以实现应用保活
在ActivityManagerServiceEx.java文件中构造函数中提高adj值,其中该updateOomAdjLocked()方法就是更新adj值

static HashMap<String, ProtectArea> sPreProtectAreaList;
static {
    sPreProtectAreaList = new HashMap<String, ProtectArea>();
}
public ActivityManagerServiceEx(Context systemContext) {
    super(systemContext);

    mIsInHome = true;
    mExHandler = new ExHandler((Looper) mHandler.getLooper());
    addProtectArea("包名", new ProtectArea(0, 0, ProcessProtection.PROCESS_STATUS_RUNNING));
}

public void addProtectArea(final String processName, final ProtectArea area) {
    if(processName == null || area == null) {
        return;
    }
    if (DEBUG_AMSEX) Slog.d(TAG, "addProtectArea, processName: " + processName
            + " ProtectArea: " + area);
    mHandler.post(new Runnable() {

        @Override
        public void run() {
            synchronized (ActivityManagerServiceEx.this) {
                sPreProtectAreaList.put(processName, area);
                updateOomAdjLocked();
            }
        }
    });
}
}

在ActivityManagerService.java文件中开启进程会调用startProcess()方法,然后再次执行startProcessLocked()该方法

@Override
public void startProcess(String processName, ApplicationInfo info,
        boolean knownToBeDead, String hostingType, ComponentName hostingName) {
    try {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "startProcess:"
                    + processName);
        }
        synchronized (ActivityManagerService.this) {
            startProcessLocked(processName, info, knownToBeDead, 0 /* intentFlags */,
                    new HostingRecord(hostingType, hostingName),
                    false /* allowWhileBooting */, false /* isolated */,
                    true /* keepIfLarge */);
        }
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    }
}

在ProcessList.java文件中startProcessLocked()方法中更新ProcessRecord 中的protectStatus 、 protectMinAdj 、protectMaxAdj 三个值

boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
        boolean disableHiddenApiChecks, boolean mountExtStorageFull,
        String abiOverride) {
    if (app.pendingStart) {
        return true;
    }
    long startTime = SystemClock.elapsedRealtime();
    if (app.pid > 0 && app.pid != ActivityManagerService.MY_PID) {
        checkSlow(startTime, "startProcess: removing from pids map");
        mService.mPidsSelfLocked.remove(app);
        mService.mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);
        checkSlow(startTime, "startProcess: done removing from pids map");
        app.setPid(0);
        app.startSeq = 0;
    }

    if (DEBUG_PROCESSES && mService.mProcessesOnHold.contains(app)) Slog.v(
            TAG_PROCESSES,
            "startProcessLocked removing on hold: " + app);
    mService.mProcessesOnHold.remove(app);

    checkSlow(startTime, "startProcess: starting to update cpu stats");
    mService.updateCpuStats();
    checkSlow(startTime, "startProcess: done updating cpu stats");

    try {
        try {
            final int userId = UserHandle.getUserId(app.uid);
            AppGlobals.getPackageManager().checkPackageStartable(app.info.packageName, userId);
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }

        int uid = app.uid;
        int[] gids = null;
        int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;
        if (!app.isolated) {
            int[] permGids = null;
            try {
                checkSlow(startTime, "startProcess: getting gids from package manager");
                final IPackageManager pm = AppGlobals.getPackageManager();
                permGids = pm.getPackageGids(app.info.packageName,
                        MATCH_DIRECT_BOOT_AUTO, app.userId);
                if (StorageManager.hasIsolatedStorage() && mountExtStorageFull) {
                    mountExternal = Zygote.MOUNT_EXTERNAL_FULL;
                } else {
                    StorageManagerInternal storageManagerInternal = LocalServices.getService(
                            StorageManagerInternal.class);
                    mountExternal = storageManagerInternal.getExternalStorageMountMode(uid,
                            app.info.packageName);
                }
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            }

            /*
             * Add shared application and profile GIDs so applications can share some
             * resources like shared libraries and access user-wide resources
             */
            if (ArrayUtils.isEmpty(permGids)) {
                gids = new int[3];
            } else {
                gids = new int[permGids.length + 3];
                System.arraycopy(permGids, 0, gids, 3, permGids.length);
            }
            gids[0] = UserHandle.getSharedAppGid(UserHandle.getAppId(uid));
            gids[1] = UserHandle.getCacheAppGid(UserHandle.getAppId(uid));
            gids[2] = UserHandle.getUserGid(UserHandle.getUserId(uid));

            // Replace any invalid GIDs
            if (gids[0] == UserHandle.ERR_GID) gids[0] = gids[2];
            if (gids[1] == UserHandle.ERR_GID) gids[1] = gids[2];
        }
        app.mountMode = mountExternal;
        checkSlow(startTime, "startProcess: building args");
        if (mService.mAtmInternal.isFactoryTestProcess(app.getWindowProcessController())) {
            uid = 0;
        }
        int runtimeFlags = 0;
        if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
            runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;
            runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;
            // Also turn on CheckJNI for debuggable apps. It's quite
            // awkward to turn on otherwise.
            runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;

            // Check if the developer does not want ART verification
            if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(),
                    android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {
                runtimeFlags |= Zygote.DISABLE_VERIFIER;
                Slog.w(TAG_PROCESSES, app + ": ART verification disabled");
            }
        }
        // Run the app in safe mode if its manifest requests so or the
        // system is booted in safe mode.
        if ((app.info.flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0 ||
                mService.mSafeMode == true) {
            runtimeFlags |= Zygote.DEBUG_ENABLE_SAFEMODE;
        }
        if ((app.info.privateFlags & ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL) != 0) {
            runtimeFlags |= Zygote.PROFILE_FROM_SHELL;
        }
        if ("1".equals(SystemProperties.get("debug.checkjni"))) {
            runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;
        }
        String genDebugInfoProperty = SystemProperties.get("debug.generate-debug-info");
        if ("1".equals(genDebugInfoProperty) || "true".equals(genDebugInfoProperty)) {
            runtimeFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO;
        }
        String genMiniDebugInfoProperty = SystemProperties.get("dalvik.vm.minidebuginfo");
        if ("1".equals(genMiniDebugInfoProperty) || "true".equals(genMiniDebugInfoProperty)) {
            runtimeFlags |= Zygote.DEBUG_GENERATE_MINI_DEBUG_INFO;
        }
        if ("1".equals(SystemProperties.get("debug.jni.logging"))) {
            runtimeFlags |= Zygote.DEBUG_ENABLE_JNI_LOGGING;
        }
        if ("1".equals(SystemProperties.get("debug.assert"))) {
            runtimeFlags |= Zygote.DEBUG_ENABLE_ASSERT;
        }
        if (mService.mNativeDebuggingApp != null
                && mService.mNativeDebuggingApp.equals(app.processName)) {
            // Enable all debug flags required by the native debugger.
            runtimeFlags |= Zygote.DEBUG_ALWAYS_JIT;          // Don't interpret anything
            runtimeFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO; // Generate debug info
            runtimeFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE;   // Disbale optimizations
            mService.mNativeDebuggingApp = null;
        }

        if (app.info.isEmbeddedDexUsed()
                || (app.info.isPrivilegedApp()
                    && DexManager.isPackageSelectedToRunOob(app.pkgList.mPkgList.keySet()))) {
            runtimeFlags |= Zygote.ONLY_USE_SYSTEM_OAT_FILES;
        }

        if (!disableHiddenApiChecks && !mService.mHiddenApiBlacklist.isDisabled()) {
            app.info.maybeUpdateHiddenApiEnforcementPolicy(
                    mService.mHiddenApiBlacklist.getPolicy());
            @ApplicationInfo.HiddenApiEnforcementPolicy int policy =
                    app.info.getHiddenApiEnforcementPolicy();
            int policyBits = (policy << Zygote.API_ENFORCEMENT_POLICY_SHIFT);
            if ((policyBits & Zygote.API_ENFORCEMENT_POLICY_MASK) != policyBits) {
                throw new IllegalStateException("Invalid API policy: " + policy);
            }
            runtimeFlags |= policyBits;
        }

        String useAppImageCache = SystemProperties.get(
                PROPERTY_USE_APP_IMAGE_STARTUP_CACHE, "");
        // Property defaults to true currently.
        if (!TextUtils.isEmpty(useAppImageCache) && !useAppImageCache.equals("false")) {
            runtimeFlags |= Zygote.USE_APP_IMAGE_STARTUP_CACHE;
        }

        String invokeWith = null;
        if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
            // Debuggable apps may include a wrapper script with their library directory.
            String wrapperFileName = app.info.nativeLibraryDir + "/wrap.sh";
            StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
            try {
                if (new File(wrapperFileName).exists()) {
                    invokeWith = "/system/bin/logwrapper " + wrapperFileName;
                }
            } finally {
                StrictMode.setThreadPolicy(oldPolicy);
            }
        }

        String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;
        if (requiredAbi == null) {
            requiredAbi = Build.SUPPORTED_ABIS[0];
        }

        String instructionSet = null;
        if (app.info.primaryCpuAbi != null) {
            instructionSet = VMRuntime.getInstructionSet(app.info.primaryCpuAbi);
        }

        app.gids = gids;
        app.setRequiredAbi(requiredAbi);
        app.instructionSet = instructionSet;

        // the per-user SELinux context must be set
        if (TextUtils.isEmpty(app.info.seInfoUser)) {
            Slog.wtf(ActivityManagerService.TAG, "SELinux tag not defined",
                    new IllegalStateException("SELinux tag not defined for "
                            + app.info.packageName + " (uid " + app.uid + ")"));
        }
        final String seInfo = app.info.seInfo
                + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser);
        // Start the process.  It will either succeed and return a result containing
        // the PID of the new process, or else throw a RuntimeException.
        final String entryPoint = "android.app.ActivityThread";

        boolean startSuccess =  startProcessLocked(hostingRecord, entryPoint,
                    app, uid, gids, runtimeFlags, mountExternal, seInfo, requiredAbi,
                    instructionSet, invokeWith, startTime);

        if (app.processName != null) {
            ProtectArea pa = sPreProtectAreaList.get(app.processName);
            if (pa != null) {
                //这里就是更新adj
                app.protectStatus = pa.mLevel;
                app.protectMinAdj = pa.mMinAdj;
                app.protectMaxAdj = pa.mMaxAdj;
            }
            if (DEBUG_AMSEX) {
                Slog.d(TAG, "startProcessLocked app.protectLevel :"
                        + app.protectStatus + " app.protectMinAdj: " + app.protectMinAdj
                        + " app.protectMaxAdj: " + app.protectMaxAdj);
            }
        }

        return startSuccess;
    } catch (RuntimeException e) {
        Slog.e(ActivityManagerService.TAG, "Failure starting process " + app.processName, e);

        // Something went very wrong while trying to start this process; one
        // common case is when the package is frozen due to an active
        // upgrade. To recover, clean up any active bookkeeping related to
        // starting this process. (We already invoked this method once when
        // the package was initially frozen through KILL_APPLICATION_MSG, so
        // it doesn't hurt to use it again.)
        mService.forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid),
                false, false, true, false, false, app.userId, "start failure");
        return false;
    }
}

备注:cat /proc/{PID}/oom_adj 的值变成0就说明修改成功了
二、低内存应用添加白名单
在LmKillerTracker.java文件中如果系统内存不足的时候,这个线程通过Socket读取进程ID,然后通过doLmkForceStop()方法中shouldForceStop()来过滤白名单的应用,如果应用在白名单中就不杀死应用,最后杀死应用是通过forceStopPackage方法来杀掉进程的

public class LmKillerTracker extends Thread {
    private final static String TAG = "LmKillerTracker";
    private static final String LMK_TRACKER_SOCKET = "lmfs";
    private ActivityManagerServiceEx mService;
    private ActivityTaskManagerService mAtm;
    private boolean mConnected;
    private List<String> mLmKillerBypassPackages = new ArrayList<String>();

    public LmKillerTracker() {
        mService = (ActivityManagerServiceEx) ActivityManager.getService();
        mAtm = mService.mActivityTaskManager;
        String[] lmKillerTrackerWhitelist = Resources.getSystem().getStringArray(
                com.android.internal.R.array.low_memory_killer_tracker_whitelist);
        mLmKillerBypassPackages = Arrays.asList(lmKillerTrackerWhitelist);
    }

    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
        int retryCount = 0;
        LocalSocket lmkillerSocket = null;
        mConnected = false;
        try {
            while(true) {
                LocalSocket s = null;
                LocalSocketAddress lSAddr;
                try {
                    s = new LocalSocket();
                    lSAddr = new LocalSocketAddress(LMK_TRACKER_SOCKET,
                            LocalSocketAddress.Namespace.RESERVED);
                    s.connect(lSAddr);
                } catch (IOException e) {
                    mConnected = false;
                    try {
                        if (s != null) {
                            s.close();
                        }
                    } catch (Exception e2) {
                    }

                    if (retryCount == 8) {
                        Slog.e(TAG, "can't find lmkiller socket after "
                                + retryCount + " retry, abort LmkTracker");
                        return;
                    } else if ( retryCount >= 0 && retryCount < 8) {
                        Slog.d(TAG, "retrying " + retryCount);
                    }

                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException er) {
                    }
                    retryCount++;
                    continue;
                }
                retryCount = 0;
                lmkillerSocket = s;
                Slog.i(TAG, "connected to lmkiller");
                mConnected = true;
                try {
                    InputStream is = lmkillerSocket.getInputStream();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
                    while (true) {
                        String line = reader.readLine();
                        try {
                            int pid = Integer.parseInt(line);
                            doLmkForceStop(pid);
                        } catch (Exception e) {
                            Slog.e(TAG, "doLmkForceStop encounter exception, read line "
                                    + line, e);
                        }
                    }
                } catch (Exception e) {
                    Slog.e(TAG, "caugth exception, closing lmk tracker", e);
                }

                try{
                    if (lmkillerSocket != null) lmkillerSocket.close();
                } catch (IOException e) {
                }
            }
        } catch (Exception e) {
        }
    }

    public void doLmkForceStop(int pid) {
        Slog.d(TAG, "doLmkForceStop pid=" + pid);
        if (pid <= 0) {
            return;
        }
        String pkgName = null;
        ProcessRecord pr = null;
        ProcessRecord parent = null;
        try {
            synchronized (mService.mPidsSelfLocked) {
                pr = mService.mPidsSelfLocked.get(pid);
            }

            if (pr == null) return;
            if (pr.info != null) {
                if (TextUtils.isEmpty(pr.processName)) return;
                if (pr.processName.indexOf(":") > 0) {
                    synchronized (mService) {
                        parent = mService.getProcessRecordLocked(
                                pr.info.packageName, pr.uid, true);
                    }
                }
                pkgName = pr.info.packageName;
            }
        } catch (Exception e) {
            Slog.w(TAG, "get process record from ams failed", e);
        }

        if (pkgName == null) return;

        // Found nothing, no processes found, abort directly.
        if (pr == null && parent == null) return;

        // Current process record can not be stopped.
        if (pr != null && !shouldForceStop(pr)) return;

        // related process record can not be stopped?
        boolean main = pkgName.equals(pr.processName);
        if (main) {
            //parent die, walk thougth to see if we can stop all childs:
            Slog.i(TAG, pr.processName+"die, see childs:");
            ArrayList <ProcessRecord> list = getRelatedProcesses(pkgName);
            boolean shallStop = true;
            for (ProcessRecord process : list) {
                if (!shouldForceStop(process)) {
                    shallStop = false;
                    break;
                }
            }
            if(!shallStop) {
                return;
            }
        } else {
            // child die , we shall not stop parent.
            if (parent != null) return;
        }

        mService.forceStopPackage(pkgName, UserHandle.USER_CURRENT);
        Slog.i(TAG, "force stop pkg:" + pkgName + ", pid:" + pid
                + " (adj " + pr.setAdj + ") has done.");
    }

    public boolean isConnected() {
        return mConnected;
    }

    private ArrayList<ProcessRecord> getRelatedProcesses(String pkgName) {
        ArrayList<ProcessRecord> list = new ArrayList<>();
        synchronized (mService.mPidsSelfLocked) {
            final int size = mService.mPidsSelfLocked.size();
            for (int i = 0; i < size; i++) {
                final int pid =mService.mPidsSelfLocked.keyAt(i);
                final ProcessRecord process = mService.mPidsSelfLocked.valueAt(i);
                if (process == null) continue;
                if (process.info != null && pkgName.equals(process.info.packageName)
                    && !process.processName.equals(pkgName)) {
                    Slog.i(TAG, "get related "+process.processName+",adj:"+process.setAdj+"for package"+pkgName);
                    list.add(process);
                }
            }
        }
        return list;
    }

    private boolean isSystemApp(ProcessRecord pr) {
        if (pr.info.isPrivilegedApp() || pr.info.isSystemApp()
                || pr.info.isUpdatedSystemApp()) {
            return true;
        }
        return false;
    }

    private boolean isEnabledInputMethod(String pkgName){
        if (pkgName == null) return false;
        IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
        IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
        List<InputMethodInfo> inputMethods;
        try {
            inputMethods = service.getEnabledInputMethodList(UserHandle.myUserId());
        } catch (RemoteException e) {
            return false;
        }
        if (inputMethods == null || inputMethods.size() == 0) return false;
        for (InputMethodInfo info : inputMethods){
            if (info == null || info.getPackageName() == null) continue;
            if (info.getPackageName().equals(pkgName)) return true;
        }
        return false;
    }

    private boolean isInstrumentedApp(ProcessRecord pr) {
        if (pr.getActiveInstrumentation() != null) return true;
        return false;
    }

    private boolean isInWhitelist(ProcessRecord pr) {
        String pkgName = pr.info.packageName;

        for (String token : mLmKillerBypassPackages) {
            if (pkgName.startsWith(token)) {
                return true;
            }
        }
        return false;
    }

    private boolean shouldForceStop(ProcessRecord pr) {
        String pkgName = pr.info.packageName;
        WindowProcessController windowProcessController = pr.getWindowProcessController();
        int pid = pr.pid;
        if (mAtm.hasActivityInTopTask(windowProcessController)) {
            Slog.i(TAG, "Skipped process pkgName:" + pkgName + ", pid:" + pid +
                    ", has activity in top task !");
            return false;
        } else if (mAtm.hasRelativeToptaskPackageInProcess(windowProcessController)) {
            Slog.i(TAG, "Skipped process pkgName:" + pkgName + ", pid:" + pid +
                    ", has relative top task package !");
            return false;
        } else if (isSystemApp(pr)) {
            Slog.i(TAG, "Skipped process pkgName:" + pkgName + ", pid:" + pid +
                    ", is system app !");
            return false;
        } else if (isInstrumentedApp(pr)) {
            Slog.i(TAG, "Skipped process pkgName:" + pkgName + ", pid:" + pid +
                    ", the application being instrumented !");
            return false;
        } else if (isEnabledInputMethod(pkgName)) {
            Slog.i(TAG, "Skipped process pkgName:" + pkgName + ", pid:" + pid +
                    ", the application is EnabledInputMethod !");
            return false;
        } else if (isInWhitelist(pr)) {
            Slog.i(TAG, "Skipped process pkgName:" + pkgName + ", pid:" + pid +
                    ", the application is in whitelist !");
            return false;
        } else if (mService.isProcessHeldWakeLock(pr)) {
            Slog.i(TAG, "Skipped process pkgName:" + pkgName + ", pid:" + pid +
                    ", the application is held wake lock !");
            return false;
        }
        return true;
    }

}
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-22 18:47:44  更:2022-04-22 18:48:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 22:03:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码